Browse Source

Add fuzzy search to history view

Saoud Rizwan 1 year ago
parent
commit
1fbf657410
3 changed files with 85 additions and 21 deletions
  1. 10 0
      webview-ui/package-lock.json
  2. 1 0
      webview-ui/package.json
  3. 74 21
      webview-ui/src/components/HistoryView.tsx

+ 10 - 0
webview-ui/package-lock.json

@@ -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",

+ 1 - 0
webview-ui/package.json

@@ -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",

+ 74 - 21
webview-ui/src/components/HistoryView.tsx

@@ -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(() => {
-		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 (
 	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)}
-									</div>
+										}}
+										dangerouslySetInnerHTML={{ __html: item.task }}
+									/>
 									<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)