update
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { readdir, readFile, stat, writeFile } from 'node:fs/promises'
|
||||
import { join, relative, sep } from 'node:path'
|
||||
import { listFiles } from './search-service'
|
||||
|
||||
export interface FileNode {
|
||||
name: string
|
||||
@@ -9,7 +10,7 @@ export interface FileNode {
|
||||
children?: FileNode[]
|
||||
}
|
||||
|
||||
/** Directories never walked — noise or huge, and not part of "the project". */
|
||||
/** Directories never walked by the fallback (rg already honors these as globs). */
|
||||
const IGNORE_DIRS = new Set([
|
||||
'node_modules', '.git', 'out', 'dist', 'build', '.next', '.nuxt',
|
||||
'.cache', 'coverage', 'vendor', '.idea', '.vscode', '.helder', '.turbo',
|
||||
@@ -22,10 +23,62 @@ function ignored(name: string): boolean {
|
||||
return IGNORE_DIRS.has(name) || name === '.DS_Store'
|
||||
}
|
||||
|
||||
/** Recursive project tree, dirs first then files, alphabetical. */
|
||||
function looksBinary(buf: Buffer): boolean {
|
||||
const n = Math.min(buf.length, 8000)
|
||||
for (let i = 0; i < n; i++) if (buf[i] === 0) return true
|
||||
return false
|
||||
}
|
||||
|
||||
// ---- tree from a flat path list (the rg-backed primary path) ----------------
|
||||
|
||||
function sortTree(node: FileNode): void {
|
||||
if (!node.children) return
|
||||
node.children.sort((a, b) => {
|
||||
if (a.type !== b.type) return a.type === 'dir' ? -1 : 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
for (const c of node.children) sortTree(c)
|
||||
}
|
||||
|
||||
/** Build a nested tree from relative file paths (dirs first, alphabetical). */
|
||||
export function buildTreeFromPaths(rootName: string, paths: string[]): FileNode {
|
||||
const root: FileNode = { name: rootName, type: 'dir', path: '', open: true, children: [] }
|
||||
const dirs = new Map<string, FileNode>([['', root]])
|
||||
for (const rel of paths) {
|
||||
const parts = rel.split('/').filter(Boolean)
|
||||
let parentPath = ''
|
||||
let parent = root
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const isFile = i === parts.length - 1
|
||||
const curPath = parentPath ? `${parentPath}/${parts[i]}` : parts[i]
|
||||
if (isFile) {
|
||||
parent.children!.push({ name: parts[i], type: 'file', path: curPath })
|
||||
} else {
|
||||
let dir = dirs.get(curPath)
|
||||
if (!dir) {
|
||||
dir = { name: parts[i], type: 'dir', path: curPath, open: parts.slice(0, i + 1).length <= 1, children: [] }
|
||||
dirs.set(curPath, dir)
|
||||
parent.children!.push(dir)
|
||||
}
|
||||
parent = dir
|
||||
parentPath = curPath
|
||||
}
|
||||
}
|
||||
}
|
||||
sortTree(root)
|
||||
return root
|
||||
}
|
||||
|
||||
function rootName(root: string): string {
|
||||
return root.split(sep).filter(Boolean).pop() || root
|
||||
}
|
||||
|
||||
/** Project tree. Primary: rg file list (honors gitignore + excludes). Fallback:
|
||||
* a plain recursive walk (when ripgrep is unavailable). */
|
||||
export async function readTree(root: string): Promise<FileNode> {
|
||||
const name = root.split(sep).filter(Boolean).pop() || root
|
||||
return { name, type: 'dir', path: '', open: true, children: await readDir(root, root, 0) }
|
||||
const paths = await listFiles(root).catch(() => [] as string[])
|
||||
if (paths.length) return buildTreeFromPaths(rootName(root), paths)
|
||||
return { name: rootName(root), type: 'dir', path: '', open: true, children: await readDir(root, root, 0) }
|
||||
}
|
||||
|
||||
async function readDir(abs: string, root: string, depth: number): Promise<FileNode[]> {
|
||||
@@ -55,12 +108,6 @@ async function readDir(abs: string, root: string, depth: number): Promise<FileNo
|
||||
return [...dirs, ...files]
|
||||
}
|
||||
|
||||
function looksBinary(buf: Buffer): boolean {
|
||||
const n = Math.min(buf.length, 8000)
|
||||
for (let i = 0; i < n; i++) if (buf[i] === 0) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/** Read a single text file (relative path) → string. */
|
||||
export async function readProjectFile(root: string, rel: string): Promise<string> {
|
||||
const buf = await readFile(join(root, rel))
|
||||
@@ -74,14 +121,38 @@ export async function writeProjectFile(root: string, rel: string, content: strin
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an in-memory content index of all (small, text) files — powers content
|
||||
* search and plain-file viewing without touching disk per keystroke. Capped to
|
||||
* keep large repos sane. PHASE: swap content search to ripgrep when scaling up.
|
||||
* In-memory content index of all (small, text) files — powers content viewing.
|
||||
* Primary: read the rg file list; fallback: walk. Capped for large repos.
|
||||
*/
|
||||
export async function readAll(root: string): Promise<Record<string, string>> {
|
||||
const paths = await listFiles(root).catch(() => [] as string[])
|
||||
if (paths.length) return readListed(root, paths)
|
||||
return readAllWalk(root)
|
||||
}
|
||||
|
||||
async function readListed(root: string, paths: string[]): Promise<Record<string, string>> {
|
||||
const out: Record<string, string> = {}
|
||||
let count = 0
|
||||
for (const rel of paths) {
|
||||
if (count >= MAX_INDEXED_FILES) break
|
||||
try {
|
||||
const abs = join(root, rel)
|
||||
const s = await stat(abs)
|
||||
if (s.size > MAX_FILE_BYTES) continue
|
||||
const buf = await readFile(abs)
|
||||
if (looksBinary(buf)) continue
|
||||
out[rel] = buf.toString('utf8')
|
||||
count++
|
||||
} catch {
|
||||
/* skip unreadable */
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
async function readAllWalk(root: string): Promise<Record<string, string>> {
|
||||
const out: Record<string, string> = {}
|
||||
let count = 0
|
||||
async function walk(abs: string): Promise<void> {
|
||||
if (count >= MAX_INDEXED_FILES) return
|
||||
let entries: import('node:fs').Dirent[]
|
||||
@@ -102,8 +173,7 @@ export async function readAll(root: string): Promise<Record<string, string>> {
|
||||
if (s.size > MAX_FILE_BYTES) continue
|
||||
const buf = await readFile(childAbs)
|
||||
if (looksBinary(buf)) continue
|
||||
const rel = relative(root, childAbs).split(sep).join('/')
|
||||
out[rel] = buf.toString('utf8')
|
||||
out[relative(root, childAbs).split(sep).join('/')] = buf.toString('utf8')
|
||||
count++
|
||||
} catch {
|
||||
/* skip unreadable */
|
||||
@@ -111,7 +181,6 @@ export async function readAll(root: string): Promise<Record<string, string>> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(root)
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user