gdharness 1.1.32 → 1.1.34
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 +178 -27
- package/build/index.js +171 -22
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -8892,6 +8892,7 @@ function wholeWord(name, flags = "gu") {
|
|
|
8892
8892
|
function occurrencesOf(source, name, regions = regionsOf(source)) {
|
|
8893
8893
|
const found = [];
|
|
8894
8894
|
let index = 0;
|
|
8895
|
+
let code = null;
|
|
8895
8896
|
for (const match of source.matchAll(wholeWord(name))) {
|
|
8896
8897
|
const offset = match.index;
|
|
8897
8898
|
while (index < regions.length - 1 && (regions[index]?.end ?? 0) <= offset) {
|
|
@@ -8901,16 +8902,17 @@ function occurrencesOf(source, name, regions = regionsOf(source)) {
|
|
|
8901
8902
|
let afterDot = false;
|
|
8902
8903
|
let declaredAs = null;
|
|
8903
8904
|
if (kind === "code") {
|
|
8905
|
+
code ??= codeOf(source, regions);
|
|
8904
8906
|
let back = offset - 1;
|
|
8905
|
-
while (back >= 0 && /[\s\\]/.test(
|
|
8907
|
+
while (back >= 0 && /[\s\\]/.test(code[back] ?? "")) {
|
|
8906
8908
|
back -= 1;
|
|
8907
8909
|
}
|
|
8908
|
-
afterDot =
|
|
8910
|
+
afterDot = code[back] === "." && code[back - 1] !== ".";
|
|
8909
8911
|
let wordStart = back + 1;
|
|
8910
|
-
while (wordStart > 0 && isIdentifierPart(
|
|
8912
|
+
while (wordStart > 0 && isIdentifierPart(code[wordStart - 1])) {
|
|
8911
8913
|
wordStart -= 1;
|
|
8912
8914
|
}
|
|
8913
|
-
const word =
|
|
8915
|
+
const word = code.slice(wordStart, back + 1);
|
|
8914
8916
|
declaredAs = DECLARING.has(word) ? word : null;
|
|
8915
8917
|
}
|
|
8916
8918
|
found.push({ offset, kind, afterDot, declaredAs });
|
|
@@ -17756,7 +17758,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17756
17758
|
},
|
|
17757
17759
|
{
|
|
17758
17760
|
name: "script_edit",
|
|
17759
|
-
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 find_children string, a node named like the class, a call on a value whose type the analyser could not tell. 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.",
|
|
17760
17762
|
parameters: {
|
|
17761
17763
|
projectPath: PROJECT_PATH,
|
|
17762
17764
|
scriptPath: SCRIPT_PATH,
|
|
@@ -39654,6 +39656,33 @@ function projectTexts(projectPath) {
|
|
|
39654
39656
|
visit(projectPath, "");
|
|
39655
39657
|
return found;
|
|
39656
39658
|
}
|
|
39659
|
+
function ignoredTexts(projectPath) {
|
|
39660
|
+
const found = [];
|
|
39661
|
+
const visit = (directory, prefix, ignored) => {
|
|
39662
|
+
const under = ignored || steppedOver(directory);
|
|
39663
|
+
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
39664
|
+
if (skipped(entry.name)) {
|
|
39665
|
+
continue;
|
|
39666
|
+
}
|
|
39667
|
+
const absolute = join20(directory, entry.name);
|
|
39668
|
+
const spelled = `${prefix}${entry.name}`;
|
|
39669
|
+
if (entry.isDirectory()) {
|
|
39670
|
+
visit(absolute, `${spelled}/`, under);
|
|
39671
|
+
continue;
|
|
39672
|
+
}
|
|
39673
|
+
const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
39674
|
+
if (!under || !entry.isFile() || BINARY_EXTENSIONS.has(extension) || entry.name === ".gdignore") {
|
|
39675
|
+
continue;
|
|
39676
|
+
}
|
|
39677
|
+
const bytes = readFileSync17(absolute);
|
|
39678
|
+
if (!isBinary(bytes)) {
|
|
39679
|
+
found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
|
|
39680
|
+
}
|
|
39681
|
+
}
|
|
39682
|
+
};
|
|
39683
|
+
visit(projectPath, "", false);
|
|
39684
|
+
return found;
|
|
39685
|
+
}
|
|
39657
39686
|
function searchProject(projectPath, options) {
|
|
39658
39687
|
const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
|
|
39659
39688
|
const result = {
|
|
@@ -39901,33 +39930,79 @@ function declaredIn(scripts) {
|
|
|
39901
39930
|
}
|
|
39902
39931
|
return declared;
|
|
39903
39932
|
}
|
|
39933
|
+
function docBlockAt(text, offset) {
|
|
39934
|
+
const lineStartOf = (at) => text.lastIndexOf(`
|
|
39935
|
+
`, at - 1) + 1;
|
|
39936
|
+
const contentOf = (lineStart) => {
|
|
39937
|
+
let at = lineStart;
|
|
39938
|
+
while (text[at] === " " || text[at] === "\t") {
|
|
39939
|
+
at += 1;
|
|
39940
|
+
}
|
|
39941
|
+
return text.startsWith("##", at) ? at + 2 : null;
|
|
39942
|
+
};
|
|
39943
|
+
let top = lineStartOf(offset);
|
|
39944
|
+
if (contentOf(top) === null) {
|
|
39945
|
+
return null;
|
|
39946
|
+
}
|
|
39947
|
+
while (top > 0 && contentOf(lineStartOf(top - 1)) !== null) {
|
|
39948
|
+
top = lineStartOf(top - 1);
|
|
39949
|
+
}
|
|
39950
|
+
let joined = "";
|
|
39951
|
+
const from = [];
|
|
39952
|
+
for (let line = top;; ) {
|
|
39953
|
+
const content = contentOf(line);
|
|
39954
|
+
if (content === null) {
|
|
39955
|
+
break;
|
|
39956
|
+
}
|
|
39957
|
+
const nextBreak = text.indexOf(`
|
|
39958
|
+
`, line);
|
|
39959
|
+
let end = nextBreak < 0 ? text.length : nextBreak;
|
|
39960
|
+
if (text[end - 1] === "\r") {
|
|
39961
|
+
end -= 1;
|
|
39962
|
+
}
|
|
39963
|
+
if (joined.length > 0) {
|
|
39964
|
+
joined += " ";
|
|
39965
|
+
from.push(-1);
|
|
39966
|
+
}
|
|
39967
|
+
for (let at = content;at < end; at += 1) {
|
|
39968
|
+
joined += text[at];
|
|
39969
|
+
from.push(at);
|
|
39970
|
+
}
|
|
39971
|
+
if (nextBreak < 0) {
|
|
39972
|
+
break;
|
|
39973
|
+
}
|
|
39974
|
+
line = nextBreak + 1;
|
|
39975
|
+
}
|
|
39976
|
+
return { joined, from };
|
|
39977
|
+
}
|
|
39904
39978
|
function docLinkAt(text, offset) {
|
|
39905
|
-
const
|
|
39906
|
-
|
|
39907
|
-
|
|
39908
|
-
|
|
39909
|
-
|
|
39910
|
-
const
|
|
39911
|
-
const
|
|
39912
|
-
|
|
39979
|
+
const block = docBlockAt(text, offset);
|
|
39980
|
+
const at = block?.from.indexOf(offset) ?? -1;
|
|
39981
|
+
if (block === null || at < 0) {
|
|
39982
|
+
return null;
|
|
39983
|
+
}
|
|
39984
|
+
const { joined } = block;
|
|
39985
|
+
const open = joined.lastIndexOf("[", at);
|
|
39986
|
+
const close = joined.indexOf("]", at);
|
|
39987
|
+
if (open < 0 || close < 0) {
|
|
39913
39988
|
return null;
|
|
39914
39989
|
}
|
|
39915
|
-
const inside =
|
|
39990
|
+
const inside = joined.slice(open + 1, close);
|
|
39916
39991
|
if (inside.includes("[") || inside.includes("]")) {
|
|
39917
39992
|
return null;
|
|
39918
39993
|
}
|
|
39919
|
-
const match =
|
|
39994
|
+
const match = /^\s*(?:([a-z_]+)\s+)?([\p{L}_][\p{L}\p{N}_]*(?:\.[\p{L}_][\p{L}\p{N}_]*)*)\s*$/u.exec(inside);
|
|
39920
39995
|
if (match === null || match[1] !== undefined && !DOC_LINK_KINDS.has(match[1])) {
|
|
39921
39996
|
return null;
|
|
39922
39997
|
}
|
|
39923
|
-
const
|
|
39924
|
-
const
|
|
39925
|
-
let
|
|
39998
|
+
const path = match[2] ?? "";
|
|
39999
|
+
const parts = path.split(".");
|
|
40000
|
+
let partAt = open + 1 + inside.trimEnd().length - path.length;
|
|
39926
40001
|
for (const [index, part] of parts.entries()) {
|
|
39927
|
-
if (
|
|
40002
|
+
if (partAt === at) {
|
|
39928
40003
|
return { kind: match[1] ?? null, parts, index };
|
|
39929
40004
|
}
|
|
39930
|
-
|
|
40005
|
+
partAt += part.length + 1;
|
|
39931
40006
|
}
|
|
39932
40007
|
return null;
|
|
39933
40008
|
}
|
|
@@ -40325,6 +40400,33 @@ function planMove(args, plan) {
|
|
|
40325
40400
|
mentions: mentions.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line)
|
|
40326
40401
|
};
|
|
40327
40402
|
}
|
|
40403
|
+
function mentionIgnored(plan, ignored, name, movedFrom) {
|
|
40404
|
+
const mentions = [...plan.mentions];
|
|
40405
|
+
const why = "under a directory holding a .gdignore, which the engine does not load, so it was not changed";
|
|
40406
|
+
for (const file of ignored) {
|
|
40407
|
+
const lines = new Lines(file.text);
|
|
40408
|
+
const offsets = occurrencesOf(file.text, name, [{ kind: "code", start: 0, end: file.text.length }]).map((one) => one.offset);
|
|
40409
|
+
if (movedFrom !== null) {
|
|
40410
|
+
let at = file.text.indexOf(movedFrom);
|
|
40411
|
+
while (at >= 0) {
|
|
40412
|
+
offsets.push(at);
|
|
40413
|
+
at = file.text.indexOf(movedFrom, at + movedFrom.length);
|
|
40414
|
+
}
|
|
40415
|
+
}
|
|
40416
|
+
const seen = new Set;
|
|
40417
|
+
for (const offset of offsets) {
|
|
40418
|
+
const line = lines.positionOf(offset).line + 1;
|
|
40419
|
+
if (!seen.has(line)) {
|
|
40420
|
+
seen.add(line);
|
|
40421
|
+
mentions.push({ file: file.path, line, kind: "ignored", text: lines.lineAt(offset).trim(), why });
|
|
40422
|
+
}
|
|
40423
|
+
}
|
|
40424
|
+
}
|
|
40425
|
+
return {
|
|
40426
|
+
edits: plan.edits,
|
|
40427
|
+
mentions: mentions.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line)
|
|
40428
|
+
};
|
|
40429
|
+
}
|
|
40328
40430
|
function rewritesOf(plan, texts, move) {
|
|
40329
40431
|
const byPath = new Map(texts.map((file) => [file.path, file]));
|
|
40330
40432
|
const byFile = new Map;
|
|
@@ -40582,6 +40684,20 @@ async function renameSymbol(request, services) {
|
|
|
40582
40684
|
conflicts.push(`${newName} is already declared in ${id.endsWith("#") ? fileOf(id) : `${fileOf(id)}, class ${id.slice(id.lastIndexOf("#") + 1)}`}`);
|
|
40583
40685
|
}
|
|
40584
40686
|
}
|
|
40687
|
+
const heirs = graph.descendants(root);
|
|
40688
|
+
const inUse = [];
|
|
40689
|
+
for (const script of scripts.values()) {
|
|
40690
|
+
for (const occurrence of script.occurrences(newName)) {
|
|
40691
|
+
const body = graph.bodyAt(script.path, occurrence.offset);
|
|
40692
|
+
if (occurrence.kind === "code" && !occurrence.afterDot && heirs.has(body) && graph.declaration(body, newName)?.offset !== occurrence.offset) {
|
|
40693
|
+
inUse.push({ file: script.path, line: script.lines.positionOf(occurrence.offset).line + 1 });
|
|
40694
|
+
}
|
|
40695
|
+
}
|
|
40696
|
+
}
|
|
40697
|
+
if (inUse.length > 0) {
|
|
40698
|
+
const places = inUse.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line).slice(0, 10).map((place) => `${place.file}:${place.line}`);
|
|
40699
|
+
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` : ""}`);
|
|
40700
|
+
}
|
|
40585
40701
|
if (conflicts.length > 0) {
|
|
40586
40702
|
return {
|
|
40587
40703
|
ok: false,
|
|
@@ -40637,6 +40753,7 @@ async function renameSymbol(request, services) {
|
|
|
40637
40753
|
exists: (path) => existsSync18(`${projectPath}/${path.slice("res://".length)}`)
|
|
40638
40754
|
}, plan);
|
|
40639
40755
|
}
|
|
40756
|
+
plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? null : declaringPath);
|
|
40640
40757
|
const rewrites = rewritesOf(plan, texts, moveTo === null || request.newScriptPath === null ? null : { from: declaringPath, to: moveTo, absoluteTo: request.newScriptPath });
|
|
40641
40758
|
const graph = new ClassGraph(scripts, declared);
|
|
40642
40759
|
const rank = (path) => (declaringFiles.includes(path) ? 0 : 1000) + graph.ancestors(`${path}#`).length;
|
|
@@ -40652,7 +40769,7 @@ async function renameSymbol(request, services) {
|
|
|
40652
40769
|
...overrides.length > 0 ? { overridesRenamed: overrides } : {},
|
|
40653
40770
|
...moveTo === null ? {} : { moved: { from: declaringPath, to: moveTo } },
|
|
40654
40771
|
changed: changedReport(plan.edits, rewrites),
|
|
40655
|
-
|
|
40772
|
+
...leftAloneReport(plan.mentions),
|
|
40656
40773
|
summary: summaryOf(plan.edits, plan.mentions, rewrites)
|
|
40657
40774
|
}
|
|
40658
40775
|
};
|
|
@@ -40747,6 +40864,27 @@ function changedReport(edits, rewrites) {
|
|
|
40747
40864
|
};
|
|
40748
40865
|
});
|
|
40749
40866
|
}
|
|
40867
|
+
function leftAloneReport(mentions) {
|
|
40868
|
+
const full = [];
|
|
40869
|
+
const prose = new Map;
|
|
40870
|
+
for (const mention of mentions) {
|
|
40871
|
+
if ((mention.kind === "comment" || mention.kind === "text") && mention.why === undefined) {
|
|
40872
|
+
const key = `${mention.file}
|
|
40873
|
+
${mention.kind}`;
|
|
40874
|
+
const group = prose.get(key) ?? { file: mention.file, kind: mention.kind, lines: [] };
|
|
40875
|
+
if (!group.lines.includes(mention.line)) {
|
|
40876
|
+
group.lines.push(mention.line);
|
|
40877
|
+
}
|
|
40878
|
+
prose.set(key, group);
|
|
40879
|
+
} else {
|
|
40880
|
+
full.push(mention);
|
|
40881
|
+
}
|
|
40882
|
+
}
|
|
40883
|
+
return {
|
|
40884
|
+
leftAlone: full,
|
|
40885
|
+
...prose.size > 0 ? { leftAloneInProse: [...prose.values()] } : {}
|
|
40886
|
+
};
|
|
40887
|
+
}
|
|
40750
40888
|
function summaryOf(edits, mentions, rewrites) {
|
|
40751
40889
|
const count = (values) => {
|
|
40752
40890
|
const counted = {};
|
|
@@ -45892,7 +46030,18 @@ class GodotServer {
|
|
|
45892
46030
|
rebuildCache: outcome.isClass || outcome.moved !== null,
|
|
45893
46031
|
moved: outcome.moved !== null
|
|
45894
46032
|
});
|
|
45895
|
-
|
|
46033
|
+
const said = readString(settled.report, "note");
|
|
46034
|
+
const leftChanged = settled.ok ? said : [
|
|
46035
|
+
"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.",
|
|
46036
|
+
...said === undefined ? [] : [said]
|
|
46037
|
+
].join(" ");
|
|
46038
|
+
return this.jsonTextResponse({
|
|
46039
|
+
ok: settled.ok,
|
|
46040
|
+
written: true,
|
|
46041
|
+
...outcome.report,
|
|
46042
|
+
...settled.report,
|
|
46043
|
+
...leftChanged === undefined ? {} : { note: leftChanged }
|
|
46044
|
+
});
|
|
45896
46045
|
}
|
|
45897
46046
|
async settleRenameInEditor(projectPath, changed, names) {
|
|
45898
46047
|
const { gone, oldClass, newClass } = names;
|
|
@@ -47588,14 +47737,16 @@ formatting. \`script_diagnostics\` and \`script_info\` come from the editor's ow
|
|
|
47588
47737
|
\`project_export\`, \`project_test\`, \`script_edit\`, \`script_info structure\` and
|
|
47589
47738
|
\`editor_classes\` need nothing open: they run a short headless engine and are gone before the
|
|
47590
47739
|
answer is printed. The exception is \`script_edit rename\` of a method, variable, signal or other
|
|
47591
|
-
member, which asks the editor's language server which uses are that class's.
|
|
47740
|
+
member, which asks the editor's language server which uses are that class's. \`project_settings
|
|
47741
|
+
get\` is on that list too, reading as it does like a look at a config file: it starts an engine so
|
|
47742
|
+
that a setting nobody wrote down still answers with the default the engine registers for it.
|
|
47592
47743
|
|
|
47593
47744
|
To rename a class or a member across the project, use \`script_edit rename\` rather than a text
|
|
47594
47745
|
replacement: it changes the uses the engine resolves, doc links and what scenes record, and lists
|
|
47595
|
-
every other occurrence of the word
|
|
47596
|
-
|
|
47597
|
-
|
|
47598
|
-
|
|
47746
|
+
every other occurrence of the word for you to read: a \`find_children\` string or a file under a
|
|
47747
|
+
\`.gdignore\` directory under \`leftAlone\`, and a word in a comment or a text file by line under
|
|
47748
|
+
\`leftAloneInProse\`. Pass \`preview\` first on a large rename. If the answer has \`ok: false\`
|
|
47749
|
+
beside \`written: true\`, the files were changed and a script did not compile afterwards.
|
|
47599
47750
|
|
|
47600
47751
|
Editing a file while a game is running does not change the running game. \`auto_reload\` is an
|
|
47601
47752
|
editor plugin and reloads into the editor's own process; the game is a separate process holding
|
package/build/index.js
CHANGED
|
@@ -26268,6 +26268,7 @@ function wholeWord(name, flags = "gu") {
|
|
|
26268
26268
|
function occurrencesOf(source, name, regions = regionsOf(source)) {
|
|
26269
26269
|
const found = [];
|
|
26270
26270
|
let index = 0;
|
|
26271
|
+
let code = null;
|
|
26271
26272
|
for (const match of source.matchAll(wholeWord(name))) {
|
|
26272
26273
|
const offset = match.index;
|
|
26273
26274
|
while (index < regions.length - 1 && (regions[index]?.end ?? 0) <= offset) {
|
|
@@ -26277,16 +26278,17 @@ function occurrencesOf(source, name, regions = regionsOf(source)) {
|
|
|
26277
26278
|
let afterDot = false;
|
|
26278
26279
|
let declaredAs = null;
|
|
26279
26280
|
if (kind === "code") {
|
|
26281
|
+
code ??= codeOf(source, regions);
|
|
26280
26282
|
let back = offset - 1;
|
|
26281
|
-
while (back >= 0 && /[\s\\]/.test(
|
|
26283
|
+
while (back >= 0 && /[\s\\]/.test(code[back] ?? "")) {
|
|
26282
26284
|
back -= 1;
|
|
26283
26285
|
}
|
|
26284
|
-
afterDot =
|
|
26286
|
+
afterDot = code[back] === "." && code[back - 1] !== ".";
|
|
26285
26287
|
let wordStart = back + 1;
|
|
26286
|
-
while (wordStart > 0 && isIdentifierPart(
|
|
26288
|
+
while (wordStart > 0 && isIdentifierPart(code[wordStart - 1])) {
|
|
26287
26289
|
wordStart -= 1;
|
|
26288
26290
|
}
|
|
26289
|
-
const word =
|
|
26291
|
+
const word = code.slice(wordStart, back + 1);
|
|
26290
26292
|
declaredAs = DECLARING.has(word) ? word : null;
|
|
26291
26293
|
}
|
|
26292
26294
|
found.push({ offset, kind, afterDot, declaredAs });
|
|
@@ -30405,6 +30407,33 @@ function projectTexts(projectPath) {
|
|
|
30405
30407
|
visit(projectPath, "");
|
|
30406
30408
|
return found;
|
|
30407
30409
|
}
|
|
30410
|
+
function ignoredTexts(projectPath) {
|
|
30411
|
+
const found = [];
|
|
30412
|
+
const visit = (directory, prefix, ignored) => {
|
|
30413
|
+
const under = ignored || steppedOver(directory);
|
|
30414
|
+
for (const entry of readdirSync5(directory, { withFileTypes: true })) {
|
|
30415
|
+
if (skipped(entry.name)) {
|
|
30416
|
+
continue;
|
|
30417
|
+
}
|
|
30418
|
+
const absolute = join15(directory, entry.name);
|
|
30419
|
+
const spelled = `${prefix}${entry.name}`;
|
|
30420
|
+
if (entry.isDirectory()) {
|
|
30421
|
+
visit(absolute, `${spelled}/`, under);
|
|
30422
|
+
continue;
|
|
30423
|
+
}
|
|
30424
|
+
const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
30425
|
+
if (!under || !entry.isFile() || BINARY_EXTENSIONS.has(extension) || entry.name === ".gdignore") {
|
|
30426
|
+
continue;
|
|
30427
|
+
}
|
|
30428
|
+
const bytes = readFileSync13(absolute);
|
|
30429
|
+
if (!isBinary(bytes)) {
|
|
30430
|
+
found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
|
|
30431
|
+
}
|
|
30432
|
+
}
|
|
30433
|
+
};
|
|
30434
|
+
visit(projectPath, "", false);
|
|
30435
|
+
return found;
|
|
30436
|
+
}
|
|
30408
30437
|
function searchProject(projectPath, options) {
|
|
30409
30438
|
const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
|
|
30410
30439
|
const result = {
|
|
@@ -30613,33 +30642,79 @@ var DOC_LINK_KINDS = new Set([
|
|
|
30613
30642
|
"annotation",
|
|
30614
30643
|
"theme_item"
|
|
30615
30644
|
]);
|
|
30645
|
+
function docBlockAt(text, offset) {
|
|
30646
|
+
const lineStartOf = (at) => text.lastIndexOf(`
|
|
30647
|
+
`, at - 1) + 1;
|
|
30648
|
+
const contentOf = (lineStart) => {
|
|
30649
|
+
let at = lineStart;
|
|
30650
|
+
while (text[at] === " " || text[at] === "\t") {
|
|
30651
|
+
at += 1;
|
|
30652
|
+
}
|
|
30653
|
+
return text.startsWith("##", at) ? at + 2 : null;
|
|
30654
|
+
};
|
|
30655
|
+
let top = lineStartOf(offset);
|
|
30656
|
+
if (contentOf(top) === null) {
|
|
30657
|
+
return null;
|
|
30658
|
+
}
|
|
30659
|
+
while (top > 0 && contentOf(lineStartOf(top - 1)) !== null) {
|
|
30660
|
+
top = lineStartOf(top - 1);
|
|
30661
|
+
}
|
|
30662
|
+
let joined = "";
|
|
30663
|
+
const from = [];
|
|
30664
|
+
for (let line = top;; ) {
|
|
30665
|
+
const content = contentOf(line);
|
|
30666
|
+
if (content === null) {
|
|
30667
|
+
break;
|
|
30668
|
+
}
|
|
30669
|
+
const nextBreak = text.indexOf(`
|
|
30670
|
+
`, line);
|
|
30671
|
+
let end = nextBreak < 0 ? text.length : nextBreak;
|
|
30672
|
+
if (text[end - 1] === "\r") {
|
|
30673
|
+
end -= 1;
|
|
30674
|
+
}
|
|
30675
|
+
if (joined.length > 0) {
|
|
30676
|
+
joined += " ";
|
|
30677
|
+
from.push(-1);
|
|
30678
|
+
}
|
|
30679
|
+
for (let at = content;at < end; at += 1) {
|
|
30680
|
+
joined += text[at];
|
|
30681
|
+
from.push(at);
|
|
30682
|
+
}
|
|
30683
|
+
if (nextBreak < 0) {
|
|
30684
|
+
break;
|
|
30685
|
+
}
|
|
30686
|
+
line = nextBreak + 1;
|
|
30687
|
+
}
|
|
30688
|
+
return { joined, from };
|
|
30689
|
+
}
|
|
30616
30690
|
function docLinkAt(text, offset) {
|
|
30617
|
-
const
|
|
30618
|
-
|
|
30619
|
-
|
|
30620
|
-
|
|
30621
|
-
|
|
30622
|
-
const
|
|
30623
|
-
const
|
|
30624
|
-
|
|
30691
|
+
const block = docBlockAt(text, offset);
|
|
30692
|
+
const at = block?.from.indexOf(offset) ?? -1;
|
|
30693
|
+
if (block === null || at < 0) {
|
|
30694
|
+
return null;
|
|
30695
|
+
}
|
|
30696
|
+
const { joined } = block;
|
|
30697
|
+
const open = joined.lastIndexOf("[", at);
|
|
30698
|
+
const close = joined.indexOf("]", at);
|
|
30699
|
+
if (open < 0 || close < 0) {
|
|
30625
30700
|
return null;
|
|
30626
30701
|
}
|
|
30627
|
-
const inside =
|
|
30702
|
+
const inside = joined.slice(open + 1, close);
|
|
30628
30703
|
if (inside.includes("[") || inside.includes("]")) {
|
|
30629
30704
|
return null;
|
|
30630
30705
|
}
|
|
30631
|
-
const match =
|
|
30706
|
+
const match = /^\s*(?:([a-z_]+)\s+)?([\p{L}_][\p{L}\p{N}_]*(?:\.[\p{L}_][\p{L}\p{N}_]*)*)\s*$/u.exec(inside);
|
|
30632
30707
|
if (match === null || match[1] !== undefined && !DOC_LINK_KINDS.has(match[1])) {
|
|
30633
30708
|
return null;
|
|
30634
30709
|
}
|
|
30635
|
-
const
|
|
30636
|
-
const
|
|
30637
|
-
let
|
|
30710
|
+
const path = match[2] ?? "";
|
|
30711
|
+
const parts = path.split(".");
|
|
30712
|
+
let partAt = open + 1 + inside.trimEnd().length - path.length;
|
|
30638
30713
|
for (const [index, part] of parts.entries()) {
|
|
30639
|
-
if (
|
|
30714
|
+
if (partAt === at) {
|
|
30640
30715
|
return { kind: match[1] ?? null, parts, index };
|
|
30641
30716
|
}
|
|
30642
|
-
|
|
30717
|
+
partAt += part.length + 1;
|
|
30643
30718
|
}
|
|
30644
30719
|
return null;
|
|
30645
30720
|
}
|
|
@@ -31046,6 +31121,33 @@ function planMove(args, plan) {
|
|
|
31046
31121
|
mentions: mentions.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line)
|
|
31047
31122
|
};
|
|
31048
31123
|
}
|
|
31124
|
+
function mentionIgnored(plan, ignored, name, movedFrom) {
|
|
31125
|
+
const mentions = [...plan.mentions];
|
|
31126
|
+
const why = "under a directory holding a .gdignore, which the engine does not load, so it was not changed";
|
|
31127
|
+
for (const file of ignored) {
|
|
31128
|
+
const lines = new Lines(file.text);
|
|
31129
|
+
const offsets = occurrencesOf(file.text, name, [{ kind: "code", start: 0, end: file.text.length }]).map((one) => one.offset);
|
|
31130
|
+
if (movedFrom !== null) {
|
|
31131
|
+
let at = file.text.indexOf(movedFrom);
|
|
31132
|
+
while (at >= 0) {
|
|
31133
|
+
offsets.push(at);
|
|
31134
|
+
at = file.text.indexOf(movedFrom, at + movedFrom.length);
|
|
31135
|
+
}
|
|
31136
|
+
}
|
|
31137
|
+
const seen = new Set;
|
|
31138
|
+
for (const offset of offsets) {
|
|
31139
|
+
const line = lines.positionOf(offset).line + 1;
|
|
31140
|
+
if (!seen.has(line)) {
|
|
31141
|
+
seen.add(line);
|
|
31142
|
+
mentions.push({ file: file.path, line, kind: "ignored", text: lines.lineAt(offset).trim(), why });
|
|
31143
|
+
}
|
|
31144
|
+
}
|
|
31145
|
+
}
|
|
31146
|
+
return {
|
|
31147
|
+
edits: plan.edits,
|
|
31148
|
+
mentions: mentions.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line)
|
|
31149
|
+
};
|
|
31150
|
+
}
|
|
31049
31151
|
function rewritesOf(plan, texts, move) {
|
|
31050
31152
|
const byPath = new Map(texts.map((file) => [file.path, file]));
|
|
31051
31153
|
const byFile = new Map;
|
|
@@ -31548,6 +31650,20 @@ async function renameSymbol(request, services) {
|
|
|
31548
31650
|
conflicts.push(`${newName} is already declared in ${id.endsWith("#") ? fileOf(id) : `${fileOf(id)}, class ${id.slice(id.lastIndexOf("#") + 1)}`}`);
|
|
31549
31651
|
}
|
|
31550
31652
|
}
|
|
31653
|
+
const heirs = graph.descendants(root);
|
|
31654
|
+
const inUse = [];
|
|
31655
|
+
for (const script of scripts.values()) {
|
|
31656
|
+
for (const occurrence of script.occurrences(newName)) {
|
|
31657
|
+
const body = graph.bodyAt(script.path, occurrence.offset);
|
|
31658
|
+
if (occurrence.kind === "code" && !occurrence.afterDot && heirs.has(body) && graph.declaration(body, newName)?.offset !== occurrence.offset) {
|
|
31659
|
+
inUse.push({ file: script.path, line: script.lines.positionOf(occurrence.offset).line + 1 });
|
|
31660
|
+
}
|
|
31661
|
+
}
|
|
31662
|
+
}
|
|
31663
|
+
if (inUse.length > 0) {
|
|
31664
|
+
const places = inUse.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line).slice(0, 10).map((place) => `${place.file}:${place.line}`);
|
|
31665
|
+
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` : ""}`);
|
|
31666
|
+
}
|
|
31551
31667
|
if (conflicts.length > 0) {
|
|
31552
31668
|
return {
|
|
31553
31669
|
ok: false,
|
|
@@ -31603,6 +31719,7 @@ async function renameSymbol(request, services) {
|
|
|
31603
31719
|
exists: (path) => existsSync14(`${projectPath}/${path.slice("res://".length)}`)
|
|
31604
31720
|
}, plan);
|
|
31605
31721
|
}
|
|
31722
|
+
plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? null : declaringPath);
|
|
31606
31723
|
const rewrites = rewritesOf(plan, texts, moveTo === null || request.newScriptPath === null ? null : { from: declaringPath, to: moveTo, absoluteTo: request.newScriptPath });
|
|
31607
31724
|
const graph = new ClassGraph(scripts, declared);
|
|
31608
31725
|
const rank = (path) => (declaringFiles.includes(path) ? 0 : 1000) + graph.ancestors(`${path}#`).length;
|
|
@@ -31618,7 +31735,7 @@ async function renameSymbol(request, services) {
|
|
|
31618
31735
|
...overrides.length > 0 ? { overridesRenamed: overrides } : {},
|
|
31619
31736
|
...moveTo === null ? {} : { moved: { from: declaringPath, to: moveTo } },
|
|
31620
31737
|
changed: changedReport(plan.edits, rewrites),
|
|
31621
|
-
|
|
31738
|
+
...leftAloneReport(plan.mentions),
|
|
31622
31739
|
summary: summaryOf(plan.edits, plan.mentions, rewrites)
|
|
31623
31740
|
}
|
|
31624
31741
|
};
|
|
@@ -31713,6 +31830,27 @@ function changedReport(edits, rewrites) {
|
|
|
31713
31830
|
};
|
|
31714
31831
|
});
|
|
31715
31832
|
}
|
|
31833
|
+
function leftAloneReport(mentions) {
|
|
31834
|
+
const full = [];
|
|
31835
|
+
const prose = new Map;
|
|
31836
|
+
for (const mention of mentions) {
|
|
31837
|
+
if ((mention.kind === "comment" || mention.kind === "text") && mention.why === undefined) {
|
|
31838
|
+
const key = `${mention.file}
|
|
31839
|
+
${mention.kind}`;
|
|
31840
|
+
const group = prose.get(key) ?? { file: mention.file, kind: mention.kind, lines: [] };
|
|
31841
|
+
if (!group.lines.includes(mention.line)) {
|
|
31842
|
+
group.lines.push(mention.line);
|
|
31843
|
+
}
|
|
31844
|
+
prose.set(key, group);
|
|
31845
|
+
} else {
|
|
31846
|
+
full.push(mention);
|
|
31847
|
+
}
|
|
31848
|
+
}
|
|
31849
|
+
return {
|
|
31850
|
+
leftAlone: full,
|
|
31851
|
+
...prose.size > 0 ? { leftAloneInProse: [...prose.values()] } : {}
|
|
31852
|
+
};
|
|
31853
|
+
}
|
|
31716
31854
|
function summaryOf(edits, mentions, rewrites) {
|
|
31717
31855
|
const count = (values) => {
|
|
31718
31856
|
const counted = {};
|
|
@@ -32899,7 +33037,7 @@ var TOOL_SPECS = [
|
|
|
32899
33037
|
},
|
|
32900
33038
|
{
|
|
32901
33039
|
name: "script_edit",
|
|
32902
|
-
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
|
|
33040
|
+
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 find_children string, a node named like the class, a call on a value whose type the analyser could not tell. 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.",
|
|
32903
33041
|
parameters: {
|
|
32904
33042
|
projectPath: PROJECT_PATH,
|
|
32905
33043
|
scriptPath: SCRIPT_PATH,
|
|
@@ -38464,7 +38602,18 @@ class GodotServer {
|
|
|
38464
38602
|
rebuildCache: outcome.isClass || outcome.moved !== null,
|
|
38465
38603
|
moved: outcome.moved !== null
|
|
38466
38604
|
});
|
|
38467
|
-
|
|
38605
|
+
const said = readString(settled.report, "note");
|
|
38606
|
+
const leftChanged = settled.ok ? said : [
|
|
38607
|
+
"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.",
|
|
38608
|
+
...said === undefined ? [] : [said]
|
|
38609
|
+
].join(" ");
|
|
38610
|
+
return this.jsonTextResponse({
|
|
38611
|
+
ok: settled.ok,
|
|
38612
|
+
written: true,
|
|
38613
|
+
...outcome.report,
|
|
38614
|
+
...settled.report,
|
|
38615
|
+
...leftChanged === undefined ? {} : { note: leftChanged }
|
|
38616
|
+
});
|
|
38468
38617
|
}
|
|
38469
38618
|
async settleRenameInEditor(projectPath, changed, names) {
|
|
38470
38619
|
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.34",
|
|
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",
|