mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-04-20 21:00:29 +08:00
refactor(lsp): effectify client and server boundaries
This commit is contained in:
@@ -29,7 +29,7 @@ export const requiresExtensionsForCustomServers = Schema.makeFilter<
|
||||
boolean | Record<string, Schema.Schema.Type<typeof Entry>>
|
||||
>((data) => {
|
||||
if (typeof data === "boolean") return undefined
|
||||
const serverIds = new Set(Object.values(LSPServer).map((server) => server.id))
|
||||
const serverIds = new Set(Object.values(LSPServer.Builtins).map((server) => server.id))
|
||||
const ok = Object.entries(data).every(([id, config]) => {
|
||||
if ("disabled" in config && config.disabled) return true
|
||||
if (serverIds.has(id)) return true
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "path"
|
||||
import { pathToFileURL, fileURLToPath } from "url"
|
||||
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
|
||||
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
|
||||
import { Effect } from "effect"
|
||||
import { Log } from "../util"
|
||||
import { Process } from "../util"
|
||||
import { LANGUAGE_EXTENSIONS } from "./language"
|
||||
@@ -18,7 +19,19 @@ const DIAGNOSTICS_DEBOUNCE_MS = 150
|
||||
|
||||
const log = Log.create({ service: "lsp.client" })
|
||||
|
||||
export type Info = NonNullable<Awaited<ReturnType<typeof create>>>
|
||||
type Connection = ReturnType<typeof createMessageConnection>
|
||||
|
||||
export interface Info {
|
||||
readonly root: string
|
||||
readonly serverID: string
|
||||
readonly connection: Connection
|
||||
readonly notify: {
|
||||
readonly open: (input: { path: string }) => Effect.Effect<void>
|
||||
}
|
||||
readonly diagnostics: Map<string, Diagnostic[]>
|
||||
readonly waitForDiagnostics: (input: { path: string }) => Effect.Effect<void>
|
||||
readonly shutdown: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export type Diagnostic = VSCodeDiagnostic
|
||||
|
||||
@@ -39,7 +52,11 @@ export const Event = {
|
||||
),
|
||||
}
|
||||
|
||||
export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
|
||||
export const create = Effect.fn("LSPClient.create")(function* (input: {
|
||||
serverID: string
|
||||
server: LSPServer.Handle
|
||||
root: string
|
||||
}) {
|
||||
const l = log.clone().tag("serverID", input.serverID)
|
||||
l.info("starting client")
|
||||
|
||||
@@ -64,10 +81,7 @@ export async function create(input: { serverID: string; server: LSPServer.Handle
|
||||
l.info("window/workDoneProgress/create", params)
|
||||
return null
|
||||
})
|
||||
connection.onRequest("workspace/configuration", async () => {
|
||||
// Return server initialization options
|
||||
return [input.server.initialization ?? {}]
|
||||
})
|
||||
connection.onRequest("workspace/configuration", async () => [input.server.initialization ?? {}])
|
||||
connection.onRequest("client/registerCapability", async () => {})
|
||||
connection.onRequest("client/unregisterCapability", async () => {})
|
||||
connection.onRequest("workspace/workspaceFolders", async () => [
|
||||
@@ -79,145 +93,144 @@ export async function create(input: { serverID: string; server: LSPServer.Handle
|
||||
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,
|
||||
},
|
||||
workspace: {
|
||||
configuration: true,
|
||||
didChangeWatchedFiles: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
},
|
||||
textDocument: {
|
||||
synchronization: {
|
||||
didOpen: true,
|
||||
didChange: 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
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
log.info("workspace/didChangeWatchedFiles", input)
|
||||
await connection.sendNotification("workspace/didChangeWatchedFiles", {
|
||||
changes: [
|
||||
yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
withTimeout(
|
||||
connection.sendRequest("initialize", {
|
||||
rootUri: pathToFileURL(input.root).href,
|
||||
processId: input.server.process.pid,
|
||||
workspaceFolders: [
|
||||
{
|
||||
uri: pathToFileURL(input.path).href,
|
||||
type: 1, // Created
|
||||
name: "workspace",
|
||||
uri: pathToFileURL(input.root).href,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
log.info("textDocument/didOpen", input)
|
||||
diagnostics.delete(input.path)
|
||||
await connection.sendNotification("textDocument/didOpen", {
|
||||
textDocument: {
|
||||
uri: pathToFileURL(input.path).href,
|
||||
languageId,
|
||||
version: 0,
|
||||
text,
|
||||
initializationOptions: {
|
||||
...input.server.initialization,
|
||||
},
|
||||
})
|
||||
files[input.path] = 0
|
||||
return
|
||||
},
|
||||
},
|
||||
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),
|
||||
capabilities: {
|
||||
window: {
|
||||
workDoneProgress: true,
|
||||
},
|
||||
workspace: {
|
||||
configuration: true,
|
||||
didChangeWatchedFiles: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
},
|
||||
textDocument: {
|
||||
synchronization: {
|
||||
didOpen: true,
|
||||
didChange: true,
|
||||
},
|
||||
publishDiagnostics: {
|
||||
versionSupport: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
45_000,
|
||||
),
|
||||
catch: (error) => {
|
||||
l.error("initialize error", { error })
|
||||
return new InitializeError(
|
||||
{ serverID: input.serverID },
|
||||
{
|
||||
cause: error,
|
||||
},
|
||||
)
|
||||
log.info("waiting for diagnostics", { path: normalizedPath })
|
||||
let unsub: () => void
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | undefined
|
||||
return await withTimeout(
|
||||
},
|
||||
})
|
||||
|
||||
yield* Effect.tryPromise(() => connection.sendNotification("initialized", {}))
|
||||
|
||||
if (input.server.initialization) {
|
||||
yield* Effect.tryPromise(() =>
|
||||
connection.sendNotification("workspace/didChangeConfiguration", {
|
||||
settings: input.server.initialization,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const files: Record<string, number> = {}
|
||||
|
||||
const open = Effect.fn("LSPClient.notify.open")(function* (next: { path: string }) {
|
||||
next.path = path.isAbsolute(next.path) ? next.path : path.resolve(Instance.directory, next.path)
|
||||
const text = yield* Effect.promise(() => Filesystem.readText(next.path)).pipe(Effect.orDie)
|
||||
const extension = path.extname(next.path)
|
||||
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
|
||||
|
||||
const version = files[next.path]
|
||||
if (version !== undefined) {
|
||||
log.info("workspace/didChangeWatchedFiles", next)
|
||||
yield* Effect.tryPromise(() =>
|
||||
connection.sendNotification("workspace/didChangeWatchedFiles", {
|
||||
changes: [
|
||||
{
|
||||
uri: pathToFileURL(next.path).href,
|
||||
type: 2,
|
||||
},
|
||||
],
|
||||
}),
|
||||
).pipe(Effect.orDie)
|
||||
|
||||
const nextVersion = version + 1
|
||||
files[next.path] = nextVersion
|
||||
log.info("textDocument/didChange", {
|
||||
path: next.path,
|
||||
version: nextVersion,
|
||||
})
|
||||
yield* Effect.tryPromise(() =>
|
||||
connection.sendNotification("textDocument/didChange", {
|
||||
textDocument: {
|
||||
uri: pathToFileURL(next.path).href,
|
||||
version: nextVersion,
|
||||
},
|
||||
contentChanges: [{ text }],
|
||||
}),
|
||||
).pipe(Effect.orDie)
|
||||
return
|
||||
}
|
||||
|
||||
log.info("workspace/didChangeWatchedFiles", next)
|
||||
yield* Effect.tryPromise(() =>
|
||||
connection.sendNotification("workspace/didChangeWatchedFiles", {
|
||||
changes: [
|
||||
{
|
||||
uri: pathToFileURL(next.path).href,
|
||||
type: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
).pipe(Effect.orDie)
|
||||
|
||||
log.info("textDocument/didOpen", next)
|
||||
diagnostics.delete(next.path)
|
||||
yield* Effect.tryPromise(() =>
|
||||
connection.sendNotification("textDocument/didOpen", {
|
||||
textDocument: {
|
||||
uri: pathToFileURL(next.path).href,
|
||||
languageId,
|
||||
version: 0,
|
||||
text,
|
||||
},
|
||||
}),
|
||||
).pipe(Effect.orDie)
|
||||
files[next.path] = 0
|
||||
})
|
||||
|
||||
const waitForDiagnostics = Effect.fn("LSPClient.waitForDiagnostics")(function* (next: { path: string }) {
|
||||
const normalizedPath = Filesystem.normalizePath(
|
||||
path.isAbsolute(next.path) ? next.path : path.resolve(Instance.directory, next.path),
|
||||
)
|
||||
log.info("waiting for diagnostics", { path: normalizedPath })
|
||||
let unsub: (() => void) | undefined
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | undefined
|
||||
yield* Effect.promise(() =>
|
||||
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 (event.properties.path === normalizedPath && event.properties.serverID === input.serverID) {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => {
|
||||
log.info("got diagnostics", { path: normalizedPath })
|
||||
@@ -228,23 +241,43 @@ export async function create(input: { serverID: string; server: LSPServer.Handle
|
||||
})
|
||||
}),
|
||||
3000,
|
||||
)
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
),
|
||||
).pipe(
|
||||
Effect.catch(() => Effect.void),
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
unsub?.()
|
||||
})
|
||||
},
|
||||
async shutdown() {
|
||||
l.info("shutting down")
|
||||
connection.end()
|
||||
connection.dispose()
|
||||
await Process.stop(input.server.process)
|
||||
l.info("shutdown")
|
||||
},
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
const shutdown = Effect.fn("LSPClient.shutdown")(function* () {
|
||||
l.info("shutting down")
|
||||
connection.end()
|
||||
connection.dispose()
|
||||
yield* Effect.promise(() => Process.stop(input.server.process)).pipe(Effect.orDie)
|
||||
l.info("shutdown")
|
||||
})
|
||||
|
||||
l.info("initialized")
|
||||
|
||||
return result
|
||||
}
|
||||
return {
|
||||
root: input.root,
|
||||
get serverID() {
|
||||
return input.serverID
|
||||
},
|
||||
get connection() {
|
||||
return connection
|
||||
},
|
||||
notify: {
|
||||
open,
|
||||
},
|
||||
get diagnostics() {
|
||||
return diagnostics
|
||||
},
|
||||
waitForDiagnostics,
|
||||
shutdown,
|
||||
} satisfies Info
|
||||
})
|
||||
|
||||
@@ -134,7 +134,7 @@ interface State {
|
||||
clients: LSPClient.Info[]
|
||||
servers: Record<string, LSPServer.Info>
|
||||
broken: Set<string>
|
||||
spawning: Map<string, Promise<LSPClient.Info | undefined>>
|
||||
spawning: Map<string, Effect.Effect<LSPClient.Info | undefined>>
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
@@ -170,7 +170,7 @@ export const layer = Layer.effect(
|
||||
if (!cfg.lsp) {
|
||||
log.info("all LSPs are disabled")
|
||||
} else {
|
||||
for (const server of Object.values(LSPServer)) {
|
||||
for (const server of Object.values(LSPServer.Builtins)) {
|
||||
servers[server.id] = server
|
||||
}
|
||||
|
||||
@@ -187,15 +187,16 @@ export const layer = Layer.effect(
|
||||
servers[name] = {
|
||||
...existing,
|
||||
id: name,
|
||||
root: existing?.root ?? (async () => Instance.directory),
|
||||
root: existing?.root ?? (() => Effect.succeed(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,
|
||||
}),
|
||||
spawn: (root) =>
|
||||
Effect.sync(() => ({
|
||||
process: lspspawn(item.command[0], item.command.slice(1), {
|
||||
cwd: root,
|
||||
env: { ...process.env, ...item.env },
|
||||
}),
|
||||
initialization: item.initialization,
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,110 +216,121 @@ export const layer = Layer.effect(
|
||||
}
|
||||
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(async () => {
|
||||
await Promise.all(s.clients.map((client) => client.shutdown()))
|
||||
}),
|
||||
Effect.forEach(s.clients, (client) => client.shutdown(), { concurrency: "unbounded", discard: true }),
|
||||
)
|
||||
|
||||
return s
|
||||
}),
|
||||
)
|
||||
|
||||
const request = Effect.fnUntraced(function* <A>(
|
||||
client: LSPClient.Info,
|
||||
method: string,
|
||||
params: unknown,
|
||||
fallback: A,
|
||||
) {
|
||||
return yield* (Effect.tryPromise(() => client.connection.sendRequest<A>(method, params)).pipe(
|
||||
Effect.catch(() => Effect.succeed(fallback)),
|
||||
))
|
||||
})
|
||||
|
||||
const scheduleClient = Effect.fnUntraced(function* (s: State, server: LSPServer.Info, root: string, key: string) {
|
||||
const handle = yield* (server.spawn(root).pipe(
|
||||
Effect.catch((error: unknown) =>
|
||||
Effect.sync(() => {
|
||||
s.broken.add(key)
|
||||
log.error(`Failed to spawn LSP server ${server.id}`, { error })
|
||||
}).pipe(Effect.as(undefined)),
|
||||
),
|
||||
))
|
||||
if (!handle) {
|
||||
s.broken.add(key)
|
||||
return undefined
|
||||
}
|
||||
|
||||
log.info("spawned lsp server", { serverID: server.id, root })
|
||||
|
||||
const client = yield* LSPClient.create({
|
||||
serverID: server.id,
|
||||
server: handle,
|
||||
root,
|
||||
}).pipe(
|
||||
Effect.catch((error: unknown) =>
|
||||
Effect.gen(function* () {
|
||||
s.broken.add(key)
|
||||
yield* (Effect.promise(() => Process.stop(handle.process)).pipe(Effect.catch(() => Effect.void)))
|
||||
log.error(`Failed to initialize LSP client ${server.id}`, { error })
|
||||
return undefined
|
||||
}),
|
||||
),
|
||||
)
|
||||
if (!client) return undefined
|
||||
|
||||
const existing = s.clients.find((x) => x.root === root && x.serverID === server.id)
|
||||
if (existing) {
|
||||
yield* (Effect.promise(() => Process.stop(handle.process)).pipe(Effect.catch(() => Effect.void)))
|
||||
return existing
|
||||
}
|
||||
|
||||
s.clients.push(client)
|
||||
return client
|
||||
})
|
||||
|
||||
const awaitSpawn = Effect.fnUntraced(function* (s: State, server: LSPServer.Info, root: string, key: string) {
|
||||
const inflight = s.spawning.get(key)
|
||||
if (inflight) return yield* inflight
|
||||
|
||||
const task = yield* Effect.cached(scheduleClient(s, server, root, key))
|
||||
s.spawning.set(key, task)
|
||||
return yield* task.pipe(
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
if (s.spawning.get(key) === task) s.spawning.delete(key)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
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[] = []
|
||||
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
|
||||
})
|
||||
for (const server of Object.values(s.servers)) {
|
||||
if (server.extensions.length && !server.extensions.includes(extension)) continue
|
||||
|
||||
if (!handle) return undefined
|
||||
log.info("spawned lsp server", { serverID: server.id, root })
|
||||
const root = yield* server.root(file)
|
||||
if (!root) continue
|
||||
|
||||
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
|
||||
})
|
||||
const key = root + server.id
|
||||
if (s.broken.has(key)) continue
|
||||
|
||||
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
|
||||
const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
|
||||
if (match) {
|
||||
result.push(match)
|
||||
continue
|
||||
}
|
||||
|
||||
for (const server of Object.values(s.servers)) {
|
||||
if (server.extensions.length && !server.extensions.includes(extension)) continue
|
||||
const hadInflight = s.spawning.has(key)
|
||||
const client = yield* awaitSpawn(s, server, root, key)
|
||||
if (!client) continue
|
||||
|
||||
const root = await server.root(file)
|
||||
if (!root) continue
|
||||
if (s.broken.has(root + server.id)) continue
|
||||
result.push(client)
|
||||
if (!hadInflight) Bus.publish(Event.Updated, {})
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
const run = Effect.fnUntraced(function* <T>(file: string, fn: (client: LSPClient.Info) => Promise<T>) {
|
||||
const run = Effect.fnUntraced(function* <T>(file: string, fn: (client: LSPClient.Info) => Effect.Effect<T>) {
|
||||
const clients = yield* getClients(file)
|
||||
return yield* Effect.promise(() => Promise.all(clients.map((x) => fn(x))))
|
||||
return yield* Effect.forEach(clients, fn, { concurrency: "unbounded" })
|
||||
})
|
||||
|
||||
const runAll = Effect.fnUntraced(function* <T>(fn: (client: LSPClient.Info) => Promise<T>) {
|
||||
const runAll = Effect.fnUntraced(function* <T>(fn: (client: LSPClient.Info) => Effect.Effect<T>) {
|
||||
const s = yield* InstanceState.get(state)
|
||||
return yield* Effect.promise(() => Promise.all(s.clients.map((x) => fn(x))))
|
||||
return yield* Effect.forEach(s.clients, fn, { concurrency: "unbounded" })
|
||||
})
|
||||
|
||||
const init = Effect.fn("LSP.init")(function* () {
|
||||
@@ -341,38 +353,40 @@ export const layer = Layer.effect(
|
||||
|
||||
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 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 = yield* 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(() =>
|
||||
yield* Effect.tryPromise(() =>
|
||||
Promise.all(
|
||||
clients.map(async (client) => {
|
||||
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
|
||||
await client.notify.open({ path: input })
|
||||
const wait = waitForDiagnostics ? Effect.runPromise(client.waitForDiagnostics({ path: input })) : Promise.resolve()
|
||||
await Effect.runPromise(client.notify.open({ path: input }))
|
||||
return wait
|
||||
}),
|
||||
).catch((err) => {
|
||||
log.error("failed to touch file", { err, file: input })
|
||||
}),
|
||||
),
|
||||
).pipe(
|
||||
Effect.catch((err: unknown) =>
|
||||
Effect.sync(() => {
|
||||
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)
|
||||
const all = yield* runAll((client) => Effect.succeed(client.diagnostics))
|
||||
for (const result of all) {
|
||||
for (const [p, diags] of result.entries()) {
|
||||
const arr = results[p] || []
|
||||
@@ -385,78 +399,65 @@ export const layer = Layer.effect(
|
||||
|
||||
const hover = Effect.fn("LSP.hover")(function* (input: LocInput) {
|
||||
return yield* run(input.file, (client) =>
|
||||
client.connection
|
||||
.sendRequest("textDocument/hover", {
|
||||
request(client, "textDocument/hover", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
})
|
||||
.catch(() => null),
|
||||
}, null),
|
||||
)
|
||||
})
|
||||
|
||||
const definition = Effect.fn("LSP.definition")(function* (input: LocInput) {
|
||||
const results = yield* run(input.file, (client) =>
|
||||
client.connection
|
||||
.sendRequest("textDocument/definition", {
|
||||
request(client, "textDocument/definition", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
})
|
||||
.catch(() => null),
|
||||
}, 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", {
|
||||
request(client, "textDocument/references", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
context: { includeDeclaration: true },
|
||||
})
|
||||
.catch(() => []),
|
||||
}, [] as any[]),
|
||||
)
|
||||
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", {
|
||||
request(client, "textDocument/implementation", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
})
|
||||
.catch(() => null),
|
||||
}, 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(() => []),
|
||||
)
|
||||
const results = yield* run(file, (client) => request(client, "textDocument/documentSymbol", { textDocument: { uri } }, [] as any[]))
|
||||
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<Symbol[]>("workspace/symbol", { query })
|
||||
.then((result) => result.filter((x) => kinds.includes(x.kind)).slice(0, 10))
|
||||
.catch(() => [] as Symbol[]),
|
||||
request(client, "workspace/symbol", { query }, [] as Symbol[]).pipe(
|
||||
Effect.map((result) => result.filter((x) => kinds.includes(x.kind)).slice(0, 10)),
|
||||
),
|
||||
)
|
||||
return results.flat()
|
||||
})
|
||||
|
||||
const prepareCallHierarchy = Effect.fn("LSP.prepareCallHierarchy")(function* (input: LocInput) {
|
||||
const results = yield* run(input.file, (client) =>
|
||||
client.connection
|
||||
.sendRequest("textDocument/prepareCallHierarchy", {
|
||||
request(client, "textDocument/prepareCallHierarchy", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
})
|
||||
.catch(() => []),
|
||||
}, [] as any[]),
|
||||
)
|
||||
return results.flat().filter(Boolean)
|
||||
})
|
||||
@@ -465,16 +466,16 @@ export const layer = Layer.effect(
|
||||
input: LocInput,
|
||||
direction: "callHierarchy/incomingCalls" | "callHierarchy/outgoingCalls",
|
||||
) {
|
||||
const results = yield* run(input.file, async (client) => {
|
||||
const items = await client.connection
|
||||
.sendRequest<unknown[] | null>("textDocument/prepareCallHierarchy", {
|
||||
const results = yield* run(input.file, (client) =>
|
||||
Effect.gen(function* () {
|
||||
const items = yield* request(client, "textDocument/prepareCallHierarchy", {
|
||||
textDocument: { uri: pathToFileURL(input.file).href },
|
||||
position: { line: input.line, character: input.character },
|
||||
})
|
||||
.catch(() => [] as unknown[])
|
||||
if (!items?.length) return []
|
||||
return client.connection.sendRequest(direction, { item: items[0] }).catch(() => [])
|
||||
})
|
||||
}, [] as unknown[])
|
||||
if (!items.length) return []
|
||||
return yield* request(client, direction, { item: items[0] }, [] as unknown[])
|
||||
}),
|
||||
)
|
||||
return results.flat().filter(Boolean)
|
||||
})
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { which } from "../util/which"
|
||||
import { Module } from "@opencode-ai/shared/util/module"
|
||||
import { spawn } from "./launch"
|
||||
import { Npm } from "../npm"
|
||||
import { Effect } from "effect"
|
||||
|
||||
const log = Log.create({ service: "lsp.server" })
|
||||
const pathExists = async (p: string) =>
|
||||
@@ -29,9 +30,10 @@ export interface Handle {
|
||||
initialization?: Record<string, any>
|
||||
}
|
||||
|
||||
type RootFunction = (file: string) => Promise<string | undefined>
|
||||
type RawRootFunction = (file: string) => Promise<string | undefined>
|
||||
type RootFunction = (file: string) => Effect.Effect<string | undefined>
|
||||
|
||||
const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): RootFunction => {
|
||||
const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): RawRootFunction => {
|
||||
return async (file) => {
|
||||
if (excludePatterns) {
|
||||
const excludedFiles = Filesystem.up({
|
||||
@@ -55,15 +57,36 @@ const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): Roo
|
||||
}
|
||||
}
|
||||
|
||||
export interface RawInfo {
|
||||
id: string
|
||||
extensions: string[]
|
||||
global?: boolean
|
||||
root: RawRootFunction
|
||||
spawn(root: string): Promise<Handle | undefined>
|
||||
}
|
||||
|
||||
export interface Info {
|
||||
id: string
|
||||
extensions: string[]
|
||||
global?: boolean
|
||||
root: RootFunction
|
||||
spawn(root: string): Promise<Handle | undefined>
|
||||
spawn(root: string): Effect.Effect<Handle | undefined>
|
||||
}
|
||||
|
||||
export const Deno: Info = {
|
||||
const effectify = (info: RawInfo): Info => ({
|
||||
...info,
|
||||
root: (file) => Effect.promise(() => info.root(file)),
|
||||
spawn: (root) => Effect.promise(() => info.spawn(root)),
|
||||
})
|
||||
|
||||
const effectifyAll = <T extends Record<string, RawInfo>>(infos: T): { [K in keyof T]: Info } =>
|
||||
Object.fromEntries(Object.entries(infos).map(([key, value]) => [key, effectify(value)])) as { [K in keyof T]: Info }
|
||||
|
||||
// Temporary migration bridge: `Builtins` exposes Effect-shaped `root` / `spawn`
|
||||
// while the per-server definitions still use their older Promise bodies.
|
||||
// Follow-up: convert the individual server definitions in place and delete this wrapper.
|
||||
|
||||
export const Deno: RawInfo = {
|
||||
id: "deno",
|
||||
root: async (file) => {
|
||||
const files = Filesystem.up({
|
||||
@@ -91,7 +114,7 @@ export const Deno: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Typescript: Info = {
|
||||
export const Typescript: RawInfo = {
|
||||
id: "typescript",
|
||||
root: NearestRoot(
|
||||
["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"],
|
||||
@@ -121,7 +144,7 @@ export const Typescript: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Vue: Info = {
|
||||
export const Vue: RawInfo = {
|
||||
id: "vue",
|
||||
extensions: [".vue"],
|
||||
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||
@@ -150,7 +173,7 @@ export const Vue: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const ESLint: Info = {
|
||||
export const ESLint: RawInfo = {
|
||||
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"],
|
||||
@@ -207,7 +230,7 @@ export const ESLint: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Oxlint: Info = {
|
||||
export const Oxlint: RawInfo = {
|
||||
id: "oxlint",
|
||||
root: NearestRoot([
|
||||
".oxlintrc.json",
|
||||
@@ -280,7 +303,7 @@ export const Oxlint: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Biome: Info = {
|
||||
export const Biome: RawInfo = {
|
||||
id: "biome",
|
||||
root: NearestRoot([
|
||||
"biome.json",
|
||||
@@ -342,7 +365,7 @@ export const Biome: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Gopls: Info = {
|
||||
export const Gopls: RawInfo = {
|
||||
id: "gopls",
|
||||
root: async (file) => {
|
||||
const work = await NearestRoot(["go.work"])(file)
|
||||
@@ -381,7 +404,7 @@ export const Gopls: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Rubocop: Info = {
|
||||
export const Rubocop: RawInfo = {
|
||||
id: "ruby-lsp",
|
||||
root: NearestRoot(["Gemfile"]),
|
||||
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
||||
@@ -419,7 +442,7 @@ export const Rubocop: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Ty: Info = {
|
||||
export const Ty: RawInfo = {
|
||||
id: "ty",
|
||||
extensions: [".py", ".pyi"],
|
||||
root: NearestRoot([
|
||||
@@ -481,7 +504,7 @@ export const Ty: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Pyright: Info = {
|
||||
export const Pyright: RawInfo = {
|
||||
id: "pyright",
|
||||
extensions: [".py", ".pyi"],
|
||||
root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
|
||||
@@ -525,7 +548,7 @@ export const Pyright: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const ElixirLS: Info = {
|
||||
export const ElixirLS: RawInfo = {
|
||||
id: "elixir-ls",
|
||||
extensions: [".ex", ".exs"],
|
||||
root: NearestRoot(["mix.exs", "mix.lock"]),
|
||||
@@ -588,7 +611,7 @@ export const ElixirLS: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Zls: Info = {
|
||||
export const Zls: RawInfo = {
|
||||
id: "zls",
|
||||
extensions: [".zig", ".zon"],
|
||||
root: NearestRoot(["build.zig"]),
|
||||
@@ -700,7 +723,7 @@ export const Zls: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const CSharp: Info = {
|
||||
export const CSharp: RawInfo = {
|
||||
id: "csharp",
|
||||
root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
|
||||
extensions: [".cs"],
|
||||
@@ -737,7 +760,7 @@ export const CSharp: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const FSharp: Info = {
|
||||
export const FSharp: RawInfo = {
|
||||
id: "fsharp",
|
||||
root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]),
|
||||
extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
|
||||
@@ -774,7 +797,7 @@ export const FSharp: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const SourceKit: Info = {
|
||||
export const SourceKit: RawInfo = {
|
||||
id: "sourcekit-lsp",
|
||||
extensions: [".swift", ".objc", "objcpp"],
|
||||
root: NearestRoot(["Package.swift", "*.xcodeproj", "*.xcworkspace"]),
|
||||
@@ -808,7 +831,7 @@ export const SourceKit: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const RustAnalyzer: Info = {
|
||||
export const RustAnalyzer: RawInfo = {
|
||||
id: "rust",
|
||||
root: async (root) => {
|
||||
const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root)
|
||||
@@ -854,7 +877,7 @@ export const RustAnalyzer: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Clangd: Info = {
|
||||
export const Clangd: RawInfo = {
|
||||
id: "clangd",
|
||||
root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd"]),
|
||||
extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
|
||||
@@ -1000,7 +1023,7 @@ export const Clangd: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Svelte: Info = {
|
||||
export const Svelte: RawInfo = {
|
||||
id: "svelte",
|
||||
extensions: [".svelte"],
|
||||
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||
@@ -1027,7 +1050,7 @@ export const Svelte: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Astro: Info = {
|
||||
export const Astro: RawInfo = {
|
||||
id: "astro",
|
||||
extensions: [".astro"],
|
||||
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||
@@ -1065,7 +1088,7 @@ export const Astro: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const JDTLS: Info = {
|
||||
export const JDTLS: RawInfo = {
|
||||
id: "jdtls",
|
||||
root: async (file) => {
|
||||
// Without exclusions, NearestRoot defaults to instance directory so we can't
|
||||
@@ -1186,7 +1209,7 @@ export const JDTLS: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const KotlinLS: Info = {
|
||||
export const KotlinLS: RawInfo = {
|
||||
id: "kotlin-ls",
|
||||
extensions: [".kt", ".kts"],
|
||||
root: async (file) => {
|
||||
@@ -1285,7 +1308,7 @@ export const KotlinLS: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const YamlLS: Info = {
|
||||
export const YamlLS: RawInfo = {
|
||||
id: "yaml-ls",
|
||||
extensions: [".yaml", ".yml"],
|
||||
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||
@@ -1311,7 +1334,7 @@ export const YamlLS: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const LuaLS: Info = {
|
||||
export const LuaLS: RawInfo = {
|
||||
id: "lua-ls",
|
||||
root: NearestRoot([
|
||||
".luarc.json",
|
||||
@@ -1452,7 +1475,7 @@ export const LuaLS: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const PHPIntelephense: Info = {
|
||||
export const PHPIntelephense: RawInfo = {
|
||||
id: "php intelephense",
|
||||
extensions: [".php"],
|
||||
root: NearestRoot(["composer.json", "composer.lock", ".php-version"]),
|
||||
@@ -1483,7 +1506,7 @@ export const PHPIntelephense: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Prisma: Info = {
|
||||
export const Prisma: RawInfo = {
|
||||
id: "prisma",
|
||||
extensions: [".prisma"],
|
||||
root: NearestRoot(["schema.prisma", "prisma/schema.prisma", "prisma"], ["package.json"]),
|
||||
@@ -1501,7 +1524,7 @@ export const Prisma: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Dart: Info = {
|
||||
export const Dart: RawInfo = {
|
||||
id: "dart",
|
||||
extensions: [".dart"],
|
||||
root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]),
|
||||
@@ -1519,7 +1542,7 @@ export const Dart: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Ocaml: Info = {
|
||||
export const Ocaml: RawInfo = {
|
||||
id: "ocaml-lsp",
|
||||
extensions: [".ml", ".mli"],
|
||||
root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"]),
|
||||
@@ -1536,7 +1559,7 @@ export const Ocaml: Info = {
|
||||
}
|
||||
},
|
||||
}
|
||||
export const BashLS: Info = {
|
||||
export const BashLS: RawInfo = {
|
||||
id: "bash",
|
||||
extensions: [".sh", ".bash", ".zsh", ".ksh"],
|
||||
root: async () => Instance.directory,
|
||||
@@ -1562,7 +1585,7 @@ export const BashLS: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const TerraformLS: Info = {
|
||||
export const TerraformLS: RawInfo = {
|
||||
id: "terraform",
|
||||
extensions: [".tf", ".tfvars"],
|
||||
root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]),
|
||||
@@ -1643,7 +1666,7 @@ export const TerraformLS: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const TexLab: Info = {
|
||||
export const TexLab: RawInfo = {
|
||||
id: "texlab",
|
||||
extensions: [".tex", ".bib"],
|
||||
root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
|
||||
@@ -1731,7 +1754,7 @@ export const TexLab: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const DockerfileLS: Info = {
|
||||
export const DockerfileLS: RawInfo = {
|
||||
id: "dockerfile",
|
||||
extensions: [".dockerfile", "Dockerfile"],
|
||||
root: async () => Instance.directory,
|
||||
@@ -1757,7 +1780,7 @@ export const DockerfileLS: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Gleam: Info = {
|
||||
export const Gleam: RawInfo = {
|
||||
id: "gleam",
|
||||
extensions: [".gleam"],
|
||||
root: NearestRoot(["gleam.toml"]),
|
||||
@@ -1775,7 +1798,7 @@ export const Gleam: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Clojure: Info = {
|
||||
export const Clojure: RawInfo = {
|
||||
id: "clojure-lsp",
|
||||
extensions: [".clj", ".cljs", ".cljc", ".edn"],
|
||||
root: NearestRoot(["deps.edn", "project.clj", "shadow-cljs.edn", "bb.edn", "build.boot"]),
|
||||
@@ -1796,7 +1819,7 @@ export const Clojure: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Nixd: Info = {
|
||||
export const Nixd: RawInfo = {
|
||||
id: "nixd",
|
||||
extensions: [".nix"],
|
||||
root: async (file) => {
|
||||
@@ -1827,7 +1850,7 @@ export const Nixd: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Tinymist: Info = {
|
||||
export const Tinymist: RawInfo = {
|
||||
id: "tinymist",
|
||||
extensions: [".typ", ".typc"],
|
||||
root: NearestRoot(["typst.toml"]),
|
||||
@@ -1919,7 +1942,7 @@ export const Tinymist: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const HLS: Info = {
|
||||
export const HLS: RawInfo = {
|
||||
id: "haskell-language-server",
|
||||
extensions: [".hs", ".lhs"],
|
||||
root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"]),
|
||||
@@ -1937,7 +1960,7 @@ export const HLS: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const JuliaLS: Info = {
|
||||
export const JuliaLS: RawInfo = {
|
||||
id: "julials",
|
||||
extensions: [".jl"],
|
||||
root: NearestRoot(["Project.toml", "Manifest.toml", "*.jl"]),
|
||||
@@ -1954,3 +1977,43 @@ export const JuliaLS: Info = {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const Builtins = effectifyAll({
|
||||
Deno,
|
||||
Typescript,
|
||||
Vue,
|
||||
ESLint,
|
||||
Oxlint,
|
||||
Biome,
|
||||
Gopls,
|
||||
Rubocop,
|
||||
Ty,
|
||||
Pyright,
|
||||
ElixirLS,
|
||||
Zls,
|
||||
CSharp,
|
||||
FSharp,
|
||||
SourceKit,
|
||||
RustAnalyzer,
|
||||
Clangd,
|
||||
Svelte,
|
||||
Astro,
|
||||
JDTLS,
|
||||
KotlinLS,
|
||||
YamlLS,
|
||||
LuaLS,
|
||||
PHPIntelephense,
|
||||
Prisma,
|
||||
Dart,
|
||||
Ocaml,
|
||||
BashLS,
|
||||
TerraformLS,
|
||||
TexLab,
|
||||
DockerfileLS,
|
||||
Gleam,
|
||||
Clojure,
|
||||
Nixd,
|
||||
Tinymist,
|
||||
HLS,
|
||||
JuliaLS,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test, beforeEach } from "bun:test"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { LSPClient } from "../../src/lsp"
|
||||
import { LSPServer } from "../../src/lsp"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
@@ -27,11 +28,13 @@ describe("LSPClient interop", () => {
|
||||
const client = await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
fn: () =>
|
||||
LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: process.cwd(),
|
||||
}),
|
||||
Effect.runPromise(
|
||||
LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: process.cwd(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
await client.connection.sendNotification("test/trigger", {
|
||||
@@ -42,7 +45,7 @@ describe("LSPClient interop", () => {
|
||||
|
||||
expect(client.connection).toBeDefined()
|
||||
|
||||
await client.shutdown()
|
||||
await Effect.runPromise(client.shutdown())
|
||||
})
|
||||
|
||||
test("handles client/registerCapability request", async () => {
|
||||
@@ -51,11 +54,13 @@ describe("LSPClient interop", () => {
|
||||
const client = await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
fn: () =>
|
||||
LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: process.cwd(),
|
||||
}),
|
||||
Effect.runPromise(
|
||||
LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: process.cwd(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
await client.connection.sendNotification("test/trigger", {
|
||||
@@ -66,7 +71,7 @@ describe("LSPClient interop", () => {
|
||||
|
||||
expect(client.connection).toBeDefined()
|
||||
|
||||
await client.shutdown()
|
||||
await Effect.runPromise(client.shutdown())
|
||||
})
|
||||
|
||||
test("handles client/unregisterCapability request", async () => {
|
||||
@@ -75,11 +80,13 @@ describe("LSPClient interop", () => {
|
||||
const client = await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
fn: () =>
|
||||
LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: process.cwd(),
|
||||
}),
|
||||
Effect.runPromise(
|
||||
LSPClient.create({
|
||||
serverID: "fake",
|
||||
server: handle as unknown as LSPServer.Handle,
|
||||
root: process.cwd(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
await client.connection.sendNotification("test/trigger", {
|
||||
@@ -90,6 +97,6 @@ describe("LSPClient interop", () => {
|
||||
|
||||
expect(client.connection).toBeDefined()
|
||||
|
||||
await client.shutdown()
|
||||
await Effect.runPromise(client.shutdown())
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user