Sfoglia il codice sorgente

feat: unwrap lsp namespaces to flat exports + barrel (#22748)

Kit Langton 2 giorni fa
parent
commit
509bc11f81

+ 1 - 1
packages/opencode/src/config/config.ts

@@ -19,7 +19,7 @@ import {
   printParseErrorCode,
 } from "jsonc-parser"
 import { Instance, type InstanceContext } from "../project/instance"
-import { LSPServer } from "../lsp/server"
+import { LSPServer } from "../lsp"
 import { Installation } from "@/installation"
 import { ConfigMarkdown } from "."
 import { existsSync } from "fs"

+ 195 - 197
packages/opencode/src/lsp/client.ts

@@ -8,7 +8,7 @@ import { Log } from "../util"
 import { Process } from "../util"
 import { LANGUAGE_EXTENSIONS } from "./language"
 import z from "zod"
-import type { LSPServer } from "./server"
+import type { LSPServer } from "."
 import { NamedError } from "@opencode-ai/shared/util/error"
 import { withTimeout } from "../util/timeout"
 import { Instance } from "../project/instance"
@@ -16,237 +16,235 @@ import { Filesystem } from "../util"
 
 const DIAGNOSTICS_DEBOUNCE_MS = 150
 
-export namespace LSPClient {
-  const log = Log.create({ service: "lsp.client" })
+const log = Log.create({ service: "lsp.client" })
 
-  export type Info = NonNullable<Awaited<ReturnType<typeof create>>>
+export type Info = NonNullable<Awaited<ReturnType<typeof create>>>
 
-  export type Diagnostic = VSCodeDiagnostic
+export type Diagnostic = VSCodeDiagnostic
 
-  export const InitializeError = NamedError.create(
-    "LSPInitializeError",
+export const InitializeError = NamedError.create(
+  "LSPInitializeError",
+  z.object({
+    serverID: z.string(),
+  }),
+)
+
+export const Event = {
+  Diagnostics: BusEvent.define(
+    "lsp.client.diagnostics",
     z.object({
       serverID: z.string(),
+      path: z.string(),
     }),
-  )
-
-  export const Event = {
-    Diagnostics: BusEvent.define(
-      "lsp.client.diagnostics",
-      z.object({
-        serverID: z.string(),
-        path: z.string(),
-      }),
-    ),
-  }
+  ),
+}
 
-  export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
-    const l = log.clone().tag("serverID", input.serverID)
-    l.info("starting client")
+export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
+  const l = log.clone().tag("serverID", input.serverID)
+  l.info("starting client")
 
-    const connection = createMessageConnection(
-      new StreamMessageReader(input.server.process.stdout as any),
-      new StreamMessageWriter(input.server.process.stdin as any),
-    )
+  const connection = createMessageConnection(
+    new StreamMessageReader(input.server.process.stdout as any),
+    new StreamMessageWriter(input.server.process.stdin as any),
+  )
 
-    const diagnostics = new Map<string, Diagnostic[]>()
-    connection.onNotification("textDocument/publishDiagnostics", (params) => {
-      const filePath = Filesystem.normalizePath(fileURLToPath(params.uri))
-      l.info("textDocument/publishDiagnostics", {
-        path: filePath,
-        count: params.diagnostics.length,
-      })
-      const exists = diagnostics.has(filePath)
-      diagnostics.set(filePath, params.diagnostics)
-      if (!exists && input.serverID === "typescript") return
-      void Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID })
+  const diagnostics = new Map<string, Diagnostic[]>()
+  connection.onNotification("textDocument/publishDiagnostics", (params) => {
+    const filePath = Filesystem.normalizePath(fileURLToPath(params.uri))
+    l.info("textDocument/publishDiagnostics", {
+      path: filePath,
+      count: params.diagnostics.length,
     })
-    connection.onRequest("window/workDoneProgress/create", (params) => {
-      l.info("window/workDoneProgress/create", params)
-      return null
-    })
-    connection.onRequest("workspace/configuration", async () => {
-      // Return server initialization options
-      return [input.server.initialization ?? {}]
-    })
-    connection.onRequest("client/registerCapability", async () => {})
-    connection.onRequest("client/unregisterCapability", async () => {})
-    connection.onRequest("workspace/workspaceFolders", async () => [
-      {
-        name: "workspace",
-        uri: pathToFileURL(input.root).href,
+    const exists = diagnostics.has(filePath)
+    diagnostics.set(filePath, params.diagnostics)
+    if (!exists && input.serverID === "typescript") return
+    Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID })
+  })
+  connection.onRequest("window/workDoneProgress/create", (params) => {
+    l.info("window/workDoneProgress/create", params)
+    return null
+  })
+  connection.onRequest("workspace/configuration", async () => {
+    // Return server initialization options
+    return [input.server.initialization ?? {}]
+  })
+  connection.onRequest("client/registerCapability", async () => {})
+  connection.onRequest("client/unregisterCapability", async () => {})
+  connection.onRequest("workspace/workspaceFolders", async () => [
+    {
+      name: "workspace",
+      uri: pathToFileURL(input.root).href,
+    },
+  ])
+  connection.listen()
+
+  l.info("sending initialize")
+  await withTimeout(
+    connection.sendRequest("initialize", {
+      rootUri: pathToFileURL(input.root).href,
+      processId: input.server.process.pid,
+      workspaceFolders: [
+        {
+          name: "workspace",
+          uri: pathToFileURL(input.root).href,
+        },
+      ],
+      initializationOptions: {
+        ...input.server.initialization,
       },
-    ])
-    connection.listen()
-
-    l.info("sending initialize")
-    await withTimeout(
-      connection.sendRequest("initialize", {
-        rootUri: pathToFileURL(input.root).href,
-        processId: input.server.process.pid,
-        workspaceFolders: [
-          {
-            name: "workspace",
-            uri: pathToFileURL(input.root).href,
-          },
-        ],
-        initializationOptions: {
-          ...input.server.initialization,
+      capabilities: {
+        window: {
+          workDoneProgress: true,
         },
-        capabilities: {
-          window: {
-            workDoneProgress: true,
+        workspace: {
+          configuration: true,
+          didChangeWatchedFiles: {
+            dynamicRegistration: true,
           },
-          workspace: {
-            configuration: true,
-            didChangeWatchedFiles: {
-              dynamicRegistration: true,
-            },
+        },
+        textDocument: {
+          synchronization: {
+            didOpen: true,
+            didChange: true,
           },
-          textDocument: {
-            synchronization: {
-              didOpen: true,
-              didChange: true,
-            },
-            publishDiagnostics: {
-              versionSupport: true,
-            },
+          publishDiagnostics: {
+            versionSupport: true,
           },
         },
-      }),
-      45_000,
-    ).catch((err) => {
-      l.error("initialize error", { error: err })
-      throw new InitializeError(
-        { serverID: input.serverID },
-        {
-          cause: err,
-        },
-      )
-    })
-
-    await connection.sendNotification("initialized", {})
-
-    if (input.server.initialization) {
-      await connection.sendNotification("workspace/didChangeConfiguration", {
-        settings: input.server.initialization,
-      })
-    }
-
-    const files: {
-      [path: string]: number
-    } = {}
-
-    const result = {
-      root: input.root,
-      get serverID() {
-        return input.serverID
       },
-      get connection() {
-        return connection
+    }),
+    45_000,
+  ).catch((err) => {
+    l.error("initialize error", { error: err })
+    throw new InitializeError(
+      { serverID: input.serverID },
+      {
+        cause: err,
       },
-      notify: {
-        async open(input: { path: string }) {
-          input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
-          const text = await Filesystem.readText(input.path)
-          const extension = path.extname(input.path)
-          const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
-
-          const version = files[input.path]
-          if (version !== undefined) {
-            log.info("workspace/didChangeWatchedFiles", input)
-            await connection.sendNotification("workspace/didChangeWatchedFiles", {
-              changes: [
-                {
-                  uri: pathToFileURL(input.path).href,
-                  type: 2, // Changed
-                },
-              ],
-            })
-
-            const next = version + 1
-            files[input.path] = next
-            log.info("textDocument/didChange", {
-              path: input.path,
-              version: next,
-            })
-            await connection.sendNotification("textDocument/didChange", {
-              textDocument: {
-                uri: pathToFileURL(input.path).href,
-                version: next,
-              },
-              contentChanges: [{ text }],
-            })
-            return
-          }
+    )
+  })
+
+  await connection.sendNotification("initialized", {})
 
+  if (input.server.initialization) {
+    await connection.sendNotification("workspace/didChangeConfiguration", {
+      settings: input.server.initialization,
+    })
+  }
+
+  const files: {
+    [path: string]: number
+  } = {}
+
+  const result = {
+    root: input.root,
+    get serverID() {
+      return input.serverID
+    },
+    get connection() {
+      return connection
+    },
+    notify: {
+      async open(input: { path: string }) {
+        input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
+        const text = await Filesystem.readText(input.path)
+        const extension = path.extname(input.path)
+        const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
+
+        const version = files[input.path]
+        if (version !== undefined) {
           log.info("workspace/didChangeWatchedFiles", input)
           await connection.sendNotification("workspace/didChangeWatchedFiles", {
             changes: [
               {
                 uri: pathToFileURL(input.path).href,
-                type: 1, // Created
+                type: 2, // Changed
               },
             ],
           })
 
-          log.info("textDocument/didOpen", input)
-          diagnostics.delete(input.path)
-          await connection.sendNotification("textDocument/didOpen", {
+          const next = version + 1
+          files[input.path] = next
+          log.info("textDocument/didChange", {
+            path: input.path,
+            version: next,
+          })
+          await connection.sendNotification("textDocument/didChange", {
             textDocument: {
               uri: pathToFileURL(input.path).href,
-              languageId,
-              version: 0,
-              text,
+              version: next,
             },
+            contentChanges: [{ text }],
           })
-          files[input.path] = 0
           return
-        },
-      },
-      get diagnostics() {
-        return diagnostics
+        }
+
+        log.info("workspace/didChangeWatchedFiles", input)
+        await connection.sendNotification("workspace/didChangeWatchedFiles", {
+          changes: [
+            {
+              uri: pathToFileURL(input.path).href,
+              type: 1, // Created
+            },
+          ],
+        })
+
+        log.info("textDocument/didOpen", input)
+        diagnostics.delete(input.path)
+        await connection.sendNotification("textDocument/didOpen", {
+          textDocument: {
+            uri: pathToFileURL(input.path).href,
+            languageId,
+            version: 0,
+            text,
+          },
+        })
+        files[input.path] = 0
+        return
       },
-      async waitForDiagnostics(input: { path: string }) {
-        const normalizedPath = Filesystem.normalizePath(
-          path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path),
-        )
-        log.info("waiting for diagnostics", { path: normalizedPath })
-        let unsub: () => void
-        let debounceTimer: ReturnType<typeof setTimeout> | undefined
-        return await withTimeout(
-          new Promise<void>((resolve) => {
-            unsub = Bus.subscribe(Event.Diagnostics, (event) => {
-              if (event.properties.path === normalizedPath && event.properties.serverID === result.serverID) {
-                // Debounce to allow LSP to send follow-up diagnostics (e.g., semantic after syntax)
-                if (debounceTimer) clearTimeout(debounceTimer)
-                debounceTimer = setTimeout(() => {
-                  log.info("got diagnostics", { path: normalizedPath })
-                  unsub?.()
-                  resolve()
-                }, DIAGNOSTICS_DEBOUNCE_MS)
-              }
-            })
-          }),
-          3000,
-        )
-          .catch(() => {})
-          .finally(() => {
-            if (debounceTimer) clearTimeout(debounceTimer)
-            unsub?.()
+    },
+    get diagnostics() {
+      return diagnostics
+    },
+    async waitForDiagnostics(input: { path: string }) {
+      const normalizedPath = Filesystem.normalizePath(
+        path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path),
+      )
+      log.info("waiting for diagnostics", { path: normalizedPath })
+      let unsub: () => void
+      let debounceTimer: ReturnType<typeof setTimeout> | undefined
+      return await withTimeout(
+        new Promise<void>((resolve) => {
+          unsub = Bus.subscribe(Event.Diagnostics, (event) => {
+            if (event.properties.path === normalizedPath && event.properties.serverID === result.serverID) {
+              // Debounce to allow LSP to send follow-up diagnostics (e.g., semantic after syntax)
+              if (debounceTimer) clearTimeout(debounceTimer)
+              debounceTimer = setTimeout(() => {
+                log.info("got diagnostics", { path: normalizedPath })
+                unsub?.()
+                resolve()
+              }, DIAGNOSTICS_DEBOUNCE_MS)
+            }
           })
-      },
-      async shutdown() {
-        l.info("shutting down")
-        connection.end()
-        connection.dispose()
-        await Process.stop(input.server.process)
-        l.info("shutdown")
-      },
-    }
+        }),
+        3000,
+      )
+        .catch(() => {})
+        .finally(() => {
+          if (debounceTimer) clearTimeout(debounceTimer)
+          unsub?.()
+        })
+    },
+    async shutdown() {
+      l.info("shutting down")
+      connection.end()
+      connection.dispose()
+      await Process.stop(input.server.process)
+      l.info("shutdown")
+    },
+  }
 
-    l.info("initialized")
+  l.info("initialized")
 
-    return result
-  }
+  return result
 }

+ 3 - 537
packages/opencode/src/lsp/index.ts

@@ -1,537 +1,3 @@
-import { BusEvent } from "@/bus/bus-event"
-import { Bus } from "@/bus"
-import { Log } from "../util"
-import { LSPClient } from "./client"
-import path from "path"
-import { pathToFileURL, fileURLToPath } from "url"
-import { LSPServer } from "./server"
-import z from "zod"
-import { Config } from "../config"
-import { Instance } from "../project/instance"
-import { Flag } from "@/flag/flag"
-import { Process } from "../util"
-import { spawn as lspspawn } from "./launch"
-import { Effect, Layer, Context } from "effect"
-import { InstanceState } from "@/effect"
-
-export namespace LSP {
-  const log = Log.create({ service: "lsp" })
-
-  export const Event = {
-    Updated: BusEvent.define("lsp.updated", z.object({})),
-  }
-
-  export const Range = z
-    .object({
-      start: z.object({
-        line: z.number(),
-        character: z.number(),
-      }),
-      end: z.object({
-        line: z.number(),
-        character: z.number(),
-      }),
-    })
-    .meta({
-      ref: "Range",
-    })
-  export type Range = z.infer<typeof Range>
-
-  export const Symbol = z
-    .object({
-      name: z.string(),
-      kind: z.number(),
-      location: z.object({
-        uri: z.string(),
-        range: Range,
-      }),
-    })
-    .meta({
-      ref: "Symbol",
-    })
-  export type Symbol = z.infer<typeof Symbol>
-
-  export const DocumentSymbol = z
-    .object({
-      name: z.string(),
-      detail: z.string().optional(),
-      kind: z.number(),
-      range: Range,
-      selectionRange: Range,
-    })
-    .meta({
-      ref: "DocumentSymbol",
-    })
-  export type DocumentSymbol = z.infer<typeof DocumentSymbol>
-
-  export const Status = z
-    .object({
-      id: z.string(),
-      name: z.string(),
-      root: z.string(),
-      status: z.union([z.literal("connected"), z.literal("error")]),
-    })
-    .meta({
-      ref: "LSPStatus",
-    })
-  export type Status = z.infer<typeof Status>
-
-  enum SymbolKind {
-    File = 1,
-    Module = 2,
-    Namespace = 3,
-    Package = 4,
-    Class = 5,
-    Method = 6,
-    Property = 7,
-    Field = 8,
-    Constructor = 9,
-    Enum = 10,
-    Interface = 11,
-    Function = 12,
-    Variable = 13,
-    Constant = 14,
-    String = 15,
-    Number = 16,
-    Boolean = 17,
-    Array = 18,
-    Object = 19,
-    Key = 20,
-    Null = 21,
-    EnumMember = 22,
-    Struct = 23,
-    Event = 24,
-    Operator = 25,
-    TypeParameter = 26,
-  }
-
-  const kinds = [
-    SymbolKind.Class,
-    SymbolKind.Function,
-    SymbolKind.Method,
-    SymbolKind.Interface,
-    SymbolKind.Variable,
-    SymbolKind.Constant,
-    SymbolKind.Struct,
-    SymbolKind.Enum,
-  ]
-
-  const filterExperimentalServers = (servers: Record<string, LSPServer.Info>) => {
-    if (Flag.OPENCODE_EXPERIMENTAL_LSP_TY) {
-      if (servers["pyright"]) {
-        log.info("LSP server pyright is disabled because OPENCODE_EXPERIMENTAL_LSP_TY is enabled")
-        delete servers["pyright"]
-      }
-    } else {
-      if (servers["ty"]) {
-        delete servers["ty"]
-      }
-    }
-  }
-
-  type LocInput = { file: string; line: number; character: number }
-
-  interface State {
-    clients: LSPClient.Info[]
-    servers: Record<string, LSPServer.Info>
-    broken: Set<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 touchFile: (input: string, waitForDiagnostics?: boolean) => Effect.Effect<void>
-    readonly diagnostics: () => Effect.Effect<Record<string, LSPClient.Diagnostic[]>>
-    readonly hover: (input: LocInput) => Effect.Effect<any>
-    readonly definition: (input: LocInput) => Effect.Effect<any[]>
-    readonly references: (input: LocInput) => Effect.Effect<any[]>
-    readonly implementation: (input: LocInput) => Effect.Effect<any[]>
-    readonly documentSymbol: (uri: string) => Effect.Effect<(LSP.DocumentSymbol | LSP.Symbol)[]>
-    readonly workspaceSymbol: (query: string) => Effect.Effect<LSP.Symbol[]>
-    readonly prepareCallHierarchy: (input: LocInput) => Effect.Effect<any[]>
-    readonly incomingCalls: (input: LocInput) => Effect.Effect<any[]>
-    readonly outgoingCalls: (input: LocInput) => Effect.Effect<any[]>
-  }
-
-  export class Service extends Context.Service<Service, Interface>()("@opencode/LSP") {}
-
-  export const layer = Layer.effect(
-    Service,
-    Effect.gen(function* () {
-      const config = yield* Config.Service
-
-      const state = yield* InstanceState.make<State>(
-        Effect.fn("LSP.state")(function* () {
-          const cfg = yield* config.get()
-
-          const servers: Record<string, LSPServer.Info> = {}
-
-          if (cfg.lsp === false) {
-            log.info("all LSPs are disabled")
-          } else {
-            for (const server of Object.values(LSPServer)) {
-              servers[server.id] = server
-            }
-
-            filterExperimentalServers(servers)
-
-            for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
-              const existing = servers[name]
-              if (item.disabled) {
-                log.info(`LSP server ${name} is disabled`)
-                delete servers[name]
-                continue
-              }
-              servers[name] = {
-                ...existing,
-                id: name,
-                root: existing?.root ?? (async () => Instance.directory),
-                extensions: item.extensions ?? existing?.extensions ?? [],
-                spawn: async (root) => ({
-                  process: lspspawn(item.command[0], item.command.slice(1), {
-                    cwd: root,
-                    env: { ...process.env, ...item.env },
-                  }),
-                  initialization: item.initialization,
-                }),
-              }
-            }
-
-            log.info("enabled LSP servers", {
-              serverIds: Object.values(servers)
-                .map((server) => server.id)
-                .join(", "),
-            })
-          }
-
-          const s: State = {
-            clients: [],
-            servers,
-            broken: new Set(),
-            spawning: new Map(),
-          }
-
-          yield* Effect.addFinalizer(() =>
-            Effect.promise(async () => {
-              await Promise.all(s.clients.map((client) => client.shutdown()))
-            }),
-          )
-
-          return s
-        }),
-      )
-
-      const getClients = Effect.fnUntraced(function* (file: string) {
-        if (!Instance.containsPath(file)) return [] as LSPClient.Info[]
-        const s = yield* InstanceState.get(state)
-        return yield* Effect.promise(async () => {
-          const extension = path.parse(file).ext || file
-          const result: LSPClient.Info[] = []
-
-          async function schedule(server: LSPServer.Info, root: string, key: string) {
-            const handle = await server
-              .spawn(root)
-              .then((value) => {
-                if (!value) s.broken.add(key)
-                return value
-              })
-              .catch((err) => {
-                s.broken.add(key)
-                log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
-                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,
-            }).catch(async (err) => {
-              s.broken.add(key)
-              await Process.stop(handle.process)
-              log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
-              return undefined
-            })
-
-            if (!client) return undefined
-
-            const existing = s.clients.find((x) => x.root === root && x.serverID === server.id)
-            if (existing) {
-              await Process.stop(handle.process)
-              return existing
-            }
-
-            s.clients.push(client)
-            return client
-          }
-
-          for (const server of Object.values(s.servers)) {
-            if (server.extensions.length && !server.extensions.includes(extension)) continue
-
-            const root = await server.root(file)
-            if (!root) continue
-            if (s.broken.has(root + server.id)) continue
-
-            const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
-            if (match) {
-              result.push(match)
-              continue
-            }
-
-            const inflight = s.spawning.get(root + server.id)
-            if (inflight) {
-              const client = await inflight
-              if (!client) continue
-              result.push(client)
-              continue
-            }
-
-            const task = schedule(server, root, root + server.id)
-            s.spawning.set(root + server.id, task)
-
-            void task.finally(() => {
-              if (s.spawning.get(root + server.id) === task) {
-                s.spawning.delete(root + server.id)
-              }
-            })
-
-            const client = await task
-            if (!client) continue
-
-            result.push(client)
-            void Bus.publish(Event.Updated, {})
-          }
-
-          return result
-        })
-      })
-
-      const run = Effect.fnUntraced(function* <T>(file: string, fn: (client: LSPClient.Info) => Promise<T>) {
-        const clients = yield* getClients(file)
-        return yield* Effect.promise(() => Promise.all(clients.map((x) => fn(x))))
-      })
-
-      const runAll = Effect.fnUntraced(function* <T>(fn: (client: LSPClient.Info) => Promise<T>) {
-        const s = yield* InstanceState.get(state)
-        return yield* Effect.promise(() => Promise.all(s.clients.map((x) => fn(x))))
-      })
-
-      const init = Effect.fn("LSP.init")(function* () {
-        yield* InstanceState.get(state)
-      })
-
-      const status = Effect.fn("LSP.status")(function* () {
-        const s = yield* InstanceState.get(state)
-        const result: Status[] = []
-        for (const client of s.clients) {
-          result.push({
-            id: client.serverID,
-            name: s.servers[client.serverID].id,
-            root: path.relative(Instance.directory, client.root),
-            status: "connected",
-          })
-        }
-        return result
-      })
-
-      const hasClients = Effect.fn("LSP.hasClients")(function* (file: string) {
-        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)
-            if (!root) continue
-            if (s.broken.has(root + server.id)) continue
-            return true
-          }
-          return false
-        })
-      })
-
-      const touchFile = Effect.fn("LSP.touchFile")(function* (input: string, waitForDiagnostics?: boolean) {
-        log.info("touching file", { file: input })
-        const clients = yield* getClients(input)
-        yield* Effect.promise(() =>
-          Promise.all(
-            clients.map(async (client) => {
-              const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
-              await client.notify.open({ path: input })
-              return wait
-            }),
-          ).catch((err) => {
-            log.error("failed to touch file", { err, file: input })
-          }),
-        )
-      })
-
-      const diagnostics = Effect.fn("LSP.diagnostics")(function* () {
-        const results: Record<string, LSPClient.Diagnostic[]> = {}
-        const all = yield* runAll(async (client) => client.diagnostics)
-        for (const result of all) {
-          for (const [p, diags] of result.entries()) {
-            const arr = results[p] || []
-            arr.push(...diags)
-            results[p] = arr
-          }
-        }
-        return results
-      })
-
-      const hover = Effect.fn("LSP.hover")(function* (input: LocInput) {
-        return yield* run(input.file, (client) =>
-          client.connection
-            .sendRequest("textDocument/hover", {
-              textDocument: { uri: pathToFileURL(input.file).href },
-              position: { line: input.line, character: input.character },
-            })
-            .catch(() => null),
-        )
-      })
-
-      const definition = Effect.fn("LSP.definition")(function* (input: LocInput) {
-        const results = yield* run(input.file, (client) =>
-          client.connection
-            .sendRequest("textDocument/definition", {
-              textDocument: { uri: pathToFileURL(input.file).href },
-              position: { line: input.line, character: input.character },
-            })
-            .catch(() => null),
-        )
-        return results.flat().filter(Boolean)
-      })
-
-      const references = Effect.fn("LSP.references")(function* (input: LocInput) {
-        const results = yield* run(input.file, (client) =>
-          client.connection
-            .sendRequest("textDocument/references", {
-              textDocument: { uri: pathToFileURL(input.file).href },
-              position: { line: input.line, character: input.character },
-              context: { includeDeclaration: true },
-            })
-            .catch(() => []),
-        )
-        return results.flat().filter(Boolean)
-      })
-
-      const implementation = Effect.fn("LSP.implementation")(function* (input: LocInput) {
-        const results = yield* run(input.file, (client) =>
-          client.connection
-            .sendRequest("textDocument/implementation", {
-              textDocument: { uri: pathToFileURL(input.file).href },
-              position: { line: input.line, character: input.character },
-            })
-            .catch(() => null),
-        )
-        return results.flat().filter(Boolean)
-      })
-
-      const documentSymbol = Effect.fn("LSP.documentSymbol")(function* (uri: string) {
-        const file = fileURLToPath(uri)
-        const results = yield* run(file, (client) =>
-          client.connection.sendRequest("textDocument/documentSymbol", { textDocument: { uri } }).catch(() => []),
-        )
-        return (results.flat() as (LSP.DocumentSymbol | LSP.Symbol)[]).filter(Boolean)
-      })
-
-      const workspaceSymbol = Effect.fn("LSP.workspaceSymbol")(function* (query: string) {
-        const results = yield* runAll((client) =>
-          client.connection
-            .sendRequest("workspace/symbol", { query })
-            .then((result: any) => result.filter((x: LSP.Symbol) => kinds.includes(x.kind)))
-            .then((result: any) => result.slice(0, 10))
-            .catch(() => []),
-        )
-        return results.flat() as LSP.Symbol[]
-      })
-
-      const prepareCallHierarchy = Effect.fn("LSP.prepareCallHierarchy")(function* (input: LocInput) {
-        const results = yield* run(input.file, (client) =>
-          client.connection
-            .sendRequest("textDocument/prepareCallHierarchy", {
-              textDocument: { uri: pathToFileURL(input.file).href },
-              position: { line: input.line, character: input.character },
-            })
-            .catch(() => []),
-        )
-        return results.flat().filter(Boolean)
-      })
-
-      const callHierarchyRequest = Effect.fnUntraced(function* (
-        input: LocInput,
-        direction: "callHierarchy/incomingCalls" | "callHierarchy/outgoingCalls",
-      ) {
-        const results = yield* run(input.file, async (client) => {
-          const items = (await client.connection
-            .sendRequest("textDocument/prepareCallHierarchy", {
-              textDocument: { uri: pathToFileURL(input.file).href },
-              position: { line: input.line, character: input.character },
-            })
-            .catch(() => [])) as any[]
-          if (!items?.length) return []
-          return client.connection.sendRequest(direction, { item: items[0] }).catch(() => [])
-        })
-        return results.flat().filter(Boolean)
-      })
-
-      const incomingCalls = Effect.fn("LSP.incomingCalls")(function* (input: LocInput) {
-        return yield* callHierarchyRequest(input, "callHierarchy/incomingCalls")
-      })
-
-      const outgoingCalls = Effect.fn("LSP.outgoingCalls")(function* (input: LocInput) {
-        return yield* callHierarchyRequest(input, "callHierarchy/outgoingCalls")
-      })
-
-      return Service.of({
-        init,
-        status,
-        hasClients,
-        touchFile,
-        diagnostics,
-        hover,
-        definition,
-        references,
-        implementation,
-        documentSymbol,
-        workspaceSymbol,
-        prepareCallHierarchy,
-        incomingCalls,
-        outgoingCalls,
-      })
-    }),
-  )
-
-  export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
-
-  export namespace Diagnostic {
-    const MAX_PER_FILE = 20
-
-    export function pretty(diagnostic: LSPClient.Diagnostic) {
-      const severityMap = {
-        1: "ERROR",
-        2: "WARN",
-        3: "INFO",
-        4: "HINT",
-      }
-
-      const severity = severityMap[diagnostic.severity || 1]
-      const line = diagnostic.range.start.line + 1
-      const col = diagnostic.range.start.character + 1
-
-      return `${severity} [${line}:${col}] ${diagnostic.message}`
-    }
-
-    export function report(file: string, issues: LSPClient.Diagnostic[]) {
-      const errors = issues.filter((item) => item.severity === 1)
-      if (errors.length === 0) return ""
-      const limited = errors.slice(0, MAX_PER_FILE)
-      const more = errors.length - MAX_PER_FILE
-      const suffix = more > 0 ? `\n... and ${more} more` : ""
-      return `<diagnostics file="${file}">\n${limited.map(pretty).join("\n")}${suffix}\n</diagnostics>`
-    }
-  }
-}
+export * as LSP from "./lsp"
+export * as LSPClient from "./client"
+export * as LSPServer from "./server"

+ 535 - 0
packages/opencode/src/lsp/lsp.ts

@@ -0,0 +1,535 @@
+import { BusEvent } from "@/bus/bus-event"
+import { Bus } from "@/bus"
+import { Log } from "../util"
+import { LSPClient } from "."
+import path from "path"
+import { pathToFileURL, fileURLToPath } from "url"
+import { LSPServer } from "."
+import z from "zod"
+import { Config } from "../config"
+import { Instance } from "../project/instance"
+import { Flag } from "@/flag/flag"
+import { Process } from "../util"
+import { spawn as lspspawn } from "./launch"
+import { Effect, Layer, Context } from "effect"
+import { InstanceState } from "@/effect"
+
+const log = Log.create({ service: "lsp" })
+
+export const Event = {
+  Updated: BusEvent.define("lsp.updated", z.object({})),
+}
+
+export const Range = z
+  .object({
+    start: z.object({
+      line: z.number(),
+      character: z.number(),
+    }),
+    end: z.object({
+      line: z.number(),
+      character: z.number(),
+    }),
+  })
+  .meta({
+    ref: "Range",
+  })
+export type Range = z.infer<typeof Range>
+
+export const Symbol = z
+  .object({
+    name: z.string(),
+    kind: z.number(),
+    location: z.object({
+      uri: z.string(),
+      range: Range,
+    }),
+  })
+  .meta({
+    ref: "Symbol",
+  })
+export type Symbol = z.infer<typeof Symbol>
+
+export const DocumentSymbol = z
+  .object({
+    name: z.string(),
+    detail: z.string().optional(),
+    kind: z.number(),
+    range: Range,
+    selectionRange: Range,
+  })
+  .meta({
+    ref: "DocumentSymbol",
+  })
+export type DocumentSymbol = z.infer<typeof DocumentSymbol>
+
+export const Status = z
+  .object({
+    id: z.string(),
+    name: z.string(),
+    root: z.string(),
+    status: z.union([z.literal("connected"), z.literal("error")]),
+  })
+  .meta({
+    ref: "LSPStatus",
+  })
+export type Status = z.infer<typeof Status>
+
+enum SymbolKind {
+  File = 1,
+  Module = 2,
+  Namespace = 3,
+  Package = 4,
+  Class = 5,
+  Method = 6,
+  Property = 7,
+  Field = 8,
+  Constructor = 9,
+  Enum = 10,
+  Interface = 11,
+  Function = 12,
+  Variable = 13,
+  Constant = 14,
+  String = 15,
+  Number = 16,
+  Boolean = 17,
+  Array = 18,
+  Object = 19,
+  Key = 20,
+  Null = 21,
+  EnumMember = 22,
+  Struct = 23,
+  Event = 24,
+  Operator = 25,
+  TypeParameter = 26,
+}
+
+const kinds = [
+  SymbolKind.Class,
+  SymbolKind.Function,
+  SymbolKind.Method,
+  SymbolKind.Interface,
+  SymbolKind.Variable,
+  SymbolKind.Constant,
+  SymbolKind.Struct,
+  SymbolKind.Enum,
+]
+
+const filterExperimentalServers = (servers: Record<string, LSPServer.Info>) => {
+  if (Flag.OPENCODE_EXPERIMENTAL_LSP_TY) {
+    if (servers["pyright"]) {
+      log.info("LSP server pyright is disabled because OPENCODE_EXPERIMENTAL_LSP_TY is enabled")
+      delete servers["pyright"]
+    }
+  } else {
+    if (servers["ty"]) {
+      delete servers["ty"]
+    }
+  }
+}
+
+type LocInput = { file: string; line: number; character: number }
+
+interface State {
+  clients: LSPClient.Info[]
+  servers: Record<string, LSPServer.Info>
+  broken: Set<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 touchFile: (input: string, waitForDiagnostics?: boolean) => Effect.Effect<void>
+  readonly diagnostics: () => Effect.Effect<Record<string, LSPClient.Diagnostic[]>>
+  readonly hover: (input: LocInput) => Effect.Effect<any>
+  readonly definition: (input: LocInput) => Effect.Effect<any[]>
+  readonly references: (input: LocInput) => Effect.Effect<any[]>
+  readonly implementation: (input: LocInput) => Effect.Effect<any[]>
+  readonly documentSymbol: (uri: string) => Effect.Effect<(DocumentSymbol | Symbol)[]>
+  readonly workspaceSymbol: (query: string) => Effect.Effect<Symbol[]>
+  readonly prepareCallHierarchy: (input: LocInput) => Effect.Effect<any[]>
+  readonly incomingCalls: (input: LocInput) => Effect.Effect<any[]>
+  readonly outgoingCalls: (input: LocInput) => Effect.Effect<any[]>
+}
+
+export class Service extends Context.Service<Service, Interface>()("@opencode/LSP") {}
+
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const config = yield* Config.Service
+
+    const state = yield* InstanceState.make<State>(
+      Effect.fn("LSP.state")(function* () {
+        const cfg = yield* config.get()
+
+        const servers: Record<string, LSPServer.Info> = {}
+
+        if (cfg.lsp === false) {
+          log.info("all LSPs are disabled")
+        } else {
+          for (const server of Object.values(LSPServer)) {
+            servers[server.id] = server
+          }
+
+          filterExperimentalServers(servers)
+
+          for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
+            const existing = servers[name]
+            if (item.disabled) {
+              log.info(`LSP server ${name} is disabled`)
+              delete servers[name]
+              continue
+            }
+            servers[name] = {
+              ...existing,
+              id: name,
+              root: existing?.root ?? (async () => Instance.directory),
+              extensions: item.extensions ?? existing?.extensions ?? [],
+              spawn: async (root) => ({
+                process: lspspawn(item.command[0], item.command.slice(1), {
+                  cwd: root,
+                  env: { ...process.env, ...item.env },
+                }),
+                initialization: item.initialization,
+              }),
+            }
+          }
+
+          log.info("enabled LSP servers", {
+            serverIds: Object.values(servers)
+              .map((server) => server.id)
+              .join(", "),
+          })
+        }
+
+        const s: State = {
+          clients: [],
+          servers,
+          broken: new Set(),
+          spawning: new Map(),
+        }
+
+        yield* Effect.addFinalizer(() =>
+          Effect.promise(async () => {
+            await Promise.all(s.clients.map((client) => client.shutdown()))
+          }),
+        )
+
+        return s
+      }),
+    )
+
+    const getClients = Effect.fnUntraced(function* (file: string) {
+      if (!Instance.containsPath(file)) return [] as LSPClient.Info[]
+      const s = yield* InstanceState.get(state)
+      return yield* Effect.promise(async () => {
+        const extension = path.parse(file).ext || file
+        const result: LSPClient.Info[] = []
+
+        async function schedule(server: LSPServer.Info, root: string, key: string) {
+          const handle = await server
+            .spawn(root)
+            .then((value) => {
+              if (!value) s.broken.add(key)
+              return value
+            })
+            .catch((err) => {
+              s.broken.add(key)
+              log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
+              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,
+          }).catch(async (err) => {
+            s.broken.add(key)
+            await Process.stop(handle.process)
+            log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
+            return undefined
+          })
+
+          if (!client) return undefined
+
+          const existing = s.clients.find((x) => x.root === root && x.serverID === server.id)
+          if (existing) {
+            await Process.stop(handle.process)
+            return existing
+          }
+
+          s.clients.push(client)
+          return client
+        }
+
+        for (const server of Object.values(s.servers)) {
+          if (server.extensions.length && !server.extensions.includes(extension)) continue
+
+          const root = await server.root(file)
+          if (!root) continue
+          if (s.broken.has(root + server.id)) continue
+
+          const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
+          if (match) {
+            result.push(match)
+            continue
+          }
+
+          const inflight = s.spawning.get(root + server.id)
+          if (inflight) {
+            const client = await inflight
+            if (!client) continue
+            result.push(client)
+            continue
+          }
+
+          const task = schedule(server, root, root + server.id)
+          s.spawning.set(root + server.id, task)
+
+          task.finally(() => {
+            if (s.spawning.get(root + server.id) === task) {
+              s.spawning.delete(root + server.id)
+            }
+          })
+
+          const client = await task
+          if (!client) continue
+
+          result.push(client)
+          Bus.publish(Event.Updated, {})
+        }
+
+        return result
+      })
+    })
+
+    const run = Effect.fnUntraced(function* <T>(file: string, fn: (client: LSPClient.Info) => Promise<T>) {
+      const clients = yield* getClients(file)
+      return yield* Effect.promise(() => Promise.all(clients.map((x) => fn(x))))
+    })
+
+    const runAll = Effect.fnUntraced(function* <T>(fn: (client: LSPClient.Info) => Promise<T>) {
+      const s = yield* InstanceState.get(state)
+      return yield* Effect.promise(() => Promise.all(s.clients.map((x) => fn(x))))
+    })
+
+    const init = Effect.fn("LSP.init")(function* () {
+      yield* InstanceState.get(state)
+    })
+
+    const status = Effect.fn("LSP.status")(function* () {
+      const s = yield* InstanceState.get(state)
+      const result: Status[] = []
+      for (const client of s.clients) {
+        result.push({
+          id: client.serverID,
+          name: s.servers[client.serverID].id,
+          root: path.relative(Instance.directory, client.root),
+          status: "connected",
+        })
+      }
+      return result
+    })
+
+    const hasClients = Effect.fn("LSP.hasClients")(function* (file: string) {
+      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)
+          if (!root) continue
+          if (s.broken.has(root + server.id)) continue
+          return true
+        }
+        return false
+      })
+    })
+
+    const touchFile = Effect.fn("LSP.touchFile")(function* (input: string, waitForDiagnostics?: boolean) {
+      log.info("touching file", { file: input })
+      const clients = yield* getClients(input)
+      yield* Effect.promise(() =>
+        Promise.all(
+          clients.map(async (client) => {
+            const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
+            await client.notify.open({ path: input })
+            return wait
+          }),
+        ).catch((err) => {
+          log.error("failed to touch file", { err, file: input })
+        }),
+      )
+    })
+
+    const diagnostics = Effect.fn("LSP.diagnostics")(function* () {
+      const results: Record<string, LSPClient.Diagnostic[]> = {}
+      const all = yield* runAll(async (client) => client.diagnostics)
+      for (const result of all) {
+        for (const [p, diags] of result.entries()) {
+          const arr = results[p] || []
+          arr.push(...diags)
+          results[p] = arr
+        }
+      }
+      return results
+    })
+
+    const hover = Effect.fn("LSP.hover")(function* (input: LocInput) {
+      return yield* run(input.file, (client) =>
+        client.connection
+          .sendRequest("textDocument/hover", {
+            textDocument: { uri: pathToFileURL(input.file).href },
+            position: { line: input.line, character: input.character },
+          })
+          .catch(() => null),
+      )
+    })
+
+    const definition = Effect.fn("LSP.definition")(function* (input: LocInput) {
+      const results = yield* run(input.file, (client) =>
+        client.connection
+          .sendRequest("textDocument/definition", {
+            textDocument: { uri: pathToFileURL(input.file).href },
+            position: { line: input.line, character: input.character },
+          })
+          .catch(() => null),
+      )
+      return results.flat().filter(Boolean)
+    })
+
+    const references = Effect.fn("LSP.references")(function* (input: LocInput) {
+      const results = yield* run(input.file, (client) =>
+        client.connection
+          .sendRequest("textDocument/references", {
+            textDocument: { uri: pathToFileURL(input.file).href },
+            position: { line: input.line, character: input.character },
+            context: { includeDeclaration: true },
+          })
+          .catch(() => []),
+      )
+      return results.flat().filter(Boolean)
+    })
+
+    const implementation = Effect.fn("LSP.implementation")(function* (input: LocInput) {
+      const results = yield* run(input.file, (client) =>
+        client.connection
+          .sendRequest("textDocument/implementation", {
+            textDocument: { uri: pathToFileURL(input.file).href },
+            position: { line: input.line, character: input.character },
+          })
+          .catch(() => null),
+      )
+      return results.flat().filter(Boolean)
+    })
+
+    const documentSymbol = Effect.fn("LSP.documentSymbol")(function* (uri: string) {
+      const file = fileURLToPath(uri)
+      const results = yield* run(file, (client) =>
+        client.connection.sendRequest("textDocument/documentSymbol", { textDocument: { uri } }).catch(() => []),
+      )
+      return (results.flat() as (DocumentSymbol | Symbol)[]).filter(Boolean)
+    })
+
+    const workspaceSymbol = Effect.fn("LSP.workspaceSymbol")(function* (query: string) {
+      const results = yield* runAll((client) =>
+        client.connection
+          .sendRequest("workspace/symbol", { query })
+          .then((result: any) => result.filter((x: Symbol) => kinds.includes(x.kind)))
+          .then((result: any) => result.slice(0, 10))
+          .catch(() => []),
+      )
+      return results.flat() as Symbol[]
+    })
+
+    const prepareCallHierarchy = Effect.fn("LSP.prepareCallHierarchy")(function* (input: LocInput) {
+      const results = yield* run(input.file, (client) =>
+        client.connection
+          .sendRequest("textDocument/prepareCallHierarchy", {
+            textDocument: { uri: pathToFileURL(input.file).href },
+            position: { line: input.line, character: input.character },
+          })
+          .catch(() => []),
+      )
+      return results.flat().filter(Boolean)
+    })
+
+    const callHierarchyRequest = Effect.fnUntraced(function* (
+      input: LocInput,
+      direction: "callHierarchy/incomingCalls" | "callHierarchy/outgoingCalls",
+    ) {
+      const results = yield* run(input.file, async (client) => {
+        const items = (await client.connection
+          .sendRequest("textDocument/prepareCallHierarchy", {
+            textDocument: { uri: pathToFileURL(input.file).href },
+            position: { line: input.line, character: input.character },
+          })
+          .catch(() => [])) as any[]
+        if (!items?.length) return []
+        return client.connection.sendRequest(direction, { item: items[0] }).catch(() => [])
+      })
+      return results.flat().filter(Boolean)
+    })
+
+    const incomingCalls = Effect.fn("LSP.incomingCalls")(function* (input: LocInput) {
+      return yield* callHierarchyRequest(input, "callHierarchy/incomingCalls")
+    })
+
+    const outgoingCalls = Effect.fn("LSP.outgoingCalls")(function* (input: LocInput) {
+      return yield* callHierarchyRequest(input, "callHierarchy/outgoingCalls")
+    })
+
+    return Service.of({
+      init,
+      status,
+      hasClients,
+      touchFile,
+      diagnostics,
+      hover,
+      definition,
+      references,
+      implementation,
+      documentSymbol,
+      workspaceSymbol,
+      prepareCallHierarchy,
+      incomingCalls,
+      outgoingCalls,
+    })
+  }),
+)
+
+export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
+
+export namespace Diagnostic {
+  const MAX_PER_FILE = 20
+
+  export function pretty(diagnostic: LSPClient.Diagnostic) {
+    const severityMap = {
+      1: "ERROR",
+      2: "WARN",
+      3: "INFO",
+      4: "HINT",
+    }
+
+    const severity = severityMap[diagnostic.severity || 1]
+    const line = diagnostic.range.start.line + 1
+    const col = diagnostic.range.start.character + 1
+
+    return `${severity} [${line}:${col}] ${diagnostic.message}`
+  }
+
+  export function report(file: string, issues: LSPClient.Diagnostic[]) {
+    const errors = issues.filter((item) => item.severity === 1)
+    if (errors.length === 0) return ""
+    const limited = errors.slice(0, MAX_PER_FILE)
+    const more = errors.length - MAX_PER_FILE
+    const suffix = more > 0 ? `\n... and ${more} more` : ""
+    return `<diagnostics file="${file}">\n${limited.map(pretty).join("\n")}${suffix}\n</diagnostics>`
+  }
+}

