Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@ import AppKit

extension NativeTextViewCoordinator {

/// Supplies a per-document `UndoManager` to the text view.
///
/// AppKit reuses one `NSTextView` across every open document, so the built-in
/// view-wide undo manager would blend files together (and used to be wiped on
/// each switch). Returning a manager keyed on the current `documentId` gives
/// each file its own undo stack that survives switching away and back.
/// Returning the *same* instance for a given document on every call is
/// required — a fresh manager per call breaks undo.
public func undoManager(for view: NSTextView) -> UndoManager? {
let key = documentId ?? "__default__"
if let existing = undoManagers[key] {
return existing
}
let manager = UndoManager()
undoManagers[key] = manager
return manager
}

/// Force base typingAttributes on every change so AppKit's auto-inheritance
/// can't bleed a heading paragraphStyle into the trailing extra-line
/// fragment's metrics.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ public final class NativeTextViewCoordinator: NSObject, NSTextViewDelegate {
/// Remembered scroll offset (`bounds.origin.y`) per `documentId` — saved on
/// switch-away, restored on switch-back.
var scrollOffsets: [String: CGFloat] = [:]
/// Per-`documentId` undo manager. AppKit reuses a single `NSTextView` across
/// all open documents, so its built-in (view-wide) undo manager would mix
/// files together. Keying a manager on the current document gives each file
/// its own undo stack that survives switching away and back. Vended through
/// the `undoManager(for:)` delegate method; pruned alongside `scrollOffsets`.
var undoManagers: [String: UndoManager] = [:]
@Binding var text: String
@Binding var isWikiLinkActive: Bool
var fontName: String
Expand Down
17 changes: 16 additions & 1 deletion Sources/MarkdownEngine/TextView/NativeTextViewWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,16 @@ public struct NativeTextViewWrapper: NSViewRepresentable {
$0.key == documentId || retained.contains($0.key)
}
}
// Evict undo stacks for documents no longer retained (keep the
// current one). removeAllActions() first so a stale registered undo
// can't later fire against a swapped-out document.
let staleUndoKeys = context.coordinator.undoManagers.keys.filter { key in
key != documentId && key != "__default__" && !retained.contains(key)
}
for key in staleUndoKeys {
context.coordinator.undoManagers[key]?.removeAllActions()
context.coordinator.undoManagers.removeValue(forKey: key)
}
}

let wtActive: Bool = {
Expand Down Expand Up @@ -479,8 +489,13 @@ public struct NativeTextViewWrapper: NSViewRepresentable {
retainedScrollDocumentIds?.contains(outgoingId) ?? true {
context.coordinator.scrollOffsets[outgoingId] = nsView.contentView.bounds.origin.y
}
// Per-document undo: close the OUTGOING document's open coalescing group
// (while its manager is still active), then switch the active documentId so
// `undoManager(for:)` starts vending the INCOMING document's own manager. We
// no longer clear undo here — that `removeAllActions()` is what killed Cmd+Z
// across a file switch.
textView.breakUndoCoalescing()
context.coordinator.documentId = documentId
textView.undoManager?.removeAllActions()
context.coordinator.didInitialFormatting = false
context.coordinator.didEnsureLayoutForCurrentDocument = false
context.coordinator.resetImageEmbedState()
Expand Down
Loading