gdharness 1.1.50 → 1.1.52
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 +218 -44
- package/build/index.js +216 -43
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -8544,7 +8544,8 @@ function sameCodeNote(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
|
8544
8544
|
if (addonVersion === undefined || addonVersion === serverVersion || addonDigest === undefined || addonDigest === "" || addonDigest !== shippedDigest) {
|
|
8545
8545
|
return;
|
|
8546
8546
|
}
|
|
8547
|
-
|
|
8547
|
+
const same = `The editor loaded the ${addonVersion} addons, which are the same code this ${serverVersion} server ships, so nothing needs restarting`;
|
|
8548
|
+
return isNewer(addonVersion, serverVersion) ? `${same}. This server is the older of the two, which a reconnect in your harness settles once the project pins ${addonVersion}.` : `${same}; addonVersion is the version the editor loaded when it started.`;
|
|
8548
8549
|
}
|
|
8549
8550
|
function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
|
|
8550
8551
|
if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
|
|
@@ -17826,8 +17827,16 @@ var init_tool_definitions = __esm(() => {
|
|
|
17826
17827
|
type: "boolean",
|
|
17827
17828
|
description: "structure: also list what the script inherits, walking extends through the project's other scripts. Each such member carries inherited_from, the file declaring it, and the answer gains inherits_from, the chain that was walked. A name the script overrides is listed at both its lines rather than once. A native base is not walked, since it declares nothing in a file: editor_classes info answers for those. Default false."
|
|
17828
17829
|
},
|
|
17829
|
-
line: {
|
|
17830
|
-
|
|
17830
|
+
line: {
|
|
17831
|
+
type: "integer",
|
|
17832
|
+
minimum: 1,
|
|
17833
|
+
description: "completion, hover: the line, counted from 1 as the editor shows it."
|
|
17834
|
+
},
|
|
17835
|
+
character: {
|
|
17836
|
+
type: "integer",
|
|
17837
|
+
minimum: 1,
|
|
17838
|
+
description: "completion, hover: the column, counted from 1 as the editor shows it, with a tab counted as one. symbols gives each name at the line and column hover is asked at."
|
|
17839
|
+
}
|
|
17831
17840
|
},
|
|
17832
17841
|
requires: ["projectPath", "scriptPath"],
|
|
17833
17842
|
operations: {
|
|
@@ -17835,7 +17844,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
17835
17844
|
summary: "functions, variables, signals, class_name and extends, read from the file",
|
|
17836
17845
|
requires: []
|
|
17837
17846
|
},
|
|
17838
|
-
symbols: {
|
|
17847
|
+
symbols: {
|
|
17848
|
+
summary: "document symbols from the language server, each with its kind, the line and column of its name, and the lastLine of its declaration",
|
|
17849
|
+
requires: []
|
|
17850
|
+
},
|
|
17839
17851
|
completion: { summary: "completions at a position", requires: ["line", "character"] },
|
|
17840
17852
|
hover: { summary: "hover text at a position", requires: ["line", "character"] }
|
|
17841
17853
|
},
|
|
@@ -17843,13 +17855,13 @@ var init_tool_definitions = __esm(() => {
|
|
|
17843
17855
|
},
|
|
17844
17856
|
{
|
|
17845
17857
|
name: "script_diagnostics",
|
|
17846
|
-
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. 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.",
|
|
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.",
|
|
17847
17859
|
parameters: {
|
|
17848
17860
|
projectPath: PROJECT_PATH,
|
|
17849
17861
|
scriptPath: {
|
|
17850
17862
|
type: ["string", "array"],
|
|
17851
17863
|
items: { type: "string" },
|
|
17852
|
-
description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered as it always was
|
|
17864
|
+
description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered as it always was. Several are answered together, with clean, errors and warnings for all of them: a script with nothing to report is named in nothingReported, and every other is answered under scripts in that same shape, including a script that could not be read, named with why while the rest are still asked about. Any number of scripts is one call, which reports progress as it goes.'
|
|
17853
17865
|
}
|
|
17854
17866
|
},
|
|
17855
17867
|
requires: ["projectPath", "scriptPath"]
|
|
@@ -17897,7 +17909,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17897
17909
|
},
|
|
17898
17910
|
{
|
|
17899
17911
|
name: "editor_launch",
|
|
17900
|
-
description: "Opens the Godot editor on a project, or restarts the one already connected. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
17912
|
+
description: "Opens the Godot editor on a project, or restarts the one already connected. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
17901
17913
|
parameters: {
|
|
17902
17914
|
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
17903
17915
|
hidden: {
|
|
@@ -18052,7 +18064,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18052
18064
|
},
|
|
18053
18065
|
{
|
|
18054
18066
|
name: "editor_status",
|
|
18055
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
18067
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
18056
18068
|
parameters: {},
|
|
18057
18069
|
requires: []
|
|
18058
18070
|
},
|
|
@@ -39112,6 +39124,10 @@ import { readFile, realpath } from "node:fs/promises";
|
|
|
39112
39124
|
import { createConnection as createConnection3 } from "node:net";
|
|
39113
39125
|
import { dirname as dirname14, join as join20, resolve as resolve5 } from "node:path";
|
|
39114
39126
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
39127
|
+
function initializeTimeoutMs() {
|
|
39128
|
+
const override = Number.parseInt(envValue("GDHARNESS_LSP_INITIALIZE_TIMEOUT_MS") ?? "", 10);
|
|
39129
|
+
return Number.isInteger(override) && override > 0 ? override : 180000;
|
|
39130
|
+
}
|
|
39115
39131
|
function diagnosticsKey(uri) {
|
|
39116
39132
|
let decoded;
|
|
39117
39133
|
try {
|
|
@@ -39145,7 +39161,11 @@ class GodotLSPClient {
|
|
|
39145
39161
|
documentVersions = new Map;
|
|
39146
39162
|
documentTurns = new Map;
|
|
39147
39163
|
timeouts;
|
|
39148
|
-
constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1", timeouts = {
|
|
39164
|
+
constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1", timeouts = {
|
|
39165
|
+
requestMs: REQUEST_TIMEOUT_MS2,
|
|
39166
|
+
referencesMs: REFERENCES_TIMEOUT_MS,
|
|
39167
|
+
initializeMs: initializeTimeoutMs()
|
|
39168
|
+
}) {
|
|
39149
39169
|
this.port = port;
|
|
39150
39170
|
this.host = host;
|
|
39151
39171
|
this.timeouts = timeouts;
|
|
@@ -39455,7 +39475,7 @@ class GodotLSPClient {
|
|
|
39455
39475
|
name: resolvedRootPath
|
|
39456
39476
|
}
|
|
39457
39477
|
]
|
|
39458
|
-
});
|
|
39478
|
+
}, this.timeouts.initializeMs);
|
|
39459
39479
|
const served = this.servedWorkspace;
|
|
39460
39480
|
if (served !== null && !isSameDirectory(served, resolvedRootPath)) {
|
|
39461
39481
|
throw new AnotherProjectsServer(this.port, served, resolvedRootPath);
|
|
@@ -39641,10 +39661,78 @@ function normalizeLSPError(error, port) {
|
|
|
39641
39661
|
if (error.message.includes("ECONNREFUSED") || error.message.includes("Failed to connect to Godot LSP") || error.message.includes("socket closed")) {
|
|
39642
39662
|
return `Godot LSP is unavailable on port ${port}. Start the Godot editor and enable Language Server in Editor Settings, or set GDHARNESS_LSP_PORT to the port it serves.`;
|
|
39643
39663
|
}
|
|
39664
|
+
if (error instanceof LSPTimeout && error.method === "initialize") {
|
|
39665
|
+
return `The language server on port ${port} took the connection and did not answer initialize within ${String(error.seconds)}s. Godot reads every script in the project before it answers the first initialize after its editor starts, and goes on reading after this call stops waiting, so call again once the editor has settled.`;
|
|
39666
|
+
}
|
|
39644
39667
|
return error.message;
|
|
39645
39668
|
}
|
|
39646
39669
|
return String(error);
|
|
39647
39670
|
}
|
|
39671
|
+
function asRecord(value) {
|
|
39672
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
39673
|
+
}
|
|
39674
|
+
function named(value, names) {
|
|
39675
|
+
return typeof value === "number" && Number.isInteger(value) ? names[value - 1] ?? value : value;
|
|
39676
|
+
}
|
|
39677
|
+
function shownPosition(position) {
|
|
39678
|
+
const { line, character } = asRecord(position);
|
|
39679
|
+
return typeof line === "number" && typeof character === "number" ? { line: line + 1, column: character + 1 } : null;
|
|
39680
|
+
}
|
|
39681
|
+
function shownRange(range) {
|
|
39682
|
+
const start = shownPosition(asRecord(range)["start"]);
|
|
39683
|
+
const end = shownPosition(asRecord(range)["end"]);
|
|
39684
|
+
return {
|
|
39685
|
+
...start === null ? {} : { line: start.line, column: start.column },
|
|
39686
|
+
...end === null ? {} : { endLine: end.line, endColumn: end.column }
|
|
39687
|
+
};
|
|
39688
|
+
}
|
|
39689
|
+
function shownDiagnostic(entry) {
|
|
39690
|
+
const { range, severity, message, code } = asRecord(entry);
|
|
39691
|
+
return {
|
|
39692
|
+
...shownRange(range),
|
|
39693
|
+
severity: named(severity, SEVERITIES),
|
|
39694
|
+
message,
|
|
39695
|
+
...code === undefined || code === null || code === 0 || code === "" ? {} : { code }
|
|
39696
|
+
};
|
|
39697
|
+
}
|
|
39698
|
+
function shownSymbol(entry) {
|
|
39699
|
+
const symbol = asRecord(entry);
|
|
39700
|
+
const whole = asRecord(symbol["range"] ?? asRecord(symbol["location"])["range"]);
|
|
39701
|
+
const at = shownPosition(asRecord(symbol["selectionRange"] ?? whole)["start"]);
|
|
39702
|
+
const last = shownPosition(whole["end"]);
|
|
39703
|
+
const children = Array.isArray(symbol["children"]) ? symbol["children"] : [];
|
|
39704
|
+
const { documentation, native_class: nativeClass, deprecated, detail } = symbol;
|
|
39705
|
+
return {
|
|
39706
|
+
name: symbol["name"],
|
|
39707
|
+
kind: named(symbol["kind"], SYMBOL_KINDS),
|
|
39708
|
+
...typeof detail === "string" && detail !== "" ? { detail } : {},
|
|
39709
|
+
...at === null ? {} : { line: at.line, column: at.column },
|
|
39710
|
+
...last === null ? {} : { lastLine: last.line },
|
|
39711
|
+
...typeof documentation === "string" && documentation !== "" ? { documentation } : {},
|
|
39712
|
+
...typeof nativeClass === "string" && nativeClass !== "" ? { nativeClass } : {},
|
|
39713
|
+
...deprecated === true ? { deprecated } : {},
|
|
39714
|
+
...children.length > 0 ? { children: children.map(shownSymbol) } : {}
|
|
39715
|
+
};
|
|
39716
|
+
}
|
|
39717
|
+
function shownCompletion(entry) {
|
|
39718
|
+
const { data: _request, kind, ...rest } = asRecord(entry);
|
|
39719
|
+
return { ...rest, kind: named(kind, COMPLETION_KINDS) };
|
|
39720
|
+
}
|
|
39721
|
+
function shownHover(hover) {
|
|
39722
|
+
if (hover === null || typeof hover !== "object") {
|
|
39723
|
+
return hover;
|
|
39724
|
+
}
|
|
39725
|
+
const { range, ...rest } = asRecord(hover);
|
|
39726
|
+
return { ...rest, ...shownRange(range) };
|
|
39727
|
+
}
|
|
39728
|
+
function protocolPosition(args) {
|
|
39729
|
+
const line = args["line"];
|
|
39730
|
+
const character = args["character"];
|
|
39731
|
+
if (typeof line !== "number" || typeof character !== "number" || !Number.isInteger(line) || !Number.isInteger(character) || line < 1 || character < 1) {
|
|
39732
|
+
throw new ArgumentRefusal(`line and character are counted from 1, as the editor shows them; got line ${String(line)} and character ${String(character)}.`);
|
|
39733
|
+
}
|
|
39734
|
+
return { line: line - 1, character: character - 1 };
|
|
39735
|
+
}
|
|
39648
39736
|
async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
39649
39737
|
const requestedProjectPath = resolve5(projectPathValue);
|
|
39650
39738
|
let projectPath;
|
|
@@ -39691,29 +39779,21 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
39691
39779
|
switch (toolName) {
|
|
39692
39780
|
case "lsp_get_diagnostics": {
|
|
39693
39781
|
const diagnostics = await client.getDiagnostics(scriptPath, content);
|
|
39694
|
-
return asToolResponse({ diagnostics });
|
|
39782
|
+
return asToolResponse({ diagnostics: diagnostics.map(shownDiagnostic) });
|
|
39695
39783
|
}
|
|
39696
39784
|
case "lsp_get_completions": {
|
|
39697
|
-
const line =
|
|
39698
|
-
const character = Number(parsedArgs["character"]);
|
|
39699
|
-
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
39700
|
-
throw new ArgumentRefusal("Arguments line and character must be numbers.");
|
|
39701
|
-
}
|
|
39785
|
+
const { line, character } = protocolPosition(parsedArgs);
|
|
39702
39786
|
const completions = await client.getCompletions(scriptPath, content, line, character);
|
|
39703
|
-
return asToolResponse({ completions });
|
|
39787
|
+
return asToolResponse({ completions: completions.map(shownCompletion) });
|
|
39704
39788
|
}
|
|
39705
39789
|
case "lsp_get_hover": {
|
|
39706
|
-
const line =
|
|
39707
|
-
const character = Number(parsedArgs["character"]);
|
|
39708
|
-
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
39709
|
-
throw new ArgumentRefusal("Arguments line and character must be numbers.");
|
|
39710
|
-
}
|
|
39790
|
+
const { line, character } = protocolPosition(parsedArgs);
|
|
39711
39791
|
const hover = await client.getHover(scriptPath, content, line, character);
|
|
39712
|
-
return asToolResponse({ hover });
|
|
39792
|
+
return asToolResponse({ hover: shownHover(hover) });
|
|
39713
39793
|
}
|
|
39714
39794
|
case "lsp_get_symbols": {
|
|
39715
39795
|
const symbols = await client.getDocumentSymbols(scriptPath, content);
|
|
39716
|
-
return asToolResponse({ symbols });
|
|
39796
|
+
return asToolResponse({ symbols: symbols.map(shownSymbol) });
|
|
39717
39797
|
}
|
|
39718
39798
|
default:
|
|
39719
39799
|
return asToolResponse({ error: `Unknown LSP tool: ${toolName}` });
|
|
@@ -39723,11 +39803,12 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
39723
39803
|
error: normalizeLSPError(error, client.port),
|
|
39724
39804
|
...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
|
|
39725
39805
|
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {},
|
|
39726
|
-
...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {}
|
|
39806
|
+
...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {},
|
|
39807
|
+
...error instanceof LSPTimeout && error.method === "initialize" ? { stillReading: true } : {}
|
|
39727
39808
|
});
|
|
39728
39809
|
}
|
|
39729
39810
|
}
|
|
39730
|
-
var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, REQUEST_TIMEOUT_MS2 = 1e4, REFERENCES_TIMEOUT_MS = 180000, LSPTimeout, ArgumentRefusal, NoDiagnosticsPublished, AnotherProjectsServer;
|
|
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;
|
|
39731
39812
|
var init_lsp_client = __esm(() => {
|
|
39732
39813
|
init_answer_json();
|
|
39733
39814
|
init_errors();
|
|
@@ -39754,6 +39835,62 @@ var init_lsp_client = __esm(() => {
|
|
|
39754
39835
|
this.serves = serves;
|
|
39755
39836
|
}
|
|
39756
39837
|
};
|
|
39838
|
+
SEVERITIES = ["error", "warning", "information", "hint"];
|
|
39839
|
+
SYMBOL_KINDS = [
|
|
39840
|
+
"file",
|
|
39841
|
+
"module",
|
|
39842
|
+
"namespace",
|
|
39843
|
+
"package",
|
|
39844
|
+
"class",
|
|
39845
|
+
"method",
|
|
39846
|
+
"property",
|
|
39847
|
+
"field",
|
|
39848
|
+
"constructor",
|
|
39849
|
+
"enum",
|
|
39850
|
+
"interface",
|
|
39851
|
+
"function",
|
|
39852
|
+
"variable",
|
|
39853
|
+
"constant",
|
|
39854
|
+
"string",
|
|
39855
|
+
"number",
|
|
39856
|
+
"boolean",
|
|
39857
|
+
"array",
|
|
39858
|
+
"object",
|
|
39859
|
+
"key",
|
|
39860
|
+
"null",
|
|
39861
|
+
"enum member",
|
|
39862
|
+
"struct",
|
|
39863
|
+
"event",
|
|
39864
|
+
"operator",
|
|
39865
|
+
"type parameter"
|
|
39866
|
+
];
|
|
39867
|
+
COMPLETION_KINDS = [
|
|
39868
|
+
"text",
|
|
39869
|
+
"method",
|
|
39870
|
+
"function",
|
|
39871
|
+
"constructor",
|
|
39872
|
+
"field",
|
|
39873
|
+
"variable",
|
|
39874
|
+
"class",
|
|
39875
|
+
"interface",
|
|
39876
|
+
"module",
|
|
39877
|
+
"property",
|
|
39878
|
+
"unit",
|
|
39879
|
+
"value",
|
|
39880
|
+
"enum",
|
|
39881
|
+
"keyword",
|
|
39882
|
+
"snippet",
|
|
39883
|
+
"color",
|
|
39884
|
+
"file",
|
|
39885
|
+
"reference",
|
|
39886
|
+
"folder",
|
|
39887
|
+
"enum member",
|
|
39888
|
+
"constant",
|
|
39889
|
+
"struct",
|
|
39890
|
+
"event",
|
|
39891
|
+
"operator",
|
|
39892
|
+
"type parameter"
|
|
39893
|
+
];
|
|
39757
39894
|
});
|
|
39758
39895
|
|
|
39759
39896
|
// src/outside.ts
|
|
@@ -42091,6 +42228,16 @@ function endedPreviousRun(ended) {
|
|
|
42091
42228
|
function previousRunLeft(ended) {
|
|
42092
42229
|
return (ended?.ending === "refused" || ended?.ending === "lingering") && ended.pid !== null ? ended.pid : undefined;
|
|
42093
42230
|
}
|
|
42231
|
+
function editorsOfProjectIn(tree, projectPath) {
|
|
42232
|
+
const found = [];
|
|
42233
|
+
for (const [pid, listed] of tree) {
|
|
42234
|
+
const read = readCommandLine(listed.command);
|
|
42235
|
+
if (read.editor && read.oneShot === null && read.projectPath !== null && /godot/i.test(read.executable) && !CONSOLE_WRAPPER.test(read.executable) && isSameDirectory(read.projectPath, projectPath)) {
|
|
42236
|
+
found.push(pid);
|
|
42237
|
+
}
|
|
42238
|
+
}
|
|
42239
|
+
return found.sort((a, b) => a - b);
|
|
42240
|
+
}
|
|
42094
42241
|
function engineNamesOf(executable) {
|
|
42095
42242
|
return CONSOLE_WRAPPER.test(executable) ? [executable, executable.replace(CONSOLE_WRAPPER, "$1")] : [executable];
|
|
42096
42243
|
}
|
|
@@ -42618,12 +42765,6 @@ function scriptsToDiagnose(projectPath, given) {
|
|
|
42618
42765
|
if (unique.length === 0) {
|
|
42619
42766
|
return { ok: false, reason: `No .gd script is under ${empty.join(" or ")}.` };
|
|
42620
42767
|
}
|
|
42621
|
-
if (unique.length > MOST_DIAGNOSED) {
|
|
42622
|
-
return {
|
|
42623
|
-
ok: false,
|
|
42624
|
-
reason: `scriptPath names ${unique.length} scripts, and one call asks about at most ${MOST_DIAGNOSED}, each a round trip through the language server: name narrower folders, or call again for the rest.`
|
|
42625
|
-
};
|
|
42626
|
-
}
|
|
42627
42768
|
return { ok: true, value: unique };
|
|
42628
42769
|
}
|
|
42629
42770
|
function isDirectory(path) {
|
|
@@ -44489,11 +44630,16 @@ class GodotServer {
|
|
|
44489
44630
|
return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
|
|
44490
44631
|
}
|
|
44491
44632
|
const progress = callProgress();
|
|
44492
|
-
const
|
|
44633
|
+
const cancelled = callSignal();
|
|
44634
|
+
const reported = [];
|
|
44635
|
+
const nothingReported = [];
|
|
44493
44636
|
let errors = 0;
|
|
44494
44637
|
let warnings = 0;
|
|
44495
44638
|
let unread = 0;
|
|
44496
44639
|
for (const [at, scriptPath] of scripts.value.entries()) {
|
|
44640
|
+
if (cancelled?.aborted === true) {
|
|
44641
|
+
return this.createErrorResponse(`Cancelled after ${at} of ${scripts.value.length} scripts.`);
|
|
44642
|
+
}
|
|
44497
44643
|
progress?.(at + 1, `${at + 1} of ${scripts.value.length}: ${scriptPath}`);
|
|
44498
44644
|
const one = await this.diagnosticsOf({ ...args, scriptPath });
|
|
44499
44645
|
if ("refusal" in one) {
|
|
@@ -44501,20 +44647,25 @@ class GodotServer {
|
|
|
44501
44647
|
return one.refusal;
|
|
44502
44648
|
}
|
|
44503
44649
|
unread += 1;
|
|
44504
|
-
|
|
44650
|
+
reported.push({ scriptPath, error: one.refusal.content[0]?.text ?? "not read" });
|
|
44505
44651
|
continue;
|
|
44506
44652
|
}
|
|
44507
44653
|
errors += Number(one.answer["errors"]);
|
|
44508
44654
|
warnings += Number(one.answer["warnings"]);
|
|
44509
|
-
|
|
44655
|
+
if (readArray(one.answer, "diagnostics")?.length === 0) {
|
|
44656
|
+
nothingReported.push(scriptPath);
|
|
44657
|
+
} else {
|
|
44658
|
+
reported.push(one.answer);
|
|
44659
|
+
}
|
|
44510
44660
|
}
|
|
44511
44661
|
return this.jsonTextResponse(this.markedIfStale({
|
|
44512
44662
|
clean: errors === 0 && unread === 0,
|
|
44513
|
-
scriptsChecked:
|
|
44663
|
+
scriptsChecked: scripts.value.length,
|
|
44514
44664
|
errors,
|
|
44515
44665
|
warnings,
|
|
44516
44666
|
...unread > 0 ? { scriptsNotRead: unread } : {},
|
|
44517
|
-
scripts:
|
|
44667
|
+
scripts: reported,
|
|
44668
|
+
nothingReported
|
|
44518
44669
|
}));
|
|
44519
44670
|
}
|
|
44520
44671
|
markedIfStale(answer) {
|
|
@@ -44528,6 +44679,9 @@ class GodotServer {
|
|
|
44528
44679
|
if (payload["refusedArguments"] === true) {
|
|
44529
44680
|
return { refusal: this.createErrorResponse(reason), perScript: true };
|
|
44530
44681
|
}
|
|
44682
|
+
if (payload["stillReading"] === true) {
|
|
44683
|
+
return { refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`), perScript: false };
|
|
44684
|
+
}
|
|
44531
44685
|
if (typeof payload["serves"] === "string") {
|
|
44532
44686
|
return {
|
|
44533
44687
|
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
@@ -44547,10 +44701,7 @@ class GodotServer {
|
|
|
44547
44701
|
};
|
|
44548
44702
|
}
|
|
44549
44703
|
const diagnostics = readArray(payload, "diagnostics") ?? [];
|
|
44550
|
-
const errors = diagnostics.filter((entry) =>
|
|
44551
|
-
const severity = asParams(entry)["severity"];
|
|
44552
|
-
return severity === 1 || severity === "error" || severity === "ERROR";
|
|
44553
|
-
}).length;
|
|
44704
|
+
const errors = diagnostics.filter((entry) => asParams(entry)["severity"] === "error").length;
|
|
44554
44705
|
return {
|
|
44555
44706
|
answer: {
|
|
44556
44707
|
scriptPath: readString(args, "scriptPath"),
|
|
@@ -44778,14 +44929,16 @@ class GodotServer {
|
|
|
44778
44929
|
const greeted = hasSaidWhoItIs(status);
|
|
44779
44930
|
const stale = greeted && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
|
|
44780
44931
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
44932
|
+
const arriving = status.connected || this.ownProject === null ? [] : await this.editorsNotYetConnected(this.ownProject) ?? [];
|
|
44781
44933
|
return {
|
|
44782
44934
|
...status,
|
|
44783
44935
|
serverVersion: SERVER_VERSION,
|
|
44784
44936
|
addonIsStale: greeted ? stale : undefined,
|
|
44785
44937
|
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,
|
|
44786
44938
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
44787
|
-
mayYetConnect: status.connected ? undefined : unfinished !== null ? unfinished.underway : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
44788
|
-
|
|
44939
|
+
mayYetConnect: status.connected ? undefined : arriving.length > 0 ? true : unfinished !== null ? unfinished.underway : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
44940
|
+
editorsNotYetConnected: arriving.length > 0 ? arriving : undefined,
|
|
44941
|
+
restartInterrupted: unfinished === null || unfinished.underway || arriving.length > 0 ? undefined : {
|
|
44789
44942
|
quitEditorPid: unfinished.note.editorPid,
|
|
44790
44943
|
quitAt: unfinished.note.quitAt,
|
|
44791
44944
|
byServerPid: unfinished.note.byPid,
|
|
@@ -45082,6 +45235,14 @@ class GodotServer {
|
|
|
45082
45235
|
if (refused !== null) {
|
|
45083
45236
|
return refused;
|
|
45084
45237
|
}
|
|
45238
|
+
const arriving = await this.editorsNotYetConnected(project.value.path);
|
|
45239
|
+
if (arriving !== undefined && arriving.length > 0) {
|
|
45240
|
+
const named = arriving.map((one) => `pid ${one.pid}${one.startedAt === null ? "" : `, started ${one.startedAt}`}`).join("; ");
|
|
45241
|
+
return this.createErrorResponse(`An editor of this project is already running (${named}) and has not reached this server yet: an editor dials in once it is up, later while it imports, so opening another would put two editors on one project and one language server port.`, [
|
|
45242
|
+
"editor_status names it under editorsNotYetConnected and says when it has connected",
|
|
45243
|
+
`If it never connects it is not reaching this server: end ${arriving.length === 1 ? "that pid" : "those pids"} and open again`
|
|
45244
|
+
]);
|
|
45245
|
+
}
|
|
45085
45246
|
const engine = await this.engine();
|
|
45086
45247
|
if (!engine.ok) {
|
|
45087
45248
|
return engine.response;
|
|
@@ -45106,6 +45267,19 @@ class GodotServer {
|
|
|
45106
45267
|
settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
|
|
45107
45268
|
});
|
|
45108
45269
|
}
|
|
45270
|
+
async editorsNotYetConnected(projectPath) {
|
|
45271
|
+
const tree = await processTree();
|
|
45272
|
+
if (tree === undefined) {
|
|
45273
|
+
return;
|
|
45274
|
+
}
|
|
45275
|
+
const connected = this.godotBridge.isConnected() ? this.godotBridge.getStatus().editorPid : undefined;
|
|
45276
|
+
const pids = editorsOfProjectIn(tree, projectPath).filter((pid) => pid !== connected);
|
|
45277
|
+
const started = await startTimesOf(pids);
|
|
45278
|
+
return pids.map((pid) => {
|
|
45279
|
+
const at = started.get(pid) ?? tree.get(pid)?.startedAt;
|
|
45280
|
+
return { pid, startedAt: at === undefined ? null : new Date(at).toISOString() };
|
|
45281
|
+
});
|
|
45282
|
+
}
|
|
45109
45283
|
anEditorThatCannotConnect(projectPath) {
|
|
45110
45284
|
if (this.ownProject !== null && !isSameDirectory(this.ownProject, projectPath)) {
|
|
45111
45285
|
return this.createErrorResponse(`This server serves ${this.ownProject}, and an editor of ${projectPath} would dial that project's own server rather than this one.`, [
|
|
@@ -47370,7 +47544,7 @@ async function runGodotServer() {
|
|
|
47370
47544
|
const server = new GodotServer;
|
|
47371
47545
|
await server.run();
|
|
47372
47546
|
}
|
|
47373
|
-
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/"
|
|
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/";
|
|
47374
47548
|
var init_server3 = __esm(() => {
|
|
47375
47549
|
init_mcp();
|
|
47376
47550
|
init_stdio2();
|
package/build/index.js
CHANGED
|
@@ -10069,7 +10069,8 @@ function sameCodeNote(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
|
10069
10069
|
if (addonVersion === undefined || addonVersion === serverVersion || addonDigest === undefined || addonDigest === "" || addonDigest !== shippedDigest) {
|
|
10070
10070
|
return;
|
|
10071
10071
|
}
|
|
10072
|
-
|
|
10072
|
+
const same = `The editor loaded the ${addonVersion} addons, which are the same code this ${serverVersion} server ships, so nothing needs restarting`;
|
|
10073
|
+
return isNewer(addonVersion, serverVersion) ? `${same}. This server is the older of the two, which a reconnect in your harness settles once the project pins ${addonVersion}.` : `${same}; addonVersion is the version the editor loaded when it started.`;
|
|
10073
10074
|
}
|
|
10074
10075
|
function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
|
|
10075
10076
|
if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
|
|
@@ -29715,6 +29716,10 @@ var DEFAULT_LSP_PORT = 6005;
|
|
|
29715
29716
|
var DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
29716
29717
|
var REQUEST_TIMEOUT_MS2 = 1e4;
|
|
29717
29718
|
var REFERENCES_TIMEOUT_MS = 180000;
|
|
29719
|
+
function initializeTimeoutMs() {
|
|
29720
|
+
const override = Number.parseInt(envValue("GDHARNESS_LSP_INITIALIZE_TIMEOUT_MS") ?? "", 10);
|
|
29721
|
+
return Number.isInteger(override) && override > 0 ? override : 180000;
|
|
29722
|
+
}
|
|
29718
29723
|
|
|
29719
29724
|
class LSPTimeout extends Error {
|
|
29720
29725
|
method;
|
|
@@ -29772,7 +29777,11 @@ class GodotLSPClient {
|
|
|
29772
29777
|
documentVersions = new Map;
|
|
29773
29778
|
documentTurns = new Map;
|
|
29774
29779
|
timeouts;
|
|
29775
|
-
constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1", timeouts = {
|
|
29780
|
+
constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1", timeouts = {
|
|
29781
|
+
requestMs: REQUEST_TIMEOUT_MS2,
|
|
29782
|
+
referencesMs: REFERENCES_TIMEOUT_MS,
|
|
29783
|
+
initializeMs: initializeTimeoutMs()
|
|
29784
|
+
}) {
|
|
29776
29785
|
this.port = port;
|
|
29777
29786
|
this.host = host;
|
|
29778
29787
|
this.timeouts = timeouts;
|
|
@@ -30082,7 +30091,7 @@ class GodotLSPClient {
|
|
|
30082
30091
|
name: resolvedRootPath
|
|
30083
30092
|
}
|
|
30084
30093
|
]
|
|
30085
|
-
});
|
|
30094
|
+
}, this.timeouts.initializeMs);
|
|
30086
30095
|
const served = this.servedWorkspace;
|
|
30087
30096
|
if (served !== null && !isSameDirectory(served, resolvedRootPath)) {
|
|
30088
30097
|
throw new AnotherProjectsServer(this.port, served, resolvedRootPath);
|
|
@@ -30268,10 +30277,134 @@ function normalizeLSPError(error, port) {
|
|
|
30268
30277
|
if (error.message.includes("ECONNREFUSED") || error.message.includes("Failed to connect to Godot LSP") || error.message.includes("socket closed")) {
|
|
30269
30278
|
return `Godot LSP is unavailable on port ${port}. Start the Godot editor and enable Language Server in Editor Settings, or set GDHARNESS_LSP_PORT to the port it serves.`;
|
|
30270
30279
|
}
|
|
30280
|
+
if (error instanceof LSPTimeout && error.method === "initialize") {
|
|
30281
|
+
return `The language server on port ${port} took the connection and did not answer initialize within ${String(error.seconds)}s. Godot reads every script in the project before it answers the first initialize after its editor starts, and goes on reading after this call stops waiting, so call again once the editor has settled.`;
|
|
30282
|
+
}
|
|
30271
30283
|
return error.message;
|
|
30272
30284
|
}
|
|
30273
30285
|
return String(error);
|
|
30274
30286
|
}
|
|
30287
|
+
var SEVERITIES = ["error", "warning", "information", "hint"];
|
|
30288
|
+
var SYMBOL_KINDS = [
|
|
30289
|
+
"file",
|
|
30290
|
+
"module",
|
|
30291
|
+
"namespace",
|
|
30292
|
+
"package",
|
|
30293
|
+
"class",
|
|
30294
|
+
"method",
|
|
30295
|
+
"property",
|
|
30296
|
+
"field",
|
|
30297
|
+
"constructor",
|
|
30298
|
+
"enum",
|
|
30299
|
+
"interface",
|
|
30300
|
+
"function",
|
|
30301
|
+
"variable",
|
|
30302
|
+
"constant",
|
|
30303
|
+
"string",
|
|
30304
|
+
"number",
|
|
30305
|
+
"boolean",
|
|
30306
|
+
"array",
|
|
30307
|
+
"object",
|
|
30308
|
+
"key",
|
|
30309
|
+
"null",
|
|
30310
|
+
"enum member",
|
|
30311
|
+
"struct",
|
|
30312
|
+
"event",
|
|
30313
|
+
"operator",
|
|
30314
|
+
"type parameter"
|
|
30315
|
+
];
|
|
30316
|
+
var COMPLETION_KINDS = [
|
|
30317
|
+
"text",
|
|
30318
|
+
"method",
|
|
30319
|
+
"function",
|
|
30320
|
+
"constructor",
|
|
30321
|
+
"field",
|
|
30322
|
+
"variable",
|
|
30323
|
+
"class",
|
|
30324
|
+
"interface",
|
|
30325
|
+
"module",
|
|
30326
|
+
"property",
|
|
30327
|
+
"unit",
|
|
30328
|
+
"value",
|
|
30329
|
+
"enum",
|
|
30330
|
+
"keyword",
|
|
30331
|
+
"snippet",
|
|
30332
|
+
"color",
|
|
30333
|
+
"file",
|
|
30334
|
+
"reference",
|
|
30335
|
+
"folder",
|
|
30336
|
+
"enum member",
|
|
30337
|
+
"constant",
|
|
30338
|
+
"struct",
|
|
30339
|
+
"event",
|
|
30340
|
+
"operator",
|
|
30341
|
+
"type parameter"
|
|
30342
|
+
];
|
|
30343
|
+
function asRecord(value) {
|
|
30344
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
30345
|
+
}
|
|
30346
|
+
function named(value, names) {
|
|
30347
|
+
return typeof value === "number" && Number.isInteger(value) ? names[value - 1] ?? value : value;
|
|
30348
|
+
}
|
|
30349
|
+
function shownPosition(position) {
|
|
30350
|
+
const { line, character } = asRecord(position);
|
|
30351
|
+
return typeof line === "number" && typeof character === "number" ? { line: line + 1, column: character + 1 } : null;
|
|
30352
|
+
}
|
|
30353
|
+
function shownRange(range) {
|
|
30354
|
+
const start = shownPosition(asRecord(range)["start"]);
|
|
30355
|
+
const end = shownPosition(asRecord(range)["end"]);
|
|
30356
|
+
return {
|
|
30357
|
+
...start === null ? {} : { line: start.line, column: start.column },
|
|
30358
|
+
...end === null ? {} : { endLine: end.line, endColumn: end.column }
|
|
30359
|
+
};
|
|
30360
|
+
}
|
|
30361
|
+
function shownDiagnostic(entry) {
|
|
30362
|
+
const { range, severity, message, code } = asRecord(entry);
|
|
30363
|
+
return {
|
|
30364
|
+
...shownRange(range),
|
|
30365
|
+
severity: named(severity, SEVERITIES),
|
|
30366
|
+
message,
|
|
30367
|
+
...code === undefined || code === null || code === 0 || code === "" ? {} : { code }
|
|
30368
|
+
};
|
|
30369
|
+
}
|
|
30370
|
+
function shownSymbol(entry) {
|
|
30371
|
+
const symbol = asRecord(entry);
|
|
30372
|
+
const whole = asRecord(symbol["range"] ?? asRecord(symbol["location"])["range"]);
|
|
30373
|
+
const at = shownPosition(asRecord(symbol["selectionRange"] ?? whole)["start"]);
|
|
30374
|
+
const last = shownPosition(whole["end"]);
|
|
30375
|
+
const children = Array.isArray(symbol["children"]) ? symbol["children"] : [];
|
|
30376
|
+
const { documentation, native_class: nativeClass, deprecated, detail } = symbol;
|
|
30377
|
+
return {
|
|
30378
|
+
name: symbol["name"],
|
|
30379
|
+
kind: named(symbol["kind"], SYMBOL_KINDS),
|
|
30380
|
+
...typeof detail === "string" && detail !== "" ? { detail } : {},
|
|
30381
|
+
...at === null ? {} : { line: at.line, column: at.column },
|
|
30382
|
+
...last === null ? {} : { lastLine: last.line },
|
|
30383
|
+
...typeof documentation === "string" && documentation !== "" ? { documentation } : {},
|
|
30384
|
+
...typeof nativeClass === "string" && nativeClass !== "" ? { nativeClass } : {},
|
|
30385
|
+
...deprecated === true ? { deprecated } : {},
|
|
30386
|
+
...children.length > 0 ? { children: children.map(shownSymbol) } : {}
|
|
30387
|
+
};
|
|
30388
|
+
}
|
|
30389
|
+
function shownCompletion(entry) {
|
|
30390
|
+
const { data: _request, kind, ...rest } = asRecord(entry);
|
|
30391
|
+
return { ...rest, kind: named(kind, COMPLETION_KINDS) };
|
|
30392
|
+
}
|
|
30393
|
+
function shownHover(hover) {
|
|
30394
|
+
if (hover === null || typeof hover !== "object") {
|
|
30395
|
+
return hover;
|
|
30396
|
+
}
|
|
30397
|
+
const { range, ...rest } = asRecord(hover);
|
|
30398
|
+
return { ...rest, ...shownRange(range) };
|
|
30399
|
+
}
|
|
30400
|
+
function protocolPosition(args) {
|
|
30401
|
+
const line = args["line"];
|
|
30402
|
+
const character = args["character"];
|
|
30403
|
+
if (typeof line !== "number" || typeof character !== "number" || !Number.isInteger(line) || !Number.isInteger(character) || line < 1 || character < 1) {
|
|
30404
|
+
throw new ArgumentRefusal(`line and character are counted from 1, as the editor shows them; got line ${String(line)} and character ${String(character)}.`);
|
|
30405
|
+
}
|
|
30406
|
+
return { line: line - 1, character: character - 1 };
|
|
30407
|
+
}
|
|
30275
30408
|
async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
30276
30409
|
const requestedProjectPath = resolve3(projectPathValue);
|
|
30277
30410
|
let projectPath;
|
|
@@ -30318,29 +30451,21 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
30318
30451
|
switch (toolName) {
|
|
30319
30452
|
case "lsp_get_diagnostics": {
|
|
30320
30453
|
const diagnostics = await client.getDiagnostics(scriptPath, content);
|
|
30321
|
-
return asToolResponse({ diagnostics });
|
|
30454
|
+
return asToolResponse({ diagnostics: diagnostics.map(shownDiagnostic) });
|
|
30322
30455
|
}
|
|
30323
30456
|
case "lsp_get_completions": {
|
|
30324
|
-
const line =
|
|
30325
|
-
const character = Number(parsedArgs["character"]);
|
|
30326
|
-
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
30327
|
-
throw new ArgumentRefusal("Arguments line and character must be numbers.");
|
|
30328
|
-
}
|
|
30457
|
+
const { line, character } = protocolPosition(parsedArgs);
|
|
30329
30458
|
const completions = await client.getCompletions(scriptPath, content, line, character);
|
|
30330
|
-
return asToolResponse({ completions });
|
|
30459
|
+
return asToolResponse({ completions: completions.map(shownCompletion) });
|
|
30331
30460
|
}
|
|
30332
30461
|
case "lsp_get_hover": {
|
|
30333
|
-
const line =
|
|
30334
|
-
const character = Number(parsedArgs["character"]);
|
|
30335
|
-
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
30336
|
-
throw new ArgumentRefusal("Arguments line and character must be numbers.");
|
|
30337
|
-
}
|
|
30462
|
+
const { line, character } = protocolPosition(parsedArgs);
|
|
30338
30463
|
const hover = await client.getHover(scriptPath, content, line, character);
|
|
30339
|
-
return asToolResponse({ hover });
|
|
30464
|
+
return asToolResponse({ hover: shownHover(hover) });
|
|
30340
30465
|
}
|
|
30341
30466
|
case "lsp_get_symbols": {
|
|
30342
30467
|
const symbols = await client.getDocumentSymbols(scriptPath, content);
|
|
30343
|
-
return asToolResponse({ symbols });
|
|
30468
|
+
return asToolResponse({ symbols: symbols.map(shownSymbol) });
|
|
30344
30469
|
}
|
|
30345
30470
|
default:
|
|
30346
30471
|
return asToolResponse({ error: `Unknown LSP tool: ${toolName}` });
|
|
@@ -30350,7 +30475,8 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
30350
30475
|
error: normalizeLSPError(error, client.port),
|
|
30351
30476
|
...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
|
|
30352
30477
|
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {},
|
|
30353
|
-
...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {}
|
|
30478
|
+
...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {},
|
|
30479
|
+
...error instanceof LSPTimeout && error.method === "initialize" ? { stillReading: true } : {}
|
|
30354
30480
|
});
|
|
30355
30481
|
}
|
|
30356
30482
|
}
|
|
@@ -33507,8 +33633,16 @@ var TOOL_SPECS = [
|
|
|
33507
33633
|
type: "boolean",
|
|
33508
33634
|
description: "structure: also list what the script inherits, walking extends through the project's other scripts. Each such member carries inherited_from, the file declaring it, and the answer gains inherits_from, the chain that was walked. A name the script overrides is listed at both its lines rather than once. A native base is not walked, since it declares nothing in a file: editor_classes info answers for those. Default false."
|
|
33509
33635
|
},
|
|
33510
|
-
line: {
|
|
33511
|
-
|
|
33636
|
+
line: {
|
|
33637
|
+
type: "integer",
|
|
33638
|
+
minimum: 1,
|
|
33639
|
+
description: "completion, hover: the line, counted from 1 as the editor shows it."
|
|
33640
|
+
},
|
|
33641
|
+
character: {
|
|
33642
|
+
type: "integer",
|
|
33643
|
+
minimum: 1,
|
|
33644
|
+
description: "completion, hover: the column, counted from 1 as the editor shows it, with a tab counted as one. symbols gives each name at the line and column hover is asked at."
|
|
33645
|
+
}
|
|
33512
33646
|
},
|
|
33513
33647
|
requires: ["projectPath", "scriptPath"],
|
|
33514
33648
|
operations: {
|
|
@@ -33516,7 +33650,10 @@ var TOOL_SPECS = [
|
|
|
33516
33650
|
summary: "functions, variables, signals, class_name and extends, read from the file",
|
|
33517
33651
|
requires: []
|
|
33518
33652
|
},
|
|
33519
|
-
symbols: {
|
|
33653
|
+
symbols: {
|
|
33654
|
+
summary: "document symbols from the language server, each with its kind, the line and column of its name, and the lastLine of its declaration",
|
|
33655
|
+
requires: []
|
|
33656
|
+
},
|
|
33520
33657
|
completion: { summary: "completions at a position", requires: ["line", "character"] },
|
|
33521
33658
|
hover: { summary: "hover text at a position", requires: ["line", "character"] }
|
|
33522
33659
|
},
|
|
@@ -33524,13 +33661,13 @@ var TOOL_SPECS = [
|
|
|
33524
33661
|
},
|
|
33525
33662
|
{
|
|
33526
33663
|
name: "script_diagnostics",
|
|
33527
|
-
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. 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.",
|
|
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.",
|
|
33528
33665
|
parameters: {
|
|
33529
33666
|
projectPath: PROJECT_PATH,
|
|
33530
33667
|
scriptPath: {
|
|
33531
33668
|
type: ["string", "array"],
|
|
33532
33669
|
items: { type: "string" },
|
|
33533
|
-
description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered as it always was
|
|
33670
|
+
description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered as it always was. Several are answered together, with clean, errors and warnings for all of them: a script with nothing to report is named in nothingReported, and every other is answered under scripts in that same shape, including a script that could not be read, named with why while the rest are still asked about. Any number of scripts is one call, which reports progress as it goes.'
|
|
33534
33671
|
}
|
|
33535
33672
|
},
|
|
33536
33673
|
requires: ["projectPath", "scriptPath"]
|
|
@@ -33578,7 +33715,7 @@ var TOOL_SPECS = [
|
|
|
33578
33715
|
},
|
|
33579
33716
|
{
|
|
33580
33717
|
name: "editor_launch",
|
|
33581
|
-
description: "Opens the Godot editor on a project, or restarts the one already connected. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
33718
|
+
description: "Opens the Godot editor on a project, or restarts the one already connected. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
33582
33719
|
parameters: {
|
|
33583
33720
|
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
33584
33721
|
hidden: {
|
|
@@ -33733,7 +33870,7 @@ var TOOL_SPECS = [
|
|
|
33733
33870
|
},
|
|
33734
33871
|
{
|
|
33735
33872
|
name: "editor_status",
|
|
33736
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
33873
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
33737
33874
|
parameters: {},
|
|
33738
33875
|
requires: []
|
|
33739
33876
|
},
|
|
@@ -34517,6 +34654,16 @@ function previousRunLeft(ended) {
|
|
|
34517
34654
|
return (ended?.ending === "refused" || ended?.ending === "lingering") && ended.pid !== null ? ended.pid : undefined;
|
|
34518
34655
|
}
|
|
34519
34656
|
var CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
34657
|
+
function editorsOfProjectIn(tree, projectPath) {
|
|
34658
|
+
const found = [];
|
|
34659
|
+
for (const [pid, listed] of tree) {
|
|
34660
|
+
const read = readCommandLine(listed.command);
|
|
34661
|
+
if (read.editor && read.oneShot === null && read.projectPath !== null && /godot/i.test(read.executable) && !CONSOLE_WRAPPER.test(read.executable) && isSameDirectory(read.projectPath, projectPath)) {
|
|
34662
|
+
found.push(pid);
|
|
34663
|
+
}
|
|
34664
|
+
}
|
|
34665
|
+
return found.sort((a, b) => a - b);
|
|
34666
|
+
}
|
|
34520
34667
|
function engineNamesOf(executable) {
|
|
34521
34668
|
return CONSOLE_WRAPPER.test(executable) ? [executable, executable.replace(CONSOLE_WRAPPER, "$1")] : [executable];
|
|
34522
34669
|
}
|
|
@@ -35062,7 +35209,6 @@ function testPathsIn(projectPath, given) {
|
|
|
35062
35209
|
value: unique.filter((path) => !unique.some((other) => other !== path && within(path, other))).map((path) => `res://${path}`)
|
|
35063
35210
|
};
|
|
35064
35211
|
}
|
|
35065
|
-
var MOST_DIAGNOSED = 300;
|
|
35066
35212
|
function scriptsToDiagnose(projectPath, given) {
|
|
35067
35213
|
const listed = Array.isArray(given);
|
|
35068
35214
|
if (!listed) {
|
|
@@ -35106,12 +35252,6 @@ function scriptsToDiagnose(projectPath, given) {
|
|
|
35106
35252
|
if (unique.length === 0) {
|
|
35107
35253
|
return { ok: false, reason: `No .gd script is under ${empty.join(" or ")}.` };
|
|
35108
35254
|
}
|
|
35109
|
-
if (unique.length > MOST_DIAGNOSED) {
|
|
35110
|
-
return {
|
|
35111
|
-
ok: false,
|
|
35112
|
-
reason: `scriptPath names ${unique.length} scripts, and one call asks about at most ${MOST_DIAGNOSED}, each a round trip through the language server: name narrower folders, or call again for the rest.`
|
|
35113
|
-
};
|
|
35114
|
-
}
|
|
35115
35255
|
return { ok: true, value: unique };
|
|
35116
35256
|
}
|
|
35117
35257
|
function isDirectory(path) {
|
|
@@ -36977,11 +37117,16 @@ class GodotServer {
|
|
|
36977
37117
|
return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
|
|
36978
37118
|
}
|
|
36979
37119
|
const progress = callProgress();
|
|
36980
|
-
const
|
|
37120
|
+
const cancelled = callSignal();
|
|
37121
|
+
const reported = [];
|
|
37122
|
+
const nothingReported = [];
|
|
36981
37123
|
let errors = 0;
|
|
36982
37124
|
let warnings = 0;
|
|
36983
37125
|
let unread = 0;
|
|
36984
37126
|
for (const [at, scriptPath] of scripts.value.entries()) {
|
|
37127
|
+
if (cancelled?.aborted === true) {
|
|
37128
|
+
return this.createErrorResponse(`Cancelled after ${at} of ${scripts.value.length} scripts.`);
|
|
37129
|
+
}
|
|
36985
37130
|
progress?.(at + 1, `${at + 1} of ${scripts.value.length}: ${scriptPath}`);
|
|
36986
37131
|
const one = await this.diagnosticsOf({ ...args, scriptPath });
|
|
36987
37132
|
if ("refusal" in one) {
|
|
@@ -36989,20 +37134,25 @@ class GodotServer {
|
|
|
36989
37134
|
return one.refusal;
|
|
36990
37135
|
}
|
|
36991
37136
|
unread += 1;
|
|
36992
|
-
|
|
37137
|
+
reported.push({ scriptPath, error: one.refusal.content[0]?.text ?? "not read" });
|
|
36993
37138
|
continue;
|
|
36994
37139
|
}
|
|
36995
37140
|
errors += Number(one.answer["errors"]);
|
|
36996
37141
|
warnings += Number(one.answer["warnings"]);
|
|
36997
|
-
|
|
37142
|
+
if (readArray(one.answer, "diagnostics")?.length === 0) {
|
|
37143
|
+
nothingReported.push(scriptPath);
|
|
37144
|
+
} else {
|
|
37145
|
+
reported.push(one.answer);
|
|
37146
|
+
}
|
|
36998
37147
|
}
|
|
36999
37148
|
return this.jsonTextResponse(this.markedIfStale({
|
|
37000
37149
|
clean: errors === 0 && unread === 0,
|
|
37001
|
-
scriptsChecked:
|
|
37150
|
+
scriptsChecked: scripts.value.length,
|
|
37002
37151
|
errors,
|
|
37003
37152
|
warnings,
|
|
37004
37153
|
...unread > 0 ? { scriptsNotRead: unread } : {},
|
|
37005
|
-
scripts:
|
|
37154
|
+
scripts: reported,
|
|
37155
|
+
nothingReported
|
|
37006
37156
|
}));
|
|
37007
37157
|
}
|
|
37008
37158
|
markedIfStale(answer) {
|
|
@@ -37016,6 +37166,9 @@ class GodotServer {
|
|
|
37016
37166
|
if (payload["refusedArguments"] === true) {
|
|
37017
37167
|
return { refusal: this.createErrorResponse(reason), perScript: true };
|
|
37018
37168
|
}
|
|
37169
|
+
if (payload["stillReading"] === true) {
|
|
37170
|
+
return { refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`), perScript: false };
|
|
37171
|
+
}
|
|
37019
37172
|
if (typeof payload["serves"] === "string") {
|
|
37020
37173
|
return {
|
|
37021
37174
|
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
@@ -37035,10 +37188,7 @@ class GodotServer {
|
|
|
37035
37188
|
};
|
|
37036
37189
|
}
|
|
37037
37190
|
const diagnostics = readArray(payload, "diagnostics") ?? [];
|
|
37038
|
-
const errors = diagnostics.filter((entry) =>
|
|
37039
|
-
const severity = asParams(entry)["severity"];
|
|
37040
|
-
return severity === 1 || severity === "error" || severity === "ERROR";
|
|
37041
|
-
}).length;
|
|
37191
|
+
const errors = diagnostics.filter((entry) => asParams(entry)["severity"] === "error").length;
|
|
37042
37192
|
return {
|
|
37043
37193
|
answer: {
|
|
37044
37194
|
scriptPath: readString(args, "scriptPath"),
|
|
@@ -37266,14 +37416,16 @@ class GodotServer {
|
|
|
37266
37416
|
const greeted = hasSaidWhoItIs(status);
|
|
37267
37417
|
const stale = greeted && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
|
|
37268
37418
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
37419
|
+
const arriving = status.connected || this.ownProject === null ? [] : await this.editorsNotYetConnected(this.ownProject) ?? [];
|
|
37269
37420
|
return {
|
|
37270
37421
|
...status,
|
|
37271
37422
|
serverVersion: SERVER_VERSION,
|
|
37272
37423
|
addonIsStale: greeted ? stale : undefined,
|
|
37273
37424
|
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,
|
|
37274
37425
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
37275
|
-
mayYetConnect: status.connected ? undefined : unfinished !== null ? unfinished.underway : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
37276
|
-
|
|
37426
|
+
mayYetConnect: status.connected ? undefined : arriving.length > 0 ? true : unfinished !== null ? unfinished.underway : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
37427
|
+
editorsNotYetConnected: arriving.length > 0 ? arriving : undefined,
|
|
37428
|
+
restartInterrupted: unfinished === null || unfinished.underway || arriving.length > 0 ? undefined : {
|
|
37277
37429
|
quitEditorPid: unfinished.note.editorPid,
|
|
37278
37430
|
quitAt: unfinished.note.quitAt,
|
|
37279
37431
|
byServerPid: unfinished.note.byPid,
|
|
@@ -37570,6 +37722,14 @@ class GodotServer {
|
|
|
37570
37722
|
if (refused !== null) {
|
|
37571
37723
|
return refused;
|
|
37572
37724
|
}
|
|
37725
|
+
const arriving = await this.editorsNotYetConnected(project.value.path);
|
|
37726
|
+
if (arriving !== undefined && arriving.length > 0) {
|
|
37727
|
+
const named = arriving.map((one) => `pid ${one.pid}${one.startedAt === null ? "" : `, started ${one.startedAt}`}`).join("; ");
|
|
37728
|
+
return this.createErrorResponse(`An editor of this project is already running (${named}) and has not reached this server yet: an editor dials in once it is up, later while it imports, so opening another would put two editors on one project and one language server port.`, [
|
|
37729
|
+
"editor_status names it under editorsNotYetConnected and says when it has connected",
|
|
37730
|
+
`If it never connects it is not reaching this server: end ${arriving.length === 1 ? "that pid" : "those pids"} and open again`
|
|
37731
|
+
]);
|
|
37732
|
+
}
|
|
37573
37733
|
const engine = await this.engine();
|
|
37574
37734
|
if (!engine.ok) {
|
|
37575
37735
|
return engine.response;
|
|
@@ -37594,6 +37754,19 @@ class GodotServer {
|
|
|
37594
37754
|
settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
|
|
37595
37755
|
});
|
|
37596
37756
|
}
|
|
37757
|
+
async editorsNotYetConnected(projectPath) {
|
|
37758
|
+
const tree = await processTree();
|
|
37759
|
+
if (tree === undefined) {
|
|
37760
|
+
return;
|
|
37761
|
+
}
|
|
37762
|
+
const connected = this.godotBridge.isConnected() ? this.godotBridge.getStatus().editorPid : undefined;
|
|
37763
|
+
const pids = editorsOfProjectIn(tree, projectPath).filter((pid) => pid !== connected);
|
|
37764
|
+
const started = await startTimesOf(pids);
|
|
37765
|
+
return pids.map((pid) => {
|
|
37766
|
+
const at = started.get(pid) ?? tree.get(pid)?.startedAt;
|
|
37767
|
+
return { pid, startedAt: at === undefined ? null : new Date(at).toISOString() };
|
|
37768
|
+
});
|
|
37769
|
+
}
|
|
37597
37770
|
anEditorThatCannotConnect(projectPath) {
|
|
37598
37771
|
if (this.ownProject !== null && !isSameDirectory(this.ownProject, projectPath)) {
|
|
37599
37772
|
return this.createErrorResponse(`This server serves ${this.ownProject}, and an editor of ${projectPath} would dial that project's own server rather than this one.`, [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.52",
|
|
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",
|