@adecore/lsp 0.38.0 → 0.40.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.js +2 -4
- package/dist/edits.d.ts +4 -0
- package/dist/edits.js +8 -5
- package/dist/file-operations.js +6 -3
- package/dist/session.d.ts +2 -0
- package/dist/session.js +14 -18
- package/dist/test-transport.js +1 -1
- package/dist/text-mirror.js +2 -5
- package/dist/transport.d.ts +1 -0
- package/dist/transport.js +7 -4
- package/dist/uris.d.ts +1 -0
- package/dist/uris.js +5 -2
- package/dist/watched-files.js +2 -2
- package/dist/workspace-operations.js +2 -3
- package/package.json +2 -2
- package/src/completion-defaults.ts +24 -4
- package/src/connection.ts +45 -0
- package/src/document.ts +39 -0
- package/src/edits.ts +58 -5
- package/src/file-operations.ts +13 -3
- package/src/glob.ts +6 -0
- package/src/session.ts +84 -18
- package/src/test-transport.ts +6 -1
- package/src/testing.ts +11 -0
- package/src/text-mirror.ts +27 -6
- package/src/transport.ts +58 -4
- package/src/uris.ts +17 -2
- package/src/vue.ts +15 -0
- package/src/watched-files.ts +14 -2
- package/src/workspace-operations.ts +51 -3
package/src/document.ts
CHANGED
|
@@ -68,12 +68,15 @@ export class LspDocument {
|
|
|
68
68
|
this.languageId = item.languageId;
|
|
69
69
|
this.content = new TextMirror(item.text);
|
|
70
70
|
this.revision = item.version ?? 1;
|
|
71
|
+
|
|
71
72
|
if (!Number.isSafeInteger(this.revision) || this.revision < 0) {
|
|
72
73
|
throw new LspError('Invalid document version');
|
|
73
74
|
}
|
|
75
|
+
|
|
74
76
|
this.ready = session.syncOptions.openClose
|
|
75
77
|
? session.notify('textDocument/didOpen', { textDocument: { ...item, version: this.revision } })
|
|
76
78
|
: Promise.resolve();
|
|
79
|
+
|
|
77
80
|
this.synced = this.ready;
|
|
78
81
|
void this.ready.catch((error) => session.connection.reportError(error));
|
|
79
82
|
}
|
|
@@ -94,9 +97,11 @@ export class LspDocument {
|
|
|
94
97
|
updateText(text: string): Promise<void> {
|
|
95
98
|
this.assertOpen();
|
|
96
99
|
const current = this.content.text;
|
|
100
|
+
|
|
97
101
|
if (text === current) {
|
|
98
102
|
return this.synced;
|
|
99
103
|
}
|
|
104
|
+
|
|
100
105
|
return this.applyChanges([minimalChange(current, text)]);
|
|
101
106
|
}
|
|
102
107
|
|
|
@@ -108,24 +113,32 @@ export class LspDocument {
|
|
|
108
113
|
applyChanges(changes: readonly ContentChange[]): Promise<void> {
|
|
109
114
|
this.assertOpen();
|
|
110
115
|
const kind = this.session.syncOptions.change ?? 0;
|
|
116
|
+
|
|
111
117
|
if (!kind) {
|
|
112
118
|
return Promise.reject(new LspError('Server did not negotiate document changes', ErrorCodes.MethodNotFound));
|
|
113
119
|
}
|
|
120
|
+
|
|
114
121
|
try {
|
|
115
122
|
this.content.applyChanges(changes);
|
|
116
123
|
} catch (error) {
|
|
117
124
|
return Promise.reject(error);
|
|
118
125
|
}
|
|
126
|
+
|
|
119
127
|
const version = ++this.revision;
|
|
120
128
|
this.cancelRequests();
|
|
129
|
+
|
|
121
130
|
if (kind === 1) {
|
|
122
131
|
return this.queueFullText();
|
|
123
132
|
}
|
|
133
|
+
|
|
124
134
|
const contentChanges = changes.map((change) => ({ ...change }));
|
|
135
|
+
|
|
125
136
|
this.synced = this.synced.then(() =>
|
|
126
137
|
this.session.connection.notify('textDocument/didChange', { textDocument: { uri: this.uri, version }, contentChanges })
|
|
127
138
|
);
|
|
139
|
+
|
|
128
140
|
void this.synced.catch((error) => this.session.connection.reportError(error));
|
|
141
|
+
|
|
129
142
|
return this.synced;
|
|
130
143
|
}
|
|
131
144
|
|
|
@@ -138,6 +151,7 @@ export class LspDocument {
|
|
|
138
151
|
await this.synced;
|
|
139
152
|
this.assertCurrent(version);
|
|
140
153
|
const save = this.session.syncOptions.save;
|
|
154
|
+
|
|
141
155
|
if (save) {
|
|
142
156
|
await this.session.notify('textDocument/didSave', {
|
|
143
157
|
textDocument: { uri: this.uri },
|
|
@@ -282,12 +296,15 @@ export class LspDocument {
|
|
|
282
296
|
if (this.closePromise) {
|
|
283
297
|
return this.closePromise;
|
|
284
298
|
}
|
|
299
|
+
|
|
285
300
|
if (this.closed) {
|
|
286
301
|
return Promise.resolve();
|
|
287
302
|
}
|
|
303
|
+
|
|
288
304
|
this.closed = true;
|
|
289
305
|
this.cancelRequests();
|
|
290
306
|
this.sendFullText?.();
|
|
307
|
+
|
|
291
308
|
this.closePromise = this.synced
|
|
292
309
|
.then(async () => {
|
|
293
310
|
if (!this.session.connection.isClosed && this.session.syncOptions.openClose) {
|
|
@@ -295,6 +312,7 @@ export class LspDocument {
|
|
|
295
312
|
}
|
|
296
313
|
})
|
|
297
314
|
.finally(() => this.session.forgetDocument(this));
|
|
315
|
+
|
|
298
316
|
return this.closePromise;
|
|
299
317
|
}
|
|
300
318
|
|
|
@@ -312,29 +330,39 @@ export class LspDocument {
|
|
|
312
330
|
|
|
313
331
|
private resolve<T extends object>(method: string, item: T, options?: DocumentRequestOptions): Promise<T> {
|
|
314
332
|
const version = this.resultVersions.get(item);
|
|
333
|
+
|
|
315
334
|
if (version !== undefined && version !== this.version) {
|
|
316
335
|
return Promise.reject(new StaleResultError(this.uri));
|
|
317
336
|
}
|
|
337
|
+
|
|
318
338
|
return this.perform(method, item, options);
|
|
319
339
|
}
|
|
320
340
|
|
|
321
341
|
private async perform<T>(method: string, params: unknown, options: DocumentRequestOptions = {}): Promise<T> {
|
|
322
342
|
this.assertOpen();
|
|
343
|
+
|
|
323
344
|
if (!this.session.supports(method, this)) {
|
|
324
345
|
throw new LspError(`Server does not support ${method}`, ErrorCodes.MethodNotFound);
|
|
325
346
|
}
|
|
347
|
+
|
|
326
348
|
const version = this.version;
|
|
327
349
|
const controller = new AbortController();
|
|
350
|
+
|
|
328
351
|
if (options.cancelPrevious !== false) {
|
|
329
352
|
this.latest.get(method)?.abort();
|
|
330
353
|
}
|
|
354
|
+
|
|
331
355
|
this.latest.set(method, controller);
|
|
332
356
|
this.inFlight.add(controller);
|
|
357
|
+
|
|
333
358
|
const abort = (): void => controller.abort();
|
|
359
|
+
|
|
334
360
|
options.signal?.addEventListener('abort', abort, { once: true });
|
|
361
|
+
|
|
335
362
|
if (options.signal?.aborted) {
|
|
336
363
|
abort();
|
|
337
364
|
}
|
|
365
|
+
|
|
338
366
|
try {
|
|
339
367
|
this.sendFullText?.();
|
|
340
368
|
await this.synced;
|
|
@@ -342,13 +370,16 @@ export class LspDocument {
|
|
|
342
370
|
const result = await this.session.request<T>(method, params, { signal: controller.signal, timeoutMs: options.timeoutMs });
|
|
343
371
|
this.assertCurrent(version);
|
|
344
372
|
this.remember(result, version);
|
|
373
|
+
|
|
345
374
|
return result;
|
|
346
375
|
} catch (error) {
|
|
347
376
|
this.assertCurrent(version);
|
|
377
|
+
|
|
348
378
|
throw error;
|
|
349
379
|
} finally {
|
|
350
380
|
options.signal?.removeEventListener('abort', abort);
|
|
351
381
|
this.inFlight.delete(controller);
|
|
382
|
+
|
|
352
383
|
if (this.latest.get(method) === controller) {
|
|
353
384
|
this.latest.delete(method);
|
|
354
385
|
}
|
|
@@ -360,7 +391,9 @@ export class LspDocument {
|
|
|
360
391
|
if (!result || typeof result !== 'object') {
|
|
361
392
|
return;
|
|
362
393
|
}
|
|
394
|
+
|
|
363
395
|
this.resultVersions.set(result, version);
|
|
396
|
+
|
|
364
397
|
if (Array.isArray(result)) {
|
|
365
398
|
for (const item of result) {
|
|
366
399
|
this.remember(item, version);
|
|
@@ -387,17 +420,21 @@ export class LspDocument {
|
|
|
387
420
|
if (this.sendFullText) {
|
|
388
421
|
return this.synced;
|
|
389
422
|
}
|
|
423
|
+
|
|
390
424
|
const due = new Promise<void>((resolve) => {
|
|
391
425
|
const timer = setTimeout(resolve, this.session.options.fullSyncDelayMs ?? FULL_SYNC_DELAY_MS);
|
|
426
|
+
|
|
392
427
|
this.sendFullText = () => {
|
|
393
428
|
clearTimeout(timer);
|
|
394
429
|
resolve();
|
|
395
430
|
};
|
|
396
431
|
});
|
|
432
|
+
|
|
397
433
|
this.synced = this.synced
|
|
398
434
|
.then(() => due)
|
|
399
435
|
.then(async () => {
|
|
400
436
|
this.sendFullText = undefined;
|
|
437
|
+
|
|
401
438
|
if (!this.session.connection.isClosed) {
|
|
402
439
|
await this.session.connection.notify('textDocument/didChange', {
|
|
403
440
|
textDocument: { uri: this.uri, version: this.revision },
|
|
@@ -405,7 +442,9 @@ export class LspDocument {
|
|
|
405
442
|
});
|
|
406
443
|
}
|
|
407
444
|
});
|
|
445
|
+
|
|
408
446
|
void this.synced.catch((error) => this.session.connection.reportError(error));
|
|
447
|
+
|
|
409
448
|
return this.synced;
|
|
410
449
|
}
|
|
411
450
|
|
package/src/edits.ts
CHANGED
|
@@ -16,10 +16,10 @@ export interface PlannedDocumentEdit {
|
|
|
16
16
|
created?: true;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const CR = 13;
|
|
20
|
-
const LF = 10;
|
|
19
|
+
export const CR = 13;
|
|
20
|
+
export const LF = 10;
|
|
21
21
|
|
|
22
|
-
function checkPosition(position: Position): void {
|
|
22
|
+
export function checkPosition(position: Position): void {
|
|
23
23
|
if (!Number.isInteger(position.line) || !Number.isInteger(position.character) || position.line < 0 || position.character < 0) {
|
|
24
24
|
throw new LspError('Invalid edit position');
|
|
25
25
|
}
|
|
@@ -30,27 +30,35 @@ export function offsetAt(text: string, position: Position): number {
|
|
|
30
30
|
checkPosition(position);
|
|
31
31
|
let line = 0;
|
|
32
32
|
let offset = 0;
|
|
33
|
+
|
|
33
34
|
while (line < position.line && offset < text.length) {
|
|
34
35
|
const char = text.charCodeAt(offset++);
|
|
36
|
+
|
|
35
37
|
if (char === CR) {
|
|
36
38
|
if (text.charCodeAt(offset) === LF) {
|
|
37
39
|
offset++;
|
|
38
40
|
}
|
|
41
|
+
|
|
39
42
|
line++;
|
|
40
43
|
} else if (char === LF) {
|
|
41
44
|
line++;
|
|
42
45
|
}
|
|
43
46
|
}
|
|
47
|
+
|
|
44
48
|
if (line !== position.line) {
|
|
45
49
|
throw new LspError('Edit line is outside the document');
|
|
46
50
|
}
|
|
51
|
+
|
|
47
52
|
let end = offset;
|
|
53
|
+
|
|
48
54
|
while (end < text.length && text[end] !== '\r' && text[end] !== '\n') {
|
|
49
55
|
end++;
|
|
50
56
|
}
|
|
57
|
+
|
|
51
58
|
if (offset + position.character > end) {
|
|
52
59
|
throw new LspError('Edit character is outside the line');
|
|
53
60
|
}
|
|
61
|
+
|
|
54
62
|
return offset + position.character;
|
|
55
63
|
}
|
|
56
64
|
|
|
@@ -58,15 +66,19 @@ export function positionAt(text: string, offset: number): Position {
|
|
|
58
66
|
if (!Number.isInteger(offset) || offset < 0 || offset > text.length) {
|
|
59
67
|
throw new LspError('Offset is outside the document');
|
|
60
68
|
}
|
|
69
|
+
|
|
61
70
|
let line = 0;
|
|
62
71
|
let lineStart = 0;
|
|
63
72
|
let i = 0;
|
|
73
|
+
|
|
64
74
|
while (i < offset) {
|
|
65
75
|
const code = text.charCodeAt(i);
|
|
76
|
+
|
|
66
77
|
if (code === CR && text.charCodeAt(i + 1) === LF) {
|
|
67
78
|
if (i + 2 > offset) {
|
|
68
79
|
break;
|
|
69
80
|
}
|
|
81
|
+
|
|
70
82
|
i += 2;
|
|
71
83
|
line++;
|
|
72
84
|
lineStart = i;
|
|
@@ -78,6 +90,7 @@ export function positionAt(text: string, offset: number): Position {
|
|
|
78
90
|
i++;
|
|
79
91
|
}
|
|
80
92
|
}
|
|
93
|
+
|
|
81
94
|
return { line, character: offset - lineStart };
|
|
82
95
|
}
|
|
83
96
|
|
|
@@ -89,8 +102,10 @@ export function endPosition(text: string): Position {
|
|
|
89
102
|
/* `offsetAt` for many positions in one text: the starts of its lines are found once instead of on every call. */
|
|
90
103
|
function offsetsIn(text: string): (position: Position) => number {
|
|
91
104
|
const starts = [0];
|
|
105
|
+
|
|
92
106
|
for (let i = 0; i < text.length; i++) {
|
|
93
107
|
const code = text.charCodeAt(i);
|
|
108
|
+
|
|
94
109
|
if (code === CR && text.charCodeAt(i + 1) === LF) {
|
|
95
110
|
i++;
|
|
96
111
|
starts.push(i + 1);
|
|
@@ -98,17 +113,22 @@ function offsetsIn(text: string): (position: Position) => number {
|
|
|
98
113
|
starts.push(i + 1);
|
|
99
114
|
}
|
|
100
115
|
}
|
|
116
|
+
|
|
101
117
|
return (position) => {
|
|
102
118
|
checkPosition(position);
|
|
103
119
|
const start = starts[position.line];
|
|
120
|
+
|
|
104
121
|
if (start === undefined) {
|
|
105
122
|
throw new LspError('Edit line is outside the document');
|
|
106
123
|
}
|
|
124
|
+
|
|
107
125
|
const next = starts[position.line + 1];
|
|
108
126
|
const end = next === undefined ? text.length : next - (text.charCodeAt(next - 1) === LF && text.charCodeAt(next - 2) === CR ? 2 : 1);
|
|
127
|
+
|
|
109
128
|
if (start + position.character > end) {
|
|
110
129
|
throw new LspError('Edit character is outside the line');
|
|
111
130
|
}
|
|
131
|
+
|
|
112
132
|
return start + position.character;
|
|
113
133
|
};
|
|
114
134
|
}
|
|
@@ -116,6 +136,7 @@ function offsetsIn(text: string): (position: Position) => number {
|
|
|
116
136
|
/* Edits are simultaneous and in the coordinates of `text`; two inserts at one place keep their order. */
|
|
117
137
|
export function applyTextEdits(text: string, edits: readonly TextEdit[]): string {
|
|
118
138
|
const offsetOf = offsetsIn(text);
|
|
139
|
+
|
|
119
140
|
const spans = edits
|
|
120
141
|
.map((edit, index) => ({
|
|
121
142
|
start: offsetOf(edit.range.start),
|
|
@@ -124,37 +145,49 @@ export function applyTextEdits(text: string, edits: readonly TextEdit[]): string
|
|
|
124
145
|
index
|
|
125
146
|
}))
|
|
126
147
|
.sort((a, b) => a.start - b.start || a.index - b.index);
|
|
148
|
+
|
|
127
149
|
let cursor = 0;
|
|
128
150
|
const parts: string[] = [];
|
|
151
|
+
|
|
129
152
|
for (const span of spans) {
|
|
130
153
|
if (span.end < span.start) {
|
|
131
154
|
throw new LspError('Edit range is reversed');
|
|
132
155
|
}
|
|
156
|
+
|
|
133
157
|
if (span.start < cursor) {
|
|
134
158
|
throw new LspError('Workspace edits overlap');
|
|
135
159
|
}
|
|
160
|
+
|
|
136
161
|
parts.push(text.slice(cursor, span.start), span.text);
|
|
137
162
|
cursor = span.end;
|
|
138
163
|
}
|
|
164
|
+
|
|
139
165
|
parts.push(text.slice(cursor));
|
|
166
|
+
|
|
140
167
|
return parts.join('');
|
|
141
168
|
}
|
|
142
169
|
|
|
143
170
|
/* `didChange` entries apply one after the other, each in the coordinates the one before left. */
|
|
144
171
|
export function applyContentChanges(text: string, changes: readonly ContentChange[]): string {
|
|
145
172
|
let result = text;
|
|
173
|
+
|
|
146
174
|
for (const change of changes) {
|
|
147
175
|
if (!change.range) {
|
|
148
176
|
result = change.text;
|
|
177
|
+
|
|
149
178
|
continue;
|
|
150
179
|
}
|
|
180
|
+
|
|
151
181
|
const start = offsetAt(result, change.range.start);
|
|
152
182
|
const end = offsetAt(result, change.range.end);
|
|
183
|
+
|
|
153
184
|
if (end < start) {
|
|
154
185
|
throw new LspError('Change range is reversed');
|
|
155
186
|
}
|
|
187
|
+
|
|
156
188
|
result = result.slice(0, start) + change.text + result.slice(end);
|
|
157
189
|
}
|
|
190
|
+
|
|
158
191
|
return result;
|
|
159
192
|
}
|
|
160
193
|
|
|
@@ -174,22 +207,28 @@ function isLowSurrogate(code: number): boolean {
|
|
|
174
207
|
export function minimalChange(before: string, after: string): ContentChange {
|
|
175
208
|
const shortest = Math.min(before.length, after.length);
|
|
176
209
|
let start = 0;
|
|
210
|
+
|
|
177
211
|
while (start < shortest && before.charCodeAt(start) === after.charCodeAt(start)) {
|
|
178
212
|
start++;
|
|
179
213
|
}
|
|
214
|
+
|
|
180
215
|
if (start > 0 && (isHighSurrogate(before.charCodeAt(start - 1)) || (before[start - 1] === '\r' && before[start] === '\n'))) {
|
|
181
216
|
start--;
|
|
182
217
|
}
|
|
218
|
+
|
|
183
219
|
let end = 0;
|
|
220
|
+
|
|
184
221
|
while (end < shortest - start && before.charCodeAt(before.length - 1 - end) === after.charCodeAt(after.length - 1 - end)) {
|
|
185
222
|
end++;
|
|
186
223
|
}
|
|
224
|
+
|
|
187
225
|
if (
|
|
188
226
|
end > 0 &&
|
|
189
227
|
(isLowSurrogate(before.charCodeAt(before.length - end)) || (before[before.length - end] === '\n' && before[before.length - end - 1] === '\r'))
|
|
190
228
|
) {
|
|
191
229
|
end--;
|
|
192
230
|
}
|
|
231
|
+
|
|
193
232
|
return {
|
|
194
233
|
range: { start: positionAt(before, start), end: positionAt(before, before.length - end) },
|
|
195
234
|
text: after.slice(start, after.length - end)
|
|
@@ -201,9 +240,11 @@ type KeyedSnapshots = ReadonlyMap<string, { uri: string; snapshot: DocumentSnaps
|
|
|
201
240
|
|
|
202
241
|
function snapshotEdit(uri: string, snapshots: KeyedSnapshots): PlannedDocumentEdit {
|
|
203
242
|
const found = snapshots.get(fileUriKey(uri));
|
|
243
|
+
|
|
204
244
|
if (!found) {
|
|
205
245
|
throw new LspError(`Missing workspace snapshot: ${uri}`);
|
|
206
246
|
}
|
|
247
|
+
|
|
207
248
|
return { uri: found.uri, version: found.snapshot.version, before: found.snapshot.text, text: found.snapshot.text };
|
|
208
249
|
}
|
|
209
250
|
|
|
@@ -211,6 +252,7 @@ function snapshotEdit(uri: string, snapshots: KeyedSnapshots): PlannedDocumentEd
|
|
|
211
252
|
function planCreate(change: CreateFile, snapshots: KeyedSnapshots, planned: Map<string, PlannedDocumentEdit>): void {
|
|
212
253
|
const key = fileUriKey(change.uri);
|
|
213
254
|
const existing = planned.get(key) ?? (snapshots.has(key) ? snapshotEdit(change.uri, snapshots) : null);
|
|
255
|
+
|
|
214
256
|
if (existing === null) {
|
|
215
257
|
planned.set(key, { uri: change.uri, version: null, before: '', text: '', created: true });
|
|
216
258
|
} else if (change.options?.overwrite) {
|
|
@@ -220,6 +262,11 @@ function planCreate(change: CreateFile, snapshots: KeyedSnapshots, planned: Map<
|
|
|
220
262
|
}
|
|
221
263
|
}
|
|
222
264
|
|
|
265
|
+
/* The changes of an edit in order, with the older `changes` map read as unversioned text edits. */
|
|
266
|
+
export function documentChangesOf(edit: WorkspaceEdit): NonNullable<WorkspaceEdit['documentChanges']> {
|
|
267
|
+
return edit.documentChanges ?? Object.entries(edit.changes ?? {}).map(([uri, edits]) => ({ textDocument: { uri, version: null }, edits }));
|
|
268
|
+
}
|
|
269
|
+
|
|
223
270
|
/*
|
|
224
271
|
* Plans text edits and created files in memory, in the order the edit gives them. `snapshots` holds the
|
|
225
272
|
* files that are there; a file the edit creates is absent from it. A rename or delete needs the host's own handling and is refused.
|
|
@@ -227,22 +274,28 @@ function planCreate(change: CreateFile, snapshots: KeyedSnapshots, planned: Map<
|
|
|
227
274
|
export function planWorkspaceEdit(edit: WorkspaceEdit, snapshots: ReadonlyMap<string, DocumentSnapshot>): PlannedDocumentEdit[] {
|
|
228
275
|
const keyed: KeyedSnapshots = new Map([...snapshots].map(([uri, snapshot]) => [fileUriKey(uri), { uri, snapshot }]));
|
|
229
276
|
const planned = new Map<string, PlannedDocumentEdit>();
|
|
230
|
-
|
|
231
|
-
for (const change of
|
|
277
|
+
|
|
278
|
+
for (const change of documentChangesOf(edit)) {
|
|
232
279
|
if ('kind' in change) {
|
|
233
280
|
if (change.kind !== 'create') {
|
|
234
281
|
throw new LspError(`A host file-operation handler is required for ${change.kind}`);
|
|
235
282
|
}
|
|
283
|
+
|
|
236
284
|
planCreate(change, keyed, planned);
|
|
285
|
+
|
|
237
286
|
continue;
|
|
238
287
|
}
|
|
288
|
+
|
|
239
289
|
const { uri, version } = change.textDocument;
|
|
240
290
|
const key = fileUriKey(uri);
|
|
241
291
|
const current = planned.get(key) ?? snapshotEdit(uri, keyed);
|
|
292
|
+
|
|
242
293
|
if (version !== null && current.version !== version) {
|
|
243
294
|
throw new LspError(`Workspace edit version mismatch: ${uri}`, ErrorCodes.ContentModified);
|
|
244
295
|
}
|
|
296
|
+
|
|
245
297
|
planned.set(key, { ...current, text: applyTextEdits(current.text, change.edits) });
|
|
246
298
|
}
|
|
299
|
+
|
|
247
300
|
return [...planned.values()];
|
|
248
301
|
}
|
package/src/file-operations.ts
CHANGED
|
@@ -18,33 +18,43 @@ export interface DeletedFile extends FileDelete {
|
|
|
18
18
|
|
|
19
19
|
function filterTakes(filter: FileOperationFilter, uri: string, directory: boolean): boolean {
|
|
20
20
|
const { matches, glob, options } = filter.pattern;
|
|
21
|
+
|
|
21
22
|
if (matches !== undefined && (matches === 'folder') !== directory) {
|
|
22
23
|
return false;
|
|
23
24
|
}
|
|
25
|
+
|
|
24
26
|
let url: URL;
|
|
27
|
+
|
|
25
28
|
try {
|
|
26
29
|
url = new URL(uri);
|
|
27
30
|
} catch {
|
|
28
31
|
return false;
|
|
29
32
|
}
|
|
33
|
+
|
|
30
34
|
if (filter.scheme !== undefined && filter.scheme !== url.protocol.slice(0, -1)) {
|
|
31
35
|
return false;
|
|
32
36
|
}
|
|
37
|
+
|
|
33
38
|
const path = decodeURIComponent(url.pathname);
|
|
39
|
+
|
|
34
40
|
return options?.ignoreCase === true ? globMatch(path.toLowerCase(), glob.toLowerCase()) : globMatch(path, glob);
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
function anyTakes(filters: readonly FileOperationFilter[], uri: string, directory: boolean): boolean {
|
|
44
|
+
return filters.some((filter) => filterTakes(filter, uri, directory));
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
/* The renames a server asked to hear about, by the filters it registered. A rename is matched on the place it leaves. */
|
|
38
48
|
export function renamesTaken(filters: readonly FileOperationFilter[], files: readonly RenamedFile[]): FileRename[] {
|
|
39
|
-
return files.filter((file) => filters
|
|
49
|
+
return files.filter((file) => anyTakes(filters, file.oldUri, file.directory)).map(({ oldUri, newUri }) => ({ oldUri, newUri }));
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
/* The files made that a server asked to hear about, by the filters it registered. */
|
|
43
53
|
export function createsTaken(filters: readonly FileOperationFilter[], files: readonly CreatedFile[]): FileCreate[] {
|
|
44
|
-
return files.filter((file) => filters
|
|
54
|
+
return files.filter((file) => anyTakes(filters, file.uri, file.directory)).map(({ uri }) => ({ uri }));
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
/* 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
58
|
export function deletesTaken(filters: readonly FileOperationFilter[], files: readonly DeletedFile[]): FileDelete[] {
|
|
49
|
-
return files.filter((file) => filters
|
|
59
|
+
return files.filter((file) => anyTakes(filters, file.uri, file.directory)).map(({ uri }) => ({ uri }));
|
|
50
60
|
}
|
package/src/glob.ts
CHANGED
|
@@ -7,11 +7,14 @@ function escapeRegex(char: string): string {
|
|
|
7
7
|
function compileGlob(pattern: string): RegExp | null {
|
|
8
8
|
let expression = '';
|
|
9
9
|
let depth = 0;
|
|
10
|
+
|
|
10
11
|
for (let i = 0; i < pattern.length; i++) {
|
|
11
12
|
const char = pattern[i];
|
|
13
|
+
|
|
12
14
|
if (char === '*') {
|
|
13
15
|
if (pattern[i + 1] === '*') {
|
|
14
16
|
i++;
|
|
17
|
+
|
|
15
18
|
if (pattern[i + 1] === '/') {
|
|
16
19
|
expression += '(?:.*/)?';
|
|
17
20
|
i++;
|
|
@@ -25,6 +28,7 @@ function compileGlob(pattern: string): RegExp | null {
|
|
|
25
28
|
expression += '[^/]';
|
|
26
29
|
} else if (char === '[') {
|
|
27
30
|
const close = pattern.indexOf(']', i + 2);
|
|
31
|
+
|
|
28
32
|
if (close === -1) {
|
|
29
33
|
expression += '\\[';
|
|
30
34
|
} else {
|
|
@@ -44,6 +48,7 @@ function compileGlob(pattern: string): RegExp | null {
|
|
|
44
48
|
expression += escapeRegex(char);
|
|
45
49
|
}
|
|
46
50
|
}
|
|
51
|
+
|
|
47
52
|
try {
|
|
48
53
|
return new RegExp(`^${expression}$`);
|
|
49
54
|
} catch {
|
|
@@ -56,5 +61,6 @@ export function globMatch(path: string, pattern: string): boolean {
|
|
|
56
61
|
if (!compiled.has(pattern)) {
|
|
57
62
|
compiled.set(pattern, compileGlob(pattern));
|
|
58
63
|
}
|
|
64
|
+
|
|
59
65
|
return compiled.get(pattern)?.test(path) ?? false;
|
|
60
66
|
}
|