Skip to content

Undo history not invalidated on save — index-based commands can corrupt after reconstructChords() #18

Description

@byrongamatos

Summary

The editor's undo commands store note array indices captured at construction time. On save, _buildSaveBody() runs flattenChords() + reconstructChords(), which rewrites arr.notes (and relocates same-time multi-note groups into arr.chords) — but the undo history is not cleared. So undoing a command created before a save can index into the wrong note or hit undefined.

Surfaced during the post-merge review of #16 (teaching marks). SetTeachingMarkCmd was flagged, but it is not the origin — it copies the exact index pattern used by every existing command.

Root cause

  • Commands snapshot this.indices / this.index into notes() at construction and roll back via notes()[i]: ChangeFretCmd, ResizeSustainCmd, MoveNoteCmd, SetBendShapeCmd, SetBendIntentCmd, SetTeachingMarkCmd, … (screen.js).
  • reconstructChords() does arr.notes = newNotes and moves same-time groups into arr.chords (screen.js ~line 666/714/728).
  • S.history = new EditHistory() runs only on song load (screen.js ~3173), not on save (_buildSaveBody ~3453).

Net: (stale undo stack) + (renumbered arr.notes after save) → wrong-note or undefined mutation on undo.

Why it's more reachable via teaching marks

"Group as strum" (ch) operates on multi-note same-time selections — exactly the groups reconstructChords() moves out of arr.notes into arr.chords. So that workflow is more likely than a typical single-note edit to have its indices invalidated by the next save.

Repro (sketch)

  1. Select a 2+ note same-time group, "Group as strum" (or any index-based edit).
  2. Save (sloppak or chart) → reconstructChords() moves those notes into a chord, rewriting arr.notes.
  3. Undo → the command indexes into the post-reconstruct arr.notes and mutates the wrong note / throws.

Options

  1. Invalidate the undo/redo stack on successful save (mirror the load-time new EditHistory()). Simplest and correct; cost is losing cross-save undo.
  2. Identity-stable targets — give notes a stable id (or store object refs) and have commands resolve targets by id at exec/rollback, with reconstructChords() preserving identity. More work, keeps cross-save undo.
  3. Re-key the live history after reconstructChords() by mapping old→new positions. Fiddly; (1) or (2) preferred.

Recommend (1) as an immediate, low-risk fix; consider (2) later if cross-save undo is wanted.

Scope / honesty

Pre-existing and systemic — affects all index-based commands, not specific to teaching marks. No data-loss on the happy path (no save between edit and undo). Filing separately rather than patching one command.

Part of got-feedBack/feedBack#334 (surfaced during the teaching-marks review loop).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions