Gim/internal/input/keymap.go
Hayden Hargreaves 5405d5a6bd
All checks were successful
Run Test Suite / test (push) Successful in 47s
fix: added b and B text objects and note about failing case.
2026-03-26 14:15:59 -07:00

275 lines
9.0 KiB
Go

package input
import (
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/command"
"git.gophernest.net/azpect/TextEditor/internal/motion"
"git.gophernest.net/azpect/TextEditor/internal/operator"
"git.gophernest.net/azpect/TextEditor/internal/textobject"
)
// 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'
charMotions map[string]action.Motion // motions that need character argument: f/t/F/T
modifiers map[string]any // modifiers for text objects: i/a
textObjects map[string]action.TextObject // motions that need text objects: i.e., 'viw'
}
// NewNormalKeymap: Creates a keymap for normal mode with all standard vim bindings.
func NewNormalKeymap() *Keymap {
return &Keymap{
motions: map[string]action.Motion{
"j": motion.MoveDown{Count: 1},
"k": motion.MoveUp{Count: 1},
"h": motion.MoveLeft{Count: 1},
"l": motion.MoveRight{Count: 1},
"G": motion.MoveToBottom{},
"gg": motion.MoveToTop{},
"0": motion.MoveToLineStart{},
"$": motion.MoveToLineEnd{},
"_": motion.MoveToLineContentStart{},
"^": motion.MoveToLineContentStart{},
"|": motion.MoveToColumn{Count: 0},
"w": motion.MoveForwardWord{Count: 1},
"W": motion.MoveForwardWORD{Count: 1},
"e": motion.MoveForwardWordEnd{Count: 1},
"E": motion.MoveForwardWORDEnd{Count: 1},
"b": motion.MoveBackwardWord{Count: 1},
"ctrl+u": motion.ScrollUpHalfPage{},
"ctrl+d": motion.ScrollDownHalfPage{},
";": action.RepeatFind{Count: 1, Reverse: false},
".": action.RepeatFind{Count: 1, Reverse: true},
},
operators: map[string]action.Operator{
"d": operator.DeleteOperator{},
"y": operator.YankOperator{},
"c": operator.ChangeOperator{}, // TODO: Finish implementing
// "s": SubstitueOp{},
// "~": SwapCaseOp{},
},
actions: map[string]action.Action{
"i": action.EnterInsert{},
"a": action.EnterInsertAfter{},
"I": action.EnterInsertLineStart{},
"A": action.EnterInsertLineEnd{},
"o": action.OpenLineBelow{},
"O": action.OpenLineAbove{},
"x": action.DeleteChar{Count: 1},
"X": action.DeletePrevChar{Count: 1},
":": action.EnterComandMode{},
"v": action.EnterVisualMode{},
"V": action.EnterVisualLineMode{},
"ctrl+v": action.EnterVisualBlockMode{},
"D": action.DeleteToEndOfLine{Count: 1},
"C": action.ChangeToEndOfLine{Count: 1},
"s": action.SubstituteChar{Count: 1},
"S": action.SubstituteLine{Count: 1},
"p": action.Paste{Count: 1},
"P": action.PasteBefore{Count: 1},
},
charMotions: map[string]action.Motion{
"f": action.FindChar{Forward: true, Inclusive: true, Repeated: false},
"F": action.FindChar{Forward: false, Inclusive: true, Repeated: false},
"t": action.FindChar{Forward: true, Inclusive: false, Repeated: false},
"T": action.FindChar{Forward: false, Inclusive: false, Repeated: false},
},
modifiers: map[string]any{
"i": nil,
"a": nil,
},
textObjects: map[string]action.TextObject{
"w": textobject.Word{},
"W": textobject.WORD{},
// TODO: 's' and 'p'
"{": textobject.Delimiter{Char: '{'},
"}": textobject.Delimiter{Char: '}'},
"(": textobject.Delimiter{Char: '('},
")": textobject.Delimiter{Char: ')'},
"[": textobject.Delimiter{Char: '['},
"]": textobject.Delimiter{Char: ']'},
"<": textobject.Delimiter{Char: '<'},
">": textobject.Delimiter{Char: '>'},
"\"": textobject.Delimiter{Char: '"'},
"'": textobject.Delimiter{Char: '\''},
"`": textobject.Delimiter{Char: '`'},
"b": textobject.Delimiter{Char: '('},
"B": textobject.Delimiter{Char: '{'},
},
}
}
// NewVisualKeymap: Creates a keymap for visual modes (character, line, block).
func NewVisualKeymap() *Keymap {
return &Keymap{
motions: map[string]action.Motion{
"j": motion.MoveDown{Count: 1},
"k": motion.MoveUp{Count: 1},
"h": motion.MoveLeft{Count: 1},
"l": motion.MoveRight{Count: 1},
"G": motion.MoveToBottom{},
"gg": motion.MoveToTop{},
"0": motion.MoveToLineStart{},
"$": motion.MoveToLineEnd{},
"_": motion.MoveToLineContentStart{},
"^": motion.MoveToLineContentStart{},
"|": motion.MoveToColumn{Count: 0},
"w": motion.MoveForwardWord{Count: 1},
"W": motion.MoveForwardWORD{Count: 1},
"e": motion.MoveForwardWordEnd{Count: 1},
"E": motion.MoveForwardWORDEnd{Count: 1},
"b": motion.MoveBackwardWord{Count: 1},
},
operators: map[string]action.Operator{
"d": operator.DeleteOperator{},
"x": operator.DeleteOperator{},
"X": operator.DeleteOperator{},
"y": operator.YankOperator{},
"c": operator.ChangeOperator{},
},
actions: map[string]action.Action{
"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},
},
modifiers: map[string]any{
"i": nil,
"a": nil,
},
textObjects: map[string]action.TextObject{
"w": textobject.Word{},
"W": textobject.WORD{},
// TODO: 's' and 'p'
"{": textobject.Delimiter{Char: '{'},
"}": textobject.Delimiter{Char: '}'},
"(": textobject.Delimiter{Char: '('},
")": textobject.Delimiter{Char: ')'},
"[": textobject.Delimiter{Char: '['},
"]": textobject.Delimiter{Char: ']'},
"<": textobject.Delimiter{Char: '<'},
">": textobject.Delimiter{Char: '>'},
"\"": textobject.Delimiter{Char: '"'},
"'": textobject.Delimiter{Char: '\''},
"`": textobject.Delimiter{Char: '`'},
"b": textobject.Delimiter{Char: '('},
"B": textobject.Delimiter{Char: '{'},
},
}
}
// NewInsertKeymap: Creates a keymap for insert mode with editing actions.
func NewInsertKeymap() *Keymap {
return &Keymap{
motions: map[string]action.Motion{
"down": motion.MoveDown{Count: 1},
"up": motion.MoveUp{Count: 1},
"left": motion.MoveLeft{Count: 1},
"right": motion.MoveRight{Count: 1},
},
operators: map[string]action.Operator{}, // this will likely be empty
actions: map[string]action.Action{
"enter": action.InsertNewline{},
"backspace": action.InsertBackspace{},
"delete": action.InsertDelete{},
"tab": action.InsertTab{},
"ctrl+w": action.InsertDeletePreviousWord{},
},
}
}
// NewCommandKeymap: Creates a keymap for command mode with command line editing.
func NewCommandKeymap() *Keymap {
return &Keymap{
motions: map[string]action.Motion{
"left": motion.MoveCommandLeft{},
"right": motion.MoveCommandRight{},
"up": motion.MoveCommandHistoryUp{},
"down": motion.MoveCommandHistoryDown{},
},
operators: map[string]action.Operator{}, // this will likely be empty
actions: map[string]action.Action{
"esc": action.ExitCommandMode{},
"enter": action.CommandExecute{Registry: command.DefaultRegistry},
"backspace": action.CommandBackspace{},
"delete": action.CommandDelete{},
"ctrl+w": action.CommandDeletePreviousWord{},
},
}
}
// 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
}
if o, ok := km.operators[key]; ok {
return "operator", o
}
if a, ok := km.actions[key]; ok {
return "action", a
}
if cm, ok := km.charMotions[key]; ok {
return "char_motion", cm
}
if mo, ok := km.modifiers[key]; ok {
return "modifier", mo
}
if to, ok := km.textObjects[key]; ok {
return "text_object", to
}
return "", nil
}
// Keymap.HasPrefix: Returns true if any binding starts with the given prefix.
func (km *Keymap) HasPrefix(prefix string) bool {
for key := range km.motions {
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
return true
}
}
for key := range km.operators {
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
return true
}
}
for key := range km.actions {
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
return true
}
}
for key := range km.charMotions {
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
return true
}
}
for key := range km.modifiers {
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
return true
}
}
for key := range km.textObjects {
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
}