@adecore/lsp 0.17.0-beta.1 → 0.17.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/LICENSE +1 -1
- package/README.md +36 -22
- package/dist/edits.d.ts +1 -0
- package/dist/edits.js +32 -10
- package/dist/session.js +1 -1
- package/package.json +1 -1
- package/src/edits.ts +35 -11
- package/src/session.ts +1 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,33 +1,47 @@
|
|
|
1
1
|
# @adecore/lsp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@adecore/lsp)
|
|
4
|
+
[](https://adecore.dev/lsp/)
|
|
4
5
|
|
|
5
|
-
A
|
|
6
|
+
A client for the Language Server Protocol 3.17, without a DOM and without Node or Bun APIs, so it runs in a backend, a utility process or a page. It also defines `LanguageService`, the interface [`@adecore/editor-react`](https://adecore.dev/editor-react/) asks its features through. It starts no server: the app hands it a transport.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## Install
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
```sh
|
|
11
|
+
bun add @adecore/lsp
|
|
12
|
+
```
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
## Use
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- `JsonRpcConnection`: correlation, `$/cancelRequest`, a request timeout (30 seconds, `0` turns it off) and the requests a server sends. `LspError` carries a JSON-RPC code; `ErrorCodes` names the ones used here.
|
|
16
|
-
- Transports: `createStreamTransport` over a `ByteStream` (a child's stdin and stdout, which the host adapts), `ContentLengthDecoder` and `encodeMessage` for the framing, `createMemoryTransportPair` and `connectWebSocket`.
|
|
17
|
-
- Edits: `applyTextEdits` and `planWorkspaceEdit` (simultaneous edits and a multi-file plan, which refuses file operations), `applyContentChanges` (sequential), `minimalChange`, `offsetAt`, `positionAt` and `endPosition`.
|
|
18
|
-
- File renames: `willRenameFiles` and `didRenameFiles` of `LspSession` take the files that move (`RenamedFile`, which says whether each is a folder) and pass on only those that the filters of the server take, from its capabilities and its registrations (`fileOperationFilters`, `renamesTaken`: scheme, glob on the path the file leaves, `matches`, `ignoreCase`). The session declares `resourceOperations: ['rename']`, `workspace.fileOperations` and the `refactor.move` kind in `initialize`.
|
|
19
|
-
- `bridgeVueTypeScript` relays Vue's `tsserver/request` to the TypeScript server, and `vueServerOrder` says which of the two servers of a `.vue` document to ask first.
|
|
20
|
-
- `pathToFileUri` and `fileUriToPath`.
|
|
21
|
-
- `LanguageService`: what the editor asks of the language side (open, change and close a document, every feature above, `executeCommand`, `supports`, `providerOptions`, `onProvidersChanged` and `onDiagnostics`). Results stay in LSP shapes. A request takes `signal` and `parallel`, which lets many requests of one feature run side by side instead of each taking over from the one before. A host adapts server sessions or its own transport to this interface. Server discovery, process startup, authorization and saving remain outside the library.
|
|
22
|
-
- `@adecore/lsp/testing`: `FakeLanguageServer`, a server on the far end of a transport that answers the handshake, mirrors document text and answers whatever a test registers, and `createMemoryTransportPair`.
|
|
16
|
+
```ts
|
|
17
|
+
import { LspSession, createStreamTransport, pathToFileUri } from '@adecore/lsp';
|
|
23
18
|
|
|
24
|
-
|
|
19
|
+
const session = new LspSession(createStreamTransport(stream), { rootUri: pathToFileUri('/work') });
|
|
20
|
+
await session.initialize();
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
const document = session.openDocument({ uri: pathToFileUri('/work/example.ts'), languageId: 'typescript', text });
|
|
23
|
+
await document.updateText(nextText);
|
|
24
|
+
const hover = await document.hover({ line: 0, character: 6 });
|
|
25
|
+
```
|
|
30
26
|
|
|
31
|
-
|
|
27
|
+
An answer for a text that changed in the meantime rejects with `StaleResultError`.
|
|
32
28
|
|
|
33
|
-
|
|
29
|
+
## Entry points
|
|
30
|
+
|
|
31
|
+
| Import | What it holds |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `@adecore/lsp` | `LspSession`, `LspDocument`, the transports, the edit and URI helpers, the Vue bridge and the protocol types |
|
|
34
|
+
| `@adecore/lsp/testing` | `FakeLanguageServer` and `createMemoryTransportPair` |
|
|
35
|
+
|
|
36
|
+
## Documentation
|
|
37
|
+
|
|
38
|
+
| Page | What it covers |
|
|
39
|
+
|---|---|
|
|
40
|
+
| [Sessions and transports](https://adecore.dev/lsp/sessions) | Stdio, WebSocket and memory transports, the handshake, capabilities and errors |
|
|
41
|
+
| [Documents and edits](https://adecore.dev/lsp/documents) | Versions, requests, stale answers, edit helpers, workspace edits and renames |
|
|
42
|
+
| [Language service](https://adecore.dev/lsp/language-service) | The interface for editor features, and Vue with TypeScript |
|
|
43
|
+
| [Testing](https://adecore.dev/lsp/testing) | A fake server in memory |
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
FSL-1.1-MIT
|
package/dist/edits.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface PlannedDocumentEdit {
|
|
|
8
8
|
version: number | null;
|
|
9
9
|
before: string;
|
|
10
10
|
text: string;
|
|
11
|
+
created?: true;
|
|
11
12
|
}
|
|
12
13
|
export declare function offsetAt(text: string, position: Position): number;
|
|
13
14
|
export declare function positionAt(text: string, offset: number): Position;
|
package/dist/edits.js
CHANGED
|
@@ -137,25 +137,47 @@ export function minimalChange(before, after) {
|
|
|
137
137
|
text: after.slice(start, after.length - end)
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
|
-
|
|
140
|
+
function snapshotEdit(uri, snapshots) {
|
|
141
|
+
const snapshot = snapshots.get(uri);
|
|
142
|
+
if (!snapshot) {
|
|
143
|
+
throw new LspError(`Missing workspace snapshot: ${uri}`);
|
|
144
|
+
}
|
|
145
|
+
return { uri, version: snapshot.version, before: snapshot.text, text: snapshot.text };
|
|
146
|
+
}
|
|
147
|
+
/* A file that is there already is refused, unless the create says to empty it or to leave it as it is. */
|
|
148
|
+
function planCreate(change, snapshots, planned) {
|
|
149
|
+
const existing = planned.get(change.uri) ?? (snapshots.has(change.uri) ? snapshotEdit(change.uri, snapshots) : null);
|
|
150
|
+
if (existing === null) {
|
|
151
|
+
planned.set(change.uri, { uri: change.uri, version: null, before: '', text: '', created: true });
|
|
152
|
+
}
|
|
153
|
+
else if (change.options?.overwrite) {
|
|
154
|
+
planned.set(change.uri, { ...existing, text: '' });
|
|
155
|
+
}
|
|
156
|
+
else if (!change.options?.ignoreIfExists) {
|
|
157
|
+
throw new LspError(`${change.uri} already exists`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/*
|
|
161
|
+
* Plans text edits and created files in memory, in the order the edit gives them. `snapshots` holds the
|
|
162
|
+
* 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.
|
|
163
|
+
*/
|
|
141
164
|
export function planWorkspaceEdit(edit, snapshots) {
|
|
142
165
|
const planned = new Map();
|
|
143
166
|
const changes = edit.documentChanges ?? Object.entries(edit.changes ?? {}).map(([uri, edits]) => ({ textDocument: { uri, version: null }, edits }));
|
|
144
167
|
for (const change of changes) {
|
|
145
168
|
if ('kind' in change) {
|
|
146
|
-
|
|
169
|
+
if (change.kind !== 'create') {
|
|
170
|
+
throw new LspError(`A host file-operation handler is required for ${change.kind}`);
|
|
171
|
+
}
|
|
172
|
+
planCreate(change, snapshots, planned);
|
|
173
|
+
continue;
|
|
147
174
|
}
|
|
148
175
|
const { uri, version } = change.textDocument;
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
151
|
-
throw new LspError(`Missing workspace snapshot: ${uri}`);
|
|
152
|
-
}
|
|
153
|
-
if (version !== null && snapshot.version !== version) {
|
|
176
|
+
const current = planned.get(uri) ?? snapshotEdit(uri, snapshots);
|
|
177
|
+
if (version !== null && current.version !== version) {
|
|
154
178
|
throw new LspError(`Workspace edit version mismatch: ${uri}`, ErrorCodes.ContentModified);
|
|
155
179
|
}
|
|
156
|
-
|
|
157
|
-
const text = applyTextEdits(before, change.edits);
|
|
158
|
-
planned.set(uri, { uri, version: snapshot.version, before: snapshot.text, text });
|
|
180
|
+
planned.set(uri, { ...current, text: applyTextEdits(current.text, change.edits) });
|
|
159
181
|
}
|
|
160
182
|
return [...planned.values()];
|
|
161
183
|
}
|
package/dist/session.js
CHANGED
|
@@ -395,7 +395,7 @@ function clientCapabilities(options) {
|
|
|
395
395
|
general: { positionEncodings: ['utf-16'] },
|
|
396
396
|
workspace: {
|
|
397
397
|
applyEdit: !!options.onApplyEdit,
|
|
398
|
-
workspaceEdit: { documentChanges: true, resourceOperations: ['rename'], failureHandling: 'abort' },
|
|
398
|
+
workspaceEdit: { documentChanges: true, resourceOperations: ['create', 'rename'], failureHandling: 'abort' },
|
|
399
399
|
fileOperations: { dynamicRegistration: true, willRename: true, didRename: true },
|
|
400
400
|
configuration: true,
|
|
401
401
|
workspaceFolders: true,
|
package/package.json
CHANGED
package/src/edits.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ErrorCodes, LspError } from './connection.ts';
|
|
2
|
-
import type { ContentChange, Position, TextEdit, WorkspaceEdit } from './protocol.ts';
|
|
2
|
+
import type { ContentChange, CreateFile, Position, TextEdit, WorkspaceEdit } from './protocol.ts';
|
|
3
3
|
|
|
4
4
|
export interface DocumentSnapshot {
|
|
5
5
|
text: string;
|
|
@@ -11,6 +11,8 @@ export interface PlannedDocumentEdit {
|
|
|
11
11
|
version: number | null;
|
|
12
12
|
before: string;
|
|
13
13
|
text: string;
|
|
14
|
+
/* A file the edit creates, which was not there: the host makes it with `text` and fails when it is there by then. */
|
|
15
|
+
created?: true;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/* A line ends at `\n`, `\r\n` or a lone `\r`, which is how the protocol counts lines. */
|
|
@@ -158,25 +160,47 @@ export function minimalChange(before: string, after: string): ContentChange {
|
|
|
158
160
|
};
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
|
|
163
|
+
function snapshotEdit(uri: string, snapshots: ReadonlyMap<string, DocumentSnapshot>): PlannedDocumentEdit {
|
|
164
|
+
const snapshot = snapshots.get(uri);
|
|
165
|
+
if (!snapshot) {
|
|
166
|
+
throw new LspError(`Missing workspace snapshot: ${uri}`);
|
|
167
|
+
}
|
|
168
|
+
return { uri, version: snapshot.version, before: snapshot.text, text: snapshot.text };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* A file that is there already is refused, unless the create says to empty it or to leave it as it is. */
|
|
172
|
+
function planCreate(change: CreateFile, snapshots: ReadonlyMap<string, DocumentSnapshot>, planned: Map<string, PlannedDocumentEdit>): void {
|
|
173
|
+
const existing = planned.get(change.uri) ?? (snapshots.has(change.uri) ? snapshotEdit(change.uri, snapshots) : null);
|
|
174
|
+
if (existing === null) {
|
|
175
|
+
planned.set(change.uri, { uri: change.uri, version: null, before: '', text: '', created: true });
|
|
176
|
+
} else if (change.options?.overwrite) {
|
|
177
|
+
planned.set(change.uri, { ...existing, text: '' });
|
|
178
|
+
} else if (!change.options?.ignoreIfExists) {
|
|
179
|
+
throw new LspError(`${change.uri} already exists`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/*
|
|
184
|
+
* Plans text edits and created files in memory, in the order the edit gives them. `snapshots` holds the
|
|
185
|
+
* 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.
|
|
186
|
+
*/
|
|
162
187
|
export function planWorkspaceEdit(edit: WorkspaceEdit, snapshots: ReadonlyMap<string, DocumentSnapshot>): PlannedDocumentEdit[] {
|
|
163
188
|
const planned = new Map<string, PlannedDocumentEdit>();
|
|
164
189
|
const changes = edit.documentChanges ?? Object.entries(edit.changes ?? {}).map(([uri, edits]) => ({ textDocument: { uri, version: null }, edits }));
|
|
165
190
|
for (const change of changes) {
|
|
166
191
|
if ('kind' in change) {
|
|
167
|
-
|
|
192
|
+
if (change.kind !== 'create') {
|
|
193
|
+
throw new LspError(`A host file-operation handler is required for ${change.kind}`);
|
|
194
|
+
}
|
|
195
|
+
planCreate(change, snapshots, planned);
|
|
196
|
+
continue;
|
|
168
197
|
}
|
|
169
198
|
const { uri, version } = change.textDocument;
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
throw new LspError(`Missing workspace snapshot: ${uri}`);
|
|
173
|
-
}
|
|
174
|
-
if (version !== null && snapshot.version !== version) {
|
|
199
|
+
const current = planned.get(uri) ?? snapshotEdit(uri, snapshots);
|
|
200
|
+
if (version !== null && current.version !== version) {
|
|
175
201
|
throw new LspError(`Workspace edit version mismatch: ${uri}`, ErrorCodes.ContentModified);
|
|
176
202
|
}
|
|
177
|
-
|
|
178
|
-
const text = applyTextEdits(before, change.edits);
|
|
179
|
-
planned.set(uri, { uri, version: snapshot.version, before: snapshot.text, text });
|
|
203
|
+
planned.set(uri, { ...current, text: applyTextEdits(current.text, change.edits) });
|
|
180
204
|
}
|
|
181
205
|
return [...planned.values()];
|
|
182
206
|
}
|
package/src/session.ts
CHANGED
|
@@ -480,7 +480,7 @@ function clientCapabilities(options: LspSessionOptions): object {
|
|
|
480
480
|
general: { positionEncodings: ['utf-16'] },
|
|
481
481
|
workspace: {
|
|
482
482
|
applyEdit: !!options.onApplyEdit,
|
|
483
|
-
workspaceEdit: { documentChanges: true, resourceOperations: ['rename'], failureHandling: 'abort' },
|
|
483
|
+
workspaceEdit: { documentChanges: true, resourceOperations: ['create', 'rename'], failureHandling: 'abort' },
|
|
484
484
|
fileOperations: { dynamicRegistration: true, willRename: true, didRename: true },
|
|
485
485
|
configuration: true,
|
|
486
486
|
workspaceFolders: true,
|