Skip to content

Commit c0cd8ce

Browse files
committed
update lsp docs
1 parent e095e91 commit c0cd8ce

File tree

1 file changed

+260
-86
lines changed

1 file changed

+260
-86
lines changed

docs/advanced-apis/lsp.md

Lines changed: 260 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,210 @@
11
# LSP API
22

3-
Use this API to register/manage language servers used by Acode's CodeMirror LSP client.
3+
Use this API to register and manage language servers for Acode's CodeMirror LSP integration.
44

55
## Import
66

77
```js
88
const lsp = acode.require("lsp");
99
```
1010

11-
## Important Transport Reality
11+
## Current API Shape
1212

13-
CodeMirror's LSP client in Acode communicates via **WebSocket** transport.
13+
The public API is intentionally small:
1414

15-
- `transport.kind: "websocket"` is the recommended and practical mode.
16-
- `transport.kind: "stdio"` is **not a direct stdio pipe** to the editor.
17-
- In this codebase, `stdio` mode still expects a **WebSocket bridge URL**.
15+
- `lsp.defineServer(...)`
16+
- `lsp.defineBundle(...)`
17+
- `lsp.register(entry, options?)`
18+
- `lsp.upsert(entry)`
19+
- `lsp.installers.*`
20+
- `lsp.servers.*`
21+
- `lsp.bundles.*`
1822

19-
For local stdio language servers, use an **AXS bridge** (`launcher.bridge`) and keep transport as websocket.
23+
`bundle` is the public name for what the internal runtime still calls a provider:
24+
25+
- a bundle can own one or more server definitions
26+
- a bundle can also provide install/check behavior hooks
27+
- most plugins only need a single server
28+
- use a bundle when you ship a family of related servers or custom install logic
29+
30+
## Transport Reality
31+
32+
Acode's LSP client still speaks WebSocket to the transport layer.
33+
34+
- `transport.kind: "websocket"` is the normal and recommended setup
35+
- local stdio servers should usually be launched through `launcher.bridge`
36+
- `transport.kind: "stdio"` still expects a WebSocket bridge URL
37+
- `transport.kind: "external"` is available for custom transport factories
38+
39+
For local servers, prefer `transport.kind: "websocket"` plus an AXS bridge.
2040

2141
::: warning
22-
`stdio` with only `transport.command` is not enough in Acode.
23-
You still need a WebSocket bridge endpoint (AXS or equivalent) for the client connection.
42+
`transport.kind: "stdio"` is not a direct pipe from the editor to the server.
43+
It still resolves to the WebSocket transport layer and requires a bridge URL.
2444
:::
2545

26-
## Recommended Setup (Local Server via AXS Bridge)
46+
## Recommended Single-Server Setup
47+
48+
Use `defineServer()` and `upsert()` for idempotent registration.
49+
50+
```js
51+
const lsp = acode.require("lsp");
52+
53+
const typescriptServer = lsp.defineServer({
54+
id: "typescript-custom",
55+
label: "TypeScript (Custom)",
56+
languages: [
57+
"javascript",
58+
"javascriptreact",
59+
"typescript",
60+
"typescriptreact",
61+
"tsx",
62+
"jsx",
63+
],
64+
useWorkspaceFolders: true,
65+
transport: {
66+
kind: "websocket",
67+
},
68+
command: "typescript-language-server",
69+
args: ["--stdio"],
70+
checkCommand: "which typescript-language-server",
71+
installer: lsp.installers.npm({
72+
executable: "typescript-language-server",
73+
packages: ["typescript-language-server", "typescript"],
74+
}),
75+
initializationOptions: {
76+
provideFormatter: true,
77+
},
78+
});
79+
80+
lsp.upsert(typescriptServer);
81+
```
82+
83+
## Bundle Setup
84+
85+
Use a bundle when one plugin contributes multiple servers or needs custom install behavior.
86+
87+
```js
88+
const lsp = acode.require("lsp");
89+
90+
const htmlServer = lsp.defineServer({
91+
id: "my-html",
92+
label: "My HTML Server",
93+
languages: ["html"],
94+
transport: {
95+
kind: "websocket",
96+
},
97+
command: "vscode-html-language-server",
98+
args: ["--stdio"],
99+
installer: lsp.installers.npm({
100+
executable: "vscode-html-language-server",
101+
packages: ["vscode-langservers-extracted"],
102+
}),
103+
});
104+
105+
const cssServer = lsp.defineServer({
106+
id: "my-css",
107+
label: "My CSS Server",
108+
languages: ["css", "scss", "less"],
109+
transport: {
110+
kind: "websocket",
111+
},
112+
command: "vscode-css-language-server",
113+
args: ["--stdio"],
114+
installer: lsp.installers.npm({
115+
executable: "vscode-css-language-server",
116+
packages: ["vscode-langservers-extracted"],
117+
}),
118+
});
119+
120+
const webBundle = lsp.defineBundle({
121+
id: "my-web-bundle",
122+
label: "My Web Bundle",
123+
servers: [htmlServer, cssServer],
124+
});
125+
126+
lsp.upsert(webBundle);
127+
```
128+
129+
## Bundle Hooks
130+
131+
Bundles can own behavior, not just server lists.
132+
133+
Available hooks:
134+
135+
- `getExecutable(serverId, manifest)`
136+
- `checkInstallation(serverId, manifest)`
137+
- `installServer(serverId, manifest, mode, options?)`
138+
139+
Example:
27140

28141
```js
29-
lsp.registerServer(
30-
{
31-
id: "typescript-custom",
32-
label: "TypeScript (Custom)",
33-
languages: ["javascript", "javascriptreact", "typescript", "typescriptreact", "tsx", "jsx"],
34-
transport: {
35-
kind: "websocket",
36-
// url can be omitted when using launcher.bridge auto-port mode
142+
const bundle = lsp.defineBundle({
143+
id: "my-bundle",
144+
label: "My Bundle",
145+
servers: [myServer],
146+
hooks: {
147+
getExecutable(serverId, manifest) {
148+
return manifest.launcher?.install?.binaryPath || null;
37149
},
38-
launcher: {
39-
bridge: {
40-
kind: "axs",
41-
command: "typescript-language-server",
42-
args: ["--stdio"],
43-
},
44-
checkCommand: "which typescript-language-server",
45-
install: {
46-
command:
47-
"apk add --no-cache nodejs npm && npm install -g typescript-language-server typescript",
48-
},
150+
async checkInstallation(serverId, manifest) {
151+
return {
152+
status: "present",
153+
version: null,
154+
canInstall: true,
155+
canUpdate: true,
156+
};
49157
},
50-
enabled: true,
51-
initializationOptions: {
52-
provideFormatter: true,
158+
async installServer(serverId, manifest, mode) {
159+
console.log("install", serverId, mode);
160+
return true;
53161
},
54162
},
55-
{ replace: true },
56-
);
163+
});
57164
```
58165
59-
## Remote/External WebSocket Server
166+
## Structured Installers
167+
168+
Prefer structured installers over raw shell whenever possible.
169+
170+
Available installer builders:
171+
172+
- `lsp.installers.apk(...)`
173+
- `lsp.installers.npm(...)`
174+
- `lsp.installers.pip(...)`
175+
- `lsp.installers.cargo(...)`
176+
- `lsp.installers.githubRelease(...)`
177+
- `lsp.installers.manual(...)`
178+
- `lsp.installers.shell(...)`
179+
180+
Example:
60181
61182
```js
62-
lsp.registerServer({
63-
id: "remote-lsp",
64-
label: "Remote LSP",
183+
const server = lsp.defineServer({
184+
id: "python-custom",
185+
label: "Python (pylsp)",
186+
languages: ["python"],
187+
command: "pylsp",
188+
installer: lsp.installers.pip({
189+
executable: "pylsp",
190+
packages: ["python-lsp-server[all]"],
191+
}),
192+
});
193+
```
194+
195+
### Installer Notes
196+
197+
- managed installers should declare the executable they provide
198+
- `githubRelease()` is intended for arch-aware downloaded binaries
199+
- `manual()` is useful when the binary already exists at a known path
200+
- `shell()` should be treated as the advanced fallback, not the default path
201+
202+
## Remote WebSocket Server
203+
204+
```js
205+
lsp.upsert({
206+
id: "remote-json-lsp",
207+
label: "Remote JSON LSP",
65208
languages: ["json"],
66209
transport: {
67210
kind: "websocket",
@@ -75,66 +218,89 @@ lsp.registerServer({
75218
});
76219
```
77220
78-
## API Reference
79-
80-
### `registerServer(definition, options?)`
81-
82-
- `options.replace?: boolean` (default `false`)
83-
- `false`: existing server with same id is kept
84-
- `true`: existing server is replaced
85-
86-
Definition fields commonly used:
87-
88-
- `id` (required): normalized to lowercase
89-
- `label` (optional)
90-
- `languages` (required): non-empty array, normalized to lowercase
91-
- `enabled` (optional, default `true`)
92-
- `transport` (required)
93-
- `kind`: `"websocket"` or `"stdio"` (see transport note above)
94-
- `url?: string`
95-
- `command?: string` (required by registry validation when using `stdio`)
96-
- `args?: string[]`
97-
- `options?: { binary?, timeout?, reconnect?, maxReconnectAttempts? }`
98-
- `launcher` (optional)
99-
- `startCommand?: string | string[]`
100-
- `command?: string`
101-
- `args?: string[]`
102-
- `checkCommand?: string`
103-
- `install?: { command?: string }`
104-
- `bridge?: { kind: "axs", command: string, args?: string[], port?: number, session?: string }`
105-
- `initializationOptions?: object`
106-
- `clientConfig?: object`
107-
- `startupTimeout?: number`
108-
- `capabilityOverrides?: object`
109-
- `rootUri?: (uri, context) => string | null`
110-
- `resolveLanguageId?: (context) => string | null`
111-
- `useWorkspaceFolders?: boolean`
112-
113-
### Other Registry Methods
114-
115-
- `unregisterServer(id)`
116-
- `updateServer(id, updater)`
117-
- `getServer(id)`
118-
- `listServers()`
119-
- `getServersForLanguage(languageId, options?)`
221+
## Definition API
222+
223+
### `lsp.defineServer(options)`
224+
225+
Builds a normalized server manifest for later registration.
226+
227+
Common fields:
228+
229+
- `id`: required, normalized to lowercase by the registry
230+
- `label`: optional display label
231+
- `languages`: required non-empty array
232+
- `enabled`: defaults to `true`
233+
- `transport`
234+
- `command` and `args`: used to create an AXS launcher bridge
235+
- `installer`: structured installer config
236+
- `checkCommand`
237+
- `versionCommand`
238+
- `updateCommand`
239+
- `initializationOptions`
240+
- `clientConfig`
241+
- `startupTimeout`
242+
- `capabilityOverrides`
243+
- `rootUri`
244+
- `resolveLanguageId`
245+
- `useWorkspaceFolders`
246+
247+
### `lsp.defineBundle(options)`
248+
249+
Creates a bundle record.
250+
251+
Fields:
252+
253+
- `id`: required bundle id
254+
- `label`: optional
255+
- `servers`: array returned by `lsp.defineServer(...)`
256+
- `hooks?`: optional behavioral hooks
257+
258+
## Registration API
259+
260+
### `lsp.register(entry, options?)`
261+
262+
Registers either a server or bundle if the id is free.
263+
264+
- `options.replace?: boolean` defaults to `false`
265+
266+
### `lsp.upsert(entry)`
267+
268+
Registers or replaces either a server or bundle. This is the preferred method for plugin startup code.
269+
270+
## Server Inspection API
271+
272+
- `lsp.servers.get(id)`
273+
- `lsp.servers.list()`
274+
- `lsp.servers.listForLanguage(languageId, options?)`
275+
- `lsp.servers.update(id, updater)`
276+
- `lsp.servers.unregister(id)`
277+
- `lsp.servers.onChange(listener)`
278+
279+
Example:
120280
121281
```js
122-
const jsServers = lsp.getServersForLanguage("javascript");
282+
const jsServers = lsp.servers.listForLanguage("javascript");
123283

124-
lsp.updateServer("typescript-custom", (current) => ({
284+
lsp.servers.update("typescript-custom", (current) => ({
125285
...current,
126286
enabled: false,
127287
}));
128288
```
129289
130-
`getServersForLanguage` options:
290+
`listForLanguage()` options:
131291
132-
- `includeDisabled?: boolean` (default `false`)
292+
- `includeDisabled?: boolean` default `false`
293+
294+
## Bundle Inspection API
295+
296+
- `lsp.bundles.list()`
297+
- `lsp.bundles.getForServer(serverId)`
298+
- `lsp.bundles.unregister(id)`
133299
134300
## Client Manager
135301
136-
- `clientManager.setOptions(options)`
137-
- `clientManager.getActiveClients()`
302+
- `lsp.clientManager.setOptions(options)`
303+
- `lsp.clientManager.getActiveClients()`
138304
139305
```js
140306
lsp.clientManager.setOptions({
@@ -144,3 +310,11 @@ lsp.clientManager.setOptions({
144310
const activeClients = lsp.clientManager.getActiveClients();
145311
console.log(activeClients);
146312
```
313+
314+
## Best Practices
315+
316+
- Prefer `lsp.upsert(...)` during plugin init.
317+
- Prefer `defineServer()` and `defineBundle()` instead of hand-assembling objects everywhere.
318+
- Prefer structured installers over raw shell commands.
319+
- Use a bundle when your plugin owns a family of related servers or custom install logic.
320+
- Use `useWorkspaceFolders: true` for heavy workspace-aware servers like TypeScript or Rust.

0 commit comments

Comments
 (0)