Skip to content

Commit af30525

Browse files
committed
fix(search): highlight the matched substring, not an earlier scattered occurrence
Contiguous substring matches (exact/prefix/contains) now report the substring's own indices instead of the greedy subsequence scan positions, so HighlightedText bolds the characters the user actually matched. Restructures fuzzyMatch to handle the substring tier first; scores are unchanged for these cases.
1 parent efcde50 commit af30525

2 files changed

Lines changed: 36 additions & 7 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,16 @@ describe('fuzzyMatch — positions for highlighting', () => {
220220
expect(fuzzyMatch('Slack', 'slk').positions).toEqual([0, 1, 4])
221221
})
222222

223+
it('highlights the substring itself, not an earlier scattered occurrence', () => {
224+
const result = fuzzyMatch('a_apple', 'apple')
225+
expect(result.matched).toBe(true)
226+
expect(result.positions).toEqual([2, 3, 4, 5, 6])
227+
})
228+
229+
it('highlights a mid-string substring at its real position', () => {
230+
expect(fuzzyMatch('Webhook', 'hook').positions).toEqual([3, 4, 5, 6])
231+
})
232+
223233
it('reports empty positions for empty query', () => {
224234
const result = fuzzyMatch('Slack', '')
225235
expect(result.matched).toBe(true)

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ function tokenFallback(lowerText: string, lowerQuery: string): FuzzyResult {
175175
* Falls back to order-independent token matching for multi-word queries
176176
* (`message slack` matches "Slack Send Message") which a strict left-to-right
177177
* subsequence would miss.
178+
*
179+
* Contiguous substring matches report the indices of the substring itself, so
180+
* highlighting always bolds the run the user actually matched rather than an
181+
* earlier scattered occurrence of the same characters.
178182
*/
179183
export function fuzzyMatch(text: string, query: string): FuzzyResult {
180184
if (!query) return { matched: true, score: 1, positions: [] }
@@ -183,6 +187,27 @@ export function fuzzyMatch(text: string, query: string): FuzzyResult {
183187
const lowerText = text.toLowerCase()
184188
const lowerQuery = query.toLowerCase()
185189

190+
const substringIndex = lowerText.indexOf(lowerQuery)
191+
if (substringIndex !== -1) {
192+
const length = lowerQuery.length
193+
const positions = Array.from({ length }, (_, k) => substringIndex + k)
194+
195+
let score = 1
196+
if (substringIndex === 0) score += 10
197+
else if (SEPARATORS.has(lowerText[substringIndex - 1])) score += 8
198+
else if (isCamelBoundary(text, substringIndex)) score += 6
199+
score += (length - 1) * 6
200+
201+
if (lowerText === lowerQuery) score += 120
202+
else if (substringIndex === 0) score += 50
203+
else score += 25
204+
205+
score -= substringIndex * 0.5
206+
score -= (length - 1) * 0.15
207+
score -= lowerText.length * 0.1
208+
return { matched: true, score, positions }
209+
}
210+
186211
const positions: number[] = []
187212
let queryIndex = 0
188213
let score = 0
@@ -203,13 +228,7 @@ export function fuzzyMatch(text: string, query: string): FuzzyResult {
203228
queryIndex++
204229
}
205230

206-
if (queryIndex === lowerQuery.length) {
207-
if (lowerText === lowerQuery) score += 120
208-
else if (lowerText.startsWith(lowerQuery)) score += 50
209-
else if (lowerText.includes(lowerQuery)) score += 25
210-
else if (!isHardBoundary(lowerText, positions[0])) {
211-
return tokenFallback(lowerText, lowerQuery)
212-
}
231+
if (queryIndex === lowerQuery.length && isHardBoundary(lowerText, positions[0])) {
213232
score -= positions[0] * 0.5
214233
score -= (positions[positions.length - 1] - positions[0]) * 0.15
215234
score -= lowerText.length * 0.1

0 commit comments

Comments
 (0)