Gim/internal/input/handler.go
Hayden Hargreaves 0de38ec837
All checks were successful
Run Test Suite / test (push) Successful in 13s
Run Test Suite / test (pull_request) Successful in 13s
feat: implementing the repeat commands! Tested
2026-03-13 23:00:26 -07:00

434 lines
12 KiB
Go

package input
import (
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
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)
)
// 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"
// Keymaps
normalKeymap *Keymap
visualKeymap *Keymap
insertKeymap *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(),
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 {
// ESC always resets everything
if key == "esc" {
h.Reset()
if m.Mode() == core.InsertMode {
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.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)
}
// 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" {
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)
}
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 := 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 := core.CharwiseInclusive
if m.Mode() == core.VisualLineMode {
mtype = core.Linewise
}
cmd := op.Operate(m, 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.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
}
// 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 := 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)
}
// 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
mot.Execute(m)
end := win.Cursor
cmd := h.operator.Operate(m, start, end, mot.Type())
h.Reset()
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 {
mot = r.WithCount(count).(action.Motion)
}
// If operator pending (e.g., "df{char}"), get range and operate
if h.operator != nil {
win := m.ActiveWindow()
start := win.Cursor
mot.Execute(m)
end := win.Cursor
cmd := h.operator.Operate(m, start, end, mot.Type())
h.Reset()
return cmd
}
// Otherwise just execute the motion
cmd := mot.Execute(m)
h.Reset()
return cmd
}
// 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.
func (h *Handler) Reset() {
h.state = StateReady
h.count1 = 0
h.count2 = 0
h.operator = nil
h.operatorKey = ""
h.buffer = ""
h.pending = ""
h.charMotionType = ""
}
// 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.
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)
}
// Handler.handleCommandKey: Processes a keypress in command mode, executing
// it as an action or inserting it into the command line.
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
}