gdharness 1.1.30 → 1.1.32
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 +1950 -157
- package/build/godot/addons/gdharness_editor/bridge_client.gd +6 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +9 -0
- package/build/godot/operations/godot_operations.gd +5 -0
- package/build/godot/operations/rename_checks.gd +80 -0
- package/build/index.js +1989 -213
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -384,9 +384,18 @@ function readCommandLine(command) {
|
|
|
384
384
|
return {
|
|
385
385
|
executable,
|
|
386
386
|
projectPath,
|
|
387
|
-
editor: words.some((word) => word === "-e" || word === "--editor")
|
|
387
|
+
editor: words.some((word) => word === "-e" || word === "--editor"),
|
|
388
|
+
oneShot: words.find((word) => ONE_SHOT_OPTIONS.includes(word)) ?? null
|
|
388
389
|
};
|
|
389
390
|
}
|
|
391
|
+
async function oneShotOptionOf(pid) {
|
|
392
|
+
const tree = await processTree();
|
|
393
|
+
const listed = tree?.get(pid);
|
|
394
|
+
if (listed === undefined || listed.command === "") {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
return readCommandLine(listed.command).oneShot;
|
|
398
|
+
}
|
|
390
399
|
function commandWords(command) {
|
|
391
400
|
const words = [];
|
|
392
401
|
let word = "";
|
|
@@ -422,10 +431,31 @@ async function askPosix() {
|
|
|
422
431
|
});
|
|
423
432
|
return stdout;
|
|
424
433
|
}
|
|
425
|
-
var run, ASK_TIMEOUT_MS = 15000, lastListingFailure;
|
|
434
|
+
var run, ASK_TIMEOUT_MS = 15000, lastListingFailure, ONE_SHOT_OPTIONS;
|
|
426
435
|
var init_process_children = __esm(() => {
|
|
427
436
|
init_windows_ask();
|
|
428
437
|
run = promisify(execFile);
|
|
438
|
+
ONE_SHOT_OPTIONS = [
|
|
439
|
+
"--import",
|
|
440
|
+
"--quit",
|
|
441
|
+
"--quit-after",
|
|
442
|
+
"--export-release",
|
|
443
|
+
"--export-debug",
|
|
444
|
+
"--export-pack",
|
|
445
|
+
"--export-patch",
|
|
446
|
+
"--doctool",
|
|
447
|
+
"--gdscript-docs",
|
|
448
|
+
"--script",
|
|
449
|
+
"-s",
|
|
450
|
+
"--check-only",
|
|
451
|
+
"--build-solutions",
|
|
452
|
+
"--dump-gdextension-interface",
|
|
453
|
+
"--dump-extension-api",
|
|
454
|
+
"--dump-extension-api-with-docs",
|
|
455
|
+
"--validate-extension-api",
|
|
456
|
+
"--convert-3to4",
|
|
457
|
+
"--validate-conversion-3to4"
|
|
458
|
+
];
|
|
429
459
|
});
|
|
430
460
|
|
|
431
461
|
// src/bridge-announce.ts
|
|
@@ -8703,6 +8733,396 @@ var init_issues = __esm(() => {
|
|
|
8703
8733
|
ENHANCEMENT_URL = `${NEW_ISSUE}?template=feature_request.md`;
|
|
8704
8734
|
});
|
|
8705
8735
|
|
|
8736
|
+
// src/gdscript-source.ts
|
|
8737
|
+
function isIdentifierStart(character) {
|
|
8738
|
+
return character !== undefined && IDENTIFIER_START.test(character);
|
|
8739
|
+
}
|
|
8740
|
+
function isIdentifierPart(character) {
|
|
8741
|
+
return character !== undefined && IDENTIFIER_PART.test(character);
|
|
8742
|
+
}
|
|
8743
|
+
function percentIsUniqueNode(source, at) {
|
|
8744
|
+
const next = source[at + 1];
|
|
8745
|
+
if (!isIdentifierStart(next) && next !== '"' && next !== "'") {
|
|
8746
|
+
return false;
|
|
8747
|
+
}
|
|
8748
|
+
let back = at - 1;
|
|
8749
|
+
while (back >= 0 && (source[back] === " " || source[back] === "\t")) {
|
|
8750
|
+
back -= 1;
|
|
8751
|
+
}
|
|
8752
|
+
const before = source[back];
|
|
8753
|
+
if (before === undefined || before === `
|
|
8754
|
+
`) {
|
|
8755
|
+
return true;
|
|
8756
|
+
}
|
|
8757
|
+
if (isIdentifierPart(before) || before === ")" || before === "]" || before === '"' || before === "'") {
|
|
8758
|
+
let wordStart = back;
|
|
8759
|
+
while (wordStart > 0 && isIdentifierPart(source[wordStart - 1])) {
|
|
8760
|
+
wordStart -= 1;
|
|
8761
|
+
}
|
|
8762
|
+
const word = source.slice(wordStart, back + 1);
|
|
8763
|
+
return ["return", "and", "or", "not", "in", "is", "as", "if", "elif", "else", "while", "await"].includes(word);
|
|
8764
|
+
}
|
|
8765
|
+
return true;
|
|
8766
|
+
}
|
|
8767
|
+
function stringEnd(source, at) {
|
|
8768
|
+
const quote = source[at] ?? '"';
|
|
8769
|
+
const fence = source.startsWith(quote.repeat(3), at) ? quote.repeat(3) : quote;
|
|
8770
|
+
let end = at + fence.length;
|
|
8771
|
+
while (end < source.length && !source.startsWith(fence, end)) {
|
|
8772
|
+
if (fence.length === 1 && source[end] === `
|
|
8773
|
+
`) {
|
|
8774
|
+
return end;
|
|
8775
|
+
}
|
|
8776
|
+
end += source[end] === "\\" ? 2 : 1;
|
|
8777
|
+
}
|
|
8778
|
+
return Math.min(end + fence.length, source.length);
|
|
8779
|
+
}
|
|
8780
|
+
function regionsOf(source) {
|
|
8781
|
+
const regions = [];
|
|
8782
|
+
let codeStart = 0;
|
|
8783
|
+
let at = 0;
|
|
8784
|
+
const close = (kind, start, end) => {
|
|
8785
|
+
if (codeStart < start) {
|
|
8786
|
+
regions.push({ kind: "code", start: codeStart, end: start });
|
|
8787
|
+
}
|
|
8788
|
+
regions.push({ kind, start, end });
|
|
8789
|
+
codeStart = end;
|
|
8790
|
+
at = end;
|
|
8791
|
+
};
|
|
8792
|
+
while (at < source.length) {
|
|
8793
|
+
const here = source[at];
|
|
8794
|
+
if (here === "#") {
|
|
8795
|
+
const lineEnd = source.indexOf(`
|
|
8796
|
+
`, at);
|
|
8797
|
+
const end = lineEnd === -1 ? source.length : lineEnd;
|
|
8798
|
+
const lineStart = source.lastIndexOf(`
|
|
8799
|
+
`, at - 1) + 1;
|
|
8800
|
+
const alone = source.slice(lineStart, at).trim() === "";
|
|
8801
|
+
close(alone && source.startsWith("##", at) ? "doc" : "comment", at, end);
|
|
8802
|
+
continue;
|
|
8803
|
+
}
|
|
8804
|
+
if (here === '"' || here === "'") {
|
|
8805
|
+
const prefixed = at > 0 && ["r", "&", "^"].includes(source[at - 1] ?? "") && !isIdentifierPart(source[at - 2]);
|
|
8806
|
+
const start = prefixed ? at - 1 : at;
|
|
8807
|
+
const dollar = source[start - 1] === "$" || source[start - 1] === "%" && percentIsUniqueNode(source, start - 1);
|
|
8808
|
+
close(dollar ? "nodePath" : "string", dollar ? start - 1 : start, stringEnd(source, at));
|
|
8809
|
+
continue;
|
|
8810
|
+
}
|
|
8811
|
+
if (here === "$" || here === "%" && percentIsUniqueNode(source, at)) {
|
|
8812
|
+
if (source[at + 1] === '"' || source[at + 1] === "'") {
|
|
8813
|
+
at += 1;
|
|
8814
|
+
continue;
|
|
8815
|
+
}
|
|
8816
|
+
let end = at + 1;
|
|
8817
|
+
while (end < source.length && (isIdentifierPart(source[end]) || source[end] === "/" || source[end] === "%")) {
|
|
8818
|
+
end += 1;
|
|
8819
|
+
}
|
|
8820
|
+
if (end > at + 1) {
|
|
8821
|
+
close("nodePath", at, end);
|
|
8822
|
+
continue;
|
|
8823
|
+
}
|
|
8824
|
+
}
|
|
8825
|
+
if (isIdentifierStart(here)) {
|
|
8826
|
+
while (at < source.length && isIdentifierPart(source[at])) {
|
|
8827
|
+
at += 1;
|
|
8828
|
+
}
|
|
8829
|
+
continue;
|
|
8830
|
+
}
|
|
8831
|
+
at += 1;
|
|
8832
|
+
}
|
|
8833
|
+
if (codeStart < source.length) {
|
|
8834
|
+
regions.push({ kind: "code", start: codeStart, end: source.length });
|
|
8835
|
+
}
|
|
8836
|
+
return regions;
|
|
8837
|
+
}
|
|
8838
|
+
function codeOf(source, regions = regionsOf(source)) {
|
|
8839
|
+
let code = "";
|
|
8840
|
+
for (const region of regions) {
|
|
8841
|
+
const text = source.slice(region.start, region.end);
|
|
8842
|
+
code += region.kind === "code" ? text : text.replace(/[^\n]/g, " ");
|
|
8843
|
+
}
|
|
8844
|
+
return code;
|
|
8845
|
+
}
|
|
8846
|
+
|
|
8847
|
+
class Lines {
|
|
8848
|
+
starts = [0];
|
|
8849
|
+
text;
|
|
8850
|
+
constructor(text) {
|
|
8851
|
+
this.text = text;
|
|
8852
|
+
for (let at = text.indexOf(`
|
|
8853
|
+
`);at !== -1; at = text.indexOf(`
|
|
8854
|
+
`, at + 1)) {
|
|
8855
|
+
this.starts.push(at + 1);
|
|
8856
|
+
}
|
|
8857
|
+
}
|
|
8858
|
+
positionOf(offset) {
|
|
8859
|
+
let low = 0;
|
|
8860
|
+
let high = this.starts.length - 1;
|
|
8861
|
+
while (low < high) {
|
|
8862
|
+
const middle = low + high + 1 >> 1;
|
|
8863
|
+
if ((this.starts[middle] ?? 0) <= offset) {
|
|
8864
|
+
low = middle;
|
|
8865
|
+
} else {
|
|
8866
|
+
high = middle - 1;
|
|
8867
|
+
}
|
|
8868
|
+
}
|
|
8869
|
+
return { line: low, character: offset - (this.starts[low] ?? 0) };
|
|
8870
|
+
}
|
|
8871
|
+
offsetOf(position) {
|
|
8872
|
+
const start = this.starts[position.line];
|
|
8873
|
+
if (start === undefined) {
|
|
8874
|
+
return null;
|
|
8875
|
+
}
|
|
8876
|
+
const offset = start + position.character;
|
|
8877
|
+
return offset > this.text.length ? null : offset;
|
|
8878
|
+
}
|
|
8879
|
+
lineAt(offset) {
|
|
8880
|
+
const { line } = this.positionOf(offset);
|
|
8881
|
+
const start = this.starts[line] ?? 0;
|
|
8882
|
+
const end = this.starts[line + 1] ?? this.text.length + 1;
|
|
8883
|
+
return this.text.slice(start, end - 1).replace(/\r$/, "");
|
|
8884
|
+
}
|
|
8885
|
+
}
|
|
8886
|
+
function escapedForPattern(text) {
|
|
8887
|
+
return text.replace(/[\\^$.*+?()[\]{}|/]/g, "\\$&");
|
|
8888
|
+
}
|
|
8889
|
+
function wholeWord(name, flags = "gu") {
|
|
8890
|
+
return new RegExp(`(?<![\\p{L}\\p{N}_])${escapedForPattern(name)}(?![\\p{L}\\p{N}_])`, flags);
|
|
8891
|
+
}
|
|
8892
|
+
function occurrencesOf(source, name, regions = regionsOf(source)) {
|
|
8893
|
+
const found = [];
|
|
8894
|
+
let index = 0;
|
|
8895
|
+
for (const match of source.matchAll(wholeWord(name))) {
|
|
8896
|
+
const offset = match.index;
|
|
8897
|
+
while (index < regions.length - 1 && (regions[index]?.end ?? 0) <= offset) {
|
|
8898
|
+
index += 1;
|
|
8899
|
+
}
|
|
8900
|
+
const kind = regions[index]?.kind ?? "code";
|
|
8901
|
+
let afterDot = false;
|
|
8902
|
+
let declaredAs = null;
|
|
8903
|
+
if (kind === "code") {
|
|
8904
|
+
let back = offset - 1;
|
|
8905
|
+
while (back >= 0 && /[\s\\]/.test(source[back] ?? "")) {
|
|
8906
|
+
back -= 1;
|
|
8907
|
+
}
|
|
8908
|
+
afterDot = source[back] === "." && source[back - 1] !== ".";
|
|
8909
|
+
let wordStart = back + 1;
|
|
8910
|
+
while (wordStart > 0 && isIdentifierPart(source[wordStart - 1])) {
|
|
8911
|
+
wordStart -= 1;
|
|
8912
|
+
}
|
|
8913
|
+
const word = source.slice(wordStart, back + 1);
|
|
8914
|
+
declaredAs = DECLARING.has(word) ? word : null;
|
|
8915
|
+
}
|
|
8916
|
+
found.push({ offset, kind, afterDot, declaredAs });
|
|
8917
|
+
}
|
|
8918
|
+
return found;
|
|
8919
|
+
}
|
|
8920
|
+
function extendsTarget(source, code, after) {
|
|
8921
|
+
let from = after;
|
|
8922
|
+
while (source[from] === " " || source[from] === "\t") {
|
|
8923
|
+
from += 1;
|
|
8924
|
+
}
|
|
8925
|
+
const quote = source[from];
|
|
8926
|
+
if (quote === '"' || quote === "'") {
|
|
8927
|
+
const end = source.indexOf(quote, from + 1);
|
|
8928
|
+
return end === -1 ? null : source.slice(from, end + 1);
|
|
8929
|
+
}
|
|
8930
|
+
const dotted = new RegExp(`^${NAME}(?:\\.${NAME})*`, "u").exec(code.slice(from));
|
|
8931
|
+
return dotted?.[0] ?? null;
|
|
8932
|
+
}
|
|
8933
|
+
function shapeOf(source) {
|
|
8934
|
+
const regions = regionsOf(source);
|
|
8935
|
+
const code = codeOf(source, regions);
|
|
8936
|
+
const lines = [];
|
|
8937
|
+
let start = 0;
|
|
8938
|
+
for (const text of code.split(`
|
|
8939
|
+
`)) {
|
|
8940
|
+
const indent = /^[ \t]*/.exec(text)?.[0].length ?? 0;
|
|
8941
|
+
lines.push({ start, text, indent });
|
|
8942
|
+
start += text.length + 1;
|
|
8943
|
+
}
|
|
8944
|
+
let className = null;
|
|
8945
|
+
const parseBody = (name, extendsAs, from, indent, bodyStart) => {
|
|
8946
|
+
const declarations = [];
|
|
8947
|
+
const inner = [];
|
|
8948
|
+
let index = from;
|
|
8949
|
+
let openEnum = null;
|
|
8950
|
+
let topExtends = extendsAs;
|
|
8951
|
+
while (index < lines.length) {
|
|
8952
|
+
const line = lines[index];
|
|
8953
|
+
if (line === undefined) {
|
|
8954
|
+
break;
|
|
8955
|
+
}
|
|
8956
|
+
if (openEnum !== null) {
|
|
8957
|
+
openEnum.text += `
|
|
8958
|
+
${line.text}`;
|
|
8959
|
+
if (line.text.includes("}")) {
|
|
8960
|
+
collectEnumValues(openEnum.text, openEnum.from, declarations);
|
|
8961
|
+
openEnum = null;
|
|
8962
|
+
}
|
|
8963
|
+
index += 1;
|
|
8964
|
+
continue;
|
|
8965
|
+
}
|
|
8966
|
+
if (line.text.trim() === "") {
|
|
8967
|
+
index += 1;
|
|
8968
|
+
continue;
|
|
8969
|
+
}
|
|
8970
|
+
if (line.indent < indent) {
|
|
8971
|
+
break;
|
|
8972
|
+
}
|
|
8973
|
+
if (line.indent > indent) {
|
|
8974
|
+
index += 1;
|
|
8975
|
+
continue;
|
|
8976
|
+
}
|
|
8977
|
+
const body = line.text.slice(line.indent);
|
|
8978
|
+
const at = line.start + line.indent;
|
|
8979
|
+
if (name === null) {
|
|
8980
|
+
const declared = CLASS_NAME_LINE.exec(body);
|
|
8981
|
+
if (declared?.[1] !== undefined) {
|
|
8982
|
+
className = { name: declared[1], offset: at + declared[0].length - declared[1].length };
|
|
8983
|
+
}
|
|
8984
|
+
if (EXTENDS_LINE.test(body) || CLASS_NAME_LINE.test(body)) {
|
|
8985
|
+
const clause = EXTENDS_CLAUSE.exec(body);
|
|
8986
|
+
if (clause !== null && topExtends === null) {
|
|
8987
|
+
topExtends = extendsTarget(source, code, at + clause.index + clause[0].length);
|
|
8988
|
+
}
|
|
8989
|
+
}
|
|
8990
|
+
}
|
|
8991
|
+
const declared = DECLARATION_LINE.exec(body);
|
|
8992
|
+
if (declared !== null) {
|
|
8993
|
+
const [whole, , isStatic, keyword, declaredName] = declared;
|
|
8994
|
+
if (keyword !== undefined && declaredName !== undefined) {
|
|
8995
|
+
const offset = at + whole.length - declaredName.length;
|
|
8996
|
+
const kind = keyword;
|
|
8997
|
+
declarations.push({ name: declaredName, kind, offset, isStatic: isStatic !== undefined });
|
|
8998
|
+
if (kind === "enum") {
|
|
8999
|
+
const brace = body.indexOf("{");
|
|
9000
|
+
if (brace !== -1) {
|
|
9001
|
+
openEnum = { from: at + brace, text: body.slice(brace) };
|
|
9002
|
+
if (body.includes("}", brace)) {
|
|
9003
|
+
collectEnumValues(openEnum.text, openEnum.from, declarations);
|
|
9004
|
+
openEnum = null;
|
|
9005
|
+
}
|
|
9006
|
+
}
|
|
9007
|
+
}
|
|
9008
|
+
if (kind === "class") {
|
|
9009
|
+
const rest = body.slice(whole.length);
|
|
9010
|
+
const clause = EXTENDS_CLAUSE.exec(rest);
|
|
9011
|
+
const innerExtends = clause === null ? null : extendsTarget(source, code, at + whole.length + clause.index + clause[0].length);
|
|
9012
|
+
const childIndent = lines.slice(index + 1).find((next) => next.text.trim() !== "")?.indent ?? indent + 1;
|
|
9013
|
+
if (childIndent > indent) {
|
|
9014
|
+
const parsed = parseBody(declaredName, innerExtends, index + 1, childIndent, line.start);
|
|
9015
|
+
inner.push(parsed.body);
|
|
9016
|
+
index = parsed.next;
|
|
9017
|
+
continue;
|
|
9018
|
+
}
|
|
9019
|
+
inner.push({
|
|
9020
|
+
name: declaredName,
|
|
9021
|
+
extendsAs: innerExtends,
|
|
9022
|
+
declarations: [],
|
|
9023
|
+
inner: [],
|
|
9024
|
+
start: line.start,
|
|
9025
|
+
end: line.start + line.text.length
|
|
9026
|
+
});
|
|
9027
|
+
}
|
|
9028
|
+
}
|
|
9029
|
+
} else if (/^enum\s*\{/u.test(body)) {
|
|
9030
|
+
const brace = body.indexOf("{");
|
|
9031
|
+
openEnum = { from: at + brace, text: body.slice(brace) };
|
|
9032
|
+
if (body.includes("}", brace)) {
|
|
9033
|
+
collectEnumValues(openEnum.text, openEnum.from, declarations);
|
|
9034
|
+
openEnum = null;
|
|
9035
|
+
}
|
|
9036
|
+
}
|
|
9037
|
+
index += 1;
|
|
9038
|
+
}
|
|
9039
|
+
const last = lines[index - 1];
|
|
9040
|
+
return {
|
|
9041
|
+
body: {
|
|
9042
|
+
name,
|
|
9043
|
+
extendsAs: topExtends,
|
|
9044
|
+
declarations,
|
|
9045
|
+
inner,
|
|
9046
|
+
start: bodyStart,
|
|
9047
|
+
end: last === undefined ? bodyStart : last.start + last.text.length
|
|
9048
|
+
},
|
|
9049
|
+
next: index
|
|
9050
|
+
};
|
|
9051
|
+
};
|
|
9052
|
+
const { body } = parseBody(null, null, 0, 0, 0);
|
|
9053
|
+
return { className, body };
|
|
9054
|
+
}
|
|
9055
|
+
function collectEnumValues(text, from, into) {
|
|
9056
|
+
const close = text.indexOf("}");
|
|
9057
|
+
const inside = text.slice(1, close === -1 ? text.length : close);
|
|
9058
|
+
for (const match of inside.matchAll(new RegExp(`(^|,)\\s*(${NAME})`, "gu"))) {
|
|
9059
|
+
const valueName = match[2];
|
|
9060
|
+
if (valueName === undefined) {
|
|
9061
|
+
continue;
|
|
9062
|
+
}
|
|
9063
|
+
const offset = from + 1 + match.index + match[0].length - valueName.length;
|
|
9064
|
+
into.push({ name: valueName, kind: "enumValue", offset, isStatic: true });
|
|
9065
|
+
}
|
|
9066
|
+
}
|
|
9067
|
+
function isIdentifier(name) {
|
|
9068
|
+
return new RegExp(`^${NAME}$`, "u").test(name) && !KEYWORDS.has(name);
|
|
9069
|
+
}
|
|
9070
|
+
var IDENTIFIER_START, IDENTIFIER_PART, DECLARING, NAME = "[\\p{L}_][\\p{L}\\p{N}_]*", ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*", DECLARATION_LINE, CLASS_NAME_LINE, EXTENDS_LINE, EXTENDS_CLAUSE, KEYWORDS;
|
|
9071
|
+
var init_gdscript_source = __esm(() => {
|
|
9072
|
+
IDENTIFIER_START = /[\p{L}_]/u;
|
|
9073
|
+
IDENTIFIER_PART = /[\p{L}\p{N}_]/u;
|
|
9074
|
+
DECLARING = new Set(["var", "const", "func", "signal", "enum", "class", "for", "class_name"]);
|
|
9075
|
+
DECLARATION_LINE = new RegExp(`^(${ANNOTATIONS})(static\\s+)?(func|var|const|signal|enum|class)\\s+(${NAME})`, "u");
|
|
9076
|
+
CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
|
|
9077
|
+
EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
|
|
9078
|
+
EXTENDS_CLAUSE = /\bextends(?=\s)/u;
|
|
9079
|
+
KEYWORDS = new Set([
|
|
9080
|
+
"and",
|
|
9081
|
+
"as",
|
|
9082
|
+
"assert",
|
|
9083
|
+
"await",
|
|
9084
|
+
"break",
|
|
9085
|
+
"breakpoint",
|
|
9086
|
+
"class",
|
|
9087
|
+
"class_name",
|
|
9088
|
+
"const",
|
|
9089
|
+
"continue",
|
|
9090
|
+
"elif",
|
|
9091
|
+
"else",
|
|
9092
|
+
"enum",
|
|
9093
|
+
"extends",
|
|
9094
|
+
"false",
|
|
9095
|
+
"for",
|
|
9096
|
+
"func",
|
|
9097
|
+
"if",
|
|
9098
|
+
"in",
|
|
9099
|
+
"is",
|
|
9100
|
+
"match",
|
|
9101
|
+
"namespace",
|
|
9102
|
+
"not",
|
|
9103
|
+
"null",
|
|
9104
|
+
"or",
|
|
9105
|
+
"pass",
|
|
9106
|
+
"preload",
|
|
9107
|
+
"return",
|
|
9108
|
+
"self",
|
|
9109
|
+
"signal",
|
|
9110
|
+
"static",
|
|
9111
|
+
"super",
|
|
9112
|
+
"trait",
|
|
9113
|
+
"true",
|
|
9114
|
+
"var",
|
|
9115
|
+
"void",
|
|
9116
|
+
"when",
|
|
9117
|
+
"while",
|
|
9118
|
+
"yield",
|
|
9119
|
+
"INF",
|
|
9120
|
+
"NAN",
|
|
9121
|
+
"PI",
|
|
9122
|
+
"TAU"
|
|
9123
|
+
]);
|
|
9124
|
+
});
|
|
9125
|
+
|
|
8706
9126
|
// src/class-cache.ts
|
|
8707
9127
|
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
|
|
8708
9128
|
import { join as join9 } from "node:path";
|
|
@@ -8753,33 +9173,6 @@ function scriptsNaming(projectPath, classes) {
|
|
|
8753
9173
|
});
|
|
8754
9174
|
return naming.sort((a, b) => a.script.localeCompare(b.script));
|
|
8755
9175
|
}
|
|
8756
|
-
function codeOf(source) {
|
|
8757
|
-
let code = "";
|
|
8758
|
-
let at = 0;
|
|
8759
|
-
while (at < source.length) {
|
|
8760
|
-
const here = source[at] ?? "";
|
|
8761
|
-
if (here === "#") {
|
|
8762
|
-
const end = source.indexOf(`
|
|
8763
|
-
`, at);
|
|
8764
|
-
at = end === -1 ? source.length : end;
|
|
8765
|
-
continue;
|
|
8766
|
-
}
|
|
8767
|
-
if (here === '"' || here === "'") {
|
|
8768
|
-
const fence = source.startsWith(here.repeat(3), at) ? here.repeat(3) : here;
|
|
8769
|
-
let end = at + fence.length;
|
|
8770
|
-
while (end < source.length && !source.startsWith(fence, end)) {
|
|
8771
|
-
end += source[end] === "\\" ? 2 : 1;
|
|
8772
|
-
}
|
|
8773
|
-
const literal = source.slice(at, Math.min(end + fence.length, source.length));
|
|
8774
|
-
code += literal.replace(/[^\n]/g, " ");
|
|
8775
|
-
at = end + fence.length;
|
|
8776
|
-
continue;
|
|
8777
|
-
}
|
|
8778
|
-
code += here;
|
|
8779
|
-
at += 1;
|
|
8780
|
-
}
|
|
8781
|
-
return code;
|
|
8782
|
-
}
|
|
8783
9176
|
function cachedClasses(projectPath) {
|
|
8784
9177
|
const cache = join9(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
8785
9178
|
if (!existsSync9(cache)) {
|
|
@@ -9038,6 +9431,7 @@ function unseenByEditor(projectPath, editorHolds) {
|
|
|
9038
9431
|
}
|
|
9039
9432
|
var DECLARATION, TOOL;
|
|
9040
9433
|
var init_class_cache = __esm(() => {
|
|
9434
|
+
init_gdscript_source();
|
|
9041
9435
|
DECLARATION = /^(?:@[a-z_]+(?:\([^)\n]*\))?\s+)*class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m;
|
|
9042
9436
|
TOOL = /^[ \t]*(?:@[a-z_]+(?:\([^)\n]*\))?[ \t]+)*@tool\b/m;
|
|
9043
9437
|
});
|
|
@@ -17144,7 +17538,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17144
17538
|
requires: ["resourcePath", "options"]
|
|
17145
17539
|
},
|
|
17146
17540
|
reimport: {
|
|
17147
|
-
summary: "reimport what status says is outdated or failed, one resource with resourcePath, or what is current as well with force. Through the open editor when one serves the project, so it reloads what it holds, waiting for it to finish; otherwise through the engine's own import pass. Answered from a second status read: reimported lists what is current now, notReimported what is not,
|
|
17541
|
+
summary: "reimport what status says is outdated or failed, one resource with resourcePath, or what is current as well with force. Through the open editor when one serves the project, so it reloads what it holds, waiting for it to finish; otherwise through the engine's own import pass. The editor scans first, since its reimport passes over a file it has not taken in, such as one an installer has just written; what that scan imported is named under importedByTheScan and is not imported again unless force asks. Answered from a second status read: reimported lists what is current now, notReimported what is not, one entry per status and reason with the paths sharing them, and via says which of the two did the work. A whole-project reimport also answers missingSource, the .import files whose source is not beside them, empty when there are none, whether or not anything needed reimporting; one resourcePath is not a walk of the project and leaves it out. stillImporting is an editor that had not finished within timeoutMs",
|
|
17148
17542
|
requires: []
|
|
17149
17543
|
},
|
|
17150
17544
|
uid: {
|
|
@@ -17362,7 +17756,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17362
17756
|
},
|
|
17363
17757
|
{
|
|
17364
17758
|
name: "script_edit",
|
|
17365
|
-
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.",
|
|
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.",
|
|
17366
17760
|
parameters: {
|
|
17367
17761
|
projectPath: PROJECT_PATH,
|
|
17368
17762
|
scriptPath: SCRIPT_PATH,
|
|
@@ -17377,12 +17771,32 @@ var init_tool_definitions = __esm(() => {
|
|
|
17377
17771
|
enum: ["singleton", "state_machine", "component", "resource"],
|
|
17378
17772
|
description: "create: a starting shape."
|
|
17379
17773
|
},
|
|
17380
|
-
modifications: SCRIPT_MODIFICATIONS
|
|
17774
|
+
modifications: SCRIPT_MODIFICATIONS,
|
|
17775
|
+
symbol: {
|
|
17776
|
+
type: "string",
|
|
17777
|
+
ops: ["rename"],
|
|
17778
|
+
description: "rename: what to rename, declared by the script at scriptPath: its class_name, or a method, variable, constant, signal, enum, enum value or inner class at its top level. A class is renamed from the files and needs no editor: every bare use in code, the declaration, doc links such as [Told] and [method Told.say], and the script_class a .tres records. A script that declares something of its own by the old name, and a use after a dot, are left alone, since which of those mean the class cannot be told from the text. A member is renamed from what the editor's language server resolves to it, so it needs the editor open on this project; a method is renamed together with every override of it in the classes inheriting it, and from the topmost class declaring it when scriptPath's is itself an override. Doc links naming the member through any class inheriting it are renamed too, and so are a scene's or resource's saved value of a variable on a node or resource whose script inherits it, and a connection naming the signal on its emitter or the method on its receiver. A name the engine itself declares on the native base, such as _ready, is refused, as is a new name already taken: by a class, an autoload, an identifier in code, a native class or built-in type, or a member anywhere in the class's chain."
|
|
17779
|
+
},
|
|
17780
|
+
newName: { type: "string", ops: ["rename"], description: "rename: the new name." },
|
|
17781
|
+
newScriptPath: {
|
|
17782
|
+
type: "string",
|
|
17783
|
+
ops: ["rename"],
|
|
17784
|
+
description: "rename: also move the declaring script here, with its .uid beside it, rewriting every string that names it by path in the same style it was written (res:// or relative), and the relative paths in the moved script itself."
|
|
17785
|
+
},
|
|
17786
|
+
preview: {
|
|
17787
|
+
type: "boolean",
|
|
17788
|
+
ops: ["rename"],
|
|
17789
|
+
description: "rename: answer with what would change and what would be left alone, and write nothing. Default false."
|
|
17790
|
+
}
|
|
17381
17791
|
},
|
|
17382
17792
|
requires: ["projectPath", "scriptPath"],
|
|
17383
17793
|
operations: {
|
|
17384
17794
|
create: { summary: "a new script file", requires: [] },
|
|
17385
|
-
modify: { summary: "add to an existing script", requires: ["modifications"] }
|
|
17795
|
+
modify: { summary: "add to an existing script", requires: ["modifications"] },
|
|
17796
|
+
rename: {
|
|
17797
|
+
summary: "rename a class or a member wherever the engine uses it, across the project",
|
|
17798
|
+
requires: ["symbol", "newName"]
|
|
17799
|
+
}
|
|
17386
17800
|
}
|
|
17387
17801
|
},
|
|
17388
17802
|
{
|
|
@@ -17617,7 +18031,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17617
18031
|
},
|
|
17618
18032
|
{
|
|
17619
18033
|
name: "editor_status",
|
|
17620
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs.",
|
|
18034
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
17621
18035
|
parameters: {},
|
|
17622
18036
|
requires: []
|
|
17623
18037
|
},
|
|
@@ -34410,7 +34824,7 @@ function theFirstOf(path) {
|
|
|
34410
34824
|
closeSync(handle);
|
|
34411
34825
|
}
|
|
34412
34826
|
}
|
|
34413
|
-
function
|
|
34827
|
+
function shapeOf2(text) {
|
|
34414
34828
|
const values = [];
|
|
34415
34829
|
let shape = "";
|
|
34416
34830
|
let at = 0;
|
|
@@ -34440,7 +34854,7 @@ function shapeOf(text) {
|
|
|
34440
34854
|
}
|
|
34441
34855
|
function bursts(entries, before, keep) {
|
|
34442
34856
|
const shapes = new Map;
|
|
34443
|
-
const shaped = entries.map((entry) =>
|
|
34857
|
+
const shaped = entries.map((entry) => shapeOf2(entry.text));
|
|
34444
34858
|
for (const [at, entry] of entries.entries()) {
|
|
34445
34859
|
const key = `${entry.severity}\x00${shaped[at]?.shape ?? entry.text}`;
|
|
34446
34860
|
const group = shapes.get(key) ?? { indices: [], values: [] };
|
|
@@ -37477,7 +37891,7 @@ function getDefaultBridge(ownProject = null) {
|
|
|
37477
37891
|
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
|
|
37478
37892
|
return defaultBridge;
|
|
37479
37893
|
}
|
|
37480
|
-
var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, OTHER_PROJECT_CLOSE_CODE = 4001, CONNECT_WINDOW_MS = 35000, GodotBridge, defaultBridge = null;
|
|
37894
|
+
var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, OTHER_PROJECT_CLOSE_CODE = 4001, ONE_SHOT_CLOSE_CODE = 4002, CONNECT_WINDOW_MS = 35000, GodotBridge, defaultBridge = null;
|
|
37481
37895
|
var init_godot_bridge = __esm(() => {
|
|
37482
37896
|
init_wrapper();
|
|
37483
37897
|
init_errors();
|
|
@@ -37490,6 +37904,9 @@ var init_godot_bridge = __esm(() => {
|
|
|
37490
37904
|
socket = null;
|
|
37491
37905
|
pingInterval = null;
|
|
37492
37906
|
connectionInfo = null;
|
|
37907
|
+
oneShotReader = null;
|
|
37908
|
+
oneShots = new Map;
|
|
37909
|
+
turnedAway = null;
|
|
37493
37910
|
pendingRequests = new Map;
|
|
37494
37911
|
resourceQueues = new Map;
|
|
37495
37912
|
wantedPort;
|
|
@@ -37504,6 +37921,9 @@ var init_godot_bridge = __esm(() => {
|
|
|
37504
37921
|
this.timeoutMs = timeoutMs;
|
|
37505
37922
|
this.ownProject = ownProject;
|
|
37506
37923
|
}
|
|
37924
|
+
identifyEditorsWith(reader) {
|
|
37925
|
+
this.oneShotReader = reader;
|
|
37926
|
+
}
|
|
37507
37927
|
get mayMove() {
|
|
37508
37928
|
return this.ownProject !== null;
|
|
37509
37929
|
}
|
|
@@ -37610,25 +38030,33 @@ var init_godot_bridge = __esm(() => {
|
|
|
37610
38030
|
this.log("info", "WebSocket bridge stopped");
|
|
37611
38031
|
}
|
|
37612
38032
|
isConnected() {
|
|
37613
|
-
return this.socket?.readyState === import_websocket.default.OPEN;
|
|
38033
|
+
return this.socket?.readyState === import_websocket.default.OPEN && this.connectionInfo?.identifying !== true;
|
|
38034
|
+
}
|
|
38035
|
+
isArriving() {
|
|
38036
|
+
const info = this.connectionInfo;
|
|
38037
|
+
return this.socket?.readyState === import_websocket.default.OPEN && info !== null && (info.greeted !== true || info.identifying === true);
|
|
37614
38038
|
}
|
|
37615
38039
|
getStatus() {
|
|
38040
|
+
const identifying = this.connectionInfo?.identifying === true;
|
|
38041
|
+
const editor = identifying ? null : this.connectionInfo;
|
|
37616
38042
|
return {
|
|
37617
38043
|
host: this.host,
|
|
37618
38044
|
port: this.port,
|
|
37619
38045
|
connected: this.isConnected(),
|
|
37620
|
-
projectPath:
|
|
37621
|
-
connectedAt:
|
|
37622
|
-
lastPongAt:
|
|
37623
|
-
addonVersion:
|
|
37624
|
-
addonDigest:
|
|
37625
|
-
editorPid:
|
|
37626
|
-
lspPort:
|
|
37627
|
-
dapPort:
|
|
37628
|
-
debugPort:
|
|
37629
|
-
openedByAServer:
|
|
37630
|
-
startedByAnEditor:
|
|
37631
|
-
syncsBreakpoints:
|
|
38046
|
+
projectPath: editor?.projectPath,
|
|
38047
|
+
connectedAt: editor?.connectedAt,
|
|
38048
|
+
lastPongAt: editor?.lastPongAt,
|
|
38049
|
+
addonVersion: editor?.addonVersion,
|
|
38050
|
+
addonDigest: editor?.addonDigest,
|
|
38051
|
+
editorPid: editor?.editorPid,
|
|
38052
|
+
lspPort: editor?.lspPort,
|
|
38053
|
+
dapPort: editor?.dapPort,
|
|
38054
|
+
debugPort: editor?.debugPort,
|
|
38055
|
+
openedByAServer: editor?.openedByAServer,
|
|
38056
|
+
startedByAnEditor: editor?.startedByAnEditor,
|
|
38057
|
+
syncsBreakpoints: editor?.syncsBreakpoints,
|
|
38058
|
+
turnedAway: this.turnedAway ?? undefined,
|
|
38059
|
+
identifying: identifying ? true : undefined,
|
|
37632
38060
|
pendingRequests: this.pendingRequests.size,
|
|
37633
38061
|
queuedResources: this.resourceQueues.size,
|
|
37634
38062
|
listeningSince: this.listeningSince ?? undefined,
|
|
@@ -37776,6 +38204,12 @@ var init_godot_bridge = __esm(() => {
|
|
|
37776
38204
|
this.connectionInfo.startedByAnEditor = typeof message.started_by_an_editor === "boolean" ? message.started_by_an_editor : undefined;
|
|
37777
38205
|
this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
|
|
37778
38206
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
38207
|
+
this.connectionInfo.greeted = true;
|
|
38208
|
+
const pid = this.connectionInfo.editorPid;
|
|
38209
|
+
if (!this.connectionInfo.openedByAServer && pid !== undefined && this.oneShotReader !== null) {
|
|
38210
|
+
this.identify(pid);
|
|
38211
|
+
return;
|
|
38212
|
+
}
|
|
37779
38213
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
37780
38214
|
}
|
|
37781
38215
|
return;
|
|
@@ -37866,6 +38300,45 @@ var init_godot_bridge = __esm(() => {
|
|
|
37866
38300
|
this.stopKeepalive();
|
|
37867
38301
|
stranger?.close(OTHER_PROJECT_CLOSE_CODE, said);
|
|
37868
38302
|
}
|
|
38303
|
+
identify(pid) {
|
|
38304
|
+
const known = this.oneShots.get(pid);
|
|
38305
|
+
if (known !== undefined) {
|
|
38306
|
+
this.turnAway(pid, known);
|
|
38307
|
+
return;
|
|
38308
|
+
}
|
|
38309
|
+
const info = this.connectionInfo;
|
|
38310
|
+
const socket = this.socket;
|
|
38311
|
+
const reader = this.oneShotReader;
|
|
38312
|
+
if (info === null || reader === null) {
|
|
38313
|
+
return;
|
|
38314
|
+
}
|
|
38315
|
+
info.identifying = true;
|
|
38316
|
+
reader(pid).catch(() => {
|
|
38317
|
+
return;
|
|
38318
|
+
}).then((option) => {
|
|
38319
|
+
if (this.socket !== socket || this.connectionInfo !== info) {
|
|
38320
|
+
return;
|
|
38321
|
+
}
|
|
38322
|
+
if (typeof option === "string") {
|
|
38323
|
+
this.oneShots.set(pid, option);
|
|
38324
|
+
this.turnAway(pid, option);
|
|
38325
|
+
return;
|
|
38326
|
+
}
|
|
38327
|
+
info.identifying = false;
|
|
38328
|
+
this.log("info", `Godot editor identified: pid ${pid}`);
|
|
38329
|
+
this.emitBridgeEvent("godot_connected", { projectPath: info.projectPath });
|
|
38330
|
+
});
|
|
38331
|
+
}
|
|
38332
|
+
turnAway(pid, option) {
|
|
38333
|
+
const said = `pid ${pid} was started with ${option}, which does one job and quits, so it is not an editor`;
|
|
38334
|
+
this.log("info", `Turning away an engine run: ${said}`);
|
|
38335
|
+
this.turnedAway = { pid, option, at: new Date };
|
|
38336
|
+
const run = this.socket;
|
|
38337
|
+
this.socket = null;
|
|
38338
|
+
this.connectionInfo = null;
|
|
38339
|
+
this.stopKeepalive();
|
|
38340
|
+
run?.close(ONE_SHOT_CLOSE_CODE, said);
|
|
38341
|
+
}
|
|
37869
38342
|
handleDisconnect(disconnectedSocket, reason) {
|
|
37870
38343
|
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
37871
38344
|
this.log("debug", "Ignoring stale Godot socket disconnect event");
|
|
@@ -38791,6 +39264,39 @@ class GodotLSPClient {
|
|
|
38791
39264
|
}
|
|
38792
39265
|
});
|
|
38793
39266
|
}
|
|
39267
|
+
async getReferences(filePath, content, line, character) {
|
|
39268
|
+
await this.ensureConnected();
|
|
39269
|
+
await this.ensureInitializedForFile(filePath);
|
|
39270
|
+
const result = await this.withDocument(filePath, async () => {
|
|
39271
|
+
const uri = this.syncDocument(filePath, content);
|
|
39272
|
+
try {
|
|
39273
|
+
return await this.sendRequest("textDocument/references", {
|
|
39274
|
+
textDocument: { uri },
|
|
39275
|
+
position: { line, character },
|
|
39276
|
+
context: { includeDeclaration: true }
|
|
39277
|
+
});
|
|
39278
|
+
} finally {
|
|
39279
|
+
this.closeDocument(uri);
|
|
39280
|
+
}
|
|
39281
|
+
});
|
|
39282
|
+
return locationsIn(result);
|
|
39283
|
+
}
|
|
39284
|
+
async getDefinitions(filePath, content, line, character) {
|
|
39285
|
+
await this.ensureConnected();
|
|
39286
|
+
await this.ensureInitializedForFile(filePath);
|
|
39287
|
+
const result = await this.withDocument(filePath, async () => {
|
|
39288
|
+
const uri = this.syncDocument(filePath, content);
|
|
39289
|
+
try {
|
|
39290
|
+
return await this.sendRequest("textDocument/definition", {
|
|
39291
|
+
textDocument: { uri },
|
|
39292
|
+
position: { line, character }
|
|
39293
|
+
});
|
|
39294
|
+
} finally {
|
|
39295
|
+
this.closeDocument(uri);
|
|
39296
|
+
}
|
|
39297
|
+
});
|
|
39298
|
+
return locationsIn(result);
|
|
39299
|
+
}
|
|
38794
39300
|
async getDocumentSymbols(filePath, content) {
|
|
38795
39301
|
await this.ensureConnected();
|
|
38796
39302
|
await this.ensureInitializedForFile(filePath);
|
|
@@ -38810,6 +39316,22 @@ class GodotLSPClient {
|
|
|
38810
39316
|
return this.connected;
|
|
38811
39317
|
}
|
|
38812
39318
|
}
|
|
39319
|
+
function locationsIn(result) {
|
|
39320
|
+
const entries = Array.isArray(result) ? result : result === null || result === undefined ? [] : [result];
|
|
39321
|
+
const found = [];
|
|
39322
|
+
for (const entry of entries) {
|
|
39323
|
+
const location = entry;
|
|
39324
|
+
const uri = location.uri ?? location.targetUri;
|
|
39325
|
+
const start = location.range?.start ?? location.targetSelectionRange?.start;
|
|
39326
|
+
if (typeof uri !== "string" || typeof start?.line !== "number" || typeof start.character !== "number") {
|
|
39327
|
+
continue;
|
|
39328
|
+
}
|
|
39329
|
+
try {
|
|
39330
|
+
found.push({ file: fileURLToPath3(uri), line: start.line, character: start.character });
|
|
39331
|
+
} catch {}
|
|
39332
|
+
}
|
|
39333
|
+
return found;
|
|
39334
|
+
}
|
|
38813
39335
|
function asToolResponse(payload) {
|
|
38814
39336
|
return {
|
|
38815
39337
|
content: [
|
|
@@ -39103,6 +39625,35 @@ function scriptsWithoutUid(projectPath) {
|
|
|
39103
39625
|
function isBinary(bytes) {
|
|
39104
39626
|
return bytes.subarray(0, 8000).includes(0);
|
|
39105
39627
|
}
|
|
39628
|
+
function projectTexts(projectPath) {
|
|
39629
|
+
const found = [];
|
|
39630
|
+
const visit = (directory, prefix) => {
|
|
39631
|
+
if (steppedOver(directory)) {
|
|
39632
|
+
return;
|
|
39633
|
+
}
|
|
39634
|
+
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
39635
|
+
if (skipped(entry.name)) {
|
|
39636
|
+
continue;
|
|
39637
|
+
}
|
|
39638
|
+
const absolute = join20(directory, entry.name);
|
|
39639
|
+
const spelled = `${prefix}${entry.name}`;
|
|
39640
|
+
if (entry.isDirectory()) {
|
|
39641
|
+
visit(absolute, `${spelled}/`);
|
|
39642
|
+
continue;
|
|
39643
|
+
}
|
|
39644
|
+
const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
39645
|
+
if (!entry.isFile() || BINARY_EXTENSIONS.has(extension)) {
|
|
39646
|
+
continue;
|
|
39647
|
+
}
|
|
39648
|
+
const bytes = readFileSync17(absolute);
|
|
39649
|
+
if (!isBinary(bytes)) {
|
|
39650
|
+
found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
|
|
39651
|
+
}
|
|
39652
|
+
}
|
|
39653
|
+
};
|
|
39654
|
+
visit(projectPath, "");
|
|
39655
|
+
return found;
|
|
39656
|
+
}
|
|
39106
39657
|
function searchProject(projectPath, options) {
|
|
39107
39658
|
const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
|
|
39108
39659
|
const result = {
|
|
@@ -39161,7 +39712,7 @@ function searchProject(projectPath, options) {
|
|
|
39161
39712
|
visit(projectPath);
|
|
39162
39713
|
return result;
|
|
39163
39714
|
}
|
|
39164
|
-
var SKIPPED, SCENE_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID;
|
|
39715
|
+
var SKIPPED, SCENE_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID, BINARY_EXTENSIONS;
|
|
39165
39716
|
var init_project_scan = __esm(() => {
|
|
39166
39717
|
SKIPPED = new Set(["node_modules"]);
|
|
39167
39718
|
SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
|
|
@@ -39196,19 +39747,1050 @@ var init_project_scan = __esm(() => {
|
|
|
39196
39747
|
"shader"
|
|
39197
39748
|
]);
|
|
39198
39749
|
CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
|
|
39750
|
+
BINARY_EXTENSIONS = new Set([
|
|
39751
|
+
...[...ASSET_EXTENSIONS].filter((extension) => !["svg", "gltf", "gdshader", "shader", "fnt"].includes(extension)),
|
|
39752
|
+
"scn",
|
|
39753
|
+
"res",
|
|
39754
|
+
"zip",
|
|
39755
|
+
"pck",
|
|
39756
|
+
"exe",
|
|
39757
|
+
"dll",
|
|
39758
|
+
"so",
|
|
39759
|
+
"dylib"
|
|
39760
|
+
]);
|
|
39761
|
+
});
|
|
39762
|
+
|
|
39763
|
+
// src/rename.ts
|
|
39764
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync10, readFileSync as readFileSync18, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync12 } from "node:fs";
|
|
39765
|
+
import { dirname as dirname14, posix } from "node:path";
|
|
39766
|
+
|
|
39767
|
+
class Script {
|
|
39768
|
+
path;
|
|
39769
|
+
text;
|
|
39770
|
+
regions;
|
|
39771
|
+
shape;
|
|
39772
|
+
lines;
|
|
39773
|
+
constructor(path, text) {
|
|
39774
|
+
this.path = path;
|
|
39775
|
+
this.text = text;
|
|
39776
|
+
this.regions = regionsOf(text);
|
|
39777
|
+
this.shape = shapeOf(text);
|
|
39778
|
+
this.lines = new Lines(text);
|
|
39779
|
+
}
|
|
39780
|
+
occurrences(name) {
|
|
39781
|
+
return occurrencesOf(this.text, name, this.regions);
|
|
39782
|
+
}
|
|
39783
|
+
}
|
|
39784
|
+
function classId(file, inner) {
|
|
39785
|
+
return `${file}#${inner.join(".")}`;
|
|
39786
|
+
}
|
|
39787
|
+
function fileOf(id) {
|
|
39788
|
+
return id.slice(0, id.lastIndexOf("#"));
|
|
39789
|
+
}
|
|
39790
|
+
|
|
39791
|
+
class ClassGraph {
|
|
39792
|
+
nodes = new Map;
|
|
39793
|
+
declared;
|
|
39794
|
+
constructor(scripts, declared) {
|
|
39795
|
+
this.declared = declared;
|
|
39796
|
+
for (const script of scripts.values()) {
|
|
39797
|
+
const add = (body, inner) => {
|
|
39798
|
+
const id = classId(script.path, inner);
|
|
39799
|
+
this.nodes.set(id, { id, file: script.path, inner, body, parent: null, native: null });
|
|
39800
|
+
for (const child of body.inner) {
|
|
39801
|
+
if (child.name !== null) {
|
|
39802
|
+
add(child, [...inner, child.name]);
|
|
39803
|
+
}
|
|
39804
|
+
}
|
|
39805
|
+
};
|
|
39806
|
+
add(script.shape.body, []);
|
|
39807
|
+
}
|
|
39808
|
+
for (const node of this.nodes.values()) {
|
|
39809
|
+
const resolved = this.resolve(node, node.body.extendsAs);
|
|
39810
|
+
node.parent = resolved.id;
|
|
39811
|
+
node.native = resolved.native;
|
|
39812
|
+
}
|
|
39813
|
+
}
|
|
39814
|
+
resolve(node, written) {
|
|
39815
|
+
if (written === null) {
|
|
39816
|
+
return { id: null, native: "RefCounted" };
|
|
39817
|
+
}
|
|
39818
|
+
if (written.startsWith('"') || written.startsWith("'")) {
|
|
39819
|
+
const path = resolvedPath(written.slice(1, -1), node.file);
|
|
39820
|
+
const id = classId(path, []);
|
|
39821
|
+
return this.nodes.has(id) ? { id, native: null } : { id: null, native: null };
|
|
39822
|
+
}
|
|
39823
|
+
const [first = "", ...rest] = written.split(".");
|
|
39824
|
+
for (let depth = node.inner.length;depth >= 0; depth -= 1) {
|
|
39825
|
+
const scope = this.nodes.get(classId(node.file, node.inner.slice(0, depth)));
|
|
39826
|
+
if (scope?.body.inner.some((child) => child.name === first) === true) {
|
|
39827
|
+
const id = classId(node.file, [...node.inner.slice(0, depth), first, ...rest]);
|
|
39828
|
+
return this.nodes.has(id) ? { id, native: null } : { id: null, native: null };
|
|
39829
|
+
}
|
|
39830
|
+
}
|
|
39831
|
+
const globalFile = this.declared.get(first);
|
|
39832
|
+
if (globalFile !== undefined) {
|
|
39833
|
+
const id = classId(globalFile, rest);
|
|
39834
|
+
return this.nodes.has(id) ? { id, native: null } : { id: null, native: null };
|
|
39835
|
+
}
|
|
39836
|
+
return { id: null, native: rest.length === 0 ? first : null };
|
|
39837
|
+
}
|
|
39838
|
+
ancestors(id) {
|
|
39839
|
+
const found = [];
|
|
39840
|
+
let at = this.nodes.get(id)?.parent ?? null;
|
|
39841
|
+
while (at !== null && !found.includes(at)) {
|
|
39842
|
+
found.push(at);
|
|
39843
|
+
at = this.nodes.get(at)?.parent ?? null;
|
|
39844
|
+
}
|
|
39845
|
+
return found;
|
|
39846
|
+
}
|
|
39847
|
+
nativeBase(id) {
|
|
39848
|
+
const chain = [id, ...this.ancestors(id)];
|
|
39849
|
+
return this.nodes.get(chain[chain.length - 1] ?? id)?.native ?? null;
|
|
39850
|
+
}
|
|
39851
|
+
descendants(id) {
|
|
39852
|
+
const found = new Set([id]);
|
|
39853
|
+
for (const node of this.nodes.values()) {
|
|
39854
|
+
if (this.ancestors(node.id).includes(id)) {
|
|
39855
|
+
found.add(node.id);
|
|
39856
|
+
}
|
|
39857
|
+
}
|
|
39858
|
+
return found;
|
|
39859
|
+
}
|
|
39860
|
+
bodyAt(file, offset) {
|
|
39861
|
+
let best = classId(file, []);
|
|
39862
|
+
let depth = 0;
|
|
39863
|
+
for (const node of this.nodes.values()) {
|
|
39864
|
+
if (node.file === file && node.inner.length > depth && node.body.start <= offset && offset <= node.body.end) {
|
|
39865
|
+
best = node.id;
|
|
39866
|
+
depth = node.inner.length;
|
|
39867
|
+
}
|
|
39868
|
+
}
|
|
39869
|
+
return best;
|
|
39870
|
+
}
|
|
39871
|
+
declaration(id, name) {
|
|
39872
|
+
const found = this.nodes.get(id)?.body.declarations.find((one) => one.name === name);
|
|
39873
|
+
return found === undefined ? undefined : { kind: found.kind, offset: found.offset };
|
|
39874
|
+
}
|
|
39875
|
+
}
|
|
39876
|
+
function resolvedPath(written, from) {
|
|
39877
|
+
if (written.startsWith("res://")) {
|
|
39878
|
+
return `res://${posix.normalize(written.slice("res://".length))}`;
|
|
39879
|
+
}
|
|
39880
|
+
const directory = posix.dirname(from.slice("res://".length));
|
|
39881
|
+
return `res://${posix.normalize(posix.join(directory, written))}`;
|
|
39882
|
+
}
|
|
39883
|
+
function relativeTo(target, from) {
|
|
39884
|
+
return posix.relative(posix.dirname(from.slice("res://".length)), target.slice("res://".length));
|
|
39885
|
+
}
|
|
39886
|
+
function scriptsOf(texts) {
|
|
39887
|
+
const scripts = new Map;
|
|
39888
|
+
for (const file of texts) {
|
|
39889
|
+
if (file.path.endsWith(".gd")) {
|
|
39890
|
+
scripts.set(file.path, new Script(file.path, file.text));
|
|
39891
|
+
}
|
|
39892
|
+
}
|
|
39893
|
+
return scripts;
|
|
39894
|
+
}
|
|
39895
|
+
function declaredIn(scripts) {
|
|
39896
|
+
const declared = new Map;
|
|
39897
|
+
for (const script of scripts.values()) {
|
|
39898
|
+
if (script.shape.className !== null) {
|
|
39899
|
+
declared.set(script.shape.className.name, script.path);
|
|
39900
|
+
}
|
|
39901
|
+
}
|
|
39902
|
+
return declared;
|
|
39903
|
+
}
|
|
39904
|
+
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) {
|
|
39913
|
+
return null;
|
|
39914
|
+
}
|
|
39915
|
+
const inside = text.slice(open + 1, close);
|
|
39916
|
+
if (inside.includes("[") || inside.includes("]")) {
|
|
39917
|
+
return null;
|
|
39918
|
+
}
|
|
39919
|
+
const match = /^(?:([a-z_]+)\s+)?([\p{L}_][\p{L}\p{N}_]*(?:\.[\p{L}_][\p{L}\p{N}_]*)*)$/u.exec(inside);
|
|
39920
|
+
if (match === null || match[1] !== undefined && !DOC_LINK_KINDS.has(match[1])) {
|
|
39921
|
+
return null;
|
|
39922
|
+
}
|
|
39923
|
+
const parts = (match[2] ?? "").split(".");
|
|
39924
|
+
const pathStart = open + 1 + inside.length - (match[2] ?? "").length;
|
|
39925
|
+
let at = pathStart;
|
|
39926
|
+
for (const [index, part] of parts.entries()) {
|
|
39927
|
+
if (at === offset) {
|
|
39928
|
+
return { kind: match[1] ?? null, parts, index };
|
|
39929
|
+
}
|
|
39930
|
+
at += part.length + 1;
|
|
39931
|
+
}
|
|
39932
|
+
return null;
|
|
39933
|
+
}
|
|
39934
|
+
|
|
39935
|
+
class PlanBuilder {
|
|
39936
|
+
edits = new Map;
|
|
39937
|
+
mentions = [];
|
|
39938
|
+
oldName;
|
|
39939
|
+
newName;
|
|
39940
|
+
constructor(oldName, newName) {
|
|
39941
|
+
this.oldName = oldName;
|
|
39942
|
+
this.newName = newName;
|
|
39943
|
+
}
|
|
39944
|
+
edit(file, offset, kind, length = this.oldName.length, replacement = this.newName) {
|
|
39945
|
+
const key = `${file}:${offset}`;
|
|
39946
|
+
if (!this.edits.has(key)) {
|
|
39947
|
+
this.edits.set(key, { file, offset, length, replacement, kind });
|
|
39948
|
+
}
|
|
39949
|
+
}
|
|
39950
|
+
edited(file, offset) {
|
|
39951
|
+
return this.edits.has(`${file}:${offset}`);
|
|
39952
|
+
}
|
|
39953
|
+
mention(file, lines, offset, kind, why) {
|
|
39954
|
+
this.mentions.push({
|
|
39955
|
+
file,
|
|
39956
|
+
line: lines.positionOf(offset).line + 1,
|
|
39957
|
+
kind,
|
|
39958
|
+
text: lines.lineAt(offset).trim(),
|
|
39959
|
+
...why === undefined ? {} : { why }
|
|
39960
|
+
});
|
|
39961
|
+
}
|
|
39962
|
+
mentionTheRest(texts, scripts, whyFor) {
|
|
39963
|
+
const already = new Set(this.mentions.map((one) => `${one.file}:${one.line}:${one.kind}:${one.text}`));
|
|
39964
|
+
for (const file of texts) {
|
|
39965
|
+
const script = scripts.get(file.path);
|
|
39966
|
+
const lines = script?.lines ?? new Lines(file.text);
|
|
39967
|
+
const occurrences = script?.occurrences(this.oldName) ?? occurrencesOf(file.text, this.oldName, [{ kind: "code", start: 0, end: file.text.length }]);
|
|
39968
|
+
for (const occurrence of occurrences) {
|
|
39969
|
+
if (this.edited(file.path, occurrence.offset)) {
|
|
39970
|
+
continue;
|
|
39971
|
+
}
|
|
39972
|
+
const kind = script !== undefined ? occurrence.kind : /\.(tscn|tres)$/.test(file.path) ? "scene" : "text";
|
|
39973
|
+
const why = whyFor(file.path, occurrence, kind);
|
|
39974
|
+
const key = `${file.path}:${lines.positionOf(occurrence.offset).line + 1}:${kind}:${lines.lineAt(occurrence.offset).trim()}`;
|
|
39975
|
+
if (already.has(key)) {
|
|
39976
|
+
continue;
|
|
39977
|
+
}
|
|
39978
|
+
already.add(key);
|
|
39979
|
+
this.mention(file.path, lines, occurrence.offset, kind, why);
|
|
39980
|
+
}
|
|
39981
|
+
}
|
|
39982
|
+
}
|
|
39983
|
+
build() {
|
|
39984
|
+
const edits = [...this.edits.values()].sort((a, b) => a.file.localeCompare(b.file) || a.offset - b.offset);
|
|
39985
|
+
const mentions = [...this.mentions].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
39986
|
+
return { edits, mentions };
|
|
39987
|
+
}
|
|
39988
|
+
}
|
|
39989
|
+
function declaresItsOwn(script, name, except) {
|
|
39990
|
+
return script.occurrences(name).some((occurrence) => occurrence.kind === "code" && occurrence.offset !== except && occurrence.declaredAs !== null && occurrence.declaredAs !== "class_name");
|
|
39991
|
+
}
|
|
39992
|
+
function planClassRename(args) {
|
|
39993
|
+
const { texts, scripts, declaring, oldName, newName } = args;
|
|
39994
|
+
const plan = new PlanBuilder(oldName, newName);
|
|
39995
|
+
const declarationAt = declaring.shape.className?.offset ?? null;
|
|
39996
|
+
for (const script of scripts.values()) {
|
|
39997
|
+
const ownDeclaration = script.path === declaring.path ? declarationAt : null;
|
|
39998
|
+
const shadowed = declaresItsOwn(script, oldName, ownDeclaration);
|
|
39999
|
+
for (const occurrence of script.occurrences(oldName)) {
|
|
40000
|
+
if (occurrence.kind === "code") {
|
|
40001
|
+
if (occurrence.offset === ownDeclaration) {
|
|
40002
|
+
plan.edit(script.path, occurrence.offset, "declaration");
|
|
40003
|
+
} else if (!shadowed && !occurrence.afterDot && occurrence.declaredAs === null) {
|
|
40004
|
+
plan.edit(script.path, occurrence.offset, "code");
|
|
40005
|
+
}
|
|
40006
|
+
} else if (occurrence.kind === "doc") {
|
|
40007
|
+
const link = docLinkAt(script.text, occurrence.offset);
|
|
40008
|
+
if (link?.index === 0 && !shadowed) {
|
|
40009
|
+
plan.edit(script.path, occurrence.offset, "doc link");
|
|
40010
|
+
}
|
|
40011
|
+
}
|
|
40012
|
+
}
|
|
40013
|
+
}
|
|
40014
|
+
for (const file of texts) {
|
|
40015
|
+
if (!/\.(tscn|tres)$/.test(file.path)) {
|
|
40016
|
+
continue;
|
|
40017
|
+
}
|
|
40018
|
+
for (const section of sectionsOf(file.text)) {
|
|
40019
|
+
if (section.kind === "gd_resource" || section.kind === "gd_scene") {
|
|
40020
|
+
const recorded = section.attributes.get("script_class");
|
|
40021
|
+
if (recorded?.value === oldName) {
|
|
40022
|
+
plan.edit(file.path, recorded.offset, "resource class");
|
|
40023
|
+
}
|
|
40024
|
+
}
|
|
40025
|
+
}
|
|
40026
|
+
}
|
|
40027
|
+
plan.mentionTheRest(texts, scripts, (file, occurrence, kind) => {
|
|
40028
|
+
const script = scripts.get(file);
|
|
40029
|
+
if (kind !== "code" || script === undefined) {
|
|
40030
|
+
return;
|
|
40031
|
+
}
|
|
40032
|
+
if (occurrence.declaredAs === "class_name") {
|
|
40033
|
+
return `this script declares a class of the same name`;
|
|
40034
|
+
}
|
|
40035
|
+
if (occurrence.declaredAs !== null || declaresItsOwn(script, oldName, null)) {
|
|
40036
|
+
return `this script declares something of its own named ${oldName}, so which uses mean the class cannot be told from the text`;
|
|
40037
|
+
}
|
|
40038
|
+
if (occurrence.afterDot) {
|
|
40039
|
+
return `after a dot, a member of something else rather than the class`;
|
|
40040
|
+
}
|
|
40041
|
+
return;
|
|
40042
|
+
});
|
|
40043
|
+
return plan.build();
|
|
40044
|
+
}
|
|
40045
|
+
function planMemberRename(args) {
|
|
40046
|
+
const { texts, scripts, graph, root, family, kind, oldName, newName, resolved } = args;
|
|
40047
|
+
const plan = new PlanBuilder(oldName, newName);
|
|
40048
|
+
const heirs = graph.descendants(root);
|
|
40049
|
+
const docKind = DOC_KIND[kind];
|
|
40050
|
+
const familyAt = new Set(family.map((one) => `${one.file}:${one.offset}`));
|
|
40051
|
+
for (const one of family) {
|
|
40052
|
+
plan.edit(one.file, one.offset, "declaration");
|
|
40053
|
+
}
|
|
40054
|
+
for (const one of resolved) {
|
|
40055
|
+
const script = scripts.get(one.file);
|
|
40056
|
+
if (script === undefined || script.text.slice(one.offset, one.offset + oldName.length) !== oldName) {
|
|
40057
|
+
continue;
|
|
40058
|
+
}
|
|
40059
|
+
const occurrence = script.occurrences(oldName).find((each) => each.offset === one.offset);
|
|
40060
|
+
if (occurrence === undefined) {
|
|
40061
|
+
continue;
|
|
40062
|
+
}
|
|
40063
|
+
if (occurrence.kind === "code") {
|
|
40064
|
+
plan.edit(one.file, one.offset, familyAt.has(`${one.file}:${one.offset}`) ? "declaration" : "code");
|
|
40065
|
+
} else if (occurrence.kind === "doc" && docLinkAt(script.text, one.offset) !== null) {
|
|
40066
|
+
plan.edit(one.file, one.offset, "doc link");
|
|
40067
|
+
} else {
|
|
40068
|
+
plan.mention(one.file, script.lines, one.offset, occurrence.kind, `the language server resolved this to the ${kind === "func" ? "method" : "member"}, and it is inside ${occurrence.kind === "string" ? "a string" : occurrence.kind === "nodePath" ? "a node path" : "a comment"}, so it is left for you`);
|
|
40069
|
+
}
|
|
40070
|
+
}
|
|
40071
|
+
for (const script of scripts.values()) {
|
|
40072
|
+
for (const occurrence of script.occurrences(oldName)) {
|
|
40073
|
+
if (occurrence.kind !== "doc") {
|
|
40074
|
+
continue;
|
|
40075
|
+
}
|
|
40076
|
+
const link = docLinkAt(script.text, occurrence.offset);
|
|
40077
|
+
if (link === null || link.index !== link.parts.length - 1 || link.kind !== docKind || docKind === null) {
|
|
40078
|
+
continue;
|
|
40079
|
+
}
|
|
40080
|
+
const qualifiers = link.parts.slice(0, -1);
|
|
40081
|
+
let owner = null;
|
|
40082
|
+
if (qualifiers.length === 0) {
|
|
40083
|
+
owner = graph.bodyAt(script.path, occurrence.offset);
|
|
40084
|
+
} else {
|
|
40085
|
+
const [first = "", ...rest] = qualifiers;
|
|
40086
|
+
const file = graph.declared.get(first);
|
|
40087
|
+
owner = file === undefined ? null : `${file}#${rest.join(".")}`;
|
|
40088
|
+
}
|
|
40089
|
+
if (owner !== null && heirs.has(owner)) {
|
|
40090
|
+
plan.edit(script.path, occurrence.offset, "doc link");
|
|
40091
|
+
}
|
|
40092
|
+
}
|
|
40093
|
+
}
|
|
40094
|
+
planScenes(texts, heirs, kind, oldName, plan);
|
|
40095
|
+
plan.mentionTheRest(texts, scripts, (_file, occurrence, mentionKind) => mentionKind === "code" ? 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" : undefined);
|
|
40096
|
+
return plan.build();
|
|
40097
|
+
}
|
|
40098
|
+
function sectionsOf(text) {
|
|
40099
|
+
const sections = [];
|
|
40100
|
+
let current = null;
|
|
40101
|
+
let depth = 0;
|
|
40102
|
+
let inString = false;
|
|
40103
|
+
let offset = 0;
|
|
40104
|
+
for (const line of text.split(`
|
|
40105
|
+
`)) {
|
|
40106
|
+
const atTop = depth === 0 && !inString;
|
|
40107
|
+
const header = atTop ? /^\[([a-z_]+)(.*)\]\s*$/.exec(line.replace(/\r$/, "")) : null;
|
|
40108
|
+
if (header !== null) {
|
|
40109
|
+
const attributes = new Map;
|
|
40110
|
+
const rest = header[2] ?? "";
|
|
40111
|
+
const restAt = offset + 1 + (header[1]?.length ?? 0);
|
|
40112
|
+
for (const attribute of rest.matchAll(/([a-z_]+)=("(?:[^"\\]|\\.)*"|[A-Za-z]+\("(?:[^"\\]|\\.)*"\)|[^\s\]]+)/g)) {
|
|
40113
|
+
const raw = attribute[2] ?? "";
|
|
40114
|
+
const quoted = raw.startsWith('"');
|
|
40115
|
+
attributes.set(attribute[1] ?? "", {
|
|
40116
|
+
value: quoted ? raw.slice(1, -1) : raw,
|
|
40117
|
+
offset: restAt + attribute.index + (attribute[1]?.length ?? 0) + 1 + (quoted ? 1 : 0)
|
|
40118
|
+
});
|
|
40119
|
+
}
|
|
40120
|
+
current = { kind: header[1] ?? "", attributes, properties: [] };
|
|
40121
|
+
sections.push(current);
|
|
40122
|
+
} else if (atTop && current !== null) {
|
|
40123
|
+
const property = /^([A-Za-z_][\w/:]*) = (.*)$/.exec(line.replace(/\r$/, ""));
|
|
40124
|
+
if (property !== null) {
|
|
40125
|
+
current.properties.push({ key: property[1] ?? "", offset, value: property[2] ?? "" });
|
|
40126
|
+
}
|
|
40127
|
+
}
|
|
40128
|
+
for (let at = 0;at < line.length; at += 1) {
|
|
40129
|
+
const character = line[at];
|
|
40130
|
+
if (inString) {
|
|
40131
|
+
if (character === "\\") {
|
|
40132
|
+
at += 1;
|
|
40133
|
+
} else if (character === '"') {
|
|
40134
|
+
inString = false;
|
|
40135
|
+
}
|
|
40136
|
+
} else if (character === '"') {
|
|
40137
|
+
inString = true;
|
|
40138
|
+
} else if (character === "[" || character === "{" || character === "(") {
|
|
40139
|
+
depth += 1;
|
|
40140
|
+
} else if (character === "]" || character === "}" || character === ")") {
|
|
40141
|
+
depth = Math.max(0, depth - 1);
|
|
40142
|
+
}
|
|
40143
|
+
}
|
|
40144
|
+
if (header !== null) {
|
|
40145
|
+
depth = 0;
|
|
40146
|
+
}
|
|
40147
|
+
offset += line.length + 1;
|
|
40148
|
+
}
|
|
40149
|
+
return sections;
|
|
40150
|
+
}
|
|
40151
|
+
function resourceReference(value) {
|
|
40152
|
+
const match = /^(Ext|Sub)Resource\(\s*"([^"]*)"\s*\)$/.exec(value.trim());
|
|
40153
|
+
return match === null ? null : { external: match[1] === "Ext", id: match[2] ?? "" };
|
|
40154
|
+
}
|
|
40155
|
+
|
|
40156
|
+
class SceneReader {
|
|
40157
|
+
cache = new Map;
|
|
40158
|
+
texts;
|
|
40159
|
+
uids;
|
|
40160
|
+
constructor(texts, uids) {
|
|
40161
|
+
this.texts = texts;
|
|
40162
|
+
this.uids = uids;
|
|
40163
|
+
}
|
|
40164
|
+
sections(path) {
|
|
40165
|
+
let found = this.cache.get(path);
|
|
40166
|
+
if (found === undefined) {
|
|
40167
|
+
const text = this.texts.get(path);
|
|
40168
|
+
found = text === undefined ? [] : sectionsOf(text);
|
|
40169
|
+
this.cache.set(path, found);
|
|
40170
|
+
}
|
|
40171
|
+
return found;
|
|
40172
|
+
}
|
|
40173
|
+
external(path, id) {
|
|
40174
|
+
const entry = this.sections(path).find((section) => section.kind === "ext_resource" && section.attributes.get("id")?.value === id);
|
|
40175
|
+
if (entry === undefined) {
|
|
40176
|
+
return null;
|
|
40177
|
+
}
|
|
40178
|
+
const written = entry.attributes.get("path")?.value;
|
|
40179
|
+
if (written !== undefined) {
|
|
40180
|
+
return resolvedPath(written, path);
|
|
40181
|
+
}
|
|
40182
|
+
const uid = entry.attributes.get("uid")?.value;
|
|
40183
|
+
return uid === undefined ? null : this.uids.get(uid) ?? null;
|
|
40184
|
+
}
|
|
40185
|
+
scriptOf(path, section, depth = 0) {
|
|
40186
|
+
const script = section.properties.find((property) => property.key === "script");
|
|
40187
|
+
if (script !== undefined) {
|
|
40188
|
+
const reference = resourceReference(script.value);
|
|
40189
|
+
return reference?.external === true ? this.external(path, reference.id) : null;
|
|
40190
|
+
}
|
|
40191
|
+
const instance = section.attributes.get("instance")?.value;
|
|
40192
|
+
const reference = instance === undefined ? null : resourceReference(instance);
|
|
40193
|
+
if (reference?.external !== true || depth > 8) {
|
|
40194
|
+
return null;
|
|
40195
|
+
}
|
|
40196
|
+
const scene = this.external(path, reference.id);
|
|
40197
|
+
if (scene === null) {
|
|
40198
|
+
return null;
|
|
40199
|
+
}
|
|
40200
|
+
const rootNode = this.sections(scene).find((candidate) => candidate.kind === "node" && !candidate.attributes.has("parent"));
|
|
40201
|
+
return rootNode === undefined ? null : this.scriptOf(scene, rootNode, depth + 1);
|
|
40202
|
+
}
|
|
40203
|
+
nodesByPath(path) {
|
|
40204
|
+
const nodes = new Map;
|
|
40205
|
+
for (const section of this.sections(path)) {
|
|
40206
|
+
if (section.kind !== "node") {
|
|
40207
|
+
continue;
|
|
40208
|
+
}
|
|
40209
|
+
const name = section.attributes.get("name")?.value ?? "";
|
|
40210
|
+
const parent = section.attributes.get("parent")?.value;
|
|
40211
|
+
nodes.set(parent === undefined ? "." : parent === "." ? name : `${parent}/${name}`, section);
|
|
40212
|
+
}
|
|
40213
|
+
return nodes;
|
|
40214
|
+
}
|
|
40215
|
+
}
|
|
40216
|
+
function planScenes(texts, heirs, kind, oldName, plan) {
|
|
40217
|
+
const byPath = new Map(texts.map((file) => [file.path, file.text]));
|
|
40218
|
+
const uids = new Map;
|
|
40219
|
+
for (const file of texts) {
|
|
40220
|
+
if (file.path.endsWith(".uid")) {
|
|
40221
|
+
uids.set(file.text.trim(), file.path.slice(0, -".uid".length));
|
|
40222
|
+
}
|
|
40223
|
+
}
|
|
40224
|
+
const reader = new SceneReader(byPath, uids);
|
|
40225
|
+
const inherits = (script) => script !== null && heirs.has(`${script}#`);
|
|
40226
|
+
for (const file of texts) {
|
|
40227
|
+
if (!/\.(tscn|tres)$/.test(file.path) || !file.text.includes(oldName)) {
|
|
40228
|
+
continue;
|
|
40229
|
+
}
|
|
40230
|
+
const sections = reader.sections(file.path);
|
|
40231
|
+
if (kind === "var") {
|
|
40232
|
+
for (const section of sections) {
|
|
40233
|
+
if (!["node", "resource", "sub_resource"].includes(section.kind)) {
|
|
40234
|
+
continue;
|
|
40235
|
+
}
|
|
40236
|
+
const property = section.properties.find((one) => one.key === oldName);
|
|
40237
|
+
if (property !== undefined && inherits(reader.scriptOf(file.path, section))) {
|
|
40238
|
+
plan.edit(file.path, property.offset, "scene property");
|
|
40239
|
+
}
|
|
40240
|
+
}
|
|
40241
|
+
}
|
|
40242
|
+
if (kind === "signal" || kind === "func") {
|
|
40243
|
+
const nodes = reader.nodesByPath(file.path);
|
|
40244
|
+
for (const section of sections) {
|
|
40245
|
+
if (section.kind !== "connection") {
|
|
40246
|
+
continue;
|
|
40247
|
+
}
|
|
40248
|
+
const attribute = section.attributes.get(kind === "signal" ? "signal" : "method");
|
|
40249
|
+
const node = nodes.get(section.attributes.get(kind === "signal" ? "from" : "to")?.value ?? "");
|
|
40250
|
+
if (attribute?.value === oldName && node !== undefined && inherits(reader.scriptOf(file.path, node))) {
|
|
40251
|
+
plan.edit(file.path, attribute.offset, "scene connection");
|
|
40252
|
+
}
|
|
40253
|
+
}
|
|
40254
|
+
}
|
|
40255
|
+
}
|
|
40256
|
+
}
|
|
40257
|
+
function literalsOf(file, script) {
|
|
40258
|
+
const found = [];
|
|
40259
|
+
if (script !== undefined) {
|
|
40260
|
+
for (const region of script.regions) {
|
|
40261
|
+
if (region.kind !== "string") {
|
|
40262
|
+
continue;
|
|
40263
|
+
}
|
|
40264
|
+
const literal = file.text.slice(region.start, region.end);
|
|
40265
|
+
const quote = /["']/.exec(literal);
|
|
40266
|
+
if (quote === null || literal.startsWith('"""', quote.index) || literal.startsWith("'''", quote.index)) {
|
|
40267
|
+
continue;
|
|
40268
|
+
}
|
|
40269
|
+
found.push({ offset: region.start + quote.index + 1, value: literal.slice(quote.index + 1, -1) });
|
|
40270
|
+
}
|
|
40271
|
+
return found;
|
|
40272
|
+
}
|
|
40273
|
+
for (const match of file.text.matchAll(/"((?:[^"\\\n]|\\.)*)"/g)) {
|
|
40274
|
+
found.push({ offset: match.index + 1, value: match[1] ?? "" });
|
|
40275
|
+
}
|
|
40276
|
+
return found;
|
|
40277
|
+
}
|
|
40278
|
+
function planMove(args, plan) {
|
|
40279
|
+
const { texts, scripts, from, to, exists } = args;
|
|
40280
|
+
const edits = [...plan.edits];
|
|
40281
|
+
const mentions = [...plan.mentions];
|
|
40282
|
+
const editedAt = new Set(edits.map((one) => `${one.file}:${one.offset}`));
|
|
40283
|
+
const add = (file, offset, length, replacement) => {
|
|
40284
|
+
if (!editedAt.has(`${file}:${offset}`)) {
|
|
40285
|
+
editedAt.add(`${file}:${offset}`);
|
|
40286
|
+
edits.push({ file, offset, length, replacement, kind: "path" });
|
|
40287
|
+
}
|
|
40288
|
+
};
|
|
40289
|
+
for (const file of texts) {
|
|
40290
|
+
const script = scripts.get(file.path);
|
|
40291
|
+
for (const literal of literalsOf(file, script)) {
|
|
40292
|
+
const autoload = literal.value.startsWith("*") ? "*" : "";
|
|
40293
|
+
const written = literal.value.slice(autoload.length);
|
|
40294
|
+
if (written === "" || written.startsWith("uid://") || written.includes(`
|
|
40295
|
+
`)) {
|
|
40296
|
+
continue;
|
|
40297
|
+
}
|
|
40298
|
+
const isAbsolute = written.startsWith("res://");
|
|
40299
|
+
const target = resolvedPath(written, file.path);
|
|
40300
|
+
if (target === from) {
|
|
40301
|
+
const replacement = isAbsolute ? to : relativeTo(to, file.path === from ? to : file.path);
|
|
40302
|
+
add(file.path, literal.offset + autoload.length, written.length, replacement);
|
|
40303
|
+
} else if (file.path === from && !isAbsolute && /\.[a-z0-9]+$/i.test(written) && exists(target)) {
|
|
40304
|
+
add(file.path, literal.offset, written.length, relativeTo(target, to));
|
|
40305
|
+
}
|
|
40306
|
+
}
|
|
40307
|
+
const lines = script?.lines ?? new Lines(file.text);
|
|
40308
|
+
for (const match of file.text.matchAll(new RegExp(from.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&"), "g"))) {
|
|
40309
|
+
const offset = match.index;
|
|
40310
|
+
const covered = edits.some((one) => one.file === file.path && one.offset <= offset && offset < one.offset + one.length);
|
|
40311
|
+
if (!covered) {
|
|
40312
|
+
const region = script?.regions.find((one) => one.start <= offset && offset < one.end);
|
|
40313
|
+
mentions.push({
|
|
40314
|
+
file: file.path,
|
|
40315
|
+
line: lines.positionOf(offset).line + 1,
|
|
40316
|
+
kind: region !== undefined ? region.kind : /\.(tscn|tres)$/.test(file.path) ? "scene" : "text",
|
|
40317
|
+
text: lines.lineAt(offset).trim(),
|
|
40318
|
+
why: `names ${from} outside a string the engine reads as a path`
|
|
40319
|
+
});
|
|
40320
|
+
}
|
|
40321
|
+
}
|
|
40322
|
+
}
|
|
40323
|
+
return {
|
|
40324
|
+
edits: edits.sort((a, b) => a.file.localeCompare(b.file) || a.offset - b.offset),
|
|
40325
|
+
mentions: mentions.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line)
|
|
40326
|
+
};
|
|
40327
|
+
}
|
|
40328
|
+
function rewritesOf(plan, texts, move) {
|
|
40329
|
+
const byPath = new Map(texts.map((file) => [file.path, file]));
|
|
40330
|
+
const byFile = new Map;
|
|
40331
|
+
for (const edit of plan.edits) {
|
|
40332
|
+
byFile.set(edit.file, [...byFile.get(edit.file) ?? [], edit]);
|
|
40333
|
+
}
|
|
40334
|
+
if (move !== null && !byFile.has(move.from)) {
|
|
40335
|
+
byFile.set(move.from, []);
|
|
40336
|
+
}
|
|
40337
|
+
const rewrites = [];
|
|
40338
|
+
for (const [path, edits] of byFile) {
|
|
40339
|
+
const file = byPath.get(path);
|
|
40340
|
+
if (file === undefined) {
|
|
40341
|
+
continue;
|
|
40342
|
+
}
|
|
40343
|
+
let after = file.text;
|
|
40344
|
+
let lastStart = Number.POSITIVE_INFINITY;
|
|
40345
|
+
for (const edit of [...edits].sort((a, b) => b.offset - a.offset)) {
|
|
40346
|
+
if (edit.offset + edit.length > lastStart) {
|
|
40347
|
+
throw new Error(`two edits overlap in ${path} at offset ${edit.offset}`);
|
|
40348
|
+
}
|
|
40349
|
+
after = after.slice(0, edit.offset) + edit.replacement + after.slice(edit.offset + edit.length);
|
|
40350
|
+
lastStart = edit.offset;
|
|
40351
|
+
}
|
|
40352
|
+
rewrites.push({
|
|
40353
|
+
path,
|
|
40354
|
+
absolute: file.absolute,
|
|
40355
|
+
before: file.text,
|
|
40356
|
+
after,
|
|
40357
|
+
writeTo: move?.from === path ? move.absoluteTo : file.absolute
|
|
40358
|
+
});
|
|
40359
|
+
}
|
|
40360
|
+
return rewrites.sort((a, b) => a.path.localeCompare(b.path));
|
|
40361
|
+
}
|
|
40362
|
+
function applyRewrites(rewrites) {
|
|
40363
|
+
for (const rewrite of rewrites) {
|
|
40364
|
+
const now = readFileSync18(rewrite.absolute, "utf8");
|
|
40365
|
+
if (now !== rewrite.before) {
|
|
40366
|
+
throw new Error(`${rewrite.path} changed on disk while the rename was being worked out; nothing was written`);
|
|
40367
|
+
}
|
|
40368
|
+
if (rewrite.writeTo !== rewrite.absolute && existsSync17(rewrite.writeTo)) {
|
|
40369
|
+
throw new Error(`${rewrite.writeTo} already exists; nothing was written`);
|
|
40370
|
+
}
|
|
40371
|
+
}
|
|
40372
|
+
const done = [];
|
|
40373
|
+
try {
|
|
40374
|
+
for (const rewrite of rewrites) {
|
|
40375
|
+
mkdirSync10(dirname14(rewrite.writeTo), { recursive: true });
|
|
40376
|
+
const staging = `${rewrite.writeTo}.gdharness-rename`;
|
|
40377
|
+
writeFileSync12(staging, rewrite.after, "utf8");
|
|
40378
|
+
renameSync(staging, rewrite.writeTo);
|
|
40379
|
+
let movedUid = false;
|
|
40380
|
+
if (rewrite.writeTo !== rewrite.absolute) {
|
|
40381
|
+
unlinkSync2(rewrite.absolute);
|
|
40382
|
+
if (existsSync17(`${rewrite.absolute}.uid`)) {
|
|
40383
|
+
renameSync(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
|
|
40384
|
+
movedUid = true;
|
|
40385
|
+
}
|
|
40386
|
+
}
|
|
40387
|
+
done.push({ rewrite, movedUid });
|
|
40388
|
+
}
|
|
40389
|
+
} catch (error) {
|
|
40390
|
+
for (const { rewrite, movedUid } of done.reverse()) {
|
|
40391
|
+
try {
|
|
40392
|
+
writeFileSync12(rewrite.absolute, rewrite.before, "utf8");
|
|
40393
|
+
if (rewrite.writeTo !== rewrite.absolute) {
|
|
40394
|
+
unlinkSync2(rewrite.writeTo);
|
|
40395
|
+
if (movedUid) {
|
|
40396
|
+
renameSync(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
|
|
40397
|
+
}
|
|
40398
|
+
}
|
|
40399
|
+
} catch {}
|
|
40400
|
+
}
|
|
40401
|
+
throw error;
|
|
40402
|
+
}
|
|
40403
|
+
}
|
|
40404
|
+
var DOC_LINK_KINDS, DOC_KIND;
|
|
40405
|
+
var init_rename = __esm(() => {
|
|
40406
|
+
init_gdscript_source();
|
|
40407
|
+
DOC_LINK_KINDS = new Set([
|
|
40408
|
+
"method",
|
|
40409
|
+
"member",
|
|
40410
|
+
"signal",
|
|
40411
|
+
"constant",
|
|
40412
|
+
"enum",
|
|
40413
|
+
"annotation",
|
|
40414
|
+
"theme_item"
|
|
40415
|
+
]);
|
|
40416
|
+
DOC_KIND = {
|
|
40417
|
+
func: "method",
|
|
40418
|
+
var: "member",
|
|
40419
|
+
signal: "signal",
|
|
40420
|
+
const: "constant",
|
|
40421
|
+
enumValue: "constant",
|
|
40422
|
+
enum: "enum",
|
|
40423
|
+
class: null
|
|
40424
|
+
};
|
|
40425
|
+
});
|
|
40426
|
+
|
|
40427
|
+
// src/rename-symbol.ts
|
|
40428
|
+
import { existsSync as existsSync18, realpathSync as realpathSync5 } from "node:fs";
|
|
40429
|
+
import { basename as basename5, dirname as dirname15, join as join21, relative as relative4, resolve as resolve6 } from "node:path";
|
|
40430
|
+
function resourcePathOf(projectPath, absolute) {
|
|
40431
|
+
const inside = relative4(onDiskSpelling(projectPath), onDiskSpelling(absolute));
|
|
40432
|
+
if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
|
|
40433
|
+
return null;
|
|
40434
|
+
}
|
|
40435
|
+
return `res://${inside.replace(/\\/g, "/")}`;
|
|
40436
|
+
}
|
|
40437
|
+
function onDiskSpelling(path) {
|
|
40438
|
+
const missing = [];
|
|
40439
|
+
let at = resolve6(path);
|
|
40440
|
+
for (;; ) {
|
|
40441
|
+
try {
|
|
40442
|
+
return join21(realpathSync5.native(at), ...missing.reverse());
|
|
40443
|
+
} catch {
|
|
40444
|
+
const parent = dirname15(at);
|
|
40445
|
+
if (parent === at) {
|
|
40446
|
+
return resolve6(path);
|
|
40447
|
+
}
|
|
40448
|
+
missing.push(basename5(at));
|
|
40449
|
+
at = parent;
|
|
40450
|
+
}
|
|
40451
|
+
}
|
|
40452
|
+
}
|
|
40453
|
+
function usedAsIdentifier(scripts, name, most) {
|
|
40454
|
+
const found = [];
|
|
40455
|
+
for (const script of scripts.values()) {
|
|
40456
|
+
for (const occurrence of script.occurrences(name)) {
|
|
40457
|
+
if (occurrence.kind === "code" && !occurrence.afterDot) {
|
|
40458
|
+
found.push(`${script.path}:${script.lines.positionOf(occurrence.offset).line + 1}`);
|
|
40459
|
+
if (found.length >= most) {
|
|
40460
|
+
return found;
|
|
40461
|
+
}
|
|
40462
|
+
}
|
|
40463
|
+
}
|
|
40464
|
+
}
|
|
40465
|
+
return found;
|
|
40466
|
+
}
|
|
40467
|
+
function autoloadNames(texts) {
|
|
40468
|
+
const settings = texts.find((file) => file.path === "res://project.godot");
|
|
40469
|
+
return settings === undefined ? [] : Object.keys(parseProjectGodot(settings.text)["autoload"] ?? {});
|
|
40470
|
+
}
|
|
40471
|
+
async function renameSymbol(request, services) {
|
|
40472
|
+
const { projectPath, symbol, newName } = request;
|
|
40473
|
+
if (!isIdentifier(newName)) {
|
|
40474
|
+
return {
|
|
40475
|
+
ok: false,
|
|
40476
|
+
reason: `${newName} is not a name GDScript accepts: a letter or underscore, then letters, digits and underscores, and not a keyword.`
|
|
40477
|
+
};
|
|
40478
|
+
}
|
|
40479
|
+
if (newName === symbol) {
|
|
40480
|
+
return { ok: false, reason: `${symbol} is already called ${newName}; nothing to rename.` };
|
|
40481
|
+
}
|
|
40482
|
+
const declaringPath = resourcePathOf(projectPath, request.scriptPath);
|
|
40483
|
+
if (declaringPath === null) {
|
|
40484
|
+
return { ok: false, reason: `${request.scriptPath} is not inside the project.` };
|
|
40485
|
+
}
|
|
40486
|
+
let moveTo = null;
|
|
40487
|
+
if (request.newScriptPath !== null) {
|
|
40488
|
+
moveTo = resourcePathOf(projectPath, request.newScriptPath);
|
|
40489
|
+
if (moveTo === null || !moveTo.endsWith(".gd")) {
|
|
40490
|
+
return {
|
|
40491
|
+
ok: false,
|
|
40492
|
+
reason: `newScriptPath must be a .gd path inside the project, and ${request.newScriptPath} is not.`
|
|
40493
|
+
};
|
|
40494
|
+
}
|
|
40495
|
+
if (existsSync18(request.newScriptPath)) {
|
|
40496
|
+
return { ok: false, reason: `${moveTo} already exists, so the script cannot be moved there.` };
|
|
40497
|
+
}
|
|
40498
|
+
}
|
|
40499
|
+
const texts = projectTexts(projectPath);
|
|
40500
|
+
const scripts = scriptsOf(texts);
|
|
40501
|
+
const declaring = scripts.get(declaringPath);
|
|
40502
|
+
if (declaring === undefined) {
|
|
40503
|
+
return {
|
|
40504
|
+
ok: false,
|
|
40505
|
+
reason: `${declaringPath} is not among the scripts the engine sees: it is under a directory holding a .gdignore, or one spelled with a dot.`
|
|
40506
|
+
};
|
|
40507
|
+
}
|
|
40508
|
+
const declared = declaredIn(scripts);
|
|
40509
|
+
const isClass = declaring.shape.className?.name === symbol;
|
|
40510
|
+
let plan;
|
|
40511
|
+
let what;
|
|
40512
|
+
let resolvedBy;
|
|
40513
|
+
let overrides = [];
|
|
40514
|
+
let declaringFiles = [declaringPath];
|
|
40515
|
+
if (isClass) {
|
|
40516
|
+
what = "class";
|
|
40517
|
+
resolvedBy = "the files";
|
|
40518
|
+
const conflicts = [];
|
|
40519
|
+
const holder = declared.get(newName);
|
|
40520
|
+
if (holder !== undefined) {
|
|
40521
|
+
conflicts.push(`${newName} is already the class_name of ${holder}`);
|
|
40522
|
+
}
|
|
40523
|
+
if (autoloadNames(texts).includes(newName)) {
|
|
40524
|
+
conflicts.push(`${newName} is already an autoload in project.godot`);
|
|
40525
|
+
}
|
|
40526
|
+
for (const place of usedAsIdentifier(scripts, newName, 10)) {
|
|
40527
|
+
conflicts.push(`${newName} is already an identifier at ${place}`);
|
|
40528
|
+
}
|
|
40529
|
+
const taken = (await services.namesTaken([newName], null))[newName];
|
|
40530
|
+
if (taken !== undefined && taken.as.length > 0) {
|
|
40531
|
+
conflicts.push(`${newName} is ${taken.as.join(" and ")}`);
|
|
40532
|
+
}
|
|
40533
|
+
if (conflicts.length > 0) {
|
|
40534
|
+
return {
|
|
40535
|
+
ok: false,
|
|
40536
|
+
reason: `${symbol} cannot be renamed to ${newName}: the name is taken.`,
|
|
40537
|
+
conflicts
|
|
40538
|
+
};
|
|
40539
|
+
}
|
|
40540
|
+
plan = planClassRename({ texts, scripts, declaring, oldName: symbol, newName });
|
|
40541
|
+
} else {
|
|
40542
|
+
const declaration = declaring.shape.body.declarations.find((one) => one.name === symbol);
|
|
40543
|
+
if (declaration === undefined) {
|
|
40544
|
+
const names = [
|
|
40545
|
+
...declaring.shape.className === null ? [] : [declaring.shape.className.name],
|
|
40546
|
+
...declaring.shape.body.declarations.map((one) => one.name)
|
|
40547
|
+
];
|
|
40548
|
+
return {
|
|
40549
|
+
ok: false,
|
|
40550
|
+
reason: `${declaringPath} declares no ${symbol} at its top level. What it declares there: ${names.join(", ") || "nothing"}. A local variable or a member of an inner class is not renamed by this call.`
|
|
40551
|
+
};
|
|
40552
|
+
}
|
|
40553
|
+
const kind = declaration.kind;
|
|
40554
|
+
what = WHAT[kind];
|
|
40555
|
+
resolvedBy = "the editor's language server";
|
|
40556
|
+
const graph = new ClassGraph(scripts, declared);
|
|
40557
|
+
const own = `${declaringPath}#`;
|
|
40558
|
+
let root = own;
|
|
40559
|
+
if (kind === "func") {
|
|
40560
|
+
for (const ancestor of graph.ancestors(own)) {
|
|
40561
|
+
if (graph.declaration(ancestor, symbol)?.kind === "func") {
|
|
40562
|
+
root = ancestor;
|
|
40563
|
+
}
|
|
40564
|
+
}
|
|
40565
|
+
}
|
|
40566
|
+
const native = graph.nativeBase(root);
|
|
40567
|
+
const taken = await services.namesTaken([symbol, newName], native);
|
|
40568
|
+
const engineOwned = taken[symbol]?.declaredBy ?? null;
|
|
40569
|
+
if (engineOwned !== null) {
|
|
40570
|
+
return {
|
|
40571
|
+
ok: false,
|
|
40572
|
+
reason: `${symbol} is declared by the engine's ${engineOwned}, which ${root === own ? declaringPath : fileOf(root)} inherits from, so the script ${kind === "func" ? "overrides" : "shadows"} an engine name rather than declaring its own, and the engine calls it by that name.`
|
|
40573
|
+
};
|
|
40574
|
+
}
|
|
40575
|
+
const conflicts = [];
|
|
40576
|
+
const newOwner = taken[newName]?.declaredBy ?? null;
|
|
40577
|
+
if (newOwner !== null) {
|
|
40578
|
+
conflicts.push(`${newName} is declared by the engine's ${newOwner}, which this class inherits from`);
|
|
40579
|
+
}
|
|
40580
|
+
for (const id of new Set([root, ...graph.ancestors(root), ...graph.descendants(root)])) {
|
|
40581
|
+
if (graph.declaration(id, newName) !== undefined) {
|
|
40582
|
+
conflicts.push(`${newName} is already declared in ${id.endsWith("#") ? fileOf(id) : `${fileOf(id)}, class ${id.slice(id.lastIndexOf("#") + 1)}`}`);
|
|
40583
|
+
}
|
|
40584
|
+
}
|
|
40585
|
+
if (conflicts.length > 0) {
|
|
40586
|
+
return {
|
|
40587
|
+
ok: false,
|
|
40588
|
+
reason: `${symbol} cannot be renamed to ${newName}: the name is taken.`,
|
|
40589
|
+
conflicts
|
|
40590
|
+
};
|
|
40591
|
+
}
|
|
40592
|
+
const family = [];
|
|
40593
|
+
if (kind === "func") {
|
|
40594
|
+
for (const id of graph.descendants(root)) {
|
|
40595
|
+
const found = graph.declaration(id, symbol);
|
|
40596
|
+
if (found?.kind === "func") {
|
|
40597
|
+
family.push({ file: fileOf(id), offset: found.offset });
|
|
40598
|
+
}
|
|
40599
|
+
}
|
|
40600
|
+
} else {
|
|
40601
|
+
family.push({ file: declaringPath, offset: declaration.offset });
|
|
40602
|
+
}
|
|
40603
|
+
overrides = family.map((one) => one.file).filter((file) => file !== fileOf(root));
|
|
40604
|
+
declaringFiles = family.map((one) => one.file);
|
|
40605
|
+
const resolved = await resolveMember(projectPath, texts, scripts, family, symbol, services);
|
|
40606
|
+
if (resolved === null) {
|
|
40607
|
+
const first = family[0];
|
|
40608
|
+
const script = first === undefined ? undefined : scripts.get(first.file);
|
|
40609
|
+
const line = first === undefined || script === undefined ? 0 : script.lines.positionOf(first.offset).line + 1;
|
|
40610
|
+
return {
|
|
40611
|
+
ok: false,
|
|
40612
|
+
reason: `The language server resolved nothing at ${first?.file ?? declaringPath}:${line}, where ${symbol} is declared, not even the declaration, so it is not analysing that script and its answer would rename nothing.`,
|
|
40613
|
+
advice: [
|
|
40614
|
+
"script_diagnostics on that script says whether the editor can parse it",
|
|
40615
|
+
"editor_status says whether the editor connected is this project's"
|
|
40616
|
+
]
|
|
40617
|
+
};
|
|
40618
|
+
}
|
|
40619
|
+
plan = planMemberRename({
|
|
40620
|
+
texts,
|
|
40621
|
+
scripts,
|
|
40622
|
+
graph,
|
|
40623
|
+
root,
|
|
40624
|
+
family,
|
|
40625
|
+
kind,
|
|
40626
|
+
oldName: symbol,
|
|
40627
|
+
newName,
|
|
40628
|
+
resolved
|
|
40629
|
+
});
|
|
40630
|
+
}
|
|
40631
|
+
if (moveTo !== null) {
|
|
40632
|
+
plan = planMove({
|
|
40633
|
+
texts,
|
|
40634
|
+
scripts,
|
|
40635
|
+
from: declaringPath,
|
|
40636
|
+
to: moveTo,
|
|
40637
|
+
exists: (path) => existsSync18(`${projectPath}/${path.slice("res://".length)}`)
|
|
40638
|
+
}, plan);
|
|
40639
|
+
}
|
|
40640
|
+
const rewrites = rewritesOf(plan, texts, moveTo === null || request.newScriptPath === null ? null : { from: declaringPath, to: moveTo, absoluteTo: request.newScriptPath });
|
|
40641
|
+
const graph = new ClassGraph(scripts, declared);
|
|
40642
|
+
const rank = (path) => (declaringFiles.includes(path) ? 0 : 1000) + graph.ancestors(`${path}#`).length;
|
|
40643
|
+
const scriptsInOrder = rewrites.map((rewrite) => rewrite.path).filter((path) => path.endsWith(".gd")).sort((a, b) => rank(a) - rank(b) || a.localeCompare(b)).map((path) => path === declaringPath && moveTo !== null ? moveTo : path);
|
|
40644
|
+
return {
|
|
40645
|
+
ok: true,
|
|
40646
|
+
isClass,
|
|
40647
|
+
moved: moveTo === null ? null : { from: declaringPath, to: moveTo },
|
|
40648
|
+
scriptsInOrder,
|
|
40649
|
+
rewrites,
|
|
40650
|
+
report: {
|
|
40651
|
+
renamed: { what, from: symbol, to: newName, declaredIn: declaringPath, resolvedBy },
|
|
40652
|
+
...overrides.length > 0 ? { overridesRenamed: overrides } : {},
|
|
40653
|
+
...moveTo === null ? {} : { moved: { from: declaringPath, to: moveTo } },
|
|
40654
|
+
changed: changedReport(plan.edits, rewrites),
|
|
40655
|
+
leftAlone: plan.mentions,
|
|
40656
|
+
summary: summaryOf(plan.edits, plan.mentions, rewrites)
|
|
40657
|
+
}
|
|
40658
|
+
};
|
|
40659
|
+
}
|
|
40660
|
+
async function resolveMember(projectPath, texts, scripts, family, name, services) {
|
|
40661
|
+
const absoluteOf = new Map(texts.map((file) => [file.path, file.absolute]));
|
|
40662
|
+
const key = (place) => `${place.file}:${place.offset}`;
|
|
40663
|
+
const toResolved = (located) => {
|
|
40664
|
+
const file = resourcePathOf(projectPath, located.file);
|
|
40665
|
+
const offset = file === null ? null : scripts.get(file)?.lines.offsetOf(located);
|
|
40666
|
+
return file === null || offset === null || offset === undefined ? null : { file, offset };
|
|
40667
|
+
};
|
|
40668
|
+
const ask = async (question, place) => {
|
|
40669
|
+
const script = scripts.get(place.file);
|
|
40670
|
+
const absolute = absoluteOf.get(place.file);
|
|
40671
|
+
if (script === undefined || absolute === undefined) {
|
|
40672
|
+
return [];
|
|
40673
|
+
}
|
|
40674
|
+
const answered = await services[question](absolute, script.text, script.lines.positionOf(place.offset));
|
|
40675
|
+
return answered.map(toResolved).filter((one) => one !== null);
|
|
40676
|
+
};
|
|
40677
|
+
const ours = new Map;
|
|
40678
|
+
const settled = new Set;
|
|
40679
|
+
const root = family[0];
|
|
40680
|
+
if (root === undefined) {
|
|
40681
|
+
return [];
|
|
40682
|
+
}
|
|
40683
|
+
const seeded = await ask("references", root);
|
|
40684
|
+
if (seeded.length === 0) {
|
|
40685
|
+
return null;
|
|
40686
|
+
}
|
|
40687
|
+
for (const place of seeded) {
|
|
40688
|
+
ours.set(key(place), place);
|
|
40689
|
+
}
|
|
40690
|
+
const declarations = new Set(family.map(key));
|
|
40691
|
+
for (const script of scripts.values()) {
|
|
40692
|
+
for (const occurrence of script.occurrences(name)) {
|
|
40693
|
+
const place = { file: script.path, offset: occurrence.offset };
|
|
40694
|
+
if (occurrence.kind !== "code" || ours.has(key(place)) || settled.has(key(place))) {
|
|
40695
|
+
continue;
|
|
40696
|
+
}
|
|
40697
|
+
const declared = await ask("definitions", place);
|
|
40698
|
+
const only = declared.length === 1 ? declared[0] : undefined;
|
|
40699
|
+
if (only === undefined) {
|
|
40700
|
+
settled.add(key(place));
|
|
40701
|
+
continue;
|
|
40702
|
+
}
|
|
40703
|
+
const isOurs = declarations.has(key(only));
|
|
40704
|
+
const same = [place, ...await ask("references", place)];
|
|
40705
|
+
for (const one of same) {
|
|
40706
|
+
if (isOurs) {
|
|
40707
|
+
ours.set(key(one), one);
|
|
40708
|
+
} else {
|
|
40709
|
+
settled.add(key(one));
|
|
40710
|
+
}
|
|
40711
|
+
}
|
|
40712
|
+
}
|
|
40713
|
+
}
|
|
40714
|
+
return [...ours.values()];
|
|
40715
|
+
}
|
|
40716
|
+
function changedReport(edits, rewrites) {
|
|
40717
|
+
return rewrites.map((rewrite) => {
|
|
40718
|
+
const lines = rewrite.after.split(`
|
|
40719
|
+
`);
|
|
40720
|
+
const byLine = new Map;
|
|
40721
|
+
const starts = [0];
|
|
40722
|
+
for (let at = rewrite.before.indexOf(`
|
|
40723
|
+
`);at !== -1; at = rewrite.before.indexOf(`
|
|
40724
|
+
`, at + 1)) {
|
|
40725
|
+
starts.push(at + 1);
|
|
40726
|
+
}
|
|
40727
|
+
let count = 0;
|
|
40728
|
+
for (const edit of edits) {
|
|
40729
|
+
if (edit.file !== rewrite.path) {
|
|
40730
|
+
continue;
|
|
40731
|
+
}
|
|
40732
|
+
count += 1;
|
|
40733
|
+
let line = 0;
|
|
40734
|
+
while (line + 1 < starts.length && (starts[line + 1] ?? 0) <= edit.offset) {
|
|
40735
|
+
line += 1;
|
|
40736
|
+
}
|
|
40737
|
+
byLine.set(line, (byLine.get(line) ?? new Set).add(edit.kind));
|
|
40738
|
+
}
|
|
40739
|
+
return {
|
|
40740
|
+
file: rewrite.path,
|
|
40741
|
+
edits: count,
|
|
40742
|
+
lines: [...byLine.entries()].sort(([a], [b]) => a - b).map(([line, kinds]) => ({
|
|
40743
|
+
line: line + 1,
|
|
40744
|
+
kinds: [...kinds],
|
|
40745
|
+
text: (lines[line] ?? "").replace(/\r$/, "").trim()
|
|
40746
|
+
}))
|
|
40747
|
+
};
|
|
40748
|
+
});
|
|
40749
|
+
}
|
|
40750
|
+
function summaryOf(edits, mentions, rewrites) {
|
|
40751
|
+
const count = (values) => {
|
|
40752
|
+
const counted = {};
|
|
40753
|
+
for (const value of values) {
|
|
40754
|
+
counted[value] = (counted[value] ?? 0) + 1;
|
|
40755
|
+
}
|
|
40756
|
+
return counted;
|
|
40757
|
+
};
|
|
40758
|
+
return {
|
|
40759
|
+
filesChanged: rewrites.length,
|
|
40760
|
+
edits: edits.length,
|
|
40761
|
+
editsByKind: count(edits.map((edit) => edit.kind)),
|
|
40762
|
+
leftAlone: mentions.length,
|
|
40763
|
+
leftAloneByKind: count(mentions.map((mention) => mention.kind))
|
|
40764
|
+
};
|
|
40765
|
+
}
|
|
40766
|
+
var WHAT;
|
|
40767
|
+
var init_rename_symbol = __esm(() => {
|
|
40768
|
+
init_gdscript_source();
|
|
40769
|
+
init_project_scan();
|
|
40770
|
+
init_rename();
|
|
40771
|
+
init_resources();
|
|
40772
|
+
WHAT = {
|
|
40773
|
+
func: "method",
|
|
40774
|
+
var: "variable",
|
|
40775
|
+
const: "constant",
|
|
40776
|
+
signal: "signal",
|
|
40777
|
+
enum: "enum",
|
|
40778
|
+
class: "inner class",
|
|
40779
|
+
enumValue: "enum value"
|
|
40780
|
+
};
|
|
39199
40781
|
});
|
|
39200
40782
|
|
|
39201
40783
|
// src/restart-note.ts
|
|
39202
|
-
import { mkdirSync as
|
|
39203
|
-
import { dirname as
|
|
40784
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync19, rmSync as rmSync9, writeFileSync as writeFileSync13 } from "node:fs";
|
|
40785
|
+
import { dirname as dirname16, join as join22 } from "node:path";
|
|
39204
40786
|
function restartNotePath(projectPath) {
|
|
39205
|
-
return
|
|
40787
|
+
return join22(projectPath, ".godot", "gdharness-restart.json");
|
|
39206
40788
|
}
|
|
39207
40789
|
function noteRestartBegun(note) {
|
|
39208
40790
|
const path = restartNotePath(note.projectPath);
|
|
39209
40791
|
try {
|
|
39210
|
-
|
|
39211
|
-
|
|
40792
|
+
mkdirSync11(dirname16(path), { recursive: true });
|
|
40793
|
+
writeFileSync13(path, `${JSON.stringify(note, null, 2)}
|
|
39212
40794
|
`, "utf8");
|
|
39213
40795
|
} catch (error) {
|
|
39214
40796
|
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
@@ -39216,7 +40798,7 @@ function noteRestartBegun(note) {
|
|
|
39216
40798
|
}
|
|
39217
40799
|
function restartOwed(projectPath) {
|
|
39218
40800
|
try {
|
|
39219
|
-
const parsed = JSON.parse(
|
|
40801
|
+
const parsed = JSON.parse(readFileSync19(restartNotePath(projectPath), "utf8"));
|
|
39220
40802
|
if (typeof parsed !== "object" || parsed === null) {
|
|
39221
40803
|
return null;
|
|
39222
40804
|
}
|
|
@@ -39244,62 +40826,62 @@ var init_restart_note = __esm(() => {
|
|
|
39244
40826
|
import { execFile as execFile4 } from "node:child_process";
|
|
39245
40827
|
import { createHash as createHash4 } from "node:crypto";
|
|
39246
40828
|
import {
|
|
39247
|
-
mkdirSync as
|
|
40829
|
+
mkdirSync as mkdirSync12,
|
|
39248
40830
|
openSync as openSync2,
|
|
39249
40831
|
readdirSync as readdirSync8,
|
|
39250
|
-
readFileSync as
|
|
39251
|
-
realpathSync as
|
|
40832
|
+
readFileSync as readFileSync20,
|
|
40833
|
+
realpathSync as realpathSync6,
|
|
39252
40834
|
rmSync as rmSync10,
|
|
39253
40835
|
statSync as statSync8,
|
|
39254
|
-
writeFileSync as
|
|
40836
|
+
writeFileSync as writeFileSync14
|
|
39255
40837
|
} from "node:fs";
|
|
39256
|
-
import { basename as
|
|
40838
|
+
import { basename as basename6, dirname as dirname17, join as join23, resolve as resolve7 } from "node:path";
|
|
39257
40839
|
import { promisify as promisify4 } from "node:util";
|
|
39258
40840
|
function runsDirectory() {
|
|
39259
|
-
return
|
|
40841
|
+
return join23(runtimeDirectory(), "runs");
|
|
39260
40842
|
}
|
|
39261
40843
|
function realAsFarAsItExists(path) {
|
|
39262
40844
|
const missing = [];
|
|
39263
40845
|
let at = path;
|
|
39264
40846
|
for (;; ) {
|
|
39265
40847
|
try {
|
|
39266
|
-
return
|
|
40848
|
+
return join23(realpathSync6.native(at), ...missing);
|
|
39267
40849
|
} catch {
|
|
39268
|
-
const parent =
|
|
40850
|
+
const parent = dirname17(at);
|
|
39269
40851
|
if (parent === at) {
|
|
39270
40852
|
return path;
|
|
39271
40853
|
}
|
|
39272
|
-
missing.unshift(
|
|
40854
|
+
missing.unshift(basename6(at));
|
|
39273
40855
|
at = parent;
|
|
39274
40856
|
}
|
|
39275
40857
|
}
|
|
39276
40858
|
}
|
|
39277
40859
|
function projectKey(projectPath) {
|
|
39278
|
-
const real = realAsFarAsItExists(
|
|
40860
|
+
const real = realAsFarAsItExists(resolve7(projectPath)).replace(/[\\/]+$/, "");
|
|
39279
40861
|
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
39280
40862
|
return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
39281
40863
|
}
|
|
39282
40864
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
39283
|
-
return
|
|
40865
|
+
return join23(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
39284
40866
|
}
|
|
39285
40867
|
function openTranscript(startedAt) {
|
|
39286
|
-
|
|
39287
|
-
const path =
|
|
40868
|
+
mkdirSync12(runsDirectory(), { recursive: true });
|
|
40869
|
+
const path = join23(runsDirectory(), `run-${startedAt}.log`);
|
|
39288
40870
|
return { path, fd: openSync2(path, "a") };
|
|
39289
40871
|
}
|
|
39290
40872
|
function noteIsAbout(note, playing) {
|
|
39291
40873
|
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
39292
40874
|
}
|
|
39293
40875
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
39294
|
-
return
|
|
40876
|
+
return join23(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
39295
40877
|
}
|
|
39296
40878
|
function writeEditorRunNote(note) {
|
|
39297
|
-
|
|
39298
|
-
|
|
40879
|
+
mkdirSync12(runsDirectory(), { recursive: true });
|
|
40880
|
+
writeFileSync14(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
|
|
39299
40881
|
}
|
|
39300
40882
|
function readEditorRunNote(projectPath) {
|
|
39301
40883
|
for (const directory of runtimeDirectories()) {
|
|
39302
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
40884
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join23(directory, "runs", "editor-run.json"));
|
|
39303
40885
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
39304
40886
|
return note;
|
|
39305
40887
|
}
|
|
@@ -39309,7 +40891,7 @@ function readEditorRunNote(projectPath) {
|
|
|
39309
40891
|
function editorNoteAt(path) {
|
|
39310
40892
|
let parsed;
|
|
39311
40893
|
try {
|
|
39312
|
-
parsed = JSON.parse(
|
|
40894
|
+
parsed = JSON.parse(readFileSync20(path, "utf8"));
|
|
39313
40895
|
} catch {
|
|
39314
40896
|
return null;
|
|
39315
40897
|
}
|
|
@@ -39333,7 +40915,7 @@ function readRunRecord(projectPath) {
|
|
|
39333
40915
|
if (own !== null) {
|
|
39334
40916
|
return own;
|
|
39335
40917
|
}
|
|
39336
|
-
const shared = recordAt(
|
|
40918
|
+
const shared = recordAt(join23(directory, "runs", SHARED_NOTE));
|
|
39337
40919
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
39338
40920
|
return shared;
|
|
39339
40921
|
}
|
|
@@ -39345,13 +40927,13 @@ function everyRunRecord() {
|
|
|
39345
40927
|
for (const directory of runtimeDirectories()) {
|
|
39346
40928
|
let entries;
|
|
39347
40929
|
try {
|
|
39348
|
-
entries = readdirSync8(
|
|
40930
|
+
entries = readdirSync8(join23(directory, "runs"));
|
|
39349
40931
|
} catch {
|
|
39350
40932
|
continue;
|
|
39351
40933
|
}
|
|
39352
40934
|
for (const entry of entries) {
|
|
39353
40935
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
39354
|
-
const record = recordAt(
|
|
40936
|
+
const record = recordAt(join23(directory, "runs", entry));
|
|
39355
40937
|
if (record !== null) {
|
|
39356
40938
|
found.push(record);
|
|
39357
40939
|
}
|
|
@@ -39363,7 +40945,7 @@ function everyRunRecord() {
|
|
|
39363
40945
|
function recordAt(path) {
|
|
39364
40946
|
let parsed;
|
|
39365
40947
|
try {
|
|
39366
|
-
parsed = JSON.parse(
|
|
40948
|
+
parsed = JSON.parse(readFileSync20(path, "utf8"));
|
|
39367
40949
|
} catch {
|
|
39368
40950
|
return null;
|
|
39369
40951
|
}
|
|
@@ -39414,7 +40996,7 @@ async function runningAs(pid) {
|
|
|
39414
40996
|
const began = await startedAt(pid, null);
|
|
39415
40997
|
const when = began === null ? {} : { startedAt: began };
|
|
39416
40998
|
if (process.platform === "linux") {
|
|
39417
|
-
const raw =
|
|
40999
|
+
const raw = readFileSync20(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
39418
41000
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
39419
41001
|
}
|
|
39420
41002
|
const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
|
|
@@ -39520,7 +41102,7 @@ function whyNotTheRun(record, running, needed) {
|
|
|
39520
41102
|
if (after > SAME_RUN_WINDOW_MS) {
|
|
39521
41103
|
return `that process started ${Math.round(after / 1000)} seconds after the run had`;
|
|
39522
41104
|
}
|
|
39523
|
-
const engine = record.command === undefined ? null :
|
|
41105
|
+
const engine = record.command === undefined ? null : basename6(record.command);
|
|
39524
41106
|
const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
|
|
39525
41107
|
const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
|
|
39526
41108
|
if (wanted !== null && !said.includes(wanted)) {
|
|
@@ -39559,7 +41141,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
39559
41141
|
}
|
|
39560
41142
|
function clearRunRecord(projectPath) {
|
|
39561
41143
|
for (const directory of runtimeDirectories()) {
|
|
39562
|
-
const shared =
|
|
41144
|
+
const shared = join23(directory, "runs", SHARED_NOTE);
|
|
39563
41145
|
const sharedRecord = recordAt(shared);
|
|
39564
41146
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
39565
41147
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
@@ -39589,7 +41171,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
39589
41171
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
39590
41172
|
continue;
|
|
39591
41173
|
}
|
|
39592
|
-
const path =
|
|
41174
|
+
const path = join23(directory, entry);
|
|
39593
41175
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
39594
41176
|
continue;
|
|
39595
41177
|
}
|
|
@@ -39616,17 +41198,17 @@ import {
|
|
|
39616
41198
|
closeSync as closeSync2,
|
|
39617
41199
|
constants,
|
|
39618
41200
|
copyFileSync,
|
|
39619
|
-
existsSync as
|
|
41201
|
+
existsSync as existsSync19,
|
|
39620
41202
|
fstatSync,
|
|
39621
|
-
mkdirSync as
|
|
41203
|
+
mkdirSync as mkdirSync13,
|
|
39622
41204
|
openSync as openSync3,
|
|
39623
41205
|
readdirSync as readdirSync9,
|
|
39624
|
-
readFileSync as
|
|
41206
|
+
readFileSync as readFileSync21,
|
|
39625
41207
|
readSync as readSync2,
|
|
39626
41208
|
rmdirSync as rmdirSync2,
|
|
39627
41209
|
statSync as statSync9
|
|
39628
41210
|
} from "node:fs";
|
|
39629
|
-
import { basename as
|
|
41211
|
+
import { basename as basename7, dirname as dirname18, extname as extname2, isAbsolute as isAbsolute3, join as join24, normalize as normalize2, relative as relative5 } from "node:path";
|
|
39630
41212
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
39631
41213
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
39632
41214
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -39647,16 +41229,16 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
39647
41229
|
if (extname2(outputPath).toLowerCase() !== ".png") {
|
|
39648
41230
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
39649
41231
|
}
|
|
39650
|
-
const directory = realPathOr(
|
|
39651
|
-
if (!
|
|
39652
|
-
return `outputPath's directory ${
|
|
41232
|
+
const directory = realPathOr(dirname18(outputPath));
|
|
41233
|
+
if (!existsSync19(directory)) {
|
|
41234
|
+
return `outputPath's directory ${dirname18(outputPath)} does not exist; a capture creates no directories.`;
|
|
39653
41235
|
}
|
|
39654
|
-
const where =
|
|
41236
|
+
const where = join24(directory, basename7(outputPath));
|
|
39655
41237
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
39656
41238
|
if (inside !== undefined) {
|
|
39657
41239
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
39658
41240
|
}
|
|
39659
|
-
if (
|
|
41241
|
+
if (existsSync19(where)) {
|
|
39660
41242
|
return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
|
|
39661
41243
|
}
|
|
39662
41244
|
return null;
|
|
@@ -39684,7 +41266,7 @@ function patienceForFrames(frames, atLeast) {
|
|
|
39684
41266
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
39685
41267
|
}
|
|
39686
41268
|
function projectSpelling(projectPath, absolutePath) {
|
|
39687
|
-
return `res://${
|
|
41269
|
+
return `res://${relative5(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
|
|
39688
41270
|
}
|
|
39689
41271
|
function whichGame(args) {
|
|
39690
41272
|
return { projectPath: args["projectPath"], pid: args["pid"] };
|
|
@@ -40182,7 +41764,7 @@ function aboveTheRunner(entry) {
|
|
|
40182
41764
|
}
|
|
40183
41765
|
function elsewhereIn(projectPath, asked) {
|
|
40184
41766
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
40185
|
-
if (wanted === "" ||
|
|
41767
|
+
if (wanted === "" || existsSync19(join24(projectPath, wanted))) {
|
|
40186
41768
|
return "";
|
|
40187
41769
|
}
|
|
40188
41770
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -40196,7 +41778,7 @@ function elsewhereIn(projectPath, asked) {
|
|
|
40196
41778
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
40197
41779
|
}
|
|
40198
41780
|
function hasMainScene(projectFile) {
|
|
40199
|
-
const scene = parseProjectGodot(
|
|
41781
|
+
const scene = parseProjectGodot(readFileSync21(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
40200
41782
|
return typeof scene === "string" && scene !== "";
|
|
40201
41783
|
}
|
|
40202
41784
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -40204,13 +41786,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
40204
41786
|
return false;
|
|
40205
41787
|
}
|
|
40206
41788
|
try {
|
|
40207
|
-
return
|
|
41789
|
+
return readFileSync21(join24(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
40208
41790
|
} catch {
|
|
40209
41791
|
return false;
|
|
40210
41792
|
}
|
|
40211
41793
|
}
|
|
40212
41794
|
function editorPlaysHeadless(projectFile) {
|
|
40213
|
-
const runArgs = parseProjectGodot(
|
|
41795
|
+
const runArgs = parseProjectGodot(readFileSync21(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
40214
41796
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
40215
41797
|
}
|
|
40216
41798
|
function camelCased(params) {
|
|
@@ -40229,7 +41811,7 @@ function needsItsOwnProcess(asked) {
|
|
|
40229
41811
|
class GodotServer {
|
|
40230
41812
|
mcp;
|
|
40231
41813
|
locator = new GodotLocator;
|
|
40232
|
-
operationsScript =
|
|
41814
|
+
operationsScript = join24(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
40233
41815
|
godotBridge;
|
|
40234
41816
|
tools = buildToolDefinitions();
|
|
40235
41817
|
activeProcess = null;
|
|
@@ -40259,6 +41841,7 @@ class GodotServer {
|
|
|
40259
41841
|
constructor() {
|
|
40260
41842
|
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
40261
41843
|
this.godotBridge = getDefaultBridge(this.ownProject);
|
|
41844
|
+
this.godotBridge.identifyEditorsWith(oneShotOptionOf);
|
|
40262
41845
|
this.godotBridge.on("godot_connected", () => {
|
|
40263
41846
|
this.settleTheLaunchReading();
|
|
40264
41847
|
});
|
|
@@ -40531,7 +42114,7 @@ class GodotServer {
|
|
|
40531
42114
|
return checked.response;
|
|
40532
42115
|
}
|
|
40533
42116
|
const named = args["projectPath"];
|
|
40534
|
-
if (typeof named === "string" &&
|
|
42117
|
+
if (typeof named === "string" && existsSync19(join24(named, "project.godot"))) {
|
|
40535
42118
|
this.lastProjectPath = named;
|
|
40536
42119
|
}
|
|
40537
42120
|
this.updates.refresh();
|
|
@@ -40724,6 +42307,9 @@ class GodotServer {
|
|
|
40724
42307
|
return { ok: true, op };
|
|
40725
42308
|
}
|
|
40726
42309
|
async dispatch(tool, op, args) {
|
|
42310
|
+
if (tool !== "editor_status" && this.godotBridge.isArriving()) {
|
|
42311
|
+
await this.waitForBridge(() => !this.godotBridge.isArriving(), Date.now() + GREETING_WAIT_MS);
|
|
42312
|
+
}
|
|
40727
42313
|
if (ENGINE_PASSES[tool]?.[op] !== undefined) {
|
|
40728
42314
|
return op === "reimport" ? await this.handleReimport(args) : await this.handleRefreshUids(args);
|
|
40729
42315
|
}
|
|
@@ -40824,6 +42410,8 @@ class GodotServer {
|
|
|
40824
42410
|
default:
|
|
40825
42411
|
return await bridge("connect_animation_states");
|
|
40826
42412
|
}
|
|
42413
|
+
case "script_edit":
|
|
42414
|
+
return await this.handleRenameSymbol(args);
|
|
40827
42415
|
case "script_info":
|
|
40828
42416
|
switch (op) {
|
|
40829
42417
|
case "symbols":
|
|
@@ -40999,8 +42587,8 @@ class GodotServer {
|
|
|
40999
42587
|
if (path === undefined) {
|
|
41000
42588
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
41001
42589
|
}
|
|
41002
|
-
const file =
|
|
41003
|
-
if (!
|
|
42590
|
+
const file = join24(path, "project.godot");
|
|
42591
|
+
if (!existsSync19(file)) {
|
|
41004
42592
|
return {
|
|
41005
42593
|
ok: false,
|
|
41006
42594
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -41440,8 +43028,39 @@ class GodotServer {
|
|
|
41440
43028
|
}
|
|
41441
43029
|
const viaEditor = this.editorHasOpen(projectPath);
|
|
41442
43030
|
const extra = {};
|
|
43031
|
+
const statusAsk = resourcePath === undefined ? { includeUpToDate: true } : { resourcePath };
|
|
41443
43032
|
if (viaEditor) {
|
|
41444
|
-
const
|
|
43033
|
+
const scan = await this.handleRescanFilesystem({ projectPath, timeoutMs });
|
|
43034
|
+
if (scan.isError === true) {
|
|
43035
|
+
return { ok: false, response: scan };
|
|
43036
|
+
}
|
|
43037
|
+
const scanned = asParams(JSON.parse(scan.content[0]?.text ?? "{}"));
|
|
43038
|
+
if (scanned["stillWorking"] === true) {
|
|
43039
|
+
return {
|
|
43040
|
+
ok: true,
|
|
43041
|
+
payload: {
|
|
43042
|
+
via: "editor",
|
|
43043
|
+
stillImporting: true,
|
|
43044
|
+
asked: targets,
|
|
43045
|
+
...sourceless,
|
|
43046
|
+
note: `The editor's scan of the project had not finished after ${timeoutMs} ms, and the reimport waits for it. project_import status says what it has done so far; pass a longer timeoutMs to wait it out.`
|
|
43047
|
+
}
|
|
43048
|
+
};
|
|
43049
|
+
}
|
|
43050
|
+
let remaining = targets;
|
|
43051
|
+
if (!force) {
|
|
43052
|
+
const between = await this.operation("get_import_status", statusAsk, projectPath);
|
|
43053
|
+
if (!between.ok) {
|
|
43054
|
+
return { ok: false, response: this.answer(between) };
|
|
43055
|
+
}
|
|
43056
|
+
const fresh = new Map(importStatuses(between.payload).map((one) => [one.path, one.status]));
|
|
43057
|
+
const caughtUp = targets.filter((path) => fresh.get(path) === "up_to_date");
|
|
43058
|
+
remaining = targets.filter((path) => !caughtUp.includes(path));
|
|
43059
|
+
if (caughtUp.length > 0) {
|
|
43060
|
+
extra["importedByTheScan"] = caughtUp;
|
|
43061
|
+
}
|
|
43062
|
+
}
|
|
43063
|
+
const inEditor = remaining.length === 0 ? { ok: true, finished: true } : await this.reimportInTheEditor(remaining, timeoutMs);
|
|
41445
43064
|
if (!inEditor.ok) {
|
|
41446
43065
|
return inEditor;
|
|
41447
43066
|
}
|
|
@@ -41472,17 +43091,22 @@ class GodotServer {
|
|
|
41472
43091
|
extra["extensionNote"] = imported.extensionNote;
|
|
41473
43092
|
}
|
|
41474
43093
|
}
|
|
41475
|
-
const after = await this.operation("get_import_status",
|
|
43094
|
+
const after = await this.operation("get_import_status", statusAsk, projectPath);
|
|
41476
43095
|
if (!after.ok) {
|
|
41477
43096
|
return { ok: false, response: this.answer(after) };
|
|
41478
43097
|
}
|
|
41479
43098
|
const now = new Map(importStatuses(after.payload).map((one) => [one.path, one]));
|
|
41480
43099
|
const reimported = targets.filter((path) => now.get(path)?.status === "up_to_date");
|
|
41481
|
-
const
|
|
41482
|
-
|
|
41483
|
-
status
|
|
41484
|
-
|
|
41485
|
-
|
|
43100
|
+
const groups = new Map;
|
|
43101
|
+
for (const path of targets.filter((one) => now.get(one)?.status !== "up_to_date")) {
|
|
43102
|
+
const status = now.get(path)?.status ?? "unknown";
|
|
43103
|
+
const reason = now.get(path)?.reason;
|
|
43104
|
+
const key = `${status}\x00${reason ?? ""}`;
|
|
43105
|
+
const group = groups.get(key) ?? { status, ...reason === undefined ? {} : { reason }, paths: [] };
|
|
43106
|
+
group.paths.push(path);
|
|
43107
|
+
groups.set(key, group);
|
|
43108
|
+
}
|
|
43109
|
+
const notReimported = [...groups.values()];
|
|
41486
43110
|
return {
|
|
41487
43111
|
ok: true,
|
|
41488
43112
|
payload: {
|
|
@@ -41574,11 +43198,11 @@ class GodotServer {
|
|
|
41574
43198
|
if (!engine.ok) {
|
|
41575
43199
|
return engine.response;
|
|
41576
43200
|
}
|
|
41577
|
-
const application = parseProjectGodot(
|
|
43201
|
+
const application = parseProjectGodot(readFileSync21(project.value.file, "utf8"))["application"] ?? {};
|
|
41578
43202
|
const name = application["config/name"];
|
|
41579
43203
|
const mainScene = application["run/main_scene"];
|
|
41580
43204
|
const info = {
|
|
41581
|
-
name: typeof name === "string" && name !== "" ? name :
|
|
43205
|
+
name: typeof name === "string" && name !== "" ? name : basename7(project.value.path),
|
|
41582
43206
|
path: project.value.path,
|
|
41583
43207
|
mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
|
|
41584
43208
|
godotVersion: await this.godotVersion(engine.value),
|
|
@@ -41630,18 +43254,18 @@ class GodotServer {
|
|
|
41630
43254
|
if (!engine.ok) {
|
|
41631
43255
|
return engine.response;
|
|
41632
43256
|
}
|
|
41633
|
-
const folder =
|
|
43257
|
+
const folder = dirname18(output.absolutePath);
|
|
41634
43258
|
try {
|
|
41635
|
-
|
|
43259
|
+
mkdirSync13(folder, { recursive: true });
|
|
41636
43260
|
} catch (error) {
|
|
41637
|
-
return this.createErrorResponse(`Cannot create ${
|
|
43261
|
+
return this.createErrorResponse(`Cannot create ${dirname18(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
41638
43262
|
}
|
|
41639
43263
|
const debug = readBoolean(args, "debug") ?? false;
|
|
41640
43264
|
const exportLogs = scratchDirectory("export");
|
|
41641
43265
|
const exportArgs = [
|
|
41642
43266
|
"--headless",
|
|
41643
43267
|
"--log-file",
|
|
41644
|
-
|
|
43268
|
+
join24(exportLogs, "engine.log"),
|
|
41645
43269
|
"--path",
|
|
41646
43270
|
project.value.path,
|
|
41647
43271
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -41649,7 +43273,7 @@ class GodotServer {
|
|
|
41649
43273
|
output.absolutePath
|
|
41650
43274
|
];
|
|
41651
43275
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
41652
|
-
const writtenAt = () =>
|
|
43276
|
+
const writtenAt = () => existsSync19(output.absolutePath) ? statSync9(output.absolutePath).mtimeMs : null;
|
|
41653
43277
|
const before = writtenAt();
|
|
41654
43278
|
let ending;
|
|
41655
43279
|
try {
|
|
@@ -41672,7 +43296,7 @@ class GodotServer {
|
|
|
41672
43296
|
return contained.response;
|
|
41673
43297
|
}
|
|
41674
43298
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
41675
|
-
if (!
|
|
43299
|
+
if (!existsSync19(join24(project.value.path, runner))) {
|
|
41676
43300
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
41677
43301
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
41678
43302
|
]);
|
|
@@ -41739,21 +43363,21 @@ class GodotServer {
|
|
|
41739
43363
|
await untilGone([run.process.pid]);
|
|
41740
43364
|
}
|
|
41741
43365
|
await run.settled;
|
|
41742
|
-
const reportsDir =
|
|
43366
|
+
const reportsDir = join24(project.value.path, ".godot", ...ours.split("/"));
|
|
41743
43367
|
let report = null;
|
|
41744
43368
|
let reportProblem = null;
|
|
41745
43369
|
try {
|
|
41746
|
-
const written =
|
|
43370
|
+
const written = existsSync19(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
41747
43371
|
const newest = written.at(-1);
|
|
41748
43372
|
if (newest !== undefined) {
|
|
41749
|
-
report = parseJUnit(
|
|
43373
|
+
report = parseJUnit(readFileSync21(join24(reportsDir, newest, "results.xml"), "utf8"));
|
|
41750
43374
|
}
|
|
41751
43375
|
} catch (error) {
|
|
41752
43376
|
reportProblem = errorMessage(error);
|
|
41753
43377
|
} finally {
|
|
41754
43378
|
discard(reportsDir, userData);
|
|
41755
43379
|
try {
|
|
41756
|
-
rmdirSync2(
|
|
43380
|
+
rmdirSync2(dirname18(reportsDir));
|
|
41757
43381
|
} catch {}
|
|
41758
43382
|
}
|
|
41759
43383
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
@@ -41931,11 +43555,11 @@ class GodotServer {
|
|
|
41931
43555
|
const cached = cachedClasses(projectPath);
|
|
41932
43556
|
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
|
|
41933
43557
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
41934
|
-
if (!contained.ok || !
|
|
43558
|
+
if (!contained.ok || !existsSync19(contained.absolutePath)) {
|
|
41935
43559
|
return null;
|
|
41936
43560
|
}
|
|
41937
43561
|
try {
|
|
41938
|
-
return
|
|
43562
|
+
return readFileSync21(contained.absolutePath, "utf8");
|
|
41939
43563
|
} catch {
|
|
41940
43564
|
return null;
|
|
41941
43565
|
}
|
|
@@ -41991,23 +43615,26 @@ class GodotServer {
|
|
|
41991
43615
|
}
|
|
41992
43616
|
sourceOfClass(projectPath, resourcePath) {
|
|
41993
43617
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
41994
|
-
if (!contained.ok || !
|
|
43618
|
+
if (!contained.ok || !existsSync19(contained.absolutePath)) {
|
|
41995
43619
|
return null;
|
|
41996
43620
|
}
|
|
41997
43621
|
try {
|
|
41998
|
-
return
|
|
43622
|
+
return readFileSync21(contained.absolutePath, "utf8");
|
|
41999
43623
|
} catch {
|
|
42000
43624
|
return null;
|
|
42001
43625
|
}
|
|
42002
43626
|
}
|
|
42003
43627
|
async handleLSP(toolName, args) {
|
|
43628
|
+
return handleLSPTool(await this.lsp(), toolName, args);
|
|
43629
|
+
}
|
|
43630
|
+
async lsp() {
|
|
42004
43631
|
const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
|
|
42005
43632
|
if (this.lspClient !== null && this.lspClient.port !== port) {
|
|
42006
43633
|
await this.lspClient.disconnect();
|
|
42007
43634
|
this.lspClient = null;
|
|
42008
43635
|
}
|
|
42009
43636
|
this.lspClient ??= new GodotLSPClient(port);
|
|
42010
|
-
return
|
|
43637
|
+
return this.lspClient;
|
|
42011
43638
|
}
|
|
42012
43639
|
async handleDAP(toolName, args) {
|
|
42013
43640
|
return handleDAPTool(this.dap(), toolName, args);
|
|
@@ -42303,7 +43930,7 @@ class GodotServer {
|
|
|
42303
43930
|
return null;
|
|
42304
43931
|
}
|
|
42305
43932
|
try {
|
|
42306
|
-
return settingKeys(
|
|
43933
|
+
return settingKeys(readFileSync21(join24(projectPath, "project.godot"), "utf8"));
|
|
42307
43934
|
} catch {
|
|
42308
43935
|
return null;
|
|
42309
43936
|
}
|
|
@@ -42317,14 +43944,7 @@ class GodotServer {
|
|
|
42317
43944
|
]);
|
|
42318
43945
|
}
|
|
42319
43946
|
if (!hasSaidWhoItIs(before)) {
|
|
42320
|
-
|
|
42321
|
-
if (!settled) {
|
|
42322
|
-
return this.createErrorResponse(`The editor has connected and has not said who it is within ${GREETING_WAIT_MS / 1000}s, so whether this server or the editor itself restarts it cannot be told.`, [
|
|
42323
|
-
"editor_status says when it has, under greeting while it has not",
|
|
42324
|
-
"Then ask for the restart again"
|
|
42325
|
-
]);
|
|
42326
|
-
}
|
|
42327
|
-
return this.handleRestartEditor(args);
|
|
43947
|
+
return this.createErrorResponse(`The editor has connected and has not said who it is within ${GREETING_WAIT_MS / 1000}s, so whether this server or the editor itself restarts it cannot be told.`, ["editor_status says when it has, under greeting while it has not", "Then ask for the restart again"]);
|
|
42328
43948
|
}
|
|
42329
43949
|
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
42330
43950
|
if (!mine && args["hidden"] === true) {
|
|
@@ -42518,6 +44138,10 @@ class GodotServer {
|
|
|
42518
44138
|
const dap = portFromEnvOrNull("GDHARNESS_DAP_PORT") ?? await freePort(DEFAULT_DAP_PORT);
|
|
42519
44139
|
return { lsp, dap };
|
|
42520
44140
|
}
|
|
44141
|
+
editorOnTheBridge() {
|
|
44142
|
+
const pid = this.godotBridge.getStatus().editorPid;
|
|
44143
|
+
return pid === undefined ? "the editor on this bridge" : `the editor on this bridge (pid ${pid})`;
|
|
44144
|
+
}
|
|
42521
44145
|
async handleRunProject(args, op) {
|
|
42522
44146
|
const project = this.project(args);
|
|
42523
44147
|
if (!project.ok) {
|
|
@@ -42525,7 +44149,7 @@ class GodotServer {
|
|
|
42525
44149
|
}
|
|
42526
44150
|
const open = this.godotBridge.isConnected() ? this.godotBridge.getStatus().projectPath : undefined;
|
|
42527
44151
|
if (op !== "check" && open !== undefined && open !== "" && !isSameDirectory(open, project.value.path)) {
|
|
42528
|
-
return this.createErrorResponse(`This start names ${project.value.path}, and
|
|
44152
|
+
return this.createErrorResponse(`This start names ${project.value.path}, and ${this.editorOnTheBridge()} has ${open} open.`, [
|
|
42529
44153
|
"editor_status names the project this server's editor is showing",
|
|
42530
44154
|
"One server serves one editor: start a second server for the other project"
|
|
42531
44155
|
]);
|
|
@@ -42628,7 +44252,7 @@ class GodotServer {
|
|
|
42628
44252
|
}
|
|
42629
44253
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
42630
44254
|
const budgetMs = budget.ms;
|
|
42631
|
-
if (!
|
|
44255
|
+
if (!existsSync19(join24(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
42632
44256
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
42633
44257
|
}
|
|
42634
44258
|
let asking = null;
|
|
@@ -42879,7 +44503,7 @@ class GodotServer {
|
|
|
42879
44503
|
return 0;
|
|
42880
44504
|
}
|
|
42881
44505
|
try {
|
|
42882
|
-
return reportAlreadyInTranscript(
|
|
44506
|
+
return reportAlreadyInTranscript(readFileSync21(game.transcript), readFileSync21(report));
|
|
42883
44507
|
} catch (error) {
|
|
42884
44508
|
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
42885
44509
|
return 0;
|
|
@@ -43637,7 +45261,7 @@ class GodotServer {
|
|
|
43637
45261
|
notes.push(endedWithoutACode({
|
|
43638
45262
|
throughEditor: run.throughEditor,
|
|
43639
45263
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
43640
|
-
couldAnnounce: run.projectPath !== null &&
|
|
45264
|
+
couldAnnounce: run.projectPath !== null && existsSync19(join24(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
43641
45265
|
endedHere: typeof run.endedHere === "string",
|
|
43642
45266
|
pickedUp: run.pickedUp === true
|
|
43643
45267
|
}));
|
|
@@ -43884,11 +45508,9 @@ class GodotServer {
|
|
|
43884
45508
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 30000;
|
|
43885
45509
|
const started = Date.now();
|
|
43886
45510
|
const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
|
|
43887
|
-
|
|
43888
|
-
|
|
43889
|
-
|
|
43890
|
-
const atRisk = before === null ? [] : blind.unseen.filter((one) => before.has(one.className));
|
|
43891
|
-
const ghosts = blind.stillHeld ?? [];
|
|
45511
|
+
let before = null;
|
|
45512
|
+
let writtenBefore = null;
|
|
45513
|
+
let blind = { unseen: [] };
|
|
43892
45514
|
let waitedForEditorMs = 0;
|
|
43893
45515
|
let first;
|
|
43894
45516
|
let completedBefore;
|
|
@@ -43904,6 +45526,9 @@ class GodotServer {
|
|
|
43904
45526
|
if (wasBusy) {
|
|
43905
45527
|
waitedForEditorMs += Date.now() - busySince;
|
|
43906
45528
|
}
|
|
45529
|
+
before = projectPath === "" ? null : cachedClasses(projectPath);
|
|
45530
|
+
writtenBefore = projectPath === "" ? null : cacheWrittenAt(projectPath);
|
|
45531
|
+
blind = await this.classesTheEditorCannotSee(args);
|
|
43907
45532
|
first = await this.handleViaBridge("rescan_filesystem", args);
|
|
43908
45533
|
if (first.isError) {
|
|
43909
45534
|
return first;
|
|
@@ -43920,6 +45545,9 @@ class GodotServer {
|
|
|
43920
45545
|
}
|
|
43921
45546
|
await new Promise((settle) => setTimeout(settle, 100));
|
|
43922
45547
|
}
|
|
45548
|
+
const held = before;
|
|
45549
|
+
const atRisk = held === null ? [] : blind.unseen.filter((one) => held.has(one.className));
|
|
45550
|
+
const ghosts = blind.stillHeld ?? [];
|
|
43923
45551
|
let busy = true;
|
|
43924
45552
|
let idleSince = null;
|
|
43925
45553
|
while (Date.now() - started < timeoutMs) {
|
|
@@ -44057,9 +45685,12 @@ class GodotServer {
|
|
|
44057
45685
|
});
|
|
44058
45686
|
}
|
|
44059
45687
|
async reloadDependents(projectPath, classes) {
|
|
45688
|
+
return await this.reloadInEditor(scriptsNaming(projectPath, classes));
|
|
45689
|
+
}
|
|
45690
|
+
async reloadInEditor(scripts) {
|
|
44060
45691
|
const reloaded = [];
|
|
44061
45692
|
const notReloaded = [];
|
|
44062
|
-
for (const { script: scriptPath, tool } of
|
|
45693
|
+
for (const { script: scriptPath, tool } of scripts) {
|
|
44063
45694
|
if (tool) {
|
|
44064
45695
|
notReloaded.push({
|
|
44065
45696
|
scriptPath,
|
|
@@ -44176,6 +45807,159 @@ class GodotServer {
|
|
|
44176
45807
|
});
|
|
44177
45808
|
return this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
44178
45809
|
}
|
|
45810
|
+
async handleRenameSymbol(given) {
|
|
45811
|
+
const project = this.project(given);
|
|
45812
|
+
if (!project.ok) {
|
|
45813
|
+
return project.response;
|
|
45814
|
+
}
|
|
45815
|
+
const contained = this.containProjectFiles(given);
|
|
45816
|
+
if (!contained.ok) {
|
|
45817
|
+
return contained.response;
|
|
45818
|
+
}
|
|
45819
|
+
const args = contained.value;
|
|
45820
|
+
const projectPath = project.value.path;
|
|
45821
|
+
const scriptPath = resolveWithinProject(projectPath, readNonEmptyString(args, "scriptPath") ?? "");
|
|
45822
|
+
if (!scriptPath.ok) {
|
|
45823
|
+
return this.createErrorResponse(scriptPath.reason);
|
|
45824
|
+
}
|
|
45825
|
+
if (!existsSync19(scriptPath.absolutePath)) {
|
|
45826
|
+
return this.createErrorResponse(`Script file does not exist: ${scriptPath.absolutePath}`);
|
|
45827
|
+
}
|
|
45828
|
+
const asked = readNonEmptyString(args, "newScriptPath");
|
|
45829
|
+
const moveTo = asked === undefined ? null : resolveWithinProject(projectPath, asked);
|
|
45830
|
+
if (moveTo !== null && !moveTo.ok) {
|
|
45831
|
+
return this.createErrorResponse(moveTo.reason);
|
|
45832
|
+
}
|
|
45833
|
+
const languageServer = {
|
|
45834
|
+
problem: null,
|
|
45835
|
+
initialized: false
|
|
45836
|
+
};
|
|
45837
|
+
const askTheLanguageServer = async (question) => {
|
|
45838
|
+
const client = await this.lsp();
|
|
45839
|
+
try {
|
|
45840
|
+
if (!languageServer.initialized) {
|
|
45841
|
+
await client.initialize(projectPath);
|
|
45842
|
+
languageServer.initialized = true;
|
|
45843
|
+
}
|
|
45844
|
+
return await question(client);
|
|
45845
|
+
} catch (error) {
|
|
45846
|
+
languageServer.problem = normalizeLSPError(error, client.port);
|
|
45847
|
+
throw error;
|
|
45848
|
+
}
|
|
45849
|
+
};
|
|
45850
|
+
const symbol = readNonEmptyString(args, "symbol") ?? "";
|
|
45851
|
+
const newName = readNonEmptyString(args, "newName") ?? "";
|
|
45852
|
+
const outcome = await renameSymbol({
|
|
45853
|
+
projectPath,
|
|
45854
|
+
scriptPath: scriptPath.absolutePath,
|
|
45855
|
+
symbol,
|
|
45856
|
+
newName,
|
|
45857
|
+
newScriptPath: moveTo === null ? null : moveTo.absolutePath
|
|
45858
|
+
}, {
|
|
45859
|
+
namesTaken: async (names, base) => {
|
|
45860
|
+
const asked = await this.operation("names_taken", { names: [...names], base: base ?? "" }, projectPath);
|
|
45861
|
+
if (!asked.ok) {
|
|
45862
|
+
throw new Error(`the engine could not be asked whether ${names.join(" or ")} is taken: ${asked.message}`);
|
|
45863
|
+
}
|
|
45864
|
+
return asParams(asked.payload["taken"]);
|
|
45865
|
+
},
|
|
45866
|
+
references: async (absolute, text, at) => await askTheLanguageServer((client) => client.getReferences(absolute, text, at.line, at.character)),
|
|
45867
|
+
definitions: async (absolute, text, at) => await askTheLanguageServer((client) => client.getDefinitions(absolute, text, at.line, at.character))
|
|
45868
|
+
}).catch((error) => ({ ok: false, reason: errorMessage(error) }));
|
|
45869
|
+
if (!outcome.ok) {
|
|
45870
|
+
if (languageServer.problem !== null) {
|
|
45871
|
+
return this.createErrorResponse(`Renaming a member needs the editor's language server, because which uses of the name are this class's depends on the types only the engine's analyser knows. ${languageServer.problem}`, [
|
|
45872
|
+
"editor_launch opens the editor, whose language server this uses",
|
|
45873
|
+
"A class_name is renamed from the files and needs no editor"
|
|
45874
|
+
]);
|
|
45875
|
+
}
|
|
45876
|
+
const said = outcome.conflicts === undefined ? "" : ` ${outcome.conflicts.join("; ")}.`;
|
|
45877
|
+
return this.createErrorResponse(`${outcome.reason}${said}`, outcome.advice ?? []);
|
|
45878
|
+
}
|
|
45879
|
+
if (readBoolean(args, "preview") === true) {
|
|
45880
|
+
return this.jsonTextResponse({ preview: true, ...outcome.report });
|
|
45881
|
+
}
|
|
45882
|
+
try {
|
|
45883
|
+
applyRewrites(outcome.rewrites);
|
|
45884
|
+
} catch (error) {
|
|
45885
|
+
return this.createErrorResponse(`Nothing was renamed: ${errorMessage(error)}`);
|
|
45886
|
+
}
|
|
45887
|
+
const settled = this.godotBridge.isConnected() ? await this.settleRenameInEditor(projectPath, outcome.scriptsInOrder, {
|
|
45888
|
+
gone: outcome.moved === null ? [] : [outcome.moved.from],
|
|
45889
|
+
oldClass: outcome.isClass ? symbol : null,
|
|
45890
|
+
newClass: outcome.isClass ? newName : null
|
|
45891
|
+
}) : await this.settleRenameOnDisk(projectPath, outcome.scriptsInOrder, {
|
|
45892
|
+
rebuildCache: outcome.isClass || outcome.moved !== null,
|
|
45893
|
+
moved: outcome.moved !== null
|
|
45894
|
+
});
|
|
45895
|
+
return this.jsonTextResponse({ ok: settled.ok, ...outcome.report, ...settled.report });
|
|
45896
|
+
}
|
|
45897
|
+
async settleRenameInEditor(projectPath, changed, names) {
|
|
45898
|
+
const { gone, oldClass, newClass } = names;
|
|
45899
|
+
const scanned = asParams(JSON.parse((await this.handleRescanFilesystem({
|
|
45900
|
+
projectPath,
|
|
45901
|
+
timeoutMs: 60000,
|
|
45902
|
+
updateFiles: [...gone, ...changed]
|
|
45903
|
+
})).content[0]?.text ?? "{}"));
|
|
45904
|
+
const reloadedByTheScan = new Set((readArray(scanned, "dependentsReloaded") ?? []).map(String));
|
|
45905
|
+
const toReload = [];
|
|
45906
|
+
for (const script of changed) {
|
|
45907
|
+
if (!reloadedByTheScan.has(script)) {
|
|
45908
|
+
const source = this.sourceOfClass(projectPath, script);
|
|
45909
|
+
toReload.push({ script, tool: source !== null && isToolScript(source) });
|
|
45910
|
+
}
|
|
45911
|
+
}
|
|
45912
|
+
const done = await this.reloadInEditor(toReload);
|
|
45913
|
+
const notReloaded = done.notReloaded;
|
|
45914
|
+
const reloaded = [...reloadedByTheScan, ...done.reloaded].sort();
|
|
45915
|
+
const checked = oldClass === null ? null : await this.classesTheEditorCannotSee({ projectPath });
|
|
45916
|
+
const stillHeld = oldClass !== null && (checked?.stillHeld?.includes(oldClass) === true || (readArray(scanned, "cacheDropped") ?? []).map(String).includes(oldClass) || (readArray(scanned, "cacheAtMissingPaths") ?? []).map(String).includes(oldClass));
|
|
45917
|
+
const unseen = newClass !== null && checked?.unseen.some((one) => one.className === newClass) === true;
|
|
45918
|
+
const notes = [];
|
|
45919
|
+
if (stillHeld) {
|
|
45920
|
+
notes.push(`The editor still holds ${oldClass} after the scan, so it resolves the old name until it is restarted with editor_launch restart, and writes it back into the class cache on every scan until then.`);
|
|
45921
|
+
}
|
|
45922
|
+
if (unseen) {
|
|
45923
|
+
notes.push(`The editor does not hold ${newClass} after the scan, so every use of it reads as an unknown identifier there: editor_rescan on its own, or editor_launch restart.`);
|
|
45924
|
+
}
|
|
45925
|
+
if (scanned["stillWorking"] === true) {
|
|
45926
|
+
notes.push("The editor was still scanning when the wait ran out, so it may not hold the new names yet.");
|
|
45927
|
+
}
|
|
45928
|
+
return {
|
|
45929
|
+
ok: scanned["stillWorking"] !== true && notReloaded.length === 0 && !stillHeld && !unseen,
|
|
45930
|
+
report: {
|
|
45931
|
+
editor: {
|
|
45932
|
+
reloaded,
|
|
45933
|
+
...notReloaded.length > 0 ? { notReloaded } : {},
|
|
45934
|
+
...stillHeld ? { stillHolds: oldClass } : {},
|
|
45935
|
+
...typeof scanned["note"] === "string" ? { scanNote: scanned["note"] } : {}
|
|
45936
|
+
},
|
|
45937
|
+
...notes.length > 0 ? { note: notes.join(" ") } : {}
|
|
45938
|
+
}
|
|
45939
|
+
};
|
|
45940
|
+
}
|
|
45941
|
+
async settleRenameOnDisk(projectPath, changed, asked) {
|
|
45942
|
+
const report = {};
|
|
45943
|
+
let ok = true;
|
|
45944
|
+
if (asked.moved) {
|
|
45945
|
+
const engine = await this.engine();
|
|
45946
|
+
const imported = engine.ok ? await runImport(engine.value, projectPath) : null;
|
|
45947
|
+
const problem = imported === null ? engine.ok ? "" : engine.response.content.map((block) => block.text).join(" ") : imported.ok ? null : imported.message;
|
|
45948
|
+
report["uidTable"] = problem === null ? "rebuilt by an import pass" : `not rebuilt: ${problem}`;
|
|
45949
|
+
ok &&= problem === null;
|
|
45950
|
+
}
|
|
45951
|
+
if (asked.rebuildCache) {
|
|
45952
|
+
const rebuilt = await this.rebuildClassCache(projectPath);
|
|
45953
|
+
report["classCache"] = rebuilt.ok ? rebuilt.payload : { rebuilt: false, problem: rebuilt.message };
|
|
45954
|
+
ok &&= rebuilt.ok;
|
|
45955
|
+
}
|
|
45956
|
+
if (changed.length > 0) {
|
|
45957
|
+
const compiled = await this.operation("check_scripts", { paths: [...changed] }, projectPath);
|
|
45958
|
+
report["compiled"] = compiled.ok ? { ...compiled.payload, ...engineExtras(compiled) } : { problem: compiled.message };
|
|
45959
|
+
ok &&= compiled.ok && (readArray(compiled.payload, "failed") ?? []).length === 0;
|
|
45960
|
+
}
|
|
45961
|
+
return { ok, report };
|
|
45962
|
+
}
|
|
44179
45963
|
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
44180
45964
|
const first = answered.content[0];
|
|
44181
45965
|
if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
|
|
@@ -44242,7 +46026,7 @@ class GodotServer {
|
|
|
44242
46026
|
const open = this.godotBridge.getStatus().projectPath;
|
|
44243
46027
|
const meant = readNonEmptyString(args, "projectPath");
|
|
44244
46028
|
if (open !== undefined && open !== "" && meant !== undefined && !isSameDirectory(open, meant)) {
|
|
44245
|
-
return this.createErrorResponse(`This call names ${meant}, and
|
|
46029
|
+
return this.createErrorResponse(`This call names ${meant}, and ${this.editorOnTheBridge()} has ${open} open.`, [
|
|
44246
46030
|
"editor_status names the project this server's editor is showing",
|
|
44247
46031
|
"One server serves one editor: start a second server for the other project"
|
|
44248
46032
|
]);
|
|
@@ -44304,7 +46088,7 @@ class GodotServer {
|
|
|
44304
46088
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
44305
46089
|
const run = await this.currentRun();
|
|
44306
46090
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
44307
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
46091
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync19(join24(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
|
|
44308
46092
|
return null;
|
|
44309
46093
|
}
|
|
44310
46094
|
const already = runtimesAnnounced().running;
|
|
@@ -44388,7 +46172,7 @@ class GodotServer {
|
|
|
44388
46172
|
}
|
|
44389
46173
|
}
|
|
44390
46174
|
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
44391
|
-
const screenshotPath = screenshotDir ?
|
|
46175
|
+
const screenshotPath = screenshotDir ? join24(screenshotDir, "capture.png") : null;
|
|
44392
46176
|
const staleNote = this.runtimeStaleNote(choice.endpoint);
|
|
44393
46177
|
const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
|
|
44394
46178
|
const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
|
|
@@ -44437,7 +46221,7 @@ ${staleNote}`);
|
|
|
44437
46221
|
|
|
44438
46222
|
${staleNote}`}`
|
|
44439
46223
|
},
|
|
44440
|
-
{ type: "image", data:
|
|
46224
|
+
{ type: "image", data: readFileSync21(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
44441
46225
|
]
|
|
44442
46226
|
};
|
|
44443
46227
|
} finally {
|
|
@@ -44571,6 +46355,8 @@ var init_server3 = __esm(() => {
|
|
|
44571
46355
|
init_process_children();
|
|
44572
46356
|
init_process_time();
|
|
44573
46357
|
init_project_scan();
|
|
46358
|
+
init_rename();
|
|
46359
|
+
init_rename_symbol();
|
|
44574
46360
|
init_resources();
|
|
44575
46361
|
init_restart_note();
|
|
44576
46362
|
init_run_record();
|
|
@@ -44581,7 +46367,7 @@ var init_server3 = __esm(() => {
|
|
|
44581
46367
|
init_tool_definitions();
|
|
44582
46368
|
init_update_check();
|
|
44583
46369
|
run4 = promisify5(execFile5);
|
|
44584
|
-
__dirname2 =
|
|
46370
|
+
__dirname2 = dirname18(fileURLToPath5(import.meta.url));
|
|
44585
46371
|
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
44586
46372
|
LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
44587
46373
|
VALUE_NAMES = {
|
|
@@ -44605,6 +46391,7 @@ var init_server3 = __esm(() => {
|
|
|
44605
46391
|
"scriptPath",
|
|
44606
46392
|
"resourcePath",
|
|
44607
46393
|
"newPath",
|
|
46394
|
+
"newScriptPath",
|
|
44608
46395
|
"path",
|
|
44609
46396
|
"script"
|
|
44610
46397
|
];
|
|
@@ -44630,8 +46417,8 @@ var init_server3 = __esm(() => {
|
|
|
44630
46417
|
init_bridge_announce();
|
|
44631
46418
|
init_errors();
|
|
44632
46419
|
init_godot_path();
|
|
44633
|
-
import { existsSync as
|
|
44634
|
-
import { join as
|
|
46420
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
46421
|
+
import { join as join25, resolve as resolve8 } from "node:path";
|
|
44635
46422
|
|
|
44636
46423
|
// src/harnesses.ts
|
|
44637
46424
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -45800,7 +47587,13 @@ formatting. \`script_diagnostics\` and \`script_info\` come from the editor's ow
|
|
|
45800
47587
|
\`project_info\`, \`project_dependencies\`, \`project_settings\`, \`project_import\`,
|
|
45801
47588
|
\`project_export\`, \`project_test\`, \`script_edit\`, \`script_info structure\` and
|
|
45802
47589
|
\`editor_classes\` need nothing open: they run a short headless engine and are gone before the
|
|
45803
|
-
answer is printed.
|
|
47590
|
+
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.
|
|
47592
|
+
|
|
47593
|
+
To rename a class or a member across the project, use \`script_edit rename\` rather than a text
|
|
47594
|
+
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
|
|
45804
47597
|
a config file: it starts an engine so that a setting nobody wrote down still answers with the
|
|
45805
47598
|
default the engine registers for it.
|
|
45806
47599
|
|
|
@@ -45951,8 +47744,8 @@ class UsageError extends Refusal {
|
|
|
45951
47744
|
}
|
|
45952
47745
|
function projectArgument(at) {
|
|
45953
47746
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
45954
|
-
const projectPath =
|
|
45955
|
-
if (!
|
|
47747
|
+
const projectPath = resolve8(named ?? ".");
|
|
47748
|
+
if (!existsSync20(join25(projectPath, "project.godot"))) {
|
|
45956
47749
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
45957
47750
|
}
|
|
45958
47751
|
return projectPath;
|
|
@@ -45997,7 +47790,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
45997
47790
|
return;
|
|
45998
47791
|
}
|
|
45999
47792
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
46000
|
-
if (!
|
|
47793
|
+
if (!existsSync20(join25(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
46001
47794
|
console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
|
|
46002
47795
|
}
|
|
46003
47796
|
}
|
|
@@ -46129,7 +47922,7 @@ async function setup() {
|
|
|
46129
47922
|
console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
|
|
46130
47923
|
}
|
|
46131
47924
|
if (!args.includes("--no-skill")) {
|
|
46132
|
-
const directories = skillDirectories(chosen.harnesses, projectPath,
|
|
47925
|
+
const directories = skillDirectories(chosen.harnesses, projectPath, existsSync20);
|
|
46133
47926
|
for (const written of writeSkill(directories, getLocalVersion())) {
|
|
46134
47927
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
46135
47928
|
}
|
|
@@ -46243,7 +48036,7 @@ async function upgrade() {
|
|
|
46243
48036
|
moved.push(took.from);
|
|
46244
48037
|
}
|
|
46245
48038
|
}
|
|
46246
|
-
for (const written of writeSkill(skillDirectories(already, projectPath,
|
|
48039
|
+
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync20), version)) {
|
|
46247
48040
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
46248
48041
|
}
|
|
46249
48042
|
console.log(`
|