mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Merge remote-tracking branch 'origin/main' into vscode-lm-provider
This commit is contained in:
5
.changeset/tiny-snakes-chew.md
Normal file
5
.changeset/tiny-snakes-chew.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"roo-cline": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improvements to fuzzy search in mentions, history, and model lists
|
||||||
12
webview-ui/package-lock.json
generated
12
webview-ui/package-lock.json
generated
@@ -18,7 +18,7 @@
|
|||||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||||
"debounce": "^2.1.1",
|
"debounce": "^2.1.1",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fuse.js": "^7.0.0",
|
"fzf": "^0.5.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-remark": "^2.1.0",
|
"react-remark": "^2.1.0",
|
||||||
@@ -7480,12 +7480,10 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fuse.js": {
|
"node_modules/fzf": {
|
||||||
"version": "7.0.0",
|
"version": "0.5.2",
|
||||||
"license": "Apache-2.0",
|
"resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz",
|
||||||
"engines": {
|
"integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q=="
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/gensync": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||||
"debounce": "^2.1.1",
|
"debounce": "^2.1.1",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fuse.js": "^7.0.0",
|
"fzf": "^0.5.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-remark": "^2.1.0",
|
"react-remark": "^2.1.0",
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ 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 React, { memo, useMemo, useState, useEffect } from "react"
|
import React, { memo, useMemo, useState, useEffect } from "react"
|
||||||
import Fuse, { FuseResult } from "fuse.js"
|
import { Fzf } from "fzf"
|
||||||
import { formatLargeNumber } from "../../utils/format"
|
import { formatLargeNumber } from "../../utils/format"
|
||||||
|
import { highlightFzfMatch } from "../../utils/highlight"
|
||||||
|
|
||||||
type HistoryViewProps = {
|
type HistoryViewProps = {
|
||||||
onDone: () => void
|
onDone: () => void
|
||||||
@@ -67,20 +68,21 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||||||
return taskHistory.filter((item) => item.ts && item.task)
|
return taskHistory.filter((item) => item.ts && item.task)
|
||||||
}, [taskHistory])
|
}, [taskHistory])
|
||||||
|
|
||||||
const fuse = useMemo(() => {
|
const fzf = useMemo(() => {
|
||||||
return new Fuse(presentableTasks, {
|
return new Fzf(presentableTasks, {
|
||||||
keys: ["task"],
|
selector: item => item.task
|
||||||
threshold: 0.6,
|
|
||||||
shouldSort: true,
|
|
||||||
isCaseSensitive: false,
|
|
||||||
ignoreLocation: false,
|
|
||||||
includeMatches: true,
|
|
||||||
minMatchCharLength: 1,
|
|
||||||
})
|
})
|
||||||
}, [presentableTasks])
|
}, [presentableTasks])
|
||||||
|
|
||||||
const taskHistorySearchResults = useMemo(() => {
|
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
|
// First apply search if needed
|
||||||
const searchResults = searchQuery ? results : presentableTasks;
|
const searchResults = searchQuery ? results : presentableTasks;
|
||||||
@@ -104,7 +106,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||||||
return (b.ts || 0) - (a.ts || 0);
|
return (b.ts || 0) - (a.ts || 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [presentableTasks, searchQuery, fuse, sortOption])
|
}, [presentableTasks, searchQuery, fzf, sortOption])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -463,112 +465,4 @@ const ExportButton = ({ itemId }: { itemId: string }) => (
|
|||||||
</VSCodeButton>
|
</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
|
|
||||||
|
|
||||||
for (i = 0; i < pathValue.length - 1; i++) {
|
|
||||||
if (pathValue[i] === "__proto__" || pathValue[i] === "constructor") return
|
|
||||||
obj = obj[pathValue[i]] as Record<string, any>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathValue[i] !== "__proto__" && pathValue[i] !== "constructor") {
|
|
||||||
obj[pathValue[i]] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
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),
|
|
||||||
highlight: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build final string
|
|
||||||
return parts
|
|
||||||
.map(part =>
|
|
||||||
part.highlight
|
|
||||||
? `<span class="${highlightClassName}">${part.text}</span>`
|
|
||||||
: part.text
|
|
||||||
)
|
|
||||||
.join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(HistoryView)
|
export default memo(HistoryView)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
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 React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { useRemark } from "react-remark"
|
import { useRemark } from "react-remark"
|
||||||
import { useMount } from "react-use"
|
import { useMount } from "react-use"
|
||||||
@@ -7,7 +7,7 @@ import styled from "styled-components"
|
|||||||
import { glamaDefaultModelId } from "../../../../src/shared/api"
|
import { glamaDefaultModelId } from "../../../../src/shared/api"
|
||||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||||
import { vscode } from "../../utils/vscode"
|
import { vscode } from "../../utils/vscode"
|
||||||
import { highlight } from "../history/HistoryView"
|
import { highlightFzfMatch } from "../../utils/highlight"
|
||||||
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
|
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
|
||||||
|
|
||||||
const GlamaModelPicker: React.FC = () => {
|
const GlamaModelPicker: React.FC = () => {
|
||||||
@@ -72,25 +72,21 @@ const GlamaModelPicker: React.FC = () => {
|
|||||||
}))
|
}))
|
||||||
}, [modelIds])
|
}, [modelIds])
|
||||||
|
|
||||||
const fuse = useMemo(() => {
|
const fzf = useMemo(() => {
|
||||||
return new Fuse(searchableItems, {
|
return new Fzf(searchableItems, {
|
||||||
keys: ["html"], // highlight function will update this
|
selector: item => item.html
|
||||||
threshold: 0.6,
|
|
||||||
shouldSort: true,
|
|
||||||
isCaseSensitive: false,
|
|
||||||
ignoreLocation: false,
|
|
||||||
includeMatches: true,
|
|
||||||
minMatchCharLength: 1,
|
|
||||||
})
|
})
|
||||||
}, [searchableItems])
|
}, [searchableItems])
|
||||||
|
|
||||||
const modelSearchResults = useMemo(() => {
|
const modelSearchResults = useMemo(() => {
|
||||||
let results: { id: string; html: string }[] = searchTerm
|
if (!searchTerm) return searchableItems
|
||||||
? highlight(fuse.search(searchTerm), "model-item-highlight")
|
|
||||||
: searchableItems
|
const searchResults = fzf.find(searchTerm)
|
||||||
// results.sort((a, b) => a.id.localeCompare(b.id)) NOTE: sorting like this causes ids in objects to be reordered and mismatched
|
return searchResults.map(result => ({
|
||||||
return results
|
...result.item,
|
||||||
}, [searchableItems, searchTerm, fuse])
|
html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight")
|
||||||
|
}))
|
||||||
|
}, [searchableItems, searchTerm, fzf])
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (!isDropdownVisible) return
|
if (!isDropdownVisible) return
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
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 React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { useRemark } from "react-remark"
|
import { useRemark } from "react-remark"
|
||||||
import styled from "styled-components"
|
import styled from "styled-components"
|
||||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||||
import { vscode } from "../../utils/vscode"
|
import { vscode } from "../../utils/vscode"
|
||||||
import { highlight } from "../history/HistoryView"
|
import { highlightFzfMatch } from "../../utils/highlight"
|
||||||
|
|
||||||
const OpenAiModelPicker: React.FC = () => {
|
const OpenAiModelPicker: React.FC = () => {
|
||||||
const { apiConfiguration, setApiConfiguration, openAiModels, onUpdateApiConfig } = useExtensionState()
|
const { apiConfiguration, setApiConfiguration, openAiModels, onUpdateApiConfig } = useExtensionState()
|
||||||
@@ -71,25 +71,21 @@ const OpenAiModelPicker: React.FC = () => {
|
|||||||
}))
|
}))
|
||||||
}, [modelIds])
|
}, [modelIds])
|
||||||
|
|
||||||
const fuse = useMemo(() => {
|
const fzf = useMemo(() => {
|
||||||
return new Fuse(searchableItems, {
|
return new Fzf(searchableItems, {
|
||||||
keys: ["html"], // highlight function will update this
|
selector: item => item.html
|
||||||
threshold: 0.6,
|
|
||||||
shouldSort: true,
|
|
||||||
isCaseSensitive: false,
|
|
||||||
ignoreLocation: false,
|
|
||||||
includeMatches: true,
|
|
||||||
minMatchCharLength: 1,
|
|
||||||
})
|
})
|
||||||
}, [searchableItems])
|
}, [searchableItems])
|
||||||
|
|
||||||
const modelSearchResults = useMemo(() => {
|
const modelSearchResults = useMemo(() => {
|
||||||
let results: { id: string; html: string }[] = searchTerm
|
if (!searchTerm) return searchableItems
|
||||||
? highlight(fuse.search(searchTerm), "model-item-highlight")
|
|
||||||
: searchableItems
|
const searchResults = fzf.find(searchTerm)
|
||||||
// results.sort((a, b) => a.id.localeCompare(b.id)) NOTE: sorting like this causes ids in objects to be reordered and mismatched
|
return searchResults.map(result => ({
|
||||||
return results
|
...result.item,
|
||||||
}, [searchableItems, searchTerm, fuse])
|
html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight")
|
||||||
|
}))
|
||||||
|
}, [searchableItems, searchTerm, fzf])
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (!isDropdownVisible) return
|
if (!isDropdownVisible) return
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
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 React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { useRemark } from "react-remark"
|
import { useRemark } from "react-remark"
|
||||||
import { useMount } from "react-use"
|
import { useMount } from "react-use"
|
||||||
@@ -7,7 +7,7 @@ import styled from "styled-components"
|
|||||||
import { openRouterDefaultModelId } from "../../../../src/shared/api"
|
import { openRouterDefaultModelId } from "../../../../src/shared/api"
|
||||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||||
import { vscode } from "../../utils/vscode"
|
import { vscode } from "../../utils/vscode"
|
||||||
import { highlight } from "../history/HistoryView"
|
import { highlightFzfMatch } from "../../utils/highlight"
|
||||||
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
|
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
|
||||||
|
|
||||||
const OpenRouterModelPicker: React.FC = () => {
|
const OpenRouterModelPicker: React.FC = () => {
|
||||||
@@ -71,25 +71,21 @@ const OpenRouterModelPicker: React.FC = () => {
|
|||||||
}))
|
}))
|
||||||
}, [modelIds])
|
}, [modelIds])
|
||||||
|
|
||||||
const fuse = useMemo(() => {
|
const fzf = useMemo(() => {
|
||||||
return new Fuse(searchableItems, {
|
return new Fzf(searchableItems, {
|
||||||
keys: ["html"], // highlight function will update this
|
selector: item => item.html
|
||||||
threshold: 0.6,
|
|
||||||
shouldSort: true,
|
|
||||||
isCaseSensitive: false,
|
|
||||||
ignoreLocation: false,
|
|
||||||
includeMatches: true,
|
|
||||||
minMatchCharLength: 1,
|
|
||||||
})
|
})
|
||||||
}, [searchableItems])
|
}, [searchableItems])
|
||||||
|
|
||||||
const modelSearchResults = useMemo(() => {
|
const modelSearchResults = useMemo(() => {
|
||||||
let results: { id: string; html: string }[] = searchTerm
|
if (!searchTerm) return searchableItems
|
||||||
? highlight(fuse.search(searchTerm), "model-item-highlight")
|
|
||||||
: searchableItems
|
const searchResults = fzf.find(searchTerm)
|
||||||
// results.sort((a, b) => a.id.localeCompare(b.id)) NOTE: sorting like this causes ids in objects to be reordered and mismatched
|
return searchResults.map(result => ({
|
||||||
return results
|
...result.item,
|
||||||
}, [searchableItems, searchTerm, fuse])
|
html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight")
|
||||||
|
}))
|
||||||
|
}, [searchableItems, searchTerm, fzf])
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (!isDropdownVisible) return
|
if (!isDropdownVisible) return
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { mentionRegex } from "../../../src/shared/context-mentions"
|
import { mentionRegex } from "../../../src/shared/context-mentions"
|
||||||
|
import { Fzf } from "fzf"
|
||||||
|
|
||||||
export function insertMention(
|
export function insertMention(
|
||||||
text: string,
|
text: string,
|
||||||
@@ -147,13 +148,21 @@ export function getContextMenuOptions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get matching items, separating by type
|
// Create searchable strings array for fzf
|
||||||
const matchingItems = queryItems.filter((item) =>
|
const searchableItems = queryItems.map(item => ({
|
||||||
item.value?.toLowerCase().includes(lowerQuery) ||
|
original: item,
|
||||||
item.label?.toLowerCase().includes(lowerQuery) ||
|
searchStr: [item.value, item.label, item.description].filter(Boolean).join(' ')
|
||||||
item.description?.toLowerCase().includes(lowerQuery)
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
|
// Initialize fzf instance for fuzzy search
|
||||||
|
const fzf = new Fzf(searchableItems, {
|
||||||
|
selector: item => item.searchStr
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get fuzzy matching items
|
||||||
|
const matchingItems = query ? fzf.find(query).map(result => result.item.original) : []
|
||||||
|
|
||||||
|
// Separate matches by type
|
||||||
const fileMatches = matchingItems.filter(item =>
|
const fileMatches = matchingItems.filter(item =>
|
||||||
item.type === ContextMenuOptionType.File ||
|
item.type === ContextMenuOptionType.File ||
|
||||||
item.type === ContextMenuOptionType.Folder
|
item.type === ContextMenuOptionType.Folder
|
||||||
@@ -169,7 +178,18 @@ export function getContextMenuOptions(
|
|||||||
|
|
||||||
// Combine suggestions with matching items in the desired order
|
// Combine suggestions with matching items in the desired order
|
||||||
if (suggestions.length > 0 || matchingItems.length > 0) {
|
if (suggestions.length > 0 || matchingItems.length > 0) {
|
||||||
return [...suggestions, ...fileMatches, ...gitMatches, ...otherMatches]
|
const allItems = [...suggestions, ...fileMatches, ...gitMatches, ...otherMatches]
|
||||||
|
|
||||||
|
// Remove duplicates based on type and value
|
||||||
|
const seen = new Set()
|
||||||
|
const deduped = allItems.filter(item => {
|
||||||
|
const key = `${item.type}-${item.value}`
|
||||||
|
if (seen.has(key)) return false
|
||||||
|
seen.add(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return deduped
|
||||||
}
|
}
|
||||||
|
|
||||||
return [{ type: ContextMenuOptionType.NoResults }]
|
return [{ type: ContextMenuOptionType.NoResults }]
|
||||||
|
|||||||
44
webview-ui/src/utils/highlight.ts
Normal file
44
webview-ui/src/utils/highlight.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
export function highlightFzfMatch(text: string, positions: number[], highlightClassName: string = "history-item-highlight") {
|
||||||
|
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="${highlightClassName}">${part.text}</span>`
|
||||||
|
: part.text
|
||||||
|
)
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user