+ 1690 - 1692
packages/opencode/src/lsp/server.ts

@@ -15,1944 +15,1942 @@ import { Module } from "@opencode-ai/shared/util/module"
 import { spawn } from "./launch"
 import { Npm } from "../npm"
 
-export namespace LSPServer {
-  const log = Log.create({ service: "lsp.server" })
-  const pathExists = async (p: string) =>
-    fs
-      .stat(p)
-      .then(() => true)
-      .catch(() => false)
-  const run = (cmd: string[], opts: Process.RunOptions = {}) => Process.run(cmd, { ...opts, nothrow: true })
-  const output = (cmd: string[], opts: Process.RunOptions = {}) => Process.text(cmd, { ...opts, nothrow: true })
-
-  export interface Handle {
-    process: ChildProcessWithoutNullStreams
-    initialization?: Record<string, any>
-  }
+const log = Log.create({ service: "lsp.server" })
+const pathExists = async (p: string) =>
+  fs
+    .stat(p)
+    .then(() => true)
+    .catch(() => false)
+const run = (cmd: string[], opts: Process.RunOptions = {}) => Process.run(cmd, { ...opts, nothrow: true })
+const output = (cmd: string[], opts: Process.RunOptions = {}) => Process.text(cmd, { ...opts, nothrow: true })
+
+export interface Handle {
+  process: ChildProcessWithoutNullStreams
+  initialization?: Record<string, any>
+}
 
-  type RootFunction = (file: string) => Promise<string | undefined>
+type RootFunction = (file: string) => Promise<string | undefined>
 
-  const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): RootFunction => {
-    return async (file) => {
-      if (excludePatterns) {
-        const excludedFiles = Filesystem.up({
-          targets: excludePatterns,
-          start: path.dirname(file),
-          stop: Instance.directory,
-        })
-        const excluded = await excludedFiles.next()
-        await excludedFiles.return()
-        if (excluded.value) return undefined
-      }
-      const files = Filesystem.up({
-        targets: includePatterns,
+const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): RootFunction => {
+  return async (file) => {
+    if (excludePatterns) {
+      const excludedFiles = Filesystem.up({
+        targets: excludePatterns,
         start: path.dirname(file),
         stop: Instance.directory,
       })
-      const first = await files.next()
-      await files.return()
-      if (!first.value) return Instance.directory
-      return path.dirname(first.value)
+      const excluded = await excludedFiles.next()
+      await excludedFiles.return()
+      if (excluded.value) return undefined
     }
+    const files = Filesystem.up({
+      targets: includePatterns,
+      start: path.dirname(file),
+      stop: Instance.directory,
+    })
+    const first = await files.next()
+    await files.return()
+    if (!first.value) return Instance.directory
+    return path.dirname(first.value)
   }
+}
 
-  export interface Info {
-    id: string
-    extensions: string[]
-    global?: boolean
-    root: RootFunction
-    spawn(root: string): Promise<Handle | undefined>
-  }
-
-  export const Deno: Info = {
-    id: "deno",
-    root: async (file) => {
-      const files = Filesystem.up({
-        targets: ["deno.json", "deno.jsonc"],
-        start: path.dirname(file),
-        stop: Instance.directory,
-      })
-      const first = await files.next()
-      await files.return()
-      if (!first.value) return undefined
-      return path.dirname(first.value)
-    },
-    extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"],
-    async spawn(root) {
-      const deno = which("deno")
-      if (!deno) {
-        log.info("deno not found, please install deno first")
-        return
-      }
-      return {
-        process: spawn(deno, ["lsp"], {
-          cwd: root,
-        }),
-      }
-    },
-  }
+export interface Info {
+  id: string
+  extensions: string[]
+  global?: boolean
+  root: RootFunction
+  spawn(root: string): Promise<Handle | undefined>
+}
 
-  export const Typescript: Info = {
-    id: "typescript",
-    root: NearestRoot(
-      ["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"],
-      ["deno.json", "deno.jsonc"],
-    ),
-    extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
-    async spawn(root) {
-      const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory)
-      log.info("typescript server", { tsserver })
-      if (!tsserver) return
-      const bin = await Npm.which("typescript-language-server")
-      if (!bin) return
-      const proc = spawn(bin, ["--stdio"], {
+export const Deno: Info = {
+  id: "deno",
+  root: async (file) => {
+    const files = Filesystem.up({
+      targets: ["deno.json", "deno.jsonc"],
+      start: path.dirname(file),
+      stop: Instance.directory,
+    })
+    const first = await files.next()
+    await files.return()
+    if (!first.value) return undefined
+    return path.dirname(first.value)
+  },
+  extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"],
+  async spawn(root) {
+    const deno = which("deno")
+    if (!deno) {
+      log.info("deno not found, please install deno first")
+      return
+    }
+    return {
+      process: spawn(deno, ["lsp"], {
         cwd: root,
-        env: {
-          ...process.env,
-        },
-      })
-      return {
-        process: proc,
-        initialization: {
-          tsserver: {
-            path: tsserver,
-          },
-        },
-      }
-    },
-  }
+      }),
+    }
+  },
+}
 
-  export const Vue: Info = {
-    id: "vue",
-    extensions: [".vue"],
-    root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
-    async spawn(root) {
-      let binary = which("vue-language-server")
-      const args: string[] = []
-      if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        const resolved = await Npm.which("@vue/language-server")
-        if (!resolved) return
-        binary = resolved
-      }
-      args.push("--stdio")
-      const proc = spawn(binary, args, {
-        cwd: root,
-        env: {
-          ...process.env,
+export const Typescript: Info = {
+  id: "typescript",
+  root: NearestRoot(
+    ["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"],
+    ["deno.json", "deno.jsonc"],
+  ),
+  extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
+  async spawn(root) {
+    const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory)
+    log.info("typescript server", { tsserver })
+    if (!tsserver) return
+    const bin = await Npm.which("typescript-language-server")
+    if (!bin) return
+    const proc = spawn(bin, ["--stdio"], {
+      cwd: root,
+      env: {
+        ...process.env,
+      },
+    })
+    return {
+      process: proc,
+      initialization: {
+        tsserver: {
+          path: tsserver,
         },
-      })
-      return {
-        process: proc,
-        initialization: {
-          // Leave empty; the server will auto-detect workspace TypeScript.
-        },
-      }
-    },
-  }
+      },
+    }
+  },
+}
 
-  export const ESLint: Info = {
-    id: "eslint",
-    root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
-    extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
-    async spawn(root) {
-      const eslint = Module.resolve("eslint", Instance.directory)
-      if (!eslint) return
-      log.info("spawning eslint server")
-      const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
-      if (!(await Filesystem.exists(serverPath))) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        log.info("downloading and building VS Code ESLint server")
-        const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
-        if (!response.ok) return
+export const Vue: Info = {
+  id: "vue",
+  extensions: [".vue"],
+  root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
+  async spawn(root) {
+    let binary = which("vue-language-server")
+    const args: string[] = []
+    if (!binary) {
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      const resolved = await Npm.which("@vue/language-server")
+      if (!resolved) return
+      binary = resolved
+    }
+    args.push("--stdio")
+    const proc = spawn(binary, args, {
+      cwd: root,
+      env: {
+        ...process.env,
+      },
+    })
+    return {
+      process: proc,
+      initialization: {
+        // Leave empty; the server will auto-detect workspace TypeScript.
+      },
+    }
+  },
+}
 
-        const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
-        if (response.body) await Filesystem.writeStream(zipPath, response.body)
+export const ESLint: Info = {
+  id: "eslint",
+  root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
+  extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
+  async spawn(root) {
+    const eslint = Module.resolve("eslint", Instance.directory)
+    if (!eslint) return
+    log.info("spawning eslint server")
+    const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
+    if (!(await Filesystem.exists(serverPath))) {
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      log.info("downloading and building VS Code ESLint server")
+      const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
+      if (!response.ok) return
+
+      const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
+      if (response.body) await Filesystem.writeStream(zipPath, response.body)
+
+      const ok = await Archive.extractZip(zipPath, Global.Path.bin)
+        .then(() => true)
+        .catch((error) => {
+          log.error("Failed to extract vscode-eslint archive", { error })
+          return false
+        })
+      if (!ok) return
+      await fs.rm(zipPath, { force: true })
 
-        const ok = await Archive.extractZip(zipPath, Global.Path.bin)
-          .then(() => true)
-          .catch((error) => {
-            log.error("Failed to extract vscode-eslint archive", { error })
-            return false
-          })
-        if (!ok) return
-        await fs.rm(zipPath, { force: true })
+      const extractedPath = path.join(Global.Path.bin, "vscode-eslint-main")
+      const finalPath = path.join(Global.Path.bin, "vscode-eslint")
 
-        const extractedPath = path.join(Global.Path.bin, "vscode-eslint-main")
-        const finalPath = path.join(Global.Path.bin, "vscode-eslint")
+      const stats = await fs.stat(finalPath).catch(() => undefined)
+      if (stats) {
+        log.info("removing old eslint installation", { path: finalPath })
+        await fs.rm(finalPath, { force: true, recursive: true })
+      }
+      await fs.rename(extractedPath, finalPath)
 
-        const stats = await fs.stat(finalPath).catch(() => undefined)
-        if (stats) {
-          log.info("removing old eslint installation", { path: finalPath })
-          await fs.rm(finalPath, { force: true, recursive: true })
-        }
-        await fs.rename(extractedPath, finalPath)
+      const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"
+      await Process.run([npmCmd, "install"], { cwd: finalPath })
+      await Process.run([npmCmd, "run", "compile"], { cwd: finalPath })
 
-        const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"
-        await Process.run([npmCmd, "install"], { cwd: finalPath })
-        await Process.run([npmCmd, "run", "compile"], { cwd: finalPath })
+      log.info("installed VS Code ESLint server", { serverPath })
+    }
 
-        log.info("installed VS Code ESLint server", { serverPath })
-      }
+    const proc = spawn("node", [serverPath, "--stdio"], {
+      cwd: root,
+      env: {
+        ...process.env,
+      },
+    })
 
-      const proc = spawn("node", [serverPath, "--stdio"], {
-        cwd: root,
-        env: {
-          ...process.env,
-        },
+    return {
+      process: proc,
+    }
+  },
+}
+
+export const Oxlint: Info = {
+  id: "oxlint",
+  root: NearestRoot([
+    ".oxlintrc.json",
+    "package-lock.json",
+    "bun.lockb",
+    "bun.lock",
+    "pnpm-lock.yaml",
+    "yarn.lock",
+    "package.json",
+  ]),
+  extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue", ".astro", ".svelte"],
+  async spawn(root) {
+    const ext = process.platform === "win32" ? ".cmd" : ""
+
+    const serverTarget = path.join("node_modules", ".bin", "oxc_language_server" + ext)
+    const lintTarget = path.join("node_modules", ".bin", "oxlint" + ext)
+
+    const resolveBin = async (target: string) => {
+      const localBin = path.join(root, target)
+      if (await Filesystem.exists(localBin)) return localBin
+
+      const candidates = Filesystem.up({
+        targets: [target],
+        start: root,
+        stop: Instance.worktree,
       })
+      const first = await candidates.next()
+      await candidates.return()
+      if (first.value) return first.value
 
-      return {
-        process: proc,
-      }
-    },
-  }
+      return undefined
+    }
 
-  export const Oxlint: Info = {
-    id: "oxlint",
-    root: NearestRoot([
-      ".oxlintrc.json",
-      "package-lock.json",
-      "bun.lockb",
-      "bun.lock",
-      "pnpm-lock.yaml",
-      "yarn.lock",
-      "package.json",
-    ]),
-    extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue", ".astro", ".svelte"],
-    async spawn(root) {
-      const ext = process.platform === "win32" ? ".cmd" : ""
-
-      const serverTarget = path.join("node_modules", ".bin", "oxc_language_server" + ext)
-      const lintTarget = path.join("node_modules", ".bin", "oxlint" + ext)
-
-      const resolveBin = async (target: string) => {
-        const localBin = path.join(root, target)
-        if (await Filesystem.exists(localBin)) return localBin
-
-        const candidates = Filesystem.up({
-          targets: [target],
-          start: root,
-          stop: Instance.worktree,
-        })
-        const first = await candidates.next()
-        await candidates.return()
-        if (first.value) return first.value
-
-        return undefined
-      }
-
-      let lintBin = await resolveBin(lintTarget)
-      if (!lintBin) {
-        const found = which("oxlint")
-        if (found) lintBin = found
-      }
-
-      if (lintBin) {
-        const proc = spawn(lintBin, ["--help"])
-        await proc.exited
-        if (proc.stdout) {
-          const help = await text(proc.stdout)
-          if (help.includes("--lsp")) {
-            return {
-              process: spawn(lintBin, ["--lsp"], {
-                cwd: root,
-              }),
-            }
+    let lintBin = await resolveBin(lintTarget)
+    if (!lintBin) {
+      const found = which("oxlint")
+      if (found) lintBin = found
+    }
+
+    if (lintBin) {
+      const proc = spawn(lintBin, ["--help"])
+      await proc.exited
+      if (proc.stdout) {
+        const help = await text(proc.stdout)
+        if (help.includes("--lsp")) {
+          return {
+            process: spawn(lintBin, ["--lsp"], {
+              cwd: root,
+            }),
           }
         }
       }
+    }
 
-      let serverBin = await resolveBin(serverTarget)
-      if (!serverBin) {
-        const found = which("oxc_language_server")
-        if (found) serverBin = found
-      }
-      if (serverBin) {
-        return {
-          process: spawn(serverBin, [], {
-            cwd: root,
-          }),
-        }
+    let serverBin = await resolveBin(serverTarget)
+    if (!serverBin) {
+      const found = which("oxc_language_server")
+      if (found) serverBin = found
+    }
+    if (serverBin) {
+      return {
+        process: spawn(serverBin, [], {
+          cwd: root,
+        }),
       }
+    }
 
-      log.info("oxlint not found, please install oxlint")
-      return
-    },
-  }
+    log.info("oxlint not found, please install oxlint")
+    return
+  },
+}
 
-  export const Biome: Info = {
-    id: "biome",
-    root: NearestRoot([
-      "biome.json",
-      "biome.jsonc",
-      "package-lock.json",
-      "bun.lockb",
-      "bun.lock",
-      "pnpm-lock.yaml",
-      "yarn.lock",
-    ]),
-    extensions: [
-      ".ts",
-      ".tsx",
-      ".js",
-      ".jsx",
-      ".mjs",
-      ".cjs",
-      ".mts",
-      ".cts",
-      ".json",
-      ".jsonc",
-      ".vue",
-      ".astro",
-      ".svelte",
-      ".css",
-      ".graphql",
-      ".gql",
-      ".html",
-    ],
-    async spawn(root) {
-      const localBin = path.join(root, "node_modules", ".bin", "biome")
-      let bin: string | undefined
-      if (await Filesystem.exists(localBin)) bin = localBin
-      if (!bin) {
-        const found = which("biome")
-        if (found) bin = found
-      }
-
-      let args = ["lsp-proxy", "--stdio"]
-
-      if (!bin) {
-        const resolved = Module.resolve("biome", root)
-        if (!resolved) return
-        bin = await Npm.which("biome")
-        if (!bin) return
-        args = ["lsp-proxy", "--stdio"]
-      }
-
-      const proc = spawn(bin, args, {
-        cwd: root,
-        env: {
-          ...process.env,
-        },
-      })
+export const Biome: Info = {
+  id: "biome",
+  root: NearestRoot([
+    "biome.json",
+    "biome.jsonc",
+    "package-lock.json",
+    "bun.lockb",
+    "bun.lock",
+    "pnpm-lock.yaml",
+    "yarn.lock",
+  ]),
+  extensions: [
+    ".ts",
+    ".tsx",
+    ".js",
+    ".jsx",
+    ".mjs",
+    ".cjs",
+    ".mts",
+    ".cts",
+    ".json",
+    ".jsonc",
+    ".vue",
+    ".astro",
+    ".svelte",
+    ".css",
+    ".graphql",
+    ".gql",
+    ".html",
+  ],
+  async spawn(root) {
+    const localBin = path.join(root, "node_modules", ".bin", "biome")
+    let bin: string | undefined
+    if (await Filesystem.exists(localBin)) bin = localBin
+    if (!bin) {
+      const found = which("biome")
+      if (found) bin = found
+    }
 
-      return {
-        process: proc,
-      }
-    },
-  }
+    let args = ["lsp-proxy", "--stdio"]
 
-  export const Gopls: Info = {
-    id: "gopls",
-    root: async (file) => {
-      const work = await NearestRoot(["go.work"])(file)
-      if (work) return work
-      return NearestRoot(["go.mod", "go.sum"])(file)
-    },
-    extensions: [".go"],
-    async spawn(root) {
-      let bin = which("gopls")
-      if (!bin) {
-        if (!which("go")) return
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+    if (!bin) {
+      const resolved = Module.resolve("biome", root)
+      if (!resolved) return
+      bin = await Npm.which("biome")
+      if (!bin) return
+      args = ["lsp-proxy", "--stdio"]
+    }
 
-        log.info("installing gopls")
-        const proc = Process.spawn(["go", "install", "golang.org/x/tools/gopls@latest"], {
-          env: { ...process.env, GOBIN: Global.Path.bin },
-          stdout: "pipe",
-          stderr: "pipe",
-          stdin: "pipe",
-        })
-        const exit = await proc.exited
-        if (exit !== 0) {
-          log.error("Failed to install gopls")
-          return
-        }
-        bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : ""))
-        log.info(`installed gopls`, {
-          bin,
-        })
-      }
-      return {
-        process: spawn(bin!, {
-          cwd: root,
-        }),
-      }
-    },
-  }
+    const proc = spawn(bin, args, {
+      cwd: root,
+      env: {
+        ...process.env,
+      },
+    })
 
-  export const Rubocop: Info = {
-    id: "ruby-lsp",
-    root: NearestRoot(["Gemfile"]),
-    extensions: [".rb", ".rake", ".gemspec", ".ru"],
-    async spawn(root) {
-      let bin = which("rubocop")
-      if (!bin) {
-        const ruby = which("ruby")
-        const gem = which("gem")
-        if (!ruby || !gem) {
-          log.info("Ruby not found, please install Ruby first")
-          return
-        }
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        log.info("installing rubocop")
-        const proc = Process.spawn(["gem", "install", "rubocop", "--bindir", Global.Path.bin], {
-          stdout: "pipe",
-          stderr: "pipe",
-          stdin: "pipe",
-        })
-        const exit = await proc.exited
-        if (exit !== 0) {
-          log.error("Failed to install rubocop")
-          return
-        }
-        bin = path.join(Global.Path.bin, "rubocop" + (process.platform === "win32" ? ".exe" : ""))
-        log.info(`installed rubocop`, {
-          bin,
-        })
-      }
-      return {
-        process: spawn(bin!, ["--lsp"], {
-          cwd: root,
-        }),
-      }
-    },
-  }
+    return {
+      process: proc,
+    }
+  },
+}
 
-  export const Ty: Info = {
-    id: "ty",
-    extensions: [".py", ".pyi"],
-    root: NearestRoot([
-      "pyproject.toml",
-      "ty.toml",
-      "setup.py",
-      "setup.cfg",
-      "requirements.txt",
-      "Pipfile",
-      "pyrightconfig.json",
-    ]),
-    async spawn(root) {
-      if (!Flag.OPENCODE_EXPERIMENTAL_LSP_TY) {
-        return undefined
-      }
-
-      let binary = which("ty")
-
-      const initialization: Record<string, string> = {}
-
-      const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
-        (p): p is string => p !== undefined,
-      )
-      for (const venvPath of potentialVenvPaths) {
-        const isWindows = process.platform === "win32"
-        const potentialPythonPath = isWindows
-          ? path.join(venvPath, "Scripts", "python.exe")
-          : path.join(venvPath, "bin", "python")
-        if (await Filesystem.exists(potentialPythonPath)) {
-          initialization["pythonPath"] = potentialPythonPath
-          break
-        }
-      }
+export const Gopls: Info = {
+  id: "gopls",
+  root: async (file) => {
+    const work = await NearestRoot(["go.work"])(file)
+    if (work) return work
+    return NearestRoot(["go.mod", "go.sum"])(file)
+  },
+  extensions: [".go"],
+  async spawn(root) {
+    let bin = which("gopls")
+    if (!bin) {
+      if (!which("go")) return
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
 
-      if (!binary) {
-        for (const venvPath of potentialVenvPaths) {
-          const isWindows = process.platform === "win32"
-          const potentialTyPath = isWindows
-            ? path.join(venvPath, "Scripts", "ty.exe")
-            : path.join(venvPath, "bin", "ty")
-          if (await Filesystem.exists(potentialTyPath)) {
-            binary = potentialTyPath
-            break
-          }
-        }
+      log.info("installing gopls")
+      const proc = Process.spawn(["go", "install", "golang.org/x/tools/gopls@latest"], {
+        env: { ...process.env, GOBIN: Global.Path.bin },
+        stdout: "pipe",
+        stderr: "pipe",
+        stdin: "pipe",
+      })
+      const exit = await proc.exited
+      if (exit !== 0) {
+        log.error("Failed to install gopls")
+        return
       }
+      bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : ""))
+      log.info(`installed gopls`, {
+        bin,
+      })
+    }
+    return {
+      process: spawn(bin!, {
+        cwd: root,
+      }),
+    }
+  },
+}
 
-      if (!binary) {
-        log.error("ty not found, please install ty first")
+export const Rubocop: Info = {
+  id: "ruby-lsp",
+  root: NearestRoot(["Gemfile"]),
+  extensions: [".rb", ".rake", ".gemspec", ".ru"],
+  async spawn(root) {
+    let bin = which("rubocop")
+    if (!bin) {
+      const ruby = which("ruby")
+      const gem = which("gem")
+      if (!ruby || !gem) {
+        log.info("Ruby not found, please install Ruby first")
         return
       }
-
-      const proc = spawn(binary, ["server"], {
-        cwd: root,
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      log.info("installing rubocop")
+      const proc = Process.spawn(["gem", "install", "rubocop", "--bindir", Global.Path.bin], {
+        stdout: "pipe",
+        stderr: "pipe",
+        stdin: "pipe",
       })
-
-      return {
-        process: proc,
-        initialization,
+      const exit = await proc.exited
+      if (exit !== 0) {
+        log.error("Failed to install rubocop")
+        return
       }
-    },
-  }
+      bin = path.join(Global.Path.bin, "rubocop" + (process.platform === "win32" ? ".exe" : ""))
+      log.info(`installed rubocop`, {
+        bin,
+      })
+    }
+    return {
+      process: spawn(bin!, ["--lsp"], {
+        cwd: root,
+      }),
+    }
+  },
+}
 
-  export const Pyright: Info = {
-    id: "pyright",
-    extensions: [".py", ".pyi"],
-    root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
-    async spawn(root) {
-      let binary = which("pyright-langserver")
-      const args = []
-      if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        const resolved = await Npm.which("pyright")
-        if (!resolved) return
-        binary = resolved
-      }
-      args.push("--stdio")
+export const Ty: Info = {
+  id: "ty",
+  extensions: [".py", ".pyi"],
+  root: NearestRoot([
+    "pyproject.toml",
+    "ty.toml",
+    "setup.py",
+    "setup.cfg",
+    "requirements.txt",
+    "Pipfile",
+    "pyrightconfig.json",
+  ]),
+  async spawn(root) {
+    if (!Flag.OPENCODE_EXPERIMENTAL_LSP_TY) {
+      return undefined
+    }
 
-      const initialization: Record<string, string> = {}
+    let binary = which("ty")
 
-      const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
-        (p): p is string => p !== undefined,
-      )
+    const initialization: Record<string, string> = {}
+
+    const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
+      (p): p is string => p !== undefined,
+    )
+    for (const venvPath of potentialVenvPaths) {
+      const isWindows = process.platform === "win32"
+      const potentialPythonPath = isWindows
+        ? path.join(venvPath, "Scripts", "python.exe")
+        : path.join(venvPath, "bin", "python")
+      if (await Filesystem.exists(potentialPythonPath)) {
+        initialization["pythonPath"] = potentialPythonPath
+        break
+      }
+    }
+
+    if (!binary) {
       for (const venvPath of potentialVenvPaths) {
         const isWindows = process.platform === "win32"
-        const potentialPythonPath = isWindows
-          ? path.join(venvPath, "Scripts", "python.exe")
-          : path.join(venvPath, "bin", "python")
-        if (await Filesystem.exists(potentialPythonPath)) {
-          initialization["pythonPath"] = potentialPythonPath
+        const potentialTyPath = isWindows
+          ? path.join(venvPath, "Scripts", "ty.exe")
+          : path.join(venvPath, "bin", "ty")
+        if (await Filesystem.exists(potentialTyPath)) {
+          binary = potentialTyPath
           break
         }
       }
+    }
 
-      const proc = spawn(binary, args, {
-        cwd: root,
-        env: {
-          ...process.env,
-        },
-      })
-      return {
-        process: proc,
-        initialization,
-      }
-    },
-  }
-
-  export const ElixirLS: Info = {
-    id: "elixir-ls",
-    extensions: [".ex", ".exs"],
-    root: NearestRoot(["mix.exs", "mix.lock"]),
-    async spawn(root) {
-      let binary = which("elixir-ls")
-      if (!binary) {
-        const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
-        binary = path.join(
-          Global.Path.bin,
-          "elixir-ls-master",
-          "release",
-          process.platform === "win32" ? "language_server.bat" : "language_server.sh",
-        )
-
-        if (!(await Filesystem.exists(binary))) {
-          const elixir = which("elixir")
-          if (!elixir) {
-            log.error("elixir is required to run elixir-ls")
-            return
-          }
-
-          if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-          log.info("downloading elixir-ls from GitHub releases")
-
-          const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
-          if (!response.ok) return
-          const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
-          if (response.body) await Filesystem.writeStream(zipPath, response.body)
-
-          const ok = await Archive.extractZip(zipPath, Global.Path.bin)
-            .then(() => true)
-            .catch((error) => {
-              log.error("Failed to extract elixir-ls archive", { error })
-              return false
-            })
-          if (!ok) return
+    if (!binary) {
+      log.error("ty not found, please install ty first")
+      return
+    }
 
-          await fs.rm(zipPath, {
-            force: true,
-            recursive: true,
-          })
+    const proc = spawn(binary, ["server"], {
+      cwd: root,
+    })
 
-          const cwd = path.join(Global.Path.bin, "elixir-ls-master")
-          const env = { MIX_ENV: "prod", ...process.env }
-          await Process.run(["mix", "deps.get"], { cwd, env })
-          await Process.run(["mix", "compile"], { cwd, env })
-          await Process.run(["mix", "elixir_ls.release2", "-o", "release"], { cwd, env })
+    return {
+      process: proc,
+      initialization,
+    }
+  },
+}
 
-          log.info(`installed elixir-ls`, {
-            path: elixirLsPath,
-          })
-        }
+export const Pyright: Info = {
+  id: "pyright",
+  extensions: [".py", ".pyi"],
+  root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
+  async spawn(root) {
+    let binary = which("pyright-langserver")
+    const args = []
+    if (!binary) {
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      const resolved = await Npm.which("pyright")
+      if (!resolved) return
+      binary = resolved
+    }
+    args.push("--stdio")
+
+    const initialization: Record<string, string> = {}
+
+    const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
+      (p): p is string => p !== undefined,
+    )
+    for (const venvPath of potentialVenvPaths) {
+      const isWindows = process.platform === "win32"
+      const potentialPythonPath = isWindows
+        ? path.join(venvPath, "Scripts", "python.exe")
+        : path.join(venvPath, "bin", "python")
+      if (await Filesystem.exists(potentialPythonPath)) {
+        initialization["pythonPath"] = potentialPythonPath
+        break
       }
+    }
 
-      return {
-        process: spawn(binary, {
-          cwd: root,
-        }),
-      }
-    },
-  }
+    const proc = spawn(binary, args, {
+      cwd: root,
+      env: {
+        ...process.env,
+      },
+    })
+    return {
+      process: proc,
+      initialization,
+    }
+  },
+}
+
+export const ElixirLS: Info = {
+  id: "elixir-ls",
+  extensions: [".ex", ".exs"],
+  root: NearestRoot(["mix.exs", "mix.lock"]),
+  async spawn(root) {
+    let binary = which("elixir-ls")
+    if (!binary) {
+      const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
+      binary = path.join(
+        Global.Path.bin,
+        "elixir-ls-master",
+        "release",
+        process.platform === "win32" ? "language_server.bat" : "language_server.sh",
+      )
 
-  export const Zls: Info = {
-    id: "zls",
-    extensions: [".zig", ".zon"],
-    root: NearestRoot(["build.zig"]),
-    async spawn(root) {
-      let bin = which("zls")
-
-      if (!bin) {
-        const zig = which("zig")
-        if (!zig) {
-          log.error("Zig is required to use zls. Please install Zig first.")
+      if (!(await Filesystem.exists(binary))) {
+        const elixir = which("elixir")
+        if (!elixir) {
+          log.error("elixir is required to run elixir-ls")
           return
         }
 
         if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        log.info("downloading zls from GitHub releases")
+        log.info("downloading elixir-ls from GitHub releases")
 
-        const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
-        if (!releaseResponse.ok) {
-          log.error("Failed to fetch zls release info")
-          return
-        }
+        const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
+        if (!response.ok) return
+        const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
+        if (response.body) await Filesystem.writeStream(zipPath, response.body)
 
-        const release = (await releaseResponse.json()) as any
+        const ok = await Archive.extractZip(zipPath, Global.Path.bin)
+          .then(() => true)
+          .catch((error) => {
+            log.error("Failed to extract elixir-ls archive", { error })
+            return false
+          })
+        if (!ok) return
 
-        const platform = process.platform
-        const arch = process.arch
-        let assetName = ""
+        await fs.rm(zipPath, {
+          force: true,
+          recursive: true,
+        })
 
-        let zlsArch: string = arch
-        if (arch === "arm64") zlsArch = "aarch64"
-        else if (arch === "x64") zlsArch = "x86_64"
-        else if (arch === "ia32") zlsArch = "x86"
+        const cwd = path.join(Global.Path.bin, "elixir-ls-master")
+        const env = { MIX_ENV: "prod", ...process.env }
+        await Process.run(["mix", "deps.get"], { cwd, env })
+        await Process.run(["mix", "compile"], { cwd, env })
+        await Process.run(["mix", "elixir_ls.release2", "-o", "release"], { cwd, env })
 
-        let zlsPlatform: string = platform
-        if (platform === "darwin") zlsPlatform = "macos"
-        else if (platform === "win32") zlsPlatform = "windows"
+        log.info(`installed elixir-ls`, {
+          path: elixirLsPath,
+        })
+      }
+    }
 
-        const ext = platform === "win32" ? "zip" : "tar.xz"
+    return {
+      process: spawn(binary, {
+        cwd: root,
+      }),
+    }
+  },
+}
 
-        assetName = `zls-${zlsArch}-${zlsPlatform}.${ext}`
+export const Zls: Info = {
+  id: "zls",
+  extensions: [".zig", ".zon"],
+  root: NearestRoot(["build.zig"]),
+  async spawn(root) {
+    let bin = which("zls")
+
+    if (!bin) {
+      const zig = which("zig")
+      if (!zig) {
+        log.error("Zig is required to use zls. Please install Zig first.")
+        return
+      }
 
-        const supportedCombos = [
-          "zls-x86_64-linux.tar.xz",
-          "zls-x86_64-macos.tar.xz",
-          "zls-x86_64-windows.zip",
-          "zls-aarch64-linux.tar.xz",
-          "zls-aarch64-macos.tar.xz",
-          "zls-aarch64-windows.zip",
-          "zls-x86-linux.tar.xz",
-          "zls-x86-windows.zip",
-        ]
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      log.info("downloading zls from GitHub releases")
 
-        if (!supportedCombos.includes(assetName)) {
-          log.error(`Platform ${platform} and architecture ${arch} is not supported by zls`)
-          return
-        }
+      const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
+      if (!releaseResponse.ok) {
+        log.error("Failed to fetch zls release info")
+        return
+      }
 
-        const asset = release.assets.find((a: any) => a.name === assetName)
-        if (!asset) {
-          log.error(`Could not find asset ${assetName} in latest zls release`)
-          return
-        }
+      const release = (await releaseResponse.json()) as any
 
-        const downloadUrl = asset.browser_download_url
-        const downloadResponse = await fetch(downloadUrl)
-        if (!downloadResponse.ok) {
-          log.error("Failed to download zls")
-          return
-        }
+      const platform = process.platform
+      const arch = process.arch
+      let assetName = ""
+
+      let zlsArch: string = arch
+      if (arch === "arm64") zlsArch = "aarch64"
+      else if (arch === "x64") zlsArch = "x86_64"
+      else if (arch === "ia32") zlsArch = "x86"
+
+      let zlsPlatform: string = platform
+      if (platform === "darwin") zlsPlatform = "macos"
+      else if (platform === "win32") zlsPlatform = "windows"
+
+      const ext = platform === "win32" ? "zip" : "tar.xz"
+
+      assetName = `zls-${zlsArch}-${zlsPlatform}.${ext}`
+
+      const supportedCombos = [
+        "zls-x86_64-linux.tar.xz",
+        "zls-x86_64-macos.tar.xz",
+        "zls-x86_64-windows.zip",
+        "zls-aarch64-linux.tar.xz",
+        "zls-aarch64-macos.tar.xz",
+        "zls-aarch64-windows.zip",
+        "zls-x86-linux.tar.xz",
+        "zls-x86-windows.zip",
+      ]
+
+      if (!supportedCombos.includes(assetName)) {
+        log.error(`Platform ${platform} and architecture ${arch} is not supported by zls`)
+        return
+      }
 
-        const tempPath = path.join(Global.Path.bin, assetName)
-        if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
+      const asset = release.assets.find((a: any) => a.name === assetName)
+      if (!asset) {
+        log.error(`Could not find asset ${assetName} in latest zls release`)
+        return
+      }
 
-        if (ext === "zip") {
-          const ok = await Archive.extractZip(tempPath, Global.Path.bin)
-            .then(() => true)
-            .catch((error) => {
-              log.error("Failed to extract zls archive", { error })
-              return false
-            })
-          if (!ok) return
-        } else {
-          await run(["tar", "-xf", tempPath], { cwd: Global.Path.bin })
-        }
+      const downloadUrl = asset.browser_download_url
+      const downloadResponse = await fetch(downloadUrl)
+      if (!downloadResponse.ok) {
+        log.error("Failed to download zls")
+        return
+      }
 
-        await fs.rm(tempPath, { force: true })
+      const tempPath = path.join(Global.Path.bin, assetName)
+      if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
 
-        bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : ""))
+      if (ext === "zip") {
+        const ok = await Archive.extractZip(tempPath, Global.Path.bin)
+          .then(() => true)
+          .catch((error) => {
+            log.error("Failed to extract zls archive", { error })
+            return false
+          })
+        if (!ok) return
+      } else {
+        await run(["tar", "-xf", tempPath], { cwd: Global.Path.bin })
+      }
 
-        if (!(await Filesystem.exists(bin))) {
-          log.error("Failed to extract zls binary")
-          return
-        }
+      await fs.rm(tempPath, { force: true })
 
-        if (platform !== "win32") {
-          await fs.chmod(bin, 0o755).catch(() => {})
-        }
+      bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : ""))
 
-        log.info(`installed zls`, { bin })
+      if (!(await Filesystem.exists(bin))) {
+        log.error("Failed to extract zls binary")
+        return
       }
 
-      return {
-        process: spawn(bin, {
-          cwd: root,
-        }),
+      if (platform !== "win32") {
+        await fs.chmod(bin, 0o755).catch(() => {})
       }
-    },
-  }
 
-  export const CSharp: Info = {
-    id: "csharp",
-    root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
-    extensions: [".cs"],
-    async spawn(root) {
-      let bin = which("csharp-ls")
-      if (!bin) {
-        if (!which("dotnet")) {
-          log.error(".NET SDK is required to install csharp-ls")
-          return
-        }
+      log.info(`installed zls`, { bin })
+    }
 
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        log.info("installing csharp-ls via dotnet tool")
-        const proc = Process.spawn(["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin], {
-          stdout: "pipe",
-          stderr: "pipe",
-          stdin: "pipe",
-        })
-        const exit = await proc.exited
-        if (exit !== 0) {
-          log.error("Failed to install csharp-ls")
-          return
-        }
+    return {
+      process: spawn(bin, {
+        cwd: root,
+      }),
+    }
+  },
+}
 
-        bin = path.join(Global.Path.bin, "csharp-ls" + (process.platform === "win32" ? ".exe" : ""))
-        log.info(`installed csharp-ls`, { bin })
+export const CSharp: Info = {
+  id: "csharp",
+  root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
+  extensions: [".cs"],
+  async spawn(root) {
+    let bin = which("csharp-ls")
+    if (!bin) {
+      if (!which("dotnet")) {
+        log.error(".NET SDK is required to install csharp-ls")
+        return
       }
 
-      return {
-        process: spawn(bin, {
-          cwd: root,
-        }),
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      log.info("installing csharp-ls via dotnet tool")
+      const proc = Process.spawn(["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin], {
+        stdout: "pipe",
+        stderr: "pipe",
+        stdin: "pipe",
+      })
+      const exit = await proc.exited
+      if (exit !== 0) {
+        log.error("Failed to install csharp-ls")
+        return
       }
-    },
-  }
 
-  export const FSharp: Info = {
-    id: "fsharp",
-    root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]),
-    extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
-    async spawn(root) {
-      let bin = which("fsautocomplete")
-      if (!bin) {
-        if (!which("dotnet")) {
-          log.error(".NET SDK is required to install fsautocomplete")
-          return
-        }
+      bin = path.join(Global.Path.bin, "csharp-ls" + (process.platform === "win32" ? ".exe" : ""))
+      log.info(`installed csharp-ls`, { bin })
+    }
 
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        log.info("installing fsautocomplete via dotnet tool")
-        const proc = Process.spawn(["dotnet", "tool", "install", "fsautocomplete", "--tool-path", Global.Path.bin], {
-          stdout: "pipe",
-          stderr: "pipe",
-          stdin: "pipe",
-        })
-        const exit = await proc.exited
-        if (exit !== 0) {
-          log.error("Failed to install fsautocomplete")
-          return
-        }
+    return {
+      process: spawn(bin, {
+        cwd: root,
+      }),
+    }
+  },
+}
+
+export const FSharp: Info = {
+  id: "fsharp",
+  root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]),
+  extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
+  async spawn(root) {
+    let bin = which("fsautocomplete")
+    if (!bin) {
+      if (!which("dotnet")) {
+        log.error(".NET SDK is required to install fsautocomplete")
+        return
+      }
 
-        bin = path.join(Global.Path.bin, "fsautocomplete" + (process.platform === "win32" ? ".exe" : ""))
-        log.info(`installed fsautocomplete`, { bin })
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      log.info("installing fsautocomplete via dotnet tool")
+      const proc = Process.spawn(["dotnet", "tool", "install", "fsautocomplete", "--tool-path", Global.Path.bin], {
+        stdout: "pipe",
+        stderr: "pipe",
+        stdin: "pipe",
+      })
+      const exit = await proc.exited
+      if (exit !== 0) {
+        log.error("Failed to install fsautocomplete")
+        return
       }
 
+      bin = path.join(Global.Path.bin, "fsautocomplete" + (process.platform === "win32" ? ".exe" : ""))
+      log.info(`installed fsautocomplete`, { bin })
+    }
+
+    return {
+      process: spawn(bin, {
+        cwd: root,
+      }),
+    }
+  },
+}
+
+export const SourceKit: Info = {
+  id: "sourcekit-lsp",
+  extensions: [".swift", ".objc", "objcpp"],
+  root: NearestRoot(["Package.swift", "*.xcodeproj", "*.xcworkspace"]),
+  async spawn(root) {
+    // Check if sourcekit-lsp is available in the PATH
+    // This is installed with the Swift toolchain
+    const sourcekit = which("sourcekit-lsp")
+    if (sourcekit) {
       return {
-        process: spawn(bin, {
+        process: spawn(sourcekit, {
           cwd: root,
         }),
       }
-    },
-  }
+    }
 
-  export const SourceKit: Info = {
-    id: "sourcekit-lsp",
-    extensions: [".swift", ".objc", "objcpp"],
-    root: NearestRoot(["Package.swift", "*.xcodeproj", "*.xcworkspace"]),
-    async spawn(root) {
-      // Check if sourcekit-lsp is available in the PATH
-      // This is installed with the Swift toolchain
-      const sourcekit = which("sourcekit-lsp")
-      if (sourcekit) {
-        return {
-          process: spawn(sourcekit, {
-            cwd: root,
-          }),
-        }
-      }
+    // If sourcekit-lsp not found, check if xcrun is available
+    // This is specific to macOS where sourcekit-lsp is typically installed with Xcode
+    if (!which("xcrun")) return
 
-      // If sourcekit-lsp not found, check if xcrun is available
-      // This is specific to macOS where sourcekit-lsp is typically installed with Xcode
-      if (!which("xcrun")) return
+    const lspLoc = await output(["xcrun", "--find", "sourcekit-lsp"])
 
-      const lspLoc = await output(["xcrun", "--find", "sourcekit-lsp"])
+    if (lspLoc.code !== 0) return
 
-      if (lspLoc.code !== 0) return
+    const bin = lspLoc.text.trim()
 
-      const bin = lspLoc.text.trim()
+    return {
+      process: spawn(bin, {
+        cwd: root,
+      }),
+    }
+  },
+}
 
-      return {
-        process: spawn(bin, {
-          cwd: root,
-        }),
-      }
-    },
-  }
+export const RustAnalyzer: Info = {
+  id: "rust",
+  root: async (root) => {
+    const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root)
+    if (crateRoot === undefined) {
+      return undefined
+    }
+    let currentDir = crateRoot
 
-  export const RustAnalyzer: Info = {
-    id: "rust",
-    root: async (root) => {
-      const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root)
-      if (crateRoot === undefined) {
-        return undefined
-      }
-      let currentDir = crateRoot
-
-      while (currentDir !== path.dirname(currentDir)) {
-        // Stop at filesystem root
-        const cargoTomlPath = path.join(currentDir, "Cargo.toml")
-        try {
-          const cargoTomlContent = await Filesystem.readText(cargoTomlPath)
-          if (cargoTomlContent.includes("[workspace]")) {
-            return currentDir
-          }
-        } catch {
-          // File doesn't exist or can't be read, continue searching up
+    while (currentDir !== path.dirname(currentDir)) {
+      // Stop at filesystem root
+      const cargoTomlPath = path.join(currentDir, "Cargo.toml")
+      try {
+        const cargoTomlContent = await Filesystem.readText(cargoTomlPath)
+        if (cargoTomlContent.includes("[workspace]")) {
+          return currentDir
         }
+      } catch {
+        // File doesn't exist or can't be read, continue searching up
+      }
 
-        const parentDir = path.dirname(currentDir)
-        if (parentDir === currentDir) break // Reached filesystem root
-        currentDir = parentDir
+      const parentDir = path.dirname(currentDir)
+      if (parentDir === currentDir) break // Reached filesystem root
+      currentDir = parentDir
 
-        // Stop if we've gone above the app root
-        if (!currentDir.startsWith(Instance.worktree)) break
-      }
+      // Stop if we've gone above the app root
+      if (!currentDir.startsWith(Instance.worktree)) break
+    }
 
-      return crateRoot
-    },
-    extensions: [".rs"],
-    async spawn(root) {
-      const bin = which("rust-analyzer")
-      if (!bin) {
-        log.info("rust-analyzer not found in path, please install it")
-        return
+    return crateRoot
+  },
+  extensions: [".rs"],
+  async spawn(root) {
+    const bin = which("rust-analyzer")
+    if (!bin) {
+      log.info("rust-analyzer not found in path, please install it")
+      return
+    }
+    return {
+      process: spawn(bin, {
+        cwd: root,
+      }),
+    }
+  },
+}
+
+export const Clangd: Info = {
+  id: "clangd",
+  root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd"]),
+  extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
+  async spawn(root) {
+    const args = ["--background-index", "--clang-tidy"]
+    const fromPath = which("clangd")
+    if (fromPath) {
+      return {
+        process: spawn(fromPath, args, {
+          cwd: root,
+        }),
       }
+    }
+
+    const ext = process.platform === "win32" ? ".exe" : ""
+    const direct = path.join(Global.Path.bin, "clangd" + ext)
+    if (await Filesystem.exists(direct)) {
       return {
-        process: spawn(bin, {
+        process: spawn(direct, args, {
           cwd: root,
         }),
       }
-    },
-  }
+    }
 
-  export const Clangd: Info = {
-    id: "clangd",
-    root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd"]),
-    extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
-    async spawn(root) {
-      const args = ["--background-index", "--clang-tidy"]
-      const fromPath = which("clangd")
-      if (fromPath) {
+    const entries = await fs.readdir(Global.Path.bin, { withFileTypes: true }).catch(() => [])
+    for (const entry of entries) {
+      if (!entry.isDirectory()) continue
+      if (!entry.name.startsWith("clangd_")) continue
+      const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext)
+      if (await Filesystem.exists(candidate)) {
         return {
-          process: spawn(fromPath, args, {
+          process: spawn(candidate, args, {
             cwd: root,
           }),
         }
       }
+    }
 
-      const ext = process.platform === "win32" ? ".exe" : ""
-      const direct = path.join(Global.Path.bin, "clangd" + ext)
-      if (await Filesystem.exists(direct)) {
-        return {
-          process: spawn(direct, args, {
-            cwd: root,
-          }),
-        }
+    if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+    log.info("downloading clangd from GitHub releases")
+
+    const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
+    if (!releaseResponse.ok) {
+      log.error("Failed to fetch clangd release info")
+      return
+    }
+
+    const release: {
+      tag_name?: string
+      assets?: { name?: string; browser_download_url?: string }[]
+    } = await releaseResponse.json()
+
+    const tag = release.tag_name
+    if (!tag) {
+      log.error("clangd release did not include a tag name")
+      return
+    }
+    const platform = process.platform
+    const tokens: Record<string, string> = {
+      darwin: "mac",
+      linux: "linux",
+      win32: "windows",
+    }
+    const token = tokens[platform]
+    if (!token) {
+      log.error(`Platform ${platform} is not supported by clangd auto-download`)
+      return
+    }
+
+    const assets = release.assets ?? []
+    const valid = (item: { name?: string; browser_download_url?: string }) => {
+      if (!item.name) return false
+      if (!item.browser_download_url) return false
+      if (!item.name.includes(token)) return false
+      return item.name.includes(tag)
+    }
+
+    const asset =
+      assets.find((item) => valid(item) && item.name?.endsWith(".zip")) ??
+      assets.find((item) => valid(item) && item.name?.endsWith(".tar.xz")) ??
+      assets.find((item) => valid(item))
+    if (!asset?.name || !asset.browser_download_url) {
+      log.error("clangd could not match release asset", { tag, platform })
+      return
+    }
+
+    const name = asset.name
+    const downloadResponse = await fetch(asset.browser_download_url)
+    if (!downloadResponse.ok) {
+      log.error("Failed to download clangd")
+      return
+    }
+
+    const archive = path.join(Global.Path.bin, name)
+    const buf = await downloadResponse.arrayBuffer()
+    if (buf.byteLength === 0) {
+      log.error("Failed to write clangd archive")
+      return
+    }
+    await Filesystem.write(archive, Buffer.from(buf))
+
+    const zip = name.endsWith(".zip")
+    const tar = name.endsWith(".tar.xz")
+    if (!zip && !tar) {
+      log.error("clangd encountered unsupported asset", { asset: name })
+      return
+    }
+
+    if (zip) {
+      const ok = await Archive.extractZip(archive, Global.Path.bin)
+        .then(() => true)
+        .catch((error) => {
+          log.error("Failed to extract clangd archive", { error })
+          return false
+        })
+      if (!ok) return
+    }
+    if (tar) {
+      await run(["tar", "-xf", archive], { cwd: Global.Path.bin })
+    }
+    await fs.rm(archive, { force: true })
+
+    const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext)
+    if (!(await Filesystem.exists(bin))) {
+      log.error("Failed to extract clangd binary")
+      return
+    }
+
+    if (platform !== "win32") {
+      await fs.chmod(bin, 0o755).catch(() => {})
+    }
+
+    await fs.unlink(path.join(Global.Path.bin, "clangd")).catch(() => {})
+    await fs.symlink(bin, path.join(Global.Path.bin, "clangd")).catch(() => {})
+
+    log.info(`installed clangd`, { bin })
+
+    return {
+      process: spawn(bin, args, {
+        cwd: root,
+      }),
+    }
+  },
+}
+
+export const Svelte: Info = {
+  id: "svelte",
+  extensions: [".svelte"],
+  root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
+  async spawn(root) {
+    let binary = which("svelteserver")
+    const args: string[] = []
+    if (!binary) {
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      const resolved = await Npm.which("svelte-language-server")
+      if (!resolved) return
+      binary = resolved
+    }
+    args.push("--stdio")
+    const proc = spawn(binary, args, {
+      cwd: root,
+      env: {
+        ...process.env,
+      },
+    })
+    return {
+      process: proc,
+      initialization: {},
+    }
+  },
+}
+
+export const Astro: Info = {
+  id: "astro",
+  extensions: [".astro"],
+  root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
+  async spawn(root) {
+    const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory)
+    if (!tsserver) {
+      log.info("typescript not found, required for Astro language server")
+      return
+    }
+    const tsdk = path.dirname(tsserver)
+
+    let binary = which("astro-ls")
+    const args: string[] = []
+    if (!binary) {
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      const resolved = await Npm.which("@astrojs/language-server")
+      if (!resolved) return
+      binary = resolved
+    }
+    args.push("--stdio")
+    const proc = spawn(binary, args, {
+      cwd: root,
+      env: {
+        ...process.env,
+      },
+    })
+    return {
+      process: proc,
+      initialization: {
+        typescript: {
+          tsdk,
+        },
+      },
+    }
+  },
+}
+
+export const JDTLS: Info = {
+  id: "jdtls",
+  root: async (file) => {
+    // Without exclusions, NearestRoot defaults to instance directory so we can't
+    // distinguish between a) no project found and b) project found at instance dir.
+    // So we can't choose the root from (potential) monorepo markers first.
+    // Look for potential subproject markers first while excluding potential monorepo markers.
+    const settingsMarkers = ["settings.gradle", "settings.gradle.kts"]
+    const gradleMarkers = ["gradlew", "gradlew.bat"]
+    const exclusionsForMonorepos = gradleMarkers.concat(settingsMarkers)
+
+    const [projectRoot, wrapperRoot, settingsRoot] = await Promise.all([
+      NearestRoot(
+        ["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"],
+        exclusionsForMonorepos,
+      )(file),
+      NearestRoot(gradleMarkers, settingsMarkers)(file),
+      NearestRoot(settingsMarkers)(file),
+    ])
+
+    // If projectRoot is undefined we know we are in a monorepo or no project at all.
+    // So can safely fall through to the other roots
+    if (projectRoot) return projectRoot
+    if (wrapperRoot) return wrapperRoot
+    if (settingsRoot) return settingsRoot
+  },
+  extensions: [".java"],
+  async spawn(root) {
+    const java = which("java")
+    if (!java) {
+      log.error("Java 21 or newer is required to run the JDTLS. Please install it first.")
+      return
+    }
+    const javaMajorVersion = await run(["java", "-version"]).then((result) => {
+      const m = /"(\d+)\.\d+\.\d+"/.exec(result.stderr.toString())
+      return !m ? undefined : parseInt(m[1])
+    })
+    if (javaMajorVersion == null || javaMajorVersion < 21) {
+      log.error("JDTLS requires at least Java 21.")
+      return
+    }
+    const distPath = path.join(Global.Path.bin, "jdtls")
+    const launcherDir = path.join(distPath, "plugins")
+    const installed = await pathExists(launcherDir)
+    if (!installed) {
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      log.info("Downloading JDTLS LSP server.")
+      await fs.mkdir(distPath, { recursive: true })
+      const releaseURL =
+        "https://www.eclipse.org/downloads/download.php?file=/jdtls/snapshots/jdt-language-server-latest.tar.gz"
+      const archiveName = "release.tar.gz"
+
+      log.info("Downloading JDTLS archive", { url: releaseURL, dest: distPath })
+      const download = await fetch(releaseURL)
+      if (!download.ok || !download.body) {
+        log.error("Failed to download JDTLS", { status: download.status, statusText: download.statusText })
+        return
       }
+      await Filesystem.writeStream(path.join(distPath, archiveName), download.body)
 
-      const entries = await fs.readdir(Global.Path.bin, { withFileTypes: true }).catch(() => [])
-      for (const entry of entries) {
-        if (!entry.isDirectory()) continue
-        if (!entry.name.startsWith("clangd_")) continue
-        const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext)
-        if (await Filesystem.exists(candidate)) {
-          return {
-            process: spawn(candidate, args, {
-              cwd: root,
-            }),
-          }
-        }
+      log.info("Extracting JDTLS archive")
+      const tarResult = await run(["tar", "-xzf", archiveName], { cwd: distPath })
+      if (tarResult.code !== 0) {
+        log.error("Failed to extract JDTLS", { exitCode: tarResult.code, stderr: tarResult.stderr.toString() })
+        return
       }
 
+      await fs.rm(path.join(distPath, archiveName), { force: true })
+      log.info("JDTLS download and extraction completed")
+    }
+    const jarFileName =
+      (await fs.readdir(launcherDir).catch(() => []))
+        .find((item) => /^org\.eclipse\.equinox\.launcher_.*\.jar$/.test(item))
+        ?.trim() ?? ""
+    const launcherJar = path.join(launcherDir, jarFileName)
+    if (!(await pathExists(launcherJar))) {
+      log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`)
+      return
+    }
+    const configFile = path.join(
+      distPath,
+      (() => {
+        switch (process.platform) {
+          case "darwin":
+            return "config_mac"
+          case "linux":
+            return "config_linux"
+          case "win32":
+            return "config_win"
+          default:
+            return "config_linux"
+        }
+      })(),
+    )
+    const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-jdtls-data"))
+    return {
+      process: spawn(
+        java,
+        [
+          "-jar",
+          launcherJar,
+          "-configuration",
+          configFile,
+          "-data",
+          dataDir,
+          "-Declipse.application=org.eclipse.jdt.ls.core.id1",
+          "-Dosgi.bundles.defaultStartLevel=4",
+          "-Declipse.product=org.eclipse.jdt.ls.core.product",
+          "-Dlog.level=ALL",
+          "--add-modules=ALL-SYSTEM",
+          "--add-opens java.base/java.util=ALL-UNNAMED",
+          "--add-opens java.base/java.lang=ALL-UNNAMED",
+        ],
+        {
+          cwd: root,
+        },
+      ),
+    }
+  },
+}
+
+export const KotlinLS: Info = {
+  id: "kotlin-ls",
+  extensions: [".kt", ".kts"],
+  root: async (file) => {
+    // 1) Nearest Gradle root (multi-project or included build)
+    const settingsRoot = await NearestRoot(["settings.gradle.kts", "settings.gradle"])(file)
+    if (settingsRoot) return settingsRoot
+    // 2) Gradle wrapper (strong root signal)
+    const wrapperRoot = await NearestRoot(["gradlew", "gradlew.bat"])(file)
+    if (wrapperRoot) return wrapperRoot
+    // 3) Single-project or module-level build
+    const buildRoot = await NearestRoot(["build.gradle.kts", "build.gradle"])(file)
+    if (buildRoot) return buildRoot
+    // 4) Maven fallback
+    return NearestRoot(["pom.xml"])(file)
+  },
+  async spawn(root) {
+    const distPath = path.join(Global.Path.bin, "kotlin-ls")
+    const launcherScript =
+      process.platform === "win32" ? path.join(distPath, "kotlin-lsp.cmd") : path.join(distPath, "kotlin-lsp.sh")
+    const installed = await Filesystem.exists(launcherScript)
+    if (!installed) {
       if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-      log.info("downloading clangd from GitHub releases")
+      log.info("Downloading Kotlin Language Server from GitHub.")
 
-      const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
+      const releaseResponse = await fetch("https://api.github.com/repos/Kotlin/kotlin-lsp/releases/latest")
       if (!releaseResponse.ok) {
-        log.error("Failed to fetch clangd release info")
+        log.error("Failed to fetch kotlin-lsp release info")
         return
       }
 
-      const release: {
-        tag_name?: string
-        assets?: { name?: string; browser_download_url?: string }[]
-      } = await releaseResponse.json()
+      const release = await releaseResponse.json()
+      const version = release.name?.replace(/^v/, "")
 
-      const tag = release.tag_name
-      if (!tag) {
-        log.error("clangd release did not include a tag name")
+      if (!version) {
+        log.error("Could not determine Kotlin LSP version from release")
         return
       }
+
       const platform = process.platform
-      const tokens: Record<string, string> = {
-        darwin: "mac",
-        linux: "linux",
-        win32: "windows",
-      }
-      const token = tokens[platform]
-      if (!token) {
-        log.error(`Platform ${platform} is not supported by clangd auto-download`)
+      const arch = process.arch
+
+      let kotlinArch: string = arch
+      if (arch === "arm64") kotlinArch = "aarch64"
+      else if (arch === "x64") kotlinArch = "x64"
+
+      let kotlinPlatform: string = platform
+      if (platform === "darwin") kotlinPlatform = "mac"
+      else if (platform === "linux") kotlinPlatform = "linux"
+      else if (platform === "win32") kotlinPlatform = "win"
+
+      const supportedCombos = ["mac-x64", "mac-aarch64", "linux-x64", "linux-aarch64", "win-x64", "win-aarch64"]
+
+      const combo = `${kotlinPlatform}-${kotlinArch}`
+
+      if (!supportedCombos.includes(combo)) {
+        log.error(`Platform ${platform}/${arch} is not supported by Kotlin LSP`)
         return
       }
 
-      const assets = release.assets ?? []
-      const valid = (item: { name?: string; browser_download_url?: string }) => {
-        if (!item.name) return false
-        if (!item.browser_download_url) return false
-        if (!item.name.includes(token)) return false
-        return item.name.includes(tag)
-      }
-
-      const asset =
-        assets.find((item) => valid(item) && item.name?.endsWith(".zip")) ??
-        assets.find((item) => valid(item) && item.name?.endsWith(".tar.xz")) ??
-        assets.find((item) => valid(item))
-      if (!asset?.name || !asset.browser_download_url) {
-        log.error("clangd could not match release asset", { tag, platform })
+      const assetName = `kotlin-lsp-${version}-${kotlinPlatform}-${kotlinArch}.zip`
+      const releaseURL = `https://download-cdn.jetbrains.com/kotlin-lsp/${version}/${assetName}`
+
+      await fs.mkdir(distPath, { recursive: true })
+      const archivePath = path.join(distPath, "kotlin-ls.zip")
+      const download = await fetch(releaseURL)
+      if (!download.ok || !download.body) {
+        log.error("Failed to download Kotlin Language Server", {
+          status: download.status,
+          statusText: download.statusText,
+        })
         return
       }
+      await Filesystem.writeStream(archivePath, download.body)
+      const ok = await Archive.extractZip(archivePath, distPath)
+        .then(() => true)
+        .catch((error) => {
+          log.error("Failed to extract Kotlin LS archive", { error })
+          return false
+        })
+      if (!ok) return
+      await fs.rm(archivePath, { force: true })
+      if (process.platform !== "win32") {
+        await fs.chmod(launcherScript, 0o755).catch(() => {})
+      }
+      log.info("Installed Kotlin Language Server", { path: launcherScript })
+    }
+    if (!(await Filesystem.exists(launcherScript))) {
+      log.error(`Failed to locate the Kotlin LS launcher script in the installed directory: ${distPath}.`)
+      return
+    }
+    return {
+      process: spawn(launcherScript, ["--stdio"], {
+        cwd: root,
+      }),
+    }
+  },
+}
 
-      const name = asset.name
-      const downloadResponse = await fetch(asset.browser_download_url)
-      if (!downloadResponse.ok) {
-        log.error("Failed to download clangd")
+export const YamlLS: Info = {
+  id: "yaml-ls",
+  extensions: [".yaml", ".yml"],
+  root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
+  async spawn(root) {
+    let binary = which("yaml-language-server")
+    const args: string[] = []
+    if (!binary) {
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      const resolved = await Npm.which("yaml-language-server")
+      if (!resolved) return
+      binary = resolved
+    }
+    args.push("--stdio")
+    const proc = spawn(binary, args, {
+      cwd: root,
+      env: {
+        ...process.env,
+      },
+    })
+    return {
+      process: proc,
+    }
+  },
+}
+
+export const LuaLS: Info = {
+  id: "lua-ls",
+  root: NearestRoot([
+    ".luarc.json",
+    ".luarc.jsonc",
+    ".luacheckrc",
+    ".stylua.toml",
+    "stylua.toml",
+    "selene.toml",
+    "selene.yml",
+  ]),
+  extensions: [".lua"],
+  async spawn(root) {
+    let bin = which("lua-language-server")
+
+    if (!bin) {
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      log.info("downloading lua-language-server from GitHub releases")
+
+      const releaseResponse = await fetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest")
+      if (!releaseResponse.ok) {
+        log.error("Failed to fetch lua-language-server release info")
+        return
+      }
+
+      const release = await releaseResponse.json()
+
+      const platform = process.platform
+      const arch = process.arch
+      let assetName = ""
+
+      let lualsArch: string = arch
+      if (arch === "arm64") lualsArch = "arm64"
+      else if (arch === "x64") lualsArch = "x64"
+      else if (arch === "ia32") lualsArch = "ia32"
+
+      let lualsPlatform: string = platform
+      if (platform === "darwin") lualsPlatform = "darwin"
+      else if (platform === "linux") lualsPlatform = "linux"
+      else if (platform === "win32") lualsPlatform = "win32"
+
+      const ext = platform === "win32" ? "zip" : "tar.gz"
+
+      assetName = `lua-language-server-${release.tag_name}-${lualsPlatform}-${lualsArch}.${ext}`
+
+      const supportedCombos = [
+        "darwin-arm64.tar.gz",
+        "darwin-x64.tar.gz",
+        "linux-x64.tar.gz",
+        "linux-arm64.tar.gz",
+        "win32-x64.zip",
+        "win32-ia32.zip",
+      ]
+
+      const assetSuffix = `${lualsPlatform}-${lualsArch}.${ext}`
+      if (!supportedCombos.includes(assetSuffix)) {
+        log.error(`Platform ${platform} and architecture ${arch} is not supported by lua-language-server`)
         return
       }
 
-      const archive = path.join(Global.Path.bin, name)
-      const buf = await downloadResponse.arrayBuffer()
-      if (buf.byteLength === 0) {
-        log.error("Failed to write clangd archive")
+      const asset = release.assets.find((a: any) => a.name === assetName)
+      if (!asset) {
+        log.error(`Could not find asset ${assetName} in latest lua-language-server release`)
         return
       }
-      await Filesystem.write(archive, Buffer.from(buf))
 
-      const zip = name.endsWith(".zip")
-      const tar = name.endsWith(".tar.xz")
-      if (!zip && !tar) {
-        log.error("clangd encountered unsupported asset", { asset: name })
+      const downloadUrl = asset.browser_download_url
+      const downloadResponse = await fetch(downloadUrl)
+      if (!downloadResponse.ok) {
+        log.error("Failed to download lua-language-server")
         return
       }
 
-      if (zip) {
-        const ok = await Archive.extractZip(archive, Global.Path.bin)
+      const tempPath = path.join(Global.Path.bin, assetName)
+      if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
+
+      // Unlike zls which is a single self-contained binary,
+      // lua-language-server needs supporting files (meta/, locale/, etc.)
+      // Extract entire archive to dedicated directory to preserve all files
+      const installDir = path.join(Global.Path.bin, `lua-language-server-${lualsArch}-${lualsPlatform}`)
+
+      // Remove old installation if exists
+      const stats = await fs.stat(installDir).catch(() => undefined)
+      if (stats) {
+        await fs.rm(installDir, { force: true, recursive: true })
+      }
+
+      await fs.mkdir(installDir, { recursive: true })
+
+      if (ext === "zip") {
+        const ok = await Archive.extractZip(tempPath, installDir)
           .then(() => true)
           .catch((error) => {
-            log.error("Failed to extract clangd archive", { error })
+            log.error("Failed to extract lua-language-server archive", { error })
+            return false
+          })
+        if (!ok) return
+      } else {
+        const ok = await run(["tar", "-xzf", tempPath, "-C", installDir])
+          .then((result) => result.code === 0)
+          .catch((error: unknown) => {
+            log.error("Failed to extract lua-language-server archive", { error })
             return false
           })
         if (!ok) return
       }
-      if (tar) {
-        await run(["tar", "-xf", archive], { cwd: Global.Path.bin })
-      }
-      await fs.rm(archive, { force: true })
 
-      const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext)
+      await fs.rm(tempPath, { force: true })
+
+      // Binary is located in bin/ subdirectory within the extracted archive
+      bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : ""))
+
       if (!(await Filesystem.exists(bin))) {
-        log.error("Failed to extract clangd binary")
+        log.error("Failed to extract lua-language-server binary")
         return
       }
 
       if (platform !== "win32") {
-        await fs.chmod(bin, 0o755).catch(() => {})
+        const ok = await fs
+          .chmod(bin, 0o755)
+          .then(() => true)
+          .catch((error: unknown) => {
+            log.error("Failed to set executable permission for lua-language-server binary", {
+              error,
+            })
+            return false
+          })
+        if (!ok) return
       }
 
-      await fs.unlink(path.join(Global.Path.bin, "clangd")).catch(() => {})
-      await fs.symlink(bin, path.join(Global.Path.bin, "clangd")).catch(() => {})
+      log.info(`installed lua-language-server`, { bin })
+    }
 
-      log.info(`installed clangd`, { bin })
+    return {
+      process: spawn(bin, {
+        cwd: root,
+      }),
+    }
+  },
+}
 
-      return {
-        process: spawn(bin, args, {
-          cwd: root,
-        }),
-      }
-    },
-  }
+export const PHPIntelephense: Info = {
+  id: "php intelephense",
+  extensions: [".php"],
+  root: NearestRoot(["composer.json", "composer.lock", ".php-version"]),
+  async spawn(root) {
+    let binary = which("intelephense")
+    const args: string[] = []
+    if (!binary) {
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      const resolved = await Npm.which("intelephense")
+      if (!resolved) return
+      binary = resolved
+    }
+    args.push("--stdio")
+    const proc = spawn(binary, args, {
+      cwd: root,
+      env: {
+        ...process.env,
+      },
+    })
+    return {
+      process: proc,
+      initialization: {
+        telemetry: {
+          enabled: false,
+        },
+      },
+    }
+  },
+}
 
-  export const Svelte: Info = {
-    id: "svelte",
-    extensions: [".svelte"],
-    root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
-    async spawn(root) {
-      let binary = which("svelteserver")
-      const args: string[] = []
-      if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        const resolved = await Npm.which("svelte-language-server")
-        if (!resolved) return
-        binary = resolved
-      }
-      args.push("--stdio")
-      const proc = spawn(binary, args, {
+export const Prisma: Info = {
+  id: "prisma",
+  extensions: [".prisma"],
+  root: NearestRoot(["schema.prisma", "prisma/schema.prisma", "prisma"], ["package.json"]),
+  async spawn(root) {
+    const prisma = which("prisma")
+    if (!prisma) {
+      log.info("prisma not found, please install prisma")
+      return
+    }
+    return {
+      process: spawn(prisma, ["language-server"], {
         cwd: root,
-        env: {
-          ...process.env,
-        },
-      })
-      return {
-        process: proc,
-        initialization: {},
-      }
-    },
-  }
+      }),
+    }
+  },
+}
+
+export const Dart: Info = {
+  id: "dart",
+  extensions: [".dart"],
+  root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]),
+  async spawn(root) {
+    const dart = which("dart")
+    if (!dart) {
+      log.info("dart not found, please install dart first")
+      return
+    }
+    return {
+      process: spawn(dart, ["language-server", "--lsp"], {
+        cwd: root,
+      }),
+    }
+  },
+}
+
+export const Ocaml: Info = {
+  id: "ocaml-lsp",
+  extensions: [".ml", ".mli"],
+  root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"]),
+  async spawn(root) {
+    const bin = which("ocamllsp")
+    if (!bin) {
+      log.info("ocamllsp not found, please install ocaml-lsp-server")
+      return
+    }
+    return {
+      process: spawn(bin, {
+        cwd: root,
+      }),
+    }
+  },
+}
+export const BashLS: Info = {
+  id: "bash",
+  extensions: [".sh", ".bash", ".zsh", ".ksh"],
+  root: async () => Instance.directory,
+  async spawn(root) {
+    let binary = which("bash-language-server")
+    const args: string[] = []
+    if (!binary) {
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      const resolved = await Npm.which("bash-language-server")
+      if (!resolved) return
+      binary = resolved
+    }
+    args.push("start")
+    const proc = spawn(binary, args, {
+      cwd: root,
+      env: {
+        ...process.env,
+      },
+    })
+    return {
+      process: proc,
+    }
+  },
+}
+
+export const TerraformLS: Info = {
+  id: "terraform",
+  extensions: [".tf", ".tfvars"],
+  root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]),
+  async spawn(root) {
+    let bin = which("terraform-ls")
+
+    if (!bin) {
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      log.info("downloading terraform-ls from HashiCorp releases")
 
-  export const Astro: Info = {
-    id: "astro",
-    extensions: [".astro"],
-    root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
-    async spawn(root) {
-      const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory)
-      if (!tsserver) {
-        log.info("typescript not found, required for Astro language server")
+      const releaseResponse = await fetch("https://api.releases.hashicorp.com/v1/releases/terraform-ls/latest")
+      if (!releaseResponse.ok) {
+        log.error("Failed to fetch terraform-ls release info")
         return
       }
-      const tsdk = path.dirname(tsserver)
 
-      let binary = which("astro-ls")
-      const args: string[] = []
-      if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        const resolved = await Npm.which("@astrojs/language-server")
-        if (!resolved) return
-        binary = resolved
+      const release = (await releaseResponse.json()) as {
+        version?: string
+        builds?: { arch?: string; os?: string; url?: string }[]
       }
-      args.push("--stdio")
-      const proc = spawn(binary, args, {
-        cwd: root,
-        env: {
-          ...process.env,
-        },
-      })
-      return {
-        process: proc,
-        initialization: {
-          typescript: {
-            tsdk,
-          },
-        },
-      }
-    },
-  }
 
-  export const JDTLS: Info = {
-    id: "jdtls",
-    root: async (file) => {
-      // Without exclusions, NearestRoot defaults to instance directory so we can't
-      // distinguish between a) no project found and b) project found at instance dir.
-      // So we can't choose the root from (potential) monorepo markers first.
-      // Look for potential subproject markers first while excluding potential monorepo markers.
-      const settingsMarkers = ["settings.gradle", "settings.gradle.kts"]
-      const gradleMarkers = ["gradlew", "gradlew.bat"]
-      const exclusionsForMonorepos = gradleMarkers.concat(settingsMarkers)
-
-      const [projectRoot, wrapperRoot, settingsRoot] = await Promise.all([
-        NearestRoot(
-          ["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"],
-          exclusionsForMonorepos,
-        )(file),
-        NearestRoot(gradleMarkers, settingsMarkers)(file),
-        NearestRoot(settingsMarkers)(file),
-      ])
-
-      // If projectRoot is undefined we know we are in a monorepo or no project at all.
-      // So can safely fall through to the other roots
-      if (projectRoot) return projectRoot
-      if (wrapperRoot) return wrapperRoot
-      if (settingsRoot) return settingsRoot
-    },
-    extensions: [".java"],
-    async spawn(root) {
-      const java = which("java")
-      if (!java) {
-        log.error("Java 21 or newer is required to run the JDTLS. Please install it first.")
-        return
-      }
-      const javaMajorVersion = await run(["java", "-version"]).then((result) => {
-        const m = /"(\d+)\.\d+\.\d+"/.exec(result.stderr.toString())
-        return !m ? undefined : parseInt(m[1])
-      })
-      if (javaMajorVersion == null || javaMajorVersion < 21) {
-        log.error("JDTLS requires at least Java 21.")
-        return
-      }
-      const distPath = path.join(Global.Path.bin, "jdtls")
-      const launcherDir = path.join(distPath, "plugins")
-      const installed = await pathExists(launcherDir)
-      if (!installed) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        log.info("Downloading JDTLS LSP server.")
-        await fs.mkdir(distPath, { recursive: true })
-        const releaseURL =
-          "https://www.eclipse.org/downloads/download.php?file=/jdtls/snapshots/jdt-language-server-latest.tar.gz"
-        const archiveName = "release.tar.gz"
-
-        log.info("Downloading JDTLS archive", { url: releaseURL, dest: distPath })
-        const download = await fetch(releaseURL)
-        if (!download.ok || !download.body) {
-          log.error("Failed to download JDTLS", { status: download.status, statusText: download.statusText })
-          return
-        }
-        await Filesystem.writeStream(path.join(distPath, archiveName), download.body)
+      const platform = process.platform
+      const arch = process.arch
 
-        log.info("Extracting JDTLS archive")
-        const tarResult = await run(["tar", "-xzf", archiveName], { cwd: distPath })
-        if (tarResult.code !== 0) {
-          log.error("Failed to extract JDTLS", { exitCode: tarResult.code, stderr: tarResult.stderr.toString() })
-          return
-        }
+      const tfArch = arch === "arm64" ? "arm64" : "amd64"
+      const tfPlatform = platform === "win32" ? "windows" : platform
 
-        await fs.rm(path.join(distPath, archiveName), { force: true })
-        log.info("JDTLS download and extraction completed")
-      }
-      const jarFileName =
-        (await fs.readdir(launcherDir).catch(() => []))
-          .find((item) => /^org\.eclipse\.equinox\.launcher_.*\.jar$/.test(item))
-          ?.trim() ?? ""
-      const launcherJar = path.join(launcherDir, jarFileName)
-      if (!(await pathExists(launcherJar))) {
-        log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`)
+      const builds = release.builds ?? []
+      const build = builds.find((b) => b.arch === tfArch && b.os === tfPlatform)
+      if (!build?.url) {
+        log.error(`Could not find build for ${tfPlatform}/${tfArch} terraform-ls release version ${release.version}`)
         return
       }
-      const configFile = path.join(
-        distPath,
-        (() => {
-          switch (process.platform) {
-            case "darwin":
-              return "config_mac"
-            case "linux":
-              return "config_linux"
-            case "win32":
-              return "config_win"
-            default:
-              return "config_linux"
-          }
-        })(),
-      )
-      const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-jdtls-data"))
-      return {
-        process: spawn(
-          java,
-          [
-            "-jar",
-            launcherJar,
-            "-configuration",
-            configFile,
-            "-data",
-            dataDir,
-            "-Declipse.application=org.eclipse.jdt.ls.core.id1",
-            "-Dosgi.bundles.defaultStartLevel=4",
-            "-Declipse.product=org.eclipse.jdt.ls.core.product",
-            "-Dlog.level=ALL",
-            "--add-modules=ALL-SYSTEM",
-            "--add-opens java.base/java.util=ALL-UNNAMED",
-            "--add-opens java.base/java.lang=ALL-UNNAMED",
-          ],
-          {
-            cwd: root,
-          },
-        ),
-      }
-    },
-  }
 
-  export const KotlinLS: Info = {
-    id: "kotlin-ls",
-    extensions: [".kt", ".kts"],
-    root: async (file) => {
-      // 1) Nearest Gradle root (multi-project or included build)
-      const settingsRoot = await NearestRoot(["settings.gradle.kts", "settings.gradle"])(file)
-      if (settingsRoot) return settingsRoot
-      // 2) Gradle wrapper (strong root signal)
-      const wrapperRoot = await NearestRoot(["gradlew", "gradlew.bat"])(file)
-      if (wrapperRoot) return wrapperRoot
-      // 3) Single-project or module-level build
-      const buildRoot = await NearestRoot(["build.gradle.kts", "build.gradle"])(file)
-      if (buildRoot) return buildRoot
-      // 4) Maven fallback
-      return NearestRoot(["pom.xml"])(file)
-    },
-    async spawn(root) {
-      const distPath = path.join(Global.Path.bin, "kotlin-ls")
-      const launcherScript =
-        process.platform === "win32" ? path.join(distPath, "kotlin-lsp.cmd") : path.join(distPath, "kotlin-lsp.sh")
-      const installed = await Filesystem.exists(launcherScript)
-      if (!installed) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        log.info("Downloading Kotlin Language Server from GitHub.")
-
-        const releaseResponse = await fetch("https://api.github.com/repos/Kotlin/kotlin-lsp/releases/latest")
-        if (!releaseResponse.ok) {
-          log.error("Failed to fetch kotlin-lsp release info")
-          return
-        }
-
-        const release = await releaseResponse.json()
-        const version = release.name?.replace(/^v/, "")
-
-        if (!version) {
-          log.error("Could not determine Kotlin LSP version from release")
-          return
-        }
-
-        const platform = process.platform
-        const arch = process.arch
-
-        let kotlinArch: string = arch
-        if (arch === "arm64") kotlinArch = "aarch64"
-        else if (arch === "x64") kotlinArch = "x64"
-
-        let kotlinPlatform: string = platform
-        if (platform === "darwin") kotlinPlatform = "mac"
-        else if (platform === "linux") kotlinPlatform = "linux"
-        else if (platform === "win32") kotlinPlatform = "win"
-
-        const supportedCombos = ["mac-x64", "mac-aarch64", "linux-x64", "linux-aarch64", "win-x64", "win-aarch64"]
+      const downloadResponse = await fetch(build.url)
+      if (!downloadResponse.ok) {
+        log.error("Failed to download terraform-ls")
+        return
+      }
 
-        const combo = `${kotlinPlatform}-${kotlinArch}`
+      const tempPath = path.join(Global.Path.bin, "terraform-ls.zip")
+      if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
 
-        if (!supportedCombos.includes(combo)) {
-          log.error(`Platform ${platform}/${arch} is not supported by Kotlin LSP`)
-          return
-        }
+      const ok = await Archive.extractZip(tempPath, Global.Path.bin)
+        .then(() => true)
+        .catch((error) => {
+          log.error("Failed to extract terraform-ls archive", { error })
+          return false
+        })
+      if (!ok) return
+      await fs.rm(tempPath, { force: true })
 
-        const assetName = `kotlin-lsp-${version}-${kotlinPlatform}-${kotlinArch}.zip`
-        const releaseURL = `https://download-cdn.jetbrains.com/kotlin-lsp/${version}/${assetName}`
+      bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : ""))
 
-        await fs.mkdir(distPath, { recursive: true })
-        const archivePath = path.join(distPath, "kotlin-ls.zip")
-        const download = await fetch(releaseURL)
-        if (!download.ok || !download.body) {
-          log.error("Failed to download Kotlin Language Server", {
-            status: download.status,
-            statusText: download.statusText,
-          })
-          return
-        }
-        await Filesystem.writeStream(archivePath, download.body)
-        const ok = await Archive.extractZip(archivePath, distPath)
-          .then(() => true)
-          .catch((error) => {
-            log.error("Failed to extract Kotlin LS archive", { error })
-            return false
-          })
-        if (!ok) return
-        await fs.rm(archivePath, { force: true })
-        if (process.platform !== "win32") {
-          await fs.chmod(launcherScript, 0o755).catch(() => {})
-        }
-        log.info("Installed Kotlin Language Server", { path: launcherScript })
-      }
-      if (!(await Filesystem.exists(launcherScript))) {
-        log.error(`Failed to locate the Kotlin LS launcher script in the installed directory: ${distPath}.`)
+      if (!(await Filesystem.exists(bin))) {
+        log.error("Failed to extract terraform-ls binary")
         return
       }
-      return {
-        process: spawn(launcherScript, ["--stdio"], {
-          cwd: root,
-        }),
-      }
-    },
-  }
 
-  export const YamlLS: Info = {
-    id: "yaml-ls",
-    extensions: [".yaml", ".yml"],
-    root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
-    async spawn(root) {
-      let binary = which("yaml-language-server")
-      const args: string[] = []
-      if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        const resolved = await Npm.which("yaml-language-server")
-        if (!resolved) return
-        binary = resolved
-      }
-      args.push("--stdio")
-      const proc = spawn(binary, args, {
-        cwd: root,
-        env: {
-          ...process.env,
-        },
-      })
-      return {
-        process: proc,
+      if (platform !== "win32") {
+        await fs.chmod(bin, 0o755).catch(() => {})
       }
-    },
-  }
-
-  export const LuaLS: Info = {
-    id: "lua-ls",
-    root: NearestRoot([
-      ".luarc.json",
-      ".luarc.jsonc",
-      ".luacheckrc",
-      ".stylua.toml",
-      "stylua.toml",
-      "selene.toml",
-      "selene.yml",
-    ]),
-    extensions: [".lua"],
-    async spawn(root) {
-      let bin = which("lua-language-server")
-
-      if (!bin) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        log.info("downloading lua-language-server from GitHub releases")
-
-        const releaseResponse = await fetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest")
-        if (!releaseResponse.ok) {
-          log.error("Failed to fetch lua-language-server release info")
-          return
-        }
-
-        const release = await releaseResponse.json()
-
-        const platform = process.platform
-        const arch = process.arch
-        let assetName = ""
-
-        let lualsArch: string = arch
-        if (arch === "arm64") lualsArch = "arm64"
-        else if (arch === "x64") lualsArch = "x64"
-        else if (arch === "ia32") lualsArch = "ia32"
-
-        let lualsPlatform: string = platform
-        if (platform === "darwin") lualsPlatform = "darwin"
-        else if (platform === "linux") lualsPlatform = "linux"
-        else if (platform === "win32") lualsPlatform = "win32"
-
-        const ext = platform === "win32" ? "zip" : "tar.gz"
-
-        assetName = `lua-language-server-${release.tag_name}-${lualsPlatform}-${lualsArch}.${ext}`
-
-        const supportedCombos = [
-          "darwin-arm64.tar.gz",
-          "darwin-x64.tar.gz",
-          "linux-x64.tar.gz",
-          "linux-arm64.tar.gz",
-          "win32-x64.zip",
-          "win32-ia32.zip",
-        ]
-
-        const assetSuffix = `${lualsPlatform}-${lualsArch}.${ext}`
-        if (!supportedCombos.includes(assetSuffix)) {
-          log.error(`Platform ${platform} and architecture ${arch} is not supported by lua-language-server`)
-          return
-        }
-
-        const asset = release.assets.find((a: any) => a.name === assetName)
-        if (!asset) {
-          log.error(`Could not find asset ${assetName} in latest lua-language-server release`)
-          return
-        }
-
-        const downloadUrl = asset.browser_download_url
-        const downloadResponse = await fetch(downloadUrl)
-        if (!downloadResponse.ok) {
-          log.error("Failed to download lua-language-server")
-          return
-        }
-
-        const tempPath = path.join(Global.Path.bin, assetName)
-        if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
-
-        // Unlike zls which is a single self-contained binary,
-        // lua-language-server needs supporting files (meta/, locale/, etc.)
-        // Extract entire archive to dedicated directory to preserve all files
-        const installDir = path.join(Global.Path.bin, `lua-language-server-${lualsArch}-${lualsPlatform}`)
-
-        // Remove old installation if exists
-        const stats = await fs.stat(installDir).catch(() => undefined)
-        if (stats) {
-          await fs.rm(installDir, { force: true, recursive: true })
-        }
-
-        await fs.mkdir(installDir, { recursive: true })
-
-        if (ext === "zip") {
-          const ok = await Archive.extractZip(tempPath, installDir)
-            .then(() => true)
-            .catch((error) => {
-              log.error("Failed to extract lua-language-server archive", { error })
-              return false
-            })
-          if (!ok) return
-        } else {
-          const ok = await run(["tar", "-xzf", tempPath, "-C", installDir])
-            .then((result) => result.code === 0)
-            .catch((error: unknown) => {
-              log.error("Failed to extract lua-language-server archive", { error })
-              return false
-            })
-          if (!ok) return
-        }
 
-        await fs.rm(tempPath, { force: true })
+      log.info(`installed terraform-ls`, { bin })
+    }
 
-        // Binary is located in bin/ subdirectory within the extracted archive
-        bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : ""))
+    return {
+      process: spawn(bin, ["serve"], {
+        cwd: root,
+      }),
+      initialization: {
+        experimentalFeatures: {
+          prefillRequiredFields: true,
+          validateOnSave: true,
+        },
+      },
+    }
+  },
+}
 
-        if (!(await Filesystem.exists(bin))) {
-          log.error("Failed to extract lua-language-server binary")
-          return
-        }
+export const TexLab: Info = {
+  id: "texlab",
+  extensions: [".tex", ".bib"],
+  root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
+  async spawn(root) {
+    let bin = which("texlab")
 
-        if (platform !== "win32") {
-          const ok = await fs
-            .chmod(bin, 0o755)
-            .then(() => true)
-            .catch((error: unknown) => {
-              log.error("Failed to set executable permission for lua-language-server binary", {
-                error,
-              })
-              return false
-            })
-          if (!ok) return
-        }
-
-        log.info(`installed lua-language-server`, { bin })
-      }
+    if (!bin) {
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      log.info("downloading texlab from GitHub releases")
 
-      return {
-        process: spawn(bin, {
-          cwd: root,
-        }),
+      const response = await fetch("https://api.github.com/repos/latex-lsp/texlab/releases/latest")
+      if (!response.ok) {
+        log.error("Failed to fetch texlab release info")
+        return
       }
-    },
-  }
 
-  export const PHPIntelephense: Info = {
-    id: "php intelephense",
-    extensions: [".php"],
-    root: NearestRoot(["composer.json", "composer.lock", ".php-version"]),
-    async spawn(root) {
-      let binary = which("intelephense")
-      const args: string[] = []
-      if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        const resolved = await Npm.which("intelephense")
-        if (!resolved) return
-        binary = resolved
-      }
-      args.push("--stdio")
-      const proc = spawn(binary, args, {
-        cwd: root,
-        env: {
-          ...process.env,
-        },
-      })
-      return {
-        process: proc,
-        initialization: {
-          telemetry: {
-            enabled: false,
-          },
-        },
+      const release = (await response.json()) as {
+        tag_name?: string
+        assets?: { name?: string; browser_download_url?: string }[]
       }
-    },
-  }
-
-  export const Prisma: Info = {
-    id: "prisma",
-    extensions: [".prisma"],
-    root: NearestRoot(["schema.prisma", "prisma/schema.prisma", "prisma"], ["package.json"]),
-    async spawn(root) {
-      const prisma = which("prisma")
-      if (!prisma) {
-        log.info("prisma not found, please install prisma")
+      const version = release.tag_name?.replace("v", "")
+      if (!version) {
+        log.error("texlab release did not include a version tag")
         return
       }
-      return {
-        process: spawn(prisma, ["language-server"], {
-          cwd: root,
-        }),
-      }
-    },
-  }
 
-  export const Dart: Info = {
-    id: "dart",
-    extensions: [".dart"],
-    root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]),
-    async spawn(root) {
-      const dart = which("dart")
-      if (!dart) {
-        log.info("dart not found, please install dart first")
+      const platform = process.platform
+      const arch = process.arch
+
+      const texArch = arch === "arm64" ? "aarch64" : "x86_64"
+      const texPlatform = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux"
+      const ext = platform === "win32" ? "zip" : "tar.gz"
+      const assetName = `texlab-${texArch}-${texPlatform}.${ext}`
+
+      const assets = release.assets ?? []
+      const asset = assets.find((a) => a.name === assetName)
+      if (!asset?.browser_download_url) {
+        log.error(`Could not find asset ${assetName} in texlab release`)
         return
       }
-      return {
-        process: spawn(dart, ["language-server", "--lsp"], {
-          cwd: root,
-        }),
-      }
-    },
-  }
 
-  export const Ocaml: Info = {
-    id: "ocaml-lsp",
-    extensions: [".ml", ".mli"],
-    root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"]),
-    async spawn(root) {
-      const bin = which("ocamllsp")
-      if (!bin) {
-        log.info("ocamllsp not found, please install ocaml-lsp-server")
+      const downloadResponse = await fetch(asset.browser_download_url)
+      if (!downloadResponse.ok) {
+        log.error("Failed to download texlab")
         return
       }
-      return {
-        process: spawn(bin, {
-          cwd: root,
-        }),
-      }
-    },
-  }
-  export const BashLS: Info = {
-    id: "bash",
-    extensions: [".sh", ".bash", ".zsh", ".ksh"],
-    root: async () => Instance.directory,
-    async spawn(root) {
-      let binary = which("bash-language-server")
-      const args: string[] = []
-      if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        const resolved = await Npm.which("bash-language-server")
-        if (!resolved) return
-        binary = resolved
-      }
-      args.push("start")
-      const proc = spawn(binary, args, {
-        cwd: root,
-        env: {
-          ...process.env,
-        },
-      })
-      return {
-        process: proc,
-      }
-    },
-  }
-
-  export const TerraformLS: Info = {
-    id: "terraform",
-    extensions: [".tf", ".tfvars"],
-    root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]),
-    async spawn(root) {
-      let bin = which("terraform-ls")
-
-      if (!bin) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        log.info("downloading terraform-ls from HashiCorp releases")
-
-        const releaseResponse = await fetch("https://api.releases.hashicorp.com/v1/releases/terraform-ls/latest")
-        if (!releaseResponse.ok) {
-          log.error("Failed to fetch terraform-ls release info")
-          return
-        }
-
-        const release = (await releaseResponse.json()) as {
-          version?: string
-          builds?: { arch?: string; os?: string; url?: string }[]
-        }
-
-        const platform = process.platform
-        const arch = process.arch
-
-        const tfArch = arch === "arm64" ? "arm64" : "amd64"
-        const tfPlatform = platform === "win32" ? "windows" : platform
-
-        const builds = release.builds ?? []
-        const build = builds.find((b) => b.arch === tfArch && b.os === tfPlatform)
-        if (!build?.url) {
-          log.error(`Could not find build for ${tfPlatform}/${tfArch} terraform-ls release version ${release.version}`)
-          return
-        }
-
-        const downloadResponse = await fetch(build.url)
-        if (!downloadResponse.ok) {
-          log.error("Failed to download terraform-ls")
-          return
-        }
 
-        const tempPath = path.join(Global.Path.bin, "terraform-ls.zip")
-        if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
+      const tempPath = path.join(Global.Path.bin, assetName)
+      if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
 
+      if (ext === "zip") {
         const ok = await Archive.extractZip(tempPath, Global.Path.bin)
           .then(() => true)
           .catch((error) => {
-            log.error("Failed to extract terraform-ls archive", { error })
+            log.error("Failed to extract texlab archive", { error })
             return false
           })
         if (!ok) return
-        await fs.rm(tempPath, { force: true })
-
-        bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : ""))
-
-        if (!(await Filesystem.exists(bin))) {
-          log.error("Failed to extract terraform-ls binary")
-          return
-        }
-
-        if (platform !== "win32") {
-          await fs.chmod(bin, 0o755).catch(() => {})
-        }
-
-        log.info(`installed terraform-ls`, { bin })
       }
