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.
Files changed (3) hide show
  1. package/build/cli.js +178 -27
  2. package/build/index.js +171 -22
  3. 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(source[back] ?? "")) {
8907
+ while (back >= 0 && /[\s\\]/.test(code[back] ?? "")) {
8906
8908
  back -= 1;
8907
8909
  }
8908
- afterDot = source[back] === "." && source[back - 1] !== ".";
8910
+ afterDot = code[back] === "." && code[back - 1] !== ".";
8909
8911
  let wordStart = back + 1;
8910
- while (wordStart > 0 && isIdentifierPart(source[wordStart - 1])) {
8912
+ while (wordStart > 0 && isIdentifierPart(code[wordStart - 1])) {
8911
8913
  wordStart -= 1;
8912
8914
  }
8913
- const word = source.slice(wordStart, back + 1);
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: 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 or text) and, where it is not plain, why: a find_children string, a word in a comment, a node named like the class, a call on a value whose type the analyser could not tell. Read leftAlone before calling the rename done; nothing in it 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.",
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 lineStart = text.lastIndexOf(`
39906
- `, offset - 1) + 1;
39907
- const nextBreak = text.indexOf(`
39908
- `, offset);
39909
- const lineEnd = nextBreak < 0 ? text.length : nextBreak;
39910
- const open = text.lastIndexOf("[", offset);
39911
- const close = text.indexOf("]", offset);
39912
- if (open < lineStart || close === -1 || close > lineEnd) {
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 = text.slice(open + 1, close);
39990
+ const inside = joined.slice(open + 1, close);
39916
39991
  if (inside.includes("[") || inside.includes("]")) {
39917
39992
  return null;
39918
39993
  }
39919
- const match = /^(?:([a-z_]+)\s+)?([\p{L}_][\p{L}\p{N}_]*(?:\.[\p{L}_][\p{L}\p{N}_]*)*)$/u.exec(inside);
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 parts = (match[2] ?? "").split(".");
39924
- const pathStart = open + 1 + inside.length - (match[2] ?? "").length;
39925
- let at = pathStart;
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 (at === offset) {
40002
+ if (partAt === at) {
39928
40003
  return { kind: match[1] ?? null, parts, index };
39929
40004
  }
39930
- at += part.length + 1;
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
- leftAlone: plan.mentions,
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
- return this.jsonTextResponse({ ok: settled.ok, ...outcome.report, ...settled.report });
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 under \`leftAlone\`, a \`find_children\` string or a word in a
47596
- comment, for you to read. Pass \`preview\` first on a large rename. \`project_settings get\` is on that list too, reading as it does like a look at
47597
- a config file: it starts an engine so that a setting nobody wrote down still answers with the
47598
- default the engine registers for it.
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(source[back] ?? "")) {
26283
+ while (back >= 0 && /[\s\\]/.test(code[back] ?? "")) {
26282
26284
  back -= 1;
26283
26285
  }
26284
- afterDot = source[back] === "." && source[back - 1] !== ".";
26286
+ afterDot = code[back] === "." && code[back - 1] !== ".";
26285
26287
  let wordStart = back + 1;
26286
- while (wordStart > 0 && isIdentifierPart(source[wordStart - 1])) {
26288
+ while (wordStart > 0 && isIdentifierPart(code[wordStart - 1])) {
26287
26289
  wordStart -= 1;
26288
26290
  }
26289
- const word = source.slice(wordStart, back + 1);
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 lineStart = text.lastIndexOf(`
30618
- `, offset - 1) + 1;
30619
- const nextBreak = text.indexOf(`
30620
- `, offset);
30621
- const lineEnd = nextBreak < 0 ? text.length : nextBreak;
30622
- const open = text.lastIndexOf("[", offset);
30623
- const close = text.indexOf("]", offset);
30624
- if (open < lineStart || close === -1 || close > lineEnd) {
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 = text.slice(open + 1, close);
30702
+ const inside = joined.slice(open + 1, close);
30628
30703
  if (inside.includes("[") || inside.includes("]")) {
30629
30704
  return null;
30630
30705
  }
30631
- const match = /^(?:([a-z_]+)\s+)?([\p{L}_][\p{L}\p{N}_]*(?:\.[\p{L}_][\p{L}\p{N}_]*)*)$/u.exec(inside);
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 parts = (match[2] ?? "").split(".");
30636
- const pathStart = open + 1 + inside.length - (match[2] ?? "").length;
30637
- let at = pathStart;
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 (at === offset) {
30714
+ if (partAt === at) {
30640
30715
  return { kind: match[1] ?? null, parts, index };
30641
30716
  }
30642
- at += part.length + 1;
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
- leftAlone: plan.mentions,
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: 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 or text) and, where it is not plain, why: a find_children string, a word in a comment, a node named like the class, a call on a value whose type the analyser could not tell. Read leftAlone before calling the rename done; nothing in it 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.",
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
- return this.jsonTextResponse({ ok: settled.ok, ...outcome.report, ...settled.report });
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.32",
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",