From 9ca29fc54ce05636964ea3f9101f5a3ebc4727cd Mon Sep 17 00:00:00 2001 From: darshan-jahagirdar Date: Tue, 12 May 2026 17:38:16 +0530 Subject: [PATCH 1/2] Store view regions in undo history --- edit.go | 37 +++++++++++++++++++++++++++++++++---- undo.go | 2 ++ undo_test.go | 39 +++++++++++++++++++++++++++++++++++++++ view.go | 1 + 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/edit.go b/edit.go index 1ced5ab..5e7fc22 100644 --- a/edit.go +++ b/edit.go @@ -7,6 +7,7 @@ package backend import ( "fmt" + "github.com/limetext/backend/render" "github.com/limetext/text" ) @@ -20,6 +21,9 @@ type ( invalid bool composite text.CompositeAction savedSel text.RegionSet + finalSel text.RegionSet + savedRegs render.ViewRegionMap + finalRegs render.ViewRegionMap savedCount int command string args Args @@ -36,9 +40,36 @@ func newEdit(v *View) *Edit { for _, r := range v.Sel().Regions() { ret.savedSel.Add(r) } + ret.savedRegs = cloneViewRegionMap(v.regions) return ret } +func cloneViewRegionMap(src render.ViewRegionMap) render.ViewRegionMap { + if src == nil { + return nil + } + dst := make(render.ViewRegionMap, len(src)) + for key, regions := range src { + dst[key] = *regions.Clone() + } + return dst +} + +func copyRegionSet(dst *text.RegionSet, src text.RegionSet) { + dst.Clear() + dst.AddAll(src.Regions()) +} + +func (e *Edit) saveFinalRegions() { + copyRegionSet(&e.finalSel, *e.v.Sel()) + e.finalRegs = cloneViewRegionMap(e.v.regions) +} + +func (e *Edit) restoreRegions(sel text.RegionSet, regs render.ViewRegionMap) { + copyRegionSet(e.v.Sel(), sel) + e.v.regions = cloneViewRegionMap(regs) +} + // Returns a string describing this Edit object. Should typically not be manually called. func (e *Edit) String() string { return fmt.Sprintf("%s: %v, %v, %v", e.command, e.args, e.bypassUndo, e.composite) @@ -47,13 +78,11 @@ func (e *Edit) String() string { // Applies the action of this Edit object. Should typically not be manually called. func (e *Edit) Apply() { e.composite.Apply() + e.restoreRegions(e.finalSel, e.finalRegs) } // Reverses the application of this Edit object. Should typically not be manually called. func (e *Edit) Undo() { e.composite.Undo() - e.v.Sel().Clear() - for _, r := range e.savedSel.Regions() { - e.v.Sel().Add(r) - } + e.restoreRegions(e.savedSel, e.savedRegs) } diff --git a/undo.go b/undo.go index d8085e8..b91873d 100644 --- a/undo.go +++ b/undo.go @@ -121,6 +121,7 @@ func (us *UndoStack) GlueFrom(mark int) { } e.v = us.actions[mark].v e.savedSel.AddAll(us.actions[mark].savedSel.Regions()) + e.savedRegs = cloneViewRegionMap(us.actions[mark].savedRegs) entries := make([]entry, us.position-mark) for i := range entries { @@ -133,5 +134,6 @@ func (us *UndoStack) GlueFrom(mark int) { us.actions = us.actions[:mark+1] e.args = make(Args) e.args["commands"] = entries + e.saveFinalRegions() us.Add(&e) } diff --git a/undo_test.go b/undo_test.go index 621c831..48e85cb 100644 --- a/undo_test.go +++ b/undo_test.go @@ -5,6 +5,7 @@ package backend import ( + "reflect" "testing" "github.com/limetext/text" @@ -207,3 +208,41 @@ func TestUndoStackGlueFrom(t *testing.T) { t.Errorf("Expected the UndoStack to only contain 2 things, but it had %d", len(us.actions)) } } + +func TestUndoStackRestoresViewRegions(t *testing.T) { + w := GetEditor().NewWindow() + defer w.Close() + + v := w.NewFile() + defer func() { + v.SetScratch(true) + v.Close() + }() + + e := v.BeginEdit() + v.Insert(e, 0, "abcdef") + v.EndEdit(e) + + const key = "test" + original := []text.Region{{A: 1, B: 3}, {A: 4, B: 6}} + v.AddRegions(key, original, "scope", "icon", 0) + + e = v.BeginEdit() + v.Erase(e, text.Region{A: 0, B: v.Size()}) + v.EndEdit(e) + + afterErase := v.GetRegions(key) + if reflect.DeepEqual(afterErase, original) { + t.Fatalf("expected edit to adjust regions before undo, got unchanged regions %v", afterErase) + } + + v.undoStack.Undo(true) + if got := v.GetRegions(key); !reflect.DeepEqual(got, original) { + t.Fatalf("expected undo to restore regions %v, got %v", original, got) + } + + v.undoStack.Redo(true) + if got := v.GetRegions(key); !reflect.DeepEqual(got, afterErase) { + t.Fatalf("expected redo to restore edited regions %v, got %v", afterErase, got) + } +} diff --git a/view.go b/view.go index bfa4298..0e091c9 100644 --- a/view.go +++ b/view.go @@ -431,6 +431,7 @@ func (v *View) EndEdit(edit *Edit) { for j := len(v.editstack) - 1; j >= i; j-- { current_edit := v.editstack[j] current_edit.invalid = true + current_edit.saveFinalRegions() sel_same := reflect.DeepEqual(*v.Sel(), current_edit.savedSel) buf_same := v.ChangeCount() == current_edit.savedCount eq := (sel_same && buf_same && current_edit.composite.Len() == 0) From b896afbe4052db25b2d65fbd32c41a7bca1ba633 Mon Sep 17 00:00:00 2001 From: darshan-jahagirdar Date: Tue, 12 May 2026 17:58:45 +0530 Subject: [PATCH 2/2] Test glued undo region snapshots --- undo_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/undo_test.go b/undo_test.go index 48e85cb..40069ce 100644 --- a/undo_test.go +++ b/undo_test.go @@ -246,3 +246,49 @@ func TestUndoStackRestoresViewRegions(t *testing.T) { t.Fatalf("expected redo to restore edited regions %v, got %v", afterErase, got) } } + +func TestUndoStackGlueFromRestoresViewRegions(t *testing.T) { + w := GetEditor().NewWindow() + defer w.Close() + + v := w.NewFile() + defer func() { + v.SetScratch(true) + v.Close() + }() + + e := v.BeginEdit() + v.Insert(e, 0, "abcdef") + v.EndEdit(e) + + mark := v.undoStack.Position() + + const key = "test" + original := []text.Region{{A: 1, B: 3}, {A: 4, B: 6}} + v.AddRegions(key, original, "scope", "icon", 0) + + e = v.BeginEdit() + v.Erase(e, text.Region{A: 1, B: 2}) + v.EndEdit(e) + + e = v.BeginEdit() + v.Insert(e, 2, "Z") + v.EndEdit(e) + + final := v.GetRegions(key) + if reflect.DeepEqual(final, original) { + t.Fatalf("expected edits to adjust regions before glue, got unchanged regions %v", final) + } + + v.undoStack.GlueFrom(mark) + + v.undoStack.Undo(true) + if got := v.GetRegions(key); !reflect.DeepEqual(got, original) { + t.Fatalf("expected glued undo to restore regions %v, got %v", original, got) + } + + v.undoStack.Redo(true) + if got := v.GetRegions(key); !reflect.DeepEqual(got, final) { + t.Fatalf("expected glued redo to restore regions %v, got %v", final, got) + } +}