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). EditorView.swift is the single largest file in the macOS app at 5,436 lines and contains at least 17 top-level types. Much of its size is not the SwiftUI view itself but a collection of AppKit classes, NSViewRepresentables, helper DTOs, and a ~410-line markdown styling engine that all happen to live in the same file.
Goal
Split EditorView.swift into focused, single-purpose files so that:
The actual EditorView SwiftUI struct is <500 lines and reads like a view, not a framework.
Markdown styling, wiki-link insertion, inline embeds, code-block buttons, and git history each live in their own testable unit.
The NSTextView subclass (LinkAwareTextView, ~1,400 lines on its own) is split by responsibility.
We can write unit tests against the styling engine without instantiating SwiftUI.
Non-goals: behavior changes, visual changes, or rewriting the regex-based styling. This is pure file-level extraction.
Current structure (evidence)
Top-level types in EditorView.swift:
Type
Lines
Purpose
EditorView (struct)
181–688
Main SwiftUI view — edit/view mode routing, git history modal, embedded notes sidebar
Move CompletionViewController + its extensions (4780–4980) to WikiLinkPicker.swift.
The wikiLinkCompletionHandler / wikiLinkDismissHandler closures on AppState remain for now — untangling those is deferred to chore: refactor god object #237.
Phase 4 — Extract inline embed views
EmbeddedNotesPanel, EmbeddedNoteView, EmbeddedImageView, YouTubePreviewView, FlippedNSView each to their own file under InlineEmbeds/.
Address the unmanaged URLSession in EmbeddedImageView (line ~4646) while it's touched: cancel the data task in removeFromSuperview().
Phase 5 — Extract markdown styling engine
applyMarkdownStyling() (1640–2050) and applyPreviewStyling() (1474–1630) move to MarkdownStyling.swift as free functions or a MarkdownStyler type taking NSTextStorage + settings snapshot.
This is the highest-value extraction: makes the styling engine unit-testable against a hand-built NSTextStorage.
Must keep behavior bit-exact — add golden tests (input markdown → expected attribute runs) before moving.
loadFileHistory(), selectCommit(), and the history modal UI (EditorView 290–505) → GitHistoryModal.swift.
While there: the synchronous GitService init on main thread (464, 480) and synchronous commit content fetch (487) should be made async.
Phase 9 — Extract RawEditor.Coordinator
Move RawEditor + its Coordinator to RawEditor.swift + RawEditorCoordinator.swift.
The debounce interval (80ms, line 1048) becomes a named constant.
Phase 10 — Final cleanup
EditorView.swift should be <500 lines holding just the SwiftUI view.
Audit the consumePending*() helpers (lines 6–36) — if chore: refactor god object #237 has moved pending signals to EditorState, update these to read from the store directly. Otherwise leave as-is and delete in the chore: refactor god object #237 follow-up.
Testability milestones
After Phase 5: MarkdownStyler has golden tests covering headings, code blocks, wiki-links, tags, callouts, blockquotes, frontmatter — runs in <1s, no SwiftUI.
After Phase 4: EmbeddedImageView can be instantiated in a test window without touching the network (inject the URLSession).
After Phase 7: LinkAwareTextView responsibilities are file-local; reading any one concern no longer requires scrolling past 1,400 lines.
Concerns to address while touching this code
Synchronous file reads during styling (lines 3180, 3240, 3333, 3344): String(contentsOf:) / Data(contentsOf:) for inline image resolution. Move to background + cache.
Synchronous FileManager.fileExists / createFile for debug logging (2198–2205). Delete or gate behind a debug flag.
Silent git failures (464–467, 480–485) — surface to UI or log properly.
Recursive collectLinkAwareTextViews() (60–68) has no cycle detection. Unlikely to cycle in practice but worth a depth cap.
Risks
Bit-exact styling preservation. The markdown styling engine is regex-heavy and visually sensitive. Add golden tests before moving it.
Objective-C runtime interaction.objc_getAssociatedObject usage must be preserved carefully when extracting — these associations are keyed by static pointer and must stay unique across the split.
AppState coupling remains. This issue does not decouple EditorView from AppState — that's chore: refactor god object #237's job. The two refactors can proceed in parallel as long as we don't fight over the same file at the same time.
Context
Follow-up to #237 (AppState god-object refactor).
EditorView.swiftis the single largest file in the macOS app at 5,436 lines and contains at least 17 top-level types. Much of its size is not the SwiftUI view itself but a collection of AppKit classes, NSViewRepresentables, helper DTOs, and a ~410-line markdown styling engine that all happen to live in the same file.Goal
Split
EditorView.swiftinto focused, single-purpose files so that:EditorViewSwiftUI struct is <500 lines and reads like a view, not a framework.LinkAwareTextView, ~1,400 lines on its own) is split by responsibility.Non-goals: behavior changes, visual changes, or rewriting the regex-based styling. This is pure file-level extraction.
Current structure (evidence)
Top-level types in
EditorView.swift:EditorView(struct)RawEditor(struct)LinkAwareTextViewRawEditor.Coordinator(class)MarkdownTheme(struct)EditorFontSignature(struct)NSAttributedString.KeyextensionLinkAwareTextViewextension #1LinkAwareTextView(class)HTMLToMarkdownConverter(struct)InlineImageMatch/InlineEmbedMatch/EmbeddedNoteInfo/SidebarEmbedType/SidebarEmbedInfoEmbeddedNotesPanel(struct)FlippedNSView(class)EmbeddedNoteView(class)EmbeddedImageView(class)CompletionViewController(class)CompletionViewControllerextensionsCodeBlockMatch(struct)LinkAwareTextViewextension #2MarkdownPreviewView(struct)AppState coupling: 61 call sites into
appState, of which:appState.settings— 19 reads (font family, size, line height, hide-markdown flag)pendingCursorPosition,pendingCursorRange,pendingScrollOffsetY,pendingSearchQuery,pendingCursorTargetPaneIndex) — consumed viaconsumePending*()helpers at lines 6–36 (read-and-null pattern)openFile,openFileInNewTab,openTagInNewTab,switchTab,focusPane,createNote,presentCommandPalettewikiLinkCompletionHandler/wikiLinkDismissHandlerclosures stored on AppStateTarget file layout
Approx line totals: 5,436 → ~3,800 across ~15 files (some gains from removing duplicated helpers).
Phased plan
Each phase is one PR, compiles, runs smoke tests from #237 Phase 0, and can be reverted.
Phase 1 — Leaf extractions (safe, no coupling changes)
Move types that have no other types depending on them inside the file:
MarkdownTheme+EditorFontSignature+NSAttributedString.Keyextension →MarkdownStyling.swiftHTMLToMarkdownConverter→HTMLToMarkdownConverter.swiftInlineImageMatch,InlineEmbedMatch,CodeBlockMatch,EmbeddedNoteInfo,SidebarEmbedType,SidebarEmbedInfo) →InlineEmbeds/InlineEmbedMatchers.swiftFlippedNSView→ co-locate withEmbeddedNotesPanelextraction in Phase 4Phase 2 — Extract
MarkdownPreviewViewMarkdownPreviewView(5142–5436) is a self-contained NSViewRepresentable over WKWebView.MarkdownPreviewView.swift. Done.Phase 3 — Extract
CompletionViewController(wiki-link picker)CompletionViewController+ its extensions (4780–4980) toWikiLinkPicker.swift.wikiLinkCompletionHandler/wikiLinkDismissHandlerclosures on AppState remain for now — untangling those is deferred to chore: refactor god object #237.Phase 4 — Extract inline embed views
EmbeddedNotesPanel,EmbeddedNoteView,EmbeddedImageView,YouTubePreviewView,FlippedNSVieweach to their own file underInlineEmbeds/.EmbeddedImageView(line ~4646) while it's touched: cancel the data task inremoveFromSuperview().Phase 5 — Extract markdown styling engine
applyMarkdownStyling()(1640–2050) andapplyPreviewStyling()(1474–1630) move toMarkdownStyling.swiftas free functions or aMarkdownStylertype takingNSTextStorage+ settings snapshot.NSTextStorage.Phase 6 — Extract code-block copy buttons + collapsible sections
LinkAwareTextViewextension feat: add ability to clone remote repository for notes sync #2 (4995–5140) →CodeBlockCopyButtons.swift.CollapsibleSections.swift.objc_getAssociatedObject/objc_setAssociatedObjectpattern (4997–5003) with a typed wrapper while touching it.Phase 7 — Split
LinkAwareTextViewby responsibilityLargest phase.
LinkAwareTextView(2216–3595) is ~1,400 lines; split into:LinkAwareTextView.swift(event handling, init, properties)+Drawing.swift—drawBackground(in:)and full-width backgrounds (2276–2366)+Tracking.swift— hover + tracking areas (2400–2450)+Search.swift— search highlighting via notifications (2609–2700)+SlashCommands.swift— slash-command expansion (3150–3300), merge with existingSlashCommands.swiftif possible+TablePrettifier.swift— table pretty-printing (3400–3500)Phase 8 — Extract git history modal
loadFileHistory(),selectCommit(), and the history modal UI (EditorView 290–505) →GitHistoryModal.swift.GitServiceinit on main thread (464, 480) and synchronous commit content fetch (487) should be made async.Phase 9 — Extract
RawEditor.CoordinatorRawEditor+ itsCoordinatortoRawEditor.swift+RawEditorCoordinator.swift.Phase 10 — Final cleanup
EditorView.swiftshould be <500 lines holding just the SwiftUI view.consumePending*()helpers (lines 6–36) — if chore: refactor god object #237 has moved pending signals toEditorState, update these to read from the store directly. Otherwise leave as-is and delete in the chore: refactor god object #237 follow-up.Testability milestones
MarkdownStylerhas golden tests covering headings, code blocks, wiki-links, tags, callouts, blockquotes, frontmatter — runs in <1s, no SwiftUI.EmbeddedImageViewcan be instantiated in a test window without touching the network (inject the URLSession).LinkAwareTextViewresponsibilities are file-local; reading any one concern no longer requires scrolling past 1,400 lines.Concerns to address while touching this code
String(contentsOf:)/Data(contentsOf:)for inline image resolution. Move to background + cache.FileManager.fileExists/createFilefor debug logging (2198–2205). Delete or gate behind a debug flag.collectLinkAwareTextViews()(60–68) has no cycle detection. Unlikely to cycle in practice but worth a depth cap.Risks
objc_getAssociatedObjectusage must be preserved carefully when extracting — these associations are keyed by static pointer and must stay unique across the split.Out of scope
FileTreeViewandSettingsManagerbreakups — separate issues.