gdharness 1.1.52 → 1.1.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +178 -56
- package/build/index.js +180 -53
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -9131,7 +9131,7 @@ var init_gdscript_source = __esm(() => {
|
|
|
9131
9131
|
|
|
9132
9132
|
// src/class-cache.ts
|
|
9133
9133
|
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
|
|
9134
|
-
import { join as join9 } from "node:path";
|
|
9134
|
+
import { join as join9, posix } from "node:path";
|
|
9135
9135
|
function isToolScript(source) {
|
|
9136
9136
|
return TOOL.test(codeOf(source));
|
|
9137
9137
|
}
|
|
@@ -9237,6 +9237,27 @@ function missingMemberIn(message) {
|
|
|
9237
9237
|
const enumName = inBase[3];
|
|
9238
9238
|
return enumName === undefined ? { kind: "member", member, type } : { kind: "enum member", member, type, enum: enumName };
|
|
9239
9239
|
}
|
|
9240
|
+
function externalMemberIn(message, line, classes) {
|
|
9241
|
+
const member = /Could not resolve external class member "([A-Za-z_]\w*)"/.exec(message)?.[1];
|
|
9242
|
+
if (member === undefined || line === undefined) {
|
|
9243
|
+
return null;
|
|
9244
|
+
}
|
|
9245
|
+
for (const [, type] of line.matchAll(new RegExp(String.raw`\b([A-Za-z_]\w*)\s*\.\s*${member}\b`, "g"))) {
|
|
9246
|
+
if (type !== undefined && classes.has(type)) {
|
|
9247
|
+
return { kind: "member", member, type };
|
|
9248
|
+
}
|
|
9249
|
+
}
|
|
9250
|
+
return null;
|
|
9251
|
+
}
|
|
9252
|
+
function baseScriptOf(source, from, classes) {
|
|
9253
|
+
const found = /^(?:@\w+(?:\([^)]*\))?\s+)*(?:class_name\s+\w+\s+)?extends\s+(?:"([^"]+)"|'([^']+)'|([A-Za-z_]\w*))/m.exec(source);
|
|
9254
|
+
const quoted = found?.[1] ?? found?.[2];
|
|
9255
|
+
if (quoted !== undefined) {
|
|
9256
|
+
return quoted.startsWith("res://") ? quoted : posix.join(posix.dirname(from), quoted).replace(/^res:\/(?!\/)/, "res://");
|
|
9257
|
+
}
|
|
9258
|
+
const named = found?.[3];
|
|
9259
|
+
return named === undefined ? undefined : classes.get(named);
|
|
9260
|
+
}
|
|
9240
9261
|
function declaresMember(source, missing) {
|
|
9241
9262
|
const name = missing.member.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9242
9263
|
const top = String.raw`^(?:@\w+(?:\([^)]*\))?\s+)*`;
|
|
@@ -9296,24 +9317,39 @@ function classesNamedIn(source, known) {
|
|
|
9296
9317
|
}
|
|
9297
9318
|
return [...named].sort();
|
|
9298
9319
|
}
|
|
9299
|
-
function contradictedDiagnostics(messages, classes, sourceOf) {
|
|
9320
|
+
function contradictedDiagnostics(messages, classes, sourceOf, alsoDenied = []) {
|
|
9300
9321
|
const keyOf = (missing) => [missing.type, missing.enum ?? "", missing.member].join(".");
|
|
9301
|
-
const denials =
|
|
9322
|
+
const denials = [
|
|
9323
|
+
...messages.map((message) => missingMemberIn(message)).filter((missing) => missing !== null),
|
|
9324
|
+
...alsoDenied
|
|
9325
|
+
];
|
|
9302
9326
|
const calledOnTheClass = new Set(denials.filter((missing) => missing.kind === "static method").map((missing) => keyOf(missing)));
|
|
9303
9327
|
const read = new Map;
|
|
9328
|
+
const sourceAt = (path) => {
|
|
9329
|
+
if (!read.has(path)) {
|
|
9330
|
+
read.set(path, sourceOf(path));
|
|
9331
|
+
}
|
|
9332
|
+
return read.get(path) ?? null;
|
|
9333
|
+
};
|
|
9304
9334
|
const found = new Map;
|
|
9305
9335
|
for (const missing of denials) {
|
|
9306
|
-
const declaredIn = classes.get(missing.type);
|
|
9307
9336
|
const key = keyOf(missing);
|
|
9308
|
-
if (
|
|
9337
|
+
if (found.has(key) || missing.kind === "method" && calledOnTheClass.has(key)) {
|
|
9309
9338
|
continue;
|
|
9310
9339
|
}
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9340
|
+
const seen = new Set;
|
|
9341
|
+
let at = classes.get(missing.type);
|
|
9342
|
+
while (at !== undefined && !seen.has(at)) {
|
|
9343
|
+
seen.add(at);
|
|
9344
|
+
const source = sourceAt(at);
|
|
9345
|
+
if (source === null) {
|
|
9346
|
+
break;
|
|
9347
|
+
}
|
|
9348
|
+
if (declaresMember(source, missing)) {
|
|
9349
|
+
found.set(key, { ...missing, declaredIn: at });
|
|
9350
|
+
break;
|
|
9351
|
+
}
|
|
9352
|
+
at = baseScriptOf(source, at, classes);
|
|
9317
9353
|
}
|
|
9318
9354
|
}
|
|
9319
9355
|
return [...found.values()];
|
|
@@ -9326,7 +9362,7 @@ function staleAnalysisNote(contradicted, dependsOn) {
|
|
|
9326
9362
|
const onInstances = contradicted.filter((one) => one.kind === "method" || one.kind === "property");
|
|
9327
9363
|
const onTheClass = contradicted.filter((one) => one.kind === "member" || one.kind === "static method" || one.kind === "enum member");
|
|
9328
9364
|
return [
|
|
9329
|
-
`The editor is reporting against an older copy of ${types.size === 1 ? "a type" : "some types"} ` + "named under contradictedByTheFile. Each member listed is declared in the
|
|
9365
|
+
`The editor is reporting against an older copy of ${types.size === 1 ? "a type" : "some types"} ` + "named under contradictedByTheFile. Each member listed is declared in the script under its " + "declaredIn, the class itself or a project class it extends, so those diagnostics are wrong " + "however the code is written.",
|
|
9330
9366
|
instanceRemedy(onInstances, dependsOn, onTheClass.length > 0),
|
|
9331
9367
|
classRemedy(onTheClass)
|
|
9332
9368
|
].filter((part) => part !== "").join(" ");
|
|
@@ -9343,7 +9379,7 @@ function instanceRemedy(entries, dependsOn, besideOthers) {
|
|
|
9343
9379
|
}
|
|
9344
9380
|
const declaring = declaringScripts(entries);
|
|
9345
9381
|
const lever = dependsOn.length === 0 ? ", and the named types depend on no other global class, so that lever is not available here. " + "That is the common case rather than the awkward one: an edit near a type that depends on " + "something refreshes it before the fault is noticed, which is why a type with no " + "dependencies is where this turns up" : `, so changing ${[...dependsOn].sort().join(" or ")} and then rescanning is worth a try`;
|
|
9346
|
-
return `${besideOthers ? `For ${entries.map(written).join(", ")}, run` : "Run"} editor_rescan first: ` + "it costs about half a second, and it has cleared this in every reproduction measured since the " + "scan timing was fixed: both in one project, the scan returning in 243ms and 275ms, and one in " + "another where the same call also rebuilt the copy. " + "The fault behind it is that a script the editor has loaded keeps the copy it built, and nothing " + `rebuilds that copy when the file changes; ${declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]} recompiles that copy` : `reloadScript takes one script, so a call each for ${declaring.join(" and ")} recompiles those copies`} from the file into the same ` + "object and answers with
|
|
9382
|
+
return `${besideOthers ? `For ${entries.map(written).join(", ")}, run` : "Run"} editor_rescan first: ` + "it costs about half a second, and it has cleared this in every reproduction measured since the " + "scan timing was fixed: both in one project, the scan returning in 243ms and 275ms, and one in " + "another where the same call also rebuilt the copy. " + "The fault behind it is that a script the editor has loaded keeps the copy it built, and nothing " + `rebuilds that copy when the file changes; ${declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]} recompiles that copy` : `reloadScript takes one script, so a call each for ${declaring.join(" and ")} recompiles those copies`} from the file into the same ` + "object and answers with what that added under methodsAdded and constantsAdded. Read that and the " + "next diagnostics as two readings, because they are of two things: measured in one window on " + "one editor, the analyser resolved a call to a newly added method while the built copy of that " + "same script did not have it. So the reload is worth doing and is not what clears these; the " + "scan is. The other lever measured is that a held type is refreshed when something it " + `depends on changes rather than when it changes itself${lever}. editor_launch restart has always ` + "worked and costs a window. project_import refresh_classes does not, and answers added: [] while " + "this is happening.";
|
|
9347
9383
|
}
|
|
9348
9384
|
function classRemedy(entries) {
|
|
9349
9385
|
if (entries.length === 0) {
|
|
@@ -9367,6 +9403,24 @@ function notReloadedNote(broughtIn, notReloaded) {
|
|
|
9367
9403
|
}
|
|
9368
9404
|
return `The scan brought in ${broughtIn.join(", ")}, and ${all.join(", ")} ${all.length === 1 ? "names one of them and" : "name them and"} could not be reloaded, so the editor still holds what it compiled while it could not see the class and reports that to the language server. ${remedies.join(" ")}`;
|
|
9369
9405
|
}
|
|
9406
|
+
function reloadDifference(reloaded) {
|
|
9407
|
+
const between = (before, after) => {
|
|
9408
|
+
if (before === undefined || after === undefined) {
|
|
9409
|
+
return null;
|
|
9410
|
+
}
|
|
9411
|
+
const was = new Set(before);
|
|
9412
|
+
const is = new Set(after);
|
|
9413
|
+
return { added: after.filter((name) => !was.has(name)), removed: before.filter((name) => !is.has(name)) };
|
|
9414
|
+
};
|
|
9415
|
+
const methods = between(reloaded.heldBefore, reloaded.methods);
|
|
9416
|
+
const constants = between(reloaded.heldConstantsBefore, reloaded.constants);
|
|
9417
|
+
return {
|
|
9418
|
+
...reloaded.methods === undefined ? {} : { methodsHeld: reloaded.methods.length },
|
|
9419
|
+
...methods === null ? {} : { methodsAdded: methods.added, methodsRemoved: methods.removed },
|
|
9420
|
+
...reloaded.constants === undefined ? {} : { constantsHeld: reloaded.constants.length },
|
|
9421
|
+
...constants === null ? {} : { constantsAdded: constants.added, constantsRemoved: constants.removed }
|
|
9422
|
+
};
|
|
9423
|
+
}
|
|
9370
9424
|
function failedReloadNote(failed) {
|
|
9371
9425
|
const missing = (before, after) => before.filter((name) => !after.includes(name));
|
|
9372
9426
|
const lost = [
|
|
@@ -9382,7 +9436,7 @@ function failedReloadNote(failed) {
|
|
|
9382
9436
|
} else if (!failed.restored) {
|
|
9383
9437
|
copy = `${unusable} The editor compiled it again from the text it was built from, and that failed too${failed.restoredAs === "" ? "" : ` (${failed.restoredAs})`}, so it stays that way until the script compiles: reload it then, or restart the editor with editor_launch restart.`;
|
|
9384
9438
|
} else if (unchanged) {
|
|
9385
|
-
copy = "The copy the editor holds was compiled again from the text it was built from, which puts it back as it was, with the members
|
|
9439
|
+
copy = "The copy the editor holds was compiled again from the text it was built from, which puts it back as it was, with the members it held before, which listMembers lists.";
|
|
9386
9440
|
} else {
|
|
9387
9441
|
copy = `The copy the editor holds was compiled again from the text it was built from and changed even so${lost.length === 0 ? "" : `, and has lost ${lost.join(", ")}`}: anything diagnosed against it now is answered from what is left, and reloading it once it compiles rebuilds it.`;
|
|
9388
9442
|
}
|
|
@@ -17855,7 +17909,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17855
17909
|
},
|
|
17856
17910
|
{
|
|
17857
17911
|
name: "script_diagnostics",
|
|
17858
|
-
description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
17912
|
+
description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, or a project class it extends does, a method, property, constant, static function or enum member, comes back under contradictedByTheFile with the script declaring it, and so does one a diagnostic names alone as an external class member, read against the class it is looked up on in the line it is on; and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
17859
17913
|
parameters: {
|
|
17860
17914
|
projectPath: PROJECT_PATH,
|
|
17861
17915
|
scriptPath: {
|
|
@@ -18080,7 +18134,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
18080
18134
|
},
|
|
18081
18135
|
reloadScript: {
|
|
18082
18136
|
type: "string",
|
|
18083
|
-
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one.
|
|
18137
|
+
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. What the reload changed comes back under methodsAdded, methodsRemoved, constantsAdded and constantsRemoved, with each enum value as Kind.SHORT among the constants, since an enum member is held as a constant and never shows among the methods; methodsHeld and constantsHeld count what the copy holds afterwards, because a reload that compiled nothing and answered OK is the failure worth catching. listMembers adds the whole lists. A script that does not compile is not reloaded: reloadProblem gives Godot's error, says whether the copy the editor holds survived, and names the classes the file uses, whose held copies being behind is what failed one before. A @tool script is not reloaded either, since it runs inside the editor and reloading one from inside a call can take the editor down: reloadProblem says so, and editor_launch restart recompiles it. A path spelled in another case than the file is reloaded under the spelling on disk, which is the one the editor holds its copy under, and that spelling comes back under reloadedAs."
|
|
18138
|
+
},
|
|
18139
|
+
listMembers: {
|
|
18140
|
+
type: "boolean",
|
|
18141
|
+
description: "With reloadScript: also answer the whole member lists, heldBeforeReload and reloadedMethods for the methods, heldConstantsBeforeReload and reloadedConstants for the constants. Default false: a script holding hundreds of constants answers hundreds of lines twice to say what one reload added."
|
|
18084
18142
|
}
|
|
18085
18143
|
},
|
|
18086
18144
|
requires: ["projectPath"]
|
|
@@ -38117,6 +38175,21 @@ var init_wrapper = __esm(() => {
|
|
|
38117
38175
|
import { randomUUID } from "node:crypto";
|
|
38118
38176
|
import { EventEmitter } from "node:events";
|
|
38119
38177
|
import http from "node:http";
|
|
38178
|
+
function closeReason(text, keep) {
|
|
38179
|
+
if (Buffer.byteLength(text) <= MOST_CLOSE_REASON_BYTES) {
|
|
38180
|
+
return text;
|
|
38181
|
+
}
|
|
38182
|
+
const characters = Array.from(text);
|
|
38183
|
+
const marker = "...";
|
|
38184
|
+
while (Buffer.byteLength(characters.join("")) + marker.length > MOST_CLOSE_REASON_BYTES) {
|
|
38185
|
+
if (keep === "end") {
|
|
38186
|
+
characters.shift();
|
|
38187
|
+
} else {
|
|
38188
|
+
characters.pop();
|
|
38189
|
+
}
|
|
38190
|
+
}
|
|
38191
|
+
return keep === "end" ? `${marker}${characters.join("")}` : `${characters.join("")}${marker}`;
|
|
38192
|
+
}
|
|
38120
38193
|
function resolveDefaultBridgePort() {
|
|
38121
38194
|
return portFromEnv("GDHARNESS_BRIDGE_PORT", DEFAULT_PORT);
|
|
38122
38195
|
}
|
|
@@ -38143,7 +38216,7 @@ function getDefaultBridge(ownProject = null) {
|
|
|
38143
38216
|
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
|
|
38144
38217
|
return defaultBridge;
|
|
38145
38218
|
}
|
|
38146
|
-
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;
|
|
38219
|
+
var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, MOST_CLOSE_REASON_BYTES = 123, OTHER_PROJECT_CLOSE_CODE = 4001, ONE_SHOT_CLOSE_CODE = 4002, CONNECT_WINDOW_MS = 35000, GodotBridge, defaultBridge = null;
|
|
38147
38220
|
var init_godot_bridge = __esm(() => {
|
|
38148
38221
|
init_wrapper();
|
|
38149
38222
|
init_errors();
|
|
@@ -38293,7 +38366,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
38293
38366
|
const editor = identifying ? null : this.connectionInfo;
|
|
38294
38367
|
return {
|
|
38295
38368
|
host: this.host,
|
|
38296
|
-
port: this.
|
|
38369
|
+
port: this.boundPort,
|
|
38297
38370
|
connected: this.isConnected(),
|
|
38298
38371
|
projectPath: editor?.projectPath,
|
|
38299
38372
|
connectedAt: editor?.connectedAt,
|
|
@@ -38550,7 +38623,8 @@ var init_godot_bridge = __esm(() => {
|
|
|
38550
38623
|
this.socket = null;
|
|
38551
38624
|
this.connectionInfo = null;
|
|
38552
38625
|
this.stopKeepalive();
|
|
38553
|
-
|
|
38626
|
+
const fits = closeReason(said, "start") === said;
|
|
38627
|
+
stranger?.close(OTHER_PROJECT_CLOSE_CODE, fits ? said : closeReason(`this server serves ${this.ownProject ?? ""}`, "end"));
|
|
38554
38628
|
}
|
|
38555
38629
|
identify(pid) {
|
|
38556
38630
|
const known = this.oneShots.get(pid);
|
|
@@ -38589,7 +38663,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
38589
38663
|
this.socket = null;
|
|
38590
38664
|
this.connectionInfo = null;
|
|
38591
38665
|
this.stopKeepalive();
|
|
38592
|
-
run?.close(ONE_SHOT_CLOSE_CODE, said);
|
|
38666
|
+
run?.close(ONE_SHOT_CLOSE_CODE, closeReason(said, "start"));
|
|
38593
38667
|
}
|
|
38594
38668
|
handleDisconnect(disconnectedSocket, reason) {
|
|
38595
38669
|
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
@@ -39123,6 +39197,7 @@ import { existsSync as existsSync16, realpathSync as realpathSync4 } from "node:
|
|
|
39123
39197
|
import { readFile, realpath } from "node:fs/promises";
|
|
39124
39198
|
import { createConnection as createConnection3 } from "node:net";
|
|
39125
39199
|
import { dirname as dirname14, join as join20, resolve as resolve5 } from "node:path";
|
|
39200
|
+
import { setTimeout as delay3 } from "node:timers/promises";
|
|
39126
39201
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
39127
39202
|
function initializeTimeoutMs() {
|
|
39128
39203
|
const override = Number.parseInt(envValue("GDHARNESS_LSP_INITIALIZE_TIMEOUT_MS") ?? "", 10);
|
|
@@ -39161,17 +39236,38 @@ class GodotLSPClient {
|
|
|
39161
39236
|
documentVersions = new Map;
|
|
39162
39237
|
documentTurns = new Map;
|
|
39163
39238
|
timeouts;
|
|
39239
|
+
servedHere;
|
|
39164
39240
|
constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1", timeouts = {
|
|
39165
39241
|
requestMs: REQUEST_TIMEOUT_MS2,
|
|
39166
39242
|
referencesMs: REFERENCES_TIMEOUT_MS,
|
|
39167
|
-
initializeMs: initializeTimeoutMs()
|
|
39168
|
-
|
|
39243
|
+
initializeMs: initializeTimeoutMs(),
|
|
39244
|
+
listenMs: LANGUAGE_SERVER_START_MS
|
|
39245
|
+
}, servedHere = () => false) {
|
|
39169
39246
|
this.port = port;
|
|
39170
39247
|
this.host = host;
|
|
39171
39248
|
this.timeouts = timeouts;
|
|
39249
|
+
this.servedHere = servedHere;
|
|
39172
39250
|
this.pendingRequests = new Map;
|
|
39173
39251
|
}
|
|
39174
39252
|
async connect() {
|
|
39253
|
+
const until = Date.now() + this.timeouts.listenMs;
|
|
39254
|
+
for (;; ) {
|
|
39255
|
+
try {
|
|
39256
|
+
await this.connectOnce();
|
|
39257
|
+
return;
|
|
39258
|
+
} catch (error) {
|
|
39259
|
+
const refused = error instanceof Error && error.message.includes("ECONNREFUSED");
|
|
39260
|
+
if (!refused || !this.servedHere()) {
|
|
39261
|
+
throw error;
|
|
39262
|
+
}
|
|
39263
|
+
if (Date.now() >= until) {
|
|
39264
|
+
throw new LanguageServerNotListening(this.port, this.timeouts.listenMs);
|
|
39265
|
+
}
|
|
39266
|
+
await delay3(LANGUAGE_SERVER_RETRY_MS);
|
|
39267
|
+
}
|
|
39268
|
+
}
|
|
39269
|
+
}
|
|
39270
|
+
async connectOnce() {
|
|
39175
39271
|
if (this.connected && this.socket) {
|
|
39176
39272
|
return;
|
|
39177
39273
|
}
|
|
@@ -39804,11 +39900,12 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
39804
39900
|
...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
|
|
39805
39901
|
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {},
|
|
39806
39902
|
...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {},
|
|
39807
|
-
...error instanceof LSPTimeout && error.method === "initialize" ? { stillReading: true } : {}
|
|
39903
|
+
...error instanceof LSPTimeout && error.method === "initialize" ? { stillReading: true } : {},
|
|
39904
|
+
...error instanceof LanguageServerNotListening ? { stillReading: true } : {}
|
|
39808
39905
|
});
|
|
39809
39906
|
}
|
|
39810
39907
|
}
|
|
39811
|
-
var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, REQUEST_TIMEOUT_MS2 = 1e4, REFERENCES_TIMEOUT_MS = 180000, LSPTimeout, ArgumentRefusal, NoDiagnosticsPublished, AnotherProjectsServer, SEVERITIES, SYMBOL_KINDS, COMPLETION_KINDS;
|
|
39908
|
+
var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, REQUEST_TIMEOUT_MS2 = 1e4, REFERENCES_TIMEOUT_MS = 180000, LSPTimeout, LANGUAGE_SERVER_START_MS = 30000, LANGUAGE_SERVER_RETRY_MS = 250, LanguageServerNotListening, ArgumentRefusal, NoDiagnosticsPublished, AnotherProjectsServer, SEVERITIES, SYMBOL_KINDS, COMPLETION_KINDS;
|
|
39812
39909
|
var init_lsp_client = __esm(() => {
|
|
39813
39910
|
init_answer_json();
|
|
39814
39911
|
init_errors();
|
|
@@ -39824,6 +39921,11 @@ var init_lsp_client = __esm(() => {
|
|
|
39824
39921
|
this.seconds = timeoutMs / 1000;
|
|
39825
39922
|
}
|
|
39826
39923
|
};
|
|
39924
|
+
LanguageServerNotListening = class LanguageServerNotListening extends Error {
|
|
39925
|
+
constructor(port, waitedMs) {
|
|
39926
|
+
super(`The editor on the bridge says its language server is on port ${port}, and nothing accepted a connection there in ${String(waitedMs / 1000)}s. An editor starts it a few seconds after it reaches the bridge, so straight after editor_launch this is the editor still starting, and calling again answers. One that goes on refusing could not take the port; editor_output shows what the editor printed.`);
|
|
39927
|
+
}
|
|
39928
|
+
};
|
|
39827
39929
|
ArgumentRefusal = class ArgumentRefusal extends Refusal {
|
|
39828
39930
|
};
|
|
39829
39931
|
NoDiagnosticsPublished = class NoDiagnosticsPublished extends Error {
|
|
@@ -40233,7 +40335,7 @@ var init_project_scan = __esm(() => {
|
|
|
40233
40335
|
|
|
40234
40336
|
// src/rename.ts
|
|
40235
40337
|
import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync19, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync13 } from "node:fs";
|
|
40236
|
-
import { dirname as dirname15, posix } from "node:path";
|
|
40338
|
+
import { dirname as dirname15, posix as posix2 } from "node:path";
|
|
40237
40339
|
|
|
40238
40340
|
class Script {
|
|
40239
40341
|
path;
|
|
@@ -40346,13 +40448,13 @@ class ClassGraph {
|
|
|
40346
40448
|
}
|
|
40347
40449
|
function resolvedPath(written, from) {
|
|
40348
40450
|
if (written.startsWith("res://")) {
|
|
40349
|
-
return `res://${
|
|
40451
|
+
return `res://${posix2.normalize(written.slice("res://".length))}`;
|
|
40350
40452
|
}
|
|
40351
|
-
const directory =
|
|
40352
|
-
return `res://${
|
|
40453
|
+
const directory = posix2.dirname(from.slice("res://".length));
|
|
40454
|
+
return `res://${posix2.normalize(posix2.join(directory, written))}`;
|
|
40353
40455
|
}
|
|
40354
40456
|
function relativeTo(target, from) {
|
|
40355
|
-
return
|
|
40457
|
+
return posix2.relative(posix2.dirname(from.slice("res://".length)), target.slice("res://".length));
|
|
40356
40458
|
}
|
|
40357
40459
|
function scriptsOf(texts) {
|
|
40358
40460
|
const scripts = new Map;
|
|
@@ -42014,7 +42116,7 @@ import {
|
|
|
42014
42116
|
statSync as statSync9
|
|
42015
42117
|
} from "node:fs";
|
|
42016
42118
|
import { basename as basename7, dirname as dirname19, extname as extname2, isAbsolute as isAbsolute3, join as join26, normalize as normalize2, relative as relative5 } from "node:path";
|
|
42017
|
-
import { setTimeout as
|
|
42119
|
+
import { setTimeout as delay4 } from "node:timers/promises";
|
|
42018
42120
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
42019
42121
|
import { promisify as promisify5 } from "node:util";
|
|
42020
42122
|
function editorRestartTimeoutMs() {
|
|
@@ -42147,7 +42249,7 @@ async function untilGone(pids, withinMs = STOP_WAIT_MS) {
|
|
|
42147
42249
|
const gone = () => pids.every((pid) => !alive(pid));
|
|
42148
42250
|
const deadline = Date.now() + withinMs;
|
|
42149
42251
|
while (!gone() && Date.now() < deadline) {
|
|
42150
|
-
await
|
|
42252
|
+
await delay4(50);
|
|
42151
42253
|
}
|
|
42152
42254
|
return gone();
|
|
42153
42255
|
}
|
|
@@ -42904,8 +43006,12 @@ class GodotServer {
|
|
|
42904
43006
|
console.error("gdharness running on stdio");
|
|
42905
43007
|
sweepAbandonedScratch();
|
|
42906
43008
|
try {
|
|
42907
|
-
|
|
42908
|
-
|
|
43009
|
+
try {
|
|
43010
|
+
await this.takeOverFromAPredecessor();
|
|
43011
|
+
await this.godotBridge.start();
|
|
43012
|
+
} finally {
|
|
43013
|
+
this.handingOverFrom = null;
|
|
43014
|
+
}
|
|
42909
43015
|
this.bridgeStartupError = null;
|
|
42910
43016
|
const bridgeStatus = this.godotBridge.getStatus();
|
|
42911
43017
|
console.error(`[SERVER] Godot Editor Bridge started on ${bridgeStatus.host}:${bridgeStatus.port}`);
|
|
@@ -42942,10 +43048,9 @@ class GodotServer {
|
|
|
42942
43048
|
taken = true;
|
|
42943
43049
|
this.logDebug(`Took port ${wanted} over from pid ${announced.pid}`);
|
|
42944
43050
|
} catch {
|
|
42945
|
-
await
|
|
43051
|
+
await delay4(HANDOVER_POLL_MS);
|
|
42946
43052
|
}
|
|
42947
43053
|
}
|
|
42948
|
-
this.handingOverFrom = null;
|
|
42949
43054
|
this.notHandedOverBy = taken ? null : announced.pid;
|
|
42950
43055
|
}
|
|
42951
43056
|
keepTryingTheBridge() {
|
|
@@ -42977,10 +43082,9 @@ class GodotServer {
|
|
|
42977
43082
|
if (this.ownProject === null) {
|
|
42978
43083
|
return;
|
|
42979
43084
|
}
|
|
42980
|
-
const status = this.godotBridge.getStatus();
|
|
42981
43085
|
this.announcedAt = announceBridge(this.ownProject, {
|
|
42982
|
-
host:
|
|
42983
|
-
port:
|
|
43086
|
+
host: this.godotBridge.getStatus().host,
|
|
43087
|
+
port: this.godotBridge.port,
|
|
42984
43088
|
version: SERVER_VERSION
|
|
42985
43089
|
});
|
|
42986
43090
|
this.watchForASuccessor();
|
|
@@ -44715,14 +44819,14 @@ class GodotServer {
|
|
|
44715
44819
|
}
|
|
44716
44820
|
whereTheFileDisagrees(diagnostics, args) {
|
|
44717
44821
|
const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
|
|
44718
|
-
const worthChecking = messages.some((message) => missingMemberIn(message) !== null || unknownTypeIn(message) !== null);
|
|
44822
|
+
const worthChecking = messages.some((message) => missingMemberIn(message) !== null || unknownTypeIn(message) !== null || message.includes(EXTERNAL_MEMBER));
|
|
44719
44823
|
const project = worthChecking ? this.project(args) : null;
|
|
44720
44824
|
if (project === null || !project.ok) {
|
|
44721
44825
|
return {};
|
|
44722
44826
|
}
|
|
44723
44827
|
const projectPath = project.value.path;
|
|
44724
44828
|
const cached = cachedClasses(projectPath);
|
|
44725
|
-
const
|
|
44829
|
+
const sourceOf = (resourcePath) => {
|
|
44726
44830
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
44727
44831
|
if (!contained.ok || !existsSync20(contained.absolutePath)) {
|
|
44728
44832
|
return null;
|
|
@@ -44732,7 +44836,16 @@ class GodotServer {
|
|
|
44732
44836
|
} catch {
|
|
44733
44837
|
return null;
|
|
44734
44838
|
}
|
|
44735
|
-
}
|
|
44839
|
+
};
|
|
44840
|
+
const external = diagnostics.filter((entry) => String(asParams(entry)["message"]).includes(EXTERNAL_MEMBER));
|
|
44841
|
+
const diagnosed = external.length === 0 ? null : sourceOf(readString(args, "scriptPath") ?? "");
|
|
44842
|
+
const lines = diagnosed?.split(`
|
|
44843
|
+
`) ?? [];
|
|
44844
|
+
const alsoDenied = cached === null ? [] : external.map((entry) => {
|
|
44845
|
+
const { message, line } = asParams(entry);
|
|
44846
|
+
return externalMemberIn(String(message), typeof line === "number" ? lines[line - 1] : undefined, cached);
|
|
44847
|
+
}).filter((missing) => missing !== null);
|
|
44848
|
+
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, sourceOf, alsoDenied);
|
|
44736
44849
|
const unloaded = messages.some((message) => unknownTypeIn(message) !== null) ? unloadedTypes(messages, declaredClasses(projectPath), cached) : [];
|
|
44737
44850
|
const notes = [];
|
|
44738
44851
|
if (contradicted.length > 0) {
|
|
@@ -44802,7 +44915,10 @@ class GodotServer {
|
|
|
44802
44915
|
await this.lspClient.disconnect();
|
|
44803
44916
|
this.lspClient = null;
|
|
44804
44917
|
}
|
|
44805
|
-
this.lspClient ??= new GodotLSPClient(port)
|
|
44918
|
+
this.lspClient ??= new GodotLSPClient(port, undefined, undefined, () => {
|
|
44919
|
+
const status = this.godotBridge.getStatus();
|
|
44920
|
+
return status.connected && status.lspPort === port;
|
|
44921
|
+
});
|
|
44806
44922
|
return this.lspClient;
|
|
44807
44923
|
}
|
|
44808
44924
|
async handleDAP(toolName, args) {
|
|
@@ -44923,19 +45039,22 @@ class GodotServer {
|
|
|
44923
45039
|
return { note: owed, underway: restartUnderway(owed) };
|
|
44924
45040
|
}
|
|
44925
45041
|
async getEditorStatusPayload() {
|
|
45042
|
+
const wasConnected = this.godotBridge.getStatus().connected;
|
|
45043
|
+
const launchedWasUp = !wasConnected && await this.launchedEditorIsUp();
|
|
45044
|
+
const arrivingFound = wasConnected || this.ownProject === null ? [] : await this.editorsNotYetConnected(this.ownProject) ?? [];
|
|
44926
45045
|
const status = this.godotBridge.getStatus();
|
|
44927
|
-
const launchedIsUp = !status.connected &&
|
|
45046
|
+
const launchedIsUp = !status.connected && launchedWasUp;
|
|
45047
|
+
const arriving = status.connected ? [] : arrivingFound;
|
|
44928
45048
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
44929
45049
|
const greeted = hasSaidWhoItIs(status);
|
|
44930
45050
|
const stale = greeted && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
|
|
44931
45051
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
44932
|
-
const arriving = status.connected || this.ownProject === null ? [] : await this.editorsNotYetConnected(this.ownProject) ?? [];
|
|
44933
45052
|
return {
|
|
44934
45053
|
...status,
|
|
44935
45054
|
serverVersion: SERVER_VERSION,
|
|
44936
45055
|
addonIsStale: greeted ? stale : undefined,
|
|
44937
45056
|
greeting: status.connected && !greeted ? "The editor has connected and has not yet said who it is: its project, pid, addon version and ports arrive with its greeting a moment after the socket opens, later while it imports. Ask again for them." : undefined,
|
|
44938
|
-
bridgeAvailable: this.bridgeStartupError === null,
|
|
45057
|
+
bridgeAvailable: this.bridgeStartupError === null && status.port !== null,
|
|
44939
45058
|
mayYetConnect: status.connected ? undefined : arriving.length > 0 ? true : unfinished !== null ? unfinished.underway : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
44940
45059
|
editorsNotYetConnected: arriving.length > 0 ? arriving : undefined,
|
|
44941
45060
|
restartInterrupted: unfinished === null || unfinished.underway || arriving.length > 0 ? undefined : {
|
|
@@ -44979,7 +45098,7 @@ class GodotServer {
|
|
|
44979
45098
|
try {
|
|
44980
45099
|
const askedAt = Date.now();
|
|
44981
45100
|
const asked = this.godotBridge.invokeTool("playing_status", {});
|
|
44982
|
-
const answer = await Promise.race([asked,
|
|
45101
|
+
const answer = await Promise.race([asked, delay4(PLAYING_STATUS_MS).then(() => null)]);
|
|
44983
45102
|
if (answer === null) {
|
|
44984
45103
|
this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
|
|
44985
45104
|
asked.then(() => {
|
|
@@ -45004,7 +45123,7 @@ class GodotServer {
|
|
|
45004
45123
|
try {
|
|
45005
45124
|
const answer = await Promise.race([
|
|
45006
45125
|
this.godotBridge.invokeTool("scan_status", {}),
|
|
45007
|
-
|
|
45126
|
+
delay4(PLAYING_STATUS_MS).then(() => null)
|
|
45008
45127
|
]);
|
|
45009
45128
|
if (answer === null) {
|
|
45010
45129
|
return null;
|
|
@@ -45216,7 +45335,7 @@ class GodotServer {
|
|
|
45216
45335
|
if (Date.now() >= deadline) {
|
|
45217
45336
|
return false;
|
|
45218
45337
|
}
|
|
45219
|
-
await
|
|
45338
|
+
await delay4(250);
|
|
45220
45339
|
}
|
|
45221
45340
|
}
|
|
45222
45341
|
async handleLaunchEditor(args) {
|
|
@@ -45726,7 +45845,7 @@ class GodotServer {
|
|
|
45726
45845
|
if (Date.now() >= deadline) {
|
|
45727
45846
|
return false;
|
|
45728
45847
|
}
|
|
45729
|
-
await
|
|
45848
|
+
await delay4(100);
|
|
45730
45849
|
}
|
|
45731
45850
|
}
|
|
45732
45851
|
async endActiveGame(reason, going) {
|
|
@@ -46318,7 +46437,7 @@ class GodotServer {
|
|
|
46318
46437
|
while (cancelled?.aborted !== true && await this.runStillGoing(run) && Date.now() < until) {
|
|
46319
46438
|
this.drainEditorOutput(run);
|
|
46320
46439
|
this.drainTranscript(run);
|
|
46321
|
-
await
|
|
46440
|
+
await delay4(RUN_POLL_MS);
|
|
46322
46441
|
}
|
|
46323
46442
|
return await this.handleGetDebugOutput(args, await this.runStillGoing(run) ? budgetMs : undefined);
|
|
46324
46443
|
}
|
|
@@ -46355,7 +46474,7 @@ class GodotServer {
|
|
|
46355
46474
|
if (Date.now() >= until) {
|
|
46356
46475
|
return false;
|
|
46357
46476
|
}
|
|
46358
|
-
await
|
|
46477
|
+
await delay4(50);
|
|
46359
46478
|
}
|
|
46360
46479
|
}
|
|
46361
46480
|
async handleEditorConsole(args) {
|
|
@@ -46865,10 +46984,13 @@ class GodotServer {
|
|
|
46865
46984
|
waitedMs: Date.now() - started,
|
|
46866
46985
|
waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
|
|
46867
46986
|
reloadedAs: reloaded.as,
|
|
46868
|
-
|
|
46869
|
-
|
|
46870
|
-
|
|
46871
|
-
|
|
46987
|
+
...reloadDifference(reloaded),
|
|
46988
|
+
...args["listMembers"] === true ? {
|
|
46989
|
+
heldBeforeReload: reloaded.heldBefore,
|
|
46990
|
+
reloadedMethods: reloaded.methods,
|
|
46991
|
+
heldConstantsBeforeReload: reloaded.heldConstantsBefore,
|
|
46992
|
+
reloadedConstants: reloaded.constants
|
|
46993
|
+
} : {},
|
|
46872
46994
|
reloadProblem: reloaded.problem,
|
|
46873
46995
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
46874
46996
|
broughtIn: broughtIn.length > 0 ? broughtIn : undefined,
|
|
@@ -47544,7 +47666,7 @@ async function runGodotServer() {
|
|
|
47544
47666
|
const server = new GodotServer;
|
|
47545
47667
|
await server.run();
|
|
47546
47668
|
}
|
|
47547
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
47669
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", EXTERNAL_MEMBER = 'Could not resolve external class member "';
|
|
47548
47670
|
var init_server3 = __esm(() => {
|
|
47549
47671
|
init_mcp();
|
|
47550
47672
|
init_stdio2();
|
package/build/index.js
CHANGED
|
@@ -10261,7 +10261,7 @@ import {
|
|
|
10261
10261
|
statSync as statSync8
|
|
10262
10262
|
} from "node:fs";
|
|
10263
10263
|
import { basename as basename5, dirname as dirname15, extname as extname2, isAbsolute as isAbsolute3, join as join22, normalize as normalize2, relative as relative4 } from "node:path";
|
|
10264
|
-
import { setTimeout as
|
|
10264
|
+
import { setTimeout as delay4 } from "node:timers/promises";
|
|
10265
10265
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
10266
10266
|
import { promisify as promisify5 } from "node:util";
|
|
10267
10267
|
|
|
@@ -26191,7 +26191,7 @@ function callProgress() {
|
|
|
26191
26191
|
|
|
26192
26192
|
// src/class-cache.ts
|
|
26193
26193
|
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
|
|
26194
|
-
import { join as join6 } from "node:path";
|
|
26194
|
+
import { join as join6, posix } from "node:path";
|
|
26195
26195
|
|
|
26196
26196
|
// src/gdscript-source.ts
|
|
26197
26197
|
var IDENTIFIER_START = /[\p{L}_]/u;
|
|
@@ -26692,6 +26692,27 @@ function missingMemberIn(message) {
|
|
|
26692
26692
|
const enumName = inBase[3];
|
|
26693
26693
|
return enumName === undefined ? { kind: "member", member, type } : { kind: "enum member", member, type, enum: enumName };
|
|
26694
26694
|
}
|
|
26695
|
+
function externalMemberIn(message, line, classes) {
|
|
26696
|
+
const member = /Could not resolve external class member "([A-Za-z_]\w*)"/.exec(message)?.[1];
|
|
26697
|
+
if (member === undefined || line === undefined) {
|
|
26698
|
+
return null;
|
|
26699
|
+
}
|
|
26700
|
+
for (const [, type] of line.matchAll(new RegExp(String.raw`\b([A-Za-z_]\w*)\s*\.\s*${member}\b`, "g"))) {
|
|
26701
|
+
if (type !== undefined && classes.has(type)) {
|
|
26702
|
+
return { kind: "member", member, type };
|
|
26703
|
+
}
|
|
26704
|
+
}
|
|
26705
|
+
return null;
|
|
26706
|
+
}
|
|
26707
|
+
function baseScriptOf(source, from, classes) {
|
|
26708
|
+
const found = /^(?:@\w+(?:\([^)]*\))?\s+)*(?:class_name\s+\w+\s+)?extends\s+(?:"([^"]+)"|'([^']+)'|([A-Za-z_]\w*))/m.exec(source);
|
|
26709
|
+
const quoted = found?.[1] ?? found?.[2];
|
|
26710
|
+
if (quoted !== undefined) {
|
|
26711
|
+
return quoted.startsWith("res://") ? quoted : posix.join(posix.dirname(from), quoted).replace(/^res:\/(?!\/)/, "res://");
|
|
26712
|
+
}
|
|
26713
|
+
const named = found?.[3];
|
|
26714
|
+
return named === undefined ? undefined : classes.get(named);
|
|
26715
|
+
}
|
|
26695
26716
|
function declaresMember(source, missing) {
|
|
26696
26717
|
const name = missing.member.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
26697
26718
|
const top = String.raw`^(?:@\w+(?:\([^)]*\))?\s+)*`;
|
|
@@ -26751,24 +26772,39 @@ function classesNamedIn(source, known) {
|
|
|
26751
26772
|
}
|
|
26752
26773
|
return [...named].sort();
|
|
26753
26774
|
}
|
|
26754
|
-
function contradictedDiagnostics(messages, classes, sourceOf) {
|
|
26775
|
+
function contradictedDiagnostics(messages, classes, sourceOf, alsoDenied = []) {
|
|
26755
26776
|
const keyOf = (missing) => [missing.type, missing.enum ?? "", missing.member].join(".");
|
|
26756
|
-
const denials =
|
|
26777
|
+
const denials = [
|
|
26778
|
+
...messages.map((message) => missingMemberIn(message)).filter((missing) => missing !== null),
|
|
26779
|
+
...alsoDenied
|
|
26780
|
+
];
|
|
26757
26781
|
const calledOnTheClass = new Set(denials.filter((missing) => missing.kind === "static method").map((missing) => keyOf(missing)));
|
|
26758
26782
|
const read = new Map;
|
|
26783
|
+
const sourceAt = (path) => {
|
|
26784
|
+
if (!read.has(path)) {
|
|
26785
|
+
read.set(path, sourceOf(path));
|
|
26786
|
+
}
|
|
26787
|
+
return read.get(path) ?? null;
|
|
26788
|
+
};
|
|
26759
26789
|
const found = new Map;
|
|
26760
26790
|
for (const missing of denials) {
|
|
26761
|
-
const declaredIn = classes.get(missing.type);
|
|
26762
26791
|
const key = keyOf(missing);
|
|
26763
|
-
if (
|
|
26792
|
+
if (found.has(key) || missing.kind === "method" && calledOnTheClass.has(key)) {
|
|
26764
26793
|
continue;
|
|
26765
26794
|
}
|
|
26766
|
-
|
|
26767
|
-
|
|
26768
|
-
|
|
26769
|
-
|
|
26770
|
-
|
|
26771
|
-
|
|
26795
|
+
const seen = new Set;
|
|
26796
|
+
let at = classes.get(missing.type);
|
|
26797
|
+
while (at !== undefined && !seen.has(at)) {
|
|
26798
|
+
seen.add(at);
|
|
26799
|
+
const source = sourceAt(at);
|
|
26800
|
+
if (source === null) {
|
|
26801
|
+
break;
|
|
26802
|
+
}
|
|
26803
|
+
if (declaresMember(source, missing)) {
|
|
26804
|
+
found.set(key, { ...missing, declaredIn: at });
|
|
26805
|
+
break;
|
|
26806
|
+
}
|
|
26807
|
+
at = baseScriptOf(source, at, classes);
|
|
26772
26808
|
}
|
|
26773
26809
|
}
|
|
26774
26810
|
return [...found.values()];
|
|
@@ -26781,7 +26817,7 @@ function staleAnalysisNote(contradicted, dependsOn) {
|
|
|
26781
26817
|
const onInstances = contradicted.filter((one) => one.kind === "method" || one.kind === "property");
|
|
26782
26818
|
const onTheClass = contradicted.filter((one) => one.kind === "member" || one.kind === "static method" || one.kind === "enum member");
|
|
26783
26819
|
return [
|
|
26784
|
-
`The editor is reporting against an older copy of ${types.size === 1 ? "a type" : "some types"} ` + "named under contradictedByTheFile. Each member listed is declared in the
|
|
26820
|
+
`The editor is reporting against an older copy of ${types.size === 1 ? "a type" : "some types"} ` + "named under contradictedByTheFile. Each member listed is declared in the script under its " + "declaredIn, the class itself or a project class it extends, so those diagnostics are wrong " + "however the code is written.",
|
|
26785
26821
|
instanceRemedy(onInstances, dependsOn, onTheClass.length > 0),
|
|
26786
26822
|
classRemedy(onTheClass)
|
|
26787
26823
|
].filter((part) => part !== "").join(" ");
|
|
@@ -26798,7 +26834,7 @@ function instanceRemedy(entries, dependsOn, besideOthers) {
|
|
|
26798
26834
|
}
|
|
26799
26835
|
const declaring = declaringScripts(entries);
|
|
26800
26836
|
const lever = dependsOn.length === 0 ? ", and the named types depend on no other global class, so that lever is not available here. " + "That is the common case rather than the awkward one: an edit near a type that depends on " + "something refreshes it before the fault is noticed, which is why a type with no " + "dependencies is where this turns up" : `, so changing ${[...dependsOn].sort().join(" or ")} and then rescanning is worth a try`;
|
|
26801
|
-
return `${besideOthers ? `For ${entries.map(written).join(", ")}, run` : "Run"} editor_rescan first: ` + "it costs about half a second, and it has cleared this in every reproduction measured since the " + "scan timing was fixed: both in one project, the scan returning in 243ms and 275ms, and one in " + "another where the same call also rebuilt the copy. " + "The fault behind it is that a script the editor has loaded keeps the copy it built, and nothing " + `rebuilds that copy when the file changes; ${declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]} recompiles that copy` : `reloadScript takes one script, so a call each for ${declaring.join(" and ")} recompiles those copies`} from the file into the same ` + "object and answers with
|
|
26837
|
+
return `${besideOthers ? `For ${entries.map(written).join(", ")}, run` : "Run"} editor_rescan first: ` + "it costs about half a second, and it has cleared this in every reproduction measured since the " + "scan timing was fixed: both in one project, the scan returning in 243ms and 275ms, and one in " + "another where the same call also rebuilt the copy. " + "The fault behind it is that a script the editor has loaded keeps the copy it built, and nothing " + `rebuilds that copy when the file changes; ${declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]} recompiles that copy` : `reloadScript takes one script, so a call each for ${declaring.join(" and ")} recompiles those copies`} from the file into the same ` + "object and answers with what that added under methodsAdded and constantsAdded. Read that and the " + "next diagnostics as two readings, because they are of two things: measured in one window on " + "one editor, the analyser resolved a call to a newly added method while the built copy of that " + "same script did not have it. So the reload is worth doing and is not what clears these; the " + "scan is. The other lever measured is that a held type is refreshed when something it " + `depends on changes rather than when it changes itself${lever}. editor_launch restart has always ` + "worked and costs a window. project_import refresh_classes does not, and answers added: [] while " + "this is happening.";
|
|
26802
26838
|
}
|
|
26803
26839
|
function classRemedy(entries) {
|
|
26804
26840
|
if (entries.length === 0) {
|
|
@@ -26822,6 +26858,24 @@ function notReloadedNote(broughtIn, notReloaded) {
|
|
|
26822
26858
|
}
|
|
26823
26859
|
return `The scan brought in ${broughtIn.join(", ")}, and ${all.join(", ")} ${all.length === 1 ? "names one of them and" : "name them and"} could not be reloaded, so the editor still holds what it compiled while it could not see the class and reports that to the language server. ${remedies.join(" ")}`;
|
|
26824
26860
|
}
|
|
26861
|
+
function reloadDifference(reloaded) {
|
|
26862
|
+
const between = (before, after) => {
|
|
26863
|
+
if (before === undefined || after === undefined) {
|
|
26864
|
+
return null;
|
|
26865
|
+
}
|
|
26866
|
+
const was = new Set(before);
|
|
26867
|
+
const is = new Set(after);
|
|
26868
|
+
return { added: after.filter((name) => !was.has(name)), removed: before.filter((name) => !is.has(name)) };
|
|
26869
|
+
};
|
|
26870
|
+
const methods = between(reloaded.heldBefore, reloaded.methods);
|
|
26871
|
+
const constants = between(reloaded.heldConstantsBefore, reloaded.constants);
|
|
26872
|
+
return {
|
|
26873
|
+
...reloaded.methods === undefined ? {} : { methodsHeld: reloaded.methods.length },
|
|
26874
|
+
...methods === null ? {} : { methodsAdded: methods.added, methodsRemoved: methods.removed },
|
|
26875
|
+
...reloaded.constants === undefined ? {} : { constantsHeld: reloaded.constants.length },
|
|
26876
|
+
...constants === null ? {} : { constantsAdded: constants.added, constantsRemoved: constants.removed }
|
|
26877
|
+
};
|
|
26878
|
+
}
|
|
26825
26879
|
function failedReloadNote(failed) {
|
|
26826
26880
|
const missing = (before, after) => before.filter((name) => !after.includes(name));
|
|
26827
26881
|
const lost = [
|
|
@@ -26837,7 +26891,7 @@ function failedReloadNote(failed) {
|
|
|
26837
26891
|
} else if (!failed.restored) {
|
|
26838
26892
|
copy = `${unusable} The editor compiled it again from the text it was built from, and that failed too${failed.restoredAs === "" ? "" : ` (${failed.restoredAs})`}, so it stays that way until the script compiles: reload it then, or restart the editor with editor_launch restart.`;
|
|
26839
26893
|
} else if (unchanged) {
|
|
26840
|
-
copy = "The copy the editor holds was compiled again from the text it was built from, which puts it back as it was, with the members
|
|
26894
|
+
copy = "The copy the editor holds was compiled again from the text it was built from, which puts it back as it was, with the members it held before, which listMembers lists.";
|
|
26841
26895
|
} else {
|
|
26842
26896
|
copy = `The copy the editor holds was compiled again from the text it was built from and changed even so${lost.length === 0 ? "" : `, and has lost ${lost.join(", ")}`}: anything diagnosed against it now is answered from what is left, and reloading it once it compiles rebuilds it.`;
|
|
26843
26897
|
}
|
|
@@ -28181,6 +28235,22 @@ var DEFAULT_HOST = "127.0.0.1";
|
|
|
28181
28235
|
var DEFAULT_TIMEOUT_MS = 30000;
|
|
28182
28236
|
var KEEPALIVE_INTERVAL_MS = 1e4;
|
|
28183
28237
|
var SECOND_CONNECTION_CLOSE_CODE = 4000;
|
|
28238
|
+
var MOST_CLOSE_REASON_BYTES = 123;
|
|
28239
|
+
function closeReason(text, keep) {
|
|
28240
|
+
if (Buffer.byteLength(text) <= MOST_CLOSE_REASON_BYTES) {
|
|
28241
|
+
return text;
|
|
28242
|
+
}
|
|
28243
|
+
const characters = Array.from(text);
|
|
28244
|
+
const marker = "...";
|
|
28245
|
+
while (Buffer.byteLength(characters.join("")) + marker.length > MOST_CLOSE_REASON_BYTES) {
|
|
28246
|
+
if (keep === "end") {
|
|
28247
|
+
characters.shift();
|
|
28248
|
+
} else {
|
|
28249
|
+
characters.pop();
|
|
28250
|
+
}
|
|
28251
|
+
}
|
|
28252
|
+
return keep === "end" ? `${marker}${characters.join("")}` : `${characters.join("")}${marker}`;
|
|
28253
|
+
}
|
|
28184
28254
|
var OTHER_PROJECT_CLOSE_CODE = 4001;
|
|
28185
28255
|
var ONE_SHOT_CLOSE_CODE = 4002;
|
|
28186
28256
|
function resolveDefaultBridgePort() {
|
|
@@ -28351,7 +28421,7 @@ class GodotBridge extends EventEmitter {
|
|
|
28351
28421
|
const editor = identifying ? null : this.connectionInfo;
|
|
28352
28422
|
return {
|
|
28353
28423
|
host: this.host,
|
|
28354
|
-
port: this.
|
|
28424
|
+
port: this.boundPort,
|
|
28355
28425
|
connected: this.isConnected(),
|
|
28356
28426
|
projectPath: editor?.projectPath,
|
|
28357
28427
|
connectedAt: editor?.connectedAt,
|
|
@@ -28608,7 +28678,8 @@ class GodotBridge extends EventEmitter {
|
|
|
28608
28678
|
this.socket = null;
|
|
28609
28679
|
this.connectionInfo = null;
|
|
28610
28680
|
this.stopKeepalive();
|
|
28611
|
-
|
|
28681
|
+
const fits = closeReason(said, "start") === said;
|
|
28682
|
+
stranger?.close(OTHER_PROJECT_CLOSE_CODE, fits ? said : closeReason(`this server serves ${this.ownProject ?? ""}`, "end"));
|
|
28612
28683
|
}
|
|
28613
28684
|
identify(pid) {
|
|
28614
28685
|
const known = this.oneShots.get(pid);
|
|
@@ -28647,7 +28718,7 @@ class GodotBridge extends EventEmitter {
|
|
|
28647
28718
|
this.socket = null;
|
|
28648
28719
|
this.connectionInfo = null;
|
|
28649
28720
|
this.stopKeepalive();
|
|
28650
|
-
run?.close(ONE_SHOT_CLOSE_CODE, said);
|
|
28721
|
+
run?.close(ONE_SHOT_CLOSE_CODE, closeReason(said, "start"));
|
|
28651
28722
|
}
|
|
28652
28723
|
handleDisconnect(disconnectedSocket, reason) {
|
|
28653
28724
|
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
@@ -29711,6 +29782,7 @@ import { existsSync as existsSync12, realpathSync as realpathSync3 } from "node:
|
|
|
29711
29782
|
import { readFile, realpath } from "node:fs/promises";
|
|
29712
29783
|
import { createConnection as createConnection3 } from "node:net";
|
|
29713
29784
|
import { dirname as dirname9, join as join15, resolve as resolve3 } from "node:path";
|
|
29785
|
+
import { setTimeout as delay3 } from "node:timers/promises";
|
|
29714
29786
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
29715
29787
|
var DEFAULT_LSP_PORT = 6005;
|
|
29716
29788
|
var DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
@@ -29730,6 +29802,14 @@ class LSPTimeout extends Error {
|
|
|
29730
29802
|
this.seconds = timeoutMs / 1000;
|
|
29731
29803
|
}
|
|
29732
29804
|
}
|
|
29805
|
+
var LANGUAGE_SERVER_START_MS = 30000;
|
|
29806
|
+
var LANGUAGE_SERVER_RETRY_MS = 250;
|
|
29807
|
+
|
|
29808
|
+
class LanguageServerNotListening extends Error {
|
|
29809
|
+
constructor(port, waitedMs) {
|
|
29810
|
+
super(`The editor on the bridge says its language server is on port ${port}, and nothing accepted a connection there in ${String(waitedMs / 1000)}s. An editor starts it a few seconds after it reaches the bridge, so straight after editor_launch this is the editor still starting, and calling again answers. One that goes on refusing could not take the port; editor_output shows what the editor printed.`);
|
|
29811
|
+
}
|
|
29812
|
+
}
|
|
29733
29813
|
|
|
29734
29814
|
class ArgumentRefusal extends Refusal {
|
|
29735
29815
|
}
|
|
@@ -29777,17 +29857,38 @@ class GodotLSPClient {
|
|
|
29777
29857
|
documentVersions = new Map;
|
|
29778
29858
|
documentTurns = new Map;
|
|
29779
29859
|
timeouts;
|
|
29860
|
+
servedHere;
|
|
29780
29861
|
constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1", timeouts = {
|
|
29781
29862
|
requestMs: REQUEST_TIMEOUT_MS2,
|
|
29782
29863
|
referencesMs: REFERENCES_TIMEOUT_MS,
|
|
29783
|
-
initializeMs: initializeTimeoutMs()
|
|
29784
|
-
|
|
29864
|
+
initializeMs: initializeTimeoutMs(),
|
|
29865
|
+
listenMs: LANGUAGE_SERVER_START_MS
|
|
29866
|
+
}, servedHere = () => false) {
|
|
29785
29867
|
this.port = port;
|
|
29786
29868
|
this.host = host;
|
|
29787
29869
|
this.timeouts = timeouts;
|
|
29870
|
+
this.servedHere = servedHere;
|
|
29788
29871
|
this.pendingRequests = new Map;
|
|
29789
29872
|
}
|
|
29790
29873
|
async connect() {
|
|
29874
|
+
const until = Date.now() + this.timeouts.listenMs;
|
|
29875
|
+
for (;; ) {
|
|
29876
|
+
try {
|
|
29877
|
+
await this.connectOnce();
|
|
29878
|
+
return;
|
|
29879
|
+
} catch (error) {
|
|
29880
|
+
const refused = error instanceof Error && error.message.includes("ECONNREFUSED");
|
|
29881
|
+
if (!refused || !this.servedHere()) {
|
|
29882
|
+
throw error;
|
|
29883
|
+
}
|
|
29884
|
+
if (Date.now() >= until) {
|
|
29885
|
+
throw new LanguageServerNotListening(this.port, this.timeouts.listenMs);
|
|
29886
|
+
}
|
|
29887
|
+
await delay3(LANGUAGE_SERVER_RETRY_MS);
|
|
29888
|
+
}
|
|
29889
|
+
}
|
|
29890
|
+
}
|
|
29891
|
+
async connectOnce() {
|
|
29791
29892
|
if (this.connected && this.socket) {
|
|
29792
29893
|
return;
|
|
29793
29894
|
}
|
|
@@ -30476,7 +30577,8 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
30476
30577
|
...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
|
|
30477
30578
|
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {},
|
|
30478
30579
|
...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {},
|
|
30479
|
-
...error instanceof LSPTimeout && error.method === "initialize" ? { stillReading: true } : {}
|
|
30580
|
+
...error instanceof LSPTimeout && error.method === "initialize" ? { stillReading: true } : {},
|
|
30581
|
+
...error instanceof LanguageServerNotListening ? { stillReading: true } : {}
|
|
30480
30582
|
});
|
|
30481
30583
|
}
|
|
30482
30584
|
}
|
|
@@ -30812,7 +30914,7 @@ function searchProject(projectPath, options) {
|
|
|
30812
30914
|
|
|
30813
30915
|
// src/rename.ts
|
|
30814
30916
|
import { existsSync as existsSync14, mkdirSync as mkdirSync8, readFileSync as readFileSync15, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
|
|
30815
|
-
import { dirname as dirname10, posix } from "node:path";
|
|
30917
|
+
import { dirname as dirname10, posix as posix2 } from "node:path";
|
|
30816
30918
|
class Script {
|
|
30817
30919
|
path;
|
|
30818
30920
|
text;
|
|
@@ -30924,13 +31026,13 @@ class ClassGraph {
|
|
|
30924
31026
|
}
|
|
30925
31027
|
function resolvedPath(written, from) {
|
|
30926
31028
|
if (written.startsWith("res://")) {
|
|
30927
|
-
return `res://${
|
|
31029
|
+
return `res://${posix2.normalize(written.slice("res://".length))}`;
|
|
30928
31030
|
}
|
|
30929
|
-
const directory =
|
|
30930
|
-
return `res://${
|
|
31031
|
+
const directory = posix2.dirname(from.slice("res://".length));
|
|
31032
|
+
return `res://${posix2.normalize(posix2.join(directory, written))}`;
|
|
30931
31033
|
}
|
|
30932
31034
|
function relativeTo(target, from) {
|
|
30933
|
-
return
|
|
31035
|
+
return posix2.relative(posix2.dirname(from.slice("res://".length)), target.slice("res://".length));
|
|
30934
31036
|
}
|
|
30935
31037
|
function scriptsOf(texts) {
|
|
30936
31038
|
const scripts = new Map;
|
|
@@ -33661,7 +33763,7 @@ var TOOL_SPECS = [
|
|
|
33661
33763
|
},
|
|
33662
33764
|
{
|
|
33663
33765
|
name: "script_diagnostics",
|
|
33664
|
-
description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
33766
|
+
description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, or a project class it extends does, a method, property, constant, static function or enum member, comes back under contradictedByTheFile with the script declaring it, and so does one a diagnostic names alone as an external class member, read against the class it is looked up on in the line it is on; and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
33665
33767
|
parameters: {
|
|
33666
33768
|
projectPath: PROJECT_PATH,
|
|
33667
33769
|
scriptPath: {
|
|
@@ -33886,7 +33988,11 @@ var TOOL_SPECS = [
|
|
|
33886
33988
|
},
|
|
33887
33989
|
reloadScript: {
|
|
33888
33990
|
type: "string",
|
|
33889
|
-
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one.
|
|
33991
|
+
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. What the reload changed comes back under methodsAdded, methodsRemoved, constantsAdded and constantsRemoved, with each enum value as Kind.SHORT among the constants, since an enum member is held as a constant and never shows among the methods; methodsHeld and constantsHeld count what the copy holds afterwards, because a reload that compiled nothing and answered OK is the failure worth catching. listMembers adds the whole lists. A script that does not compile is not reloaded: reloadProblem gives Godot's error, says whether the copy the editor holds survived, and names the classes the file uses, whose held copies being behind is what failed one before. A @tool script is not reloaded either, since it runs inside the editor and reloading one from inside a call can take the editor down: reloadProblem says so, and editor_launch restart recompiles it. A path spelled in another case than the file is reloaded under the spelling on disk, which is the one the editor holds its copy under, and that spelling comes back under reloadedAs."
|
|
33992
|
+
},
|
|
33993
|
+
listMembers: {
|
|
33994
|
+
type: "boolean",
|
|
33995
|
+
description: "With reloadScript: also answer the whole member lists, heldBeforeReload and reloadedMethods for the methods, heldConstantsBeforeReload and reloadedConstants for the constants. Default false: a script holding hundreds of constants answers hundreds of lines twice to say what one reload added."
|
|
33890
33996
|
}
|
|
33891
33997
|
},
|
|
33892
33998
|
requires: ["projectPath"]
|
|
@@ -34572,7 +34678,7 @@ async function untilGone(pids, withinMs = STOP_WAIT_MS) {
|
|
|
34572
34678
|
const gone = () => pids.every((pid) => !alive(pid));
|
|
34573
34679
|
const deadline = Date.now() + withinMs;
|
|
34574
34680
|
while (!gone() && Date.now() < deadline) {
|
|
34575
|
-
await
|
|
34681
|
+
await delay4(50);
|
|
34576
34682
|
}
|
|
34577
34683
|
return gone();
|
|
34578
34684
|
}
|
|
@@ -35254,6 +35360,7 @@ function scriptsToDiagnose(projectPath, given) {
|
|
|
35254
35360
|
}
|
|
35255
35361
|
return { ok: true, value: unique };
|
|
35256
35362
|
}
|
|
35363
|
+
var EXTERNAL_MEMBER = 'Could not resolve external class member "';
|
|
35257
35364
|
function isDirectory(path) {
|
|
35258
35365
|
try {
|
|
35259
35366
|
return statSync8(path).isDirectory();
|
|
@@ -35391,8 +35498,12 @@ class GodotServer {
|
|
|
35391
35498
|
console.error("gdharness running on stdio");
|
|
35392
35499
|
sweepAbandonedScratch();
|
|
35393
35500
|
try {
|
|
35394
|
-
|
|
35395
|
-
|
|
35501
|
+
try {
|
|
35502
|
+
await this.takeOverFromAPredecessor();
|
|
35503
|
+
await this.godotBridge.start();
|
|
35504
|
+
} finally {
|
|
35505
|
+
this.handingOverFrom = null;
|
|
35506
|
+
}
|
|
35396
35507
|
this.bridgeStartupError = null;
|
|
35397
35508
|
const bridgeStatus = this.godotBridge.getStatus();
|
|
35398
35509
|
console.error(`[SERVER] Godot Editor Bridge started on ${bridgeStatus.host}:${bridgeStatus.port}`);
|
|
@@ -35429,10 +35540,9 @@ class GodotServer {
|
|
|
35429
35540
|
taken = true;
|
|
35430
35541
|
this.logDebug(`Took port ${wanted} over from pid ${announced.pid}`);
|
|
35431
35542
|
} catch {
|
|
35432
|
-
await
|
|
35543
|
+
await delay4(HANDOVER_POLL_MS);
|
|
35433
35544
|
}
|
|
35434
35545
|
}
|
|
35435
|
-
this.handingOverFrom = null;
|
|
35436
35546
|
this.notHandedOverBy = taken ? null : announced.pid;
|
|
35437
35547
|
}
|
|
35438
35548
|
keepTryingTheBridge() {
|
|
@@ -35464,10 +35574,9 @@ class GodotServer {
|
|
|
35464
35574
|
if (this.ownProject === null) {
|
|
35465
35575
|
return;
|
|
35466
35576
|
}
|
|
35467
|
-
const status = this.godotBridge.getStatus();
|
|
35468
35577
|
this.announcedAt = announceBridge(this.ownProject, {
|
|
35469
|
-
host:
|
|
35470
|
-
port:
|
|
35578
|
+
host: this.godotBridge.getStatus().host,
|
|
35579
|
+
port: this.godotBridge.port,
|
|
35471
35580
|
version: SERVER_VERSION
|
|
35472
35581
|
});
|
|
35473
35582
|
this.watchForASuccessor();
|
|
@@ -37202,14 +37311,14 @@ class GodotServer {
|
|
|
37202
37311
|
}
|
|
37203
37312
|
whereTheFileDisagrees(diagnostics, args) {
|
|
37204
37313
|
const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
|
|
37205
|
-
const worthChecking = messages.some((message) => missingMemberIn(message) !== null || unknownTypeIn(message) !== null);
|
|
37314
|
+
const worthChecking = messages.some((message) => missingMemberIn(message) !== null || unknownTypeIn(message) !== null || message.includes(EXTERNAL_MEMBER));
|
|
37206
37315
|
const project = worthChecking ? this.project(args) : null;
|
|
37207
37316
|
if (project === null || !project.ok) {
|
|
37208
37317
|
return {};
|
|
37209
37318
|
}
|
|
37210
37319
|
const projectPath = project.value.path;
|
|
37211
37320
|
const cached = cachedClasses(projectPath);
|
|
37212
|
-
const
|
|
37321
|
+
const sourceOf = (resourcePath) => {
|
|
37213
37322
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
37214
37323
|
if (!contained.ok || !existsSync17(contained.absolutePath)) {
|
|
37215
37324
|
return null;
|
|
@@ -37219,7 +37328,16 @@ class GodotServer {
|
|
|
37219
37328
|
} catch {
|
|
37220
37329
|
return null;
|
|
37221
37330
|
}
|
|
37222
|
-
}
|
|
37331
|
+
};
|
|
37332
|
+
const external = diagnostics.filter((entry) => String(asParams(entry)["message"]).includes(EXTERNAL_MEMBER));
|
|
37333
|
+
const diagnosed = external.length === 0 ? null : sourceOf(readString(args, "scriptPath") ?? "");
|
|
37334
|
+
const lines = diagnosed?.split(`
|
|
37335
|
+
`) ?? [];
|
|
37336
|
+
const alsoDenied = cached === null ? [] : external.map((entry) => {
|
|
37337
|
+
const { message, line } = asParams(entry);
|
|
37338
|
+
return externalMemberIn(String(message), typeof line === "number" ? lines[line - 1] : undefined, cached);
|
|
37339
|
+
}).filter((missing) => missing !== null);
|
|
37340
|
+
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, sourceOf, alsoDenied);
|
|
37223
37341
|
const unloaded = messages.some((message) => unknownTypeIn(message) !== null) ? unloadedTypes(messages, declaredClasses(projectPath), cached) : [];
|
|
37224
37342
|
const notes = [];
|
|
37225
37343
|
if (contradicted.length > 0) {
|
|
@@ -37289,7 +37407,10 @@ class GodotServer {
|
|
|
37289
37407
|
await this.lspClient.disconnect();
|
|
37290
37408
|
this.lspClient = null;
|
|
37291
37409
|
}
|
|
37292
|
-
this.lspClient ??= new GodotLSPClient(port)
|
|
37410
|
+
this.lspClient ??= new GodotLSPClient(port, undefined, undefined, () => {
|
|
37411
|
+
const status = this.godotBridge.getStatus();
|
|
37412
|
+
return status.connected && status.lspPort === port;
|
|
37413
|
+
});
|
|
37293
37414
|
return this.lspClient;
|
|
37294
37415
|
}
|
|
37295
37416
|
async handleDAP(toolName, args) {
|
|
@@ -37410,19 +37531,22 @@ class GodotServer {
|
|
|
37410
37531
|
return { note: owed, underway: restartUnderway(owed) };
|
|
37411
37532
|
}
|
|
37412
37533
|
async getEditorStatusPayload() {
|
|
37534
|
+
const wasConnected = this.godotBridge.getStatus().connected;
|
|
37535
|
+
const launchedWasUp = !wasConnected && await this.launchedEditorIsUp();
|
|
37536
|
+
const arrivingFound = wasConnected || this.ownProject === null ? [] : await this.editorsNotYetConnected(this.ownProject) ?? [];
|
|
37413
37537
|
const status = this.godotBridge.getStatus();
|
|
37414
|
-
const launchedIsUp = !status.connected &&
|
|
37538
|
+
const launchedIsUp = !status.connected && launchedWasUp;
|
|
37539
|
+
const arriving = status.connected ? [] : arrivingFound;
|
|
37415
37540
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
37416
37541
|
const greeted = hasSaidWhoItIs(status);
|
|
37417
37542
|
const stale = greeted && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
|
|
37418
37543
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
37419
|
-
const arriving = status.connected || this.ownProject === null ? [] : await this.editorsNotYetConnected(this.ownProject) ?? [];
|
|
37420
37544
|
return {
|
|
37421
37545
|
...status,
|
|
37422
37546
|
serverVersion: SERVER_VERSION,
|
|
37423
37547
|
addonIsStale: greeted ? stale : undefined,
|
|
37424
37548
|
greeting: status.connected && !greeted ? "The editor has connected and has not yet said who it is: its project, pid, addon version and ports arrive with its greeting a moment after the socket opens, later while it imports. Ask again for them." : undefined,
|
|
37425
|
-
bridgeAvailable: this.bridgeStartupError === null,
|
|
37549
|
+
bridgeAvailable: this.bridgeStartupError === null && status.port !== null,
|
|
37426
37550
|
mayYetConnect: status.connected ? undefined : arriving.length > 0 ? true : unfinished !== null ? unfinished.underway : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
37427
37551
|
editorsNotYetConnected: arriving.length > 0 ? arriving : undefined,
|
|
37428
37552
|
restartInterrupted: unfinished === null || unfinished.underway || arriving.length > 0 ? undefined : {
|
|
@@ -37466,7 +37590,7 @@ class GodotServer {
|
|
|
37466
37590
|
try {
|
|
37467
37591
|
const askedAt = Date.now();
|
|
37468
37592
|
const asked = this.godotBridge.invokeTool("playing_status", {});
|
|
37469
|
-
const answer = await Promise.race([asked,
|
|
37593
|
+
const answer = await Promise.race([asked, delay4(PLAYING_STATUS_MS).then(() => null)]);
|
|
37470
37594
|
if (answer === null) {
|
|
37471
37595
|
this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
|
|
37472
37596
|
asked.then(() => {
|
|
@@ -37491,7 +37615,7 @@ class GodotServer {
|
|
|
37491
37615
|
try {
|
|
37492
37616
|
const answer = await Promise.race([
|
|
37493
37617
|
this.godotBridge.invokeTool("scan_status", {}),
|
|
37494
|
-
|
|
37618
|
+
delay4(PLAYING_STATUS_MS).then(() => null)
|
|
37495
37619
|
]);
|
|
37496
37620
|
if (answer === null) {
|
|
37497
37621
|
return null;
|
|
@@ -37703,7 +37827,7 @@ class GodotServer {
|
|
|
37703
37827
|
if (Date.now() >= deadline) {
|
|
37704
37828
|
return false;
|
|
37705
37829
|
}
|
|
37706
|
-
await
|
|
37830
|
+
await delay4(250);
|
|
37707
37831
|
}
|
|
37708
37832
|
}
|
|
37709
37833
|
async handleLaunchEditor(args) {
|
|
@@ -38213,7 +38337,7 @@ class GodotServer {
|
|
|
38213
38337
|
if (Date.now() >= deadline) {
|
|
38214
38338
|
return false;
|
|
38215
38339
|
}
|
|
38216
|
-
await
|
|
38340
|
+
await delay4(100);
|
|
38217
38341
|
}
|
|
38218
38342
|
}
|
|
38219
38343
|
async endActiveGame(reason, going) {
|
|
@@ -38805,7 +38929,7 @@ class GodotServer {
|
|
|
38805
38929
|
while (cancelled?.aborted !== true && await this.runStillGoing(run) && Date.now() < until) {
|
|
38806
38930
|
this.drainEditorOutput(run);
|
|
38807
38931
|
this.drainTranscript(run);
|
|
38808
|
-
await
|
|
38932
|
+
await delay4(RUN_POLL_MS);
|
|
38809
38933
|
}
|
|
38810
38934
|
return await this.handleGetDebugOutput(args, await this.runStillGoing(run) ? budgetMs : undefined);
|
|
38811
38935
|
}
|
|
@@ -38842,7 +38966,7 @@ class GodotServer {
|
|
|
38842
38966
|
if (Date.now() >= until) {
|
|
38843
38967
|
return false;
|
|
38844
38968
|
}
|
|
38845
|
-
await
|
|
38969
|
+
await delay4(50);
|
|
38846
38970
|
}
|
|
38847
38971
|
}
|
|
38848
38972
|
async handleEditorConsole(args) {
|
|
@@ -39352,10 +39476,13 @@ class GodotServer {
|
|
|
39352
39476
|
waitedMs: Date.now() - started,
|
|
39353
39477
|
waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
|
|
39354
39478
|
reloadedAs: reloaded.as,
|
|
39355
|
-
|
|
39356
|
-
|
|
39357
|
-
|
|
39358
|
-
|
|
39479
|
+
...reloadDifference(reloaded),
|
|
39480
|
+
...args["listMembers"] === true ? {
|
|
39481
|
+
heldBeforeReload: reloaded.heldBefore,
|
|
39482
|
+
reloadedMethods: reloaded.methods,
|
|
39483
|
+
heldConstantsBeforeReload: reloaded.heldConstantsBefore,
|
|
39484
|
+
reloadedConstants: reloaded.constants
|
|
39485
|
+
} : {},
|
|
39359
39486
|
reloadProblem: reloaded.problem,
|
|
39360
39487
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
39361
39488
|
broughtIn: broughtIn.length > 0 ? broughtIn : undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.54",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|