Gim/internal/input/handler.go
Hayden Hargreaves 77374ba447 feat: implemented 'dd' and other 'd' for visual mode.
Tested 'dd' but not visual mode.
2026-02-11 17:56:06 -07:00

255 lines
5.5 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
currentKeymap *Keymap
}
func NewHandler() *Handler {
return &Handler{
// keymap: NewNormalKeymap(),
normalKeymap: NewNormalKeymap(),
visualKeymap: NewVisualKeymap(),
currentKeymap: nil,
}
}
func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
// ESC always resets everything
// TODO: This should prob be relocated
if key == "esc" {
h.Reset()
m.SetMode(action.NormalMode)
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
// TODO: Do we need to reset anywhere?
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)
cmd := op.Operate(m, start, end)
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 here
pg := m.(PositionGetter)
start := pg.GetCursorPosition()
mot.Execute(m)
end := pg.GetCursorPosition()
cmd := h.operator.Operate(m, start, end)
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 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
}