import { t } from "structural"; import { fileTracker } from "./file-tracker.ts"; import { LspClient } from "../lsp/client.ts"; import type { Config } from "../config.ts"; import type { Transport } from "../transports/transport-common.ts"; import { getLspClientForFile } from "../lsp/detect.ts"; import { Result, ok, err, attempt } from "../libocto/result.ts"; export type LspFileOnlyArgs = t.GetType; export const LspFileOnlyArgumentsSchema = t.subtype({ filePath: t.str.comment("Path to the file to query"), }); export const LineSchema = t.num.comment("1-indexed line number"); export const CharSchema = t.num.comment("1-indexed column number"); export const LspPositionArgumentsSchema = t.subtype({ filePath: t.str.comment("Path to the file to query"), line: LineSchema, character: CharSchema, }); export type LspPositionArgs = t.GetType; export function getLspExtensionsComment(extensions: Set): string { const extensionList = Array.from(extensions).sort().join(", "); return `Only works on ${extensionList} files; this tool will fail on other file types.`; } type BootstrapResult = | { success: true; client: LspClient } | { success: false; message: { content: string } }; export async function bootstrapLspClient( transport: Transport, config: Config, filePath: string, ): Promise { const lspClientResult = await getLspClientForFile(transport.cwd, config, filePath); if (lspClientResult == null) { return { success: false, message: { content: "No LSP server available for this file type. Fall back to other approaches like reading files directly.", }, }; } return { success: true, client: lspClientResult }; } export async function withLspFile( abortSignal: AbortSignal, transport: Transport, client: LspClient, filePath: string, fn: (client: LspClient) => Promise, ): Promise> { const content = await fileTracker.read(transport, abortSignal, filePath); try { await client.openFile(filePath, content); return ok(await fn(client)); } catch { return err(`LSP client failed to process ${filePath}`); } } export async function runLspPositionQuery( abortSignal: AbortSignal, transport: Transport, config: Config, args: LspPositionArgs, toolName: string, queryFn: (client: LspClient, filePath: string, line: number, character: number) => Promise, formatResult: (result: R, filePath: string, line: number, character: number) => string, ): Promise> { const { filePath, line, character } = args; const resolvedPath = await transport.resolvePath(abortSignal, filePath); return attempt(`LSP ${toolName} failed for ${resolvedPath}`, async () => { const boot = await bootstrapLspClient(transport, config, resolvedPath); if (!boot.success) return ok(boot.message); const { client } = boot; return withLspFile(abortSignal, transport, client, resolvedPath, async client => { const result = await queryFn(client, resolvedPath, line - 1, character - 1); return { content: formatResult(result, resolvedPath, line, character), }; }); }); } export async function runLspFileQuery( abortSignal: AbortSignal, transport: Transport, config: Config, args: LspFileOnlyArgs, toolName: string, queryFn: (client: LspClient, filePath: string) => Promise, formatResult: (result: R, filePath: string) => string, ): Promise> { const { filePath } = args; const resolvedPath = await transport.resolvePath(abortSignal, filePath); return attempt(`LSP ${toolName} failed for ${resolvedPath}`, async () => { const boot = await bootstrapLspClient(transport, config, resolvedPath); if (!boot.success) return ok(boot.message); const { client } = boot; return withLspFile(abortSignal, transport, client, resolvedPath, async client => { const result = await queryFn(client, resolvedPath); return { content: formatResult(result, resolvedPath), }; }); }); }