feat: implemented find char motions. tested.
This commit is contained in:
parent
f12ce37beb
commit
050935941c
83
internal/action/find.go
Normal file
83
internal/action/find.go
Normal 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
1754
internal/action/find_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -83,3 +83,10 @@ type DoublePresser interface {
|
|||||||
type Repeatable interface {
|
type Repeatable interface {
|
||||||
WithCount(n int) Action
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -14,18 +14,20 @@ const (
|
|||||||
StateCount
|
StateCount
|
||||||
StateOperatorPending
|
StateOperatorPending
|
||||||
StateMotionCount
|
StateMotionCount
|
||||||
|
StateWaitingForChar // Waiting for character argument (f/t/F/T)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler: Manages input processing with a state machine for vim-style commands.
|
// Handler: Manages input processing with a state machine for vim-style commands.
|
||||||
// Handles counts, operators, motions, and multi-key sequences.
|
// Handles counts, operators, motions, and multi-key sequences.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
state InputState
|
state InputState
|
||||||
count1 int
|
count1 int
|
||||||
count2 int
|
count2 int
|
||||||
operator action.Operator
|
operator action.Operator
|
||||||
operatorKey string // track which key started operator (for dd, yy, cc)
|
operatorKey string // track which key started operator (for dd, yy, cc)
|
||||||
buffer string // for display (what user has typed)
|
buffer string // for display (what user has typed)
|
||||||
pending string // partial key sequence (e.g., "g" waiting for second key)
|
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
|
// Keymaps
|
||||||
normalKeymap *Keymap
|
normalKeymap *Keymap
|
||||||
@ -70,6 +72,11 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
|||||||
return h.handleCommandKey(m, key)
|
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)
|
// Try to accumulate count (only if no pending sequence)
|
||||||
if h.pending == "" && h.tryAccumulateCount(key) {
|
if h.pending == "" && h.tryAccumulateCount(key) {
|
||||||
return nil
|
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.
|
// 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 {
|
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 {
|
switch h.state {
|
||||||
case StateReady, StateCount:
|
case StateReady, StateCount:
|
||||||
return h.handleInitial(m, kind, binding, key)
|
return h.handleInitial(m, kind, binding, key)
|
||||||
@ -220,6 +234,83 @@ func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any,
|
|||||||
return nil
|
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
|
// 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.
|
// true if successful, false if the key is not a digit or is an invalid count.
|
||||||
func (h *Handler) tryAccumulateCount(key string) bool {
|
func (h *Handler) tryAccumulateCount(key string) bool {
|
||||||
@ -277,6 +368,7 @@ func (h *Handler) Reset() {
|
|||||||
h.operatorKey = ""
|
h.operatorKey = ""
|
||||||
h.buffer = ""
|
h.buffer = ""
|
||||||
h.pending = ""
|
h.pending = ""
|
||||||
|
h.charMotionType = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler.Pending: Returns the accumulated input buffer for display.
|
// Handler.Pending: Returns the accumulated input buffer for display.
|
||||||
|
|||||||
@ -9,9 +9,10 @@ import (
|
|||||||
|
|
||||||
// Keymap: Maps key sequences to motions, operators, and actions.
|
// Keymap: Maps key sequences to motions, operators, and actions.
|
||||||
type Keymap struct {
|
type Keymap struct {
|
||||||
motions map[string]action.Motion
|
motions map[string]action.Motion
|
||||||
operators map[string]action.Operator
|
operators map[string]action.Operator
|
||||||
actions map[string]action.Action // standalone actions: i.e., 'i', 'a'
|
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.
|
// 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.Paste{Count: 1},
|
||||||
"P": action.PasteBefore{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) {
|
func (km *Keymap) Lookup(key string) (kind string, value any) {
|
||||||
if m, ok := km.motions[key]; ok {
|
if m, ok := km.motions[key]; ok {
|
||||||
return "motion", m
|
return "motion", m
|
||||||
@ -151,6 +158,9 @@ func (km *Keymap) Lookup(key string) (kind string, value any) {
|
|||||||
if a, ok := km.actions[key]; ok {
|
if a, ok := km.actions[key]; ok {
|
||||||
return "action", a
|
return "action", a
|
||||||
}
|
}
|
||||||
|
if cm, ok := km.charMotions[key]; ok {
|
||||||
|
return "char_motion", cm
|
||||||
|
}
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,5 +181,19 @@ func (km *Keymap) HasPrefix(prefix string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for key := range km.charMotions {
|
||||||
|
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user