feat(editor): 文件窗口与资源管理器二合一 + 声明式设置 select 行 (#151)
* feat(editor): merged explorer mode — path input, docked file tree, global name search
New editorExplorer pref (default on, editor card gear): the editor tab gains
a path-input header and a toggleable right-docked tree panel (per-tab state
in tab.meta.treeOpen; file tabs default closed, the seeded path-less home
tab defaults open). Fresh sessions seed that empty editor tab instead of
the explorer tab when merged mode is on; the standalone explorer tab stays
in the + menu unchanged.
- Extract FileTree.tsx from ExplorerView (shared by both hosts)
- Host fs.search route: recursive case-insensitive name search rooted at
the session cwd, budget-capped (200 matches / 100k visited), skips .git,
never descends symlink dirs
- EditorHost reads the pref reactively (boolean snapshot) and keeps the
pre-merge layout verbatim when off
* feat(settings): select row type for declarative settings; editorExplorer as iconed select
- SidebarSettingToggle gains type 'select' with options {value,title,desc,icon}
and multi (default false; stores the picked values as an array in options
order). Any option carrying an icon renders big-icon option cards in the
dropdown and the icon in the closed anchor; icon-less rows are one-line text.
- SelectRow in SideCardSection rides the primitives Menu (portal); single-pick
commits and closes, multi toggles and stays open. Wired for both host-prefs
toggles and plugin-owned pluginToggles via the new onSelectValue seam.
- The editorExplorer merged-mode pref is now an iconed select (merged vs
separate) instead of a switch; the stored value stays boolean.
- Advertise the capability as 'settingSelect' in SIDEBAR_FEATURES.
* feat(editor): unify explorer into the editor surface, resizable tree dock, hoisted toolbar
- One tree implementation, two presentations: the shared TreePanel (global
name search + refresh + FileTree) docks as the editor tab's right panel
and — in merged mode — fills the explorer tab's window; the pref-off
explorer keeps its legacy header shell.
- The docked panel drag-resizes from its left edge (160-480px clamp),
persisted as tab.meta.treeWidth alongside treeOpen.
- The text editor's preview/edit/save toolbar moves into the path-input
header row: FileViewerProps gains internal toolbar fields ('host' mode
skips the viewer's own row, reports EditorToolbarState and registers
EditorToolbarControls); non-merged mode is byte-identical to before.
* feat(editor): single files window with in-place switching; explorer descriptor removed
- editor tab replaces explorer on the + menu (order 10, Files); persisted
explorer tabs migrate to the editor home tab via sanitizeState
- editorExplorer now only controls open behavior: merged = in-place
updateTab, independent = openSidebarFile; both modes share the merged
chrome (path input + hoisted preview/edit/save toolbar + tree toggle)
- file tree context menu gains 'open in new tab' / 'open to the side'
(mintTabId avoids the id safety net; insertLeafAt splits right)
- tree dock resize uses pointer capture (touch-action none, 6px handle)
- defaults: openByDefault off, sidebar width 35%
- file tree clips horizontal overflow (no sideways scroll)
- e2e: mount lane arms the editor-chunk wait before goto and clicks the
tree row's left edge (hover @-reference button sits mid-row); drag lane
expands the panel first (collapsed push writes 0px), waits for the
slide-in to settle, and picks the leftmost col-resize strip; 'Files'
title assertions require draggable to skip the bottom panel's welcome
cards
* feat(editor): split mode turns the path-less window into the standalone explorer
In independent (split) mode a path-less files window renders ONLY the
tree panel (search + FileTree, full-window form) — no path input, no
hoisted toolbar, no tree toggle. File clicks open per-path editor tabs
(full chrome, tree docked closed by default) through openSidebarFile,
unchanged. Merged mode is untouched (chrome + docked tree, in-place
switching). TreePanel's previously reserved 'full' prop now has its
consumer.
This commit is contained in:
+57
-4
@@ -10,6 +10,59 @@ import {
|
||||
describe('sidebar state', () => {
|
||||
const state = (): SidebarState => makeDefaultState()
|
||||
|
||||
it('makeDefaultState seeds per the seed enum (editor-home / none)', () => {
|
||||
// Default and explicit 'editor-home' seed an EMPTY editor tab (the files
|
||||
// window) with the tree panel pinned open.
|
||||
for (const s of [makeDefaultState(), makeDefaultState(400, true, 'editor-home')]) {
|
||||
const leaf = s.splits as { tabs: SidebarTab[]; active: string | null }
|
||||
expect(leaf.tabs).toHaveLength(1)
|
||||
expect(leaf.tabs[0]!.type).toBe('editor')
|
||||
expect(leaf.tabs[0]!.title).toBe('Files')
|
||||
expect(leaf.tabs[0]!.path).toBeUndefined()
|
||||
expect(leaf.tabs[0]!.meta).toEqual({ treeOpen: true })
|
||||
expect(leaf.active).toBe(leaf.tabs[0]!.id)
|
||||
}
|
||||
// The seeded home tab survives the persist round-trip (meta intact).
|
||||
const restored = sanitizeState(JSON.parse(JSON.stringify(makeDefaultState())))
|
||||
const restoredLeaf = restored!.splits as { tabs: SidebarTab[] }
|
||||
expect(restoredLeaf.tabs[0]!.meta).toEqual({ treeOpen: true })
|
||||
// 'none' seeds an empty pane.
|
||||
const bare = makeDefaultState(400, true, 'none')
|
||||
const bareLeaf = bare.splits as { tabs: SidebarTab[]; active: string | null }
|
||||
expect(bareLeaf.tabs).toHaveLength(0)
|
||||
expect(bareLeaf.active).toBeNull()
|
||||
})
|
||||
|
||||
it('sanitizeState migrates persisted explorer tabs to editor home tabs (both trees)', () => {
|
||||
const valid = sanitizeState({
|
||||
panelOpen: true,
|
||||
width: 400,
|
||||
nextTerminal: 1,
|
||||
activePane: 'pane:1',
|
||||
expanded: [],
|
||||
splits: {
|
||||
kind: 'leaf',
|
||||
id: 'pane:1',
|
||||
active: 'ex-right',
|
||||
tabs: [{ id: 'ex-right', type: 'explorer', title: 'Explorer', meta: { treeWidth: 300 } }],
|
||||
},
|
||||
bottomSplits: {
|
||||
kind: 'leaf',
|
||||
id: 'pane:b',
|
||||
active: 'ex-bottom',
|
||||
tabs: [{ id: 'ex-bottom', type: 'explorer', title: 'Explorer' }],
|
||||
},
|
||||
})
|
||||
const right = (valid?.splits as { tabs: SidebarTab[] }).tabs
|
||||
expect(right).toHaveLength(1)
|
||||
// Migrated: editor home tab (no path), tree pinned open, prior meta kept.
|
||||
expect(right[0]).toMatchObject({ id: 'ex-right', type: 'editor', title: 'Files', meta: { treeOpen: true, treeWidth: 300 } })
|
||||
expect(right[0]!.path).toBeUndefined()
|
||||
const bottom = (valid?.bottomSplits as { tabs: SidebarTab[] }).tabs
|
||||
expect(bottom).toHaveLength(1)
|
||||
expect(bottom[0]).toMatchObject({ id: 'ex-bottom', type: 'editor', title: 'Files', meta: { treeOpen: true } })
|
||||
})
|
||||
|
||||
it('opens tabs into the active pane and dedupes by id (safety net)', () => {
|
||||
let s = state()
|
||||
const gitTab = { id: 'git', type: 'git' as const, title: 'Git' }
|
||||
@@ -411,8 +464,8 @@ describe('sidebar state', () => {
|
||||
const tab = { id: 'git', type: 'git' as const, title: 'Git' }
|
||||
s = openTabInActivePane(s, tab)
|
||||
expect((s.bottomSplits as { tabs: SidebarTab[] }).tabs.map(t => t.id)).toContain('git')
|
||||
// The right tree is untouched.
|
||||
expect((s.splits as { tabs: SidebarTab[] }).tabs.map(t => t.type)).toEqual(['explorer'])
|
||||
// The right tree is untouched (its seeded files-window home tab stays).
|
||||
expect((s.splits as { tabs: SidebarTab[] }).tabs.map(t => t.type)).toEqual(['editor'])
|
||||
expect(s.activePane).toBe(bottomPane)
|
||||
// The id safety net works across trees: reopening the same id focuses it.
|
||||
const after = openTabInActivePane(s, tab)
|
||||
@@ -677,8 +730,8 @@ describe('persisted state sanitization', () => {
|
||||
it('falls back from a stale active pane instead of dropping the open', () => {
|
||||
let s = makeDefaultState()
|
||||
const paneA = allLeaves(s.splits)[0]!.id
|
||||
const explorerTab = allLeaves(s.splits)[0]!.tabs.find(tab => tab.type === 'explorer')!.id
|
||||
s = closeTab(s, paneA, explorerTab)
|
||||
const seededTab = allLeaves(s.splits)[0]!.tabs.find(tab => tab.type === 'editor')!.id
|
||||
s = closeTab(s, paneA, seededTab)
|
||||
s = openTabInActivePane(s, { id: 'editor:/a.ts', type: 'editor', title: 'a.ts', path: '/a.ts' })
|
||||
const split = insertLeafAt(s.splits, paneA, 'col', { id: 'terminal:1', type: 'terminal', title: 'Terminal 1' }, false)
|
||||
s = { ...s, splits: split.node, activePane: paneA }
|
||||
|
||||
Reference in New Issue
Block a user