feat: substitute and change motions/actions, tested
This commit is contained in:
parent
63fec65be4
commit
db52b63db1
109
internal/action/change.go
Normal file
109
internal/action/change.go
Normal file
@ -0,0 +1,109 @@
|
||||
package action
|
||||
|
||||
import tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
// ChangeToEndOfLine implements Action (C) - changes from cursor to end of line
|
||||
type ChangeToEndOfLine struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
func (a ChangeToEndOfLine) Execute(m Model) tea.Cmd {
|
||||
pos := m.CursorX()
|
||||
line := m.Line(m.CursorY())
|
||||
|
||||
// Save deleted text to register
|
||||
if pos < len(line) {
|
||||
m.UpdateDefaultRegister(CharwiseRegister, []string{line[pos:]})
|
||||
}
|
||||
|
||||
// Delete to end of line
|
||||
m.SetLine(m.CursorY(), line[:pos])
|
||||
|
||||
// Enter insert mode
|
||||
m.SetMode(InsertMode)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure ChangeToEndOfLine implements Repeatable
|
||||
var _ Repeatable = ChangeToEndOfLine{}
|
||||
|
||||
func (a ChangeToEndOfLine) WithCount(n int) Action {
|
||||
return ChangeToEndOfLine{Count: n}
|
||||
}
|
||||
|
||||
// SubstituteChar implements Action (s) - deletes character and enters insert mode
|
||||
type SubstituteChar struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
func (a SubstituteChar) Execute(m Model) tea.Cmd {
|
||||
pos := m.CursorX()
|
||||
line := m.Line(m.CursorY())
|
||||
|
||||
// Calculate how many chars to delete (limited by line length)
|
||||
count := min(a.Count, len(line)-pos)
|
||||
|
||||
if count > 0 {
|
||||
// Save deleted text to register
|
||||
m.UpdateDefaultRegister(CharwiseRegister, []string{line[pos : pos+count]})
|
||||
|
||||
// Delete the characters
|
||||
m.SetLine(m.CursorY(), line[:pos]+line[pos+count:])
|
||||
}
|
||||
|
||||
// Enter insert mode
|
||||
m.SetMode(InsertMode)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure SubstituteChar implements Repeatable
|
||||
var _ Repeatable = SubstituteChar{}
|
||||
|
||||
func (a SubstituteChar) WithCount(n int) Action {
|
||||
return SubstituteChar{Count: n}
|
||||
}
|
||||
|
||||
// SubstituteLine implements Action (S) - clears line and enters insert mode
|
||||
type SubstituteLine struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
func (a SubstituteLine) Execute(m Model) tea.Cmd {
|
||||
y := m.CursorY()
|
||||
|
||||
// Calculate how many lines to substitute
|
||||
count := min(a.Count, m.LineCount()-y)
|
||||
|
||||
var lines []string
|
||||
|
||||
// Collect and delete lines
|
||||
for range count {
|
||||
lines = append(lines, m.Line(y))
|
||||
m.DeleteLine(y)
|
||||
}
|
||||
|
||||
// Save deleted lines to register
|
||||
m.UpdateDefaultRegister(LinewiseRegister, lines)
|
||||
|
||||
// Insert empty line at original position
|
||||
insertY := min(y, m.LineCount())
|
||||
m.InsertLine(insertY, "")
|
||||
|
||||
// Position cursor
|
||||
m.SetCursorY(insertY)
|
||||
m.SetCursorX(0)
|
||||
|
||||
// Enter insert mode
|
||||
m.SetMode(InsertMode)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure SubstituteLine implements Repeatable
|
||||
var _ Repeatable = SubstituteLine{}
|
||||
|
||||
func (a SubstituteLine) WithCount(n int) Action {
|
||||
return SubstituteLine{Count: n}
|
||||
}
|
||||
1014
internal/editor/integration_operator_change_test.go
Normal file
1014
internal/editor/integration_operator_change_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -152,7 +152,10 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
|
||||
mtype = action.Linewise
|
||||
}
|
||||
cmd := op.Operate(m, start, end, mtype)
|
||||
m.SetMode(action.NormalMode)
|
||||
// Only reset to normal mode if operator didn't enter insert mode
|
||||
if m.Mode() != action.InsertMode {
|
||||
m.SetMode(action.NormalMode)
|
||||
}
|
||||
h.Reset()
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -55,6 +55,9 @@ func NewNormalKeymap() *Keymap {
|
||||
"V": action.EnterVisualLineMode{},
|
||||
"ctrl+v": action.EnterVisualBlockMode{},
|
||||
"D": action.DeleteToEndOfLine{Count: 1},
|
||||
"C": action.ChangeToEndOfLine{Count: 1},
|
||||
"s": action.SubstituteChar{Count: 1},
|
||||
"S": action.SubstituteLine{Count: 1},
|
||||
"p": action.Paste{Count: 1},
|
||||
"P": action.PasteBefore{Count: 1},
|
||||
},
|
||||
@ -85,11 +88,7 @@ func NewVisualKeymap() *Keymap {
|
||||
"d": operator.DeleteOperator{},
|
||||
"x": operator.DeleteOperator{},
|
||||
"y": operator.YankOperator{},
|
||||
// "c": ChangeOp{},
|
||||
// "y": YankOp{},
|
||||
// "p": PasteOp{},
|
||||
// "s": SubstitueOp{},
|
||||
// "~": SwapCaseOp{},
|
||||
"c": operator.ChangeOperator{},
|
||||
},
|
||||
actions: map[string]action.Action{
|
||||
"p": action.VisualPaste{Count: 1},
|
||||
|
||||
173
internal/operator/change.go
Normal file
173
internal/operator/change.go
Normal file
@ -0,0 +1,173 @@
|
||||
package operator
|
||||
|
||||
import (
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Implements Operator (c)
|
||||
type ChangeOperator struct{}
|
||||
|
||||
func (o ChangeOperator) Operate(m action.Model, start, end action.Position, mtype action.MotionType) tea.Cmd {
|
||||
switch m.Mode() {
|
||||
case action.VisualMode:
|
||||
changeCharSelection(m, start, end)
|
||||
case action.VisualLineMode:
|
||||
changeLineSelection(m, start, end)
|
||||
case action.VisualBlockMode:
|
||||
changeBlockSelection(m, start, end)
|
||||
case action.NormalMode:
|
||||
changeNormalMode(m, start, end, mtype)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func changeNormalMode(m action.Model, start, end action.Position, mtype action.MotionType) {
|
||||
// Normalize so start is always before or equal to end
|
||||
if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
// Linewise motions (j, k, G, gg) always operate on whole lines
|
||||
if mtype == action.Linewise {
|
||||
changeLineSelection(m, start, end)
|
||||
return
|
||||
}
|
||||
|
||||
// Charwise motions on same line
|
||||
if start.Line == end.Line {
|
||||
// No movement = nothing to change
|
||||
if start.Col == end.Col && mtype == action.CharwiseExclusive {
|
||||
m.SetMode(action.InsertMode)
|
||||
return
|
||||
}
|
||||
// Exclusive motion: end position not included, so back up one
|
||||
if mtype == action.CharwiseExclusive {
|
||||
end.Col--
|
||||
}
|
||||
if end.Col >= start.Col {
|
||||
changeCharSelection(m, start, end)
|
||||
} else {
|
||||
m.SetMode(action.InsertMode)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Charwise motion spanning multiple lines
|
||||
changeCharSelection(m, start, end)
|
||||
}
|
||||
|
||||
func changeCharSelection(m action.Model, start, end action.Position) {
|
||||
var deletedText string
|
||||
|
||||
if start.Line == end.Line {
|
||||
line := m.Line(start.Line)
|
||||
endCol := min(end.Col+1, len(line))
|
||||
deletedText = line[start.Col:endCol]
|
||||
m.SetLine(start.Line, line[:start.Col]+line[endCol:])
|
||||
} else {
|
||||
startLine := m.Line(start.Line)
|
||||
endLine := m.Line(end.Line)
|
||||
|
||||
// Extract deleted text
|
||||
deletedText = startLine[start.Col:] + "\n"
|
||||
for y := start.Line + 1; y < end.Line; y++ {
|
||||
deletedText += m.Line(y) + "\n"
|
||||
}
|
||||
endCol := min(end.Col+1, len(endLine))
|
||||
deletedText += endLine[:endCol]
|
||||
|
||||
prefix := startLine[:start.Col]
|
||||
suffix := ""
|
||||
if end.Col+1 < len(endLine) {
|
||||
suffix = endLine[end.Col+1:]
|
||||
}
|
||||
|
||||
// Delete from end back to start to preserve indices
|
||||
for i := end.Line; i >= start.Line; i-- {
|
||||
m.DeleteLine(i)
|
||||
}
|
||||
m.InsertLine(start.Line, prefix+suffix)
|
||||
}
|
||||
|
||||
m.SetCursorY(start.Line)
|
||||
m.SetCursorX(start.Col)
|
||||
m.ClampCursorX()
|
||||
m.SetMode(action.InsertMode)
|
||||
|
||||
// Update register with deleted text
|
||||
m.UpdateDefaultRegister(action.CharwiseRegister, []string{deletedText})
|
||||
}
|
||||
|
||||
func changeLineSelection(m action.Model, start, end action.Position) {
|
||||
var lines []string
|
||||
|
||||
for i := end.Line; i >= start.Line; i-- {
|
||||
lines = append([]string{m.Line(i)}, lines...)
|
||||
m.DeleteLine(i)
|
||||
}
|
||||
|
||||
// Insert an empty line for editing
|
||||
insertY := min(start.Line, m.LineCount())
|
||||
m.InsertLine(insertY, "")
|
||||
|
||||
m.SetCursorY(insertY)
|
||||
m.SetCursorX(0)
|
||||
m.SetMode(action.InsertMode)
|
||||
|
||||
// Update registers
|
||||
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
|
||||
}
|
||||
|
||||
func changeBlockSelection(m action.Model, start, end action.Position) {
|
||||
startCol := min(start.Col, end.Col)
|
||||
endCol := max(start.Col, end.Col)
|
||||
|
||||
for y := start.Line; y <= end.Line; y++ {
|
||||
line := m.Line(y)
|
||||
if startCol >= len(line) {
|
||||
continue
|
||||
}
|
||||
ec := min(endCol+1, len(line))
|
||||
m.SetLine(y, line[:startCol]+line[ec:])
|
||||
}
|
||||
|
||||
m.SetCursorY(start.Line)
|
||||
m.SetCursorX(startCol)
|
||||
m.ClampCursorX()
|
||||
m.SetMode(action.InsertMode)
|
||||
}
|
||||
|
||||
// Verify ChangeOperator implements DoublePresser
|
||||
var _ action.DoublePresser = ChangeOperator{}
|
||||
|
||||
// Double press handles cc - change the entire line
|
||||
func (o ChangeOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
||||
startY := m.CursorY()
|
||||
|
||||
// If we have a higher value than lines remaining, we can only run so many times
|
||||
opCount := min(count, m.LineCount()-startY)
|
||||
|
||||
var lines []string
|
||||
|
||||
// Collect lines to delete (always delete at startY since lines shift up)
|
||||
for range opCount {
|
||||
lines = append(lines, m.Line(startY))
|
||||
m.DeleteLine(startY)
|
||||
}
|
||||
|
||||
// Put deleted lines in register
|
||||
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
|
||||
|
||||
// Insert empty line at the original position for editing
|
||||
// If we deleted everything, startY might be past end, so clamp it
|
||||
insertY := min(startY, m.LineCount())
|
||||
m.InsertLine(insertY, "")
|
||||
|
||||
// Position cursor on the new empty line
|
||||
m.SetCursorY(insertY)
|
||||
m.SetCursorX(0)
|
||||
m.SetMode(action.InsertMode)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -52,7 +52,6 @@ func (o DeleteOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
||||
|
||||
// Put her in the register!
|
||||
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
|
||||
// m.SetRegister('"', action.LinewiseRegister, lines)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user