@adecore/lsp 0.41.1 → 0.43.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/src/session.ts CHANGED
@@ -16,16 +16,31 @@ import type {
16
16
  FileOperationFilter,
17
17
  FileSystemWatcher,
18
18
  InitializeResult,
19
+ LogMessageParams,
20
+ LogTraceParams,
19
21
  LspTransport,
22
+ MessageActionItem,
23
+ NotebookDocument,
24
+ NotebookDocumentChangeEvent,
25
+ PreviousResultId,
20
26
  ProgressParams,
27
+ ProgressToken,
21
28
  ProviderOptions,
22
29
  PublishDiagnosticsParams,
23
30
  Registration,
24
31
  RequestOptions,
25
32
  ServerCapabilities,
33
+ ShowDocumentParams,
34
+ ShowDocumentResult,
35
+ ShowMessageParams,
36
+ ShowMessageRequestParams,
37
+ TextDocumentContentResult,
26
38
  TextDocumentSyncOptions,
39
+ TraceValue,
40
+ WorkspaceDiagnosticReport,
27
41
  WorkspaceEdit,
28
42
  WorkspaceFolder,
43
+ WorkspaceFoldersChangeEvent,
29
44
  WorkspaceSymbol,
30
45
  WorkspaceSymbolResult
31
46
  } from './protocol.ts';
@@ -53,7 +68,21 @@ export interface LspSessionOptions {
53
68
  resourceOperations?: readonly ResourceOperationKind[];
54
69
  onConfiguration?: (item: ConfigurationItem) => unknown | Promise<unknown>;
55
70
  onApplyEdit?: (params: ApplyWorkspaceEditParams) => ApplyWorkspaceEditResult | Promise<ApplyWorkspaceEditResult>;
56
- onShowMessage?: (params: { type: number; message: string; actions?: { title: string }[] }) => { title: string } | null | Promise<{ title: string } | null>;
71
+ /* Answers `window/showMessageRequest` with the action a person chose, or null when they dismissed it. */
72
+ onShowMessage?: (params: ShowMessageRequestParams) => MessageActionItem | null | Promise<MessageActionItem | null>;
73
+ /* A message the server wants a person to read, without an answer (`window/showMessage`). */
74
+ onMessage?: (params: ShowMessageParams) => void;
75
+ /* A line for a log of the server (`window/logMessage`). */
76
+ onLog?: (params: LogMessageParams) => void;
77
+ /* A trace of the server, sent while `trace` is not `off` (`$/logTrace`). */
78
+ onTrace?: (params: LogTraceParams) => void;
79
+ onTelemetry?: (data: unknown) => void;
80
+ /* Shows a document or a web address the server asks for; without it the client answers that it did not. */
81
+ onShowDocument?: (params: ShowDocumentParams) => ShowDocumentResult | Promise<ShowDocumentResult>;
82
+ /* The trace level sent with `initialize`; `setTrace` changes it later. `off` by default. */
83
+ trace?: TraceValue;
84
+ /* The language of the interface, such as `en` or `nl-NL`, for the server's messages. */
85
+ locale?: string;
57
86
  }
58
87
 
59
88
  /* The capability that advertises each method statically. */
@@ -91,7 +120,20 @@ const PROVIDERS: Record<string, keyof ServerCapabilities> = {
91
120
  'textDocument/prepareCallHierarchy': 'callHierarchyProvider',
92
121
  'callHierarchy/incomingCalls': 'callHierarchyProvider',
93
122
  'callHierarchy/outgoingCalls': 'callHierarchyProvider',
94
- 'textDocument/diagnostic': 'diagnosticProvider'
123
+ 'textDocument/diagnostic': 'diagnosticProvider',
124
+ 'workspace/diagnostic': 'diagnosticProvider',
125
+ 'textDocument/documentLink': 'documentLinkProvider',
126
+ 'documentLink/resolve': 'documentLinkProvider',
127
+ 'textDocument/documentColor': 'colorProvider',
128
+ 'textDocument/colorPresentation': 'colorProvider',
129
+ 'textDocument/linkedEditingRange': 'linkedEditingRangeProvider',
130
+ 'textDocument/moniker': 'monikerProvider',
131
+ 'textDocument/prepareTypeHierarchy': 'typeHierarchyProvider',
132
+ 'typeHierarchy/supertypes': 'typeHierarchyProvider',
133
+ 'typeHierarchy/subtypes': 'typeHierarchyProvider',
134
+ 'textDocument/inlineValue': 'inlineValueProvider',
135
+ 'textDocument/inlineCompletion': 'inlineCompletionProvider',
136
+ 'textDocument/rangesFormatting': 'documentRangeFormattingProvider'
95
137
  };
