Added debounce for search and chat text inputs to help with typing speed responsiveness with large sessions (#2065)
This commit is contained in:
Generated
+8
-1
@@ -23,6 +23,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
"@radix-ui/themes": "^3.1.5",
|
"@radix-ui/themes": "^3.1.5",
|
||||||
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"framer-motion": "^11.11.11",
|
"framer-motion": "^11.11.11",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@@ -4486,6 +4488,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz",
|
||||||
|
"integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/mdast": {
|
"node_modules/@types/mdast": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
||||||
@@ -11151,7 +11159,6 @@
|
|||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.castarray": {
|
"node_modules/lodash.castarray": {
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
"@radix-ui/themes": "^3.1.5",
|
"@radix-ui/themes": "^3.1.5",
|
||||||
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
@@ -96,6 +97,7 @@
|
|||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"framer-motion": "^11.11.11",
|
"framer-motion": "^11.11.11",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useRef, useState, useEffect } from 'react';
|
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import Stop from './ui/Stop';
|
import Stop from './ui/Stop';
|
||||||
import { Attach, Send } from './icons';
|
import { Attach, Send } from './icons';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
interface InputProps {
|
interface InputProps {
|
||||||
handleSubmit: (e: React.FormEvent) => void;
|
handleSubmit: (e: React.FormEvent) => void;
|
||||||
@@ -18,12 +19,14 @@ export default function Input({
|
|||||||
commandHistory = [],
|
commandHistory = [],
|
||||||
initialValue = '',
|
initialValue = '',
|
||||||
}: InputProps) {
|
}: InputProps) {
|
||||||
const [value, setValue] = useState(initialValue);
|
const [_value, setValue] = useState(initialValue);
|
||||||
|
const [displayValue, setDisplayValue] = useState(initialValue); // For immediate visual feedback
|
||||||
|
|
||||||
// Update internal value when initialValue changes
|
// Update internal value when initialValue changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialValue) {
|
if (initialValue) {
|
||||||
setValue(initialValue);
|
setValue(initialValue);
|
||||||
|
setDisplayValue(initialValue);
|
||||||
}
|
}
|
||||||
}, [initialValue]);
|
}, [initialValue]);
|
||||||
|
|
||||||
@@ -39,12 +42,28 @@ export default function Input({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Debounced function to update actual value
|
||||||
|
const debouncedSetValue = useCallback(
|
||||||
|
debounce((val: string) => {
|
||||||
|
setValue(val);
|
||||||
|
}, 150),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Debounced autosize function
|
||||||
|
const debouncedAutosize = useCallback(
|
||||||
|
debounce((textArea: HTMLTextAreaElement, value: string) => {
|
||||||
|
textArea.style.height = '0px'; // Reset height
|
||||||
|
const scrollHeight = textArea.scrollHeight;
|
||||||
|
textArea.style.height = Math.min(scrollHeight, maxHeight) + 'px';
|
||||||
|
}, 150),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const useAutosizeTextArea = (textAreaRef: HTMLTextAreaElement | null, value: string) => {
|
const useAutosizeTextArea = (textAreaRef: HTMLTextAreaElement | null, value: string) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (textAreaRef) {
|
if (textAreaRef) {
|
||||||
textAreaRef.style.height = '0px'; // Reset height
|
debouncedAutosize(textAreaRef, value);
|
||||||
const scrollHeight = textAreaRef.scrollHeight;
|
|
||||||
textAreaRef.style.height = Math.min(scrollHeight, maxHeight) + 'px';
|
|
||||||
}
|
}
|
||||||
}, [textAreaRef, value]);
|
}, [textAreaRef, value]);
|
||||||
};
|
};
|
||||||
@@ -52,13 +71,22 @@ export default function Input({
|
|||||||
const minHeight = '1rem';
|
const minHeight = '1rem';
|
||||||
const maxHeight = 10 * 24;
|
const maxHeight = 10 * 24;
|
||||||
|
|
||||||
useAutosizeTextArea(textAreaRef.current, value);
|
useAutosizeTextArea(textAreaRef.current, displayValue);
|
||||||
|
|
||||||
const handleChange = (evt: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleChange = (evt: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const val = evt.target.value;
|
const val = evt.target.value;
|
||||||
setValue(val);
|
setDisplayValue(val); // Update display immediately
|
||||||
|
debouncedSetValue(val); // Debounce the actual state update
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Cleanup debounced functions on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
debouncedSetValue.cancel();
|
||||||
|
debouncedAutosize.cancel();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Handlers for composition events, which are crucial for proper IME behavior
|
// Handlers for composition events, which are crucial for proper IME behavior
|
||||||
const handleCompositionStart = (evt: React.CompositionEvent<HTMLTextAreaElement>) => {
|
const handleCompositionStart = (evt: React.CompositionEvent<HTMLTextAreaElement>) => {
|
||||||
setIsComposing(true);
|
setIsComposing(true);
|
||||||
@@ -73,7 +101,7 @@ export default function Input({
|
|||||||
|
|
||||||
// Save current input if we're just starting to navigate history
|
// Save current input if we're just starting to navigate history
|
||||||
if (historyIndex === -1) {
|
if (historyIndex === -1) {
|
||||||
setSavedInput(value);
|
setSavedInput(displayValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate new history index
|
// Calculate new history index
|
||||||
@@ -98,8 +126,10 @@ export default function Input({
|
|||||||
setHistoryIndex(newIndex);
|
setHistoryIndex(newIndex);
|
||||||
if (newIndex === -1) {
|
if (newIndex === -1) {
|
||||||
// Restore saved input when going past the end of history
|
// Restore saved input when going past the end of history
|
||||||
|
setDisplayValue(savedInput);
|
||||||
setValue(savedInput);
|
setValue(savedInput);
|
||||||
} else {
|
} else {
|
||||||
|
setDisplayValue(commandHistory[newIndex] || '');
|
||||||
setValue(commandHistory[newIndex] || '');
|
setValue(commandHistory[newIndex] || '');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -118,7 +148,9 @@ export default function Input({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (evt.altKey) {
|
if (evt.altKey) {
|
||||||
setValue(value + '\n');
|
const newValue = displayValue + '\n';
|
||||||
|
setDisplayValue(newValue);
|
||||||
|
setValue(newValue);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,8 +159,9 @@ export default function Input({
|
|||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
||||||
// Only submit if not loading and has content
|
// Only submit if not loading and has content
|
||||||
if (!isLoading && value.trim()) {
|
if (!isLoading && displayValue.trim()) {
|
||||||
handleSubmit(new CustomEvent('submit', { detail: { value } }));
|
handleSubmit(new CustomEvent('submit', { detail: { value: displayValue } }));
|
||||||
|
setDisplayValue('');
|
||||||
setValue('');
|
setValue('');
|
||||||
setHistoryIndex(-1);
|
setHistoryIndex(-1);
|
||||||
setSavedInput('');
|
setSavedInput('');
|
||||||
@@ -138,8 +171,9 @@ export default function Input({
|
|||||||
|
|
||||||
const onFormSubmit = (e: React.FormEvent) => {
|
const onFormSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (value.trim() && !isLoading) {
|
if (displayValue.trim() && !isLoading) {
|
||||||
handleSubmit(new CustomEvent('submit', { detail: { value } }));
|
handleSubmit(new CustomEvent('submit', { detail: { value: displayValue } }));
|
||||||
|
setDisplayValue('');
|
||||||
setValue('');
|
setValue('');
|
||||||
setHistoryIndex(-1);
|
setHistoryIndex(-1);
|
||||||
setSavedInput('');
|
setSavedInput('');
|
||||||
@@ -150,10 +184,9 @@ export default function Input({
|
|||||||
const path = await window.electron.selectFileOrDirectory();
|
const path = await window.electron.selectFileOrDirectory();
|
||||||
if (path) {
|
if (path) {
|
||||||
// Append the path to existing text, with a space if there's existing text
|
// Append the path to existing text, with a space if there's existing text
|
||||||
setValue((prev) => {
|
const newValue = displayValue.trim() ? `${displayValue.trim()} ${path}` : path;
|
||||||
const currentText = prev.trim();
|
setDisplayValue(newValue);
|
||||||
return currentText ? `${currentText} ${path}` : path;
|
setValue(newValue);
|
||||||
});
|
|
||||||
textAreaRef.current?.focus();
|
textAreaRef.current?.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -167,7 +200,7 @@ export default function Input({
|
|||||||
autoFocus
|
autoFocus
|
||||||
id="dynamic-textarea"
|
id="dynamic-textarea"
|
||||||
placeholder="What can goose help with? ⌘↑/⌘↓"
|
placeholder="What can goose help with? ⌘↑/⌘↓"
|
||||||
value={value}
|
value={displayValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onCompositionStart={handleCompositionStart}
|
onCompositionStart={handleCompositionStart}
|
||||||
onCompositionEnd={handleCompositionEnd}
|
onCompositionEnd={handleCompositionEnd}
|
||||||
@@ -209,9 +242,9 @@ export default function Input({
|
|||||||
type="submit"
|
type="submit"
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
disabled={!value.trim()}
|
disabled={!displayValue.trim()}
|
||||||
className={`absolute right-2 top-1/2 -translate-y-1/2 text-textSubtle hover:text-textStandard ${
|
className={`absolute right-2 top-1/2 -translate-y-1/2 text-textSubtle hover:text-textStandard ${
|
||||||
!value.trim() ? 'text-textSubtle cursor-not-allowed' : ''
|
!displayValue.trim() ? 'text-textSubtle cursor-not-allowed' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Send />
|
<Send />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, KeyboardEvent, useState } from 'react';
|
import React, { useEffect, KeyboardEvent, useState, useCallback } from 'react';
|
||||||
import { Search as SearchIcon } from 'lucide-react';
|
import { Search as SearchIcon } from 'lucide-react';
|
||||||
import { ArrowDown, ArrowUp, Close } from '../icons';
|
import { ArrowDown, ArrowUp, Close } from '../icons';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for the SearchBar component
|
* Props for the SearchBar component
|
||||||
@@ -27,6 +28,7 @@ interface SearchBarProps {
|
|||||||
* - Navigation between results with arrows
|
* - Navigation between results with arrows
|
||||||
* - Keyboard shortcuts (↑/↓ for navigation, Esc to close)
|
* - Keyboard shortcuts (↑/↓ for navigation, Esc to close)
|
||||||
* - Smooth animations for enter/exit
|
* - Smooth animations for enter/exit
|
||||||
|
* - Debounced search for better performance
|
||||||
*/
|
*/
|
||||||
export const SearchBar: React.FC<SearchBarProps> = ({
|
export const SearchBar: React.FC<SearchBarProps> = ({
|
||||||
onSearch,
|
onSearch,
|
||||||
@@ -35,18 +37,33 @@ export const SearchBar: React.FC<SearchBarProps> = ({
|
|||||||
searchResults,
|
searchResults,
|
||||||
}) => {
|
}) => {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [displayTerm, setDisplayTerm] = useState(''); // For immediate visual feedback
|
||||||
const [caseSensitive, setCaseSensitive] = useState(false);
|
const [caseSensitive, setCaseSensitive] = useState(false);
|
||||||
const [isExiting, setIsExiting] = useState(false);
|
const [isExiting, setIsExiting] = useState(false);
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Create debounced search function
|
||||||
|
const debouncedSearch = useCallback(
|
||||||
|
debounce((term: string, caseSensitive: boolean) => {
|
||||||
|
onSearch(term, caseSensitive);
|
||||||
|
}, 150),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
|
|
||||||
|
// Cleanup debounced function
|
||||||
|
return () => {
|
||||||
|
debouncedSearch.cancel();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
setSearchTerm(value);
|
setDisplayTerm(value); // Update display immediately
|
||||||
onSearch(value, caseSensitive);
|
setSearchTerm(value); // Update actual term
|
||||||
|
debouncedSearch(value, caseSensitive);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
@@ -67,13 +84,16 @@ export const SearchBar: React.FC<SearchBarProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleCaseSensitive = () => {
|
const toggleCaseSensitive = () => {
|
||||||
setCaseSensitive(!caseSensitive);
|
const newCaseSensitive = !caseSensitive;
|
||||||
onSearch(searchTerm, !caseSensitive);
|
setCaseSensitive(newCaseSensitive);
|
||||||
|
// Immediately trigger a new search with updated case sensitivity
|
||||||
|
debouncedSearch(searchTerm, newCaseSensitive);
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsExiting(true);
|
setIsExiting(true);
|
||||||
|
debouncedSearch.cancel(); // Cancel any pending searches
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onClose();
|
onClose();
|
||||||
}, 150); // Match animation duration
|
}, 150); // Match animation duration
|
||||||
@@ -93,7 +113,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({
|
|||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
id="search-input"
|
id="search-input"
|
||||||
type="text"
|
type="text"
|
||||||
value={searchTerm}
|
value={displayTerm}
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Search conversation..."
|
placeholder="Search conversation..."
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect, PropsWithChildren } from 'react';
|
import React, { useState, useEffect, PropsWithChildren, useCallback } from 'react';
|
||||||
import { SearchBar } from './SearchBar';
|
import { SearchBar } from './SearchBar';
|
||||||
import { SearchHighlighter } from '../../utils/searchHighlighter';
|
import { SearchHighlighter } from '../../utils/searchHighlighter';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
import '../../styles/search.css';
|
import '../../styles/search.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,6 +15,7 @@ interface SearchViewProps {
|
|||||||
/**
|
/**
|
||||||
* SearchView wraps content in a searchable container with a search bar that appears
|
* SearchView wraps content in a searchable container with a search bar that appears
|
||||||
* when Cmd/Ctrl+F is pressed. Supports case-sensitive search and result navigation.
|
* when Cmd/Ctrl+F is pressed. Supports case-sensitive search and result navigation.
|
||||||
|
* Features debounced search for better performance with large content.
|
||||||
*/
|
*/
|
||||||
export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
|
export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
|
||||||
className = '',
|
className = '',
|
||||||
@@ -27,14 +29,38 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
|
|||||||
|
|
||||||
const highlighterRef = React.useRef<SearchHighlighter | null>(null);
|
const highlighterRef = React.useRef<SearchHighlighter | null>(null);
|
||||||
const containerRef = React.useRef<HTMLDivElement | null>(null);
|
const containerRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
const lastSearchRef = React.useRef<{ term: string; caseSensitive: boolean }>({
|
||||||
|
term: '',
|
||||||
|
caseSensitive: false,
|
||||||
|
});
|
||||||
|
|
||||||
// Clean up highlighter on unmount
|
// Create debounced highlight function
|
||||||
|
const debouncedHighlight = useCallback(
|
||||||
|
debounce((term: string, caseSensitive: boolean, highlighter: SearchHighlighter) => {
|
||||||
|
const highlights = highlighter.highlight(term, caseSensitive);
|
||||||
|
const count = highlights.length;
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
setSearchResults({
|
||||||
|
currentIndex: 1,
|
||||||
|
count,
|
||||||
|
});
|
||||||
|
highlighter.setCurrentMatch(0, true); // Explicitly scroll when setting initial match
|
||||||
|
} else {
|
||||||
|
setSearchResults(null);
|
||||||
|
}
|
||||||
|
}, 150),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clean up highlighter and debounced functions on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (highlighterRef.current) {
|
if (highlighterRef.current) {
|
||||||
highlighterRef.current.destroy();
|
highlighterRef.current.destroy();
|
||||||
highlighterRef.current = null;
|
highlighterRef.current = null;
|
||||||
}
|
}
|
||||||
|
debouncedHighlight.cancel();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -54,10 +80,14 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the search operation when a user enters a search term.
|
* Handles the search operation when a user enters a search term.
|
||||||
|
* Uses debouncing to prevent excessive highlighting operations.
|
||||||
* @param term - The text to search for
|
* @param term - The text to search for
|
||||||
* @param caseSensitive - Whether to perform a case-sensitive search
|
* @param caseSensitive - Whether to perform a case-sensitive search
|
||||||
*/
|
*/
|
||||||
const handleSearch = (term: string, caseSensitive: boolean) => {
|
const handleSearch = (term: string, caseSensitive: boolean) => {
|
||||||
|
// Store the latest search parameters
|
||||||
|
lastSearchRef.current = { term, caseSensitive };
|
||||||
|
|
||||||
if (!term) {
|
if (!term) {
|
||||||
setSearchResults(null);
|
setSearchResults(null);
|
||||||
if (highlighterRef.current) {
|
if (highlighterRef.current) {
|
||||||
@@ -71,29 +101,25 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
|
|||||||
|
|
||||||
if (!highlighterRef.current) {
|
if (!highlighterRef.current) {
|
||||||
highlighterRef.current = new SearchHighlighter(container, (count) => {
|
highlighterRef.current = new SearchHighlighter(container, (count) => {
|
||||||
if (count > 0) {
|
// Only update if this is still the latest search
|
||||||
setSearchResults((prev) => ({
|
if (
|
||||||
currentIndex: prev?.currentIndex || 1,
|
lastSearchRef.current.term === term &&
|
||||||
count,
|
lastSearchRef.current.caseSensitive === caseSensitive
|
||||||
}));
|
) {
|
||||||
} else {
|
if (count > 0) {
|
||||||
setSearchResults(null);
|
setSearchResults((prev) => ({
|
||||||
|
currentIndex: prev?.currentIndex || 1,
|
||||||
|
count,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setSearchResults(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const highlights = highlighterRef.current.highlight(term, caseSensitive);
|
// Debounce the highlight operation
|
||||||
const count = highlights.length;
|
debouncedHighlight(term, caseSensitive, highlighterRef.current);
|
||||||
|
|
||||||
if (count > 0) {
|
|
||||||
setSearchResults({
|
|
||||||
currentIndex: 1,
|
|
||||||
count,
|
|
||||||
});
|
|
||||||
highlighterRef.current.setCurrentMatch(0, true); // Explicitly scroll when setting initial match
|
|
||||||
} else {
|
|
||||||
setSearchResults(null);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,7 +153,7 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the search interface and clears all highlights.
|
* Closes the search interface and cleans up highlights.
|
||||||
*/
|
*/
|
||||||
const handleCloseSearch = () => {
|
const handleCloseSearch = () => {
|
||||||
setIsSearchVisible(false);
|
setIsSearchVisible(false);
|
||||||
@@ -135,6 +161,8 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
|
|||||||
if (highlighterRef.current) {
|
if (highlighterRef.current) {
|
||||||
highlighterRef.current.clearHighlights();
|
highlighterRef.current.clearHighlights();
|
||||||
}
|
}
|
||||||
|
// Cancel any pending highlight operations
|
||||||
|
debouncedHighlight.cancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user