From 5901446094988aa5cd8e11efdaa131c3949106f1 Mon Sep 17 00:00:00 2001 From: David Brailovsky Date: Fri, 18 Sep 2026 12:47:20 +0200 Subject: [PATCH] fix(tui): reduce fuzzy search latency Use native substring search to find successive fuzzy-match characters while preserving scoring and result ordering. Closes #9267 --- packages/tui/CHANGELOG.md | 4 ++++ packages/tui/src/fuzzy.ts | 49 ++++++++++++++++++++------------------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 7926e8d2f..5b129fb52 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Reduced fuzzy search latency for long texts by using native substring search instead of scanning each character in JavaScript ([#9267](https://github.com/earendil-works/pi/issues/9267)). + ### Fixed - Fixed fullscreen clipboard failures hiding actionable backend error messages behind a generic notice, and extended failure notices to five seconds ([#9618](https://github.com/earendil-works/pi/issues/9618)). diff --git a/packages/tui/src/fuzzy.ts b/packages/tui/src/fuzzy.ts index 73c10dcf4..1b5352c1c 100644 --- a/packages/tui/src/fuzzy.ts +++ b/packages/tui/src/fuzzy.ts @@ -27,33 +27,34 @@ export function fuzzyMatch(query: string, text: string): FuzzyMatch { let lastMatchIndex = -1; let consecutiveMatches = 0; - for (let i = 0; i < textLower.length && queryIndex < normalizedQuery.length; i++) { - if (textLower[i] === normalizedQuery[queryIndex]) { - const isWordBoundary = i === 0 || /[\s\-_./:]/.test(textLower[i - 1]!); + while (queryIndex < normalizedQuery.length) { + const i = textLower.indexOf(normalizedQuery[queryIndex]!, lastMatchIndex + 1); + if (i === -1) break; - // Reward consecutive matches - if (lastMatchIndex === i - 1) { - consecutiveMatches++; - score -= consecutiveMatches * 5; - } else { - consecutiveMatches = 0; - // Penalize gaps - if (lastMatchIndex >= 0) { - score += (i - lastMatchIndex - 1) * 2; - } + const isWordBoundary = i === 0 || /[\s\-_./:]/.test(textLower[i - 1]!); + + // Reward consecutive matches + if (lastMatchIndex === i - 1) { + consecutiveMatches++; + score -= consecutiveMatches * 5; + } else { + consecutiveMatches = 0; + // Penalize gaps + if (lastMatchIndex >= 0) { + score += (i - lastMatchIndex - 1) * 2; } - - // Reward word boundary matches - if (isWordBoundary) { - score -= 10; - } - - // Slight penalty for later matches - score += i * 0.1; - - lastMatchIndex = i; - queryIndex++; } + + // Reward word boundary matches + if (isWordBoundary) { + score -= 10; + } + + // Slight penalty for later matches + score += i * 0.1; + + lastMatchIndex = i; + queryIndex++; } if (queryIndex < normalizedQuery.length) {