use super::*;
#[test]
fn a_save_asks_before_writing_over_changes_made_elsewhere() {
let path = std::env::temp_dir().join(format!("fm84-disk-changed-{}.txt", std::process::id()));
std::fs::write(&path, "one\n").unwrap();
let mut app_state = AppState::new();
app_state.open_editor(path.clone()).unwrap();
let ctrl_s = |app_state: &mut AppState| handle_key(app_state, KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL));
let asked = |app_state: &AppState| matches!(app_state.dialog, Some(Dialog::DiskChanged { .. }));
press(&mut app_state, KeyCode::Char('A'));
ctrl_s(&mut app_state);
press(&mut app_state, KeyCode::Char('B'));
ctrl_s(&mut app_state);
assert!(!asked(&app_state));
assert_eq!(std::fs::read_to_string(&path).unwrap(), "ABone\n");
std::fs::write(&path, "written elsewhere\n").unwrap();
press(&mut app_state, KeyCode::Char('C'));
ctrl_s(&mut app_state);
assert!(asked(&app_state));
assert_eq!(std::fs::read_to_string(&path).unwrap(), "written elsewhere\n");
press(&mut app_state, KeyCode::Esc);
assert!(app_state.dialog.is_none() && app_state.editor_is_modified());
press(&mut app_state, KeyCode::F(2));
assert!(asked(&app_state));
press(&mut app_state, KeyCode::Char('o'));
assert_eq!(std::fs::read_to_string(&path).unwrap(), "ABCone\n");
press(&mut app_state, KeyCode::Char('D'));
ctrl_s(&mut app_state);
assert!(!asked(&app_state));
std::fs::write(&path, "theirs\nand more\n").unwrap();
press(&mut app_state, KeyCode::Char('E'));
ctrl_s(&mut app_state);
press(&mut app_state, KeyCode::Char('r'));
let state = app_state.editor().unwrap();
assert_eq!(state.lines, ["theirs", "and more", ""]);
assert!(!state.modified);
std::fs::write(&path, "again elsewhere\n").unwrap();
press(&mut app_state, KeyCode::Char('F'));
press(&mut app_state, KeyCode::Esc);
press(&mut app_state, KeyCode::Char('y'));
assert!(asked(&app_state) && app_state.is_editing());
press(&mut app_state, KeyCode::Char('o'));
assert!(!app_state.is_editing());
assert_eq!(std::fs::read_to_string(&path).unwrap(), "Ftheirs\nand more\n");
app_state.open_editor(path.clone()).unwrap();
std::fs::remove_file(&path).unwrap();
press(&mut app_state, KeyCode::Char('G'));
ctrl_s(&mut app_state);
assert!(matches!(app_state.dialog, Some(Dialog::DiskChanged { now: None, .. })));
press(&mut app_state, KeyCode::Char('r'));
assert!(asked(&app_state), "nothing there to reload");
press(&mut app_state, KeyCode::Char('o'));
assert_eq!(std::fs::read_to_string(&path).unwrap(), "GFtheirs\nand more\n");
std::fs::remove_file(&path).unwrap();
}
#[test]
fn replace_is_driven_from_the_keys() {
let path = std::env::temp_dir().join(format!("fm84-replace-keys-{}.txt", std::process::id()));
std::fs::write(&path, "cat cat cat").unwrap();
let mut app_state = AppState::new();
app_state.open_editor(path.clone()).unwrap();
let answer = |app_state: &mut AppState, text: &str| {
app_state.dialog_input().unwrap().set(text.to_string());
press(app_state, KeyCode::Enter);
};
handle_key(&mut app_state, KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
answer(&mut app_state, "cat");
answer(&mut app_state, "dog");
press(&mut app_state, KeyCode::Enter);
assert!(matches!(app_state.dialog, Some(Dialog::Replacing { replaced: 0, .. })), "Enter is not an answer");
press(&mut app_state, KeyCode::Char('y'));
press(&mut app_state, KeyCode::Esc);
assert!(app_state.dialog.is_none());
assert_eq!(app_state.editor().unwrap().lines, ["dog cat cat"]);
assert_eq!(app_state.find_note.as_deref(), Some("Replaced 1"));
press(&mut app_state, KeyCode::F(4));
assert_eq!(app_state.dialog_input().unwrap().text, "cat");
press(&mut app_state, KeyCode::Enter);
assert_eq!(app_state.dialog_input().unwrap().text, "dog");
press(&mut app_state, KeyCode::Enter);
press(&mut app_state, KeyCode::Char('a'));
assert_eq!(app_state.editor().unwrap().lines, ["dog dog dog"]);
std::fs::remove_file(path).unwrap();
}
fn editing(name: &str, text: &str) -> (AppState, std::path::PathBuf) {
let path = std::env::temp_dir().join(format!("fm84-lines-{}-{name}", std::process::id()));
std::fs::write(&path, text).unwrap();
let mut app_state = AppState::new();
app_state.options = crate::options::Options::default();
app_state.open_editor(path.clone()).unwrap();
app_state.drawn.editor_viewport_height = 20;
(app_state, path)
}
fn put(app_state: &mut AppState, line: usize, col: usize) {
let state = app_state.editor_mut().unwrap();
state.selection_anchor = None;
state.cursor_line = line;
state.cursor_col = col;
}
fn lines(app_state: &AppState) -> Vec<String> {
app_state.editor().unwrap().lines.clone()
}
#[test]
fn enter_keeps_the_indent() {
let (mut app_state, path) = editing("indent.rs", "fn main() {\n let x = 1;\n\tif x {\n}\n");
put(&mut app_state, 1, 14);
press(&mut app_state, KeyCode::Enter);
assert_eq!(lines(&app_state)[2], " ");
assert_eq!(app_state.editor().unwrap().cursor_col, 4);
press(&mut app_state, KeyCode::Enter);
assert_eq!(&lines(&app_state)[1..4], [" let x = 1;", "", " "]);
put(&mut app_state, 4, 6);
press(&mut app_state, KeyCode::Enter);
assert_eq!(&lines(&app_state)[4..6], ["\tif x ", "\t{"]);
put(&mut app_state, 1, 2);
press(&mut app_state, KeyCode::Enter);
assert_eq!(&lines(&app_state)[1..3], ["", " let x = 1;"]);
handle_key(&mut app_state, KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL));
assert_eq!(&lines(&app_state)[1..3], [" let x = 1;", ""]);
std::fs::remove_file(path).unwrap();
}
#[test]
fn tab_and_shift_tab_indent_the_lines_selected() {
let (mut app_state, path) = editing("tabs.py", "def f():\nreturn 1\n\nx = 2\ny = 3\n");
let state = app_state.editor_mut().unwrap();
state.selection_anchor = Some((1, 3));
state.cursor_line = 4;
state.cursor_col = 0;
press(&mut app_state, KeyCode::Tab);
assert_eq!(lines(&app_state), ["def f():", " return 1", "", " x = 2", "y = 3", ""]);
assert_eq!(app_state.editor().unwrap().selection(), Some(((1, 0), (3, 9))));
press(&mut app_state, KeyCode::Tab);
assert_eq!(lines(&app_state)[1], " return 1");
handle_key(&mut app_state, KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT));
assert_eq!(lines(&app_state)[3], " x = 2");
put(&mut app_state, 1, 6);
handle_key(&mut app_state, KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT));
assert_eq!(lines(&app_state)[1], "return 1");
assert_eq!(app_state.editor().unwrap().cursor_col, 2);
put(&mut app_state, 4, 0);
press(&mut app_state, KeyCode::Tab);
assert_eq!(lines(&app_state)[4], "\ty = 3");
std::fs::remove_file(path).unwrap();
let (mut app_state, path) = editing("tabs.go", "func f() {\n\treturn\n}\n");
let state = app_state.editor_mut().unwrap();
state.selection_anchor = Some((0, 0));
state.cursor_line = 2;
state.cursor_col = 1;
press(&mut app_state, KeyCode::Tab);
assert_eq!(lines(&app_state), ["\tfunc f() {", "\t\treturn", "\t}", ""]);
std::fs::remove_file(path).unwrap();
}
#[test]
fn ctrl_slash_comments_and_uncomments() {
let (mut app_state, path) = editing("comment.rs", "fn f() {\n one();\n\n two();\n}\n");
let state = app_state.editor_mut().unwrap();
state.selection_anchor = Some((1, 2));
state.cursor_line = 3;
state.cursor_col = 4;
handle_key(&mut app_state, KeyEvent::new(KeyCode::Char('7'), KeyModifiers::CONTROL));
assert_eq!(&lines(&app_state)[1..4], [" // one();", "", " // two();"]);
handle_key(&mut app_state, KeyEvent::new(KeyCode::Char('/'), KeyModifiers::CONTROL));
assert_eq!(&lines(&app_state)[1..4], [" one();", "", " two();"]);
put(&mut app_state, 1, 4);
handle_key(&mut app_state, KeyEvent::new(KeyCode::Char('/'), KeyModifiers::CONTROL));
assert_eq!(lines(&app_state)[1], " // one();");
assert_eq!(app_state.editor().unwrap().cursor_col, 7, "the cursor stays on the code");
std::fs::remove_file(path).unwrap();
for (name, mark) in [("x.py", "#"), ("x.lua", "--"), ("x.ini", ";"), ("Makefile", "#")] {
let (mut app_state, path) = editing(name, "a\n");
handle_key(&mut app_state, KeyEvent::new(KeyCode::Char('7'), KeyModifiers::CONTROL));
assert_eq!(lines(&app_state)[0], format!("{mark} a"), "{name}");
std::fs::remove_file(path).unwrap();
}
let (mut app_state, path) = editing("nbsp.py", "\u{a0}# a\n");
handle_key(&mut app_state, KeyEvent::new(KeyCode::Char('7'), KeyModifiers::CONTROL));
assert_eq!(lines(&app_state)[0], "# \u{a0}# a");
std::fs::remove_file(path).unwrap();
}
#[test]
fn find_and_replace_by_regular_expression() {
let (mut app_state, path) = editing("regex.txt", "width = 10\nheight = 200\nname = x\n");
let ctrl = |app_state: &mut AppState, c: char| handle_key(app_state, KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL));
let typed = |app_state: &mut AppState, text: &str| {
let input = app_state.dialog_input().unwrap();
input.set(text.to_string());
press(app_state, KeyCode::Enter);
};
ctrl(&mut app_state, 'f');
handle_key(&mut app_state, KeyEvent::new(KeyCode::Char('r'), KeyModifiers::ALT));
assert!(app_state.recall.find_regex);
typed(&mut app_state, r"\d{3}");
assert_eq!(app_state.editor().unwrap().selection(), Some(((1, 9), (1, 12))));
ctrl(&mut app_state, 'f');
typed(&mut app_state, "(unclosed");
assert!(app_state.error.as_deref().is_some_and(|error| error.starts_with("Not a regular expression")));
assert!(matches!(app_state.dialog, Some(Dialog::Prompt(PromptKind::Find, _))));
app_state.error = None;
press(&mut app_state, KeyCode::Esc);
put(&mut app_state, 0, 0);
ctrl(&mut app_state, 'r');
typed(&mut app_state, r"^(\w+) = (\d+)$");
typed(&mut app_state, "$2 is $1");
press(&mut app_state, KeyCode::Char('a'));
assert_eq!(lines(&app_state), ["10 is width", "200 is height", "name = x", ""]);
ctrl(&mut app_state, 'z');
assert_eq!(lines(&app_state)[0], "width = 10");
ctrl(&mut app_state, 'f');
handle_key(&mut app_state, KeyEvent::new(KeyCode::Char('r'), KeyModifiers::ALT));
typed(&mut app_state, r"\d{3}");
assert!(app_state.find_note.as_deref().is_some_and(|note| note.starts_with("Not found")));
std::fs::remove_file(path).unwrap();
let (mut app_state, path) = editing("boundary.txt", "concat cat\n");
ctrl(&mut app_state, 'r');
handle_key(&mut app_state, KeyEvent::new(KeyCode::Char('r'), KeyModifiers::ALT));
typed(&mut app_state, r"\Bcat");
typed(&mut app_state, "[$0]");
press(&mut app_state, KeyCode::Char('a'));
assert_eq!(lines(&app_state)[0], "con[cat] cat");
std::fs::remove_file(path).unwrap();
}