decorated-pi 0.2.2 → 0.4.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 +82 -74
- package/extensions/file-times.ts +124 -0
- package/extensions/guidance.ts +5 -3
- package/extensions/index.ts +6 -2
- package/extensions/io.ts +587 -0
- package/extensions/lsp/client.ts +181 -428
- package/extensions/lsp/env.ts +45 -12
- package/extensions/lsp/format.ts +102 -237
- package/extensions/lsp/index.ts +8 -11
- package/extensions/lsp/manager.ts +249 -0
- package/extensions/lsp/prompt.ts +3 -42
- package/extensions/lsp/protocol.ts +219 -0
- package/extensions/lsp/servers.ts +80 -160
- package/extensions/lsp/tools.ts +175 -510
- package/extensions/lsp/types.ts +42 -0
- package/extensions/mcp/builtin.ts +126 -0
- package/extensions/mcp/client.ts +106 -0
- package/extensions/mcp/index.ts +123 -0
- package/extensions/{extend-model.ts → model-integration.ts} +127 -4
- package/extensions/patch.ts +842 -0
- package/extensions/providers/ark-coding.ts +2 -0
- package/extensions/safety/detect.ts +78 -707
- package/extensions/safety/entropy.ts +226 -0
- package/extensions/safety/index.ts +44 -97
- package/extensions/safety/patterns.ts +155 -0
- package/extensions/safety/types.ts +50 -0
- package/extensions/settings.ts +10 -0
- package/extensions/slash.ts +165 -9
- package/extensions/smart-at.ts +339 -111
- package/extensions/subdir-agents.ts +43 -13
- package/package.json +3 -4
- package/tsconfig.json +16 -0
- package/extensions/lsp/server-manager.ts +0 -309
- package/extensions/lsp/trust.ts +0 -45
package/extensions/lsp/client.ts
CHANGED
|
@@ -1,508 +1,272 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LSP Client — JSON-RPC
|
|
3
|
-
*
|
|
4
|
-
* Based on @spences10/pi-lsp by Scott Spence
|
|
5
|
-
* https://github.com/spences10/my-pi/tree/main/packages/pi-lsp (MIT License)
|
|
6
|
-
*
|
|
7
|
-
* Modifications: added rename() method, simplified type exports
|
|
2
|
+
* LSP Client — high-level LSP operations over JSON-RPC stdio.
|
|
8
3
|
*/
|
|
9
|
-
import { spawn, ChildProcess } from "node:child_process";
|
|
10
|
-
import { EventEmitter } from "node:events";
|
|
11
4
|
import { pathToFileURL } from "node:url";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
character: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface LspRange {
|
|
28
|
-
start: LspPosition;
|
|
29
|
-
end: LspPosition;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface LspLocation {
|
|
33
|
-
uri: string;
|
|
34
|
-
range: LspRange;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface LspDiagnostic {
|
|
38
|
-
range: LspRange;
|
|
39
|
-
severity?: number;
|
|
40
|
-
code?: unknown;
|
|
41
|
-
source?: string;
|
|
42
|
-
message: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface LspHover {
|
|
46
|
-
contents: unknown;
|
|
47
|
-
range?: LspRange;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export type LspDocumentSymbol = {
|
|
51
|
-
name: string;
|
|
52
|
-
kind: number;
|
|
53
|
-
range: LspRange;
|
|
54
|
-
selectionRange: LspRange;
|
|
55
|
-
containerName?: string;
|
|
56
|
-
detail?: string;
|
|
57
|
-
children?: LspDocumentSymbol[];
|
|
58
|
-
uri?: string;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
type PendingRequest = {
|
|
62
|
-
resolve: (value: unknown) => void;
|
|
63
|
-
reject: (error: Error) => void;
|
|
64
|
-
timer: NodeJS.Timeout;
|
|
65
|
-
};
|
|
5
|
+
import {
|
|
6
|
+
LspProtocol,
|
|
7
|
+
LspProtocolError,
|
|
8
|
+
} from "./protocol.js";
|
|
9
|
+
import type {
|
|
10
|
+
LspDiagnostic,
|
|
11
|
+
LspDocumentSymbol,
|
|
12
|
+
LspHover,
|
|
13
|
+
LspLocation,
|
|
14
|
+
LspPosition,
|
|
15
|
+
LspRange,
|
|
16
|
+
} from "./types.js";
|
|
66
17
|
|
|
67
18
|
export class LspClientStartError extends Error {
|
|
68
|
-
command: string;
|
|
69
|
-
args: string[];
|
|
70
|
-
code?: string;
|
|
71
|
-
|
|
72
19
|
constructor(
|
|
73
20
|
message: string,
|
|
74
|
-
|
|
21
|
+
public readonly command: string,
|
|
22
|
+
public readonly args: string[],
|
|
23
|
+
public readonly code?: string,
|
|
24
|
+
cause?: Error
|
|
75
25
|
) {
|
|
76
|
-
super(message,
|
|
26
|
+
super(message, cause ? { cause } : undefined);
|
|
77
27
|
this.name = "LspClientStartError";
|
|
78
|
-
this.command = options.command;
|
|
79
|
-
this.args = options.args;
|
|
80
|
-
this.code = options.code;
|
|
81
28
|
}
|
|
82
29
|
}
|
|
83
30
|
|
|
31
|
+
export interface LspClientOptions {
|
|
32
|
+
command: string;
|
|
33
|
+
args: string[];
|
|
34
|
+
root_uri: string;
|
|
35
|
+
language_id_for_uri: (uri: string) => string | undefined;
|
|
36
|
+
env?: NodeJS.ProcessEnv;
|
|
37
|
+
request_timeout_ms?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
84
40
|
interface OpenDoc {
|
|
85
41
|
version: number;
|
|
86
|
-
text: string;
|
|
87
42
|
}
|
|
88
43
|
|
|
89
|
-
|
|
90
|
-
|
|
44
|
+
/**
|
|
45
|
+
* High-level LSP client.
|
|
46
|
+
*
|
|
47
|
+
* Wraps LspProtocol with LSP-specific operations:
|
|
48
|
+
* document open/didChange, hover, definition, references,
|
|
49
|
+
* document symbols, rename, diagnostics.
|
|
50
|
+
*/
|
|
51
|
+
export class LspClient {
|
|
52
|
+
#protocol = new LspProtocol();
|
|
91
53
|
#options: LspClientOptions;
|
|
92
|
-
#next_id = 1;
|
|
93
|
-
#pending = new Map<number, PendingRequest>();
|
|
94
|
-
#buffer = Buffer.alloc(0);
|
|
95
54
|
#initialized = false;
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#diagnostic_waiters = new Set<() => void>();
|
|
55
|
+
#openDocs = new Map<string, OpenDoc>();
|
|
56
|
+
#diagnosticsByUri = new Map<string, LspDiagnostic[]>();
|
|
99
57
|
|
|
100
58
|
constructor(options: LspClientOptions) {
|
|
101
|
-
super();
|
|
102
59
|
this.#options = options;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
async start(): Promise<void> {
|
|
106
|
-
this.#proc = spawn(this.#options.command, this.#options.args, {
|
|
107
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
108
|
-
env: create_child_process_env(),
|
|
60
|
+
this.#protocol.on("diagnostics", (params: { uri: string; diagnostics: LspDiagnostic[] }) => {
|
|
61
|
+
this.#diagnosticsByUri.set(params.uri, params.diagnostics);
|
|
109
62
|
});
|
|
110
|
-
|
|
63
|
+
}
|
|
111
64
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
});
|
|
65
|
+
get protocol(): LspProtocol {
|
|
66
|
+
return this.#protocol;
|
|
67
|
+
}
|
|
116
68
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
start_reject = null;
|
|
121
|
-
reject(error);
|
|
122
|
-
return true;
|
|
123
|
-
};
|
|
69
|
+
#request(method: string, params: unknown, timeoutMs?: number): Promise<unknown> {
|
|
70
|
+
return this.#protocol.request(method, params, timeoutMs ?? this.#options.request_timeout_ms ?? 30_000);
|
|
71
|
+
}
|
|
124
72
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
73
|
+
/** Start the LSP server and complete initialization handshake. */
|
|
74
|
+
async start(timeoutMs?: number): Promise<void> {
|
|
75
|
+
try {
|
|
76
|
+
await this.#protocol.spawn(
|
|
77
|
+
this.#options.command,
|
|
78
|
+
this.#options.args,
|
|
79
|
+
this.#options.env ?? process.env,
|
|
80
|
+
);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
83
|
+
throw new LspClientStartError(
|
|
84
|
+
code === "ENOENT"
|
|
85
|
+
? `command "${this.#options.command}" not found`
|
|
86
|
+
: `Failed to spawn ${this.#options.command}: ${(err as Error).message}`,
|
|
87
|
+
this.#options.command,
|
|
88
|
+
this.#options.args,
|
|
134
89
|
code,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
proc.on("error", (err) => {
|
|
138
|
-
const wrapped = start_error(
|
|
139
|
-
`Failed to spawn ${this.#options.command}`,
|
|
140
|
-
err,
|
|
141
|
-
error_code(err)
|
|
90
|
+
err instanceof Error ? err : undefined,
|
|
142
91
|
);
|
|
143
|
-
|
|
144
|
-
this.#emit_error(wrapped);
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
proc.on("close", () => {
|
|
149
|
-
if (!this.#initialized) {
|
|
150
|
-
reject_start(
|
|
151
|
-
start_error(
|
|
152
|
-
`LSP server ${this.#options.command} closed before initialization`
|
|
153
|
-
)
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
for (const pending of this.#pending.values()) {
|
|
157
|
-
clearTimeout(pending.timer);
|
|
158
|
-
pending.reject(new Error("LSP server closed"));
|
|
159
|
-
}
|
|
160
|
-
this.#pending.clear();
|
|
161
|
-
this.#initialized = false;
|
|
162
|
-
this.#proc = null;
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
proc.stderr?.on("data", () => {
|
|
166
|
-
// Discard stderr; many servers are chatty.
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
proc.stdout?.on("data", (chunk: Buffer) => {
|
|
170
|
-
this.#buffer = Buffer.concat([this.#buffer, chunk]);
|
|
171
|
-
this.#drain_buffer();
|
|
172
|
-
});
|
|
92
|
+
}
|
|
173
93
|
|
|
174
94
|
try {
|
|
175
|
-
await
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
hierarchicalDocumentSymbolSupport: true,
|
|
187
|
-
},
|
|
188
|
-
rename: { prepareSupport: true },
|
|
189
|
-
},
|
|
190
|
-
workspace: { workspaceFolders: true, symbol: {} },
|
|
95
|
+
await this.#request("initialize", {
|
|
96
|
+
processId: process.pid,
|
|
97
|
+
rootUri: this.#options.root_uri,
|
|
98
|
+
capabilities: {
|
|
99
|
+
textDocument: {
|
|
100
|
+
publishDiagnostics: { relatedInformation: true },
|
|
101
|
+
hover: { contentFormat: ["markdown", "plaintext"] },
|
|
102
|
+
definition: { linkSupport: false },
|
|
103
|
+
references: {},
|
|
104
|
+
documentSymbol: { hierarchicalDocumentSymbolSupport: true },
|
|
105
|
+
rename: { prepareSupport: true },
|
|
191
106
|
},
|
|
192
|
-
workspaceFolders:
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
]);
|
|
198
|
-
|
|
199
|
-
this.#notify("initialized", {});
|
|
107
|
+
workspace: { workspaceFolders: true, symbol: {} },
|
|
108
|
+
},
|
|
109
|
+
workspaceFolders: [{ uri: this.#options.root_uri, name: "workspace" }],
|
|
110
|
+
}, timeoutMs);
|
|
111
|
+
this.#protocol.notify("initialized", {});
|
|
200
112
|
this.#initialized = true;
|
|
201
|
-
|
|
202
|
-
} catch (error) {
|
|
113
|
+
} catch (err) {
|
|
203
114
|
await this.stop();
|
|
204
|
-
throw
|
|
115
|
+
throw err;
|
|
205
116
|
}
|
|
206
117
|
}
|
|
207
118
|
|
|
208
|
-
|
|
119
|
+
isReady(): boolean {
|
|
209
120
|
return this.#initialized;
|
|
210
121
|
}
|
|
211
122
|
|
|
212
|
-
|
|
213
|
-
|
|
123
|
+
/** Open or update a document in the LSP server. */
|
|
124
|
+
async ensureDocumentOpen(uri: string, text: string): Promise<void> {
|
|
125
|
+
const existing = this.#openDocs.get(uri);
|
|
126
|
+
const nextVersion = existing ? existing.version + 1 : 1;
|
|
127
|
+
this.#openDocs.set(uri, { version: nextVersion });
|
|
128
|
+
this.#diagnosticsByUri.delete(uri);
|
|
129
|
+
|
|
214
130
|
if (existing) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// cached version, diagnostics may be stale due to changes in
|
|
218
|
-
// other files. Never skip the sync based on text comparison.
|
|
219
|
-
const next_version = existing.version + 1;
|
|
220
|
-
this.#open_docs.set(uri, { version: next_version, text });
|
|
221
|
-
this.#diagnostics_by_uri.delete(uri);
|
|
222
|
-
this.#notify("textDocument/didChange", {
|
|
223
|
-
textDocument: { uri, version: next_version },
|
|
131
|
+
this.#protocol.notify("textDocument/didChange", {
|
|
132
|
+
textDocument: { uri, version: nextVersion },
|
|
224
133
|
contentChanges: [{ text }],
|
|
225
134
|
});
|
|
226
|
-
|
|
135
|
+
} else {
|
|
136
|
+
const languageId = this.#options.language_id_for_uri(uri) ?? "plaintext";
|
|
137
|
+
this.#protocol.notify("textDocument/didOpen", {
|
|
138
|
+
textDocument: { uri, languageId, version: 1, text },
|
|
139
|
+
});
|
|
227
140
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
this.#
|
|
232
|
-
|
|
233
|
-
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getDiagnostics(uri: string): LspDiagnostic[] {
|
|
144
|
+
return this.#diagnosticsByUri.get(uri) ?? [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Wait for diagnostics, with optional timeout. */
|
|
148
|
+
async waitForDiagnostics(uri: string, timeoutMs = 1500): Promise<LspDiagnostic[]> {
|
|
149
|
+
if (this.#diagnosticsByUri.has(uri)) {
|
|
150
|
+
return this.getDiagnostics(uri);
|
|
151
|
+
}
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
let active = true;
|
|
154
|
+
const handler = (event: { uri: string; diagnostics: LspDiagnostic[] }) => {
|
|
155
|
+
if (event.uri !== uri || !active) return;
|
|
156
|
+
active = false;
|
|
157
|
+
this.#protocol.off("diagnostics", handler);
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
resolve(this.getDiagnostics(uri));
|
|
160
|
+
};
|
|
161
|
+
const timer = setTimeout(() => {
|
|
162
|
+
if (!active) return;
|
|
163
|
+
active = false;
|
|
164
|
+
this.#protocol.off("diagnostics", handler);
|
|
165
|
+
resolve(this.getDiagnostics(uri));
|
|
166
|
+
}, timeoutMs);
|
|
167
|
+
this.#protocol.on("diagnostics", handler);
|
|
234
168
|
});
|
|
235
169
|
}
|
|
236
170
|
|
|
237
|
-
async hover(
|
|
238
|
-
|
|
239
|
-
position: LspPosition
|
|
240
|
-
): Promise<LspHover | null> {
|
|
241
|
-
const result = (await this.#request("textDocument/hover", {
|
|
171
|
+
async hover(uri: string, position: LspPosition, timeoutMs?: number): Promise<LspHover | null> {
|
|
172
|
+
return (await this.#request("textDocument/hover", {
|
|
242
173
|
textDocument: { uri },
|
|
243
174
|
position,
|
|
244
|
-
})) as LspHover | null;
|
|
245
|
-
return result ?? null;
|
|
175
|
+
}, timeoutMs)) as LspHover | null;
|
|
246
176
|
}
|
|
247
177
|
|
|
248
|
-
async definition(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
});
|
|
256
|
-
return normalize_location_result(result);
|
|
178
|
+
async definition(uri: string, position: LspPosition, timeoutMs?: number): Promise<LspLocation[]> {
|
|
179
|
+
return normalizeLocations(
|
|
180
|
+
await this.#request("textDocument/definition", {
|
|
181
|
+
textDocument: { uri },
|
|
182
|
+
position,
|
|
183
|
+
}, timeoutMs),
|
|
184
|
+
);
|
|
257
185
|
}
|
|
258
186
|
|
|
259
187
|
async references(
|
|
260
188
|
uri: string,
|
|
261
189
|
position: LspPosition,
|
|
262
|
-
|
|
190
|
+
includeDeclaration = true,
|
|
191
|
+
timeoutMs?: number,
|
|
263
192
|
): Promise<LspLocation[]> {
|
|
264
|
-
|
|
265
|
-
textDocument
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
193
|
+
return normalizeLocations(
|
|
194
|
+
await this.#request("textDocument/references", {
|
|
195
|
+
textDocument: { uri },
|
|
196
|
+
position,
|
|
197
|
+
context: { includeDeclaration },
|
|
198
|
+
}, timeoutMs),
|
|
199
|
+
);
|
|
270
200
|
}
|
|
271
201
|
|
|
272
|
-
async
|
|
273
|
-
|
|
274
|
-
textDocument
|
|
275
|
-
|
|
276
|
-
|
|
202
|
+
async documentSymbols(uri: string, timeoutMs?: number): Promise<LspDocumentSymbol[]> {
|
|
203
|
+
return normalizeDocumentSymbols(
|
|
204
|
+
await this.#request("textDocument/documentSymbol", {
|
|
205
|
+
textDocument: { uri },
|
|
206
|
+
}, timeoutMs),
|
|
207
|
+
);
|
|
277
208
|
}
|
|
278
209
|
|
|
279
210
|
async rename(
|
|
280
211
|
uri: string,
|
|
281
212
|
position: LspPosition,
|
|
282
|
-
newName: string
|
|
213
|
+
newName: string,
|
|
214
|
+
timeoutMs?: number,
|
|
283
215
|
): Promise<Record<string, { oldText: string; newText: string }>> {
|
|
284
216
|
const result = (await this.#request("textDocument/rename", {
|
|
285
217
|
textDocument: { uri },
|
|
286
218
|
position,
|
|
287
219
|
newName,
|
|
288
|
-
})) as
|
|
220
|
+
}, timeoutMs)) as { changes?: Record<string, Array<{ range: LspRange; newText: string }>> } | null;
|
|
289
221
|
|
|
290
|
-
// Normalize WorkspaceEdit to a simple record
|
|
291
222
|
const edits: Record<string, { oldText: string; newText: string }> = {};
|
|
292
|
-
if (result?.changes)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
} else {
|
|
300
|
-
edits[path] = {
|
|
301
|
-
oldText: change.range ? `[${change.range.start.line}:${change.range.start.character}-${change.range.end.line}:${change.range.end.character}]` : "",
|
|
302
|
-
newText: change.newText ?? "",
|
|
303
|
-
};
|
|
304
|
-
}
|
|
223
|
+
if (!result?.changes) return edits;
|
|
224
|
+
|
|
225
|
+
for (const [fileUri, changes] of Object.entries(result.changes)) {
|
|
226
|
+
const path = uriToPath(fileUri);
|
|
227
|
+
for (const change of changes) {
|
|
228
|
+
if (!edits[path]) {
|
|
229
|
+
edits[path] = { oldText: "", newText: "" };
|
|
305
230
|
}
|
|
231
|
+
edits[path].oldText += change.range
|
|
232
|
+
? `[${change.range.start.line}:${change.range.start.character}-${change.range.end.line}:${change.range.end.character}]`
|
|
233
|
+
: "";
|
|
234
|
+
edits[path].newText += change.newText ?? "";
|
|
306
235
|
}
|
|
307
236
|
}
|
|
308
237
|
return edits;
|
|
309
238
|
}
|
|
310
239
|
|
|
311
|
-
get_diagnostics(uri: string): LspDiagnostic[] {
|
|
312
|
-
return this.#diagnostics_by_uri.get(uri) ?? [];
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async wait_for_diagnostics(
|
|
316
|
-
uri: string,
|
|
317
|
-
timeout_ms: number = 1500
|
|
318
|
-
): Promise<LspDiagnostic[]> {
|
|
319
|
-
if (this.#diagnostics_by_uri.has(uri)) {
|
|
320
|
-
return this.get_diagnostics(uri);
|
|
321
|
-
}
|
|
322
|
-
return new Promise((resolve) => {
|
|
323
|
-
let active = true;
|
|
324
|
-
const cleanup = () => {
|
|
325
|
-
if (!active) return;
|
|
326
|
-
active = false;
|
|
327
|
-
this.off("diagnostics", handler);
|
|
328
|
-
clearTimeout(timer);
|
|
329
|
-
this.#diagnostic_waiters.delete(cleanup);
|
|
330
|
-
resolve(this.get_diagnostics(uri));
|
|
331
|
-
};
|
|
332
|
-
const handler = (event_uri: string) => {
|
|
333
|
-
if (event_uri !== uri) return;
|
|
334
|
-
cleanup();
|
|
335
|
-
};
|
|
336
|
-
const timer = setTimeout(cleanup, timeout_ms);
|
|
337
|
-
this.on("diagnostics", handler);
|
|
338
|
-
this.#diagnostic_waiters.add(cleanup);
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
240
|
async stop(): Promise<void> {
|
|
343
241
|
if (this.#initialized) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
} catch {
|
|
348
|
-
// Server may already be dead; proceed.
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
for (const pending of this.#pending.values()) {
|
|
352
|
-
clearTimeout(pending.timer);
|
|
353
|
-
pending.reject(new Error("LSP client stopped"));
|
|
354
|
-
}
|
|
355
|
-
this.#pending.clear();
|
|
356
|
-
for (const cleanup of Array.from(this.#diagnostic_waiters)) {
|
|
357
|
-
cleanup();
|
|
358
|
-
}
|
|
359
|
-
if (this.#proc) {
|
|
360
|
-
this.#proc.kill();
|
|
361
|
-
this.#proc = null;
|
|
362
|
-
}
|
|
363
|
-
this.#initialized = false;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
#request(
|
|
367
|
-
method: string,
|
|
368
|
-
params: unknown,
|
|
369
|
-
timeout_override?: number
|
|
370
|
-
): Promise<unknown> {
|
|
371
|
-
return new Promise((resolve, reject) => {
|
|
372
|
-
const id = this.#next_id++;
|
|
373
|
-
const timeout_ms =
|
|
374
|
-
timeout_override ?? this.#options.request_timeout_ms ?? 30_000;
|
|
375
|
-
const timer = setTimeout(() => {
|
|
376
|
-
if (this.#pending.has(id)) {
|
|
377
|
-
this.#pending.delete(id);
|
|
378
|
-
reject(new Error(`LSP request ${method} timed out`));
|
|
379
|
-
}
|
|
380
|
-
}, timeout_ms);
|
|
381
|
-
this.#pending.set(id, { resolve, reject, timer });
|
|
382
|
-
try {
|
|
383
|
-
this.#send({ jsonrpc: "2.0", id, method, params });
|
|
384
|
-
} catch (error) {
|
|
385
|
-
clearTimeout(timer);
|
|
386
|
-
this.#pending.delete(id);
|
|
387
|
-
reject(error);
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
#notify(method: string, params: unknown): void {
|
|
393
|
-
this.#send({ jsonrpc: "2.0", method, params });
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
#send(message: Record<string, unknown>): void {
|
|
397
|
-
if (!this.#proc?.stdin?.writable) {
|
|
398
|
-
throw new Error("LSP server not connected");
|
|
399
|
-
}
|
|
400
|
-
const body = Buffer.from(JSON.stringify(message), "utf8");
|
|
401
|
-
const header = Buffer.from(
|
|
402
|
-
`Content-Length: ${body.length}\r\n\r\n`,
|
|
403
|
-
"ascii"
|
|
404
|
-
);
|
|
405
|
-
this.#proc.stdin.write(Buffer.concat([header, body]));
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
#emit_error(error: Error): void {
|
|
409
|
-
if (this.listenerCount("error") > 0) {
|
|
410
|
-
this.emit("error", error);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
#drain_buffer(): void {
|
|
415
|
-
while (true) {
|
|
416
|
-
const header_end = this.#buffer.indexOf("\r\n\r\n");
|
|
417
|
-
if (header_end === -1) return;
|
|
418
|
-
const header = this.#buffer.subarray(0, header_end).toString("ascii");
|
|
419
|
-
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
420
|
-
if (!match) {
|
|
421
|
-
this.#buffer = this.#buffer.subarray(header_end + 4);
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
const length = Number(match[1]);
|
|
425
|
-
const body_start = header_end + 4;
|
|
426
|
-
if (this.#buffer.length < body_start + length) return;
|
|
427
|
-
const body = this.#buffer.subarray(body_start, body_start + length);
|
|
428
|
-
this.#buffer = this.#buffer.subarray(body_start + length);
|
|
429
|
-
try {
|
|
430
|
-
this.#handle_message(JSON.parse(body.toString("utf8")));
|
|
431
|
-
} catch (error) {
|
|
432
|
-
this.#emit_error(error as Error);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
#handle_message(message: Record<string, unknown>): void {
|
|
438
|
-
const numeric_id =
|
|
439
|
-
typeof message.id === "number"
|
|
440
|
-
? message.id
|
|
441
|
-
: typeof message.id === "string" && /^-?\d+$/.test(message.id)
|
|
442
|
-
? Number(message.id)
|
|
443
|
-
: null;
|
|
444
|
-
|
|
445
|
-
if (numeric_id != null && this.#pending.has(numeric_id)) {
|
|
446
|
-
const pending = this.#pending.get(numeric_id)!;
|
|
447
|
-
this.#pending.delete(numeric_id);
|
|
448
|
-
clearTimeout(pending.timer);
|
|
449
|
-
if (message.error) {
|
|
450
|
-
const err = message.error as Record<string, unknown>;
|
|
451
|
-
pending.reject(
|
|
452
|
-
new Error(`LSP error ${err.code}: ${err.message}`)
|
|
453
|
-
);
|
|
454
|
-
} else {
|
|
455
|
-
pending.resolve(message.result);
|
|
456
|
-
}
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (
|
|
461
|
-
message.method === "textDocument/publishDiagnostics" &&
|
|
462
|
-
message.params
|
|
463
|
-
) {
|
|
464
|
-
const params = message.params as {
|
|
465
|
-
uri: string;
|
|
466
|
-
diagnostics: LspDiagnostic[];
|
|
467
|
-
};
|
|
468
|
-
this.#diagnostics_by_uri.set(params.uri, params.diagnostics);
|
|
469
|
-
this.emit("diagnostics", params.uri);
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Respond to server-to-client requests we don't implement
|
|
474
|
-
if (message.method && message.id != null) {
|
|
475
|
-
this.#send({ jsonrpc: "2.0", id: message.id, result: null });
|
|
242
|
+
await this.#protocol.shutdown(1000);
|
|
243
|
+
} else {
|
|
244
|
+
this.#protocol.kill();
|
|
476
245
|
}
|
|
477
246
|
}
|
|
478
247
|
}
|
|
479
248
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
): LspLocation[] {
|
|
249
|
+
// ─── Result normalization ────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
function normalizeLocations(result: unknown): LspLocation[] {
|
|
483
252
|
if (!result) return [];
|
|
484
253
|
const entries = Array.isArray(result) ? result : [result];
|
|
485
254
|
return entries.map((entry: any) => {
|
|
486
|
-
if ("uri" in entry) return entry;
|
|
255
|
+
if ("uri" in entry && "range" in entry) return entry as LspLocation;
|
|
487
256
|
return {
|
|
488
257
|
uri: entry.targetUri,
|
|
489
258
|
range: entry.targetSelectionRange ?? entry.targetRange,
|
|
490
|
-
};
|
|
259
|
+
} as LspLocation;
|
|
491
260
|
});
|
|
492
261
|
}
|
|
493
262
|
|
|
494
|
-
|
|
495
|
-
result
|
|
496
|
-
|
|
497
|
-
if (
|
|
498
|
-
if (
|
|
499
|
-
(result as any[]).length === 0 ||
|
|
500
|
-
("range" in (result as any[])[0] && "selectionRange" in (result as any[])[0])
|
|
501
|
-
) {
|
|
263
|
+
function normalizeDocumentSymbols(result: unknown): LspDocumentSymbol[] {
|
|
264
|
+
if (!result || !Array.isArray(result) || result.length === 0) return [];
|
|
265
|
+
const first = result[0];
|
|
266
|
+
if ("range" in first && "selectionRange" in first) {
|
|
502
267
|
return result as LspDocumentSymbol[];
|
|
503
268
|
}
|
|
504
|
-
|
|
505
|
-
return symbol_info.map((entry) => ({
|
|
269
|
+
return result.map((entry: any) => ({
|
|
506
270
|
name: entry.name,
|
|
507
271
|
kind: entry.kind,
|
|
508
272
|
range: entry.location.range,
|
|
@@ -512,25 +276,14 @@ export function normalize_document_symbol_result(
|
|
|
512
276
|
}));
|
|
513
277
|
}
|
|
514
278
|
|
|
515
|
-
|
|
516
|
-
return pathToFileURL(file_path).href;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function file_url_to_path(uri: string): string {
|
|
279
|
+
function uriToPath(uri: string): string {
|
|
520
280
|
try {
|
|
521
|
-
return uri.startsWith("file:")
|
|
522
|
-
? new URL(uri).pathname
|
|
523
|
-
: uri;
|
|
281
|
+
return uri.startsWith("file:") ? new URL(uri).pathname : uri;
|
|
524
282
|
} catch {
|
|
525
283
|
return uri;
|
|
526
284
|
}
|
|
527
285
|
}
|
|
528
286
|
|
|
529
|
-
function
|
|
530
|
-
return
|
|
531
|
-
error !== null &&
|
|
532
|
-
"code" in error &&
|
|
533
|
-
typeof (error as Record<string, unknown>).code === "string"
|
|
534
|
-
? (error as Record<string, string>).code
|
|
535
|
-
: undefined;
|
|
287
|
+
export function filePathToUri(filePath: string): string {
|
|
288
|
+
return pathToFileURL(filePath).href;
|
|
536
289
|
}
|