breakpoint-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/bridge.js +189 -0
- package/dist/config.js +71 -0
- package/dist/confirm.js +45 -0
- package/dist/csdap.js +331 -0
- package/dist/cslsp.js +217 -0
- package/dist/dap.js +341 -0
- package/dist/framing.js +125 -0
- package/dist/index.js +110 -0
- package/dist/logger.js +11 -0
- package/dist/lsp.js +219 -0
- package/dist/paths.js +28 -0
- package/dist/schemas.js +587 -0
- package/dist/stdio.js +101 -0
- package/dist/subscriptions.js +141 -0
- package/dist/tasks.js +146 -0
- package/dist/tools/assetgen.js +360 -0
- package/dist/tools/backend.js +532 -0
- package/dist/tools/cli.js +130 -0
- package/dist/tools/csdap.js +377 -0
- package/dist/tools/cslsp.js +285 -0
- package/dist/tools/dap.js +504 -0
- package/dist/tools/editor.js +1919 -0
- package/dist/tools/knowledge.js +517 -0
- package/dist/tools/lsp-common.js +121 -0
- package/dist/tools/lsp.js +576 -0
- package/dist/tools/netcode.js +411 -0
- package/dist/tools/processes.js +103 -0
- package/dist/tools/resources.js +27 -0
- package/dist/tools/runtime.js +125 -0
- package/package.json +57 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { toFileUri, toFsPath, readFileText } from "../paths.js";
|
|
4
|
+
import { gate } from "../confirm.js";
|
|
5
|
+
import { COMPLETION_KIND, SYMBOL_KIND, ok, fail, markupToString, isMethodNotFound, normalizeLocations, applyTextEdits, } from "./lsp-common.js";
|
|
6
|
+
/**
|
|
7
|
+
* Returned by gd_workspace_symbols when the connected Godot build's GDScript
|
|
8
|
+
* language server has no `workspace/symbol` method. This is an engine limitation
|
|
9
|
+
* (observed through Godot 4.7, which replies -32601 Method not found), not a host
|
|
10
|
+
* fault, so the message is explicit and points at the working alternative rather
|
|
11
|
+
* than leaking a raw JSON-RPC error code.
|
|
12
|
+
*/
|
|
13
|
+
function unsupportedWorkspaceSymbols() {
|
|
14
|
+
return {
|
|
15
|
+
isError: true,
|
|
16
|
+
content: [{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: "gd_workspace_symbols is unsupported by the connected Godot build: its GDScript " +
|
|
19
|
+
"language server does not implement LSP 'workspace/symbol' (replies -32601 Method " +
|
|
20
|
+
"not found; observed through Godot 4.7). This is an engine limitation, not a host " +
|
|
21
|
+
"error. Use gd_document_symbols for a single file's symbols, or gd_definition / " +
|
|
22
|
+
"gd_references to navigate by a known name.",
|
|
23
|
+
}],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Returned by gd_code_action when the connected Godot build's GDScript language
|
|
28
|
+
* server doesn't offer code actions. Godot advertises `codeActionProvider: false`
|
|
29
|
+
* on current builds (confirmed in CI against 4.3-stable) and replies -32601 to a
|
|
30
|
+
* `textDocument/codeAction` request. Same graceful-degradation contract as
|
|
31
|
+
* gd_workspace_symbols: a clear message, not a raw JSON-RPC error.
|
|
32
|
+
*/
|
|
33
|
+
function unsupportedCodeAction() {
|
|
34
|
+
return {
|
|
35
|
+
isError: true,
|
|
36
|
+
content: [{
|
|
37
|
+
type: "text",
|
|
38
|
+
text: "gd_code_action is unsupported by the connected Godot build: its GDScript " +
|
|
39
|
+
"language server does not offer code actions (advertises codeActionProvider:false " +
|
|
40
|
+
"and replies -32601 Method not found; observed on Godot 4.3). This is an engine " +
|
|
41
|
+
"limitation, not a host error. Use gd_diagnostics to surface issues and " +
|
|
42
|
+
"gd_completion / gd_rename to make edits.",
|
|
43
|
+
}],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Generic graceful-degradation message for an optional LSP method the connected
|
|
48
|
+
* Godot build doesn't implement. The newer read-only providers below
|
|
49
|
+
* (documentHighlight, foldingRange, typeDefinition, implementation, declaration,
|
|
50
|
+
* documentLink, formatting) are all *advertised* by Godot 4.3's language server —
|
|
51
|
+
* but, as the D7 probe proved for workspace/symbol, advertised is not the same as
|
|
52
|
+
* implemented. Each tool feature-detects its capability AND catches a -32601 from
|
|
53
|
+
* a build that advertises the capability yet still answers "method not found",
|
|
54
|
+
* returning this clear message instead of leaking a raw JSON-RPC error.
|
|
55
|
+
*/
|
|
56
|
+
function unsupportedLsp(tool, method, capability, alt) {
|
|
57
|
+
return {
|
|
58
|
+
isError: true,
|
|
59
|
+
content: [{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: `${tool} is unsupported by the connected Godot build: its GDScript language server ` +
|
|
62
|
+
`does not implement LSP '${method}' (advertises no ${capability}, or replies -32601 ` +
|
|
63
|
+
`Method not found). This is an engine limitation, not a host error. ${alt}`,
|
|
64
|
+
}],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// textDocument/documentHighlight DocumentHighlightKind -> readable name.
|
|
68
|
+
const HIGHLIGHT_KIND = { 1: "text", 2: "read", 3: "write" };
|
|
69
|
+
function normalizeHighlights(result) {
|
|
70
|
+
const arr = Array.isArray(result) ? result : [];
|
|
71
|
+
return arr.map((h) => {
|
|
72
|
+
const hh = h;
|
|
73
|
+
const r = hh.range ?? {};
|
|
74
|
+
return {
|
|
75
|
+
line: r.start?.line ?? 0, character: r.start?.character ?? 0,
|
|
76
|
+
end_line: r.end?.line ?? 0, end_character: r.end?.character ?? 0,
|
|
77
|
+
kind: hh.kind ? HIGHLIGHT_KIND[hh.kind] ?? String(hh.kind) : "text",
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function normalizeFolding(result) {
|
|
82
|
+
const arr = Array.isArray(result) ? result : [];
|
|
83
|
+
return arr.map((f) => {
|
|
84
|
+
const ff = f;
|
|
85
|
+
return { start_line: ff.startLine ?? 0, end_line: ff.endLine ?? 0, kind: ff.kind ?? "" };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function normalizeLinks(result) {
|
|
89
|
+
const arr = Array.isArray(result) ? result : [];
|
|
90
|
+
return arr.map((l) => {
|
|
91
|
+
const ll = l;
|
|
92
|
+
const r = ll.range ?? {};
|
|
93
|
+
return {
|
|
94
|
+
line: r.start?.line ?? 0, character: r.start?.character ?? 0,
|
|
95
|
+
end_line: r.end?.line ?? 0, end_character: r.end?.character ?? 0,
|
|
96
|
+
target: ll.target ?? "",
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// textDocument/documentColor -> ColorInformation[]: each { range, color:{red,green,blue,alpha} }
|
|
101
|
+
// with every channel a float in 0..1. We surface the raw 0..1 components AND a
|
|
102
|
+
// convenience #RRGGBBAA hex (Godot's Color.to_html() ordering) so a caller can
|
|
103
|
+
// eyeball the swatch without re-deriving it.
|
|
104
|
+
function normalizeColors(result) {
|
|
105
|
+
const arr = Array.isArray(result) ? result : [];
|
|
106
|
+
const hex2 = (v) => Math.max(0, Math.min(255, Math.round((v ?? 0) * 255))).toString(16).padStart(2, "0");
|
|
107
|
+
return arr.map((c) => {
|
|
108
|
+
const cc = c;
|
|
109
|
+
const r = cc.range ?? {};
|
|
110
|
+
const col = cc.color ?? {};
|
|
111
|
+
const red = col.red ?? 0, green = col.green ?? 0, blue = col.blue ?? 0, alpha = col.alpha ?? 0;
|
|
112
|
+
return {
|
|
113
|
+
line: r.start?.line ?? 0, character: r.start?.character ?? 0,
|
|
114
|
+
end_line: r.end?.line ?? 0, end_character: r.end?.character ?? 0,
|
|
115
|
+
red, green, blue, alpha,
|
|
116
|
+
hex: `#${hex2(red)}${hex2(green)}${hex2(blue)}${hex2(alpha)}`,
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// offsetOf / applyTextEdits now live in ./lsp-common.js (shared with the C#
|
|
121
|
+
// rename mutator, cs_rename). Imported above.
|
|
122
|
+
export function registerLspTools(server, lsp, cfg) {
|
|
123
|
+
const openAndPos = async (path) => {
|
|
124
|
+
const uri = toFileUri(path, cfg.projectPath);
|
|
125
|
+
await lsp.ensureOpen(uri, readFileText(toFsPath(path, cfg.projectPath)));
|
|
126
|
+
return uri;
|
|
127
|
+
};
|
|
128
|
+
const posSchema = {
|
|
129
|
+
path: z.string().describe("Script path (res://..., absolute, or project-relative)"),
|
|
130
|
+
line: z.number().int().describe("0-based line"),
|
|
131
|
+
character: z.number().int().describe("0-based character"),
|
|
132
|
+
};
|
|
133
|
+
server.registerTool("gd_completion", { title: "GDScript completion", description: "Type-aware code completion at a position via the Godot language server.", inputSchema: posSchema }, async ({ path, line, character }) => {
|
|
134
|
+
try {
|
|
135
|
+
const uri = await openAndPos(path);
|
|
136
|
+
const result = await lsp.request("textDocument/completion", { textDocument: { uri }, position: { line, character } });
|
|
137
|
+
const raw = Array.isArray(result) ? result : (result?.items ?? []);
|
|
138
|
+
const items = raw.map((i) => {
|
|
139
|
+
const it = i;
|
|
140
|
+
return { label: it.label ?? "", kind: it.kind ? COMPLETION_KIND[it.kind] ?? String(it.kind) : "", detail: it.detail ?? "", insertText: it.insertText ?? it.label ?? "" };
|
|
141
|
+
});
|
|
142
|
+
return ok({ items });
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
return fail(err);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
server.registerTool("gd_hover", { title: "GDScript hover", description: "Hover documentation/type info at a position.", inputSchema: posSchema }, async ({ path, line, character }) => {
|
|
149
|
+
try {
|
|
150
|
+
const uri = await openAndPos(path);
|
|
151
|
+
const result = (await lsp.request("textDocument/hover", { textDocument: { uri }, position: { line, character } }));
|
|
152
|
+
let contents = "";
|
|
153
|
+
const c = result?.contents;
|
|
154
|
+
if (typeof c === "string")
|
|
155
|
+
contents = c;
|
|
156
|
+
else if (Array.isArray(c))
|
|
157
|
+
contents = c.map((x) => (typeof x === "string" ? x : x?.value ?? "")).join("\n");
|
|
158
|
+
else if (c && typeof c === "object")
|
|
159
|
+
contents = c.value ?? "";
|
|
160
|
+
return ok({ contents });
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
return fail(err);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
server.registerTool("gd_definition", { title: "GDScript go-to-definition", description: "Resolve the definition location(s) of the symbol at a position.", inputSchema: posSchema }, async ({ path, line, character }) => {
|
|
167
|
+
try {
|
|
168
|
+
const uri = await openAndPos(path);
|
|
169
|
+
const result = await lsp.request("textDocument/definition", { textDocument: { uri }, position: { line, character } });
|
|
170
|
+
return ok({ locations: normalizeLocations(result) });
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
return fail(err);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
server.registerTool("gd_references", {
|
|
177
|
+
title: "GDScript find-references",
|
|
178
|
+
description: "Find all references to the symbol at a position.",
|
|
179
|
+
inputSchema: { ...posSchema, include_declaration: z.boolean().optional().describe("Include the declaration (default true)") },
|
|
180
|
+
}, async ({ path, line, character, include_declaration }) => {
|
|
181
|
+
try {
|
|
182
|
+
const uri = await openAndPos(path);
|
|
183
|
+
const result = await lsp.request("textDocument/references", {
|
|
184
|
+
textDocument: { uri }, position: { line, character },
|
|
185
|
+
context: { includeDeclaration: include_declaration ?? true },
|
|
186
|
+
});
|
|
187
|
+
return ok({ locations: normalizeLocations(result) });
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
return fail(err);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
server.registerTool("gd_rename", {
|
|
194
|
+
title: "GDScript rename symbol",
|
|
195
|
+
description: "Rename a symbol project-wide. Returns the planned edit; pass apply=true to WRITE the changes to disk (DESTRUCTIVE — confirm with the user).",
|
|
196
|
+
inputSchema: {
|
|
197
|
+
...posSchema,
|
|
198
|
+
new_name: z.string().describe("New symbol name"),
|
|
199
|
+
apply: z.boolean().optional().describe("Write edits to disk (default false = dry run)"),
|
|
200
|
+
confirm: z.boolean().optional().describe("Auto-approve writing edits (skip the confirmation prompt); only relevant with apply=true"),
|
|
201
|
+
},
|
|
202
|
+
}, async ({ path, line, character, new_name, apply, confirm }) => {
|
|
203
|
+
try {
|
|
204
|
+
const uri = await openAndPos(path);
|
|
205
|
+
const edit = (await lsp.request("textDocument/rename", {
|
|
206
|
+
textDocument: { uri }, position: { line, character }, newName: new_name,
|
|
207
|
+
}));
|
|
208
|
+
const changes = edit?.changes ?? {};
|
|
209
|
+
const files = Object.keys(changes);
|
|
210
|
+
let editCount = 0;
|
|
211
|
+
for (const f of files)
|
|
212
|
+
editCount += changes[f].length;
|
|
213
|
+
let written = [];
|
|
214
|
+
if (apply) {
|
|
215
|
+
const blocked = await gate(server, confirm, `Rename to "${new_name}" — write ${editCount} edit(s) across ${files.length} file(s)`);
|
|
216
|
+
if (blocked)
|
|
217
|
+
return blocked;
|
|
218
|
+
for (const fileUri of files) {
|
|
219
|
+
const fsPath = decodeURIComponent(fileUri.replace(/^file:\/\//, ""));
|
|
220
|
+
const before = fs.readFileSync(fsPath, "utf8");
|
|
221
|
+
fs.writeFileSync(fsPath, applyTextEdits(before, changes[fileUri]), "utf8");
|
|
222
|
+
written.push(fsPath);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return ok({ changed_files: files, edit_count: editCount, applied: Boolean(apply), written });
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
return fail(err);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
server.registerTool("gd_document_symbols", {
|
|
232
|
+
title: "GDScript document symbols",
|
|
233
|
+
description: "List the symbols (classes, functions, variables, signals) declared in a script.",
|
|
234
|
+
inputSchema: { path: z.string().describe("Script path") },
|
|
235
|
+
}, async ({ path }) => {
|
|
236
|
+
try {
|
|
237
|
+
const uri = await openAndPos(path);
|
|
238
|
+
const result = (await lsp.request("textDocument/documentSymbol", { textDocument: { uri } }));
|
|
239
|
+
const symbols = (result ?? []).map((s) => {
|
|
240
|
+
const sym = s;
|
|
241
|
+
const range = sym.range ?? sym.selectionRange ?? sym.location?.range ?? {};
|
|
242
|
+
return { name: sym.name ?? "", kind: sym.kind ? SYMBOL_KIND[sym.kind] ?? String(sym.kind) : "", line: range.start?.line ?? 0 };
|
|
243
|
+
});
|
|
244
|
+
return ok({ symbols });
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
return fail(err);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
server.registerTool("gd_workspace_symbols", {
|
|
251
|
+
title: "GDScript workspace symbols",
|
|
252
|
+
description: "Search symbols across the whole project by name. Note: Godot's GDScript " +
|
|
253
|
+
"language server does not implement LSP 'workspace/symbol' (observed through " +
|
|
254
|
+
"4.7), so on those builds this returns a clear 'unsupported' error rather " +
|
|
255
|
+
"than results — use gd_document_symbols for per-file symbols.",
|
|
256
|
+
inputSchema: { query: z.string().describe("Symbol name query") },
|
|
257
|
+
}, async ({ query }) => {
|
|
258
|
+
try {
|
|
259
|
+
// Feature-detect before calling: if the server never advertised
|
|
260
|
+
// workspaceSymbolProvider, skip the request and return a clear message
|
|
261
|
+
// instead of provoking a raw -32601.
|
|
262
|
+
const caps = await lsp.getServerCapabilities();
|
|
263
|
+
if (!caps.workspaceSymbolProvider)
|
|
264
|
+
return unsupportedWorkspaceSymbols();
|
|
265
|
+
const result = (await lsp.request("workspace/symbol", { query }));
|
|
266
|
+
const symbols = (result ?? []).map((s) => {
|
|
267
|
+
const sym = s;
|
|
268
|
+
return {
|
|
269
|
+
name: sym.name ?? "",
|
|
270
|
+
kind: sym.kind ? SYMBOL_KIND[sym.kind] ?? String(sym.kind) : "",
|
|
271
|
+
uri: sym.location?.uri ?? "",
|
|
272
|
+
line: sym.location?.range?.start?.line ?? 0,
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
return ok({ symbols });
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
// Belt-and-suspenders: some builds advertise the capability but still
|
|
279
|
+
// answer -32601 (or the equivalent "method not found"). Treat that as the
|
|
280
|
+
// same engine limitation rather than an opaque protocol error.
|
|
281
|
+
const e = err;
|
|
282
|
+
if (e.code === -32601 || /method not found/i.test(e.message ?? "")) {
|
|
283
|
+
return unsupportedWorkspaceSymbols();
|
|
284
|
+
}
|
|
285
|
+
return fail(err);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
server.registerTool("gd_diagnostics", {
|
|
289
|
+
title: "GDScript diagnostics",
|
|
290
|
+
description: "Return compile/lint diagnostics (errors, warnings) for a script. Opens the file and waits briefly for the server to publish.",
|
|
291
|
+
inputSchema: {
|
|
292
|
+
path: z.string().describe("Script path"),
|
|
293
|
+
wait_ms: z.number().int().positive().optional().describe("Max time to wait for the first publish (default 1500)"),
|
|
294
|
+
},
|
|
295
|
+
}, async ({ path, wait_ms }) => {
|
|
296
|
+
try {
|
|
297
|
+
const uri = await openAndPos(path);
|
|
298
|
+
const diagnostics = await lsp.waitForDiagnostics(uri, wait_ms ?? 1500);
|
|
299
|
+
const named = diagnostics.map((d) => ({
|
|
300
|
+
severity: (["", "error", "warning", "info", "hint"][d.severity] ?? "error"),
|
|
301
|
+
message: d.message, line: d.line, character: d.character,
|
|
302
|
+
}));
|
|
303
|
+
return ok({ uri, diagnostics: named });
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
return fail(err);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
server.registerTool("gd_signature_help", {
|
|
310
|
+
title: "GDScript signature help",
|
|
311
|
+
description: "Show the call signature(s) and active parameter at a position (the parameter hints an IDE pops up inside a call). " +
|
|
312
|
+
"Godot's GDScript language server advertises signatureHelpProvider.",
|
|
313
|
+
inputSchema: posSchema,
|
|
314
|
+
}, async ({ path, line, character }) => {
|
|
315
|
+
try {
|
|
316
|
+
const uri = await openAndPos(path);
|
|
317
|
+
const result = (await lsp.request("textDocument/signatureHelp", { textDocument: { uri }, position: { line, character } }));
|
|
318
|
+
const signatures = (result?.signatures ?? []).map((s) => {
|
|
319
|
+
const sig = s;
|
|
320
|
+
const sigLabel = sig.label ?? "";
|
|
321
|
+
const parameters = (sig.parameters ?? []).map((p) => {
|
|
322
|
+
const par = p;
|
|
323
|
+
let plabel = "";
|
|
324
|
+
if (typeof par.label === "string")
|
|
325
|
+
plabel = par.label;
|
|
326
|
+
// Per LSP, a parameter label may be a [start,end] offset pair into the signature label.
|
|
327
|
+
else if (Array.isArray(par.label) && par.label.length === 2)
|
|
328
|
+
plabel = sigLabel.slice(Number(par.label[0]), Number(par.label[1]));
|
|
329
|
+
return { label: plabel, documentation: markupToString(par.documentation) };
|
|
330
|
+
});
|
|
331
|
+
return { label: sigLabel, documentation: markupToString(sig.documentation), parameters };
|
|
332
|
+
});
|
|
333
|
+
return ok({ signatures, active_signature: result?.activeSignature ?? 0, active_parameter: result?.activeParameter ?? 0 });
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
return fail(err);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
server.registerTool("gd_code_action", {
|
|
340
|
+
title: "GDScript code actions",
|
|
341
|
+
description: "List the code actions (quick fixes / refactors) the language server offers for a range — the lightbulb menu. " +
|
|
342
|
+
"Read-only: returns the available actions (title, kind, whether each carries a WorkspaceEdit or a Command) without applying any. " +
|
|
343
|
+
"end_line/end_character default to the start position (a caret, not a selection).",
|
|
344
|
+
inputSchema: {
|
|
345
|
+
path: z.string().describe("Script path (res://..., absolute, or project-relative)"),
|
|
346
|
+
start_line: z.number().int().describe("0-based start line"),
|
|
347
|
+
start_character: z.number().int().describe("0-based start character"),
|
|
348
|
+
end_line: z.number().int().optional().describe("0-based end line (default = start_line)"),
|
|
349
|
+
end_character: z.number().int().optional().describe("0-based end character (default = start_character)"),
|
|
350
|
+
only: z.array(z.string()).optional().describe("Restrict to these CodeActionKind prefixes, e.g. 'quickfix', 'refactor', 'source'"),
|
|
351
|
+
},
|
|
352
|
+
}, async ({ path, start_line, start_character, end_line, end_character, only }) => {
|
|
353
|
+
try {
|
|
354
|
+
// Feature-detect: Godot's GDScript LSP advertises codeActionProvider:false
|
|
355
|
+
// on current builds (confirmed in CI on 4.3-stable) and replies -32601 to
|
|
356
|
+
// the request. Skip the call and return a clear message rather than leaking
|
|
357
|
+
// a raw JSON-RPC error, mirroring gd_workspace_symbols.
|
|
358
|
+
const caps = await lsp.getServerCapabilities();
|
|
359
|
+
if (!caps.codeActionProvider)
|
|
360
|
+
return unsupportedCodeAction();
|
|
361
|
+
const uri = await openAndPos(path);
|
|
362
|
+
const range = {
|
|
363
|
+
start: { line: start_line, character: start_character },
|
|
364
|
+
end: { line: end_line ?? start_line, character: end_character ?? start_character },
|
|
365
|
+
};
|
|
366
|
+
const context = { diagnostics: [] };
|
|
367
|
+
if (only && only.length)
|
|
368
|
+
context.only = only;
|
|
369
|
+
const result = (await lsp.request("textDocument/codeAction", { textDocument: { uri }, range, context }));
|
|
370
|
+
const actions = (result ?? []).map((a) => {
|
|
371
|
+
const act = a;
|
|
372
|
+
// A bare Command has `command` as a string; a CodeAction nests a Command object under `command`.
|
|
373
|
+
const command = typeof act.command === "string" ? act.command : act.command?.command ?? null;
|
|
374
|
+
return { title: act.title ?? "", kind: act.kind ?? "", has_edit: act.edit !== undefined, command };
|
|
375
|
+
});
|
|
376
|
+
return ok({ actions });
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
// Belt-and-suspenders: a build that advertises the capability but still
|
|
380
|
+
// answers -32601 gets the same graceful "unsupported" treatment.
|
|
381
|
+
const e = err;
|
|
382
|
+
if (e.code === -32601 || /method not found/i.test(e.message ?? ""))
|
|
383
|
+
return unsupportedCodeAction();
|
|
384
|
+
return fail(err);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
// ---- Phase 1 LSP-depth: read-only navigation/inspection providers --------
|
|
388
|
+
// Godot 4.3's GDScript language server advertises documentHighlight, folding,
|
|
389
|
+
// typeDefinition, implementation, declaration, documentLink and formatting in
|
|
390
|
+
// its initialize capabilities. Each tool below feature-detects the capability
|
|
391
|
+
// and keeps a -32601 belt-and-suspenders (the D7 lesson: advertised ≠ honoured),
|
|
392
|
+
// returning a clear "unsupported" message rather than a raw JSON-RPC error.
|
|
393
|
+
server.registerTool("gd_document_highlight", {
|
|
394
|
+
title: "GDScript document highlights",
|
|
395
|
+
description: "Highlight every occurrence of the symbol at a position WITHIN the same file, tagged read / write / text " +
|
|
396
|
+
"(the shading an editor shows for a variable's uses when the caret is on it). Read-only. " +
|
|
397
|
+
"Godot's GDScript language server advertises documentHighlightProvider; feature-detected.",
|
|
398
|
+
inputSchema: posSchema,
|
|
399
|
+
}, async ({ path, line, character }) => {
|
|
400
|
+
const alt = "Use gd_references for project-wide uses of the symbol.";
|
|
401
|
+
try {
|
|
402
|
+
const caps = await lsp.getServerCapabilities();
|
|
403
|
+
if (!caps.documentHighlightProvider)
|
|
404
|
+
return unsupportedLsp("gd_document_highlight", "textDocument/documentHighlight", "documentHighlightProvider", alt);
|
|
405
|
+
const uri = await openAndPos(path);
|
|
406
|
+
const result = await lsp.request("textDocument/documentHighlight", { textDocument: { uri }, position: { line, character } });
|
|
407
|
+
return ok({ highlights: normalizeHighlights(result) });
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
if (isMethodNotFound(err))
|
|
411
|
+
return unsupportedLsp("gd_document_highlight", "textDocument/documentHighlight", "documentHighlightProvider", alt);
|
|
412
|
+
return fail(err);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
server.registerTool("gd_type_definition", {
|
|
416
|
+
title: "GDScript go-to-type-definition",
|
|
417
|
+
description: "Resolve the location of the TYPE of the symbol at a position (jump to the class of a typed variable), as opposed to the " +
|
|
418
|
+
"symbol's own definition. Godot's GDScript language server advertises typeDefinitionProvider; feature-detected.",
|
|
419
|
+
inputSchema: posSchema,
|
|
420
|
+
}, async ({ path, line, character }) => {
|
|
421
|
+
const alt = "Use gd_definition to jump to the symbol's own definition.";
|
|
422
|
+
try {
|
|
423
|
+
const caps = await lsp.getServerCapabilities();
|
|
424
|
+
if (!caps.typeDefinitionProvider)
|
|
425
|
+
return unsupportedLsp("gd_type_definition", "textDocument/typeDefinition", "typeDefinitionProvider", alt);
|
|
426
|
+
const uri = await openAndPos(path);
|
|
427
|
+
const result = await lsp.request("textDocument/typeDefinition", { textDocument: { uri }, position: { line, character } });
|
|
428
|
+
return ok({ locations: normalizeLocations(result) });
|
|
429
|
+
}
|
|
430
|
+
catch (err) {
|
|
431
|
+
if (isMethodNotFound(err))
|
|
432
|
+
return unsupportedLsp("gd_type_definition", "textDocument/typeDefinition", "typeDefinitionProvider", alt);
|
|
433
|
+
return fail(err);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
server.registerTool("gd_implementation", {
|
|
437
|
+
title: "GDScript go-to-implementation",
|
|
438
|
+
description: "Resolve the implementation location(s) of the symbol at a position (e.g. the concrete override of a method). " +
|
|
439
|
+
"Godot's GDScript language server advertises implementationProvider; feature-detected.",
|
|
440
|
+
inputSchema: posSchema,
|
|
441
|
+
}, async ({ path, line, character }) => {
|
|
442
|
+
const alt = "Use gd_definition / gd_references to navigate the symbol.";
|
|
443
|
+
try {
|
|
444
|
+
const caps = await lsp.getServerCapabilities();
|
|
445
|
+
if (!caps.implementationProvider)
|
|
446
|
+
return unsupportedLsp("gd_implementation", "textDocument/implementation", "implementationProvider", alt);
|
|
447
|
+
const uri = await openAndPos(path);
|
|
448
|
+
const result = await lsp.request("textDocument/implementation", { textDocument: { uri }, position: { line, character } });
|
|
449
|
+
return ok({ locations: normalizeLocations(result) });
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
if (isMethodNotFound(err))
|
|
453
|
+
return unsupportedLsp("gd_implementation", "textDocument/implementation", "implementationProvider", alt);
|
|
454
|
+
return fail(err);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
server.registerTool("gd_declaration", {
|
|
458
|
+
title: "GDScript go-to-declaration",
|
|
459
|
+
description: "Resolve the declaration location(s) of the symbol at a position. (For many symbols this coincides with the definition; the " +
|
|
460
|
+
"two differ for forward-declared or re-exported names.) Godot's GDScript language server advertises declarationProvider; feature-detected.",
|
|
461
|
+
inputSchema: posSchema,
|
|
462
|
+
}, async ({ path, line, character }) => {
|
|
463
|
+
const alt = "Use gd_definition to resolve the symbol's definition.";
|
|
464
|
+
try {
|
|
465
|
+
const caps = await lsp.getServerCapabilities();
|
|
466
|
+
if (!caps.declarationProvider)
|
|
467
|
+
return unsupportedLsp("gd_declaration", "textDocument/declaration", "declarationProvider", alt);
|
|
468
|
+
const uri = await openAndPos(path);
|
|
469
|
+
const result = await lsp.request("textDocument/declaration", { textDocument: { uri }, position: { line, character } });
|
|
470
|
+
return ok({ locations: normalizeLocations(result) });
|
|
471
|
+
}
|
|
472
|
+
catch (err) {
|
|
473
|
+
if (isMethodNotFound(err))
|
|
474
|
+
return unsupportedLsp("gd_declaration", "textDocument/declaration", "declarationProvider", alt);
|
|
475
|
+
return fail(err);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
server.registerTool("gd_folding_ranges", {
|
|
479
|
+
title: "GDScript folding ranges",
|
|
480
|
+
description: "List the foldable regions of a script (functions, blocks, comment/region markers) — the ranges an editor's fold gutter offers. " +
|
|
481
|
+
"Read-only. Godot's GDScript language server advertises foldingRangeProvider; feature-detected.",
|
|
482
|
+
inputSchema: { path: z.string().describe("Script path (res://..., absolute, or project-relative)") },
|
|
483
|
+
}, async ({ path }) => {
|
|
484
|
+
const alt = "Use gd_document_symbols to outline the file's structure instead.";
|
|
485
|
+
try {
|
|
486
|
+
const caps = await lsp.getServerCapabilities();
|
|
487
|
+
if (!caps.foldingRangeProvider)
|
|
488
|
+
return unsupportedLsp("gd_folding_ranges", "textDocument/foldingRange", "foldingRangeProvider", alt);
|
|
489
|
+
const uri = await openAndPos(path);
|
|
490
|
+
const result = await lsp.request("textDocument/foldingRange", { textDocument: { uri } });
|
|
491
|
+
return ok({ ranges: normalizeFolding(result) });
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
if (isMethodNotFound(err))
|
|
495
|
+
return unsupportedLsp("gd_folding_ranges", "textDocument/foldingRange", "foldingRangeProvider", alt);
|
|
496
|
+
return fail(err);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
server.registerTool("gd_document_link", {
|
|
500
|
+
title: "GDScript document links",
|
|
501
|
+
description: "List the links embedded in a script (res:// paths or URLs the language server recognizes) with their source ranges and targets. " +
|
|
502
|
+
"Read-only. Godot's GDScript language server advertises documentLinkProvider; feature-detected.",
|
|
503
|
+
inputSchema: { path: z.string().describe("Script path (res://..., absolute, or project-relative)") },
|
|
504
|
+
}, async ({ path }) => {
|
|
505
|
+
const alt = "Links are an editor-only convenience; there is no host-side alternative.";
|
|
506
|
+
try {
|
|
507
|
+
const caps = await lsp.getServerCapabilities();
|
|
508
|
+
if (!caps.documentLinkProvider)
|
|
509
|
+
return unsupportedLsp("gd_document_link", "textDocument/documentLink", "documentLinkProvider", alt);
|
|
510
|
+
const uri = await openAndPos(path);
|
|
511
|
+
const result = await lsp.request("textDocument/documentLink", { textDocument: { uri } });
|
|
512
|
+
return ok({ links: normalizeLinks(result) });
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
if (isMethodNotFound(err))
|
|
516
|
+
return unsupportedLsp("gd_document_link", "textDocument/documentLink", "documentLinkProvider", alt);
|
|
517
|
+
return fail(err);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
server.registerTool("gd_formatting", {
|
|
521
|
+
title: "GDScript format (preview)",
|
|
522
|
+
description: "Compute how the language server would reformat a whole script and return the formatted TEXT — WITHOUT writing anything to disk " +
|
|
523
|
+
"(read-only preview; apply it yourself with a file write if you want it). " +
|
|
524
|
+
"Godot's GDScript language server advertises documentFormattingProvider; feature-detected.",
|
|
525
|
+
inputSchema: {
|
|
526
|
+
path: z.string().describe("Script path (res://..., absolute, or project-relative)"),
|
|
527
|
+
tab_size: z.number().int().positive().optional().describe("Indent width the server should assume (default 4)"),
|
|
528
|
+
insert_spaces: z.boolean().optional().describe("Indent with spaces instead of tabs (default false — Godot uses tabs)"),
|
|
529
|
+
},
|
|
530
|
+
}, async ({ path, tab_size, insert_spaces }) => {
|
|
531
|
+
const alt = "Formatting has no host-side fallback; the connected build must implement it.";
|
|
532
|
+
try {
|
|
533
|
+
const caps = await lsp.getServerCapabilities();
|
|
534
|
+
if (!caps.documentFormattingProvider)
|
|
535
|
+
return unsupportedLsp("gd_formatting", "textDocument/formatting", "documentFormattingProvider", alt);
|
|
536
|
+
const fsPath = toFsPath(path, cfg.projectPath);
|
|
537
|
+
const before = readFileText(fsPath);
|
|
538
|
+
const uri = toFileUri(path, cfg.projectPath);
|
|
539
|
+
await lsp.ensureOpen(uri, before);
|
|
540
|
+
const result = (await lsp.request("textDocument/formatting", {
|
|
541
|
+
textDocument: { uri },
|
|
542
|
+
options: { tabSize: tab_size ?? 4, insertSpaces: insert_spaces ?? false },
|
|
543
|
+
}));
|
|
544
|
+
const edits = result ?? [];
|
|
545
|
+
const formatted = applyTextEdits(before, edits);
|
|
546
|
+
return ok({ edit_count: edits.length, formatted });
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
if (isMethodNotFound(err))
|
|
550
|
+
return unsupportedLsp("gd_formatting", "textDocument/formatting", "documentFormattingProvider", alt);
|
|
551
|
+
return fail(err);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
server.registerTool("gd_document_color", {
|
|
555
|
+
title: "GDScript document colors",
|
|
556
|
+
description: "List the color literals the language server recognizes in a script — the Color(...) values an editor draws an inline swatch for — with each one's source range, its RGBA components (floats 0..1) and a convenience #RRGGBBAA hex. Read-only. " +
|
|
557
|
+
"Godot's GDScript language server advertises colorProvider; feature-detected with a -32601 belt-and-suspenders (advertised ≠ implemented — the D7 lesson).",
|
|
558
|
+
inputSchema: { path: z.string().describe("Script path (res://..., absolute, or project-relative)") },
|
|
559
|
+
}, async ({ path }) => {
|
|
560
|
+
const alt = "Color inlays are an editor-only convenience; there is no host-side alternative.";
|
|
561
|
+
try {
|
|
562
|
+
const caps = await lsp.getServerCapabilities();
|
|
563
|
+
if (!caps.colorProvider)
|
|
564
|
+
return unsupportedLsp("gd_document_color", "textDocument/documentColor", "colorProvider", alt);
|
|
565
|
+
const uri = await openAndPos(path);
|
|
566
|
+
const result = await lsp.request("textDocument/documentColor", { textDocument: { uri } });
|
|
567
|
+
return ok({ colors: normalizeColors(result) });
|
|
568
|
+
}
|
|
569
|
+
catch (err) {
|
|
570
|
+
if (isMethodNotFound(err))
|
|
571
|
+
return unsupportedLsp("gd_document_color", "textDocument/documentColor", "colorProvider", alt);
|
|
572
|
+
return fail(err);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
//# sourceMappingURL=lsp.js.map
|