Compare commits
4 Commits
1aed168369
...
b7234b4639
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7234b4639 | ||
|
|
3397f07ab7 | ||
|
|
52929322d7 | ||
|
|
62b517ec6e |
424
FEATURES.md
Normal file
424
FEATURES.md
Normal file
@ -0,0 +1,424 @@
|
||||
# TextEditor Feature Checklist
|
||||
|
||||
## Normal Mode Motions
|
||||
|
||||
### Basic Movement
|
||||
- [x] `h` - Move left
|
||||
- [x] `j` - Move down
|
||||
- [x] `k` - Move up
|
||||
- [x] `l` - Move right
|
||||
|
||||
### Word Movement
|
||||
- [x] `w` - Forward to start of word
|
||||
- [x] `e` - Forward to end of word
|
||||
- [x] `b` - Backward to start of word
|
||||
- [ ] `W` - Forward to start of WORD (whitespace-delimited)
|
||||
- [ ] `E` - Forward 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
|
||||
- [x] `$` - Move to end of line
|
||||
- [x] `_` - Move to first non-whitespace
|
||||
- [x] `^` - Move to first non-whitespace (alias for `_`)
|
||||
- [ ] `|` - Move to column N
|
||||
|
||||
### File Movement
|
||||
- [x] `G` - Move to bottom of file (or line N with count)
|
||||
- [x] `gg` - Move to top of file (or line N with count)
|
||||
- [ ] `H` - Move to top of screen
|
||||
- [ ] `M` - Move to middle of screen
|
||||
- [ ] `L` - Move to bottom of screen
|
||||
|
||||
### Scroll
|
||||
- [x] `ctrl+u` - Scroll up half page
|
||||
- [x] `ctrl+d` - Scroll down half 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
|
||||
- [ ] `zt` - Scroll cursor to top
|
||||
- [ ] `zb` - Scroll cursor to bottom
|
||||
|
||||
### Search Movement
|
||||
- [ ] `f{char}` - Find char forward on line
|
||||
- [ ] `F{char}` - Find char backward on line
|
||||
- [ ] `t{char}` - Till char forward on line
|
||||
- [ ] `T{char}` - Till char backward on line
|
||||
- [ ] `;` - Repeat last f/F/t/T
|
||||
- [ ] `,` - Repeat last f/F/t/T reversed
|
||||
- [ ] `/` - Search forward
|
||||
- [ ] `?` - Search backward
|
||||
- [ ] `n` - Next search result
|
||||
- [ ] `N` - Previous search result
|
||||
- [ ] `*` - Search word under cursor forward
|
||||
- [ ] `#` - Search word under cursor backward
|
||||
|
||||
### Other Movement
|
||||
- [ ] `%` - Jump to matching bracket
|
||||
- [ ] `{` - Jump to previous paragraph
|
||||
- [ ] `}` - Jump to next paragraph
|
||||
- [ ] `(` - Jump to previous sentence
|
||||
- [ ] `)` - Jump to next sentence
|
||||
|
||||
---
|
||||
|
||||
## Operators
|
||||
|
||||
### Implemented
|
||||
- [x] `d` - Delete operator
|
||||
- [x] `y` - Yank operator
|
||||
- [x] `dd` - Delete line (double press)
|
||||
- [x] `yy` - Yank line (double press)
|
||||
|
||||
### Not Implemented
|
||||
- [ ] `c` - Change operator
|
||||
- [ ] `cc` - Change line
|
||||
- [ ] `>` - Indent right
|
||||
- [ ] `<` - Indent left
|
||||
- [ ] `=` - Auto-indent
|
||||
- [ ] `gq` - Format text
|
||||
- [ ] `gu` - Lowercase
|
||||
- [ ] `gU` - Uppercase
|
||||
- [ ] `g~` - Swap case
|
||||
- [ ] `!` - Filter through external command
|
||||
|
||||
---
|
||||
|
||||
## Actions
|
||||
|
||||
### Insert Mode Entry
|
||||
- [x] `i` - Insert before cursor
|
||||
- [x] `a` - Insert after cursor
|
||||
- [x] `I` - Insert at start of line
|
||||
- [x] `A` - Insert at end of line
|
||||
- [x] `o` - Open line below
|
||||
- [x] `O` - Open line above
|
||||
- [ ] `s` - Substitute character (delete + insert)
|
||||
- [ ] `S` - Substitute line (delete line + insert)
|
||||
- [ ] `C` - Change to end of line
|
||||
- [ ] `gi` - Insert at last insert position
|
||||
|
||||
### Delete Actions
|
||||
- [x] `x` - Delete character under cursor
|
||||
- [x] `D` - Delete to end of line
|
||||
- [ ] `X` - Delete character before cursor
|
||||
- [ ] `J` - Join lines
|
||||
- [ ] `gJ` - Join lines without space
|
||||
|
||||
### Yank/Paste
|
||||
- [x] `p` - Paste after cursor
|
||||
- [x] `P` - Paste before cursor
|
||||
- [ ] `gp` - Paste after and move cursor to end
|
||||
- [ ] `gP` - Paste before and move cursor to end
|
||||
|
||||
### Registers
|
||||
- [x] Unnamed register (`"`)
|
||||
- [x] Numbered registers (`0-9`)
|
||||
- [x] Register types (charwise, linewise, blockwise)
|
||||
- [ ] `"` - Register prefix (select register for next operation)
|
||||
- [ ] Named registers (`a-z`)
|
||||
- [ ] Append to named registers (`A-Z`)
|
||||
- [ ] Black hole register (`_`)
|
||||
- [ ] System clipboard (`+`, `*`)
|
||||
- [ ] Expression register (`=`)
|
||||
- [ ] Last search register (`/`)
|
||||
|
||||
### Undo/Redo
|
||||
- [ ] `u` - Undo
|
||||
- [ ] `ctrl+r` - Redo
|
||||
- [ ] `.` - Repeat last change
|
||||
- [ ] `U` - Undo all changes on line
|
||||
|
||||
### Other Normal Mode
|
||||
- [ ] `r{char}` - Replace character
|
||||
- [ ] `R` - Replace mode
|
||||
- [ ] `~` - Swap case of character
|
||||
- [ ] `ctrl+a` - Increment number
|
||||
- [ ] `ctrl+x` - Decrement number
|
||||
- [ ] `q{reg}` - Record macro
|
||||
- [ ] `@{reg}` - Play macro
|
||||
- [ ] `@@` - Repeat last macro
|
||||
- [x] `ctrl+c` - Quit (custom)
|
||||
|
||||
---
|
||||
|
||||
## Visual Mode
|
||||
|
||||
### Mode Entry
|
||||
- [x] `v` - Character-wise visual mode
|
||||
- [x] `V` - Line-wise visual mode
|
||||
- [x] `ctrl+v` - Block-wise visual mode
|
||||
- [ ] `gv` - Reselect last visual selection
|
||||
|
||||
### Visual Mode Operations
|
||||
- [x] Motions work in visual mode
|
||||
- [x] `d` / `x` - Delete selection
|
||||
- [x] `y` - Yank selection
|
||||
- [ ] `c` - Change selection
|
||||
- [ ] `>` - Indent selection
|
||||
- [ ] `<` - Unindent selection
|
||||
- [ ] `=` - Auto-indent selection
|
||||
- [ ] `~` - Swap case of selection
|
||||
- [ ] `u` - Lowercase selection
|
||||
- [ ] `U` - Uppercase selection
|
||||
- [ ] `J` - Join selected lines
|
||||
- [ ] `o` - Go to other end of selection
|
||||
- [ ] `O` - Go to other corner (block mode)
|
||||
|
||||
---
|
||||
|
||||
## Insert Mode
|
||||
|
||||
### Text Input
|
||||
- [x] Character insertion
|
||||
- [x] `Enter` - Insert newline
|
||||
- [x] `Tab` - Insert tab/spaces
|
||||
- [x] `Backspace` - Delete character before cursor
|
||||
- [x] `Delete` - Delete character under cursor
|
||||
- [x] `ctrl+w` - Delete word before cursor
|
||||
|
||||
### Movement in Insert Mode
|
||||
- [x] Arrow keys (up/down/left/right)
|
||||
- [ ] `ctrl+h` - Backspace (alias)
|
||||
- [ ] `ctrl+j` - Insert newline (alias)
|
||||
- [ ] `ctrl+t` - Indent line
|
||||
- [ ] `ctrl+d` - Unindent line
|
||||
|
||||
### Exit Insert Mode
|
||||
- [x] `Esc` - Exit to normal mode
|
||||
- [ ] `ctrl+c` - Exit to normal mode
|
||||
- [ ] `ctrl+[` - Exit to normal mode
|
||||
|
||||
---
|
||||
|
||||
## Command Mode
|
||||
|
||||
### Entry/Exit
|
||||
- [x] `:` - Enter command mode
|
||||
- [x] `Esc` - Exit command mode
|
||||
- [x] `Enter` - Execute command
|
||||
|
||||
### Editing
|
||||
- [x] Character input
|
||||
- [x] `Backspace` - Delete character
|
||||
- [x] `Delete` - Delete character
|
||||
- [x] `ctrl+w` - Delete word
|
||||
- [x] `Left` / `Right` - Move cursor
|
||||
|
||||
### Commands Implemented
|
||||
- [x] `:set number` / `:set nonumber` - Toggle line numbers
|
||||
- [x] `:set number!` - Toggle line numbers
|
||||
- [x] `:set tabstop=N` - Set tab width
|
||||
- [x] `:register {name}` - Show register contents
|
||||
- [ ] `:w` - Write file
|
||||
- [ ] `:q` - Quit
|
||||
- [ ] `:wq` - Write and quit
|
||||
- [ ] `:q!` - Force quit
|
||||
- [ ] `:e {file}` - Edit file
|
||||
- [ ] `:bn` / `:bp` - Next/previous buffer
|
||||
- [ ] `:{range}` - Go to line
|
||||
- [ ] `:%s/old/new/g` - Search and replace
|
||||
- [ ] `:!{cmd}` - Run shell command
|
||||
- [ ] `:help` - Show help
|
||||
|
||||
---
|
||||
|
||||
## Text Objects
|
||||
|
||||
### 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
|
||||
|
||||
---
|
||||
|
||||
## Marks & Jumps
|
||||
|
||||
### Not Implemented
|
||||
- [ ] `m{a-z}` - Set local mark
|
||||
- [ ] `m{A-Z}` - Set global mark
|
||||
- [ ] `` `{mark} `` - Jump to mark (exact position)
|
||||
- [ ] `'{mark}` - Jump to mark (line start)
|
||||
- [ ] ``` `` ``` - Jump to previous position
|
||||
- [ ] `''` - Jump to previous line
|
||||
- [ ] `` `. `` - Jump to last change
|
||||
- [ ] `ctrl+o` - Jump back in jump list
|
||||
- [ ] `ctrl+i` - Jump forward in jump list
|
||||
|
||||
---
|
||||
|
||||
## Buffers
|
||||
|
||||
Buffers are in-memory representations of files. A buffer exists for each open file.
|
||||
|
||||
### Buffer Model
|
||||
- [ ] Buffer struct (id, filename, lines, modified flag, cursor position)
|
||||
- [ ] Buffer list/manager
|
||||
- [ ] Current buffer tracking
|
||||
- [ ] Buffer-local settings (tabstop, filetype, etc.)
|
||||
- [ ] Modified/dirty state tracking
|
||||
- [ ] Read-only buffer support
|
||||
|
||||
### Buffer Navigation
|
||||
- [ ] `:e {file}` - Edit file (open in new buffer or switch to existing)
|
||||
- [ ] `:bn` / `:bnext` - Next buffer
|
||||
- [ ] `:bp` / `:bprev` - Previous buffer
|
||||
- [ ] `:b {name}` - Switch to buffer by name (partial match)
|
||||
- [ ] `:b {number}` - Switch to buffer by number
|
||||
- [ ] `:bf` / `:bfirst` - First buffer
|
||||
- [ ] `:bl` / `:blast` - Last buffer
|
||||
- [ ] `:buffers` / `:ls` - List all buffers
|
||||
- [ ] `ctrl+^` / `ctrl+6` - Switch to alternate (previous) buffer
|
||||
|
||||
### Buffer Operations
|
||||
- [ ] `:bd` / `:bdelete` - Delete buffer (close file)
|
||||
- [ ] `:bd!` - Force delete buffer (discard changes)
|
||||
- [ ] `:bw` / `:bwipeout` - Wipe buffer (remove completely)
|
||||
- [ ] `:w` - Write current buffer to file
|
||||
- [ ] `:w {file}` - Write buffer to specific file
|
||||
- [ ] `:wa` - Write all modified buffers
|
||||
- [ ] `:sav {file}` - Save as (write to new file, switch to it)
|
||||
|
||||
### Buffer State
|
||||
- [ ] Track cursor position per buffer
|
||||
- [ ] Track undo history per buffer
|
||||
- [ ] Track marks per buffer
|
||||
- [ ] Remember scroll position when switching
|
||||
- [ ] Alternate buffer (`#`) tracking
|
||||
|
||||
### Buffer Indicators
|
||||
- [ ] `%` - Current buffer (in buffer list)
|
||||
- [ ] `#` - Alternate buffer
|
||||
- [ ] `a` - Active (loaded and visible)
|
||||
- [ ] `h` - Hidden (loaded but not visible)
|
||||
- [ ] `+` - Modified
|
||||
- [ ] `-` - Read-only
|
||||
- [ ] `=` - Readonly (cannot be modified)
|
||||
|
||||
### Hidden Buffers
|
||||
- [ ] `:set hidden` - Allow switching with unsaved changes
|
||||
- [ ] Prompt to save when closing modified buffer
|
||||
- [ ] `:q` behavior with modified buffers
|
||||
|
||||
### Argument List (Advanced)
|
||||
- [ ] `:args` - Show argument list
|
||||
- [ ] `:next` / `:prev` - Navigate argument list
|
||||
- [ ] `:argadd` / `:argdelete` - Modify argument list
|
||||
|
||||
---
|
||||
|
||||
## Plugins / Extensions
|
||||
|
||||
### Harpoon (File Navigation)
|
||||
- [ ] Quick file marks (1-4 files)
|
||||
- [ ] Add file to list
|
||||
- [ ] Navigate to marked file
|
||||
- [ ] Show file list
|
||||
|
||||
### Telescope / Fuzzy Finder
|
||||
- [ ] File picker
|
||||
- [ ] Buffer picker
|
||||
- [ ] Grep/search
|
||||
- [ ] Command palette
|
||||
|
||||
### File Explorer
|
||||
- [ ] Tree view
|
||||
- [ ] File operations (create, delete, rename)
|
||||
- [ ] Navigation
|
||||
|
||||
### LSP Support
|
||||
- [ ] Go to definition
|
||||
- [ ] Find references
|
||||
- [ ] Hover information
|
||||
- [ ] Diagnostics
|
||||
- [ ] Code actions
|
||||
- [ ] Completion
|
||||
|
||||
### Git Integration
|
||||
- [ ] Git status
|
||||
- [ ] Git diff
|
||||
- [ ] Git blame
|
||||
- [ ] Hunk navigation
|
||||
|
||||
### Other
|
||||
- [ ] Multiple cursors
|
||||
- [ ] Snippets
|
||||
- [ ] Auto-pairs (brackets, quotes)
|
||||
- [ ] Surround (change surrounding chars)
|
||||
- [ ] Comment toggle
|
||||
- [ ] Indentation guides
|
||||
- [ ] Syntax highlighting
|
||||
- [ ] Statusline customization
|
||||
- [ ] Themes
|
||||
|
||||
---
|
||||
|
||||
## Editor Features
|
||||
|
||||
### Display
|
||||
- [x] Line numbers
|
||||
- [x] Cursor position tracking
|
||||
- [x] Viewport/scrolling
|
||||
- [x] ScrollOff setting
|
||||
- [x] Relative line numbers
|
||||
- [ ] Cursor line highlight
|
||||
- [ ] Column highlight
|
||||
- [ ] Wrap/nowrap
|
||||
- [ ] Word wrap at window edge
|
||||
- [ ] Show whitespace characters
|
||||
- [ ] Color column (ruler)
|
||||
|
||||
### Files
|
||||
- [ ] File reading
|
||||
- [ ] File writing
|
||||
- [ ] Auto-save
|
||||
- [ ] Backup files
|
||||
- [ ] Swap files
|
||||
- [ ] File encoding
|
||||
- [ ] Line endings (LF/CRLF)
|
||||
|
||||
### Search & Replace
|
||||
- [ ] Incremental search
|
||||
- [ ] Search highlighting
|
||||
- [ ] Case sensitivity options
|
||||
- [ ] Regex support
|
||||
- [ ] Search and replace
|
||||
|
||||
### Misc
|
||||
- [ ] Split windows
|
||||
- [ ] Tabs
|
||||
- [ ] Sessions
|
||||
- [ ] Persistent undo
|
||||
- [ ] Spell check
|
||||
|
||||
---
|
||||
|
||||
## Testing Coverage
|
||||
|
||||
### Well 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] 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] Command mode basics
|
||||
- [x] Register behavior
|
||||
@ -306,3 +306,422 @@ func TestMoveToLineContentStart(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoveToLineContentStartAlias(t *testing.T) {
|
||||
t.Run("test '^' from middle of line with no leading whitespace", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
|
||||
sendKeys(tm, "^")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '^' from middle of line with leading whitespace", func(t *testing.T) {
|
||||
lines := []string{" hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
||||
sendKeys(tm, "^")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '^' from start of line with leading whitespace", func(t *testing.T) {
|
||||
lines := []string{" hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "^")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '^' from start of line with no leading whitespace", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "^")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '^' from middle of line with only whitespace", func(t *testing.T) {
|
||||
lines := []string{" "}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "^")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '^' from end of line with only whitespace", func(t *testing.T) {
|
||||
lines := []string{" "}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
||||
sendKeys(tm, "^")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '^' on empty line", func(t *testing.T) {
|
||||
lines := []string{""}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "^")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- | (Pipe / Move to Column) Tests ---
|
||||
// In Vim, | moves to column N (1-indexed). So:
|
||||
// - | = 1| = column 1 = index 0
|
||||
// - 5| = column 5 = index 4
|
||||
// - 10| = column 10 = index 9
|
||||
|
||||
func TestMoveToColumn(t *testing.T) {
|
||||
t.Run("test '|' alone goes to column 1 (index 0)", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// | with no count = 1| = column 1 = index 0
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '1|' goes to column 1 (index 0)", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "1", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '5|' goes to column 5 (index 4)", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "5", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Column 5 = index 4 (the 'o' in hello)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '10|' goes to column 10 (index 9)", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "1", "0", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Column 10 = index 9 (the 'l' in world)
|
||||
if m.CursorX() != 9 {
|
||||
t.Errorf("CursorX() = %d, want 9", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '|' already at column 1", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '5|' already at column 5", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
||||
sendKeys(tm, "5", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoveToColumnClamp(t *testing.T) {
|
||||
t.Run("test '20|' clamps to end of short line", func(t *testing.T) {
|
||||
lines := []string{"hello"} // 5 chars, max index 4
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "2", "0", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Column 20 exceeds line length, should clamp to last char (index 4)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '100|' clamps to end of line", func(t *testing.T) {
|
||||
lines := []string{"hello world"} // 11 chars, max index 10
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "1", "0", "0", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Should clamp to last char (index 10)
|
||||
if m.CursorX() != 10 {
|
||||
t.Errorf("CursorX() = %d, want 10", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '6|' clamps on 5-char line", func(t *testing.T) {
|
||||
lines := []string{"hello"} // 5 chars
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "6", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Column 6 = index 5, but line only has 5 chars (max index 4)
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '|' on empty line stays at 0", func(t *testing.T) {
|
||||
lines := []string{""}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '5|' on empty line stays at 0", func(t *testing.T) {
|
||||
lines := []string{""}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "5", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '3|' on 2-char line clamps", func(t *testing.T) {
|
||||
lines := []string{"ab"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "3", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Column 3 = index 2, but line only has 2 chars (max index 1)
|
||||
if m.CursorX() != 1 {
|
||||
t.Errorf("CursorX() = %d, want 1", m.CursorX())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoveToColumnPreservesLine(t *testing.T) {
|
||||
t.Run("test '|' preserves Y position", func(t *testing.T) {
|
||||
lines := []string{"line one", "line two", "line three"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 1})
|
||||
sendKeys(tm, "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 1 {
|
||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '5|' preserves Y position", func(t *testing.T) {
|
||||
lines := []string{"line one", "line two", "line three"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
|
||||
sendKeys(tm, "5", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 2 {
|
||||
t.Errorf("CursorY() = %d, want 2", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '|' on different lines", func(t *testing.T) {
|
||||
lines := []string{"short", "longer line here"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1})
|
||||
sendKeys(tm, "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 1 {
|
||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoveToColumnWithWhitespace(t *testing.T) {
|
||||
t.Run("test '5|' with leading whitespace", func(t *testing.T) {
|
||||
lines := []string{" hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "5", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Column 5 = index 4 = 'h' in " hello"
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '3|' lands on whitespace", func(t *testing.T) {
|
||||
lines := []string{" hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "3", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Column 3 = index 2 = third space
|
||||
if m.CursorX() != 2 {
|
||||
t.Errorf("CursorX() = %d, want 2", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '|' with tabs", func(t *testing.T) {
|
||||
lines := []string{"\thello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
||||
sendKeys(tm, "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// | goes to column 1 = index 0 = the tab
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test '2|' with tabs", func(t *testing.T) {
|
||||
lines := []string{"\thello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "2", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Column 2 = index 1 = 'h' in "\thello"
|
||||
if m.CursorX() != 1 {
|
||||
t.Errorf("CursorX() = %d, want 1", m.CursorX())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoveToColumnWithOperator(t *testing.T) {
|
||||
t.Run("test 'd|' deletes to column 1", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "d", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// 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.Line(0) != " world" {
|
||||
t.Errorf("Line(0) = %q, want ' world'", m.Line(0))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'd5|' deletes to column 5", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "d", "5", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Deletes from cursor (0) to column 5 (index 4), so "hell" deleted
|
||||
// Result: "o world"
|
||||
if m.Line(0) != "o world" {
|
||||
t.Errorf("Line(0) = %q, want 'o world'", m.Line(0))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'y5|' yanks to column 5", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "y", "5", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
reg, ok := m.GetRegister('"')
|
||||
if !ok {
|
||||
t.Fatal("unnamed register not found")
|
||||
}
|
||||
// Should yank "hell" (indices 0-3, up to but not including col 5)
|
||||
if len(reg.Content) != 1 || reg.Content[0] != "hell" {
|
||||
t.Errorf("register content = %q, want 'hell'", reg.Content)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'y|' yanks to column 1 (nothing if at start)", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "y", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
reg, ok := m.GetRegister('"')
|
||||
if !ok {
|
||||
t.Fatal("unnamed register not found")
|
||||
}
|
||||
// At col 0, y| should yank nothing (empty string)
|
||||
if len(reg.Content) != 1 || reg.Content[0] != "" {
|
||||
t.Errorf("register content = %q, want ''", reg.Content)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoveToColumnInVisualMode(t *testing.T) {
|
||||
t.Run("test 'v5|' selects to column 5", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "v", "5", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.AnchorX() != 0 {
|
||||
t.Errorf("AnchorX() = %d, want 0", m.AnchorX())
|
||||
}
|
||||
if m.CursorX() != 4 {
|
||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'v|' selects backward to column 1", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "v", "|")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.AnchorX() != 5 {
|
||||
t.Errorf("AnchorX() = %d, want 5", m.AnchorX())
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'v5|d' deletes selection to column 5", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
||||
sendKeys(tm, "v", "5", "|", "d")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Visual selection from 0 to 4 inclusive, delete "hello"
|
||||
if m.Line(0) != " world" {
|
||||
t.Errorf("Line(0) = %q, want ' world'", m.Line(0))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -25,6 +25,8 @@ func NewNormalKeymap() *Keymap {
|
||||
"0": motion.MoveToLineStart{},
|
||||
"$": motion.MoveToLineEnd{},
|
||||
"_": motion.MoveToLineContentStart{},
|
||||
"^": motion.MoveToLineContentStart{},
|
||||
"|": motion.MoveToColumn{Count: 0},
|
||||
"w": motion.MoveForwardWord{Count: 1},
|
||||
"e": motion.MoveForwardWordEnd{Count: 1},
|
||||
"b": motion.MoveBackwardWord{Count: 1},
|
||||
@ -69,6 +71,8 @@ func NewVisualKeymap() *Keymap {
|
||||
"0": motion.MoveToLineStart{},
|
||||
"$": motion.MoveToLineEnd{},
|
||||
"_": motion.MoveToLineContentStart{},
|
||||
"^": motion.MoveToLineContentStart{},
|
||||
"|": motion.MoveToColumn{Count: 0},
|
||||
"w": motion.MoveForwardWord{Count: 1},
|
||||
"e": motion.MoveForwardWordEnd{Count: 1},
|
||||
"b": motion.MoveBackwardWord{Count: 1},
|
||||
|
||||
@ -74,6 +74,26 @@ func (a MoveToLineContentStart) Execute(m action.Model) tea.Cmd {
|
||||
|
||||
func (a MoveToLineContentStart) Type() action.MotionType { return action.CharwiseExclusive }
|
||||
|
||||
// MoveToColumn implements Motion (|) - charwise
|
||||
type MoveToColumn struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
func (a MoveToColumn) Execute(m action.Model) tea.Cmd {
|
||||
line := m.Line(m.CursorY())
|
||||
col := min(a.Count-1, len(line)-1)
|
||||
|
||||
m.SetCursorX(col)
|
||||
m.ClampCursorX()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveToColumn) Type() action.MotionType { return action.CharwiseExclusive }
|
||||
|
||||
func (a MoveToColumn) WithCount(n int) action.Action {
|
||||
return MoveToColumn{Count: n}
|
||||
}
|
||||
|
||||
// TODO: Count for these, maybe?
|
||||
|
||||
// ScrollDownHalfPage implements Motion (ctrl+d) - linewise
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user