feat: add Cmd+Shift+T shortcut, menu item, and full wiring
Add "Reopen Closed Tab" to File menu with CmdOrCtrl+Shift+T accelerator. Wire onReopenClosedTab through useAppKeyboard, useMenuEvents, useAppCommands, and App.tsx. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ const FILE_DAILY_NOTE: &str = "file-daily-note";
|
||||
const FILE_QUICK_OPEN: &str = "file-quick-open";
|
||||
const FILE_SAVE: &str = "file-save";
|
||||
const FILE_CLOSE_TAB: &str = "file-close-tab";
|
||||
const FILE_REOPEN_CLOSED_TAB: &str = "file-reopen-closed-tab";
|
||||
|
||||
const EDIT_FIND_IN_VAULT: &str = "edit-find-in-vault";
|
||||
const EDIT_TOGGLE_RAW_EDITOR: &str = "edit-toggle-raw-editor";
|
||||
@@ -62,6 +63,7 @@ const CUSTOM_IDS: &[&str] = &[
|
||||
FILE_QUICK_OPEN,
|
||||
FILE_SAVE,
|
||||
FILE_CLOSE_TAB,
|
||||
FILE_REOPEN_CLOSED_TAB,
|
||||
EDIT_FIND_IN_VAULT,
|
||||
EDIT_TOGGLE_RAW_EDITOR,
|
||||
EDIT_TOGGLE_DIFF,
|
||||
@@ -167,6 +169,10 @@ fn build_file_menu(app: &App) -> MenuResult {
|
||||
.id(FILE_CLOSE_TAB)
|
||||
.accelerator("CmdOrCtrl+W")
|
||||
.build(app)?;
|
||||
let reopen_closed_tab = MenuItemBuilder::new("Reopen Closed Tab")
|
||||
.id(FILE_REOPEN_CLOSED_TAB)
|
||||
.accelerator("CmdOrCtrl+Shift+T")
|
||||
.build(app)?;
|
||||
|
||||
Ok(SubmenuBuilder::new(app, "File")
|
||||
.item(&new_note)
|
||||
@@ -176,6 +182,7 @@ fn build_file_menu(app: &App) -> MenuResult {
|
||||
.separator()
|
||||
.item(&save)
|
||||
.item(&close_tab)
|
||||
.item(&reopen_closed_tab)
|
||||
.build()?)
|
||||
}
|
||||
|
||||
|
||||
@@ -596,6 +596,7 @@ function App() {
|
||||
onInstallMcp: installMcp,
|
||||
onEmptyTrash: handleEmptyTrash,
|
||||
trashedCount,
|
||||
onReopenClosedTab: notes.handleReopenClosedTab,
|
||||
onReindexVault: indexing.triggerFullReindex,
|
||||
onReloadVault: vault.reloadVault,
|
||||
onRepairVault: handleRepairVault,
|
||||
|
||||
@@ -67,6 +67,7 @@ interface AppCommandsConfig {
|
||||
onInstallMcp?: () => void
|
||||
onEmptyTrash?: () => void
|
||||
trashedCount?: number
|
||||
onReopenClosedTab?: () => void
|
||||
onReindexVault?: () => void
|
||||
onReloadVault?: () => void
|
||||
onRepairVault?: () => void
|
||||
@@ -116,6 +117,7 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] {
|
||||
onGoForward: config.onGoForward,
|
||||
onToggleAIChat: config.onToggleAIChat,
|
||||
onToggleRawEditor: config.onToggleRawEditor,
|
||||
onReopenClosedTab: config.onReopenClosedTab,
|
||||
activeTabPathRef: config.activeTabPathRef,
|
||||
handleCloseTabRef: config.handleCloseTabRef,
|
||||
})
|
||||
@@ -156,6 +158,7 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] {
|
||||
onReloadVault: config.onReloadVault,
|
||||
onRepairVault: config.onRepairVault,
|
||||
onEmptyTrash: config.onEmptyTrash,
|
||||
onReopenClosedTab: config.onReopenClosedTab,
|
||||
activeTabPathRef: config.activeTabPathRef,
|
||||
handleCloseTabRef: config.handleCloseTabRef,
|
||||
activeTabPath: config.activeTabPath,
|
||||
|
||||
@@ -183,6 +183,23 @@ describe('useAppKeyboard', () => {
|
||||
expect(actions.onZoomReset).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Cmd+Shift+T triggers reopen closed tab', () => {
|
||||
const actions = makeActions()
|
||||
const onReopenClosedTab = vi.fn()
|
||||
renderHook(() => useAppKeyboard({ ...actions, onReopenClosedTab }))
|
||||
fireKey('t', { metaKey: true, shiftKey: true })
|
||||
expect(onReopenClosedTab).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Cmd+Shift+T does not trigger other shortcuts', () => {
|
||||
const actions = makeActions()
|
||||
const onReopenClosedTab = vi.fn()
|
||||
renderHook(() => useAppKeyboard({ ...actions, onReopenClosedTab }))
|
||||
fireKey('t', { metaKey: true, shiftKey: true })
|
||||
expect(actions.onQuickOpen).not.toHaveBeenCalled()
|
||||
expect(actions.onCreateNote).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Cmd+I triggers toggle AI chat', () => {
|
||||
const actions = makeActions()
|
||||
const onToggleAIChat = vi.fn()
|
||||
|
||||
@@ -19,6 +19,7 @@ interface KeyboardActions {
|
||||
onGoForward?: () => void
|
||||
onToggleAIChat?: () => void
|
||||
onToggleRawEditor?: () => void
|
||||
onReopenClosedTab?: () => void
|
||||
activeTabPathRef: React.MutableRefObject<string | null>
|
||||
handleCloseTabRef: React.MutableRefObject<(path: string) => void>
|
||||
}
|
||||
@@ -64,7 +65,7 @@ function handleCmdKey(e: KeyboardEvent, keyMap: Record<string, ShortcutHandler>)
|
||||
|
||||
export function useAppKeyboard({
|
||||
onQuickOpen, onCommandPalette, onSearch, onCreateNote, onOpenDailyNote, onSave, onOpenSettings, onTrashNote, onArchiveNote,
|
||||
onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor, activeTabPathRef, handleCloseTabRef,
|
||||
onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor, onReopenClosedTab, activeTabPathRef, handleCloseTabRef,
|
||||
}: KeyboardActions) {
|
||||
useEffect(() => {
|
||||
const withActiveTab = (fn: (path: string) => void): ShortcutHandler => () => {
|
||||
@@ -100,11 +101,17 @@ export function useAppKeyboard({
|
||||
onSearch()
|
||||
return
|
||||
}
|
||||
// Cmd+Shift+T: reopen last closed tab
|
||||
if ((e.metaKey || e.ctrlKey) && e.shiftKey && (e.key === 't' || e.key === 'T')) {
|
||||
e.preventDefault()
|
||||
onReopenClosedTab?.()
|
||||
return
|
||||
}
|
||||
if (!handleViewModeKey(e, onSetViewMode)) {
|
||||
handleCmdKey(e, cmdKeyMap)
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [onQuickOpen, onCommandPalette, onSearch, onCreateNote, onOpenDailyNote, onSave, onOpenSettings, onTrashNote, onArchiveNote, activeTabPathRef, handleCloseTabRef, onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor])
|
||||
}, [onQuickOpen, onCommandPalette, onSearch, onCreateNote, onOpenDailyNote, onSave, onOpenSettings, onTrashNote, onArchiveNote, activeTabPathRef, handleCloseTabRef, onSetViewMode, onZoomIn, onZoomOut, onZoomReset, onGoBack, onGoForward, onToggleAIChat, onToggleRawEditor, onReopenClosedTab])
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ function makeHandlers(): MenuEventHandlers {
|
||||
onInstallMcp: vi.fn(),
|
||||
onReindexVault: vi.fn(),
|
||||
onReloadVault: vi.fn(),
|
||||
onReopenClosedTab: vi.fn(),
|
||||
activeTabPathRef: { current: '/vault/test.md' } as React.MutableRefObject<string | null>,
|
||||
handleCloseTabRef: { current: vi.fn() } as React.MutableRefObject<(path: string) => void>,
|
||||
activeTabPath: '/vault/test.md',
|
||||
@@ -313,6 +314,13 @@ describe('dispatchMenuEvent', () => {
|
||||
expect(h.onReloadVault).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// File menu: reopen closed tab
|
||||
it('file-reopen-closed-tab triggers reopen closed tab', () => {
|
||||
const h = makeHandlers()
|
||||
dispatchMenuEvent('file-reopen-closed-tab', h)
|
||||
expect(h.onReopenClosedTab).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Edge cases
|
||||
it('unknown event ID does nothing', () => {
|
||||
const h = makeHandlers()
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface MenuEventHandlers {
|
||||
onResolveConflicts?: () => void
|
||||
onViewChanges?: () => void
|
||||
onInstallMcp?: () => void
|
||||
onReopenClosedTab?: () => void
|
||||
onReindexVault?: () => void
|
||||
onReloadVault?: () => void
|
||||
onRepairVault?: () => void
|
||||
@@ -83,6 +84,7 @@ type OptionalHandler =
|
||||
| 'onCreateTheme' | 'onRestoreDefaultThemes'
|
||||
| 'onCommitPush' | 'onResolveConflicts' | 'onViewChanges' | 'onInstallMcp' | 'onReindexVault' | 'onReloadVault' | 'onRepairVault'
|
||||
| 'onEmptyTrash'
|
||||
| 'onReopenClosedTab'
|
||||
|
||||
const OPTIONAL_EVENT_MAP: Record<string, OptionalHandler> = {
|
||||
'view-go-back': 'onGoBack',
|
||||
@@ -105,6 +107,7 @@ const OPTIONAL_EVENT_MAP: Record<string, OptionalHandler> = {
|
||||
'vault-reload': 'onReloadVault',
|
||||
'vault-repair': 'onRepairVault',
|
||||
'note-empty-trash': 'onEmptyTrash',
|
||||
'file-reopen-closed-tab': 'onReopenClosedTab',
|
||||
}
|
||||
|
||||
function dispatchActiveTabEvent(id: string, h: MenuEventHandlers): boolean {
|
||||
|
||||
Reference in New Issue
Block a user