@ff-labs/fff-bun 0.1.0-nightly.044314f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +283 -0
- package/examples/grep.ts +283 -0
- package/examples/search.ts +222 -0
- package/package.json +86 -0
- package/scripts/cli.ts +114 -0
- package/scripts/postinstall.ts +51 -0
- package/src/download.ts +230 -0
- package/src/ffi.ts +374 -0
- package/src/finder.ts +380 -0
- package/src/index.test.ts +230 -0
- package/src/index.ts +74 -0
- package/src/platform.ts +121 -0
- package/src/types.ts +430 -0
package/README.md
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# fff - Fast File Finder
|
|
2
|
+
|
|
3
|
+
High-performance fuzzy file finder for Bun, powered by Rust. Perfect for LLM agent tools that need to search through codebases.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Blazing fast** - Rust-powered fuzzy search with parallel processing
|
|
8
|
+
- **Smart ranking** - Frecency-based scoring (frequency + recency)
|
|
9
|
+
- **Git-aware** - Shows file git status in results
|
|
10
|
+
- **Query history** - Learns from your search patterns
|
|
11
|
+
- **Type-safe** - Full TypeScript support with Result types
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @ff-labs/bun
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The correct native binary for your platform is installed automatically via platform-specific packages (e.g. `@ff-labs/fff-bun-darwin-arm64`, `@ff-labs/fff-bun-linux-x64-gnu`). No GitHub downloads are needed.
|
|
20
|
+
|
|
21
|
+
### Supported Platforms
|
|
22
|
+
|
|
23
|
+
| Platform | Architecture | Package |
|
|
24
|
+
|----------|-------------|---------|
|
|
25
|
+
| macOS | ARM64 (Apple Silicon) | `@ff-labs/fff-bun-darwin-arm64` |
|
|
26
|
+
| macOS | x64 (Intel) | `@ff-labs/fff-bun-darwin-x64` |
|
|
27
|
+
| Linux | x64 (glibc) | `@ff-labs/fff-bun-linux-x64-gnu` |
|
|
28
|
+
| Linux | ARM64 (glibc) | `@ff-labs/fff-bun-linux-arm64-gnu` |
|
|
29
|
+
| Linux | x64 (musl) | `@ff-labs/fff-bun-linux-x64-musl` |
|
|
30
|
+
| Linux | ARM64 (musl) | `@ff-labs/fff-bun-linux-arm64-musl` |
|
|
31
|
+
| Windows | x64 | `@ff-labs/fff-bun-win32-x64` |
|
|
32
|
+
| Windows | ARM64 | `@ff-labs/fff-bun-win32-arm64` |
|
|
33
|
+
|
|
34
|
+
If the platform package isn't available, the postinstall script will attempt to download from GitHub releases as a fallback.
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { FileFinder } from "fff";
|
|
40
|
+
|
|
41
|
+
// Initialize with a directory
|
|
42
|
+
const result = FileFinder.init({ basePath: "/path/to/project" });
|
|
43
|
+
if (!result.ok) {
|
|
44
|
+
console.error(result.error);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Wait for initial scan
|
|
49
|
+
FileFinder.waitForScan(5000);
|
|
50
|
+
|
|
51
|
+
// Search for files
|
|
52
|
+
const search = FileFinder.search("main.ts");
|
|
53
|
+
if (search.ok) {
|
|
54
|
+
for (const item of search.value.items) {
|
|
55
|
+
console.log(item.relativePath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Cleanup when done
|
|
60
|
+
FileFinder.destroy();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API Reference
|
|
64
|
+
|
|
65
|
+
### `FileFinder.init(options)`
|
|
66
|
+
|
|
67
|
+
Initialize the file finder.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
interface InitOptions {
|
|
71
|
+
basePath: string; // Directory to index (required)
|
|
72
|
+
frecencyDbPath?: string; // Frecency DB path (omit to skip frecency)
|
|
73
|
+
historyDbPath?: string; // History DB path (omit to skip query tracking)
|
|
74
|
+
useUnsafeNoLock?: boolean; // Faster but less safe DB mode
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = FileFinder.init({ basePath: "/my/project" });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `FileFinder.search(query, options?)`
|
|
81
|
+
|
|
82
|
+
Search for files.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
interface SearchOptions {
|
|
86
|
+
maxThreads?: number; // Parallel threads (0 = auto)
|
|
87
|
+
currentFile?: string; // Deprioritize this file
|
|
88
|
+
comboBoostMultiplier?: number; // Query history boost
|
|
89
|
+
minComboCount?: number; // Min history matches
|
|
90
|
+
pageIndex?: number; // Pagination offset
|
|
91
|
+
pageSize?: number; // Results per page
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const result = FileFinder.search("main.ts", { pageSize: 10 });
|
|
95
|
+
if (result.ok) {
|
|
96
|
+
console.log(`Found ${result.value.totalMatched} files`);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Query Syntax
|
|
101
|
+
|
|
102
|
+
- `foo bar` - Match files containing "foo" and "bar"
|
|
103
|
+
- `src/` - Match files in src directory
|
|
104
|
+
- `file.ts:42` - Match file.ts with line 42
|
|
105
|
+
- `file.ts:42:10` - Match with line and column
|
|
106
|
+
|
|
107
|
+
### `FileFinder.trackAccess(filePath)`
|
|
108
|
+
|
|
109
|
+
Track file access for frecency scoring.
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// Call when user opens a file
|
|
113
|
+
FileFinder.trackAccess("/path/to/file.ts");
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `FileFinder.liveGrep(query, options?)`
|
|
117
|
+
|
|
118
|
+
Search file contents with SIMD-accelerated matching.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
interface GrepOptions {
|
|
122
|
+
maxFileSize?: number; // Max file size in bytes (default: 10MB)
|
|
123
|
+
maxMatchesPerFile?: number; // Max matches per file (default: 200)
|
|
124
|
+
smartCase?: boolean; // Case-insensitive if all lowercase (default: true)
|
|
125
|
+
fileOffset?: number; // Pagination offset (default: 0)
|
|
126
|
+
pageLimit?: number; // Max matches to return (default: 50)
|
|
127
|
+
mode?: "plain" | "regex" | "fuzzy"; // Search mode (default: "plain")
|
|
128
|
+
timeBudgetMs?: number; // Time limit in ms, 0 = unlimited (default: 0)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Plain text search
|
|
132
|
+
const result = FileFinder.liveGrep("TODO", { pageLimit: 20 });
|
|
133
|
+
if (result.ok) {
|
|
134
|
+
for (const match of result.value.items) {
|
|
135
|
+
console.log(`${match.relativePath}:${match.lineNumber}: ${match.lineContent}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Regex search
|
|
140
|
+
const regexResult = FileFinder.liveGrep("fn\\s+\\w+", { mode: "regex" });
|
|
141
|
+
|
|
142
|
+
// Fuzzy search
|
|
143
|
+
const fuzzyResult = FileFinder.liveGrep("imprt recat", { mode: "fuzzy" });
|
|
144
|
+
|
|
145
|
+
// Pagination
|
|
146
|
+
const page1 = FileFinder.liveGrep("error");
|
|
147
|
+
if (page1.ok && page1.value.nextCursor) {
|
|
148
|
+
const page2 = FileFinder.liveGrep("error", {
|
|
149
|
+
cursor: page1.value.nextCursor,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// With file constraints
|
|
154
|
+
const tsOnly = FileFinder.liveGrep("*.ts useState");
|
|
155
|
+
const srcOnly = FileFinder.liveGrep("src/ handleClick");
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `FileFinder.trackQuery(query, selectedFile)`
|
|
159
|
+
|
|
160
|
+
Track query completion for smart suggestions.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// Call when user selects a file from search
|
|
164
|
+
FileFinder.trackQuery("main", "/path/to/main.ts");
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### `FileFinder.healthCheck(testPath?)`
|
|
168
|
+
|
|
169
|
+
Get diagnostic information.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
const health = FileFinder.healthCheck();
|
|
173
|
+
if (health.ok) {
|
|
174
|
+
console.log(`Version: ${health.value.version}`);
|
|
175
|
+
console.log(`Indexed: ${health.value.filePicker.indexedFiles} files`);
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Other Methods
|
|
180
|
+
|
|
181
|
+
- `FileFinder.liveGrep(query, options?)` - Search file contents
|
|
182
|
+
- `FileFinder.scanFiles()` - Trigger rescan
|
|
183
|
+
- `FileFinder.isScanning()` - Check scan status
|
|
184
|
+
- `FileFinder.getScanProgress()` - Get scan progress
|
|
185
|
+
- `FileFinder.waitForScan(timeoutMs)` - Wait for scan
|
|
186
|
+
- `FileFinder.reindex(newPath)` - Change indexed directory
|
|
187
|
+
- `FileFinder.refreshGitStatus()` - Refresh git cache
|
|
188
|
+
- `FileFinder.getHistoricalQuery(offset)` - Get past queries
|
|
189
|
+
- `FileFinder.destroy()` - Cleanup resources
|
|
190
|
+
|
|
191
|
+
## Result Types
|
|
192
|
+
|
|
193
|
+
All methods return a `Result<T>` type for explicit error handling:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
type Result<T> =
|
|
197
|
+
| { ok: true; value: T }
|
|
198
|
+
| { ok: false; error: string };
|
|
199
|
+
|
|
200
|
+
const result = FileFinder.search("foo");
|
|
201
|
+
if (result.ok) {
|
|
202
|
+
// result.value is SearchResult
|
|
203
|
+
} else {
|
|
204
|
+
// result.error is string
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Search Result Types
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
interface SearchResult {
|
|
212
|
+
items: FileItem[];
|
|
213
|
+
scores: Score[];
|
|
214
|
+
totalMatched: number;
|
|
215
|
+
totalFiles: number;
|
|
216
|
+
location?: Location;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
interface FileItem {
|
|
220
|
+
path: string;
|
|
221
|
+
relativePath: string;
|
|
222
|
+
fileName: string;
|
|
223
|
+
size: number;
|
|
224
|
+
modified: number;
|
|
225
|
+
gitStatus: string; // 'clean', 'modified', 'untracked', etc.
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Grep Result Types
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
interface GrepResult {
|
|
233
|
+
items: GrepMatch[];
|
|
234
|
+
totalMatched: number;
|
|
235
|
+
totalFilesSearched: number;
|
|
236
|
+
totalFiles: number;
|
|
237
|
+
filteredFileCount: number;
|
|
238
|
+
nextCursor: GrepCursor | null; // Pass to options.cursor for next page
|
|
239
|
+
regexFallbackError?: string; // Set if regex was invalid
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface GrepMatch {
|
|
243
|
+
path: string;
|
|
244
|
+
relativePath: string;
|
|
245
|
+
fileName: string;
|
|
246
|
+
gitStatus: string;
|
|
247
|
+
lineNumber: number; // 1-based
|
|
248
|
+
col: number; // 0-based byte column
|
|
249
|
+
byteOffset: number; // Absolute byte offset in file
|
|
250
|
+
lineContent: string; // The matched line text
|
|
251
|
+
matchRanges: [number, number][]; // Byte offsets for highlighting
|
|
252
|
+
fuzzyScore?: number; // Only in fuzzy mode
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Building from Source
|
|
257
|
+
|
|
258
|
+
If prebuilt binaries aren't available for your platform:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
# Clone the repository
|
|
262
|
+
git clone https://github.com/dmtrKovalenko/fff.nvim
|
|
263
|
+
cd fff.nvim
|
|
264
|
+
|
|
265
|
+
# Build the C library
|
|
266
|
+
cargo build --release -p fff-c
|
|
267
|
+
|
|
268
|
+
# The binary will be at target/release/libfff_c.{so,dylib,dll}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## CLI Tools
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
# Download binary manually (fallback if npm package unavailable)
|
|
275
|
+
bunx fff download [tag]
|
|
276
|
+
|
|
277
|
+
# Show platform info and binary location
|
|
278
|
+
bunx fff info
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## License
|
|
282
|
+
|
|
283
|
+
MIT
|
package/examples/grep.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Interactive live grep demo
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun examples/grep.ts [directory] [--mode=plain|regex|fuzzy]
|
|
7
|
+
*
|
|
8
|
+
* Indexes the specified directory (or cwd) and provides an interactive
|
|
9
|
+
* content search prompt with match highlighting.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { FileFinder } from "../src/index";
|
|
13
|
+
import type { GrepMode } from "../src/types";
|
|
14
|
+
import * as readline from "readline";
|
|
15
|
+
|
|
16
|
+
const RESET = "\x1b[0m";
|
|
17
|
+
const BOLD = "\x1b[1m";
|
|
18
|
+
const DIM = "\x1b[2m";
|
|
19
|
+
const GREEN = "\x1b[32m";
|
|
20
|
+
const YELLOW = "\x1b[33m";
|
|
21
|
+
const BLUE = "\x1b[34m";
|
|
22
|
+
const CYAN = "\x1b[36m";
|
|
23
|
+
const RED = "\x1b[31m";
|
|
24
|
+
const BG_YELLOW = "\x1b[43m";
|
|
25
|
+
const BLACK = "\x1b[30m";
|
|
26
|
+
|
|
27
|
+
function formatGitStatus(status: string): string {
|
|
28
|
+
switch (status) {
|
|
29
|
+
case "modified":
|
|
30
|
+
return `${YELLOW}M${RESET}`;
|
|
31
|
+
case "untracked":
|
|
32
|
+
return `${GREEN}?${RESET}`;
|
|
33
|
+
case "added":
|
|
34
|
+
return `${GREEN}A${RESET}`;
|
|
35
|
+
case "deleted":
|
|
36
|
+
return `${RED}D${RESET}`;
|
|
37
|
+
case "renamed":
|
|
38
|
+
return `${BLUE}R${RESET}`;
|
|
39
|
+
case "clear":
|
|
40
|
+
case "current":
|
|
41
|
+
return `${DIM} ${RESET}`;
|
|
42
|
+
default:
|
|
43
|
+
return `${DIM}${status.charAt(0)}${RESET}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Highlight match ranges within line content using ANSI escape codes.
|
|
49
|
+
* The match_ranges are byte offsets into line_content.
|
|
50
|
+
*/
|
|
51
|
+
function highlightLine(content: string, ranges: [number, number][]): string {
|
|
52
|
+
if (ranges.length === 0) return content;
|
|
53
|
+
|
|
54
|
+
// Convert the string to a buffer to work with byte offsets
|
|
55
|
+
const buf = Buffer.from(content, "utf-8");
|
|
56
|
+
const parts: string[] = [];
|
|
57
|
+
let lastEnd = 0;
|
|
58
|
+
|
|
59
|
+
for (const [start, end] of ranges) {
|
|
60
|
+
// Clamp to valid range
|
|
61
|
+
const s = Math.max(0, Math.min(start, buf.length));
|
|
62
|
+
const e = Math.max(s, Math.min(end, buf.length));
|
|
63
|
+
|
|
64
|
+
if (s > lastEnd) {
|
|
65
|
+
parts.push(buf.subarray(lastEnd, s).toString("utf-8"));
|
|
66
|
+
}
|
|
67
|
+
parts.push(
|
|
68
|
+
`${BG_YELLOW}${BLACK}${buf.subarray(s, e).toString("utf-8")}${RESET}`
|
|
69
|
+
);
|
|
70
|
+
lastEnd = e;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (lastEnd < buf.length) {
|
|
74
|
+
parts.push(buf.subarray(lastEnd).toString("utf-8"));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return parts.join("");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseArgs(): { directory: string; mode: GrepMode } {
|
|
81
|
+
let directory = process.cwd();
|
|
82
|
+
let mode: GrepMode = "plain";
|
|
83
|
+
|
|
84
|
+
for (const arg of process.argv.slice(2)) {
|
|
85
|
+
if (arg.startsWith("--mode=")) {
|
|
86
|
+
const m = arg.slice(7);
|
|
87
|
+
if (m === "plain" || m === "regex" || m === "fuzzy") {
|
|
88
|
+
mode = m;
|
|
89
|
+
} else {
|
|
90
|
+
console.error(`Unknown mode: ${m}. Use plain, regex, or fuzzy.`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
} else if (!arg.startsWith("-")) {
|
|
94
|
+
directory = arg;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { directory, mode };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function main() {
|
|
102
|
+
const { directory, mode } = parseArgs();
|
|
103
|
+
|
|
104
|
+
console.log(`${BOLD}${CYAN}fff - Live Grep Demo${RESET}`);
|
|
105
|
+
console.log(`${DIM}Mode: ${mode}${RESET}\n`);
|
|
106
|
+
|
|
107
|
+
if (!FileFinder.isAvailable()) {
|
|
108
|
+
console.error(`${RED}Error: Native library not found.${RESET}`);
|
|
109
|
+
console.error("Build with: cargo build --release -p fff-c");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(`${DIM}Initializing index for: ${directory}${RESET}`);
|
|
114
|
+
const initResult = FileFinder.init({
|
|
115
|
+
basePath: directory,
|
|
116
|
+
warmupMmapCache: true,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (!initResult.ok) {
|
|
120
|
+
console.error(`${RED}Init failed: ${initResult.error}${RESET}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Wait for scan
|
|
125
|
+
process.stdout.write(`${DIM}Scanning files...${RESET}`);
|
|
126
|
+
const startTime = Date.now();
|
|
127
|
+
|
|
128
|
+
while (FileFinder.isScanning()) {
|
|
129
|
+
const progress = FileFinder.getScanProgress();
|
|
130
|
+
if (progress.ok) {
|
|
131
|
+
process.stdout.write(
|
|
132
|
+
`\r${DIM}Scanning files... ${progress.value.scannedFilesCount}${RESET} `
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const scanTime = Date.now() - startTime;
|
|
139
|
+
const finalProgress = FileFinder.getScanProgress();
|
|
140
|
+
const totalFiles = finalProgress.ok
|
|
141
|
+
? finalProgress.value.scannedFilesCount
|
|
142
|
+
: 0;
|
|
143
|
+
|
|
144
|
+
console.log(
|
|
145
|
+
`\r${GREEN}✓${RESET} Indexed ${BOLD}${totalFiles}${RESET} files in ${scanTime}ms\n`
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
console.log(
|
|
149
|
+
`${BOLD}Enter a search pattern${RESET} (or 'q' to quit, ':mode plain|regex|fuzzy' to switch):\n`
|
|
150
|
+
);
|
|
151
|
+
console.log(
|
|
152
|
+
`${DIM}Tip: prefix with *.ext to filter by extension, e.g. "*.ts useState"${RESET}\n`
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
let currentMode = mode;
|
|
156
|
+
|
|
157
|
+
const rl = readline.createInterface({
|
|
158
|
+
input: process.stdin,
|
|
159
|
+
output: process.stdout,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const prompt = () => {
|
|
163
|
+
const modeLabel =
|
|
164
|
+
currentMode === "plain" ? "txt" : currentMode === "regex" ? "re" : "fzy";
|
|
165
|
+
|
|
166
|
+
rl.question(`${CYAN}grep[${modeLabel}]>${RESET} `, (query) => {
|
|
167
|
+
if (query.toLowerCase() === "q" || query.toLowerCase() === "quit") {
|
|
168
|
+
console.log(`\n${DIM}Goodbye!${RESET}`);
|
|
169
|
+
FileFinder.destroy();
|
|
170
|
+
rl.close();
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Handle mode switching
|
|
175
|
+
if (query.startsWith(":mode ")) {
|
|
176
|
+
const newMode = query.slice(6).trim();
|
|
177
|
+
if (
|
|
178
|
+
newMode === "plain" ||
|
|
179
|
+
newMode === "regex" ||
|
|
180
|
+
newMode === "fuzzy"
|
|
181
|
+
) {
|
|
182
|
+
currentMode = newMode;
|
|
183
|
+
console.log(`${DIM}Switched to ${currentMode} mode${RESET}\n`);
|
|
184
|
+
} else {
|
|
185
|
+
console.log(
|
|
186
|
+
`${RED}Unknown mode: ${newMode}. Use plain, regex, or fuzzy.${RESET}\n`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
prompt();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (query.trim() === "") {
|
|
194
|
+
prompt();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const searchStart = Date.now();
|
|
199
|
+
const result = FileFinder.liveGrep(query, {
|
|
200
|
+
mode: currentMode,
|
|
201
|
+
pageLimit: 30,
|
|
202
|
+
timeBudgetMs: 5000,
|
|
203
|
+
});
|
|
204
|
+
const searchTime = Date.now() - searchStart;
|
|
205
|
+
|
|
206
|
+
if (!result.ok) {
|
|
207
|
+
console.log(`${RED}Grep error: ${result.error}${RESET}\n`);
|
|
208
|
+
prompt();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const {
|
|
213
|
+
items,
|
|
214
|
+
totalMatched,
|
|
215
|
+
totalFilesSearched,
|
|
216
|
+
totalFiles: indexedFiles,
|
|
217
|
+
filteredFileCount,
|
|
218
|
+
nextCursor,
|
|
219
|
+
regexFallbackError,
|
|
220
|
+
} = result.value;
|
|
221
|
+
|
|
222
|
+
console.log();
|
|
223
|
+
|
|
224
|
+
if (regexFallbackError) {
|
|
225
|
+
console.log(
|
|
226
|
+
`${YELLOW}Regex error: ${regexFallbackError} (fell back to literal match)${RESET}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log(
|
|
231
|
+
`${DIM}${BOLD}${totalMatched}${RESET}${DIM} matches across ${totalFilesSearched}/${filteredFileCount} files (${searchTime}ms)${RESET}`
|
|
232
|
+
);
|
|
233
|
+
console.log();
|
|
234
|
+
|
|
235
|
+
if (items.length === 0) {
|
|
236
|
+
console.log(`${DIM}No matches found.${RESET}\n`);
|
|
237
|
+
prompt();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Group matches by file for display
|
|
242
|
+
let lastFile = "";
|
|
243
|
+
for (const match of items) {
|
|
244
|
+
if (match.relativePath !== lastFile) {
|
|
245
|
+
lastFile = match.relativePath;
|
|
246
|
+
const git = formatGitStatus(match.gitStatus);
|
|
247
|
+
console.log(
|
|
248
|
+
`${BOLD}${BLUE}${match.relativePath}${RESET} ${git}`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const lineNum = String(match.lineNumber).padStart(4);
|
|
253
|
+
const highlighted = highlightLine(
|
|
254
|
+
match.lineContent,
|
|
255
|
+
match.matchRanges
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
let suffix = "";
|
|
259
|
+
if (match.fuzzyScore !== undefined) {
|
|
260
|
+
suffix = ` ${DIM}(score: ${match.fuzzyScore})${RESET}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
console.log(`${DIM}${lineNum}:${RESET} ${highlighted}${suffix}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (nextCursor) {
|
|
267
|
+
console.log(
|
|
268
|
+
`\n${DIM}... more results available${RESET}`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log();
|
|
273
|
+
prompt();
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
prompt();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
main().catch((err) => {
|
|
281
|
+
console.error(`${RED}Fatal error: ${err.message}${RESET}`);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
});
|