@adecore/lsp 0.29.1 → 0.29.3

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/edits.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ErrorCodes, LspError } from './connection.js';
2
+ import { fileUriKey } from './uris.js';
2
3
  const CR = 13;
3
4
  const LF = 10;
4
5
  function checkPosition(position) {
@@ -171,20 +172,21 @@ export function minimalChange(before, after) {
171
172
  };
172
173
  }
173
174
  function snapshotEdit(uri, snapshots) {
174
- const snapshot = snapshots.get(uri);
175
- if (!snapshot) {
175
+ const found = snapshots.get(fileUriKey(uri));
176
+ if (!found) {
176
177
  throw new LspError(`Missing workspace snapshot: ${uri}`);
177
178
  }
178
- return { uri, version: snapshot.version, before: snapshot.text, text: snapshot.text };
179
+ return { uri: found.uri, version: found.snapshot.version, before: found.snapshot.text, text: found.snapshot.text };
179
180
  }
180
181
  /* A file that is there already is refused, unless the create says to empty it or to leave it as it is. */
181
182
  function planCreate(change, snapshots, planned) {
182
- const existing = planned.get(change.uri) ?? (snapshots.has(change.uri) ? snapshotEdit(change.uri, snapshots) : null);
183
+ const key = fileUriKey(change.uri);
184
+ const existing = planned.get(key) ?? (snapshots.has(key) ? snapshotEdit(change.uri, snapshots) : null);
183
185
  if (existing === null) {
184
- planned.set(change.uri, { uri: change.uri, version: null, before: '', text: '', created: true });
186
+ planned.set(key, { uri: change.uri, version: null, before: '', text: '', created: true });
185
187
  }
186
188
  else if (change.options?.overwrite) {
187
- planned.set(change.uri, { ...existing, text: '' });
189
+ planned.set(key, { ...existing, text: '' });
188
190
  }
189
191
  else if (!change.options?.ignoreIfExists) {
190
192
  throw new LspError(`${change.uri} already exists`);
@@ -195,6 +197,7 @@ function planCreate(change, snapshots, planned) {
195
197
  * 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.
196
198
  */
197
199
  export function planWorkspaceEdit(edit, snapshots) {
200
+ const keyed = new Map([...snapshots].map(([uri, snapshot]) => [fileUriKey(uri), { uri, snapshot }]));
198
201
  const planned = new Map();
199
202
  const changes = edit.documentChanges ?? Object.entries(edit.changes ?? {}).map(([uri, edits]) => ({ textDocument: { uri, version: null }, edits }));
200
203
  for (const change of changes) {
@@ -202,15 +205,16 @@ export function planWorkspaceEdit(edit, snapshots) {
202
205
  if (change.kind !== 'create') {
203
206
  throw new LspError(`A host file-operation handler is required for ${change.kind}`);
204
207
  }
205
- planCreate(change, snapshots, planned);
208
+ planCreate(change, keyed, planned);
206
209
  continue;
207
210
  }
208
211
  const { uri, version } = change.textDocument;
209
- const current = planned.get(uri) ?? snapshotEdit(uri, snapshots);
212
+ const key = fileUriKey(uri);
213
+ const current = planned.get(key) ?? snapshotEdit(uri, keyed);
210
214
  if (version !== null && current.version !== version) {
211
215
  throw new LspError(`Workspace edit version mismatch: ${uri}`, ErrorCodes.ContentModified);
212
216
  }
213
- planned.set(uri, { ...current, text: applyTextEdits(current.text, change.edits) });
217
+ planned.set(key, { ...current, text: applyTextEdits(current.text, change.edits) });
214
218
  }
215
219
  return [...planned.values()];
216
220
  }
package/dist/session.js CHANGED
@@ -2,6 +2,7 @@ import { ErrorCodes, JsonRpcConnection, LspError } from './connection.js';
2
2
  import { LspDocument } from './document.js';
3
3
  import { createsTaken, deletesTaken, renamesTaken } from './file-operations.js';
4
4
  import { globMatch } from './glob.js';
5
+ import { fileUriKey } from './uris.js';
5
6
  /* The capability that advertises each method statically. */
6
7
  const PROVIDERS = {
7
8
  'textDocument/completion': 'completionProvider',
@@ -111,11 +112,11 @@ export class LspSession {
111
112
  });
112
113
  this.connection.onNotification('textDocument/publishDiagnostics', (params) => {
113
114
  const diagnostics = params;
114
- const document = this.documents.get(diagnostics.uri);
115
+ const document = this.documents.get(fileUriKey(diagnostics.uri));
115
116
  if (!document || document.isClosed || (diagnostics.version !== undefined && diagnostics.version !== document.version)) {
116
117
  return;
117
118
  }
118
- this.emitDiagnostics(diagnostics);
119
+ this.emitDiagnostics({ ...diagnostics, uri: document.uri });
119
120
  });
120
121
  this.connection.onRequest('workspace/configuration', async (params) => {
121
122
  const { items } = params;
@@ -202,15 +203,15 @@ export class LspSession {
202
203
  }
203
204
  openDocument(item) {
204
205
  this.assertReady();
205
- if (this.documents.has(item.uri)) {
206
+ if (this.documents.has(fileUriKey(item.uri))) {
206
207
  throw new LspError(`Document already open: ${item.uri}`);
207
208
  }
208
209
  const document = new LspDocument(this, item);
209
- this.documents.set(item.uri, document);
210
+ this.documents.set(fileUriKey(item.uri), document);
210
211
  return document;
211
212
  }
212
213
  getDocument(uri) {
213
- return this.documents.get(uri);
214
+ return this.documents.get(fileUriKey(uri));
214
215
  }
215
216
  /* The options the server gave a method, from its capabilities or a registration whose selector takes `document`. */
216
217
  providerOptions(method, document) {
@@ -355,10 +356,10 @@ export class LspSession {
355
356
  }
356
357
  /* Called by a document that closed, so its diagnostics go with it. */
357
358
  forgetDocument(document) {
358
- if (this.documents.get(document.uri) !== document) {
359
+ if (this.documents.get(fileUriKey(document.uri)) !== document) {
359
360
  return;
360
361
  }
361
- this.documents.delete(document.uri);
362
+ this.documents.delete(fileUriKey(document.uri));
362
363
  this.emitDiagnostics({ uri: document.uri, diagnostics: [] });
363
364
  }
364
365
  assertReady() {
package/dist/uris.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export declare function pathToFileUri(path: string): string;
2
2
  export declare function fileUriToPath(uri: string): string | null;
3
+ export declare function fileUriKey(uri: string): string;
package/dist/uris.js CHANGED
@@ -37,3 +37,14 @@ export function fileUriToPath(uri) {
37
37
  }
38
38
  return path;
39
39
  }
40
+ /*
41
+ * One key for every way a URI can name the same Windows file: servers write the drive in either
42
+ * case, and some escape its colon (`file:///c%3A/...`). Any other URI is its own key.
43
+ */
44
+ export function fileUriKey(uri) {
45
+ const path = fileUriToPath(uri);
46
+ if (path === null || !/^[A-Za-z]:\\/.test(path)) {
47
+ return uri;
48
+ }
49
+ return pathToFileUri(`${path[0].toUpperCase()}${path.slice(1)}`);
50
+ }
@@ -1,3 +1,3 @@
1
1
  import type { FileSystemWatcher } from './protocol.ts';
2
2
  export type FileChangeType = 1 | 2 | 3;
3
- export declare function watchesFile(watchers: readonly FileSystemWatcher[], root: string, path: string, type: FileChangeType): boolean;
3
+ export declare function watchesFile(watchers: readonly FileSystemWatcher[], root: string, changed: string, type: FileChangeType): boolean;
@@ -3,6 +3,14 @@ import { fileUriToPath } from './uris.js';
3
3
  const EVERY_CHANGE = 7;
4
4
  /* Folders whose files a server hears about only when a pattern of its own names the folder. */
5
5
  const SPARING_FOLDERS = ['node_modules', 'vendor', '.git'];
6
+ /* A Windows path with forward slashes and its drive in capitals, so it compares with a pattern and a root however it was written. A POSIX path is left alone, a backslash being part of a name there. */
7
+ function slashed(path) {
8
+ if (!/^[A-Za-z]:[\\/]/.test(path) && !path.startsWith('\\\\')) {
9
+ return path;
10
+ }
11
+ const forward = path.replaceAll('\\', '/');
12
+ return /^[a-z]:/.test(forward) ? `${forward[0].toUpperCase()}${forward.slice(1)}` : forward;
13
+ }
6
14
  function relativeTo(root, path) {
7
15
  const prefix = root.endsWith('/') ? root : `${root}/`;
8
16
  return path.startsWith(prefix) ? path.slice(prefix.length) : null;
@@ -14,15 +22,16 @@ function patternOf(watcher) {
14
22
  return { base: undefined, pattern: globPattern };
15
23
  }
16
24
  const base = fileUriToPath(typeof globPattern.baseUri === 'string' ? globPattern.baseUri : globPattern.baseUri.uri);
17
- return base === null ? null : { base, pattern: globPattern.pattern };
25
+ return base === null ? null : { base: slashed(base), pattern: globPattern.pattern };
18
26
  }
19
27
  /*
20
28
  * Whether a server asked to hear about this change. A string pattern is relative to the project folder
21
29
  * (or absolute), a relative pattern to its own base. A path inside `node_modules`, `vendor` or `.git` only
22
30
  * matches a pattern that names that folder, since `**` over a dependency tree is what a server never means.
23
31
  */
24
- export function watchesFile(watchers, root, path, type) {
25
- const inProject = relativeTo(root, path);
32
+ export function watchesFile(watchers, root, changed, type) {
33
+ const path = slashed(changed);
34
+ const inProject = relativeTo(slashed(root), path);
26
35
  const segments = (inProject ?? path).split('/');
27
36
  return watchers.some((watcher) => {
28
37
  if (((watcher.kind ?? EVERY_CHANGE) & (1 << (type - 1))) === 0) {
@@ -33,7 +42,7 @@ export function watchesFile(watchers, root, path, type) {
33
42
  return false;
34
43
  }
35
44
  const { base, pattern } = parts;
36
- const subject = pattern.startsWith('/') ? path : base === undefined ? inProject : relativeTo(base, path);
45
+ const subject = pattern.startsWith('/') || /^[A-Za-z]:\//.test(pattern) ? path : base === undefined ? inProject : relativeTo(base, path);
37
46
  if (subject === null || !globMatch(subject, pattern)) {
38
47
  return false;
39
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/lsp",
3
- "version": "0.29.1",
3
+ "version": "0.29.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/edits.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ErrorCodes, LspError } from './connection.ts';
2
2
  import type { ContentChange, CreateFile, Position, TextEdit, WorkspaceEdit } from './protocol.ts';
3
+ import { fileUriKey } from './uris.ts';
3
4
 
4
5
  export interface DocumentSnapshot {
5
6
  text: string;
@@ -195,21 +196,25 @@ export function minimalChange(before: string, after: string): ContentChange {
195
196
  };
196
197
  }
197
198
 
198
- function snapshotEdit(uri: string, snapshots: ReadonlyMap<string, DocumentSnapshot>): PlannedDocumentEdit {
199
- const snapshot = snapshots.get(uri);
200
- if (!snapshot) {
199
+ // The snapshots by `fileUriKey`, each with the URI it was given under, which the plan keeps.
200
+ type KeyedSnapshots = ReadonlyMap<string, { uri: string; snapshot: DocumentSnapshot }>;
201
+
202
+ function snapshotEdit(uri: string, snapshots: KeyedSnapshots): PlannedDocumentEdit {
203
+ const found = snapshots.get(fileUriKey(uri));
204
+ if (!found) {
201
205
  throw new LspError(`Missing workspace snapshot: ${uri}`);
202
206
  }
203
- return { uri, version: snapshot.version, before: snapshot.text, text: snapshot.text };
207
+ return { uri: found.uri, version: found.snapshot.version, before: found.snapshot.text, text: found.snapshot.text };
204
208
  }
205
209
 
206
210
  /* A file that is there already is refused, unless the create says to empty it or to leave it as it is. */
207
- function planCreate(change: CreateFile, snapshots: ReadonlyMap<string, DocumentSnapshot>, planned: Map<string, PlannedDocumentEdit>): void {
208
- const existing = planned.get(change.uri) ?? (snapshots.has(change.uri) ? snapshotEdit(change.uri, snapshots) : null);
211
+ function planCreate(change: CreateFile, snapshots: KeyedSnapshots, planned: Map<string, PlannedDocumentEdit>): void {
212
+ const key = fileUriKey(change.uri);
213
+ const existing = planned.get(key) ?? (snapshots.has(key) ? snapshotEdit(change.uri, snapshots) : null);
209
214
  if (existing === null) {
210
- planned.set(change.uri, { uri: change.uri, version: null, before: '', text: '', created: true });
215
+ planned.set(key, { uri: change.uri, version: null, before: '', text: '', created: true });
211
216
  } else if (change.options?.overwrite) {
212
- planned.set(change.uri, { ...existing, text: '' });
217
+ planned.set(key, { ...existing, text: '' });
213
218
  } else if (!change.options?.ignoreIfExists) {
214
219
  throw new LspError(`${change.uri} already exists`);
215
220
  }
@@ -220,6 +225,7 @@ function planCreate(change: CreateFile, snapshots: ReadonlyMap<string, DocumentS
220
225
  * 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.
221
226
  */
222
227
  export function planWorkspaceEdit(edit: WorkspaceEdit, snapshots: ReadonlyMap<string, DocumentSnapshot>): PlannedDocumentEdit[] {
228
+ const keyed: KeyedSnapshots = new Map([...snapshots].map(([uri, snapshot]) => [fileUriKey(uri), { uri, snapshot }]));
223
229
  const planned = new Map<string, PlannedDocumentEdit>();
224
230
  const changes = edit.documentChanges ?? Object.entries(edit.changes ?? {}).map(([uri, edits]) => ({ textDocument: { uri, version: null }, edits }));
225
231
  for (const change of changes) {
@@ -227,15 +233,16 @@ export function planWorkspaceEdit(edit: WorkspaceEdit, snapshots: ReadonlyMap<st
227
233
  if (change.kind !== 'create') {
228
234
  throw new LspError(`A host file-operation handler is required for ${change.kind}`);
229
235
  }
230
- planCreate(change, snapshots, planned);
236
+ planCreate(change, keyed, planned);
231
237
  continue;
232
238
  }
233
239
  const { uri, version } = change.textDocument;
234
- const current = planned.get(uri) ?? snapshotEdit(uri, snapshots);
240
+ const key = fileUriKey(uri);
241
+ const current = planned.get(key) ?? snapshotEdit(uri, keyed);
235
242
  if (version !== null && current.version !== version) {
236
243
  throw new LspError(`Workspace edit version mismatch: ${uri}`, ErrorCodes.ContentModified);
237
244
  }
238
- planned.set(uri, { ...current, text: applyTextEdits(current.text, change.edits) });
245
+ planned.set(key, { ...current, text: applyTextEdits(current.text, change.edits) });
239
246
  }
240
247
  return [...planned.values()];
241
248
  }
package/src/session.ts CHANGED
@@ -2,6 +2,7 @@ import { ErrorCodes, JsonRpcConnection, LspError, type RequestHandler } from './
2
2
  import { LspDocument } from './document.ts';
3
3
  import { createsTaken, deletesTaken, renamesTaken, type CreatedFile, type DeletedFile, type RenamedFile } from './file-operations.ts';
4
4
  import { globMatch } from './glob.ts';
5
+ import { fileUriKey } from './uris.ts';
5
6
  import type { ResourceOperationKind } from './workspace-operations.ts';
6
7
  import type {
7
8
  ApplyWorkspaceEditParams,
@@ -173,11 +174,11 @@ export class LspSession {
173
174
  });
174
175
  this.connection.onNotification('textDocument/publishDiagnostics', (params) => {
175
176
  const diagnostics = params as PublishDiagnosticsParams;
176
- const document = this.documents.get(diagnostics.uri);
177
+ const document = this.documents.get(fileUriKey(diagnostics.uri));
177
178
  if (!document || document.isClosed || (diagnostics.version !== undefined && diagnostics.version !== document.version)) {
178
179
  return;
179
180
  }
180
- this.emitDiagnostics(diagnostics);
181
+ this.emitDiagnostics({ ...diagnostics, uri: document.uri });
181
182
  });
182
183
  this.connection.onRequest('workspace/configuration', async (params) => {
183
184
  const { items } = params as { items: ConfigurationItem[] };
@@ -275,16 +276,16 @@ export class LspSession {
275
276
 
276
277
  openDocument(item: DocumentItem): LspDocument {
277
278
  this.assertReady();
278
- if (this.documents.has(item.uri)) {
279
+ if (this.documents.has(fileUriKey(item.uri))) {
279
280
  throw new LspError(`Document already open: ${item.uri}`);
280
281
  }
281
282
  const document = new LspDocument(this, item);
282
- this.documents.set(item.uri, document);
283
+ this.documents.set(fileUriKey(item.uri), document);
283
284
  return document;
284
285
  }
285
286
 
286
287
  getDocument(uri: string): LspDocument | undefined {
287
- return this.documents.get(uri);
288
+ return this.documents.get(fileUriKey(uri));
288
289
  }
289
290
 
290
291
  /* The options the server gave a method, from its capabilities or a registration whose selector takes `document`. */
@@ -453,10 +454,10 @@ export class LspSession {
453
454
 
454
455
  /* Called by a document that closed, so its diagnostics go with it. */
455
456
  forgetDocument(document: LspDocument): void {
456
- if (this.documents.get(document.uri) !== document) {
457
+ if (this.documents.get(fileUriKey(document.uri)) !== document) {
457
458
  return;
458
459
  }
459
- this.documents.delete(document.uri);
460
+ this.documents.delete(fileUriKey(document.uri));
460
461
  this.emitDiagnostics({ uri: document.uri, diagnostics: [] });
461
462
  }
462
463
 
package/src/uris.ts CHANGED
@@ -38,3 +38,15 @@ export function fileUriToPath(uri: string): string | null {
38
38
  }
39
39
  return path;
40
40
  }
41
+
42
+ /*
43
+ * One key for every way a URI can name the same Windows file: servers write the drive in either
44
+ * case, and some escape its colon (`file:///c%3A/...`). Any other URI is its own key.
45
+ */
46
+ export function fileUriKey(uri: string): string {
47
+ const path = fileUriToPath(uri);
48
+ if (path === null || !/^[A-Za-z]:\\/.test(path)) {
49
+ return uri;
50
+ }
51
+ return pathToFileUri(`${path[0]!.toUpperCase()}${path.slice(1)}`);
52
+ }
@@ -10,6 +10,15 @@ const EVERY_CHANGE = 7;
10
10
  /* Folders whose files a server hears about only when a pattern of its own names the folder. */
11
11
  const SPARING_FOLDERS = ['node_modules', 'vendor', '.git'];
12
12
 
13
+ /* A Windows path with forward slashes and its drive in capitals, so it compares with a pattern and a root however it was written. A POSIX path is left alone, a backslash being part of a name there. */
14
+ function slashed(path: string): string {
15
+ if (!/^[A-Za-z]:[\\/]/.test(path) && !path.startsWith('\\\\')) {
16
+ return path;
17
+ }
18
+ const forward = path.replaceAll('\\', '/');
19
+ return /^[a-z]:/.test(forward) ? `${forward[0]!.toUpperCase()}${forward.slice(1)}` : forward;
20
+ }
21
+
13
22
  function relativeTo(root: string, path: string): string | null {
14
23
  const prefix = root.endsWith('/') ? root : `${root}/`;
15
24
  return path.startsWith(prefix) ? path.slice(prefix.length) : null;
@@ -22,7 +31,7 @@ function patternOf(watcher: FileSystemWatcher): { base: string | undefined; patt
22
31
  return { base: undefined, pattern: globPattern };
23
32
  }
24
33
  const base = fileUriToPath(typeof globPattern.baseUri === 'string' ? globPattern.baseUri : globPattern.baseUri.uri);
25
- return base === null ? null : { base, pattern: globPattern.pattern };
34
+ return base === null ? null : { base: slashed(base), pattern: globPattern.pattern };
26
35
  }
27
36
 
28
37
  /*
@@ -30,8 +39,9 @@ function patternOf(watcher: FileSystemWatcher): { base: string | undefined; patt
30
39
  * (or absolute), a relative pattern to its own base. A path inside `node_modules`, `vendor` or `.git` only
31
40
  * matches a pattern that names that folder, since `**` over a dependency tree is what a server never means.
32
41
  */
33
- export function watchesFile(watchers: readonly FileSystemWatcher[], root: string, path: string, type: FileChangeType): boolean {
34
- const inProject = relativeTo(root, path);
42
+ export function watchesFile(watchers: readonly FileSystemWatcher[], root: string, changed: string, type: FileChangeType): boolean {
43
+ const path = slashed(changed);
44
+ const inProject = relativeTo(slashed(root), path);
35
45
  const segments = (inProject ?? path).split('/');
36
46
  return watchers.some((watcher) => {
37
47
  if (((watcher.kind ?? EVERY_CHANGE) & (1 << (type - 1))) === 0) {
@@ -42,7 +52,7 @@ export function watchesFile(watchers: readonly FileSystemWatcher[], root: string
42
52
  return false;
43
53
  }
44
54
  const { base, pattern } = parts;
45
- const subject = pattern.startsWith('/') ? path : base === undefined ? inProject : relativeTo(base, path);
55
+ const subject = pattern.startsWith('/') || /^[A-Za-z]:\//.test(pattern) ? path : base === undefined ? inProject : relativeTo(base, path);
46
56
  if (subject === null || !globMatch(subject, pattern)) {
47
57
  return false;
48
58
  }