UNPKG

pi-lens

Version:

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

295 lines (294 loc) • 9.98 kB
import { fileURLToPath, pathToFileURL } from "node:url"; function asRecord(value) { return typeof value === "object" && value !== null ? value : undefined; } function finiteNumber(value) { return typeof value === "number" && Number.isFinite(value) ? value : undefined; } export function lspStatusFromFailureKind(failureKind) { if (failureKind === "success" || failureKind === "fallback_success") { return "success"; } if (failureKind === "empty_result") return "empty"; if (failureKind === "unsupported") return "unsupported"; if (failureKind === "bad_input" || failureKind === "invalid_operation" || failureKind === "missing_file_path") { return "bad_input"; } if (failureKind === "no_server") return "no_server"; if (failureKind === "lsp_disabled") return "lsp_disabled"; if (failureKind === "filepath_is_directory") return "filepath_is_directory"; if (failureKind === "tracked_snapshot") return "tracked_snapshot"; return "error"; } function lspToolLocationFromUriRange(uri, range) { if (typeof uri !== "string" || !uri.startsWith("file:")) return undefined; const rangeLike = asRecord(range); const startLine = finiteNumber(rangeLike?.start?.line); const startCharacter = finiteNumber(rangeLike?.start?.character); const endLine = finiteNumber(rangeLike?.end?.line); const endCharacter = finiteNumber(rangeLike?.end?.character); if (startLine === undefined || startCharacter === undefined || endLine === undefined || endCharacter === undefined) { return undefined; } try { return { uri, filePath: fileURLToPath(uri), // LLM-facing coordinates: 1-based, matching editor/tool params. range: { start: { line: Math.floor(startLine) + 1, character: Math.floor(startCharacter) + 1, }, end: { line: Math.floor(endLine) + 1, character: Math.floor(endCharacter) + 1, }, }, }; } catch { return undefined; } } function lspToolLocationFromFileRange(filePath, range) { if (!filePath || filePath === "(workspace)") return undefined; const rangeLike = asRecord(range); const startLine = finiteNumber(rangeLike?.start?.line); const startCharacter = finiteNumber(rangeLike?.start?.character); const endLine = finiteNumber(rangeLike?.end?.line); const endCharacter = finiteNumber(rangeLike?.end?.character); if (startLine === undefined || startCharacter === undefined || endLine === undefined || endCharacter === undefined) { return undefined; } return { uri: pathToFileURL(filePath).href, filePath, range: { start: { line: Math.floor(startLine) + 1, character: Math.floor(startCharacter) + 1, }, end: { line: Math.floor(endLine) + 1, character: Math.floor(endCharacter) + 1, }, }, }; } function pushToolLocation(out, uri, range) { const loc = lspToolLocationFromUriRange(uri, range); if (loc) out.push(loc); } function collectLocationSummaries(result, filePath) { const out = []; for (const entry of Array.isArray(result) ? result : [result]) { const record = asRecord(entry); if (!record) continue; pushToolLocation(out, record.uri, record.range); pushToolLocation(out, record.targetUri, record.targetSelectionRange ?? record.targetRange); if (!record.uri && !record.targetUri) { const local = lspToolLocationFromFileRange(filePath, record.range); let alreadyIncluded = false; for (const loc of out) { if (loc.uri === local?.uri) { alreadyIncluded = true; break; } } if (local && !alreadyIncluded) out.push(local); } } return out; } function collectWorkspaceSymbolLocationSummaries(result) { const out = []; for (const entry of Array.isArray(result) ? result : [result]) { const symbol = asRecord(entry); const location = asRecord(symbol?.location); if (!location) continue; pushToolLocation(out, location.uri, location.range); pushToolLocation(out, location.targetUri, location.targetSelectionRange ?? location.targetRange); } return out; } function collectCallHierarchyLocationSummaries(result, operation) { const out = []; for (const entry of Array.isArray(result) ? result : [result]) { const record = asRecord(entry); const item = asRecord(operation === "incomingCalls" ? record?.from : record?.to); pushToolLocation(out, item?.uri, item?.selectionRange ?? item?.range); } return out; } export function collectLspToolLocationsForOperation(operation, result, filePath) { if ([ "definition", "typeDefinition", "declaration", "references", "implementation", "prepareCallHierarchy", ].includes(operation)) { return collectLocationSummaries(result, filePath); } if (operation === "workspaceSymbol") { return collectWorkspaceSymbolLocationSummaries(result); } if (operation === "documentSymbol" || operation === "findSymbol") { return collectLocationSummaries(result, filePath); } if (operation === "incomingCalls" || operation === "outgoingCalls") { return collectCallHierarchyLocationSummaries(result, operation); } return []; } function parseBalancedJsonPrefix(text) { const trimmed = text.trim(); const start = trimmed.search(/[[{]/); if (start < 0) return undefined; const open = trimmed[start]; const close = open === "[" ? "]" : "}"; let depth = 0; let inString = false; let escaped = false; for (let i = start; i < trimmed.length; i += 1) { const ch = trimmed[i]; if (inString) { if (escaped) escaped = false; else if (ch === "\\") escaped = true; else if (ch === '"') inString = false; continue; } if (ch === '"') { inString = true; continue; } if (ch === open) depth += 1; if (ch === close) depth -= 1; if (depth === 0) { try { return { value: JSON.parse(trimmed.slice(start, i + 1)), rest: trimmed.slice(i + 1).trim(), }; } catch { return undefined; } } } return undefined; } export function parseLspNavigationTextPayload(text) { const notes = []; const hints = []; const errors = []; const trimmed = text.trim(); if (!trimmed) return { notes, hints, errors }; try { return { result: JSON.parse(trimmed), notes, hints, errors }; } catch { // Continue with note/hint extraction and balanced-prefix parsing. } const lines = trimmed.split(/\r?\n/); let jsonCandidate = trimmed; if (/^Note:/i.test(lines[0] ?? "") && lines.length > 1) { notes.push((lines.shift() ?? "").replace(/^Note:\s*/i, "")); jsonCandidate = lines.join("\n").trim(); } const parsed = parseBalancedJsonPrefix(jsonCandidate); if (parsed) { for (const entry of parsed.rest.split(/\r?\n/)) { const line = entry.trim(); if (!line) continue; if (/^Hint:/i.test(line)) hints.push(line.replace(/^Hint:\s*/i, "")); else if (/^Note:/i.test(line)) notes.push(line.replace(/^Note:\s*/i, "")); else notes.push(line); } return { result: parsed.value, notes, hints, errors }; } for (const entry of lines) { const line = entry.trim(); if (!line) continue; if (/^Hint:/i.test(line)) hints.push(line.replace(/^Hint:\s*/i, "")); else if (/^Note:/i.test(line)) notes.push(line.replace(/^Note:\s*/i, "")); else if (/^LSP error:/i.test(line)) errors.push(line); else notes.push(line); } return { notes, hints, errors }; } function withoutUndefined(value) { for (const key of Object.keys(value)) { if (value[key] === undefined) delete value[key]; } return value; } export function buildLspNavigationEnvelope(options) { const parsed = parseLspNavigationTextPayload(options.text ?? ""); const status = lspStatusFromFailureKind(options.failureKind); const result = parsed.result; const notes = [...parsed.notes]; const hints = [...parsed.hints]; const errors = [...parsed.errors]; if (options.isError && errors.length === 0 && (options.text ?? "").trim()) { errors.push((options.text ?? "").trim()); } const locations = collectLspToolLocationsForOperation(options.operation, result, options.filePath ?? ""); return withoutUndefined({ tool: "lsp_navigation", operation: options.operation, ok: !options.isError, status, filePath: options.filePath, resultCount: options.resultCount, result, locations: locations.length > 0 ? locations : undefined, notes: notes.length > 0 ? notes : undefined, hints: hints.length > 0 ? hints : undefined, errors: errors.length > 0 ? errors : undefined, metadata: options.details, }); }