Compare commits
No commits in common. "c6215a37cb3b5c1c709b71b5be810a85d5ba734f" and "ddbc860530ae816865e854c17d51728db31ca5b7" have entirely different histories.
c6215a37cb
...
ddbc860530
@ -64,12 +64,6 @@ type Model interface {
|
|||||||
GetRegister(name rune) (core.Register, bool)
|
GetRegister(name rune) (core.Register, bool)
|
||||||
SetRegister(name rune, t core.RegisterType, cnt []string) error
|
SetRegister(name rune, t core.RegisterType, cnt []string) error
|
||||||
UpdateDefaultRegister(t core.RegisterType, cnt []string)
|
UpdateDefaultRegister(t core.RegisterType, cnt []string)
|
||||||
|
|
||||||
// Dot operator - accumulate keys for repeat
|
|
||||||
SetLastChangeKeys(keys []string)
|
|
||||||
LastChangeKeys() []string
|
|
||||||
ClearLastChangeKeys()
|
|
||||||
HandleKey(key string) tea.Cmd
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action is the base interface - anything executable
|
// Action is the base interface - anything executable
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package action
|
|||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
"git.gophernest.net/azpect/TextEditor/internal/style"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockModel is a shared test implementation of the Model interface.
|
// MockModel is a shared test implementation of the Model interface.
|
||||||
@ -24,7 +23,6 @@ type MockModel struct {
|
|||||||
CommandHistoryCur int
|
CommandHistoryCur int
|
||||||
LastFindVal core.LastFindCommand
|
LastFindVal core.LastFindCommand
|
||||||
StylesVal style.Styles
|
StylesVal style.Styles
|
||||||
LastChangeKeysList []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMockModel creates a mock with an empty buffer and 24x80 window.
|
// NewMockModel creates a mock with an empty buffer and 24x80 window.
|
||||||
@ -133,9 +131,3 @@ func (m *MockModel) SetRegister(name rune, t core.RegisterType, cnt []string) er
|
|||||||
func (m *MockModel) UpdateDefaultRegister(t core.RegisterType, cnt []string) {
|
func (m *MockModel) UpdateDefaultRegister(t core.RegisterType, cnt []string) {
|
||||||
m.RegistersMap['"'] = core.Register{Type: t, Content: cnt}
|
m.RegistersMap['"'] = core.Register{Type: t, Content: cnt}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dot operator
|
|
||||||
func (m *MockModel) SetLastChangeKeys(keys []string) { m.LastChangeKeysList = keys }
|
|
||||||
func (m *MockModel) LastChangeKeys() []string { return m.LastChangeKeysList }
|
|
||||||
func (m *MockModel) ClearLastChangeKeys() { m.LastChangeKeysList = []string{} }
|
|
||||||
func (m *MockModel) HandleKey(key string) tea.Cmd { return nil }
|
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
package action
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Repeat implements Action (.) - repeat last input
|
|
||||||
type Repeat struct {
|
|
||||||
Count int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Repeat) Execute(m Model) tea.Cmd {
|
|
||||||
keys := m.LastChangeKeys()
|
|
||||||
|
|
||||||
if len(keys) == 1 && keys[0] == "." {
|
|
||||||
m.SetCommandOutput(&core.CommandOutput{
|
|
||||||
Lines: []string{"Cannot repeat '.'"},
|
|
||||||
Inline: true,
|
|
||||||
IsError: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmds []tea.Cmd
|
|
||||||
for _, key := range keys {
|
|
||||||
cmd := m.HandleKey(key)
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tea.Batch(cmds...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure Repeat implements Repeatable
|
|
||||||
var _ Repeatable = Repeat{}
|
|
||||||
|
|
||||||
// Repeat.WithCount: Returns a new Repeat with the given count.
|
|
||||||
func (a Repeat) WithCount(n int) Action {
|
|
||||||
return Repeat{Count: n}
|
|
||||||
}
|
|
||||||
@ -82,8 +82,7 @@ func addSpecialRegisters(reg map[rune]Register) {
|
|||||||
|
|
||||||
// Small delete? Expression?
|
// Small delete? Expression?
|
||||||
|
|
||||||
// VIM: Last inserted text (readonly)
|
// Last inserted text (readonly)
|
||||||
// GIM: Content stored for the '.' operator (for debugging)
|
|
||||||
reg['.'] = emptyRegister()
|
reg['.'] = emptyRegister()
|
||||||
|
|
||||||
// Current file name (readonly)
|
// Current file name (readonly)
|
||||||
|
|||||||
@ -1,581 +0,0 @@
|
|||||||
package editor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// P0: Basic Recording Tests
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorRecording(t *testing.T) {
|
|
||||||
t.Run("records simple delete", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
||||||
sendKeys(tm, "x")
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
keys := m.LastChangeKeys()
|
|
||||||
if len(keys) != 1 || keys[0] != "x" {
|
|
||||||
t.Errorf("LastChangeKeys() = %v, want [\"x\"]", keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also verify . register
|
|
||||||
reg, ok := m.GetRegister('.')
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("dot register not found")
|
|
||||||
}
|
|
||||||
if reg.Content[0] != "x" {
|
|
||||||
t.Errorf("dot register = %q, want \"x\"", reg.Content[0])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("records operator motion", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello world"}))
|
|
||||||
sendKeys(tm, "d", "w")
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
keys := m.LastChangeKeys()
|
|
||||||
if len(keys) != 2 || keys[0] != "d" || keys[1] != "w" {
|
|
||||||
t.Errorf("LastChangeKeys() = %v, want [\"d\", \"w\"]", keys)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("records double press operator", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello", "world"}))
|
|
||||||
sendKeys(tm, "d", "d")
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
keys := m.LastChangeKeys()
|
|
||||||
if len(keys) != 2 || keys[0] != "d" || keys[1] != "d" {
|
|
||||||
t.Errorf("LastChangeKeys() = %v, want [\"d\", \"d\"]", keys)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("records visual operation", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3"}))
|
|
||||||
sendKeys(tm, "V", "j", "x")
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
keys := m.LastChangeKeys()
|
|
||||||
if len(keys) != 3 || keys[0] != "V" || keys[1] != "j" || keys[2] != "x" {
|
|
||||||
t.Errorf("LastChangeKeys() = %v, want [\"V\", \"j\", \"x\"]", keys)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("records insert mode", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
||||||
sendKeys(tm, "i", "X", "Y", "Z", "esc")
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
keys := m.LastChangeKeys()
|
|
||||||
if len(keys) != 5 || keys[0] != "i" || keys[1] != "X" || keys[2] != "Y" || keys[3] != "Z" || keys[4] != "esc" {
|
|
||||||
t.Errorf("LastChangeKeys() = %v, want [\"i\", \"X\", \"Y\", \"Z\", \"esc\"]", keys)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// P0: Non-Recording Tests
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorNonRecording(t *testing.T) {
|
|
||||||
t.Run("does not record pure motions", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3", "line4"}))
|
|
||||||
sendKeys(tm, "k", "k", "k")
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
keys := m.LastChangeKeys()
|
|
||||||
// Pure motions should result in empty recording
|
|
||||||
if len(keys) != 0 {
|
|
||||||
t.Errorf("LastChangeKeys() = %v, want []", keys)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("does not record dot operator itself", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
||||||
sendKeys(tm, "x", ".", ".")
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
keys := m.LastChangeKeys()
|
|
||||||
// Should still be just ["x"], not ["x", ".", "."]
|
|
||||||
if len(keys) != 1 || keys[0] != "x" {
|
|
||||||
t.Errorf("LastChangeKeys() = %v, want [\"x\"]", keys)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("does not record command mode entry", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
||||||
sendKeys(tm, ":")
|
|
||||||
sendKeys(tm, "esc") // Exit command mode
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
keys := m.LastChangeKeys()
|
|
||||||
// Command mode entry should not record
|
|
||||||
if len(keys) != 0 {
|
|
||||||
t.Errorf("LastChangeKeys() = %v, want []", keys)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("does not record visual mode entry without action", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
||||||
sendKeys(tm, "v", "esc")
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
keys := m.LastChangeKeys()
|
|
||||||
// Just entering and exiting visual mode should not record
|
|
||||||
if len(keys) != 0 {
|
|
||||||
t.Errorf("LastChangeKeys() = %v, want []", keys)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// P0: Basic Replay Tests
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorReplay(t *testing.T) {
|
|
||||||
t.Run("repeats simple delete", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
||||||
sendKeys(tm, "x") // "ello"
|
|
||||||
sendKeys(tm, ".") // "llo"
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
if m.ActiveBuffer().Line(0) != "llo" {
|
|
||||||
t.Errorf("buffer = %q, want \"llo\"", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("repeats operator motion", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"one two three"}))
|
|
||||||
sendKeys(tm, "d", "w") // "two three"
|
|
||||||
sendKeys(tm, ".") // "three"
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
if m.ActiveBuffer().Line(0) != "three" {
|
|
||||||
t.Errorf("buffer = %q, want \"three\"", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("repeats double press operator", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3"}))
|
|
||||||
sendKeys(tm, "d", "d") // Delete first line -> "line2", "line3"
|
|
||||||
sendKeys(tm, ".") // Delete next line -> "line3"
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
if m.ActiveBuffer().LineCount() != 1 {
|
|
||||||
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
|
|
||||||
}
|
|
||||||
if m.ActiveBuffer().Line(0) != "line3" {
|
|
||||||
t.Errorf("buffer = %q, want \"line3\"", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// P1: Recording Replacement Tests
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorRecordingReplacement(t *testing.T) {
|
|
||||||
t.Run("new action replaces old recording", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello world"}))
|
|
||||||
sendKeys(tm, "x") // Record ["x"]
|
|
||||||
sendKeys(tm, "l", "l") // Motions clear recording buffer but don't save
|
|
||||||
sendKeys(tm, "d", "w") // Record ["d", "w"]
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
keys := m.LastChangeKeys()
|
|
||||||
// Should be the latest action ["d", "w"], not ["x"]
|
|
||||||
if len(keys) != 2 || keys[0] != "d" || keys[1] != "w" {
|
|
||||||
t.Errorf("LastChangeKeys() = %v, want [\"d\", \"w\"]", keys)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("motions clear recording without saving", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3", "line4"}))
|
|
||||||
sendKeys(tm, "x") // Record ["x"]
|
|
||||||
sendKeys(tm, "j", "j", "j") // Motions don't overwrite saved recording
|
|
||||||
sendKeys(tm, "d", "d") // New action replaces
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
keys := m.LastChangeKeys()
|
|
||||||
// Should be ["d", "d"] from the last modifying action
|
|
||||||
if len(keys) != 2 || keys[0] != "d" || keys[1] != "d" {
|
|
||||||
t.Errorf("LastChangeKeys() = %v, want [\"d\", \"d\"]", keys)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// P1: Visual Mode Tests
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorVisualMode(t *testing.T) {
|
|
||||||
t.Run("repeats visual line operation", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"a", "b", "c", "d", "e"}))
|
|
||||||
sendKeys(tm, "V", "j", "x") // Delete lines 0-1 -> "c", "d", "e"
|
|
||||||
// Cursor should be at line 0 after deletion
|
|
||||||
sendKeys(tm, ".") // Repeat -> delete next 2 lines -> "e"
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
if m.ActiveBuffer().LineCount() != 1 {
|
|
||||||
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
|
|
||||||
}
|
|
||||||
if m.ActiveBuffer().Line(0) != "e" {
|
|
||||||
t.Errorf("buffer = %q, want \"e\"", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// P1: Insert Mode Tests
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorInsertMode(t *testing.T) {
|
|
||||||
t.Run("repeats insert mode operation", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
||||||
sendKeys(tm, "i", "X", "Y", "Z", "esc") // "XYZhello", cursor at col 2 (on Z)
|
|
||||||
// Move cursor after XYZ (to col 3, between Z and h)
|
|
||||||
sendKeys(tm, "l")
|
|
||||||
sendKeys(tm, ".") // Should insert XYZ again at col 3
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
if m.ActiveBuffer().Line(0) != "XYZXYZhello" {
|
|
||||||
t.Errorf("buffer = %q, want \"XYZXYZhello\"", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// P1: Multiple Repeat Tests
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorMultipleRepeats(t *testing.T) {
|
|
||||||
t.Run("dot can be used multiple times", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello world foo bar"}))
|
|
||||||
sendKeys(tm, "d", "w") // "world foo bar"
|
|
||||||
sendKeys(tm, ".") // "foo bar"
|
|
||||||
sendKeys(tm, ".") // "bar"
|
|
||||||
sendKeys(tm, ".") // ""
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
line := m.ActiveBuffer().Line(0)
|
|
||||||
// After deleting 4 words, should be empty or just whitespace
|
|
||||||
if line != "" && line != " " {
|
|
||||||
t.Errorf("buffer = %q, want empty or space", line)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// P2: Edge Cases
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorEdgeCases(t *testing.T) {
|
|
||||||
t.Run("repeat at start of file", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
||||||
sendKeys(tm, "x") // "ello"
|
|
||||||
sendKeys(tm, ".") // "llo"
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
if m.ActiveBuffer().Line(0) != "llo" {
|
|
||||||
t.Errorf("buffer = %q, want \"llo\"", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("repeat after undo", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello world"}))
|
|
||||||
sendKeys(tm, "x") // "ello world"
|
|
||||||
sendKeys(tm, "u") // Undo -> "hello world"
|
|
||||||
sendKeys(tm, ".") // Repeat should still work -> "ello world"
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
if m.ActiveBuffer().Line(0) != "ello world" {
|
|
||||||
t.Errorf("buffer = %q, want \"ello world\"", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("repeat with no recorded change", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
||||||
sendKeys(tm, ".") // Dot with nothing recorded
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
// Should not crash, buffer should be unchanged
|
|
||||||
if m.ActiveBuffer().Line(0) != "hello" {
|
|
||||||
t.Errorf("buffer = %q, want \"hello\"", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// P2: Integration with Counts
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorWithCounts(t *testing.T) {
|
|
||||||
t.Run("recording includes count", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello world"}))
|
|
||||||
sendKeys(tm, "3", "x")
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
keys := m.LastChangeKeys()
|
|
||||||
// Should record both the count and the action
|
|
||||||
if len(keys) != 2 || keys[0] != "3" || keys[1] != "x" {
|
|
||||||
t.Errorf("LastChangeKeys() = %v, want [\"3\", \"x\"]", keys)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("repeat preserves count", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
||||||
sendKeys(tm, "3", "x") // Delete 3 chars -> "lo"
|
|
||||||
sendKeys(tm, ".") // Should delete 3 more (or try to)
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
// After deleting 3 + 3 = 6 chars, should be empty or have no chars left
|
|
||||||
if m.ActiveBuffer().Line(0) != "" {
|
|
||||||
t.Errorf("buffer = %q, want empty", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// P2: Complex Sequences
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorComplexSequences(t *testing.T) {
|
|
||||||
t.Run("complex sequence of operations", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3"}))
|
|
||||||
sendKeys(tm, "x") // Delete 'l' from line1 -> ["ine1", "line2", "line3"]
|
|
||||||
sendKeys(tm, "j", "j") // Move to line 2
|
|
||||||
sendKeys(tm, "d", "d") // Delete line 2 (line3) -> ["ine1", "line2"], cursor at line 1
|
|
||||||
sendKeys(tm, "k") // Move up to line 0
|
|
||||||
sendKeys(tm, ".") // Repeat dd - deletes line 0 (ine1) -> ["line2"]
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
// After the sequence, dd was recorded and repeated at line 0, deleting it
|
|
||||||
if m.ActiveBuffer().LineCount() != 1 {
|
|
||||||
t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount())
|
|
||||||
}
|
|
||||||
if m.ActiveBuffer().Line(0) != "line2" {
|
|
||||||
t.Errorf("buffer = %q, want \"line2\"", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// Additional Coverage: Change Operator
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorChangeOperator(t *testing.T) {
|
|
||||||
t.Run("repeats change word", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"one two three"}))
|
|
||||||
sendKeys(tm, "c", "w", "X", "esc") // Change "one " to "X" -> "Xtwo three"
|
|
||||||
sendKeys(tm, "w") // Move to "two"
|
|
||||||
sendKeys(tm, ".") // Repeat cw -> change "two " to "X" -> "Xtwo X"
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
// cw deletes word and space after, result is "Xtwo X" + trailing content
|
|
||||||
if m.ActiveBuffer().Line(0) != "Xtwo Xthree" && m.ActiveBuffer().Line(0) != "Xtwo X" {
|
|
||||||
t.Errorf("buffer = %q, want \"Xtwo X\" or \"Xtwo Xthree\"", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("repeats change line", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"line1", "line2", "line3"}))
|
|
||||||
sendKeys(tm, "c", "c", "N", "E", "W", "esc") // Change line -> "NEW"
|
|
||||||
sendKeys(tm, "j") // Move to next line
|
|
||||||
sendKeys(tm, ".") // Repeat cc
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
if m.ActiveBuffer().LineCount() != 3 {
|
|
||||||
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
|
|
||||||
}
|
|
||||||
if m.ActiveBuffer().Line(0) != "NEW" || m.ActiveBuffer().Line(1) != "NEW" {
|
|
||||||
t.Errorf("lines = [%q, %q], want [\"NEW\", \"NEW\"]",
|
|
||||||
m.ActiveBuffer().Line(0), m.ActiveBuffer().Line(1))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// Additional Coverage: Paste Operations
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorPaste(t *testing.T) {
|
|
||||||
t.Run("repeats paste", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello", "world"}))
|
|
||||||
sendKeys(tm, "y", "y") // Yank line
|
|
||||||
sendKeys(tm, "p") // Paste -> "hello", "hello", "world"
|
|
||||||
sendKeys(tm, ".") // Repeat paste -> "hello", "hello", "hello", "world"
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
if m.ActiveBuffer().LineCount() != 4 {
|
|
||||||
t.Errorf("LineCount() = %d, want 4", m.ActiveBuffer().LineCount())
|
|
||||||
}
|
|
||||||
if m.ActiveBuffer().Line(0) != "hello" || m.ActiveBuffer().Line(1) != "hello" || m.ActiveBuffer().Line(2) != "hello" {
|
|
||||||
t.Errorf("first 3 lines should be \"hello\"")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// Additional Coverage: Append Mode
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorAppendMode(t *testing.T) {
|
|
||||||
t.Run("repeats append", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
||||||
sendKeys(tm, "a", "X", "Y", "esc") // Append XY after 'h' -> "hXYello", cursor at Y
|
|
||||||
sendKeys(tm, "l") // Move right one char
|
|
||||||
sendKeys(tm, ".") // Repeat append at new position
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
// Result will depend on exact cursor behavior after 'a' mode
|
|
||||||
if m.ActiveBuffer().Line(0) != "hXYeXYllo" {
|
|
||||||
t.Errorf("buffer = %q, want \"hXYeXYllo\"", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("repeats append at end of line", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello"}))
|
|
||||||
sendKeys(tm, "A", "!", "esc") // Append at end -> "hello!"
|
|
||||||
sendKeys(tm, "j") // Move to another line (if exists) or stay
|
|
||||||
sendKeys(tm, ".") // Repeat A!
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
// Should append at end of current line
|
|
||||||
if m.ActiveBuffer().Line(0) != "hello!!" {
|
|
||||||
t.Errorf("buffer = %q, want \"hello!!\"", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// Additional Coverage: Visual Character Mode
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorVisualCharMode(t *testing.T) {
|
|
||||||
t.Run("repeats visual char delete", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello world"}))
|
|
||||||
sendKeys(tm, "v", "l", "l", "x") // Select "hel" and delete -> "lo world"
|
|
||||||
sendKeys(tm, ".") // Repeat -> delete "lo " -> "world"
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
if m.ActiveBuffer().Line(0) != "world" {
|
|
||||||
t.Errorf("buffer = %q, want \"world\"", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// Additional Coverage: Text Objects
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorTextObjects(t *testing.T) {
|
|
||||||
t.Run("repeats delete inner word", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"one two three"}))
|
|
||||||
sendKeys(tm, "d", "i", "w") // Delete "one" -> " two three"
|
|
||||||
sendKeys(tm, "w") // Move to "two"
|
|
||||||
sendKeys(tm, ".") // Repeat diw -> delete "two"
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
if m.ActiveBuffer().Line(0) != " three" {
|
|
||||||
t.Errorf("buffer = %q, want \" three\"", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("repeats delete a word", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"one two three"}))
|
|
||||||
sendKeys(tm, "d", "a", "w") // Delete "one " -> "two three"
|
|
||||||
sendKeys(tm, ".") // Repeat daw -> delete "two "
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
if m.ActiveBuffer().Line(0) != "three" {
|
|
||||||
t.Errorf("buffer = %q, want \"three\"", m.ActiveBuffer().Line(0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// Additional Coverage: Open Line
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorOpenLine(t *testing.T) {
|
|
||||||
t.Run("repeats open line below", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"line1", "line2"}))
|
|
||||||
sendKeys(tm, "o", "N", "E", "W", "esc") // Open below and insert "NEW"
|
|
||||||
sendKeys(tm, "j") // Move down
|
|
||||||
sendKeys(tm, ".") // Repeat
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
// Should have: line1, NEW, line2, NEW
|
|
||||||
if m.ActiveBuffer().LineCount() != 4 {
|
|
||||||
t.Errorf("LineCount() = %d, want 4", m.ActiveBuffer().LineCount())
|
|
||||||
}
|
|
||||||
if m.ActiveBuffer().Line(1) != "NEW" || m.ActiveBuffer().Line(3) != "NEW" {
|
|
||||||
t.Errorf("lines[1] = %q, lines[3] = %q, both want \"NEW\"",
|
|
||||||
m.ActiveBuffer().Line(1), m.ActiveBuffer().Line(3))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("repeats open line above", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"line1", "line2"}))
|
|
||||||
sendKeys(tm, "j") // Move to line2
|
|
||||||
sendKeys(tm, "O", "T", "O", "P", "esc") // Open above and insert "TOP"
|
|
||||||
sendKeys(tm, "j", "j") // Move down past inserted line
|
|
||||||
sendKeys(tm, ".") // Repeat
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
// Should have: line1, TOP, TOP, line2
|
|
||||||
if m.ActiveBuffer().LineCount() != 4 {
|
|
||||||
t.Errorf("LineCount() = %d, want 4", m.ActiveBuffer().LineCount())
|
|
||||||
}
|
|
||||||
if m.ActiveBuffer().Line(1) != "TOP" || m.ActiveBuffer().Line(2) != "TOP" {
|
|
||||||
t.Errorf("lines[1] = %q, lines[2] = %q, both want \"TOP\"",
|
|
||||||
m.ActiveBuffer().Line(1), m.ActiveBuffer().Line(2))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// Additional Coverage: Character Find Motions
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func TestDotOperatorCharMotions(t *testing.T) {
|
|
||||||
t.Run("repeats delete to char", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello world foo"}))
|
|
||||||
sendKeys(tm, "d", "f", "o") // Delete from 'h' until and including first 'o' -> "llo world foo"
|
|
||||||
sendKeys(tm, "w") // Move to next word
|
|
||||||
sendKeys(tm, ".") // Repeat dfo
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
// After dfo from start: "llo world foo"
|
|
||||||
// After w: cursor somewhere in the line
|
|
||||||
// After . (repeat dfo): delete until next 'o'
|
|
||||||
// Actual result will depend on word motion and where 'o' is found
|
|
||||||
line := m.ActiveBuffer().Line(0)
|
|
||||||
if len(line) == 0 {
|
|
||||||
t.Errorf("buffer should not be empty after two dfo operations")
|
|
||||||
}
|
|
||||||
if line != " rld foo" {
|
|
||||||
t.Errorf("line is '%s', but expected ' rld foo'", line)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("repeats change until char", func(t *testing.T) {
|
|
||||||
tm := newTestModel(t, WithLines([]string{"hello;world;end"}))
|
|
||||||
sendKeys(tm, "c", "t", ";", "X", "esc") // Change "hello" (until ;) to "X" -> "X;world;end"
|
|
||||||
sendKeys(tm, "f", ";") // Move to first ';'
|
|
||||||
sendKeys(tm, "l") // Move past ';' to 'w'
|
|
||||||
sendKeys(tm, ".") // Repeat ct; from 'w'
|
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
|
||||||
// After ct; from 'w': changes "world" (until next ;) to "X" -> result varies
|
|
||||||
// Accept the actual implementation behavior
|
|
||||||
line := m.ActiveBuffer().Line(0)
|
|
||||||
if len(line) < 3 {
|
|
||||||
t.Errorf("buffer = %q, seems too short", line)
|
|
||||||
}
|
|
||||||
if line != "Xo;Xd;end" {
|
|
||||||
t.Errorf("line is '%s', but expected 'Xo;Xd;end'", line)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -51,9 +51,6 @@ type Model struct {
|
|||||||
|
|
||||||
// Visual styles
|
// Visual styles
|
||||||
styles style.Styles
|
styles style.Styles
|
||||||
|
|
||||||
// Dot operator state
|
|
||||||
lastChangeKeys []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model.Init: Initialize the model and start any commands that may need to run. Required
|
// Model.Init: Initialize the model and start any commands that may need to run. Required
|
||||||
@ -123,25 +120,6 @@ func (m *Model) GetLastFind() *core.LastFindCommand {
|
|||||||
return &m.lastFind
|
return &m.lastFind
|
||||||
}
|
}
|
||||||
|
|
||||||
// Does update the '.' register
|
|
||||||
func (m *Model) SetLastChangeKeys(keys []string) {
|
|
||||||
m.lastChangeKeys = keys
|
|
||||||
|
|
||||||
m.SetRegister('.', core.CharwiseRegister, []string{strings.Join(keys, "")})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) LastChangeKeys() []string {
|
|
||||||
return m.lastChangeKeys
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) ClearLastChangeKeys() {
|
|
||||||
m.lastChangeKeys = []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) HandleKey(key string) tea.Cmd {
|
|
||||||
return m.input.Handle(m, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) ExitInsertMode() {
|
func (m *Model) ExitInsertMode() {
|
||||||
win := m.ActiveWindow()
|
win := m.ActiveWindow()
|
||||||
if m.insertCount > 1 {
|
if m.insertCount > 1 {
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
package input
|
package input
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/operator"
|
"git.gophernest.net/azpect/TextEditor/internal/operator"
|
||||||
@ -34,9 +32,6 @@ type Handler struct {
|
|||||||
charMotionType string // which char motion is waiting: "f", "t", "F", or "T"
|
charMotionType string // which char motion is waiting: "f", "t", "F", or "T"
|
||||||
modifier string // which modifier used for text object: "i" or "a"
|
modifier string // which modifier used for text object: "i" or "a"
|
||||||
|
|
||||||
// Dot operator - accumulate keys for current operation
|
|
||||||
recordingKeys []string
|
|
||||||
|
|
||||||
// Keymaps
|
// Keymaps
|
||||||
normalKeymap *Keymap
|
normalKeymap *Keymap
|
||||||
visualKeymap *Keymap
|
visualKeymap *Keymap
|
||||||
@ -61,21 +56,8 @@ func NewHandler() *Handler {
|
|||||||
// Handler.Handle: Main entry point for processing a keypress. Routes to appropriate
|
// Handler.Handle: Main entry point for processing a keypress. Routes to appropriate
|
||||||
// handler based on current mode and state.
|
// handler based on current mode and state.
|
||||||
func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
||||||
ignoreKeys := []string{".", "u", "ctrl+r"}
|
|
||||||
|
|
||||||
// Record key for dot operator (except in insert/command mode which handle separately)
|
|
||||||
if m.Mode() != core.InsertMode && m.Mode() != core.CommandMode && !slices.Contains(ignoreKeys, key) {
|
|
||||||
h.recordingKeys = append(h.recordingKeys, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ESC always resets everything
|
// ESC always resets everything
|
||||||
if key == "esc" {
|
if key == "esc" {
|
||||||
// If insert mode, keep the escape
|
|
||||||
if m.Mode() == core.InsertMode {
|
|
||||||
m.SetLastChangeKeys(append(m.LastChangeKeys(), key))
|
|
||||||
}
|
|
||||||
|
|
||||||
h.recordingKeys = []string{} // Clear recording on ESC
|
|
||||||
h.Reset()
|
h.Reset()
|
||||||
if m.Mode() == core.InsertMode {
|
if m.Mode() == core.InsertMode {
|
||||||
// Before exiting insert mode, end the block in the undo stack
|
// Before exiting insert mode, end the block in the undo stack
|
||||||
@ -206,12 +188,6 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
|
|||||||
mot = res.Resolve(m)
|
mot = res.Resolve(m)
|
||||||
}
|
}
|
||||||
cmd := h.executeMotion(m, mot)
|
cmd := h.executeMotion(m, mot)
|
||||||
|
|
||||||
// Only clear recording for pure motions in normal mode
|
|
||||||
// In visual mode, motions are part of building the selection
|
|
||||||
if !m.Mode().IsVisualMode() {
|
|
||||||
h.recordingKeys = []string{}
|
|
||||||
}
|
|
||||||
h.Reset()
|
h.Reset()
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
@ -230,7 +206,7 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
|
|||||||
if m.Mode() != core.InsertMode {
|
if m.Mode() != core.InsertMode {
|
||||||
m.SetMode(core.NormalMode)
|
m.SetMode(core.NormalMode)
|
||||||
}
|
}
|
||||||
h.RecordAndReset(m)
|
h.Reset()
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
// In normal mode, wait for a motion to define the range
|
// In normal mode, wait for a motion to define the range
|
||||||
@ -245,12 +221,7 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
|
|||||||
act = r.WithCount(count)
|
act = r.WithCount(count)
|
||||||
}
|
}
|
||||||
cmd := h.executeAction(m, act)
|
cmd := h.executeAction(m, act)
|
||||||
// Only record if we're not entering visual mode (visual ops record when they complete)
|
h.Reset()
|
||||||
if m.Mode().IsVisualMode() {
|
|
||||||
h.Reset() // In visual mode now, don't save yet
|
|
||||||
} else {
|
|
||||||
h.RecordAndReset(m)
|
|
||||||
}
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,7 +240,7 @@ func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any,
|
|||||||
// Only call DoublePress if the operator supports it
|
// Only call DoublePress if the operator supports it
|
||||||
if dp, ok := h.operator.(action.DoublePresser); ok {
|
if dp, ok := h.operator.(action.DoublePresser); ok {
|
||||||
cmd := h.executeDoublePress(m, dp, count)
|
cmd := h.executeDoublePress(m, dp, count)
|
||||||
h.RecordAndReset(m)
|
h.Reset()
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
h.Reset()
|
h.Reset()
|
||||||
@ -297,7 +268,7 @@ func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any,
|
|||||||
h.executeMotion(m, mot)
|
h.executeMotion(m, mot)
|
||||||
end := win.Cursor
|
end := win.Cursor
|
||||||
cmd := h.executeOperator(m, h.operator, start, end, mot.Type())
|
cmd := h.executeOperator(m, h.operator, start, end, mot.Type())
|
||||||
h.RecordAndReset(m)
|
h.Reset()
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,7 +343,7 @@ func (h *Handler) handleCharMotion(m action.Model, key string) tea.Cmd {
|
|||||||
h.executeMotion(m, mot)
|
h.executeMotion(m, mot)
|
||||||
end := win.Cursor
|
end := win.Cursor
|
||||||
cmd := h.executeOperator(m, h.operator, start, end, mot.Type())
|
cmd := h.executeOperator(m, h.operator, start, end, mot.Type())
|
||||||
h.RecordAndReset(m)
|
h.Reset()
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,7 +374,7 @@ func (h *Handler) handleTextObject(m action.Model, kind string, binding any, key
|
|||||||
// If we have an operator pending (e.g., "diw")
|
// If we have an operator pending (e.g., "diw")
|
||||||
if h.operator != nil {
|
if h.operator != nil {
|
||||||
cmd := h.executeOperator(m, h.operator, start, end, mtype)
|
cmd := h.executeOperator(m, h.operator, start, end, mtype)
|
||||||
h.RecordAndReset(m)
|
h.Reset()
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,7 +456,6 @@ func (h *Handler) effectiveCount() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handler.Reset: Clears all handler state including counts, operators, and buffers.
|
// Handler.Reset: Clears all handler state including counts, operators, and buffers.
|
||||||
// Does NOT clear recordingKeys - those accumulate across an operation.
|
|
||||||
func (h *Handler) Reset() {
|
func (h *Handler) Reset() {
|
||||||
h.state = StateReady
|
h.state = StateReady
|
||||||
h.count1 = 0
|
h.count1 = 0
|
||||||
@ -496,28 +466,6 @@ func (h *Handler) Reset() {
|
|||||||
h.pending = ""
|
h.pending = ""
|
||||||
h.charMotionType = ""
|
h.charMotionType = ""
|
||||||
h.modifier = ""
|
h.modifier = ""
|
||||||
// NOTE: recordingKeys is NOT cleared here - it accumulates across the operation
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) RecordAndReset(m action.Model) {
|
|
||||||
// Save the recorded keys to the model for dot operator
|
|
||||||
// Filter out mode-switch keys that don't modify the buffer
|
|
||||||
ignoreStates := []string{":", "v", "V", "."}
|
|
||||||
|
|
||||||
if len(h.recordingKeys) > 0 {
|
|
||||||
// Check if the entire sequence is just a mode switch
|
|
||||||
shouldRecord := true
|
|
||||||
if len(h.recordingKeys) == 1 && slices.Contains(ignoreStates, h.recordingKeys[0]) {
|
|
||||||
shouldRecord = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if shouldRecord {
|
|
||||||
m.SetLastChangeKeys(h.recordingKeys)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h.recordingKeys = []string{} // Clear recording after saving
|
|
||||||
h.Reset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler.Pending: Returns the accumulated input buffer for display.
|
// Handler.Pending: Returns the accumulated input buffer for display.
|
||||||
@ -541,7 +489,6 @@ func (h *Handler) handleInsertKey(m action.Model, key string) tea.Cmd {
|
|||||||
|
|
||||||
// Record the key for count replay (e.g. 5i...)
|
// Record the key for count replay (e.g. 5i...)
|
||||||
m.SetInsertKeys(append(m.InsertKeys(), key))
|
m.SetInsertKeys(append(m.InsertKeys(), key))
|
||||||
m.SetLastChangeKeys(append(m.LastChangeKeys(), key))
|
|
||||||
|
|
||||||
// Check the insert keymap first
|
// Check the insert keymap first
|
||||||
kind, binding := h.insertKeymap.Lookup(key)
|
kind, binding := h.insertKeymap.Lookup(key)
|
||||||
|
|||||||
@ -41,7 +41,7 @@ func NewNormalKeymap() *Keymap {
|
|||||||
"ctrl+u": motion.ScrollUpHalfPage{},
|
"ctrl+u": motion.ScrollUpHalfPage{},
|
||||||
"ctrl+d": motion.ScrollDownHalfPage{},
|
"ctrl+d": motion.ScrollDownHalfPage{},
|
||||||
";": action.RepeatFind{Count: 1, Reverse: false},
|
";": action.RepeatFind{Count: 1, Reverse: false},
|
||||||
",": action.RepeatFind{Count: 1, Reverse: true},
|
".": action.RepeatFind{Count: 1, Reverse: true},
|
||||||
},
|
},
|
||||||
operators: map[string]action.Operator{
|
operators: map[string]action.Operator{
|
||||||
"d": operator.DeleteOperator{},
|
"d": operator.DeleteOperator{},
|
||||||
@ -71,7 +71,6 @@ func NewNormalKeymap() *Keymap {
|
|||||||
"P": action.PasteBefore{Count: 1},
|
"P": action.PasteBefore{Count: 1},
|
||||||
"u": action.Undo{},
|
"u": action.Undo{},
|
||||||
"ctrl+r": action.Redo{},
|
"ctrl+r": action.Redo{},
|
||||||
".": action.Repeat{Count: 1},
|
|
||||||
},
|
},
|
||||||
charMotions: map[string]action.Motion{
|
charMotions: map[string]action.Motion{
|
||||||
"f": action.FindChar{Forward: true, Inclusive: true, Repeated: false},
|
"f": action.FindChar{Forward: true, Inclusive: true, Repeated: false},
|
||||||
@ -135,7 +134,6 @@ func NewVisualKeymap() *Keymap {
|
|||||||
},
|
},
|
||||||
actions: map[string]action.Action{
|
actions: map[string]action.Action{
|
||||||
"p": action.VisualPaste{Count: 1},
|
"p": action.VisualPaste{Count: 1},
|
||||||
".": action.Repeat{Count: 1},
|
|
||||||
// ":": action.EnterComandMode{}, // Different OP
|
// ":": action.EnterComandMode{}, // Different OP
|
||||||
},
|
},
|
||||||
charMotions: map[string]action.Motion{
|
charMotions: map[string]action.Motion{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user