update
Some checks failed
CI / check (push) Has been cancelled

This commit is contained in:
2026-06-24 13:59:58 +02:00
parent 43131915c0
commit 6c3a021bb9
2 changed files with 68 additions and 4 deletions

View File

@@ -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)} />}

View File

@@ -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' },
] ]