lattice_host/keymap_visual.rs
1//! Visual-mode binding registration + drift-test helpers.
2//!
3//! Audit slice 8.e. The second mode migrated off `input::translate`'s
4//! hand-rolled match table; follows the [slice 8.d
5//! template][crate::keymap_replace] (`register_<mode>_bindings` +
6//! `dispatch_<mode>` + drift test against a frozen reference body
7//! of the legacy translator).
8//!
9//! ## Surface
10//!
11//! Visual mode is the same chord table for charwise / linewise /
12//! blockwise -- the kind only changes how `Range::Selection`
13//! resolves at operator-dispatch time (see
14//! `lattice-grammar::dispatcher` §5.2.3). Two block-only
15//! exceptions land before the trie lookup:
16//!
17//! - `I` -> [`Action::EnterBlockVisualInsert`] (blockwise only)
18//! - `A` -> [`Action::EnterBlockVisualAppend`] (blockwise only)
19//!
20//! These are pre-dispatch overrides rather than a separate
21//! `BindingMode::VisualBlock` -- the architecture's eventual model
22//! is a minor-mode layer pushed at blockwise entry / popped at
23//! exit (see `docs/dev/architecture/keymap-architecture.md` §5.3); slice 8.e keeps
24//! the surgical pre-check until that layer machinery lands. The
25//! drift test below pins the kind branch so a future graduation
26//! to `push_layer` is mechanical.
27//!
28//! Common-to-all-kinds bindings registered by
29//! [`register_visual_bindings`]:
30//!
31//! - **Exits**: `<Esc>` / `v` / `V` -> `ExitVisual`.
32//! - **Motions** (extend the selection): `h` / `<Left>` /
33//! `j` / `<Down>` / `k` / `<Up>` / `l` / `<Right>` /
34//! `0` / `<Home>` / `$` / `<End>` / `^` / `w` / `b` / `e` /
35//! `W` / `B` / `E` / `}` / `{` / `)` / `(` / `G`. Each binds
36//! to `CommandInvocation::of(motion.0)` -- the
37//! non-`legacy_action` path; the dispatcher returns
38//! `Action::Invoke(command.command.clone())`.
39//! - **Operators on selection**: an operator acts on the active
40//! selection BY DESIGN, so its Visual binding is generated once at
41//! operator registration, NOT hand-listed here. Every operator
42//! ([`crate::keymap_normal::register_operator_bindings`] -- builtin `d` / `c`
43//! / `y` / `>` / `<`, case `gU` / `gu` / `g~`, contributed `zn`, ...)
44//! binds its trigger chord to
45//! `CommandInvocation::of(op.0).with_range(Range::Selection)`; the
46//! operator dispatcher's range walker resolves `Range::Selection`
47//! against the active visual selection. The ONLY operator entries
48//! that remain in [`register_visual_bindings`] are the two
49//! Visual-only ALIASES `x` -> delete and `s` -> change (in Normal
50//! `x` / `s` are different commands, so they are not the operators'
51//! trigger chord and cannot come from the registration).
52//! - **Text objects** (set the selection to the object's span):
53//! `i<obj>` / `a<obj>` for every object in the SHARED
54//! [`crate::keymap_normal::text_object_rows`] table -- `viw`,
55//! `vaw`, `vi{`, `vap`, `vaf` (function), `vac` (class),
56//! `vaC` (comment), ... These are TWO-key chords resolved by
57//! the same partial-chord machinery Normal mode uses (`i` / `a`
58//! absorbs into the host's `partial_chord`, the object char
59//! resolves the pair); see [`dispatch_visual`]. Each binds to a
60//! bare `CommandInvocation::of(tobj.0)`, which the grammar's
61//! `execute_text_object` turns into an `Effect::SelectionChange`
62//! spanning the object. There is ZERO per-object code: the
63//! binder iterates the shared table, so Visual and the Normal
64//! operator-pending resolver can never drift.
65//!
66//! Slice 8.e's net win: every motion / operator binding moves
67//! off the `legacy_action` bridge and onto a real
68//! `CommandInvocation`. Only the three `ExitVisual` shapes plus
69//! the two block-only `Enter*` paths still carry a `legacy_action`
70//! -- they don't have a `CommandInvocation` peer today.
71
72use lattice_grammar::SourceLocation;
73use lattice_grammar::VisualKind;
74use lattice_grammar::builtins::Builtins;
75use lattice_grammar::command::CommandInvocation;
76use lattice_syntax::SyntaxTextObjectIds;
77
78use crate::action::Action;
79use crate::actions::ActionIds;
80use crate::chord::{KeyChord, KeyMods, SpecialKey};
81use crate::keymap::BindingMode;
82use crate::keymap_registry::KeymapHandle;
83use crate::keymap_trie::{ChordPattern, KeymapLayer, LookupResult};
84
85/// Register every chord the legacy `input::translate_visual`
86/// recognised into the supplied handle's `Builtin` layer under
87/// `BindingMode::Visual`. Called at App startup; the registration
88/// captures `builtins`'s motion / operator ids by value, so the
89/// resulting `BoundCommand`s never re-resolve at lookup time.
90///
91/// Sources are tagged at this file + line so `:describe-key`
92/// shows e.g.
93/// `h -> motion:char-left (builtin, keymap_visual.rs:NN)`.
94pub fn register_visual_bindings(
95 handle: &KeymapHandle,
96 builtins: &Builtins,
97 actions: &ActionIds,
98 syntax_textobjects: &SyntaxTextObjectIds,
99) {
100 let layer = KeymapLayer::Builtin;
101 let mode = BindingMode::Visual;
102
103 // Exits: <Esc>, v, V. Promoted to typed `ExitVisual` action
104 // in slice 8.i.1.h.
105 handle.bind(
106 layer,
107 mode,
108 &[ChordPattern::Literal(KeyChord::special(SpecialKey::Esc))],
109 CommandInvocation::of(actions.exit_visual),
110 source(),
111 );
112 handle.bind(
113 layer,
114 mode,
115 &[ChordPattern::Literal(KeyChord::char('v'))],
116 CommandInvocation::of(actions.exit_visual),
117 source(),
118 );
119 handle.bind(
120 layer,
121 mode,
122 &[ChordPattern::Literal(KeyChord::char('V'))],
123 CommandInvocation::of(actions.exit_visual),
124 source(),
125 );
126
127 // `:` -- enter the command line from Visual (vim's `:'<,'>`). The
128 // `EnterCommandLine` handler is Visual-aware: it captures the
129 // selection into `last_visual` (so `'<` / `'>` and `Range::Selection`
130 // resolve to it) and prefills the cmdline with `'<,'>`. Without this
131 // binding `:` was unbound in Visual → `dispatch_visual` returned
132 // `Action::None`, so you could not invoke ANY command (`:s`,
133 // `:narrow`, `:w`, …) from a selection.
134 handle.bind(
135 layer,
136 mode,
137 &[ChordPattern::Literal(KeyChord::char(':'))],
138 CommandInvocation::of(actions.enter_command_line),
139 source(),
140 );
141
142 // `o` -- swap the cursor to the other end of the selection so the
143 // next motion / text object alters that end (vim's Visual `o`).
144 handle.bind(
145 layer,
146 mode,
147 &[ChordPattern::Literal(KeyChord::char('o'))],
148 CommandInvocation::of(actions.swap_visual_ends),
149 source(),
150 );
151
152 // VM.1 (2026-09-15): motions are NOT listed here any more, neither the
153 // builtin ones nor TSM.4's sixteen structural ones. They are DERIVED from
154 // each binding's command kind: since VM.4 the keymap mirrors every
155 // `CommandKind::Motion` Normal binding into Visual at the write
156 // (keymap-architecture.md §15), and `keymap_normal::expand_grammar_rows`
157 // adds the operator-pending peers.
158 //
159 // The list that used to live here was a re-registration of
160 // `keymap_normal::motion_rows`, and its doc comment claimed to be a single
161 // source of truth for three surfaces. It was not: `gg`, `f` / `F` / `t` /
162 // `T`, `<C-d>` / `<C-u>` and `<PageUp>` / `<PageDown>` all entered the
163 // Normal binder by other doors and so were dead in Visual. A second copy
164 // of a list cannot notice what the first one never had, and a plugin motion
165 // had no route into either copy — org's `[[` moved the cursor in Normal and
166 // did nothing here.
167 //
168 // What remains below is what is genuinely Visual-SPECIFIC: chords that mean
169 // something different here than in Normal.
170
171 // Visual-only operator ALIASES. The canonical operator trigger
172 // chords (`d` / `c` / `y` / `>` / `<`, case `gU` / `gu` / `g~`,
173 // contributed `zn`, ...) are deliberately NOT listed here: every
174 // operator gets its Visual selection-bind intrinsically from
175 // [`crate::keymap_normal::register_operator_bindings`] -- an operator acts on
176 // the active selection BY DESIGN, so the binding is generated once
177 // per operator at registration, uniformly across builtin and
178 // contributed operators. What remains here are the two chords that
179 // are operators ONLY in Visual: in Normal `x` is delete-char and `s`
180 // is substitute, so they are not the delete / change operators'
181 // trigger chord and cannot be derived from the operator registration.
182 // `Range::Selection` resolves at dispatch time to the active visual
183 // region.
184 let operator_aliases: &[(ChordPattern, lattice_grammar::registry::OperatorId)] = &[
185 (literal(KeyChord::char('x')), builtins.delete),
186 (literal(KeyChord::char('s')), builtins.change),
187 ];
188 for (chord, op) in operator_aliases {
189 handle.bind(
190 layer,
191 mode,
192 std::slice::from_ref(chord),
193 CommandInvocation::of(op.0).with_range(lattice_grammar::Range::Selection),
194 source(),
195 );
196 }
197
198 // `r<X>` -- replace every selected char with X (vim's Visual `r`).
199 // A two-key wildcard: `[r]` alone returns Partial, so `dispatch_visual`
200 // absorbs it into `partial_chord`; `[r, CharLiteral]` then folds the
201 // captured char into the operator's `Args::Char` via
202 // `action_from_bound_with_capture`. `Range::Selection` resolves to the
203 // active region at dispatch time; the `replace-char` operator overwrites
204 // every non-newline char in it and the host auto-exits Visual after the
205 // operator, exactly as `d` / `c` / `y` do. Unlike the Normal-mode `r`
206 // binding there is no target motion, so the captured char routes
207 // straight to the operator's args.
208 handle.bind(
209 layer,
210 mode,
211 &[literal(KeyChord::char('r')), ChordPattern::CharLiteral],
212 CommandInvocation::of(builtins.replace_char.0)
213 .with_range(lattice_grammar::Range::Selection),
214 source(),
215 );
216
217 // Text objects: `i<obj>` (inner) / `a<obj>` (around) set the
218 // selection to the object's span -- `viw`, `vaf`, `vaC`, `vi{`,
219 // ... Rows come from the SHARED
220 // [`crate::keymap_normal::text_object_rows`] table, the exact
221 // same table the Normal-mode operator-pending resolver consumes,
222 // so `viw` / `vaf` and `diw` / `daf` can never drift and there is
223 // ZERO per-object code here. A bare text-object invocation
224 // dispatches through the grammar's `execute_text_object`, which
225 // returns `Effect::SelectionChange` spanning the object; the host
226 // adopts both endpoints (see the `Effect::SelectionChange` arm in
227 // `dispatch.rs`). The two-key chord (`[i, w]`) resolves via the
228 // same partial-chord machinery Normal uses -- see `dispatch_visual`.
229 for (chord_aliases, inner_id, around_id) in
230 crate::keymap_normal::text_object_rows(builtins, syntax_textobjects)
231 {
232 for (prefix_char, tobj) in [('i', inner_id), ('a', around_id)] {
233 for chord in &chord_aliases {
234 handle.bind(
235 layer,
236 mode,
237 &[literal(KeyChord::char(prefix_char)), chord.clone()],
238 CommandInvocation::of(tobj.0),
239 source(),
240 );
241 }
242 }
243 }
244}
245
246fn literal(chord: KeyChord) -> ChordPattern {
247 ChordPattern::Literal(chord)
248}
249
250fn source() -> SourceLocation {
251 // Per-row file + caller line would require a macro; the
252 // line-of-this-helper is fine for slice 8.e -- the motion /
253 // operator id in the bound command already disambiguates the
254 // entry to `:describe-key`. A row-precise capture lands when
255 // the catalog enumeration replaces these inline calls (slice
256 // 8.i).
257 SourceLocation::builtin_file(file!(), line!())
258}
259
260/// Strip SHIFT / ALT / SUPER for the Visual trie lookup -- the
261/// catalog binds bare chords only (SHIFT on bare letters is already
262/// folded into the case by `KeyChord::from_event`). CTRL is filtered
263/// by the caller before this runs. Same treatment as Normal /
264/// Replace mode -- legacy `translate_visual` matched on `event.code`
265/// alone after the CONTROL guard, so non-CONTROL modifiers are
266/// transparent.
267/// What [`lookup_visual_chord`] matched with, and the chord that produced it.
268struct VisualLookup {
269 result: LookupResult,
270 resolved: KeyChord,
271}
272
273/// Look `path` up RAW first, falling back to the normalized form.
274///
275/// OS.0d, and the third and last copy of the defect OS.0b found in Insert and
276/// OS.0c found in Normal. `normalize_for_visual_lookup` strips ALT and SUPER —
277/// and, unlike its two siblings, SHIFT as well — off every incoming chord
278/// before any lookup, so a mode declaring `binding-mode: visual` on
279/// `<M-Right>` or `<M-S-Right>` registers correctly and can never fire.
280///
281/// The extra SHIFT strip is why this needed its own helper rather than reusing
282/// Normal's: `<M-S-Right>` differs from `<M-Right>` only in SHIFT, so a
283/// normalize-first lookup collapses the two org verbs onto one. Raw-first
284/// separates them and the fallback still answers for every bare-chord binding
285/// the Visual catalog has always relied on — `V` then `d` is unaffected,
286/// because raw and normalized are the same chord there.
287fn lookup_visual_chord(
288 handle: &KeymapHandle,
289 path: &[KeyChord],
290 chord: KeyChord,
291 active_minor_modes: &[lattice_mode::ModeId],
292) -> VisualLookup {
293 let mut raw_path: Vec<KeyChord> = path.to_vec();
294 raw_path.push(chord);
295 let raw = handle.lookup_with_context(BindingMode::Visual, &raw_path, active_minor_modes);
296 if matches!(raw, LookupResult::Bound { .. } | LookupResult::Partial) {
297 return VisualLookup {
298 result: raw,
299 resolved: chord,
300 };
301 }
302 let normalized = normalize_for_visual_lookup(chord);
303 if normalized == chord {
304 return VisualLookup {
305 result: raw,
306 resolved: chord,
307 };
308 }
309 let mut normalized_path: Vec<KeyChord> = path.to_vec();
310 normalized_path.push(normalized);
311 VisualLookup {
312 result: handle.lookup_with_context(
313 BindingMode::Visual,
314 &normalized_path,
315 active_minor_modes,
316 ),
317 resolved: normalized,
318 }
319}
320
321fn normalize_for_visual_lookup(chord: KeyChord) -> KeyChord {
322 KeyChord {
323 key: chord.key,
324 mods: chord
325 .mods
326 .without(KeyMods::SHIFT)
327 .without(KeyMods::ALT)
328 .without(KeyMods::SUPER),
329 }
330}
331
332/// Dispatch a Visual-mode key event through the keymap registry.
333///
334/// 1. CONTROL-bearing key -> `Action::None`. (Legacy short-
335/// circuited `CONTROL` and only `CONTROL`.)
336/// 2. Mid-sequence (a text-object prefix `i` / `a` already in
337/// `partial_chord`): resolve `[partial_chord..., chord]` against
338/// the Visual catalog. This is the SAME partial-chord machinery
339/// Normal mode uses -- `viw` is `v` then the two-key chord
340/// `[i, w]`, `vaf` is `[a, f]`, etc. There is NO per-object code:
341/// every text object in [`crate::keymap_normal::text_object_rows`]
342/// works automatically, exactly as the user asked ("we shouldn't
343/// have to do any custom handling of any text objects").
344/// 3. Fresh keystroke. Blockwise overlay: `Char('I')` / `Char('A')`
345/// go to `EnterBlockVisualInsert` / `EnterBlockVisualAppend`
346/// before lookup. Charwise / linewise fall through.
347/// 4. Single-chord lookup. `Bound` -> `Invoke` (folding any wildcard
348/// capture); `Partial` -> `AbsorbPartialChord` (a bare `i` / `a`
349/// starts a text object -- absorb it so the next key resolves the
350/// pair); `Unbound` -> `Action::None`.
351///
352/// `partial_chord` is the host's running multi-key prefix (the same
353/// `Editor::partial_chord` the Normal path threads); it is empty on a
354/// fresh chord and holds the absorbed `[i]` / `[a]` mid-text-object.
355pub fn dispatch_visual(
356 handle: &KeymapHandle,
357 chord: &KeyChord,
358 kind: VisualKind,
359 partial_chord: &[KeyChord],
360 active_minor_modes: &[lattice_mode::ModeId],
361) -> Action {
362 // SN.3d.2: `<C-g>` toggles Visual → Select (reserved in both modes
363 // for the toggle; select-mode.md §4). Must precede the CONTROL
364 // short-circuit below — symmetric with `translate_select`, which
365 // handles `<C-g>` as a hardcoded mode-control chord on the Select
366 // side. `<C-g>` is otherwise unbound in Visual today.
367 if chord.mods.ctrl() && matches!(chord.key, crate::chord::KeyKind::Char('g')) {
368 return Action::ToggleVisualSelect;
369 }
370 // CG.1 (2026-08-07): the blanket `if chord.mods.ctrl() { return
371 // Action::None }` that used to sit here is GONE. It short-circuited
372 // every CONTROL chord before the trie was consulted, so a binding
373 // registered under `BindingMode::Visual` with a CTRL chord could
374 // never fire — the same defect PBH.3 fixed in Normal, where the
375 // count-digit guard made every `<C-digit>` chord unreachable.
376 //
377 // It was invisible because the Visual catalog registers no CTRL
378 // chords, so nothing in-tree noticed. Plugins can: `keymap_host.rs`
379 // maps WIT `Visual` / `Select` straight through, so a plugin binding
380 // `<C-x>` in Visual got silent nothing — an extensibility hole
381 // (paramount goal #2), not just a missing feature.
382 //
383 // The trie is authoritative now. `<C-g>` above stays a hardcoded
384 // arm because SN.3d's toggle is mode *control* (it changes which
385 // dispatcher runs), not a command lookup.
386
387 // Mid-sequence: a text-object prefix was absorbed last keystroke.
388 // Resolve the full path; the blockwise `I` / `A` overlay does not
389 // apply here (it is a fresh-chord-only shortcut).
390 if !partial_chord.is_empty() {
391 let found = lookup_visual_chord(handle, partial_chord, *chord, active_minor_modes);
392 return match found.result {
393 LookupResult::Bound { command, captured } => {
394 crate::keymap_normal::action_from_bound_with_capture(&command, &captured)
395 }
396 LookupResult::Partial => Action::AbsorbPartialChord(found.resolved),
397 LookupResult::Unbound => Action::None,
398 };
399 }
400
401 if matches!(kind, VisualKind::Blockwise) {
402 match chord.key {
403 crate::chord::KeyKind::Char('I') => return Action::EnterBlockVisualInsert,
404 crate::chord::KeyKind::Char('A') => return Action::EnterBlockVisualAppend,
405 _ => {}
406 }
407 }
408 let found = lookup_visual_chord(handle, &[], *chord, active_minor_modes);
409 let chord = found.resolved;
410 match found.result {
411 LookupResult::Bound { command, captured } => {
412 crate::keymap_normal::action_from_bound_with_capture(&command, &captured)
413 }
414 // A bare `i` / `a` is a text-object prefix: absorb it so the
415 // next key resolves `[i / a, obj]`. (Before text objects
416 // landed, Visual had no multi-key chords and this returned
417 // `Action::None`.)
418 LookupResult::Partial => Action::AbsorbPartialChord(chord),
419 LookupResult::Unbound => Action::None,
420 }
421}
422
423/// OS.0d regression tests: the Visual peer of `keymap_insert::os0b_tests` and
424/// `keymap_normal::os0c_tests`.
425///
426/// Visual's normalize strips SHIFT as well as ALT and SUPER, which is why it
427/// needed its own helper: `<M-S-Right>` differs from `<M-Right>` only in SHIFT,
428/// so normalizing first collapses two distinct org verbs onto one binding.
429#[cfg(test)]
430mod os0d_tests {
431 #![allow(clippy::unwrap_used, clippy::panic)]
432 use super::*;
433 use crate::chord::{KeyKind, SpecialKey};
434 use crate::keymap_trie::{ChordPattern, KeymapLayer};
435 use lattice_grammar::SourceLocation;
436 use lattice_grammar::command::CommandInvocation;
437 use lattice_mode::ModeId;
438 use lattice_protocol::ids::CommandId;
439
440 fn source() -> SourceLocation {
441 SourceLocation::builtin_file(file!(), line!())
442 }
443
444 fn lit(chord: KeyChord) -> ChordPattern {
445 ChordPattern::Literal(chord)
446 }
447
448 fn bound_command(result: &LookupResult) -> CommandId {
449 match result {
450 LookupResult::Bound { command, .. } => command.command.command,
451 other => panic!("expected Bound, got {other:?}"),
452 }
453 }
454
455 fn bind_visual(h: &KeymapHandle, mode: ModeId, chord: KeyChord, id: CommandId) {
456 h.bind(
457 KeymapLayer::MajorMode(mode),
458 BindingMode::Visual,
459 &[lit(chord)],
460 CommandInvocation::of(id),
461 source(),
462 );
463 }
464
465 /// An ALT-bearing Visual binding must reach the trie as pressed.
466 #[test]
467 fn an_alt_bearing_visual_binding_is_reachable() {
468 let h = KeymapHandle::new();
469 let mode = ModeId::new("os0d-alt-mode");
470 let id = CommandId::new(u64::MAX - 21);
471 let chord = KeyChord::new(KeyKind::Special(SpecialKey::Right), KeyMods::ALT);
472 bind_visual(&h, mode, chord, id);
473
474 let found = lookup_visual_chord(&h, &[], chord, &[mode]);
475 assert_eq!(bound_command(&found.result), id);
476 assert_eq!(found.resolved, chord);
477 }
478
479 /// The `&[mode]` on every lookup below is load-bearing, not boilerplate:
480 /// `bind_visual` binds into `KeymapLayer::MajorMode(mode)`, and since the
481 /// Visual dispatcher started gating on the active-mode slice a binding is
482 /// only reachable when its mode is active. These read `&[]` until then —
483 /// and passed, because the lookup ignored activation for every mode in the
484 /// editor. That is the bug they now have to be correct about.
485 ///
486 /// **The reason this needed its own helper.** `<M-S-Right>` and `<M-Right>`
487 /// differ only in SHIFT, which Visual's normalize strips — so a
488 /// normalize-first lookup would collapse OS.6's two verbs onto one.
489 #[test]
490 fn shift_alt_and_alt_are_different_bindings() {
491 let h = KeymapHandle::new();
492 let mode = ModeId::new("os0d-shift-mode");
493 let plain = CommandId::new(u64::MAX - 22);
494 let shifted = CommandId::new(u64::MAX - 23);
495 let alt_right = KeyChord::new(KeyKind::Special(SpecialKey::Right), KeyMods::ALT);
496 let shift_alt_right = KeyChord::new(
497 KeyKind::Special(SpecialKey::Right),
498 KeyMods::ALT | KeyMods::SHIFT,
499 );
500 bind_visual(&h, mode, alt_right, plain);
501 bind_visual(&h, mode, shift_alt_right, shifted);
502
503 assert_eq!(
504 bound_command(&lookup_visual_chord(&h, &[], alt_right, &[mode]).result),
505 plain
506 );
507 assert_eq!(
508 bound_command(&lookup_visual_chord(&h, &[], shift_alt_right, &[mode]).result),
509 shifted,
510 "<M-S-Right> must not collapse onto <M-Right>"
511 );
512 }
513
514 /// What must NOT change: the Visual catalog binds bare chords, and an
515 /// incoming SHIFT-bearing chord still falls back to the bare binding.
516 #[test]
517 fn a_bare_binding_is_still_reached_through_the_fallback() {
518 let h = KeymapHandle::new();
519 let mode = ModeId::new("os0d-bare-mode");
520 let id = CommandId::new(u64::MAX - 24);
521 let bare = KeyChord::new(KeyKind::Special(SpecialKey::Right), KeyMods::NONE);
522 bind_visual(&h, mode, bare, id);
523
524 let shifted = KeyChord::new(KeyKind::Special(SpecialKey::Right), KeyMods::SHIFT);
525 let found = lookup_visual_chord(&h, &[], shifted, &[mode]);
526 assert_eq!(
527 bound_command(&found.result),
528 id,
529 "<S-Right> with nothing claiming it still falls back to <Right>"
530 );
531 assert_eq!(
532 found.resolved, bare,
533 "and that is the form a Partial absorbs"
534 );
535 }
536}