package input import ( "git.gophernest.net/azpect/TextEditor/internal/action" tea "github.com/charmbracelet/bubbletea" ) type InputState int const ( StateReady InputState = iota StateCount StateOperatorPending StateMotionCount ) // PositionGetter is used to get cursor position for operator ranges type PositionGetter interface { GetCursorPosition() *action.Position } 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) // Keymaps normalKeymap *Keymap visualKeymap *Keymap insertKeymap *Keymap commandKeymap *Keymap currentKeymap *Keymap } func NewHandler() *Handler { return &Handler{ // keymap: NewNormalKeymap(), normalKeymap: NewNormalKeymap(), visualKeymap: NewVisualKeymap(), insertKeymap: NewInsertKeymap(), commandKeymap: NewCommandKeymap(), currentKeymap: nil, } } func (h *Handler) Handle(m action.Model, key string) tea.Cmd { // ESC always resets everything if key == "esc" { h.Reset() if m.Mode() == action.InsertMode { m.ExitInsertMode() } else { m.SetMode(action.NormalMode) } return nil } // Insert/command mode bypasses the normal state machine entirely switch m.Mode() { case action.InsertMode: return h.handleInsertKey(m, key) case action.CommandMode: return h.handleCommandKey(m, key) } // 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 action.NormalMode: h.currentKeymap = h.normalKeymap case action.VisualMode, action.VisualLineMode, action.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 } // dispatch routes to the right handler based on current state func (h *Handler) dispatch(m action.Model, kind string, binding any, key string) tea.Cmd { switch h.state { case StateReady, StateCount: return h.handleInitial(m, kind, binding, key) case StateOperatorPending, StateMotionCount: return h.handleAfterOperator(m, kind, binding, key) } h.Reset() return nil } 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) } cmd := mot.Execute(m) 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 := action.CharwiseInclusive if m.Mode() == action.VisualLineMode { mtype = action.Linewise } cmd := op.Operate(m, start, end, mtype) // 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 } // 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 := act.Execute(m) h.Reset() return cmd } h.Reset() return nil } func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any, key string) tea.Cmd { count := h.effectiveCount() // 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 := dp.DoublePress(m, count) h.Reset() return cmd } h.Reset() 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) } // Get range and motion type pg := m.(PositionGetter) start := pg.GetCursorPosition() mot.Execute(m) end := pg.GetCursorPosition() cmd := h.operator.Operate(m, *start, *end, mot.Type()) h.Reset() return cmd } h.Reset() return nil } 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 } func (h *Handler) currentCount() int { if h.state == StateOperatorPending || h.state == StateMotionCount { return h.count2 } return h.count1 } func (h *Handler) effectiveCount() int { c1, c2 := h.count1, h.count2 if c1 == 0 { c1 = 1 } if c2 == 0 { c2 = 1 } return c1 * c2 } func (h *Handler) Reset() { h.state = StateReady h.count1 = 0 h.count2 = 0 h.operator = nil h.operatorKey = "" h.buffer = "" h.pending = "" } func (h *Handler) Pending() string { return h.buffer } func (h *Handler) handleInsertKey(m action.Model, key string) tea.Cmd { // Record the key for count replay (e.g. 5i...) m.SetInsertKeys(append(m.InsertKeys(), 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) 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) } func normalizeVisualSelection(m action.Model) (action.Position, action.Position) { a := action.Position{Line: m.AnchorY(), Col: m.AnchorX()} c := action.Position{Line: m.CursorY(), Col: m.CursorX()} if a.Line < c.Line || (a.Line == c.Line && a.Col <= c.Col) { return a, c } return c, a }