From 7663a498d0bc899914f6320a32c1e2969bdd9339 Mon Sep 17 00:00:00 2001 From: DE YU Date: Fri, 30 Jan 2026 16:36:29 +0800 Subject: [PATCH] Add support for multiple versions --- package.json | 8 +- src/debugger/godot3/server_controller.ts | 7 +- src/debugger/godot4/server_controller.ts | 7 +- src/extension.ts | 11 +- src/lsp/ClientConnectionManager.ts | 7 +- src/utils/godot_utils.ts | 177 ++++++++++++++++++++++- 6 files changed, 196 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 3e0115fcc..05039b79a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/debugger/godot3/server_controller.ts b/src/debugger/godot3/server_controller.ts index 109c8deb1..e679b8e37 100644 --- a/src/debugger/godot3/server_controller.ts +++ b/src/debugger/godot3/server_controller.ts @@ -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"; @@ -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}"`); diff --git a/src/debugger/godot4/server_controller.ts b/src/debugger/godot4/server_controller.ts index 144afbb70..bcba0145c 100644 --- a/src/debugger/godot4/server_controller.ts +++ b/src/debugger/godot4/server_controller.ts @@ -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"; @@ -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}"`); diff --git a/src/extension.ts b/src/extension.ts index 901c0c40e..64d9c098d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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"; @@ -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) { @@ -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) { @@ -241,7 +242,11 @@ async function get_godot_path(): Promise { 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 { diff --git a/src/lsp/ClientConnectionManager.ts b/src/lsp/ClientConnectionManager.ts index 210019ee8..a2e288b24 100644 --- a/src/lsp/ClientConnectionManager.ts +++ b/src/lsp/ClientConnectionManager.ts @@ -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"; @@ -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": { diff --git a/src/utils/godot_utils.ts b/src/utils/godot_utils.ts index fde49ca36..2569c1d50 100644 --- a/src/utils/godot_utils.ts +++ b/src/utils/godot_utils.ts @@ -63,6 +63,7 @@ export async function get_project_file(): Promise { } let projectVersion: string | undefined = undefined; +let projectUsesCSharp: boolean | undefined = undefined; export async function get_project_version(): Promise { if (projectVersion) { @@ -84,16 +85,29 @@ export async function get_project_version(): Promise { 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 { + 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, @@ -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 { + 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 { + 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 { + 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);