update fixing the build of the app
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
import { createRequire } from 'node:module'
|
|
||||||
import { spawn } from 'node:child_process'
|
import { spawn } from 'node:child_process'
|
||||||
import { relative, sep } from 'node:path'
|
import { relative, sep } from 'node:path'
|
||||||
import { getConfig } from './config'
|
import { getConfig } from './config'
|
||||||
@@ -6,18 +5,23 @@ import { getConfig } from './config'
|
|||||||
/** ripgrep is the single source of "what files are in the project": it powers
|
/** ripgrep is the single source of "what files are in the project": it powers
|
||||||
* content search, the file-name list, AND the Explorer tree / content index
|
* content search, the file-name list, AND the Explorer tree / content index
|
||||||
* (via fs-service) — so gitignore + files.exclude are honored everywhere the
|
* (via fs-service) — so gitignore + files.exclude are honored everywhere the
|
||||||
* same way. Substring (fixed-string), smart-case search. */
|
* same way. Substring (fixed-string), smart-case search.
|
||||||
const require = createRequire(import.meta.url)
|
*
|
||||||
|
* @vscode/ripgrep ships as ESM, so load it with dynamic import() (works for
|
||||||
let rgPath: string | null = null
|
* ESM and CJS) and cache the resolved binary path. */
|
||||||
try {
|
const rgPathPromise: Promise<string | null> = (async () => {
|
||||||
rgPath = (require('@vscode/ripgrep') as { rgPath: string }).rgPath
|
try {
|
||||||
|
const mod = await import('@vscode/ripgrep')
|
||||||
|
let p = (mod as { rgPath: string }).rgPath
|
||||||
// When packaged the binary is unpacked from the asar; rgPath still points
|
// When packaged the binary is unpacked from the asar; rgPath still points
|
||||||
// inside app.asar, so redirect it. No-op in dev (path has no app.asar).
|
// inside app.asar, so redirect it. No-op in dev (path has no app.asar).
|
||||||
if (rgPath) rgPath = rgPath.replace(/\bapp\.asar\b/, 'app.asar.unpacked')
|
if (p) p = p.replace(/\bapp\.asar\b/, 'app.asar.unpacked')
|
||||||
} catch (e) {
|
return p || null
|
||||||
|
} catch (e) {
|
||||||
console.error('[helder] @vscode/ripgrep unavailable:', (e as Error).message)
|
console.error('[helder] @vscode/ripgrep unavailable:', (e as Error).message)
|
||||||
}
|
return null
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
export interface ContentHit { no: number; ln: string; ix: number }
|
export interface ContentHit { no: number; ln: string; ix: number }
|
||||||
export interface ContentGroup { path: string; hits: ContentHit[] }
|
export interface ContentGroup { path: string; hits: ContentHit[] }
|
||||||
@@ -31,8 +35,8 @@ const BASE_IGNORE = [
|
|||||||
const MAX_FILES = 400
|
const MAX_FILES = 400
|
||||||
const MAX_LINE = 1000
|
const MAX_LINE = 1000
|
||||||
|
|
||||||
export function rgAvailable(): boolean {
|
export async function rgAvailable(): Promise<boolean> {
|
||||||
return !!rgPath
|
return !!(await rgPathPromise)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Glob/ignore args derived from config (files.exclude, files.followGitignore). */
|
/** Glob/ignore args derived from config (files.exclude, files.followGitignore). */
|
||||||
@@ -49,9 +53,10 @@ function toRel(root: string, p: string): string {
|
|||||||
return relative(root, p).split(sep).join('/')
|
return relative(root, p).split(sep).join('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function searchContent(root: string, query: string): Promise<ContentGroup[]> {
|
export async function searchContent(root: string, query: string): Promise<ContentGroup[]> {
|
||||||
|
const rgPath = await rgPathPromise
|
||||||
|
if (!rgPath || query.trim().length < 2) return []
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!rgPath || query.trim().length < 2) return resolve([])
|
|
||||||
const child = spawn(rgPath, [
|
const child = spawn(rgPath, [
|
||||||
'--json', '--fixed-strings', '--smart-case', '--hidden',
|
'--json', '--fixed-strings', '--smart-case', '--hidden',
|
||||||
'--max-count', '50', '--max-columns', '2000',
|
'--max-count', '50', '--max-columns', '2000',
|
||||||
@@ -90,9 +95,10 @@ export function searchContent(root: string, query: string): Promise<ContentGroup
|
|||||||
|
|
||||||
/** All project files (relative paths), honoring gitignore + excludes. Includes
|
/** All project files (relative paths), honoring gitignore + excludes. Includes
|
||||||
* dotfiles (--hidden) so .env etc. show up unless ignored. */
|
* dotfiles (--hidden) so .env etc. show up unless ignored. */
|
||||||
export function listFiles(root: string): Promise<string[]> {
|
export async function listFiles(root: string): Promise<string[]> {
|
||||||
|
const rgPath = await rgPathPromise
|
||||||
|
if (!rgPath) return []
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!rgPath) return resolve([])
|
|
||||||
const child = spawn(rgPath, ['--files', '--hidden', ...ignoreArgs(), '--', root])
|
const child = spawn(rgPath, ['--files', '--hidden', ...ignoreArgs(), '--', root])
|
||||||
let buf = ''
|
let buf = ''
|
||||||
const out: string[] = []
|
const out: string[] = []
|
||||||
|
|||||||
@@ -289,9 +289,11 @@ body {
|
|||||||
/* ============ overlays ============ */
|
/* ============ overlays ============ */
|
||||||
.scrim { position:fixed; inset:0; background:rgba(8,9,11,0.5); z-index:50; display:flex; justify-content:center; align-items:flex-start; padding-top:90px; backdrop-filter:blur(1.5px); }
|
.scrim { position:fixed; inset:0; background:rgba(8,9,11,0.5); z-index:50; display:flex; justify-content:center; align-items:flex-start; padding-top:90px; backdrop-filter:blur(1.5px); }
|
||||||
.palette { width:620px; max-width:92vw; background:#212429; border:1px solid var(--border-2); border-radius:11px; box-shadow:0 24px 70px rgba(0,0,0,.55); overflow:hidden; }
|
.palette { width:620px; max-width:92vw; background:#212429; border:1px solid var(--border-2); border-radius:11px; box-shadow:0 24px 70px rgba(0,0,0,.55); overflow:hidden; }
|
||||||
.palette .pi { display:flex; align-items:center; gap:10px; padding:12px 15px; border-bottom:1px solid var(--border); }
|
.palette .pi, .search-modal .pi { display:flex; align-items:center; gap:10px; padding:12px 15px; border-bottom:1px solid var(--border); }
|
||||||
.palette .pi input { flex:1; background:transparent; border:0; outline:0; color:var(--fg-0); font-size:15px; font-family:var(--ui); }
|
.palette .pi input, .search-modal .pi input { flex:1; min-width:0; background:transparent; border:0; outline:0; color:var(--fg-0); font-size:15px; font-family:var(--ui); }
|
||||||
.palette .pi .mode-chip { font-size:10px; color:var(--fg-3); border:1px solid var(--border-2); border-radius:5px; padding:2px 7px; font-family:var(--mono); }
|
.palette .pi input::placeholder, .search-modal .pi input::placeholder { color:var(--fg-3); }
|
||||||
|
.palette .pi svg, .search-modal .pi svg { flex:0 0 auto; }
|
||||||
|
.palette .pi .mode-chip, .search-modal .pi .mode-chip { flex:0 0 auto; white-space:nowrap; font-size:10px; color:var(--fg-3); border:1px solid var(--border-2); border-radius:5px; padding:2px 7px; font-family:var(--mono); }
|
||||||
.palette .results { max-height:380px; overflow:auto; padding:6px; }
|
.palette .results { max-height:380px; overflow:auto; padding:6px; }
|
||||||
.pres { display:flex; align-items:center; gap:10px; padding:7px 10px; border-radius:7px; cursor:pointer; }
|
.pres { display:flex; align-items:center; gap:10px; padding:7px 10px; border-radius:7px; cursor:pointer; }
|
||||||
.pres.sel { background:var(--accent-soft); }
|
.pres.sel { background:var(--accent-soft); }
|
||||||
|
|||||||
40
test/search.test.ts
Normal file
40
test/search.test.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
|
import { simpleGit } from 'simple-git'
|
||||||
|
import { listFiles, rgAvailable, searchContent } from '../src/main/search-service'
|
||||||
|
|
||||||
|
let dir = ''
|
||||||
|
afterEach(async () => { if (dir) await rm(dir, { recursive: true, force: true }) })
|
||||||
|
|
||||||
|
describe('search-service (real ripgrep via dynamic import)', () => {
|
||||||
|
it('ripgrep is available (catches the ESM require() regression)', async () => {
|
||||||
|
expect(await rgAvailable()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('finds content matches with line number + column', async () => {
|
||||||
|
dir = await mkdtemp(join(tmpdir(), 'helder-search-'))
|
||||||
|
await simpleGit(dir).init()
|
||||||
|
await writeFile(join(dir, 'a.ts'), 'const x = 1\nconst balance = 2\n')
|
||||||
|
await writeFile(join(dir, 'b.ts'), 'nothing relevant\n')
|
||||||
|
const groups = await searchContent(dir, 'balance')
|
||||||
|
expect(groups).toHaveLength(1)
|
||||||
|
expect(groups[0].path).toBe('a.ts')
|
||||||
|
expect(groups[0].hits[0].no).toBe(2)
|
||||||
|
expect(groups[0].hits[0].ln).toContain('balance')
|
||||||
|
expect(groups[0].hits[0].ix).toBe('const '.length)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns [] for queries under 2 characters', async () => {
|
||||||
|
dir = await mkdtemp(join(tmpdir(), 'helder-search-'))
|
||||||
|
expect(await searchContent(dir, 'a')).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lists project files', async () => {
|
||||||
|
dir = await mkdtemp(join(tmpdir(), 'helder-search-'))
|
||||||
|
await simpleGit(dir).init()
|
||||||
|
await writeFile(join(dir, 'keep.ts'), 'x')
|
||||||
|
expect(await listFiles(dir)).toContain('keep.ts')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user