Compare commits
No commits in common. "c2b5e6f67cafbd048449fbd9aa4a0e8d2f40fc60" and "c62bbc89eee58ac494ee56e0786d6ba2dbd380c1" have entirely different histories.
c2b5e6f67c
...
c62bbc89ee
@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
func main() {
|
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(
|
tea.NewProgram(
|
||||||
editor.NewModel(lines, action.Position{Line: 0, Col: 0}),
|
editor.NewModel(lines, action.Position{Line: 0, Col: 0}),
|
||||||
tea.WithAltScreen(),
|
tea.WithAltScreen(),
|
||||||
|
|||||||
@ -9,9 +9,6 @@ const (
|
|||||||
NormalMode Mode = iota
|
NormalMode Mode = iota
|
||||||
InsertMode
|
InsertMode
|
||||||
CommandMode
|
CommandMode
|
||||||
VisualMode
|
|
||||||
VisualLineMode
|
|
||||||
VisualBlockMode
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Model defines the interface for editor state that actions can modify
|
// Model defines the interface for editor state that actions can modify
|
||||||
@ -31,29 +28,12 @@ type Model interface {
|
|||||||
SetCursorY(y int)
|
SetCursorY(y int)
|
||||||
ClampCursorX()
|
ClampCursorX()
|
||||||
|
|
||||||
// Anchor
|
|
||||||
AnchorX() int
|
|
||||||
AnchorY() int
|
|
||||||
SetAnchorX(x int)
|
|
||||||
SetAnchorY(y int)
|
|
||||||
|
|
||||||
// Insert
|
|
||||||
InsertKeys() []string
|
|
||||||
SetInsertKeys(keys []string)
|
|
||||||
|
|
||||||
// Settings
|
|
||||||
TabSize() int
|
|
||||||
|
|
||||||
// Mode
|
// Mode
|
||||||
Mode() Mode
|
Mode() Mode
|
||||||
SetMode(mode Mode)
|
SetMode(mode Mode)
|
||||||
IsVisualMode() bool
|
|
||||||
|
|
||||||
// Insert recording (for count replay)
|
// Insert recording (for count replay)
|
||||||
SetInsertRecording(count int, action Action)
|
SetInsertRecording(count int, action Action)
|
||||||
|
|
||||||
// ExitInsertMode handles replay, cursor step-back, and mode transition on esc
|
|
||||||
ExitInsertMode()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position represents a location in the buffer
|
// Position represents a location in the buffer
|
||||||
@ -75,7 +55,7 @@ type Motion interface {
|
|||||||
type Operator interface {
|
type Operator interface {
|
||||||
Operate(m Model, start, end Position) tea.Cmd
|
Operate(m Model, start, end Position) tea.Cmd
|
||||||
// DoublePress handles dd, yy, cc (line-wise)
|
// DoublePress handles dd, yy, cc (line-wise)
|
||||||
DoublePress(m Model, count int) tea.Cmd
|
DoublePress(m Model) tea.Cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repeatable actions track count
|
// Repeatable actions track count
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
package action
|
package action
|
||||||
|
|
||||||
import (
|
import tea "github.com/charmbracelet/bubbletea"
|
||||||
"strings"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EnterInsert implements Action (i)
|
// EnterInsert implements Action (i)
|
||||||
type EnterInsert struct {
|
type EnterInsert struct {
|
||||||
@ -125,154 +121,3 @@ func (a OpenLineAbove) Execute(m Model) tea.Cmd {
|
|||||||
func (a OpenLineAbove) WithCount(n int) Action {
|
func (a OpenLineAbove) WithCount(n int) Action {
|
||||||
return OpenLineAbove{Count: n}
|
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 {
|
func (a Quit) Execute(m Model) tea.Cmd {
|
||||||
return tea.Quit
|
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})
|
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlC})
|
||||||
case "ctrl+d":
|
case "ctrl+d":
|
||||||
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlD})
|
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:
|
default:
|
||||||
tm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(key)})
|
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))
|
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))
|
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) {
|
t.Run("test 'x' in middle of line", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
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")
|
sendKeys(tm, "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
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) {
|
t.Run("test 'x' at end of line", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
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")
|
sendKeys(tm, "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
@ -77,7 +77,7 @@ func TestDeleteCharWithCount(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("test '2x' from middle", func(t *testing.T) {
|
t.Run("test '2x' from middle", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
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")
|
sendKeys(tm, "2", "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
|
|||||||
@ -32,7 +32,7 @@ func TestEnterInsert(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("test 'i' insert in middle", func(t *testing.T) {
|
t.Run("test 'i' insert in middle", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
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")
|
sendKeys(tm, "i", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
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) {
|
t.Run("test 'i' cursor moves back on esc", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
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")
|
sendKeys(tm, "i", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
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) {
|
t.Run("test 'a' from middle of line", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
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")
|
sendKeys(tm, "a", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
@ -90,7 +90,7 @@ func TestEnterInsertAfter(t *testing.T) {
|
|||||||
func TestEnterInsertLineStart(t *testing.T) {
|
func TestEnterInsertLineStart(t *testing.T) {
|
||||||
t.Run("test 'I' enters insert mode at line start", func(t *testing.T) {
|
t.Run("test 'I' enters insert mode at line start", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
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")
|
sendKeys(tm, "I", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
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) {
|
t.Run("test 'I' from end of line", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
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")
|
sendKeys(tm, "I", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
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) {
|
t.Run("test 'A' from middle of line", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
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")
|
sendKeys(tm, "A", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
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) {
|
t.Run("test 'o' from middle of file", func(t *testing.T) {
|
||||||
lines := []string{"line 1", "line 2", "line 3"}
|
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")
|
sendKeys(tm, "o", "n", "e", "w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
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) {
|
t.Run("test 'o' at end of file", func(t *testing.T) {
|
||||||
lines := []string{"line 1", "line 2"}
|
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")
|
sendKeys(tm, "o", "n", "e", "w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
@ -233,7 +233,7 @@ func TestOpenLineBelowWithCount(t *testing.T) {
|
|||||||
func TestOpenLineAbove(t *testing.T) {
|
func TestOpenLineAbove(t *testing.T) {
|
||||||
t.Run("test 'O' creates line above", func(t *testing.T) {
|
t.Run("test 'O' creates line above", func(t *testing.T) {
|
||||||
lines := []string{"line 1", "line 2"}
|
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")
|
sendKeys(tm, "O", "n", "e", "w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
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) {
|
t.Run("test 'O' cursor at start of new line", func(t *testing.T) {
|
||||||
lines := []string{"line 1", "line 2"}
|
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")
|
sendKeys(tm, "O", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
@ -274,7 +274,7 @@ func TestOpenLineAbove(t *testing.T) {
|
|||||||
func TestOpenLineAboveWithCount(t *testing.T) {
|
func TestOpenLineAboveWithCount(t *testing.T) {
|
||||||
t.Run("test '3O' creates 3 lines above", func(t *testing.T) {
|
t.Run("test '3O' creates 3 lines above", func(t *testing.T) {
|
||||||
lines := []string{"line 1"}
|
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")
|
sendKeys(tm, "3", "O", "x", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
@ -294,7 +294,7 @@ func TestOpenLineAboveWithCount(t *testing.T) {
|
|||||||
func TestInsertModeEnter(t *testing.T) {
|
func TestInsertModeEnter(t *testing.T) {
|
||||||
t.Run("test enter splits line", func(t *testing.T) {
|
t.Run("test enter splits line", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
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")
|
sendKeys(tm, "i", "enter", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
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) {
|
t.Run("test enter at end of line", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
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")
|
sendKeys(tm, "i", "enter", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
@ -347,7 +347,7 @@ func TestInsertModeEnter(t *testing.T) {
|
|||||||
func TestInsertModeBackspace(t *testing.T) {
|
func TestInsertModeBackspace(t *testing.T) {
|
||||||
t.Run("test backspace deletes character", func(t *testing.T) {
|
t.Run("test backspace deletes character", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
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")
|
sendKeys(tm, "i", "backspace", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
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) {
|
t.Run("test backspace at start of line joins lines", func(t *testing.T) {
|
||||||
lines := []string{"hello", "world"}
|
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")
|
sendKeys(tm, "i", "backspace", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
@ -383,7 +383,7 @@ func TestInsertModeBackspace(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("test multiple backspaces", func(t *testing.T) {
|
t.Run("test multiple backspaces", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
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")
|
sendKeys(tm, "i", "backspace", "backspace", "backspace", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
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"}
|
lines := []string{"long line", "small"}
|
||||||
|
|
||||||
t.Run("test 'j' with overflow", func(t *testing.T) {
|
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")
|
sendKeys(tm, "j")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
@ -75,7 +75,7 @@ func TestMoveDownWithOverflow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'j' without overflow", func(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")
|
sendKeys(tm, "j")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
@ -143,7 +143,7 @@ func TestMoveUpWithOverflow(t *testing.T) {
|
|||||||
lines := []string{"small", "long line"}
|
lines := []string{"small", "long line"}
|
||||||
|
|
||||||
t.Run("test 'k' with overflow", func(t *testing.T) {
|
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")
|
sendKeys(tm, "k")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
@ -154,7 +154,7 @@ func TestMoveUpWithOverflow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'k' without overflow", func(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")
|
sendKeys(tm, "k")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
|
|||||||
@ -14,8 +14,8 @@ func TestMoveToBottom(t *testing.T) {
|
|||||||
sendKeys(tm, "G")
|
sendKeys(tm, "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 5 {
|
if m.cursor.y != 5 {
|
||||||
t.Errorf("CursorY() = %d, want 5", m.CursorY())
|
t.Errorf("cursor.y = %d, want 5", m.cursor.y)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -24,8 +24,8 @@ func TestMoveToBottom(t *testing.T) {
|
|||||||
sendKeys(tm, "G")
|
sendKeys(tm, "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 5 {
|
if m.cursor.y != 5 {
|
||||||
t.Errorf("CursorY() = %d, want 5", m.CursorY())
|
t.Errorf("cursor.y = %d, want 5", m.cursor.y)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -34,23 +34,23 @@ func TestMoveToBottom(t *testing.T) {
|
|||||||
sendKeys(tm, "G")
|
sendKeys(tm, "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 5 {
|
if m.cursor.y != 5 {
|
||||||
t.Errorf("CursorY() = %d, want 5", m.CursorY())
|
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"}
|
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")
|
sendKeys(tm, "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 1 {
|
if m.cursor.y != 1 {
|
||||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
t.Errorf("cursor.y = %d, want 1", m.cursor.y)
|
||||||
}
|
}
|
||||||
want := len(lines[1])
|
want := len(lines[1])
|
||||||
if m.CursorX() != want {
|
if m.cursor.x != want {
|
||||||
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
|
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -60,8 +60,8 @@ func TestMoveToBottom(t *testing.T) {
|
|||||||
sendKeys(tm, "G")
|
sendKeys(tm, "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 0 {
|
if m.cursor.y != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -72,8 +72,8 @@ func TestMoveToTop(t *testing.T) {
|
|||||||
sendKeys(tm, "g", "g")
|
sendKeys(tm, "g", "g")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 0 {
|
if m.cursor.y != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -82,8 +82,8 @@ func TestMoveToTop(t *testing.T) {
|
|||||||
sendKeys(tm, "g", "g")
|
sendKeys(tm, "g", "g")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 0 {
|
if m.cursor.y != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -92,28 +92,28 @@ func TestMoveToTop(t *testing.T) {
|
|||||||
sendKeys(tm, "g", "g")
|
sendKeys(tm, "g", "g")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 0 {
|
if m.cursor.y != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
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"}
|
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")
|
sendKeys(tm, "g", "g")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 0 {
|
if m.cursor.y != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
|
||||||
}
|
}
|
||||||
want := len(lines[0])
|
want := len(lines[0])
|
||||||
if m.CursorX() != want {
|
if m.cursor.x != want {
|
||||||
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
|
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 0, $ and _ Tests ---
|
// --- 0 and $ Tests ---
|
||||||
|
|
||||||
func TestMoveToLineStart(t *testing.T) {
|
func TestMoveToLineStart(t *testing.T) {
|
||||||
t.Run("test '0' from middle of line", func(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")
|
sendKeys(tm, "0")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.cursor.x != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '0' from end of line", func(t *testing.T) {
|
t.Run("test '0' from end of line", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
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")
|
sendKeys(tm, "0")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.cursor.x != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -142,8 +142,8 @@ func TestMoveToLineStart(t *testing.T) {
|
|||||||
sendKeys(tm, "0")
|
sendKeys(tm, "0")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.cursor.x != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -153,8 +153,8 @@ func TestMoveToLineStart(t *testing.T) {
|
|||||||
sendKeys(tm, "0")
|
sendKeys(tm, "0")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.cursor.x != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -163,8 +163,8 @@ func TestMoveToLineStart(t *testing.T) {
|
|||||||
sendKeys(tm, "0")
|
sendKeys(tm, "0")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 2 {
|
if m.cursor.y != 2 {
|
||||||
t.Errorf("CursorY() = %d, want 2", m.CursorY())
|
t.Errorf("cursor.y = %d, want 2", m.cursor.y)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -177,32 +177,32 @@ func TestMoveToLineEnd(t *testing.T) {
|
|||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
want := len(lines[0])
|
want := len(lines[0])
|
||||||
if m.CursorX() != want {
|
if m.cursor.x != want {
|
||||||
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
|
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '$' from middle of line", func(t *testing.T) {
|
t.Run("test '$' from middle of line", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
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, "$")
|
sendKeys(tm, "$")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
want := len(lines[0])
|
want := len(lines[0])
|
||||||
if m.CursorX() != want {
|
if m.cursor.x != want {
|
||||||
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
|
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '$' already at end", func(t *testing.T) {
|
t.Run("test '$' already at end", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
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, "$")
|
sendKeys(tm, "$")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
want := len(lines[0])
|
want := len(lines[0])
|
||||||
if m.CursorX() != want {
|
if m.cursor.x != want {
|
||||||
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
|
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -212,8 +212,8 @@ func TestMoveToLineEnd(t *testing.T) {
|
|||||||
sendKeys(tm, "$")
|
sendKeys(tm, "$")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.cursor.x != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -222,87 +222,8 @@ func TestMoveToLineEnd(t *testing.T) {
|
|||||||
sendKeys(tm, "$")
|
sendKeys(tm, "$")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 2 {
|
if m.cursor.y != 2 {
|
||||||
t.Errorf("CursorY() = %d, want 2", m.CursorY())
|
t.Errorf("cursor.y = %d, want 2", m.cursor.y)
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
package editor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/input"
|
"git.gophernest.net/azpect/TextEditor/internal/input"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
@ -14,23 +12,19 @@ type cursor struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
lines []string
|
lines []string
|
||||||
cursor cursor
|
cursor cursor
|
||||||
anchor cursor // starting point for visual modes
|
s_gutter int
|
||||||
mode action.Mode
|
mode action.Mode
|
||||||
win_h int
|
win_h int
|
||||||
win_w int
|
win_w int
|
||||||
command string
|
command string
|
||||||
input *input.Handler
|
input *input.Handler
|
||||||
|
|
||||||
// Insert repetition
|
// Insert repetition
|
||||||
insertCount int
|
insertCount int
|
||||||
insertKeys []string
|
insertKeys []string
|
||||||
insertAction action.Action
|
insertAction action.Action
|
||||||
|
|
||||||
// Settings
|
|
||||||
gutterSize int
|
|
||||||
tabSize int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel(lines []string, pos action.Position) Model {
|
func NewModel(lines []string, pos action.Position) Model {
|
||||||
@ -40,11 +34,10 @@ func NewModel(lines []string, pos action.Position) Model {
|
|||||||
x: pos.Col,
|
x: pos.Col,
|
||||||
y: pos.Line,
|
y: pos.Line,
|
||||||
},
|
},
|
||||||
gutterSize: 5,
|
s_gutter: 5,
|
||||||
tabSize: 2,
|
mode: action.NormalMode,
|
||||||
mode: action.NormalMode,
|
command: "",
|
||||||
command: "",
|
input: input.NewHandler(),
|
||||||
input: input.NewHandler(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,37 +100,6 @@ func (m *Model) SetCursorY(y int) {
|
|||||||
m.cursor.y = y
|
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() {
|
func (m *Model) ClampCursorX() {
|
||||||
lineLen := len(m.lines[m.cursor.y])
|
lineLen := len(m.lines[m.cursor.y])
|
||||||
if lineLen == 0 {
|
if lineLen == 0 {
|
||||||
@ -155,12 +117,6 @@ func (m *Model) SetMode(mode action.Mode) {
|
|||||||
m.mode = 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) {
|
func (m *Model) SetInsertRecording(count int, act action.Action) {
|
||||||
m.insertCount = count
|
m.insertCount = count
|
||||||
m.insertKeys = []string{}
|
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) {
|
func (m *Model) processInsertKey(key string) {
|
||||||
x := m.CursorX()
|
x := m.CursorX()
|
||||||
y := m.CursorY()
|
y := m.CursorY()
|
||||||
@ -214,12 +158,17 @@ func (m *Model) processInsertKey(key string) {
|
|||||||
|
|
||||||
switch key {
|
switch key {
|
||||||
case "enter":
|
case "enter":
|
||||||
|
|
||||||
|
// Simple case, at end, just create a line
|
||||||
if x == len(l) {
|
if x == len(l) {
|
||||||
m.InsertLine(y+1, "")
|
m.InsertLine(y+1, "")
|
||||||
|
|
||||||
|
// otherwise, splice
|
||||||
} else {
|
} else {
|
||||||
m.SetLine(y, l[:x])
|
m.SetLine(y, l[:x])
|
||||||
m.InsertLine(y+1, l[x:])
|
m.InsertLine(y+1, l[x:])
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetCursorY(y + 1)
|
m.SetCursorY(y + 1)
|
||||||
m.SetCursorX(0)
|
m.SetCursorX(0)
|
||||||
|
|
||||||
@ -236,53 +185,7 @@ func (m *Model) processInsertKey(key string) {
|
|||||||
m.SetCursorX(newX)
|
m.SetCursorX(newX)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "delete":
|
// Regular character
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if x < len(l) {
|
if x < len(l) {
|
||||||
m.SetLine(y, l[:x]+key+l[x:])
|
m.SetLine(y, l[:x]+key+l[x:])
|
||||||
|
|||||||
@ -1,16 +1,13 @@
|
|||||||
package editor
|
package editor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m Model) cursorStyle() lipgloss.Style {
|
func (m Model) cursorStyle() lipgloss.Style {
|
||||||
switch m.mode {
|
switch m.mode {
|
||||||
case action.NormalMode,
|
case action.NormalMode:
|
||||||
action.VisualMode,
|
|
||||||
action.VisualBlockMode,
|
|
||||||
action.VisualLineMode:
|
|
||||||
// Block cursor for normal mode
|
// Block cursor for normal mode
|
||||||
return lipgloss.NewStyle().Reverse(true)
|
return lipgloss.NewStyle().Reverse(true)
|
||||||
case action.InsertMode:
|
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 {
|
func (m Model) gutterStyle(currentLine bool) lipgloss.Style {
|
||||||
bg := lipgloss.Color("236")
|
|
||||||
fg := lipgloss.Color("243")
|
fg := lipgloss.Color("243")
|
||||||
if currentLine {
|
if currentLine {
|
||||||
fg = lipgloss.Color("#d69d00")
|
fg = lipgloss.Color("#d69d00")
|
||||||
}
|
}
|
||||||
return lipgloss.NewStyle().
|
return lipgloss.NewStyle().
|
||||||
Width(m.gutterSize).
|
Width(m.s_gutter).
|
||||||
Background(bg).
|
Background(lipgloss.Color("236")).
|
||||||
Foreground(fg)
|
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
|
m.win_w = msg.Width
|
||||||
|
|
||||||
case tea.KeyMsg:
|
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 {
|
switch m.mode {
|
||||||
case action.NormalMode,
|
case action.NormalMode:
|
||||||
action.InsertMode,
|
|
||||||
action.VisualMode,
|
|
||||||
action.VisualBlockMode,
|
|
||||||
action.VisualLineMode:
|
|
||||||
return m, m.input.Handle(&m, msg.String())
|
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:
|
case action.CommandMode:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "esc":
|
case "esc":
|
||||||
|
|||||||
@ -7,55 +7,6 @@ import (
|
|||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"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 {
|
func (m Model) View() string {
|
||||||
var view strings.Builder
|
var view strings.Builder
|
||||||
|
|
||||||
@ -70,17 +21,17 @@ func (m Model) View() string {
|
|||||||
)
|
)
|
||||||
if y > m.cursor.y {
|
if y > m.cursor.y {
|
||||||
lineNumber = 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 {
|
} else if y < m.cursor.y {
|
||||||
lineNumber = m.cursor.y - y
|
lineNumber = m.cursor.y - y
|
||||||
gutter = fmt.Sprintf("%*d ", m.gutterSize-1, lineNumber)
|
gutter = fmt.Sprintf("%*d ", m.s_gutter-1, lineNumber)
|
||||||
} else {
|
} else {
|
||||||
lineNumber = y + 1
|
lineNumber = y + 1
|
||||||
currentLine = true
|
currentLine = true
|
||||||
if lineNumber < 100 {
|
if lineNumber < 100 {
|
||||||
gutter = fmt.Sprintf("%*d ", m.gutterSize-2, lineNumber)
|
gutter = fmt.Sprintf("%*d ", m.s_gutter-2, lineNumber)
|
||||||
} else {
|
} 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))
|
view.WriteString(m.gutterStyle(currentLine).Render(gutter))
|
||||||
@ -94,20 +45,11 @@ func (m Model) View() string {
|
|||||||
view.WriteString(m.cursorStyle().Render(" "))
|
view.WriteString(m.cursorStyle().Render(" "))
|
||||||
}
|
}
|
||||||
} else if x < len(runes) {
|
} else if x < len(runes) {
|
||||||
if m.IsVisualMode() && posIsAnchor(m, x, y) {
|
view.WriteRune(runes[x])
|
||||||
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 {
|
} else {
|
||||||
format := fmt.Sprintf("%%-%ds ", m.gutterSize-1)
|
format := fmt.Sprintf("%%-%ds ", m.s_gutter-1)
|
||||||
fmt.Fprintf(&view, format, "~")
|
fmt.Fprintf(&view, format, "~")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,20 +65,11 @@ func (m Model) View() string {
|
|||||||
modeString = "INSERT"
|
modeString = "INSERT"
|
||||||
case action.CommandMode:
|
case action.CommandMode:
|
||||||
modeString = "COMMAND"
|
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
|
var bar string
|
||||||
if m.Mode() == action.CommandMode {
|
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)
|
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())
|
|
||||||
} else {
|
} 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)
|
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
|
package input
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InputState int
|
type InputState int
|
||||||
@ -27,22 +27,12 @@ type Handler struct {
|
|||||||
operatorKey string // track which key started operator (for dd, yy, cc)
|
operatorKey string // track which key started operator (for dd, yy, cc)
|
||||||
buffer string // for display (what user has typed)
|
buffer string // for display (what user has typed)
|
||||||
pending string // partial key sequence (e.g., "g" waiting for second key)
|
pending string // partial key sequence (e.g., "g" waiting for second key)
|
||||||
|
keymap *Keymap
|
||||||
// Keymaps
|
|
||||||
normalKeymap *Keymap
|
|
||||||
visualKeymap *Keymap
|
|
||||||
insertKeymap *Keymap
|
|
||||||
|
|
||||||
currentKeymap *Keymap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler() *Handler {
|
func NewHandler() *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
// keymap: NewNormalKeymap(),
|
keymap: NewNormalKeymap(),
|
||||||
normalKeymap: NewNormalKeymap(),
|
|
||||||
visualKeymap: NewVisualKeymap(),
|
|
||||||
insertKeymap: NewInsertKeymap(),
|
|
||||||
currentKeymap: nil,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,19 +40,9 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
|||||||
// ESC always resets everything
|
// ESC always resets everything
|
||||||
if key == "esc" {
|
if key == "esc" {
|
||||||
h.Reset()
|
h.Reset()
|
||||||
if m.Mode() == action.InsertMode {
|
|
||||||
m.ExitInsertMode()
|
|
||||||
} else {
|
|
||||||
m.SetMode(action.NormalMode)
|
|
||||||
}
|
|
||||||
return nil
|
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)
|
// Try to accumulate count (only if no pending sequence)
|
||||||
if h.pending == "" && h.tryAccumulateCount(key) {
|
if h.pending == "" && h.tryAccumulateCount(key) {
|
||||||
return nil
|
return nil
|
||||||
@ -71,18 +51,8 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
|||||||
// Build the sequence (pending + new key)
|
// Build the sequence (pending + new key)
|
||||||
sequence := h.pending + 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
|
// Check for exact match with full sequence
|
||||||
kind, binding := h.currentKeymap.Lookup(sequence)
|
kind, binding := h.keymap.Lookup(sequence)
|
||||||
if kind != "" {
|
if kind != "" {
|
||||||
h.pending = ""
|
h.pending = ""
|
||||||
h.buffer += key
|
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?
|
// No exact match - could this be a prefix of something?
|
||||||
if h.currentKeymap.HasPrefix(sequence) {
|
if h.keymap.HasPrefix(sequence) {
|
||||||
h.pending = sequence
|
h.pending = sequence
|
||||||
h.buffer += key
|
h.buffer += key
|
||||||
return nil // wait for more keys
|
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
|
// Not a prefix either - if we had pending, try just the new key
|
||||||
if h.pending != "" {
|
if h.pending != "" {
|
||||||
h.pending = ""
|
h.pending = ""
|
||||||
kind, binding = h.currentKeymap.Lookup(key)
|
kind, binding = h.keymap.Lookup(key)
|
||||||
if kind != "" {
|
if kind != "" {
|
||||||
h.buffer = key
|
h.buffer = key
|
||||||
return h.dispatch(m, kind, binding, 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
|
return cmd
|
||||||
|
|
||||||
case "operator":
|
case "operator":
|
||||||
op := binding.(action.Operator)
|
h.operator = 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.operatorKey = key
|
h.operatorKey = key
|
||||||
h.state = StateOperatorPending
|
h.state = StateOperatorPending
|
||||||
return nil
|
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
|
// dd, yy, cc - same operator key pressed twice
|
||||||
if kind == "operator" && key == h.operatorKey {
|
if kind == "operator" && key == h.operatorKey {
|
||||||
cmd := h.operator.DoublePress(m, count)
|
cmd := h.operator.DoublePress(m)
|
||||||
h.Reset()
|
h.Reset()
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@ -252,29 +212,3 @@ func (h *Handler) Reset() {
|
|||||||
func (h *Handler) Pending() string {
|
func (h *Handler) Pending() string {
|
||||||
return h.buffer
|
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 (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/motion"
|
"git.gophernest.net/azpect/TextEditor/internal/motion"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/operator"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Keymap struct {
|
type Keymap struct {
|
||||||
@ -20,21 +19,14 @@ func NewNormalKeymap() *Keymap {
|
|||||||
"h": motion.MoveLeft{Count: 1},
|
"h": motion.MoveLeft{Count: 1},
|
||||||
"l": motion.MoveRight{Count: 1},
|
"l": motion.MoveRight{Count: 1},
|
||||||
"G": motion.MoveToBottom{},
|
"G": motion.MoveToBottom{},
|
||||||
"gg": motion.MoveToTop{},
|
"gg": motion.MoveToTop{}, // multi-key example
|
||||||
"0": motion.MoveToLineStart{},
|
"0": motion.MoveToLineStart{},
|
||||||
"$": motion.MoveToLineEnd{},
|
"$": motion.MoveToLineEnd{},
|
||||||
"_": motion.MoveToLineContentStart{},
|
|
||||||
"w": motion.MoveForwardWord{Count: 1},
|
|
||||||
"e": motion.MoveForwardWordEnd{Count: 1},
|
|
||||||
"b": motion.MoveBackwardWord{Count: 1},
|
|
||||||
},
|
},
|
||||||
operators: map[string]action.Operator{
|
operators: map[string]action.Operator{
|
||||||
"d": operator.DeleteOperator{},
|
// "d": DeleteOp{},
|
||||||
// "c": ChangeOp{},
|
// "c": ChangeOp{},
|
||||||
// "y": YankOp{},
|
// "y": YankOp{},
|
||||||
// "p": PasteOp{},
|
|
||||||
// "s": SubstitueOp{},
|
|
||||||
// "~": SwapCaseOp{},
|
|
||||||
},
|
},
|
||||||
actions: map[string]action.Action{
|
actions: map[string]action.Action{
|
||||||
"i": action.EnterInsert{},
|
"i": action.EnterInsert{},
|
||||||
@ -45,67 +37,10 @@ func NewNormalKeymap() *Keymap {
|
|||||||
"O": action.OpenLineAbove{},
|
"O": action.OpenLineAbove{},
|
||||||
"x": action.DeleteChar{Count: 1},
|
"x": action.DeleteChar{Count: 1},
|
||||||
"ctrl+c": action.Quit{},
|
"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
|
// Lookup returns what type of binding a key is
|
||||||
func (km *Keymap) Lookup(key string) (kind string, value any) {
|
func (km *Keymap) Lookup(key string) (kind string, value any) {
|
||||||
if m, ok := km.motions[key]; ok {
|
if m, ok := km.motions[key]; ok {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
package motion
|
package motion
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MoveToTop implements Motion (gg)
|
// MoveToTop implements Motion (gg)
|
||||||
@ -40,26 +40,3 @@ func (a MoveToLineEnd) Execute(m action.Model) tea.Cmd {
|
|||||||
m.ClampCursorX()
|
m.ClampCursorX()
|
||||||
return nil
|
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