96
138
 
97
139
  /* A method whose dynamic registration is made under the name of another. */
@@ -106,10 +148,24 @@ const REGISTRATION_METHODS: Record<string, string> = {
106
148
  'workspaceSymbol/resolve': 'workspace/symbol',
107
149
  'textDocument/semanticTokens/full': 'textDocument/semanticTokens',
108
150
  'textDocument/semanticTokens/full/delta': 'textDocument/semanticTokens',
109
- 'textDocument/semanticTokens/range': 'textDocument/semanticTokens'
151
+ 'textDocument/semanticTokens/range': 'textDocument/semanticTokens',
152
+ 'documentLink/resolve': 'textDocument/documentLink',
153
+ 'textDocument/colorPresentation': 'textDocument/documentColor',
154
+ 'typeHierarchy/supertypes': 'textDocument/prepareTypeHierarchy',
155
+ 'typeHierarchy/subtypes': 'textDocument/prepareTypeHierarchy',
156
+ 'textDocument/rangesFormatting': 'textDocument/rangeFormatting',
157
+ 'workspace/diagnostic': 'textDocument/diagnostic'
110
158
  };
111
159
 
112
- const REFRESH_REQUESTS = ['workspace/semanticTokens/refresh', 'workspace/inlayHint/refresh', 'workspace/diagnostic/refresh', 'workspace/codeLens/refresh'];
160
+ const REFRESH_REQUESTS = [
161
+ 'workspace/semanticTokens/refresh',
162
+ 'workspace/inlayHint/refresh',
163
+ 'workspace/diagnostic/refresh',
164
+ 'workspace/codeLens/refresh',
165
+ 'workspace/inlineValue/refresh',
166
+ 'workspace/foldingRange/refresh',
167
+ 'workspace/textDocumentContent/refresh'
168
+ ];
113
169
 
114
170
  function matches(options: ProviderOptions, document?: Pick<DocumentItem, 'uri' | 'languageId'>): boolean {
115
171
  if (options.documentSelector == null) {
@@ -152,6 +208,14 @@ function offersMethod(method: string, options: ProviderOptions): boolean {
152
208
  return !!options.range;
153
209
  }
154
210
 
211
+ if (method === 'textDocument/rangesFormatting') {
212
+ return options.rangesSupport === true;
213
+ }
214
+
215
+ if (method === 'workspace/diagnostic') {
216
+ return options.workspaceDiagnostics === true;
217
+ }
218
+
155
219
  return true;
156
220
  }
157
221
 
@@ -169,6 +233,7 @@ export class LspSession {
169
233
  private readonly diagnosticListeners = new Set<(params: PublishDiagnosticsParams) => void>();
170
234
  private readonly capabilityListeners = new Set<() => void>();
171
235
  private configuration: Record<string, unknown>;
236
+ private folders: WorkspaceFolder[] | null;
172
237
 
173
238
  constructor(transport: LspTransport, options: LspSessionOptions = {}) {
174
239
  this.options = options;
@@ -200,6 +265,7 @@ export class LspSession {
200
265
  return Promise.all(items.map((item) => (options.onConfiguration ? options.onConfiguration(item) : this.readConfiguration(item.section))));
201
266
  });
202
267
 
268
+ this.folders = options.workspaceFolders ?? (options.rootUri ? [{ uri: options.rootUri, name: 'Workspace' }] : null);
203
269
  this.connection.onRequest('workspace/workspaceFolders', () => this.workspaceFolders);
204
270
 
205
271
  this.connection.onRequest(
@@ -207,11 +273,12 @@ export class LspSession {
207
273
  (params) => options.onApplyEdit?.(params as ApplyWorkspaceEditParams) ?? { applied: false, failureReason: 'No workspace edit handler is attached' }
208
274
  );
209
275
 
210
- this.connection.onRequest(
211
- 'window/showMessageRequest',
212
- (params) => options.onShowMessage?.(params as Parameters<NonNullable<LspSessionOptions['onShowMessage']>>[0]) ?? null
213
- );
214
-
276
+ this.connection.onRequest('window/showMessageRequest', (params) => options.onShowMessage?.(params as ShowMessageRequestParams) ?? null);
277
+ this.connection.onRequest('window/showDocument', (params) => options.onShowDocument?.(params as ShowDocumentParams) ?? { success: false });
278
+ this.connection.onNotification('window/showMessage', (params) => options.onMessage?.(params as ShowMessageParams));
279
+ this.connection.onNotification('window/logMessage', (params) => options.onLog?.(params as LogMessageParams));
280
+ this.connection.onNotification('$/logTrace', (params) => options.onTrace?.(params as LogTraceParams));
281
+ this.connection.onNotification('telemetry/event', (params) => options.onTelemetry?.(params));
215
282
  this.connection.onRequest('window/workDoneProgress/create', () => null);
216
283
 
217
284
  this.connection.onRequest('client/registerCapability', (params) => {
@@ -257,7 +324,7 @@ export class LspSession {
257
324
  }
258
325
 
259
326
  get workspaceFolders(): WorkspaceFolder[] | null {
260
- return this.options.workspaceFolders ?? (this.options.rootUri ? [{ uri: this.options.rootUri, name: 'Workspace' }] : null);
327
+ return this.folders === null ? null : [...this.folders];
261
328
  }
262
329
 
263
330
  get syncOptions(): TextDocumentSyncOptions {
@@ -284,6 +351,8 @@ export class LspSession {
284
351
  rootUri: this.options.rootUri ?? null,
285
352
  workspaceFolders: this.workspaceFolders,
286
353
  clientInfo: this.options.clientInfo ?? { name: '@adecore/lsp' },
354
+ locale: this.options.locale,
355
+ trace: this.options.trace ?? 'off',
287
356
  capabilities: clientCapabilities(this.options),
288
357
  initializationOptions: this.options.initializationOptions
289
358
  });
@@ -342,7 +411,17 @@ export class LspSession {
342
411
  }
343
412
  }
344
413
 
345
- const provider = this.serverCapabilities[PROVIDERS[method]];
414
+ if (method === 'textDocument/willSaveWaitUntil' && this.syncOptions.willSaveWaitUntil) {
415
+ candidates.push({});
416
+ }
417
+
418
+ const content = this.serverCapabilities.workspace?.textDocumentContent;
419
+
420
+ if (method === 'workspace/textDocumentContent' && content !== undefined) {
421
+ candidates.push({ schemes: content.schemes });
422
+ }
423
+
424
+ const provider = PROVIDERS[method] === undefined ? undefined : this.serverCapabilities[PROVIDERS[method]];
346
425
 
347
426
  if (provider === true) {
348
427
  candidates.push({});
@@ -393,6 +472,103 @@ export class LspSession {
393
472
  return this.request('workspaceSymbol/resolve', symbol, options);
394
473
  }
395
474
 
475
+ /*
476
+ * The diagnostics of every file of the project, for a server that offers them. `previousResultIds` are what the
477
+ * client last heard per document, so the server can answer `unchanged`; `onPartialResult` streams reports while it works.
478
+ */
479
+ workspaceDiagnostics(
480
+ previousResultIds: readonly PreviousResultId[] = [],
481
+ identifier?: string,
482
+ options?: RequestOptions
483
+ ): Promise<WorkspaceDiagnosticReport> {
484
+ if (!this.supports('workspace/diagnostic')) {
485
+ return Promise.reject(new LspError('Server does not support workspace diagnostics', ErrorCodes.MethodNotFound));
486
+ }
487
+ return this.request('workspace/diagnostic', { identifier, previousResultIds: [...previousResultIds] }, options);
488
+ }
489
+
490
+ /* The text of a document of a scheme the server owns, such as a decompiled class, which is shown read-only. */
491
+ textDocumentContent(uri: string, options?: RequestOptions): Promise<TextDocumentContentResult> {
492
+ const scheme = uri.slice(0, Math.max(0, uri.indexOf(':')));
493
+ if (!this.providerOptions('workspace/textDocumentContent')?.schemes?.includes(scheme)) {
494
+ return Promise.reject(new LspError(`Server does not serve documents of ${scheme}:`, ErrorCodes.MethodNotFound));
495
+ }
496
+ return this.request('workspace/textDocumentContent', { uri }, options);
497
+ }
498
+
499
+ /* Changes the trace level the server sends `$/logTrace` at. */
500
+ setTrace(value: TraceValue): Promise<void> {
501
+ return this.notify('$/setTrace', { value });
502
+ }
503
+
504
+ /* Asks the server to stop work it reported as cancellable under `token`. */
505
+ cancelWorkDoneProgress(token: ProgressToken): Promise<void> {
506
+ return this.notify('window/workDoneProgress/cancel', { token });
507
+ }
508
+
509
+ /* Adds and removes folders of the workspace, and tells a server that asked to hear about it. */
510
+ async changeWorkspaceFolders(event: WorkspaceFoldersChangeEvent): Promise<void> {
511
+ const removed = new Set(event.removed.map((folder) => fileUriKey(folder.uri)));
512
+ const kept = (this.folders ?? []).filter((folder) => !removed.has(fileUriKey(folder.uri)));
513
+ const known = new Set(kept.map((folder) => fileUriKey(folder.uri)));
514
+ this.folders = [...kept, ...event.added.filter((folder) => !known.has(fileUriKey(folder.uri)))];
515
+ const notifications = this.serverCapabilities.workspace?.workspaceFolders?.changeNotifications;
516
+ const registered = [...this.registrations.values()].some((registration) => registration.method === 'workspace/didChangeWorkspaceFolders');
517
+ if (this.phase === 'ready' && (notifications === true || typeof notifications === 'string' || registered)) {
518
+ await this.notify('workspace/didChangeWorkspaceFolders', { event: { added: [...event.added], removed: [...event.removed] } });
519
+ }
520
+ }
521
+
522
+ /* Whether the server synchronizes notebooks of this type and scheme; the `notebook*` methods do nothing for one it does not. */
523
+ syncsNotebook(notebook: Pick<NotebookDocument, 'uri' | 'notebookType'>): boolean {
524
+ const selectors = this.serverCapabilities.notebookDocumentSync?.notebookSelector ?? [];
525
+ const scheme = notebook.uri.slice(0, Math.max(0, notebook.uri.indexOf(':')));
526
+ return selectors.some(({ notebook: filter }) => {
527
+ if (filter === undefined) {
528
+ return true;
529
+ }
530
+ if (typeof filter === 'string') {
531
+ return filter === '*' || filter === notebook.notebookType;
532
+ }
533
+ return (
534
+ (!filter.notebookType || filter.notebookType === notebook.notebookType) &&
535
+ (!filter.scheme || filter.scheme === scheme) &&
536
+ (!filter.pattern || globMatch(decodeURIComponent(new URL(notebook.uri).pathname), filter.pattern))
537
+ );
538
+ });
539
+ }
540
+
541
+ /* Opens a notebook with the documents of its cells, which do not go through `openDocument`. */
542
+ async openNotebook(notebook: NotebookDocument, cellTextDocuments: readonly DocumentItem[]): Promise<void> {
543
+ if (this.syncsNotebook(notebook)) {
544
+ await this.notify('notebookDocument/didOpen', {
545
+ notebookDocument: notebook,
546
+ cellTextDocuments: cellTextDocuments.map((item) => ({ ...item, version: item.version ?? 0 }))
547
+ });
548
+ }
549
+ }
550
+
551
+ async changeNotebook(notebook: Pick<NotebookDocument, 'uri' | 'notebookType' | 'version'>, change: NotebookDocumentChangeEvent): Promise<void> {
552
+ if (this.syncsNotebook(notebook)) {
553
+ await this.notify('notebookDocument/didChange', { notebookDocument: { uri: notebook.uri, version: notebook.version }, change });
554
+ }
555
+ }
556
+
557
+ async saveNotebook(notebook: Pick<NotebookDocument, 'uri' | 'notebookType'>): Promise<void> {
558
+ if (this.syncsNotebook(notebook) && this.serverCapabilities.notebookDocumentSync?.save === true) {
559
+ await this.notify('notebookDocument/didSave', { notebookDocument: { uri: notebook.uri } });
560
+ }
561
+ }
562
+
563
+ async closeNotebook(notebook: Pick<NotebookDocument, 'uri' | 'notebookType'>, cellUris: readonly string[]): Promise<void> {
564
+ if (this.syncsNotebook(notebook)) {
565
+ await this.notify('notebookDocument/didClose', {
566
+ notebookDocument: { uri: notebook.uri },
567
+ cellTextDocuments: cellUris.map((uri) => ({ uri }))
568
+ });
569
+ }
570
+ }
571
+
396
572
  async setConfiguration(settings: Record<string, unknown>): Promise<void> {
397
573
  this.configuration = settings;
398
574
  await this.notify('workspace/didChangeConfiguration', { settings });
@@ -591,7 +767,13 @@ function clientCapabilities(options: LspSessionOptions): object {
591
767
  const symbolKind = { valueSet: Array.from({ length: 26 }, (_, index) => index + 1) };
592
768
 
593
769
  return {
594
- general: { positionEncodings: ['utf-16'] },
770
+ general: {
771
+ positionEncodings: ['utf-16'],
772
+ staleRequestSupport: { cancel: true, retryOnContentModified: [] },
773
+ regularExpressions: { engine: 'ECMAScript', version: 'ES2020' },
774
+ markdown: { parser: 'remark', version: '15' }
775
+ },
776
+ notebookDocument: { synchronization: { dynamicRegistration: false, executionSummarySupport: true } },
595
777
  workspace: {
596
778
  applyEdit: !!options.onApplyEdit,
597
779
  workspaceEdit: { documentChanges: true, resourceOperations: [...(options.resourceOperations ?? ['create', 'rename'])], failureHandling: 'abort' },
@@ -612,11 +794,18 @@ function clientCapabilities(options: LspSessionOptions): object {
612
794
  semanticTokens: { refreshSupport: true },
613
795
  inlayHint: { refreshSupport: true },
614
796
  codeLens: { refreshSupport: true },
615
- diagnostics: { refreshSupport: true }
797
+ diagnostics: { refreshSupport: true },
798
+ inlineValue: { refreshSupport: true },
799
+ foldingRange: { refreshSupport: true },
800
+ textDocumentContent: { dynamicRegistration: false }
801
+ },
802
+ window: {
803
+ workDoneProgress: true,
804
+ showMessage: { messageActionItem: { additionalPropertiesSupport: false } },
805
+ showDocument: { support: !!options.onShowDocument }
616
806
  },
617
- window: { workDoneProgress: true, showMessage: { messageActionItem: { additionalPropertiesSupport: false } } },
618
807
  textDocument: {
619
- synchronization: { dynamicRegistration: false, didSave: true },
808
+ synchronization: { dynamicRegistration: false, willSave: true, willSaveWaitUntil: true, didSave: true },
620
809
  publishDiagnostics: {
621
810
  versionSupport: true,
622
811
  relatedInformation: true,
@@ -628,6 +817,7 @@ function clientCapabilities(options: LspSessionOptions): object {
628
817
  ...dynamic,
629
818
  contextSupport: true,
630
819
  insertTextMode: 1,
820
+ completionItemKind: { valueSet: Array.from({ length: 25 }, (_, index) => index + 1) },
631
821
  ...(options.completionItemDefaults?.length ? { completionList: { itemDefaults: [...options.completionItemDefaults] } } : {}),
632
822
  completionItem: {
633
823
  snippetSupport: options.snippetSupport ?? false,
@@ -635,6 +825,7 @@ function clientCapabilities(options: LspSessionOptions): object {
635
825
  commitCharactersSupport: true,
636
826
  documentationFormat: ['markdown', 'plaintext'],
637
827
  deprecatedSupport: true,
828
+ tagSupport: { valueSet: [1] },
638
829
  preselectSupport: true,
639
830
  insertReplaceSupport: true,
640
831
  labelDetailsSupport: true,
@@ -682,11 +873,23 @@ function clientCapabilities(options: LspSessionOptions): object {
682
873
  },
683
874
  resolveSupport: { properties: ['edit', 'command'] }
684
875
  },
685
- codeLens: dynamic,
876
+ codeLens: { ...dynamic, resolveSupport: { properties: ['command'] } },
686
877
  formatting: dynamic,
687
- rangeFormatting: dynamic,
878
+ rangeFormatting: { ...dynamic, rangesSupport: true },
688
879
  onTypeFormatting: dynamic,
689
- foldingRange: { ...dynamic, lineFoldingOnly: false, foldingRange: { collapsedText: false } },
880
+ foldingRange: {
881
+ ...dynamic,
882
+ lineFoldingOnly: false,
883
+ foldingRangeKind: { valueSet: ['comment', 'imports', 'region'] },
884
+ foldingRange: { collapsedText: false }
885
+ },
886
+ documentLink: { ...dynamic, tooltipSupport: true },
887
+ colorProvider: dynamic,
888
+ linkedEditingRange: dynamic,
889
+ moniker: dynamic,
890
+ typeHierarchy: dynamic,
891
+ inlineValue: dynamic,
892
+ inlineCompletion: dynamic,
690
893
  selectionRange: dynamic,
691
894
  semanticTokens: {
692
895
  ...dynamic,
@@ -0,0 +1,60 @@
1
+ import { ErrorCodes, LspError } from './connection.ts';
2
+ import type { Disposable } from './protocol.ts';
3
+ import type { LanguageService } from './service.ts';
4
+
5
+ const REQUESTS = [
6
+ 'completion',
7
+ 'resolveCompletion',
8
+ 'hover',
9
+ 'signatureHelp',
10
+ 'definition',
11
+ 'declaration',
12
+ 'typeDefinition',
13
+ 'implementation',
14
+ 'references',
15
+ 'documentHighlights',
16
+ 'documentSymbols',
17
+ 'prepareRename',
18
+ 'rename',
19
+ 'codeActions',
20
+ 'resolveCodeAction',
21
+ 'executeCommand',
22
+ 'formatting',
23
+ 'rangeFormatting',
24
+ 'semanticTokens',
25
+ 'semanticTokensDelta',
26
+ 'semanticTokensRange',
27
+ 'inlayHints',
28
+ 'resolveInlayHint',
29
+ 'workspaceSymbols',
30
+ 'foldingRanges',
31
+ 'selectionRanges',
32
+ 'codeLenses',
33
+ 'resolveCodeLens'
34
+ ] as const satisfies readonly (keyof LanguageService)[];
35
+
36
+ const NOTHING: Disposable = { dispose: () => undefined };
37
+
38
+ function refuse(): Promise<never> {
39
+ return Promise.reject(new LspError('No language server serves this document', ErrorCodes.MethodNotFound));
40
+ }
41
+
42
+ /*
43
+ * A service for documents no server serves: it takes documents and answers `supports` with false, so a
44
+ * feature that checks first never asks, and rejects every request with an `LspError` of code -32601.
45
+ */
46
+ export function unservedLanguageService(): LanguageService {
47
+ // Every request has the same refusal whatever its parameters, which the record type cannot say.
48
+ const requests = Object.fromEntries(REQUESTS.map((name) => [name, refuse])) as unknown as Pick<LanguageService, (typeof REQUESTS)[number]>;
49
+
50
+ return {
51
+ ...requests,
52
+ openDocument: () => Promise.resolve(),
53
+ changeDocument: () => Promise.resolve(),
54
+ closeDocument: () => Promise.resolve(),
55
+ supports: () => false,
56
+ providerOptions: () => undefined,
57
+ onProvidersChanged: () => NOTHING,
58
+ onDiagnostics: () => NOTHING
59
+ };
60
+ }