init helder

This commit is contained in:
2026-06-15 22:33:46 +02:00
parent 3d77fdfeff
commit 3f5078841d
38 changed files with 6349 additions and 2083 deletions

View File

@@ -0,0 +1,87 @@
import { createRequire } from 'node:module'
import { spawn } from 'node:child_process'
import { relative, sep } from 'node:path'
/** Content search via ripgrep; file-name list via `rg --files`. Substring
* (fixed-string), smart-case — matching the prototype's search semantics. */
const require = createRequire(import.meta.url)
let rgPath: string | null = null
try {
rgPath = (require('@vscode/ripgrep') as { rgPath: string }).rgPath
} catch (e) {
console.error('[helder] @vscode/ripgrep unavailable:', (e as Error).message)
}
export interface ContentHit { no: number; ln: string; ix: number }
export interface ContentGroup { path: string; hits: ContentHit[] }
const IGNORE_GLOBS = ['node_modules', '.git', 'out', 'dist', 'build', '.cache', 'vendor', 'coverage', '.helder']
.flatMap((d) => ['--glob', `!${d}`])
const MAX_FILES = 400
const MAX_LINE = 1000
function toRel(root: string, p: string): string {
return relative(root, p).split(sep).join('/')
}
export function searchContent(root: string, query: string): Promise<ContentGroup[]> {
return new Promise((resolve) => {
if (!rgPath || query.trim().length < 2) return resolve([])
const child = spawn(rgPath, [
'--json', '--fixed-strings', '--smart-case',
'--max-count', '50', '--max-columns', '2000',
...IGNORE_GLOBS, '-e', query, '--', root,
])
const order: string[] = []
const groups = new Map<string, ContentGroup>()
let buf = ''
let done = false
const finish = (): void => { if (done) return; done = true; resolve(order.slice(0, MAX_FILES).map((p) => groups.get(p)!)) }
child.stdout.on('data', (chunk: Buffer) => {
buf += chunk.toString()
let nl: number
while ((nl = buf.indexOf('\n')) >= 0) {
const line = buf.slice(0, nl); buf = buf.slice(nl + 1)
if (!line) continue
let msg: { type: string; data: { path?: { text?: string }; lines?: { text?: string }; line_number?: number; submatches?: { start: number }[] } }
try { msg = JSON.parse(line) } catch { continue }
if (msg.type !== 'match') continue
const abs = msg.data.path?.text
const text = msg.data.lines?.text
if (!abs || text == null) continue
const rel = toRel(root, abs)
let g = groups.get(rel)
if (!g) { if (groups.size >= MAX_FILES) continue; g = { path: rel, hits: [] }; groups.set(rel, g); order.push(rel) }
const ln = text.replace(/\n$/, '').slice(0, MAX_LINE)
const ix = msg.data.submatches && msg.data.submatches[0] ? msg.data.submatches[0].start : 0
g.hits.push({ no: msg.data.line_number || 0, ln, ix: Math.min(ix, ln.length) })
}
})
child.on('close', finish)
child.on('error', finish)
})
}
export function listFiles(root: string): Promise<string[]> {
return new Promise((resolve) => {
if (!rgPath) return resolve([])
const child = spawn(rgPath, ['--files', ...IGNORE_GLOBS, '--', root])
let buf = ''
const out: string[] = []
let done = false
const finish = (): void => { if (done) return; done = true; resolve(out) }
child.stdout.on('data', (chunk: Buffer) => {
buf += chunk.toString()
let nl: number
while ((nl = buf.indexOf('\n')) >= 0) {
const line = buf.slice(0, nl); buf = buf.slice(nl + 1)
if (line) out.push(toRel(root, line))
}
})
child.on('close', () => { if (buf.trim()) out.push(toRel(root, buf.trim())); finish() })
child.on('error', finish)
})
}