Compare commits
24 Commits
db52b63db1
...
f12ce37beb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f12ce37beb | ||
|
|
d4980c5532 | ||
|
|
8364d8b880 | ||
|
|
c963d66e3b | ||
|
|
997c4143ca | ||
|
|
41fd9bd45a | ||
|
|
eb872fcfdf | ||
|
|
a103af0a83 | ||
|
|
354fbc4f9b | ||
|
|
15d847e3c8 | ||
|
|
c126242ee1 | ||
|
|
93968e7333 | ||
|
|
098641f5c0 | ||
|
|
dc9a814508 | ||
|
|
03c3a41162 | ||
|
|
ccb061989a | ||
|
|
154558b790 | ||
|
|
b1b3edf810 | ||
|
|
9b1bf35a8e | ||
|
|
770cbcceb7 | ||
|
|
ea4638d815 | ||
|
|
3339dd4409 | ||
|
|
65f96a5089 | ||
|
|
88fa53a4d7 |
30
.github/workflows/EditorTests.yml
vendored
Normal file
30
.github/workflows/EditorTests.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
name: Run Test Suite
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod # Use mod file directly
|
||||||
|
cache: false
|
||||||
|
# go-version: "1.25.5" # Pin version
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: go mod download
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: go test -v ./...
|
||||||
92
FEATURES.md
92
FEATURES.md
@ -74,8 +74,8 @@
|
|||||||
- [x] `yy` - Yank line (double press)
|
- [x] `yy` - Yank line (double press)
|
||||||
|
|
||||||
### Not Implemented
|
### Not Implemented
|
||||||
- [ ] `c` - Change operator
|
- [x] `c` - Change operator
|
||||||
- [ ] `cc` - Change line
|
- [x] `cc` - Change line
|
||||||
- [ ] `>` - Indent right
|
- [ ] `>` - Indent right
|
||||||
- [ ] `<` - Indent left
|
- [ ] `<` - Indent left
|
||||||
- [ ] `=` - Auto-indent
|
- [ ] `=` - Auto-indent
|
||||||
@ -96,9 +96,9 @@
|
|||||||
- [x] `A` - Insert at end of line
|
- [x] `A` - Insert at end of line
|
||||||
- [x] `o` - Open line below
|
- [x] `o` - Open line below
|
||||||
- [x] `O` - Open line above
|
- [x] `O` - Open line above
|
||||||
- [ ] `s` - Substitute character (delete + insert)
|
- [x] `s` - Substitute character (delete + insert)
|
||||||
- [ ] `S` - Substitute line (delete line + insert)
|
- [x] `S` - Substitute line (delete line + insert)
|
||||||
- [ ] `C` - Change to end of line
|
- [x] `C` - Change to end of line
|
||||||
- [ ] `gi` - Insert at last insert position
|
- [ ] `gi` - Insert at last insert position
|
||||||
|
|
||||||
### Delete Actions
|
### Delete Actions
|
||||||
@ -213,11 +213,11 @@
|
|||||||
- [x] `:set number!` - Toggle line numbers
|
- [x] `:set number!` - Toggle line numbers
|
||||||
- [x] `:set tabstop=N` - Set tab width
|
- [x] `:set tabstop=N` - Set tab width
|
||||||
- [x] `:register {name}` - Show register contents
|
- [x] `:register {name}` - Show register contents
|
||||||
- [ ] `:w` - Write file
|
- [x] `:w` - Write file
|
||||||
- [ ] `:q` - Quit
|
- [x] `:q` - Quit
|
||||||
- [ ] `:wq` - Write and quit
|
- [x] `:wq` - Write and quit
|
||||||
- [ ] `:q!` - Force quit
|
- [x] `:q!` - Force quit
|
||||||
- [ ] `:e {file}` - Edit file
|
- [x] `:e {file}` - Edit file
|
||||||
- [ ] `:bn` / `:bp` - Next/previous buffer
|
- [ ] `:bn` / `:bp` - Next/previous buffer
|
||||||
- [ ] `:{range}` - Go to line
|
- [ ] `:{range}` - Go to line
|
||||||
- [ ] `:%s/old/new/g` - Search and replace
|
- [ ] `:%s/old/new/g` - Search and replace
|
||||||
@ -264,15 +264,15 @@
|
|||||||
Buffers are in-memory representations of files. A buffer exists for each open file.
|
Buffers are in-memory representations of files. A buffer exists for each open file.
|
||||||
|
|
||||||
### Buffer Model
|
### Buffer Model
|
||||||
- [ ] Buffer struct (id, filename, lines, modified flag, cursor position)
|
- [x] Buffer struct (id, filename, lines, modified flag, cursor position)
|
||||||
- [ ] Buffer list/manager
|
- [ ] Buffer list/manager
|
||||||
- [ ] Current buffer tracking
|
- [x] Current buffer tracking
|
||||||
- [ ] Buffer-local settings (tabstop, filetype, etc.)
|
- [ ] Buffer-local settings (tabstop, filetype, etc.)
|
||||||
- [ ] Modified/dirty state tracking
|
- [x] Modified/dirty state tracking
|
||||||
- [ ] Read-only buffer support
|
- [x] Read-only buffer support
|
||||||
|
|
||||||
### Buffer Navigation
|
### Buffer Navigation
|
||||||
- [ ] `:e {file}` - Edit file (open in new buffer or switch to existing)
|
- [x] `:e {file}` - Edit file (open in new buffer or switch to existing)
|
||||||
- [ ] `:bn` / `:bnext` - Next buffer
|
- [ ] `:bn` / `:bnext` - Next buffer
|
||||||
- [ ] `:bp` / `:bprev` - Previous buffer
|
- [ ] `:bp` / `:bprev` - Previous buffer
|
||||||
- [ ] `:b {name}` - Switch to buffer by name (partial match)
|
- [ ] `:b {name}` - Switch to buffer by name (partial match)
|
||||||
@ -286,9 +286,9 @@ Buffers are in-memory representations of files. A buffer exists for each open fi
|
|||||||
- [ ] `:bd` / `:bdelete` - Delete buffer (close file)
|
- [ ] `:bd` / `:bdelete` - Delete buffer (close file)
|
||||||
- [ ] `:bd!` - Force delete buffer (discard changes)
|
- [ ] `:bd!` - Force delete buffer (discard changes)
|
||||||
- [ ] `:bw` / `:bwipeout` - Wipe buffer (remove completely)
|
- [ ] `:bw` / `:bwipeout` - Wipe buffer (remove completely)
|
||||||
- [ ] `:w` - Write current buffer to file
|
- [x] `:w` - Write current buffer to file
|
||||||
- [ ] `:w {file}` - Write buffer to specific file
|
- [x] `:w {file}` - Write buffer to specific file
|
||||||
- [ ] `:wa` - Write all modified buffers
|
- [x] `:wa` - Write all modified buffers
|
||||||
- [ ] `:sav {file}` - Save as (write to new file, switch to it)
|
- [ ] `:sav {file}` - Save as (write to new file, switch to it)
|
||||||
|
|
||||||
### Buffer State
|
### Buffer State
|
||||||
@ -309,8 +309,8 @@ Buffers are in-memory representations of files. A buffer exists for each open fi
|
|||||||
|
|
||||||
### Hidden Buffers
|
### Hidden Buffers
|
||||||
- [ ] `:set hidden` - Allow switching with unsaved changes
|
- [ ] `:set hidden` - Allow switching with unsaved changes
|
||||||
- [ ] Prompt to save when closing modified buffer
|
- [x] Prompt to save when closing modified buffer
|
||||||
- [ ] `:q` behavior with modified buffers
|
- [x] `:q` behavior with modified buffers
|
||||||
|
|
||||||
### Argument List (Advanced)
|
### Argument List (Advanced)
|
||||||
- [ ] `:args` - Show argument list
|
- [ ] `:args` - Show argument list
|
||||||
@ -381,8 +381,8 @@ Buffers are in-memory representations of files. A buffer exists for each open fi
|
|||||||
- [ ] Color column (ruler)
|
- [ ] Color column (ruler)
|
||||||
|
|
||||||
### Files
|
### Files
|
||||||
- [ ] File reading
|
- [x] File reading
|
||||||
- [ ] File writing
|
- [x] File writing
|
||||||
- [ ] Auto-save
|
- [ ] Auto-save
|
||||||
- [ ] Backup files
|
- [ ] Backup files
|
||||||
- [ ] Swap files
|
- [ ] Swap files
|
||||||
@ -405,9 +405,49 @@ Buffers are in-memory representations of files. A buffer exists for each open fi
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Testing Coverage
|
### Well Tested - Editor Core
|
||||||
|
|
||||||
### Well Tested
|
#### 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] Basic motions (h, j, k, l)
|
||||||
- [x] Word motions (w, e, b)
|
- [x] Word motions (w, e, b)
|
||||||
- [x] Jump motions (G, gg, 0, $, _, ^, |)
|
- [x] Jump motions (G, gg, 0, $, _, ^, |)
|
||||||
@ -415,10 +455,12 @@ Buffers are in-memory representations of files. A buffer exists for each open fi
|
|||||||
- [x] Delete operator (d, dd)
|
- [x] Delete operator (d, dd)
|
||||||
- [x] Yank operator (y, yy)
|
- [x] Yank operator (y, yy)
|
||||||
- [x] Paste actions (p, P)
|
- [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 entry (i, a, I, A, o, O)
|
||||||
- [x] Insert mode editing (enter, backspace, delete, tab, ctrl+w)
|
- [x] Insert mode editing (enter, backspace, delete, tab, ctrl+w)
|
||||||
- [x] Visual modes (v, V, ctrl+v)
|
- [x] Visual modes (v, V, ctrl+v)
|
||||||
- [x] Visual mode with motions
|
- [x] Visual mode with motions
|
||||||
- [x] Delete actions (x, D)
|
- [x] Delete actions (x, D)
|
||||||
- [x] Command mode basics
|
|
||||||
- [x] Register behavior
|
- [x] Register behavior
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,32 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"os"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/program"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/editor"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
func generateLines(n int) []string {
|
// main: Entry point for the Gim text editor. Creates a buffer and window,
|
||||||
lines := make([]string, n)
|
// initializes the editor model, and runs the BubbleTea TUI program.
|
||||||
for i := range n {
|
func main() {
|
||||||
lines[i] = fmt.Sprintf("line %d", i+1)
|
// <exe> <filename>
|
||||||
}
|
args := os.Args[1:]
|
||||||
return lines
|
|
||||||
|
var prog *tea.Program
|
||||||
|
if len(args) < 1 {
|
||||||
|
prog = program.NewProgramBuilder().
|
||||||
|
EmptyProgram().
|
||||||
|
WithOpt(tea.WithAltScreen()).
|
||||||
|
Build()
|
||||||
|
} else {
|
||||||
|
prog = program.NewProgramBuilder().
|
||||||
|
FileProgram(args[0]).
|
||||||
|
WithOpt(tea.WithAltScreen()).
|
||||||
|
Build()
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
if _, err := prog.Run(); err != nil {
|
||||||
tea.NewProgram(
|
panic(err)
|
||||||
editor.NewModel(generateLines(64), action.Position{Line: 0, Col: 0}),
|
}
|
||||||
tea.WithAltScreen(),
|
|
||||||
).Run()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,152 +0,0 @@
|
|||||||
package action
|
|
||||||
|
|
||||||
import (
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Mode constants for editor mode
|
|
||||||
type Mode int
|
|
||||||
|
|
||||||
const (
|
|
||||||
NormalMode Mode = iota
|
|
||||||
InsertMode
|
|
||||||
CommandMode
|
|
||||||
VisualMode
|
|
||||||
VisualLineMode
|
|
||||||
VisualBlockMode
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m Mode) ToString() string {
|
|
||||||
switch m {
|
|
||||||
case NormalMode:
|
|
||||||
return "NORMAL"
|
|
||||||
case InsertMode:
|
|
||||||
return "INSERT"
|
|
||||||
case CommandMode:
|
|
||||||
return "COMMAND"
|
|
||||||
case VisualMode:
|
|
||||||
return "VISUAL"
|
|
||||||
case VisualLineMode:
|
|
||||||
return "V-LINE"
|
|
||||||
case VisualBlockMode:
|
|
||||||
return "V-BLOCK"
|
|
||||||
default:
|
|
||||||
return "-----"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Mode) IsVisualMode() bool {
|
|
||||||
return m == VisualMode ||
|
|
||||||
m == VisualLineMode ||
|
|
||||||
m == VisualBlockMode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Model defines the interface for editor state that actions can modify
|
|
||||||
type Model interface {
|
|
||||||
// Text buffer
|
|
||||||
Lines() []string
|
|
||||||
Line(idx int) string
|
|
||||||
SetLine(idx int, content string)
|
|
||||||
InsertLine(idx int, content string)
|
|
||||||
DeleteLine(idx int)
|
|
||||||
LineCount() int
|
|
||||||
|
|
||||||
// Cursor
|
|
||||||
CursorX() int
|
|
||||||
CursorY() int
|
|
||||||
SetCursorX(x int)
|
|
||||||
SetCursorY(y int)
|
|
||||||
ClampCursorX()
|
|
||||||
|
|
||||||
// Window
|
|
||||||
ScrollY() int
|
|
||||||
SetScrollY(y int)
|
|
||||||
WinH() int
|
|
||||||
WinW() int
|
|
||||||
ViewPortH() int
|
|
||||||
|
|
||||||
// Anchor
|
|
||||||
AnchorX() int
|
|
||||||
AnchorY() int
|
|
||||||
SetAnchorX(x int)
|
|
||||||
SetAnchorY(y int)
|
|
||||||
|
|
||||||
// Insert
|
|
||||||
InsertKeys() []string
|
|
||||||
SetInsertKeys(keys []string)
|
|
||||||
|
|
||||||
// Command mode
|
|
||||||
Command() string
|
|
||||||
SetCommand(cmd string)
|
|
||||||
CommandCursor() int
|
|
||||||
SetCommandCursor(cur int)
|
|
||||||
CommandError() error
|
|
||||||
SetCommandError(err error)
|
|
||||||
CommandOutput() string
|
|
||||||
SetCommandOutput(out string)
|
|
||||||
|
|
||||||
// Settings
|
|
||||||
Settings() Settings
|
|
||||||
SetSettings(s Settings)
|
|
||||||
|
|
||||||
// Registers
|
|
||||||
Registers() map[rune]Register
|
|
||||||
GetRegister(name rune) (Register, bool)
|
|
||||||
SetRegister(name rune, t RegisterType, cnt []string) error
|
|
||||||
UpdateDefaultRegister(t RegisterType, cnt []string)
|
|
||||||
|
|
||||||
// Mode
|
|
||||||
Mode() Mode
|
|
||||||
SetMode(mode Mode)
|
|
||||||
|
|
||||||
// Insert recording (for count replay)
|
|
||||||
SetInsertRecording(count int, action Action)
|
|
||||||
|
|
||||||
// ExitInsertMode handles replay, cursor step-back, and mode transition on esc
|
|
||||||
ExitInsertMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Position represents a location in the buffer
|
|
||||||
type Position struct {
|
|
||||||
Line, Col int
|
|
||||||
}
|
|
||||||
|
|
||||||
// MotionType indicates how a motion operates
|
|
||||||
type MotionType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
CharwiseExclusive MotionType = iota // w, b, h, l, 0, ^ - end position not included
|
|
||||||
CharwiseInclusive // e, $, f - end position is included
|
|
||||||
Linewise // j, k, G, gg, {, } - operates on whole lines
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsCharwise returns true if the motion type is character-based (not linewise)
|
|
||||||
func (mt MotionType) IsCharwise() bool {
|
|
||||||
return mt == CharwiseExclusive || mt == CharwiseInclusive
|
|
||||||
}
|
|
||||||
|
|
||||||
// Action is the base interface - anything executable
|
|
||||||
type Action interface {
|
|
||||||
Execute(m Model) tea.Cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Motion moves the cursor and returns the range covered
|
|
||||||
type Motion interface {
|
|
||||||
Action
|
|
||||||
Type() MotionType
|
|
||||||
}
|
|
||||||
|
|
||||||
// Operator acts on a range (delete, yank, change)
|
|
||||||
type Operator interface {
|
|
||||||
Operate(m Model, start, end Position, mtype MotionType) tea.Cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoublePresser is an optional interface for operators that support double-press (dd, yy, cc)
|
|
||||||
type DoublePresser interface {
|
|
||||||
DoublePress(m Model, count int) tea.Cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repeatable actions track count
|
|
||||||
type Repeatable interface {
|
|
||||||
WithCount(n int) Action
|
|
||||||
}
|
|
||||||
@ -1,26 +1,33 @@
|
|||||||
package action
|
package action
|
||||||
|
|
||||||
import tea "github.com/charmbracelet/bubbletea"
|
import (
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
// ChangeToEndOfLine implements Action (C) - changes from cursor to end of line
|
// ChangeToEndOfLine implements Action (C) - changes from cursor to end of line
|
||||||
type ChangeToEndOfLine struct {
|
type ChangeToEndOfLine struct {
|
||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChangeToEndOfLine.Execute: Changes from cursor to end of line and enters insert mode (C key).
|
||||||
func (a ChangeToEndOfLine) Execute(m Model) tea.Cmd {
|
func (a ChangeToEndOfLine) Execute(m Model) tea.Cmd {
|
||||||
pos := m.CursorX()
|
win := m.ActiveWindow()
|
||||||
line := m.Line(m.CursorY())
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
pos := win.Cursor.Col
|
||||||
|
line := buf.Lines[win.Cursor.Line]
|
||||||
|
|
||||||
// Save deleted text to register
|
// Save deleted text to register
|
||||||
if pos < len(line) {
|
if pos < len(line) {
|
||||||
m.UpdateDefaultRegister(CharwiseRegister, []string{line[pos:]})
|
m.UpdateDefaultRegister(core.CharwiseRegister, []string{line[pos:]})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete to end of line
|
// Delete to end of line
|
||||||
m.SetLine(m.CursorY(), line[:pos])
|
buf.SetLine(win.Cursor.Line, line[:pos])
|
||||||
|
|
||||||
// Enter insert mode
|
// Enter insert mode
|
||||||
m.SetMode(InsertMode)
|
m.SetMode(core.InsertMode)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -28,6 +35,7 @@ func (a ChangeToEndOfLine) Execute(m Model) tea.Cmd {
|
|||||||
// Ensure ChangeToEndOfLine implements Repeatable
|
// Ensure ChangeToEndOfLine implements Repeatable
|
||||||
var _ Repeatable = ChangeToEndOfLine{}
|
var _ Repeatable = ChangeToEndOfLine{}
|
||||||
|
|
||||||
|
// ChangeToEndOfLine.WithCount: Returns a new ChangeToEndOfLine with the given count.
|
||||||
func (a ChangeToEndOfLine) WithCount(n int) Action {
|
func (a ChangeToEndOfLine) WithCount(n int) Action {
|
||||||
return ChangeToEndOfLine{Count: n}
|
return ChangeToEndOfLine{Count: n}
|
||||||
}
|
}
|
||||||
@ -37,23 +45,27 @@ type SubstituteChar struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubstituteChar.Execute: Deletes Count characters and enters insert mode (s key).
|
||||||
func (a SubstituteChar) Execute(m Model) tea.Cmd {
|
func (a SubstituteChar) Execute(m Model) tea.Cmd {
|
||||||
pos := m.CursorX()
|
win := m.ActiveWindow()
|
||||||
line := m.Line(m.CursorY())
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
pos := win.Cursor.Col
|
||||||
|
line := buf.Lines[win.Cursor.Line]
|
||||||
|
|
||||||
// Calculate how many chars to delete (limited by line length)
|
// Calculate how many chars to delete (limited by line length)
|
||||||
count := min(a.Count, len(line)-pos)
|
count := min(a.Count, len(line)-pos)
|
||||||
|
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
// Save deleted text to register
|
// Save deleted text to register
|
||||||
m.UpdateDefaultRegister(CharwiseRegister, []string{line[pos : pos+count]})
|
m.UpdateDefaultRegister(core.CharwiseRegister, []string{line[pos : pos+count]})
|
||||||
|
|
||||||
// Delete the characters
|
// Delete the characters
|
||||||
m.SetLine(m.CursorY(), line[:pos]+line[pos+count:])
|
buf.SetLine(win.Cursor.Line, line[:pos]+line[pos+count:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter insert mode
|
// Enter insert mode
|
||||||
m.SetMode(InsertMode)
|
m.SetMode(core.InsertMode)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -61,6 +73,7 @@ func (a SubstituteChar) Execute(m Model) tea.Cmd {
|
|||||||
// Ensure SubstituteChar implements Repeatable
|
// Ensure SubstituteChar implements Repeatable
|
||||||
var _ Repeatable = SubstituteChar{}
|
var _ Repeatable = SubstituteChar{}
|
||||||
|
|
||||||
|
// SubstituteChar.WithCount: Returns a new SubstituteChar with the given count.
|
||||||
func (a SubstituteChar) WithCount(n int) Action {
|
func (a SubstituteChar) WithCount(n int) Action {
|
||||||
return SubstituteChar{Count: n}
|
return SubstituteChar{Count: n}
|
||||||
}
|
}
|
||||||
@ -70,33 +83,36 @@ type SubstituteLine struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubstituteLine.Execute: Clears Count lines and enters insert mode (S key).
|
||||||
func (a SubstituteLine) Execute(m Model) tea.Cmd {
|
func (a SubstituteLine) Execute(m Model) tea.Cmd {
|
||||||
y := m.CursorY()
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
y := win.Cursor.Line
|
||||||
|
|
||||||
// Calculate how many lines to substitute
|
// Calculate how many lines to substitute
|
||||||
count := min(a.Count, m.LineCount()-y)
|
count := min(a.Count, buf.LineCount()-y)
|
||||||
|
|
||||||
var lines []string
|
var lines []string
|
||||||
|
|
||||||
// Collect and delete lines
|
// Collect and delete lines
|
||||||
for range count {
|
for range count {
|
||||||
lines = append(lines, m.Line(y))
|
lines = append(lines, buf.Lines[y])
|
||||||
m.DeleteLine(y)
|
buf.DeleteLine(y)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save deleted lines to register
|
// Save deleted lines to register
|
||||||
m.UpdateDefaultRegister(LinewiseRegister, lines)
|
m.UpdateDefaultRegister(core.LinewiseRegister, lines)
|
||||||
|
|
||||||
// Insert empty line at original position
|
// Insert empty line at original position
|
||||||
insertY := min(y, m.LineCount())
|
insertY := min(y, buf.LineCount())
|
||||||
m.InsertLine(insertY, "")
|
buf.InsertLine(insertY, "")
|
||||||
|
|
||||||
// Position cursor
|
// Position cursor
|
||||||
m.SetCursorY(insertY)
|
win.SetCursorPos(insertY, 0)
|
||||||
m.SetCursorX(0)
|
|
||||||
|
|
||||||
// Enter insert mode
|
// Enter insert mode
|
||||||
m.SetMode(InsertMode)
|
m.SetMode(core.InsertMode)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -104,6 +120,7 @@ func (a SubstituteLine) Execute(m Model) tea.Cmd {
|
|||||||
// Ensure SubstituteLine implements Repeatable
|
// Ensure SubstituteLine implements Repeatable
|
||||||
var _ Repeatable = SubstituteLine{}
|
var _ Repeatable = SubstituteLine{}
|
||||||
|
|
||||||
|
// SubstituteLine.WithCount: Returns a new SubstituteLine with the given count.
|
||||||
func (a SubstituteLine) WithCount(n int) Action {
|
func (a SubstituteLine) WithCount(n int) Action {
|
||||||
return SubstituteLine{Count: n}
|
return SubstituteLine{Count: n}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,29 @@
|
|||||||
package action
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ExitCommandMode implements Action - exits command mode and returns to normal mode.
|
||||||
type ExitCommandMode struct{}
|
type ExitCommandMode struct{}
|
||||||
|
|
||||||
|
// ExitCommandMode.Execute: Exits command mode and returns to normal mode (Esc key).
|
||||||
func (a ExitCommandMode) Execute(m Model) tea.Cmd {
|
func (a ExitCommandMode) Execute(m Model) tea.Cmd {
|
||||||
m.SetCommandCursor(0)
|
m.SetCommandCursor(0)
|
||||||
m.SetCommand("")
|
m.SetCommand("")
|
||||||
m.SetCommandOutput("")
|
m.SetCommandOutput("")
|
||||||
m.SetCommandError(nil)
|
m.SetCommandError(nil)
|
||||||
m.SetMode(NormalMode)
|
m.SetMode(core.NormalMode)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InsertCommandChar implements Action - inserts a character in command mode.
|
||||||
type InsertCommandChar struct {
|
type InsertCommandChar struct {
|
||||||
Char string
|
Char string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InsertCommandChar.Execute: Inserts a character at the command cursor position.
|
||||||
func (a InsertCommandChar) Execute(m Model) tea.Cmd {
|
func (a InsertCommandChar) Execute(m Model) tea.Cmd {
|
||||||
cur := m.CommandCursor()
|
cur := m.CommandCursor()
|
||||||
cmd := m.Command()
|
cmd := m.Command()
|
||||||
@ -28,8 +33,10 @@ func (a InsertCommandChar) Execute(m Model) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CommandBackspace implements Action - deletes character before cursor in command mode.
|
||||||
type CommandBackspace struct{}
|
type CommandBackspace struct{}
|
||||||
|
|
||||||
|
// CommandBackspace.Execute: Deletes the character before the command cursor (Backspace key).
|
||||||
func (a CommandBackspace) Execute(m Model) tea.Cmd {
|
func (a CommandBackspace) Execute(m Model) tea.Cmd {
|
||||||
cur := m.CommandCursor()
|
cur := m.CommandCursor()
|
||||||
cmd := m.Command()
|
cmd := m.Command()
|
||||||
@ -42,8 +49,10 @@ func (a CommandBackspace) Execute(m Model) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CommandDelete implements Action - deletes character at cursor in command mode.
|
||||||
type CommandDelete struct{}
|
type CommandDelete struct{}
|
||||||
|
|
||||||
|
// CommandDelete.Execute: Deletes the character at the command cursor (Delete key).
|
||||||
func (a CommandDelete) Execute(m Model) tea.Cmd {
|
func (a CommandDelete) Execute(m Model) tea.Cmd {
|
||||||
cur := m.CommandCursor()
|
cur := m.CommandCursor()
|
||||||
cmd := m.Command()
|
cmd := m.Command()
|
||||||
@ -62,8 +71,10 @@ func (a CommandDelete) Execute(m Model) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CommandDeletePreviousWord implements Action - deletes word before cursor in command mode.
|
||||||
type CommandDeletePreviousWord struct{}
|
type CommandDeletePreviousWord struct{}
|
||||||
|
|
||||||
|
// CommandDeletePreviousWord.Execute: Deletes the word before the command cursor (Ctrl+W).
|
||||||
func (a CommandDeletePreviousWord) Execute(m Model) tea.Cmd {
|
func (a CommandDeletePreviousWord) Execute(m Model) tea.Cmd {
|
||||||
cur := m.CommandCursor()
|
cur := m.CommandCursor()
|
||||||
cmd := m.Command()
|
cmd := m.Command()
|
||||||
@ -101,22 +112,24 @@ func (a CommandDeletePreviousWord) Execute(m Model) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CommandExecute implements Action - executes the command line.
|
||||||
type CommandExecute struct {
|
type CommandExecute struct {
|
||||||
Registry CommandRegistry
|
Registry CommandRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommandRegistry interface for executing commands
|
// CommandRegistry: Interface for executing commands.
|
||||||
type CommandRegistry interface {
|
type CommandRegistry interface {
|
||||||
Execute(m Model, cmdLine string) (tea.Cmd, error)
|
Execute(m Model, cmdLine string) (tea.Cmd, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CommandExecute.Execute: Executes the command line (Enter key).
|
||||||
func (a CommandExecute) Execute(m Model) tea.Cmd {
|
func (a CommandExecute) Execute(m Model) tea.Cmd {
|
||||||
cmdLine := m.Command()
|
cmdLine := m.Command()
|
||||||
|
|
||||||
// Clear command state and return to normal mode
|
// Clear command state and return to normal mode
|
||||||
m.SetCommandCursor(0)
|
m.SetCommandCursor(0)
|
||||||
m.SetCommandError(nil)
|
m.SetCommandError(nil)
|
||||||
m.SetMode(NormalMode)
|
m.SetMode(core.NormalMode)
|
||||||
|
|
||||||
if a.Registry == nil || cmdLine == "" {
|
if a.Registry == nil || cmdLine == "" {
|
||||||
return nil
|
return nil
|
||||||
@ -124,7 +137,6 @@ func (a CommandExecute) Execute(m Model) tea.Cmd {
|
|||||||
|
|
||||||
cmd, err := a.Registry.Execute(m, cmdLine)
|
cmd, err := a.Registry.Execute(m, cmdLine)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: Display error message to user
|
|
||||||
m.SetCommandError(err)
|
m.SetCommandError(err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,70 +7,80 @@ type DeleteChar struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteChar.Execute: Deletes Count characters at the cursor position (x key).
|
||||||
func (a DeleteChar) Execute(m Model) tea.Cmd {
|
func (a DeleteChar) Execute(m Model) tea.Cmd {
|
||||||
pos := m.CursorX()
|
win := m.ActiveWindow()
|
||||||
line := m.Line(m.CursorY())
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
pos := win.Cursor.Col
|
||||||
|
line := buf.Lines[win.Cursor.Line]
|
||||||
for i := 0; i < a.Count && pos < len(line); i++ {
|
for i := 0; i < a.Count && pos < len(line); i++ {
|
||||||
line = line[:pos] + line[pos+1:]
|
line = line[:pos] + line[pos+1:]
|
||||||
m.SetLine(m.CursorY(), line)
|
buf.SetLine(win.Cursor.Line, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteChar.WithCount: Returns a new DeleteChar with the given count.
|
||||||
func (a DeleteChar) WithCount(n int) Action {
|
func (a DeleteChar) WithCount(n int) Action {
|
||||||
return DeleteChar{Count: n}
|
return DeleteChar{Count: n}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteToEndOfLine implements Action (D) - deletes from cursor to end of line
|
||||||
|
// and optionally Count-1 additional lines below.
|
||||||
type DeleteToEndOfLine struct {
|
type DeleteToEndOfLine struct {
|
||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteToEndOfLine.Execute: Deletes from cursor to end of line and Count-1 lines below (D key).
|
||||||
func (a DeleteToEndOfLine) Execute(m Model) tea.Cmd {
|
func (a DeleteToEndOfLine) Execute(m Model) tea.Cmd {
|
||||||
// Delete to end of line
|
win := m.ActiveWindow()
|
||||||
pos := m.CursorX()
|
buf := m.ActiveBuffer()
|
||||||
line := m.Line(m.CursorY())
|
|
||||||
|
|
||||||
m.SetLine(m.CursorY(), line[:pos])
|
// Delete to end of line
|
||||||
m.SetCursorX(pos - 1)
|
pos := win.Cursor.Col
|
||||||
|
line := buf.Lines[win.Cursor.Line]
|
||||||
|
|
||||||
|
buf.SetLine(win.Cursor.Line, line[:pos])
|
||||||
|
win.SetCursorCol(pos - 1)
|
||||||
|
|
||||||
// If count is greater, than we will delete the next N - 1 lines below
|
// If count is greater, than we will delete the next N - 1 lines below
|
||||||
initY := m.CursorY()
|
initY := win.Cursor.Line
|
||||||
if a.Count > 1 {
|
if a.Count > 1 {
|
||||||
// Copied from `internal/operator/delete.go`
|
// Copied from `internal/operator/delete.go`
|
||||||
opCount := min(a.Count-1, m.LineCount()-m.CursorY())
|
opCount := min(a.Count-1, buf.LineCount()-win.Cursor.Line)
|
||||||
|
|
||||||
// Down one
|
// Down one
|
||||||
m.SetCursorY(initY + 1)
|
win.SetCursorLine(initY + 1)
|
||||||
|
|
||||||
for range opCount {
|
for range opCount {
|
||||||
y := m.CursorY() // Changed from the copied code
|
y := win.Cursor.Line // Changed from the copied code
|
||||||
|
|
||||||
// Stop if were on the starting line
|
// Stop if were on the starting line
|
||||||
if y == initY {
|
if y == initY {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
m.DeleteLine(y)
|
buf.DeleteLine(y)
|
||||||
|
|
||||||
if m.LineCount() == 0 {
|
if buf.LineCount() == 0 {
|
||||||
m.InsertLine(0, "")
|
buf.InsertLine(0, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
if y >= m.LineCount() {
|
if y >= buf.LineCount() {
|
||||||
y = m.LineCount() - 1
|
y = buf.LineCount() - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetCursorY(y)
|
win.SetCursorLine(y)
|
||||||
m.ClampCursorX()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetCursorY(initY)
|
win.SetCursorLine(initY)
|
||||||
m.ClampCursorX()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteToEndOfLine.WithCount: Returns a new DeleteToEndOfLine with the given count.
|
||||||
func (a DeleteToEndOfLine) WithCount(n int) Action {
|
func (a DeleteToEndOfLine) WithCount(n int) Action {
|
||||||
return DeleteToEndOfLine{Count: n}
|
return DeleteToEndOfLine{Count: n}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package action
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -11,13 +12,15 @@ type EnterInsert struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnterInsert.Execute: Enters insert mode at the cursor position (i key).
|
||||||
func (a EnterInsert) Execute(m Model) tea.Cmd {
|
func (a EnterInsert) Execute(m Model) tea.Cmd {
|
||||||
// Start recording
|
// Start recording
|
||||||
m.SetInsertRecording(a.Count, a)
|
m.SetInsertRecording(a.Count, a)
|
||||||
m.SetMode(InsertMode)
|
m.SetMode(core.InsertMode)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnterInsert.WithCount: Returns a new EnterInsert with the given count.
|
||||||
func (a EnterInsert) WithCount(n int) Action {
|
func (a EnterInsert) WithCount(n int) Action {
|
||||||
return EnterInsert{Count: n}
|
return EnterInsert{Count: n}
|
||||||
}
|
}
|
||||||
@ -27,16 +30,18 @@ type EnterInsertAfter struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnterInsertAfter.Execute: Enters insert mode after the cursor position (a key).
|
||||||
func (a EnterInsertAfter) Execute(m Model) tea.Cmd {
|
func (a EnterInsertAfter) Execute(m Model) tea.Cmd {
|
||||||
m.SetCursorX(m.CursorX() + 1)
|
win := m.ActiveWindow()
|
||||||
m.ClampCursorX()
|
win.SetCursorCol(win.Cursor.Col + 1)
|
||||||
|
|
||||||
// Start recording
|
// Start recording
|
||||||
m.SetInsertRecording(a.Count, a)
|
m.SetInsertRecording(a.Count, a)
|
||||||
m.SetMode(InsertMode)
|
m.SetMode(core.InsertMode)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnterInsertAfter.WithCount: Returns a new EnterInsertAfter with the given count.
|
||||||
func (a EnterInsertAfter) WithCount(n int) Action {
|
func (a EnterInsertAfter) WithCount(n int) Action {
|
||||||
return EnterInsertAfter{Count: n}
|
return EnterInsertAfter{Count: n}
|
||||||
}
|
}
|
||||||
@ -46,16 +51,18 @@ type EnterInsertLineStart struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnterInsertLineStart.Execute: Enters insert mode at the start of the line (I key).
|
||||||
func (a EnterInsertLineStart) Execute(m Model) tea.Cmd {
|
func (a EnterInsertLineStart) Execute(m Model) tea.Cmd {
|
||||||
m.SetCursorX(0)
|
win := m.ActiveWindow()
|
||||||
m.ClampCursorX()
|
win.SetCursorCol(0)
|
||||||
|
|
||||||
// Start recording
|
// Start recording
|
||||||
m.SetInsertRecording(a.Count, a)
|
m.SetInsertRecording(a.Count, a)
|
||||||
m.SetMode(InsertMode)
|
m.SetMode(core.InsertMode)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnterInsertLineStart.WithCount: Returns a new EnterInsertLineStart with the given count.
|
||||||
func (a EnterInsertLineStart) WithCount(n int) Action {
|
func (a EnterInsertLineStart) WithCount(n int) Action {
|
||||||
return EnterInsertLineStart{Count: n}
|
return EnterInsertLineStart{Count: n}
|
||||||
}
|
}
|
||||||
@ -65,16 +72,19 @@ type EnterInsertLineEnd struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnterInsertLineEnd.Execute: Enters insert mode at the end of the line (A key).
|
||||||
func (a EnterInsertLineEnd) Execute(m Model) tea.Cmd {
|
func (a EnterInsertLineEnd) Execute(m Model) tea.Cmd {
|
||||||
m.SetCursorX(len(m.Line(m.CursorY())))
|
win := m.ActiveWindow()
|
||||||
m.ClampCursorX()
|
buf := m.ActiveBuffer()
|
||||||
|
win.SetCursorCol(len(buf.Lines[win.Cursor.Line]))
|
||||||
|
|
||||||
// Start recording
|
// Start recording
|
||||||
m.SetInsertRecording(a.Count, a)
|
m.SetInsertRecording(a.Count, a)
|
||||||
m.SetMode(InsertMode)
|
m.SetMode(core.InsertMode)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnterInsertLineEnd.WithCount: Returns a new EnterInsertLineEnd with the given count.
|
||||||
func (a EnterInsertLineEnd) WithCount(n int) Action {
|
func (a EnterInsertLineEnd) WithCount(n int) Action {
|
||||||
return EnterInsertLineEnd{Count: n}
|
return EnterInsertLineEnd{Count: n}
|
||||||
}
|
}
|
||||||
@ -84,24 +94,28 @@ type OpenLineBelow struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenLineBelow.Execute: Opens a new line below the cursor and enters insert mode (o key).
|
||||||
func (a OpenLineBelow) Execute(m Model) tea.Cmd {
|
func (a OpenLineBelow) Execute(m Model) tea.Cmd {
|
||||||
pos := m.CursorY()
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
if pos >= m.LineCount() {
|
pos := win.Cursor.Line
|
||||||
m.InsertLine(m.LineCount(), "")
|
|
||||||
|
if pos >= buf.LineCount() {
|
||||||
|
buf.InsertLine(buf.LineCount(), "")
|
||||||
} else {
|
} else {
|
||||||
m.InsertLine(pos+1, "")
|
buf.InsertLine(pos+1, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetCursorY(m.CursorY() + 1)
|
win.SetCursorPos(win.Cursor.Line+1, 0)
|
||||||
m.SetCursorX(0)
|
|
||||||
|
|
||||||
// Start recording
|
// Start recording
|
||||||
m.SetInsertRecording(a.Count, a)
|
m.SetInsertRecording(a.Count, a)
|
||||||
m.SetMode(InsertMode)
|
m.SetMode(core.InsertMode)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenLineBelow.WithCount: Returns a new OpenLineBelow with the given count.
|
||||||
func (a OpenLineBelow) WithCount(n int) Action {
|
func (a OpenLineBelow) WithCount(n int) Action {
|
||||||
return OpenLineBelow{Count: n}
|
return OpenLineBelow{Count: n}
|
||||||
}
|
}
|
||||||
@ -111,17 +125,22 @@ type OpenLineAbove struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenLineAbove.Execute: Opens a new line above the cursor and enters insert mode (O key).
|
||||||
func (a OpenLineAbove) Execute(m Model) tea.Cmd {
|
func (a OpenLineAbove) Execute(m Model) tea.Cmd {
|
||||||
pos := m.CursorY()
|
win := m.ActiveWindow()
|
||||||
m.InsertLine(pos, "")
|
buf := m.ActiveBuffer()
|
||||||
m.SetCursorX(0)
|
|
||||||
|
pos := win.Cursor.Line
|
||||||
|
buf.InsertLine(pos, "")
|
||||||
|
win.SetCursorCol(0)
|
||||||
|
|
||||||
// Start recording
|
// Start recording
|
||||||
m.SetInsertRecording(a.Count, a)
|
m.SetInsertRecording(a.Count, a)
|
||||||
m.SetMode(InsertMode)
|
m.SetMode(core.InsertMode)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenLineAbove.WithCount: Returns a new OpenLineAbove with the given count.
|
||||||
func (a OpenLineAbove) WithCount(n int) Action {
|
func (a OpenLineAbove) WithCount(n int) Action {
|
||||||
return OpenLineAbove{Count: n}
|
return OpenLineAbove{Count: n}
|
||||||
}
|
}
|
||||||
@ -133,51 +152,61 @@ type InsertChar struct {
|
|||||||
Char string
|
Char string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InsertChar.Execute: Inserts a single character at the cursor position.
|
||||||
func (a InsertChar) Execute(m Model) tea.Cmd {
|
func (a InsertChar) Execute(m Model) tea.Cmd {
|
||||||
x, y := m.CursorX(), m.CursorY()
|
win := m.ActiveWindow()
|
||||||
l := m.Line(y)
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
x, y := win.Cursor.Col, win.Cursor.Line
|
||||||
|
l := buf.Lines[y]
|
||||||
if x < len(l) {
|
if x < len(l) {
|
||||||
m.SetLine(y, l[:x]+a.Char+l[x:])
|
buf.SetLine(y, l[:x]+a.Char+l[x:])
|
||||||
} else {
|
} else {
|
||||||
m.SetLine(y, l+a.Char)
|
buf.SetLine(y, l+a.Char)
|
||||||
}
|
}
|
||||||
m.SetCursorX(x + len(a.Char))
|
win.SetCursorCol(x + len(a.Char))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InsertNewline splits the current line at the cursor (enter key)
|
// InsertNewline splits the current line at the cursor (enter key)
|
||||||
type InsertNewline struct{}
|
type InsertNewline struct{}
|
||||||
|
|
||||||
|
// InsertNewline.Execute: Splits the current line at the cursor (Enter key).
|
||||||
func (a InsertNewline) Execute(m Model) tea.Cmd {
|
func (a InsertNewline) Execute(m Model) tea.Cmd {
|
||||||
x, y := m.CursorX(), m.CursorY()
|
win := m.ActiveWindow()
|
||||||
l := m.Line(y)
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
x, y := win.Cursor.Col, win.Cursor.Line
|
||||||
|
l := buf.Lines[y]
|
||||||
if x == len(l) {
|
if x == len(l) {
|
||||||
m.InsertLine(y+1, "")
|
buf.InsertLine(y+1, "")
|
||||||
} else {
|
} else {
|
||||||
m.SetLine(y, l[:x])
|
buf.SetLine(y, l[:x])
|
||||||
m.InsertLine(y+1, l[x:])
|
buf.InsertLine(y+1, l[x:])
|
||||||
}
|
}
|
||||||
m.SetCursorY(y + 1)
|
win.SetCursorPos(y+1, 0)
|
||||||
m.SetCursorX(0)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InsertBackspace deletes the character before the cursor
|
// InsertBackspace deletes the character before the cursor
|
||||||
type InsertBackspace struct{}
|
type InsertBackspace struct{}
|
||||||
|
|
||||||
|
// InsertBackspace.Execute: Deletes the character before the cursor (Backspace key).
|
||||||
func (a InsertBackspace) Execute(m Model) tea.Cmd {
|
func (a InsertBackspace) Execute(m Model) tea.Cmd {
|
||||||
x, y := m.CursorX(), m.CursorY()
|
win := m.ActiveWindow()
|
||||||
l := m.Line(y)
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
x, y := win.Cursor.Col, win.Cursor.Line
|
||||||
|
l := buf.Lines[y]
|
||||||
if x > 0 {
|
if x > 0 {
|
||||||
m.SetLine(y, l[:x-1]+l[x:])
|
buf.SetLine(y, l[:x-1]+l[x:])
|
||||||
m.SetCursorX(x - 1)
|
win.SetCursorCol(x - 1)
|
||||||
} else if y > 0 {
|
} else if y > 0 {
|
||||||
prevLine := m.Line(y - 1)
|
prevLine := buf.Lines[y-1]
|
||||||
newX := len(prevLine)
|
newX := len(prevLine)
|
||||||
m.SetLine(y-1, prevLine+l)
|
buf.SetLine(y-1, prevLine+l)
|
||||||
m.DeleteLine(y)
|
buf.DeleteLine(y)
|
||||||
m.SetCursorY(y - 1)
|
win.SetCursorPos(y-1, newX)
|
||||||
m.SetCursorX(newX)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -185,15 +214,19 @@ func (a InsertBackspace) Execute(m Model) tea.Cmd {
|
|||||||
// InsertDelete deletes the character under/after the cursor (delete key)
|
// InsertDelete deletes the character under/after the cursor (delete key)
|
||||||
type InsertDelete struct{}
|
type InsertDelete struct{}
|
||||||
|
|
||||||
|
// InsertDelete.Execute: Deletes the character at the cursor position (Delete key).
|
||||||
func (a InsertDelete) Execute(m Model) tea.Cmd {
|
func (a InsertDelete) Execute(m Model) tea.Cmd {
|
||||||
x, y := m.CursorX(), m.CursorY()
|
win := m.ActiveWindow()
|
||||||
l := m.Line(y)
|
buf := m.ActiveBuffer()
|
||||||
if x == len(l) && y < m.LineCount()-1 {
|
|
||||||
nextLine := m.Line(y + 1)
|
x, y := win.Cursor.Col, win.Cursor.Line
|
||||||
m.SetLine(y, l+nextLine)
|
l := buf.Lines[y]
|
||||||
m.DeleteLine(y + 1)
|
if x == len(l) && y < buf.LineCount()-1 {
|
||||||
|
nextLine := buf.Lines[y+1]
|
||||||
|
buf.SetLine(y, l+nextLine)
|
||||||
|
buf.DeleteLine(y + 1)
|
||||||
} else if x < len(l) {
|
} else if x < len(l) {
|
||||||
m.SetLine(y, l[:x]+l[x+1:])
|
buf.SetLine(y, l[:x]+l[x+1:])
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -201,22 +234,28 @@ func (a InsertDelete) Execute(m Model) tea.Cmd {
|
|||||||
// InsertTab inserts spaces equal to the tab size
|
// InsertTab inserts spaces equal to the tab size
|
||||||
type InsertTab struct{}
|
type InsertTab struct{}
|
||||||
|
|
||||||
|
// InsertTab.Execute: Inserts spaces equal to the tab size (Tab key).
|
||||||
func (a InsertTab) Execute(m Model) tea.Cmd {
|
func (a InsertTab) Execute(m Model) tea.Cmd {
|
||||||
x, y := m.CursorX(), m.CursorY()
|
win := m.ActiveWindow()
|
||||||
l := m.Line(y)
|
buf := m.ActiveBuffer()
|
||||||
tabs := strings.Repeat(" ", m.Settings().TabSize)
|
|
||||||
|
x, y := win.Cursor.Col, win.Cursor.Line
|
||||||
|
l := buf.Lines[y]
|
||||||
|
tabs := strings.Repeat(" ", m.Settings().TabStop)
|
||||||
if x < len(l) {
|
if x < len(l) {
|
||||||
m.SetLine(y, l[:x]+tabs+l[x:])
|
buf.SetLine(y, l[:x]+tabs+l[x:])
|
||||||
} else {
|
} else {
|
||||||
m.SetLine(y, l+tabs)
|
buf.SetLine(y, l+tabs)
|
||||||
}
|
}
|
||||||
m.SetCursorX(x + len(tabs))
|
win.SetCursorCol(x + len(tabs))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InsertDeletePreviousWord deletes the word before the cursor (ctrl+w)
|
// InsertDeletePreviousWord deletes the word before the cursor (ctrl+w)
|
||||||
type InsertDeletePreviousWord struct{}
|
type InsertDeletePreviousWord struct{}
|
||||||
|
|
||||||
|
// isWordChar: Returns true if the character is a word character (alphanumeric
|
||||||
|
// or underscore).
|
||||||
func isWordChar(c byte) bool {
|
func isWordChar(c byte) bool {
|
||||||
return (c >= 'a' && c <= 'z') ||
|
return (c >= 'a' && c <= 'z') ||
|
||||||
(c >= 'A' && c <= 'Z') ||
|
(c >= 'A' && c <= 'Z') ||
|
||||||
@ -224,23 +263,28 @@ func isWordChar(c byte) bool {
|
|||||||
c == '_'
|
c == '_'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isPunctuation: Returns true if the character is punctuation (not whitespace
|
||||||
|
// and not a word character).
|
||||||
func isPunctuation(c byte) bool {
|
func isPunctuation(c byte) bool {
|
||||||
return c != ' ' && c != '\t' && !isWordChar(c)
|
return c != ' ' && c != '\t' && !isWordChar(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InsertDeletePreviousWord.Execute: Deletes the word before the cursor (Ctrl+W).
|
||||||
func (a InsertDeletePreviousWord) Execute(m Model) tea.Cmd {
|
func (a InsertDeletePreviousWord) Execute(m Model) tea.Cmd {
|
||||||
x, y := m.CursorX(), m.CursorY()
|
win := m.ActiveWindow()
|
||||||
line := m.Line(y)
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
x, y := win.Cursor.Col, win.Cursor.Line
|
||||||
|
line := buf.Lines[y]
|
||||||
|
|
||||||
// At start of line: merge with previous line (same as backspace)
|
// At start of line: merge with previous line (same as backspace)
|
||||||
if x == 0 {
|
if x == 0 {
|
||||||
if y > 0 {
|
if y > 0 {
|
||||||
prevLine := m.Line(y - 1)
|
prevLine := buf.Lines[y-1]
|
||||||
newX := len(prevLine)
|
newX := len(prevLine)
|
||||||
m.SetLine(y-1, prevLine+line)
|
buf.SetLine(y-1, prevLine+line)
|
||||||
m.DeleteLine(y)
|
buf.DeleteLine(y)
|
||||||
m.SetCursorY(y - 1)
|
win.SetCursorPos(y-1, newX)
|
||||||
m.SetCursorX(newX)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -254,8 +298,8 @@ func (a InsertDeletePreviousWord) Execute(m Model) tea.Cmd {
|
|||||||
newX--
|
newX--
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetLine(y, line[:newX]+line[x:])
|
buf.SetLine(y, line[:newX]+line[x:])
|
||||||
m.SetCursorX(newX)
|
win.SetCursorCol(newX)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -271,8 +315,8 @@ func (a InsertDeletePreviousWord) Execute(m Model) tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete everything from newX up to x in one operation
|
// Delete everything from newX up to x in one operation
|
||||||
m.SetLine(y, line[:newX]+line[x:])
|
buf.SetLine(y, line[:newX]+line[x:])
|
||||||
m.SetCursorX(newX)
|
win.SetCursorCol(newX)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
85
internal/action/interface.go
Normal file
85
internal/action/interface.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Model defines the interface for editor state that actions can modify
|
||||||
|
type Model interface {
|
||||||
|
// ==================================================
|
||||||
|
// Core Data Access
|
||||||
|
// ==================================================
|
||||||
|
Windows() []*core.Window
|
||||||
|
ActiveWindow() *core.Window
|
||||||
|
Buffers() []*core.Buffer
|
||||||
|
SetBuffers(bufs []*core.Buffer)
|
||||||
|
ActiveBuffer() *core.Buffer
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// Insert Mode State
|
||||||
|
// ==================================================
|
||||||
|
InsertKeys() []string
|
||||||
|
SetInsertKeys(keys []string)
|
||||||
|
|
||||||
|
// Insert recording (for count replay)
|
||||||
|
SetInsertRecording(count int, action Action)
|
||||||
|
|
||||||
|
// ExitInsertMode handles replay, cursor step-back, and mode transition on esc
|
||||||
|
ExitInsertMode()
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// Command Mode State
|
||||||
|
// ==================================================
|
||||||
|
Command() string
|
||||||
|
SetCommand(cmd string)
|
||||||
|
CommandCursor() int
|
||||||
|
SetCommandCursor(cur int)
|
||||||
|
CommandError() error
|
||||||
|
SetCommandError(err error)
|
||||||
|
CommandOutput() string
|
||||||
|
SetCommandOutput(out string)
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// Editor-wide State
|
||||||
|
// ==================================================
|
||||||
|
Mode() core.Mode
|
||||||
|
SetMode(mode core.Mode)
|
||||||
|
|
||||||
|
Settings() core.EditorSettings
|
||||||
|
SetSettings(s core.EditorSettings)
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// Registers
|
||||||
|
// ==================================================
|
||||||
|
Registers() map[rune]core.Register
|
||||||
|
GetRegister(name rune) (core.Register, bool)
|
||||||
|
SetRegister(name rune, t core.RegisterType, cnt []string) error
|
||||||
|
UpdateDefaultRegister(t core.RegisterType, cnt []string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action is the base interface - anything executable
|
||||||
|
type Action interface {
|
||||||
|
Execute(m Model) tea.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Motion moves the cursor and returns the range covered
|
||||||
|
type Motion interface {
|
||||||
|
Action
|
||||||
|
Type() core.MotionType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operator acts on a range (delete, yank, change)
|
||||||
|
type Operator interface {
|
||||||
|
Operate(m Model, start, end core.Position, mtype core.MotionType) tea.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoublePresser is an optional interface for operators that support double-press (dd, yy, cc)
|
||||||
|
type DoublePresser interface {
|
||||||
|
DoublePress(m Model, count int) tea.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeatable actions track count
|
||||||
|
type Repeatable interface {
|
||||||
|
WithCount(n int) Action
|
||||||
|
}
|
||||||
@ -1,19 +1,24 @@
|
|||||||
package action
|
package action
|
||||||
|
|
||||||
import tea "github.com/charmbracelet/bubbletea"
|
import (
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
// Quit implements Action (ctrl+c)
|
// Quit implements Action (ctrl+c)
|
||||||
type Quit struct{}
|
type Quit struct{}
|
||||||
|
|
||||||
|
// Quit.Execute: Quits the editor (Ctrl+C).
|
||||||
func (a Quit) Execute(m Model) tea.Cmd {
|
func (a Quit) Execute(m Model) tea.Cmd {
|
||||||
return tea.Quit
|
return tea.Quit
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quit implements Action (:)
|
// EnterComandMode implements Action (:) - enters command mode.
|
||||||
type EnterComandMode struct{}
|
type EnterComandMode struct{}
|
||||||
|
|
||||||
|
// EnterComandMode.Execute: Enters command mode (: key).
|
||||||
func (a EnterComandMode) Execute(m Model) tea.Cmd {
|
func (a EnterComandMode) Execute(m Model) tea.Cmd {
|
||||||
m.SetMode(CommandMode)
|
m.SetMode(core.CommandMode)
|
||||||
m.SetCommand("")
|
m.SetCommand("")
|
||||||
m.SetCommandOutput("")
|
m.SetCommandOutput("")
|
||||||
m.SetCommandError(nil)
|
m.SetCommandError(nil)
|
||||||
@ -21,32 +26,38 @@ func (a EnterComandMode) Execute(m Model) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quit implements Action (v)
|
// EnterVisualMode implements Action (v) - enters visual character mode.
|
||||||
type EnterVisualMode struct{}
|
type EnterVisualMode struct{}
|
||||||
|
|
||||||
|
// EnterVisualMode.Execute: Enters visual character mode (v key).
|
||||||
func (a EnterVisualMode) Execute(m Model) tea.Cmd {
|
func (a EnterVisualMode) Execute(m Model) tea.Cmd {
|
||||||
m.SetAnchorX(m.CursorX())
|
win := m.ActiveWindow()
|
||||||
m.SetAnchorY(m.CursorY())
|
win.SetAnchorCol(win.Cursor.Col)
|
||||||
m.SetMode(VisualMode)
|
win.SetAnchorLine(win.Cursor.Line)
|
||||||
|
m.SetMode(core.VisualMode)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quit implements Action (V)
|
// EnterVisualLineMode implements Action (V) - enters visual line mode.
|
||||||
type EnterVisualLineMode struct{}
|
type EnterVisualLineMode struct{}
|
||||||
|
|
||||||
|
// EnterVisualLineMode.Execute: Enters visual line mode (V key).
|
||||||
func (a EnterVisualLineMode) Execute(m Model) tea.Cmd {
|
func (a EnterVisualLineMode) Execute(m Model) tea.Cmd {
|
||||||
m.SetAnchorX(m.CursorX())
|
win := m.ActiveWindow()
|
||||||
m.SetAnchorY(m.CursorY())
|
win.SetAnchorCol(win.Cursor.Col)
|
||||||
m.SetMode(VisualLineMode)
|
win.SetAnchorLine(win.Cursor.Line)
|
||||||
|
m.SetMode(core.VisualLineMode)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quit implements Action (ctrl+v)
|
// EnterVisualBlockMode implements Action (Ctrl+V) - enters visual block mode.
|
||||||
type EnterVisualBlockMode struct{}
|
type EnterVisualBlockMode struct{}
|
||||||
|
|
||||||
|
// EnterVisualBlockMode.Execute: Enters visual block mode (Ctrl+V).
|
||||||
func (a EnterVisualBlockMode) Execute(m Model) tea.Cmd {
|
func (a EnterVisualBlockMode) Execute(m Model) tea.Cmd {
|
||||||
m.SetAnchorX(m.CursorX())
|
win := m.ActiveWindow()
|
||||||
m.SetAnchorY(m.CursorY())
|
win.SetAnchorCol(win.Cursor.Col)
|
||||||
m.SetMode(VisualBlockMode)
|
win.SetAnchorLine(win.Cursor.Line)
|
||||||
|
m.SetMode(core.VisualBlockMode)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -12,7 +13,11 @@ type Paste struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Paste.Execute: Pastes register content after the cursor position (p key).
|
||||||
func (a Paste) Execute(m Model) tea.Cmd {
|
func (a Paste) Execute(m Model) tea.Cmd {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
// Get reg
|
// Get reg
|
||||||
reg, found := m.GetRegister('"')
|
reg, found := m.GetRegister('"')
|
||||||
if !found {
|
if !found {
|
||||||
@ -26,25 +31,25 @@ func (a Paste) Execute(m Model) tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch reg.Type {
|
switch reg.Type {
|
||||||
case LinewiseRegister:
|
case core.LinewiseRegister:
|
||||||
{
|
{
|
||||||
initY := m.CursorY()
|
initY := win.Cursor.Line
|
||||||
lines := reg.Content
|
lines := reg.Content
|
||||||
insertPos := initY + 1
|
insertPos := initY + 1
|
||||||
|
|
||||||
// Run count times
|
// Run count times
|
||||||
for range a.Count {
|
for range a.Count {
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
m.InsertLine(insertPos, line)
|
buf.InsertLine(insertPos, line)
|
||||||
insertPos++
|
insertPos++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.LineCount() > 1 {
|
if buf.LineCount() > 1 {
|
||||||
m.SetCursorY(initY + 1)
|
win.SetCursorLine(initY + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case CharwiseRegister:
|
case core.CharwiseRegister:
|
||||||
{
|
{
|
||||||
lines := reg.Content
|
lines := reg.Content
|
||||||
|
|
||||||
@ -54,22 +59,21 @@ func (a Paste) Execute(m Model) tea.Cmd {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
x := m.CursorX()
|
x := win.Cursor.Col
|
||||||
y := m.CursorY()
|
y := win.Cursor.Line
|
||||||
|
|
||||||
cnt := strings.Repeat(lines[0], max(1, a.Count))
|
cnt := strings.Repeat(lines[0], max(1, a.Count))
|
||||||
curLine := m.Line(y)
|
curLine := buf.Lines[y]
|
||||||
|
|
||||||
// Catch edge cases, end of line, start of blank line
|
// Catch edge cases, end of line, start of blank line
|
||||||
insertAt := min(x+1, len(curLine))
|
insertAt := min(x+1, len(curLine))
|
||||||
newLine := curLine[:insertAt] + cnt + curLine[insertAt:]
|
newLine := curLine[:insertAt] + cnt + curLine[insertAt:]
|
||||||
m.SetLine(y, newLine)
|
buf.SetLine(y, newLine)
|
||||||
|
|
||||||
m.SetCursorX(x + len(cnt))
|
win.SetCursorCol(x + len(cnt))
|
||||||
m.ClampCursorX()
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
m.SetCommandError(fmt.Errorf("Register type is not implemented."))
|
m.SetCommandError(fmt.Errorf("core.Register type is not implemented."))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -78,6 +82,7 @@ func (a Paste) Execute(m Model) tea.Cmd {
|
|||||||
// Ensure Paste implements Repeatable
|
// Ensure Paste implements Repeatable
|
||||||
var _ Repeatable = Paste{}
|
var _ Repeatable = Paste{}
|
||||||
|
|
||||||
|
// Paste.WithCount: Returns a new Paste with the given count.
|
||||||
func (a Paste) WithCount(n int) Action {
|
func (a Paste) WithCount(n int) Action {
|
||||||
return Paste{Count: n}
|
return Paste{Count: n}
|
||||||
}
|
}
|
||||||
@ -87,7 +92,11 @@ type PasteBefore struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PasteBefore.Execute: Pastes register content before the cursor position (P key).
|
||||||
func (a PasteBefore) Execute(m Model) tea.Cmd {
|
func (a PasteBefore) Execute(m Model) tea.Cmd {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
// Get reg
|
// Get reg
|
||||||
reg, found := m.GetRegister('"')
|
reg, found := m.GetRegister('"')
|
||||||
if !found {
|
if !found {
|
||||||
@ -96,21 +105,21 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch reg.Type {
|
switch reg.Type {
|
||||||
case LinewiseRegister:
|
case core.LinewiseRegister:
|
||||||
{
|
{
|
||||||
initY := m.CursorY()
|
initY := win.Cursor.Line
|
||||||
lines := reg.Content
|
lines := reg.Content
|
||||||
insertPos := initY // Leave here, this will effectively move the lines below
|
insertPos := initY // Leave here, this will effectively move the lines below
|
||||||
|
|
||||||
// Run count times
|
// Run count times
|
||||||
for range a.Count {
|
for range a.Count {
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
m.InsertLine(insertPos, line)
|
buf.InsertLine(insertPos, line)
|
||||||
insertPos++
|
insertPos++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case CharwiseRegister:
|
case core.CharwiseRegister:
|
||||||
{
|
{
|
||||||
lines := reg.Content
|
lines := reg.Content
|
||||||
|
|
||||||
@ -120,22 +129,21 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
x := m.CursorX()
|
x := win.Cursor.Col
|
||||||
y := m.CursorY()
|
y := win.Cursor.Line
|
||||||
|
|
||||||
cnt := strings.Repeat(lines[0], max(1, a.Count))
|
cnt := strings.Repeat(lines[0], max(1, a.Count))
|
||||||
curLine := m.Line(y)
|
curLine := buf.Lines[y]
|
||||||
|
|
||||||
// Catch edge cases, end of line, start of blank line
|
// Catch edge cases, end of line, start of blank line
|
||||||
insertAt := min(x, len(curLine))
|
insertAt := min(x, len(curLine))
|
||||||
newLine := curLine[:insertAt] + cnt + curLine[insertAt:]
|
newLine := curLine[:insertAt] + cnt + curLine[insertAt:]
|
||||||
m.SetLine(y, newLine)
|
buf.SetLine(y, newLine)
|
||||||
|
|
||||||
m.SetCursorX(x + len(cnt))
|
win.SetCursorCol(x + len(cnt))
|
||||||
m.ClampCursorX()
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
m.SetCommandError(fmt.Errorf("Register type is not implemented."))
|
m.SetCommandError(fmt.Errorf("core.Register type is not implemented."))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -144,6 +152,7 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
|
|||||||
// Ensure PasteBefore implements Repeatable
|
// Ensure PasteBefore implements Repeatable
|
||||||
var _ Repeatable = PasteBefore{}
|
var _ Repeatable = PasteBefore{}
|
||||||
|
|
||||||
|
// PasteBefore.WithCount: Returns a new PasteBefore with the given count.
|
||||||
func (a PasteBefore) WithCount(n int) Action {
|
func (a PasteBefore) WithCount(n int) Action {
|
||||||
return PasteBefore{Count: n}
|
return PasteBefore{Count: n}
|
||||||
}
|
}
|
||||||
@ -153,6 +162,7 @@ type VisualPaste struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VisualPaste.Execute: Replaces visual selection with register content (p in visual mode).
|
||||||
func (a VisualPaste) Execute(m Model) tea.Cmd {
|
func (a VisualPaste) Execute(m Model) tea.Cmd {
|
||||||
// Get register content to paste
|
// Get register content to paste
|
||||||
reg, found := m.GetRegister('"')
|
reg, found := m.GetRegister('"')
|
||||||
@ -167,27 +177,26 @@ func (a VisualPaste) Execute(m Model) tea.Cmd {
|
|||||||
start, end := normalizeSelection(m)
|
start, end := normalizeSelection(m)
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
case VisualMode:
|
case core.VisualMode:
|
||||||
visualCharPaste(m, reg, start, end)
|
visualCharPaste(m, reg, start, end)
|
||||||
case VisualBlockMode:
|
case core.VisualBlockMode:
|
||||||
visualBlockPaste(m, reg, start, end)
|
visualBlockPaste(m, reg, start, end)
|
||||||
case VisualLineMode:
|
case core.VisualLineMode:
|
||||||
visualLinePaste(m, reg, start, end)
|
visualLinePaste(m, reg, start, end)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit visual mode
|
// Exit visual mode
|
||||||
m.SetMode(NormalMode)
|
m.SetMode(core.NormalMode)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizeSelection returns start and end positions with start always before end
|
// normalizeSelection: Returns start and end positions with start always before end.
|
||||||
func normalizeSelection(m Model) (Position, Position) {
|
func normalizeSelection(m Model) (core.Position, core.Position) {
|
||||||
anchorX, anchorY := m.AnchorX(), m.AnchorY()
|
win := m.ActiveWindow()
|
||||||
cursorX, cursorY := m.CursorX(), m.CursorY()
|
|
||||||
|
|
||||||
start := Position{Line: anchorY, Col: anchorX}
|
start := core.Position{Line: win.Anchor.Line, Col: win.Anchor.Col}
|
||||||
end := Position{Line: cursorY, Col: cursorX}
|
end := core.Position{Line: win.Cursor.Line, Col: win.Cursor.Col}
|
||||||
|
|
||||||
// Normalize so start is always before end
|
// Normalize so start is always before end
|
||||||
if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
|
if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
|
||||||
@ -197,8 +206,11 @@ func normalizeSelection(m Model) (Position, Position) {
|
|||||||
return start, end
|
return start, end
|
||||||
}
|
}
|
||||||
|
|
||||||
// visualCharPaste handles paste in visual (character) mode
|
// visualCharPaste: Handles paste operation in visual (character) mode.
|
||||||
func visualCharPaste(m Model, reg Register, start, end Position) {
|
func visualCharPaste(m Model, reg core.Register, start, end core.Position) {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
// First, extract the text that will be deleted (to save to register)
|
// First, extract the text that will be deleted (to save to register)
|
||||||
deletedText := extractCharSelection(m, start, end)
|
deletedText := extractCharSelection(m, start, end)
|
||||||
|
|
||||||
@ -208,56 +220,57 @@ func visualCharPaste(m Model, reg Register, start, end Position) {
|
|||||||
// Insert the register content at start position
|
// Insert the register content at start position
|
||||||
if len(reg.Content) == 0 {
|
if len(reg.Content) == 0 {
|
||||||
// Empty register - just delete (already done)
|
// Empty register - just delete (already done)
|
||||||
} else if reg.Type == CharwiseRegister {
|
} else if reg.Type == core.CharwiseRegister {
|
||||||
// Charwise paste: insert text at cursor position
|
// Charwise paste: insert text at cursor position
|
||||||
if len(reg.Content) == 1 {
|
if len(reg.Content) == 1 {
|
||||||
line := m.Line(start.Line)
|
line := buf.Lines[start.Line]
|
||||||
insertAt := min(start.Col, len(line))
|
insertAt := min(start.Col, len(line))
|
||||||
newLine := line[:insertAt] + reg.Content[0] + line[insertAt:]
|
newLine := line[:insertAt] + reg.Content[0] + line[insertAt:]
|
||||||
m.SetLine(start.Line, newLine)
|
buf.SetLine(start.Line, newLine)
|
||||||
|
|
||||||
// Cursor at end of pasted text
|
// Cursor at end of pasted text
|
||||||
m.SetCursorX(insertAt + len(reg.Content[0]) - 1)
|
win.SetCursorCol(insertAt + len(reg.Content[0]) - 1)
|
||||||
m.SetCursorY(start.Line)
|
win.SetCursorLine(start.Line)
|
||||||
}
|
}
|
||||||
} else if reg.Type == LinewiseRegister {
|
} else if reg.Type == core.LinewiseRegister {
|
||||||
// Linewise paste in visual char mode: replace selection with lines
|
// Linewise paste in visual char mode: replace selection with lines
|
||||||
// Insert each line from register
|
// Insert each line from register
|
||||||
for i, content := range reg.Content {
|
for i, content := range reg.Content {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
// First line: insert at start position
|
// First line: insert at start position
|
||||||
line := m.Line(start.Line)
|
line := buf.Lines[start.Line]
|
||||||
insertAt := min(start.Col, len(line))
|
insertAt := min(start.Col, len(line))
|
||||||
newLine := line[:insertAt] + content
|
newLine := line[:insertAt] + content
|
||||||
if len(reg.Content) == 1 {
|
if len(reg.Content) == 1 {
|
||||||
// Single line register - append rest of line
|
// Single line register - append rest of line
|
||||||
newLine += line[insertAt:]
|
newLine += line[insertAt:]
|
||||||
}
|
}
|
||||||
m.SetLine(start.Line, newLine)
|
buf.SetLine(start.Line, newLine)
|
||||||
} else {
|
} else {
|
||||||
// Subsequent lines: insert new lines
|
// Subsequent lines: insert new lines
|
||||||
m.InsertLine(start.Line+i, content)
|
buf.InsertLine(start.Line+i, content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.SetCursorY(start.Line)
|
win.SetCursorLine(start.Line)
|
||||||
m.SetCursorX(start.Col)
|
win.SetCursorCol(start.Col)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.ClampCursorX()
|
|
||||||
|
|
||||||
// Update register with deleted text
|
// Update register with deleted text
|
||||||
m.UpdateDefaultRegister(CharwiseRegister, []string{deletedText})
|
m.UpdateDefaultRegister(core.CharwiseRegister, []string{deletedText})
|
||||||
}
|
}
|
||||||
|
|
||||||
// visualBlockPaste handles paste in visual block mode
|
// visualBlockPaste: Handles paste operation in visual block mode.
|
||||||
func visualBlockPaste(m Model, reg Register, start, end Position) {
|
func visualBlockPaste(m Model, reg core.Register, start, end core.Position) {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
startCol := min(start.Col, end.Col)
|
startCol := min(start.Col, end.Col)
|
||||||
endCol := max(start.Col, end.Col)
|
endCol := max(start.Col, end.Col)
|
||||||
|
|
||||||
// Extract deleted text (for register)
|
// Extract deleted text (for register)
|
||||||
var deletedLines []string
|
var deletedLines []string
|
||||||
for y := start.Line; y <= end.Line; y++ {
|
for y := start.Line; y <= end.Line; y++ {
|
||||||
line := m.Line(y)
|
line := buf.Lines[y]
|
||||||
if startCol < len(line) {
|
if startCol < len(line) {
|
||||||
ec := min(endCol+1, len(line))
|
ec := min(endCol+1, len(line))
|
||||||
deletedLines = append(deletedLines, line[startCol:ec])
|
deletedLines = append(deletedLines, line[startCol:ec])
|
||||||
@ -268,94 +281,97 @@ func visualBlockPaste(m Model, reg Register, start, end Position) {
|
|||||||
|
|
||||||
// Delete the block selection
|
// Delete the block selection
|
||||||
for y := start.Line; y <= end.Line; y++ {
|
for y := start.Line; y <= end.Line; y++ {
|
||||||
line := m.Line(y)
|
line := buf.Lines[y]
|
||||||
if startCol >= len(line) {
|
if startCol >= len(line) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ec := min(endCol+1, len(line))
|
ec := min(endCol+1, len(line))
|
||||||
m.SetLine(y, line[:startCol]+line[ec:])
|
buf.SetLine(y, line[:startCol]+line[ec:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert register content
|
// Insert register content
|
||||||
if len(reg.Content) > 0 {
|
if len(reg.Content) > 0 {
|
||||||
pasteContent := reg.Content[0]
|
pasteContent := reg.Content[0]
|
||||||
if reg.Type == LinewiseRegister && len(reg.Content) > 0 {
|
if reg.Type == core.LinewiseRegister && len(reg.Content) > 0 {
|
||||||
pasteContent = reg.Content[0]
|
pasteContent = reg.Content[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
for y := start.Line; y <= end.Line; y++ {
|
for y := start.Line; y <= end.Line; y++ {
|
||||||
line := m.Line(y)
|
line := buf.Lines[y]
|
||||||
insertAt := min(startCol, len(line))
|
insertAt := min(startCol, len(line))
|
||||||
// Pad with spaces if needed
|
// Pad with spaces if needed
|
||||||
for len(line) < insertAt {
|
for len(line) < insertAt {
|
||||||
line += " "
|
line += " "
|
||||||
}
|
}
|
||||||
newLine := line[:insertAt] + pasteContent + line[insertAt:]
|
newLine := line[:insertAt] + pasteContent + line[insertAt:]
|
||||||
m.SetLine(y, newLine)
|
buf.SetLine(y, newLine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetCursorY(start.Line)
|
win.SetCursorLine(start.Line)
|
||||||
m.SetCursorX(startCol)
|
win.SetCursorCol(startCol)
|
||||||
m.ClampCursorX()
|
|
||||||
|
|
||||||
// Update register with deleted block text (joined)
|
// Update register with deleted block text (joined)
|
||||||
m.UpdateDefaultRegister(CharwiseRegister, []string{strings.Join(deletedLines, "\n")})
|
m.UpdateDefaultRegister(core.CharwiseRegister, []string{strings.Join(deletedLines, "\n")})
|
||||||
}
|
}
|
||||||
|
|
||||||
// visualLinePaste handles paste in visual line mode
|
// visualLinePaste: Handles paste operation in visual line mode.
|
||||||
func visualLinePaste(m Model, reg Register, start, end Position) {
|
func visualLinePaste(m Model, reg core.Register, start, end core.Position) {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
// Extract deleted lines (for register)
|
// Extract deleted lines (for register)
|
||||||
var deletedLines []string
|
var deletedLines []string
|
||||||
for y := start.Line; y <= end.Line; y++ {
|
for y := start.Line; y <= end.Line; y++ {
|
||||||
deletedLines = append(deletedLines, m.Line(y))
|
deletedLines = append(deletedLines, buf.Lines[y])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the selected lines (from end to start to preserve indices)
|
// Delete the selected lines (from end to start to preserve indices)
|
||||||
for y := end.Line; y >= start.Line; y-- {
|
for y := end.Line; y >= start.Line; y-- {
|
||||||
m.DeleteLine(y)
|
buf.DeleteLine(y)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert register content
|
// Insert register content
|
||||||
if len(reg.Content) == 0 {
|
if len(reg.Content) == 0 {
|
||||||
// Empty register - ensure at least one empty line exists
|
// Empty register - ensure at least one empty line exists
|
||||||
if m.LineCount() == 0 {
|
if buf.LineCount() == 0 {
|
||||||
m.InsertLine(0, "")
|
buf.InsertLine(0, "")
|
||||||
}
|
}
|
||||||
} else if reg.Type == LinewiseRegister {
|
} else if reg.Type == core.LinewiseRegister {
|
||||||
// Linewise register: insert each line
|
// Linewise register: insert each line
|
||||||
insertPos := start.Line
|
insertPos := start.Line
|
||||||
for _, content := range reg.Content {
|
for _, content := range reg.Content {
|
||||||
m.InsertLine(insertPos, content)
|
buf.InsertLine(insertPos, content)
|
||||||
insertPos++
|
insertPos++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Charwise register: insert as a single line
|
// Charwise register: insert as a single line
|
||||||
m.InsertLine(start.Line, reg.Content[0])
|
buf.InsertLine(start.Line, reg.Content[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we have at least one line
|
// Ensure we have at least one line
|
||||||
if m.LineCount() == 0 {
|
if buf.LineCount() == 0 {
|
||||||
m.InsertLine(0, "")
|
buf.InsertLine(0, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position cursor at start of pasted content
|
// core.Position cursor at start of pasted content
|
||||||
y := start.Line
|
y := start.Line
|
||||||
if y >= m.LineCount() {
|
if y >= buf.LineCount() {
|
||||||
y = m.LineCount() - 1
|
y = buf.LineCount() - 1
|
||||||
}
|
}
|
||||||
m.SetCursorY(y)
|
win.SetCursorLine(y)
|
||||||
m.SetCursorX(0)
|
win.SetCursorCol(0)
|
||||||
m.ClampCursorX()
|
|
||||||
|
|
||||||
// Update register with deleted lines
|
// Update register with deleted lines
|
||||||
m.UpdateDefaultRegister(LinewiseRegister, deletedLines)
|
m.UpdateDefaultRegister(core.LinewiseRegister, deletedLines)
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractCharSelection extracts text from a character selection
|
// extractCharSelection: Extracts text from a character selection range.
|
||||||
func extractCharSelection(m Model, start, end Position) string {
|
func extractCharSelection(m Model, start, end core.Position) string {
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
if start.Line == end.Line {
|
if start.Line == end.Line {
|
||||||
line := m.Line(start.Line)
|
line := buf.Lines[start.Line]
|
||||||
endCol := min(end.Col+1, len(line))
|
endCol := min(end.Col+1, len(line))
|
||||||
startCol := min(start.Col, len(line))
|
startCol := min(start.Col, len(line))
|
||||||
if startCol >= endCol {
|
if startCol >= endCol {
|
||||||
@ -368,7 +384,7 @@ func extractCharSelection(m Model, start, end Position) string {
|
|||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
|
|
||||||
// First line: from start.Col to end
|
// First line: from start.Col to end
|
||||||
firstLine := m.Line(start.Line)
|
firstLine := buf.Lines[start.Line]
|
||||||
if start.Col < len(firstLine) {
|
if start.Col < len(firstLine) {
|
||||||
result.WriteString(firstLine[start.Col:])
|
result.WriteString(firstLine[start.Col:])
|
||||||
}
|
}
|
||||||
@ -376,27 +392,30 @@ func extractCharSelection(m Model, start, end Position) string {
|
|||||||
|
|
||||||
// Middle lines: entire lines
|
// Middle lines: entire lines
|
||||||
for y := start.Line + 1; y < end.Line; y++ {
|
for y := start.Line + 1; y < end.Line; y++ {
|
||||||
result.WriteString(m.Line(y))
|
result.WriteString(buf.Lines[y])
|
||||||
result.WriteString("\n")
|
result.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last line: from beginning to end.Col
|
// Last line: from beginning to end.Col
|
||||||
lastLine := m.Line(end.Line)
|
lastLine := buf.Lines[end.Line]
|
||||||
endCol := min(end.Col+1, len(lastLine))
|
endCol := min(end.Col+1, len(lastLine))
|
||||||
result.WriteString(lastLine[:endCol])
|
result.WriteString(lastLine[:endCol])
|
||||||
|
|
||||||
return result.String()
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteCharSelectionForPaste deletes a character selection (similar to operator/delete.go)
|
// deleteCharSelectionForPaste: Deletes a character selection for paste operations.
|
||||||
func deleteCharSelectionForPaste(m Model, start, end Position) {
|
func deleteCharSelectionForPaste(m Model, start, end core.Position) {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
if start.Line == end.Line {
|
if start.Line == end.Line {
|
||||||
line := m.Line(start.Line)
|
line := buf.Lines[start.Line]
|
||||||
endCol := min(end.Col+1, len(line))
|
endCol := min(end.Col+1, len(line))
|
||||||
m.SetLine(start.Line, line[:start.Col]+line[endCol:])
|
buf.SetLine(start.Line, line[:start.Col]+line[endCol:])
|
||||||
} else {
|
} else {
|
||||||
startLine := m.Line(start.Line)
|
startLine := buf.Lines[start.Line]
|
||||||
endLine := m.Line(end.Line)
|
endLine := buf.Lines[end.Line]
|
||||||
|
|
||||||
prefix := ""
|
prefix := ""
|
||||||
if start.Col < len(startLine) {
|
if start.Col < len(startLine) {
|
||||||
@ -410,18 +429,19 @@ func deleteCharSelectionForPaste(m Model, start, end Position) {
|
|||||||
|
|
||||||
// Delete from end back to start to preserve indices
|
// Delete from end back to start to preserve indices
|
||||||
for i := end.Line; i >= start.Line; i-- {
|
for i := end.Line; i >= start.Line; i-- {
|
||||||
m.DeleteLine(i)
|
buf.DeleteLine(i)
|
||||||
}
|
}
|
||||||
m.InsertLine(start.Line, prefix+suffix)
|
buf.InsertLine(start.Line, prefix+suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetCursorY(start.Line)
|
win.SetCursorLine(start.Line)
|
||||||
m.SetCursorX(start.Col)
|
win.SetCursorCol(start.Col)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure VisualPaste implements Repeatable
|
// Ensure VisualPaste implements Repeatable
|
||||||
var _ Repeatable = VisualPaste{}
|
var _ Repeatable = VisualPaste{}
|
||||||
|
|
||||||
|
// VisualPaste.WithCount: Returns a new VisualPaste with the given count.
|
||||||
func (a VisualPaste) WithCount(n int) Action {
|
func (a VisualPaste) WithCount(n int) Action {
|
||||||
return VisualPaste{Count: n}
|
return VisualPaste{Count: n}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
package action
|
|
||||||
|
|
||||||
type RegisterType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
CharwiseRegister RegisterType = iota
|
|
||||||
LinewiseRegister
|
|
||||||
BlockwiseRegister
|
|
||||||
)
|
|
||||||
|
|
||||||
type Register struct {
|
|
||||||
Type RegisterType
|
|
||||||
Content []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultRegisters() map[rune]Register {
|
|
||||||
reg := make(map[rune]Register)
|
|
||||||
|
|
||||||
addSpecialRegisters(reg)
|
|
||||||
addNamedRegisters(reg)
|
|
||||||
addNumberedRegisters(reg)
|
|
||||||
|
|
||||||
return reg
|
|
||||||
}
|
|
||||||
|
|
||||||
func addNamedRegisters(reg map[rune]Register) {
|
|
||||||
name := 'a'
|
|
||||||
|
|
||||||
for name <= 'z' {
|
|
||||||
reg[name] = emptyRegister()
|
|
||||||
name++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addNumberedRegisters(reg map[rune]Register) {
|
|
||||||
name := '0'
|
|
||||||
|
|
||||||
for name <= '9' {
|
|
||||||
reg[name] = emptyRegister()
|
|
||||||
name++
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func addSpecialRegisters(reg map[rune]Register) {
|
|
||||||
// Unnamed (default)
|
|
||||||
reg['"'] = emptyRegister()
|
|
||||||
|
|
||||||
// Black hole (readonly)
|
|
||||||
reg['_'] = emptyRegister()
|
|
||||||
|
|
||||||
// System clipboard
|
|
||||||
reg['*'] = emptyRegister()
|
|
||||||
|
|
||||||
// Small delete? Expression?
|
|
||||||
|
|
||||||
// Last inserted text (readonly)
|
|
||||||
reg['.'] = emptyRegister()
|
|
||||||
|
|
||||||
// Current file name (readonly)
|
|
||||||
reg['%'] = emptyRegister()
|
|
||||||
|
|
||||||
// Last executed command (readonly)
|
|
||||||
reg[':'] = emptyRegister()
|
|
||||||
|
|
||||||
// Alternate (previous) file (readonly)
|
|
||||||
reg['#'] = emptyRegister()
|
|
||||||
}
|
|
||||||
|
|
||||||
func emptyRegister() Register {
|
|
||||||
return Register{
|
|
||||||
Type: CharwiseRegister,
|
|
||||||
Content: []string{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package action
|
|
||||||
|
|
||||||
type Settings struct {
|
|
||||||
Number bool
|
|
||||||
RelativeNumber bool
|
|
||||||
GutterSize int
|
|
||||||
TabSize int
|
|
||||||
ScrollOff int
|
|
||||||
// TODO: Colors
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDefaultSettings() Settings {
|
|
||||||
return Settings{
|
|
||||||
Number: true,
|
|
||||||
RelativeNumber: true,
|
|
||||||
GutterSize: 5,
|
|
||||||
TabSize: 2,
|
|
||||||
ScrollOff: 8,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
|
|
||||||
hello
|
|
||||||
@ -1,60 +1,222 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
// QuitMsg signals the application should quit
|
// QuitMsg: Message signaling the application should quit.
|
||||||
type QuitMsg struct{}
|
type QuitMsg struct{}
|
||||||
|
|
||||||
// ErrorMsg signals an error to display
|
// ErrorMsg: Message signaling an error to display.
|
||||||
type ErrorMsg struct {
|
type ErrorMsg struct {
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdQuit handles :quit / :q
|
// --------------------------------------------------
|
||||||
func cmdQuit(m action.Model, args []string) tea.Cmd {
|
// Quit Commands
|
||||||
return func() tea.Msg {
|
// --------------------------------------------------
|
||||||
return tea.Quit()
|
|
||||||
|
// cmdQuit: Handles :quit / :q command.
|
||||||
|
func cmdQuit(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
|
// :q! forces quit, ignoring unsaved changes
|
||||||
|
if force {
|
||||||
|
return tea.Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
bufs := m.Buffers()
|
||||||
|
|
||||||
|
// Cannot exit if any buffer has unsaved changes
|
||||||
|
for _, buf := range bufs {
|
||||||
|
if buf.Modified {
|
||||||
|
m.SetCommandError(fmt.Errorf("unsaved changes to '%s'", buf.Filename))
|
||||||
|
m.ActiveWindow().SetBuffer(buf)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdQuitAll handles :qall / :qa
|
return tea.Quit
|
||||||
func cmdQuitAll(m action.Model, args []string) tea.Cmd {
|
}
|
||||||
return func() tea.Msg {
|
|
||||||
return tea.Quit()
|
// cmdQuitAll: Handles :qall / :qa command.
|
||||||
|
func cmdQuitAll(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
|
// TODO: Until splits are implemented, this is the same as cmdQuit
|
||||||
|
return cmdQuit(m, args, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// File Commands (write & edit)
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
// cmdWrite: Handles :write / :w command
|
||||||
|
func cmdWrite(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
cmd, err := writeBuffer(m, buf, args, force)
|
||||||
|
if err != nil {
|
||||||
|
m.SetCommandError(err)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdWriteAll: Handles :wall / :wa command
|
||||||
|
func cmdWriteAll(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
|
bufs := m.Buffers()
|
||||||
|
for _, buf := range bufs {
|
||||||
|
if buf.Modified {
|
||||||
|
cmd, err := writeBuffer(m, buf, args, force)
|
||||||
|
if err != nil {
|
||||||
|
m.SetCommandError(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdWrite handles :write / :w
|
return tea.Batch(cmds...)
|
||||||
func cmdWrite(m action.Model, args []string) tea.Cmd {
|
}
|
||||||
// TODO: Implement file saving
|
|
||||||
// If args provided, save to that filename
|
// cmdWriteQuit: Handles :wq command
|
||||||
// Otherwise save to current file
|
func cmdWriteQuit(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
cmd, err := writeBuffer(m, buf, args, force)
|
||||||
|
if err != nil {
|
||||||
|
m.SetCommandError(err)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
return tea.Batch(cmd, tea.Quit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdWriteQuitAll: Handles :wqall / :wqa / :xa command.
|
||||||
|
// Writes all modified buffers then quits.
|
||||||
|
func cmdWriteQuitAll(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
|
bufs := m.Buffers()
|
||||||
|
for _, buf := range bufs {
|
||||||
|
if buf.Modified {
|
||||||
|
cmd, err := writeBuffer(m, buf, args, force)
|
||||||
|
if err != nil {
|
||||||
|
m.SetCommandError(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmds = append(cmds, tea.Quit)
|
||||||
|
return tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdEdit: Handles :edit / :e
|
||||||
|
func cmdEdit(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
|
// must have arguments, cant edit nothing
|
||||||
|
if len(args) < 1 {
|
||||||
|
m.SetCommandError(fmt.Errorf(":edit requires an argument"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdWriteAll handles :wall / :wa
|
// Vim's Approach:
|
||||||
func cmdWriteAll(m action.Model, args []string) tea.Cmd {
|
// " When you do :edit filename.txt
|
||||||
// TODO: Implement saving all buffers
|
// 1. Check if file exists and is readable (if not, open new buffer)
|
||||||
|
// 2. Detect file encoding (UTF-8, etc.)
|
||||||
|
// 3. Read entire file into memory
|
||||||
|
// 4. Split by line endings (respecting fileformat)
|
||||||
|
// 5. Create new buffer with these lines
|
||||||
|
// 6. Set buffer metadata:
|
||||||
|
// - buftype = "" (normal file)
|
||||||
|
// - modified = 0 (not modified)
|
||||||
|
// - fileformat = "unix" | "dos" | "mac"
|
||||||
|
// - fileencoding = "utf-8" (etc.)
|
||||||
|
|
||||||
|
filename := args[0]
|
||||||
|
ext := filepath.Ext(filename)
|
||||||
|
|
||||||
|
// If the buffer already exists, just switch to it.
|
||||||
|
bufs := m.Buffers()
|
||||||
|
for _, buf := range bufs {
|
||||||
|
if buf.Filename == filename {
|
||||||
|
m.ActiveWindow().SetBuffer(buf)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
notFound := errors.Is(err, os.ErrNotExist)
|
||||||
|
|
||||||
|
if err != nil && !notFound {
|
||||||
|
m.SetCommandError(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if file != nil {
|
||||||
|
defer file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a buffer with the new file name, writing the file will
|
||||||
|
// handle the saving logic
|
||||||
|
if notFound {
|
||||||
|
buf := core.NewBufferBuilder().
|
||||||
|
WithType(core.FileBuffer).
|
||||||
|
WithFilename(filename).
|
||||||
|
WithFiletype(ext).
|
||||||
|
Listed().
|
||||||
|
Loaded().
|
||||||
|
Build()
|
||||||
|
|
||||||
|
m.SetBuffers(append(m.Buffers(), &buf))
|
||||||
|
m.ActiveWindow().SetBuffer(&buf)
|
||||||
|
|
||||||
|
// Need to adjust the cursor when we make a new file
|
||||||
|
m.ActiveWindow().ClampCursor()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdWriteQuit handles :wq
|
var lines []string
|
||||||
func cmdWriteQuit(m action.Model, args []string) tea.Cmd {
|
|
||||||
// TODO: Save then quit
|
// BUG: We are unable to open and edit files owned by root. How do we handle that?
|
||||||
return func() tea.Msg {
|
|
||||||
return tea.Quit()
|
scanner := bufio.NewScanner(file)
|
||||||
}
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
line = strings.TrimSuffix(line, "\r")
|
||||||
|
|
||||||
|
// BUG: This is bad, we don't want to this, but we have to
|
||||||
|
cleaned := strings.ReplaceAll(line, "\t", strings.Repeat(" ", m.Settings().TabStop))
|
||||||
|
lines = append(lines, cleaned)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdRegisters handles :register
|
buf := core.NewBufferBuilder().
|
||||||
func cmdRegisters(m action.Model, args []string) tea.Cmd {
|
WithType(core.FileBuffer).
|
||||||
|
WithFilename(filename).
|
||||||
|
WithFiletype(ext).
|
||||||
|
WithLines(lines).
|
||||||
|
Listed().
|
||||||
|
Loaded().
|
||||||
|
Build()
|
||||||
|
|
||||||
|
m.SetBuffers(append(m.Buffers(), &buf))
|
||||||
|
m.ActiveWindow().SetBuffer(&buf)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Register Commands
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
// cmdRegisters: Handles :register command (debug - displays register content).
|
||||||
|
func cmdRegisters(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
// TODO: This is temporary, for debugging
|
// TODO: This is temporary, for debugging
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
m.SetCommandError(fmt.Errorf("Please provide a name. Register dump not yet implemented."))
|
m.SetCommandError(fmt.Errorf("Please provide a name. Register dump not yet implemented."))
|
||||||
@ -79,7 +241,11 @@ func cmdRegisters(m action.Model, args []string) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdSet handles :set option[=value]
|
// --------------------------------------------------
|
||||||
|
// Settings Commands
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
// cmdSet: Handles :set option[=value] command for configuring editor settings.
|
||||||
// Examples:
|
// Examples:
|
||||||
//
|
//
|
||||||
// :set number - enable number
|
// :set number - enable number
|
||||||
@ -87,7 +253,7 @@ func cmdRegisters(m action.Model, args []string) tea.Cmd {
|
|||||||
// :set number! - toggle number
|
// :set number! - toggle number
|
||||||
// :set tabstop=4 - set tabstop to 4
|
// :set tabstop=4 - set tabstop to 4
|
||||||
// :set ts=4 - set tabstop to 4 (abbreviation)
|
// :set ts=4 - set tabstop to 4 (abbreviation)
|
||||||
func cmdSet(m action.Model, args []string) tea.Cmd {
|
func cmdSet(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
out := fmt.Sprintf("%+v", m.Settings())
|
out := fmt.Sprintf("%+v", m.Settings())
|
||||||
m.SetCommandOutput(out)
|
m.SetCommandOutput(out)
|
||||||
@ -104,15 +270,16 @@ func cmdSet(m action.Model, args []string) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setting represents a configurable option
|
// Setting: Represents a configurable editor option.
|
||||||
type Setting struct {
|
type Setting struct {
|
||||||
Name string
|
Name string
|
||||||
ShortForm string
|
ShortForm string
|
||||||
Type SettingType
|
Type SettingType
|
||||||
Get func(s action.Settings) any
|
Get func(m action.Model) any
|
||||||
Set func(m action.Model, val any)
|
Set func(m action.Model, val any)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SettingType: Enumeration of setting value types.
|
||||||
type SettingType int
|
type SettingType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -121,54 +288,72 @@ const (
|
|||||||
StringSetting
|
StringSetting
|
||||||
)
|
)
|
||||||
|
|
||||||
// settingsMap defines all available settings
|
// settingsMap defines all available settings (both global and window-local)
|
||||||
var settingsMap = []Setting{
|
var settingsMap = []Setting{
|
||||||
|
// Global editor settings
|
||||||
|
{
|
||||||
|
Name: "tabstop",
|
||||||
|
ShortForm: "ts",
|
||||||
|
Type: IntSetting,
|
||||||
|
Get: func(m action.Model) any { return m.Settings().TabStop },
|
||||||
|
Set: func(m action.Model, val any) {
|
||||||
|
s := m.Settings()
|
||||||
|
s.TabStop = val.(int)
|
||||||
|
m.SetSettings(s)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Window-local settings
|
||||||
{
|
{
|
||||||
Name: "number",
|
Name: "number",
|
||||||
ShortForm: "nu",
|
ShortForm: "nu",
|
||||||
Type: BoolSetting,
|
Type: BoolSetting,
|
||||||
Get: func(s action.Settings) any { return s.Number },
|
Get: func(m action.Model) any { return m.ActiveWindow().Options.Number },
|
||||||
Set: func(m action.Model, val any) {
|
Set: func(m action.Model, val any) {
|
||||||
s := m.Settings()
|
w := m.ActiveWindow()
|
||||||
s.Number = val.(bool)
|
o := w.Options
|
||||||
m.SetSettings(s)
|
o.Number = val.(bool)
|
||||||
|
w.SetOptions(o)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "relativenumber",
|
Name: "relativenumber",
|
||||||
ShortForm: "rnu",
|
ShortForm: "rnu",
|
||||||
Type: BoolSetting,
|
Type: BoolSetting,
|
||||||
Get: func(s action.Settings) any { return s.RelativeNumber },
|
Get: func(m action.Model) any { return m.ActiveWindow().Options.RelativeNumber },
|
||||||
Set: func(m action.Model, val any) {
|
Set: func(m action.Model, val any) {
|
||||||
s := m.Settings()
|
w := m.ActiveWindow()
|
||||||
s.RelativeNumber = val.(bool)
|
o := w.Options
|
||||||
m.SetSettings(s)
|
o.RelativeNumber = val.(bool)
|
||||||
},
|
w.SetOptions(o)
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "tabstop",
|
|
||||||
ShortForm: "ts",
|
|
||||||
Type: IntSetting,
|
|
||||||
Get: func(s action.Settings) any { return s.TabSize },
|
|
||||||
Set: func(m action.Model, val any) {
|
|
||||||
s := m.Settings()
|
|
||||||
s.TabSize = val.(int)
|
|
||||||
m.SetSettings(s)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "scrolloff",
|
Name: "scrolloff",
|
||||||
ShortForm: "so",
|
ShortForm: "so",
|
||||||
Type: IntSetting,
|
Type: IntSetting,
|
||||||
Get: func(s action.Settings) any { return s.ScrollOff },
|
Get: func(m action.Model) any { return m.ActiveWindow().Options.ScrollOff },
|
||||||
Set: func(m action.Model, val any) {
|
Set: func(m action.Model, val any) {
|
||||||
s := m.Settings()
|
w := m.ActiveWindow()
|
||||||
s.ScrollOff = val.(int)
|
o := w.Options
|
||||||
m.SetSettings(s)
|
o.ScrollOff = val.(int)
|
||||||
|
w.SetOptions(o)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "guttersize",
|
||||||
|
ShortForm: "gu",
|
||||||
|
Type: IntSetting,
|
||||||
|
Get: func(m action.Model) any { return m.ActiveWindow().Options.GutterSize },
|
||||||
|
Set: func(m action.Model, val any) {
|
||||||
|
w := m.ActiveWindow()
|
||||||
|
o := w.Options
|
||||||
|
o.GutterSize = val.(int)
|
||||||
|
w.SetOptions(o)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lookupSetting: Finds a setting by name, short form, or prefix.
|
||||||
func lookupSetting(name string) *Setting {
|
func lookupSetting(name string) *Setting {
|
||||||
for i := range settingsMap {
|
for i := range settingsMap {
|
||||||
s := &settingsMap[i]
|
s := &settingsMap[i]
|
||||||
@ -183,16 +368,13 @@ func lookupSetting(name string) *Setting {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseSetOption: Parses and applies a single :set option.
|
||||||
func parseSetOption(m action.Model, opt string) error {
|
func parseSetOption(m action.Model, opt string) error {
|
||||||
// Handle toggle: option!
|
// Handle toggle: option!
|
||||||
if name, ok := strings.CutSuffix(opt, "!"); ok {
|
if name, ok := strings.CutSuffix(opt, "!"); ok {
|
||||||
setting := lookupSetting(name)
|
setting := lookupSetting(name)
|
||||||
if setting == nil {
|
if setting != nil && setting.Type == BoolSetting {
|
||||||
return nil // Unknown setting
|
currentVal := setting.Get(m).(bool)
|
||||||
}
|
|
||||||
if setting.Type == BoolSetting {
|
|
||||||
// Toggle the boolean
|
|
||||||
currentVal := setting.Get(m.Settings()).(bool)
|
|
||||||
setting.Set(m, !currentVal)
|
setting.Set(m, !currentVal)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -201,10 +383,7 @@ func parseSetOption(m action.Model, opt string) error {
|
|||||||
// Handle disable: nooption
|
// Handle disable: nooption
|
||||||
if name, ok := strings.CutPrefix(opt, "no"); ok {
|
if name, ok := strings.CutPrefix(opt, "no"); ok {
|
||||||
setting := lookupSetting(name)
|
setting := lookupSetting(name)
|
||||||
if setting == nil {
|
if setting != nil && setting.Type == BoolSetting {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if setting.Type == BoolSetting {
|
|
||||||
setting.Set(m, false)
|
setting.Set(m, false)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -214,10 +393,9 @@ func parseSetOption(m action.Model, opt string) error {
|
|||||||
if strings.Contains(opt, "=") {
|
if strings.Contains(opt, "=") {
|
||||||
parts := strings.SplitN(opt, "=", 2)
|
parts := strings.SplitN(opt, "=", 2)
|
||||||
name, value := parts[0], parts[1]
|
name, value := parts[0], parts[1]
|
||||||
|
|
||||||
setting := lookupSetting(name)
|
setting := lookupSetting(name)
|
||||||
if setting == nil {
|
if setting != nil {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
switch setting.Type {
|
switch setting.Type {
|
||||||
case IntSetting:
|
case IntSetting:
|
||||||
intVal, err := strconv.Atoi(value)
|
intVal, err := strconv.Atoi(value)
|
||||||
@ -232,15 +410,13 @@ func parseSetOption(m action.Model, opt string) error {
|
|||||||
boolVal := value == "true" || value == "1" || value == "yes"
|
boolVal := value == "true" || value == "1" || value == "yes"
|
||||||
setting.Set(m, boolVal)
|
setting.Set(m, boolVal)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle enable: option (boolean only)
|
// Handle enable: option (boolean only)
|
||||||
setting := lookupSetting(opt)
|
setting := lookupSetting(opt)
|
||||||
if setting == nil {
|
if setting != nil && setting.Type == BoolSetting {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if setting.Type == BoolSetting {
|
|
||||||
setting.Set(m, true)
|
setting.Set(m, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3883
internal/command/handlers_test.go
Normal file
3883
internal/command/handlers_test.go
Normal file
File diff suppressed because it is too large
Load Diff
83
internal/command/io.go
Normal file
83
internal/command/io.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeBuffer(m action.Model, buf *core.Buffer, args []string, force bool) (tea.Cmd, error) {
|
||||||
|
// Key Steps:
|
||||||
|
// - Backup Creation (if backup is set): Copy original to .bak file
|
||||||
|
// - Line Ending Application: Apply fileformat setting (unix=\n, dos=\r\n, mac=\r)
|
||||||
|
// - Character Encoding: Convert from internal representation to fileencoding
|
||||||
|
// - Atomic Write: Write to temporary file first (.swp or similar)
|
||||||
|
// - Atomic Rename: Rename temp file to target filename (atomic operation)
|
||||||
|
// - Metadata Update: Clear modified flag, update timestamp
|
||||||
|
// Safety Mechanisms:
|
||||||
|
// " Vim's write safety
|
||||||
|
// 1. Check file permissions
|
||||||
|
// 2. If file exists and 'writebackup' set:
|
||||||
|
// - Create backup (.bak)
|
||||||
|
// 3. Write to temporary file (.swp~)
|
||||||
|
// 4. Verify write succeeded
|
||||||
|
// 5. Rename temp to target (atomic)
|
||||||
|
// 6. Remove backup if 'backup' not set
|
||||||
|
// 7. Update buffer metadata
|
||||||
|
|
||||||
|
// TODO: Implement atomic and safe writes
|
||||||
|
|
||||||
|
// Check readonly flag ONLY if not forced with !
|
||||||
|
if !force && buf.ReadOnly {
|
||||||
|
return nil, fmt.Errorf("cannot write to 'readonly' buffer")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !force && buf.Type == core.ScatchBuffer {
|
||||||
|
return nil, fmt.Errorf("cannot write to buffer of type 'ScratchBuffer'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the filename; differs by the type
|
||||||
|
var filename string
|
||||||
|
if len(args) > 0 {
|
||||||
|
filename = args[0]
|
||||||
|
} else {
|
||||||
|
if buf.Filename == "" {
|
||||||
|
return nil, fmt.Errorf("cannot write: no file name provided")
|
||||||
|
}
|
||||||
|
filename = buf.Filename
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0664)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Same write operation regardless of where the file came from
|
||||||
|
var bytes int
|
||||||
|
|
||||||
|
// Using a bufio.Writer because its more efficient
|
||||||
|
writer := bufio.NewWriter(file)
|
||||||
|
for _, line := range buf.Lines {
|
||||||
|
n, err := writer.WriteString(line + "\n")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes += n
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.Flush(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
output := fmt.Sprintf("'%s', %dL %db written", filename, buf.LineCount(), bytes)
|
||||||
|
m.SetCommandOutput(output)
|
||||||
|
buf.SetModified(false)
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
@ -8,32 +8,31 @@ import (
|
|||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Command represents a command that can be executed from command mode
|
// Command: Represents a command that can be executed from command mode.
|
||||||
type Command struct {
|
type Command struct {
|
||||||
Name string // Full name: "quit"
|
Name string // Full name: "quit"
|
||||||
ShortForm string // Minimum abbreviation: "q"
|
ShortForm string // Minimum abbreviation: "q"
|
||||||
Handler func(m action.Model, args []string) tea.Cmd // Handler function
|
Handler func(m action.Model, args []string, force bool) tea.Cmd // Handler function
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registry holds all registered commands
|
// Registry: Holds all registered commands.
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
commands []Command
|
commands []Command
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRegistry creates a new command registry with default commands
|
// NewRegistry: Creates a new command registry with default commands.
|
||||||
func NewRegistry() *Registry {
|
func NewRegistry() *Registry {
|
||||||
r := &Registry{}
|
r := &Registry{}
|
||||||
r.registerDefaults()
|
r.registerDefaults()
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register adds a command to the registry
|
// Registry.Register: Adds a command to the registry.
|
||||||
func (r *Registry) Register(cmd Command) {
|
func (r *Registry) Register(cmd Command) {
|
||||||
r.commands = append(r.commands, cmd)
|
r.commands = append(r.commands, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup finds a command by name or abbreviation
|
// Registry.Lookup: Finds a command by name or abbreviation with error handling.
|
||||||
// Returns the command and any error (unknown or ambiguous)
|
|
||||||
func (r *Registry) Lookup(input string) (*Command, error) {
|
func (r *Registry) Lookup(input string) (*Command, error) {
|
||||||
if input == "" {
|
if input == "" {
|
||||||
return nil, fmt.Errorf("no command given")
|
return nil, fmt.Errorf("no command given")
|
||||||
@ -75,31 +74,41 @@ func (r *Registry) Lookup(input string) (*Command, error) {
|
|||||||
return matches[0], nil
|
return matches[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse splits a command line into command name and arguments
|
// Parse: Splits a command line into command name and arguments.
|
||||||
func Parse(cmdLine string) (name string, args []string) {
|
func Parse(cmdLine string) (name string, args []string, force bool) {
|
||||||
parts := strings.Fields(cmdLine)
|
parts := strings.Fields(cmdLine)
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
return "", nil
|
return "", nil, false
|
||||||
}
|
|
||||||
return parts[0], parts[1:]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute parses and executes a command line
|
name = parts[0]
|
||||||
|
args = parts[1:]
|
||||||
|
|
||||||
|
// Check if command ends with ! (force flag)
|
||||||
|
if strings.HasSuffix(name, "!") {
|
||||||
|
name = strings.TrimSuffix(name, "!")
|
||||||
|
force = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return name, args, force
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registry.Execute: Parses and executes a command line.
|
||||||
func (r *Registry) Execute(m action.Model, cmdLine string) (tea.Cmd, error) {
|
func (r *Registry) Execute(m action.Model, cmdLine string) (tea.Cmd, error) {
|
||||||
name, args := Parse(cmdLine)
|
name, args, force := Parse(cmdLine)
|
||||||
|
|
||||||
cmd, err := r.Lookup(name)
|
cmd, err := r.Lookup(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd.Handler(m, args), nil
|
return cmd.Handler(m, args, force), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultRegistry is the global command registry
|
// DefaultRegistry is the global command registry
|
||||||
var DefaultRegistry = NewRegistry()
|
var DefaultRegistry = NewRegistry()
|
||||||
|
|
||||||
// registerDefaults registers the built-in commands
|
// Registry.registerDefaults: Registers the built-in commands.
|
||||||
func (r *Registry) registerDefaults() {
|
func (r *Registry) registerDefaults() {
|
||||||
// Quit commands
|
// Quit commands
|
||||||
r.Register(Command{
|
r.Register(Command{
|
||||||
@ -133,6 +142,12 @@ func (r *Registry) registerDefaults() {
|
|||||||
Handler: cmdWriteQuit,
|
Handler: cmdWriteQuit,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
r.Register(Command{
|
||||||
|
Name: "wqall",
|
||||||
|
ShortForm: "wqa",
|
||||||
|
Handler: cmdWriteQuitAll,
|
||||||
|
})
|
||||||
|
|
||||||
// Set command
|
// Set command
|
||||||
r.Register(Command{
|
r.Register(Command{
|
||||||
Name: "set",
|
Name: "set",
|
||||||
@ -146,4 +161,11 @@ func (r *Registry) registerDefaults() {
|
|||||||
ShortForm: "reg",
|
ShortForm: "reg",
|
||||||
Handler: cmdRegisters,
|
Handler: cmdRegisters,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// File commands
|
||||||
|
r.Register(Command{
|
||||||
|
Name: "edit",
|
||||||
|
ShortForm: "e",
|
||||||
|
Handler: cmdEdit,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -89,6 +89,26 @@ func TestRegistryLookup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("e matches edit", func(t *testing.T) {
|
||||||
|
cmd, err := r.Lookup("e")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Lookup error: %v", err)
|
||||||
|
}
|
||||||
|
if cmd.Name != "edit" {
|
||||||
|
t.Errorf("cmd.Name = %q, want \"edit\"", cmd.Name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ed matches edit", func(t *testing.T) {
|
||||||
|
cmd, err := r.Lookup("ed")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Lookup error: %v", err)
|
||||||
|
}
|
||||||
|
if cmd.Name != "edit" {
|
||||||
|
t.Errorf("cmd.Name = %q, want \"edit\"", cmd.Name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("unknown command returns error", func(t *testing.T) {
|
t.Run("unknown command returns error", func(t *testing.T) {
|
||||||
_, err := r.Lookup("xyz")
|
_, err := r.Lookup("xyz")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -106,17 +126,20 @@ func TestRegistryLookup(t *testing.T) {
|
|||||||
|
|
||||||
func TestParse(t *testing.T) {
|
func TestParse(t *testing.T) {
|
||||||
t.Run("command only", func(t *testing.T) {
|
t.Run("command only", func(t *testing.T) {
|
||||||
name, args := Parse("quit")
|
name, args, force := Parse("quit")
|
||||||
if name != "quit" {
|
if name != "quit" {
|
||||||
t.Errorf("name = %q, want \"quit\"", name)
|
t.Errorf("name = %q, want \"quit\"", name)
|
||||||
}
|
}
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
t.Errorf("len(args) = %d, want 0", len(args))
|
t.Errorf("len(args) = %d, want 0", len(args))
|
||||||
}
|
}
|
||||||
|
if force {
|
||||||
|
t.Error("force should be false")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("command with one arg", func(t *testing.T) {
|
t.Run("command with one arg", func(t *testing.T) {
|
||||||
name, args := Parse("set number")
|
name, args, force := Parse("set number")
|
||||||
if name != "set" {
|
if name != "set" {
|
||||||
t.Errorf("name = %q, want \"set\"", name)
|
t.Errorf("name = %q, want \"set\"", name)
|
||||||
}
|
}
|
||||||
@ -126,10 +149,13 @@ func TestParse(t *testing.T) {
|
|||||||
if args[0] != "number" {
|
if args[0] != "number" {
|
||||||
t.Errorf("args[0] = %q, want \"number\"", args[0])
|
t.Errorf("args[0] = %q, want \"number\"", args[0])
|
||||||
}
|
}
|
||||||
|
if force {
|
||||||
|
t.Error("force should be false")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("command with multiple args", func(t *testing.T) {
|
t.Run("command with multiple args", func(t *testing.T) {
|
||||||
name, args := Parse("set number tabstop=4")
|
name, args, force := Parse("set number tabstop=4")
|
||||||
if name != "set" {
|
if name != "set" {
|
||||||
t.Errorf("name = %q, want \"set\"", name)
|
t.Errorf("name = %q, want \"set\"", name)
|
||||||
}
|
}
|
||||||
@ -142,26 +168,74 @@ func TestParse(t *testing.T) {
|
|||||||
if args[1] != "tabstop=4" {
|
if args[1] != "tabstop=4" {
|
||||||
t.Errorf("args[1] = %q, want \"tabstop=4\"", args[1])
|
t.Errorf("args[1] = %q, want \"tabstop=4\"", args[1])
|
||||||
}
|
}
|
||||||
|
if force {
|
||||||
|
t.Error("force should be false")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("empty string", func(t *testing.T) {
|
t.Run("empty string", func(t *testing.T) {
|
||||||
name, args := Parse("")
|
name, args, force := Parse("")
|
||||||
if name != "" {
|
if name != "" {
|
||||||
t.Errorf("name = %q, want \"\"", name)
|
t.Errorf("name = %q, want \"\"", name)
|
||||||
}
|
}
|
||||||
if args != nil {
|
if args != nil {
|
||||||
t.Errorf("args = %v, want nil", args)
|
t.Errorf("args = %v, want nil", args)
|
||||||
}
|
}
|
||||||
|
if force {
|
||||||
|
t.Error("force should be false")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("whitespace only", func(t *testing.T) {
|
t.Run("whitespace only", func(t *testing.T) {
|
||||||
name, args := Parse(" ")
|
name, args, force := Parse(" ")
|
||||||
if name != "" {
|
if name != "" {
|
||||||
t.Errorf("name = %q, want \"\"", name)
|
t.Errorf("name = %q, want \"\"", name)
|
||||||
}
|
}
|
||||||
if args != nil {
|
if args != nil {
|
||||||
t.Errorf("args = %v, want nil", args)
|
t.Errorf("args = %v, want nil", args)
|
||||||
}
|
}
|
||||||
|
if force {
|
||||||
|
t.Error("force should be false")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("command with force flag", func(t *testing.T) {
|
||||||
|
name, args, force := Parse("quit!")
|
||||||
|
if name != "quit" {
|
||||||
|
t.Errorf("name = %q, want \"quit\"", name)
|
||||||
|
}
|
||||||
|
if len(args) != 0 {
|
||||||
|
t.Errorf("len(args) = %d, want 0", len(args))
|
||||||
|
}
|
||||||
|
if !force {
|
||||||
|
t.Error("force should be true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("write command with force", func(t *testing.T) {
|
||||||
|
name, _, force := Parse("w!")
|
||||||
|
if name != "w" {
|
||||||
|
t.Errorf("name = %q, want \"w\"", name)
|
||||||
|
}
|
||||||
|
if !force {
|
||||||
|
t.Error("force should be true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("command with force and args", func(t *testing.T) {
|
||||||
|
name, args, force := Parse("w! file.txt")
|
||||||
|
if name != "w" {
|
||||||
|
t.Errorf("name = %q, want \"w\"", name)
|
||||||
|
}
|
||||||
|
if len(args) != 1 {
|
||||||
|
t.Errorf("len(args) = %d, want 1", len(args))
|
||||||
|
}
|
||||||
|
if args[0] != "file.txt" {
|
||||||
|
t.Errorf("args[0] = %q, want \"file.txt\"", args[0])
|
||||||
|
}
|
||||||
|
if !force {
|
||||||
|
t.Error("force should be true")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
130
internal/core/buffer.go
Normal file
130
internal/core/buffer.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
type BufferOptions struct {
|
||||||
|
// tabstop expandtab
|
||||||
|
}
|
||||||
|
|
||||||
|
type BufferType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScatchBuffer BufferType = iota
|
||||||
|
FileBuffer
|
||||||
|
DirectoryBuffer
|
||||||
|
)
|
||||||
|
|
||||||
|
type Buffer struct {
|
||||||
|
// Buffer data
|
||||||
|
Id int
|
||||||
|
Type BufferType
|
||||||
|
|
||||||
|
// File data
|
||||||
|
Filename string
|
||||||
|
Filetype string
|
||||||
|
Lines []string
|
||||||
|
|
||||||
|
// Flags (not used yet)
|
||||||
|
Modified bool
|
||||||
|
Loaded bool
|
||||||
|
Listed bool
|
||||||
|
ReadOnly bool
|
||||||
|
|
||||||
|
// Options BufferOptions
|
||||||
|
// UndoTree TODO: This will be big
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// Helper methods
|
||||||
|
// ==================================================
|
||||||
|
|
||||||
|
// Buffer.Line: Get the line at an index. Returns an empty string if the index
|
||||||
|
// is out of bounds.
|
||||||
|
func (b *Buffer) Line(idx int) string {
|
||||||
|
if idx < 0 || idx >= len(b.Lines) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
b.Lines[idx] = content
|
||||||
|
}
|
||||||
|
b.Modified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer.InsertLine: Insert a line with content at an index. The index is clamped
|
||||||
|
// to valid bounds (0 to len(Lines)). The new line is inserted before the line at
|
||||||
|
// the given index. This function sets the modified flag.
|
||||||
|
func (b *Buffer) InsertLine(idx int, content string) {
|
||||||
|
if idx < 0 {
|
||||||
|
idx = 0
|
||||||
|
}
|
||||||
|
if idx > len(b.Lines) {
|
||||||
|
idx = len(b.Lines)
|
||||||
|
}
|
||||||
|
b.Lines = append(b.Lines[:idx], append([]string{content}, b.Lines[idx:]...)...)
|
||||||
|
b.Modified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer.DeleteLine: Delete a line at an index. Does nothing if the index is out
|
||||||
|
// of bounds. This function sets the modified flag.
|
||||||
|
func (b *Buffer) DeleteLine(idx int) {
|
||||||
|
if idx >= 0 && idx < len(b.Lines) {
|
||||||
|
b.Lines = append(b.Lines[:idx], b.Lines[idx+1:]...)
|
||||||
|
}
|
||||||
|
b.Modified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer.LineCount: Get the number of lines in the buffer.
|
||||||
|
func (b *Buffer) LineCount() int {
|
||||||
|
return len(b.Lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// Setters
|
||||||
|
// ==================================================
|
||||||
|
|
||||||
|
// Buffer.SetFilename: Set the filename associated with this buffer. This is
|
||||||
|
// typically the path to the file on disk that this buffer represents.
|
||||||
|
func (b *Buffer) SetFilename(filename string) {
|
||||||
|
b.Filename = filename
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer.SetFiletype: Set the filetype of this buffer. The filetype is used
|
||||||
|
// for syntax highlighting and other language-specific features.
|
||||||
|
func (b *Buffer) SetFiletype(filetype string) {
|
||||||
|
b.Filetype = filetype
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = lines
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer.SetModified: Set the modified flag for this buffer. A modified buffer
|
||||||
|
// has unsaved changes that differ from the file on disk.
|
||||||
|
func (b *Buffer) SetModified(modified bool) {
|
||||||
|
b.Modified = modified
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer.SetLoaded: Set the loaded flag for this buffer. A loaded buffer has
|
||||||
|
// its content in memory, while an unloaded buffer exists only as metadata.
|
||||||
|
func (b *Buffer) SetLoaded(loaded bool) {
|
||||||
|
b.Loaded = loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer.SetListed: Set the listed flag for this buffer. A listed buffer appears
|
||||||
|
// in buffer lists (like :ls), while an unlisted buffer is hidden from normal
|
||||||
|
// buffer navigation.
|
||||||
|
func (b *Buffer) SetListed(listed bool) {
|
||||||
|
b.Listed = listed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer.SetType: Set the buffers type. This type is used to determine handling
|
||||||
|
// of I/O functions.
|
||||||
|
func (b *Buffer) SetType(t BufferType) {
|
||||||
|
b.Type = t
|
||||||
|
}
|
||||||
89
internal/core/buffer_builder.go
Normal file
89
internal/core/buffer_builder.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
// Not great, but maybe the best way
|
||||||
|
var CurrentBufferId int = 1
|
||||||
|
|
||||||
|
type BufferBuilder struct {
|
||||||
|
buffer Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBufferBuilder: Creates a new buffer builder. The buffer builder implements a
|
||||||
|
// builder pattern to create a buffer with the defined properties and values.
|
||||||
|
func NewBufferBuilder() *BufferBuilder {
|
||||||
|
return &BufferBuilder{
|
||||||
|
buffer: Buffer{
|
||||||
|
Id: 0, // This is set when built
|
||||||
|
Type: ScatchBuffer, // Default buffer type
|
||||||
|
Filename: "",
|
||||||
|
Filetype: "",
|
||||||
|
Lines: []string{""},
|
||||||
|
Modified: false,
|
||||||
|
Loaded: false,
|
||||||
|
Listed: false,
|
||||||
|
ReadOnly: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferBuilder.WithFilename: Attaches a file name to the buffer that is being built.
|
||||||
|
func (b *BufferBuilder) WithFilename(filename string) *BufferBuilder {
|
||||||
|
b.buffer.Filename = filename
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferBuilder.WithFiletype: Attaches a file type to the buffer that is being built.
|
||||||
|
func (b *BufferBuilder) WithFiletype(filetype string) *BufferBuilder {
|
||||||
|
b.buffer.Filetype = filetype
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferBuilder.WithLines: Attaches a lines to the buffer that is being built.
|
||||||
|
func (b *BufferBuilder) WithLines(lines []string) *BufferBuilder {
|
||||||
|
b.buffer.Lines = lines
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferBuilder.Modified: Sets the modified flag of the buffer being built. By default,
|
||||||
|
// buffers are built with the modified flag being false.
|
||||||
|
func (b *BufferBuilder) Modified() *BufferBuilder {
|
||||||
|
b.buffer.Modified = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferBuilder.Loaded: Sets the loaded flag of the buffer being built. By default,
|
||||||
|
// buffers are built with the loaded flag being false.
|
||||||
|
func (b *BufferBuilder) Loaded() *BufferBuilder {
|
||||||
|
b.buffer.Loaded = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferBuilder.Listed: Sets the listed flag of the buffer being built. By default,
|
||||||
|
// buffers are built with the listed flag being false.
|
||||||
|
func (b *BufferBuilder) Listed() *BufferBuilder {
|
||||||
|
b.buffer.Listed = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferBuilder.ReadOnly: Sets the readonly flag of the buffer being built. By default,
|
||||||
|
// buffers are built with the readonly flag being false.
|
||||||
|
func (b *BufferBuilder) ReadOnly() *BufferBuilder {
|
||||||
|
b.buffer.ReadOnly = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferBuilder.Listed: Sets the type of the buffer being built. By default, buffers
|
||||||
|
// are build with the ScatchBuffer type.
|
||||||
|
func (b *BufferBuilder) WithType(t BufferType) *BufferBuilder {
|
||||||
|
b.buffer.Type = t
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferBuilder.Build: Build the final buffer and return it to the caller. Final
|
||||||
|
// step in the process. This is where the ID is set, so many buffers can be "in-progress"
|
||||||
|
// but the ID will be set when they are built. Meaning, this is not thread safe.
|
||||||
|
func (b *BufferBuilder) Build() Buffer {
|
||||||
|
b.buffer.Id = CurrentBufferId
|
||||||
|
CurrentBufferId++
|
||||||
|
|
||||||
|
return b.buffer
|
||||||
|
}
|
||||||
302
internal/core/buffer_test.go
Normal file
302
internal/core/buffer_test.go
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Buffer Tests (generated by ClaudeCode)
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
func TestBuffer_InsertLine(t *testing.T) {
|
||||||
|
t.Run("inserts at beginning", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"line 1", "line 2"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
buf.InsertLine(0, "new line")
|
||||||
|
|
||||||
|
if buf.LineCount() != 3 {
|
||||||
|
t.Errorf("expected 3 lines, got %d", buf.LineCount())
|
||||||
|
}
|
||||||
|
if buf.Line(0) != "new line" {
|
||||||
|
t.Errorf("expected 'new line', got '%s'", buf.Line(0))
|
||||||
|
}
|
||||||
|
if buf.Line(1) != "line 1" {
|
||||||
|
t.Errorf("expected 'line 1' at index 1, got '%s'", buf.Line(1))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("inserts at end", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"line 1", "line 2"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
buf.InsertLine(2, "new line")
|
||||||
|
|
||||||
|
if buf.LineCount() != 3 {
|
||||||
|
t.Errorf("expected 3 lines, got %d", buf.LineCount())
|
||||||
|
}
|
||||||
|
if buf.Line(2) != "new line" {
|
||||||
|
t.Errorf("expected 'new line' at end, got '%s'", buf.Line(2))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("inserts in middle", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"line 1", "line 3"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
buf.InsertLine(1, "line 2")
|
||||||
|
|
||||||
|
if buf.LineCount() != 3 {
|
||||||
|
t.Errorf("expected 3 lines, got %d", buf.LineCount())
|
||||||
|
}
|
||||||
|
if buf.Line(1) != "line 2" {
|
||||||
|
t.Errorf("expected 'line 2' at index 1, got '%s'", buf.Line(1))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles empty buffer", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().Build()
|
||||||
|
|
||||||
|
buf.InsertLine(0, "first line")
|
||||||
|
|
||||||
|
if buf.LineCount() != 2 { // Original empty line + new line
|
||||||
|
t.Errorf("expected 2 lines, got %d", buf.LineCount())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuffer_DeleteLine(t *testing.T) {
|
||||||
|
t.Run("deletes from beginning", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"DELETE ME", "line 2", "line 3"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
buf.DeleteLine(0)
|
||||||
|
|
||||||
|
if buf.LineCount() != 2 {
|
||||||
|
t.Errorf("expected 2 lines, got %d", buf.LineCount())
|
||||||
|
}
|
||||||
|
if buf.Line(0) != "line 2" {
|
||||||
|
t.Errorf("expected 'line 2' at index 0, got '%s'", buf.Line(0))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deletes from middle", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"line 1", "DELETE ME", "line 3"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
buf.DeleteLine(1)
|
||||||
|
|
||||||
|
if buf.LineCount() != 2 {
|
||||||
|
t.Errorf("expected 2 lines, got %d", buf.LineCount())
|
||||||
|
}
|
||||||
|
if buf.Line(1) != "line 3" {
|
||||||
|
t.Errorf("expected 'line 3' at index 1, got '%s'", buf.Line(1))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deletes from end", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"line 1", "line 2", "DELETE ME"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
buf.DeleteLine(2)
|
||||||
|
|
||||||
|
if buf.LineCount() != 2 {
|
||||||
|
t.Errorf("expected 2 lines, got %d", buf.LineCount())
|
||||||
|
}
|
||||||
|
if buf.Line(1) != "line 2" {
|
||||||
|
t.Errorf("expected 'line 2' at index 1, got '%s'", buf.Line(1))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can delete all lines", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"only line"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
buf.DeleteLine(0)
|
||||||
|
|
||||||
|
// Buffer allows being completely empty (0 lines)
|
||||||
|
if buf.LineCount() != 0 {
|
||||||
|
t.Errorf("expected 0 lines after deleting last line, got %d", buf.LineCount())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuffer_SetLine(t *testing.T) {
|
||||||
|
t.Run("updates existing line", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"old content"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
buf.SetLine(0, "new content")
|
||||||
|
|
||||||
|
if buf.Line(0) != "new content" {
|
||||||
|
t.Errorf("expected 'new content', got '%s'", buf.Line(0))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates middle line", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"line 1", "old", "line 3"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
buf.SetLine(1, "new")
|
||||||
|
|
||||||
|
if buf.Line(1) != "new" {
|
||||||
|
t.Errorf("expected 'new', got '%s'", buf.Line(1))
|
||||||
|
}
|
||||||
|
// Verify other lines unchanged
|
||||||
|
if buf.Line(0) != "line 1" {
|
||||||
|
t.Error("line 0 should be unchanged")
|
||||||
|
}
|
||||||
|
if buf.Line(2) != "line 3" {
|
||||||
|
t.Error("line 2 should be unchanged")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can set to empty string", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"has content"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
buf.SetLine(0, "")
|
||||||
|
|
||||||
|
if buf.Line(0) != "" {
|
||||||
|
t.Errorf("expected empty line, got '%s'", buf.Line(0))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuffer_LineCount(t *testing.T) {
|
||||||
|
t.Run("empty buffer has one line", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().Build()
|
||||||
|
|
||||||
|
if buf.LineCount() != 1 {
|
||||||
|
t.Errorf("expected 1 line in empty buffer, got %d", buf.LineCount())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("counts multiple lines", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"a", "b", "c", "d", "e"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
if buf.LineCount() != 5 {
|
||||||
|
t.Errorf("expected 5 lines, got %d", buf.LineCount())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("counts after insertions", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"line 1"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
buf.InsertLine(1, "line 2")
|
||||||
|
buf.InsertLine(2, "line 3")
|
||||||
|
|
||||||
|
if buf.LineCount() != 3 {
|
||||||
|
t.Errorf("expected 3 lines, got %d", buf.LineCount())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("counts after deletions", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"a", "b", "c", "d", "e"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
buf.DeleteLine(2)
|
||||||
|
buf.DeleteLine(2)
|
||||||
|
|
||||||
|
if buf.LineCount() != 3 {
|
||||||
|
t.Errorf("expected 3 lines after 2 deletions, got %d", buf.LineCount())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuffer_Line(t *testing.T) {
|
||||||
|
t.Run("retrieves correct line", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"first", "second", "third"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
if buf.Line(0) != "first" {
|
||||||
|
t.Errorf("expected 'first', got '%s'", buf.Line(0))
|
||||||
|
}
|
||||||
|
if buf.Line(1) != "second" {
|
||||||
|
t.Errorf("expected 'second', got '%s'", buf.Line(1))
|
||||||
|
}
|
||||||
|
if buf.Line(2) != "third" {
|
||||||
|
t.Errorf("expected 'third', got '%s'", buf.Line(2))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles special characters", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"hello\tworld", "foo\nbar"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
if buf.Line(0) != "hello\tworld" {
|
||||||
|
t.Errorf("expected tabs preserved, got '%s'", buf.Line(0))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles unicode", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"hello 世界", "emoji 🎉"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
if buf.Line(0) != "hello 世界" {
|
||||||
|
t.Errorf("expected unicode preserved, got '%s'", buf.Line(0))
|
||||||
|
}
|
||||||
|
if buf.Line(1) != "emoji 🎉" {
|
||||||
|
t.Errorf("expected emoji preserved, got '%s'", buf.Line(1))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBufferBuilder(t *testing.T) {
|
||||||
|
t.Run("builds with default values", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().Build()
|
||||||
|
|
||||||
|
if buf.LineCount() != 1 {
|
||||||
|
t.Errorf("expected 1 empty line, got %d", buf.LineCount())
|
||||||
|
}
|
||||||
|
if buf.Filename != "" {
|
||||||
|
t.Errorf("expected empty filename, got '%s'", buf.Filename)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("builds with custom lines", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"line 1", "line 2"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
if buf.LineCount() != 2 {
|
||||||
|
t.Errorf("expected 2 lines, got %d", buf.LineCount())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("builds with filename", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithFilename("test.txt").
|
||||||
|
Build()
|
||||||
|
|
||||||
|
if buf.Filename != "test.txt" {
|
||||||
|
t.Errorf("expected filename 'test.txt', got '%s'", buf.Filename)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("builds with filetype", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithFiletype("go").
|
||||||
|
Build()
|
||||||
|
|
||||||
|
if buf.Filetype != "go" {
|
||||||
|
t.Errorf("expected filetype 'go', got '%s'", buf.Filetype)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
41
internal/core/mode.go
Normal file
41
internal/core/mode.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
// Mode constants for editor mode
|
||||||
|
type Mode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
NormalMode Mode = iota
|
||||||
|
InsertMode
|
||||||
|
CommandMode
|
||||||
|
VisualMode
|
||||||
|
VisualLineMode
|
||||||
|
VisualBlockMode
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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:
|
||||||
|
return "NORMAL"
|
||||||
|
case InsertMode:
|
||||||
|
return "INSERT"
|
||||||
|
case CommandMode:
|
||||||
|
return "COMMAND"
|
||||||
|
case VisualMode:
|
||||||
|
return "VISUAL"
|
||||||
|
case VisualLineMode:
|
||||||
|
return "V-LINE"
|
||||||
|
case VisualBlockMode:
|
||||||
|
return "V-BLOCK"
|
||||||
|
default:
|
||||||
|
return "-----"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode.IsVisualMode: Returns true if the mode is any visual mode variant.
|
||||||
|
func (m Mode) IsVisualMode() bool {
|
||||||
|
return m == VisualMode ||
|
||||||
|
m == VisualLineMode ||
|
||||||
|
m == VisualBlockMode
|
||||||
|
}
|
||||||
6
internal/core/position.go
Normal file
6
internal/core/position.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
// Position represents a location in the buffer
|
||||||
|
type Position struct {
|
||||||
|
Line, Col int
|
||||||
|
}
|
||||||
93
internal/core/register.go
Normal file
93
internal/core/register.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
// RegisterType: Indicates how the register content should be interpreted when
|
||||||
|
// pasting. Charwise treats content as continuous text, linewise operates on
|
||||||
|
// complete lines, and blockwise represents rectangular selections.
|
||||||
|
type RegisterType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CharwiseRegister RegisterType = iota
|
||||||
|
LinewiseRegister
|
||||||
|
BlockwiseRegister
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register: Stores yanked or deleted text with metadata about how it should be
|
||||||
|
// pasted. The Type determines paste behavior and Content holds the text lines.
|
||||||
|
type Register struct {
|
||||||
|
Type RegisterType
|
||||||
|
Content []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultRegisters: Creates and initializes the complete set of vim-style
|
||||||
|
// registers. Returns a map containing special registers (", *, _, etc.),
|
||||||
|
// named registers (a-z), and numbered registers (0-9).
|
||||||
|
func DefaultRegisters() map[rune]Register {
|
||||||
|
reg := make(map[rune]Register)
|
||||||
|
|
||||||
|
addSpecialRegisters(reg)
|
||||||
|
addNamedRegisters(reg)
|
||||||
|
addNumberedRegisters(reg)
|
||||||
|
|
||||||
|
return reg
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNamedRegisters: Initializes the 26 named registers (a-z) used for explicit
|
||||||
|
// yank/delete operations. Users can target these with commands like "ayy or "ap.
|
||||||
|
func addNamedRegisters(reg map[rune]Register) {
|
||||||
|
name := 'a'
|
||||||
|
|
||||||
|
for name <= 'z' {
|
||||||
|
reg[name] = emptyRegister()
|
||||||
|
name++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNumberedRegisters: Initializes the numbered registers (0-9) which form the
|
||||||
|
// delete history. Register 0 holds the most recent yank, and 1-9 hold previous
|
||||||
|
// deletes in chronological order.
|
||||||
|
func addNumberedRegisters(reg map[rune]Register) {
|
||||||
|
name := '0'
|
||||||
|
|
||||||
|
for name <= '9' {
|
||||||
|
reg[name] = emptyRegister()
|
||||||
|
name++
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// addSpecialRegisters: Initializes special-purpose registers with specific
|
||||||
|
// behaviors. Includes the unnamed register ("), black hole (_), clipboard (*),
|
||||||
|
// last insert (.), current filename (%), last command (:), and alternate file (#).
|
||||||
|
func addSpecialRegisters(reg map[rune]Register) {
|
||||||
|
// Unnamed (default)
|
||||||
|
reg['"'] = emptyRegister()
|
||||||
|
|
||||||
|
// Black hole (readonly)
|
||||||
|
reg['_'] = emptyRegister()
|
||||||
|
|
||||||
|
// System clipboard
|
||||||
|
reg['*'] = emptyRegister()
|
||||||
|
|
||||||
|
// Small delete? Expression?
|
||||||
|
|
||||||
|
// Last inserted text (readonly)
|
||||||
|
reg['.'] = emptyRegister()
|
||||||
|
|
||||||
|
// Current file name (readonly)
|
||||||
|
reg['%'] = emptyRegister()
|
||||||
|
|
||||||
|
// Last executed command (readonly)
|
||||||
|
reg[':'] = emptyRegister()
|
||||||
|
|
||||||
|
// Alternate (previous) file (readonly)
|
||||||
|
reg['#'] = emptyRegister()
|
||||||
|
}
|
||||||
|
|
||||||
|
// emptyRegister: Creates a new register initialized with charwise type and empty
|
||||||
|
// content. Used as the default state for all registers during initialization.
|
||||||
|
func emptyRegister() Register {
|
||||||
|
return Register{
|
||||||
|
Type: CharwiseRegister,
|
||||||
|
Content: []string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
15
internal/core/settings.go
Normal file
15
internal/core/settings.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
// EditorSettings: Configuration options for editor display and behavior.
|
||||||
|
type EditorSettings struct {
|
||||||
|
TabStop int
|
||||||
|
// TODO: Colors
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultSettings: Creates a Settings struct with sensible defaults for
|
||||||
|
// line numbers, gutter width, tab size, and scroll offset.
|
||||||
|
func NewDefaultSettings() EditorSettings {
|
||||||
|
return EditorSettings{
|
||||||
|
TabStop: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
15
internal/core/types.go
Normal file
15
internal/core/types.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
// MotionType indicates how a motion operates
|
||||||
|
type MotionType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CharwiseExclusive MotionType = iota // w, b, h, l, 0, ^ - end position not included
|
||||||
|
CharwiseInclusive // e, $, f - end position is included
|
||||||
|
Linewise // j, k, G, gg, {, } - operates on whole lines
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsCharwise returns true if the motion type is character-based (not linewise)
|
||||||
|
func (mt MotionType) IsCharwise() bool {
|
||||||
|
return mt == CharwiseExclusive || mt == CharwiseInclusive
|
||||||
|
}
|
||||||
205
internal/core/window.go
Normal file
205
internal/core/window.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
type WinOptions struct {
|
||||||
|
Number bool
|
||||||
|
RelativeNumber bool
|
||||||
|
GutterSize int
|
||||||
|
// Wrap bool
|
||||||
|
ScrollOff int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultWinOptions() WinOptions {
|
||||||
|
return WinOptions{
|
||||||
|
Number: true,
|
||||||
|
RelativeNumber: true,
|
||||||
|
GutterSize: 5,
|
||||||
|
ScrollOff: 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Window struct {
|
||||||
|
Id int
|
||||||
|
Number int // Ignored for now, will be used when splits come into play
|
||||||
|
Buffer *Buffer
|
||||||
|
|
||||||
|
Cursor Position // DO NOT MODIFY DIRECTLY, USE SETTERS
|
||||||
|
Anchor Position
|
||||||
|
|
||||||
|
ScrollY int
|
||||||
|
Height int
|
||||||
|
Width int
|
||||||
|
|
||||||
|
// Folds TODO
|
||||||
|
Options WinOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// Helper methods
|
||||||
|
// ==================================================
|
||||||
|
|
||||||
|
// Window.ClampCursor: Clamps the cursor in the all directions to ensure the cursor
|
||||||
|
// does not go into an invalid position. Such as negative values or past the end of
|
||||||
|
// the line. In the Y direction it validates that the cursor does not pass the end
|
||||||
|
// of the content or attempt to be "above" the content (negative value). This function
|
||||||
|
// is automatically called in any time the cursor changes. It only needs to be called
|
||||||
|
// when a force clamp is needed.
|
||||||
|
func (w *Window) ClampCursor() {
|
||||||
|
// Clamp line to valid range [0, lineCount-1]
|
||||||
|
maxLine := max(w.Buffer.LineCount()-1, 0)
|
||||||
|
if w.Cursor.Line < 0 {
|
||||||
|
w.Cursor.Line = 0
|
||||||
|
} else if w.Cursor.Line > maxLine {
|
||||||
|
w.Cursor.Line = maxLine
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle empty buffer - no lines to clamp column against
|
||||||
|
if w.Buffer.LineCount() == 0 {
|
||||||
|
w.Cursor.Line = 0
|
||||||
|
w.Cursor.Col = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp column to valid range [0, lineLen]
|
||||||
|
lineLen := len(w.Buffer.Lines[w.Cursor.Line])
|
||||||
|
if w.Cursor.Col < 0 {
|
||||||
|
w.Cursor.Col = 0
|
||||||
|
} else if lineLen == 0 {
|
||||||
|
w.Cursor.Col = 0
|
||||||
|
} else if w.Cursor.Col >= lineLen {
|
||||||
|
w.Cursor.Col = lineLen // Allow cursor after last char (insert mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window.AdjustScroll ensures the cursor stays within the height with scrollOff margins.
|
||||||
|
// Call this after any cursor movement.
|
||||||
|
func (w *Window) AdjustScroll() {
|
||||||
|
if w.Height <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewPort := w.ViewportHeight()
|
||||||
|
|
||||||
|
// Effective scrollOff (can't be more than half the viewport)
|
||||||
|
off := min(w.Options.ScrollOff, viewPort/2)
|
||||||
|
|
||||||
|
// Cursor too close to top — scroll up
|
||||||
|
if w.Cursor.Line < w.ScrollY+off {
|
||||||
|
w.ScrollY = w.Cursor.Line - off
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor too close to bottom — scroll down
|
||||||
|
if w.Cursor.Line > w.ScrollY+viewPort-1-off {
|
||||||
|
w.ScrollY = w.Cursor.Line - viewPort + 1 + off
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp scrollY to valid range
|
||||||
|
maxScroll := max(0, w.Buffer.LineCount()-viewPort)
|
||||||
|
w.ScrollY = max(0, min(w.ScrollY, maxScroll))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// Getters (for computed values)
|
||||||
|
// ==================================================
|
||||||
|
|
||||||
|
func (w *Window) ViewportHeight() int {
|
||||||
|
// TODO: This will need more magic when splits come into play
|
||||||
|
|
||||||
|
// -1 for command bar
|
||||||
|
// -1 for status line
|
||||||
|
return w.Height - 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// Setters
|
||||||
|
// ==================================================
|
||||||
|
|
||||||
|
// Window.SetNumber: Sets the position-based number of this window. Currently ignored
|
||||||
|
// until splits are implemented.
|
||||||
|
func (w *Window) SetNumber(number int) {
|
||||||
|
w.Number = number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window.SetBuffer: Sets the buffer that this window should display. This is used when
|
||||||
|
// switching between buffers or opening a new file in the current window. This function
|
||||||
|
// does clamp the cursor to the current buffer
|
||||||
|
func (w *Window) SetBuffer(buffer *Buffer) {
|
||||||
|
w.Buffer = buffer
|
||||||
|
w.ClampCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window.SetCursor: Sets the cursor position in this window to the given position.
|
||||||
|
func (w *Window) SetCursor(cursor Position) {
|
||||||
|
w.Cursor = cursor
|
||||||
|
w.ClampCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window.SetCursorLine: Sets the line number of the cursor position.
|
||||||
|
func (w *Window) SetCursorLine(line int) {
|
||||||
|
w.Cursor.Line = line
|
||||||
|
w.ClampCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window.SetCursorCol: Sets the column number of the cursor position.
|
||||||
|
func (w *Window) SetCursorCol(col int) {
|
||||||
|
w.Cursor.Col = col
|
||||||
|
w.ClampCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window.SetCursorPos: Sets both the line and column of the cursor position. This is
|
||||||
|
// a convenience method for setting both components at once.
|
||||||
|
func (w *Window) SetCursorPos(line, col int) {
|
||||||
|
w.Cursor.Line = line
|
||||||
|
w.Cursor.Col = col
|
||||||
|
w.ClampCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window.SetAnchor: Sets the anchor position in this window. The anchor is used for
|
||||||
|
// visual mode selections as the starting point of the selection.
|
||||||
|
func (w *Window) SetAnchor(anchor Position) {
|
||||||
|
w.Anchor = anchor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window.SetAnchorLine: Sets the line number of the anchor position.
|
||||||
|
func (w *Window) SetAnchorLine(line int) {
|
||||||
|
w.Anchor.Line = line
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window.SetAnchorCol: Sets the column number of the anchor position.
|
||||||
|
func (w *Window) SetAnchorCol(col int) {
|
||||||
|
w.Anchor.Col = col
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window.SetAnchorPos: Sets both the line and column of the anchor position. This is
|
||||||
|
// a convenience method for setting both components at once.
|
||||||
|
func (w *Window) SetAnchorPos(line, col int) {
|
||||||
|
w.Anchor.Line = line
|
||||||
|
w.Anchor.Col = col
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window.SetScrollY: Sets the vertical scroll offset of this window. This determines
|
||||||
|
// which line appears at the top of the visible viewport.
|
||||||
|
func (w *Window) SetScrollY(scrollY int) {
|
||||||
|
w.ScrollY = scrollY
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window.SetHeight: Sets the height of this window in lines.
|
||||||
|
func (w *Window) SetHeight(height int) {
|
||||||
|
w.Height = height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window.SetWidth: Sets the width of this window in columns.
|
||||||
|
func (w *Window) SetWidth(width int) {
|
||||||
|
w.Width = width
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window.SetDimensions: Sets both the width and height of this window. This is a
|
||||||
|
// convenience method for setting both dimensions at once.
|
||||||
|
func (w *Window) SetDimensions(width, height int) {
|
||||||
|
w.Width = width
|
||||||
|
w.Height = height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window.SetOptions: Sets the options of this window.
|
||||||
|
func (w *Window) SetOptions(opts WinOptions) {
|
||||||
|
w.Options = opts
|
||||||
|
}
|
||||||
111
internal/core/window_builder.go
Normal file
111
internal/core/window_builder.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
// Not great, but maybe the best way
|
||||||
|
var CurrentWindowId int = 1000
|
||||||
|
|
||||||
|
type WindowBuilder struct {
|
||||||
|
window Window
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWindowBuilder: Creates a new window builder. The window builder implements a
|
||||||
|
// builder pattern to create a window with the defined properties and values.
|
||||||
|
func NewWindowBuilder() *WindowBuilder {
|
||||||
|
return &WindowBuilder{
|
||||||
|
window: Window{
|
||||||
|
Id: 0, // This is set when built
|
||||||
|
Number: 1, // Ignored for now, will be used for splits
|
||||||
|
Buffer: nil,
|
||||||
|
Cursor: Position{Line: 0, Col: 0},
|
||||||
|
Anchor: Position{Line: 0, Col: 0},
|
||||||
|
ScrollY: 0,
|
||||||
|
Height: 0,
|
||||||
|
Width: 0,
|
||||||
|
Options: NewDefaultWinOptions(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowBuilder.WithNumber: Attaches a window number to the window that is being built.
|
||||||
|
// Window numbers are position-based and change when windows are rearranged. This is
|
||||||
|
// ignored for now, but will be used when splits are implemented.
|
||||||
|
func (w *WindowBuilder) WithNumber(number int) *WindowBuilder {
|
||||||
|
w.window.Number = number
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowBuilder.WithBuffer: Attaches a buffer to the window that is being built. The
|
||||||
|
// window will display and edit the content of this buffer.
|
||||||
|
func (w *WindowBuilder) WithBuffer(buffer *Buffer) *WindowBuilder {
|
||||||
|
w.window.Buffer = buffer
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowBuilder.WithCursor: Sets the cursor position in the window that is being built.
|
||||||
|
func (w *WindowBuilder) WithCursor(cursor Position) *WindowBuilder {
|
||||||
|
w.window.Cursor = cursor
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowBuilder.WithCursorPos: Sets the cursor position in the window that is being built.
|
||||||
|
// This is an alias for WithCursor that accepts line and column separately.
|
||||||
|
func (w *WindowBuilder) WithCursorPos(line, col int) *WindowBuilder {
|
||||||
|
w.window.Cursor = Position{Line: line, Col: col}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowBuilder.WithAnchor: Sets the anchor position in the window that is being built.
|
||||||
|
// The anchor is used for visual mode selections.
|
||||||
|
func (w *WindowBuilder) WithAnchor(anchor Position) *WindowBuilder {
|
||||||
|
w.window.Anchor = anchor
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowBuilder.WithAnchorPos: Sets the anchor position in the window that is being built.
|
||||||
|
// This is an alias for WithAnchor that accepts line and column separately.
|
||||||
|
func (w *WindowBuilder) WithAnchorPos(line, col int) *WindowBuilder {
|
||||||
|
w.window.Anchor = Position{Line: line, Col: col}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowBuilder.WithScrollY: Sets the vertical scroll offset of the window that is being built.
|
||||||
|
func (w *WindowBuilder) WithScrollY(scrollY int) *WindowBuilder {
|
||||||
|
w.window.ScrollY = scrollY
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowBuilder.WithHeight: Sets the height of the window that is being built.
|
||||||
|
func (w *WindowBuilder) WithHeight(height int) *WindowBuilder {
|
||||||
|
w.window.Height = height
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowBuilder.WithWidth: Sets the width of the window that is being built.
|
||||||
|
func (w *WindowBuilder) WithWidth(width int) *WindowBuilder {
|
||||||
|
w.window.Width = width
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowBuilder.WithDimensions: Sets both width and height of the window that is being built.
|
||||||
|
// This is a convenience method for setting dimensions in one call.
|
||||||
|
func (w *WindowBuilder) WithDimensions(width, height int) *WindowBuilder {
|
||||||
|
w.window.Width = width
|
||||||
|
w.window.Height = height
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowBuilder.WithOptions: Applies the options to the window that is being built.
|
||||||
|
// This is a convenience method for setting all options in one call.
|
||||||
|
func (w *WindowBuilder) WithOptions(options WinOptions) *WindowBuilder {
|
||||||
|
w.window.Options = options
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowBuilder.Build: Build the final window and return it to the caller. Final
|
||||||
|
// step in the process. This is where the ID is set, so many windows can be "in-progress"
|
||||||
|
// but the ID will be set when they are built. Meaning, this is not thread safe.
|
||||||
|
func (w *WindowBuilder) Build() Window {
|
||||||
|
w.window.Id = CurrentWindowId
|
||||||
|
CurrentWindowId++
|
||||||
|
|
||||||
|
return w.window
|
||||||
|
}
|
||||||
493
internal/core/window_test.go
Normal file
493
internal/core/window_test.go
Normal file
@ -0,0 +1,493 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Window Tests (generated by ClaudeCode)
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
func TestWindow_SetCursorLine(t *testing.T) {
|
||||||
|
t.Run("clamps cursor below zero", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"line 1", "line 2", "line 3"}).
|
||||||
|
Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win.SetCursorLine(-5)
|
||||||
|
|
||||||
|
if win.Cursor.Line != 0 {
|
||||||
|
t.Errorf("expected cursor at line 0, got %d", win.Cursor.Line)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("clamps cursor past end", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"line 1", "line 2", "line 3"}).
|
||||||
|
Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win.SetCursorLine(999)
|
||||||
|
|
||||||
|
if win.Cursor.Line != 2 { // 3 lines, max index is 2
|
||||||
|
t.Errorf("expected cursor at line 2, got %d", win.Cursor.Line)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("allows valid position", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"line 1", "line 2", "line 3"}).
|
||||||
|
Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win.SetCursorLine(1)
|
||||||
|
|
||||||
|
if win.Cursor.Line != 1 {
|
||||||
|
t.Errorf("expected cursor at line 1, got %d", win.Cursor.Line)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles empty buffer", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win.SetCursorLine(5)
|
||||||
|
|
||||||
|
if win.Cursor.Line != 0 {
|
||||||
|
t.Errorf("expected cursor at line 0 for empty buffer, got %d", win.Cursor.Line)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindow_SetCursorCol(t *testing.T) {
|
||||||
|
t.Run("clamps to line length", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"hello"}).
|
||||||
|
Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win.SetCursorCol(999)
|
||||||
|
|
||||||
|
// "hello" is 5 chars, max col should be 5 (after last char for insert mode)
|
||||||
|
if win.Cursor.Col > 5 {
|
||||||
|
t.Errorf("expected cursor col <= 5, got %d", win.Cursor.Col)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("clamps below zero", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"hello"}).
|
||||||
|
Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win.SetCursorCol(-10)
|
||||||
|
|
||||||
|
if win.Cursor.Col != 0 {
|
||||||
|
t.Errorf("expected cursor col 0, got %d", win.Cursor.Col)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles empty line", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{""}).
|
||||||
|
Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win.SetCursorCol(5)
|
||||||
|
|
||||||
|
if win.Cursor.Col != 0 {
|
||||||
|
t.Errorf("expected cursor at col 0 on empty line, got %d", win.Cursor.Col)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("allows cursor at end of line", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"hello"}).
|
||||||
|
Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win.SetCursorCol(5) // After last char
|
||||||
|
|
||||||
|
if win.Cursor.Col != 5 {
|
||||||
|
t.Errorf("expected cursor at col 5, got %d", win.Cursor.Col)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindow_AdjustScroll(t *testing.T) {
|
||||||
|
t.Run("scrolls down when cursor goes below viewport", func(t *testing.T) {
|
||||||
|
// Create buffer with many lines
|
||||||
|
lines := make([]string, 100)
|
||||||
|
for i := range lines {
|
||||||
|
lines[i] = "line"
|
||||||
|
}
|
||||||
|
buf := NewBufferBuilder().WithLines(lines).Build()
|
||||||
|
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithHeight(24).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
// Start at top
|
||||||
|
win.SetCursorLine(0)
|
||||||
|
win.AdjustScroll()
|
||||||
|
initialScroll := win.ScrollY
|
||||||
|
|
||||||
|
// Move cursor way down
|
||||||
|
win.SetCursorLine(50)
|
||||||
|
win.AdjustScroll()
|
||||||
|
|
||||||
|
// Scroll should have increased
|
||||||
|
if win.ScrollY <= initialScroll {
|
||||||
|
t.Errorf("expected scroll to increase, was %d, now %d", initialScroll, win.ScrollY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor should be visible
|
||||||
|
viewport := win.ViewportHeight()
|
||||||
|
if win.Cursor.Line < win.ScrollY || win.Cursor.Line >= win.ScrollY+viewport {
|
||||||
|
t.Errorf("cursor at %d not visible in scroll range [%d, %d)",
|
||||||
|
win.Cursor.Line, win.ScrollY, win.ScrollY+viewport)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("scrolls up when cursor goes above viewport", func(t *testing.T) {
|
||||||
|
lines := make([]string, 100)
|
||||||
|
for i := range lines {
|
||||||
|
lines[i] = "line"
|
||||||
|
}
|
||||||
|
buf := NewBufferBuilder().WithLines(lines).Build()
|
||||||
|
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithHeight(24).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
// Start at bottom
|
||||||
|
win.SetCursorLine(80)
|
||||||
|
win.AdjustScroll()
|
||||||
|
initialScroll := win.ScrollY
|
||||||
|
|
||||||
|
// Move cursor to top
|
||||||
|
win.SetCursorLine(5)
|
||||||
|
win.AdjustScroll()
|
||||||
|
|
||||||
|
// Scroll should have decreased
|
||||||
|
if win.ScrollY >= initialScroll {
|
||||||
|
t.Errorf("expected scroll to decrease, was %d, now %d", initialScroll, win.ScrollY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor should be visible
|
||||||
|
viewport := win.ViewportHeight()
|
||||||
|
if win.Cursor.Line < win.ScrollY || win.Cursor.Line >= win.ScrollY+viewport {
|
||||||
|
t.Errorf("cursor at %d not visible in scroll range [%d, %d)",
|
||||||
|
win.Cursor.Line, win.ScrollY, win.ScrollY+viewport)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("respects scrolloff margin", func(t *testing.T) {
|
||||||
|
lines := make([]string, 100)
|
||||||
|
for i := range lines {
|
||||||
|
lines[i] = "line"
|
||||||
|
}
|
||||||
|
buf := NewBufferBuilder().WithLines(lines).Build()
|
||||||
|
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithHeight(24).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
// Set scrolloff to 5
|
||||||
|
opts := win.Options
|
||||||
|
opts.ScrollOff = 5
|
||||||
|
win.SetOptions(opts)
|
||||||
|
|
||||||
|
// Move to line 20 and adjust
|
||||||
|
win.SetCursorLine(20)
|
||||||
|
win.AdjustScroll()
|
||||||
|
|
||||||
|
viewport := win.ViewportHeight()
|
||||||
|
distFromTop := win.Cursor.Line - win.ScrollY
|
||||||
|
distFromBottom := (win.ScrollY + viewport - 1) - win.Cursor.Line
|
||||||
|
|
||||||
|
// At least one should respect scrolloff
|
||||||
|
if distFromTop < opts.ScrollOff && distFromBottom < opts.ScrollOff {
|
||||||
|
t.Errorf("scrolloff %d not respected: top=%d, bottom=%d",
|
||||||
|
opts.ScrollOff, distFromTop, distFromBottom)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles scrolloff larger than half viewport", func(t *testing.T) {
|
||||||
|
lines := make([]string, 100)
|
||||||
|
for i := range lines {
|
||||||
|
lines[i] = "line"
|
||||||
|
}
|
||||||
|
buf := NewBufferBuilder().WithLines(lines).Build()
|
||||||
|
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithHeight(24).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
// Set scrolloff larger than half viewport
|
||||||
|
opts := win.Options
|
||||||
|
opts.ScrollOff = 999
|
||||||
|
win.SetOptions(opts)
|
||||||
|
|
||||||
|
win.SetCursorLine(50)
|
||||||
|
win.AdjustScroll()
|
||||||
|
|
||||||
|
// Should not panic or error
|
||||||
|
viewport := win.ViewportHeight()
|
||||||
|
if win.Cursor.Line < win.ScrollY || win.Cursor.Line >= win.ScrollY+viewport {
|
||||||
|
t.Error("cursor should still be visible with large scrolloff")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles small viewport", func(t *testing.T) {
|
||||||
|
lines := make([]string, 100)
|
||||||
|
for i := range lines {
|
||||||
|
lines[i] = "line"
|
||||||
|
}
|
||||||
|
buf := NewBufferBuilder().WithLines(lines).Build()
|
||||||
|
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithHeight(5). // Very small
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win.SetCursorLine(50)
|
||||||
|
win.AdjustScroll()
|
||||||
|
|
||||||
|
// Should not panic
|
||||||
|
viewport := win.ViewportHeight()
|
||||||
|
if viewport > 0 && (win.Cursor.Line < win.ScrollY || win.Cursor.Line >= win.ScrollY+viewport) {
|
||||||
|
t.Error("cursor should be visible in small viewport")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindow_ViewportHeight(t *testing.T) {
|
||||||
|
t.Run("calculates viewport height correctly", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithHeight(24).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
// Height - 2 (status bar + command bar)
|
||||||
|
expected := 22
|
||||||
|
if win.ViewportHeight() != expected {
|
||||||
|
t.Errorf("expected viewport height %d, got %d", expected, win.ViewportHeight())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles small window", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithHeight(3).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
// 3 - 2 = 1
|
||||||
|
expected := 1
|
||||||
|
if win.ViewportHeight() != expected {
|
||||||
|
t.Errorf("expected viewport height %d, got %d", expected, win.ViewportHeight())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles zero height", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithHeight(0).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
// With height 0, viewport is 0 - 2 (status + command bars) = -2
|
||||||
|
// This is an edge case that shouldn't occur in practice, but shouldn't panic
|
||||||
|
result := win.ViewportHeight()
|
||||||
|
expected := -2
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("expected viewport height %d for zero height window, got %d", expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindow_SetOptions(t *testing.T) {
|
||||||
|
t.Run("updates options", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
newOpts := WinOptions{
|
||||||
|
Number: false,
|
||||||
|
RelativeNumber: false,
|
||||||
|
GutterSize: 10,
|
||||||
|
ScrollOff: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
win.SetOptions(newOpts)
|
||||||
|
|
||||||
|
if win.Options.Number != false {
|
||||||
|
t.Error("expected Number to be false")
|
||||||
|
}
|
||||||
|
if win.Options.RelativeNumber != false {
|
||||||
|
t.Error("expected RelativeNumber to be false")
|
||||||
|
}
|
||||||
|
if win.Options.GutterSize != 10 {
|
||||||
|
t.Errorf("expected GutterSize 10, got %d", win.Options.GutterSize)
|
||||||
|
}
|
||||||
|
if win.Options.ScrollOff != 3 {
|
||||||
|
t.Errorf("expected ScrollOff 3, got %d", win.Options.ScrollOff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can toggle individual options", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
// Get current options
|
||||||
|
opts := win.Options
|
||||||
|
originalNumber := opts.Number
|
||||||
|
|
||||||
|
// Toggle number
|
||||||
|
opts.Number = !opts.Number
|
||||||
|
win.SetOptions(opts)
|
||||||
|
|
||||||
|
if win.Options.Number == originalNumber {
|
||||||
|
t.Error("Number option should have toggled")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindow_SetAnchor(t *testing.T) {
|
||||||
|
t.Run("sets anchor line", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"a", "b", "c"}).
|
||||||
|
Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win.SetAnchorLine(2)
|
||||||
|
|
||||||
|
if win.Anchor.Line != 2 {
|
||||||
|
t.Errorf("expected anchor line 2, got %d", win.Anchor.Line)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sets anchor col", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"hello world"}).
|
||||||
|
Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win.SetAnchorCol(5)
|
||||||
|
|
||||||
|
if win.Anchor.Col != 5 {
|
||||||
|
t.Errorf("expected anchor col 5, got %d", win.Anchor.Col)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindow_SetDimensions(t *testing.T) {
|
||||||
|
t.Run("updates width and height", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win.SetDimensions(100, 50)
|
||||||
|
|
||||||
|
if win.Width != 100 {
|
||||||
|
t.Errorf("expected width 100, got %d", win.Width)
|
||||||
|
}
|
||||||
|
if win.Height != 50 {
|
||||||
|
t.Errorf("expected height 50, got %d", win.Height)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindowBuilder(t *testing.T) {
|
||||||
|
t.Run("builds with defaults", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
// Should have default options
|
||||||
|
if win.Options.Number != true {
|
||||||
|
t.Error("expected default Number to be true")
|
||||||
|
}
|
||||||
|
if win.Options.RelativeNumber != true {
|
||||||
|
t.Error("expected default RelativeNumber to be true")
|
||||||
|
}
|
||||||
|
if win.Options.ScrollOff != 8 {
|
||||||
|
t.Errorf("expected default ScrollOff 8, got %d", win.Options.ScrollOff)
|
||||||
|
}
|
||||||
|
if win.Options.GutterSize != 5 {
|
||||||
|
t.Errorf("expected default GutterSize 5, got %d", win.Options.GutterSize)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("builds with custom cursor position", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"a", "b", "c"}).
|
||||||
|
Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithCursorPos(2, 0).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
if win.Cursor.Line != 2 {
|
||||||
|
t.Errorf("expected cursor line 2, got %d", win.Cursor.Line)
|
||||||
|
}
|
||||||
|
if win.Cursor.Col != 0 {
|
||||||
|
t.Errorf("expected cursor col 0, got %d", win.Cursor.Col)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("builds with custom dimensions", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithDimensions(120, 40).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
if win.Width != 120 {
|
||||||
|
t.Errorf("expected width 120, got %d", win.Width)
|
||||||
|
}
|
||||||
|
if win.Height != 40 {
|
||||||
|
t.Errorf("expected height 40, got %d", win.Height)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("assigns unique IDs", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().Build()
|
||||||
|
win1 := NewWindowBuilder().WithBuffer(&buf).Build()
|
||||||
|
win2 := NewWindowBuilder().WithBuffer(&buf).Build()
|
||||||
|
|
||||||
|
if win1.Id == win2.Id {
|
||||||
|
t.Errorf("expected unique IDs, both were %d", win1.Id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ package editor
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestHelperExamples demonstrates the different ways to use the test helpers
|
// TestHelperExamples demonstrates the different ways to use the test helpers
|
||||||
@ -19,18 +19,20 @@ func TestHelperExamples(t *testing.T) {
|
|||||||
WithLines([]string{"hello", "world"}),
|
WithLines([]string{"hello", "world"}),
|
||||||
)
|
)
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if len(m.lines) != 2 {
|
buf := m.ActiveBuffer()
|
||||||
t.Errorf("expected 2 lines, got %d", len(m.lines))
|
if buf.LineCount() != 2 {
|
||||||
|
t.Errorf("expected 2 lines, got %d", buf.LineCount())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("custom cursor position", func(t *testing.T) {
|
t.Run("custom cursor position", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithCursorPos(action.Position{Line: 2, Col: 3}),
|
WithCursorPos(core.Position{Line: 2, Col: 3}),
|
||||||
)
|
)
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 2 || m.CursorX() != 3 {
|
win := m.ActiveWindow()
|
||||||
t.Errorf("expected cursor at (2,3), got (%d,%d)", m.CursorY(), m.CursorX())
|
if win.Cursor.Line != 2 || win.Cursor.Col != 3 {
|
||||||
|
t.Errorf("expected cursor at (2,3), got (%d,%d)", win.Cursor.Line, win.Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -39,15 +41,16 @@ func TestHelperExamples(t *testing.T) {
|
|||||||
WithTermSize(120, 40),
|
WithTermSize(120, 40),
|
||||||
)
|
)
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.WinW() != 120 || m.WinH() != 40 {
|
win := m.ActiveWindow()
|
||||||
t.Errorf("expected size 120x40, got %dx%d", m.WinW(), m.WinH())
|
if win.Width != 120 || win.Height != 40 {
|
||||||
|
t.Errorf("expected size 120x40, got %dx%d", win.Width, win.Height)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with register content for paste testing", func(t *testing.T) {
|
t.Run("with register content for paste testing", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithRegister('"', action.CharwiseRegister, []string{"foo"}),
|
WithRegister('"', core.CharwiseRegister, []string{"foo"}),
|
||||||
)
|
)
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
|
|
||||||
@ -55,7 +58,7 @@ func TestHelperExamples(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected register to be set")
|
t.Fatal("expected register to be set")
|
||||||
}
|
}
|
||||||
if reg.Type != action.CharwiseRegister {
|
if reg.Type != core.CharwiseRegister {
|
||||||
t.Errorf("expected charwise register, got %v", reg.Type)
|
t.Errorf("expected charwise register, got %v", reg.Type)
|
||||||
}
|
}
|
||||||
if len(reg.Content) != 1 || reg.Content[0] != "foo" {
|
if len(reg.Content) != 1 || reg.Content[0] != "foo" {
|
||||||
@ -66,25 +69,28 @@ func TestHelperExamples(t *testing.T) {
|
|||||||
t.Run("combine multiple options", func(t *testing.T) {
|
t.Run("combine multiple options", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line one", "line two", "line three"}),
|
WithLines([]string{"line one", "line two", "line three"}),
|
||||||
WithCursorPos(action.Position{Line: 1, Col: 5}),
|
WithCursorPos(core.Position{Line: 1, Col: 5}),
|
||||||
WithTermSize(100, 30),
|
WithTermSize(100, 30),
|
||||||
WithRegister('"', action.LinewiseRegister, []string{"deleted line 1", "deleted line 2"}),
|
WithRegister('"', core.LinewiseRegister, []string{"deleted line 1", "deleted line 2"}),
|
||||||
)
|
)
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
|
|
||||||
// Verify all options were applied
|
// Verify all options were applied
|
||||||
if len(m.Lines()) != 3 {
|
win := m.ActiveWindow()
|
||||||
t.Errorf("expected 3 lines, got %d", len(m.Lines()))
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
if buf.LineCount() != 3 {
|
||||||
|
t.Errorf("expected 3 lines, got %d", buf.LineCount())
|
||||||
}
|
}
|
||||||
if m.CursorY() != 1 || m.CursorX() != 5 {
|
if win.Cursor.Line != 1 || win.Cursor.Col != 5 {
|
||||||
t.Errorf("expected cursor at (1,5), got (%d,%d)", m.CursorY(), m.CursorX())
|
t.Errorf("expected cursor at (1,5), got (%d,%d)", win.Cursor.Line, win.Cursor.Col)
|
||||||
}
|
}
|
||||||
if m.WinW() != 100 || m.WinH() != 30 {
|
if win.Width != 100 || win.Height != 30 {
|
||||||
t.Errorf("expected size 100x30, got %dx%d", m.WinW(), m.WinH())
|
t.Errorf("expected size 100x30, got %dx%d", win.Width, win.Height)
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, ok := m.GetRegister('"')
|
reg, ok := m.GetRegister('"')
|
||||||
if !ok || reg.Type != action.LinewiseRegister {
|
if !ok || reg.Type != core.LinewiseRegister {
|
||||||
t.Error("register not set correctly")
|
t.Error("register not set correctly")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -93,25 +99,29 @@ func TestHelperExamples(t *testing.T) {
|
|||||||
// Old style helpers still work for existing tests
|
// Old style helpers still work for existing tests
|
||||||
tm1 := newTestModelWithLines(t, []string{"a", "b"})
|
tm1 := newTestModelWithLines(t, []string{"a", "b"})
|
||||||
m1 := getFinalModel(t, tm1)
|
m1 := getFinalModel(t, tm1)
|
||||||
if len(m1.Lines()) != 2 {
|
buf1 := m1.ActiveBuffer()
|
||||||
|
if buf1.LineCount() != 2 {
|
||||||
t.Error("newTestModelWithLines failed")
|
t.Error("newTestModelWithLines failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
tm2 := newTestModelWithCursorPos(t, action.Position{Line: 1, Col: 2})
|
tm2 := newTestModelWithCursorPos(t, core.Position{Line: 1, Col: 2})
|
||||||
m2 := getFinalModel(t, tm2)
|
m2 := getFinalModel(t, tm2)
|
||||||
if m2.CursorY() != 1 {
|
win2 := m2.ActiveWindow()
|
||||||
|
if win2.Cursor.Line != 1 {
|
||||||
t.Error("newTestModelWithCursorPos failed")
|
t.Error("newTestModelWithCursorPos failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
tm3 := newTestModelWithLinesAndCursorPos(t, []string{"x"}, action.Position{Line: 0, Col: 0})
|
tm3 := newTestModelWithLinesAndCursorPos(t, []string{"x"}, core.Position{Line: 0, Col: 0})
|
||||||
m3 := getFinalModel(t, tm3)
|
m3 := getFinalModel(t, tm3)
|
||||||
if len(m3.Lines()) != 1 {
|
buf3 := m3.ActiveBuffer()
|
||||||
|
if buf3.LineCount() != 1 {
|
||||||
t.Error("newTestModelWithLinesAndCursorPos failed")
|
t.Error("newTestModelWithLinesAndCursorPos failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
tm4 := newTestModelWithTermSize(t, []string{"y"}, action.Position{Line: 0, Col: 0}, 50, 20)
|
tm4 := newTestModelWithTermSize(t, []string{"y"}, core.Position{Line: 0, Col: 0}, 50, 20)
|
||||||
m4 := getFinalModel(t, tm4)
|
m4 := getFinalModel(t, tm4)
|
||||||
if m4.WinW() != 50 {
|
win4 := m4.ActiveWindow()
|
||||||
|
if win4.Width != 50 {
|
||||||
t.Error("newTestModelWithTermSize failed")
|
t.Error("newTestModelWithTermSize failed")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -4,11 +4,14 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/x/exp/teatest"
|
"github.com/charmbracelet/x/exp/teatest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NOTE: Lots of this actually sucks ass, but it works for now...
|
||||||
|
// TODO: Refactor this to implement the builder pattern
|
||||||
|
|
||||||
// sendKeys sends a sequence of keys to the test model
|
// sendKeys sends a sequence of keys to the test model
|
||||||
func sendKeys(tm *teatest.TestModel, keys ...string) {
|
func sendKeys(tm *teatest.TestModel, keys ...string) {
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
@ -35,16 +38,23 @@ func sendKeys(tm *teatest.TestModel, keys ...string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendKeyString is a convenience function for sending many keys.
|
||||||
|
func sendKeyString(tm *teatest.TestModel, keyString string) {
|
||||||
|
for _, key := range keyString {
|
||||||
|
sendKeys(tm, string(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestModelOption is a functional option for configuring test models
|
// TestModelOption is a functional option for configuring test models
|
||||||
type TestModelOption func(*testModelConfig)
|
type TestModelOption func(*testModelConfig)
|
||||||
|
|
||||||
type testModelConfig struct {
|
type testModelConfig struct {
|
||||||
lines []string
|
lines []string
|
||||||
pos action.Position
|
pos core.Position
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
regName rune
|
regName rune
|
||||||
regType action.RegisterType
|
regType core.RegisterType
|
||||||
regContent []string
|
regContent []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +66,7 @@ func WithLines(lines []string) TestModelOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithCursorPos sets the initial cursor position
|
// WithCursorPos sets the initial cursor position
|
||||||
func WithCursorPos(pos action.Position) TestModelOption {
|
func WithCursorPos(pos core.Position) TestModelOption {
|
||||||
return func(c *testModelConfig) {
|
return func(c *testModelConfig) {
|
||||||
c.pos = pos
|
c.pos = pos
|
||||||
}
|
}
|
||||||
@ -71,7 +81,7 @@ func WithTermSize(width, height int) TestModelOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithRegister sets a register's content
|
// WithRegister sets a register's content
|
||||||
func WithRegister(name rune, regType action.RegisterType, content []string) TestModelOption {
|
func WithRegister(name rune, regType core.RegisterType, content []string) TestModelOption {
|
||||||
return func(c *testModelConfig) {
|
return func(c *testModelConfig) {
|
||||||
c.regName = name
|
c.regName = name
|
||||||
c.regType = regType
|
c.regType = regType
|
||||||
@ -84,7 +94,7 @@ func newTestModel(t *testing.T, opts ...TestModelOption) *teatest.TestModel {
|
|||||||
// Default configuration
|
// Default configuration
|
||||||
cfg := testModelConfig{
|
cfg := testModelConfig{
|
||||||
lines: []string{"line 1", "line 2", "line 3", "line 4", "line 5", "line 6"},
|
lines: []string{"line 1", "line 2", "line 3", "line 4", "line 5", "line 6"},
|
||||||
pos: action.Position{Col: 0, Line: 0},
|
pos: core.Position{Col: 0, Line: 0},
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 24,
|
height: 24,
|
||||||
}
|
}
|
||||||
@ -94,12 +104,31 @@ func newTestModel(t *testing.T, opts ...TestModelOption) *teatest.TestModel {
|
|||||||
opt(&cfg)
|
opt(&cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create model
|
buf := core.NewBufferBuilder().
|
||||||
m := NewModel(cfg.lines, cfg.pos)
|
WithLines(cfg.lines).
|
||||||
|
Build()
|
||||||
|
|
||||||
// Set register if provided
|
win := core.NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithCursorPos(cfg.pos.Line, cfg.pos.Col).
|
||||||
|
WithDimensions(cfg.width, cfg.height).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
// Create model with default registers
|
||||||
|
m := NewModelBuilder().
|
||||||
|
AddBuffer(&buf).
|
||||||
|
AddWindow(&win).
|
||||||
|
WithActiveWindowId(win.Id).
|
||||||
|
WithTermSize(cfg.width, cfg.height).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
// Set register if provided (must be done AFTER Build because SetRegister
|
||||||
|
// requires the register to already exist in the default register map)
|
||||||
if cfg.regContent != nil {
|
if cfg.regContent != nil {
|
||||||
m.SetRegister(cfg.regName, cfg.regType, cfg.regContent)
|
err := m.SetRegister(cfg.regName, cfg.regType, cfg.regContent)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to set register %c: %v", cfg.regName, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return teatest.NewTestModel(t, m, teatest.WithInitialTermSize(cfg.width, cfg.height))
|
return teatest.NewTestModel(t, m, teatest.WithInitialTermSize(cfg.width, cfg.height))
|
||||||
@ -110,21 +139,21 @@ func newTestModelWithLines(t *testing.T, lines []string) *teatest.TestModel {
|
|||||||
return newTestModel(t, WithLines(lines))
|
return newTestModel(t, WithLines(lines))
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestModelWithCursorPos(t *testing.T, pos action.Position) *teatest.TestModel {
|
func newTestModelWithCursorPos(t *testing.T, pos core.Position) *teatest.TestModel {
|
||||||
return newTestModel(t, WithCursorPos(pos))
|
return newTestModel(t, WithCursorPos(pos))
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestModelWithLinesAndCursorPos(t *testing.T, lines []string, pos action.Position) *teatest.TestModel {
|
func newTestModelWithLinesAndCursorPos(t *testing.T, lines []string, pos core.Position) *teatest.TestModel {
|
||||||
return newTestModel(t, WithLines(lines), WithCursorPos(pos))
|
return newTestModel(t, WithLines(lines), WithCursorPos(pos))
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestModelWithTermSize(t *testing.T, lines []string, pos action.Position, width, height int) *teatest.TestModel {
|
func newTestModelWithTermSize(t *testing.T, lines []string, pos core.Position, width, height int) *teatest.TestModel {
|
||||||
return newTestModel(t, WithLines(lines), WithCursorPos(pos), WithTermSize(width, height))
|
return newTestModel(t, WithLines(lines), WithCursorPos(pos), WithTermSize(width, height))
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFinalModel extracts the final model state (sends ctrl+c to quit first)
|
// getFinalModel extracts the final model state (sends ctrl+c to quit first)
|
||||||
func getFinalModel(t *testing.T, tm *teatest.TestModel) Model {
|
func getFinalModel(t *testing.T, tm *teatest.TestModel) *Model {
|
||||||
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlC})
|
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlC})
|
||||||
fm := tm.FinalModel(t, teatest.WithFinalTimeout(time.Second))
|
fm := tm.FinalModel(t, teatest.WithFinalTimeout(time.Second))
|
||||||
return fm.(Model)
|
return fm.(*Model)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,13 +3,11 @@ package editor
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NOTE: AI Generated tests
|
// NOTE: AI Generated tests
|
||||||
|
|
||||||
// Default settings are: Number=true, RelativeNumber=true, TabSize=2, ScrollOff=8
|
|
||||||
|
|
||||||
func TestCommandSetBoolean(t *testing.T) {
|
func TestCommandSetBoolean(t *testing.T) {
|
||||||
t.Run("':set nonumber' disables line numbers", func(t *testing.T) {
|
t.Run("':set nonumber' disables line numbers", func(t *testing.T) {
|
||||||
// Default has Number=true
|
// Default has Number=true
|
||||||
@ -19,7 +17,7 @@ func TestCommandSetBoolean(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "n", "o", "n", "u", "m", "b", "e", "r", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "n", "o", "n", "u", "m", "b", "e", "r", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Settings().Number {
|
if m.ActiveWindow().Options.Number {
|
||||||
t.Error("expected Number to be false after :set nonumber")
|
t.Error("expected Number to be false after :set nonumber")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -33,7 +31,7 @@ func TestCommandSetBoolean(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if !m.Settings().Number {
|
if !m.ActiveWindow().Options.Number {
|
||||||
t.Error("expected Number to be true after :set nu")
|
t.Error("expected Number to be true after :set nu")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -46,7 +44,7 @@ func TestCommandSetBoolean(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "m", "b", "e", "r", "!", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "m", "b", "e", "r", "!", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Settings().Number {
|
if m.ActiveWindow().Options.Number {
|
||||||
t.Error("expected Number to be false after :set number!")
|
t.Error("expected Number to be false after :set number!")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -60,7 +58,7 @@ func TestCommandSetBoolean(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "!", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "!", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if !m.Settings().Number {
|
if !m.ActiveWindow().Options.Number {
|
||||||
t.Error("expected Number to be true after double toggle")
|
t.Error("expected Number to be true after double toggle")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -73,7 +71,7 @@ func TestCommandSetBoolean(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "n", "o", "r", "n", "u", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "n", "o", "r", "n", "u", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Settings().RelativeNumber {
|
if m.ActiveWindow().Options.RelativeNumber {
|
||||||
t.Error("expected RelativeNumber to be false after :set nornu")
|
t.Error("expected RelativeNumber to be false after :set nornu")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -87,7 +85,7 @@ func TestCommandSetBoolean(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "r", "n", "u", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "r", "n", "u", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if !m.Settings().RelativeNumber {
|
if !m.ActiveWindow().Options.RelativeNumber {
|
||||||
t.Error("expected RelativeNumber to be true after :set rnu")
|
t.Error("expected RelativeNumber to be true after :set rnu")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -95,15 +93,15 @@ func TestCommandSetBoolean(t *testing.T) {
|
|||||||
|
|
||||||
func TestCommandSetInteger(t *testing.T) {
|
func TestCommandSetInteger(t *testing.T) {
|
||||||
t.Run("':set tabstop=4' sets tab size", func(t *testing.T) {
|
t.Run("':set tabstop=4' sets tab size", func(t *testing.T) {
|
||||||
// Default TabSize=2
|
// Default TabStop=2
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLines(t, lines)
|
tm := newTestModelWithLines(t, lines)
|
||||||
|
|
||||||
sendKeys(tm, ":", "s", "e", "t", " ", "t", "a", "b", "s", "t", "o", "p", "=", "4", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "t", "a", "b", "s", "t", "o", "p", "=", "4", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Settings().TabSize != 4 {
|
if m.Settings().TabStop != 4 {
|
||||||
t.Errorf("TabSize = %d, want 4", m.Settings().TabSize)
|
t.Errorf("TabStop = %d, want 4", m.Settings().TabStop)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -114,8 +112,8 @@ func TestCommandSetInteger(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "t", "s", "=", "8", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "t", "s", "=", "8", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Settings().TabSize != 8 {
|
if m.Settings().TabStop != 8 {
|
||||||
t.Errorf("TabSize = %d, want 8", m.Settings().TabSize)
|
t.Errorf("TabStop = %d, want 8", m.Settings().TabStop)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -127,8 +125,8 @@ func TestCommandSetInteger(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "s", "c", "r", "o", "l", "l", "o", "f", "f", "=", "5", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "s", "c", "r", "o", "l", "l", "o", "f", "f", "=", "5", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Settings().ScrollOff != 5 {
|
if m.ActiveWindow().Options.ScrollOff != 5 {
|
||||||
t.Errorf("ScrollOff = %d, want 5", m.Settings().ScrollOff)
|
t.Errorf("ScrollOff = %d, want 5", m.ActiveWindow().Options.ScrollOff)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -139,8 +137,8 @@ func TestCommandSetInteger(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "s", "o", "=", "1", "0", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "s", "o", "=", "1", "0", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Settings().ScrollOff != 10 {
|
if m.ActiveWindow().Options.ScrollOff != 10 {
|
||||||
t.Errorf("ScrollOff = %d, want 10", m.Settings().ScrollOff)
|
t.Errorf("ScrollOff = %d, want 10", m.ActiveWindow().Options.ScrollOff)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -153,7 +151,7 @@ func TestCommandModeNavigation(t *testing.T) {
|
|||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// After esc we should be back in normal mode
|
// After esc we should be back in normal mode
|
||||||
if m.Mode() != action.NormalMode {
|
if m.Mode() != core.NormalMode {
|
||||||
t.Errorf("Mode() = %v, want NormalMode after esc", m.Mode())
|
t.Errorf("Mode() = %v, want NormalMode after esc", m.Mode())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -164,7 +162,7 @@ func TestCommandModeNavigation(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "esc")
|
sendKeys(tm, ":", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Mode() != action.NormalMode {
|
if m.Mode() != core.NormalMode {
|
||||||
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
|
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -175,7 +173,7 @@ func TestCommandModeNavigation(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Mode() != action.NormalMode {
|
if m.Mode() != core.NormalMode {
|
||||||
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
|
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -339,3 +337,16 @@ func TestCommandModeErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCommandEdit(t *testing.T) {
|
||||||
|
t.Run(":edit with no args fails", func(t *testing.T) {
|
||||||
|
tm := newTestModel(t)
|
||||||
|
sendKeyString(tm, ":edit")
|
||||||
|
sendKeys(tm, "enter")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.commandError == nil {
|
||||||
|
t.Error("expected commandError to be set for edit without args")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package editor
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDeleteChar(t *testing.T) {
|
func TestDeleteChar(t *testing.T) {
|
||||||
@ -13,30 +13,30 @@ func TestDeleteChar(t *testing.T) {
|
|||||||
sendKeys(tm, "x")
|
sendKeys(tm, "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "ello" {
|
if m.ActiveBuffer().Lines[0] != "ello" {
|
||||||
t.Errorf("lines[0] = %q, want 'ello'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'ello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'x' in middle of line", func(t *testing.T) {
|
t.Run("test 'x' in middle of line", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "x")
|
sendKeys(tm, "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "helo" {
|
if m.ActiveBuffer().Lines[0] != "helo" {
|
||||||
t.Errorf("lines[0] = %q, want 'helo'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'helo'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'x' at end of line", func(t *testing.T) {
|
t.Run("test 'x' at end of line", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
|
||||||
sendKeys(tm, "x")
|
sendKeys(tm, "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "hell" {
|
if m.ActiveBuffer().Lines[0] != "hell" {
|
||||||
t.Errorf("lines[0] = %q, want 'hell'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'hell'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -46,8 +46,8 @@ func TestDeleteChar(t *testing.T) {
|
|||||||
sendKeys(tm, "x", "x")
|
sendKeys(tm, "x", "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "llo" {
|
if m.ActiveBuffer().Lines[0] != "llo" {
|
||||||
t.Errorf("lines[0] = %q, want 'llo'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'llo'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -59,8 +59,8 @@ func TestDeleteCharWithCount(t *testing.T) {
|
|||||||
sendKeys(tm, "3", "x")
|
sendKeys(tm, "3", "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "lo" {
|
if m.ActiveBuffer().Lines[0] != "lo" {
|
||||||
t.Errorf("lines[0] = %q, want 'lo'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'lo'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -70,19 +70,19 @@ func TestDeleteCharWithCount(t *testing.T) {
|
|||||||
sendKeys(tm, "1", "0", "x")
|
sendKeys(tm, "1", "0", "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "" {
|
if m.ActiveBuffer().Lines[0] != "" {
|
||||||
t.Errorf("lines[0] = %q, want ''", m.lines[0])
|
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '2x' from middle", func(t *testing.T) {
|
t.Run("test '2x' from middle", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
|
||||||
sendKeys(tm, "2", "x")
|
sendKeys(tm, "2", "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "hlo" {
|
if m.ActiveBuffer().Lines[0] != "hlo" {
|
||||||
t.Errorf("lines[0] = %q, want 'hlo'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'hlo'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -94,11 +94,11 @@ func TestDeleteCharEdgeCases(t *testing.T) {
|
|||||||
sendKeys(tm, "x")
|
sendKeys(tm, "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "" {
|
if m.ActiveBuffer().Lines[0] != "" {
|
||||||
t.Errorf("Line(0) = %q, want ''", m.Line(0))
|
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -108,30 +108,30 @@ func TestDeleteCharEdgeCases(t *testing.T) {
|
|||||||
sendKeys(tm, "x")
|
sendKeys(tm, "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "" {
|
if m.ActiveBuffer().Lines[0] != "" {
|
||||||
t.Errorf("Line(0) = %q, want ''", m.Line(0))
|
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'x' at last char deletes it", func(t *testing.T) {
|
t.Run("test 'x' at last char deletes it", func(t *testing.T) {
|
||||||
lines := []string{"ab"}
|
lines := []string{"ab"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
|
||||||
sendKeys(tm, "x")
|
sendKeys(tm, "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "a" {
|
if m.ActiveBuffer().Lines[0] != "a" {
|
||||||
t.Errorf("Line(0) = %q, want 'a'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'a'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'x' with whitespace", func(t *testing.T) {
|
t.Run("test 'x' with whitespace", func(t *testing.T) {
|
||||||
lines := []string{"a b c"}
|
lines := []string{"a b c"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
|
||||||
sendKeys(tm, "x")
|
sendKeys(tm, "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "ab c" {
|
if m.ActiveBuffer().Lines[0] != "ab c" {
|
||||||
t.Errorf("Line(0) = %q, want 'ab c'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'ab c'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -141,11 +141,11 @@ func TestDeleteCharEdgeCases(t *testing.T) {
|
|||||||
sendKeys(tm, "x")
|
sendKeys(tm, "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 2 {
|
if m.ActiveBuffer().LineCount() != 2 {
|
||||||
t.Errorf("LineCount() = %d, want 2", m.LineCount())
|
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(1) != "world" {
|
if m.ActiveBuffer().Lines[1] != "world" {
|
||||||
t.Errorf("Line(1) = %q, want 'world'", m.Line(1))
|
t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -155,19 +155,19 @@ func TestDeleteCharEdgeCases(t *testing.T) {
|
|||||||
sendKeys(tm, "x", "x", "x")
|
sendKeys(tm, "x", "x", "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "lo" {
|
if m.ActiveBuffer().Lines[0] != "lo" {
|
||||||
t.Errorf("Line(0) = %q, want 'lo'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'lo'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'x' on line with tabs", func(t *testing.T) {
|
t.Run("test 'x' on line with tabs", func(t *testing.T) {
|
||||||
lines := []string{"a\tb"}
|
lines := []string{"a\tb"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
|
||||||
sendKeys(tm, "x")
|
sendKeys(tm, "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "ab" {
|
if m.ActiveBuffer().Lines[0] != "ab" {
|
||||||
t.Errorf("Line(0) = %q, want 'ab'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'ab'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -177,19 +177,19 @@ func TestDeleteCharEdgeCases(t *testing.T) {
|
|||||||
sendKeys(tm, "5", "x")
|
sendKeys(tm, "5", "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "" {
|
if m.ActiveBuffer().Lines[0] != "" {
|
||||||
t.Errorf("Line(0) = %q, want ''", m.Line(0))
|
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'x' in middle preserves surrounding chars", func(t *testing.T) {
|
t.Run("test 'x' in middle preserves surrounding chars", func(t *testing.T) {
|
||||||
lines := []string{"abcde"}
|
lines := []string{"abcde"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "x")
|
sendKeys(tm, "x")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "abde" {
|
if m.ActiveBuffer().Lines[0] != "abde" {
|
||||||
t.Errorf("Line(0) = %q, want 'abde'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'abde'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -197,12 +197,12 @@ func TestDeleteCharEdgeCases(t *testing.T) {
|
|||||||
func TestDeleteToEndOfLine(t *testing.T) {
|
func TestDeleteToEndOfLine(t *testing.T) {
|
||||||
t.Run("test 'D' deletes to end of line", func(t *testing.T) {
|
t.Run("test 'D' deletes to end of line", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "hello" {
|
if m.ActiveBuffer().Lines[0] != "hello" {
|
||||||
t.Errorf("Line(0) = %q, want 'hello'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'hello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -212,31 +212,31 @@ func TestDeleteToEndOfLine(t *testing.T) {
|
|||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "" {
|
if m.ActiveBuffer().Lines[0] != "" {
|
||||||
t.Errorf("Line(0) = %q, want ''", m.Line(0))
|
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'D' at last character", func(t *testing.T) {
|
t.Run("test 'D' at last character", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
|
||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "hell" {
|
if m.ActiveBuffer().Lines[0] != "hell" {
|
||||||
t.Errorf("Line(0) = %q, want 'hell'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'hell'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'D' cursor position after delete", func(t *testing.T) {
|
t.Run("test 'D' cursor position after delete", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Cursor should move to last character of remaining text
|
// Cursor should move to last character of remaining text
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -246,39 +246,39 @@ func TestDeleteToEndOfLine(t *testing.T) {
|
|||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "" {
|
if m.ActiveBuffer().Lines[0] != "" {
|
||||||
t.Errorf("Line(0) = %q, want ''", m.Line(0))
|
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'D' with count deletes following lines", func(t *testing.T) {
|
t.Run("test 'D' with count deletes following lines", func(t *testing.T) {
|
||||||
lines := []string{"hello", "world", "hi", "mom"}
|
lines := []string{"hello", "world", "hi", "mom"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "2", "D")
|
sendKeys(tm, "2", "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 3 {
|
if m.ActiveBuffer().LineCount() != 3 {
|
||||||
t.Errorf("LineCount() = %q, want '3'", m.LineCount())
|
t.Errorf("LineCount() = %q, want '3'", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(0) != "he" {
|
if m.ActiveBuffer().Lines[0] != "he" {
|
||||||
t.Errorf("Line(0) = %q, want 'he'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'he'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.Line(1) != "hi" {
|
if m.ActiveBuffer().Lines[1] != "hi" {
|
||||||
t.Errorf("Line(1) = %q, want 'hi'", m.Line(1))
|
t.Errorf("Line(1) = %q, want 'hi'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'D' with count deletes following lines with overflow", func(t *testing.T) {
|
t.Run("test 'D' with count deletes following lines with overflow", func(t *testing.T) {
|
||||||
lines := []string{"hello", "world", "hi", "mom"}
|
lines := []string{"hello", "world", "hi", "mom"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "8", "D")
|
sendKeys(tm, "8", "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 1 {
|
if m.ActiveBuffer().LineCount() != 1 {
|
||||||
t.Errorf("LineCount() = %q, want '1'", m.LineCount())
|
t.Errorf("LineCount() = %q, want '1'", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(0) != "he" {
|
if m.ActiveBuffer().Lines[0] != "he" {
|
||||||
t.Errorf("Line(0) = %q, want 'he'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'he'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -290,51 +290,51 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
|
|||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "" {
|
if m.ActiveBuffer().Lines[0] != "" {
|
||||||
t.Errorf("Line(0) = %q, want ''", m.Line(0))
|
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'D' at end of file", func(t *testing.T) {
|
t.Run("test 'D' at end of file", func(t *testing.T) {
|
||||||
lines := []string{"line 1", "line 2"}
|
lines := []string{"line 1", "line 2"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 2 {
|
if m.ActiveBuffer().LineCount() != 2 {
|
||||||
t.Errorf("LineCount() = %d, want 2", m.LineCount())
|
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(1) != "" {
|
if m.ActiveBuffer().Lines[1] != "" {
|
||||||
t.Errorf("Line(1) = %q, want ''", m.Line(1))
|
t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'D' preserves lines above", func(t *testing.T) {
|
t.Run("test 'D' preserves lines above", func(t *testing.T) {
|
||||||
lines := []string{"line 1", "line 2", "line 3"}
|
lines := []string{"line 1", "line 2", "line 3"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "line 1" {
|
if m.ActiveBuffer().Lines[0] != "line 1" {
|
||||||
t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.Line(2) != "line 3" {
|
if m.ActiveBuffer().Lines[2] != "line 3" {
|
||||||
t.Errorf("Line(2) = %q, want 'line 3'", m.Line(2))
|
t.Errorf("Line(2) = %q, want 'line 3'", m.ActiveBuffer().Lines[2])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'D' cursor clamps to valid position", func(t *testing.T) {
|
t.Run("test 'D' cursor clamps to valid position", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "he" {
|
if m.ActiveBuffer().Lines[0] != "he" {
|
||||||
t.Errorf("Line(0) = %q, want 'he'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'he'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
// Cursor should clamp to last char
|
// Cursor should clamp to last char
|
||||||
if m.CursorX() != 1 {
|
if m.ActiveWindow().Cursor.Col != 1 {
|
||||||
t.Errorf("CursorX() = %d, want 1", m.CursorX())
|
t.Errorf("CursorX() = %d, want 1", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -344,77 +344,77 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
|
|||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "" {
|
if m.ActiveBuffer().Lines[0] != "" {
|
||||||
t.Errorf("Line(0) = %q, want ''", m.Line(0))
|
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'D' from middle of whitespace-only line", func(t *testing.T) {
|
t.Run("test 'D' from middle of whitespace-only line", func(t *testing.T) {
|
||||||
lines := []string{" "}
|
lines := []string{" "}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != " " {
|
if m.ActiveBuffer().Lines[0] != " " {
|
||||||
t.Errorf("Line(0) = %q, want ' '", m.Line(0))
|
t.Errorf("Line(0) = %q, want ' '", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'D' with tabs", func(t *testing.T) {
|
t.Run("test 'D' with tabs", func(t *testing.T) {
|
||||||
lines := []string{"hello\tworld"}
|
lines := []string{"hello\tworld"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "hello" {
|
if m.ActiveBuffer().Lines[0] != "hello" {
|
||||||
t.Errorf("Line(0) = %q, want 'hello'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'hello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'D' on line with only one char remaining after cursor", func(t *testing.T) {
|
t.Run("test 'D' on line with only one char remaining after cursor", func(t *testing.T) {
|
||||||
lines := []string{"ab"}
|
lines := []string{"ab"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
|
||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "a" {
|
if m.ActiveBuffer().Lines[0] != "a" {
|
||||||
t.Errorf("Line(0) = %q, want 'a'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'a'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'D' does not affect line below", func(t *testing.T) {
|
t.Run("test 'D' does not affect line below", func(t *testing.T) {
|
||||||
lines := []string{"hello", "world"}
|
lines := []string{"hello", "world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(1) != "world" {
|
if m.ActiveBuffer().Lines[1] != "world" {
|
||||||
t.Errorf("Line(1) = %q, want 'world'", m.Line(1))
|
t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'D' with multiple lines", func(t *testing.T) {
|
t.Run("test 'D' with multiple lines", func(t *testing.T) {
|
||||||
lines := []string{"first line", "second line", "third line"}
|
lines := []string{"first line", "second line", "third line"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "first" {
|
if m.ActiveBuffer().Lines[0] != "first" {
|
||||||
t.Errorf("Line(0) = %q, want 'first'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'first'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.LineCount() != 3 {
|
if m.ActiveBuffer().LineCount() != 3 {
|
||||||
t.Errorf("LineCount() = %d, want 3", m.LineCount())
|
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'D' preserves cursor Y position", func(t *testing.T) {
|
t.Run("test 'D' preserves cursor Y position", func(t *testing.T) {
|
||||||
lines := []string{"line 1", "line 2", "line 3"}
|
lines := []string{"line 1", "line 2", "line 3"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 1})
|
||||||
sendKeys(tm, "D")
|
sendKeys(tm, "D")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 1 {
|
if m.ActiveWindow().Cursor.Line != 1 {
|
||||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package editor
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Insert Mode Entry Tests ---
|
// --- Insert Mode Entry Tests ---
|
||||||
@ -14,8 +14,8 @@ func TestEnterInsert(t *testing.T) {
|
|||||||
sendKeys(tm, "i")
|
sendKeys(tm, "i")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.mode != action.InsertMode {
|
if m.mode != core.InsertMode {
|
||||||
t.Errorf("mode = %d, want InsertMode (%d)", m.mode, action.InsertMode)
|
t.Errorf("mode = %d, want InsertMode (%d)", m.mode, core.InsertMode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -25,30 +25,30 @@ func TestEnterInsert(t *testing.T) {
|
|||||||
sendKeys(tm, "i", "X", "esc")
|
sendKeys(tm, "i", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "Xhello" {
|
if m.ActiveBuffer().Lines[0] != "Xhello" {
|
||||||
t.Errorf("lines[0] = %q, want 'Xhello'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'Xhello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'i' insert in middle", func(t *testing.T) {
|
t.Run("test 'i' insert in middle", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "i", "X", "esc")
|
sendKeys(tm, "i", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "heXllo" {
|
if m.ActiveBuffer().Lines[0] != "heXllo" {
|
||||||
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'i' cursor moves back on esc", func(t *testing.T) {
|
t.Run("test 'i' cursor moves back on esc", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "i", "X", "esc")
|
sendKeys(tm, "i", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.x != 2 {
|
if m.ActiveWindow().Cursor.Col != 2 {
|
||||||
t.Errorf("cursor.x = %d, want 2", m.cursor.x)
|
t.Errorf("cursor.x = %d, want 2", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -59,8 +59,8 @@ func TestEnterInsertAfter(t *testing.T) {
|
|||||||
sendKeys(tm, "a")
|
sendKeys(tm, "a")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.mode != action.InsertMode {
|
if m.mode != core.InsertMode {
|
||||||
t.Errorf("mode = %d, want InsertMode (%d)", m.mode, action.InsertMode)
|
t.Errorf("mode = %d, want InsertMode (%d)", m.mode, core.InsertMode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -70,19 +70,19 @@ func TestEnterInsertAfter(t *testing.T) {
|
|||||||
sendKeys(tm, "a", "X", "esc")
|
sendKeys(tm, "a", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "hXello" {
|
if m.ActiveBuffer().Lines[0] != "hXello" {
|
||||||
t.Errorf("lines[0] = %q, want 'hXello'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'hXello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'a' from middle of line", func(t *testing.T) {
|
t.Run("test 'a' from middle of line", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "a", "X", "esc")
|
sendKeys(tm, "a", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "helXlo" {
|
if m.ActiveBuffer().Lines[0] != "helXlo" {
|
||||||
t.Errorf("lines[0] = %q, want 'helXlo'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'helXlo'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -90,23 +90,23 @@ func TestEnterInsertAfter(t *testing.T) {
|
|||||||
func TestEnterInsertLineStart(t *testing.T) {
|
func TestEnterInsertLineStart(t *testing.T) {
|
||||||
t.Run("test 'I' enters insert mode at line start", func(t *testing.T) {
|
t.Run("test 'I' enters insert mode at line start", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||||
sendKeys(tm, "I", "X", "esc")
|
sendKeys(tm, "I", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "Xhello" {
|
if m.ActiveBuffer().Lines[0] != "Xhello" {
|
||||||
t.Errorf("lines[0] = %q, want 'Xhello'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'Xhello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'I' from end of line", func(t *testing.T) {
|
t.Run("test 'I' from end of line", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "I", "X", "esc")
|
sendKeys(tm, "I", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "Xhello" {
|
if m.ActiveBuffer().Lines[0] != "Xhello" {
|
||||||
t.Errorf("lines[0] = %q, want 'Xhello'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'Xhello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -118,19 +118,19 @@ func TestEnterInsertLineEnd(t *testing.T) {
|
|||||||
sendKeys(tm, "A", "X", "esc")
|
sendKeys(tm, "A", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "helloX" {
|
if m.ActiveBuffer().Lines[0] != "helloX" {
|
||||||
t.Errorf("lines[0] = %q, want 'helloX'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'helloX'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'A' from middle of line", func(t *testing.T) {
|
t.Run("test 'A' from middle of line", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "A", "X", "esc")
|
sendKeys(tm, "A", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "helloX" {
|
if m.ActiveBuffer().Lines[0] != "helloX" {
|
||||||
t.Errorf("lines[0] = %q, want 'helloX'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'helloX'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -144,39 +144,39 @@ func TestOpenLineBelow(t *testing.T) {
|
|||||||
sendKeys(tm, "o", "n", "e", "w", "esc")
|
sendKeys(tm, "o", "n", "e", "w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if len(m.lines) != 3 {
|
if m.ActiveBuffer().LineCount() != 3 {
|
||||||
t.Errorf("len(lines) = %d, want 3", len(m.lines))
|
t.Errorf("len(lines) = %d, want 3", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.lines[1] != "new" {
|
if m.ActiveBuffer().Lines[1] != "new" {
|
||||||
t.Errorf("lines[1] = %q, want 'new'", m.lines[1])
|
t.Errorf("lines[1] = %q, want 'new'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'o' from middle of file", func(t *testing.T) {
|
t.Run("test 'o' from middle of file", func(t *testing.T) {
|
||||||
lines := []string{"line 1", "line 2", "line 3"}
|
lines := []string{"line 1", "line 2", "line 3"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
||||||
sendKeys(tm, "o", "n", "e", "w", "esc")
|
sendKeys(tm, "o", "n", "e", "w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if len(m.lines) != 4 {
|
if m.ActiveBuffer().LineCount() != 4 {
|
||||||
t.Errorf("len(lines) = %d, want 4", len(m.lines))
|
t.Errorf("len(lines) = %d, want 4", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.lines[2] != "new" {
|
if m.ActiveBuffer().Lines[2] != "new" {
|
||||||
t.Errorf("lines[2] = %q, want 'new'", m.lines[2])
|
t.Errorf("lines[2] = %q, want 'new'", m.ActiveBuffer().Lines[2])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'o' at end of file", func(t *testing.T) {
|
t.Run("test 'o' at end of file", func(t *testing.T) {
|
||||||
lines := []string{"line 1", "line 2"}
|
lines := []string{"line 1", "line 2"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
||||||
sendKeys(tm, "o", "n", "e", "w", "esc")
|
sendKeys(tm, "o", "n", "e", "w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if len(m.lines) != 3 {
|
if m.ActiveBuffer().LineCount() != 3 {
|
||||||
t.Errorf("len(lines) = %d, want 3", len(m.lines))
|
t.Errorf("len(lines) = %d, want 3", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.lines[2] != "new" {
|
if m.ActiveBuffer().Lines[2] != "new" {
|
||||||
t.Errorf("lines[2] = %q, want 'new'", m.lines[2])
|
t.Errorf("lines[2] = %q, want 'new'", m.ActiveBuffer().Lines[2])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -186,11 +186,11 @@ func TestOpenLineBelow(t *testing.T) {
|
|||||||
sendKeys(tm, "o", "esc")
|
sendKeys(tm, "o", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.y != 1 {
|
if m.ActiveWindow().Cursor.Line != 1 {
|
||||||
t.Errorf("cursor.y = %d, want 1", m.cursor.y)
|
t.Errorf("cursor.y = %d, want 1", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
if m.cursor.x != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
|
t.Errorf("cursor.x = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -202,12 +202,12 @@ func TestOpenLineBelowWithCount(t *testing.T) {
|
|||||||
sendKeys(tm, "3", "o", "x", "esc")
|
sendKeys(tm, "3", "o", "x", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if len(m.lines) != 4 {
|
if m.ActiveBuffer().LineCount() != 4 {
|
||||||
t.Errorf("len(lines) = %d, want 4", len(m.lines))
|
t.Errorf("len(lines) = %d, want 4", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
for i := 1; i <= 3; i++ {
|
for i := 1; i <= 3; i++ {
|
||||||
if m.lines[i] != "x" {
|
if m.ActiveBuffer().Lines[i] != "x" {
|
||||||
t.Errorf("lines[%d] = %q, want 'x'", i, m.lines[i])
|
t.Errorf("lines[%d] = %q, want 'x'", i, m.ActiveBuffer().Lines[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -218,14 +218,14 @@ func TestOpenLineBelowWithCount(t *testing.T) {
|
|||||||
sendKeys(tm, "2", "o", "a", "b", "esc")
|
sendKeys(tm, "2", "o", "a", "b", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if len(m.lines) != 3 {
|
if m.ActiveBuffer().LineCount() != 3 {
|
||||||
t.Errorf("len(lines) = %d, want 3", len(m.lines))
|
t.Errorf("len(lines) = %d, want 3", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.lines[1] != "ab" {
|
if m.ActiveBuffer().Lines[1] != "ab" {
|
||||||
t.Errorf("lines[1] = %q, want 'ab'", m.lines[1])
|
t.Errorf("lines[1] = %q, want 'ab'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
if m.lines[2] != "ab" {
|
if m.ActiveBuffer().Lines[2] != "ab" {
|
||||||
t.Errorf("lines[2] = %q, want 'ab'", m.lines[2])
|
t.Errorf("lines[2] = %q, want 'ab'", m.ActiveBuffer().Lines[2])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -233,15 +233,15 @@ func TestOpenLineBelowWithCount(t *testing.T) {
|
|||||||
func TestOpenLineAbove(t *testing.T) {
|
func TestOpenLineAbove(t *testing.T) {
|
||||||
t.Run("test 'O' creates line above", func(t *testing.T) {
|
t.Run("test 'O' creates line above", func(t *testing.T) {
|
||||||
lines := []string{"line 1", "line 2"}
|
lines := []string{"line 1", "line 2"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
||||||
sendKeys(tm, "O", "n", "e", "w", "esc")
|
sendKeys(tm, "O", "n", "e", "w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if len(m.lines) != 3 {
|
if m.ActiveBuffer().LineCount() != 3 {
|
||||||
t.Errorf("len(lines) = %d, want 3", len(m.lines))
|
t.Errorf("len(lines) = %d, want 3", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.lines[1] != "new" {
|
if m.ActiveBuffer().Lines[1] != "new" {
|
||||||
t.Errorf("lines[1] = %q, want 'new'", m.lines[1])
|
t.Errorf("lines[1] = %q, want 'new'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -251,22 +251,22 @@ func TestOpenLineAbove(t *testing.T) {
|
|||||||
sendKeys(tm, "O", "n", "e", "w", "esc")
|
sendKeys(tm, "O", "n", "e", "w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if len(m.lines) != 3 {
|
if m.ActiveBuffer().LineCount() != 3 {
|
||||||
t.Errorf("len(lines) = %d, want 3", len(m.lines))
|
t.Errorf("len(lines) = %d, want 3", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.lines[0] != "new" {
|
if m.ActiveBuffer().Lines[0] != "new" {
|
||||||
t.Errorf("lines[0] = %q, want 'new'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'new'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'O' cursor at start of new line", func(t *testing.T) {
|
t.Run("test 'O' cursor at start of new line", func(t *testing.T) {
|
||||||
lines := []string{"line 1", "line 2"}
|
lines := []string{"line 1", "line 2"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 1})
|
||||||
sendKeys(tm, "O", "esc")
|
sendKeys(tm, "O", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.x != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
|
t.Errorf("cursor.x = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -274,16 +274,16 @@ func TestOpenLineAbove(t *testing.T) {
|
|||||||
func TestOpenLineAboveWithCount(t *testing.T) {
|
func TestOpenLineAboveWithCount(t *testing.T) {
|
||||||
t.Run("test '3O' creates 3 lines above", func(t *testing.T) {
|
t.Run("test '3O' creates 3 lines above", func(t *testing.T) {
|
||||||
lines := []string{"line 1"}
|
lines := []string{"line 1"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "3", "O", "x", "esc")
|
sendKeys(tm, "3", "O", "x", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if len(m.lines) != 4 {
|
if m.ActiveBuffer().LineCount() != 4 {
|
||||||
t.Errorf("len(lines) = %d, want 4", len(m.lines))
|
t.Errorf("len(lines) = %d, want 4", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
if m.lines[i] != "x" {
|
if m.ActiveBuffer().Lines[i] != "x" {
|
||||||
t.Errorf("lines[%d] = %q, want 'x'", i, m.lines[i])
|
t.Errorf("lines[%d] = %q, want 'x'", i, m.ActiveBuffer().Lines[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -294,35 +294,35 @@ func TestOpenLineAboveWithCount(t *testing.T) {
|
|||||||
func TestInsertModeEnter(t *testing.T) {
|
func TestInsertModeEnter(t *testing.T) {
|
||||||
t.Run("test enter splits line", func(t *testing.T) {
|
t.Run("test enter splits line", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "i", "enter", "esc")
|
sendKeys(tm, "i", "enter", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if len(m.lines) != 2 {
|
if m.ActiveBuffer().LineCount() != 2 {
|
||||||
t.Errorf("len(lines) = %d, want 2", len(m.lines))
|
t.Errorf("len(lines) = %d, want 2", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.lines[0] != "hello" {
|
if m.ActiveBuffer().Lines[0] != "hello" {
|
||||||
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.lines[1] != " world" {
|
if m.ActiveBuffer().Lines[1] != " world" {
|
||||||
t.Errorf("lines[1] = %q, want ' world'", m.lines[1])
|
t.Errorf("lines[1] = %q, want ' world'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test enter at end of line", func(t *testing.T) {
|
t.Run("test enter at end of line", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "i", "enter", "esc")
|
sendKeys(tm, "i", "enter", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if len(m.lines) != 2 {
|
if m.ActiveBuffer().LineCount() != 2 {
|
||||||
t.Errorf("len(lines) = %d, want 2", len(m.lines))
|
t.Errorf("len(lines) = %d, want 2", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.lines[0] != "hello" {
|
if m.ActiveBuffer().Lines[0] != "hello" {
|
||||||
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.lines[1] != "" {
|
if m.ActiveBuffer().Lines[1] != "" {
|
||||||
t.Errorf("lines[1] = %q, want ''", m.lines[1])
|
t.Errorf("lines[1] = %q, want ''", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -332,14 +332,14 @@ func TestInsertModeEnter(t *testing.T) {
|
|||||||
sendKeys(tm, "i", "enter", "esc")
|
sendKeys(tm, "i", "enter", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if len(m.lines) != 2 {
|
if m.ActiveBuffer().LineCount() != 2 {
|
||||||
t.Errorf("len(lines) = %d, want 2", len(m.lines))
|
t.Errorf("len(lines) = %d, want 2", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.lines[0] != "" {
|
if m.ActiveBuffer().Lines[0] != "" {
|
||||||
t.Errorf("lines[0] = %q, want ''", m.lines[0])
|
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.lines[1] != "hello" {
|
if m.ActiveBuffer().Lines[1] != "hello" {
|
||||||
t.Errorf("lines[1] = %q, want 'hello'", m.lines[1])
|
t.Errorf("lines[1] = %q, want 'hello'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -347,26 +347,26 @@ func TestInsertModeEnter(t *testing.T) {
|
|||||||
func TestInsertModeBackspace(t *testing.T) {
|
func TestInsertModeBackspace(t *testing.T) {
|
||||||
t.Run("test backspace deletes character", func(t *testing.T) {
|
t.Run("test backspace deletes character", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||||
sendKeys(tm, "i", "backspace", "esc")
|
sendKeys(tm, "i", "backspace", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "helo" {
|
if m.ActiveBuffer().Lines[0] != "helo" {
|
||||||
t.Errorf("lines[0] = %q, want 'helo'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'helo'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test backspace at start of line joins lines", func(t *testing.T) {
|
t.Run("test backspace at start of line joins lines", func(t *testing.T) {
|
||||||
lines := []string{"hello", "world"}
|
lines := []string{"hello", "world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
||||||
sendKeys(tm, "i", "backspace", "esc")
|
sendKeys(tm, "i", "backspace", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if len(m.lines) != 1 {
|
if m.ActiveBuffer().LineCount() != 1 {
|
||||||
t.Errorf("len(lines) = %d, want 1", len(m.lines))
|
t.Errorf("len(lines) = %d, want 1", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.lines[0] != "helloworld" {
|
if m.ActiveBuffer().Lines[0] != "helloworld" {
|
||||||
t.Errorf("lines[0] = %q, want 'helloworld'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'helloworld'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -376,19 +376,19 @@ func TestInsertModeBackspace(t *testing.T) {
|
|||||||
sendKeys(tm, "i", "backspace", "esc")
|
sendKeys(tm, "i", "backspace", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "hello" {
|
if m.ActiveBuffer().Lines[0] != "hello" {
|
||||||
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test multiple backspaces", func(t *testing.T) {
|
t.Run("test multiple backspaces", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "i", "backspace", "backspace", "backspace", "esc")
|
sendKeys(tm, "i", "backspace", "backspace", "backspace", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "he" {
|
if m.ActiveBuffer().Lines[0] != "he" {
|
||||||
t.Errorf("lines[0] = %q, want 'he'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'he'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -396,26 +396,26 @@ func TestInsertModeBackspace(t *testing.T) {
|
|||||||
func TestInsertModeDelete(t *testing.T) {
|
func TestInsertModeDelete(t *testing.T) {
|
||||||
t.Run("test delete deletes character", func(t *testing.T) {
|
t.Run("test delete deletes character", func(t *testing.T) {
|
||||||
lines := []string{"world"}
|
lines := []string{"world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||||
sendKeys(tm, "i", "delete", "esc")
|
sendKeys(tm, "i", "delete", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "word" {
|
if m.ActiveBuffer().Lines[0] != "word" {
|
||||||
t.Errorf("lines[0] = %q, want 'word'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'word'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test delete at end of line joins lines", func(t *testing.T) {
|
t.Run("test delete at end of line joins lines", func(t *testing.T) {
|
||||||
lines := []string{"hello", "world"}
|
lines := []string{"hello", "world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "i", "delete", "esc")
|
sendKeys(tm, "i", "delete", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if len(m.lines) != 1 {
|
if m.ActiveBuffer().LineCount() != 1 {
|
||||||
t.Errorf("len(lines) = %d, want 1", len(m.lines))
|
t.Errorf("len(lines) = %d, want 1", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.lines[0] != "helloworld" {
|
if m.ActiveBuffer().Lines[0] != "helloworld" {
|
||||||
t.Errorf("lines[0] = %q, want 'helloworld'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'helloworld'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -425,33 +425,33 @@ func TestInsertModeDelete(t *testing.T) {
|
|||||||
sendKeys(tm, "i", "delete", "esc")
|
sendKeys(tm, "i", "delete", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if len(m.lines) != 1 {
|
if m.ActiveBuffer().LineCount() != 1 {
|
||||||
t.Errorf("len(lines) = %d, want 1", len(m.lines))
|
t.Errorf("len(lines) = %d, want 1", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.lines[0] != "world" {
|
if m.ActiveBuffer().Lines[0] != "world" {
|
||||||
t.Errorf("lines[0] = %q, want 'world'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'world'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test delete at end of last line does nothing", func(t *testing.T) {
|
t.Run("test delete at end of last line does nothing", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "i", "delete", "esc")
|
sendKeys(tm, "i", "delete", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "hello" {
|
if m.ActiveBuffer().Lines[0] != "hello" {
|
||||||
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test multiple delete", func(t *testing.T) {
|
t.Run("test multiple delete", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
|
||||||
sendKeys(tm, "i", "delete", "delete", "delete", "esc")
|
sendKeys(tm, "i", "delete", "delete", "delete", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "ho" {
|
if m.ActiveBuffer().Lines[0] != "ho" {
|
||||||
t.Errorf("lines[0] = %q, want 'he'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'he'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -460,51 +460,51 @@ func TestInsertModeDelete(t *testing.T) {
|
|||||||
func TestInsertModeArrowKeys(t *testing.T) {
|
func TestInsertModeArrowKeys(t *testing.T) {
|
||||||
t.Run("test left arrow moves cursor left", func(t *testing.T) {
|
t.Run("test left arrow moves cursor left", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||||
sendKeys(tm, "i", "left", "X", "esc")
|
sendKeys(tm, "i", "left", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "heXllo" {
|
if m.ActiveBuffer().Lines[0] != "heXllo" {
|
||||||
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test right arrow moves cursor right", func(t *testing.T) {
|
t.Run("test right arrow moves cursor right", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
|
||||||
sendKeys(tm, "i", "right", "X", "esc")
|
sendKeys(tm, "i", "right", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "heXllo" {
|
if m.ActiveBuffer().Lines[0] != "heXllo" {
|
||||||
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test up arrow moves cursor up", func(t *testing.T) {
|
t.Run("test up arrow moves cursor up", func(t *testing.T) {
|
||||||
lines := []string{"hello", "world"}
|
lines := []string{"hello", "world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 1})
|
||||||
sendKeys(tm, "i", "up", "X", "esc")
|
sendKeys(tm, "i", "up", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "heXllo" {
|
if m.ActiveBuffer().Lines[0] != "heXllo" {
|
||||||
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.lines[1] != "world" {
|
if m.ActiveBuffer().Lines[1] != "world" {
|
||||||
t.Errorf("lines[1] = %q, want 'world'", m.lines[1])
|
t.Errorf("lines[1] = %q, want 'world'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test down arrow moves cursor down", func(t *testing.T) {
|
t.Run("test down arrow moves cursor down", func(t *testing.T) {
|
||||||
lines := []string{"hello", "world"}
|
lines := []string{"hello", "world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "i", "down", "X", "esc")
|
sendKeys(tm, "i", "down", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "hello" {
|
if m.ActiveBuffer().Lines[0] != "hello" {
|
||||||
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.lines[1] != "woXrld" {
|
if m.ActiveBuffer().Lines[1] != "woXrld" {
|
||||||
t.Errorf("lines[1] = %q, want 'woXrld'", m.lines[1])
|
t.Errorf("lines[1] = %q, want 'woXrld'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -514,63 +514,63 @@ func TestInsertModeArrowKeys(t *testing.T) {
|
|||||||
sendKeys(tm, "i", "left", "X", "esc")
|
sendKeys(tm, "i", "left", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "Xhello" {
|
if m.ActiveBuffer().Lines[0] != "Xhello" {
|
||||||
t.Errorf("lines[0] = %q, want 'Xhello'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'Xhello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test right arrow at end of line does nothing", func(t *testing.T) {
|
t.Run("test right arrow at end of line does nothing", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
|
||||||
sendKeys(tm, "a", "right", "X", "esc")
|
sendKeys(tm, "a", "right", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "helloX" {
|
if m.ActiveBuffer().Lines[0] != "helloX" {
|
||||||
t.Errorf("lines[0] = %q, want 'helloX'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'helloX'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test up arrow at first line does nothing", func(t *testing.T) {
|
t.Run("test up arrow at first line does nothing", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "i", "up", "X", "esc")
|
sendKeys(tm, "i", "up", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "heXllo" {
|
if m.ActiveBuffer().Lines[0] != "heXllo" {
|
||||||
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test down arrow at last line does nothing", func(t *testing.T) {
|
t.Run("test down arrow at last line does nothing", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "i", "down", "X", "esc")
|
sendKeys(tm, "i", "down", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "heXllo" {
|
if m.ActiveBuffer().Lines[0] != "heXllo" {
|
||||||
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'heXllo'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test up arrow clamps cursor to shorter line", func(t *testing.T) {
|
t.Run("test up arrow clamps cursor to shorter line", func(t *testing.T) {
|
||||||
lines := []string{"hi", "hello"}
|
lines := []string{"hi", "hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 1})
|
||||||
sendKeys(tm, "i", "up", "X", "esc")
|
sendKeys(tm, "i", "up", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "hiX" {
|
if m.ActiveBuffer().Lines[0] != "hiX" {
|
||||||
t.Errorf("lines[0] = %q, want 'hiX'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'hiX'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test down arrow clamps cursor to shorter line", func(t *testing.T) {
|
t.Run("test down arrow clamps cursor to shorter line", func(t *testing.T) {
|
||||||
lines := []string{"hello", "hi"}
|
lines := []string{"hello", "hi"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
|
||||||
sendKeys(tm, "i", "down", "X", "esc")
|
sendKeys(tm, "i", "down", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[1] != "hiX" {
|
if m.ActiveBuffer().Lines[1] != "hiX" {
|
||||||
t.Errorf("lines[1] = %q, want 'hiX'", m.lines[1])
|
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")
|
sendKeys(tm, "i", "right", "right", "down", "X", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[1] != "woXrld" {
|
if m.ActiveBuffer().Lines[1] != "woXrld" {
|
||||||
t.Errorf("lines[1] = %q, want 'woXrld'", m.lines[1])
|
t.Errorf("lines[1] = %q, want 'woXrld'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -589,80 +589,80 @@ func TestInsertModeArrowKeys(t *testing.T) {
|
|||||||
func TestInsertModeDeletePreviousWord(t *testing.T) {
|
func TestInsertModeDeletePreviousWord(t *testing.T) {
|
||||||
t.Run("test 'ctrl+w' deletes word", func(t *testing.T) {
|
t.Run("test 'ctrl+w' deletes word", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
|
||||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "hello " {
|
if m.ActiveBuffer().Lines[0] != "hello " {
|
||||||
t.Errorf("lines[0] = %q, want 'hello '", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'hello '", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 5 {
|
if m.ActiveWindow().Cursor.Col != 5 {
|
||||||
t.Errorf("CursorX() = %d, want '5'", m.CursorX())
|
t.Errorf("CursorX() = %d, want '5'", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'ctrl+w' deletes word with whitespace", func(t *testing.T) {
|
t.Run("test 'ctrl+w' deletes word with whitespace", func(t *testing.T) {
|
||||||
lines := []string{"hello "}
|
lines := []string{"hello "}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
|
||||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "" {
|
if m.ActiveBuffer().Lines[0] != "" {
|
||||||
t.Errorf("lines[0] = %q, want ''", m.lines[0])
|
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want '0'", m.CursorX())
|
t.Errorf("CursorX() = %d, want '0'", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'ctrl+w' deletes word until period", func(t *testing.T) {
|
t.Run("test 'ctrl+w' deletes word until period", func(t *testing.T) {
|
||||||
lines := []string{"hello wo..."}
|
lines := []string{"hello wo..."}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
|
||||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "hello wo" {
|
if m.ActiveBuffer().Lines[0] != "hello wo" {
|
||||||
t.Errorf("lines[0] = %q, want 'hello wo'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'hello wo'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 7 {
|
if m.ActiveWindow().Cursor.Col != 7 {
|
||||||
t.Errorf("CursorX() = %d, want '7'", m.CursorX())
|
t.Errorf("CursorX() = %d, want '7'", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'ctrl+w' deletes line when blank", func(t *testing.T) {
|
t.Run("test 'ctrl+w' deletes line when blank", func(t *testing.T) {
|
||||||
lines := []string{"", ""}
|
lines := []string{"", ""}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
||||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 1 {
|
if m.ActiveBuffer().LineCount() != 1 {
|
||||||
t.Errorf("LineCount() = %d, want '1'", m.LineCount())
|
t.Errorf("LineCount() = %d, want '1'", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want '0'", m.CursorX())
|
t.Errorf("CursorX() = %d, want '0'", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want '0'", m.CursorY())
|
t.Errorf("CursorY() = %d, want '0'", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'ctrl+w' deletes all whitespace when line is only whitespace", func(t *testing.T) {
|
t.Run("test 'ctrl+w' deletes all whitespace when line is only whitespace", func(t *testing.T) {
|
||||||
lines := []string{" "}
|
lines := []string{" "}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
|
||||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 1 {
|
if m.ActiveBuffer().LineCount() != 1 {
|
||||||
t.Errorf("LineCount() = %d, want '1'", m.LineCount())
|
t.Errorf("LineCount() = %d, want '1'", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(0) != "" {
|
if m.ActiveBuffer().Lines[0] != "" {
|
||||||
t.Errorf("Line(0) = %s, want ''", m.Line(0))
|
t.Errorf("Line(0) = %s, want ''", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want '0'", m.CursorX())
|
t.Errorf("CursorX() = %d, want '0'", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want '0'", m.CursorY())
|
t.Errorf("CursorY() = %d, want '0'", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -672,87 +672,87 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
|
|||||||
sendKeys(tm, "i", "ctrl+w", "esc")
|
sendKeys(tm, "i", "ctrl+w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "hello" {
|
if m.ActiveBuffer().Lines[0] != "hello" {
|
||||||
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'ctrl+w' from middle of word", func(t *testing.T) {
|
t.Run("test 'ctrl+w' from middle of word", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||||
sendKeys(tm, "i", "ctrl+w", "esc")
|
sendKeys(tm, "i", "ctrl+w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "lo" {
|
if m.ActiveBuffer().Lines[0] != "lo" {
|
||||||
t.Errorf("lines[0] = %q, want 'lo'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'lo'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'ctrl+w' deletes word after punctuation", func(t *testing.T) {
|
t.Run("test 'ctrl+w' deletes word after punctuation", func(t *testing.T) {
|
||||||
lines := []string{"...hello"}
|
lines := []string{"...hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 7, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 7, Line: 0})
|
||||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "..." {
|
if m.ActiveBuffer().Lines[0] != "..." {
|
||||||
t.Errorf("lines[0] = %q, want '...'", m.lines[0])
|
t.Errorf("lines[0] = %q, want '...'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 2 {
|
if m.ActiveWindow().Cursor.Col != 2 {
|
||||||
t.Errorf("CursorX() = %d, want 2", m.CursorX())
|
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'ctrl+w' with tabs as whitespace", func(t *testing.T) {
|
t.Run("test 'ctrl+w' with tabs as whitespace", func(t *testing.T) {
|
||||||
lines := []string{"hello\tworld"}
|
lines := []string{"hello\tworld"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
|
||||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "hello\t" {
|
if m.ActiveBuffer().Lines[0] != "hello\t" {
|
||||||
t.Errorf("lines[0] = %q, want 'hello\\t'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'hello\\t'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 5 {
|
if m.ActiveWindow().Cursor.Col != 5 {
|
||||||
t.Errorf("CursorX() = %d, want 5", m.CursorX())
|
t.Errorf("CursorX() = %d, want 5", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'ctrl+w' at start of line merges with previous line content", func(t *testing.T) {
|
t.Run("test 'ctrl+w' at start of line merges with previous line content", func(t *testing.T) {
|
||||||
lines := []string{"hello", "world"}
|
lines := []string{"hello", "world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
||||||
sendKeys(tm, "i", "ctrl+w", "esc")
|
sendKeys(tm, "i", "ctrl+w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 1 {
|
if m.ActiveBuffer().LineCount() != 1 {
|
||||||
t.Errorf("LineCount() = %d, want 1", m.LineCount())
|
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.lines[0] != "helloworld" {
|
if m.ActiveBuffer().Lines[0] != "helloworld" {
|
||||||
t.Errorf("lines[0] = %q, want 'helloworld'", m.lines[0])
|
t.Errorf("lines[0] = %q, want 'helloworld'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'ctrl+w' with underscore in word", func(t *testing.T) {
|
t.Run("test 'ctrl+w' with underscore in word", func(t *testing.T) {
|
||||||
lines := []string{"hello_world"}
|
lines := []string{"hello_world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
|
||||||
sendKeys(tm, "a", "ctrl+w", "esc")
|
sendKeys(tm, "a", "ctrl+w", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.lines[0] != "" {
|
if m.ActiveBuffer().Lines[0] != "" {
|
||||||
t.Errorf("lines[0] = %q, want ''", m.lines[0])
|
t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package editor
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMoveDown(t *testing.T) {
|
func TestMoveDown(t *testing.T) {
|
||||||
@ -12,8 +12,8 @@ func TestMoveDown(t *testing.T) {
|
|||||||
sendKeys(tm, "j")
|
sendKeys(tm, "j")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.y != 1 {
|
if m.ActiveWindow().Cursor.Line != 1 {
|
||||||
t.Errorf("cursor.y = %d, want 1", m.cursor.y)
|
t.Errorf("cursor.y = %d, want 1", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -22,8 +22,8 @@ func TestMoveDown(t *testing.T) {
|
|||||||
sendKeys(tm, "j", "j", "j", "j")
|
sendKeys(tm, "j", "j", "j", "j")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.y != 4 {
|
if m.ActiveWindow().Cursor.Line != 4 {
|
||||||
t.Errorf("cursor.y = %d, want 4", m.cursor.y)
|
t.Errorf("cursor.y = %d, want 4", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -32,8 +32,8 @@ func TestMoveDown(t *testing.T) {
|
|||||||
sendKeys(tm, "j", "j", "j", "j", "j", "j", "j", "j", "j")
|
sendKeys(tm, "j", "j", "j", "j", "j", "j", "j", "j", "j")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.y != 5 {
|
if m.ActiveWindow().Cursor.Line != 5 {
|
||||||
t.Errorf("cursor.y = %d, want 5", m.cursor.y)
|
t.Errorf("cursor.y = %d, want 5", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -44,8 +44,8 @@ func TestMoveDownWithCount(t *testing.T) {
|
|||||||
sendKeys(tm, "3", "j")
|
sendKeys(tm, "3", "j")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.y != 3 {
|
if m.ActiveWindow().Cursor.Line != 3 {
|
||||||
t.Errorf("cursor.y = %d, want 3", m.cursor.y)
|
t.Errorf("cursor.y = %d, want 3", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -54,8 +54,8 @@ func TestMoveDownWithCount(t *testing.T) {
|
|||||||
sendKeys(tm, "1", "0", "j")
|
sendKeys(tm, "1", "0", "j")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.y != 5 {
|
if m.ActiveWindow().Cursor.Line != 5 {
|
||||||
t.Errorf("cursor.y = %d, want 5", m.cursor.y)
|
t.Errorf("cursor.y = %d, want 5", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -64,45 +64,45 @@ func TestMoveDownWithOverflow(t *testing.T) {
|
|||||||
lines := []string{"long line", "small"}
|
lines := []string{"long line", "small"}
|
||||||
|
|
||||||
t.Run("test 'j' with overflow", func(t *testing.T) {
|
t.Run("test 'j' with overflow", func(t *testing.T) {
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 8, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 8, Line: 0})
|
||||||
sendKeys(tm, "j")
|
sendKeys(tm, "j")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
want := len(lines[1])
|
want := len(lines[1])
|
||||||
if m.cursor.x != want {
|
if m.ActiveWindow().Cursor.Col != want {
|
||||||
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
|
t.Errorf("cursor.x = %d, want %d", m.ActiveWindow().Cursor.Col, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'j' without overflow", func(t *testing.T) {
|
t.Run("test 'j' without overflow", func(t *testing.T) {
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||||
sendKeys(tm, "j")
|
sendKeys(tm, "j")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.x != 3 {
|
if m.ActiveWindow().Cursor.Col != 3 {
|
||||||
t.Errorf("cursor.x = %d, want 3", m.cursor.x)
|
t.Errorf("cursor.x = %d, want 3", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMoveUp(t *testing.T) {
|
func TestMoveUp(t *testing.T) {
|
||||||
t.Run("test 'k'", func(t *testing.T) {
|
t.Run("test 'k'", func(t *testing.T) {
|
||||||
tm := newTestModelWithCursorPos(t, action.Position{Col: 0, Line: 2})
|
tm := newTestModelWithCursorPos(t, core.Position{Col: 0, Line: 2})
|
||||||
sendKeys(tm, "k")
|
sendKeys(tm, "k")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.y != 1 {
|
if m.ActiveWindow().Cursor.Line != 1 {
|
||||||
t.Errorf("cursor.y = %d, want 1", m.cursor.y)
|
t.Errorf("cursor.y = %d, want 1", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'kkkk'", func(t *testing.T) {
|
t.Run("test 'kkkk'", func(t *testing.T) {
|
||||||
tm := newTestModelWithCursorPos(t, action.Position{Col: 0, Line: 4})
|
tm := newTestModelWithCursorPos(t, core.Position{Col: 0, Line: 4})
|
||||||
sendKeys(tm, "k", "k", "k", "k")
|
sendKeys(tm, "k", "k", "k", "k")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.y != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
|
t.Errorf("cursor.y = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -111,30 +111,30 @@ func TestMoveUp(t *testing.T) {
|
|||||||
sendKeys(tm, "k", "k", "k")
|
sendKeys(tm, "k", "k", "k")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.y != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
|
t.Errorf("cursor.y = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMoveUpWithCount(t *testing.T) {
|
func TestMoveUpWithCount(t *testing.T) {
|
||||||
t.Run("test '3k'", func(t *testing.T) {
|
t.Run("test '3k'", func(t *testing.T) {
|
||||||
tm := newTestModelWithCursorPos(t, action.Position{Col: 0, Line: 5})
|
tm := newTestModelWithCursorPos(t, core.Position{Col: 0, Line: 5})
|
||||||
sendKeys(tm, "3", "k")
|
sendKeys(tm, "3", "k")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.y != 2 {
|
if m.ActiveWindow().Cursor.Line != 2 {
|
||||||
t.Errorf("cursor.y = %d, want 2", m.cursor.y)
|
t.Errorf("cursor.y = %d, want 2", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '10k' with overflow", func(t *testing.T) {
|
t.Run("test '10k' with overflow", func(t *testing.T) {
|
||||||
tm := newTestModelWithCursorPos(t, action.Position{Col: 0, Line: 3})
|
tm := newTestModelWithCursorPos(t, core.Position{Col: 0, Line: 3})
|
||||||
sendKeys(tm, "1", "0", "k")
|
sendKeys(tm, "1", "0", "k")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.y != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
|
t.Errorf("cursor.y = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -143,23 +143,23 @@ func TestMoveUpWithOverflow(t *testing.T) {
|
|||||||
lines := []string{"small", "long line"}
|
lines := []string{"small", "long line"}
|
||||||
|
|
||||||
t.Run("test 'k' with overflow", func(t *testing.T) {
|
t.Run("test 'k' with overflow", func(t *testing.T) {
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 1})
|
||||||
sendKeys(tm, "k")
|
sendKeys(tm, "k")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
want := len(lines[0])
|
want := len(lines[0])
|
||||||
if m.cursor.x != want {
|
if m.ActiveWindow().Cursor.Col != want {
|
||||||
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
|
t.Errorf("cursor.x = %d, want %d", m.ActiveWindow().Cursor.Col, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'k' without overflow", func(t *testing.T) {
|
t.Run("test 'k' without overflow", func(t *testing.T) {
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 1})
|
||||||
sendKeys(tm, "k")
|
sendKeys(tm, "k")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.x != 3 {
|
if m.ActiveWindow().Cursor.Col != 3 {
|
||||||
t.Errorf("cursor.x = %d, want 3", m.cursor.x)
|
t.Errorf("cursor.x = %d, want 3", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -170,8 +170,8 @@ func TestMoveRight(t *testing.T) {
|
|||||||
sendKeys(tm, "l")
|
sendKeys(tm, "l")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.x != 1 {
|
if m.ActiveWindow().Cursor.Col != 1 {
|
||||||
t.Errorf("cursor.x = %d, want 1", m.cursor.x)
|
t.Errorf("cursor.x = %d, want 1", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -180,8 +180,8 @@ func TestMoveRight(t *testing.T) {
|
|||||||
sendKeys(tm, "l", "l", "l", "l")
|
sendKeys(tm, "l", "l", "l", "l")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.x != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("cursor.x = %d, want 4", m.cursor.x)
|
t.Errorf("cursor.x = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -192,8 +192,8 @@ func TestMoveRight(t *testing.T) {
|
|||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
want := len(lines[0])
|
want := len(lines[0])
|
||||||
if m.cursor.x != want {
|
if m.ActiveWindow().Cursor.Col != want {
|
||||||
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
|
t.Errorf("cursor.x = %d, want %d", m.ActiveWindow().Cursor.Col, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -204,8 +204,8 @@ func TestMoveRightWithCount(t *testing.T) {
|
|||||||
sendKeys(tm, "3", "l")
|
sendKeys(tm, "3", "l")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.x != 3 {
|
if m.ActiveWindow().Cursor.Col != 3 {
|
||||||
t.Errorf("cursor.x = %d, want 3", m.cursor.x)
|
t.Errorf("cursor.x = %d, want 3", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -216,30 +216,30 @@ func TestMoveRightWithCount(t *testing.T) {
|
|||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
want := len(lines[0])
|
want := len(lines[0])
|
||||||
if m.cursor.x != want {
|
if m.ActiveWindow().Cursor.Col != want {
|
||||||
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
|
t.Errorf("cursor.x = %d, want %d", m.ActiveWindow().Cursor.Col, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMoveLeft(t *testing.T) {
|
func TestMoveLeft(t *testing.T) {
|
||||||
t.Run("test 'h'", func(t *testing.T) {
|
t.Run("test 'h'", func(t *testing.T) {
|
||||||
tm := newTestModelWithCursorPos(t, action.Position{Col: 3, Line: 0})
|
tm := newTestModelWithCursorPos(t, core.Position{Col: 3, Line: 0})
|
||||||
sendKeys(tm, "h")
|
sendKeys(tm, "h")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.x != 2 {
|
if m.ActiveWindow().Cursor.Col != 2 {
|
||||||
t.Errorf("cursor.x = %d, want 2", m.cursor.x)
|
t.Errorf("cursor.x = %d, want 2", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'hhhh'", func(t *testing.T) {
|
t.Run("test 'hhhh'", func(t *testing.T) {
|
||||||
tm := newTestModelWithCursorPos(t, action.Position{Col: 4, Line: 0})
|
tm := newTestModelWithCursorPos(t, core.Position{Col: 4, Line: 0})
|
||||||
sendKeys(tm, "h", "h", "h", "h")
|
sendKeys(tm, "h", "h", "h", "h")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.x != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
|
t.Errorf("cursor.x = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -248,30 +248,30 @@ func TestMoveLeft(t *testing.T) {
|
|||||||
sendKeys(tm, "h", "h", "h")
|
sendKeys(tm, "h", "h", "h")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.x != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
|
t.Errorf("cursor.x = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMoveLeftWithCount(t *testing.T) {
|
func TestMoveLeftWithCount(t *testing.T) {
|
||||||
t.Run("test '3h'", func(t *testing.T) {
|
t.Run("test '3h'", func(t *testing.T) {
|
||||||
tm := newTestModelWithCursorPos(t, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithCursorPos(t, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "3", "h")
|
sendKeys(tm, "3", "h")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.x != 2 {
|
if m.ActiveWindow().Cursor.Col != 2 {
|
||||||
t.Errorf("cursor.x = %d, want 2", m.cursor.x)
|
t.Errorf("cursor.x = %d, want 2", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '10h' with overflow", func(t *testing.T) {
|
t.Run("test '10h' with overflow", func(t *testing.T) {
|
||||||
tm := newTestModelWithCursorPos(t, action.Position{Col: 3, Line: 0})
|
tm := newTestModelWithCursorPos(t, core.Position{Col: 3, Line: 0})
|
||||||
sendKeys(tm, "1", "0", "h")
|
sendKeys(tm, "1", "0", "h")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.cursor.x != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
|
t.Errorf("cursor.x = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package editor
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- G and gg Tests ---
|
// --- G and gg Tests ---
|
||||||
@ -14,43 +14,43 @@ func TestMoveToBottom(t *testing.T) {
|
|||||||
sendKeys(tm, "G")
|
sendKeys(tm, "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 5 {
|
if m.ActiveWindow().Cursor.Line != 5 {
|
||||||
t.Errorf("CursorY() = %d, want 5", m.CursorY())
|
t.Errorf("CursorY() = %d, want 5", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'G' from middle", func(t *testing.T) {
|
t.Run("test 'G' from middle", func(t *testing.T) {
|
||||||
tm := newTestModelWithCursorPos(t, action.Position{Col: 0, Line: 2})
|
tm := newTestModelWithCursorPos(t, core.Position{Col: 0, Line: 2})
|
||||||
sendKeys(tm, "G")
|
sendKeys(tm, "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 5 {
|
if m.ActiveWindow().Cursor.Line != 5 {
|
||||||
t.Errorf("CursorY() = %d, want 5", m.CursorY())
|
t.Errorf("CursorY() = %d, want 5", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'G' already at bottom", func(t *testing.T) {
|
t.Run("test 'G' already at bottom", func(t *testing.T) {
|
||||||
tm := newTestModelWithCursorPos(t, action.Position{Col: 0, Line: 5})
|
tm := newTestModelWithCursorPos(t, core.Position{Col: 0, Line: 5})
|
||||||
sendKeys(tm, "G")
|
sendKeys(tm, "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 5 {
|
if m.ActiveWindow().Cursor.Line != 5 {
|
||||||
t.Errorf("CursorY() = %d, want 5", m.CursorY())
|
t.Errorf("CursorY() = %d, want 5", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'G' clamps CursorX()", func(t *testing.T) {
|
t.Run("test 'G' clamps CursorX()", func(t *testing.T) {
|
||||||
lines := []string{"long line here", "short"}
|
lines := []string{"long line here", "short"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
|
||||||
sendKeys(tm, "G")
|
sendKeys(tm, "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 1 {
|
if m.ActiveWindow().Cursor.Line != 1 {
|
||||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
want := len(lines[1])
|
want := len(lines[1])
|
||||||
if m.CursorX() != want {
|
if m.ActiveWindow().Cursor.Col != want {
|
||||||
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
|
t.Errorf("CursorX() = %d, want %d", m.ActiveWindow().Cursor.Col, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -60,30 +60,30 @@ func TestMoveToBottom(t *testing.T) {
|
|||||||
sendKeys(tm, "G")
|
sendKeys(tm, "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMoveToTop(t *testing.T) {
|
func TestMoveToTop(t *testing.T) {
|
||||||
t.Run("test 'gg' from bottom", func(t *testing.T) {
|
t.Run("test 'gg' from bottom", func(t *testing.T) {
|
||||||
tm := newTestModelWithCursorPos(t, action.Position{Col: 0, Line: 5})
|
tm := newTestModelWithCursorPos(t, core.Position{Col: 0, Line: 5})
|
||||||
sendKeys(tm, "g", "g")
|
sendKeys(tm, "g", "g")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'gg' from middle", func(t *testing.T) {
|
t.Run("test 'gg' from middle", func(t *testing.T) {
|
||||||
tm := newTestModelWithCursorPos(t, action.Position{Col: 0, Line: 3})
|
tm := newTestModelWithCursorPos(t, core.Position{Col: 0, Line: 3})
|
||||||
sendKeys(tm, "g", "g")
|
sendKeys(tm, "g", "g")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -92,23 +92,23 @@ func TestMoveToTop(t *testing.T) {
|
|||||||
sendKeys(tm, "g", "g")
|
sendKeys(tm, "g", "g")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'gg' clamps CursorX()", func(t *testing.T) {
|
t.Run("test 'gg' clamps CursorX()", func(t *testing.T) {
|
||||||
lines := []string{"short", "long line here"}
|
lines := []string{"short", "long line here"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 1})
|
||||||
sendKeys(tm, "g", "g")
|
sendKeys(tm, "g", "g")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
want := len(lines[0])
|
want := len(lines[0])
|
||||||
if m.CursorX() != want {
|
if m.ActiveWindow().Cursor.Col != want {
|
||||||
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
|
t.Errorf("CursorX() = %d, want %d", m.ActiveWindow().Cursor.Col, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -117,23 +117,23 @@ func TestMoveToTop(t *testing.T) {
|
|||||||
|
|
||||||
func TestMoveToLineStart(t *testing.T) {
|
func TestMoveToLineStart(t *testing.T) {
|
||||||
t.Run("test '0' from middle of line", func(t *testing.T) {
|
t.Run("test '0' from middle of line", func(t *testing.T) {
|
||||||
tm := newTestModelWithCursorPos(t, action.Position{Col: 3, Line: 0})
|
tm := newTestModelWithCursorPos(t, core.Position{Col: 3, Line: 0})
|
||||||
sendKeys(tm, "0")
|
sendKeys(tm, "0")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '0' from end of line", func(t *testing.T) {
|
t.Run("test '0' from end of line", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: len(lines[0]), Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: len(lines[0]), Line: 0})
|
||||||
sendKeys(tm, "0")
|
sendKeys(tm, "0")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -142,8 +142,8 @@ func TestMoveToLineStart(t *testing.T) {
|
|||||||
sendKeys(tm, "0")
|
sendKeys(tm, "0")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -153,18 +153,18 @@ func TestMoveToLineStart(t *testing.T) {
|
|||||||
sendKeys(tm, "0")
|
sendKeys(tm, "0")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '0' preserves line", func(t *testing.T) {
|
t.Run("test '0' preserves line", func(t *testing.T) {
|
||||||
tm := newTestModelWithCursorPos(t, action.Position{Col: 3, Line: 2})
|
tm := newTestModelWithCursorPos(t, core.Position{Col: 3, Line: 2})
|
||||||
sendKeys(tm, "0")
|
sendKeys(tm, "0")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 2 {
|
if m.ActiveWindow().Cursor.Line != 2 {
|
||||||
t.Errorf("CursorY() = %d, want 2", m.CursorY())
|
t.Errorf("CursorY() = %d, want 2", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -177,32 +177,32 @@ func TestMoveToLineEnd(t *testing.T) {
|
|||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
want := len(lines[0])
|
want := len(lines[0])
|
||||||
if m.CursorX() != want {
|
if m.ActiveWindow().Cursor.Col != want {
|
||||||
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
|
t.Errorf("CursorX() = %d, want %d", m.ActiveWindow().Cursor.Col, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '$' from middle of line", func(t *testing.T) {
|
t.Run("test '$' from middle of line", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||||
sendKeys(tm, "$")
|
sendKeys(tm, "$")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
want := len(lines[0])
|
want := len(lines[0])
|
||||||
if m.CursorX() != want {
|
if m.ActiveWindow().Cursor.Col != want {
|
||||||
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
|
t.Errorf("CursorX() = %d, want %d", m.ActiveWindow().Cursor.Col, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '$' already at end", func(t *testing.T) {
|
t.Run("test '$' already at end", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: len(lines[0]), Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: len(lines[0]), Line: 0})
|
||||||
sendKeys(tm, "$")
|
sendKeys(tm, "$")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
want := len(lines[0])
|
want := len(lines[0])
|
||||||
if m.CursorX() != want {
|
if m.ActiveWindow().Cursor.Col != want {
|
||||||
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
|
t.Errorf("CursorX() = %d, want %d", m.ActiveWindow().Cursor.Col, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -212,18 +212,18 @@ func TestMoveToLineEnd(t *testing.T) {
|
|||||||
sendKeys(tm, "$")
|
sendKeys(tm, "$")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '$' preserves line", func(t *testing.T) {
|
t.Run("test '$' preserves line", func(t *testing.T) {
|
||||||
tm := newTestModelWithCursorPos(t, action.Position{Col: 0, Line: 2})
|
tm := newTestModelWithCursorPos(t, core.Position{Col: 0, Line: 2})
|
||||||
sendKeys(tm, "$")
|
sendKeys(tm, "$")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 2 {
|
if m.ActiveWindow().Cursor.Line != 2 {
|
||||||
t.Errorf("CursorY() = %d, want 2", m.CursorY())
|
t.Errorf("CursorY() = %d, want 2", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -231,78 +231,78 @@ func TestMoveToLineEnd(t *testing.T) {
|
|||||||
func TestMoveToLineContentStart(t *testing.T) {
|
func TestMoveToLineContentStart(t *testing.T) {
|
||||||
t.Run("test '_' from middle of line with no leading whitespace", func(t *testing.T) {
|
t.Run("test '_' from middle of line with no leading whitespace", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0})
|
||||||
sendKeys(tm, "_")
|
sendKeys(tm, "_")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '_' from middle of line with leading whitespace", func(t *testing.T) {
|
t.Run("test '_' from middle of line with leading whitespace", func(t *testing.T) {
|
||||||
lines := []string{" hello world"}
|
lines := []string{" hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
|
||||||
sendKeys(tm, "_")
|
sendKeys(tm, "_")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '_' from start of line with leading whitespace", func(t *testing.T) {
|
t.Run("test '_' from start of line with leading whitespace", func(t *testing.T) {
|
||||||
lines := []string{" hello world"}
|
lines := []string{" hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "_")
|
sendKeys(tm, "_")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '_' from start of line with no leading whitespace", func(t *testing.T) {
|
t.Run("test '_' from start of line with no leading whitespace", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "_")
|
sendKeys(tm, "_")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '_' from middle of line with only whitespace", func(t *testing.T) {
|
t.Run("test '_' from middle of line with only whitespace", func(t *testing.T) {
|
||||||
lines := []string{" "}
|
lines := []string{" "}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "_")
|
sendKeys(tm, "_")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '_' from end of line with only whitespace", func(t *testing.T) {
|
t.Run("test '_' from end of line with only whitespace", func(t *testing.T) {
|
||||||
lines := []string{" "}
|
lines := []string{" "}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
|
||||||
sendKeys(tm, "_")
|
sendKeys(tm, "_")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '_' on empty line", func(t *testing.T) {
|
t.Run("test '_' on empty line", func(t *testing.T) {
|
||||||
lines := []string{""}
|
lines := []string{""}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "_")
|
sendKeys(tm, "_")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -310,78 +310,78 @@ func TestMoveToLineContentStart(t *testing.T) {
|
|||||||
func TestMoveToLineContentStartAlias(t *testing.T) {
|
func TestMoveToLineContentStartAlias(t *testing.T) {
|
||||||
t.Run("test '^' from middle of line with no leading whitespace", func(t *testing.T) {
|
t.Run("test '^' from middle of line with no leading whitespace", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0})
|
||||||
sendKeys(tm, "^")
|
sendKeys(tm, "^")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '^' from middle of line with leading whitespace", func(t *testing.T) {
|
t.Run("test '^' from middle of line with leading whitespace", func(t *testing.T) {
|
||||||
lines := []string{" hello world"}
|
lines := []string{" hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
|
||||||
sendKeys(tm, "^")
|
sendKeys(tm, "^")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '^' from start of line with leading whitespace", func(t *testing.T) {
|
t.Run("test '^' from start of line with leading whitespace", func(t *testing.T) {
|
||||||
lines := []string{" hello world"}
|
lines := []string{" hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "^")
|
sendKeys(tm, "^")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '^' from start of line with no leading whitespace", func(t *testing.T) {
|
t.Run("test '^' from start of line with no leading whitespace", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "^")
|
sendKeys(tm, "^")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '^' from middle of line with only whitespace", func(t *testing.T) {
|
t.Run("test '^' from middle of line with only whitespace", func(t *testing.T) {
|
||||||
lines := []string{" "}
|
lines := []string{" "}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "^")
|
sendKeys(tm, "^")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '^' from end of line with only whitespace", func(t *testing.T) {
|
t.Run("test '^' from end of line with only whitespace", func(t *testing.T) {
|
||||||
lines := []string{" "}
|
lines := []string{" "}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
|
||||||
sendKeys(tm, "^")
|
sendKeys(tm, "^")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '^' on empty line", func(t *testing.T) {
|
t.Run("test '^' on empty line", func(t *testing.T) {
|
||||||
lines := []string{""}
|
lines := []string{""}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "^")
|
sendKeys(tm, "^")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -395,70 +395,70 @@ func TestMoveToLineContentStartAlias(t *testing.T) {
|
|||||||
func TestMoveToColumn(t *testing.T) {
|
func TestMoveToColumn(t *testing.T) {
|
||||||
t.Run("test '|' alone goes to column 1 (index 0)", func(t *testing.T) {
|
t.Run("test '|' alone goes to column 1 (index 0)", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "|")
|
sendKeys(tm, "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// | with no count = 1| = column 1 = index 0
|
// | with no count = 1| = column 1 = index 0
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '1|' goes to column 1 (index 0)", func(t *testing.T) {
|
t.Run("test '1|' goes to column 1 (index 0)", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "1", "|")
|
sendKeys(tm, "1", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '5|' goes to column 5 (index 4)", func(t *testing.T) {
|
t.Run("test '5|' goes to column 5 (index 4)", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "5", "|")
|
sendKeys(tm, "5", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Column 5 = index 4 (the 'o' in hello)
|
// Column 5 = index 4 (the 'o' in hello)
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '10|' goes to column 10 (index 9)", func(t *testing.T) {
|
t.Run("test '10|' goes to column 10 (index 9)", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "1", "0", "|")
|
sendKeys(tm, "1", "0", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Column 10 = index 9 (the 'l' in world)
|
// Column 10 = index 9 (the 'l' in world)
|
||||||
if m.CursorX() != 9 {
|
if m.ActiveWindow().Cursor.Col != 9 {
|
||||||
t.Errorf("CursorX() = %d, want 9", m.CursorX())
|
t.Errorf("CursorX() = %d, want 9", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '|' already at column 1", func(t *testing.T) {
|
t.Run("test '|' already at column 1", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "|")
|
sendKeys(tm, "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '5|' already at column 5", func(t *testing.T) {
|
t.Run("test '5|' already at column 5", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
|
||||||
sendKeys(tm, "5", "|")
|
sendKeys(tm, "5", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -466,71 +466,71 @@ func TestMoveToColumn(t *testing.T) {
|
|||||||
func TestMoveToColumnClamp(t *testing.T) {
|
func TestMoveToColumnClamp(t *testing.T) {
|
||||||
t.Run("test '20|' clamps to end of short line", func(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
|
lines := []string{"hello"} // 5 chars, max index 4
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "2", "0", "|")
|
sendKeys(tm, "2", "0", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Column 20 exceeds line length, should clamp to last char (index 4)
|
// Column 20 exceeds line length, should clamp to last char (index 4)
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '100|' clamps to end of line", func(t *testing.T) {
|
t.Run("test '100|' clamps to end of line", func(t *testing.T) {
|
||||||
lines := []string{"hello world"} // 11 chars, max index 10
|
lines := []string{"hello world"} // 11 chars, max index 10
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "1", "0", "0", "|")
|
sendKeys(tm, "1", "0", "0", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Should clamp to last char (index 10)
|
// Should clamp to last char (index 10)
|
||||||
if m.CursorX() != 10 {
|
if m.ActiveWindow().Cursor.Col != 10 {
|
||||||
t.Errorf("CursorX() = %d, want 10", m.CursorX())
|
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '6|' clamps on 5-char line", func(t *testing.T) {
|
t.Run("test '6|' clamps on 5-char line", func(t *testing.T) {
|
||||||
lines := []string{"hello"} // 5 chars
|
lines := []string{"hello"} // 5 chars
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "6", "|")
|
sendKeys(tm, "6", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Column 6 = index 5, but line only has 5 chars (max index 4)
|
// Column 6 = index 5, but line only has 5 chars (max index 4)
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '|' on empty line stays at 0", func(t *testing.T) {
|
t.Run("test '|' on empty line stays at 0", func(t *testing.T) {
|
||||||
lines := []string{""}
|
lines := []string{""}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "|")
|
sendKeys(tm, "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '5|' on empty line stays at 0", func(t *testing.T) {
|
t.Run("test '5|' on empty line stays at 0", func(t *testing.T) {
|
||||||
lines := []string{""}
|
lines := []string{""}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "5", "|")
|
sendKeys(tm, "5", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '3|' on 2-char line clamps", func(t *testing.T) {
|
t.Run("test '3|' on 2-char line clamps", func(t *testing.T) {
|
||||||
lines := []string{"ab"}
|
lines := []string{"ab"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "3", "|")
|
sendKeys(tm, "3", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Column 3 = index 2, but line only has 2 chars (max index 1)
|
// Column 3 = index 2, but line only has 2 chars (max index 1)
|
||||||
if m.CursorX() != 1 {
|
if m.ActiveWindow().Cursor.Col != 1 {
|
||||||
t.Errorf("CursorX() = %d, want 1", m.CursorX())
|
t.Errorf("CursorX() = %d, want 1", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -538,37 +538,37 @@ func TestMoveToColumnClamp(t *testing.T) {
|
|||||||
func TestMoveToColumnPreservesLine(t *testing.T) {
|
func TestMoveToColumnPreservesLine(t *testing.T) {
|
||||||
t.Run("test '|' preserves Y position", func(t *testing.T) {
|
t.Run("test '|' preserves Y position", func(t *testing.T) {
|
||||||
lines := []string{"line one", "line two", "line three"}
|
lines := []string{"line one", "line two", "line three"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 1})
|
||||||
sendKeys(tm, "|")
|
sendKeys(tm, "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 1 {
|
if m.ActiveWindow().Cursor.Line != 1 {
|
||||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '5|' preserves Y position", func(t *testing.T) {
|
t.Run("test '5|' preserves Y position", func(t *testing.T) {
|
||||||
lines := []string{"line one", "line two", "line three"}
|
lines := []string{"line one", "line two", "line three"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
|
||||||
sendKeys(tm, "5", "|")
|
sendKeys(tm, "5", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 2 {
|
if m.ActiveWindow().Cursor.Line != 2 {
|
||||||
t.Errorf("CursorY() = %d, want 2", m.CursorY())
|
t.Errorf("CursorY() = %d, want 2", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '|' on different lines", func(t *testing.T) {
|
t.Run("test '|' on different lines", func(t *testing.T) {
|
||||||
lines := []string{"short", "longer line here"}
|
lines := []string{"short", "longer line here"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 1})
|
||||||
sendKeys(tm, "|")
|
sendKeys(tm, "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 1 {
|
if m.ActiveWindow().Cursor.Line != 1 {
|
||||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -576,49 +576,49 @@ func TestMoveToColumnPreservesLine(t *testing.T) {
|
|||||||
func TestMoveToColumnWithWhitespace(t *testing.T) {
|
func TestMoveToColumnWithWhitespace(t *testing.T) {
|
||||||
t.Run("test '5|' with leading whitespace", func(t *testing.T) {
|
t.Run("test '5|' with leading whitespace", func(t *testing.T) {
|
||||||
lines := []string{" hello"}
|
lines := []string{" hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "5", "|")
|
sendKeys(tm, "5", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Column 5 = index 4 = 'h' in " hello"
|
// Column 5 = index 4 = 'h' in " hello"
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '3|' lands on whitespace", func(t *testing.T) {
|
t.Run("test '3|' lands on whitespace", func(t *testing.T) {
|
||||||
lines := []string{" hello"}
|
lines := []string{" hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "3", "|")
|
sendKeys(tm, "3", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Column 3 = index 2 = third space
|
// Column 3 = index 2 = third space
|
||||||
if m.CursorX() != 2 {
|
if m.ActiveWindow().Cursor.Col != 2 {
|
||||||
t.Errorf("CursorX() = %d, want 2", m.CursorX())
|
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '|' with tabs", func(t *testing.T) {
|
t.Run("test '|' with tabs", func(t *testing.T) {
|
||||||
lines := []string{"\thello"}
|
lines := []string{"\thello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||||
sendKeys(tm, "|")
|
sendKeys(tm, "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// | goes to column 1 = index 0 = the tab
|
// | goes to column 1 = index 0 = the tab
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test '2|' with tabs", func(t *testing.T) {
|
t.Run("test '2|' with tabs", func(t *testing.T) {
|
||||||
lines := []string{"\thello"}
|
lines := []string{"\thello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "2", "|")
|
sendKeys(tm, "2", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Column 2 = index 1 = 'h' in "\thello"
|
// Column 2 = index 1 = 'h' in "\thello"
|
||||||
if m.CursorX() != 1 {
|
if m.ActiveWindow().Cursor.Col != 1 {
|
||||||
t.Errorf("CursorX() = %d, want 1", m.CursorX())
|
t.Errorf("CursorX() = %d, want 1", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -626,34 +626,34 @@ func TestMoveToColumnWithWhitespace(t *testing.T) {
|
|||||||
func TestMoveToColumnWithOperator(t *testing.T) {
|
func TestMoveToColumnWithOperator(t *testing.T) {
|
||||||
t.Run("test 'd|' deletes to column 1", func(t *testing.T) {
|
t.Run("test 'd|' deletes to column 1", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "d", "|")
|
sendKeys(tm, "d", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Deletes from column 1 to current position (exclusive), so "hello" deleted
|
// Deletes from column 1 to current position (exclusive), so "hello" deleted
|
||||||
// Result depends on inclusive/exclusive behavior
|
// Result depends on inclusive/exclusive behavior
|
||||||
// In Vim: d| from col 5 deletes chars 0-4, leaving " world"
|
// In Vim: d| from col 5 deletes chars 0-4, leaving " world"
|
||||||
if m.Line(0) != " world" {
|
if m.ActiveBuffer().Lines[0] != " world" {
|
||||||
t.Errorf("Line(0) = %q, want ' world'", m.Line(0))
|
t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'd5|' deletes to column 5", func(t *testing.T) {
|
t.Run("test 'd5|' deletes to column 5", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "d", "5", "|")
|
sendKeys(tm, "d", "5", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Deletes from cursor (0) to column 5 (index 4), so "hell" deleted
|
// Deletes from cursor (0) to column 5 (index 4), so "hell" deleted
|
||||||
// Result: "o world"
|
// Result: "o world"
|
||||||
if m.Line(0) != "o world" {
|
if m.ActiveBuffer().Lines[0] != "o world" {
|
||||||
t.Errorf("Line(0) = %q, want 'o world'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'o world'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'y5|' yanks to column 5", func(t *testing.T) {
|
t.Run("test 'y5|' yanks to column 5", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "y", "5", "|")
|
sendKeys(tm, "y", "5", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
@ -669,7 +669,7 @@ func TestMoveToColumnWithOperator(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("test 'y|' yanks to column 1 (nothing if at start)", func(t *testing.T) {
|
t.Run("test 'y|' yanks to column 1 (nothing if at start)", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "y", "|")
|
sendKeys(tm, "y", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
@ -687,41 +687,41 @@ func TestMoveToColumnWithOperator(t *testing.T) {
|
|||||||
func TestMoveToColumnInVisualMode(t *testing.T) {
|
func TestMoveToColumnInVisualMode(t *testing.T) {
|
||||||
t.Run("test 'v5|' selects to column 5", func(t *testing.T) {
|
t.Run("test 'v5|' selects to column 5", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "v", "5", "|")
|
sendKeys(tm, "v", "5", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.AnchorX() != 0 {
|
if m.ActiveWindow().Anchor.Col != 0 {
|
||||||
t.Errorf("AnchorX() = %d, want 0", m.AnchorX())
|
t.Errorf("AnchorX() = %d, want 0", m.ActiveWindow().Anchor.Col)
|
||||||
}
|
}
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'v|' selects backward to column 1", func(t *testing.T) {
|
t.Run("test 'v|' selects backward to column 1", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||||
sendKeys(tm, "v", "|")
|
sendKeys(tm, "v", "|")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.AnchorX() != 5 {
|
if m.ActiveWindow().Anchor.Col != 5 {
|
||||||
t.Errorf("AnchorX() = %d, want 5", m.AnchorX())
|
t.Errorf("AnchorX() = %d, want 5", m.ActiveWindow().Anchor.Col)
|
||||||
}
|
}
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'v5|d' deletes selection to column 5", func(t *testing.T) {
|
t.Run("test 'v5|d' deletes selection to column 5", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
|
||||||
sendKeys(tm, "v", "5", "|", "d")
|
sendKeys(tm, "v", "5", "|", "d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Visual selection from 0 to 4 inclusive, delete "hello"
|
// Visual selection from 0 to 4 inclusive, delete "hello"
|
||||||
if m.Line(0) != " world" {
|
if m.ActiveBuffer().Lines[0] != " world" {
|
||||||
t.Errorf("Line(0) = %q, want ' world'", m.Line(0))
|
t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NOTE: AI Generated tests
|
// NOTE: AI Generated tests
|
||||||
@ -22,12 +22,12 @@ func TestScrollBasic(t *testing.T) {
|
|||||||
t.Run("small file does not scroll", func(t *testing.T) {
|
t.Run("small file does not scroll", func(t *testing.T) {
|
||||||
// 10 lines, viewport 24 -> no scrolling needed
|
// 10 lines, viewport 24 -> no scrolling needed
|
||||||
lines := generateLines(10)
|
lines := generateLines(10)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 24)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 0}, 80, 24)
|
||||||
sendKeys(tm, "G") // go to bottom
|
sendKeys(tm, "G") // go to bottom
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ScrollY() != 0 {
|
if m.ActiveWindow().ScrollY != 0 {
|
||||||
t.Errorf("ScrollY() = %d, want 0", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want 0", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ func TestScrollBasic(t *testing.T) {
|
|||||||
// Safe zone: lines 8 to 10 (19-1-8=10)
|
// Safe zone: lines 8 to 10 (19-1-8=10)
|
||||||
// Moving to line 11+ should trigger scroll
|
// Moving to line 11+ should trigger scroll
|
||||||
lines := generateLines(50)
|
lines := generateLines(50)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 20)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 0}, 80, 20)
|
||||||
|
|
||||||
// Move down 15 times to get to line 15
|
// Move down 15 times to get to line 15
|
||||||
for range 15 {
|
for range 15 {
|
||||||
@ -45,21 +45,21 @@ func TestScrollBasic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 15 {
|
if m.ActiveWindow().Cursor.Line != 15 {
|
||||||
t.Errorf("CursorY() = %d, want 15", m.CursorY())
|
t.Errorf("CursorY() = %d, want 15", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
// With scrollOff=8, viewport=19, cursor at 15 means:
|
// With scrollOff=8, viewport=19, cursor at 15 means:
|
||||||
// cursor should be at position 10 from top (19-1-8=10)
|
// cursor should be at position 10 from top (19-1-8=10)
|
||||||
// so scrollY = 15 - 10 = 5
|
// so scrollY = 15 - 10 = 5
|
||||||
if m.ScrollY() < 1 {
|
if m.ActiveWindow().ScrollY < 1 {
|
||||||
t.Errorf("ScrollY() = %d, want > 0 (should have scrolled)", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want > 0 (should have scrolled)", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("scrolls up when cursor moves past top margin", func(t *testing.T) {
|
t.Run("scrolls up when cursor moves past top margin", func(t *testing.T) {
|
||||||
// Start at line 20, move up
|
// Start at line 20, move up
|
||||||
lines := generateLines(50)
|
lines := generateLines(50)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 20}, 80, 20)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 20}, 80, 20)
|
||||||
|
|
||||||
// First, let the model adjust (it will scroll to show cursor)
|
// First, let the model adjust (it will scroll to show cursor)
|
||||||
// Then move up 15 times
|
// Then move up 15 times
|
||||||
@ -68,43 +68,43 @@ func TestScrollBasic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 5 {
|
if m.ActiveWindow().Cursor.Line != 5 {
|
||||||
t.Errorf("CursorY() = %d, want 5", m.CursorY())
|
t.Errorf("CursorY() = %d, want 5", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
// Cursor at line 5 with scrollOff=8 means scrollY should be 0
|
// Cursor at line 5 with scrollOff=8 means scrollY should be 0
|
||||||
// (can't scroll negative, and cursor is within safe zone from top)
|
// (can't scroll negative, and cursor is within safe zone from top)
|
||||||
if m.ScrollY() != 0 {
|
if m.ActiveWindow().ScrollY != 0 {
|
||||||
t.Errorf("ScrollY() = %d, want 0", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want 0", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("G jumps to bottom and scrolls", func(t *testing.T) {
|
t.Run("G jumps to bottom and scrolls", func(t *testing.T) {
|
||||||
lines := generateLines(100)
|
lines := generateLines(100)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 20)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 0}, 80, 20)
|
||||||
sendKeys(tm, "G")
|
sendKeys(tm, "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 99 {
|
if m.ActiveWindow().Cursor.Line != 99 {
|
||||||
t.Errorf("CursorY() = %d, want 99", m.CursorY())
|
t.Errorf("CursorY() = %d, want 99", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
// With 100 lines and viewport 18 (height - 2 for status + command bar),
|
// With 100 lines and viewport 18 (height - 2 for status + command bar),
|
||||||
// max scrollY = 100 - 18 = 82
|
// max scrollY = 100 - 18 = 82
|
||||||
if m.ScrollY() != 82 {
|
if m.ActiveWindow().ScrollY != 82 {
|
||||||
t.Errorf("ScrollY() = %d, want 82", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want 82", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("gg jumps to top and scrolls", func(t *testing.T) {
|
t.Run("gg jumps to top and scrolls", func(t *testing.T) {
|
||||||
lines := generateLines(100)
|
lines := generateLines(100)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 50}, 80, 20)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 50}, 80, 20)
|
||||||
sendKeys(tm, "g", "g")
|
sendKeys(tm, "g", "g")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
if m.ScrollY() != 0 {
|
if m.ActiveWindow().ScrollY != 0 {
|
||||||
t.Errorf("ScrollY() = %d, want 0", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want 0", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -112,7 +112,7 @@ func TestScrollBasic(t *testing.T) {
|
|||||||
func TestScrollEdgeCases(t *testing.T) {
|
func TestScrollEdgeCases(t *testing.T) {
|
||||||
t.Run("scrollY never goes negative", func(t *testing.T) {
|
t.Run("scrollY never goes negative", func(t *testing.T) {
|
||||||
lines := generateLines(50)
|
lines := generateLines(50)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 20)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 0}, 80, 20)
|
||||||
|
|
||||||
// Try to move up from top
|
// Try to move up from top
|
||||||
for range 5 {
|
for range 5 {
|
||||||
@ -120,27 +120,27 @@ func TestScrollEdgeCases(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ScrollY() < 0 {
|
if m.ActiveWindow().ScrollY < 0 {
|
||||||
t.Errorf("ScrollY() = %d, want >= 0", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want >= 0", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("scrollY clamped to max scroll", func(t *testing.T) {
|
t.Run("scrollY clamped to max scroll", func(t *testing.T) {
|
||||||
lines := generateLines(30)
|
lines := generateLines(30)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 20)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 0}, 80, 20)
|
||||||
sendKeys(tm, "G")
|
sendKeys(tm, "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// 30 lines, viewport 18 (height - 2) -> maxScroll = 30 - 18 = 12
|
// 30 lines, viewport 18 (height - 2) -> maxScroll = 30 - 18 = 12
|
||||||
maxScroll := 30 - 18
|
maxScroll := 30 - 18
|
||||||
if m.ScrollY() > maxScroll {
|
if m.ActiveWindow().ScrollY > maxScroll {
|
||||||
t.Errorf("ScrollY() = %d, want <= %d", m.ScrollY(), maxScroll)
|
t.Errorf("ScrollY() = %d, want <= %d", m.ActiveWindow().ScrollY, maxScroll)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("cursor stays visible after delete at bottom", func(t *testing.T) {
|
t.Run("cursor stays visible after delete at bottom", func(t *testing.T) {
|
||||||
lines := generateLines(30)
|
lines := generateLines(30)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 29}, 80, 20)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 29}, 80, 20)
|
||||||
|
|
||||||
// Delete some lines at bottom
|
// Delete some lines at bottom
|
||||||
sendKeys(tm, "d", "d", "d", "d")
|
sendKeys(tm, "d", "d", "d", "d")
|
||||||
@ -148,9 +148,9 @@ func TestScrollEdgeCases(t *testing.T) {
|
|||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Cursor should still be visible
|
// Cursor should still be visible
|
||||||
viewportHeight := 19
|
viewportHeight := 19
|
||||||
if m.CursorY() < m.ScrollY() || m.CursorY() >= m.ScrollY()+viewportHeight {
|
if m.ActiveWindow().Cursor.Line < m.ActiveWindow().ScrollY || m.ActiveWindow().Cursor.Line >= m.ActiveWindow().ScrollY+viewportHeight {
|
||||||
t.Errorf("Cursor at %d not visible in viewport [%d, %d)",
|
t.Errorf("Cursor at %d not visible in viewport [%d, %d)",
|
||||||
m.CursorY(), m.ScrollY(), m.ScrollY()+viewportHeight)
|
m.ActiveWindow().Cursor.Line, m.ActiveWindow().ScrollY, m.ActiveWindow().ScrollY+viewportHeight)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -162,26 +162,26 @@ func TestHalfPageScrollDown(t *testing.T) {
|
|||||||
// cursor at line 15 (relY=15, in safe zone), scrollY starts at 0
|
// cursor at line 15 (relY=15, in safe zone), scrollY starts at 0
|
||||||
// After ctrl+d: newScrollY=14, newCursorY=14+15=29
|
// After ctrl+d: newScrollY=14, newCursorY=14+15=29
|
||||||
lines := generateLines(100)
|
lines := generateLines(100)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 15}, 80, 30)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 15}, 80, 30)
|
||||||
sendKeys(tm, "ctrl+d")
|
sendKeys(tm, "ctrl+d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ScrollY() != 14 {
|
if m.ActiveWindow().ScrollY != 14 {
|
||||||
t.Errorf("ScrollY() = %d, want 14", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want 14", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 29 {
|
if m.ActiveWindow().Cursor.Line != 29 {
|
||||||
t.Errorf("CursorY() = %d, want 29", m.CursorY())
|
t.Errorf("CursorY() = %d, want 29", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ctrl+d preserves cursor relative position in viewport", func(t *testing.T) {
|
t.Run("ctrl+d preserves cursor relative position in viewport", func(t *testing.T) {
|
||||||
// relY=15 before and after
|
// relY=15 before and after
|
||||||
lines := generateLines(100)
|
lines := generateLines(100)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 15}, 80, 30)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 15}, 80, 30)
|
||||||
sendKeys(tm, "ctrl+d")
|
sendKeys(tm, "ctrl+d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
relY := m.CursorY() - m.ScrollY()
|
relY := m.ActiveWindow().Cursor.Line - m.ActiveWindow().ScrollY
|
||||||
if relY != 15 {
|
if relY != 15 {
|
||||||
t.Errorf("relative position = %d, want 15", relY)
|
t.Errorf("relative position = %d, want 15", relY)
|
||||||
}
|
}
|
||||||
@ -191,15 +191,15 @@ func TestHalfPageScrollDown(t *testing.T) {
|
|||||||
// cursor at line 0 (relY=0 < scrollOff=8), clamp to scrollOff
|
// cursor at line 0 (relY=0 < scrollOff=8), clamp to scrollOff
|
||||||
// After ctrl+d: newScrollY=14, newCursorY=14+8=22
|
// After ctrl+d: newScrollY=14, newCursorY=14+8=22
|
||||||
lines := generateLines(100)
|
lines := generateLines(100)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 30)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 0}, 80, 30)
|
||||||
sendKeys(tm, "ctrl+d")
|
sendKeys(tm, "ctrl+d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ScrollY() != 14 {
|
if m.ActiveWindow().ScrollY != 14 {
|
||||||
t.Errorf("ScrollY() = %d, want 14", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want 14", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 22 {
|
if m.ActiveWindow().Cursor.Line != 22 {
|
||||||
t.Errorf("CursorY() = %d, want 22", m.CursorY())
|
t.Errorf("CursorY() = %d, want 22", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -208,16 +208,16 @@ func TestHalfPageScrollDown(t *testing.T) {
|
|||||||
// AdjustScroll puts cursor 35 at scrollY=12 (clamped), relY=23
|
// AdjustScroll puts cursor 35 at scrollY=12 (clamped), relY=23
|
||||||
// After ctrl+d: newScrollY clamped to 12, relY=23>19 clamped to 19, newCursorY=31
|
// After ctrl+d: newScrollY clamped to 12, relY=23>19 clamped to 19, newCursorY=31
|
||||||
lines := generateLines(40)
|
lines := generateLines(40)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 35}, 80, 30)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 35}, 80, 30)
|
||||||
sendKeys(tm, "ctrl+d")
|
sendKeys(tm, "ctrl+d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
maxScroll := 40 - 28
|
maxScroll := 40 - 28
|
||||||
if m.ScrollY() > maxScroll {
|
if m.ActiveWindow().ScrollY > maxScroll {
|
||||||
t.Errorf("ScrollY() = %d, want <= %d", m.ScrollY(), maxScroll)
|
t.Errorf("ScrollY() = %d, want <= %d", m.ActiveWindow().ScrollY, maxScroll)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 31 {
|
if m.ActiveWindow().Cursor.Line != 31 {
|
||||||
t.Errorf("CursorY() = %d, want 31", m.CursorY())
|
t.Errorf("CursorY() = %d, want 31", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -225,15 +225,15 @@ func TestHalfPageScrollDown(t *testing.T) {
|
|||||||
// 20 lines < viewport 28: maxScroll=0, scrollY stays 0
|
// 20 lines < viewport 28: maxScroll=0, scrollY stays 0
|
||||||
// relY=0 < scrollOff, clamp to 8; newCursorY=0+8=8
|
// relY=0 < scrollOff, clamp to 8; newCursorY=0+8=8
|
||||||
lines := generateLines(20)
|
lines := generateLines(20)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 30)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 0}, 80, 30)
|
||||||
sendKeys(tm, "ctrl+d")
|
sendKeys(tm, "ctrl+d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ScrollY() != 0 {
|
if m.ActiveWindow().ScrollY != 0 {
|
||||||
t.Errorf("ScrollY() = %d, want 0", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want 0", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 8 {
|
if m.ActiveWindow().Cursor.Line != 8 {
|
||||||
t.Errorf("CursorY() = %d, want 8", m.CursorY())
|
t.Errorf("CursorY() = %d, want 8", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -249,15 +249,15 @@ func TestHalfPageScrollDown(t *testing.T) {
|
|||||||
for i := 15; i < 100; i++ {
|
for i := 15; i < 100; i++ {
|
||||||
lines[i] = "hi"
|
lines[i] = "hi"
|
||||||
}
|
}
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 10, Line: 5}, 80, 30)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 10, Line: 5}, 80, 30)
|
||||||
sendKeys(tm, "ctrl+d")
|
sendKeys(tm, "ctrl+d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 22 {
|
if m.ActiveWindow().Cursor.Line != 22 {
|
||||||
t.Errorf("CursorY() = %d, want 22", m.CursorY())
|
t.Errorf("CursorY() = %d, want 22", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
if m.CursorX() > len(m.Line(m.CursorY())) {
|
if m.ActiveWindow().Cursor.Col > len(m.ActiveBuffer().Lines[m.ActiveWindow().Cursor.Line]) {
|
||||||
t.Errorf("CursorX() = %d exceeds line length %d", m.CursorX(), len(m.Line(m.CursorY())))
|
t.Errorf("CursorX() = %d exceeds line length %d", m.ActiveWindow().Cursor.Col, len(m.ActiveBuffer().Lines[m.ActiveWindow().Cursor.Line]))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -266,15 +266,15 @@ func TestHalfPageScrollDown(t *testing.T) {
|
|||||||
// ctrl+d #1: scrollY=14, cursorY=29, relY=15
|
// ctrl+d #1: scrollY=14, cursorY=29, relY=15
|
||||||
// ctrl+d #2: scrollY=28, cursorY=43, relY=15
|
// ctrl+d #2: scrollY=28, cursorY=43, relY=15
|
||||||
lines := generateLines(200)
|
lines := generateLines(200)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 15}, 80, 30)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 15}, 80, 30)
|
||||||
sendKeys(tm, "ctrl+d", "ctrl+d")
|
sendKeys(tm, "ctrl+d", "ctrl+d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ScrollY() != 28 {
|
if m.ActiveWindow().ScrollY != 28 {
|
||||||
t.Errorf("ScrollY() = %d, want 28", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want 28", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 43 {
|
if m.ActiveWindow().Cursor.Line != 43 {
|
||||||
t.Errorf("CursorY() = %d, want 43", m.CursorY())
|
t.Errorf("CursorY() = %d, want 43", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -284,26 +284,26 @@ func TestHalfPageScrollUp(t *testing.T) {
|
|||||||
// cursor at line 50: AdjustScroll -> scrollY=31, relY=19
|
// cursor at line 50: AdjustScroll -> scrollY=31, relY=19
|
||||||
// After ctrl+u: newScrollY=17, newCursorY=17+19=36
|
// After ctrl+u: newScrollY=17, newCursorY=17+19=36
|
||||||
lines := generateLines(100)
|
lines := generateLines(100)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 50}, 80, 30)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 50}, 80, 30)
|
||||||
sendKeys(tm, "ctrl+u")
|
sendKeys(tm, "ctrl+u")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ScrollY() != 17 {
|
if m.ActiveWindow().ScrollY != 17 {
|
||||||
t.Errorf("ScrollY() = %d, want 17", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want 17", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 36 {
|
if m.ActiveWindow().Cursor.Line != 36 {
|
||||||
t.Errorf("CursorY() = %d, want 36", m.CursorY())
|
t.Errorf("CursorY() = %d, want 36", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ctrl+u preserves cursor relative position in viewport", func(t *testing.T) {
|
t.Run("ctrl+u preserves cursor relative position in viewport", func(t *testing.T) {
|
||||||
// relY=19 before and after
|
// relY=19 before and after
|
||||||
lines := generateLines(100)
|
lines := generateLines(100)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 50}, 80, 30)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 50}, 80, 30)
|
||||||
sendKeys(tm, "ctrl+u")
|
sendKeys(tm, "ctrl+u")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
relY := m.CursorY() - m.ScrollY()
|
relY := m.ActiveWindow().Cursor.Line - m.ActiveWindow().ScrollY
|
||||||
if relY != 19 {
|
if relY != 19 {
|
||||||
t.Errorf("relative position = %d, want 19", relY)
|
t.Errorf("relative position = %d, want 19", relY)
|
||||||
}
|
}
|
||||||
@ -313,18 +313,18 @@ func TestHalfPageScrollUp(t *testing.T) {
|
|||||||
// cursor at line 10, scrollY=0, relY=10 (in safe zone)
|
// cursor at line 10, scrollY=0, relY=10 (in safe zone)
|
||||||
// ctrl+u: newScrollY=max(0,-14)=0, relY=10 preserved, cursorY=10
|
// ctrl+u: newScrollY=max(0,-14)=0, relY=10 preserved, cursorY=10
|
||||||
lines := generateLines(100)
|
lines := generateLines(100)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 10}, 80, 30)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 10}, 80, 30)
|
||||||
sendKeys(tm, "ctrl+u")
|
sendKeys(tm, "ctrl+u")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ScrollY() < 0 {
|
if m.ActiveWindow().ScrollY < 0 {
|
||||||
t.Errorf("ScrollY() = %d, want >= 0", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want >= 0", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
if m.ScrollY() != 0 {
|
if m.ActiveWindow().ScrollY != 0 {
|
||||||
t.Errorf("ScrollY() = %d, want 0", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want 0", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 10 {
|
if m.ActiveWindow().Cursor.Line != 10 {
|
||||||
t.Errorf("CursorY() = %d, want 10", m.CursorY())
|
t.Errorf("CursorY() = %d, want 10", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -332,15 +332,15 @@ func TestHalfPageScrollUp(t *testing.T) {
|
|||||||
// cursor at line 5, scrollY=0, relY=5 < scrollOff=8
|
// cursor at line 5, scrollY=0, relY=5 < scrollOff=8
|
||||||
// ctrl+u: newScrollY=0; relY clamp to 8; newCursorY=8
|
// ctrl+u: newScrollY=0; relY clamp to 8; newCursorY=8
|
||||||
lines := generateLines(100)
|
lines := generateLines(100)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 5}, 80, 30)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 5}, 80, 30)
|
||||||
sendKeys(tm, "ctrl+u")
|
sendKeys(tm, "ctrl+u")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ScrollY() != 0 {
|
if m.ActiveWindow().ScrollY != 0 {
|
||||||
t.Errorf("ScrollY() = %d, want 0", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want 0", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 8 {
|
if m.ActiveWindow().Cursor.Line != 8 {
|
||||||
t.Errorf("CursorY() = %d, want 8", m.CursorY())
|
t.Errorf("CursorY() = %d, want 8", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -349,15 +349,15 @@ func TestHalfPageScrollUp(t *testing.T) {
|
|||||||
// ctrl+u #1: newScrollY=47, cursorY=66
|
// ctrl+u #1: newScrollY=47, cursorY=66
|
||||||
// ctrl+u #2: newScrollY=33, cursorY=52
|
// ctrl+u #2: newScrollY=33, cursorY=52
|
||||||
lines := generateLines(200)
|
lines := generateLines(200)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 80}, 80, 30)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 80}, 80, 30)
|
||||||
sendKeys(tm, "ctrl+u", "ctrl+u")
|
sendKeys(tm, "ctrl+u", "ctrl+u")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ScrollY() != 33 {
|
if m.ActiveWindow().ScrollY != 33 {
|
||||||
t.Errorf("ScrollY() = %d, want 33", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want 33", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 52 {
|
if m.ActiveWindow().Cursor.Line != 52 {
|
||||||
t.Errorf("CursorY() = %d, want 52", m.CursorY())
|
t.Errorf("CursorY() = %d, want 52", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -368,15 +368,15 @@ func TestHalfPageScrollRoundTrip(t *testing.T) {
|
|||||||
// ctrl+d: scrollY=14, cursorY=29, relY=15
|
// ctrl+d: scrollY=14, cursorY=29, relY=15
|
||||||
// ctrl+u: newScrollY=max(0,14-14)=0, cursorY=0+15=15
|
// ctrl+u: newScrollY=max(0,14-14)=0, cursorY=0+15=15
|
||||||
lines := generateLines(200)
|
lines := generateLines(200)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 15}, 80, 30)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 15}, 80, 30)
|
||||||
sendKeys(tm, "ctrl+d", "ctrl+u")
|
sendKeys(tm, "ctrl+d", "ctrl+u")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ScrollY() != 0 {
|
if m.ActiveWindow().ScrollY != 0 {
|
||||||
t.Errorf("ScrollY() = %d, want 0", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want 0", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 15 {
|
if m.ActiveWindow().Cursor.Line != 15 {
|
||||||
t.Errorf("CursorY() = %d, want 15", m.CursorY())
|
t.Errorf("CursorY() = %d, want 15", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -385,29 +385,29 @@ func TestHalfPageScrollRoundTrip(t *testing.T) {
|
|||||||
// ctrl+u: scrollY=17, cursorY=36, relY=19
|
// ctrl+u: scrollY=17, cursorY=36, relY=19
|
||||||
// ctrl+d: scrollY=31, cursorY=50, relY=19
|
// ctrl+d: scrollY=31, cursorY=50, relY=19
|
||||||
lines := generateLines(200)
|
lines := generateLines(200)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 50}, 80, 30)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 50}, 80, 30)
|
||||||
sendKeys(tm, "ctrl+u", "ctrl+d")
|
sendKeys(tm, "ctrl+u", "ctrl+d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ScrollY() != 31 {
|
if m.ActiveWindow().ScrollY != 31 {
|
||||||
t.Errorf("ScrollY() = %d, want 31", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want 31", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 50 {
|
if m.ActiveWindow().Cursor.Line != 50 {
|
||||||
t.Errorf("CursorY() = %d, want 50", m.CursorY())
|
t.Errorf("CursorY() = %d, want 50", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("alternating ctrl+d and ctrl+u maintains scroll stability", func(t *testing.T) {
|
t.Run("alternating ctrl+d and ctrl+u maintains scroll stability", func(t *testing.T) {
|
||||||
lines := generateLines(200)
|
lines := generateLines(200)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 15}, 80, 30)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 15}, 80, 30)
|
||||||
sendKeys(tm, "ctrl+d", "ctrl+u", "ctrl+d", "ctrl+u")
|
sendKeys(tm, "ctrl+d", "ctrl+u", "ctrl+d", "ctrl+u")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ScrollY() != 0 {
|
if m.ActiveWindow().ScrollY != 0 {
|
||||||
t.Errorf("ScrollY() = %d, want 0 after 2 round trips", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want 0 after 2 round trips", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 15 {
|
if m.ActiveWindow().Cursor.Line != 15 {
|
||||||
t.Errorf("CursorY() = %d, want 15 after 2 round trips", m.CursorY())
|
t.Errorf("CursorY() = %d, want 15 after 2 round trips", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -415,29 +415,29 @@ func TestHalfPageScrollRoundTrip(t *testing.T) {
|
|||||||
func TestScrollWithCount(t *testing.T) {
|
func TestScrollWithCount(t *testing.T) {
|
||||||
t.Run("5j scrolls appropriately", func(t *testing.T) {
|
t.Run("5j scrolls appropriately", func(t *testing.T) {
|
||||||
lines := generateLines(50)
|
lines := generateLines(50)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 5}, 80, 20)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 5}, 80, 20)
|
||||||
|
|
||||||
sendKeys(tm, "1", "0", "j") // move down 10 lines
|
sendKeys(tm, "1", "0", "j") // move down 10 lines
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 15 {
|
if m.ActiveWindow().Cursor.Line != 15 {
|
||||||
t.Errorf("CursorY() = %d, want 15", m.CursorY())
|
t.Errorf("CursorY() = %d, want 15", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
// Should have scrolled since we moved past the safe zone
|
// Should have scrolled since we moved past the safe zone
|
||||||
if m.ScrollY() == 0 {
|
if m.ActiveWindow().ScrollY == 0 {
|
||||||
t.Errorf("ScrollY() = %d, want > 0", m.ScrollY())
|
t.Errorf("ScrollY() = %d, want > 0", m.ActiveWindow().ScrollY)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("5k scrolls appropriately", func(t *testing.T) {
|
t.Run("5k scrolls appropriately", func(t *testing.T) {
|
||||||
lines := generateLines(50)
|
lines := generateLines(50)
|
||||||
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 25}, 80, 20)
|
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 25}, 80, 20)
|
||||||
|
|
||||||
sendKeys(tm, "1", "5", "k") // move up 15 lines
|
sendKeys(tm, "1", "5", "k") // move up 15 lines
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 10 {
|
if m.ActiveWindow().Cursor.Line != 10 {
|
||||||
t.Errorf("CursorY() = %d, want 10", m.CursorY())
|
t.Errorf("CursorY() = %d, want 10", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package editor
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NOTE: Lots of AI tests here
|
// NOTE: Lots of AI tests here
|
||||||
@ -13,18 +13,18 @@ import (
|
|||||||
func TestVisualModeSelectionState(t *testing.T) {
|
func TestVisualModeSelectionState(t *testing.T) {
|
||||||
t.Run("test 'v' enters visual mode and sets anchor at cursor", func(t *testing.T) {
|
t.Run("test 'v' enters visual mode and sets anchor at cursor", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||||
sendKeys(tm, "v")
|
sendKeys(tm, "v")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Mode() != action.VisualMode {
|
if m.Mode() != core.VisualMode {
|
||||||
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
|
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
|
||||||
}
|
}
|
||||||
if m.AnchorX() != 3 {
|
if m.ActiveWindow().Anchor.Col != 3 {
|
||||||
t.Errorf("AnchorX() = %d, want 3", m.AnchorX())
|
t.Errorf("AnchorX() = %d, want 3", m.ActiveWindow().Anchor.Col)
|
||||||
}
|
}
|
||||||
if m.AnchorY() != 0 {
|
if m.ActiveWindow().Anchor.Line != 0 {
|
||||||
t.Errorf("AnchorY() = %d, want 0", m.AnchorY())
|
t.Errorf("AnchorY() = %d, want 0", m.ActiveWindow().Anchor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -34,73 +34,73 @@ func TestVisualModeSelectionState(t *testing.T) {
|
|||||||
sendKeys(tm, "v", "l")
|
sendKeys(tm, "v", "l")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.AnchorX() != 0 {
|
if m.ActiveWindow().Anchor.Col != 0 {
|
||||||
t.Errorf("AnchorX() = %d, want 0", m.AnchorX())
|
t.Errorf("AnchorX() = %d, want 0", m.ActiveWindow().Anchor.Col)
|
||||||
}
|
}
|
||||||
if m.CursorX() != 1 {
|
if m.ActiveWindow().Cursor.Col != 1 {
|
||||||
t.Errorf("CursorX() = %d, want 1", m.CursorX())
|
t.Errorf("CursorX() = %d, want 1", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'vh' creates backward selection", func(t *testing.T) {
|
t.Run("test 'vh' creates backward selection", func(t *testing.T) {
|
||||||
lines := []string{"hello world"}
|
lines := []string{"hello world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||||
sendKeys(tm, "v", "h")
|
sendKeys(tm, "v", "h")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.AnchorX() != 3 {
|
if m.ActiveWindow().Anchor.Col != 3 {
|
||||||
t.Errorf("AnchorX() = %d, want 3", m.AnchorX())
|
t.Errorf("AnchorX() = %d, want 3", m.ActiveWindow().Anchor.Col)
|
||||||
}
|
}
|
||||||
if m.CursorX() != 2 {
|
if m.ActiveWindow().Cursor.Col != 2 {
|
||||||
t.Errorf("CursorX() = %d, want 2", m.CursorX())
|
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'vj' extends selection down", func(t *testing.T) {
|
t.Run("test 'vj' extends selection down", func(t *testing.T) {
|
||||||
lines := []string{"hello", "world"}
|
lines := []string{"hello", "world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "v", "j")
|
sendKeys(tm, "v", "j")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.AnchorX() != 2 {
|
if m.ActiveWindow().Anchor.Col != 2 {
|
||||||
t.Errorf("AnchorX() = %d, want 2", m.AnchorX())
|
t.Errorf("AnchorX() = %d, want 2", m.ActiveWindow().Anchor.Col)
|
||||||
}
|
}
|
||||||
if m.AnchorY() != 0 {
|
if m.ActiveWindow().Anchor.Line != 0 {
|
||||||
t.Errorf("AnchorY() = %d, want 0", m.AnchorY())
|
t.Errorf("AnchorY() = %d, want 0", m.ActiveWindow().Anchor.Line)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 1 {
|
if m.ActiveWindow().Cursor.Line != 1 {
|
||||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'V' enters visual line mode and sets anchor at cursor", func(t *testing.T) {
|
t.Run("test 'V' enters visual line mode and sets anchor at cursor", func(t *testing.T) {
|
||||||
lines := []string{"hello", "world"}
|
lines := []string{"hello", "world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
||||||
sendKeys(tm, "V")
|
sendKeys(tm, "V")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Mode() != action.VisualLineMode {
|
if m.Mode() != core.VisualLineMode {
|
||||||
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
|
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
|
||||||
}
|
}
|
||||||
if m.AnchorY() != 1 {
|
if m.ActiveWindow().Anchor.Line != 1 {
|
||||||
t.Errorf("AnchorY() = %d, want 1", m.AnchorY())
|
t.Errorf("AnchorY() = %d, want 1", m.ActiveWindow().Anchor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'ctrl+v' enters visual block mode and sets anchor at cursor", func(t *testing.T) {
|
t.Run("test 'ctrl+v' enters visual block mode and sets anchor at cursor", func(t *testing.T) {
|
||||||
lines := []string{"hello", "world"}
|
lines := []string{"hello", "world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 1})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 1})
|
||||||
sendKeys(tm, "ctrl+v")
|
sendKeys(tm, "ctrl+v")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Mode() != action.VisualBlockMode {
|
if m.Mode() != core.VisualBlockMode {
|
||||||
t.Errorf("Mode() = %v, want VisualBlockMode", m.Mode())
|
t.Errorf("Mode() = %v, want VisualBlockMode", m.Mode())
|
||||||
}
|
}
|
||||||
if m.AnchorX() != 2 {
|
if m.ActiveWindow().Anchor.Col != 2 {
|
||||||
t.Errorf("AnchorX() = %d, want 2", m.AnchorX())
|
t.Errorf("AnchorX() = %d, want 2", m.ActiveWindow().Anchor.Col)
|
||||||
}
|
}
|
||||||
if m.AnchorY() != 1 {
|
if m.ActiveWindow().Anchor.Line != 1 {
|
||||||
t.Errorf("AnchorY() = %d, want 1", m.AnchorY())
|
t.Errorf("AnchorY() = %d, want 1", m.ActiveWindow().Anchor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ func TestVisualModeSelectionState(t *testing.T) {
|
|||||||
sendKeys(tm, "v", "l", "l", "esc")
|
sendKeys(tm, "v", "l", "l", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Mode() != action.NormalMode {
|
if m.Mode() != core.NormalMode {
|
||||||
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
|
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -125,11 +125,11 @@ func TestVisualModeDelete(t *testing.T) {
|
|||||||
sendKeys(tm, "v", "d")
|
sendKeys(tm, "v", "d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "ello" {
|
if m.ActiveBuffer().Lines[0] != "ello" {
|
||||||
t.Errorf("Line(0) = %q, want \"ello\"", m.Line(0))
|
t.Errorf("Line(0) = %q, want \"ello\"", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -139,47 +139,47 @@ func TestVisualModeDelete(t *testing.T) {
|
|||||||
sendKeys(tm, "v", "l", "l", "l", "d")
|
sendKeys(tm, "v", "l", "l", "l", "d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "o world" {
|
if m.ActiveBuffer().Lines[0] != "o world" {
|
||||||
t.Errorf("Line(0) = %q, want \"o world\"", m.Line(0))
|
t.Errorf("Line(0) = %q, want \"o world\"", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'v' backward selection 'hh d' deletes correct range", func(t *testing.T) {
|
t.Run("test 'v' backward selection 'hh d' deletes correct range", func(t *testing.T) {
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||||
sendKeys(tm, "v", "h", "h", "d")
|
sendKeys(tm, "v", "h", "h", "d")
|
||||||
|
|
||||||
// anchor=3, cursor=1 → normalized start=1, end=3 → delete "ell" → "ho"
|
// anchor=3, cursor=1 → normalized start=1, end=3 → delete "ell" → "ho"
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "ho" {
|
if m.ActiveBuffer().Lines[0] != "ho" {
|
||||||
t.Errorf("Line(0) = %q, want \"ho\"", m.Line(0))
|
t.Errorf("Line(0) = %q, want \"ho\"", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 1 {
|
if m.ActiveWindow().Cursor.Col != 1 {
|
||||||
t.Errorf("CursorX() = %d, want 1", m.CursorX())
|
t.Errorf("CursorX() = %d, want 1", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'vj d' deletes char selection across two lines", func(t *testing.T) {
|
t.Run("test 'vj d' deletes char selection across two lines", func(t *testing.T) {
|
||||||
lines := []string{"hello", "world"}
|
lines := []string{"hello", "world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||||
sendKeys(tm, "v", "j", "d")
|
sendKeys(tm, "v", "j", "d")
|
||||||
|
|
||||||
// start=(2,0), end=(2,1) → prefix="he", suffix="ld" → "held"
|
// start=(2,0), end=(2,1) → prefix="he", suffix="ld" → "held"
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 1 {
|
if m.ActiveBuffer().LineCount() != 1 {
|
||||||
t.Errorf("LineCount() = %d, want 1", m.LineCount())
|
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(0) != "held" {
|
if m.ActiveBuffer().Lines[0] != "held" {
|
||||||
t.Errorf("Line(0) = %q, want \"held\"", m.Line(0))
|
t.Errorf("Line(0) = %q, want \"held\"", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 2 {
|
if m.ActiveWindow().Cursor.Col != 2 {
|
||||||
t.Errorf("CursorX() = %d, want 2", m.CursorX())
|
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -189,14 +189,14 @@ func TestVisualModeDelete(t *testing.T) {
|
|||||||
sendKeys(tm, "V", "d")
|
sendKeys(tm, "V", "d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 1 {
|
if m.ActiveBuffer().LineCount() != 1 {
|
||||||
t.Errorf("LineCount() = %d, want 1", m.LineCount())
|
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(0) != "world" {
|
if m.ActiveBuffer().Lines[0] != "world" {
|
||||||
t.Errorf("Line(0) = %q, want \"world\"", m.Line(0))
|
t.Errorf("Line(0) = %q, want \"world\"", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -206,29 +206,29 @@ func TestVisualModeDelete(t *testing.T) {
|
|||||||
sendKeys(tm, "V", "j", "d")
|
sendKeys(tm, "V", "j", "d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 1 {
|
if m.ActiveBuffer().LineCount() != 1 {
|
||||||
t.Errorf("LineCount() = %d, want 1", m.LineCount())
|
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(0) != "testing" {
|
if m.ActiveBuffer().Lines[0] != "testing" {
|
||||||
t.Errorf("Line(0) = %q, want \"testing\"", m.Line(0))
|
t.Errorf("Line(0) = %q, want \"testing\"", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'Vkd' deletes two lines with backward selection", func(t *testing.T) {
|
t.Run("test 'Vkd' deletes two lines with backward selection", func(t *testing.T) {
|
||||||
lines := []string{"hello", "world", "testing"}
|
lines := []string{"hello", "world", "testing"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
|
||||||
sendKeys(tm, "V", "k", "d")
|
sendKeys(tm, "V", "k", "d")
|
||||||
|
|
||||||
// anchor=line2, cursor=line1 → normalized start=line1, end=line2 → delete both
|
// anchor=line2, cursor=line1 → normalized start=line1, end=line2 → delete both
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 1 {
|
if m.ActiveBuffer().LineCount() != 1 {
|
||||||
t.Errorf("LineCount() = %d, want 1", m.LineCount())
|
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(0) != "hello" {
|
if m.ActiveBuffer().Lines[0] != "hello" {
|
||||||
t.Errorf("Line(0) = %q, want \"hello\"", m.Line(0))
|
t.Errorf("Line(0) = %q, want \"hello\"", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -241,34 +241,34 @@ func TestVisualModeDelete(t *testing.T) {
|
|||||||
// "hello"[:0]+"hello"[2:] = "llo"
|
// "hello"[:0]+"hello"[2:] = "llo"
|
||||||
// "world"[:0]+"world"[2:] = "rld"
|
// "world"[:0]+"world"[2:] = "rld"
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "llo" {
|
if m.ActiveBuffer().Lines[0] != "llo" {
|
||||||
t.Errorf("Line(0) = %q, want \"llo\"", m.Line(0))
|
t.Errorf("Line(0) = %q, want \"llo\"", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.Line(1) != "rld" {
|
if m.ActiveBuffer().Lines[1] != "rld" {
|
||||||
t.Errorf("Line(1) = %q, want \"rld\"", m.Line(1))
|
t.Errorf("Line(1) = %q, want \"rld\"", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'ctrl+v' backward col selection deletes correct block", func(t *testing.T) {
|
t.Run("test 'ctrl+v' backward col selection deletes correct block", func(t *testing.T) {
|
||||||
lines := []string{"hello", "world"}
|
lines := []string{"hello", "world"}
|
||||||
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
|
||||||
sendKeys(tm, "ctrl+v", "h", "h", "j", "d")
|
sendKeys(tm, "ctrl+v", "h", "h", "j", "d")
|
||||||
|
|
||||||
// anchor=(3,0), cursor=(1,1) → cols min(3,1)=1 to max(3,1)=3, lines 0-1
|
// anchor=(3,0), cursor=(1,1) → cols min(3,1)=1 to max(3,1)=3, lines 0-1
|
||||||
// "hello"[:1]+"hello"[4:] = "h"+"o" = "ho"
|
// "hello"[:1]+"hello"[4:] = "h"+"o" = "ho"
|
||||||
// "world"[:1]+"world"[4:] = "w"+"d" = "wd"
|
// "world"[:1]+"world"[4:] = "w"+"d" = "wd"
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "ho" {
|
if m.ActiveBuffer().Lines[0] != "ho" {
|
||||||
t.Errorf("Line(0) = %q, want \"ho\"", m.Line(0))
|
t.Errorf("Line(0) = %q, want \"ho\"", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.Line(1) != "wd" {
|
if m.ActiveBuffer().Lines[1] != "wd" {
|
||||||
t.Errorf("Line(1) = %q, want \"wd\"", m.Line(1))
|
t.Errorf("Line(1) = %q, want \"wd\"", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -279,107 +279,107 @@ func TestVisualModeWordMotions(t *testing.T) {
|
|||||||
t.Run("test 'vw' selects to next word", func(t *testing.T) {
|
t.Run("test 'vw' selects to next word", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "w")
|
sendKeys(tm, "v", "w")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.AnchorX() != 0 {
|
if m.ActiveWindow().Anchor.Col != 0 {
|
||||||
t.Errorf("AnchorX() = %d, want 0", m.AnchorX())
|
t.Errorf("AnchorX() = %d, want 0", m.ActiveWindow().Anchor.Col)
|
||||||
}
|
}
|
||||||
// w moves to start of "world" at col 6
|
// w moves to start of "world" at col 6
|
||||||
if m.CursorX() != 6 {
|
if m.ActiveWindow().Cursor.Col != 6 {
|
||||||
t.Errorf("CursorX() = %d, want 6", m.CursorX())
|
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'vwd' deletes word plus space", func(t *testing.T) {
|
t.Run("test 'vwd' deletes word plus space", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "w", "d")
|
sendKeys(tm, "v", "w", "d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Deletes from 0 to 6 inclusive = "hello w", leaves "orld"
|
// Deletes from 0 to 6 inclusive = "hello w", leaves "orld"
|
||||||
if m.Line(0) != "orld" {
|
if m.ActiveBuffer().Lines[0] != "orld" {
|
||||||
t.Errorf("Line(0) = %q, want 'orld'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'orld'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 've' selects to end of word", func(t *testing.T) {
|
t.Run("test 've' selects to end of word", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "e")
|
sendKeys(tm, "v", "e")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.AnchorX() != 0 {
|
if m.ActiveWindow().Anchor.Col != 0 {
|
||||||
t.Errorf("AnchorX() = %d, want 0", m.AnchorX())
|
t.Errorf("AnchorX() = %d, want 0", m.ActiveWindow().Anchor.Col)
|
||||||
}
|
}
|
||||||
// e moves to end of "hello" at col 4
|
// e moves to end of "hello" at col 4
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'ved' deletes word", func(t *testing.T) {
|
t.Run("test 'ved' deletes word", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "e", "d")
|
sendKeys(tm, "v", "e", "d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Deletes "hello"
|
// Deletes "hello"
|
||||||
if m.Line(0) != " world" {
|
if m.ActiveBuffer().Lines[0] != " world" {
|
||||||
t.Errorf("Line(0) = %q, want ' world'", m.Line(0))
|
t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'vb' selects backward word", func(t *testing.T) {
|
t.Run("test 'vb' selects backward word", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 6}), // on 'w'
|
WithCursorPos(core.Position{Line: 0, Col: 6}), // on 'w'
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "b")
|
sendKeys(tm, "v", "b")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.AnchorX() != 6 {
|
if m.ActiveWindow().Anchor.Col != 6 {
|
||||||
t.Errorf("AnchorX() = %d, want 6", m.AnchorX())
|
t.Errorf("AnchorX() = %d, want 6", m.ActiveWindow().Anchor.Col)
|
||||||
}
|
}
|
||||||
// b moves to start of "hello" at col 0
|
// b moves to start of "hello" at col 0
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'vbd' deletes backward to word start", func(t *testing.T) {
|
t.Run("test 'vbd' deletes backward to word start", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 6}), // on 'w'
|
WithCursorPos(core.Position{Line: 0, Col: 6}), // on 'w'
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "b", "d")
|
sendKeys(tm, "v", "b", "d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Deletes from "h" (0) to "w" (6) inclusive
|
// Deletes from "h" (0) to "w" (6) inclusive
|
||||||
if m.Line(0) != "orld" {
|
if m.ActiveBuffer().Lines[0] != "orld" {
|
||||||
t.Errorf("Line(0) = %q, want 'orld'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'orld'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'v2w' selects two words", func(t *testing.T) {
|
t.Run("test 'v2w' selects two words", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"one two three"}),
|
WithLines([]string{"one two three"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "2", "w")
|
sendKeys(tm, "v", "2", "w")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// 2w moves past "one " and "two " to start of "three" at col 8
|
// 2w moves past "one " and "two " to start of "three" at col 8
|
||||||
if m.CursorX() != 8 {
|
if m.ActiveWindow().Cursor.Col != 8 {
|
||||||
t.Errorf("CursorX() = %d, want 8", m.CursorX())
|
t.Errorf("CursorX() = %d, want 8", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -390,145 +390,145 @@ func TestVisualModeJumpMotions(t *testing.T) {
|
|||||||
t.Run("test 'v$' selects to end of line", func(t *testing.T) {
|
t.Run("test 'v$' selects to end of line", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "$")
|
sendKeys(tm, "v", "$")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.AnchorX() != 0 {
|
if m.ActiveWindow().Anchor.Col != 0 {
|
||||||
t.Errorf("AnchorX() = %d, want 0", m.AnchorX())
|
t.Errorf("AnchorX() = %d, want 0", m.ActiveWindow().Anchor.Col)
|
||||||
}
|
}
|
||||||
// $ moves past end of line
|
// $ moves past end of line
|
||||||
if m.CursorX() != 11 {
|
if m.ActiveWindow().Cursor.Col != 11 {
|
||||||
t.Errorf("CursorX() = %d, want 11", m.CursorX())
|
t.Errorf("CursorX() = %d, want 11", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'v$d' deletes to end of line", func(t *testing.T) {
|
t.Run("test 'v$d' deletes to end of line", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 6}), // on 'w'
|
WithCursorPos(core.Position{Line: 0, Col: 6}), // on 'w'
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "$", "d")
|
sendKeys(tm, "v", "$", "d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "hello " {
|
if m.ActiveBuffer().Lines[0] != "hello " {
|
||||||
t.Errorf("Line(0) = %q, want 'hello '", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'hello '", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'v0' selects to beginning of line", func(t *testing.T) {
|
t.Run("test 'v0' selects to beginning of line", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 6}),
|
WithCursorPos(core.Position{Line: 0, Col: 6}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "0")
|
sendKeys(tm, "v", "0")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.AnchorX() != 6 {
|
if m.ActiveWindow().Anchor.Col != 6 {
|
||||||
t.Errorf("AnchorX() = %d, want 6", m.AnchorX())
|
t.Errorf("AnchorX() = %d, want 6", m.ActiveWindow().Anchor.Col)
|
||||||
}
|
}
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'v0d' deletes to beginning of line", func(t *testing.T) {
|
t.Run("test 'v0d' deletes to beginning of line", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 6}), // on 'w'
|
WithCursorPos(core.Position{Line: 0, Col: 6}), // on 'w'
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "0", "d")
|
sendKeys(tm, "v", "0", "d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// Deletes from 'h' (0) to 'w' (6) inclusive
|
// Deletes from 'h' (0) to 'w' (6) inclusive
|
||||||
if m.Line(0) != "orld" {
|
if m.ActiveBuffer().Lines[0] != "orld" {
|
||||||
t.Errorf("Line(0) = %q, want 'orld'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'orld'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'v_' selects to first non-whitespace", func(t *testing.T) {
|
t.Run("test 'v_' selects to first non-whitespace", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{" hello world"}),
|
WithLines([]string{" hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 10}), // on 'w'
|
WithCursorPos(core.Position{Line: 0, Col: 10}), // on 'w'
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "_")
|
sendKeys(tm, "v", "_")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.AnchorX() != 10 {
|
if m.ActiveWindow().Anchor.Col != 10 {
|
||||||
t.Errorf("AnchorX() = %d, want 10", m.AnchorX())
|
t.Errorf("AnchorX() = %d, want 10", m.ActiveWindow().Anchor.Col)
|
||||||
}
|
}
|
||||||
// _ moves to first non-ws at col 4
|
// _ moves to first non-ws at col 4
|
||||||
if m.CursorX() != 4 {
|
if m.ActiveWindow().Cursor.Col != 4 {
|
||||||
t.Errorf("CursorX() = %d, want 4", m.CursorX())
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'vG' selects to bottom of file", func(t *testing.T) {
|
t.Run("test 'vG' selects to bottom of file", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "G")
|
sendKeys(tm, "v", "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.AnchorY() != 0 {
|
if m.ActiveWindow().Anchor.Line != 0 {
|
||||||
t.Errorf("AnchorY() = %d, want 0", m.AnchorY())
|
t.Errorf("AnchorY() = %d, want 0", m.ActiveWindow().Anchor.Line)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 2 {
|
if m.ActiveWindow().Cursor.Line != 2 {
|
||||||
t.Errorf("CursorY() = %d, want 2", m.CursorY())
|
t.Errorf("CursorY() = %d, want 2", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'vGd' deletes to bottom of file", func(t *testing.T) {
|
t.Run("test 'vGd' deletes to bottom of file", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 3}), // on 'e' of "line"
|
WithCursorPos(core.Position{Line: 0, Col: 3}), // on 'e' of "line"
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "G", "d")
|
sendKeys(tm, "v", "G", "d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// G goes to last line at same col, deletes from (0,3) to (2,3)
|
// G goes to last line at same col, deletes from (0,3) to (2,3)
|
||||||
// Keeps "lin" from first line + "e 3" from last line = "lin 3"
|
// Keeps "lin" from first line + "e 3" from last line = "lin 3"
|
||||||
if m.LineCount() != 1 {
|
if m.ActiveBuffer().LineCount() != 1 {
|
||||||
t.Errorf("LineCount() = %d, want 1", m.LineCount())
|
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(0) != "lin 3" {
|
if m.ActiveBuffer().Lines[0] != "lin 3" {
|
||||||
t.Errorf("Line(0) = %q, want 'lin 3'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'lin 3'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'vgg' selects to top of file", func(t *testing.T) {
|
t.Run("test 'vgg' selects to top of file", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 2, Col: 0}),
|
WithCursorPos(core.Position{Line: 2, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "g", "g")
|
sendKeys(tm, "v", "g", "g")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.AnchorY() != 2 {
|
if m.ActiveWindow().Anchor.Line != 2 {
|
||||||
t.Errorf("AnchorY() = %d, want 2", m.AnchorY())
|
t.Errorf("AnchorY() = %d, want 2", m.ActiveWindow().Anchor.Line)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'vggd' deletes to top of file", func(t *testing.T) {
|
t.Run("test 'vggd' deletes to top of file", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 2, Col: 3}),
|
WithCursorPos(core.Position{Line: 2, Col: 3}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "g", "g", "d")
|
sendKeys(tm, "v", "g", "g", "d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// gg goes to first line at same col, deletes selection
|
// gg goes to first line at same col, deletes selection
|
||||||
// Keeps "lin" from first line + " 3" from last line = "lin 3"
|
// Keeps "lin" from first line + " 3" from last line = "lin 3"
|
||||||
if m.LineCount() != 1 {
|
if m.ActiveBuffer().LineCount() != 1 {
|
||||||
t.Errorf("LineCount() = %d, want 1", m.LineCount())
|
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(0) != "lin 3" {
|
if m.ActiveBuffer().Lines[0] != "lin 3" {
|
||||||
t.Errorf("Line(0) = %q, want 'lin 3'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'lin 3'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -539,49 +539,49 @@ func TestVisualLineModeJumpMotions(t *testing.T) {
|
|||||||
t.Run("test 'VG' selects all lines to bottom", func(t *testing.T) {
|
t.Run("test 'VG' selects all lines to bottom", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "V", "G")
|
sendKeys(tm, "V", "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.AnchorY() != 0 {
|
if m.ActiveWindow().Anchor.Line != 0 {
|
||||||
t.Errorf("AnchorY() = %d, want 0", m.AnchorY())
|
t.Errorf("AnchorY() = %d, want 0", m.ActiveWindow().Anchor.Line)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 2 {
|
if m.ActiveWindow().Cursor.Line != 2 {
|
||||||
t.Errorf("CursorY() = %d, want 2", m.CursorY())
|
t.Errorf("CursorY() = %d, want 2", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'VGd' deletes all lines", func(t *testing.T) {
|
t.Run("test 'VGd' deletes all lines", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "V", "G", "d")
|
sendKeys(tm, "V", "G", "d")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
// All lines deleted, should have empty buffer
|
// All lines deleted, should have empty buffer
|
||||||
if m.LineCount() != 1 {
|
if m.ActiveBuffer().LineCount() != 1 {
|
||||||
t.Errorf("LineCount() = %d, want 1", m.LineCount())
|
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(0) != "" {
|
if m.ActiveBuffer().Lines[0] != "" {
|
||||||
t.Errorf("Line(0) = %q, want ''", m.Line(0))
|
t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test 'Vgg' selects lines to top", func(t *testing.T) {
|
t.Run("test 'Vgg' selects lines to top", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 2, Col: 0}),
|
WithCursorPos(core.Position{Line: 2, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "V", "g", "g")
|
sendKeys(tm, "V", "g", "g")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.AnchorY() != 2 {
|
if m.ActiveWindow().Anchor.Line != 2 {
|
||||||
t.Errorf("AnchorY() = %d, want 2", m.AnchorY())
|
t.Errorf("AnchorY() = %d, want 2", m.ActiveWindow().Anchor.Line)
|
||||||
}
|
}
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@ -15,7 +15,7 @@ func TestYankLineBasic(t *testing.T) {
|
|||||||
t.Run("yy yanks current line to register", func(t *testing.T) {
|
t.Run("yy yanks current line to register", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "y")
|
sendKeys(tm, "y", "y")
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ func TestYankLineBasic(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.LinewiseRegister {
|
if reg.Type != core.LinewiseRegister {
|
||||||
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
|
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
if len(reg.Content) != 1 {
|
if len(reg.Content) != 1 {
|
||||||
@ -38,45 +38,45 @@ func TestYankLineBasic(t *testing.T) {
|
|||||||
t.Run("yy does not modify buffer", func(t *testing.T) {
|
t.Run("yy does not modify buffer", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 1, Col: 0}),
|
WithCursorPos(core.Position{Line: 1, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "y")
|
sendKeys(tm, "y", "y")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 3 {
|
if m.ActiveBuffer().LineCount() != 3 {
|
||||||
t.Errorf("LineCount() = %d, want 3", m.LineCount())
|
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(0) != "line 1" {
|
if m.ActiveBuffer().Lines[0] != "line 1" {
|
||||||
t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.Line(1) != "line 2" {
|
if m.ActiveBuffer().Lines[1] != "line 2" {
|
||||||
t.Errorf("Line(1) = %q, want 'line 2'", m.Line(1))
|
t.Errorf("Line(1) = %q, want 'line 2'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
if m.Line(2) != "line 3" {
|
if m.ActiveBuffer().Lines[2] != "line 3" {
|
||||||
t.Errorf("Line(2) = %q, want 'line 3'", m.Line(2))
|
t.Errorf("Line(2) = %q, want 'line 3'", m.ActiveBuffer().Lines[2])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("yy does not move cursor", func(t *testing.T) {
|
t.Run("yy does not move cursor", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 1, Col: 3}),
|
WithCursorPos(core.Position{Line: 1, Col: 3}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "y")
|
sendKeys(tm, "y", "y")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 1 {
|
if m.ActiveWindow().Cursor.Line != 1 {
|
||||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
if m.CursorX() != 3 {
|
if m.ActiveWindow().Cursor.Col != 3 {
|
||||||
t.Errorf("CursorX() = %d, want 3", m.CursorX())
|
t.Errorf("CursorX() = %d, want 3", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("yy from middle of file", func(t *testing.T) {
|
t.Run("yy from middle of file", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"first", "second", "third", "fourth"}),
|
WithLines([]string{"first", "second", "third", "fourth"}),
|
||||||
WithCursorPos(action.Position{Line: 2, Col: 0}),
|
WithCursorPos(core.Position{Line: 2, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "y")
|
sendKeys(tm, "y", "y")
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ func TestYankLineBasic(t *testing.T) {
|
|||||||
t.Run("yy at last line", func(t *testing.T) {
|
t.Run("yy at last line", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "last line"}),
|
WithLines([]string{"line 1", "line 2", "last line"}),
|
||||||
WithCursorPos(action.Position{Line: 2, Col: 0}),
|
WithCursorPos(core.Position{Line: 2, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "y")
|
sendKeys(tm, "y", "y")
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ func TestYankLineWithCount(t *testing.T) {
|
|||||||
t.Run("2yy yanks two lines", func(t *testing.T) {
|
t.Run("2yy yanks two lines", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3", "line 4"}),
|
WithLines([]string{"line 1", "line 2", "line 3", "line 4"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "2", "y", "y")
|
sendKeys(tm, "2", "y", "y")
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ func TestYankLineWithCount(t *testing.T) {
|
|||||||
t.Run("3yy yanks three lines", func(t *testing.T) {
|
t.Run("3yy yanks three lines", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"a", "b", "c", "d", "e"}),
|
WithLines([]string{"a", "b", "c", "d", "e"}),
|
||||||
WithCursorPos(action.Position{Line: 1, Col: 0}),
|
WithCursorPos(core.Position{Line: 1, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "3", "y", "y")
|
sendKeys(tm, "3", "y", "y")
|
||||||
|
|
||||||
@ -149,7 +149,7 @@ func TestYankLineWithCount(t *testing.T) {
|
|||||||
t.Run("yy with count overflow clamps to available lines", func(t *testing.T) {
|
t.Run("yy with count overflow clamps to available lines", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 1, Col: 0}),
|
WithCursorPos(core.Position{Line: 1, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "1", "0", "y", "y") // 10yy but only 2 lines available
|
sendKeys(tm, "1", "0", "y", "y") // 10yy but only 2 lines available
|
||||||
|
|
||||||
@ -169,13 +169,13 @@ func TestYankLineWithCount(t *testing.T) {
|
|||||||
t.Run("yy with count does not modify buffer", func(t *testing.T) {
|
t.Run("yy with count does not modify buffer", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "3", "y", "y")
|
sendKeys(tm, "3", "y", "y")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 3 {
|
if m.ActiveBuffer().LineCount() != 3 {
|
||||||
t.Errorf("LineCount() = %d, want 3", m.LineCount())
|
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -184,7 +184,7 @@ func TestYankLineEdgeCases(t *testing.T) {
|
|||||||
t.Run("yy on empty line", func(t *testing.T) {
|
t.Run("yy on empty line", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "", "line 3"}),
|
WithLines([]string{"line 1", "", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 1, Col: 0}),
|
WithCursorPos(core.Position{Line: 1, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "y")
|
sendKeys(tm, "y", "y")
|
||||||
|
|
||||||
@ -201,7 +201,7 @@ func TestYankLineEdgeCases(t *testing.T) {
|
|||||||
t.Run("yy on single line buffer", func(t *testing.T) {
|
t.Run("yy on single line buffer", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"only line"}),
|
WithLines([]string{"only line"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "y")
|
sendKeys(tm, "y", "y")
|
||||||
|
|
||||||
@ -215,7 +215,7 @@ func TestYankLineEdgeCases(t *testing.T) {
|
|||||||
t.Run("yy preserves whitespace", func(t *testing.T) {
|
t.Run("yy preserves whitespace", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{" indented", "\ttabbed", " spaces "}),
|
WithLines([]string{" indented", "\ttabbed", " spaces "}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "3", "y", "y")
|
sendKeys(tm, "3", "y", "y")
|
||||||
|
|
||||||
@ -241,7 +241,7 @@ func TestYankWithLinewiseMotions(t *testing.T) {
|
|||||||
t.Run("yj yanks current line and line below", func(t *testing.T) {
|
t.Run("yj yanks current line and line below", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "j")
|
sendKeys(tm, "y", "j")
|
||||||
|
|
||||||
@ -250,7 +250,7 @@ func TestYankWithLinewiseMotions(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.LinewiseRegister {
|
if reg.Type != core.LinewiseRegister {
|
||||||
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
|
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
if len(reg.Content) != 2 {
|
if len(reg.Content) != 2 {
|
||||||
@ -267,7 +267,7 @@ func TestYankWithLinewiseMotions(t *testing.T) {
|
|||||||
t.Run("yk yanks current line and line above", func(t *testing.T) {
|
t.Run("yk yanks current line and line above", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 1, Col: 0}),
|
WithCursorPos(core.Position{Line: 1, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "k")
|
sendKeys(tm, "y", "k")
|
||||||
|
|
||||||
@ -276,7 +276,7 @@ func TestYankWithLinewiseMotions(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.LinewiseRegister {
|
if reg.Type != core.LinewiseRegister {
|
||||||
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
|
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
if len(reg.Content) != 2 {
|
if len(reg.Content) != 2 {
|
||||||
@ -293,7 +293,7 @@ func TestYankWithLinewiseMotions(t *testing.T) {
|
|||||||
t.Run("yG yanks from cursor to end of file", func(t *testing.T) {
|
t.Run("yG yanks from cursor to end of file", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3", "line 4"}),
|
WithLines([]string{"line 1", "line 2", "line 3", "line 4"}),
|
||||||
WithCursorPos(action.Position{Line: 1, Col: 0}),
|
WithCursorPos(core.Position{Line: 1, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "G")
|
sendKeys(tm, "y", "G")
|
||||||
|
|
||||||
@ -302,7 +302,7 @@ func TestYankWithLinewiseMotions(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.LinewiseRegister {
|
if reg.Type != core.LinewiseRegister {
|
||||||
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
|
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
if len(reg.Content) != 3 {
|
if len(reg.Content) != 3 {
|
||||||
@ -319,7 +319,7 @@ func TestYankWithLinewiseMotions(t *testing.T) {
|
|||||||
t.Run("ygg yanks from cursor to beginning of file", func(t *testing.T) {
|
t.Run("ygg yanks from cursor to beginning of file", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3", "line 4"}),
|
WithLines([]string{"line 1", "line 2", "line 3", "line 4"}),
|
||||||
WithCursorPos(action.Position{Line: 2, Col: 0}),
|
WithCursorPos(core.Position{Line: 2, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "g", "g")
|
sendKeys(tm, "y", "g", "g")
|
||||||
|
|
||||||
@ -328,7 +328,7 @@ func TestYankWithLinewiseMotions(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.LinewiseRegister {
|
if reg.Type != core.LinewiseRegister {
|
||||||
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
|
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
if len(reg.Content) != 3 {
|
if len(reg.Content) != 3 {
|
||||||
@ -345,7 +345,7 @@ func TestYankWithLinewiseMotions(t *testing.T) {
|
|||||||
t.Run("y2j yanks current and next two lines", func(t *testing.T) {
|
t.Run("y2j yanks current and next two lines", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"a", "b", "c", "d", "e"}),
|
WithLines([]string{"a", "b", "c", "d", "e"}),
|
||||||
WithCursorPos(action.Position{Line: 1, Col: 0}),
|
WithCursorPos(core.Position{Line: 1, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "2", "j")
|
sendKeys(tm, "y", "2", "j")
|
||||||
|
|
||||||
@ -371,26 +371,26 @@ func TestYankWithLinewiseMotions(t *testing.T) {
|
|||||||
t.Run("yj does not move cursor", func(t *testing.T) {
|
t.Run("yj does not move cursor", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 3}),
|
WithCursorPos(core.Position{Line: 0, Col: 3}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "j")
|
sendKeys(tm, "y", "j")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorY() != 0 {
|
if m.ActiveWindow().Cursor.Line != 0 {
|
||||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("yG does not modify buffer", func(t *testing.T) {
|
t.Run("yG does not modify buffer", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "G")
|
sendKeys(tm, "y", "G")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 3 {
|
if m.ActiveBuffer().LineCount() != 3 {
|
||||||
t.Errorf("LineCount() = %d, want 3", m.LineCount())
|
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -403,7 +403,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
|
|||||||
t.Run("yw yanks word under cursor", func(t *testing.T) {
|
t.Run("yw yanks word under cursor", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "w")
|
sendKeys(tm, "y", "w")
|
||||||
|
|
||||||
@ -412,7 +412,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.CharwiseRegister {
|
if reg.Type != core.CharwiseRegister {
|
||||||
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
// yw includes trailing space
|
// yw includes trailing space
|
||||||
@ -427,7 +427,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
|
|||||||
t.Run("ye yanks to end of word (exclusive)", func(t *testing.T) {
|
t.Run("ye yanks to end of word (exclusive)", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "e")
|
sendKeys(tm, "y", "e")
|
||||||
|
|
||||||
@ -436,7 +436,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.CharwiseRegister {
|
if reg.Type != core.CharwiseRegister {
|
||||||
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
// ye is inclusive of last char
|
// ye is inclusive of last char
|
||||||
@ -451,7 +451,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
|
|||||||
t.Run("yb yanks backward word", func(t *testing.T) {
|
t.Run("yb yanks backward word", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 6}), // on 'w'
|
WithCursorPos(core.Position{Line: 0, Col: 6}), // on 'w'
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "b")
|
sendKeys(tm, "y", "b")
|
||||||
|
|
||||||
@ -460,7 +460,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.CharwiseRegister {
|
if reg.Type != core.CharwiseRegister {
|
||||||
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
// yb from 'w' back to start of 'hello'
|
// yb from 'w' back to start of 'hello'
|
||||||
@ -475,7 +475,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
|
|||||||
t.Run("y$ yanks to end of line", func(t *testing.T) {
|
t.Run("y$ yanks to end of line", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 6}), // on 'w'
|
WithCursorPos(core.Position{Line: 0, Col: 6}), // on 'w'
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "$")
|
sendKeys(tm, "y", "$")
|
||||||
|
|
||||||
@ -484,7 +484,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.CharwiseRegister {
|
if reg.Type != core.CharwiseRegister {
|
||||||
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
if len(reg.Content) != 1 {
|
if len(reg.Content) != 1 {
|
||||||
@ -498,7 +498,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
|
|||||||
t.Run("y0 yanks to beginning of line", func(t *testing.T) {
|
t.Run("y0 yanks to beginning of line", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 6}), // on 'w'
|
WithCursorPos(core.Position{Line: 0, Col: 6}), // on 'w'
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "0")
|
sendKeys(tm, "y", "0")
|
||||||
|
|
||||||
@ -507,7 +507,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.CharwiseRegister {
|
if reg.Type != core.CharwiseRegister {
|
||||||
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
if len(reg.Content) != 1 {
|
if len(reg.Content) != 1 {
|
||||||
@ -521,7 +521,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
|
|||||||
t.Run("y_ yanks to first non-whitespace", func(t *testing.T) {
|
t.Run("y_ yanks to first non-whitespace", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{" hello world"}),
|
WithLines([]string{" hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 10}), // on 'w'
|
WithCursorPos(core.Position{Line: 0, Col: 10}), // on 'w'
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "_")
|
sendKeys(tm, "y", "_")
|
||||||
|
|
||||||
@ -530,7 +530,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.CharwiseRegister {
|
if reg.Type != core.CharwiseRegister {
|
||||||
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
// From 'w' back to 'h' (first non-ws)
|
// From 'w' back to 'h' (first non-ws)
|
||||||
@ -545,7 +545,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
|
|||||||
t.Run("y2w yanks two words", func(t *testing.T) {
|
t.Run("y2w yanks two words", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"one two three four"}),
|
WithLines([]string{"one two three four"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "2", "w")
|
sendKeys(tm, "y", "2", "w")
|
||||||
|
|
||||||
@ -565,26 +565,26 @@ func TestYankWithCharwiseMotions(t *testing.T) {
|
|||||||
t.Run("yw does not move cursor", func(t *testing.T) {
|
t.Run("yw does not move cursor", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "w")
|
sendKeys(tm, "y", "w")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.CursorX() != 0 {
|
if m.ActiveWindow().Cursor.Col != 0 {
|
||||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("yw does not modify buffer", func(t *testing.T) {
|
t.Run("yw does not modify buffer", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "w")
|
sendKeys(tm, "y", "w")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "hello world" {
|
if m.ActiveBuffer().Lines[0] != "hello world" {
|
||||||
t.Errorf("Line(0) = %q, want 'hello world'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'hello world'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -597,7 +597,7 @@ func TestYankVisualCharwise(t *testing.T) {
|
|||||||
t.Run("v selection then y yanks selected text", func(t *testing.T) {
|
t.Run("v selection then y yanks selected text", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "l", "l", "l", "l", "y") // select "hello"
|
sendKeys(tm, "v", "l", "l", "l", "l", "y") // select "hello"
|
||||||
|
|
||||||
@ -606,7 +606,7 @@ func TestYankVisualCharwise(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.CharwiseRegister {
|
if reg.Type != core.CharwiseRegister {
|
||||||
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
if len(reg.Content) != 1 {
|
if len(reg.Content) != 1 {
|
||||||
@ -620,7 +620,7 @@ func TestYankVisualCharwise(t *testing.T) {
|
|||||||
t.Run("v selection across lines yanks with newlines", func(t *testing.T) {
|
t.Run("v selection across lines yanks with newlines", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2"}),
|
WithLines([]string{"line 1", "line 2"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 3}),
|
WithCursorPos(core.Position{Line: 0, Col: 3}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "j", "l", "l", "y") // select "e 1\nlin"
|
sendKeys(tm, "v", "j", "l", "l", "y") // select "e 1\nlin"
|
||||||
|
|
||||||
@ -629,7 +629,7 @@ func TestYankVisualCharwise(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.CharwiseRegister {
|
if reg.Type != core.CharwiseRegister {
|
||||||
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
// Multi-line charwise yank
|
// Multi-line charwise yank
|
||||||
@ -641,12 +641,12 @@ func TestYankVisualCharwise(t *testing.T) {
|
|||||||
t.Run("visual yank exits visual mode", func(t *testing.T) {
|
t.Run("visual yank exits visual mode", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "l", "l", "y")
|
sendKeys(tm, "v", "l", "l", "y")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Mode() != action.NormalMode {
|
if m.Mode() != core.NormalMode {
|
||||||
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
|
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -654,13 +654,13 @@ func TestYankVisualCharwise(t *testing.T) {
|
|||||||
t.Run("visual yank does not modify buffer", func(t *testing.T) {
|
t.Run("visual yank does not modify buffer", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "v", "l", "l", "l", "l", "y")
|
sendKeys(tm, "v", "l", "l", "l", "l", "y")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "hello world" {
|
if m.ActiveBuffer().Lines[0] != "hello world" {
|
||||||
t.Errorf("Line(0) = %q, want 'hello world'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'hello world'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -669,7 +669,7 @@ func TestYankVisualLinewise(t *testing.T) {
|
|||||||
t.Run("V selection then y yanks entire lines", func(t *testing.T) {
|
t.Run("V selection then y yanks entire lines", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "V", "j", "y") // select lines 1 and 2
|
sendKeys(tm, "V", "j", "y") // select lines 1 and 2
|
||||||
|
|
||||||
@ -678,7 +678,7 @@ func TestYankVisualLinewise(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.LinewiseRegister {
|
if reg.Type != core.LinewiseRegister {
|
||||||
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
|
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
if len(reg.Content) != 2 {
|
if len(reg.Content) != 2 {
|
||||||
@ -695,7 +695,7 @@ func TestYankVisualLinewise(t *testing.T) {
|
|||||||
t.Run("V on single line then y yanks that line", func(t *testing.T) {
|
t.Run("V on single line then y yanks that line", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2"}),
|
WithLines([]string{"line 1", "line 2"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "V", "y")
|
sendKeys(tm, "V", "y")
|
||||||
|
|
||||||
@ -704,7 +704,7 @@ func TestYankVisualLinewise(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.LinewiseRegister {
|
if reg.Type != core.LinewiseRegister {
|
||||||
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
|
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
if len(reg.Content) != 1 {
|
if len(reg.Content) != 1 {
|
||||||
@ -718,7 +718,7 @@ func TestYankVisualLinewise(t *testing.T) {
|
|||||||
t.Run("V selection upward yanks in correct order", func(t *testing.T) {
|
t.Run("V selection upward yanks in correct order", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 2, Col: 0}),
|
WithCursorPos(core.Position{Line: 2, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "V", "k", "y") // select from line 3 upward to line 2
|
sendKeys(tm, "V", "k", "y") // select from line 3 upward to line 2
|
||||||
|
|
||||||
@ -742,12 +742,12 @@ func TestYankVisualLinewise(t *testing.T) {
|
|||||||
t.Run("visual line yank exits visual mode", func(t *testing.T) {
|
t.Run("visual line yank exits visual mode", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2"}),
|
WithLines([]string{"line 1", "line 2"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "V", "y")
|
sendKeys(tm, "V", "y")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Mode() != action.NormalMode {
|
if m.Mode() != core.NormalMode {
|
||||||
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
|
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -757,7 +757,7 @@ func TestYankVisualBlock(t *testing.T) {
|
|||||||
t.Run("ctrl+v selection then y yanks block", func(t *testing.T) {
|
t.Run("ctrl+v selection then y yanks block", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"abcdef", "ghijkl", "mnopqr"}),
|
WithLines([]string{"abcdef", "ghijkl", "mnopqr"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 1}),
|
WithCursorPos(core.Position{Line: 0, Col: 1}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "ctrl+v", "j", "j", "l", "l", "y") // select 3x3 block starting at col 1
|
sendKeys(tm, "ctrl+v", "j", "j", "l", "l", "y") // select 3x3 block starting at col 1
|
||||||
|
|
||||||
@ -766,7 +766,7 @@ func TestYankVisualBlock(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("unnamed register not found")
|
t.Fatal("unnamed register not found")
|
||||||
}
|
}
|
||||||
if reg.Type != action.BlockwiseRegister {
|
if reg.Type != core.BlockwiseRegister {
|
||||||
t.Errorf("register type = %v, want BlockwiseRegister", reg.Type)
|
t.Errorf("register type = %v, want BlockwiseRegister", reg.Type)
|
||||||
}
|
}
|
||||||
// Block should contain "bcd", "hij", "nop"
|
// Block should contain "bcd", "hij", "nop"
|
||||||
@ -787,12 +787,12 @@ func TestYankVisualBlock(t *testing.T) {
|
|||||||
t.Run("visual block yank exits visual mode", func(t *testing.T) {
|
t.Run("visual block yank exits visual mode", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"abcd", "efgh"}),
|
WithLines([]string{"abcd", "efgh"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "ctrl+v", "j", "l", "y")
|
sendKeys(tm, "ctrl+v", "j", "l", "y")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Mode() != action.NormalMode {
|
if m.Mode() != core.NormalMode {
|
||||||
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
|
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -800,7 +800,7 @@ func TestYankVisualBlock(t *testing.T) {
|
|||||||
t.Run("visual block yank with uneven line lengths", func(t *testing.T) {
|
t.Run("visual block yank with uneven line lengths", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"abcdefgh", "ij", "klmnop"}),
|
WithLines([]string{"abcdefgh", "ij", "klmnop"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "ctrl+v", "j", "j", "l", "l", "l", "y") // 4-wide block
|
sendKeys(tm, "ctrl+v", "j", "j", "l", "l", "l", "y") // 4-wide block
|
||||||
|
|
||||||
@ -824,7 +824,7 @@ func TestYankRegisterBehavior(t *testing.T) {
|
|||||||
t.Run("yy updates register 0 and unnamed register", func(t *testing.T) {
|
t.Run("yy updates register 0 and unnamed register", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2"}),
|
WithLines([]string{"line 1", "line 2"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "y")
|
sendKeys(tm, "y", "y")
|
||||||
|
|
||||||
@ -858,7 +858,7 @@ func TestYankRegisterBehavior(t *testing.T) {
|
|||||||
t.Run("multiple yanks shift numbered registers", func(t *testing.T) {
|
t.Run("multiple yanks shift numbered registers", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"first", "second", "third"}),
|
WithLines([]string{"first", "second", "third"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "y") // yank "first"
|
sendKeys(tm, "y", "y") // yank "first"
|
||||||
sendKeys(tm, "j")
|
sendKeys(tm, "j")
|
||||||
@ -894,18 +894,18 @@ func TestYankRegisterBehavior(t *testing.T) {
|
|||||||
t.Run("yank then paste uses correct content", func(t *testing.T) {
|
t.Run("yank then paste uses correct content", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"original", "to copy"}),
|
WithLines([]string{"original", "to copy"}),
|
||||||
WithCursorPos(action.Position{Line: 1, Col: 0}),
|
WithCursorPos(core.Position{Line: 1, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "y") // yank "to copy"
|
sendKeys(tm, "y", "y") // yank "to copy"
|
||||||
sendKeys(tm, "k") // move up
|
sendKeys(tm, "k") // move up
|
||||||
sendKeys(tm, "p") // paste
|
sendKeys(tm, "p") // paste
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 3 {
|
if m.ActiveBuffer().LineCount() != 3 {
|
||||||
t.Errorf("LineCount() = %d, want 3", m.LineCount())
|
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(1) != "to copy" {
|
if m.ActiveBuffer().Lines[1] != "to copy" {
|
||||||
t.Errorf("Line(1) = %q, want 'to copy'", m.Line(1))
|
t.Errorf("Line(1) = %q, want 'to copy'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -918,7 +918,7 @@ func TestYankEdgeCases(t *testing.T) {
|
|||||||
t.Run("yy on whitespace-only line", func(t *testing.T) {
|
t.Run("yy on whitespace-only line", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", " ", "line 3"}),
|
WithLines([]string{"line 1", " ", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 1, Col: 0}),
|
WithCursorPos(core.Position{Line: 1, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "y")
|
sendKeys(tm, "y", "y")
|
||||||
|
|
||||||
@ -938,7 +938,7 @@ func TestYankEdgeCases(t *testing.T) {
|
|||||||
t.Run("yw at end of line", func(t *testing.T) {
|
t.Run("yw at end of line", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello"}),
|
WithLines([]string{"hello"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 4}), // on 'o'
|
WithCursorPos(core.Position{Line: 0, Col: 4}), // on 'o'
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "w")
|
sendKeys(tm, "y", "w")
|
||||||
|
|
||||||
@ -959,7 +959,7 @@ func TestYankEdgeCases(t *testing.T) {
|
|||||||
t.Run("y$ at beginning of line yanks entire line content", func(t *testing.T) {
|
t.Run("y$ at beginning of line yanks entire line content", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "$")
|
sendKeys(tm, "y", "$")
|
||||||
|
|
||||||
@ -979,7 +979,7 @@ func TestYankEdgeCases(t *testing.T) {
|
|||||||
t.Run("y0 at beginning of line yanks nothing", func(t *testing.T) {
|
t.Run("y0 at beginning of line yanks nothing", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello"}),
|
WithLines([]string{"hello"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "0")
|
sendKeys(tm, "y", "0")
|
||||||
|
|
||||||
@ -1001,7 +1001,7 @@ func TestYankEdgeCases(t *testing.T) {
|
|||||||
longLine := strings.Repeat("a", 1000)
|
longLine := strings.Repeat("a", 1000)
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{longLine}),
|
WithLines([]string{longLine}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "y")
|
sendKeys(tm, "y", "y")
|
||||||
|
|
||||||
@ -1021,7 +1021,7 @@ func TestYankEdgeCases(t *testing.T) {
|
|||||||
t.Run("yy with special characters", func(t *testing.T) {
|
t.Run("yy with special characters", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello\tworld", "foo\nbar"}), // tab and embedded newline
|
WithLines([]string{"hello\tworld", "foo\nbar"}), // tab and embedded newline
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "y")
|
sendKeys(tm, "y", "y")
|
||||||
|
|
||||||
@ -1047,178 +1047,178 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
|
|||||||
t.Run("visual charwise yank then paste single line", func(t *testing.T) {
|
t.Run("visual charwise yank then paste single line", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
// Select "hello", yank it, move to end, paste
|
// Select "hello", yank it, move to end, paste
|
||||||
sendKeys(tm, "v", "l", "l", "l", "l", "y", "$", "p")
|
sendKeys(tm, "v", "l", "l", "l", "l", "y", "$", "p")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "hello worldhello" {
|
if m.ActiveBuffer().Lines[0] != "hello worldhello" {
|
||||||
t.Errorf("Line(0) = %q, want 'hello worldhello'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'hello worldhello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("visual charwise yank then paste before", func(t *testing.T) {
|
t.Run("visual charwise yank then paste before", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 6}), // on 'w'
|
WithCursorPos(core.Position{Line: 0, Col: 6}), // on 'w'
|
||||||
)
|
)
|
||||||
// Select "world", yank it, go to start, paste before
|
// Select "world", yank it, go to start, paste before
|
||||||
sendKeys(tm, "v", "$", "y", "0", "P")
|
sendKeys(tm, "v", "$", "y", "0", "P")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "worldhello world" {
|
if m.ActiveBuffer().Lines[0] != "worldhello world" {
|
||||||
t.Errorf("Line(0) = %q, want 'worldhello world'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'worldhello world'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("visual line yank then paste", func(t *testing.T) {
|
t.Run("visual line yank then paste", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
// V yank line 1, go to line 2, paste
|
// V yank line 1, go to line 2, paste
|
||||||
sendKeys(tm, "V", "y", "j", "p")
|
sendKeys(tm, "V", "y", "j", "p")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 4 {
|
if m.ActiveBuffer().LineCount() != 4 {
|
||||||
t.Errorf("LineCount() = %d, want 4", m.LineCount())
|
t.Errorf("LineCount() = %d, want 4", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(2) != "line 1" {
|
if m.ActiveBuffer().Lines[2] != "line 1" {
|
||||||
t.Errorf("Line(2) = %q, want 'line 1'", m.Line(2))
|
t.Errorf("Line(2) = %q, want 'line 1'", m.ActiveBuffer().Lines[2])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("visual line yank multiple lines then paste", func(t *testing.T) {
|
t.Run("visual line yank multiple lines then paste", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3", "line 4"}),
|
WithLines([]string{"line 1", "line 2", "line 3", "line 4"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
// V select lines 1-2, yank, go to end, paste
|
// V select lines 1-2, yank, go to end, paste
|
||||||
sendKeys(tm, "V", "j", "y", "G", "p")
|
sendKeys(tm, "V", "j", "y", "G", "p")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 6 {
|
if m.ActiveBuffer().LineCount() != 6 {
|
||||||
t.Errorf("LineCount() = %d, want 6", m.LineCount())
|
t.Errorf("LineCount() = %d, want 6", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(4) != "line 1" {
|
if m.ActiveBuffer().Lines[4] != "line 1" {
|
||||||
t.Errorf("Line(4) = %q, want 'line 1'", m.Line(4))
|
t.Errorf("Line(4) = %q, want 'line 1'", m.ActiveBuffer().Lines[4])
|
||||||
}
|
}
|
||||||
if m.Line(5) != "line 2" {
|
if m.ActiveBuffer().Lines[5] != "line 2" {
|
||||||
t.Errorf("Line(5) = %q, want 'line 2'", m.Line(5))
|
t.Errorf("Line(5) = %q, want 'line 2'", m.ActiveBuffer().Lines[5])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("visual line yank then paste before", func(t *testing.T) {
|
t.Run("visual line yank then paste before", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 2, Col: 0}),
|
WithCursorPos(core.Position{Line: 2, Col: 0}),
|
||||||
)
|
)
|
||||||
// V yank line 3, go to line 1, paste before
|
// V yank line 3, go to line 1, paste before
|
||||||
sendKeys(tm, "V", "y", "g", "g", "P")
|
sendKeys(tm, "V", "y", "g", "g", "P")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 4 {
|
if m.ActiveBuffer().LineCount() != 4 {
|
||||||
t.Errorf("LineCount() = %d, want 4", m.LineCount())
|
t.Errorf("LineCount() = %d, want 4", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(0) != "line 3" {
|
if m.ActiveBuffer().Lines[0] != "line 3" {
|
||||||
t.Errorf("Line(0) = %q, want 'line 3'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'line 3'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.Line(1) != "line 1" {
|
if m.ActiveBuffer().Lines[1] != "line 1" {
|
||||||
t.Errorf("Line(1) = %q, want 'line 1'", m.Line(1))
|
t.Errorf("Line(1) = %q, want 'line 1'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("yy then p duplicates line below", func(t *testing.T) {
|
t.Run("yy then p duplicates line below", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"original", "other"}),
|
WithLines([]string{"original", "other"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "y", "p")
|
sendKeys(tm, "y", "y", "p")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 3 {
|
if m.ActiveBuffer().LineCount() != 3 {
|
||||||
t.Errorf("LineCount() = %d, want 3", m.LineCount())
|
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(0) != "original" {
|
if m.ActiveBuffer().Lines[0] != "original" {
|
||||||
t.Errorf("Line(0) = %q, want 'original'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'original'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.Line(1) != "original" {
|
if m.ActiveBuffer().Lines[1] != "original" {
|
||||||
t.Errorf("Line(1) = %q, want 'original'", m.Line(1))
|
t.Errorf("Line(1) = %q, want 'original'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
if m.Line(2) != "other" {
|
if m.ActiveBuffer().Lines[2] != "other" {
|
||||||
t.Errorf("Line(2) = %q, want 'other'", m.Line(2))
|
t.Errorf("Line(2) = %q, want 'other'", m.ActiveBuffer().Lines[2])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("yy then P duplicates line above", func(t *testing.T) {
|
t.Run("yy then P duplicates line above", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"original", "other"}),
|
WithLines([]string{"original", "other"}),
|
||||||
WithCursorPos(action.Position{Line: 1, Col: 0}),
|
WithCursorPos(core.Position{Line: 1, Col: 0}),
|
||||||
)
|
)
|
||||||
sendKeys(tm, "y", "y", "P")
|
sendKeys(tm, "y", "y", "P")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 3 {
|
if m.ActiveBuffer().LineCount() != 3 {
|
||||||
t.Errorf("LineCount() = %d, want 3", m.LineCount())
|
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(0) != "original" {
|
if m.ActiveBuffer().Lines[0] != "original" {
|
||||||
t.Errorf("Line(0) = %q, want 'original'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'original'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.Line(1) != "other" {
|
if m.ActiveBuffer().Lines[1] != "other" {
|
||||||
t.Errorf("Line(1) = %q, want 'other'", m.Line(1))
|
t.Errorf("Line(1) = %q, want 'other'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
if m.Line(2) != "other" {
|
if m.ActiveBuffer().Lines[2] != "other" {
|
||||||
t.Errorf("Line(2) = %q, want 'other'", m.Line(2))
|
t.Errorf("Line(2) = %q, want 'other'", m.ActiveBuffer().Lines[2])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("yw then p pastes word after cursor", func(t *testing.T) {
|
t.Run("yw then p pastes word after cursor", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
// yw yanks "hello ", move to end of world, paste
|
// yw yanks "hello ", move to end of world, paste
|
||||||
sendKeys(tm, "y", "w", "$", "p")
|
sendKeys(tm, "y", "w", "$", "p")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "hello worldhello " {
|
if m.ActiveBuffer().Lines[0] != "hello worldhello " {
|
||||||
t.Errorf("Line(0) = %q, want 'hello worldhello '", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'hello worldhello '", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ye then p pastes word after cursor", func(t *testing.T) {
|
t.Run("ye then p pastes word after cursor", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello world"}),
|
WithLines([]string{"hello world"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
// ye yanks "hello" (inclusive), move to end of line, paste
|
// ye yanks "hello" (inclusive), move to end of line, paste
|
||||||
sendKeys(tm, "y", "e", "$", "p")
|
sendKeys(tm, "y", "e", "$", "p")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "hello worldhello" {
|
if m.ActiveBuffer().Lines[0] != "hello worldhello" {
|
||||||
t.Errorf("Line(0) = %q, want 'hello worldhello'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'hello worldhello'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("visual select partial word yank then paste", func(t *testing.T) {
|
t.Run("visual select partial word yank then paste", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"abcdefgh"}),
|
WithLines([]string{"abcdefgh"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 2}), // on 'c'
|
WithCursorPos(core.Position{Line: 0, Col: 2}), // on 'c'
|
||||||
)
|
)
|
||||||
// Select "cde", yank, go to end, paste
|
// Select "cde", yank, go to end, paste
|
||||||
sendKeys(tm, "v", "l", "l", "y", "$", "p")
|
sendKeys(tm, "v", "l", "l", "y", "$", "p")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Line(0) != "abcdefghcde" {
|
if m.ActiveBuffer().Lines[0] != "abcdefghcde" {
|
||||||
t.Errorf("Line(0) = %q, want 'abcdefghcde'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'abcdefghcde'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("visual yank empty selection does nothing", func(t *testing.T) {
|
t.Run("visual yank empty selection does nothing", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"hello"}),
|
WithLines([]string{"hello"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 2}),
|
WithCursorPos(core.Position{Line: 0, Col: 2}),
|
||||||
)
|
)
|
||||||
// Enter visual mode then immediately yank (single char)
|
// Enter visual mode then immediately yank (single char)
|
||||||
sendKeys(tm, "v", "y")
|
sendKeys(tm, "v", "y")
|
||||||
@ -1237,50 +1237,50 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
|
|||||||
t.Run("dd then p moves deleted line down", func(t *testing.T) {
|
t.Run("dd then p moves deleted line down", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
// dd deletes line 1, p pastes it below cursor (now on line 2)
|
// dd deletes line 1, p pastes it below cursor (now on line 2)
|
||||||
sendKeys(tm, "d", "d", "p")
|
sendKeys(tm, "d", "d", "p")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 3 {
|
if m.ActiveBuffer().LineCount() != 3 {
|
||||||
t.Errorf("LineCount() = %d, want 3", m.LineCount())
|
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
if m.Line(0) != "line 2" {
|
if m.ActiveBuffer().Lines[0] != "line 2" {
|
||||||
t.Errorf("Line(0) = %q, want 'line 2'", m.Line(0))
|
t.Errorf("Line(0) = %q, want 'line 2'", m.ActiveBuffer().Lines[0])
|
||||||
}
|
}
|
||||||
if m.Line(1) != "line 1" {
|
if m.ActiveBuffer().Lines[1] != "line 1" {
|
||||||
t.Errorf("Line(1) = %q, want 'line 1'", m.Line(1))
|
t.Errorf("Line(1) = %q, want 'line 1'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
if m.Line(2) != "line 3" {
|
if m.ActiveBuffer().Lines[2] != "line 3" {
|
||||||
t.Errorf("Line(2) = %q, want 'line 3'", m.Line(2))
|
t.Errorf("Line(2) = %q, want 'line 3'", m.ActiveBuffer().Lines[2])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("2yy then 2p pastes twice", func(t *testing.T) {
|
t.Run("2yy then 2p pastes twice", func(t *testing.T) {
|
||||||
tm := newTestModel(t,
|
tm := newTestModel(t,
|
||||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||||
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
WithCursorPos(core.Position{Line: 0, Col: 0}),
|
||||||
)
|
)
|
||||||
// 2yy yanks lines 1-2, 2p pastes them twice after current line
|
// 2yy yanks lines 1-2, 2p pastes them twice after current line
|
||||||
sendKeys(tm, "2", "y", "y", "2", "p")
|
sendKeys(tm, "2", "y", "y", "2", "p")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.LineCount() != 7 {
|
if m.ActiveBuffer().LineCount() != 7 {
|
||||||
t.Errorf("LineCount() = %d, want 7", m.LineCount())
|
t.Errorf("LineCount() = %d, want 7", m.ActiveBuffer().LineCount())
|
||||||
}
|
}
|
||||||
// Original + 2 copies of 2 lines = 3 + 4 = 7
|
// Original + 2 copies of 2 lines = 3 + 4 = 7
|
||||||
if m.Line(1) != "line 1" {
|
if m.ActiveBuffer().Lines[1] != "line 1" {
|
||||||
t.Errorf("Line(1) = %q, want 'line 1'", m.Line(1))
|
t.Errorf("Line(1) = %q, want 'line 1'", m.ActiveBuffer().Lines[1])
|
||||||
}
|
}
|
||||||
if m.Line(2) != "line 2" {
|
if m.ActiveBuffer().Lines[2] != "line 2" {
|
||||||
t.Errorf("Line(2) = %q, want 'line 2'", m.Line(2))
|
t.Errorf("Line(2) = %q, want 'line 2'", m.ActiveBuffer().Lines[2])
|
||||||
}
|
}
|
||||||
if m.Line(3) != "line 1" {
|
if m.ActiveBuffer().Lines[3] != "line 1" {
|
||||||
t.Errorf("Line(3) = %q, want 'line 1'", m.Line(3))
|
t.Errorf("Line(3) = %q, want 'line 1'", m.ActiveBuffer().Lines[3])
|
||||||
}
|
}
|
||||||
if m.Line(4) != "line 2" {
|
if m.ActiveBuffer().Lines[4] != "line 2" {
|
||||||
t.Errorf("Line(4) = %q, want 'line 2'", m.Line(4))
|
t.Errorf("Line(4) = %q, want 'line 2'", m.ActiveBuffer().Lines[4])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,136 +5,93 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/input"
|
"git.gophernest.net/azpect/TextEditor/internal/input"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/style"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cursor struct {
|
|
||||||
x int
|
|
||||||
y int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
lines []string
|
// Buffers
|
||||||
cursor cursor
|
buffers []*core.Buffer
|
||||||
anchor cursor // starting point for visual modes
|
//next buffer id?
|
||||||
scrollY int
|
|
||||||
mode action.Mode
|
// Windows
|
||||||
win_h int
|
windows []*core.Window
|
||||||
win_w int
|
activeWindowId int
|
||||||
|
|
||||||
|
// Editor wide state
|
||||||
|
mode core.Mode
|
||||||
|
|
||||||
|
// Terminal dimensions
|
||||||
|
termWidth int
|
||||||
|
termHeight int
|
||||||
|
|
||||||
|
// Input and key handling
|
||||||
input *input.Handler
|
input *input.Handler
|
||||||
|
|
||||||
// Insert repetition
|
// Insert mode state & repetition (applied to active window)
|
||||||
insertCount int
|
insertCount int
|
||||||
insertKeys []string
|
insertKeys []string
|
||||||
insertAction action.Action
|
insertAction action.Action
|
||||||
|
|
||||||
// Command mode
|
// Command line state
|
||||||
command string
|
command string
|
||||||
commandCursor int
|
commandCursor int
|
||||||
commandError error
|
commandError error
|
||||||
commandOutput string
|
commandOutput string
|
||||||
|
|
||||||
// Settings
|
// Global settings
|
||||||
settings action.Settings
|
settings core.EditorSettings
|
||||||
|
|
||||||
// Registers
|
// Registers
|
||||||
registers map[rune]action.Register // name -> register
|
registers map[rune]core.Register // name -> register
|
||||||
}
|
|
||||||
|
// Visual styles
|
||||||
func NewModel(lines []string, pos action.Position) Model {
|
styles style.Styles
|
||||||
return Model{
|
|
||||||
lines: lines,
|
|
||||||
cursor: cursor{
|
|
||||||
x: pos.Col,
|
|
||||||
y: pos.Line,
|
|
||||||
},
|
|
||||||
scrollY: 0,
|
|
||||||
mode: action.NormalMode,
|
|
||||||
command: "",
|
|
||||||
input: input.NewHandler(),
|
|
||||||
settings: action.NewDefaultSettings(),
|
|
||||||
registers: action.DefaultRegisters(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Model.Init: Initialize the model and start any commands that may need to run. Required
|
||||||
|
// for the bubbletea architecture.
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement action.Model interface
|
// Implement action.Model interface
|
||||||
|
|
||||||
func (m *Model) Lines() []string {
|
// ==================================================
|
||||||
return m.lines
|
// Core Data Access
|
||||||
|
// ==================================================
|
||||||
|
func (m *Model) Windows() []*core.Window {
|
||||||
|
return m.windows
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) Line(idx int) string {
|
func (m *Model) ActiveWindow() *core.Window {
|
||||||
if idx < 0 || idx >= len(m.lines) {
|
winId := m.activeWindowId
|
||||||
return ""
|
for i := range m.Windows() {
|
||||||
|
if m.windows[i].Id == winId {
|
||||||
|
return m.windows[i]
|
||||||
}
|
}
|
||||||
return m.lines[idx]
|
}
|
||||||
|
panic("Could not find window")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) SetLine(idx int, content string) {
|
func (m *Model) Buffers() []*core.Buffer {
|
||||||
if idx >= 0 && idx < len(m.lines) {
|
return m.buffers
|
||||||
m.lines[idx] = content
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) InsertLine(idx int, content string) {
|
func (m *Model) SetBuffers(bufs []*core.Buffer) {
|
||||||
if idx < 0 {
|
m.buffers = bufs
|
||||||
idx = 0
|
|
||||||
}
|
|
||||||
if idx > len(m.lines) {
|
|
||||||
idx = len(m.lines)
|
|
||||||
}
|
|
||||||
m.lines = append(m.lines[:idx], append([]string{content}, m.lines[idx:]...)...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) DeleteLine(idx int) {
|
func (m *Model) ActiveBuffer() *core.Buffer {
|
||||||
if idx >= 0 && idx < len(m.lines) {
|
win := m.ActiveWindow()
|
||||||
m.lines = append(m.lines[:idx], m.lines[idx+1:]...)
|
return win.Buffer
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) LineCount() int {
|
// ==================================================
|
||||||
return len(m.lines)
|
// Insert Mode Methods
|
||||||
}
|
// ==================================================
|
||||||
|
|
||||||
func (m *Model) CursorX() int {
|
|
||||||
return m.cursor.x
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) CursorY() int {
|
|
||||||
return m.cursor.y
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetCursorX(x int) {
|
|
||||||
m.cursor.x = x
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetCursorY(y int) {
|
|
||||||
m.cursor.y = y
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anchor methods
|
|
||||||
func (m *Model) AnchorX() int {
|
|
||||||
return m.anchor.x
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) AnchorY() int {
|
|
||||||
return m.anchor.y
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetAnchorX(x int) {
|
|
||||||
m.anchor.x = x
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetAnchorY(y int) {
|
|
||||||
m.anchor.y = y
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert methods
|
|
||||||
func (m *Model) InsertKeys() []string {
|
func (m *Model) InsertKeys() []string {
|
||||||
return m.insertKeys
|
return m.insertKeys
|
||||||
}
|
}
|
||||||
@ -143,7 +100,143 @@ func (m *Model) SetInsertKeys(keys []string) {
|
|||||||
m.insertKeys = keys
|
m.insertKeys = keys
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command mode
|
func (m *Model) SetInsertRecording(count int, act action.Action) {
|
||||||
|
m.insertCount = count
|
||||||
|
m.insertKeys = []string{}
|
||||||
|
m.insertAction = act
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) ExitInsertMode() {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
if m.insertCount > 1 {
|
||||||
|
m.replayInsert()
|
||||||
|
}
|
||||||
|
if win.Cursor.Col > 0 {
|
||||||
|
win.Cursor.Col--
|
||||||
|
}
|
||||||
|
m.mode = core.NormalMode
|
||||||
|
m.insertCount = 0
|
||||||
|
m.insertKeys = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) replayInsert() {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
// Replay (count - 1) more times
|
||||||
|
for i := 1; i < m.insertCount; i++ {
|
||||||
|
// For 'o' and 'O', we need to create a new line first
|
||||||
|
switch m.insertAction.(type) {
|
||||||
|
case action.OpenLineBelow:
|
||||||
|
pos := win.Cursor.Line
|
||||||
|
buf.InsertLine(pos+1, "")
|
||||||
|
win.SetCursorLine(pos + 1)
|
||||||
|
|
||||||
|
case action.OpenLineAbove:
|
||||||
|
pos := win.Cursor.Line
|
||||||
|
buf.InsertLine(pos, "")
|
||||||
|
|
||||||
|
// 'i' and 'a' don't need setup - just replay keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replay each recorded keystroke
|
||||||
|
for _, key := range m.insertKeys {
|
||||||
|
m.processInsertKey(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Fix this shitty shit shit shit
|
||||||
|
func (m *Model) processInsertKey(key string) {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
col := win.Cursor.Col
|
||||||
|
line := win.Cursor.Line
|
||||||
|
l := buf.Line(line)
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "enter":
|
||||||
|
if col == len(l) {
|
||||||
|
buf.InsertLine(line+1, "")
|
||||||
|
} else {
|
||||||
|
buf.SetLine(line, l[:col])
|
||||||
|
buf.InsertLine(line+1, l[col:])
|
||||||
|
}
|
||||||
|
win.SetCursorLine(line + 1)
|
||||||
|
win.SetCursorCol(0)
|
||||||
|
|
||||||
|
case "backspace":
|
||||||
|
if col > 0 {
|
||||||
|
buf.SetLine(line, l[:col-1]+l[col:])
|
||||||
|
win.SetCursorCol(col - 1)
|
||||||
|
} else if line > 0 {
|
||||||
|
prevLine := buf.Line(line - 1)
|
||||||
|
newCol := len(prevLine)
|
||||||
|
buf.SetLine(line-1, prevLine+l)
|
||||||
|
buf.DeleteLine(line)
|
||||||
|
win.SetCursorLine(line - 1)
|
||||||
|
win.SetCursorCol(newCol)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "delete":
|
||||||
|
if col == len(l) && line < buf.LineCount()-1 {
|
||||||
|
nextLine := buf.Line(line + 1)
|
||||||
|
buf.SetLine(line, l+nextLine)
|
||||||
|
buf.DeleteLine(line + 1)
|
||||||
|
} else if col < len(l) {
|
||||||
|
buf.SetLine(line, l[:col]+l[col+1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
case "tab":
|
||||||
|
tabs := strings.Repeat(" ", m.Settings().TabStop)
|
||||||
|
if col < len(l) {
|
||||||
|
buf.SetLine(line, l[:col]+tabs+l[col:])
|
||||||
|
} else {
|
||||||
|
buf.SetLine(line, l+tabs)
|
||||||
|
}
|
||||||
|
win.SetCursorCol(col + len(tabs))
|
||||||
|
|
||||||
|
case "up":
|
||||||
|
if line > 0 {
|
||||||
|
win.SetCursorLine(line - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "down":
|
||||||
|
if line+1 < buf.LineCount() {
|
||||||
|
win.SetCursorLine(line + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "left":
|
||||||
|
if col > 0 {
|
||||||
|
win.SetCursorCol(col - 1)
|
||||||
|
} else if line > 0 {
|
||||||
|
prevLine := buf.Line(line - 1)
|
||||||
|
win.SetCursorCol(len(prevLine))
|
||||||
|
win.SetCursorLine(line - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "right":
|
||||||
|
if col < len(l) {
|
||||||
|
win.SetCursorCol(col + 1)
|
||||||
|
} else if line+1 < buf.LineCount() {
|
||||||
|
win.SetCursorCol(0)
|
||||||
|
win.SetCursorLine(line + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if col < len(l) {
|
||||||
|
buf.SetLine(line, l[:col]+key+l[col:])
|
||||||
|
} else {
|
||||||
|
buf.SetLine(line, l+key)
|
||||||
|
}
|
||||||
|
win.SetCursorCol(col + len(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// Command Mode State
|
||||||
|
// ==================================================
|
||||||
func (m *Model) Command() string {
|
func (m *Model) Command() string {
|
||||||
return m.command
|
return m.command
|
||||||
}
|
}
|
||||||
@ -182,38 +275,60 @@ func (m *Model) SetCommandOutput(out string) {
|
|||||||
m.commandOutput = out
|
m.commandOutput = out
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings
|
// ==================================================
|
||||||
func (m *Model) Settings() action.Settings {
|
// Editor-wide State
|
||||||
|
// ==================================================
|
||||||
|
func (m *Model) Mode() core.Mode {
|
||||||
|
return m.mode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) SetMode(mode core.Mode) {
|
||||||
|
m.mode = mode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) Settings() core.EditorSettings {
|
||||||
return m.settings
|
return m.settings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) SetSettings(s action.Settings) {
|
func (m *Model) SetSettings(s core.EditorSettings) {
|
||||||
m.settings = s
|
m.settings = s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Model.Styles: Returns the visual styles used for rendering.
|
||||||
|
func (m *Model) Styles() style.Styles {
|
||||||
|
return m.styles
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model.SetStyles: Sets the visual styles used for rendering.
|
||||||
|
func (m *Model) SetStyles(s style.Styles) {
|
||||||
|
m.styles = s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
// Registers
|
// Registers
|
||||||
func (m *Model) Registers() map[rune]action.Register {
|
// ==================================================
|
||||||
|
func (m *Model) Registers() map[rune]core.Register {
|
||||||
return m.registers
|
return m.registers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) GetRegister(name rune) (action.Register, bool) {
|
func (m *Model) GetRegister(name rune) (core.Register, bool) {
|
||||||
reg, found := m.registers[name]
|
reg, found := m.registers[name]
|
||||||
return reg, found
|
return reg, found
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) SetRegister(name rune, t action.RegisterType, cnt []string) error {
|
func (m *Model) SetRegister(name rune, t core.RegisterType, cnt []string) error {
|
||||||
if _, found := m.GetRegister(name); !found {
|
if _, found := m.GetRegister(name); !found {
|
||||||
return fmt.Errorf("Register '%c' does not exist.", name)
|
return fmt.Errorf("Register '%c' does not exist.", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This might be slow, pointers maybe?
|
// TODO: This might be slow, pointers maybe?
|
||||||
reg := action.Register{Type: t, Content: cnt}
|
reg := core.Register{Type: t, Content: cnt}
|
||||||
m.registers[name] = reg
|
m.registers[name] = reg
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) UpdateDefaultRegister(t action.RegisterType, cnt []string) {
|
func (m *Model) UpdateDefaultRegister(t core.RegisterType, cnt []string) {
|
||||||
// Shift numbered registers: 0 -> 1 -> 2 -> ... -> 9 -> _ (discarded)
|
// Shift numbered registers: 0 -> 1 -> 2 -> ... -> 9 -> _ (discarded)
|
||||||
for i := rune('9'); i > '0'; i-- {
|
for i := rune('9'); i > '0'; i-- {
|
||||||
m.registers[i] = m.registers[i-1]
|
m.registers[i] = m.registers[i-1]
|
||||||
@ -223,199 +338,3 @@ func (m *Model) UpdateDefaultRegister(t action.RegisterType, cnt []string) {
|
|||||||
m.SetRegister('0', t, cnt)
|
m.SetRegister('0', t, cnt)
|
||||||
m.SetRegister('"', t, cnt)
|
m.SetRegister('"', t, cnt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window
|
|
||||||
func (m *Model) ScrollY() int {
|
|
||||||
return m.scrollY
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetScrollY(y int) {
|
|
||||||
m.scrollY = y
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) WinH() int {
|
|
||||||
return m.win_h
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) WinW() int {
|
|
||||||
return m.win_w
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) ViewPortH() int {
|
|
||||||
return m.win_h - 2 // -2 for status bar and commmand bar
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) ClampCursorX() {
|
|
||||||
lineLen := len(m.lines[m.cursor.y])
|
|
||||||
if lineLen == 0 {
|
|
||||||
m.cursor.x = 0
|
|
||||||
} else if m.cursor.x >= lineLen {
|
|
||||||
m.cursor.x = lineLen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdjustScroll ensures the cursor stays within the viewport with scrollOff margins.
|
|
||||||
// Call this after any cursor movement.
|
|
||||||
func (m *Model) AdjustScroll() {
|
|
||||||
viewportHeight := m.ViewPortH()
|
|
||||||
if viewportHeight <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Effective scrollOff (can't be more than half the viewport)
|
|
||||||
off := min(m.Settings().ScrollOff, viewportHeight/2)
|
|
||||||
|
|
||||||
// Cursor too close to top — scroll up
|
|
||||||
if m.CursorY() < m.ScrollY()+off {
|
|
||||||
m.SetScrollY(m.CursorY() - off)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cursor too close to bottom — scroll down
|
|
||||||
if m.CursorY() > m.ScrollY()+viewportHeight-1-off {
|
|
||||||
m.SetScrollY(m.CursorY() - viewportHeight + 1 + off)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clamp scrollY to valid range
|
|
||||||
maxScroll := max(0, m.LineCount()-viewportHeight)
|
|
||||||
m.SetScrollY(max(0, min(m.ScrollY(), maxScroll)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) Mode() action.Mode {
|
|
||||||
return m.mode
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetMode(mode action.Mode) {
|
|
||||||
m.mode = mode
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetInsertRecording(count int, act action.Action) {
|
|
||||||
m.insertCount = count
|
|
||||||
m.insertKeys = []string{}
|
|
||||||
m.insertAction = act
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) GetCursorPosition() action.Position {
|
|
||||||
return action.Position{Line: m.cursor.y, Col: m.cursor.x}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) replayInsert() {
|
|
||||||
// Replay (count - 1) more times
|
|
||||||
for i := 1; i < m.insertCount; i++ {
|
|
||||||
// For 'o' and 'O', we need to create a new line first
|
|
||||||
switch m.insertAction.(type) {
|
|
||||||
case action.OpenLineBelow:
|
|
||||||
pos := m.cursor.y
|
|
||||||
m.lines = append(m.lines[:pos+1], append([]string{""}, m.lines[pos+1:]...)...)
|
|
||||||
m.cursor.y++
|
|
||||||
m.cursor.x = 0
|
|
||||||
case action.OpenLineAbove:
|
|
||||||
pos := m.cursor.y
|
|
||||||
m.lines = append(m.lines[:pos], append([]string{""}, m.lines[pos:]...)...)
|
|
||||||
m.cursor.x = 0
|
|
||||||
// 'i' and 'a' don't need setup - just replay keys
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replay each recorded keystroke
|
|
||||||
for _, key := range m.insertKeys {
|
|
||||||
m.processInsertKey(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) ExitInsertMode() {
|
|
||||||
if m.insertCount > 1 {
|
|
||||||
m.replayInsert()
|
|
||||||
}
|
|
||||||
if m.cursor.x > 0 {
|
|
||||||
m.cursor.x--
|
|
||||||
}
|
|
||||||
m.mode = action.NormalMode
|
|
||||||
m.insertCount = 0
|
|
||||||
m.insertKeys = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) processInsertKey(key string) {
|
|
||||||
x := m.CursorX()
|
|
||||||
y := m.CursorY()
|
|
||||||
l := m.Line(y)
|
|
||||||
|
|
||||||
switch key {
|
|
||||||
case "enter":
|
|
||||||
if x == len(l) {
|
|
||||||
m.InsertLine(y+1, "")
|
|
||||||
} else {
|
|
||||||
m.SetLine(y, l[:x])
|
|
||||||
m.InsertLine(y+1, l[x:])
|
|
||||||
}
|
|
||||||
m.SetCursorY(y + 1)
|
|
||||||
m.SetCursorX(0)
|
|
||||||
|
|
||||||
case "backspace":
|
|
||||||
if x > 0 {
|
|
||||||
m.SetLine(y, l[:x-1]+l[x:])
|
|
||||||
m.SetCursorX(x - 1)
|
|
||||||
} else if y > 0 {
|
|
||||||
prevLine := m.Line(y - 1)
|
|
||||||
newX := len(prevLine)
|
|
||||||
m.SetLine(y-1, prevLine+l)
|
|
||||||
m.DeleteLine(y)
|
|
||||||
m.SetCursorY(y - 1)
|
|
||||||
m.SetCursorX(newX)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "delete":
|
|
||||||
if x == len(l) && y < m.LineCount()-1 {
|
|
||||||
nextLine := m.Line(y + 1)
|
|
||||||
m.SetLine(y, l+nextLine)
|
|
||||||
m.DeleteLine(y + 1)
|
|
||||||
} else if x < len(l) {
|
|
||||||
m.SetLine(y, l[:x]+l[x+1:])
|
|
||||||
}
|
|
||||||
|
|
||||||
case "tab":
|
|
||||||
tabs := strings.Repeat(" ", m.Settings().TabSize)
|
|
||||||
if x < len(l) {
|
|
||||||
m.SetLine(y, l[:x]+tabs+l[x:])
|
|
||||||
} else {
|
|
||||||
m.SetLine(y, l+tabs)
|
|
||||||
}
|
|
||||||
m.SetCursorX(x + len(tabs))
|
|
||||||
|
|
||||||
case "up":
|
|
||||||
if y > 0 {
|
|
||||||
m.SetCursorY(y - 1)
|
|
||||||
m.ClampCursorX()
|
|
||||||
}
|
|
||||||
|
|
||||||
case "down":
|
|
||||||
if y+1 < m.LineCount() {
|
|
||||||
m.SetCursorY(y + 1)
|
|
||||||
m.ClampCursorX()
|
|
||||||
}
|
|
||||||
|
|
||||||
case "left":
|
|
||||||
if x > 0 {
|
|
||||||
m.SetCursorX(x - 1)
|
|
||||||
} else if y > 0 {
|
|
||||||
prevLine := m.Line(y - 1)
|
|
||||||
m.SetCursorX(len(prevLine))
|
|
||||||
m.SetCursorY(y - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "right":
|
|
||||||
if x < len(l) {
|
|
||||||
m.SetCursorX(x + 1)
|
|
||||||
} else if y+1 < m.LineCount() {
|
|
||||||
m.SetCursorX(0)
|
|
||||||
m.SetCursorY(y + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
if x < len(l) {
|
|
||||||
m.SetLine(y, l[:x]+key+l[x:])
|
|
||||||
} else {
|
|
||||||
m.SetLine(y, l+key)
|
|
||||||
}
|
|
||||||
m.SetCursorX(x + len(key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
140
internal/editor/model_builder.go
Normal file
140
internal/editor/model_builder.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package editor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/input"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/style"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelBuilder struct {
|
||||||
|
model Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewModelBuilder() *ModelBuilder {
|
||||||
|
return &ModelBuilder{
|
||||||
|
model: Model{
|
||||||
|
buffers: []*core.Buffer{},
|
||||||
|
windows: []*core.Window{},
|
||||||
|
activeWindowId: -1,
|
||||||
|
mode: core.NormalMode,
|
||||||
|
termWidth: 0,
|
||||||
|
termHeight: 0,
|
||||||
|
input: input.NewHandler(),
|
||||||
|
insertCount: 0,
|
||||||
|
insertKeys: []string{},
|
||||||
|
insertAction: nil,
|
||||||
|
command: "",
|
||||||
|
commandCursor: 0,
|
||||||
|
commandError: nil,
|
||||||
|
commandOutput: "",
|
||||||
|
settings: core.NewDefaultSettings(),
|
||||||
|
registers: core.DefaultRegisters(),
|
||||||
|
styles: style.DefaultStyles(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.WithBuffers: Set the buffers for the model. Buffers represent
|
||||||
|
// the in-memory text content of files being edited.
|
||||||
|
func (mb *ModelBuilder) WithBuffers(buffers []*core.Buffer) *ModelBuilder {
|
||||||
|
mb.model.buffers = buffers
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.AddBuffer: Add a single buffer to the model's buffer list.
|
||||||
|
func (mb *ModelBuilder) AddBuffer(buffer *core.Buffer) *ModelBuilder {
|
||||||
|
mb.model.buffers = append(mb.model.buffers, buffer)
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.WithWindows: Set the windows for the model. Windows are viewports
|
||||||
|
// that display buffer content with their own cursor position and scroll state.
|
||||||
|
func (mb *ModelBuilder) WithWindows(windows []*core.Window) *ModelBuilder {
|
||||||
|
mb.model.windows = windows
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.AddWindow: Add a single window to the model's window list.
|
||||||
|
func (mb *ModelBuilder) AddWindow(window *core.Window) *ModelBuilder {
|
||||||
|
mb.model.windows = append(mb.model.windows, window)
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.WithActiveWindowId: Set the ID of the currently active window.
|
||||||
|
// This determines which window receives input and displays the cursor.
|
||||||
|
func (mb *ModelBuilder) WithActiveWindowId(id int) *ModelBuilder {
|
||||||
|
mb.model.activeWindowId = id
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.WithMode: Set the editor mode (Normal, Insert, Visual, etc).
|
||||||
|
func (mb *ModelBuilder) WithMode(mode core.Mode) *ModelBuilder {
|
||||||
|
mb.model.mode = mode
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.WithTermSize: Set the terminal dimensions in columns and rows.
|
||||||
|
func (mb *ModelBuilder) WithTermSize(width, height int) *ModelBuilder {
|
||||||
|
mb.model.termWidth = width
|
||||||
|
mb.model.termHeight = height
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.WithTermWidth: Set the terminal width in columns.
|
||||||
|
func (mb *ModelBuilder) WithTermWidth(width int) *ModelBuilder {
|
||||||
|
mb.model.termWidth = width
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.WithTermHeight: Set the terminal height in rows.
|
||||||
|
func (mb *ModelBuilder) WithTermHeight(height int) *ModelBuilder {
|
||||||
|
mb.model.termHeight = height
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.WithSettings: Set the editor settings (tabstop, scrolloff, etc).
|
||||||
|
func (mb *ModelBuilder) WithSettings(settings core.EditorSettings) *ModelBuilder {
|
||||||
|
mb.model.settings = settings
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.WithRegisters: Set the register map for yank/delete/paste operations.
|
||||||
|
func (mb *ModelBuilder) WithRegisters(registers map[rune]core.Register) *ModelBuilder {
|
||||||
|
mb.model.registers = registers
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.WithCommand: Set the command line text.
|
||||||
|
func (mb *ModelBuilder) WithCommand(command string) *ModelBuilder {
|
||||||
|
mb.model.command = command
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.WithCommandCursor: Set the cursor position in the command line.
|
||||||
|
func (mb *ModelBuilder) WithCommandCursor(cursor int) *ModelBuilder {
|
||||||
|
mb.model.commandCursor = cursor
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.WithCommandError: Set the command line error state.
|
||||||
|
func (mb *ModelBuilder) WithCommandError(err error) *ModelBuilder {
|
||||||
|
mb.model.commandError = err
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.WithCommandOutput: Set the command line output text.
|
||||||
|
func (mb *ModelBuilder) WithCommandOutput(output string) *ModelBuilder {
|
||||||
|
mb.model.commandOutput = output
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.WithStyles: Set the visual styling for the editor.
|
||||||
|
func (mb *ModelBuilder) WithStyles(styles style.Styles) *ModelBuilder {
|
||||||
|
mb.model.styles = styles
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelBuilder.Build: Build and return the configured Model instance.
|
||||||
|
func (mb *ModelBuilder) Build() *Model {
|
||||||
|
return &mb.model
|
||||||
|
}
|
||||||
@ -1,52 +0,0 @@
|
|||||||
package editor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m Model) cursorStyle() lipgloss.Style {
|
|
||||||
switch m.mode {
|
|
||||||
case action.NormalMode,
|
|
||||||
action.VisualMode,
|
|
||||||
action.VisualBlockMode,
|
|
||||||
action.VisualLineMode:
|
|
||||||
// Block cursor for normal mode
|
|
||||||
return lipgloss.NewStyle().Reverse(true)
|
|
||||||
case action.InsertMode:
|
|
||||||
// Bar/underline for insert mode
|
|
||||||
return lipgloss.NewStyle().Underline(true)
|
|
||||||
case action.CommandMode:
|
|
||||||
return lipgloss.NewStyle().Reverse(true)
|
|
||||||
default:
|
|
||||||
return lipgloss.NewStyle().Reverse(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEBUGGING STYLE
|
|
||||||
func (m Model) visualAnchorStyle() lipgloss.Style {
|
|
||||||
bg := lipgloss.Color("#a89020")
|
|
||||||
return lipgloss.NewStyle().Background(bg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) gutterStyle(currentLine bool) lipgloss.Style {
|
|
||||||
bg := lipgloss.Color("236")
|
|
||||||
fg := lipgloss.Color("243")
|
|
||||||
if currentLine {
|
|
||||||
fg = lipgloss.Color("#d69d00")
|
|
||||||
}
|
|
||||||
return lipgloss.NewStyle().
|
|
||||||
Width(m.Settings().GutterSize).
|
|
||||||
Background(bg).
|
|
||||||
Foreground(fg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) visualHighlightStyle() lipgloss.Style {
|
|
||||||
bg := lipgloss.Color("#7a6a00")
|
|
||||||
return lipgloss.NewStyle().Background(bg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) commandErrorStyle() lipgloss.Style {
|
|
||||||
fg := lipgloss.Color("#e3203a")
|
|
||||||
return lipgloss.NewStyle().Foreground(fg)
|
|
||||||
}
|
|
||||||
@ -4,14 +4,49 @@ import (
|
|||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
// 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:
|
case tea.WindowSizeMsg:
|
||||||
m.win_h = msg.Height
|
m.termHeight = msg.Height
|
||||||
m.win_w = msg.Width
|
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
|
||||||
|
}
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
// TODO: This needs to be removed, but for now its required for the tests.
|
// TODO: This needs to be removed, but for now its required for the tests.
|
||||||
@ -19,11 +54,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if msg.Type == tea.KeyCtrlC {
|
if msg.Type == tea.KeyCtrlC {
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
cmd = m.input.Handle(&m, msg.String())
|
cmd = m.input.Handle(m, msg.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep cursor in view after any update
|
// Keep cursor in view after any update
|
||||||
m.AdjustScroll()
|
win := m.ActiveWindow()
|
||||||
|
win.AdjustScroll()
|
||||||
|
|
||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,22 +4,290 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/style"
|
||||||
)
|
)
|
||||||
|
|
||||||
func posInsideSelection(m Model, col, line int) bool {
|
// Model.View: Renders the complete editor view including buffer content, line
|
||||||
switch m.Mode() {
|
// numbers, status bar, and command line.
|
||||||
case action.VisualLineMode:
|
func (m Model) View() string {
|
||||||
startY := min(m.AnchorY(), m.CursorY())
|
win := m.ActiveWindow()
|
||||||
endY := max(m.AnchorY(), m.CursorY())
|
|
||||||
|
// NOTES:
|
||||||
|
// One single command line across entire viewport
|
||||||
|
// Each window has its own line numbers and gutter
|
||||||
|
// Each window has its own status bar and mode
|
||||||
|
|
||||||
|
styles := m.Styles()
|
||||||
|
options := win.Options
|
||||||
|
|
||||||
|
// Draw window
|
||||||
|
view := viewWindow(win, styles, options, m.Mode())
|
||||||
|
|
||||||
|
// Command bar is seperate
|
||||||
|
cmdBar := drawCommandBar(m)
|
||||||
|
return view + cmdBar
|
||||||
|
}
|
||||||
|
|
||||||
|
// viewWindow: Renders a single window's content including line numbers and buffer text.
|
||||||
|
// Each window has its own line numbers, gutter, and viewport dimensions.
|
||||||
|
func viewWindow(w *core.Window, styles style.Styles, options core.WinOptions, mode core.Mode) string {
|
||||||
|
buf := w.Buffer
|
||||||
|
var view strings.Builder
|
||||||
|
|
||||||
|
// Compute window size (y)
|
||||||
|
start := w.ScrollY
|
||||||
|
end := w.ScrollY + w.ViewportHeight()
|
||||||
|
|
||||||
|
// Draw buffer lines
|
||||||
|
for lineNum := start; lineNum < end; lineNum++ {
|
||||||
|
if lineNum < buf.LineCount() {
|
||||||
|
line := drawLine(w, styles, options, mode, buf.Line(lineNum), lineNum)
|
||||||
|
view.WriteString(line)
|
||||||
|
}
|
||||||
|
view.WriteRune('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw status line
|
||||||
|
statusBar := drawStatusBar(w, mode)
|
||||||
|
view.WriteString(statusBar + "\n")
|
||||||
|
|
||||||
|
return view.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawLine: Renders a single line with syntax highlighting, cursor, and visual selection.
|
||||||
|
// Handles gutter, cursor rendering, and visual mode highlighting.
|
||||||
|
func drawLine(w *core.Window, styles style.Styles, options core.WinOptions, mode core.Mode, line string, lineNumber int) string {
|
||||||
|
runes := []rune(line)
|
||||||
|
|
||||||
|
curStyle := styles.CursorStyle(mode)
|
||||||
|
visStyle := styles.VisualHighlight
|
||||||
|
|
||||||
|
var view strings.Builder
|
||||||
|
|
||||||
|
// Draw gutter first
|
||||||
|
gutter := drawGutter(w, styles, options, lineNumber)
|
||||||
|
view.WriteString(gutter)
|
||||||
|
|
||||||
|
// Now draw the line content
|
||||||
|
for col := 0; col <= len(runes); col++ {
|
||||||
|
// Current char is cursor
|
||||||
|
if col == w.Cursor.Col && lineNumber == w.Cursor.Line {
|
||||||
|
if col < len(runes) {
|
||||||
|
view.WriteString(curStyle.Render(string(runes[col])))
|
||||||
|
} else {
|
||||||
|
view.WriteString(curStyle.Render(" "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not cursor, but not end
|
||||||
|
} else if col < len(runes) {
|
||||||
|
if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
||||||
|
view.WriteString(visStyle.Render(string(runes[col])))
|
||||||
|
} else {
|
||||||
|
view.WriteRune(runes[col])
|
||||||
|
}
|
||||||
|
// Allow highlight on blank lines or chars
|
||||||
|
} else if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
||||||
|
view.WriteString(visStyle.Render(" "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawGutter: Renders the line number gutter with support for both absolute and
|
||||||
|
// relative line numbers, highlighting the current line differently.
|
||||||
|
func drawGutter(w *core.Window, styles style.Styles, options core.WinOptions, curLine int) string {
|
||||||
|
if !(options.Number || options.RelativeNumber) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required vars
|
||||||
|
var (
|
||||||
|
view strings.Builder
|
||||||
|
gutSize int = options.GutterSize - 1 // -1 is for padding
|
||||||
|
currentLine bool = curLine == w.Cursor.Line
|
||||||
|
lineNumber int
|
||||||
|
|
||||||
|
gutter string
|
||||||
|
gutterStyle = styles.Gutter
|
||||||
|
gutterStyleCur = styles.GutterCurrentLine
|
||||||
|
)
|
||||||
|
|
||||||
|
// If we have relative setting, set the numbers relatively
|
||||||
|
if options.RelativeNumber {
|
||||||
|
if curLine > w.Cursor.Line {
|
||||||
|
lineNumber = curLine - w.Cursor.Line
|
||||||
|
}
|
||||||
|
|
||||||
|
if curLine < w.Cursor.Line {
|
||||||
|
lineNumber = w.Cursor.Line - curLine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have number setting AND not relative setting OR we are on current line, use current line number
|
||||||
|
if (options.Number && !options.RelativeNumber) || (options.Number && currentLine) {
|
||||||
|
lineNumber = curLine + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the gutter
|
||||||
|
gutter = fmt.Sprintf("%*d ", gutSize, lineNumber)
|
||||||
|
if currentLine {
|
||||||
|
view.WriteString(gutterStyleCur.Render(gutter))
|
||||||
|
} else {
|
||||||
|
view.WriteString(gutterStyle.Render(gutter))
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.String()
|
||||||
|
|
||||||
|
// if m.Settings().Number || m.Settings().RelativeNumber {
|
||||||
|
// var (
|
||||||
|
// gutter string
|
||||||
|
// currentLine bool = false
|
||||||
|
// lineNumber int
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// if m.Settings().RelativeNumber {
|
||||||
|
// // Relative line numbers: show distance from cursor, current line shows absolute
|
||||||
|
// if i > win.Cursor.Line {
|
||||||
|
// lineNumber = i - win.Cursor.Line
|
||||||
|
// gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
||||||
|
// } else if i < win.Cursor.Line {
|
||||||
|
// lineNumber = win.Cursor.Line - i
|
||||||
|
// gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
||||||
|
// } else {
|
||||||
|
// // Current line: show absolute number if Number is also set, otherwise show 0
|
||||||
|
// currentLine = true
|
||||||
|
// if m.Settings().Number {
|
||||||
|
// lineNumber = i + 1
|
||||||
|
// gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
||||||
|
// } else {
|
||||||
|
// gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, 0)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } else if m.Settings().Number {
|
||||||
|
// // Absolute line numbers only
|
||||||
|
// lineNumber = i + 1
|
||||||
|
// currentLine = (i == win.Cursor.Line)
|
||||||
|
// gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
||||||
|
// }
|
||||||
|
// if currentLine {
|
||||||
|
// view.WriteString(m.Styles().GutterCurrentLine.Render(gutter))
|
||||||
|
// } else {
|
||||||
|
// view.WriteString(m.Styles().Gutter.Render(gutter))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawStatusBar: Renders the status bar with mode and cursor position,
|
||||||
|
// padding the middle with spaces to fill the terminal width.
|
||||||
|
func drawStatusBar(w *core.Window, mode core.Mode) string {
|
||||||
|
left := leftBar(w, mode)
|
||||||
|
right := rightBar(w, mode)
|
||||||
|
|
||||||
|
diff := w.Width - (len(left) + len(right))
|
||||||
|
|
||||||
|
// This happens when the terminal spawns
|
||||||
|
if diff <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
middle := strings.Repeat(" ", diff)
|
||||||
|
return left + middle + right
|
||||||
|
}
|
||||||
|
|
||||||
|
// leftBar: Returns the left side of the status bar showing the current mode.
|
||||||
|
func leftBar(w *core.Window, mode core.Mode) string {
|
||||||
|
buf := w.Buffer
|
||||||
|
|
||||||
|
var flags []string
|
||||||
|
if buf.Modified {
|
||||||
|
flags = append(flags, "!")
|
||||||
|
}
|
||||||
|
if buf.ReadOnly {
|
||||||
|
flags = append(flags, "x")
|
||||||
|
}
|
||||||
|
|
||||||
|
var flagStr string
|
||||||
|
if len(flags) > 0 {
|
||||||
|
flagStr = "(" + strings.Join(flags, "") + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(" %s %s %s", mode.ToString(), buf.Filename, flagStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rightBar: Returns the right side of the status bar showing cursor position
|
||||||
|
// and selection count in visual mode.
|
||||||
|
func rightBar(w *core.Window, mode core.Mode) (bar string) {
|
||||||
|
if mode.IsVisualMode() {
|
||||||
|
lineCount := max(w.Anchor.Line, w.Cursor.Line) - min(w.Anchor.Line, w.Cursor.Line) + 1
|
||||||
|
bar = fmt.Sprintf("%d:%d <%d>", w.Cursor.Line+1, w.Cursor.Col+1, lineCount)
|
||||||
|
} else {
|
||||||
|
bar = fmt.Sprintf("%d:%d ", w.Cursor.Line+1, w.Cursor.Col+1)
|
||||||
|
}
|
||||||
|
buf := w.Buffer
|
||||||
|
bar = fmt.Sprintf("%s %s", buf.Filetype, bar)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawCommandBar: Renders the command line showing command input, errors, or
|
||||||
|
// output depending on the current mode and state.
|
||||||
|
func drawCommandBar(m Model) string {
|
||||||
|
// Compute left bar (command side)
|
||||||
|
var leftBar string
|
||||||
|
if m.Mode() == core.CommandMode {
|
||||||
|
leftBar = ":"
|
||||||
|
cmd := m.Command()
|
||||||
|
cur := m.CommandCursor()
|
||||||
|
for i := 0; i < len(cmd); i++ {
|
||||||
|
if i == cur {
|
||||||
|
leftBar += m.Styles().CursorStyle(m.Mode()).Render(string(cmd[i]))
|
||||||
|
} else {
|
||||||
|
leftBar += string(cmd[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cursor at end of command
|
||||||
|
if cur >= len(cmd) {
|
||||||
|
leftBar += m.Styles().CursorStyle(m.Mode()).Render(" ")
|
||||||
|
}
|
||||||
|
// bar = fmt.Sprintf("%s %d", bar, cur)
|
||||||
|
} else if m.CommandError() != nil {
|
||||||
|
leftBar = m.Styles().CommandError.Render(m.CommandError().Error())
|
||||||
|
} else if strings.TrimSpace(m.CommandOutput()) != "" {
|
||||||
|
leftBar = m.CommandOutput()
|
||||||
|
} else if strings.TrimSpace(m.Command()) != "" {
|
||||||
|
leftBar = fmt.Sprintf(":%s", m.Command())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute right bar
|
||||||
|
|
||||||
|
var rightBar string
|
||||||
|
if len(m.input.Pending()) > 0 {
|
||||||
|
width := 10 // Size of the block to display
|
||||||
|
rightBar = fmt.Sprintf("%-*s", width, m.input.Pending())
|
||||||
|
}
|
||||||
|
|
||||||
|
dif := m.termWidth - (len(leftBar) + len(rightBar))
|
||||||
|
|
||||||
|
bar := leftBar + strings.Repeat(" ", dif) + rightBar
|
||||||
|
return bar
|
||||||
|
}
|
||||||
|
|
||||||
|
// posInsideSelection: Returns true if the given position is inside the current
|
||||||
|
// visual selection, handling all three visual modes differently.
|
||||||
|
func posInsideSelection(w *core.Window, mode core.Mode, col, line int) bool {
|
||||||
|
switch mode {
|
||||||
|
case core.VisualLineMode:
|
||||||
|
startY := min(w.Anchor.Line, w.Cursor.Line)
|
||||||
|
endY := max(w.Anchor.Line, w.Cursor.Line)
|
||||||
return line >= startY && line <= endY
|
return line >= startY && line <= endY
|
||||||
|
|
||||||
case action.VisualMode:
|
case core.VisualMode:
|
||||||
ax := m.AnchorX()
|
ax := w.Anchor.Col
|
||||||
ay := m.AnchorY()
|
ay := w.Anchor.Line
|
||||||
|
|
||||||
cx := m.CursorX()
|
cx := w.Cursor.Col
|
||||||
cy := m.CursorY()
|
cy := w.Cursor.Line
|
||||||
|
|
||||||
// Normalize so start is always before end in document order
|
// Normalize so start is always before end in document order
|
||||||
var startX, startY, endX, endY int
|
var startX, startY, endX, endY int
|
||||||
@ -36,11 +304,11 @@ func posInsideSelection(m Model, col, line int) bool {
|
|||||||
beforeEnd := line < endY || (line == endY && col <= endX)
|
beforeEnd := line < endY || (line == endY && col <= endX)
|
||||||
return afterStart && beforeEnd
|
return afterStart && beforeEnd
|
||||||
|
|
||||||
case action.VisualBlockMode:
|
case core.VisualBlockMode:
|
||||||
startX := min(m.AnchorX(), m.CursorX())
|
startX := min(w.Anchor.Col, w.Cursor.Col)
|
||||||
startY := min(m.AnchorY(), m.CursorY())
|
startY := min(w.Anchor.Line, w.Cursor.Line)
|
||||||
endX := max(m.AnchorX(), m.CursorX())
|
endX := max(w.Anchor.Col, w.Cursor.Col)
|
||||||
endY := max(m.AnchorY(), m.CursorY())
|
endY := max(w.Anchor.Line, w.Cursor.Line)
|
||||||
|
|
||||||
return col >= startX && col <= endX &&
|
return col >= startX && col <= endX &&
|
||||||
line >= startY && line <= endY
|
line >= startY && line <= endY
|
||||||
@ -49,152 +317,3 @@ func posInsideSelection(m Model, col, line int) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func posIsAnchor(m Model, col, line int) bool {
|
|
||||||
ax := m.AnchorX()
|
|
||||||
ay := m.AnchorY()
|
|
||||||
return col == ax && line == ay
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) View() string {
|
|
||||||
var view strings.Builder
|
|
||||||
|
|
||||||
viewportHeight := m.ViewPortH()
|
|
||||||
start := m.ScrollY()
|
|
||||||
end := m.ScrollY() + viewportHeight
|
|
||||||
|
|
||||||
for i := start; i < end; i++ {
|
|
||||||
|
|
||||||
if i < m.LineCount() {
|
|
||||||
|
|
||||||
if m.Settings().Number || m.Settings().RelativeNumber {
|
|
||||||
var (
|
|
||||||
gutter string
|
|
||||||
currentLine bool = false
|
|
||||||
lineNumber int
|
|
||||||
)
|
|
||||||
|
|
||||||
if m.Settings().RelativeNumber {
|
|
||||||
// Relative line numbers: show distance from cursor, current line shows absolute
|
|
||||||
if i > m.CursorY() {
|
|
||||||
lineNumber = i - m.CursorY()
|
|
||||||
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
|
||||||
} else if i < m.CursorY() {
|
|
||||||
lineNumber = m.CursorY() - i
|
|
||||||
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
|
||||||
} else {
|
|
||||||
// Current line: show absolute number if Number is also set, otherwise show 0
|
|
||||||
currentLine = true
|
|
||||||
if m.Settings().Number {
|
|
||||||
lineNumber = i + 1
|
|
||||||
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
|
||||||
} else {
|
|
||||||
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if m.Settings().Number {
|
|
||||||
// Absolute line numbers only
|
|
||||||
lineNumber = i + 1
|
|
||||||
currentLine = (i == m.CursorY())
|
|
||||||
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
|
||||||
}
|
|
||||||
view.WriteString(m.gutterStyle(currentLine).Render(gutter))
|
|
||||||
}
|
|
||||||
|
|
||||||
runes := []rune(m.Line(i))
|
|
||||||
for x := 0; x <= len(runes); x++ {
|
|
||||||
if m.CursorY() == i && m.CursorX() == x {
|
|
||||||
if x < len(runes) {
|
|
||||||
view.WriteString(m.cursorStyle().Render(string(runes[x])))
|
|
||||||
} else {
|
|
||||||
view.WriteString(m.cursorStyle().Render(" "))
|
|
||||||
}
|
|
||||||
} else if x < len(runes) {
|
|
||||||
if m.Mode().IsVisualMode() && posIsAnchor(m, x, i) {
|
|
||||||
view.WriteString(m.visualAnchorStyle().Render(string(runes[x])))
|
|
||||||
} else if m.Mode().IsVisualMode() && posInsideSelection(m, x, i) {
|
|
||||||
view.WriteString(m.visualHighlightStyle().Render(string(runes[x])))
|
|
||||||
} else {
|
|
||||||
view.WriteRune(runes[x])
|
|
||||||
}
|
|
||||||
// To highlight blank lines when in visual mode
|
|
||||||
} else if m.Mode().IsVisualMode() && posInsideSelection(m, x, i) {
|
|
||||||
view.WriteString(m.visualHighlightStyle().Render(" "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Empty lines beyond file content
|
|
||||||
if m.Settings().Number || m.Settings().RelativeNumber {
|
|
||||||
format := fmt.Sprintf("%%-%ds ", m.Settings().GutterSize-1)
|
|
||||||
fmt.Fprintf(&view, format, "~")
|
|
||||||
} else {
|
|
||||||
view.WriteString("~")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
view.WriteString(drawStatusBar(m))
|
|
||||||
view.WriteString("\n")
|
|
||||||
view.WriteString(drawCommandBar(m))
|
|
||||||
|
|
||||||
return view.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawStatusBar(m Model) string {
|
|
||||||
left := leftBar(m)
|
|
||||||
right := rightBar(m)
|
|
||||||
|
|
||||||
diff := m.win_w - (len(left) + len(right))
|
|
||||||
|
|
||||||
// This happens when the terminal spawns
|
|
||||||
if diff <= 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
middle := strings.Repeat(" ", diff)
|
|
||||||
return left + middle + right
|
|
||||||
}
|
|
||||||
|
|
||||||
func leftBar(m Model) string {
|
|
||||||
return fmt.Sprintf(" %s", m.Mode().ToString())
|
|
||||||
}
|
|
||||||
|
|
||||||
func rightBar(m Model) (bar string) {
|
|
||||||
if m.Mode().IsVisualMode() {
|
|
||||||
lineCount := max(m.AnchorY(), m.CursorY()) - min(m.AnchorY(), m.CursorY()) + 1
|
|
||||||
bar = fmt.Sprintf("%d:%d <%d>", m.CursorY(), m.CursorX(), lineCount)
|
|
||||||
} else {
|
|
||||||
bar = fmt.Sprintf("%d:%d ", m.CursorY(), m.CursorX())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawCommandBar(m Model) (bar string) {
|
|
||||||
if m.Mode() == action.CommandMode {
|
|
||||||
bar = ":"
|
|
||||||
cmd := m.Command()
|
|
||||||
cur := m.CommandCursor()
|
|
||||||
for i := 0; i < len(cmd); i++ {
|
|
||||||
if i == cur {
|
|
||||||
bar += m.cursorStyle().Render(string(cmd[i]))
|
|
||||||
} else {
|
|
||||||
bar += string(cmd[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Cursor at end of command
|
|
||||||
if cur >= len(cmd) {
|
|
||||||
bar += m.cursorStyle().Render(" ")
|
|
||||||
}
|
|
||||||
// bar = fmt.Sprintf("%s %d", bar, cur)
|
|
||||||
} else if m.CommandError() != nil {
|
|
||||||
bar = m.commandErrorStyle().Render(m.CommandError().Error())
|
|
||||||
} else if strings.TrimSpace(m.CommandOutput()) != "" {
|
|
||||||
bar = m.CommandOutput()
|
|
||||||
} else if strings.TrimSpace(m.Command()) != "" {
|
|
||||||
bar = fmt.Sprintf(":%s", m.Command())
|
|
||||||
}
|
|
||||||
|
|
||||||
return bar
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,9 +2,11 @@ package input
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// InputState: Represents the current state of the input handler state machine.
|
||||||
type InputState int
|
type InputState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -14,11 +16,8 @@ const (
|
|||||||
StateMotionCount
|
StateMotionCount
|
||||||
)
|
)
|
||||||
|
|
||||||
// PositionGetter is used to get cursor position for operator ranges
|
// Handler: Manages input processing with a state machine for vim-style commands.
|
||||||
type PositionGetter interface {
|
// Handles counts, operators, motions, and multi-key sequences.
|
||||||
GetCursorPosition() action.Position
|
|
||||||
}
|
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
state InputState
|
state InputState
|
||||||
count1 int
|
count1 int
|
||||||
@ -37,6 +36,7 @@ type Handler struct {
|
|||||||
currentKeymap *Keymap
|
currentKeymap *Keymap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewHandler: Creates a new input handler with initialized keymaps for all modes.
|
||||||
func NewHandler() *Handler {
|
func NewHandler() *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
// keymap: NewNormalKeymap(),
|
// keymap: NewNormalKeymap(),
|
||||||
@ -48,23 +48,25 @@ 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 {
|
func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
||||||
// ESC always resets everything
|
// ESC always resets everything
|
||||||
if key == "esc" {
|
if key == "esc" {
|
||||||
h.Reset()
|
h.Reset()
|
||||||
if m.Mode() == action.InsertMode {
|
if m.Mode() == core.InsertMode {
|
||||||
m.ExitInsertMode()
|
m.ExitInsertMode()
|
||||||
} else {
|
} else {
|
||||||
m.SetMode(action.NormalMode)
|
m.SetMode(core.NormalMode)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert/command mode bypasses the normal state machine entirely
|
// Insert/command mode bypasses the normal state machine entirely
|
||||||
switch m.Mode() {
|
switch m.Mode() {
|
||||||
case action.InsertMode:
|
case core.InsertMode:
|
||||||
return h.handleInsertKey(m, key)
|
return h.handleInsertKey(m, key)
|
||||||
case action.CommandMode:
|
case core.CommandMode:
|
||||||
return h.handleCommandKey(m, key)
|
return h.handleCommandKey(m, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,11 +80,11 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
|||||||
|
|
||||||
// Set working keymap
|
// Set working keymap
|
||||||
switch m.Mode() {
|
switch m.Mode() {
|
||||||
case action.NormalMode:
|
case core.NormalMode:
|
||||||
h.currentKeymap = h.normalKeymap
|
h.currentKeymap = h.normalKeymap
|
||||||
case action.VisualMode,
|
case core.VisualMode,
|
||||||
action.VisualLineMode,
|
core.VisualLineMode,
|
||||||
action.VisualBlockMode:
|
core.VisualBlockMode:
|
||||||
h.currentKeymap = h.visualKeymap
|
h.currentKeymap = h.visualKeymap
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,7 +118,7 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// dispatch routes to the right handler based on current state
|
// Handler.dispatch: Routes to the appropriate handler based on current state.
|
||||||
func (h *Handler) dispatch(m action.Model, kind string, binding any, key string) tea.Cmd {
|
func (h *Handler) dispatch(m action.Model, kind string, binding any, key string) tea.Cmd {
|
||||||
switch h.state {
|
switch h.state {
|
||||||
case StateReady, StateCount:
|
case StateReady, StateCount:
|
||||||
@ -128,6 +130,8 @@ func (h *Handler) dispatch(m action.Model, kind string, binding any, key string)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler.handleInitial: Handles input when no operator is pending. Executes
|
||||||
|
// motions, actions, or stores operators waiting for a motion.
|
||||||
func (h *Handler) handleInitial(m action.Model, kind string, binding any, key string) tea.Cmd {
|
func (h *Handler) handleInitial(m action.Model, kind string, binding any, key string) tea.Cmd {
|
||||||
count := h.effectiveCount()
|
count := h.effectiveCount()
|
||||||
|
|
||||||
@ -147,14 +151,14 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
|
|||||||
if m.Mode().IsVisualMode() {
|
if m.Mode().IsVisualMode() {
|
||||||
start, end := normalizeVisualSelection(m)
|
start, end := normalizeVisualSelection(m)
|
||||||
// Visual line mode is linewise, others are charwise inclusive
|
// Visual line mode is linewise, others are charwise inclusive
|
||||||
mtype := action.CharwiseInclusive
|
mtype := core.CharwiseInclusive
|
||||||
if m.Mode() == action.VisualLineMode {
|
if m.Mode() == core.VisualLineMode {
|
||||||
mtype = action.Linewise
|
mtype = core.Linewise
|
||||||
}
|
}
|
||||||
cmd := op.Operate(m, start, end, mtype)
|
cmd := op.Operate(m, start, end, mtype)
|
||||||
// Only reset to normal mode if operator didn't enter insert mode
|
// Only reset to normal mode if operator didn't enter insert mode
|
||||||
if m.Mode() != action.InsertMode {
|
if m.Mode() != core.InsertMode {
|
||||||
m.SetMode(action.NormalMode)
|
m.SetMode(core.NormalMode)
|
||||||
}
|
}
|
||||||
h.Reset()
|
h.Reset()
|
||||||
return cmd
|
return cmd
|
||||||
@ -179,8 +183,11 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler.handleAfterOperator: Handles input when an operator is pending.
|
||||||
|
// Processes double-press operators (dd, yy) or applies operator to motion range.
|
||||||
func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any, key string) tea.Cmd {
|
func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any, key string) tea.Cmd {
|
||||||
count := h.effectiveCount()
|
count := h.effectiveCount()
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
|
||||||
// dd, yy, cc - same operator key pressed twice
|
// dd, yy, cc - same operator key pressed twice
|
||||||
if kind == "operator" && key == h.operatorKey {
|
if kind == "operator" && key == h.operatorKey {
|
||||||
@ -201,10 +208,9 @@ func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any,
|
|||||||
mot = r.WithCount(count).(action.Motion)
|
mot = r.WithCount(count).(action.Motion)
|
||||||
}
|
}
|
||||||
// Get range and motion type
|
// Get range and motion type
|
||||||
pg := m.(PositionGetter)
|
start := win.Cursor
|
||||||
start := pg.GetCursorPosition()
|
|
||||||
mot.Execute(m)
|
mot.Execute(m)
|
||||||
end := pg.GetCursorPosition()
|
end := win.Cursor
|
||||||
cmd := h.operator.Operate(m, start, end, mot.Type())
|
cmd := h.operator.Operate(m, start, end, mot.Type())
|
||||||
h.Reset()
|
h.Reset()
|
||||||
return cmd
|
return cmd
|
||||||
@ -214,6 +220,8 @@ func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler.tryAccumulateCount: Attempts to add a digit to the count. Returns
|
||||||
|
// true if successful, false if the key is not a digit or is an invalid count.
|
||||||
func (h *Handler) tryAccumulateCount(key string) bool {
|
func (h *Handler) tryAccumulateCount(key string) bool {
|
||||||
if len(key) != 1 || key[0] < '0' || key[0] > '9' {
|
if len(key) != 1 || key[0] < '0' || key[0] > '9' {
|
||||||
return false
|
return false
|
||||||
@ -239,6 +247,7 @@ func (h *Handler) tryAccumulateCount(key string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler.currentCount: Returns the count currently being accumulated based on state.
|
||||||
func (h *Handler) currentCount() int {
|
func (h *Handler) currentCount() int {
|
||||||
if h.state == StateOperatorPending || h.state == StateMotionCount {
|
if h.state == StateOperatorPending || h.state == StateMotionCount {
|
||||||
return h.count2
|
return h.count2
|
||||||
@ -246,6 +255,8 @@ func (h *Handler) currentCount() int {
|
|||||||
return h.count1
|
return h.count1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler.effectiveCount: Calculates the final count by multiplying count1 and
|
||||||
|
// count2, treating 0 as 1 for both.
|
||||||
func (h *Handler) effectiveCount() int {
|
func (h *Handler) effectiveCount() int {
|
||||||
c1, c2 := h.count1, h.count2
|
c1, c2 := h.count1, h.count2
|
||||||
if c1 == 0 {
|
if c1 == 0 {
|
||||||
@ -257,6 +268,7 @@ func (h *Handler) effectiveCount() int {
|
|||||||
return c1 * c2
|
return c1 * c2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler.Reset: Clears all handler state including counts, operators, and buffers.
|
||||||
func (h *Handler) Reset() {
|
func (h *Handler) Reset() {
|
||||||
h.state = StateReady
|
h.state = StateReady
|
||||||
h.count1 = 0
|
h.count1 = 0
|
||||||
@ -267,10 +279,13 @@ func (h *Handler) Reset() {
|
|||||||
h.pending = ""
|
h.pending = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler.Pending: Returns the accumulated input buffer for display.
|
||||||
func (h *Handler) Pending() string {
|
func (h *Handler) Pending() string {
|
||||||
return h.buffer
|
return h.buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler.handleInsertKey: Processes a keypress in insert mode, recording it
|
||||||
|
// for count replay and executing it as an action or character insertion.
|
||||||
func (h *Handler) handleInsertKey(m action.Model, key string) tea.Cmd {
|
func (h *Handler) handleInsertKey(m action.Model, key string) tea.Cmd {
|
||||||
// Record the key for count replay (e.g. 5i...)
|
// Record the key for count replay (e.g. 5i...)
|
||||||
m.SetInsertKeys(append(m.InsertKeys(), key))
|
m.SetInsertKeys(append(m.InsertKeys(), key))
|
||||||
@ -288,6 +303,8 @@ func (h *Handler) handleInsertKey(m action.Model, key string) tea.Cmd {
|
|||||||
return action.InsertChar{Char: key}.Execute(m)
|
return action.InsertChar{Char: key}.Execute(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler.handleCommandKey: Processes a keypress in command mode, executing
|
||||||
|
// it as an action or inserting it into the command line.
|
||||||
func (h *Handler) handleCommandKey(m action.Model, key string) tea.Cmd {
|
func (h *Handler) handleCommandKey(m action.Model, key string) tea.Cmd {
|
||||||
kind, binding := h.commandKeymap.Lookup(key)
|
kind, binding := h.commandKeymap.Lookup(key)
|
||||||
switch kind {
|
switch kind {
|
||||||
@ -301,9 +318,12 @@ func (h *Handler) handleCommandKey(m action.Model, key string) tea.Cmd {
|
|||||||
return action.InsertCommandChar{Char: key}.Execute(m)
|
return action.InsertCommandChar{Char: key}.Execute(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeVisualSelection(m action.Model) (action.Position, action.Position) {
|
// normalizeVisualSelection: Returns the visual selection with start before end,
|
||||||
a := action.Position{Line: m.AnchorY(), Col: m.AnchorX()}
|
// regardless of which direction the selection was made.
|
||||||
c := action.Position{Line: m.CursorY(), Col: m.CursorX()}
|
func normalizeVisualSelection(m action.Model) (core.Position, core.Position) {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
a := core.Position{Line: win.Anchor.Line, Col: win.Anchor.Col}
|
||||||
|
c := core.Position{Line: win.Cursor.Line, Col: win.Cursor.Col}
|
||||||
if a.Line < c.Line || (a.Line == c.Line && a.Col <= c.Col) {
|
if a.Line < c.Line || (a.Line == c.Line && a.Col <= c.Col) {
|
||||||
return a, c
|
return a, c
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,12 +7,14 @@ import (
|
|||||||
"git.gophernest.net/azpect/TextEditor/internal/operator"
|
"git.gophernest.net/azpect/TextEditor/internal/operator"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Keymap: Maps key sequences to motions, operators, and actions.
|
||||||
type Keymap struct {
|
type Keymap struct {
|
||||||
motions map[string]action.Motion
|
motions map[string]action.Motion
|
||||||
operators map[string]action.Operator
|
operators map[string]action.Operator
|
||||||
actions map[string]action.Action // standalone actions: i.e., 'i', 'a'
|
actions map[string]action.Action // standalone actions: i.e., 'i', 'a'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewNormalKeymap: Creates a keymap for normal mode with all standard vim bindings.
|
||||||
func NewNormalKeymap() *Keymap {
|
func NewNormalKeymap() *Keymap {
|
||||||
return &Keymap{
|
return &Keymap{
|
||||||
motions: map[string]action.Motion{
|
motions: map[string]action.Motion{
|
||||||
@ -64,6 +66,7 @@ func NewNormalKeymap() *Keymap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewVisualKeymap: Creates a keymap for visual modes (character, line, block).
|
||||||
func NewVisualKeymap() *Keymap {
|
func NewVisualKeymap() *Keymap {
|
||||||
return &Keymap{
|
return &Keymap{
|
||||||
motions: map[string]action.Motion{
|
motions: map[string]action.Motion{
|
||||||
@ -97,6 +100,7 @@ func NewVisualKeymap() *Keymap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewInsertKeymap: Creates a keymap for insert mode with editing actions.
|
||||||
func NewInsertKeymap() *Keymap {
|
func NewInsertKeymap() *Keymap {
|
||||||
return &Keymap{
|
return &Keymap{
|
||||||
motions: map[string]action.Motion{
|
motions: map[string]action.Motion{
|
||||||
@ -117,6 +121,7 @@ func NewInsertKeymap() *Keymap {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCommandKeymap: Creates a keymap for command mode with command line editing.
|
||||||
func NewCommandKeymap() *Keymap {
|
func NewCommandKeymap() *Keymap {
|
||||||
return &Keymap{
|
return &Keymap{
|
||||||
motions: map[string]action.Motion{
|
motions: map[string]action.Motion{
|
||||||
@ -135,7 +140,7 @@ func NewCommandKeymap() *Keymap {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup returns what type of binding a key is
|
// Keymap.Lookup: Returns the type and value of a key binding (motion, operator, or action).
|
||||||
func (km *Keymap) Lookup(key string) (kind string, value any) {
|
func (km *Keymap) Lookup(key string) (kind string, value any) {
|
||||||
if m, ok := km.motions[key]; ok {
|
if m, ok := km.motions[key]; ok {
|
||||||
return "motion", m
|
return "motion", m
|
||||||
@ -149,7 +154,7 @@ func (km *Keymap) Lookup(key string) (kind string, value any) {
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasPrefix returns true if any binding starts with this prefix
|
// Keymap.HasPrefix: Returns true if any binding starts with the given prefix.
|
||||||
func (km *Keymap) HasPrefix(prefix string) bool {
|
func (km *Keymap) HasPrefix(prefix string) bool {
|
||||||
for key := range km.motions {
|
for key := range km.motions {
|
||||||
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
|
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
package motion
|
package motion
|
||||||
|
|
||||||
import (
|
import (
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MoveDown implements Motion (j) - linewise
|
// MoveDown implements Motion (j) - linewise
|
||||||
@ -10,15 +11,18 @@ type MoveDown struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MoveDown.Execute: Moves the cursor down by Count lines without going past
|
||||||
|
// the last line of the buffer.
|
||||||
func (a MoveDown) Execute(m action.Model) tea.Cmd {
|
func (a MoveDown) Execute(m action.Model) tea.Cmd {
|
||||||
for i := 0; i < a.Count && m.CursorY() < m.LineCount()-1; i++ {
|
win := m.ActiveWindow()
|
||||||
m.SetCursorY(m.CursorY() + 1)
|
buf := m.ActiveBuffer()
|
||||||
|
for i := 0; i < a.Count && win.Cursor.Line < buf.LineCount()-1; i++ {
|
||||||
|
win.SetCursorLine(win.Cursor.Line + 1)
|
||||||
}
|
}
|
||||||
m.ClampCursorX()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveDown) Type() action.MotionType { return action.Linewise }
|
func (a MoveDown) Type() core.MotionType { return core.Linewise }
|
||||||
|
|
||||||
func (a MoveDown) WithCount(n int) action.Action {
|
func (a MoveDown) WithCount(n int) action.Action {
|
||||||
return MoveDown{Count: n}
|
return MoveDown{Count: n}
|
||||||
@ -29,15 +33,17 @@ type MoveUp struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MoveUp.Execute: Moves the cursor up by Count lines without going above
|
||||||
|
// line 0.
|
||||||
func (a MoveUp) Execute(m action.Model) tea.Cmd {
|
func (a MoveUp) Execute(m action.Model) tea.Cmd {
|
||||||
for i := 0; i < a.Count && m.CursorY() > 0; i++ {
|
win := m.ActiveWindow()
|
||||||
m.SetCursorY(m.CursorY() - 1)
|
for i := 0; i < a.Count && win.Cursor.Line > 0; i++ {
|
||||||
|
win.SetCursorLine(win.Cursor.Line - 1)
|
||||||
}
|
}
|
||||||
m.ClampCursorX()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveUp) Type() action.MotionType { return action.Linewise }
|
func (a MoveUp) Type() core.MotionType { return core.Linewise }
|
||||||
|
|
||||||
func (a MoveUp) WithCount(n int) action.Action {
|
func (a MoveUp) WithCount(n int) action.Action {
|
||||||
return MoveUp{Count: n}
|
return MoveUp{Count: n}
|
||||||
@ -48,15 +54,17 @@ type MoveLeft struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MoveLeft.Execute: Moves the cursor left by Count columns without going
|
||||||
|
// past column 0.
|
||||||
func (a MoveLeft) Execute(m action.Model) tea.Cmd {
|
func (a MoveLeft) Execute(m action.Model) tea.Cmd {
|
||||||
for i := 0; i < a.Count && m.CursorX() > 0; i++ {
|
win := m.ActiveWindow()
|
||||||
m.SetCursorX(m.CursorX() - 1)
|
for i := 0; i < a.Count && win.Cursor.Col > 0; i++ {
|
||||||
|
win.SetCursorCol(win.Cursor.Col - 1)
|
||||||
}
|
}
|
||||||
m.ClampCursorX()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveLeft) Type() action.MotionType { return action.CharwiseExclusive }
|
func (a MoveLeft) Type() core.MotionType { return core.CharwiseExclusive }
|
||||||
|
|
||||||
func (a MoveLeft) WithCount(n int) action.Action {
|
func (a MoveLeft) WithCount(n int) action.Action {
|
||||||
return MoveLeft{Count: n}
|
return MoveLeft{Count: n}
|
||||||
@ -67,16 +75,19 @@ type MoveRight struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MoveRight.Execute: Moves the cursor right by Count columns without going
|
||||||
|
// past the end of the line.
|
||||||
func (a MoveRight) Execute(m action.Model) tea.Cmd {
|
func (a MoveRight) Execute(m action.Model) tea.Cmd {
|
||||||
lineLen := len(m.Line(m.CursorY()))
|
win := m.ActiveWindow()
|
||||||
for i := 0; i < a.Count && m.CursorX() <= lineLen; i++ {
|
buf := m.ActiveBuffer()
|
||||||
m.SetCursorX(m.CursorX() + 1)
|
lineLen := len(buf.Lines[win.Cursor.Line])
|
||||||
|
for i := 0; i < a.Count && win.Cursor.Col <= lineLen; i++ {
|
||||||
|
win.SetCursorCol(win.Cursor.Col + 1)
|
||||||
}
|
}
|
||||||
m.ClampCursorX()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveRight) Type() action.MotionType { return action.CharwiseExclusive }
|
func (a MoveRight) Type() core.MotionType { return core.CharwiseExclusive }
|
||||||
|
|
||||||
func (a MoveRight) WithCount(n int) action.Action {
|
func (a MoveRight) WithCount(n int) action.Action {
|
||||||
return MoveRight{Count: n}
|
return MoveRight{Count: n}
|
||||||
|
|||||||
@ -2,25 +2,32 @@ package motion
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MoveCommandLeft implements Motion - moves cursor left in command line.
|
||||||
type MoveCommandLeft struct{}
|
type MoveCommandLeft struct{}
|
||||||
|
|
||||||
|
// MoveCommandLeft.Execute: Moves the command line cursor one position to the left.
|
||||||
func (a MoveCommandLeft) Execute(m action.Model) tea.Cmd {
|
func (a MoveCommandLeft) Execute(m action.Model) tea.Cmd {
|
||||||
// The set function handles bounds
|
// The set function handles bounds
|
||||||
m.SetCommandCursor(m.CommandCursor() - 1)
|
m.SetCommandCursor(m.CommandCursor() - 1)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveCommandLeft) Type() action.MotionType { return action.CharwiseExclusive }
|
// MoveCommandLeft.Type: Returns CharwiseExclusive for command line motion.
|
||||||
|
func (a MoveCommandLeft) Type() core.MotionType { return core.CharwiseExclusive }
|
||||||
|
|
||||||
|
// MoveCommandRight implements Motion - moves cursor right in command line.
|
||||||
type MoveCommandRight struct{}
|
type MoveCommandRight struct{}
|
||||||
|
|
||||||
|
// MoveCommandRight.Execute: Moves the command line cursor one position to the right.
|
||||||
func (a MoveCommandRight) Execute(m action.Model) tea.Cmd {
|
func (a MoveCommandRight) Execute(m action.Model) tea.Cmd {
|
||||||
// The set function handles bounds
|
// The set function handles bounds
|
||||||
m.SetCommandCursor(m.CommandCursor() + 1)
|
m.SetCommandCursor(m.CommandCursor() + 1)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveCommandRight) Type() action.MotionType { return action.CharwiseExclusive }
|
// MoveCommandRight.Type: Returns CharwiseExclusive for command line motion.
|
||||||
|
func (a MoveCommandRight) Type() core.MotionType { return core.CharwiseExclusive }
|
||||||
|
|||||||
@ -2,58 +2,70 @@ package motion
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MoveToTop implements Motion (gg) - linewise
|
// MoveToTop implements Motion (gg) - linewise
|
||||||
type MoveToTop struct{}
|
type MoveToTop struct{}
|
||||||
|
|
||||||
|
// MoveToTop.Execute: Moves the cursor to the first line of the buffer.
|
||||||
func (a MoveToTop) Execute(m action.Model) tea.Cmd {
|
func (a MoveToTop) Execute(m action.Model) tea.Cmd {
|
||||||
m.SetCursorY(0)
|
win := m.ActiveWindow()
|
||||||
m.ClampCursorX()
|
win.SetCursorLine(0)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveToTop) Type() action.MotionType { return action.Linewise }
|
func (a MoveToTop) Type() core.MotionType { return core.Linewise }
|
||||||
|
|
||||||
// MoveToBottom implements Motion (G) - linewise
|
// MoveToBottom implements Motion (G) - linewise
|
||||||
type MoveToBottom struct{}
|
type MoveToBottom struct{}
|
||||||
|
|
||||||
|
// MoveToBottom.Execute: Moves the cursor to the last line of the buffer.
|
||||||
func (a MoveToBottom) Execute(m action.Model) tea.Cmd {
|
func (a MoveToBottom) Execute(m action.Model) tea.Cmd {
|
||||||
m.SetCursorY(m.LineCount() - 1)
|
win := m.ActiveWindow()
|
||||||
m.ClampCursorX()
|
buf := m.ActiveBuffer()
|
||||||
|
win.SetCursorLine(buf.LineCount() - 1)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveToBottom) Type() action.MotionType { return action.Linewise }
|
func (a MoveToBottom) Type() core.MotionType { return core.Linewise }
|
||||||
|
|
||||||
// MoveToLineStart implements Motion (0) - charwise
|
// MoveToLineStart implements Motion (0) - charwise
|
||||||
type MoveToLineStart struct{}
|
type MoveToLineStart struct{}
|
||||||
|
|
||||||
|
// MoveToLineStart.Execute: Moves the cursor to the beginning of the current line.
|
||||||
func (a MoveToLineStart) Execute(m action.Model) tea.Cmd {
|
func (a MoveToLineStart) Execute(m action.Model) tea.Cmd {
|
||||||
m.SetCursorX(0)
|
win := m.ActiveWindow()
|
||||||
m.ClampCursorX()
|
win.SetCursorCol(0)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveToLineStart) Type() action.MotionType { return action.CharwiseExclusive }
|
func (a MoveToLineStart) Type() core.MotionType { return core.CharwiseExclusive }
|
||||||
|
|
||||||
// MoveToLineEnd implements Motion ($) - charwise
|
// MoveToLineEnd implements Motion ($) - charwise
|
||||||
type MoveToLineEnd struct{}
|
type MoveToLineEnd struct{}
|
||||||
|
|
||||||
|
// MoveToLineEnd.Execute: Moves the cursor to the end of the current line.
|
||||||
func (a MoveToLineEnd) Execute(m action.Model) tea.Cmd {
|
func (a MoveToLineEnd) Execute(m action.Model) tea.Cmd {
|
||||||
m.SetCursorX(len(m.Line(m.CursorY())))
|
win := m.ActiveWindow()
|
||||||
m.ClampCursorX()
|
buf := m.ActiveBuffer()
|
||||||
|
win.SetCursorCol(len(buf.Lines[win.Cursor.Line]))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveToLineEnd) Type() action.MotionType { return action.CharwiseInclusive }
|
func (a MoveToLineEnd) Type() core.MotionType { return core.CharwiseInclusive }
|
||||||
|
|
||||||
// MoveToLineContentStart implements Motion (_) - charwise
|
// MoveToLineContentStart implements Motion (_) - charwise
|
||||||
type MoveToLineContentStart struct{}
|
type MoveToLineContentStart struct{}
|
||||||
|
|
||||||
|
// MoveToLineContentStart.Execute: Moves the cursor to the first non-whitespace
|
||||||
|
// character on the current line.
|
||||||
func (a MoveToLineContentStart) Execute(m action.Model) tea.Cmd {
|
func (a MoveToLineContentStart) Execute(m action.Model) tea.Cmd {
|
||||||
line := m.Line(m.CursorY())
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
line := buf.Lines[win.Cursor.Line]
|
||||||
x := 0
|
x := 0
|
||||||
for x < len(line) {
|
for x < len(line) {
|
||||||
ch := line[x]
|
ch := line[x]
|
||||||
@ -68,27 +80,30 @@ func (a MoveToLineContentStart) Execute(m action.Model) tea.Cmd {
|
|||||||
x--
|
x--
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetCursorX(x)
|
win.SetCursorCol(x)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveToLineContentStart) Type() action.MotionType { return action.CharwiseExclusive }
|
func (a MoveToLineContentStart) Type() core.MotionType { return core.CharwiseExclusive }
|
||||||
|
|
||||||
// MoveToColumn implements Motion (|) - charwise
|
// MoveToColumn implements Motion (|) - charwise
|
||||||
type MoveToColumn struct {
|
type MoveToColumn struct {
|
||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MoveToColumn.Execute: Moves the cursor to the column specified by Count.
|
||||||
func (a MoveToColumn) Execute(m action.Model) tea.Cmd {
|
func (a MoveToColumn) Execute(m action.Model) tea.Cmd {
|
||||||
line := m.Line(m.CursorY())
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
line := buf.Lines[win.Cursor.Line]
|
||||||
col := min(a.Count-1, len(line)-1)
|
col := min(a.Count-1, len(line)-1)
|
||||||
|
|
||||||
m.SetCursorX(col)
|
win.SetCursorCol(col)
|
||||||
m.ClampCursorX()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveToColumn) Type() action.MotionType { return action.CharwiseExclusive }
|
func (a MoveToColumn) Type() core.MotionType { return core.CharwiseExclusive }
|
||||||
|
|
||||||
func (a MoveToColumn) WithCount(n int) action.Action {
|
func (a MoveToColumn) WithCount(n int) action.Action {
|
||||||
return MoveToColumn{Count: n}
|
return MoveToColumn{Count: n}
|
||||||
@ -99,23 +114,28 @@ func (a MoveToColumn) WithCount(n int) action.Action {
|
|||||||
// ScrollDownHalfPage implements Motion (ctrl+d) - linewise
|
// ScrollDownHalfPage implements Motion (ctrl+d) - linewise
|
||||||
type ScrollDownHalfPage struct{}
|
type ScrollDownHalfPage struct{}
|
||||||
|
|
||||||
|
// ScrollDownHalfPage.Execute: Scrolls down half a page while maintaining the
|
||||||
|
// cursor's relative position in the viewport.
|
||||||
func (a ScrollDownHalfPage) Execute(m action.Model) tea.Cmd {
|
func (a ScrollDownHalfPage) Execute(m action.Model) tea.Cmd {
|
||||||
viewportHeight := m.ViewPortH()
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
viewportHeight := win.Height - 2
|
||||||
if viewportHeight <= 0 {
|
if viewportHeight <= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
scroll := viewportHeight / 2
|
scroll := viewportHeight / 2
|
||||||
scrollOff := m.Settings().ScrollOff
|
scrollOff := win.Options.ScrollOff
|
||||||
|
|
||||||
// Current relative position in viewport
|
// Current relative position in viewport
|
||||||
relY := m.CursorY() - m.ScrollY()
|
relY := win.Cursor.Line - win.ScrollY
|
||||||
|
|
||||||
// Scroll down, clamped to valid range
|
// Scroll down, clamped to valid range
|
||||||
newScrollY := m.ScrollY() + scroll
|
newScrollY := win.ScrollY + scroll
|
||||||
maxScroll := max(0, m.LineCount()-viewportHeight)
|
maxScroll := max(0, buf.LineCount()-viewportHeight)
|
||||||
newScrollY = min(newScrollY, maxScroll)
|
newScrollY = min(newScrollY, maxScroll)
|
||||||
m.SetScrollY(newScrollY)
|
win.SetScrollY(newScrollY)
|
||||||
|
|
||||||
// Maintain relative position, respecting scrollOff
|
// Maintain relative position, respecting scrollOff
|
||||||
if relY < scrollOff {
|
if relY < scrollOff {
|
||||||
@ -126,33 +146,37 @@ func (a ScrollDownHalfPage) Execute(m action.Model) tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newCursorY := newScrollY + relY
|
newCursorY := newScrollY + relY
|
||||||
newCursorY = max(0, min(newCursorY, m.LineCount()-1))
|
newCursorY = max(0, min(newCursorY, buf.LineCount()-1))
|
||||||
m.SetCursorY(newCursorY)
|
win.SetCursorLine(newCursorY)
|
||||||
m.ClampCursorX()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a ScrollDownHalfPage) Type() action.MotionType { return action.Linewise }
|
func (a ScrollDownHalfPage) Type() core.MotionType { return core.Linewise }
|
||||||
|
|
||||||
// ScrollUpHalfPage implements Motion (ctrl+u) - linewise
|
// ScrollUpHalfPage implements Motion (ctrl+u) - linewise
|
||||||
type ScrollUpHalfPage struct{}
|
type ScrollUpHalfPage struct{}
|
||||||
|
|
||||||
|
// ScrollUpHalfPage.Execute: Scrolls up half a page while maintaining the
|
||||||
|
// cursor's relative position in the viewport.
|
||||||
func (a ScrollUpHalfPage) Execute(m action.Model) tea.Cmd {
|
func (a ScrollUpHalfPage) Execute(m action.Model) tea.Cmd {
|
||||||
viewportHeight := m.ViewPortH()
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
viewportHeight := win.Height - 2
|
||||||
if viewportHeight <= 0 {
|
if viewportHeight <= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
scroll := viewportHeight / 2
|
scroll := viewportHeight / 2
|
||||||
scrollOff := m.Settings().ScrollOff
|
scrollOff := win.Options.ScrollOff
|
||||||
|
|
||||||
// Current relative position in viewport
|
// Current relative position in viewport
|
||||||
relY := m.CursorY() - m.ScrollY()
|
relY := win.Cursor.Line - win.ScrollY
|
||||||
|
|
||||||
// Scroll up, clamped to valid range
|
// Scroll up, clamped to valid range
|
||||||
newScrollY := m.ScrollY() - scroll
|
newScrollY := win.ScrollY - scroll
|
||||||
newScrollY = max(0, newScrollY)
|
newScrollY = max(0, newScrollY)
|
||||||
m.SetScrollY(newScrollY)
|
win.SetScrollY(newScrollY)
|
||||||
|
|
||||||
// Maintain relative position, respecting scrollOff
|
// Maintain relative position, respecting scrollOff
|
||||||
if relY < scrollOff {
|
if relY < scrollOff {
|
||||||
@ -163,11 +187,10 @@ func (a ScrollUpHalfPage) Execute(m action.Model) tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newCursorY := newScrollY + relY
|
newCursorY := newScrollY + relY
|
||||||
newCursorY = max(0, min(newCursorY, m.LineCount()-1))
|
newCursorY = max(0, min(newCursorY, buf.LineCount()-1))
|
||||||
m.SetCursorY(newCursorY)
|
win.SetCursorLine(newCursorY)
|
||||||
m.ClampCursorX()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a ScrollUpHalfPage) Type() action.MotionType { return action.Linewise }
|
func (a ScrollUpHalfPage) Type() core.MotionType { return core.Linewise }
|
||||||
|
|||||||
@ -2,9 +2,12 @@ package motion
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// isWordChar: Returns true if the character is a word character (alphanumeric
|
||||||
|
// or underscore).
|
||||||
func isWordChar(c byte) bool {
|
func isWordChar(c byte) bool {
|
||||||
return (c >= 'a' && c <= 'z') ||
|
return (c >= 'a' && c <= 'z') ||
|
||||||
(c >= 'A' && c <= 'Z') ||
|
(c >= 'A' && c <= 'Z') ||
|
||||||
@ -12,12 +15,16 @@ func isWordChar(c byte) bool {
|
|||||||
c == '_'
|
c == '_'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isWordPunctuation: Returns true if the character is punctuation (not whitespace
|
||||||
|
// and not a word character).
|
||||||
func isWordPunctuation(c byte) bool {
|
func isWordPunctuation(c byte) bool {
|
||||||
return c != ' ' && c != '\t' && !isWordChar(c)
|
return c != ' ' && c != '\t' && !isWordChar(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextWordStart(m action.Model, x, y int) (int, int) {
|
// nextWordStart: Finds the start of the next word from position (x,y), handling
|
||||||
line := m.Line(y)
|
// word boundaries and line crossing.
|
||||||
|
func nextWordStart(buf *core.Buffer, x, y int) (int, int) {
|
||||||
|
line := buf.Lines[y]
|
||||||
|
|
||||||
// Skip current class
|
// Skip current class
|
||||||
if x < len(line) {
|
if x < len(line) {
|
||||||
@ -46,13 +53,13 @@ func nextWordStart(m action.Model, x, y int) (int, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If next line is the end of the file, exit now
|
// If next line is the end of the file, exit now
|
||||||
if y+1 >= m.LineCount() {
|
if y+1 >= buf.LineCount() {
|
||||||
return x, y
|
return x, y
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move to first char of next line
|
// Move to first char of next line
|
||||||
y++
|
y++
|
||||||
line = m.Line(y)
|
line = buf.Lines[y]
|
||||||
x = 0
|
x = 0
|
||||||
|
|
||||||
// If the first char of the new line is no whitespace, stay here!
|
// If the first char of the new line is no whitespace, stay here!
|
||||||
@ -64,8 +71,10 @@ func nextWordStart(m action.Model, x, y int) (int, int) {
|
|||||||
return x, y
|
return x, y
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextWORDStart(m action.Model, x, y int) (int, int) {
|
// nextWORDStart: Finds the start of the next WORD from position (x,y), treating
|
||||||
line := m.Line(y)
|
// all non-whitespace as a single class.
|
||||||
|
func nextWORDStart(buf *core.Buffer, x, y int) (int, int) {
|
||||||
|
line := buf.Lines[y]
|
||||||
|
|
||||||
// Skip current WORD (all non-whitespace is one class for W)
|
// Skip current WORD (all non-whitespace is one class for W)
|
||||||
for x < len(line) && line[x] != ' ' && line[x] != '\t' {
|
for x < len(line) && line[x] != ' ' && line[x] != '\t' {
|
||||||
@ -85,13 +94,13 @@ func nextWORDStart(m action.Model, x, y int) (int, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If next line is the end of the file, exit now
|
// If next line is the end of the file, exit now
|
||||||
if y+1 >= m.LineCount() {
|
if y+1 >= buf.LineCount() {
|
||||||
return x, y
|
return x, y
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move to first char of next line
|
// Move to first char of next line
|
||||||
y++
|
y++
|
||||||
line = m.Line(y)
|
line = buf.Lines[y]
|
||||||
x = 0
|
x = 0
|
||||||
|
|
||||||
// If the first char of the new line is no whitespace, stay here!
|
// If the first char of the new line is no whitespace, stay here!
|
||||||
@ -103,21 +112,23 @@ func nextWORDStart(m action.Model, x, y int) (int, int) {
|
|||||||
return x, y
|
return x, y
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextWordEnd(m action.Model, x, y int) (int, int) {
|
// nextWordEnd: Finds the end of the next word from position (x,y), respecting
|
||||||
line := m.Line(y)
|
// word character classes.
|
||||||
|
func nextWordEnd(buf *core.Buffer, x, y int) (int, int) {
|
||||||
|
line := buf.Lines[y]
|
||||||
|
|
||||||
// Advance once to avoid being stuck on the current end
|
// Advance once to avoid being stuck on the current end
|
||||||
x++
|
x++
|
||||||
if x >= len(line) {
|
if x >= len(line) {
|
||||||
// At last line of file, pin cursor to end of file
|
// At last line of file, pin cursor to end of file
|
||||||
if y+1 >= m.LineCount() {
|
if y+1 >= buf.LineCount() {
|
||||||
return len(line) - 1, y
|
return len(line) - 1, y
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, move to next line
|
// Otherwise, move to next line
|
||||||
y++
|
y++
|
||||||
x = 0
|
x = 0
|
||||||
line = m.Line(y)
|
line = buf.Lines[y]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip whitespace and cross lines if needed
|
// Skip whitespace and cross lines if needed
|
||||||
@ -133,13 +144,13 @@ func nextWordEnd(m action.Model, x, y int) (int, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If next line is the end of the file, exit now
|
// If next line is the end of the file, exit now
|
||||||
if y+1 >= m.LineCount() {
|
if y+1 >= buf.LineCount() {
|
||||||
return x, y
|
return x, y
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move to first char of next line
|
// Move to first char of next line
|
||||||
y++
|
y++
|
||||||
line = m.Line(y)
|
line = buf.Lines[y]
|
||||||
x = 0
|
x = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,21 +171,23 @@ func nextWordEnd(m action.Model, x, y int) (int, int) {
|
|||||||
return x, y
|
return x, y
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextWORDEnd(m action.Model, x, y int) (int, int) {
|
// nextWORDEnd: Finds the end of the next WORD from position (x,y), treating
|
||||||
line := m.Line(y)
|
// all non-whitespace as a single class.
|
||||||
|
func nextWORDEnd(buf *core.Buffer, x, y int) (int, int) {
|
||||||
|
line := buf.Lines[y]
|
||||||
|
|
||||||
// Advance once to avoid being stuck on the current end
|
// Advance once to avoid being stuck on the current end
|
||||||
x++
|
x++
|
||||||
if x >= len(line) {
|
if x >= len(line) {
|
||||||
// At last line of file, pin cursor to end of file
|
// At last line of file, pin cursor to end of file
|
||||||
if y+1 >= m.LineCount() {
|
if y+1 >= buf.LineCount() {
|
||||||
return len(line) - 1, y
|
return len(line) - 1, y
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, move to next line
|
// Otherwise, move to next line
|
||||||
y++
|
y++
|
||||||
x = 0
|
x = 0
|
||||||
line = m.Line(y)
|
line = buf.Lines[y]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip whitespace and cross lines if needed
|
// Skip whitespace and cross lines if needed
|
||||||
@ -190,13 +203,13 @@ func nextWORDEnd(m action.Model, x, y int) (int, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If next line is the end of the file, exit now
|
// If next line is the end of the file, exit now
|
||||||
if y+1 >= m.LineCount() {
|
if y+1 >= buf.LineCount() {
|
||||||
return x, y
|
return x, y
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move to first char of next line
|
// Move to first char of next line
|
||||||
y++
|
y++
|
||||||
line = m.Line(y)
|
line = buf.Lines[y]
|
||||||
x = 0
|
x = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,8 +221,10 @@ func nextWORDEnd(m action.Model, x, y int) (int, int) {
|
|||||||
return x, y
|
return x, y
|
||||||
}
|
}
|
||||||
|
|
||||||
func prevWordStart(m action.Model, x, y int) (int, int) {
|
// prevWordStart: Finds the start of the previous word from position (x,y),
|
||||||
line := m.Line(y)
|
// moving backward through character classes.
|
||||||
|
func prevWordStart(buf *core.Buffer, x, y int) (int, int) {
|
||||||
|
line := buf.Lines[y]
|
||||||
|
|
||||||
// Back one to avoid being stuck on the current start
|
// Back one to avoid being stuck on the current start
|
||||||
x--
|
x--
|
||||||
@ -218,7 +233,7 @@ func prevWordStart(m action.Model, x, y int) (int, int) {
|
|||||||
return 0, 0 // beginning of file, stay put
|
return 0, 0 // beginning of file, stay put
|
||||||
}
|
}
|
||||||
y--
|
y--
|
||||||
line = m.Line(y)
|
line = buf.Lines[y]
|
||||||
x = len(line) - 1
|
x = len(line) - 1
|
||||||
if x < 0 {
|
if x < 0 {
|
||||||
return 0, y // landed on an empty line
|
return 0, y // landed on an empty line
|
||||||
@ -237,7 +252,7 @@ func prevWordStart(m action.Model, x, y int) (int, int) {
|
|||||||
return 0, 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
y--
|
y--
|
||||||
line = m.Line(y)
|
line = buf.Lines[y]
|
||||||
x = len(line) - 1
|
x = len(line) - 1
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
return 0, y // empty line acts as a word boundary
|
return 0, y // empty line acts as a word boundary
|
||||||
@ -263,19 +278,25 @@ type MoveForwardWord struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MoveForwardWord.Execute: Moves the cursor forward by Count words (w motion).
|
||||||
func (a MoveForwardWord) Execute(m action.Model) tea.Cmd {
|
func (a MoveForwardWord) Execute(m action.Model) tea.Cmd {
|
||||||
x := m.CursorX()
|
win := m.ActiveWindow()
|
||||||
y := m.CursorY()
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
x := win.Cursor.Col
|
||||||
|
y := win.Cursor.Line
|
||||||
for i := 0; i < a.Count; i++ {
|
for i := 0; i < a.Count; i++ {
|
||||||
x, y = nextWordStart(m, x, y)
|
x, y = nextWordStart(buf, x, y)
|
||||||
}
|
}
|
||||||
m.SetCursorX(x)
|
win.SetCursorCol(x)
|
||||||
m.SetCursorY(y)
|
win.SetCursorLine(y)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveForwardWord) Type() action.MotionType { return action.CharwiseExclusive }
|
// MoveForwardWord.Type: Returns CharwiseExclusive for word motion.
|
||||||
|
func (a MoveForwardWord) Type() core.MotionType { return core.CharwiseExclusive }
|
||||||
|
|
||||||
|
// MoveForwardWord.WithCount: Returns a new MoveForwardWord with the given count.
|
||||||
func (a MoveForwardWord) WithCount(n int) action.Action {
|
func (a MoveForwardWord) WithCount(n int) action.Action {
|
||||||
return MoveForwardWord{Count: n}
|
return MoveForwardWord{Count: n}
|
||||||
}
|
}
|
||||||
@ -285,19 +306,25 @@ type MoveForwardWORD struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MoveForwardWORD.Execute: Moves the cursor forward by Count WORDs (W motion).
|
||||||
func (a MoveForwardWORD) Execute(m action.Model) tea.Cmd {
|
func (a MoveForwardWORD) Execute(m action.Model) tea.Cmd {
|
||||||
x := m.CursorX()
|
win := m.ActiveWindow()
|
||||||
y := m.CursorY()
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
x := win.Cursor.Col
|
||||||
|
y := win.Cursor.Line
|
||||||
for i := 0; i < a.Count; i++ {
|
for i := 0; i < a.Count; i++ {
|
||||||
x, y = nextWORDStart(m, x, y)
|
x, y = nextWORDStart(buf, x, y)
|
||||||
}
|
}
|
||||||
m.SetCursorX(x)
|
win.SetCursorCol(x)
|
||||||
m.SetCursorY(y)
|
win.SetCursorLine(y)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveForwardWORD) Type() action.MotionType { return action.CharwiseExclusive }
|
// MoveForwardWORD.Type: Returns CharwiseExclusive for WORD motion.
|
||||||
|
func (a MoveForwardWORD) Type() core.MotionType { return core.CharwiseExclusive }
|
||||||
|
|
||||||
|
// MoveForwardWORD.WithCount: Returns a new MoveForwardWORD with the given count.
|
||||||
func (a MoveForwardWORD) WithCount(n int) action.Action {
|
func (a MoveForwardWORD) WithCount(n int) action.Action {
|
||||||
return MoveForwardWORD{Count: n}
|
return MoveForwardWORD{Count: n}
|
||||||
}
|
}
|
||||||
@ -307,19 +334,25 @@ type MoveForwardWordEnd struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MoveForwardWordEnd.Execute: Moves the cursor to the end of the Count-th word (e motion).
|
||||||
func (a MoveForwardWordEnd) Execute(m action.Model) tea.Cmd {
|
func (a MoveForwardWordEnd) Execute(m action.Model) tea.Cmd {
|
||||||
x := m.CursorX()
|
win := m.ActiveWindow()
|
||||||
y := m.CursorY()
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
x := win.Cursor.Col
|
||||||
|
y := win.Cursor.Line
|
||||||
for i := 0; i < a.Count; i++ {
|
for i := 0; i < a.Count; i++ {
|
||||||
x, y = nextWordEnd(m, x, y)
|
x, y = nextWordEnd(buf, x, y)
|
||||||
}
|
}
|
||||||
m.SetCursorX(x)
|
win.SetCursorCol(x)
|
||||||
m.SetCursorY(y)
|
win.SetCursorLine(y)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveForwardWordEnd) Type() action.MotionType { return action.CharwiseInclusive }
|
// MoveForwardWordEnd.Type: Returns CharwiseInclusive for word-end motion.
|
||||||
|
func (a MoveForwardWordEnd) Type() core.MotionType { return core.CharwiseInclusive }
|
||||||
|
|
||||||
|
// MoveForwardWordEnd.WithCount: Returns a new MoveForwardWordEnd with the given count.
|
||||||
func (a MoveForwardWordEnd) WithCount(n int) action.Action {
|
func (a MoveForwardWordEnd) WithCount(n int) action.Action {
|
||||||
return MoveForwardWordEnd{Count: n}
|
return MoveForwardWordEnd{Count: n}
|
||||||
}
|
}
|
||||||
@ -329,19 +362,25 @@ type MoveForwardWORDEnd struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MoveForwardWORDEnd.Execute: Moves the cursor to the end of the Count-th WORD (E motion).
|
||||||
func (a MoveForwardWORDEnd) Execute(m action.Model) tea.Cmd {
|
func (a MoveForwardWORDEnd) Execute(m action.Model) tea.Cmd {
|
||||||
x := m.CursorX()
|
win := m.ActiveWindow()
|
||||||
y := m.CursorY()
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
x := win.Cursor.Col
|
||||||
|
y := win.Cursor.Line
|
||||||
for i := 0; i < a.Count; i++ {
|
for i := 0; i < a.Count; i++ {
|
||||||
x, y = nextWORDEnd(m, x, y)
|
x, y = nextWORDEnd(buf, x, y)
|
||||||
}
|
}
|
||||||
m.SetCursorX(x)
|
win.SetCursorCol(x)
|
||||||
m.SetCursorY(y)
|
win.SetCursorLine(y)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveForwardWORDEnd) Type() action.MotionType { return action.CharwiseInclusive }
|
// MoveForwardWORDEnd.Type: Returns CharwiseInclusive for WORD-end motion.
|
||||||
|
func (a MoveForwardWORDEnd) Type() core.MotionType { return core.CharwiseInclusive }
|
||||||
|
|
||||||
|
// MoveForwardWORDEnd.WithCount: Returns a new MoveForwardWORDEnd with the given count.
|
||||||
func (a MoveForwardWORDEnd) WithCount(n int) action.Action {
|
func (a MoveForwardWORDEnd) WithCount(n int) action.Action {
|
||||||
return MoveForwardWORDEnd{Count: n}
|
return MoveForwardWORDEnd{Count: n}
|
||||||
}
|
}
|
||||||
@ -351,19 +390,25 @@ type MoveBackwardWord struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MoveBackwardWord.Execute: Moves the cursor backward by Count words (b motion).
|
||||||
func (a MoveBackwardWord) Execute(m action.Model) tea.Cmd {
|
func (a MoveBackwardWord) Execute(m action.Model) tea.Cmd {
|
||||||
x := m.CursorX()
|
win := m.ActiveWindow()
|
||||||
y := m.CursorY()
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
x := win.Cursor.Col
|
||||||
|
y := win.Cursor.Line
|
||||||
for i := 0; i < a.Count; i++ {
|
for i := 0; i < a.Count; i++ {
|
||||||
x, y = prevWordStart(m, x, y)
|
x, y = prevWordStart(buf, x, y)
|
||||||
}
|
}
|
||||||
m.SetCursorX(x)
|
win.SetCursorCol(x)
|
||||||
m.SetCursorY(y)
|
win.SetCursorLine(y)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MoveBackwardWord) Type() action.MotionType { return action.CharwiseExclusive }
|
// 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 {
|
func (a MoveBackwardWord) WithCount(n int) action.Action {
|
||||||
return MoveBackwardWord{Count: n}
|
return MoveBackwardWord{Count: n}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,34 +2,37 @@ package operator
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Implements Operator (c)
|
// ChangeOperator implements Operator (c) - changes (deletes and enters insert mode) text.
|
||||||
type ChangeOperator struct{}
|
type ChangeOperator struct{}
|
||||||
|
|
||||||
func (o ChangeOperator) Operate(m action.Model, start, end action.Position, mtype action.MotionType) tea.Cmd {
|
// ChangeOperator.Operate: Changes text based on the current mode and motion type.
|
||||||
|
func (o ChangeOperator) Operate(m action.Model, start, end core.Position, mtype core.MotionType) tea.Cmd {
|
||||||
switch m.Mode() {
|
switch m.Mode() {
|
||||||
case action.VisualMode:
|
case core.VisualMode:
|
||||||
changeCharSelection(m, start, end)
|
changeCharSelection(m, start, end)
|
||||||
case action.VisualLineMode:
|
case core.VisualLineMode:
|
||||||
changeLineSelection(m, start, end)
|
changeLineSelection(m, start, end)
|
||||||
case action.VisualBlockMode:
|
case core.VisualBlockMode:
|
||||||
changeBlockSelection(m, start, end)
|
changeBlockSelection(m, start, end)
|
||||||
case action.NormalMode:
|
case core.NormalMode:
|
||||||
changeNormalMode(m, start, end, mtype)
|
changeNormalMode(m, start, end, mtype)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func changeNormalMode(m action.Model, start, end action.Position, mtype action.MotionType) {
|
// changeNormalMode: Changes text in normal mode based on motion type.
|
||||||
|
func changeNormalMode(m action.Model, start, end core.Position, mtype core.MotionType) {
|
||||||
// Normalize so start is always before or equal to end
|
// Normalize so start is always before or equal to end
|
||||||
if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
|
if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
|
||||||
start, end = end, start
|
start, end = end, start
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linewise motions (j, k, G, gg) always operate on whole lines
|
// Linewise motions (j, k, G, gg) always operate on whole lines
|
||||||
if mtype == action.Linewise {
|
if mtype == core.Linewise {
|
||||||
changeLineSelection(m, start, end)
|
changeLineSelection(m, start, end)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -37,18 +40,18 @@ func changeNormalMode(m action.Model, start, end action.Position, mtype action.M
|
|||||||
// Charwise motions on same line
|
// Charwise motions on same line
|
||||||
if start.Line == end.Line {
|
if start.Line == end.Line {
|
||||||
// No movement = nothing to change
|
// No movement = nothing to change
|
||||||
if start.Col == end.Col && mtype == action.CharwiseExclusive {
|
if start.Col == end.Col && mtype == core.CharwiseExclusive {
|
||||||
m.SetMode(action.InsertMode)
|
m.SetMode(core.InsertMode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Exclusive motion: end position not included, so back up one
|
// Exclusive motion: end position not included, so back up one
|
||||||
if mtype == action.CharwiseExclusive {
|
if mtype == core.CharwiseExclusive {
|
||||||
end.Col--
|
end.Col--
|
||||||
}
|
}
|
||||||
if end.Col >= start.Col {
|
if end.Col >= start.Col {
|
||||||
changeCharSelection(m, start, end)
|
changeCharSelection(m, start, end)
|
||||||
} else {
|
} else {
|
||||||
m.SetMode(action.InsertMode)
|
m.SetMode(core.InsertMode)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -57,22 +60,26 @@ func changeNormalMode(m action.Model, start, end action.Position, mtype action.M
|
|||||||
changeCharSelection(m, start, end)
|
changeCharSelection(m, start, end)
|
||||||
}
|
}
|
||||||
|
|
||||||
func changeCharSelection(m action.Model, start, end action.Position) {
|
// changeCharSelection: Changes a character-wise selection and enters insert mode.
|
||||||
|
func changeCharSelection(m action.Model, start, end core.Position) {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
var deletedText string
|
var deletedText string
|
||||||
|
|
||||||
if start.Line == end.Line {
|
if start.Line == end.Line {
|
||||||
line := m.Line(start.Line)
|
line := buf.Lines[start.Line]
|
||||||
endCol := min(end.Col+1, len(line))
|
endCol := min(end.Col+1, len(line))
|
||||||
deletedText = line[start.Col:endCol]
|
deletedText = line[start.Col:endCol]
|
||||||
m.SetLine(start.Line, line[:start.Col]+line[endCol:])
|
buf.SetLine(start.Line, line[:start.Col]+line[endCol:])
|
||||||
} else {
|
} else {
|
||||||
startLine := m.Line(start.Line)
|
startLine := buf.Lines[start.Line]
|
||||||
endLine := m.Line(end.Line)
|
endLine := buf.Lines[end.Line]
|
||||||
|
|
||||||
// Extract deleted text
|
// Extract deleted text
|
||||||
deletedText = startLine[start.Col:] + "\n"
|
deletedText = startLine[start.Col:] + "\n"
|
||||||
for y := start.Line + 1; y < end.Line; y++ {
|
for y := start.Line + 1; y < end.Line; y++ {
|
||||||
deletedText += m.Line(y) + "\n"
|
deletedText += buf.Lines[y] + "\n"
|
||||||
}
|
}
|
||||||
endCol := min(end.Col+1, len(endLine))
|
endCol := min(end.Col+1, len(endLine))
|
||||||
deletedText += endLine[:endCol]
|
deletedText += endLine[:endCol]
|
||||||
@ -85,89 +92,98 @@ func changeCharSelection(m action.Model, start, end action.Position) {
|
|||||||
|
|
||||||
// Delete from end back to start to preserve indices
|
// Delete from end back to start to preserve indices
|
||||||
for i := end.Line; i >= start.Line; i-- {
|
for i := end.Line; i >= start.Line; i-- {
|
||||||
m.DeleteLine(i)
|
buf.DeleteLine(i)
|
||||||
}
|
}
|
||||||
m.InsertLine(start.Line, prefix+suffix)
|
buf.InsertLine(start.Line, prefix+suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetCursorY(start.Line)
|
win.SetCursorLine(start.Line)
|
||||||
m.SetCursorX(start.Col)
|
win.SetCursorCol(start.Col)
|
||||||
m.ClampCursorX()
|
m.SetMode(core.InsertMode)
|
||||||
m.SetMode(action.InsertMode)
|
|
||||||
|
|
||||||
// Update register with deleted text
|
// Update register with deleted text
|
||||||
m.UpdateDefaultRegister(action.CharwiseRegister, []string{deletedText})
|
m.UpdateDefaultRegister(core.CharwiseRegister, []string{deletedText})
|
||||||
}
|
}
|
||||||
|
|
||||||
func changeLineSelection(m action.Model, start, end action.Position) {
|
// changeLineSelection: Changes entire lines and enters insert mode.
|
||||||
|
func changeLineSelection(m action.Model, start, end core.Position) {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
var lines []string
|
var lines []string
|
||||||
|
|
||||||
for i := end.Line; i >= start.Line; i-- {
|
for i := end.Line; i >= start.Line; i-- {
|
||||||
lines = append([]string{m.Line(i)}, lines...)
|
lines = append([]string{buf.Lines[i]}, lines...)
|
||||||
m.DeleteLine(i)
|
buf.DeleteLine(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert an empty line for editing
|
// Insert an empty line for editing
|
||||||
insertY := min(start.Line, m.LineCount())
|
insertY := min(start.Line, buf.LineCount())
|
||||||
m.InsertLine(insertY, "")
|
buf.InsertLine(insertY, "")
|
||||||
|
|
||||||
m.SetCursorY(insertY)
|
win.SetCursorLine(insertY)
|
||||||
m.SetCursorX(0)
|
win.SetCursorCol(0)
|
||||||
m.SetMode(action.InsertMode)
|
m.SetMode(core.InsertMode)
|
||||||
|
|
||||||
// Update registers
|
// Update registers
|
||||||
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
|
m.UpdateDefaultRegister(core.LinewiseRegister, lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
func changeBlockSelection(m action.Model, start, end action.Position) {
|
// changeBlockSelection: Changes a rectangular block selection and enters insert mode.
|
||||||
|
func changeBlockSelection(m action.Model, start, end core.Position) {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
startCol := min(start.Col, end.Col)
|
startCol := min(start.Col, end.Col)
|
||||||
endCol := max(start.Col, end.Col)
|
endCol := max(start.Col, end.Col)
|
||||||
|
|
||||||
for y := start.Line; y <= end.Line; y++ {
|
for y := start.Line; y <= end.Line; y++ {
|
||||||
line := m.Line(y)
|
line := buf.Lines[y]
|
||||||
if startCol >= len(line) {
|
if startCol >= len(line) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ec := min(endCol+1, len(line))
|
ec := min(endCol+1, len(line))
|
||||||
m.SetLine(y, line[:startCol]+line[ec:])
|
buf.SetLine(y, line[:startCol]+line[ec:])
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetCursorY(start.Line)
|
win.SetCursorLine(start.Line)
|
||||||
m.SetCursorX(startCol)
|
win.SetCursorCol(startCol)
|
||||||
m.ClampCursorX()
|
m.SetMode(core.InsertMode)
|
||||||
m.SetMode(action.InsertMode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify ChangeOperator implements DoublePresser
|
// Verify ChangeOperator implements DoublePresser
|
||||||
var _ action.DoublePresser = ChangeOperator{}
|
var _ action.DoublePresser = ChangeOperator{}
|
||||||
|
|
||||||
// Double press handles cc - change the entire line
|
// ChangeOperator.DoublePress: Handles cc - changes Count entire lines.
|
||||||
func (o ChangeOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
func (o ChangeOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
||||||
startY := m.CursorY()
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
startY := win.Cursor.Line
|
||||||
|
|
||||||
// If we have a higher value than lines remaining, we can only run so many times
|
// If we have a higher value than lines remaining, we can only run so many times
|
||||||
opCount := min(count, m.LineCount()-startY)
|
opCount := min(count, buf.LineCount()-startY)
|
||||||
|
|
||||||
var lines []string
|
var lines []string
|
||||||
|
|
||||||
// Collect lines to delete (always delete at startY since lines shift up)
|
// Collect lines to delete (always delete at startY since lines shift up)
|
||||||
for range opCount {
|
for range opCount {
|
||||||
lines = append(lines, m.Line(startY))
|
lines = append(lines, buf.Lines[startY])
|
||||||
m.DeleteLine(startY)
|
buf.DeleteLine(startY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put deleted lines in register
|
// Put deleted lines in register
|
||||||
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
|
m.UpdateDefaultRegister(core.LinewiseRegister, lines)
|
||||||
|
|
||||||
// Insert empty line at the original position for editing
|
// Insert empty line at the original position for editing
|
||||||
// If we deleted everything, startY might be past end, so clamp it
|
// If we deleted everything, startY might be past end, so clamp it
|
||||||
insertY := min(startY, m.LineCount())
|
insertY := min(startY, buf.LineCount())
|
||||||
m.InsertLine(insertY, "")
|
buf.InsertLine(insertY, "")
|
||||||
|
|
||||||
// Position cursor on the new empty line
|
// Position cursor on the new empty line
|
||||||
m.SetCursorY(insertY)
|
win.SetCursorLine(insertY)
|
||||||
m.SetCursorX(0)
|
win.SetCursorCol(0)
|
||||||
m.SetMode(action.InsertMode)
|
m.SetMode(core.InsertMode)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,21 +2,23 @@ package operator
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Implements Operator (d)
|
// DeleteOperator implements Operator (d) - deletes text in various modes.
|
||||||
type DeleteOperator struct{}
|
type DeleteOperator struct{}
|
||||||
|
|
||||||
func (o DeleteOperator) Operate(m action.Model, start, end action.Position, mtype action.MotionType) tea.Cmd {
|
// DeleteOperator.Operate: Deletes text based on the current mode and motion type.
|
||||||
|
func (o DeleteOperator) Operate(m action.Model, start, end core.Position, mtype core.MotionType) tea.Cmd {
|
||||||
switch m.Mode() {
|
switch m.Mode() {
|
||||||
case action.VisualMode:
|
case core.VisualMode:
|
||||||
deleteCharSelection(m, start, end)
|
deleteCharSelection(m, start, end)
|
||||||
case action.VisualLineMode:
|
case core.VisualLineMode:
|
||||||
deleteLineSelection(m, start, end)
|
deleteLineSelection(m, start, end)
|
||||||
case action.VisualBlockMode:
|
case core.VisualBlockMode:
|
||||||
deleteBlockSelection(m, start, end)
|
deleteBlockSelection(m, start, end)
|
||||||
case action.NormalMode:
|
case core.NormalMode:
|
||||||
deleteNormalMode(m, start, end, mtype)
|
deleteNormalMode(m, start, end, mtype)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -25,45 +27,48 @@ func (o DeleteOperator) Operate(m action.Model, start, end action.Position, mtyp
|
|||||||
// Verify DeleteOperator implements DoublePresser
|
// Verify DeleteOperator implements DoublePresser
|
||||||
var _ action.DoublePresser = DeleteOperator{}
|
var _ action.DoublePresser = DeleteOperator{}
|
||||||
|
|
||||||
// Double press handles dd - delete the entire line
|
// DeleteOperator.DoublePress: Handles dd - deletes Count entire lines.
|
||||||
func (o DeleteOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
func (o DeleteOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
// If we have a higher value than lines remaining, we can only run so many times
|
// If we have a higher value than lines remaining, we can only run so many times
|
||||||
opCount := min(count, m.LineCount()-m.CursorY())
|
opCount := min(count, buf.LineCount()-win.Cursor.Line)
|
||||||
|
|
||||||
var lines []string
|
var lines []string
|
||||||
|
|
||||||
for range opCount {
|
for range opCount {
|
||||||
y := m.CursorY()
|
y := win.Cursor.Line
|
||||||
lines = append(lines, m.Line(y))
|
lines = append(lines, buf.Lines[y])
|
||||||
|
|
||||||
m.DeleteLine(y)
|
buf.DeleteLine(y)
|
||||||
|
|
||||||
if m.LineCount() == 0 {
|
if buf.LineCount() == 0 {
|
||||||
m.InsertLine(0, "")
|
buf.InsertLine(0, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
if y >= m.LineCount() {
|
if y >= buf.LineCount() {
|
||||||
y = m.LineCount() - 1
|
y = buf.LineCount() - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetCursorY(y)
|
win.SetCursorLine(y)
|
||||||
m.ClampCursorX()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put her in the register!
|
// Put her in the register!
|
||||||
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
|
m.UpdateDefaultRegister(core.LinewiseRegister, lines)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteNormalMode(m action.Model, start, end action.Position, mtype action.MotionType) {
|
// deleteNormalMode: Deletes text in normal mode based on motion type.
|
||||||
|
func deleteNormalMode(m action.Model, start, end core.Position, mtype core.MotionType) {
|
||||||
// Normalize so start is always before or equal to end
|
// Normalize so start is always before or equal to end
|
||||||
if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
|
if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
|
||||||
start, end = end, start
|
start, end = end, start
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linewise motions (j, k, G, gg) always operate on whole lines
|
// Linewise motions (j, k, G, gg) always operate on whole lines
|
||||||
if mtype == action.Linewise {
|
if mtype == core.Linewise {
|
||||||
deleteLineSelection(m, start, end)
|
deleteLineSelection(m, start, end)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -71,11 +76,11 @@ func deleteNormalMode(m action.Model, start, end action.Position, mtype action.M
|
|||||||
// Charwise motions on same line
|
// Charwise motions on same line
|
||||||
if start.Line == end.Line {
|
if start.Line == end.Line {
|
||||||
// No movement = nothing to delete
|
// No movement = nothing to delete
|
||||||
if start.Col == end.Col && mtype == action.CharwiseExclusive {
|
if start.Col == end.Col && mtype == core.CharwiseExclusive {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Exclusive motion: end position not included, so back up one
|
// Exclusive motion: end position not included, so back up one
|
||||||
if mtype == action.CharwiseExclusive {
|
if mtype == core.CharwiseExclusive {
|
||||||
end.Col--
|
end.Col--
|
||||||
}
|
}
|
||||||
if end.Col >= start.Col {
|
if end.Col >= start.Col {
|
||||||
@ -88,14 +93,18 @@ func deleteNormalMode(m action.Model, start, end action.Position, mtype action.M
|
|||||||
deleteCharSelection(m, start, end)
|
deleteCharSelection(m, start, end)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteCharSelection(m action.Model, start, end action.Position) {
|
// deleteCharSelection: Deletes a character-wise selection.
|
||||||
|
func deleteCharSelection(m action.Model, start, end core.Position) {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
if start.Line == end.Line {
|
if start.Line == end.Line {
|
||||||
line := m.Line(start.Line)
|
line := buf.Lines[start.Line]
|
||||||
endCol := min(end.Col+1, len(line))
|
endCol := min(end.Col+1, len(line))
|
||||||
m.SetLine(start.Line, line[:start.Col]+line[endCol:])
|
buf.SetLine(start.Line, line[:start.Col]+line[endCol:])
|
||||||
} else {
|
} else {
|
||||||
startLine := m.Line(start.Line)
|
startLine := buf.Lines[start.Line]
|
||||||
endLine := m.Line(end.Line)
|
endLine := buf.Lines[end.Line]
|
||||||
|
|
||||||
prefix := startLine[:start.Col]
|
prefix := startLine[:start.Col]
|
||||||
suffix := ""
|
suffix := ""
|
||||||
@ -105,54 +114,59 @@ func deleteCharSelection(m action.Model, start, end action.Position) {
|
|||||||
|
|
||||||
// Delete from end back to start to preserve indices
|
// Delete from end back to start to preserve indices
|
||||||
for i := end.Line; i >= start.Line; i-- {
|
for i := end.Line; i >= start.Line; i-- {
|
||||||
m.DeleteLine(i)
|
buf.DeleteLine(i)
|
||||||
}
|
}
|
||||||
m.InsertLine(start.Line, prefix+suffix)
|
buf.InsertLine(start.Line, prefix+suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetCursorY(start.Line)
|
win.SetCursorLine(start.Line)
|
||||||
m.SetCursorX(start.Col)
|
win.SetCursorCol(start.Col)
|
||||||
m.ClampCursorX()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteLineSelection(m action.Model, start, end action.Position) {
|
// deleteLineSelection: Deletes entire lines in a selection range.
|
||||||
|
func deleteLineSelection(m action.Model, start, end core.Position) {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
var lines []string
|
var lines []string
|
||||||
|
|
||||||
for i := end.Line; i >= start.Line; i-- {
|
for i := end.Line; i >= start.Line; i-- {
|
||||||
lines = append(lines, m.Line(i))
|
lines = append(lines, buf.Lines[i])
|
||||||
m.DeleteLine(i)
|
buf.DeleteLine(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.LineCount() == 0 {
|
if buf.LineCount() == 0 {
|
||||||
m.InsertLine(0, "")
|
buf.InsertLine(0, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
y := start.Line
|
y := start.Line
|
||||||
if y >= m.LineCount() {
|
if y >= buf.LineCount() {
|
||||||
y = m.LineCount() - 1
|
y = buf.LineCount() - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetCursorY(y)
|
win.SetCursorLine(y)
|
||||||
m.ClampCursorX()
|
|
||||||
|
|
||||||
// Update registers
|
// Update registers
|
||||||
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
|
m.UpdateDefaultRegister(core.LinewiseRegister, lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteBlockSelection(m action.Model, start, end action.Position) {
|
// deleteBlockSelection: Deletes a rectangular block selection.
|
||||||
|
func deleteBlockSelection(m action.Model, start, end core.Position) {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
startCol := min(start.Col, end.Col)
|
startCol := min(start.Col, end.Col)
|
||||||
endCol := max(start.Col, end.Col)
|
endCol := max(start.Col, end.Col)
|
||||||
|
|
||||||
for y := start.Line; y <= end.Line; y++ {
|
for y := start.Line; y <= end.Line; y++ {
|
||||||
line := m.Line(y)
|
line := buf.Lines[y]
|
||||||
if startCol >= len(line) {
|
if startCol >= len(line) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ec := min(endCol+1, len(line))
|
ec := min(endCol+1, len(line))
|
||||||
m.SetLine(y, line[:startCol]+line[ec:])
|
buf.SetLine(y, line[:startCol]+line[ec:])
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetCursorY(start.Line)
|
win.SetCursorLine(start.Line)
|
||||||
m.SetCursorX(startCol)
|
win.SetCursorCol(startCol)
|
||||||
m.ClampCursorX()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,53 +4,64 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Implements Operator (y)
|
// YankOperator implements Operator (y) - copies text to register in various modes.
|
||||||
type YankOperator struct{}
|
type YankOperator struct{}
|
||||||
|
|
||||||
func (o YankOperator) Operate(m action.Model, start, end action.Position, mtype action.MotionType) tea.Cmd {
|
// YankOperator.Operate: Copies text to register based on the current mode and motion type.
|
||||||
|
func (o YankOperator) Operate(m action.Model, start, end core.Position, mtype core.MotionType) tea.Cmd {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
|
||||||
switch m.Mode() {
|
switch m.Mode() {
|
||||||
case action.VisualMode:
|
case core.VisualMode:
|
||||||
yankVisualMode(m, start, end)
|
yankVisualMode(m, start, end)
|
||||||
case action.VisualLineMode:
|
case core.VisualLineMode:
|
||||||
yankVisualLineMode(m, start, end)
|
yankVisualLineMode(m, start, end)
|
||||||
case action.VisualBlockMode:
|
case core.VisualBlockMode:
|
||||||
yankVisualBlockMode(m, start, end)
|
yankVisualBlockMode(m, start, end)
|
||||||
case action.NormalMode:
|
case core.NormalMode:
|
||||||
yankNormalMode(m, start, end, mtype)
|
yankNormalMode(m, start, end, mtype)
|
||||||
default:
|
default:
|
||||||
m.SetCommandError(fmt.Errorf("'y' operator not yet implemented."))
|
m.SetCommandError(fmt.Errorf("'y' operator not yet implemented."))
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetCursorX(start.Col)
|
win.SetCursorCol(start.Col)
|
||||||
m.SetCursorY(start.Line)
|
win.SetCursorLine(start.Line)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify YankOperator implements DoublePresser
|
// Verify YankOperator implements DoublePresser
|
||||||
var _ action.DoublePresser = YankOperator{}
|
var _ action.DoublePresser = YankOperator{}
|
||||||
|
|
||||||
|
// YankOperator.DoublePress: Handles yy - copies Count entire lines to register.
|
||||||
func (o YankOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
func (o YankOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
||||||
y := m.CursorY()
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
y := win.Cursor.Line
|
||||||
|
|
||||||
// If we have a higher value than lines remaining, we can only run so many times
|
// If we have a higher value than lines remaining, we can only run so many times
|
||||||
opCount := min(count, m.LineCount()-y)
|
opCount := min(count, buf.LineCount()-y)
|
||||||
|
|
||||||
var lines []string
|
var lines []string
|
||||||
|
|
||||||
for i := range opCount {
|
for i := range opCount {
|
||||||
lines = append(lines, m.Line(y+i))
|
lines = append(lines, buf.Lines[y+i])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put her in the register!
|
// Put her in the register!
|
||||||
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
|
m.UpdateDefaultRegister(core.LinewiseRegister, lines)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func yankNormalMode(m action.Model, start, end action.Position, mtype action.MotionType) {
|
// yankNormalMode: Copies text to register in normal mode based on motion type.
|
||||||
|
func yankNormalMode(m action.Model, start, end core.Position, mtype core.MotionType) {
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case mtype.IsCharwise():
|
case mtype.IsCharwise():
|
||||||
// This shouldn't happen
|
// This shouldn't happen
|
||||||
@ -59,22 +70,22 @@ func yankNormalMode(m action.Model, start, end action.Position, mtype action.Mot
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
line := m.Line(start.Line)
|
line := buf.Lines[start.Line]
|
||||||
|
|
||||||
startX := min(start.Col, end.Col)
|
startX := min(start.Col, end.Col)
|
||||||
endX := max(start.Col, end.Col)
|
endX := max(start.Col, end.Col)
|
||||||
|
|
||||||
// Inclusive motions include the end character
|
// Inclusive motions include the end character
|
||||||
if mtype == action.CharwiseInclusive {
|
if mtype == core.CharwiseInclusive {
|
||||||
endX++
|
endX++
|
||||||
}
|
}
|
||||||
|
|
||||||
endX = min(endX, len(line)) // Catch overflow
|
endX = min(endX, len(line)) // Catch overflow
|
||||||
|
|
||||||
cnt := line[startX:endX]
|
cnt := line[startX:endX]
|
||||||
m.UpdateDefaultRegister(action.CharwiseRegister, []string{cnt})
|
m.UpdateDefaultRegister(core.CharwiseRegister, []string{cnt})
|
||||||
|
|
||||||
case mtype == action.Linewise:
|
case mtype == core.Linewise:
|
||||||
// This shouldn't happen
|
// This shouldn't happen
|
||||||
if start.Col != end.Col {
|
if start.Col != end.Col {
|
||||||
m.SetCommandError(fmt.Errorf("Start column and end column must match for linewise yank operations."))
|
m.SetCommandError(fmt.Errorf("Start column and end column must match for linewise yank operations."))
|
||||||
@ -85,12 +96,15 @@ func yankNormalMode(m action.Model, start, end action.Position, mtype action.Mot
|
|||||||
startY := min(start.Line, end.Line)
|
startY := min(start.Line, end.Line)
|
||||||
endY := max(start.Line, end.Line)
|
endY := max(start.Line, end.Line)
|
||||||
|
|
||||||
cnt := m.Lines()[startY : endY+1]
|
cnt := buf.Lines[startY : endY+1]
|
||||||
m.UpdateDefaultRegister(action.LinewiseRegister, cnt)
|
m.UpdateDefaultRegister(core.LinewiseRegister, cnt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func yankVisualMode(m action.Model, start, end action.Position) {
|
// yankVisualMode: Copies character-wise visual selection to register.
|
||||||
|
func yankVisualMode(m action.Model, start, end core.Position) {
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
// Normalize so start is before end
|
// Normalize so start is before end
|
||||||
if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
|
if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
|
||||||
start, end = end, start
|
start, end = end, start
|
||||||
@ -98,11 +112,11 @@ func yankVisualMode(m action.Model, start, end action.Position) {
|
|||||||
|
|
||||||
// Single line selection
|
// Single line selection
|
||||||
if start.Line == end.Line {
|
if start.Line == end.Line {
|
||||||
line := m.Line(start.Line)
|
line := buf.Lines[start.Line]
|
||||||
endCol := min(end.Col+1, len(line)) // +1 because visual selection is inclusive
|
endCol := min(end.Col+1, len(line)) // +1 because visual selection is inclusive
|
||||||
startCol := min(start.Col, len(line))
|
startCol := min(start.Col, len(line))
|
||||||
cnt := line[startCol:endCol]
|
cnt := line[startCol:endCol]
|
||||||
m.UpdateDefaultRegister(action.CharwiseRegister, []string{cnt})
|
m.UpdateDefaultRegister(core.CharwiseRegister, []string{cnt})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,24 +124,27 @@ func yankVisualMode(m action.Model, start, end action.Position) {
|
|||||||
var content []string
|
var content []string
|
||||||
|
|
||||||
// First line: from start.Col to end of line
|
// First line: from start.Col to end of line
|
||||||
firstLine := m.Line(start.Line)
|
firstLine := buf.Lines[start.Line]
|
||||||
startCol := min(start.Col, len(firstLine))
|
startCol := min(start.Col, len(firstLine))
|
||||||
content = append(content, firstLine[startCol:])
|
content = append(content, firstLine[startCol:])
|
||||||
|
|
||||||
// Middle lines: entire lines
|
// Middle lines: entire lines
|
||||||
for y := start.Line + 1; y < end.Line; y++ {
|
for y := start.Line + 1; y < end.Line; y++ {
|
||||||
content = append(content, m.Line(y))
|
content = append(content, buf.Lines[y])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last line: from beginning to end.Col (inclusive)
|
// Last line: from beginning to end.Col (inclusive)
|
||||||
lastLine := m.Line(end.Line)
|
lastLine := buf.Lines[end.Line]
|
||||||
endCol := min(end.Col+1, len(lastLine))
|
endCol := min(end.Col+1, len(lastLine))
|
||||||
content = append(content, lastLine[:endCol])
|
content = append(content, lastLine[:endCol])
|
||||||
|
|
||||||
m.UpdateDefaultRegister(action.CharwiseRegister, content)
|
m.UpdateDefaultRegister(core.CharwiseRegister, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func yankVisualLineMode(m action.Model, start, end action.Position) {
|
// yankVisualLineMode: Copies line-wise visual selection to register.
|
||||||
|
func yankVisualLineMode(m action.Model, start, end core.Position) {
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
// This shouldn't happen
|
// This shouldn't happen
|
||||||
if start.Col != end.Col {
|
if start.Col != end.Col {
|
||||||
m.SetCommandError(fmt.Errorf("Start column and end column must match for linewise yank operations."))
|
m.SetCommandError(fmt.Errorf("Start column and end column must match for linewise yank operations."))
|
||||||
@ -138,12 +155,15 @@ func yankVisualLineMode(m action.Model, start, end action.Position) {
|
|||||||
startY := min(start.Line, end.Line)
|
startY := min(start.Line, end.Line)
|
||||||
endY := max(start.Line, end.Line)
|
endY := max(start.Line, end.Line)
|
||||||
|
|
||||||
cnt := m.Lines()[startY : endY+1]
|
cnt := buf.Lines[startY : endY+1]
|
||||||
m.UpdateDefaultRegister(action.LinewiseRegister, cnt)
|
m.UpdateDefaultRegister(core.LinewiseRegister, cnt)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func yankVisualBlockMode(m action.Model, start, end action.Position) {
|
// yankVisualBlockMode: Copies block-wise visual selection to register.
|
||||||
|
func yankVisualBlockMode(m action.Model, start, end core.Position) {
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
// Normalize so startY <= endY and startX <= endX
|
// Normalize so startY <= endY and startX <= endX
|
||||||
startY := min(start.Line, end.Line)
|
startY := min(start.Line, end.Line)
|
||||||
endY := max(start.Line, end.Line)
|
endY := max(start.Line, end.Line)
|
||||||
@ -153,7 +173,7 @@ func yankVisualBlockMode(m action.Model, start, end action.Position) {
|
|||||||
var content []string
|
var content []string
|
||||||
|
|
||||||
for y := startY; y <= endY; y++ {
|
for y := startY; y <= endY; y++ {
|
||||||
line := m.Line(y)
|
line := buf.Lines[y]
|
||||||
|
|
||||||
// Handle lines shorter than the block selection
|
// Handle lines shorter than the block selection
|
||||||
if startX >= len(line) {
|
if startX >= len(line) {
|
||||||
@ -165,5 +185,5 @@ func yankVisualBlockMode(m action.Model, start, end action.Position) {
|
|||||||
content = append(content, line[startX:lineEndX])
|
content = append(content, line[startX:lineEndX])
|
||||||
}
|
}
|
||||||
|
|
||||||
m.UpdateDefaultRegister(action.BlockwiseRegister, content)
|
m.UpdateDefaultRegister(core.BlockwiseRegister, content)
|
||||||
}
|
}
|
||||||
|
|||||||
120
internal/program/program_builder.go
Normal file
120
internal/program/program_builder.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package program
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/editor"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProgramBuilder struct {
|
||||||
|
model *editor.Model
|
||||||
|
opts []tea.ProgramOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProgramBuilder() *ProgramBuilder {
|
||||||
|
return &ProgramBuilder{
|
||||||
|
model: &editor.Model{},
|
||||||
|
opts: []tea.ProgramOption{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProgramBuilder.FileProgram: Sets the internal state of the builder to the required
|
||||||
|
// state to start the program (editor) with a filename. This is what will happen when
|
||||||
|
// a user runs 'gim <filename>'.
|
||||||
|
func (p *ProgramBuilder) FileProgram(filename string) *ProgramBuilder {
|
||||||
|
// Only difference: open the file
|
||||||
|
ext := filepath.Ext(filename)
|
||||||
|
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
notFound := errors.Is(err, os.ErrNotExist)
|
||||||
|
if err != nil && !notFound {
|
||||||
|
// TODO: Handle this
|
||||||
|
panic(fmt.Errorf("Failed to find file: %w", err))
|
||||||
|
}
|
||||||
|
if file != nil {
|
||||||
|
defer file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := core.NewBufferBuilder().
|
||||||
|
WithType(core.FileBuffer).
|
||||||
|
WithFilename(filename).
|
||||||
|
WithFiletype(ext).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win := core.NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithOptions(core.NewDefaultWinOptions()).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
p.model = editor.NewModelBuilder().
|
||||||
|
AddBuffer(&buf).
|
||||||
|
AddWindow(&win).
|
||||||
|
WithActiveWindowId(win.Id).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
// If we did not find the file, all we need to do is set the filename and type
|
||||||
|
if notFound {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we have to create everything, then read the file (since we need settings)
|
||||||
|
// COPIED FROM `internal/command/handlers.go`
|
||||||
|
var lines []string
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
line = strings.TrimSuffix(line, "\r")
|
||||||
|
|
||||||
|
// BUG: This is bad, we don't want to this, but we have to
|
||||||
|
cleaned := strings.ReplaceAll(line, "\t", strings.Repeat(" ", p.model.Settings().TabStop))
|
||||||
|
lines = append(lines, cleaned)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only setting lines if we found some
|
||||||
|
if len(lines) > 0 {
|
||||||
|
p.model.ActiveBuffer().SetLines(lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProgramBuilder.EmptyProgram: Sets the internal state of the builder to the required
|
||||||
|
// state to start the program (editor) in the current directory. This is what will
|
||||||
|
// happen when a user runs 'gim'.
|
||||||
|
func (p *ProgramBuilder) EmptyProgram() *ProgramBuilder {
|
||||||
|
buf := core.NewBufferBuilder().
|
||||||
|
ReadOnly().
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win := core.NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithOptions(core.NewDefaultWinOptions()).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
p.model = editor.NewModelBuilder().
|
||||||
|
AddBuffer(&buf).
|
||||||
|
AddWindow(&win).
|
||||||
|
WithActiveWindowId(win.Id).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProgramBuilder.WithOpt: Add an option to the list of options that will be used when
|
||||||
|
// the program is built.
|
||||||
|
func (p *ProgramBuilder) WithOpt(opt tea.ProgramOption) *ProgramBuilder {
|
||||||
|
p.opts = append(p.opts, opt)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProgramBuilder.Build: Build and return the configured tea.Program instance.
|
||||||
|
func (p *ProgramBuilder) Build() *tea.Program {
|
||||||
|
return tea.NewProgram(p.model, p.opts...)
|
||||||
|
}
|
||||||
1066
internal/program/program_builder_test.go
Normal file
1066
internal/program/program_builder_test.go
Normal file
File diff suppressed because it is too large
Load Diff
75
internal/style/style.go
Normal file
75
internal/style/style.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package style
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Styles holds all the visual styling for the editor.
|
||||||
|
type Styles struct {
|
||||||
|
// Cursor styles by mode
|
||||||
|
CursorNormal lipgloss.Style
|
||||||
|
CursorInsert lipgloss.Style
|
||||||
|
CursorCommand lipgloss.Style
|
||||||
|
|
||||||
|
// Gutter (line numbers)
|
||||||
|
Gutter lipgloss.Style
|
||||||
|
GutterCurrentLine lipgloss.Style
|
||||||
|
|
||||||
|
// Visual mode
|
||||||
|
VisualHighlight lipgloss.Style
|
||||||
|
VisualAnchor lipgloss.Style // debugging
|
||||||
|
|
||||||
|
// Status bar
|
||||||
|
StatusBar lipgloss.Style
|
||||||
|
StatusBarActive lipgloss.Style
|
||||||
|
|
||||||
|
// Command line
|
||||||
|
CommandError lipgloss.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultStyles returns the default editor color scheme.
|
||||||
|
func DefaultStyles() Styles {
|
||||||
|
return Styles{
|
||||||
|
CursorNormal: lipgloss.NewStyle().Reverse(true),
|
||||||
|
CursorInsert: lipgloss.NewStyle().Underline(true),
|
||||||
|
CursorCommand: lipgloss.NewStyle().Reverse(true),
|
||||||
|
|
||||||
|
Gutter: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color("236")).
|
||||||
|
Foreground(lipgloss.Color("243")),
|
||||||
|
|
||||||
|
GutterCurrentLine: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color("236")).
|
||||||
|
Foreground(lipgloss.Color("#d69d00")),
|
||||||
|
|
||||||
|
VisualHighlight: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color("#7a6a00")),
|
||||||
|
|
||||||
|
VisualAnchor: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color("#a89020")),
|
||||||
|
|
||||||
|
StatusBar: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color("236")).
|
||||||
|
Foreground(lipgloss.Color("243")),
|
||||||
|
|
||||||
|
StatusBarActive: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color("62")).
|
||||||
|
Foreground(lipgloss.Color("230")),
|
||||||
|
|
||||||
|
CommandError: lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#e3203a")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CursorStyle returns the appropriate cursor style for the given mode.
|
||||||
|
func (s Styles) CursorStyle(mode core.Mode) lipgloss.Style {
|
||||||
|
switch mode {
|
||||||
|
case core.InsertMode:
|
||||||
|
return s.CursorInsert
|
||||||
|
case core.CommandMode:
|
||||||
|
return s.CursorCommand
|
||||||
|
default:
|
||||||
|
return s.CursorNormal
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user