UNPKG

pi-lens

Version:

Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo

1,299 lines • 56.8 kB
/**
 * lsp_navigation tool definition
 *
 * Extracted from index.ts for maintainability.
 */
import * as nodeFs from "node:fs";
import * as path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { Type } from "../clients/deps/typebox.js";
import { logLatency } from "../clients/latency-logger.js";
import { compactRenderResult } from "./render-compact.js";
import { applyWorkspaceEdit, summarizeWorkspaceEdit, } from "../clients/lsp/edits.js";
import { getLSPService } from "../clients/lsp/index.js";
import { buildLspNavigationEnvelope } from "./lsp-structured-output.js";
const VALID_OPERATIONS = [
    "definition",
    "typeDefinition",
    "declaration",
    "references",
    "hover",
    "signatureHelp",
    "documentSymbol",
    "findSymbol",
    "workspaceSymbol",
    "codeAction",
    "rename",
    "rename_file",
    "implementation",
    "prepareCallHierarchy",
    "incomingCalls",
    "outgoingCalls",
    "executeCommand",
    "workspaceDiagnostics",
    "capabilities",
];
const NAVIGABLE_SYMBOL_KINDS = new Set([
    5, // Class
    6, // Method
    8, // Field
    11, // Interface
    12, // Function
    13, // Variable
    22, // EnumMember
    23, // Struct
]);
function normalizeOperation(value) {
    if (typeof value !== "string")
        return "";
    return value.trim().replace(/^["']+|["']+$/g, "");
}
function isValidOperation(value) {
    return VALID_OPERATIONS.includes(value);
}
function operationSupportStatus(operation, support) {
    if (!support)
        return null;
    if (operation === "definition")
        return support.definition;
    if (operation === "typeDefinition")
        return support.typeDefinition;
    if (operation === "declaration")
        return support.declaration;
    if (operation === "references")
        return support.references;
    if (operation === "hover")
        return support.hover;
    if (operation === "signatureHelp")
        return support.signatureHelp;
    if (operation === "documentSymbol" || operation === "findSymbol")
        return support.documentSymbol;
    if (operation === "workspaceSymbol")
        return support.workspaceSymbol;
    if (operation === "codeAction")
        return support.codeAction;
    if (operation === "rename")
        return support.rename;
    if (operation === "implementation")
        return support.implementation;
    if (operation === "prepareCallHierarchy" ||
        operation === "incomingCalls" ||
        operation === "outgoingCalls")
        return support.callHierarchy;
    return null;
}
function emptyReasonForOperation(operation) {
    if (operation === "signatureHelp")
        return "position-sensitive-or-no-signature";
    if (operation === "codeAction")
        return "no-applicable-actions";
    if (operation === "rename")
        return "no-rename-edits-or-symbol-not-renamable";
    if (operation === "rename_file")
        return "no-file-rename-result";
    if (operation === "findSymbol")
        return "no-matching-symbols";
    if (operation === "workspaceSymbol")
        return "no-matching-symbols-or-server-index-unavailable";
    if (operation === "capabilities")
        return "no-active-lsp-servers";
    if (operation === "executeCommand")
        return "command-returned-no-result";
    if (operation === "incomingCalls" || operation === "outgoingCalls")
        return "no-call-hierarchy-results";
    return "no-results";
}
function escapeRegExp(value) {
    return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function parseSymbolSelector(symbol) {
    const trimmed = symbol.trim();
    const match = /^([^#]*)(?:#(-?\d+))?$/.exec(trimmed);
    const baseSymbol = (match?.[1] ?? trimmed).trim();
    const rawOccurrence = match?.[2];
    if (!rawOccurrence)
        return { baseSymbol, occurrence: 1 };
    const occurrence = Number.parseInt(rawOccurrence, 10);
    if (!Number.isFinite(occurrence) || occurrence < 1) {
        return {
            baseSymbol,
            occurrence: 1,
            debug: `invalid occurrence selector #${rawOccurrence}; using #1`,
        };
    }
    return { baseSymbol, occurrence };
}
function findNthMatch(lineText, regex, occurrence) {
    let seen = 0;
    regex.lastIndex = 0;
    let match;
    while ((match = regex.exec(lineText)) !== null) {
        seen += 1;
        if (seen === occurrence)
            return match;
        if (match[0].length === 0)
            regex.lastIndex += 1;
    }
    return null;
}
function firstNonWhitespaceCharacter(lineText) {
    const match = /\S/.exec(lineText);
    return (match?.index ?? 0) + 1;
}
function resolveSymbolColumn(content, line1, character, symbol) {
    if (typeof character === "number" && character > 0) {
        return { character, strategy: "explicit" };
    }
    const lineText = content.split(/\r?\n/)[line1 - 1] ?? "";
    if (!symbol || symbol.trim().length === 0) {
        return {
            character: 1,
            strategy: "fallback",
            debug: "character omitted and no symbol supplied; using column 1",
        };
    }
    const { baseSymbol, occurrence, debug } = parseSymbolSelector(symbol);
    if (!baseSymbol) {
        return {
            character: firstNonWhitespaceCharacter(lineText),
            requestedSymbol: symbol,
            baseSymbol,
            requestedOccurrence: occurrence,
            usedOccurrence: 1,
            strategy: "fallback",
            debug: debug ?? "empty symbol selector; using first non-whitespace column",
        };
    }
    const pattern = `\\b${escapeRegExp(baseSymbol)}\\b`;
    const exactRegex = new RegExp(pattern, "g");
    const exact = findNthMatch(lineText, exactRegex, occurrence);
    if (exact) {
        return {
            character: exact.index + 1,
            requestedSymbol: symbol,
            baseSymbol,
            requestedOccurrence: occurrence,
            usedOccurrence: occurrence,
            strategy: "word-boundary",
            debug,
        };
    }
    const firstExact = findNthMatch(lineText, exactRegex, 1);
    if (firstExact && occurrence !== 1) {
        return {
            character: firstExact.index + 1,
            requestedSymbol: symbol,
            baseSymbol,
            requestedOccurrence: occurrence,
            usedOccurrence: 1,
            strategy: "word-boundary",
            debug: `${debug ? `${debug}; ` : ""}occurrence #${occurrence} not found; using #1`,
        };
    }
    const insensitiveRegex = new RegExp(pattern, "gi");
    const insensitive = findNthMatch(lineText, insensitiveRegex, occurrence);
    if (insensitive) {
        return {
            character: insensitive.index + 1,
            requestedSymbol: symbol,
            baseSymbol,
            requestedOccurrence: occurrence,
            usedOccurrence: occurrence,
            strategy: "case-insensitive",
            debug: debug ?? "exact-case symbol not found; used case-insensitive match",
        };
    }
    const firstInsensitive = findNthMatch(lineText, insensitiveRegex, 1);
    if (firstInsensitive && occurrence !== 1) {
        return {
            character: firstInsensitive.index + 1,
            requestedSymbol: symbol,
            baseSymbol,
            requestedOccurrence: occurrence,
            usedOccurrence: 1,
            strategy: "case-insensitive",
            debug: `${debug ? `${debug}; ` : ""}occurrence #${occurrence} not found case-insensitively; using #1`,
        };
    }
    return {
        character: firstNonWhitespaceCharacter(lineText),
        requestedSymbol: symbol,
        baseSymbol,
        requestedOccurrence: occurrence,
        usedOccurrence: 1,
        strategy: "fallback",
        debug: `${debug ? `${debug}; ` : ""}symbol not found on line; using first non-whitespace column`,
    };
}
function tokenAtPosition(content, line1, char1) {
    const lines = content.split(/\r?\n/);
    const line = lines[line1 - 1];
    if (!line)
        return undefined;
    const chars = [...line];
    const idx = Math.max(0, Math.min(chars.length - 1, char1 - 1));
    const isWord = (ch) => !!ch && /[A-Za-z0-9_?!]/.test(ch);
    let left = idx;
    let right = idx;
    if (!isWord(chars[idx]) && isWord(chars[idx + 1])) {
        left = idx + 1;
        right = idx + 1;
    }
    while (left > 0 && isWord(chars[left - 1]))
        left -= 1;
    while (right < chars.length - 1 && isWord(chars[right + 1]))
        right += 1;
    const token = chars
        .slice(left, right + 1)
        .join("")
        .trim();
    return token.length > 0 ? token : undefined;
}
const SYMBOL_KIND_LABELS = {
    2: "module",
    3: "namespace",
    4: "package",
    5: "class",
    6: "method",
    7: "property",
    8: "field",
    9: "constructor",
    10: "enum",
    11: "interface",
    12: "function",
    13: "variable",
    14: "constant",
    15: "string",
    16: "number",
    17: "boolean",
    18: "array",
    19: "object",
    20: "key",
    21: "null",
    22: "enumMember",
    23: "struct",
    24: "event",
    25: "operator",
    26: "typeParameter",
};
function symbolKindLabel(kind) {
    return kind == null ? "symbol" : (SYMBOL_KIND_LABELS[kind] ?? "symbol");
}
function rangeStart(range) {
    const start = range?.start;
    return {
        line: typeof start?.line === "number" ? start.line + 1 : undefined,
        character: typeof start?.character === "number" ? start.character + 1 : undefined,
    };
}
function findSymbolMatches(symbols, query, options) {
    const normalizedQuery = query.trim().toLowerCase();
    if (!normalizedQuery)
        return [];
    const matches = [];
    const matchesText = (symbol) => {
        const values = [symbol.name, symbol.detail]
            .filter((value) => Boolean(value))
            .map((value) => value.trim().toLowerCase());
        return options.exactMatch
            ? values.some((value) => value === normalizedQuery)
            : values.some((value) => value.includes(normalizedQuery));
    };
    const matchesKind = (symbol) => {
        if (options.kinds.size === 0)
            return true;
        return options.kinds.has(symbolKindLabel(symbol.kind).toLowerCase());
    };
    const visit = (entries, depth) => {
        for (const symbol of entries) {
            if (symbol.name && matchesText(symbol) && matchesKind(symbol)) {
                const preferredRange = symbol.selectionRange ?? symbol.range;
                const start = rangeStart(preferredRange);
                matches.push({
                    name: symbol.name,
                    kind: symbolKindLabel(symbol.kind),
                    kindCode: symbol.kind,
                    detail: symbol.detail,
                    line: start.line,
                    character: start.character,
                    depth,
                    location: symbol.location,
                    range: preferredRange,
                });
                if (matches.length >= options.maxResults)
                    return;
            }
            if (!options.topLevelOnly && symbol.children?.length) {
                visit(symbol.children, depth + 1);
                if (matches.length >= options.maxResults)
                    return;
            }
        }
    };
    visit(symbols, 1);
    return matches;
}
function flattenSymbols(symbols) {
    const all = [];
    for (const symbol of symbols) {
        all.push(symbol);
        if (symbol.children && symbol.children.length > 0) {
            all.push(...flattenSymbols(symbol.children));
        }
    }
    return all;
}
function pickLocalSymbolLocation(symbols, token, filePath) {
    const flat = flattenSymbols(symbols).filter((symbol) => symbol.name === token);
    if (flat.length === 0)
        return [];
    const uri = pathToFileURL(filePath).href;
    return flat
        .map((symbol) => {
        if (symbol.location?.uri && symbol.location.range) {
            return { uri: symbol.location.uri, range: symbol.location.range };
        }
        if (symbol.range) {
            return { uri, range: symbol.range };
        }
        return undefined;
    })
        .filter((entry) => Boolean(entry));
}
function workspaceSymbolDedupeKey(symbol) {
    const location = symbol.location;
    const start = rangeStart(location?.range ?? symbol.range ?? symbol.selectionRange);
    return [
        symbol.name ?? "",
        symbol.detail ?? "",
        symbol.kind ?? "",
        location?.uri ?? "",
        start.line ?? "",
        start.character ?? "",
    ].join(":");
}
function dedupeWorkspaceSymbols(symbols) {
    const out = [];
    const seen = new Set();
    for (const symbol of symbols) {
        const key = workspaceSymbolDedupeKey(symbol);
        if (seen.has(key))
            continue;
        seen.add(key);
        out.push(symbol);
    }
    return out;
}
function asRecord(value) {
    return typeof value === "object" && value !== null
        ? value
        : undefined;
}
function searchReadFromUriRange(uri, range) {
    if (typeof uri !== "string" || !uri.startsWith("file:"))
        return undefined;
    const rangeLike = asRecord(range);
    const startLine = rangeLike?.start?.line;
    if (typeof startLine !== "number" || !Number.isFinite(startLine)) {
        return undefined;
    }
    const endLine = rangeLike?.end?.line;
    try {
        return {
            file: fileURLToPath(uri),
            startLine: Math.max(1, Math.floor(startLine) + 1),
            endLine: typeof endLine === "number" && Number.isFinite(endLine)
                ? Math.max(1, Math.floor(endLine) + 1)
                : undefined,
        };
    }
    catch {
        return undefined;
    }
}
function pushSearchRead(out, uri, range) {
    const loc = searchReadFromUriRange(uri, range);
    if (loc)
        out.push(loc);
}
function collectLocationSearchReads(result) {
    const out = [];
    for (const entry of Array.isArray(result) ? result : [result]) {
        const record = asRecord(entry);
        if (!record)
            continue;
        pushSearchRead(out, record.uri, record.range);
        pushSearchRead(out, record.targetUri, record.targetSelectionRange ?? record.targetRange);
    }
    return out;
}
function collectWorkspaceSymbolSearchReads(result) {
    const out = [];
    for (const entry of Array.isArray(result) ? result : [result]) {
        const symbol = asRecord(entry);
        const location = asRecord(symbol?.location);
        if (!location)
            continue;
        pushSearchRead(out, location.uri, location.range);
        pushSearchRead(out, location.targetUri, location.targetSelectionRange ?? location.targetRange);
    }
    return out;
}
function collectCallHierarchySearchReads(result, operation, callHierarchyItem) {
    const out = [];
    for (const entry of Array.isArray(result) ? result : [result]) {
        const record = asRecord(entry);
        if (!record)
            continue;
        const item = asRecord(operation === "incomingCalls" ? record.from : record.to);
        pushSearchRead(out, item?.uri, item?.selectionRange ?? item?.range);
        const rangeUri = operation === "incomingCalls" ? item?.uri : callHierarchyItem?.uri;
        const fromRanges = record.fromRanges;
        if (Array.isArray(fromRanges)) {
            for (const range of fromRanges)
                pushSearchRead(out, rangeUri, range);
        }
    }
    return out;
}
function collectSearchReadsForOperation(operation, result, callHierarchyItem) {
    if ([
        "definition",
        "typeDefinition",
        "declaration",
        "references",
        "implementation",
    ].includes(operation)) {
        return collectLocationSearchReads(result);
    }
    if (operation === "workspaceSymbol") {
        return collectWorkspaceSymbolSearchReads(result);
    }
    if (operation === "incomingCalls" || operation === "outgoingCalls") {
        return collectCallHierarchySearchReads(result, operation, callHierarchyItem);
    }
    return [];
}
function formatCapabilities(snapshots, filePath) {
    if (snapshots.length === 0) {
        return filePath
            ? `No active LSP server for ${path.basename(filePath)}. Open/touch the file first or run another LSP operation to start the server.`
            : "No active LSP servers in this session.";
    }
    const rows = [
        ["definition", (s) => !!s.operationSupport.definition],
        ["typeDefinition", (s) => !!s.operationSupport.typeDefinition],
        ["declaration", (s) => !!s.operationSupport.declaration],
        ["references", (s) => !!s.operationSupport.references],
        ["hover", (s) => !!s.operationSupport.hover],
        ["rename", (s) => !!s.operationSupport.rename],
        ["codeAction", (s) => !!s.operationSupport.codeAction],
        ["workspaceSymbol", (s) => !!s.operationSupport.workspaceSymbol],
        ["implementation", (s) => !!s.operationSupport.implementation],
        ["signatureHelp", (s) => !!s.operationSupport.signatureHelp],
        ["incomingCalls", (s) => !!s.operationSupport.callHierarchy],
        ["outgoingCalls", (s) => !!s.operationSupport.callHierarchy],
        [
            "workspaceDiagnostics",
            (s) => s.workspaceDiagnosticsSupport.mode === "pull",
            "pull diagnostics",
        ],
        [
            "rename_file",
            () => true,
            "willRenameFiles/didRenameFiles helper available",
        ],
    ];
    const lines = [];
    for (const snapshot of snapshots) {
        const label = filePath
            ? `${snapshot.serverId} (${path.basename(filePath)})`
            : `${snapshot.serverId} (${snapshot.root})`;
        lines.push(label);
        for (const [name, supported, note] of rows) {
            const suffix = note ? ` (${note})` : "";
            lines.push(`  ${name} ${supported(snapshot) ? "✓" : "✗"}${suffix}`);
        }
        const commands = snapshot.advertisedCommands ?? [];
        lines.push(`  executeCommand ${commands.length > 0 ? "✓" : "✗"} (${commands.length} advertised command(s)` +
            (commands.length > 0 ? `: ${commands.slice(0, 20).join(", ")}` : "") +
            ")");
    }
    return lines.join("\n");
}
function classifyCodeActions(actions) {
    if (!actions || actions.length === 0)
        return { quickfix: 0, refactor: 0, other: 0 };
    let quickfix = 0;
    let refactor = 0;
    let other = 0;
    for (const action of actions) {
        const kind = action.kind ?? "";
        if (kind.startsWith("quickfix"))
            quickfix += 1;
        else if (kind.startsWith("refactor"))
            refactor += 1;
        else
            other += 1;
    }
    return { quickfix, refactor, other };
}
async function openFileBestEffort(lspService, filePath, waitForDiagnostics = false) {
    let fileContent;
    try {
        fileContent = nodeFs.readFileSync(filePath, "utf-8");
    }
    catch {
        return;
    }
    if (!fileContent)
        return;
    try {
        if (typeof lspService.touchFile === "function") {
            await lspService.touchFile(filePath, fileContent, {
                diagnostics: waitForDiagnostics ? "document" : "none",
                source: "lsp_navigation",
                clientScope: waitForDiagnostics ? "all" : "primary",
            });
        }
        else {
            await lspService.openFile(filePath, fileContent);
        }
    }
    catch {
        /* LSP server may not be ready yet — proceed anyway */
    }
}
export function createLspNavigationTool(getFlag) {
    return {
        name: "lsp_navigation",
        label: "LSP Navigate",
        description: "Navigate code using LSP (Language Server Protocol). LSP is enabled by default; disable with --no-lsp.\n" +
            "Operations:\n" +
            "- definition: Jump to where a symbol is defined\n" +
            "- typeDefinition: Jump to the definition of a symbol's TYPE (e.g. the class/interface of a variable)\n" +
            "- declaration: Jump to a symbol's declaration (e.g. an extern/forward decl, distinct from its definition)\n" +
            "- references: Find all usages of a symbol\n" +
            "- hover: Get type/doc info at a position\n" +
            "- signatureHelp: Show callable signatures at cursor\n" +
            "- documentSymbol: List all symbols (functions/classes/vars) in a file\n" +
            "- findSymbol: Search document symbols in a file by name/detail with optional kind/top-level/exact filters\n" +
            "- workspaceSymbol: Search symbols across the whole project (best with path context)\n" +
            "- codeAction: Find available quick fixes/refactors at a range\n" +
            "- rename: Compute or apply workspace edits for renaming a symbol\n" +
            "- rename_file: Preview/apply LSP-aware source file rename notifications\n" +
            "- implementation: Jump to interface implementations\n" +
            "- prepareCallHierarchy: Get callable item at position (for incoming/outgoing)\n" +
            "- incomingCalls: Find all functions/methods that CALL this function\n" +
            "- outgoingCalls: Find all functions/methods CALLED by this function\n" +
            "- executeCommand: Run a server-advertised command via workspace/executeCommand. HARDENED: allowlisted to commands the server advertised; dry-run by default (reports whether advertised) — set apply:true to actually run. Pass command (+ optional commandArguments).\n" +
            "- workspaceDiagnostics: List all diagnostics tracked by active LSP clients\n" +
            "- capabilities: Show cached operation support for active LSP servers\n\n" +
            "Line and character are 1-based (as shown in editors). For position-based operations, prefer passing symbol when you know the line but not the exact character; character can be omitted or -1 and pi-lens will resolve the symbol column. Use symbol#N for repeated symbols on the same line (1-based occurrence).",
        promptSnippet: "Find definitions, references, and hover info via LSP",
        renderResult: compactRenderResult(({ details, args, isError, text }) => {
            const op = details?.operation ??
                (typeof args.operation === "string" ? args.operation : "lsp");
            if (isError || details?.supported === false) {
                return `lsp_navigation ${op} — ${details?.emptyReason ?? text.split("\n")[0] ?? "unavailable"}`;
            }
            const n = details?.resultCount ?? 0;
            if (n === 0) {
                return `lsp_navigation ${op} — ${details?.emptyReason ?? "no results"}`;
            }
            return `lsp_navigation ${op} — ${n} result${n === 1 ? "" : "s"}`;
        }),
        parameters: Type.Object({
            operation: Type.String({
                description: "LSP operation to perform. Valid values: " +
                    VALID_OPERATIONS.join(", "),
            }),
            path: Type.Optional(Type.String({
                description: "Absolute or relative file path. Required for file-scoped operations; optional for workspaceSymbol/workspaceDiagnostics.",
            })),
            line: Type.Optional(Type.Number({
                description: "Line number (1-based). Required for definition/references/hover/implementation",
            })),
            character: Type.Optional(Type.Number({
                description: "Character offset (1-based). Optional when symbol is provided; use -1 to force symbol-column resolution.",
            })),
            symbol: Type.Optional(Type.String({
                description: "Symbol name on the target line for automatic character resolution. Use symbol#N to select the Nth occurrence on the line.",
            })),
            endLine: Type.Optional(Type.Number({
                description: "End line (1-based). Optional; used by codeAction range.",
            })),
            endCharacter: Type.Optional(Type.Number({
                description: "End character (1-based). Optional; used by codeAction range.",
            })),
            newName: Type.Optional(Type.String({
                description: "Required for rename operation.",
            })),
            newFilePath: Type.Optional(Type.String({
                description: "Required for rename_file operation.",
            })),
            apply: Type.Optional(Type.Boolean({
                description: "rename/executeCommand: apply for real. rename defaults to preview; executeCommand defaults to a dry-run that only reports whether the command is advertised — set apply:true to actually run it.",
            })),
            command: Type.Optional(Type.String({
                description: "executeCommand only: the server command id to run. Must be one the server advertised (see the capabilities operation).",
            })),
            commandArguments: Type.Optional(Type.Array(Type.Unknown(), {
                description: "executeCommand only: arguments array passed to workspace/executeCommand.",
            })),
            query: Type.Optional(Type.String({
                description: "Symbol name to search. Used by workspaceSymbol and findSymbol.",
            })),
            kinds: Type.Optional(Type.Array(Type.String(), {
                description: "findSymbol only: restrict matches to symbol kind labels such as function, class, method, variable, interface.",
            })),
            exactMatch: Type.Optional(Type.Boolean({
                description: "findSymbol only: match whole symbol names/details exactly instead of substring matching.",
            })),
            topLevelOnly: Type.Optional(Type.Boolean({
                description: "findSymbol only: do not search nested child symbols.",
            })),
            maxResults: Type.Optional(Type.Number({
                description: "findSymbol only: maximum matches to return. Default 20.",
            })),
            callHierarchyItem: Type.Optional(Type.Object({
                name: Type.String(),
                kind: Type.Number(),
                uri: Type.String(),
                range: Type.Object({
                    start: Type.Object({
                        line: Type.Number(),
                        character: Type.Number(),
                    }),
                    end: Type.Object({
                        line: Type.Number(),
                        character: Type.Number(),
                    }),
                }),
                selectionRange: Type.Object({
                    start: Type.Object({
                        line: Type.Number(),
                        character: Type.Number(),
                    }),
                    end: Type.Object({
                        line: Type.Number(),
                        character: Type.Number(),
                    }),
                }),
            }, {
                description: "Call hierarchy item. Required for incomingCalls/outgoingCalls",
            })),
        }),
        async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
            const startedAt = Date.now();
            let supported = null;
            let diagnosticsMode = "unknown";
            let columnResolution;
            const finalize = (payload, meta) => {
                const normalizedFilePath = meta.filePath.replace(/\\/g, "/");
                logLatency({
                    type: "phase",
                    phase: "lsp_navigation_result",
                    filePath: normalizedFilePath,
                    durationMs: Date.now() - startedAt,
                    metadata: {
                        operation: meta.operation,
                        failureKind: meta.failureKind,
                        resultCount: meta.resultCount,
                        supported,
                        diagnosticsMode,
                        columnResolution,
                    },
                });
                const text = payload.content[0]?.text ?? "";
                const envelope = buildLspNavigationEnvelope({
                    operation: meta.operation,
                    filePath: meta.filePath,
                    failureKind: meta.failureKind,
                    resultCount: meta.resultCount,
                    text,
                    isError: payload.isError,
                    details: payload.details,
                });
                return {
                    ...payload,
                    content: [
                        {
                            type: "text",
                            // Compact JSON: omit indentation. Saves ~50% on rename / workspace-edits payloads
                            // while keeping the envelope machine-parseable. Tests use JSON.parse so
                            // they are agnostic to whitespace.
                            text: JSON.stringify(envelope),
                        },
                    ],
                    details: {
                        ...(payload.details ?? {}),
                        failureKind: meta.failureKind,
                    },
                };
            };
            if (getFlag("no-lsp")) {
                return finalize({
                    content: [
                        {
                            type: "text",
                            text: "lsp_navigation requires LSP to be enabled. Remove --no-lsp to use LSP navigation.",
                        },
                    ],
                    isError: true,
                }, {
                    operation: "precheck",
                    filePath: "(workspace)",
                    failureKind: "lsp_disabled",
                    resultCount: 0,
                });
            }
            const { operation: rawOperation, path: rawPath, line, character, symbol, endLine, endCharacter, newName, newFilePath, apply, command, commandArguments, query, kinds, exactMatch, topLevelOnly, maxResults, callHierarchyItem, } = params;
            const normalizedOperation = normalizeOperation(rawOperation);
            if (!isValidOperation(normalizedOperation)) {
                return finalize({
                    content: [
                        {
                            type: "text",
                            text: `Unknown lsp_navigation operation "${normalizedOperation || String(rawOperation ?? "") || ""}". ` +
                                `Valid operations: ${VALID_OPERATIONS.join(", ")}`,
                        },
                    ],
                    isError: true,
                    details: {
                        rawOperation,
                        normalizedOperation,
                        validOperations: VALID_OPERATIONS,
                    },
                }, {
                    operation: normalizedOperation || "invalid",
                    filePath: "(workspace)",
                    failureKind: "invalid_operation",
                    resultCount: 0,
                });
            }
            const operation = normalizedOperation;
            const isCallHierarchyTraversal = operation === "incomingCalls" || operation === "outgoingCalls";
            const needsFilePath = operation !== "workspaceDiagnostics" &&
                operation !== "workspaceSymbol" &&
                operation !== "executeCommand" &&
                operation !== "capabilities" &&
                !isCallHierarchyTraversal;
            if (needsFilePath && (!rawPath || rawPath.trim().length === 0)) {
                return finalize({
                    content: [
                        {
                            type: "text",
                            text: `path is required for ${operation}`,
                        },
                    ],
                    isError: true,
                }, {
                    operation,
                    filePath: "(workspace)",
                    failureKind: "missing_file_path",
                    resultCount: 0,
                });
            }
            const filePath = rawPath
                ? path.isAbsolute(rawPath)
                    ? rawPath
                    : path.resolve(ctx.cwd || ".", rawPath)
                : "";
            let filePathIsDirectory = false;
            if (filePath) {
                try {
                    filePathIsDirectory = nodeFs.statSync(filePath).isDirectory();
                }
                catch {
                    // non-existent path — existing error paths handle this
                }
            }
            const lspService = getLSPService();
            if (operation === "capabilities") {
                const snapshots = await lspService.getCapabilitySnapshots(rawPath ? filePath : undefined);
                const output = formatCapabilities(snapshots, rawPath ? filePath : undefined);
                return finalize({
                    content: [{ type: "text", text: output }],
                    details: {
                        operation,
                        resultCount: snapshots.length,
                        servers: snapshots.map((snapshot) => snapshot.serverId),
                    },
                }, {
                    operation,
                    filePath: rawPath ? filePath : "(workspace)",
                    failureKind: snapshots.length === 0 ? "empty_result" : "success",
                    resultCount: snapshots.length,
                });
            }
            if (operation === "workspaceDiagnostics") {
                const wsDiagSupport = await lspService.getWorkspaceDiagnosticsSupport(rawPath ? filePath : undefined);
                diagnosticsMode = wsDiagSupport?.mode ?? "unknown";
                if (rawPath && !filePathIsDirectory) {
                    const hasLSP = lspService.supportsLSP(filePath);
                    if (!hasLSP) {
                        return finalize({
                            content: [
                                {
                                    type: "text",
                                    text: `No LSP server available for ${path.basename(filePath)}. Check that the language server is installed.`,
                                },
                            ],
                            isError: true,
                        }, {
                            operation,
                            filePath,
                            failureKind: "no_server",
                            resultCount: 0,
                        });
                    }
                    await openFileBestEffort(lspService, filePath, true);
                    const diagnostics = await lspService.getDiagnostics(filePath);
                    const result = [
                        {
                            filePath,
                            diagnostics,
                            count: diagnostics.length,
                        },
                    ];
                    const noteMap = {
                        pull: "Note: path mode requests pull diagnostics for this file and returns the aggregated result",
                        "push-only": "Note: server is push-only; result depends on published diagnostics for this file",
                    };
                    const note = noteMap[diagnosticsMode] ??
                        "Note: workspace diagnostics mode unknown (no active capability snapshot)";
                    const resultCount = diagnostics.length;
                    return finalize({
                        content: [
                            {
                                type: "text",
                                text: `${note}\n${JSON.stringify(result)}`,
                            },
                        ],
                        details: {
                            operation,
                            resultCount,
                            diagnosticsMode,
                            coverage: "requested-file",
                        },
                    }, {
                        operation,
                        filePath,
                        failureKind: resultCount === 0 ? "empty_result" : "success",
                        resultCount,
                    });
                }
                const allDiagnostics = await lspService.getAllDiagnostics();
                const result = Array.from(allDiagnostics.entries()).map(([trackedFile, { diags }]) => ({
                    filePath: trackedFile,
                    diagnostics: diags,
                    count: diags.length,
                }));
                const noteMap2 = {
                    "push-only": "Note: push-only tracked diagnostics snapshot (not full workspace pull diagnostics).",
                    pull: "Note: tracked diagnostics snapshot from active clients. Provide path to force file-level diagnostics collection",
                };
                const note = noteMap2[diagnosticsMode] ??
                    "Note: workspace diagnostics mode unknown (no active capability snapshot)";
                return finalize({
                    content: [
                        {
                            type: "text",
                            text: `${note}\n${JSON.stringify(result)}`,
                        },
                    ],
                    details: {
                        operation,
                        resultCount: result.length,
                        diagnosticsMode,
                        coverage: "tracked-open-files",
                    },
                }, {
                    operation,
                    filePath: rawPath ? filePath : "(workspace)",
                    failureKind: diagnosticsMode === "push-only" ? "tracked_snapshot" : "success",
                    resultCount: result.length,
                });
            }
            if (needsFilePath && filePathIsDirectory) {
                return finalize({
                    content: [
                        {
                            type: "text",
                            text: `path must be a source file, got directory: ${filePath}. Pass a source file path, or omit path for workspace-level operations.`,
                        },
                    ],
                    isError: true,
                }, {
                    operation,
                    filePath,
                    failureKind: "filepath_is_directory",
                    resultCount: 0,
                });
            }
            const hasLSP = filePath ? lspService.supportsLSP(filePath) : false;
            if (needsFilePath && !hasLSP) {
                return finalize({
                    content: [
                        {
                            type: "text",
                            text: `No LSP server available for ${path.basename(filePath)}. Check that the language server is installed.`,
                        },
                    ],
                    isError: true,
                }, {
                    operation,
                    filePath,
                    failureKind: "no_server",
                    resultCount: 0,
                });
            }
            if (needsFilePath) {
                const support = await lspService.getOperationSupport(filePath);
                supported = operationSupportStatus(operation, support);
                if (supported === false) {
                    return finalize({
                        content: [
                            {
                                type: "text",
                                text: `LSP server for ${path.basename(filePath)} does not advertise support for ${operation}`,
                            },
                        ],
                        isError: true,
                        details: {
                            operation,
                            supported: false,
                            emptyReason: "unsupported",
                        },
                    }, { operation, filePath, failureKind: "unsupported", resultCount: 0 });
                }
                await openFileBestEffort(lspService, filePath);
            }
            // Convert 1-based editor coords to 0-based LSP coords.
            const lspLine = (line ?? 1) - 1;
            const needsPosition = [
                "definition",
                "typeDefinition",
                "declaration",
                "references",
                "hover",
                "signatureHelp",
                "codeAction",
                "rename",
                "implementation",
                "prepareCallHierarchy",
            ].includes(operation);
            const resolvedCharacter = needsPosition && filePath
                ? resolveSymbolColumn(nodeFs.existsSync(filePath)
                    ? nodeFs.readFileSync(filePath, "utf-8")
                    : "", line ?? 1, character, symbol)
                : {
                    character: character ?? 1,
                    strategy: "explicit",
                };
            columnResolution = resolvedCharacter;
            const lspChar = resolvedCharacter.character - 1;
            const lspEndLine = (endLine ?? line ?? 1) - 1;
            const lspEndChar = (endCharacter ?? resolvedCharacter.character) - 1;
            const runWorkspaceSymbolOperation = async () => {
                supported = operationSupportStatus(operation, await lspService.getOperationSupport(rawPath ? filePath : undefined));
                if (supported === false) {
                    throw new Error("__UNSUPPORTED__ Active LSP server does not advertise support for workspaceSymbol");
                }
                if (!query || query.trim().length === 0) {
                    throw new Error("__BADINPUT__ query parameter required for workspaceSymbol");
                }
                if (rawPath) {
                    await openFileBestEffort(lspService, filePath);
                }
                try {
                    const raw = await lspService.workspaceSymbol(query ?? "", rawPath ? filePath : undefined);
                    const filtered = (Array.isArray(raw) ? raw : [raw]).filter((s) => typeof s === "object" &&
                        s !== null &&
                        (!s.kind || NAVIGABLE_SYMBOL_KINDS.has(s.kind)));
                    return dedupeWorkspaceSymbols(filtered).slice(0, 15);
                }
                catch (err) {
                    const msg = err instanceof Error ? err.message : String(err);
                    if (rawPath && /No Project/i.test(msg)) {
                        await openFileBestEffort(lspService, filePath);
                        await new Promise((resolve) => setTimeout(resolve, 120));
                        const retryRaw = await lspService.workspaceSymbol(query ?? "", filePath);
                        const retrySymbols = (Array.isArray(retryRaw) ? retryRaw : [retryRaw]).filter((s) => typeof s === "object" && s !== null);
                        return dedupeWorkspaceSymbols(retrySymbols);
                    }
                    throw err;
                }
            };
            const runOperation = async () => {
                switch (operation) {
                    case "definition":
                        return lspService.definition(filePath, lspLine, lspChar);
                    case "typeDefinition":
                        return lspService.typeDefinition(filePath, lspLine, lspChar);
                    case "declaration":
                        return lspService.declaration(filePath, lspLine, lspChar);
                    case "references":
                        return lspService.references(filePath, lspLine, lspChar);
                    case "hover":
                        return lspService.hover(filePath, lspLine, lspChar);
                    case "signatureHelp":
                        return lspService.signatureHelp(filePath, lspLine, lspChar);
                    case "documentSymbol":
                        return lspService.documentSymbol(filePath);
                    case "findSymbol": {
                        if (!query || query.trim().length === 0) {
                            throw new Error("__BADINPUT__ query parameter required for findSymbol");
                        }
                        const symbols = (await lspService.documentSymbol(filePath));
                        return findSymbolMatches(symbols, query, {
                            maxResults: Math.max(1, Math.min(100, maxResults ?? 20)),
                            topLevelOnly: topLevelOnly ?? false,
                            exactMatch: exactMatch ?? false,
                            kinds: new Set((kinds ?? [])
                                .map((kind) => kind.trim().toLowerCase())
                                .filter(Boolean)),
                        });
                    }
                    case "workspaceSymbol":
                        return runWorkspaceSymbolOperation();
                    case "codeAction":
                        return lspService.codeAction(filePath, lspLine, lspChar, lspEndLine, lspEndChar);
                    case "rename": {
                        if (!newName || newName.trim().length === 0) {
                            throw new Error("__BADINPUT__ newName parameter required for rename");
                        }
                        const edit = await lspService.rename(filePath, lspLine, lspChar, newName);
                        if (!edit)
                            return null;
                        if (!apply) {
                            return {
                                applied: false,
                                summary: summarizeWorkspaceEdit(edit, ctx.cwd || "."),
                                edit,
                            };
                        }
                        const applied = await applyWorkspaceEdit(edit, ctx.cwd || ".");
                        for (const touchedFile of applied.files) {
                            try {
                                await openFileBestEffort(lspService, touchedFile, false);
                            }
                            catch {
                                // Best-effort LSP resync only; disk edit already succeeded.
                            }
                        }
                        return { applied: true, ...applied };
                    }
                    case "rename_file": {
                        if (!newFilePath || newFilePath.trim().length === 0) {
                            throw new Error("__BADINPUT__ newFilePath parameter required for rename_file");
                        }
                        const resolvedNewFilePath = path.isAbsolute(newFilePath)
                            ? newFilePath
                            : path.resolve(ctx.cwd || ".", newFilePath);
                        const result = await lspService.renameFile(filePath, resolvedNewFilePath, {
                            cwd: ctx.cwd || ".",
                            apply: apply ?? false,
                        });
                        if (result.applied) {
                            for (const touchedFile of result.files ?? []) {
                                try {
                                    await openFileBestEffort(lspService, touchedFile, false);
                                }
                                catch {
                                    // Best-effort LSP resync only; disk edit already succeeded.
                                }
                            }
                        }
                        return result;
                    }
                    case "implementation":
                        return lspService.implementation(filePath, lspLine, lspChar);
                    case "prepareCallHierarchy":
                        return lspService.prepareCallHierarchy(filePath, lspLine, lspChar);
                    case "executeCommand": {
                        if (!command || command.trim().length === 0) {
                            throw new Error("__BADINPUT__ command parameter required for executeCommand");
                        }
                        const advertised = await lspService.getAdvertisedCommands(rawPath ? filePath : undefined);
                        const isAdvertised = advertised.includes(command);
                        // Dry-run by default: report advertisement status without running.
                        // Mutation only on explicit apply:true (and the client re-checks
                        // the allowlist — defense in depth).
                        if (apply !== true) {
                            return {
                                executed: false,
                                dryRun: true,
                                command,
                                advertised: isAdvertised,
                                advertisedCommands: advertised,
                                note: isAdvertised
                                    ? "Command is advertised. Re-run with apply:true to execute."
                                    : "Command is NOT advertised by the active server; execution would be refused.",
                            };
                        }
                        if (!isAdvertised) {
                            throw new Error(`__UNSUPPORTED__ command "${command}" is not advertised by the active LSP server (advertised: ${advertised.join(", ") || "none"})`);
                        }
                        return lspService.executeCommand(rawPath ? filePath : undefined, command, commandArguments);
                    }
                    case "incomingCalls": {
                        if (!callHierarchyItem) {
                            throw new Error("__BADINPUT__ callHierarchyItem parameter required for incomingCalls");
                        }
                        return lspService.incomingCalls(callHierarchyItem);
                    }
                    case "outgoingCalls": {
                        if (!callHierarchyItem) {
                            throw new Error("__BADINPUT__ callHierarchyItem parameter required for outgoingCalls");
                        }
                        return lspService.outgoingCalls(callHierarchyItem);
                    }
                    default:
                        return [];
                }
            };
            let result;
            let usedDocumentSymbolFallback = false;
            try {
                result = await runOperation();
                const isEmptyInitial = !result || (Array.isArray(result) && result.length === 0);
                const shouldRetryOnEmpty = isEmptyInitial &&
                    needsFilePath &&
                    [
                        "definition",
                        "typeDefinition",
                        "declaration",
                        "references",
                        "hover",
                        "signatureHelp",
                        "codeAction",
                        "rename",
                        "implementation",
                    ].includes(operation);
                if (shouldRetryOnEmpty) {
                    await openFileBestEffort(lspService, filePath, true);
                    result = await runOperation();
                }
                const stillEmpty = !result || (Array.isArray(result) && result.length === 0);
                if (stillEmpty && needsFilePath && operation === "definition") {
                    const content = nodeFs.readFileSync(filePath, "utf-8");
                    const token = line && character
                        ? tokenAtPosition(content, line, character)
                        : undefined;
                    if (token) {
                        const docSymbols = (await lspService.documentSymbol(filePath));
                        const locations = pickLocalSymbolLocation(docSymbols, token, filePath);
                        if (locations.length > 0) {
                            result = locations;
                            usedDocumentSymbolFallback = true;
                        }
                    }
                }
            }
            catch (err) {
                const msg = err instanceof Error ? err.message : String(err);
                if (msg.startsWith("__UNSUPPORTED__ ")) {
                    return finalize({
                        content: [
                            {
                                type: "text",
                                text: msg.replace("__UNSUPPORTED__ ", ""),
                            },
                        ],
                        isError: true,
                        details: {
                            operation,
                            supported: false,
                            emptyReason: "unsupported",
                        },
                    }, { operation, filePath, failureKind: "unsupported", resultCount: 0 });
                }
                if (msg.startsWith("__BADINPUT__ ")) {
                    return finalize({
                        content: [
                            {
                                type: "text",
                                text: msg.replace("__BADINPUT__ ", ""),
                            },
                        ],
                        isError: true,
                        details: {},
                    }, { operation, filePath, failureKind: "bad_input", resultCount: 0 });
                }
                return finalize({
                    content: [
                        {
                            type: "text",
                            text: `LSP error: ${err instanceof Error ? err.message : String(err)}`,
                        },
                    ],
                    isError: true,
                    details: {},
                }, { operation, filePath, failureKind: "lsp_error", resultCount: 0 });
            }
            const isEmpty = !result || (Array.isArray(result) && result.length === 0);
            const fileCtx = filePath ? " at " + path.basename(filePath) : "";
            const lineCtx = line ? ":" + line + ":" + character : "";
            let output = isEmpty
                ? "No results for " + operation + fileCtx + lineCtx
                : JSON.stringify(result);
            if (isEmpty && operation === "workspaceSymbol" && !rawPath) {
                output +=
                    "\nHint: provide path to scope workspaceSymbol to the active language server/root.";
            }
            if (usedDocumentSymbolFallback) {
                output +=
                    "\nNote: served from documentSymbol fallback due to empty primary result.";
            }
            if (operation === "references" &&
                Array.isArray(result) &&
                result.length <= 2) {
                output +=
                    "\nHint: references from usage sites can be partial; retry from the symbol definition for broader cross-file results.";
            }
            const actionStats = operation === "codeAction" && Array.isArray(result)
                ? classifyCodeActions(result)
                : null;
            if (operation === "codeAction" && actionStats) {
                if (actionStats.quickfix === 0 && actionStats.refactor > 0) {
                    output +=
                        "\nNote: no diagnostic quick fixes returned; refactor-only actions available.";
                }
            }
            const resultCount = Array.isArray(result)
                ? result.length
                : result
                    ? 1
                    : 0;
            const searchReads = collectSearchReadsForOperation(operation, result, callHierarchyItem);
            return finalize({
                content: [{ type: "text", text: output }],
                details: {
                    operation,
                    supported,
                    searchReads: searchReads.length > 0 ? searchReads : undefined,
                    emptyReason: isEmpty
                        ? emptyReasonForOperation(operation)
                        : undefined,
                    codeActionKinds: actionStats ?? undefined,
                    columnResolution: columnResolution?.strategy === "explicit"
                        ? undefined
                        : columnResolution,
                    resultCount,
                },
            }, {
                operation,
                filePath: rawPath ? filePath : "(workspace)",
                failureKind: isEmpty
                    ? "empty_result"
                    : usedDocumentSymbolFallback
                        ? "fallback_success"
                        : "success",
                resultCount,
            });
        },
    };
}