use std::fs;
use escriba_buffer::BufferSet;
use escriba_core::{Action, Mode, Motion};
use escriba_keymap::{Key, Keymap};
use escriba_runtime::EditorState;
fn load_and_apply(src: &str) -> (EditorState, escriba_lisp::ApplyReport) {
let mut buffers = BufferSet::new();
let active_id = buffers.scratch("");
let mut state = EditorState::new_with_buffer(buffers, active_id);
let plan = escriba_lisp::apply_source(src).expect("parse rc");
let _ = escriba_lisp::apply_plan_to_commands(&plan, &mut state.commands);
let report = escriba_lisp::apply_plan_to_keymap(&plan, &mut state.keymap);
(state, report)
}
#[test]
fn sample_rc_fixture_parses_and_applies() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples")
.join("sample-rc.lisp");
let src = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display()));
let (_state, report) = load_and_apply(&src);
assert!(
report.warnings.is_empty(),
"sample rc should apply cleanly: {:?}",
report.warnings
);
assert!(
report.keybinds_applied > 0,
"sample rc should apply at least one keybind",
);
}
#[test]
fn applied_keybind_overrides_default_vim() {
let (state, _report) = load_and_apply(
r#"(defkeybind :mode "normal" :key "h" :action "move-right")"#,
);
let binding = state
.keymap
.lookup(Mode::Normal, &Key::Char('h'))
.expect("h should still be bound in normal mode");
assert_eq!(
binding.action,
Action::Move(Motion::Right),
"rc override should replace the default move-left",
);
}
#[test]
fn unknown_action_defers_to_command_registry() {
let (state, report) = load_and_apply(
r#"(defkeybind :mode "normal" :key "<C-p>" :action "picker.files")"#,
);
assert_eq!(report.keybinds_deferred_to_commands, 1);
let binding = state
.keymap
.lookup(Mode::Normal, &Key::Ctrl('p'))
.expect("C-p should be bound");
match &binding.action {
Action::Command { name, .. } => assert_eq!(name, "picker.files"),
other => panic!("expected Action::Command, got {other:?}"),
}
}
#[test]
fn apply_leaves_unrelated_defaults_intact() {
let (state, _report) = load_and_apply(
r#"(defkeybind :mode "normal" :key "h" :action "move-right")"#,
);
let j_binding = state
.keymap
.lookup(Mode::Normal, &Key::Char('j'))
.expect("default_vim should bind j");
assert_eq!(j_binding.action, Action::Move(Motion::Down));
}
#[test]
fn multi_key_sequence_warns_without_crashing() {
let (_state, report) = load_and_apply(
r#"(defkeybind :mode "normal" :key "gh" :action "doc-start")"#,
);
assert!(report.keybinds_applied == 0 || report.warnings.is_empty());
if report.warnings.is_empty() {
} else {
assert_eq!(report.warnings.len(), 1);
assert!(report.warnings[0].contains("multi-key"));
}
}
#[test]
fn fresh_keymap_without_rc_stays_default_vim() {
let km = Keymap::default_vim();
let binding = km
.lookup(Mode::Normal, &Key::Char('h'))
.expect("default_vim binds h");
assert_eq!(binding.action, Action::Move(Motion::Left));
}
#[test]
fn defcmd_registers_and_keybind_dispatches_without_panic() {
let (mut state, report) = load_and_apply(
r#"
(defcmd :name "write-all"
:description "Write every modified buffer"
:action "buffer.write-all")
(defkeybind :mode "normal" :key "W" :action "write-all")
"#,
);
assert!(
state.commands.contains("write-all"),
"defcmd should register into EditorState.commands",
);
assert_eq!(report.keybinds_deferred_to_commands, 1);
let binding = state
.keymap
.lookup(Mode::Normal, &Key::Char('W'))
.expect("W should be bound in normal mode");
match &binding.action {
Action::Command { name, .. } => assert_eq!(name, "write-all"),
other => panic!("expected Action::Command, got {other:?}"),
}
state.on_key(&Key::Char('W'));
assert!(!state.quit_requested);
}
#[test]
fn keybind_to_unregistered_command_defers_without_panic() {
let (mut state, report) = load_and_apply(
r#"(defkeybind :mode "normal" :key "W" :action "no.such.command")"#,
);
assert_eq!(report.keybinds_deferred_to_commands, 1);
assert!(!state.commands.contains("no.such.command"));
state.on_key(&Key::Char('W'));
assert!(!state.quit_requested);
}
#[test]
fn defcmd_write_all_saves_modified_file_buffer_end_to_end() {
use escriba_core::{Edit, Position};
let path = std::env::temp_dir().join("escriba-it-defcmd-writeall.txt");
std::fs::write(&path, "before").expect("seed temp file");
let mut buffers = BufferSet::new();
let id = buffers.open(&path).expect("open temp file");
{
let buf = buffers.get_mut(id).expect("buffer present");
buf.apply(&Edit::insert(Position::ZERO, "X".to_string()))
.expect("edit applies");
buf.modified = true;
}
let mut state = EditorState::new_with_buffer(buffers, id);
let plan = escriba_lisp::apply_source(
r#"
(defcmd :name "w-all" :description "Write all" :action "buffer.write-all")
(defkeybind :mode "normal" :key "W" :action "w-all")
"#,
)
.expect("parse rc");
let _ = escriba_lisp::apply_plan_to_commands(&plan, &mut state.commands);
let _ = escriba_lisp::apply_plan_to_keymap(&plan, &mut state.keymap);
assert!(state.commands.contains("w-all"));
state.on_key(&Key::Char('W'));
let on_disk = std::fs::read_to_string(&path).expect("read back temp file");
let _ = std::fs::remove_file(&path);
assert!(
on_disk.starts_with('X'),
"buffer.write-all should persist the in-memory edit, got {on_disk:?}",
);
}
#[test]
fn leader_sequence_dispatches_command_end_to_end() {
use escriba_core::{Edit, Position};
let path = std::env::temp_dir().join("escriba-it-leader-write.txt");
std::fs::write(&path, "z").expect("seed temp file");
let mut buffers = BufferSet::new();
let id = buffers.open(&path).expect("open temp file");
{
let buf = buffers.get_mut(id).expect("buffer present");
buf.apply(&Edit::insert(Position::ZERO, "Q".to_string()))
.expect("edit applies");
buf.modified = true;
}
let mut state = EditorState::new_with_buffer(buffers, id);
let plan = escriba_lisp::apply_source(
r#"
(defcmd :name "w-all" :description "Write all" :action "buffer.write-all")
(defkeybind :mode "normal" :key "<leader>w" :action "w-all")
"#,
)
.expect("parse rc");
let _ = escriba_lisp::apply_plan_to_commands(&plan, &mut state.commands);
let report = escriba_lisp::apply_plan_to_keymap(&plan, &mut state.keymap);
assert_eq!(report.keybinds_sequences, 1, "<leader>w binds as a sequence");
state.on_key(&Key::Char(','));
assert_eq!(state.pending_keys, vec![Key::Char(',')]);
state.on_key(&Key::Char('w'));
assert!(state.pending_keys.is_empty());
let on_disk = std::fs::read_to_string(&path).expect("read back temp file");
let _ = std::fs::remove_file(&path);
assert!(
on_disk.starts_with('Q'),
"<leader>w should run write-all and persist the edit, got {on_disk:?}",
);
}
#[test]
fn defoption_applies_to_editor_option_store() {
let mut buffers = BufferSet::new();
let id = buffers.scratch("");
let mut state = EditorState::new_with_buffer(buffers, id);
let plan = escriba_lisp::apply_source(
r#"
(defoption :name "number" :value "true")
(defoption :name "tabstop" :value "4")
"#,
)
.expect("parse rc");
escriba_lisp::apply_plan_to_options(&plan, &mut state.options);
assert_eq!(state.options.get("number").map(String::as_str), Some("true"));
assert_eq!(state.options.get("tabstop").map(String::as_str), Some("4"));
}
#[test]
fn declarative_and_imperative_option_tiers_share_one_store() {
let mut buffers = BufferSet::new();
let id = buffers.scratch("");
let mut state = EditorState::new_with_buffer(buffers, id);
let plan =
escriba_lisp::apply_source(r#"(defoption :name "number" :value "true")"#).expect("parse");
escriba_lisp::apply_plan_to_options(&plan, &mut state.options);
assert_eq!(state.options.get("number").map(String::as_str), Some("true"));
state.run_lisp(r#"(set-option "number" "false")"#).unwrap();
assert_eq!(
state.options.get("number").map(String::as_str),
Some("false"),
);
}