diff --git a/packages/opencode/specs/effect-migration.md b/packages/opencode/specs/effect-migration.md
index 4f195917fde..8e29e0d2d28 100644
--- a/packages/opencode/specs/effect-migration.md
+++ b/packages/opencode/specs/effect-migration.md
@@ -140,5 +140,5 @@ Still open and likely worth migrating:
- [ ] `SessionCompaction`
- [ ] `Provider`
- [ ] `Project`
-- [ ] `LSP`
+- [x] `LSP`
- [ ] `MCP`
diff --git a/packages/opencode/src/effect/instances.ts b/packages/opencode/src/effect/instances.ts
index 6fcfddb24f5..d197ba2c2a2 100644
--- a/packages/opencode/src/effect/instances.ts
+++ b/packages/opencode/src/effect/instances.ts
@@ -3,6 +3,7 @@ import { File } from "@/file/service"
import { FileTime } from "@/file/time-service"
import { FileWatcher } from "@/file/watcher"
import { Format } from "@/format/service"
+import { LSP } from "@/lsp"
import { Permission } from "@/permission/service"
import { Instance } from "@/project/instance"
import { Vcs } from "@/project/vcs"
@@ -26,6 +27,7 @@ export type InstanceServices =
| File.Service
| Skill.Service
| Snapshot.Service
+ | LSP.Service
// NOTE: LayerMap only passes the key (directory string) to lookup, but we need
// the full instance context (directory, worktree, project). We read from the
@@ -46,6 +48,7 @@ function lookup(_key: string) {
File.layer,
Skill.defaultLayer,
Snapshot.defaultLayer,
+ LSP.layer,
).pipe(Layer.provide(ctx))
}
diff --git a/packages/opencode/src/effect/runtime.ts b/packages/opencode/src/effect/runtime.ts
index e6f1f326262..2cbc4b9e7e6 100644
--- a/packages/opencode/src/effect/runtime.ts
+++ b/packages/opencode/src/effect/runtime.ts
@@ -20,6 +20,10 @@ export function runPromiseInstance(effect: Effect.Effect(effect: Effect.Effect) {
+ return runtime.runSync(effect.pipe(Effect.provide(Instances.get(Instance.directory))))
+}
+
export function disposeRuntime() {
return runtime.dispose()
}
diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts
index 2eb1ad93e98..3b8b749ba12 100644
--- a/packages/opencode/src/lsp/index.ts
+++ b/packages/opencode/src/lsp/index.ts
@@ -1,5 +1,7 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
+import { InstanceContext } from "@/effect/instance-context"
+import { runPromiseInstance } from "@/effect/runtime"
import { Log } from "../util/log"
import { LSPClient } from "./client"
import path from "path"
@@ -7,10 +9,10 @@ import { pathToFileURL, fileURLToPath } from "url"
import { LSPServer } from "./server"
import z from "zod"
import { Config } from "../config/config"
-import { Instance } from "../project/instance"
import { Flag } from "@/flag/flag"
import { Process } from "../util/process"
import { spawn as lspspawn } from "./launch"
+import { Effect, Fiber, Layer, ServiceMap } from "effect"
export namespace LSP {
const log = Log.create({ service: "lsp" })
@@ -77,77 +79,6 @@ export namespace LSP {
}
}
- const state = Instance.state(
- async () => {
- const clients: LSPClient.Info[] = []
- const servers: Record = {}
- const cfg = await Config.get()
-
- if (cfg.lsp === false) {
- log.info("all LSPs are disabled")
- return {
- broken: new Set(),
- servers,
- clients,
- spawning: new Map>(),
- }
- }
-
- 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) => {
- return {
- 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(", "),
- })
-
- return {
- broken: new Set(),
- servers,
- clients,
- spawning: new Map>(),
- }
- },
- async (state) => {
- await Promise.all(state.clients.map((client) => client.shutdown()))
- },
- )
-
- export async function init() {
- return state()
- }
-
export const Status = z
.object({
id: z.string(),
@@ -160,162 +91,6 @@ export namespace LSP {
})
export type Status = z.infer
- export async function status() {
- return state().then((x) => {
- const result: Status[] = []
- for (const client of x.clients) {
- result.push({
- id: client.serverID,
- name: x.servers[client.serverID].id,
- root: path.relative(Instance.directory, client.root),
- status: "connected",
- })
- }
- return result
- })
- }
-
- async function getClients(file: string) {
- const s = await state()
- 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 })
-
- 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
- }
-
- export async function hasClients(file: string) {
- const s = await state()
- 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
- }
-
- export async function touchFile(input: string, waitForDiagnostics?: boolean) {
- log.info("touching file", { file: input })
- const clients = await getClients(input)
- await 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 })
- })
- }
-
- export async function diagnostics() {
- const results: Record = {}
- for (const result of await runAll(async (client) => client.diagnostics)) {
- for (const [path, diagnostics] of result.entries()) {
- const arr = results[path] || []
- arr.push(...diagnostics)
- results[path] = arr
- }
- }
- return results
- }
-
- export async function hover(input: { file: string; line: number; character: number }) {
- return run(input.file, (client) => {
- return client.connection
- .sendRequest("textDocument/hover", {
- textDocument: {
- uri: pathToFileURL(input.file).href,
- },
- position: {
- line: input.line,
- character: input.character,
- },
- })
- .catch(() => null)
- })
- }
-
enum SymbolKind {
File = 1,
Module = 2,
@@ -356,114 +131,504 @@ export namespace LSP {
SymbolKind.Enum,
]
- export async function workspaceSymbol(query: string) {
- return runAll((client) =>
- client.connection
- .sendRequest("workspace/symbol", {
- query,
+ export interface Interface {
+ readonly init: () => Effect.Effect
+ readonly status: () => Effect.Effect
+ readonly hasClients: (file: string) => Effect.Effect
+ readonly touchFile: (input: string, waitForDiagnostics?: boolean) => Effect.Effect
+ readonly diagnostics: () => Effect.Effect>
+ readonly hover: (input: { file: string; line: number; character: number }) => Effect.Effect
+ readonly workspaceSymbol: (query: string) => Effect.Effect
+ readonly documentSymbol: (uri: string) => Effect.Effect<(LSP.DocumentSymbol | LSP.Symbol)[]>
+ readonly definition: (input: { file: string; line: number; character: number }) => Effect.Effect
+ readonly references: (input: { file: string; line: number; character: number }) => Effect.Effect
+ readonly implementation: (input: { file: string; line: number; character: number }) => Effect.Effect
+ readonly prepareCallHierarchy: (input: { file: string; line: number; character: number }) => Effect.Effect
+ readonly incomingCalls: (input: { file: string; line: number; character: number }) => Effect.Effect
+ readonly outgoingCalls: (input: { file: string; line: number; character: number }) => Effect.Effect
+ }
+
+ export class Service extends ServiceMap.Service()("@opencode/LSP") {}
+
+ export const layer = Layer.effect(
+ Service,
+ Effect.gen(function* () {
+ const instance = yield* InstanceContext
+ const directory = instance.directory
+
+ const clients: LSPClient.Info[] = []
+ const servers: Record = {}
+ const broken = new Set()
+ const spawning = new Map>()
+
+ // Load server configs lazily — forkScoped so it doesn't block layer construction
+ const load = Effect.fn("LSP.load")(function* () {
+ yield* Effect.promise(async () => {
+ const cfg = await Config.get()
+
+ if (cfg.lsp === false) {
+ log.info("all LSPs are disabled")
+ return
+ }
+
+ 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 () => directory),
+ extensions: item.extensions ?? existing?.extensions ?? [],
+ spawn: async (root) => {
+ return {
+ 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 loadFiber = yield* load().pipe(
+ Effect.catchCause((cause) => Effect.sync(() => log.error("init failed", { cause }))),
+ Effect.forkScoped,
+ )
+
+ // Cleanup: shut down all LSP clients when the scope is closed
+ yield* Effect.addFinalizer(() =>
+ Effect.promise(async () => {
+ await Promise.all(clients.map((client) => client.shutdown()))
+ }),
+ )
+
+ async function getClientsForFile(file: string) {
+ 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) broken.add(key)
+ return value
+ })
+ .catch((err) => {
+ 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 })
+
+ const client = await LSPClient.create({
+ serverID: server.id,
+ server: handle,
+ root,
+ }).catch(async (err) => {
+ 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 = clients.find((x) => x.root === root && x.serverID === server.id)
+ if (existing) {
+ await Process.stop(handle.process)
+ return existing
+ }
+
+ clients.push(client)
+ return client
+ }
+
+ for (const server of Object.values(servers)) {
+ if (server.extensions.length && !server.extensions.includes(extension)) continue
+
+ const root = await server.root(file)
+ if (!root) continue
+ if (broken.has(root + server.id)) continue
+
+ const match = clients.find((x) => x.root === root && x.serverID === server.id)
+ if (match) {
+ result.push(match)
+ continue
+ }
+
+ const inflight = 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)
+ spawning.set(root + server.id, task)
+
+ task.finally(() => {
+ if (spawning.get(root + server.id) === task) {
+ spawning.delete(root + server.id)
+ }
+ })
+
+ const client = await task
+ if (!client) continue
+
+ result.push(client)
+ Bus.publish(Event.Updated, {})
+ }
+
+ return result
+ }
+
+ async function runAllClients(input: (client: LSPClient.Info) => Promise): Promise {
+ const tasks = clients.map((x) => input(x))
+ return Promise.all(tasks)
+ }
+
+ async function runForFile(file: string, input: (client: LSPClient.Info) => Promise): Promise {
+ const matched = await getClientsForFile(file)
+ const tasks = matched.map((x) => input(x))
+ return Promise.all(tasks)
+ }
+
+ const init = Effect.fn("LSP.init")(function* () {
+ yield* Fiber.join(loadFiber)
+ })
+
+ const status = Effect.fn("LSP.status")(function* () {
+ yield* Fiber.join(loadFiber)
+ const result: Status[] = []
+ for (const client of clients) {
+ result.push({
+ id: client.serverID,
+ name: servers[client.serverID].id,
+ root: path.relative(directory, client.root),
+ status: "connected",
+ })
+ }
+ return result
+ })
+
+ const hasClients = Effect.fn("LSP.hasClients")(function* (file: string) {
+ yield* Fiber.join(loadFiber)
+ return yield* Effect.promise(async () => {
+ const extension = path.parse(file).ext || file
+ for (const server of Object.values(servers)) {
+ if (server.extensions.length && !server.extensions.includes(extension)) continue
+ const root = await server.root(file)
+ if (!root) continue
+ if (broken.has(root + server.id)) continue
+ return true
+ }
+ return false
+ })
+ })
+
+ const touchFile = Effect.fn("LSP.touchFile")(function* (input: string, waitForDiagnostics?: boolean) {
+ yield* Fiber.join(loadFiber)
+ yield* Effect.promise(async () => {
+ log.info("touching file", { file: input })
+ const matched = await getClientsForFile(input)
+ await Promise.all(
+ matched.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* () {
+ yield* Fiber.join(loadFiber)
+ return yield* Effect.promise(async () => {
+ const results: Record = {}
+ for (const result of await runAllClients(async (client) => client.diagnostics)) {
+ for (const [path, diagnostics] of result.entries()) {
+ const arr = results[path] || []
+ arr.push(...diagnostics)
+ results[path] = arr
+ }
+ }
+ return results
+ })
+ })
+
+ const hover = Effect.fn("LSP.hover")(function* (input: { file: string; line: number; character: number }) {
+ yield* Fiber.join(loadFiber)
+ return yield* Effect.promise(async () => {
+ return runForFile(input.file, (client) => {
+ return client.connection
+ .sendRequest("textDocument/hover", {
+ textDocument: {
+ uri: pathToFileURL(input.file).href,
+ },
+ position: {
+ line: input.line,
+ character: input.character,
+ },
+ })
+ .catch(() => null)
+ })
+ })
+ })
+
+ const workspaceSymbol = Effect.fn("LSP.workspaceSymbol")(function* (query: string) {
+ yield* Fiber.join(loadFiber)
+ return yield* Effect.promise(async () => {
+ return runAllClients((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(() => []),
+ ).then((result) => result.flat() as LSP.Symbol[])
+ })
+ })
+
+ const documentSymbol = Effect.fn("LSP.documentSymbol")(function* (uri: string) {
+ yield* Fiber.join(loadFiber)
+ return yield* Effect.promise(async () => {
+ const file = fileURLToPath(uri)
+ return runForFile(file, (client) =>
+ client.connection
+ .sendRequest("textDocument/documentSymbol", {
+ textDocument: {
+ uri,
+ },
+ })
+ .catch(() => []),
+ )
+ .then((result) => result.flat() as (LSP.DocumentSymbol | LSP.Symbol)[])
+ .then((result) => result.filter(Boolean))
+ })
+ })
+
+ const definition = Effect.fn("LSP.definition")(function* (input: {
+ file: string
+ line: number
+ character: number
+ }) {
+ yield* Fiber.join(loadFiber)
+ return yield* Effect.promise(async () => {
+ return runForFile(input.file, (client) =>
+ client.connection
+ .sendRequest("textDocument/definition", {
+ textDocument: { uri: pathToFileURL(input.file).href },
+ position: { line: input.line, character: input.character },
+ })
+ .catch(() => null),
+ ).then((result) => result.flat().filter(Boolean))
+ })
+ })
+
+ const references = Effect.fn("LSP.references")(function* (input: {
+ file: string
+ line: number
+ character: number
+ }) {
+ yield* Fiber.join(loadFiber)
+ return yield* Effect.promise(async () => {
+ return runForFile(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(() => []),
+ ).then((result) => result.flat().filter(Boolean))
+ })
+ })
+
+ const implementation = Effect.fn("LSP.implementation")(function* (input: {
+ file: string
+ line: number
+ character: number
+ }) {
+ yield* Fiber.join(loadFiber)
+ return yield* Effect.promise(async () => {
+ return runForFile(input.file, (client) =>
+ client.connection
+ .sendRequest("textDocument/implementation", {
+ textDocument: { uri: pathToFileURL(input.file).href },
+ position: { line: input.line, character: input.character },
+ })
+ .catch(() => null),
+ ).then((result) => result.flat().filter(Boolean))
})
- .then((result: any) => result.filter((x: LSP.Symbol) => kinds.includes(x.kind)))
- .then((result: any) => result.slice(0, 10))
- .catch(() => []),
- ).then((result) => result.flat() as LSP.Symbol[])
+ })
+
+ const prepareCallHierarchy = Effect.fn("LSP.prepareCallHierarchy")(function* (input: {
+ file: string
+ line: number
+ character: number
+ }) {
+ yield* Fiber.join(loadFiber)
+ return yield* Effect.promise(async () => {
+ return runForFile(input.file, (client) =>
+ client.connection
+ .sendRequest("textDocument/prepareCallHierarchy", {
+ textDocument: { uri: pathToFileURL(input.file).href },
+ position: { line: input.line, character: input.character },
+ })
+ .catch(() => []),
+ ).then((result) => result.flat().filter(Boolean))
+ })
+ })
+
+ const incomingCalls = Effect.fn("LSP.incomingCalls")(function* (input: {
+ file: string
+ line: number
+ character: number
+ }) {
+ yield* Fiber.join(loadFiber)
+ return yield* Effect.promise(async () => {
+ return runForFile(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("callHierarchy/incomingCalls", { item: items[0] })
+ .catch(() => [])
+ }).then((result) => result.flat().filter(Boolean))
+ })
+ })
+
+ const outgoingCalls = Effect.fn("LSP.outgoingCalls")(function* (input: {
+ file: string
+ line: number
+ character: number
+ }) {
+ yield* Fiber.join(loadFiber)
+ return yield* Effect.promise(async () => {
+ return runForFile(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("callHierarchy/outgoingCalls", { item: items[0] })
+ .catch(() => [])
+ }).then((result) => result.flat().filter(Boolean))
+ })
+ })
+
+ log.info("init")
+ return Service.of({
+ init,
+ status,
+ hasClients,
+ touchFile,
+ diagnostics,
+ hover,
+ workspaceSymbol,
+ documentSymbol,
+ definition,
+ references,
+ implementation,
+ prepareCallHierarchy,
+ incomingCalls,
+ outgoingCalls,
+ })
+ }),
+ )
+
+ // Async facades
+ export async function init() {
+ return runPromiseInstance(Service.use((svc) => svc.init()))
+ }
+
+ export async function status() {
+ return runPromiseInstance(Service.use((svc) => svc.status()))
+ }
+
+ export async function hasClients(file: string) {
+ return runPromiseInstance(Service.use((svc) => svc.hasClients(file)))
+ }
+
+ export async function touchFile(input: string, waitForDiagnostics?: boolean) {
+ return runPromiseInstance(Service.use((svc) => svc.touchFile(input, waitForDiagnostics)))
+ }
+
+ export async function diagnostics() {
+ return runPromiseInstance(Service.use((svc) => svc.diagnostics()))
+ }
+
+ export async function hover(input: { file: string; line: number; character: number }) {
+ return runPromiseInstance(Service.use((svc) => svc.hover(input)))
+ }
+
+ export async function workspaceSymbol(query: string) {
+ return runPromiseInstance(Service.use((svc) => svc.workspaceSymbol(query)))
}
export async function documentSymbol(uri: string) {
- const file = fileURLToPath(uri)
- return run(file, (client) =>
- client.connection
- .sendRequest("textDocument/documentSymbol", {
- textDocument: {
- uri,
- },
- })
- .catch(() => []),
- )
- .then((result) => result.flat() as (LSP.DocumentSymbol | LSP.Symbol)[])
- .then((result) => result.filter(Boolean))
+ return runPromiseInstance(Service.use((svc) => svc.documentSymbol(uri)))
}
export async function definition(input: { file: string; line: number; character: number }) {
- return run(input.file, (client) =>
- client.connection
- .sendRequest("textDocument/definition", {
- textDocument: { uri: pathToFileURL(input.file).href },
- position: { line: input.line, character: input.character },
- })
- .catch(() => null),
- ).then((result) => result.flat().filter(Boolean))
+ return runPromiseInstance(Service.use((svc) => svc.definition(input)))
}
export async function references(input: { file: string; line: number; character: number }) {
- return 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(() => []),
- ).then((result) => result.flat().filter(Boolean))
+ return runPromiseInstance(Service.use((svc) => svc.references(input)))
}
export async function implementation(input: { file: string; line: number; character: number }) {
- return run(input.file, (client) =>
- client.connection
- .sendRequest("textDocument/implementation", {
- textDocument: { uri: pathToFileURL(input.file).href },
- position: { line: input.line, character: input.character },
- })
- .catch(() => null),
- ).then((result) => result.flat().filter(Boolean))
+ return runPromiseInstance(Service.use((svc) => svc.implementation(input)))
}
export async function prepareCallHierarchy(input: { file: string; line: number; character: number }) {
- return run(input.file, (client) =>
- client.connection
- .sendRequest("textDocument/prepareCallHierarchy", {
- textDocument: { uri: pathToFileURL(input.file).href },
- position: { line: input.line, character: input.character },
- })
- .catch(() => []),
- ).then((result) => result.flat().filter(Boolean))
+ return runPromiseInstance(Service.use((svc) => svc.prepareCallHierarchy(input)))
}
export async function incomingCalls(input: { file: string; line: number; character: number }) {
- return 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("callHierarchy/incomingCalls", { item: items[0] }).catch(() => [])
- }).then((result) => result.flat().filter(Boolean))
+ return runPromiseInstance(Service.use((svc) => svc.incomingCalls(input)))
}
export async function outgoingCalls(input: { file: string; line: number; character: number }) {
- return 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("callHierarchy/outgoingCalls", { item: items[0] }).catch(() => [])
- }).then((result) => result.flat().filter(Boolean))
- }
-
- async function runAll(input: (client: LSPClient.Info) => Promise): Promise {
- const clients = await state().then((x) => x.clients)
- const tasks = clients.map((x) => input(x))
- return Promise.all(tasks)
- }
-
- async function run(file: string, input: (client: LSPClient.Info) => Promise): Promise {
- const clients = await getClients(file)
- const tasks = clients.map((x) => input(x))
- return Promise.all(tasks)
+ return runPromiseInstance(Service.use((svc) => svc.outgoingCalls(input)))
}
export namespace Diagnostic {