@@ -5,7 +5,7 @@ import type { ContextTarget } from './components'
|
|||||||
import { Editor, SplitView } from './editor'
|
import { Editor, SplitView } from './editor'
|
||||||
import type { Cursor, Mode, Selection } from './editor'
|
import type { Cursor, Mode, Selection } from './editor'
|
||||||
import { Terminal, lid } from './terminals'
|
import { Terminal, lid } from './terminals'
|
||||||
import { ConfirmModal, ContextMenu, HelpModal, HistoryModal, NamePopup, PassPopup, SearchModal, Toasts } from './overlays'
|
import { ConfirmModal, ContextMenu, HelpModal, HistoryModal, NamePopup, PassPopup, ProjectsModal, SearchModal, Toasts } from './overlays'
|
||||||
import { ProjectLauncher } from './launcher'
|
import { ProjectLauncher } from './launcher'
|
||||||
import type { Menu, Toast } from './overlays'
|
import type { Menu, Toast } from './overlays'
|
||||||
import type { FileNode, GitStatus } from './types'
|
import type { FileNode, GitStatus } from './types'
|
||||||
@@ -79,7 +79,7 @@ export function App(): React.ReactElement {
|
|||||||
const [openDirs, setOpenDirs] = useState<Set<string>>(new Set())
|
const [openDirs, setOpenDirs] = useState<Set<string>>(new Set())
|
||||||
const [cursor, setCursor] = useState<Cursor | null>(null)
|
const [cursor, setCursor] = useState<Cursor | null>(null)
|
||||||
const [selection, setSelection] = useState<Selection | null>(null)
|
const [selection, setSelection] = useState<Selection | null>(null)
|
||||||
const [overlay, setOverlay] = useState<'search' | 'history' | 'help' | null>(null)
|
const [overlay, setOverlay] = useState<'search' | 'history' | 'help' | 'projects' | null>(null)
|
||||||
const [searchInit, setSearchInit] = useState('') // seed query for ⌘F-with-selection
|
const [searchInit, setSearchInit] = useState('') // seed query for ⌘F-with-selection
|
||||||
// Most-recently-opened files, newest first, de-duplicated. Drives the ⌘↓/⌘↑ navigator.
|
// Most-recently-opened files, newest first, de-duplicated. Drives the ⌘↓/⌘↑ navigator.
|
||||||
const [history, setHistory] = useState<string[]>([])
|
const [history, setHistory] = useState<string[]>([])
|
||||||
@@ -659,8 +659,8 @@ export function App(): React.ReactElement {
|
|||||||
const meta = e.metaKey || e.ctrlKey
|
const meta = e.metaKey || e.ctrlKey
|
||||||
const ae = document.activeElement as HTMLElement | null
|
const ae = document.activeElement as HTMLElement | null
|
||||||
const inField = !!ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA')
|
const inField = !!ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA')
|
||||||
// The history navigator owns the keyboard while open (it listens in capture phase).
|
// The history / project navigators own the keyboard while open (capture phase).
|
||||||
if (overlay === 'history') return
|
if (overlay === 'history' || overlay === 'projects') return
|
||||||
// An open context menu owns the keyboard (arrows / ↵ / esc handled there).
|
// An open context menu owns the keyboard (arrows / ↵ / esc handled there).
|
||||||
if (menu) return
|
if (menu) return
|
||||||
const inPanel = activePanel === 'git' || activePanel === 'tree'
|
const inPanel = activePanel === 'git' || activePanel === 'tree'
|
||||||
@@ -692,6 +692,9 @@ export function App(): React.ReactElement {
|
|||||||
setHistInitSel(e.key === 'ArrowDown' ? Math.min(1, history.length - 1) : 0)
|
setHistInitSel(e.key === 'ArrowDown' ? Math.min(1, history.length - 1) : 0)
|
||||||
setOverlay('history')
|
setOverlay('history')
|
||||||
}
|
}
|
||||||
|
// ⇧⌘O opens the recent-project history picker (⌘O — opening a new folder —
|
||||||
|
// is the native File-menu accelerator, so the renderer is free to own ⇧⌘O).
|
||||||
|
else if (meta && e.shiftKey && e.key.toLowerCase() === 'o') { e.preventDefault(); setOverlay('projects') }
|
||||||
// ⌘F opens the unified search (it covers file names too), seeded with the
|
// ⌘F opens the unified search (it covers file names too), seeded with the
|
||||||
// current selection when there is one.
|
// current selection when there is one.
|
||||||
else if (meta && e.key.toLowerCase() === 'f') { e.preventDefault(); setSearchInit(selectedSearchText()); setOverlay('search') }
|
else if (meta && e.key.toLowerCase() === 'f') { e.preventDefault(); setSearchInit(selectedSearchText()); setOverlay('search') }
|
||||||
@@ -847,6 +850,7 @@ export function App(): React.ReactElement {
|
|||||||
onCancel={() => setNewFolderPopup(null)} />}
|
onCancel={() => setNewFolderPopup(null)} />}
|
||||||
{overlay === 'search' && <SearchModal initialQuery={searchInit} onOpen={openFile} onOpenAt={(p, n) => openFile(p, { line: n })} onClose={() => setOverlay(null)} changeSet={changeSet} activePath={active} activeText={bufferText(active)} showHidden={showHidden} />}
|
{overlay === 'search' && <SearchModal initialQuery={searchInit} onOpen={openFile} onOpenAt={(p, n) => openFile(p, { line: n })} onClose={() => setOverlay(null)} changeSet={changeSet} activePath={active} activeText={bufferText(active)} showHidden={showHidden} />}
|
||||||
{overlay === 'history' && <HistoryModal history={history} initialSel={histInitSel} onOpen={openFile} onClose={() => setOverlay(null)} changeSet={changeSet} />}
|
{overlay === 'history' && <HistoryModal history={history} initialSel={histInitSel} onOpen={openFile} onClose={() => setOverlay(null)} changeSet={changeSet} />}
|
||||||
|
{overlay === 'projects' && <ProjectsModal recents={proj.recents} currentRoot={proj.root} onOpen={(p) => actions.openProjectPath(p)} onClose={() => setOverlay(null)} />}
|
||||||
{overlay === 'help' && <HelpModal onClose={() => setOverlay(null)} />}
|
{overlay === 'help' && <HelpModal onClose={() => setOverlay(null)} />}
|
||||||
{confirm && <ConfirmModal title={confirm.title} body={confirm.body} confirmLabel={confirm.confirmLabel} danger onConfirm={confirm.onConfirm} onClose={() => setConfirm(null)} />}
|
{confirm && <ConfirmModal title={confirm.title} body={confirm.body} confirmLabel={confirm.confirmLabel} danger onConfirm={confirm.onConfirm} onClose={() => setConfirm(null)} />}
|
||||||
{menu && <ContextMenu menu={menu} onClose={() => setMenu(null)} />}
|
{menu && <ContextMenu menu={menu} onClose={() => setMenu(null)} />}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* Overlays: combined search (content + file names), context menu, toast, pass-popup */
|
/* Overlays: combined search (content + file names), context menu, toast, pass-popup */
|
||||||
import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useProject } from './project'
|
import { useProject } from './project'
|
||||||
|
import type { RecentProject } from './project'
|
||||||
import { fuzzy } from './fuzzy'
|
import { fuzzy } from './fuzzy'
|
||||||
import { FileIcon, Icon } from './components'
|
import { FileIcon, Icon } from './components'
|
||||||
import type { OpenFile } from './components'
|
import type { OpenFile } from './components'
|
||||||
@@ -347,6 +348,64 @@ export function HistoryModal({ history, initialSel, onOpen, onClose, changeSet }
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Project history picker (⇧⌘O). Same keyboard model as the launcher and the
|
||||||
|
* recent-files navigator: ⌘↑/⌘↓ (or plain arrows) move, ↵ switches the current
|
||||||
|
* window to that project, esc closes. The currently-open project is filtered
|
||||||
|
* out — reopening it is a no-op. */
|
||||||
|
export function ProjectsModal({ recents, currentRoot, onOpen, onClose }: {
|
||||||
|
recents: RecentProject[]
|
||||||
|
currentRoot: string | null
|
||||||
|
onOpen: (path: string) => void
|
||||||
|
onClose: () => void
|
||||||
|
}): React.ReactElement {
|
||||||
|
const list = useMemo(() => recents.filter((p) => p.path !== currentRoot), [recents, currentRoot])
|
||||||
|
const [sel, setSel] = useState(0)
|
||||||
|
const selRef = useRef(sel); selRef.current = sel
|
||||||
|
const listRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onKey(e: KeyboardEvent): void {
|
||||||
|
if (e.key === 'ArrowDown') { e.preventDefault(); setSel((s) => Math.min(s + 1, list.length - 1)) }
|
||||||
|
else if (e.key === 'ArrowUp') { e.preventDefault(); setSel((s) => Math.max(s - 1, 0)) }
|
||||||
|
else if (e.key === 'Enter') { e.preventDefault(); const p = list[selRef.current]; if (p) { onOpen(p.path); onClose() } }
|
||||||
|
else if (e.key === 'Escape') { e.preventDefault(); onClose() }
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKey, true)
|
||||||
|
return () => window.removeEventListener('keydown', onKey, true)
|
||||||
|
}, [list, onOpen, onClose])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = listRef.current && listRef.current.querySelector('.hist-row.sel')
|
||||||
|
if (el) el.scrollIntoView({ block: 'nearest' })
|
||||||
|
}, [sel])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="scrim" onMouseDown={onClose}>
|
||||||
|
<div className="history-modal" onMouseDown={(e) => e.stopPropagation()}>
|
||||||
|
<div className="pi">
|
||||||
|
{Icon.reveal({ style: { color: 'var(--fg-3)' } })}
|
||||||
|
<span className="hist-title">Open recent project</span>
|
||||||
|
<span className="mode-chip">{list.length} project{list.length === 1 ? '' : 's'} · <kbd>⌘↓</kbd> <kbd>⌘↑</kbd> <kbd>↵</kbd></span>
|
||||||
|
</div>
|
||||||
|
<div className="hist-list" ref={listRef}>
|
||||||
|
{list.length === 0 && <div className="pempty">No other recent projects</div>}
|
||||||
|
{list.map((p, i) => (
|
||||||
|
<div key={p.path} className={'hist-row' + (i === sel ? ' sel' : '')} title={p.path}
|
||||||
|
onMouseEnter={() => setSel(i)}
|
||||||
|
onClick={() => { onOpen(p.path); onClose() }}>
|
||||||
|
{Icon.reveal()}
|
||||||
|
<div className="hist-txt">
|
||||||
|
<span className="fn">{p.name}</span>
|
||||||
|
<span className="fd">{p.path}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/* Keyboard-shortcuts reference (opened from the title-bar ? button). */
|
/* Keyboard-shortcuts reference (opened from the title-bar ? button). */
|
||||||
const SHORTCUTS: { keys: string[]; label: string }[] = [
|
const SHORTCUTS: { keys: string[]; label: string }[] = [
|
||||||
{ keys: ['⌘', 'F'], label: 'Search contents & names (seeded by selection)' },
|
{ keys: ['⌘', 'F'], label: 'Search contents & names (seeded by selection)' },
|
||||||
@@ -368,6 +427,7 @@ const SHORTCUTS: { keys: string[]; label: string }[] = [
|
|||||||
{ keys: ['⌘', 'W'], label: 'Close the current file' },
|
{ keys: ['⌘', 'W'], label: 'Close the current file' },
|
||||||
{ keys: ['⌘', 'D'], label: 'Delete the current file (confirm)' },
|
{ keys: ['⌘', 'D'], label: 'Delete the current file (confirm)' },
|
||||||
{ keys: ['⌘', 'O'], label: 'Open a project folder' },
|
{ keys: ['⌘', 'O'], label: 'Open a project folder' },
|
||||||
|
{ keys: ['⇧', '⌘', 'O'], label: 'Open a recent project (history picker)' },
|
||||||
{ keys: ['Esc'], label: 'Close an overlay / split view' },
|
{ keys: ['Esc'], label: 'Close an overlay / split view' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user