-
-      return {
-        process: spawn(bin, ["serve"], {
-          cwd: root,
-        }),
-        initialization: {
-          experimentalFeatures: {
-            prefillRequiredFields: true,
-            validateOnSave: true,
-          },
-        },
+      if (ext === "tar.gz") {
+        await run(["tar", "-xzf", tempPath], { cwd: Global.Path.bin })
       }
-    },
-  }
-
-  export const TexLab: Info = {
-    id: "texlab",
-    extensions: [".tex", ".bib"],
-    root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
-    async spawn(root) {
-      let bin = which("texlab")
 
-      if (!bin) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        log.info("downloading texlab from GitHub releases")
+      await fs.rm(tempPath, { force: true })
 
-        const response = await fetch("https://api.github.com/repos/latex-lsp/texlab/releases/latest")
-        if (!response.ok) {
-          log.error("Failed to fetch texlab release info")
-          return
-        }
+      bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : ""))
 
-        const release = (await response.json()) as {
-          tag_name?: string
-          assets?: { name?: string; browser_download_url?: string }[]
-        }
-        const version = release.tag_name?.replace("v", "")
-        if (!version) {
-          log.error("texlab release did not include a version tag")
-          return
-        }
-
-        const platform = process.platform
-        const arch = process.arch
-
-        const texArch = arch === "arm64" ? "aarch64" : "x86_64"
-        const texPlatform = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux"
-        const ext = platform === "win32" ? "zip" : "tar.gz"
-        const assetName = `texlab-${texArch}-${texPlatform}.${ext}`
-
-        const assets = release.assets ?? []
-        const asset = assets.find((a) => a.name === assetName)
-        if (!asset?.browser_download_url) {
-          log.error(`Could not find asset ${assetName} in texlab release`)
-          return
-        }
-
-        const downloadResponse = await fetch(asset.browser_download_url)
-        if (!downloadResponse.ok) {
-          log.error("Failed to download texlab")
-          return
-        }
-
-        const tempPath = path.join(Global.Path.bin, assetName)
-        if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
-
-        if (ext === "zip") {
-          const ok = await Archive.extractZip(tempPath, Global.Path.bin)
-            .then(() => true)
-            .catch((error) => {
-              log.error("Failed to extract texlab archive", { error })
-              return false
-            })
-          if (!ok) return
-        }
-        if (ext === "tar.gz") {
-          await run(["tar", "-xzf", tempPath], { cwd: Global.Path.bin })
-        }
+      if (!(await Filesystem.exists(bin))) {
+        log.error("Failed to extract texlab binary")
+        return
+      }
 
