feat(ui): migrate all shortcut surfaces to the centralized registry

This commit is contained in:
Bohdan Triapitsyn
2026-08-26 10:59:04 +03:00
parent f1c3870909
commit 94f6b5fd38
24 changed files with 1057 additions and 904 deletions
+37 -64
View File
@@ -45,7 +45,7 @@ import {
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
import { useFileSearchStore } from '@/stores/useFileSearchStore';
import { useDeviceInfo } from '@/lib/device';
import { cn, getModifierLabel, getRevealLabelKey, hasModifier } from '@/lib/utils';
import { cn, getRevealLabelKey } from '@/lib/utils';
import { getLanguageFromExtension, getImageMimeType, isBinaryFile, isDrawioFile, isImageFile, isPdfFile, isSvgFile, looksLikeBinaryText } from '@/lib/toolHelpers';
import { shouldAllowFileDraftSave, shouldScheduleFileAutosave } from '@/lib/fileEditorAutosave';
import { getRuntimeUrlResolver } from '@/lib/runtime-url';
@@ -75,7 +75,8 @@ import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
import { getDefaultTheme } from '@/lib/theme/themes';
import { isBrowserClientRuntime, openDesktopFileInApp, openDesktopPath } from '@/lib/desktop';
import { useOpenInAppsStore } from '@/stores/useOpenInAppsStore';
import { eventMatchesShortcut, getEffectiveShortcutCombo } from '@/lib/shortcuts';
import { useKeybind, useKeybinds } from '@/hooks/useKeybind';
import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
import { useI18n } from '@/lib/i18n';
import { sessionEvents } from '@/lib/sessionEvents';
import { syncScheduledTaskLoops } from '@/lib/scheduledTasksApi';
@@ -1032,7 +1033,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const setPendingFileNavigation = useUIStore((state) => state.setPendingFileNavigation);
const pendingFileFocusPath = useUIStore((state) => state.pendingFileFocusPath);
const setPendingFileFocusPath = useUIStore((state) => state.setPendingFileFocusPath);
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
const fileEditorKeymap = useUIStore((state) => state.fileEditorKeymap);
const settingsDefaultFileViewerPreview = useConfigStore((state) => state.settingsDefaultFileViewerPreview);
const showMessageTTSButtons = useConfigStore((state) => state.showMessageTTSButtons);
@@ -1759,35 +1759,28 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
setAutoSaveStatus('idle');
}, [selectedFile?.path]);
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!hasModifier(e)) {
return;
}
useKeybinds({
save_file: (event) => {
if (!(event.target instanceof Node) || !editorWrapperRef.current?.contains(event.target)) return false;
if (e.key.toLowerCase() === 's') {
e.preventDefault();
// Cancel pending auto-save; user wants immediate save
if (autoSaveTimerRef.current) {
clearTimeout(autoSaveTimerRef.current);
autoSaveTimerRef.current = null;
}
if (!isSaving) {
void saveDraft().then((saved) => {
if (!saved) return;
setAutoSaveStatus('saved');
setTimeout(() => setAutoSaveStatus('idle'), 2000);
});
}
} else if (e.key.toLowerCase() === 'f') {
e.preventDefault();
setIsSearchOpen(true);
// Cancel pending auto-save because the explicit save should run immediately.
if (autoSaveTimerRef.current) {
clearTimeout(autoSaveTimerRef.current);
autoSaveTimerRef.current = null;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isSaving, saveDraft]);
if (!isSaving) {
void saveDraft().then((saved) => {
if (!saved) return;
setAutoSaveStatus('saved');
setTimeout(() => setAutoSaveStatus('idle'), 2000);
});
}
},
find_in_file: (event) => {
if (!(event.target instanceof Node) || !editorWrapperRef.current?.contains(event.target)) return false;
setIsSearchOpen(true);
},
});
const loadSelectedFile = React.useCallback(async (node: FileNode) => {
const loadId = activeFileLoadIdRef.current + 1;
@@ -2906,42 +2899,21 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
};
}, [isMobile, nudgeEditorSelectionAboveKeyboard]);
React.useEffect(() => {
useKeybind('open_go_to_line', (event) => {
if (!canEdit || textViewMode !== 'edit' || isMobile) {
return;
return false;
}
const goToLineCombo = getEffectiveShortcutCombo('open_go_to_line', shortcutOverrides);
const target = event.target as Element | null;
if (target?.closest('[role="dialog"]')) return false;
if (!(target instanceof Node) || !editorWrapperRef.current?.contains(target)) return false;
const handleKeyDown = (event: KeyboardEvent) => {
const target = event.target as Element | null;
if (target?.closest('[role="dialog"]')) {
return;
}
const isEditorTarget = Boolean(target?.closest('.cm-editor'));
const isTypingTarget = Boolean(target?.closest('input, textarea, [contenteditable="true"], [role="textbox"]'));
if (isTypingTarget && !isEditorTarget) return false;
const isEditorTarget = Boolean(target?.closest('.cm-editor'));
const isTypingTarget = Boolean(
target?.closest('input, textarea, [contenteditable="true"], [role="textbox"]')
);
if (isTypingTarget && !isEditorTarget) {
return;
}
const activeElement = document.activeElement as Element | null;
const editorHasFocus = Boolean(activeElement?.closest('.cm-editor'));
if (!editorHasFocus) {
return;
}
if (eventMatchesShortcut(event, goToLineCombo)) {
event.preventDefault();
setIsGoToLineOpen(true);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [canEdit, isMobile, shortcutOverrides, textViewMode]);
setIsGoToLineOpen(true);
});
const editorFontSize = useUIStore((state) => state.editorFontSize);
@@ -3196,6 +3168,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
}
const docked = layout === 'docked';
const saveShortcut = formatShortcutForDisplay(getEffectiveShortcutCombo('save_file'));
const wrapperCls = docked
? 'pointer-events-auto flex flex-wrap items-center gap-1'
: 'pointer-events-auto flex items-center gap-1 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-1 shadow-sm';
@@ -3225,14 +3198,14 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
<Icon name="check" className="size-3.5" />
{t('filesView.editor.saved')}
</span>
) : isDirty ? withTooltip(t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: `${getModifierLabel()}+S` }),
) : isDirty ? withTooltip(t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: saveShortcut }),
<Button
variant="ghost"
size="sm"
onClick={() => void saveDraft()}
className="h-6 gap-1 px-1 text-muted-foreground opacity-80 hover:bg-transparent hover:opacity-100 focus-visible:bg-transparent active:bg-transparent"
title={t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: `${getModifierLabel()}+S` })}
aria-label={t('filesView.editor.saveAria', { shortcut: `${getModifierLabel()}+S` })}
title={t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: saveShortcut })}
aria-label={t('filesView.editor.saveAria', { shortcut: saveShortcut })}
>
<Icon name="save-3" className="size-4" />
</Button>