Skip to content

refactor: migrate undo system from custom stacks to NSUndoManager#599

Merged
datlechin merged 11 commits intomainfrom
refactor/nsundo-manager-migration
Apr 6, 2026
Merged

refactor: migrate undo system from custom stacks to NSUndoManager#599
datlechin merged 11 commits intomainfrom
refactor/nsundo-manager-migration

Conversation

@datlechin
Copy link
Copy Markdown
Collaborator

Summary

Replaces two custom LIFO undo stack classes (DataChangeUndoManager, StructureUndoManager) with Apple's NSUndoManager. This is a full migration — the custom classes are deleted.

What changes:

  • Edit > Undo now shows proper action names: "Undo Edit Cell", "Undo Delete Row", "Undo Add Column", etc.
  • NSUndoManager handles redo stack management automatically (no more isRedoing flag)
  • Responder chain integration works correctly with menu validation
  • levelsOfUndo = 100 preserves the existing stack depth limit

What stays the same:

  • All change tracking logic (changes array, changeIndex, modifiedCells, O(1) lookups) is preserved
  • Text editor's CEUndoManager is untouched (dependency code)
  • Command routing in TableProApp.swift stays as-is
  • Public API of DataChangeManager and StructureChangeManager is unchanged

Phase 1 — StructureChangeManager (-196 lines):

  • Deleted StructureUndoManager.swift
  • Moved SchemaUndoAction enum into StructureChangeManager
  • Created applySchemaUndo(_:) — single method that applies inverse + registers reverse
  • All 15 push() calls → registerUndo(withTarget:) + setActionName()

Phase 2 — DataChangeManager (-274 lines):

  • Deleted DataChangeUndoManager.swift
  • Created applyDataUndo(_:) for all 5 UndoAction cases
  • Unified undoLastChange()/redoLastChange() with UndoResult return type
  • groupsByEvent = false prevents auto-grouping of independent edits
  • Batch operations use beginUndoGrouping()/endUndoGrouping()

Net: -635 lines of custom undo code deleted

Test plan

  • Edit a cell → Edit menu shows "Undo Edit Cell" with ⌘Z shortcut
  • Delete a row → Edit menu shows "Undo Delete Row"
  • Batch delete 3 rows → single "Undo Delete Rows"
  • Undo cell edit → value reverts, Edit menu shows "Redo Edit Cell"
  • Redo cell edit → value re-applies
  • Multiple undo/redo cycles → consistent state
  • New edit after undo → redo is cleared
  • 100+ edits → oldest undo entries are dropped
  • Structure view: edit column → "Undo Edit Column"
  • Structure view: add/delete column → proper action names
  • Structure view: undo/redo cycles work correctly
  • SQL editor undo (Cmd+Z in editor) → still works independently
  • hasChanges updates correctly (save/discard buttons enable/disable)

datlechin added 11 commits April 6, 2026 14:52
- Fix primaryKeyChange undo closure capturing stale workingPrimaryKey
- Fix batch insertion undo leaking insertedRowData entries
- Clear undo history in loadSchema to prevent stale actions on table switch
- Make undoManager private to prevent bypass of lastUndoResult contract
…ivity crash

NSHostingView wrapping MainContentView creates a separate SwiftUI evaluation
context. When updateNSView sets rootView, the inner hosting view re-evaluates
MainContentView.body, which re-entrantly accesses @observable properties
(QueryTabManager.tabs) already being read by the outer tree — triggering a
Swift exclusivity violation crash.

The PanelResizeHandle approach keeps MainContentView in the same SwiftUI tree,
avoiding re-entrant access.
With groupsByEvent=false, NSUndoManager requires explicit beginUndoGrouping()
before registerUndo(). Without it, registerUndo throws an exception that AppKit
catches silently, causing all undo registration to be no-ops. This broke cell
edit commit — changes were recorded in the tracking arrays but never registered
with the undo manager.

The default groupsByEvent=true auto-creates groups per run loop pass, which is
correct for individual cell edits.
- rowInsertion: capture savedValues BEFORE undoRowInsertion clears them,
  register reverse AFTER redo repopulates insertedRowData
- batchRowInsertion redo: restore insertedRowData for each re-inserted row
  so SQL generation produces correct INSERT values
StructureChangeManager:
- S1: Distinguish undo-of-new-add vs redo-of-existing-delete in columnAdd/Delete,
  indexAdd/Delete, foreignKeyAdd/Delete by checking currentColumns/Indexes/FKs
- S2: Update pendingChanges for newly-added items on undo of edit (add else branches)
- S3: Call validate() after every undo/redo to keep validationErrors fresh

DataChangeManager:
- D1: Populate insertedRowData in batchRowInsertion redo path
- D2: Shift existing row indices up when redoing row insertion (new shiftRowIndicesUp helper)
- D3: Shift insertedRowData and modifiedCells keys in undoRowInsertion
- D4: Use rows.contains instead of rows.first for batch deletion undo/redo detection
- D5: Set groupsByEvent=false with explicit beginUndoGrouping/endUndoGrouping
  via registerUndoAction helper to prevent run-loop auto-grouping
@datlechin datlechin merged commit ab8e269 into main Apr 6, 2026
2 checks passed
@datlechin datlechin deleted the refactor/nsundo-manager-migration branch April 6, 2026 08:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant