Skip to content

feat: flexible split panes with session persistence (#543)#704

Open
batonogov wants to merge 14 commits intomainfrom
feat/split-panes-dnd-543-split
Open

feat: flexible split panes with session persistence (#543)#704
batonogov wants to merge 14 commits intomainfrom
feat/split-panes-dnd-543-split

Conversation

@batonogov
Copy link
Copy Markdown
Owner

Summary

Split panes driven by drag & drop — builds on #645 with all review blockers resolved:

  • God-file split: PaneTreeView.swift (716 lines) → 5 focused files (PaneTreeView, PaneDividerView, PaneLeafView, PaneDropZone, PaneFocusDetector)
  • Deduplication: Shared TabCloseHelper eliminates ~80 lines of copy-paste between PaneLeafView and ContentView
  • Session persistence: Split pane layout survives app restart — PaneNode tree + per-pane tab assignments saved/restored via SessionState
  • Drag tab to right/bottom edge → split
  • Drag tab between panes → move
  • Drop zones highlight during drag
  • Divider drag for resize
  • Empty panes auto-removed when last tab closed

Closes #543

Test plan

  • 2701 unit tests pass (including 25 PaneManager + MultiPaneIntegration + PaneLeafClose + PaneFocusNSView tests)
  • SwiftLint clean across all new/modified files
  • Build succeeds
  • Manual: drag tab to edge, verify split appears
  • Manual: drag between panes, verify tab moves
  • Manual: resize divider
  • Manual: quit and reopen — verify split layout is restored
  • Visual verification of drop zone highlights

batonogov and others added 13 commits March 29, 2026 15:54
Implement Phase 2 of flexible split panes:
- PaneManager: manages pane layout tree with per-pane TabManagers
- PaneTreeView: recursive SwiftUI view rendering PaneNode splits
- Drag tab to right edge → split right, bottom edge → split down
- Drag tab between panes → move tab
- PaneDividerView: draggable divider for resize with cursor feedback
- Drop zone overlays with semi-transparent indicators
- TabDragInfo: encoded drag data for cross-pane tab transfer
- 25 unit tests covering PaneManager, TabDragInfo, PaneDropZone
- Replace magic numbers (300, 200) in drop zone detection with
  percentage-based thresholds using actual pane size from GeometryReader
- Switch TabDragInfo from pipe-separated encoding to JSON (Codable)
- Use custom UTType (.paneTabDrag) instead of .text for drag operations,
  registered as exported type in Info.plist
- Add guard against repeated NSCursor.push() in onContinuousHover to
  prevent cursor stack leak
- Remove dead paneTabDragUTType global variable, replaced by UTType extension
- Add TODO for persisting split pane layout in SessionState
- Update TabDragInfoTests for JSON encoding format
…nd exclude PaneTreeView from coverage

- PaneManager: focus cycle, updateSplitRatio, move tab edge cases (invalid source, non-existent tab, content preservation), remove pane edge cases, rapid splits, nil tabURL split
- TabDragInfo: unicode paths, deep paths, partial/null/array JSON, extra fields, multiple instances
- PaneDropZone: all zones identity, sendable conformance, switch exhaustiveness
- Exclude PaneTreeView.swift from coverage (pure SwiftUI view)
Resolve merge conflicts in AccessibilityIdentifiers, ContentView,
PineApp, and ProjectManager to incorporate toast notifications,
inline diff hunks, tab context menu actions, and git fetcher changes
from main while preserving split panes functionality.
- Use custom UTType .paneTabDrag instead of .text in SinglePaneSplitDropDelegate
  to prevent external text drops from breaking the app
- Add onDisappear cleanup for NSCursor push/pop in PaneDividerView to prevent
  cursor leaks when SwiftUI deallocates the view without hover .ended phase
- Replace magic pixel numbers (300/200) with percentage-based thresholds (0.7)
  in SinglePaneSplitDropDelegate, unified with PaneSplitDropDelegate via shared
  PaneDropZone.zone(for:in:) static method
- Fix moveTab to preserve all EditorTab state (cursorPosition, scrollOffset,
  foldState, isPinned, encoding, etc.) via new EditorTab.reidentified(from:)
- Remove duplicated PaneEditorTabBar, reuse EditorTabBar with overridePaneID
- Replace trivial PaneDropZone.Equatable tests with meaningful tests for
  drop zone calculation, EditorTab.reidentified, and tab state preservation
- PaneLeafView now shows dirty tab confirmation dialog on close
- Connected onCloseOtherTabs/onCloseTabsToTheRight/onCloseAllTabs
  context menu handlers
- Wired lineDiffs from GitStatusProvider for gutter diff markers
- Wired blameLines from git blame for inline blame annotations
- Added diffHunks/onAcceptHunk/onRevertHunk for inline diff
- Added StatusBarView to each pane leaf
- Removed dead PaneContent.terminal case from enum
- Updated PaneNodeTests for single PaneContent case
- Added PaneLeafCloseTests with 12 tests covering close logic
…tap conflicts

1. pm.tabManager → pm.activeTabManager in all menu commands (Cmd+S/W,
   Save As, Duplicate, Ctrl+Tab, Cmd+1..9), CloseDelegate, and
   applicationShouldTerminate/WillTerminate. Multi-pane mode now
   correctly targets the focused pane.

2. Merged two .onDrop handlers in EditorAreaView into a single
   EditorAreaUnifiedDropDelegate — the second .onDrop was overriding
   the first, breaking file drops from Finder in single-pane mode.

3. Replaced .onTapGesture in PaneLeafView with PaneFocusDetector
   (NSView local event monitor) — the tap gesture was blocking clicks
   on the code editor text and tab bar buttons.

4. Reordered moveTab in PaneManager: add to destination first, then
   remove from source. Prevents tab loss if append fails.

5. Session persistence now collects tabs from ALL panes via
   pm.allTabs, so split-pane tabs survive save/restore cycles.

6. DocumentEditedTracker uses pm.hasUnsavedChanges (all panes) instead
   of single tabManager.
…cycle

CloseDelegate.closeActiveTab() now removes the active pane when closing
its last tab, matching PaneLeafView behavior. PaneFocusNSView.paneManager
is now a weak reference to prevent retain cycles.
- Resolve merge conflict in ContentView.swift (keep pane layout logic)
- Remove stale onAcceptHunk/onRevertHunk params from PaneTreeView and
  ContentView (CodeEditorView no longer accepts them after main changes)
- Add nonisolated(unsafe) on PaneFocusNSView.monitor for deinit access
- Move @mainactor from individual test methods to struct level on
  PaneManagerTests, MultiPaneIntegrationTests, PaneLeafCloseTests,
  PaneFocusNSViewTests (matches convention from #690)
- Replace force unwrapping with guard-let in MultiPaneIntegrationTests
  (SwiftLint fix)
… files

Break up the 716-line god-file into 5 focused files:
- PaneTreeView.swift — PaneTreeView + PaneSplitView (recursive tree rendering)
- PaneDividerView.swift — draggable divider between panes
- PaneLeafView.swift — single leaf pane with editor, git, and tab management
- PaneDropZone.swift — drop zone enum, overlay, preference key, drop delegate
- PaneFocusDetector.swift — NSViewRepresentable focus detection via event monitor

Private types promoted to internal where needed for cross-file access.
Move duplicated tab close confirmation logic from PaneLeafView and
ContentView+Helpers into a shared TabCloseHelper enum. Both call sites
now delegate to the helper, eliminating ~80 lines of copy-paste code.
Save and restore the PaneNode tree, per-pane tab assignments, and active
pane ID in SessionState. On restore, multi-pane layouts are recreated
with tabs distributed to their original panes. Falls back gracefully to
single-pane restore for sessions saved before this change.
@batonogov batonogov added the enhancement New feature or request label Mar 31, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 31, 2026

✅ Code Coverage: 72.9%

Threshold: 70%

Coverage is above the minimum threshold.

Generated by CI — see job summary for detailed file-level breakdown.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: flexible split panes — editor and terminal anywhere (Zed-style layout)

1 participant