package editor import ( "testing" "git.gophernest.net/azpect/TextEditor/internal/action" ) func TestDeleteLine(t *testing.T) { t.Run("test 'dd' deletes first line", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "d") 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()) } if m.Line(0) != "world" { t.Errorf("Line(0) = %s, want 'world'", m.Line(0)) } }) t.Run("test 'dd' deletes middle line", func(t *testing.T) { lines := []string{"hello", "world", "testing"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1}) sendKeys(tm, "d", "d") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want '2'", m.LineCount()) } if m.CursorX() != 0 { t.Errorf("CursorX() = %d, want '0'", m.CursorX()) } if m.CursorY() != 1 { t.Errorf("CursorY() = %d, want '1'", m.CursorY()) } if m.Line(0) != "hello" { t.Errorf("Line(0) = %s, want 'hello'", m.Line(0)) } if m.Line(1) != "testing" { t.Errorf("Line(1) = %s, want 'testing'", m.Line(1)) } }) t.Run("test 'dd' deletes last line", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1}) sendKeys(tm, "d", "d") 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()) } if m.Line(0) != "hello" { t.Errorf("Line(0) = %s, want 'hello'", m.Line(0)) } }) t.Run("test 'dd' deletes line and preserves column", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0}) sendKeys(tm, "d", "d") m := getFinalModel(t, tm) if m.LineCount() != 1 { t.Errorf("LineCount() = %d, want '1'", m.LineCount()) } if m.CursorX() != 3 { t.Errorf("CursorX() = %d, want '3'", m.CursorX()) } if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want '0'", m.CursorY()) } if m.Line(0) != "world" { t.Errorf("Line(0) = %s, want 'world'", m.Line(0)) } }) t.Run("test '3dd' deletes three lines", func(t *testing.T) { lines := []string{"hello", "world", "testing", "line", "another line"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1}) sendKeys(tm, "3", "d", "d") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want '2'", m.LineCount()) } if m.CursorX() != 0 { t.Errorf("CursorX() = %d, want '0'", m.CursorX()) } if m.CursorY() != 1 { t.Errorf("CursorY() = %d, want '1'", m.CursorY()) } if m.Line(0) != "hello" { t.Errorf("Line(0) = %s, want 'hello'", m.Line(0)) } if m.Line(1) != "another line" { t.Errorf("Line(1) = %s, want 'another line'", m.Line(1)) } }) t.Run("test 'dd' deletes only line and preserves content", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "d") 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()) } if m.Line(0) != "" { t.Errorf("Line(0) = %s, want ''", m.Line(0)) } }) t.Run("test 'dd' with no lines preserves content", func(t *testing.T) { lines := []string{""} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "d") 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()) } if m.Line(0) != "" { t.Errorf("Line(0) = %s, want ''", m.Line(0)) } }) t.Run("test 'dd' clamps cursor when next line is shorter", func(t *testing.T) { lines := []string{"hello world", "hi"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 7, Line: 0}) sendKeys(tm, "d", "d") m := getFinalModel(t, tm) if m.CursorX() != 2 { t.Errorf("CursorX() = %d, want 2", m.CursorX()) } }) t.Run("test '3dd' count exceeds remaining lines", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "3", "d", "d") m := getFinalModel(t, tm) if m.LineCount() != 1 { t.Errorf("LineCount() = %d, want 1", m.LineCount()) } if m.Line(0) != "" { t.Errorf("Line(0) = %q, want \"\"", m.Line(0)) } }) t.Run("test '3dd' starting near end of file", func(t *testing.T) { lines := []string{"hello", "world", "testing"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1}) sendKeys(tm, "3", "d", "d") m := getFinalModel(t, tm) if m.LineCount() != 1 { t.Errorf("LineCount() = %d, want 1", m.LineCount()) } if m.Line(0) != "hello" { t.Errorf("Line(0) = %q, want \"hello\"", m.Line(0)) } if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want 0", m.CursorY()) } }) } func TestDeleteOperatorWithHorozontalMotion(t *testing.T) { t.Run("test 'dl' deletes current character from start", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "l") m := getFinalModel(t, tm) if m.CursorX() != 0 { t.Errorf("CursorX() = %d, want '0'", m.CursorX()) } if m.Line(0) != "ello" { t.Errorf("Line(0) = %s, want 'ello'", m.Line(0)) } }) t.Run("test 'dl' deletes current character from middle", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0}) sendKeys(tm, "d", "l") m := getFinalModel(t, tm) if m.CursorX() != 2 { t.Errorf("CursorX() = %d, want '2'", m.CursorX()) } if m.Line(0) != "helo" { t.Errorf("Line(0) = %s, want 'helo'", m.Line(0)) } }) t.Run("test 'dl' deletes current character from end", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0}) sendKeys(tm, "d", "l") m := getFinalModel(t, tm) if m.CursorX() != 4 { t.Errorf("CursorX() = %d, want '4'", m.CursorX()) } if m.Line(0) != "hell" { t.Errorf("Line(0) = %s, want 'hell'", m.Line(0)) } }) t.Run("test 'dh' does nothing on first char", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "h") m := getFinalModel(t, tm) if m.CursorX() != 0 { t.Errorf("CursorX() = %d, want '0'", m.CursorX()) } if m.Line(0) != "hello" { t.Errorf("Line(0) = %s, want 'hello'", m.Line(0)) } }) t.Run("test 'dh' deletes character to the left from middle", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0}) sendKeys(tm, "d", "h") m := getFinalModel(t, tm) // dh deletes char to the left (position 1, 'e'), cursor moves to that position if m.CursorX() != 1 { t.Errorf("CursorX() = %d, want 1", m.CursorX()) } if m.Line(0) != "hllo" { t.Errorf("Line(0) = %q, want 'hllo'", m.Line(0)) } }) t.Run("test 'dh' deletes character to the left from end", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0}) sendKeys(tm, "d", "h") m := getFinalModel(t, tm) // dh deletes char to the left (position 3, 'l'), cursor moves to that position if m.CursorX() != 3 { t.Errorf("CursorX() = %d, want 3", m.CursorX()) } if m.Line(0) != "helo" { t.Errorf("Line(0) = %q, want 'helo'", m.Line(0)) } }) t.Run("test '2dl' deletes two characters", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "2", "d", "l") m := getFinalModel(t, tm) if m.Line(0) != "llo" { t.Errorf("Line(0) = %q, want 'llo'", m.Line(0)) } }) t.Run("test 'd2l' deletes two characters", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "2", "l") m := getFinalModel(t, tm) if m.Line(0) != "llo" { t.Errorf("Line(0) = %q, want 'llo'", m.Line(0)) } }) t.Run("test '2dh' deletes two characters backwards", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0}) sendKeys(tm, "2", "d", "h") m := getFinalModel(t, tm) if m.Line(0) != "heo" { t.Errorf("Line(0) = %q, want 'heo'", m.Line(0)) } }) } func TestDeleteOperatorWithVerticalMotion(t *testing.T) { t.Run("test 'dj' deletes current and next line", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3", "line 4"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1}) sendKeys(tm, "d", "j") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } if m.Line(0) != "line 1" { t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0)) } if m.Line(1) != "line 4" { t.Errorf("Line(1) = %q, want 'line 4'", m.Line(1)) } if m.CursorY() != 1 { t.Errorf("CursorY() = %d, want 1", m.CursorY()) } }) t.Run("test 'dj' from first line", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "j") m := getFinalModel(t, tm) if m.LineCount() != 1 { t.Errorf("LineCount() = %d, want 1", m.LineCount()) } if m.Line(0) != "line 3" { t.Errorf("Line(0) = %q, want 'line 3'", m.Line(0)) } if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want 0", m.CursorY()) } }) // Note: In vim, dj from last line does nothing. Our implementation treats all // linewise motions consistently - they operate on at least the current line. t.Run("test 'dj' from last line deletes current line", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2}) sendKeys(tm, "d", "j") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } if m.Line(0) != "line 1" { t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0)) } if m.Line(1) != "line 2" { t.Errorf("Line(1) = %q, want 'line 2'", m.Line(1)) } }) t.Run("test 'd2j' deletes current and next two lines", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1}) sendKeys(tm, "d", "2", "j") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } if m.Line(0) != "line 1" { t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0)) } if m.Line(1) != "line 5" { t.Errorf("Line(1) = %q, want 'line 5'", m.Line(1)) } }) t.Run("test '2dj' deletes current and next two lines", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1}) sendKeys(tm, "2", "d", "j") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } if m.Line(0) != "line 1" { t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0)) } if m.Line(1) != "line 5" { t.Errorf("Line(1) = %q, want 'line 5'", m.Line(1)) } }) t.Run("test 'dk' deletes current and previous line", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3", "line 4"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2}) sendKeys(tm, "d", "k") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } if m.Line(0) != "line 1" { t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0)) } if m.Line(1) != "line 4" { t.Errorf("Line(1) = %q, want 'line 4'", m.Line(1)) } if m.CursorY() != 1 { t.Errorf("CursorY() = %d, want 1", m.CursorY()) } }) // Note: In vim, dk from first line does nothing. Our implementation treats all // linewise motions consistently - they operate on at least the current line. t.Run("test 'dk' from first line deletes current line", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "k") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } if m.Line(0) != "line 2" { t.Errorf("Line(0) = %q, want 'line 2'", m.Line(0)) } if m.Line(1) != "line 3" { t.Errorf("Line(1) = %q, want 'line 3'", m.Line(1)) } }) t.Run("test 'dk' from second line", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1}) sendKeys(tm, "d", "k") m := getFinalModel(t, tm) if m.LineCount() != 1 { t.Errorf("LineCount() = %d, want 1", m.LineCount()) } if m.Line(0) != "line 3" { t.Errorf("Line(0) = %q, want 'line 3'", m.Line(0)) } if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want 0", m.CursorY()) } }) t.Run("test 'd2k' deletes current and previous two lines", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 3}) sendKeys(tm, "d", "2", "k") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } if m.Line(0) != "line 1" { t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0)) } if m.Line(1) != "line 5" { t.Errorf("Line(1) = %q, want 'line 5'", m.Line(1)) } }) t.Run("test '2dk' deletes current and previous two lines", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 3}) sendKeys(tm, "2", "d", "k") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } if m.Line(0) != "line 1" { t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0)) } if m.Line(1) != "line 5" { t.Errorf("Line(1) = %q, want 'line 5'", m.Line(1)) } }) t.Run("test 'dj' with count exceeding remaining lines", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1}) sendKeys(tm, "d", "5", "j") m := getFinalModel(t, tm) // Should delete from line 1 to end if m.LineCount() != 1 { t.Errorf("LineCount() = %d, want 1", m.LineCount()) } if m.Line(0) != "line 1" { t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0)) } }) t.Run("test 'dk' with count exceeding previous lines", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1}) sendKeys(tm, "d", "5", "k") m := getFinalModel(t, tm) // Should delete from line 0 to line 1 if m.LineCount() != 1 { t.Errorf("LineCount() = %d, want 1", m.LineCount()) } if m.Line(0) != "line 3" { t.Errorf("Line(0) = %q, want 'line 3'", m.Line(0)) } }) } func TestDeleteOperatorWithJumpMotion(t *testing.T) { t.Run("test 'dG' deletes from cursor to end of file", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2}) sendKeys(tm, "d", "G") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } if m.Line(0) != "line 1" { t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0)) } if m.Line(1) != "line 2" { t.Errorf("Line(1) = %q, want 'line 2'", m.Line(1)) } }) t.Run("test 'dG' from first line deletes everything", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "G") m := getFinalModel(t, tm) if m.LineCount() != 1 { t.Errorf("LineCount() = %d, want 1", m.LineCount()) } if m.Line(0) != "" { t.Errorf("Line(0) = %q, want ''", m.Line(0)) } }) t.Run("test 'dG' from last line deletes only last line", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2}) sendKeys(tm, "d", "G") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } if m.Line(0) != "line 1" { t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0)) } if m.Line(1) != "line 2" { t.Errorf("Line(1) = %q, want 'line 2'", m.Line(1)) } }) t.Run("test 'dG' positions cursor correctly", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3", "line 4"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 1}) sendKeys(tm, "d", "G") m := getFinalModel(t, tm) if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want 0", m.CursorY()) } }) t.Run("test 'dgg' deletes from cursor to start of file", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2}) sendKeys(tm, "d", "g", "g") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } if m.Line(0) != "line 4" { t.Errorf("Line(0) = %q, want 'line 4'", m.Line(0)) } if m.Line(1) != "line 5" { t.Errorf("Line(1) = %q, want 'line 5'", m.Line(1)) } }) t.Run("test 'dgg' from last line deletes everything", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2}) sendKeys(tm, "d", "g", "g") m := getFinalModel(t, tm) if m.LineCount() != 1 { t.Errorf("LineCount() = %d, want 1", m.LineCount()) } if m.Line(0) != "" { t.Errorf("Line(0) = %q, want ''", m.Line(0)) } }) t.Run("test 'dgg' from first line deletes only first line", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "g", "g") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } if m.Line(0) != "line 2" { t.Errorf("Line(0) = %q, want 'line 2'", m.Line(0)) } if m.Line(1) != "line 3" { t.Errorf("Line(1) = %q, want 'line 3'", m.Line(1)) } }) t.Run("test 'dgg' positions cursor at start", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3", "line 4"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 2}) sendKeys(tm, "d", "g", "g") m := getFinalModel(t, tm) if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want 0", m.CursorY()) } }) t.Run("test 'dG' on single line file deletes the line", func(t *testing.T) { lines := []string{"only line"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "G") m := getFinalModel(t, tm) if m.LineCount() != 1 { t.Errorf("LineCount() = %d, want 1", m.LineCount()) } if m.Line(0) != "" { t.Errorf("Line(0) = %q, want ''", m.Line(0)) } }) t.Run("test 'dgg' on single line file deletes the line", func(t *testing.T) { lines := []string{"only line"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "g", "g") m := getFinalModel(t, tm) if m.LineCount() != 1 { t.Errorf("LineCount() = %d, want 1", m.LineCount()) } if m.Line(0) != "" { t.Errorf("Line(0) = %q, want ''", m.Line(0)) } }) t.Run("test 'dG' clamps cursor when file shrinks", func(t *testing.T) { lines := []string{"short", "this is a longer line", "line 3", "line 4"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1}) sendKeys(tm, "d", "G") m := getFinalModel(t, tm) // Cursor was at col 10, but "short" only has 5 chars if m.CursorX() > len("short") { t.Errorf("CursorX() = %d, should be clamped to line length %d", m.CursorX(), len("short")) } }) }