| 24 | } |
| 25 | |
| 26 | export class FastFinder implements AsyncIterable<string> { |
| 27 | private keepOnlyExecutables: boolean; |
| 28 | private executableExtensions = new Array<string>(); |
| 29 | private processes = new Array<Instance<Process>>(); |
| 30 | private pending = 0; |
| 31 | private readyToComplete = false; |
| 32 | private distinct = new Set<string>(); |
| 33 | |
| 34 | #files = accumulator<string>().autoComplete(false); |
| 35 | |
| 36 | [Symbol.asyncIterator](): AsyncIterator<string> { |
| 37 | this.readyToComplete = true; |
| 38 | if (this.pending === 0) { |
| 39 | this.#files.complete(); |
| 40 | } |
| 41 | |
| 42 | return this.#files[Symbol.asyncIterator](); |
| 43 | } |
| 44 | |
| 45 | constructor(private fileGlobs: string[], options?: { executable?: boolean; executableExtensions?: string[] }) { |
| 46 | strict(ripgrep, 'initRipGrep must be called before using FastFinder'); |
| 47 | |
| 48 | this.keepOnlyExecutables = options?.executable ?? false; |
| 49 | if (this.keepOnlyExecutables && process.platform === 'win32') { |
| 50 | this.executableExtensions = options?.executableExtensions ?? ['.exe', '.bat', '.cmd', '.ps1']; |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | /** |
| 55 | * Add one or more locations to scan, with an optionally specified depth. |
| 56 | * |
| 57 | * The scanning of those locations begins immediately and is done asynchronously. |
| 58 | * |
| 59 | */ |
| 60 | scan(...location: string[]): FastFinder; |
| 61 | scan(depth: number, ...location: string[]): FastFinder; |
| 62 | scan(...location: (string | number)[]): FastFinder { |
| 63 | const depth = (typeof location[0] === 'number' ? location.shift() as number : 0) + 1; |
| 64 | const globs = this.executableExtensions.length ? |
| 65 | this.fileGlobs.map(glob => this.executableExtensions.map(ext => glob.includes('**') ? glob : `**/${glob}${ext}`)).flat() : |
| 66 | this.fileGlobs.map(glob => glob.includes('**') ? glob : `**/${glob}`); |
| 67 | |
| 68 | // only search locations that exist |
| 69 | location = location.filter(each => existsSync(each.toString())); |
| 70 | |
| 71 | // only search if there are globs and locations to search |
| 72 | if (globs.length && location.length) { |
| 73 | this.pending++; |
| 74 | void ripgrep!(...globs.map(each => ['--glob', each]).flat(), '--max-depth', depth, '--null-data', '--no-messages', '-L', '--files', ...location.map(each => each.toString())).then(async proc => { |
| 75 | const process = proc as unknown as Instance<Process>; |
| 76 | this.processes.push(process); |
| 77 | for await (const line of process.stdio) { |
| 78 | if (this.distinct.has(line)) { |
| 79 | continue; |
| 80 | } |
| 81 | this.distinct.add(line); |
| 82 | if (!this.keepOnlyExecutables || await filepath.isExecutable(line)) { |
| 83 | this.#files.add(line); |
nothing calls this directly
no test coverage detected