-        await fs.rm(tempPath, { force: true })
+      if (platform !== "win32") {
+        await fs.chmod(bin, 0o755).catch(() => {})
+      }
 
-        bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : ""))
+      log.info("installed texlab", { bin })
+    }
 
-        if (!(await Filesystem.exists(bin))) {
-          log.error("Failed to extract texlab binary")
-          return
-        }
+    return {
+      process: spawn(bin, {
+        cwd: root,
+      }),
+    }
+  },
+}
 
-        if (platform !== "win32") {
-          await fs.chmod(bin, 0o755).catch(() => {})
-        }
+export const DockerfileLS: Info = {
+  id: "dockerfile",
+  extensions: [".dockerfile", "Dockerfile"],
+  root: async () => Instance.directory,
+  async spawn(root) {
+    let binary = which("docker-langserver")
+    const args: string[] = []
+    if (!binary) {
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      const resolved = await Npm.which("dockerfile-language-server-nodejs")
+      if (!resolved) return
+      binary = resolved
+    }
+    args.push("--stdio")
+    const proc = spawn(binary, args, {
+      cwd: root,
+      env: {
+        ...process.env,
+      },
+    })
+    return {
+      process: proc,
+    }
+  },
+}
 
-        log.info("installed texlab", { bin })
-      }
+export const Gleam: Info = {
+  id: "gleam",
+  extensions: [".gleam"],
+  root: NearestRoot(["gleam.toml"]),
+  async spawn(root) {
+    const gleam = which("gleam")
+    if (!gleam) {
+      log.info("gleam not found, please install gleam first")
+      return
+    }
+    return {
+      process: spawn(gleam, ["lsp"], {
+        cwd: root,
+      }),
+    }
+  },
+}
 
