730 lines
20 KiB
Go
730 lines
20 KiB
Go
package input
|
|
|
|
import (
|
|
"slices"
|
|
|
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
|
"git.gophernest.net/azpect/TextEditor/internal/operator"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
// InputState: Represents the current state of the input handler state machine.
|
|
type InputState int
|
|
|
|
const (
|
|
StateReady InputState = iota
|
|
StateCount
|
|
StateOperatorPending
|
|
StateMotionCount
|
|
StateWaitingForChar // Waiting for character argument (f/t/F/T)
|
|
StateWaitingForTextObject // Waiting for text object (after i/a)
|
|
)
|
|
|
|
// Handler: Manages input processing with a state machine for vim-style commands.
|
|
// Handles counts, operators, motions, and multi-key sequences.
|
|
type Handler struct {
|
|
state InputState
|
|
count1 int
|
|
count2 int
|
|
operator action.Operator
|
|
operatorKey string // track which key started operator (for dd, yy, cc)
|
|
buffer string // for display (what user has typed)
|
|
pending string // partial key sequence (e.g., "g" waiting for second key)
|
|
charMotionType string // which char motion is waiting: "f", "t", "F", or "T"
|
|
modifier string // which modifier used for text object: "i" or "a"
|
|
|
|
// Dot operator - accumulate keys for current operation
|
|
recordingKeys []string
|
|
|
|
// Keymaps
|
|
normalKeymap *Keymap
|
|
visualKeymap *Keymap
|
|
insertKeymap *Keymap
|
|
replaceKeymap *Keymap
|
|
commandKeymap *Keymap
|
|
searchKeymap *Keymap
|
|
|
|
currentKeymap *Keymap
|
|
}
|
|
|
|
// NewHandler: Creates a new input handler with initialized keymaps for all modes.
|
|
func NewHandler() *Handler {
|
|
return &Handler{
|
|
// keymap: NewNormalKeymap(),
|
|
normalKeymap: NewNormalKeymap(),
|
|
visualKeymap: NewVisualKeymap(),
|
|
insertKeymap: NewInsertKeymap(),
|
|
replaceKeymap: NewReplaceKeymap(),
|
|
commandKeymap: NewCommandKeymap(),
|
|
searchKeymap: NewSearchKeymap(),
|
|
currentKeymap: nil,
|
|
}
|
|
}
|
|
|
|
// Handler.Handle: Main entry point for processing a keypress. Routes to appropriate
|
|
// handler based on current mode and state.
|
|
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
|
|
if key == "esc" && m.Mode() != core.SearchMode {
|
|
// 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()
|
|
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()
|
|
if buf.UndoStack != nil {
|
|
buf.UndoStack.EndBlock(win.Cursor)
|
|
}
|
|
m.ExitInsertMode()
|
|
} else {
|
|
m.SetMode(core.NormalMode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Insert/command mode bypasses the normal state machine entirely
|
|
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)
|
|
case core.SearchMode:
|
|
return h.handleSearchKey(m, key)
|
|
}
|
|
|
|
// If waiting for character argument (f/t/F/T), capture it
|
|
if h.state == StateWaitingForChar {
|
|
return h.handleCharMotion(m, key)
|
|
}
|
|
|
|
if h.state == StateWaitingForTextObject {
|
|
return h.handleTextObjectKey(m, key)
|
|
}
|
|
|
|
// i/a after operator or in visual mode = text object modifier
|
|
if (key == "i" || key == "a") && h.pending == "" {
|
|
if h.state == StateOperatorPending ||
|
|
h.state == StateMotionCount ||
|
|
m.Mode().IsVisualMode() {
|
|
h.modifier = key
|
|
h.state = StateWaitingForTextObject
|
|
h.buffer += key
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Try to accumulate count (only if no pending sequence)
|
|
if h.pending == "" && h.tryAccumulateCount(key) {
|
|
return nil
|
|
}
|
|
|
|
// Build the sequence (pending + new key)
|
|
sequence := h.pending + key
|
|
|
|
// Set working keymap
|
|
switch m.Mode() {
|
|
case core.NormalMode:
|
|
h.currentKeymap = h.normalKeymap
|
|
case core.VisualMode,
|
|
core.VisualLineMode,
|
|
core.VisualBlockMode:
|
|
h.currentKeymap = h.visualKeymap
|
|
}
|
|
|
|
// Check for exact match with full sequence
|
|
kind, binding := h.currentKeymap.Lookup(sequence)
|
|
if kind != "" {
|
|
h.pending = ""
|
|
h.buffer += key
|
|
return h.dispatch(m, kind, binding, sequence)
|
|
}
|
|
|
|
// No exact match - could this be a prefix of something?
|
|
if h.currentKeymap.HasPrefix(sequence) {
|
|
h.pending = sequence
|
|
h.buffer += key
|
|
return nil // wait for more keys
|
|
}
|
|
|
|
// Not a prefix either - if we had pending, try just the new key
|
|
if h.pending != "" {
|
|
h.pending = ""
|
|
kind, binding = h.currentKeymap.Lookup(key)
|
|
if kind != "" {
|
|
h.buffer = key
|
|
return h.dispatch(m, kind, binding, key)
|
|
}
|
|
}
|
|
|
|
// Nothing matched
|
|
h.Reset()
|
|
return nil
|
|
}
|
|
|
|
// Handler.dispatch: Routes to the appropriate handler based on current state.
|
|
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" && !m.Mode().IsVisualMode() {
|
|
m.SetMode(core.WaitingMode)
|
|
}
|
|
h.charMotionType = key
|
|
h.state = StateWaitingForChar
|
|
return nil
|
|
}
|
|
|
|
switch h.state {
|
|
case StateReady, StateCount:
|
|
return h.handleInitial(m, kind, binding, key)
|
|
case StateOperatorPending, StateMotionCount:
|
|
return h.handleAfterOperator(m, kind, binding, key)
|
|
case StateWaitingForTextObject:
|
|
return h.handleTextObject(m, kind, binding, key)
|
|
}
|
|
h.Reset()
|
|
return nil
|
|
}
|
|
|
|
// Handler.handleInitial: Handles input when no operator is pending. Executes
|
|
// motions, actions, or stores operators waiting for a motion.
|
|
func (h *Handler) handleInitial(m action.Model, kind string, binding any, key string) tea.Cmd {
|
|
count := h.effectiveCount()
|
|
|
|
switch kind {
|
|
case "motion":
|
|
mot := binding.(action.Motion)
|
|
if r, ok := mot.(action.Repeatable); ok {
|
|
mot = r.WithCount(count).(action.Motion)
|
|
}
|
|
// Resolve late-binding motions (e.g. RepeatFind) before executing so
|
|
// that Type() on the resolved motion is accurate.
|
|
if res, ok := mot.(action.Resolvable); ok {
|
|
mot = res.Resolve(m)
|
|
}
|
|
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()
|
|
return cmd
|
|
|
|
case "operator":
|
|
op := binding.(action.Operator)
|
|
// 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 inclusive
|
|
mtype := core.CharwiseInclusive
|
|
if m.Mode() == core.VisualLineMode {
|
|
mtype = core.Linewise
|
|
}
|
|
cmd := h.executeOperator(m, op, start, end, mtype)
|
|
// Only reset to normal mode if operator didn't enter insert mode
|
|
if m.Mode() != core.InsertMode {
|
|
m.SetMode(core.NormalMode)
|
|
}
|
|
h.RecordAndReset(m)
|
|
return cmd
|
|
}
|
|
// In normal mode, wait for a motion to define the range
|
|
h.operator = op
|
|
h.operatorKey = key
|
|
h.state = StateOperatorPending
|
|
return nil
|
|
|
|
case "action":
|
|
act := binding.(action.Action)
|
|
if r, ok := act.(action.Repeatable); ok {
|
|
act = r.WithCount(count)
|
|
}
|
|
cmd := h.executeAction(m, act)
|
|
// Only record if we're not entering visual mode (visual ops record when they complete)
|
|
if m.Mode().IsVisualMode() {
|
|
h.Reset() // In visual mode now, don't save yet
|
|
} else {
|
|
h.RecordAndReset(m)
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
h.Reset()
|
|
return nil
|
|
}
|
|
|
|
// Handler.handleAfterOperator: Handles input when an operator is pending.
|
|
// Processes double-press operators (dd, yy) or applies operator to motion range.
|
|
func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any, key string) tea.Cmd {
|
|
count := h.effectiveCount()
|
|
win := m.ActiveWindow()
|
|
|
|
// dd, yy, cc - same operator key pressed twice
|
|
if kind == "operator" && key == h.operatorKey {
|
|
// Only call DoublePress if the operator supports it
|
|
if dp, ok := h.operator.(action.DoublePresser); ok {
|
|
cmd := h.executeDoublePress(m, dp, count)
|
|
h.RecordAndReset(m)
|
|
return cmd
|
|
}
|
|
h.Reset()
|
|
return nil
|
|
}
|
|
|
|
// Do not quit when we see a/i (allow for text objects)
|
|
if kind == "modifier" {
|
|
return nil
|
|
}
|
|
|
|
// Motion after operator
|
|
if kind == "motion" {
|
|
mot := binding.(action.Motion)
|
|
if r, ok := mot.(action.Repeatable); ok {
|
|
mot = r.WithCount(count).(action.Motion)
|
|
}
|
|
// Resolve late-binding motions (e.g. RepeatFind) before executing so
|
|
// that Type() on the resolved motion is accurate.
|
|
if res, ok := mot.(action.Resolvable); ok {
|
|
mot = res.Resolve(m)
|
|
}
|
|
// Get range and motion type
|
|
start := win.Cursor
|
|
h.executeMotion(m, mot)
|
|
end := win.Cursor
|
|
cmd := h.executeOperator(m, h.operator, start, end, mot.Type())
|
|
h.RecordAndReset(m)
|
|
return cmd
|
|
}
|
|
|
|
h.Reset()
|
|
return nil
|
|
}
|
|
|
|
// Handler.handleCharMotion: Handles input when waiting for a character argument
|
|
// for f/t/F/T motions. Captures the character and creates the appropriate motion.
|
|
//
|
|
// USAGE FOR IMPLEMENTING f/t/F/T MOTIONS:
|
|
//
|
|
// You need to create a CharMotion interface in internal/action/interface.go:
|
|
//
|
|
// type CharMotion interface {
|
|
// Motion
|
|
// WithChar(char string) Motion
|
|
// }
|
|
//
|
|
// Then implement it for your FindChar motion (example):
|
|
//
|
|
// type FindChar struct {
|
|
// Char string
|
|
// Forward bool // true = f/t, false = F/T
|
|
// To bool // true = f/F (to char), false = t/T (till before/after char)
|
|
// Count int
|
|
// }
|
|
//
|
|
// func (f FindChar) WithChar(char string) action.Motion {
|
|
// f.Char = char
|
|
// return f
|
|
// }
|
|
//
|
|
// The state machine will:
|
|
// 1. Call WithChar(key) to set the character
|
|
// 2. Apply count if the motion is Repeatable
|
|
// 3. If operator pending (df{char}), execute motion and operate on range
|
|
// 4. Otherwise just execute the motion
|
|
func (h *Handler) handleCharMotion(m action.Model, key string) tea.Cmd {
|
|
count := h.effectiveCount()
|
|
|
|
// Get the char motion from the keymap
|
|
// The keymap should have registered f/t/F/T as "char_motion" type
|
|
// and stored the motion template (without character set yet)
|
|
motion := h.currentKeymap.LookupCharMotion(h.charMotionType)
|
|
if motion == nil {
|
|
// Motion not found - shouldn't happen if keymap configured correctly
|
|
h.Reset()
|
|
return nil
|
|
}
|
|
|
|
// Type assert to CharMotion interface and set the character
|
|
charMot, ok := motion.(action.CharMotion)
|
|
if !ok {
|
|
// Motion doesn't implement CharMotion interface
|
|
h.Reset()
|
|
return nil
|
|
}
|
|
|
|
// Set the character that was pressed
|
|
mot := charMot.WithChar(key)
|
|
|
|
// Apply count if supported
|
|
if r, ok := mot.(action.Repeatable); ok {
|
|
result := r.WithCount(count)
|
|
// WithCount returns Action, but char motions still implement Motion
|
|
if m, ok := result.(action.Motion); ok {
|
|
mot = m
|
|
}
|
|
}
|
|
|
|
// If operator pending (e.g., "df{char}"), get range and operate
|
|
if h.operator != nil {
|
|
win := m.ActiveWindow()
|
|
start := win.Cursor
|
|
h.executeMotion(m, mot)
|
|
end := win.Cursor
|
|
cmd := h.executeOperator(m, h.operator, start, end, mot.Type())
|
|
h.RecordAndReset(m)
|
|
return cmd
|
|
}
|
|
|
|
// Otherwise just execute the motion
|
|
cmd := h.executeMotion(m, mot)
|
|
|
|
// ReplaceChar modifies the buffer, so it should be repeatable with '.'
|
|
// (unlike f/t/F/T which are pure motions)
|
|
if _, isReplace := mot.(action.ReplaceChar); isReplace {
|
|
h.RecordAndReset(m)
|
|
} else {
|
|
h.Reset()
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
// Handler.handleTextObject: Handles input when waiting for text object after i/a.
|
|
// Processes text objects like 'w', ')', '"', etc. and applies pending operator if any.
|
|
func (h *Handler) handleTextObject(m action.Model, kind string, binding any, key string) tea.Cmd {
|
|
// Not sure what count is fore
|
|
// count := h.effectiveCount()
|
|
|
|
if kind != "text_object" {
|
|
// Invalid - expected a text object
|
|
h.Reset()
|
|
return nil
|
|
}
|
|
|
|
textObj := binding.(action.TextObject)
|
|
win := m.ActiveWindow()
|
|
|
|
// Calculate the region
|
|
start, end, mtype := textObj.GetRange(m, win.Cursor, h.modifier)
|
|
|
|
// If we have an operator pending (e.g., "diw")
|
|
if h.operator != nil {
|
|
cmd := h.executeOperator(m, h.operator, start, end, mtype)
|
|
h.RecordAndReset(m)
|
|
return cmd
|
|
}
|
|
|
|
// In visual mode (e.g., "viw")
|
|
if m.Mode().IsVisualMode() {
|
|
// Set anchor and cursor to define the selection
|
|
win.Anchor = start
|
|
win.Cursor = end
|
|
h.Reset()
|
|
return nil
|
|
}
|
|
|
|
// Shouldn't reach here - text object without operator or visual mode
|
|
h.Reset()
|
|
return nil
|
|
}
|
|
|
|
// Handler.handleTextObjectKey: Handles the key press when waiting for a text object.
|
|
// Looks up the text object directly (bypassing normal motion lookup).
|
|
func (h *Handler) handleTextObjectKey(m action.Model, key string) tea.Cmd {
|
|
// Look up text object directly
|
|
textObj, ok := h.currentKeymap.textObjects[key]
|
|
if !ok {
|
|
// Not a valid text object
|
|
h.Reset()
|
|
return nil
|
|
}
|
|
|
|
// Call the existing handleTextObject with the found text object
|
|
return h.handleTextObject(m, "text_object", textObj, key)
|
|
}
|
|
|
|
// Handler.tryAccumulateCount: Attempts to add a digit to the count. Returns
|
|
// true if successful, false if the key is not a digit or is an invalid count.
|
|
func (h *Handler) tryAccumulateCount(key string) bool {
|
|
if len(key) != 1 || key[0] < '0' || key[0] > '9' {
|
|
return false
|
|
}
|
|
|
|
digit := int(key[0] - '0')
|
|
|
|
// 0 at start is a motion, not a count
|
|
if digit == 0 && h.currentCount() == 0 {
|
|
return false
|
|
}
|
|
|
|
switch h.state {
|
|
case StateReady, StateCount:
|
|
h.count1 = h.count1*10 + digit
|
|
h.state = StateCount
|
|
case StateOperatorPending, StateMotionCount:
|
|
h.count2 = h.count2*10 + digit
|
|
h.state = StateMotionCount
|
|
}
|
|
|
|
h.buffer += key
|
|
return true
|
|
}
|
|
|
|
// Handler.currentCount: Returns the count currently being accumulated based on state.
|
|
func (h *Handler) currentCount() int {
|
|
if h.state == StateOperatorPending || h.state == StateMotionCount {
|
|
return h.count2
|
|
}
|
|
return h.count1
|
|
}
|
|
|
|
// Handler.effectiveCount: Calculates the final count by multiplying count1 and
|
|
// count2, treating 0 as 1 for both.
|
|
func (h *Handler) effectiveCount() int {
|
|
c1, c2 := h.count1, h.count2
|
|
if c1 == 0 {
|
|
c1 = 1
|
|
}
|
|
if c2 == 0 {
|
|
c2 = 1
|
|
}
|
|
return c1 * c2
|
|
}
|
|
|
|
// Handler.Reset: Clears all handler state including counts, operators, and buffers.
|
|
// Does NOT clear recordingKeys - those accumulate across an operation.
|
|
func (h *Handler) Reset() {
|
|
h.state = StateReady
|
|
h.count1 = 0
|
|
h.count2 = 0
|
|
h.operator = nil
|
|
h.operatorKey = ""
|
|
h.buffer = ""
|
|
h.pending = ""
|
|
h.charMotionType = ""
|
|
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.
|
|
func (h *Handler) Pending() string {
|
|
return h.buffer
|
|
}
|
|
|
|
// Handler.handleInsertKey: Processes a keypress in insert mode, recording it
|
|
// for count replay and executing it as an action or character insertion.
|
|
//
|
|
// This function does not make use of the execute abstractions, to prevent each
|
|
// key inserted from creating a new block in the undo stack.
|
|
func (h *Handler) handleInsertKey(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)
|
|
}
|
|
|
|
// 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.insertKeymap.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.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)
|
|
}
|
|
|
|
// 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.
|
|
func (h *Handler) handleCommandKey(m action.Model, key string) tea.Cmd {
|
|
kind, binding := h.commandKeymap.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.InsertCommandChar{Char: key}.Execute(m)
|
|
}
|
|
|
|
// Handler.handleSearchKey: Processes a keypress in search mode, executing
|
|
// it as an action or inserting it into the search line. This does not record
|
|
// anything into the undo stack.
|
|
func (h *Handler) handleSearchKey(m action.Model, key string) tea.Cmd {
|
|
kind, binding := h.searchKeymap.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.InsertSearchChar{Char: key}.Execute(m)
|
|
}
|
|
|
|
// normalizeVisualSelection: Returns the visual selection with start before end,
|
|
// regardless of which direction the selection was made.
|
|
func normalizeVisualSelection(m action.Model) (core.Position, core.Position) {
|
|
win := m.ActiveWindow()
|
|
a := core.Position{Line: win.Anchor.Line, Col: win.Anchor.Col}
|
|
c := core.Position{Line: win.Cursor.Line, Col: win.Cursor.Col}
|
|
if a.Line < c.Line || (a.Line == c.Line && a.Col <= c.Col) {
|
|
return a, c
|
|
}
|
|
return c, a
|
|
}
|
|
|
|
func (h *Handler) executeAction(m action.Model, act action.Action) tea.Cmd {
|
|
win := m.ActiveWindow()
|
|
buf := m.ActiveBuffer()
|
|
|
|
if buf.UndoStack != nil {
|
|
buf.UndoStack.BeginBlock(win.Cursor)
|
|
}
|
|
|
|
cmd := act.Execute(m)
|
|
|
|
// If the action one that includes insert mode, we should not end the block, we want to
|
|
// include the text from the insert mode in the block.
|
|
_, O := act.(action.OpenLineAbove)
|
|
_, o := act.(action.OpenLineBelow)
|
|
_, s := act.(action.SubstituteChar)
|
|
_, S := act.(action.SubstituteLine)
|
|
_, C := act.(action.ChangeToEndOfLine)
|
|
if o || O || s || S || C {
|
|
return nil
|
|
}
|
|
|
|
if buf.UndoStack != nil {
|
|
buf.UndoStack.EndBlock(win.Cursor)
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (h *Handler) executeMotion(m action.Model, mot action.Motion) tea.Cmd {
|
|
// These do not change the buffer, so no need to record anything
|
|
return mot.Execute(m)
|
|
}
|
|
|
|
func (h *Handler) executeOperator(m action.Model, op action.Operator, start, end core.Position, mtype core.MotionType) tea.Cmd {
|
|
buf := m.ActiveBuffer()
|
|
win := m.ActiveWindow()
|
|
|
|
if buf.UndoStack != nil {
|
|
buf.UndoStack.BeginBlock(win.Cursor)
|
|
}
|
|
|
|
cmd := op.Operate(m, start, end, mtype)
|
|
|
|
// If operator is one that enters insert mode, we do not want to end the block.
|
|
_, c := op.(operator.ChangeOperator)
|
|
if c {
|
|
return cmd
|
|
}
|
|
|
|
if buf.UndoStack != nil {
|
|
buf.UndoStack.EndBlock(win.Cursor)
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (h *Handler) executeDoublePress(m action.Model, dp action.DoublePresser, count int) tea.Cmd {
|
|
buf := m.ActiveBuffer()
|
|
win := m.ActiveWindow()
|
|
|
|
if buf.UndoStack != nil {
|
|
buf.UndoStack.BeginBlock(win.Cursor)
|
|
}
|
|
|
|
cmd := dp.DoublePress(m, count)
|
|
|
|
// If operator being double pressed is one that enters insert mode, we do not
|
|
// want to end the block.
|
|
_, c := dp.(operator.ChangeOperator)
|
|
if c {
|
|
return cmd
|
|
}
|
|
|
|
if buf.UndoStack != nil {
|
|
buf.UndoStack.EndBlock(win.Cursor)
|
|
}
|
|
|
|
return cmd
|
|
}
|