feature/find #1
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 {
|
||||
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,6 +14,7 @@ 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.
|
||||
@ -26,6 +27,7 @@ type Handler struct {
|
||||
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.
|
||||
|
||||
@ -12,6 +12,7 @@ type Keymap struct {
|
||||
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},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,6 +104,12 @@ func NewVisualKeymap() *Keymap {
|
||||
"p": action.VisualPaste{Count: 1},
|
||||
// ":": action.EnterComandMode{}, // Different OP
|
||||
},
|
||||
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 +153,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 +164,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 +187,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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user