Skip to content

Commit 04da686

Browse files
committed
Merge remote-tracking branch 'origin/main' into vscode-lm-provider
2 parents 4194300 + ef8d02d commit 04da686

File tree

9 files changed

+135
-186
lines changed

9 files changed

+135
-186
lines changed

.changeset/tiny-snakes-chew.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Improvements to fuzzy search in mentions, history, and model lists

webview-ui/package-lock.json

Lines changed: 5 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"@vscode/webview-ui-toolkit": "^1.4.0",
1414
"debounce": "^2.1.1",
1515
"fast-deep-equal": "^3.1.3",
16-
"fuse.js": "^7.0.0",
16+
"fzf": "^0.5.2",
1717
"react": "^18.3.1",
1818
"react-dom": "^18.3.1",
1919
"react-remark": "^2.1.0",

webview-ui/src/components/history/HistoryView.tsx

Lines changed: 14 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { useExtensionState } from "../../context/ExtensionStateContext"
33
import { vscode } from "../../utils/vscode"
44
import { Virtuoso } from "react-virtuoso"
55
import React, { memo, useMemo, useState, useEffect } from "react"
6-
import Fuse, { FuseResult } from "fuse.js"
6+
import { Fzf } from "fzf"
77
import { formatLargeNumber } from "../../utils/format"
8+
import { highlightFzfMatch } from "../../utils/highlight"
89

910
type HistoryViewProps = {
1011
onDone: () => void
@@ -67,20 +68,21 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
6768
return taskHistory.filter((item) => item.ts && item.task)
6869
}, [taskHistory])
6970

70-
const fuse = useMemo(() => {
71-
return new Fuse(presentableTasks, {
72-
keys: ["task"],
73-
threshold: 0.6,
74-
shouldSort: true,
75-
isCaseSensitive: false,
76-
ignoreLocation: false,
77-
includeMatches: true,
78-
minMatchCharLength: 1,
71+
const fzf = useMemo(() => {
72+
return new Fzf(presentableTasks, {
73+
selector: item => item.task
7974
})
8075
}, [presentableTasks])
8176

8277
const taskHistorySearchResults = useMemo(() => {
83-
let results = searchQuery ? highlight(fuse.search(searchQuery)) : presentableTasks
78+
let results = presentableTasks
79+
if (searchQuery) {
80+
const searchResults = fzf.find(searchQuery)
81+
results = searchResults.map(result => ({
82+
...result.item,
83+
task: highlightFzfMatch(result.item.task, Array.from(result.positions))
84+
}))
85+
}
8486

8587
// First apply search if needed
8688
const searchResults = searchQuery ? results : presentableTasks;
@@ -104,7 +106,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
104106
return (b.ts || 0) - (a.ts || 0);
105107
}
106108
});
107-
}, [presentableTasks, searchQuery, fuse, sortOption])
109+
}, [presentableTasks, searchQuery, fzf, sortOption])
108110

109111
return (
110112
<>
@@ -463,112 +465,4 @@ const ExportButton = ({ itemId }: { itemId: string }) => (
463465
</VSCodeButton>
464466
)
465467

466-
// https://gist.github.com/evenfrost/1ba123656ded32fb7a0cd4651efd4db0
467-
export const highlight = (
468-
fuseSearchResult: FuseResult<any>[],
469-
highlightClassName: string = "history-item-highlight",
470-
) => {
471-
const set = (obj: Record<string, any>, path: string, value: any) => {
472-
const pathValue = path.split(".")
473-
let i: number
474-
475-
for (i = 0; i < pathValue.length - 1; i++) {
476-
if (pathValue[i] === "__proto__" || pathValue[i] === "constructor") return
477-
obj = obj[pathValue[i]] as Record<string, any>
478-
}
479-
480-
if (pathValue[i] !== "__proto__" && pathValue[i] !== "constructor") {
481-
obj[pathValue[i]] = value
482-
}
483-
}
484-
485-
// Function to merge overlapping regions
486-
const mergeRegions = (regions: [number, number][]): [number, number][] => {
487-
if (regions.length === 0) return regions
488-
489-
// Sort regions by start index
490-
regions.sort((a, b) => a[0] - b[0])
491-
492-
const merged: [number, number][] = [regions[0]]
493-
494-
for (let i = 1; i < regions.length; i++) {
495-
const last = merged[merged.length - 1]
496-
const current = regions[i]
497-
498-
if (current[0] <= last[1] + 1) {
499-
// Overlapping or adjacent regions
500-
last[1] = Math.max(last[1], current[1])
501-
} else {
502-
merged.push(current)
503-
}
504-
}
505-
506-
return merged
507-
}
508-
509-
const generateHighlightedText = (inputText: string, regions: [number, number][] = []) => {
510-
if (regions.length === 0) {
511-
return inputText
512-
}
513-
514-
// Sort and merge overlapping regions
515-
const mergedRegions = mergeRegions(regions)
516-
517-
// Convert regions to a list of parts with their highlight status
518-
const parts: { text: string; highlight: boolean }[] = []
519-
let lastIndex = 0
520-
521-
mergedRegions.forEach(([start, end]) => {
522-
// Add non-highlighted text before this region
523-
if (start > lastIndex) {
524-
parts.push({
525-
text: inputText.substring(lastIndex, start),
526-
highlight: false
527-
})
528-
}
529-
530-
// Add highlighted text
531-
parts.push({
532-
text: inputText.substring(start, end + 1),
533-
highlight: true
534-
})
535-
536-
lastIndex = end + 1
537-
})
538-
539-
// Add any remaining text
540-
if (lastIndex < inputText.length) {
541-
parts.push({
542-
text: inputText.substring(lastIndex),
543-
highlight: false
544-
})
545-
}
546-
547-
// Build final string
548-
return parts
549-
.map(part =>
550-
part.highlight
551-
? `<span class="${highlightClassName}">${part.text}</span>`
552-
: part.text
553-
)
554-
.join('')
555-
}
556-
557-
return fuseSearchResult
558-
.filter(({ matches }) => matches && matches.length)
559-
.map(({ item, matches }) => {
560-
const highlightedItem = { ...item }
561-
562-
matches?.forEach((match) => {
563-
if (match.key && typeof match.value === "string" && match.indices) {
564-
// Merge overlapping regions before generating highlighted text
565-
const mergedIndices = mergeRegions([...match.indices])
566-
set(highlightedItem, match.key, generateHighlightedText(match.value, mergedIndices))
567-
}
568-
})
569-
570-
return highlightedItem
571-
})
572-
}
573-
574468
export default memo(HistoryView)

webview-ui/src/components/settings/GlamaModelPicker.tsx

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
2-
import Fuse from "fuse.js"
2+
import { Fzf } from "fzf"
33
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
44
import { useRemark } from "react-remark"
55
import { useMount } from "react-use"
66
import styled from "styled-components"
77
import { glamaDefaultModelId } from "../../../../src/shared/api"
88
import { useExtensionState } from "../../context/ExtensionStateContext"
99
import { vscode } from "../../utils/vscode"
10-
import { highlight } from "../history/HistoryView"
10+
import { highlightFzfMatch } from "../../utils/highlight"
1111
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
1212

1313
const GlamaModelPicker: React.FC = () => {
@@ -72,25 +72,21 @@ const GlamaModelPicker: React.FC = () => {
7272
}))
7373
}, [modelIds])
7474

75-
const fuse = useMemo(() => {
76-
return new Fuse(searchableItems, {
77-
keys: ["html"], // highlight function will update this
78-
threshold: 0.6,
79-
shouldSort: true,
80-
isCaseSensitive: false,
81-
ignoreLocation: false,
82-
includeMatches: true,
83-
minMatchCharLength: 1,
75+
const fzf = useMemo(() => {
76+
return new Fzf(searchableItems, {
77+
selector: item => item.html
8478
})
8579
}, [searchableItems])
8680

8781
const modelSearchResults = useMemo(() => {
88-
let results: { id: string; html: string }[] = searchTerm
89-
? highlight(fuse.search(searchTerm), "model-item-highlight")
90-
: searchableItems
91-
// results.sort((a, b) => a.id.localeCompare(b.id)) NOTE: sorting like this causes ids in objects to be reordered and mismatched
92-
return results
93-
}, [searchableItems, searchTerm, fuse])
82+
if (!searchTerm) return searchableItems
83+
84+
const searchResults = fzf.find(searchTerm)
85+
return searchResults.map(result => ({
86+
...result.item,
87+
html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight")
88+
}))
89+
}, [searchableItems, searchTerm, fzf])
9490

9591
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
9692
if (!isDropdownVisible) return

webview-ui/src/components/settings/OpenAiModelPicker.tsx

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
2-
import Fuse from "fuse.js"
2+
import { Fzf } from "fzf"
33
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
44
import { useRemark } from "react-remark"
55
import styled from "styled-components"
66
import { useExtensionState } from "../../context/ExtensionStateContext"
77
import { vscode } from "../../utils/vscode"
8-
import { highlight } from "../history/HistoryView"
8+
import { highlightFzfMatch } from "../../utils/highlight"
99

1010
const OpenAiModelPicker: React.FC = () => {
1111
const { apiConfiguration, setApiConfiguration, openAiModels, onUpdateApiConfig } = useExtensionState()
@@ -71,25 +71,21 @@ const OpenAiModelPicker: React.FC = () => {
7171
}))
7272
}, [modelIds])
7373

