Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
50 changes: 35 additions & 15 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -904,18 +904,21 @@ export namespace Config {
z.literal(false),
z.record(
z.string(),
z.union([
z.object({
disabled: z.literal(true),
}),
z.object({
command: z.array(z.string()),
extensions: z.array(z.string()).optional(),
disabled: z.boolean().optional(),
env: z.record(z.string(), z.string()).optional(),
initialization: z.record(z.string(), z.any()).optional(),
}),
]),
z.object({
command: z.array(z.string()).optional(),
extensions: z.array(z.string()).optional(),
disabled: z.boolean().optional(),
env: z.record(z.string(), z.string()).optional(),
initialization: z.record(z.string(), z.any()).optional(),
timeout: z
.number()
.int()
.positive()
.optional()
.describe(
"Timeout in milliseconds for this LSP server's initialization. Default is 45000 (45 seconds).",
),
}),
),
])
.optional()
Expand All @@ -927,12 +930,29 @@ export namespace Config {

return Object.entries(data).every(([id, config]) => {
if (config.disabled) return true
if (serverIds.has(id)) return true
return Boolean(config.extensions)

const hasCommand = Boolean(config.command)
const isBuiltIn = serverIds.has(id)

// Unknown servers must have command
if (!isBuiltIn && !hasCommand) return false

// Custom server with command must have extensions
if (hasCommand) {
return Boolean(config.extensions)
}

// Built-in server override does not allow these
if (config.env || config.initialization || config.extensions) {
return false
}

return true
})
},
{
error: "For custom LSP servers, 'extensions' array is required.",
error:
"Invalid LSP configuration. Built-in servers can only override 'timeout' and 'disabled'. Custom servers require 'command' and 'extensions'.",
},
),
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Log } from "../util/log"
import { LANGUAGE_EXTENSIONS } from "./language"
import z from "zod"
import type { LSPServer } from "./server"
import { LSP } from "./index"
import { NamedError } from "@opencode-ai/util/error"
import { withTimeout } from "../util/timeout"
import { Instance } from "../project/instance"
Expand Down Expand Up @@ -39,7 +40,7 @@ export namespace LSPClient {
),
}

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

Expand Down Expand Up @@ -113,7 +114,7 @@ export namespace LSPClient {
},
},
}),
45_000,
input.timeout ?? LSP.DEFAULT_LSP_TIMEOUT,
).catch((err) => {
l.error("initialize error", { error: err })
throw new InitializeError(
Expand Down
33 changes: 32 additions & 1 deletion packages/opencode/src/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { Flag } from "@/flag/flag"
export namespace LSP {
const log = Log.create({ service: "lsp" })

export const DEFAULT_LSP_TIMEOUT = 45_000

export const Event = {
Updated: BusEvent.define("lsp.updated", z.object({})),
}
Expand Down Expand Up @@ -99,20 +101,39 @@ export namespace LSP {
filterExperimentalServers(servers)

for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
if (typeof item !== "object") continue
const existing = servers[name]
if (item.disabled) {
log.info(`LSP server ${name} is disabled`)
delete servers[name]
continue
}

if (!item.command) {
// No command = built-in server override
if (!existing) {
log.error(`LSP server ${name} not found. Custom servers require 'command' array.`)
continue
}

// Apply overrides to built-in server
servers[name] = {
...existing,
...(item.timeout !== undefined && { timeout: item.timeout }),
}
continue
}

const command = item.command
servers[name] = {
...existing,
id: name,
root: existing?.root ?? (async () => Instance.directory),
extensions: item.extensions ?? existing?.extensions ?? [],
timeout: item.timeout ?? existing?.timeout,
spawn: async (root) => {
return {
process: spawn(item.command[0], item.command.slice(1), {
process: spawn(command[0], command.slice(1), {
cwd: root,
env: {
...process.env,
Expand Down Expand Up @@ -195,10 +216,20 @@ export namespace LSP {
if (!handle) return undefined
log.info("spawned lsp server", { serverID: server.id })

const timeout = server.timeout ?? DEFAULT_LSP_TIMEOUT
if (server.timeout !== undefined) {
log.info("LSP server timeout overridden", {
serverID: server.id,
timeout: server.timeout,
default: DEFAULT_LSP_TIMEOUT,
})
}

const client = await LSPClient.create({
serverID: server.id,
server: handle,
root,
timeout,
}).catch((err) => {
s.broken.add(key)
handle.process.kill()
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/lsp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export namespace LSPServer {
global?: boolean
root: RootFunction
spawn(root: string): Promise<Handle | undefined>
timeout?: number
}

export const Deno: Info = {
Expand Down
Loading