From ff9b8c33e188540836d2c43e9d19688570af2014 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Wed, 15 Jan 2025 02:50:52 -0500 Subject: [PATCH] Remove all other usages of fuse.js --- webview-ui/package-lock.json | 8 - webview-ui/package.json | 1 - .../src/components/history/HistoryView.tsx | 156 ++++++------------ .../components/settings/GlamaModelPicker.tsx | 74 +++++++-- .../components/settings/OpenAiModelPicker.tsx | 74 +++++++-- .../settings/OpenRouterModelPicker.tsx | 74 +++++++-- 6 files changed, 218 insertions(+), 169 deletions(-) diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 5203e5a..781a331 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -18,7 +18,6 @@ "@vscode/webview-ui-toolkit": "^1.4.0", "debounce": "^2.1.1", "fast-deep-equal": "^3.1.3", - "fuse.js": "^7.0.0", "fzf": "^0.5.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -7481,13 +7480,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fuse.js": { - "version": "7.0.0", - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } - }, "node_modules/fzf": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz", diff --git a/webview-ui/package.json b/webview-ui/package.json index 3cb9a31..6b4d192 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -13,7 +13,6 @@ "@vscode/webview-ui-toolkit": "^1.4.0", "debounce": "^2.1.1", "fast-deep-equal": "^3.1.3", - "fuse.js": "^7.0.0", "fzf": "^0.5.2", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx index 28ff461..13150f9 100644 --- a/webview-ui/src/components/history/HistoryView.tsx +++ b/webview-ui/src/components/history/HistoryView.tsx @@ -3,7 +3,7 @@ import { useExtensionState } from "../../context/ExtensionStateContext" import { vscode } from "../../utils/vscode" import { Virtuoso } from "react-virtuoso" import React, { memo, useMemo, useState, useEffect } from "react" -import Fuse, { FuseResult } from "fuse.js" +import { Fzf } from "fzf" import { formatLargeNumber } from "../../utils/format" type HistoryViewProps = { @@ -67,20 +67,21 @@ const HistoryView = ({ onDone }: HistoryViewProps) => { return taskHistory.filter((item) => item.ts && item.task) }, [taskHistory]) - const fuse = useMemo(() => { - return new Fuse(presentableTasks, { - keys: ["task"], - threshold: 0.6, - shouldSort: true, - isCaseSensitive: false, - ignoreLocation: false, - includeMatches: true, - minMatchCharLength: 1, + const fzf = useMemo(() => { + return new Fzf(presentableTasks, { + selector: item => item.task }) }, [presentableTasks]) const taskHistorySearchResults = useMemo(() => { - let results = searchQuery ? highlight(fuse.search(searchQuery)) : presentableTasks + let results = presentableTasks + if (searchQuery) { + const searchResults = fzf.find(searchQuery) + results = searchResults.map(result => ({ + ...result.item, + task: highlightFzfMatch(result.item.task, Array.from(result.positions)) + })) + } // First apply search if needed const searchResults = searchQuery ? results : presentableTasks; @@ -104,7 +105,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => { return (b.ts || 0) - (a.ts || 0); } }); - }, [presentableTasks, searchQuery, fuse, sortOption]) + }, [presentableTasks, searchQuery, fzf, sortOption]) return ( <> @@ -463,112 +464,49 @@ const ExportButton = ({ itemId }: { itemId: string }) => ( ) -// https://gist.github.com/evenfrost/1ba123656ded32fb7a0cd4651efd4db0 -export const highlight = ( - fuseSearchResult: FuseResult[], - highlightClassName: string = "history-item-highlight", -) => { - const set = (obj: Record, path: string, value: any) => { - const pathValue = path.split(".") - let i: number +const highlightFzfMatch = (text: string, positions: number[], highlightClassName: string = "history-item-highlight") => { + if (!positions.length) return text - for (i = 0; i < pathValue.length - 1; i++) { - if (pathValue[i] === "__proto__" || pathValue[i] === "constructor") return - obj = obj[pathValue[i]] as Record - } + const parts: { text: string; highlight: boolean }[] = [] + let lastIndex = 0 - if (pathValue[i] !== "__proto__" && pathValue[i] !== "constructor") { - obj[pathValue[i]] = value - } - } + // Sort positions to ensure we process them in order + positions.sort((a, b) => a - b) - // Function to merge overlapping regions - const mergeRegions = (regions: [number, number][]): [number, number][] => { - if (regions.length === 0) return regions - - // Sort regions by start index - regions.sort((a, b) => a[0] - b[0]) - - const merged: [number, number][] = [regions[0]] - - for (let i = 1; i < regions.length; i++) { - const last = merged[merged.length - 1] - const current = regions[i] - - if (current[0] <= last[1] + 1) { - // Overlapping or adjacent regions - last[1] = Math.max(last[1], current[1]) - } else { - merged.push(current) - } - } - - return merged - } - - const generateHighlightedText = (inputText: string, regions: [number, number][] = []) => { - if (regions.length === 0) { - return inputText - } - - // Sort and merge overlapping regions - const mergedRegions = mergeRegions(regions) - - // Convert regions to a list of parts with their highlight status - const parts: { text: string; highlight: boolean }[] = [] - let lastIndex = 0 - - mergedRegions.forEach(([start, end]) => { - // Add non-highlighted text before this region - if (start > lastIndex) { - parts.push({ - text: inputText.substring(lastIndex, start), - highlight: false - }) - } - - // Add highlighted text + positions.forEach((pos) => { + // Add non-highlighted text before this position + if (pos > lastIndex) { parts.push({ - text: inputText.substring(start, end + 1), - highlight: true - }) - - lastIndex = end + 1 - }) - - // Add any remaining text - if (lastIndex < inputText.length) { - parts.push({ - text: inputText.substring(lastIndex), + text: text.substring(lastIndex, pos), highlight: false }) } - - // Build final string - return parts - .map(part => - part.highlight - ? `${part.text}` - : part.text - ) - .join('') + + // Add highlighted character + parts.push({ + text: text[pos], + highlight: true + }) + + lastIndex = pos + 1 + }) + + // Add any remaining text + if (lastIndex < text.length) { + parts.push({ + text: text.substring(lastIndex), + highlight: false + }) } - return fuseSearchResult - .filter(({ matches }) => matches && matches.length) - .map(({ item, matches }) => { - const highlightedItem = { ...item } - - matches?.forEach((match) => { - if (match.key && typeof match.value === "string" && match.indices) { - // Merge overlapping regions before generating highlighted text - const mergedIndices = mergeRegions([...match.indices]) - set(highlightedItem, match.key, generateHighlightedText(match.value, mergedIndices)) - } - }) - - return highlightedItem - }) + // Build final string + return parts + .map(part => + part.highlight + ? `${part.text}` + : part.text + ) + .join('') } export default memo(HistoryView) diff --git a/webview-ui/src/components/settings/GlamaModelPicker.tsx b/webview-ui/src/components/settings/GlamaModelPicker.tsx index fcc3269..5164ba5 100644 --- a/webview-ui/src/components/settings/GlamaModelPicker.tsx +++ b/webview-ui/src/components/settings/GlamaModelPicker.tsx @@ -1,5 +1,5 @@ import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import Fuse from "fuse.js" +import { Fzf } from "fzf" import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react" import { useRemark } from "react-remark" import { useMount } from "react-use" @@ -7,7 +7,6 @@ import styled from "styled-components" import { glamaDefaultModelId } from "../../../../src/shared/api" import { useExtensionState } from "../../context/ExtensionStateContext" import { vscode } from "../../utils/vscode" -import { highlight } from "../history/HistoryView" import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions" const GlamaModelPicker: React.FC = () => { @@ -72,25 +71,66 @@ const GlamaModelPicker: React.FC = () => { })) }, [modelIds]) - const fuse = useMemo(() => { - return new Fuse(searchableItems, { - keys: ["html"], // highlight function will update this - threshold: 0.6, - shouldSort: true, - isCaseSensitive: false, - ignoreLocation: false, - includeMatches: true, - minMatchCharLength: 1, + const highlightFzfMatch = (text: string, positions: number[]) => { + if (!positions.length) return text + + const parts: { text: string; highlight: boolean }[] = [] + let lastIndex = 0 + + // Sort positions to ensure we process them in order + positions.sort((a, b) => a - b) + + positions.forEach((pos) => { + // Add non-highlighted text before this position + if (pos > lastIndex) { + parts.push({ + text: text.substring(lastIndex, pos), + highlight: false + }) + } + + // Add highlighted character + parts.push({ + text: text[pos], + highlight: true + }) + + lastIndex = pos + 1 + }) + + // Add any remaining text + if (lastIndex < text.length) { + parts.push({ + text: text.substring(lastIndex), + highlight: false + }) + } + + // Build final string + return parts + .map(part => + part.highlight + ? `${part.text}` + : part.text + ) + .join('') + } + + const fzf = useMemo(() => { + return new Fzf(searchableItems, { + selector: item => item.html }) }, [searchableItems]) const modelSearchResults = useMemo(() => { - let results: { id: string; html: string }[] = searchTerm - ? highlight(fuse.search(searchTerm), "model-item-highlight") - : searchableItems - // results.sort((a, b) => a.id.localeCompare(b.id)) NOTE: sorting like this causes ids in objects to be reordered and mismatched - return results - }, [searchableItems, searchTerm, fuse]) + if (!searchTerm) return searchableItems + + const searchResults = fzf.find(searchTerm) + return searchResults.map(result => ({ + ...result.item, + html: highlightFzfMatch(result.item.html, Array.from(result.positions)) + })) + }, [searchableItems, searchTerm, fzf]) const handleKeyDown = (event: KeyboardEvent) => { if (!isDropdownVisible) return diff --git a/webview-ui/src/components/settings/OpenAiModelPicker.tsx b/webview-ui/src/components/settings/OpenAiModelPicker.tsx index 1e192ed..2e674dd 100644 --- a/webview-ui/src/components/settings/OpenAiModelPicker.tsx +++ b/webview-ui/src/components/settings/OpenAiModelPicker.tsx @@ -1,11 +1,10 @@ import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import Fuse from "fuse.js" +import { Fzf } from "fzf" import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react" import { useRemark } from "react-remark" import styled from "styled-components" import { useExtensionState } from "../../context/ExtensionStateContext" import { vscode } from "../../utils/vscode" -import { highlight } from "../history/HistoryView" const OpenAiModelPicker: React.FC = () => { const { apiConfiguration, setApiConfiguration, openAiModels, onUpdateApiConfig } = useExtensionState() @@ -71,25 +70,66 @@ const OpenAiModelPicker: React.FC = () => { })) }, [modelIds]) - const fuse = useMemo(() => { - return new Fuse(searchableItems, { - keys: ["html"], // highlight function will update this - threshold: 0.6, - shouldSort: true, - isCaseSensitive: false, - ignoreLocation: false, - includeMatches: true, - minMatchCharLength: 1, + const highlightFzfMatch = (text: string, positions: number[]) => { + if (!positions.length) return text + + const parts: { text: string; highlight: boolean }[] = [] + let lastIndex = 0 + + // Sort positions to ensure we process them in order + positions.sort((a, b) => a - b) + + positions.forEach((pos) => { + // Add non-highlighted text before this position + if (pos > lastIndex) { + parts.push({ + text: text.substring(lastIndex, pos), + highlight: false + }) + } + + // Add highlighted character + parts.push({ + text: text[pos], + highlight: true + }) + + lastIndex = pos + 1 + }) + + // Add any remaining text + if (lastIndex < text.length) { + parts.push({ + text: text.substring(lastIndex), + highlight: false + }) + } + + // Build final string + return parts + .map(part => + part.highlight + ? `${part.text}` + : part.text + ) + .join('') + } + + const fzf = useMemo(() => { + return new Fzf(searchableItems, { + selector: item => item.html }) }, [searchableItems]) const modelSearchResults = useMemo(() => { - let results: { id: string; html: string }[] = searchTerm - ? highlight(fuse.search(searchTerm), "model-item-highlight") - : searchableItems - // results.sort((a, b) => a.id.localeCompare(b.id)) NOTE: sorting like this causes ids in objects to be reordered and mismatched - return results - }, [searchableItems, searchTerm, fuse]) + if (!searchTerm) return searchableItems + + const searchResults = fzf.find(searchTerm) + return searchResults.map(result => ({ + ...result.item, + html: highlightFzfMatch(result.item.html, Array.from(result.positions)) + })) + }, [searchableItems, searchTerm, fzf]) const handleKeyDown = (event: KeyboardEvent) => { if (!isDropdownVisible) return diff --git a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx b/webview-ui/src/components/settings/OpenRouterModelPicker.tsx index b8ba201..13e308d 100644 --- a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx +++ b/webview-ui/src/components/settings/OpenRouterModelPicker.tsx @@ -1,5 +1,5 @@ import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import Fuse from "fuse.js" +import { Fzf } from "fzf" import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react" import { useRemark } from "react-remark" import { useMount } from "react-use" @@ -7,7 +7,6 @@ import styled from "styled-components" import { openRouterDefaultModelId } from "../../../../src/shared/api" import { useExtensionState } from "../../context/ExtensionStateContext" import { vscode } from "../../utils/vscode" -import { highlight } from "../history/HistoryView" import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions" const OpenRouterModelPicker: React.FC = () => { @@ -71,25 +70,66 @@ const OpenRouterModelPicker: React.FC = () => { })) }, [modelIds]) - const fuse = useMemo(() => { - return new Fuse(searchableItems, { - keys: ["html"], // highlight function will update this - threshold: 0.6, - shouldSort: true, - isCaseSensitive: false, - ignoreLocation: false, - includeMatches: true, - minMatchCharLength: 1, + const highlightFzfMatch = (text: string, positions: number[]) => { + if (!positions.length) return text + + const parts: { text: string; highlight: boolean }[] = [] + let lastIndex = 0 + + // Sort positions to ensure we process them in order + positions.sort((a, b) => a - b) + + positions.forEach((pos) => { + // Add non-highlighted text before this position + if (pos > lastIndex) { + parts.push({ + text: text.substring(lastIndex, pos), + highlight: false + }) + } + + // Add highlighted character + parts.push({ + text: text[pos], + highlight: true + }) + + lastIndex = pos + 1 + }) + + // Add any remaining text + if (lastIndex < text.length) { + parts.push({ + text: text.substring(lastIndex), + highlight: false + }) + } + + // Build final string + return parts + .map(part => + part.highlight + ? `${part.text}` + : part.text + ) + .join('') + } + + const fzf = useMemo(() => { + return new Fzf(searchableItems, { + selector: item => item.html }) }, [searchableItems]) const modelSearchResults = useMemo(() => { - let results: { id: string; html: string }[] = searchTerm - ? highlight(fuse.search(searchTerm), "model-item-highlight") - : searchableItems - // results.sort((a, b) => a.id.localeCompare(b.id)) NOTE: sorting like this causes ids in objects to be reordered and mismatched - return results - }, [searchableItems, searchTerm, fuse]) + if (!searchTerm) return searchableItems + + const searchResults = fzf.find(searchTerm) + return searchResults.map(result => ({ + ...result.item, + html: highlightFzfMatch(result.item.html, Array.from(result.positions)) + })) + }, [searchableItems, searchTerm, fzf]) const handleKeyDown = (event: KeyboardEvent) => { if (!isDropdownVisible) return