Compare commits

..

No commits in common. "master" and "feature/text-objs" have entirely different histories.

60 changed files with 1379 additions and 7649 deletions

View File

@ -14,8 +14,8 @@
- [x] `b` - Backward to start of word
- [x] `W` - Forward to start of WORD (whitespace-delimited)
- [x] `E` - Forward to end of WORD
- [x] `B` - Backward to start of WORD
- [x] `ge` - Backward to end of word
- [ ] `B` - Backward to start of WORD
- [ ] `ge` - Backward to end of word
### Line Movement
- [x] `0` - Move to start of line
@ -34,8 +34,8 @@
### Scroll
- [x] `ctrl+u` - Scroll up half page
- [x] `ctrl+d` - Scroll down half page
- [x] `ctrl+b` - Scroll up full page
- [x] `ctrl+f` - Scroll down full page
- [ ] `ctrl+b` - Scroll up full page
- [ ] `ctrl+f` - Scroll down full page
- [ ] `ctrl+y` - Scroll up one line
- [ ] `ctrl+e` - Scroll down one line
- [ ] `zz` - Center cursor on screen
@ -127,14 +127,14 @@
- [ ] Last search register (`/`)
### Undo/Redo
- [x] `u` - Undo
- [x] `ctrl+r` - Redo
- [x] `.` - Repeat last change
- [ ] `u` - Undo
- [ ] `ctrl+r` - Redo
- [ ] `.` - Repeat last change
- [ ] `U` - Undo all changes on line
### Other Normal Mode
- [x] `r{char}` - Replace character
- [x] `R` - Replace mode
- [ ] `r{char}` - Replace character
- [ ] `R` - Replace mode
- [ ] `~` - Swap case of character
- [ ] `ctrl+a` - Increment number
- [ ] `ctrl+x` - Decrement number
@ -219,7 +219,7 @@
- [x] `:q!` - Force quit
- [x] `:e {file}` - Edit file
- [x] `:bn` / `:bp` - Next/previous buffer
- [x] `:{range}` - Go to line
- [ ] `:{range}` - Go to line
- [ ] `:%s/old/new/g` - Search and replace
- [ ] `:!{cmd}` - Run shell command
- [ ] `:help` - Show help
@ -228,20 +228,18 @@
## Text Objects
### Implemented
- [x] `iw` / `aw` - Inner/around word
- [x] `iW` / `aW` - Inner/around WORD
- [x] `is` / `as` - Inner/around sentence
- [x] `ip` / `ap` - Inner/around paragraph
- [x] `i"` / `a"` - Inner/around double quotes
- [x] `i'` / `a'` - Inner/around single quotes
- [x] `` i` `` / `` a` `` - Inner/around backticks
- [x] `i(` / `a(` - Inner/around parentheses
- [x] `i[` / `a[` - Inner/around brackets
- [x] `i{` / `a{` - Inner/around braces
- [x] `i<` / `a<` - Inner/around angle brackets
### Not Implemented
- [ ] `iw` / `aw` - Inner/around word
- [ ] `iW` / `aW` - Inner/around WORD
- [ ] `is` / `as` - Inner/around sentence
- [ ] `ip` / `ap` - Inner/around paragraph
- [ ] `i"` / `a"` - Inner/around double quotes
- [ ] `i'` / `a'` - Inner/around single quotes
- [ ] `` i` `` / `` a` `` - Inner/around backticks
- [ ] `i(` / `a(` - Inner/around parentheses
- [ ] `i[` / `a[` - Inner/around brackets
- [ ] `i{` / `a{` - Inner/around braces
- [ ] `i<` / `a<` - Inner/around angle brackets
- [ ] `it` / `at` - Inner/around tag
---
@ -373,8 +371,7 @@ Buffers are in-memory representations of files. A buffer exists for each open fi
### Display
- [x] Line numbers
- [x] Cursor position tracking
- [x] Viewport/scrolling (Y)
- [ ] Viewport/scrolling (X)
- [x] Viewport/scrolling
- [x] ScrollOff setting
- [x] Relative line numbers
- [ ] Cursor line highlight
@ -408,3 +405,63 @@ Buffers are in-memory representations of files. A buffer exists for each open fi
- [ ] Spell check
---
### Well Tested - Editor Core
#### Command Execution (179 tests)
- [x] Command parsing and validation
- [x] Command lookup and prefix matching
- [x] Force flag handling (!)
- [x] Write commands (`:w`, `:w {file}`, `:w!`)
- [x] Write all commands (`:wa`, `:wall`, `:wa!`)
- [x] Quit commands (`:q`, `:q!`, `:qa`, `:qa!`)
- [x] Write-quit commands (`:wq`, `:wq!`, `:wqa`, `:wqa!`)
- [x] Edit command (`:e {file}`)
- [x] Register display (`:register`, `:reg {name}`)
- [x] Set commands (`:set number`, `:set tabstop=N`, etc.)
- [x] Setting lookup and validation
- [x] Buffer-level readonly protection
- [x] Scratch buffer write protection
- [x] Force write bypassing readonly/scratch checks
- [x] Multiple buffer write operations
- [x] File write error handling (permissions, paths)
- [x] Modified buffer tracking
- [x] Unicode filename and content handling
- [x] Edge cases (empty args, long filenames, special chars)
#### Program Initialization (70 tests)
- [x] Empty program creation
- [x] File program with nonexistent files (new file buffers)
- [x] File program with existing files (content loading)
- [x] Line ending handling (Unix `\n`, Windows `\r\n`, mixed)
- [x] Tab to space conversion based on TabStop
- [x] Unicode content preservation (CJK, emoji)
- [x] File extension and type detection
- [x] Buffer state initialization (flags, metadata)
- [x] Large file handling (10,000+ lines)
- [x] Long line handling (10,000+ chars)
- [x] Empty file handling
- [x] Builder pattern method chaining
- [x] Program option accumulation
- [x] Model state defaults (settings, registers, mode)
- [x] Error handling (permissions, invalid paths)
- [x] Integration workflows (end-to-end)
- [x] Edge cases (empty filenames, relative paths, dot files)
### Moderately Tested
- [x] Basic motions (h, j, k, l)
- [x] Word motions (w, e, b)
- [x] Jump motions (G, gg, 0, $, _, ^, |)
- [x] Scroll actions (ctrl+u, ctrl+d)
- [x] Delete operator (d, dd)
- [x] Yank operator (y, yy)
- [x] Paste actions (p, P)
- [x] Change operator (c, cc, C)
- [x] Substitute action (s, S)
- [x] Insert mode entry (i, a, I, A, o, O)
- [x] Insert mode editing (enter, backspace, delete, tab, ctrl+w)
- [x] Visual modes (v, V, ctrl+v)
- [x] Visual mode with motions
- [x] Delete actions (x, D)
- [x] Register behavior

View File

@ -64,28 +64,6 @@ While the undo tree method that vim uses is powerful, I rarely find myself using
approach is more natural to "non-vim" users and much simpler to implement. Implementing a feature similar
to Vims undo tree would many times longer than a simple stack.
#### Vim-like Replace vs. Custom Replace
The way that vim's replace mode is implemented is quite complex, keeping track of the previous
line backspace can only delete newly replaced characters. This is a complex feature, one that
I rarely use, and even find a bit un-intuitive. Implementing replace mode in a way where all
actions function the same as insert mode (other than the actual character typing) allows for
a much simpler implementation, as well as a more intuitive user experience.
Replace mode implements and replaces (no pun intended) the last inserted keys of insert mode. Due to
the infrequent use of replace mode, and the '.' action for insert mode, this felt like a natural
trade off.
---
## TODO List
- Ops like change, and substitute and such should add to paste reg
- Delete op should also add to paste reg
- Gap buffer implementation (this shouldn't be TOO hard)
- Alternate buffer handling and implementation
- Scroll in X direction
---
## 🎯 Features

39
V0.1.md
View File

@ -1,39 +0,0 @@
# Gim v0.1 MVP
## Must Have (Blocker for Release)
- [x] Modal editing (normal, insert, visual)
- [x] Core motions and operators
- [x] Text objects
- [x] Undo/redo
- [x] File I/O
- [x] Buffer switching
- [ ] Search (/, ?, n, N) with highlighting
- [ ] Syntax highlighting (Chroma + tree-sitter for Go/Python/JS)
- [ ] % (matching bracket)
- [ ] J (join lines)
- [ ] H/M/L (screen movement)
- [ ] Status line (mode, filename, position, modified flag)
## Should Have (Makes it Usable)
- [ ] :substitute (%s/old/new/g) - at least basic version
- [ ] Better command-mode autocomplete/hints
- [ ] Configuration file support (~/.gimrc or similar)
- [ ] Persistent undo history
- [ ] Line wrapping display option
- [ ] Handle large files gracefully (>10k lines)
## Nice to Have (Polish)
- [ ] Incremental search (search as you type)
- [ ] * and # (search word under cursor)
- [ ] Cursor line highlight
- [ ] Scroll cursor to center (zz, zt, zb)
- [ ] Named registers (a-z)
- [ ] Black hole register (_)
## Won't Have in v0.1 (Future)
- LSP integration → v0.2
- Fuzzy finder → v0.3
- Splits/windows → v0.3
- Macros → v0.2
- Git integration → v0.3
- Plugin system → v1.0

View File

@ -23,13 +23,11 @@ func main() {
prog = program.NewProgramBuilder().
EmptyProgram().
WithOpt(tea.WithAltScreen()).
WithOpt(tea.WithMouseCellMotion()).
Build()
} else {
prog = program.NewProgramBuilder().
FileProgram(args[0]).
WithOpt(tea.WithAltScreen()).
WithOpt(tea.WithMouseCellMotion()).
Build()
}

View File

@ -24,8 +24,6 @@
glibc_multi
];
name = "Gim";
# Define the shell that will be executed.
# Here, we explicitly use zsh.
# Note: pkgs.zsh needs to be included in `packages` or `nativeBuildInputs`

View File

@ -16,7 +16,7 @@ func (a ChangeToEndOfLine) Execute(m Model) tea.Cmd {
buf := m.ActiveBuffer()
pos := win.Cursor.Col
line := buf.Line(win.Cursor.Line)
line := buf.Lines[win.Cursor.Line]
// Save deleted text to register
if pos < len(line) {
@ -51,7 +51,7 @@ func (a SubstituteChar) Execute(m Model) tea.Cmd {
buf := m.ActiveBuffer()
pos := win.Cursor.Col
line := buf.Line(win.Cursor.Line)
line := buf.Lines[win.Cursor.Line]
// Calculate how many chars to delete (limited by line length)
count := min(a.Count, len(line)-pos)
@ -97,7 +97,7 @@ func (a SubstituteLine) Execute(m Model) tea.Cmd {
// Collect and delete lines
for range count {
lines = append(lines, buf.Line(y))
lines = append(lines, buf.Lines[y])
buf.DeleteLine(y)
}

View File

@ -1,8 +1,6 @@
package action
import (
"strconv"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
@ -141,12 +139,6 @@ func (a CommandExecute) Execute(m Model) tea.Cmd {
// history = append([]string{cmdLine}, history...)
m.SetCommandHistory(history)
// try to parse entire thing as a number
num, err := strconv.Atoi(cmdLine)
if err == nil {
return cmdGoToLine(m, num)
}
cmd, err := a.Registry.Execute(m, cmdLine)
if err != nil {
out := core.CommandOutput{
@ -160,23 +152,3 @@ func (a CommandExecute) Execute(m Model) tea.Cmd {
return cmd
}
// cmdGoToLine: DOES NOT implement command.Command. Instead, is called directly
// by CommandExecture.Execute(). Jumps the cursor to the line provided
func cmdGoToLine(m Model, line int) tea.Cmd {
// number below 0 just goes back that many
win := m.ActiveWindow()
buf := m.ActiveBuffer()
if line <= 0 {
newLine := max(0, win.Cursor.Line+line)
win.SetCursorLine(newLine)
return nil
}
newLine := min(line-1, buf.LineCount())
win.SetCursorLine(newLine)
return nil
}

View File

@ -1,725 +0,0 @@
package action
import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
// mockCommandRegistry for testing - returns nil for all commands (used for numeric goto)
type mockCommandRegistry struct{}
func (r *mockCommandRegistry) Execute(m Model, cmdLine string) (tea.Cmd, error) {
return nil, nil
}
// TestCommandExecute_GoToLine tests the :<num> command functionality
func TestCommandExecute_GoToLine(t *testing.T) {
t.Run("goto positive line number within bounds", func(t *testing.T) {
// Create buffer with 10 lines
buf := core.NewBufferBuilder().
WithLines([]string{"line1", "line2", "line3", "line4", "line5", "line6", "line7", "line8", "line9", "line10"}).
Build()
m := NewMockModelWithBuffer(&buf)
win := m.ActiveWindow()
win.SetCursorLine(0)
// Set command to "5"
m.SetCommand("5")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
// Should jump to line 5 (0-indexed: line 4)
if win.Cursor.Line != 4 {
t.Fatalf("expected cursor at line 4, got %d", win.Cursor.Line)
}
// Should exit command mode
if m.Mode() != core.NormalMode {
t.Fatalf("expected normal mode, got %v", m.Mode())
}
})
t.Run("goto line 1", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithLines([]string{"line1", "line2", "line3", "line4", "line5"}).
Build()
m := NewMockModelWithBuffer(&buf)
win := m.ActiveWindow()
win.SetCursorLine(3) // Start at line 4
m.SetCommand("1")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
// Should jump to line 1 (0-indexed: line 0)
if win.Cursor.Line != 0 {
t.Fatalf("expected cursor at line 0, got %d", win.Cursor.Line)
}
})
t.Run("goto last line", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithLines([]string{"line1", "line2", "line3", "line4", "line5"}).
Build()
m := NewMockModelWithBuffer(&buf)
win := m.ActiveWindow()
win.SetCursorLine(0)
m.SetCommand("5")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
// Should jump to line 5 (0-indexed: line 4)
if win.Cursor.Line != 4 {
t.Fatalf("expected cursor at line 4, got %d", win.Cursor.Line)
}
})
t.Run("goto line beyond buffer length clamps to last line", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithLines([]string{"line1", "line2", "line3"}).
Build()
m := NewMockModelWithBuffer(&buf)
win := m.ActiveWindow()
win.SetCursorLine(0)
m.SetCommand("999")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
// Should clamp to last line (0-indexed: line 2)
// Note: cmdGoToLine uses min(line-1, buf.LineCount())
// LineCount() returns 3, so min(998, 3) = 3
// But SetCursorLine calls ClampCursor() which clamps to valid range [0, LineCount-1]
lastLine := buf.LineCount() - 1
if win.Cursor.Line != lastLine {
t.Fatalf("expected cursor at line %d (last line), got %d", lastLine, win.Cursor.Line)
}
})
t.Run("goto line zero goes to beginning", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithLines([]string{"line1", "line2", "line3", "line4", "line5"}).
Build()
m := NewMockModelWithBuffer(&buf)
win := m.ActiveWindow()
win.SetCursorLine(3)
m.SetCommand("0")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
// Line 0 or below goes relative: max(0, currentLine + 0) = currentLine
// But with the logic: if line <= 0, newLine = max(0, win.Cursor.Line + line)
// So for line=0: max(0, 3+0) = 3 (stays at same line)
if win.Cursor.Line != 3 {
t.Fatalf("expected cursor at line 3, got %d", win.Cursor.Line)
}
})
t.Run("negative number moves relative backwards", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithLines([]string{"line1", "line2", "line3", "line4", "line5", "line6", "line7", "line8", "line9", "line10"}).
Build()
m := NewMockModelWithBuffer(&buf)
win := m.ActiveWindow()
win.SetCursorLine(5) // Start at line 6 (0-indexed)
m.SetCommand("-3")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
// Should move back 3 lines: 5 + (-3) = 2
if win.Cursor.Line != 2 {
t.Fatalf("expected cursor at line 2, got %d", win.Cursor.Line)
}
})
t.Run("negative number beyond start clamps to line 0", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithLines([]string{"line1", "line2", "line3", "line4", "line5"}).
Build()
m := NewMockModelWithBuffer(&buf)
win := m.ActiveWindow()
win.SetCursorLine(2)
m.SetCommand("-10")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
// Should clamp to line 0: max(0, 2 + (-10)) = max(0, -8) = 0
if win.Cursor.Line != 0 {
t.Fatalf("expected cursor at line 0, got %d", win.Cursor.Line)
}
})
t.Run("goto same line", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithLines([]string{"line1", "line2", "line3", "line4", "line5"}).
Build()
m := NewMockModelWithBuffer(&buf)
win := m.ActiveWindow()
win.SetCursorLine(2) // At line 3
m.SetCommand("3")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
// Should stay at line 3 (0-indexed: line 2)
if win.Cursor.Line != 2 {
t.Fatalf("expected cursor at line 2, got %d", win.Cursor.Line)
}
})
t.Run("empty buffer", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithLines([]string{""}).
Build()
m := NewMockModelWithBuffer(&buf)
win := m.ActiveWindow()
m.SetCommand("1")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
// Should stay at line 0
if win.Cursor.Line != 0 {
t.Fatalf("expected cursor at line 0, got %d", win.Cursor.Line)
}
})
t.Run("single line buffer goto line 1", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithLines([]string{"only line"}).
Build()
m := NewMockModelWithBuffer(&buf)
win := m.ActiveWindow()
m.SetCommand("1")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
if win.Cursor.Line != 0 {
t.Fatalf("expected cursor at line 0, got %d", win.Cursor.Line)
}
})
t.Run("single line buffer goto beyond", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithLines([]string{"only line"}).
Build()
m := NewMockModelWithBuffer(&buf)
win := m.ActiveWindow()
m.SetCommand("10")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
// Should clamp to the available lines
if win.Cursor.Line > 0 {
t.Fatalf("expected cursor at line 0 or 1, got %d", win.Cursor.Line)
}
})
t.Run("large line number", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithLines([]string{"line1", "line2", "line3"}).
Build()
m := NewMockModelWithBuffer(&buf)
win := m.ActiveWindow()
m.SetCommand("1000000")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
// Should clamp to last available line
lineCount := buf.LineCount()
if win.Cursor.Line > lineCount {
t.Fatalf("expected cursor at or before line %d, got %d", lineCount, win.Cursor.Line)
}
})
t.Run("command with leading/trailing spaces", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithLines([]string{"line1", "line2", "line3", "line4", "line5"}).
Build()
m := NewMockModelWithBuffer(&buf)
win := m.ActiveWindow()
win.SetCursorLine(0)
// strconv.Atoi should handle leading spaces
m.SetCommand(" 3 ")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
// strconv.Atoi(" 3 ") will fail, so this won't be treated as a line number
// It should stay at the same position and potentially show an error
// depending on the CommandRegistry behavior
})
t.Run("mixed alphanumeric command not treated as line number", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithLines([]string{"line1", "line2", "line3"}).
Build()
m := NewMockModelWithBuffer(&buf)
win := m.ActiveWindow()
initialLine := 1
win.SetCursorLine(initialLine)
m.SetCommand("3abc")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
// "3abc" is not a valid number, so won't trigger goto line
// Cursor should stay at original position
if win.Cursor.Line != initialLine {
t.Fatalf("expected cursor at line %d, got %d", initialLine, win.Cursor.Line)
}
})
}
// TestCommandExecute_EmptyCommand tests empty command behavior
func TestCommandExecute_EmptyCommand(t *testing.T) {
t.Run("empty command does nothing", func(t *testing.T) {
m := NewMockModel()
win := m.ActiveWindow()
initialLine := 0
win.SetCursorLine(initialLine)
m.SetCommand("")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
// Should exit command mode
if m.Mode() != core.NormalMode {
t.Fatalf("expected normal mode, got %v", m.Mode())
}
// Cursor should not move
if win.Cursor.Line != initialLine {
t.Fatalf("expected cursor at line %d, got %d", initialLine, win.Cursor.Line)
}
})
}
// TestCommandExecute_CommandHistory tests that commands are added to history
func TestCommandExecute_CommandHistory(t *testing.T) {
t.Run("numeric command added to history", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithLines([]string{"line1", "line2", "line3", "line4", "line5"}).
Build()
m := NewMockModelWithBuffer(&buf)
m.SetCommand("3")
m.SetMode(core.CommandMode)
m.SetCommandHistory([]string{})
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
history := m.CommandHistory()
if len(history) != 1 {
t.Fatalf("expected 1 item in history, got %d", len(history))
}
if history[0] != "3" {
t.Fatalf("expected '3' in history, got '%s'", history[0])
}
})
t.Run("commands prepended to existing history", func(t *testing.T) {
m := NewMockModel()
m.SetCommandHistory([]string{"oldcommand1", "oldcommand2"})
m.SetCommand("5")
m.SetMode(core.CommandMode)
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
history := m.CommandHistory()
if len(history) != 3 {
t.Fatalf("expected 3 items in history, got %d", len(history))
}
if history[0] != "5" {
t.Fatalf("expected '5' as first item, got '%s'", history[0])
}
if history[1] != "oldcommand1" {
t.Fatalf("expected 'oldcommand1' as second item, got '%s'", history[1])
}
})
}
// TestCommandExecute_ModeTransition tests command mode exit behavior
func TestCommandExecute_ModeTransition(t *testing.T) {
t.Run("exits command mode after execution", func(t *testing.T) {
m := NewMockModel()
m.SetMode(core.CommandMode)
m.SetCommand("5")
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
if m.Mode() != core.NormalMode {
t.Fatalf("expected normal mode, got %v", m.Mode())
}
})
t.Run("resets command cursor to 0", func(t *testing.T) {
m := NewMockModel()
m.SetCommandCursor(10)
m.SetCommand("5")
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
if m.CommandCursor() != 0 {
t.Fatalf("expected command cursor at 0, got %d", m.CommandCursor())
}
})
t.Run("resets command history cursor to 0", func(t *testing.T) {
m := NewMockModel()
m.SetCommandHistoryCursor(5)
m.SetCommand("5")
action := CommandExecute{Registry: &mockCommandRegistry{}}
action.Execute(m)
if m.CommandHistoryCursor() != 0 {
t.Fatalf("expected command history cursor at 0, got %d", m.CommandHistoryCursor())
}
})
}
// TestExitCommandMode tests the Esc key behavior
func TestExitCommandMode(t *testing.T) {
t.Run("exit command mode clears state", func(t *testing.T) {
m := NewMockModel()
m.SetMode(core.CommandMode)
m.SetCommand("test command")
m.SetCommandCursor(5)
m.SetCommandHistoryCursor(3)
action := ExitCommandMode{}
action.Execute(m)
if m.Mode() != core.NormalMode {
t.Fatalf("expected normal mode, got %v", m.Mode())
}
if m.Command() != "" {
t.Fatalf("expected empty command, got '%s'", m.Command())
}
if m.CommandCursor() != 0 {
t.Fatalf("expected command cursor at 0, got %d", m.CommandCursor())
}
if m.CommandHistoryCursor() != 0 {
t.Fatalf("expected command history cursor at 0, got %d", m.CommandHistoryCursor())
}
})
}
// TestInsertCommandChar tests character insertion in command mode
func TestInsertCommandChar(t *testing.T) {
t.Run("insert character at empty command", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("")
m.SetCommandCursor(0)
action := InsertCommandChar{Char: "5"}
action.Execute(m)
if m.Command() != "5" {
t.Fatalf("expected '5', got '%s'", m.Command())
}
if m.CommandCursor() != 1 {
t.Fatalf("expected cursor at 1, got %d", m.CommandCursor())
}
})
t.Run("insert character at end", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("12")
m.SetCommandCursor(2)
action := InsertCommandChar{Char: "3"}
action.Execute(m)
if m.Command() != "123" {
t.Fatalf("expected '123', got '%s'", m.Command())
}
if m.CommandCursor() != 3 {
t.Fatalf("expected cursor at 3, got %d", m.CommandCursor())
}
})
t.Run("insert character in middle", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("13")
m.SetCommandCursor(1)
action := InsertCommandChar{Char: "2"}
action.Execute(m)
if m.Command() != "123" {
t.Fatalf("expected '123', got '%s'", m.Command())
}
if m.CommandCursor() != 2 {
t.Fatalf("expected cursor at 2, got %d", m.CommandCursor())
}
})
}
// TestCommandBackspace tests backspace in command mode
func TestCommandBackspace(t *testing.T) {
t.Run("backspace at beginning does nothing", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("123")
m.SetCommandCursor(0)
action := CommandBackspace{}
action.Execute(m)
if m.Command() != "123" {
t.Fatalf("expected '123', got '%s'", m.Command())
}
if m.CommandCursor() != 0 {
t.Fatalf("expected cursor at 0, got %d", m.CommandCursor())
}
})
t.Run("backspace in middle", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("123")
m.SetCommandCursor(2)
action := CommandBackspace{}
action.Execute(m)
if m.Command() != "13" {
t.Fatalf("expected '13', got '%s'", m.Command())
}
if m.CommandCursor() != 1 {
t.Fatalf("expected cursor at 1, got %d", m.CommandCursor())
}
})
t.Run("backspace at end", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("123")
m.SetCommandCursor(3)
action := CommandBackspace{}
action.Execute(m)
if m.Command() != "12" {
t.Fatalf("expected '12', got '%s'", m.Command())
}
if m.CommandCursor() != 2 {
t.Fatalf("expected cursor at 2, got %d", m.CommandCursor())
}
})
}
// TestCommandDelete tests delete key in command mode
func TestCommandDelete(t *testing.T) {
t.Run("delete at cursor position 0 deletes character after cursor", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("123")
m.SetCommandCursor(0)
action := CommandDelete{}
action.Execute(m)
// At position 0, delete removes char at position 1 (the next char)
// Result: "1" + "3" = "13"
if m.Command() != "13" {
t.Fatalf("expected '13', got '%s'", m.Command())
}
})
t.Run("delete at cursor position 1", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("123")
m.SetCommandCursor(1)
action := CommandDelete{}
action.Execute(m)
// At position 1, delete removes char at position 2 (the next char)
// Result: "12" + "" = "12"
if m.Command() != "12" {
t.Fatalf("expected '12', got '%s'", m.Command())
}
})
t.Run("delete at last character", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("123")
m.SetCommandCursor(2)
action := CommandDelete{}
action.Execute(m)
if m.Command() != "12" {
t.Fatalf("expected '12', got '%s'", m.Command())
}
})
t.Run("delete at end acts as backspace", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("123")
m.SetCommandCursor(3)
action := CommandDelete{}
action.Execute(m)
if m.Command() != "12" {
t.Fatalf("expected '12', got '%s'", m.Command())
}
if m.CommandCursor() != 2 {
t.Fatalf("expected cursor at 2, got %d", m.CommandCursor())
}
})
}
// TestCommandDeletePreviousWord tests Ctrl+W behavior in command mode
func TestCommandDeletePreviousWord(t *testing.T) {
t.Run("delete word from middle of text", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("hello world")
m.SetCommandCursor(11) // At end
action := CommandDeletePreviousWord{}
action.Execute(m)
if m.Command() != "hello " {
t.Fatalf("expected 'hello ', got '%s'", m.Command())
}
if m.CommandCursor() != 6 {
t.Fatalf("expected cursor at 6, got %d", m.CommandCursor())
}
})
t.Run("delete word with leading spaces", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("hello world")
m.SetCommandCursor(13) // At end
action := CommandDeletePreviousWord{}
action.Execute(m)
if m.Command() != "hello " {
t.Fatalf("expected 'hello ', got '%s'", m.Command())
}
})
t.Run("delete at beginning does nothing", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("hello")
m.SetCommandCursor(0)
action := CommandDeletePreviousWord{}
action.Execute(m)
if m.Command() != "hello" {
t.Fatalf("expected 'hello', got '%s'", m.Command())
}
if m.CommandCursor() != 0 {
t.Fatalf("expected cursor at 0, got %d", m.CommandCursor())
}
})
t.Run("delete punctuation", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("hello...")
m.SetCommandCursor(8) // After the dots
action := CommandDeletePreviousWord{}
action.Execute(m)
if m.Command() != "hello" {
t.Fatalf("expected 'hello', got '%s'", m.Command())
}
if m.CommandCursor() != 5 {
t.Fatalf("expected cursor at 5, got %d", m.CommandCursor())
}
})
t.Run("delete word in middle of command", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("one two three")
m.SetCommandCursor(7) // After "two"
action := CommandDeletePreviousWord{}
action.Execute(m)
if m.Command() != "one three" {
t.Fatalf("expected 'one three', got '%s'", m.Command())
}
if m.CommandCursor() != 4 {
t.Fatalf("expected cursor at 4, got %d", m.CommandCursor())
}
})
t.Run("delete single character word", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("a b c")
m.SetCommandCursor(3) // After "b"
action := CommandDeletePreviousWord{}
action.Execute(m)
if m.Command() != "a c" {
t.Fatalf("expected 'a c', got '%s'", m.Command())
}
})
t.Run("delete from whitespace position", func(t *testing.T) {
m := NewMockModel()
m.SetCommand("hello world")
m.SetCommandCursor(6) // At the space after "hello"
action := CommandDeletePreviousWord{}
action.Execute(m)
// Should skip the space and delete "hello" and the space
if m.Command() != "world" {
t.Fatalf("expected 'world', got '%s'", m.Command())
}
if m.CommandCursor() != 0 {
t.Fatalf("expected cursor at 0, got %d", m.CommandCursor())
}
})
}

View File

