Skip to content
Closed
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
54 changes: 33 additions & 21 deletions packages/opencode/src/lsp/lsp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event"
import * as Log from "@opencode-ai/core/util/log"
import * as LSPClient from "./client"
import path from "path"
import { pathToFileURL, fileURLToPath } from "url"
Expand All @@ -14,8 +15,6 @@ import { containsPath } from "@/project/instance-context"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { RuntimeFlags } from "@/effect/runtime-flags"

const log = Log.create({ service: "lsp" })

export const Event = {
Updated: EventV2.define({ type: "lsp.updated", schema: {} }),
}
Expand Down Expand Up @@ -101,7 +100,6 @@ const kinds = [
const filterExperimentalServers = (servers: Record<string, LSPServer.Info>, flags: RuntimeFlags.Info) => {
if (flags.experimentalLspTy) {
if (servers["pyright"]) {
log.info("LSP server pyright is disabled because OPENCODE_EXPERIMENTAL_LSP_TY is enabled")
delete servers["pyright"]
}
} else {
Expand All @@ -116,14 +114,15 @@ type LocInput = { file: string; line: number; character: number }
interface State {
clients: LSPClient.Info[]
servers: Record<string, LSPServer.Info>
broken: Set<string>
broken: Map<string, string>
spawning: Map<string, Promise<LSPClient.Info | undefined>>
}

export interface Interface {
readonly init: () => Effect.Effect<void>
readonly status: () => Effect.Effect<Status[]>
readonly hasClients: (file: string) => Effect.Effect<boolean>
readonly failureReason: (file: string) => Effect.Effect<string | undefined>
readonly touchFile: (input: string, diagnostics?: "document" | "full") => Effect.Effect<void>
readonly diagnostics: () => Effect.Effect<Record<string, LSPClient.Diagnostic[]>>
readonly hover: (input: LocInput) => Effect.Effect<any>
Expand Down Expand Up @@ -153,7 +152,7 @@ export const layer = Layer.effect(
const servers: Record<string, LSPServer.Info> = {}

if (!cfg.lsp) {
log.info("all LSPs are disabled")
yield* Effect.logInfo("all LSPs are disabled")
} else {
for (const server of Object.values(LSPServer)) {
servers[server.id] = server
Expand All @@ -165,7 +164,7 @@ export const layer = Layer.effect(
for (const [name, item] of Object.entries(cfg.lsp)) {
const existing = servers[name]
if (item.disabled) {
log.info(`LSP server ${name} is disabled`)
yield* Effect.logInfo(`LSP server ${name} is disabled`)
delete servers[name]
continue
}
Expand All @@ -185,7 +184,7 @@ export const layer = Layer.effect(
}
}

log.info("enabled LSP servers", {
yield* Effect.logInfo("enabled LSP servers", {
serverIds: Object.values(servers)
.map((server) => server.id)
.join(", "),
Expand All @@ -195,7 +194,7 @@ export const layer = Layer.effect(
const s: State = {
clients: [],
servers,
broken: new Set(),
broken: new Map(),
spawning: new Map(),
}

Expand All @@ -222,28 +221,24 @@ export const layer = Layer.effect(
const handle = await server
.spawn(root, ctx, flags)
.then((value) => {
if (!value) s.broken.add(key)
if (!value) s.broken.set(key, `LSP server '${server.id}' is not available`)
return value
})
.catch((err) => {
s.broken.add(key)
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
s.broken.set(key, err instanceof Error ? err.message : `Failed to spawn LSP server ${server.id}`)
return undefined
})

if (!handle) return undefined
log.info("spawned lsp server", { serverID: server.id, root })

const client = await LSPClient.create({
serverID: server.id,
server: handle,
root,
directory: ctx.directory,
instance: ctx,
}).catch(async (err) => {
s.broken.add(key)
}).catch(async () => {
s.broken.set(key, `Failed to initialize LSP client ${server.id}`)
await Process.stop(handle.process)
log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
return undefined
})

Expand Down Expand Up @@ -349,8 +344,24 @@ export const layer = Layer.effect(
})
})

const failureReason = Effect.fn("LSP.failureReason")(function* (file: string) {
const ctx = yield* InstanceState.context
const s = yield* InstanceState.get(state)
return yield* Effect.promise(async () => {
const extension = path.parse(file).ext || file
for (const server of Object.values(s.servers)) {
if (server.extensions.length && !server.extensions.includes(extension)) continue
const root = await server.root(file, ctx)
if (!root) continue
const reason = s.broken.get(root + server.id)
if (reason) return reason
}
return undefined
})
})

const touchFile = Effect.fn("LSP.touchFile")(function* (input: string, diagnostics?: "document" | "full") {
log.info("touching file", { file: input })
yield* Effect.logInfo("touching file", { file: input })
const clients = yield* getClients(input)
yield* Effect.promise(() =>
Promise.all(
Expand All @@ -365,9 +376,7 @@ export const layer = Layer.effect(
after,
})
}),
).catch((err) => {
log.error("failed to touch file", { err, file: input })
}),
).catch(() => {}),
)
})

Expand Down Expand Up @@ -491,6 +500,7 @@ export const layer = Layer.effect(
init,
status,
hasClients,
failureReason,
touchFile,
diagnostics,
hover,
Expand All @@ -514,4 +524,6 @@ export const defaultLayer = layer.pipe(

export * as Diagnostic from "./diagnostic"

export const node = LayerNode.make(layer, [Config.node, RuntimeFlags.node, FSUtil.node, EventV2Bridge.node])

export * as LSP from "./lsp"
Loading
Loading