1package vimkeys
2
3import (
4 "errors"
5 "strings"
6 "testing"
7)
8
9func TestParseBinding_SingleKeys(t *testing.T) {
10 tests := []struct {
11 input string
12 want Sequence
13 }{
14 {input: "j", want: Sequence{{Sym: "j"}}},
15 {input: "shift+j", want: Sequence{{Sym: "j", Mods: ModShift}}},
16 {input: "ctrl++", want: Sequence{{Sym: "+", Mods: ModCtrl}}},
17 {input: "ctrl+-", want: Sequence{{Sym: "-", Mods: ModCtrl}}},
18 {input: "<C-d>", want: Sequence{{Sym: "d", Mods: ModCtrl}}},
19 {input: "<CR>", want: Sequence{{Sym: "CR"}}},
20 {input: "<Return>", want: Sequence{{Sym: "CR"}}},
21 {input: "<Esc>", want: Sequence{{Sym: "Esc"}}},
22 {input: "<Escape>", want: Sequence{{Sym: "Esc"}}},
23 {input: "<Space>", want: Sequence{{Sym: "Space"}}},
24 {input: "<Tab>", want: Sequence{{Sym: "Tab"}}},
25 {input: "<BackSpace>", want: Sequence{{Sym: "BackSpace"}}},
26 {input: "<Delete>", want: Sequence{{Sym: "Delete"}}},
27 {input: "<Left>", want: Sequence{{Sym: "Left"}}},
28 {input: "<Right>", want: Sequence{{Sym: "Right"}}},
29 {input: "<Up>", want: Sequence{{Sym: "Up"}}},
30 {input: "<Down>", want: Sequence{{Sym: "Down"}}},
31 {input: "<Home>", want: Sequence{{Sym: "Home"}}},
32 {input: "<End>", want: Sequence{{Sym: "End"}}},
33 {input: "<PageUp>", want: Sequence{{Sym: "PageUp"}}},
34 {input: "<PageDown>", want: Sequence{{Sym: "PageDown"}}},
35 {input: "<C-Tab>", want: Sequence{{Sym: "Tab", Mods: ModCtrl}}},
36 {input: "<S-BackSpace>", want: Sequence{{Sym: "BackSpace", Mods: ModShift}}},
37 {input: "<A-Delete>", want: Sequence{{Sym: "Delete", Mods: ModAlt}}},
38 {input: "<C-Left>", want: Sequence{{Sym: "Left", Mods: ModCtrl}}},
39 {input: "<lt>", want: Sequence{{Sym: "<"}}},
40 {input: "<gt>", want: Sequence{{Sym: ">"}}},
41 {input: "<C-gt>", want: Sequence{{Sym: ">", Mods: ModCtrl}}},
42 {input: "<Plus>", want: Sequence{{Sym: "+"}}},
43 {input: "enter", want: Sequence{{Sym: "CR"}}},
44 {input: "escape", want: Sequence{{Sym: "Esc"}}},
45 {input: "Enter", want: Sequence{{Sym: "CR"}}},
46 {input: "Escape", want: Sequence{{Sym: "Esc"}}},
47 }
48 for _, tt := range tests {
49 t.Run(tt.input, func(t *testing.T) {
50 got, err := ParseBinding(tt.input)
51 if err != nil {
52 t.Fatalf("ParseBinding(%q) error = %v", tt.input, err)
53 }
54 if !got.Equal(tt.want) {
55 t.Fatalf("ParseBinding(%q) = %v, want %v", tt.input, got, tt.want)
56 }
57 })
58 }
59}
60
61func TestParseBinding_Sequences(t *testing.T) {
62 tests := []struct {
63 input string
64 want Sequence
65 }{
66 {input: "]]", want: Sequence{{Sym: "]"}, {Sym: "]"}}},
67 {input: "yah", want: Sequence{{Sym: "y"}, {Sym: "a"}, {Sym: "h"}}},
68 {input: "gO", want: Sequence{{Sym: "g"}, {Sym: "o", Mods: ModShift}}},
69 }
70 for _, tt := range tests {
71 t.Run(tt.input, func(t *testing.T) {
72 got, err := ParseBinding(tt.input)
73 if err != nil {
74 t.Fatalf("ParseBinding(%q) error = %v", tt.input, err)
75 }
76 if !got.Equal(tt.want) {
77 t.Fatalf("ParseBinding(%q) = %v, want %v", tt.input, got, tt.want)
78 }
79 })
80 }
81}
82
83func TestParseBinding_Malformed(t *testing.T) {
84 tests := []struct {
85 input string
86 want error
87 }{
88 {input: "", want: ErrEmptyBinding},
89 {input: " ", want: ErrEmptyBinding},
90 {input: "<", want: ErrBadBinding},
91 {input: "<>", want: ErrBadBinding},
92 {input: "<Bad>", want: ErrBadBinding},
93 {input: "shift+", want: ErrBadBinding},
94 {input: "ctrl+", want: ErrBadBinding},
95 }
96 for _, tt := range tests {
97 t.Run(tt.input, func(t *testing.T) {
98 _, err := ParseBinding(tt.input)
99 if err == nil {
100 t.Fatalf("ParseBinding(%q) expected error", tt.input)
101 }
102 if !errors.Is(err, tt.want) {
103 t.Fatalf("ParseBinding(%q) error = %v, want %v", tt.input, err, tt.want)
104 }
105 })
106 }
107}
108
109func TestParseBinding_CanonicalString(t *testing.T) {
110 tests := []struct {
111 input string
112 want string
113 }{
114 {input: "j", want: "j"},
115 {input: "shift+j", want: "J"},
116 {input: "ctrl++", want: "<C-+>"},
117 {input: "ctrl+-", want: "<C-->"},
118 {input: "<C-->", want: "<C-->"},
119 {input: "<C-d>", want: "<C-d>"},
120 {input: "<Plus>", want: "<Plus>"},
121 {input: "+", want: "<Plus>"},
122 {input: "<CR>", want: "<Return>"},
123 {input: "<Return>", want: "<Return>"},
124 {input: "<Esc>", want: "<Escape>"},
125 {input: "<Escape>", want: "<Escape>"},
126 {input: "<Space>", want: "<Space>"},
127 {input: "<Tab>", want: "<Tab>"},
128 {input: "<BackSpace>", want: "<BackSpace>"},
129 {input: "<Delete>", want: "<Delete>"},
130 {input: "<Left>", want: "<Left>"},
131 {input: "<Right>", want: "<Right>"},
132 {input: "<Up>", want: "<Up>"},
133 {input: "<Down>", want: "<Down>"},
134 {input: "<Home>", want: "<Home>"},
135 {input: "<End>", want: "<End>"},
136 {input: "<PageUp>", want: "<PageUp>"},
137 {input: "<PageDown>", want: "<PageDown>"},
138 {input: "<C-Tab>", want: "<C-Tab>"},
139 {input: "<S-BackSpace>", want: "<S-BackSpace>"},
140 {input: "<A-Delete>", want: "<A-Delete>"},
141 {input: "<C-Home>", want: "<C-Home>"},
142 {input: "<A-PageDown>", want: "<A-PageDown>"},
143 {input: "<lt>", want: "<lt>"},
144 {input: "<gt>", want: "<gt>"},
145 {input: ">", want: "<gt>"},
146 {input: "ctrl+>", want: "<C-gt>"},
147 {input: "<C-gt>", want: "<C-gt>"},
148 {input: "gO", want: "gO"},
149 }
150 for _, tt := range tests {
151 t.Run(tt.input, func(t *testing.T) {
152 seq, err := ParseBinding(tt.input)
153 if err != nil {
154 t.Fatalf("ParseBinding(%q) error = %v", tt.input, err)
155 }
156 if got := seq.String(); got != tt.want {
157 t.Fatalf("ParseBinding(%q).String() = %q, want %q", tt.input, got, tt.want)
158 }
159 })
160 }
161}
162
163func TestParseBinding_LegacyChords(t *testing.T) {
164 tests := []struct {
165 input string
166 want Sequence
167 }{
168 {input: "ctrl+d", want: Sequence{{Sym: "d", Mods: ModCtrl}}},
169 {input: "control+j", want: Sequence{{Sym: "j", Mods: ModCtrl}}},
170 {input: "alt+k", want: Sequence{{Sym: "k", Mods: ModAlt}}},
171 {input: "ctrl+J", want: Sequence{{Sym: "j", Mods: ModCtrl | ModShift}}},
172 }
173 for _, tt := range tests {
174 t.Run(tt.input, func(t *testing.T) {
175 got, err := ParseBinding(tt.input)
176 if err != nil {
177 t.Fatalf("ParseBinding(%q) error = %v", tt.input, err)
178 }
179 if !got.Equal(tt.want) {
180 t.Fatalf("ParseBinding(%q) = %v, want %v", tt.input, got, tt.want)
181 }
182 })
183 }
184}
185
186func TestParseBinding_MoreMalformed(t *testing.T) {
187 tests := []string{
188 "ctrl+shift+j+k",
189 "<C->",
190 "<C--d>",
191 "<Bad>",
192 "ctrl+",
193 }
194 for _, input := range tests {
195 t.Run(input, func(t *testing.T) {
196 _, err := ParseBinding(input)
197 if err == nil {
198 t.Fatalf("ParseBinding(%q) expected error", input)
199 }
200 if !errors.Is(err, ErrBadBinding) {
201 t.Fatalf("ParseBinding(%q) error = %v, want %v", input, err, ErrBadBinding)
202 }
203 })
204 }
205}
206
207func TestParseBinding_PlusAlone(t *testing.T) {
208 got, err := ParseBinding("+")
209 if err != nil {
210 t.Fatalf("ParseBinding error = %v", err)
211 }
212 want := Sequence{{Sym: "+"}}
213 if !got.Equal(want) {
214 t.Fatalf("ParseBinding = %v, want %v", got, want)
215 }
216}
217
218func TestParseBinding_LooksLikeChordButRaw(t *testing.T) {
219 got, err := ParseBinding("]c")
220 if err != nil {
221 t.Fatalf("ParseBinding error = %v", err)
222 }
223 want := Sequence{{Sym: "]"}, {Sym: "c"}}
224 if !got.Equal(want) {
225 t.Fatalf("ParseBinding = %v, want %v", got, want)
226 }
227}
228
229func TestParseBinding_BareModifierLikeIsLiteral(t *testing.T) {
230 // Modifiers are canonical only inside <...>; bare "C-d" is Shift+c, '-', d.
231 tests := []struct {
232 input string
233 want Sequence
234 }{
235 {
236 input: "C-d",
237 want: Sequence{
238 {Sym: "c", Mods: ModShift},
239 {Sym: "-"},
240 {Sym: "d"},
241 },
242 },
243 {
244 input: "S-j",
245 want: Sequence{
246 {Sym: "s", Mods: ModShift},
247 {Sym: "-"},
248 {Sym: "j"},
249 },
250 },
251 {
252 input: "A-k",
253 want: Sequence{
254 {Sym: "a", Mods: ModShift},
255 {Sym: "-"},
256 {Sym: "k"},
257 },
258 },
259 }
260 for _, tt := range tests {
261 t.Run(tt.input, func(t *testing.T) {
262 got, err := ParseBinding(tt.input)
263 if err != nil {
264 t.Fatalf("ParseBinding(%q) error = %v", tt.input, err)
265 }
266 if !got.Equal(tt.want) {
267 t.Fatalf("ParseBinding(%q) = %v, want %v", tt.input, got, tt.want)
268 }
269 if got := got.String(); got != tt.input {
270 t.Fatalf("ParseBinding(%q).String() = %q, want %q", tt.input, got, tt.input)
271 }
272 })
273 }
274}
275
276func TestParseBinding_CtrlGtRoundTrip(t *testing.T) {
277 tests := []struct {
278 input string
279 want Sequence
280 wantString string
281 }{
282 {
283 input: "ctrl+>",
284 want: Sequence{{Sym: ">", Mods: ModCtrl}},
285 wantString: "<C-gt>",
286 },
287 {
288 input: "<C-gt>",
289 want: Sequence{{Sym: ">", Mods: ModCtrl}},
290 wantString: "<C-gt>",
291 },
292 {
293 input: "<S-gt>",
294 want: Sequence{{Sym: ">", Mods: ModShift}},
295 wantString: "<S-gt>",
296 },
297 {
298 input: "<A-gt>",
299 want: Sequence{{Sym: ">", Mods: ModAlt}},
300 wantString: "<A-gt>",
301 },
302 {
303 input: ">",
304 want: Sequence{{Sym: ">"}},
305 wantString: "<gt>",
306 },
307 {
308 input: "<gt>",
309 want: Sequence{{Sym: ">"}},
310 wantString: "<gt>",
311 },
312 }
313 for _, tt := range tests {
314 t.Run(tt.input, func(t *testing.T) {
315 first, err := ParseBinding(tt.input)
316 if err != nil {
317 t.Fatalf("ParseBinding(%q) error = %v", tt.input, err)
318 }
319 if !first.Equal(tt.want) {
320 t.Fatalf("ParseBinding(%q) = %v, want %v", tt.input, first, tt.want)
321 }
322 if got := first.String(); got != tt.wantString {
323 t.Fatalf("ParseBinding(%q).String() = %q, want %q", tt.input, got, tt.wantString)
324 }
325 second, err := ParseBinding(first.String())
326 if err != nil {
327 t.Fatalf("ParseBinding(%q) from canonical %q error = %v", tt.input, first.String(), err)
328 }
329 if !first.Equal(second) {
330 t.Fatalf("round trip %q -> %q -> %v, want %v", tt.input, first.String(), second, first)
331 }
332 })
333 }
334}
335
336func TestParseBinding_ShiftCHyphenDRoundTrip(t *testing.T) {
337 input := "<S-c>-d"
338 want := Sequence{
339 {Sym: "c", Mods: ModShift},
340 {Sym: "-"},
341 {Sym: "d"},
342 }
343 first, err := ParseBinding(input)
344 if err != nil {
345 t.Fatalf("ParseBinding(%q) error = %v", input, err)
346 }
347 if !first.Equal(want) {
348 t.Fatalf("ParseBinding(%q) = %v, want %v", input, first, want)
349 }
350 canonical := first.String()
351 if canonical != "C-d" {
352 t.Fatalf("String() = %q, want %q", canonical, "C-d")
353 }
354 second, err := ParseBinding(canonical)
355 if err != nil {
356 t.Fatalf("ParseBinding(%q) error = %v", canonical, err)
357 }
358 if !first.Equal(second) {
359 t.Fatalf("round trip %q -> %q -> %v, want %v", input, canonical, second, first)
360 }
361}
362
363func TestParseBinding_SpecialNamesInSequence(t *testing.T) {
364 tests := []struct {
365 input string
366 want Sequence
367 }{
368 {input: "j<Esc>", want: Sequence{{Sym: "j"}, {Sym: "Esc"}}},
369 {input: "<Space>j", want: Sequence{{Sym: "Space"}, {Sym: "j"}}},
370 {input: "<CR>j", want: Sequence{{Sym: "CR"}, {Sym: "j"}}},
371 {input: "<lt>j", want: Sequence{{Sym: "<"}, {Sym: "j"}}},
372 {input: "<gt>j", want: Sequence{{Sym: ">"}, {Sym: "j"}}},
373 {input: "<Plus>j", want: Sequence{{Sym: "+"}, {Sym: "j"}}},
374 {input: "j<Tab>", want: Sequence{{Sym: "j"}, {Sym: "Tab"}}},
375 {input: "<BackSpace>j", want: Sequence{{Sym: "BackSpace"}, {Sym: "j"}}},
376 }
377 for _, tt := range tests {
378 t.Run(tt.input, func(t *testing.T) {
379 got, err := ParseBinding(tt.input)
380 if err != nil {
381 t.Fatalf("ParseBinding(%q) error = %v", tt.input, err)
382 }
383 if !got.Equal(tt.want) {
384 t.Fatalf("ParseBinding(%q) = %v, want %v", tt.input, got, tt.want)
385 }
386 })
387 }
388}
389
390func TestParseBinding_BareNamedPrefixIsLiteral(t *testing.T) {
391 // Named keys are canonical only in <...>; bare "Spacej" is Shift+s + p + a + c + e + j.
392 seq := Sequence{
393 {Sym: "s", Mods: ModShift},
394 {Sym: "p"},
395 {Sym: "a"},
396 {Sym: "c"},
397 {Sym: "e"},
398 {Sym: "j"},
399 }
400 if got := seq.String(); got != "Spacej" {
401 t.Fatalf("seq.String() = %q, want %q", got, "Spacej")
402 }
403 got, err := ParseBinding("Spacej")
404 if err != nil {
405 t.Fatalf("ParseBinding(%q) error = %v", "Spacej", err)
406 }
407 if !got.Equal(seq) {
408 t.Fatalf("ParseBinding(%q) = %v, want %v", "Spacej", got, seq)
409 }
410}
411
412func TestParseBinding_PrintableGlyphs(t *testing.T) {
413 got, err := ParseBinding("!@#")
414 if err != nil {
415 t.Fatalf("ParseBinding error = %v", err)
416 }
417 want := Sequence{{Sym: "!"}, {Sym: "@"}, {Sym: "#"}}
418 if !got.Equal(want) {
419 t.Fatalf("ParseBinding = %v, want %v", got, want)
420 }
421}
422
423func TestParseBinding_MixedAngleSequences(t *testing.T) {
424 tests := []struct {
425 input string
426 want Sequence
427 }{
428 {
429 input: "<C-d>j",
430 want: Sequence{{Sym: "d", Mods: ModCtrl}, {Sym: "j"}},
431 },
432 {
433 input: "<Escape>j",
434 want: Sequence{{Sym: "Esc"}, {Sym: "j"}},
435 },
436 }
437 for _, tt := range tests {
438 t.Run(tt.input, func(t *testing.T) {
439 got, err := ParseBinding(tt.input)
440 if err != nil {
441 t.Fatalf("ParseBinding(%q) error = %v", tt.input, err)
442 }
443 if !got.Equal(tt.want) {
444 t.Fatalf("ParseBinding(%q) = %v, want %v", tt.input, got, tt.want)
445 }
446 })
447 }
448}
449
450func TestParseBinding_CtrlHyphenRoundTrip(t *testing.T) {
451 tests := []struct {
452 input string
453 want Sequence
454 wantString string
455 }{
456 {
457 input: "ctrl+-",
458 want: Sequence{{Sym: "-", Mods: ModCtrl}},
459 wantString: "<C-->",
460 },
461 {
462 input: "<C-->",
463 want: Sequence{{Sym: "-", Mods: ModCtrl}},
464 wantString: "<C-->",
465 },
466 {
467 input: "<C-S-->",
468 want: Sequence{{Sym: "-", Mods: ModCtrl | ModShift}},
469 wantString: "<C-S-->",
470 },
471 {
472 input: "<C-A-->",
473 want: Sequence{{Sym: "-", Mods: ModCtrl | ModAlt}},
474 wantString: "<C-A-->",
475 },
476 {
477 input: "<C-S-A-->",
478 want: Sequence{{Sym: "-", Mods: ModCtrl | ModShift | ModAlt}},
479 wantString: "<C-S-A-->",
480 },
481 }
482 for _, tt := range tests {
483 t.Run(tt.input, func(t *testing.T) {
484 first, err := ParseBinding(tt.input)
485 if err != nil {
486 t.Fatalf("ParseBinding(%q) error = %v", tt.input, err)
487 }
488 if !first.Equal(tt.want) {
489 t.Fatalf("ParseBinding(%q) = %v, want %v", tt.input, first, tt.want)
490 }
491 if got := first.String(); got != tt.wantString {
492 t.Fatalf("ParseBinding(%q).String() = %q, want %q", tt.input, got, tt.wantString)
493 }
494 second, err := ParseBinding(first.String())
495 if err != nil {
496 t.Fatalf("ParseBinding(%q) from canonical %q error = %v", tt.input, first.String(), err)
497 }
498 if !first.Equal(second) {
499 t.Fatalf("round trip %q -> %q -> %v, want %v", tt.input, first.String(), second, first)
500 }
501 })
502 }
503}
504
505func TestParseBinding_CtrlPlusRoundTrip(t *testing.T) {
506 tests := []struct {
507 input string
508 want Sequence
509 wantString string
510 }{
511 {
512 input: "ctrl++",
513 want: Sequence{{Sym: "+", Mods: ModCtrl}},
514 wantString: "<C-+>",
515 },
516 {
517 input: "<C-+>",
518 want: Sequence{{Sym: "+", Mods: ModCtrl}},
519 wantString: "<C-+>",
520 },
521 {
522 input: "<C-S-+>",
523 want: Sequence{{Sym: "+", Mods: ModCtrl | ModShift}},
524 wantString: "<C-S-+>",
525 },
526 {
527 input: "<C-A-+>",
528 want: Sequence{{Sym: "+", Mods: ModCtrl | ModAlt}},
529 wantString: "<C-A-+>",
530 },
531 {
532 input: "<C-S-A-+>",
533 want: Sequence{{Sym: "+", Mods: ModCtrl | ModShift | ModAlt}},
534 wantString: "<C-S-A-+>",
535 },
536 }
537 for _, tt := range tests {
538 t.Run(tt.input, func(t *testing.T) {
539 first, err := ParseBinding(tt.input)
540 if err != nil {
541 t.Fatalf("ParseBinding(%q) error = %v", tt.input, err)
542 }
543 if !first.Equal(tt.want) {
544 t.Fatalf("ParseBinding(%q) = %v, want %v", tt.input, first, tt.want)
545 }
546 if got := first.String(); got != tt.wantString {
547 t.Fatalf("ParseBinding(%q).String() = %q, want %q", tt.input, got, tt.wantString)
548 }
549 second, err := ParseBinding(first.String())
550 if err != nil {
551 t.Fatalf("ParseBinding(%q) from canonical %q error = %v", tt.input, first.String(), err)
552 }
553 if !first.Equal(second) {
554 t.Fatalf("round trip %q -> %q -> %v, want %v", tt.input, first.String(), second, first)
555 }
556 })
557 }
558}
559
560func TestParseBinding_MultiModifierRoundTrip(t *testing.T) {
561 input := "<C-S-a>"
562 first, err := ParseBinding(input)
563 if err != nil {
564 t.Fatalf("ParseBinding(%q) error = %v", input, err)
565 }
566 want := Sequence{{Sym: "a", Mods: ModCtrl | ModShift}}
567 if !first.Equal(want) {
568 t.Fatalf("ParseBinding(%q) = %v, want %v", input, first, want)
569 }
570
571 canonical := first.String()
572 if canonical != "<C-S-a>" {
573 t.Fatalf("String() = %q, want %q", canonical, "<C-S-a>")
574 }
575
576 second, err := ParseBinding(canonical)
577 if err != nil {
578 t.Fatalf("ParseBinding(%q) error = %v", canonical, err)
579 }
580 if !first.Equal(second) {
581 t.Fatalf("round trip %q -> %q -> %v, want %v", input, canonical, second, first)
582 }
583}
584
585func TestParseBinding_RoundTrip(t *testing.T) {
586 inputs := []string{
587 "j",
588 "shift+j",
589 "ctrl++",
590 "ctrl+-",
591 "<C-->",
592 "<C-S-->",
593 "<C-A-->",
594 "]]",
595 "yah",
596 "gO",
597 "<C-d>",
598 "<CR>",
599 "<Esc>",
600 "<Space>",
601 "<Tab>",
602 "<BackSpace>",
603 "<Delete>",
604 "<Left>",
605 "<Right>",
606 "<Up>",
607 "<Down>",
608 "<Home>",
609 "<End>",
610 "<PageUp>",
611 "<PageDown>",
612 "<C-Tab>",
613 "<S-BackSpace>",
614 "<A-Delete>",
615 "<C-Left>",
616 "<S-Home>",
617 "<A-PageUp>",
618 "<lt>",
619 "<gt>",
620 ">",
621 "ctrl+>",
622 "<C-gt>",
623 "<C-d>j",
624 "<Escape>j",
625 "<C-S-a>",
626 "<S-c>-d",
627 "C-d",
628 "S-j",
629 "A-k",
630 "Spacej",
631 "Escj",
632 "Tabx",
633 "enter",
634 "escape",
635 "return",
636 "esc",
637 }
638 for _, input := range inputs {
639 t.Run(input, func(t *testing.T) {
640 first, err := ParseBinding(input)
641 if err != nil {
642 t.Fatalf("ParseBinding(%q) error = %v", input, err)
643 }
644 canonical := first.String()
645 second, err := ParseBinding(canonical)
646 if err != nil {
647 t.Fatalf("ParseBinding(%q) from canonical %q error = %v", input, canonical, err)
648 }
649 if !first.Equal(second) {
650 t.Fatalf("round trip %q -> %q -> %v, want %v", input, canonical, second, first)
651 }
652 })
653 }
654}
655
656// TestParseBinding_DeterministicRoundTripProperty asserts ParseBinding(seq.String()) == seq
657// over a fixed representative set of printable, named, and modifier sequences.
658// The corpus is deterministic (no flaky randomness).
659func TestParseBinding_DeterministicRoundTripProperty(t *testing.T) {
660 corpus := []Sequence{
661 {{Sym: "j"}},
662 {{Sym: "j", Mods: ModShift}},
663 {{Sym: "]"}, {Sym: "]"}},
664 {{Sym: "y"}, {Sym: "a"}, {Sym: "h"}},
665 {{Sym: "g"}, {Sym: "o", Mods: ModShift}},
666 {{Sym: "d", Mods: ModCtrl}},
667 {{Sym: "a", Mods: ModCtrl | ModShift}},
668 {{Sym: "k", Mods: ModAlt}},
669 {{Sym: "CR"}},
670 {{Sym: "Esc"}},
671 {{Sym: "Space"}},
672 {{Sym: "Tab"}},
673 {{Sym: "BackSpace"}},
674 {{Sym: "Delete"}},
675 {{Sym: "Left"}},
676 {{Sym: "Right"}},
677 {{Sym: "Up"}},
678 {{Sym: "Down"}},
679 {{Sym: "Home"}},
680 {{Sym: "End"}},
681 {{Sym: "PageUp"}},
682 {{Sym: "PageDown"}},
683 {{Sym: "Tab", Mods: ModCtrl}},
684 {{Sym: "BackSpace", Mods: ModShift}},
685 {{Sym: "Delete", Mods: ModAlt}},
686 {{Sym: "<"}},
687 {{Sym: ">"}},
688 {{Sym: ">", Mods: ModCtrl}},
689 {{Sym: "+"}},
690 {{Sym: "+", Mods: ModCtrl}},
691 {{Sym: "+", Mods: ModCtrl | ModShift}},
692 {{Sym: "-", Mods: ModCtrl}},
693 {{Sym: "-", Mods: ModCtrl | ModShift}},
694 {{Sym: "-", Mods: ModCtrl | ModAlt}},
695 {{Sym: "!"}},
696 {{Sym: "@"}},
697 {{Sym: "#"}},
698 {{Sym: "Space"}, {Sym: "j"}},
699 {{Sym: "d", Mods: ModCtrl}, {Sym: "j"}},
700 {{Sym: "Esc"}, {Sym: "j"}},
701 // Literal printable run that formerly collided with greedy "Space" recognition.
702 {
703 {Sym: "s", Mods: ModShift},
704 {Sym: "p"},
705 {Sym: "a"},
706 {Sym: "c"},
707 {Sym: "e"},
708 {Sym: "j"},
709 },
710 {
711 {Sym: "e", Mods: ModShift},
712 {Sym: "s"},
713 {Sym: "c"},
714 {Sym: "j"},
715 },
716 {
717 {Sym: "t", Mods: ModShift},
718 {Sym: "a"},
719 {Sym: "b"},
720 {Sym: "x"},
721 },
722 // Bare modifier-like text must stay literal (no C-/S-/A- stealing).
723 {
724 {Sym: "c", Mods: ModShift},
725 {Sym: "-"},
726 {Sym: "d"},
727 },
728 // Representative multi-byte printable runes (accented BMP + emoji).
729 {{Sym: "é"}},
730 {{Sym: "é", Mods: ModCtrl}},
731 {{Sym: "ñ", Mods: ModAlt}},
732 {{Sym: "😀"}},
733 {{Sym: "😀", Mods: ModCtrl}},
734 }
735 for _, seq := range corpus {
736 canonical := seq.String()
737 t.Run(canonical, func(t *testing.T) {
738 got, err := ParseBinding(canonical)
739 if err != nil {
740 t.Fatalf("ParseBinding(%q) error = %v", canonical, err)
741 }
742 if !got.Equal(seq) {
743 t.Fatalf("ParseBinding(%q) = %v, want %v", canonical, got, seq)
744 }
745 })
746 }
747}
748
749// TestParseBinding_LegacyAtomCollisionRoundTrip ensures rune sequences whose
750// naive Key.String concatenation would equal a legacy atom are emitted with
751// enough angle-literal tokens to round-trip without becoming CR/Esc.
752func TestParseBinding_LegacyAtomCollisionRoundTrip(t *testing.T) {
753 tests := []struct {
754 name string
755 seq Sequence
756 wantString string
757 }{
758 {
759 name: "Shift+e n t e r",
760 seq: Sequence{
761 {Sym: "e", Mods: ModShift},
762 {Sym: "n"}, {Sym: "t"}, {Sym: "e"}, {Sym: "r"},
763 },
764 wantString: "<S-e>nter",
765 },
766 {
767 name: "e n t e r",
768 seq: Sequence{
769 {Sym: "e"}, {Sym: "n"}, {Sym: "t"}, {Sym: "e"}, {Sym: "r"},
770 },
771 wantString: "<e>nter",
772 },
773 {
774 name: "Shift+r e t u r n",
775 seq: Sequence{
776 {Sym: "r", Mods: ModShift},
777 {Sym: "e"}, {Sym: "t"}, {Sym: "u"}, {Sym: "r"}, {Sym: "n"},
778 },
779 wantString: "<S-r>eturn",
780 },
781 {
782 name: "r e t u r n",
783 seq: Sequence{
784 {Sym: "r"}, {Sym: "e"}, {Sym: "t"}, {Sym: "u"}, {Sym: "r"}, {Sym: "n"},
785 },
786 wantString: "<r>eturn",
787 },
788 {
789 name: "Shift+e s c",
790 seq: Sequence{
791 {Sym: "e", Mods: ModShift},
792 {Sym: "s"}, {Sym: "c"},
793 },
794 wantString: "<S-e>sc",
795 },
796 {
797 name: "e s c",
798 seq: Sequence{{Sym: "e"}, {Sym: "s"}, {Sym: "c"}},
799 wantString: "<e>sc",
800 },
801 {
802 name: "Shift+e s c a p e",
803 seq: Sequence{
804 {Sym: "e", Mods: ModShift},
805 {Sym: "s"}, {Sym: "c"}, {Sym: "a"}, {Sym: "p"}, {Sym: "e"},
806 },
807 wantString: "<S-e>scape",
808 },
809 {
810 name: "e s c a p e",
811 seq: Sequence{
812 {Sym: "e"}, {Sym: "s"}, {Sym: "c"}, {Sym: "a"}, {Sym: "p"}, {Sym: "e"},
813 },
814 wantString: "<e>scape",
815 },
816 }
817 for _, tt := range tests {
818 t.Run(tt.name, func(t *testing.T) {
819 if got := tt.seq.String(); got != tt.wantString {
820 t.Fatalf("Sequence.String() = %q, want %q", got, tt.wantString)
821 }
822 got, err := ParseBinding(tt.wantString)
823 if err != nil {
824 t.Fatalf("ParseBinding(%q) error = %v", tt.wantString, err)
825 }
826 if !got.Equal(tt.seq) {
827 t.Fatalf("ParseBinding(%q) = %v, want %v", tt.wantString, got, tt.seq)
828 }
829 if again := got.String(); again != tt.wantString {
830 t.Fatalf("canonical unstable %q -> %q", tt.wantString, again)
831 }
832 })
833 }
834}
835
836// TestParseBinding_LegacyChordCollisionRoundTrip ensures a rune sequence that
837// would spell a legacy chord via a literal '+' is emitted with <Plus> so it
838// cannot reparse as that chord.
839func TestParseBinding_LegacyChordCollisionRoundTrip(t *testing.T) {
840 tests := []struct {
841 name string
842 seq Sequence
843 wantString string
844 }{
845 {
846 name: "ctrl + d runes",
847 seq: Sequence{
848 {Sym: "c"}, {Sym: "t"}, {Sym: "r"}, {Sym: "l"},
849 {Sym: "+"},
850 {Sym: "d"},
851 },
852 wantString: "ctrl<Plus>d",
853 },
854 {
855 name: "Ctrl + d mixed case start",
856 seq: Sequence{
857 {Sym: "c", Mods: ModShift},
858 {Sym: "t"}, {Sym: "r"}, {Sym: "l"},
859 {Sym: "+"},
860 {Sym: "d"},
861 },
862 wantString: "Ctrl<Plus>d",
863 },
864 {
865 name: "control + j runes",
866 seq: Sequence{
867 {Sym: "c"}, {Sym: "o"}, {Sym: "n"}, {Sym: "t"}, {Sym: "r"}, {Sym: "o"}, {Sym: "l"},
868 {Sym: "+"},
869 {Sym: "j"},
870 },
871 wantString: "control<Plus>j",
872 },
873 {
874 name: "shift + j runes",
875 seq: Sequence{
876 {Sym: "s"}, {Sym: "h"}, {Sym: "i"}, {Sym: "f"}, {Sym: "t"},
877 {Sym: "+"},
878 {Sym: "j"},
879 },
880 wantString: "shift<Plus>j",
881 },
882 {
883 name: "alt + k runes",
884 seq: Sequence{
885 {Sym: "a"}, {Sym: "l"}, {Sym: "t"},
886 {Sym: "+"},
887 {Sym: "k"},
888 },
889 wantString: "alt<Plus>k",
890 },
891 }
892 for _, tt := range tests {
893 t.Run(tt.name, func(t *testing.T) {
894 if got := tt.seq.String(); got != tt.wantString {
895 t.Fatalf("Sequence.String() = %q, want %q", got, tt.wantString)
896 }
897 got, err := ParseBinding(tt.wantString)
898 if err != nil {
899 t.Fatalf("ParseBinding(%q) error = %v", tt.wantString, err)
900 }
901 if !got.Equal(tt.seq) {
902 t.Fatalf("ParseBinding(%q) = %v, want %v", tt.wantString, got, tt.seq)
903 }
904 })
905 }
906}
907
908// TestParseBinding_LegacyAtomsAndChordsStillAccepted preserves exact raw
909// legacy config inputs as named atoms / chords.
910func TestParseBinding_LegacyAtomsAndChordsStillAccepted(t *testing.T) {
911 tests := []struct {
912 input string
913 want Sequence
914 }{
915 {input: "enter", want: Sequence{{Sym: "CR"}}},
916 {input: "return", want: Sequence{{Sym: "CR"}}},
917 {input: "Enter", want: Sequence{{Sym: "CR"}}},
918 {input: "Return", want: Sequence{{Sym: "CR"}}},
919 {input: "ENTER", want: Sequence{{Sym: "CR"}}},
920 {input: "escape", want: Sequence{{Sym: "Esc"}}},
921 {input: "esc", want: Sequence{{Sym: "Esc"}}},
922 {input: "Escape", want: Sequence{{Sym: "Esc"}}},
923 {input: "Esc", want: Sequence{{Sym: "Esc"}}},
924 {input: "ESC", want: Sequence{{Sym: "Esc"}}},
925 {input: "ctrl+d", want: Sequence{{Sym: "d", Mods: ModCtrl}}},
926 {input: "control+j", want: Sequence{{Sym: "j", Mods: ModCtrl}}},
927 {input: "shift+j", want: Sequence{{Sym: "j", Mods: ModShift}}},
928 {input: "alt+k", want: Sequence{{Sym: "k", Mods: ModAlt}}},
929 }
930 for _, tt := range tests {
931 t.Run(tt.input, func(t *testing.T) {
932 got, err := ParseBinding(tt.input)
933 if err != nil {
934 t.Fatalf("ParseBinding(%q) error = %v", tt.input, err)
935 }
936 if !got.Equal(tt.want) {
937 t.Fatalf("ParseBinding(%q) = %v, want %v", tt.input, got, tt.want)
938 }
939 })
940 }
941}
942
943// TestParseBinding_OneCharAngleLiteral accepts <e>/<S-e> while rejecting <Nope>.
944func TestParseBinding_OneCharAngleLiteral(t *testing.T) {
945 got, err := ParseBinding("<e>")
946 if err != nil {
947 t.Fatalf("ParseBinding(<e>) error = %v", err)
948 }
949 if want := (Sequence{{Sym: "e"}}); !got.Equal(want) {
950 t.Fatalf("ParseBinding(<e>) = %v, want %v", got, want)
951 }
952 got, err = ParseBinding("<S-e>")
953 if err != nil {
954 t.Fatalf("ParseBinding(<S-e>) error = %v", err)
955 }
956 if want := (Sequence{{Sym: "e", Mods: ModShift}}); !got.Equal(want) {
957 t.Fatalf("ParseBinding(<S-e>) = %v, want %v", got, want)
958 }
959 if _, err := ParseBinding("<Nope>"); !errors.Is(err, ErrBadBinding) {
960 t.Fatalf("ParseBinding(<Nope>) error = %v, want %v", err, ErrBadBinding)
961 }
962}
963
964// TestParseBinding_CtrlEAcuteRoundTrip covers the runtime Unicode canonical
965// invariant: ctrl+é / <C-é> parse to Key{Sym:"é", ModCtrl} and String
966// re-parses identically.
967func TestParseBinding_CtrlEAcuteRoundTrip(t *testing.T) {
968 want := Sequence{{Sym: "é", Mods: ModCtrl}}
969 for _, input := range []string{"ctrl+é", "<C-é>"} {
970 t.Run(input, func(t *testing.T) {
971 got, err := ParseBinding(input)
972 if err != nil {
973 t.Fatalf("ParseBinding(%q) error = %v", input, err)
974 }
975 if !got.Equal(want) {
976 t.Fatalf("ParseBinding(%q) = %v, want %v", input, got, want)
977 }
978 canonical := got.String()
979 if canonical != "<C-é>" {
980 t.Fatalf("String() = %q, want %q", canonical, "<C-é>")
981 }
982 again, err := ParseBinding(canonical)
983 if err != nil {
984 t.Fatalf("ParseBinding(%q) error = %v", canonical, err)
985 }
986 if !again.Equal(want) {
987 t.Fatalf("round trip %q -> %q -> %v, want %v", input, canonical, again, want)
988 }
989 })
990 }
991}
992
993// TestParseBinding_ReplacementCharRoundTrip ensures valid printable U+FFFD is not
994// rejected by the single-rune token path (r == utf8.RuneError is U+FFFD itself).
995func TestParseBinding_ReplacementCharRoundTrip(t *testing.T) {
996 const repl = "\uFFFD"
997 want := Sequence{{Sym: repl, Mods: ModCtrl}}
998 for _, input := range []string{"ctrl+" + repl, "<C-" + repl + ">"} {
999 t.Run(input, func(t *testing.T) {
1000 got, err := ParseBinding(input)
1001 if err != nil {
1002 t.Fatalf("ParseBinding(%q) error = %v", input, err)
1003 }
1004 if !got.Equal(want) {
1005 t.Fatalf("ParseBinding(%q) = %v, want %v", input, got, want)
1006 }
1007 canonical := got.String()
1008 wantCanonical := "<C-" + repl + ">"
1009 if canonical != wantCanonical {
1010 t.Fatalf("String() = %q, want %q", canonical, wantCanonical)
1011 }
1012 again, err := ParseBinding(canonical)
1013 if err != nil {
1014 t.Fatalf("ParseBinding(%q) error = %v", canonical, err)
1015 }
1016 if !again.Equal(want) {
1017 t.Fatalf("round trip %q -> %q -> %v, want %v", input, canonical, again, want)
1018 }
1019 })
1020 }
1021 modifiedWant := Sequence{{Sym: repl}}
1022 got, err := ParseBinding("<" + repl + ">")
1023 if err != nil {
1024 t.Fatalf("ParseBinding angle literal error = %v", err)
1025 }
1026 if !got.Equal(modifiedWant) {
1027 t.Fatalf("ParseBinding(<U+FFFD>) = %v, want %v", got, modifiedWant)
1028 }
1029}
1030
1031// TestParseBinding_LegacyCollisionModel systematically covers every exact
1032// legacy atom alias, modifier+plus chord spellings, case variants, and
1033// adjacent non-colliding sequences while retaining round-trip.
1034func TestParseBinding_LegacyCollisionModel(t *testing.T) {
1035 type atomCase struct {
1036 alias string
1037 seq Sequence
1038 }
1039 atoms := []atomCase{
1040 {alias: "enter", seq: Sequence{{Sym: "e"}, {Sym: "n"}, {Sym: "t"}, {Sym: "e"}, {Sym: "r"}}},
1041 {alias: "Enter", seq: Sequence{{Sym: "e", Mods: ModShift}, {Sym: "n"}, {Sym: "t"}, {Sym: "e"}, {Sym: "r"}}},
1042 {alias: "ENTER", seq: Sequence{
1043 {Sym: "e", Mods: ModShift}, {Sym: "n", Mods: ModShift}, {Sym: "t", Mods: ModShift},
1044 {Sym: "e", Mods: ModShift}, {Sym: "r", Mods: ModShift},
1045 }},
1046 {alias: "return", seq: Sequence{{Sym: "r"}, {Sym: "e"}, {Sym: "t"}, {Sym: "u"}, {Sym: "r"}, {Sym: "n"}}},
1047 {alias: "Return", seq: Sequence{{Sym: "r", Mods: ModShift}, {Sym: "e"}, {Sym: "t"}, {Sym: "u"}, {Sym: "r"}, {Sym: "n"}}},
1048 {alias: "escape", seq: Sequence{{Sym: "e"}, {Sym: "s"}, {Sym: "c"}, {Sym: "a"}, {Sym: "p"}, {Sym: "e"}}},
1049 {alias: "Escape", seq: Sequence{{Sym: "e", Mods: ModShift}, {Sym: "s"}, {Sym: "c"}, {Sym: "a"}, {Sym: "p"}, {Sym: "e"}}},
1050 {alias: "esc", seq: Sequence{{Sym: "e"}, {Sym: "s"}, {Sym: "c"}}},
1051 {alias: "Esc", seq: Sequence{{Sym: "e", Mods: ModShift}, {Sym: "s"}, {Sym: "c"}}},
1052 {alias: "ESC", seq: Sequence{{Sym: "e", Mods: ModShift}, {Sym: "s", Mods: ModShift}, {Sym: "c", Mods: ModShift}}},
1053 }
1054 for _, tt := range atoms {
1055 t.Run("atom/"+tt.alias, func(t *testing.T) {
1056 canonical := tt.seq.String()
1057 if strings.EqualFold(canonical, tt.alias) {
1058 t.Fatalf("Sequence.String() still collides with legacy atom %q", tt.alias)
1059 }
1060 got, err := ParseBinding(canonical)
1061 if err != nil {
1062 t.Fatalf("ParseBinding(%q) error = %v", canonical, err)
1063 }
1064 if !got.Equal(tt.seq) {
1065 t.Fatalf("ParseBinding(%q) = %v, want %v", canonical, got, tt.seq)
1066 }
1067 raw, err := ParseBinding(tt.alias)
1068 if err != nil {
1069 t.Fatalf("ParseBinding(%q) error = %v", tt.alias, err)
1070 }
1071 if raw.Equal(tt.seq) {
1072 t.Fatalf("legacy atom %q must not equal rune sequence", tt.alias)
1073 }
1074 })
1075 }
1076
1077 chordSpellings := []struct {
1078 legacy string
1079 seq Sequence
1080 }{
1081 {
1082 legacy: "ctrl+d",
1083 seq: Sequence{
1084 {Sym: "c"}, {Sym: "t"}, {Sym: "r"}, {Sym: "l"}, {Sym: "+"}, {Sym: "d"},
1085 },
1086 },
1087 {
1088 legacy: "control+j",
1089 seq: Sequence{
1090 {Sym: "c"}, {Sym: "o"}, {Sym: "n"}, {Sym: "t"}, {Sym: "r"}, {Sym: "o"}, {Sym: "l"},
1091 {Sym: "+"}, {Sym: "j"},
1092 },
1093 },
1094 {
1095 legacy: "shift+a",
1096 seq: Sequence{
1097 {Sym: "s"}, {Sym: "h"}, {Sym: "i"}, {Sym: "f"}, {Sym: "t"},
1098 {Sym: "+"}, {Sym: "a"},
1099 },
1100 },
1101 {
1102 legacy: "alt+b",
1103 seq: Sequence{
1104 {Sym: "a"}, {Sym: "l"}, {Sym: "t"}, {Sym: "+"}, {Sym: "b"},
1105 },
1106 },
1107 }
1108 for _, tt := range chordSpellings {
1109 t.Run("chord/"+tt.legacy, func(t *testing.T) {
1110 canonical := tt.seq.String()
1111 if canonical == tt.legacy {
1112 t.Fatalf("Sequence.String() still equals legacy chord %q", tt.legacy)
1113 }
1114 got, err := ParseBinding(canonical)
1115 if err != nil {
1116 t.Fatalf("ParseBinding(%q) error = %v", canonical, err)
1117 }
1118 if !got.Equal(tt.seq) {
1119 t.Fatalf("ParseBinding(%q) = %v, want %v", canonical, got, tt.seq)
1120 }
1121 chord, err := ParseBinding(tt.legacy)
1122 if err != nil {
1123 t.Fatalf("ParseBinding(%q) error = %v", tt.legacy, err)
1124 }
1125 if chord.Equal(tt.seq) {
1126 t.Fatalf("legacy chord %q must not equal rune sequence", tt.legacy)
1127 }
1128 })
1129 }
1130
1131 // Adjacent sequences that share prefixes but are not exact atoms.
1132 adjacent := []Sequence{
1133 {{Sym: "e"}, {Sym: "n"}, {Sym: "t"}, {Sym: "e"}, {Sym: "r"}, {Sym: "j"}},
1134 {{Sym: "e", Mods: ModShift}, {Sym: "s"}, {Sym: "c"}, {Sym: "j"}},
1135 {{Sym: "c"}, {Sym: "t"}, {Sym: "r"}, {Sym: "l"}, {Sym: "+"}, {Sym: "d"}, {Sym: "j"}},
1136 }
1137 for _, seq := range adjacent {
1138 canonical := seq.String()
1139 t.Run("adjacent/"+canonical, func(t *testing.T) {
1140 got, err := ParseBinding(canonical)
1141 if err != nil {
1142 t.Fatalf("ParseBinding(%q) error = %v", canonical, err)
1143 }
1144 if !got.Equal(seq) {
1145 t.Fatalf("ParseBinding(%q) = %v, want %v", canonical, got, seq)
1146 }
1147 })
1148 }
1149}
1150
1151// TestParseBinding_ExhaustiveCanonicalModelRoundTrip covers the full legal Key
1152// surface that Sequence.String can emit: letters × modifier combos, printable
1153// ASCII punctuation in runtime-canonical modifier states, named symbols ×
1154// modifier combos, and adjacent pairs that can spell modifier/named prefixes.
1155func TestParseBinding_ExhaustiveCanonicalModelRoundTrip(t *testing.T) {
1156 allMods := []Mods{
1157 0,
1158 ModCtrl,
1159 ModShift,
1160 ModAlt,
1161 ModCtrl | ModShift,
1162 ModCtrl | ModAlt,
1163 ModShift | ModAlt,
1164 ModCtrl | ModShift | ModAlt,
1165 }
1166
1167 var corpus []Sequence
1168
1169 for c := 'a'; c <= 'z'; c++ {
1170 sym := string(c)
1171 for _, mods := range allMods {
1172 corpus = append(corpus, Sequence{{Sym: sym, Mods: mods}})
1173 }
1174 }
1175
1176 punctuation := []string{
1177 "!", `"`, "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/",
1178 ":", ";", "<", "=", ">", "?", "@", "[", `\`, "]", "^", "_", "`", "{", "|", "}", "~",
1179 }
1180 for _, sym := range punctuation {
1181 for _, mods := range allMods {
1182 // Unmodified punctuation is always legal; modified forms are emitted
1183 // as angle keys and must round-trip.
1184 corpus = append(corpus, Sequence{{Sym: sym, Mods: mods}})
1185 }
1186 }
1187
1188 named := []string{
1189 "CR", "Esc", "Space", "Tab", "BackSpace", "Delete",
1190 "Left", "Right", "Up", "Down", "Home", "End", "PageUp", "PageDown",
1191 }
1192 for _, sym := range named {
1193 for _, mods := range allMods {
1194 corpus = append(corpus, Sequence{{Sym: sym, Mods: mods}})
1195 }
1196 }
1197
1198 // Representative adjacent pairs that can spell modifier/named prefixes,
1199 // including exact legacy atom/chord collision spellings.
1200 adjacentPairs := []Sequence{
1201 {{Sym: "c", Mods: ModShift}, {Sym: "-"}, {Sym: "d"}},
1202 {{Sym: "s", Mods: ModShift}, {Sym: "-"}, {Sym: "j"}},
1203 {{Sym: "a", Mods: ModShift}, {Sym: "-"}, {Sym: "k"}},
1204 {{Sym: "c", Mods: ModShift}, {Sym: "-"}, {Sym: "s", Mods: ModShift}, {Sym: "-"}, {Sym: "a"}},
1205 {{Sym: "s", Mods: ModShift}, {Sym: "p"}, {Sym: "a"}, {Sym: "c"}, {Sym: "e"}, {Sym: "j"}},
1206 {{Sym: "e", Mods: ModShift}, {Sym: "s"}, {Sym: "c"}, {Sym: "j"}},
1207 {{Sym: "t", Mods: ModShift}, {Sym: "a"}, {Sym: "b"}, {Sym: "x"}},
1208 {{Sym: "r", Mods: ModShift}, {Sym: "e"}, {Sym: "t"}, {Sym: "u"}, {Sym: "r"}, {Sym: "n"}, {Sym: "x"}},
1209 {{Sym: "e"}, {Sym: "n"}, {Sym: "t"}, {Sym: "e"}, {Sym: "r"}},
1210 {{Sym: "e", Mods: ModShift}, {Sym: "n"}, {Sym: "t"}, {Sym: "e"}, {Sym: "r"}},
1211 {{Sym: "e"}, {Sym: "s"}, {Sym: "c"}},
1212 {{Sym: "e", Mods: ModShift}, {Sym: "s"}, {Sym: "c"}, {Sym: "a"}, {Sym: "p"}, {Sym: "e"}},
1213 {{Sym: "r"}, {Sym: "e"}, {Sym: "t"}, {Sym: "u"}, {Sym: "r"}, {Sym: "n"}},
1214 {{Sym: "c"}, {Sym: "t"}, {Sym: "r"}, {Sym: "l"}, {Sym: "+"}, {Sym: "d"}},
1215 {{Sym: "d", Mods: ModCtrl}, {Sym: "j"}},
1216 {{Sym: ">"}, {Sym: "j"}},
1217 {{Sym: "<"}, {Sym: "j"}},
1218 {{Sym: "g"}, {Sym: "o", Mods: ModShift}},
1219 {{Sym: "]"}, {Sym: "]"}},
1220 }
1221 corpus = append(corpus, adjacentPairs...)
1222
1223 seen := make(map[string]struct{}, len(corpus))
1224 for _, seq := range corpus {
1225 canonical := seq.String()
1226 if _, ok := seen[canonical]; ok {
1227 continue
1228 }
1229 seen[canonical] = struct{}{}
1230 t.Run(canonical, func(t *testing.T) {
1231 got, err := ParseBinding(canonical)
1232 if err != nil {
1233 t.Fatalf("ParseBinding(%q) error = %v", canonical, err)
1234 }
1235 if !got.Equal(seq) {
1236 t.Fatalf("ParseBinding(%q) = %v, want %v", canonical, got, seq)
1237 }
1238 if again := got.String(); again != canonical {
1239 t.Fatalf("canonical unstable %q -> %q", canonical, again)
1240 }
1241 })
1242 }
1243}