[PR #13306] feat(lsp): non-blocking LSP initialization to prevent timeout on large projects #14607

Closed
opened 2026-02-16 18:19:23 -05:00 by yindo · 0 comments
Owner

Original Pull Request: https://github.com/anomalyco/opencode/pull/13306

State: closed
Merged: No


Problem

On large projects (e.g., Android monorepos with kotlin-lsp), getClients() blocks for 45+ seconds while waiting for LSP servers to initialize. This causes timeout errors before any language features become available, making the editor unusable during startup.

Fixes #7477
Related: #13328

Solution

1. Non-blocking getClients()

Make getClients() synchronous and non-blocking by returning immediately with an empty array when LSP state isn't ready, then spawning servers in the background.

How it works

  1. getClients() is now synchronous — returns LSPClient.Info[] directly instead of Promise<LSPClient.Info[]>
  2. Early return pattern — if cachedState isn't populated yet, kicks off background initialization via void state().then(...) and returns []
  3. Fire-and-forget server spawningserver.root() resolution and schedule() run in detached promises instead of blocking the call
  4. Event-driven refreshBus.publish(Event.Updated, {}) fires when a client becomes ready, so the editor picks up new language servers as they come online

Key changes in packages/opencode/src/lsp/index.ts

Before After
async function getClients(file) blocks on await state() and await server.root() function getClients(file) returns synchronously
Callers wait for all servers to be ready Callers get whatever is ready now, new servers appear via bus events
45s+ blocking on large projects Instant return, progressive server availability

2. Configurable diagnostics timeout

Add OPENCODE_EXPERIMENTAL_LSP_DIAGNOSTICS_TIMEOUT_MS flag to make the 3s diagnostics timeout configurable (default raised to 10s).

  • New flag in flag.ts using existing number() helper
  • client.ts reads Flag.OPENCODE_EXPERIMENTAL_LSP_DIAGNOSTICS_TIMEOUT_MS ?? 10_000 instead of hardcoded 3000
  • 3 tests covering default, custom, and invalid flag values

Why not just increase the timeout? (vs PR #6997)

PR #6997 adds a configurable timeout, which treats the symptom but not the cause. The fundamental issue is that getClients() is a blocking call in a hot path — every touchFile() and run() call waits for full LSP initialization. Even with a longer timeout, users still experience a frozen editor during startup.

This PR eliminates the blocking entirely and makes the diagnostics timeout configurable for users who need more time for heavy LSP servers.

Testing

  • 6 tests for non-blocking getClients():
    • Returns immediately during initialization (no blocking)
    • Returns [] when state is not cached
    • Returns ready clients after initialization completes
    • Bus.publish(Event.Updated) fires when a client becomes ready
    • Broken set prevents retries of failed servers
    • Spawning map prevents duplicate spawn attempts
  • 3 tests for configurable diagnostics timeout:
    • Defaults to 10_000ms when flag is undefined
    • Uses custom timeout when flag is set
    • Falls back to 10_000ms when flag value is invalid
  • Full test suite: 935 pass, 5 skip, 0 fail
  • Typecheck: All 12 packages pass

Files changed

  • packages/opencode/src/lsp/index.ts — non-blocking getClients() implementation
  • packages/opencode/src/lsp/index.test.ts — 6 tests for non-blocking behavior
  • packages/opencode/src/flag/flag.tsOPENCODE_EXPERIMENTAL_LSP_DIAGNOSTICS_TIMEOUT_MS flag
  • packages/opencode/src/lsp/client.ts — uses configurable timeout (line 232)
  • packages/opencode/src/lsp/client.test.ts — 3 tests for timeout configuration
**Original Pull Request:** https://github.com/anomalyco/opencode/pull/13306 **State:** closed **Merged:** No --- ## Problem On large projects (e.g., Android monorepos with `kotlin-lsp`), `getClients()` blocks for 45+ seconds while waiting for LSP servers to initialize. This causes timeout errors before any language features become available, making the editor unusable during startup. Fixes #7477 Related: #13328 ## Solution ### 1. Non-blocking `getClients()` Make `getClients()` **synchronous and non-blocking** by returning immediately with an empty array when LSP state isn't ready, then spawning servers in the background. #### How it works 1. **`getClients()` is now synchronous** — returns `LSPClient.Info[]` directly instead of `Promise<LSPClient.Info[]>` 2. **Early return pattern** — if `cachedState` isn't populated yet, kicks off background initialization via `void state().then(...)` and returns `[]` 3. **Fire-and-forget server spawning** — `server.root()` resolution and `schedule()` run in detached promises instead of blocking the call 4. **Event-driven refresh** — `Bus.publish(Event.Updated, {})` fires when a client becomes ready, so the editor picks up new language servers as they come online #### Key changes in `packages/opencode/src/lsp/index.ts` | Before | After | |--------|-------| | `async function getClients(file)` blocks on `await state()` and `await server.root()` | `function getClients(file)` returns synchronously | | Callers wait for all servers to be ready | Callers get whatever is ready *now*, new servers appear via bus events | | 45s+ blocking on large projects | Instant return, progressive server availability | ### 2. Configurable diagnostics timeout Add `OPENCODE_EXPERIMENTAL_LSP_DIAGNOSTICS_TIMEOUT_MS` flag to make the 3s diagnostics timeout configurable (default raised to 10s). - New flag in `flag.ts` using existing `number()` helper - `client.ts` reads `Flag.OPENCODE_EXPERIMENTAL_LSP_DIAGNOSTICS_TIMEOUT_MS ?? 10_000` instead of hardcoded `3000` - 3 tests covering default, custom, and invalid flag values ### Why not just increase the timeout? (vs PR #6997) PR #6997 adds a configurable timeout, which treats the symptom but not the cause. The fundamental issue is that `getClients()` is a blocking call in a hot path — every `touchFile()` and `run()` call waits for full LSP initialization. Even with a longer timeout, users still experience a frozen editor during startup. This PR eliminates the blocking entirely **and** makes the diagnostics timeout configurable for users who need more time for heavy LSP servers. ## Testing - **6 tests** for non-blocking `getClients()`: - Returns immediately during initialization (no blocking) - Returns `[]` when state is not cached - Returns ready clients after initialization completes - `Bus.publish(Event.Updated)` fires when a client becomes ready - Broken set prevents retries of failed servers - Spawning map prevents duplicate spawn attempts - **3 tests** for configurable diagnostics timeout: - Defaults to 10_000ms when flag is undefined - Uses custom timeout when flag is set - Falls back to 10_000ms when flag value is invalid - **Full test suite**: 935 pass, 5 skip, 0 fail - **Typecheck**: All 12 packages pass ## Files changed - `packages/opencode/src/lsp/index.ts` — non-blocking `getClients()` implementation - `packages/opencode/src/lsp/index.test.ts` — 6 tests for non-blocking behavior - `packages/opencode/src/flag/flag.ts` — `OPENCODE_EXPERIMENTAL_LSP_DIAGNOSTICS_TIMEOUT_MS` flag - `packages/opencode/src/lsp/client.ts` — uses configurable timeout (line 232) - `packages/opencode/src/lsp/client.test.ts` — 3 tests for timeout configuration
yindo added the pull-request label 2026-02-16 18:19:23 -05:00
yindo closed this issue 2026-02-16 18:19:23 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#14607