import * as fs from "node:fs";
import * as fsSync from "node:fs";
import * as path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { ensureFileOpen, getActiveClients, getOrCreateClient, refreshFile, sendRequest, setIdleTimeout, WARMUP_TIMEOUT_MS, } from "./client.js";
import { getServerForFile, getServersForFile, loadConfig, hasRootMarkers, resolveCommand } from "./config.js";
import { applyTextEdits, applyWorkspaceEdit } from "./edits.js";
import { ToolAbortError, clampTimeout, throwIfAborted } from "./helpers.js";
import { detectLspmux } from "./lspmux.js";
import { lspSchema, } from "./types.js";
import { applyCodeAction, collectGlobMatches, dedupeWorkspaceSymbols, extractHoverText, fileToUri, filterWorkspaceSymbols, formatCallHierarchyItem, formatCodeAction, formatDiagnostic, formatDiagnosticsSummary, formatDocumentSymbol, formatGroupedDiagnosticMessages, formatLocation, formatSignatureHelp, formatSymbolInformation, formatWorkspaceEdit, hasGlobPattern, readLocationContext, resolveSymbolColumn, sortDiagnostics, symbolKindToIcon, uriToFile, } from "./utils.js";
export { lspSchema } from "./types.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const lspDescription = fsSync.readFileSync(path.join(__dirname, "lsp.md"), "utf-8");
export async function warmupLspServers(cwd) {
    const config = loadConfig(cwd);
    setIdleTimeout(config.idleTimeoutMs);
    const servers = [];
    const lspServers = getLspServers(config);
    const results = await Promise.allSettled(lspServers.map(async ([name, serverConfig]) => {
        const client = await getOrCreateClient(serverConfig, cwd, serverConfig.warmupTimeoutMs ?? WARMUP_TIMEOUT_MS);
        return { name, client, fileTypes: serverConfig.fileTypes };
    }));
    for (let i = 0; i < results.length; i++) {
        const result = results[i];
        const [name, serverConfig] = lspServers[i];
        if (result.status === "fulfilled") {
            servers.push({
                name: result.value.name,
                status: "ready",
                fileTypes: result.value.fileTypes,
            });
        }
        else {
            servers.push({
                name,
                status: "error",
                fileTypes: serverConfig.fileTypes,
                error: result.reason?.message ?? String(result.reason),
            });
        }
    }
    return { servers };
}
export function getLspStatus() {
    return getActiveClients();
}
// =============================================================================
// Internal Helpers
// =============================================================================
const configCache = new Map();
function getConfig(cwd) {
    let config = configCache.get(cwd);
    if (!config) {
        config = loadConfig(cwd);
        setIdleTimeout(config.idleTimeoutMs);
        configCache.set(cwd, config);
    }
    return config;
}
function getLspServers(config) {
    return Object.entries(config.servers);
}
const DIAGNOSTIC_MESSAGE_LIMIT = 50;
const SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS = 3000;
const BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS = 400;
const MAX_GLOB_DIAGNOSTIC_TARGETS = 20;
const WORKSPACE_SYMBOL_LIMIT = 200;
function limitDiagnosticMessages(messages) {
    if (messages.length <= DIAGNOSTIC_MESSAGE_LIMIT) {
        return messages;
    }
    return messages.slice(0, DIAGNOSTIC_MESSAGE_LIMIT);
}
const LOCATION_CONTEXT_LINES = 1;
const REFERENCE_CONTEXT_LIMIT = 50;
function normalizeLocationResult(result) {
    if (!result)
        return [];
    const raw = Array.isArray(result) ? result : [result];
    return raw.flatMap(loc => {
        if ("uri" in loc) {
            return [loc];
        }
        if ("targetUri" in loc) {
            const link = loc;
            return [{ uri: link.targetUri, range: link.targetSelectionRange ?? link.targetRange }];
        }
        return [];
    });
}
async function formatLocationWithContext(location, cwd) {
    const header = `  ${formatLocation(location, cwd)}`;
    const context = await readLocationContext(uriToFile(location.uri), location.range.start.line + 1, LOCATION_CONTEXT_LINES);
    if (context.length === 0) {
        return header;
    }
    return `${header}\n${context.map(lineText => `    ${lineText}`).join("\n")}`;
}
async function formatLocationResults(result, label, cwd) {
    const locations = normalizeLocationResult(result);
    if (locations.length === 0) {
        return `No ${label} found`;
    }
    const lines = await Promise.all(locations.map(location => formatLocationWithContext(location, cwd)));
    return `Found ${locations.length} ${label}(s):\n${lines.join("\n")}`;
}
async function formatCallHierarchyResults(client, position, uri, direction, cwd, signal) {
    const prepareResult = (await sendRequest(client, "textDocument/prepareCallHierarchy", { textDocument: { uri }, position }, signal));
    if (!prepareResult || prepareResult.length === 0) {
        return "No call hierarchy item found at this position";
    }
    const method = direction === "incoming" ? "callHierarchy/incomingCalls" : "callHierarchy/outgoingCalls";
    const callResult = (await sendRequest(client, method, { item: prepareResult[0] }, signal));
    if (!callResult || callResult.length === 0) {
        const verb = direction === "incoming" ? "incoming calls" : "outgoing calls";
        const prep = direction === "incoming" ? "for" : "from";
        return `No ${verb} found ${prep} ${prepareResult[0].name}`;
    }
    const lines = [];
    const limited = callResult.slice(0, REFERENCE_CONTEXT_LIMIT);
    for (const call of limited) {
        const item = "from" in call ? call.from : call.to;
        const header = formatCallHierarchyItem(item, cwd);
        const filePath = uriToFile(item.uri);
        const callLine = ("from" in call ? call.fromRanges[0]?.start.line : undefined) ?? item.selectionRange.start.line;
        const context = await readLocationContext(filePath, callLine + 1, LOCATION_CONTEXT_LINES);
        if (context.length > 0) {
            lines.push(`  ${header}\n${context.map(l => `    ${l}`).join("\n")}`);
        }
        else {
            lines.push(`  ${header}`);
        }
    }
    const noun = direction === "incoming" ? "caller" : "callee";
    const prep = direction === "incoming" ? "of" : "from";
    const truncation = callResult.length > REFERENCE_CONTEXT_LIMIT
        ? `\n  ... ${callResult.length - REFERENCE_CONTEXT_LIMIT} additional ${noun}(s) omitted`
        : "";
    return `${callResult.length} ${noun}(s) ${prep} ${prepareResult[0].name}:\n${lines.join("\n")}${truncation}`;
}
async function reloadServer(client, serverName, signal) {
    let output = `Restarted ${serverName}`;
    const reloadMethods = ["rust-analyzer/reloadWorkspace", "workspace/didChangeConfiguration"];
    for (const method of reloadMethods) {
        try {
            await sendRequest(client, method, method.includes("Configuration") ? { settings: {} } : null, signal);
            output = `Reloaded ${serverName}`;
            break;
        }
        catch {
            // Method not supported, try next
        }
    }
    if (output.startsWith("Restarted")) {
        client.proc.kill();
        // Wait for the process to actually exit so the crash recovery handler
        // removes the client from the cache. Without this, the next
        // getOrCreateClient call may return the dead client (#815).
        await Promise.race([
            client.proc.exited,
            new Promise(r => setTimeout(r, 3000)),
        ]);
    }
    return output;
}
async function waitForDiagnostics(client, uri, timeoutMs = 3000, signal, minVersion) {
    const start = Date.now();
    while (Date.now() - start < timeoutMs) {
        throwIfAborted(signal);
        const diagnostics = client.diagnostics.get(uri);
        const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
        if (diagnostics !== undefined && versionOk)
            return diagnostics;
        await new Promise(resolve => setTimeout(resolve, 100));
    }
    return client.diagnostics.get(uri) ?? [];
}
function detectProjectType(cwd) {
    if (fs.existsSync(path.join(cwd, "Cargo.toml"))) {
        return { type: "rust", command: ["cargo", "check", "--message-format=short"], description: "Rust (cargo check)" };
    }
    if (fs.existsSync(path.join(cwd, "tsconfig.json"))) {
        return { type: "typescript", command: ["npx", "tsc", "--noEmit"], description: "TypeScript (tsc --noEmit)" };
    }
    if (fs.existsSync(path.join(cwd, "go.mod"))) {
        return { type: "go", command: ["go", "build", "./..."], description: "Go (go build)" };
    }
    if (fs.existsSync(path.join(cwd, "pyproject.toml")) || fs.existsSync(path.join(cwd, "pyrightconfig.json"))) {
        return { type: "python", command: ["pyright"], description: "Python (pyright)" };
    }
    return { type: "unknown", description: "Unknown project type" };
}
async function runWorkspaceDiagnostics(cwd, signal) {
    throwIfAborted(signal);
    const projectType = detectProjectType(cwd);
    if (!projectType.command) {
        return {
            output: "Cannot detect project type. Supported: Rust (Cargo.toml), TypeScript (tsconfig.json), Go (go.mod), Python (pyproject.toml)",
            projectType,
        };
    }
    const [cmd, ...cmdArgs] = projectType.command;
    const proc = spawn(cmd, cmdArgs, {
        cwd,
        stdio: ["ignore", "pipe", "pipe"],
    });
    const abortHandler = () => {
        proc.kill();
    };
    if (signal) {
        signal.addEventListener("abort", abortHandler, { once: true });
    }
    try {
        const stdoutChunks = [];
        const stderrChunks = [];
        proc.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
        proc.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
        const exitCode = await new Promise((resolve) => {
            proc.on("exit", (code) => resolve(code ?? 1));
        });
        const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
        const stderr = Buffer.concat(stderrChunks).toString("utf-8");
        throwIfAborted(signal);
        const combined = (stdout + stderr).trim();
        if (!combined) {
            return { output: "No issues found", projectType };
        }
        const lines = combined.split("\n");
        if (lines.length > 50) {
            return { output: `${lines.slice(0, 50).join("\n")}\n... and ${lines.length - 50} more lines`, projectType };
        }
        return { output: combined, projectType };
    }
    catch (e) {
        if (signal?.aborted) {
            throw new ToolAbortError();
        }
        return { output: `Failed to run ${projectType.command.join(" ")}: ${e}`, projectType };
    }
    finally {
        signal?.removeEventListener("abort", abortHandler);
    }
}
// =============================================================================
// Path Resolution
// =============================================================================
function resolveToCwd(file, cwd) {
    return path.resolve(cwd, file);
}
// =============================================================================
// Tool Factory
// =============================================================================
/**
 * Create an LSP tool configured for a specific working directory.
 */
