feat: implemented replace mode, tested!
Looking great, maybe I will actually use this lol
This commit is contained in:
parent
6033e58d0e
commit
43b3992522
@ -133,8 +133,8 @@
|
||||
- [ ] `U` - Undo all changes on line
|
||||
|
||||
### Other Normal Mode
|
||||
- [ ] `r{char}` - Replace character
|
||||
- [ ] `R` - Replace mode
|
||||
- [x] `r{char}` - Replace character
|
||||
- [x] `R` - Replace mode
|
||||
- [ ] `~` - Swap case of character
|
||||
- [ ] `ctrl+a` - Increment number
|
||||
- [ ] `ctrl+x` - Decrement number
|
||||
|
||||
@ -64,6 +64,14 @@ While the undo tree method that vim uses is powerful, I rarely find myself using
|
||||
approach is more natural to "non-vim" users and much simpler to implement. Implementing a feature similar
|
||||
to Vims undo tree would many times longer than a simple stack.
|
||||
|
||||
#### Vim-like Replace vs. Custom Replace
|
||||
|
||||
The way that vim's replace mode is implemented is quite complex, keeping track of the previous
|
||||
line backspace can only delete newly replaced characters. This is a complex feature, one that
|
||||
I rarely use, and even find a bit un-intuitive. Implementing replace mode in a way where all
|
||||
actions function the same as insert mode (other than the actual character typing) allows for
|
||||
a much simpler implementation, as well as a more intuitive user experience.
|
||||
|
||||
---
|
||||
|
||||
## TODO List
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
@ -61,12 +63,67 @@ func (a EnterReplace) WithCount(n int) Action {
|
||||
}
|
||||
|
||||
func (a EnterReplace) Execute(m Model) tea.Cmd {
|
||||
|
||||
m.SetCommandOutput(&core.CommandOutput{
|
||||
Lines: []string{"Replace mode (R) not implemented yet"},
|
||||
Inline: true,
|
||||
IsError: true,
|
||||
})
|
||||
|
||||
m.SetMode(core.ReplaceMode)
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReplaceModeChar struct {
|
||||
Char string
|
||||
}
|
||||
|
||||
// ReplaceModeChar.Execute: Inserts a single character at the cursor position, overwriting current
|
||||
// character.
|
||||
func (a ReplaceModeChar) Execute(m Model) tea.Cmd {
|
||||
win := m.ActiveWindow()
|
||||
buf := m.ActiveBuffer()
|
||||
|
||||
x, y := win.Cursor.Col, win.Cursor.Line
|
||||
l := buf.Line(y)
|
||||
if x < len(l) {
|
||||
buf.SetLine(y, l[:x]+a.Char+l[x+1:])
|
||||
} else {
|
||||
buf.SetLine(y, l+a.Char)
|
||||
}
|
||||
win.SetCursorCol(x + len(a.Char))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplaceNewline splits the current line at the cursor (enter key)
|
||||
type ReplaceNewline struct{}
|
||||
|
||||
// ReplaceNewline.Execute: Splits the current line at the cursor (Enter key).
|
||||
func (a ReplaceNewline) Execute(m Model) tea.Cmd {
|
||||
win := m.ActiveWindow()
|
||||
buf := m.ActiveBuffer()
|
||||
|
||||
x, y := win.Cursor.Col, win.Cursor.Line
|
||||
l := buf.Line(y)
|
||||
if x == len(l) {
|
||||
buf.InsertLine(y+1, "")
|
||||
} else {
|
||||
buf.SetLine(y, l[:x])
|
||||
buf.InsertLine(y+1, l[x+1:])
|
||||
}
|
||||
win.SetCursorPos(y+1, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplaceTab inserts spaces equal to the tab size
|
||||
type ReplaceTab struct{}
|
||||
|
||||
// ReplaceTab.Execute: Inserts spaces equal to the tab size (Tab key).
|
||||
func (a ReplaceTab) Execute(m Model) tea.Cmd {
|
||||
win := m.ActiveWindow()
|
||||
buf := m.ActiveBuffer()
|
||||
|
||||
x, y := win.Cursor.Col, win.Cursor.Line
|
||||
l := buf.Line(y)
|
||||
tabs := strings.Repeat(" ", m.Settings().TabStop)
|
||||
if x < len(l) {
|
||||
buf.SetLine(y, l[:x]+tabs+l[x+1:])
|
||||
} else {
|
||||
buf.SetLine(y, l+tabs)
|
||||
}
|
||||
win.SetCursorCol(x + len(tabs))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -12,13 +12,14 @@ const (
|
||||
VisualLineMode
|
||||
VisualBlockMode
|
||||
ReplaceMode
|
||||
WaitingMode // Same as NORMAL output, but cursor is the REPLACE cursor
|
||||
)
|
||||
|
||||
// Mode.ToString: Returns a human-readable string representation of the mode
|
||||
// for display in the status bar.
|
||||
func (m Mode) ToString() string {
|
||||
switch m {
|
||||
case NormalMode:
|
||||
case NormalMode, WaitingMode:
|
||||
return "NORMAL"
|
||||
case InsertMode:
|
||||
return "INSERT"
|
||||
|
||||
@ -34,6 +34,16 @@ func sendKeys(tm *teatest.TestModel, keys ...string) {
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlR})
|
||||
case "ctrl+w":
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlW})
|
||||
case "tab":
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyTab})
|
||||
case "left":
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyLeft})
|
||||
case "right":
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyRight})
|
||||
case "up":
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyUp})
|
||||
case "down":
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyDown})
|
||||
default:
|
||||
tm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(key)})
|
||||
}
|
||||
|
||||
@ -6,6 +6,10 @@ import (
|
||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||
)
|
||||
|
||||
// ==================================================
|
||||
// Replace Char (r) Tests
|
||||
// ==================================================
|
||||
|
||||
func TestReplaceChar(t *testing.T) {
|
||||
t.Run("test 'rx' replaces character under cursor", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
@ -564,3 +568,473 @@ func TestReplaceCharCombinations(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ==================================================
|
||||
// Replace Mode (R) Tests
|
||||
// ==================================================
|
||||
|
||||
func TestReplaceModeEntry(t *testing.T) {
|
||||
t.Run("test 'R' enters replace mode", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.Mode() != core.ReplaceMode {
|
||||
t.Errorf("Mode() = %v, want ReplaceMode", m.Mode())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'esc' exits replace mode to normal", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R", "x", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.Mode() != core.NormalMode {
|
||||
t.Errorf("Mode() = %v, want NormalMode", m.Mode())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplaceModeBasic(t *testing.T) {
|
||||
t.Run("test single character overwrites", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "Xello" {
|
||||
t.Errorf("lines[0] = %q, want 'Xello'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test multiple characters overwrite", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "abc")
|
||||
sendKeys(tm, "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "abclo" {
|
||||
t.Errorf("lines[0] = %q, want 'abclo'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test typing past end of line appends", func(t *testing.T) {
|
||||
lines := []string{"hi"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "there")
|
||||
sendKeys(tm, "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "hithere" {
|
||||
t.Errorf("lines[0] = %q, want 'hithere'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test replace mode starting at end of line", func(t *testing.T) {
|
||||
lines := []string{"hi"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "!!")
|
||||
sendKeys(tm, "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "hi!!" {
|
||||
t.Errorf("lines[0] = %q, want 'hi!!'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test replace mode from middle of line", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "XX")
|
||||
sendKeys(tm, "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "heXXo" {
|
||||
t.Errorf("lines[0] = %q, want 'heXXo'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test cursor position after exiting replace mode", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "abc")
|
||||
sendKeys(tm, "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Cursor should be at last replaced character
|
||||
if m.ActiveWindow().Cursor.Col != 2 {
|
||||
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplaceModeBackspace(t *testing.T) {
|
||||
t.Run("test backspace deletes character", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "abc")
|
||||
sendKeys(tm, "backspace", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "ablo" {
|
||||
t.Errorf("lines[0] = %q, want 'ablo'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test backspace at start does nothing", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R", "backspace", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "hello" {
|
||||
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test multiple backspaces", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "abc")
|
||||
sendKeys(tm, "backspace", "backspace", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "alo" {
|
||||
t.Errorf("lines[0] = %q, want 'alo'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test backspace then type more", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "abc")
|
||||
sendKeys(tm, "backspace")
|
||||
sendKeyString(tm, "XY")
|
||||
sendKeys(tm, "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "abXY" {
|
||||
t.Errorf("lines[0] = %q, want 'abXY'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplaceModeNavigation(t *testing.T) {
|
||||
t.Run("test right arrow moves cursor", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R", "right", "right", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "heXlo" {
|
||||
t.Errorf("lines[0] = %q, want 'heXlo'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test left arrow moves cursor", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
|
||||
sendKeys(tm, "R", "left", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "hXllo" {
|
||||
t.Errorf("lines[0] = %q, want 'hXllo'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test up/down arrows navigate between lines", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R", "X", "down", "Y", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "Xello" {
|
||||
t.Errorf("lines[0] = %q, want 'Xello'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
if m.ActiveBuffer().Lines[1].String() != "wYrld" {
|
||||
t.Errorf("lines[1] = %q, want 'wYrld'", m.ActiveBuffer().Lines[1].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test up arrow from first line stays on first line", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R", "up", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "Xello" {
|
||||
t.Errorf("lines[0] = %q, want 'Xello'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test down arrow from last line stays on last line", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
||||
sendKeys(tm, "R", "down", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[1].String() != "Xorld" {
|
||||
t.Errorf("lines[1] = %q, want 'Xorld'", m.ActiveBuffer().Lines[1].String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplaceModeSpecialKeys(t *testing.T) {
|
||||
t.Run("test enter splits line", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
|
||||
sendKeys(tm, "R", "enter", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "hello" {
|
||||
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
if m.ActiveBuffer().Lines[1].String() != "world" {
|
||||
t.Errorf("lines[1] = %q, want 'world'", m.ActiveBuffer().Lines[1].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test tab inserts tab character", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R", "tab", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Tab is expanded to spaces based on tabstop setting (default 2)
|
||||
if m.ActiveBuffer().Lines[0].String() != " ello" {
|
||||
t.Errorf("lines[0] = %q, want ' ello'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test ctrl+w deletes previous word", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "foo bar")
|
||||
sendKeys(tm, "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "foo orld" {
|
||||
t.Errorf("lines[0] = %q, want 'foo orld'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test ctrl+w at beginning does nothing", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R", "ctrl+w", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "hello" {
|
||||
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test delete key removes character ahead", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R", "delete", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "ello" {
|
||||
t.Errorf("lines[0] = %q, want 'ello'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplaceModeUndo(t *testing.T) {
|
||||
t.Run("test replace mode changes can be undone", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "abc")
|
||||
sendKeys(tm, "esc", "u")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "hello" {
|
||||
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test replace mode changes can be redone", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "abc")
|
||||
sendKeys(tm, "esc", "u", "ctrl+r")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "abclo" {
|
||||
t.Errorf("lines[0] = %q, want 'abclo'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test multiple undo operations", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "abc")
|
||||
sendKeys(tm, "esc")
|
||||
sendKeys(tm, "l", "R")
|
||||
sendKeyString(tm, "XY")
|
||||
sendKeys(tm, "esc", "u")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
// Only the second replace should be undone
|
||||
if m.ActiveBuffer().Lines[0].String() != "abclo" {
|
||||
t.Errorf("lines[0] = %q, want 'abclo'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplaceModeEdgeCases(t *testing.T) {
|
||||
t.Run("test replace mode on empty line", func(t *testing.T) {
|
||||
lines := []string{""}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "test")
|
||||
sendKeys(tm, "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "test" {
|
||||
t.Errorf("lines[0] = %q, want 'test'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test replace mode on different lines", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
||||
sendKeys(tm, "R", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "hello" {
|
||||
t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
if m.ActiveBuffer().Lines[1].String() != "Xorld" {
|
||||
t.Errorf("lines[1] = %q, want 'Xorld'", m.ActiveBuffer().Lines[1].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test overwriting tab character", func(t *testing.T) {
|
||||
lines := []string{"a\tb"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
|
||||
sendKeys(tm, "R", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "aXb" {
|
||||
t.Errorf("lines[0] = %q, want 'aXb'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test replace mode with whitespace characters", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "a b")
|
||||
sendKeys(tm, "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "a blo" {
|
||||
t.Errorf("lines[0] = %q, want 'a blo'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test replace mode on single character line", func(t *testing.T) {
|
||||
lines := []string{"a"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R", "X", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "X" {
|
||||
t.Errorf("lines[0] = %q, want 'X'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test replace mode preserves other lines", func(t *testing.T) {
|
||||
lines := []string{"first", "second", "third"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "XXX")
|
||||
sendKeys(tm, "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "first" {
|
||||
t.Errorf("lines[0] = %q, want 'first'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
if m.ActiveBuffer().Lines[1].String() != "XXXond" {
|
||||
t.Errorf("lines[1] = %q, want 'XXXond'", m.ActiveBuffer().Lines[1].String())
|
||||
}
|
||||
if m.ActiveBuffer().Lines[2].String() != "third" {
|
||||
t.Errorf("lines[2] = %q, want 'third'", m.ActiveBuffer().Lines[2].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test replace mode with special characters", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "@#$")
|
||||
sendKeys(tm, "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "@#$lo" {
|
||||
t.Errorf("lines[0] = %q, want '@#$lo'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplaceModeRepeat(t *testing.T) {
|
||||
t.Run("test replace mode can be repeated with '.'", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "XX")
|
||||
sendKeys(tm, "esc", "j", ".")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "XXllo" {
|
||||
t.Errorf("lines[0] = %q, want 'XXllo'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
if m.ActiveBuffer().Lines[1].String() != "wXXld" {
|
||||
t.Errorf("lines[1] = %q, want 'wXXld'", m.ActiveBuffer().Lines[1].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test dot repeats last replace operation", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R")
|
||||
sendKeyString(tm, "abc")
|
||||
sendKeys(tm, "esc", "w", ".")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "abclo abcld" {
|
||||
t.Errorf("lines[0] = %q, want 'abclo abcld'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test dot after navigation", func(t *testing.T) {
|
||||
lines := []string{"aaa", "bbb", "ccc"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "R", "X", "esc", "j", "j", ".")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Lines[0].String() != "Xaa" {
|
||||
t.Errorf("lines[0] = %q, want 'Xaa'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
if m.ActiveBuffer().Lines[2].String() != "Xcc" {
|
||||
t.Errorf("lines[2] = %q, want 'Xcc'", m.ActiveBuffer().Lines[2].String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -11,76 +11,7 @@ type ModelBuilder struct {
|
||||
model Model
|
||||
}
|
||||
|
||||
// RPGLE
|
||||
// abap
|
||||
// algol
|
||||
// algol_nu
|
||||
// arduino
|
||||
// ashen
|
||||
// aura-theme-dark
|
||||
// aura-theme-dark-soft
|
||||
// autumn
|
||||
// average
|
||||
// base16-snazzy
|
||||
// borland
|
||||
// bw
|
||||
// catppuccin-frappe
|
||||
// catppuccin-latte
|
||||
// catppuccin-macchiato
|
||||
// catppuccin-mocha
|
||||
// colorful
|
||||
// doom-one
|
||||
// doom-one2
|
||||
// dracula
|
||||
// emacs
|
||||
// evergarden
|
||||
// friendly
|
||||
// fruity
|
||||
// github
|
||||
// github-dark
|
||||
// gruvbox
|
||||
// gruvbox-light
|
||||
// hr_high_contrast
|
||||
// hrdark
|
||||
// igor
|
||||
// lovelace
|
||||
// manni
|
||||
// modus-operandi
|
||||
// modus-vivendi
|
||||
// monokai
|
||||
// monokailight
|
||||
// murphy
|
||||
// native
|
||||
// nord
|
||||
// nordic
|
||||
// onedark
|
||||
// onesenterprise
|
||||
// paraiso-dark
|
||||
// paraiso-light
|
||||
// pastie
|
||||
// perldoc
|
||||
// pygments
|
||||
// rainbow_dash
|
||||
// rose-pine
|
||||
// rose-pine-dawn
|
||||
// rose-pine-moon
|
||||
// rrt
|
||||
// solarized-dark
|
||||
// solarized-dark256
|
||||
// solarized-light
|
||||
// swapoff
|
||||
// tango
|
||||
// tokyonight-day
|
||||
// tokyonight-moon
|
||||
// tokyonight-night
|
||||
// tokyonight-storm
|
||||
// trac
|
||||
// vim
|
||||
// vs
|
||||
// vulcan
|
||||
// witchhazel
|
||||
// xcode
|
||||
// xcode-dark
|
||||
// NewModelBuilder: Builds and returns a new model, using the default color scheme (kanagawa-wave).
|
||||
func NewModelBuilder() *ModelBuilder {
|
||||
chromaStyle := styles.Get("kanagawa-wave")
|
||||
|
||||
|
||||
@ -41,6 +41,7 @@ type Handler struct {
|
||||
normalKeymap *Keymap
|
||||
visualKeymap *Keymap
|
||||
insertKeymap *Keymap
|
||||
replaceKeymap *Keymap
|
||||
commandKeymap *Keymap
|
||||
|
||||
currentKeymap *Keymap
|
||||
@ -53,6 +54,7 @@ func NewHandler() *Handler {
|
||||
normalKeymap: NewNormalKeymap(),
|
||||
visualKeymap: NewVisualKeymap(),
|
||||
insertKeymap: NewInsertKeymap(),
|
||||
replaceKeymap: NewReplaceKeymap(),
|
||||
commandKeymap: NewCommandKeymap(),
|
||||
currentKeymap: nil,
|
||||
}
|
||||
@ -77,7 +79,7 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
||||
|
||||
h.recordingKeys = []string{} // Clear recording on ESC
|
||||
h.Reset()
|
||||
if m.Mode() == core.InsertMode {
|
||||
if m.Mode() == core.InsertMode || m.Mode() == core.ReplaceMode {
|
||||
// Before exiting insert mode, end the block in the undo stack
|
||||
win := m.ActiveWindow()
|
||||
buf := m.ActiveBuffer()
|
||||
@ -95,6 +97,8 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
||||
switch m.Mode() {
|
||||
case core.InsertMode:
|
||||
return h.handleInsertKey(m, key)
|
||||
case core.ReplaceMode:
|
||||
return h.handleReplaceKey(m, key)
|
||||
case core.CommandMode:
|
||||
return h.handleCommandKey(m, key)
|
||||
}
|
||||
@ -173,7 +177,7 @@ func (h *Handler) dispatch(m action.Model, kind string, binding any, key string)
|
||||
// Handle character motions (f/t/F/T) - transition to waiting state
|
||||
if kind == "char_motion" {
|
||||
if key == "r" {
|
||||
m.SetMode(core.ReplaceMode)
|
||||
m.SetMode(core.WaitingMode)
|
||||
}
|
||||
h.charMotionType = key
|
||||
h.state = StateWaitingForChar
|
||||
@ -570,6 +574,34 @@ func (h *Handler) handleInsertKey(m action.Model, key string) tea.Cmd {
|
||||
return action.InsertChar{Char: key}.Execute(m)
|
||||
}
|
||||
|
||||
func (h *Handler) handleReplaceKey(m action.Model, key string) tea.Cmd {
|
||||
buf := m.ActiveBuffer()
|
||||
win := m.ActiveWindow()
|
||||
|
||||
// Start undo block on first insert key
|
||||
if buf.UndoStack != nil && !buf.UndoStack.Recording() {
|
||||
buf.UndoStack.BeginBlock(win.Cursor)
|
||||
}
|
||||
|
||||
// TODO: Handle differently
|
||||
|
||||
// Record the key for count replay (e.g. 5i...)
|
||||
m.SetInsertKeys(append(m.InsertKeys(), key))
|
||||
m.SetLastChangeKeys(append(m.LastChangeKeys(), key))
|
||||
|
||||
// Check the insert keymap first
|
||||
kind, binding := h.replaceKeymap.Lookup(key)
|
||||
switch kind {
|
||||
case "action":
|
||||
return binding.(action.Action).Execute(m)
|
||||
case "motion":
|
||||
return binding.(action.Motion).Execute(m)
|
||||
}
|
||||
|
||||
// Fallback: treat as a regular character to "insert"
|
||||
return action.ReplaceModeChar{Char: key}.Execute(m)
|
||||
}
|
||||
|
||||
// Handler.handleCommandKey: Processes a keypress in command mode, executing
|
||||
// it as an action or inserting it into the command line. This does not record
|
||||
// anything into the undo stack.
|
||||
|
||||
@ -148,6 +148,8 @@ func NewVisualKeymap() *Keymap {
|
||||
"X": operator.DeleteOperator{},
|
||||
"y": operator.YankOperator{},
|
||||
"c": operator.ChangeOperator{},
|
||||
"s": operator.ChangeOperator{}, // Same as c in visual mode
|
||||
"R": operator.ChangeOperator{}, // Seems to do the same thing
|
||||
},
|
||||
actions: map[string]action.Action{
|
||||
"p": action.VisualPaste{Count: 1, Replace: true},
|
||||
@ -206,7 +208,7 @@ func NewInsertKeymap() *Keymap {
|
||||
}
|
||||
}
|
||||
|
||||
// NewReplaceKeymap: Creates a keymap for replace mode with editing actions.
|
||||
// NewReplaceKeymap: Creates a keymap for replace mode with editing actions. All actions
|
||||
func NewReplaceKeymap() *Keymap {
|
||||
return &Keymap{
|
||||
motions: map[string]action.Motion{
|
||||
@ -217,10 +219,10 @@ func NewReplaceKeymap() *Keymap {
|
||||
},
|
||||
operators: map[string]action.Operator{}, // this will likely be empty
|
||||
actions: map[string]action.Action{
|
||||
"enter": action.InsertNewline{},
|
||||
"enter": action.ReplaceNewline{},
|
||||
"backspace": action.InsertBackspace{},
|
||||
"delete": action.InsertDelete{},
|
||||
"tab": action.InsertTab{},
|
||||
"tab": action.ReplaceTab{}, // TODO: This needs replacing
|
||||
"ctrl+w": action.InsertDeletePreviousWord{},
|
||||
},
|
||||
}
|
||||
|
||||
@ -186,8 +186,10 @@ func (s Styles) CursorStyle(mode core.Mode, style lipgloss.Style) lipgloss.Style
|
||||
return lipgloss.NewStyle().
|
||||
Background(style.GetForeground()).
|
||||
Foreground(style.GetBackground())
|
||||
case core.ReplaceMode:
|
||||
case core.ReplaceMode, core.WaitingMode:
|
||||
return lipgloss.NewStyle().
|
||||
Background(style.GetBackground()).
|
||||
Foreground(style.GetForeground()).
|
||||
Underline(true)
|
||||
default:
|
||||
return lipgloss.NewStyle().
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user