gdharness 1.1.53 → 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.
Files changed (3) hide show
  1. package/build/cli.js +144 -43
  2. package/build/index.js +146 -41
  3. 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 = messages.map((message) => missingMemberIn(message)).filter((missing) => missing !== null);
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 (declaredIn === undefined || missing.kind === "method" && calledOnTheClass.has(key)) {
9337
+ if (found.has(key) || missing.kind === "method" && calledOnTheClass.has(key)) {
9309
9338
  continue;
9310
9339
  }
9311
- if (!read.has(declaredIn)) {
9312
- read.set(declaredIn, sourceOf(declaredIn));
9313
- }
9314
- const source = read.get(declaredIn) ?? null;
9315
- if (source !== null && declaresMember(source, missing)) {
9316
- found.set(key, { ...missing, declaredIn });
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 file the class cache " + "points at, so those diagnostics are wrong however the code is written.",
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 the members it has afterwards under reloadedMethods. 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.";
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 under heldBeforeReload and heldConstantsBeforeReload.";
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. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching, and what it held before under heldBeforeReload. Its constants come back the same way, under heldConstantsBeforeReload and reloadedConstants, with each enum value as Kind.SHORT: an enum member is held as a constant and never shows among the methods. 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."
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"]
@@ -39139,6 +39197,7 @@ import { existsSync as existsSync16, realpathSync as realpathSync4 } from "node:
39139
39197
  import { readFile, realpath } from "node:fs/promises";
39140
39198
  import { createConnection as createConnection3 } from "node:net";
39141
39199
  import { dirname as dirname14, join as join20, resolve as resolve5 } from "node:path";
39200
+ import { setTimeout as delay3 } from "node:timers/promises";
39142
39201
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
39143
39202
  function initializeTimeoutMs() {
39144
39203
  const override = Number.parseInt(envValue("GDHARNESS_LSP_INITIALIZE_TIMEOUT_MS") ?? "", 10);
@@ -39177,17 +39236,38 @@ class GodotLSPClient {
39177
39236
  documentVersions = new Map;
39178
39237
  documentTurns = new Map;
39179
39238
  timeouts;
39239
+ servedHere;
39180
39240
  constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1", timeouts = {
39181
39241
  requestMs: REQUEST_TIMEOUT_MS2,
39182
39242
  referencesMs: REFERENCES_TIMEOUT_MS,
39183
- initializeMs: initializeTimeoutMs()
39184
- }) {
39243
+ initializeMs: initializeTimeoutMs(),
39244
+ listenMs: LANGUAGE_SERVER_START_MS
39245
+ }, servedHere = () => false) {
39185
39246
  this.port = port;
39186
39247
  this.host = host;
39187
39248
  this.timeouts = timeouts;
39249
+ this.servedHere = servedHere;
39188
39250
  this.pendingRequests = new Map;
39189
39251
  }
39190
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() {
39191
39271
  if (this.connected && this.socket) {
39192
39272
  return;
39193
39273
  }
@@ -39820,11 +39900,12 @@ async function handleLSPTool(client, toolName, args) {
39820
39900
  ...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
39821
39901
  ...error instanceof ArgumentRefusal ? { refusedArguments: true } : {},
39822
39902
  ...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {},
39823
- ...error instanceof LSPTimeout && error.method === "initialize" ? { stillReading: true } : {}
39903
+ ...error instanceof LSPTimeout && error.method === "initialize" ? { stillReading: true } : {},
39904
+ ...error instanceof LanguageServerNotListening ? { stillReading: true } : {}
39824
39905
  });
39825
39906
  }
39826
39907
  }
