Not totally complete WRT tests, but lots of progress. These interfaces make everything easy.
286 lines
6.4 KiB
Go
286 lines
6.4 KiB
Go
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
|
|
|
|
currentKeymap *Keymap
|
|
}
|
|
|
|
func NewHandler() *Handler {
|
|
return &Handler{
|
|
// keymap: NewNormalKeymap(),
|
|
normalKeymap: NewNormalKeymap(),
|
|
visualKeymap: NewVisualKeymap(),
|
|
insertKeymap: NewInsertKeymap(),
|
|
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 mode bypasses the normal state machine entirely
|
|
if m.Mode() == action.InsertMode {
|
|
return h.handleInsertKey(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.IsVisualMode() {
|
|
start, end := normalizeVisualSelection(m)
|
|
// Visual line mode is linewise, others are charwise
|
|
mtype := action.Charwise
|
|
if m.Mode() == action.VisualLineMode {
|
|
mtype = action.Linewise
|
|
}
|
|
cmd := op.Operate(m, start, end, mtype)
|
|
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 {
|
|
cmd := h.operator.DoublePress(m, count)
|
|
h.Reset()
|
|
return cmd
|
|
}
|
|
|
|
// 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 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
|
|
}
|