diff --git a/internal/action/action.go b/internal/action/action.go index d39dff0..d4bd846 100644 --- a/internal/action/action.go +++ b/internal/action/action.go @@ -57,6 +57,7 @@ type Model interface { SetCursorX(x int) SetCursorY(y int) ClampCursorX() + ClampCursorXNormal() // Window ScrollY() int diff --git a/internal/action/delete.go b/internal/action/delete.go index 1fc4850..6142417 100644 --- a/internal/action/delete.go +++ b/internal/action/delete.go @@ -21,3 +21,56 @@ func (a DeleteChar) Execute(m Model) tea.Cmd { func (a DeleteChar) WithCount(n int) Action { return DeleteChar{Count: n} } + +type DeleteToEndOfLine struct { + Count int +} + +func (a DeleteToEndOfLine) Execute(m Model) tea.Cmd { + // Delete to end of line + pos := m.CursorX() + line := m.Line(m.CursorY()) + + m.SetLine(m.CursorY(), line[:pos]) + m.SetCursorX(pos - 1) + + // If count is greater, than we will delete the next N - 1 lines below + initY := m.CursorY() + if a.Count > 1 { + // Copied from `internal/operator/delete.go` + opCount := min(a.Count-1, m.LineCount()-m.CursorY()) + + // Down one + m.SetCursorY(initY + 1) + + for range opCount { + y := m.CursorY() // Changed from the copied code + + // Stop if were on the starting line + if y == initY { + break + } + m.DeleteLine(y) + + if m.LineCount() == 0 { + m.InsertLine(0, "") + } + + if y >= m.LineCount() { + y = m.LineCount() - 1 + } + + m.SetCursorY(y) + m.ClampCursorX() + } + } + + m.SetCursorY(initY) + m.ClampCursorX() + + return nil +} + +func (a DeleteToEndOfLine) WithCount(n int) Action { + return DeleteToEndOfLine{Count: n} +} diff --git a/internal/editor/integration_delete_test.go b/internal/editor/integration_delete_test.go index ba5b6e7..8e1652c 100644 --- a/internal/editor/integration_delete_test.go +++ b/internal/editor/integration_delete_test.go @@ -86,3 +86,92 @@ func TestDeleteCharWithCount(t *testing.T) { } }) } + +func TestDeleteToEndOfLine(t *testing.T) { + t.Run("test 'D' deletes to end of line", func(t *testing.T) { + lines := []string{"hello world"} + tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0}) + sendKeys(tm, "D") + + m := getFinalModel(t, tm) + if m.Line(0) != "hello" { + t.Errorf("Line(0) = %q, want 'hello'", m.Line(0)) + } + }) + + t.Run("test 'D' from start of line deletes entire content", func(t *testing.T) { + lines := []string{"hello world"} + tm := newTestModelWithLines(t, lines) + sendKeys(tm, "D") + + m := getFinalModel(t, tm) + if m.Line(0) != "" { + t.Errorf("Line(0) = %q, want ''", m.Line(0)) + } + }) + + t.Run("test 'D' at last character", func(t *testing.T) { + lines := []string{"hello"} + tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0}) + sendKeys(tm, "D") + + m := getFinalModel(t, tm) + if m.Line(0) != "hell" { + t.Errorf("Line(0) = %q, want 'hell'", m.Line(0)) + } + }) + + t.Run("test 'D' cursor position after delete", func(t *testing.T) { + lines := []string{"hello world"} + tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0}) + sendKeys(tm, "D") + + m := getFinalModel(t, tm) + // Cursor should move to last character of remaining text + if m.CursorX() != 4 { + t.Errorf("CursorX() = %d, want 4", m.CursorX()) + } + }) + + t.Run("test 'D' deletes nothing on blank line", func(t *testing.T) { + lines := []string{""} + tm := newTestModelWithLines(t, lines) + sendKeys(tm, "D") + + m := getFinalModel(t, tm) + if m.Line(0) != "" { + t.Errorf("Line(0) = %q, want ''", m.Line(0)) + } + }) + + t.Run("test 'D' with count deletes following lines", func(t *testing.T) { + lines := []string{"hello", "world", "hi", "mom"} + tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0}) + sendKeys(tm, "2", "D") + + m := getFinalModel(t, tm) + if m.LineCount() != 3 { + t.Errorf("LineCount() = %q, want '3'", m.LineCount()) + } + if m.Line(0) != "he" { + t.Errorf("Line(0) = %q, want 'he'", m.Line(0)) + } + if m.Line(1) != "hi" { + t.Errorf("Line(1) = %q, want 'hi'", m.Line(1)) + } + }) + + t.Run("test 'D' with count deletes following lines with overflow", func(t *testing.T) { + lines := []string{"hello", "world", "hi", "mom"} + tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0}) + sendKeys(tm, "8", "D") + + m := getFinalModel(t, tm) + if m.LineCount() != 1 { + t.Errorf("LineCount() = %q, want '1'", m.LineCount()) + } + if m.Line(0) != "he" { + t.Errorf("Line(0) = %q, want 'he'", m.Line(0)) + } + }) +} diff --git a/internal/input/keymap.go b/internal/input/keymap.go index 8c44b26..63c45a8 100644 --- a/internal/input/keymap.go +++ b/internal/input/keymap.go @@ -50,6 +50,7 @@ func NewNormalKeymap() *Keymap { "v": action.EnterVisualMode{}, "V": action.EnterVisualLineMode{}, "ctrl+v": action.EnterVisualBlockMode{}, + "D": action.DeleteToEndOfLine{Count: 1}, }, } }