39827
- 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;
39828
39909
  var init_lsp_client = __esm(() => {
39829
39910
  init_answer_json();
39830
39911
  init_errors();
@@ -39840,6 +39921,11 @@ var init_lsp_client = __esm(() => {
39840
39921
  this.seconds = timeoutMs / 1000;
39841
39922
  }
39842
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
+ };
39843
39929
  ArgumentRefusal = class ArgumentRefusal extends Refusal {
39844
39930
  };
39845
39931
  NoDiagnosticsPublished = class NoDiagnosticsPublished extends Error {
@@ -40249,7 +40335,7 @@ var init_project_scan = __esm(() => {
40249
40335
 
40250
40336
  // src/rename.ts
40251
40337
  import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync19, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync13 } from "node:fs";
40252
- import { dirname as dirname15, posix } from "node:path";
40338
+ import { dirname as dirname15, posix as posix2 } from "node:path";
40253
40339
 
40254
40340
  class Script {
40255
40341
  path;
@@ -40362,13 +40448,13 @@ class ClassGraph {
40362
40448
  }
40363
40449
  function resolvedPath(written, from) {
40364
40450
  if (written.startsWith("res://")) {
40365
- return `res://${posix.normalize(written.slice("res://".length))}`;
40451
+ return `res://${posix2.normalize(written.slice("res://".length))}`;
40366
40452
  }
40367
- const directory = posix.dirname(from.slice("res://".length));
40368
- return `res://${posix.normalize(posix.join(directory, written))}`;
40453
+ const directory = posix2.dirname(from.slice("res://".length));
40454
+ return `res://${posix2.normalize(posix2.join(directory, written))}`;
40369
40455
  }
40370
40456
  function relativeTo(target, from) {
40371
- return posix.relative(posix.dirname(from.slice("res://".length)), target.slice("res://".length));
40457
+ return posix2.relative(posix2.dirname(from.slice("res://".length)), target.slice("res://".length));
40372
40458
  }
40373
40459
  function scriptsOf(texts) {
40374
40460
  const scripts = new Map;
@@ -42030,7 +42116,7 @@ import {
42030
42116
  statSync as statSync9
42031
42117
  } from "node:fs";
42032
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";
42033
- import { setTimeout as delay3 } from "node:timers/promises";
42119
+ import { setTimeout as delay4 } from "node:timers/promises";
42034
42120
  import { fileURLToPath as fileURLToPath5 } from "node:url";
42035
42121
  import { promisify as promisify5 } from "node:util";
42036
42122
  function editorRestartTimeoutMs() {
@@ -42163,7 +42249,7 @@ async function untilGone(pids, withinMs = STOP_WAIT_MS) {
42163
42249
  const gone = () => pids.every((pid) => !alive(pid));
42164
42250
  const deadline = Date.now() + withinMs;
42165
42251
  while (!gone() && Date.now() < deadline) {
42166
- await delay3(50);
42252
+ await delay4(50);
42167
42253
  }
42168
42254
  return gone();
42169
42255
  }
@@ -42962,7 +43048,7 @@ class GodotServer {
42962
43048
  taken = true;
42963
43049
  this.logDebug(`Took port ${wanted} over from pid ${announced.pid}`);
42964
43050
  } catch {
42965
- await delay3(HANDOVER_POLL_MS);
43051
+ await delay4(HANDOVER_POLL_MS);
42966
43052
  }
42967
43053
  }
42968
43054
  this.notHandedOverBy = taken ? null : announced.pid;
@@ -44733,14 +44819,14 @@ class GodotServer {
44733
44819
  }
44734
44820
  whereTheFileDisagrees(diagnostics, args) {
44735
44821
  const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
44736
- 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));
44737
44823
  const project = worthChecking ? this.project(args) : null;
44738
44824
  if (project === null || !project.ok) {
44739
44825
  return {};
44740
44826
  }
44741
44827
  const projectPath = project.value.path;
44742
44828
  const cached = cachedClasses(projectPath);
44743
- const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
44829
+ const sourceOf = (resourcePath) => {
44744
44830
  const contained = resolveWithinProject(projectPath, resourcePath);
44745
44831
  if (!contained.ok || !existsSync20(contained.absolutePath)) {
44746
44832
  return null;
@@ -44750,7 +44836,16 @@ class GodotServer {
44750
44836
  } catch {
44751
44837
  return null;
44752
44838
  }
44753
- });
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);
44754
44849
  const unloaded = messages.some((message) => unknownTypeIn(message) !== null) ? unloadedTypes(messages, declaredClasses(projectPath), cached) : [];
44755
44850
  const notes = [];
44756
44851
  if (contradicted.length > 0) {
@@ -44820,7 +44915,10 @@ class GodotServer {
44820
44915
  await this.lspClient.disconnect();
44821
44916
  this.lspClient = null;
44822
44917
  }
44823
- 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
+ });
44824
44922
  return this.lspClient;
44825
44923
  }
44826
44924
  async handleDAP(toolName, args) {
@@ -45000,7 +45098,7 @@ class GodotServer {
45000
45098
  try {
45001
45099
  const askedAt = Date.now();
45002
45100
  const asked = this.godotBridge.invokeTool("playing_status", {});
45003
- const answer = await Promise.race([asked, delay3(PLAYING_STATUS_MS).then(() => null)]);
45101
+ const answer = await Promise.race([asked, delay4(PLAYING_STATUS_MS).then(() => null)]);
45004
45102
  if (answer === null) {
45005
45103
  this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
45006
45104
  asked.then(() => {
@@ -45025,7 +45123,7 @@ class GodotServer {
45025
45123
  try {
45026
45124
  const answer = await Promise.race([
45027
45125
  this.godotBridge.invokeTool("scan_status", {}),
45028
- delay3(PLAYING_STATUS_MS).then(() => null)
45126
+ delay4(PLAYING_STATUS_MS).then(() => null)
45029
45127
  ]);
45030
45128
  if (answer === null) {
45031
45129
  return null;
@@ -45237,7 +45335,7 @@ class GodotServer {
45237
45335
  if (Date.now() >= deadline) {
45238
45336
  return false;
45239
45337
  }
45240
- await delay3(250);
45338
+ await delay4(250);
45241
45339
  }
45242
45340
  }
45243
45341
  async handleLaunchEditor(args) {
@@ -45747,7 +45845,7 @@ class GodotServer {
45747
45845
  if (Date.now() >= deadline) {
45748
45846
  return false;
45749
45847
  }
45750
- await delay3(100);
45848
+ await delay4(100);
45751
45849
  }
45752
45850
  }
45753
45851
  async endActiveGame(reason, going) {
@@ -46339,7 +46437,7 @@ class GodotServer {
46339
46437
  while (cancelled?.aborted !== true && await this.runStillGoing(run) && Date.now() < until) {
46340
46438
  this.drainEditorOutput(run);
46341
46439
  this.drainTranscript(run);
46342
- await delay3(RUN_POLL_MS);
46440
+ await delay4(RUN_POLL_MS);
46343
46441
  }
46344
46442
  return await this.handleGetDebugOutput(args, await this.runStillGoing(run) ? budgetMs : undefined);
46345
46443
  }
@@ -46376,7 +46474,7 @@ class GodotServer {
46376
46474
  if (Date.now() >= until) {
46377
46475
  return false;
46378
46476
  }
46379
- await delay3(50);
46477
+ await delay4(50);
46380
46478
  }
46381
46479
  }
46382
46480
  async handleEditorConsole(args) {
@@ -46886,10 +46984,13 @@ class GodotServer {
46886
46984
  waitedMs: Date.now() - started,
46887
46985
  waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
46888
46986
  reloadedAs: reloaded.as,
46889
- heldBeforeReload: reloaded.heldBefore,
46890
- reloadedMethods: reloaded.methods,
46891
- heldConstantsBeforeReload: reloaded.heldConstantsBefore,
46892
- reloadedConstants: reloaded.constants,
46987
+ ...reloadDifference(reloaded),
46988
+ ...args["listMembers"] === true ? {
46989
+ heldBeforeReload: reloaded.heldBefore,
46990
+ reloadedMethods: reloaded.methods,
46991
+ heldConstantsBeforeReload: reloaded.heldConstantsBefore,
46992
+ reloadedConstants: reloaded.constants
46993
+ } : {},
46893
46994
  reloadProblem: reloaded.problem,
46894
46995
  unseenByEditor: unseen.length > 0 ? unseen : undefined,
46895
46996
  broughtIn: broughtIn.length > 0 ? broughtIn : undefined,
@@ -47565,7 +47666,7 @@ async function runGodotServer() {
47565
47666
  const server = new GodotServer;
47566
47667
  await server.run();
47567
47668
  }
47568
- 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 "';
47569
47670
  var init_server3 = __esm(() => {
47570
47671
  init_mcp();
47571
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 delay3 } from "node:timers/promises";
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 = messages.map((message) => missingMemberIn(message)).filter((missing) => missing !== null);
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 (declaredIn === undefined || missing.kind === "method" && calledOnTheClass.has(key)) {
26792
+ if (found.has(key) || missing.kind === "method" && calledOnTheClass.has(key)) {
26764
26793
  continue;
26765
26794
  }
26766
- if (!read.has(declaredIn)) {
26767
- read.set(declaredIn, sourceOf(declaredIn));
26768
- }
26769
- const source = read.get(declaredIn) ?? null;
26770
- if (source !== null && declaresMember(source, missing)) {
26771
- found.set(key, { ...missing, declaredIn });
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 file the class cache " + "points at, so those diagnostics are wrong however the code is written.",
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 the members it has afterwards under reloadedMethods. 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.";
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 under heldBeforeReload and heldConstantsBeforeReload.";
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
  }
@@ -29728,6 +29782,7 @@ import { existsSync as existsSync12, realpathSync as realpathSync3 } from "node:
29728
29782
  import { readFile, realpath } from "node:fs/promises";
29729
29783
  import { createConnection as createConnection3 } from "node:net";
29730
29784
  import { dirname as dirname9, join as join15, resolve as resolve3 } from "node:path";
29785
+ import { setTimeout as delay3 } from "node:timers/promises";
29731
29786
  import { fileURLToPath, pathToFileURL } from "node:url";
29732
29787
  var DEFAULT_LSP_PORT = 6005;
29733
29788
  var DIAGNOSTICS_TIMEOUT_MS = 5000;
@@ -29747,6 +29802,14 @@ class LSPTimeout extends Error {
29747
29802
  this.seconds = timeoutMs / 1000;
29748
29803
  }
29749
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
+ }
29750
29813
 
29751
29814
  class ArgumentRefusal extends Refusal {
29752
29815
  }
@@ -29794,17 +29857,38 @@ class GodotLSPClient {
29794
29857
  documentVersions = new Map;
29795
29858
  documentTurns = new Map;
29796
29859
  timeouts;
29860
+ servedHere;
29797
29861
  constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1", timeouts = {
29798
29862
  requestMs: REQUEST_TIMEOUT_MS2,
29799
29863
  referencesMs: REFERENCES_TIMEOUT_MS,
29800
- initializeMs: initializeTimeoutMs()
29801
- }) {
29864
+ initializeMs: initializeTimeoutMs(),
29865
+ listenMs: LANGUAGE_SERVER_START_MS
29866
+ }, servedHere = () => false) {
29802
29867
  this.port = port;
29803
29868
  this.host = host;
29804
29869
  this.timeouts = timeouts;
29870
+ this.servedHere = servedHere;
29805
29871
  this.pendingRequests = new Map;
29806
29872
  }
29807
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() {
29808
29892
  if (this.connected && this.socket) {
29809
29893
  return;
29810
29894
  }
@@ -30493,7 +30577,8 @@ async function handleLSPTool(client, toolName, args) {
30493
30577
  ...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
30494
30578
  ...error instanceof ArgumentRefusal ? { refusedArguments: true } : {},
30495
30579
  ...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {},
30496
- ...error instanceof LSPTimeout && error.method === "initialize" ? { stillReading: true } : {}
30580
+ ...error instanceof LSPTimeout && error.method === "initialize" ? { stillReading: true } : {},
30581
+ ...error instanceof LanguageServerNotListening ? { stillReading: true } : {}
30497
30582
  });
30498
30583
  }
30499
30584
  }
@@ -30829,7 +30914,7 @@ function searchProject(projectPath, options) {
30829
30914
 
30830
30915
  // src/rename.ts
30831
30916
  import { existsSync as existsSync14, mkdirSync as mkdirSync8, readFileSync as readFileSync15, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
30832
- import { dirname as dirname10, posix } from "node:path";
30917
+ import { dirname as dirname10, posix as posix2 } from "node:path";
30833
30918
  class Script {
30834
30919
  path;
30835
30920
  text;
@@ -30941,13 +31026,13 @@ class ClassGraph {
30941
31026
  }
30942
31027
  function resolvedPath(written, from) {
30943
31028
  if (written.startsWith("res://")) {
30944
- return `res://${posix.normalize(written.slice("res://".length))}`;
31029
+ return `res://${posix2.normalize(written.slice("res://".length))}`;
30945
31030
  }
30946
- const directory = posix.dirname(from.slice("res://".length));
30947
- return `res://${posix.normalize(posix.join(directory, written))}`;
31031
+ const directory = posix2.dirname(from.slice("res://".length));
31032
+ return `res://${posix2.normalize(posix2.join(directory, written))}`;
30948
31033
  }
30949
31034
  function relativeTo(target, from) {
30950
- return posix.relative(posix.dirname(from.slice("res://".length)), target.slice("res://".length));
31035
+ return posix2.relative(posix2.dirname(from.slice("res://".length)), target.slice("res://".length));
30951
31036
  }
30952
31037
  function scriptsOf(texts) {
30953
31038
  const scripts = new Map;
@@ -33678,7 +33763,7 @@ var TOOL_SPECS = [
33678
33763
  },
33679
33764
  {
33680
33765
  name: "script_diagnostics",
33681
- 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.",
33682
33767
  parameters: {
33683
33768
  projectPath: PROJECT_PATH,
33684
33769
  scriptPath: {
@@ -33903,7 +33988,11 @@ var TOOL_SPECS = [
33903
33988
  },
33904
33989
  reloadScript: {
33905
33990
  type: "string",
33906
- 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. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching, and what it held before under heldBeforeReload. Its constants come back the same way, under heldConstantsBeforeReload and reloadedConstants, with each enum value as Kind.SHORT: an enum member is held as a constant and never shows among the methods. 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."
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."
33907
33996
  }
33908
33997
  },
33909
33998
  requires: ["projectPath"]
@@ -34589,7 +34678,7 @@ async function untilGone(pids, withinMs = STOP_WAIT_MS) {
34589
34678
  const gone = () => pids.every((pid) => !alive(pid));
34590
34679
  const deadline = Date.now() + withinMs;
34591
34680
  while (!gone() && Date.now() < deadline) {
34592
- await delay3(50);
34681
+ await delay4(50);
34593
34682
  }
34594
34683
  return gone();
34595
34684
  }
@@ -35271,6 +35360,7 @@ function scriptsToDiagnose(projectPath, given) {
35271
35360
  }
35272
35361
  return { ok: true, value: unique };
35273
35362
  }
35363
+ var EXTERNAL_MEMBER = 'Could not resolve external class member "';
35274
35364
  function isDirectory(path) {
35275
35365
  try {
35276
35366
  return statSync8(path).isDirectory();
@@ -35450,7 +35540,7 @@ class GodotServer {
35450
35540
  taken = true;
35451
35541
  this.logDebug(`Took port ${wanted} over from pid ${announced.pid}`);
35452
35542
  } catch {
35453
- await delay3(HANDOVER_POLL_MS);
35543
+ await delay4(HANDOVER_POLL_MS);
35454
35544
  }
35455
35545
  }
35456
35546
  this.notHandedOverBy = taken ? null : announced.pid;
@@ -37221,14 +37311,14 @@ class GodotServer {
37221
37311
  }
37222
37312
  whereTheFileDisagrees(diagnostics, args) {
37223
37313
  const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
37224
- 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));
37225
37315
  const project = worthChecking ? this.project(args) : null;
37226
37316
  if (project === null || !project.ok) {
37227
37317
  return {};
37228
37318
  }
37229
37319
  const projectPath = project.value.path;
37230
37320
  const cached = cachedClasses(projectPath);
37231
- const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
37321
+ const sourceOf = (resourcePath) => {
37232
37322
  const contained = resolveWithinProject(projectPath, resourcePath);
37233
37323
  if (!contained.ok || !existsSync17(contained.absolutePath)) {
37234
37324
  return null;
@@ -37238,7 +37328,16 @@ class GodotServer {
37238
37328
  } catch {
37239
37329
  return null;
37240
37330
  }
37241
- });
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);
37242
37341
  const unloaded = messages.some((message) => unknownTypeIn(message) !== null) ? unloadedTypes(messages, declaredClasses(projectPath), cached) : [];
37243
37342
  const notes = [];
37244
37343
  if (contradicted.length > 0) {
@@ -37308,7 +37407,10 @@ class GodotServer {
37308
37407
  await this.lspClient.disconnect();
37309
37408
  this.lspClient = null;
37310
37409
  }
37311
- 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
+ });
37312
37414
  return this.lspClient;
37313
37415
  }
37314
37416
  async handleDAP(toolName, args) {
@@ -37488,7 +37590,7 @@ class GodotServer {
37488
37590
  try {
37489
37591
  const askedAt = Date.now();
37490
37592
  const asked = this.godotBridge.invokeTool("playing_status", {});
37491
- const answer = await Promise.race([asked, delay3(PLAYING_STATUS_MS).then(() => null)]);
37593
+ const answer = await Promise.race([asked, delay4(PLAYING_STATUS_MS).then(() => null)]);
37492
37594
  if (answer === null) {
37493
37595
  this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
37494
37596
  asked.then(() => {
@@ -37513,7 +37615,7 @@ class GodotServer {
37513
37615
  try {
37514
37616
  const answer = await Promise.race([
37515
37617
  this.godotBridge.invokeTool("scan_status", {}),
37516
- delay3(PLAYING_STATUS_MS).then(() => null)
37618
+ delay4(PLAYING_STATUS_MS).then(() => null)
37517
37619
  ]);
37518
37620
  if (answer === null) {
37519
37621
  return null;
@@ -37725,7 +37827,7 @@ class GodotServer {
37725
37827
  if (Date.now() >= deadline) {
37726
37828
  return false;
37727
37829
  }
37728
- await delay3(250);
37830
+ await delay4(250);
37729
37831
  }
37730
37832
  }
37731
37833
  async handleLaunchEditor(args) {
@@ -38235,7 +38337,7 @@ class GodotServer {
38235
38337
  if (Date.now() >= deadline) {
38236
38338
  return false;
38237
38339
  }
38238
- await delay3(100);
38340
+ await delay4(100);
38239
38341
  }
38240
38342
  }
38241
38343
  async endActiveGame(reason, going) {
@@ -38827,7 +38929,7 @@ class GodotServer {
38827
38929
  while (cancelled?.aborted !== true && await this.runStillGoing(run) && Date.now() < until) {
38828
38930
  this.drainEditorOutput(run);
38829
38931
  this.drainTranscript(run);
38830
- await delay3(RUN_POLL_MS);
38932
+ await delay4(RUN_POLL_MS);
38831
38933
  }
38832
38934
  return await this.handleGetDebugOutput(args, await this.runStillGoing(run) ? budgetMs : undefined);
38833
38935
  }
@@ -38864,7 +38966,7 @@ class GodotServer {
38864
38966
  if (Date.now() >= until) {
38865
38967
  return false;
38866
38968
  }
38867
- await delay3(50);
38969
+ await delay4(50);
38868
38970
  }
38869
38971
  }
38870
38972
  async handleEditorConsole(args) {
@@ -39374,10 +39476,13 @@ class GodotServer {
39374
39476
  waitedMs: Date.now() - started,
39375
39477
  waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
39376
39478
  reloadedAs: reloaded.as,
39377
- heldBeforeReload: reloaded.heldBefore,
39378
- reloadedMethods: reloaded.methods,
39379
- heldConstantsBeforeReload: reloaded.heldConstantsBefore,
39380
- reloadedConstants: reloaded.constants,
39479
+ ...reloadDifference(reloaded),
39480
+ ...args["listMembers"] === true ? {
39481
+ heldBeforeReload: reloaded.heldBefore,
39482
+ reloadedMethods: reloaded.methods,
39483
+ heldConstantsBeforeReload: reloaded.heldConstantsBefore,
39484
+ reloadedConstants: reloaded.constants
39485
+ } : {},
39381
39486
  reloadProblem: reloaded.problem,
39382
39487
  unseenByEditor: unseen.length > 0 ? unseen : undefined,
39383
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.53",
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",