feat: make keybindings configurable

This commit is contained in:
Valdemar Lorenzen
2026-10-05 15:57:33 +02:00
parent 368442ba29
commit 3fd89fcbed
7 changed files with 466 additions and 137 deletions
+100 -4
View File
@@ -48,13 +48,16 @@ impl EditorSession {
Err(error) => notices.push(format!("{}: {error}", path.display())),
}
}
for (key, command) in config.bindings {
if let Err(error) = editor.bind(key, command) {
notices.push(format!("binding {key:?}: {error}"));
for binding in config.bindings {
if let Err(error) = editor.bind(&binding.mode, &binding.key, binding.command) {
notices.push(format!(
"binding {} {:?}: {error}",
binding.mode, binding.key
));
}
}
if notices.is_empty() {
editor.set_message("i: insert • Esc: normal • :w save • :q quit • :plugins");
editor.set_message("Ready • --no-config uses default keybindings");
} else {
editor.set_message(notices.join(" | "));
}
@@ -318,6 +321,99 @@ end
}
}
#[test]
fn configured_bindings_preserve_modal_editing_history_and_literal_paste() {
let fixture = Fixture::new();
fixture.healthy();
let mut session = fixture.open(
r#"
Vamp.configure do |config|
config.plugin "healthy.rb"
config.bind "n", "core.insert_first_nonblank"
config.bind "i", nil
config.bind "<C-r>", "core.redo"
config.bind "<C-c>", "core.normal", mode: "insert"
config.bind "<Esc>", nil, mode: "insert"
config.bind "!", "core.newline", mode: "insert"
config.bind "X", nil, mode: "insert"
config.bind ":", nil
config.bind "<Space>", "core.command"
config.bind "<Enter>", nil, mode: "command"
config.bind "<C-x>", "core.submit", mode: "command"
config.bind "<C-c>", "core.normal", mode: "command"
config.bind "<C-h>", "core.backspace", mode: "command"
config.bind "g", "core.undo"
config.bind "g", "healthy"
config.bind "n", "core.missing"
config.bind "n", "core.undo", mode: "missing"
config.bind "<Unknown>", "core.undo"
config.bind "<C-f>", "healthy", mode: "insert"
end
"#,
);
let message = session.editor().message();
for error in [
"Unknown action for NORMAL: core.missing",
"Unknown binding mode: missing",
"Invalid binding key",
"Ex/plugin commands can only be bound in Normal mode",
] {
assert!(message.contains(error), "{message}");
}
session.handle(Input::Char('i'));
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
session.handle(Input::Char('n'));
for ch in "界Xab!".chars() {
session.handle(Input::Char(ch));
}
assert_eq!(session.editor().lines(), &["界ab", ""]);
session.handle(Input::Escape);
assert_eq!(session.editor().mode(), crate::editor::Mode::Insert);
session.handle(Input::Paste("X!".into()));
assert_eq!(session.editor().lines(), &["界ab", "X!"]);
session.handle(Input::Ctrl('C'));
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
session.handle(Input::Char('u'));
assert_eq!(session.editor().lines(), &["界ab", ""]);
session.handle(Input::Char('u'));
assert_eq!(session.editor().lines(), &[String::new()]);
session.handle(Input::Ctrl('r'));
session.handle(Input::Ctrl('r'));
assert_eq!(session.editor().lines(), &["界ab", "X!"]);
session.handle(Input::Char(':'));
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
session.handle(Input::Char(' '));
for ch in "w界".chars() {
session.handle(Input::Char(ch));
}
session.handle(Input::Backspace);
assert_eq!(session.editor().command_line(), "w");
session.handle(Input::Enter);
assert_eq!(session.editor().mode(), crate::editor::Mode::Command);
session.handle(Input::Ctrl('x'));
assert_eq!(
fs::read_to_string(fixture.0.join("buffer.txt")).unwrap(),
"界ab\nX!"
);
session.handle(Input::Char(' '));
session.handle(Input::Char('q'));
session.handle(Input::Ctrl('c'));
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
assert!(session.editor().command_line().is_empty());
assert!(!session.editor().should_quit());
session.handle(Input::Char('g'));
assert_eq!(session.editor().lines(), &["healthy "]);
session.handle(Input::Char(' '));
session.handle(Input::Char('q'));
session.handle(Input::Ctrl('x'));
assert!(!session.editor().should_quit());
session.handle(Input::Char(' '));
session.handle(Input::Char('q'));
session.handle(Input::Char('!'));
session.handle(Input::Ctrl('x'));
assert!(session.editor().should_quit());
}
#[test]
fn broken_config_falls_back_to_core_without_failing_the_session() {
let fixture = Fixture::new();