@ -13,7 +13,7 @@ func (a DeleteChar) Execute(m Model) tea.Cmd {
buf := m.ActiveBuffer()
pos := win.Cursor.Col
line := buf.Line(win.Cursor.Line)
line := buf.Lines[win.Cursor.Line]
for i := 0; i < a.Count && pos < len(line); i++ {
line = line[:pos] + line[pos+1:]
buf.SetLine(win.Cursor.Line, line)
@ -38,7 +38,7 @@ func (a DeletePrevChar) Execute(m Model) tea.Cmd {
buf := m.ActiveBuffer()
pos := win.Cursor.Col
line := buf.Line(win.Cursor.Line)
line := buf.Lines[win.Cursor.Line]
for i := 0; i < a.Count && pos <= len(line); i++ {
if pos > 0 {
line = line[:pos-1] + line[pos:]
@ -69,7 +69,7 @@ func (a DeleteToEndOfLine) Execute(m Model) tea.Cmd {
// Delete to end of line
pos := win.Cursor.Col
line := buf.Line(win.Cursor.Line)
line := buf.Lines[win.Cursor.Line]
buf.SetLine(win.Cursor.Line, line[:pos])
win.SetCursorCol(pos - 1)

View File

@ -76,7 +76,7 @@ type EnterInsertLineEnd struct {
func (a EnterInsertLineEnd) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
win.SetCursorCol(buf.Lines[win.Cursor.Line].Len())
win.SetCursorCol(len(buf.Lines[win.Cursor.Line]))
// Start recording
m.SetInsertRecording(a.Count, a)
@ -158,7 +158,7 @@ func (a InsertChar) Execute(m Model) tea.Cmd {
buf := m.ActiveBuffer()
x, y := win.Cursor.Col, win.Cursor.Line
l := buf.Line(y)
l := buf.Lines[y]
if x < len(l) {
buf.SetLine(y, l[:x]+a.Char+l[x:])
} else {
@ -177,7 +177,7 @@ func (a InsertNewline) Execute(m Model) tea.Cmd {
buf := m.ActiveBuffer()
x, y := win.Cursor.Col, win.Cursor.Line
l := buf.Line(y)
l := buf.Lines[y]
if x == len(l) {
buf.InsertLine(y+1, "")
} else {
@ -197,12 +197,12 @@ func (a InsertBackspace) Execute(m Model) tea.Cmd {
buf := m.ActiveBuffer()
x, y := win.Cursor.Col, win.Cursor.Line
l := buf.Line(y)
l := buf.Lines[y]
if x > 0 {
buf.SetLine(y, l[:x-1]+l[x:])
win.SetCursorCol(x - 1)
} else if y > 0 {
prevLine := buf.Line(y - 1)
prevLine := buf.Lines[y-1]
newX := len(prevLine)
buf.SetLine(y-1, prevLine+l)
buf.DeleteLine(y)
@ -220,9 +220,9 @@ func (a InsertDelete) Execute(m Model) tea.Cmd {
buf := m.ActiveBuffer()
x, y := win.Cursor.Col, win.Cursor.Line
l := buf.Line(y)
l := buf.Lines[y]
if x == len(l) && y < buf.LineCount()-1 {
nextLine := buf.Line(y + 1)
nextLine := buf.Lines[y+1]
buf.SetLine(y, l+nextLine)
buf.DeleteLine(y + 1)
} else if x < len(l) {
@ -240,7 +240,7 @@ func (a InsertTab) Execute(m Model) tea.Cmd {
buf := m.ActiveBuffer()
x, y := win.Cursor.Col, win.Cursor.Line
l := buf.Line(y)
l := buf.Lines[y]
tabs := strings.Repeat(" ", m.Settings().TabStop)
if x < len(l) {
buf.SetLine(y, l[:x]+tabs+l[x:])
@ -275,12 +275,12 @@ func (a InsertDeletePreviousWord) Execute(m Model) tea.Cmd {
buf := m.ActiveBuffer()
x, y := win.Cursor.Col, win.Cursor.Line
line := buf.Line(y)
line := buf.Lines[y]
// At start of line: merge with previous line (same as backspace)
if x == 0 {
if y > 0 {
prevLine := buf.Line(y - 1)
prevLine := buf.Lines[y-1]
newX := len(prevLine)
buf.SetLine(y-1, prevLine+line)
buf.DeleteLine(y)

View File

@ -64,12 +64,6 @@ type Model interface {
GetRegister(name rune) (core.Register, bool)
SetRegister(name rune, t core.RegisterType, cnt []string) error
UpdateDefaultRegister(t core.RegisterType, cnt []string)
// Dot operator - accumulate keys for repeat
SetLastChangeKeys(keys []string)
LastChangeKeys() []string
ClearLastChangeKeys()
HandleKey(key string) tea.Cmd
}
// Action is the base interface - anything executable

View File

@ -60,27 +60,3 @@ func (a EnterVisualBlockMode) Execute(m Model) tea.Cmd {
m.SetMode(core.VisualBlockMode)
return nil
}
// TODO: Implement count?
type Undo struct{}
func (a Undo) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
if buf.UndoStack.CanUndo() {
buf.Undo(win)
}
return nil
}
// TODO: Implement count?
type Redo struct{}
func (a Redo) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
if buf.UndoStack.CanRedo() {
buf.Redo(win)
}
return nil
}

View File

@ -3,7 +3,6 @@ package action
import (
"git.gophernest.net/azpect/TextEditor/internal/core"
"git.gophernest.net/azpect/TextEditor/internal/style"
tea "github.com/charmbracelet/bubbletea"
)
// MockModel is a shared test implementation of the Model interface.
@ -24,7 +23,6 @@ type MockModel struct {
CommandHistoryCur int
LastFindVal core.LastFindCommand
StylesVal style.Styles
LastChangeKeysList []string
}
// NewMockModel creates a mock with an empty buffer and 24x80 window.
@ -133,9 +131,3 @@ func (m *MockModel) SetRegister(name rune, t core.RegisterType, cnt []string) er
func (m *MockModel) UpdateDefaultRegister(t core.RegisterType, cnt []string) {
m.RegistersMap['"'] = core.Register{Type: t, Content: cnt}
}
// Dot operator
func (m *MockModel) SetLastChangeKeys(keys []string) { m.LastChangeKeysList = keys }
func (m *MockModel) LastChangeKeys() []string { return m.LastChangeKeysList }
func (m *MockModel) ClearLastChangeKeys() { m.LastChangeKeysList = []string{} }
func (m *MockModel) HandleKey(key string) tea.Cmd { return nil }

View File

@ -63,7 +63,7 @@ func (a Paste) Execute(m Model) tea.Cmd {
x := win.Cursor.Col
y := win.Cursor.Line
curLine := buf.Line(y)
curLine := buf.Lines[y]
insertAt := min(x+1, len(curLine))
if len(lines) == 1 {
@ -101,11 +101,11 @@ func (a Paste) Execute(m Model) tea.Cmd {
// Last line: append the suffix
lastLineIdx := y + len(pastedLines) - 1
buf.SetLine(lastLineIdx, buf.Line(lastLineIdx)+suffix)
buf.SetLine(lastLineIdx, buf.Lines[lastLineIdx]+suffix)
// Set cursor to end of last pasted content (before suffix)
win.SetCursorLine(lastLineIdx)
win.SetCursorCol(len(buf.Line(lastLineIdx)) - len(suffix) - 1)
win.SetCursorCol(len(buf.Lines[lastLineIdx]) - len(suffix) - 1)
}
}
default:
@ -175,7 +175,7 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
x := win.Cursor.Col
y := win.Cursor.Line
curLine := buf.Line(y)
curLine := buf.Lines[y]
insertAt := min(x, len(curLine))
if len(lines) == 1 {
@ -213,11 +213,11 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
// Last line: append the suffix
lastLineIdx := y + len(pastedLines) - 1
buf.SetLine(lastLineIdx, buf.Line(lastLineIdx)+suffix)
buf.SetLine(lastLineIdx, buf.Lines[lastLineIdx]+suffix)
// Set cursor to end of last pasted content (before suffix)
win.SetCursorLine(lastLineIdx)
win.SetCursorCol(len(buf.Line(lastLineIdx)) - len(suffix) - 1)
win.SetCursorCol(len(buf.Lines[lastLineIdx]) - len(suffix) - 1)
}
}
default:
@ -240,11 +240,9 @@ func (a PasteBefore) WithCount(n int) Action {
return PasteBefore{Count: n}
}
// VisualPaste implements Action (p/p in visual mode) - replaces selection with
// register content when Replace flag is set
// VisualPaste implements Action (p in visual mode) - replaces selection with register content
type VisualPaste struct {
Count int
Replace bool
Count int
}
// VisualPaste.Execute: Replaces visual selection with register content (p in visual mode).
@ -268,11 +266,11 @@ func (a VisualPaste) Execute(m Model) tea.Cmd {
switch mode {
case core.VisualMode:
visualCharPaste(m, reg, start, end, a.Replace)
visualCharPaste(m, reg, start, end)
case core.VisualBlockMode:
visualBlockPaste(m, reg, start, end, a.Replace)
visualBlockPaste(m, reg, start, end)
case core.VisualLineMode:
visualLinePaste(m, reg, start, end, a.Replace)
visualLinePaste(m, reg, start, end)
}
// Exit visual mode
@ -297,7 +295,7 @@ func normalizeSelection(m Model) (core.Position, core.Position) {
}
// visualCharPaste: Handles paste operation in visual (character) mode.
func visualCharPaste(m Model, reg core.Register, start, end core.Position, replace bool) {
func visualCharPaste(m Model, reg core.Register, start, end core.Position) {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -313,7 +311,7 @@ func visualCharPaste(m Model, reg core.Register, start, end core.Position, repla
} else if reg.Type == core.CharwiseRegister {
// Charwise paste: insert text at cursor position
if len(reg.Content) == 1 {
line := buf.Line(start.Line)
line := buf.Lines[start.Line]
insertAt := min(start.Col, len(line))
newLine := line[:insertAt] + reg.Content[0] + line[insertAt:]
buf.SetLine(start.Line, newLine)
@ -328,7 +326,7 @@ func visualCharPaste(m Model, reg core.Register, start, end core.Position, repla
for i, content := range reg.Content {
if i == 0 {
// First line: insert at start position
line := buf.Line(start.Line)
line := buf.Lines[start.Line]
insertAt := min(start.Col, len(line))
newLine := line[:insertAt] + content
if len(reg.Content) == 1 {
@ -346,13 +344,11 @@ func visualCharPaste(m Model, reg core.Register, start, end core.Position, repla
}
// Update register with deleted text
if replace {
m.UpdateDefaultRegister(core.CharwiseRegister, []string{deletedText})
}
m.UpdateDefaultRegister(core.CharwiseRegister, []string{deletedText})
}
// visualBlockPaste: Handles paste operation in visual block mode.
func visualBlockPaste(m Model, reg core.Register, start, end core.Position, replace bool) {
func visualBlockPaste(m Model, reg core.Register, start, end core.Position) {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -362,7 +358,7 @@ func visualBlockPaste(m Model, reg core.Register, start, end core.Position, repl
// Extract deleted text (for register)
var deletedLines []string
for y := start.Line; y <= end.Line; y++ {
line := buf.Line(y)
line := buf.Lines[y]
if startCol < len(line) {
ec := min(endCol+1, len(line))
deletedLines = append(deletedLines, line[startCol:ec])
@ -373,7 +369,7 @@ func visualBlockPaste(m Model, reg core.Register, start, end core.Position, repl
// Delete the block selection
for y := start.Line; y <= end.Line; y++ {
line := buf.Line(y)
line := buf.Lines[y]
if startCol >= len(line) {
continue
}
@ -389,7 +385,7 @@ func visualBlockPaste(m Model, reg core.Register, start, end core.Position, repl
}
for y := start.Line; y <= end.Line; y++ {
line := buf.Line(y)
line := buf.Lines[y]
insertAt := min(startCol, len(line))
// Pad with spaces if needed
for len(line) < insertAt {
@ -404,20 +400,18 @@ func visualBlockPaste(m Model, reg core.Register, start, end core.Position, repl
win.SetCursorCol(startCol)
// Update register with deleted block text (joined)
if replace {
m.UpdateDefaultRegister(core.CharwiseRegister, []string{strings.Join(deletedLines, "\n")})
}
m.UpdateDefaultRegister(core.CharwiseRegister, []string{strings.Join(deletedLines, "\n")})
}
// visualLinePaste: Handles paste operation in visual line mode.
func visualLinePaste(m Model, reg core.Register, start, end core.Position, replace bool) {
func visualLinePaste(m Model, reg core.Register, start, end core.Position) {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
// Extract deleted lines (for register)
var deletedLines []string
for y := start.Line; y <= end.Line; y++ {
deletedLines = append(deletedLines, buf.Line(y))
deletedLines = append(deletedLines, buf.Lines[y])
}
// Delete the selected lines (from end to start to preserve indices)
@ -457,9 +451,7 @@ func visualLinePaste(m Model, reg core.Register, start, end core.Position, repla
win.SetCursorCol(0)
// Update register with deleted lines
if replace {
m.UpdateDefaultRegister(core.LinewiseRegister, deletedLines)
}
m.UpdateDefaultRegister(core.LinewiseRegister, deletedLines)
}
// extractCharSelection: Extracts text from a character selection range.
@ -467,7 +459,7 @@ func extractCharSelection(m Model, start, end core.Position) string {
buf := m.ActiveBuffer()
if start.Line == end.Line {
line := buf.Line(start.Line)
line := buf.Lines[start.Line]
endCol := min(end.Col+1, len(line))
startCol := min(start.Col, len(line))
if startCol >= endCol {
@ -480,7 +472,7 @@ func extractCharSelection(m Model, start, end core.Position) string {
var result strings.Builder
// First line: from start.Col to end
firstLine := buf.Line(start.Line)
firstLine := buf.Lines[start.Line]
if start.Col < len(firstLine) {
result.WriteString(firstLine[start.Col:])
}
@ -488,12 +480,12 @@ func extractCharSelection(m Model, start, end core.Position) string {
// Middle lines: entire lines
for y := start.Line + 1; y < end.Line; y++ {
result.WriteString(buf.Line(y))
result.WriteString(buf.Lines[y])
result.WriteString("\n")
}
// Last line: from beginning to end.Col
lastLine := buf.Line(end.Line)
lastLine := buf.Lines[end.Line]
endCol := min(end.Col+1, len(lastLine))
result.WriteString(lastLine[:endCol])
@ -506,12 +498,12 @@ func deleteCharSelectionForPaste(m Model, start, end core.Position) {
buf := m.ActiveBuffer()
if start.Line == end.Line {
line := buf.Line(start.Line)
line := buf.Lines[start.Line]
endCol := min(end.Col+1, len(line))
buf.SetLine(start.Line, line[:start.Col]+line[endCol:])
} else {
startLine := buf.Line(start.Line)
endLine := buf.Line(end.Line)
startLine := buf.Lines[start.Line]
endLine := buf.Lines[end.Line]
prefix := ""
if start.Col < len(startLine) {
@ -539,5 +531,5 @@ var _ Repeatable = VisualPaste{}
// VisualPaste.WithCount: Returns a new VisualPaste with the given count.
func (a VisualPaste) WithCount(n int) Action {
return VisualPaste{Count: n, Replace: a.Replace}
return VisualPaste{Count: n}
}

View File

@ -1,41 +0,0 @@
package action
import (
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
// Repeat implements Action (.) - repeat last input
type Repeat struct {
Count int
}
func (a Repeat) Execute(m Model) tea.Cmd {
keys := m.LastChangeKeys()
if len(keys) == 1 && keys[0] == "." {
m.SetCommandOutput(&core.CommandOutput{
Lines: []string{"Cannot repeat '.'"},
Inline: true,
IsError: true,
})
return nil
}
var cmds []tea.Cmd
for _, key := range keys {
cmd := m.HandleKey(key)
cmds = append(cmds, cmd)
}
return tea.Batch(cmds...)
}
// Ensure Repeat implements Repeatable
var _ Repeatable = Repeat{}
// Repeat.WithCount: Returns a new Repeat with the given count.
func (a Repeat) WithCount(n int) Action {
return Repeat{Count: n}
}

View File

@ -1,129 +0,0 @@
package action
import (
"strings"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
type ReplaceChar struct {
Char string
Count int
}
func (m ReplaceChar) WithChar(char string) Motion {
m.Char = char
return m
}
func (m ReplaceChar) Type() core.MotionType {
return core.CharwiseInclusive
}
// WithCount sets the count (required by Repeatable interface)
func (m ReplaceChar) WithCount(n int) Action {
m.Count = n
return m
}
func (a ReplaceChar) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
if buf.UndoStack != nil && !buf.UndoStack.Recording() {
buf.UndoStack.BeginBlock(win.Cursor)
}
pos := win.Cursor.Col
line := buf.Line(win.Cursor.Line)
for i := 0; i < a.Count && pos < len(line); i++ {
line = line[:pos] + a.Char + line[pos+1:]
buf.SetLine(win.Cursor.Line, line)
pos++
}
win.SetCursorCol(pos - 1)
m.SetMode(core.NormalMode)
if buf.UndoStack != nil {
buf.UndoStack.EndBlock(win.Cursor)
}
return nil
}
type EnterReplace struct {
Count int
}
func (a EnterReplace) WithCount(n int) Action {
a.Count = n
return a
}
func (a EnterReplace) Execute(m Model) tea.Cmd {
m.SetMode(core.ReplaceMode)
return nil
}
type ReplaceModeChar struct {
Char string
}
// ReplaceModeChar.Execute: Inserts a single character at the cursor position, overwriting current
// character.
func (a ReplaceModeChar) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
x, y := win.Cursor.Col, win.Cursor.Line
l := buf.Line(y)
if x < len(l) {
buf.SetLine(y, l[:x]+a.Char+l[x+1:])
} else {
buf.SetLine(y, l+a.Char)
}
win.SetCursorCol(x + len(a.Char))
return nil
}
// ReplaceNewline splits the current line at the cursor (enter key)
type ReplaceNewline struct{}
// ReplaceNewline.Execute: Splits the current line at the cursor (Enter key).
func (a ReplaceNewline) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
x, y := win.Cursor.Col, win.Cursor.Line
l := buf.Line(y)
if x == len(l) {
buf.InsertLine(y+1, "")
} else {
buf.SetLine(y, l[:x])
buf.InsertLine(y+1, l[x+1:])
}
win.SetCursorPos(y+1, 0)
return nil
}
// ReplaceTab inserts spaces equal to the tab size
type ReplaceTab struct{}
// ReplaceTab.Execute: Inserts spaces equal to the tab size (Tab key).
func (a ReplaceTab) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
x, y := win.Cursor.Col, win.Cursor.Line
l := buf.Line(y)
tabs := strings.Repeat(" ", m.Settings().TabStop)
if x < len(l) {
buf.SetLine(y, l[:x]+tabs+l[x+1:])
} else {
buf.SetLine(y, l+tabs)
}
win.SetCursorCol(x + len(tabs))
return nil
}

View File

@ -933,29 +933,3 @@ func cmdListColorschemes(m action.Model, args []string, force bool) tea.Cmd {
return nil
}
func cmdUndoList(m action.Model, args []string, force bool) tea.Cmd {
_, _ = args, force
lines := m.ActiveBuffer().UndoStack.List()
// For now, display an error when empty
if len(lines) == 0 {
m.SetCommandOutput(&core.CommandOutput{
Lines: []string{"Undo stack is empty"},
Inline: true,
IsError: true,
})
return nil
}
m.SetMode(core.CommandOutputMode)
m.SetCommandOutput(&core.CommandOutput{
Title: ":undo",
Lines: lines,
Inline: false,
IsError: false,
})
return nil
}

View File

@ -63,7 +63,7 @@ func writeBuffer(m action.Model, buf *core.Buffer, args []string, force bool) (t
// Using a bufio.Writer because its more efficient
writer := bufio.NewWriter(file)
for _, line := range buf.Lines {
n, err := writer.WriteString(line.String() + "\n")
n, err := writer.WriteString(line + "\n")
if err != nil {
return nil, err
}

View File

@ -237,11 +237,4 @@ func (r *Registry) registerDefaults() {
ShortForm: "colorschemes",
Handler: cmdListColorschemes,
})
// Undo stack commands
r.Register(Command{
Name: "undo",
ShortForm: "u",
Handler: cmdUndoList,
})
}

View File

@ -20,7 +20,7 @@ type Buffer struct {
// File data
Filename string
Filetype string
Lines []*GapBuffer // Changed from []string to []*GapBuffer
Lines []string
// Flags (not used yet)
Modified bool
@ -29,7 +29,7 @@ type Buffer struct {
ReadOnly bool
// Options BufferOptions
UndoStack *UndoStack
// UndoTree TODO: This will be big
}
// ==================================================
@ -42,18 +42,14 @@ func (b *Buffer) Line(idx int) string {
if idx < 0 || idx >= len(b.Lines) {
return ""
}
return b.Lines[idx].String()
return b.Lines[idx]
}
// Buffer.SetLine: Set the content of the line at an index. Does nothing if the
// index is out of bounds. This function sets the modified flag.
func (b *Buffer) SetLine(idx int, content string) {
if idx >= 0 && idx < len(b.Lines) {
// Record set line in undo stack
if b.UndoStack != nil {
b.UndoStack.RecordSetLine(idx, b.Lines[idx].String(), content)
}
b.Lines[idx].Set(content)
b.Lines[idx] = content
}
b.Modified = true
}
@ -68,14 +64,7 @@ func (b *Buffer) InsertLine(idx int, content string) {
if idx > len(b.Lines) {
idx = len(b.Lines)
}
// Record insert line in undo stack
if b.UndoStack != nil {
b.UndoStack.RecordInsertLine(idx, content)
}
newLine := NewGapBuffer(content)
b.Lines = append(b.Lines[:idx], append([]*GapBuffer{newLine}, b.Lines[idx:]...)...)
b.Lines = append(b.Lines[:idx], append([]string{content}, b.Lines[idx:]...)...)
b.Modified = true
}
@ -83,10 +72,6 @@ func (b *Buffer) InsertLine(idx int, content string) {
// of bounds. This function sets the modified flag.
func (b *Buffer) DeleteLine(idx int) {
if idx >= 0 && idx < len(b.Lines) {
// Record delete line in undo stack
if b.UndoStack != nil {
b.UndoStack.RecordDeleteLine(idx, b.Lines[idx].String())
}
b.Lines = append(b.Lines[:idx], b.Lines[idx+1:]...)
}
b.Modified = true
@ -97,101 +82,6 @@ func (b *Buffer) LineCount() int {
return len(b.Lines)
}
// ==================================================
// Undo Stack
// ==================================================
func (b *Buffer) Undo(w *Window) bool {
if b.UndoStack == nil {
return false
}
block := b.UndoStack.Undo()
if block == nil {
return false
}
// Apply changes in REVERSE order
for i := len(block.Changes) - 1; i >= 0; i-- {
change := block.Changes[i]
// Temporarily disable recording while we undo
wasRecording := b.UndoStack.recording
b.UndoStack.recording = false
switch change.Type {
case SetLineChange:
// Restore old data
if change.Line >= 0 && change.Line < len(b.Lines) {
b.Lines[change.Line].Set(change.OldData)
}
case InsertLineChange:
// Remove the inserted line
if change.Line >= 0 && change.Line < len(b.Lines) {
b.Lines = append(b.Lines[:change.Line], b.Lines[change.Line+1:]...)
}
case DeleteLineChange:
// Re-insert the deleted line
if change.Line <= len(b.Lines) {
newLine := NewGapBuffer(change.OldData)
b.Lines = append(b.Lines[:change.Line], append([]*GapBuffer{newLine}, b.Lines[change.Line:]...)...)
}
}
b.UndoStack.recording = wasRecording
}
// Restore cursor position
w.SetCursorLine(block.OldCursor.Line)
w.SetCursorCol(block.OldCursor.Col)
return true
}
func (b *Buffer) Redo(w *Window) bool {
if b.UndoStack == nil {
return false
}
block := b.UndoStack.Redo()
if block == nil {
return false
}
// Apply changes in FORWARD order
for _, change := range block.Changes {
// Temporarily disable recording while we redo
wasRecording := b.UndoStack.recording
b.UndoStack.recording = false
switch change.Type {
case SetLineChange:
// Apply new data
if change.Line >= 0 && change.Line < len(b.Lines) {
b.Lines[change.Line].Set(change.NewData)
}
case InsertLineChange:
// Re-insert the line
if change.Line <= len(b.Lines) {
newLine := NewGapBuffer(change.NewData)
b.Lines = append(b.Lines[:change.Line], append([]*GapBuffer{newLine}, b.Lines[change.Line:]...)...)
}
case DeleteLineChange:
// Re-delete the line
if change.Line >= 0 && change.Line < len(b.Lines) {
b.Lines = append(b.Lines[:change.Line], b.Lines[change.Line+1:]...)
}
}
b.UndoStack.recording = wasRecording
}
// Restore cursor position
w.SetCursorLine(block.NewCursor.Line)
w.SetCursorCol(block.NewCursor.Col)
return true
}
// ==================================================
// Setters
// ==================================================
@ -211,10 +101,7 @@ func (b *Buffer) SetFiletype(filetype string) {
// Buffer.SetLines: Replace all lines in the buffer with the provided lines.
// This is useful when loading a file or resetting buffer content.
func (b *Buffer) SetLines(lines []string) {
b.Lines = make([]*GapBuffer, len(lines))
for i, line := range lines {
b.Lines[i] = NewGapBuffer(line)
}
b.Lines = lines
}
// Buffer.SetModified: Set the modified flag for this buffer. A modified buffer

View File

@ -12,16 +12,15 @@ type BufferBuilder struct {
func NewBufferBuilder() *BufferBuilder {
return &BufferBuilder{
buffer: Buffer{
Id: 0, // This is set when built
Type: ScatchBuffer, // Default buffer type
Filename: "",
Filetype: "",
Lines: []*GapBuffer{NewEmptyGapBuffer()},
Modified: false,
Loaded: false,
Listed: false,
ReadOnly: false,
UndoStack: NewUndoStack(), // Empty undo stack
Id: 0, // This is set when built
Type: ScatchBuffer, // Default buffer type
Filename: "",
Filetype: "",
Lines: []string{""},
Modified: false,
Loaded: false,
Listed: false,
ReadOnly: false,
},
}
}
@ -40,10 +39,7 @@ func (b *BufferBuilder) WithFiletype(filetype string) *BufferBuilder {
// BufferBuilder.WithLines: Attaches a lines to the buffer that is being built.
func (b *BufferBuilder) WithLines(lines []string) *BufferBuilder {
b.buffer.Lines = make([]*GapBuffer, len(lines))
for i, line := range lines {
b.buffer.Lines[i] = NewGapBuffer(line)
}
b.buffer.Lines = lines
return b
}

View File

@ -5,7 +5,6 @@ import (
)
const CommandOutputExitMessage = "Press ENTER to continue"
const CommandOutputScrollMessage = "Use j/k to scroll"
type CommandOutput struct {
Title string

View File

@ -1,180 +0,0 @@
package core
// GapBuffer represents a single line of text using the gap buffer data structure.
// It maintains a gap (empty space) in the buffer where the cursor is positioned,
// making insertions and deletions at the cursor position very efficient (O(1)).
type GapBuffer struct {
buffer []rune // The underlying buffer containing text and gap
gapStart int // Index where the gap starts
gapEnd int // Index where the gap ends (exclusive)
}
// NewGapBuffer: creates a new gap buffer with the given initial content.
// The gap is positioned at the end of the content.
func NewGapBuffer(content string) *GapBuffer {
runes := []rune(content)
initialCapacity := len(runes) + 16 // Extra space for the gap
buffer := make([]rune, initialCapacity)
copy(buffer, runes)
return &GapBuffer{
buffer: buffer,
gapStart: len(runes),
gapEnd: initialCapacity,
}
}
// NewEmptyGapBuffer: Creates a new empty gap buffer with default capacity.
func NewEmptyGapBuffer() *GapBuffer {
initialCapacity := 16
return &GapBuffer{
buffer: make([]rune, initialCapacity),
gapStart: 0,
gapEnd: initialCapacity,
}
}
// GapBuffer.String: Converts the gap buffer to a string, excluding the gap.
func (gb *GapBuffer) String() string {
result := make([]rune, 0, gb.Len())
result = append(result, gb.buffer[:gb.gapStart]...)
result = append(result, gb.buffer[gb.gapEnd:]...)
return string(result)
}
// GapBuffer.Len: Returns the length of the text (excluding the gap).
func (gb *GapBuffer) Len() int {
return len(gb.buffer) - (gb.gapEnd - gb.gapStart)
}
// GapBuffer.GapSize: Returns the current size of the gap.
func (gb *GapBuffer) GapSize() int {
return gb.gapEnd - gb.gapStart
}
// GapBuffer.Insert: Inserts a string at the specified position.
// This moves the gap to the position first, then inserts the text.
func (gb *GapBuffer) Insert(pos int, text string) {
if pos < 0 || pos > gb.Len() {
return // Invalid position
}
runes := []rune(text)
if len(runes) == 0 {
return
}
// Move gap to insertion position
gb.moveGap(pos)
// Ensure gap is large enough
if gb.GapSize() < len(runes) {
gb.grow(len(runes))
}
// Insert runes at gap start
copy(gb.buffer[gb.gapStart:], runes)
gb.gapStart += len(runes)
}
// GapBuffer.Delete: Deletes count runes starting at position pos.
func (gb *GapBuffer) Delete(pos, count int) {
if pos < 0 || pos >= gb.Len() || count <= 0 {
return
}
// Clamp count to not exceed buffer length
if pos+count > gb.Len() {
count = gb.Len() - pos
}
// Move gap to deletion position
gb.moveGap(pos)
// Expand gap to absorb deleted characters
gb.gapEnd += count
}
// GapBuffer.RuneAt: Returns the rune at the specified position.
func (gb *GapBuffer) RuneAt(pos int) rune {
if pos < 0 || pos >= gb.Len() {
return 0
}
if pos < gb.gapStart {
return gb.buffer[pos]
}
return gb.buffer[pos+gb.GapSize()]
}
// GapBuffer.Substring: Returns a substring from start to end (exclusive).
func (gb *GapBuffer) Substring(start, end int) string {
if start < 0 {
start = 0
}
if end > gb.Len() {
end = gb.Len()
}
if start >= end {
return ""
}
result := make([]rune, 0, end-start)
for i := start; i < end; i++ {
result = append(result, gb.RuneAt(i))
}
return string(result)
}
// GapBuffer.moveGap: Moves the gap to the specified position.
func (gb *GapBuffer) moveGap(pos int) {
if pos < gb.gapStart {
// Move gap left: shift text from [pos, gapStart) to [gapEnd-delta, gapEnd)
delta := gb.gapStart - pos
copy(gb.buffer[gb.gapEnd-delta:gb.gapEnd], gb.buffer[pos:gb.gapStart])
gb.gapStart = pos
gb.gapEnd -= delta
} else if pos > gb.gapStart {
// Move gap right: shift text from [gapEnd, gapEnd+delta) to [gapStart, gapStart+delta)
delta := pos - gb.gapStart
copy(gb.buffer[gb.gapStart:gb.gapStart+delta], gb.buffer[gb.gapEnd:gb.gapEnd+delta])
gb.gapStart += delta
gb.gapEnd += delta
}
}
// GapBuffer.grow: Expands the buffer to accommodate at least minGapSize additional characters.
func (gb *GapBuffer) grow(minGapSize int) {
oldLen := len(gb.buffer)
newGapSize := minGapSize * 2 // Double the required size for future insertions
newLen := oldLen + newGapSize - gb.GapSize()
newBuffer := make([]rune, newLen)
// Copy text before gap
copy(newBuffer, gb.buffer[:gb.gapStart])
// Copy text after gap to new position
newGapEnd := newLen - (oldLen - gb.gapEnd)
copy(newBuffer[newGapEnd:], gb.buffer[gb.gapEnd:])
gb.buffer = newBuffer
gb.gapEnd = newGapEnd
}
// GapBuffer.Set: Replaces the entire content of the gap buffer.
func (gb *GapBuffer) Set(content string) {
runes := []rune(content)
capacity := len(runes) + 16
gb.buffer = make([]rune, capacity)
copy(gb.buffer, runes)
gb.gapStart = len(runes)
gb.gapEnd = capacity
}
// GapBuffer.Clear: Removes all content from the gap buffer.
func (gb *GapBuffer) Clear() {
gb.gapStart = 0
gb.gapEnd = len(gb.buffer)
}

View File

@ -1,455 +0,0 @@
package core
import "testing"
func TestGapBufferString(t *testing.T) {
t.Run("gapBuffer.String() on empty buffer returns \"\"", func(t *testing.T) {
buf := NewEmptyGapBuffer()
str := buf.String()
if str != "" {
t.Fatalf("buf.String() expected '', got '%s'", str)
}
})
t.Run("gapBuffer.String() on string returns the string", func(t *testing.T) {
buf := NewGapBuffer("Hello world")
str := buf.String()
if str != "Hello world" {
t.Fatalf("buf.String() expected 'Hello world', got '%s'", str)
}
})
t.Run("gapBuffer.String() after moving gap returns the string", func(t *testing.T) {
buf := NewGapBuffer("Hello world")
buf.moveGap(6)
str := buf.String()
if str != "Hello world" {
t.Fatalf("buf.String() expected 'Hello world', got '%s'", str)
}
})
t.Run("gapBuffer.String() after growing gap returns the string", func(t *testing.T) {
buf := NewGapBuffer("Hello world")
buf.grow(16)
str := buf.String()
if str != "Hello world" {
t.Fatalf("buf.String() expected 'Hello world', got '%s'", str)
}
})
t.Run("gapBuffer.String() handles unicode characters", func(t *testing.T) {
buf := NewGapBuffer("Hello 世界 🌍")
str := buf.String()
if str != "Hello 世界 🌍" {
t.Fatalf("buf.String() expected 'Hello 世界 🌍', got '%s'", str)
}
})
}
func TestGapBufferLen(t *testing.T) {
t.Run("empty buffer has length 0", func(t *testing.T) {
buf := NewEmptyGapBuffer()
if buf.Len() != 0 {
t.Fatalf("expected length 0, got %d", buf.Len())
}
})
t.Run("buffer with content returns correct length", func(t *testing.T) {
buf := NewGapBuffer("Hello")
if buf.Len() != 5 {
t.Fatalf("expected length 5, got %d", buf.Len())
}
})
t.Run("length with unicode characters", func(t *testing.T) {
buf := NewGapBuffer("Hi 🌍")
if buf.Len() != 4 {
t.Fatalf("expected length 4, got %d", buf.Len())
}
})
}
func TestGapBufferInsert(t *testing.T) {
t.Run("insert at beginning", func(t *testing.T) {
buf := NewGapBuffer("world")
buf.Insert(0, "Hello ")
if buf.String() != "Hello world" {
t.Fatalf("expected 'Hello world', got '%s'", buf.String())
}
})
t.Run("insert at end", func(t *testing.T) {
buf := NewGapBuffer("Hello")
buf.Insert(5, " world")
if buf.String() != "Hello world" {
t.Fatalf("expected 'Hello world', got '%s'", buf.String())
}
})
t.Run("insert in middle", func(t *testing.T) {
buf := NewGapBuffer("Helloworld")
buf.Insert(5, " ")
if buf.String() != "Hello world" {
t.Fatalf("expected 'Hello world', got '%s'", buf.String())
}
})
t.Run("insert into empty buffer", func(t *testing.T) {
buf := NewEmptyGapBuffer()
buf.Insert(0, "Hello")
if buf.String() != "Hello" {
t.Fatalf("expected 'Hello', got '%s'", buf.String())
}
})
t.Run("insert empty string does nothing", func(t *testing.T) {
buf := NewGapBuffer("Hello")
buf.Insert(2, "")
if buf.String() != "Hello" {
t.Fatalf("expected 'Hello', got '%s'", buf.String())
}
})
t.Run("insert at invalid position does nothing", func(t *testing.T) {
buf := NewGapBuffer("Hello")
buf.Insert(-1, "X")
if buf.String() != "Hello" {
t.Fatalf("expected 'Hello', got '%s'", buf.String())
}
buf.Insert(100, "X")
if buf.String() != "Hello" {
t.Fatalf("expected 'Hello', got '%s'", buf.String())
}
})
t.Run("insert unicode characters", func(t *testing.T) {
buf := NewGapBuffer("Hello")
buf.Insert(5, " 世界")
if buf.String() != "Hello 世界" {
t.Fatalf("expected 'Hello 世界', got '%s'", buf.String())
}
})
t.Run("multiple consecutive insertions", func(t *testing.T) {
buf := NewEmptyGapBuffer()
buf.Insert(0, "a")
buf.Insert(1, "b")
buf.Insert(2, "c")
if buf.String() != "abc" {
t.Fatalf("expected 'abc', got '%s'", buf.String())
}
})
t.Run("insert large text triggers grow", func(t *testing.T) {
buf := NewGapBuffer("Hello")
longText := "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
buf.Insert(5, longText)
expected := "Hello" + longText
if buf.String() != expected {
t.Fatalf("expected '%s', got '%s'", expected, buf.String())
}
})
}
func TestGapBufferDelete(t *testing.T) {
t.Run("delete from beginning", func(t *testing.T) {
buf := NewGapBuffer("Hello world")
buf.Delete(0, 6)
if buf.String() != "world" {
t.Fatalf("expected 'world', got '%s'", buf.String())
}
})
t.Run("delete from end", func(t *testing.T) {
buf := NewGapBuffer("Hello world")
buf.Delete(5, 6)
if buf.String() != "Hello" {
t.Fatalf("expected 'Hello', got '%s'", buf.String())
}
})
t.Run("delete from middle", func(t *testing.T) {
buf := NewGapBuffer("Hello world")
buf.Delete(5, 1)
if buf.String() != "Helloworld" {
t.Fatalf("expected 'Helloworld', got '%s'", buf.String())
}
})
t.Run("delete single character", func(t *testing.T) {
buf := NewGapBuffer("Hello")
buf.Delete(1, 1)
if buf.String() != "Hllo" {
t.Fatalf("expected 'Hllo', got '%s'", buf.String())
}
})
t.Run("delete all content", func(t *testing.T) {
buf := NewGapBuffer("Hello")
buf.Delete(0, 5)
if buf.String() != "" {
t.Fatalf("expected '', got '%s'", buf.String())
}
})
t.Run("delete with count exceeding length", func(t *testing.T) {
buf := NewGapBuffer("Hello")
buf.Delete(2, 100)
if buf.String() != "He" {
t.Fatalf("expected 'He', got '%s'", buf.String())
}
})
t.Run("delete at invalid position does nothing", func(t *testing.T) {
buf := NewGapBuffer("Hello")
buf.Delete(-1, 2)
if buf.String() != "Hello" {
t.Fatalf("expected 'Hello', got '%s'", buf.String())
}
buf.Delete(100, 2)
if buf.String() != "Hello" {
t.Fatalf("expected 'Hello', got '%s'", buf.String())
}
})
t.Run("delete with zero count does nothing", func(t *testing.T) {
buf := NewGapBuffer("Hello")
buf.Delete(2, 0)
if buf.String() != "Hello" {
t.Fatalf("expected 'Hello', got '%s'", buf.String())
}
})
t.Run("delete unicode characters", func(t *testing.T) {
buf := NewGapBuffer("Hello 世界")
buf.Delete(6, 2)
if buf.String() != "Hello " {
t.Fatalf("expected 'Hello ', got '%s'", buf.String())
}
})
}
func TestGapBufferRuneAt(t *testing.T) {
t.Run("get rune at valid position", func(t *testing.T) {
buf := NewGapBuffer("Hello")
if buf.RuneAt(0) != 'H' {
t.Fatalf("expected 'H', got '%c'", buf.RuneAt(0))
}
if buf.RuneAt(4) != 'o' {
t.Fatalf("expected 'o', got '%c'", buf.RuneAt(4))
}
})
t.Run("get rune before gap", func(t *testing.T) {
buf := NewGapBuffer("Hello world")
buf.moveGap(6)
if buf.RuneAt(5) != ' ' {
t.Fatalf("expected ' ', got '%c'", buf.RuneAt(5))
}
})
t.Run("get rune after gap", func(t *testing.T) {
buf := NewGapBuffer("Hello world")
buf.moveGap(6)
if buf.RuneAt(6) != 'w' {
t.Fatalf("expected 'w', got '%c'", buf.RuneAt(6))
}
})
t.Run("get rune at invalid position returns 0", func(t *testing.T) {
buf := NewGapBuffer("Hello")
if buf.RuneAt(-1) != 0 {
t.Fatalf("expected 0, got '%c'", buf.RuneAt(-1))
}
if buf.RuneAt(100) != 0 {
t.Fatalf("expected 0, got '%c'", buf.RuneAt(100))
}
})
t.Run("get unicode rune", func(t *testing.T) {
buf := NewGapBuffer("🌍世界")
if buf.RuneAt(0) != '🌍' {
t.Fatalf("expected '🌍', got '%c'", buf.RuneAt(0))
}
if buf.RuneAt(1) != '世' {
t.Fatalf("expected '世', got '%c'", buf.RuneAt(1))
}
})
}
func TestGapBufferSubstring(t *testing.T) {
t.Run("substring from middle", func(t *testing.T) {
buf := NewGapBuffer("Hello world")
if buf.Substring(0, 5) != "Hello" {
t.Fatalf("expected 'Hello', got '%s'", buf.Substring(0, 5))
}
if buf.Substring(6, 11) != "world" {
t.Fatalf("expected 'world', got '%s'", buf.Substring(6, 11))
}
})
t.Run("substring entire content", func(t *testing.T) {
buf := NewGapBuffer("Hello")
if buf.Substring(0, 5) != "Hello" {
t.Fatalf("expected 'Hello', got '%s'", buf.Substring(0, 5))
}
})
t.Run("substring with start >= end returns empty", func(t *testing.T) {
buf := NewGapBuffer("Hello")
if buf.Substring(3, 3) != "" {
t.Fatalf("expected '', got '%s'", buf.Substring(3, 3))
}
if buf.Substring(3, 2) != "" {
t.Fatalf("expected '', got '%s'", buf.Substring(3, 2))
}
})
t.Run("substring clamps to buffer bounds", func(t *testing.T) {
buf := NewGapBuffer("Hello")
if buf.Substring(-5, 3) != "Hel" {
t.Fatalf("expected 'Hel', got '%s'", buf.Substring(-5, 3))
}
if buf.Substring(2, 100) != "llo" {
t.Fatalf("expected 'llo', got '%s'", buf.Substring(2, 100))
}
})
t.Run("substring with gap in middle", func(t *testing.T) {
buf := NewGapBuffer("Hello world")
buf.moveGap(6)
if buf.Substring(0, 11) != "Hello world" {
t.Fatalf("expected 'Hello world', got '%s'", buf.Substring(0, 11))
}
})
t.Run("substring with unicode", func(t *testing.T) {
buf := NewGapBuffer("Hi 世界")
if buf.Substring(3, 5) != "世界" {
t.Fatalf("expected '世界', got '%s'", buf.Substring(3, 5))
}
})
}
func TestGapBufferSet(t *testing.T) {
t.Run("set replaces content", func(t *testing.T) {
buf := NewGapBuffer("Hello")
buf.Set("Goodbye")
if buf.String() != "Goodbye" {
t.Fatalf("expected 'Goodbye', got '%s'", buf.String())
}
})
t.Run("set to empty string", func(t *testing.T) {
buf := NewGapBuffer("Hello")
buf.Set("")
if buf.String() != "" {
t.Fatalf("expected '', got '%s'", buf.String())
}
})
t.Run("set on empty buffer", func(t *testing.T) {
buf := NewEmptyGapBuffer()
buf.Set("Hello")
if buf.String() != "Hello" {
t.Fatalf("expected 'Hello', got '%s'", buf.String())
}
})
t.Run("set resets gap position", func(t *testing.T) {
buf := NewGapBuffer("Hello")
buf.moveGap(2)
buf.Set("World")
if buf.String() != "World" {
t.Fatalf("expected 'World', got '%s'", buf.String())
}
if buf.gapStart != 5 {
t.Fatalf("expected gap start at 5, got %d", buf.gapStart)
}
})
}
func TestGapBufferClear(t *testing.T) {
t.Run("clear removes all content", func(t *testing.T) {
buf := NewGapBuffer("Hello world")
buf.Clear()
if buf.String() != "" {
t.Fatalf("expected '', got '%s'", buf.String())
}
})
t.Run("clear on empty buffer", func(t *testing.T) {
buf := NewEmptyGapBuffer()
buf.Clear()
if buf.String() != "" {
t.Fatalf("expected '', got '%s'", buf.String())
}
})
t.Run("clear resets gap to start", func(t *testing.T) {
buf := NewGapBuffer("Hello")
buf.Clear()
if buf.gapStart != 0 {
t.Fatalf("expected gap start at 0, got %d", buf.gapStart)
}
if buf.gapEnd != len(buf.buffer) {
t.Fatalf("expected gap end at %d, got %d", len(buf.buffer), buf.gapEnd)
}
})
}
func TestGapBufferGapSize(t *testing.T) {
t.Run("gap size on new buffer", func(t *testing.T) {
buf := NewGapBuffer("Hello")
if buf.GapSize() != 16 {
t.Fatalf("expected gap size 16, got %d", buf.GapSize())
}
})
t.Run("gap size on empty buffer", func(t *testing.T) {
buf := NewEmptyGapBuffer()
if buf.GapSize() != 16 {
t.Fatalf("expected gap size 16, got %d", buf.GapSize())
}
})
t.Run("gap size decreases after insert", func(t *testing.T) {
buf := NewGapBuffer("Hello")
initialGapSize := buf.GapSize()
buf.Insert(5, "XX")
if buf.GapSize() != initialGapSize-2 {
t.Fatalf("expected gap size %d, got %d", initialGapSize-2, buf.GapSize())
}
})
}
func TestGapBufferComplex(t *testing.T) {
t.Run("sequence of operations", func(t *testing.T) {
buf := NewEmptyGapBuffer()
buf.Insert(0, "Hello")
buf.Insert(5, " world")
buf.Delete(5, 1)
buf.Insert(5, ", ")
if buf.String() != "Hello, world" {
t.Fatalf("expected 'Hello, world', got '%s'", buf.String())
}
})
t.Run("insert and delete at different positions", func(t *testing.T) {
buf := NewGapBuffer("abcdefgh")
buf.Delete(2, 4)
buf.Insert(2, "XX")
if buf.String() != "abXXgh" {
t.Fatalf("expected 'abXXgh', got '%s'", buf.String())
}
})
t.Run("editing with gap movement", func(t *testing.T) {
buf := NewGapBuffer("Hello world")
buf.Insert(0, ">> ")
buf.Insert(buf.Len(), " <<")
if buf.String() != ">> Hello world <<" {
t.Fatalf("expected '>> Hello world <<', got '%s'", buf.String())
}
})
}

View File

@ -11,15 +11,13 @@ const (
VisualMode
VisualLineMode
VisualBlockMode
ReplaceMode
WaitingMode // Same as NORMAL output, but cursor is the REPLACE cursor
)
// Mode.ToString: Returns a human-readable string representation of the mode
// for display in the status bar.
func (m Mode) ToString() string {
switch m {
case NormalMode, WaitingMode:
case NormalMode:
return "NORMAL"
case InsertMode:
return "INSERT"
@ -31,8 +29,6 @@ func (m Mode) ToString() string {
return "V-LINE"
case VisualBlockMode:
return "V-BLOCK"
case ReplaceMode:
return "REPLACE"
default:
return "-----"
}

View File

@ -82,8 +82,7 @@ func addSpecialRegisters(reg map[rune]Register) {
// Small delete? Expression?
// VIM: Last inserted text (readonly)
// GIM: Content stored for the '.' operator (for debugging)
// Last inserted text (readonly)
reg['.'] = emptyRegister()
// Current file name (readonly)

View File

@ -1,180 +0,0 @@
package core
import (
"fmt"
"slices"
)
type ChangeType string
const (
SetLineChange ChangeType = "SetLine"
InsertLineChange ChangeType = "InsertLine"
DeleteLineChange ChangeType = "DeleteLine"
)
type Change struct {
Type ChangeType
Line int
OldData string
NewData string
}
type ChangeBlock struct {
Changes []Change
OldCursor Position // Before OP
NewCursor Position // After OP
}
type UndoStack struct {
undoStack []ChangeBlock
redoStack []ChangeBlock
current []Change
recording bool
oldCursor Position
}
func NewUndoStack() *UndoStack {
return &UndoStack{
undoStack: []ChangeBlock{},
redoStack: []ChangeBlock{},
current: []Change{},
recording: false,
oldCursor: Position{},
}
}
func (u *UndoStack) BeginBlock(cursor Position) {
u.current = []Change{}
u.recording = true
u.oldCursor = cursor
}
func (u *UndoStack) EndBlock(cursor Position) {
// If not recording or nothing changed, we can exit safely
if !u.recording || len(u.current) == 0 {
return
}
block := ChangeBlock{
Changes: u.current,
OldCursor: u.oldCursor,
NewCursor: cursor,
}
u.undoStack = append(u.undoStack, block)
u.redoStack = []ChangeBlock{} // Reset old changes, can no longer redo
u.recording = false
u.current = []Change{}
}
func (u *UndoStack) RecordSetLine(line int, oldData, newData string) {
if !u.recording {
return
}
change := Change{
Type: SetLineChange,
Line: line,
OldData: oldData,
NewData: newData,
}
u.current = append(u.current, change)
}
func (u *UndoStack) RecordInsertLine(line int, newData string) {
if !u.recording {
return
}
change := Change{
Type: InsertLineChange,
Line: line,
NewData: newData,
}
u.current = append(u.current, change)
}
func (u *UndoStack) RecordDeleteLine(line int, oldData string) {
if !u.recording {
return
}
change := Change{
Type: DeleteLineChange,
Line: line,
OldData: oldData,
}
u.current = append(u.current, change)
}
func (u *UndoStack) Undo() *ChangeBlock {
if len(u.undoStack) == 0 {
return nil
}
// Pop from undo stack
size := len(u.undoStack)
block := u.undoStack[size-1]
u.undoStack = u.undoStack[:size-1]
// Push to redo stack
u.redoStack = append(u.redoStack, block)
return &block
}
func (u *UndoStack) Redo() *ChangeBlock {
if len(u.redoStack) == 0 {
return nil
}
// Pop from redo stack
size := len(u.redoStack)
block := u.redoStack[size-1]
u.redoStack = u.redoStack[:size-1]
// Push to undo stack
u.undoStack = append(u.undoStack, block)
return &block
}
func (u *UndoStack) CanUndo() bool {
return len(u.undoStack) > 0
}
func (u *UndoStack) CanRedo() bool {
return len(u.redoStack) > 0
}
func (u *UndoStack) Recording() bool {
return u.recording
}
func (u *UndoStack) List() []string {
var lines []string
stack := slices.Clone(u.undoStack)
slices.Reverse(stack)
for _, b := range stack {
lines = append(lines, fmt.Sprintf(
"block (%d:%d) -> (%d:%d)",
b.OldCursor.Line,
b.OldCursor.Col,
b.NewCursor.Line,
b.NewCursor.Col,
))
for _, c := range b.Changes {
lines = append(lines, fmt.Sprintf(
"\t%q #%d (%s) -> (%s)",
c.Type,
c.Line,
c.OldData,
c.NewData,
))
}
}
return lines
}

View File

@ -60,7 +60,7 @@ func (w *Window) ClampCursor() {
}
// Clamp column to valid range [0, lineLen]
lineLen := w.Buffer.Lines[w.Cursor.Line].Len()
lineLen := len(w.Buffer.Lines[w.Cursor.Line])
if w.Cursor.Col < 0 {
w.Cursor.Col = 0
} else if lineLen == 0 {

View File

@ -30,20 +30,8 @@ func sendKeys(tm *teatest.TestModel, keys ...string) {
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlU})
case "ctrl+v":
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlV})
case "ctrl+r":
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlR})
case "ctrl+w":
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlW})
case "tab":
tm.Send(tea.KeyMsg{Type: tea.KeyTab})
case "left":
tm.Send(tea.KeyMsg{Type: tea.KeyLeft})
case "right":
tm.Send(tea.KeyMsg{Type: tea.KeyRight})
case "up":
tm.Send(tea.KeyMsg{Type: tea.KeyUp})
case "down":
tm.Send(tea.KeyMsg{Type: tea.KeyDown})
default:
tm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(key)})
}

View File

@ -13,8 +13,8 @@ func TestDeleteChar(t *testing.T) {
sendKeys(tm, "x")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "ello" {
t.Errorf("lines[0] = %q, want 'ello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ello" {
t.Errorf("lines[0] = %q, want 'ello'", m.ActiveBuffer().Lines[0])
}
})
@ -24,8 +24,8 @@ func TestDeleteChar(t *testing.T) {
sendKeys(tm, "x")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "helo" {
t.Errorf("lines[0] = %q, want 'helo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "helo" {
t.Errorf("lines[0] = %q, want 'helo'", m.ActiveBuffer().Lines[0])
}
})
@ -35,8 +35,8 @@ func TestDeleteChar(t *testing.T) {
sendKeys(tm, "x")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hell" {
t.Errorf("lines[0] = %q, want 'hell'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hell" {
t.Errorf("lines[0] = %q, want 'hell'", m.ActiveBuffer().Lines[0])
}
})
@ -46,8 +46,8 @@ func TestDeleteChar(t *testing.T) {
sendKeys(tm, "x", "x")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "llo" {
t.Errorf("lines[0] = %q, want 'llo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "llo" {
t.Errorf("lines[0] = %q, want 'llo'", m.ActiveBuffer().Lines[0])
}
})
}
@ -59,8 +59,8 @@ func TestDeleteCharWithCount(t *testing.T) {
sendKeys(tm, "3", "x")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "lo" {
t.Errorf("lines[0] = %q, want 'lo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "lo" {
t.Errorf("lines[0] = %q, want 'lo'", m.ActiveBuffer().Lines[0])
}
})
@ -70,8 +70,8 @@ func TestDeleteCharWithCount(t *testing.T) {
sendKeys(tm, "1", "0", "x")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -81,8 +81,8 @@ func TestDeleteCharWithCount(t *testing.T) {
sendKeys(tm, "2", "x")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hlo" {
t.Errorf("lines[0] = %q, want 'hlo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hlo" {
t.Errorf("lines[0] = %q, want 'hlo'", m.ActiveBuffer().Lines[0])
}
})
}
@ -94,8 +94,8 @@ func TestDeleteCharEdgeCases(t *testing.T) {
sendKeys(tm, "x")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
@ -108,8 +108,8 @@ func TestDeleteCharEdgeCases(t *testing.T) {
sendKeys(tm, "x")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -119,8 +119,8 @@ func TestDeleteCharEdgeCases(t *testing.T) {
sendKeys(tm, "x")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "a" {
t.Errorf("Line(0) = %q, want 'a'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "a" {
t.Errorf("Line(0) = %q, want 'a'", m.ActiveBuffer().Lines[0])
}
})
@ -130,8 +130,8 @@ func TestDeleteCharEdgeCases(t *testing.T) {
sendKeys(tm, "x")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "ab c" {
t.Errorf("Line(0) = %q, want 'ab c'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ab c" {
t.Errorf("Line(0) = %q, want 'ab c'", m.ActiveBuffer().Lines[0])
}
})
@ -144,8 +144,8 @@ func TestDeleteCharEdgeCases(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[1].String() != "world" {
t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "world" {
t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1])
}
})
@ -155,8 +155,8 @@ func TestDeleteCharEdgeCases(t *testing.T) {
sendKeys(tm, "x", "x", "x")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "lo" {
t.Errorf("Line(0) = %q, want 'lo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "lo" {
t.Errorf("Line(0) = %q, want 'lo'", m.ActiveBuffer().Lines[0])
}
})
@ -166,8 +166,8 @@ func TestDeleteCharEdgeCases(t *testing.T) {
sendKeys(tm, "x")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "ab" {
t.Errorf("Line(0) = %q, want 'ab'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ab" {
t.Errorf("Line(0) = %q, want 'ab'", m.ActiveBuffer().Lines[0])
}
})
@ -177,8 +177,8 @@ func TestDeleteCharEdgeCases(t *testing.T) {
sendKeys(tm, "5", "x")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -188,8 +188,8 @@ func TestDeleteCharEdgeCases(t *testing.T) {
sendKeys(tm, "x")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "abde" {
t.Errorf("Line(0) = %q, want 'abde'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "abde" {
t.Errorf("Line(0) = %q, want 'abde'", m.ActiveBuffer().Lines[0])
}
})
}
@ -201,8 +201,8 @@ func TestDeleteCharBackward(t *testing.T) {
sendKeys(tm, "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "ello" {
t.Errorf("lines[0] = %q, want 'ello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ello" {
t.Errorf("lines[0] = %q, want 'ello'", m.ActiveBuffer().Lines[0])
}
})
@ -212,8 +212,8 @@ func TestDeleteCharBackward(t *testing.T) {
sendKeys(tm, "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "helo" {
t.Errorf("lines[0] = %q, want 'helo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "helo" {
t.Errorf("lines[0] = %q, want 'helo'", m.ActiveBuffer().Lines[0])
}
})
@ -223,8 +223,8 @@ func TestDeleteCharBackward(t *testing.T) {
sendKeys(tm, "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "helo" {
t.Errorf("lines[0] = %q, want 'helo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "helo" {
t.Errorf("lines[0] = %q, want 'helo'", m.ActiveBuffer().Lines[0])
}
})
@ -234,8 +234,8 @@ func TestDeleteCharBackward(t *testing.T) {
sendKeys(tm, "X", "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "llo" {
t.Errorf("lines[0] = %q, want 'llo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "llo" {
t.Errorf("lines[0] = %q, want 'llo'", m.ActiveBuffer().Lines[0])
}
})
}
@ -247,8 +247,8 @@ func TestDeleteCharBackwardWithCount(t *testing.T) {
sendKeys(tm, "3", "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "lo" {
t.Errorf("lines[0] = %q, want 'lo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "lo" {
t.Errorf("lines[0] = %q, want 'lo'", m.ActiveBuffer().Lines[0])
}
})
@ -258,8 +258,8 @@ func TestDeleteCharBackwardWithCount(t *testing.T) {
sendKeys(tm, "1", "0", "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "o" {
t.Errorf("lines[0] = %q, want 'o'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "o" {
t.Errorf("lines[0] = %q, want 'o'", m.ActiveBuffer().Lines[0])
}
})
@ -269,8 +269,8 @@ func TestDeleteCharBackwardWithCount(t *testing.T) {
sendKeys(tm, "2", "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hlo" {
t.Errorf("lines[0] = %q, want 'hlo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hlo" {
t.Errorf("lines[0] = %q, want 'hlo'", m.ActiveBuffer().Lines[0])
}
})
}
@ -282,8 +282,8 @@ func TestDeleteCharBackwardEdgeCases(t *testing.T) {
sendKeys(tm, "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("Line(0) = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("Line(0) = %q, want 'hello'", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
@ -296,8 +296,8 @@ func TestDeleteCharBackwardEdgeCases(t *testing.T) {
sendKeys(tm, "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
@ -310,8 +310,8 @@ func TestDeleteCharBackwardEdgeCases(t *testing.T) {
sendKeys(tm, "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "a" {
t.Errorf("Line(0) = %q, want 'a'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "a" {
t.Errorf("Line(0) = %q, want 'a'", m.ActiveBuffer().Lines[0])
}
})
@ -321,8 +321,8 @@ func TestDeleteCharBackwardEdgeCases(t *testing.T) {
sendKeys(tm, "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "b" {
t.Errorf("Line(0) = %q, want 'b'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "b" {
t.Errorf("Line(0) = %q, want 'b'", m.ActiveBuffer().Lines[0])
}
})
@ -332,8 +332,8 @@ func TestDeleteCharBackwardEdgeCases(t *testing.T) {
sendKeys(tm, "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "ab c" {
t.Errorf("Line(0) = %q, want 'ab c'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ab c" {
t.Errorf("Line(0) = %q, want 'ab c'", m.ActiveBuffer().Lines[0])
}
})
@ -346,8 +346,8 @@ func TestDeleteCharBackwardEdgeCases(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[1].String() != "world" {
t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "world" {
t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1])
}
})
@ -357,8 +357,8 @@ func TestDeleteCharBackwardEdgeCases(t *testing.T) {
sendKeys(tm, "X", "X", "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "lo" {
t.Errorf("Line(0) = %q, want 'lo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "lo" {
t.Errorf("Line(0) = %q, want 'lo'", m.ActiveBuffer().Lines[0])
}
})
@ -368,8 +368,8 @@ func TestDeleteCharBackwardEdgeCases(t *testing.T) {
sendKeys(tm, "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "ab" {
t.Errorf("Line(0) = %q, want 'ab'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ab" {
t.Errorf("Line(0) = %q, want 'ab'", m.ActiveBuffer().Lines[0])
}
})
@ -379,8 +379,8 @@ func TestDeleteCharBackwardEdgeCases(t *testing.T) {
sendKeys(tm, "5", "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "c" {
t.Errorf("Line(0) = %q, want 'c'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "c" {
t.Errorf("Line(0) = %q, want 'c'", m.ActiveBuffer().Lines[0])
}
})
@ -390,8 +390,8 @@ func TestDeleteCharBackwardEdgeCases(t *testing.T) {
sendKeys(tm, "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "abde" {
t.Errorf("Line(0) = %q, want 'abde'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "abde" {
t.Errorf("Line(0) = %q, want 'abde'", m.ActiveBuffer().Lines[0])
}
})
@ -413,8 +413,8 @@ func TestDeleteCharBackwardEdgeCases(t *testing.T) {
sendKeys(tm, "3", "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "ho" {
t.Errorf("Line(0) = %q, want 'ho'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ho" {
t.Errorf("Line(0) = %q, want 'ho'", m.ActiveBuffer().Lines[0])
}
// Cursor should be at position 1 after deleting 3 chars backward from position 4
if m.ActiveWindow().Cursor.Col != 1 {
@ -428,11 +428,11 @@ func TestDeleteCharBackwardEdgeCases(t *testing.T) {
sendKeys(tm, "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("Line(0) = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("Line(0) = %q, want 'hello'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "world" {
t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "world" {
t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1])
}
})
@ -442,8 +442,8 @@ func TestDeleteCharBackwardEdgeCases(t *testing.T) {
sendKeys(tm, "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != " " {
t.Errorf("Line(0) = %q, want ' '", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " " {
t.Errorf("Line(0) = %q, want ' '", m.ActiveBuffer().Lines[0])
}
})
@ -453,8 +453,8 @@ func TestDeleteCharBackwardEdgeCases(t *testing.T) {
sendKeys(tm, "X")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "helo" {
t.Errorf("Line(0) = %q, want 'helo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "helo" {
t.Errorf("Line(0) = %q, want 'helo'", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 3 {
t.Errorf("CursorX() = %d, want 3", m.ActiveWindow().Cursor.Col)
@ -469,8 +469,8 @@ func TestDeleteToEndOfLine(t *testing.T) {
sendKeys(tm, "D")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("Line(0) = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("Line(0) = %q, want 'hello'", m.ActiveBuffer().Lines[0])
}
})
@ -480,8 +480,8 @@ func TestDeleteToEndOfLine(t *testing.T) {
sendKeys(tm, "D")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -491,8 +491,8 @@ func TestDeleteToEndOfLine(t *testing.T) {
sendKeys(tm, "D")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hell" {
t.Errorf("Line(0) = %q, want 'hell'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hell" {
t.Errorf("Line(0) = %q, want 'hell'", m.ActiveBuffer().Lines[0])
}
})
@ -514,8 +514,8 @@ func TestDeleteToEndOfLine(t *testing.T) {
sendKeys(tm, "D")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -528,11 +528,11 @@ func TestDeleteToEndOfLine(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %q, want '3'", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "he" {
t.Errorf("Line(0) = %q, want 'he'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "he" {
t.Errorf("Line(0) = %q, want 'he'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "hi" {
t.Errorf("Line(1) = %q, want 'hi'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "hi" {
t.Errorf("Line(1) = %q, want 'hi'", m.ActiveBuffer().Lines[1])
}
})
@ -545,8 +545,8 @@ func TestDeleteToEndOfLine(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %q, want '1'", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "he" {
t.Errorf("Line(0) = %q, want 'he'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "he" {
t.Errorf("Line(0) = %q, want 'he'", m.ActiveBuffer().Lines[0])
}
})
}
@ -558,8 +558,8 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
sendKeys(tm, "D")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -572,8 +572,8 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[1].String() != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1])
}
})
@ -583,11 +583,11 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
sendKeys(tm, "D")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[2].String() != "line 3" {
t.Errorf("Line(2) = %q, want 'line 3'", m.ActiveBuffer().Lines[2].String())
if m.ActiveBuffer().Lines[2] != "line 3" {
t.Errorf("Line(2) = %q, want 'line 3'", m.ActiveBuffer().Lines[2])
}
})
@ -597,8 +597,8 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
sendKeys(tm, "D")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "he" {
t.Errorf("Line(0) = %q, want 'he'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "he" {
t.Errorf("Line(0) = %q, want 'he'", m.ActiveBuffer().Lines[0])
}
// Cursor should clamp to last char
if m.ActiveWindow().Cursor.Col != 1 {
@ -612,8 +612,8 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
sendKeys(tm, "D")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -623,8 +623,8 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
sendKeys(tm, "D")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != " " {
t.Errorf("Line(0) = %q, want ' '", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " " {
t.Errorf("Line(0) = %q, want ' '", m.ActiveBuffer().Lines[0])
}
})
@ -634,8 +634,8 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
sendKeys(tm, "D")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("Line(0) = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("Line(0) = %q, want 'hello'", m.ActiveBuffer().Lines[0])
}
})
@ -645,8 +645,8 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
sendKeys(tm, "D")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "a" {
t.Errorf("Line(0) = %q, want 'a'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "a" {
t.Errorf("Line(0) = %q, want 'a'", m.ActiveBuffer().Lines[0])
}
})
@ -656,8 +656,8 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
sendKeys(tm, "D")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[1].String() != "world" {
t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "world" {
t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1])
}
})
@ -667,8 +667,8 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
sendKeys(tm, "D")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "first" {
t.Errorf("Line(0) = %q, want 'first'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "first" {
t.Errorf("Line(0) = %q, want 'first'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())

View File

@ -25,8 +25,8 @@ func TestEnterInsert(t *testing.T) {
sendKeys(tm, "i", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "Xhello" {
t.Errorf("lines[0] = %q, want 'Xhello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "Xhello" {
t.Errorf("lines[0] = %q, want 'Xhello'", m.ActiveBuffer().Lines[0])
}
})
@ -36,8 +36,8 @@ func TestEnterInsert(t *testing.T) {
sendKeys(tm, "i", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0])
}
})
@ -70,8 +70,8 @@ func TestEnterInsertAfter(t *testing.T) {
sendKeys(tm, "a", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hXello" {
t.Errorf("lines[0] = %q, want 'hXello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hXello" {
t.Errorf("lines[0] = %q, want 'hXello'", m.ActiveBuffer().Lines[0])
}
})
@ -81,8 +81,8 @@ func TestEnterInsertAfter(t *testing.T) {
sendKeys(tm, "a", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "helXlo" {
t.Errorf("lines[0] = %q, want 'helXlo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "helXlo" {
t.Errorf("lines[0] = %q, want 'helXlo'", m.ActiveBuffer().Lines[0])
}
})
}
@ -94,8 +94,8 @@ func TestEnterInsertLineStart(t *testing.T) {
sendKeys(tm, "I", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "Xhello" {
t.Errorf("lines[0] = %q, want 'Xhello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "Xhello" {
t.Errorf("lines[0] = %q, want 'Xhello'", m.ActiveBuffer().Lines[0])
}
})
@ -105,8 +105,8 @@ func TestEnterInsertLineStart(t *testing.T) {
sendKeys(tm, "I", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "Xhello" {
t.Errorf("lines[0] = %q, want 'Xhello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "Xhello" {
t.Errorf("lines[0] = %q, want 'Xhello'", m.ActiveBuffer().Lines[0])
}
})
}
@ -118,8 +118,8 @@ func TestEnterInsertLineEnd(t *testing.T) {
sendKeys(tm, "A", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "helloX" {
t.Errorf("lines[0] = %q, want 'helloX'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "helloX" {
t.Errorf("lines[0] = %q, want 'helloX'", m.ActiveBuffer().Lines[0])
}
})
@ -129,8 +129,8 @@ func TestEnterInsertLineEnd(t *testing.T) {
sendKeys(tm, "A", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "helloX" {
t.Errorf("lines[0] = %q, want 'helloX'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "helloX" {
t.Errorf("lines[0] = %q, want 'helloX'", m.ActiveBuffer().Lines[0])
}
})
}
@ -147,8 +147,8 @@ func TestOpenLineBelow(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("len(lines) = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[1].String() != "new" {
t.Errorf("lines[1] = %q, want 'new'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "new" {
t.Errorf("lines[1] = %q, want 'new'", m.ActiveBuffer().Lines[1])
}
})
@ -161,8 +161,8 @@ func TestOpenLineBelow(t *testing.T) {
if m.ActiveBuffer().LineCount() != 4 {
t.Errorf("len(lines) = %d, want 4", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[2].String() != "new" {
t.Errorf("lines[2] = %q, want 'new'", m.ActiveBuffer().Lines[2].String())
if m.ActiveBuffer().Lines[2] != "new" {
t.Errorf("lines[2] = %q, want 'new'", m.ActiveBuffer().Lines[2])
}
})
@ -175,8 +175,8 @@ func TestOpenLineBelow(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("len(lines) = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[2].String() != "new" {
t.Errorf("lines[2] = %q, want 'new'", m.ActiveBuffer().Lines[2].String())
if m.ActiveBuffer().Lines[2] != "new" {
t.Errorf("lines[2] = %q, want 'new'", m.ActiveBuffer().Lines[2])
}
})
@ -206,8 +206,8 @@ func TestOpenLineBelowWithCount(t *testing.T) {
t.Errorf("len(lines) = %d, want 4", m.ActiveBuffer().LineCount())
}
for i := 1; i <= 3; i++ {
if m.ActiveBuffer().Lines[i].String() != "x" {
t.Errorf("lines[%d] = %q, want 'x'", i, m.ActiveBuffer().Lines[i].String())
if m.ActiveBuffer().Lines[i] != "x" {
t.Errorf("lines[%d] = %q, want 'x'", i, m.ActiveBuffer().Lines[i])
}
}
})
@ -221,11 +221,11 @@ func TestOpenLineBelowWithCount(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("len(lines) = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[1].String() != "ab" {
t.Errorf("lines[1] = %q, want 'ab'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "ab" {
t.Errorf("lines[1] = %q, want 'ab'", m.ActiveBuffer().Lines[1])
}
if m.ActiveBuffer().Lines[2].String() != "ab" {
t.Errorf("lines[2] = %q, want 'ab'", m.ActiveBuffer().Lines[2].String())
if m.ActiveBuffer().Lines[2] != "ab" {
t.Errorf("lines[2] = %q, want 'ab'", m.ActiveBuffer().Lines[2])
}
})
}
@ -240,8 +240,8 @@ func TestOpenLineAbove(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("len(lines) = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[1].String() != "new" {
t.Errorf("lines[1] = %q, want 'new'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "new" {
t.Errorf("lines[1] = %q, want 'new'", m.ActiveBuffer().Lines[1])
}
})
@ -254,8 +254,8 @@ func TestOpenLineAbove(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("len(lines) = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "new" {
t.Errorf("lines[0] = %q, want 'new'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "new" {
t.Errorf("lines[0] = %q, want 'new'", m.ActiveBuffer().Lines[0])
}
})
@ -282,8 +282,8 @@ func TestOpenLineAboveWithCount(t *testing.T) {
t.Errorf("len(lines) = %d, want 4", m.ActiveBuffer().LineCount())
}
for i := 0; i < 3; i++ {
if m.ActiveBuffer().Lines[i].String() != "x" {
t.Errorf("lines[%d] = %q, want 'x'", i, m.ActiveBuffer().Lines[i].String())
if m.ActiveBuffer().Lines[i] != "x" {
t.Errorf("lines[%d] = %q, want 'x'", i, m.ActiveBuffer().Lines[i])
}
}
})
@ -301,11 +301,11 @@ func TestInsertModeEnter(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("len(lines) = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != " world" {
t.Errorf("lines[1] = %q, want ' world'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != " world" {
t.Errorf("lines[1] = %q, want ' world'", m.ActiveBuffer().Lines[1])
}
})
@ -318,11 +318,11 @@ func TestInsertModeEnter(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("len(lines) = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "" {
t.Errorf("lines[1] = %q, want ''", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "" {
t.Errorf("lines[1] = %q, want ''", m.ActiveBuffer().Lines[1])
}
})
@ -335,11 +335,11 @@ func TestInsertModeEnter(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("len(lines) = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "hello" {
t.Errorf("lines[1] = %q, want 'hello'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "hello" {
t.Errorf("lines[1] = %q, want 'hello'", m.ActiveBuffer().Lines[1])
}
})
}
@ -351,8 +351,8 @@ func TestInsertModeBackspace(t *testing.T) {
sendKeys(tm, "i", "backspace", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "helo" {
t.Errorf("lines[0] = %q, want 'helo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "helo" {
t.Errorf("lines[0] = %q, want 'helo'", m.ActiveBuffer().Lines[0])
}
})
@ -365,8 +365,8 @@ func TestInsertModeBackspace(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("len(lines) = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "helloworld" {
t.Errorf("lines[0] = %q, want 'helloworld'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "helloworld" {
t.Errorf("lines[0] = %q, want 'helloworld'", m.ActiveBuffer().Lines[0])
}
})
@ -376,8 +376,8 @@ func TestInsertModeBackspace(t *testing.T) {
sendKeys(tm, "i", "backspace", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0])
}
})
@ -387,8 +387,8 @@ func TestInsertModeBackspace(t *testing.T) {
sendKeys(tm, "i", "backspace", "backspace", "backspace", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "he" {
t.Errorf("lines[0] = %q, want 'he'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "he" {
t.Errorf("lines[0] = %q, want 'he'", m.ActiveBuffer().Lines[0])
}
})
}
@ -400,8 +400,8 @@ func TestInsertModeDelete(t *testing.T) {
sendKeys(tm, "i", "delete", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "word" {
t.Errorf("lines[0] = %q, want 'word'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "word" {
t.Errorf("lines[0] = %q, want 'word'", m.ActiveBuffer().Lines[0])
}
})
@ -414,8 +414,8 @@ func TestInsertModeDelete(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("len(lines) = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "helloworld" {
t.Errorf("lines[0] = %q, want 'helloworld'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "helloworld" {
t.Errorf("lines[0] = %q, want 'helloworld'", m.ActiveBuffer().Lines[0])
}
})
@ -428,8 +428,8 @@ func TestInsertModeDelete(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("len(lines) = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "world" {
t.Errorf("lines[0] = %q, want 'world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "world" {
t.Errorf("lines[0] = %q, want 'world'", m.ActiveBuffer().Lines[0])
}
})
@ -439,8 +439,8 @@ func TestInsertModeDelete(t *testing.T) {
sendKeys(tm, "i", "delete", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0])
}
})
@ -450,8 +450,8 @@ func TestInsertModeDelete(t *testing.T) {
sendKeys(tm, "i", "delete", "delete", "delete", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "ho" {
t.Errorf("lines[0] = %q, want 'he'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ho" {
t.Errorf("lines[0] = %q, want 'he'", m.ActiveBuffer().Lines[0])
}
})
@ -464,8 +464,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "left", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0])
}
})
@ -475,8 +475,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "right", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0])
}
})
@ -486,11 +486,11 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "up", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "world" {
t.Errorf("lines[1] = %q, want 'world'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "world" {
t.Errorf("lines[1] = %q, want 'world'", m.ActiveBuffer().Lines[1])
}
})
@ -500,11 +500,11 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "down", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "woXrld" {
t.Errorf("lines[1] = %q, want 'woXrld'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "woXrld" {
t.Errorf("lines[1] = %q, want 'woXrld'", m.ActiveBuffer().Lines[1])
}
})
@ -514,8 +514,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "left", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "Xhello" {
t.Errorf("lines[0] = %q, want 'Xhello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "Xhello" {
t.Errorf("lines[0] = %q, want 'Xhello'", m.ActiveBuffer().Lines[0])
}
})
@ -525,8 +525,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "a", "right", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "helloX" {
t.Errorf("lines[0] = %q, want 'helloX'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "helloX" {
t.Errorf("lines[0] = %q, want 'helloX'", m.ActiveBuffer().Lines[0])
}
})
@ -536,8 +536,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "up", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0])
}
})
@ -547,8 +547,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "down", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0])
}
})
@ -558,8 +558,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "up", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hiX" {
t.Errorf("lines[0] = %q, want 'hiX'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hiX" {
t.Errorf("lines[0] = %q, want 'hiX'", m.ActiveBuffer().Lines[0])
}
})
@ -569,8 +569,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "down", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[1].String() != "hiX" {
t.Errorf("lines[1] = %q, want 'hiX'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "hiX" {
t.Errorf("lines[1] = %q, want 'hiX'", m.ActiveBuffer().Lines[1])
}
})
@ -580,8 +580,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "right", "right", "down", "X", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[1].String() != "woXrld" {
t.Errorf("lines[1] = %q, want 'woXrld'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "woXrld" {
t.Errorf("lines[1] = %q, want 'woXrld'", m.ActiveBuffer().Lines[1])
}
})
}
@ -593,8 +593,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello " {
t.Errorf("lines[0] = %q, want 'hello '", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello " {
t.Errorf("lines[0] = %q, want 'hello '", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 5 {
t.Errorf("CursorX() = %d, want '5'", m.ActiveWindow().Cursor.Col)
@ -607,8 +607,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want '0'", m.ActiveWindow().Cursor.Col)
@ -621,8 +621,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello wo" {
t.Errorf("lines[0] = %q, want 'hello wo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello wo" {
t.Errorf("lines[0] = %q, want 'hello wo'", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 7 {
t.Errorf("CursorX() = %d, want '7'", m.ActiveWindow().Cursor.Col)
@ -655,8 +655,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want '1'", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %s, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %s, want ''", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want '0'", m.ActiveWindow().Cursor.Col)
@ -672,8 +672,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "i", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
@ -686,8 +686,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "i", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "lo" {
t.Errorf("lines[0] = %q, want 'lo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "lo" {
t.Errorf("lines[0] = %q, want 'lo'", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
@ -700,8 +700,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "..." {
t.Errorf("lines[0] = %q, want '...'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "..." {
t.Errorf("lines[0] = %q, want '...'", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 2 {
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
@ -714,8 +714,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello\t" {
t.Errorf("lines[0] = %q, want 'hello\\t'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello\t" {
t.Errorf("lines[0] = %q, want 'hello\\t'", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 5 {
t.Errorf("CursorX() = %d, want 5", m.ActiveWindow().Cursor.Col)
@ -731,8 +731,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "helloworld" {
t.Errorf("lines[0] = %q, want 'helloworld'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "helloworld" {
t.Errorf("lines[0] = %q, want 'helloworld'", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
@ -748,8 +748,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)

View File

@ -176,7 +176,7 @@ func TestMoveToLineEnd(t *testing.T) {
sendKeys(tm, "$")
m := getFinalModel(t, tm)
want := len(lines[0]) - 1
want := len(lines[0])
if m.ActiveWindow().Cursor.Col != want {
t.Errorf("CursorX() = %d, want %d", m.ActiveWindow().Cursor.Col, want)
}
@ -188,7 +188,7 @@ func TestMoveToLineEnd(t *testing.T) {
sendKeys(tm, "$")
m := getFinalModel(t, tm)
want := len(lines[0]) - 1
want := len(lines[0])
if m.ActiveWindow().Cursor.Col != want {
t.Errorf("CursorX() = %d, want %d", m.ActiveWindow().Cursor.Col, want)
}
@ -200,7 +200,7 @@ func TestMoveToLineEnd(t *testing.T) {
sendKeys(tm, "$")
m := getFinalModel(t, tm)
want := len(lines[0]) - 1
want := len(lines[0])
if m.ActiveWindow().Cursor.Col != want {
t.Errorf("CursorX() = %d, want %d", m.ActiveWindow().Cursor.Col, want)
}
@ -633,8 +633,8 @@ func TestMoveToColumnWithOperator(t *testing.T) {
// Deletes from column 1 to current position (exclusive), so "hello" deleted
// Result depends on inclusive/exclusive behavior
// In Vim: d| from col 5 deletes chars 0-4, leaving " world"
if m.ActiveBuffer().Lines[0].String() != " world" {
t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " world" {
t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0])
}
})
@ -646,8 +646,8 @@ func TestMoveToColumnWithOperator(t *testing.T) {
m := getFinalModel(t, tm)
// Deletes from cursor (0) to column 5 (index 4), so "hell" deleted
// Result: "o world"
if m.ActiveBuffer().Lines[0].String() != "o world" {
t.Errorf("Line(0) = %q, want 'o world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "o world" {
t.Errorf("Line(0) = %q, want 'o world'", m.ActiveBuffer().Lines[0])
}
})
@ -720,8 +720,8 @@ func TestMoveToColumnInVisualMode(t *testing.T) {
m := getFinalModel(t, tm)
// Visual selection from 0 to 4 inclusive, delete "hello"
if m.ActiveBuffer().Lines[0].String() != " world" {
t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " world" {
t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0])
}
})
}

View File

@ -928,8 +928,8 @@ func TestMoveForwardWORDWithOperator(t *testing.T) {
m := getFinalModel(t, tm)
// Should delete "hello.world " (including trailing space)
if m.ActiveBuffer().Lines[0].String() != "next" {
t.Errorf("Line(0) = %q, want 'next'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "next" {
t.Errorf("Line(0) = %q, want 'next'", m.ActiveBuffer().Lines[0])
}
})
@ -946,11 +946,11 @@ func TestMoveForwardWORDWithOperator(t *testing.T) {
sendKeys(tm2, "d", "w")
m2 := getFinalModel(t, tm2)
if m1.ActiveBuffer().Lines[0].String() != "next" {
t.Errorf("dW: Line(0) = %q, want 'next'", m1.ActiveBuffer().Lines[0].String())
if m1.ActiveBuffer().Lines[0] != "next" {
t.Errorf("dW: Line(0) = %q, want 'next'", m1.ActiveBuffer().Lines[0])
}
if m2.ActiveBuffer().Lines[0].String() != ".world next" {
t.Errorf("dw: Line(0) = %q, want '.world next'", m2.ActiveBuffer().Lines[0].String())
if m2.ActiveBuffer().Lines[0] != ".world next" {
t.Errorf("dw: Line(0) = %q, want '.world next'", m2.ActiveBuffer().Lines[0])
}
})
@ -960,8 +960,8 @@ func TestMoveForwardWORDWithOperator(t *testing.T) {
sendKeys(tm, "d", "2", "W")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "three four" {
t.Errorf("Line(0) = %q, want 'three four'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "three four" {
t.Errorf("Line(0) = %q, want 'three four'", m.ActiveBuffer().Lines[0])
}
})
@ -1028,8 +1028,8 @@ func TestMoveForwardWORDInVisualMode(t *testing.T) {
m := getFinalModel(t, tm)
// Visual selection from 0 to 12, delete "hello.world "
if m.ActiveBuffer().Lines[0].String() != "ext" {
t.Errorf("Line(0) = %q, want 'ext'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ext" {
t.Errorf("Line(0) = %q, want 'ext'", m.ActiveBuffer().Lines[0])
}
})
@ -1557,8 +1557,8 @@ func TestMoveForwardWORDEndWithOperator(t *testing.T) {
m := getFinalModel(t, tm)
// Should delete "hello.world" leaving " next"
if m.ActiveBuffer().Lines[0].String() != " next" {
t.Errorf("Line(0) = %q, want ' next'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " next" {
t.Errorf("Line(0) = %q, want ' next'", m.ActiveBuffer().Lines[0])
}
})
@ -1570,8 +1570,8 @@ func TestMoveForwardWORDEndWithOperator(t *testing.T) {
m1 := getFinalModel(t, tm1)
// 'de' should delete "hello" leaving ".world next"
if m1.ActiveBuffer().Lines[0].String() != ".world next" {
t.Errorf("'de': Line(0) = %q, want '.world next'", m1.ActiveBuffer().Lines[0].String())
if m1.ActiveBuffer().Lines[0] != ".world next" {
t.Errorf("'de': Line(0) = %q, want '.world next'", m1.ActiveBuffer().Lines[0])
}
// Now test 'dE'
@ -1580,8 +1580,8 @@ func TestMoveForwardWORDEndWithOperator(t *testing.T) {
m2 := getFinalModel(t, tm2)
// 'dE' should delete "hello.world" leaving " next"
if m2.ActiveBuffer().Lines[0].String() != " next" {
t.Errorf("'dE': Line(0) = %q, want ' next'", m2.ActiveBuffer().Lines[0].String())
if m2.ActiveBuffer().Lines[0] != " next" {
t.Errorf("'dE': Line(0) = %q, want ' next'", m2.ActiveBuffer().Lines[0])
}
})
@ -1592,8 +1592,8 @@ func TestMoveForwardWORDEndWithOperator(t *testing.T) {
m := getFinalModel(t, tm)
// Should delete "one.a two.b" leaving " three"
if m.ActiveBuffer().Lines[0].String() != " three" {
t.Errorf("Line(0) = %q, want ' three'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " three" {
t.Errorf("Line(0) = %q, want ' three'", m.ActiveBuffer().Lines[0])
}
})
@ -1653,8 +1653,8 @@ func TestMoveForwardWORDEndInVisualMode(t *testing.T) {
m := getFinalModel(t, tm)
// Should delete "hello.world" leaving " next"
if m.ActiveBuffer().Lines[0].String() != " next" {
t.Errorf("Line(0) = %q, want ' next'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " next" {
t.Errorf("Line(0) = %q, want ' next'", m.ActiveBuffer().Lines[0])
}
})
@ -1685,816 +1685,3 @@ func TestMoveForwardWORDEndInVisualMode(t *testing.T) {
}
})
}
// --- B Motion Tests ---
func TestMoveBackwardWORD(t *testing.T) {
t.Run("test 'B' moves backward one WORD", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "B")
m := getFinalModel(t, tm)
// Should move to start of "world" (index 6)
if m.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'BB' moves backward two WORDs", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "B", "B")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test '2B' moves backward two WORDs with count", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "2", "B")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'B' on punctuation-heavy text", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "B")
m := getFinalModel(t, tm)
// "hello.world" is one WORD, should move to "next" (index 12)
if m.ActiveWindow().Cursor.Col != 12 {
t.Errorf("CursorX() = %d, want 12", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'B' vs 'b' on punctuation", func(t *testing.T) {
lines := []string{"hello.world next"}
// Test 'b'
tm1 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm1, "b")
m1 := getFinalModel(t, tm1)
// 'b' moves to start of "next" (index 12)
if m1.ActiveWindow().Cursor.Col != 12 {
t.Errorf("'b': CursorX() = %d, want 12", m1.ActiveWindow().Cursor.Col)
}
// Test 'B'
tm2 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm2, "B")
m2 := getFinalModel(t, tm2)
// 'B' treats "hello.world" as one WORD, moves to index 12
if m2.ActiveWindow().Cursor.Col != 12 {
t.Errorf("'B': CursorX() = %d, want 12", m2.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'B' crosses lines backward", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 1})
sendKeys(tm, "B")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Line != 1 {
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'B' at beginning of file", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLines(t, lines) // Cursor at 0,0
sendKeys(tm, "B")
m := getFinalModel(t, tm)
// Should stay at 0,0
if m.ActiveWindow().Cursor.Col != 0 || m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("Cursor = (%d,%d), want (0,0)", m.ActiveWindow().Cursor.Col, m.ActiveWindow().Cursor.Line)
}
})
t.Run("test 'B' with multiple spaces", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 13, Line: 0})
sendKeys(tm, "B")
m := getFinalModel(t, tm)
// Should skip spaces and move to "hello"
if m.ActiveWindow().Cursor.Col != 9 {
t.Errorf("CursorX() = %d, want 9", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'B' on empty lines", func(t *testing.T) {
lines := []string{"hello", "", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 2})
sendKeys(tm, "B")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Line != 2 {
t.Errorf("CursorY() = %d, want 2", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'B' complex code-like text", func(t *testing.T) {
lines := []string{"foo.bar(baz) next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 16, Line: 0})
sendKeys(tm, "B")
m := getFinalModel(t, tm)
// "foo.bar(baz)" is one WORD, should move to "next" (index 13)
if m.ActiveWindow().Cursor.Col != 13 {
t.Errorf("CursorX() = %d, want 13", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'BB' complex code-like text", func(t *testing.T) {
lines := []string{"foo.bar(baz) next.thing"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 22, Line: 0})
sendKeys(tm, "B", "B")
m := getFinalModel(t, tm)
// Should move to start of first WORD "foo.bar(baz)" (index 0)
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
}
})
}
func TestMoveBackwardWORDWithOperator(t *testing.T) {
t.Run("test 'dB' deletes backward WORD including punctuation", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "d", "B")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello.world t" {
t.Errorf("Line(0) = %q, want 'hello.world t'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'dB' vs 'db' on dotted text", func(t *testing.T) {
lines := []string{"hello.world next"}
// First test 'db'
tm1 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm1, "d", "b")
m1 := getFinalModel(t, tm1)
if m1.ActiveBuffer().Lines[0].String() != "hello.world t" {
t.Errorf("'db': Line(0) = %q, want 'hello.world t'", m1.ActiveBuffer().Lines[0].String())
}
// Now test 'dB'
tm2 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm2, "d", "B")
m2 := getFinalModel(t, tm2)
if m2.ActiveBuffer().Lines[0].String() != "hello.world t" {
t.Errorf("'dB': Line(0) = %q, want 'hello.world t'", m2.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'd2B' deletes two WORDs backward", func(t *testing.T) {
lines := []string{"one.a two.b three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 16, Line: 0})
sendKeys(tm, "d", "2", "B")
m := getFinalModel(t, tm)
// Should delete "two.b three" leaving "one.a e"
if m.ActiveBuffer().Lines[0].String() != "one.a e" {
t.Errorf("Line(0) = %q, want 'one.a e'", m.ActiveBuffer().Lines[0].String())
}
})
// BUG: This is a failing tests, cursor is not moving at start of yank
t.Run("test 'yB' yanks WORD including punctuation", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "y", "B")
m := getFinalModel(t, tm)
// Text should remain unchanged
if m.ActiveBuffer().Lines[0].String() != "hello.world next" {
t.Errorf("Line(0) = %q, want 'hello.world next'", m.ActiveBuffer().Lines[0].String())
}
// Cursor should be at start of yanked region (index 12)
if m.ActiveWindow().Cursor.Col != 12 {
t.Errorf("CursorX() = %d, want 12", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'cB' changes WORD backward", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "c", "B")
m := getFinalModel(t, tm)
// Should delete " next" and enter insert mode
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "hello.world t" {
t.Errorf("Line(0) = %q, want 'hello.world t'", m.ActiveBuffer().Lines[0].String())
}
})
}
func TestMoveBackwardWORDVisualMode(t *testing.T) {
t.Run("test 'vB' selects backward WORD", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "v", "B")
m := getFinalModel(t, tm)
if m.Mode() != core.VisualMode {
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
}
// Cursor at start of "next" (index 12)
if m.ActiveWindow().Cursor.Col != 12 {
t.Errorf("CursorX() = %d, want 12", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'vBd' deletes selected WORD", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "v", "B", "d")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello.world " {
t.Errorf("Line(0) = %q, want 'hello.world '", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'v2B' selects backward two WORDs", func(t *testing.T) {
lines := []string{"foo.bar baz.qux rest"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 19, Line: 0})
sendKeys(tm, "v", "2", "B")
m := getFinalModel(t, tm)
// Cursor at start of "baz.qux" (index 8)
if m.ActiveWindow().Cursor.Col != 8 {
t.Errorf("CursorX() = %d, want 8", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'VB' in visual line mode", func(t *testing.T) {
lines := []string{"hello.world", "next line"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 1})
sendKeys(tm, "V", "B")
m := getFinalModel(t, tm)
if m.Mode() != core.VisualLineMode {
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
}
// Should select both lines
if m.ActiveWindow().Cursor.Line != 1 {
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
}
})
}
// --- ge Motion Tests ---
func TestMoveBackwardWordEnd(t *testing.T) {
t.Run("test 'ge' moves backward to previous word end", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should move to end of "hello" (index 4)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gege' moves backward two word ends", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "g", "e", "g", "e")
m := getFinalModel(t, tm)
// Should move to end of "one" (index 2)
if m.ActiveWindow().Cursor.Col != 2 {
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test '2ge' moves backward two word ends with count", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "2", "g", "e")
m := getFinalModel(t, tm)
// Should move to end of "one" (index 2)
if m.ActiveWindow().Cursor.Col != 2 {
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'ge' on punctuation-heavy text", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should move to end of "world" (index 10)
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'ge' crosses lines backward", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should move to end of "hello" on previous line (index 4)
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'ge' at beginning of file", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLines(t, lines) // Cursor at 0,0
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should stay at 0,0
if m.ActiveWindow().Cursor.Col != 0 || m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("Cursor = (%d,%d), want (0,0)", m.ActiveWindow().Cursor.Col, m.ActiveWindow().Cursor.Line)
}
})
t.Run("test 'ge' with multiple spaces", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 13, Line: 0})
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should skip spaces and move to end of "hello" (index 4)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'ge' on empty lines", func(t *testing.T) {
lines := []string{"hello", "", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should skip empty line and move to end of "hello" (index 4)
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'ge' respects word classes", func(t *testing.T) {
lines := []string{"foo-bar baz"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should move to end of "bar" (index 6)
if m.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'ge' on method chain", func(t *testing.T) {
lines := []string{"obj.method().chain() next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 24, Line: 0})
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should move to end of ")" (index 19)
if m.ActiveWindow().Cursor.Col != 19 {
t.Errorf("CursorX() = %d, want 19", m.ActiveWindow().Cursor.Col)
}
})
}
func TestMoveBackwardWordEndWithOperator(t *testing.T) {
t.Run("test 'dge' deletes backward to word end", func(t *testing.T) {
lines := []string{"hello world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "d", "g", "e")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello worl" {
t.Errorf("Line(0) = %q, want 'hello worl'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'd2ge' deletes backward two word ends", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "d", "2", "g", "e")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "on" {
t.Errorf("Line(0) = %q, want 'on'", m.ActiveBuffer().Lines[0].String())
}
})
// BUG: This is a failing tests, cursor is not moving at start of yank
t.Run("test 'yge' yanks backward to word end", func(t *testing.T) {
lines := []string{"hello world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "y", "g", "e")
m := getFinalModel(t, tm)
// Text should remain unchanged
if m.ActiveBuffer().Lines[0].String() != "hello world next" {
t.Errorf("Line(0) = %q, want 'hello world next'", m.ActiveBuffer().Lines[0].String())
}
// Cursor should be at end of "world" (index 10)
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'cge' changes backward to word end", func(t *testing.T) {
lines := []string{"hello world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "c", "g", "e")
m := getFinalModel(t, tm)
// Should delete and enter insert mode
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "hello worl" {
t.Errorf("Line(0) = %q, want 'hello worl'", m.ActiveBuffer().Lines[0].String())
}
})
}
func TestMoveBackwardWordEndVisualMode(t *testing.T) {
t.Run("test 'vge' selects backward to word end", func(t *testing.T) {
lines := []string{"hello world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "v", "g", "e")
m := getFinalModel(t, tm)
if m.Mode() != core.VisualMode {
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
}
// Cursor at end of "world" (index 10)
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'vged' deletes selection", func(t *testing.T) {
lines := []string{"hello world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "v", "g", "e", "d")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello worl" {
t.Errorf("Line(0) = %q, want 'hello worl'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'v2ge' selects backward two word ends", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "v", "2", "g", "e")
m := getFinalModel(t, tm)
// Cursor at end of "one" (index 2)
if m.ActiveWindow().Cursor.Col != 2 {
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'Vge' in visual line mode", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "V", "g", "e")
m := getFinalModel(t, tm)
if m.Mode() != core.VisualLineMode {
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
}
// Should select both lines
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
}
// --- gE Motion Tests ---
func TestMoveBackwardWORDEnd(t *testing.T) {
t.Run("test 'gE' moves backward to previous WORD end", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// Should move to end of "hello" (index 4)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gEgE' moves backward two WORD ends", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "g", "E", "g", "E")
m := getFinalModel(t, tm)
// Should move to end of "one" (index 2)
if m.ActiveWindow().Cursor.Col != 2 {
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test '2gE' moves backward two WORD ends with count", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "2", "g", "E")
m := getFinalModel(t, tm)
// Should move to end of "one" (index 2)
if m.ActiveWindow().Cursor.Col != 2 {
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gE' on punctuation-heavy text", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// "hello.world" is one WORD ending at index 10
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gE' vs 'ge' on punctuation", func(t *testing.T) {
lines := []string{"hello.world next"}
// Test 'ge'
tm1 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm1, "g", "e")
m1 := getFinalModel(t, tm1)
// 'ge' treats punctuation as separate word
if m1.ActiveWindow().Cursor.Col != 10 {
t.Errorf("'ge': CursorX() = %d, want 10", m1.ActiveWindow().Cursor.Col)
}
// Test 'gE'
tm2 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm2, "g", "E")
m2 := getFinalModel(t, tm2)
// 'gE' treats "hello.world" as one WORD
if m2.ActiveWindow().Cursor.Col != 10 {
t.Errorf("'gE': CursorX() = %d, want 10", m2.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gE' crosses lines backward", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// Should move to end of "hello" on previous line (index 4)
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gE' at beginning of file", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLines(t, lines) // Cursor at 0,0
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// Should stay at 0,0
if m.ActiveWindow().Cursor.Col != 0 || m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("Cursor = (%d,%d), want (0,0)", m.ActiveWindow().Cursor.Col, m.ActiveWindow().Cursor.Line)
}
})
t.Run("test 'gE' with multiple spaces", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 13, Line: 0})
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// Should skip spaces and move to end of "hello" (index 4)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gE' on empty lines", func(t *testing.T) {
lines := []string{"hello", "", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// Should skip empty line and move to end of "hello" (index 4)
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gE' complex code-like text", func(t *testing.T) {
lines := []string{"foo.bar(baz) next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 16, Line: 0})
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// "foo.bar(baz)" is one WORD ending at index 11
if m.ActiveWindow().Cursor.Col != 11 {
t.Errorf("CursorX() = %d, want 11", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gE' on method chain", func(t *testing.T) {
lines := []string{"obj.method().chain() next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 24, Line: 0})
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// Entire chain is one WORD, ends at index 19
if m.ActiveWindow().Cursor.Col != 19 {
t.Errorf("CursorX() = %d, want 19", m.ActiveWindow().Cursor.Col)
}
})
}
func TestMoveBackwardWORDEndWithOperator(t *testing.T) {
t.Run("test 'dgE' deletes backward to WORD end", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "d", "g", "E")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello.worl" {
t.Errorf("Line(0) = %q, want 'hello.worl'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'dgE' vs 'dge' on dotted text", func(t *testing.T) {
lines := []string{"hello.world next"}
// First test 'dge'
tm1 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm1, "d", "g", "e")
m1 := getFinalModel(t, tm1)
// 'dge' should delete to end of "world"
if m1.ActiveBuffer().Lines[0].String() != "hello.worl" {
t.Errorf("'dge': Line(0) = %q, want 'hello.worl'", m1.ActiveBuffer().Lines[0].String())
}
// Now test 'dgE'
tm2 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm2, "d", "g", "E")
m2 := getFinalModel(t, tm2)
if m2.ActiveBuffer().Lines[0].String() != "hello.worl" {
t.Errorf("'dgE': Line(0) = %q, want 'hello.worl'", m2.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'd2gE' deletes backward two WORD ends", func(t *testing.T) {
lines := []string{"one.a two.b three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 16, Line: 0})
sendKeys(tm, "d", "2", "g", "E")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "one." {
t.Errorf("Line(0) = %q, want 'one.'", m.ActiveBuffer().Lines[0].String())
}
})
// BUG: This is a failing tests, cursor is not moving at start of yank
t.Run("test 'ygE' yanks backward to WORD end", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "y", "g", "E")
m := getFinalModel(t, tm)
// Text should remain unchanged
if m.ActiveBuffer().Lines[0].String() != "hello.world next" {
t.Errorf("Line(0) = %q, want 'hello.world next'", m.ActiveBuffer().Lines[0].String())
}
// Cursor should be at end of "hello.world" (index 10)
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'cgE' changes backward to WORD end", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "c", "g", "E")
m := getFinalModel(t, tm)
// Should delete and enter insert mode
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "hello.worl" {
t.Errorf("Line(0) = %q, want 'hello.world t'", m.ActiveBuffer().Lines[0].String())
}
})
}
func TestMoveBackwardWORDEndVisualMode(t *testing.T) {
t.Run("test 'vgE' selects backward to WORD end", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "v", "g", "E")
m := getFinalModel(t, tm)
if m.Mode() != core.VisualMode {
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
}
// Cursor at end of "hello.world" (index 10)
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'vgEd' deletes selection", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "v", "g", "E", "d")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello.worl" {
t.Errorf("Line(0) = %q, want 'hello.worl'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'v2gE' selects backward two WORD ends", func(t *testing.T) {
lines := []string{"foo.bar baz.qux rest"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 19, Line: 0})
sendKeys(tm, "v", "2", "g", "E")
m := getFinalModel(t, tm)
// Cursor at end of "foo.bar" (index 6)
if m.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'VgE' in visual line mode", func(t *testing.T) {
lines := []string{"hello.world", "next line"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "V", "g", "E")
m := getFinalModel(t, tm)
if m.Mode() != core.VisualLineMode {
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
}
// Should select both lines
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
}

View File

@ -26,11 +26,11 @@ func TestChangeLine(t *testing.T) {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
// First line should be empty (ready for insert)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "world" {
t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "world" {
t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1])
}
})
@ -45,14 +45,14 @@ func TestChangeLine(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "line one" {
t.Errorf("Line(0) = %q, want 'line one'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line one" {
t.Errorf("Line(0) = %q, want 'line one'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1])
}
if m.ActiveBuffer().Lines[2].String() != "line three" {
t.Errorf("Line(2) = %q, want 'line three'", m.ActiveBuffer().Lines[2].String())
if m.ActiveBuffer().Lines[2] != "line three" {
t.Errorf("Line(2) = %q, want 'line three'", m.ActiveBuffer().Lines[2])
}
})
@ -67,11 +67,11 @@ func TestChangeLine(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("Line(0) = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("Line(0) = %q, want 'hello'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1])
}
})
@ -109,8 +109,8 @@ func TestChangeLine(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -148,11 +148,11 @@ func TestChangeLineWithCount(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line three" {
t.Errorf("Line(1) = %q, want 'line three'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line three" {
t.Errorf("Line(1) = %q, want 'line three'", m.ActiveBuffer().Lines[1])
}
})
@ -171,14 +171,14 @@ func TestChangeLineWithCount(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "one" {
t.Errorf("Line(0) = %q, want 'one'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "one" {
t.Errorf("Line(0) = %q, want 'one'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1])
}
if m.ActiveBuffer().Lines[2].String() != "five" {
t.Errorf("Line(2) = %q, want 'five'", m.ActiveBuffer().Lines[2].String())
if m.ActiveBuffer().Lines[2] != "five" {
t.Errorf("Line(2) = %q, want 'five'", m.ActiveBuffer().Lines[2])
}
})
@ -196,8 +196,8 @@ func TestChangeLineWithCount(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -235,8 +235,8 @@ func TestChangeWithHorizontalMotion(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "ello world" {
t.Errorf("Line(0) = %q, want 'ello world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ello world" {
t.Errorf("Line(0) = %q, want 'ello world'", m.ActiveBuffer().Lines[0])
}
})
@ -251,8 +251,8 @@ func TestChangeWithHorizontalMotion(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "llo world" {
t.Errorf("Line(0) = %q, want 'llo world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "llo world" {
t.Errorf("Line(0) = %q, want 'llo world'", m.ActiveBuffer().Lines[0])
}
})
@ -267,8 +267,8 @@ func TestChangeWithHorizontalMotion(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "hell world" {
t.Errorf("Line(0) = %q, want 'hell world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hell world" {
t.Errorf("Line(0) = %q, want 'hell world'", m.ActiveBuffer().Lines[0])
}
})
@ -283,8 +283,8 @@ func TestChangeWithHorizontalMotion(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "hello " {
t.Errorf("Line(0) = %q, want 'hello '", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello " {
t.Errorf("Line(0) = %q, want 'hello '", m.ActiveBuffer().Lines[0])
}
})
@ -299,8 +299,8 @@ func TestChangeWithHorizontalMotion(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "world" {
t.Errorf("Line(0) = %q, want 'world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "world" {
t.Errorf("Line(0) = %q, want 'world'", m.ActiveBuffer().Lines[0])
}
})
@ -317,8 +317,8 @@ func TestChangeWithHorizontalMotion(t *testing.T) {
}
// ^ is exclusive motion, so position 8 (space) is not included
// Delete positions 3-7 ("hello"), leaving " " + " world" = " world"
if m.ActiveBuffer().Lines[0].String() != " world" {
t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " world" {
t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0])
}
})
}
@ -339,8 +339,8 @@ func TestChangeWithWordMotion(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "world" {
t.Errorf("Line(0) = %q, want 'world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "world" {
t.Errorf("Line(0) = %q, want 'world'", m.ActiveBuffer().Lines[0])
}
})
@ -355,8 +355,8 @@ func TestChangeWithWordMotion(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "heworld" {
t.Errorf("Line(0) = %q, want 'heworld'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "heworld" {
t.Errorf("Line(0) = %q, want 'heworld'", m.ActiveBuffer().Lines[0])
}
})
@ -371,8 +371,8 @@ func TestChangeWithWordMotion(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != " world" {
t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " world" {
t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0])
}
})
@ -387,8 +387,8 @@ func TestChangeWithWordMotion(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "world" {
t.Errorf("Line(0) = %q, want 'world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "world" {
t.Errorf("Line(0) = %q, want 'world'", m.ActiveBuffer().Lines[0])
}
})
@ -403,8 +403,8 @@ func TestChangeWithWordMotion(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "three four" {
t.Errorf("Line(0) = %q, want 'three four'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "three four" {
t.Errorf("Line(0) = %q, want 'three four'", m.ActiveBuffer().Lines[0])
}
})
@ -419,8 +419,8 @@ func TestChangeWithWordMotion(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "next" {
t.Errorf("Line(0) = %q, want 'next'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "next" {
t.Errorf("Line(0) = %q, want 'next'", m.ActiveBuffer().Lines[0])
}
})
@ -435,8 +435,8 @@ func TestChangeWithWordMotion(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != " next" {
t.Errorf("Line(0) = %q, want ' next'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " next" {
t.Errorf("Line(0) = %q, want ' next'", m.ActiveBuffer().Lines[0])
}
})
}
@ -461,11 +461,11 @@ func TestChangeWithVerticalMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line three" {
t.Errorf("Line(1) = %q, want 'line three'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line three" {
t.Errorf("Line(1) = %q, want 'line three'", m.ActiveBuffer().Lines[1])
}
})
@ -484,11 +484,11 @@ func TestChangeWithVerticalMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line three" {
t.Errorf("Line(1) = %q, want 'line three'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line three" {
t.Errorf("Line(1) = %q, want 'line three'", m.ActiveBuffer().Lines[1])
}
})
@ -507,11 +507,11 @@ func TestChangeWithVerticalMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "four" {
t.Errorf("Line(1) = %q, want 'four'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "four" {
t.Errorf("Line(1) = %q, want 'four'", m.ActiveBuffer().Lines[1])
}
})
}
@ -536,8 +536,8 @@ func TestChangeWithJumpMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -556,8 +556,8 @@ func TestChangeWithJumpMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -576,11 +576,11 @@ func TestChangeWithJumpMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line one" {
t.Errorf("Line(0) = %q, want 'line one'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line one" {
t.Errorf("Line(0) = %q, want 'line one'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1])
}
})
}
@ -601,8 +601,8 @@ func TestChangeToEndOfLine(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "hello " {
t.Errorf("Line(0) = %q, want 'hello '", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello " {
t.Errorf("Line(0) = %q, want 'hello '", m.ActiveBuffer().Lines[0])
}
})
@ -617,8 +617,8 @@ func TestChangeToEndOfLine(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -634,8 +634,8 @@ func TestChangeToEndOfLine(t *testing.T) {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// Should delete last char
if m.ActiveBuffer().Lines[0].String() != "hell" {
t.Errorf("Line(0) = %q, want 'hell'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hell" {
t.Errorf("Line(0) = %q, want 'hell'", m.ActiveBuffer().Lines[0])
}
})
@ -673,8 +673,8 @@ func TestSubstituteCharacter(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "ello" {
t.Errorf("Line(0) = %q, want 'ello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ello" {
t.Errorf("Line(0) = %q, want 'ello'", m.ActiveBuffer().Lines[0])
}
})
@ -689,8 +689,8 @@ func TestSubstituteCharacter(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "llo" {
t.Errorf("Line(0) = %q, want 'llo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "llo" {
t.Errorf("Line(0) = %q, want 'llo'", m.ActiveBuffer().Lines[0])
}
})
@ -705,8 +705,8 @@ func TestSubstituteCharacter(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "hell" {
t.Errorf("Line(0) = %q, want 'hell'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hell" {
t.Errorf("Line(0) = %q, want 'hell'", m.ActiveBuffer().Lines[0])
}
})
}
@ -727,8 +727,8 @@ func TestSubstituteLine(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -740,14 +740,14 @@ func TestSubstituteLine(t *testing.T) {
sendKeys(tm, "S")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "line one" {
t.Errorf("Line(0) = %q, want 'line one'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line one" {
t.Errorf("Line(0) = %q, want 'line one'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1])
}
if m.ActiveBuffer().Lines[2].String() != "line three" {
t.Errorf("Line(2) = %q, want 'line three'", m.ActiveBuffer().Lines[2].String())
if m.ActiveBuffer().Lines[2] != "line three" {
t.Errorf("Line(2) = %q, want 'line three'", m.ActiveBuffer().Lines[2])
}
})
@ -766,11 +766,11 @@ func TestSubstituteLine(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "three" {
t.Errorf("Line(1) = %q, want 'three'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "three" {
t.Errorf("Line(1) = %q, want 'three'", m.ActiveBuffer().Lines[1])
}
})
}
@ -791,8 +791,8 @@ func TestVisualModeChange(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "ello world" {
t.Errorf("Line(0) = %q, want 'ello world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ello world" {
t.Errorf("Line(0) = %q, want 'ello world'", m.ActiveBuffer().Lines[0])
}
})
@ -807,8 +807,8 @@ func TestVisualModeChange(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != " world" {
t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " world" {
t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0])
}
})
@ -823,8 +823,8 @@ func TestVisualModeChange(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "hello " {
t.Errorf("Line(0) = %q, want 'hello '", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello " {
t.Errorf("Line(0) = %q, want 'hello '", m.ActiveBuffer().Lines[0])
}
})
@ -878,8 +878,8 @@ func TestVisualLineModeChange(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[1].String() != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1])
}
})
@ -898,14 +898,14 @@ func TestVisualLineModeChange(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line one" {
t.Errorf("Line(0) = %q, want 'line one'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line one" {
t.Errorf("Line(0) = %q, want 'line one'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "" {
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1])
}
if m.ActiveBuffer().Lines[2].String() != "line four" {
t.Errorf("Line(2) = %q, want 'line four'", m.ActiveBuffer().Lines[2].String())
if m.ActiveBuffer().Lines[2] != "line four" {
t.Errorf("Line(2) = %q, want 'line four'", m.ActiveBuffer().Lines[2])
}
})
@ -946,8 +946,8 @@ func TestChangeEdgeCases(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -963,8 +963,8 @@ func TestChangeEdgeCases(t *testing.T) {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// cw on last word should change to end of line
if m.ActiveBuffer().Lines[0].String() != "hello " {
t.Errorf("Line(0) = %q, want 'hello '", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello " {
t.Errorf("Line(0) = %q, want 'hello '", m.ActiveBuffer().Lines[0])
}
})
@ -979,8 +979,8 @@ func TestChangeEdgeCases(t *testing.T) {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})

View File

@ -24,8 +24,8 @@ func TestDeleteLine(t *testing.T) {
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want '0'", m.ActiveWindow().Cursor.Line)
}
if m.ActiveBuffer().Lines[0].String() != "world" {
t.Errorf("Line(0) = %s, want 'world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "world" {
t.Errorf("Line(0) = %s, want 'world'", m.ActiveBuffer().Lines[0])
}
})
@ -44,11 +44,11 @@ func TestDeleteLine(t *testing.T) {
if m.ActiveWindow().Cursor.Line != 1 {
t.Errorf("CursorY() = %d, want '1'", m.ActiveWindow().Cursor.Line)
}
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("Line(0) = %s, want 'hello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("Line(0) = %s, want 'hello'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "testing" {
t.Errorf("Line(1) = %s, want 'testing'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "testing" {
t.Errorf("Line(1) = %s, want 'testing'", m.ActiveBuffer().Lines[1])
}
})
@ -67,8 +67,8 @@ func TestDeleteLine(t *testing.T) {
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want '0'", m.ActiveWindow().Cursor.Line)
}
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("Line(0) = %s, want 'hello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("Line(0) = %s, want 'hello'", m.ActiveBuffer().Lines[0])
}
})
@ -87,8 +87,8 @@ func TestDeleteLine(t *testing.T) {
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want '0'", m.ActiveWindow().Cursor.Line)
}
if m.ActiveBuffer().Lines[0].String() != "world" {
t.Errorf("Line(0) = %s, want 'world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "world" {
t.Errorf("Line(0) = %s, want 'world'", m.ActiveBuffer().Lines[0])
}
})
@ -107,12 +107,12 @@ func TestDeleteLine(t *testing.T) {
if m.ActiveWindow().Cursor.Line != 1 {
t.Errorf("CursorY() = %d, want '1'", m.ActiveWindow().Cursor.Line)
}
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("Line(0) = %s, want 'hello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("Line(0) = %s, want 'hello'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "another line" {
t.Errorf("Line(1) = %s, want 'another line'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "another line" {
t.Errorf("Line(1) = %s, want 'another line'", m.ActiveBuffer().Lines[1])
}
})
@ -131,8 +131,8 @@ func TestDeleteLine(t *testing.T) {
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want '0'", m.ActiveWindow().Cursor.Line)
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %s, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %s, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -151,8 +151,8 @@ func TestDeleteLine(t *testing.T) {
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want '0'", m.ActiveWindow().Cursor.Line)
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %s, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %s, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -176,8 +176,8 @@ func TestDeleteLine(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want \"\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want \"\"", m.ActiveBuffer().Lines[0])
}
})
@ -190,8 +190,8 @@ func TestDeleteLine(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("Line(0) = %q, want \"hello\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("Line(0) = %q, want \"hello\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
@ -210,8 +210,8 @@ func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want '0'", m.ActiveWindow().Cursor.Col)
}
if m.ActiveBuffer().Lines[0].String() != "ello" {
t.Errorf("Line(0) = %s, want 'ello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ello" {
t.Errorf("Line(0) = %s, want 'ello'", m.ActiveBuffer().Lines[0])
}
})
@ -224,8 +224,8 @@ func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
if m.ActiveWindow().Cursor.Col != 2 {
t.Errorf("CursorX() = %d, want '2'", m.ActiveWindow().Cursor.Col)
}
if m.ActiveBuffer().Lines[0].String() != "helo" {
t.Errorf("Line(0) = %s, want 'helo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "helo" {
t.Errorf("Line(0) = %s, want 'helo'", m.ActiveBuffer().Lines[0])
}
})
@ -238,8 +238,8 @@ func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want '4'", m.ActiveWindow().Cursor.Col)
}
if m.ActiveBuffer().Lines[0].String() != "hell" {
t.Errorf("Line(0) = %s, want 'hell'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hell" {
t.Errorf("Line(0) = %s, want 'hell'", m.ActiveBuffer().Lines[0])
}
})
@ -252,8 +252,8 @@ func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want '0'", m.ActiveWindow().Cursor.Col)
}
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("Line(0) = %s, want 'hello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("Line(0) = %s, want 'hello'", m.ActiveBuffer().Lines[0])
}
})
@ -267,8 +267,8 @@ func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
if m.ActiveWindow().Cursor.Col != 1 {
t.Errorf("CursorX() = %d, want 1", m.ActiveWindow().Cursor.Col)
}
if m.ActiveBuffer().Lines[0].String() != "hllo" {
t.Errorf("Line(0) = %q, want 'hllo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hllo" {
t.Errorf("Line(0) = %q, want 'hllo'", m.ActiveBuffer().Lines[0])
}
})
@ -282,8 +282,8 @@ func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
if m.ActiveWindow().Cursor.Col != 3 {
t.Errorf("CursorX() = %d, want 3", m.ActiveWindow().Cursor.Col)
}
if m.ActiveBuffer().Lines[0].String() != "helo" {
t.Errorf("Line(0) = %q, want 'helo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "helo" {
t.Errorf("Line(0) = %q, want 'helo'", m.ActiveBuffer().Lines[0])
}
})
@ -293,8 +293,8 @@ func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
sendKeys(tm, "2", "d", "l")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "llo" {
t.Errorf("Line(0) = %q, want 'llo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "llo" {
t.Errorf("Line(0) = %q, want 'llo'", m.ActiveBuffer().Lines[0])
}
})
@ -304,8 +304,8 @@ func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
sendKeys(tm, "d", "2", "l")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "llo" {
t.Errorf("Line(0) = %q, want 'llo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "llo" {
t.Errorf("Line(0) = %q, want 'llo'", m.ActiveBuffer().Lines[0])
}
})
@ -315,8 +315,8 @@ func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
sendKeys(tm, "2", "d", "h")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "heo" {
t.Errorf("Line(0) = %q, want 'heo'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "heo" {
t.Errorf("Line(0) = %q, want 'heo'", m.ActiveBuffer().Lines[0])
}
})
}
@ -331,11 +331,11 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line 4" {
t.Errorf("Line(1) = %q, want 'line 4'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 4" {
t.Errorf("Line(1) = %q, want 'line 4'", m.ActiveBuffer().Lines[1])
}
if m.ActiveWindow().Cursor.Line != 1 {
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
@ -351,8 +351,8 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 3" {
t.Errorf("Line(0) = %q, want 'line 3'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 3" {
t.Errorf("Line(0) = %q, want 'line 3'", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
@ -370,11 +370,11 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line 2" {
t.Errorf("Line(1) = %q, want 'line 2'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 2" {
t.Errorf("Line(1) = %q, want 'line 2'", m.ActiveBuffer().Lines[1])
}
})
@ -387,11 +387,11 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line 5" {
t.Errorf("Line(1) = %q, want 'line 5'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 5" {
t.Errorf("Line(1) = %q, want 'line 5'", m.ActiveBuffer().Lines[1])
}
})
@ -404,11 +404,11 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line 5" {
t.Errorf("Line(1) = %q, want 'line 5'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 5" {
t.Errorf("Line(1) = %q, want 'line 5'", m.ActiveBuffer().Lines[1])
}
})
@ -421,11 +421,11 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line 4" {
t.Errorf("Line(1) = %q, want 'line 4'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 4" {
t.Errorf("Line(1) = %q, want 'line 4'", m.ActiveBuffer().Lines[1])
}
if m.ActiveWindow().Cursor.Line != 1 {
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
@ -443,11 +443,11 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 2" {
t.Errorf("Line(0) = %q, want 'line 2'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 2" {
t.Errorf("Line(0) = %q, want 'line 2'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line 3" {
t.Errorf("Line(1) = %q, want 'line 3'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 3" {
t.Errorf("Line(1) = %q, want 'line 3'", m.ActiveBuffer().Lines[1])
}
})
@ -460,8 +460,8 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 3" {
t.Errorf("Line(0) = %q, want 'line 3'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 3" {
t.Errorf("Line(0) = %q, want 'line 3'", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
@ -477,11 +477,11 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line 5" {
t.Errorf("Line(1) = %q, want 'line 5'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 5" {
t.Errorf("Line(1) = %q, want 'line 5'", m.ActiveBuffer().Lines[1])
}
})
@ -494,11 +494,11 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line 5" {
t.Errorf("Line(1) = %q, want 'line 5'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 5" {
t.Errorf("Line(1) = %q, want 'line 5'", m.ActiveBuffer().Lines[1])
}
})
@ -512,8 +512,8 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0])
}
})
@ -527,8 +527,8 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 3" {
t.Errorf("Line(0) = %q, want 'line 3'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 3" {
t.Errorf("Line(0) = %q, want 'line 3'", m.ActiveBuffer().Lines[0])
}
})
}
@ -541,8 +541,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
sendKeys(tm, "d", "w")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "world" {
t.Errorf("Line(0) = %q, want \"world\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "world" {
t.Errorf("Line(0) = %q, want \"world\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
@ -555,8 +555,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
sendKeys(tm, "d", "w")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "heworld" {
t.Errorf("Line(0) = %q, want \"heworld\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "heworld" {
t.Errorf("Line(0) = %q, want \"heworld\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 2 {
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
@ -569,8 +569,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
sendKeys(tm, "d", "w")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello " {
t.Errorf("Line(0) = %q, want \"hello \"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello " {
t.Errorf("Line(0) = %q, want \"hello \"", m.ActiveBuffer().Lines[0])
}
})
@ -580,8 +580,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
sendKeys(tm, "2", "d", "w")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "three four" {
t.Errorf("Line(0) = %q, want \"three four\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "three four" {
t.Errorf("Line(0) = %q, want \"three four\"", m.ActiveBuffer().Lines[0])
}
})
@ -591,8 +591,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
sendKeys(tm, "d", "2", "w")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "three four" {
t.Errorf("Line(0) = %q, want \"three four\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "three four" {
t.Errorf("Line(0) = %q, want \"three four\"", m.ActiveBuffer().Lines[0])
}
})
@ -603,8 +603,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
m := getFinalModel(t, tm)
// 'w' motion stops at punctuation, so it should delete "hello"
if m.ActiveBuffer().Lines[0].String() != ", world" {
t.Errorf("Line(0) = %q, want \", world\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != ", world" {
t.Errorf("Line(0) = %q, want \", world\"", m.ActiveBuffer().Lines[0])
}
})
@ -616,8 +616,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
m := getFinalModel(t, tm)
// 'e' is inclusive - deletes "hello" (cols 0-4 inclusive), leaves " world"
if m.ActiveBuffer().Lines[0].String() != " world" {
t.Errorf("Line(0) = %q, want \" world\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " world" {
t.Errorf("Line(0) = %q, want \" world\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
@ -631,8 +631,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
m := getFinalModel(t, tm)
// From 'l' (col 2) to 'o' (col 4) inclusive → deletes "llo"
if m.ActiveBuffer().Lines[0].String() != "he world" {
t.Errorf("Line(0) = %q, want \"he world\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "he world" {
t.Errorf("Line(0) = %q, want \"he world\"", m.ActiveBuffer().Lines[0])
}
})
@ -643,8 +643,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
m := getFinalModel(t, tm)
// From 'o' of "hello" (col 4) to 'd' of "world" (col 10) inclusive
if m.ActiveBuffer().Lines[0].String() != "hell" {
t.Errorf("Line(0) = %q, want \"hell\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hell" {
t.Errorf("Line(0) = %q, want \"hell\"", m.ActiveBuffer().Lines[0])
}
})
@ -655,8 +655,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
m := getFinalModel(t, tm)
// Deletes "one" and " two" (to end of second word inclusive)
if m.ActiveBuffer().Lines[0].String() != " three four" {
t.Errorf("Line(0) = %q, want \" three four\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " three four" {
t.Errorf("Line(0) = %q, want \" three four\"", m.ActiveBuffer().Lines[0])
}
})
@ -667,8 +667,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
sendKeys(tm, "d", "b")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello world" {
t.Errorf("Line(0) = %q, want \"hello world\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello world" {
t.Errorf("Line(0) = %q, want \"hello world\"", m.ActiveBuffer().Lines[0])
}
})
@ -679,8 +679,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
m := getFinalModel(t, tm)
// cursor at 'r' of "world", db should delete back to start of "world"
if m.ActiveBuffer().Lines[0].String() != "hello rld" {
t.Errorf("Line(0) = %q, want \"hello rld\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello rld" {
t.Errorf("Line(0) = %q, want \"hello rld\"", m.ActiveBuffer().Lines[0])
}
})
@ -691,8 +691,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
m := getFinalModel(t, tm)
// cursor at 'w', db should delete "hello " back
if m.ActiveBuffer().Lines[0].String() != "world" {
t.Errorf("Line(0) = %q, want \"world\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "world" {
t.Errorf("Line(0) = %q, want \"world\"", m.ActiveBuffer().Lines[0])
}
})
@ -703,8 +703,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
m := getFinalModel(t, tm)
// cursor at 'f' of "four", 2db should delete "two three "
if m.ActiveBuffer().Lines[0].String() != "one four" {
t.Errorf("Line(0) = %q, want \"one four\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "one four" {
t.Errorf("Line(0) = %q, want \"one four\"", m.ActiveBuffer().Lines[0])
}
})
@ -715,8 +715,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
m := getFinalModel(t, tm)
// cursor at 'd' (last char), db should delete back to start of "world"
if m.ActiveBuffer().Lines[0].String() != "hello d" {
t.Errorf("Line(0) = %q, want \"hello d\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello d" {
t.Errorf("Line(0) = %q, want \"hello d\"", m.ActiveBuffer().Lines[0])
}
})
}
@ -729,8 +729,8 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
sendKeys(tm, "d", "0")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello world" {
t.Errorf("Line(0) = %q, want \"hello world\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello world" {
t.Errorf("Line(0) = %q, want \"hello world\"", m.ActiveBuffer().Lines[0])
}
})
@ -740,8 +740,8 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
sendKeys(tm, "d", "0")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "world" {
t.Errorf("Line(0) = %q, want \"world\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "world" {
t.Errorf("Line(0) = %q, want \"world\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
@ -754,8 +754,8 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
sendKeys(tm, "d", "0")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "d" {
t.Errorf("Line(0) = %q, want \"d\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "d" {
t.Errorf("Line(0) = %q, want \"d\"", m.ActiveBuffer().Lines[0])
}
})
@ -765,8 +765,8 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
sendKeys(tm, "d", "0")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "lo world" {
t.Errorf("Line(0) = %q, want \"lo world\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "lo world" {
t.Errorf("Line(0) = %q, want \"lo world\"", m.ActiveBuffer().Lines[0])
}
})
@ -777,8 +777,8 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
sendKeys(tm, "d", "$")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want \"\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want \"\"", m.ActiveBuffer().Lines[0])
}
})
@ -788,8 +788,8 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
sendKeys(tm, "d", "$")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello " {
t.Errorf("Line(0) = %q, want \"hello \"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello " {
t.Errorf("Line(0) = %q, want \"hello \"", m.ActiveBuffer().Lines[0])
}
})
@ -799,8 +799,8 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
sendKeys(tm, "d", "$")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello worl" {
t.Errorf("Line(0) = %q, want \"hello worl\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello worl" {
t.Errorf("Line(0) = %q, want \"hello worl\"", m.ActiveBuffer().Lines[0])
}
})
@ -810,11 +810,11 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
sendKeys(tm, "d", "$")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello " {
t.Errorf("Line(0) = %q, want \"hello \"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello " {
t.Errorf("Line(0) = %q, want \"hello \"", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "second line" {
t.Errorf("Line(1) = %q, want \"second line\"", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "second line" {
t.Errorf("Line(1) = %q, want \"second line\"", m.ActiveBuffer().Lines[1])
}
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
@ -827,8 +827,8 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
sendKeys(tm, "d", "$")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want \"\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want \"\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
@ -843,8 +843,8 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
m := getFinalModel(t, tm)
// From col 0 to first non-whitespace (col 3, 'h') - deletes leading spaces
if m.ActiveBuffer().Lines[0].String() != "hello world" {
t.Errorf("Line(0) = %q, want \"hello world\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello world" {
t.Errorf("Line(0) = %q, want \"hello world\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
@ -858,8 +858,8 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
m := getFinalModel(t, tm)
// From col 1 to first non-whitespace (col 3, 'h') - deletes cols 1-2
if m.ActiveBuffer().Lines[0].String() != " hello world" {
t.Errorf("Line(0) = %q, want \" hello world\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " hello world" {
t.Errorf("Line(0) = %q, want \" hello world\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 1 {
t.Errorf("CursorX() = %d, want 1", m.ActiveWindow().Cursor.Col)
@ -874,8 +874,8 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
m := getFinalModel(t, tm)
// From col 6 ('l') back to col 3 ('h')
// Should delete "hel" leaving " lo world"
if m.ActiveBuffer().Lines[0].String() != " lo world" {
t.Errorf("Line(0) = %q, want \" lo world\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " lo world" {
t.Errorf("Line(0) = %q, want \" lo world\"", m.ActiveBuffer().Lines[0])
}
})
@ -887,8 +887,8 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
m := getFinalModel(t, tm)
// From col 5 (' ') back to col 0 ('h')
// Should delete "hello" leaving " world"
if m.ActiveBuffer().Lines[0].String() != " world" {
t.Errorf("Line(0) = %q, want \" world\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " world" {
t.Errorf("Line(0) = %q, want \" world\"", m.ActiveBuffer().Lines[0])
}
})
}
@ -903,11 +903,11 @@ func TestDeleteOperatorWithJumpMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line 2" {
t.Errorf("Line(1) = %q, want 'line 2'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 2" {
t.Errorf("Line(1) = %q, want 'line 2'", m.ActiveBuffer().Lines[1])
}
})
@ -920,8 +920,8 @@ func TestDeleteOperatorWithJumpMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -934,11 +934,11 @@ func TestDeleteOperatorWithJumpMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line 2" {
t.Errorf("Line(1) = %q, want 'line 2'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 2" {
t.Errorf("Line(1) = %q, want 'line 2'", m.ActiveBuffer().Lines[1])
}
})
@ -962,11 +962,11 @@ func TestDeleteOperatorWithJumpMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 4" {
t.Errorf("Line(0) = %q, want 'line 4'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 4" {
t.Errorf("Line(0) = %q, want 'line 4'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line 5" {
t.Errorf("Line(1) = %q, want 'line 5'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 5" {
t.Errorf("Line(1) = %q, want 'line 5'", m.ActiveBuffer().Lines[1])
}
})
@ -979,8 +979,8 @@ func TestDeleteOperatorWithJumpMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -993,11 +993,11 @@ func TestDeleteOperatorWithJumpMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 2" {
t.Errorf("Line(0) = %q, want 'line 2'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 2" {
t.Errorf("Line(0) = %q, want 'line 2'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line 3" {
t.Errorf("Line(1) = %q, want 'line 3'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 3" {
t.Errorf("Line(1) = %q, want 'line 3'", m.ActiveBuffer().Lines[1])
}
})
@ -1021,8 +1021,8 @@ func TestDeleteOperatorWithJumpMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -1035,8 +1035,8 @@ func TestDeleteOperatorWithJumpMotion(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})

File diff suppressed because it is too large Load Diff

View File

@ -1,581 +0,0 @@
package editor
import (
"testing"
)
// ==================================================
// P0: Basic Recording Tests
// ==================================================
func TestDotOperatorRecording(t *testing.T) {
t.Run("records simple delete", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello"}))
sendKeys(tm, "x")
m := getFinalModel(t, tm)
keys := m.LastChangeKeys()
if len(keys) != 1 || keys[0] != "x" {
t.Errorf("LastChangeKeys() = %v, want [\"x\"]", keys)
}
// Also verify . register
reg, ok := m.GetRegister('.')
if !ok {
t.Fatal("dot register not found")
}
if reg.Content[0] != "x" {
t.Errorf("dot register = %q, want \"x\"", reg.Content[0])
}
})
t.Run("records operator motion", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello world"}))
sendKeys(tm, "d", "w")
m := getFinalModel(t, tm)
keys := m.LastChangeKeys()
if len(keys) != 2 || keys[0] != "d" || keys[1] != "w" {
t.Errorf("LastChangeKeys() = %v, want [\"d\", \"w\"]", keys)
}
})
t.Run("records double press operator", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello", "world"}))
sendKeys(tm, "d", "d")
m := getFinalModel(t, tm)
keys := m.LastChangeKeys()
if len(keys) != 2 || keys[0] != "d" || keys[1] != "d" {
t.Errorf("LastChangeKeys() = %v, want [\"d\", \"d\"]", keys)
}
})
t.Run("records visual operation", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3"}))
sendKeys(tm, "V", "j", "x")
m := getFinalModel(t, tm)
keys := m.LastChangeKeys()
if len(keys) != 3 || keys[0] != "V" || keys[1] != "j" || keys[2] != "x" {
t.Errorf("LastChangeKeys() = %v, want [\"V\", \"j\", \"x\"]", keys)
}
})
t.Run("records insert mode", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello"}))
sendKeys(tm, "i", "X", "Y", "Z", "esc")
m := getFinalModel(t, tm)
keys := m.LastChangeKeys()
if len(keys) != 5 || keys[0] != "i" || keys[1] != "X" || keys[2] != "Y" || keys[3] != "Z" || keys[4] != "esc" {
t.Errorf("LastChangeKeys() = %v, want [\"i\", \"X\", \"Y\", \"Z\", \"esc\"]", keys)
}
})
}
// ==================================================
// P0: Non-Recording Tests
// ==================================================
func TestDotOperatorNonRecording(t *testing.T) {
t.Run("does not record pure motions", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3", "line4"}))
sendKeys(tm, "k", "k", "k")
m := getFinalModel(t, tm)
keys := m.LastChangeKeys()
// Pure motions should result in empty recording
if len(keys) != 0 {
t.Errorf("LastChangeKeys() = %v, want []", keys)
}
})
t.Run("does not record dot operator itself", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello"}))
sendKeys(tm, "x", ".", ".")
m := getFinalModel(t, tm)
keys := m.LastChangeKeys()
// Should still be just ["x"], not ["x", ".", "."]
if len(keys) != 1 || keys[0] != "x" {
t.Errorf("LastChangeKeys() = %v, want [\"x\"]", keys)
}
})
t.Run("does not record command mode entry", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello"}))
sendKeys(tm, ":")
sendKeys(tm, "esc") // Exit command mode
m := getFinalModel(t, tm)
keys := m.LastChangeKeys()
// Command mode entry should not record
if len(keys) != 0 {
t.Errorf("LastChangeKeys() = %v, want []", keys)
}
})
t.Run("does not record visual mode entry without action", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello"}))
sendKeys(tm, "v", "esc")
m := getFinalModel(t, tm)
keys := m.LastChangeKeys()
// Just entering and exiting visual mode should not record
if len(keys) != 0 {
t.Errorf("LastChangeKeys() = %v, want []", keys)
}
})
}
// ==================================================
// P0: Basic Replay Tests
// ==================================================
func TestDotOperatorReplay(t *testing.T) {
t.Run("repeats simple delete", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello"}))
sendKeys(tm, "x") // "ello"
sendKeys(tm, ".") // "llo"
m := getFinalModel(t, tm)
if m.ActiveBuffer().Line(0) != "llo" {
t.Errorf("buffer = %q, want \"llo\"", m.ActiveBuffer().Line(0))
}
})
t.Run("repeats operator motion", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"one two three"}))
sendKeys(tm, "d", "w") // "two three"
sendKeys(tm, ".") // "three"
m := getFinalModel(t, tm)
if m.ActiveBuffer().Line(0) != "three" {
t.Errorf("buffer = %q, want \"three\"", m.ActiveBuffer().Line(0))
}
})
t.Run("repeats double press operator", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3"}))
sendKeys(tm, "d", "d") // Delete first line -> "line2", "line3"
sendKeys(tm, ".") // Delete next line -> "line3"
m := getFinalModel(t, tm)
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Line(0) != "line3" {
t.Errorf("buffer = %q, want \"line3\"", m.ActiveBuffer().Line(0))
}
})
}
// ==================================================
// P1: Recording Replacement Tests
// ==================================================
func TestDotOperatorRecordingReplacement(t *testing.T) {
t.Run("new action replaces old recording", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello world"}))
sendKeys(tm, "x") // Record ["x"]
sendKeys(tm, "l", "l") // Motions clear recording buffer but don't save
sendKeys(tm, "d", "w") // Record ["d", "w"]
m := getFinalModel(t, tm)
keys := m.LastChangeKeys()
// Should be the latest action ["d", "w"], not ["x"]
if len(keys) != 2 || keys[0] != "d" || keys[1] != "w" {
t.Errorf("LastChangeKeys() = %v, want [\"d\", \"w\"]", keys)
}
})
t.Run("motions clear recording without saving", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3", "line4"}))
sendKeys(tm, "x") // Record ["x"]
sendKeys(tm, "j", "j", "j") // Motions don't overwrite saved recording
sendKeys(tm, "d", "d") // New action replaces
m := getFinalModel(t, tm)
keys := m.LastChangeKeys()
// Should be ["d", "d"] from the last modifying action
if len(keys) != 2 || keys[0] != "d" || keys[1] != "d" {
t.Errorf("LastChangeKeys() = %v, want [\"d\", \"d\"]", keys)
}
})
}
// ==================================================
// P1: Visual Mode Tests
// ==================================================
func TestDotOperatorVisualMode(t *testing.T) {
t.Run("repeats visual line operation", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"a", "b", "c", "d", "e"}))
sendKeys(tm, "V", "j", "x") // Delete lines 0-1 -> "c", "d", "e"
// Cursor should be at line 0 after deletion
sendKeys(tm, ".") // Repeat -> delete next 2 lines -> "e"
m := getFinalModel(t, tm)
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Line(0) != "e" {
t.Errorf("buffer = %q, want \"e\"", m.ActiveBuffer().Line(0))
}
})
}
// ==================================================
// P1: Insert Mode Tests
// ==================================================
func TestDotOperatorInsertMode(t *testing.T) {
t.Run("repeats insert mode operation", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello"}))
sendKeys(tm, "i", "X", "Y", "Z", "esc") // "XYZhello", cursor at col 2 (on Z)
// Move cursor after XYZ (to col 3, between Z and h)
sendKeys(tm, "l")
sendKeys(tm, ".") // Should insert XYZ again at col 3
m := getFinalModel(t, tm)
if m.ActiveBuffer().Line(0) != "XYZXYZhello" {
t.Errorf("buffer = %q, want \"XYZXYZhello\"", m.ActiveBuffer().Line(0))
}
})
}
// ==================================================
// P1: Multiple Repeat Tests
// ==================================================
func TestDotOperatorMultipleRepeats(t *testing.T) {
t.Run("dot can be used multiple times", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello world foo bar"}))
sendKeys(tm, "d", "w") // "world foo bar"
sendKeys(tm, ".") // "foo bar"
sendKeys(tm, ".") // "bar"
sendKeys(tm, ".") // ""
m := getFinalModel(t, tm)
line := m.ActiveBuffer().Line(0)
// After deleting 4 words, should be empty or just whitespace
if line != "" && line != " " {
t.Errorf("buffer = %q, want empty or space", line)
}
})
}
// ==================================================
// P2: Edge Cases
// ==================================================
func TestDotOperatorEdgeCases(t *testing.T) {
t.Run("repeat at start of file", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello"}))
sendKeys(tm, "x") // "ello"
sendKeys(tm, ".") // "llo"
m := getFinalModel(t, tm)
if m.ActiveBuffer().Line(0) != "llo" {
t.Errorf("buffer = %q, want \"llo\"", m.ActiveBuffer().Line(0))
}
})
t.Run("repeat after undo", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello world"}))
sendKeys(tm, "x") // "ello world"
sendKeys(tm, "u") // Undo -> "hello world"
sendKeys(tm, ".") // Repeat should still work -> "ello world"
m := getFinalModel(t, tm)
if m.ActiveBuffer().Line(0) != "ello world" {
t.Errorf("buffer = %q, want \"ello world\"", m.ActiveBuffer().Line(0))
}
})
t.Run("repeat with no recorded change", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello"}))
sendKeys(tm, ".") // Dot with nothing recorded
m := getFinalModel(t, tm)
// Should not crash, buffer should be unchanged
if m.ActiveBuffer().Line(0) != "hello" {
t.Errorf("buffer = %q, want \"hello\"", m.ActiveBuffer().Line(0))
}
})
}
// ==================================================
// P2: Integration with Counts
// ==================================================
func TestDotOperatorWithCounts(t *testing.T) {
t.Run("recording includes count", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello world"}))
sendKeys(tm, "3", "x")
m := getFinalModel(t, tm)
keys := m.LastChangeKeys()
// Should record both the count and the action
if len(keys) != 2 || keys[0] != "3" || keys[1] != "x" {
t.Errorf("LastChangeKeys() = %v, want [\"3\", \"x\"]", keys)
}
})
t.Run("repeat preserves count", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello"}))
sendKeys(tm, "3", "x") // Delete 3 chars -> "lo"
sendKeys(tm, ".") // Should delete 3 more (or try to)
m := getFinalModel(t, tm)
// After deleting 3 + 3 = 6 chars, should be empty or have no chars left
if m.ActiveBuffer().Line(0) != "" {
t.Errorf("buffer = %q, want empty", m.ActiveBuffer().Line(0))
}
})
}
// ==================================================
// P2: Complex Sequences
// ==================================================
func TestDotOperatorComplexSequences(t *testing.T) {
t.Run("complex sequence of operations", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3"}))
sendKeys(tm, "x") // Delete 'l' from line1 -> ["ine1", "line2", "line3"]
sendKeys(tm, "j", "j") // Move to line 2
sendKeys(tm, "d", "d") // Delete line 2 (line3) -> ["ine1", "line2"], cursor at line 1
sendKeys(tm, "k") // Move up to line 0
sendKeys(tm, ".") // Repeat dd - deletes line 0 (ine1) -> ["line2"]
m := getFinalModel(t, tm)
// After the sequence, dd was recorded and repeated at line 0, deleting it
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Line(0) != "line2" {
t.Errorf("buffer = %q, want \"line2\"", m.ActiveBuffer().Line(0))
}
})
}
// ==================================================
// Additional Coverage: Change Operator
// ==================================================
func TestDotOperatorChangeOperator(t *testing.T) {
t.Run("repeats change word", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"one two three"}))
sendKeys(tm, "c", "w", "X", "esc") // Change "one " to "X" -> "Xtwo three"
sendKeys(tm, "w") // Move to "two"
sendKeys(tm, ".") // Repeat cw -> change "two " to "X" -> "Xtwo X"
m := getFinalModel(t, tm)
// cw deletes word and space after, result is "Xtwo X" + trailing content
if m.ActiveBuffer().Line(0) != "Xtwo Xthree" && m.ActiveBuffer().Line(0) != "Xtwo X" {
t.Errorf("buffer = %q, want \"Xtwo X\" or \"Xtwo Xthree\"", m.ActiveBuffer().Line(0))
}
})
t.Run("repeats change line", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3"}))
sendKeys(tm, "c", "c", "N", "E", "W", "esc") // Change line -> "NEW"
sendKeys(tm, "j") // Move to next line
sendKeys(tm, ".") // Repeat cc
m := getFinalModel(t, tm)
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Line(0) != "NEW" || m.ActiveBuffer().Line(1) != "NEW" {
t.Errorf("lines = [%q, %q], want [\"NEW\", \"NEW\"]",
m.ActiveBuffer().Line(0), m.ActiveBuffer().Line(1))
}
})
}
// ==================================================
// Additional Coverage: Paste Operations
// ==================================================
func TestDotOperatorPaste(t *testing.T) {
t.Run("repeats paste", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello", "world"}))
sendKeys(tm, "y", "y") // Yank line
sendKeys(tm, "p") // Paste -> "hello", "hello", "world"
sendKeys(tm, ".") // Repeat paste -> "hello", "hello", "hello", "world"
m := getFinalModel(t, tm)
if m.ActiveBuffer().LineCount() != 4 {
t.Errorf("LineCount() = %d, want 4", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Line(0) != "hello" || m.ActiveBuffer().Line(1) != "hello" || m.ActiveBuffer().Line(2) != "hello" {
t.Errorf("first 3 lines should be \"hello\"")
}
})
}
// ==================================================
// Additional Coverage: Append Mode
// ==================================================
func TestDotOperatorAppendMode(t *testing.T) {
t.Run("repeats append", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello"}))
sendKeys(tm, "a", "X", "Y", "esc") // Append XY after 'h' -> "hXYello", cursor at Y
sendKeys(tm, "l") // Move right one char
sendKeys(tm, ".") // Repeat append at new position
m := getFinalModel(t, tm)
// Result will depend on exact cursor behavior after 'a' mode
if m.ActiveBuffer().Line(0) != "hXYeXYllo" {
t.Errorf("buffer = %q, want \"hXYeXYllo\"", m.ActiveBuffer().Line(0))
}
})
t.Run("repeats append at end of line", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello"}))
sendKeys(tm, "A", "!", "esc") // Append at end -> "hello!"
sendKeys(tm, "j") // Move to another line (if exists) or stay
sendKeys(tm, ".") // Repeat A!
m := getFinalModel(t, tm)
// Should append at end of current line
if m.ActiveBuffer().Line(0) != "hello!!" {
t.Errorf("buffer = %q, want \"hello!!\"", m.ActiveBuffer().Line(0))
}
})
}
// ==================================================
// Additional Coverage: Visual Character Mode
// ==================================================
func TestDotOperatorVisualCharMode(t *testing.T) {
t.Run("repeats visual char delete", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello world"}))
sendKeys(tm, "v", "l", "l", "x") // Select "hel" and delete -> "lo world"
sendKeys(tm, ".") // Repeat -> delete "lo " -> "world"
m := getFinalModel(t, tm)
if m.ActiveBuffer().Line(0) != "world" {
t.Errorf("buffer = %q, want \"world\"", m.ActiveBuffer().Line(0))
}
})
}
// ==================================================
// Additional Coverage: Text Objects
// ==================================================
func TestDotOperatorTextObjects(t *testing.T) {
t.Run("repeats delete inner word", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"one two three"}))
sendKeys(tm, "d", "i", "w") // Delete "one" -> " two three"
sendKeys(tm, "w") // Move to "two"
sendKeys(tm, ".") // Repeat diw -> delete "two"
m := getFinalModel(t, tm)
if m.ActiveBuffer().Line(0) != " three" {
t.Errorf("buffer = %q, want \" three\"", m.ActiveBuffer().Line(0))
}
})
t.Run("repeats delete a word", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"one two three"}))
sendKeys(tm, "d", "a", "w") // Delete "one " -> "two three"
sendKeys(tm, ".") // Repeat daw -> delete "two "
m := getFinalModel(t, tm)
if m.ActiveBuffer().Line(0) != "three" {
t.Errorf("buffer = %q, want \"three\"", m.ActiveBuffer().Line(0))
}
})
}
// ==================================================
// Additional Coverage: Open Line
// ==================================================
func TestDotOperatorOpenLine(t *testing.T) {
t.Run("repeats open line below", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"line1", "line2"}))
sendKeys(tm, "o", "N", "E", "W", "esc") // Open below and insert "NEW"
sendKeys(tm, "j") // Move down
sendKeys(tm, ".") // Repeat
m := getFinalModel(t, tm)
// Should have: line1, NEW, line2, NEW
if m.ActiveBuffer().LineCount() != 4 {
t.Errorf("LineCount() = %d, want 4", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Line(1) != "NEW" || m.ActiveBuffer().Line(3) != "NEW" {
t.Errorf("lines[1] = %q, lines[3] = %q, both want \"NEW\"",
m.ActiveBuffer().Line(1), m.ActiveBuffer().Line(3))
}
})
t.Run("repeats open line above", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"line1", "line2"}))
sendKeys(tm, "j") // Move to line2
sendKeys(tm, "O", "T", "O", "P", "esc") // Open above and insert "TOP"
sendKeys(tm, "j", "j") // Move down past inserted line
sendKeys(tm, ".") // Repeat
m := getFinalModel(t, tm)
// Should have: line1, TOP, TOP, line2
if m.ActiveBuffer().LineCount() != 4 {
t.Errorf("LineCount() = %d, want 4", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Line(1) != "TOP" || m.ActiveBuffer().Line(2) != "TOP" {
t.Errorf("lines[1] = %q, lines[2] = %q, both want \"TOP\"",
m.ActiveBuffer().Line(1), m.ActiveBuffer().Line(2))
}
})
}
// ==================================================
// Additional Coverage: Character Find Motions
// ==================================================
func TestDotOperatorCharMotions(t *testing.T) {
t.Run("repeats delete to char", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello world foo"}))
sendKeys(tm, "d", "f", "o") // Delete from 'h' until and including first 'o' -> "llo world foo"
sendKeys(tm, "w") // Move to next word
sendKeys(tm, ".") // Repeat dfo
m := getFinalModel(t, tm)
// After dfo from start: "llo world foo"
// After w: cursor somewhere in the line
// After . (repeat dfo): delete until next 'o'
// Actual result will depend on word motion and where 'o' is found
line := m.ActiveBuffer().Line(0)
if len(line) == 0 {
t.Errorf("buffer should not be empty after two dfo operations")
}
if line != " rld foo" {
t.Errorf("line is '%s', but expected ' rld foo'", line)
}
})
t.Run("repeats change until char", func(t *testing.T) {
tm := newTestModel(t, WithLines([]string{"hello;world;end"}))
sendKeys(tm, "c", "t", ";", "X", "esc") // Change "hello" (until ;) to "X" -> "X;world;end"
sendKeys(tm, "f", ";") // Move to first ';'
sendKeys(tm, "l") // Move past ';' to 'w'
sendKeys(tm, ".") // Repeat ct; from 'w'
m := getFinalModel(t, tm)
// After ct; from 'w': changes "world" (until next ;) to "X" -> result varies
// Accept the actual implementation behavior
line := m.ActiveBuffer().Line(0)
if len(line) < 3 {
t.Errorf("buffer = %q, seems too short", line)
}
if line != "Xo;Xd;end" {
t.Errorf("line is '%s', but expected 'Xo;Xd;end'", line)
}
})
}

File diff suppressed because it is too large Load Diff

View File

@ -256,8 +256,8 @@ func TestHalfPageScrollDown(t *testing.T) {
if m.ActiveWindow().Cursor.Line != 22 {
t.Errorf("CursorY() = %d, want 22", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col > m.ActiveBuffer().Lines[m.ActiveWindow().Cursor.Line].Len() {
t.Errorf("CursorX() = %d exceeds line length %d", m.ActiveWindow().Cursor.Col, m.ActiveBuffer().Lines[m.ActiveWindow().Cursor.Line].Len())
if m.ActiveWindow().Cursor.Col > len(m.ActiveBuffer().Lines[m.ActiveWindow().Cursor.Line]) {
t.Errorf("CursorX() = %d exceeds line length %d", m.ActiveWindow().Cursor.Col, len(m.ActiveBuffer().Lines[m.ActiveWindow().Cursor.Line]))
}
})
@ -412,271 +412,6 @@ func TestHalfPageScrollRoundTrip(t *testing.T) {
})
}
// Tests use terminal 80x30: viewportH=28, full-scroll=28, scrollOff=8, safe zone relY 8-19.
func TestFullPageScrollDown(t *testing.T) {
t.Run("ctrl+f scrolls viewport down by full page", func(t *testing.T) {
// cursor at line 15 (relY=15, in safe zone), scrollY starts at 0
// After ctrl+f: newScrollY=28, newCursorY=28+15=43
lines := generateLines(100)
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 15}, 80, 30)
sendKeys(tm, "ctrl+f")
m := getFinalModel(t, tm)
if m.ActiveWindow().ScrollY != 28 {
t.Errorf("ScrollY() = %d, want 28", m.ActiveWindow().ScrollY)
}
if m.ActiveWindow().Cursor.Line != 43 {
t.Errorf("CursorY() = %d, want 43", m.ActiveWindow().Cursor.Line)
}
})
t.Run("ctrl+f preserves cursor relative position in viewport", func(t *testing.T) {
// relY=15 before and after
lines := generateLines(100)
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 15}, 80, 30)
sendKeys(tm, "ctrl+f")
m := getFinalModel(t, tm)
relY := m.ActiveWindow().Cursor.Line - m.ActiveWindow().ScrollY
if relY != 15 {
t.Errorf("relative position = %d, want 15", relY)
}
})
t.Run("ctrl+f clamps cursor to scrollOff when cursor is near top", func(t *testing.T) {
// cursor at line 0 (relY=0 < scrollOff=8), clamp to scrollOff
// After ctrl+f: newScrollY=28, newCursorY=28+8=36
lines := generateLines(100)
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 0}, 80, 30)
sendKeys(tm, "ctrl+f")
m := getFinalModel(t, tm)
if m.ActiveWindow().ScrollY != 28 {
t.Errorf("ScrollY() = %d, want 28", m.ActiveWindow().ScrollY)
}
if m.ActiveWindow().Cursor.Line != 36 {
t.Errorf("CursorY() = %d, want 36", m.ActiveWindow().Cursor.Line)
}
})
t.Run("ctrl+f at end of file does not scroll past max", func(t *testing.T) {
// 50 lines, viewport 28: maxScroll=22
// cursor at line 45: AdjustScroll puts cursor at scrollY=22 (clamped), relY=23
// After ctrl+f: newScrollY clamped to 22, relY=23>19 clamped to 19, newCursorY=41
lines := generateLines(50)
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 45}, 80, 30)
sendKeys(tm, "ctrl+f")
m := getFinalModel(t, tm)
maxScroll := 50 - 28
if m.ActiveWindow().ScrollY > maxScroll {
t.Errorf("ScrollY() = %d, want <= %d", m.ActiveWindow().ScrollY, maxScroll)
}
if m.ActiveWindow().Cursor.Line != 41 {
t.Errorf("CursorY() = %d, want 41", m.ActiveWindow().Cursor.Line)
}
})
t.Run("ctrl+f on file smaller than viewport does not crash", func(t *testing.T) {
// 20 lines < viewport 28: maxScroll=0, scrollY stays 0
// relY=0 < scrollOff, clamp to 8; newCursorY=0+8=8
lines := generateLines(20)
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 0}, 80, 30)
sendKeys(tm, "ctrl+f")
m := getFinalModel(t, tm)
if m.ActiveWindow().ScrollY != 0 {
t.Errorf("ScrollY() = %d, want 0", m.ActiveWindow().ScrollY)
}
if m.ActiveWindow().Cursor.Line != 8 {
t.Errorf("CursorY() = %d, want 8", m.ActiveWindow().Cursor.Line)
}
})
t.Run("ctrl+f clamps cursor x to new line length", func(t *testing.T) {
// Lines 0-27: "hello world" (11 chars), lines 28-99: "hi" (2 chars)
// cursor at line 5, cursorX=10
// ctrl+f: relY=5<8 clamp to 8; newScrollY=28; newCursorY=36 (a "hi" line)
// ClampCursorX: 10 >= 2, so cursorX=2
lines := make([]string, 100)
for i := range 28 {
lines[i] = "hello world"
}
for i := 28; i < 100; i++ {
lines[i] = "hi"
}
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 10, Line: 5}, 80, 30)
sendKeys(tm, "ctrl+f")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Line != 36 {
t.Errorf("CursorY() = %d, want 36", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col > m.ActiveBuffer().Lines[m.ActiveWindow().Cursor.Line].Len() {
t.Errorf("CursorX() = %d exceeds line length %d", m.ActiveWindow().Cursor.Col, m.ActiveBuffer().Lines[m.ActiveWindow().Cursor.Line].Len())
}
})
t.Run("multiple ctrl+f presses scroll incrementally", func(t *testing.T) {
// cursor at line 15, scrollY=0, relY=15
// ctrl+f #1: scrollY=28, cursorY=43, relY=15
// ctrl+f #2: scrollY=56, cursorY=71, relY=15
lines := generateLines(200)
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 15}, 80, 30)
sendKeys(tm, "ctrl+f", "ctrl+f")
m := getFinalModel(t, tm)
if m.ActiveWindow().ScrollY != 56 {
t.Errorf("ScrollY() = %d, want 56", m.ActiveWindow().ScrollY)
}
if m.ActiveWindow().Cursor.Line != 71 {
t.Errorf("CursorY() = %d, want 71", m.ActiveWindow().Cursor.Line)
}
})
}
func TestFullPageScrollUp(t *testing.T) {
t.Run("ctrl+b scrolls viewport up by full page", func(t *testing.T) {
// cursor at line 50: AdjustScroll -> scrollY=31, relY=19
// After ctrl+b: newScrollY=3, newCursorY=3+19=22
lines := generateLines(100)
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 50}, 80, 30)
sendKeys(tm, "ctrl+b")
m := getFinalModel(t, tm)
if m.ActiveWindow().ScrollY != 3 {
t.Errorf("ScrollY() = %d, want 3", m.ActiveWindow().ScrollY)
}
if m.ActiveWindow().Cursor.Line != 22 {
t.Errorf("CursorY() = %d, want 22", m.ActiveWindow().Cursor.Line)
}
})
t.Run("ctrl+b preserves cursor relative position in viewport", func(t *testing.T) {
// relY=19 before and after
lines := generateLines(100)
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 50}, 80, 30)
sendKeys(tm, "ctrl+b")
m := getFinalModel(t, tm)
relY := m.ActiveWindow().Cursor.Line - m.ActiveWindow().ScrollY
if relY != 19 {
t.Errorf("relative position = %d, want 19", relY)
}
})
t.Run("ctrl+b at top of file does not make scrollY negative", func(t *testing.T) {
// cursor at line 20, scrollY=0, relY=20 (clamped to 19 by scrollOff)
// AdjustScroll: relY=20>19, clamp to 19, scrollY stays 0, cursorY=19
// ctrl+b: newScrollY=max(0,-28)=0, relY=19 preserved, cursorY=19
lines := generateLines(100)
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 20}, 80, 30)
sendKeys(tm, "ctrl+b")
m := getFinalModel(t, tm)
if m.ActiveWindow().ScrollY < 0 {
t.Errorf("ScrollY() = %d, want >= 0", m.ActiveWindow().ScrollY)
}
if m.ActiveWindow().ScrollY != 0 {
t.Errorf("ScrollY() = %d, want 0", m.ActiveWindow().ScrollY)
}
// Cursor should be clamped to safe zone bottom (relY=19)
if m.ActiveWindow().Cursor.Line != 19 {
t.Errorf("CursorY() = %d, want 19", m.ActiveWindow().Cursor.Line)
}
})
t.Run("ctrl+b clamps cursor to scrollOff when cursor is near top of viewport", func(t *testing.T) {
// cursor at line 30, scrollY=0, relY=30>19 -> clamp to 19, cursorY=19
// ctrl+b: newScrollY=0; relY=19; newCursorY=19
lines := generateLines(100)
// First normalize the position
m := getFinalModel(t, newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 30}, 80, 30))
initialY := m.ActiveWindow().Cursor.Line
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: initialY}, 80, 30)
sendKeys(tm, "ctrl+b")
m2 := getFinalModel(t, tm)
if m2.ActiveWindow().ScrollY != 0 {
t.Errorf("ScrollY() = %d, want 0", m2.ActiveWindow().ScrollY)
}
// Cursor should remain in safe zone
relY := m2.ActiveWindow().Cursor.Line - m2.ActiveWindow().ScrollY
if relY < 8 || relY > 19 {
t.Errorf("relY = %d, want in range [8, 19]", relY)
}
})
t.Run("multiple ctrl+b presses scroll incrementally", func(t *testing.T) {
// cursor at line 80: AdjustScroll -> scrollY=61, relY=19
// ctrl+b #1: newScrollY=33, cursorY=52
// ctrl+b #2: newScrollY=5, cursorY=24
lines := generateLines(200)
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 80}, 80, 30)
sendKeys(tm, "ctrl+b", "ctrl+b")
m := getFinalModel(t, tm)
if m.ActiveWindow().ScrollY != 5 {
t.Errorf("ScrollY() = %d, want 5", m.ActiveWindow().ScrollY)
}
if m.ActiveWindow().Cursor.Line != 24 {
t.Errorf("CursorY() = %d, want 24", m.ActiveWindow().Cursor.Line)
}
})
}
func TestFullPageScrollRoundTrip(t *testing.T) {
t.Run("ctrl+f then ctrl+b returns cursor to original position", func(t *testing.T) {
// cursor at line 15, scrollY=0, relY=15
// ctrl+f: scrollY=28, cursorY=43, relY=15
// ctrl+b: newScrollY=max(0,28-28)=0, cursorY=0+15=15
lines := generateLines(200)
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 15}, 80, 30)
sendKeys(tm, "ctrl+f", "ctrl+b")
m := getFinalModel(t, tm)
if m.ActiveWindow().ScrollY != 0 {
t.Errorf("ScrollY() = %d, want 0", m.ActiveWindow().ScrollY)
}
if m.ActiveWindow().Cursor.Line != 15 {
t.Errorf("CursorY() = %d, want 15", m.ActiveWindow().Cursor.Line)
}
})
t.Run("ctrl+b then ctrl+f returns cursor to original position", func(t *testing.T) {
// cursor at line 50: AdjustScroll -> scrollY=31, relY=19
// ctrl+b: scrollY=3, cursorY=22, relY=19
// ctrl+f: scrollY=31, cursorY=50, relY=19
lines := generateLines(200)
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 50}, 80, 30)
sendKeys(tm, "ctrl+b", "ctrl+f")
m := getFinalModel(t, tm)
if m.ActiveWindow().ScrollY != 31 {
t.Errorf("ScrollY() = %d, want 31", m.ActiveWindow().ScrollY)
}
if m.ActiveWindow().Cursor.Line != 50 {
t.Errorf("CursorY() = %d, want 50", m.ActiveWindow().Cursor.Line)
}
})
t.Run("alternating ctrl+f and ctrl+b maintains scroll stability", func(t *testing.T) {
lines := generateLines(200)
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 15}, 80, 30)
sendKeys(tm, "ctrl+f", "ctrl+b", "ctrl+f", "ctrl+b")
m := getFinalModel(t, tm)
if m.ActiveWindow().ScrollY != 0 {
t.Errorf("ScrollY() = %d, want 0 after 2 round trips", m.ActiveWindow().ScrollY)
}
if m.ActiveWindow().Cursor.Line != 15 {
t.Errorf("CursorY() = %d, want 15 after 2 round trips", m.ActiveWindow().Cursor.Line)
}
})
}
func TestScrollWithCount(t *testing.T) {
t.Run("5j scrolls appropriately", func(t *testing.T) {
lines := generateLines(50)

View File

@ -33,8 +33,8 @@ func TestTextObjectInnerWord(t *testing.T) {
sendKeys(tm, "d", "i", "w")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != " world" {
t.Errorf("lines[0] = %q, want ' world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " world" {
t.Errorf("lines[0] = %q, want ' world'", m.ActiveBuffer().Lines[0])
}
})
@ -44,8 +44,8 @@ func TestTextObjectInnerWord(t *testing.T) {
sendKeys(tm, "c", "i", "w")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != " world" {
t.Errorf("lines[0] = %q, want ' world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " world" {
t.Errorf("lines[0] = %q, want ' world'", m.ActiveBuffer().Lines[0])
}
if m.Mode() != core.InsertMode {
t.Errorf("Expected insert mode after ciw")
@ -138,8 +138,8 @@ func TestTextObjectAroundWord(t *testing.T) {
sendKeys(tm, "d", "a", "w")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "world" {
t.Errorf("lines[0] = %q, want 'world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "world" {
t.Errorf("lines[0] = %q, want 'world'", m.ActiveBuffer().Lines[0])
}
})
@ -149,8 +149,8 @@ func TestTextObjectAroundWord(t *testing.T) {
sendKeys(tm, "d", "a", "w")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello " {
t.Errorf("lines[0] = %q, want 'hello '", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello " {
t.Errorf("lines[0] = %q, want 'hello '", m.ActiveBuffer().Lines[0])
}
})
}
@ -179,8 +179,8 @@ func TestTextObjectInnerWORD(t *testing.T) {
sendKeys(tm, "d", "i", "W")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != " test" {
t.Errorf("lines[0] = %q, want ' test'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " test" {
t.Errorf("lines[0] = %q, want ' test'", m.ActiveBuffer().Lines[0])
}
})
}
@ -205,8 +205,8 @@ func TestTextObjectAroundWORD(t *testing.T) {
sendKeys(tm, "d", "a", "W")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "baz" {
t.Errorf("lines[0] = %q, want 'baz'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "baz" {
t.Errorf("lines[0] = %q, want 'baz'", m.ActiveBuffer().Lines[0])
}
})
}
@ -248,8 +248,8 @@ func TestTextObjectAngleBrackets(t *testing.T) {
sendKeys(tm, "d", "i", "<")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "<>" {
t.Errorf("lines[0] = %q, want '<>'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "<>" {
t.Errorf("lines[0] = %q, want '<>'", m.ActiveBuffer().Lines[0])
}
})
@ -259,8 +259,8 @@ func TestTextObjectAngleBrackets(t *testing.T) {
sendKeys(tm, "d", "a", "<")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -271,8 +271,8 @@ func TestTextObjectAngleBrackets(t *testing.T) {
m := getFinalModel(t, tm)
// Should remain unchanged
if m.ActiveBuffer().Lines[0].String() != "<>" {
t.Errorf("lines[0] = %q, want '<>'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "<>" {
t.Errorf("lines[0] = %q, want '<>'", m.ActiveBuffer().Lines[0])
}
})
@ -321,8 +321,8 @@ func TestTextObjectParentheses(t *testing.T) {
sendKeys(tm, "d", "i", "(")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "func()" {
t.Errorf("lines[0] = %q, want 'func()'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "func()" {
t.Errorf("lines[0] = %q, want 'func()'", m.ActiveBuffer().Lines[0])
}
})
@ -332,8 +332,8 @@ func TestTextObjectParentheses(t *testing.T) {
sendKeys(tm, "d", "a", "(")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "func" {
t.Errorf("lines[0] = %q, want 'func'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "func" {
t.Errorf("lines[0] = %q, want 'func'", m.ActiveBuffer().Lines[0])
}
})
@ -343,8 +343,8 @@ func TestTextObjectParentheses(t *testing.T) {
sendKeys(tm, "d", "i", "(")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "()" {
t.Errorf("lines[0] = %q, want '()'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "()" {
t.Errorf("lines[0] = %q, want '()'", m.ActiveBuffer().Lines[0])
}
})
}
@ -368,8 +368,8 @@ func TestTextObjectBraces(t *testing.T) {
sendKeys(tm, "d", "i", "{")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "{}" {
t.Errorf("lines[0] = %q, want '{}'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "{}" {
t.Errorf("lines[0] = %q, want '{}'", m.ActiveBuffer().Lines[0])
}
})
@ -379,8 +379,8 @@ func TestTextObjectBraces(t *testing.T) {
sendKeys(tm, "d", "a", "{")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
}
@ -404,8 +404,8 @@ func TestTextObjectBrackets(t *testing.T) {
sendKeys(tm, "d", "i", "[")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "[]" {
t.Errorf("lines[0] = %q, want '[]'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "[]" {
t.Errorf("lines[0] = %q, want '[]'", m.ActiveBuffer().Lines[0])
}
})
@ -415,8 +415,8 @@ func TestTextObjectBrackets(t *testing.T) {
sendKeys(tm, "d", "a", "[")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
}
@ -440,8 +440,8 @@ func TestTextObjectDoubleQuotes(t *testing.T) {
sendKeys(tm, "d", "i", `"`)
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != `""` {
t.Errorf("lines[0] = %q, want '\"\"'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != `""` {
t.Errorf("lines[0] = %q, want '\"\"'", m.ActiveBuffer().Lines[0])
}
})
@ -451,8 +451,8 @@ func TestTextObjectDoubleQuotes(t *testing.T) {
sendKeys(tm, "d", "a", `"`)
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
@ -462,8 +462,8 @@ func TestTextObjectDoubleQuotes(t *testing.T) {
sendKeys(tm, "d", "i", `"`)
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != `""` {
t.Errorf("lines[0] = %q, want '\"\"'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != `""` {
t.Errorf("lines[0] = %q, want '\"\"'", m.ActiveBuffer().Lines[0])
}
})
}
@ -487,8 +487,8 @@ func TestTextObjectSingleQuotes(t *testing.T) {
sendKeys(tm, "d", "i", "'")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "''" {
t.Errorf("lines[0] = %q, want \"''\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "''" {
t.Errorf("lines[0] = %q, want \"''\"", m.ActiveBuffer().Lines[0])
}
})
@ -498,8 +498,8 @@ func TestTextObjectSingleQuotes(t *testing.T) {
sendKeys(tm, "d", "a", "'")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
}
@ -523,8 +523,8 @@ func TestTextObjectBackticks(t *testing.T) {
sendKeys(tm, "d", "i", "`")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "``" {
t.Errorf("lines[0] = %q, want '``'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "``" {
t.Errorf("lines[0] = %q, want '``'", m.ActiveBuffer().Lines[0])
}
})
@ -534,8 +534,8 @@ func TestTextObjectBackticks(t *testing.T) {
sendKeys(tm, "d", "a", "`")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0])
}
})
}
@ -551,8 +551,8 @@ func TestTextObjectEdgeCases(t *testing.T) {
sendKeys(tm, "d", "i", "w")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != " b c" {
t.Errorf("lines[0] = %q, want ' b c'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " b c" {
t.Errorf("lines[0] = %q, want ' b c'", m.ActiveBuffer().Lines[0])
}
})
@ -564,8 +564,8 @@ func TestTextObjectEdgeCases(t *testing.T) {
sendKeys(tm, "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "<world>" {
t.Errorf("lines[0] = %q, want '<world>'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "<world>" {
t.Errorf("lines[0] = %q, want '<world>'", m.ActiveBuffer().Lines[0])
}
})
@ -577,8 +577,8 @@ func TestTextObjectEdgeCases(t *testing.T) {
m := getFinalModel(t, tm)
// 'p' pastes after cursor, so "arg" is pasted after 't' -> "testarg"
if m.ActiveBuffer().Lines[1].String() != "testarg" {
t.Errorf("lines[1] = %q, want 'testarg'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "testarg" {
t.Errorf("lines[1] = %q, want 'testarg'", m.ActiveBuffer().Lines[1])
}
})
@ -589,8 +589,8 @@ func TestTextObjectEdgeCases(t *testing.T) {
m := getFinalModel(t, tm)
// Should remain unchanged since cursor is not inside parens
if m.ActiveBuffer().Lines[0].String() != "before (hello) after" {
t.Errorf("lines[0] = %q, want 'before (hello) after'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "before (hello) after" {
t.Errorf("lines[0] = %q, want 'before (hello) after'", m.ActiveBuffer().Lines[0])
}
})
@ -600,8 +600,8 @@ func TestTextObjectEdgeCases(t *testing.T) {
sendKeys(tm, "d", "i", "(")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "before () after" {
t.Errorf("lines[0] = %q, want 'before () after'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "before () after" {
t.Errorf("lines[0] = %q, want 'before () after'", m.ActiveBuffer().Lines[0])
}
})
@ -612,8 +612,8 @@ func TestTextObjectEdgeCases(t *testing.T) {
m := getFinalModel(t, tm)
// 'a' should delete including the delimiters
if m.ActiveBuffer().Lines[0].String() != "before after" {
t.Errorf("lines[0] = %q, want 'before after'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "before after" {
t.Errorf("lines[0] = %q, want 'before after'", m.ActiveBuffer().Lines[0])
}
})
@ -624,8 +624,8 @@ func TestTextObjectEdgeCases(t *testing.T) {
m := getFinalModel(t, tm)
// Cursor on '(' at position 5, should still select inside
if m.ActiveBuffer().Lines[0].String() != "text () more" {
t.Errorf("lines[0] = %q, want 'text () more'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "text () more" {
t.Errorf("lines[0] = %q, want 'text () more'", m.ActiveBuffer().Lines[0])
}
})
@ -638,8 +638,8 @@ func TestTextObjectEdgeCases(t *testing.T) {
m := getFinalModel(t, tm)
// Cursor on ')', should still select inside
if m.ActiveBuffer().Lines[0].String() != "text () more" {
t.Errorf("lines[0] = %q, want 'text () more'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "text () more" {
t.Errorf("lines[0] = %q, want 'text () more'", m.ActiveBuffer().Lines[0])
}
})
@ -650,8 +650,8 @@ func TestTextObjectEdgeCases(t *testing.T) {
m := getFinalModel(t, tm)
// Should select the first pair it finds
if m.ActiveBuffer().Lines[0].String() != "() bar (baz)" {
t.Errorf("lines[0] = %q, want '() bar (baz)'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "() bar (baz)" {
t.Errorf("lines[0] = %q, want '() bar (baz)'", m.ActiveBuffer().Lines[0])
}
})
@ -663,8 +663,8 @@ func TestTextObjectEdgeCases(t *testing.T) {
m := getFinalModel(t, tm)
// Should search forward and find the second pair
if m.ActiveBuffer().Lines[0].String() != "(foo) bar ()" {
t.Errorf("lines[0] = %q, want '(foo) bar ()'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "(foo) bar ()" {
t.Errorf("lines[0] = %q, want '(foo) bar ()'", m.ActiveBuffer().Lines[0])
}
})
@ -676,8 +676,8 @@ func TestTextObjectEdgeCases(t *testing.T) {
m := getFinalModel(t, tm)
// Should select the first pair since cursor is inside it
if m.ActiveBuffer().Lines[0].String() != "() bar (baz)" {
t.Errorf("lines[0] = %q, want '() bar (baz)'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "() bar (baz)" {
t.Errorf("lines[0] = %q, want '() bar (baz)'", m.ActiveBuffer().Lines[0])
}
})
@ -688,8 +688,8 @@ func TestTextObjectEdgeCases(t *testing.T) {
m := getFinalModel(t, tm)
// Should find and select first quoted string
if m.ActiveBuffer().Lines[0].String() != `foo "" baz "qux"` {
t.Errorf("lines[0] = %q, want 'foo \"\" baz \"qux\"'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != `foo "" baz "qux"` {
t.Errorf("lines[0] = %q, want 'foo \"\" baz \"qux\"'", m.ActiveBuffer().Lines[0])
}
})
@ -701,8 +701,8 @@ func TestTextObjectEdgeCases(t *testing.T) {
m := getFinalModel(t, tm)
// Should search forward and find second string
if m.ActiveBuffer().Lines[0].String() != `"foo" bar ""` {
t.Errorf("lines[0] = %q, want '\"foo\" bar \"\"'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != `"foo" bar ""` {
t.Errorf("lines[0] = %q, want '\"foo\" bar \"\"'", m.ActiveBuffer().Lines[0])
}
})
}
@ -727,8 +727,8 @@ func TestTextObjectMultiLineDelimiters(t *testing.T) {
"func test() {",
"}",
}
if !slicesEqual(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("lines = %v, want %v", bufferLinesToStrings(m.ActiveBuffer()), expected)
if !slicesEqual(m.ActiveBuffer().Lines, expected) {
t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
@ -746,8 +746,8 @@ func TestTextObjectMultiLineDelimiters(t *testing.T) {
expected := []string{
"func test() ",
}
if !slicesEqual(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("lines = %v, want %v", bufferLinesToStrings(m.ActiveBuffer()), expected)
if !slicesEqual(m.ActiveBuffer().Lines, expected) {
t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
@ -792,8 +792,8 @@ func TestTextObjectMultiLineDelimiters(t *testing.T) {
"function(",
")",
}
if !slicesEqual(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("lines = %v, want %v", bufferLinesToStrings(m.ActiveBuffer()), expected)
if !slicesEqual(m.ActiveBuffer().Lines, expected) {
t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
@ -815,8 +815,8 @@ func TestTextObjectMultiLineDelimiters(t *testing.T) {
"outer {",
"}",
}
if !slicesEqual(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("lines = %v, want %v", bufferLinesToStrings(m.ActiveBuffer()), expected)
if !slicesEqual(m.ActiveBuffer().Lines, expected) {
t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
@ -841,8 +841,8 @@ func TestTextObjectMultiLineDelimiters(t *testing.T) {
" more",
"}",
}
if !slicesEqual(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("lines = %v, want %v", bufferLinesToStrings(m.ActiveBuffer()), expected)
if !slicesEqual(m.ActiveBuffer().Lines, expected) {
t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
@ -869,8 +869,8 @@ func TestTextObjectMultiLineDelimiters(t *testing.T) {
" }",
"}",
}
if !slicesEqual(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("lines = %v, want %v", bufferLinesToStrings(m.ActiveBuffer()), expected)
if !slicesEqual(m.ActiveBuffer().Lines, expected) {
t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
@ -889,8 +889,8 @@ func TestTextObjectMultiLineDelimiters(t *testing.T) {
"function(arg) {",
"}",
}
if !slicesEqual(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("lines = %v, want %v", bufferLinesToStrings(m.ActiveBuffer()), expected)
if !slicesEqual(m.ActiveBuffer().Lines, expected) {
t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
@ -909,8 +909,8 @@ func TestTextObjectMultiLineDelimiters(t *testing.T) {
"function(arg) {",
"}",
}
if !slicesEqual(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("lines = %v, want %v", bufferLinesToStrings(m.ActiveBuffer()), expected)
if !slicesEqual(m.ActiveBuffer().Lines, expected) {
t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
@ -933,8 +933,8 @@ func TestTextObjectMultiLineDelimiters(t *testing.T) {
"}",
"after",
}
if !slicesEqual(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("lines = %v, want %v", bufferLinesToStrings(m.ActiveBuffer()), expected)
if !slicesEqual(m.ActiveBuffer().Lines, expected) {
t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
@ -959,21 +959,12 @@ func TestTextObjectMultiLineDelimiters(t *testing.T) {
" more",
")",
}
if !slicesEqual(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("lines = %v, want %v", bufferLinesToStrings(m.ActiveBuffer()), expected)
if !slicesEqual(m.ActiveBuffer().Lines, expected) {
t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
}
// Helper function to get buffer lines as strings
func bufferLinesToStrings(buf *core.Buffer) []string {
result := make([]string, buf.LineCount())
for i := 0; i < buf.LineCount(); i++ {
result[i] = buf.Line(i)
}
return result
}
// Helper function to compare slices
func slicesEqual(a, b []string) bool {
if len(a) != len(b) {

View File

@ -1,992 +0,0 @@
package editor
import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/core"
)
// equalStringSlices compares two string slices for equality
func equalStringSlices(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// ============================================================================
// BASIC UNDO/REDO TESTS
// ============================================================================
func TestUndoBasicOperations(t *testing.T) {
t.Run("undo single character delete with x", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "x") // Delete 'h'
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
}
// Verify undo stack is empty
if m.ActiveBuffer().UndoStack.CanUndo() {
t.Error("Expected undo stack to be empty after undoing all changes")
}
})
t.Run("undo and redo single character delete", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "x") // Delete 'h'
sendKeys(tm, "u") // Undo
sendKeys(tm, "ctrl+r") // Redo
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "ello" {
t.Errorf("lines[0] = %q, want 'ello'", m.ActiveBuffer().Lines[0].String())
}
// Verify redo stack is empty
if m.ActiveBuffer().UndoStack.CanRedo() {
t.Error("Expected redo stack to be empty after redoing all changes")
}
})
t.Run("undo multiple x operations creates separate undo blocks", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "x") // Delete 'h' -> "ello"
sendKeys(tm, "x") // Delete 'e' -> "llo"
sendKeys(tm, "x") // Delete first 'l' -> "lo"
sendKeys(tm, "u") // Undo last x -> "llo"
sendKeys(tm, "u") // Undo second x -> "ello"
sendKeys(tm, "u") // Undo first x -> "hello"
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("After 3 undos: lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
}
// Verify undo stack is empty
if m.ActiveBuffer().UndoStack.CanUndo() {
t.Error("Expected undo stack to be empty after undoing all changes")
}
})
t.Run("undo single X (delete backward)", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "X") // Delete 'e'
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
}
})
}
func TestUndoCursorRestoration(t *testing.T) {
t.Run("undo restores cursor position after x", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0})
sendKeys(tm, "x") // Delete 'w' at position 6
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 6 {
t.Errorf("cursor col = %d, want 6", m.ActiveWindow().Cursor.Col)
}
if m.ActiveBuffer().Lines[0].String() != "hello world" {
t.Errorf("lines[0] = %q, want 'hello world'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("redo restores cursor position after operation", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "x") // Delete at position 0
sendKeys(tm, "u") // Undo
sendKeys(tm, "ctrl+r") // Redo
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("cursor col = %d, want 0", m.ActiveWindow().Cursor.Col)
}
})
}
// ============================================================================
// INSERT MODE UNDO TESTS
// ============================================================================
func TestUndoInsertMode(t *testing.T) {
t.Run("insert mode groups all characters into one undo", func(t *testing.T) {
lines := []string{""}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "i") // Enter insert mode
sendKeyString(tm, "hello") // Type 5 characters
sendKeys(tm, "esc") // Exit insert mode
sendKeys(tm, "u") // Undo once
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0].String())
}
// Verify only one undo was needed
if m.ActiveBuffer().UndoStack.CanUndo() {
t.Error("Expected undo stack to be empty after single undo of insert session")
}
})
t.Run("multiple insert sessions create separate undo blocks", func(t *testing.T) {
lines := []string{""}
tm := newTestModelWithLines(t, lines)
// First insert session
sendKeys(tm, "i")
sendKeyString(tm, "hello")
sendKeys(tm, "esc")
// Second insert session
sendKeys(tm, "a") // Append
sendKeyString(tm, " world")
sendKeys(tm, "esc")
// Undo second session
sendKeys(tm, "u")
// Undo first session
sendKeys(tm, "u")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("After 2 undos: lines[0] = %q, want ''", m.ActiveBuffer().Lines[0].String())
}
// Verify undo stack is empty
if m.ActiveBuffer().UndoStack.CanUndo() {
t.Error("Expected undo stack to be empty after undoing all changes")
}
})
t.Run("insert with newlines groups everything into one undo", func(t *testing.T) {
lines := []string{""}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "i")
sendKeyString(tm, "line1")
sendKeys(tm, "enter")
sendKeyString(tm, "line2")
sendKeys(tm, "enter")
sendKeyString(tm, "line3")
sendKeys(tm, "esc")
// Single undo should remove everything
sendKeys(tm, "u")
m := getFinalModel(t, tm)
if len(m.ActiveBuffer().Lines) != 1 || m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("After undo: got %d lines with content %q, want 1 empty line",
len(m.ActiveBuffer().Lines), m.ActiveBuffer().Lines)
}
})
t.Run("insert mode with backspace is grouped into one undo", func(t *testing.T) {
lines := []string{""}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "i")
sendKeyString(tm, "hello")
sendKeys(tm, "backspace", "backspace") // Delete "lo"
sendKeyString(tm, "y") // Type "y"
sendKeys(tm, "esc")
// Single undo should remove entire insert session
sendKeys(tm, "u")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("After undo: lines[0] = %q, want ''", m.ActiveBuffer().Lines[0].String())
}
})
}
// ============================================================================
// OPERATOR UNDO TESTS (dd, cc, etc.)
// ============================================================================
func TestUndoDeleteOperator(t *testing.T) {
t.Run("dd creates one undo block", func(t *testing.T) {
lines := []string{"line1", "line2", "line3"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "d", "d") // Delete line1
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if len(m.ActiveBuffer().Lines) != 3 {
t.Errorf("line count = %d, want 3", len(m.ActiveBuffer().Lines))
}
if m.ActiveBuffer().Lines[0].String() != "line1" {
t.Errorf("lines[0] = %q, want 'line1'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("3dd creates one undo block for all 3 lines", func(t *testing.T) {
lines := []string{"line1", "line2", "line3", "line4"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "3", "d", "d") // Delete 3 lines
sendKeys(tm, "u") // Undo once
m := getFinalModel(t, tm)
if len(m.ActiveBuffer().Lines) != 4 {
t.Errorf("line count = %d, want 4", len(m.ActiveBuffer().Lines))
}
if m.ActiveBuffer().Lines[0].String() != "line1" {
t.Errorf("lines[0] = %q, want 'line1'", m.ActiveBuffer().Lines[0].String())
}
if m.ActiveBuffer().Lines[2].String() != "line3" {
t.Errorf("lines[2] = %q, want 'line3'", m.ActiveBuffer().Lines[2].String())
}
})
t.Run("dw (delete word) creates one undo block", func(t *testing.T) {
lines := []string{"hello world foo"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "d", "w") // Delete "hello "
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello world foo" {
t.Errorf("lines[0] = %q, want 'hello world foo'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("D (delete to end of line) undoes correctly", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0})
sendKeys(tm, "D") // Delete "world"
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello world" {
t.Errorf("lines[0] = %q, want 'hello world'", m.ActiveBuffer().Lines[0].String())
}
if m.ActiveWindow().Cursor.Col != 6 {
t.Errorf("cursor col = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
}
func TestUndoChangeOperator(t *testing.T) {
t.Run("cc (change line) undoes correctly", func(t *testing.T) {
lines := []string{"original line", "line2"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "c", "c") // Change line (deletes and enters insert)
sendKeyString(tm, "new line") // Type new content
sendKeys(tm, "esc") // Exit insert mode
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "original line" {
t.Errorf("lines[0] = %q, want 'original line'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("cw (change word) undoes correctly", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "c", "w") // Change word
sendKeyString(tm, "hi") // Type replacement
sendKeys(tm, "esc")
sendKeys(tm, "u")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello world" {
t.Errorf("lines[0] = %q, want 'hello world'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("s (substitute char) undoes correctly", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "s") // Substitute character
sendKeyString(tm, "H") // Type replacement
sendKeys(tm, "esc")
sendKeys(tm, "u")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("S (substitute line) undoes correctly", func(t *testing.T) {
lines := []string{"original", "line2"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "S") // Substitute line
sendKeyString(tm, "replaced") // Type replacement
sendKeys(tm, "esc")
sendKeys(tm, "u")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "original" {
t.Errorf("lines[0] = %q, want 'original'", m.ActiveBuffer().Lines[0].String())
}
})
}
// ============================================================================
// VISUAL MODE UNDO TESTS
// ============================================================================
func TestUndoVisualMode(t *testing.T) {
t.Run("visual char mode delete undoes correctly", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "v") // Enter visual char mode
sendKeys(tm, "l", "l", "l", "l") // Select "hello"
sendKeys(tm, "d") // Delete selection
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello world" {
t.Errorf("lines[0] = %q, want 'hello world'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("visual line mode delete undoes correctly", func(t *testing.T) {
lines := []string{"line1", "line2", "line3"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "V") // Enter visual line mode
sendKeys(tm, "j") // Select 2 lines
sendKeys(tm, "d") // Delete
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if len(m.ActiveBuffer().Lines) != 3 {
t.Errorf("line count = %d, want 3", len(m.ActiveBuffer().Lines))
}
if m.ActiveBuffer().Lines[0].String() != "line1" {
t.Errorf("lines[0] = %q, want 'line1'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("visual block mode delete undoes correctly", func(t *testing.T) {
lines := []string{"hello", "world", "test"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "ctrl+v") // Enter visual block mode
sendKeys(tm, "j", "j") // Select 3 lines
sendKeys(tm, "l", "l") // Select 3 columns
sendKeys(tm, "d") // Delete block
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
}
if m.ActiveBuffer().Lines[1].String() != "world" {
t.Errorf("lines[1] = %q, want 'world'", m.ActiveBuffer().Lines[1].String())
}
})
t.Run("visual char mode change undoes correctly", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "v") // Visual char mode
sendKeys(tm, "l", "l", "l", "l") // Select "hello"
sendKeys(tm, "c") // Change
sendKeyString(tm, "hi") // Type replacement
sendKeys(tm, "esc")
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello world" {
t.Errorf("lines[0] = %q, want 'hello world'", m.ActiveBuffer().Lines[0].String())
}
})
}
// ============================================================================
// TEXT OBJECT UNDO TESTS
// ============================================================================
func TestUndoTextObjects(t *testing.T) {
t.Run("diw (delete inner word) undoes correctly", func(t *testing.T) {
lines := []string{"hello world foo"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "d", "i", "w") // Delete inner word
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello world foo" {
t.Errorf("lines[0] = %q, want 'hello world foo'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("daw (delete a word) undoes correctly", func(t *testing.T) {
lines := []string{"hello world foo"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0})
sendKeys(tm, "d", "a", "w") // Delete a word
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello world foo" {
t.Errorf("lines[0] = %q, want 'hello world foo'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("ci( changes inside parens undoes correctly", func(t *testing.T) {
lines := []string{"before (hello) after"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 9, Line: 0})
sendKeys(tm, "c", "i", "(") // Change inside parens
sendKeyString(tm, "world") // Type replacement
sendKeys(tm, "esc")
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "before (hello) after" {
t.Errorf("lines[0] = %q, want 'before (hello) after'", m.ActiveBuffer().Lines[0].String())
}
})
}
// ============================================================================
// UNDO/REDO SEQUENCE TESTS
// ============================================================================
func TestUndoRedoSequences(t *testing.T) {
t.Run("undo then redo multiple times", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "x") // Delete 'h' -> "ello"
sendKeys(tm, "x") // Delete 'e' -> "llo"
// Undo twice
sendKeys(tm, "u", "u")
// Redo twice
sendKeys(tm, "ctrl+r", "ctrl+r")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "llo" {
t.Errorf("After 2 redos: lines[0] = %q, want 'llo'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("new change after undo clears redo stack", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "x") // Delete 'h' -> "ello"
sendKeys(tm, "x") // Delete 'e' -> "llo"
sendKeys(tm, "u") // Undo -> "ello"
sendKeys(tm, "x") // New change -> "llo"
m := getFinalModel(t, tm)
// Verify redo is not possible
if m.ActiveBuffer().UndoStack.CanRedo() {
t.Error("Expected redo stack to be cleared after new change")
}
// Verify content
if m.ActiveBuffer().Lines[0].String() != "llo" {
t.Errorf("lines[0] = %q, want 'llo'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("complex sequence: operations, undo, redo, more operations", func(t *testing.T) {
lines := []string{"line1", "line2", "line3"}
tm := newTestModelWithLines(t, lines)
// Do operations
sendKeys(tm, "d", "d") // Delete line1
sendKeys(tm, "d", "d") // Delete line2
// Undo once
sendKeys(tm, "u")
// Redo
sendKeys(tm, "ctrl+r")
// New operation
sendKeys(tm, "i")
sendKeyString(tm, "new")
sendKeys(tm, "esc")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "newline3" {
t.Errorf("After insert: lines[0] = %q, want 'newline3'", m.ActiveBuffer().Lines[0].String())
}
})
}
// ============================================================================
// EDGE CASE TESTS
// ============================================================================
func TestUndoEdgeCases(t *testing.T) {
t.Run("undo on empty undo stack does nothing", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "u") // Undo when nothing to undo
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("redo on empty redo stack does nothing", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "ctrl+r") // Redo when nothing to redo
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("undo after exhausting redo stack", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "x") // Delete
sendKeys(tm, "u") // Undo
sendKeys(tm, "ctrl+r") // Redo
sendKeys(tm, "ctrl+r") // Try redo again (should do nothing)
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "ello" {
t.Errorf("lines[0] = %q, want 'ello'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("undo operation that left buffer empty", func(t *testing.T) {
lines := []string{"only line"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "d", "d") // Delete only line (buffer should have empty line)
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if len(m.ActiveBuffer().Lines) != 1 {
t.Errorf("line count = %d, want 1", len(m.ActiveBuffer().Lines))
}
if m.ActiveBuffer().Lines[0].String() != "only line" {
t.Errorf("lines[0] = %q, want 'only line'", m.ActiveBuffer().Lines[0].String())
}
})
}
// ============================================================================
// MULTI-LINE OPERATION TESTS
// ============================================================================
func TestUndoMultiLineOperations(t *testing.T) {
t.Run("undo multi-line delete from visual mode", func(t *testing.T) {
lines := []string{"line1", "line2", "line3", "line4", "line5"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "V") // Visual line mode
sendKeys(tm, "j", "j") // Select 3 lines
sendKeys(tm, "d") // Delete
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if len(m.ActiveBuffer().Lines) != 5 {
t.Errorf("line count = %d, want 5", len(m.ActiveBuffer().Lines))
}
for i := 0; i < 5; i++ {
expected := "line" + string(rune('1'+i))
if m.ActiveBuffer().Lines[i].String() != expected {
t.Errorf("lines[%d] = %q, want %q", i, m.ActiveBuffer().Lines[i].String(), expected)
}
}
})
t.Run("undo delete spanning multiple lines with motion", func(t *testing.T) {
lines := []string{"line1", "line2", "line3"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "d", "j") // Delete current line and line below
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if len(m.ActiveBuffer().Lines) != 3 {
t.Errorf("line count = %d, want 3", len(m.ActiveBuffer().Lines))
}
if m.ActiveBuffer().Lines[0].String() != "line1" {
t.Errorf("lines[0] = %q, want 'line1'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("undo o (open line below) operation", func(t *testing.T) {
lines := []string{"line1", "line2"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "o") // Open line below
sendKeyString(tm, "new line") // Type content
sendKeys(tm, "esc") // Exit insert
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if len(m.ActiveBuffer().Lines) != 2 {
t.Errorf("line count = %d, want 2", len(m.ActiveBuffer().Lines))
}
if m.ActiveBuffer().Lines[0].String() != "line1" {
t.Errorf("lines[0] = %q, want 'line1'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("undo O (open line above) operation", func(t *testing.T) {
lines := []string{"line1", "line2"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Line: 1, Col: 0})
sendKeys(tm, "O") // Open line above
sendKeyString(tm, "new line") // Type content
sendKeys(tm, "esc") // Exit insert
sendKeys(tm, "u") // Undo
m := getFinalModel(t, tm)
if len(m.ActiveBuffer().Lines) != 2 {
t.Errorf("line count = %d, want 2", len(m.ActiveBuffer().Lines))
}
if m.ActiveBuffer().Lines[1].String() != "line2" {
t.Errorf("lines[1] = %q, want 'line2'", m.ActiveBuffer().Lines[1].String())
}
})
}
// ============================================================================
// UNDO STACK INSPECTION TESTS
// ============================================================================
func TestUndoStackStructure(t *testing.T) {
t.Run("verify undo stack has correct number of blocks", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLines(t, lines)
// Perform 3 separate operations
sendKeys(tm, "x") // Op 1
sendKeys(tm, "x") // Op 2
sendKeys(tm, "x") // Op 3
m := getFinalModel(t, tm)
// Should have 3 undo blocks
undoCount := 0
for m.ActiveBuffer().UndoStack.CanUndo() {
m.ActiveBuffer().UndoStack.Undo()
undoCount++
}
if undoCount != 3 {
t.Errorf("undo block count = %d, want 3", undoCount)
}
})
t.Run("verify insert mode creates single block with multiple changes", func(t *testing.T) {
lines := []string{""}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "i")
sendKeyString(tm, "hello")
sendKeys(tm, "esc")
// Verify single undo removes everything
sendKeys(tm, "u")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("After undo: lines[0] = %q, want ''", m.ActiveBuffer().Lines[0].String())
}
if m.ActiveBuffer().UndoStack.CanUndo() {
t.Error("Expected empty undo stack after single undo")
}
})
t.Run("verify dd creates single block with correct change types", func(t *testing.T) {
lines := []string{"line1", "line2"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "d", "d")
// Verify undo restores correctly
sendKeys(tm, "u")
m := getFinalModel(t, tm)
if len(m.ActiveBuffer().Lines) != 2 {
t.Errorf("line count after undo = %d, want 2", len(m.ActiveBuffer().Lines))
}
// Verify undo stack is empty after undo
if m.ActiveBuffer().UndoStack.CanUndo() {
t.Error("Expected empty undo stack after undoing all changes")
}
})
}
// ============================================================================
// COMPLEX SCENARIO TESTS
// ============================================================================
func TestUndoComplexScenarios(t *testing.T) {
t.Run("realistic editing session with multiple undo/redo", func(t *testing.T) {
lines := []string{"func main() {", "}", ""}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Line: 1, Col: 0})
// Insert a line
sendKeys(tm, "O")
sendKeyString(tm, "\tfmt.Println(\"hello\")")
sendKeys(tm, "esc")
// Delete a word
sendKeys(tm, "d", "i", "w")
// Undo delete
sendKeys(tm, "u")
// Undo insert
sendKeys(tm, "u")
// Redo both
sendKeys(tm, "ctrl+r", "ctrl+r")
m := getFinalModel(t, tm)
if len(m.ActiveBuffer().Lines) != 4 {
t.Errorf("After 2 redos: line count = %d, want 4", len(m.ActiveBuffer().Lines))
}
})
t.Run("alternating operations and undos", func(t *testing.T) {
lines := []string{"abc"}
tm := newTestModelWithLines(t, lines)
sendKeys(tm, "x") // Delete 'a' -> "bc"
sendKeys(tm, "u") // Undo -> "abc"
sendKeys(tm, "$") // Move to end
sendKeys(tm, "x") // Delete 'c' -> "ab"
sendKeys(tm, "u") // Undo -> "abc"
sendKeys(tm, "0") // Move to start
sendKeys(tm, "x") // Delete 'a' -> "bc"
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "bc" {
t.Errorf("lines[0] = %q, want 'bc'", m.ActiveBuffer().Lines[0].String())
}
})
}
// =================================================================
// PASTE OPERATIONS TESTS
// =================================================================
func TestUndoPasteOperations(t *testing.T) {
t.Run("basic p (paste after) undo", func(t *testing.T) {
tm := newTestModelWithLines(t, []string{"line1", "line2"})
// Yank first line and paste after second line
sendKeys(tm, "y", "y") // yank current line (line1)
sendKeys(tm, "j") // move to line2
sendKeys(tm, "p") // paste after line2
sendKeys(tm, "u") // undo paste
m := getFinalModel(t, tm)
expected := []string{"line1", "line2"}
if len(m.ActiveBuffer().Lines) != len(expected) {
t.Errorf("len(Lines) = %d, want %d", len(m.ActiveBuffer().Lines), len(expected))
}
for i, exp := range expected {
if m.ActiveBuffer().Lines[i].String() != exp {
t.Errorf("Lines[%d] = %q, want %q", i, m.ActiveBuffer().Lines[i].String(), exp)
}
}
// Cursor should be back at line2
if m.ActiveWindow().Cursor.Line != 1 {
t.Errorf("Cursor.Line = %d, want 1", m.ActiveWindow().Cursor.Line)
}
})
t.Run("basic P (paste before) undo", func(t *testing.T) {
tm := newTestModelWithLines(t, []string{"line1", "line2"})
// Yank first line and paste before second line
sendKeys(tm, "y", "y") // yank current line (line1)
sendKeys(tm, "j") // move to line2
sendKeys(tm, "P") // paste before line2
sendKeys(tm, "u") // undo paste
m := getFinalModel(t, tm)
expected := []string{"line1", "line2"}
if len(m.ActiveBuffer().Lines) != len(expected) {
t.Errorf("len(Lines) = %d, want %d", len(m.ActiveBuffer().Lines), len(expected))
}
for i, exp := range expected {
if m.ActiveBuffer().Lines[i].String() != exp {
t.Errorf("Lines[%d] = %q, want %q", i, m.ActiveBuffer().Lines[i].String(), exp)
}
}
// Cursor should be back at line2
if m.ActiveWindow().Cursor.Line != 1 {
t.Errorf("Cursor.Line = %d, want 1", m.ActiveWindow().Cursor.Line)
}
})
t.Run("charwise paste undo", func(t *testing.T) {
tm := newTestModelWithLines(t, []string{"hello world"})
// Yank "hello" and paste after "world"
sendKeys(tm, "y", "w") // yank word "hello"
sendKeys(tm, "$") // move to end
sendKeys(tm, "p") // paste after cursor
sendKeys(tm, "u") // undo paste
m := getFinalModel(t, tm)
expected := []string{"hello world"}
if !equalStringSlices(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("Lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
t.Run("visual mode paste undo", func(t *testing.T) {
tm := newTestModelWithLines(t, []string{"hello world", "foo bar"})
// Yank "hello" then select "world" and paste over it
sendKeys(tm, "y", "w") // yank "hello"
sendKeys(tm, "w") // move to "world"
sendKeys(tm, "v", "e") // select "world"
sendKeys(tm, "p") // paste over selection
sendKeys(tm, "u") // undo paste
m := getFinalModel(t, tm)
expected := []string{"hello world", "foo bar"}
if len(m.ActiveBuffer().Lines) != len(expected) {
t.Errorf("len(Lines) = %d, want %d", len(m.ActiveBuffer().Lines), len(expected))
}
for i, exp := range expected {
if m.ActiveBuffer().Lines[i].String() != exp {
t.Errorf("Lines[%d] = %q, want %q", i, m.ActiveBuffer().Lines[i].String(), exp)
}
}
})
t.Run("multiple paste operations undo separately", func(t *testing.T) {
tm := newTestModelWithLines(t, []string{"base"})
sendKeys(tm, "y", "y") // yank "base"
sendKeys(tm, "p") // paste: "base\nbase"
sendKeys(tm, "p") // paste: "base\nbase\nbase"
sendKeys(tm, "u") // undo last paste: "base\nbase"
sendKeys(tm, "u") // undo first paste: "base"
m := getFinalModel(t, tm)
expected := []string{"base"}
if !equalStringSlices(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("Lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
t.Run("paste with count undo", func(t *testing.T) {
tm := newTestModelWithLines(t, []string{"test"})
sendKeys(tm, "y", "y") // yank "test"
sendKeyString(tm, "3p") // paste 3 times
sendKeys(tm, "u") // undo (should undo all 3 pastes as one block)
m := getFinalModel(t, tm)
expected := []string{"test"}
if !equalStringSlices(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("Lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
}
// =================================================================
// COMPLEX COUNT OPERATIONS TESTS
// =================================================================
func TestUndoComplexCountOperations(t *testing.T) {
t.Run("5dd undo", func(t *testing.T) {
tm := newTestModelWithLines(t, []string{"1", "2", "3", "4", "5", "6", "7"})
sendKeys(tm, "j", "j") // move to line 3
sendKeyString(tm, "5dd") // delete 5 lines (3,4,5,6,7)
sendKeys(tm, "u") // undo
m := getFinalModel(t, tm)
expected := []string{"1", "2", "3", "4", "5", "6", "7"}
if !equalStringSlices(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("Lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
// Cursor should be back at line 3 (index 2)
if m.ActiveWindow().Cursor.Line != 2 {
t.Errorf("Cursor.Line = %d, want 2", m.ActiveWindow().Cursor.Line)
}
})
t.Run("3cw (change 3 words) undo", func(t *testing.T) {
tm := newTestModelWithLines(t, []string{"one two three four five"})
sendKeys(tm, "3", "c", "w") // change 3 words
sendKeys(tm, "CHANGED") // type replacement
sendKeys(tm, "esc") // exit insert mode
sendKeys(tm, "u") // undo
m := getFinalModel(t, tm)
expected := []string{"one two three four five"}
if !equalStringSlices(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("Lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
t.Run("10x (delete 10 chars) undo", func(t *testing.T) {
tm := newTestModelWithLines(t, []string{"abcdefghijklmnopqrstuvwxyz"})
sendKeys(tm, "5", "|") // move to column 5 (f)
sendKeyString(tm, "10x") // delete 10 chars (fghijklmno)
sendKeys(tm, "u") // undo
m := getFinalModel(t, tm)
expected := []string{"abcdefghijklmnopqrstuvwxyz"}
if !equalStringSlices(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("Lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
// Cursor should be back at column 4 (index of 'e', 0-based)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("Cursor.Col = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("2cc (change 2 lines) undo", func(t *testing.T) {
tm := newTestModelWithLines(t, []string{"line1", "line2", "line3", "line4"})
sendKeys(tm, "j") // move to line2
sendKeys(tm, "2", "c", "c") // change 2 lines (line2, line3)
sendKeys(tm, "NEW", "LINE") // type replacement
sendKeys(tm, "esc") // exit insert mode
sendKeys(tm, "u") // undo
m := getFinalModel(t, tm)
expected := []string{"line1", "line2", "line3", "line4"}
if !equalStringSlices(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("Lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
t.Run("4diw (delete 4 words) undo", func(t *testing.T) {
tm := newTestModelWithLines(t, []string{"word1 word2 word3 word4 word5"})
sendKeys(tm, "w") // move to word2
sendKeyString(tm, "4diw") // delete 4 words (word2, word3, word4, word5)
sendKeys(tm, "u") // undo
m := getFinalModel(t, tm)
expected := []string{"word1 word2 word3 word4 word5"}
if !equalStringSlices(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("Lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
})
t.Run("complex count with paste: 3p after 2yy", func(t *testing.T) {
tm := newTestModelWithLines(t, []string{"A", "B", "C", "D"})
sendKeyString(tm, "2yy") // yank 2 lines (A, B)
sendKeys(tm, "j", "j") // move to line C
sendKeyString(tm, "3p") // paste 3 times
sendKeys(tm, "u") // undo paste
m := getFinalModel(t, tm)
expected := []string{"A", "B", "C", "D"}
if !equalStringSlices(bufferLinesToStrings(m.ActiveBuffer()), expected) {
t.Errorf("Lines = %v, want %v", m.ActiveBuffer().Lines, expected)
}
// Cursor should be back at line C (index 2)
if m.ActiveWindow().Cursor.Line != 2 {
t.Errorf("Cursor.Line = %d, want 2", m.ActiveWindow().Cursor.Line)
}
})
}

View File

@ -125,8 +125,8 @@ func TestVisualModeDelete(t *testing.T) {
sendKeys(tm, "v", "d")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "ello" {
t.Errorf("Line(0) = %q, want \"ello\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ello" {
t.Errorf("Line(0) = %q, want \"ello\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
@ -139,8 +139,8 @@ func TestVisualModeDelete(t *testing.T) {
sendKeys(tm, "v", "l", "l", "l", "d")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "o world" {
t.Errorf("Line(0) = %q, want \"o world\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "o world" {
t.Errorf("Line(0) = %q, want \"o world\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
@ -154,8 +154,8 @@ func TestVisualModeDelete(t *testing.T) {
// anchor=3, cursor=1 → normalized start=1, end=3 → delete "ell" → "ho"
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "ho" {
t.Errorf("Line(0) = %q, want \"ho\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ho" {
t.Errorf("Line(0) = %q, want \"ho\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 1 {
t.Errorf("CursorX() = %d, want 1", m.ActiveWindow().Cursor.Col)
@ -172,8 +172,8 @@ func TestVisualModeDelete(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "held" {
t.Errorf("Line(0) = %q, want \"held\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "held" {
t.Errorf("Line(0) = %q, want \"held\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Col != 2 {
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
@ -192,8 +192,8 @@ func TestVisualModeDelete(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "world" {
t.Errorf("Line(0) = %q, want \"world\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "world" {
t.Errorf("Line(0) = %q, want \"world\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
@ -209,8 +209,8 @@ func TestVisualModeDelete(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "testing" {
t.Errorf("Line(0) = %q, want \"testing\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "testing" {
t.Errorf("Line(0) = %q, want \"testing\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
@ -227,8 +227,8 @@ func TestVisualModeDelete(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "hello" {
t.Errorf("Line(0) = %q, want \"hello\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("Line(0) = %q, want \"hello\"", m.ActiveBuffer().Lines[0])
}
})
@ -241,11 +241,11 @@ func TestVisualModeDelete(t *testing.T) {
// "hello"[:0]+"hello"[2:] = "llo"
// "world"[:0]+"world"[2:] = "rld"
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "llo" {
t.Errorf("Line(0) = %q, want \"llo\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "llo" {
t.Errorf("Line(0) = %q, want \"llo\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "rld" {
t.Errorf("Line(1) = %q, want \"rld\"", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "rld" {
t.Errorf("Line(1) = %q, want \"rld\"", m.ActiveBuffer().Lines[1])
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
@ -264,11 +264,11 @@ func TestVisualModeDelete(t *testing.T) {
// "hello"[:1]+"hello"[4:] = "h"+"o" = "ho"
// "world"[:1]+"world"[4:] = "w"+"d" = "wd"
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "ho" {
t.Errorf("Line(0) = %q, want \"ho\"", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "ho" {
t.Errorf("Line(0) = %q, want \"ho\"", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "wd" {
t.Errorf("Line(1) = %q, want \"wd\"", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "wd" {
t.Errorf("Line(1) = %q, want \"wd\"", m.ActiveBuffer().Lines[1])
}
})
}
@ -302,8 +302,8 @@ func TestVisualModeWordMotions(t *testing.T) {
m := getFinalModel(t, tm)
// Deletes from 0 to 6 inclusive = "hello w", leaves "orld"
if m.ActiveBuffer().Lines[0].String() != "orld" {
t.Errorf("Line(0) = %q, want 'orld'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "orld" {
t.Errorf("Line(0) = %q, want 'orld'", m.ActiveBuffer().Lines[0])
}
})
@ -333,8 +333,8 @@ func TestVisualModeWordMotions(t *testing.T) {
m := getFinalModel(t, tm)
// Deletes "hello"
if m.ActiveBuffer().Lines[0].String() != " world" {
t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != " world" {
t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0])
}
})
@ -364,8 +364,8 @@ func TestVisualModeWordMotions(t *testing.T) {
m := getFinalModel(t, tm)
// Deletes from "h" (0) to "w" (6) inclusive
if m.ActiveBuffer().Lines[0].String() != "orld" {
t.Errorf("Line(0) = %q, want 'orld'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "orld" {
t.Errorf("Line(0) = %q, want 'orld'", m.ActiveBuffer().Lines[0])
}
})
@ -398,8 +398,9 @@ func TestVisualModeJumpMotions(t *testing.T) {
if m.ActiveWindow().Anchor.Col != 0 {
t.Errorf("AnchorX() = %d, want 0", m.ActiveWindow().Anchor.Col)
}
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
// $ moves past end of line
if m.ActiveWindow().Cursor.Col != 11 {
t.Errorf("CursorX() = %d, want 11", m.ActiveWindow().Cursor.Col)
}
})
@ -411,8 +412,8 @@ func TestVisualModeJumpMotions(t *testing.T) {
sendKeys(tm, "v", "$", "d")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello " {
t.Errorf("Line(0) = %q, want 'hello '", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello " {
t.Errorf("Line(0) = %q, want 'hello '", m.ActiveBuffer().Lines[0])
}
})
@ -441,8 +442,8 @@ func TestVisualModeJumpMotions(t *testing.T) {
m := getFinalModel(t, tm)
// Deletes from 'h' (0) to 'w' (6) inclusive
if m.ActiveBuffer().Lines[0].String() != "orld" {
t.Errorf("Line(0) = %q, want 'orld'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "orld" {
t.Errorf("Line(0) = %q, want 'orld'", m.ActiveBuffer().Lines[0])
}
})
@ -492,8 +493,8 @@ func TestVisualModeJumpMotions(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "lin 3" {
t.Errorf("Line(0) = %q, want 'lin 3'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "lin 3" {
t.Errorf("Line(0) = %q, want 'lin 3'", m.ActiveBuffer().Lines[0])
}
})
@ -526,8 +527,8 @@ func TestVisualModeJumpMotions(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "lin 3" {
t.Errorf("Line(0) = %q, want 'lin 3'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "lin 3" {
t.Errorf("Line(0) = %q, want 'lin 3'", m.ActiveBuffer().Lines[0])
}
})
}
@ -563,8 +564,8 @@ func TestVisualLineModeJumpMotions(t *testing.T) {
if m.ActiveBuffer().LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "" {
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
}
})

View File

@ -46,14 +46,14 @@ func TestYankLineBasic(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 1" {
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line 2" {
t.Errorf("Line(1) = %q, want 'line 2'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 2" {
t.Errorf("Line(1) = %q, want 'line 2'", m.ActiveBuffer().Lines[1])
}
if m.ActiveBuffer().Lines[2].String() != "line 3" {
t.Errorf("Line(2) = %q, want 'line 3'", m.ActiveBuffer().Lines[2].String())
if m.ActiveBuffer().Lines[2] != "line 3" {
t.Errorf("Line(2) = %q, want 'line 3'", m.ActiveBuffer().Lines[2])
}
})
@ -583,8 +583,8 @@ func TestYankWithCharwiseMotions(t *testing.T) {
sendKeys(tm, "y", "w")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello world" {
t.Errorf("Line(0) = %q, want 'hello world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello world" {
t.Errorf("Line(0) = %q, want 'hello world'", m.ActiveBuffer().Lines[0])
}
})
}
@ -659,8 +659,8 @@ func TestYankVisualCharwise(t *testing.T) {
sendKeys(tm, "v", "l", "l", "l", "l", "y")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello world" {
t.Errorf("Line(0) = %q, want 'hello world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello world" {
t.Errorf("Line(0) = %q, want 'hello world'", m.ActiveBuffer().Lines[0])
}
})
}
@ -904,8 +904,8 @@ func TestYankRegisterBehavior(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[1].String() != "to copy" {
t.Errorf("Line(1) = %q, want 'to copy'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "to copy" {
t.Errorf("Line(1) = %q, want 'to copy'", m.ActiveBuffer().Lines[1])
}
})
}
@ -1053,8 +1053,8 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
sendKeys(tm, "v", "l", "l", "l", "l", "y", "$", "p")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello worldhello" {
t.Errorf("Line(0) = %q, want 'hello worldhello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello worldhello" {
t.Errorf("Line(0) = %q, want 'hello worldhello'", m.ActiveBuffer().Lines[0])
}
})
@ -1067,8 +1067,8 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
sendKeys(tm, "v", "$", "y", "0", "P")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "worldhello world" {
t.Errorf("Line(0) = %q, want 'worldhello world'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "worldhello world" {
t.Errorf("Line(0) = %q, want 'worldhello world'", m.ActiveBuffer().Lines[0])
}
})
@ -1084,8 +1084,8 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
if m.ActiveBuffer().LineCount() != 4 {
t.Errorf("LineCount() = %d, want 4", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[2].String() != "line 1" {
t.Errorf("Line(2) = %q, want 'line 1'", m.ActiveBuffer().Lines[2].String())
if m.ActiveBuffer().Lines[2] != "line 1" {
t.Errorf("Line(2) = %q, want 'line 1'", m.ActiveBuffer().Lines[2])
}
})
@ -1101,11 +1101,11 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
if m.ActiveBuffer().LineCount() != 6 {
t.Errorf("LineCount() = %d, want 6", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[4].String() != "line 1" {
t.Errorf("Line(4) = %q, want 'line 1'", m.ActiveBuffer().Lines[4].String())
if m.ActiveBuffer().Lines[4] != "line 1" {
t.Errorf("Line(4) = %q, want 'line 1'", m.ActiveBuffer().Lines[4])
}
if m.ActiveBuffer().Lines[5].String() != "line 2" {
t.Errorf("Line(5) = %q, want 'line 2'", m.ActiveBuffer().Lines[5].String())
if m.ActiveBuffer().Lines[5] != "line 2" {
t.Errorf("Line(5) = %q, want 'line 2'", m.ActiveBuffer().Lines[5])
}
})
@ -1121,11 +1121,11 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
if m.ActiveBuffer().LineCount() != 4 {
t.Errorf("LineCount() = %d, want 4", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 3" {
t.Errorf("Line(0) = %q, want 'line 3'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 3" {
t.Errorf("Line(0) = %q, want 'line 3'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line 1" {
t.Errorf("Line(1) = %q, want 'line 1'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 1" {
t.Errorf("Line(1) = %q, want 'line 1'", m.ActiveBuffer().Lines[1])
}
})
@ -1140,14 +1140,14 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "original" {
t.Errorf("Line(0) = %q, want 'original'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "original" {
t.Errorf("Line(0) = %q, want 'original'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "original" {
t.Errorf("Line(1) = %q, want 'original'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "original" {
t.Errorf("Line(1) = %q, want 'original'", m.ActiveBuffer().Lines[1])
}
if m.ActiveBuffer().Lines[2].String() != "other" {
t.Errorf("Line(2) = %q, want 'other'", m.ActiveBuffer().Lines[2].String())
if m.ActiveBuffer().Lines[2] != "other" {
t.Errorf("Line(2) = %q, want 'other'", m.ActiveBuffer().Lines[2])
}
})
@ -1162,14 +1162,14 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "original" {
t.Errorf("Line(0) = %q, want 'original'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "original" {
t.Errorf("Line(0) = %q, want 'original'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "other" {
t.Errorf("Line(1) = %q, want 'other'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "other" {
t.Errorf("Line(1) = %q, want 'other'", m.ActiveBuffer().Lines[1])
}
if m.ActiveBuffer().Lines[2].String() != "other" {
t.Errorf("Line(2) = %q, want 'other'", m.ActiveBuffer().Lines[2].String())
if m.ActiveBuffer().Lines[2] != "other" {
t.Errorf("Line(2) = %q, want 'other'", m.ActiveBuffer().Lines[2])
}
})
@ -1182,8 +1182,8 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
sendKeys(tm, "y", "w", "$", "p")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello worldhello " {
t.Errorf("Line(0) = %q, want 'hello worldhello '", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello worldhello " {
t.Errorf("Line(0) = %q, want 'hello worldhello '", m.ActiveBuffer().Lines[0])
}
})
@ -1196,8 +1196,8 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
sendKeys(tm, "y", "e", "$", "p")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello worldhello" {
t.Errorf("Line(0) = %q, want 'hello worldhello'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "hello worldhello" {
t.Errorf("Line(0) = %q, want 'hello worldhello'", m.ActiveBuffer().Lines[0])
}
})
@ -1210,8 +1210,8 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
sendKeys(tm, "v", "l", "l", "y", "$", "p")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "abcdefghcde" {
t.Errorf("Line(0) = %q, want 'abcdefghcde'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "abcdefghcde" {
t.Errorf("Line(0) = %q, want 'abcdefghcde'", m.ActiveBuffer().Lines[0])
}
})
@ -1246,14 +1246,14 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0].String() != "line 2" {
t.Errorf("Line(0) = %q, want 'line 2'", m.ActiveBuffer().Lines[0].String())
if m.ActiveBuffer().Lines[0] != "line 2" {
t.Errorf("Line(0) = %q, want 'line 2'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1].String() != "line 1" {
t.Errorf("Line(1) = %q, want 'line 1'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 1" {
t.Errorf("Line(1) = %q, want 'line 1'", m.ActiveBuffer().Lines[1])
}
if m.ActiveBuffer().Lines[2].String() != "line 3" {
t.Errorf("Line(2) = %q, want 'line 3'", m.ActiveBuffer().Lines[2].String())
if m.ActiveBuffer().Lines[2] != "line 3" {
t.Errorf("Line(2) = %q, want 'line 3'", m.ActiveBuffer().Lines[2])
}
})
@ -1270,17 +1270,17 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
t.Errorf("LineCount() = %d, want 7", m.ActiveBuffer().LineCount())
}
// Original + 2 copies of 2 lines = 3 + 4 = 7
if m.ActiveBuffer().Lines[1].String() != "line 1" {
t.Errorf("Line(1) = %q, want 'line 1'", m.ActiveBuffer().Lines[1].String())
if m.ActiveBuffer().Lines[1] != "line 1" {
t.Errorf("Line(1) = %q, want 'line 1'", m.ActiveBuffer().Lines[1])
}
if m.ActiveBuffer().Lines[2].String() != "line 2" {
t.Errorf("Line(2) = %q, want 'line 2'", m.ActiveBuffer().Lines[2].String())
if m.ActiveBuffer().Lines[2] != "line 2" {
t.Errorf("Line(2) = %q, want 'line 2'", m.ActiveBuffer().Lines[2])
}
if m.ActiveBuffer().Lines[3].String() != "line 1" {
t.Errorf("Line(3) = %q, want 'line 1'", m.ActiveBuffer().Lines[3].String())
if m.ActiveBuffer().Lines[3] != "line 1" {
t.Errorf("Line(3) = %q, want 'line 1'", m.ActiveBuffer().Lines[3])
}
if m.ActiveBuffer().Lines[4].String() != "line 2" {
t.Errorf("Line(4) = %q, want 'line 2'", m.ActiveBuffer().Lines[4].String())
if m.ActiveBuffer().Lines[4] != "line 2" {
t.Errorf("Line(4) = %q, want 'line 2'", m.ActiveBuffer().Lines[4])
}
})
}

View File

@ -51,9 +51,6 @@ type Model struct {
// Visual styles
styles style.Styles
// Dot operator state
lastChangeKeys []string
}
// Model.Init: Initialize the model and start any commands that may need to run. Required
@ -123,25 +120,6 @@ func (m *Model) GetLastFind() *core.LastFindCommand {
return &m.lastFind
}
// Does update the '.' register
func (m *Model) SetLastChangeKeys(keys []string) {
m.lastChangeKeys = keys
m.SetRegister('.', core.CharwiseRegister, []string{strings.Join(keys, "")})
}
func (m *Model) LastChangeKeys() []string {
return m.lastChangeKeys
}
func (m *Model) ClearLastChangeKeys() {
m.lastChangeKeys = []string{}
}
func (m *Model) HandleKey(key string) tea.Cmd {
return m.input.Handle(m, key)
}
func (m *Model) ExitInsertMode() {
win := m.ActiveWindow()
if m.insertCount > 1 {

View File

@ -11,7 +11,76 @@ type ModelBuilder struct {
model Model
}
// NewModelBuilder: Builds and returns a new model, using the default color scheme (kanagawa-wave).
// RPGLE
// abap
// algol
// algol_nu
// arduino
// ashen
// aura-theme-dark
// aura-theme-dark-soft
// autumn
// average
// base16-snazzy
// borland
// bw
// catppuccin-frappe
// catppuccin-latte
// catppuccin-macchiato
// catppuccin-mocha
// colorful
// doom-one
// doom-one2
// dracula
// emacs
// evergarden
// friendly
// fruity
// github
// github-dark
// gruvbox
// gruvbox-light
// hr_high_contrast
// hrdark
// igor
// lovelace
// manni
// modus-operandi
// modus-vivendi
// monokai
// monokailight
// murphy
// native
// nord
// nordic
// onedark
// onesenterprise
// paraiso-dark
// paraiso-light
// pastie
// perldoc
// pygments
// rainbow_dash
// rose-pine
// rose-pine-dawn
// rose-pine-moon
// rrt
// solarized-dark
// solarized-dark256
// solarized-light
// swapoff
// tango
// tokyonight-day
// tokyonight-moon
// tokyonight-night
// tokyonight-storm
// trac
// vim
// vs
// vulcan
// witchhazel
// xcode
// xcode-dark
func NewModelBuilder() *ModelBuilder {
chromaStyle := styles.Get("kanagawa-wave")

View File

@ -1,95 +1,83 @@
package editor
import (
"git.gophernest.net/azpect/TextEditor/internal/core"
"git.gophernest.net/azpect/TextEditor/internal/motion"
tea "github.com/charmbracelet/bubbletea"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
// Model.Update: Handles BubbleTea messages including window resizes and key
// presses. Routes input to the handler and adjusts scroll after updates.
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
var cmd tea.Cmd
switch msg := msg.(type) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.termHeight = msg.Height
m.termWidth = msg.Width
case tea.WindowSizeMsg:
m.termHeight = msg.Height
m.termWidth = msg.Width
// TODO: Implement a layout method that handles this
//
// func (m *Model) layoutWindows() {
// if len(m.windows) == 0 {
// return
// }
//
// if len(m.windows) == 1 {
// // Single window - full screen
// m.windows[0].Width = m.termWidth
// m.windows[0].Height = m.termHeight
// return
// }
//
// // Multiple windows - distribute space
// // This is where you'd implement split layout logic
// // For example, horizontal split:
// halfHeight := m.termHeight / 2
// for i, win := range m.windows {
// win.Width = m.termWidth
// if i < len(m.windows)-1 {
// win.Height = halfHeight
// } else {
// // Last window gets remainder
// win.Height = m.termHeight - (halfHeight * (len(m.windows) - 1))
// }
// }
// }
for i := range m.windows {
m.windows[i].Height = msg.Height
m.windows[i].Width = msg.Width
}
// TODO: Implement a layout method that handles this
//
// func (m *Model) layoutWindows() {
// if len(m.windows) == 0 {
// return
// }
//
// if len(m.windows) == 1 {
// // Single window - full screen
// m.windows[0].Width = m.termWidth
// m.windows[0].Height = m.termHeight
// return
// }
//
// // Multiple windows - distribute space
// // This is where you'd implement split layout logic
// // For example, horizontal split:
// halfHeight := m.termHeight / 2
// for i, win := range m.windows {
// win.Width = m.termWidth
// if i < len(m.windows)-1 {
// win.Height = halfHeight
// } else {
// // Last window gets remainder
// win.Height = m.termHeight - (halfHeight * (len(m.windows) - 1))
// }
// }
// }
for i := range m.windows {
m.windows[i].Height = msg.Height
m.windows[i].Width = msg.Width
}
// TODO: This is not great, totally temporary. But I don't like vim's handling, so this is up to me
case tea.MouseMsg:
switch msg.Button {
case tea.MouseButtonWheelUp:
scrollAction := motion.ScrollUpPage{Divisor: 4} // Quarter page
cmd = scrollAction.Execute(m)
case tea.MouseButtonWheelDown:
scrollAction := motion.ScrollDownPage{Divisor: 4} // Quarter page
cmd = scrollAction.Execute(m)
}
case tea.KeyMsg:
// TODO: This needs to be removed, but for now its required for the tests.
// Ctrl+C always quits regardless of mode
if msg.Type == tea.KeyCtrlC {
return m, tea.Quit
}
case tea.KeyMsg:
// TODO: This needs to be removed, but for now its required for the tests.
// Ctrl+C always quits regardless of mode
if msg.Type == tea.KeyCtrlC {
return m, tea.Quit
}
// TODO: This is not great
// TODO: Any vim action should exit also
// Simple override for command output mode for now
if m.Mode() == core.CommandOutputMode {
// TODO: Implement g/G/d/u
switch msg.String() {
case "enter":
m.SetMode(core.NormalMode)
m.SetCommandOutput(&core.CommandOutput{})
case "j":
m.CommandOutput().ScrollDown(m.termHeight)
case "k":
m.CommandOutput().ScrollUp()
}
} else {
cmd = m.input.Handle(m, msg.String())
}
}
// TODO: This is not great
// TODO: Any vim action should exit also
// Simple override for command output mode for now
if m.Mode() == core.CommandOutputMode {
// TODO: Implement g/G/d/u
switch msg.String() {
case "enter":
m.SetMode(core.NormalMode)
m.SetCommandOutput(&core.CommandOutput{})
case "j":
m.CommandOutput().ScrollDown(m.termHeight)
case "k":
m.CommandOutput().ScrollUp()
}
} else {
cmd = m.input.Handle(m, msg.String())
}
}
// Keep cursor in view after any update
win := m.ActiveWindow()
win.AdjustScroll()
// Keep cursor in view after any update
win := m.ActiveWindow()
win.AdjustScroll()
return m, cmd
return m, cmd
}

View File

@ -2,11 +2,12 @@ package editor
import (
"fmt"
"strconv"
"strings"
"git.gophernest.net/azpect/TextEditor/internal/core"
"git.gophernest.net/azpect/TextEditor/internal/style"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/charmbracelet/lipgloss"
)
@ -23,10 +24,6 @@ func (m Model) View() string {
styles := m.Styles()
options := win.Options
// Adjust gutter to fit line len
maxLineLen := len(strconv.Itoa(win.Buffer.LineCount()))
options.GutterSize = max(options.GutterSize, maxLineLen+2)
// Draw window
view := viewWindow(win, styles, options, m.Mode())
@ -55,7 +52,12 @@ func viewWindow(w *core.Window, styles style.Styles, options core.WinOptions, mo
end := w.ScrollY + w.ViewportHeight()
// Chroma stuff
lexer := style.GetLexer(buf)
name := strings.ReplaceAll(buf.Filetype, ".", "")
lexer := lexers.Get(name)
if lexer == nil {
lexer = lexers.Fallback
}
lexer = chroma.Coalesce(lexer) // Merge tokens together
// Draw buffer lines
for lineNum := start; lineNum < end; lineNum++ {
@ -339,11 +341,7 @@ func overlayCommandOutputWindow(view string, cmd *core.CommandOutput, styles sty
content := styles.LineStyle.Render(strings.ReplaceAll(l, "\n", "\\n"))
overlay = append(overlay, content)
}
msg := core.CommandOutputExitMessage
if len(cmd.Lines) > len(cmd.Viewport(termHeight)) {
msg += ". " + core.CommandOutputScrollMessage
}
overlay = append(overlay, styles.CommandContinueMessage.Render(msg))
overlay = append(overlay, styles.CommandContinueMessage.Render(core.CommandOutputExitMessage))
// NOTE: strings.Split on "\n" is safe as long as no style uses .Width()/.Height()/.Padding()/.Margin(),
// which would cause Lipgloss to embed newlines internally and corrupt the line count.

View File

@ -1,11 +1,8 @@
package input
import (
"slices"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
"git.gophernest.net/azpect/TextEditor/internal/operator"
tea "github.com/charmbracelet/bubbletea"
)
@ -34,14 +31,10 @@ type Handler struct {
charMotionType string // which char motion is waiting: "f", "t", "F", or "T"
modifier string // which modifier used for text object: "i" or "a"
// Dot operator - accumulate keys for current operation
recordingKeys []string
// Keymaps
normalKeymap *Keymap
visualKeymap *Keymap
insertKeymap *Keymap
replaceKeymap *Keymap
commandKeymap *Keymap
currentKeymap *Keymap
@ -54,7 +47,6 @@ func NewHandler() *Handler {
normalKeymap: NewNormalKeymap(),
visualKeymap: NewVisualKeymap(),
insertKeymap: NewInsertKeymap(),
replaceKeymap: NewReplaceKeymap(),
commandKeymap: NewCommandKeymap(),
currentKeymap: nil,
}
@ -63,29 +55,10 @@ func NewHandler() *Handler {
// Handler.Handle: Main entry point for processing a keypress. Routes to appropriate
// handler based on current mode and state.
func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
ignoreKeys := []string{".", "u", "ctrl+r"}
// Record key for dot operator (except in insert/command mode which handle separately)
if m.Mode() != core.InsertMode && m.Mode() != core.CommandMode && !slices.Contains(ignoreKeys, key) {
h.recordingKeys = append(h.recordingKeys, key)
}
// ESC always resets everything
if key == "esc" {
// If insert mode, keep the escape
if m.Mode() == core.InsertMode {
m.SetLastChangeKeys(append(m.LastChangeKeys(), key))
}
h.recordingKeys = []string{} // Clear recording on ESC
h.Reset()
if m.Mode() == core.InsertMode || m.Mode() == core.ReplaceMode {
// Before exiting insert mode, end the block in the undo stack
win := m.ActiveWindow()
buf := m.ActiveBuffer()
if buf.UndoStack != nil {
buf.UndoStack.EndBlock(win.Cursor)
}
if m.Mode() == core.InsertMode {
m.ExitInsertMode()
} else {
m.SetMode(core.NormalMode)
@ -97,8 +70,6 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
switch m.Mode() {
case core.InsertMode:
return h.handleInsertKey(m, key)
case core.ReplaceMode:
return h.handleReplaceKey(m, key)
case core.CommandMode:
return h.handleCommandKey(m, key)
}
@ -176,9 +147,6 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
func (h *Handler) dispatch(m action.Model, kind string, binding any, key string) tea.Cmd {
// Handle character motions (f/t/F/T) - transition to waiting state
if kind == "char_motion" {
if key == "r" {
m.SetMode(core.WaitingMode)
}
h.charMotionType = key
h.state = StateWaitingForChar
return nil
@ -212,13 +180,7 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
if res, ok := mot.(action.Resolvable); ok {
mot = res.Resolve(m)
}
cmd := h.executeMotion(m, mot)
// Only clear recording for pure motions in normal mode
// In visual mode, motions are part of building the selection
if !m.Mode().IsVisualMode() {
h.recordingKeys = []string{}
}
cmd := mot.Execute(m)
h.Reset()
return cmd
@ -232,12 +194,12 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
if m.Mode() == core.VisualLineMode {
mtype = core.Linewise
}
cmd := h.executeOperator(m, op, start, end, mtype)
cmd := op.Operate(m, start, end, mtype)
// Only reset to normal mode if operator didn't enter insert mode
if m.Mode() != core.InsertMode {
m.SetMode(core.NormalMode)
}
h.RecordAndReset(m)
h.Reset()
return cmd
}
// In normal mode, wait for a motion to define the range
@ -251,13 +213,8 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
if r, ok := act.(action.Repeatable); ok {
act = r.WithCount(count)
}
cmd := h.executeAction(m, act)
// Only record if we're not entering visual mode (visual ops record when they complete)
if m.Mode().IsVisualMode() {
h.Reset() // In visual mode now, don't save yet
} else {
h.RecordAndReset(m)
}
cmd := act.Execute(m)
h.Reset()
return cmd
}
@ -275,8 +232,8 @@ func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any,
if kind == "operator" && key == h.operatorKey {
// Only call DoublePress if the operator supports it
if dp, ok := h.operator.(action.DoublePresser); ok {
cmd := h.executeDoublePress(m, dp, count)
h.RecordAndReset(m)
cmd := dp.DoublePress(m, count)
h.Reset()
return cmd
}
h.Reset()
@ -301,10 +258,10 @@ func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any,
}
// Get range and motion type
start := win.Cursor
h.executeMotion(m, mot)
mot.Execute(m)
end := win.Cursor
cmd := h.executeOperator(m, h.operator, start, end, mot.Type())
h.RecordAndReset(m)
cmd := h.operator.Operate(m, start, end, mot.Type())
h.Reset()
return cmd
}
@ -369,34 +326,23 @@ func (h *Handler) handleCharMotion(m action.Model, key string) tea.Cmd {
// Apply count if supported
if r, ok := mot.(action.Repeatable); ok {
result := r.WithCount(count)
// WithCount returns Action, but char motions still implement Motion
if m, ok := result.(action.Motion); ok {
mot = m
}
mot = r.WithCount(count).(action.Motion)
}
// If operator pending (e.g., "df{char}"), get range and operate
if h.operator != nil {
win := m.ActiveWindow()
start := win.Cursor
h.executeMotion(m, mot)
mot.Execute(m)
end := win.Cursor
cmd := h.executeOperator(m, h.operator, start, end, mot.Type())
h.RecordAndReset(m)
cmd := h.operator.Operate(m, start, end, mot.Type())
h.Reset()
return cmd
}
// Otherwise just execute the motion
cmd := h.executeMotion(m, mot)
// ReplaceChar modifies the buffer, so it should be repeatable with '.'
// (unlike f/t/F/T which are pure motions)
if _, isReplace := mot.(action.ReplaceChar); isReplace {
h.RecordAndReset(m)
} else {
h.Reset()
}
cmd := mot.Execute(m)
h.Reset()
return cmd
}
@ -420,8 +366,8 @@ func (h *Handler) handleTextObject(m action.Model, kind string, binding any, key
// If we have an operator pending (e.g., "diw")
if h.operator != nil {
cmd := h.executeOperator(m, h.operator, start, end, mtype)
h.RecordAndReset(m)
cmd := h.operator.Operate(m, start, end, mtype)
h.Reset()
return cmd
}
@ -503,7 +449,6 @@ func (h *Handler) effectiveCount() int {
}
// Handler.Reset: Clears all handler state including counts, operators, and buffers.
// Does NOT clear recordingKeys - those accumulate across an operation.
func (h *Handler) Reset() {
h.state = StateReady
h.count1 = 0
@ -514,28 +459,6 @@ func (h *Handler) Reset() {
h.pending = ""
h.charMotionType = ""
h.modifier = ""
// NOTE: recordingKeys is NOT cleared here - it accumulates across the operation
}
func (h *Handler) RecordAndReset(m action.Model) {
// Save the recorded keys to the model for dot operator
// Filter out mode-switch keys that don't modify the buffer
ignoreStates := []string{":", "v", "V", "."}
if len(h.recordingKeys) > 0 {
// Check if the entire sequence is just a mode switch
shouldRecord := true
if len(h.recordingKeys) == 1 && slices.Contains(ignoreStates, h.recordingKeys[0]) {
shouldRecord = false
}
if shouldRecord {
m.SetLastChangeKeys(h.recordingKeys)
}
}
h.recordingKeys = []string{} // Clear recording after saving
h.Reset()
}
// Handler.Pending: Returns the accumulated input buffer for display.
@ -545,21 +468,9 @@ func (h *Handler) Pending() string {
// Handler.handleInsertKey: Processes a keypress in insert mode, recording it
// for count replay and executing it as an action or character insertion.
//
// This function does not make use of the execute abstractions, to prevent each
// key inserted from creating a new block in the undo stack.
func (h *Handler) handleInsertKey(m action.Model, key string) tea.Cmd {
buf := m.ActiveBuffer()
win := m.ActiveWindow()
// Start undo block on first insert key
if buf.UndoStack != nil && !buf.UndoStack.Recording() {
buf.UndoStack.BeginBlock(win.Cursor)
}
// Record the key for count replay (e.g. 5i...)
m.SetInsertKeys(append(m.InsertKeys(), key))
m.SetLastChangeKeys(append(m.LastChangeKeys(), key))
// Check the insert keymap first
kind, binding := h.insertKeymap.Lookup(key)
@ -574,35 +485,8 @@ func (h *Handler) handleInsertKey(m action.Model, key string) tea.Cmd {
return action.InsertChar{Char: key}.Execute(m)
}
func (h *Handler) handleReplaceKey(m action.Model, key string) tea.Cmd {
buf := m.ActiveBuffer()
win := m.ActiveWindow()
// Start undo block on first insert key
if buf.UndoStack != nil && !buf.UndoStack.Recording() {
buf.UndoStack.BeginBlock(win.Cursor)
}
// Record the key for count replay (e.g. 5i...)
m.SetInsertKeys(append(m.InsertKeys(), key))
m.SetLastChangeKeys(append(m.LastChangeKeys(), key))
// Check the insert keymap first
kind, binding := h.replaceKeymap.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.ReplaceModeChar{Char: key}.Execute(m)
}
// Handler.handleCommandKey: Processes a keypress in command mode, executing
// it as an action or inserting it into the command line. This does not record
// anything into the undo stack.
// it as an action or inserting it into the command line.
func (h *Handler) handleCommandKey(m action.Model, key string) tea.Cmd {
kind, binding := h.commandKeymap.Lookup(key)
switch kind {
@ -627,83 +511,3 @@ func normalizeVisualSelection(m action.Model) (core.Position, core.Position) {
}
return c, a
}
func (h *Handler) executeAction(m action.Model, act action.Action) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
if buf.UndoStack != nil {
buf.UndoStack.BeginBlock(win.Cursor)
}
cmd := act.Execute(m)
// If the action one that includes insert mode, we should not end the block, we want to
// include the text from the insert mode in the block.
_, O := act.(action.OpenLineAbove)
_, o := act.(action.OpenLineBelow)
_, s := act.(action.SubstituteChar)
_, S := act.(action.SubstituteLine)
_, C := act.(action.ChangeToEndOfLine)
if o || O || s || S || C {
return nil
}
if buf.UndoStack != nil {
buf.UndoStack.EndBlock(win.Cursor)
}
return cmd
}
func (h *Handler) executeMotion(m action.Model, mot action.Motion) tea.Cmd {
// These do not change the buffer, so no need to record anything
return mot.Execute(m)
}
func (h *Handler) executeOperator(m action.Model, op action.Operator, start, end core.Position, mtype core.MotionType) tea.Cmd {
buf := m.ActiveBuffer()
win := m.ActiveWindow()
if buf.UndoStack != nil {
buf.UndoStack.BeginBlock(win.Cursor)
}
cmd := op.Operate(m, start, end, mtype)
// If operator is one that enters insert mode, we do not want to end the block.
_, c := op.(operator.ChangeOperator)
if c {
return cmd
}
if buf.UndoStack != nil {
buf.UndoStack.EndBlock(win.Cursor)
}
return cmd
}
func (h *Handler) executeDoublePress(m action.Model, dp action.DoublePresser, count int) tea.Cmd {
buf := m.ActiveBuffer()
win := m.ActiveWindow()
if buf.UndoStack != nil {
buf.UndoStack.BeginBlock(win.Cursor)
}
cmd := dp.DoublePress(m, count)
// If operator being double pressed is one that enters insert mode, we do not
// want to end the block.
_, c := dp.(operator.ChangeOperator)
if c {
return cmd
}
if buf.UndoStack != nil {
buf.UndoStack.EndBlock(win.Cursor)
}
return cmd
}

View File

@ -38,15 +38,10 @@ func NewNormalKeymap() *Keymap {
"e": motion.MoveForwardWordEnd{Count: 1},
"E": motion.MoveForwardWORDEnd{Count: 1},
"b": motion.MoveBackwardWord{Count: 1},
"B": motion.MoveBackwardWORD{Count: 1},
"ge": motion.MoveBackwardWordEnd{Count: 1},
"gE": motion.MoveBackwardWORDEnd{Count: 1},
"ctrl+u": motion.ScrollUpPage{Divisor: 2},
"ctrl+d": motion.ScrollDownPage{Divisor: 2},
"ctrl+b": motion.ScrollUpPage{Divisor: 1},
"ctrl+f": motion.ScrollDownPage{Divisor: 1},
"ctrl+u": motion.ScrollUpHalfPage{},
"ctrl+d": motion.ScrollDownHalfPage{},
";": action.RepeatFind{Count: 1, Reverse: false},
",": action.RepeatFind{Count: 1, Reverse: true},
".": action.RepeatFind{Count: 1, Reverse: true},
},
operators: map[string]action.Operator{
"d": operator.DeleteOperator{},
@ -74,17 +69,12 @@ func NewNormalKeymap() *Keymap {
"S": action.SubstituteLine{Count: 1},
"p": action.Paste{Count: 1},
"P": action.PasteBefore{Count: 1},
"u": action.Undo{},
"ctrl+r": action.Redo{},
".": action.Repeat{Count: 1},
"R": action.EnterReplace{},
},
charMotions: map[string]action.Motion{
"f": action.FindChar{Forward: true, Inclusive: true, Repeated: false},
"F": action.FindChar{Forward: false, Inclusive: true, Repeated: false},
"t": action.FindChar{Forward: true, Inclusive: false, Repeated: false},
"T": action.FindChar{Forward: false, Inclusive: false, Repeated: false},
"r": action.ReplaceChar{Count: 1},
},
modifiers: map[string]any{
"i": nil,
@ -115,32 +105,22 @@ func NewNormalKeymap() *Keymap {
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{},
"^": motion.MoveToLineContentStart{},
"|": motion.MoveToColumn{Count: 0},
"w": motion.MoveForwardWord{Count: 1},
"W": motion.MoveForwardWORD{Count: 1},
"e": motion.MoveForwardWordEnd{Count: 1},
"E": motion.MoveForwardWORDEnd{Count: 1},
"b": motion.MoveBackwardWord{Count: 1},
"B": motion.MoveBackwardWORD{Count: 1},
"ge": motion.MoveBackwardWordEnd{Count: 1},
"gE": motion.MoveBackwardWORDEnd{Count: 1},
"ctrl+u": motion.ScrollUpPage{Divisor: 2},
"ctrl+d": motion.ScrollDownPage{Divisor: 2},
"ctrl+b": motion.ScrollUpPage{Divisor: 1},
"ctrl+f": motion.ScrollDownPage{Divisor: 1},
";": action.RepeatFind{Count: 1, Reverse: false},
",": action.RepeatFind{Count: 1, Reverse: true},
// TODO: O and o. These are fun ones! Should be simple too
"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{},
"^": motion.MoveToLineContentStart{},
"|": motion.MoveToColumn{Count: 0},
"w": motion.MoveForwardWord{Count: 1},
"W": motion.MoveForwardWORD{Count: 1},
"e": motion.MoveForwardWordEnd{Count: 1},
"E": motion.MoveForwardWORDEnd{Count: 1},
"b": motion.MoveBackwardWord{Count: 1},
},
operators: map[string]action.Operator{
"d": operator.DeleteOperator{},
@ -148,13 +128,9 @@ func NewVisualKeymap() *Keymap {
"X": operator.DeleteOperator{},
"y": operator.YankOperator{},
"c": operator.ChangeOperator{},
"s": operator.ChangeOperator{}, // Same as c in visual mode
"R": operator.ChangeOperator{}, // Seems to do the same thing
},
actions: map[string]action.Action{
"p": action.VisualPaste{Count: 1, Replace: true},
"P": action.VisualPaste{Count: 1, Replace: false},
".": action.Repeat{Count: 1},
"p": action.VisualPaste{Count: 1},
// ":": action.EnterComandMode{}, // Different OP
},
charMotions: map[string]action.Motion{
@ -206,26 +182,7 @@ func NewInsertKeymap() *Keymap {
"ctrl+w": action.InsertDeletePreviousWord{},
},
}
}
// NewReplaceKeymap: Creates a keymap for replace mode with editing actions. All actions
func NewReplaceKeymap() *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.ReplaceNewline{},
"backspace": action.InsertBackspace{},
"delete": action.InsertDelete{},
"tab": action.ReplaceTab{}, // TODO: This needs replacing
"ctrl+w": action.InsertDeletePreviousWord{},
},
}
}
// NewCommandKeymap: Creates a keymap for command mode with command line editing.

View File

@ -80,7 +80,7 @@ type MoveRight struct {
func (a MoveRight) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
lineLen := buf.Lines[win.Cursor.Line].Len()
lineLen := len(buf.Lines[win.Cursor.Line])
for i := 0; i < a.Count && win.Cursor.Col <= lineLen; i++ {
win.SetCursorCol(win.Cursor.Col + 1)
}

View File

@ -50,7 +50,7 @@ type MoveToLineEnd struct{}
func (a MoveToLineEnd) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
win.SetCursorCol(buf.Lines[win.Cursor.Line].Len() - 1)
win.SetCursorCol(len(buf.Lines[win.Cursor.Line]))
return nil
}
@ -65,7 +65,7 @@ func (a MoveToLineContentStart) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
line := buf.Line(win.Cursor.Line)
line := buf.Lines[win.Cursor.Line]
x := 0
for x < len(line) {
ch := line[x]
@ -96,7 +96,7 @@ func (a MoveToColumn) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
line := buf.Line(win.Cursor.Line)
line := buf.Lines[win.Cursor.Line]
col := min(a.Count-1, len(line)-1)
win.SetCursorCol(col)
@ -111,23 +111,21 @@ func (a MoveToColumn) WithCount(n int) action.Action {
// TODO: Count for these, maybe?
// ScrollDownPage implements Motion (ctrl+d) - linewise
type ScrollDownPage struct {
Divisor int
}
// ScrollDownHalfPage implements Motion (ctrl+d) - linewise
type ScrollDownHalfPage struct{}
// ScrollDownHalfPage.Execute: Scrolls down half a page while maintaining the
// cursor's relative position in the viewport.
func (a ScrollDownPage) Execute(m action.Model) tea.Cmd {
func (a ScrollDownHalfPage) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
viewportHeight := win.ViewportHeight()
viewportHeight := win.Height - 2
if viewportHeight <= 0 {
return nil
}
scroll := viewportHeight / a.Divisor
scroll := viewportHeight / 2
scrollOff := win.Options.ScrollOff
// Current relative position in viewport
@ -154,24 +152,22 @@ func (a ScrollDownPage) Execute(m action.Model) tea.Cmd {
return nil
}
func (a ScrollDownPage) Type() core.MotionType { return core.Linewise }
func (a ScrollDownHalfPage) Type() core.MotionType { return core.Linewise }
// ScrollUpPage implements Motion (ctrl+u) - linewise
type ScrollUpPage struct {
Divisor int
}
// ScrollUpHalfPage implements Motion (ctrl+u) - linewise
type ScrollUpHalfPage struct{}
// ScrollUpHalfPage.Execute: Scrolls up half a page while maintaining the
// cursor's relative position in the viewport.
func (a ScrollUpPage) Execute(m action.Model) tea.Cmd {
func (a ScrollUpHalfPage) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
viewportHeight := win.ViewportHeight()
viewportHeight := win.Height - 2
if viewportHeight <= 0 {
return nil
}
scroll := viewportHeight / a.Divisor
scroll := viewportHeight / 2
scrollOff := win.Options.ScrollOff
// Current relative position in viewport
@ -197,4 +193,4 @@ func (a ScrollUpPage) Execute(m action.Model) tea.Cmd {
return nil
}
func (a ScrollUpPage) Type() core.MotionType { return core.Linewise }
func (a ScrollUpHalfPage) Type() core.MotionType { return core.Linewise }

View File

@ -24,7 +24,7 @@ func isWordPunctuation(c byte) bool {
// nextWordStart: Finds the start of the next word from position (x,y), handling
// word boundaries and line crossing.
func nextWordStart(buf *core.Buffer, x, y int) (int, int) {
line := buf.Line(y)
line := buf.Lines[y]
// Skip current class
if x < len(line) {
@ -59,7 +59,7 @@ func nextWordStart(buf *core.Buffer, x, y int) (int, int) {
// Move to first char of next line
y++
line = buf.Line(y)
line = buf.Lines[y]
x = 0
// If the first char of the new line is no whitespace, stay here!
@ -74,7 +74,7 @@ func nextWordStart(buf *core.Buffer, x, y int) (int, int) {
// nextWORDStart: Finds the start of the next WORD from position (x,y), treating
// all non-whitespace as a single class.
func nextWORDStart(buf *core.Buffer, x, y int) (int, int) {
line := buf.Line(y)
line := buf.Lines[y]
// Skip current WORD (all non-whitespace is one class for W)
for x < len(line) && line[x] != ' ' && line[x] != '\t' {
@ -100,7 +100,7 @@ func nextWORDStart(buf *core.Buffer, x, y int) (int, int) {
// Move to first char of next line
y++
line = buf.Line(y)
line = buf.Lines[y]
x = 0
// If the first char of the new line is no whitespace, stay here!
@ -115,7 +115,7 @@ func nextWORDStart(buf *core.Buffer, x, y int) (int, int) {
// nextWordEnd: Finds the end of the next word from position (x,y), respecting
// word character classes.
func nextWordEnd(buf *core.Buffer, x, y int) (int, int) {
line := buf.Line(y)
line := buf.Lines[y]
// Advance once to avoid being stuck on the current end
x++
@ -128,7 +128,7 @@ func nextWordEnd(buf *core.Buffer, x, y int) (int, int) {
// Otherwise, move to next line
y++
x = 0
line = buf.Line(y)
line = buf.Lines[y]
}
// Skip whitespace and cross lines if needed
@ -150,7 +150,7 @@ func nextWordEnd(buf *core.Buffer, x, y int) (int, int) {
// Move to first char of next line
y++
line = buf.Line(y)
line = buf.Lines[y]
x = 0
}
@ -174,7 +174,7 @@ func nextWordEnd(buf *core.Buffer, x, y int) (int, int) {
// nextWORDEnd: Finds the end of the next WORD from position (x,y), treating
// all non-whitespace as a single class.
func nextWORDEnd(buf *core.Buffer, x, y int) (int, int) {
line := buf.Line(y)
line := buf.Lines[y]
// Advance once to avoid being stuck on the current end
x++
@ -187,7 +187,7 @@ func nextWORDEnd(buf *core.Buffer, x, y int) (int, int) {
// Otherwise, move to next line
y++
x = 0
line = buf.Line(y)
line = buf.Lines[y]
}
// Skip whitespace and cross lines if needed
@ -209,7 +209,7 @@ func nextWORDEnd(buf *core.Buffer, x, y int) (int, int) {
// Move to first char of next line
y++
line = buf.Line(y)
line = buf.Lines[y]
x = 0
}
@ -224,7 +224,7 @@ func nextWORDEnd(buf *core.Buffer, x, y int) (int, int) {
// prevWordStart: Finds the start of the previous word from position (x,y),
// moving backward through character classes.
func prevWordStart(buf *core.Buffer, x, y int) (int, int) {
line := buf.Line(y)
line := buf.Lines[y]
// Back one to avoid being stuck on the current start
x--
@ -233,7 +233,7 @@ func prevWordStart(buf *core.Buffer, x, y int) (int, int) {
return 0, 0 // beginning of file, stay put
}
y--
line = buf.Line(y)
line = buf.Lines[y]
x = len(line) - 1
if x < 0 {
return 0, y // landed on an empty line
@ -252,7 +252,7 @@ func prevWordStart(buf *core.Buffer, x, y int) (int, int) {
return 0, 0
}
y--
line = buf.Line(y)
line = buf.Lines[y]
x = len(line) - 1
if len(line) == 0 {
return 0, y // empty line acts as a word boundary
@ -412,290 +412,3 @@ func (a MoveBackwardWord) Type() core.MotionType { return core.CharwiseExclusive
func (a MoveBackwardWord) WithCount(n int) action.Action {
return MoveBackwardWord{Count: n}
}
// prevWORDStart: Finds the start of the previous WORD from position (x,y),
// treating all non-whitespace as a single class.
func prevWORDStart(buf *core.Buffer, x, y int) (int, int) {
line := buf.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 = buf.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 = buf.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 WORD (all non-whitespace is one class)
for x-1 >= 0 && line[x-1] != ' ' && line[x-1] != '\t' {
x--
}
return x, y
}
// prevWordEnd: Finds the end of the previous word from position (x,y),
// respecting word character classes.
func prevWordEnd(buf *core.Buffer, x, y int) (int, int) {
line := buf.Line(y)
origY := y
// Back one to avoid being stuck on the current end
x--
if x < 0 {
if y == 0 {
return 0, 0 // beginning of file, stay put
}
y--
line = buf.Line(y)
x = len(line) - 1
// Don't return early for empty line - we'll handle it in whitespace skip
}
// Skip backward through current word class if we're on one
// BUT: if we crossed lines in the "back one" step, we're already at the end of a word
if y == origY && x >= 0 && line[x] != ' ' && line[x] != '\t' {
if isWordChar(line[x]) {
// Skip word characters
for x >= 0 && isWordChar(line[x]) {
x--
if x < 0 {
if y == 0 {
return 0, 0
}
y--
line = buf.Line(y)
x = len(line) - 1
if x < 0 {
return 0, y
}
}
}
} else {
// Skip punctuation
for x >= 0 && isWordPunctuation(line[x]) {
x--
if x < 0 {
if y == 0 {
return 0, 0
}
y--
line = buf.Line(y)
x = len(line) - 1
if x < 0 {
return 0, y
}
}
}
}
}
// 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, this is our word end!
}
if y == 0 {
return 0, 0
}
y--
line = buf.Line(y)
x = len(line) - 1
if len(line) == 0 {
return 0, y // empty line acts as a word boundary
}
}
// Now x,y is at the start of the target word. Move forward to its end.
if x >= 0 {
if isWordChar(line[x]) {
for x+1 < len(line) && isWordChar(line[x+1]) {
x++
}
} else if isWordPunctuation(line[x]) {
for x+1 < len(line) && isWordPunctuation(line[x+1]) {
x++
}
}
}
return x, y
}
// prevWORDEnd: Finds the end of the previous WORD from position (x,y),
// treating all non-whitespace as a single class.
func prevWORDEnd(buf *core.Buffer, x, y int) (int, int) {
line := buf.Line(y)
origY := y
// Back one to avoid being stuck on the current end
x--
if x < 0 {
if y == 0 {
return 0, 0 // beginning of file, stay put
}
y--
line = buf.Line(y)
x = len(line) - 1
// Don't return early for empty line - we'll handle it in whitespace skip
}
// Skip backward through current WORD if we're on one
// BUT: if we crossed lines in the "back one" step, we're already at the end of a WORD
if y == origY && x >= 0 && line[x] != ' ' && line[x] != '\t' {
for x >= 0 && line[x] != ' ' && line[x] != '\t' {
x--
if x < 0 {
if y == 0 {
return 0, 0
}
y--
line = buf.Line(y)
x = len(line) - 1
if x < 0 {
return 0, y
}
}
}
}
// 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, this is our WORD end!
}
if y == 0 {
return 0, 0
}
y--
line = buf.Line(y)
x = len(line) - 1
if len(line) == 0 {
return 0, y // empty line acts as a word boundary
}
}
// Now x,y is at the start of the target WORD. Move forward to its end.
if x >= 0 {
for x+1 < len(line) && line[x+1] != ' ' && line[x+1] != '\t' {
x++
}
}
return x, y
}
// MoveBackwardWORD implements Motion (B) - charwise
type MoveBackwardWORD struct {
Count int
}
// MoveBackwardWORD.Execute: Moves the cursor backward by Count WORDs (B motion).
func (a MoveBackwardWORD) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
x := win.Cursor.Col
y := win.Cursor.Line
for i := 0; i < a.Count; i++ {
x, y = prevWORDStart(buf, x, y)
}
win.SetCursorCol(x)
win.SetCursorLine(y)
return nil
}
// MoveBackwardWORD.Type: Returns CharwiseExclusive for backward WORD motion.
func (a MoveBackwardWORD) Type() core.MotionType { return core.CharwiseExclusive }
// MoveBackwardWORD.WithCount: Returns a new MoveBackwardWORD with the given count.
func (a MoveBackwardWORD) WithCount(n int) action.Action {
return MoveBackwardWORD{Count: n}
}
// MoveBackwardWordEnd implements Motion (ge) - charwise
type MoveBackwardWordEnd struct {
Count int
}
// MoveBackwardWordEnd.Execute: Moves the cursor to the end of the previous word (ge motion).
func (a MoveBackwardWordEnd) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
x := win.Cursor.Col
y := win.Cursor.Line
for i := 0; i < a.Count; i++ {
x, y = prevWordEnd(buf, x, y)
}
win.SetCursorCol(x)
win.SetCursorLine(y)
return nil
}
// MoveBackwardWordEnd.Type: Returns CharwiseInclusive for backward word-end motion.
func (a MoveBackwardWordEnd) Type() core.MotionType { return core.CharwiseInclusive }
// MoveBackwardWordEnd.WithCount: Returns a new MoveBackwardWordEnd with the given count.
func (a MoveBackwardWordEnd) WithCount(n int) action.Action {
return MoveBackwardWordEnd{Count: n}
}
// MoveBackwardWORDEnd implements Motion (gE) - charwise
type MoveBackwardWORDEnd struct {
Count int
}
// MoveBackwardWORDEnd.Execute: Moves the cursor to the end of the previous WORD (gE motion).
func (a MoveBackwardWORDEnd) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
x := win.Cursor.Col
y := win.Cursor.Line
for i := 0; i < a.Count; i++ {
x, y = prevWORDEnd(buf, x, y)
}
win.SetCursorCol(x)
win.SetCursorLine(y)
return nil
}
// MoveBackwardWORDEnd.Type: Returns CharwiseInclusive for backward WORD-end motion.
func (a MoveBackwardWORDEnd) Type() core.MotionType { return core.CharwiseInclusive }
// MoveBackwardWORDEnd.WithCount: Returns a new MoveBackwardWORDEnd with the given count.
func (a MoveBackwardWORDEnd) WithCount(n int) action.Action {
return MoveBackwardWORDEnd{Count: n}
}

View File

@ -68,18 +68,18 @@ func changeCharSelection(m action.Model, start, end core.Position) {
var deletedText string
if start.Line == end.Line {
line := buf.Line(start.Line)
line := buf.Lines[start.Line]
endCol := min(end.Col+1, len(line))
deletedText = line[start.Col:endCol]
buf.SetLine(start.Line, line[:start.Col]+line[endCol:])
} else {
startLine := buf.Line(start.Line)
endLine := buf.Line(end.Line)
startLine := buf.Lines[start.Line]
endLine := buf.Lines[end.Line]
// Extract deleted text
deletedText = startLine[start.Col:] + "\n"
for y := start.Line + 1; y < end.Line; y++ {
deletedText += buf.Line(y) + "\n"
deletedText += buf.Lines[y] + "\n"
}
endCol := min(end.Col+1, len(endLine))
deletedText += endLine[:endCol]
@ -113,7 +113,7 @@ func changeLineSelection(m action.Model, start, end core.Position) {
var lines []string
for i := end.Line; i >= start.Line; i-- {
lines = append([]string{buf.Line(i)}, lines...)
lines = append([]string{buf.Lines[i]}, lines...)
buf.DeleteLine(i)
}
@ -138,7 +138,7 @@ func changeBlockSelection(m action.Model, start, end core.Position) {
endCol := max(start.Col, end.Col)
for y := start.Line; y <= end.Line; y++ {
line := buf.Line(y)
line := buf.Lines[y]
if startCol >= len(line) {
continue
}
@ -168,7 +168,7 @@ func (o ChangeOperator) DoublePress(m action.Model, count int) tea.Cmd {
// Collect lines to delete (always delete at startY since lines shift up)
for range opCount {
lines = append(lines, buf.Line(startY))
lines = append(lines, buf.Lines[startY])
buf.DeleteLine(startY)
}

View File

@ -39,7 +39,7 @@ func (o DeleteOperator) DoublePress(m action.Model, count int) tea.Cmd {
for range opCount {
y := win.Cursor.Line
lines = append(lines, buf.Line(y))
lines = append(lines, buf.Lines[y])
buf.DeleteLine(y)
@ -99,12 +99,12 @@ func deleteCharSelection(m action.Model, start, end core.Position) {
buf := m.ActiveBuffer()
if start.Line == end.Line {
line := buf.Line(start.Line)
line := buf.Lines[start.Line]
endCol := min(end.Col+1, len(line))
buf.SetLine(start.Line, line[:start.Col]+line[endCol:])
} else {
startLine := buf.Line(start.Line)
endLine := buf.Line(end.Line)
startLine := buf.Lines[start.Line]
endLine := buf.Lines[end.Line]
prefix := startLine[:start.Col]
suffix := ""
@ -131,7 +131,7 @@ func deleteLineSelection(m action.Model, start, end core.Position) {
var lines []string
for i := end.Line; i >= start.Line; i-- {
lines = append(lines, buf.Line(i))
lines = append(lines, buf.Lines[i])
buf.DeleteLine(i)
}
@ -159,7 +159,7 @@ func deleteBlockSelection(m action.Model, start, end core.Position) {
endCol := max(start.Col, end.Col)
for y := start.Line; y <= end.Line; y++ {
line := buf.Line(y)
line := buf.Lines[y]
if startCol >= len(line) {
continue
}

View File

@ -30,14 +30,8 @@ func (o YankOperator) Operate(m action.Model, start, end core.Position, mtype co
})
}
// Normalize so cursor is set to the earlier position (important for backward motions)
cursorPos := start
if end.Line < start.Line || (end.Line == start.Line && end.Col < start.Col) {
cursorPos = end
}
win.SetCursorCol(cursorPos.Col)
win.SetCursorLine(cursorPos.Line)
win.SetCursorCol(start.Col)
win.SetCursorLine(start.Line)
return nil
}
@ -57,7 +51,7 @@ func (o YankOperator) DoublePress(m action.Model, count int) tea.Cmd {
var lines []string
for i := range opCount {
lines = append(lines, buf.Line(y+i))
lines = append(lines, buf.Lines[y+i])
}
// Put her in the register!
@ -72,7 +66,17 @@ func yankNormalMode(m action.Model, start, end core.Position, mtype core.MotionT
switch {
case mtype.IsCharwise():
line := buf.Line(start.Line)
// This shouldn't happen
// if start.Line != end.Line {
// m.SetCommandOutput(&core.CommandOutput{
// Lines: []string{"Start line and end line must match for charwise yank operations."},
// Inline: true,
// IsError: true,
// })
// return
// }
line := buf.Lines[start.Line]
startX := min(start.Col, end.Col)
endX := max(start.Col, end.Col)
@ -87,19 +91,22 @@ func yankNormalMode(m action.Model, start, end core.Position, mtype core.MotionT
cnt := line[startX:endX]
m.UpdateDefaultRegister(core.CharwiseRegister, []string{cnt})
win := m.ActiveWindow()
win.SetCursorCol(startX)
win.SetCursorLine(start.Line)
case mtype == core.Linewise:
// This shouldn't happen
// if start.Col != end.Col {
// m.SetCommandOutput(&core.CommandOutput{
// Lines: []string{"Start column and end column must match for linewise yank operations."},
// Inline: true,
// IsError: true,
// })
// return
// }
// These don't need to be validated, they are validated before being passed into the function
startY := min(start.Line, end.Line)
endY := max(start.Line, end.Line)
var cnt []string
for i := startY; i <= endY; i++ {
cnt = append(cnt, buf.Line(i))
}
cnt := buf.Lines[startY : endY+1]
m.UpdateDefaultRegister(core.LinewiseRegister, cnt)
}
}
@ -115,7 +122,7 @@ func yankVisualMode(m action.Model, start, end core.Position) {
// Single line selection
if start.Line == end.Line {
line := buf.Line(start.Line)
line := buf.Lines[start.Line]
endCol := min(end.Col+1, len(line)) // +1 because visual selection is inclusive
startCol := min(start.Col, len(line))
cnt := line[startCol:endCol]
@ -127,17 +134,17 @@ func yankVisualMode(m action.Model, start, end core.Position) {
var content []string
// First line: from start.Col to end of line
firstLine := buf.Line(start.Line)
firstLine := buf.Lines[start.Line]
startCol := min(start.Col, len(firstLine))
content = append(content, firstLine[startCol:])
// Middle lines: entire lines
for y := start.Line + 1; y < end.Line; y++ {
content = append(content, buf.Line(y))
content = append(content, buf.Lines[y])
}
// Last line: from beginning to end.Col (inclusive)
lastLine := buf.Line(end.Line)
lastLine := buf.Lines[end.Line]
endCol := min(end.Col+1, len(lastLine))
content = append(content, lastLine[:endCol])
@ -162,10 +169,7 @@ func yankVisualLineMode(m action.Model, start, end core.Position) {
startY := min(start.Line, end.Line)
endY := max(start.Line, end.Line)
var cnt []string
for i := startY; i <= endY; i++ {
cnt = append(cnt, buf.Line(i))
}
cnt := buf.Lines[startY : endY+1]
m.UpdateDefaultRegister(core.LinewiseRegister, cnt)
}
@ -183,7 +187,7 @@ func yankVisualBlockMode(m action.Model, start, end core.Position) {
var content []string
for y := startY; y <= endY; y++ {
line := buf.Line(y)
line := buf.Lines[y]
// Handle lines shorter than the block selection
if startX >= len(line) {

View File

@ -1,11 +1,8 @@
package style
import (
"strings"
"git.gophernest.net/azpect/TextEditor/internal/core"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/charmbracelet/lipgloss"
)
@ -15,7 +12,6 @@ type Styles struct {
CursorNormal lipgloss.Style
CursorInsert lipgloss.Style
CursorCommand lipgloss.Style
CursorReplace lipgloss.Style
// Gutter (line numbers)
Gutter lipgloss.Style
@ -48,7 +44,6 @@ func DefaultStyles() Styles {
CursorNormal: lipgloss.NewStyle().Reverse(true),
CursorInsert: lipgloss.NewStyle().Underline(true),
CursorCommand: lipgloss.NewStyle().Reverse(true),
CursorReplace: lipgloss.NewStyle().Underline(true),
Gutter: lipgloss.NewStyle().
Background(lipgloss.Color("236")).
@ -97,17 +92,12 @@ func ChromaStyles(chromaStyle *chroma.Style) Styles {
CursorInsert: lipgloss.NewStyle().
Background(lipgloss.Color(bgString)).
Bold(true).
Underline(true),
CursorCommand: lipgloss.NewStyle().
Background(lipgloss.Color(bgString)).
Reverse(true),
CursorReplace: lipgloss.NewStyle().
Background(lipgloss.Color(bgString)).
Underline(true),
Gutter: lipgloss.NewStyle().
Background(lipgloss.Color(
darkenColor(lineNumbers.Background, 0.9).String()),
@ -170,8 +160,6 @@ func (s Styles) DefaultCursorStyle(mode core.Mode) lipgloss.Style {
return s.CursorInsert
case core.CommandMode:
return s.CursorCommand
case core.ReplaceMode:
return s.CursorReplace
default:
return s.CursorNormal
}
@ -186,11 +174,6 @@ func (s Styles) CursorStyle(mode core.Mode, style lipgloss.Style) lipgloss.Style
return lipgloss.NewStyle().
Background(style.GetForeground()).
Foreground(style.GetBackground())
case core.ReplaceMode, core.WaitingMode:
return lipgloss.NewStyle().
Background(style.GetBackground()).
Foreground(style.GetForeground()).
Underline(true)
default:
return lipgloss.NewStyle().
Background(s.BackgroundStyle.GetBackground()).
@ -250,33 +233,3 @@ func darkenColor(c chroma.Colour, factor float64) chroma.Colour {
b := uint8(float64(c.Blue()) * factor)
return chroma.NewColour(r, g, b)
}
// GetLexer: Uses buffer meta data or content to pick a lexer for use in applying
// highlights.
func GetLexer(buf *core.Buffer) chroma.Lexer {
var lexer chroma.Lexer
if buf.Filetype != "" {
lexer = lexers.Get(strings.TrimPrefix(buf.Filetype, "."))
}
if lexer == nil && buf.Filename != "" {
lexer = lexers.Match(buf.Filename)
}
if lexer == nil && len(buf.Lines) > 0 {
// Get first few lines for content analysis
var content strings.Builder
for i := 0; i < min(len(buf.Lines), 10); i++ {
content.WriteString(buf.Lines[i].String() + "\n")
}
lexer = lexers.Analyse(content.String())
}
if lexer == nil {
lexer = lexers.Fallback
}
lexer = chroma.Coalesce(lexer) // Merge tokens together
return lexer
}

View File

@ -75,14 +75,8 @@ func (to Delimiter) GetRange(m action.Model, cursor core.Position, modifier stri
return cursor, cursor, core.CharwiseExclusive
}
// Convert buffer lines to strings for delimiter finding
var lines []string
for i := 0; i < buf.LineCount(); i++ {
lines = append(lines, buf.Line(i))
}
// Use multi-line delimiter pair finding
start, end, found := findMultiLineDelimiterPair(lines, startDelim, endDelim, cursor, modifier == "a")
start, end, found := findMultiLineDelimiterPair(buf.Lines, startDelim, endDelim, cursor, modifier == "a")
if !found {
return cursor, cursor, core.CharwiseExclusive

View File

@ -10,7 +10,7 @@ type Word struct{}
func (to Word) GetRange(m action.Model, cursor core.Position, modifier string) (core.Position, core.Position, core.MotionType) {
buf := m.ActiveBuffer()
line := buf.Line(cursor.Line)
line := buf.Lines[cursor.Line]
// Find word boundaries
start := findWordStart(line, cursor.Col)
@ -28,7 +28,7 @@ type WORD struct{}
func (to WORD) GetRange(m action.Model, cursor core.Position, modifier string) (core.Position, core.Position, core.MotionType) {
buf := m.ActiveBuffer()
line := buf.Line(cursor.Line)
line := buf.Lines[cursor.Line]
// Find word boundaries
start := findWORDStart(line, cursor.Col)

25
qodo.md
View File

@ -1,25 +0,0 @@
### The Core Commands
* `/review`
Asks Gemini to read the entire PR diff and provide a structured summary, score the PR, identify potential bugs, and suggest high-level fixes.
* `/describe`
Automatically rewrites the PR's title and description based on the actual code changes. (Great for when you just want to push code and not write documentation).
* `/improve`
Scans the code and provides actionable, copy-pasteable snippets to improve the code, focusing on performance, security, and best practices.
* `/ask "<your question>"`
Turns the PR comment section into a chat window. It uses the PR diff as context. Example: `/ask "Did I properly handle the null pointer edge cases in the new database function?"`
### Specialized Tools
* `/test`
Asks the AI to generate unit tests specifically tailored for the new or modified code in the PR.
* `/update_changelog`
Automatically drafts an update for your `CHANGELOG.md` file based on the PR's contents.
* `/generate_labels`
Analyzes the code changes and recommends appropriate labels for the PR (e.g., `bug`, `enhancement`, `refactor`).
* `/help`
Forces the bot to reply with a quick cheat sheet of all available commands and usage instructions in case you forget them.
### Pro-Tip: Steering the AI
You can actually pass arguments directly to the commands to give Gemini specific instructions for that specific run.
For example, if you want a review but want it to be hyper-paranoid about security, you can type:
`/review --pr_reviewer.extra_instructions="Focus heavily on potential security vulnerabilities and SQL injection risks."`