-      return {
-        process: spawn(bin, {
-          cwd: root,
-        }),
-      }
-    },
-  }
+export const Clojure: Info = {
+  id: "clojure-lsp",
+  extensions: [".clj", ".cljs", ".cljc", ".edn"],
+  root: NearestRoot(["deps.edn", "project.clj", "shadow-cljs.edn", "bb.edn", "build.boot"]),
+  async spawn(root) {
+    let bin = which("clojure-lsp")
+    if (!bin && process.platform === "win32") {
+      bin = which("clojure-lsp.exe")
+    }
+    if (!bin) {
+      log.info("clojure-lsp not found, please install clojure-lsp first")
+      return
+    }
+    return {
+      process: spawn(bin, ["listen"], {
+        cwd: root,
+      }),
+    }
+  },
+}
 
-  export const DockerfileLS: Info = {
-    id: "dockerfile",
-    extensions: [".dockerfile", "Dockerfile"],
-    root: async () => Instance.directory,
-    async spawn(root) {
-      let binary = which("docker-langserver")
-      const args: string[] = []
-      if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        const resolved = await Npm.which("dockerfile-language-server-nodejs")
-        if (!resolved) return
-        binary = resolved
-      }
-      args.push("--stdio")
-      const proc = spawn(binary, args, {
+export const Nixd: Info = {
+  id: "nixd",
+  extensions: [".nix"],
+  root: async (file) => {
+    // First, look for flake.nix - the most reliable Nix project root indicator
+    const flakeRoot = await NearestRoot(["flake.nix"])(file)
+    if (flakeRoot && flakeRoot !== Instance.directory) return flakeRoot
+
+    // If no flake.nix, fall back to git repository root
+    if (Instance.worktree && Instance.worktree !== Instance.directory) return Instance.worktree
+
+    // Finally, use the instance directory as fallback
+    return Instance.directory
+  },
+  async spawn(root) {
+    const nixd = which("nixd")
+    if (!nixd) {
+      log.info("nixd not found, please install nixd first")
+      return
+    }
+    return {
+      process: spawn(nixd, [], {
         cwd: root,
         env: {
           ...process.env,
         },
-      })
-      return {
-        process: proc,
-      }
-    },
-  }
+      }),
+    }
+  },
+}
 
