โ– humdrum codex / glint v1.0.2
license AGPL-3.0
4.1 KB raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package editor

import (
	"strings"
	"testing"

	tea "github.com/charmbracelet/bubbletea"
)

// typeRunes feeds each rune as a KeyRunes message (the typing path).
func typeRunes(e *Editor, s string) {
	for _, r := range s {
		e.HandleKey(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{r}})
	}
}

func TestUndoCoalescesTyping(t *testing.T) {
	e := newEditorWith("")
	typeRunes(e, "abc")
	if e.Lines[0] != "abc" {
		t.Fatalf("setup: %q", e.Lines[0])
	}
	e.Undo()
	if e.Lines[0] != "" {
		t.Errorf("one undo should revert the whole typing run, got %q", e.Lines[0])
	}
}

func TestUndoStructuralOpsAreSeparateGroups(t *testing.T) {
	e := newEditorWith("")
	typeRunes(e, "ab")
	e.HandleKey(tea.KeyMsg{Type: tea.KeyEnter}) // structural: own group
	typeRunes(e, "cd")
	if len(e.Lines) != 2 || e.Lines[1] != "cd" {
		t.Fatalf("setup: %v", e.Lines)
	}
	e.Undo() // removes "cd"
	if len(e.Lines) != 2 || e.Lines[1] != "" {
		t.Fatalf("after undo cd: %v", e.Lines)
	}
	e.Undo() // removes the newline
	if len(e.Lines) != 1 || e.Lines[0] != "ab" {
		t.Fatalf("after undo newline: %v", e.Lines)
	}
	e.Undo() // removes "ab"
	if e.Lines[0] != "" {
		t.Errorf("after undo ab: %q", e.Lines[0])
	}
}

func TestUndoRestoresCursorAndSelection(t *testing.T) {
	e := newEditorWith("hello world")
	// Select "world" then type over it.
	e.Cursor = Position{Row: 0, Col: 6}
	e.HandleKey(tea.KeyMsg{Type: tea.KeyShiftEnd}) // selects "world"
	if e.SelectedText() != "world" {
		t.Fatalf("setup selection: %q", e.SelectedText())
	}
	e.HandleKey(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'X'}})
	if e.Lines[0] != "hello X" {
		t.Fatalf("after type-over: %q", e.Lines[0])
	}
	e.Undo()
	if e.Lines[0] != "hello world" {
		t.Errorf("undo content: %q", e.Lines[0])
	}
	if !e.HasSelection() || e.SelectedText() != "world" {
		t.Errorf("undo should restore the selection, got %q", e.SelectedText())
	}
}

func TestRedoReappliesUndoneEdit(t *testing.T) {
	e := newEditorWith("")
	typeRunes(e, "abc")
	e.Undo()
	e.Redo()
	if e.Lines[0] != "abc" {
		t.Errorf("redo should reapply, got %q", e.Lines[0])
	}
}

func TestNewEditClearsRedoStack(t *testing.T) {
	e := newEditorWith("")
	typeRunes(e, "abc")
	e.Undo() // now "" with redo available
	typeRunes(e, "z")
	e.Redo() // redo stack should be empty โ†’ no-op
	if e.Lines[0] != "z" {
		t.Errorf("redo after a new edit must be a no-op, got %q", e.Lines[0])
	}
}

func TestUndoOnEmptyHistoryIsNoOp(t *testing.T) {
	e := newEditorWith("seed")
	e.Undo()
	e.Redo()
	if e.Lines[0] != "seed" {
		t.Errorf("undo/redo with no history changed buffer: %q", e.Lines[0])
	}
}

func TestSetContentResetsHistory(t *testing.T) {
	e := newEditorWith("")
	typeRunes(e, "abc")
	e.SetContent([]byte("fresh"))
	e.Undo()
	if e.Lines[0] != "fresh" {
		t.Errorf("SetContent must clear undo history, got %q", e.Lines[0])
	}
}

func TestHistoryIsBounded(t *testing.T) {
	e := newEditorWith("")
	// Each Enter is its own structural group; exceed the cap.
	for i := 0; i < maxUndo+50; i++ {
		e.HandleKey(tea.KeyMsg{Type: tea.KeyEnter})
	}
	lines := len(e.Lines)
	for i := 0; i < maxUndo+50; i++ {
		e.Undo()
	}
	// Capped history can't undo every newline; the oldest are dropped.
	if len(e.Lines) == 1 {
		t.Errorf("history should be bounded; expected residual newlines, got fully unwound from %d", lines)
	}
}

func TestNoOpKeyDoesNotRecord(t *testing.T) {
	e := newEditorWith("x")
	typeRunes(e, "y") // "xy"... actually inserts at col0 unless cursor moved
	// Move to very start and backspace (no-op at row0,col0).
	e.Cursor = Position{Row: 0, Col: 0}
	e.HandleKey(tea.KeyMsg{Type: tea.KeyBackspace}) // no-op
	before := strings.Join(e.Lines, "\n")
	e.Undo() // should undo the typing, not the no-op backspace
	if strings.Join(e.Lines, "\n") == before {
		t.Errorf("a no-op key must not consume an undo slot")
	}
}

func TestPushUndoSupportsAppDrivenEdits(t *testing.T) {
	e := newEditorWith("hello")
	e.Cursor = Position{Row: 0, Col: 5}
	e.PushUndo() // app records before a paste/cut
	e.InsertText(" world")
	if e.Lines[0] != "hello world" {
		t.Fatalf("setup: %q", e.Lines[0])
	}
	e.Undo()
	if e.Lines[0] != "hello" {
		t.Errorf("PushUndo+Undo should revert app edit, got %q", e.Lines[0])
	}
}