mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Add fuzzy search to history view
This commit is contained in:
10
webview-ui/package-lock.json
generated
10
webview-ui/package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fuse.js": "^7.0.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
@@ -9655,6 +9656,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fuse.js": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gensync": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fuse.js": "^7.0.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useExtensionState } from "../context/ExtensionStateContext"
|
|||||||
import { vscode } from "../utils/vscode"
|
import { vscode } from "../utils/vscode"
|
||||||
import { Virtuoso } from "react-virtuoso"
|
import { Virtuoso } from "react-virtuoso"
|
||||||
import { memo, useMemo, useState } from "react"
|
import { memo, useMemo, useState } from "react"
|
||||||
|
import Fuse, { FuseResult } from "fuse.js"
|
||||||
|
|
||||||
type HistoryViewProps = {
|
type HistoryViewProps = {
|
||||||
onDone: () => void
|
onDone: () => void
|
||||||
@@ -39,25 +40,23 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||||||
return taskHistory.filter((item) => item.ts && item.task)
|
return taskHistory.filter((item) => item.ts && item.task)
|
||||||
}, [taskHistory])
|
}, [taskHistory])
|
||||||
|
|
||||||
const taskHistorySearchResults = useMemo(() => {
|
const fuse = useMemo(() => {
|
||||||
return presentableTasks.filter((item) => item.task.toLowerCase().includes(searchQuery.toLowerCase()))
|
return new Fuse(presentableTasks, {
|
||||||
}, [presentableTasks, searchQuery])
|
keys: ["task"],
|
||||||
|
threshold: 0.4,
|
||||||
|
shouldSort: true,
|
||||||
|
isCaseSensitive: false,
|
||||||
|
ignoreLocation: true,
|
||||||
|
includeMatches: true,
|
||||||
|
minMatchCharLength: 1,
|
||||||
|
})
|
||||||
|
}, [presentableTasks])
|
||||||
|
|
||||||
const highlightText = (text: string, query: string) => {
|
const taskHistorySearchResults = useMemo(() => {
|
||||||
if (!query) return text
|
if (!searchQuery) return presentableTasks
|
||||||
const parts = text.split(new RegExp(`(${query})`, "gi"))
|
const searchResults = fuse.search(searchQuery)
|
||||||
return parts.map((part, index) =>
|
return highlight(searchResults)
|
||||||
part.toLowerCase() === query.toLowerCase() ? (
|
}, [presentableTasks, searchQuery, fuse])
|
||||||
<mark
|
|
||||||
key={index}
|
|
||||||
style={{ backgroundColor: "var(--vscode-editor-findMatchHighlightBackground)", color: "inherit" }}>
|
|
||||||
{part}
|
|
||||||
</mark>
|
|
||||||
) : (
|
|
||||||
part
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -75,6 +74,10 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
.history-item-highlight {
|
||||||
|
background-color: var(--vscode-editor-findMatchHighlightBackground);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
`}
|
`}
|
||||||
</style>
|
</style>
|
||||||
<div
|
<div
|
||||||
@@ -205,9 +208,9 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||||||
whiteSpace: "pre-wrap",
|
whiteSpace: "pre-wrap",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
overflowWrap: "anywhere",
|
overflowWrap: "anywhere",
|
||||||
}}>
|
}}
|
||||||
{highlightText(item.task, searchQuery)}
|
dangerouslySetInnerHTML={{ __html: item.task }}
|
||||||
</div>
|
/>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -364,4 +367,54 @@ const ExportButton = ({ itemId }: { itemId: string }) => (
|
|||||||
</VSCodeButton>
|
</VSCodeButton>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// https://gist.github.com/evenfrost/1ba123656ded32fb7a0cd4651efd4db0
|
||||||
|
const highlight = (fuseSearchResult: FuseResult<any>[], highlightClassName: string = "history-item-highlight") => {
|
||||||
|
const set = (obj: Record<string, any>, path: string, value: any) => {
|
||||||
|
const pathValue = path.split(".")
|
||||||
|
let i: number
|
||||||
|
|
||||||
|
for (i = 0; i < pathValue.length - 1; i++) {
|
||||||
|
obj = obj[pathValue[i]] as Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
obj[pathValue[i]] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateHighlightedText = (inputText: string, regions: [number, number][] = []) => {
|
||||||
|
let content = ""
|
||||||
|
let nextUnhighlightedRegionStartingIndex = 0
|
||||||
|
|
||||||
|
regions.forEach((region) => {
|
||||||
|
const lastRegionNextIndex = region[1] + 1
|
||||||
|
|
||||||
|
content += [
|
||||||
|
inputText.substring(nextUnhighlightedRegionStartingIndex, region[0]),
|
||||||
|
`<span class="${highlightClassName}">`,
|
||||||
|
inputText.substring(region[0], lastRegionNextIndex),
|
||||||
|
"</span>",
|
||||||
|
].join("")
|
||||||
|
|
||||||
|
nextUnhighlightedRegionStartingIndex = lastRegionNextIndex
|
||||||
|
})
|
||||||
|
|
||||||
|
content += inputText.substring(nextUnhighlightedRegionStartingIndex)
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
return fuseSearchResult
|
||||||
|
.filter(({ matches }) => matches && matches.length)
|
||||||
|
.map(({ item, matches }) => {
|
||||||
|
const highlightedItem = { ...item }
|
||||||
|
|
||||||
|
matches?.forEach((match) => {
|
||||||
|
if (match.key && typeof match.value === "string") {
|
||||||
|
set(highlightedItem, match.key, generateHighlightedText(match.value, [...match.indices]))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return highlightedItem
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export default memo(HistoryView)
|
export default memo(HistoryView)
|
||||||
|
|||||||
Reference in New Issue
Block a user