English
A Server owns one LSP connection. A Transport is the message-framed channel that carries that connection's JSON-RPC envelopes. Choose the Transport from the host that owns the connection; handler registration and business logic do not change with that choice.
Selection guide
| Host and connection | Choose | Cargo invocation | Wire framing |
|---|---|---|---|
| Editor launches a native process | stdio | default features, or --no-default-features --features stdio | Content-Length |
| One native client connects to a port | TCP | --no-default-features --features tcp | Content-Length |
| One native WebSocket client connects | WebSocket | --no-default-features --features websocket | One JSON envelope per text or binary message |
| Browser or Node host transfers a port to a WASM Worker | worker-channel | --target wasm32-unknown-unknown --no-default-features --features worker-channel | One JSON envelope per MessagePort message |
| An embedding already has another message channel | custom Transport | Enable runtime-tokio on native or wasm on WASM, plus only dependencies your adapter needs | Defined by the adapter |
The first-party TCP and WebSocket builders bind once, accept one client, and then drop their listener. To serve another connection, construct another Server; connection state is intentionally not shared.
Cargo features
Default features select only stdio. Use default-features = false when a different Transport should not carry the stdio dependency graph.
toml
[dependencies]
lspf = { version = "1.0.0", default-features = false, features = ["tcp"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }The crate requires Rust 1.98 or newer. The example selects only the TCP adapter; keep the default features when using stdio.
| Feature | Default | Enables | Public effect |
|---|---|---|---|
default | Yes | stdio | The native stdio experience |
stdio | Via default | runtime-tokio, tokio-util/codec, tokio/process | stdio, StdioBuilder, stdio Transport types, and stdio child supervision on native targets |
tcp | No | runtime-tokio, tokio-util/codec, tokio/net | tcp, TcpBuilder, and the TCP Transport types on native targets |
websocket | No | runtime-tokio, tokio-tungstenite, tokio/net | websocket, WebSocketBuilder, and the WebSocket Transport types on native targets |
runtime-tokio | Through a native Transport | tokio | Native execution for Server::serve; no I/O adapter by itself |
wasm | No | wasm-bindgen-futures | WASM execution for Server::serve; no I/O adapter by itself |
worker-channel | No | wasm, js-sys, wasm-bindgen, web-sys | worker_channel and its MessagePort Transport types on wasm32 |
testing | No | runtime-tokio, tokio/test-util | Native in-memory Transport, scripted peer, wire capture, virtual clock, and lifecycle journeys |
Application code also lists dependencies it names directly. For example, a #[tokio::main] binary needs its own tokio dependency even though lspf's selected native Transport uses Tokio internally.
Target and feature compatibility
| Target and feature selection | Status | Reason or available serving path |
|---|---|---|
Native default or stdio | Supported | Serve with lspf::stdio(server) |
Native tcp | Supported | Serve with lspf::tcp(server, address) |
Native websocket | Supported | Serve with lspf::websocket(server, address) |
Native runtime-tokio without an adapter | Supported for custom Transports | Call server.serve(custom_transport) |
Native testing, alone or with another native row | Supported for external protocol tests | Use MemoryTransport, a lifecycle journey, or VirtualClock from lspf::testing |
| Native with no runtime feature | Supported for protocol-only compilation | Registration and protocol types are available, but serving is not |
Native worker-channel | Intentionally invalid | The feature emits a compile error because MessagePort belongs in a WASM Worker |
wasm32-unknown-unknown worker-channel | Supported | It implies wasm; serve with lspf::worker_channel(server, port) |
wasm32-unknown-unknown wasm without an adapter | Supported for custom Transports | Call server.serve(custom_transport) |
wasm32-unknown-unknown without wasm | Intentionally invalid | Every WASM build requires its target runtime glue |
wasm32-unknown-unknown default or stdio | Unsupported | stdio is a native adapter; disable default features |
wasm32-unknown-unknown with tcp or websocket | Intentionally invalid | These adapters require native Tokio sockets and emit a compile error |
wasm32-unknown-unknown with testing | Intentionally invalid | The deterministic testing surface uses Tokio's native virtual clock |
Do not combine native adapters with worker-channel in one build. A project that ships native and WASM artifacts selects their features in separate Cargo commands.
Buildable examples and shared handlers
The examples keep every handler in examples/shared/mod.rs. The same hover, completion, shared/ping, and didOpen hook names, parameters, and return shapes are registered for every host. Only the last serving call differs:
shared_server.rsserves the shared handler set over stdio and also compiles as a runtime-only WASM example.native_tcp.rsserves it over one TCP connection.native_websocket.rsserves it over one WebSocket connection.worker_channel.rsexports a wasm-bindgenserve(MessagePort)function for browser and Node Workers. Itsbrowserandnodehost packages compile that Rust export, generate the appropriate JavaScript glue, and validate the host files.
Build the native examples independently so each resolves only its adapter:
bash
cargo check -p lspf --example native_tcp \
--no-default-features --features tcp
cargo check -p lspf --example native_websocket \
--no-default-features --features websocketTo drive either socket example from a real editor, run the VS Code launch configuration Run LSP example client over a socket (select transport). The test client starts the example and connects its language client to the bound port, so the adapter is exercised by the editor rather than by a script. Zed launches every language server as a command over stdio and offers no socket option, so it cannot connect to these two examples.
For an unattended check, the transport probe builds, serves, and asserts one full LSP session per transport:
bash
node tools/lsp-transport-probe/main.mjs bothBuild both WASM examples for their real target:
bash
cargo check -p lspf --example shared_server \
--target wasm32-unknown-unknown --no-default-features --features wasm
cargo check -p lspf --example worker_channel \
--target wasm32-unknown-unknown --no-default-features \
--features worker-channelBrowser Worker host
Install the wasm-bindgen CLI version matching Cargo.lock, then build the checked-in browser host from the repository root:
bash
cargo install wasm-bindgen-cli --version 0.2.127 --locked
npm --prefix crates/lspf/examples/worker_channel_hosts/browser run buildThat package runs these exact Rust and wasm-bindgen commands before checking the host modules with Node's JavaScript parser:
bash
cargo build -p lspf --example worker_channel \
--target wasm32-unknown-unknown --no-default-features \
--features worker-channel --locked
wasm-bindgen --target web \
--out-dir crates/lspf/examples/worker_channel_hosts/browser/pkg \
target/wasm32-unknown-unknown/debug/examples/worker_channel.wasmServe the browser directory from an HTTP server and open index.html. main.mjs creates the channel and transfers one endpoint; its exported lspPort belongs to the LSP client. The module Worker initializes the generated web binding and passes the transferred port to the Rust serve export.
Node Worker host
Build and run the checked-in Node host from the repository root:
bash
npm --prefix crates/lspf/examples/worker_channel_hosts/node run build
npm --prefix crates/lspf/examples/worker_channel_hosts/node run smokeIts build runs the same Cargo command above and generates CommonJS bindings with this exact command:
bash
wasm-bindgen --target nodejs \
--out-dir crates/lspf/examples/worker_channel_hosts/node/pkg \
target/wasm32-unknown-unknown/debug/examples/worker_channel.wasmmain.cjs creates a worker_threads.MessageChannel, transfers the server port to worker.cjs, and uses the client port to complete initialize, initialized, shutdown, and exit. The smoke command fails unless the Worker returns a successful Outcome.
The JavaScript host owns Worker creation and termination. The lspf adapter starts and closes only the supplied port.
Continue with Stdio and custom transports for process ownership, framing rules, and embedded hosts.