feat: unwrap lsp namespaces to flat exports + barrel (#22748)
This commit is contained in:
@@ -19,7 +19,7 @@ import {
|
|||||||
printParseErrorCode,
|
printParseErrorCode,
|
||||||
} from "jsonc-parser"
|
} from "jsonc-parser"
|
||||||
import { Instance, type InstanceContext } from "../project/instance"
|
import { Instance, type InstanceContext } from "../project/instance"
|
||||||
import { LSPServer } from "../lsp/server"
|
import { LSPServer } from "../lsp"
|
||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
import { ConfigMarkdown } from "."
|
import { ConfigMarkdown } from "."
|
||||||
import { existsSync } from "fs"
|
import { existsSync } from "fs"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Log } from "../util"
|
|||||||
import { Process } from "../util"
|
import { Process } from "../util"
|
||||||
import { LANGUAGE_EXTENSIONS } from "./language"
|
import { LANGUAGE_EXTENSIONS } from "./language"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import type { LSPServer } from "./server"
|
import type { LSPServer } from "."
|
||||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||||
import { withTimeout } from "../util/timeout"
|
import { withTimeout } from "../util/timeout"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
@@ -16,21 +16,20 @@ import { Filesystem } from "../util"
|
|||||||
|
|
||||||
const DIAGNOSTICS_DEBOUNCE_MS = 150
|
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(
|
export const InitializeError = NamedError.create(
|
||||||
"LSPInitializeError",
|
"LSPInitializeError",
|
||||||
z.object({
|
z.object({
|
||||||
serverID: z.string(),
|
serverID: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Diagnostics: BusEvent.define(
|
Diagnostics: BusEvent.define(
|
||||||
"lsp.client.diagnostics",
|
"lsp.client.diagnostics",
|
||||||
z.object({
|
z.object({
|
||||||
@@ -38,9 +37,9 @@ export namespace LSPClient {
|
|||||||
path: z.string(),
|
path: z.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
|
export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
|
||||||
const l = log.clone().tag("serverID", input.serverID)
|
const l = log.clone().tag("serverID", input.serverID)
|
||||||
l.info("starting client")
|
l.info("starting client")
|
||||||
|
|
||||||
@@ -59,7 +58,7 @@ export namespace LSPClient {
|
|||||||
const exists = diagnostics.has(filePath)
|
const exists = diagnostics.has(filePath)
|
||||||
diagnostics.set(filePath, params.diagnostics)
|
diagnostics.set(filePath, params.diagnostics)
|
||||||
if (!exists && input.serverID === "typescript") return
|
if (!exists && input.serverID === "typescript") return
|
||||||
void Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID })
|
Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID })
|
||||||
})
|
})
|
||||||
connection.onRequest("window/workDoneProgress/create", (params) => {
|
connection.onRequest("window/workDoneProgress/create", (params) => {
|
||||||
l.info("window/workDoneProgress/create", params)
|
l.info("window/workDoneProgress/create", params)
|
||||||
@@ -248,5 +247,4 @@ export namespace LSPClient {
|
|||||||
l.info("initialized")
|
l.info("initialized")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,537 +1,3 @@
|
|||||||
import { BusEvent } from "@/bus/bus-event"
|
export * as LSP from "./lsp"
|
||||||
import { Bus } from "@/bus"
|
export * as LSPClient from "./client"
|
||||||
import { Log } from "../util"
|
export * as LSPServer from "./server"
|
||||||
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>`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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>`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,24 +15,23 @@ import { Module } from "@opencode-ai/shared/util/module"
|
|||||||
import { spawn } from "./launch"
|
import { spawn } from "./launch"
|
||||||
import { Npm } from "../npm"
|
import { Npm } from "../npm"
|
||||||
|
|
||||||
export namespace LSPServer {
|
const log = Log.create({ service: "lsp.server" })
|
||||||
const log = Log.create({ service: "lsp.server" })
|
const pathExists = async (p: string) =>
|
||||||
const pathExists = async (p: string) =>
|
|
||||||
fs
|
fs
|
||||||
.stat(p)
|
.stat(p)
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false)
|
.catch(() => false)
|
||||||
const run = (cmd: string[], opts: Process.RunOptions = {}) => Process.run(cmd, { ...opts, nothrow: true })
|
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 })
|
const output = (cmd: string[], opts: Process.RunOptions = {}) => Process.text(cmd, { ...opts, nothrow: true })
|
||||||
|
|
||||||
export interface Handle {
|
export interface Handle {
|
||||||
process: ChildProcessWithoutNullStreams
|
process: ChildProcessWithoutNullStreams
|
||||||
initialization?: Record<string, any>
|
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 => {
|
const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): RootFunction => {
|
||||||
return async (file) => {
|
return async (file) => {
|
||||||
if (excludePatterns) {
|
if (excludePatterns) {
|
||||||
const excludedFiles = Filesystem.up({
|
const excludedFiles = Filesystem.up({
|
||||||
@@ -54,17 +53,17 @@ export namespace LSPServer {
|
|||||||
if (!first.value) return Instance.directory
|
if (!first.value) return Instance.directory
|
||||||
return path.dirname(first.value)
|
return path.dirname(first.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Info {
|
export interface Info {
|
||||||
id: string
|
id: string
|
||||||
extensions: string[]
|
extensions: string[]
|
||||||
global?: boolean
|
global?: boolean
|
||||||
root: RootFunction
|
root: RootFunction
|
||||||
spawn(root: string): Promise<Handle | undefined>
|
spawn(root: string): Promise<Handle | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Deno: Info = {
|
export const Deno: Info = {
|
||||||
id: "deno",
|
id: "deno",
|
||||||
root: async (file) => {
|
root: async (file) => {
|
||||||
const files = Filesystem.up({
|
const files = Filesystem.up({
|
||||||
@@ -90,9 +89,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Typescript: Info = {
|
export const Typescript: Info = {
|
||||||
id: "typescript",
|
id: "typescript",
|
||||||
root: NearestRoot(
|
root: NearestRoot(
|
||||||
["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"],
|
["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"],
|
||||||
@@ -120,9 +119,9 @@ export namespace LSPServer {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Vue: Info = {
|
export const Vue: Info = {
|
||||||
id: "vue",
|
id: "vue",
|
||||||
extensions: [".vue"],
|
extensions: [".vue"],
|
||||||
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||||
@@ -149,9 +148,9 @@ export namespace LSPServer {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ESLint: Info = {
|
export const ESLint: Info = {
|
||||||
id: "eslint",
|
id: "eslint",
|
||||||
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
|
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
|
||||||
@@ -206,9 +205,9 @@ export namespace LSPServer {
|
|||||||
process: proc,
|
process: proc,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Oxlint: Info = {
|
export const Oxlint: Info = {
|
||||||
id: "oxlint",
|
id: "oxlint",
|
||||||
root: NearestRoot([
|
root: NearestRoot([
|
||||||
".oxlintrc.json",
|
".oxlintrc.json",
|
||||||
@@ -279,9 +278,9 @@ export namespace LSPServer {
|
|||||||
log.info("oxlint not found, please install oxlint")
|
log.info("oxlint not found, please install oxlint")
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Biome: Info = {
|
export const Biome: Info = {
|
||||||
id: "biome",
|
id: "biome",
|
||||||
root: NearestRoot([
|
root: NearestRoot([
|
||||||
"biome.json",
|
"biome.json",
|
||||||
@@ -341,9 +340,9 @@ export namespace LSPServer {
|
|||||||
process: proc,
|
process: proc,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Gopls: Info = {
|
export const Gopls: Info = {
|
||||||
id: "gopls",
|
id: "gopls",
|
||||||
root: async (file) => {
|
root: async (file) => {
|
||||||
const work = await NearestRoot(["go.work"])(file)
|
const work = await NearestRoot(["go.work"])(file)
|
||||||
@@ -380,9 +379,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Rubocop: Info = {
|
export const Rubocop: Info = {
|
||||||
id: "ruby-lsp",
|
id: "ruby-lsp",
|
||||||
root: NearestRoot(["Gemfile"]),
|
root: NearestRoot(["Gemfile"]),
|
||||||
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
||||||
@@ -418,9 +417,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Ty: Info = {
|
export const Ty: Info = {
|
||||||
id: "ty",
|
id: "ty",
|
||||||
extensions: [".py", ".pyi"],
|
extensions: [".py", ".pyi"],
|
||||||
root: NearestRoot([
|
root: NearestRoot([
|
||||||
@@ -482,9 +481,9 @@ export namespace LSPServer {
|
|||||||
initialization,
|
initialization,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Pyright: Info = {
|
export const Pyright: Info = {
|
||||||
id: "pyright",
|
id: "pyright",
|
||||||
extensions: [".py", ".pyi"],
|
extensions: [".py", ".pyi"],
|
||||||
root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
|
root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
|
||||||
@@ -526,9 +525,9 @@ export namespace LSPServer {
|
|||||||
initialization,
|
initialization,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ElixirLS: Info = {
|
export const ElixirLS: Info = {
|
||||||
id: "elixir-ls",
|
id: "elixir-ls",
|
||||||
extensions: [".ex", ".exs"],
|
extensions: [".ex", ".exs"],
|
||||||
root: NearestRoot(["mix.exs", "mix.lock"]),
|
root: NearestRoot(["mix.exs", "mix.lock"]),
|
||||||
@@ -589,9 +588,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Zls: Info = {
|
export const Zls: Info = {
|
||||||
id: "zls",
|
id: "zls",
|
||||||
extensions: [".zig", ".zon"],
|
extensions: [".zig", ".zon"],
|
||||||
root: NearestRoot(["build.zig"]),
|
root: NearestRoot(["build.zig"]),
|
||||||
@@ -699,9 +698,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CSharp: Info = {
|
export const CSharp: Info = {
|
||||||
id: "csharp",
|
id: "csharp",
|
||||||
root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
|
root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
|
||||||
extensions: [".cs"],
|
extensions: [".cs"],
|
||||||
@@ -736,9 +735,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FSharp: Info = {
|
export const FSharp: Info = {
|
||||||
id: "fsharp",
|
id: "fsharp",
|
||||||
root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]),
|
root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]),
|
||||||
extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
|
extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
|
||||||
@@ -773,9 +772,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SourceKit: Info = {
|
export const SourceKit: Info = {
|
||||||
id: "sourcekit-lsp",
|
id: "sourcekit-lsp",
|
||||||
extensions: [".swift", ".objc", "objcpp"],
|
extensions: [".swift", ".objc", "objcpp"],
|
||||||
root: NearestRoot(["Package.swift", "*.xcodeproj", "*.xcworkspace"]),
|
root: NearestRoot(["Package.swift", "*.xcodeproj", "*.xcworkspace"]),
|
||||||
@@ -807,9 +806,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RustAnalyzer: Info = {
|
export const RustAnalyzer: Info = {
|
||||||
id: "rust",
|
id: "rust",
|
||||||
root: async (root) => {
|
root: async (root) => {
|
||||||
const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root)
|
const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root)
|
||||||
@@ -853,9 +852,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Clangd: Info = {
|
export const Clangd: Info = {
|
||||||
id: "clangd",
|
id: "clangd",
|
||||||
root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd"]),
|
root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd"]),
|
||||||
extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
|
extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
|
||||||
@@ -999,9 +998,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Svelte: Info = {
|
export const Svelte: Info = {
|
||||||
id: "svelte",
|
id: "svelte",
|
||||||
extensions: [".svelte"],
|
extensions: [".svelte"],
|
||||||
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||||
@@ -1026,9 +1025,9 @@ export namespace LSPServer {
|
|||||||
initialization: {},
|
initialization: {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Astro: Info = {
|
export const Astro: Info = {
|
||||||
id: "astro",
|
id: "astro",
|
||||||
extensions: [".astro"],
|
extensions: [".astro"],
|
||||||
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||||
@@ -1064,9 +1063,9 @@ export namespace LSPServer {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JDTLS: Info = {
|
export const JDTLS: Info = {
|
||||||
id: "jdtls",
|
id: "jdtls",
|
||||||
root: async (file) => {
|
root: async (file) => {
|
||||||
// Without exclusions, NearestRoot defaults to instance directory so we can't
|
// Without exclusions, NearestRoot defaults to instance directory so we can't
|
||||||
@@ -1185,9 +1184,9 @@ export namespace LSPServer {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KotlinLS: Info = {
|
export const KotlinLS: Info = {
|
||||||
id: "kotlin-ls",
|
id: "kotlin-ls",
|
||||||
extensions: [".kt", ".kts"],
|
extensions: [".kt", ".kts"],
|
||||||
root: async (file) => {
|
root: async (file) => {
|
||||||
@@ -1284,9 +1283,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const YamlLS: Info = {
|
export const YamlLS: Info = {
|
||||||
id: "yaml-ls",
|
id: "yaml-ls",
|
||||||
extensions: [".yaml", ".yml"],
|
extensions: [".yaml", ".yml"],
|
||||||
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||||
@@ -1310,9 +1309,9 @@ export namespace LSPServer {
|
|||||||
process: proc,
|
process: proc,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LuaLS: Info = {
|
export const LuaLS: Info = {
|
||||||
id: "lua-ls",
|
id: "lua-ls",
|
||||||
root: NearestRoot([
|
root: NearestRoot([
|
||||||
".luarc.json",
|
".luarc.json",
|
||||||
@@ -1451,9 +1450,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PHPIntelephense: Info = {
|
export const PHPIntelephense: Info = {
|
||||||
id: "php intelephense",
|
id: "php intelephense",
|
||||||
extensions: [".php"],
|
extensions: [".php"],
|
||||||
root: NearestRoot(["composer.json", "composer.lock", ".php-version"]),
|
root: NearestRoot(["composer.json", "composer.lock", ".php-version"]),
|
||||||
@@ -1482,9 +1481,9 @@ export namespace LSPServer {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Prisma: Info = {
|
export const Prisma: Info = {
|
||||||
id: "prisma",
|
id: "prisma",
|
||||||
extensions: [".prisma"],
|
extensions: [".prisma"],
|
||||||
root: NearestRoot(["schema.prisma", "prisma/schema.prisma", "prisma"], ["package.json"]),
|
root: NearestRoot(["schema.prisma", "prisma/schema.prisma", "prisma"], ["package.json"]),
|
||||||
@@ -1500,9 +1499,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Dart: Info = {
|
export const Dart: Info = {
|
||||||
id: "dart",
|
id: "dart",
|
||||||
extensions: [".dart"],
|
extensions: [".dart"],
|
||||||
root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]),
|
root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]),
|
||||||
@@ -1518,9 +1517,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Ocaml: Info = {
|
export const Ocaml: Info = {
|
||||||
id: "ocaml-lsp",
|
id: "ocaml-lsp",
|
||||||
extensions: [".ml", ".mli"],
|
extensions: [".ml", ".mli"],
|
||||||
root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"]),
|
root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"]),
|
||||||
@@ -1536,8 +1535,8 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
export const BashLS: Info = {
|
export const BashLS: Info = {
|
||||||
id: "bash",
|
id: "bash",
|
||||||
extensions: [".sh", ".bash", ".zsh", ".ksh"],
|
extensions: [".sh", ".bash", ".zsh", ".ksh"],
|
||||||
root: async () => Instance.directory,
|
root: async () => Instance.directory,
|
||||||
@@ -1561,9 +1560,9 @@ export namespace LSPServer {
|
|||||||
process: proc,
|
process: proc,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TerraformLS: Info = {
|
export const TerraformLS: Info = {
|
||||||
id: "terraform",
|
id: "terraform",
|
||||||
extensions: [".tf", ".tfvars"],
|
extensions: [".tf", ".tfvars"],
|
||||||
root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]),
|
root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]),
|
||||||
@@ -1642,9 +1641,9 @@ export namespace LSPServer {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TexLab: Info = {
|
export const TexLab: Info = {
|
||||||
id: "texlab",
|
id: "texlab",
|
||||||
extensions: [".tex", ".bib"],
|
extensions: [".tex", ".bib"],
|
||||||
root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
|
root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
|
||||||
@@ -1730,9 +1729,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DockerfileLS: Info = {
|
export const DockerfileLS: Info = {
|
||||||
id: "dockerfile",
|
id: "dockerfile",
|
||||||
extensions: [".dockerfile", "Dockerfile"],
|
extensions: [".dockerfile", "Dockerfile"],
|
||||||
root: async () => Instance.directory,
|
root: async () => Instance.directory,
|
||||||
@@ -1756,9 +1755,9 @@ export namespace LSPServer {
|
|||||||
process: proc,
|
process: proc,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Gleam: Info = {
|
export const Gleam: Info = {
|
||||||
id: "gleam",
|
id: "gleam",
|
||||||
extensions: [".gleam"],
|
extensions: [".gleam"],
|
||||||
root: NearestRoot(["gleam.toml"]),
|
root: NearestRoot(["gleam.toml"]),
|
||||||
@@ -1774,9 +1773,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Clojure: Info = {
|
export const Clojure: Info = {
|
||||||
id: "clojure-lsp",
|
id: "clojure-lsp",
|
||||||
extensions: [".clj", ".cljs", ".cljc", ".edn"],
|
extensions: [".clj", ".cljs", ".cljc", ".edn"],
|
||||||
root: NearestRoot(["deps.edn", "project.clj", "shadow-cljs.edn", "bb.edn", "build.boot"]),
|
root: NearestRoot(["deps.edn", "project.clj", "shadow-cljs.edn", "bb.edn", "build.boot"]),
|
||||||
@@ -1795,9 +1794,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Nixd: Info = {
|
export const Nixd: Info = {
|
||||||
id: "nixd",
|
id: "nixd",
|
||||||
extensions: [".nix"],
|
extensions: [".nix"],
|
||||||
root: async (file) => {
|
root: async (file) => {
|
||||||
@@ -1826,9 +1825,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tinymist: Info = {
|
export const Tinymist: Info = {
|
||||||
id: "tinymist",
|
id: "tinymist",
|
||||||
extensions: [".typ", ".typc"],
|
extensions: [".typ", ".typc"],
|
||||||
root: NearestRoot(["typst.toml"]),
|
root: NearestRoot(["typst.toml"]),
|
||||||
@@ -1918,9 +1917,9 @@ export namespace LSPServer {
|
|||||||
process: spawn(bin, { cwd: root }),
|
process: spawn(bin, { cwd: root }),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HLS: Info = {
|
export const HLS: Info = {
|
||||||
id: "haskell-language-server",
|
id: "haskell-language-server",
|
||||||
extensions: [".hs", ".lhs"],
|
extensions: [".hs", ".lhs"],
|
||||||
root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"]),
|
root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"]),
|
||||||
@@ -1936,9 +1935,9 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JuliaLS: Info = {
|
export const JuliaLS: Info = {
|
||||||
id: "julials",
|
id: "julials",
|
||||||
extensions: [".jl"],
|
extensions: [".jl"],
|
||||||
root: NearestRoot(["Project.toml", "Manifest.toml", "*.jl"]),
|
root: NearestRoot(["Project.toml", "Manifest.toml", "*.jl"]),
|
||||||
@@ -1954,5 +1953,4 @@ export namespace LSPServer {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test, beforeEach } from "bun:test"
|
import { describe, expect, test, beforeEach } from "bun:test"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { LSPClient } from "../../src/lsp/client"
|
import { LSPClient } from "../../src/lsp"
|
||||||
import { LSPServer } from "../../src/lsp/server"
|
import { LSPServer } from "../../src/lsp"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { Log } from "../../src/util"
|
import { Log } from "../../src/util"
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, expect, spyOn } from "bun:test"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { LSP } from "../../src/lsp"
|
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 * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { LSP } from "../../src/lsp"
|
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 * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
|
|||||||
Reference in New Issue
Block a user