Add fuzzy search to history view

This commit is contained in:
Saoud Rizwan
2024-09-11 15:16:12 -04:00
parent 1b41ee0a6b
commit 1fbf657410
3 changed files with 85 additions and 21 deletions

View File

@@ -17,6 +17,7 @@
"@types/react-dom": "^18.3.0",
"@vscode/webview-ui-toolkit": "^1.4.0",
"fast-deep-equal": "^3.1.3",
"fuse.js": "^7.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
@@ -9655,6 +9656,15 @@
"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": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",

View File

@@ -12,6 +12,7 @@
"@types/react-dom": "^18.3.0",
"@vscode/webview-ui-toolkit": "^1.4.0",
"fast-deep-equal": "^3.1.3",
"fuse.js": "^7.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",

View File

@@ -3,6 +3,7 @@ import { useExtensionState } from "../context/ExtensionStateContext"
import { vscode } from "../utils/vscode"
import { Virtuoso } from "react-virtuoso"
import { memo, useMemo, useState } from "react"
import Fuse, { FuseResult } from "fuse.js"
type HistoryViewProps = {
onDone: () => void
@@ -39,25 +40,23 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
return taskHistory.filter((item) => item.ts && item.task)
}, [taskHistory])
const taskHistorySearchResults = useMemo(() => {
return presentableTasks.filter((item) => item.task.toLowerCase().includes(searchQuery.toLowerCase()))
}, [presentableTasks, searchQuery])
const fuse = useMemo(() => {
return new Fuse(presentableTasks, {
keys: ["task"],
threshold: 0.4,
shouldSort: true,
isCaseSensitive: false,
ignoreLocation: true,
includeMatches: true,
minMatchCharLength: 1,
})
}, [presentableTasks])
const highlightText = (text: string, query: string) => {
if (!query) return text
const parts = text.split(new RegExp(`(${query})`, "gi"))
return parts.map((part, index) =>
part.toLowerCase() === query.toLowerCase() ? (
<mark
key={index}
style={{ backgroundColor: "var(--vscode-editor-findMatchHighlightBackground)", color: "inherit" }}>
{part}
</mark>
) : (
part
)
)
}
const taskHistorySearchResults = useMemo(() => {
if (!searchQuery) return presentableTasks
const searchResults = fuse.search(searchQuery)
return highlight(searchResults)
}, [presentableTasks, searchQuery, fuse])
return (
<>
@@ -75,6 +74,10 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
opacity: 1;
pointer-events: auto;
}
.history-item-highlight {
background-color: var(--vscode-editor-findMatchHighlightBackground);
color: inherit;
}
`}
</style>
<div
@@ -205,9 +208,9 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
{highlightText(item.task, searchQuery)}
</div>
}}
dangerouslySetInnerHTML={{ __html: item.task }}
/>
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
<div
style={{
@@ -364,4 +367,54 @@ const ExportButton = ({ itemId }: { itemId: string }) => (
</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)