@adecore/lsp 0.22.0 → 0.23.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/dist/completion-defaults.d.ts +4 -0
- package/dist/completion-defaults.js +83 -0
- package/dist/document.d.ts +5 -1
- package/dist/document.js +14 -0
- package/dist/file-operations.d.ts +9 -1
- package/dist/file-operations.js +12 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/protocol.d.ts +29 -0
- package/dist/service.d.ts +11 -1
- package/dist/session.d.ts +11 -3
- package/dist/session.js +47 -4
- package/dist/workspace-operations.d.ts +45 -0
- package/dist/workspace-operations.js +232 -0
- package/package.json +1 -1
- package/src/completion-defaults.ts +97 -0
- package/src/document.ts +21 -0
- package/src/file-operations.ts +25 -5
- package/src/index.ts +14 -1
- package/src/protocol.ts +42 -0
- package/src/service.ts +22 -0
- package/src/session.ts +62 -5
- package/src/workspace-operations.ts +341 -0
|
@@ -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
|
@@ -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
|
}
|
package/src/file-operations.ts
CHANGED
|
@@ -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
|
-
|
|
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') !==
|
|
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(
|
|
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 {
|
|
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
|
}
|