feat: Allow for user-defined "chord" / multi-key keybindings (and correct vi motions when targeting <space> char)#1016
Conversation
|
Am experimenting with the nushell-side of things for configuring in Currently, keychord configuration looks like this: $env.config.keybindings ++= [
{
name: "jj_normal"
modifier: "none"
keycode: [ "char_j", "char_j" ]
mode: "vi_insert"
event: { send: "vichangemode", mode: "normal" }
}
{
name: "save_buffer"
modifier: "control"
keycode: [ "char_x", "char_s" ]
mode: "emacs"
event: { send: "submit" }
}
{
name: "mixed_mods"
modifier: "none"
keycode: [
{ modifier: "alt", keycode: "char_w" }
{ modifier: "none", keycode: "char_j" }
]
mode: "emacs"
event: { send: "openeditor" }
}
]How do we feel about it? |
vi motions when targeting <space> char)vi motions when targeting <space> char)
|
I'm not in a position to review this, but just want to say that this is what's keeping me from using nushell – I've have a serious Edit. Actually I just quit my jk addiction and use caps lock as esc. Have been a happy nushell user for a few months now. |
|
Well this is a massive Change, I just overflew it. I understand that this is important to you and I am already thinking about a way how to make repeated/pending keys part of the editor such that Thanks, but as long as I can't make sure |
|
Very fair feedback. I've been dogfooding my fork of nushell using this branch for ~4-5 months now. If you feel this approach is valuable for the project, I'd be more than happy to revive this pr & ensure it's well-tested (incl. using your new Vi harness). Otherwise, I'm perfectly happy maintaining my personal fork. Totally your call! |
|
@benvansleen My idea: It might be cleanest to close this in favor of a fresh PR for the refactor, happy to look at it as a draft. |
|
Heads up Could the resolver sit on top of that trie rather than the flat list? Worth coordinating with @reubeno? |
|
Would be happy to! Think a trie is a great idea. Should have time to take a closer look at #989 this weekend. |
|
That sounds great, thanks @benvansleen! |
I care a great deal about nushell's vi mode support. I got myself addicted to "jj" to exit vi normal mode in basically all cli tools with a vi mode -- without it, I feel like I've lost my fingers!
A while back, I proposed #670. Since
reedlinedoes not support multi-key "chords," I special-cased logic intovi/mod.rsto listen for repeated keypresses in insert mode for a designated "exit-insert-mode" trigger.The feedback led to a broader discussion. The main gist (as I understood it at the time) was that we should not special-case this logic; ideally, we would generalize this functionality to enable other kinds of user-defined key chords.
In the meantime, I've been maintaining a personal
reedlinefork with this special-case logic. It's fulfilled my needs, but is kind of a PITA. So: I thought why not take another stab?Full disclosure: I've been trying to experiment with LSP-informed LLM code generation for languages with well-developed type systems (eg through something like rust LSP w/ opencode). The first draft of this PR was predominantly LLM generated, but I have reviewed the changes & am test-driving this as my daily-driver shell.
User-facing changes
ReedlineEvent:ViChangeModehandling to better mimic vi/vim behavior (eg when moving from insert -> normal modes, the cursor should move left 1 char)emacsandvi) now track a sequence state of keypressesf<space>does not behave as expected in nushell; it basically inserts a space at b.o.l. and jacks up the undo bufferWhat does this achieve for
nushell?in
nu-cli/src/reedline_config.rs, you could:Obviously, this would be better configured as part of the user's nushell startup script. If this PR is approved / we like this direction, I'll submit work on the nushell side to allow for configuring
$env.config.keybindingsaccordingly.How does it work?
Key event flow (Emacs / Vi):
KeyEvent -> normalize -> KeyCombination
-> KeySequenceState.process_combo(...)
-> SequenceResolution.into_event(|combo| fallback(combo))
-> ReedlineEvent returned to engine
Notes:
pending_exactholds an exact match that is also a prefix of a longer sequence.If the longer sequence fails, we emit the saved event and keep trailing keys.
SequenceResolution.events= matched sequencesSequenceResolution.combos= raw keys to replay through fallbackinto_eventcombines both into a single ReedlineEventVi specifics:
so
comboscan be re-fed into vi’s grammar instead of becoming edits.Timeout path:
Engine timeout -> EditMode.flush_pending_sequence()
-> KeySequenceState.flush_with_combos()
-> SequenceResolution.into_event(...)
-> ReedlineEvent
Walking through an example!
Scenario: insert mode, sequence binding
j j→ViChangeMode("normal".into())Initial state:
KeySequenceState.buffer = []pending_exact = Nonesequence_bindings contains [j, j] -> ViChangeMode("normal".into())Step 2: user presses j again
Result:
ViChangeMode("normal".into())is emitted immediately.Step 3: If the user doesn’t press another key
Failure/timeout example: