Compare commits
No commits in common. "c2b5e6f67cafbd048449fbd9aa4a0e8d2f40fc60" and "c62bbc89eee58ac494ee56e0786d6ba2dbd380c1" have entirely different histories.
c2b5e6f67c
...
c62bbc89ee
@ -8,7 +8,7 @@ import (
|
||||
|
||||
func main() {
|
||||
|
||||
lines := []string{"Hello world testing main.go", "line 2", "line 3", "line 4", "line 5"}
|
||||
lines := []string{"Hello world", "line 2", "line 3", "line 4", "line 5"}
|
||||
tea.NewProgram(
|
||||
editor.NewModel(lines, action.Position{Line: 0, Col: 0}),
|
||||
tea.WithAltScreen(),
|
||||
|
||||
@ -9,9 +9,6 @@ const (
|
||||
NormalMode Mode = iota
|
||||
InsertMode
|
||||
CommandMode
|
||||
VisualMode
|
||||
VisualLineMode
|
||||
VisualBlockMode
|
||||
)
|
||||
|
||||
// Model defines the interface for editor state that actions can modify
|
||||
@ -31,29 +28,12 @@ type Model interface {
|
||||
SetCursorY(y int)
|
||||
ClampCursorX()
|
||||
|
||||
// Anchor
|
||||
AnchorX() int
|
||||
AnchorY() int
|
||||
SetAnchorX(x int)
|
||||
SetAnchorY(y int)
|
||||
|
||||
// Insert
|
||||
InsertKeys() []string
|
||||
SetInsertKeys(keys []string)
|
||||
|
||||
// Settings
|
||||
TabSize() int
|
||||
|
||||
// Mode
|
||||
Mode() Mode
|
||||
SetMode(mode Mode)
|
||||
IsVisualMode() bool
|
||||
|
||||
// Insert recording (for count replay)
|
||||
SetInsertRecording(count int, action Action)
|
||||
|
||||
// ExitInsertMode handles replay, cursor step-back, and mode transition on esc
|
||||
ExitInsertMode()
|
||||
}
|
||||
|
||||
// Position represents a location in the buffer
|
||||
@ -75,7 +55,7 @@ type Motion interface {
|
||||
type Operator interface {
|
||||
Operate(m Model, start, end Position) tea.Cmd
|
||||
// DoublePress handles dd, yy, cc (line-wise)
|
||||
DoublePress(m Model, count int) tea.Cmd
|
||||
DoublePress(m Model) tea.Cmd
|
||||
}
|
||||
|
||||
// Repeatable actions track count
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
import tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
// EnterInsert implements Action (i)
|
||||
type EnterInsert struct {
|
||||
@ -125,154 +121,3 @@ func (a OpenLineAbove) Execute(m Model) tea.Cmd {
|
||||
func (a OpenLineAbove) WithCount(n int) Action {
|
||||
return OpenLineAbove{Count: n}
|
||||
}
|
||||
|
||||
// --- Insert mode edit actions ---
|
||||
|
||||
// InsertChar inserts a single character (or rune sequence) at the cursor
|
||||
type InsertChar struct {
|
||||
Char string
|
||||
}
|
||||
|
||||
func (a InsertChar) Execute(m Model) tea.Cmd {
|
||||
x, y := m.CursorX(), m.CursorY()
|
||||
l := m.Line(y)
|
||||
if x < len(l) {
|
||||
m.SetLine(y, l[:x]+a.Char+l[x:])
|
||||
} else {
|
||||
m.SetLine(y, l+a.Char)
|
||||
}
|
||||
m.SetCursorX(x + len(a.Char))
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertNewline splits the current line at the cursor (enter key)
|
||||
type InsertNewline struct{}
|
||||
|
||||
func (a InsertNewline) Execute(m Model) tea.Cmd {
|
||||
x, y := m.CursorX(), m.CursorY()
|
||||
l := m.Line(y)
|
||||
if x == len(l) {
|
||||
m.InsertLine(y+1, "")
|
||||
} else {
|
||||
m.SetLine(y, l[:x])
|
||||
m.InsertLine(y+1, l[x:])
|
||||
}
|
||||
m.SetCursorY(y + 1)
|
||||
m.SetCursorX(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertBackspace deletes the character before the cursor
|
||||
type InsertBackspace struct{}
|
||||
|
||||
func (a InsertBackspace) Execute(m Model) tea.Cmd {
|
||||
x, y := m.CursorX(), m.CursorY()
|
||||
l := m.Line(y)
|
||||
if x > 0 {
|
||||
m.SetLine(y, l[:x-1]+l[x:])
|
||||
m.SetCursorX(x - 1)
|
||||
} else if y > 0 {
|
||||
prevLine := m.Line(y - 1)
|
||||
newX := len(prevLine)
|
||||
m.SetLine(y-1, prevLine+l)
|
||||
m.DeleteLine(y)
|
||||
m.SetCursorY(y - 1)
|
||||
m.SetCursorX(newX)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertDelete deletes the character under/after the cursor (delete key)
|
||||
type InsertDelete struct{}
|
||||
|
||||
func (a InsertDelete) Execute(m Model) tea.Cmd {
|
||||
x, y := m.CursorX(), m.CursorY()
|
||||
l := m.Line(y)
|
||||
if x == len(l) && y < m.LineCount()-1 {
|
||||
nextLine := m.Line(y + 1)
|
||||
m.SetLine(y, l+nextLine)
|
||||
m.DeleteLine(y + 1)
|
||||
} else if x < len(l) {
|
||||
m.SetLine(y, l[:x]+l[x+1:])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertTab inserts spaces equal to the tab size
|
||||
type InsertTab struct{}
|
||||
|
||||
func (a InsertTab) Execute(m Model) tea.Cmd {
|
||||
x, y := m.CursorX(), m.CursorY()
|
||||
l := m.Line(y)
|
||||
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))
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertDeletePreviousWord deletes the word before the cursor (ctrl+w)
|
||||
type InsertDeletePreviousWord struct{}
|
||||
|
||||
func isWordChar(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '_'
|
||||
}
|
||||
|
||||
func isPunctuation(c byte) bool {
|
||||
return c != ' ' && c != '\t' && !isWordChar(c)
|
||||
}
|
||||
|
||||
func (a InsertDeletePreviousWord) Execute(m Model) tea.Cmd {
|
||||
x, y := m.CursorX(), m.CursorY()
|
||||
line := m.Line(y)
|
||||
|
||||
// At start of line: merge with previous line (same as backspace)
|
||||
if x == 0 {
|
||||
if y > 0 {
|
||||
prevLine := m.Line(y - 1)
|
||||
newX := len(prevLine)
|
||||
m.SetLine(y-1, prevLine+line)
|
||||
m.DeleteLine(y)
|
||||
m.SetCursorY(y - 1)
|
||||
m.SetCursorX(newX)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scan backwards to find the new cursor position (don't mutate yet)
|
||||
newX := x
|
||||
|
||||
// If we are on puncuation, we should just skip them all and quit
|
||||
if isPunctuation(line[newX-1]) {
|
||||
for newX > 0 && isPunctuation(line[newX-1]) {
|
||||
newX--
|
||||
}
|
||||
|
||||
m.SetLine(y, line[:newX]+line[x:])
|
||||
m.SetCursorX(newX)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip whitespace immediately before the cursor
|
||||
for newX > 0 && (line[newX-1] == ' ' || line[newX-1] == '\t') {
|
||||
newX--
|
||||
}
|
||||
|
||||
// Skip the word characters before the cursor
|
||||
for newX > 0 && isWordChar(line[newX-1]) {
|
||||
newX--
|
||||
}
|
||||
|
||||
// Delete everything from newX up to x in one operation
|
||||
m.SetLine(y, line[:newX]+line[x:])
|
||||
m.SetCursorX(newX)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -8,41 +8,3 @@ type Quit struct{}
|
||||
func (a Quit) Execute(m Model) tea.Cmd {
|
||||
return tea.Quit
|
||||
}
|
||||
|
||||
// Quit implements Action (:)
|
||||
type EnterComandMode struct{}
|
||||
|
||||
func (a EnterComandMode) Execute(m Model) tea.Cmd {
|
||||
m.SetMode(CommandMode)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Quit implements Action (v)
|
||||
type EnterVisualMode struct{}
|
||||
|
||||
func (a EnterVisualMode) Execute(m Model) tea.Cmd {
|
||||
m.SetAnchorX(m.CursorX())
|
||||
m.SetAnchorY(m.CursorY())
|
||||
m.SetMode(VisualMode)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Quit implements Action (V)
|
||||
type EnterVisualLineMode struct{}
|
||||
|
||||
func (a EnterVisualLineMode) Execute(m Model) tea.Cmd {
|
||||
m.SetAnchorX(m.CursorX())
|
||||
m.SetAnchorY(m.CursorY())
|
||||
m.SetMode(VisualLineMode)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Quit implements Action (ctrl+v)
|
||||
type EnterVisualBlockMode struct{}
|
||||
|
||||
func (a EnterVisualBlockMode) Execute(m Model) tea.Cmd {
|
||||
m.SetAnchorX(m.CursorX())
|
||||
m.SetAnchorY(m.CursorY())
|
||||
m.SetMode(VisualBlockMode)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -23,10 +23,6 @@ func sendKeys(tm *teatest.TestModel, keys ...string) {
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlC})
|
||||
case "ctrl+d":
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlD})
|
||||
case "ctrl+v":
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlV})
|
||||
case "ctrl+w":
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlW})
|
||||
default:
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(key)})
|
||||
}
|
||||
@ -48,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 newTestModelWithLinesAndCursorPos(t *testing.T, lines []string, pos action.Position) *teatest.TestModel {
|
||||
func newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 1})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "i", "backspace", "backspace", "backspace", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
@ -392,367 +392,3 @@ func TestInsertModeBackspace(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInsertModeDelete(t *testing.T) {
|
||||
t.Run("test delete deletes character", func(t *testing.T) {
|
||||
lines := []string{"world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "i", "delete", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "word" {
|
||||
t.Errorf("lines[0] = %q, want 'word'", m.lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test delete at end of line joins lines", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "i", "delete", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if len(m.lines) != 1 {
|
||||
t.Errorf("len(lines) = %d, want 1", len(m.lines))
|
||||
}
|
||||
if m.lines[0] != "helloworld" {
|
||||
t.Errorf("lines[0] = %q, want 'helloworld'", m.lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test delete at start of empty line joins lines", func(t *testing.T) {
|
||||
lines := []string{"", "world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "i", "delete", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if len(m.lines) != 1 {
|
||||
t.Errorf("len(lines) = %d, want 1", len(m.lines))
|
||||
}
|
||||
if m.lines[0] != "world" {
|
||||
t.Errorf("lines[0] = %q, want 'world'", m.lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test delete at end of last line does nothing", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "i", "delete", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "hello" {
|
||||
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test multiple delete", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
|
||||
sendKeys(tm, "i", "delete", "delete", "delete", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "ho" {
|
||||
t.Errorf("lines[0] = %q, want 'he'", m.lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestInsertModeArrowKeys(t *testing.T) {
|
||||
t.Run("test left arrow moves cursor left", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "i", "left", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "heXllo" {
|
||||
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test right arrow moves cursor right", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
|
||||
sendKeys(tm, "i", "right", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "heXllo" {
|
||||
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test up arrow moves cursor up", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 1})
|
||||
sendKeys(tm, "i", "up", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "heXllo" {
|
||||
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0])
|
||||
}
|
||||
if m.lines[1] != "world" {
|
||||
t.Errorf("lines[1] = %q, want 'world'", m.lines[1])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test down arrow moves cursor down", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "i", "down", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "hello" {
|
||||
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0])
|
||||
}
|
||||
if m.lines[1] != "woXrld" {
|
||||
t.Errorf("lines[1] = %q, want 'woXrld'", m.lines[1])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test left arrow at start of line does nothing", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "i", "left", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "Xhello" {
|
||||
t.Errorf("lines[0] = %q, want 'Xhello'", m.lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test right arrow at end of line does nothing", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
||||
sendKeys(tm, "a", "right", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "helloX" {
|
||||
t.Errorf("lines[0] = %q, want 'helloX'", m.lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test up arrow at first line does nothing", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "i", "up", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "heXllo" {
|
||||
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test down arrow at last line does nothing", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "i", "down", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "heXllo" {
|
||||
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test up arrow clamps cursor to shorter line", func(t *testing.T) {
|
||||
lines := []string{"hi", "hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 1})
|
||||
sendKeys(tm, "i", "up", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "hiX" {
|
||||
t.Errorf("lines[0] = %q, want 'hiX'", m.lines[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test down arrow clamps cursor to shorter line", func(t *testing.T) {
|
||||
lines := []string{"hello", "hi"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
||||
sendKeys(tm, "i", "down", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[1] != "hiX" {
|
||||
t.Errorf("lines[1] = %q, want 'hiX'", m.lines[1])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test multiple arrow keys in sequence", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "i", "right", "right", "down", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[1] != "woXrld" {
|
||||
t.Errorf("lines[1] = %q, want 'woXrld'", m.lines[1])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInsertModeDeletePreviousWord(t *testing.T) {
|
||||
t.Run("test 'ctrl+w' deletes word", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "hello " {
|
||||
t.Errorf("lines[0] = %q, want 'hello '", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 5 {
|
||||
t.Errorf("CursorX() = %d, want '5'", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' deletes word with whitespace", func(t *testing.T) {
|
||||
lines := []string{"hello "}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "" {
|
||||
t.Errorf("lines[0] = %q, want ''", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want '0'", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' deletes word until period", func(t *testing.T) {
|
||||
lines := []string{"hello wo..."}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "hello wo" {
|
||||
t.Errorf("lines[0] = %q, want 'hello wo'", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 7 {
|
||||
t.Errorf("CursorX() = %d, want '7'", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' deletes line when blank", func(t *testing.T) {
|
||||
lines := []string{"", ""}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
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())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' deletes all whitespace when line is only whitespace", func(t *testing.T) {
|
||||
lines := []string{" "}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 1 {
|
||||
t.Errorf("LineCount() = %d, want '1'", m.LineCount())
|
||||
}
|
||||
if m.Line(0) != "" {
|
||||
t.Errorf("Line(0) = %s, want ''", m.Line(0))
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want '0'", m.CursorX())
|
||||
}
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("CursorY() = %d, want '0'", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' at start of first line does nothing", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "i", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "hello" {
|
||||
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' from middle of word", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "i", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "lo" {
|
||||
t.Errorf("lines[0] = %q, want 'lo'", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' deletes word after punctuation", func(t *testing.T) {
|
||||
lines := []string{"...hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 7, Line: 0})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "..." {
|
||||
t.Errorf("lines[0] = %q, want '...'", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 2 {
|
||||
t.Errorf("CursorX() = %d, want 2", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' with tabs as whitespace", func(t *testing.T) {
|
||||
lines := []string{"hello\tworld"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "hello\t" {
|
||||
t.Errorf("lines[0] = %q, want 'hello\\t'", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 5 {
|
||||
t.Errorf("CursorX() = %d, want 5", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' at start of line merges with previous line content", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
||||
sendKeys(tm, "i", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 1 {
|
||||
t.Errorf("LineCount() = %d, want 1", m.LineCount())
|
||||
}
|
||||
if m.lines[0] != "helloworld" {
|
||||
t.Errorf("lines[0] = %q, want 'helloworld'", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+w' with underscore in word", func(t *testing.T) {
|
||||
lines := []string{"hello_world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.lines[0] != "" {
|
||||
t.Errorf("lines[0] = %q, want ''", m.lines[0])
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 8, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1})
|
||||
tm := newTestModelWithCursorPosAndLines(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 := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 1})
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 3, Line: 1})
|
||||
sendKeys(tm, "k")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
|
||||
@ -14,8 +14,8 @@ func TestMoveToBottom(t *testing.T) {
|
||||
sendKeys(tm, "G")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 5 {
|
||||
t.Errorf("CursorY() = %d, want 5", m.CursorY())
|
||||
if m.cursor.y != 5 {
|
||||
t.Errorf("cursor.y = %d, want 5", m.cursor.y)
|
||||
}
|
||||
})
|
||||
|
||||
@ -24,8 +24,8 @@ func TestMoveToBottom(t *testing.T) {
|
||||
sendKeys(tm, "G")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 5 {
|
||||
t.Errorf("CursorY() = %d, want 5", m.CursorY())
|
||||
if m.cursor.y != 5 {
|
||||
t.Errorf("cursor.y = %d, want 5", m.cursor.y)
|
||||
}
|
||||
})
|
||||
|
||||
@ -34,23 +34,23 @@ func TestMoveToBottom(t *testing.T) {
|
||||
sendKeys(tm, "G")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 5 {
|
||||
t.Errorf("CursorY() = %d, want 5", m.CursorY())
|
||||
if m.cursor.y != 5 {
|
||||
t.Errorf("cursor.y = %d, want 5", m.cursor.y)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'G' clamps CursorX()", func(t *testing.T) {
|
||||
t.Run("test 'G' clamps cursor.x", func(t *testing.T) {
|
||||
lines := []string{"long line here", "short"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "G")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 1 {
|
||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
||||
if m.cursor.y != 1 {
|
||||
t.Errorf("cursor.y = %d, want 1", m.cursor.y)
|
||||
}
|
||||
want := len(lines[1])
|
||||
if m.CursorX() != want {
|
||||
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
|
||||
if m.cursor.x != want {
|
||||
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
|
||||
}
|
||||
})
|
||||
|
||||
@ -60,8 +60,8 @@ func TestMoveToBottom(t *testing.T) {
|
||||
sendKeys(tm, "G")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
||||
if m.cursor.y != 0 {
|
||||
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -72,8 +72,8 @@ func TestMoveToTop(t *testing.T) {
|
||||
sendKeys(tm, "g", "g")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
||||
if m.cursor.y != 0 {
|
||||
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
|
||||
}
|
||||
})
|
||||
|
||||
@ -82,8 +82,8 @@ func TestMoveToTop(t *testing.T) {
|
||||
sendKeys(tm, "g", "g")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
||||
if m.cursor.y != 0 {
|
||||
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
|
||||
}
|
||||
})
|
||||
|
||||
@ -92,28 +92,28 @@ func TestMoveToTop(t *testing.T) {
|
||||
sendKeys(tm, "g", "g")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
||||
if m.cursor.y != 0 {
|
||||
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'gg' clamps CursorX()", func(t *testing.T) {
|
||||
t.Run("test 'gg' clamps cursor.x", func(t *testing.T) {
|
||||
lines := []string{"short", "long line here"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1})
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 10, Line: 1})
|
||||
sendKeys(tm, "g", "g")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
||||
if m.cursor.y != 0 {
|
||||
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
|
||||
}
|
||||
want := len(lines[0])
|
||||
if m.CursorX() != want {
|
||||
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
|
||||
if m.cursor.x != want {
|
||||
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- 0, $ and _ Tests ---
|
||||
// --- 0 and $ Tests ---
|
||||
|
||||
func TestMoveToLineStart(t *testing.T) {
|
||||
t.Run("test '0' from middle of line", func(t *testing.T) {
|
||||
@ -121,19 +121,19 @@ func TestMoveToLineStart(t *testing.T) {
|
||||
sendKeys(tm, "0")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
if m.cursor.x != 0 {
|
||||
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '0' from end of line", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: len(lines[0]), Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: len(lines[0]), Line: 0})
|
||||
sendKeys(tm, "0")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
if m.cursor.x != 0 {
|
||||
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
|
||||
}
|
||||
})
|
||||
|
||||
@ -142,8 +142,8 @@ func TestMoveToLineStart(t *testing.T) {
|
||||
sendKeys(tm, "0")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
if m.cursor.x != 0 {
|
||||
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
|
||||
}
|
||||
})
|
||||
|
||||
@ -153,8 +153,8 @@ func TestMoveToLineStart(t *testing.T) {
|
||||
sendKeys(tm, "0")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
if m.cursor.x != 0 {
|
||||
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
|
||||
}
|
||||
})
|
||||
|
||||
@ -163,8 +163,8 @@ func TestMoveToLineStart(t *testing.T) {
|
||||
sendKeys(tm, "0")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 2 {
|
||||
t.Errorf("CursorY() = %d, want 2", m.CursorY())
|
||||
if m.cursor.y != 2 {
|
||||
t.Errorf("cursor.y = %d, want 2", m.cursor.y)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -177,32 +177,32 @@ func TestMoveToLineEnd(t *testing.T) {
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
want := len(lines[0])
|
||||
if m.CursorX() != want {
|
||||
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
|
||||
if m.cursor.x != want {
|
||||
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '$' from middle of line", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "$")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
want := len(lines[0])
|
||||
if m.CursorX() != want {
|
||||
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
|
||||
if m.cursor.x != want {
|
||||
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '$' already at end", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: len(lines[0]), Line: 0})
|
||||
tm := newTestModelWithCursorPosAndLines(t, lines, action.Position{Col: len(lines[0]), Line: 0})
|
||||
sendKeys(tm, "$")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
want := len(lines[0])
|
||||
if m.CursorX() != want {
|
||||
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
|
||||
if m.cursor.x != want {
|
||||
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
|
||||
}
|
||||
})
|
||||
|
||||
@ -212,8 +212,8 @@ func TestMoveToLineEnd(t *testing.T) {
|
||||
sendKeys(tm, "$")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
if m.cursor.x != 0 {
|
||||
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
|
||||
}
|
||||
})
|
||||
|
||||
@ -222,87 +222,8 @@ func TestMoveToLineEnd(t *testing.T) {
|
||||
sendKeys(tm, "$")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 2 {
|
||||
t.Errorf("CursorY() = %d, want 2", m.CursorY())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoveToLineContentStart(t *testing.T) {
|
||||
t.Run("test '_' from middle of line with no leading whitespace", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
|
||||
sendKeys(tm, "_")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '_' from middle of line with leading whitespace", func(t *testing.T) {
|
||||
lines := []string{" hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "_")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '_' from start of line with leading whitespace", func(t *testing.T) {
|
||||
lines := []string{" hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "_")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '_' from start of line with no leading whitespace", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "_")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '_' from middle of line with only whitespace", func(t *testing.T) {
|
||||
lines := []string{" "}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "_")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '_' from end of line with only whitespace", func(t *testing.T) {
|
||||
lines := []string{" "}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
||||
sendKeys(tm, "_")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '_' on empty line", func(t *testing.T) {
|
||||
lines := []string{""}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "_")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
if m.cursor.y != 2 {
|
||||
t.Errorf("cursor.y = %d, want 2", m.cursor.y)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,341 +0,0 @@
|
||||
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,199 +0,0 @@
|
||||
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())
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@ -1,272 +0,0 @@
|
||||
package editor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
)
|
||||
|
||||
// --- Visual Mode Selection State Tests ---
|
||||
|
||||
func TestVisualModeSelectionState(t *testing.T) {
|
||||
t.Run("test 'v' enters visual mode and sets anchor at cursor", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "v")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.Mode() != action.VisualMode {
|
||||
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
|
||||
}
|
||||
if m.AnchorX() != 3 {
|
||||
t.Errorf("AnchorX() = %d, want 3", m.AnchorX())
|
||||
}
|
||||
if m.AnchorY() != 0 {
|
||||
t.Errorf("AnchorY() = %d, want 0", m.AnchorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'vl' moves cursor right, anchor stays", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "v", "l")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.AnchorX() != 0 {
|
||||
t.Errorf("AnchorX() = %d, want 0", m.AnchorX())
|
||||
}
|
||||
if m.CursorX() != 1 {
|
||||
t.Errorf("CursorX() = %d, want 1", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'vh' creates backward selection", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "v", "h")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.AnchorX() != 3 {
|
||||
t.Errorf("AnchorX() = %d, want 3", m.AnchorX())
|
||||
}
|
||||
if m.CursorX() != 2 {
|
||||
t.Errorf("CursorX() = %d, want 2", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'vj' extends selection down", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "v", "j")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.AnchorX() != 2 {
|
||||
t.Errorf("AnchorX() = %d, want 2", m.AnchorX())
|
||||
}
|
||||
if m.AnchorY() != 0 {
|
||||
t.Errorf("AnchorY() = %d, want 0", m.AnchorY())
|
||||
}
|
||||
if m.CursorY() != 1 {
|
||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'V' enters visual line mode and sets anchor at cursor", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
||||
sendKeys(tm, "V")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.Mode() != action.VisualLineMode {
|
||||
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
|
||||
}
|
||||
if m.AnchorY() != 1 {
|
||||
t.Errorf("AnchorY() = %d, want 1", m.AnchorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+v' enters visual block mode and sets anchor at cursor", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 1})
|
||||
sendKeys(tm, "ctrl+v")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.Mode() != action.VisualBlockMode {
|
||||
t.Errorf("Mode() = %v, want VisualBlockMode", m.Mode())
|
||||
}
|
||||
if m.AnchorX() != 2 {
|
||||
t.Errorf("AnchorX() = %d, want 2", m.AnchorX())
|
||||
}
|
||||
if m.AnchorY() != 1 {
|
||||
t.Errorf("AnchorY() = %d, want 1", m.AnchorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'esc' returns to normal mode from visual", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "v", "l", "l", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.Mode() != action.NormalMode {
|
||||
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- Visual Mode Delete Tests ---
|
||||
|
||||
func TestVisualModeDelete(t *testing.T) {
|
||||
t.Run("test 'vd' deletes single char under cursor", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "v", "d")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.Line(0) != "ello" {
|
||||
t.Errorf("Line(0) = %q, want \"ello\"", m.Line(0))
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'vlll d' deletes four chars on same line", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "v", "l", "l", "l", "d")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.Line(0) != "o world" {
|
||||
t.Errorf("Line(0) = %q, want \"o world\"", m.Line(0))
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'v' backward selection 'hh d' deletes correct range", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "v", "h", "h", "d")
|
||||
|
||||
// anchor=3, cursor=1 → normalized start=1, end=3 → delete "ell" → "ho"
|
||||
m := getFinalModel(t, tm)
|
||||
if m.Line(0) != "ho" {
|
||||
t.Errorf("Line(0) = %q, want \"ho\"", m.Line(0))
|
||||
}
|
||||
if m.CursorX() != 1 {
|
||||
t.Errorf("CursorX() = %d, want 1", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'vj d' deletes char selection across two lines", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "v", "j", "d")
|
||||
|
||||
// start=(2,0), end=(2,1) → prefix="he", suffix="ld" → "held"
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 1 {
|
||||
t.Errorf("LineCount() = %d, want 1", m.LineCount())
|
||||
}
|
||||
if m.Line(0) != "held" {
|
||||
t.Errorf("Line(0) = %q, want \"held\"", m.Line(0))
|
||||
}
|
||||
if m.CursorX() != 2 {
|
||||
t.Errorf("CursorX() = %d, want 2", m.CursorX())
|
||||
}
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'Vd' deletes current line", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "V", "d")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 1 {
|
||||
t.Errorf("LineCount() = %d, want 1", m.LineCount())
|
||||
}
|
||||
if m.Line(0) != "world" {
|
||||
t.Errorf("Line(0) = %q, want \"world\"", m.Line(0))
|
||||
}
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'Vjd' deletes two lines", func(t *testing.T) {
|
||||
lines := []string{"hello", "world", "testing"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "V", "j", "d")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 1 {
|
||||
t.Errorf("LineCount() = %d, want 1", m.LineCount())
|
||||
}
|
||||
if m.Line(0) != "testing" {
|
||||
t.Errorf("Line(0) = %q, want \"testing\"", m.Line(0))
|
||||
}
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'Vkd' deletes two lines with backward selection", func(t *testing.T) {
|
||||
lines := []string{"hello", "world", "testing"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
|
||||
sendKeys(tm, "V", "k", "d")
|
||||
|
||||
// anchor=line2, cursor=line1 → normalized start=line1, end=line2 → delete both
|
||||
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))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+v ljd' deletes block selection", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "ctrl+v", "l", "j", "d")
|
||||
|
||||
// anchor=(0,0), cursor=(1,1) → block cols 0-1, lines 0-1
|
||||
// "hello"[:0]+"hello"[2:] = "llo"
|
||||
// "world"[:0]+"world"[2:] = "rld"
|
||||
m := getFinalModel(t, tm)
|
||||
if m.Line(0) != "llo" {
|
||||
t.Errorf("Line(0) = %q, want \"llo\"", m.Line(0))
|
||||
}
|
||||
if m.Line(1) != "rld" {
|
||||
t.Errorf("Line(1) = %q, want \"rld\"", m.Line(1))
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+v' backward col selection deletes correct block", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "ctrl+v", "h", "h", "j", "d")
|
||||
|
||||
// anchor=(3,0), cursor=(1,1) → cols min(3,1)=1 to max(3,1)=3, lines 0-1
|
||||
// "hello"[:1]+"hello"[4:] = "h"+"o" = "ho"
|
||||
// "world"[:1]+"world"[4:] = "w"+"d" = "wd"
|
||||
m := getFinalModel(t, tm)
|
||||
if m.Line(0) != "ho" {
|
||||
t.Errorf("Line(0) = %q, want \"ho\"", m.Line(0))
|
||||
}
|
||||
if m.Line(1) != "wd" {
|
||||
t.Errorf("Line(1) = %q, want \"wd\"", m.Line(1))
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1,8 +1,6 @@
|
||||
package editor
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/input"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
@ -16,7 +14,7 @@ type cursor struct {
|
||||
type Model struct {
|
||||
lines []string
|
||||
cursor cursor
|
||||
anchor cursor // starting point for visual modes
|
||||
s_gutter int
|
||||
mode action.Mode
|
||||
win_h int
|
||||
win_w int
|
||||
@ -27,10 +25,6 @@ type Model struct {
|
||||
insertCount int
|
||||
insertKeys []string
|
||||
insertAction action.Action
|
||||
|
||||
// Settings
|
||||
gutterSize int
|
||||
tabSize int
|
||||
}
|
||||
|
||||
func NewModel(lines []string, pos action.Position) Model {
|
||||
@ -40,8 +34,7 @@ func NewModel(lines []string, pos action.Position) Model {
|
||||
x: pos.Col,
|
||||
y: pos.Line,
|
||||
},
|
||||
gutterSize: 5,
|
||||
tabSize: 2,
|
||||
s_gutter: 5,
|
||||
mode: action.NormalMode,
|
||||
command: "",
|
||||
input: input.NewHandler(),
|
||||
@ -107,37 +100,6 @@ func (m *Model) SetCursorY(y int) {
|
||||
m.cursor.y = y
|
||||
}
|
||||
|
||||
// Anchor methods
|
||||
func (m *Model) AnchorX() int {
|
||||
return m.anchor.x
|
||||
}
|
||||
|
||||
func (m *Model) AnchorY() int {
|
||||
return m.anchor.y
|
||||
}
|
||||
|
||||
func (m *Model) SetAnchorX(x int) {
|
||||
m.anchor.x = x
|
||||
}
|
||||
|
||||
func (m *Model) SetAnchorY(y int) {
|
||||
m.anchor.y = y
|
||||
}
|
||||
|
||||
// Insert methods
|
||||
func (m *Model) InsertKeys() []string {
|
||||
return m.insertKeys
|
||||
}
|
||||
|
||||
func (m *Model) SetInsertKeys(keys []string) {
|
||||
m.insertKeys = keys
|
||||
}
|
||||
|
||||
// Settings
|
||||
func (m *Model) TabSize() int {
|
||||
return m.tabSize
|
||||
}
|
||||
|
||||
func (m *Model) ClampCursorX() {
|
||||
lineLen := len(m.lines[m.cursor.y])
|
||||
if lineLen == 0 {
|
||||
@ -155,12 +117,6 @@ func (m *Model) SetMode(mode action.Mode) {
|
||||
m.mode = mode
|
||||
}
|
||||
|
||||
func (m *Model) IsVisualMode() bool {
|
||||
return m.mode == action.VisualMode ||
|
||||
m.mode == action.VisualLineMode ||
|
||||
m.mode == action.VisualBlockMode
|
||||
}
|
||||
|
||||
func (m *Model) SetInsertRecording(count int, act action.Action) {
|
||||
m.insertCount = count
|
||||
m.insertKeys = []string{}
|
||||
@ -195,18 +151,6 @@ func (m *Model) replayInsert() {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) ExitInsertMode() {
|
||||
if m.insertCount > 1 {
|
||||
m.replayInsert()
|
||||
}
|
||||
if m.cursor.x > 0 {
|
||||
m.cursor.x--
|
||||
}
|
||||
m.mode = action.NormalMode
|
||||
m.insertCount = 0
|
||||
m.insertKeys = nil
|
||||
}
|
||||
|
||||
func (m *Model) processInsertKey(key string) {
|
||||
x := m.CursorX()
|
||||
y := m.CursorY()
|
||||
@ -214,12 +158,17 @@ func (m *Model) processInsertKey(key string) {
|
||||
|
||||
switch key {
|
||||
case "enter":
|
||||
|
||||
// Simple case, at end, just create a line
|
||||
if x == len(l) {
|
||||
m.InsertLine(y+1, "")
|
||||
|
||||
// otherwise, splice
|
||||
} else {
|
||||
m.SetLine(y, l[:x])
|
||||
m.InsertLine(y+1, l[x:])
|
||||
}
|
||||
|
||||
m.SetCursorY(y + 1)
|
||||
m.SetCursorX(0)
|
||||
|
||||
@ -236,53 +185,7 @@ func (m *Model) processInsertKey(key string) {
|
||||
m.SetCursorX(newX)
|
||||
}
|
||||
|
||||
case "delete":
|
||||
if x == len(l) && y < m.LineCount()-1 {
|
||||
nextLine := m.Line(y + 1)
|
||||
m.SetLine(y, l+nextLine)
|
||||
m.DeleteLine(y + 1)
|
||||
} else if x < len(l) {
|
||||
m.SetLine(y, l[:x]+l[x+1:])
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
case "up":
|
||||
if y > 0 {
|
||||
m.SetCursorY(y - 1)
|
||||
m.ClampCursorX()
|
||||
}
|
||||
|
||||
case "down":
|
||||
if y+1 < m.LineCount() {
|
||||
m.SetCursorY(y + 1)
|
||||
m.ClampCursorX()
|
||||
}
|
||||
|
||||
case "left":
|
||||
if x > 0 {
|
||||
m.SetCursorX(x - 1)
|
||||
} else if y > 0 {
|
||||
prevLine := m.Line(y - 1)
|
||||
m.SetCursorX(len(prevLine))
|
||||
m.SetCursorY(y - 1)
|
||||
}
|
||||
|
||||
case "right":
|
||||
if x < len(l) {
|
||||
m.SetCursorX(x + 1)
|
||||
} else if y+1 < m.LineCount() {
|
||||
m.SetCursorX(0)
|
||||
m.SetCursorY(y + 1)
|
||||
}
|
||||
|
||||
// Regular character
|
||||
default:
|
||||
if x < len(l) {
|
||||
m.SetLine(y, l[:x]+key+l[x:])
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
package editor
|
||||
|
||||
import (
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
)
|
||||
|
||||
func (m Model) cursorStyle() lipgloss.Style {
|
||||
switch m.mode {
|
||||
case action.NormalMode,
|
||||
action.VisualMode,
|
||||
action.VisualBlockMode,
|
||||
action.VisualLineMode:
|
||||
case action.NormalMode:
|
||||
// Block cursor for normal mode
|
||||
return lipgloss.NewStyle().Reverse(true)
|
||||
case action.InsertMode:
|
||||
@ -23,25 +20,13 @@ func (m Model) cursorStyle() lipgloss.Style {
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUGGING STYLE
|
||||
func (m Model) visualAnchorStyle() lipgloss.Style {
|
||||
bg := lipgloss.Color("#a89020")
|
||||
return lipgloss.NewStyle().Background(bg)
|
||||
}
|
||||
|
||||
func (m Model) gutterStyle(currentLine bool) lipgloss.Style {
|
||||
bg := lipgloss.Color("236")
|
||||
fg := lipgloss.Color("243")
|
||||
if currentLine {
|
||||
fg = lipgloss.Color("#d69d00")
|
||||
}
|
||||
return lipgloss.NewStyle().
|
||||
Width(m.gutterSize).
|
||||
Background(bg).
|
||||
Width(m.s_gutter).
|
||||
Background(lipgloss.Color("236")).
|
||||
Foreground(fg)
|
||||
}
|
||||
|
||||
func (m Model) visualHighlightStyle() lipgloss.Style {
|
||||
bg := lipgloss.Color("#7a6a00")
|
||||
return lipgloss.NewStyle().Background(bg)
|
||||
}
|
||||
|
||||
@ -13,20 +13,36 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.win_w = msg.Width
|
||||
|
||||
case tea.KeyMsg:
|
||||
// BUG: for use in debugging, until we have command mode
|
||||
if msg.String() == "ctrl+c" {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
switch m.mode {
|
||||
case action.NormalMode,
|
||||
action.InsertMode,
|
||||
action.VisualMode,
|
||||
action.VisualBlockMode,
|
||||
action.VisualLineMode:
|
||||
case action.NormalMode:
|
||||
return m, m.input.Handle(&m, msg.String())
|
||||
|
||||
// The only one left to migrate!
|
||||
// TODO: This should be handled elsewhere
|
||||
case action.InsertMode:
|
||||
key := msg.String()
|
||||
|
||||
switch key {
|
||||
case "esc":
|
||||
if m.insertCount > 1 {
|
||||
m.replayInsert()
|
||||
}
|
||||
|
||||
// Allow i to step back, but a to stay put
|
||||
if m.cursor.x > 0 {
|
||||
m.cursor.x--
|
||||
}
|
||||
m.mode = action.NormalMode
|
||||
m.insertCount = 0
|
||||
m.insertKeys = nil
|
||||
|
||||
case "ctrl+c", "ctrl+d":
|
||||
return m, tea.Quit
|
||||
|
||||
default:
|
||||
// Record and process
|
||||
m.insertKeys = append(m.insertKeys, key)
|
||||
m.processInsertKey(key)
|
||||
}
|
||||
case action.CommandMode:
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
|
||||
@ -7,55 +7,6 @@ import (
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
)
|
||||
|
||||
func posInsideSelection(m Model, col, line int) bool {
|
||||
switch m.Mode() {
|
||||
case action.VisualLineMode:
|
||||
startY := min(m.AnchorY(), m.CursorY())
|
||||
endY := max(m.AnchorY(), m.CursorY())
|
||||
return line >= startY && line <= endY
|
||||
|
||||
case action.VisualMode:
|
||||
ax := m.AnchorX()
|
||||
ay := m.AnchorY()
|
||||
|
||||
cx := m.CursorX()
|
||||
cy := m.CursorY()
|
||||
|
||||
// Normalize so start is always before end in document order
|
||||
var startX, startY, endX, endY int
|
||||
if ay < cy || (ay == cy && ax <= cx) {
|
||||
startX, startY = ax, ay
|
||||
endX, endY = cx, cy
|
||||
} else {
|
||||
startX, startY = cx, cy
|
||||
endX, endY = ax, ay
|
||||
}
|
||||
|
||||
// Position is inside if it falls within [start, end] inclusive
|
||||
afterStart := line > startY || (line == startY && col >= startX)
|
||||
beforeEnd := line < endY || (line == endY && col <= endX)
|
||||
return afterStart && beforeEnd
|
||||
|
||||
case action.VisualBlockMode:
|
||||
startX := min(m.AnchorX(), m.CursorX())
|
||||
startY := min(m.AnchorY(), m.CursorY())
|
||||
endX := max(m.AnchorX(), m.CursorX())
|
||||
endY := max(m.AnchorY(), m.CursorY())
|
||||
|
||||
return col >= startX && col <= endX &&
|
||||
line >= startY && line <= endY
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func posIsAnchor(m Model, col, line int) bool {
|
||||
ax := m.AnchorX()
|
||||
ay := m.AnchorY()
|
||||
return col == ax && line == ay
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
var view strings.Builder
|
||||
|
||||
@ -70,17 +21,17 @@ func (m Model) View() string {
|
||||
)
|
||||
if y > m.cursor.y {
|
||||
lineNumber = y - m.cursor.y
|
||||
gutter = fmt.Sprintf("%*d ", m.gutterSize-1, lineNumber)
|
||||
gutter = fmt.Sprintf("%*d ", m.s_gutter-1, lineNumber)
|
||||
} else if y < m.cursor.y {
|
||||
lineNumber = m.cursor.y - y
|
||||
gutter = fmt.Sprintf("%*d ", m.gutterSize-1, lineNumber)
|
||||
gutter = fmt.Sprintf("%*d ", m.s_gutter-1, lineNumber)
|
||||
} else {
|
||||
lineNumber = y + 1
|
||||
currentLine = true
|
||||
if lineNumber < 100 {
|
||||
gutter = fmt.Sprintf("%*d ", m.gutterSize-2, lineNumber)
|
||||
gutter = fmt.Sprintf("%*d ", m.s_gutter-2, lineNumber)
|
||||
} else {
|
||||
gutter = fmt.Sprintf("%*d ", m.gutterSize-1, lineNumber)
|
||||
gutter = fmt.Sprintf("%*d ", m.s_gutter-1, lineNumber)
|
||||
}
|
||||
}
|
||||
view.WriteString(m.gutterStyle(currentLine).Render(gutter))
|
||||
@ -94,20 +45,11 @@ func (m Model) View() string {
|
||||
view.WriteString(m.cursorStyle().Render(" "))
|
||||
}
|
||||
} else if x < len(runes) {
|
||||
if m.IsVisualMode() && posIsAnchor(m, x, y) {
|
||||
view.WriteString(m.visualAnchorStyle().Render(string(runes[x])))
|
||||
} else if m.IsVisualMode() && posInsideSelection(m, x, y) {
|
||||
view.WriteString(m.visualHighlightStyle().Render(string(runes[x])))
|
||||
} else {
|
||||
view.WriteRune(runes[x])
|
||||
}
|
||||
// To highlight blank lines when in visual mode
|
||||
} else if m.IsVisualMode() && posInsideSelection(m, x, y) {
|
||||
view.WriteString(m.visualHighlightStyle().Render(" "))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
format := fmt.Sprintf("%%-%ds ", m.gutterSize-1)
|
||||
format := fmt.Sprintf("%%-%ds ", m.s_gutter-1)
|
||||
fmt.Fprintf(&view, format, "~")
|
||||
}
|
||||
|
||||
@ -123,20 +65,11 @@ func (m Model) View() string {
|
||||
modeString = "INSERT"
|
||||
case action.CommandMode:
|
||||
modeString = "COMMAND"
|
||||
case action.VisualMode:
|
||||
modeString = "VISUAL"
|
||||
case action.VisualLineMode:
|
||||
modeString = "V-LINE"
|
||||
case action.VisualBlockMode:
|
||||
modeString = "V-BLOCK"
|
||||
}
|
||||
|
||||
// DEBUG BAR! Def not the final bar
|
||||
var bar string
|
||||
if m.Mode() == action.CommandMode {
|
||||
bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) :%s ", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.command)
|
||||
} else if m.IsVisualMode() {
|
||||
bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) <%d, %d> ", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.AnchorX(), m.AnchorY())
|
||||
if m.mode == action.CommandMode {
|
||||
bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) %s ", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.command)
|
||||
} else {
|
||||
bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) | %s | %+v | %d", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.input.Pending(), m.insertKeys, m.insertCount)
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
)
|
||||
|
||||
type InputState int
|
||||
@ -27,22 +27,12 @@ type Handler struct {
|
||||
operatorKey string // track which key started operator (for dd, yy, cc)
|
||||
buffer string // for display (what user has typed)
|
||||
pending string // partial key sequence (e.g., "g" waiting for second key)
|
||||
|
||||
// Keymaps
|
||||
normalKeymap *Keymap
|
||||
visualKeymap *Keymap
|
||||
insertKeymap *Keymap
|
||||
|
||||
currentKeymap *Keymap
|
||||
keymap *Keymap
|
||||
}
|
||||
|
||||
func NewHandler() *Handler {
|
||||
return &Handler{
|
||||
// keymap: NewNormalKeymap(),
|
||||
normalKeymap: NewNormalKeymap(),
|
||||
visualKeymap: NewVisualKeymap(),
|
||||
insertKeymap: NewInsertKeymap(),
|
||||
currentKeymap: nil,
|
||||
keymap: NewNormalKeymap(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,19 +40,9 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
||||
// ESC always resets everything
|
||||
if key == "esc" {
|
||||
h.Reset()
|
||||
if m.Mode() == action.InsertMode {
|
||||
m.ExitInsertMode()
|
||||
} else {
|
||||
m.SetMode(action.NormalMode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Insert mode bypasses the normal state machine entirely
|
||||
if m.Mode() == action.InsertMode {
|
||||
return h.handleInsertKey(m, key)
|
||||
}
|
||||
|
||||
// Try to accumulate count (only if no pending sequence)
|
||||
if h.pending == "" && h.tryAccumulateCount(key) {
|
||||
return nil
|
||||
@ -71,18 +51,8 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
||||
// Build the sequence (pending + new key)
|
||||
sequence := h.pending + key
|
||||
|
||||
// Set working keymap
|
||||
switch m.Mode() {
|
||||
case action.NormalMode:
|
||||
h.currentKeymap = h.normalKeymap
|
||||
case action.VisualMode,
|
||||
action.VisualLineMode,
|
||||
action.VisualBlockMode:
|
||||
h.currentKeymap = h.visualKeymap
|
||||
}
|
||||
|
||||
// Check for exact match with full sequence
|
||||
kind, binding := h.currentKeymap.Lookup(sequence)
|
||||
kind, binding := h.keymap.Lookup(sequence)
|
||||
if kind != "" {
|
||||
h.pending = ""
|
||||
h.buffer += key
|
||||
@ -90,7 +60,7 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
||||
}
|
||||
|
||||
// No exact match - could this be a prefix of something?
|
||||
if h.currentKeymap.HasPrefix(sequence) {
|
||||
if h.keymap.HasPrefix(sequence) {
|
||||
h.pending = sequence
|
||||
h.buffer += key
|
||||
return nil // wait for more keys
|
||||
@ -99,7 +69,7 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
||||
// Not a prefix either - if we had pending, try just the new key
|
||||
if h.pending != "" {
|
||||
h.pending = ""
|
||||
kind, binding = h.currentKeymap.Lookup(key)
|
||||
kind, binding = h.keymap.Lookup(key)
|
||||
if kind != "" {
|
||||
h.buffer = key
|
||||
return h.dispatch(m, kind, binding, key)
|
||||
@ -137,17 +107,7 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
|
||||
return cmd
|
||||
|
||||
case "operator":
|
||||
op := binding.(action.Operator)
|
||||
// In visual mode, the selection is already defined — operate immediately
|
||||
if m.IsVisualMode() {
|
||||
start, end := normalizeVisualSelection(m)
|
||||
cmd := op.Operate(m, start, end)
|
||||
m.SetMode(action.NormalMode)
|
||||
h.Reset()
|
||||
return cmd
|
||||
}
|
||||
// In normal mode, wait for a motion to define the range
|
||||
h.operator = op
|
||||
h.operator = binding.(action.Operator)
|
||||
h.operatorKey = key
|
||||
h.state = StateOperatorPending
|
||||
return nil
|
||||
@ -171,7 +131,7 @@ func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any,
|
||||
|
||||
// dd, yy, cc - same operator key pressed twice
|
||||
if kind == "operator" && key == h.operatorKey {
|
||||
cmd := h.operator.DoublePress(m, count)
|
||||
cmd := h.operator.DoublePress(m)
|
||||
h.Reset()
|
||||
return cmd
|
||||
}
|
||||
@ -252,29 +212,3 @@ func (h *Handler) Reset() {
|
||||
func (h *Handler) Pending() string {
|
||||
return h.buffer
|
||||
}
|
||||
|
||||
func (h *Handler) handleInsertKey(m action.Model, key string) tea.Cmd {
|
||||
// Record the key for count replay (e.g. 5i...)
|
||||
m.SetInsertKeys(append(m.InsertKeys(), key))
|
||||
|
||||
// Check the insert keymap first
|
||||
kind, binding := h.insertKeymap.Lookup(key)
|
||||
switch kind {
|
||||
case "action":
|
||||
return binding.(action.Action).Execute(m)
|
||||
case "motion":
|
||||
return binding.(action.Motion).Execute(m)
|
||||
}
|
||||
|
||||
// Fallback: treat as a regular character to insert
|
||||
return action.InsertChar{Char: key}.Execute(m)
|
||||
}
|
||||
|
||||
func normalizeVisualSelection(m action.Model) (action.Position, action.Position) {
|
||||
a := action.Position{Line: m.AnchorY(), Col: m.AnchorX()}
|
||||
c := action.Position{Line: m.CursorY(), Col: m.CursorX()}
|
||||
if a.Line < c.Line || (a.Line == c.Line && a.Col <= c.Col) {
|
||||
return a, c
|
||||
}
|
||||
return c, a
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ package input
|
||||
import (
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/motion"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/operator"
|
||||
)
|
||||
|
||||
type Keymap struct {
|
||||
@ -20,21 +19,14 @@ func NewNormalKeymap() *Keymap {
|
||||
"h": motion.MoveLeft{Count: 1},
|
||||
"l": motion.MoveRight{Count: 1},
|
||||
"G": motion.MoveToBottom{},
|
||||
"gg": motion.MoveToTop{},
|
||||
"gg": motion.MoveToTop{}, // multi-key example
|
||||
"0": motion.MoveToLineStart{},
|
||||
"$": motion.MoveToLineEnd{},
|
||||
"_": motion.MoveToLineContentStart{},
|
||||
"w": motion.MoveForwardWord{Count: 1},
|
||||
"e": motion.MoveForwardWordEnd{Count: 1},
|
||||
"b": motion.MoveBackwardWord{Count: 1},
|
||||
},
|
||||
operators: map[string]action.Operator{
|
||||
"d": operator.DeleteOperator{},
|
||||
// "d": DeleteOp{},
|
||||
// "c": ChangeOp{},
|
||||
// "y": YankOp{},
|
||||
// "p": PasteOp{},
|
||||
// "s": SubstitueOp{},
|
||||
// "~": SwapCaseOp{},
|
||||
},
|
||||
actions: map[string]action.Action{
|
||||
"i": action.EnterInsert{},
|
||||
@ -45,67 +37,10 @@ func NewNormalKeymap() *Keymap {
|
||||
"O": action.OpenLineAbove{},
|
||||
"x": action.DeleteChar{Count: 1},
|
||||
"ctrl+c": action.Quit{},
|
||||
":": action.EnterComandMode{},
|
||||
"v": action.EnterVisualMode{},
|
||||
"V": action.EnterVisualLineMode{},
|
||||
"ctrl+v": action.EnterVisualBlockMode{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewVisualKeymap() *Keymap {
|
||||
return &Keymap{
|
||||
motions: map[string]action.Motion{
|
||||
"j": motion.MoveDown{Count: 1},
|
||||
"k": motion.MoveUp{Count: 1},
|
||||
"h": motion.MoveLeft{Count: 1},
|
||||
"l": motion.MoveRight{Count: 1},
|
||||
"G": motion.MoveToBottom{},
|
||||
"gg": motion.MoveToTop{},
|
||||
"0": motion.MoveToLineStart{},
|
||||
"$": motion.MoveToLineEnd{},
|
||||
"_": motion.MoveToLineContentStart{},
|
||||
"w": motion.MoveForwardWord{Count: 1},
|
||||
"e": motion.MoveForwardWordEnd{Count: 1},
|
||||
"b": motion.MoveBackwardWord{Count: 1},
|
||||
},
|
||||
operators: map[string]action.Operator{
|
||||
"d": operator.DeleteOperator{},
|
||||
"x": operator.DeleteOperator{},
|
||||
// "c": ChangeOp{},
|
||||
// "y": YankOp{},
|
||||
// "p": PasteOp{},
|
||||
// "s": SubstitueOp{},
|
||||
// "~": SwapCaseOp{},
|
||||
},
|
||||
actions: map[string]action.Action{
|
||||
"ctrl+c": action.Quit{},
|
||||
// ":": action.EnterComandMode{}, // Different OP
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewInsertKeymap() *Keymap {
|
||||
return &Keymap{
|
||||
motions: map[string]action.Motion{
|
||||
"down": motion.MoveDown{Count: 1},
|
||||
"up": motion.MoveUp{Count: 1},
|
||||
"left": motion.MoveLeft{Count: 1},
|
||||
"right": motion.MoveRight{Count: 1},
|
||||
},
|
||||
operators: map[string]action.Operator{}, // this will likely be empty
|
||||
actions: map[string]action.Action{
|
||||
"enter": action.InsertNewline{},
|
||||
"backspace": action.InsertBackspace{},
|
||||
"delete": action.InsertDelete{},
|
||||
"tab": action.InsertTab{},
|
||||
"ctrl+w": action.InsertDeletePreviousWord{},
|
||||
"ctrl+c": action.Quit{},
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Lookup returns what type of binding a key is
|
||||
func (km *Keymap) Lookup(key string) (kind string, value any) {
|
||||
if m, ok := km.motions[key]; ok {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package motion
|
||||
|
||||
import (
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
)
|
||||
|
||||
// MoveToTop implements Motion (gg)
|
||||
@ -40,26 +40,3 @@ func (a MoveToLineEnd) Execute(m action.Model) tea.Cmd {
|
||||
m.ClampCursorX()
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveToLineContentStart implements Motion (_)
|
||||
type MoveToLineContentStart struct{}
|
||||
|
||||
func (a MoveToLineContentStart) Execute(m action.Model) tea.Cmd {
|
||||
line := m.Line(m.CursorY())
|
||||
x := 0
|
||||
for x < len(line) {
|
||||
ch := line[x]
|
||||
if ch != ' ' && ch != '\t' {
|
||||
break
|
||||
}
|
||||
x++
|
||||
}
|
||||
|
||||
// If we are on the last char, we overflew, back once
|
||||
if x == len(line) && x > 0 {
|
||||
x--
|
||||
}
|
||||
|
||||
m.SetCursorX(x)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1,232 +0,0 @@
|
||||
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}
|
||||
}
|
||||
@ -1,108 +0,0 @@
|
||||
package operator
|
||||
|
||||
import (
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Implements Operator (x)
|
||||
type DeleteOperator struct{}
|
||||
|
||||
func (o DeleteOperator) Operate(m action.Model, start, end action.Position) tea.Cmd {
|
||||
switch m.Mode() {
|
||||
case action.VisualMode:
|
||||
deleteCharSelection(m, start, end)
|
||||
case action.VisualLineMode:
|
||||
deleteLineSelection(m, start, end)
|
||||
case action.VisualBlockMode:
|
||||
deleteBlockSelection(m, start, end)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Double press handles dd - delete the entire line
|
||||
func (o DeleteOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
||||
// If we have a higher value than lines remaining, we can only run so many times
|
||||
opCount := min(count, m.LineCount()-m.CursorY())
|
||||
|
||||
for range opCount {
|
||||
y := m.CursorY()
|
||||
m.DeleteLine(y)
|
||||
|
||||
if m.LineCount() == 0 {
|
||||
m.InsertLine(0, "")
|
||||
}
|
||||
|
||||
if y >= m.LineCount() {
|
||||
y = m.LineCount() - 1
|
||||
}
|
||||
|
||||
m.SetCursorY(y)
|
||||
m.ClampCursorX()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteCharSelection(m action.Model, start, end action.Position) {
|
||||
if start.Line == end.Line {
|
||||
line := m.Line(start.Line)
|
||||
endCol := min(end.Col+1, len(line))
|
||||
m.SetLine(start.Line, line[:start.Col]+line[endCol:])
|
||||
} else {
|
||||
startLine := m.Line(start.Line)
|
||||
endLine := m.Line(end.Line)
|
||||
|
||||
prefix := startLine[:start.Col]
|
||||
suffix := ""
|
||||
if end.Col+1 < len(endLine) {
|
||||
suffix = endLine[end.Col+1:]
|
||||
}
|
||||
|
||||
// Delete from end back to start to preserve indices
|
||||
for i := end.Line; i >= start.Line; i-- {
|
||||
m.DeleteLine(i)
|
||||
}
|
||||
m.InsertLine(start.Line, prefix+suffix)
|
||||
}
|
||||
|
||||
m.SetCursorY(start.Line)
|
||||
m.SetCursorX(start.Col)
|
||||
m.ClampCursorX()
|
||||
}
|
||||
|
||||
func deleteLineSelection(m action.Model, start, end action.Position) {
|
||||
for i := end.Line; i >= start.Line; i-- {
|
||||
m.DeleteLine(i)
|
||||
}
|
||||
|
||||
if m.LineCount() == 0 {
|
||||
m.InsertLine(0, "")
|
||||
}
|
||||
|
||||
y := start.Line
|
||||
if y >= m.LineCount() {
|
||||
y = m.LineCount() - 1
|
||||
}
|
||||
|
||||
m.SetCursorY(y)
|
||||
m.ClampCursorX()
|
||||
}
|
||||
|
||||
func deleteBlockSelection(m action.Model, start, end action.Position) {
|
||||
startCol := min(start.Col, end.Col)
|
||||
endCol := max(start.Col, end.Col)
|
||||
|
||||
for y := start.Line; y <= end.Line; y++ {
|
||||
line := m.Line(y)
|
||||
if startCol >= len(line) {
|
||||
continue
|
||||
}
|
||||
ec := min(endCol+1, len(line))
|
||||
m.SetLine(y, line[:startCol]+line[ec:])
|
||||
}
|
||||
|
||||
m.SetCursorY(start.Line)
|
||||
m.SetCursorX(startCol)
|
||||
m.ClampCursorX()
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user