feat: huge implementation! 'd' op working well, tested good
Not totally complete WRT tests, but lots of progress. These interfaces make everything easy.
This commit is contained in:
parent
db70ca39f1
commit
07589e3897
@ -1,16 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/editor"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func generateLines(n int) []string {
|
||||
lines := make([]string, n)
|
||||
for i := range n {
|
||||
lines[i] = fmt.Sprintf("line %d", i+1)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
lines := []string{"Hello world testing main.go", "line 2", "line 3", "line 4", "line 5"}
|
||||
// lines := []string{"Hello world testing main.go", "line 2", "line 3", "line 4", "line 5"}
|
||||
tea.NewProgram(
|
||||
editor.NewModel(lines, action.Position{Line: 0, Col: 0}),
|
||||
editor.NewModel(generateLines(32), action.Position{Line: 0, Col: 0}),
|
||||
tea.WithAltScreen(),
|
||||
).Run()
|
||||
}
|
||||
|
||||
@ -66,6 +66,14 @@ type Position struct {
|
||||
Line, Col int
|
||||
}
|
||||
|
||||
// MotionType indicates how a motion operates
|
||||
type MotionType int
|
||||
|
||||
const (
|
||||
Charwise MotionType = iota // h, l, w, e, f, t - operates on characters
|
||||
Linewise // j, k, G, gg, {, } - operates on whole lines
|
||||
)
|
||||
|
||||
// Action is the base interface - anything executable
|
||||
type Action interface {
|
||||
Execute(m Model) tea.Cmd
|
||||
@ -74,11 +82,12 @@ type Action interface {
|
||||
// Motion moves the cursor and returns the range covered
|
||||
type Motion interface {
|
||||
Action
|
||||
Type() MotionType
|
||||
}
|
||||
|
||||
// Operator acts on a range (delete, yank, change)
|
||||
type Operator interface {
|
||||
Operate(m Model, start, end Position) tea.Cmd
|
||||
Operate(m Model, start, end Position, mtype MotionType) tea.Cmd
|
||||
// DoublePress handles dd, yy, cc (line-wise)
|
||||
DoublePress(m Model, count int) tea.Cmd
|
||||
}
|
||||
|
||||
@ -197,3 +197,496 @@ func TestDeleteLine(t *testing.T) {
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -141,7 +141,12 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
|
||||
// In visual mode, the selection is already defined — operate immediately
|
||||
if m.IsVisualMode() {
|
||||
start, end := normalizeVisualSelection(m)
|
||||
cmd := op.Operate(m, start, end)
|
||||
// Visual line mode is linewise, others are charwise
|
||||
mtype := action.Charwise
|
||||
if m.Mode() == action.VisualLineMode {
|
||||
mtype = action.Linewise
|
||||
}
|
||||
cmd := op.Operate(m, start, end, mtype)
|
||||
m.SetMode(action.NormalMode)
|
||||
h.Reset()
|
||||
return cmd
|
||||
@ -182,12 +187,12 @@ func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any,
|
||||
if r, ok := mot.(action.Repeatable); ok {
|
||||
mot = r.WithCount(count).(action.Motion)
|
||||
}
|
||||
// Get range here
|
||||
// Get range and motion type
|
||||
pg := m.(PositionGetter)
|
||||
start := pg.GetCursorPosition()
|
||||
mot.Execute(m)
|
||||
end := pg.GetCursorPosition()
|
||||
cmd := h.operator.Operate(m, start, end)
|
||||
cmd := h.operator.Operate(m, start, end, mot.Type())
|
||||
h.Reset()
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import (
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
)
|
||||
|
||||
// MoveDown implements Motion (j)
|
||||
// MoveDown implements Motion (j) - linewise
|
||||
type MoveDown struct {
|
||||
Count int
|
||||
}
|
||||
@ -18,11 +18,13 @@ func (a MoveDown) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveDown) Type() action.MotionType { return action.Linewise }
|
||||
|
||||
func (a MoveDown) WithCount(n int) action.Action {
|
||||
return MoveDown{Count: n}
|
||||
}
|
||||
|
||||
// MoveUp implements Motion (k)
|
||||
// MoveUp implements Motion (k) - linewise
|
||||
type MoveUp struct {
|
||||
Count int
|
||||
}
|
||||
@ -35,11 +37,13 @@ func (a MoveUp) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveUp) Type() action.MotionType { return action.Linewise }
|
||||
|
||||
func (a MoveUp) WithCount(n int) action.Action {
|
||||
return MoveUp{Count: n}
|
||||
}
|
||||
|
||||
// MoveLeft implements Motion (h)
|
||||
// MoveLeft implements Motion (h) - charwise
|
||||
type MoveLeft struct {
|
||||
Count int
|
||||
}
|
||||
@ -52,11 +56,13 @@ func (a MoveLeft) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveLeft) Type() action.MotionType { return action.Charwise }
|
||||
|
||||
func (a MoveLeft) WithCount(n int) action.Action {
|
||||
return MoveLeft{Count: n}
|
||||
}
|
||||
|
||||
// MoveRight implements Motion (l)
|
||||
// MoveRight implements Motion (l) - charwise
|
||||
type MoveRight struct {
|
||||
Count int
|
||||
}
|
||||
@ -70,6 +76,8 @@ func (a MoveRight) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveRight) Type() action.MotionType { return action.Charwise }
|
||||
|
||||
func (a MoveRight) WithCount(n int) action.Action {
|
||||
return MoveRight{Count: n}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// MoveToTop implements Motion (gg)
|
||||
// MoveToTop implements Motion (gg) - linewise
|
||||
type MoveToTop struct{}
|
||||
|
||||
func (a MoveToTop) Execute(m action.Model) tea.Cmd {
|
||||
@ -14,7 +14,9 @@ func (a MoveToTop) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveToBottom implements Motion (G)
|
||||
func (a MoveToTop) Type() action.MotionType { return action.Linewise }
|
||||
|
||||
// MoveToBottom implements Motion (G) - linewise
|
||||
type MoveToBottom struct{}
|
||||
|
||||
func (a MoveToBottom) Execute(m action.Model) tea.Cmd {
|
||||
@ -23,7 +25,9 @@ func (a MoveToBottom) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveToLineStart implements Motion (0)
|
||||
func (a MoveToBottom) Type() action.MotionType { return action.Linewise }
|
||||
|
||||
// MoveToLineStart implements Motion (0) - charwise
|
||||
type MoveToLineStart struct{}
|
||||
|
||||
func (a MoveToLineStart) Execute(m action.Model) tea.Cmd {
|
||||
@ -32,7 +36,9 @@ func (a MoveToLineStart) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveToLineEnd implements Motion ($)
|
||||
func (a MoveToLineStart) Type() action.MotionType { return action.Charwise }
|
||||
|
||||
// MoveToLineEnd implements Motion ($) - charwise
|
||||
type MoveToLineEnd struct{}
|
||||
|
||||
func (a MoveToLineEnd) Execute(m action.Model) tea.Cmd {
|
||||
@ -41,7 +47,9 @@ func (a MoveToLineEnd) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveToLineContentStart implements Motion (_)
|
||||
func (a MoveToLineEnd) Type() action.MotionType { return action.Charwise }
|
||||
|
||||
// MoveToLineContentStart implements Motion (_) - charwise
|
||||
type MoveToLineContentStart struct{}
|
||||
|
||||
func (a MoveToLineContentStart) Execute(m action.Model) tea.Cmd {
|
||||
@ -63,3 +71,5 @@ func (a MoveToLineContentStart) Execute(m action.Model) tea.Cmd {
|
||||
m.SetCursorX(x)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveToLineContentStart) Type() action.MotionType { return action.Charwise }
|
||||
|
||||
@ -171,11 +171,11 @@ func prevWordStart(m action.Model, x, y int) (int, int) {
|
||||
return x, y
|
||||
}
|
||||
|
||||
// MoveForwardWord implements Motion (w) - charwise
|
||||
type MoveForwardWord struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
// Execute implements [action.Action].
|
||||
func (a MoveForwardWord) Execute(m action.Model) tea.Cmd {
|
||||
x := m.CursorX()
|
||||
y := m.CursorY()
|
||||
@ -187,15 +187,17 @@ func (a MoveForwardWord) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveForwardWord) Type() action.MotionType { return action.Charwise }
|
||||
|
||||
func (a MoveForwardWord) WithCount(n int) action.Action {
|
||||
return MoveForwardWord{Count: n}
|
||||
}
|
||||
|
||||
// MoveForwardWordEnd implements Motion (e) - charwise
|
||||
type MoveForwardWordEnd struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
// Execute implements [action.Action].
|
||||
func (a MoveForwardWordEnd) Execute(m action.Model) tea.Cmd {
|
||||
x := m.CursorX()
|
||||
y := m.CursorY()
|
||||
@ -207,15 +209,17 @@ func (a MoveForwardWordEnd) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveForwardWordEnd) Type() action.MotionType { return action.Charwise }
|
||||
|
||||
func (a MoveForwardWordEnd) WithCount(n int) action.Action {
|
||||
return MoveForwardWordEnd{Count: n}
|
||||
}
|
||||
|
||||
// MoveBackwardWord implements Motion (b) - charwise
|
||||
type MoveBackwardWord struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
// Execute implements [action.Action].
|
||||
func (a MoveBackwardWord) Execute(m action.Model) tea.Cmd {
|
||||
x := m.CursorX()
|
||||
y := m.CursorY()
|
||||
@ -227,6 +231,8 @@ func (a MoveBackwardWord) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveBackwardWord) Type() action.MotionType { return action.Charwise }
|
||||
|
||||
func (a MoveBackwardWord) WithCount(n int) action.Action {
|
||||
return MoveBackwardWord{Count: n}
|
||||
}
|
||||
|
||||
@ -5,10 +5,10 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Implements Operator (x)
|
||||
// Implements Operator (d)
|
||||
type DeleteOperator struct{}
|
||||
|
||||
func (o DeleteOperator) Operate(m action.Model, start, end action.Position) tea.Cmd {
|
||||
func (o DeleteOperator) Operate(m action.Model, start, end action.Position, mtype action.MotionType) tea.Cmd {
|
||||
switch m.Mode() {
|
||||
case action.VisualMode:
|
||||
deleteCharSelection(m, start, end)
|
||||
@ -16,6 +16,8 @@ func (o DeleteOperator) Operate(m action.Model, start, end action.Position) tea.
|
||||
deleteLineSelection(m, start, end)
|
||||
case action.VisualBlockMode:
|
||||
deleteBlockSelection(m, start, end)
|
||||
case action.NormalMode:
|
||||
deleteNormalMode(m, start, end, mtype)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -44,6 +46,36 @@ func (o DeleteOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteNormalMode(m action.Model, start, end action.Position, mtype action.MotionType) {
|
||||
// Normalize so start is always before or equal to end
|
||||
if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
// Linewise motions (j, k, G, gg) always operate on whole lines
|
||||
if mtype == action.Linewise {
|
||||
deleteLineSelection(m, start, end)
|
||||
return
|
||||
}
|
||||
|
||||
// Charwise motions on same line
|
||||
if start.Line == end.Line {
|
||||
// No movement = nothing to delete
|
||||
if start.Col == end.Col {
|
||||
return
|
||||
}
|
||||
// Exclusive motion: delete [start.Col, end.Col)
|
||||
end.Col--
|
||||
if end.Col >= start.Col {
|
||||
deleteCharSelection(m, start, end)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Charwise motion spanning multiple lines (e.g., d/search)
|
||||
deleteCharSelection(m, start, end)
|
||||
}
|
||||
|
||||
func deleteCharSelection(m action.Model, start, end action.Position) {
|
||||
if start.Line == end.Line {
|
||||
line := m.Line(start.Line)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user