diff --git a/.github/workflows/validation-template.yml b/.github/workflows/validation-template.yml new file mode 100644 index 00000000..99871602 --- /dev/null +++ b/.github/workflows/validation-template.yml @@ -0,0 +1,51 @@ +name: Validation template + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: validation-template-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + node-quality: + name: Node quality + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + cache: npm + - name: Install dependencies + run: npm ci --ignore-scripts + - name: Check package formatting and linting + run: npm run check:ci + + rust-quality: + name: Rust quality + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + - name: Install Zig + uses: goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406 # v2.2.1 + with: + version: 0.16.0 + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + with: + components: rustfmt, clippy + - name: Check Rust formatting + run: cargo fmt --all -- --check + - name: Run Clippy + run: cargo clippy --no-default-features --features zlob -- -D warnings diff --git a/crates/fff-core/src/index/bigram_filter.rs b/crates/fff-core/src/index/bigram_filter.rs index 6ce2a319..09051d5f 100644 --- a/crates/fff-core/src/index/bigram_filter.rs +++ b/crates/fff-core/src/index/bigram_filter.rs @@ -264,7 +264,7 @@ impl BigramIndexBuilder { fn flush_seen(&self, seen: &[u64; SEEN_WORDS], word_idx: usize, bit_mask: u64) { let col_base = self.col_data_ptr(); let words = self.words; - for (blk, block) in seen.chunks_exact(8).enumerate() { + for (blk, block) in seen.as_chunks::<8>().0.iter().enumerate() { // OR-test whole blocks so the mostly-empty bitmap scans fast. if block.iter().fold(0u64, |a, &w| a | w) == 0 { continue; diff --git a/package-lock.json b/package-lock.json index 429b55de..cbc3ebb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "fff.nvim", + "name": "fff", "lockfileVersion": 3, "requires": true, "packages": { @@ -1065,6 +1065,19 @@ "koffi": "^2.9.0" } }, + "node_modules/@ff-labs/fff-bin-android-arm64": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@ff-labs/fff-bin-android-arm64/-/fff-bin-android-arm64-0.0.0.tgz", + "integrity": "sha512-3ZWgk0f4CQB+X+sQ4dE7/mD0duXnh9pLYO7PZwab0JkvlfUTVMwp2z0bZCvrDirZNB48piAmOY6kR8FpCyjzqA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@ff-labs/fff-bun": { "resolved": "packages/fff-bun", "link": true diff --git a/packages/fff-bun/examples/glob-bench.ts b/packages/fff-bun/examples/glob-bench.ts index c4b73341..524a9342 100644 --- a/packages/fff-bun/examples/glob-bench.ts +++ b/packages/fff-bun/examples/glob-bench.ts @@ -1,4 +1,5 @@ #!/usr/bin/env bun + /** * Glob benchmark: fff.glob vs Bun.Glob vs npm `glob`. * @@ -18,8 +19,8 @@ * bun add glob */ -import { performance } from "node:perf_hooks"; import { resolve } from "node:path"; +import { performance } from "node:perf_hooks"; import { Glob as BunGlob } from "bun"; import { FileFinder } from "../src/index"; @@ -30,7 +31,7 @@ try { const mod: { glob: (pattern: string, opts: { cwd: string }) => Promise; } = - // @ts-ignore - optional peer; resolved at runtime, may be absent + // @ts-expect-error - optional peer; resolved at runtime, may be absent await import("glob"); npmGlob = mod.glob; } catch { diff --git a/packages/fff-bun/examples/grep.ts b/packages/fff-bun/examples/grep.ts index 622117de..854053cf 100755 --- a/packages/fff-bun/examples/grep.ts +++ b/packages/fff-bun/examples/grep.ts @@ -1,4 +1,5 @@ #!/usr/bin/env bun + /** * Interactive live grep demo * @@ -9,9 +10,9 @@ * content search prompt with match highlighting. */ +import * as readline from "node:readline"; import { FileFinder } from "../src/index"; import type { GrepMode } from "../src/types"; -import * as readline from "node:readline"; const RESET = "\x1b[0m"; const BOLD = "\x1b[1m"; diff --git a/packages/fff-bun/examples/search.ts b/packages/fff-bun/examples/search.ts index b6c8403d..d419b2b9 100755 --- a/packages/fff-bun/examples/search.ts +++ b/packages/fff-bun/examples/search.ts @@ -1,4 +1,5 @@ #!/usr/bin/env bun + /** * Interactive file finder demo * @@ -15,11 +16,11 @@ * :mixed - mixed file + directory search */ -import { FileFinder } from "../src/index"; -import type { DirItem, FileItem, MixedItem, Score } from "../src/index"; -import * as readline from "node:readline"; -import { join } from "node:path"; import { homedir } from "node:os"; +import { join } from "node:path"; +import * as readline from "node:readline"; +import type { DirItem, FileItem, MixedItem, Score } from "../src/index"; +import { FileFinder } from "../src/index"; const RESET = "\x1b[0m"; const BOLD = "\x1b[1m"; diff --git a/packages/fff-bun/examples/watch.ts b/packages/fff-bun/examples/watch.ts index b44ac234..3fa23dd3 100644 --- a/packages/fff-bun/examples/watch.ts +++ b/packages/fff-bun/examples/watch.ts @@ -1,6 +1,6 @@ #!/usr/bin/env bun -import { FileFinder } from "../src/index"; import type { WatchEvent } from "../src/index"; +import { FileFinder } from "../src/index"; const KIND = { created: "+ created ", diff --git a/packages/fff-bun/src/fff-api.ts b/packages/fff-bun/src/fff-api.ts index d22c6de5..2d0038aa 100644 --- a/packages/fff-bun/src/fff-api.ts +++ b/packages/fff-bun/src/fff-api.ts @@ -573,16 +573,10 @@ export interface FileFinderApi { glob(pattern: string, options?: GlobOptions): Result; /** Fuzzy directory search. */ - directorySearch( - query: string, - options?: DirSearchOptions, - ): Result; + directorySearch(query: string, options?: DirSearchOptions): Result; /** Fuzzy search over files and directories interleaved by score. */ - mixedSearch( - query: string, - options?: SearchOptions, - ): Result; + mixedSearch(query: string, options?: SearchOptions): Result; /** Content search (live grep). */ grep(query: string, options?: GrepOptions): Result; @@ -647,10 +641,7 @@ export interface FileFinderApi { * Events are debounced and submitted in batches per 100-ms window at most 128 events. * Gitignored and other ignored files are never triggering watcher. */ - watch( - callback: WatchBatchCallback, - options?: WatchOptions, - ): Result; + watch(callback: WatchBatchCallback, options?: WatchOptions): Result; watch( pattern: string, callback: WatchBatchCallback, diff --git a/packages/fff-bun/src/finder.ts b/packages/fff-bun/src/finder.ts index d8210b43..0565f516 100644 --- a/packages/fff-bun/src/finder.ts +++ b/packages/fff-bun/src/finder.ts @@ -9,7 +9,26 @@ */ import { FFIType, JSCallback, type Pointer } from "bun:ffi"; - +import type { + DirSearchOptions, + DirSearchResult, + InitOptions as FFFInitOptions, + FileFinderApi, + GlobOptions, + GrepOptions, + GrepResult, + HealthCheck, + MixedSearchResult, + MultiGrepOptions, + Result, + ScanProgress, + SearchOptions, + SearchResult, + WatchBatchCallback, + WatchOptions, + WatchUnsubscribe, +} from "./fff-api"; +import { err } from "./fff-api"; import { ensureLoaded, ffiCreate, @@ -38,28 +57,6 @@ import { readWatchEventBatch, } from "./ffi"; -import type { - DirSearchOptions, - DirSearchResult, - InitOptions as FFFInitOptions, - FileFinderApi, - GlobOptions, - GrepOptions, - GrepResult, - HealthCheck, - MixedSearchResult, - MultiGrepOptions, - Result, - ScanProgress, - SearchOptions, - SearchResult, - WatchBatchCallback, - WatchOptions, - WatchUnsubscribe, -} from "./fff-api"; - -import { err } from "./fff-api"; - /** * FileFinder - Fast file finder with fuzzy search * diff --git a/packages/fff-bun/test.ts b/packages/fff-bun/test.ts index de29fdb2..655717bb 100644 --- a/packages/fff-bun/test.ts +++ b/packages/fff-bun/test.ts @@ -1,5 +1,5 @@ +import { dirname, resolve } from "node:path"; import { FileFinder } from "./src/index"; -import { resolve, dirname } from "node:path"; async function main() { console.log("=== fff Test Script ===\n"); diff --git a/packages/fff-node/src/fff-api.ts b/packages/fff-node/src/fff-api.ts index d22c6de5..2d0038aa 100644 --- a/packages/fff-node/src/fff-api.ts +++ b/packages/fff-node/src/fff-api.ts @@ -573,16 +573,10 @@ export interface FileFinderApi { glob(pattern: string, options?: GlobOptions): Result; /** Fuzzy directory search. */ - directorySearch( - query: string, - options?: DirSearchOptions, - ): Result; + directorySearch(query: string, options?: DirSearchOptions): Result; /** Fuzzy search over files and directories interleaved by score. */ - mixedSearch( - query: string, - options?: SearchOptions, - ): Result; + mixedSearch(query: string, options?: SearchOptions): Result; /** Content search (live grep). */ grep(query: string, options?: GrepOptions): Result; @@ -647,10 +641,7 @@ export interface FileFinderApi { * Events are debounced and submitted in batches per 100-ms window at most 128 events. * Gitignored and other ignored files are never triggering watcher. */ - watch( - callback: WatchBatchCallback, - options?: WatchOptions, - ): Result; + watch(callback: WatchBatchCallback, options?: WatchOptions): Result; watch( pattern: string, callback: WatchBatchCallback, diff --git a/packages/fff-node/src/ffi.ts b/packages/fff-node/src/ffi.ts index e2315600..61080f42 100644 --- a/packages/fff-node/src/ffi.ts +++ b/packages/fff-node/src/ffi.ts @@ -246,11 +246,7 @@ function readResultEnvelope( paramsValue: unknown[], ): { rawPtr: JsExternal; struct: FffResultRaw } | Result { loadLibrary(); - const { rawPtr, struct: structData } = callRaw( - funcName, - paramsType, - paramsValue, - ); + const { rawPtr, struct: structData } = callRaw(funcName, paramsType, paramsValue); if (structData.success === 0) { const errorStr = readCString(structData.error); @@ -328,8 +324,7 @@ function callJsonResult( if (isNullPointer(handlePtr)) return { ok: true, value: undefined as T }; const jsonStr = readCString(handlePtr); freeString(handlePtr); - if (jsonStr === null || jsonStr === "") - return { ok: true, value: undefined as T }; + if (jsonStr === null || jsonStr === "") return { ok: true, value: undefined as T }; try { return { ok: true, value: snakeToCamel(JSON.parse(jsonStr)) as T }; } catch { @@ -849,16 +844,10 @@ function readGrepMatchFromRaw(raw: FffGrepMatchRaw): GrepMatch { match.fuzzyScore = raw.fuzzy_score; } if (raw.context_before_count > 0) { - match.contextBefore = readCStringArray( - raw.context_before, - raw.context_before_count, - ); + match.contextBefore = readCStringArray(raw.context_before, raw.context_before_count); } if (raw.context_after_count > 0) { - match.contextAfter = readCStringArray( - raw.context_after, - raw.context_after_count, - ); + match.contextAfter = readCStringArray(raw.context_after, raw.context_after_count); } if (raw.is_definition !== 0) { match.isDefinition = true; @@ -927,8 +916,7 @@ function parseGrepResult(rawPtr: JsExternal): Result { totalFilesSearched: gr.total_files_searched, totalFiles: gr.total_files, filteredFileCount: gr.filtered_file_count, - nextCursor: - gr.next_file_offset > 0 ? createGrepCursor(gr.next_file_offset) : null, + nextCursor: gr.next_file_offset > 0 ? createGrepCursor(gr.next_file_offset) : null, }; if (regexFallbackError) { grepResult.regexFallbackError = regexFallbackError; @@ -1280,14 +1268,7 @@ export function ffiGlob( DataType.U32, // page_index DataType.U32, // page_size ], - paramsValue: [ - handle, - pattern, - currentFile, - maxThreads, - pageIndex, - pageSize, - ], + paramsValue: [handle, pattern, currentFile, maxThreads, pageIndex, pageSize], freeResultMemory: false, }) as JsExternal; @@ -1319,14 +1300,7 @@ export function ffiSearchDirectories( DataType.U32, // page_index DataType.U32, // page_size ], - paramsValue: [ - handle, - query, - currentFile ?? "", - maxThreads, - pageIndex, - pageSize, - ], + paramsValue: [handle, query, currentFile ?? "", maxThreads, pageIndex, pageSize], freeResultMemory: false, }) as JsExternal; @@ -1545,11 +1519,7 @@ export function ffiGetScanProgress(handle: NativeHandle): Result<{ isWarmupComplete: boolean; }> { loadLibrary(); - const res = readResultEnvelope( - "fff_get_scan_progress", - [DataType.External], - [handle], - ); + const res = readResultEnvelope("fff_get_scan_progress", [DataType.External], [handle]); if ("ok" in res) return res; const handlePtr = res.struct.handle; @@ -1584,10 +1554,7 @@ export function ffiGetScanProgress(handle: NativeHandle): Result<{ /** * Wait for a tree scan to complete. */ -export function ffiWaitForScan( - handle: NativeHandle, - timeoutMs: number, -): Result { +export function ffiWaitForScan(handle: NativeHandle, timeoutMs: number): Result { return callBoolResult( "fff_wait_for_scan", [DataType.External, DataType.U64], @@ -1598,10 +1565,7 @@ export function ffiWaitForScan( /** * Restart index in new path. */ -export function ffiRestartIndex( - handle: NativeHandle, - newPath: string, -): Result { +export function ffiRestartIndex(handle: NativeHandle, newPath: string): Result { return callVoidResult( "fff_restart_index", [DataType.External, DataType.String], @@ -1772,8 +1736,7 @@ function ensureWatchTrampoline(): JsExternal { // fff watcher uses a single cross-boundary FFI callback to deliver all events which we then manually // mapping to the user's javascript functions function ensureWatchCallbackRegistered(handle: NativeHandle): Result { - if (watchInstances.has(handle as unknown)) - return { ok: true, value: undefined }; + if (watchInstances.has(handle as unknown)) return { ok: true, value: undefined }; const trampoline = ensureWatchTrampoline(); const registered = callVoidResult( "fff_set_watch_callback", @@ -1785,11 +1748,7 @@ function ensureWatchCallbackRegistered(handle: NativeHandle): Result { } function releaseWatchTrampolineIfIdle(): void { - if ( - watchHandlers.size > 0 || - watchInstances.size > 0 || - watchTrampoline === null - ) + if (watchHandlers.size > 0 || watchInstances.size > 0 || watchTrampoline === null) return; freePointer({ paramsType: [WATCH_TRAMPOLINE_TYPE], @@ -1835,10 +1794,7 @@ export function ffiWatch( * this returns the callback can never run again (a late native tail batch * misses the map lookup and is dropped). */ -export function ffiUnwatch( - handle: NativeHandle, - watchId: number, -): Result { +export function ffiUnwatch(handle: NativeHandle, watchId: number): Result { const result = callBoolResult( "fff_unwatch", [DataType.External, DataType.U64], diff --git a/packages/fff-node/src/finder.ts b/packages/fff-node/src/finder.ts index e80b4ef6..3b38e5a1 100644 --- a/packages/fff-node/src/finder.ts +++ b/packages/fff-node/src/finder.ts @@ -8,6 +8,26 @@ * All methods return Result types for explicit error handling. */ +import type { + DirSearchOptions, + DirSearchResult, + FileFinderApi, + GlobOptions, + GrepOptions, + GrepResult, + HealthCheck, + InitOptions, + MixedSearchResult, + MultiGrepOptions, + Result, + ScanProgress, + SearchOptions, + SearchResult, + WatchBatchCallback, + WatchOptions, + WatchUnsubscribe, +} from "./fff-api.js"; +import { err } from "./fff-api.js"; import { ensureLoaded, ffiCreate, @@ -35,28 +55,6 @@ import { type NativeHandle, } from "./ffi.js"; -import type { - DirSearchOptions, - DirSearchResult, - FileFinderApi, - GlobOptions, - GrepOptions, - GrepResult, - HealthCheck, - InitOptions, - MixedSearchResult, - MultiGrepOptions, - Result, - ScanProgress, - SearchOptions, - SearchResult, - WatchBatchCallback, - WatchOptions, - WatchUnsubscribe, -} from "./fff-api.js"; - -import { err } from "./fff-api.js"; - /** * FileFinder - Fast file finder with fuzzy search * diff --git a/packages/fff-node/src/index.ts b/packages/fff-node/src/index.ts index c1ac706f..e09c8a00 100644 --- a/packages/fff-node/src/index.ts +++ b/packages/fff-node/src/index.ts @@ -43,7 +43,6 @@ export { binaryExists, findBinary, } from "./binary.js"; -export { closeLibrary } from "./ffi.js"; export type { DbHealth, DirItem, @@ -75,6 +74,7 @@ export type { } from "./fff-api.js"; // Result helpers export { err, ok } from "./fff-api.js"; +export { closeLibrary } from "./ffi.js"; export { FileFinder } from "./finder.js"; export { getLibExtension, diff --git a/packages/pi-fff/src/aux-finders.ts b/packages/pi-fff/src/aux-finders.ts index 37bfe23c..c63ce7b6 100644 --- a/packages/pi-fff/src/aux-finders.ts +++ b/packages/pi-fff/src/aux-finders.ts @@ -60,8 +60,7 @@ export class AuxFinderPool { if (this.entries.length >= MAX_AUX) { let oldest = this.entries[0]; - for (const e of this.entries) - if (e.lastUsed < oldest.lastUsed) oldest = e; + for (const e of this.entries) if (e.lastUsed < oldest.lastUsed) oldest = e; if (!oldest.finder.isDestroyed) oldest.finder.destroy(); this.entries = this.entries.filter((e) => e !== oldest); } @@ -96,9 +95,7 @@ export class AuxFinderPool { // remainder usable as a fuzzy path constraint relative to that root. Glob and // nonexistent segments both go into the suffix: we walk up to the nearest // existing ancestor so partially-wrong paths still resolve to a search root. -export function resolveAuxRoot( - absPath: string, -): { root: string; suffix: string } | null { +export function resolveAuxRoot(absPath: string): { root: string; suffix: string } | null { const trimmed = path.normalize(absPath.trim()).replace(/\/+$/, "") || "/"; if (!path.isAbsolute(trimmed)) return null; if (trimmed === path.sep) return { root: path.sep, suffix: "" }; @@ -158,7 +155,6 @@ export function routePathConstraint( return resolveAuxRoot(candidate); } - export function rootCovers(root: string, target: string): boolean { if (root === target) return true; const prefix = root.endsWith(path.sep) ? root : root + path.sep; diff --git a/packages/pi-fff/src/index.ts b/packages/pi-fff/src/index.ts index 08b53401..f2d510d1 100644 --- a/packages/pi-fff/src/index.ts +++ b/packages/pi-fff/src/index.ts @@ -261,9 +261,7 @@ function createFffMentionProvider( const query = prefix.startsWith('@"') ? prefix.slice(2) : prefix.slice(1); const items = await getItems(query, options.signal); - return options.signal.aborted || items.length === 0 - ? null - : { items, prefix }; + return options.signal.aborted || items.length === 0 ? null : { items, prefix }; }, applyCompletion(_lines, cursorLine, cursorCol, item, prefix) { const currentLine = _lines[cursorLine] || ""; @@ -272,11 +270,7 @@ function createFffMentionProvider( const newLine = before + item.value + after; const newCursorCol = cursorCol - prefix.length + item.value.length; return { - lines: [ - ..._lines.slice(0, cursorLine), - newLine, - ..._lines.slice(cursorLine + 1), - ], + lines: [..._lines.slice(0, cursorLine), newLine, ..._lines.slice(cursorLine + 1)], cursorLine, cursorCol: newCursorCol, }; @@ -343,7 +337,7 @@ export default function fffExtension(pi: ExtensionAPI) { return currentMode !== "tools-only"; } - let auxPool = new AuxFinderPool({ + const auxPool = new AuxFinderPool({ enableFsRootScanning, }); @@ -407,9 +401,7 @@ export default function fffExtension(pi: ExtensionAPI) { const aux = await auxPool.acquire(route.root); // A broader covering picker may have been reused; rebase the suffix so the // constraint stays relative to the picker's actual root. - const rebase = nodePath - .relative(aux.root, route.root) - .replaceAll(nodePath.sep, "/"); + const rebase = nodePath.relative(aux.root, route.root).replaceAll(nodePath.sep, "/"); const suffix = [rebase, route.suffix].filter(Boolean).join("/"); const query = buildQuery(suffix || undefined, pattern, exclude, aux.root); return { finder: aux.finder, query, root: aux.root }; @@ -426,22 +418,20 @@ export default function fffExtension(pi: ExtensionAPI) { const result = f.mixedSearch(query, { pageSize: MENTION_MAX_RESULTS }); if (!result.ok) return []; - return result.value.items - .slice(0, MENTION_MAX_RESULTS) - .map((mixed: MixedItem) => { - if (mixed.type === "directory") { - return { - value: buildAtCompletionValue(mixed.item.relativePath), - label: mixed.item.dirName, - description: mixed.item.relativePath, - }; - } + return result.value.items.slice(0, MENTION_MAX_RESULTS).map((mixed: MixedItem) => { + if (mixed.type === "directory") { return { value: buildAtCompletionValue(mixed.item.relativePath), - label: mixed.item.fileName, + label: mixed.item.dirName, description: mixed.item.relativePath, }; - }); + } + return { + value: buildAtCompletionValue(mixed.item.relativePath), + label: mixed.item.fileName, + description: mixed.item.relativePath, + }; + }); } function registerAutocompleteProvider(ctx: { @@ -477,21 +467,11 @@ export default function fffExtension(pi: ExtensionAPI) { return current.getSuggestions(lines, cursorLine, cursorCol, options); }, applyCompletion(lines, cursorLine, cursorCol, item, prefix) { - return current.applyCompletion( - lines, - cursorLine, - cursorCol, - item, - prefix, - ); + return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix); }, shouldTriggerFileCompletion(lines, cursorLine, cursorCol) { return ( - current.shouldTriggerFileCompletion?.( - lines, - cursorLine, - cursorCol, - ) ?? true + current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true ); }, }; @@ -506,14 +486,12 @@ export default function fffExtension(pi: ExtensionAPI) { }); pi.registerFlag("fff-frecency-db", { - description: - "Path to the frecency database (overrides FFF_FRECENCY_DB env)", + description: "Path to the frecency database (overrides FFF_FRECENCY_DB env)", type: "string", }); pi.registerFlag("fff-history-db", { - description: - "Path to the query history database (overrides FFF_HISTORY_DB env)", + description: "Path to the query history database (overrides FFF_HISTORY_DB env)", type: "string", }); @@ -573,20 +551,15 @@ export default function fffExtension(pi: ExtensionAPI) { context: any, maxLines = 15, ) => { - const text = - (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); - const output = - result.content?.find((c) => c.type === "text")?.text?.trim() ?? ""; + const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); + const output = result.content?.find((c) => c.type === "text")?.text?.trim() ?? ""; if (!output) { text.setText(theme.fg("muted", "No output")); return text; } const lines = output.split("\n"); - const displayLines = lines.slice( - 0, - options.expanded ? lines.length : maxLines, - ); + const displayLines = lines.slice(0, options.expanded ? lines.length : maxLines); let content = `\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`; if (lines.length > displayLines.length) { content += theme.fg( @@ -652,11 +625,7 @@ export default function fffExtension(pi: ExtensionAPI) { if (signal?.aborted) throw new Error("Operation aborted"); const pattern = params.pattern; - const aux = await resolveFinderForPath( - params.path, - pattern, - params.exclude, - ); + const aux = await resolveFinderForPath(params.path, pattern, params.exclude); const picker = aux ? aux.finder : await ensureFinder(activeCwd); const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT); @@ -667,8 +636,7 @@ export default function fffExtension(pi: ExtensionAPI) { // Auto-detect: regex if the pattern has regex metacharacters AND parses // as a valid regex, otherwise plain literal. The fuzzy fallback below // only kicks in for plain mode — regex queries are intentional. - const hasRegexSyntax = - pattern !== pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const hasRegexSyntax = pattern !== pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); let mode: GrepMode = hasRegexSyntax ? "regex" : "plain"; if (mode === "regex") { @@ -749,14 +717,10 @@ export default function fffExtension(pi: ExtensionAPI) { let output = formatGrepOutput(result); const notices: string[] = []; if (result.regexFallbackError) { - notices.push( - `Invalid regex: ${result.regexFallbackError}, used literal match`, - ); + notices.push(`Invalid regex: ${result.regexFallbackError}, used literal match`); } if (result.nextCursor) { - notices.push( - `Continue with cursor="${storeCursor(result.nextCursor)}"`, - ); + notices.push(`Continue with cursor="${storeCursor(result.nextCursor)}"`); } if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`; @@ -772,8 +736,7 @@ export default function fffExtension(pi: ExtensionAPI) { }, renderCall(args, theme, context) { - const text = - (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); + const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); const pattern = args?.pattern ?? ""; const path = args?.path ?? "."; let content = @@ -845,16 +808,11 @@ export default function fffExtension(pi: ExtensionAPI) { const aux = resumed ? resumed.auxRoot ? { - finder: (await auxPool.acquire(resumed.auxRoot, { exact: true })) - .finder, + finder: (await auxPool.acquire(resumed.auxRoot, { exact: true })).finder, root: resumed.auxRoot, } : null - : await resolveFinderForPath( - params.path, - params.pattern, - params.exclude, - ); + : await resolveFinderForPath(params.path, params.pattern, params.exclude); const picker = aux ? aux.finder : await ensureFinder(activeCwd); const effectiveLimit = resumed @@ -886,8 +844,7 @@ export default function fffExtension(pi: ExtensionAPI) { // shown so far there's another page to fetch. const shownSoFar = pageIndex * effectiveLimit + result.items.length; const hasMore = - result.items.length >= effectiveLimit && - result.totalMatched > shownSoFar; + result.items.length >= effectiveLimit && result.totalMatched > shownSoFar; const notices: string[] = []; if (formatted.weak && formatted.shownCount > 0) @@ -922,8 +879,7 @@ export default function fffExtension(pi: ExtensionAPI) { }, renderCall(args, theme, context) { - const text = - (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); + const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); const pattern = args?.pattern ?? ""; const path = args?.path ?? "."; let content = @@ -956,9 +912,7 @@ export default function fffExtension(pi: ExtensionAPI) { constraints: Type.Optional( Type.String({ description: "File filter, e.g. '*.{ts,tsx} !test/'" }), ), - context: Type.Optional( - Type.Number({ description: "Context lines before+after" }), - ), + context: Type.Optional(Type.Number({ description: "Context lines before+after" })), limit: Type.Optional( Type.Number({ description: `Max matches (default ${DEFAULT_GREP_LIMIT})`, @@ -1024,8 +978,7 @@ export default function fffExtension(pi: ExtensionAPI) { }, renderCall(args, theme, context) { - const text = - (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); + const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); const patterns = args?.patterns ?? []; const constraints = args?.constraints; let content = @@ -1047,8 +1000,7 @@ export default function fffExtension(pi: ExtensionAPI) { // --- commands --- pi.registerCommand("fff-mode", { - description: - "Show or set FFF mode: /fff-mode [tools-and-ui | tools-only | override]", + description: "Show or set FFF mode: /fff-mode [tools-and-ui | tools-only | override]", handler: async (args, ctx) => { const arg = (args || "").trim(); @@ -1062,10 +1014,7 @@ export default function fffExtension(pi: ExtensionAPI) { // Validate and set mode if (!VALID_MODES.includes(arg as FffMode)) { - ctx.ui.notify( - `Usage: /fff-mode [${VALID_MODES.join(" | ")}]`, - "warning", - ); + ctx.ui.notify(`Usage: /fff-mode [${VALID_MODES.join(" | ")}]`, "warning"); return; } diff --git a/packages/pi-fff/src/query.ts b/packages/pi-fff/src/query.ts index 38ccb6dc..501d8333 100644 --- a/packages/pi-fff/src/query.ts +++ b/packages/pi-fff/src/query.ts @@ -10,11 +10,7 @@ export function normalizePathConstraint( if (path.isAbsolute(trimmed)) { const relative = path.relative(cwd, trimmed).replaceAll(path.sep, "/"); if (relative === "") return null; - if ( - relative.startsWith("../") || - relative === ".." || - path.isAbsolute(relative) - ) { + if (relative.startsWith("../") || relative === ".." || path.isAbsolute(relative)) { throw new Error( `Path constraint must be relative to the workspace: ${pathConstraint}`, ); diff --git a/packages/pi-fff/test/aux-finders.test.ts b/packages/pi-fff/test/aux-finders.test.ts index 7237aa03..53263ca5 100644 --- a/packages/pi-fff/test/aux-finders.test.ts +++ b/packages/pi-fff/test/aux-finders.test.ts @@ -98,9 +98,7 @@ describe("routePathConstraint", () => { }); test("returns null when .. resolves back inside the workspace", () => { - expect( - routePathConstraint("../workspace/src", workspace), - ).toBeNull(); + expect(routePathConstraint("../workspace/src", workspace)).toBeNull(); }); }); }); diff --git a/packages/shared/fff-api.ts b/packages/shared/fff-api.ts index 01b2201d..dcd9f2ac 100644 --- a/packages/shared/fff-api.ts +++ b/packages/shared/fff-api.ts @@ -567,16 +567,10 @@ export interface FileFinderApi { glob(pattern: string, options?: GlobOptions): Result; /** Fuzzy directory search. */ - directorySearch( - query: string, - options?: DirSearchOptions, - ): Result; + directorySearch(query: string, options?: DirSearchOptions): Result; /** Fuzzy search over files and directories interleaved by score. */ - mixedSearch( - query: string, - options?: SearchOptions, - ): Result; + mixedSearch(query: string, options?: SearchOptions): Result; /** Content search (live grep). */ grep(query: string, options?: GrepOptions): Result; @@ -641,10 +635,7 @@ export interface FileFinderApi { * Events are debounced and submitted in batches per 100-ms window at most 128 events. * Gitignored and other ignored files are never triggering watcher. */ - watch( - callback: WatchBatchCallback, - options?: WatchOptions, - ): Result; + watch(callback: WatchBatchCallback, options?: WatchOptions): Result; watch( pattern: string, callback: WatchBatchCallback,