Initial commit: omp-lsp DSH plugin (oh-my-pi port) with docs and tests
This commit is contained in:
commit
17b4a5f433
11 files changed
+1091
No files matched your search
+108
@@ -0,0 +1,108 @@
|
||||
// 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]
|
||||
}
|
||||
Reference in new issue
Block a user