feature/find #1

Merged
azpect merged 2 commits from feature/find into master 2026-03-13 10:56:47 -07:00
5 changed files with 1971 additions and 11 deletions
Showing only changes of commit 050935941c - Show all commits

83
internal/action/find.go Normal file
View File

@ -0,0 +1,83 @@
package action
import (
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
type FindChar struct {
Char string
Forward bool
Inclusive bool
Count int
}
func (m FindChar) WithChar(char string) Motion {
m.Char = char
return m
}
func (m FindChar) Type() core.MotionType {
if m.Inclusive {
return core.CharwiseInclusive
}
return core.CharwiseExclusive
}
// WithCount sets the count (required by Repeatable interface)
func (f FindChar) WithCount(n int) Action {
f.Count = n
return f
}
func (a FindChar) Execute(m Model) tea.Cmd {
// Get the line
// Get the current position, moved based on inputs
win := m.ActiveWindow()
buf := win.Buffer
line := buf.Line(win.Cursor.Line)
col := win.Cursor.Col
if len(line) <= 0 {
return nil
}
if a.Forward {
for x := col; x < len(line); x++ {
if string(line[x]) == a.Char {
if a.Count == 1 {
if a.Inclusive {
win.SetCursorCol(x)
} else {
win.SetCursorCol(x - 1)
}
break
} else {
a.Count--
}
}
}
}
if !a.Forward {
for x := col; x >= 0; x-- {
if string(line[x]) == a.Char {
if a.Count == 1 {
if a.Inclusive {
win.SetCursorCol(x)
} else {
win.SetCursorCol(x + 1)
}
break
} else {
a.Count--
}
}
}
}
return nil
}

1754
internal/action/find_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -83,3 +83,10 @@ type DoublePresser interface {
type Repeatable interface {
WithCount(n int) Action
}
// CharMotion is a motion that requires a character argument (f/t/F/T)
// The state machine will call WithChar to set the character before executing
type CharMotion interface {
Motion
WithChar(char string) Motion
}

View File

@ -14,18 +14,20 @@ const (
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)
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
@ -70,6 +72,11 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
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
@ -120,6 +127,13 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
// 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)
@ -220,6 +234,83 @@ func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any,
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 {
@ -277,6 +368,7 @@ func (h *Handler) Reset() {
h.operatorKey = ""
h.buffer = ""
h.pending = ""
h.charMotionType = ""
}
// Handler.Pending: Returns the accumulated input buffer for display.

View File

@ -9,9 +9,10 @@ import (
// Keymap: Maps key sequences to motions, operators, and actions.
type Keymap struct {
motions map[string]action.Motion
operators map[string]action.Operator
actions map[string]action.Action // standalone actions: i.e., 'i', 'a'
motions map[string]action.Motion
operators map[string]action.Operator
actions map[string]action.Action // standalone actions: i.e., 'i', 'a'
charMotions map[string]action.Motion // motions that need character argument: f/t/F/T
}
// NewNormalKeymap: Creates a keymap for normal mode with all standard vim bindings.
@ -63,6 +64,12 @@ func NewNormalKeymap() *Keymap {
"p": action.Paste{Count: 1},
"P": action.PasteBefore{Count: 1},
},
charMotions: map[string]action.Motion{
"f": action.FindChar{Forward: true, Inclusive: true},
"F": action.FindChar{Forward: false, Inclusive: true},
"t": action.FindChar{Forward: true, Inclusive: false},
"T": action.FindChar{Forward: false, Inclusive: false},
},
}
}
@ -140,7 +147,7 @@ func NewCommandKeymap() *Keymap {
}
// Keymap.Lookup: Returns the type and value of a key binding (motion, operator, or action).
// Keymap.Lookup: Returns the type and value of a key binding (motion, operator, action, or char_motion).
func (km *Keymap) Lookup(key string) (kind string, value any) {
if m, ok := km.motions[key]; ok {
return "motion", m
@ -151,6 +158,9 @@ func (km *Keymap) Lookup(key string) (kind string, value any) {
if a, ok := km.actions[key]; ok {
return "action", a
}
if cm, ok := km.charMotions[key]; ok {
return "char_motion", cm
}
return "", nil
}
@ -171,5 +181,19 @@ func (km *Keymap) HasPrefix(prefix string) bool {
return true
}
}
for key := range km.charMotions {
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
return true
}
}
return false
}
// Keymap.LookupCharMotion: Returns the motion template for character motions (f/t/F/T).
// The returned motion should implement the CharMotion interface.
func (km *Keymap) LookupCharMotion(key string) action.Motion {
if cm, ok := km.charMotions[key]; ok {
return cm
}
return nil
}