You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
Split the file into focused, cohesive units.
Extract file-mutation calls (currently direct to AppState) behind a seam so the chore: refactor god object #237FileService extraction has a clean integration point.
Move synchronous file-system I/O out of view-body paths.
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.
Context
Follow-up to #237 (AppState god-object refactor).
FileTreeView.swiftis 1,608 lines and contains 16 top-level types plus a singleFileTreeViewstruct 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
FileServiceextraction has a clean integration point.Non-goals: behavior or visual changes.
Current structure (evidence)
FileTreeModeenumFileNodestructBrowserEditorActionstructBrowserDeleteTargetstructbuildFileTreeLevel()free functionFileMoveConflictstructFileTreeViewstructFileNodeRowstructFileNodeDragModifieronDragfor non-folder nodesFolderDropModifierPinnedItemRowstructPinnedFolderDropModifierBrowserItemEditorSheetFlatNavigatorBackButtonFlatFolderRowFlatFileRowView.appearancePickerSheetextFileTreeView responsibilities (lines 134–854)
body, 8+onChange/onReceivechainsmainContent,headerSection,todayButton,pinnedSection,sortControlsfileTreeScrollContent,fileListContent,folderTreeContent,flatSortedFiles()(synchronous disk I/O on every sort)refresh,refreshWithoutNavigation,ensureChildrenLoaded(synchronous)expandPath,revealSelectionpresentCreateNote,presentCreateFolder,presentRename,handleEditorSubmitpresentDelete,confirmDeletehandleDrop,presentMoveFile,confirmMoveWithOverwriteAppState coupling (file mutations)
All file mutations go direct to
appState— these are the integration points for #237'sFileService:appState.createNote(named:in:)— line 768appState.createNamedNoteFromTemplate(_:named:in:)— line 766appState.createFolder(named:in:)— line 772appState.renameItem(at:to:)— line 777appState.deleteItem(at:)— line 793appState.moveFile(at:toFolder:)— line 826appState.moveFile(at:toFolder:overwrite:)— line 846appState.dropFile(fileURL:ontoPinnedItem:)— line 396Other 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).@Statevariables in one struct.Target file layout
Phased plan
Phase 1 — Leaf extractions (safe)
FileNode,FileTreeMode,BrowserEditorAction,BrowserDeleteTarget,FileMoveConflicteach 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,FlatFileRow→FlatNavigator/.Phase 3 — Extract dialogs
BrowserItemEditorSheet→Dialogs/BrowserItemEditorSheet.swift.Phase 4 — Extract rows and drag-drop modifiers
FileNodeRow,PinnedItemRow→ own files.FileNodeDragModifier,FolderDropModifier,PinnedFolderDropModifier→DragDrop/.Phase 5 — Introduce
FileOperationHandlerseamFileOperationHandlerthat wraps the 8 directappStatemutation calls.FileTreeView.handleEditorSubmit,confirmDelete,presentMoveFile,confirmMoveWithOverwrite,handleDropcall the handler instead of AppState.FileOperationHandlerjust forwards to AppState — no behavior change. This is the seam chore: refactor god object #237'sFileServiceplugs into.Phase 6 — Extract
FileMoveCoordinatorhandleDrop,presentMoveFile,confirmMoveWithOverwrite, and thependingMoveRefreshSkipsworkaround intoFileMoveCoordinator.Phase 7 — Move synchronous disk I/O off the view body
flatSortedFiles()(618–634) callsresourceValues(...contentModificationDateKey)per file per sort click. Cache results keyed by URL + mtime or move to a backgroundTaskthat publishes sorted results.ensureChildrenLoaded()+expandPathtree walks — keep top-level synchronous (it's bounded), but make the recursive expansion path lazy/async.FlatFolderRowper-rowappState.folderAppearance(for:)calls — cache at the parent level and pass down.Phase 8 — Final cleanup
FileTreeView.swiftshould be <400 lines holding the view body + 5–6@Stateat most.onChange/onReceivechain inbody(166–264) — collapse redundant listeners, extract long handlers to methods.Testability milestones
FileTreeBuildertestable against temp directories;FileNodetests cover lazy-children and mtime caching.FileOperationHandleris mockable;FileTreeViewpreviews can run with a no-op handler.FileMoveCoordinatorhas unit tests covering the conflict-then-overwrite flow without a full AppState.Risks
pendingMoveRefreshSkipsexists because the async interleaving is already subtle.FileOperationHandlerseam) is best sequenced after chore: refactor god object #237 Phase 2 (FileServiceextraction) so the handler can forward toFileServicedirectly 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