@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 CHANGED
@@ -6,7 +6,7 @@ FSL-1.1-MIT
6
6
 
7
7
  ## Notice
8
8
 
9
- Copyright 2026 Bas Milius
9
+ Copyright 2026 - present Bas Milius
10
10
 
11
11
  ## Terms and Conditions
12
12
 
package/README.md CHANGED
@@ -1,33 +1,47 @@
1
1
  # @adecore/lsp
2
2
 
3
- Read the [LSP handbook](https://adecore.dev/lsp/handbook/) for the complete setup, concepts, host integration and testing guides.
3
+ [![npm](https://img.shields.io/npm/v/@adecore/lsp)](https://www.npmjs.com/package/@adecore/lsp)
4
+ [![Docs](https://img.shields.io/badge/docs-adecore.dev-blue)](https://adecore.dev/lsp/)
4
5
 
5
- A DOM-free LSP 3.17 client, and the `LanguageService` interface the smart editor asks its language features through. It spawns nothing and imports no Node or Bun API outside its tests, so a host can use it in a server process or browser. The document model it feeds is `@adecore/editor-core`; the view is `@adecore/editor`.
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
- Positions are LSP positions: zero-based lines and UTF-16 characters, with a line ending at `\n`, `\r\n` or a lone `\r`. Turning them into editor offsets is the view's job.
8
+ ## Install
8
9
 
9
- ## API
10
+ ```sh
11
+ bun add @adecore/lsp
12
+ ```
10
13
 
11
- Everything comes from the package root; `@adecore/lsp/testing` holds the test doubles.
14
+ ## Use
12
15
 
13
- - `LspSession`: one conversation with one server over an `LspTransport`. `initialize` negotiates capabilities (UTF-16 only, snippets off unless asked), `openDocument` returns an `LspDocument`, and `supports` / `providerOptions` answer from static capabilities and dynamic registrations alike. It answers the server's configuration, folder, registration, progress and message requests, and `onDiagnostics`, `onCapabilitiesChanged` (registrations and refresh requests), `onProgress`, `onNotification` and `onError` observe it. `shutdown` closes the documents, then sends `shutdown` and `exit`.
14
- - `LspDocument`: one open file in one session, and the owner of its version. `applyChanges` takes `didChange` entries and sends them as they are to a server that negotiated incremental sync, or the whole text to one that negotiated full sync. `updateText` sends the one range that differs from the held text. Every call raises the version by one. A request is cancelled when the text changes or a newer one for the same feature is made (`cancelPrevious`), and an answer for an older version rejects with `StaleResultError`, so does resolving a completion item, code action, lens or hint of an older version. One method per feature: completion, hover, signature help, the four navigations, references, highlights, symbols, rename, code actions, code lenses, formatting, folding, semantic tokens (full, delta, range), inlay hints and pull diagnostics.
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
- ## Known limits
19
+ const session = new LspSession(createStreamTransport(stream), { rootUri: pathToFileUri('/work') });
20
+ await session.initialize();
25
21
 
26
- - Unversioned diagnostics cannot be proven fresh and are accepted while their document is open.
27
- - The package applies no workspace edit and touches no file. File creates, renames and deletes in a `WorkspaceEdit` are the host's, and the host only declares renames.
28
- - Only UTF-16 position encoding is negotiated; a server that insists on another one fails to initialize.
29
- - Servers that negotiated no document changes (`change: 0`) cannot follow an edit.
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
- ## Packaging
27
+ An answer for a text that changed in the meantime rejects with `StaleResultError`.
32
28
 
33
- This unpublished package is private at version `0.0.0` and retains FSL-1.1-MIT. Source consumers enable `source` in the bundler and TypeScript `customConditions`. Default consumers use compiled JavaScript and declarations. Run `bun run build`, `bun run typecheck` and `bun run test` from the package after workspace installation. Tests use memory transports and fake servers; no installed language server is required.
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
- /* Plans text edits in memory. A create, rename or delete needs the host's own handling and is refused. */
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
- throw new LspError(`A host file-operation handler is required for ${change.kind}`);
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 snapshot = snapshots.get(uri);
150
- if (!snapshot) {
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
- const before = planned.get(uri)?.text ?? snapshot.text;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/lsp",
3
- "version": "0.17.0-beta.1",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
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
- /* Plans text edits in memory. A create, rename or delete needs the host's own handling and is refused. */
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
- throw new LspError(`A host file-operation handler is required for ${change.kind}`);
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 snapshot = snapshots.get(uri);
171
- if (!snapshot) {
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
- const before = planned.get(uri)?.text ?? snapshot.text;
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,