use std::fs;
use crossterm::event::KeyCode;
use yosh::env::ShellEnv;
use yosh::env::aliases::AliasStore;
use yosh::interactive::completion::CompletionContext;
use yosh::interactive::command_completion::{CommandCompleter, CommandCompletionContext};
use yosh::interactive::edit_action::EditAction;
use yosh::interactive::fuzzy_search::FuzzySearchUI;
use yosh::interactive::highlight::{CheckerEnv, HighlightScanner};
use yosh::interactive::history::History;
use yosh::interactive::keymap::{BufferState, Keymap};
use yosh::interactive::line_editor::LineEditor;
use yosh::interactive::parse_status::{classify_parse, ParseStatus};
use yosh::interactive::prompt::expand_prompt;
mod helpers;
use helpers::mock_terminal::{MockTerminal, alt, chars, ctrl, key};
#[test]
fn test_insert_char_at_start() {
let mut ed = LineEditor::new();
ed.insert_char('a');
assert_eq!(ed.buffer(), "a");
assert_eq!(ed.cursor(), 1);
}
#[test]
fn test_insert_char_multiple() {
let mut ed = LineEditor::new();
ed.insert_char('a');
ed.insert_char('b');
ed.insert_char('c');
assert_eq!(ed.buffer(), "abc");
assert_eq!(ed.cursor(), 3);
}
#[test]
fn test_insert_char_at_middle() {
let mut ed = LineEditor::new();
ed.insert_char('a');
ed.insert_char('c');
ed.move_cursor_left();
ed.insert_char('b');
assert_eq!(ed.buffer(), "abc");
assert_eq!(ed.cursor(), 2);
}
#[test]
fn test_delete_char_backspace() {
let mut ed = LineEditor::new();
ed.insert_char('a');
ed.insert_char('b');
ed.backspace();
assert_eq!(ed.buffer(), "a");
assert_eq!(ed.cursor(), 1);
}
#[test]
fn test_backspace_at_start_does_nothing() {
let mut ed = LineEditor::new();
ed.backspace();
assert_eq!(ed.buffer(), "");
assert_eq!(ed.cursor(), 0);
}
#[test]
fn test_delete_at_cursor() {
let mut ed = LineEditor::new();
ed.insert_char('a');
ed.insert_char('b');
ed.insert_char('c');
ed.move_cursor_left();
ed.delete();
assert_eq!(ed.buffer(), "ab");
assert_eq!(ed.cursor(), 2);
}
#[test]
fn test_delete_at_end_does_nothing() {
let mut ed = LineEditor::new();
ed.insert_char('a');
ed.delete();
assert_eq!(ed.buffer(), "a");
assert_eq!(ed.cursor(), 1);
}
#[test]
fn test_move_cursor_left() {
let mut ed = LineEditor::new();
ed.insert_char('a');
ed.insert_char('b');
ed.move_cursor_left();
assert_eq!(ed.cursor(), 1);
}
#[test]
fn test_move_cursor_left_at_start_does_nothing() {
let mut ed = LineEditor::new();
ed.move_cursor_left();
assert_eq!(ed.cursor(), 0);
}
#[test]
fn test_move_cursor_right() {
let mut ed = LineEditor::new();
ed.insert_char('a');
ed.insert_char('b');
ed.move_cursor_left();
ed.move_cursor_left();
ed.move_cursor_right();
assert_eq!(ed.cursor(), 1);
}
#[test]
fn test_move_cursor_right_at_end_does_nothing() {
let mut ed = LineEditor::new();
ed.insert_char('a');
ed.move_cursor_right();
assert_eq!(ed.cursor(), 1);
}
#[test]
fn test_move_to_start() {
let mut ed = LineEditor::new();
ed.insert_char('a');
ed.insert_char('b');
ed.insert_char('c');
ed.move_to_start();
assert_eq!(ed.cursor(), 0);
}
#[test]
fn test_move_to_end() {
let mut ed = LineEditor::new();
ed.insert_char('a');
ed.insert_char('b');
ed.insert_char('c');
ed.move_to_start();
ed.move_to_end();
assert_eq!(ed.cursor(), 3);
}
#[test]
fn test_clear_buffer() {
let mut ed = LineEditor::new();
ed.insert_char('a');
ed.insert_char('b');
ed.clear();
assert_eq!(ed.buffer(), "");
assert_eq!(ed.cursor(), 0);
}
#[test]
fn test_is_empty() {
let mut ed = LineEditor::new();
assert!(ed.is_empty());
ed.insert_char('a');
assert!(!ed.is_empty());
}
#[test]
fn test_to_string() {
let mut ed = LineEditor::new();
ed.insert_char('h');
ed.insert_char('i');
assert_eq!(ed.to_string(), "hi");
}
#[test]
fn test_backspace_in_middle() {
let mut ed = LineEditor::new();
ed.insert_char('a');
ed.insert_char('b');
ed.insert_char('c');
ed.move_cursor_left();
ed.backspace();
assert_eq!(ed.buffer(), "ac");
assert_eq!(ed.cursor(), 1);
}
#[test]
fn test_prompt_default_ps1() {
let mut env = ShellEnv::new("yosh", vec![]);
let _ = env.vars.unset("PS1");
let prompt = expand_prompt(&mut env, "PS1");
assert_eq!(prompt, "$ ");
}
#[test]
fn test_prompt_default_ps2() {
let mut env = ShellEnv::new("yosh", vec![]);
let _ = env.vars.unset("PS2");
let prompt = expand_prompt(&mut env, "PS2");
assert_eq!(prompt, "> ");
}
#[test]
fn test_prompt_custom_ps1() {
let mut env = ShellEnv::new("yosh", vec![]);
env.vars.set("PS1", "myshell> ").unwrap();
let prompt = expand_prompt(&mut env, "PS1");
assert_eq!(prompt, "myshell> ");
}
#[test]
fn test_prompt_with_variable_expansion() {
let mut env = ShellEnv::new("yosh", vec![]);
env.vars.set("MYVAR", "hello").unwrap();
env.vars.set("PS1", "${MYVAR}$ ").unwrap();
let prompt = expand_prompt(&mut env, "PS1");
assert_eq!(prompt, "hello$ ");
}
#[test]
fn test_prompt_empty_string() {
let mut env = ShellEnv::new("yosh", vec![]);
env.vars.set("PS1", "").unwrap();
let prompt = expand_prompt(&mut env, "PS1");
assert_eq!(prompt, "");
}
#[test]
fn test_classify_complete_command() {
let aliases = AliasStore::default();
match classify_parse("echo hello\n", &aliases) {
ParseStatus::Complete(_) => {}
other => panic!("expected Complete, got {:?}", other),
}
}
#[test]
fn test_classify_empty_input() {
let aliases = AliasStore::default();
match classify_parse("\n", &aliases) {
ParseStatus::Empty => {}
other => panic!("expected Empty, got {:?}", other),
}
}
#[test]
fn test_classify_incomplete_if() {
let aliases = AliasStore::default();
match classify_parse("if true; then\n", &aliases) {
ParseStatus::Incomplete => {}
other => panic!("expected Incomplete, got {:?}", other),
}
}
#[test]
fn test_classify_incomplete_while() {
let aliases = AliasStore::default();
match classify_parse("while true; do\n", &aliases) {
ParseStatus::Incomplete => {}
other => panic!("expected Incomplete, got {:?}", other),
}
}
#[test]
fn test_classify_incomplete_single_quote() {
let aliases = AliasStore::default();
match classify_parse("echo 'hello\n", &aliases) {
ParseStatus::Incomplete => {}
other => panic!("expected Incomplete, got {:?}", other),
}
}
#[test]
fn test_classify_incomplete_double_quote() {
let aliases = AliasStore::default();
match classify_parse("echo \"hello\n", &aliases) {
ParseStatus::Incomplete => {}
other => panic!("expected Incomplete, got {:?}", other),
}
}
#[test]
fn test_classify_incomplete_backslash_newline() {
let aliases = AliasStore::default();
match classify_parse("echo hello \\\n", &aliases) {
ParseStatus::Incomplete => {}
other => panic!("expected Incomplete, got {:?}", other),
}
}
#[test]
fn test_classify_incomplete_pipe() {
let aliases = AliasStore::default();
match classify_parse("echo hello |\n", &aliases) {
ParseStatus::Incomplete => {}
other => panic!("expected Incomplete, got {:?}", other),
}
}
#[test]
fn test_classify_incomplete_and_or() {
let aliases = AliasStore::default();
match classify_parse("true &&\n", &aliases) {
ParseStatus::Incomplete => {}
other => panic!("expected Incomplete, got {:?}", other),
}
}
#[test]
fn test_classify_error() {
let aliases = AliasStore::default();
match classify_parse("echo hello >>\n", &aliases) {
ParseStatus::Error(_) => {}
other => panic!("expected Error, got {:?}", other),
}
}
#[test]
fn test_classify_multiple_commands() {
let aliases = AliasStore::default();
match classify_parse("echo a; echo b\n", &aliases) {
ParseStatus::Complete(_) => {}
other => panic!("expected Complete, got {:?}", other),
}
}
#[test]
fn test_mock_basic_input() {
let mut events = chars("hello");
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("hello".to_string()));
}
#[test]
fn test_mock_ctrl_c_returns_empty() {
let events = vec![
key(KeyCode::Char('a')),
key(KeyCode::Char('b')),
ctrl('c'),
];
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some(String::new()));
}
#[test]
fn test_mock_ctrl_d_empty_returns_none() {
let events = vec![ctrl('d')];
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_mock_ctrl_d_nonempty_deletes_char() {
let events = vec![
key(KeyCode::Char('a')),
key(KeyCode::Char('b')),
key(KeyCode::Left),
ctrl('d'),
key(KeyCode::Enter),
];
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("a".to_string()));
}
#[test]
fn test_mock_ctrl_a_and_ctrl_e() {
let events = vec![
key(KeyCode::Char('a')),
key(KeyCode::Char('b')),
key(KeyCode::Char('c')),
ctrl('a'),
key(KeyCode::Char('x')),
ctrl('e'),
key(KeyCode::Char('y')),
key(KeyCode::Enter),
];
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("xabcy".to_string()));
}
#[test]
fn test_mock_ctrl_b_and_ctrl_f() {
let events = vec![
key(KeyCode::Char('a')),
key(KeyCode::Char('b')),
key(KeyCode::Char('c')),
ctrl('b'),
ctrl('b'),
key(KeyCode::Char('x')),
ctrl('f'),
key(KeyCode::Char('y')),
key(KeyCode::Enter),
];
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("axbyc".to_string()));
}
#[test]
fn test_mock_home_end_keys() {
let events = vec![
key(KeyCode::Char('a')),
key(KeyCode::Char('b')),
key(KeyCode::Char('c')),
key(KeyCode::Home),
key(KeyCode::Char('x')),
key(KeyCode::End),
key(KeyCode::Char('y')),
key(KeyCode::Enter),
];
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("xabcy".to_string()));
}
#[test]
fn test_mock_backspace() {
let events = vec![
key(KeyCode::Char('a')),
key(KeyCode::Char('b')),
key(KeyCode::Char('c')),
key(KeyCode::Backspace),
key(KeyCode::Backspace),
key(KeyCode::Enter),
];
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("a".to_string()));
}
#[test]
fn test_mock_delete_key() {
let events = vec![
key(KeyCode::Char('a')),
key(KeyCode::Char('b')),
key(KeyCode::Char('c')),
key(KeyCode::Home),
key(KeyCode::Delete),
key(KeyCode::Enter),
];
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("bc".to_string()));
}
#[test]
fn test_mock_history_up_down() {
let mut history = History::new();
history.add("first", 500, "");
history.add("second", 500, "");
let events = vec![
key(KeyCode::Up),
key(KeyCode::Up),
key(KeyCode::Down),
key(KeyCode::Enter),
];
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("second".to_string()));
}
#[test]
fn test_mock_history_up_and_edit() {
let mut history = History::new();
history.add("echo old", 500, "");
let mut events = vec![key(KeyCode::Up)];
events.extend(vec![key(KeyCode::Backspace); 3]);
events.extend(chars("new"));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("echo new".to_string()));
}
#[test]
fn test_mock_history_preserves_typed_text() {
let mut history = History::new();
history.add("old", 500, "");
let mut events = chars("partial");
events.push(key(KeyCode::Up));
events.push(key(KeyCode::Down));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("partial".to_string()));
}
#[test]
fn test_mock_ctrl_r_selects_matching_entry() {
let mut history = History::new();
history.add("ls -la", 500, "");
history.add("git commit -m 'fix'", 500, "");
history.add("cargo test", 500, "");
let mut events = vec![ctrl('r')];
events.extend(chars("git"));
events.push(key(KeyCode::Enter)); events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("git commit -m 'fix'".to_string()));
}
#[test]
fn test_mock_ctrl_r_cancel_with_esc() {
let mut history = History::new();
history.add("ls -la", 500, "");
history.add("git commit", 500, "");
let mut events = chars("hello");
events.push(ctrl('r'));
events.extend(chars("git"));
events.push(key(KeyCode::Esc)); events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("hello".to_string()));
}
#[test]
fn test_mock_ctrl_r_navigate_up() {
let mut history = History::new();
history.add("echo first", 500, "");
history.add("echo second", 500, "");
history.add("echo third", 500, "");
let events = vec![
ctrl('r'),
key(KeyCode::Up), key(KeyCode::Enter), key(KeyCode::Enter), ];
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("echo second".to_string()));
}
#[test]
fn test_mock_ctrl_r_backspace_updates_candidates() {
let mut history = History::new();
history.add("git log", 500, "");
history.add("cargo test", 500, "");
let events = vec![
ctrl('r'),
key(KeyCode::Char('g')),
key(KeyCode::Char('i')),
key(KeyCode::Backspace),
key(KeyCode::Backspace),
key(KeyCode::Char('c')),
key(KeyCode::Char('a')),
key(KeyCode::Enter), key(KeyCode::Enter), ];
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("cargo test".to_string()));
}
#[test]
fn test_mock_fuzzy_search_direct_select() {
let mut history = History::new();
history.add("ls -la", 500, "");
history.add("git status", 500, "");
history.add("cargo build", 500, "");
let mut events = chars("sta");
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let result = FuzzySearchUI::run(&history, &mut term).unwrap();
assert_eq!(result, Some("git status".to_string()));
}
#[test]
fn test_mock_fuzzy_search_direct_cancel() {
let mut history = History::new();
history.add("ls -la", 500, "");
let events = vec![key(KeyCode::Esc)];
let mut term = MockTerminal::new(events);
let result = FuzzySearchUI::run(&history, &mut term).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_mock_fuzzy_search_empty_history() {
let history = History::new();
let mut term = MockTerminal::new(vec![]);
let result = FuzzySearchUI::run(&history, &mut term).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_mock_ctrl_r_with_ctrl_g_cancel() {
let mut history = History::new();
history.add("some command", 500, "");
let events = vec![
ctrl('r'),
ctrl('g'), key(KeyCode::Enter), ];
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some(String::new()));
}
#[test]
fn test_fuzzy_search_arrow_keys_no_cursor_drift() {
let mut history = History::new();
history.add("echo first", 500, "");
history.add("echo second", 500, "");
history.add("echo third", 500, "");
history.add("echo fourth", 500, "");
history.add("echo fifth", 500, "");
let events = vec![
key(KeyCode::Up),
key(KeyCode::Up),
key(KeyCode::Up),
key(KeyCode::Down),
key(KeyCode::Down),
key(KeyCode::Esc), ];
let mut term = MockTerminal::new(events);
let _ = FuzzySearchUI::run(&history, &mut term).unwrap();
assert_eq!(
term.cursor_row(),
0,
"cursor drifted {} rows from origin after ↑↓ navigation in fuzzy search",
term.cursor_row()
);
}
#[test]
fn test_fuzzy_search_select_no_cursor_drift() {
let mut history = History::new();
history.add("echo first", 500, "");
history.add("echo second", 500, "");
history.add("echo third", 500, "");
let events = vec![
key(KeyCode::Up), key(KeyCode::Up), key(KeyCode::Down), key(KeyCode::Enter), ];
let mut term = MockTerminal::new(events);
let result = FuzzySearchUI::run(&history, &mut term).unwrap();
assert_eq!(result, Some("echo second".to_string()));
assert_eq!(
term.cursor_row(),
0,
"cursor drifted {} rows from origin after selection in fuzzy search",
term.cursor_row()
);
}
#[test]
fn test_suggest_accept_full_with_right_arrow() {
let mut history = History::new();
history.add("git commit -m 'fix'", 500, "");
let mut events = chars("git c");
events.push(key(KeyCode::Right));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("git commit -m 'fix'".to_string()));
}
#[test]
fn test_suggest_accept_full_with_ctrl_f() {
let mut history = History::new();
history.add("cargo test --release", 500, "");
let mut events = chars("cargo t");
events.push(ctrl('f'));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("cargo test --release".to_string()));
}
#[test]
fn test_right_arrow_normal_when_no_suggestion() {
let mut history = History::new();
history.add("git commit", 500, "");
let mut events = chars("abc");
events.push(key(KeyCode::Left));
events.push(key(KeyCode::Right));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("abc".to_string()));
}
#[test]
fn test_suggest_appears_on_typing() {
let mut history = History::new();
history.add("git commit -m 'fix'", 500, "");
let mut events = chars("git c");
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("git c".to_string()));
let output = term.output().join("");
assert!(
output.contains("[DIM]"),
"suggestion should trigger dim rendering"
);
assert!(
output.contains("ommit -m 'fix'"),
"suggestion text should appear in output"
);
}
#[test]
fn test_suggest_hidden_when_cursor_not_at_end() {
let mut history = History::new();
history.add("echo hello world", 500, "");
let mut events = chars("echo h");
events.push(key(KeyCode::Left));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let _ = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
let output_parts = term.output();
let last_outputs = output_parts.iter().rev().take(10).collect::<Vec<_>>();
let last_chunk: String = last_outputs.iter().rev().map(|s| s.as_str()).collect();
let last_dim_pos = last_chunk.rfind("[DIM]");
let last_nodim_pos = last_chunk.rfind("[/DIM]");
match (last_dim_pos, last_nodim_pos) {
(Some(d), Some(nd)) => assert!(d < nd, "suggestion should not be active after cursor moved left"),
(None, _) => {}
(Some(_), None) => panic!("unclosed [DIM] in output"),
}
}
#[test]
fn test_suggest_cleared_on_history_navigation() {
let mut history = History::new();
history.add("echo hello", 500, "");
history.add("echo world", 500, "");
let mut events = chars("echo ");
events.push(key(KeyCode::Up));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("echo world".to_string()));
}
#[test]
fn test_suggest_accept_word_with_alt_f() {
let mut history = History::new();
history.add("git commit -m 'fix'", 500, "");
let mut events = chars("git");
events.push(alt('f'));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("git commit".to_string()));
}
#[test]
fn test_suggest_accept_word_stepwise() {
let mut history = History::new();
history.add("git commit -m 'fix'", 500, "");
let mut events = chars("git");
events.push(alt('f')); events.push(alt('f')); events.push(alt('f')); events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("git commit -m 'fix'".to_string()));
}
#[test]
fn test_alt_f_noop_without_suggestion() {
let mut history = History::new();
let mut events = chars("hello");
events.push(alt('f'));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("hello".to_string()));
}
#[test]
fn test_ctrl_r_redraws_prompt_after_selection() {
let mut history = History::new();
history.add("echo hello", 500, "");
let events = vec![
ctrl('r'),
key(KeyCode::Enter), key(KeyCode::Enter), ];
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("mysh$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("echo hello".to_string()));
let output = term.output().join("");
assert!(
output.contains("mysh$ "),
"prompt was not redrawn after Ctrl+R selection"
);
}
#[test]
fn test_suggest_updates_on_backspace() {
let mut history = History::new();
history.add("echo hello", 500, "");
history.add("echo world", 500, "");
let mut events = chars("echo w");
events.push(key(KeyCode::Backspace));
events.push(key(KeyCode::Right)); events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let result = editor.read_line("$ ", &[], &mut history, &mut term).unwrap();
assert_eq!(result, Some("echo world".to_string()));
}
#[test]
fn test_tab_completes_single_candidate() {
let tmp = tempfile::TempDir::new().unwrap();
fs::File::create(tmp.path().join("unique_file.txt")).unwrap();
let ctx = CompletionContext {
cwd: tmp.path().to_str().unwrap().to_string(),
home: "/home/user".to_string(),
show_dotfiles: false,
};
let mut events = chars("ls uni");
events.push(key(KeyCode::Tab));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let aliases = AliasStore::default();
let mut command_completer = CommandCompleter::new();
let mut cmd_ctx = CommandCompletionContext {
completer: &mut command_completer,
path: "",
builtins: &[],
aliases: &aliases,
};
let mut scanner = HighlightScanner::new();
let checker_env = CheckerEnv { path: "", aliases: &aliases };
let result = editor
.read_line_with_completion("$ ", &[], &mut history, &mut term, &ctx, &mut cmd_ctx, &mut scanner, &checker_env, "")
.unwrap();
assert_eq!(result, Some("ls unique_file.txt ".to_string()));
}
#[test]
fn test_tab_completes_common_prefix() {
let tmp = tempfile::TempDir::new().unwrap();
fs::File::create(tmp.path().join("file_alpha.rs")).unwrap();
fs::File::create(tmp.path().join("file_beta.rs")).unwrap();
let ctx = CompletionContext {
cwd: tmp.path().to_str().unwrap().to_string(),
home: "/home/user".to_string(),
show_dotfiles: false,
};
let mut events = chars("ls file");
events.push(key(KeyCode::Tab));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let aliases = AliasStore::default();
let mut command_completer = CommandCompleter::new();
let mut cmd_ctx = CommandCompletionContext {
completer: &mut command_completer,
path: "",
builtins: &[],
aliases: &aliases,
};
let mut scanner = HighlightScanner::new();
let checker_env = CheckerEnv { path: "", aliases: &aliases };
let result = editor
.read_line_with_completion("$ ", &[], &mut history, &mut term, &ctx, &mut cmd_ctx, &mut scanner, &checker_env, "")
.unwrap();
assert_eq!(result, Some("ls file_".to_string()));
}
#[test]
fn test_tab_directory_appends_slash() {
let tmp = tempfile::TempDir::new().unwrap();
fs::create_dir(tmp.path().join("mydir")).unwrap();
let ctx = CompletionContext {
cwd: tmp.path().to_str().unwrap().to_string(),
home: "/home/user".to_string(),
show_dotfiles: false,
};
let mut events = chars("ls my");
events.push(key(KeyCode::Tab));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let aliases = AliasStore::default();
let mut command_completer = CommandCompleter::new();
let mut cmd_ctx = CommandCompletionContext {
completer: &mut command_completer,
path: "",
builtins: &[],
aliases: &aliases,
};
let mut scanner = HighlightScanner::new();
let checker_env = CheckerEnv { path: "", aliases: &aliases };
let result = editor
.read_line_with_completion("$ ", &[], &mut history, &mut term, &ctx, &mut cmd_ctx, &mut scanner, &checker_env, "")
.unwrap();
assert_eq!(result, Some("ls mydir/".to_string()));
}
#[test]
fn test_tab_no_match_does_nothing() {
let tmp = tempfile::TempDir::new().unwrap();
fs::File::create(tmp.path().join("abc.txt")).unwrap();
let ctx = CompletionContext {
cwd: tmp.path().to_str().unwrap().to_string(),
home: "/home/user".to_string(),
show_dotfiles: false,
};
let mut events = chars("ls xyz");
events.push(key(KeyCode::Tab));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let aliases = AliasStore::default();
let mut command_completer = CommandCompleter::new();
let mut cmd_ctx = CommandCompletionContext {
completer: &mut command_completer,
path: "",
builtins: &[],
aliases: &aliases,
};
let mut scanner = HighlightScanner::new();
let checker_env = CheckerEnv { path: "", aliases: &aliases };
let result = editor
.read_line_with_completion("$ ", &[], &mut history, &mut term, &ctx, &mut cmd_ctx, &mut scanner, &checker_env, "")
.unwrap();
assert_eq!(result, Some("ls xyz".to_string()));
}
#[test]
fn test_double_tab_opens_completion_ui() {
let dir = std::env::temp_dir().join(format!("yosh-tab-test5-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("file_alpha.rs"), "").unwrap();
fs::write(dir.join("file_beta.rs"), "").unwrap();
let ctx = yosh::interactive::completion::CompletionContext {
cwd: dir.to_str().unwrap().to_string(),
home: "/tmp".to_string(),
show_dotfiles: false,
};
let mut events = chars("ls file_");
events.push(key(KeyCode::Tab)); events.push(key(KeyCode::Tab)); events.push(key(KeyCode::Up)); events.push(key(KeyCode::Enter)); events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let aliases = AliasStore::default();
let mut command_completer = CommandCompleter::new();
let mut cmd_ctx = CommandCompletionContext {
completer: &mut command_completer,
path: "",
builtins: &[],
aliases: &aliases,
};
let mut scanner = HighlightScanner::new();
let checker_env = CheckerEnv { path: "", aliases: &aliases };
let result = editor.read_line_with_completion("$ ", &[], &mut history, &mut term, &ctx, &mut cmd_ctx, &mut scanner, &checker_env, "").unwrap();
assert_eq!(result, Some("ls file_beta.rs ".to_string()));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_tab_command_completion_at_line_start() {
let tmp = tempfile::TempDir::new().unwrap();
let bin_dir = tempfile::TempDir::new().unwrap();
let cmd_path = bin_dir.path().join("yosh_test_mycmd");
fs::File::create(&cmd_path).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&cmd_path, fs::Permissions::from_mode(0o755)).unwrap();
}
let ctx = CompletionContext {
cwd: tmp.path().to_str().unwrap().to_string(),
home: "/tmp".to_string(),
show_dotfiles: false,
};
let mut command_completer = CommandCompleter::new();
let aliases = AliasStore::default();
let path_str = bin_dir.path().to_str().unwrap().to_string();
let mut cmd_ctx = CommandCompletionContext {
completer: &mut command_completer,
path: &path_str,
builtins: &[],
aliases: &aliases,
};
let mut events = chars("yosh_test_my");
events.push(key(KeyCode::Tab));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let mut scanner = HighlightScanner::new();
let checker_env = CheckerEnv { path: "", aliases: &aliases };
let result = editor
.read_line_with_completion("$ ", &[], &mut history, &mut term, &ctx, &mut cmd_ctx, &mut scanner, &checker_env, "")
.unwrap();
assert_eq!(result, Some("yosh_test_mycmd ".to_string()));
}
#[test]
fn test_tab_command_position_path_fallback() {
let tmp = tempfile::TempDir::new().unwrap();
fs::File::create(tmp.path().join("myscript.sh")).unwrap();
let ctx = CompletionContext {
cwd: tmp.path().to_str().unwrap().to_string(),
home: "/tmp".to_string(),
show_dotfiles: false,
};
let mut command_completer = CommandCompleter::new();
let aliases = AliasStore::default();
let mut cmd_ctx = CommandCompletionContext {
completer: &mut command_completer,
path: "",
builtins: &[],
aliases: &aliases,
};
let mut events = chars("./my");
events.push(key(KeyCode::Tab));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let mut scanner = HighlightScanner::new();
let checker_env = CheckerEnv { path: "", aliases: &aliases };
let result = editor
.read_line_with_completion("$ ", &[], &mut history, &mut term, &ctx, &mut cmd_ctx, &mut scanner, &checker_env, "")
.unwrap();
assert_eq!(result, Some("./myscript.sh ".to_string()));
}
#[test]
fn test_tab_argument_position_uses_path_completion() {
let tmp = tempfile::TempDir::new().unwrap();
fs::File::create(tmp.path().join("testfile.txt")).unwrap();
let ctx = CompletionContext {
cwd: tmp.path().to_str().unwrap().to_string(),
home: "/tmp".to_string(),
show_dotfiles: false,
};
let mut command_completer = CommandCompleter::new();
let aliases = AliasStore::default();
let mut cmd_ctx = CommandCompletionContext {
completer: &mut command_completer,
path: "",
builtins: &[],
aliases: &aliases,
};
let mut events = chars("cat test");
events.push(key(KeyCode::Tab));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let mut scanner = HighlightScanner::new();
let checker_env = CheckerEnv { path: "", aliases: &aliases };
let result = editor
.read_line_with_completion("$ ", &[], &mut history, &mut term, &ctx, &mut cmd_ctx, &mut scanner, &checker_env, "")
.unwrap();
assert_eq!(result, Some("cat testfile.txt ".to_string()));
}
#[test]
fn test_tab_completes_builtin() {
let tmp = tempfile::TempDir::new().unwrap();
let ctx = CompletionContext {
cwd: tmp.path().to_str().unwrap().to_string(),
home: "/tmp".to_string(),
show_dotfiles: false,
};
let mut command_completer = CommandCompleter::new();
let aliases = AliasStore::default();
let mut cmd_ctx = CommandCompletionContext {
completer: &mut command_completer,
path: "",
builtins: &["export", "exec", "exit"],
aliases: &aliases,
};
let mut events = chars("expo");
events.push(key(KeyCode::Tab));
events.push(key(KeyCode::Enter));
let mut term = MockTerminal::new(events);
let mut editor = LineEditor::new();
let mut history = History::new();
let mut scanner = HighlightScanner::new();
let checker_env = CheckerEnv { path: "", aliases: &aliases };
let result = editor
.read_line_with_completion("$ ", &[], &mut history, &mut term, &ctx, &mut cmd_ctx, &mut scanner, &checker_env, "")
.unwrap();
assert_eq!(result, Some("export ".to_string()));
}
#[test]
fn test_kill_ring_kill_and_yank() {
use yosh::interactive::kill_ring::KillRing;
let mut kr = KillRing::new(60);
kr.kill("hello", false);
assert_eq!(kr.yank(), Some("hello"));
}
#[test]
fn test_kill_ring_multiple_kills() {
use yosh::interactive::kill_ring::KillRing;
let mut kr = KillRing::new(60);
kr.kill("first", false);
kr.kill("second", false);
assert_eq!(kr.yank(), Some("second"));
}
#[test]
fn test_kill_ring_append_forward() {
use yosh::interactive::kill_ring::KillRing;
let mut kr = KillRing::new(60);
kr.kill("hello", false);
kr.kill(" world", true);
assert_eq!(kr.yank(), Some("hello world"));
}
#[test]
fn test_kill_ring_yank_pop_cycles() {
use yosh::interactive::kill_ring::KillRing;
let mut kr = KillRing::new(60);
kr.kill("first", false);
kr.kill("second", false);
kr.kill("third", false);
assert_eq!(kr.yank(), Some("third"));
assert_eq!(kr.yank_pop(), Some("second"));
assert_eq!(kr.yank_pop(), Some("first"));
assert_eq!(kr.yank_pop(), Some("third"));
}
#[test]
fn test_kill_ring_yank_empty() {
use yosh::interactive::kill_ring::KillRing;
let mut kr = KillRing::new(60);
assert_eq!(kr.yank(), None);
}
#[test]
fn test_kill_ring_yank_pop_empty() {
use yosh::interactive::kill_ring::KillRing;
let mut kr = KillRing::new(60);
assert_eq!(kr.yank_pop(), None);
}
#[test]
fn test_kill_ring_max_size() {
use yosh::interactive::kill_ring::KillRing;
let mut kr = KillRing::new(3);
kr.kill("a", false);
kr.kill("b", false);
kr.kill("c", false);
kr.kill("d", false);
assert_eq!(kr.yank(), Some("d"));
assert_eq!(kr.yank_pop(), Some("c"));
assert_eq!(kr.yank_pop(), Some("b"));
assert_eq!(kr.yank_pop(), Some("d"));
}
#[test]
fn test_kill_ring_prepend() {
use yosh::interactive::kill_ring::KillRing;
let mut kr = KillRing::new(60);
kr.kill("world", false);
kr.prepend("hello ", true);
assert_eq!(kr.yank(), Some("hello world"));
}
#[test]
fn test_undo_save_and_restore() {
use yosh::interactive::undo::UndoManager;
let mut um = UndoManager::new(256);
um.save(&['h', 'e', 'l', 'l', 'o'], 5);
let (buf, pos) = um.undo().unwrap();
assert_eq!(buf, vec!['h', 'e', 'l', 'l', 'o']);
assert_eq!(pos, 5);
}
#[test]
fn test_undo_multiple_states() {
use yosh::interactive::undo::UndoManager;
let mut um = UndoManager::new(256);
um.save(&[], 0);
um.save(&['a'], 1);
let (buf, pos) = um.undo().unwrap();
assert_eq!(buf, vec!['a']);
assert_eq!(pos, 1);
let (buf, pos) = um.undo().unwrap();
assert_eq!(buf, vec![]);
assert_eq!(pos, 0);
assert!(um.undo().is_none());
}
#[test]
fn test_undo_empty_returns_none() {
use yosh::interactive::undo::UndoManager;
let mut um = UndoManager::new(256);
assert!(um.undo().is_none());
}
#[test]
fn test_undo_clear_resets_stack() {
use yosh::interactive::undo::UndoManager;
let mut um = UndoManager::new(256);
um.save(&['a'], 1);
um.save(&['a', 'b'], 2);
um.clear();
assert!(um.undo().is_none());
}
#[test]
fn test_undo_respects_max_size() {
use yosh::interactive::undo::UndoManager;
let mut um = UndoManager::new(2);
um.save(&[], 0);
um.save(&['a'], 1);
um.save(&['a', 'b'], 2); let (buf, _) = um.undo().unwrap();
assert_eq!(buf, vec!['a', 'b']);
let (buf, _) = um.undo().unwrap();
assert_eq!(buf, vec!['a']);
assert!(um.undo().is_none());
}
fn default_state() -> BufferState {
BufferState {
is_empty: false,
at_end: false,
has_suggestion: false,
last_action: EditAction::Noop,
}
}
fn key_event(code: KeyCode, modifiers: crossterm::event::KeyModifiers) -> crossterm::event::KeyEvent {
crossterm::event::KeyEvent::new(code, modifiers)
}
#[test]
fn test_keymap_ctrl_k() {
let mut km = Keymap::new();
let (action, count) = km.resolve(
key_event(KeyCode::Char('k'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(action, EditAction::KillToEnd);
assert_eq!(count, 1);
}
#[test]
fn test_keymap_ctrl_u() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('u'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(action, EditAction::KillToStart);
}
#[test]
fn test_keymap_ctrl_w() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('w'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(action, EditAction::KillBackwardWord);
}
#[test]
fn test_keymap_ctrl_y() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('y'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(action, EditAction::Yank);
}
#[test]
fn test_keymap_alt_y_after_yank() {
let mut km = Keymap::new();
let state = BufferState {
last_action: EditAction::Yank,
..default_state()
};
let (action, _) = km.resolve(
key_event(KeyCode::Char('y'), crossterm::event::KeyModifiers::ALT),
&state,
);
assert_eq!(action, EditAction::YankPop);
}
#[test]
fn test_keymap_alt_y_without_yank() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('y'), crossterm::event::KeyModifiers::ALT),
&default_state(),
);
assert_eq!(action, EditAction::Noop);
}
#[test]
fn test_keymap_alt_b() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('b'), crossterm::event::KeyModifiers::ALT),
&default_state(),
);
assert_eq!(action, EditAction::MoveBackwardWord);
}
#[test]
fn test_keymap_alt_d() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('d'), crossterm::event::KeyModifiers::ALT),
&default_state(),
);
assert_eq!(action, EditAction::KillForwardWord);
}
#[test]
fn test_keymap_ctrl_t() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('t'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(action, EditAction::TransposeChars);
}
#[test]
fn test_keymap_alt_t() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('t'), crossterm::event::KeyModifiers::ALT),
&default_state(),
);
assert_eq!(action, EditAction::TransposeWords);
}
#[test]
fn test_keymap_alt_u() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('u'), crossterm::event::KeyModifiers::ALT),
&default_state(),
);
assert_eq!(action, EditAction::UpcaseWord);
}
#[test]
fn test_keymap_alt_l() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('l'), crossterm::event::KeyModifiers::ALT),
&default_state(),
);
assert_eq!(action, EditAction::DowncaseWord);
}
#[test]
fn test_keymap_alt_c() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('c'), crossterm::event::KeyModifiers::ALT),
&default_state(),
);
assert_eq!(action, EditAction::CapitalizeWord);
}
#[test]
fn test_keymap_ctrl_underscore() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('_'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(action, EditAction::Undo);
}
#[test]
fn test_keymap_ctrl_l() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('l'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(action, EditAction::ClearScreen);
}
#[test]
fn test_keymap_ctrl_g_cancel() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('g'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(action, EditAction::Cancel);
}
#[test]
fn test_keymap_ctrl_d_empty_is_eof() {
let mut km = Keymap::new();
let state = BufferState { is_empty: true, ..default_state() };
let (action, _) = km.resolve(
key_event(KeyCode::Char('d'), crossterm::event::KeyModifiers::CONTROL),
&state,
);
assert_eq!(action, EditAction::Eof);
}
#[test]
fn test_keymap_ctrl_d_nonempty_is_delete() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('d'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(action, EditAction::DeleteForward);
}
#[test]
fn test_keymap_right_with_suggestion_accepts() {
let mut km = Keymap::new();
let state = BufferState {
at_end: true,
has_suggestion: true,
..default_state()
};
let (action, _) = km.resolve(
key_event(KeyCode::Right, crossterm::event::KeyModifiers::empty()),
&state,
);
assert_eq!(action, EditAction::AcceptSuggestion);
}
#[test]
fn test_keymap_right_without_suggestion_moves() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Right, crossterm::event::KeyModifiers::empty()),
&default_state(),
);
assert_eq!(action, EditAction::MoveForward);
}
#[test]
fn test_keymap_alt_f_with_suggestion_accepts_word() {
let mut km = Keymap::new();
let state = BufferState {
has_suggestion: true,
..default_state()
};
let (action, _) = km.resolve(
key_event(KeyCode::Char('f'), crossterm::event::KeyModifiers::ALT),
&state,
);
assert_eq!(action, EditAction::AcceptWordSuggestion);
}
#[test]
fn test_keymap_alt_f_without_suggestion_moves_word() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('f'), crossterm::event::KeyModifiers::ALT),
&default_state(),
);
assert_eq!(action, EditAction::MoveForwardWord);
}
#[test]
fn test_keymap_numeric_arg() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Char('3'), crossterm::event::KeyModifiers::ALT),
&default_state(),
);
assert_eq!(action, EditAction::SetNumericArg(3));
assert_eq!(km.pending_numeric_arg(), Some(3));
let (action, count) = km.resolve(
key_event(KeyCode::Char('f'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(action, EditAction::MoveForward);
assert_eq!(count, 3);
assert_eq!(km.pending_numeric_arg(), None);
}
#[test]
fn test_keymap_numeric_arg_multi_digit() {
let mut km = Keymap::new();
km.resolve(
key_event(KeyCode::Char('1'), crossterm::event::KeyModifiers::ALT),
&default_state(),
);
km.resolve(
key_event(KeyCode::Char('5'), crossterm::event::KeyModifiers::ALT),
&default_state(),
);
assert_eq!(km.pending_numeric_arg(), Some(15));
}
#[test]
fn test_keymap_ctrl_g_resets_numeric_arg() {
let mut km = Keymap::new();
km.resolve(
key_event(KeyCode::Char('5'), crossterm::event::KeyModifiers::ALT),
&default_state(),
);
assert_eq!(km.pending_numeric_arg(), Some(5));
let (action, _) = km.resolve(
key_event(KeyCode::Char('g'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(action, EditAction::Cancel);
assert_eq!(km.pending_numeric_arg(), None);
}
#[test]
fn test_keymap_existing_bindings_preserved() {
let mut km = Keymap::new();
let (a, _) = km.resolve(
key_event(KeyCode::Char('a'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(a, EditAction::MoveToStart);
let (a, _) = km.resolve(
key_event(KeyCode::Char('e'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(a, EditAction::MoveToEnd);
let (a, _) = km.resolve(
key_event(KeyCode::Char('b'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(a, EditAction::MoveBackward);
let (a, _) = km.resolve(
key_event(KeyCode::Enter, crossterm::event::KeyModifiers::empty()),
&default_state(),
);
assert_eq!(a, EditAction::Submit);
let (a, _) = km.resolve(
key_event(KeyCode::Char('c'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(a, EditAction::Interrupt);
let (a, _) = km.resolve(
key_event(KeyCode::Char('r'), crossterm::event::KeyModifiers::CONTROL),
&default_state(),
);
assert_eq!(a, EditAction::FuzzySearch);
}
#[test]
fn test_keymap_alt_backspace() {
let mut km = Keymap::new();
let (action, _) = km.resolve(
key_event(KeyCode::Backspace, crossterm::event::KeyModifiers::ALT),
&default_state(),
);
assert_eq!(action, EditAction::KillBackwardWord);
}
#[test]
fn test_move_backward_word() {
let mut ed = LineEditor::new();
for ch in "hello world".chars() { ed.insert_char(ch); }
ed.move_backward_word();
assert_eq!(ed.cursor(), 6);
ed.move_backward_word();
assert_eq!(ed.cursor(), 0);
ed.move_backward_word();
assert_eq!(ed.cursor(), 0);
}
#[test]
fn test_move_forward_word() {
let mut ed = LineEditor::new();
for ch in "hello world".chars() { ed.insert_char(ch); }
ed.move_to_start();
ed.move_forward_word();
assert_eq!(ed.cursor(), 5);
ed.move_forward_word();
assert_eq!(ed.cursor(), 11);
ed.move_forward_word();
assert_eq!(ed.cursor(), 11);
}
#[test]
fn test_move_backward_word_with_multiple_spaces() {
let mut ed = LineEditor::new();
for ch in "foo bar".chars() { ed.insert_char(ch); }
ed.move_backward_word();
assert_eq!(ed.cursor(), 6);
}
#[test]
fn test_move_forward_word_with_symbols() {
let mut ed = LineEditor::new();
for ch in "foo--bar".chars() { ed.insert_char(ch); }
ed.move_to_start();
ed.move_forward_word();
assert_eq!(ed.cursor(), 3);
ed.move_forward_word();
assert_eq!(ed.cursor(), 8);
}
#[test]
fn test_kill_to_end() {
let mut ed = LineEditor::new();
for ch in "hello world".chars() { ed.insert_char(ch); }
ed.move_to_start();
for _ in 0..5 { ed.move_cursor_right(); }
let killed = ed.kill_to_end();
assert_eq!(ed.buffer(), "hello");
assert_eq!(killed, " world");
}
#[test]
fn test_kill_to_start() {
let mut ed = LineEditor::new();
for ch in "hello world".chars() { ed.insert_char(ch); }
ed.move_to_start();
for _ in 0..5 { ed.move_cursor_right(); }
let killed = ed.kill_to_start();
assert_eq!(ed.buffer(), " world");
assert_eq!(ed.cursor(), 0);
assert_eq!(killed, "hello");
}
#[test]
fn test_kill_backward_word() {
let mut ed = LineEditor::new();
for ch in "hello world".chars() { ed.insert_char(ch); }
let killed = ed.kill_backward_word();
assert_eq!(ed.buffer(), "hello ");
assert_eq!(killed, "world");
}
#[test]
fn test_kill_forward_word() {
let mut ed = LineEditor::new();
for ch in "hello world".chars() { ed.insert_char(ch); }
ed.move_to_start();
let killed = ed.kill_forward_word();
assert_eq!(ed.buffer(), " world");
assert_eq!(killed, "hello");
}
#[test]
fn test_transpose_chars_middle() {
let mut ed = LineEditor::new();
for ch in "abc".chars() { ed.insert_char(ch); }
ed.move_to_start();
ed.move_cursor_right();
ed.transpose_chars();
assert_eq!(ed.buffer(), "bac");
assert_eq!(ed.cursor(), 2);
}
#[test]
fn test_transpose_chars_at_end() {
let mut ed = LineEditor::new();
for ch in "abc".chars() { ed.insert_char(ch); }
ed.transpose_chars();
assert_eq!(ed.buffer(), "acb");
assert_eq!(ed.cursor(), 3);
}
#[test]
fn test_transpose_chars_at_start_noop() {
let mut ed = LineEditor::new();
for ch in "abc".chars() { ed.insert_char(ch); }
ed.move_to_start();
ed.transpose_chars();
assert_eq!(ed.buffer(), "abc");
assert_eq!(ed.cursor(), 0);
}
#[test]
fn test_upcase_word() {
let mut ed = LineEditor::new();
for ch in "hello world".chars() { ed.insert_char(ch); }
ed.move_to_start();
ed.upcase_word();
assert_eq!(ed.buffer(), "HELLO world");
assert_eq!(ed.cursor(), 5);
}
#[test]
fn test_downcase_word() {
let mut ed = LineEditor::new();
for ch in "HELLO WORLD".chars() { ed.insert_char(ch); }
ed.move_to_start();
ed.downcase_word();
assert_eq!(ed.buffer(), "hello WORLD");
assert_eq!(ed.cursor(), 5);
}
#[test]
fn test_capitalize_word() {
let mut ed = LineEditor::new();
for ch in "hello world".chars() { ed.insert_char(ch); }
ed.move_to_start();
ed.capitalize_word();
assert_eq!(ed.buffer(), "Hello world");
assert_eq!(ed.cursor(), 5);
}
#[test]
fn test_transpose_words() {
let mut ed = LineEditor::new();
for ch in "hello world".chars() { ed.insert_char(ch); }
ed.transpose_words();
assert_eq!(ed.buffer(), "world hello");
assert_eq!(ed.cursor(), 11);
}
#[test]
fn test_transpose_words_cursor_in_middle() {
let mut ed = LineEditor::new();
for ch in "aaa bbb ccc".chars() { ed.insert_char(ch); }
ed.move_to_start();
for _ in 0..5 { ed.move_cursor_right(); }
ed.transpose_words();
assert_eq!(ed.buffer(), "bbb aaa ccc");
assert_eq!(ed.cursor(), 7);
}
#[test]
fn test_mock_ctrl_k_kills_to_end() {
let mut ed = LineEditor::new();
let mut history = History::new();
let events = [
chars("hello world"),
vec![ctrl('a')],
vec![key(KeyCode::Right); 5],
vec![ctrl('k')],
vec![key(KeyCode::Enter)],
].concat();
let mut term = MockTerminal::new(events);
let result = ed.read_line("$ ", &[], &mut history, &mut term);
assert_eq!(result.unwrap().unwrap(), "hello");
}
#[test]
fn test_mock_ctrl_u_kills_to_start() {
let mut ed = LineEditor::new();
let mut history = History::new();
let events = [
chars("hello world"),
vec![ctrl('a')],
vec![key(KeyCode::Right); 5],
vec![ctrl('u')],
vec![key(KeyCode::Enter)],
].concat();
let mut term = MockTerminal::new(events);
let result = ed.read_line("$ ", &[], &mut history, &mut term);
assert_eq!(result.unwrap().unwrap(), " world");
}
#[test]
fn test_mock_ctrl_w_kills_backward_word() {
let mut ed = LineEditor::new();
let mut history = History::new();
let events = [
chars("hello world"),
vec![ctrl('w')],
vec![key(KeyCode::Enter)],
].concat();
let mut term = MockTerminal::new(events);
let result = ed.read_line("$ ", &[], &mut history, &mut term);
assert_eq!(result.unwrap().unwrap(), "hello ");
}
#[test]
fn test_mock_ctrl_y_yanks() {
let mut ed = LineEditor::new();
let mut history = History::new();
let events = [
chars("hello world"),
vec![ctrl('w')],
vec![ctrl('a')],
vec![ctrl('y')],
vec![key(KeyCode::Enter)],
].concat();
let mut term = MockTerminal::new(events);
let result = ed.read_line("$ ", &[], &mut history, &mut term);
assert_eq!(result.unwrap().unwrap(), "worldhello ");
}
#[test]
fn test_mock_ctrl_underscore_undo() {
let mut ed = LineEditor::new();
let mut history = History::new();
let events = [
chars("hello"),
vec![ctrl('a')],
vec![ctrl('k')],
vec![ctrl('_')],
vec![key(KeyCode::Enter)],
].concat();
let mut term = MockTerminal::new(events);
let result = ed.read_line("$ ", &[], &mut history, &mut term);
assert_eq!(result.unwrap().unwrap(), "hello");
}
#[test]
fn test_mock_alt_b_word_backward() {
let mut ed = LineEditor::new();
let mut history = History::new();
let events = [
chars("hello world"),
vec![alt('b')],
vec![ctrl('k')],
vec![key(KeyCode::Enter)],
].concat();
let mut term = MockTerminal::new(events);
let result = ed.read_line("$ ", &[], &mut history, &mut term);
assert_eq!(result.unwrap().unwrap(), "hello ");
}
#[test]
fn test_mock_ctrl_l_clears_screen() {
let mut ed = LineEditor::new();
let mut history = History::new();
let events = [
chars("test"),
vec![ctrl('l')],
vec![key(KeyCode::Enter)],
].concat();
let mut term = MockTerminal::new(events);
let result = ed.read_line("$ ", &[], &mut history, &mut term);
assert_eq!(result.unwrap().unwrap(), "test");
}
#[test]
fn test_mock_ctrl_t_transpose() {
let mut ed = LineEditor::new();
let mut history = History::new();
let events = [
chars("ab"),
vec![ctrl('t')],
vec![key(KeyCode::Enter)],
].concat();
let mut term = MockTerminal::new(events);
let result = ed.read_line("$ ", &[], &mut history, &mut term);
assert_eq!(result.unwrap().unwrap(), "ba");
}
#[test]
fn test_mock_alt_u_upcase() {
let mut ed = LineEditor::new();
let mut history = History::new();
let events = [
chars("hello"),
vec![ctrl('a')],
vec![alt('u')],
vec![key(KeyCode::Enter)],
].concat();
let mut term = MockTerminal::new(events);
let result = ed.read_line("$ ", &[], &mut history, &mut term);
assert_eq!(result.unwrap().unwrap(), "HELLO");
}
#[test]
fn test_mock_numeric_arg_movement() {
let mut ed = LineEditor::new();
let mut history = History::new();
let events = [
chars("abcdef"),
vec![ctrl('a')],
vec![alt('3')],
vec![ctrl('f')],
vec![ctrl('k')],
vec![key(KeyCode::Enter)],
].concat();
let mut term = MockTerminal::new(events);
let result = ed.read_line("$ ", &[], &mut history, &mut term);
assert_eq!(result.unwrap().unwrap(), "abc");
}