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}, "%": motion.JumpToMatchingDelimiter{}, }, 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}, "/": action.EnterSearchMode{Forward: true}, "?": action.EnterSearchMode{Forward: false}, }, 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}, "%": motion.JumpToMatchingDelimiter{}, // 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}, "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: '{'}, }, } } // 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{}, }, } } // NewSearchKeymap: Creates a keymap for search mode with command line editing. func NewSearchKeymap() *Keymap { return &Keymap{ motions: map[string]action.Motion{ "left": motion.MoveSearchLeft{}, "right": motion.MoveSearchRight{}, // "up": motion.MoveCommandHistoryUp{}, // "down": motion.MoveCommandHistoryDown{}, }, operators: map[string]action.Operator{}, // this will likely be empty actions: map[string]action.Action{ "esc": action.ExitSearchMode{}, "enter": action.SearchExecute{}, "backspace": action.SearchBackspace{}, "delete": action.SearchDelete{}, "ctrl+w": action.SearchDeletePreviousWord{}, }, } } // 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 }