gdharness 1.1.33 → 1.1.35
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/build/cli.js +125 -33
- package/build/index.js +122 -28
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -17758,7 +17758,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17758
17758
|
},
|
|
17759
17759
|
{
|
|
17760
17760
|
name: "script_edit",
|
|
17761
|
-
description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read, and a script declaring a class_name is put in the project's class list, with registered saying whether it is there. modify makes every addition or none, and answers with the line each one is on in the file written. rename renames a class or a member wherever the engine uses it and writes every file or none
|
|
17761
|
+
description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read, and a script declaring a class_name is put in the project's class list, with registered saying whether it is there. modify makes every addition or none, and answers with the line each one is on in the file written. rename renames a class or a member wherever the engine uses it and writes every file or none. A new name already taken is refused with nothing written, a member's included when a class inheriting it already uses the name in code, as a local or a parameter. changed lists each file with its changed lines as they now read, and leftAlone lists every other whole-word occurrence of the old name with what the tokenizer makes of it (code, string, comment, doc, nodePath, scene, text, or ignored for a file under a .gdignore directory, which is never changed) and, where it is not plain, why: a string that is the old name whole, such as find_children's type or a call's method name, which nothing renames or compiles and which is listed first; a node named like the class; a call on a value whose type the analyser could not tell. A member rename answers under languageServer how many requests it made and how long they took. A comment or a line of a text file with nothing more to say is given under leftAloneInProse by file and line instead. Read both before calling the rename done; nothing in them was changed. Afterwards an open editor scans the changed files and reloads every changed script, which is also the compile check, and its answer comes back under editor; with no editor the class cache is rebuilt and the changed scripts are compiled in a fresh engine, under classCache and compiled. written is true once the files are written, and ok is false when what came after the write failed: the files are left changed, and note says so.",
|
|
17762
17762
|
parameters: {
|
|
17763
17763
|
projectPath: PROJECT_PATH,
|
|
17764
17764
|
scriptPath: SCRIPT_PATH,
|
|
@@ -38854,9 +38854,11 @@ class GodotLSPClient {
|
|
|
38854
38854
|
diagnosticsWaiters = new Map;
|
|
38855
38855
|
documentVersions = new Map;
|
|
38856
38856
|
documentTurns = new Map;
|
|
38857
|
-
|
|
38857
|
+
timeouts;
|
|
38858
|
+
constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1", timeouts = { requestMs: REQUEST_TIMEOUT_MS2, referencesMs: REFERENCES_TIMEOUT_MS }) {
|
|
38858
38859
|
this.port = port;
|
|
38859
38860
|
this.host = host;
|
|
38861
|
+
this.timeouts = timeouts;
|
|
38860
38862
|
this.pendingRequests = new Map;
|
|
38861
38863
|
}
|
|
38862
38864
|
async connect() {
|
|
@@ -38953,7 +38955,7 @@ class GodotLSPClient {
|
|
|
38953
38955
|
const rootPath = this.rootPath ?? dirname13(resolve5(filePath));
|
|
38954
38956
|
await this.initialize(rootPath);
|
|
38955
38957
|
}
|
|
38956
|
-
async sendRequest(method, params) {
|
|
38958
|
+
async sendRequest(method, params, timeoutMs = this.timeouts.requestMs) {
|
|
38957
38959
|
await this.ensureConnected();
|
|
38958
38960
|
if (!this.socket) {
|
|
38959
38961
|
throw new Refusal("Not connected to Godot LSP");
|
|
@@ -38970,8 +38972,8 @@ class GodotLSPClient {
|
|
|
38970
38972
|
return new Promise((resolveRequest, rejectRequest) => {
|
|
38971
38973
|
const timer = setTimeout(() => {
|
|
38972
38974
|
this.pendingRequests.delete(id);
|
|
38973
|
-
rejectRequest(new
|
|
38974
|
-
},
|
|
38975
|
+
rejectRequest(new LSPTimeout(method, timeoutMs));
|
|
38976
|
+
}, timeoutMs);
|
|
38975
38977
|
this.pendingRequests.set(id, {
|
|
38976
38978
|
resolve: resolveRequest,
|
|
38977
38979
|
reject: rejectRequest,
|
|
@@ -39276,7 +39278,7 @@ class GodotLSPClient {
|
|
|
39276
39278
|
textDocument: { uri },
|
|
39277
39279
|
position: { line, character },
|
|
39278
39280
|
context: { includeDeclaration: true }
|
|
39279
|
-
});
|
|
39281
|
+
}, this.timeouts.referencesMs);
|
|
39280
39282
|
} finally {
|
|
39281
39283
|
this.closeDocument(uri);
|
|
39282
39284
|
}
|
|
@@ -39434,12 +39436,21 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
39434
39436
|
});
|
|
39435
39437
|
}
|
|
39436
39438
|
}
|
|
39437
|
-
var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, ArgumentRefusal, AnotherProjectsServer;
|
|
39439
|
+
var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, REQUEST_TIMEOUT_MS2 = 1e4, REFERENCES_TIMEOUT_MS = 180000, LSPTimeout, ArgumentRefusal, AnotherProjectsServer;
|
|
39438
39440
|
var init_lsp_client = __esm(() => {
|
|
39439
39441
|
init_errors();
|
|
39440
39442
|
init_framing();
|
|
39441
39443
|
init_paths();
|
|
39442
39444
|
init_ports();
|
|
39445
|
+
LSPTimeout = class LSPTimeout extends Error {
|
|
39446
|
+
method;
|
|
39447
|
+
seconds;
|
|
39448
|
+
constructor(method, timeoutMs) {
|
|
39449
|
+
super(`LSP request timed out after ${String(timeoutMs / 1000)}s: ${method}`);
|
|
39450
|
+
this.method = method;
|
|
39451
|
+
this.seconds = timeoutMs / 1000;
|
|
39452
|
+
}
|
|
39453
|
+
};
|
|
39443
39454
|
ArgumentRefusal = class ArgumentRefusal extends Refusal {
|
|
39444
39455
|
};
|
|
39445
39456
|
AnotherProjectsServer = class AnotherProjectsServer extends Refusal {
|
|
@@ -40061,6 +40072,10 @@ class PlanBuilder {
|
|
|
40061
40072
|
return { edits, mentions };
|
|
40062
40073
|
}
|
|
40063
40074
|
}
|
|
40075
|
+
function isWholeString(text, offset, name) {
|
|
40076
|
+
const before = text[offset - 1];
|
|
40077
|
+
return (before === '"' || before === "'") && text[offset + name.length] === before;
|
|
40078
|
+
}
|
|
40064
40079
|
function declaresItsOwn(script, name, except) {
|
|
40065
40080
|
return script.occurrences(name).some((occurrence) => occurrence.kind === "code" && occurrence.offset !== except && occurrence.declaredAs !== null && occurrence.declaredAs !== "class_name");
|
|
40066
40081
|
}
|
|
@@ -40101,6 +40116,9 @@ function planClassRename(args) {
|
|
|
40101
40116
|
}
|
|
40102
40117
|
plan.mentionTheRest(texts, scripts, (file, occurrence, kind) => {
|
|
40103
40118
|
const script = scripts.get(file);
|
|
40119
|
+
if (kind === "string" && script !== undefined && isWholeString(script.text, occurrence.offset, oldName)) {
|
|
40120
|
+
return WHOLE_CLASS_NAME;
|
|
40121
|
+
}
|
|
40104
40122
|
if (kind !== "code" || script === undefined) {
|
|
40105
40123
|
return;
|
|
40106
40124
|
}
|
|
@@ -40167,7 +40185,16 @@ function planMemberRename(args) {
|
|
|
40167
40185
|
}
|
|
40168
40186
|
}
|
|
40169
40187
|
planScenes(texts, heirs, kind, oldName, plan);
|
|
40170
|
-
plan.mentionTheRest(texts, scripts, (
|
|
40188
|
+
plan.mentionTheRest(texts, scripts, (file, occurrence, mentionKind) => {
|
|
40189
|
+
const script = scripts.get(file);
|
|
40190
|
+
if (mentionKind === "string" && script !== undefined && isWholeString(script.text, occurrence.offset, oldName)) {
|
|
40191
|
+
return WHOLE_MEMBER_NAME;
|
|
40192
|
+
}
|
|
40193
|
+
if (mentionKind !== "code") {
|
|
40194
|
+
return;
|
|
40195
|
+
}
|
|
40196
|
+
return occurrence.afterDot ? "not resolved to this member by the language server: another class with a member of the same name, or a value whose type the analyser could not tell" : "not resolved to this member by the language server";
|
|
40197
|
+
});
|
|
40171
40198
|
return plan.build();
|
|
40172
40199
|
}
|
|
40173
40200
|
function sectionsOf(text) {
|
|
@@ -40503,7 +40530,7 @@ function applyRewrites(rewrites) {
|
|
|
40503
40530
|
throw error;
|
|
40504
40531
|
}
|
|
40505
40532
|
}
|
|
40506
|
-
var DOC_LINK_KINDS, DOC_KIND;
|
|
40533
|
+
var DOC_LINK_KINDS, DOC_KIND, WHOLE_CLASS_NAME = "the whole string is the class's name, as find_children's type, a comparison with get_global_name() or a list of accepted type names takes it, and it was not changed: if it names this class, it no longer matches anything after the rename, and no compile says so", WHOLE_MEMBER_NAME = "the whole string is the member's name, as call, connect, has_method, get or set take it, and it was not changed: if it names this member, it no longer finds it after the rename, and no compile says so";
|
|
40507
40534
|
var init_rename = __esm(() => {
|
|
40508
40535
|
init_gdscript_source();
|
|
40509
40536
|
DOC_LINK_KINDS = new Set([
|
|
@@ -40612,6 +40639,7 @@ async function renameSymbol(request, services) {
|
|
|
40612
40639
|
let plan;
|
|
40613
40640
|
let what;
|
|
40614
40641
|
let resolvedBy;
|
|
40642
|
+
let took = null;
|
|
40615
40643
|
let overrides = [];
|
|
40616
40644
|
let declaringFiles = [declaringPath];
|
|
40617
40645
|
if (isClass) {
|
|
@@ -40684,6 +40712,20 @@ async function renameSymbol(request, services) {
|
|
|
40684
40712
|
conflicts.push(`${newName} is already declared in ${id.endsWith("#") ? fileOf(id) : `${fileOf(id)}, class ${id.slice(id.lastIndexOf("#") + 1)}`}`);
|
|
40685
40713
|
}
|
|
40686
40714
|
}
|
|
40715
|
+
const heirs = graph.descendants(root);
|
|
40716
|
+
const inUse = [];
|
|
40717
|
+
for (const script of scripts.values()) {
|
|
40718
|
+
for (const occurrence of script.occurrences(newName)) {
|
|
40719
|
+
const body = graph.bodyAt(script.path, occurrence.offset);
|
|
40720
|
+
if (occurrence.kind === "code" && !occurrence.afterDot && heirs.has(body) && graph.declaration(body, newName)?.offset !== occurrence.offset) {
|
|
40721
|
+
inUse.push({ file: script.path, line: script.lines.positionOf(occurrence.offset).line + 1 });
|
|
40722
|
+
}
|
|
40723
|
+
}
|
|
40724
|
+
}
|
|
40725
|
+
if (inUse.length > 0) {
|
|
40726
|
+
const places = inUse.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line).slice(0, 10).map((place) => `${place.file}:${place.line}`);
|
|
40727
|
+
conflicts.push(`${newName} is already used in code where ${symbol} is inherited, as a local, a parameter or another name the renamed ${what} would shadow or be shadowed by, at ${places.join(", ")}${inUse.length > places.length ? ` and ${inUse.length - places.length} more` : ""}`);
|
|
40728
|
+
}
|
|
40687
40729
|
if (conflicts.length > 0) {
|
|
40688
40730
|
return {
|
|
40689
40731
|
ok: false,
|
|
@@ -40704,7 +40746,23 @@ async function renameSymbol(request, services) {
|
|
|
40704
40746
|
}
|
|
40705
40747
|
overrides = family.map((one) => one.file).filter((file) => file !== fileOf(root));
|
|
40706
40748
|
declaringFiles = family.map((one) => one.file);
|
|
40707
|
-
|
|
40749
|
+
took = { references: { asked: 0, ms: 0 }, definitions: { asked: 0, ms: 0 } };
|
|
40750
|
+
let resolved;
|
|
40751
|
+
try {
|
|
40752
|
+
resolved = await resolveMember(projectPath, texts, scripts, family, symbol, services, took);
|
|
40753
|
+
} catch (error) {
|
|
40754
|
+
if (!(error instanceof LSPTimeout)) {
|
|
40755
|
+
throw error;
|
|
40756
|
+
}
|
|
40757
|
+
return {
|
|
40758
|
+
ok: false,
|
|
40759
|
+
reason: `The editor's language server took the ${error.method} request for ${symbol} and had not answered it after ${String(error.seconds)}s, so nothing was renamed.${error.method === "textDocument/references" ? " Godot answers a references request by looking for the name's text in every script and resolving each match, so a name also written as a common local takes longest." : ""}`,
|
|
40760
|
+
advice: [
|
|
40761
|
+
"script_diagnostics on the declaring script says whether the language server is answering other requests",
|
|
40762
|
+
"The request that ran out is still being answered by the editor, and another sent now waits behind it"
|
|
40763
|
+
]
|
|
40764
|
+
};
|
|
40765
|
+
}
|
|
40708
40766
|
if (resolved === null) {
|
|
40709
40767
|
const first = family[0];
|
|
40710
40768
|
const script = first === undefined ? undefined : scripts.get(first.file);
|
|
@@ -40752,15 +40810,16 @@ async function renameSymbol(request, services) {
|
|
|
40752
40810
|
rewrites,
|
|
40753
40811
|
report: {
|
|
40754
40812
|
renamed: { what, from: symbol, to: newName, declaredIn: declaringPath, resolvedBy },
|
|
40813
|
+
...took === null ? {} : { languageServer: took },
|
|
40755
40814
|
...overrides.length > 0 ? { overridesRenamed: overrides } : {},
|
|
40756
40815
|
...moveTo === null ? {} : { moved: { from: declaringPath, to: moveTo } },
|
|
40757
40816
|
changed: changedReport(plan.edits, rewrites),
|
|
40758
|
-
|
|
40817
|
+
...leftAloneReport(plan.mentions),
|
|
40759
40818
|
summary: summaryOf(plan.edits, plan.mentions, rewrites)
|
|
40760
40819
|
}
|
|
40761
40820
|
};
|
|
40762
40821
|
}
|
|
40763
|
-
async function resolveMember(projectPath, texts, scripts, family, name, services) {
|
|
40822
|
+
async function resolveMember(projectPath, texts, scripts, family, name, services, took) {
|
|
40764
40823
|
const absoluteOf = new Map(texts.map((file) => [file.path, file.absolute]));
|
|
40765
40824
|
const key = (place) => `${place.file}:${place.offset}`;
|
|
40766
40825
|
const toResolved = (located) => {
|
|
@@ -40774,11 +40833,16 @@ async function resolveMember(projectPath, texts, scripts, family, name, services
|
|
|
40774
40833
|
if (script === undefined || absolute === undefined) {
|
|
40775
40834
|
return [];
|
|
40776
40835
|
}
|
|
40777
|
-
const
|
|
40778
|
-
|
|
40836
|
+
const started = performance.now();
|
|
40837
|
+
try {
|
|
40838
|
+
const answered = await services[question](absolute, script.text, script.lines.positionOf(place.offset));
|
|
40839
|
+
return answered.map(toResolved).filter((one) => one !== null);
|
|
40840
|
+
} finally {
|
|
40841
|
+
took[question].asked += 1;
|
|
40842
|
+
took[question].ms += Math.round(performance.now() - started);
|
|
40843
|
+
}
|
|
40779
40844
|
};
|
|
40780
40845
|
const ours = new Map;
|
|
40781
|
-
const settled = new Set;
|
|
40782
40846
|
const root = family[0];
|
|
40783
40847
|
if (root === undefined) {
|
|
40784
40848
|
return [];
|
|
@@ -40794,23 +40858,13 @@ async function resolveMember(projectPath, texts, scripts, family, name, services
|
|
|
40794
40858
|
for (const script of scripts.values()) {
|
|
40795
40859
|
for (const occurrence of script.occurrences(name)) {
|
|
40796
40860
|
const place = { file: script.path, offset: occurrence.offset };
|
|
40797
|
-
if (occurrence.kind !== "code" || ours.has(key(place))
|
|
40861
|
+
if (occurrence.kind !== "code" || ours.has(key(place))) {
|
|
40798
40862
|
continue;
|
|
40799
40863
|
}
|
|
40800
40864
|
const declared = await ask("definitions", place);
|
|
40801
40865
|
const only = declared.length === 1 ? declared[0] : undefined;
|
|
40802
|
-
if (only
|
|
40803
|
-
|
|
40804
|
-
continue;
|
|
40805
|
-
}
|
|
40806
|
-
const isOurs = declarations.has(key(only));
|
|
40807
|
-
const same = [place, ...await ask("references", place)];
|
|
40808
|
-
for (const one of same) {
|
|
40809
|
-
if (isOurs) {
|
|
40810
|
-
ours.set(key(one), one);
|
|
40811
|
-
} else {
|
|
40812
|
-
settled.add(key(one));
|
|
40813
|
-
}
|
|
40866
|
+
if (only !== undefined && declarations.has(key(only))) {
|
|
40867
|
+
ours.set(key(place), place);
|
|
40814
40868
|
}
|
|
40815
40869
|
}
|
|
40816
40870
|
}
|
|
@@ -40850,6 +40904,28 @@ function changedReport(edits, rewrites) {
|
|
|
40850
40904
|
};
|
|
40851
40905
|
});
|
|
40852
40906
|
}
|
|
40907
|
+
function leftAloneReport(mentions) {
|
|
40908
|
+
const full = [];
|
|
40909
|
+
const prose = new Map;
|
|
40910
|
+
for (const mention of mentions) {
|
|
40911
|
+
if ((mention.kind === "comment" || mention.kind === "text") && mention.why === undefined) {
|
|
40912
|
+
const key = `${mention.file}
|
|
40913
|
+
${mention.kind}`;
|
|
40914
|
+
const group = prose.get(key) ?? { file: mention.file, kind: mention.kind, lines: [] };
|
|
40915
|
+
if (!group.lines.includes(mention.line)) {
|
|
40916
|
+
group.lines.push(mention.line);
|
|
40917
|
+
}
|
|
40918
|
+
prose.set(key, group);
|
|
40919
|
+
} else {
|
|
40920
|
+
full.push(mention);
|
|
40921
|
+
}
|
|
40922
|
+
}
|
|
40923
|
+
const loudFirst = (mention) => mention.kind === "string" && mention.why !== undefined ? 0 : 1;
|
|
40924
|
+
return {
|
|
40925
|
+
leftAlone: full.sort((a, b) => loudFirst(a) - loudFirst(b)),
|
|
40926
|
+
...prose.size > 0 ? { leftAloneInProse: [...prose.values()] } : {}
|
|
40927
|
+
};
|
|
40928
|
+
}
|
|
40853
40929
|
function summaryOf(edits, mentions, rewrites) {
|
|
40854
40930
|
const count = (values) => {
|
|
40855
40931
|
const counted = {};
|
|
@@ -40869,6 +40945,7 @@ function summaryOf(edits, mentions, rewrites) {
|
|
|
40869
40945
|
var WHAT;
|
|
40870
40946
|
var init_rename_symbol = __esm(() => {
|
|
40871
40947
|
init_gdscript_source();
|
|
40948
|
+
init_lsp_client();
|
|
40872
40949
|
init_project_scan();
|
|
40873
40950
|
init_rename();
|
|
40874
40951
|
init_resources();
|
|
@@ -45946,7 +46023,9 @@ class GodotServer {
|
|
|
45946
46023
|
}
|
|
45947
46024
|
return await question(client);
|
|
45948
46025
|
} catch (error) {
|
|
45949
|
-
|
|
46026
|
+
if (!(error instanceof LSPTimeout)) {
|
|
46027
|
+
languageServer.problem = normalizeLSPError(error, client.port);
|
|
46028
|
+
}
|
|
45950
46029
|
throw error;
|
|
45951
46030
|
}
|
|
45952
46031
|
};
|
|
@@ -45995,7 +46074,18 @@ class GodotServer {
|
|
|
45995
46074
|
rebuildCache: outcome.isClass || outcome.moved !== null,
|
|
45996
46075
|
moved: outcome.moved !== null
|
|
45997
46076
|
});
|
|
45998
|
-
|
|
46077
|
+
const said = readString(settled.report, "note");
|
|
46078
|
+
const leftChanged = settled.ok ? said : [
|
|
46079
|
+
"The rename was written: every file under changed now reads as shown there and is left that way. ok is false for what came after the write, said under editor or compiled.",
|
|
46080
|
+
...said === undefined ? [] : [said]
|
|
46081
|
+
].join(" ");
|
|
46082
|
+
return this.jsonTextResponse({
|
|
46083
|
+
ok: settled.ok,
|
|
46084
|
+
written: true,
|
|
46085
|
+
...outcome.report,
|
|
46086
|
+
...settled.report,
|
|
46087
|
+
...leftChanged === undefined ? {} : { note: leftChanged }
|
|
46088
|
+
});
|
|
45999
46089
|
}
|
|
46000
46090
|
async settleRenameInEditor(projectPath, changed, names) {
|
|
46001
46091
|
const { gone, oldClass, newClass } = names;
|
|
@@ -47697,9 +47787,11 @@ that a setting nobody wrote down still answers with the default the engine regis
|
|
|
47697
47787
|
|
|
47698
47788
|
To rename a class or a member across the project, use \`script_edit rename\` rather than a text
|
|
47699
47789
|
replacement: it changes the uses the engine resolves, doc links and what scenes record, and lists
|
|
47700
|
-
every other occurrence of the word
|
|
47701
|
-
|
|
47702
|
-
|
|
47790
|
+
every other occurrence of the word for you to read: a string that is the name whole, such as a
|
|
47791
|
+
\`find_children\` type, first under \`leftAlone\` since nothing compiles it, a file under a
|
|
47792
|
+
\`.gdignore\` directory there too, and a word in a comment or a text file by line under
|
|
47793
|
+
\`leftAloneInProse\`. Pass \`preview\` first on a large rename. If the answer has \`ok: false\`
|
|
47794
|
+
beside \`written: true\`, the files were changed and a script did not compile afterwards.
|
|
47703
47795
|
|
|
47704
47796
|
Editing a file while a game is running does not change the running game. \`auto_reload\` is an
|
|
47705
47797
|
editor plugin and reloads into the editor's own process; the game is a separate process holding
|
package/build/index.js
CHANGED
|
@@ -29540,6 +29540,18 @@ import { dirname as dirname8, join as join14, resolve as resolve3 } from "node:p
|
|
|
29540
29540
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
29541
29541
|
var DEFAULT_LSP_PORT = 6005;
|
|
29542
29542
|
var DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
29543
|
+
var REQUEST_TIMEOUT_MS2 = 1e4;
|
|
29544
|
+
var REFERENCES_TIMEOUT_MS = 180000;
|
|
29545
|
+
|
|
29546
|
+
class LSPTimeout extends Error {
|
|
29547
|
+
method;
|
|
29548
|
+
seconds;
|
|
29549
|
+
constructor(method, timeoutMs) {
|
|
29550
|
+
super(`LSP request timed out after ${String(timeoutMs / 1000)}s: ${method}`);
|
|
29551
|
+
this.method = method;
|
|
29552
|
+
this.seconds = timeoutMs / 1000;
|
|
29553
|
+
}
|
|
29554
|
+
}
|
|
29543
29555
|
|
|
29544
29556
|
class ArgumentRefusal extends Refusal {
|
|
29545
29557
|
}
|
|
@@ -29583,9 +29595,11 @@ class GodotLSPClient {
|
|
|
29583
29595
|
diagnosticsWaiters = new Map;
|
|
29584
29596
|
documentVersions = new Map;
|
|
29585
29597
|
documentTurns = new Map;
|
|
29586
|
-
|
|
29598
|
+
timeouts;
|
|
29599
|
+
constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1", timeouts = { requestMs: REQUEST_TIMEOUT_MS2, referencesMs: REFERENCES_TIMEOUT_MS }) {
|
|
29587
29600
|
this.port = port;
|
|
29588
29601
|
this.host = host;
|
|
29602
|
+
this.timeouts = timeouts;
|
|
29589
29603
|
this.pendingRequests = new Map;
|
|
29590
29604
|
}
|
|
29591
29605
|
async connect() {
|
|
@@ -29682,7 +29696,7 @@ class GodotLSPClient {
|
|
|
29682
29696
|
const rootPath = this.rootPath ?? dirname8(resolve3(filePath));
|
|
29683
29697
|
await this.initialize(rootPath);
|
|
29684
29698
|
}
|
|
29685
|
-
async sendRequest(method, params) {
|
|
29699
|
+
async sendRequest(method, params, timeoutMs = this.timeouts.requestMs) {
|
|
29686
29700
|
await this.ensureConnected();
|
|
29687
29701
|
if (!this.socket) {
|
|
29688
29702
|
throw new Refusal("Not connected to Godot LSP");
|
|
@@ -29699,8 +29713,8 @@ class GodotLSPClient {
|
|
|
29699
29713
|
return new Promise((resolveRequest, rejectRequest) => {
|
|
29700
29714
|
const timer = setTimeout(() => {
|
|
29701
29715
|
this.pendingRequests.delete(id);
|
|
29702
|
-
rejectRequest(new
|
|
29703
|
-
},
|
|
29716
|
+
rejectRequest(new LSPTimeout(method, timeoutMs));
|
|
29717
|
+
}, timeoutMs);
|
|
29704
29718
|
this.pendingRequests.set(id, {
|
|
29705
29719
|
resolve: resolveRequest,
|
|
29706
29720
|
reject: rejectRequest,
|
|
@@ -30005,7 +30019,7 @@ class GodotLSPClient {
|
|
|
30005
30019
|
textDocument: { uri },
|
|
30006
30020
|
position: { line, character },
|
|
30007
30021
|
context: { includeDeclaration: true }
|
|
30008
|
-
});
|
|
30022
|
+
}, this.timeouts.referencesMs);
|
|
30009
30023
|
} finally {
|
|
30010
30024
|
this.closeDocument(uri);
|
|
30011
30025
|
}
|
|
@@ -30782,6 +30796,12 @@ class PlanBuilder {
|
|
|
30782
30796
|
return { edits, mentions };
|
|
30783
30797
|
}
|
|
30784
30798
|
}
|
|
30799
|
+
function isWholeString(text, offset, name) {
|
|
30800
|
+
const before = text[offset - 1];
|
|
30801
|
+
return (before === '"' || before === "'") && text[offset + name.length] === before;
|
|
30802
|
+
}
|
|
30803
|
+
var WHOLE_CLASS_NAME = "the whole string is the class's name, as find_children's type, a comparison with get_global_name() or a list of accepted type names takes it, and it was not changed: if it names this class, it no longer matches anything after the rename, and no compile says so";
|
|
30804
|
+
var WHOLE_MEMBER_NAME = "the whole string is the member's name, as call, connect, has_method, get or set take it, and it was not changed: if it names this member, it no longer finds it after the rename, and no compile says so";
|
|
30785
30805
|
function declaresItsOwn(script, name, except) {
|
|
30786
30806
|
return script.occurrences(name).some((occurrence) => occurrence.kind === "code" && occurrence.offset !== except && occurrence.declaredAs !== null && occurrence.declaredAs !== "class_name");
|
|
30787
30807
|
}
|
|
@@ -30822,6 +30842,9 @@ function planClassRename(args) {
|
|
|
30822
30842
|
}
|
|
30823
30843
|
plan.mentionTheRest(texts, scripts, (file, occurrence, kind) => {
|
|
30824
30844
|
const script = scripts.get(file);
|
|
30845
|
+
if (kind === "string" && script !== undefined && isWholeString(script.text, occurrence.offset, oldName)) {
|
|
30846
|
+
return WHOLE_CLASS_NAME;
|
|
30847
|
+
}
|
|
30825
30848
|
if (kind !== "code" || script === undefined) {
|
|
30826
30849
|
return;
|
|
30827
30850
|
}
|
|
@@ -30888,7 +30911,16 @@ function planMemberRename(args) {
|
|
|
30888
30911
|
}
|
|
30889
30912
|
}
|
|
30890
30913
|
planScenes(texts, heirs, kind, oldName, plan);
|
|
30891
|
-
plan.mentionTheRest(texts, scripts, (
|
|
30914
|
+
plan.mentionTheRest(texts, scripts, (file, occurrence, mentionKind) => {
|
|
30915
|
+
const script = scripts.get(file);
|
|
30916
|
+
if (mentionKind === "string" && script !== undefined && isWholeString(script.text, occurrence.offset, oldName)) {
|
|
30917
|
+
return WHOLE_MEMBER_NAME;
|
|
30918
|
+
}
|
|
30919
|
+
if (mentionKind !== "code") {
|
|
30920
|
+
return;
|
|
30921
|
+
}
|
|
30922
|
+
return occurrence.afterDot ? "not resolved to this member by the language server: another class with a member of the same name, or a value whose type the analyser could not tell" : "not resolved to this member by the language server";
|
|
30923
|
+
});
|
|
30892
30924
|
return plan.build();
|
|
30893
30925
|
}
|
|
30894
30926
|
function sectionsOf(text) {
|
|
@@ -31578,6 +31610,7 @@ async function renameSymbol(request, services) {
|
|
|
31578
31610
|
let plan;
|
|
31579
31611
|
let what;
|
|
31580
31612
|
let resolvedBy;
|
|
31613
|
+
let took = null;
|
|
31581
31614
|
let overrides = [];
|
|
31582
31615
|
let declaringFiles = [declaringPath];
|
|
31583
31616
|
if (isClass) {
|
|
@@ -31650,6 +31683,20 @@ async function renameSymbol(request, services) {
|
|
|
31650
31683
|
conflicts.push(`${newName} is already declared in ${id.endsWith("#") ? fileOf(id) : `${fileOf(id)}, class ${id.slice(id.lastIndexOf("#") + 1)}`}`);
|
|
31651
31684
|
}
|
|
31652
31685
|
}
|
|
31686
|
+
const heirs = graph.descendants(root);
|
|
31687
|
+
const inUse = [];
|
|
31688
|
+
for (const script of scripts.values()) {
|
|
31689
|
+
for (const occurrence of script.occurrences(newName)) {
|
|
31690
|
+
const body = graph.bodyAt(script.path, occurrence.offset);
|
|
31691
|
+
if (occurrence.kind === "code" && !occurrence.afterDot && heirs.has(body) && graph.declaration(body, newName)?.offset !== occurrence.offset) {
|
|
31692
|
+
inUse.push({ file: script.path, line: script.lines.positionOf(occurrence.offset).line + 1 });
|
|
31693
|
+
}
|
|
31694
|
+
}
|
|
31695
|
+
}
|
|
31696
|
+
if (inUse.length > 0) {
|
|
31697
|
+
const places = inUse.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line).slice(0, 10).map((place) => `${place.file}:${place.line}`);
|
|
31698
|
+
conflicts.push(`${newName} is already used in code where ${symbol} is inherited, as a local, a parameter or another name the renamed ${what} would shadow or be shadowed by, at ${places.join(", ")}${inUse.length > places.length ? ` and ${inUse.length - places.length} more` : ""}`);
|
|
31699
|
+
}
|
|
31653
31700
|
if (conflicts.length > 0) {
|
|
31654
31701
|
return {
|
|
31655
31702
|
ok: false,
|
|
@@ -31670,7 +31717,23 @@ async function renameSymbol(request, services) {
|
|
|
31670
31717
|
}
|
|
31671
31718
|
overrides = family.map((one) => one.file).filter((file) => file !== fileOf(root));
|
|
31672
31719
|
declaringFiles = family.map((one) => one.file);
|
|
31673
|
-
|
|
31720
|
+
took = { references: { asked: 0, ms: 0 }, definitions: { asked: 0, ms: 0 } };
|
|
31721
|
+
let resolved;
|
|
31722
|
+
try {
|
|
31723
|
+
resolved = await resolveMember(projectPath, texts, scripts, family, symbol, services, took);
|
|
31724
|
+
} catch (error) {
|
|
31725
|
+
if (!(error instanceof LSPTimeout)) {
|
|
31726
|
+
throw error;
|
|
31727
|
+
}
|
|
31728
|
+
return {
|
|
31729
|
+
ok: false,
|
|
31730
|
+
reason: `The editor's language server took the ${error.method} request for ${symbol} and had not answered it after ${String(error.seconds)}s, so nothing was renamed.${error.method === "textDocument/references" ? " Godot answers a references request by looking for the name's text in every script and resolving each match, so a name also written as a common local takes longest." : ""}`,
|
|
31731
|
+
advice: [
|
|
31732
|
+
"script_diagnostics on the declaring script says whether the language server is answering other requests",
|
|
31733
|
+
"The request that ran out is still being answered by the editor, and another sent now waits behind it"
|
|
31734
|
+
]
|
|
31735
|
+
};
|
|
31736
|
+
}
|
|
31674
31737
|
if (resolved === null) {
|
|
31675
31738
|
const first = family[0];
|
|
31676
31739
|
const script = first === undefined ? undefined : scripts.get(first.file);
|
|
@@ -31718,15 +31781,16 @@ async function renameSymbol(request, services) {
|
|
|
31718
31781
|
rewrites,
|
|
31719
31782
|
report: {
|
|
31720
31783
|
renamed: { what, from: symbol, to: newName, declaredIn: declaringPath, resolvedBy },
|
|
31784
|
+
...took === null ? {} : { languageServer: took },
|
|
31721
31785
|
...overrides.length > 0 ? { overridesRenamed: overrides } : {},
|
|
31722
31786
|
...moveTo === null ? {} : { moved: { from: declaringPath, to: moveTo } },
|
|
31723
31787
|
changed: changedReport(plan.edits, rewrites),
|
|
31724
|
-
|
|
31788
|
+
...leftAloneReport(plan.mentions),
|
|
31725
31789
|
summary: summaryOf(plan.edits, plan.mentions, rewrites)
|
|
31726
31790
|
}
|
|
31727
31791
|
};
|
|
31728
31792
|
}
|
|
31729
|
-
async function resolveMember(projectPath, texts, scripts, family, name, services) {
|
|
31793
|
+
async function resolveMember(projectPath, texts, scripts, family, name, services, took) {
|
|
31730
31794
|
const absoluteOf = new Map(texts.map((file) => [file.path, file.absolute]));
|
|
31731
31795
|
const key = (place) => `${place.file}:${place.offset}`;
|
|
31732
31796
|
const toResolved = (located) => {
|
|
@@ -31740,11 +31804,16 @@ async function resolveMember(projectPath, texts, scripts, family, name, services
|
|
|
31740
31804
|
if (script === undefined || absolute === undefined) {
|
|
31741
31805
|
return [];
|
|
31742
31806
|
}
|
|
31743
|
-
const
|
|
31744
|
-
|
|
31807
|
+
const started = performance.now();
|
|
31808
|
+
try {
|
|
31809
|
+
const answered = await services[question](absolute, script.text, script.lines.positionOf(place.offset));
|
|
31810
|
+
return answered.map(toResolved).filter((one) => one !== null);
|
|
31811
|
+
} finally {
|
|
31812
|
+
took[question].asked += 1;
|
|
31813
|
+
took[question].ms += Math.round(performance.now() - started);
|
|
31814
|
+
}
|
|
31745
31815
|
};
|
|
31746
31816
|
const ours = new Map;
|
|
31747
|
-
const settled = new Set;
|
|
31748
31817
|
const root = family[0];
|
|
31749
31818
|
if (root === undefined) {
|
|
31750
31819
|
return [];
|
|
@@ -31760,23 +31829,13 @@ async function resolveMember(projectPath, texts, scripts, family, name, services
|
|
|
31760
31829
|
for (const script of scripts.values()) {
|
|
31761
31830
|
for (const occurrence of script.occurrences(name)) {
|
|
31762
31831
|
const place = { file: script.path, offset: occurrence.offset };
|
|
31763
|
-
if (occurrence.kind !== "code" || ours.has(key(place))
|
|
31832
|
+
if (occurrence.kind !== "code" || ours.has(key(place))) {
|
|
31764
31833
|
continue;
|
|
31765
31834
|
}
|
|
31766
31835
|
const declared = await ask("definitions", place);
|
|
31767
31836
|
const only = declared.length === 1 ? declared[0] : undefined;
|
|
31768
|
-
if (only
|
|
31769
|
-
|
|
31770
|
-
continue;
|
|
31771
|
-
}
|
|
31772
|
-
const isOurs = declarations.has(key(only));
|
|
31773
|
-
const same = [place, ...await ask("references", place)];
|
|
31774
|
-
for (const one of same) {
|
|
31775
|
-
if (isOurs) {
|
|
31776
|
-
ours.set(key(one), one);
|
|
31777
|
-
} else {
|
|
31778
|
-
settled.add(key(one));
|
|
31779
|
-
}
|
|
31837
|
+
if (only !== undefined && declarations.has(key(only))) {
|
|
31838
|
+
ours.set(key(place), place);
|
|
31780
31839
|
}
|
|
31781
31840
|
}
|
|
31782
31841
|
}
|
|
@@ -31816,6 +31875,28 @@ function changedReport(edits, rewrites) {
|
|
|
31816
31875
|
};
|
|
31817
31876
|
});
|
|
31818
31877
|
}
|
|
31878
|
+
function leftAloneReport(mentions) {
|
|
31879
|
+
const full = [];
|
|
31880
|
+
const prose = new Map;
|
|
31881
|
+
for (const mention of mentions) {
|
|
31882
|
+
if ((mention.kind === "comment" || mention.kind === "text") && mention.why === undefined) {
|
|
31883
|
+
const key = `${mention.file}
|
|
31884
|
+
${mention.kind}`;
|
|
31885
|
+
const group = prose.get(key) ?? { file: mention.file, kind: mention.kind, lines: [] };
|
|
31886
|
+
if (!group.lines.includes(mention.line)) {
|
|
31887
|
+
group.lines.push(mention.line);
|
|
31888
|
+
}
|
|
31889
|
+
prose.set(key, group);
|
|
31890
|
+
} else {
|
|
31891
|
+
full.push(mention);
|
|
31892
|
+
}
|
|
31893
|
+
}
|
|
31894
|
+
const loudFirst = (mention) => mention.kind === "string" && mention.why !== undefined ? 0 : 1;
|
|
31895
|
+
return {
|
|
31896
|
+
leftAlone: full.sort((a, b) => loudFirst(a) - loudFirst(b)),
|
|
31897
|
+
...prose.size > 0 ? { leftAloneInProse: [...prose.values()] } : {}
|
|
31898
|
+
};
|
|
31899
|
+
}
|
|
31819
31900
|
function summaryOf(edits, mentions, rewrites) {
|
|
31820
31901
|
const count = (values) => {
|
|
31821
31902
|
const counted = {};
|
|
@@ -33002,7 +33083,7 @@ var TOOL_SPECS = [
|
|
|
33002
33083
|
},
|
|
33003
33084
|
{
|
|
33004
33085
|
name: "script_edit",
|
|
33005
|
-
description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read, and a script declaring a class_name is put in the project's class list, with registered saying whether it is there. modify makes every addition or none, and answers with the line each one is on in the file written. rename renames a class or a member wherever the engine uses it and writes every file or none
|
|
33086
|
+
description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read, and a script declaring a class_name is put in the project's class list, with registered saying whether it is there. modify makes every addition or none, and answers with the line each one is on in the file written. rename renames a class or a member wherever the engine uses it and writes every file or none. A new name already taken is refused with nothing written, a member's included when a class inheriting it already uses the name in code, as a local or a parameter. changed lists each file with its changed lines as they now read, and leftAlone lists every other whole-word occurrence of the old name with what the tokenizer makes of it (code, string, comment, doc, nodePath, scene, text, or ignored for a file under a .gdignore directory, which is never changed) and, where it is not plain, why: a string that is the old name whole, such as find_children's type or a call's method name, which nothing renames or compiles and which is listed first; a node named like the class; a call on a value whose type the analyser could not tell. A member rename answers under languageServer how many requests it made and how long they took. A comment or a line of a text file with nothing more to say is given under leftAloneInProse by file and line instead. Read both before calling the rename done; nothing in them was changed. Afterwards an open editor scans the changed files and reloads every changed script, which is also the compile check, and its answer comes back under editor; with no editor the class cache is rebuilt and the changed scripts are compiled in a fresh engine, under classCache and compiled. written is true once the files are written, and ok is false when what came after the write failed: the files are left changed, and note says so.",
|
|
33006
33087
|
parameters: {
|
|
33007
33088
|
projectPath: PROJECT_PATH,
|
|
33008
33089
|
scriptPath: SCRIPT_PATH,
|
|
@@ -38518,7 +38599,9 @@ class GodotServer {
|
|
|
38518
38599
|
}
|
|
38519
38600
|
return await question(client);
|
|
38520
38601
|
} catch (error) {
|
|
38521
|
-
|
|
38602
|
+
if (!(error instanceof LSPTimeout)) {
|
|
38603
|
+
languageServer.problem = normalizeLSPError(error, client.port);
|
|
38604
|
+
}
|
|
38522
38605
|
throw error;
|
|
38523
38606
|
}
|
|
38524
38607
|
};
|
|
@@ -38567,7 +38650,18 @@ class GodotServer {
|
|
|
38567
38650
|
rebuildCache: outcome.isClass || outcome.moved !== null,
|
|
38568
38651
|
moved: outcome.moved !== null
|
|
38569
38652
|
});
|
|
38570
|
-
|
|
38653
|
+
const said = readString(settled.report, "note");
|
|
38654
|
+
const leftChanged = settled.ok ? said : [
|
|
38655
|
+
"The rename was written: every file under changed now reads as shown there and is left that way. ok is false for what came after the write, said under editor or compiled.",
|
|
38656
|
+
...said === undefined ? [] : [said]
|
|
38657
|
+
].join(" ");
|
|
38658
|
+
return this.jsonTextResponse({
|
|
38659
|
+
ok: settled.ok,
|
|
38660
|
+
written: true,
|
|
38661
|
+
...outcome.report,
|
|
38662
|
+
...settled.report,
|
|
38663
|
+
...leftChanged === undefined ? {} : { note: leftChanged }
|
|
38664
|
+
});
|
|
38571
38665
|
}
|
|
38572
38666
|
async settleRenameInEditor(projectPath, changed, names) {
|
|
38573
38667
|
const { gone, oldClass, newClass } = names;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.35",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|