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 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(), 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" { // 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) } // 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.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) } // 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 }