74-
const fuse = useMemo(() => {
75-
return new Fuse(searchableItems, {
76-
keys: ["html"], // highlight function will update this
77-
threshold: 0.6,
78-
shouldSort: true,
79-
isCaseSensitive: false,
80-
ignoreLocation: false,
81-
includeMatches: true,
82-
minMatchCharLength: 1,
74+
const fzf = useMemo(() => {
75+
return new Fzf(searchableItems, {
76+
selector: item => item.html
8377
})
8478
}, [searchableItems])
8579

8680
const modelSearchResults = useMemo(() => {
87-
let results: { id: string; html: string }[] = searchTerm
88-
? highlight(fuse.search(searchTerm), "model-item-highlight")
89-
: searchableItems
90-
// results.sort((a, b) => a.id.localeCompare(b.id)) NOTE: sorting like this causes ids in objects to be reordered and mismatched
91-
return results
92-
}, [searchableItems, searchTerm, fuse])
81+
if (!searchTerm) return searchableItems
82+
83+
const searchResults = fzf.find(searchTerm)
84+
return searchResults.map(result => ({
85+
...result.item,
86+
html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight")
87+
}))
88+
}, [searchableItems, searchTerm, fzf])
9389

9490
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
9591
if (!isDropdownVisible) return

webview-ui/src/components/settings/OpenRouterModelPicker.tsx

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
2-
import Fuse from "fuse.js"
2+
import { Fzf } from "fzf"
33
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
44
import { useRemark } from "react-remark"
55
import { useMount } from "react-use"
66
import styled from "styled-components"
77
import { openRouterDefaultModelId } from "../../../../src/shared/api"
88
import { useExtensionState } from "../../context/ExtensionStateContext"
99
import { vscode } from "../../utils/vscode"
10-
import { highlight } from "../history/HistoryView"
10+
import { highlightFzfMatch } from "../../utils/highlight"
1111
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
1212

1313
const OpenRouterModelPicker: React.FC = () => {
@@ -71,25 +71,21 @@ const OpenRouterModelPicker: React.FC = () => {
7171
}))
7272
}, [modelIds])
7373

74-
const fuse = useMemo(() => {
75-
return new Fuse(searchableItems, {
76-
keys: ["html"], // highlight function will update this
77-
threshold: 0.6,
78-
shouldSort: true,
79-
isCaseSensitive: false,
80-
ignoreLocation: false,
81-
includeMatches: true,
82-
minMatchCharLength: 1,
74+
const fzf = useMemo(() => {
75+
return new Fzf(searchableItems, {
76+
selector: item => item.html
8377
})
8478
}, [searchableItems])
8579

8680
const modelSearchResults = useMemo(() => {
87-
let results: { id: string; html: string }[] = searchTerm
88-
? highlight(fuse.search(searchTerm), "model-item-highlight")
89-
: searchableItems
90-
// results.sort((a, b) => a.id.localeCompare(b.id)) NOTE: sorting like this causes ids in objects to be reordered and mismatched
91-
return results
92-
}, [searchableItems, searchTerm, fuse])
81+
if (!searchTerm) return searchableItems
82+
83+
const searchResults = fzf.find(searchTerm)
84+
return searchResults.map(result => ({
85+
...result.item,
86+
html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight")
87+
}))
88+
}, [searchableItems, searchTerm, fzf])
9389

9490
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
9591
if (!isDropdownVisible) return

0 commit comments

Comments
 (0)