feat: yank operator implemented. tested
This commit is contained in:
parent
774d0d0071
commit
ca5a0a99a5
@ -89,6 +89,12 @@ type Model interface {
|
||||
Settings() Settings
|
||||
SetSettings(s Settings)
|
||||
|
||||
// Registers
|
||||
Registers() map[rune]Register
|
||||
GetRegister(name rune) (Register, bool)
|
||||
SetRegister(name rune, t RegisterType, cnt []string) error
|
||||
UpdateDefault(t RegisterType, cnt []string)
|
||||
|
||||
// Mode
|
||||
Mode() Mode
|
||||
SetMode(mode Mode)
|
||||
@ -109,10 +115,16 @@ type Position struct {
|
||||
type MotionType int
|
||||
|
||||
const (
|
||||
Charwise MotionType = iota // h, l, w, e, f, t - operates on characters
|
||||
Linewise // j, k, G, gg, {, } - operates on whole lines
|
||||
CharwiseExclusive MotionType = iota // w, b, h, l, 0, ^ - end position not included
|
||||
CharwiseInclusive // e, $, f - end position is included
|
||||
Linewise // j, k, G, gg, {, } - operates on whole lines
|
||||
)
|
||||
|
||||
// IsCharwise returns true if the motion type is character-based (not linewise)
|
||||
func (mt MotionType) IsCharwise() bool {
|
||||
return mt == CharwiseExclusive || mt == CharwiseInclusive
|
||||
}
|
||||
|
||||
// Action is the base interface - anything executable
|
||||
type Action interface {
|
||||
Execute(m Model) tea.Cmd
|
||||
@ -127,7 +139,10 @@ type Motion interface {
|
||||
// Operator acts on a range (delete, yank, change)
|
||||
type Operator interface {
|
||||
Operate(m Model, start, end Position, mtype MotionType) tea.Cmd
|
||||
// DoublePress handles dd, yy, cc (line-wise)
|
||||
}
|
||||
|
||||
// DoublePresser is an optional interface for operators that support double-press (dd, yy, cc)
|
||||
type DoublePresser interface {
|
||||
DoublePress(m Model, count int) tea.Cmd
|
||||
}
|
||||
|
||||
|
||||
149
internal/action/paste.go
Normal file
149
internal/action/paste.go
Normal file
@ -0,0 +1,149 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Paste implements Action (p) - pastes after cursor
|
||||
type Paste struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
func (a Paste) Execute(m Model) tea.Cmd {
|
||||
// Get reg
|
||||
reg, found := m.GetRegister('"')
|
||||
if !found {
|
||||
m.SetCommandError(fmt.Errorf("\"\" register is broken. Uh oh."))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exit if blank
|
||||
if len(reg.Content) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch reg.Type {
|
||||
case LinewiseRegister:
|
||||
{
|
||||
initY := m.CursorY()
|
||||
lines := reg.Content
|
||||
insertPos := initY + 1
|
||||
|
||||
// Run count times
|
||||
for range a.Count {
|
||||
for _, line := range lines {
|
||||
m.InsertLine(insertPos, line)
|
||||
insertPos++
|
||||
}
|
||||
}
|
||||
|
||||
if m.LineCount() > 1 {
|
||||
m.SetCursorY(initY + 1)
|
||||
}
|
||||
}
|
||||
case CharwiseRegister:
|
||||
{
|
||||
lines := reg.Content
|
||||
|
||||
// Shouldn't happen, just a check
|
||||
if len(lines) != 1 {
|
||||
m.SetCommandError(fmt.Errorf("Charwise register should only have a single line of content."))
|
||||
break
|
||||
}
|
||||
|
||||
x := m.CursorX()
|
||||
y := m.CursorY()
|
||||
|
||||
cnt := strings.Repeat(lines[0], max(1, a.Count))
|
||||
curLine := m.Line(y)
|
||||
|
||||
// Catch edge cases, end of line, start of blank line
|
||||
insertAt := min(x+1, len(curLine))
|
||||
newLine := curLine[:insertAt] + cnt + curLine[insertAt:]
|
||||
m.SetLine(y, newLine)
|
||||
|
||||
m.SetCursorX(x + len(cnt))
|
||||
m.ClampCursorX()
|
||||
}
|
||||
default:
|
||||
m.SetCommandError(fmt.Errorf("Register type is not implemented."))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure Paste implements Repeatable
|
||||
var _ Repeatable = Paste{}
|
||||
|
||||
func (a Paste) WithCount(n int) Action {
|
||||
return Paste{Count: n}
|
||||
}
|
||||
|
||||
// PasteBefore implements Action (P) - pastes before cursor
|
||||
type PasteBefore struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
func (a PasteBefore) Execute(m Model) tea.Cmd {
|
||||
// Get reg
|
||||
reg, found := m.GetRegister('"')
|
||||
if !found {
|
||||
m.SetCommandError(fmt.Errorf("\"\" register is broken. Uh oh."))
|
||||
return nil
|
||||
}
|
||||
|
||||
switch reg.Type {
|
||||
case LinewiseRegister:
|
||||
{
|
||||
initY := m.CursorY()
|
||||
lines := reg.Content
|
||||
insertPos := initY // Leave here, this will effectively move the lines below
|
||||
|
||||
// Run count times
|
||||
for range a.Count {
|
||||
for _, line := range lines {
|
||||
m.InsertLine(insertPos, line)
|
||||
insertPos++
|
||||
}
|
||||
}
|
||||
}
|
||||
case CharwiseRegister:
|
||||
{
|
||||
lines := reg.Content
|
||||
|
||||
// Shouldn't happen, just a check
|
||||
if len(lines) != 1 {
|
||||
m.SetCommandError(fmt.Errorf("Charwise register should only have a single line of content."))
|
||||
break
|
||||
}
|
||||
|
||||
x := m.CursorX()
|
||||
y := m.CursorY()
|
||||
|
||||
cnt := strings.Repeat(lines[0], max(1, a.Count))
|
||||
curLine := m.Line(y)
|
||||
|
||||
// Catch edge cases, end of line, start of blank line
|
||||
insertAt := min(x, len(curLine))
|
||||
newLine := curLine[:insertAt] + cnt + curLine[insertAt:]
|
||||
m.SetLine(y, newLine)
|
||||
|
||||
m.SetCursorX(x + len(cnt))
|
||||
m.ClampCursorX()
|
||||
}
|
||||
default:
|
||||
m.SetCommandError(fmt.Errorf("Register type is not implemented."))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure PasteBefore implements Repeatable
|
||||
var _ Repeatable = PasteBefore{}
|
||||
|
||||
func (a PasteBefore) WithCount(n int) Action {
|
||||
return PasteBefore{Count: n}
|
||||
}
|
||||
75
internal/action/register.go
Normal file
75
internal/action/register.go
Normal file
@ -0,0 +1,75 @@
|
||||
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{},
|
||||
}
|
||||
}
|
||||
2
internal/action/tmp
Normal file
2
internal/action/tmp
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
hello
|
||||
@ -53,6 +53,32 @@ func cmdWriteQuit(m action.Model, args []string) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
// cmdRegisters handles :register
|
||||
func cmdRegisters(m action.Model, args []string) tea.Cmd {
|
||||
// TODO: This is temporary, for debugging
|
||||
if len(args) < 1 {
|
||||
m.SetCommandError(fmt.Errorf("Please provide a name. Register dump not yet implemented."))
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(args[0]) != 1 {
|
||||
m.SetCommandError(fmt.Errorf("Name should be a single character."))
|
||||
return nil
|
||||
}
|
||||
|
||||
name := rune(args[0][0])
|
||||
reg, found := m.GetRegister(name)
|
||||
if !found {
|
||||
m.SetCommandError(fmt.Errorf("Could not find register '%c'.", name))
|
||||
return nil
|
||||
}
|
||||
|
||||
content := strings.Join(reg.Content, "\\n")
|
||||
t := reg.Type
|
||||
m.SetCommandOutput(fmt.Sprintf("Type: %d Name: \"%c Content: %s", t, name, content))
|
||||
return nil
|
||||
}
|
||||
|
||||
// cmdSet handles :set option[=value]
|
||||
// Examples:
|
||||
//
|
||||
|
||||
@ -10,8 +10,8 @@ import (
|
||||
|
||||
// Command represents a command that can be executed from command mode
|
||||
type Command struct {
|
||||
Name string // Full name: "quit"
|
||||
ShortForm string // Minimum abbreviation: "q"
|
||||
Name string // Full name: "quit"
|
||||
ShortForm string // Minimum abbreviation: "q"
|
||||
Handler func(m action.Model, args []string) tea.Cmd // Handler function
|
||||
}
|
||||
|
||||
@ -139,4 +139,11 @@ func (r *Registry) registerDefaults() {
|
||||
ShortForm: "se",
|
||||
Handler: cmdSet,
|
||||
})
|
||||
|
||||
// Register commands
|
||||
r.Register(Command{
|
||||
Name: "register",
|
||||
ShortForm: "reg",
|
||||
Handler: cmdRegisters,
|
||||
})
|
||||
}
|
||||
|
||||
124
internal/editor/TEST_HELPERS.md
Normal file
124
internal/editor/TEST_HELPERS.md
Normal file
@ -0,0 +1,124 @@
|
||||
# Test Helper Reference
|
||||
|
||||
## Overview
|
||||
|
||||
The test helpers use the **functional options pattern** to make test setup flexible and composable.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```go
|
||||
// Default model (6 lines, cursor at 0,0, terminal 80x24)
|
||||
tm := newTestModel(t)
|
||||
|
||||
// With custom lines
|
||||
tm := newTestModel(t,
|
||||
WithLines([]string{"hello", "world"}),
|
||||
)
|
||||
|
||||
// With custom cursor position
|
||||
tm := newTestModel(t,
|
||||
WithCursorPos(action.Position{Line: 1, Col: 5}),
|
||||
)
|
||||
|
||||
// With custom terminal size
|
||||
tm := newTestModel(t,
|
||||
WithTermSize(120, 40),
|
||||
)
|
||||
|
||||
// With register content (useful for paste tests)
|
||||
tm := newTestModel(t,
|
||||
WithRegister('"', action.CharwiseRegister, []string{"yanked text"}),
|
||||
)
|
||||
```
|
||||
|
||||
## Combining Options
|
||||
|
||||
You can combine multiple options in a single call:
|
||||
|
||||
```go
|
||||
tm := newTestModel(t,
|
||||
WithLines([]string{"line 1", "line 2", "line 3"}),
|
||||
WithCursorPos(action.Position{Line: 1, Col: 0}),
|
||||
WithTermSize(100, 30),
|
||||
WithRegister('"', action.LinewiseRegister, []string{"deleted line"}),
|
||||
)
|
||||
```
|
||||
|
||||
## Available Options
|
||||
|
||||
| Option | Parameters | Description |
|
||||
|--------|-----------|-------------|
|
||||
| `WithLines` | `[]string` | Set buffer lines |
|
||||
| `WithCursorPos` | `action.Position` | Set cursor position |
|
||||
| `WithTermSize` | `width, height int` | Set terminal dimensions |
|
||||
| `WithRegister` | `name rune, type RegisterType, content []string` | Set register content |
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
The old helper functions still work for existing tests:
|
||||
|
||||
```go
|
||||
newTestModelWithLines(t, []string{"a", "b"})
|
||||
newTestModelWithCursorPos(t, action.Position{Line: 1, Col: 2})
|
||||
newTestModelWithLinesAndCursorPos(t, lines, pos)
|
||||
newTestModelWithTermSize(t, lines, pos, width, height)
|
||||
```
|
||||
|
||||
## Example Test
|
||||
|
||||
```go
|
||||
func TestPasteCharwise(t *testing.T) {
|
||||
tm := newTestModel(t,
|
||||
WithLines([]string{"hello world"}),
|
||||
WithCursorPos(action.Position{Line: 0, Col: 5}),
|
||||
WithRegister('"', action.CharwiseRegister, []string{"PASTE"}),
|
||||
)
|
||||
|
||||
sendKeys(tm, "p")
|
||||
m := getFinalModel(t, tm)
|
||||
|
||||
// Assert expected behavior
|
||||
if m.Line(0) != "hello PASTEworld" {
|
||||
t.Errorf("unexpected result: %s", m.Line(0))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Default Values
|
||||
|
||||
When options are not specified:
|
||||
|
||||
- **Lines**: `[]string{"line 1", "line 2", "line 3", "line 4", "line 5", "line 6"}`
|
||||
- **Cursor**: `{Line: 0, Col: 0}`
|
||||
- **Terminal**: `80x24`
|
||||
- **Register**: None set
|
||||
|
||||
## Adding New Options
|
||||
|
||||
To add a new option:
|
||||
|
||||
1. Add field to `testModelConfig`
|
||||
2. Create a `With*` function that returns `TestModelOption`
|
||||
3. Apply the option in `newTestModel` after creating the model
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
// In testModelConfig
|
||||
type testModelConfig struct {
|
||||
// ... existing fields
|
||||
scrollY int
|
||||
}
|
||||
|
||||
// New option function
|
||||
func WithScrollY(y int) TestModelOption {
|
||||
return func(c *testModelConfig) {
|
||||
c.scrollY = y
|
||||
}
|
||||
}
|
||||
|
||||
// Apply in newTestModel
|
||||
if cfg.scrollY > 0 {
|
||||
m.SetScrollY(cfg.scrollY)
|
||||
}
|
||||
```
|
||||
118
internal/editor/helpers_example_test.go
Normal file
118
internal/editor/helpers_example_test.go
Normal file
@ -0,0 +1,118 @@
|
||||
package editor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
)
|
||||
|
||||
// TestHelperExamples demonstrates the different ways to use the test helpers
|
||||
func TestHelperExamples(t *testing.T) {
|
||||
t.Run("basic model with defaults", func(t *testing.T) {
|
||||
// Uses default: 6 lines, cursor at 0,0, terminal 80x24
|
||||
tm := newTestModel(t)
|
||||
_ = getFinalModel(t, tm)
|
||||
})
|
||||
|
||||
t.Run("custom lines only", func(t *testing.T) {
|
||||
tm := newTestModel(t,
|
||||
WithLines([]string{"hello", "world"}),
|
||||
)
|
||||
m := getFinalModel(t, tm)
|
||||
if len(m.lines) != 2 {
|
||||
t.Errorf("expected 2 lines, got %d", len(m.lines))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("custom cursor position", func(t *testing.T) {
|
||||
tm := newTestModel(t,
|
||||
WithCursorPos(action.Position{Line: 2, Col: 3}),
|
||||
)
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 2 || m.CursorX() != 3 {
|
||||
t.Errorf("expected cursor at (2,3), got (%d,%d)", m.CursorY(), m.CursorX())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("custom terminal size", func(t *testing.T) {
|
||||
tm := newTestModel(t,
|
||||
WithTermSize(120, 40),
|
||||
)
|
||||
m := getFinalModel(t, tm)
|
||||
if m.WinW() != 120 || m.WinH() != 40 {
|
||||
t.Errorf("expected size 120x40, got %dx%d", m.WinW(), m.WinH())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with register content for paste testing", func(t *testing.T) {
|
||||
tm := newTestModel(t,
|
||||
WithLines([]string{"hello world"}),
|
||||
WithRegister('"', action.CharwiseRegister, []string{"foo"}),
|
||||
)
|
||||
m := getFinalModel(t, tm)
|
||||
|
||||
reg, ok := m.GetRegister('"')
|
||||
if !ok {
|
||||
t.Fatal("expected register to be set")
|
||||
}
|
||||
if reg.Type != action.CharwiseRegister {
|
||||
t.Errorf("expected charwise register, got %v", reg.Type)
|
||||
}
|
||||
if len(reg.Content) != 1 || reg.Content[0] != "foo" {
|
||||
t.Errorf("expected content ['foo'], got %v", reg.Content)
|
||||
}
|
||||
})
|
||||
|
||||
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}),
|
||||
WithTermSize(100, 30),
|
||||
WithRegister('"', action.LinewiseRegister, []string{"deleted line 1", "deleted line 2"}),
|
||||
)
|
||||
m := getFinalModel(t, tm)
|
||||
|
||||
// Verify all options were applied
|
||||
if len(m.Lines()) != 3 {
|
||||
t.Errorf("expected 3 lines, got %d", len(m.Lines()))
|
||||
}
|
||||
if m.CursorY() != 1 || m.CursorX() != 5 {
|
||||
t.Errorf("expected cursor at (1,5), got (%d,%d)", m.CursorY(), m.CursorX())
|
||||
}
|
||||
if m.WinW() != 100 || m.WinH() != 30 {
|
||||
t.Errorf("expected size 100x30, got %dx%d", m.WinW(), m.WinH())
|
||||
}
|
||||
|
||||
reg, ok := m.GetRegister('"')
|
||||
if !ok || reg.Type != action.LinewiseRegister {
|
||||
t.Error("register not set correctly")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("backward compatible helpers still work", func(t *testing.T) {
|
||||
// Old style helpers still work for existing tests
|
||||
tm1 := newTestModelWithLines(t, []string{"a", "b"})
|
||||
m1 := getFinalModel(t, tm1)
|
||||
if len(m1.Lines()) != 2 {
|
||||
t.Error("newTestModelWithLines failed")
|
||||
}
|
||||
|
||||
tm2 := newTestModelWithCursorPos(t, action.Position{Line: 1, Col: 2})
|
||||
m2 := getFinalModel(t, tm2)
|
||||
if m2.CursorY() != 1 {
|
||||
t.Error("newTestModelWithCursorPos failed")
|
||||
}
|
||||
|
||||
tm3 := newTestModelWithLinesAndCursorPos(t, []string{"x"}, action.Position{Line: 0, Col: 0})
|
||||
m3 := getFinalModel(t, tm3)
|
||||
if len(m3.Lines()) != 1 {
|
||||
t.Error("newTestModelWithLinesAndCursorPos failed")
|
||||
}
|
||||
|
||||
tm4 := newTestModelWithTermSize(t, []string{"y"}, action.Position{Line: 0, Col: 0}, 50, 20)
|
||||
m4 := getFinalModel(t, tm4)
|
||||
if m4.WinW() != 50 {
|
||||
t.Error("newTestModelWithTermSize failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -35,27 +35,91 @@ func sendKeys(tm *teatest.TestModel, keys ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
// newTestModel creates a test model with default content
|
||||
func newTestModel(t *testing.T) *teatest.TestModel {
|
||||
lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5", "line 6"}
|
||||
return teatest.NewTestModel(t, NewModel(lines, action.Position{Col: 0, Line: 0}), teatest.WithInitialTermSize(80, 24))
|
||||
// TestModelOption is a functional option for configuring test models
|
||||
type TestModelOption func(*testModelConfig)
|
||||
|
||||
type testModelConfig struct {
|
||||
lines []string
|
||||
pos action.Position
|
||||
width int
|
||||
height int
|
||||
regName rune
|
||||
regType action.RegisterType
|
||||
regContent []string
|
||||
}
|
||||
|
||||
// WithLines sets the initial buffer lines
|
||||
func WithLines(lines []string) TestModelOption {
|
||||
return func(c *testModelConfig) {
|
||||
c.lines = lines
|
||||
}
|
||||
}
|
||||
|
||||
// WithCursorPos sets the initial cursor position
|
||||
func WithCursorPos(pos action.Position) TestModelOption {
|
||||
return func(c *testModelConfig) {
|
||||
c.pos = pos
|
||||
}
|
||||
}
|
||||
|
||||
// WithTermSize sets the terminal dimensions
|
||||
func WithTermSize(width, height int) TestModelOption {
|
||||
return func(c *testModelConfig) {
|
||||
c.width = width
|
||||
c.height = height
|
||||
}
|
||||
}
|
||||
|
||||
// WithRegister sets a register's content
|
||||
func WithRegister(name rune, regType action.RegisterType, content []string) TestModelOption {
|
||||
return func(c *testModelConfig) {
|
||||
c.regName = name
|
||||
c.regType = regType
|
||||
c.regContent = content
|
||||
}
|
||||
}
|
||||
|
||||
// newTestModel creates a test model with optional configuration
|
||||
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},
|
||||
width: 80,
|
||||
height: 24,
|
||||
}
|
||||
|
||||
// Apply options
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
|
||||
// Create model
|
||||
m := NewModel(cfg.lines, cfg.pos)
|
||||
|
||||
// Set register if provided
|
||||
if cfg.regContent != nil {
|
||||
m.SetRegister(cfg.regName, cfg.regType, cfg.regContent)
|
||||
}
|
||||
|
||||
return teatest.NewTestModel(t, m, teatest.WithInitialTermSize(cfg.width, cfg.height))
|
||||
}
|
||||
|
||||
// Convenience functions for backward compatibility
|
||||
func newTestModelWithLines(t *testing.T, lines []string) *teatest.TestModel {
|
||||
return teatest.NewTestModel(t, NewModel(lines, action.Position{Col: 0, Line: 0}), teatest.WithInitialTermSize(80, 24))
|
||||
return newTestModel(t, WithLines(lines))
|
||||
}
|
||||
|
||||
func newTestModelWithCursorPos(t *testing.T, pos action.Position) *teatest.TestModel {
|
||||
lines := []string{"line 1", "line 2", "line 3", "line 4", "line 5", "line 6"}
|
||||
return teatest.NewTestModel(t, NewModel(lines, pos), teatest.WithInitialTermSize(80, 24))
|
||||
return newTestModel(t, WithCursorPos(pos))
|
||||
}
|
||||
|
||||
func newTestModelWithLinesAndCursorPos(t *testing.T, lines []string, pos action.Position) *teatest.TestModel {
|
||||
return teatest.NewTestModel(t, NewModel(lines, pos), teatest.WithInitialTermSize(80, 24))
|
||||
return newTestModel(t, WithLines(lines), WithCursorPos(pos))
|
||||
}
|
||||
|
||||
func newTestModelWithTermSize(t *testing.T, lines []string, pos action.Position, width, height int) *teatest.TestModel {
|
||||
return teatest.NewTestModel(t, NewModel(lines, pos), teatest.WithInitialTermSize(width, height))
|
||||
return newTestModel(t, WithLines(lines), WithCursorPos(pos), WithTermSize(width, height))
|
||||
}
|
||||
|
||||
// getFinalModel extracts the final model state (sends ctrl+c to quit first)
|
||||
|
||||
@ -615,9 +615,9 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
|
||||
sendKeys(tm, "d", "e")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// 'e' is inclusive - deletes "hello" (cols 0-4 inclusive)
|
||||
if m.Line(0) != "o world" {
|
||||
t.Errorf("Line(0) = %q, want \"o world\"", m.Line(0))
|
||||
// 'e' is inclusive - deletes "hello" (cols 0-4 inclusive), leaves " world"
|
||||
if m.Line(0) != " world" {
|
||||
t.Errorf("Line(0) = %q, want \" world\"", m.Line(0))
|
||||
}
|
||||
if m.CursorX() != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
||||
@ -631,8 +631,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// From 'l' (col 2) to 'o' (col 4) inclusive → deletes "llo"
|
||||
if m.Line(0) != "heo world" {
|
||||
t.Errorf("Line(0) = %q, want \"heo world\"", m.Line(0))
|
||||
if m.Line(0) != "he world" {
|
||||
t.Errorf("Line(0) = %q, want \"he world\"", m.Line(0))
|
||||
}
|
||||
})
|
||||
|
||||
@ -643,8 +643,8 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// From 'o' of "hello" (col 4) to 'd' of "world" (col 10) inclusive
|
||||
if m.Line(0) != "helld" {
|
||||
t.Errorf("Line(0) = %q, want \"helld\"", m.Line(0))
|
||||
if m.Line(0) != "hell" {
|
||||
t.Errorf("Line(0) = %q, want \"hell\"", m.Line(0))
|
||||
}
|
||||
})
|
||||
|
||||
@ -654,9 +654,9 @@ func TestDeleteOperatorWithWordMotion(t *testing.T) {
|
||||
sendKeys(tm, "2", "d", "e")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Deletes "one" and "two" (to end of second word inclusive)
|
||||
if m.Line(0) != "o three four" {
|
||||
t.Errorf("Line(0) = %q, want \"o three four\"", m.Line(0))
|
||||
// Deletes "one" and " two" (to end of second word inclusive)
|
||||
if m.Line(0) != " three four" {
|
||||
t.Errorf("Line(0) = %q, want \" three four\"", m.Line(0))
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
576
internal/editor/integration_paste_test.go
Normal file
576
internal/editor/integration_paste_test.go
Normal file
@ -0,0 +1,576 @@
|
||||
package editor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
)
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "p")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 3 {
|
||||
t.Errorf("LineCount() = %d, want 3", m.LineCount())
|
||||
}
|
||||
if m.Line(0) != "line 1" {
|
||||
t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
|
||||
}
|
||||
if m.Line(1) != "inserted" {
|
||||
t.Errorf("Line(1) = %q, want 'inserted'", m.Line(1))
|
||||
}
|
||||
if m.Line(2) != "line 2" {
|
||||
t.Errorf("Line(2) = %q, want 'line 2'", m.Line(2))
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "p")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 1 {
|
||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "p")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 4 {
|
||||
t.Errorf("LineCount() = %d, want 4", m.LineCount())
|
||||
}
|
||||
if m.Line(2) != "inserted" {
|
||||
t.Errorf("Line(2) = %q, want 'inserted'", m.Line(2))
|
||||
}
|
||||
if m.CursorY() != 2 {
|
||||
t.Errorf("CursorY() = %d, want 2", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "p")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 3 {
|
||||
t.Errorf("LineCount() = %d, want 3", m.LineCount())
|
||||
}
|
||||
if m.Line(2) != "inserted" {
|
||||
t.Errorf("Line(2) = %q, want 'inserted'", m.Line(2))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "p")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 5 {
|
||||
t.Errorf("LineCount() = %d, want 5", m.LineCount())
|
||||
}
|
||||
if m.Line(0) != "line 1" {
|
||||
t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
|
||||
}
|
||||
if m.Line(1) != "first" {
|
||||
t.Errorf("Line(1) = %q, want 'first'", m.Line(1))
|
||||
}
|
||||
if m.Line(2) != "second" {
|
||||
t.Errorf("Line(2) = %q, want 'second'", m.Line(2))
|
||||
}
|
||||
if m.Line(3) != "third" {
|
||||
t.Errorf("Line(3) = %q, want 'third'", m.Line(3))
|
||||
}
|
||||
if m.Line(4) != "line 2" {
|
||||
t.Errorf("Line(4) = %q, want 'line 2'", m.Line(4))
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "p")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 1 {
|
||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "2", "p")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 4 {
|
||||
t.Errorf("LineCount() = %d, want 4", m.LineCount())
|
||||
}
|
||||
// Both "inserted" lines should appear after line 1
|
||||
if m.Line(1) != "inserted" {
|
||||
t.Errorf("Line(1) = %q, want 'inserted'", m.Line(1))
|
||||
}
|
||||
if m.Line(2) != "inserted" {
|
||||
t.Errorf("Line(2) = %q, want 'inserted'", m.Line(2))
|
||||
}
|
||||
if m.Line(3) != "line 2" {
|
||||
t.Errorf("Line(3) = %q, want 'line 2'", m.Line(3))
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "3", "p")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 4 {
|
||||
t.Errorf("LineCount() = %d, want 4", m.LineCount())
|
||||
}
|
||||
for i := 1; i <= 3; i++ {
|
||||
if m.Line(i) != "pasted" {
|
||||
t.Errorf("Line(%d) = %q, want 'pasted'", i, m.Line(i))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "2", "p")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Should be: original, first, second, first, second
|
||||
if m.LineCount() != 5 {
|
||||
t.Errorf("LineCount() = %d, want 5", m.LineCount())
|
||||
}
|
||||
if m.Line(0) != "original" {
|
||||
t.Errorf("Line(0) = %q, want 'original'", m.Line(0))
|
||||
}
|
||||
if m.Line(1) != "first" {
|
||||
t.Errorf("Line(1) = %q, want 'first'", m.Line(1))
|
||||
}
|
||||
if m.Line(2) != "second" {
|
||||
t.Errorf("Line(2) = %q, want 'second'", m.Line(2))
|
||||
}
|
||||
if m.Line(3) != "first" {
|
||||
t.Errorf("Line(3) = %q, want 'first'", m.Line(3))
|
||||
}
|
||||
if m.Line(4) != "second" {
|
||||
t.Errorf("Line(4) = %q, want 'second'", m.Line(4))
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "3", "p")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 1 {
|
||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Tests for P (paste before)
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "P")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 3 {
|
||||
t.Errorf("LineCount() = %d, want 3", m.LineCount())
|
||||
}
|
||||
if m.Line(0) != "line 1" {
|
||||
t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
|
||||
}
|
||||
if m.Line(1) != "inserted" {
|
||||
t.Errorf("Line(1) = %q, want 'inserted'", m.Line(1))
|
||||
}
|
||||
if m.Line(2) != "line 2" {
|
||||
t.Errorf("Line(2) = %q, want 'line 2'", m.Line(2))
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "P")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 1 {
|
||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "P")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 3 {
|
||||
t.Errorf("LineCount() = %d, want 3", m.LineCount())
|
||||
}
|
||||
if m.Line(0) != "inserted" {
|
||||
t.Errorf("Line(0) = %q, want 'inserted'", m.Line(0))
|
||||
}
|
||||
if m.Line(1) != "line 1" {
|
||||
t.Errorf("Line(1) = %q, want 'line 1'", m.Line(1))
|
||||
}
|
||||
if m.CursorY() != 0 {
|
||||
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "P")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 4 {
|
||||
t.Errorf("LineCount() = %d, want 4", m.LineCount())
|
||||
}
|
||||
if m.Line(1) != "inserted" {
|
||||
t.Errorf("Line(1) = %q, want 'inserted'", m.Line(1))
|
||||
}
|
||||
if m.Line(2) != "line 2" {
|
||||
t.Errorf("Line(2) = %q, want 'line 2'", m.Line(2))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "P")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 5 {
|
||||
t.Errorf("LineCount() = %d, want 5", m.LineCount())
|
||||
}
|
||||
if m.Line(0) != "line 1" {
|
||||
t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
|
||||
}
|
||||
if m.Line(1) != "first" {
|
||||
t.Errorf("Line(1) = %q, want 'first'", m.Line(1))
|
||||
}
|
||||
if m.Line(2) != "second" {
|
||||
t.Errorf("Line(2) = %q, want 'second'", m.Line(2))
|
||||
}
|
||||
if m.Line(3) != "third" {
|
||||
t.Errorf("Line(3) = %q, want 'third'", m.Line(3))
|
||||
}
|
||||
if m.Line(4) != "line 2" {
|
||||
t.Errorf("Line(4) = %q, want 'line 2'", m.Line(4))
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "P")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 1 {
|
||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "2", "P")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 4 {
|
||||
t.Errorf("LineCount() = %d, want 4", m.LineCount())
|
||||
}
|
||||
// Both "inserted" lines should appear before line 2
|
||||
if m.Line(1) != "inserted" {
|
||||
t.Errorf("Line(1) = %q, want 'inserted'", m.Line(1))
|
||||
}
|
||||
if m.Line(2) != "inserted" {
|
||||
t.Errorf("Line(2) = %q, want 'inserted'", m.Line(2))
|
||||
}
|
||||
if m.Line(3) != "line 2" {
|
||||
t.Errorf("Line(3) = %q, want 'line 2'", m.Line(3))
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "3", "P")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 4 {
|
||||
t.Errorf("LineCount() = %d, want 4", m.LineCount())
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
if m.Line(i) != "pasted" {
|
||||
t.Errorf("Line(%d) = %q, want 'pasted'", i, m.Line(i))
|
||||
}
|
||||
}
|
||||
if m.Line(3) != "original" {
|
||||
t.Errorf("Line(3) = %q, want 'original'", m.Line(3))
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "2", "P")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Should be: first, second, first, second, original
|
||||
if m.LineCount() != 5 {
|
||||
t.Errorf("LineCount() = %d, want 5", m.LineCount())
|
||||
}
|
||||
if m.Line(0) != "first" {
|
||||
t.Errorf("Line(0) = %q, want 'first'", m.Line(0))
|
||||
}
|
||||
if m.Line(1) != "second" {
|
||||
t.Errorf("Line(1) = %q, want 'second'", m.Line(1))
|
||||
}
|
||||
if m.Line(2) != "first" {
|
||||
t.Errorf("Line(2) = %q, want 'first'", m.Line(2))
|
||||
}
|
||||
if m.Line(3) != "second" {
|
||||
t.Errorf("Line(3) = %q, want 'second'", m.Line(3))
|
||||
}
|
||||
if m.Line(4) != "original" {
|
||||
t.Errorf("Line(4) = %q, want 'original'", m.Line(4))
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "3", "P")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.CursorY() != 1 {
|
||||
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "P")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 2 {
|
||||
t.Errorf("LineCount() = %d, want 2", m.LineCount())
|
||||
}
|
||||
if m.Line(0) != "inserted" {
|
||||
t.Errorf("Line(0) = %q, want 'inserted'", m.Line(0))
|
||||
}
|
||||
if m.Line(1) != "only line" {
|
||||
t.Errorf("Line(1) = %q, want 'only line'", m.Line(1))
|
||||
}
|
||||
})
|
||||
|
||||
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{}),
|
||||
)
|
||||
sendKeys(tm, "P")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 2 {
|
||||
t.Errorf("LineCount() = %d, want 2", m.LineCount())
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "P")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.Line(0) != " indented" {
|
||||
t.Errorf("Line(0) = %q, want ' indented'", m.Line(0))
|
||||
}
|
||||
if m.Line(1) != "\ttabbed" {
|
||||
t.Errorf("Line(1) = %q, want '\\ttabbed'", m.Line(1))
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "1", "0", "P") // 10P
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 11 {
|
||||
t.Errorf("LineCount() = %d, want 11", m.LineCount())
|
||||
}
|
||||
// Original should be at the end
|
||||
if m.Line(10) != "original" {
|
||||
t.Errorf("Line(10) = %q, want 'original'", m.Line(10))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "p")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 2 {
|
||||
t.Errorf("LineCount() = %d, want 2", m.LineCount())
|
||||
}
|
||||
if m.Line(0) != "only line" {
|
||||
t.Errorf("Line(0) = %q, want 'only line'", m.Line(0))
|
||||
}
|
||||
if m.Line(1) != "inserted" {
|
||||
t.Errorf("Line(1) = %q, want 'inserted'", m.Line(1))
|
||||
}
|
||||
})
|
||||
|
||||
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{}),
|
||||
)
|
||||
sendKeys(tm, "p")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 2 {
|
||||
t.Errorf("LineCount() = %d, want 2", m.LineCount())
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "p")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.Line(1) != " indented" {
|
||||
t.Errorf("Line(1) = %q, want ' indented'", m.Line(1))
|
||||
}
|
||||
if m.Line(2) != "\ttabbed" {
|
||||
t.Errorf("Line(2) = %q, want '\\ttabbed'", m.Line(2))
|
||||
}
|
||||
})
|
||||
|
||||
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"}),
|
||||
)
|
||||
sendKeys(tm, "1", "0", "p") // 10p
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.LineCount() != 11 {
|
||||
t.Errorf("LineCount() = %d, want 11", m.LineCount())
|
||||
}
|
||||
})
|
||||
}
|
||||
1040
internal/editor/integration_yank_test.go
Normal file
1040
internal/editor/integration_yank_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
||||
package editor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
@ -36,6 +37,9 @@ type Model struct {
|
||||
|
||||
// Settings
|
||||
settings action.Settings
|
||||
|
||||
// Registers
|
||||
registers map[rune]action.Register // name -> register
|
||||
}
|
||||
|
||||
func NewModel(lines []string, pos action.Position) Model {
|
||||
@ -45,11 +49,12 @@ func NewModel(lines []string, pos action.Position) Model {
|
||||
x: pos.Col,
|
||||
y: pos.Line,
|
||||
},
|
||||
scrollY: 0,
|
||||
mode: action.NormalMode,
|
||||
command: "",
|
||||
input: input.NewHandler(),
|
||||
settings: action.NewDefaultSettings(),
|
||||
scrollY: 0,
|
||||
mode: action.NormalMode,
|
||||
command: "",
|
||||
input: input.NewHandler(),
|
||||
settings: action.NewDefaultSettings(),
|
||||
registers: action.DefaultRegisters(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,6 +191,40 @@ func (m *Model) SetSettings(s action.Settings) {
|
||||
m.settings = s
|
||||
}
|
||||
|
||||
// Registers
|
||||
func (m *Model) Registers() map[rune]action.Register {
|
||||
return m.registers
|
||||
}
|
||||
|
||||
func (m *Model) GetRegister(name rune) (action.Register, bool) {
|
||||
reg, found := m.registers[name]
|
||||
return reg, found
|
||||
}
|
||||
|
||||
func (m *Model) SetRegister(name rune, t action.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}
|
||||
m.registers[name] = reg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Errors?
|
||||
func (m *Model) UpdateDefault(t action.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]
|
||||
}
|
||||
|
||||
// 0 and " both hold the new content independently
|
||||
m.SetRegister('0', t, cnt)
|
||||
m.SetRegister('"', t, cnt)
|
||||
}
|
||||
|
||||
// Window
|
||||
func (m *Model) ScrollY() int {
|
||||
return m.scrollY
|
||||
|
||||
@ -146,8 +146,8 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
|
||||
// In visual mode, the selection is already defined — operate immediately
|
||||
if m.Mode().IsVisualMode() {
|
||||
start, end := normalizeVisualSelection(m)
|
||||
// Visual line mode is linewise, others are charwise
|
||||
mtype := action.Charwise
|
||||
// Visual line mode is linewise, others are charwise inclusive
|
||||
mtype := action.CharwiseInclusive
|
||||
if m.Mode() == action.VisualLineMode {
|
||||
mtype = action.Linewise
|
||||
}
|
||||
@ -181,9 +181,14 @@ func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any,
|
||||
|
||||
// dd, yy, cc - same operator key pressed twice
|
||||
if kind == "operator" && key == h.operatorKey {
|
||||
cmd := h.operator.DoublePress(m, count)
|
||||
// Only call DoublePress if the operator supports it
|
||||
if dp, ok := h.operator.(action.DoublePresser); ok {
|
||||
cmd := dp.DoublePress(m, count)
|
||||
h.Reset()
|
||||
return cmd
|
||||
}
|
||||
h.Reset()
|
||||
return cmd
|
||||
return nil
|
||||
}
|
||||
|
||||
// Motion after operator
|
||||
|
||||
@ -33,9 +33,8 @@ func NewNormalKeymap() *Keymap {
|
||||
},
|
||||
operators: map[string]action.Operator{
|
||||
"d": operator.DeleteOperator{},
|
||||
"y": operator.YankOperator{},
|
||||
// "c": ChangeOp{},
|
||||
// "y": YankOp{},
|
||||
// "p": PasteOp{},
|
||||
// "s": SubstitueOp{},
|
||||
// "~": SwapCaseOp{},
|
||||
},
|
||||
@ -52,6 +51,8 @@ func NewNormalKeymap() *Keymap {
|
||||
"V": action.EnterVisualLineMode{},
|
||||
"ctrl+v": action.EnterVisualBlockMode{},
|
||||
"D": action.DeleteToEndOfLine{Count: 1},
|
||||
"p": action.Paste{Count: 1},
|
||||
"P": action.PasteBefore{Count: 1},
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -75,6 +76,7 @@ func NewVisualKeymap() *Keymap {
|
||||
operators: map[string]action.Operator{
|
||||
"d": operator.DeleteOperator{},
|
||||
"x": operator.DeleteOperator{},
|
||||
"y": operator.YankOperator{},
|
||||
// "c": ChangeOp{},
|
||||
// "y": YankOp{},
|
||||
// "p": PasteOp{},
|
||||
|
||||
@ -56,7 +56,7 @@ func (a MoveLeft) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveLeft) Type() action.MotionType { return action.Charwise }
|
||||
func (a MoveLeft) Type() action.MotionType { return action.CharwiseExclusive }
|
||||
|
||||
func (a MoveLeft) WithCount(n int) action.Action {
|
||||
return MoveLeft{Count: n}
|
||||
@ -76,7 +76,7 @@ func (a MoveRight) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveRight) Type() action.MotionType { return action.Charwise }
|
||||
func (a MoveRight) Type() action.MotionType { return action.CharwiseExclusive }
|
||||
|
||||
func (a MoveRight) WithCount(n int) action.Action {
|
||||
return MoveRight{Count: n}
|
||||
|
||||
@ -13,7 +13,7 @@ func (a MoveCommandLeft) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveCommandLeft) Type() action.MotionType { return action.Charwise }
|
||||
func (a MoveCommandLeft) Type() action.MotionType { return action.CharwiseExclusive }
|
||||
|
||||
type MoveCommandRight struct{}
|
||||
|
||||
@ -23,4 +23,4 @@ func (a MoveCommandRight) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveCommandRight) Type() action.MotionType { return action.Charwise }
|
||||
func (a MoveCommandRight) Type() action.MotionType { return action.CharwiseExclusive }
|
||||
|
||||
@ -36,7 +36,7 @@ func (a MoveToLineStart) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveToLineStart) Type() action.MotionType { return action.Charwise }
|
||||
func (a MoveToLineStart) Type() action.MotionType { return action.CharwiseExclusive }
|
||||
|
||||
// MoveToLineEnd implements Motion ($) - charwise
|
||||
type MoveToLineEnd struct{}
|
||||
@ -47,7 +47,7 @@ func (a MoveToLineEnd) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveToLineEnd) Type() action.MotionType { return action.Charwise }
|
||||
func (a MoveToLineEnd) Type() action.MotionType { return action.CharwiseInclusive }
|
||||
|
||||
// MoveToLineContentStart implements Motion (_) - charwise
|
||||
type MoveToLineContentStart struct{}
|
||||
@ -72,7 +72,7 @@ func (a MoveToLineContentStart) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveToLineContentStart) Type() action.MotionType { return action.Charwise }
|
||||
func (a MoveToLineContentStart) Type() action.MotionType { return action.CharwiseExclusive }
|
||||
|
||||
// TODO: Count for these, maybe?
|
||||
|
||||
|
||||
@ -187,7 +187,7 @@ func (a MoveForwardWord) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveForwardWord) Type() action.MotionType { return action.Charwise }
|
||||
func (a MoveForwardWord) Type() action.MotionType { return action.CharwiseExclusive }
|
||||
|
||||
func (a MoveForwardWord) WithCount(n int) action.Action {
|
||||
return MoveForwardWord{Count: n}
|
||||
@ -209,7 +209,7 @@ func (a MoveForwardWordEnd) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveForwardWordEnd) Type() action.MotionType { return action.Charwise }
|
||||
func (a MoveForwardWordEnd) Type() action.MotionType { return action.CharwiseInclusive }
|
||||
|
||||
func (a MoveForwardWordEnd) WithCount(n int) action.Action {
|
||||
return MoveForwardWordEnd{Count: n}
|
||||
@ -231,7 +231,7 @@ func (a MoveBackwardWord) Execute(m action.Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a MoveBackwardWord) Type() action.MotionType { return action.Charwise }
|
||||
func (a MoveBackwardWord) Type() action.MotionType { return action.CharwiseExclusive }
|
||||
|
||||
func (a MoveBackwardWord) WithCount(n int) action.Action {
|
||||
return MoveBackwardWord{Count: n}
|
||||
|
||||
@ -22,13 +22,20 @@ func (o DeleteOperator) Operate(m action.Model, start, end action.Position, mtyp
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify DeleteOperator implements DoublePresser
|
||||
var _ action.DoublePresser = DeleteOperator{}
|
||||
|
||||
// Double press handles dd - delete the entire line
|
||||
func (o DeleteOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
||||
// If we have a higher value than lines remaining, we can only run so many times
|
||||
opCount := min(count, m.LineCount()-m.CursorY())
|
||||
|
||||
var lines []string
|
||||
|
||||
for range opCount {
|
||||
y := m.CursorY()
|
||||
lines = append(lines, m.Line(y))
|
||||
|
||||
m.DeleteLine(y)
|
||||
|
||||
if m.LineCount() == 0 {
|
||||
@ -43,6 +50,10 @@ func (o DeleteOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
||||
m.ClampCursorX()
|
||||
}
|
||||
|
||||
// Put her in the register!
|
||||
m.UpdateDefault(action.LinewiseRegister, lines)
|
||||
// m.SetRegister('"', action.LinewiseRegister, lines)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -61,11 +72,13 @@ 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 {
|
||||
if start.Col == end.Col && mtype == action.CharwiseExclusive {
|
||||
return
|
||||
}
|
||||
// Exclusive motion: delete [start.Col, end.Col)
|
||||
end.Col--
|
||||
// Exclusive motion: end position not included, so back up one
|
||||
if mtype == action.CharwiseExclusive {
|
||||
end.Col--
|
||||
}
|
||||
if end.Col >= start.Col {
|
||||
deleteCharSelection(m, start, end)
|
||||
}
|
||||
@ -104,7 +117,10 @@ func deleteCharSelection(m action.Model, start, end action.Position) {
|
||||
}
|
||||
|
||||
func deleteLineSelection(m action.Model, start, end action.Position) {
|
||||
var lines []string
|
||||
|
||||
for i := end.Line; i >= start.Line; i-- {
|
||||
lines = append(lines, m.Line(i))
|
||||
m.DeleteLine(i)
|
||||
}
|
||||
|
||||
@ -119,6 +135,9 @@ func deleteLineSelection(m action.Model, start, end action.Position) {
|
||||
|
||||
m.SetCursorY(y)
|
||||
m.ClampCursorX()
|
||||
|
||||
// Update registers
|
||||
m.UpdateDefault(action.LinewiseRegister, lines)
|
||||
}
|
||||
|
||||
func deleteBlockSelection(m action.Model, start, end action.Position) {
|
||||
|
||||
169
internal/operator/yank.go
Normal file
169
internal/operator/yank.go
Normal file
@ -0,0 +1,169 @@
|
||||
package operator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Implements Operator (y)
|
||||
type YankOperator struct{}
|
||||
|
||||
func (o YankOperator) Operate(m action.Model, start, end action.Position, mtype action.MotionType) tea.Cmd {
|
||||
switch m.Mode() {
|
||||
case action.VisualMode:
|
||||
yankVisualMode(m, start, end)
|
||||
case action.VisualLineMode:
|
||||
yankVisualLineMode(m, start, end)
|
||||
case action.VisualBlockMode:
|
||||
yankVisualBlockMode(m, start, end)
|
||||
case action.NormalMode:
|
||||
yankNormalMode(m, start, end, mtype)
|
||||
default:
|
||||
m.SetCommandError(fmt.Errorf("'y' operator not yet implemented."))
|
||||
}
|
||||
|
||||
m.SetCursorX(start.Col)
|
||||
m.SetCursorY(start.Line)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify YankOperator implements DoublePresser
|
||||
var _ action.DoublePresser = YankOperator{}
|
||||
|
||||
func (o YankOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
||||
y := m.CursorY()
|
||||
|
||||
// If we have a higher value than lines remaining, we can only run so many times
|
||||
opCount := min(count, m.LineCount()-y)
|
||||
|
||||
var lines []string
|
||||
|
||||
for i := range opCount {
|
||||
lines = append(lines, m.Line(y+i))
|
||||
}
|
||||
|
||||
// Put her in the register!
|
||||
m.UpdateDefault(action.LinewiseRegister, lines)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func yankNormalMode(m action.Model, start, end action.Position, mtype action.MotionType) {
|
||||
switch {
|
||||
case mtype.IsCharwise():
|
||||
// This shouldn't happen
|
||||
if start.Line != end.Line {
|
||||
m.SetCommandError(fmt.Errorf("Start line and end line must match for charwise yank operations."))
|
||||
return
|
||||
}
|
||||
|
||||
line := m.Line(start.Line)
|
||||
|
||||
startX := min(start.Col, end.Col)
|
||||
endX := max(start.Col, end.Col)
|
||||
|
||||
// Inclusive motions include the end character
|
||||
if mtype == action.CharwiseInclusive {
|
||||
endX++
|
||||
}
|
||||
|
||||
endX = min(endX, len(line)) // Catch overflow
|
||||
|
||||
cnt := line[startX:endX]
|
||||
m.UpdateDefault(action.CharwiseRegister, []string{cnt})
|
||||
|
||||
case mtype == action.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."))
|
||||
return
|
||||
}
|
||||
|
||||
// These don't need to be validated, they are validated before being passed into the function
|
||||
startY := min(start.Line, end.Line)
|
||||
endY := max(start.Line, end.Line)
|
||||
|
||||
cnt := m.Lines()[startY : endY+1]
|
||||
m.UpdateDefault(action.LinewiseRegister, cnt)
|
||||
}
|
||||
}
|
||||
|
||||
func yankVisualMode(m action.Model, start, end action.Position) {
|
||||
// Normalize so start is before end
|
||||
if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
// Single line selection
|
||||
if start.Line == end.Line {
|
||||
line := m.Line(start.Line)
|
||||
endCol := min(end.Col+1, len(line)) // +1 because visual selection is inclusive
|
||||
startCol := min(start.Col, len(line))
|
||||
cnt := line[startCol:endCol]
|
||||
m.UpdateDefault(action.CharwiseRegister, []string{cnt})
|
||||
return
|
||||
}
|
||||
|
||||
// Multi-line selection
|
||||
var content []string
|
||||
|
||||
// First line: from start.Col to end of line
|
||||
firstLine := m.Line(start.Line)
|
||||
startCol := min(start.Col, len(firstLine))
|
||||
content = append(content, firstLine[startCol:])
|
||||
|
||||
// Middle lines: entire lines
|
||||
for y := start.Line + 1; y < end.Line; y++ {
|
||||
content = append(content, m.Line(y))
|
||||
}
|
||||
|
||||
// Last line: from beginning to end.Col (inclusive)
|
||||
lastLine := m.Line(end.Line)
|
||||
endCol := min(end.Col+1, len(lastLine))
|
||||
content = append(content, lastLine[:endCol])
|
||||
|
||||
m.UpdateDefault(action.CharwiseRegister, content)
|
||||
}
|
||||
|
||||
func yankVisualLineMode(m action.Model, start, end action.Position) {
|
||||
// This shouldn't happen
|
||||
if start.Col != end.Col {
|
||||
m.SetCommandError(fmt.Errorf("Start column and end column must match for linewise yank operations."))
|
||||
return
|
||||
}
|
||||
|
||||
// These don't need to be validated, they are validated before being passed into the function
|
||||
startY := min(start.Line, end.Line)
|
||||
endY := max(start.Line, end.Line)
|
||||
|
||||
cnt := m.Lines()[startY : endY+1]
|
||||
m.UpdateDefault(action.LinewiseRegister, cnt)
|
||||
|
||||
}
|
||||
|
||||
func yankVisualBlockMode(m action.Model, start, end action.Position) {
|
||||
// Normalize so startY <= endY and startX <= endX
|
||||
startY := min(start.Line, end.Line)
|
||||
endY := max(start.Line, end.Line)
|
||||
startX := min(start.Col, end.Col)
|
||||
endX := max(start.Col, end.Col) + 1 // +1 for inclusive
|
||||
|
||||
var content []string
|
||||
|
||||
for y := startY; y <= endY; y++ {
|
||||
line := m.Line(y)
|
||||
|
||||
// Handle lines shorter than the block selection
|
||||
if startX >= len(line) {
|
||||
content = append(content, "")
|
||||
continue
|
||||
}
|
||||
|
||||
lineEndX := min(endX, len(line))
|
||||
content = append(content, line[startX:lineEndX])
|
||||
}
|
||||
|
||||
m.UpdateDefault(action.BlockwiseRegister, content)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user