feat: lots of word actions. 'w', 'e', and 'b'. Includes tests.
This commit is contained in:
parent
3d3948d7e3
commit
84a7983a21
@ -44,7 +44,7 @@ func newTestModelWithCursorPos(t *testing.T, pos action.Position) *teatest.TestM
|
||||
return teatest.NewTestModel(t, NewModel(lines, pos), teatest.WithInitialTermSize(80, 24))
|
||||
}
|
||||
|
||||
func newTestModelWithCursorPosAndLines(t *testing.T, lines []string, pos action.Position) *teatest.TestModel {
|
||||
func newTestModelWithLinesAndCursorPos(t *testing.T, lines []string, pos action.Position) *teatest.TestModel {
|
||||
return teatest.NewTestModel(t, NewModel(lines, pos), teatest.WithInitialTermSize(80, 24))
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ func TestDeleteChar(t *testing.T) {
|
||||
|
||||
t.Run("test 'x' in middle of line", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 2, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "x")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -31,7 +31,7 @@ func TestDeleteChar(t *testing.T) {
|
||||
|
||||
t.Run("test 'x' at end of line", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 4, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
||||
sendKeys(tm, "x")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -77,7 +77,7 @@ func TestDeleteCharWithCount(t *testing.T) {
|
||||
|
||||
t.Run("test '2x' from middle", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 1, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
|
||||
sendKeys(tm, "2", "x")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
|
||||
@ -32,7 +32,7 @@ func TestEnterInsert(t *testing.T) {
|
||||
|
||||
t.Run("test 'i' insert in middle", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 2, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "i", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -43,7 +43,7 @@ func TestEnterInsert(t *testing.T) {
|
||||
|
||||
t.Run("test 'i' cursor moves back on esc", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 2, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "i", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -77,7 +77,7 @@ func TestEnterInsertAfter(t *testing.T) {
|
||||
|
||||
t.Run("test 'a' from middle of line", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 2, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "a", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -90,7 +90,7 @@ func TestEnterInsertAfter(t *testing.T) {
|
||||
func TestEnterInsertLineStart(t *testing.T) {
|
||||
t.Run("test 'I' enters insert mode at line start", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 3, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "I", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -101,7 +101,7 @@ func TestEnterInsertLineStart(t *testing.T) {
|
||||
|
||||
t.Run("test 'I' from end of line", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 5, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "I", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -125,7 +125,7 @@ func TestEnterInsertLineEnd(t *testing.T) {
|
||||
|
||||
t.Run("test 'A' from middle of line", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 2, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "A", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -154,7 +154,7 @@ func TestOpenLineBelow(t *testing.T) {
|
||||
|
||||
t.Run("test 'o' from middle of file", func(t *testing.T) {
|
||||
lines := []string{"line 1", "line 2", "line 3"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 0, Line: 1})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
||||
sendKeys(tm, "o", "n", "e", "w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -168,7 +168,7 @@ func TestOpenLineBelow(t *testing.T) {
|
||||
|
||||
t.Run("test 'o' at end of file", func(t *testing.T) {
|
||||
lines := []string{"line 1", "line 2"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 0, Line: 1})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
||||
sendKeys(tm, "o", "n", "e", "w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -233,7 +233,7 @@ func TestOpenLineBelowWithCount(t *testing.T) {
|
||||
func TestOpenLineAbove(t *testing.T) {
|
||||
t.Run("test 'O' creates line above", func(t *testing.T) {
|
||||
lines := []string{"line 1", "line 2"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 0, Line: 1})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
||||
sendKeys(tm, "O", "n", "e", "w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -261,7 +261,7 @@ func TestOpenLineAbove(t *testing.T) {
|
||||
|
||||
t.Run("test 'O' cursor at start of new line", func(t *testing.T) {
|
||||
lines := []string{"line 1", "line 2"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 3, Line: 1})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 1})
|
||||
sendKeys(tm, "O", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -274,7 +274,7 @@ func TestOpenLineAbove(t *testing.T) {
|
||||
func TestOpenLineAboveWithCount(t *testing.T) {
|
||||
t.Run("test '3O' creates 3 lines above", func(t *testing.T) {
|
||||
lines := []string{"line 1"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 0, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "3", "O", "x", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -294,7 +294,7 @@ func TestOpenLineAboveWithCount(t *testing.T) {
|
||||
func TestInsertModeEnter(t *testing.T) {
|
||||
t.Run("test enter splits line", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 5, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "i", "enter", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -311,7 +311,7 @@ func TestInsertModeEnter(t *testing.T) {
|
||||
|
||||
t.Run("test enter at end of line", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 5, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "i", "enter", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -347,7 +347,7 @@ func TestInsertModeEnter(t *testing.T) {
|
||||
func TestInsertModeBackspace(t *testing.T) {
|
||||
t.Run("test backspace deletes character", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 3, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "i", "backspace", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -358,7 +358,7 @@ func TestInsertModeBackspace(t *testing.T) {
|
||||
|
||||
t.Run("test backspace at start of line joins lines", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 0, Line: 1})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
||||
sendKeys(tm, "i", "backspace", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -383,7 +383,7 @@ func TestInsertModeBackspace(t *testing.T) {
|
||||
|
||||
t.Run("test multiple backspaces", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 5, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "i", "backspace", "backspace", "backspace", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -396,7 +396,7 @@ func TestInsertModeBackspace(t *testing.T) {
|
||||
func TestInsertModeDelete(t *testing.T) {
|
||||
t.Run("test delete deletes character", func(t *testing.T) {
|
||||
lines := []string{"world"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 3, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "i", "delete", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -407,7 +407,7 @@ func TestInsertModeDelete(t *testing.T) {
|
||||
|
||||
t.Run("test delete at end of line joins lines", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 5, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "i", "delete", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -435,7 +435,7 @@ func TestInsertModeDelete(t *testing.T) {
|
||||
|
||||
t.Run("test delete at end of last line does nothing", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 5, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "i", "delete", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -446,7 +446,7 @@ func TestInsertModeDelete(t *testing.T) {
|
||||
|
||||
t.Run("test multiple delete", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 1, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
|
||||
sendKeys(tm, "i", "delete", "delete", "delete", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
|
||||
@ -64,7 +64,7 @@ func TestMoveDownWithOverflow(t *testing.T) {
|
||||
lines := []string{"long line", "small"}
|
||||
|
||||
t.Run("test 'j' with overflow", func(t *testing.T) {
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 8, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 8, Line: 0})
|
||||
sendKeys(tm, "j")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -75,7 +75,7 @@ func TestMoveDownWithOverflow(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("test 'j' without overflow", func(t *testing.T) {
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 3, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "j")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -143,7 +143,7 @@ func TestMoveUpWithOverflow(t *testing.T) {
|
||||
lines := []string{"small", "long line"}
|
||||
|
||||
t.Run("test 'k' with overflow", func(t *testing.T) {
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 10, Line: 1})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1})
|
||||
sendKeys(tm, "k")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -154,7 +154,7 @@ func TestMoveUpWithOverflow(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("test 'k' without overflow", func(t *testing.T) {
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 3, Line: 1})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 1})
|
||||
sendKeys(tm, "k")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
|
||||
@ -41,7 +41,7 @@ func TestMoveToBottom(t *testing.T) {
|
||||
|
||||
t.Run("test 'G' clamps cursor.x", func(t *testing.T) {
|
||||
lines := []string{"long line here", "short"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 10, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "G")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -99,7 +99,7 @@ func TestMoveToTop(t *testing.T) {
|
||||
|
||||
t.Run("test 'gg' clamps cursor.x", func(t *testing.T) {
|
||||
lines := []string{"short", "long line here"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 10, Line: 1})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1})
|
||||
sendKeys(tm, "g", "g")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -128,7 +128,7 @@ func TestMoveToLineStart(t *testing.T) {
|
||||
|
||||
t.Run("test '0' from end of line", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: len(lines[0]), Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: len(lines[0]), Line: 0})
|
||||
sendKeys(tm, "0")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -184,7 +184,7 @@ func TestMoveToLineEnd(t *testing.T) {
|
||||
|
||||
t.Run("test '$' from middle of line", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 3, Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "$")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -196,7 +196,7 @@ func TestMoveToLineEnd(t *testing.T) {
|
||||
|
||||
t.Run("test '$' already at end", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: len(lines[0]), Line: 0})
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: len(lines[0]), Line: 0})
|
||||
sendKeys(tm, "$")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
|
||||
341
internal/editor/integration_motion_word_test.go
Normal file
341
internal/editor/integration_motion_word_test.go
Normal file
@ -0,0 +1,341 @@
|
||||
package editor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
)
|
||||
|
||||
// --- w, e, and b Tests ---
|
||||
|
||||
func TestMoveForwardWord(t *testing.T) {
|
||||
t.Run("test 'w' moves forward one word", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "w")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 6 {
|
||||
t.Errorf("m.CursorX() = %d, want 6", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'www' moves forward three words", func(t *testing.T) {
|
||||
lines := []string{"hello world hello world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "w", "w", "w")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 18 {
|
||||
t.Errorf("m.CursorX() = %d, want 18", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'w' moves to next line", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "w")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("m.CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
if m.CursorY() != 1 {
|
||||
t.Errorf("m.CursorY() = %d, want 1", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'w' at end of file preserves position", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "w")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 5 {
|
||||
t.Errorf("m.CursorX() = %d, want 5", m.CursorX())
|
||||
}
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("m.CursorY() = %d, want 0", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'w' stops at punctuation", func(t *testing.T) {
|
||||
lines := []string{"hello.word"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "w")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 5 {
|
||||
t.Errorf("m.CursorX() = %d, want 5", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'w' skips underscore", func(t *testing.T) {
|
||||
lines := []string{"hello_world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "w")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 11 {
|
||||
t.Errorf("m.CursorX() = %d, want 11", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'w' moves once past punctuation", func(t *testing.T) {
|
||||
lines := []string{".hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "w")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 1 {
|
||||
t.Errorf("m.CursorX() = %d, want 1", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'w' from middle of word skips only remaining chars", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "w")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 6 {
|
||||
t.Errorf("m.CursorX() = %d, want 6", m.CursorX())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoveToWordEnd(t *testing.T) {
|
||||
t.Run("test 'e' moves to end of current word", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "e")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("m.CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'eee' moves to end of three words", func(t *testing.T) {
|
||||
lines := []string{"hello world hello world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "e", "e", "e")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 16 {
|
||||
t.Errorf("m.CursorX() = %d, want 16", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'e' moves to next line when at end of word", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
||||
sendKeys(tm, "e")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("m.CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
if m.CursorY() != 1 {
|
||||
t.Errorf("m.CursorY() = %d, want 1", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'e' at end of file preserves position", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
||||
sendKeys(tm, "e")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("m.CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("m.CursorY() = %d, want 0", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'e' stops at end of word before punctuation", func(t *testing.T) {
|
||||
lines := []string{"hello.world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "e")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("m.CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'e' stops at end of punctuation sequence", func(t *testing.T) {
|
||||
lines := []string{"..hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "e")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 1 {
|
||||
t.Errorf("m.CursorX() = %d, want 1", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'e' skips underscore", func(t *testing.T) {
|
||||
lines := []string{"hello_world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "e")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 10 {
|
||||
t.Errorf("m.CursorX() = %d, want 10", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'e' moves past leading punctuation to end of word", func(t *testing.T) {
|
||||
lines := []string{".hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "e")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 5 {
|
||||
t.Errorf("m.CursorX() = %d, want 5", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'e' from middle of word moves to end of current word", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "e")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("m.CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ee' lands at end of each class in multi-char punctuation", func(t *testing.T) {
|
||||
lines := []string{"hello..world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "e", "e")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 6 {
|
||||
t.Errorf("m.CursorX() = %d, want 6", m.CursorX())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoveBackwardWord(t *testing.T) {
|
||||
t.Run("test 'b' moves to start of current word", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "b")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("m.CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'b' at word start moves to end previous", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
|
||||
sendKeys(tm, "b")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("m.CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'bbb' moves to back three word", func(t *testing.T) {
|
||||
lines := []string{"hello world hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 23, Line: 0})
|
||||
sendKeys(tm, "b", "b", "b")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 6 {
|
||||
t.Errorf("m.CursorX() = %d, want 6", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'b' moves to prev line when at start of line", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
||||
sendKeys(tm, "b")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("m.CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("m.CursorY() = %d, want 0", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'b' at start of file preserves position", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "b")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("m.CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'b' stops at punctuation", func(t *testing.T) {
|
||||
lines := []string{"hello.world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
|
||||
sendKeys(tm, "b")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 5 {
|
||||
t.Errorf("m.CursorX() = %d, want 5", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'b' stops at end of punctuation sequence", func(t *testing.T) {
|
||||
lines := []string{"..hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "b")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("m.CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'b' stops at end of punctuation sequence before word", func(t *testing.T) {
|
||||
lines := []string{"hello..world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 7, Line: 0})
|
||||
sendKeys(tm, "b")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 5 {
|
||||
t.Errorf("m.CursorX() = %d, want 5", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'b' stops at end of punctuation sequence on newline", func(t *testing.T) {
|
||||
lines := []string{"hello.", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
||||
sendKeys(tm, "b")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 5 {
|
||||
t.Errorf("m.CursorX() = %d, want 5", m.CursorX())
|
||||
}
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("m.CursorY() = %d, want 0", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'b' skips underscore", func(t *testing.T) {
|
||||
lines := []string{"hello_world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "b")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("m.CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
package editor
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/input"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
@ -12,19 +14,22 @@ type cursor struct {
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
lines []string
|
||||
cursor cursor
|
||||
s_gutter int
|
||||
mode action.Mode
|
||||
win_h int
|
||||
win_w int
|
||||
command string
|
||||
input *input.Handler
|
||||
lines []string
|
||||
cursor cursor
|
||||
mode action.Mode
|
||||
win_h int
|
||||
win_w int
|
||||
command string
|
||||
input *input.Handler
|
||||
|
||||
// Insert repetition
|
||||
insertCount int
|
||||
insertKeys []string
|
||||
insertAction action.Action
|
||||
|
||||
// Settings
|
||||
gutterSize int
|
||||
tabSize int
|
||||
}
|
||||
|
||||
func NewModel(lines []string, pos action.Position) Model {
|
||||
@ -34,10 +39,11 @@ func NewModel(lines []string, pos action.Position) Model {
|
||||
x: pos.Col,
|
||||
y: pos.Line,
|
||||
},
|
||||
s_gutter: 5,
|
||||
mode: action.NormalMode,
|
||||
command: "",
|
||||
input: input.NewHandler(),
|
||||
gutterSize: 5,
|
||||
tabSize: 2,
|
||||
mode: action.NormalMode,
|
||||
command: "",
|
||||
input: input.NewHandler(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,6 +200,16 @@ func (m *Model) processInsertKey(key string) {
|
||||
m.SetLine(y, l[:x]+l[x+1:])
|
||||
}
|
||||
|
||||
// TODO: This handling is wrong, we should be able to delete an entire tab with a single space
|
||||
case "tab":
|
||||
tabs := strings.Repeat(" ", m.tabSize)
|
||||
if x < len(l) {
|
||||
m.SetLine(y, l[:x]+tabs+l[x:])
|
||||
} else {
|
||||
m.SetLine(y, l+tabs)
|
||||
}
|
||||
m.SetCursorX(x + len(tabs))
|
||||
|
||||
// Regular character
|
||||
default:
|
||||
if x < len(l) {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package editor
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) cursorStyle() lipgloss.Style {
|
||||
@ -26,7 +26,7 @@ func (m Model) gutterStyle(currentLine bool) lipgloss.Style {
|
||||
fg = lipgloss.Color("#d69d00")
|
||||
}
|
||||
return lipgloss.NewStyle().
|
||||
Width(m.s_gutter).
|
||||
Width(m.gutterSize).
|
||||
Background(lipgloss.Color("236")).
|
||||
Foreground(fg)
|
||||
}
|
||||
|
||||
@ -21,17 +21,17 @@ func (m Model) View() string {
|
||||
)
|
||||
if y > m.cursor.y {
|
||||
lineNumber = y - m.cursor.y
|
||||
gutter = fmt.Sprintf("%*d ", m.s_gutter-1, lineNumber)
|
||||
gutter = fmt.Sprintf("%*d ", m.gutterSize-1, lineNumber)
|
||||
} else if y < m.cursor.y {
|
||||
lineNumber = m.cursor.y - y
|
||||
gutter = fmt.Sprintf("%*d ", m.s_gutter-1, lineNumber)
|
||||
gutter = fmt.Sprintf("%*d ", m.gutterSize-1, lineNumber)
|
||||
} else {
|
||||
lineNumber = y + 1
|
||||
currentLine = true
|
||||
if lineNumber < 100 {
|
||||
gutter = fmt.Sprintf("%*d ", m.s_gutter-2, lineNumber)
|
||||
gutter = fmt.Sprintf("%*d ", m.gutterSize-2, lineNumber)
|
||||
} else {
|
||||
gutter = fmt.Sprintf("%*d ", m.s_gutter-1, lineNumber)
|
||||
gutter = fmt.Sprintf("%*d ", m.gutterSize-1, lineNumber)
|
||||
}
|
||||
}
|
||||
view.WriteString(m.gutterStyle(currentLine).Render(gutter))
|
||||
@ -49,7 +49,7 @@ func (m Model) View() string {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
format := fmt.Sprintf("%%-%ds ", m.s_gutter-1)
|
||||
format := fmt.Sprintf("%%-%ds ", m.gutterSize-1)
|
||||
fmt.Fprintf(&view, format, "~")
|
||||
}
|
||||
|
||||
|
||||
@ -22,6 +22,9 @@ func NewNormalKeymap() *Keymap {
|
||||
"gg": motion.MoveToTop{},
|
||||
"0": motion.MoveToLineStart{},
|
||||
"$": motion.MoveToLineEnd{},
|
||||
"w": motion.MoveForwardWord{Count: 1},
|
||||
"e": motion.MoveForwardWordEnd{Count: 1},
|
||||
"b": motion.MoveBackwardWord{Count: 1},
|
||||
},
|
||||
operators: map[string]action.Operator{
|
||||
// "d": DeleteOp{},
|
||||
|
||||
232
internal/motion/word.go
Normal file
232
internal/motion/word.go
Normal file
@ -0,0 +1,232 @@
|
||||
package motion
|
||||
|
||||
import (
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func isWordChar(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '_'
|
||||
}
|
||||
|
||||
func isWordPunctuation(c byte) bool {
|
||||
return c != ' ' && c != '\t' && !isWordChar(c)
|
||||
}
|
||||
|
||||
func nextWordStart(m action.Model, x, y int) (int, int) {
|
||||
line := m.Line(y)
|
||||
|
||||
// Skip current class
|
||||
if x < len(line) {
|
||||
if isWordChar(line[x]) {
|
||||
for x < len(line) && isWordChar(line[x]) {
|
||||
x++
|
||||
}
|
||||
} else if line[x] != ' ' && line[x] != '\t' {
|
||||
// punctuation class
|
||||
for x < len(line) && isWordPunctuation(line[x]) {
|
||||
x++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip whitespace and cross lines if needed
|
||||
for {
|
||||
// Walk over white space
|
||||
for x < len(line) && (line[x] == ' ' || line[x] == '\t') {
|
||||
x++
|
||||
}
|
||||
|
||||
// Were on the new word, nothing else to do (no lines to cross
|
||||
if x < len(line) {
|
||||
break
|
||||
}
|
||||
|
||||
// If next line is the end of the file, exit now
|
||||
if y+1 >= m.LineCount() {
|
||||
return x, y
|
||||
}
|
||||
|
||||
// Move to first char of next line
|
||||
y++
|
||||
line = m.Line(y)
|
||||
x = 0
|
||||
|
||||
// If the first char of the new line is no whitespace, stay here!
|
||||
if len(line) > 0 && line[0] != ' ' && line[0] != '\t' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return x, y
|
||||
}
|
||||
|
||||
func nextWordEnd(m action.Model, x, y int) (int, int) {
|
||||
line := m.Line(y)
|
||||
|
||||
// Advance once to avoid being stuck on the current end
|
||||
x++
|
||||
if x >= len(line) {
|
||||
// At last line of file, pin cursor to end of file
|
||||
if y+1 >= m.LineCount() {
|
||||
return len(line) - 1, y
|
||||
}
|
||||
|
||||
// Otherwise, move to next line
|
||||
y++
|
||||
x = 0
|
||||
line = m.Line(y)
|
||||
}
|
||||
|
||||
// Skip whitespace and cross lines if needed
|
||||
for {
|
||||
// Walk over white space
|
||||
for x < len(line) && (line[x] == ' ' || line[x] == '\t') {
|
||||
x++
|
||||
}
|
||||
|
||||
// Were on the new word, nothing else to do (no lines to cross
|
||||
if x < len(line) {
|
||||
break
|
||||
}
|
||||
|
||||
// If next line is the end of the file, exit now
|
||||
if y+1 >= m.LineCount() {
|
||||
return x, y
|
||||
}
|
||||
|
||||
// Move to first char of next line
|
||||
y++
|
||||
line = m.Line(y)
|
||||
x = 0
|
||||
}
|
||||
|
||||
// Move to end of current char class, stop before it ends
|
||||
if isWordChar(line[x]) {
|
||||
for x+1 < len(line) && isWordChar(line[x+1]) {
|
||||
x++
|
||||
}
|
||||
} else {
|
||||
for x+1 < len(line) &&
|
||||
line[x+1] != ' ' &&
|
||||
line[x+1] != '\t' &&
|
||||
!isWordChar(line[x+1]) {
|
||||
x++
|
||||
}
|
||||
}
|
||||
|
||||
return x, y
|
||||
}
|
||||
|
||||
func prevWordStart(m action.Model, x, y int) (int, int) {
|
||||
line := m.Line(y)
|
||||
|
||||
// Back one to avoid being stuck on the current start
|
||||
x--
|
||||
if x < 0 {
|
||||
if y == 0 {
|
||||
return 0, 0 // beginning of file, stay put
|
||||
}
|
||||
y--
|
||||
line = m.Line(y)
|
||||
x = len(line) - 1
|
||||
if x < 0 {
|
||||
return 0, y // landed on an empty line
|
||||
}
|
||||
}
|
||||
|
||||
// Skip whitespace backward, crossing lines if needed
|
||||
for {
|
||||
for x >= 0 && (line[x] == ' ' || line[x] == '\t') {
|
||||
x--
|
||||
}
|
||||
if x >= 0 {
|
||||
break // landed on a non-whitespace char
|
||||
}
|
||||
if y == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
y--
|
||||
line = m.Line(y)
|
||||
x = len(line) - 1
|
||||
if len(line) == 0 {
|
||||
return 0, y // empty line acts as a word boundary
|
||||
}
|
||||
}
|
||||
|
||||
// Skip to the start of the current char class
|
||||
if isWordChar(line[x]) {
|
||||
for x-1 >= 0 && isWordChar(line[x-1]) {
|
||||
x--
|
||||
}
|
||||
} else {
|
||||
for x-1 >= 0 && isWordPunctuation(line[x-1]) {
|
||||
x--
|
||||
}
|
||||
}
|
||||
|
||||
return x, y
|
||||
}
|
||||
|
||||
type MoveForwardWord struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
// Execute implements [action.Action].
|
||||
func (a MoveForwardWord) Execute(m action.Model) tea.Cmd {
|
||||
x := m.CursorX()
|
||||
y := m.CursorY()
|
||||
for i := 0; i < a.Count; i++ {
|
||||
x, y = nextWordStart(m, x, y)
|
||||
}
|
||||
m.SetCursorX(x)
|
||||
m.SetCursorY(y)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveForwardWord) WithCount(n int) action.Action {
|
||||
return MoveForwardWord{Count: n}
|
||||
}
|
||||
|
||||
type MoveForwardWordEnd struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
// Execute implements [action.Action].
|
||||
func (a MoveForwardWordEnd) Execute(m action.Model) tea.Cmd {
|
||||
x := m.CursorX()
|
||||
y := m.CursorY()
|
||||
for i := 0; i < a.Count; i++ {
|
||||
x, y = nextWordEnd(m, x, y)
|
||||
}
|
||||
m.SetCursorX(x)
|
||||
m.SetCursorY(y)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveForwardWordEnd) WithCount(n int) action.Action {
|
||||
return MoveForwardWordEnd{Count: n}
|
||||
}
|
||||
|
||||
type MoveBackwardWord struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
// Execute implements [action.Action].
|
||||
func (a MoveBackwardWord) Execute(m action.Model) tea.Cmd {
|
||||
x := m.CursorX()
|
||||
y := m.CursorY()
|
||||
for i := 0; i < a.Count; i++ {
|
||||
x, y = prevWordStart(m, x, y)
|
||||
}
|
||||
m.SetCursorX(x)
|
||||
m.SetCursorY(y)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveBackwardWord) WithCount(n int) action.Action {
|
||||
return MoveBackwardWord{Count: n}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user