|
|
@@ -11,6 +11,9 @@ import { Instance } from "../project/instance"
|
|
|
import { Flag } from "@/flag/flag"
|
|
|
import { Process } from "../util/process"
|
|
|
import { spawn as lspspawn } from "./launch"
|
|
|
+import { Effect, Layer, ServiceMap } from "effect"
|
|
|
+import { InstanceState } from "@/effect/instance-state"
|
|
|
+import { makeRuntime } from "@/effect/run-service"
|
|
|
|
|
|
export namespace LSP {
|
|
|
const log = Log.create({ service: "lsp" })
|
|
|
@@ -62,92 +65,6 @@ export namespace LSP {
|
|
|
})
|
|
|
export type DocumentSymbol = z.infer<typeof DocumentSymbol>
|
|
|
|
|
|
- const filterExperimentalServers = (servers: Record<string, LSPServer.Info>) => {
|
|
|
- if (Flag.OPENCODE_EXPERIMENTAL_LSP_TY) {
|
|
|
- // If experimental flag is enabled, disable pyright
|
|
|
- if (servers["pyright"]) {
|
|
|
- log.info("LSP server pyright is disabled because OPENCODE_EXPERIMENTAL_LSP_TY is enabled")
|
|
|
- delete servers["pyright"]
|
|
|
- }
|
|
|
- } else {
|
|
|
- // If experimental flag is disabled, disable ty
|
|
|
- if (servers["ty"]) {
|
|
|
- delete servers["ty"]
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const state = Instance.state(
|
|
|
- async () => {
|
|
|
- const clients: LSPClient.Info[] = []
|
|
|
- const servers: Record<string, LSPServer.Info> = {}
|
|
|
- const cfg = await Config.get()
|
|
|
-
|
|
|
- if (cfg.lsp === false) {
|
|
|
- log.info("all LSPs are disabled")
|
|
|
- return {
|
|
|
- broken: new Set<string>(),
|
|
|
- servers,
|
|
|
- clients,
|
|
|
- spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- 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<string>(),
|
|
|
- servers,
|
|
|
- clients,
|
|
|
- spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
|
|
|
- }
|
|
|
- },
|
|
|
- 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,168 +77,6 @@ export namespace LSP {
|
|
|
})
|
|
|
export type Status = z.infer<typeof Status>
|
|
|
|
|
|
- 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()
|
|
|
-
|
|
|
- // Only spawn LSP clients for files within the instance directory
|
|
|
- if (!Instance.containsPath(file)) {
|
|
|
- return []
|
|
|
- }
|
|
|
-
|
|
|
- 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<string, LSPClient.Diagnostic[]> = {}
|
|
|
- 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,
|
|
|
@@ -362,115 +117,423 @@ export namespace LSP {
|
|
|
SymbolKind.Enum,
|
|
|
]
|
|
|
|
|
|
- export async function workspaceSymbol(query: string) {
|
|
|
- return 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(() => []),
|
|
|
- ).then((result) => result.flat() as LSP.Symbol[])
|
|
|
+ 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"]
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- 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))
|
|
|
- }
|
|
|
+ type LocInput = { file: string; line: number; character: number }
|
|
|
|
|
|
- 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))
|
|
|
+ interface State {
|
|
|
+ clients: LSPClient.Info[]
|
|
|
+ servers: Record<string, LSPServer.Info>
|
|
|
+ broken: Set<string>
|
|
|
+ spawning: Map<string, Promise<LSPClient.Info | undefined>>
|
|
|
}
|
|
|
|
|
|
- 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))
|
|
|
+ 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 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))
|
|
|
- }
|
|
|
+ export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/LSP") {}
|
|
|
+
|
|
|
+ export const layer = Layer.effect(
|
|
|
+ Service,
|
|
|
+ Effect.gen(function* () {
|
|
|
+ const state = yield* InstanceState.make<State>(
|
|
|
+ Effect.fn("LSP.state")(function* () {
|
|
|
+ const cfg = yield* Effect.promise(() => 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 })
|
|
|
+
|
|
|
+ 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
|
|
|
|
|
|
- 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 },
|
|
|
+ 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
|
|
|
})
|
|
|
- .catch(() => []),
|
|
|
- ).then((result) => result.flat().filter(Boolean))
|
|
|
- }
|
|
|
+ })
|
|
|
|
|
|
- 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 },
|
|
|
+ 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
|
|
|
})
|
|
|
- .catch(() => [])) as any[]
|
|
|
- if (!items?.length) return []
|
|
|
- return client.connection.sendRequest("callHierarchy/incomingCalls", { item: items[0] }).catch(() => [])
|
|
|
- }).then((result) => result.flat().filter(Boolean))
|
|
|
- }
|
|
|
+ })
|
|
|
|
|
|
- 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 },
|
|
|
+ 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(() => [])
|
|
|
})
|
|
|
- .catch(() => [])) as any[]
|
|
|
- if (!items?.length) return []
|
|
|
- return client.connection.sendRequest("callHierarchy/outgoingCalls", { item: items[0] }).catch(() => [])
|
|
|
- }).then((result) => result.flat().filter(Boolean))
|
|
|
- }
|
|
|
+ return results.flat().filter(Boolean)
|
|
|
+ })
|
|
|
|
|
|
- async function runAll<T>(input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
|
|
|
- const clients = await state().then((x) => x.clients)
|
|
|
- const tasks = clients.map((x) => input(x))
|
|
|
- return Promise.all(tasks)
|
|
|
- }
|
|
|
+ const incomingCalls = Effect.fn("LSP.incomingCalls")(function* (input: LocInput) {
|
|
|
+ return yield* callHierarchyRequest(input, "callHierarchy/incomingCalls")
|
|
|
+ })
|
|
|
|
|
|
- async function run<T>(file: string, input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
|
|
|
- const clients = await getClients(file)
|
|
|
- const tasks = clients.map((x) => input(x))
|
|
|
- return Promise.all(tasks)
|
|
|
- }
|
|
|
+ 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,
|
|
|
+ })
|
|
|
+ }),
|
|
|
+ )
|
|
|
+
|
|
|
+ const { runPromise } = makeRuntime(Service, layer)
|
|
|
+
|
|
|
+ export const init = async () => runPromise((svc) => svc.init())
|
|
|
+
|
|
|
+ export const status = async () => runPromise((svc) => svc.status())
|
|
|
+
|
|
|
+ export const hasClients = async (file: string) => runPromise((svc) => svc.hasClients(file))
|
|
|
+
|
|
|
+ export const touchFile = async (input: string, waitForDiagnostics?: boolean) =>
|
|
|
+ runPromise((svc) => svc.touchFile(input, waitForDiagnostics))
|
|
|
+
|
|
|
+ export const diagnostics = async () => runPromise((svc) => svc.diagnostics())
|
|
|
+
|
|
|
+ export const hover = async (input: LocInput) => runPromise((svc) => svc.hover(input))
|
|
|
+
|
|
|
+ export const definition = async (input: LocInput) => runPromise((svc) => svc.definition(input))
|
|
|
+
|
|
|
+ export const references = async (input: LocInput) => runPromise((svc) => svc.references(input))
|
|
|
+
|
|
|
+ export const implementation = async (input: LocInput) => runPromise((svc) => svc.implementation(input))
|
|
|
+
|
|
|
+ export const documentSymbol = async (uri: string) => runPromise((svc) => svc.documentSymbol(uri))
|
|
|
+
|
|
|
+ export const workspaceSymbol = async (query: string) => runPromise((svc) => svc.workspaceSymbol(query))
|
|
|
+
|
|
|
+ export const prepareCallHierarchy = async (input: LocInput) => runPromise((svc) => svc.prepareCallHierarchy(input))
|
|
|
+
|
|
|
+ export const incomingCalls = async (input: LocInput) => runPromise((svc) => svc.incomingCalls(input))
|
|
|
+
|
|
|
+ export const outgoingCalls = async (input: LocInput) => runPromise((svc) => svc.outgoingCalls(input))
|
|
|
|
|
|
export namespace Diagnostic {
|
|
|
export function pretty(diagnostic: LSPClient.Diagnostic) {
|