Skip to content

Commit 2f8d58a

Browse files
committed
feat: Add configurable lsp timeout
1 parent 2ca0ae7 commit 2f8d58a

File tree

5 files changed

+315
-18
lines changed

5 files changed

+315
-18
lines changed

packages/opencode/src/config/config.ts

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -876,21 +876,39 @@ export namespace Config {
876876
lsp: z
877877
.union([
878878
z.literal(false),
879-
z.record(
880-
z.string(),
881-
z.union([
882-
z.object({
883-
disabled: z.literal(true),
884-
}),
885-
z.object({
886-
command: z.array(z.string()),
887-
extensions: z.array(z.string()).optional(),
888-
disabled: z.boolean().optional(),
889-
env: z.record(z.string(), z.string()).optional(),
890-
initialization: z.record(z.string(), z.any()).optional(),
891-
}),
892-
]),
893-
),
879+
z
880+
.object({
881+
timeout: z
882+
.number()
883+
.int()
884+
.positive()
885+
.optional()
886+
.describe(
887+
"Global timeout in milliseconds for LSP server initialization. Default is 45000 (45 seconds).",
888+
),
889+
})
890+
.catchall(
891+
z.union([
892+
z.object({
893+
disabled: z.literal(true),
894+
}),
895+
z.object({
896+
command: z.array(z.string()).optional(),
897+
extensions: z.array(z.string()).optional(),
898+
disabled: z.boolean().optional(),
899+
env: z.record(z.string(), z.string()).optional(),
900+
initialization: z.record(z.string(), z.any()).optional(),
901+
timeout: z
902+
.number()
903+
.int()
904+
.positive()
905+
.optional()
906+
.describe(
907+
"Timeout in milliseconds for this LSP server's initialization. Overrides global lsp.timeout.",
908+
),
909+
}),
910+
]),
911+
),
894912
])
895913
.optional()
896914
.refine(
@@ -900,6 +918,8 @@ export namespace Config {
900918
const serverIds = new Set(Object.values(LSPServer).map((s) => s.id))
901919

902920
return Object.entries(data).every(([id, config]) => {
921+
if (id === "timeout") return true
922+
if (typeof config !== "object") return false
903923
if (config.disabled) return true
904924
if (serverIds.has(id)) return true
905925
return Boolean(config.extensions)

packages/opencode/src/lsp/client.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Log } from "../util/log"
88
import { LANGUAGE_EXTENSIONS } from "./language"
99
import z from "zod"
1010
import type { LSPServer } from "./server"
11+
import { LSP } from "./index"
1112
import { NamedError } from "@opencode-ai/util/error"
1213
import { withTimeout } from "../util/timeout"
1314
import { Instance } from "../project/instance"
@@ -39,7 +40,7 @@ export namespace LSPClient {
3940
),
4041
}
4142

42-
export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
43+
export async function create(input: { serverID: string; server: LSPServer.Handle; root: string; timeout?: number }) {
4344
const l = log.clone().tag("serverID", input.serverID)
4445
l.info("starting client")
4546

@@ -113,7 +114,7 @@ export namespace LSPClient {
113114
},
114115
},
115116
}),
116-
45_000,
117+
input.timeout ?? LSP.DEFAULT_LSP_TIMEOUT,
117118
).catch((err) => {
118119
l.error("initialize error", { error: err })
119120
throw new InitializeError(

packages/opencode/src/lsp/index.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { Flag } from "@/flag/flag"
1414
export namespace LSP {
1515
const log = Log.create({ service: "lsp" })
1616

17+
export const DEFAULT_LSP_TIMEOUT = 45_000
18+
1719
export const Event = {
1820
Updated: BusEvent.define("lsp.updated", z.object({})),
1921
}
@@ -89,30 +91,57 @@ export namespace LSP {
8991
servers,
9092
clients,
9193
spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
94+
globalTimeout: DEFAULT_LSP_TIMEOUT,
9295
}
9396
}
9497

98+
const globalTimeout = typeof cfg.lsp === "object" && cfg.lsp?.timeout ? cfg.lsp.timeout : DEFAULT_LSP_TIMEOUT
99+
if (globalTimeout !== DEFAULT_LSP_TIMEOUT) {
100+
log.info("LSP timeout overridden", { timeout: globalTimeout, default: DEFAULT_LSP_TIMEOUT })
101+
}
102+
95103
for (const server of Object.values(LSPServer)) {
96104
servers[server.id] = server
97105
}
98106

99107
filterExperimentalServers(servers)
100108

101109
for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
110+
// Skip 'timeout' key since it's a global config option, not a server definition
111+
if (name === "timeout") continue
112+
if (typeof item !== "object") continue
102113
const existing = servers[name]
103114
if (item.disabled) {
104115
log.info(`LSP server ${name} is disabled`)
105116
delete servers[name]
106117
continue
107118
}
119+
120+
if (!item.command) {
121+
// No command = built-in server override
122+
if (!existing) {
123+
log.error(`LSP server ${name} not found. Custom servers require 'command' array.`)
124+
continue
125+
}
126+
127+
// Apply overrides to built-in server
128+
servers[name] = {
129+
...existing,
130+
...(item.timeout !== undefined && { timeout: item.timeout }),
131+
}
132+
continue
133+
}
134+
135+
const command = item.command
108136
servers[name] = {
109137
...existing,
110138
id: name,
111139
root: existing?.root ?? (async () => Instance.directory),
112140
extensions: item.extensions ?? existing?.extensions ?? [],
141+
timeout: item.timeout ?? existing?.timeout,
113142
spawn: async (root) => {
114143
return {
115-
process: spawn(item.command[0], item.command.slice(1), {
144+
process: spawn(command[0], command.slice(1), {
116145
cwd: root,
117146
env: {
118147
...process.env,
@@ -136,6 +165,7 @@ export namespace LSP {
136165
servers,
137166
clients,
138167
spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
168+
globalTimeout,
139169
}
140170
},
141171
async (state) => {
@@ -195,10 +225,20 @@ export namespace LSP {
195225
if (!handle) return undefined
196226
log.info("spawned lsp server", { serverID: server.id })
197227

228+
const timeout = server.timeout ?? s.globalTimeout
229+
if (server.timeout !== undefined) {
230+
log.info("LSP server timeout overridden", {
231+
serverID: server.id,
232+
timeout: server.timeout,
233+
globalTimeout: s.globalTimeout,
234+
})
235+
}
236+
198237
const client = await LSPClient.create({
199238
serverID: server.id,
200239
server: handle,
201240
root,
241+
timeout,
202242
}).catch((err) => {
203243
s.broken.add(key)
204244
handle.process.kill()

packages/opencode/src/lsp/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export namespace LSPServer {
5151
global?: boolean
5252
root: RootFunction
5353
spawn(root: string): Promise<Handle | undefined>
54+
timeout?: number
5455
}
5556

5657
export const Deno: Info = {

0 commit comments

Comments
 (0)