@adecore/lsp 0.22.1 → 0.23.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.
@@ -0,0 +1,232 @@
1
+ import { ErrorCodes, LspError } from './connection.js';
2
+ import { applyTextEdits } from './edits.js';
3
+ /* The key a URI is known by: a folder spelled with a trailing slash is the same folder, except the root. */
4
+ function keyOf(uri) {
5
+ const root = /^[^/:]+:\/\/[^/]*\/+$/.test(uri);
6
+ return root ? uri : uri.replace(/\/+$/, '');
7
+ }
8
+ function isUnder(uri, folder) {
9
+ return uri.startsWith(folder.endsWith('/') ? folder : `${folder}/`);
10
+ }
11
+ function ancestorsOf(uri) {
12
+ const found = [];
13
+ let end = uri.lastIndexOf('/');
14
+ while (end > 0) {
15
+ found.push(uri.slice(0, end));
16
+ end = uri.lastIndexOf('/', end - 1);
17
+ }
18
+ return found;
19
+ }
20
+ /*
21
+ * Plans a workspace edit with its resource operations without changing anything, and refuses (an `LspError`)
22
+ * the first thing that does not fit. Folders are matched by URI prefix, as written, with a trailing slash
23
+ * ignored: the URIs of one edit are expected to be spelled the same. Each URI is read from `source` at most once.
24
+ *
25
+ * Checks, in the order the edit gives its changes:
26
+ * - A version in a text edit must equal the source's version. A file no editor holds has no version to check,
27
+ * so a version on it is refused with `ContentModified`. When the source reports no version for an open file,
28
+ * the version is not enforced and the host's check of the text stands in. A file the plan makes is not checked.
29
+ * - An operation outside `source.operations` is refused.
30
+ * - A create over a file that is there is refused, unless it says `overwrite` (planned as an overwrite) or
31
+ * `ignoreIfExists` (left out). A rename onto a target that is there works the same way; a rename of
32
+ * something that is not there is always refused. Without `source.exists`, that is left to the host.
33
+ * - A delete of something that is not there is refused unless it says `ignoreIfNotExists`. Emptiness of a
34
+ * folder cannot be told, so deleting a folder, or anything that exists and is no text file when the source
35
+ * cannot say which, needs `recursive`.
36
+ * - Text edits to a URI after it was deleted or renamed away are refused. Edits to a renamed file are read
37
+ * from where it stood, and edits to a file the plan makes become its text.
38
+ */
39
+ export async function planWorkspaceOperations(edit, source) {
40
+ const probes = new Map();
41
+ const folders = new Map();
42
+ // null is a tombstone: the URI, and everything under it, is gone.
43
+ const overlay = new Map();
44
+ // Renames in order, to find where a file under a moved folder was read from.
45
+ const moves = [];
46
+ const steps = [];
47
+ const probe = (uri) => {
48
+ let found = probes.get(uri);
49
+ if (found === undefined) {
50
+ found = (async () => {
51
+ const text = await source.read(uri);
52
+ return { text, exists: text !== null ? true : source.exists === undefined ? null : await source.exists(uri) };
53
+ })();
54
+ probes.set(uri, found);
55
+ }
56
+ return found;
57
+ };
58
+ const folderAt = (uri) => {
59
+ let found = folders.get(uri);
60
+ if (found === undefined) {
61
+ found = source.isFolder === undefined ? Promise.resolve(true) : source.isFolder(uri);
62
+ folders.set(uri, found);
63
+ }
64
+ return found;
65
+ };
66
+ const originOf = (uri) => {
67
+ let origin = uri;
68
+ for (let i = moves.length - 1; i >= 0; i--) {
69
+ const move = moves[i];
70
+ if (origin === move.newUri || isUnder(origin, move.newUri)) {
71
+ origin = move.oldUri + origin.slice(move.newUri.length);
72
+ }
73
+ }
74
+ return origin;
75
+ };
76
+ const lookup = async (uri) => {
77
+ if (overlay.has(uri)) {
78
+ return overlay.get(uri);
79
+ }
80
+ for (const ancestor of ancestorsOf(uri)) {
81
+ if (overlay.has(ancestor)) {
82
+ const node = overlay.get(ancestor);
83
+ if (node === null || node.kind === 'text') {
84
+ return null;
85
+ }
86
+ break;
87
+ }
88
+ }
89
+ const origin = originOf(uri);
90
+ const found = await probe(origin);
91
+ let node = null;
92
+ if (found.text !== null) {
93
+ node = { kind: 'text', text: found.text.text, version: found.text.version, open: found.text.open, readUri: origin, create: null };
94
+ }
95
+ else if (found.exists !== false) {
96
+ node = { kind: found.exists === null ? 'maybe' : 'other', text: '', version: null, open: false, readUri: origin, create: null };
97
+ }
98
+ if (node !== null) {
99
+ overlay.set(uri, node);
100
+ }
101
+ return node;
102
+ };
103
+ const demand = (kind, uri) => {
104
+ if (!source.operations.includes(kind)) {
105
+ const verb = kind === 'create' ? 'Creating' : kind === 'rename' ? 'Moving' : 'Deleting';
106
+ throw new LspError(`${verb} ${uri} is not supported here`);
107
+ }
108
+ };
109
+ const forget = (uri) => {
110
+ for (const key of [...overlay.keys()]) {
111
+ if (key === uri || isUnder(key, uri)) {
112
+ overlay.delete(key);
113
+ }
114
+ }
115
+ };
116
+ const keyed = (change) => {
117
+ if ('textDocument' in change) {
118
+ return { ...change, textDocument: { ...change.textDocument, uri: keyOf(change.textDocument.uri) } };
119
+ }
120
+ if (change.kind === 'rename') {
121
+ return { ...change, oldUri: keyOf(change.oldUri), newUri: keyOf(change.newUri) };
122
+ }
123
+ return { ...change, uri: keyOf(change.uri) };
124
+ };
125
+ const changes = edit.documentChanges ?? Object.entries(edit.changes ?? {}).map(([uri, edits]) => ({ textDocument: { uri, version: null }, edits }));
126
+ for (const original of changes) {
127
+ const change = keyed(original);
128
+ if ('textDocument' in change) {
129
+ const { uri, version } = change.textDocument;
130
+ const node = await lookup(uri);
131
+ if (node === null || node.kind !== 'text') {
132
+ throw new LspError(`${uri} cannot be read as text`);
133
+ }
134
+ if (version !== null && node.create === null) {
135
+ if (!node.open) {
136
+ throw new LspError(`The version of ${uri} cannot be checked because no editor holds it`, ErrorCodes.ContentModified);
137
+ }
138
+ if (node.version !== null && node.version !== version) {
139
+ throw new LspError(`Workspace edit version mismatch: ${uri}`, ErrorCodes.ContentModified);
140
+ }
141
+ }
142
+ const after = applyTextEdits(node.text, change.edits);
143
+ if (node.create !== null) {
144
+ node.create.text = after;
145
+ }
146
+ else {
147
+ steps.push({
148
+ kind: 'edits',
149
+ uri,
150
+ readUri: node.readUri,
151
+ edits: change.edits,
152
+ before: node.text,
153
+ after,
154
+ version: node.version,
155
+ open: node.open
156
+ });
157
+ }
158
+ node.text = after;
159
+ }
160
+ else if (change.kind === 'create') {
161
+ demand('create', change.uri);
162
+ const found = await lookup(change.uri);
163
+ const there = found?.kind === 'maybe' ? null : found;
164
+ const overwrite = change.options?.overwrite === true;
165
+ if (there !== null && !overwrite) {
166
+ if (change.options?.ignoreIfExists === true) {
167
+ continue;
168
+ }
169
+ throw new LspError(`${change.uri} already exists`);
170
+ }
171
+ if (there !== null && there.kind === 'other') {
172
+ throw new LspError(`${change.uri} already exists and is not a text file`);
173
+ }
174
+ const made = { kind: 'create', uri: change.uri, text: '', overwrite: there !== null };
175
+ steps.push(made);
176
+ overlay.set(change.uri, { kind: 'text', text: '', version: null, open: false, readUri: null, create: made });
177
+ }
178
+ else if (change.kind === 'rename') {
179
+ demand('rename', change.oldUri);
180
+ const { oldUri, newUri } = change;
181
+ const from = await lookup(oldUri);
182
+ if (from === null) {
183
+ throw new LspError(`${oldUri} does not exist`);
184
+ }
185
+ if (oldUri === newUri) {
186
+ continue;
187
+ }
188
+ const target = await lookup(newUri);
189
+ const taken = target?.kind === 'maybe' ? null : target;
190
+ const overwrite = change.options?.overwrite === true;
191
+ if (taken !== null && !overwrite) {
192
+ if (change.options?.ignoreIfExists === true) {
193
+ continue;
194
+ }
195
+ throw new LspError(`${newUri} already exists`);
196
+ }
197
+ const carried = [];
198
+ for (const [key, node] of overlay) {
199
+ if (key !== oldUri && isUnder(key, oldUri)) {
200
+ carried.push([newUri + key.slice(oldUri.length), node]);
201
+ }
202
+ }
203
+ forget(newUri);
204
+ forget(oldUri);
205
+ overlay.set(oldUri, null);
206
+ overlay.set(newUri, from);
207
+ for (const [key, node] of carried) {
208
+ overlay.set(key, node);
209
+ }
210
+ moves.push({ oldUri, newUri });
211
+ steps.push({ kind: 'rename', oldUri, newUri, overwrite: taken !== null });
212
+ }
213
+ else {
214
+ demand('delete', change.uri);
215
+ const found = await lookup(change.uri);
216
+ if (found === null) {
217
+ if (change.options?.ignoreIfNotExists === true) {
218
+ continue;
219
+ }
220
+ throw new LspError(`${change.uri} does not exist`);
221
+ }
222
+ const recursive = change.options?.recursive === true;
223
+ if (found.kind !== 'text' && !recursive && (await folderAt(found.readUri ?? change.uri))) {
224
+ throw new LspError(`${change.uri} is a folder, or cannot be told from one, and is only deleted with recursive set`);
225
+ }
226
+ forget(change.uri);
227
+ overlay.set(change.uri, null);
228
+ steps.push({ kind: 'delete', uri: change.uri, recursive });
229
+ }
230
+ }
231
+ return { steps: steps.map((step) => (step.kind === 'create' ? { ...step } : step)) };
232
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/lsp",
3
- "version": "0.22.1",
3
+ "version": "0.23.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -0,0 +1,97 @@
1
+ import type { CompletionItem, CompletionList, Range } from './protocol.ts';
2
+
3
+ function validRange(range: Range, singleLine = true): boolean {
4
+ if (range === null || typeof range !== 'object') {
5
+ return false;
6
+ }
7
+ const { start, end } = range;
8
+ return (
9
+ start !== undefined &&
10
+ start !== null &&
11
+ typeof start === 'object' &&
12
+ end !== undefined &&
13
+ end !== null &&
14
+ typeof end === 'object' &&
15
+ [start.line, start.character, end.line, end.character].every((value) => Number.isSafeInteger(value) && value >= 0) &&
16
+ (!singleLine || start.line === end.line) &&
17
+ (start.line < end.line || (start.line === end.line && start.character <= end.character))
18
+ );
19
+ }
20
+
21
+ function validEditRange(range: Range | { insert: Range; replace: Range }, singleLine = true): boolean {
22
+ if (range === null || typeof range !== 'object' || Array.isArray(range)) {
23
+ return false;
24
+ }
25
+ if ('start' in range) {
26
+ return validRange(range, singleLine);
27
+ }
28
+ return (
29
+ validRange(range.insert) &&
30
+ validRange(range.replace) &&
31
+ range.insert.start.line === range.replace.start.line &&
32
+ range.insert.start.character === range.replace.start.character &&
33
+ range.insert.end.character <= range.replace.end.character
34
+ );
35
+ }
36
+
37
+ export function applyCompletionDefaults(
38
+ item: CompletionItem,
39
+ defaults?: CompletionList['itemDefaults'],
40
+ options?: { onInvalidRange?: (error: RangeError) => void }
41
+ ): CompletionItem {
42
+ if (typeof item?.label !== 'string') {
43
+ throw new TypeError('A completion item must have a string label.');
44
+ }
45
+ for (const value of [item.filterText, item.sortText, item.insertText, item.textEditText]) {
46
+ if (value !== undefined && typeof value !== 'string') {
47
+ throw new TypeError('Completion ranking and insertion text must be strings.');
48
+ }
49
+ }
50
+ if (item.textEdit !== undefined) {
51
+ const edit = item.textEdit;
52
+ if (
53
+ edit === null ||
54
+ typeof edit !== 'object' ||
55
+ typeof edit.newText !== 'string' ||
56
+ !('range' in edit ? validEditRange(edit.range, false) : validEditRange(edit))
57
+ ) {
58
+ throw new RangeError('Invalid completion item edit.');
59
+ }
60
+ }
61
+ if (defaults === undefined) {
62
+ return item;
63
+ }
64
+ const next = { ...item };
65
+ if (next.commitCharacters === undefined && defaults.commitCharacters !== undefined) {
66
+ next.commitCharacters = defaults.commitCharacters;
67
+ }
68
+ if (next.insertTextFormat === undefined && defaults.insertTextFormat !== undefined) {
69
+ next.insertTextFormat = defaults.insertTextFormat;
70
+ }
71
+ if (next.insertTextMode === undefined && defaults.insertTextMode !== undefined) {
72
+ next.insertTextMode = defaults.insertTextMode;
73
+ }
74
+ if (next.data === undefined && defaults.data !== undefined) {
75
+ next.data = defaults.data;
76
+ }
77
+ if (next.textEdit === undefined && defaults.editRange !== undefined) {
78
+ const range = defaults.editRange;
79
+ const newText = item.textEditText ?? item.label;
80
+ if (typeof newText !== 'string') {
81
+ throw new TypeError('Completion edit text must be a string.');
82
+ }
83
+ const invalid = (message: string): CompletionItem => {
84
+ const error = new RangeError(message);
85
+ if (options?.onInvalidRange === undefined) {
86
+ throw error;
87
+ }
88
+ options.onInvalidRange(error);
89
+ return next;
90
+ };
91
+ if (!validEditRange(range)) {
92
+ return invalid('Invalid completion default edit range.');
93
+ }
94
+ next.textEdit = 'start' in range ? { range, newText } : { insert: range.insert, replace: range.replace, newText };
95
+ }
96
+ return next;
97
+ }
package/src/document.ts CHANGED
@@ -2,6 +2,9 @@ import { ErrorCodes, LspError, StaleResultError } from './connection.ts';
2
2
  import { applyContentChanges, minimalChange } from './edits.ts';
3
3
  import type { LspSession } from './session.ts';
4
4
  import type {
5
+ CallHierarchyIncomingCall,
6
+ CallHierarchyItem,
7
+ CallHierarchyOutgoingCall,
5
8
  CodeAction,
6
9
  CodeActionContext,
7
10
  CodeLens,
@@ -210,6 +213,11 @@ export class LspDocument {
210
213
  return this.feature('textDocument/rangeFormatting', { range, options: formatting }, options);
211
214
  }
212
215
 
216
+ /* `ch` is the character that was typed; `position` is where the caret is after it. */
217
+ onTypeFormatting(position: Position, ch: string, formatting: FormattingOptions, options?: DocumentRequestOptions): Promise<TextEdit[] | null> {
218
+ return this.feature('textDocument/onTypeFormatting', { position, ch, options: formatting }, options);
219
+ }
220
+
213
221
  foldingRanges(options?: DocumentRequestOptions): Promise<FoldingRange[] | null> {
214
222
  return this.feature('textDocument/foldingRange', {}, options);
215
223
  }
@@ -238,6 +246,19 @@ export class LspDocument {
238
246
  return this.resolve('inlayHint/resolve', hint, options);
239
247
  }
240
248
 
249
+ prepareCallHierarchy(position: Position, options?: DocumentRequestOptions): Promise<CallHierarchyItem[] | null> {
250
+ return this.feature('textDocument/prepareCallHierarchy', { position }, options);
251
+ }
252
+
253
+ /* The callers of an item the server gave. Neighbouring expansions run beside each other, so a newer one does not cancel an older unless `cancelPrevious` says so. */
254
+ incomingCalls(item: CallHierarchyItem, options?: DocumentRequestOptions): Promise<CallHierarchyIncomingCall[] | null> {
255
+ return this.perform('callHierarchy/incomingCalls', { item }, { cancelPrevious: false, ...options });
256
+ }
257
+
258
+ outgoingCalls(item: CallHierarchyItem, options?: DocumentRequestOptions): Promise<CallHierarchyOutgoingCall[] | null> {
259
+ return this.perform('callHierarchy/outgoingCalls', { item }, { cancelPrevious: false, ...options });
260
+ }
261
+
241
262
  diagnostics(previousResultId?: string, identifier?: string, options?: DocumentRequestOptions): Promise<DocumentDiagnosticReport> {
242
263
  return this.feature('textDocument/diagnostic', { previousResultId, identifier }, options);
243
264
  }
@@ -1,19 +1,29 @@
1
1
  import { globMatch } from './glob.ts';
2
- import type { FileOperationFilter, FileRename } from './protocol.ts';
2
+ import type { FileCreate, FileDelete, FileOperationFilter, FileRename } from './protocol.ts';
3
3
 
4
4
  /* A file or folder that moves, which says what it is since a filter may take only one of the two. */
5
5
  export interface RenamedFile extends FileRename {
6
6
  directory: boolean;
7
7
  }
8
8
 
9
- function filterTakes(filter: FileOperationFilter, file: RenamedFile): boolean {
9
+ /* A file or folder that is made. */
10
+ export interface CreatedFile extends FileCreate {
11
+ directory: boolean;
12
+ }
13
+
14
+ /* A file or folder that is deleted. */
15
+ export interface DeletedFile extends FileDelete {
16
+ directory: boolean;
17
+ }
18
+
19
+ function filterTakes(filter: FileOperationFilter, uri: string, directory: boolean): boolean {
10
20
  const { matches, glob, options } = filter.pattern;
11
- if (matches !== undefined && (matches === 'folder') !== file.directory) {
21
+ if (matches !== undefined && (matches === 'folder') !== directory) {
12
22
  return false;
13
23
  }
14
24
  let url: URL;
15
25
  try {
16
- url = new URL(file.oldUri);
26
+ url = new URL(uri);
17
27
  } catch {
18
28
  return false;
19
29
  }
@@ -26,5 +36,15 @@ function filterTakes(filter: FileOperationFilter, file: RenamedFile): boolean {
26
36
 
27
37
  /* The renames a server asked to hear about, by the filters it registered. A rename is matched on the place it leaves. */
28
38
  export function renamesTaken(filters: readonly FileOperationFilter[], files: readonly RenamedFile[]): FileRename[] {
29
- return files.filter((file) => filters.some((filter) => filterTakes(filter, file))).map(({ oldUri, newUri }) => ({ oldUri, newUri }));
39
+ return files.filter((file) => filters.some((filter) => filterTakes(filter, file.oldUri, file.directory))).map(({ oldUri, newUri }) => ({ oldUri, newUri }));
40
+ }
41
+
42
+ /* The files made that a server asked to hear about, by the filters it registered. */
43
+ export function createsTaken(filters: readonly FileOperationFilter[], files: readonly CreatedFile[]): FileCreate[] {
44
+ return files.filter((file) => filters.some((filter) => filterTakes(filter, file.uri, file.directory))).map(({ uri }) => ({ uri }));
45
+ }
46
+
47
+ /* The files deleted that a server asked to hear about, by the filters it registered. A folder is matched as one, not by what was in it. */
48
+ export function deletesTaken(filters: readonly FileOperationFilter[], files: readonly DeletedFile[]): FileDelete[] {
49
+ return files.filter((file) => filters.some((filter) => filterTakes(filter, file.uri, file.directory))).map(({ uri }) => ({ uri }));
30
50
  }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { ErrorCodes, JsonRpcConnection, LspError, StaleResultError, type RequestHandler } from './connection.ts';
2
+ export { applyCompletionDefaults } from './completion-defaults.ts';
2
3
  export { LspDocument } from './document.ts';
3
4
  export {
4
5
  applyContentChanges,
@@ -11,7 +12,19 @@ export {
11
12
  type DocumentSnapshot,
12
13
  type PlannedDocumentEdit
13
14
  } from './edits.ts';
14
- export { renamesTaken, type RenamedFile } from './file-operations.ts';
15
+ export {
16
+ planWorkspaceOperations,
17
+ type PlannedCreateStep,
18
+ type PlannedDeleteStep,
19
+ type PlannedRenameStep,
20
+ type PlannedTextStep,
21
+ type ResourceOperationKind,
22
+ type WorkspaceEditPlan,
23
+ type WorkspaceEditSource,
24
+ type WorkspaceEditStep,
25
+ type WorkspaceEditText
26
+ } from './workspace-operations.ts';
27
+ export { createsTaken, deletesTaken, renamesTaken, type CreatedFile, type DeletedFile, type RenamedFile } from './file-operations.ts';
15
28
  export { watchesFile, type FileChangeType } from './watched-files.ts';
16
29
  export { LspSession, type LspSessionOptions, type SessionState } from './session.ts';
17
30
  export type { DiagnosticsReport, LanguageDocument, LanguageRequestOptions, LanguageService } from './service.ts';
package/src/protocol.ts CHANGED
@@ -204,6 +204,30 @@ export interface WorkspaceSymbol {
204
204
  data?: unknown;
205
205
  }
206
206
 
207
+ /* A function, method or class that calls are traced through; `data` is the server's own and goes back to it untouched. */
208
+ export interface CallHierarchyItem {
209
+ name: string;
210
+ kind: SymbolKind;
211
+ tags?: 1[];
212
+ detail?: string;
213
+ uri: string;
214
+ range: Range;
215
+ selectionRange: Range;
216
+ data?: unknown;
217
+ }
218
+
219
+ /* A caller of an item. `fromRanges` are the call sites, in the file of `from`. */
220
+ export interface CallHierarchyIncomingCall {
221
+ from: CallHierarchyItem;
222
+ fromRanges: Range[];
223
+ }
224
+
225
+ /* An item that another one calls. `fromRanges` are the call sites, in the file of the item that was asked about. */
226
+ export interface CallHierarchyOutgoingCall {
227
+ to: CallHierarchyItem;
228
+ fromRanges: Range[];
229
+ }
230
+
207
231
  export type DocumentSymbolResult = DocumentSymbol[] | SymbolInformation[] | null;
208
232
  export type WorkspaceSymbolResult = SymbolInformation[] | WorkspaceSymbol[] | null;
209
233
 
@@ -273,6 +297,19 @@ export interface FileRename {
273
297
  newUri: string;
274
298
  }
275
299
 
300
+ /* One entry of `workspace/willCreateFiles` and `didCreateFiles`. */
301
+ export interface FileCreate {
302
+ uri: string;
303
+ }
304
+
305
+ /* One entry of `workspace/willDeleteFiles` and `didDeleteFiles`. */
306
+ export interface FileDelete {
307
+ uri: string;
308
+ }
309
+
310
+ /* The file operations a server can ask to hear about, each in a `will` form that may answer with an edit and a `did` form that only informs. */
311
+ export type FileOperation = 'willRename' | 'didRename' | 'willCreate' | 'didCreate' | 'willDelete' | 'didDelete';
312
+
276
313
  export interface FileOperationFilter {
277
314
  scheme?: string;
278
315
  pattern: { glob: string; matches?: 'file' | 'folder'; options?: { ignoreCase?: boolean } };
@@ -403,6 +440,9 @@ export interface ProviderOptions {
403
440
  resolveProvider?: boolean;
404
441
  prepareProvider?: boolean;
405
442
  triggerCharacters?: string[];
443
+ /* Of on type formatting: the first character that asks, and the others that do. */
444
+ firstTriggerCharacter?: string;
445
+ moreTriggerCharacter?: string[];
406
446
  retriggerCharacters?: string[];
407
447
  commands?: string[];
408
448
  watchers?: FileSystemWatcher[];
@@ -447,6 +487,8 @@ export interface ServerCapabilities {
447
487
  selectionRangeProvider?: boolean | ProviderOptions;
448
488
  semanticTokensProvider?: ProviderOptions & { legend: SemanticTokensLegend };
449
489
  inlayHintProvider?: boolean | ProviderOptions;
490
+ callHierarchyProvider?: boolean | ProviderOptions;
491
+ documentOnTypeFormattingProvider?: ProviderOptions;
450
492
  diagnosticProvider?: ProviderOptions;
451
493
  [capability: string]: unknown;
452
494
  }
package/src/service.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import type {
2
+ CallHierarchyIncomingCall,
3
+ CallHierarchyItem,
4
+ CallHierarchyOutgoingCall,
2
5
  CodeAction,
3
6
  CodeActionContext,
4
7
  CodeLens,
@@ -30,6 +33,7 @@ import type {
30
33
  WorkspaceEdit,
31
34
  WorkspaceSymbolResult
32
35
  } from './protocol.ts';
36
+ import type { CreatedFile, DeletedFile } from './file-operations.ts';
33
37
 
34
38
  export interface LanguageDocument {
35
39
  /* The `file:` URI the language servers know the file by, which `pathToFileUri` makes of a path. */
@@ -67,6 +71,8 @@ export interface LanguageService {
67
71
  /* Applies `didChange` entries in order. Every call raises the version by one. */
68
72
  changeDocument(uri: string, changes: readonly ContentChange[]): Promise<void>;
69
73
  closeDocument(uri: string): Promise<void>;
74
+ /* The version the servers hold an open document at, for checking a workspace edit; null when it is not open. Without it such versions cannot be checked. */
75
+ versionOf?(uri: string): number | null;
70
76
 
71
77
  /* Whether any server of the document answers `method` now, and the options it gave (trigger characters, a legend, resolve support). */
72
78
  supports(method: string, uri: string): boolean;
@@ -94,6 +100,8 @@ export interface LanguageService {
94
100
  executeCommand(uri: string, command: Command, options?: LanguageRequestOptions): Promise<unknown>;
95
101
  formatting(uri: string, formatting: FormattingOptions, options?: LanguageRequestOptions): Promise<TextEdit[] | null>;
96
102
  rangeFormatting(uri: string, range: Range, formatting: FormattingOptions, options?: LanguageRequestOptions): Promise<TextEdit[] | null>;
103
+ /* Optional so a service from before it still fits; `providerOptions('textDocument/onTypeFormatting', uri)` has the trigger characters. */
104
+ onTypeFormatting?(uri: string, position: Position, ch: string, formatting: FormattingOptions, options?: LanguageRequestOptions): Promise<TextEdit[] | null>;
97
105
  semanticTokens(uri: string, options?: LanguageRequestOptions): Promise<SemanticTokens | null>;
98
106
  semanticTokensDelta(uri: string, previousResultId: string, options?: LanguageRequestOptions): Promise<SemanticTokens | SemanticTokensDelta | null>;
99
107
  semanticTokensRange(uri: string, range: Range, options?: LanguageRequestOptions): Promise<SemanticTokens | null>;
@@ -104,6 +112,20 @@ export interface LanguageService {
104
112
  foldingRanges(uri: string, options?: LanguageRequestOptions): Promise<FoldingRange[] | null>;
105
113
  /* One chain per position, the smallest range first, each in its `parent`. */
106
114
  selectionRanges(uri: string, positions: readonly Position[], options?: LanguageRequestOptions): Promise<SelectionRange[] | null>;
115
+ /* The call hierarchy methods are optional so a service from before them still fits; `supports('textDocument/prepareCallHierarchy', uri)` says whether a server answers. */
116
+ prepareCallHierarchy?(uri: string, position: Position, options?: LanguageRequestOptions): Promise<CallHierarchyItem[] | null>;
117
+ /* `uri` is the document the hierarchy was prepared in, which names the server to ask. */
118
+ incomingCalls?(uri: string, item: CallHierarchyItem, options?: LanguageRequestOptions): Promise<CallHierarchyIncomingCall[] | null>;
119
+ outgoingCalls?(uri: string, item: CallHierarchyItem, options?: LanguageRequestOptions): Promise<CallHierarchyOutgoingCall[] | null>;
120
+ /*
121
+ * File operation notifications are optional so a service from before them still fits. Each asks the servers
122
+ * that registered for the operation and keeps the files their filters take; `will*` answers the edits the
123
+ * servers want made first (null when there are none) and `did*` informs them afterwards.
124
+ */
125
+ willCreateFiles?(files: readonly CreatedFile[], options?: LanguageRequestOptions): Promise<WorkspaceEdit | null>;
126
+ didCreateFiles?(files: readonly CreatedFile[]): Promise<void>;
127
+ willDeleteFiles?(files: readonly DeletedFile[], options?: LanguageRequestOptions): Promise<WorkspaceEdit | null>;
128
+ didDeleteFiles?(files: readonly DeletedFile[]): Promise<void>;
107
129
  codeLenses(uri: string, options?: LanguageRequestOptions): Promise<CodeLens[] | null>;
108
130
  resolveCodeLens(uri: string, lens: CodeLens, options?: LanguageRequestOptions): Promise<CodeLens>;
109
131
  }