gsd-pi 2.8.2 → 2.9.0
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/README.md +2 -1
- package/dist/cli.js +5 -0
- package/dist/loader.js +1 -1
- package/dist/update-check.d.ts +24 -0
- package/dist/update-check.js +93 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.d.ts +4 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js +758 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js +267 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js +101 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js +709 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js +64 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js +574 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js +4 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +80 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +5 -0
- package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/core/extensions/types.ts +4 -2
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/client.ts +880 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/config.ts +325 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/defaults.json +456 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/edits.ts +109 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/index.ts +943 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp.md +33 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/types.ts +421 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/utils.ts +682 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/index.ts +10 -0
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +94 -2
- package/node_modules/@gsd/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
- package/node_modules/@gsd/pi-coding-agent/src/modes/rpc/rpc-types.ts +2 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +4 -2
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js +758 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.js +267 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
- package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/edits.js +101 -0
- package/packages/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
- package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
- package/packages/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.js +709 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
- package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.js +64 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.js +574 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js +4 -0
- package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +80 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +5 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/types.ts +4 -2
- package/packages/pi-coding-agent/src/core/lsp/client.ts +880 -0
- package/packages/pi-coding-agent/src/core/lsp/config.ts +325 -0
- package/packages/pi-coding-agent/src/core/lsp/defaults.json +456 -0
- package/packages/pi-coding-agent/src/core/lsp/edits.ts +109 -0
- package/packages/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
- package/packages/pi-coding-agent/src/core/lsp/index.ts +943 -0
- package/packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
- package/packages/pi-coding-agent/src/core/lsp/lsp.md +33 -0
- package/packages/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
- package/packages/pi-coding-agent/src/core/lsp/types.ts +421 -0
- package/packages/pi-coding-agent/src/core/lsp/utils.ts +682 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/core/tools/index.ts +10 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +94 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +2 -1
- package/src/resources/extensions/ask-user-questions.ts +42 -2
- package/src/resources/extensions/bg-shell/index.ts +34 -37
- package/src/resources/extensions/browser-tools/core.d.ts +205 -0
- package/src/resources/extensions/browser-tools/index.ts +2 -2
- package/src/resources/extensions/browser-tools/refs.ts +1 -1
- package/src/resources/extensions/browser-tools/tools/session.ts +1 -1
- package/src/resources/extensions/context7/index.ts +2 -2
- package/src/resources/extensions/get-secrets-from-user.ts +3 -2
- package/src/resources/extensions/google-search/index.ts +1 -1
- package/src/resources/extensions/gsd/auto.ts +126 -12
- package/src/resources/extensions/gsd/commands.ts +218 -3
- package/src/resources/extensions/gsd/doctor.ts +1 -1
- package/src/resources/extensions/gsd/git-service.ts +163 -13
- package/src/resources/extensions/gsd/guided-flow.ts +19 -9
- package/src/resources/extensions/gsd/index.ts +17 -7
- package/src/resources/extensions/gsd/preferences.ts +1 -1
- package/src/resources/extensions/gsd/tests/git-service.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/migrate-command.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +10 -10
- package/src/resources/extensions/gsd/tests/next-milestone-id.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/worktree.test.ts +352 -0
- package/src/resources/extensions/gsd/types.ts +1 -0
- package/src/resources/extensions/gsd/worktree.ts +20 -1
- package/src/resources/extensions/mac-tools/index.ts +1 -1
- package/src/resources/extensions/search-the-web/command-search-provider.ts +1 -1
- package/src/resources/extensions/search-the-web/format.ts +1 -1
- package/src/resources/extensions/search-the-web/index.ts +5 -5
- package/src/resources/extensions/search-the-web/native-search.ts +5 -6
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +7 -7
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +11 -11
- package/src/resources/extensions/search-the-web/tool-search.ts +10 -10
- package/src/resources/extensions/shared/interview-ui.ts +2 -2
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import * as fsPromises from "node:fs/promises";
|
|
3
|
+
import type { Writable } from "node:stream";
|
|
4
|
+
import { killProcessTree } from "../../utils/shell.js";
|
|
5
|
+
import { ToolAbortError, isEnoent, throwIfAborted, untilAborted } from "./helpers.js";
|
|
6
|
+
import { applyWorkspaceEdit } from "./edits.js";
|
|
7
|
+
import { getLspmuxCommand, isLspmuxSupported } from "./lspmux.js";
|
|
8
|
+
import type {
|
|
9
|
+
Diagnostic,
|
|
10
|
+
LspClient,
|
|
11
|
+
LspJsonRpcNotification,
|
|
12
|
+
LspJsonRpcRequest,
|
|
13
|
+
LspJsonRpcResponse,
|
|
14
|
+
ServerConfig,
|
|
15
|
+
WorkspaceEdit,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
import { detectLanguageId, fileToUri } from "./utils.js";
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Client State
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
const clients = new Map<string, LspClient>();
|
|
24
|
+
const clientLocks = new Map<string, Promise<LspClient>>();
|
|
25
|
+
const fileOperationLocks = new Map<string, Promise<void>>();
|
|
26
|
+
|
|
27
|
+
// Idle timeout configuration (disabled by default)
|
|
28
|
+
let idleTimeoutMs: number | null = null;
|
|
29
|
+
let idleCheckInterval: ReturnType<typeof setInterval> | null = null;
|
|
30
|
+
const IDLE_CHECK_INTERVAL_MS = 60 * 1000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Configure the idle timeout for LSP clients.
|
|
34
|
+
*/
|
|
35
|
+
export function setIdleTimeout(ms: number | null | undefined): void {
|
|
36
|
+
idleTimeoutMs = ms ?? null;
|
|
37
|
+
|
|
38
|
+
if (idleTimeoutMs && idleTimeoutMs > 0) {
|
|
39
|
+
startIdleChecker();
|
|
40
|
+
} else {
|
|
41
|
+
stopIdleChecker();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function startIdleChecker(): void {
|
|
46
|
+
if (idleCheckInterval) return;
|
|
47
|
+
idleCheckInterval = setInterval(() => {
|
|
48
|
+
if (!idleTimeoutMs) return;
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
for (const [key, client] of Array.from(clients.entries())) {
|
|
51
|
+
if (now - client.lastActivity > idleTimeoutMs) {
|
|
52
|
+
shutdownClient(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}, IDLE_CHECK_INTERVAL_MS);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stopIdleChecker(): void {
|
|
59
|
+
if (idleCheckInterval) {
|
|
60
|
+
clearInterval(idleCheckInterval);
|
|
61
|
+
idleCheckInterval = null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Client Capabilities
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
const CLIENT_CAPABILITIES = {
|
|
70
|
+
textDocument: {
|
|
71
|
+
synchronization: {
|
|
72
|
+
didSave: true,
|
|
73
|
+
dynamicRegistration: false,
|
|
74
|
+
willSave: false,
|
|
75
|
+
willSaveWaitUntil: false,
|
|
76
|
+
},
|
|
77
|
+
hover: {
|
|
78
|
+
contentFormat: ["markdown", "plaintext"],
|
|
79
|
+
dynamicRegistration: false,
|
|
80
|
+
},
|
|
81
|
+
definition: {
|
|
82
|
+
dynamicRegistration: false,
|
|
83
|
+
linkSupport: true,
|
|
84
|
+
},
|
|
85
|
+
typeDefinition: {
|
|
86
|
+
dynamicRegistration: false,
|
|
87
|
+
linkSupport: true,
|
|
88
|
+
},
|
|
89
|
+
implementation: {
|
|
90
|
+
dynamicRegistration: false,
|
|
91
|
+
linkSupport: true,
|
|
92
|
+
},
|
|
93
|
+
references: {
|
|
94
|
+
dynamicRegistration: false,
|
|
95
|
+
},
|
|
96
|
+
documentSymbol: {
|
|
97
|
+
dynamicRegistration: false,
|
|
98
|
+
hierarchicalDocumentSymbolSupport: true,
|
|
99
|
+
symbolKind: {
|
|
100
|
+
valueSet: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
rename: {
|
|
104
|
+
dynamicRegistration: false,
|
|
105
|
+
prepareSupport: true,
|
|
106
|
+
},
|
|
107
|
+
codeAction: {
|
|
108
|
+
dynamicRegistration: false,
|
|
109
|
+
codeActionLiteralSupport: {
|
|
110
|
+
codeActionKind: {
|
|
111
|
+
valueSet: [
|
|
112
|
+
"quickfix",
|
|
113
|
+
"refactor",
|
|
114
|
+
"refactor.extract",
|
|
115
|
+
"refactor.inline",
|
|
116
|
+
"refactor.rewrite",
|
|
117
|
+
"source",
|
|
118
|
+
"source.organizeImports",
|
|
119
|
+
"source.fixAll",
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
resolveSupport: {
|
|
124
|
+
properties: ["edit"],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
formatting: {
|
|
128
|
+
dynamicRegistration: false,
|
|
129
|
+
},
|
|
130
|
+
rangeFormatting: {
|
|
131
|
+
dynamicRegistration: false,
|
|
132
|
+
},
|
|
133
|
+
publishDiagnostics: {
|
|
134
|
+
relatedInformation: true,
|
|
135
|
+
versionSupport: false,
|
|
136
|
+
tagSupport: { valueSet: [1, 2] },
|
|
137
|
+
codeDescriptionSupport: true,
|
|
138
|
+
dataSupport: true,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
workspace: {
|
|
142
|
+
applyEdit: true,
|
|
143
|
+
workspaceEdit: {
|
|
144
|
+
documentChanges: true,
|
|
145
|
+
resourceOperations: ["create", "rename", "delete"],
|
|
146
|
+
failureHandling: "textOnlyTransactional",
|
|
147
|
+
},
|
|
148
|
+
configuration: true,
|
|
149
|
+
symbol: {
|
|
150
|
+
dynamicRegistration: false,
|
|
151
|
+
symbolKind: {
|
|
152
|
+
valueSet: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
experimental: {
|
|
157
|
+
snippetTextEdit: true,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// =============================================================================
|
|
162
|
+
// LSP Message Protocol
|
|
163
|
+
// =============================================================================
|
|
164
|
+
|
|
165
|
+
function parseMessage(
|
|
166
|
+
buffer: Buffer,
|
|
167
|
+
): { message: LspJsonRpcResponse | LspJsonRpcNotification | null; remaining: Buffer } | null {
|
|
168
|
+
const headerEndIndex = findHeaderEnd(buffer);
|
|
169
|
+
if (headerEndIndex === -1) return null;
|
|
170
|
+
|
|
171
|
+
const headerText = new TextDecoder().decode(buffer.slice(0, headerEndIndex));
|
|
172
|
+
const contentLengthMatch = headerText.match(/Content-Length: (\d+)/i);
|
|
173
|
+
if (!contentLengthMatch) return null;
|
|
174
|
+
|
|
175
|
+
const contentLength = Number.parseInt(contentLengthMatch[1], 10);
|
|
176
|
+
const messageStart = headerEndIndex + 4; // Skip \r\n\r\n
|
|
177
|
+
const messageEnd = messageStart + contentLength;
|
|
178
|
+
|
|
179
|
+
if (buffer.length < messageEnd) return null;
|
|
180
|
+
|
|
181
|
+
const messageBytes = buffer.subarray(messageStart, messageEnd);
|
|
182
|
+
const messageText = new TextDecoder().decode(messageBytes);
|
|
183
|
+
const remaining = Buffer.from(buffer.subarray(messageEnd));
|
|
184
|
+
|
|
185
|
+
let message: LspJsonRpcResponse | LspJsonRpcNotification;
|
|
186
|
+
try {
|
|
187
|
+
message = JSON.parse(messageText);
|
|
188
|
+
} catch {
|
|
189
|
+
// Malformed JSON from LSP server — skip this message and advance past it
|
|
190
|
+
return { message: null, remaining };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { message, remaining };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function findHeaderEnd(buffer: Uint8Array): number {
|
|
197
|
+
for (let i = 0; i < buffer.length - 3; i++) {
|
|
198
|
+
if (buffer[i] === 13 && buffer[i + 1] === 10 && buffer[i + 2] === 13 && buffer[i + 3] === 10) {
|
|
199
|
+
return i;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return -1;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function writeMessage(
|
|
206
|
+
stdin: Writable | null,
|
|
207
|
+
message: LspJsonRpcRequest | LspJsonRpcNotification | LspJsonRpcResponse,
|
|
208
|
+
): Promise<void> {
|
|
209
|
+
if (!stdin) {
|
|
210
|
+
throw new Error("LSP process stdin is not available");
|
|
211
|
+
}
|
|
212
|
+
const content = JSON.stringify(message);
|
|
213
|
+
const header = `Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n`;
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
stdin.write(header + content, (err?: Error | null) => {
|
|
216
|
+
if (err) reject(err);
|
|
217
|
+
else resolve();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// =============================================================================
|
|
223
|
+
// Message Reader
|
|
224
|
+
// =============================================================================
|
|
225
|
+
|
|
226
|
+
async function startMessageReader(client: LspClient): Promise<void> {
|
|
227
|
+
if (client.isReading) return;
|
|
228
|
+
client.isReading = true;
|
|
229
|
+
|
|
230
|
+
const stdout = client.proc.stdout;
|
|
231
|
+
if (!stdout) {
|
|
232
|
+
client.isReading = false;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return new Promise<void>((resolve) => {
|
|
237
|
+
stdout.on("data", async (chunk: Buffer) => {
|
|
238
|
+
const currentBuffer: Buffer = Buffer.concat([client.messageBuffer, chunk]);
|
|
239
|
+
client.messageBuffer = currentBuffer;
|
|
240
|
+
|
|
241
|
+
let workingBuffer = currentBuffer;
|
|
242
|
+
let parsed = parseMessage(workingBuffer);
|
|
243
|
+
while (parsed) {
|
|
244
|
+
const { message, remaining } = parsed;
|
|
245
|
+
workingBuffer = remaining;
|
|
246
|
+
|
|
247
|
+
if (!message) {
|
|
248
|
+
parsed = parseMessage(workingBuffer);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if ("id" in message && message.id !== undefined) {
|
|
253
|
+
const pending = client.pendingRequests.get(message.id);
|
|
254
|
+
if (pending) {
|
|
255
|
+
client.pendingRequests.delete(message.id);
|
|
256
|
+
if ("error" in message && message.error) {
|
|
257
|
+
pending.reject(new Error(`LSP error: ${message.error.message}`));
|
|
258
|
+
} else {
|
|
259
|
+
pending.resolve(message.result);
|
|
260
|
+
}
|
|
261
|
+
} else if ("method" in message) {
|
|
262
|
+
await handleServerRequest(client, message as LspJsonRpcRequest);
|
|
263
|
+
}
|
|
264
|
+
} else if ("method" in message) {
|
|
265
|
+
if (message.method === "textDocument/publishDiagnostics" && message.params) {
|
|
266
|
+
const params = message.params as { uri: string; diagnostics: Diagnostic[] };
|
|
267
|
+
client.diagnostics.set(params.uri, params.diagnostics);
|
|
268
|
+
client.diagnosticsVersion += 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
parsed = parseMessage(workingBuffer);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
client.messageBuffer = workingBuffer;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
stdout.on("end", () => {
|
|
279
|
+
client.isReading = false;
|
|
280
|
+
resolve();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
stdout.on("error", () => {
|
|
284
|
+
client.isReading = false;
|
|
285
|
+
resolve();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// =============================================================================
|
|
291
|
+
// Server Request Handlers
|
|
292
|
+
// =============================================================================
|
|
293
|
+
|
|
294
|
+
async function handleConfigurationRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
|
|
295
|
+
if (typeof message.id !== "number") return;
|
|
296
|
+
const params = message.params as { items?: Array<{ section?: string }> };
|
|
297
|
+
const items = params?.items ?? [];
|
|
298
|
+
const result = items.map(item => {
|
|
299
|
+
const section = item.section ?? "";
|
|
300
|
+
return client.config.settings?.[section] ?? {};
|
|
301
|
+
});
|
|
302
|
+
await sendResponse(client, message.id, result, "workspace/configuration");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function handleApplyEditRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
|
|
306
|
+
if (typeof message.id !== "number") return;
|
|
307
|
+
const params = message.params as { edit?: WorkspaceEdit };
|
|
308
|
+
if (!params?.edit) {
|
|
309
|
+
await sendResponse(
|
|
310
|
+
client,
|
|
311
|
+
message.id,
|
|
312
|
+
{ applied: false, failureReason: "No edit provided" },
|
|
313
|
+
"workspace/applyEdit",
|
|
314
|
+
);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
await applyWorkspaceEdit(params.edit, client.cwd);
|
|
320
|
+
await sendResponse(client, message.id, { applied: true }, "workspace/applyEdit");
|
|
321
|
+
} catch (err: unknown) {
|
|
322
|
+
await sendResponse(client, message.id, { applied: false, failureReason: String(err) }, "workspace/applyEdit");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function handleServerRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
|
|
327
|
+
if (message.method === "workspace/configuration") {
|
|
328
|
+
await handleConfigurationRequest(client, message);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (message.method === "workspace/applyEdit") {
|
|
332
|
+
await handleApplyEditRequest(client, message);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (typeof message.id !== "number") return;
|
|
336
|
+
await sendResponse(client, message.id, null, message.method, {
|
|
337
|
+
code: -32601,
|
|
338
|
+
message: `Method not found: ${message.method}`,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function sendResponse(
|
|
343
|
+
client: LspClient,
|
|
344
|
+
id: number,
|
|
345
|
+
result: unknown,
|
|
346
|
+
_method: string,
|
|
347
|
+
error?: { code: number; message: string; data?: unknown },
|
|
348
|
+
): Promise<void> {
|
|
349
|
+
const response: LspJsonRpcResponse = {
|
|
350
|
+
jsonrpc: "2.0",
|
|
351
|
+
id,
|
|
352
|
+
...(error ? { error } : { result }),
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
await writeMessage(client.proc.stdin, response);
|
|
357
|
+
} catch {
|
|
358
|
+
// Failed to respond to server request
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// =============================================================================
|
|
363
|
+
// Stderr Buffer
|
|
364
|
+
// =============================================================================
|
|
365
|
+
|
|
366
|
+
async function startStderrReader(client: LspClient): Promise<void> {
|
|
367
|
+
const stderr = client.proc.stderr;
|
|
368
|
+
if (!stderr) return;
|
|
369
|
+
|
|
370
|
+
return new Promise<void>((resolve) => {
|
|
371
|
+
stderr.on("data", (chunk: Buffer) => {
|
|
372
|
+
const text = chunk.toString("utf-8");
|
|
373
|
+
client.stderrBuffer += text;
|
|
374
|
+
if (client.stderrBuffer.length > 4096) {
|
|
375
|
+
client.stderrBuffer = client.stderrBuffer.slice(-4096);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
stderr.on("end", () => {
|
|
380
|
+
resolve();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
stderr.on("error", () => {
|
|
384
|
+
resolve();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// =============================================================================
|
|
390
|
+
// Client Management
|
|
391
|
+
// =============================================================================
|
|
392
|
+
|
|
393
|
+
/** Timeout for warmup initialize requests (5 seconds) */
|
|
394
|
+
export const WARMUP_TIMEOUT_MS = 5000;
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Get or create an LSP client for the given server configuration and working directory.
|
|
398
|
+
*/
|
|
399
|
+
export async function getOrCreateClient(config: ServerConfig, cwd: string, initTimeoutMs?: number): Promise<LspClient> {
|
|
400
|
+
const key = `${config.command}:${cwd}`;
|
|
401
|
+
|
|
402
|
+
const existingClient = clients.get(key);
|
|
403
|
+
if (existingClient) {
|
|
404
|
+
existingClient.lastActivity = Date.now();
|
|
405
|
+
return existingClient;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const existingLock = clientLocks.get(key);
|
|
409
|
+
if (existingLock) {
|
|
410
|
+
return existingLock;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const clientPromise = (async () => {
|
|
414
|
+
const baseCommand = config.resolvedCommand ?? config.command;
|
|
415
|
+
const baseArgs = config.args ?? [];
|
|
416
|
+
|
|
417
|
+
// Wrap with lspmux if available and supported
|
|
418
|
+
const { command, args, env } = isLspmuxSupported(baseCommand)
|
|
419
|
+
? await getLspmuxCommand(baseCommand, baseArgs)
|
|
420
|
+
: { command: baseCommand, args: baseArgs };
|
|
421
|
+
|
|
422
|
+
const proc = spawn(command, args, {
|
|
423
|
+
cwd,
|
|
424
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
425
|
+
env: env ? { ...process.env, ...env } : undefined,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const exitedPromise = new Promise<number>((resolve) => {
|
|
429
|
+
proc.on("exit", (code: number | null) => resolve(code ?? 1));
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const client: LspClient = {
|
|
433
|
+
name: key,
|
|
434
|
+
cwd,
|
|
435
|
+
proc: {
|
|
436
|
+
stdin: proc.stdin,
|
|
437
|
+
stdout: proc.stdout,
|
|
438
|
+
stderr: proc.stderr,
|
|
439
|
+
pid: proc.pid ?? 0,
|
|
440
|
+
exitCode: null,
|
|
441
|
+
exited: exitedPromise,
|
|
442
|
+
kill: (signal?: number) => proc.kill(signal),
|
|
443
|
+
},
|
|
444
|
+
config,
|
|
445
|
+
requestId: 0,
|
|
446
|
+
diagnostics: new Map(),
|
|
447
|
+
diagnosticsVersion: 0,
|
|
448
|
+
openFiles: new Map(),
|
|
449
|
+
pendingRequests: new Map(),
|
|
450
|
+
messageBuffer: Buffer.alloc(0),
|
|
451
|
+
isReading: false,
|
|
452
|
+
lastActivity: Date.now(),
|
|
453
|
+
stderrBuffer: "",
|
|
454
|
+
};
|
|
455
|
+
clients.set(key, client);
|
|
456
|
+
|
|
457
|
+
// Register crash recovery
|
|
458
|
+
exitedPromise.then((code: number) => {
|
|
459
|
+
client.proc.exitCode = code;
|
|
460
|
+
clients.delete(key);
|
|
461
|
+
clientLocks.delete(key);
|
|
462
|
+
|
|
463
|
+
if (client.pendingRequests.size > 0) {
|
|
464
|
+
const stderr = client.stderrBuffer.trim();
|
|
465
|
+
const err = new Error(
|
|
466
|
+
stderr ? `LSP server exited (code ${code}): ${stderr}` : `LSP server exited unexpectedly (code ${code})`,
|
|
467
|
+
);
|
|
468
|
+
for (const pending of client.pendingRequests.values()) {
|
|
469
|
+
pending.reject(err);
|
|
470
|
+
}
|
|
471
|
+
client.pendingRequests.clear();
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Start background readers
|
|
476
|
+
startMessageReader(client);
|
|
477
|
+
startStderrReader(client);
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
const initResult = (await sendRequest(
|
|
481
|
+
client,
|
|
482
|
+
"initialize",
|
|
483
|
+
{
|
|
484
|
+
processId: process.pid,
|
|
485
|
+
rootUri: fileToUri(cwd),
|
|
486
|
+
rootPath: cwd,
|
|
487
|
+
capabilities: CLIENT_CAPABILITIES,
|
|
488
|
+
initializationOptions: config.initOptions ?? {},
|
|
489
|
+
workspaceFolders: [{ uri: fileToUri(cwd), name: cwd.split("/").pop() ?? "workspace" }],
|
|
490
|
+
},
|
|
491
|
+
undefined, // signal
|
|
492
|
+
initTimeoutMs,
|
|
493
|
+
)) as { capabilities?: unknown };
|
|
494
|
+
|
|
495
|
+
if (!initResult) {
|
|
496
|
+
throw new Error("Failed to initialize LSP: no response");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
client.serverCapabilities = initResult.capabilities as LspClient["serverCapabilities"];
|
|
500
|
+
|
|
501
|
+
await sendNotification(client, "initialized", {});
|
|
502
|
+
|
|
503
|
+
return client;
|
|
504
|
+
} catch (err) {
|
|
505
|
+
clients.delete(key);
|
|
506
|
+
clientLocks.delete(key);
|
|
507
|
+
try {
|
|
508
|
+
killProcessTree(proc.pid ?? 0);
|
|
509
|
+
} catch {
|
|
510
|
+
proc.kill();
|
|
511
|
+
}
|
|
512
|
+
throw err;
|
|
513
|
+
} finally {
|
|
514
|
+
clientLocks.delete(key);
|
|
515
|
+
}
|
|
516
|
+
})();
|
|
517
|
+
|
|
518
|
+
clientLocks.set(key, clientPromise);
|
|
519
|
+
return clientPromise;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Ensure a file is opened in the LSP client.
|
|
524
|
+
*/
|
|
525
|
+
export async function ensureFileOpen(client: LspClient, filePath: string, signal?: AbortSignal): Promise<void> {
|
|
526
|
+
throwIfAborted(signal);
|
|
527
|
+
const uri = fileToUri(filePath);
|
|
528
|
+
const lockKey = `${client.name}:${uri}`;
|
|
529
|
+
|
|
530
|
+
if (client.openFiles.has(uri)) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const existingLock = fileOperationLocks.get(lockKey);
|
|
535
|
+
if (existingLock) {
|
|
536
|
+
await untilAborted(signal, () => existingLock);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const openPromise = (async () => {
|
|
541
|
+
throwIfAborted(signal);
|
|
542
|
+
if (client.openFiles.has(uri)) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
let content: string;
|
|
547
|
+
try {
|
|
548
|
+
content = await fsPromises.readFile(filePath, "utf-8");
|
|
549
|
+
throwIfAborted(signal);
|
|
550
|
+
} catch (err: unknown) {
|
|
551
|
+
if (isEnoent(err)) return;
|
|
552
|
+
throw err;
|
|
553
|
+
}
|
|
554
|
+
const languageId = detectLanguageId(filePath);
|
|
555
|
+
throwIfAborted(signal);
|
|
556
|
+
|
|
557
|
+
await sendNotification(client, "textDocument/didOpen", {
|
|
558
|
+
textDocument: {
|
|
559
|
+
uri,
|
|
560
|
+
languageId,
|
|
561
|
+
version: 1,
|
|
562
|
+
text: content,
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
client.openFiles.set(uri, { version: 1, languageId });
|
|
567
|
+
client.lastActivity = Date.now();
|
|
568
|
+
})();
|
|
569
|
+
|
|
570
|
+
fileOperationLocks.set(lockKey, openPromise);
|
|
571
|
+
try {
|
|
572
|
+
await openPromise;
|
|
573
|
+
} finally {
|
|
574
|
+
fileOperationLocks.delete(lockKey);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Sync in-memory content to the LSP client without reading from disk.
|
|
580
|
+
*/
|
|
581
|
+
export async function syncContent(
|
|
582
|
+
client: LspClient,
|
|
583
|
+
filePath: string,
|
|
584
|
+
content: string,
|
|
585
|
+
signal?: AbortSignal,
|
|
586
|
+
): Promise<void> {
|
|
587
|
+
const uri = fileToUri(filePath);
|
|
588
|
+
const lockKey = `${client.name}:${uri}`;
|
|
589
|
+
throwIfAborted(signal);
|
|
590
|
+
|
|
591
|
+
const existingLock = fileOperationLocks.get(lockKey);
|
|
592
|
+
if (existingLock) {
|
|
593
|
+
await untilAborted(signal, () => existingLock);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const syncPromise = (async () => {
|
|
597
|
+
client.diagnostics.delete(uri);
|
|
598
|
+
|
|
599
|
+
const info = client.openFiles.get(uri);
|
|
600
|
+
|
|
601
|
+
if (!info) {
|
|
602
|
+
const languageId = detectLanguageId(filePath);
|
|
603
|
+
throwIfAborted(signal);
|
|
604
|
+
await sendNotification(client, "textDocument/didOpen", {
|
|
605
|
+
textDocument: {
|
|
606
|
+
uri,
|
|
607
|
+
languageId,
|
|
608
|
+
version: 1,
|
|
609
|
+
text: content,
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
client.openFiles.set(uri, { version: 1, languageId });
|
|
613
|
+
client.lastActivity = Date.now();
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const version = ++info.version;
|
|
618
|
+
throwIfAborted(signal);
|
|
619
|
+
await sendNotification(client, "textDocument/didChange", {
|
|
620
|
+
textDocument: { uri, version },
|
|
621
|
+
contentChanges: [{ text: content }],
|
|
622
|
+
});
|
|
623
|
+
client.lastActivity = Date.now();
|
|
624
|
+
})();
|
|
625
|
+
|
|
626
|
+
fileOperationLocks.set(lockKey, syncPromise);
|
|
627
|
+
try {
|
|
628
|
+
await syncPromise;
|
|
629
|
+
} finally {
|
|
630
|
+
fileOperationLocks.delete(lockKey);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Notify LSP that a file was saved.
|
|
636
|
+
*/
|
|
637
|
+
export async function notifySaved(client: LspClient, filePath: string, signal?: AbortSignal): Promise<void> {
|
|
638
|
+
const uri = fileToUri(filePath);
|
|
639
|
+
const info = client.openFiles.get(uri);
|
|
640
|
+
if (!info) return;
|
|
641
|
+
|
|
642
|
+
throwIfAborted(signal);
|
|
643
|
+
await sendNotification(client, "textDocument/didSave", {
|
|
644
|
+
textDocument: { uri },
|
|
645
|
+
});
|
|
646
|
+
client.lastActivity = Date.now();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Refresh a file in the LSP client.
|
|
651
|
+
*/
|
|
652
|
+
export async function refreshFile(client: LspClient, filePath: string, signal?: AbortSignal): Promise<void> {
|
|
653
|
+
throwIfAborted(signal);
|
|
654
|
+
const uri = fileToUri(filePath);
|
|
655
|
+
const lockKey = `${client.name}:${uri}`;
|
|
656
|
+
|
|
657
|
+
const existingLock = fileOperationLocks.get(lockKey);
|
|
658
|
+
if (existingLock) {
|
|
659
|
+
await untilAborted(signal, () => existingLock);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const refreshPromise = (async () => {
|
|
663
|
+
throwIfAborted(signal);
|
|
664
|
+
const info = client.openFiles.get(uri);
|
|
665
|
+
|
|
666
|
+
if (!info) {
|
|
667
|
+
await ensureFileOpen(client, filePath, signal);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
let content: string;
|
|
672
|
+
try {
|
|
673
|
+
content = await fsPromises.readFile(filePath, "utf-8");
|
|
674
|
+
throwIfAborted(signal);
|
|
675
|
+
} catch (err: unknown) {
|
|
676
|
+
if (isEnoent(err)) return;
|
|
677
|
+
throw err;
|
|
678
|
+
}
|
|
679
|
+
const version = ++info.version;
|
|
680
|
+
throwIfAborted(signal);
|
|
681
|
+
|
|
682
|
+
await sendNotification(client, "textDocument/didChange", {
|
|
683
|
+
textDocument: { uri, version },
|
|
684
|
+
contentChanges: [{ text: content }],
|
|
685
|
+
});
|
|
686
|
+
throwIfAborted(signal);
|
|
687
|
+
|
|
688
|
+
await sendNotification(client, "textDocument/didSave", {
|
|
689
|
+
textDocument: { uri },
|
|
690
|
+
text: content,
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
client.lastActivity = Date.now();
|
|
694
|
+
})();
|
|
695
|
+
|
|
696
|
+
fileOperationLocks.set(lockKey, refreshPromise);
|
|
697
|
+
try {
|
|
698
|
+
await refreshPromise;
|
|
699
|
+
} finally {
|
|
700
|
+
fileOperationLocks.delete(lockKey);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Shutdown a specific client by key.
|
|
706
|
+
*/
|
|
707
|
+
export function shutdownClient(key: string): void {
|
|
708
|
+
const client = clients.get(key);
|
|
709
|
+
if (!client) return;
|
|
710
|
+
|
|
711
|
+
for (const pending of Array.from(client.pendingRequests.values())) {
|
|
712
|
+
pending.reject(new Error("LSP client shutdown"));
|
|
713
|
+
}
|
|
714
|
+
client.pendingRequests.clear();
|
|
715
|
+
|
|
716
|
+
sendRequest(client, "shutdown", null).catch(() => {});
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
killProcessTree(client.proc.pid);
|
|
720
|
+
} catch {
|
|
721
|
+
client.proc.kill();
|
|
722
|
+
}
|
|
723
|
+
clients.delete(key);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// =============================================================================
|
|
727
|
+
// LSP Protocol Methods
|
|
728
|
+
// =============================================================================
|
|
729
|
+
|
|
730
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
731
|
+
|
|
732
|
+
export async function sendRequest(
|
|
733
|
+
client: LspClient,
|
|
734
|
+
method: string,
|
|
735
|
+
params: unknown,
|
|
736
|
+
signal?: AbortSignal,
|
|
737
|
+
timeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
738
|
+
): Promise<unknown> {
|
|
739
|
+
const id = ++client.requestId;
|
|
740
|
+
if (signal?.aborted) {
|
|
741
|
+
const reason = signal.reason instanceof Error ? signal.reason : new ToolAbortError();
|
|
742
|
+
return Promise.reject(reason);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const request: LspJsonRpcRequest = {
|
|
746
|
+
jsonrpc: "2.0",
|
|
747
|
+
id,
|
|
748
|
+
method,
|
|
749
|
+
params,
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
client.lastActivity = Date.now();
|
|
753
|
+
|
|
754
|
+
const { promise, resolve, reject } = Promise.withResolvers<unknown>();
|
|
755
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
756
|
+
const cleanup = () => {
|
|
757
|
+
if (signal) {
|
|
758
|
+
signal.removeEventListener("abort", abortHandler);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
const abortHandler = () => {
|
|
762
|
+
if (client.pendingRequests.has(id)) {
|
|
763
|
+
client.pendingRequests.delete(id);
|
|
764
|
+
}
|
|
765
|
+
void sendNotification(client, "$/cancelRequest", { id }).catch(() => {});
|
|
766
|
+
if (timeout) clearTimeout(timeout);
|
|
767
|
+
cleanup();
|
|
768
|
+
const reason = signal?.reason instanceof Error ? signal.reason : new ToolAbortError();
|
|
769
|
+
reject(reason);
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
timeout = setTimeout(() => {
|
|
773
|
+
if (client.pendingRequests.has(id)) {
|
|
774
|
+
client.pendingRequests.delete(id);
|
|
775
|
+
const err = new Error(`LSP request ${method} timed out after ${timeoutMs}ms`);
|
|
776
|
+
cleanup();
|
|
777
|
+
reject(err);
|
|
778
|
+
}
|
|
779
|
+
}, timeoutMs);
|
|
780
|
+
if (signal) {
|
|
781
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
782
|
+
if (signal.aborted) {
|
|
783
|
+
abortHandler();
|
|
784
|
+
return promise;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
client.pendingRequests.set(id, {
|
|
789
|
+
resolve: (result: unknown) => {
|
|
790
|
+
if (timeout) clearTimeout(timeout);
|
|
791
|
+
cleanup();
|
|
792
|
+
resolve(result);
|
|
793
|
+
},
|
|
794
|
+
reject: (err: Error) => {
|
|
795
|
+
if (timeout) clearTimeout(timeout);
|
|
796
|
+
cleanup();
|
|
797
|
+
reject(err);
|
|
798
|
+
},
|
|
799
|
+
method,
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
writeMessage(client.proc.stdin, request).catch((err: Error) => {
|
|
803
|
+
if (timeout) clearTimeout(timeout);
|
|
804
|
+
client.pendingRequests.delete(id);
|
|
805
|
+
cleanup();
|
|
806
|
+
reject(err);
|
|
807
|
+
});
|
|
808
|
+
return promise;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export async function sendNotification(client: LspClient, method: string, params: unknown): Promise<void> {
|
|
812
|
+
const notification: LspJsonRpcNotification = {
|
|
813
|
+
jsonrpc: "2.0",
|
|
814
|
+
method,
|
|
815
|
+
params,
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
client.lastActivity = Date.now();
|
|
819
|
+
await writeMessage(client.proc.stdin, notification);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Shutdown all LSP clients.
|
|
824
|
+
*/
|
|
825
|
+
export function shutdownAll(): void {
|
|
826
|
+
const clientsToShutdown = Array.from(clients.values());
|
|
827
|
+
clients.clear();
|
|
828
|
+
|
|
829
|
+
const err = new Error("LSP client shutdown");
|
|
830
|
+
for (const client of clientsToShutdown) {
|
|
831
|
+
const reqs = Array.from(client.pendingRequests.values());
|
|
832
|
+
client.pendingRequests.clear();
|
|
833
|
+
for (const pending of reqs) {
|
|
834
|
+
pending.reject(err);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
void (async () => {
|
|
838
|
+
const timeout = new Promise<void>(resolve => setTimeout(resolve, 5_000));
|
|
839
|
+
const result = sendRequest(client, "shutdown", null).catch(() => {});
|
|
840
|
+
await Promise.race([result, timeout]);
|
|
841
|
+
try {
|
|
842
|
+
killProcessTree(client.proc.pid);
|
|
843
|
+
} catch {
|
|
844
|
+
client.proc.kill();
|
|
845
|
+
}
|
|
846
|
+
})().catch(() => {});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/** Status of an LSP server */
|
|
851
|
+
export interface LspServerStatus {
|
|
852
|
+
name: string;
|
|
853
|
+
status: "connecting" | "ready" | "error";
|
|
854
|
+
fileTypes: string[];
|
|
855
|
+
error?: string;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
export function getActiveClients(): LspServerStatus[] {
|
|
859
|
+
return Array.from(clients.values()).map(client => ({
|
|
860
|
+
name: client.config.command,
|
|
861
|
+
status: "ready" as const,
|
|
862
|
+
fileTypes: client.config.fileTypes,
|
|
863
|
+
}));
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// =============================================================================
|
|
867
|
+
// Process Cleanup
|
|
868
|
+
// =============================================================================
|
|
869
|
+
|
|
870
|
+
if (typeof process !== "undefined") {
|
|
871
|
+
process.on("beforeExit", shutdownAll);
|
|
872
|
+
process.on("SIGINT", () => {
|
|
873
|
+
shutdownAll();
|
|
874
|
+
process.exit(0);
|
|
875
|
+
});
|
|
876
|
+
process.on("SIGTERM", () => {
|
|
877
|
+
shutdownAll();
|
|
878
|
+
process.exit(0);
|
|
879
|
+
});
|
|
880
|
+
}
|