refactor: huge refactor, this looks amazing.

Lots of comments from the AI. Some tests are not passing though
This commit is contained in:
Hayden Hargreaves 2026-03-04 21:45:47 -07:00
parent 154558b790
commit ccb061989a
48 changed files with 1219 additions and 1129 deletions

View File

@ -3,16 +3,18 @@ package main
import (
"fmt"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
"git.gophernest.net/azpect/TextEditor/internal/editor"
tea "github.com/charmbracelet/bubbletea"
)
// main: Entry point for the Gim text editor. Creates a buffer and window,
// initializes the editor model, and runs the BubbleTea TUI program.
func main() {
buf := action.NewBufferBuilder().
buf := core.NewBufferBuilder().
Build()
win := action.NewWindowBuilder().
win := core.NewWindowBuilder().
WithBuffer(&buf).
Build()

View File

@ -1,12 +1,16 @@
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
type ChangeToEndOfLine struct {
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 {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -16,14 +20,14 @@ func (a ChangeToEndOfLine) Execute(m Model) tea.Cmd {
// Save deleted text to register
if pos < len(line) {
m.UpdateDefaultRegister(CharwiseRegister, []string{line[pos:]})
m.UpdateDefaultRegister(core.CharwiseRegister, []string{line[pos:]})
}
// Delete to end of line
buf.SetLine(win.Cursor.Line, line[:pos])
// Enter insert mode
m.SetMode(InsertMode)
m.SetMode(core.InsertMode)
return nil
}
@ -31,6 +35,7 @@ func (a ChangeToEndOfLine) Execute(m Model) tea.Cmd {
// Ensure ChangeToEndOfLine implements Repeatable
var _ Repeatable = ChangeToEndOfLine{}
// ChangeToEndOfLine.WithCount: Returns a new ChangeToEndOfLine with the given count.
func (a ChangeToEndOfLine) WithCount(n int) Action {
return ChangeToEndOfLine{Count: n}
}
@ -40,6 +45,7 @@ type SubstituteChar struct {
Count int
}
// SubstituteChar.Execute: Deletes Count characters and enters insert mode (s key).
func (a SubstituteChar) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -52,14 +58,14 @@ func (a SubstituteChar) Execute(m Model) tea.Cmd {
if count > 0 {
// 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
buf.SetLine(win.Cursor.Line, line[:pos]+line[pos+count:])
}
// Enter insert mode
m.SetMode(InsertMode)
m.SetMode(core.InsertMode)
return nil
}
@ -67,6 +73,7 @@ func (a SubstituteChar) Execute(m Model) tea.Cmd {
// Ensure SubstituteChar implements Repeatable
var _ Repeatable = SubstituteChar{}
// SubstituteChar.WithCount: Returns a new SubstituteChar with the given count.
func (a SubstituteChar) WithCount(n int) Action {
return SubstituteChar{Count: n}
}
@ -76,6 +83,7 @@ type SubstituteLine struct {
Count int
}
// SubstituteLine.Execute: Clears Count lines and enters insert mode (S key).
func (a SubstituteLine) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -94,7 +102,7 @@ func (a SubstituteLine) Execute(m Model) tea.Cmd {
}
// Save deleted lines to register
m.UpdateDefaultRegister(LinewiseRegister, lines)
m.UpdateDefaultRegister(core.LinewiseRegister, lines)
// Insert empty line at original position
insertY := min(y, buf.LineCount())
@ -104,7 +112,7 @@ func (a SubstituteLine) Execute(m Model) tea.Cmd {
win.SetCursorPos(insertY, 0)
// Enter insert mode
m.SetMode(InsertMode)
m.SetMode(core.InsertMode)
return nil
}
@ -112,6 +120,7 @@ func (a SubstituteLine) Execute(m Model) tea.Cmd {
// Ensure SubstituteLine implements Repeatable
var _ Repeatable = SubstituteLine{}
// SubstituteLine.WithCount: Returns a new SubstituteLine with the given count.
func (a SubstituteLine) WithCount(n int) Action {
return SubstituteLine{Count: n}
}

View File

@ -1,24 +1,29 @@
package action
import (
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
// ExitCommandMode implements Action - exits command mode and returns to normal mode.
type ExitCommandMode struct{}
// ExitCommandMode.Execute: Exits command mode and returns to normal mode (Esc key).
func (a ExitCommandMode) Execute(m Model) tea.Cmd {
m.SetCommandCursor(0)
m.SetCommand("")
m.SetCommandOutput("")
m.SetCommandError(nil)
m.SetMode(NormalMode)
m.SetMode(core.NormalMode)
return nil
}
// InsertCommandChar implements Action - inserts a character in command mode.
type InsertCommandChar struct {
Char string
}
// InsertCommandChar.Execute: Inserts a character at the command cursor position.
func (a InsertCommandChar) Execute(m Model) tea.Cmd {
cur := m.CommandCursor()
cmd := m.Command()
@ -28,8 +33,10 @@ func (a InsertCommandChar) Execute(m Model) tea.Cmd {
return nil
}
// CommandBackspace implements Action - deletes character before cursor in command mode.
type CommandBackspace struct{}
// CommandBackspace.Execute: Deletes the character before the command cursor (Backspace key).
func (a CommandBackspace) Execute(m Model) tea.Cmd {
cur := m.CommandCursor()
cmd := m.Command()
@ -42,8 +49,10 @@ func (a CommandBackspace) Execute(m Model) tea.Cmd {
return nil
}
// CommandDelete implements Action - deletes character at cursor in command mode.
type CommandDelete struct{}
// CommandDelete.Execute: Deletes the character at the command cursor (Delete key).
func (a CommandDelete) Execute(m Model) tea.Cmd {
cur := m.CommandCursor()
cmd := m.Command()
@ -62,8 +71,10 @@ func (a CommandDelete) Execute(m Model) tea.Cmd {
return nil
}
// CommandDeletePreviousWord implements Action - deletes word before cursor in command mode.
type CommandDeletePreviousWord struct{}
// CommandDeletePreviousWord.Execute: Deletes the word before the command cursor (Ctrl+W).
func (a CommandDeletePreviousWord) Execute(m Model) tea.Cmd {
cur := m.CommandCursor()
cmd := m.Command()
@ -101,22 +112,24 @@ func (a CommandDeletePreviousWord) Execute(m Model) tea.Cmd {
return nil
}
// CommandExecute implements Action - executes the command line.
type CommandExecute struct {
Registry CommandRegistry
}
// CommandRegistry interface for executing commands
// CommandRegistry: Interface for executing commands.
type CommandRegistry interface {
Execute(m Model, cmdLine string) (tea.Cmd, error)
}
// CommandExecute.Execute: Executes the command line (Enter key).
func (a CommandExecute) Execute(m Model) tea.Cmd {
cmdLine := m.Command()
// Clear command state and return to normal mode
m.SetCommandCursor(0)
m.SetCommandError(nil)
m.SetMode(NormalMode)
m.SetMode(core.NormalMode)
if a.Registry == nil || cmdLine == "" {
return nil

View File

@ -7,6 +7,7 @@ type DeleteChar struct {
Count int
}
// DeleteChar.Execute: Deletes Count characters at the cursor position (x key).
func (a DeleteChar) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -21,14 +22,18 @@ func (a DeleteChar) Execute(m Model) tea.Cmd {
return nil
}
// DeleteChar.WithCount: Returns a new DeleteChar with the given count.
func (a DeleteChar) WithCount(n int) Action {
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 {
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 {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -75,6 +80,7 @@ func (a DeleteToEndOfLine) Execute(m Model) tea.Cmd {
return nil
}
// DeleteToEndOfLine.WithCount: Returns a new DeleteToEndOfLine with the given count.
func (a DeleteToEndOfLine) WithCount(n int) Action {
return DeleteToEndOfLine{Count: n}
}

View File

@ -3,6 +3,7 @@ package action
import (
"strings"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
@ -11,13 +12,15 @@ type EnterInsert struct {
Count int
}
// EnterInsert.Execute: Enters insert mode at the cursor position (i key).
func (a EnterInsert) Execute(m Model) tea.Cmd {
// Start recording
m.SetInsertRecording(a.Count, a)
m.SetMode(InsertMode)
m.SetMode(core.InsertMode)
return nil
}
// EnterInsert.WithCount: Returns a new EnterInsert with the given count.
func (a EnterInsert) WithCount(n int) Action {
return EnterInsert{Count: n}
}
@ -27,16 +30,18 @@ type EnterInsertAfter struct {
Count int
}
// EnterInsertAfter.Execute: Enters insert mode after the cursor position (a key).
func (a EnterInsertAfter) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
win.SetCursorCol(win.Cursor.Col + 1)
// Start recording
m.SetInsertRecording(a.Count, a)
m.SetMode(InsertMode)
m.SetMode(core.InsertMode)
return nil
}
// EnterInsertAfter.WithCount: Returns a new EnterInsertAfter with the given count.
func (a EnterInsertAfter) WithCount(n int) Action {
return EnterInsertAfter{Count: n}
}
@ -46,16 +51,18 @@ type EnterInsertLineStart struct {
Count int
}
// EnterInsertLineStart.Execute: Enters insert mode at the start of the line (I key).
func (a EnterInsertLineStart) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
win.SetCursorCol(0)
// Start recording
m.SetInsertRecording(a.Count, a)
m.SetMode(InsertMode)
m.SetMode(core.InsertMode)
return nil
}
// EnterInsertLineStart.WithCount: Returns a new EnterInsertLineStart with the given count.
func (a EnterInsertLineStart) WithCount(n int) Action {
return EnterInsertLineStart{Count: n}
}
@ -65,6 +72,7 @@ type EnterInsertLineEnd struct {
Count int
}
// EnterInsertLineEnd.Execute: Enters insert mode at the end of the line (A key).
func (a EnterInsertLineEnd) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -72,10 +80,11 @@ func (a EnterInsertLineEnd) Execute(m Model) tea.Cmd {
// Start recording
m.SetInsertRecording(a.Count, a)
m.SetMode(InsertMode)
m.SetMode(core.InsertMode)
return nil
}
// EnterInsertLineEnd.WithCount: Returns a new EnterInsertLineEnd with the given count.
func (a EnterInsertLineEnd) WithCount(n int) Action {
return EnterInsertLineEnd{Count: n}
}
@ -85,6 +94,7 @@ type OpenLineBelow struct {
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 {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -101,10 +111,11 @@ func (a OpenLineBelow) Execute(m Model) tea.Cmd {
// Start recording
m.SetInsertRecording(a.Count, a)
m.SetMode(InsertMode)
m.SetMode(core.InsertMode)
return nil
}
// OpenLineBelow.WithCount: Returns a new OpenLineBelow with the given count.
func (a OpenLineBelow) WithCount(n int) Action {
return OpenLineBelow{Count: n}
}
@ -114,6 +125,7 @@ type OpenLineAbove struct {
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 {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -124,10 +136,11 @@ func (a OpenLineAbove) Execute(m Model) tea.Cmd {
// Start recording
m.SetInsertRecording(a.Count, a)
m.SetMode(InsertMode)
m.SetMode(core.InsertMode)
return nil
}
// OpenLineAbove.WithCount: Returns a new OpenLineAbove with the given count.
func (a OpenLineAbove) WithCount(n int) Action {
return OpenLineAbove{Count: n}
}
@ -139,6 +152,7 @@ type InsertChar struct {
Char string
}
// InsertChar.Execute: Inserts a single character at the cursor position.
func (a InsertChar) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -157,6 +171,7 @@ func (a InsertChar) Execute(m Model) tea.Cmd {
// InsertNewline splits the current line at the cursor (enter key)
type InsertNewline struct{}
// InsertNewline.Execute: Splits the current line at the cursor (Enter key).
func (a InsertNewline) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -176,6 +191,7 @@ func (a InsertNewline) Execute(m Model) tea.Cmd {
// InsertBackspace deletes the character before the cursor
type InsertBackspace struct{}
// InsertBackspace.Execute: Deletes the character before the cursor (Backspace key).
func (a InsertBackspace) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -198,6 +214,7 @@ func (a InsertBackspace) Execute(m Model) tea.Cmd {
// InsertDelete deletes the character under/after the cursor (delete key)
type InsertDelete struct{}
// InsertDelete.Execute: Deletes the character at the cursor position (Delete key).
func (a InsertDelete) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -217,6 +234,7 @@ func (a InsertDelete) Execute(m Model) tea.Cmd {
// InsertTab inserts spaces equal to the tab size
type InsertTab struct{}
// InsertTab.Execute: Inserts spaces equal to the tab size (Tab key).
func (a InsertTab) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -236,6 +254,8 @@ func (a InsertTab) Execute(m Model) tea.Cmd {
// InsertDeletePreviousWord deletes the word before the cursor (ctrl+w)
type InsertDeletePreviousWord struct{}
// isWordChar: Returns true if the character is a word character (alphanumeric
// or underscore).
func isWordChar(c byte) bool {
return (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
@ -243,10 +263,13 @@ func isWordChar(c byte) bool {
c == '_'
}
// isPunctuation: Returns true if the character is punctuation (not whitespace
// and not a word character).
func isPunctuation(c byte) bool {
return c != ' ' && c != '\t' && !isWordChar(c)
}
// InsertDeletePreviousWord.Execute: Deletes the word before the cursor (Ctrl+W).
func (a InsertDeletePreviousWord) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()

View File

@ -1,6 +1,7 @@
package action
import (
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
@ -9,10 +10,10 @@ type Model interface {
// ==================================================
// Core Data Access
// ==================================================
Windows() []*Window
ActiveWindow() *Window
Buffers() []*Buffer
ActiveBuffer() *Buffer
Windows() []*core.Window
ActiveWindow() *core.Window
Buffers() []*core.Buffer
ActiveBuffer() *core.Buffer
// ==================================================
// Insert Mode State
@ -41,70 +42,20 @@ type Model interface {
// ==================================================
// Editor-wide State
// ==================================================
Mode() Mode
SetMode(mode Mode)
Mode() core.Mode
SetMode(mode core.Mode)
Settings() Settings
SetSettings(s Settings)
Settings() core.Settings
SetSettings(s core.Settings)
// ==================================================
// Registers
// ==================================================
Registers() map[rune]Register
GetRegister(name rune) (Register, bool)
SetRegister(name rune, t RegisterType, cnt []string) error
UpdateDefaultRegister(t RegisterType, cnt []string)
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)
// ==================================================
// Depreciated
// ==================================================
// 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)
}
// 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
@ -115,12 +66,12 @@ type Action interface {
// Motion moves the cursor and returns the range covered
type Motion interface {
Action
Type() MotionType
Type() core.MotionType
}
// Operator acts on a range (delete, yank, change)
type Operator interface {
Operate(m Model, start, end Position, mtype MotionType) tea.Cmd
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)

View File

@ -1,19 +1,24 @@
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)
type Quit struct{}
// Quit.Execute: Quits the editor (Ctrl+C).
func (a Quit) Execute(m Model) tea.Cmd {
return tea.Quit
}
// Quit implements Action (:)
// EnterComandMode implements Action (:) - enters command mode.
type EnterComandMode struct{}
// EnterComandMode.Execute: Enters command mode (: key).
func (a EnterComandMode) Execute(m Model) tea.Cmd {
m.SetMode(CommandMode)
m.SetMode(core.CommandMode)
m.SetCommand("")
m.SetCommandOutput("")
m.SetCommandError(nil)
@ -21,35 +26,38 @@ func (a EnterComandMode) Execute(m Model) tea.Cmd {
return nil
}
// Quit implements Action (v)
// EnterVisualMode implements Action (v) - enters visual character mode.
type EnterVisualMode struct{}
// EnterVisualMode.Execute: Enters visual character mode (v key).
func (a EnterVisualMode) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
win.SetAnchorCol(win.Cursor.Col)
win.SetAnchorLine(win.Cursor.Line)
m.SetMode(VisualMode)
m.SetMode(core.VisualMode)
return nil
}
// Quit implements Action (V)
// EnterVisualLineMode implements Action (V) - enters visual line mode.
type EnterVisualLineMode struct{}
// EnterVisualLineMode.Execute: Enters visual line mode (V key).
func (a EnterVisualLineMode) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
win.SetAnchorCol(win.Cursor.Col)
win.SetAnchorLine(win.Cursor.Line)
m.SetMode(VisualLineMode)
m.SetMode(core.VisualLineMode)
return nil
}
// Quit implements Action (ctrl+v)
// EnterVisualBlockMode implements Action (Ctrl+V) - enters visual block mode.
type EnterVisualBlockMode struct{}
// EnterVisualBlockMode.Execute: Enters visual block mode (Ctrl+V).
func (a EnterVisualBlockMode) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
win.SetAnchorCol(win.Cursor.Col)
win.SetAnchorLine(win.Cursor.Line)
m.SetMode(VisualBlockMode)
m.SetMode(core.VisualBlockMode)
return nil
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
@ -12,6 +13,7 @@ type Paste struct {
Count int
}
// Paste.Execute: Pastes register content after the cursor position (p key).
func (a Paste) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -29,7 +31,7 @@ func (a Paste) Execute(m Model) tea.Cmd {
}
switch reg.Type {
case LinewiseRegister:
case core.LinewiseRegister:
{
initY := win.Cursor.Line
lines := reg.Content
@ -47,7 +49,7 @@ func (a Paste) Execute(m Model) tea.Cmd {
win.SetCursorLine(initY + 1)
}
}
case CharwiseRegister:
case core.CharwiseRegister:
{
lines := reg.Content
@ -71,7 +73,7 @@ func (a Paste) Execute(m Model) tea.Cmd {
win.SetCursorCol(x + len(cnt))
}
default:
m.SetCommandError(fmt.Errorf("Register type is not implemented."))
m.SetCommandError(fmt.Errorf("core.Register type is not implemented."))
}
return nil
@ -80,6 +82,7 @@ func (a Paste) Execute(m Model) tea.Cmd {
// Ensure Paste implements Repeatable
var _ Repeatable = Paste{}
// Paste.WithCount: Returns a new Paste with the given count.
func (a Paste) WithCount(n int) Action {
return Paste{Count: n}
}
@ -89,6 +92,7 @@ type PasteBefore struct {
Count int
}
// PasteBefore.Execute: Pastes register content before the cursor position (P key).
func (a PasteBefore) Execute(m Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -101,7 +105,7 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
}
switch reg.Type {
case LinewiseRegister:
case core.LinewiseRegister:
{
initY := win.Cursor.Line
lines := reg.Content
@ -115,7 +119,7 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
}
}
}
case CharwiseRegister:
case core.CharwiseRegister:
{
lines := reg.Content
@ -139,7 +143,7 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
win.SetCursorCol(x + len(cnt))
}
default:
m.SetCommandError(fmt.Errorf("Register type is not implemented."))
m.SetCommandError(fmt.Errorf("core.Register type is not implemented."))
}
return nil
@ -148,6 +152,7 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
// Ensure PasteBefore implements Repeatable
var _ Repeatable = PasteBefore{}
// PasteBefore.WithCount: Returns a new PasteBefore with the given count.
func (a PasteBefore) WithCount(n int) Action {
return PasteBefore{Count: n}
}
@ -157,6 +162,7 @@ type VisualPaste struct {
Count int
}
// VisualPaste.Execute: Replaces visual selection with register content (p in visual mode).
func (a VisualPaste) Execute(m Model) tea.Cmd {
// Get register content to paste
reg, found := m.GetRegister('"')
@ -171,26 +177,26 @@ func (a VisualPaste) Execute(m Model) tea.Cmd {
start, end := normalizeSelection(m)
switch mode {
case VisualMode:
case core.VisualMode:
visualCharPaste(m, reg, start, end)
case VisualBlockMode:
case core.VisualBlockMode:
visualBlockPaste(m, reg, start, end)
case VisualLineMode:
case core.VisualLineMode:
visualLinePaste(m, reg, start, end)
}
// Exit visual mode
m.SetMode(NormalMode)
m.SetMode(core.NormalMode)
return nil
}
// normalizeSelection returns start and end positions with start always before end
func normalizeSelection(m Model) (Position, Position) {
// normalizeSelection: Returns start and end positions with start always before end.
func normalizeSelection(m Model) (core.Position, core.Position) {
win := m.ActiveWindow()
start := Position{Line: win.Anchor.Line, Col: win.Anchor.Col}
end := Position{Line: win.Cursor.Line, Col: win.Cursor.Col}
start := core.Position{Line: win.Anchor.Line, Col: win.Anchor.Col}
end := core.Position{Line: win.Cursor.Line, Col: win.Cursor.Col}
// Normalize so start is always before end
if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
@ -200,8 +206,8 @@ func normalizeSelection(m Model) (Position, Position) {
return start, end
}
// visualCharPaste handles paste in visual (character) mode
func visualCharPaste(m Model, reg Register, start, end Position) {
// visualCharPaste: Handles paste operation in visual (character) mode.
func visualCharPaste(m Model, reg core.Register, start, end core.Position) {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -214,7 +220,7 @@ func visualCharPaste(m Model, reg Register, start, end Position) {
// Insert the register content at start position
if len(reg.Content) == 0 {
// Empty register - just delete (already done)
} else if reg.Type == CharwiseRegister {
} else if reg.Type == core.CharwiseRegister {
// Charwise paste: insert text at cursor position
if len(reg.Content) == 1 {
line := buf.Lines[start.Line]
@ -226,7 +232,7 @@ func visualCharPaste(m Model, reg Register, start, end Position) {
win.SetCursorCol(insertAt + len(reg.Content[0]) - 1)
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
// Insert each line from register
for i, content := range reg.Content {
@ -250,11 +256,11 @@ func visualCharPaste(m Model, reg Register, start, end Position) {
}
// Update register with deleted text
m.UpdateDefaultRegister(CharwiseRegister, []string{deletedText})
m.UpdateDefaultRegister(core.CharwiseRegister, []string{deletedText})
}
// visualBlockPaste handles paste in visual block mode
func visualBlockPaste(m Model, reg Register, start, end Position) {
// visualBlockPaste: Handles paste operation in visual block mode.
func visualBlockPaste(m Model, reg core.Register, start, end core.Position) {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -286,7 +292,7 @@ func visualBlockPaste(m Model, reg Register, start, end Position) {
// Insert register content
if len(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]
}
@ -306,11 +312,11 @@ func visualBlockPaste(m Model, reg Register, start, end Position) {
win.SetCursorCol(startCol)
// 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
func visualLinePaste(m Model, reg Register, start, end Position) {
// visualLinePaste: Handles paste operation in visual line mode.
func visualLinePaste(m Model, reg core.Register, start, end core.Position) {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -331,7 +337,7 @@ func visualLinePaste(m Model, reg Register, start, end Position) {
if buf.LineCount() == 0 {
buf.InsertLine(0, "")
}
} else if reg.Type == LinewiseRegister {
} else if reg.Type == core.LinewiseRegister {
// Linewise register: insert each line
insertPos := start.Line
for _, content := range reg.Content {
@ -348,7 +354,7 @@ func visualLinePaste(m Model, reg Register, start, end Position) {
buf.InsertLine(0, "")
}
// Position cursor at start of pasted content
// core.Position cursor at start of pasted content
y := start.Line
if y >= buf.LineCount() {
y = buf.LineCount() - 1
@ -357,11 +363,11 @@ func visualLinePaste(m Model, reg Register, start, end Position) {
win.SetCursorCol(0)
// Update register with deleted lines
m.UpdateDefaultRegister(LinewiseRegister, deletedLines)
m.UpdateDefaultRegister(core.LinewiseRegister, deletedLines)
}
// extractCharSelection extracts text from a character selection
func extractCharSelection(m Model, start, end Position) string {
// extractCharSelection: Extracts text from a character selection range.
func extractCharSelection(m Model, start, end core.Position) string {
buf := m.ActiveBuffer()
if start.Line == end.Line {
@ -398,8 +404,8 @@ func extractCharSelection(m Model, start, end Position) string {
return result.String()
}
// deleteCharSelectionForPaste deletes a character selection (similar to operator/delete.go)
func deleteCharSelectionForPaste(m Model, start, end Position) {
// deleteCharSelectionForPaste: Deletes a character selection for paste operations.
func deleteCharSelectionForPaste(m Model, start, end core.Position) {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -435,6 +441,7 @@ func deleteCharSelectionForPaste(m Model, start, end Position) {
// Ensure VisualPaste implements Repeatable
var _ Repeatable = VisualPaste{}
// VisualPaste.WithCount: Returns a new VisualPaste with the given count.
func (a VisualPaste) WithCount(n int) Action {
return VisualPaste{Count: n}
}

View File

@ -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{},
}
}

View File

@ -6,32 +6,33 @@ import (
"strings"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
// QuitMsg signals the application should quit
// QuitMsg: Message signaling the application should quit.
type QuitMsg struct{}
// ErrorMsg signals an error to display
// ErrorMsg: Message signaling an error to display.
type ErrorMsg struct {
Err error
}
// cmdQuit handles :quit / :q
// cmdQuit: Handles :quit / :q command.
func cmdQuit(m action.Model, args []string) tea.Cmd {
return func() tea.Msg {
return tea.Quit()
}
}
// cmdQuitAll handles :qall / :qa
// cmdQuitAll: Handles :qall / :qa command.
func cmdQuitAll(m action.Model, args []string) tea.Cmd {
return func() tea.Msg {
return tea.Quit()
}
}
// cmdWrite handles :write / :w
// cmdWrite: Handles :write / :w command (TODO: implement file saving).
func cmdWrite(m action.Model, args []string) tea.Cmd {
// TODO: Implement file saving
// If args provided, save to that filename
@ -39,13 +40,13 @@ func cmdWrite(m action.Model, args []string) tea.Cmd {
return nil
}
// cmdWriteAll handles :wall / :wa
// cmdWriteAll: Handles :wall / :wa command (TODO: implement saving all buffers).
func cmdWriteAll(m action.Model, args []string) tea.Cmd {
// TODO: Implement saving all buffers
return nil
}
// cmdWriteQuit handles :wq
// cmdWriteQuit: Handles :wq command (TODO: save then quit).
func cmdWriteQuit(m action.Model, args []string) tea.Cmd {
// TODO: Save then quit
return func() tea.Msg {
@ -53,7 +54,7 @@ func cmdWriteQuit(m action.Model, args []string) tea.Cmd {
}
}
// cmdRegisters handles :register
// cmdRegisters: Handles :register command (debug - displays register content).
func cmdRegisters(m action.Model, args []string) tea.Cmd {
// TODO: This is temporary, for debugging
if len(args) < 1 {
@ -79,7 +80,7 @@ func cmdRegisters(m action.Model, args []string) tea.Cmd {
return nil
}
// cmdSet handles :set option[=value]
// cmdSet: Handles :set option[=value] command for configuring editor settings.
// Examples:
//
// :set number - enable number
@ -104,15 +105,16 @@ func cmdSet(m action.Model, args []string) tea.Cmd {
return nil
}
// Setting represents a configurable option
// Setting: Represents a configurable editor option.
type Setting struct {
Name string
ShortForm string
Type SettingType
Get func(s action.Settings) any
Get func(s core.Settings) any
Set func(m action.Model, val any)
}
// SettingType: Enumeration of setting value types.
type SettingType int
const (
@ -127,7 +129,7 @@ var settingsMap = []Setting{
Name: "number",
ShortForm: "nu",
Type: BoolSetting,
Get: func(s action.Settings) any { return s.Number },
Get: func(s core.Settings) any { return s.Number },
Set: func(m action.Model, val any) {
s := m.Settings()
s.Number = val.(bool)
@ -138,7 +140,7 @@ var settingsMap = []Setting{
Name: "relativenumber",
ShortForm: "rnu",
Type: BoolSetting,
Get: func(s action.Settings) any { return s.RelativeNumber },
Get: func(s core.Settings) any { return s.RelativeNumber },
Set: func(m action.Model, val any) {
s := m.Settings()
s.RelativeNumber = val.(bool)
@ -149,7 +151,7 @@ var settingsMap = []Setting{
Name: "tabstop",
ShortForm: "ts",
Type: IntSetting,
Get: func(s action.Settings) any { return s.TabSize },
Get: func(s core.Settings) any { return s.TabSize },
Set: func(m action.Model, val any) {
s := m.Settings()
s.TabSize = val.(int)
@ -160,7 +162,7 @@ var settingsMap = []Setting{
Name: "scrolloff",
ShortForm: "so",
Type: IntSetting,
Get: func(s action.Settings) any { return s.ScrollOff },
Get: func(s core.Settings) any { return s.ScrollOff },
Set: func(m action.Model, val any) {
s := m.Settings()
s.ScrollOff = val.(int)
@ -169,6 +171,7 @@ var settingsMap = []Setting{
},
}
// lookupSetting: Finds a setting by name, short form, or prefix.
func lookupSetting(name string) *Setting {
for i := range settingsMap {
s := &settingsMap[i]
@ -183,6 +186,7 @@ func lookupSetting(name string) *Setting {
return nil
}
// parseSetOption: Parses and applies a single :set option.
func parseSetOption(m action.Model, opt string) error {
// Handle toggle: option!
if name, ok := strings.CutSuffix(opt, "!"); ok {

View File

@ -8,32 +8,31 @@ import (
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 {
Name string // Full name: "quit"
ShortForm string // Minimum abbreviation: "q"
Handler func(m action.Model, args []string) tea.Cmd // Handler function
}
// Registry holds all registered commands
// Registry: Holds all registered commands.
type Registry struct {
commands []Command
}
// NewRegistry creates a new command registry with default commands
// NewRegistry: Creates a new command registry with default commands.
func NewRegistry() *Registry {
r := &Registry{}
r.registerDefaults()
return r
}
// Register adds a command to the registry
// Registry.Register: Adds a command to the registry.
func (r *Registry) Register(cmd Command) {
r.commands = append(r.commands, cmd)
}
// Lookup finds a command by name or abbreviation
// Returns the command and any error (unknown or ambiguous)
// Registry.Lookup: Finds a command by name or abbreviation with error handling.
func (r *Registry) Lookup(input string) (*Command, error) {
if input == "" {
return nil, fmt.Errorf("no command given")
@ -75,7 +74,7 @@ func (r *Registry) Lookup(input string) (*Command, error) {
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) {
parts := strings.Fields(cmdLine)
if len(parts) == 0 {
@ -84,7 +83,7 @@ func Parse(cmdLine string) (name string, args []string) {
return parts[0], parts[1:]
}
// Execute parses and executes a command line
// Registry.Execute: Parses and executes a command line.
func (r *Registry) Execute(m action.Model, cmdLine string) (tea.Cmd, error) {
name, args := Parse(cmdLine)
@ -99,7 +98,7 @@ func (r *Registry) Execute(m action.Model, cmdLine string) (tea.Cmd, error) {
// DefaultRegistry is the global command registry
var DefaultRegistry = NewRegistry()
// registerDefaults registers the built-in commands
// Registry.registerDefaults: Registers the built-in commands.
func (r *Registry) registerDefaults() {
// Quit commands
r.Register(Command{

View File

@ -1,4 +1,4 @@
package action
package core
type BufferOptions struct {
// tabstop expandtab

View File

@ -1,4 +1,4 @@
package action
package core
// Not great, but maybe the best way
var CurrentBufferId int = 1

View File

@ -1,4 +1,4 @@
package action
package core
// Mode constants for editor mode
type Mode int
@ -12,6 +12,8 @@ const (
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:
@ -31,6 +33,7 @@ func (m Mode) ToString() string {
}
}
// Mode.IsVisualMode: Returns true if the mode is any visual mode variant.
func (m Mode) IsVisualMode() bool {
return m == VisualMode ||
m == VisualLineMode ||

View 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
View 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{},
}
}

View File

@ -1,5 +1,6 @@
package action
package core
// Settings: Configuration options for editor display and behavior.
type Settings struct {
Number bool
RelativeNumber bool
@ -9,6 +10,8 @@ type Settings struct {
// TODO: Colors
}
// NewDefaultSettings: Creates a Settings struct with sensible defaults for
// line numbers, gutter width, tab size, and scroll offset.
func NewDefaultSettings() Settings {
return Settings{
Number: true,

15
internal/core/types.go Normal file
View 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
}

View File

@ -1,8 +1,4 @@
package action
import (
"strings"
)
package core
// TODO: No more global settings, window-wide settings
type WinOptions struct {
@ -84,61 +80,6 @@ func (w *Window) AdjustScroll() {
w.ScrollY = max(0, min(w.ScrollY, maxScroll))
}
// ==================================================
// View methods
// ==================================================
// Window.View ...
func (w *Window) View(m Model) string {
buf := w.Buffer
viewport := w.Height - 2 // command bar (1) + status line (1)
start := w.ScrollY
end := w.ScrollY + viewport
var view strings.Builder
for i := start; i < end; i++ {
// past the file, just draw the '~'
if i >= buf.LineCount() {
// TODO: Handle gutter and line numbers
view.WriteString("~")
view.WriteString("\n")
continue
}
line := w.drawLine(m, buf.Line(i), i)
view.WriteString(line)
// Break to next line
view.WriteString("\n")
}
return view.String()
}
// TODO: Only pass what we need from the model, not the entire model.
func (w *Window) drawLine(m Model, line string, lineNum int) string {
chars := []rune(line)
for col := 0; col <= len(chars); col++ {
// Currently on the cursor
if w.Cursor.Line == lineNum && w.Cursor.Col == col {
if col < len(chars) {
return m.Styles().CursorStyle(m.Mode()).Render(string(chars[col]))
} else {
return m.Styles().CursorStyle(m.Mode()).Render(" ")
}
}
}
return ""
}
func (w *Window) drawGutter() string {
return ""
}
// ==================================================
// Setters
// ==================================================

View File

@ -1,4 +1,4 @@
package action
package core
// Not great, but maybe the best way
var CurrentWindowId int = 1000

View File

@ -3,7 +3,7 @@ package editor
import (
"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
@ -27,7 +27,7 @@ func TestHelperExamples(t *testing.T) {
t.Run("custom cursor position", func(t *testing.T) {
tm := newTestModel(t,
WithCursorPos(action.Position{Line: 2, Col: 3}),
WithCursorPos(core.Position{Line: 2, Col: 3}),
)
m := getFinalModel(t, tm)
win := m.ActiveWindow()
@ -50,7 +50,7 @@ func TestHelperExamples(t *testing.T) {
t.Run("with register content for paste testing", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithRegister('"', action.CharwiseRegister, []string{"foo"}),
WithRegister('"', core.CharwiseRegister, []string{"foo"}),
)
m := getFinalModel(t, tm)
@ -58,7 +58,7 @@ func TestHelperExamples(t *testing.T) {
if !ok {
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)
}
if len(reg.Content) != 1 || reg.Content[0] != "foo" {
@ -69,9 +69,9 @@ func TestHelperExamples(t *testing.T) {
t.Run("combine multiple options", func(t *testing.T) {
tm := newTestModel(t,
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),
WithRegister('"', action.LinewiseRegister, []string{"deleted line 1", "deleted line 2"}),
WithRegister('"', core.LinewiseRegister, []string{"deleted line 1", "deleted line 2"}),
)
m := getFinalModel(t, tm)
@ -90,7 +90,7 @@ func TestHelperExamples(t *testing.T) {
}
reg, ok := m.GetRegister('"')
if !ok || reg.Type != action.LinewiseRegister {
if !ok || reg.Type != core.LinewiseRegister {
t.Error("register not set correctly")
}
})
@ -104,21 +104,21 @@ func TestHelperExamples(t *testing.T) {
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)
win2 := m2.ActiveWindow()
if win2.Cursor.Line != 1 {
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)
buf3 := m3.ActiveBuffer()
if buf3.LineCount() != 1 {
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)
win4 := m4.ActiveWindow()
if win4.Width != 50 {

View File

@ -4,7 +4,7 @@ import (
"testing"
"time"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/x/exp/teatest"
)
@ -42,11 +42,11 @@ type TestModelOption func(*testModelConfig)
type testModelConfig struct {
lines []string
pos action.Position
pos core.Position
width int
height int
regName rune
regType action.RegisterType
regType core.RegisterType
regContent []string
}
@ -58,7 +58,7 @@ func WithLines(lines []string) TestModelOption {
}
// WithCursorPos sets the initial cursor position
func WithCursorPos(pos action.Position) TestModelOption {
func WithCursorPos(pos core.Position) TestModelOption {
return func(c *testModelConfig) {
c.pos = pos
}
@ -73,7 +73,7 @@ func WithTermSize(width, height int) TestModelOption {
}
// 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) {
c.regName = name
c.regType = regType
@ -86,7 +86,7 @@ func newTestModel(t *testing.T, opts ...TestModelOption) *teatest.TestModel {
// Default configuration
cfg := testModelConfig{
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,
height: 24,
}
@ -96,11 +96,11 @@ func newTestModel(t *testing.T, opts ...TestModelOption) *teatest.TestModel {
opt(&cfg)
}
buf := action.NewBufferBuilder().
buf := core.NewBufferBuilder().
WithLines(cfg.lines).
Build()
win := action.NewWindowBuilder().
win := core.NewWindowBuilder().
WithBuffer(&buf).
WithCursorPos(cfg.pos.Line, cfg.pos.Col).
WithDimensions(cfg.width, cfg.height).
@ -131,15 +131,15 @@ func newTestModelWithLines(t *testing.T, lines []string) *teatest.TestModel {
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))
}
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))
}
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))
}

View File

@ -3,7 +3,7 @@ package editor
import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
)
// NOTE: AI Generated tests
@ -153,7 +153,7 @@ func TestCommandModeNavigation(t *testing.T) {
m := getFinalModel(t, tm)
// 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())
}
})
@ -164,7 +164,7 @@ func TestCommandModeNavigation(t *testing.T) {
sendKeys(tm, ":", "esc")
m := getFinalModel(t, tm)
if m.Mode() != action.NormalMode {
if m.Mode() != core.NormalMode {
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
}
})
@ -175,7 +175,7 @@ func TestCommandModeNavigation(t *testing.T) {
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "enter")
m := getFinalModel(t, tm)
if m.Mode() != action.NormalMode {
if m.Mode() != core.NormalMode {
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
}
})

View File

@ -3,7 +3,7 @@ package editor
import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
)
func TestDeleteChar(t *testing.T) {
@ -20,7 +20,7 @@ func TestDeleteChar(t *testing.T) {
t.Run("test 'x' in middle of line", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "x")
m := getFinalModel(t, tm)
@ -31,7 +31,7 @@ func TestDeleteChar(t *testing.T) {
t.Run("test 'x' at end of line", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
sendKeys(tm, "x")
m := getFinalModel(t, tm)
@ -77,7 +77,7 @@ func TestDeleteCharWithCount(t *testing.T) {
t.Run("test '2x' from middle", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
sendKeys(tm, "2", "x")
m := getFinalModel(t, tm)
@ -115,7 +115,7 @@ func TestDeleteCharEdgeCases(t *testing.T) {
t.Run("test 'x' at last char deletes it", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -126,7 +126,7 @@ func TestDeleteCharEdgeCases(t *testing.T) {
t.Run("test 'x' with whitespace", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -162,7 +162,7 @@ func TestDeleteCharEdgeCases(t *testing.T) {
t.Run("test 'x' on line with tabs", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -184,7 +184,7 @@ func TestDeleteCharEdgeCases(t *testing.T) {
t.Run("test 'x' in middle preserves surrounding chars", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -197,7 +197,7 @@ func TestDeleteCharEdgeCases(t *testing.T) {
func TestDeleteToEndOfLine(t *testing.T) {
t.Run("test 'D' deletes to end of line", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -219,7 +219,7 @@ func TestDeleteToEndOfLine(t *testing.T) {
t.Run("test 'D' at last character", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -230,7 +230,7 @@ func TestDeleteToEndOfLine(t *testing.T) {
t.Run("test 'D' cursor position after delete", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -253,7 +253,7 @@ func TestDeleteToEndOfLine(t *testing.T) {
t.Run("test 'D' with count deletes following lines", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -270,7 +270,7 @@ func TestDeleteToEndOfLine(t *testing.T) {
t.Run("test 'D' with count deletes following lines with overflow", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -297,7 +297,7 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
t.Run("test 'D' at end of file", func(t *testing.T) {
lines := []string{"line 1", "line 2"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "D")
m := getFinalModel(t, tm)
@ -311,7 +311,7 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
t.Run("test 'D' preserves lines above", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -325,7 +325,7 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
t.Run("test 'D' cursor clamps to valid position", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -351,7 +351,7 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
t.Run("test 'D' from middle of whitespace-only line", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -362,7 +362,7 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
t.Run("test 'D' with tabs", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -373,7 +373,7 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
t.Run("test 'D' on line with only one char remaining after cursor", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -384,7 +384,7 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
t.Run("test 'D' does not affect line below", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -395,7 +395,7 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
t.Run("test 'D' with multiple lines", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -409,7 +409,7 @@ func TestDeleteToEndOfLineEdgeCases(t *testing.T) {
t.Run("test 'D' preserves cursor Y position", func(t *testing.T) {
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")
m := getFinalModel(t, tm)

View File

@ -3,7 +3,7 @@ package editor
import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
)
// --- Insert Mode Entry Tests ---
@ -14,8 +14,8 @@ func TestEnterInsert(t *testing.T) {
sendKeys(tm, "i")
m := getFinalModel(t, tm)
if m.mode != action.InsertMode {
t.Errorf("mode = %d, want InsertMode (%d)", m.mode, action.InsertMode)
if m.mode != core.InsertMode {
t.Errorf("mode = %d, want InsertMode (%d)", m.mode, core.InsertMode)
}
})
@ -32,7 +32,7 @@ func TestEnterInsert(t *testing.T) {
t.Run("test 'i' insert in middle", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "i", "X", "esc")
m := getFinalModel(t, tm)
@ -43,7 +43,7 @@ func TestEnterInsert(t *testing.T) {
t.Run("test 'i' cursor moves back on esc", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "i", "X", "esc")
m := getFinalModel(t, tm)
@ -59,8 +59,8 @@ func TestEnterInsertAfter(t *testing.T) {
sendKeys(tm, "a")
m := getFinalModel(t, tm)
if m.mode != action.InsertMode {
t.Errorf("mode = %d, want InsertMode (%d)", m.mode, action.InsertMode)
if m.mode != core.InsertMode {
t.Errorf("mode = %d, want InsertMode (%d)", m.mode, core.InsertMode)
}
})
@ -77,7 +77,7 @@ func TestEnterInsertAfter(t *testing.T) {
t.Run("test 'a' from middle of line", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "a", "X", "esc")
m := getFinalModel(t, tm)
@ -90,7 +90,7 @@ func TestEnterInsertAfter(t *testing.T) {
func TestEnterInsertLineStart(t *testing.T) {
t.Run("test 'I' enters insert mode at line start", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
sendKeys(tm, "I", "X", "esc")
m := getFinalModel(t, tm)
@ -101,7 +101,7 @@ func TestEnterInsertLineStart(t *testing.T) {
t.Run("test 'I' from end of line", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "I", "X", "esc")
m := getFinalModel(t, tm)
@ -125,7 +125,7 @@ func TestEnterInsertLineEnd(t *testing.T) {
t.Run("test 'A' from middle of line", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "A", "X", "esc")
m := getFinalModel(t, tm)
@ -154,7 +154,7 @@ func TestOpenLineBelow(t *testing.T) {
t.Run("test 'o' from middle of file", func(t *testing.T) {
lines := []string{"line 1", "line 2", "line 3"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "o", "n", "e", "w", "esc")
m := getFinalModel(t, tm)
@ -168,7 +168,7 @@ func TestOpenLineBelow(t *testing.T) {
t.Run("test 'o' at end of file", func(t *testing.T) {
lines := []string{"line 1", "line 2"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "o", "n", "e", "w", "esc")
m := getFinalModel(t, tm)
@ -233,7 +233,7 @@ func TestOpenLineBelowWithCount(t *testing.T) {
func TestOpenLineAbove(t *testing.T) {
t.Run("test 'O' creates line above", func(t *testing.T) {
lines := []string{"line 1", "line 2"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "O", "n", "e", "w", "esc")
m := getFinalModel(t, tm)
@ -261,7 +261,7 @@ func TestOpenLineAbove(t *testing.T) {
t.Run("test 'O' cursor at start of new line", func(t *testing.T) {
lines := []string{"line 1", "line 2"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 1})
sendKeys(tm, "O", "esc")
m := getFinalModel(t, tm)
@ -274,7 +274,7 @@ func TestOpenLineAbove(t *testing.T) {
func TestOpenLineAboveWithCount(t *testing.T) {
t.Run("test '3O' creates 3 lines above", func(t *testing.T) {
lines := []string{"line 1"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "3", "O", "x", "esc")
m := getFinalModel(t, tm)
@ -294,7 +294,7 @@ func TestOpenLineAboveWithCount(t *testing.T) {
func TestInsertModeEnter(t *testing.T) {
t.Run("test enter splits line", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "i", "enter", "esc")
m := getFinalModel(t, tm)
@ -311,7 +311,7 @@ func TestInsertModeEnter(t *testing.T) {
t.Run("test enter at end of line", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "i", "enter", "esc")
m := getFinalModel(t, tm)
@ -347,7 +347,7 @@ func TestInsertModeEnter(t *testing.T) {
func TestInsertModeBackspace(t *testing.T) {
t.Run("test backspace deletes character", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
sendKeys(tm, "i", "backspace", "esc")
m := getFinalModel(t, tm)
@ -358,7 +358,7 @@ func TestInsertModeBackspace(t *testing.T) {
t.Run("test backspace at start of line joins lines", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "i", "backspace", "esc")
m := getFinalModel(t, tm)
@ -383,7 +383,7 @@ func TestInsertModeBackspace(t *testing.T) {
t.Run("test multiple backspaces", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "i", "backspace", "backspace", "backspace", "esc")
m := getFinalModel(t, tm)
@ -396,7 +396,7 @@ func TestInsertModeBackspace(t *testing.T) {
func TestInsertModeDelete(t *testing.T) {
t.Run("test delete deletes character", func(t *testing.T) {
lines := []string{"world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
sendKeys(tm, "i", "delete", "esc")
m := getFinalModel(t, tm)
@ -407,7 +407,7 @@ func TestInsertModeDelete(t *testing.T) {
t.Run("test delete at end of line joins lines", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "i", "delete", "esc")
m := getFinalModel(t, tm)
@ -435,7 +435,7 @@ func TestInsertModeDelete(t *testing.T) {
t.Run("test delete at end of last line does nothing", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "i", "delete", "esc")
m := getFinalModel(t, tm)
@ -446,7 +446,7 @@ func TestInsertModeDelete(t *testing.T) {
t.Run("test multiple delete", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -460,7 +460,7 @@ func TestInsertModeDelete(t *testing.T) {
func TestInsertModeArrowKeys(t *testing.T) {
t.Run("test left arrow moves cursor left", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
sendKeys(tm, "i", "left", "X", "esc")
m := getFinalModel(t, tm)
@ -471,7 +471,7 @@ func TestInsertModeArrowKeys(t *testing.T) {
t.Run("test right arrow moves cursor right", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
sendKeys(tm, "i", "right", "X", "esc")
m := getFinalModel(t, tm)
@ -482,7 +482,7 @@ func TestInsertModeArrowKeys(t *testing.T) {
t.Run("test up arrow moves cursor up", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 1})
sendKeys(tm, "i", "up", "X", "esc")
m := getFinalModel(t, tm)
@ -496,7 +496,7 @@ func TestInsertModeArrowKeys(t *testing.T) {
t.Run("test down arrow moves cursor down", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "i", "down", "X", "esc")
m := getFinalModel(t, tm)
@ -521,7 +521,7 @@ func TestInsertModeArrowKeys(t *testing.T) {
t.Run("test right arrow at end of line does nothing", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
sendKeys(tm, "a", "right", "X", "esc")
m := getFinalModel(t, tm)
@ -532,7 +532,7 @@ func TestInsertModeArrowKeys(t *testing.T) {
t.Run("test up arrow at first line does nothing", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "i", "up", "X", "esc")
m := getFinalModel(t, tm)
@ -543,7 +543,7 @@ func TestInsertModeArrowKeys(t *testing.T) {
t.Run("test down arrow at last line does nothing", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "i", "down", "X", "esc")
m := getFinalModel(t, tm)
@ -554,7 +554,7 @@ func TestInsertModeArrowKeys(t *testing.T) {
t.Run("test up arrow clamps cursor to shorter line", func(t *testing.T) {
lines := []string{"hi", "hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 1})
sendKeys(tm, "i", "up", "X", "esc")
m := getFinalModel(t, tm)
@ -565,7 +565,7 @@ func TestInsertModeArrowKeys(t *testing.T) {
t.Run("test down arrow clamps cursor to shorter line", func(t *testing.T) {
lines := []string{"hello", "hi"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
sendKeys(tm, "i", "down", "X", "esc")
m := getFinalModel(t, tm)
@ -589,7 +589,7 @@ func TestInsertModeArrowKeys(t *testing.T) {
func TestInsertModeDeletePreviousWord(t *testing.T) {
t.Run("test 'ctrl+w' deletes word", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
@ -603,7 +603,7 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
t.Run("test 'ctrl+w' deletes word with whitespace", func(t *testing.T) {
lines := []string{"hello "}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
@ -617,7 +617,7 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
t.Run("test 'ctrl+w' deletes word until period", func(t *testing.T) {
lines := []string{"hello wo..."}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
@ -631,7 +631,7 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
t.Run("test 'ctrl+w' deletes line when blank", func(t *testing.T) {
lines := []string{"", ""}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
@ -648,7 +648,7 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
t.Run("test 'ctrl+w' deletes all whitespace when line is only whitespace", func(t *testing.T) {
lines := []string{" "}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
@ -682,7 +682,7 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
t.Run("test 'ctrl+w' from middle of word", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
sendKeys(tm, "i", "ctrl+w", "esc")
m := getFinalModel(t, tm)
@ -696,7 +696,7 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
t.Run("test 'ctrl+w' deletes word after punctuation", func(t *testing.T) {
lines := []string{"...hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 7, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 7, Line: 0})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
@ -710,7 +710,7 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
t.Run("test 'ctrl+w' with tabs as whitespace", func(t *testing.T) {
lines := []string{"hello\tworld"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)
@ -724,7 +724,7 @@ func TestInsertModeDeletePreviousWord(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"}
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")
m := getFinalModel(t, tm)
@ -744,7 +744,7 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
t.Run("test 'ctrl+w' with underscore in word", func(t *testing.T) {
lines := []string{"hello_world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm)

View File

@ -3,7 +3,7 @@ package editor
import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
)
func TestMoveDown(t *testing.T) {
@ -64,7 +64,7 @@ func TestMoveDownWithOverflow(t *testing.T) {
lines := []string{"long line", "small"}
t.Run("test 'j' with overflow", func(t *testing.T) {
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 8, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 8, Line: 0})
sendKeys(tm, "j")
m := getFinalModel(t, tm)
@ -75,7 +75,7 @@ func TestMoveDownWithOverflow(t *testing.T) {
})
t.Run("test 'j' without overflow", func(t *testing.T) {
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
sendKeys(tm, "j")
m := getFinalModel(t, tm)
@ -87,7 +87,7 @@ func TestMoveDownWithOverflow(t *testing.T) {
func TestMoveUp(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")
m := getFinalModel(t, tm)
@ -97,7 +97,7 @@ func TestMoveUp(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")
m := getFinalModel(t, tm)
@ -119,7 +119,7 @@ func TestMoveUp(t *testing.T) {
func TestMoveUpWithCount(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")
m := getFinalModel(t, tm)
@ -129,7 +129,7 @@ func TestMoveUpWithCount(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")
m := getFinalModel(t, tm)
@ -143,7 +143,7 @@ func TestMoveUpWithOverflow(t *testing.T) {
lines := []string{"small", "long line"}
t.Run("test 'k' with overflow", func(t *testing.T) {
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 1})
sendKeys(tm, "k")
m := getFinalModel(t, tm)
@ -154,7 +154,7 @@ func TestMoveUpWithOverflow(t *testing.T) {
})
t.Run("test 'k' without overflow", func(t *testing.T) {
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 1})
sendKeys(tm, "k")
m := getFinalModel(t, tm)
@ -224,7 +224,7 @@ func TestMoveRightWithCount(t *testing.T) {
func TestMoveLeft(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")
m := getFinalModel(t, tm)
@ -234,7 +234,7 @@ func TestMoveLeft(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")
m := getFinalModel(t, tm)
@ -256,7 +256,7 @@ func TestMoveLeft(t *testing.T) {
func TestMoveLeftWithCount(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")
m := getFinalModel(t, tm)
@ -266,7 +266,7 @@ func TestMoveLeftWithCount(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")
m := getFinalModel(t, tm)

View File

@ -3,7 +3,7 @@ package editor
import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
)
// --- G and gg Tests ---
@ -20,7 +20,7 @@ func TestMoveToBottom(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")
m := getFinalModel(t, tm)
@ -30,7 +30,7 @@ func TestMoveToBottom(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")
m := getFinalModel(t, tm)
@ -41,7 +41,7 @@ func TestMoveToBottom(t *testing.T) {
t.Run("test 'G' clamps CursorX()", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -68,7 +68,7 @@ func TestMoveToBottom(t *testing.T) {
func TestMoveToTop(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")
m := getFinalModel(t, tm)
@ -78,7 +78,7 @@ func TestMoveToTop(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")
m := getFinalModel(t, tm)
@ -99,7 +99,7 @@ func TestMoveToTop(t *testing.T) {
t.Run("test 'gg' clamps CursorX()", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -117,7 +117,7 @@ func TestMoveToTop(t *testing.T) {
func TestMoveToLineStart(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")
m := getFinalModel(t, tm)
@ -128,7 +128,7 @@ func TestMoveToLineStart(t *testing.T) {
t.Run("test '0' from end of line", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: len(lines[0]), Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: len(lines[0]), Line: 0})
sendKeys(tm, "0")
m := getFinalModel(t, tm)
@ -159,7 +159,7 @@ func TestMoveToLineStart(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")
m := getFinalModel(t, tm)
@ -184,7 +184,7 @@ func TestMoveToLineEnd(t *testing.T) {
t.Run("test '$' from middle of line", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
sendKeys(tm, "$")
m := getFinalModel(t, tm)
@ -196,7 +196,7 @@ func TestMoveToLineEnd(t *testing.T) {
t.Run("test '$' already at end", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: len(lines[0]), Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: len(lines[0]), Line: 0})
sendKeys(tm, "$")
m := getFinalModel(t, tm)
@ -218,7 +218,7 @@ func TestMoveToLineEnd(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, "$")
m := getFinalModel(t, tm)
@ -231,7 +231,7 @@ func TestMoveToLineEnd(t *testing.T) {
func TestMoveToLineContentStart(t *testing.T) {
t.Run("test '_' from middle of line with no leading whitespace", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0})
sendKeys(tm, "_")
m := getFinalModel(t, tm)
@ -242,7 +242,7 @@ func TestMoveToLineContentStart(t *testing.T) {
t.Run("test '_' from middle of line with leading whitespace", func(t *testing.T) {
lines := []string{" hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "_")
m := getFinalModel(t, tm)
@ -253,7 +253,7 @@ func TestMoveToLineContentStart(t *testing.T) {
t.Run("test '_' from start of line with leading whitespace", func(t *testing.T) {
lines := []string{" hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "_")
m := getFinalModel(t, tm)
@ -264,7 +264,7 @@ func TestMoveToLineContentStart(t *testing.T) {
t.Run("test '_' from start of line with no leading whitespace", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "_")
m := getFinalModel(t, tm)
@ -275,7 +275,7 @@ func TestMoveToLineContentStart(t *testing.T) {
t.Run("test '_' from middle of line with only whitespace", func(t *testing.T) {
lines := []string{" "}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "_")
m := getFinalModel(t, tm)
@ -286,7 +286,7 @@ func TestMoveToLineContentStart(t *testing.T) {
t.Run("test '_' from end of line with only whitespace", func(t *testing.T) {
lines := []string{" "}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
sendKeys(tm, "_")
m := getFinalModel(t, tm)
@ -297,7 +297,7 @@ func TestMoveToLineContentStart(t *testing.T) {
t.Run("test '_' on empty line", func(t *testing.T) {
lines := []string{""}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "_")
m := getFinalModel(t, tm)
@ -310,7 +310,7 @@ func TestMoveToLineContentStart(t *testing.T) {
func TestMoveToLineContentStartAlias(t *testing.T) {
t.Run("test '^' from middle of line with no leading whitespace", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0})
sendKeys(tm, "^")
m := getFinalModel(t, tm)
@ -321,7 +321,7 @@ func TestMoveToLineContentStartAlias(t *testing.T) {
t.Run("test '^' from middle of line with leading whitespace", func(t *testing.T) {
lines := []string{" hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "^")
m := getFinalModel(t, tm)
@ -332,7 +332,7 @@ func TestMoveToLineContentStartAlias(t *testing.T) {
t.Run("test '^' from start of line with leading whitespace", func(t *testing.T) {
lines := []string{" hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "^")
m := getFinalModel(t, tm)
@ -343,7 +343,7 @@ func TestMoveToLineContentStartAlias(t *testing.T) {
t.Run("test '^' from start of line with no leading whitespace", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "^")
m := getFinalModel(t, tm)
@ -354,7 +354,7 @@ func TestMoveToLineContentStartAlias(t *testing.T) {
t.Run("test '^' from middle of line with only whitespace", func(t *testing.T) {
lines := []string{" "}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "^")
m := getFinalModel(t, tm)
@ -365,7 +365,7 @@ func TestMoveToLineContentStartAlias(t *testing.T) {
t.Run("test '^' from end of line with only whitespace", func(t *testing.T) {
lines := []string{" "}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
sendKeys(tm, "^")
m := getFinalModel(t, tm)
@ -376,7 +376,7 @@ func TestMoveToLineContentStartAlias(t *testing.T) {
t.Run("test '^' on empty line", func(t *testing.T) {
lines := []string{""}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "^")
m := getFinalModel(t, tm)
@ -395,7 +395,7 @@ func TestMoveToLineContentStartAlias(t *testing.T) {
func TestMoveToColumn(t *testing.T) {
t.Run("test '|' alone goes to column 1 (index 0)", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "|")
m := getFinalModel(t, tm)
@ -407,7 +407,7 @@ func TestMoveToColumn(t *testing.T) {
t.Run("test '1|' goes to column 1 (index 0)", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "1", "|")
m := getFinalModel(t, tm)
@ -418,7 +418,7 @@ func TestMoveToColumn(t *testing.T) {
t.Run("test '5|' goes to column 5 (index 4)", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "5", "|")
m := getFinalModel(t, tm)
@ -430,7 +430,7 @@ func TestMoveToColumn(t *testing.T) {
t.Run("test '10|' goes to column 10 (index 9)", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "1", "0", "|")
m := getFinalModel(t, tm)
@ -442,7 +442,7 @@ func TestMoveToColumn(t *testing.T) {
t.Run("test '|' already at column 1", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "|")
m := getFinalModel(t, tm)
@ -453,7 +453,7 @@ func TestMoveToColumn(t *testing.T) {
t.Run("test '5|' already at column 5", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
sendKeys(tm, "5", "|")
m := getFinalModel(t, tm)
@ -466,7 +466,7 @@ func TestMoveToColumn(t *testing.T) {
func TestMoveToColumnClamp(t *testing.T) {
t.Run("test '20|' clamps to end of short line", func(t *testing.T) {
lines := []string{"hello"} // 5 chars, max index 4
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "2", "0", "|")
m := getFinalModel(t, tm)
@ -478,7 +478,7 @@ func TestMoveToColumnClamp(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
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", "|")
m := getFinalModel(t, tm)
@ -490,7 +490,7 @@ func TestMoveToColumnClamp(t *testing.T) {
t.Run("test '6|' clamps on 5-char line", func(t *testing.T) {
lines := []string{"hello"} // 5 chars
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "6", "|")
m := getFinalModel(t, tm)
@ -502,7 +502,7 @@ func TestMoveToColumnClamp(t *testing.T) {
t.Run("test '|' on empty line stays at 0", func(t *testing.T) {
lines := []string{""}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "|")
m := getFinalModel(t, tm)
@ -513,7 +513,7 @@ func TestMoveToColumnClamp(t *testing.T) {
t.Run("test '5|' on empty line stays at 0", func(t *testing.T) {
lines := []string{""}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "5", "|")
m := getFinalModel(t, tm)
@ -524,7 +524,7 @@ func TestMoveToColumnClamp(t *testing.T) {
t.Run("test '3|' on 2-char line clamps", func(t *testing.T) {
lines := []string{"ab"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "3", "|")
m := getFinalModel(t, tm)
@ -538,7 +538,7 @@ func TestMoveToColumnClamp(t *testing.T) {
func TestMoveToColumnPreservesLine(t *testing.T) {
t.Run("test '|' preserves Y position", func(t *testing.T) {
lines := []string{"line one", "line two", "line three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 1})
sendKeys(tm, "|")
m := getFinalModel(t, tm)
@ -549,7 +549,7 @@ func TestMoveToColumnPreservesLine(t *testing.T) {
t.Run("test '5|' preserves Y position", func(t *testing.T) {
lines := []string{"line one", "line two", "line three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
sendKeys(tm, "5", "|")
m := getFinalModel(t, tm)
@ -560,7 +560,7 @@ func TestMoveToColumnPreservesLine(t *testing.T) {
t.Run("test '|' on different lines", func(t *testing.T) {
lines := []string{"short", "longer line here"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 1})
sendKeys(tm, "|")
m := getFinalModel(t, tm)
@ -576,7 +576,7 @@ func TestMoveToColumnPreservesLine(t *testing.T) {
func TestMoveToColumnWithWhitespace(t *testing.T) {
t.Run("test '5|' with leading whitespace", func(t *testing.T) {
lines := []string{" hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "5", "|")
m := getFinalModel(t, tm)
@ -588,7 +588,7 @@ func TestMoveToColumnWithWhitespace(t *testing.T) {
t.Run("test '3|' lands on whitespace", func(t *testing.T) {
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", "|")
m := getFinalModel(t, tm)
@ -600,7 +600,7 @@ func TestMoveToColumnWithWhitespace(t *testing.T) {
t.Run("test '|' with tabs", func(t *testing.T) {
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, "|")
m := getFinalModel(t, tm)
@ -612,7 +612,7 @@ func TestMoveToColumnWithWhitespace(t *testing.T) {
t.Run("test '2|' with tabs", func(t *testing.T) {
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", "|")
m := getFinalModel(t, tm)
@ -626,7 +626,7 @@ func TestMoveToColumnWithWhitespace(t *testing.T) {
func TestMoveToColumnWithOperator(t *testing.T) {
t.Run("test 'd|' deletes to column 1", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "d", "|")
m := getFinalModel(t, tm)
@ -640,7 +640,7 @@ func TestMoveToColumnWithOperator(t *testing.T) {
t.Run("test 'd5|' deletes to column 5", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "d", "5", "|")
m := getFinalModel(t, tm)
@ -653,7 +653,7 @@ func TestMoveToColumnWithOperator(t *testing.T) {
t.Run("test 'y5|' yanks to column 5", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "y", "5", "|")
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) {
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", "|")
m := getFinalModel(t, tm)
@ -687,7 +687,7 @@ func TestMoveToColumnWithOperator(t *testing.T) {
func TestMoveToColumnInVisualMode(t *testing.T) {
t.Run("test 'v5|' selects to column 5", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "v", "5", "|")
m := getFinalModel(t, tm)
@ -701,7 +701,7 @@ func TestMoveToColumnInVisualMode(t *testing.T) {
t.Run("test 'v|' selects backward to column 1", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "v", "|")
m := getFinalModel(t, tm)
@ -715,7 +715,7 @@ func TestMoveToColumnInVisualMode(t *testing.T) {
t.Run("test 'v5|d' deletes selection to column 5", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0})
sendKeys(tm, "v", "5", "|", "d")
m := getFinalModel(t, tm)

View File

@ -3,7 +3,7 @@ package editor
import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
)
// --- w, e, and b Tests ---
@ -47,7 +47,7 @@ func TestMoveForwardWord(t *testing.T) {
t.Run("test 'w' at end of file preserves position", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "w")
m := getFinalModel(t, tm)
@ -94,7 +94,7 @@ func TestMoveForwardWord(t *testing.T) {
t.Run("test 'w' from middle of word skips only remaining chars", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "w")
m := getFinalModel(t, tm)
@ -129,7 +129,7 @@ func TestMoveToWordEnd(t *testing.T) {
t.Run("test 'e' moves to next line when at end of word", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
sendKeys(tm, "e")
m := getFinalModel(t, tm)
@ -143,7 +143,7 @@ func TestMoveToWordEnd(t *testing.T) {
t.Run("test 'e' at end of file preserves position", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
sendKeys(tm, "e")
m := getFinalModel(t, tm)
@ -201,7 +201,7 @@ func TestMoveToWordEnd(t *testing.T) {
t.Run("test 'e' from middle of word moves to end of current word", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "e")
m := getFinalModel(t, tm)
@ -225,7 +225,7 @@ func TestMoveToWordEnd(t *testing.T) {
func TestMoveBackwardWord(t *testing.T) {
t.Run("test 'b' moves to start of current word", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
sendKeys(tm, "b")
m := getFinalModel(t, tm)
@ -236,7 +236,7 @@ func TestMoveBackwardWord(t *testing.T) {
t.Run("test 'b' at word start moves to end previous", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0})
sendKeys(tm, "b")
m := getFinalModel(t, tm)
@ -247,7 +247,7 @@ func TestMoveBackwardWord(t *testing.T) {
t.Run("test 'bbb' moves to back three word", func(t *testing.T) {
lines := []string{"hello world hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 23, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 23, Line: 0})
sendKeys(tm, "b", "b", "b")
m := getFinalModel(t, tm)
@ -258,7 +258,7 @@ func TestMoveBackwardWord(t *testing.T) {
t.Run("test 'b' moves to prev line when at start of line", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "b")
m := getFinalModel(t, tm)
@ -283,7 +283,7 @@ func TestMoveBackwardWord(t *testing.T) {
t.Run("test 'b' stops at punctuation", func(t *testing.T) {
lines := []string{"hello.world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0})
sendKeys(tm, "b")
m := getFinalModel(t, tm)
@ -294,7 +294,7 @@ func TestMoveBackwardWord(t *testing.T) {
t.Run("test 'b' stops at end of punctuation sequence", func(t *testing.T) {
lines := []string{"..hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "b")
m := getFinalModel(t, tm)
@ -305,7 +305,7 @@ func TestMoveBackwardWord(t *testing.T) {
t.Run("test 'b' stops at end of punctuation sequence before word", func(t *testing.T) {
lines := []string{"hello..world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 7, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 7, Line: 0})
sendKeys(tm, "b")
m := getFinalModel(t, tm)
@ -316,7 +316,7 @@ func TestMoveBackwardWord(t *testing.T) {
t.Run("test 'b' stops at end of punctuation sequence on newline", func(t *testing.T) {
lines := []string{"hello.", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "b")
m := getFinalModel(t, tm)
@ -330,7 +330,7 @@ func TestMoveBackwardWord(t *testing.T) {
t.Run("test 'b' skips underscore", func(t *testing.T) {
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, "b")
m := getFinalModel(t, tm)
@ -394,7 +394,7 @@ func TestMoveForwardWORD(t *testing.T) {
t.Run("test 'W' from middle of WORD", func(t *testing.T) {
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, "W")
m := getFinalModel(t, tm)
@ -406,7 +406,7 @@ func TestMoveForwardWORD(t *testing.T) {
t.Run("test 'W' from last char of WORD", func(t *testing.T) {
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, "W")
m := getFinalModel(t, tm)
@ -418,7 +418,7 @@ func TestMoveForwardWORD(t *testing.T) {
t.Run("test 'W' from space before WORD", func(t *testing.T) {
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, "W")
m := getFinalModel(t, tm)
@ -529,7 +529,7 @@ func TestMoveForwardWORD(t *testing.T) {
t.Run("test 'W' from punctuation to next WORD", func(t *testing.T) {
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, "W")
m := getFinalModel(t, tm)
@ -599,7 +599,7 @@ func TestMoveForwardWORD(t *testing.T) {
t.Run("test 'W' from middle line to next", func(t *testing.T) {
lines := []string{"first", "second third", "fourth"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 7, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 7, Line: 1})
sendKeys(tm, "W")
m := getFinalModel(t, tm)
@ -616,7 +616,7 @@ func TestMoveForwardWORD(t *testing.T) {
t.Run("test 'W' at end of file stays put", func(t *testing.T) {
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, "W")
m := getFinalModel(t, tm)
@ -628,7 +628,7 @@ func TestMoveForwardWORD(t *testing.T) {
t.Run("test 'W' on last WORD of file", func(t *testing.T) {
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, "W")
m := getFinalModel(t, tm)
@ -702,7 +702,7 @@ func TestMoveForwardWORD(t *testing.T) {
t.Run("test 'W' from leading whitespace", func(t *testing.T) {
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, "W")
m := getFinalModel(t, tm)
@ -885,7 +885,7 @@ func TestMoveForwardWORD(t *testing.T) {
t.Run("test 'W' with URL-like text", func(t *testing.T) {
lines := []string{"visit https://example.com/path?q=1 today"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0})
sendKeys(tm, "W")
m := getFinalModel(t, tm)
@ -897,7 +897,7 @@ func TestMoveForwardWORD(t *testing.T) {
t.Run("test 'W' with email-like text", func(t *testing.T) {
lines := []string{"contact user@example.com now"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 8, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 8, Line: 0})
sendKeys(tm, "W")
m := getFinalModel(t, tm)
@ -909,7 +909,7 @@ func TestMoveForwardWORD(t *testing.T) {
t.Run("test 'W' with file path", func(t *testing.T) {
lines := []string{"edit /home/user/file.txt now"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "W")
m := getFinalModel(t, tm)
@ -1009,7 +1009,7 @@ func TestMoveForwardWORDInVisualMode(t *testing.T) {
sendKeys(tm, "v", "W")
m := getFinalModel(t, tm)
if m.Mode() != action.VisualMode {
if m.Mode() != core.VisualMode {
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
}
if m.ActiveWindow().Anchor.Col != 0 {
@ -1054,7 +1054,7 @@ func TestMoveForwardWORDInVisualMode(t *testing.T) {
sendKeys(tm, "V", "W")
m := getFinalModel(t, tm)
if m.Mode() != action.VisualLineMode {
if m.Mode() != core.VisualLineMode {
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
}
// W should still move cursor, but line mode selects whole lines
@ -1114,7 +1114,7 @@ func TestMoveForwardWORDEnd(t *testing.T) {
t.Run("test 'E' from middle of WORD", func(t *testing.T) {
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, "E")
m := getFinalModel(t, tm)
@ -1126,7 +1126,7 @@ func TestMoveForwardWORDEnd(t *testing.T) {
t.Run("test 'E' from last char of WORD moves to next WORD end", func(t *testing.T) {
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, "E")
m := getFinalModel(t, tm)
@ -1138,7 +1138,7 @@ func TestMoveForwardWORDEnd(t *testing.T) {
t.Run("test 'E' from space moves to next WORD end", func(t *testing.T) {
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, "E")
m := getFinalModel(t, tm)
@ -1261,7 +1261,7 @@ func TestMoveForwardWORDEnd(t *testing.T) {
t.Run("test 'E' from punctuation within WORD", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "E")
m := getFinalModel(t, tm)
@ -1277,7 +1277,7 @@ func TestMoveForwardWORDEnd(t *testing.T) {
t.Run("test 'E' moves to next line when at end", func(t *testing.T) {
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, "E")
m := getFinalModel(t, tm)
@ -1637,7 +1637,7 @@ func TestMoveForwardWORDEndInVisualMode(t *testing.T) {
sendKeys(tm, "v", "E")
m := getFinalModel(t, tm)
if m.Mode() != action.VisualMode {
if m.Mode() != core.VisualMode {
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
}
// Cursor at end of "hello.world" (index 10)
@ -1676,7 +1676,7 @@ func TestMoveForwardWORDEndInVisualMode(t *testing.T) {
sendKeys(tm, "V", "E")
m := getFinalModel(t, tm)
if m.Mode() != action.VisualLineMode {
if m.Mode() != core.VisualLineMode {
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
}
// Cursor at end of "hello.world" (index 10)

View File

@ -3,7 +3,7 @@ package editor
import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
)
// =============================================================================
@ -14,12 +14,12 @@ func TestChangeLine(t *testing.T) {
t.Run("cc changes first line and enters insert mode", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello", "world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "c")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().LineCount() != 2 {
@ -37,12 +37,12 @@ func TestChangeLine(t *testing.T) {
t.Run("cc changes middle line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
)
sendKeys(tm, "c", "c")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "line one" {
@ -59,12 +59,12 @@ func TestChangeLine(t *testing.T) {
t.Run("cc changes last line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello", "world"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
)
sendKeys(tm, "c", "c")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "hello" {
@ -78,7 +78,7 @@ func TestChangeLine(t *testing.T) {
t.Run("cc puts deleted line in register", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world", "second line"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "c")
@ -90,7 +90,7 @@ func TestChangeLine(t *testing.T) {
if len(reg.Content) != 1 || reg.Content[0] != "hello world" {
t.Errorf("register content = %q, want 'hello world'", reg.Content)
}
if reg.Type != action.LinewiseRegister {
if reg.Type != core.LinewiseRegister {
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
}
})
@ -98,12 +98,12 @@ func TestChangeLine(t *testing.T) {
t.Run("cc on single line file", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"only line"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "c")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().LineCount() != 1 {
@ -117,7 +117,7 @@ func TestChangeLine(t *testing.T) {
t.Run("cc cursor at column 0", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello", "world"}),
WithCursorPos(action.Position{Line: 0, Col: 3}),
WithCursorPos(core.Position{Line: 0, Col: 3}),
)
sendKeys(tm, "c", "c")
@ -136,12 +136,12 @@ func TestChangeLineWithCount(t *testing.T) {
t.Run("2cc changes two lines", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three", "line four"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "2", "c", "c")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// Should have 3 lines: empty + line three + line four
@ -159,12 +159,12 @@ func TestChangeLineWithCount(t *testing.T) {
t.Run("3cc changes three lines", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"one", "two", "three", "four", "five"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
)
sendKeys(tm, "3", "c", "c")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// Should have 3 lines: one + empty + five
@ -185,12 +185,12 @@ func TestChangeLineWithCount(t *testing.T) {
t.Run("5cc with only 3 lines remaining changes all remaining", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"one", "two", "three"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "5", "c", "c")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().LineCount() != 1 {
@ -204,7 +204,7 @@ func TestChangeLineWithCount(t *testing.T) {
t.Run("2cc puts both lines in register", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"first", "second", "third"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "2", "c", "c")
@ -227,12 +227,12 @@ func TestChangeWithHorizontalMotion(t *testing.T) {
t.Run("cl changes single character", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "l")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "ello world" {
@ -243,12 +243,12 @@ func TestChangeWithHorizontalMotion(t *testing.T) {
t.Run("c2l changes two characters", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "2", "l")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "llo world" {
@ -259,12 +259,12 @@ func TestChangeWithHorizontalMotion(t *testing.T) {
t.Run("ch changes character to the left", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 5}),
WithCursorPos(core.Position{Line: 0, Col: 5}),
)
sendKeys(tm, "c", "h")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "hell world" {
@ -275,12 +275,12 @@ func TestChangeWithHorizontalMotion(t *testing.T) {
t.Run("c$ changes to end of line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 6}),
WithCursorPos(core.Position{Line: 0, Col: 6}),
)
sendKeys(tm, "c", "$")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "hello " {
@ -291,12 +291,12 @@ func TestChangeWithHorizontalMotion(t *testing.T) {
t.Run("c0 changes to start of line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 6}),
WithCursorPos(core.Position{Line: 0, Col: 6}),
)
sendKeys(tm, "c", "0")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "world" {
@ -307,12 +307,12 @@ func TestChangeWithHorizontalMotion(t *testing.T) {
t.Run("c^ changes to first non-blank", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{" hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 8}),
WithCursorPos(core.Position{Line: 0, Col: 8}),
)
sendKeys(tm, "c", "^")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// ^ is exclusive motion, so position 8 (space) is not included
@ -331,12 +331,12 @@ func TestChangeWithWordMotion(t *testing.T) {
t.Run("cw changes word", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "w")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "world" {
@ -347,12 +347,12 @@ func TestChangeWithWordMotion(t *testing.T) {
t.Run("cw from middle of word changes to next word", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 2}),
WithCursorPos(core.Position{Line: 0, Col: 2}),
)
sendKeys(tm, "c", "w")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "heworld" {
@ -363,12 +363,12 @@ func TestChangeWithWordMotion(t *testing.T) {
t.Run("ce changes to end of word", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "e")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != " world" {
@ -379,12 +379,12 @@ func TestChangeWithWordMotion(t *testing.T) {
t.Run("cb changes backward word", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 6}),
WithCursorPos(core.Position{Line: 0, Col: 6}),
)
sendKeys(tm, "c", "b")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "world" {
@ -395,12 +395,12 @@ func TestChangeWithWordMotion(t *testing.T) {
t.Run("c2w changes two words", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"one two three four"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "2", "w")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "three four" {
@ -411,12 +411,12 @@ func TestChangeWithWordMotion(t *testing.T) {
t.Run("cW changes WORD", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello.world next"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "W")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "next" {
@ -427,12 +427,12 @@ func TestChangeWithWordMotion(t *testing.T) {
t.Run("cE changes to end of WORD", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello.world next"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "E")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != " next" {
@ -449,12 +449,12 @@ func TestChangeWithVerticalMotion(t *testing.T) {
t.Run("cj changes current and next line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "j")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// Should have empty line + line three
@ -472,12 +472,12 @@ func TestChangeWithVerticalMotion(t *testing.T) {
t.Run("ck changes current and previous line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
)
sendKeys(tm, "c", "k")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// Should have empty line + line three
@ -495,12 +495,12 @@ func TestChangeWithVerticalMotion(t *testing.T) {
t.Run("c2j changes three lines", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"one", "two", "three", "four", "five"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "2", "j")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// Should have empty + four + five
@ -524,12 +524,12 @@ func TestChangeWithJumpMotion(t *testing.T) {
t.Run("cG changes from cursor to end of file", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "G")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// All lines should be replaced with one empty line
@ -544,12 +544,12 @@ func TestChangeWithJumpMotion(t *testing.T) {
t.Run("cgg changes from cursor to start of file", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three"}),
WithCursorPos(action.Position{Line: 2, Col: 0}),
WithCursorPos(core.Position{Line: 2, Col: 0}),
)
sendKeys(tm, "c", "g", "g")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// All lines should be replaced with one empty line
@ -564,12 +564,12 @@ func TestChangeWithJumpMotion(t *testing.T) {
t.Run("cG from middle changes to end", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three", "line four"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
)
sendKeys(tm, "c", "G")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// Should have line one + empty
@ -593,12 +593,12 @@ func TestChangeToEndOfLine(t *testing.T) {
t.Run("C changes from cursor to end of line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 6}),
WithCursorPos(core.Position{Line: 0, Col: 6}),
)
sendKeys(tm, "C")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "hello " {
@ -609,12 +609,12 @@ func TestChangeToEndOfLine(t *testing.T) {
t.Run("C at start of line clears entire line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "C")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "" {
@ -625,12 +625,12 @@ func TestChangeToEndOfLine(t *testing.T) {
t.Run("C at end of line enters insert mode", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 4}),
WithCursorPos(core.Position{Line: 0, Col: 4}),
)
sendKeys(tm, "C")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// Should delete last char
@ -642,7 +642,7 @@ func TestChangeToEndOfLine(t *testing.T) {
t.Run("C puts deleted text in register", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 6}),
WithCursorPos(core.Position{Line: 0, Col: 6}),
)
sendKeys(tm, "C")
@ -665,12 +665,12 @@ func TestSubstituteCharacter(t *testing.T) {
t.Run("s deletes character and enters insert mode", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "s")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "ello" {
@ -681,12 +681,12 @@ func TestSubstituteCharacter(t *testing.T) {
t.Run("2s deletes two characters", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "2", "s")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "llo" {
@ -697,12 +697,12 @@ func TestSubstituteCharacter(t *testing.T) {
t.Run("s at end of line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 4}),
WithCursorPos(core.Position{Line: 0, Col: 4}),
)
sendKeys(tm, "s")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "hell" {
@ -719,12 +719,12 @@ func TestSubstituteLine(t *testing.T) {
t.Run("S clears line and enters insert mode", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 5}),
WithCursorPos(core.Position{Line: 0, Col: 5}),
)
sendKeys(tm, "S")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "" {
@ -735,7 +735,7 @@ func TestSubstituteLine(t *testing.T) {
t.Run("S preserves other lines", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
)
sendKeys(tm, "S")
@ -754,12 +754,12 @@ func TestSubstituteLine(t *testing.T) {
t.Run("2S substitutes two lines", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"one", "two", "three", "four"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "2", "S")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// Should have empty + three + four
@ -783,12 +783,12 @@ func TestVisualModeChange(t *testing.T) {
t.Run("vc changes single character", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "v", "c")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "ello world" {
@ -799,12 +799,12 @@ func TestVisualModeChange(t *testing.T) {
t.Run("v with motion then c changes selection", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "v", "e", "c") // select "hello"
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != " world" {
@ -815,12 +815,12 @@ func TestVisualModeChange(t *testing.T) {
t.Run("v$c changes to end of line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 6}),
WithCursorPos(core.Position{Line: 0, Col: 6}),
)
sendKeys(tm, "v", "$", "c")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "hello " {
@ -831,12 +831,12 @@ func TestVisualModeChange(t *testing.T) {
t.Run("visual selection spanning lines then c", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three"}),
WithCursorPos(action.Position{Line: 0, Col: 5}),
WithCursorPos(core.Position{Line: 0, Col: 5}),
)
sendKeys(tm, "v", "j", "l", "l", "l", "l", "c") // select across lines
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// Should merge lines with selection removed
@ -848,7 +848,7 @@ func TestVisualModeChange(t *testing.T) {
t.Run("visual change puts deleted text in register", func(t *testing.T) {
tm := newTestModel(t,
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", "c") // select "hello"
@ -867,12 +867,12 @@ func TestVisualLineModeChange(t *testing.T) {
t.Run("Vc changes line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
)
sendKeys(tm, "V", "c")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().LineCount() != 3 {
@ -886,12 +886,12 @@ func TestVisualLineModeChange(t *testing.T) {
t.Run("Vjc changes multiple lines", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three", "line four"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
)
sendKeys(tm, "V", "j", "c") // select lines two and three
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// Should have: line one, empty, line four
@ -912,7 +912,7 @@ func TestVisualLineModeChange(t *testing.T) {
t.Run("visual line change puts lines in register", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "V", "j", "c") // select first two lines
@ -921,7 +921,7 @@ func TestVisualLineModeChange(t *testing.T) {
if !ok {
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)
}
if len(reg.Content) != 2 {
@ -938,12 +938,12 @@ func TestChangeEdgeCases(t *testing.T) {
t.Run("cc on empty line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"", "world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "c")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "" {
@ -954,12 +954,12 @@ func TestChangeEdgeCases(t *testing.T) {
t.Run("cw on last word of line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 6}),
WithCursorPos(core.Position{Line: 0, Col: 6}),
)
sendKeys(tm, "c", "w")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
// cw on last word should change to end of line
@ -971,12 +971,12 @@ func TestChangeEdgeCases(t *testing.T) {
t.Run("c$ on empty line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{""}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "$")
m := getFinalModel(t, tm)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0] != "" {
@ -987,13 +987,13 @@ func TestChangeEdgeCases(t *testing.T) {
t.Run("cj at last line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello", "world"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
)
sendKeys(tm, "c", "j")
m := getFinalModel(t, tm)
// cj at last line should just change current line (can't go down)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
})
@ -1001,13 +1001,13 @@ func TestChangeEdgeCases(t *testing.T) {
t.Run("ck at first line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello", "world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "c", "k")
m := getFinalModel(t, tm)
// ck at first line should just change current line (can't go up)
if m.Mode() != action.InsertMode {
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
})

View File

@ -3,7 +3,7 @@ package editor
import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
)
// NOTE: AI Generated tests
@ -31,7 +31,7 @@ func TestDeleteLine(t *testing.T) {
t.Run("test 'dd' deletes middle line", func(t *testing.T) {
lines := []string{"hello", "world", "testing"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "d", "d")
m := getFinalModel(t, tm)
@ -54,7 +54,7 @@ func TestDeleteLine(t *testing.T) {
t.Run("test 'dd' deletes last line", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "d", "d")
m := getFinalModel(t, tm)
@ -74,7 +74,7 @@ func TestDeleteLine(t *testing.T) {
t.Run("test 'dd' deletes line and preserves column", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
sendKeys(tm, "d", "d")
m := getFinalModel(t, tm)
@ -94,7 +94,7 @@ func TestDeleteLine(t *testing.T) {
t.Run("test '3dd' deletes three lines", func(t *testing.T) {
lines := []string{"hello", "world", "testing", "line", "another line"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "3", "d", "d")
m := getFinalModel(t, tm)
@ -158,7 +158,7 @@ func TestDeleteLine(t *testing.T) {
t.Run("test 'dd' clamps cursor when next line is shorter", func(t *testing.T) {
lines := []string{"hello world", "hi"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 7, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 7, Line: 0})
sendKeys(tm, "d", "d")
m := getFinalModel(t, tm)
@ -183,7 +183,7 @@ func TestDeleteLine(t *testing.T) {
t.Run("test '3dd' starting near end of file", func(t *testing.T) {
lines := []string{"hello", "world", "testing"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "3", "d", "d")
m := getFinalModel(t, tm)
@ -217,7 +217,7 @@ func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
t.Run("test 'dl' deletes current character from middle", func(t *testing.T) {
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, "d", "l")
m := getFinalModel(t, tm)
@ -231,7 +231,7 @@ func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
t.Run("test 'dl' deletes current character from end", func(t *testing.T) {
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", "l")
m := getFinalModel(t, tm)
@ -259,7 +259,7 @@ func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
t.Run("test 'dh' deletes character to the left from middle", func(t *testing.T) {
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, "d", "h")
m := getFinalModel(t, tm)
@ -274,7 +274,7 @@ func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
t.Run("test 'dh' deletes character to the left from end", func(t *testing.T) {
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", "h")
m := getFinalModel(t, tm)
@ -311,7 +311,7 @@ func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
t.Run("test '2dh' deletes two characters backwards", func(t *testing.T) {
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, "2", "d", "h")
m := getFinalModel(t, tm)
@ -324,7 +324,7 @@ func TestDeleteOperatorWithHorozontalMotion(t *testing.T) {
func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
t.Run("test 'dj' deletes current and next line", func(t *testing.T) {
lines := []string{"line 1", "line 2", "line 3", "line 4"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "d", "j")
m := getFinalModel(t, tm)
@ -363,7 +363,7 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
// linewise motions consistently - they operate on at least the current line.
t.Run("test 'dj' from last line deletes current line", func(t *testing.T) {
lines := []string{"line 1", "line 2", "line 3"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
sendKeys(tm, "d", "j")
m := getFinalModel(t, tm)
@ -380,7 +380,7 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
t.Run("test 'd2j' deletes current and next two lines", func(t *testing.T) {
lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "d", "2", "j")
m := getFinalModel(t, tm)
@ -397,7 +397,7 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
t.Run("test '2dj' deletes current and next two lines", func(t *testing.T) {
lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "2", "d", "j")
m := getFinalModel(t, tm)
@ -414,7 +414,7 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
t.Run("test 'dk' deletes current and previous line", func(t *testing.T) {
lines := []string{"line 1", "line 2", "line 3", "line 4"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
sendKeys(tm, "d", "k")
m := getFinalModel(t, tm)
@ -453,7 +453,7 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
t.Run("test 'dk' from second line", func(t *testing.T) {
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", "k")
m := getFinalModel(t, tm)
@ -470,7 +470,7 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
t.Run("test 'd2k' deletes current and previous two lines", func(t *testing.T) {
lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 3})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 3})
sendKeys(tm, "d", "2", "k")
m := getFinalModel(t, tm)
@ -487,7 +487,7 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
t.Run("test '2dk' deletes current and previous two lines", func(t *testing.T) {
lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 3})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 3})
sendKeys(tm, "2", "d", "k")
m := getFinalModel(t, tm)
@ -504,7 +504,7 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
t.Run("test 'dj' with count exceeding remaining lines", func(t *testing.T) {
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", "5", "j")
m := getFinalModel(t, tm)
@ -519,7 +519,7 @@ func TestDeleteOperatorWithVerticalMotion(t *testing.T) {
t.Run("test 'dk' with count exceeding previous lines", func(t *testing.T) {
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", "5", "k")
m := getFinalModel(t, tm)
@ -551,7 +551,7 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
t.Run("test 'dw' deletes from middle of word to start of next word", func(t *testing.T) {
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", "w")
m := getFinalModel(t, tm)
@ -565,7 +565,7 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
t.Run("test 'dw' at last word deletes to end of line", func(t *testing.T) {
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, "d", "w")
m := getFinalModel(t, tm)
@ -626,7 +626,7 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
t.Run("test 'de' deletes to end of word from middle (inclusive)", func(t *testing.T) {
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", "e")
m := getFinalModel(t, tm)
@ -638,7 +638,7 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
t.Run("test 'de' at end of word jumps to next word end", func(t *testing.T) {
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, "d", "e")
m := getFinalModel(t, tm)
@ -674,7 +674,7 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
t.Run("test 'db' deletes back to start of current word", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 8, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 8, Line: 0})
sendKeys(tm, "d", "b")
m := getFinalModel(t, tm)
@ -686,7 +686,7 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
t.Run("test 'db' at start of word deletes previous word", func(t *testing.T) {
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, "d", "b")
m := getFinalModel(t, tm)
@ -698,7 +698,7 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
t.Run("test '2db' deletes two words backward", func(t *testing.T) {
lines := []string{"one two three four"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 14, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 14, Line: 0})
sendKeys(tm, "2", "d", "b")
m := getFinalModel(t, tm)
@ -710,7 +710,7 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
t.Run("test 'db' at end of line", func(t *testing.T) {
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, "d", "b")
m := getFinalModel(t, tm)
@ -736,7 +736,7 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
t.Run("test 'd0' deletes from cursor to start of line", func(t *testing.T) {
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, "d", "0")
m := getFinalModel(t, tm)
@ -750,7 +750,7 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
t.Run("test 'd0' from end of line", func(t *testing.T) {
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, "d", "0")
m := getFinalModel(t, tm)
@ -761,7 +761,7 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
t.Run("test 'd0' from middle of line", func(t *testing.T) {
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, "d", "0")
m := getFinalModel(t, tm)
@ -784,7 +784,7 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
t.Run("test 'd$' deletes to end of line from middle", func(t *testing.T) {
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, "d", "$")
m := getFinalModel(t, tm)
@ -795,7 +795,7 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
t.Run("test 'd$' at end of line deletes last char", func(t *testing.T) {
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, "d", "$")
m := getFinalModel(t, tm)
@ -806,7 +806,7 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
t.Run("test 'd$' does not affect other lines", func(t *testing.T) {
lines := []string{"hello world", "second line"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0})
sendKeys(tm, "d", "$")
m := getFinalModel(t, tm)
@ -853,7 +853,7 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
t.Run("test 'd_' from middle whitespace deletes to first non-blank", func(t *testing.T) {
lines := []string{" hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
sendKeys(tm, "d", "_")
m := getFinalModel(t, tm)
@ -868,7 +868,7 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
t.Run("test 'd_' from after first char deletes back to first non-whitespace", func(t *testing.T) {
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, "d", "_")
m := getFinalModel(t, tm)
@ -881,7 +881,7 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
t.Run("test 'd_' on line with no leading whitespace", func(t *testing.T) {
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", "_")
m := getFinalModel(t, tm)
@ -896,7 +896,7 @@ func TestDeleteOperatorWithLinePositionMotion(t *testing.T) {
func TestDeleteOperatorWithJumpMotion(t *testing.T) {
t.Run("test 'dG' deletes from cursor to end of file", func(t *testing.T) {
lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
sendKeys(tm, "d", "G")
m := getFinalModel(t, tm)
@ -927,7 +927,7 @@ func TestDeleteOperatorWithJumpMotion(t *testing.T) {
t.Run("test 'dG' from last line deletes only last line", func(t *testing.T) {
lines := []string{"line 1", "line 2", "line 3"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
sendKeys(tm, "d", "G")
m := getFinalModel(t, tm)
@ -944,7 +944,7 @@ func TestDeleteOperatorWithJumpMotion(t *testing.T) {
t.Run("test 'dG' positions cursor correctly", func(t *testing.T) {
lines := []string{"line 1", "line 2", "line 3", "line 4"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 1})
sendKeys(tm, "d", "G")
m := getFinalModel(t, tm)
@ -955,7 +955,7 @@ func TestDeleteOperatorWithJumpMotion(t *testing.T) {
t.Run("test 'dgg' deletes from cursor to start of file", func(t *testing.T) {
lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
sendKeys(tm, "d", "g", "g")
m := getFinalModel(t, tm)
@ -972,7 +972,7 @@ func TestDeleteOperatorWithJumpMotion(t *testing.T) {
t.Run("test 'dgg' from last line deletes everything", func(t *testing.T) {
lines := []string{"line 1", "line 2", "line 3"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
sendKeys(tm, "d", "g", "g")
m := getFinalModel(t, tm)
@ -1003,7 +1003,7 @@ func TestDeleteOperatorWithJumpMotion(t *testing.T) {
t.Run("test 'dgg' positions cursor at start", func(t *testing.T) {
lines := []string{"line 1", "line 2", "line 3", "line 4"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 2})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 2})
sendKeys(tm, "d", "g", "g")
m := getFinalModel(t, tm)
@ -1042,7 +1042,7 @@ func TestDeleteOperatorWithJumpMotion(t *testing.T) {
t.Run("test 'dG' clamps cursor when file shrinks", func(t *testing.T) {
lines := []string{"short", "this is a longer line", "line 3", "line 4"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 1})
sendKeys(tm, "d", "G")
m := getFinalModel(t, tm)

View File

@ -3,15 +3,15 @@ package editor
import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
)
func TestPasteLinewiseBasic(t *testing.T) {
t.Run("p pastes single line after cursor line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"inserted"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"inserted"}),
)
sendKeys(tm, "p")
@ -33,8 +33,8 @@ func TestPasteLinewiseBasic(t *testing.T) {
t.Run("p moves cursor to first pasted line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2", "line 3"}),
WithCursorPos(action.Position{Line: 0, Col: 5}),
WithRegister('"', action.LinewiseRegister, []string{"inserted"}),
WithCursorPos(core.Position{Line: 0, Col: 5}),
WithRegister('"', core.LinewiseRegister, []string{"inserted"}),
)
sendKeys(tm, "p")
@ -47,8 +47,8 @@ func TestPasteLinewiseBasic(t *testing.T) {
t.Run("p from middle of file", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2", "line 3"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"inserted"}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"inserted"}),
)
sendKeys(tm, "p")
@ -67,8 +67,8 @@ func TestPasteLinewiseBasic(t *testing.T) {
t.Run("p at end of file", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"inserted"}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"inserted"}),
)
sendKeys(tm, "p")
@ -86,8 +86,8 @@ func TestPasteLinewiseMultipleLines(t *testing.T) {
t.Run("p pastes multiple lines in correct order", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"first", "second", "third"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"first", "second", "third"}),
)
sendKeys(tm, "p")
@ -115,8 +115,8 @@ func TestPasteLinewiseMultipleLines(t *testing.T) {
t.Run("p with multiple lines moves cursor to first pasted line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"first", "second"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"first", "second"}),
)
sendKeys(tm, "p")
@ -131,8 +131,8 @@ func TestPasteLinewiseWithCount(t *testing.T) {
t.Run("2p pastes content twice", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"inserted"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"inserted"}),
)
sendKeys(tm, "2", "p")
@ -155,8 +155,8 @@ func TestPasteLinewiseWithCount(t *testing.T) {
t.Run("3p pastes content three times", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"original"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"pasted"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"pasted"}),
)
sendKeys(tm, "3", "p")
@ -174,8 +174,8 @@ func TestPasteLinewiseWithCount(t *testing.T) {
t.Run("2p with multiple lines pastes all lines twice in order", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"original"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"first", "second"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"first", "second"}),
)
sendKeys(tm, "2", "p")
@ -204,8 +204,8 @@ func TestPasteLinewiseWithCount(t *testing.T) {
t.Run("count paste moves cursor to first pasted line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"inserted"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"inserted"}),
)
sendKeys(tm, "3", "p")
@ -222,8 +222,8 @@ func TestPasteBeforeLinewiseBasic(t *testing.T) {
t.Run("P pastes single line before cursor line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"inserted"}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"inserted"}),
)
sendKeys(tm, "P")
@ -245,8 +245,8 @@ func TestPasteBeforeLinewiseBasic(t *testing.T) {
t.Run("P moves cursor to first pasted line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2", "line 3"}),
WithCursorPos(action.Position{Line: 1, Col: 5}),
WithRegister('"', action.LinewiseRegister, []string{"inserted"}),
WithCursorPos(core.Position{Line: 1, Col: 5}),
WithRegister('"', core.LinewiseRegister, []string{"inserted"}),
)
sendKeys(tm, "P")
@ -259,8 +259,8 @@ func TestPasteBeforeLinewiseBasic(t *testing.T) {
t.Run("P at first line pastes at very top", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"inserted"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"inserted"}),
)
sendKeys(tm, "P")
@ -282,8 +282,8 @@ func TestPasteBeforeLinewiseBasic(t *testing.T) {
t.Run("P from middle of file", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2", "line 3"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"inserted"}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"inserted"}),
)
sendKeys(tm, "P")
@ -304,8 +304,8 @@ func TestPasteBeforeLinewiseMultipleLines(t *testing.T) {
t.Run("P pastes multiple lines in correct order", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"first", "second", "third"}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"first", "second", "third"}),
)
sendKeys(tm, "P")
@ -333,8 +333,8 @@ func TestPasteBeforeLinewiseMultipleLines(t *testing.T) {
t.Run("P with multiple lines moves cursor to first pasted line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"first", "second"}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"first", "second"}),
)
sendKeys(tm, "P")
@ -349,8 +349,8 @@ func TestPasteBeforeLinewiseWithCount(t *testing.T) {
t.Run("2P pastes content twice", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"inserted"}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"inserted"}),
)
sendKeys(tm, "2", "P")
@ -373,8 +373,8 @@ func TestPasteBeforeLinewiseWithCount(t *testing.T) {
t.Run("3P pastes content three times", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"original"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"pasted"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"pasted"}),
)
sendKeys(tm, "3", "P")
@ -395,8 +395,8 @@ func TestPasteBeforeLinewiseWithCount(t *testing.T) {
t.Run("2P with multiple lines pastes all lines twice in order", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"original"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"first", "second"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"first", "second"}),
)
sendKeys(tm, "2", "P")
@ -425,8 +425,8 @@ func TestPasteBeforeLinewiseWithCount(t *testing.T) {
t.Run("count paste before moves cursor to first pasted line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"inserted"}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"inserted"}),
)
sendKeys(tm, "3", "P")
@ -441,8 +441,8 @@ func TestPasteBeforeLinewiseEdgeCases(t *testing.T) {
t.Run("P on single line buffer", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"only line"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"inserted"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"inserted"}),
)
sendKeys(tm, "P")
@ -461,8 +461,8 @@ func TestPasteBeforeLinewiseEdgeCases(t *testing.T) {
t.Run("P with empty register content does nothing", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{}),
)
sendKeys(tm, "P")
@ -475,8 +475,8 @@ func TestPasteBeforeLinewiseEdgeCases(t *testing.T) {
t.Run("P preserves indentation in pasted lines", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{" indented", "\ttabbed"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{" indented", "\ttabbed"}),
)
sendKeys(tm, "P")
@ -492,8 +492,8 @@ func TestPasteBeforeLinewiseEdgeCases(t *testing.T) {
t.Run("P with large count", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"original"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"x"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"x"}),
)
sendKeys(tm, "1", "0", "P") // 10P
@ -512,8 +512,8 @@ func TestPasteLinewiseEdgeCases(t *testing.T) {
t.Run("p on single line buffer", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"only line"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"inserted"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"inserted"}),
)
sendKeys(tm, "p")
@ -532,8 +532,8 @@ func TestPasteLinewiseEdgeCases(t *testing.T) {
t.Run("p with empty register content does nothing", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{}),
)
sendKeys(tm, "p")
@ -546,8 +546,8 @@ func TestPasteLinewiseEdgeCases(t *testing.T) {
t.Run("p preserves indentation in pasted lines", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{" indented", "\ttabbed"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{" indented", "\ttabbed"}),
)
sendKeys(tm, "p")
@ -563,8 +563,8 @@ func TestPasteLinewiseEdgeCases(t *testing.T) {
t.Run("p with large count", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"original"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"x"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"x"}),
)
sendKeys(tm, "1", "0", "p") // 10p
@ -583,8 +583,8 @@ func TestPasteCharwiseBasic(t *testing.T) {
t.Run("p pastes text after cursor", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 4}), // on 'o'
WithRegister('"', action.CharwiseRegister, []string{"XYZ"}),
WithCursorPos(core.Position{Line: 0, Col: 4}), // on 'o'
WithRegister('"', core.CharwiseRegister, []string{"XYZ"}),
)
sendKeys(tm, "p")
@ -598,8 +598,8 @@ func TestPasteCharwiseBasic(t *testing.T) {
t.Run("p at start of line pastes after first char", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"X"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"X"}),
)
sendKeys(tm, "p")
@ -612,8 +612,8 @@ func TestPasteCharwiseBasic(t *testing.T) {
t.Run("p at end of line pastes at end", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 4}), // on 'o'
WithRegister('"', action.CharwiseRegister, []string{"!"}),
WithCursorPos(core.Position{Line: 0, Col: 4}), // on 'o'
WithRegister('"', core.CharwiseRegister, []string{"!"}),
)
sendKeys(tm, "p")
@ -626,8 +626,8 @@ func TestPasteCharwiseBasic(t *testing.T) {
t.Run("p on empty line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{""}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"text"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"text"}),
)
sendKeys(tm, "p")
@ -640,8 +640,8 @@ func TestPasteCharwiseBasic(t *testing.T) {
t.Run("p does not add new lines", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello", "world"}),
WithCursorPos(action.Position{Line: 0, Col: 2}),
WithRegister('"', action.CharwiseRegister, []string{"X"}),
WithCursorPos(core.Position{Line: 0, Col: 2}),
WithRegister('"', core.CharwiseRegister, []string{"X"}),
)
sendKeys(tm, "p")
@ -656,8 +656,8 @@ func TestPasteCharwiseWithCount(t *testing.T) {
t.Run("2p pastes content twice", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"X"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"X"}),
)
sendKeys(tm, "2", "p")
@ -670,8 +670,8 @@ func TestPasteCharwiseWithCount(t *testing.T) {
t.Run("3p pastes word three times", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"start end"}),
WithCursorPos(action.Position{Line: 0, Col: 4}), // on 't'
WithRegister('"', action.CharwiseRegister, []string{"-"}),
WithCursorPos(core.Position{Line: 0, Col: 4}), // on 't'
WithRegister('"', core.CharwiseRegister, []string{"-"}),
)
sendKeys(tm, "3", "p")
@ -684,8 +684,8 @@ func TestPasteCharwiseWithCount(t *testing.T) {
t.Run("count paste with multi-char content", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"ab"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"XY"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"XY"}),
)
sendKeys(tm, "2", "p")
@ -700,8 +700,8 @@ func TestPasteCharwiseCursorPosition(t *testing.T) {
t.Run("p moves cursor to end of pasted text", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"XYZ"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"XYZ"}),
)
sendKeys(tm, "p")
@ -715,8 +715,8 @@ func TestPasteCharwiseCursorPosition(t *testing.T) {
t.Run("p cursor stays on same line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"X"}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"X"}),
)
sendKeys(tm, "p")
@ -735,8 +735,8 @@ func TestPasteBeforeCharwiseBasic(t *testing.T) {
t.Run("P pastes text before cursor", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 5}), // on space
WithRegister('"', action.CharwiseRegister, []string{"XYZ"}),
WithCursorPos(core.Position{Line: 0, Col: 5}), // on space
WithRegister('"', core.CharwiseRegister, []string{"XYZ"}),
)
sendKeys(tm, "P")
@ -750,8 +750,8 @@ func TestPasteBeforeCharwiseBasic(t *testing.T) {
t.Run("P at start of line pastes at beginning", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"X"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"X"}),
)
sendKeys(tm, "P")
@ -764,8 +764,8 @@ func TestPasteBeforeCharwiseBasic(t *testing.T) {
t.Run("P at end of line pastes before last char", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 4}), // on 'o'
WithRegister('"', action.CharwiseRegister, []string{"X"}),
WithCursorPos(core.Position{Line: 0, Col: 4}), // on 'o'
WithRegister('"', core.CharwiseRegister, []string{"X"}),
)
sendKeys(tm, "P")
@ -778,8 +778,8 @@ func TestPasteBeforeCharwiseBasic(t *testing.T) {
t.Run("P on empty line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{""}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"text"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"text"}),
)
sendKeys(tm, "P")
@ -794,8 +794,8 @@ func TestPasteBeforeCharwiseWithCount(t *testing.T) {
t.Run("2P pastes content twice", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 2}), // on first 'l'
WithRegister('"', action.CharwiseRegister, []string{"X"}),
WithCursorPos(core.Position{Line: 0, Col: 2}), // on first 'l'
WithRegister('"', core.CharwiseRegister, []string{"X"}),
)
sendKeys(tm, "2", "P")
@ -808,8 +808,8 @@ func TestPasteBeforeCharwiseWithCount(t *testing.T) {
t.Run("3P pastes word three times", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"ab"}),
WithCursorPos(action.Position{Line: 0, Col: 1}), // on 'b'
WithRegister('"', action.CharwiseRegister, []string{"-"}),
WithCursorPos(core.Position{Line: 0, Col: 1}), // on 'b'
WithRegister('"', core.CharwiseRegister, []string{"-"}),
)
sendKeys(tm, "3", "P")
@ -828,8 +828,8 @@ func TestPasteCharwiseMultiLine(t *testing.T) {
t.Run("p with multi-line charwise content errors gracefully", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"line1", "line2"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"line1", "line2"}),
)
sendKeys(tm, "p")
@ -843,8 +843,8 @@ func TestPasteCharwiseMultiLine(t *testing.T) {
t.Run("P with multi-line charwise content errors gracefully", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"line1", "line2"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"line1", "line2"}),
)
sendKeys(tm, "P")
@ -864,8 +864,8 @@ func TestPasteBlockwiseBasic(t *testing.T) {
t.Run("p with blockwise content errors gracefully", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"aaaa", "bbbb", "cccc"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.BlockwiseRegister, []string{"XX", "YY", "ZZ"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.BlockwiseRegister, []string{"XX", "YY", "ZZ"}),
)
sendKeys(tm, "p")
@ -879,8 +879,8 @@ func TestPasteBlockwiseBasic(t *testing.T) {
t.Run("P with blockwise content errors gracefully", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"aaaa", "bbbb", "cccc"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.BlockwiseRegister, []string{"XX", "YY", "ZZ"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.BlockwiseRegister, []string{"XX", "YY", "ZZ"}),
)
sendKeys(tm, "P")
@ -900,8 +900,8 @@ func TestPasteCharwiseEdgeCases(t *testing.T) {
t.Run("p with empty charwise register does nothing", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{}),
)
sendKeys(tm, "p")
@ -914,8 +914,8 @@ func TestPasteCharwiseEdgeCases(t *testing.T) {
t.Run("p with empty string in register", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{""}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{""}),
)
sendKeys(tm, "p")
@ -928,8 +928,8 @@ func TestPasteCharwiseEdgeCases(t *testing.T) {
t.Run("p preserves special characters", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"ab"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"\t"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"\t"}),
)
sendKeys(tm, "p")
@ -942,8 +942,8 @@ func TestPasteCharwiseEdgeCases(t *testing.T) {
t.Run("p with spaces", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"ab"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{" "}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{" "}),
)
sendKeys(tm, "p")
@ -956,8 +956,8 @@ func TestPasteCharwiseEdgeCases(t *testing.T) {
t.Run("p on line with only whitespace", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{" "}),
WithCursorPos(action.Position{Line: 0, Col: 2}),
WithRegister('"', action.CharwiseRegister, []string{"X"}),
WithCursorPos(core.Position{Line: 0, Col: 2}),
WithRegister('"', core.CharwiseRegister, []string{"X"}),
)
sendKeys(tm, "p")
@ -970,8 +970,8 @@ func TestPasteCharwiseEdgeCases(t *testing.T) {
t.Run("P with empty charwise register does nothing", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{}),
)
sendKeys(tm, "P")
@ -984,8 +984,8 @@ func TestPasteCharwiseEdgeCases(t *testing.T) {
t.Run("large count paste", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"ab"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"X"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"X"}),
)
sendKeys(tm, "1", "0", "p") // 10p
@ -1004,7 +1004,7 @@ func TestYankThenPasteCharwise(t *testing.T) {
t.Run("yw then p pastes yanked word", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "y", "w") // yank "hello "
sendKeys(tm, "$") // go to end
@ -1019,7 +1019,7 @@ func TestYankThenPasteCharwise(t *testing.T) {
t.Run("ye then p pastes yanked word", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "y", "e") // yank "hello"
sendKeys(tm, "$") // go to end
@ -1034,7 +1034,7 @@ func TestYankThenPasteCharwise(t *testing.T) {
t.Run("visual select then y then p", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "v", "l", "l", "y") // select and yank "hel"
sendKeys(tm, "$") // go to end
@ -1061,8 +1061,8 @@ func TestVisualModePasteCharwise(t *testing.T) {
t.Run("vp replaces selection with charwise register", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"REPLACED"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"REPLACED"}),
)
sendKeys(tm, "v", "l", "l", "l", "l", "p") // select "hello", paste
@ -1075,8 +1075,8 @@ func TestVisualModePasteCharwise(t *testing.T) {
t.Run("vp replaces single character", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"X"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"X"}),
)
sendKeys(tm, "v", "p") // select "h", paste
@ -1089,8 +1089,8 @@ func TestVisualModePasteCharwise(t *testing.T) {
t.Run("vp replaces word in middle of line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world goodbye"}),
WithCursorPos(action.Position{Line: 0, Col: 6}),
WithRegister('"', action.CharwiseRegister, []string{"EARTH"}),
WithCursorPos(core.Position{Line: 0, Col: 6}),
WithRegister('"', core.CharwiseRegister, []string{"EARTH"}),
)
sendKeys(tm, "v", "e", "p") // select "world", paste
@ -1103,8 +1103,8 @@ func TestVisualModePasteCharwise(t *testing.T) {
t.Run("vp replaces to end of line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 6}),
WithRegister('"', action.CharwiseRegister, []string{"universe"}),
WithCursorPos(core.Position{Line: 0, Col: 6}),
WithRegister('"', core.CharwiseRegister, []string{"universe"}),
)
sendKeys(tm, "v", "$", "p") // select "world", paste
@ -1117,8 +1117,8 @@ func TestVisualModePasteCharwise(t *testing.T) {
t.Run("vp with empty register deletes selection", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{""}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{""}),
)
sendKeys(tm, "v", "l", "l", "l", "l", "p") // select "hello", paste empty
@ -1135,8 +1135,8 @@ func TestVisualModePasteCharwise(t *testing.T) {
t.Run("vp replaces selection spanning multiple lines", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three"}),
WithCursorPos(action.Position{Line: 0, Col: 5}),
WithRegister('"', action.CharwiseRegister, []string{"REPLACED"}),
WithCursorPos(core.Position{Line: 0, Col: 5}),
WithRegister('"', core.CharwiseRegister, []string{"REPLACED"}),
)
// v at (0,5), j goes to (1,5), h goes to (1,4) = space after "line"
// Selection: "one\nline " (from 'o' to space)
@ -1159,8 +1159,8 @@ func TestVisualModePasteCharwise(t *testing.T) {
t.Run("vp cursor at start of pasted content", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 6}),
WithRegister('"', action.CharwiseRegister, []string{"EARTH"}),
WithCursorPos(core.Position{Line: 0, Col: 6}),
WithRegister('"', core.CharwiseRegister, []string{"EARTH"}),
)
sendKeys(tm, "v", "e", "p") // select "world", paste "EARTH"
@ -1175,13 +1175,13 @@ func TestVisualModePasteCharwise(t *testing.T) {
t.Run("vp exits visual mode", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"X"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"X"}),
)
sendKeys(tm, "v", "l", "p")
m := getFinalModel(t, tm)
if m.Mode() != action.NormalMode {
if m.Mode() != core.NormalMode {
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
}
})
@ -1193,8 +1193,8 @@ func TestVisualModePasteCharwise(t *testing.T) {
t.Run("vp with linewise register replaces selection", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"NEW LINE"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"NEW LINE"}),
)
sendKeys(tm, "v", "l", "l", "l", "l", "p") // select "hello", paste linewise content
@ -1214,8 +1214,8 @@ func TestVisualLinePaste(t *testing.T) {
t.Run("Vp replaces line with charwise register", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"REPLACED"}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"REPLACED"}),
)
sendKeys(tm, "V", "p") // select line two, paste
@ -1231,8 +1231,8 @@ func TestVisualLinePaste(t *testing.T) {
t.Run("Vp replaces line with linewise register", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"NEW LINE"}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"NEW LINE"}),
)
sendKeys(tm, "V", "p") // select line two, paste
@ -1248,8 +1248,8 @@ func TestVisualLinePaste(t *testing.T) {
t.Run("Vp replaces multiple lines with linewise register", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three", "line four"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"REPLACED"}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"REPLACED"}),
)
sendKeys(tm, "V", "j", "p") // select lines two and three, paste
@ -1271,8 +1271,8 @@ func TestVisualLinePaste(t *testing.T) {
t.Run("Vp replaces multiple lines with multiple linewise register lines", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two", "line three", "line four"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"NEW A", "NEW B"}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"NEW A", "NEW B"}),
)
sendKeys(tm, "V", "j", "p") // select lines two and three, paste two lines
@ -1297,8 +1297,8 @@ func TestVisualLinePaste(t *testing.T) {
t.Run("Vp on first line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"FIRST"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"FIRST"}),
)
sendKeys(tm, "V", "p")
@ -1317,8 +1317,8 @@ func TestVisualLinePaste(t *testing.T) {
t.Run("Vp on last line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"LAST"}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"LAST"}),
)
sendKeys(tm, "V", "p")
@ -1337,13 +1337,13 @@ func TestVisualLinePaste(t *testing.T) {
t.Run("Vp exits visual line mode", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"REPLACED"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"REPLACED"}),
)
sendKeys(tm, "V", "p")
m := getFinalModel(t, tm)
if m.Mode() != action.NormalMode {
if m.Mode() != core.NormalMode {
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
}
})
@ -1357,8 +1357,8 @@ func TestVisualPasteRegisterBehavior(t *testing.T) {
t.Run("vp puts deleted text into register", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"NEW"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"NEW"}),
)
sendKeys(tm, "v", "l", "l", "l", "l", "p") // select "hello", paste "NEW"
@ -1376,8 +1376,8 @@ func TestVisualPasteRegisterBehavior(t *testing.T) {
t.Run("Vp puts deleted line into register", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line one", "line two"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"REPLACED"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"REPLACED"}),
)
sendKeys(tm, "V", "p")
@ -1389,7 +1389,7 @@ func TestVisualPasteRegisterBehavior(t *testing.T) {
if len(reg.Content) != 1 || reg.Content[0] != "line one" {
t.Errorf("register content = %q, want 'line one'", reg.Content)
}
if reg.Type != action.LinewiseRegister {
if reg.Type != core.LinewiseRegister {
t.Errorf("register type = %v, want LinewiseRegister", reg.Type)
}
})
@ -1403,8 +1403,8 @@ func TestVisualPasteEdgeCases(t *testing.T) {
t.Run("vp on empty line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"", "line two"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"NEW"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"NEW"}),
)
sendKeys(tm, "v", "p")
@ -1417,8 +1417,8 @@ func TestVisualPasteEdgeCases(t *testing.T) {
t.Run("vp selecting entire line content", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello", "world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"REPLACED"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"REPLACED"}),
)
sendKeys(tm, "v", "$", "p") // select entire "hello"
@ -1431,8 +1431,8 @@ func TestVisualPasteEdgeCases(t *testing.T) {
t.Run("vp with backwards selection", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 5}),
WithRegister('"', action.CharwiseRegister, []string{"X"}),
WithCursorPos(core.Position{Line: 0, Col: 5}),
WithRegister('"', core.CharwiseRegister, []string{"X"}),
)
sendKeys(tm, "v", "h", "h", "h", "h", "h", "p") // select backwards "hello ", paste
@ -1445,8 +1445,8 @@ func TestVisualPasteEdgeCases(t *testing.T) {
t.Run("vp in single character file", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"a"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.CharwiseRegister, []string{"XYZ"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"XYZ"}),
)
sendKeys(tm, "v", "p")
@ -1459,8 +1459,8 @@ func TestVisualPasteEdgeCases(t *testing.T) {
t.Run("Vp on single line file", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"only line"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithRegister('"', action.LinewiseRegister, []string{"REPLACED"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.LinewiseRegister, []string{"REPLACED"}),
)
sendKeys(tm, "V", "p")

View File

@ -4,7 +4,7 @@ import (
"fmt"
"testing"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
)
// NOTE: AI Generated tests
@ -22,7 +22,7 @@ func TestScrollBasic(t *testing.T) {
t.Run("small file does not scroll", func(t *testing.T) {
// 10 lines, viewport 24 -> no scrolling needed
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
m := getFinalModel(t, tm)
@ -37,7 +37,7 @@ func TestScrollBasic(t *testing.T) {
// Safe zone: lines 8 to 10 (19-1-8=10)
// Moving to line 11+ should trigger scroll
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
for range 15 {
@ -59,7 +59,7 @@ func TestScrollBasic(t *testing.T) {
t.Run("scrolls up when cursor moves past top margin", func(t *testing.T) {
// Start at line 20, move up
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)
// Then move up 15 times
@ -80,7 +80,7 @@ func TestScrollBasic(t *testing.T) {
t.Run("G jumps to bottom and scrolls", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -96,7 +96,7 @@ func TestScrollBasic(t *testing.T) {
t.Run("gg jumps to top and scrolls", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -112,7 +112,7 @@ func TestScrollBasic(t *testing.T) {
func TestScrollEdgeCases(t *testing.T) {
t.Run("scrollY never goes negative", func(t *testing.T) {
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
for range 5 {
@ -127,7 +127,7 @@ func TestScrollEdgeCases(t *testing.T) {
t.Run("scrollY clamped to max scroll", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -140,7 +140,7 @@ func TestScrollEdgeCases(t *testing.T) {
t.Run("cursor stays visible after delete at bottom", func(t *testing.T) {
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
sendKeys(tm, "d", "d", "d", "d")
@ -162,7 +162,7 @@ func TestHalfPageScrollDown(t *testing.T) {
// cursor at line 15 (relY=15, in safe zone), scrollY starts at 0
// After ctrl+d: newScrollY=14, newCursorY=14+15=29
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")
m := getFinalModel(t, tm)
@ -177,7 +177,7 @@ func TestHalfPageScrollDown(t *testing.T) {
t.Run("ctrl+d preserves cursor relative position in viewport", func(t *testing.T) {
// relY=15 before and after
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")
m := getFinalModel(t, tm)
@ -191,7 +191,7 @@ func TestHalfPageScrollDown(t *testing.T) {
// cursor at line 0 (relY=0 < scrollOff=8), clamp to scrollOff
// After ctrl+d: newScrollY=14, newCursorY=14+8=22
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")
m := getFinalModel(t, tm)
@ -208,7 +208,7 @@ func TestHalfPageScrollDown(t *testing.T) {
// 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
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")
m := getFinalModel(t, tm)
@ -225,7 +225,7 @@ func TestHalfPageScrollDown(t *testing.T) {
// 20 lines < viewport 28: maxScroll=0, scrollY stays 0
// relY=0 < scrollOff, clamp to 8; newCursorY=0+8=8
lines := generateLines(20)
tm := newTestModelWithTermSize(t, lines, action.Position{Col: 0, Line: 0}, 80, 30)
tm := newTestModelWithTermSize(t, lines, core.Position{Col: 0, Line: 0}, 80, 30)
sendKeys(tm, "ctrl+d")
m := getFinalModel(t, tm)
@ -249,7 +249,7 @@ func TestHalfPageScrollDown(t *testing.T) {
for i := 15; i < 100; i++ {
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")
m := getFinalModel(t, tm)
@ -266,7 +266,7 @@ func TestHalfPageScrollDown(t *testing.T) {
// ctrl+d #1: scrollY=14, cursorY=29, relY=15
// ctrl+d #2: scrollY=28, cursorY=43, relY=15
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")
m := getFinalModel(t, tm)
@ -284,7 +284,7 @@ func TestHalfPageScrollUp(t *testing.T) {
// cursor at line 50: AdjustScroll -> scrollY=31, relY=19
// After ctrl+u: newScrollY=17, newCursorY=17+19=36
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")
m := getFinalModel(t, tm)
@ -299,7 +299,7 @@ func TestHalfPageScrollUp(t *testing.T) {
t.Run("ctrl+u preserves cursor relative position in viewport", func(t *testing.T) {
// relY=19 before and after
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")
m := getFinalModel(t, tm)
@ -313,7 +313,7 @@ func TestHalfPageScrollUp(t *testing.T) {
// cursor at line 10, scrollY=0, relY=10 (in safe zone)
// ctrl+u: newScrollY=max(0,-14)=0, relY=10 preserved, cursorY=10
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")
m := getFinalModel(t, tm)
@ -332,7 +332,7 @@ func TestHalfPageScrollUp(t *testing.T) {
// cursor at line 5, scrollY=0, relY=5 < scrollOff=8
// ctrl+u: newScrollY=0; relY clamp to 8; newCursorY=8
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")
m := getFinalModel(t, tm)
@ -349,7 +349,7 @@ func TestHalfPageScrollUp(t *testing.T) {
// ctrl+u #1: newScrollY=47, cursorY=66
// ctrl+u #2: newScrollY=33, cursorY=52
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")
m := getFinalModel(t, tm)
@ -368,7 +368,7 @@ func TestHalfPageScrollRoundTrip(t *testing.T) {
// ctrl+d: scrollY=14, cursorY=29, relY=15
// ctrl+u: newScrollY=max(0,14-14)=0, cursorY=0+15=15
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")
m := getFinalModel(t, tm)
@ -385,7 +385,7 @@ func TestHalfPageScrollRoundTrip(t *testing.T) {
// ctrl+u: scrollY=17, cursorY=36, relY=19
// ctrl+d: scrollY=31, cursorY=50, relY=19
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")
m := getFinalModel(t, tm)
@ -399,7 +399,7 @@ func TestHalfPageScrollRoundTrip(t *testing.T) {
t.Run("alternating ctrl+d and ctrl+u maintains scroll stability", func(t *testing.T) {
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")
m := getFinalModel(t, tm)
@ -415,7 +415,7 @@ func TestHalfPageScrollRoundTrip(t *testing.T) {
func TestScrollWithCount(t *testing.T) {
t.Run("5j scrolls appropriately", func(t *testing.T) {
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
@ -431,7 +431,7 @@ func TestScrollWithCount(t *testing.T) {
t.Run("5k scrolls appropriately", func(t *testing.T) {
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

View File

@ -3,7 +3,7 @@ package editor
import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
)
// NOTE: Lots of AI tests here
@ -13,11 +13,11 @@ import (
func TestVisualModeSelectionState(t *testing.T) {
t.Run("test 'v' enters visual mode and sets anchor at cursor", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
sendKeys(tm, "v")
m := getFinalModel(t, tm)
if m.Mode() != action.VisualMode {
if m.Mode() != core.VisualMode {
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
}
if m.ActiveWindow().Anchor.Col != 3 {
@ -44,7 +44,7 @@ func TestVisualModeSelectionState(t *testing.T) {
t.Run("test 'vh' creates backward selection", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
sendKeys(tm, "v", "h")
m := getFinalModel(t, tm)
@ -58,7 +58,7 @@ func TestVisualModeSelectionState(t *testing.T) {
t.Run("test 'vj' extends selection down", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "v", "j")
m := getFinalModel(t, tm)
@ -75,11 +75,11 @@ func TestVisualModeSelectionState(t *testing.T) {
t.Run("test 'V' enters visual line mode and sets anchor at cursor", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "V")
m := getFinalModel(t, tm)
if m.Mode() != action.VisualLineMode {
if m.Mode() != core.VisualLineMode {
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
}
if m.ActiveWindow().Anchor.Line != 1 {
@ -89,11 +89,11 @@ func TestVisualModeSelectionState(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"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 1})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 1})
sendKeys(tm, "ctrl+v")
m := getFinalModel(t, tm)
if m.Mode() != action.VisualBlockMode {
if m.Mode() != core.VisualBlockMode {
t.Errorf("Mode() = %v, want VisualBlockMode", m.Mode())
}
if m.ActiveWindow().Anchor.Col != 2 {
@ -110,7 +110,7 @@ func TestVisualModeSelectionState(t *testing.T) {
sendKeys(tm, "v", "l", "l", "esc")
m := getFinalModel(t, tm)
if m.Mode() != action.NormalMode {
if m.Mode() != core.NormalMode {
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
}
})
@ -149,7 +149,7 @@ func TestVisualModeDelete(t *testing.T) {
t.Run("test 'v' backward selection 'hh d' deletes correct range", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
sendKeys(tm, "v", "h", "h", "d")
// anchor=3, cursor=1 → normalized start=1, end=3 → delete "ell" → "ho"
@ -164,7 +164,7 @@ func TestVisualModeDelete(t *testing.T) {
t.Run("test 'vj d' deletes char selection across two lines", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "v", "j", "d")
// start=(2,0), end=(2,1) → prefix="he", suffix="ld" → "held"
@ -219,7 +219,7 @@ func TestVisualModeDelete(t *testing.T) {
t.Run("test 'Vkd' deletes two lines with backward selection", func(t *testing.T) {
lines := []string{"hello", "world", "testing"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 2})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
sendKeys(tm, "V", "k", "d")
// anchor=line2, cursor=line1 → normalized start=line1, end=line2 → delete both
@ -257,7 +257,7 @@ func TestVisualModeDelete(t *testing.T) {
t.Run("test 'ctrl+v' backward col selection deletes correct block", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0})
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0})
sendKeys(tm, "ctrl+v", "h", "h", "j", "d")
// anchor=(3,0), cursor=(1,1) → cols min(3,1)=1 to max(3,1)=3, lines 0-1
@ -279,7 +279,7 @@ func TestVisualModeWordMotions(t *testing.T) {
t.Run("test 'vw' selects to next word", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "v", "w")
@ -296,7 +296,7 @@ func TestVisualModeWordMotions(t *testing.T) {
t.Run("test 'vwd' deletes word plus space", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "v", "w", "d")
@ -310,7 +310,7 @@ func TestVisualModeWordMotions(t *testing.T) {
t.Run("test 've' selects to end of word", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "v", "e")
@ -327,7 +327,7 @@ func TestVisualModeWordMotions(t *testing.T) {
t.Run("test 'ved' deletes word", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "v", "e", "d")
@ -341,7 +341,7 @@ func TestVisualModeWordMotions(t *testing.T) {
t.Run("test 'vb' selects backward word", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -358,7 +358,7 @@ func TestVisualModeWordMotions(t *testing.T) {
t.Run("test 'vbd' deletes backward to word start", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -372,7 +372,7 @@ func TestVisualModeWordMotions(t *testing.T) {
t.Run("test 'v2w' selects two words", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"one two three"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "v", "2", "w")
@ -390,7 +390,7 @@ func TestVisualModeJumpMotions(t *testing.T) {
t.Run("test 'v$' selects to end of line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "v", "$")
@ -407,7 +407,7 @@ func TestVisualModeJumpMotions(t *testing.T) {
t.Run("test 'v$d' deletes to end of line", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -420,7 +420,7 @@ func TestVisualModeJumpMotions(t *testing.T) {
t.Run("test 'v0' selects to beginning of line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 6}),
WithCursorPos(core.Position{Line: 0, Col: 6}),
)
sendKeys(tm, "v", "0")
@ -436,7 +436,7 @@ func TestVisualModeJumpMotions(t *testing.T) {
t.Run("test 'v0d' deletes to beginning of line", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -450,7 +450,7 @@ func TestVisualModeJumpMotions(t *testing.T) {
t.Run("test 'v_' selects to first non-whitespace", func(t *testing.T) {
tm := newTestModel(t,
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", "_")
@ -467,7 +467,7 @@ func TestVisualModeJumpMotions(t *testing.T) {
t.Run("test 'vG' selects to bottom of file", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -483,7 +483,7 @@ func TestVisualModeJumpMotions(t *testing.T) {
t.Run("test 'vGd' deletes to bottom of file", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -501,7 +501,7 @@ func TestVisualModeJumpMotions(t *testing.T) {
t.Run("test 'vgg' selects to top of file", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -517,7 +517,7 @@ func TestVisualModeJumpMotions(t *testing.T) {
t.Run("test 'vggd' deletes to top of file", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -539,7 +539,7 @@ func TestVisualLineModeJumpMotions(t *testing.T) {
t.Run("test 'VG' selects all lines to bottom", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -555,7 +555,7 @@ func TestVisualLineModeJumpMotions(t *testing.T) {
t.Run("test 'VGd' deletes all lines", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -572,7 +572,7 @@ func TestVisualLineModeJumpMotions(t *testing.T) {
t.Run("test 'Vgg' selects lines to top", func(t *testing.T) {
tm := newTestModel(t,
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")

View File

@ -4,7 +4,7 @@ import (
"strings"
"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) {
tm := newTestModel(t,
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")
@ -24,7 +24,7 @@ func TestYankLineBasic(t *testing.T) {
if !ok {
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)
}
if len(reg.Content) != 1 {
@ -38,7 +38,7 @@ func TestYankLineBasic(t *testing.T) {
t.Run("yy does not modify buffer", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -60,7 +60,7 @@ func TestYankLineBasic(t *testing.T) {
t.Run("yy does not move cursor", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -76,7 +76,7 @@ func TestYankLineBasic(t *testing.T) {
t.Run("yy from middle of file", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"first", "second", "third", "fourth"}),
WithCursorPos(action.Position{Line: 2, Col: 0}),
WithCursorPos(core.Position{Line: 2, Col: 0}),
)
sendKeys(tm, "y", "y")
@ -90,7 +90,7 @@ func TestYankLineBasic(t *testing.T) {
t.Run("yy at last line", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -106,7 +106,7 @@ func TestYankLineWithCount(t *testing.T) {
t.Run("2yy yanks two lines", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -126,7 +126,7 @@ func TestYankLineWithCount(t *testing.T) {
t.Run("3yy yanks three lines", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -149,7 +149,7 @@ func TestYankLineWithCount(t *testing.T) {
t.Run("yy with count overflow clamps to available lines", func(t *testing.T) {
tm := newTestModel(t,
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
@ -169,7 +169,7 @@ func TestYankLineWithCount(t *testing.T) {
t.Run("yy with count does not modify buffer", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -184,7 +184,7 @@ func TestYankLineEdgeCases(t *testing.T) {
t.Run("yy on empty line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "", "line 3"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
)
sendKeys(tm, "y", "y")
@ -201,7 +201,7 @@ func TestYankLineEdgeCases(t *testing.T) {
t.Run("yy on single line buffer", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"only line"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "y", "y")
@ -215,7 +215,7 @@ func TestYankLineEdgeCases(t *testing.T) {
t.Run("yy preserves whitespace", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{" indented", "\ttabbed", " spaces "}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
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) {
tm := newTestModel(t,
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")
@ -250,7 +250,7 @@ func TestYankWithLinewiseMotions(t *testing.T) {
if !ok {
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)
}
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) {
tm := newTestModel(t,
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")
@ -276,7 +276,7 @@ func TestYankWithLinewiseMotions(t *testing.T) {
if !ok {
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)
}
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) {
tm := newTestModel(t,
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")
@ -302,7 +302,7 @@ func TestYankWithLinewiseMotions(t *testing.T) {
if !ok {
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)
}
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) {
tm := newTestModel(t,
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")
@ -328,7 +328,7 @@ func TestYankWithLinewiseMotions(t *testing.T) {
if !ok {
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)
}
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) {
tm := newTestModel(t,
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")
@ -371,7 +371,7 @@ func TestYankWithLinewiseMotions(t *testing.T) {
t.Run("yj does not move cursor", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -384,7 +384,7 @@ func TestYankWithLinewiseMotions(t *testing.T) {
t.Run("yG does not modify buffer", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -403,7 +403,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
t.Run("yw yanks word under cursor", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "y", "w")
@ -412,7 +412,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
if !ok {
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)
}
// 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) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "y", "e")
@ -436,7 +436,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
if !ok {
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)
}
// 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) {
tm := newTestModel(t,
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")
@ -460,7 +460,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
if !ok {
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)
}
// 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) {
tm := newTestModel(t,
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", "$")
@ -484,7 +484,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
if !ok {
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)
}
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) {
tm := newTestModel(t,
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")
@ -507,7 +507,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
if !ok {
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)
}
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) {
tm := newTestModel(t,
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", "_")
@ -530,7 +530,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
if !ok {
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)
}
// 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) {
tm := newTestModel(t,
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")
@ -565,7 +565,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
t.Run("yw does not move cursor", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "y", "w")
@ -578,7 +578,7 @@ func TestYankWithCharwiseMotions(t *testing.T) {
t.Run("yw does not modify buffer", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "y", "w")
@ -597,7 +597,7 @@ func TestYankVisualCharwise(t *testing.T) {
t.Run("v selection then y yanks selected text", func(t *testing.T) {
tm := newTestModel(t,
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"
@ -606,7 +606,7 @@ func TestYankVisualCharwise(t *testing.T) {
if !ok {
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)
}
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) {
tm := newTestModel(t,
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"
@ -629,7 +629,7 @@ func TestYankVisualCharwise(t *testing.T) {
if !ok {
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)
}
// Multi-line charwise yank
@ -641,12 +641,12 @@ func TestYankVisualCharwise(t *testing.T) {
t.Run("visual yank exits visual mode", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "v", "l", "l", "y")
m := getFinalModel(t, tm)
if m.Mode() != action.NormalMode {
if m.Mode() != core.NormalMode {
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
}
})
@ -654,7 +654,7 @@ func TestYankVisualCharwise(t *testing.T) {
t.Run("visual yank does not modify buffer", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -669,7 +669,7 @@ func TestYankVisualLinewise(t *testing.T) {
t.Run("V selection then y yanks entire lines", func(t *testing.T) {
tm := newTestModel(t,
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
@ -678,7 +678,7 @@ func TestYankVisualLinewise(t *testing.T) {
if !ok {
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)
}
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) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "V", "y")
@ -704,7 +704,7 @@ func TestYankVisualLinewise(t *testing.T) {
if !ok {
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)
}
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) {
tm := newTestModel(t,
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
@ -742,12 +742,12 @@ func TestYankVisualLinewise(t *testing.T) {
t.Run("visual line yank exits visual mode", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "V", "y")
m := getFinalModel(t, tm)
if m.Mode() != action.NormalMode {
if m.Mode() != core.NormalMode {
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) {
tm := newTestModel(t,
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
@ -766,7 +766,7 @@ func TestYankVisualBlock(t *testing.T) {
if !ok {
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)
}
// 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) {
tm := newTestModel(t,
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")
m := getFinalModel(t, tm)
if m.Mode() != action.NormalMode {
if m.Mode() != core.NormalMode {
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) {
tm := newTestModel(t,
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
@ -824,7 +824,7 @@ func TestYankRegisterBehavior(t *testing.T) {
t.Run("yy updates register 0 and unnamed register", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", "line 2"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "y", "y")
@ -858,7 +858,7 @@ func TestYankRegisterBehavior(t *testing.T) {
t.Run("multiple yanks shift numbered registers", func(t *testing.T) {
tm := newTestModel(t,
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, "j")
@ -894,7 +894,7 @@ func TestYankRegisterBehavior(t *testing.T) {
t.Run("yank then paste uses correct content", func(t *testing.T) {
tm := newTestModel(t,
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, "k") // move up
@ -918,7 +918,7 @@ func TestYankEdgeCases(t *testing.T) {
t.Run("yy on whitespace-only line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"line 1", " ", "line 3"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
)
sendKeys(tm, "y", "y")
@ -938,7 +938,7 @@ func TestYankEdgeCases(t *testing.T) {
t.Run("yw at end of line", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -959,7 +959,7 @@ func TestYankEdgeCases(t *testing.T) {
t.Run("y$ at beginning of line yanks entire line content", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello world"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
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) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "y", "0")
@ -1001,7 +1001,7 @@ func TestYankEdgeCases(t *testing.T) {
longLine := strings.Repeat("a", 1000)
tm := newTestModel(t,
WithLines([]string{longLine}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "y", "y")
@ -1021,7 +1021,7 @@ func TestYankEdgeCases(t *testing.T) {
t.Run("yy with special characters", func(t *testing.T) {
tm := newTestModel(t,
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")
@ -1047,7 +1047,7 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
t.Run("visual charwise yank then paste single line", func(t *testing.T) {
tm := newTestModel(t,
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
sendKeys(tm, "v", "l", "l", "l", "l", "y", "$", "p")
@ -1061,7 +1061,7 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
t.Run("visual charwise yank then paste before", func(t *testing.T) {
tm := newTestModel(t,
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
sendKeys(tm, "v", "$", "y", "0", "P")
@ -1075,7 +1075,7 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
t.Run("visual line yank then paste", func(t *testing.T) {
tm := newTestModel(t,
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
sendKeys(tm, "V", "y", "j", "p")
@ -1092,7 +1092,7 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
t.Run("visual line yank multiple lines then paste", func(t *testing.T) {
tm := newTestModel(t,
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
sendKeys(tm, "V", "j", "y", "G", "p")
@ -1112,7 +1112,7 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
t.Run("visual line yank then paste before", func(t *testing.T) {
tm := newTestModel(t,
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
sendKeys(tm, "V", "y", "g", "g", "P")
@ -1132,7 +1132,7 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
t.Run("yy then p duplicates line below", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"original", "other"}),
WithCursorPos(action.Position{Line: 0, Col: 0}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
)
sendKeys(tm, "y", "y", "p")
@ -1154,7 +1154,7 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
t.Run("yy then P duplicates line above", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"original", "other"}),
WithCursorPos(action.Position{Line: 1, Col: 0}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
)
sendKeys(tm, "y", "y", "P")
@ -1176,7 +1176,7 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
t.Run("yw then p pastes word after cursor", func(t *testing.T) {
tm := newTestModel(t,
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
sendKeys(tm, "y", "w", "$", "p")
@ -1190,7 +1190,7 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
t.Run("ye then p pastes word after cursor", func(t *testing.T) {
tm := newTestModel(t,
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
sendKeys(tm, "y", "e", "$", "p")
@ -1204,7 +1204,7 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
t.Run("visual select partial word yank then paste", func(t *testing.T) {
tm := newTestModel(t,
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
sendKeys(tm, "v", "l", "l", "y", "$", "p")
@ -1218,7 +1218,7 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
t.Run("visual yank empty selection does nothing", func(t *testing.T) {
tm := newTestModel(t,
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)
sendKeys(tm, "v", "y")
@ -1237,7 +1237,7 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
t.Run("dd then p moves deleted line down", func(t *testing.T) {
tm := newTestModel(t,
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)
sendKeys(tm, "d", "d", "p")
@ -1260,7 +1260,7 @@ func TestVisualYankPasteRoundTrip(t *testing.T) {
t.Run("2yy then 2p pastes twice", func(t *testing.T) {
tm := newTestModel(t,
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
sendKeys(tm, "2", "y", "y", "2", "p")

View File

@ -5,21 +5,23 @@ import (
"strings"
"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/style"
tea "github.com/charmbracelet/bubbletea"
)
type Model struct {
// Buffers
buffers []*action.Buffer
buffers []*core.Buffer
//next buffer id?
// Windows
windows []*action.Window
windows []*core.Window
activeWindowId int
// Editor wide state
mode action.Mode
mode core.Mode
// Terminal dimensions
termWidth int
@ -40,13 +42,13 @@ type Model struct {
commandOutput string
// Global settings (TODO: This needs to be refactored)
settings action.Settings
settings core.Settings
// Registers
registers map[rune]action.Register // name -> register
registers map[rune]core.Register // name -> register
// Visual styles
styles Styles
styles style.Styles
}
// Model.Init: Initialize the model and start any commands that may need to run. Required
@ -60,11 +62,11 @@ func (m Model) Init() tea.Cmd {
// ==================================================
// Core Data Access
// ==================================================
func (m *Model) Windows() []*action.Window {
func (m *Model) Windows() []*core.Window {
return m.windows
}
func (m *Model) ActiveWindow() *action.Window {
func (m *Model) ActiveWindow() *core.Window {
winId := m.activeWindowId
for i := range m.Windows() {
if m.windows[i].Id == winId {
@ -74,11 +76,11 @@ func (m *Model) ActiveWindow() *action.Window {
panic("Could not find window")
}
func (m *Model) Buffers() []*action.Buffer {
func (m *Model) Buffers() []*core.Buffer {
return m.buffers
}
func (m *Model) ActiveBuffer() *action.Buffer {
func (m *Model) ActiveBuffer() *core.Buffer {
win := m.ActiveWindow()
return win.Buffer
}
@ -108,7 +110,7 @@ func (m *Model) ExitInsertMode() {
if win.Cursor.Col > 0 {
win.Cursor.Col--
}
m.mode = action.NormalMode
m.mode = core.NormalMode
m.insertCount = 0
m.insertKeys = nil
}
@ -272,57 +274,57 @@ func (m *Model) SetCommandOutput(out string) {
// ==================================================
// Editor-wide State
// ==================================================
func (m *Model) Mode() action.Mode {
func (m *Model) Mode() core.Mode {
return m.mode
}
func (m *Model) SetMode(mode action.Mode) {
func (m *Model) SetMode(mode core.Mode) {
m.mode = mode
}
func (m *Model) Settings() action.Settings {
func (m *Model) Settings() core.Settings {
return m.settings
}
func (m *Model) SetSettings(s action.Settings) {
func (m *Model) SetSettings(s core.Settings) {
m.settings = s
}
// Model.Styles: Returns the visual styles used for rendering.
func (m *Model) Styles() Styles {
func (m *Model) Styles() style.Styles {
return m.styles
}
// Model.SetStyles: Sets the visual styles used for rendering.
func (m *Model) SetStyles(s Styles) {
func (m *Model) SetStyles(s style.Styles) {
m.styles = s
}
// ==================================================
// Registers
// ==================================================
func (m *Model) Registers() map[rune]action.Register {
func (m *Model) Registers() map[rune]core.Register {
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]
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 {
return fmt.Errorf("Register '%c' does not exist.", name)
}
// 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
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)
for i := rune('9'); i > '0'; i-- {
m.registers[i] = m.registers[i-1]

View File

@ -1,8 +1,9 @@
package editor
import (
"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/style"
)
type ModelBuilder struct {
@ -12,10 +13,10 @@ type ModelBuilder struct {
func NewModelBuilder() *ModelBuilder {
return &ModelBuilder{
model: Model{
buffers: []*action.Buffer{},
windows: []*action.Window{},
buffers: []*core.Buffer{},
windows: []*core.Window{},
activeWindowId: -1,
mode: action.NormalMode,
mode: core.NormalMode,
termWidth: 0,
termHeight: 0,
input: input.NewHandler(),
@ -26,35 +27,35 @@ func NewModelBuilder() *ModelBuilder {
commandCursor: 0,
commandError: nil,
commandOutput: "",
settings: action.NewDefaultSettings(),
registers: action.DefaultRegisters(),
styles: DefaultStyles(),
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 []*action.Buffer) *ModelBuilder {
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 *action.Buffer) *ModelBuilder {
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 []*action.Window) *ModelBuilder {
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 *action.Window) *ModelBuilder {
func (mb *ModelBuilder) AddWindow(window *core.Window) *ModelBuilder {
mb.model.windows = append(mb.model.windows, window)
return mb
}
@ -67,7 +68,7 @@ func (mb *ModelBuilder) WithActiveWindowId(id int) *ModelBuilder {
}
// ModelBuilder.WithMode: Set the editor mode (Normal, Insert, Visual, etc).
func (mb *ModelBuilder) WithMode(mode action.Mode) *ModelBuilder {
func (mb *ModelBuilder) WithMode(mode core.Mode) *ModelBuilder {
mb.model.mode = mode
return mb
}
@ -92,13 +93,13 @@ func (mb *ModelBuilder) WithTermHeight(height int) *ModelBuilder {
}
// ModelBuilder.WithSettings: Set the editor settings (tabstop, scrolloff, etc).
func (mb *ModelBuilder) WithSettings(settings action.Settings) *ModelBuilder {
func (mb *ModelBuilder) WithSettings(settings core.Settings) *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]action.Register) *ModelBuilder {
func (mb *ModelBuilder) WithRegisters(registers map[rune]core.Register) *ModelBuilder {
mb.model.registers = registers
return mb
}
@ -128,7 +129,7 @@ func (mb *ModelBuilder) WithCommandOutput(output string) *ModelBuilder {
}
// ModelBuilder.WithStyles: Set the visual styling for the editor.
func (mb *ModelBuilder) WithStyles(styles Styles) *ModelBuilder {
func (mb *ModelBuilder) WithStyles(styles style.Styles) *ModelBuilder {
mb.model.styles = styles
return mb
}

View File

@ -4,6 +4,8 @@ import (
tea "github.com/charmbracelet/bubbletea"
)
// Model.Update: Handles BubbleTea messages including window resizes and key
// presses. Routes input to the handler and adjusts scroll after updates.
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd

View File

@ -4,19 +4,21 @@ import (
"fmt"
"strings"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
)
// posInsideSelection: Returns true if the given position is inside the current
// visual selection, handling all three visual modes differently.
func posInsideSelection(m Model, col, line int) bool {
win := m.ActiveWindow()
switch m.Mode() {
case action.VisualLineMode:
case core.VisualLineMode:
startY := min(win.Anchor.Line, win.Cursor.Line)
endY := max(win.Anchor.Line, win.Cursor.Line)
return line >= startY && line <= endY
case action.VisualMode:
case core.VisualMode:
ax := win.Anchor.Col
ay := win.Anchor.Line
@ -38,7 +40,7 @@ func posInsideSelection(m Model, col, line int) bool {
beforeEnd := line < endY || (line == endY && col <= endX)
return afterStart && beforeEnd
case action.VisualBlockMode:
case core.VisualBlockMode:
startX := min(win.Anchor.Col, win.Cursor.Col)
startY := min(win.Anchor.Line, win.Cursor.Line)
endX := max(win.Anchor.Col, win.Cursor.Col)
@ -52,6 +54,8 @@ func posInsideSelection(m Model, col, line int) bool {
}
}
// posIsAnchor: Returns true if the given position matches the anchor position
// used for visual mode debugging/rendering.
func posIsAnchor(m Model, col, line int) bool {
win := m.ActiveWindow()
ax := win.Anchor.Col
@ -59,10 +63,10 @@ func posIsAnchor(m Model, col, line int) bool {
return col == ax && line == ay
}
// Model.View: Renders the complete editor view including buffer content, line
// numbers, status bar, and command line.
func (m Model) View() string {
win := m.ActiveWindow()
return win.View(m)
buf := m.ActiveBuffer()
var view strings.Builder
@ -154,6 +158,14 @@ func (m Model) View() string {
return view.String()
}
func viewWindow(w core.Window) string {
var view string
return view
}
// drawStatusBar: Renders the status bar with mode and cursor position,
// padding the middle with spaces to fill the terminal width.
func drawStatusBar(m Model) string {
left := leftBar(m)
right := rightBar(m)
@ -169,10 +181,13 @@ func drawStatusBar(m Model) string {
return left + middle + right
}
// leftBar: Returns the left side of the status bar showing the current mode.
func leftBar(m Model) string {
return fmt.Sprintf(" %s", m.Mode().ToString())
}
// rightBar: Returns the right side of the status bar showing cursor position
// and selection count in visual mode.
func rightBar(m Model) (bar string) {
win := m.ActiveWindow()
@ -185,8 +200,10 @@ func rightBar(m Model) (bar string) {
return
}
// drawCommandBar: Renders the command line showing command input, errors, or
// output depending on the current mode and state.
func drawCommandBar(m Model) (bar string) {
if m.Mode() == action.CommandMode {
if m.Mode() == core.CommandMode {
bar = ":"
cmd := m.Command()
cur := m.CommandCursor()
@ -208,6 +225,10 @@ func drawCommandBar(m Model) (bar string) {
bar = m.CommandOutput()
} else if strings.TrimSpace(m.Command()) != "" {
bar = fmt.Sprintf(":%s", m.Command())
} else if len(m.input.Pending()) > 0 {
// Get width of window and padding
rep := m.ActiveWindow().Width - 10 // 10 is padding
bar = fmt.Sprintf("%s%s", strings.Repeat(" ", rep), m.input.Pending())
}
return bar

View File

@ -2,9 +2,11 @@ package input
import (
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
// InputState: Represents the current state of the input handler state machine.
type InputState int
const (
@ -14,6 +16,8 @@ const (
StateMotionCount
)
// Handler: Manages input processing with a state machine for vim-style commands.
// Handles counts, operators, motions, and multi-key sequences.
type Handler struct {
state InputState
count1 int
@ -32,6 +36,7 @@ type Handler struct {
currentKeymap *Keymap
}
// NewHandler: Creates a new input handler with initialized keymaps for all modes.
func NewHandler() *Handler {
return &Handler{
// keymap: NewNormalKeymap(),
@ -43,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 {
// ESC always resets everything
if key == "esc" {
h.Reset()
if m.Mode() == action.InsertMode {
if m.Mode() == core.InsertMode {
m.ExitInsertMode()
} else {
m.SetMode(action.NormalMode)
m.SetMode(core.NormalMode)
}
return nil
}
// Insert/command mode bypasses the normal state machine entirely
switch m.Mode() {
case action.InsertMode:
case core.InsertMode:
return h.handleInsertKey(m, key)
case action.CommandMode:
case core.CommandMode:
return h.handleCommandKey(m, key)
}
@ -73,11 +80,11 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
// Set working keymap
switch m.Mode() {
case action.NormalMode:
case core.NormalMode:
h.currentKeymap = h.normalKeymap
case action.VisualMode,
action.VisualLineMode,
action.VisualBlockMode:
case core.VisualMode,
core.VisualLineMode,
core.VisualBlockMode:
h.currentKeymap = h.visualKeymap
}
@ -111,7 +118,7 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
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 {
switch h.state {
case StateReady, StateCount:
@ -123,6 +130,8 @@ func (h *Handler) dispatch(m action.Model, kind string, binding any, key string)
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 {
count := h.effectiveCount()
@ -142,14 +151,14 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
if m.Mode().IsVisualMode() {
start, end := normalizeVisualSelection(m)
// Visual line mode is linewise, others are charwise inclusive
mtype := action.CharwiseInclusive
if m.Mode() == action.VisualLineMode {
mtype = action.Linewise
mtype := core.CharwiseInclusive
if m.Mode() == core.VisualLineMode {
mtype = core.Linewise
}
cmd := op.Operate(m, start, end, mtype)
// Only reset to normal mode if operator didn't enter insert mode
if m.Mode() != action.InsertMode {
m.SetMode(action.NormalMode)
if m.Mode() != core.InsertMode {
m.SetMode(core.NormalMode)
}
h.Reset()
return cmd
@ -174,6 +183,8 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
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 {
count := h.effectiveCount()
win := m.ActiveWindow()
@ -209,6 +220,8 @@ func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any,
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 {
if len(key) != 1 || key[0] < '0' || key[0] > '9' {
return false
@ -234,6 +247,7 @@ func (h *Handler) tryAccumulateCount(key string) bool {
return true
}
// Handler.currentCount: Returns the count currently being accumulated based on state.
func (h *Handler) currentCount() int {
if h.state == StateOperatorPending || h.state == StateMotionCount {
return h.count2
@ -241,6 +255,8 @@ func (h *Handler) currentCount() int {
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 {
c1, c2 := h.count1, h.count2
if c1 == 0 {
@ -252,6 +268,7 @@ func (h *Handler) effectiveCount() int {
return c1 * c2
}
// Handler.Reset: Clears all handler state including counts, operators, and buffers.
func (h *Handler) Reset() {
h.state = StateReady
h.count1 = 0
@ -262,10 +279,13 @@ func (h *Handler) Reset() {
h.pending = ""
}
// Handler.Pending: Returns the accumulated input buffer for display.
func (h *Handler) Pending() string {
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 {
// Record the key for count replay (e.g. 5i...)
m.SetInsertKeys(append(m.InsertKeys(), key))
@ -283,6 +303,8 @@ func (h *Handler) handleInsertKey(m action.Model, key string) tea.Cmd {
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 {
kind, binding := h.commandKeymap.Lookup(key)
switch kind {
@ -296,10 +318,12 @@ func (h *Handler) handleCommandKey(m action.Model, key string) tea.Cmd {
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,
// regardless of which direction the selection was made.
func normalizeVisualSelection(m action.Model) (core.Position, core.Position) {
win := m.ActiveWindow()
a := action.Position{Line: win.Anchor.Line, Col: win.Anchor.Col}
c := action.Position{Line: win.Cursor.Line, Col: win.Cursor.Col}
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) {
return a, c
}

View File

@ -7,12 +7,14 @@ import (
"git.gophernest.net/azpect/TextEditor/internal/operator"
)
// Keymap: Maps key sequences to motions, operators, and actions.
type Keymap struct {
motions map[string]action.Motion
operators map[string]action.Operator
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 {
return &Keymap{
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 {
return &Keymap{
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 {
return &Keymap{
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 {
return &Keymap{
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) {
if m, ok := km.motions[key]; ok {
return "motion", m
@ -149,7 +154,7 @@ func (km *Keymap) Lookup(key string) (kind string, value any) {
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 {
for key := range km.motions {
if len(key) > len(prefix) && key[:len(prefix)] == prefix {

View File

@ -2,6 +2,7 @@ package motion
import (
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
@ -10,6 +11,8 @@ type MoveDown struct {
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 {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -19,7 +22,7 @@ func (a MoveDown) Execute(m action.Model) tea.Cmd {
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 {
return MoveDown{Count: n}
@ -30,6 +33,8 @@ type MoveUp struct {
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 {
win := m.ActiveWindow()
for i := 0; i < a.Count && win.Cursor.Line > 0; i++ {
@ -38,7 +43,7 @@ func (a MoveUp) Execute(m action.Model) tea.Cmd {
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 {
return MoveUp{Count: n}
@ -49,6 +54,8 @@ type MoveLeft struct {
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 {
win := m.ActiveWindow()
for i := 0; i < a.Count && win.Cursor.Col > 0; i++ {
@ -57,7 +64,7 @@ func (a MoveLeft) Execute(m action.Model) tea.Cmd {
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 {
return MoveLeft{Count: n}
@ -68,6 +75,8 @@ type MoveRight struct {
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 {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -78,7 +87,7 @@ func (a MoveRight) Execute(m action.Model) tea.Cmd {
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 {
return MoveRight{Count: n}

View File

@ -2,25 +2,32 @@ package motion
import (
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
// MoveCommandLeft implements Motion - moves cursor left in command line.
type MoveCommandLeft struct{}
// MoveCommandLeft.Execute: Moves the command line cursor one position to the left.
func (a MoveCommandLeft) Execute(m action.Model) tea.Cmd {
// The set function handles bounds
m.SetCommandCursor(m.CommandCursor() - 1)
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{}
// MoveCommandRight.Execute: Moves the command line cursor one position to the right.
func (a MoveCommandRight) Execute(m action.Model) tea.Cmd {
// The set function handles bounds
m.SetCommandCursor(m.CommandCursor() + 1)
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 }

View File

@ -2,23 +2,26 @@ package motion
import (
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
// MoveToTop implements Motion (gg) - linewise
type MoveToTop struct{}
// MoveToTop.Execute: Moves the cursor to the first line of the buffer.
func (a MoveToTop) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
win.SetCursorLine(0)
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
type MoveToBottom struct{}
// MoveToBottom.Execute: Moves the cursor to the last line of the buffer.
func (a MoveToBottom) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -26,22 +29,24 @@ func (a MoveToBottom) Execute(m action.Model) tea.Cmd {
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
type MoveToLineStart struct{}
// MoveToLineStart.Execute: Moves the cursor to the beginning of the current line.
func (a MoveToLineStart) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
win.SetCursorCol(0)
return nil
}
func (a MoveToLineStart) Type() action.MotionType { return action.CharwiseExclusive }
func (a MoveToLineStart) Type() core.MotionType { return core.CharwiseExclusive }
// MoveToLineEnd implements Motion ($) - charwise
type MoveToLineEnd struct{}
// MoveToLineEnd.Execute: Moves the cursor to the end of the current line.
func (a MoveToLineEnd) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -49,11 +54,13 @@ func (a MoveToLineEnd) Execute(m action.Model) tea.Cmd {
return nil
}
func (a MoveToLineEnd) Type() action.MotionType { return action.CharwiseInclusive }
func (a MoveToLineEnd) Type() core.MotionType { return core.CharwiseInclusive }
// MoveToLineContentStart implements Motion (_) - charwise
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 {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -77,13 +84,14 @@ func (a MoveToLineContentStart) Execute(m action.Model) tea.Cmd {
return nil
}
func (a MoveToLineContentStart) Type() action.MotionType { return action.CharwiseExclusive }
func (a MoveToLineContentStart) Type() core.MotionType { return core.CharwiseExclusive }
// MoveToColumn implements Motion (|) - charwise
type MoveToColumn struct {
Count int
}
// MoveToColumn.Execute: Moves the cursor to the column specified by Count.
func (a MoveToColumn) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -95,7 +103,7 @@ func (a MoveToColumn) Execute(m action.Model) tea.Cmd {
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 {
return MoveToColumn{Count: n}
@ -106,6 +114,8 @@ func (a MoveToColumn) WithCount(n int) action.Action {
// ScrollDownHalfPage implements Motion (ctrl+d) - linewise
type ScrollDownHalfPage struct{}
// ScrollDownHalfPage.Execute: Scrolls down half a page while maintaining the
// cursor's relative position in the viewport.
func (a ScrollDownHalfPage) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -142,11 +152,13 @@ func (a ScrollDownHalfPage) Execute(m action.Model) tea.Cmd {
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
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 {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -181,4 +193,4 @@ func (a ScrollUpHalfPage) Execute(m action.Model) tea.Cmd {
return nil
}
func (a ScrollUpHalfPage) Type() action.MotionType { return action.Linewise }
func (a ScrollUpHalfPage) Type() core.MotionType { return core.Linewise }

View File

@ -2,9 +2,12 @@ package motion
import (
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
// isWordChar: Returns true if the character is a word character (alphanumeric
// or underscore).
func isWordChar(c byte) bool {
return (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
@ -12,11 +15,15 @@ func isWordChar(c byte) bool {
c == '_'
}
// isWordPunctuation: Returns true if the character is punctuation (not whitespace
// and not a word character).
func isWordPunctuation(c byte) bool {
return c != ' ' && c != '\t' && !isWordChar(c)
}
func nextWordStart(buf *action.Buffer, x, y int) (int, int) {
// nextWordStart: Finds the start of the next word from position (x,y), handling
// word boundaries and line crossing.
func nextWordStart(buf *core.Buffer, x, y int) (int, int) {
line := buf.Lines[y]
// Skip current class
@ -64,7 +71,9 @@ func nextWordStart(buf *action.Buffer, x, y int) (int, int) {
return x, y
}
func nextWORDStart(buf *action.Buffer, x, y int) (int, int) {
// nextWORDStart: Finds the start of the next WORD from position (x,y), treating
// all non-whitespace as a single class.
func nextWORDStart(buf *core.Buffer, x, y int) (int, int) {
line := buf.Lines[y]
// Skip current WORD (all non-whitespace is one class for W)
@ -103,7 +112,9 @@ func nextWORDStart(buf *action.Buffer, x, y int) (int, int) {
return x, y
}
func nextWordEnd(buf *action.Buffer, x, y int) (int, int) {
// nextWordEnd: Finds the end of the next word from position (x,y), respecting
// word character classes.
func nextWordEnd(buf *core.Buffer, x, y int) (int, int) {
line := buf.Lines[y]
// Advance once to avoid being stuck on the current end
@ -160,7 +171,9 @@ func nextWordEnd(buf *action.Buffer, x, y int) (int, int) {
return x, y
}
func nextWORDEnd(buf *action.Buffer, x, y int) (int, int) {
// nextWORDEnd: Finds the end of the next WORD from position (x,y), treating
// all non-whitespace as a single class.
func nextWORDEnd(buf *core.Buffer, x, y int) (int, int) {
line := buf.Lines[y]
// Advance once to avoid being stuck on the current end
@ -208,7 +221,9 @@ func nextWORDEnd(buf *action.Buffer, x, y int) (int, int) {
return x, y
}
func prevWordStart(buf *action.Buffer, x, y int) (int, int) {
// prevWordStart: Finds the start of the previous word from position (x,y),
// moving backward through character classes.
func prevWordStart(buf *core.Buffer, x, y int) (int, int) {
line := buf.Lines[y]
// Back one to avoid being stuck on the current start
@ -263,6 +278,7 @@ type MoveForwardWord struct {
Count int
}
// MoveForwardWord.Execute: Moves the cursor forward by Count words (w motion).
func (a MoveForwardWord) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -277,8 +293,10 @@ func (a MoveForwardWord) Execute(m action.Model) tea.Cmd {
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 {
return MoveForwardWord{Count: n}
}
@ -288,6 +306,7 @@ type MoveForwardWORD struct {
Count int
}
// MoveForwardWORD.Execute: Moves the cursor forward by Count WORDs (W motion).
func (a MoveForwardWORD) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -302,8 +321,10 @@ func (a MoveForwardWORD) Execute(m action.Model) tea.Cmd {
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 {
return MoveForwardWORD{Count: n}
}
@ -313,6 +334,7 @@ type MoveForwardWordEnd struct {
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 {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -327,8 +349,10 @@ func (a MoveForwardWordEnd) Execute(m action.Model) tea.Cmd {
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 {
return MoveForwardWordEnd{Count: n}
}
@ -338,6 +362,7 @@ type MoveForwardWORDEnd struct {
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 {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -352,8 +377,10 @@ func (a MoveForwardWORDEnd) Execute(m action.Model) tea.Cmd {
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 {
return MoveForwardWORDEnd{Count: n}
}
@ -363,6 +390,7 @@ type MoveBackwardWord struct {
Count int
}
// MoveBackwardWord.Execute: Moves the cursor backward by Count words (b motion).
func (a MoveBackwardWord) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -377,8 +405,10 @@ func (a MoveBackwardWord) Execute(m action.Model) tea.Cmd {
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 {
return MoveBackwardWord{Count: n}
}

View File

@ -2,34 +2,37 @@ package operator
import (
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
// Implements Operator (c)
// ChangeOperator implements Operator (c) - changes (deletes and enters insert mode) text.
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() {
case action.VisualMode:
case core.VisualMode:
changeCharSelection(m, start, end)
case action.VisualLineMode:
case core.VisualLineMode:
changeLineSelection(m, start, end)
case action.VisualBlockMode:
case core.VisualBlockMode:
changeBlockSelection(m, start, end)
case action.NormalMode:
case core.NormalMode:
changeNormalMode(m, start, end, mtype)
}
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
if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
start, end = end, start
}
// Linewise motions (j, k, G, gg) always operate on whole lines
if mtype == action.Linewise {
if mtype == core.Linewise {
changeLineSelection(m, start, end)
return
}
@ -37,18 +40,18 @@ func changeNormalMode(m action.Model, start, end action.Position, mtype action.M
// Charwise motions on same line
if start.Line == end.Line {
// No movement = nothing to change
if start.Col == end.Col && mtype == action.CharwiseExclusive {
m.SetMode(action.InsertMode)
if start.Col == end.Col && mtype == core.CharwiseExclusive {
m.SetMode(core.InsertMode)
return
}
// Exclusive motion: end position not included, so back up one
if mtype == action.CharwiseExclusive {
if mtype == core.CharwiseExclusive {
end.Col--
}
if end.Col >= start.Col {
changeCharSelection(m, start, end)
} else {
m.SetMode(action.InsertMode)
m.SetMode(core.InsertMode)
}
return
}
@ -57,7 +60,8 @@ func changeNormalMode(m action.Model, start, end action.Position, mtype action.M
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()
@ -95,13 +99,14 @@ func changeCharSelection(m action.Model, start, end action.Position) {
win.SetCursorLine(start.Line)
win.SetCursorCol(start.Col)
m.SetMode(action.InsertMode)
m.SetMode(core.InsertMode)
// 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()
@ -118,13 +123,14 @@ func changeLineSelection(m action.Model, start, end action.Position) {
win.SetCursorLine(insertY)
win.SetCursorCol(0)
m.SetMode(action.InsertMode)
m.SetMode(core.InsertMode)
// 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()
@ -142,13 +148,13 @@ func changeBlockSelection(m action.Model, start, end action.Position) {
win.SetCursorLine(start.Line)
win.SetCursorCol(startCol)
m.SetMode(action.InsertMode)
m.SetMode(core.InsertMode)
}
// Verify ChangeOperator implements DoublePresser
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 {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -167,7 +173,7 @@ func (o ChangeOperator) DoublePress(m action.Model, count int) tea.Cmd {
}
// Put deleted lines in register
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
m.UpdateDefaultRegister(core.LinewiseRegister, lines)
// Insert empty line at the original position for editing
// If we deleted everything, startY might be past end, so clamp it
@ -177,7 +183,7 @@ func (o ChangeOperator) DoublePress(m action.Model, count int) tea.Cmd {
// Position cursor on the new empty line
win.SetCursorLine(insertY)
win.SetCursorCol(0)
m.SetMode(action.InsertMode)
m.SetMode(core.InsertMode)
return nil
}

View File

@ -2,21 +2,23 @@ package operator
import (
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
// Implements Operator (d)
// DeleteOperator implements Operator (d) - deletes text in various modes.
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() {
case action.VisualMode:
case core.VisualMode:
deleteCharSelection(m, start, end)
case action.VisualLineMode:
case core.VisualLineMode:
deleteLineSelection(m, start, end)
case action.VisualBlockMode:
case core.VisualBlockMode:
deleteBlockSelection(m, start, end)
case action.NormalMode:
case core.NormalMode:
deleteNormalMode(m, start, end, mtype)
}
return nil
@ -25,7 +27,7 @@ func (o DeleteOperator) Operate(m action.Model, start, end action.Position, mtyp
// Verify DeleteOperator implements DoublePresser
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 {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -53,19 +55,20 @@ func (o DeleteOperator) DoublePress(m action.Model, count int) tea.Cmd {
}
// Put her in the register!
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
m.UpdateDefaultRegister(core.LinewiseRegister, lines)
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
if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
start, end = end, start
}
// Linewise motions (j, k, G, gg) always operate on whole lines
if mtype == action.Linewise {
if mtype == core.Linewise {
deleteLineSelection(m, start, end)
return
}
@ -73,11 +76,11 @@ func deleteNormalMode(m action.Model, start, end action.Position, mtype action.M
// Charwise motions on same line
if start.Line == end.Line {
// No movement = nothing to delete
if start.Col == end.Col && mtype == action.CharwiseExclusive {
if start.Col == end.Col && mtype == core.CharwiseExclusive {
return
}
// Exclusive motion: end position not included, so back up one
if mtype == action.CharwiseExclusive {
if mtype == core.CharwiseExclusive {
end.Col--
}
if end.Col >= start.Col {
@ -90,7 +93,8 @@ func deleteNormalMode(m action.Model, start, end action.Position, mtype action.M
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()
@ -119,7 +123,8 @@ func deleteCharSelection(m action.Model, start, end action.Position) {
win.SetCursorCol(start.Col)
}
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()
@ -142,10 +147,11 @@ func deleteLineSelection(m action.Model, start, end action.Position) {
win.SetCursorLine(y)
// 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()

View File

@ -4,23 +4,25 @@ import (
"fmt"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
// Implements Operator (y)
// YankOperator implements Operator (y) - copies text to register in various modes.
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() {
case action.VisualMode:
case core.VisualMode:
yankVisualMode(m, start, end)
case action.VisualLineMode:
case core.VisualLineMode:
yankVisualLineMode(m, start, end)
case action.VisualBlockMode:
case core.VisualBlockMode:
yankVisualBlockMode(m, start, end)
case action.NormalMode:
case core.NormalMode:
yankNormalMode(m, start, end, mtype)
default:
m.SetCommandError(fmt.Errorf("'y' operator not yet implemented."))
@ -34,6 +36,7 @@ func (o YankOperator) Operate(m action.Model, start, end action.Position, mtype
// Verify YankOperator implements DoublePresser
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 {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
@ -50,12 +53,13 @@ func (o YankOperator) DoublePress(m action.Model, count int) tea.Cmd {
}
// Put her in the register!
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
m.UpdateDefaultRegister(core.LinewiseRegister, lines)
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 {
@ -72,16 +76,16 @@ func yankNormalMode(m action.Model, start, end action.Position, mtype action.Mot
endX := max(start.Col, end.Col)
// Inclusive motions include the end character
if mtype == action.CharwiseInclusive {
if mtype == core.CharwiseInclusive {
endX++
}
endX = min(endX, len(line)) // Catch overflow
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
if start.Col != end.Col {
m.SetCommandError(fmt.Errorf("Start column and end column must match for linewise yank operations."))
@ -93,11 +97,12 @@ func yankNormalMode(m action.Model, start, end action.Position, mtype action.Mot
endY := max(start.Line, end.Line)
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
@ -111,7 +116,7 @@ func yankVisualMode(m action.Model, start, end action.Position) {
endCol := min(end.Col+1, len(line)) // +1 because visual selection is inclusive
startCol := min(start.Col, len(line))
cnt := line[startCol:endCol]
m.UpdateDefaultRegister(action.CharwiseRegister, []string{cnt})
m.UpdateDefaultRegister(core.CharwiseRegister, []string{cnt})
return
}
@ -133,10 +138,11 @@ func yankVisualMode(m action.Model, start, end action.Position) {
endCol := min(end.Col+1, len(lastLine))
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
@ -150,11 +156,12 @@ func yankVisualLineMode(m action.Model, start, end action.Position) {
endY := max(start.Line, end.Line)
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
@ -178,5 +185,5 @@ func yankVisualBlockMode(m action.Model, start, end action.Position) {
content = append(content, line[startX:lineEndX])
}
m.UpdateDefaultRegister(action.BlockwiseRegister, content)
m.UpdateDefaultRegister(core.BlockwiseRegister, content)
}

View File

@ -1,12 +1,11 @@
package editor
package style
import (
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
"github.com/charmbracelet/lipgloss"
)
// Styles holds all the visual styling for the editor.
// Passed to Window.View() so windows can render themselves.
type Styles struct {
// Cursor styles by mode
CursorNormal lipgloss.Style
@ -64,62 +63,13 @@ func DefaultStyles() Styles {
}
// CursorStyle returns the appropriate cursor style for the given mode.
func (s Styles) CursorStyle(mode action.Mode) lipgloss.Style {
func (s Styles) CursorStyle(mode core.Mode) lipgloss.Style {
switch mode {
case action.InsertMode:
case core.InsertMode:
return s.CursorInsert
case action.CommandMode:
case core.CommandMode:
return s.CursorCommand
default:
return s.CursorNormal
}
}
// ==================================================
// Deprecated
// ==================================================
// 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)
// }