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). SettingsManager.swift is 1,470 lines and follows the same god-object anti-pattern as AppState: 32 @Published properties across 8 distinct domains, all funneled through a single class with debounced YAML/JSON persistence. Every setting mutation triggers a didSet → debounced save, and every view that reads any setting observes the entire manager.
Goal
Decompose SettingsManager into domain-scoped stores following the same pattern as #237:
Each domain (appearance, editor display, startup, sidebar panes, filtering, git/GitHub, sync, vault paths) is its own ObservableObject.
Persistence is a separate concern owned by a SettingsPersistence service — not mixed into the state classes.
Views observe only the store(s) they need — a font change no longer invalidates views that only read pinned items.
Sidebar pane configuration (~30% of the file, 212 lines of type definitions alone) gets its own module.
Non-goals: changing the on-disk YAML/JSON schema, changing default values, or breaking existing settings files.
Top-level types (212 lines of sidebar type machinery before the class even starts)
SidebarPane enum (5–27)
SidebarNotePane struct (29–50)
SidebarPaneItem enum + custom Codable (52–135)
Array<SidebarPaneItem> extension (137–150)
SidebarPosition enum (153–156)
LaunchBehavior enum (159–187)
Sidebar struct (190–200)
FixedSidebar enum (204–217)
Persistence
YAML + legacy JSON fallback
save() at 1132–1151 — snapshots on main, writes on utility queue with 0.5s debounce
flush() at 1153–1155 — sync for tests
flushDebouncedSaveBeforeReloadIfNeeded() at 1160–1166
Three separate write paths: writeLegacy (1274–1340), writeVault (1342–1391), writeGlobalOnly (1256–1272)
Three separate load paths: loadConfig, loadVaultConfig, loadGlobalConfig (1394–1430)
Migration / apply methods
applyLegacyConfig, applyVaultConfig, applyGlobalConfig, applyNoVaultDefaults — lines 842–1026. Each 50+ lines of explicit field assignment with overlapping logic.
Each phase is one PR, preserves the on-disk format, and can be reverted.
Phase 0 — Safety net
Golden-file tests: given a vault YAML + global YAML, load into SettingsManager, immediately save, assert bit-identical output. Do the same for the legacy JSON format.
These tests guard the entire refactor.
Phase 1 — Extract sidebar types
Move SidebarPane, SidebarNotePane, SidebarPaneItem, Sidebar, SidebarPosition, FixedSidebar, and the SidebarPaneItem Codable impl to Settings/Sidebar/.
Move LaunchBehavior to its own file.
Pure file moves, no behavior change. Removes ~230 lines from the main file.
Phase 2 — Extract persistence
Move Config, VaultConfig, GlobalConfig, SaveSnapshot to Settings/Persistence/.
Create SettingsPersistence class owning the debounce DispatchWorkItem, save queue, save/flush/flushIfNeeded methods.
SettingsManager now owns a SettingsPersistence instance and delegates to it — still a god object, but persistence is no longer mixed in. Interim state.
These are stateless transformations from a Config → a SettingsManager. Easy to test with fixture configs.
Phase 4 — Introduce domain stores, one at a time
Each sub-phase extracts one store, behind a facade, without changing the public API that views depend on. This lets us migrate views incrementally (Phase 5) while keeping the codebase shippable between PRs.
Order (smallest/safest first):
BrowserStore (1 property)
GitCredentialsStore (1 property, sensitive — careful with logging)
After Phase 4.i: each store is constructible in a test with no dependencies, verifies its own didSet → persistence notification.
Performance wins
Views observing only EditorDisplayStore no longer re-render when pinned items change.
SidebarLayoutStore mutations (which are the heaviest — full sidebars: [Sidebar] array rewrite on every pane move) stop invalidating the entire settings surface.
Debounced save can batch per-store or globally — TBD during Phase 2.
Risks
On-disk schema preservation. The Phase 0 golden tests are mandatory; YAML key ordering and JSON field shape must stay identical or existing users' settings files become unreadable.
didSet cascade. Currently every setting triggers save(). When splitting into stores, we need a single debounce per save — not N debounces. SettingsPersistence must expose a scheduleSave() that all stores call.
GitHub PAT handling.githubPAT is sensitive and currently lives in GlobalConfig (machine-local). Extraction must not accidentally log it or write it to the vault YAML. Consider moving it to the keychain while touching it — but scope that as a separate issue if it grows.
Migration code is ugly but correct. Do not "clean up" applyLegacyConfig logic during extraction; preserve field-for-field.
Parallel with chore: refactor god object #237. Both refactors touch the boundary where AppState.settings is accessed. Coordinate: land chore: refactor god object #237 Phase 1 (stop re-render storm) before starting this refactor's Phase 5 (view migration), so we're not migrating views twice.
Out of scope
Keychain migration for githubPAT — consider as a follow-up.
Changing the YAML / JSON schema or defaults.
Reworking theme data model (activeTheme, allThemes, customThemes) — separate concern.
Context
Follow-up to #237 (AppState god-object refactor).
SettingsManager.swiftis 1,470 lines and follows the same god-object anti-pattern as AppState: 32@Publishedproperties across 8 distinct domains, all funneled through a single class with debounced YAML/JSON persistence. Every setting mutation triggers adidSet→ debounced save, and every view that reads any setting observes the entire manager.Goal
Decompose
SettingsManagerinto domain-scoped stores following the same pattern as #237:ObservableObject.SettingsPersistenceservice — not mixed into the state classes.Non-goals: changing the on-disk YAML/JSON schema, changing default values, or breaking existing settings files.
Current structure (evidence)
Nested types inside
SettingsManager(lines 220–1470)Config(498–558) — legacy JSON all-in-oneVaultConfig(561–666) — vault-specific YAML (.synapse/settings.yml)GlobalConfig(669–716) — machine-local YAML (GitHub PAT, vault paths)SaveSnapshot(1169–1392) — value-type snapshot for background I/O32
@Publishedproperties by domainAppearance (5) — lines 318–343
activeThemeName,customThemes,folderAppearances,activeTheme(computed),allThemes(computed)Editor display (5) — lines 283–303
editorBodyFontFamily,editorMonospaceFontFamily,editorFontSize,editorLineHeight,defaultEditMode,hideMarkdownWhileEditingStartup / launch (7) — lines 224–250
onBootCommand,templatesDirectory,dailyNotesEnabled,dailyNotesFolder,dailyNotesTemplate,launchBehavior,launchSpecificNotePathFile filtering (4) — lines 227–232, 314
fileExtensionFilter,hiddenFileFolderFilter,respectGitignore,fileTreeModeSidebar panes (4) — lines 258–272
sidebars,sidebarPaneHeights,collapsedPanes,collapsedSidebarIDsPinned items (1) — line 280
pinnedItemsVault paths (2) — lines 306–312
vaultPaths,lastNoteFolderPerVaultSync (2) — lines 251–256
autoSave,autoPushGit / GitHub (1) — line 274
githubPATBrowser (1) — line 289
browserStartupURLTop-level types (212 lines of sidebar type machinery before the class even starts)
SidebarPaneenum (5–27)SidebarNotePanestruct (29–50)SidebarPaneItemenum + custom Codable (52–135)Array<SidebarPaneItem>extension (137–150)SidebarPositionenum (153–156)LaunchBehaviorenum (159–187)Sidebarstruct (190–200)FixedSidebarenum (204–217)Persistence
save()at 1132–1151 — snapshots on main, writes on utility queue with 0.5s debounceflush()at 1153–1155 — sync for testsflushDebouncedSaveBeforeReloadIfNeeded()at 1160–1166writeLegacy(1274–1340),writeVault(1342–1391),writeGlobalOnly(1256–1272)loadConfig,loadVaultConfig,loadGlobalConfig(1394–1430)Migration / apply methods
applyLegacyConfig,applyVaultConfig,applyGlobalConfig,applyNoVaultDefaults— lines 842–1026. Each 50+ lines of explicit field assignment with overlapping logic.Sidebar mutation methods (357–444)
assignPane,movePane,movePaneItem,insertNotePane,removePane,removePaneItem,isSidebarCollapsed,toggleSidebarCollapsed.movePaneItemhas subtle index-adjustment logic.File filtering methods (1047–1128)
parsedExtensions,parsedHiddenPatterns,shouldHideItem,shouldShowFile,isHiddenByAncestorVault path discovery (1437–1467)
discoverVaultPath()iterates vault candidatesTarget architecture
Phased plan
Each phase is one PR, preserves the on-disk format, and can be reverted.
Phase 0 — Safety net
SettingsManager, immediately save, assert bit-identical output. Do the same for the legacy JSON format.Phase 1 — Extract sidebar types
SidebarPane,SidebarNotePane,SidebarPaneItem,Sidebar,SidebarPosition,FixedSidebar, and theSidebarPaneItemCodable impl toSettings/Sidebar/.LaunchBehaviorto its own file.Phase 2 — Extract persistence
Config,VaultConfig,GlobalConfig,SaveSnapshottoSettings/Persistence/.SettingsPersistenceclass owning the debounceDispatchWorkItem, save queue, save/flush/flushIfNeeded methods.SettingsManagernow owns aSettingsPersistenceinstance and delegates to it — still a god object, but persistence is no longer mixed in. Interim state.Phase 3 — Extract migration
applyLegacyConfig,applyVaultConfig,applyGlobalConfig,applyNoVaultDefaults→SettingsMigration.swift.Phase 4 — Introduce domain stores, one at a time
Each sub-phase extracts one store, behind a facade, without changing the public API that views depend on. This lets us migrate views incrementally (Phase 5) while keeping the codebase shippable between PRs.
Order (smallest/safest first):
BrowserStore(1 property)GitCredentialsStore(1 property, sensitive — careful with logging)SyncStore(2 properties)VaultPathsStore(2 properties +discoverVaultPath)PinnedItemsStore(1 property)EditorDisplayStore(5 properties)StartupStore(7 properties)FilteringStore(4 properties + filter logic)AppearanceStore(5 properties + theme resolution)SidebarLayoutStore(4 properties + 8 mutation methods — largest; do last)During Phase 4,
SettingsManagerretains its public properties as computed pass-throughs to the appropriate store so views keep compiling.Phase 5 — Migrate view call sites
appState.settings.*. For each view:@EnvironmentObject.Phase 6 — Rename to
SettingsCoordinatorSettingsManageris now just a coordinator owning ~10 stores + persistence.SettingsCoordinatorinstead.Testability milestones
SettingsPersistencetests cover debounce, flush ordering, format selection (vault vs global vs legacy).didSet→ persistence notification.Performance wins
EditorDisplayStoreno longer re-render when pinned items change.SidebarLayoutStoremutations (which are the heaviest — fullsidebars: [Sidebar]array rewrite on every pane move) stop invalidating the entire settings surface.Risks
didSetcascade. Currently every setting triggerssave(). When splitting into stores, we need a single debounce per save — not N debounces.SettingsPersistencemust expose ascheduleSave()that all stores call.githubPATis sensitive and currently lives inGlobalConfig(machine-local). Extraction must not accidentally log it or write it to the vault YAML. Consider moving it to the keychain while touching it — but scope that as a separate issue if it grows.applyLegacyConfiglogic during extraction; preserve field-for-field.AppState.settingsis accessed. Coordinate: land chore: refactor god object #237 Phase 1 (stop re-render storm) before starting this refactor's Phase 5 (view migration), so we're not migrating views twice.Out of scope
githubPAT— consider as a follow-up.activeTheme,allThemes,customThemes) — separate concern.