@sackville-mcp/lsp 0.0.1-alpha.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/LICENSE +201 -0
- package/dist/index.d.mts +1247 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2604 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +34 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,1247 @@
|
|
|
1
|
+
import { MessageConnection } from "vscode-jsonrpc/node.js";
|
|
2
|
+
|
|
3
|
+
//#region src/encoding.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Position-encoding conversion — the single highest-correctness-risk corner of the LSP
|
|
6
|
+
* bridge (ADR 0011), and the reason this is the first slice. An LSP `Position.character`
|
|
7
|
+
* is a 0-based offset measured in **code units of the negotiated encoding**, NOT a column.
|
|
8
|
+
* JS strings are UTF-16, so a naive `col - 1` passes every ASCII test and then silently
|
|
9
|
+
* points at the WRONG symbol the moment a line contains a non-BMP character (emoji, CJK,
|
|
10
|
+
* combining marks) under a server that negotiated UTF-8. That is the worst failure for an
|
|
11
|
+
* agent tool — plausible, wrong, and silent — so the conversion is a pure, separately
|
|
12
|
+
* unit-tested function exercised against non-BMP fixtures here, before any server exists.
|
|
13
|
+
*
|
|
14
|
+
* Human columns are 1-based and count **Unicode code points** (what a person counts);
|
|
15
|
+
* LSP characters are 0-based code-unit offsets in the negotiated encoding.
|
|
16
|
+
*/
|
|
17
|
+
type PositionEncoding = 'utf-8' | 'utf-16' | 'utf-32';
|
|
18
|
+
/**
|
|
19
|
+
* The encodings we advertise to the server, in preference order. UTF-16 is first
|
|
20
|
+
* deliberately: it is the JS-native, best-tested path (a server honouring our preference
|
|
21
|
+
* picks the one we exercise most). We still implement all three for servers that only
|
|
22
|
+
* speak UTF-8 (e.g. clangd).
|
|
23
|
+
*/
|
|
24
|
+
declare const PREFERRED_ENCODINGS: readonly PositionEncoding[];
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the encoding to use from the server's reported `positionEncoding`. Absent ⇒ the
|
|
27
|
+
* spec default, UTF-16. **Present but unsupported ⇒ throw** — never silently default, or
|
|
28
|
+
* we would do offset math in the wrong unit and return wrong locations.
|
|
29
|
+
*/
|
|
30
|
+
declare function resolvePositionEncoding(reported: string | undefined): PositionEncoding;
|
|
31
|
+
/**
|
|
32
|
+
* Convert a 1-based human column (counting code points) on `lineText` to a 0-based LSP
|
|
33
|
+
* `character` offset in `encoding`. A column past the line end clamps to the line length.
|
|
34
|
+
*/
|
|
35
|
+
declare function toLspCharacter(lineText: string, humanColumn: number, encoding: PositionEncoding): number;
|
|
36
|
+
/**
|
|
37
|
+
* Inverse of {@link toLspCharacter}: map a 0-based LSP `character` offset back to a 1-based
|
|
38
|
+
* human code-point column. An offset landing inside a multi-unit code point clamps to that
|
|
39
|
+
* code point's start; an offset past the line end clamps to one past the last column.
|
|
40
|
+
*/
|
|
41
|
+
declare function fromLspCharacter(lineText: string, lspCharacter: number, encoding: PositionEncoding): number;
|
|
42
|
+
interface LspPositionParts {
|
|
43
|
+
line: number;
|
|
44
|
+
character: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Convert a human 1-based line:column over the full source `text` to a 0-based LSP
|
|
48
|
+
* `Position` in `encoding`. Lines are split on LF/CR/CRLF WITHOUT normalizing the document
|
|
49
|
+
* (the text we send the server is its source of truth); a leading BOM is stripped before
|
|
50
|
+
* line-1 column math.
|
|
51
|
+
*/
|
|
52
|
+
declare function toLspPosition(text: string, humanLine: number, humanColumn: number, encoding: PositionEncoding): LspPositionParts;
|
|
53
|
+
interface HumanPosition {
|
|
54
|
+
line: number;
|
|
55
|
+
column: number;
|
|
56
|
+
}
|
|
57
|
+
/** Inverse of {@link toLspPosition}: map an LSP `Position` back to human 1-based line:column. */
|
|
58
|
+
declare function fromLspPosition(text: string, position: LspPositionParts, encoding: PositionEncoding): HumanPosition;
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/normalize.d.ts
|
|
61
|
+
/**
|
|
62
|
+
* Pure LSP-result normalizers (ADR 0011, slice 1). LSP responses are polymorphic in ways
|
|
63
|
+
* that silently corrupt an agent-facing tool if mishandled — these reducers collapse each
|
|
64
|
+
* shape to one Sackville form, with no I/O. The traps the adversarial pass flagged:
|
|
65
|
+
*
|
|
66
|
+
* - **`Location` vs `LocationLink`.** definition/typeDefinition return
|
|
67
|
+
* `Location | Location[] | LocationLink[] | null`; `LocationLink` uses `targetUri` /
|
|
68
|
+
* `targetSelectionRange` — DIFFERENT field names than `Location`'s `uri`/`range`. Reading
|
|
69
|
+
* `.uri`/`.range` off a link yields `undefined` → silently empty navigation.
|
|
70
|
+
* - **`DocumentSymbol` vs `SymbolInformation`.** documentSymbol returns hierarchical
|
|
71
|
+
* `DocumentSymbol[]` (has `children`/`selectionRange`) OR flat `SymbolInformation[]` (has
|
|
72
|
+
* `location`) — two incompatible shapes under one method.
|
|
73
|
+
* - **Hover contents** can be `MarkupContent | MarkedString | MarkedString[]`.
|
|
74
|
+
*
|
|
75
|
+
* Ranges are kept in LSP 0-based form here; mapping them back to human 1-based line:column
|
|
76
|
+
* needs the file text + the negotiated encoding and lives in `encoding.ts`
|
|
77
|
+
* (`fromLspPosition`), applied at the surface.
|
|
78
|
+
*/
|
|
79
|
+
interface LspPosition {
|
|
80
|
+
line: number;
|
|
81
|
+
character: number;
|
|
82
|
+
}
|
|
83
|
+
interface LspRange {
|
|
84
|
+
start: LspPosition;
|
|
85
|
+
end: LspPosition;
|
|
86
|
+
}
|
|
87
|
+
interface Location {
|
|
88
|
+
uri: string;
|
|
89
|
+
range: LspRange;
|
|
90
|
+
}
|
|
91
|
+
interface LocationLink {
|
|
92
|
+
targetUri: string;
|
|
93
|
+
targetRange: LspRange;
|
|
94
|
+
targetSelectionRange: LspRange;
|
|
95
|
+
originSelectionRange?: LspRange;
|
|
96
|
+
}
|
|
97
|
+
interface NormalizedLocation {
|
|
98
|
+
uri: string;
|
|
99
|
+
/** The precise symbol range (LocationLink.targetSelectionRange, or Location.range). */
|
|
100
|
+
range: LspRange;
|
|
101
|
+
/** The full enclosing range, when the server sent a LocationLink. */
|
|
102
|
+
fullRange?: LspRange;
|
|
103
|
+
}
|
|
104
|
+
/** Normalize any definition/references/typeDefinition result shape to a flat location list. */
|
|
105
|
+
declare function normalizeLocations(result: Location | Location[] | LocationLink[] | null | undefined): NormalizedLocation[];
|
|
106
|
+
type MarkedString = string | {
|
|
107
|
+
language: string;
|
|
108
|
+
value: string;
|
|
109
|
+
};
|
|
110
|
+
interface MarkupContent {
|
|
111
|
+
kind: 'plaintext' | 'markdown';
|
|
112
|
+
value: string;
|
|
113
|
+
}
|
|
114
|
+
interface Hover {
|
|
115
|
+
contents: MarkupContent | MarkedString | MarkedString[];
|
|
116
|
+
range?: LspRange;
|
|
117
|
+
}
|
|
118
|
+
interface NormalizedHover {
|
|
119
|
+
value: string;
|
|
120
|
+
range?: LspRange;
|
|
121
|
+
}
|
|
122
|
+
/** Normalize hover contents (MarkupContent | MarkedString | MarkedString[]) to one string. */
|
|
123
|
+
declare function normalizeHover(hover: Hover | null | undefined): NormalizedHover | null;
|
|
124
|
+
declare function symbolKindName(kind: number): string;
|
|
125
|
+
interface DocumentSymbol {
|
|
126
|
+
name: string;
|
|
127
|
+
kind: number;
|
|
128
|
+
detail?: string;
|
|
129
|
+
range: LspRange;
|
|
130
|
+
selectionRange: LspRange;
|
|
131
|
+
children?: DocumentSymbol[];
|
|
132
|
+
}
|
|
133
|
+
interface SymbolInformation {
|
|
134
|
+
name: string;
|
|
135
|
+
kind: number;
|
|
136
|
+
location: Location;
|
|
137
|
+
containerName?: string;
|
|
138
|
+
}
|
|
139
|
+
interface NormalizedSymbol {
|
|
140
|
+
name: string;
|
|
141
|
+
kind: number;
|
|
142
|
+
kindName: string;
|
|
143
|
+
detail?: string;
|
|
144
|
+
range: LspRange;
|
|
145
|
+
children?: NormalizedSymbol[];
|
|
146
|
+
/** Set only for flat SymbolInformation results. */
|
|
147
|
+
container?: string;
|
|
148
|
+
}
|
|
149
|
+
/** Normalize documentSymbol's dual shape (hierarchical DocumentSymbol[] or flat SymbolInformation[]). */
|
|
150
|
+
declare function normalizeDocumentSymbols(result: DocumentSymbol[] | SymbolInformation[] | null | undefined): NormalizedSymbol[];
|
|
151
|
+
/**
|
|
152
|
+
* A raw `workspace/symbol` result member. Two shapes under one method (LSP 3.17):
|
|
153
|
+
* - `SymbolInformation`: `location` is a full `Location` (`{uri, range}`).
|
|
154
|
+
* - `WorkspaceSymbol`: `location` may be a full `Location` OR a uri-only `{uri}` — the server
|
|
155
|
+
* defers the range to a `workspaceSymbol/resolve` round-trip. Reading `.location.range` off
|
|
156
|
+
* the uri-only form yields `undefined`; we surface the uri and omit the range (v1 does not
|
|
157
|
+
* resolve — see fixtures README; the real `typescript-language-server` 5.3.0 sends the full
|
|
158
|
+
* `Location` form).
|
|
159
|
+
*/
|
|
160
|
+
interface WorkspaceSymbol {
|
|
161
|
+
name: string;
|
|
162
|
+
kind: number;
|
|
163
|
+
containerName?: string;
|
|
164
|
+
location: Location | {
|
|
165
|
+
uri: string;
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
interface NormalizedWorkspaceSymbol {
|
|
169
|
+
name: string;
|
|
170
|
+
kind: number;
|
|
171
|
+
kindName: string;
|
|
172
|
+
uri: string;
|
|
173
|
+
/** Absent when the server returned a uri-only `WorkspaceSymbol` (no range without resolve). */
|
|
174
|
+
range?: LspRange;
|
|
175
|
+
/** The declaring container (class/namespace), when the server reported one. */
|
|
176
|
+
container?: string;
|
|
177
|
+
}
|
|
178
|
+
/** Normalize a `workspace/symbol` result (flat `SymbolInformation[]`/`WorkspaceSymbol[]`). */
|
|
179
|
+
declare function normalizeWorkspaceSymbols(result: WorkspaceSymbol[] | null | undefined): NormalizedWorkspaceSymbol[];
|
|
180
|
+
declare function diagnosticSeverityName(severity: number): string;
|
|
181
|
+
/** A raw LSP `Diagnostic` (the member of a `publishDiagnostics` notification's `diagnostics`). */
|
|
182
|
+
interface Diagnostic {
|
|
183
|
+
range: LspRange;
|
|
184
|
+
message: string;
|
|
185
|
+
severity?: number;
|
|
186
|
+
code?: number | string;
|
|
187
|
+
source?: string;
|
|
188
|
+
tags?: number[];
|
|
189
|
+
relatedInformation?: {
|
|
190
|
+
location: Location;
|
|
191
|
+
message: string;
|
|
192
|
+
}[];
|
|
193
|
+
}
|
|
194
|
+
interface NormalizedRelatedInfo {
|
|
195
|
+
uri: string;
|
|
196
|
+
/** LSP 0-based range (mapped to human coords against its own file at the surface). */
|
|
197
|
+
range: LspRange;
|
|
198
|
+
message: string;
|
|
199
|
+
}
|
|
200
|
+
interface NormalizedDiagnostic {
|
|
201
|
+
/** LSP 0-based range (mapped to human coords in the queried file at the surface). */
|
|
202
|
+
range: LspRange;
|
|
203
|
+
message: string;
|
|
204
|
+
severity?: number;
|
|
205
|
+
/** `Error`/`Warning`/`Information`/`Hint`, when the server set a severity. */
|
|
206
|
+
severityName?: string;
|
|
207
|
+
code?: number | string;
|
|
208
|
+
source?: string;
|
|
209
|
+
/** `Unnecessary`/`Deprecated`, only when non-empty. */
|
|
210
|
+
tags?: string[];
|
|
211
|
+
related?: NormalizedRelatedInfo[];
|
|
212
|
+
}
|
|
213
|
+
/** Normalize a `publishDiagnostics` `diagnostics[]` (push model) — severity/tag names, related info. */
|
|
214
|
+
declare function normalizeDiagnostics(items: Diagnostic[] | null | undefined): NormalizedDiagnostic[];
|
|
215
|
+
/** A raw `CallHierarchyItem` (prepareCallHierarchy result + the node inside incoming/outgoing). */
|
|
216
|
+
interface CallHierarchyItem {
|
|
217
|
+
name: string;
|
|
218
|
+
kind: number;
|
|
219
|
+
detail?: string;
|
|
220
|
+
uri: string;
|
|
221
|
+
range: LspRange;
|
|
222
|
+
selectionRange: LspRange;
|
|
223
|
+
}
|
|
224
|
+
interface NormalizedCallItem {
|
|
225
|
+
name: string;
|
|
226
|
+
kind: number;
|
|
227
|
+
kindName: string;
|
|
228
|
+
detail?: string;
|
|
229
|
+
uri: string;
|
|
230
|
+
range: LspRange;
|
|
231
|
+
selectionRange: LspRange;
|
|
232
|
+
}
|
|
233
|
+
/** One edge of the call hierarchy: the other item + the ranges where the call occurs. */
|
|
234
|
+
interface NormalizedCall {
|
|
235
|
+
item: NormalizedCallItem;
|
|
236
|
+
fromRanges: LspRange[];
|
|
237
|
+
}
|
|
238
|
+
declare function normalizeCallHierarchyItem(item: CallHierarchyItem): NormalizedCallItem;
|
|
239
|
+
declare function normalizeCallHierarchyItems(result: CallHierarchyItem[] | null | undefined): NormalizedCallItem[];
|
|
240
|
+
/** `callHierarchy/incomingCalls` → `{from, fromRanges}[]`; the edge item is the CALLER. */
|
|
241
|
+
declare function normalizeIncomingCalls(result: {
|
|
242
|
+
from: CallHierarchyItem;
|
|
243
|
+
fromRanges: LspRange[];
|
|
244
|
+
}[] | null | undefined): NormalizedCall[];
|
|
245
|
+
/** `callHierarchy/outgoingCalls` → `{to, fromRanges}[]`; the edge item is the CALLEE. */
|
|
246
|
+
declare function normalizeOutgoingCalls(result: {
|
|
247
|
+
to: CallHierarchyItem;
|
|
248
|
+
fromRanges: LspRange[];
|
|
249
|
+
}[] | null | undefined): NormalizedCall[];
|
|
250
|
+
/**
|
|
251
|
+
* The tri-state query outcome (ADR 0011): an empty result is only authoritatively
|
|
252
|
+
* `no_result` when the server is READY; while it is still indexing an empty result is
|
|
253
|
+
* `not_ready` and must never be collapsed into "no definition" (an agent would act on the
|
|
254
|
+
* lie). Readiness is decided by the client (slice 2); this is the pure combinator.
|
|
255
|
+
*/
|
|
256
|
+
type QueryStatus = 'ok' | 'not_ready' | 'no_result';
|
|
257
|
+
declare function decideStatus(isEmpty: boolean, ready: boolean): QueryStatus;
|
|
258
|
+
/** A raw LSP `TextEdit` (or `AnnotatedTextEdit`, which adds `annotationId`). */
|
|
259
|
+
interface RawTextEdit {
|
|
260
|
+
range: LspRange;
|
|
261
|
+
newText: string;
|
|
262
|
+
annotationId?: string;
|
|
263
|
+
}
|
|
264
|
+
/** A raw `TextDocumentEdit` documentChanges member: a versioned doc + its edits. */
|
|
265
|
+
interface RawTextDocumentEdit {
|
|
266
|
+
textDocument: {
|
|
267
|
+
uri: string;
|
|
268
|
+
version?: number | null;
|
|
269
|
+
};
|
|
270
|
+
edits: RawTextEdit[];
|
|
271
|
+
}
|
|
272
|
+
/** Resource-operation options (LSP `CreateFileOptions`/`RenameFileOptions`/`DeleteFileOptions`). */
|
|
273
|
+
interface ResourceOpOptions {
|
|
274
|
+
overwrite?: boolean;
|
|
275
|
+
ignoreIfExists?: boolean;
|
|
276
|
+
ignoreIfNotExists?: boolean;
|
|
277
|
+
recursive?: boolean;
|
|
278
|
+
}
|
|
279
|
+
/** A raw file-resource operation documentChanges member (CreateFile/RenameFile/DeleteFile). */
|
|
280
|
+
interface RawResourceOperation {
|
|
281
|
+
kind: 'create' | 'rename' | 'delete';
|
|
282
|
+
uri?: string;
|
|
283
|
+
oldUri?: string;
|
|
284
|
+
newUri?: string;
|
|
285
|
+
options?: ResourceOpOptions;
|
|
286
|
+
}
|
|
287
|
+
interface RawWorkspaceEdit {
|
|
288
|
+
changes?: Record<string, RawTextEdit[]>;
|
|
289
|
+
documentChanges?: (RawTextDocumentEdit | RawResourceOperation)[];
|
|
290
|
+
changeAnnotations?: Record<string, {
|
|
291
|
+
label?: string;
|
|
292
|
+
needsConfirmation?: boolean;
|
|
293
|
+
}>;
|
|
294
|
+
}
|
|
295
|
+
/** One edit on a file, range kept in LSP 0-based form (mapped to human coords at the surface). */
|
|
296
|
+
interface NormalizedFileEdit {
|
|
297
|
+
range: LspRange;
|
|
298
|
+
newText: string;
|
|
299
|
+
/** A `needsConfirmation` change annotation rode this edit — preview-only, excluded from apply. */
|
|
300
|
+
needsConfirmation?: boolean;
|
|
301
|
+
/** The change-annotation label, when one was attached. */
|
|
302
|
+
annotationLabel?: string;
|
|
303
|
+
}
|
|
304
|
+
interface NormalizedFileEdits {
|
|
305
|
+
uri: string;
|
|
306
|
+
edits: NormalizedFileEdit[];
|
|
307
|
+
}
|
|
308
|
+
/** A flagged resource operation — surfaced in the preview (paths relativized at the surface). */
|
|
309
|
+
interface NormalizedResourceOp {
|
|
310
|
+
kind: 'create' | 'rename' | 'delete';
|
|
311
|
+
uris: string[];
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* One operation in `documentChanges` ORDER (the apply engine executes these in sequence — a
|
|
315
|
+
* "Move to file" is `create`→`edit`(new)→`delete`(old), so order is load-bearing). `files`/
|
|
316
|
+
* `resourceOps` are the order-free buckets kept for preview/back-compat.
|
|
317
|
+
*/
|
|
318
|
+
type NormalizedOp = {
|
|
319
|
+
type: 'edit';
|
|
320
|
+
uri: string;
|
|
321
|
+
edits: NormalizedFileEdit[];
|
|
322
|
+
} | {
|
|
323
|
+
type: 'create';
|
|
324
|
+
uri: string;
|
|
325
|
+
options?: ResourceOpOptions;
|
|
326
|
+
} | {
|
|
327
|
+
type: 'rename';
|
|
328
|
+
oldUri: string;
|
|
329
|
+
newUri: string;
|
|
330
|
+
options?: ResourceOpOptions;
|
|
331
|
+
} | {
|
|
332
|
+
type: 'delete';
|
|
333
|
+
uri: string;
|
|
334
|
+
options?: ResourceOpOptions;
|
|
335
|
+
};
|
|
336
|
+
interface NormalizedWorkspaceEdit {
|
|
337
|
+
files: NormalizedFileEdits[];
|
|
338
|
+
resourceOps: NormalizedResourceOp[];
|
|
339
|
+
/** The operations in `documentChanges` order (the apply engine's authority). */
|
|
340
|
+
operations: NormalizedOp[];
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Normalize a `WorkspaceEdit` to a uniform `files → edits` list plus a separate, flagged list of
|
|
344
|
+
* resource operations. Handles both shapes (ADR 0011 addendum §2.5):
|
|
345
|
+
* - `documentChanges` takes PRECEDENCE over `changes` when both are present (never merged).
|
|
346
|
+
* - Per-file and per-edit order is preserved.
|
|
347
|
+
* - `CreateFile`/`RenameFile`/`DeleteFile` members are surfaced under `resourceOps`, never
|
|
348
|
+
* translated into a TextEdit (v1 refuses to apply them).
|
|
349
|
+
* - An `AnnotatedTextEdit` is normalized to `{range,newText}`; if its annotation is
|
|
350
|
+
* `needsConfirmation` the edit carries `needsConfirmation: true` + the label (a preview-only
|
|
351
|
+
* signal, excluded from apply) so the server's safety signal is never silently dropped.
|
|
352
|
+
*
|
|
353
|
+
* NOTE: real `typescript-language-server` 5.3.0 returns the legacy `changes` map for an ordinary
|
|
354
|
+
* rename even when the client advertises `documentChanges` (see `test/fixtures/README.md`); the
|
|
355
|
+
* `documentChanges` branch is exercised by a synthesized fixture.
|
|
356
|
+
*/
|
|
357
|
+
declare function normalizeWorkspaceEdit(raw: RawWorkspaceEdit | null | undefined): NormalizedWorkspaceEdit;
|
|
358
|
+
/** `textDocument/prepareRename` raw result variants. */
|
|
359
|
+
type RawPrepareRename = LspRange | {
|
|
360
|
+
range: LspRange;
|
|
361
|
+
placeholder?: string;
|
|
362
|
+
} | {
|
|
363
|
+
defaultBehavior: boolean;
|
|
364
|
+
};
|
|
365
|
+
/** Normalized prepareRename outcome — a non-null value means the position IS renameable. */
|
|
366
|
+
interface PrepareRenameOutcome {
|
|
367
|
+
/** The renameable token range, when the server provided one. */
|
|
368
|
+
range?: LspRange;
|
|
369
|
+
/** A suggested placeholder (the existing identifier), when provided. */
|
|
370
|
+
placeholder?: string;
|
|
371
|
+
/** The server signalled default behavior (renameable; it derives the range itself). */
|
|
372
|
+
defaultBehavior?: boolean;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Normalize a `prepareRename` result. `null` ⇒ the position is NOT renameable (the engine maps
|
|
376
|
+
* this to a structured refusal, distinct from a tri-state `no_result`). A bare `Range`,
|
|
377
|
+
* `{range, placeholder}`, and `{defaultBehavior}` all mean renameable — the real
|
|
378
|
+
* `typescript-language-server` 5.3.0 returns a bare `Range` (see `test/fixtures/README.md`).
|
|
379
|
+
*/
|
|
380
|
+
declare function normalizePrepareRename(raw: RawPrepareRename | null | undefined): PrepareRenameOutcome | null;
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/client.d.ts
|
|
383
|
+
/** An operator-registry server entry: the binary + argv to spawn (structurally separate). */
|
|
384
|
+
interface ServerSpec {
|
|
385
|
+
command: string;
|
|
386
|
+
args: string[];
|
|
387
|
+
initializationOptions?: unknown;
|
|
388
|
+
}
|
|
389
|
+
/** An established connection to a language server, plus how to tear the process down. */
|
|
390
|
+
interface LspConnection {
|
|
391
|
+
connection: MessageConnection;
|
|
392
|
+
/** Hard teardown (SIGKILL + stream close) — last resort after graceful `shutdown`. */
|
|
393
|
+
dispose(): void;
|
|
394
|
+
}
|
|
395
|
+
/** Injected spawn seam — the only place a real process is created (tests inject a fake peer). */
|
|
396
|
+
type ServerSpawn = (spec: ServerSpec) => LspConnection;
|
|
397
|
+
/** Default live spawn: a child process over stdio, framed by the reference transport. */
|
|
398
|
+
declare const defaultServerSpawn: ServerSpawn;
|
|
399
|
+
interface LspClientOptions {
|
|
400
|
+
/** OPERATOR per-request wall-clock cap (ms) — the single authoritative deadline. */
|
|
401
|
+
timeoutMs: number;
|
|
402
|
+
/** Single-attempt mode: no retry/backoff (the gate's deterministic default). */
|
|
403
|
+
noRetry?: boolean;
|
|
404
|
+
/** Injected clock. Default `Date.now`. */
|
|
405
|
+
now?: () => number;
|
|
406
|
+
/** Injected cancellable delay. Default a `setTimeout` promise (the one seam that may). */
|
|
407
|
+
delay?: (ms: number) => Promise<void>;
|
|
408
|
+
/** Base backoff (ms) for the empty-result retry. Default 50. */
|
|
409
|
+
baseBackoffMs?: number;
|
|
410
|
+
/** Max backoff (ms). Default 1000. */
|
|
411
|
+
maxBackoffMs?: number;
|
|
412
|
+
}
|
|
413
|
+
/** Server provenance from `initialize` — `serverInfo`, optional per the protocol. */
|
|
414
|
+
interface ServerInfo {
|
|
415
|
+
name: string;
|
|
416
|
+
version?: string;
|
|
417
|
+
}
|
|
418
|
+
/** A call-hierarchy group: one prepared source item + the calls in the requested direction. */
|
|
419
|
+
interface CallHierarchyGroup {
|
|
420
|
+
source: NormalizedCallItem;
|
|
421
|
+
calls: NormalizedCall[];
|
|
422
|
+
}
|
|
423
|
+
type CallDirection = 'incoming' | 'outgoing';
|
|
424
|
+
/** A navigation result with its tri-state status and version/encoding provenance. */
|
|
425
|
+
interface NavResult<T> {
|
|
426
|
+
status: QueryStatus;
|
|
427
|
+
result: T;
|
|
428
|
+
serverInfo?: ServerInfo;
|
|
429
|
+
encoding: PositionEncoding;
|
|
430
|
+
}
|
|
431
|
+
interface InitializeSummary {
|
|
432
|
+
encoding: PositionEncoding;
|
|
433
|
+
serverInfo?: ServerInfo;
|
|
434
|
+
capabilities: ServerCapabilities;
|
|
435
|
+
}
|
|
436
|
+
/** The subset of `ServerCapabilities` this slice reads (any non-`false`/non-absent ⇒ enabled). */
|
|
437
|
+
type ServerCapabilities = Record<string, unknown>;
|
|
438
|
+
/** Thrown when a navigation is requested against a capability the server did not advertise. */
|
|
439
|
+
declare class LspUnsupportedError extends Error {
|
|
440
|
+
constructor(message: string);
|
|
441
|
+
}
|
|
442
|
+
declare class LspClient {
|
|
443
|
+
private readonly conn;
|
|
444
|
+
private readonly timeoutMs;
|
|
445
|
+
private readonly noRetry;
|
|
446
|
+
private readonly now;
|
|
447
|
+
private readonly delay;
|
|
448
|
+
private readonly baseBackoffMs;
|
|
449
|
+
private readonly maxBackoffMs;
|
|
450
|
+
private listening;
|
|
451
|
+
private _encoding;
|
|
452
|
+
private _serverInfo;
|
|
453
|
+
private _capabilities;
|
|
454
|
+
/** Active indexing work-done-progress tokens — non-empty ⇒ the server is still indexing. */
|
|
455
|
+
private readonly activeProgress;
|
|
456
|
+
/** Resolvers waiting for indexing to DRAIN (the active set to empty); fired on the final `end`. */
|
|
457
|
+
private readonly drainWaiters;
|
|
458
|
+
/**
|
|
459
|
+
* Open documents (open-once, no `didClose` by default). `version` is the per-uri monotonic
|
|
460
|
+
* document version: seeded at 1 by `didOpen`, pre-incremented by each `applyEdited` `didChange`
|
|
461
|
+
* — versions MUST strictly increase or the server ignores the change and keeps stale text.
|
|
462
|
+
*/
|
|
463
|
+
private readonly open;
|
|
464
|
+
/**
|
|
465
|
+
* Pushed diagnostics per uri (the PUSH model — `textDocument/publishDiagnostics` is a server
|
|
466
|
+
* notification, not a request; tsserver advertises no `diagnosticProvider`, so pull diagnostics
|
|
467
|
+
* are unavailable). An entry's absence ⇒ the server hasn't published for that uri yet.
|
|
468
|
+
*/
|
|
469
|
+
private readonly diagnostics;
|
|
470
|
+
/** Uris freshly `didOpen`ed whose first post-open publish hasn't arrived yet. */
|
|
471
|
+
private readonly awaitingDiagnostics;
|
|
472
|
+
/** Resolvers waiting for the NEXT publish on a uri (keyed); fired by the publish handler. */
|
|
473
|
+
private readonly diagnosticsWaiters;
|
|
474
|
+
constructor(connection: MessageConnection, options: LspClientOptions);
|
|
475
|
+
get encoding(): PositionEncoding;
|
|
476
|
+
get serverInfo(): ServerInfo | undefined;
|
|
477
|
+
get capabilities(): ServerCapabilities;
|
|
478
|
+
/** Indexing is active when at least one `$/progress` work-done token is open. */
|
|
479
|
+
get indexing(): boolean;
|
|
480
|
+
/** Any non-`false`, non-absent `*Provider` value counts as enabled (LSP convention). */
|
|
481
|
+
supports(provider: string): boolean;
|
|
482
|
+
/**
|
|
483
|
+
* `prepareRename` is advertised only by the OBJECT form `renameProvider: {prepareProvider:true}`
|
|
484
|
+
* — the boolean `supports()` helper cannot detect it, so the engine must check this before
|
|
485
|
+
* calling `prepareRename` (bare `renameProvider: true` supports rename but not prepare).
|
|
486
|
+
*/
|
|
487
|
+
get supportsPrepareRename(): boolean;
|
|
488
|
+
/**
|
|
489
|
+
* Whether the server accepts `workspace/didChangeWorkspaceFolders` — the gate for the manager's
|
|
490
|
+
* grow-only warm-server reuse. Advertised by `ServerCapabilities.workspace.workspaceFolders`:
|
|
491
|
+
* `supported !== false` AND `changeNotifications` truthy (the spec allows `true` OR a string
|
|
492
|
+
* registration id). rust-analyzer advertises it; the captured tsserver 5.3.0 does NOT, so the
|
|
493
|
+
* manager falls back to spawning a fresh per-group server there.
|
|
494
|
+
*/
|
|
495
|
+
get supportsWorkspaceFolderChange(): boolean;
|
|
496
|
+
/**
|
|
497
|
+
* Handshake. Registers the deadlock-safe inbound handlers + the `$/progress` listener
|
|
498
|
+
* BEFORE `listen()`, advertises the preferred encodings, then reads back the negotiated
|
|
499
|
+
* encoding + capabilities + provenance and sends `initialized`.
|
|
500
|
+
*/
|
|
501
|
+
initialize(rootUri: string, opts?: {
|
|
502
|
+
initializationOptions?: unknown; /** Multi-root workspace folders. Defaults to the single `[{rootUri, 'root'}]` (ADR 0011 tail). */
|
|
503
|
+
workspaceFolders?: {
|
|
504
|
+
uri: string;
|
|
505
|
+
name: string;
|
|
506
|
+
}[];
|
|
507
|
+
}): Promise<InitializeSummary>;
|
|
508
|
+
/** Register the id-bearing inbound replies (deadlock-safe) + the `$/progress` tracker. */
|
|
509
|
+
private installInboundHandlers;
|
|
510
|
+
/** A promise that resolves the next time indexing drains to empty (an `end` clears the set). */
|
|
511
|
+
private indexingDrain;
|
|
512
|
+
/**
|
|
513
|
+
* Wait until the server is NOT indexing, bounded by `deadline`. Returns true if it settled,
|
|
514
|
+
* false if the deadline elapsed while still indexing. Event-driven (resolves on the `$/progress`
|
|
515
|
+
* `end`), with the injected `delay` as the deadline backstop — so the gate drives it
|
|
516
|
+
* deterministically and a real session blocks no longer than the operator timeout.
|
|
517
|
+
*/
|
|
518
|
+
private awaitIndexingSettled;
|
|
519
|
+
/** Open a document full-text once (version 1); subsequent calls just bump the refcount. */
|
|
520
|
+
ensureOpen(uri: string, languageId: string, text: string): void;
|
|
521
|
+
/** Decrement a document's refcount (keeps the entry + version). Does NOT `didClose`. */
|
|
522
|
+
releaseDoc(uri: string): void;
|
|
523
|
+
/**
|
|
524
|
+
* After Sackville writes `newText` to `uri` on disk (write-mode, ADR 0011 addendum), resync the
|
|
525
|
+
* server's in-memory buffer with a **full-text `didChange`** so a later navigation sees
|
|
526
|
+
* post-rename positions (we never `didClose`, so the server still holds the pre-rename text).
|
|
527
|
+
* The version is **pre-incremented** — it must be strictly greater than the last `didOpen`/
|
|
528
|
+
* `didChange` or the server ignores the change. No-op for a uri the server never opened (it
|
|
529
|
+
* re-reads fresh on the next `didOpen`). Full-text (no incremental ranges) — correctness over
|
|
530
|
+
* bytes, and it avoids re-introducing offset math on the server-bound path.
|
|
531
|
+
*/
|
|
532
|
+
applyEdited(uri: string, newText: string): void;
|
|
533
|
+
/**
|
|
534
|
+
* A file moved on disk (resource-op `RenameFile`). The open-once/no-`didClose` invariant keys the
|
|
535
|
+
* server buffer by `oldUri`, which now names a non-existent path — a later query would be silently
|
|
536
|
+
* wrong (the worst failure class). MIGRATE the open entry: `didClose(oldUri)` + `didOpen(newUri)`
|
|
537
|
+
* with the moved text, carrying the refcount and the languageId to the new key. NO-OP if `oldUri`
|
|
538
|
+
* was never opened (Sackville opens only the queried file, so the renamed file is usually closed).
|
|
539
|
+
* Run inside the held multi-URI lock so no concurrent query races the key migration.
|
|
540
|
+
*/
|
|
541
|
+
didFileRename(oldUri: string, newUri: string, newText: string): void;
|
|
542
|
+
/**
|
|
543
|
+
* A file was deleted on disk (resource-op `DeleteFile`). `didClose` + evict the open entry so the
|
|
544
|
+
* server stops tracking a buffer for a path that no longer exists. NO-OP if it was never opened.
|
|
545
|
+
*/
|
|
546
|
+
didFileDelete(uri: string): void;
|
|
547
|
+
/**
|
|
548
|
+
* Notify the server that the workspace-folder set changed (`workspace/didChangeWorkspaceFolders`)
|
|
549
|
+
* — the wire primitive behind the manager's grow-only warm-server reuse. A fire-and-forget
|
|
550
|
+
* notification, so it is ordered on the single connection BEFORE any subsequent request: a query
|
|
551
|
+
* sent right after sees the new folder scope. Capability gating is the caller's job (the manager
|
|
552
|
+
* only calls this when {@link supportsWorkspaceFolderChange}); the folders are operator-allowlisted
|
|
553
|
+
* roots, never agent-supplied paths.
|
|
554
|
+
*/
|
|
555
|
+
changeWorkspaceFolders(added: {
|
|
556
|
+
uri: string;
|
|
557
|
+
name: string;
|
|
558
|
+
}[], removed: {
|
|
559
|
+
uri: string;
|
|
560
|
+
name: string;
|
|
561
|
+
}[]): void;
|
|
562
|
+
definition(uri: string, position: {
|
|
563
|
+
line: number;
|
|
564
|
+
character: number;
|
|
565
|
+
}): Promise<NavResult<NormalizedLocation[]>>;
|
|
566
|
+
typeDefinition(uri: string, position: {
|
|
567
|
+
line: number;
|
|
568
|
+
character: number;
|
|
569
|
+
}): Promise<NavResult<NormalizedLocation[]>>;
|
|
570
|
+
references(uri: string, position: {
|
|
571
|
+
line: number;
|
|
572
|
+
character: number;
|
|
573
|
+
}): Promise<NavResult<NormalizedLocation[]>>;
|
|
574
|
+
hover(uri: string, position: {
|
|
575
|
+
line: number;
|
|
576
|
+
character: number;
|
|
577
|
+
}): Promise<NavResult<NormalizedHover | null>>;
|
|
578
|
+
/** Document symbols — the file outline. Position-less (whole document); tri-state like the rest. */
|
|
579
|
+
documentSymbols(uri: string): Promise<NavResult<NormalizedSymbol[]>>;
|
|
580
|
+
/** A promise that resolves on the next `publishDiagnostics` for `uri`. */
|
|
581
|
+
private nextDiagnostics;
|
|
582
|
+
/**
|
|
583
|
+
* Diagnostics for an OPEN document (ADR 0011 staged tail; PUSH model). NOT capability-gated —
|
|
584
|
+
* `textDocument/publishDiagnostics` is a server notification every server may send, and tsserver
|
|
585
|
+
* advertises no `diagnosticProvider` (pull diagnostics are staged). The caller (manager.run) has
|
|
586
|
+
* already `didOpen`ed the file, which triggers the server's publish.
|
|
587
|
+
*
|
|
588
|
+
* Readiness (grounded in the captured timeline — `didOpen` → `$/progress` begin/end → publish
|
|
589
|
+
* ~60ms AFTER the project loads): wait out the project-load `$/progress`, then return the publish
|
|
590
|
+
* once the file's first post-open publish has arrived. An EMPTY publish is a legitimate `ok` (a
|
|
591
|
+
* clean file), never `no_result`. If the project never settles or no publish arrives within the
|
|
592
|
+
* deadline ⇒ `not_ready` (retry), the same honest-tri-state posture as the navigation reads.
|
|
593
|
+
*/
|
|
594
|
+
documentDiagnostics(uri: string): Promise<NavResult<NormalizedDiagnostic[]>>;
|
|
595
|
+
/** PUSH model — accumulate the server's `publishDiagnostics` for an open file (see {@link documentDiagnostics}). */
|
|
596
|
+
private pushDiagnostics;
|
|
597
|
+
/**
|
|
598
|
+
* PULL model — `textDocument/diagnostic` (LSP 3.17), capability-gated on `diagnosticProvider`.
|
|
599
|
+
* A request/response (unlike push), so it is deterministic for a single-shot read. Echoes the
|
|
600
|
+
* provider's `identifier` when one was advertised (rust-analyzer requires it). Tri-state, with
|
|
601
|
+
* the diagnostics-specific twist that an EMPTY report is a legitimate `ok` (a clean file), NEVER
|
|
602
|
+
* `no_result` — so this does NOT reuse {@link withRetry} (whose empty ⇒ no_result is wrong here).
|
|
603
|
+
* Readiness: wait out the project-load `$/progress`; if the send (re)starts indexing, re-query the
|
|
604
|
+
* loaded project within the deadline; a soft "not ready" `ResponseError` backs off and retries
|
|
605
|
+
* inside the deadline. Still indexing at the deadline ⇒ `not_ready` (retry).
|
|
606
|
+
*/
|
|
607
|
+
private pullDiagnostics;
|
|
608
|
+
/**
|
|
609
|
+
* `workspace/symbol` — project-wide symbol search by name (ADR 0011 staged tail). Position-less
|
|
610
|
+
* and file-less: the query is just a name fragment matched against the whole indexed workspace,
|
|
611
|
+
* so it needs no open document (the project is loaded at `initialize`). Tri-state like the rest —
|
|
612
|
+
* an empty result while the project is still indexing is `not_ready`, never collapsed into
|
|
613
|
+
* "no such symbol" (the cold-load trap the rest of the client already guards). Handles both the
|
|
614
|
+
* flat `SymbolInformation[]` (range present) and the uri-only `WorkspaceSymbol[]` shapes.
|
|
615
|
+
*/
|
|
616
|
+
workspaceSymbols(query: string): Promise<NavResult<NormalizedWorkspaceSymbol[]>>;
|
|
617
|
+
/**
|
|
618
|
+
* `textDocument/prepareRename` — the cheap validate-first pre-flight (write-mode, ADR 0011
|
|
619
|
+
* addendum). Tri-state: `null` while indexing ⇒ `not_ready`; `null` while ready ⇒ `no_result`
|
|
620
|
+
* (the engine maps that to a structured "not renameable here" refusal); a non-null outcome ⇒
|
|
621
|
+
* `ok`. Only callable when {@link supportsPrepareRename} (the engine skips it otherwise).
|
|
622
|
+
*/
|
|
623
|
+
prepareRename(uri: string, position: {
|
|
624
|
+
line: number;
|
|
625
|
+
character: number;
|
|
626
|
+
}): Promise<NavResult<PrepareRenameOutcome | null>>;
|
|
627
|
+
/**
|
|
628
|
+
* `textDocument/rename` — compute the cross-file `WorkspaceEdit` for renaming the symbol at
|
|
629
|
+
* `position` to `newName`. Capability-gated on `renameProvider`; normalized to the uniform
|
|
630
|
+
* `files`/`resourceOps` shape; tri-state (empty/`null` while indexing ⇒ `not_ready`). This
|
|
631
|
+
* computes only — applying the edit to disk is the gated engine's job (Slice F), never here.
|
|
632
|
+
*/
|
|
633
|
+
rename(uri: string, position: {
|
|
634
|
+
line: number;
|
|
635
|
+
character: number;
|
|
636
|
+
}, newName: string): Promise<NavResult<NormalizedWorkspaceEdit>>;
|
|
637
|
+
/**
|
|
638
|
+
* Call hierarchy — a TWO-round-trip protocol. `prepareCallHierarchy` resolves the symbol at
|
|
639
|
+
* `position` to one or more `CallHierarchyItem`s (null vs empty is distinct; overloads yield
|
|
640
|
+
* MANY — we keep them all, never silently the first); then per item we fetch incoming or
|
|
641
|
+
* outgoing calls. Tri-state lives on the PREPARE step (empty-while-indexing ⇒ not_ready). A
|
|
642
|
+
* prepared item with no callers/callees is a legitimate `ok` with empty `calls`. The RAW item
|
|
643
|
+
* is passed back to the calls request (it may carry an opaque `data` field the server needs).
|
|
644
|
+
*/
|
|
645
|
+
callHierarchy(uri: string, position: {
|
|
646
|
+
line: number;
|
|
647
|
+
character: number;
|
|
648
|
+
}, direction: CallDirection): Promise<NavResult<CallHierarchyGroup[]>>;
|
|
649
|
+
private navigateLocations;
|
|
650
|
+
/**
|
|
651
|
+
* The tri-state request loop: settle → send → decide, all inside one operator deadline.
|
|
652
|
+
*
|
|
653
|
+
* The load-bearing rule (ADR 0011 addendum — proven by a live `typescript-language-server`
|
|
654
|
+
* capture): **a result returned while the server is still indexing the project is NOT
|
|
655
|
+
* trustworthy** — tsserver answers an early request from a single-file *inferred* project
|
|
656
|
+
* (a non-empty BUT PARTIAL answer — e.g. a cross-file rename that sees only the opened file)
|
|
657
|
+
* and only *then* finishes loading the configured project. So:
|
|
658
|
+
*
|
|
659
|
+
* 1. Before sending, wait out any in-flight indexing (`awaitIndexingSettled`) so we hit the
|
|
660
|
+
* loaded project.
|
|
661
|
+
* 2. After sending, if indexing is active (the send itself triggered the configured-project
|
|
662
|
+
* load), the answer is from the unstable inferred project — loop to settle + re-query.
|
|
663
|
+
* 3. Once the server is settled: a non-empty result is `ok`; an empty result is `no_result`
|
|
664
|
+
* (retried with bounded backoff) — or `not_ready` only if we hit the deadline still indexing.
|
|
665
|
+
*
|
|
666
|
+
* This trades the old "return `not_ready` fast" for "wait for the correct answer within the
|
|
667
|
+
* deadline" — correctness over latency, bounded by the operator timeout.
|
|
668
|
+
*/
|
|
669
|
+
private withRetry;
|
|
670
|
+
private wrap;
|
|
671
|
+
/** Graceful teardown: LSP `shutdown` request then the `exit` notification. */
|
|
672
|
+
shutdown(): Promise<void>;
|
|
673
|
+
}
|
|
674
|
+
//#endregion
|
|
675
|
+
//#region src/registry.d.ts
|
|
676
|
+
/**
|
|
677
|
+
* The operator-bound language→server registry (ADR 0011). Safety-critical: the agent supplies
|
|
678
|
+
* only a `language` string and NEVER a binary, argv, or path. The operator binds the registry
|
|
679
|
+
* out-of-band (`SACKVILLE_LSP_SERVERS`, JSON); a language absent from it is refused, never
|
|
680
|
+
* spawned.
|
|
681
|
+
*
|
|
682
|
+
* The registry is **JSON with `command` and `args[]` structurally separate** — deliberately
|
|
683
|
+
* NOT a `lang=cmd args;…` mini-DSL the engine would re-split, because real server commands
|
|
684
|
+
* routinely contain spaces, `=`, and wrapper prefixes (`rustup run stable rust-analyzer`).
|
|
685
|
+
* Splitting a string would corrupt those; an explicit array cannot.
|
|
686
|
+
*/
|
|
687
|
+
/** One operator-registered language server: the binary + argv, kept structurally separate. */
|
|
688
|
+
interface ServerRegistryEntry {
|
|
689
|
+
command: string;
|
|
690
|
+
args: string[];
|
|
691
|
+
/** Passed verbatim as the LSP `initialize` `initializationOptions` (hardening flags, etc.). */
|
|
692
|
+
initializationOptions?: unknown;
|
|
693
|
+
/** The `languageId` sent in `didOpen`; defaults to the registry key when omitted. */
|
|
694
|
+
languageId?: string;
|
|
695
|
+
}
|
|
696
|
+
/** The full registry, keyed by the agent-facing `language` string. */
|
|
697
|
+
type ServerRegistry = Record<string, ServerRegistryEntry>;
|
|
698
|
+
/** Thrown on a malformed operator registry or a request for an unbound language. */
|
|
699
|
+
declare class LspRegistryError extends Error {
|
|
700
|
+
constructor(message: string);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Parse + validate the operator JSON registry. Fails loud on anything malformed — an
|
|
704
|
+
* operator misconfiguration must be a clear error, never a silently-empty or
|
|
705
|
+
* partially-parsed registry an agent then queries against.
|
|
706
|
+
*/
|
|
707
|
+
declare function parseServerRegistry(json: string): ServerRegistry;
|
|
708
|
+
/** Resolve a language to its registered server, or refuse it (never spawns an unbound server). */
|
|
709
|
+
declare function resolveServer(registry: ServerRegistry, language: string): ServerRegistryEntry;
|
|
710
|
+
//#endregion
|
|
711
|
+
//#region src/manager.d.ts
|
|
712
|
+
/** Thrown when the manager refuses a request (root outside the allowlist, server cap reached). */
|
|
713
|
+
declare class LspManagerError extends Error {
|
|
714
|
+
constructor(message: string);
|
|
715
|
+
}
|
|
716
|
+
interface LanguageServerManagerOptions {
|
|
717
|
+
/** The operator-bound language→server registry (the agent picks only a language). */
|
|
718
|
+
registry: ServerRegistry;
|
|
719
|
+
/** OPERATOR allowlist of project roots a server may be initialized against. */
|
|
720
|
+
allowedRoots: string[];
|
|
721
|
+
/** Per-request wall-clock cap (ms) handed to each `LspClient`. */
|
|
722
|
+
timeoutMs: number;
|
|
723
|
+
/** Injected spawn seam. Default {@link defaultServerSpawn} (real `child_process.spawn`). */
|
|
724
|
+
serverSpawn?: ServerSpawn;
|
|
725
|
+
/** Idle time before a server is eligible for reaping, in ms. Default 15 min (warm servers). */
|
|
726
|
+
idleTtlMs?: number;
|
|
727
|
+
/** Max concurrent servers. Default 8. */
|
|
728
|
+
maxServers?: number;
|
|
729
|
+
/** Grace (ms) between the LSP `shutdown`/`exit` and the hard `dispose()`. Default 2000. */
|
|
730
|
+
shutdownGraceMs?: number;
|
|
731
|
+
/** Single-attempt client mode (the gate's deterministic default; production retries). */
|
|
732
|
+
noRetry?: boolean;
|
|
733
|
+
/** Injected clock. Default `Date.now`. */
|
|
734
|
+
now?: () => number;
|
|
735
|
+
/** Injected delay (the shutdown grace). Default a `setTimeout` promise. */
|
|
736
|
+
delay?: (ms: number) => Promise<void>;
|
|
737
|
+
}
|
|
738
|
+
interface QueryInput {
|
|
739
|
+
/** The agent-facing language id (resolved against the operator registry). */
|
|
740
|
+
language: string;
|
|
741
|
+
/** The primary project root — where the queried file lives; must be in `allowedRoots`. */
|
|
742
|
+
projectRoot: string;
|
|
743
|
+
/** The document URI being queried (`file://…`). */
|
|
744
|
+
uri: string;
|
|
745
|
+
/** The document's full text (sent verbatim in `didOpen`). */
|
|
746
|
+
text: string;
|
|
747
|
+
/**
|
|
748
|
+
* Additional allowlisted roots bound as workspace folders on the SAME server (multi-root, ADR
|
|
749
|
+
* 0011 tail). The server is keyed by the sorted root GROUP, so one server handles the whole
|
|
750
|
+
* group and cross-root navigation resolves. Each must be in `allowedRoots`. Default: none.
|
|
751
|
+
*/
|
|
752
|
+
workspaceRoots?: string[];
|
|
753
|
+
}
|
|
754
|
+
/** Provenance for one live server — what `lsp_languages` surfaces (never the command/path). */
|
|
755
|
+
interface ServerDescription {
|
|
756
|
+
language: string;
|
|
757
|
+
projectRoot: string;
|
|
758
|
+
/** The full root group, set only for a multi-root server (length > 1). */
|
|
759
|
+
roots?: string[];
|
|
760
|
+
serverInfo?: {
|
|
761
|
+
name: string;
|
|
762
|
+
version?: string;
|
|
763
|
+
};
|
|
764
|
+
capabilities: Record<string, boolean>;
|
|
765
|
+
}
|
|
766
|
+
declare class LanguageServerManager {
|
|
767
|
+
private readonly registry;
|
|
768
|
+
private readonly allowedRoots;
|
|
769
|
+
private readonly timeoutMs;
|
|
770
|
+
private readonly serverSpawn;
|
|
771
|
+
private readonly idleTtlMs;
|
|
772
|
+
private readonly maxServers;
|
|
773
|
+
private readonly shutdownGraceMs;
|
|
774
|
+
private readonly noRetry;
|
|
775
|
+
private readonly now;
|
|
776
|
+
private readonly delay;
|
|
777
|
+
private readonly servers;
|
|
778
|
+
/** In-flight spawn+initialize, so concurrent first callers share one initialization. */
|
|
779
|
+
private readonly initing;
|
|
780
|
+
private reaper;
|
|
781
|
+
constructor(options: LanguageServerManagerOptions);
|
|
782
|
+
get serverCount(): number;
|
|
783
|
+
/**
|
|
784
|
+
* Run `fn` against the (shared, lazily-spawned) server for `(language, projectRoot)`, under
|
|
785
|
+
* the per-`(server, uri)` mutex. The document is opened once (refcounted in the client) and
|
|
786
|
+
* the server's idle clock is reset on entry and exit; `inFlight` is held for the duration so
|
|
787
|
+
* the reaper cannot tear the server down mid-request.
|
|
788
|
+
*/
|
|
789
|
+
run<T>(input: QueryInput, fn: (client: LspClient) => Promise<T>): Promise<T>;
|
|
790
|
+
/**
|
|
791
|
+
* Like {@link run}, but holds the per-`(server, uri)` lock for MANY uris at once — the
|
|
792
|
+
* primitive a multi-file write needs (Slice F′). Locks are acquired in a deterministic SORTED
|
|
793
|
+
* order so two concurrent multi-file renames can never deadlock (no lock-ordering cycle), and
|
|
794
|
+
* are all held across the whole `fn` (the stage+commit+`didChange` window). Does NOT `didOpen`
|
|
795
|
+
* any document — the caller opened what it needs during the compute phase (open-once/refcount).
|
|
796
|
+
*/
|
|
797
|
+
runWithUris<T>(input: {
|
|
798
|
+
language: string;
|
|
799
|
+
projectRoot: string;
|
|
800
|
+
uris: string[];
|
|
801
|
+
workspaceRoots?: string[];
|
|
802
|
+
}, fn: (client: LspClient) => Promise<T>): Promise<T>;
|
|
803
|
+
/**
|
|
804
|
+
* The resolved, sorted, de-duplicated root group for a query — the primary `projectRoot` plus any
|
|
805
|
+
* `workspaceRoots`. EVERY member is `assertRootAllowed`'d (refused before any spawn), so a
|
|
806
|
+
* multi-root group cannot smuggle in an un-allowlisted folder.
|
|
807
|
+
*/
|
|
808
|
+
private resolveGroup;
|
|
809
|
+
/** Resolve (and lazily spawn+initialize) the shared server for `(language, root group)`. */
|
|
810
|
+
private acquire;
|
|
811
|
+
/**
|
|
812
|
+
* Find the UNIQUELY-largest warm server whose root group is a strict subset of `roots` (same
|
|
813
|
+
* language, and it must advertise workspace-folder change support), grow it to `roots` in place,
|
|
814
|
+
* and re-key it. Returns the grown entry, or `undefined` when there is no candidate or the largest
|
|
815
|
+
* subset is ambiguous (≥2 candidates tie for the most roots) — in which case we spawn fresh rather
|
|
816
|
+
* than guess which to mutate. Grow-only: a subset request of an existing LARGER server is NOT
|
|
817
|
+
* shrunk (it spawns its own server) — keeping keys, `describe()`, and confinement unambiguous.
|
|
818
|
+
*/
|
|
819
|
+
private tryGrowExisting;
|
|
820
|
+
private spawnAndInit;
|
|
821
|
+
/** Provenance for every currently-live server (drives the always-on `lsp_languages` tool). */
|
|
822
|
+
describe(): ServerDescription[];
|
|
823
|
+
private assertRootAllowed;
|
|
824
|
+
/** Serialize `fn` against all other callers holding the same `(server, uri)` lock. */
|
|
825
|
+
private withUriLock;
|
|
826
|
+
/**
|
|
827
|
+
* Acquire the per-uri locks for EVERY uri (deduped, SORTED — deadlock-free ordering) and hold
|
|
828
|
+
* them all across `fn`, releasing in reverse on exit. Each lock chains on the same per-uri
|
|
829
|
+
* promise the single-uri `withUriLock` uses, so a multi-file write serializes correctly against
|
|
830
|
+
* any concurrent single-file query on one of its files.
|
|
831
|
+
*/
|
|
832
|
+
private withUriLocks;
|
|
833
|
+
/**
|
|
834
|
+
* Reap every server idle for at least `idleTtlMs` that has NO in-flight request. Returns the
|
|
835
|
+
* reaped keys. Drives the deterministic reaper logic; `nowMs` defaults to the injected clock.
|
|
836
|
+
*/
|
|
837
|
+
sweepIdle(nowMs?: number): Promise<string[]>;
|
|
838
|
+
private reap;
|
|
839
|
+
/** LSP `shutdown` → `exit` (graceful), then a clock-driven grace, then the hard `dispose()`. */
|
|
840
|
+
private gracefulStop;
|
|
841
|
+
/** Start the production idle reaper (a `setInterval` driving the injected-clock `sweepIdle`). */
|
|
842
|
+
startReaper(intervalMs: number): void;
|
|
843
|
+
stopReaper(): void;
|
|
844
|
+
/** Gracefully stop and dispose every server; the manager can be reused afterward. */
|
|
845
|
+
shutdown(): Promise<void>;
|
|
846
|
+
}
|
|
847
|
+
//#endregion
|
|
848
|
+
//#region src/confine.d.ts
|
|
849
|
+
/**
|
|
850
|
+
* The shared paired-gate + path-confinement guards for the LSP engines (ADR 0011). Factored
|
|
851
|
+
* out of `query.ts` so the read engine (`LspQueryEngine`) and the write engine
|
|
852
|
+
* (`LspRenameEngine`, Slice F) share one implementation — with a `resolveSymlinks` mode that
|
|
853
|
+
* the WRITE path always sets.
|
|
854
|
+
*
|
|
855
|
+
* The read path confines the single queried file lexically (lower stakes). The write path must
|
|
856
|
+
* confine EVERY file a `WorkspaceEdit` would touch, and lexically is not enough: `resolve()`
|
|
857
|
+
* does not canonicalize symlinks, so a symlink INSIDE the root pointing OUTSIDE it passes a
|
|
858
|
+
* prefix check and a write would clobber the out-of-root target. The write path therefore
|
|
859
|
+
* `realpath`-canonicalizes the root and each target's nearest existing ancestor and re-asserts
|
|
860
|
+
* containment, and refuses any non-`file://` scheme. Confinement is pure path/metadata work —
|
|
861
|
+
* it runs BEFORE any target file content is read.
|
|
862
|
+
*/
|
|
863
|
+
/** Thrown when the paired operator gate denies a query/edit (allowRun off, out-of-bounds, bad scheme). */
|
|
864
|
+
declare class LspGateError extends Error {
|
|
865
|
+
constructor(message: string);
|
|
866
|
+
}
|
|
867
|
+
//#endregion
|
|
868
|
+
//#region src/query.d.ts
|
|
869
|
+
/** Reads a file's text, or `undefined` if it cannot be read. */
|
|
870
|
+
type FileReader = (absolutePath: string) => string | undefined;
|
|
871
|
+
interface LspQueryEngineOptions {
|
|
872
|
+
manager: LanguageServerManager;
|
|
873
|
+
/** OPERATOR opt-in to run navigation (which requires a live indexing daemon). Deny-by-default. */
|
|
874
|
+
allowRun: boolean;
|
|
875
|
+
/** OPERATOR allowlist of project roots. Load-bearing even with allowRun. */
|
|
876
|
+
allowedRoots: string[];
|
|
877
|
+
/** Injected file reader (default `readFileSync`). */
|
|
878
|
+
readFile?: FileReader;
|
|
879
|
+
}
|
|
880
|
+
type LspQueryKind = 'definition' | 'typeDefinition' | 'references' | 'hover' | 'documentSymbols' | 'workspaceSymbol' | 'diagnostics' | 'callHierarchy';
|
|
881
|
+
interface LspQueryInput {
|
|
882
|
+
/** The agent-facing language (resolved against the operator registry). */
|
|
883
|
+
language: string;
|
|
884
|
+
/** Project root — must be in `allowedRoots`; pinned to the server's `rootUri`. */
|
|
885
|
+
projectRoot: string;
|
|
886
|
+
/**
|
|
887
|
+
* Additional allowlisted roots bound as workspace folders on the SAME server (multi-root, ADR
|
|
888
|
+
* 0011 tail) — so cross-root navigation resolves through one server. Each must be in
|
|
889
|
+
* `allowedRoots`. The queried `file` still lives under the primary `projectRoot`.
|
|
890
|
+
*/
|
|
891
|
+
workspaceRoots?: string[];
|
|
892
|
+
/**
|
|
893
|
+
* The file to query, relative to `projectRoot` (or absolute within it). Required for every kind
|
|
894
|
+
* EXCEPT `workspaceSymbol`, which searches the whole indexed project and needs no open file.
|
|
895
|
+
*/
|
|
896
|
+
file?: string;
|
|
897
|
+
/** 1-based human line — required for the position-based kinds, ignored for `documentSymbols`. */
|
|
898
|
+
line?: number;
|
|
899
|
+
/** 1-based human column (code points) — required for the position-based kinds. */
|
|
900
|
+
column?: number;
|
|
901
|
+
kind: LspQueryKind;
|
|
902
|
+
/** The search string — required for (and only used by) `workspaceSymbol`. */
|
|
903
|
+
query?: string;
|
|
904
|
+
/** Call-hierarchy direction (callers vs callees); defaults to `incoming`. */
|
|
905
|
+
direction?: CallDirection;
|
|
906
|
+
/** Optional toolchain provenance to echo (the surface computes via `detectInstalledVersion`). */
|
|
907
|
+
toolchain?: {
|
|
908
|
+
name: string;
|
|
909
|
+
version: string | null;
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
interface HumanRange {
|
|
913
|
+
start: HumanPosition;
|
|
914
|
+
end: HumanPosition;
|
|
915
|
+
}
|
|
916
|
+
interface ResultLocation {
|
|
917
|
+
uri: string;
|
|
918
|
+
range: HumanRange;
|
|
919
|
+
/** The full enclosing range, when the server sent a LocationLink. */
|
|
920
|
+
fullRange?: HumanRange;
|
|
921
|
+
/** False ⇒ the target file was unreadable; the range is a best-effort `+1` of the LSP offsets. */
|
|
922
|
+
mapped: boolean;
|
|
923
|
+
}
|
|
924
|
+
/** A document symbol with its range mapped to human 1-based coords; children recurse. */
|
|
925
|
+
interface ResultSymbol {
|
|
926
|
+
name: string;
|
|
927
|
+
kind: number;
|
|
928
|
+
kindName: string;
|
|
929
|
+
detail?: string;
|
|
930
|
+
range: HumanRange;
|
|
931
|
+
/** Set only for flat `SymbolInformation` results. */
|
|
932
|
+
container?: string;
|
|
933
|
+
children?: ResultSymbol[];
|
|
934
|
+
}
|
|
935
|
+
/** A diagnostic with its range(s) mapped to human 1-based coords. */
|
|
936
|
+
interface ResultDiagnostic {
|
|
937
|
+
range: HumanRange;
|
|
938
|
+
message: string;
|
|
939
|
+
severity?: number;
|
|
940
|
+
severityName?: string;
|
|
941
|
+
code?: number | string;
|
|
942
|
+
source?: string;
|
|
943
|
+
tags?: string[];
|
|
944
|
+
related?: {
|
|
945
|
+
uri: string;
|
|
946
|
+
range: HumanRange;
|
|
947
|
+
message: string;
|
|
948
|
+
}[];
|
|
949
|
+
}
|
|
950
|
+
/** A workspace symbol with its range (if any) mapped to human 1-based coords. */
|
|
951
|
+
interface ResultWorkspaceSymbol {
|
|
952
|
+
name: string;
|
|
953
|
+
kind: number;
|
|
954
|
+
kindName: string;
|
|
955
|
+
uri: string;
|
|
956
|
+
/** The declaring container (class/namespace), when the server reported one. */
|
|
957
|
+
container?: string;
|
|
958
|
+
/** Absent when the server returned a uri-only `WorkspaceSymbol` (no range without resolve). */
|
|
959
|
+
range?: HumanRange;
|
|
960
|
+
/** True when a range was present AND its target file was readable (encoding-faithful map). */
|
|
961
|
+
mapped: boolean;
|
|
962
|
+
}
|
|
963
|
+
/** A call-hierarchy item with its ranges mapped to human 1-based coords. */
|
|
964
|
+
interface ResultCallItem {
|
|
965
|
+
name: string;
|
|
966
|
+
kind: number;
|
|
967
|
+
kindName: string;
|
|
968
|
+
detail?: string;
|
|
969
|
+
uri: string;
|
|
970
|
+
range: HumanRange;
|
|
971
|
+
selectionRange: HumanRange;
|
|
972
|
+
}
|
|
973
|
+
/** One call edge: the other item + the human-coord ranges where the call occurs. */
|
|
974
|
+
interface ResultCall {
|
|
975
|
+
item: ResultCallItem;
|
|
976
|
+
fromRanges: HumanRange[];
|
|
977
|
+
}
|
|
978
|
+
interface ResultCallGroup {
|
|
979
|
+
source: ResultCallItem;
|
|
980
|
+
direction: CallDirection;
|
|
981
|
+
calls: ResultCall[];
|
|
982
|
+
}
|
|
983
|
+
interface LspQueryResult {
|
|
984
|
+
status: QueryStatus;
|
|
985
|
+
kind: LspQueryKind;
|
|
986
|
+
locations?: ResultLocation[];
|
|
987
|
+
hover?: {
|
|
988
|
+
value: string;
|
|
989
|
+
range?: HumanRange;
|
|
990
|
+
};
|
|
991
|
+
symbols?: ResultSymbol[];
|
|
992
|
+
workspaceSymbols?: ResultWorkspaceSymbol[];
|
|
993
|
+
diagnostics?: ResultDiagnostic[];
|
|
994
|
+
callHierarchy?: ResultCallGroup[];
|
|
995
|
+
serverInfo?: ServerInfo;
|
|
996
|
+
toolchain?: {
|
|
997
|
+
name: string;
|
|
998
|
+
version: string | null;
|
|
999
|
+
};
|
|
1000
|
+
encoding: PositionEncoding;
|
|
1001
|
+
versionWarning?: string;
|
|
1002
|
+
}
|
|
1003
|
+
declare class LspQueryEngine {
|
|
1004
|
+
private readonly manager;
|
|
1005
|
+
private readonly allowRun;
|
|
1006
|
+
private readonly allowedRoots;
|
|
1007
|
+
private readonly readFile;
|
|
1008
|
+
constructor(options: LspQueryEngineOptions);
|
|
1009
|
+
query(input: LspQueryInput): Promise<LspQueryResult>;
|
|
1010
|
+
/**
|
|
1011
|
+
* Run the project-wide `workspace/symbol` search and shape its cross-file result.
|
|
1012
|
+
*
|
|
1013
|
+
* `workspace/symbol` takes no position, but a real server still needs a *project* to search.
|
|
1014
|
+
* Some servers — notably `typescript-language-server` — only build a project once a document is
|
|
1015
|
+
* open, and answer `workspace/symbol` with a "No Project" error otherwise (caught running the
|
|
1016
|
+
* greeter example live, the cold-load lesson again). So the agent may pass an OPTIONAL anchor
|
|
1017
|
+
* `file`: when present we open it (establishing the project) before searching; when absent we
|
|
1018
|
+
* search with no document open, which works for eager indexers (gopls, rust-analyzer) that load
|
|
1019
|
+
* the project at `initialize`.
|
|
1020
|
+
*/
|
|
1021
|
+
private queryWorkspaceSymbol;
|
|
1022
|
+
/** The shared provenance/status envelope every result carries (status + encoding + provenance). */
|
|
1023
|
+
private baseResult;
|
|
1024
|
+
private shape;
|
|
1025
|
+
private mapLocation;
|
|
1026
|
+
/** Map a normalized document symbol (and its children) to human coords in the queried file. */
|
|
1027
|
+
private mapSymbol;
|
|
1028
|
+
/** Map a diagnostic's range (queried file) + any relatedInformation ranges (their own files). */
|
|
1029
|
+
private mapDiagnostic;
|
|
1030
|
+
/** Map a workspace symbol to human coords, reading its OWN target file (cross-file, read-only). */
|
|
1031
|
+
private mapWorkspaceSymbol;
|
|
1032
|
+
private mapCallItem;
|
|
1033
|
+
private mapCallGroup;
|
|
1034
|
+
private textForUri;
|
|
1035
|
+
/** Map an LSP 0-based range to a human 1-based range, encoding-faithfully when text is known. */
|
|
1036
|
+
private mapRange;
|
|
1037
|
+
}
|
|
1038
|
+
//#endregion
|
|
1039
|
+
//#region src/rename.d.ts
|
|
1040
|
+
/** Reads a file's text, or `undefined` if it cannot be read. */
|
|
1041
|
+
type FileReader$1 = (absolutePath: string) => string | undefined;
|
|
1042
|
+
/**
|
|
1043
|
+
* Lists candidate source files (absolute paths) under the allowlisted root group to scan for the
|
|
1044
|
+
* partial-rename completeness guard — same-language source only (`extension`, e.g. `.py`). Injected
|
|
1045
|
+
* so the gate never walks a real tree; `truncated` ⇒ a cap was hit and the scan is not exhaustive.
|
|
1046
|
+
*/
|
|
1047
|
+
type ProjectFileLister = (roots: string[], opts: {
|
|
1048
|
+
extension: string;
|
|
1049
|
+
}) => {
|
|
1050
|
+
files: string[];
|
|
1051
|
+
truncated: boolean;
|
|
1052
|
+
};
|
|
1053
|
+
/** Default lister: a bounded, symlink-safe recursive walk collecting `extension` files, skipping
|
|
1054
|
+
* dependency/VCS/cache/build dirs and any dotdir. Stops (`truncated`) at {@link MAX_SCAN_FILES}.
|
|
1055
|
+
* The partial-rename guard is OFF until a lister is wired (like `redact`, the surfaces wire this). */
|
|
1056
|
+
declare const defaultListFiles: ProjectFileLister;
|
|
1057
|
+
/** How exhaustively the partial-rename guard could verify the edit covers every textual use. */
|
|
1058
|
+
type RenameCompleteness = 'complete' | 'suspect' | 'unknown';
|
|
1059
|
+
/**
|
|
1060
|
+
* One physical filesystem action in an apply commit. A `write` creates-or-overwrites a file's
|
|
1061
|
+
* content (the fold of a CreateFile + its edits, or an edit to a pre-existing file); `rename`/
|
|
1062
|
+
* `delete` are the resource-op file moves/removals (`RenameFile`/`DeleteFile`).
|
|
1063
|
+
*/
|
|
1064
|
+
type PhysicalOp = {
|
|
1065
|
+
kind: 'write';
|
|
1066
|
+
absPath: string;
|
|
1067
|
+
newText: string;
|
|
1068
|
+
} | {
|
|
1069
|
+
kind: 'rename';
|
|
1070
|
+
fromAbs: string;
|
|
1071
|
+
toAbs: string;
|
|
1072
|
+
} | {
|
|
1073
|
+
kind: 'delete';
|
|
1074
|
+
absPath: string;
|
|
1075
|
+
};
|
|
1076
|
+
interface CommitResult {
|
|
1077
|
+
/** The ops that completed, in order. */
|
|
1078
|
+
completed: PhysicalOp[];
|
|
1079
|
+
/** True ⇒ an op faulted mid-execute; `completed` is the TERMINAL landed set (rename/delete are
|
|
1080
|
+
* irreversible — there is no rollback; reconcile via VCS). */
|
|
1081
|
+
partial: boolean;
|
|
1082
|
+
error?: string;
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* The write seam (injected; tests substitute a fake so the gate never touches disk). The default
|
|
1086
|
+
* **stages every `write` to a sibling temp (+ fsync) first** (no target touched until all temps
|
|
1087
|
+
* exist — the strength the pure-text rename relies on), then executes every op IN ORDER: a `write`
|
|
1088
|
+
* commits via atomic rename of its temp, a `rename`/`delete` runs the fs primitive. Resource ops
|
|
1089
|
+
* are irreversible and cannot be staged, so a fault during the execute phase is terminal —
|
|
1090
|
+
* `partial: true` names exactly what landed.
|
|
1091
|
+
*/
|
|
1092
|
+
interface RenameWriter {
|
|
1093
|
+
commit(ops: PhysicalOp[]): CommitResult;
|
|
1094
|
+
}
|
|
1095
|
+
declare const defaultRenameWriter: RenameWriter;
|
|
1096
|
+
interface LspRenameEngineOptions {
|
|
1097
|
+
manager: LanguageServerManager;
|
|
1098
|
+
/** OPERATOR opt-in to run navigation (which requires a live indexing daemon). Deny-by-default. */
|
|
1099
|
+
allowRun: boolean;
|
|
1100
|
+
/** OPERATOR allowlist of project roots. Load-bearing even with allowRun. */
|
|
1101
|
+
allowedRoots: string[];
|
|
1102
|
+
/** OPERATOR opt-in to WRITE the edit to disk. Distinct from allowRun; off ⇒ dry-run preview. */
|
|
1103
|
+
allowWrite: boolean;
|
|
1104
|
+
/**
|
|
1105
|
+
* OPERATOR opt-in to APPLY a rename whose completeness verdict is `suspect` — i.e. the old
|
|
1106
|
+
* identifier also appears in same-language files the server's edit does NOT touch (the hallmark
|
|
1107
|
+
* of an open-files-scoped server like pyright, whose cross-file rename can be silently partial).
|
|
1108
|
+
* Deny-by-default: a suspect rename is REFUSED for write unless this is set. Never an agent input.
|
|
1109
|
+
*/
|
|
1110
|
+
allowPartialRename?: boolean;
|
|
1111
|
+
/**
|
|
1112
|
+
* OPERATOR opt-in to APPLY a DESTRUCTIVE resource op — `overwrite: true` on a CreateFile
|
|
1113
|
+
* (truncate-and-replace an existing file) or a RenameFile (clobber an existing REGULAR-FILE
|
|
1114
|
+
* target). Deny-by-default; refused for write unless set. Recursive/dir delete + symlink/dir
|
|
1115
|
+
* targets STAY refused even with this gate. Meaningless without `allowWrite` (the engine
|
|
1116
|
+
* re-checks both before any destructive op); never an agent input.
|
|
1117
|
+
*/
|
|
1118
|
+
allowDestructiveResourceOps?: boolean;
|
|
1119
|
+
readFile?: FileReader$1;
|
|
1120
|
+
/** Lists same-language source files to scan for the partial-rename guard. UNSET ⇒ the guard is
|
|
1121
|
+
* inactive (no scan); the bin/CLI/MCP wire `defaultListFiles` to turn it on (cf. `redact`). */
|
|
1122
|
+
listFiles?: ProjectFileLister;
|
|
1123
|
+
writer?: RenameWriter;
|
|
1124
|
+
/** Secret redaction over every surfaced hunk (default identity; the bin wires @sackville-mcp/safety). */
|
|
1125
|
+
redact?: (text: string) => string;
|
|
1126
|
+
}
|
|
1127
|
+
interface LspRenameInput {
|
|
1128
|
+
language: string;
|
|
1129
|
+
projectRoot: string;
|
|
1130
|
+
/** The file to rename in, relative to projectRoot (or absolute within it). */
|
|
1131
|
+
file: string;
|
|
1132
|
+
/** 1-based human line of the symbol to rename. */
|
|
1133
|
+
line: number;
|
|
1134
|
+
/** 1-based human column (code points) of the symbol to rename. */
|
|
1135
|
+
column: number;
|
|
1136
|
+
/** The new identifier. Validated by isPlausibleRenameName before reaching the server. */
|
|
1137
|
+
newName: string;
|
|
1138
|
+
/**
|
|
1139
|
+
* Additional allowlisted roots bound as workspace folders on the SAME server (multi-root). A
|
|
1140
|
+
* cross-root rename may edit files in any of these; each must be in `allowedRoots`, and edited
|
|
1141
|
+
* files confine to the GROUP (`projectRoot` ∪ `workspaceRoots`), not just the primary root.
|
|
1142
|
+
*/
|
|
1143
|
+
workspaceRoots?: string[];
|
|
1144
|
+
/** Optional toolchain provenance to echo. */
|
|
1145
|
+
toolchain?: {
|
|
1146
|
+
name: string;
|
|
1147
|
+
version: string | null;
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
interface RenamePreviewEdit {
|
|
1151
|
+
range: HumanRange;
|
|
1152
|
+
oldText: string;
|
|
1153
|
+
newText: string;
|
|
1154
|
+
needsConfirmation?: boolean;
|
|
1155
|
+
annotationLabel?: string;
|
|
1156
|
+
}
|
|
1157
|
+
interface RenamePreviewFile {
|
|
1158
|
+
uri: string;
|
|
1159
|
+
/** Project-relative path (never absolute). `(out of project root)` for an out-of-root edit. */
|
|
1160
|
+
file: string;
|
|
1161
|
+
editCount: number;
|
|
1162
|
+
/** True ⇒ the edit targets a file outside the allowlisted root; its bytes are NOT surfaced. */
|
|
1163
|
+
outOfRoot?: boolean;
|
|
1164
|
+
/** The per-edit hunks (omitted for an out-of-root or unreadable file). */
|
|
1165
|
+
hunks?: RenamePreviewEdit[];
|
|
1166
|
+
}
|
|
1167
|
+
interface RenameDigest {
|
|
1168
|
+
file: string;
|
|
1169
|
+
before: string;
|
|
1170
|
+
after: string;
|
|
1171
|
+
}
|
|
1172
|
+
interface LspRenameResult {
|
|
1173
|
+
status: QueryStatus;
|
|
1174
|
+
kind: 'rename';
|
|
1175
|
+
applied: boolean;
|
|
1176
|
+
/** A structured reason the rename was not applied (not renameable, multi-file, resource ops, drift). */
|
|
1177
|
+
refused?: string;
|
|
1178
|
+
newName: string;
|
|
1179
|
+
fileCount: number;
|
|
1180
|
+
totalEditCount: number;
|
|
1181
|
+
edits: RenamePreviewFile[];
|
|
1182
|
+
resourceOps?: NormalizedResourceOp[];
|
|
1183
|
+
/** Per-file pre/post SHA-256 digests — the apply audit (only when applied). */
|
|
1184
|
+
digests?: RenameDigest[];
|
|
1185
|
+
/** Project-relative paths whose prior content a DESTRUCTIVE overwrite clobbered (gated; landed
|
|
1186
|
+
* only) — so a destructive clobber is always explicit in the envelope, not inferred from a digest. */
|
|
1187
|
+
overwritten?: string[];
|
|
1188
|
+
/** True ⇒ an irreversible resource op faulted mid-commit; `digests` names what landed (no
|
|
1189
|
+
* rollback — reconcile via VCS). */
|
|
1190
|
+
partial?: boolean;
|
|
1191
|
+
partialError?: string;
|
|
1192
|
+
/**
|
|
1193
|
+
* The partial-rename guard verdict (omitted when the rename was not `ok`). `complete` — every
|
|
1194
|
+
* same-language file mentioning the old name is in the edit; `suspect` — some are NOT (the edit
|
|
1195
|
+
* may be partial, e.g. an open-files-scoped server); `unknown` — the scan was truncated.
|
|
1196
|
+
*/
|
|
1197
|
+
completeness?: RenameCompleteness;
|
|
1198
|
+
/** Project-relative same-language files that mention the old identifier but are absent from the
|
|
1199
|
+
* edit (capped). Populated when `completeness === 'suspect'`. */
|
|
1200
|
+
suspectedMissedFiles?: string[];
|
|
1201
|
+
serverInfo?: ServerInfo;
|
|
1202
|
+
toolchain?: {
|
|
1203
|
+
name: string;
|
|
1204
|
+
version: string | null;
|
|
1205
|
+
};
|
|
1206
|
+
encoding: PositionEncoding;
|
|
1207
|
+
versionWarning?: string;
|
|
1208
|
+
}
|
|
1209
|
+
declare class LspRenameEngine {
|
|
1210
|
+
private readonly manager;
|
|
1211
|
+
private readonly allowRun;
|
|
1212
|
+
private readonly allowedRoots;
|
|
1213
|
+
private readonly allowWrite;
|
|
1214
|
+
private readonly allowPartialRename;
|
|
1215
|
+
private readonly allowDestructiveResourceOps;
|
|
1216
|
+
private readonly readFile;
|
|
1217
|
+
/** When unset the partial-rename guard is inactive (the bin/CLI/MCP wire `defaultListFiles`). */
|
|
1218
|
+
private readonly listFiles?;
|
|
1219
|
+
private readonly writer;
|
|
1220
|
+
private readonly redact;
|
|
1221
|
+
constructor(options: LspRenameEngineOptions);
|
|
1222
|
+
rename(input: LspRenameInput): Promise<LspRenameResult>;
|
|
1223
|
+
/**
|
|
1224
|
+
* Decide + execute the apply, consuming the ordered `operations` (text edits interleaved with
|
|
1225
|
+
* CreateFile/RenameFile/DeleteFile). Confine EVERY touched URI (edit + create + rename old&new +
|
|
1226
|
+
* delete) to the root group all-or-nothing BEFORE any I/O; refuse the v1 cuts early (non-default
|
|
1227
|
+
* resource-op options; editing a file also renamed in the same batch). Then, under the multi-URI
|
|
1228
|
+
* lock over ALL touched URIs, replay the ops over a virtual content map (no writes) with the
|
|
1229
|
+
* staleness guards, build a physical plan, stage-then-commit it, and resync any open buffer
|
|
1230
|
+
* (`didChange` for an edited file, `didClose`+`didOpen` migration for a renamed/deleted one).
|
|
1231
|
+
*/
|
|
1232
|
+
private applyEdit;
|
|
1233
|
+
/**
|
|
1234
|
+
* The partial-rename completeness guard. Extracts the old identifier at the queried position,
|
|
1235
|
+
* then scans the allowlisted root group for same-language files that mention it as a whole word
|
|
1236
|
+
* but are NOT covered by the server's edit (text edits + resource-op endpoints). Any such file is
|
|
1237
|
+
* a SUSPECT — the edit is likely partial (an open-files-scoped server). `unknown` ⇒ the scan was
|
|
1238
|
+
* truncated (cap hit). Server-agnostic: a whole-project-rename server covers every use ⇒ `complete`.
|
|
1239
|
+
*/
|
|
1240
|
+
private assessCompleteness;
|
|
1241
|
+
private shape;
|
|
1242
|
+
private previewFile;
|
|
1243
|
+
private hunk;
|
|
1244
|
+
}
|
|
1245
|
+
//#endregion
|
|
1246
|
+
export { type CallDirection, type CallHierarchyGroup, type CallHierarchyItem, type CommitResult, type Diagnostic, type DocumentSymbol, type FileReader, type Hover, type HumanPosition, type HumanRange, type InitializeSummary, LanguageServerManager, type LanguageServerManagerOptions, type Location, type LocationLink, LspClient, type LspClientOptions, type LspConnection, LspGateError, LspManagerError, type LspPosition, type LspPositionParts, LspQueryEngine, type LspQueryEngineOptions, type LspQueryInput, type LspQueryKind, type LspQueryResult, type LspRange, LspRegistryError, LspRenameEngine, type LspRenameEngineOptions, type LspRenameInput, type LspRenameResult, LspUnsupportedError, type MarkedString, type MarkupContent, type NavResult, type NormalizedCall, type NormalizedCallItem, type NormalizedDiagnostic, type NormalizedFileEdit, type NormalizedFileEdits, type NormalizedHover, type NormalizedLocation, type NormalizedRelatedInfo, type NormalizedResourceOp, type NormalizedSymbol, type NormalizedWorkspaceEdit, type NormalizedWorkspaceSymbol, PREFERRED_ENCODINGS, type PhysicalOp, type PositionEncoding, type PrepareRenameOutcome, type ProjectFileLister, type QueryInput, type QueryStatus, type RawPrepareRename, type RawResourceOperation, type RawTextDocumentEdit, type RawTextEdit, type RawWorkspaceEdit, type RenameCompleteness, type RenameDigest, type RenamePreviewEdit, type RenamePreviewFile, type RenameWriter, type ResultCall, type ResultCallGroup, type ResultCallItem, type ResultDiagnostic, type ResultLocation, type ResultSymbol, type ResultWorkspaceSymbol, type ServerDescription, type ServerInfo, type ServerRegistry, type ServerRegistryEntry, type ServerSpawn, type ServerSpec, type SymbolInformation, type WorkspaceSymbol, decideStatus, defaultListFiles, defaultRenameWriter, defaultServerSpawn, diagnosticSeverityName, fromLspCharacter, fromLspPosition, normalizeCallHierarchyItem, normalizeCallHierarchyItems, normalizeDiagnostics, normalizeDocumentSymbols, normalizeHover, normalizeIncomingCalls, normalizeLocations, normalizeOutgoingCalls, normalizePrepareRename, normalizeWorkspaceEdit, normalizeWorkspaceSymbols, parseServerRegistry, resolvePositionEncoding, resolveServer, symbolKindName, toLspCharacter, toLspPosition };
|
|
1247
|
+
//# sourceMappingURL=index.d.mts.map
|