@adecore/lsp 0.0.1 → 0.17.0-beta.1
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 +110 -0
- package/README.md +33 -0
- package/dist/connection.d.ts +50 -0
- package/dist/connection.js +290 -0
- package/dist/document.d.ts +60 -0
- package/dist/document.js +282 -0
- package/dist/edits.d.ts +18 -0
- package/dist/edits.js +161 -0
- package/dist/file-operations.d.ts +5 -0
- package/dist/file-operations.js +23 -0
- package/dist/glob.d.ts +1 -0
- package/dist/glob.js +67 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +10 -0
- package/dist/protocol.d.ts +470 -0
- package/dist/protocol.js +1 -0
- package/dist/service.d.ts +53 -0
- package/dist/service.js +1 -0
- package/dist/session.d.ts +78 -0
- package/dist/session.js +528 -0
- package/dist/test-transport.d.ts +33 -0
- package/dist/test-transport.js +72 -0
- package/dist/testing.d.ts +33 -0
- package/dist/testing.js +72 -0
- package/dist/transport.d.ts +27 -0
- package/dist/transport.js +258 -0
- package/dist/uris.d.ts +2 -0
- package/dist/uris.js +28 -0
- package/dist/vue.d.ts +6 -0
- package/dist/vue.js +86 -0
- package/dist/watched-files.d.ts +3 -0
- package/dist/watched-files.js +41 -0
- package/package.json +36 -3
- package/src/connection.ts +310 -0
- package/src/document.ts +357 -0
- package/src/edits.ts +182 -0
- package/src/file-operations.ts +30 -0
- package/src/glob.ts +60 -0
- package/src/index.ts +30 -0
- package/src/protocol.ts +479 -0
- package/src/service.ts +109 -0
- package/src/session.ts +613 -0
- package/src/test-transport.ts +92 -0
- package/src/testing.ts +105 -0
- package/src/transport.ts +290 -0
- package/src/uris.ts +29 -0
- package/src/vue.ts +93 -0
- package/src/watched-files.ts +49 -0
package/src/session.ts
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import { ErrorCodes, JsonRpcConnection, LspError, type RequestHandler } from './connection.ts';
|
|
2
|
+
import { LspDocument } from './document.ts';
|
|
3
|
+
import { renamesTaken, type RenamedFile } from './file-operations.ts';
|
|
4
|
+
import { globMatch } from './glob.ts';
|
|
5
|
+
import type {
|
|
6
|
+
ApplyWorkspaceEditParams,
|
|
7
|
+
ApplyWorkspaceEditResult,
|
|
8
|
+
Command,
|
|
9
|
+
ConfigurationItem,
|
|
10
|
+
Disposable,
|
|
11
|
+
DocumentItem,
|
|
12
|
+
FileEvent,
|
|
13
|
+
FileOperationFilter,
|
|
14
|
+
FileRename,
|
|
15
|
+
FileSystemWatcher,
|
|
16
|
+
InitializeResult,
|
|
17
|
+
LspTransport,
|
|
18
|
+
ProgressParams,
|
|
19
|
+
ProviderOptions,
|
|
20
|
+
PublishDiagnosticsParams,
|
|
21
|
+
Registration,
|
|
22
|
+
RequestOptions,
|
|
23
|
+
ServerCapabilities,
|
|
24
|
+
TextDocumentSyncOptions,
|
|
25
|
+
WorkspaceEdit,
|
|
26
|
+
WorkspaceFolder,
|
|
27
|
+
WorkspaceSymbol,
|
|
28
|
+
WorkspaceSymbolResult
|
|
29
|
+
} from './protocol.ts';
|
|
30
|
+
|
|
31
|
+
export type SessionState = 'new' | 'initializing' | 'ready' | 'closing' | 'closed';
|
|
32
|
+
|
|
33
|
+
export interface LspSessionOptions {
|
|
34
|
+
rootUri?: string | null;
|
|
35
|
+
workspaceFolders?: WorkspaceFolder[];
|
|
36
|
+
initializationOptions?: unknown;
|
|
37
|
+
/* Sent after `initialized` and answered to every configuration query, before any document opens. */
|
|
38
|
+
configuration?: Record<string, unknown>;
|
|
39
|
+
clientInfo?: { name: string; version?: string };
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
snippetSupport?: boolean;
|
|
42
|
+
onConfiguration?: (item: ConfigurationItem) => unknown | Promise<unknown>;
|
|
43
|
+
onApplyEdit?: (params: ApplyWorkspaceEditParams) => ApplyWorkspaceEditResult | Promise<ApplyWorkspaceEditResult>;
|
|
44
|
+
onShowMessage?: (params: { type: number; message: string; actions?: { title: string }[] }) => { title: string } | null | Promise<{ title: string } | null>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* The capability that advertises each method statically. */
|
|
48
|
+
const PROVIDERS: Record<string, keyof ServerCapabilities> = {
|
|
49
|
+
'textDocument/completion': 'completionProvider',
|
|
50
|
+
'completionItem/resolve': 'completionProvider',
|
|
51
|
+
'textDocument/hover': 'hoverProvider',
|
|
52
|
+
'textDocument/signatureHelp': 'signatureHelpProvider',
|
|
53
|
+
'textDocument/definition': 'definitionProvider',
|
|
54
|
+
'textDocument/declaration': 'declarationProvider',
|
|
55
|
+
'textDocument/typeDefinition': 'typeDefinitionProvider',
|
|
56
|
+
'textDocument/implementation': 'implementationProvider',
|
|
57
|
+
'textDocument/references': 'referencesProvider',
|
|
58
|
+
'textDocument/prepareRename': 'renameProvider',
|
|
59
|
+
'textDocument/documentHighlight': 'documentHighlightProvider',
|
|
60
|
+
'textDocument/documentSymbol': 'documentSymbolProvider',
|
|
61
|
+
'workspace/symbol': 'workspaceSymbolProvider',
|
|
62
|
+
'workspaceSymbol/resolve': 'workspaceSymbolProvider',
|
|
63
|
+
'textDocument/rename': 'renameProvider',
|
|
64
|
+
'textDocument/codeAction': 'codeActionProvider',
|
|
65
|
+
'codeAction/resolve': 'codeActionProvider',
|
|
66
|
+
'textDocument/codeLens': 'codeLensProvider',
|
|
67
|
+
'codeLens/resolve': 'codeLensProvider',
|
|
68
|
+
'workspace/executeCommand': 'executeCommandProvider',
|
|
69
|
+
'textDocument/formatting': 'documentFormattingProvider',
|
|
70
|
+
'textDocument/rangeFormatting': 'documentRangeFormattingProvider',
|
|
71
|
+
'textDocument/foldingRange': 'foldingRangeProvider',
|
|
72
|
+
'textDocument/selectionRange': 'selectionRangeProvider',
|
|
73
|
+
'textDocument/semanticTokens/full': 'semanticTokensProvider',
|
|
74
|
+
'textDocument/semanticTokens/full/delta': 'semanticTokensProvider',
|
|
75
|
+
'textDocument/semanticTokens/range': 'semanticTokensProvider',
|
|
76
|
+
'textDocument/inlayHint': 'inlayHintProvider',
|
|
77
|
+
'inlayHint/resolve': 'inlayHintProvider',
|
|
78
|
+
'textDocument/diagnostic': 'diagnosticProvider'
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/* A method whose dynamic registration is made under the name of another. */
|
|
82
|
+
const REGISTRATION_METHODS: Record<string, string> = {
|
|
83
|
+
'completionItem/resolve': 'textDocument/completion',
|
|
84
|
+
'codeAction/resolve': 'textDocument/codeAction',
|
|
85
|
+
'codeLens/resolve': 'textDocument/codeLens',
|
|
86
|
+
'inlayHint/resolve': 'textDocument/inlayHint',
|
|
87
|
+
'textDocument/prepareRename': 'textDocument/rename',
|
|
88
|
+
'workspaceSymbol/resolve': 'workspace/symbol',
|
|
89
|
+
'textDocument/semanticTokens/full': 'textDocument/semanticTokens',
|
|
90
|
+
'textDocument/semanticTokens/full/delta': 'textDocument/semanticTokens',
|
|
91
|
+
'textDocument/semanticTokens/range': 'textDocument/semanticTokens'
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const REFRESH_REQUESTS = ['workspace/semanticTokens/refresh', 'workspace/inlayHint/refresh', 'workspace/diagnostic/refresh', 'workspace/codeLens/refresh'];
|
|
95
|
+
|
|
96
|
+
function matches(options: ProviderOptions, document?: Pick<DocumentItem, 'uri' | 'languageId'>): boolean {
|
|
97
|
+
if (options.documentSelector == null) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (!document) {
|
|
101
|
+
return options.documentSelector.length > 0;
|
|
102
|
+
}
|
|
103
|
+
const url = new URL(document.uri);
|
|
104
|
+
return options.documentSelector.some(
|
|
105
|
+
(filter) =>
|
|
106
|
+
(!filter.language || filter.language === document.languageId) &&
|
|
107
|
+
(!filter.scheme || filter.scheme === url.protocol.slice(0, -1)) &&
|
|
108
|
+
(!filter.pattern || globMatch(decodeURIComponent(url.pathname), filter.pattern))
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* One conversation with one language server: the handshake, what it can do, and the documents open in it. */
|
|
113
|
+
export class LspSession {
|
|
114
|
+
readonly connection: JsonRpcConnection;
|
|
115
|
+
readonly options: LspSessionOptions;
|
|
116
|
+
serverInfo?: InitializeResult['serverInfo'];
|
|
117
|
+
private phase: SessionState = 'new';
|
|
118
|
+
private initialized?: Promise<InitializeResult>;
|
|
119
|
+
private shutdownPromise?: Promise<void>;
|
|
120
|
+
private serverCapabilities: ServerCapabilities = {};
|
|
121
|
+
private readonly registrations = new Map<string, Registration>();
|
|
122
|
+
private readonly documents = new Map<string, LspDocument>();
|
|
123
|
+
private readonly diagnosticListeners = new Set<(params: PublishDiagnosticsParams) => void>();
|
|
124
|
+
private readonly capabilityListeners = new Set<() => void>();
|
|
125
|
+
private configuration: Record<string, unknown>;
|
|
126
|
+
|
|
127
|
+
constructor(transport: LspTransport, options: LspSessionOptions = {}) {
|
|
128
|
+
this.options = options;
|
|
129
|
+
this.connection = new JsonRpcConnection(transport, options.timeoutMs);
|
|
130
|
+
this.configuration = options.configuration ?? {};
|
|
131
|
+
this.connection.onClose(() => {
|
|
132
|
+
this.phase = 'closed';
|
|
133
|
+
for (const document of [...this.documents.values()]) {
|
|
134
|
+
document.disconnect();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
this.connection.onNotification('textDocument/publishDiagnostics', (params) => {
|
|
138
|
+
const diagnostics = params as PublishDiagnosticsParams;
|
|
139
|
+
const document = this.documents.get(diagnostics.uri);
|
|
140
|
+
if (!document || document.isClosed || (diagnostics.version !== undefined && diagnostics.version !== document.version)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.emitDiagnostics(diagnostics);
|
|
144
|
+
});
|
|
145
|
+
this.connection.onRequest('workspace/configuration', async (params) => {
|
|
146
|
+
const { items } = params as { items: ConfigurationItem[] };
|
|
147
|
+
return Promise.all(items.map((item) => (options.onConfiguration ? options.onConfiguration(item) : this.readConfiguration(item.section))));
|
|
148
|
+
});
|
|
149
|
+
this.connection.onRequest('workspace/workspaceFolders', () => this.workspaceFolders);
|
|
150
|
+
this.connection.onRequest(
|
|
151
|
+
'workspace/applyEdit',
|
|
152
|
+
(params) => options.onApplyEdit?.(params as ApplyWorkspaceEditParams) ?? { applied: false, failureReason: 'No workspace edit handler is attached' }
|
|
153
|
+
);
|
|
154
|
+
this.connection.onRequest(
|
|
155
|
+
'window/showMessageRequest',
|
|
156
|
+
(params) => options.onShowMessage?.(params as Parameters<NonNullable<LspSessionOptions['onShowMessage']>>[0]) ?? null
|
|
157
|
+
);
|
|
158
|
+
this.connection.onRequest('window/workDoneProgress/create', () => null);
|
|
159
|
+
this.connection.onRequest('client/registerCapability', (params) => {
|
|
160
|
+
const { registrations } = params as { registrations: Registration[] };
|
|
161
|
+
for (const registration of registrations) {
|
|
162
|
+
this.registrations.set(registration.id, registration);
|
|
163
|
+
}
|
|
164
|
+
this.emitCapabilities();
|
|
165
|
+
return null;
|
|
166
|
+
});
|
|
167
|
+
this.connection.onRequest('client/unregisterCapability', (params) => {
|
|
168
|
+
// The misspelled field is part of the LSP 3.17 wire protocol.
|
|
169
|
+
const { unregisterations } = params as { unregisterations: { id: string; method: string }[] };
|
|
170
|
+
for (const registration of unregisterations) {
|
|
171
|
+
this.registrations.delete(registration.id);
|
|
172
|
+
}
|
|
173
|
+
this.emitCapabilities();
|
|
174
|
+
return null;
|
|
175
|
+
});
|
|
176
|
+
for (const method of REFRESH_REQUESTS) {
|
|
177
|
+
this.connection.onRequest(method, () => {
|
|
178
|
+
this.emitCapabilities();
|
|
179
|
+
return null;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
get state(): SessionState {
|
|
185
|
+
return this.phase;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
get capabilities(): Readonly<ServerCapabilities> {
|
|
189
|
+
return this.serverCapabilities;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
get workspaceFolders(): WorkspaceFolder[] | null {
|
|
193
|
+
return this.options.workspaceFolders ?? (this.options.rootUri ? [{ uri: this.options.rootUri, name: 'Workspace' }] : null);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get syncOptions(): TextDocumentSyncOptions {
|
|
197
|
+
const sync = this.serverCapabilities.textDocumentSync;
|
|
198
|
+
return typeof sync === 'number' ? { openClose: sync !== 0, change: sync } : (sync ?? {});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
initialize(): Promise<InitializeResult> {
|
|
202
|
+
if (this.initialized) {
|
|
203
|
+
return this.initialized;
|
|
204
|
+
}
|
|
205
|
+
if (this.phase !== 'new') {
|
|
206
|
+
return Promise.reject(new LspError(`Cannot initialize a ${this.phase} session`));
|
|
207
|
+
}
|
|
208
|
+
this.phase = 'initializing';
|
|
209
|
+
this.initialized = (async () => {
|
|
210
|
+
try {
|
|
211
|
+
const result = await this.connection.request<InitializeResult>('initialize', {
|
|
212
|
+
processId: null,
|
|
213
|
+
rootUri: this.options.rootUri ?? null,
|
|
214
|
+
workspaceFolders: this.workspaceFolders,
|
|
215
|
+
clientInfo: this.options.clientInfo ?? { name: '@adecore/lsp' },
|
|
216
|
+
capabilities: clientCapabilities(this.options),
|
|
217
|
+
initializationOptions: this.options.initializationOptions
|
|
218
|
+
});
|
|
219
|
+
if (result.capabilities.positionEncoding && result.capabilities.positionEncoding !== 'utf-16') {
|
|
220
|
+
throw new LspError(`Unsupported server position encoding: ${result.capabilities.positionEncoding}`);
|
|
221
|
+
}
|
|
222
|
+
this.serverCapabilities = result.capabilities;
|
|
223
|
+
this.serverInfo = result.serverInfo;
|
|
224
|
+
await this.connection.notify('initialized', {});
|
|
225
|
+
this.phase = 'ready';
|
|
226
|
+
if (this.options.configuration !== undefined) {
|
|
227
|
+
await this.connection.notify('workspace/didChangeConfiguration', { settings: this.configuration });
|
|
228
|
+
}
|
|
229
|
+
this.emitCapabilities();
|
|
230
|
+
return result;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
await this.connection.close();
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
})();
|
|
236
|
+
return this.initialized;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
openDocument(item: DocumentItem): LspDocument {
|
|
240
|
+
this.assertReady();
|
|
241
|
+
if (this.documents.has(item.uri)) {
|
|
242
|
+
throw new LspError(`Document already open: ${item.uri}`);
|
|
243
|
+
}
|
|
244
|
+
const document = new LspDocument(this, item);
|
|
245
|
+
this.documents.set(item.uri, document);
|
|
246
|
+
return document;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
getDocument(uri: string): LspDocument | undefined {
|
|
250
|
+
return this.documents.get(uri);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* The options the server gave a method, from its capabilities or a registration whose selector takes `document`. */
|
|
254
|
+
providerOptions(method: string, document?: Pick<DocumentItem, 'uri' | 'languageId'>): ProviderOptions | undefined {
|
|
255
|
+
const name = REGISTRATION_METHODS[method] ?? method;
|
|
256
|
+
const candidates: ProviderOptions[] = [];
|
|
257
|
+
for (const registration of this.registrations.values()) {
|
|
258
|
+
if (registration.method === name && matches(registration.registerOptions ?? {}, document)) {
|
|
259
|
+
candidates.push(registration.registerOptions ?? {});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const provider = this.serverCapabilities[PROVIDERS[method]];
|
|
263
|
+
if (provider === true) {
|
|
264
|
+
candidates.push({});
|
|
265
|
+
} else if (provider && typeof provider === 'object' && matches(provider as ProviderOptions, document)) {
|
|
266
|
+
candidates.push(provider as ProviderOptions);
|
|
267
|
+
}
|
|
268
|
+
return candidates.find((options) => {
|
|
269
|
+
if (method.endsWith('/resolve')) {
|
|
270
|
+
return options.resolveProvider === true;
|
|
271
|
+
}
|
|
272
|
+
if (method === 'textDocument/prepareRename') {
|
|
273
|
+
return options.prepareProvider === true;
|
|
274
|
+
}
|
|
275
|
+
if (method === 'textDocument/semanticTokens/full/delta') {
|
|
276
|
+
return typeof options.full === 'object' && options.full.delta === true;
|
|
277
|
+
}
|
|
278
|
+
if (method === 'textDocument/semanticTokens/full') {
|
|
279
|
+
return !!options.full;
|
|
280
|
+
}
|
|
281
|
+
if (method === 'textDocument/semanticTokens/range') {
|
|
282
|
+
return !!options.range;
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
supports(method: string, document?: Pick<DocumentItem, 'uri' | 'languageId'>): boolean {
|
|
289
|
+
return this.phase === 'ready' && this.providerOptions(method, document) !== undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
request<T = unknown>(method: string, params?: unknown, options?: RequestOptions): Promise<T> {
|
|
293
|
+
this.assertReady();
|
|
294
|
+
return this.connection.request<T>(method, params, options);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
notify(method: string, params?: unknown): Promise<void> {
|
|
298
|
+
this.assertReady();
|
|
299
|
+
return this.connection.notify(method, params);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
executeCommand<T = unknown>(command: Command | string, args?: unknown[], options?: RequestOptions): Promise<T> {
|
|
303
|
+
if (!this.supports('workspace/executeCommand')) {
|
|
304
|
+
return Promise.reject(new LspError('Server does not support commands', ErrorCodes.MethodNotFound));
|
|
305
|
+
}
|
|
306
|
+
return this.request('workspace/executeCommand', typeof command === 'string' ? { command, arguments: args } : command, options);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
workspaceSymbols(query: string, options?: RequestOptions): Promise<WorkspaceSymbolResult> {
|
|
310
|
+
if (!this.supports('workspace/symbol')) {
|
|
311
|
+
return Promise.reject(new LspError('Server does not support workspace symbols', ErrorCodes.MethodNotFound));
|
|
312
|
+
}
|
|
313
|
+
return this.request('workspace/symbol', { query }, options);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
resolveWorkspaceSymbol(symbol: WorkspaceSymbol, options?: RequestOptions): Promise<WorkspaceSymbol> {
|
|
317
|
+
if (!this.supports('workspaceSymbol/resolve')) {
|
|
318
|
+
return Promise.reject(new LspError('Server does not support workspace symbol resolution', ErrorCodes.MethodNotFound));
|
|
319
|
+
}
|
|
320
|
+
return this.request('workspaceSymbol/resolve', symbol, options);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async setConfiguration(settings: Record<string, unknown>): Promise<void> {
|
|
324
|
+
this.configuration = settings;
|
|
325
|
+
await this.notify('workspace/didChangeConfiguration', { settings });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
onRequest(method: string, handler: RequestHandler): Disposable {
|
|
329
|
+
return this.connection.onRequest(method, handler);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
onNotification(method: string, listener: (params: unknown) => void | Promise<void>): Disposable {
|
|
333
|
+
return this.connection.onNotification(method, listener);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
onError(listener: (error: Error) => void): Disposable {
|
|
337
|
+
return this.connection.onError(listener);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
onProgress(listener: (params: ProgressParams) => void): Disposable {
|
|
341
|
+
return this.onNotification('$/progress', (params) => listener(params as ProgressParams));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
onDiagnostics(listener: (params: PublishDiagnosticsParams) => void): Disposable {
|
|
345
|
+
this.diagnosticListeners.add(listener);
|
|
346
|
+
return {
|
|
347
|
+
dispose: () => {
|
|
348
|
+
this.diagnosticListeners.delete(listener);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/* What the server registered to hear about, which is nothing until it registers for `workspace/didChangeWatchedFiles`. */
|
|
354
|
+
watchedFiles(): FileSystemWatcher[] {
|
|
355
|
+
return [...this.registrations.values()]
|
|
356
|
+
.filter((registration) => registration.method === 'workspace/didChangeWatchedFiles')
|
|
357
|
+
.flatMap((registration) => registration.registerOptions?.watchers ?? []);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* Tells the server which files changed on disk. The caller holds back what its patterns do not name. */
|
|
361
|
+
didChangeWatchedFiles(changes: readonly FileEvent[]): Promise<void> {
|
|
362
|
+
return this.notify('workspace/didChangeWatchedFiles', { changes });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* The files a server asked to hear about when they are renamed, from its capabilities and its registrations. */
|
|
366
|
+
fileOperationFilters(operation: 'willRename' | 'didRename'): FileOperationFilter[] {
|
|
367
|
+
const fileOperations = (this.serverCapabilities.workspace as { fileOperations?: Record<string, ProviderOptions> } | undefined)?.fileOperations;
|
|
368
|
+
const method = `workspace/${operation}Files`;
|
|
369
|
+
return [
|
|
370
|
+
...(fileOperations?.[operation]?.filters ?? []),
|
|
371
|
+
...[...this.registrations.values()]
|
|
372
|
+
.filter((registration) => registration.method === method)
|
|
373
|
+
.flatMap((registration) => registration.registerOptions?.filters ?? [])
|
|
374
|
+
];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* What the server would change before these files move, or null when it takes none of them or has nothing to say. */
|
|
378
|
+
async willRenameFiles(files: readonly RenamedFile[], options?: RequestOptions): Promise<WorkspaceEdit | null> {
|
|
379
|
+
const taken = renamesTaken(this.fileOperationFilters('willRename'), files);
|
|
380
|
+
return taken.length === 0 || this.phase !== 'ready' ? null : this.request<WorkspaceEdit | null>('workspace/willRenameFiles', { files: taken }, options);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* Tells the server which of these files moved, those its filters take. */
|
|
384
|
+
async didRenameFiles(files: readonly RenamedFile[]): Promise<void> {
|
|
385
|
+
const taken: FileRename[] = renamesTaken(this.fileOperationFilters('didRename'), files);
|
|
386
|
+
if (taken.length > 0 && this.phase === 'ready') {
|
|
387
|
+
await this.notify('workspace/didRenameFiles', { files: taken });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* Registrations and the refresh requests of the server both land here, since each can change what a document may ask. */
|
|
392
|
+
onCapabilitiesChanged(listener: () => void): Disposable {
|
|
393
|
+
this.capabilityListeners.add(listener);
|
|
394
|
+
return {
|
|
395
|
+
dispose: () => {
|
|
396
|
+
this.capabilityListeners.delete(listener);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
shutdown(): Promise<void> {
|
|
402
|
+
this.shutdownPromise ??= this.stop();
|
|
403
|
+
return this.shutdownPromise;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* Called by a document that closed, so its diagnostics go with it. */
|
|
407
|
+
forgetDocument(document: LspDocument): void {
|
|
408
|
+
if (this.documents.get(document.uri) !== document) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
this.documents.delete(document.uri);
|
|
412
|
+
this.emitDiagnostics({ uri: document.uri, diagnostics: [] });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private assertReady(): void {
|
|
416
|
+
if (this.phase !== 'ready') {
|
|
417
|
+
throw new LspError(`LSP session is ${this.phase}; await initialize() first`, ErrorCodes.ServerNotInitialized);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private async stop(): Promise<void> {
|
|
422
|
+
try {
|
|
423
|
+
if (this.phase === 'initializing') {
|
|
424
|
+
await this.initialized;
|
|
425
|
+
}
|
|
426
|
+
if (this.phase === 'ready') {
|
|
427
|
+
this.phase = 'closing';
|
|
428
|
+
await Promise.all([...this.documents.values()].map((document) => document.close()));
|
|
429
|
+
await this.connection.request('shutdown', undefined, { timeoutMs: 5_000 });
|
|
430
|
+
await this.connection.notify('exit');
|
|
431
|
+
}
|
|
432
|
+
} finally {
|
|
433
|
+
await this.connection.close();
|
|
434
|
+
this.phase = 'closed';
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private readConfiguration(section?: string): unknown {
|
|
439
|
+
if (!section) {
|
|
440
|
+
return this.configuration;
|
|
441
|
+
}
|
|
442
|
+
if (Object.hasOwn(this.configuration, section)) {
|
|
443
|
+
return this.configuration[section];
|
|
444
|
+
}
|
|
445
|
+
let result: unknown = this.configuration;
|
|
446
|
+
for (const key of section.split('.')) {
|
|
447
|
+
if (!result || typeof result !== 'object' || !Object.hasOwn(result, key)) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
result = (result as Record<string, unknown>)[key];
|
|
451
|
+
}
|
|
452
|
+
return result ?? null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private emitDiagnostics(params: PublishDiagnosticsParams): void {
|
|
456
|
+
for (const listener of this.diagnosticListeners) {
|
|
457
|
+
try {
|
|
458
|
+
listener(params);
|
|
459
|
+
} catch (error) {
|
|
460
|
+
this.connection.reportError(error);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private emitCapabilities(): void {
|
|
466
|
+
for (const listener of this.capabilityListeners) {
|
|
467
|
+
try {
|
|
468
|
+
listener();
|
|
469
|
+
} catch (error) {
|
|
470
|
+
this.connection.reportError(error);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function clientCapabilities(options: LspSessionOptions): object {
|
|
477
|
+
const dynamic = { dynamicRegistration: true };
|
|
478
|
+
const symbolKind = { valueSet: Array.from({ length: 26 }, (_, index) => index + 1) };
|
|
479
|
+
return {
|
|
480
|
+
general: { positionEncodings: ['utf-16'] },
|
|
481
|
+
workspace: {
|
|
482
|
+
applyEdit: !!options.onApplyEdit,
|
|
483
|
+
workspaceEdit: { documentChanges: true, resourceOperations: ['rename'], failureHandling: 'abort' },
|
|
484
|
+
fileOperations: { dynamicRegistration: true, willRename: true, didRename: true },
|
|
485
|
+
configuration: true,
|
|
486
|
+
workspaceFolders: true,
|
|
487
|
+
executeCommand: dynamic,
|
|
488
|
+
didChangeWatchedFiles: { ...dynamic, relativePatternSupport: true },
|
|
489
|
+
symbol: { ...dynamic, symbolKind, tagSupport: { valueSet: [1] }, resolveSupport: { properties: ['location.range'] } },
|
|
490
|
+
semanticTokens: { refreshSupport: true },
|
|
491
|
+
inlayHint: { refreshSupport: true },
|
|
492
|
+
codeLens: { refreshSupport: true },
|
|
493
|
+
diagnostics: { refreshSupport: true }
|
|
494
|
+
},
|
|
495
|
+
window: { workDoneProgress: true, showMessage: { messageActionItem: { additionalPropertiesSupport: false } } },
|
|
496
|
+
textDocument: {
|
|
497
|
+
synchronization: { dynamicRegistration: false, didSave: true },
|
|
498
|
+
publishDiagnostics: {
|
|
499
|
+
versionSupport: true,
|
|
500
|
+
relatedInformation: true,
|
|
501
|
+
tagSupport: { valueSet: [1, 2] },
|
|
502
|
+
codeDescriptionSupport: true,
|
|
503
|
+
dataSupport: true
|
|
504
|
+
},
|
|
505
|
+
completion: {
|
|
506
|
+
...dynamic,
|
|
507
|
+
contextSupport: true,
|
|
508
|
+
completionItem: {
|
|
509
|
+
snippetSupport: options.snippetSupport ?? false,
|
|
510
|
+
commitCharactersSupport: true,
|
|
511
|
+
documentationFormat: ['markdown', 'plaintext'],
|
|
512
|
+
deprecatedSupport: true,
|
|
513
|
+
preselectSupport: true,
|
|
514
|
+
insertReplaceSupport: true,
|
|
515
|
+
labelDetailsSupport: true,
|
|
516
|
+
resolveSupport: { properties: ['documentation', 'detail', 'additionalTextEdits', 'command'] }
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
hover: { ...dynamic, contentFormat: ['markdown', 'plaintext'] },
|
|
520
|
+
signatureHelp: {
|
|
521
|
+
...dynamic,
|
|
522
|
+
contextSupport: true,
|
|
523
|
+
signatureInformation: {
|
|
524
|
+
documentationFormat: ['markdown', 'plaintext'],
|
|
525
|
+
parameterInformation: { labelOffsetSupport: true },
|
|
526
|
+
activeParameterSupport: true
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
definition: { ...dynamic, linkSupport: true },
|
|
530
|
+
declaration: { ...dynamic, linkSupport: true },
|
|
531
|
+
typeDefinition: { ...dynamic, linkSupport: true },
|
|
532
|
+
implementation: { ...dynamic, linkSupport: true },
|
|
533
|
+
references: dynamic,
|
|
534
|
+
documentHighlight: dynamic,
|
|
535
|
+
documentSymbol: { ...dynamic, symbolKind, hierarchicalDocumentSymbolSupport: true, tagSupport: { valueSet: [1] }, labelSupport: true },
|
|
536
|
+
rename: { ...dynamic, prepareSupport: true, prepareSupportDefaultBehavior: 1 },
|
|
537
|
+
codeAction: {
|
|
538
|
+
...dynamic,
|
|
539
|
+
dataSupport: true,
|
|
540
|
+
isPreferredSupport: true,
|
|
541
|
+
disabledSupport: true,
|
|
542
|
+
codeActionLiteralSupport: {
|
|
543
|
+
codeActionKind: {
|
|
544
|
+
valueSet: [
|
|
545
|
+
'',
|
|
546
|
+
'quickfix',
|
|
547
|
+
'refactor',
|
|
548
|
+
'refactor.extract',
|
|
549
|
+
'refactor.inline',
|
|
550
|
+
'refactor.rewrite',
|
|
551
|
+
'refactor.move',
|
|
552
|
+
'source',
|
|
553
|
+
'source.organizeImports',
|
|
554
|
+
'source.fixAll'
|
|
555
|
+
]
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
resolveSupport: { properties: ['edit', 'command'] }
|
|
559
|
+
},
|
|
560
|
+
codeLens: dynamic,
|
|
561
|
+
formatting: dynamic,
|
|
562
|
+
rangeFormatting: dynamic,
|
|
563
|
+
foldingRange: { ...dynamic, lineFoldingOnly: false, foldingRange: { collapsedText: false } },
|
|
564
|
+
selectionRange: dynamic,
|
|
565
|
+
semanticTokens: {
|
|
566
|
+
...dynamic,
|
|
567
|
+
requests: { range: true, full: { delta: true } },
|
|
568
|
+
tokenTypes: [
|
|
569
|
+
'namespace',
|
|
570
|
+
'type',
|
|
571
|
+
'class',
|
|
572
|
+
'enum',
|
|
573
|
+
'interface',
|
|
574
|
+
'struct',
|
|
575
|
+
'typeParameter',
|
|
576
|
+
'parameter',
|
|
577
|
+
'variable',
|
|
578
|
+
'property',
|
|
579
|
+
'enumMember',
|
|
580
|
+
'event',
|
|
581
|
+
'function',
|
|
582
|
+
'method',
|
|
583
|
+
'macro',
|
|
584
|
+
'keyword',
|
|
585
|
+
'modifier',
|
|
586
|
+
'comment',
|
|
587
|
+
'string',
|
|
588
|
+
'number',
|
|
589
|
+
'regexp',
|
|
590
|
+
'operator',
|
|
591
|
+
'decorator'
|
|
592
|
+
],
|
|
593
|
+
tokenModifiers: [
|
|
594
|
+
'declaration',
|
|
595
|
+
'definition',
|
|
596
|
+
'readonly',
|
|
597
|
+
'static',
|
|
598
|
+
'deprecated',
|
|
599
|
+
'abstract',
|
|
600
|
+
'async',
|
|
601
|
+
'modification',
|
|
602
|
+
'documentation',
|
|
603
|
+
'defaultLibrary'
|
|
604
|
+
],
|
|
605
|
+
formats: ['relative'],
|
|
606
|
+
overlappingTokenSupport: false,
|
|
607
|
+
multilineTokenSupport: false
|
|
608
|
+
},
|
|
609
|
+
inlayHint: { ...dynamic, resolveSupport: { properties: ['tooltip', 'textEdits', 'label.tooltip', 'label.location', 'label.command'] } },
|
|
610
|
+
diagnostic: { ...dynamic, relatedDocumentSupport: true }
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
}
|