Remove all other usages of fuse.js

This commit is contained in:
Matt Rubens
2025-01-15 02:50:52 -05:00
parent faed43b730
commit ff9b8c33e1
6 changed files with 218 additions and 169 deletions

View File

@@ -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 }) => (
</VSCodeButton>
)
// https://gist.github.com/evenfrost/1ba123656ded32fb7a0cd4651efd4db0
export 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
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<string, any>
}
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
? `<span class="${highlightClassName}">${part.text}</span>`
: 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
? `<span class="${highlightClassName}">${part.text}</span>`
: part.text
)
.join('')
}
export default memo(HistoryView)

View File

@@ -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
? `<span class="model-item-highlight">${part.text}</span>`
: 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<HTMLInputElement>) => {
if (!isDropdownVisible) return

View File

@@ -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
? `<span class="model-item-highlight">${part.text}</span>`
: 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<HTMLInputElement>) => {
if (!isDropdownVisible) return

View File

@@ -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
? `<span class="model-item-highlight">${part.text}</span>`
: 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<HTMLInputElement>) => {
if (!isDropdownVisible) return