Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,24 @@ export namespace Config {
disabled: z.boolean().optional(),
env: z.record(z.string(), z.string()).optional(),
initialization: z.record(z.string(), z.any()).optional(),
timeout: z
.object({
startup: z
.number()
.int()
.positive()
.optional()
.describe("Timeout in milliseconds for the LSP initialize request"),
diagnostics: z
.number()
.int()
.min(151)
.optional()
.describe(
"Timeout in milliseconds to wait for LSP diagnostics after opening a file. Minimum 151ms.",
),
})
.optional(),
}),
]),
),
Expand Down
8 changes: 6 additions & 2 deletions packages/opencode/src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { Instance } from "../project/instance"
import { Filesystem } from "../util/filesystem"

const DIAGNOSTICS_DEBOUNCE_MS = 150
const STARTUP_TIMEOUT_MS = 45_000
const DIAGNOSTICS_TIMEOUT_MS = 3_000

export namespace LSPClient {
const log = Log.create({ service: "lsp.client" })
Expand Down Expand Up @@ -42,6 +44,8 @@ export namespace LSPClient {

export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
const l = log.clone().tag("serverID", input.serverID)
const startup_timeout = input.server.timeout?.startup ?? STARTUP_TIMEOUT_MS
const diagnostics_timeout = input.server.timeout?.diagnostics ?? DIAGNOSTICS_TIMEOUT_MS
l.info("starting client")

const connection = createMessageConnection(
Expand Down Expand Up @@ -114,7 +118,7 @@ export namespace LSPClient {
},
},
}),
45_000,
startup_timeout,
).catch((err) => {
l.error("initialize error", { error: err })
throw new InitializeError(
Expand Down Expand Up @@ -228,7 +232,7 @@ export namespace LSPClient {
}
})
}),
3000,
diagnostics_timeout,
)
.catch(() => {})
.finally(() => {
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ export namespace LSP {
env: { ...process.env, ...item.env },
}),
initialization: item.initialization,
timeout: item.timeout,
}),
}
}
Expand Down
6 changes: 6 additions & 0 deletions packages/opencode/src/lsp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export namespace LSPServer {
export interface Handle {
process: ChildProcessWithoutNullStreams
initialization?: Record<string, any>
timeout?: Timeout
}

export interface Timeout {
startup?: number
diagnostics?: number
}

type RootFunction = (file: string) => Promise<string | undefined>
Expand Down
86 changes: 85 additions & 1 deletion packages/opencode/test/lsp/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { describe, expect, test, beforeEach } from "bun:test"
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
import path from "path"
import { LSPClient } from "../../src/lsp/client"
import { LSPServer } from "../../src/lsp/server"
import { Instance } from "../../src/project/instance"
import { Log } from "../../src/util/log"
import * as Timeout from "../../src/util/timeout"

// Minimal fake LSP server that speaks JSON-RPC over stdio
function spawnFakeServer() {
Expand All @@ -21,6 +22,10 @@ describe("LSPClient interop", () => {
await Log.init({ print: true })
})

afterEach(() => {
mock.restore()
})

test("handles workspace/workspaceFolders request", async () => {
const handle = spawnFakeServer() as any

Expand Down Expand Up @@ -92,4 +97,83 @@ describe("LSPClient interop", () => {

await client.shutdown()
})

test("uses default startup timeout and respects override", async () => {
const seen: number[] = []
spyOn(Timeout, "withTimeout").mockImplementation(async (input, ms) => {
seen.push(ms)
return input
})

const base = spawnFakeServer() as unknown as LSPServer.Handle
const client = await Instance.provide({
directory: process.cwd(),
fn: () =>
LSPClient.create({
serverID: "fake",
server: base,
root: process.cwd(),
}),
})
expect(seen[0]).toBe(45_000)
await client.shutdown()

const next = spawnFakeServer() as unknown as LSPServer.Handle
const custom = await Instance.provide({
directory: process.cwd(),
fn: () =>
LSPClient.create({
serverID: "fake",
server: {
...next,
timeout: { startup: 12_345 },
},
root: process.cwd(),
}),
})
expect(seen[1]).toBe(12_345)
await custom.shutdown()
})

test("uses default diagnostics timeout and respects override", async () => {
const seen: number[] = []
spyOn(Timeout, "withTimeout").mockImplementation(async (input, ms) => {
seen.push(ms)
if (ms === 45_000) return input
throw new Error("timeout")
})

const base = spawnFakeServer() as unknown as LSPServer.Handle
await Instance.provide({
directory: process.cwd(),
fn: async () => {
const client = await LSPClient.create({
serverID: "fake",
server: base,
root: process.cwd(),
})
await client.waitForDiagnostics({ path: __filename })
expect(seen[1]).toBe(3_000)
await client.shutdown()
},
})

const next = spawnFakeServer() as unknown as LSPServer.Handle
await Instance.provide({
directory: process.cwd(),
fn: async () => {
const custom = await LSPClient.create({
serverID: "fake",
server: {
...next,
timeout: { diagnostics: 9_876 },
},
root: process.cwd(),
})
await custom.waitForDiagnostics({ path: __filename })
expect(seen[3]).toBe(9_876)
await custom.shutdown()
},
})
})
})
27 changes: 27 additions & 0 deletions packages/web/src/content/docs/ko/lsp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ OpenCode config의 `lsp` 섹션에서 LSP 서버를 사용자 정의할 수 있
| `extensions` | string[] | 이 LSP 서버가 처리할 파일 확장자입니다 |
| `env` | object | 서버 시작 시 설정할 환경 변수입니다 |
| `initialization` | object | LSP 서버로 전송할 초기화 옵션입니다 |
| `timeout` | object | 설정된 LSP 엔트리의 timeout 설정입니다 |

예시를 살펴보겠습니다.

Expand Down Expand Up @@ -133,6 +134,32 @@ OpenCode config의 `lsp` 섹션에서 LSP 서버를 사용자 정의할 수 있

---

### Timeout

`timeout` 속성을 사용하면 설정된 LSP 엔트리의 initialize 요청 또는 diagnostics 대기 시간을 조정할 수 있습니다.

```json title="opencode.json" {5-10}
{
"$schema": "https://opencode.ai/config.json",
"lsp": {
"my-kotlin": {
"command": ["kotlin-lsp"],
"extensions": [".kt", ".kts"],
"timeout": {
"startup": 120000,
"diagnostics": 15000
}
}
}
}
```

- `timeout`은 `command`가 있는 설정된 LSP 엔트리에서 사용할 수 있습니다.
- `timeout.startup`은 LSP `initialize` 요청 timeout이며 기본값은 `45000`입니다.
- `timeout.diagnostics` 기본값은 `3000`이며 최소값은 `151`입니다.

---

### LSP 서버 비활성화

전역에서 **모든** LSP 서버를 비활성화하려면 `lsp`를 `false`로 설정하세요.
Expand Down
27 changes: 27 additions & 0 deletions packages/web/src/content/docs/lsp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Each LSP server supports the following:
| `extensions` | string[] | File extensions this LSP server should handle |
| `env` | object | Environment variables to set when starting server |
| `initialization` | object | Initialization options to send to the LSP server |
| `timeout` | object | Timeout overrides for configured LSP entries |

Let's look at some examples.

Expand Down Expand Up @@ -133,6 +134,32 @@ Initialization options vary by LSP server. Check your LSP server's documentation

---

### Timeouts

Use the `timeout` property to override initialize or diagnostics waits for a configured LSP entry:

```json title="opencode.json" {5-10}
{
"$schema": "https://opencode.ai/config.json",
"lsp": {
"my-kotlin": {
"command": ["kotlin-lsp"],
"extensions": [".kt", ".kts"],
"timeout": {
"startup": 120000,
"diagnostics": 15000
}
}
}
}
```

- `timeout` is available on configured LSP entries that define a `command`
- `timeout.startup` controls the LSP `initialize` request timeout and defaults to `45000`
- `timeout.diagnostics` defaults to `3000` and must be at least `151`

---

### Disabling LSP servers

To disable **all** LSP servers globally, set `lsp` to `false`:
Expand Down
Loading