Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -270,14 +270,14 @@
"description": "Whether to display the minimap for the Godot documentation viewer."
},
"godotTools.editorPath.godot3": {
"type": "string",
"type": ["string", "array"],
"default": "godot3",
"description": "Path to the Godot 3 editor executable. Supports environment variables using '${env:VAR_NAME}'."
"description": "Path to the Godot 3 editor executable. Can be a single path (string) or an array of paths. When an array is provided, the extension will automatically select the first compatible executable based on the project version and features (C# support). Supports environment variables using '${env:VAR_NAME}'."
},
"godotTools.editorPath.godot4": {
"type": "string",
"type": ["string", "array"],
"default": "godot",
"description": "Path to the Godot 4 editor executable. Supports environment variables using '${env:VAR_NAME}'."
"description": "Path to the Godot 4 editor executable. Can be a single path (string) or an array of paths. When an array is provided, the extension will automatically select the first compatible executable based on the project version and features (C# support). Supports environment variables using '${env:VAR_NAME}'."
},
"godotTools.editor.verbose": {
"type": "boolean",
Expand Down
7 changes: 3 additions & 4 deletions src/debugger/godot3/server_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
get_project_version,
verify_godot_version,
VERIFY_RESULT,
get_godot_executable_for_project,
} from "../../utils";
import { prompt_for_godot_executable } from "../../utils/prompts";
import { killSubProcesses, subProcess } from "../../utils/subspawn";
Expand Down Expand Up @@ -144,10 +145,8 @@ export class ServerController {
log.info("Using 'editorPath.godot3' from settings");

const settingName = "editorPath.godot3";
godotPath = get_configuration(settingName);

log.info(`Verifying version of '${godotPath}'`);
result = verify_godot_version(godotPath, "3");
log.info(`Finding compatible Godot executable from settings`);
result = await get_godot_executable_for_project(settingName);
godotPath = result.godotPath;
log.info(`Verification result: ${result.status}, version: "${result.version}"`);

Expand Down
7 changes: 3 additions & 4 deletions src/debugger/godot4/server_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
get_free_port,
get_project_version,
verify_godot_version,
get_godot_executable_for_project,
} from "../../utils";
import { prompt_for_godot_executable } from "../../utils/prompts";
import { killSubProcesses, subProcess } from "../../utils/subspawn";
Expand Down Expand Up @@ -186,10 +187,8 @@ export class ServerController {
log.info("Using 'editorPath.godot4' from settings");

const settingName = "editorPath.godot4";
godotPath = get_configuration(settingName);

log.info(`Verifying version of '${godotPath}'`);
result = verify_godot_version(godotPath, "4");
log.info(`Finding compatible Godot executable from settings`);
result = await get_godot_executable_for_project(settingName);
godotPath = result.godotPath;
log.info(`Verification result: ${result.status}, version: "${result.version}"`);

Expand Down
11 changes: 8 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
get_project_version,
verify_godot_version,
convert_uri_to_resource_path,
get_godot_executable_for_project,
} from "./utils";
import { prompt_for_godot_executable } from "./utils/prompts";
import { killSubProcesses, subProcess } from "./utils/subspawn";
Expand Down Expand Up @@ -93,7 +94,7 @@ async function initial_setup() {
return;
}
const settingName = `editorPath.godot${projectVersion[0]}`;
const result = verify_godot_version(get_configuration(settingName), projectVersion[0]);
const result = await get_godot_executable_for_project(settingName);
const godotPath = result.godotPath;

switch (result.status) {
Expand Down Expand Up @@ -157,7 +158,7 @@ async function open_workspace_with_editor() {
const projectVersion = await get_project_version();

const settingName = `editorPath.godot${projectVersion[0]}`;
const result = verify_godot_version(get_configuration(settingName), projectVersion[0]);
const result = await get_godot_executable_for_project(settingName);
const godotPath = result.godotPath;

switch (result.status) {
Expand Down Expand Up @@ -241,7 +242,11 @@ async function get_godot_path(): Promise<string | undefined> {
return undefined;
}
const settingName = `editorPath.godot${projectVersion[0]}`;
return clean_godot_path(get_configuration(settingName));
const result = await get_godot_executable_for_project(settingName);
if (result.status === "SUCCESS") {
return clean_godot_path(result.godotPath);
}
return undefined;
}

class GodotEditorTerminal implements vscode.Pseudoterminal {
Expand Down
7 changes: 4 additions & 3 deletions src/lsp/ClientConnectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
set_configuration,
set_context,
verify_godot_version,
get_godot_executable_for_project,
} from "../utils";
import { prompt_for_godot_executable, prompt_for_reload, select_godot_executable } from "../utils/prompts";
import { killSubProcesses, subProcess } from "../utils/subspawn";
Expand Down Expand Up @@ -116,10 +117,10 @@ export class ClientConnectionManager {
targetVersion = "4.2";
}
const settingName = `editorPath.godot${projectVersion[0]}`;
let godotPath = get_configuration(settingName);

const result = verify_godot_version(godotPath, projectVersion[0]);
godotPath = result.godotPath;
log.info(`Finding compatible Godot executable from settings`);
const result = await get_godot_executable_for_project(settingName);
let godotPath = result.godotPath;

switch (result.status) {
case "WRONG_VERSION": {
Expand Down
177 changes: 174 additions & 3 deletions src/utils/godot_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export async function get_project_file(): Promise<string | undefined> {
}

let projectVersion: string | undefined = undefined;
let projectUsesCSharp: boolean | undefined = undefined;

export async function get_project_version(): Promise<string | undefined> {
if (projectVersion) {
Expand All @@ -84,16 +85,29 @@ export async function get_project_version(): Promise<string | undefined> {
const match = text.match(/config\/features=PackedStringArray\((.*)\)/);
if (match) {
const line = match[0];
const version = line.match(/\"(4.[0-9]+)\"/);
if (version) {
godotVersion = version[1];
// Extract version number matching regex('\d+\.\d+')
const versionMatch = line.match(/"(\d+\.\d+)"/);
if (versionMatch) {
godotVersion = versionMatch[1];
}
// Check if C# feature is present
projectUsesCSharp = line.includes('"C#"');
}

projectVersion = godotVersion;
return projectVersion;
}

export async function get_project_uses_csharp(): Promise<boolean | undefined> {
if (projectUsesCSharp !== undefined) {
return projectUsesCSharp;
}

// Force parsing if not yet done
await get_project_version();
return projectUsesCSharp;
}

export function find_project_file(start: string, depth = 20) {
// TODO: rename this, it's actually more like "find_parent_project_file"
// This function appears to be fast enough, but if speed is ever an issue,
Expand Down Expand Up @@ -205,6 +219,163 @@ export type VERIFY_RESULT = {
version?: string;
};

export type COMPATIBLE_RESULT = {
status: "FOUND" | "NOT_FOUND";
godotPath: string | undefined;
version?: string;
isMono?: boolean;
};

export type GodotExecutableInfo = {
path: string;
version: string;
isMono: boolean;
};

/**
* Queries a Godot executable for its version and whether it's a Mono version.
* @param godotPath Path to the Godot executable
* @returns Object with version and isMono properties, or null if invalid
*/
export async function query_godot_executable(godotPath: string): Promise<GodotExecutableInfo | null> {
const target = clean_godot_path(godotPath);

try {
const output = execSync(`"${target}" --version`).toString().trim();

// Parse version: e.g., "4.3.stable.mono" or "4.3.stable"
const versionPattern = /^(([34])\.([0-9]+)(?:\.[0-9]+)?)/m;
const versionMatch = output.match(versionPattern);

if (!versionMatch) {
return null;
}

const version = versionMatch[1];

// Check if it's a mono version
const isMono = output.toLowerCase().includes("mono");

return {
path: target,
version: version,
isMono: isMono
};
} catch {
return null;
}
}

/**
* Finds a compatible Godot executable from an array of paths.
* Chooses the first executable that strictly matches the project requirements.
* @param executablePaths Array of paths to Godot executables
* @param projectVersion The required major version (e.g., "3" or "4")
* @param projectVersionMinor Optional minor version requirement (e.g., "4.3")
* @param requiresMono Whether the project requires a Mono version
* @returns COMPATIBLE_RESULT with the selected executable or NOT_FOUND status
*/
export async function find_compatible_godot_executable(
executablePaths: string[],
projectVersion: string,
projectVersionMinor?: string,
requiresMono?: boolean
): Promise<COMPATIBLE_RESULT> {
if (!executablePaths || executablePaths.length === 0) {
return { status: "NOT_FOUND", godotPath: undefined };
}

for (const godotPath of executablePaths) {
if (!godotPath || godotPath.trim() === "") {
continue;
}

const info = await query_godot_executable(godotPath);

if (!info) {
continue;
}

// Check major version matches
if (!info.version.startsWith(projectVersion + ".")) {
continue;
}

// Check minor version if specified (strictly compatible)
if (projectVersionMinor && info.version !== projectVersionMinor) {
continue;
}

// Check mono requirement if specified
if (requiresMono !== undefined && info.isMono !== requiresMono) {
continue;
}

// Found a compatible executable
return {
status: "FOUND",
godotPath: info.path,
version: info.version,
isMono: info.isMono
};
}

// No compatible executable found
return { status: "NOT_FOUND", godotPath: undefined };
}

/**
* Gets the appropriate Godot executable path for the current project.
* Supports both single path and array of paths configuration.
* @param settingName The configuration setting name (e.g., "editorPath.godot4")
* @returns VERIFY_RESULT with the verified executable path
*/
export async function get_godot_executable_for_project(settingName: string): Promise<VERIFY_RESULT> {
const EXTENSION_PREFIX = "godotTools";
const configValue = vscode.workspace.getConfiguration(EXTENSION_PREFIX).get(settingName, null);

// If no value configured
if (configValue === null || configValue === undefined) {
return { status: "INVALID_EXE", godotPath: "" };
}

// If it's an array, use the new find_compatible_godot_executable function
if (Array.isArray(configValue) && configValue.length > 0) {
const projectVersion = await get_project_version();
const projectUsesCSharp = await get_project_uses_csharp();

if (projectVersion === undefined) {
return { status: "INVALID_EXE", godotPath: "" };
}

const majorVersion = projectVersion[0];
const result = await find_compatible_godot_executable(
configValue,
majorVersion,
projectVersion,
projectUsesCSharp
);

if (result.status === "FOUND") {
return {
status: "SUCCESS",
godotPath: result.godotPath!,
version: result.version
};
} else {
return {
status: "WRONG_VERSION",
godotPath: configValue[0] || "",
version: projectVersion
};
}
}

// Single path (backward compatibility)
const singlePath = Array.isArray(configValue) ? configValue[0] : configValue;
return verify_godot_version(singlePath, projectVersion ? projectVersion[0] : "4");
}

export function verify_godot_version(godotPath: string, expectedVersion: "3" | "4" | string): VERIFY_RESULT {
let target = clean_godot_path(godotPath);

Expand Down
Loading