362 lines
12 KiB
Go
362 lines
12 KiB
Go
package editor
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// ==================================================
|
|
// P0: Basic Recording Tests
|
|
// ==================================================
|
|
|
|
func TestDotOperatorRecording(t *testing.T) {
|
|
t.Run("records simple delete", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
sendKeys(tm, "x")
|
|
|
|
m := getFinalModel(t, tm)
|
|
keys := m.LastChangeKeys()
|
|
if len(keys) != 1 || keys[0] != "x" {
|
|
t.Errorf("LastChangeKeys() = %v, want [\"x\"]", keys)
|
|
}
|
|
|
|
// Also verify . register
|
|
reg, ok := m.GetRegister('.')
|
|
if !ok {
|
|
t.Fatal("dot register not found")
|
|
}
|
|
if reg.Content[0] != "x" {
|
|
t.Errorf("dot register = %q, want \"x\"", reg.Content[0])
|
|
}
|
|
})
|
|
|
|
t.Run("records operator motion", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello world"}))
|
|
sendKeys(tm, "d", "w")
|
|
|
|
m := getFinalModel(t, tm)
|
|
keys := m.LastChangeKeys()
|
|
if len(keys) != 2 || keys[0] != "d" || keys[1] != "w" {
|
|
t.Errorf("LastChangeKeys() = %v, want [\"d\", \"w\"]", keys)
|
|
}
|
|
})
|
|
|
|
t.Run("records double press operator", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello", "world"}))
|
|
sendKeys(tm, "d", "d")
|
|
|
|
m := getFinalModel(t, tm)
|
|
keys := m.LastChangeKeys()
|
|
if len(keys) != 2 || keys[0] != "d" || keys[1] != "d" {
|
|
t.Errorf("LastChangeKeys() = %v, want [\"d\", \"d\"]", keys)
|
|
}
|
|
})
|
|
|
|
t.Run("records visual operation", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3"}))
|
|
sendKeys(tm, "V", "j", "x")
|
|
|
|
m := getFinalModel(t, tm)
|
|
keys := m.LastChangeKeys()
|
|
if len(keys) != 3 || keys[0] != "V" || keys[1] != "j" || keys[2] != "x" {
|
|
t.Errorf("LastChangeKeys() = %v, want [\"V\", \"j\", \"x\"]", keys)
|
|
}
|
|
})
|
|
|
|
t.Run("records insert mode", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
sendKeys(tm, "i", "X", "Y", "Z", "esc")
|
|
|
|
m := getFinalModel(t, tm)
|
|
keys := m.LastChangeKeys()
|
|
if len(keys) != 5 || keys[0] != "i" || keys[1] != "X" || keys[2] != "Y" || keys[3] != "Z" || keys[4] != "esc" {
|
|
t.Errorf("LastChangeKeys() = %v, want [\"i\", \"X\", \"Y\", \"Z\", \"esc\"]", keys)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ==================================================
|
|
// P0: Non-Recording Tests
|
|
// ==================================================
|
|
|
|
func TestDotOperatorNonRecording(t *testing.T) {
|
|
t.Run("does not record pure motions", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3", "line4"}))
|
|
sendKeys(tm, "k", "k", "k")
|
|
|
|
m := getFinalModel(t, tm)
|
|
keys := m.LastChangeKeys()
|
|
// Pure motions should result in empty recording
|
|
if len(keys) != 0 {
|
|
t.Errorf("LastChangeKeys() = %v, want []", keys)
|
|
}
|
|
})
|
|
|
|
t.Run("does not record dot operator itself", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
sendKeys(tm, "x", ".", ".")
|
|
|
|
m := getFinalModel(t, tm)
|
|
keys := m.LastChangeKeys()
|
|
// Should still be just ["x"], not ["x", ".", "."]
|
|
if len(keys) != 1 || keys[0] != "x" {
|
|
t.Errorf("LastChangeKeys() = %v, want [\"x\"]", keys)
|
|
}
|
|
})
|
|
|
|
t.Run("does not record command mode entry", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
sendKeys(tm, ":")
|
|
sendKeys(tm, "esc") // Exit command mode
|
|
|
|
m := getFinalModel(t, tm)
|
|
keys := m.LastChangeKeys()
|
|
// Command mode entry should not record
|
|
if len(keys) != 0 {
|
|
t.Errorf("LastChangeKeys() = %v, want []", keys)
|
|
}
|
|
})
|
|
|
|
t.Run("does not record visual mode entry without action", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
sendKeys(tm, "v", "esc")
|
|
|
|
m := getFinalModel(t, tm)
|
|
keys := m.LastChangeKeys()
|
|
// Just entering and exiting visual mode should not record
|
|
if len(keys) != 0 {
|
|
t.Errorf("LastChangeKeys() = %v, want []", keys)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ==================================================
|
|
// P0: Basic Replay Tests
|
|
// ==================================================
|
|
|
|
func TestDotOperatorReplay(t *testing.T) {
|
|
t.Run("repeats simple delete", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
sendKeys(tm, "x") // "ello"
|
|
sendKeys(tm, ".") // "llo"
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveBuffer().Line(0) != "llo" {
|
|
t.Errorf("buffer = %q, want \"llo\"", m.ActiveBuffer().Line(0))
|
|
}
|
|
})
|
|
|
|
t.Run("repeats operator motion", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"one two three"}))
|
|
sendKeys(tm, "d", "w") // "two three"
|
|
sendKeys(tm, ".") // "three"
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveBuffer().Line(0) != "three" {
|
|
t.Errorf("buffer = %q, want \"three\"", m.ActiveBuffer().Line(0))
|
|
}
|
|
})
|
|
|
|
t.Run("repeats double press operator", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3"}))
|
|
sendKeys(tm, "d", "d") // Delete first line -> "line2", "line3"
|
|
sendKeys(tm, ".") // Delete next line -> "line3"
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveBuffer().LineCount() != 1 {
|
|
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
|
|
}
|
|
if m.ActiveBuffer().Line(0) != "line3" {
|
|
t.Errorf("buffer = %q, want \"line3\"", m.ActiveBuffer().Line(0))
|
|
}
|
|
})
|
|
}
|
|
|
|
// ==================================================
|
|
// P1: Recording Replacement Tests
|
|
// ==================================================
|
|
|
|
func TestDotOperatorRecordingReplacement(t *testing.T) {
|
|
t.Run("new action replaces old recording", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello world"}))
|
|
sendKeys(tm, "x") // Record ["x"]
|
|
sendKeys(tm, "l", "l") // Motions clear recording buffer but don't save
|
|
sendKeys(tm, "d", "w") // Record ["d", "w"]
|
|
|
|
m := getFinalModel(t, tm)
|
|
keys := m.LastChangeKeys()
|
|
// Should be the latest action ["d", "w"], not ["x"]
|
|
if len(keys) != 2 || keys[0] != "d" || keys[1] != "w" {
|
|
t.Errorf("LastChangeKeys() = %v, want [\"d\", \"w\"]", keys)
|
|
}
|
|
})
|
|
|
|
t.Run("motions clear recording without saving", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3", "line4"}))
|
|
sendKeys(tm, "x") // Record ["x"]
|
|
sendKeys(tm, "j", "j", "j") // Motions don't overwrite saved recording
|
|
sendKeys(tm, "d", "d") // New action replaces
|
|
|
|
m := getFinalModel(t, tm)
|
|
keys := m.LastChangeKeys()
|
|
// Should be ["d", "d"] from the last modifying action
|
|
if len(keys) != 2 || keys[0] != "d" || keys[1] != "d" {
|
|
t.Errorf("LastChangeKeys() = %v, want [\"d\", \"d\"]", keys)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ==================================================
|
|
// P1: Visual Mode Tests
|
|
// ==================================================
|
|
|
|
func TestDotOperatorVisualMode(t *testing.T) {
|
|
t.Run("repeats visual line operation", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"a", "b", "c", "d", "e"}))
|
|
sendKeys(tm, "V", "j", "x") // Delete lines 0-1 -> "c", "d", "e"
|
|
// Cursor should be at line 0 after deletion
|
|
sendKeys(tm, ".") // Repeat -> delete next 2 lines -> "e"
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveBuffer().LineCount() != 1 {
|
|
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
|
|
}
|
|
if m.ActiveBuffer().Line(0) != "e" {
|
|
t.Errorf("buffer = %q, want \"e\"", m.ActiveBuffer().Line(0))
|
|
}
|
|
})
|
|
}
|
|
|
|
// ==================================================
|
|
// P1: Insert Mode Tests
|
|
// ==================================================
|
|
|
|
func TestDotOperatorInsertMode(t *testing.T) {
|
|
t.Run("repeats insert mode operation", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
sendKeys(tm, "i", "X", "Y", "Z", "esc") // "XYZhello", cursor at col 2 (on Z)
|
|
// Move cursor after XYZ (to col 3, between Z and h)
|
|
sendKeys(tm, "l")
|
|
sendKeys(tm, ".") // Should insert XYZ again at col 3
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveBuffer().Line(0) != "XYZXYZhello" {
|
|
t.Errorf("buffer = %q, want \"XYZXYZhello\"", m.ActiveBuffer().Line(0))
|
|
}
|
|
})
|
|
}
|
|
|
|
// ==================================================
|
|
// P1: Multiple Repeat Tests
|
|
// ==================================================
|
|
|
|
func TestDotOperatorMultipleRepeats(t *testing.T) {
|
|
t.Run("dot can be used multiple times", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello world foo bar"}))
|
|
sendKeys(tm, "d", "w") // "world foo bar"
|
|
sendKeys(tm, ".") // "foo bar"
|
|
sendKeys(tm, ".") // "bar"
|
|
sendKeys(tm, ".") // ""
|
|
|
|
m := getFinalModel(t, tm)
|
|
line := m.ActiveBuffer().Line(0)
|
|
// After deleting 4 words, should be empty or just whitespace
|
|
if line != "" && line != " " {
|
|
t.Errorf("buffer = %q, want empty or space", line)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ==================================================
|
|
// P2: Edge Cases
|
|
// ==================================================
|
|
|
|
func TestDotOperatorEdgeCases(t *testing.T) {
|
|
t.Run("repeat at start of file", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
sendKeys(tm, "x") // "ello"
|
|
sendKeys(tm, ".") // "llo"
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveBuffer().Line(0) != "llo" {
|
|
t.Errorf("buffer = %q, want \"llo\"", m.ActiveBuffer().Line(0))
|
|
}
|
|
})
|
|
|
|
t.Run("repeat after undo", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello world"}))
|
|
sendKeys(tm, "x") // "ello world"
|
|
sendKeys(tm, "u") // Undo -> "hello world"
|
|
sendKeys(tm, ".") // Repeat should still work -> "ello world"
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveBuffer().Line(0) != "ello world" {
|
|
t.Errorf("buffer = %q, want \"ello world\"", m.ActiveBuffer().Line(0))
|
|
}
|
|
})
|
|
|
|
t.Run("repeat with no recorded change", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
sendKeys(tm, ".") // Dot with nothing recorded
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should not crash, buffer should be unchanged
|
|
if m.ActiveBuffer().Line(0) != "hello" {
|
|
t.Errorf("buffer = %q, want \"hello\"", m.ActiveBuffer().Line(0))
|
|
}
|
|
})
|
|
}
|
|
|
|
// ==================================================
|
|
// P2: Integration with Counts
|
|
// ==================================================
|
|
|
|
func TestDotOperatorWithCounts(t *testing.T) {
|
|
t.Run("recording includes count", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello world"}))
|
|
sendKeys(tm, "3", "x")
|
|
|
|
m := getFinalModel(t, tm)
|
|
keys := m.LastChangeKeys()
|
|
// Should record both the count and the action
|
|
if len(keys) != 2 || keys[0] != "3" || keys[1] != "x" {
|
|
t.Errorf("LastChangeKeys() = %v, want [\"3\", \"x\"]", keys)
|
|
}
|
|
})
|
|
|
|
t.Run("repeat preserves count", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
sendKeys(tm, "3", "x") // Delete 3 chars -> "lo"
|
|
sendKeys(tm, ".") // Should delete 3 more (or try to)
|
|
|
|
m := getFinalModel(t, tm)
|
|
// After deleting 3 + 3 = 6 chars, should be empty or have no chars left
|
|
if m.ActiveBuffer().Line(0) != "" {
|
|
t.Errorf("buffer = %q, want empty", m.ActiveBuffer().Line(0))
|
|
}
|
|
})
|
|
}
|
|
|
|
// ==================================================
|
|
// P2: Complex Sequences
|
|
// ==================================================
|
|
|
|
func TestDotOperatorComplexSequences(t *testing.T) {
|
|
t.Run("complex sequence of operations", func(t *testing.T) {
|
|
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3"}))
|
|
sendKeys(tm, "x") // Delete 'l' from line1 -> ["ine1", "line2", "line3"]
|
|
sendKeys(tm, "j", "j") // Move to line 2
|
|
sendKeys(tm, "d", "d") // Delete line 2 (line3) -> ["ine1", "line2"], cursor at line 1
|
|
sendKeys(tm, "k") // Move up to line 0
|
|
sendKeys(tm, ".") // Repeat dd - deletes line 0 (ine1) -> ["line2"]
|
|
|
|
m := getFinalModel(t, tm)
|
|
// After the sequence, dd was recorded and repeated at line 0, deleting it
|
|
if m.ActiveBuffer().LineCount() != 1 {
|
|
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
|
|
}
|
|
if m.ActiveBuffer().Line(0) != "line2" {
|
|
t.Errorf("buffer = %q, want \"line2\"", m.ActiveBuffer().Line(0))
|
|
}
|
|
})
|
|
}
|