mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
widget: implement editor undo/redo
This commit adds a simple linear-history undo/redo mechanism to widget.Editor bound to Short-(Shift)-Z as well as tests for this new feature. Notes on the implementation: - using a slice to hold the history does mean that we incur allocations as the user types, but I hope that the Go slice growth heuristic means that the number of times we pay this penalty is very small. We also never shrink the slice in this implementation, which ensures that undoing work and then making additional modifications is very efficient, but could be framed as a memory leak. - this implementation creates a new history element every time we call replace(). This means that, on desktop, it's essentially one per rune of input. Users likely want to be able to undo larger units of change, so a future improvement could be to coalesce changes so long as the selection doesn't change between them. - I think it's possible to store only one of the Apply/Reverse change contents in the history slice, but it's significantly more complicated. To implement this, you'd need to add a field indicating if the modification represented a forward or backward change, and then rewrite the modification's content as you performed undo/redo operations.For the time being, I'm not sure it's worth this complexity. - Future work could introduce a limit to the number of history entries stored. If we did this, we should also change the data structure for storing history. Enforcing such a limit using a simple slice like this would be extremely inefficient. Perhaps a ring buffer or a linked list would make more sense? - Applications will likely want to be able to manipulate undo history in the future. We may wish to export undo() and redo() from the editor. Applications will also likely want a mechanism to save the undo history to disk and restore it (implementing persistent undo). I'm not sure what the most suitable API for that is yet, so I decided not to try to tackle it yet. Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
@@ -36,6 +36,64 @@ var english = system.Locale{
|
||||
Direction: system.LTR,
|
||||
}
|
||||
|
||||
// TestEditorHistory ensures that undo and redo behave correctly.
|
||||
func TestEditorHistory(t *testing.T) {
|
||||
e := new(Editor)
|
||||
// Insert some multi-byte unicode text.
|
||||
e.SetText("안П你 hello 안П你")
|
||||
assertContents(t, e, "안П你 hello 안П你", 0, 0)
|
||||
// Overwrite all of the text with the empty string.
|
||||
e.SetCaret(0, len([]rune("안П你 hello 안П你")))
|
||||
e.Insert("")
|
||||
assertContents(t, e, "", 0, 0)
|
||||
// Ensure that undoing the overwrite succeeds.
|
||||
e.undo()
|
||||
assertContents(t, e, "안П你 hello 안П你", 13, 0)
|
||||
// Ensure that redoing the overwrite succeeds.
|
||||
e.redo()
|
||||
assertContents(t, e, "", 0, 0)
|
||||
// Insert some smaller text.
|
||||
e.Insert("안П你 hello")
|
||||
assertContents(t, e, "안П你 hello", 9, 9)
|
||||
// Replace a region in the middle of the text.
|
||||
e.SetCaret(1, 5)
|
||||
e.Insert("П")
|
||||
assertContents(t, e, "안Пello", 2, 2)
|
||||
// Replace a second region in the middle.
|
||||
e.SetCaret(3, 4)
|
||||
e.Insert("П")
|
||||
assertContents(t, e, "안ПeПlo", 4, 4)
|
||||
// Ensure both operations undo successfully.
|
||||
e.undo()
|
||||
assertContents(t, e, "안Пello", 4, 3)
|
||||
e.undo()
|
||||
assertContents(t, e, "안П你 hello", 5, 1)
|
||||
// Make a new modification.
|
||||
e.Insert("Something New")
|
||||
// Ensure that redo history is discarded now that
|
||||
// we've diverged from the linear editing history.
|
||||
// This redo() call should do nothing.
|
||||
text := e.Text()
|
||||
start, end := e.Selection()
|
||||
e.redo()
|
||||
assertContents(t, e, text, start, end)
|
||||
}
|
||||
|
||||
func assertContents(t *testing.T, e *Editor, contents string, selectionStart, selectionEnd int) {
|
||||
t.Helper()
|
||||
actualContents := e.Text()
|
||||
if actualContents != contents {
|
||||
t.Errorf("expected editor to contain %s, got %s", contents, actualContents)
|
||||
}
|
||||
actualStart, actualEnd := e.Selection()
|
||||
if actualStart != selectionStart {
|
||||
t.Errorf("expected selection start to be %d, got %d", selectionStart, actualStart)
|
||||
}
|
||||
if actualEnd != selectionEnd {
|
||||
t.Errorf("expected selection end to be %d, got %d", selectionEnd, actualEnd)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEditorZeroDimensions ensures that an empty editor still reserves
|
||||
// space for displaying its caret when the constraints allow for it.
|
||||
func TestEditorZeroDimensions(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user