Skip to content

Sessions and transports ​

Transports ​

An LspTransport sends and receives whole JSON-RPC messages: send(message), onMessage(listener), onClose(listener) and close(). Messages that arrive before the first listener are kept for it. One transport carries one connection.

TransportOver
createStreamTransport(stream, options?)A ByteStream, such as the stdin and stdout of a server process, with Content-Length framing
connectWebSocket(url, options?)A WebSocket, one JSON text frame per message; resolves once the socket is open
createMemoryTransportPair()Two ends in memory, for tests; each message is JSON-copied and delivered on a microtask

A ByteStream is the app's adapter over a process: write(chunk), onData, onClose and close. Wire the server's stdout to onData and keep its stderr apart, since a log line in stdout breaks the framing. The decoder takes messages of up to 32 MiB (maxMessageBytes) and closes the stream on a frame it cannot read.

ts
import { spawn } from 'node:child_process';
import { createStreamTransport, type ByteStream } from '@adecore/lsp';

const child = spawn(executable, ['--stdio'], { cwd: projectFolder });

const stream: ByteStream = {
    write: (chunk) => void child.stdin.write(chunk),
    onData: (listener) => {
        child.stdout.on('data', listener);
        return { dispose: () => child.stdout.off('data', listener) };
    },
    onClose: (listener) => {
        const exited = (): void => listener();
        child.once('exit', exited);
        return { dispose: () => child.off('exit', exited) };
    },
    close: () => void child.kill()
};

const transport = createStreamTransport(stream);
child.stderr.on('data', (chunk) => log(String(chunk)));

connectWebSocket takes protocols, a signal to cancel the opening, timeoutMs (10 seconds by default) and a createWebSocket factory for a runtime without a global WebSocket. A binary frame or a frame that is no JSON closes the socket, and sending fails once the socket buffers more than 8 MiB. There is no reconnect and no authentication; those are the app's.

encodeMessage and ContentLengthDecoder are the framing on their own.

LspSession ​

new LspSession(transport, options?) is one conversation with one server. Call initialize() and wait for it before anything else; it sends initialize, checks the position encoding, sends initialized and then the configuration, if any.

LspSessionOptionsDefaultMeaning
rootUrinullThe workspace root
workspaceFoldersfrom rootUriThe folders of the workspace
clientInfo@adecore/lspThe name, and a version, the server sees
initializationOptionsWhat the server takes in its handshake
configuration{}Settings, sent after the handshake and answered to workspace/configuration
timeoutMs30 secondsThe deadline of a request; 0 turns it off
snippetSupportfalseWhether completion items may be snippets
onConfiguration(item)Answers each configuration item instead of configuration
onApplyEdit(params)refusesApplies a workspace/applyEdit of the server
onShowMessage(params)nullAnswers a window/showMessageRequest

Without onConfiguration, a configuration query without a section gets all settings, and a section gets the key or the dotted path, or null. setConfiguration(settings) replaces them and tells the server. Answer onApplyEdit with applied: true only once the edit is made.

state goes from new to initializing, ready, closing and closed. A closed session does not start again.

Capabilities ​

supports(method, document?) says whether the server answers a method, from its static capabilities and from what it registered since, filtered by the document selector of a registration. providerOptions(method, document?) returns the options of that provider, such as trigger characters or a semantic tokens legend. Ask for the exact method: completionItem/resolve needs resolveProvider, textDocument/prepareRename needs prepareProvider.

onCapabilitiesChanged(listener) fires when the server registers or unregisters a capability and when it asks the client to refresh semantic tokens, inlay hints, diagnostics or code lenses. Ask again for what depends on it.

Listening ​

MethodWhat it hears
onDiagnostics(listener)Published diagnostics of open documents; a report for another version is dropped, and closing a document sends an empty one
onProgress(listener)$/progress
onNotification(method, listener)Any notification of the server
onRequest(method, handler)A request of the server; the handler gets the params and an AbortSignal
onError(listener)Errors of the connection and of listeners

The session answers configuration, workspace folders, applyEdit, message requests, progress creation, registrations and refreshes itself. request(method, params?, options?), notify(method, params?), executeCommand, workspaceSymbols and resolveWorkspaceSymbol reach the server directly.

Errors ​

A request rejects with an LspError that carries a code from ErrorCodes, and its data:

CodeValueMeaning
RequestCancelled-32800Cancelled by a signal or the deadline; $/cancelRequest was sent
ContentModified-32801The text changed under the request
MethodNotFound-32601The server does not answer this method
ServerNotInitialized-32002The session is not ready
InvalidRequest-32600The message was not a valid request
InternalError-32603Anything else

StaleResultError is an LspError with ContentModified and the document's uri, for an answer about a text that moved on. Drop it and ask again.

Shutdown ​

shutdown() waits for a pending handshake, closes the documents, asks shutdown with a deadline of five seconds, sends exit and closes the connection, also when one of those fails. Calling it twice returns the same promise. Stopping the process stays with the app.

Licenses and third-party notices are listed in each package.