Files
diple/keybindings.go
2026-07-29 13:12:39 +02:00

738 lines
26 KiB
Go

package main
import (
"fmt"
"strings"
"unicode/utf8"
)
type KeyBindings struct {
General GeneralKeyBindings `toml:"general"`
Navigation NavigationKeyBindings `toml:"navigation"`
Views ViewKeyBindings `toml:"views"`
Threads ThreadKeyBindings `toml:"threads"`
Input InputKeyBindings `toml:"input"`
Vim VimKeyBindings `toml:"vim"`
}
type GeneralKeyBindings struct {
Quit []string `toml:"quit"`
Help []string `toml:"help"`
Refresh []string `toml:"refresh"`
Back []string `toml:"back"`
Confirm []string `toml:"confirm"`
Reject []string `toml:"reject"`
}
type NavigationKeyBindings struct {
Down []string `toml:"down"`
Up []string `toml:"up"`
Left []string `toml:"left"`
Right []string `toml:"right"`
First []string `toml:"first"`
Last []string `toml:"last"`
PageDown []string `toml:"page_down"`
PageUp []string `toml:"page_up"`
}
type ViewKeyBindings struct {
Open []string `toml:"open"`
Dashboard []string `toml:"dashboard"`
Health []string `toml:"health"`
Edit []string `toml:"edit"`
AutoMerge []string `toml:"auto_merge"`
MergeNow []string `toml:"merge_now"`
ToggleList []string `toml:"toggle_list"`
AI []string `toml:"ai"`
}
type ThreadKeyBindings struct {
Search []string `toml:"search"`
ClearFilter []string `toml:"clear_filter"`
NextUnread []string `toml:"next_unread"`
PreviousUnread []string `toml:"previous_unread"`
Reply []string `toml:"reply"`
Resolve []string `toml:"resolve"`
Toggle []string `toml:"toggle"`
FoldPrefix []string `toml:"fold_prefix"`
FoldToggle []string `toml:"fold_toggle"`
}
type InputKeyBindings struct {
Cancel []string `toml:"cancel"`
Submit []string `toml:"submit"`
Newline []string `toml:"newline"`
DeleteBackward []string `toml:"delete_backward"`
DeleteForward []string `toml:"delete_forward"`
Clear []string `toml:"clear"`
NextField []string `toml:"next_field"`
PreviousField []string `toml:"previous_field"`
NextCompletion []string `toml:"next_completion"`
PreviousCompletion []string `toml:"previous_completion"`
LineStart []string `toml:"line_start"`
LineEnd []string `toml:"line_end"`
}
type VimKeyBindings struct {
Insert []string `toml:"insert"`
Append []string `toml:"append"`
InsertLineStart []string `toml:"insert_line_start"`
AppendLineEnd []string `toml:"append_line_end"`
OpenBelow []string `toml:"open_below"`
OpenAbove []string `toml:"open_above"`
ReplaceCharacter []string `toml:"replace_character"`
Visual []string `toml:"visual"`
VisualLine []string `toml:"visual_line"`
SelectionOtherEnd []string `toml:"selection_other_end"`
Yank []string `toml:"yank"`
Delete []string `toml:"delete"`
DeleteBefore []string `toml:"delete_before"`
Paste []string `toml:"paste"`
LineStart []string `toml:"line_start"`
FirstNonBlank []string `toml:"first_non_blank"`
LineEnd []string `toml:"line_end"`
WordForward []string `toml:"word_forward"`
WORDForward []string `toml:"big_word_forward"`
WordBackward []string `toml:"word_backward"`
WORDBackward []string `toml:"big_word_backward"`
WordEnd []string `toml:"word_end"`
WORDEnd []string `toml:"big_word_end"`
GoPrefix []string `toml:"go_prefix"`
FindForward []string `toml:"find_forward"`
FindBackward []string `toml:"find_backward"`
TillForward []string `toml:"till_forward"`
TillBackward []string `toml:"till_backward"`
RepeatFind []string `toml:"repeat_find"`
RepeatFindReverse []string `toml:"repeat_find_reverse"`
}
func defaultKeyBindings() KeyBindings {
return KeyBindings{
General: GeneralKeyBindings{
Quit: []string{"q", "ctrl+c"}, Help: []string{"?", "f1"},
Refresh: []string{"r"}, Back: []string{"b", "esc"},
Confirm: []string{"y"}, Reject: []string{"n", "esc"},
},
Navigation: NavigationKeyBindings{
Down: []string{"j", "down"}, Up: []string{"k", "up"},
Left: []string{"h", "left"}, Right: []string{"l", "right"},
First: []string{"g"}, Last: []string{"G"},
PageDown: []string{"ctrl+d", "pgdown"}, PageUp: []string{"ctrl+u", "pgup"},
},
Views: ViewKeyBindings{
Open: []string{"enter", "l"}, Dashboard: []string{"d"},
Health: []string{"H"}, Edit: []string{"e"}, ToggleList: []string{"tab"},
AutoMerge: []string{"a"}, MergeNow: []string{"M"},
AI: []string{"A"},
},
Threads: ThreadKeyBindings{
Search: []string{"/"}, ClearFilter: []string{"F"},
NextUnread: []string{"n"}, PreviousUnread: []string{"N"},
Reply: []string{"c"}, Resolve: []string{"R"}, Toggle: []string{"enter"},
FoldPrefix: []string{"z"}, FoldToggle: []string{"a"},
},
Input: InputKeyBindings{
Cancel: []string{"esc"}, Submit: []string{"ctrl+s"}, Newline: []string{"enter"},
DeleteBackward: []string{"backspace"}, DeleteForward: []string{"delete"},
Clear: []string{"ctrl+u"}, NextField: []string{"tab"},
PreviousField: []string{"shift+tab"}, NextCompletion: []string{"ctrl+n"},
PreviousCompletion: []string{"ctrl+p"},
LineStart: []string{"home", "ctrl+a"}, LineEnd: []string{"end", "ctrl+e"},
},
Vim: VimKeyBindings{
Insert: []string{"i"}, Append: []string{"a"},
InsertLineStart: []string{"I"}, AppendLineEnd: []string{"A"},
OpenBelow: []string{"o"}, OpenAbove: []string{"O"},
ReplaceCharacter: []string{"s"}, Visual: []string{"v"}, VisualLine: []string{"V"},
SelectionOtherEnd: []string{"o"}, Yank: []string{"y"},
Delete: []string{"d", "x", "delete"}, DeleteBefore: []string{"X", "backspace"},
Paste: []string{"p"}, LineStart: []string{"0", "home"},
FirstNonBlank: []string{"^"}, LineEnd: []string{"$", "end"},
WordForward: []string{"w"}, WORDForward: []string{"W"},
WordBackward: []string{"b"}, WORDBackward: []string{"B"},
WordEnd: []string{"e"}, WORDEnd: []string{"E"}, GoPrefix: []string{"g"},
FindForward: []string{"f"}, FindBackward: []string{"F"},
TillForward: []string{"t"}, TillBackward: []string{"T"},
RepeatFind: []string{";"}, RepeatFindReverse: []string{","},
},
}
}
func keyMatches(key string, bindings []string) bool {
for _, binding := range bindings {
if key == binding {
return true
}
}
return false
}
func keyLabel(bindings []string) string {
return strings.Join(bindings, " / ")
}
func primaryKeyLabel(bindings []string) string {
if len(bindings) == 0 {
return ""
}
return bindings[0]
}
func primaryCombinedKeyLabel(groups ...[]string) string {
keys := make([]string, 0, len(groups))
for _, group := range groups {
if key := primaryKeyLabel(group); key != "" {
keys = append(keys, key)
}
}
return strings.Join(keys, " / ")
}
func primarySequenceKeyLabel(prefixes, suffixes []string) string {
return primaryKeyLabel(prefixes) + primaryKeyLabel(suffixes)
}
func combinedKeyLabel(groups ...[]string) string {
var keys []string
for _, group := range groups {
keys = append(keys, group...)
}
return keyLabel(keys)
}
func sequenceKeyLabel(prefixes, suffixes []string) string {
var sequences []string
for _, prefix := range prefixes {
for _, suffix := range suffixes {
sequences = append(sequences, prefix+suffix)
}
}
return keyLabel(sequences)
}
func (k KeyBindings) canonicalHelpKey(key string) string {
switch {
case keyMatches(key, k.General.Quit):
return "ctrl+c"
case keyMatches(key, k.General.Help), keyMatches(key, k.General.Back):
return "esc"
case keyMatches(key, k.Navigation.Down):
return "j"
case keyMatches(key, k.Navigation.Up):
return "k"
case keyMatches(key, k.Navigation.First):
return "g"
case keyMatches(key, k.Navigation.Last):
return "G"
case keyMatches(key, k.Navigation.PageDown):
return "ctrl+d"
case keyMatches(key, k.Navigation.PageUp):
return "ctrl+u"
default:
return ""
}
}
func (k KeyBindings) canonicalSearchKey(key string) string {
switch {
case keyMatches(key, k.Input.Cancel):
return "esc"
case keyMatches(key, k.Input.Newline):
return "enter"
case keyMatches(key, k.Input.PreviousCompletion):
return "up"
case keyMatches(key, k.Input.NextCompletion):
return "down"
case keyMatches(key, k.Input.DeleteBackward):
return "backspace"
case keyMatches(key, k.Input.Clear):
return "ctrl+u"
default:
return ""
}
}
func (k KeyBindings) canonicalWriteKey(key string) string {
switch {
case keyMatches(key, k.Input.Cancel):
return "esc"
case keyMatches(key, k.General.Confirm):
return "y"
case keyMatches(key, k.General.Reject):
return "n"
case keyMatches(key, k.Input.Submit):
return "ctrl+s"
case keyMatches(key, k.Input.Newline):
return "enter"
case keyMatches(key, k.Input.DeleteBackward):
return "backspace"
default:
return ""
}
}
func (k KeyBindings) canonicalMainKey(key string, current screen) string {
switch {
case keyMatches(key, k.General.Quit):
return "q"
case keyMatches(key, k.General.Help):
return "?"
case keyMatches(key, k.General.Refresh):
return "r"
case keyMatches(key, k.General.Back):
return "esc"
case keyMatches(key, k.Navigation.Down):
return "j"
case keyMatches(key, k.Navigation.Up):
return "k"
case keyMatches(key, k.Navigation.First):
return "g"
case keyMatches(key, k.Navigation.Last):
return "G"
case keyMatches(key, k.Navigation.PageDown):
return "ctrl+d"
case keyMatches(key, k.Navigation.PageUp):
return "ctrl+u"
}
if current == prScreen || current == dashboardScreen {
if keyMatches(key, k.Views.Open) {
return "enter"
}
}
if current == dashboardScreen {
switch {
case keyMatches(key, k.Views.AutoMerge):
return "a"
case keyMatches(key, k.Views.MergeNow):
return "M"
}
}
if current == threadScreen {
switch {
case keyMatches(key, k.Navigation.Left):
return "h"
case keyMatches(key, k.Navigation.Right):
return "l"
case keyMatches(key, k.Views.ToggleList):
return "tab"
case keyMatches(key, k.Threads.Search):
return "/"
case keyMatches(key, k.Threads.ClearFilter):
return "F"
case keyMatches(key, k.Threads.NextUnread):
return "n"
case keyMatches(key, k.Threads.PreviousUnread):
return "N"
case keyMatches(key, k.Threads.Reply):
return "c"
case keyMatches(key, k.Threads.Resolve):
return "R"
case keyMatches(key, k.Threads.Toggle):
return "enter"
case keyMatches(key, k.Threads.FoldPrefix):
return "z"
}
}
switch {
case keyMatches(key, k.Views.Dashboard):
return "d"
case keyMatches(key, k.Views.Health):
return "H"
case keyMatches(key, k.Views.Edit):
return "e"
case (current == dashboardScreen || current == threadScreen) && keyMatches(key, k.Views.AI):
return "A"
default:
return ""
}
}
func (k KeyBindings) canonicalPREditKey(key string, field int, confirming bool) string {
if confirming {
switch {
case keyMatches(key, k.General.Confirm):
return "y"
case keyMatches(key, k.General.Reject), keyMatches(key, k.Input.Cancel):
return "esc"
case keyMatches(key, k.Navigation.Down):
return "down"
case keyMatches(key, k.Navigation.Up):
return "up"
case keyMatches(key, k.Navigation.PageDown):
return "ctrl+d"
case keyMatches(key, k.Navigation.PageUp):
return "ctrl+u"
case keyMatches(key, k.Navigation.First):
return "g"
case keyMatches(key, k.Navigation.Last):
return "G"
}
}
switch {
case keyMatches(key, k.Input.Cancel):
return "esc"
case keyMatches(key, k.Input.Submit):
return "ctrl+s"
case keyMatches(key, k.Input.NextField):
return "tab"
case keyMatches(key, k.Input.PreviousField):
return "shift+tab"
case keyMatches(key, k.Input.NextCompletion):
return "ctrl+n"
case keyMatches(key, k.Input.PreviousCompletion):
return "ctrl+p"
case keyMatches(key, k.Navigation.PageDown):
return "ctrl+d"
case keyMatches(key, k.Navigation.PageUp):
return "ctrl+u"
}
if field != prEditBodyField {
switch {
case keyMatches(key, k.Input.Newline):
return "enter"
}
}
return ""
}
func validateKeyBindings(bindings KeyBindings) error {
groups := []struct {
name string
values map[string][]string
}{
{"keybindings.general", map[string][]string{
"quit": bindings.General.Quit, "help": bindings.General.Help,
"refresh": bindings.General.Refresh, "back": bindings.General.Back,
"confirm": bindings.General.Confirm, "reject": bindings.General.Reject,
}},
{"keybindings.navigation", map[string][]string{
"down": bindings.Navigation.Down, "up": bindings.Navigation.Up,
"left": bindings.Navigation.Left, "right": bindings.Navigation.Right,
"first": bindings.Navigation.First, "last": bindings.Navigation.Last,
"page_down": bindings.Navigation.PageDown, "page_up": bindings.Navigation.PageUp,
}},
{"keybindings.views", map[string][]string{
"open": bindings.Views.Open, "dashboard": bindings.Views.Dashboard,
"health": bindings.Views.Health, "edit": bindings.Views.Edit,
"auto_merge": bindings.Views.AutoMerge, "merge_now": bindings.Views.MergeNow,
"toggle_list": bindings.Views.ToggleList,
"ai": bindings.Views.AI,
}},
{"keybindings.threads", map[string][]string{
"search": bindings.Threads.Search, "clear_filter": bindings.Threads.ClearFilter,
"next_unread": bindings.Threads.NextUnread,
"previous_unread": bindings.Threads.PreviousUnread,
"reply": bindings.Threads.Reply, "resolve": bindings.Threads.Resolve,
"toggle": bindings.Threads.Toggle, "fold_prefix": bindings.Threads.FoldPrefix,
"fold_toggle": bindings.Threads.FoldToggle,
}},
{"keybindings.input", map[string][]string{
"cancel": bindings.Input.Cancel,
"submit": bindings.Input.Submit, "newline": bindings.Input.Newline,
"delete_backward": bindings.Input.DeleteBackward,
"delete_forward": bindings.Input.DeleteForward, "clear": bindings.Input.Clear,
"next_field": bindings.Input.NextField, "previous_field": bindings.Input.PreviousField,
"next_completion": bindings.Input.NextCompletion,
"previous_completion": bindings.Input.PreviousCompletion,
"line_start": bindings.Input.LineStart, "line_end": bindings.Input.LineEnd,
}},
{"keybindings.vim", map[string][]string{
"insert": bindings.Vim.Insert, "append": bindings.Vim.Append,
"insert_line_start": bindings.Vim.InsertLineStart,
"append_line_end": bindings.Vim.AppendLineEnd,
"open_below": bindings.Vim.OpenBelow, "open_above": bindings.Vim.OpenAbove,
"replace_character": bindings.Vim.ReplaceCharacter,
"visual": bindings.Vim.Visual, "visual_line": bindings.Vim.VisualLine,
"selection_other_end": bindings.Vim.SelectionOtherEnd,
"yank": bindings.Vim.Yank, "delete": bindings.Vim.Delete,
"delete_before": bindings.Vim.DeleteBefore, "paste": bindings.Vim.Paste,
"line_start": bindings.Vim.LineStart,
"first_non_blank": bindings.Vim.FirstNonBlank, "line_end": bindings.Vim.LineEnd,
"word_forward": bindings.Vim.WordForward, "big_word_forward": bindings.Vim.WORDForward,
"word_backward": bindings.Vim.WordBackward, "big_word_backward": bindings.Vim.WORDBackward,
"word_end": bindings.Vim.WordEnd, "big_word_end": bindings.Vim.WORDEnd,
"go_prefix": bindings.Vim.GoPrefix,
"find_forward": bindings.Vim.FindForward, "find_backward": bindings.Vim.FindBackward,
"till_forward": bindings.Vim.TillForward, "till_backward": bindings.Vim.TillBackward,
"repeat_find": bindings.Vim.RepeatFind,
"repeat_find_reverse": bindings.Vim.RepeatFindReverse,
}},
}
for _, group := range groups {
for action, keys := range group.values {
if len(keys) == 0 {
return fmt.Errorf("%s.%s must contain at least one key", group.name, action)
}
seen := make(map[string]bool)
for _, key := range keys {
if strings.TrimSpace(key) == "" {
return fmt.Errorf("%s.%s contains an empty key", group.name, action)
}
if seen[key] {
return fmt.Errorf("%s.%s contains duplicate key %q", group.name, action, key)
}
seen[key] = true
}
}
}
return validateKeyBindingContexts(bindings)
}
type contextBinding struct {
action string
keys []string
}
func validateKeyBindingContexts(bindings KeyBindings) error {
navigation := bindings.Navigation
general := bindings.General
views := bindings.Views
threads := bindings.Threads
input := bindings.Input
vim := bindings.Vim
screenCommon := []contextBinding{
{"quit", general.Quit}, {"help", general.Help}, {"refresh", general.Refresh},
{"back", general.Back}, {"down", navigation.Down}, {"up", navigation.Up},
{"first", navigation.First}, {"last", navigation.Last},
{"page_down", navigation.PageDown}, {"page_up", navigation.PageUp},
{"health", views.Health},
}
if err := validateKeyContext("pull request list", append(screenCommon,
contextBinding{"open", views.Open},
contextBinding{"dashboard", views.Dashboard},
)...); err != nil {
return err
}
if err := validateKeyContext("dashboard", append(screenCommon,
contextBinding{"open", views.Open},
contextBinding{"edit", views.Edit},
contextBinding{"auto_merge", views.AutoMerge},
contextBinding{"merge_now", views.MergeNow},
contextBinding{"ai", views.AI},
)...); err != nil {
return err
}
if err := validateKeyContext("health screen", screenCommon...); err != nil {
return err
}
if err := validateKeyContext("review threads", append(screenCommon,
contextBinding{"left", navigation.Left},
contextBinding{"right", navigation.Right},
contextBinding{"toggle_list", views.ToggleList},
contextBinding{"dashboard", views.Dashboard},
contextBinding{"search", threads.Search},
contextBinding{"clear_filter", threads.ClearFilter},
contextBinding{"next_unread", threads.NextUnread},
contextBinding{"previous_unread", threads.PreviousUnread},
contextBinding{"reply", threads.Reply},
contextBinding{"resolve", threads.Resolve},
contextBinding{"toggle", threads.Toggle},
contextBinding{"fold_prefix", threads.FoldPrefix},
contextBinding{"ai", views.AI},
)...); err != nil {
return err
}
if err := validateKeyContext("help popup",
contextBinding{"quit", general.Quit},
contextBinding{"close_help", appendCopy(general.Help, general.Back...)},
contextBinding{"down", navigation.Down},
contextBinding{"up", navigation.Up},
contextBinding{"first", navigation.First},
contextBinding{"last", navigation.Last},
contextBinding{"page_down", navigation.PageDown},
contextBinding{"page_up", navigation.PageUp},
); err != nil {
return err
}
if err := validateKeyContext("search input",
contextBinding{"quit", nonTextBindings(general.Quit)},
contextBinding{"cancel", input.Cancel},
contextBinding{"apply", input.Newline},
contextBinding{"previous_completion", input.PreviousCompletion},
contextBinding{"next_completion", input.NextCompletion},
contextBinding{"delete_backward", input.DeleteBackward},
contextBinding{"clear", input.Clear},
); err != nil {
return err
}
if err := validateKeyContext("reply input",
contextBinding{"quit", nonTextBindings(general.Quit)},
contextBinding{"cancel", input.Cancel},
contextBinding{"submit", input.Submit},
contextBinding{"newline", input.Newline},
contextBinding{"delete_backward", input.DeleteBackward},
); err != nil {
return err
}
if err := validateKeyContext("confirmation",
contextBinding{"quit", nonTextBindings(general.Quit)},
contextBinding{"confirm", general.Confirm},
contextBinding{"cancel", appendCopy(general.Reject, input.Cancel...)},
); err != nil {
return err
}
if err := validateKeyContext("AI menu",
contextBinding{"quit", nonTextBindings(general.Quit)},
contextBinding{"cancel", appendCopy(general.Back, input.Cancel...)},
contextBinding{"down", navigation.Down},
contextBinding{"up", navigation.Up},
contextBinding{"select", appendCopy(views.Open, input.Newline...)},
); err != nil {
return err
}
if err := validateKeyContext("AI confirmation",
contextBinding{"quit", nonTextBindings(general.Quit)},
contextBinding{"confirm", general.Confirm},
contextBinding{"cancel", appendCopy(general.Reject, input.Cancel...)},
contextBinding{"down", navigation.Down},
contextBinding{"up", navigation.Up},
contextBinding{"page_down", navigation.PageDown},
contextBinding{"page_up", navigation.PageUp},
); err != nil {
return err
}
editorOuter := []contextBinding{
{"help", general.Help},
{"cancel", input.Cancel},
{"submit", input.Submit},
{"next_field", input.NextField},
{"previous_field", input.PreviousField},
{"page_down", navigation.PageDown},
{"page_up", navigation.PageUp},
}
normalEditor := append(append([]contextBinding(nil), editorOuter...),
contextBinding{"left", navigation.Left},
contextBinding{"down", navigation.Down},
contextBinding{"up", navigation.Up},
contextBinding{"right", navigation.Right},
contextBinding{"last", navigation.Last},
contextBinding{"insert", vim.Insert},
contextBinding{"append", vim.Append},
contextBinding{"insert_line_start", vim.InsertLineStart},
contextBinding{"append_line_end", vim.AppendLineEnd},
contextBinding{"open_below", vim.OpenBelow},
contextBinding{"open_above", vim.OpenAbove},
contextBinding{"replace_character", vim.ReplaceCharacter},
contextBinding{"visual", vim.Visual},
contextBinding{"visual_line", vim.VisualLine},
contextBinding{"paste", vim.Paste},
contextBinding{"line_start", vim.LineStart},
contextBinding{"first_non_blank", vim.FirstNonBlank},
contextBinding{"line_end", vim.LineEnd},
contextBinding{"word_forward", vim.WordForward},
contextBinding{"big_word_forward", vim.WORDForward},
contextBinding{"word_backward", vim.WordBackward},
contextBinding{"big_word_backward", vim.WORDBackward},
contextBinding{"word_end", vim.WordEnd},
contextBinding{"big_word_end", vim.WORDEnd},
contextBinding{"go_prefix", vim.GoPrefix},
contextBinding{"find_forward", vim.FindForward},
contextBinding{"find_backward", vim.FindBackward},
contextBinding{"till_forward", vim.TillForward},
contextBinding{"till_backward", vim.TillBackward},
contextBinding{"repeat_find", vim.RepeatFind},
contextBinding{"repeat_find_reverse", vim.RepeatFindReverse},
contextBinding{"delete", vim.Delete},
contextBinding{"delete_before", vim.DeleteBefore},
)
if err := validateKeyContext("Vim Normal mode", normalEditor...); err != nil {
return err
}
visualEditor := append(append([]contextBinding(nil), editorOuter...),
contextBinding{"left", navigation.Left},
contextBinding{"down", navigation.Down},
contextBinding{"up", navigation.Up},
contextBinding{"right", navigation.Right},
contextBinding{"last", navigation.Last},
contextBinding{"visual", vim.Visual},
contextBinding{"visual_line", vim.VisualLine},
contextBinding{"selection_other_end", vim.SelectionOtherEnd},
contextBinding{"yank", vim.Yank},
contextBinding{"delete", vim.Delete},
contextBinding{"paste", vim.Paste},
contextBinding{"line_start", vim.LineStart},
contextBinding{"first_non_blank", vim.FirstNonBlank},
contextBinding{"line_end", vim.LineEnd},
contextBinding{"word_forward", vim.WordForward},
contextBinding{"big_word_forward", vim.WORDForward},
contextBinding{"word_backward", vim.WordBackward},
contextBinding{"big_word_backward", vim.WORDBackward},
contextBinding{"word_end", vim.WordEnd},
contextBinding{"big_word_end", vim.WORDEnd},
contextBinding{"go_prefix", vim.GoPrefix},
contextBinding{"find_forward", vim.FindForward},
contextBinding{"find_backward", vim.FindBackward},
contextBinding{"till_forward", vim.TillForward},
contextBinding{"till_backward", vim.TillBackward},
contextBinding{"repeat_find", vim.RepeatFind},
contextBinding{"repeat_find_reverse", vim.RepeatFindReverse},
)
if err := validateKeyContext("Vim Visual mode", visualEditor...); err != nil {
return err
}
insertEditor := append(append([]contextBinding(nil), editorOuter...),
contextBinding{"left", nonTextBindings(navigation.Left)},
contextBinding{"down", nonTextBindings(navigation.Down)},
contextBinding{"up", nonTextBindings(navigation.Up)},
contextBinding{"right", nonTextBindings(navigation.Right)},
contextBinding{"newline", input.Newline},
contextBinding{"delete_backward", input.DeleteBackward},
contextBinding{"delete_forward", input.DeleteForward},
contextBinding{"line_start", input.LineStart},
contextBinding{"line_end", input.LineEnd},
)
if err := validateKeyContext("editor Insert mode", insertEditor...); err != nil {
return err
}
return validateKeyContext("single-line editor input",
contextBinding{"help", nonTextBindings(general.Help)},
contextBinding{"cancel", input.Cancel},
contextBinding{"submit", input.Submit},
contextBinding{"next_field", input.NextField},
contextBinding{"previous_field", input.PreviousField},
contextBinding{"previous_completion", input.PreviousCompletion},
contextBinding{"next_completion", input.NextCompletion},
contextBinding{"newline", input.Newline},
contextBinding{"delete_backward", input.DeleteBackward},
contextBinding{"delete_forward", input.DeleteForward},
contextBinding{"line_start", input.LineStart},
contextBinding{"line_end", input.LineEnd},
contextBinding{"up", nonTextBindings(navigation.Up)},
contextBinding{"down", nonTextBindings(navigation.Down)},
)
}
func validateKeyContext(context string, bindings ...contextBinding) error {
assigned := make(map[string]string)
for _, binding := range bindings {
for _, key := range binding.keys {
if previous, exists := assigned[key]; exists && previous != binding.action {
return fmt.Errorf(
"keybinding conflict in %s: key %q is assigned to both %s and %s",
context, key, previous, binding.action,
)
}
assigned[key] = binding.action
}
}
return nil
}
func nonTextBindings(bindings []string) []string {
var filtered []string
for _, binding := range bindings {
if utf8.RuneCountInString(binding) != 1 {
filtered = append(filtered, binding)
}
}
return filtered
}
func appendCopy(bindings []string, more ...string) []string {
result := append([]string(nil), bindings...)
return append(result, more...)
}