fix: fixed replace action (r) in visual mode, tested
All checks were successful
Run Test Suite / test (push) Successful in 18s
All checks were successful
Run Test Suite / test (push) Successful in 18s
This commit is contained in:
parent
7a7472fd12
commit
32fe3f1edd
@ -35,6 +35,14 @@ func (a ReplaceChar) Execute(m Model) tea.Cmd {
|
||||
buf.UndoStack.BeginBlock(win.Cursor)
|
||||
}
|
||||
|
||||
switch m.Mode() {
|
||||
case core.VisualMode:
|
||||
replaceVisualCharSelection(m, a.Char)
|
||||
case core.VisualLineMode:
|
||||
replaceVisualLineSelection(m, a.Char)
|
||||
case core.VisualBlockMode:
|
||||
replaceVisualBlockSelection(m, a.Char)
|
||||
default:
|
||||
pos := win.Cursor.Col
|
||||
line := buf.Line(win.Cursor.Line)
|
||||
for i := 0; i < a.Count && pos < len(line); i++ {
|
||||
@ -44,6 +52,8 @@ func (a ReplaceChar) Execute(m Model) tea.Cmd {
|
||||
}
|
||||
|
||||
win.SetCursorCol(pos - 1)
|
||||
}
|
||||
|
||||
m.SetMode(core.NormalMode)
|
||||
|
||||
if buf.UndoStack != nil {
|
||||
@ -53,6 +63,80 @@ func (a ReplaceChar) Execute(m Model) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func replaceVisualCharSelection(m Model, char string) {
|
||||
win := m.ActiveWindow()
|
||||
buf := m.ActiveBuffer()
|
||||
|
||||
start, end := normalizeSelection(m)
|
||||
|
||||
for y := start.Line; y <= end.Line; y++ {
|
||||
line := buf.Line(y)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
from := 0
|
||||
to := len(line) - 1
|
||||
if y == start.Line {
|
||||
from = start.Col
|
||||
}
|
||||
if y == end.Line {
|
||||
to = min(end.Col, len(line)-1)
|
||||
}
|
||||
|
||||
if from < 0 {
|
||||
from = 0
|
||||
}
|
||||
if from >= len(line) || to < from {
|
||||
continue
|
||||
}
|
||||
|
||||
replaced := strings.Repeat(char, to-from+1)
|
||||
buf.SetLine(y, line[:from]+replaced+line[to+1:])
|
||||
}
|
||||
|
||||
win.SetCursorPos(start.Line, start.Col)
|
||||
}
|
||||
|
||||
func replaceVisualLineSelection(m Model, char string) {
|
||||
win := m.ActiveWindow()
|
||||
buf := m.ActiveBuffer()
|
||||
|
||||
start, end := normalizeSelection(m)
|
||||
|
||||
for y := start.Line; y <= end.Line; y++ {
|
||||
line := buf.Line(y)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
buf.SetLine(y, strings.Repeat(char, len(line)))
|
||||
}
|
||||
|
||||
win.SetCursorPos(start.Line, 0)
|
||||
}
|
||||
|
||||
func replaceVisualBlockSelection(m Model, char string) {
|
||||
win := m.ActiveWindow()
|
||||
buf := m.ActiveBuffer()
|
||||
|
||||
start, end := normalizeSelection(m)
|
||||
startCol := min(start.Col, end.Col)
|
||||
endCol := max(start.Col, end.Col)
|
||||
|
||||
for y := start.Line; y <= end.Line; y++ {
|
||||
line := buf.Line(y)
|
||||
if startCol >= len(line) {
|
||||
continue
|
||||
}
|
||||
|
||||
ec := min(endCol, len(line)-1)
|
||||
replaced := strings.Repeat(char, ec-startCol+1)
|
||||
buf.SetLine(y, line[:startCol]+replaced+line[ec+1:])
|
||||
}
|
||||
|
||||
win.SetCursorPos(start.Line, startCol)
|
||||
}
|
||||
|
||||
type EnterReplace struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
@ -1038,3 +1038,121 @@ func TestReplaceModeRepeat(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ==================================================
|
||||
// Visual Replace Char (v/V/ctrl+v + r{char}) Tests
|
||||
// ==================================================
|
||||
|
||||
func TestVisualReplaceChar(t *testing.T) {
|
||||
t.Run("test 'vlllrx' replaces each selected char in characterwise visual mode", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "v", "l", "l", "l", "r", "x")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Line(0) != "xxxxo world" {
|
||||
t.Errorf("Line(0) = %q, want %q", m.ActiveBuffer().Line(0), "xxxxo world")
|
||||
}
|
||||
if m.Mode() != core.NormalMode {
|
||||
t.Errorf("Mode() = %v, want %v", m.Mode(), core.NormalMode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test backward characterwise selection with 'r'", func(t *testing.T) {
|
||||
lines := []string{"hello"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
|
||||
sendKeys(tm, "v", "h", "h", "r", "z")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Line(0) != "hezzz" {
|
||||
t.Errorf("Line(0) = %q, want %q", m.ActiveBuffer().Line(0), "hezzz")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test 'VjrX' replaces all characters in selected lines", func(t *testing.T) {
|
||||
lines := []string{"abc", "de", "fghi"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "V", "j", "r", "X")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
assertReplaceVisualLines(t, m, []string{"XXX", "XX", "fghi"})
|
||||
if m.Mode() != core.NormalMode {
|
||||
t.Errorf("Mode() = %v, want %v", m.Mode(), core.NormalMode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test visual line backward selection with 'r'", func(t *testing.T) {
|
||||
lines := []string{"one", "two", "three"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
|
||||
sendKeys(tm, "V", "k", "r", "_")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
assertReplaceVisualLines(t, m, []string{"one", "___", "_____"})
|
||||
})
|
||||
|
||||
t.Run("test 'ctrl+vljrx' replaces each char in block selection", func(t *testing.T) {
|
||||
lines := []string{"hello", "world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "ctrl+v", "l", "j", "r", "x")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
assertReplaceVisualLines(t, m, []string{"xxllo", "xxrld"})
|
||||
})
|
||||
|
||||
t.Run("test block replace with ragged line lengths replaces available chars", func(t *testing.T) {
|
||||
lines := []string{"abcd", "xy", "1234"}
|
||||
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
|
||||
sendKeys(tm, "ctrl+v", "l", "j", "j", "r", "q")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
assertReplaceVisualLines(t, m, []string{"aqqd", "xq", "1qq4"})
|
||||
})
|
||||
}
|
||||
|
||||
// ==================================================
|
||||
// Visual Replace Char Repeat (.) Tests
|
||||
// ==================================================
|
||||
|
||||
func TestVisualReplaceCharRepeat(t *testing.T) {
|
||||
t.Run("test dot repeats characterwise visual replace", func(t *testing.T) {
|
||||
lines := []string{"hello world"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "v", "l", "l", "r", "x", "w", ".")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.ActiveBuffer().Line(0) != "xxxlo xxxld" {
|
||||
t.Errorf("Line(0) = %q, want %q", m.ActiveBuffer().Line(0), "xxxlo xxxld")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test dot repeats visual line replace on next line", func(t *testing.T) {
|
||||
lines := []string{"abcde", "vwxyz"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "V", "r", "_", "j", ".")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
assertReplaceVisualLines(t, m, []string{"_____", "_____"})
|
||||
})
|
||||
|
||||
t.Run("test dot repeats visual block replace at new location", func(t *testing.T) {
|
||||
lines := []string{"abcdef", "ghijkl", "mnopqr", "stuvwx"}
|
||||
tm := newTestModelWithLines(t, lines)
|
||||
sendKeys(tm, "ctrl+v", "l", "j", "r", "*", "j", "j", ".")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
assertReplaceVisualLines(t, m, []string{"**cdef", "**ijkl", "**opqr", "**uvwx"})
|
||||
})
|
||||
}
|
||||
|
||||
func assertReplaceVisualLines(t *testing.T, m *Model, want []string) {
|
||||
t.Helper()
|
||||
if m.ActiveBuffer().LineCount() != len(want) {
|
||||
t.Fatalf("LineCount() = %d, want %d", m.ActiveBuffer().LineCount(), len(want))
|
||||
}
|
||||
|
||||
for i := range want {
|
||||
if m.ActiveBuffer().Line(i) != want[i] {
|
||||
t.Errorf("Line(%d) = %q, want %q", i, m.ActiveBuffer().Line(i), want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,7 +176,7 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
||||
func (h *Handler) dispatch(m action.Model, kind string, binding any, key string) tea.Cmd {
|
||||
// Handle character motions (f/t/F/T) - transition to waiting state
|
||||
if kind == "char_motion" {
|
||||
if key == "r" {
|
||||
if key == "r" && !m.Mode().IsVisualMode() {
|
||||
m.SetMode(core.WaitingMode)
|
||||
}
|
||||
h.charMotionType = key
|
||||
|
||||
@ -170,6 +170,7 @@ func NewVisualKeymap() *Keymap {
|
||||
"F": action.FindChar{Forward: false, Inclusive: true},
|
||||
"t": action.FindChar{Forward: true, Inclusive: false},
|
||||
"T": action.FindChar{Forward: false, Inclusive: false},
|
||||
"r": action.ReplaceChar{Count: 1},
|
||||
},
|
||||
modifiers: map[string]any{
|
||||
"i": nil,
|
||||
|
||||
@ -6,7 +6,7 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// ChangeOperator implements Operator (c) - changes (deletes and enters insert mode) text.
|
||||
// ChangeOperator implements Operator (c, s, R) - changes (deletes and enters insert mode) text.
|
||||
type ChangeOperator struct{}
|
||||
|
||||
// ChangeOperator.Operate: Changes text based on the current mode and motion type.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user