From ceba02ed471f60bd860c6c9a51afc2538000e499 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Wed, 30 Apr 2025 19:26:54 -0700 Subject: [PATCH] fix jittery @ to mention --- .../void/browser/react/src/util/inputs.tsx | 97 ++++++++++++------- 1 file changed, 63 insertions(+), 34 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 9a01792f..32ff1cf7 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -55,12 +55,12 @@ export const WidgetComponent = ({ ctor, prop type GenerateNextOptions = (optionText: string) => Promise type Option = { - nameInMenu: string, + fullName: string, iconInMenu: ForwardRefExoticComponent & RefAttributes>, // type for lucide-react components } & ( - | { nextOptions: Option[], generateNextOptions?: undefined, nameToPaste?: undefined } - | { nextOptions?: undefined, generateNextOptions: GenerateNextOptions, nameToPaste?: undefined } - | { leafNodeType: 'File' | 'Folder', nameToPaste: string, uri: URI, nextOptions?: undefined, generateNextOptions?: undefined, } + | { nextOptions: Option[], generateNextOptions?: undefined, abbreviatedName?: undefined } + | { nextOptions?: undefined, generateNextOptions: GenerateNextOptions, abbreviatedName?: undefined } + | { leafNodeType: 'File' | 'Folder', abbreviatedName: string, uri: URI, nextOptions?: undefined, generateNextOptions?: undefined, } ) @@ -173,6 +173,13 @@ export function getRelativeWorkspacePath(accessor: ReturnType { + return getBasename(relativePath, 2) +} + const getOptionsAtPath = async (accessor: ReturnType, path: string[], optionText: string): Promise => { const toolsService = accessor.get('IToolsService') @@ -193,8 +200,8 @@ const getOptionsAtPath = async (accessor: ReturnType, path: leafNodeType: 'File', uri: uri, iconInMenu: File, - nameInMenu: relativePath, - nameToPaste: getBasename(relativePath, 2), + fullName: relativePath, + abbreviatedName: getAbbreviatedName(relativePath), } }) return res @@ -258,8 +265,8 @@ const getOptionsAtPath = async (accessor: ReturnType, path: leafNodeType: 'Folder', uri: uri, iconInMenu: Folder, // Folder - nameInMenu: relativePath, - nameToPaste: getBasename(relativePath, 2) + fullName: relativePath, + abbreviatedName: getAbbreviatedName(relativePath), })) satisfies Option[]; } } catch (error) { @@ -271,13 +278,13 @@ const getOptionsAtPath = async (accessor: ReturnType, path: const allOptions: Option[] = [ { - nameInMenu: 'files', + fullName: 'files', iconInMenu: File, generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'files')) || [], }, { - nameInMenu: 'folders', - iconInMenu: FolderClosed, + fullName: 'folders', + iconInMenu: Folder, generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'folders')) || [], }, ] @@ -289,7 +296,7 @@ const getOptionsAtPath = async (accessor: ReturnType, path: for (const pn of path) { - const selectedOption = nextOptionsAtPath.find(o => o.nameInMenu.toLowerCase() === pn.toLowerCase()) + const selectedOption = nextOptionsAtPath.find(o => o.fullName.toLowerCase() === pn.toLowerCase()) if (!selectedOption) return []; @@ -304,10 +311,10 @@ const getOptionsAtPath = async (accessor: ReturnType, path: } const optionsAtPath = nextOptionsAtPath - .filter(o => isSubsequence(o.nameInMenu, optionText)) + .filter(o => isSubsequence(o.fullName, optionText)) .sort((a, b) => { // this is a hack but good for now - const scoreA = scoreSubsequence(a.nameInMenu, optionText); - const scoreB = scoreSubsequence(b.nameInMenu, optionText); + const scoreA = scoreSubsequence(a.fullName, optionText); + const scoreB = scoreSubsequence(b.fullName, optionText); return scoreB - scoreA; }) .slice(0, numOptionsToShow) // should go last because sorting/filtering should happen on all datapoints @@ -354,6 +361,12 @@ export const VoidInputBox2 = forwardRef(fun const [optionIdx, setOptionIdx] = useState(0); const [options, setOptions] = useState([]); const [optionText, setOptionText] = useState(''); + const [didLoadInitialOptions, setDidLoadInitialOptions] = useState(false); + + const currentPathRef = useRef(JSON.stringify([])); + const areBreadcrumbsShowing = didLoadInitialOptions && optionPath.length >= 1; + + const insertTextAtCursor = (text: string) => { const textarea = textAreaRef.current; if (!textarea) return; @@ -379,15 +392,12 @@ export const VoidInputBox2 = forwardRef(fun if (!options.length) { return; } const option = options[optionIdx]; - const newPath = [...optionPath, option.nameInMenu] + const newPath = [...optionPath, option.fullName] const isLastOption = !option.generateNextOptions && !option.nextOptions - - setOptionPath(newPath) - setOptionText('') - setOptionIdx(0) + setDidLoadInitialOptions(false) if (isLastOption) { setIsMenuOpen(false) - insertTextAtCursor(option.nameToPaste) + insertTextAtCursor(option.abbreviatedName) const newSelection: StagingSelectionItem = option.leafNodeType === 'File' ? { type: 'File', @@ -404,26 +414,39 @@ export const VoidInputBox2 = forwardRef(fun console.log('selected', option.uri?.fsPath) } else { + + + currentPathRef.current = JSON.stringify(newPath); const newOpts = await getOptionsAtPath(accessor, newPath, '') || [] + if (currentPathRef.current !== JSON.stringify(newPath)) { return; } + setOptionPath(newPath) + setOptionText('') + setOptionIdx(0) setOptions(newOpts) + setDidLoadInitialOptions(true) } } const onRemoveOption = async () => { const newPath = [...optionPath.slice(0, optionPath.length - 1)] + currentPathRef.current = JSON.stringify(newPath); + const newOpts = await getOptionsAtPath(accessor, newPath, '') || [] + if (currentPathRef.current !== JSON.stringify(newPath)) { return; } setOptionPath(newPath) setOptionText('') setOptionIdx(0) - const newOpts = await getOptionsAtPath(accessor, newPath, '') || [] setOptions(newOpts) } const onOpenOptionMenu = async () => { - setOptionPath([]) + const newPath: [] = [] + currentPathRef.current = JSON.stringify([]); + const newOpts = await getOptionsAtPath(accessor, [], '') || [] + if (currentPathRef.current !== JSON.stringify([])) { return; } + setOptionPath(newPath) setOptionText('') setIsMenuOpen(true); setOptionIdx(0); - const newOpts = await getOptionsAtPath(accessor, [], '') || [] setOptions(newOpts); } const onCloseOptionMenu = () => { @@ -469,15 +492,19 @@ export const VoidInputBox2 = forwardRef(fun // debounced const onPathTextChange = useCallback((newStr: string) => { + setOptionText(newStr); if (debounceTimerRef.current !== null) { window.clearTimeout(debounceTimerRef.current); } + currentPathRef.current = JSON.stringify(optionPath); + // Set a new timeout to fetch options after a delay debounceTimerRef.current = window.setTimeout(async () => { const newOpts = await getOptionsAtPath(accessor, optionPath, newStr) || []; + if (currentPathRef.current !== JSON.stringify(optionPath)) { return; } setOptions(newOpts); setOptionIdx(0); debounceTimerRef.current = null; @@ -537,7 +564,9 @@ export const VoidInputBox2 = forwardRef(fun // do nothing } else { // letter - onPathTextChange(optionText + e.key) + if (areBreadcrumbsShowing) { + onPathTextChange(optionText + e.key) + } } } @@ -740,20 +769,20 @@ export const VoidInputBox2 = forwardRef(fun onWheel={(e) => e.stopPropagation()} > {/* Breadcrumbs Header */} -
- {optionPath.length || optionText ? + {areBreadcrumbsShowing &&
+ {optionText ?
- {optionPath.map((path, index) => ( + {/* {optionPath.map((path, index) => ( {path} - ))} + ))} */} {optionText}
- :
Enter text to filter...
+ :
Enter text to filter...
} -
+
} {/* Options list */} @@ -767,17 +796,17 @@ export const VoidInputBox2 = forwardRef(fun // Option
{ onSelectOption(); }} - onMouseOver={() => { setOptionIdx(oIdx) }} + onMouseMove={() => { setOptionIdx(oIdx) }} > {} - {o.nameInMenu} + {o.fullName} {o.nextOptions || o.generateNextOptions ? ( ) : null}