Skip to content

chore: break up FileTreeView.swift (1,608 lines) #239

@dep

Description

@dep

Context

Follow-up to #237 (AppState god-object refactor). FileTreeView.swift is 1,608 lines and contains 16 top-level types plus a single FileTreeView struct that spans lines 134–854 (~720 lines) and acts as an orchestrator for tree rendering, drag-and-drop, CRUD dialogs, pinned folders, flat navigator, and folder appearance picking.

Goal

  1. Split the file into focused, cohesive units.
  2. Extract file-mutation calls (currently direct to AppState) behind a seam so the chore: refactor god object #237 FileService extraction has a clean integration point.
  3. Move synchronous file-system I/O out of view-body paths.

Non-goals: behavior or visual changes.

Current structure (evidence)

Type Lines Purpose
FileTreeMode enum 4–7 Folder-tree vs flat-list mode
FileNode struct 9–28 Tree node w/ lazy children + cached mtime
BrowserEditorAction struct 30–59 Create/rename dialog parameters
BrowserDeleteTarget struct 61–75 Delete confirmation state
buildFileTreeLevel() free function 80–124 Synchronous directory read + filter + sort
FileMoveConflict struct 127–132 Drag-drop conflict state
FileTreeView struct 134–854 Main orchestrator
FileNodeRow struct 856–1017 Recursive row w/ expand, context menu, drag/drop
FileNodeDragModifier 1019–1032 onDrag for non-folder nodes
FolderDropModifier 1037–1086 Drop target + 0.6s hover-to-expand
PinnedItemRow struct 1089–1193 Pinned item row (folder/file/tag) + drop target
PinnedFolderDropModifier 1197–1228 Pinned folder drop
BrowserItemEditorSheet 1230–1378 Create/rename modal w/ folder picker
FlatNavigatorBackButton 1384–1439 Back button w/ drag-hover
FlatFolderRow 1442–1520 Flat-navigator folder row
FlatFileRow 1523–1590 Flat-navigator file row
View.appearancePickerSheet ext 1594–1607 Sheet wrapper

FileTreeView responsibilities (lines 134–854)

  • Layout & navigation (166–264): body, 8+ onChange/onReceive chains
  • View builders (268–479): mainContent, headerSection, todayButton, pinnedSection, sortControls
  • File/folder display (483–635): fileTreeScrollContent, fileListContent, folderTreeContent, flatSortedFiles() (synchronous disk I/O on every sort)
  • Refresh & tree building (636–689): refresh, refreshWithoutNavigation, ensureChildrenLoaded (synchronous)
  • Path expansion & scroll (691–729): expandPath, revealSelection
  • CRUD dialogs (731–787): presentCreateNote, presentCreateFolder, presentRename, handleEditorSubmit
  • Delete (789–799): presentDelete, confirmDelete
  • Drag-drop / move (805–853): handleDrop, presentMoveFile, confirmMoveWithOverwrite

AppState coupling (file mutations)

All file mutations go direct to appState — these are the integration points for #237's FileService:

  • appState.createNote(named:in:) — line 768
  • appState.createNamedNoteFromTemplate(_:named:in:) — line 766
  • appState.createFolder(named:in:) — line 772
  • appState.renameItem(at:to:) — line 777
  • appState.deleteItem(at:) — line 793
  • appState.moveFile(at:toFolder:) — line 826
  • appState.moveFile(at:toFolder:overwrite:) — line 846
  • appState.dropFile(fileURL:ontoPinnedItem:) — line 396

Other AppState touches: rootURL, allFiles, selectedFile, sortCriterion, sortAscending, pinnedItems, flat-navigator state (5 properties/methods), folderAppearance(), navigation calls (openFile, openFileInNewTab, openFileInSplit, openTodayNote, presentRootNoteSheet).

Local state smells

  • pendingMoveRefreshSkips (line 162) — counter to suppress full refresh during async moveFile. Comments acknowledge the race (157–161).
  • 14 @State variables in one struct.
  • Flat-navigator position lives in AppState but expansion state is local → brittle coordination.

Target file layout

