326 lines
11 KiB
Go
326 lines
11 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},
|
|
"H": motion.MoveToScreenTop{Count: 1},
|
|
"M": motion.MoveToScreenMiddle{},
|
|
"L": motion.MoveToScreenBottom{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},
|
|
"B": motion.MoveBackwardWORD{Count: 1},
|
|
"ge": motion.MoveBackwardWordEnd{Count: 1},
|
|
"gE": motion.MoveBackwardWORDEnd{Count: 1},
|
|
"ctrl+u": motion.ScrollUpPage{Divisor: 2},
|
|
"ctrl+d": motion.ScrollDownPage{Divisor: 2},
|
|
"ctrl+b": motion.ScrollUpPage{Divisor: 1},
|
|
"ctrl+f": motion.ScrollDownPage{Divisor: 1},
|
|
";": 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},
|
|
"u": action.Undo{},
|
|
"ctrl+r": action.Redo{},
|
|
".": action.Repeat{Count: 1},
|
|
"R": action.EnterReplace{},
|
|
"J": action.JoinLines{Preserve: false},
|
|
"gJ": action.JoinLines{Preserve: true},
|
|
},
|
|
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},
|
|
"r": action.ReplaceChar{Count: 1},
|
|
},
|
|
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},
|
|
"H": motion.MoveToScreenTop{Count: 1},
|
|
"M": motion.MoveToScreenMiddle{},
|
|
"L": motion.MoveToScreenBottom{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},
|
|
"B": motion.MoveBackwardWORD{Count: 1},
|
|
"ge": motion.MoveBackwardWordEnd{Count: 1},
|
|
"gE": motion.MoveBackwardWORDEnd{Count: 1},
|
|
"ctrl+u": motion.ScrollUpPage{Divisor: 2},
|
|
"ctrl+d": motion.ScrollDownPage{Divisor: 2},
|
|
"ctrl+b": motion.ScrollUpPage{Divisor: 1},
|
|
"ctrl+f": motion.ScrollDownPage{Divisor: 1},
|
|
";": action.RepeatFind{Count: 1, Reverse: false},
|
|
",": action.RepeatFind{Count: 1, Reverse: true},
|
|
// TODO: O and o. These are fun ones! Should be simple too
|
|
},
|
|
operators: map[string]action.Operator{
|
|
"d": operator.DeleteOperator{},
|
|
"x": operator.DeleteOperator{},
|
|
"X": operator.DeleteOperator{},
|
|
"y": operator.YankOperator{},
|
|
"c": operator.ChangeOperator{},
|
|
"s": operator.ChangeOperator{}, // Same as c in visual mode
|
|
"R": operator.ChangeOperator{}, // Seems to do the same thing
|
|
},
|
|
actions: map[string]action.Action{
|
|
"p": action.VisualPaste{Count: 1, Replace: true},
|
|
"P": action.VisualPaste{Count: 1, Replace: false},
|
|
".": action.Repeat{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{},
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewReplaceKeymap: Creates a keymap for replace mode with editing actions. All actions
|
|
func NewReplaceKeymap() *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.ReplaceNewline{},
|
|
"backspace": action.InsertBackspace{},
|
|
"delete": action.InsertDelete{},
|
|
"tab": action.ReplaceTab{}, // TODO: This needs replacing
|
|
"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
|
|
}
|