-  export const Gleam: Info = {
-    id: "gleam",
-    extensions: [".gleam"],
-    root: NearestRoot(["gleam.toml"]),
-    async spawn(root) {
-      const gleam = which("gleam")
-      if (!gleam) {
-        log.info("gleam not found, please install gleam first")
-        return
-      }
-      return {
-        process: spawn(gleam, ["lsp"], {
-          cwd: root,
-        }),
-      }
-    },
-  }
+export const Tinymist: Info = {
+  id: "tinymist",
+  extensions: [".typ", ".typc"],
+  root: NearestRoot(["typst.toml"]),
+  async spawn(root) {
+    let bin = which("tinymist")
 
-  export const Clojure: Info = {
-    id: "clojure-lsp",
-    extensions: [".clj", ".cljs", ".cljc", ".edn"],
-    root: NearestRoot(["deps.edn", "project.clj", "shadow-cljs.edn", "bb.edn", "build.boot"]),
-    async spawn(root) {
-      let bin = which("clojure-lsp")
-      if (!bin && process.platform === "win32") {
-        bin = which("clojure-lsp.exe")
-      }
-      if (!bin) {
-        log.info("clojure-lsp not found, please install clojure-lsp first")
-        return
-      }
-      return {
-        process: spawn(bin, ["listen"], {
-          cwd: root,
-        }),
-      }
-    },
-  }
+    if (!bin) {
+      if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+      log.info("downloading tinymist from GitHub releases")
 
-  export const Nixd: Info = {
-    id: "nixd",
-    extensions: [".nix"],
-    root: async (file) => {
-      // First, look for flake.nix - the most reliable Nix project root indicator
-      const flakeRoot = await NearestRoot(["flake.nix"])(file)
-      if (flakeRoot && flakeRoot !== Instance.directory) return flakeRoot
-
-      // If no flake.nix, fall back to git repository root
-      if (Instance.worktree && Instance.worktree !== Instance.directory) return Instance.worktree
-
-      // Finally, use the instance directory as fallback
-      return Instance.directory
-    },
-    async spawn(root) {
-      const nixd = which("nixd")
-      if (!nixd) {
-        log.info("nixd not found, please install nixd first")
+      const response = await fetch("https://api.github.com/repos/Myriad-Dreamin/tinymist/releases/latest")
+      if (!response.ok) {
+        log.error("Failed to fetch tinymist release info")
         return
       }
-      return {
-        process: spawn(nixd, [], {
-          cwd: root,
-          env: {
-            ...process.env,
-          },
-        }),
-      }
-    },
-  }
-
-  export const Tinymist: Info = {
-    id: "tinymist",
-    extensions: [".typ", ".typc"],
-    root: NearestRoot(["typst.toml"]),
-    async spawn(root) {
-      let bin = which("tinymist")
-
-      if (!bin) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        log.info("downloading tinymist from GitHub releases")
-
-        const response = await fetch("https://api.github.com/repos/Myriad-Dreamin/tinymist/releases/latest")
-        if (!response.ok) {
-          log.error("Failed to fetch tinymist release info")
-          return
-        }
 
-        const release = (await response.json()) as {
-          tag_name?: string
-          assets?: { name?: string; browser_download_url?: string }[]
-        }
+      const release = (await response.json()) as {
+        tag_name?: string
+        assets?: { name?: string; browser_download_url?: string }[]
+      }
 
-        const platform = process.platform
-        const arch = process.arch
-
-        const tinymistArch = arch === "arm64" ? "aarch64" : "x86_64"
-        let tinymistPlatform: string
-        let ext: string
-
-        if (platform === "darwin") {
-          tinymistPlatform = "apple-darwin"
-          ext = "tar.gz"
-        } else if (platform === "win32") {
-          tinymistPlatform = "pc-windows-msvc"
-          ext = "zip"
-        } else {
-          tinymistPlatform = "unknown-linux-gnu"
-          ext = "tar.gz"
-        }
+      const platform = process.platform
+      const arch = process.arch
 
-        const assetName = `tinymist-${tinymistArch}-${tinymistPlatform}.${ext}`
+      const tinymistArch = arch === "arm64" ? "aarch64" : "x86_64"
+      let tinymistPlatform: string
+      let ext: string
 
-        const assets = release.assets ?? []
-        const asset = assets.find((a) => a.name === assetName)
-        if (!asset?.browser_download_url) {
-          log.error(`Could not find asset ${assetName} in tinymist release`)
-          return
-        }
+      if (platform === "darwin") {
+        tinymistPlatform = "apple-darwin"
+        ext = "tar.gz"
+      } else if (platform === "win32") {
+        tinymistPlatform = "pc-windows-msvc"
+        ext = "zip"
+      } else {
+        tinymistPlatform = "unknown-linux-gnu"
+        ext = "tar.gz"
+      }
 
-        const downloadResponse = await fetch(asset.browser_download_url)
-        if (!downloadResponse.ok) {
-          log.error("Failed to download tinymist")
-          return
-        }
+      const assetName = `tinymist-${tinymistArch}-${tinymistPlatform}.${ext}`
 
-        const tempPath = path.join(Global.Path.bin, assetName)
-        if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
+      const assets = release.assets ?? []
+      const asset = assets.find((a) => a.name === assetName)
+      if (!asset?.browser_download_url) {
+        log.error(`Could not find asset ${assetName} in tinymist release`)
+        return
+      }
 
-        if (ext === "zip") {
-          const ok = await Archive.extractZip(tempPath, Global.Path.bin)
-            .then(() => true)
-            .catch((error) => {
-              log.error("Failed to extract tinymist archive", { error })
-              return false
-            })
-          if (!ok) return
-        } else {
-          await run(["tar", "-xzf", tempPath, "--strip-components=1"], { cwd: Global.Path.bin })
-        }
+      const downloadResponse = await fetch(asset.browser_download_url)
+      if (!downloadResponse.ok) {
+        log.error("Failed to download tinymist")
+        return
+      }
 
-        await fs.rm(tempPath, { force: true })
+      const tempPath = path.join(Global.Path.bin, assetName)
+      if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
 
-        bin = path.join(Global.Path.bin, "tinymist" + (platform === "win32" ? ".exe" : ""))
+      if (ext === "zip") {
+        const ok = await Archive.extractZip(tempPath, Global.Path.bin)
+          .then(() => true)
+          .catch((error) => {
+            log.error("Failed to extract tinymist archive", { error })
+            return false
+          })
+        if (!ok) return
+      } else {
+        await run(["tar", "-xzf", tempPath, "--strip-components=1"], { cwd: Global.Path.bin })
+      }
 
-        if (!(await Filesystem.exists(bin))) {
-          log.error("Failed to extract tinymist binary")
-          return
-        }
+      await fs.rm(tempPath, { force: true })
 
-        if (platform !== "win32") {
-          await fs.chmod(bin, 0o755).catch(() => {})
-        }
+      bin = path.join(Global.Path.bin, "tinymist" + (platform === "win32" ? ".exe" : ""))
 
-        log.info("installed tinymist", { bin })
+      if (!(await Filesystem.exists(bin))) {
+        log.error("Failed to extract tinymist binary")
+        return
       }
 
-      return {
-        process: spawn(bin, { cwd: root }),
+      if (platform !== "win32") {
+        await fs.chmod(bin, 0o755).catch(() => {})
       }
-    },
-  }
 
-  export const HLS: Info = {
-    id: "haskell-language-server",
-    extensions: [".hs", ".lhs"],
-    root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"]),
-    async spawn(root) {
-      const bin = which("haskell-language-server-wrapper")
-      if (!bin) {
-        log.info("haskell-language-server-wrapper not found, please install haskell-language-server")
-        return
-      }
-      return {
-        process: spawn(bin, ["--lsp"], {
-          cwd: root,
-        }),
-      }
-    },
-  }
+      log.info("installed tinymist", { bin })
+    }
 
-  export const JuliaLS: Info = {
-    id: "julials",
-    extensions: [".jl"],
-    root: NearestRoot(["Project.toml", "Manifest.toml", "*.jl"]),
-    async spawn(root) {
-      const julia = which("julia")
-      if (!julia) {
-        log.info("julia not found, please install julia first (https://julialang.org/downloads/)")
-        return
-      }
-      return {
-        process: spawn(julia, ["--startup-file=no", "--history-file=no", "-e", "using LanguageServer; runserver()"], {
-          cwd: root,
-        }),
-      }
-    },
-  }
+    return {
+      process: spawn(bin, { cwd: root }),
+    }
+  },
+}
+
+export const HLS: Info = {
+  id: "haskell-language-server",
+  extensions: [".hs", ".lhs"],
+  root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"]),
+  async spawn(root) {
+    const bin = which("haskell-language-server-wrapper")
+    if (!bin) {
+      log.info("haskell-language-server-wrapper not found, please install haskell-language-server")
+      return
+    }
+    return {
+      process: spawn(bin, ["--lsp"], {
+        cwd: root,
+      }),
+    }
+  },
+}
+
+export const JuliaLS: Info = {
+  id: "julials",
+  extensions: [".jl"],
+  root: NearestRoot(["Project.toml", "Manifest.toml", "*.jl"]),
+  async spawn(root) {
+    const julia = which("julia")
+    if (!julia) {
+      log.info("julia not found, please install julia first (https://julialang.org/downloads/)")
+      return
+    }
+    return {
+      process: spawn(julia, ["--startup-file=no", "--history-file=no", "-e", "using LanguageServer; runserver()"], {
+        cwd: root,
+      }),
+    }
+  },
 }

+ 2 - 2
packages/opencode/test/lsp/client.test.ts

@@ -1,7 +1,7 @@
 import { describe, expect, test, beforeEach } from "bun:test"
 import path from "path"
-import { LSPClient } from "../../src/lsp/client"
-import { LSPServer } from "../../src/lsp/server"
+import { LSPClient } from "../../src/lsp"
+import { LSPServer } from "../../src/lsp"
 import { Instance } from "../../src/project/instance"
 import { Log } from "../../src/util"
 

+ 1 - 1
packages/opencode/test/lsp/index.test.ts

@@ -2,7 +2,7 @@ import { describe, expect, spyOn } from "bun:test"
 import path from "path"
 import { Effect, Layer } from "effect"
 import { LSP } from "../../src/lsp"
-import { LSPServer } from "../../src/lsp/server"
+import { LSPServer } from "../../src/lsp"
 import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
 import { provideTmpdirInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"

+ 1 - 1
packages/opencode/test/lsp/lifecycle.test.ts

@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
 import path from "path"
 import { Effect, Layer } from "effect"
 import { LSP } from "../../src/lsp"
-import { LSPServer } from "../../src/lsp/server"
+import { LSPServer } from "../../src/lsp"
 import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
 import { provideTmpdirInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"