macOS/Synapse/FileTree/
 ├── FileTreeView.swift                (~350 — just the view body + state)
 ├── FileNode.swift                    (~60 — FileNode + FileTreeMode)
 ├── FileTreeBuilder.swift             (~100 — buildFileTreeLevel + sort helpers, pure)
 ├── FileNodeRow.swift                 (~200 — recursive row)
 ├── DragDrop/
 │    ├── FileNodeDragModifier.swift   (~40)
 │    ├── FolderDropModifier.swift     (~80)
 │    ├── PinnedFolderDropModifier.swift (~50)
 │    └── FileMoveCoordinator.swift    (~100 — handleDrop/presentMoveFile/confirmMove)
 ├── Pinned/
 │    └── PinnedItemRow.swift          (~150)
 ├── Dialogs/
 │    ├── BrowserItemEditorSheet.swift (~200)
 │    ├── BrowserEditorAction.swift    (~40)
 │    └── BrowserDeleteTarget.swift    (~20)
 ├── FlatNavigator/
 │    ├── FlatNavigatorBackButton.swift (~80)
 │    ├── FlatFolderRow.swift          (~100)
 │    └── FlatFileRow.swift            (~80)
 └── FileOperationHandler.swift        (~100 — glue for create/rename/delete/move; wraps appState now, swaps to FileService later)

Phased plan

Phase 1 — Leaf extractions (safe)

  • FileNode, FileTreeMode, BrowserEditorAction, BrowserDeleteTarget, FileMoveConflict each to their own file.
  • buildFileTreeLevel() free function → FileTreeBuilder.swift. It's pure; add unit tests against a temp directory.

Phase 2 — Extract flat-navigator components

  • FlatNavigatorBackButton, FlatFolderRow, FlatFileRowFlatNavigator/.
  • Trivial move — they're already self-contained structs.

Phase 3 — Extract dialogs

  • BrowserItemEditorSheetDialogs/BrowserItemEditorSheet.swift.

Phase 4 — Extract rows and drag-drop modifiers

  • FileNodeRow, PinnedItemRow → own files.
  • FileNodeDragModifier, FolderDropModifier, PinnedFolderDropModifierDragDrop/.

Phase 5 — Introduce FileOperationHandler seam

  • Create FileOperationHandler that wraps the 8 direct appState mutation calls.
  • FileTreeView.handleEditorSubmit, confirmDelete, presentMoveFile, confirmMoveWithOverwrite, handleDrop call the handler instead of AppState.
  • Initially FileOperationHandler just forwards to AppState — no behavior change. This is the seam chore: refactor god object #237's FileService plugs into.

Phase 6 — Extract FileMoveCoordinator

  • Pull handleDrop, presentMoveFile, confirmMoveWithOverwrite, and the pendingMoveRefreshSkips workaround into FileMoveCoordinator.
  • While touching it, replace the counter hack with a proper cancellation or async/await flow — or at least document the invariant.

Phase 7 — Move synchronous disk I/O off the view body

  • flatSortedFiles() (618–634) calls resourceValues(...contentModificationDateKey) per file per sort click. Cache results keyed by URL + mtime or move to a background Task that publishes sorted results.
  • ensureChildrenLoaded() + expandPath tree walks — keep top-level synchronous (it's bounded), but make the recursive expansion path lazy/async.
  • FlatFolderRow per-row appState.folderAppearance(for:) calls — cache at the parent level and pass down.

Phase 8 — Final cleanup

  • FileTreeView.swift should be <400 lines holding the view body + 5–6 @State at most.
  • Audit the onChange/onReceive chain in body (166–264) — collapse redundant listeners, extract long handlers to methods.

Testability milestones

  • After Phase 1: FileTreeBuilder testable against temp directories; FileNode tests cover lazy-children and mtime caching.
  • After Phase 5: FileOperationHandler is mockable; FileTreeView previews can run with a no-op handler.
  • After Phase 6: FileMoveCoordinator has unit tests covering the conflict-then-overwrite flow without a full AppState.

Risks

  • Drag-drop is fragile. Keep smoke tests for drag-drop flows between phases; pendingMoveRefreshSkips exists because the async interleaving is already subtle.
  • Expansion state coordination. Flat navigator and tree expansion share implicit invariants. When splitting, document them.
  • Parallel with chore: refactor god object #237. Phase 5 (FileOperationHandler seam) is best sequenced after chore: refactor god object #237 Phase 2 (FileService extraction) so the handler can forward to FileService directly instead of AppState → FileService → back. If chore: refactor god object #237 hasn't landed yet, Phase 5 still works as a forwarding shim.

Out of scope

  • Rewriting expansion state to live in one place (would need cross-cutting changes — separate issue).
  • Drag-drop ergonomics improvements.
  • Tracked separately: chore: refactor god object #237 (AppState), EditorView breakup, SettingsManager refactor.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions