Files

109 lines
5.1 KiB
JavaScript

// 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(`(?<![\\w$])${needle.replace(/[$]/g, '\\$')}(?![\\w$])`, 'g') : new RegExp(needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')
const hits = []; let x
while ((x = re.exec(hay))) { hits.push(x.index); if (!x[0].length) re.lastIndex++ }
return hits
}
let hits = find(lineStr, name)
if (!hits.length) hits = find(lineStr.toLowerCase(), name.toLowerCase())
if (hits.length < nth) return -1
return hits[nth - 1]
}