// Formatting helpers for diagnostics, locations, symbols, hovers and workspace edits. import fs from 'node:fs' import path from 'node:path' import { fromUri } from './client.js' const SEV = { 1: 'error', 2: 'warning', 3: 'info', 4: 'hint' } export function rel(file, cwd) { if (!cwd) return file const r = path.relative(cwd, file) return r && !r.startsWith('..') && !path.isAbsolute(r) ? r : file } /** Diagnostics → compact lines; returns {text, counts}. */ export function formatDiagnostics(file, items, { cwd, minSeverity = 2, max = 40 } = {}) { const filtered = items.filter((d) => (d.severity ?? 1) <= minSeverity) const counts = { error: 0, warning: 0, info: 0, hint: 0 } for (const d of items) counts[SEV[d.severity ?? 1]]++ const sorted = [...filtered].sort((a, b) => (a.severity ?? 1) - (b.severity ?? 1) || a.range.start.line - b.range.start.line) const lines = sorted.slice(0, max).map((d) => { const l = d.range.start.line + 1; const c = d.range.start.character + 1 const code = d.code != null ? ` [${typeof d.code === 'object' ? d.code.value : d.code}]` : '' const src = d.source ? ` (${d.source})` : '' return `${rel(file, cwd)}:${l}:${c} ${SEV[d.severity ?? 1]}${code}: ${String(d.message).split('\n')[0]}${src}` }) if (sorted.length > max) lines.push(`… ${sorted.length - max} more`) return { text: lines.join('\n'), counts, shown: filtered.length } } export function lineText(file, line) { try { return fs.readFileSync(file, 'utf8').split(/\r?\n/)[line] ?? '' } catch { return '' } } export function formatLocations(locs, { cwd, max = 50 } = {}) { const arr = (Array.isArray(locs) ? locs : locs ? [locs] : []).map((l) => ({ uri: l.uri ?? l.targetUri, range: l.range ?? l.targetSelectionRange ?? l.targetRange })) if (!arr.length) return 'No results.' const out = arr.slice(0, max).map((l) => { const f = fromUri(l.uri); const ln = l.range.start.line return `${rel(f, cwd)}:${ln + 1}:${l.range.start.character + 1}: ${lineText(f, ln).trim()}` }) if (arr.length > max) out.push(`… ${arr.length - max} more`) return out.join('\n') } const KIND = ['', 'file', 'module', 'namespace', 'package', 'class', 'method', 'property', 'field', 'constructor', 'enum', 'interface', 'function', 'variable', 'constant', 'string', 'number', 'boolean', 'array', 'object', 'key', 'null', 'enum-member', 'struct', 'event', 'operator', 'type-param'] export function formatSymbols(syms, { cwd, depth = 0, max = 200 } = {}) { const out = [] const walk = (list, d) => { for (const s of list || []) { if (out.length >= max) return const range = s.selectionRange ?? s.range ?? s.location?.range const where = s.location ? `${rel(fromUri(s.location.uri), cwd)}:` : '' out.push(`${' '.repeat(d)}${KIND[s.kind] ?? 'symbol'} ${s.name}${s.detail ? ` ${s.detail}` : ''} — ${where}${range ? range.start.line + 1 : '?'}`) if (s.children) walk(s.children, d + 1) } } walk(syms, depth) return out.length ? out.join('\n') : 'No symbols.' } export function formatHover(h) { if (!h?.contents) return 'No hover information.' const c = h.contents const part = (x) => (typeof x === 'string' ? x : x.value ?? '') return (Array.isArray(c) ? c.map(part) : [part(c)]).filter(Boolean).join('\n\n') || 'No hover information.' } /** WorkspaceEdit → [{file, edits:[{range,newText}]}] plus file ops. */ export function normalizeWorkspaceEdit(we) { const files = new Map(); const ops = [] const add = (uri, edits) => { const f = fromUri(uri); files.set(f, [...(files.get(f) || []), ...edits]) } for (const [uri, edits] of Object.entries(we?.changes || {})) add(uri, edits) for (const dc of we?.documentChanges || []) { if (dc.textDocument) add(dc.textDocument.uri, dc.edits) else if (dc.kind) ops.push(dc) } return { files, ops } } /** Apply LSP TextEdits (utf-16 positions) to text. */ export function applyTextEdits(text, edits) { const lineStarts = [0] for (let i = 0; i < text.length; i++) if (text[i] === '\n') lineStarts.push(i + 1) const off = (p) => Math.min(text.length, (lineStarts[p.line] ?? text.length) + p.character) const sorted = [...edits].sort((a, b) => off(b.range.start) - off(a.range.start)) let out = text for (const e of sorted) out = out.slice(0, off(e.range.start)) + e.newText + out.slice(off(e.range.end)) return out } /** Column (0-based utf-16) of `symbol` on a line; supports `name#N` occurrence selectors. */ export function columnOf(lineStr, symbol) { if (!symbol) return lineStr.search(/\S/) >= 0 ? lineStr.search(/\S/) : 0 let name = symbol; let nth = 1 const m = /^(.*)#(\d+)$/.exec(symbol) if (m) { name = m[1]; nth = Number(m[2]) } const ident = /^[A-Za-z_$][\w$]*$/.test(name) const find = (hay, needle) => { const re = ident ? new RegExp(`(?