(cwd: string, opts: LineSearchOptions)
| 121 | |
| 122 | /** Pure-JS line search. Exported for direct testing. Emits absolute `file:line:content`. */ |
| 123 | export async function jsLineSearch(cwd: string, opts: LineSearchOptions): Promise<string> { |
| 124 | const exts = opts.language ? (TYPE_EXT[opts.language.toLowerCase()] ?? null) : null; |
| 125 | // Use a non-global clone so `.test()` doesn't advance lastIndex across lines. |
| 126 | const re = new RegExp(opts.regex.source, opts.regex.flags.replace(/g/g, '')); |
| 127 | const out: string[] = []; |
| 128 | let count = 0; |
| 129 | |
| 130 | async function walk(dir: string): Promise<void> { |
| 131 | if (count >= opts.maxCount || opts.signal?.aborted) return; |
| 132 | let entries: any[]; |
| 133 | try { |
| 134 | entries = await fs.readdir(dir, { withFileTypes: true }); |
| 135 | } catch { return; } |
| 136 | |
| 137 | for (const entry of entries) { |
| 138 | if (count >= opts.maxCount || opts.signal?.aborted) return; |
| 139 | if (entry.name.startsWith('.') && entry.name !== '.env') continue; |
| 140 | const full = path.join(dir, entry.name); |
| 141 | if (entry.isDirectory()) { |
| 142 | if (IGNORED_DIRS.has(entry.name)) continue; |
| 143 | await walk(full); |
| 144 | } else if (entry.isFile()) { |
| 145 | if (exts && !exts.includes(path.extname(full).toLowerCase())) continue; |
| 146 | let buf: Buffer; |
| 147 | try { |
| 148 | const st = await fs.stat(full); |
| 149 | if (st.size > MAX_FILE_SIZE) continue; |
| 150 | buf = await fs.readFile(full); |
| 151 | } catch { continue; } |
| 152 | if (isBinaryBuffer(buf)) continue; |
| 153 | const lines = buf.toString('utf-8').split('\n'); |
| 154 | for (let i = 0; i < lines.length; i++) { |
| 155 | if (re.test(lines[i]!)) { |
| 156 | out.push(`${full}:${i + 1}:${lines[i]}`); |
| 157 | count++; |
| 158 | if (count >= opts.maxCount) break; |
| 159 | } |
| 160 | } |
| 161 | } |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | // Allow searching a single file path too (matches ripgrep's behaviour). |
| 166 | try { |
| 167 | const st = await fs.stat(cwd); |
| 168 | if (st.isFile()) { |
| 169 | const buf = await fs.readFile(cwd); |
| 170 | if (!isBinaryBuffer(buf)) { |
| 171 | const lines = buf.toString('utf-8').split('\n'); |
| 172 | for (let i = 0; i < lines.length && count < opts.maxCount; i++) { |
| 173 | if (re.test(lines[i]!)) { out.push(`${cwd}:${i + 1}:${lines[i]}`); count++; } |
| 174 | } |
| 175 | } |
| 176 | return out.join('\n'); |
| 177 | } |
| 178 | } catch { return ''; } |
| 179 | |
| 180 | await walk(cwd); |
no test coverage detected