export function createLspTool(cwd) {
    return {
        name: "lsp",
        label: "LSP",
        description: lspDescription,
        parameters: lspSchema,
        async execute(_toolCallId, params, signal, _onUpdate) {
            const { action, file, line, symbol, occurrence, query, new_name, apply, tab_size, insert_spaces, timeout } = params;
            const timeoutSec = clampTimeout(timeout);
            const timeoutSignal = AbortSignal.timeout(timeoutSec * 1000);
            signal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
            throwIfAborted(signal);
            const config = getConfig(cwd);
            // Status action doesn't need a file
            if (action === "status") {
                const servers = Object.keys(config.servers);
                const lspmuxState = await detectLspmux();
                const lspmuxStatus = lspmuxState.available
                    ? lspmuxState.running
                        ? "lspmux: active (multiplexing enabled)"
                        : "lspmux: installed but server not running"
                    : "";
                let serverStatus;
                if (servers.length > 0) {
                    serverStatus = `Active language servers: ${servers.join(", ")}`;
                }
                else {
                    // Diagnose why no servers were detected
                    const DEFAULTS = (await import("./defaults.json", { with: { type: "json" } })).default;
                    const diagnostics = ["No language servers configured for this project."];
                    const matchedButMissing = [];
                    const noMarkers = [];
                    for (const [name, def] of Object.entries(DEFAULTS)) {
                        if (hasRootMarkers(cwd, def.rootMarkers)) {
                            const resolved = resolveCommand(def.command, cwd);
                            if (!resolved) {
                                matchedButMissing.push(`  ${name}: project detected (${def.rootMarkers[0]}) but '${def.command}' not found — install it with npm/pip/brew`);
                            }
                        }
                    }
                    if (matchedButMissing.length > 0) {
                        diagnostics.push("\nDetected projects missing language servers:");
                        diagnostics.push(...matchedButMissing);
                        diagnostics.push("\nInstall the missing server command and restart NERF, or run: lsp reload");
                    }
                    else {
                        diagnostics.push("No recognized project markers found in the working directory.");
                        diagnostics.push("LSP auto-detects projects via files like package.json, Cargo.toml, go.mod, pyproject.toml, etc.");
                    }
                    serverStatus = diagnostics.join("\n");
                }
                const output = lspmuxStatus ? `${serverStatus}\n${lspmuxStatus}` : serverStatus;
                return {
                    content: [{ type: "text", text: output }],
                    details: { action, success: true, request: params },
                };
            }
            // Diagnostics can be batch or single-file
            if (action === "diagnostics") {
                if (!file) {
                    const result = await runWorkspaceDiagnostics(cwd, signal);
                    return {
                        content: [
                            {
                                type: "text",
                                text: `Workspace diagnostics (${result.projectType.description}):\n${result.output}`,
                            },
                        ],
                        details: { action, success: true, request: params },
                    };
                }
                let targets;
                let truncatedGlobTargets = false;
                if (hasGlobPattern(file)) {
                    const globMatches = await collectGlobMatches(file, cwd, MAX_GLOB_DIAGNOSTIC_TARGETS);
                    targets = globMatches.matches;
                    truncatedGlobTargets = globMatches.truncated;
                }
                else {
                    targets = [file];
                }
                if (targets.length === 0) {
                    return {
                        content: [{ type: "text", text: `No files matched pattern: ${file}` }],
                        details: { action, success: true, request: params },
                    };
                }
                const detailed = targets.length > 1 || truncatedGlobTargets;
                const diagnosticsWaitTimeoutMs = detailed
                    ? Math.min(BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS, timeoutSec * 1000)
                    : Math.min(SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS, timeoutSec * 1000);
                const results = [];
                const allServerNames = new Set();
                if (truncatedGlobTargets) {
                    results.push(`[W] Pattern matched more than ${MAX_GLOB_DIAGNOSTIC_TARGETS} files; showing first ${MAX_GLOB_DIAGNOSTIC_TARGETS}. Narrow the glob or use workspace diagnostics.`);
                }
                for (const target of targets) {
                    throwIfAborted(signal);
                    const resolved = resolveToCwd(target, cwd);
                    const servers = getServersForFile(config, resolved);
                    if (servers.length === 0) {
                        results.push(`[E] ${target}: No language server found`);
                        continue;
                    }
                    const uri = fileToUri(resolved);
                    const relPath = path.relative(cwd, resolved);
                    const allDiagnostics = [];
                    for (const [serverName, serverConfig] of servers) {
                        allServerNames.add(serverName);
                        try {
                            throwIfAborted(signal);
                            const client = await getOrCreateClient(serverConfig, cwd);
                            const minVersion = client.diagnosticsVersion;
                            await refreshFile(client, resolved, signal);
                            const diagnostics = await waitForDiagnostics(client, uri, diagnosticsWaitTimeoutMs, signal, minVersion);
                            allDiagnostics.push(...diagnostics);
                        }
                        catch (err) {
                            if (err instanceof ToolAbortError || signal?.aborted) {
                                throw err;
                            }
                        }
                    }
                    // Deduplicate
                    const seen = new Set();
                    const uniqueDiagnostics = [];
                    for (const d of allDiagnostics) {
                        const key = `${d.range.start.line}:${d.range.start.character}:${d.range.end.line}:${d.range.end.character}:${d.message}`;
                        if (!seen.has(key)) {
                            seen.add(key);
                            uniqueDiagnostics.push(d);
                        }
                    }
                    sortDiagnostics(uniqueDiagnostics);
                    if (!detailed && targets.length === 1) {
                        if (uniqueDiagnostics.length === 0) {
                            return {
                                content: [{ type: "text", text: "No diagnostics" }],
                                details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
                            };
                        }
                        const summary = formatDiagnosticsSummary(uniqueDiagnostics);
                        const formatted = uniqueDiagnostics.map(d => formatDiagnostic(d, relPath));
                        const output = `${summary}:\n${formatGroupedDiagnosticMessages(formatted)}`;
                        return {
                            content: [{ type: "text", text: output }],
                            details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
                        };
                    }
                    if (uniqueDiagnostics.length === 0) {
                        results.push(`OK ${relPath}: no issues`);
                    }
                    else {
                        const summary = formatDiagnosticsSummary(uniqueDiagnostics);
                        results.push(`[E] ${relPath}: ${summary}`);
                        const formatted = uniqueDiagnostics.map(d => formatDiagnostic(d, relPath));
                        results.push(formatGroupedDiagnosticMessages(formatted));
                    }
                }
                return {
                    content: [{ type: "text", text: results.join("\n") }],
                    details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
                };
            }
            const requiresFile = !file && action !== "symbols" && action !== "reload";
            if (requiresFile) {
                return {
                    content: [{ type: "text", text: "Error: file parameter required for this action" }],
                    details: { action, success: false },
                };
            }
            const resolvedFile = file ? resolveToCwd(file, cwd) : null;
            // Workspace symbol search (no file)
            if (action === "symbols" && !resolvedFile) {
                const normalizedQuery = query?.trim();
                if (!normalizedQuery) {
                    return {
                        content: [{ type: "text", text: "Error: query parameter required for workspace symbol search" }],
                        details: { action, success: false, request: params },
                    };
                }
                const servers = getLspServers(config);
                if (servers.length === 0) {
                    return {
                        content: [{ type: "text", text: "No language server found for this action" }],
                        details: { action, success: false, request: params },
                    };
                }
                const aggregatedSymbols = [];
                const respondingServers = new Set();
                for (const [workspaceServerName, workspaceServerConfig] of servers) {
                    throwIfAborted(signal);
                    try {
                        const workspaceClient = await getOrCreateClient(workspaceServerConfig, cwd);
                        const workspaceResult = (await sendRequest(workspaceClient, "workspace/symbol", { query: normalizedQuery }, signal));
                        if (!workspaceResult || workspaceResult.length === 0) {
                            continue;
                        }
                        respondingServers.add(workspaceServerName);
                        aggregatedSymbols.push(...filterWorkspaceSymbols(workspaceResult, normalizedQuery));
                    }
                    catch (err) {
                        if (err instanceof ToolAbortError || signal?.aborted) {
                            throw err;
                        }
                    }
                }
                const dedupedSymbols = dedupeWorkspaceSymbols(aggregatedSymbols);
                if (dedupedSymbols.length === 0) {
                    return {
                        content: [{ type: "text", text: `No symbols matching "${normalizedQuery}"` }],
                        details: {
                            action,
                            serverName: Array.from(respondingServers).join(", "),
                            success: true,
                            request: params,
                        },
                    };
                }
                const limitedSymbols = dedupedSymbols.slice(0, WORKSPACE_SYMBOL_LIMIT);
                const lines = limitedSymbols.map(s => formatSymbolInformation(s, cwd));
                const truncationLine = dedupedSymbols.length > WORKSPACE_SYMBOL_LIMIT
                    ? `\n... ${dedupedSymbols.length - WORKSPACE_SYMBOL_LIMIT} additional symbol(s) omitted`
                    : "";
                return {
                    content: [
                        {
                            type: "text",
                            text: `Found ${dedupedSymbols.length} symbol(s) matching "${normalizedQuery}":\n${lines.map(l => `  ${l}`).join("\n")}${truncationLine}`,
                        },
                    ],
                    details: {
                        action,
                        serverName: Array.from(respondingServers).join(", "),
                        success: true,
                        request: params,
                    },
                };
            }
            // Reload all servers (no file)
            if (action === "reload" && !resolvedFile) {
                const servers = getLspServers(config);
                if (servers.length === 0) {
                    return {
                        content: [{ type: "text", text: "No language server found for this action" }],
                        details: { action, success: false, request: params },
                    };
                }
                const outputs = [];
                for (const [workspaceServerName, workspaceServerConfig] of servers) {
                    throwIfAborted(signal);
                    try {
                        const workspaceClient = await getOrCreateClient(workspaceServerConfig, cwd);
                        outputs.push(await reloadServer(workspaceClient, workspaceServerName, signal));
                    }
                    catch (err) {
                        if (err instanceof ToolAbortError || signal?.aborted) {
                            throw err;
                        }
                        const errorMessage = err instanceof Error ? err.message : String(err);
                        outputs.push(`Failed to reload ${workspaceServerName}: ${errorMessage}`);
                    }
                }
                return {
                    content: [{ type: "text", text: outputs.join("\n") }],
                    details: { action, serverName: servers.map(([name]) => name).join(", "), success: true, request: params },
                };
            }
            // File-specific actions
            const serverInfo = resolvedFile ? getServerForFile(config, resolvedFile) : null;
            if (!serverInfo) {
                return {
                    content: [{ type: "text", text: "No language server found for this action" }],
                    details: { action, success: false },
                };
            }
            const [serverName, serverConfig] = serverInfo;
            try {
                const client = await getOrCreateClient(serverConfig, cwd);
                const targetFile = resolvedFile;
                if (targetFile) {
                    await ensureFileOpen(client, targetFile, signal);
                }
                const uri = targetFile ? fileToUri(targetFile) : "";
                const resolvedLine = line ?? 1;
                const resolvedCharacter = targetFile
                    ? await resolveSymbolColumn(targetFile, resolvedLine, symbol, occurrence)
                    : 0;
                const position = { line: resolvedLine - 1, character: resolvedCharacter };
                let output;
                switch (action) {
                    case "definition": {
                        const result = await sendRequest(client, "textDocument/definition", { textDocument: { uri }, position }, signal);
                        output = await formatLocationResults(result, "definition", cwd);
                        break;
                    }
                    case "type_definition": {
                        const result = await sendRequest(client, "textDocument/typeDefinition", { textDocument: { uri }, position }, signal);
                        output = await formatLocationResults(result, "type definition", cwd);
                        break;
                    }
                    case "implementation": {
                        const result = await sendRequest(client, "textDocument/implementation", { textDocument: { uri }, position }, signal);
                        output = await formatLocationResults(result, "implementation", cwd);
                        break;
                    }
                    case "references": {
                        const result = (await sendRequest(client, "textDocument/references", {
                            textDocument: { uri },
                            position,
                            context: { includeDeclaration: true },
                        }, signal));
                        if (!result || result.length === 0) {
                            output = "No references found";
                        }
                        else {
                            const contextualReferences = result.slice(0, REFERENCE_CONTEXT_LIMIT);
                            const plainReferences = result.slice(REFERENCE_CONTEXT_LIMIT);
                            const contextualLines = await Promise.all(contextualReferences.map(location => formatLocationWithContext(location, cwd)));
                            const plainLines = plainReferences.map(location => `  ${formatLocation(location, cwd)}`);
                            const lines = plainLines.length
                                ? [
                                    ...contextualLines,
                                    `  ... ${plainLines.length} additional reference(s) shown without context`,
                                    ...plainLines,
                                ]
                                : contextualLines;
                            output = `Found ${result.length} reference(s):\n${lines.join("\n")}`;
                        }
                        break;
                    }
                    case "hover": {
                        const result = (await sendRequest(client, "textDocument/hover", {
                            textDocument: { uri },
                            position,
                        }, signal));
                        if (!result || !result.contents) {
                            output = "No hover information";
                        }
                        else {
                            output = extractHoverText(result.contents);
                        }
                        break;
                    }
                    case "code_actions": {
                        const diagnostics = client.diagnostics.get(uri) ?? [];
                        const context = {
                            diagnostics,
                            only: !apply && query ? [query] : undefined,
                            triggerKind: 1,
                        };
                        const result = (await sendRequest(client, "textDocument/codeAction", {
                            textDocument: { uri },
                            range: { start: position, end: position },
                            context,
                        }, signal));
                        if (!result || result.length === 0) {
                            output = "No code actions available";
                            break;
                        }
                        if (apply === true && query) {
                            const normalizedQuery = query.trim();
                            if (normalizedQuery.length === 0) {
                                output = "Error: query parameter required when apply=true for code_actions";
                                break;
                            }
                            const parsedIndex = /^\d+$/.test(normalizedQuery) ? Number.parseInt(normalizedQuery, 10) : null;
                            const selectedAction = result.find((actionItem, index) => (parsedIndex !== null && index === parsedIndex) ||
                                actionItem.title.toLowerCase().includes(normalizedQuery.toLowerCase()));
                            if (!selectedAction) {
                                const actionLines = result.map((actionItem, index) => `  ${formatCodeAction(actionItem, index)}`);
                                output = `No code action matches "${normalizedQuery}". Available actions:\n${actionLines.join("\n")}`;
                                break;
                            }
                            const appliedAction = await applyCodeAction(selectedAction, {
                                resolveCodeAction: async (actionItem) => (await sendRequest(client, "codeAction/resolve", actionItem, signal)),
                                applyWorkspaceEdit: async (edit) => applyWorkspaceEdit(edit, cwd),
                                executeCommand: async (commandItem) => {
                                    await sendRequest(client, "workspace/executeCommand", {
                                        command: commandItem.command,
                                        arguments: commandItem.arguments ?? [],
                                    }, signal);
                                },
                            });
                            if (!appliedAction) {
                                output = `Action "${selectedAction.title}" has no workspace edit or command to apply`;
                                break;
                            }
                            const summaryLines = [];
                            if (appliedAction.edits.length > 0) {
                                summaryLines.push("  Workspace edit:");
                                summaryLines.push(...appliedAction.edits.map(item => `    ${item}`));
                            }
                            if (appliedAction.executedCommands.length > 0) {
                                summaryLines.push("  Executed command(s):");
                                summaryLines.push(...appliedAction.executedCommands.map(commandName => `    ${commandName}`));
                            }
                            output = `Applied "${appliedAction.title}":\n${summaryLines.join("\n")}`;
                            break;
                        }
                        const actionLines = result.map((actionItem, index) => `  ${formatCodeAction(actionItem, index)}`);
                        output = `${result.length} code action(s):\n${actionLines.join("\n")}`;
                        break;
                    }
                    case "symbols": {
                        if (!targetFile) {
                            output = "Error: file parameter required for document symbols";
                            break;
                        }
                        const result = (await sendRequest(client, "textDocument/documentSymbol", {
                            textDocument: { uri },
                        }, signal));
                        if (!result || result.length === 0) {
                            output = "No symbols found";
                        }
                        else {
                            const relPath = path.relative(cwd, targetFile);
                            if ("selectionRange" in result[0]) {
                                const lines = result.flatMap(s => formatDocumentSymbol(s));
                                output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
                            }
                            else {
                                const lines = result.map(s => {
                                    const line = s.location.range.start.line + 1;
                                    const icon = symbolKindToIcon(s.kind);
                                    return `${icon} ${s.name} @ line ${line}`;
                                });
                                output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
                            }
                        }
                        break;
                    }
                    case "incoming_calls": {
                        output = await formatCallHierarchyResults(client, position, uri, "incoming", cwd, signal);
                        break;
                    }
                    case "outgoing_calls": {
                        output = await formatCallHierarchyResults(client, position, uri, "outgoing", cwd, signal);
                        break;
                    }
                    case "format": {
                        if (!targetFile) {
                            output = "Error: file parameter required for format";
                            break;
                        }
                        const formatResult = (await sendRequest(client, "textDocument/formatting", {
                            textDocument: { uri },
                            options: {
                                tabSize: tab_size ?? 4,
                                insertSpaces: insert_spaces ?? true,
                            },
                        }, signal));
                        if (!formatResult || formatResult.length === 0) {
                            const relPath = path.relative(cwd, targetFile);
                            output = `${relPath}: already formatted (no changes)`;
                            break;
                        }
                        await applyTextEdits(targetFile, formatResult);
                        const relPath = path.relative(cwd, targetFile);
                        output = `Formatted ${relPath}: ${formatResult.length} edit(s) applied`;
                        break;
                    }
                    case "signature": {
                        const sigResult = (await sendRequest(client, "textDocument/signatureHelp", {
                            textDocument: { uri },
                            position,
                        }, signal));
                        if (!sigResult || !sigResult.signatures || sigResult.signatures.length === 0) {
                            output = "No signature information at this position";
                        }
                        else {
                            output = formatSignatureHelp(sigResult);
                        }
                        break;
                    }
                    case "rename": {
                        if (!new_name) {
                            return {
                                content: [{ type: "text", text: "Error: new_name parameter required for rename" }],
                                details: { action, serverName, success: false },
                            };
                        }
                        const result = (await sendRequest(client, "textDocument/rename", {
                            textDocument: { uri },
                            position,
                            newName: new_name,
                        }, signal));
                        if (!result) {
                            output = "Rename returned no edits";
                        }
                        else {
                            const shouldApply = apply !== false;
                            if (shouldApply) {
                                const applied = await applyWorkspaceEdit(result, cwd);
                                output = `Applied rename:\n${applied.map(a => `  ${a}`).join("\n")}`;
                            }
                            else {
                                const preview = formatWorkspaceEdit(result, cwd);
                                output = `Rename preview:\n${preview.map(p => `  ${p}`).join("\n")}`;
                            }
                        }
                        break;
                    }
                    case "reload": {
                        output = await reloadServer(client, serverName, signal);
                        break;
                    }
                    default:
                        output = `Unknown action: ${action}`;
                }
                return {
                    content: [{ type: "text", text: output }],
                    details: { serverName, action, success: true, request: params },
                };
            }
            catch (err) {
                if (err instanceof ToolAbortError || signal?.aborted) {
                    throw new ToolAbortError();
                }
                const errorMessage = err instanceof Error ? err.message : String(err);
                return {
                    content: [{ type: "text", text: `LSP error: ${errorMessage}` }],
                    details: { serverName, action, success: false, request: params },
                };
            }
        },
    };
}
/**
 * Default LSP tool using process.cwd().
 */
export const lspTool = createLspTool(process.cwd());
//# sourceMappingURL=index.js.map