diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 365ba9d..9c861e9 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -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", diff --git a/webview-ui/package.json b/webview-ui/package.json index 58fc516..1d19638 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -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", diff --git a/webview-ui/src/components/HistoryView.tsx b/webview-ui/src/components/HistoryView.tsx index f342b7d..3a75808 100644 --- a/webview-ui/src/components/HistoryView.tsx +++ b/webview-ui/src/components/HistoryView.tsx @@ -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() ? ( - - {part} - - ) : ( - 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; + } `}
{ whiteSpace: "pre-wrap", wordBreak: "break-word", overflowWrap: "anywhere", - }}> - {highlightText(item.task, searchQuery)} -
+ }} + dangerouslySetInnerHTML={{ __html: item.task }} + />
( ) +// https://gist.github.com/evenfrost/1ba123656ded32fb7a0cd4651efd4db0 +const highlight = (fuseSearchResult: FuseResult[], highlightClassName: string = "history-item-highlight") => { + const set = (obj: Record, 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 + } + + 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]), + ``, + inputText.substring(region[0], lastRegionNextIndex), + "", + ].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)