feat(languages): Add Godot / GDScript support (#73)

Adds GDScript, Godot resource and shader file types with tree-sitter highlighting, the GDScript language server over TCP (a headless Godot editor started on demand), "Godot Project" / current-scene run configurations, and debugging through Godot's debug adapter.

Breakpoints set through symlinked paths (/tmp, /var on macOS) were reported verified but never hit; the adapter now sends resolved paths and maps stack frames back to the paths the IDE opened. The duplicate exit event Godot sends after disconnect is ignored.

Fixes #68

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-27 15:31:25 +08:00
1 джерело 2eaa6cb0c7
коміт e3dae02de7
52 files changed
+1584 -33

No files matched your search

+11
Переглянути файл
@@ -197,4 +197,15 @@ describe("run config → launch config", () => {
// lldb-dap takes `KEY=VALUE` strings.
expect(extra(dap)).toEqual({ env: ["K=v"] });
});
it("Godot: the editor's adapter plays the main scene or the chosen one of the project", () => {
const project = rc({ kind: "godot", command: "/Apps/Godot", args: ["--path", "."], cwd: "game", debugAdapter: "godot" });
const main = launchConfigFor(project, "/p")!;
expect(main).toMatchObject({ adapter: "godot", request: "launch", cwd: "/p/game", program: null });
expect(extra(main)).toEqual({ project: "/p/game", scene: "main" });
const scene = launchConfigFor({ ...project, args: ["--path", ".", "res://levels/one.tscn"] }, "/p")!;
expect(extra(scene)).toEqual({ project: "/p/game", scene: "res://levels/one.tscn" });
const relative = launchConfigFor({ ...project, cwd: null, args: ["--path", "game", "menu.tscn"] }, "/p")!;
expect(extra(relative)).toEqual({ project: "/p/game", scene: "res://menu.tscn" });
});
});
+8
Переглянути файл
@@ -137,6 +137,14 @@ describe("remote attach requests", () => {
});
});
it("Attach to Godot connects to the editor's debug adapter for the project", () => {
const l = remoteAttachConfig(configOf(remote.godotAttachType), "/p/game")!;
expect(l).toMatchObject({ adapter: "godot", request: "attach", cwd: "/p/game" });
expect(JSON.parse(l.extraJson!)).toEqual({ debugServer: "localhost:6006", project: "/p/game" });
const custom = remoteAttachConfig(configOf(remote.godotAttachType, { port: "6010" }), "/p")!;
expect(JSON.parse(custom.extraJson!).debugServer).toBe("localhost:6010");
});
it("Attach to Node.js attaches js-debug to the inspector with local/remote roots", () => {
const l = remoteAttachConfig(configOf(nodeRemoteType, { host: "web", remoteRoot: "/usr/src/app" }), "/p")!;
expect(l.adapter).toBe("js-debug");
+19
Переглянути файл
@@ -196,6 +196,24 @@ function delveLaunch(rc: RunConfig, cwd: string): LaunchConfig {
return out;
}
/** The scene a `godot --path <dir> [scene]` command line plays: `main`, or a `res://` path. */
export function godotScene(args: readonly string[]): string {
const scene = args.find((a, i) => args[i - 1] !== "--path" && /\.t?scn$/i.test(a));
if (!scene) return "main";
return scene.startsWith("res://") ? scene : `res://${scene.replace(/^\.\//, "")}`;
}
/**
* Godot: the adapter inside the editor plays the scene itself (with the editor's run
* settings), so the launch names the project (checked against the editor's) and the scene.
*/
function godotLaunch(rc: RunConfig, cwd: string): LaunchConfig {
const i = rc.args.indexOf("--path");
const dir = i >= 0 ? rc.args[i + 1] : undefined;
const project = dir ? join(cwd, dir) : cwd;
return { ...base(rc, "godot", project), extraJson: JSON.stringify({ project, scene: godotScene(rc.args) }) };
}
/** A native program under CodeLLDB or lldb-dap: program, args, cwd, env. */
function nativeLaunch(rc: RunConfig, cwd: string, adapter: string): LaunchConfig {
const out: LaunchConfig = { ...base(rc, adapter, cwd), program: resolveCommand(rc.command, cwd), args: [...rc.args] };
@@ -219,6 +237,7 @@ export function launchConfigFor(rc: RunConfig, root: string): LaunchConfig | nul
if (adapter === "debugpy") return debugpyLaunch(rc, cwd);
if (adapter === "js-debug") return jsDebugLaunch(rc, cwd);
if (adapter === "delve") return delveLaunch(rc, cwd);
if (adapter === "godot") return godotLaunch(rc, cwd);
// Adapters without a template (hidden in Edit Configurations): generic arguments.
return {
...base(rc, adapter, cwd),
+28
Переглянути файл
@@ -12,6 +12,7 @@ export const REMOTE_JVM = "remote-jvm";
export const GO_REMOTE = "go-remote";
export const PYTHON_REMOTE = "python-remote";
export const NODE_REMOTE = "node-remote";
export const GODOT_ATTACH = "godot-attach";
type Params = Readonly<Record<string, string>>;
@@ -220,11 +221,36 @@ export const nodeRemoteType: RunConfigTypeContribution = {
commandLine: () => null,
};
// ---------------------------------------------------------------------------
// Attach to Godot (the editor's debug adapter, Editor Settings › Network › Debug Adapter)
export const godotAttachType: RunConfigTypeContribution = {
id: GODOT_ATTACH,
label: "Attach to Godot",
icon: "file-gdscript",
description: "Debug the game the Godot editor is running, through the editor's debug adapter",
defaults: { host: "localhost", port: "6006" },
debugOnly: true,
debugAdapter: "godot",
fields: [
hostField,
portField,
{
key: "hint",
label: "Before attaching",
control: "output",
value: () => "Open the project in the Godot editor and run a scene (F5 / F6)",
},
],
commandLine: () => null,
};
export const REMOTE_TYPES: readonly RunConfigTypeContribution[] = [
remoteJvmType,
goRemoteType,
pythonRemoteType,
nodeRemoteType,
godotAttachType,
];
export const isRemoteConfig = (c: Pick<RunConfig, "kind">) => REMOTE_TYPES.some((t) => t.id === c.kind);
@@ -291,6 +317,8 @@ export function remoteAttachConfig(rc: RunConfig, root: string): LaunchConfig |
justMyCode: skip,
...(map ? { pathMappings: [{ localRoot: map.local, remoteRoot: map.remote }] } : {}),
});
case GODOT_ATTACH:
return attachBase(rc, "godot", root, { debugServer: serverAddress(host, portOf(p, 6006)), project: root });
case NODE_REMOTE:
return attachBase(rc, "js-debug", root, {
type: "pwa-node",