Gim/internal/editor/integration_insert_test.go
2026-02-12 17:30:06 -07:00

630 lines
18 KiB
Go

package editor
import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/action"
)
// --- Insert Mode Entry Tests ---
func TestEnterInsert(t *testing.T) {
t.Run("test 'i' enters insert mode", func(t *testing.T) {
tm := newTestModel(t)
sendKeys(tm, "i")
m := getFinalModel(t, tm)
if m.mode != action.InsertMode {
t.Errorf("mode = %d, want InsertMode (%d)", m.mode, action.InsertMode)
}
})
t.Run("test 'i' insert at beginning", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "i", "X", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "Xhello" {
t.Errorf("lines[0] = %q, want 'Xhello'", m.lines[0])
}
})
t.Run("test 'i' insert in middle", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
sendKeys(tm, "i", "X", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0])
}
})
t.Run("test 'i' cursor moves back on esc", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
sendKeys(tm, "i", "X", "esc")
m := getFinalModel(t, tm)
if m.cursor.x != 2 {
t.Errorf("cursor.x = %d, want 2", m.cursor.x)
}
})
}
func TestEnterInsertAfter(t *testing.T) {
t.Run("test 'a' enters insert mode", func(t *testing.T) {
tm := newTestModel(t)
sendKeys(tm, "a")
m := getFinalModel(t, tm)
if m.mode != action.InsertMode {
t.Errorf("mode = %d, want InsertMode (%d)", m.mode, action.InsertMode)
}
})
t.Run("test 'a' inserts after cursor", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "a", "X", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "hXello" {
t.Errorf("lines[0] = %q, want 'hXello'", m.lines[0])
}
})
t.Run("test 'a' from middle of line", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
sendKeys(tm, "a", "X", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "helXlo" {
t.Errorf("lines[0] = %q, want 'helXlo'", m.lines[0])
}
})
}
func TestEnterInsertLineStart(t *testing.T) {
t.Run("test 'I' enters insert mode at line start", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
sendKeys(tm, "I", "X", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "Xhello" {
t.Errorf("lines[0] = %q, want 'Xhello'", m.lines[0])
}
})
t.Run("test 'I' from end of line", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
sendKeys(tm, "I", "X", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "Xhello" {
t.Errorf("lines[0] = %q, want 'Xhello'", m.lines[0])
}
})
}
func TestEnterInsertLineEnd(t *testing.T) {
t.Run("test 'A' enters insert mode at line end", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "A", "X", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "helloX" {
t.Errorf("lines[0] = %q, want 'helloX'", m.lines[0])
}
})
t.Run("test 'A' from middle of line", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
sendKeys(tm, "A", "X", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "helloX" {
t.Errorf("lines[0] = %q, want 'helloX'", m.lines[0])
}
})
}
// --- Open Line Tests ---
func TestOpenLineBelow(t *testing.T) {
t.Run("test 'o' creates line below", func(t *testing.T) {
lines := []string{"line 1", "line 2"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "o", "n", "e", "w", "esc")
m := getFinalModel(t, tm)
if len(m.lines) != 3 {
t.Errorf("len(lines) = %d, want 3", len(m.lines))
}
if m.lines[1] != "new" {
t.Errorf("lines[1] = %q, want 'new'", m.lines[1])
}
})
t.Run("test 'o' from middle of file", func(t *testing.T) {
lines := []string{"line 1", "line 2", "line 3"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
sendKeys(tm, "o", "n", "e", "w", "esc")
m := getFinalModel(t, tm)
if len(m.lines) != 4 {
t.Errorf("len(lines) = %d, want 4", len(m.lines))
}
if m.lines[2] != "new" {
t.Errorf("lines[2] = %q, want 'new'", m.lines[2])
}
})
t.Run("test 'o' at end of file", func(t *testing.T) {
lines := []string{"line 1", "line 2"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
sendKeys(tm, "o", "n", "e", "w", "esc")
m := getFinalModel(t, tm)
if len(m.lines) != 3 {
t.Errorf("len(lines) = %d, want 3", len(m.lines))
}
if m.lines[2] != "new" {
t.Errorf("lines[2] = %q, want 'new'", m.lines[2])
}
})
t.Run("test 'o' cursor moves to new line", func(t *testing.T) {
lines := []string{"line 1", "line 2"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "o", "esc")
m := getFinalModel(t, tm)
if m.cursor.y != 1 {
t.Errorf("cursor.y = %d, want 1", m.cursor.y)
}
if m.cursor.x != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
}
})
}
func TestOpenLineBelowWithCount(t *testing.T) {
t.Run("test '3o' creates 3 lines", func(t *testing.T) {
lines := []string{"line 1"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "3", "o", "x", "esc")
m := getFinalModel(t, tm)
if len(m.lines) != 4 {
t.Errorf("len(lines) = %d, want 4", len(m.lines))
}
for i := 1; i <= 3; i++ {
if m.lines[i] != "x" {
t.Errorf("lines[%d] = %q, want 'x'", i, m.lines[i])
}
}
})
t.Run("test '2o' with multiple chars", func(t *testing.T) {
lines := []string{"line 1"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "2", "o", "a", "b", "esc")
m := getFinalModel(t, tm)
if len(m.lines) != 3 {
t.Errorf("len(lines) = %d, want 3", len(m.lines))
}
if m.lines[1] != "ab" {
t.Errorf("lines[1] = %q, want 'ab'", m.lines[1])
}
if m.lines[2] != "ab" {
t.Errorf("lines[2] = %q, want 'ab'", m.lines[2])
}
})
}
func TestOpenLineAbove(t *testing.T) {
t.Run("test 'O' creates line above", func(t *testing.T) {
lines := []string{"line 1", "line 2"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
sendKeys(tm, "O", "n", "e", "w", "esc")
m := getFinalModel(t, tm)
if len(m.lines) != 3 {
t.Errorf("len(lines) = %d, want 3", len(m.lines))
}
if m.lines[1] != "new" {
t.Errorf("lines[1] = %q, want 'new'", m.lines[1])
}
})
t.Run("test 'O' at top of file", func(t *testing.T) {
lines := []string{"line 1", "line 2"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "O", "n", "e", "w", "esc")
m := getFinalModel(t, tm)
if len(m.lines) != 3 {
t.Errorf("len(lines) = %d, want 3", len(m.lines))
}
if m.lines[0] != "new" {
t.Errorf("lines[0] = %q, want 'new'", m.lines[0])
}
})
t.Run("test 'O' cursor at start of new line", func(t *testing.T) {
lines := []string{"line 1", "line 2"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 1})
sendKeys(tm, "O", "esc")
m := getFinalModel(t, tm)
if m.cursor.x != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
}
})
}
func TestOpenLineAboveWithCount(t *testing.T) {
t.Run("test '3O' creates 3 lines above", func(t *testing.T) {
lines := []string{"line 1"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
sendKeys(tm, "3", "O", "x", "esc")
m := getFinalModel(t, tm)
if len(m.lines) != 4 {
t.Errorf("len(lines) = %d, want 4", len(m.lines))
}
for i := 0; i < 3; i++ {
if m.lines[i] != "x" {
t.Errorf("lines[%d] = %q, want 'x'", i, m.lines[i])
}
}
})
}
// --- Insert Mode Special Keys ---
func TestInsertModeEnter(t *testing.T) {
t.Run("test enter splits line", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
sendKeys(tm, "i", "enter", "esc")
m := getFinalModel(t, tm)
if len(m.lines) != 2 {
t.Errorf("len(lines) = %d, want 2", len(m.lines))
}
if m.lines[0] != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0])
}
if m.lines[1] != " world" {
t.Errorf("lines[1] = %q, want ' world'", m.lines[1])
}
})
t.Run("test enter at end of line", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
sendKeys(tm, "i", "enter", "esc")
m := getFinalModel(t, tm)
if len(m.lines) != 2 {
t.Errorf("len(lines) = %d, want 2", len(m.lines))
}
if m.lines[0] != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0])
}
if m.lines[1] != "" {
t.Errorf("lines[1] = %q, want ''", m.lines[1])
}
})
t.Run("test enter at start of line", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "i", "enter", "esc")
m := getFinalModel(t, tm)
if len(m.lines) != 2 {
t.Errorf("len(lines) = %d, want 2", len(m.lines))
}
if m.lines[0] != "" {
t.Errorf("lines[0] = %q, want ''", m.lines[0])
}
if m.lines[1] != "hello" {
t.Errorf("lines[1] = %q, want 'hello'", m.lines[1])
}
})
}
func TestInsertModeBackspace(t *testing.T) {
t.Run("test backspace deletes character", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
sendKeys(tm, "i", "backspace", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "helo" {
t.Errorf("lines[0] = %q, want 'helo'", m.lines[0])
}
})
t.Run("test backspace at start of line joins lines", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
sendKeys(tm, "i", "backspace", "esc")
m := getFinalModel(t, tm)
if len(m.lines) != 1 {
t.Errorf("len(lines) = %d, want 1", len(m.lines))
}
if m.lines[0] != "helloworld" {
t.Errorf("lines[0] = %q, want 'helloworld'", m.lines[0])
}
})
t.Run("test backspace at start of first line does nothing", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "i", "backspace", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0])
}
})
t.Run("test multiple backspaces", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
sendKeys(tm, "i", "backspace", "backspace", "backspace", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "he" {
t.Errorf("lines[0] = %q, want 'he'", m.lines[0])
}
})
}
func TestInsertModeDelete(t *testing.T) {
t.Run("test delete deletes character", func(t *testing.T) {
lines := []string{"world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
sendKeys(tm, "i", "delete", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "word" {
t.Errorf("lines[0] = %q, want 'word'", m.lines[0])
}
})
t.Run("test delete at end of line joins lines", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
sendKeys(tm, "i", "delete", "esc")
m := getFinalModel(t, tm)
if len(m.lines) != 1 {
t.Errorf("len(lines) = %d, want 1", len(m.lines))
}
if m.lines[0] != "helloworld" {
t.Errorf("lines[0] = %q, want 'helloworld'", m.lines[0])
}
})
t.Run("test delete at start of empty line joins lines", func(t *testing.T) {
lines := []string{"", "world"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "i", "delete", "esc")
m := getFinalModel(t, tm)
if len(m.lines) != 1 {
t.Errorf("len(lines) = %d, want 1", len(m.lines))
}
if m.lines[0] != "world" {
t.Errorf("lines[0] = %q, want 'world'", m.lines[0])
}
})
t.Run("test delete at end of last line does nothing", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
sendKeys(tm, "i", "delete", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0])
}
})
t.Run("test multiple delete", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
sendKeys(tm, "i", "delete", "delete", "delete", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "ho" {
t.Errorf("lines[0] = %q, want 'he'", m.lines[0])
}
})
}
func TestInsertModeDeletePreviousWord(t *testing.T) {
t.Run("test 'ctrl+w' deletes word", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "hello " {
t.Errorf("lines[0] = %q, want 'hello '", m.lines[0])
}
if m.CursorX() != 5 {
t.Errorf("CursorX() = %d, want '5'", m.CursorX())
}
})
t.Run("test 'ctrl+w' deletes word with whitespace", func(t *testing.T) {
lines := []string{"hello "}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "" {
t.Errorf("lines[0] = %q, want ''", m.lines[0])
}
if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want '0'", m.CursorX())
}
})
t.Run("test 'ctrl+w' deletes word until period", func(t *testing.T) {
lines := []string{"hello wo..."}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "hello wo" {
t.Errorf("lines[0] = %q, want 'hello wo'", m.lines[0])
}
if m.CursorX() != 7 {
t.Errorf("CursorX() = %d, want '7'", m.CursorX())
}
})
t.Run("test 'ctrl+w' deletes line when blank", func(t *testing.T) {
lines := []string{"", ""}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.LineCount() != 1 {
t.Errorf("LineCount() = %d, want '1'", m.LineCount())
}
if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want '0'", m.CursorX())
}
if m.CursorY() != 0 {
t.Errorf("CursorY() = %d, want '0'", m.CursorY())
}
})
t.Run("test 'ctrl+w' deletes all whitespace when line is only whitespace", func(t *testing.T) {
lines := []string{" "}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.LineCount() != 1 {
t.Errorf("LineCount() = %d, want '1'", m.LineCount())
}
if m.Line(0) != "" {
t.Errorf("Line(0) = %s, want ''", m.Line(0))
}
if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want '0'", m.CursorX())
}
if m.CursorY() != 0 {
t.Errorf("CursorY() = %d, want '0'", m.CursorY())
}
})
t.Run("test 'ctrl+w' at start of first line does nothing", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "i", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0])
}
if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want 0", m.CursorX())
}
})
t.Run("test 'ctrl+w' from middle of word", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
sendKeys(tm, "i", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "lo" {
t.Errorf("lines[0] = %q, want 'lo'", m.lines[0])
}
if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want 0", m.CursorX())
}
})
t.Run("test 'ctrl+w' deletes word after punctuation", func(t *testing.T) {
lines := []string{"...hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 7, Line: 0})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "..." {
t.Errorf("lines[0] = %q, want '...'", m.lines[0])
}
if m.CursorX() != 2 {
t.Errorf("CursorX() = %d, want 2", m.CursorX())
}
})
t.Run("test 'ctrl+w' with tabs as whitespace", func(t *testing.T) {
lines := []string{"hello\tworld"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "hello\t" {
t.Errorf("lines[0] = %q, want 'hello\\t'", m.lines[0])
}
if m.CursorX() != 5 {
t.Errorf("CursorX() = %d, want 5", m.CursorX())
}
})
t.Run("test 'ctrl+w' at start of line merges with previous line content", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
sendKeys(tm, "i", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.LineCount())
}
if m.lines[0] != "helloworld" {
t.Errorf("lines[0] = %q, want 'helloworld'", m.lines[0])
}
if m.CursorX() != 4 {
t.Errorf("CursorX() = %d, want 4", m.CursorX())
}
if m.CursorY() != 0 {
t.Errorf("CursorY() = %d, want 0", m.CursorY())
}
})
t.Run("test 'ctrl+w' with underscore in word", func(t *testing.T) {
lines := []string{"hello_world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.lines[0] != "" {
t.Errorf("lines[0] = %q, want ''", m.lines[0])
}
if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want 0", m.CursorX())
}
})
}