Skip to content

spec: user-configurable language servers (#8803)#9848

Open
lonexreb wants to merge 1 commit intowarpdotdev:masterfrom
lonexreb:spec/8803-custom-lsp-config
Open

spec: user-configurable language servers (#8803)#9848
lonexreb wants to merge 1 commit intowarpdotdev:masterfrom
lonexreb:spec/8803-custom-lsp-config

Conversation

@lonexreb
Copy link
Copy Markdown
Contributor

@lonexreb lonexreb commented May 1, 2026

Description

Spec PR for #8803 ("Add support for adding custom language servers"), per @kevinyang372's direction on #9562 and #9568 — closing those PRs in favor of a user-configurable LSP path. This converts that product direction into testable behavior invariants plus a concrete implementation plan grounded in the existing crates/lsp/ infrastructure.

Linked Issue

What's in this PR

File Lines What
specs/GH8803/product.md ~140 15 numbered testable invariants. TOML config shape ([[lsp.servers]] array-of-tables with name / command / file_types / root_files / initialization_options / start_timeout_ms). Explicit non-goals (auto-install, version pinning, marketplace). User experience walkthrough (add → chip → enable → use → disable). Open questions (~ expansion, example-configs doc, network defaults).
specs/GH8803/tech.md ~270 File-by-file implementation plan. New UserConfiguredLanguageServer impl of LanguageServerCandidate. New LSPServerType::UserConfigured(...) variant. Per-workspace enabled_custom_servers / dismissed_custom_servers state. Settings-reload diff semantics. Per-invariant test plan. Risks called out.

Reuses lessons from the closed LSP PRs

This spec deliberately leans on patterns that survived bot review on #9562 and #9568, so the implementation phase doesn't relitigate them:

Scope discipline

  • Not implementing — this is spec-only, no Rust code yet. Implementation will land as a follow-up PR after spec approval.
  • Doesn't replace built-in servers — user-configured servers run alongside any built-in server matching the same file extension. The LSP client merges responses (existing behavior).
  • Doesn't auto-install — the user owns binary lifecycle for these (npm, brew, cargo, etc.). Warp's job is to discover, prompt, and connect.
  • Per-workspace enablement — defining a server is global; running it requires explicit opt-in per workspace via the editor footer chip.

Open questions for maintainers (also documented in product.md)

  1. ~ expansion in command[0] — Cmd-O / /open-file already use shellexpand::tilde; recommend yes for ~, defer $VAR to a follow-up. Confirm?
  2. Example configs doc — recommend shipping docs/custom-lsp-examples.md (intelephense, lua-language-server, zls, bash-language-server) alongside the implementation. Worth gating behind this spec or a separate docs PR?
  3. Schema-fetching restriction defaults — should we mandate / document that user-configured servers run with their network-fetching options disabled by default? Hard to enforce generically (each server has its own keys); spec recommends per-server initialization_options and an examples doc rather than a generic flag. Confirm?

Testing — for the spec itself

  • Both files render correctly on GitHub.
  • All 15 product invariants have a corresponding row in tech.md's testing matrix.
  • Cross-references to closed PR commits (31285c4, etc.) and existing files (crates/lsp/src/servers/intelephense.rs, app/src/settings/input.rs) verified at write time.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

CHANGELOG-NEW-FEATURE: Spec for user-configurable language servers (#8803) — defines the TOML config shape, per-workspace enablement model, and lifecycle for custom LSPs that aren't shipped built-in. Implementation PR to follow. Thanks @lonexreb!

Per @kevinyang372's direction on warpdotdev#9562 and warpdotdev#9568 (closing those PRs in
favor of a user-configurable LSP path), this spec turns warpdotdev#8803's product
direction into testable behavior invariants and a concrete
implementation plan grounded in the existing LSP infrastructure.

product.md
- 15 numbered, testable invariants covering parse/validate, chip
  behavior (per-workspace enablement, dismissal, persistence across
  restart), spawn outcomes (success / not-on-PATH / crash / timeout),
  settings reload diff semantics, coexistence with built-in servers,
  and graceful shutdown.
- TOML configuration shape with `name` / `command` / `file_types` /
  optional `root_files` / `initialization_options` / `start_timeout_ms`.
- Explicit non-goals: auto-install, version pinning, replacing built-in
  servers, marketplace.
- Open questions called out: `~` expansion in `command[0]`, example
  configs doc, schema-fetching restriction defaults.

tech.md
- File-by-file implementation plan against `crates/lsp/`,
  `app/src/settings/`, `app/src/workspace/state.rs`, `app/src/code/editor/`.
- Reuses patterns proven in the closed PRs:
  * `binary_in_path` PATH search with executable-bit check (warpdotdev#9562)
  * `warpui::r#async::FutureExt::with_timeout` for bounded init (warpdotdev#9562
    follow-up commit `31285c4`)
  * `std::env::join_paths` + `binary_filename` for cross-platform tests
    (warpdotdev#9562, warpdotdev#9568)
  * `initialization_options` plumbing for security-sensitive defaults
    (warpdotdev#9568, lessons from `handledSchemaProtocols`)
- Per-invariant test plan mapping each product.md item to a unit /
  integration test layer.
- ASCII flow diagram of settings-edit → chip-render → enable-click →
  spawn-with-timeout → route-traffic.
- Risks called out (crash loops, security of `initialization_options`,
  `Custom(String)` enum propagation, workspace-state migration).
@cla-bot cla-bot Bot added the cla-signed label May 1, 2026
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 1, 2026

@lonexreb

I'm starting a first review of this spec-only pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@github-actions github-actions Bot added the external-contributor Indicates that a PR has been opened by someone outside the Warp team. label May 1, 2026
Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This spec PR defines the product behavior and implementation plan for user-configurable language servers.

Concerns

  • The tech spec contradicts the product requirements by moving the settings UI needed for disable/re-enable behavior into follow-ups.
  • The implementation plan has feasibility gaps around crate boundaries, process timeout cleanup, and deriving LSP language IDs from file extensions.
  • The config model does not resolve conflicts when multiple custom servers match the same file type.

Security

  • Existing enabled workspaces would automatically execute a changed command after settings reload; require a fresh opt-in or invalidate enablement for command/arg changes.

Verdict

Found: 1 critical, 5 important, 1 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread specs/GH8803/tech.md
## Follow-ups (out of this spec)

- `nix flake check`-validated dev shell with all referenced LSP binaries pre-installed (would help testing).
- Settings UI: a "Custom language servers" page under Settings → Code intelligence that lists configured servers + workspace-enablement state with `Enable`/`Disable` buttons (currently described only in product.md user-experience section).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 [CRITICAL] Settings UI cannot be a follow-up because product.md requires disabling and re-enabling dismissed servers through settings (invariants 11 and 14); move this UI into Proposed changes/testing or drop those requirements from the product spec.

Comment thread specs/GH8803/tech.md

```rust
pub struct UserConfiguredLanguageServer {
config: UserLspServerConfig,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] UserLspServerConfig is defined in app/src/settings/lsp.rs, but this new crates/lsp module cannot depend on app code; specify a shared crate/type boundary or an app-to-lsp conversion layer.

Comment thread specs/GH8803/tech.md

**Option B — keep the enum closed and add a parallel `LspLanguageId` type that's either `Builtin(LanguageId)` or `Custom { extension: String, identifier: String }`.**

**Recommendation: Option A.** It's mechanical (one variant + two match arms) and avoids splitting the type system. The downside (more `Custom(...)` arms in match exhaustiveness) is acceptable.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] Returning the file extension as languageId is not sufficient for many servers (shshellscript, phtmlphp, etc.); add a configurable language_id/per-extension mapping or define how aliases are handled.

Comment thread specs/GH8803/tech.md
1. Compute diff of `[lsp.servers]` between old and new config (by `name`).
2. For added entries: nothing immediate; chip will appear on next matching file open.
3. For removed entries: shut down any running instance for that name, clear from `enabled_custom_servers` in all workspaces.
4. For changed entries (config differs but `name` matches): if currently running, restart with new config.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] [SECURITY] Auto-restarting an already-enabled server when command changes executes a new user-supplied binary without a fresh opt-in; require re-confirmation or invalidate enablement for command/arg changes.

Comment thread specs/GH8803/tech.md
- Wrap the `initialize` request in `warpui::r#async::FutureExt::with_timeout(Duration::from_millis(config.start_timeout_ms))`. Three outcomes (matches the pattern from `feat/9168-php-lsp-intelephense` commit `31285c4`):
- `Ok(Ok(_))` — server initialized; route LSP traffic.
- `Ok(Err(err))` — JSON-RPC error; surface notification with err message.
- `Err(timeout)` — kill child via `Drop`, surface timeout notification.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] Dropping a child handle is not a portable kill guarantee for std::process::Child; specify explicit kill/kill_on_drop plus wait/reap semantics for timeout cleanup.

Comment thread specs/GH8803/product.md

- **Auto-installation of user-configured servers.** The user installs the binary themselves (npm, cargo, system package manager, brew, etc.). Warp does not run package managers on the user's behalf for these.
- **Version pinning / auto-update of user-configured servers.** Out of scope; the user owns the lifecycle.
- **Per-file dynamic switching.** A single file is associated with at most one user-configured server, plus any built-in servers that already match. No "try server A, fall back to server B" runtime logic.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] The spec says a file has at most one user-configured server, but validation only enforces unique names and the tech flow renders a chip per matching entry; define whether duplicate file_types are rejected or how conflicts are resolved.

Comment thread specs/GH8803/tech.md

**File:** new `app/src/settings/lsp.rs`. Pattern matches `app/src/settings/input.rs` (`define_settings_group!` macro).

The group holds a single setting, `custom_servers`, of type `Vec<UserLspServerConfig>`. The struct:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 [SUGGESTION] The user-facing TOML is [[lsp.servers]], but the Rust setting is named custom_servers; call out the serde rename/schema mapping so implementers don't accidentally expose [[lsp.custom_servers]].

Comment thread specs/GH8803/product.md
- **Auto-installation of user-configured servers.** The user installs the binary themselves (npm, cargo, system package manager, brew, etc.). Warp does not run package managers on the user's behalf for these.
- **Version pinning / auto-update of user-configured servers.** Out of scope; the user owns the lifecycle.
- **Per-file dynamic switching.** A single file is associated with at most one user-configured server, plus any built-in servers that already match. No "try server A, fall back to server B" runtime logic.
- **Replacing built-in servers.** A user-configured server with the same `file_types` as a built-in does **not** disable the built-in. Both run; the LSP client merges their results.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User-configured server should overwrite built-in servers. It doesn't make sense to have both servers running at the same time

Comment thread specs/GH8803/product.md

### Disabling a server in a workspace

1. User opens settings → "Code intelligence" → "Custom language servers".
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Code intelligence" doesn't exist in our settings

To start, I also think it's probably fine to not have a dedicated custom language servers section in UI

Comment thread specs/GH8803/product.md

1. User opens settings → "Code intelligence" → "Custom language servers".
2. Each configured server lists which workspaces it is enabled for.
3. User clicks the workspace row's `Disable` button. Warp shuts down that server's process for that workspace and removes the per-workspace enablement record.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure we are following the existing settings > code > indexing and projects UI

Comment thread specs/GH8803/product.md

1. **Binary not on PATH:** When a user enables a server whose `command[0]` is not on PATH, Warp shows: *"Could not start `<name>`: binary `<cmd>` not found on PATH."* The chip's `Enable` button is replaced with `Open settings`.
2. **Binary on PATH but spawn fails:** Warp shows: *"`<name>` exited with status `<n>`. Last 200 bytes of stderr: `<...>`."* with `Open settings` and `Retry` buttons.
3. **Spawn hangs:** A configurable `start_timeout` (default 5s) bounds the LSP `initialize` request. On timeout, Warp shows: *"`<name>` did not respond to `initialize` within 5s."* The server's process is killed.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to do anything special for custom LSPs here

Comment thread specs/GH8803/product.md
command = ["intelephense", "--stdio"]
file_types = ["php", "phtml"]
root_files = ["composer.json", "composer.lock"] # optional; default: file's parent dir
initialization_options = { storagePath = "/tmp/intelephense" } # optional, opaque to Warp
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have support for these configs yet. Fine to skip for V0

Comment thread specs/GH8803/product.md
name = "intelephense"
command = ["intelephense", "--stdio"]
file_types = ["php", "phtml"]
root_files = ["composer.json", "composer.lock"] # optional; default: file's parent dir
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use our current root repo detection logic

Comment thread specs/GH8803/product.md
file_types = ["php", "phtml"]
root_files = ["composer.json", "composer.lock"] # optional; default: file's parent dir
initialization_options = { storagePath = "/tmp/intelephense" } # optional, opaque to Warp
start_timeout_ms = 5000 # optional, default 5000
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can remove this

@kevinyang372
Copy link
Copy Markdown
Member

Thanks for writing this up! Left some comments on the product spec

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed external-contributor Indicates that a PR has been opened by someone outside the Warp team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants