diff --git a/cli/src/hooks/__tests__/use-suggestion-engine-mention.test.ts b/cli/src/hooks/__tests__/use-suggestion-engine-mention.test.ts index 68cbd99214..09a8ef8051 100644 --- a/cli/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +++ b/cli/src/hooks/__tests__/use-suggestion-engine-mention.test.ts @@ -1,6 +1,10 @@ import { describe, test, expect } from 'bun:test' -import { isInsideStringDelimiters, parseAtInLine } from '../use-suggestion-engine' +import { + filterFileMatches, + isInsideStringDelimiters, + parseAtInLine, +} from '../use-suggestion-engine' describe('@ mention edge cases - quote detection', () => { test('isInsideStringDelimiters detects position inside double quotes', () => { @@ -359,3 +363,29 @@ describe('single quote handling - apostrophes should NOT suppress @ menu', () => }, ) }) + +describe('filterFileMatches - bounding and ranking', () => { + test('caps results to top 100 matches when exceeding threshold', () => { + const manyFiles = Array.from({ length: 250 }, (_, i) => ({ + path: `src/components/item-${i}.tsx`, + isDirectory: false, + })) + + const results = filterFileMatches(manyFiles, 'item') + expect(results.length).toBe(100) + // Check that results are properly sorted (lowest score first) + for (let i = 1; i < results.length; i++) { + expect((results[i].matchScore ?? 0) >= (results[i - 1].matchScore ?? 0)).toBe(true) + } + }) + + test('returns all matches when count is within 100 limit', () => { + const files = Array.from({ length: 25 }, (_, i) => ({ + path: `src/components/item-${i}.tsx`, + isDirectory: false, + })) + + const results = filterFileMatches(files, 'item') + expect(results.length).toBe(25) + }) +}) diff --git a/cli/src/hooks/use-suggestion-engine.ts b/cli/src/hooks/use-suggestion-engine.ts index 83af7092d6..1fcceb93d3 100644 --- a/cli/src/hooks/use-suggestion-engine.ts +++ b/cli/src/hooks/use-suggestion-engine.ts @@ -283,9 +283,13 @@ const getFileName = (filePath: string): string => { return lastSlash === -1 ? filePath : filePath.slice(lastSlash + 1) } -const createHighlightIndices = (start: number, end: number): number[] => [ - ...range(start, end), -] +const createHighlightIndices = (start: number, end: number): number[] => { + const result: number[] = [] + for (let i = start; i < end; i++) { + result.push(i) + } + return result +} const createPushUnique = ( getKey: (item: T) => K, @@ -351,9 +355,13 @@ const fuzzyMatch = ( // - Fewer gaps = better // - Longer consecutive matches = better // - Matches at word boundaries (after /) = better - const boundaryBonus = indices.filter( - (idx) => idx === 0 || text[idx - 1] === '/' - ).length + let boundaryBonus = 0 + for (let i = 0; i < indices.length; i++) { + const idx = indices[i] + if (idx === 0 || text[idx - 1] === '/') { + boundaryBonus++ + } + } const score = gaps * 10 - @@ -364,7 +372,7 @@ const fuzzyMatch = ( return { indices, score } } -const filterFileMatches = ( +export const filterFileMatches = ( pathInfos: PathInfo[], query: string, ): MatchedFileInfo[] => { @@ -489,7 +497,7 @@ const filterFileMatches = ( // Sort by score (lower is better) matches.sort((a, b) => (a.matchScore ?? 0) - (b.matchScore ?? 0)) - return matches + return matches.length > 100 ? matches.slice(0, 100) : matches } const filterAgentMatches = (