feat: implemented 'X', tested
X and x are the same in visual mode I think
This commit is contained in:
parent
a01369f407
commit
5c629496c6
@ -104,7 +104,7 @@
|
||||
### Delete Actions
|
||||
- [x] `x` - Delete character under cursor
|
||||
- [x] `D` - Delete to end of line
|
||||
- [ ] `X` - Delete character before cursor
|
||||
- [x] `X` - Delete character before cursor
|
||||
- [ ] `J` - Join lines
|
||||
- [ ] `gJ` - Join lines without space
|
||||
|
||||
|
||||
@ -27,6 +27,35 @@ func (a DeleteChar) WithCount(n int) Action {
|
||||
return DeleteChar{Count: n}
|
||||
}
|
||||
|
||||
// DeletePrevChar implements Action (x)
|
||||
type DeletePrevChar struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
// DeletePrevChar.Execute: Deletes Count characters before the cursor position (x key).
|
||||
func (a DeletePrevChar) Execute(m Model) tea.Cmd {
|
||||
win := m.ActiveWindow()
|
||||
buf := m.ActiveBuffer()
|
||||
|
||||
pos := win.Cursor.Col
|
||||
line := buf.Lines[win.Cursor.Line]
|
||||
for i := 0; i < a.Count && pos <= len(line); i++ {
|
||||
if pos > 0 {
|
||||
line = line[:pos-1] + line[pos:]
|
||||
buf.SetLine(win.Cursor.Line, line)
|
||||
pos--
|
||||
win.SetCursorCol(pos)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePrevChar.WithCount: Returns a new DeletePrevChar with the given count.
|
||||
func (a DeletePrevChar) WithCount(n int) Action {
|
||||
return DeletePrevChar{Count: n}
|
||||
}
|
||||
|
||||
// DeleteToEndOfLine implements Action (D) - deletes from cursor to end of line
|
||||
// and optionally Count-1 additional lines below.
|
||||
type DeleteToEndOfLine struct {
|
||||
|
||||
@ -194,6 +194,274 @@ func TestDeleteCharEdgeCases(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteCharBackward(t *testing.T) {
|
||||
t.Run("test 'X' deletes character before cursor", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
|
||||
sendKeys(tm, "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "ello" {
|
||||
t.Errorf("lines[0] = %q, want 'ello'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'X' in middle of line", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "helo" {
|
||||
t.Errorf("lines[0] = %q, want 'helo'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'X' at end of line", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
|
||||
sendKeys(tm, "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "helo" {
|
||||
t.Errorf("lines[0] = %q, want 'helo'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'XX' deletes two characters backward", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "X", "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "llo" {
|
||||
t.Errorf("lines[0] = %q, want 'llo'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteCharBackwardWithCount(t *testing.T) {
|
||||
t.Run("test '3X' deletes three characters backward", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "3", "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "lo" {
|
||||
t.Errorf("lines[0] = %q, want 'lo'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '10X' with overflow", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
|
||||
sendKeys(tm, "1", "0", "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "o" {
|
||||
t.Errorf("lines[0] = %q, want 'o'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '2X' from middle", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "2", "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "hlo" {
|
||||
t.Errorf("lines[0] = %q, want 'hlo'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteCharBackwardEdgeCases(t *testing.T) {
|
||||
t.Run("test 'X' at start of line does nothing", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "hello" {
|
||||
t.Errorf("Line(0) = %q, want 'hello'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
if m.ActiveWindow().Cursor.Col != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'X' on empty line does nothing", func(t *testing.T) {
|
||||
lines := []string{""}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "" {
|
||||
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
if m.ActiveWindow().Cursor.Col != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'X' on single character line from end", func(t *testing.T) {
|
||||
lines := []string{"a"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "a" {
|
||||
t.Errorf("Line(0) = %q, want 'a'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'X' at second char deletes first", func(t *testing.T) {
|
||||
lines := []string{"ab"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
|
||||
sendKeys(tm, "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "b" {
|
||||
t.Errorf("Line(0) = %q, want 'b'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'X' with whitespace", func(t *testing.T) {
|
||||
lines := []string{"a b c"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "ab c" {
|
||||
t.Errorf("Line(0) = %q, want 'ab c'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'X' preserves other lines", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
|
||||
sendKeys(tm, "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().LineCount() != 2 {
|
||||
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
|
||||
}
|
||||
if m.ActiveBuffer().Lines[1] != "world" {
|
||||
t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'X' multiple times deletes multiple chars backward", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "X", "X", "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "lo" {
|
||||
t.Errorf("Line(0) = %q, want 'lo'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'X' on line with tabs", func(t *testing.T) {
|
||||
lines := []string{"a\tb"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "ab" {
|
||||
t.Errorf("Line(0) = %q, want 'ab'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '5X' with only 3 chars available before cursor", func(t *testing.T) {
|
||||
lines := []string{"abc"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "5", "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "c" {
|
||||
t.Errorf("Line(0) = %q, want 'c'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'X' in middle preserves surrounding chars", func(t *testing.T) {
|
||||
lines := []string{"abcde"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "abde" {
|
||||
t.Errorf("Line(0) = %q, want 'abde'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'X' cursor position after delete", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Cursor should move back one position after deleting char before it
|
||||
if m.ActiveWindow().Cursor.Col != 2 {
|
||||
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '3X' cursor position after delete", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
|
||||
sendKeys(tm, "3", "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "ho" {
|
||||
t.Errorf("Line(0) = %q, want 'ho'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
// Cursor should be at position 1 after deleting 3 chars backward from position 4
|
||||
if m.ActiveWindow().Cursor.Col != 1 {
|
||||
t.Errorf("CursorX() = %d, want 1", m.ActiveWindow().Cursor.Col)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'X' at start of second line does nothing", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
||||
sendKeys(tm, "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "hello" {
|
||||
t.Errorf("Line(0) = %q, want 'hello'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
if m.ActiveBuffer().Lines[1] != "world" {
|
||||
t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'X' on whitespace-only line", func(t *testing.T) {
|
||||
lines := []string{" "}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != " " {
|
||||
t.Errorf("Line(0) = %q, want ' '", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'X' from end of line deletes last char", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
|
||||
sendKeys(tm, "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0] != "helo" {
|
||||
t.Errorf("Line(0) = %q, want 'helo'", m.ActiveBuffer().Lines[0])
|
||||
}
|
||||
if m.ActiveWindow().Cursor.Col != 3 {
|
||||
t.Errorf("CursorX() = %d, want 3", m.ActiveWindow().Cursor.Col)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteToEndOfLine(t *testing.T) {
|
||||
t.Run("test 'D' deletes to end of line", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
|
||||
@ -55,6 +55,7 @@ func NewNormalKeymap() *Keymap {
|
||||
"o": action.OpenLineBelow{},
|
||||
"O": action.OpenLineAbove{},
|
||||
"x": action.DeleteChar{Count: 1},
|
||||
"X": action.DeletePrevChar{Count: 1},
|
||||
":": action.EnterComandMode{},
|
||||
"v": action.EnterVisualMode{},
|
||||
"V": action.EnterVisualLineMode{},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user