summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/cli/src/ui/hooks/useAtCompletion.test.ts6
-rw-r--r--packages/cli/src/ui/hooks/useAtCompletion.ts2
-rw-r--r--packages/core/src/utils/filesearch/fileSearch.ts49
3 files changed, 33 insertions, 24 deletions
diff --git a/packages/cli/src/ui/hooks/useAtCompletion.test.ts b/packages/cli/src/ui/hooks/useAtCompletion.test.ts
index aa198fc1..599f8fdf 100644
--- a/packages/cli/src/ui/hooks/useAtCompletion.test.ts
+++ b/packages/cli/src/ui/hooks/useAtCompletion.test.ts
@@ -157,7 +157,7 @@ describe('useAtCompletion', () => {
});
});
- it('should NOT show a loading indicator for subsequent searches that complete under 100ms', async () => {
+ it('should NOT show a loading indicator for subsequent searches that complete under 200ms', async () => {
const structure: FileSystemStructure = { 'a.txt': '', 'b.txt': '' };
testRootDir = await createTmpDir(structure);
@@ -186,7 +186,7 @@ describe('useAtCompletion', () => {
expect(result.current.isLoadingSuggestions).toBe(false);
});
- it('should show a loading indicator and clear old suggestions for subsequent searches that take longer than 100ms', async () => {
+ it('should show a loading indicator and clear old suggestions for subsequent searches that take longer than 200ms', async () => {
const structure: FileSystemStructure = { 'a.txt': '', 'b.txt': '' };
testRootDir = await createTmpDir(structure);
@@ -194,7 +194,7 @@ describe('useAtCompletion', () => {
const originalSearch = FileSearch.prototype.search;
vi.spyOn(FileSearch.prototype, 'search').mockImplementation(
async function (...args) {
- await new Promise((resolve) => setTimeout(resolve, 200));
+ await new Promise((resolve) => setTimeout(resolve, 300));
return originalSearch.apply(this, args);
},
);
diff --git a/packages/cli/src/ui/hooks/useAtCompletion.ts b/packages/cli/src/ui/hooks/useAtCompletion.ts
index 82439c14..f6835dc8 100644
--- a/packages/cli/src/ui/hooks/useAtCompletion.ts
+++ b/packages/cli/src/ui/hooks/useAtCompletion.ts
@@ -194,7 +194,7 @@ export function useAtCompletion(props: UseAtCompletionProps): void {
slowSearchTimer.current = setTimeout(() => {
dispatch({ type: 'SET_LOADING', payload: true });
- }, 100);
+ }, 200);
try {
const results = await fileSearch.current.search(state.pattern, {
diff --git a/packages/core/src/utils/filesearch/fileSearch.ts b/packages/core/src/utils/filesearch/fileSearch.ts
index 76a099f7..480d5815 100644
--- a/packages/core/src/utils/filesearch/fileSearch.ts
+++ b/packages/core/src/utils/filesearch/fileSearch.ts
@@ -11,7 +11,7 @@ import picomatch from 'picomatch';
import { Ignore } from './ignore.js';
import { ResultCache } from './result-cache.js';
import * as cache from './crawlCache.js';
-import { Fzf, FzfResultItem } from 'fzf';
+import { AsyncFzf, FzfResultItem } from 'fzf';
export type FileSearchOptions = {
projectRoot: string;
@@ -78,18 +78,6 @@ export async function filter(
return results;
}
-/**
- * Filters a list of paths based on a given pattern using fzf.
- * @param allPaths The list of all paths to filter.
- * @param pattern The fzf pattern to filter by.
- * @returns The filtered and sorted list of paths.
- */
-function filterByFzf(allPaths: string[], pattern: string) {
- return new Fzf(allPaths)
- .find(pattern)
- .map((entry: FzfResultItem) => entry.item);
-}
-
export type SearchOptions = {
signal?: AbortSignal;
maxResults?: number;
@@ -105,6 +93,7 @@ export class FileSearch {
private readonly ignore: Ignore = new Ignore();
private resultCache: ResultCache | undefined;
private allFiles: string[] = [];
+ private fzf: AsyncFzf<string[]> | undefined;
/**
* Constructs a new `FileSearch` instance.
@@ -136,24 +125,38 @@ export class FileSearch {
pattern: string,
options: SearchOptions = {},
): Promise<string[]> {
- if (!this.resultCache) {
+ if (!this.resultCache || !this.fzf) {
throw new Error('Engine not initialized. Call initialize() first.');
}
pattern = pattern || '*';
+ let filteredCandidates;
const { files: candidates, isExactMatch } =
await this.resultCache!.get(pattern);
- let filteredCandidates;
if (isExactMatch) {
+ // Use the cached result.
filteredCandidates = candidates;
} else {
- // Apply the user's picomatch pattern filter
- filteredCandidates = pattern.includes('*')
- ? await filter(candidates, pattern, options.signal)
- : filterByFzf(this.allFiles, pattern);
- this.resultCache!.set(pattern, filteredCandidates);
+ let shouldCache = true;
+ if (pattern.includes('*')) {
+ filteredCandidates = await filter(candidates, pattern, options.signal);
+ } else {
+ filteredCandidates = await this.fzf
+ .find(pattern)
+ .then((results: Array<FzfResultItem<string>>) =>
+ results.map((entry: FzfResultItem<string>) => entry.item),
+ )
+ .catch(() => {
+ shouldCache = false;
+ return [];
+ });
+ }
+
+ if (shouldCache) {
+ this.resultCache!.set(pattern, filteredCandidates);
+ }
}
// Trade-off: We apply a two-stage filtering process.
@@ -287,5 +290,11 @@ export class FileSearch {
*/
private buildResultCache(): void {
this.resultCache = new ResultCache(this.allFiles, this.absoluteDir);
+ // The v1 algorithm is much faster since it only looks at the first
+ // occurence of the pattern. We use it for search spaces that have >20k
+ // files, because the v2 algorithm is just too slow in those cases.
+ this.fzf = new AsyncFzf(this.allFiles, {
+ fuzzy: this.allFiles.length > 20000 ? 'v1' : 'v2',
+ });
}
}