392 lines
13 KiB
Rust
392 lines
13 KiB
Rust
use std::{
|
|
fs,
|
|
path::PathBuf,
|
|
process::Command,
|
|
sync::atomic::{AtomicU64, Ordering},
|
|
thread,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use vamp::{
|
|
diff::{DisplayRow, Focus, Phase},
|
|
editor::{Cursor, Editor, Input, Mode, Selection},
|
|
session::EditorSession,
|
|
};
|
|
|
|
static NEXT: AtomicU64 = AtomicU64::new(0);
|
|
|
|
struct Fixture(PathBuf);
|
|
|
|
impl Fixture {
|
|
fn new() -> Self {
|
|
let root = std::env::temp_dir().join(format!(
|
|
"vamp-navigation-{}-{}",
|
|
std::process::id(),
|
|
NEXT.fetch_add(1, Ordering::Relaxed),
|
|
));
|
|
fs::create_dir_all(&root).unwrap();
|
|
Self(root)
|
|
}
|
|
|
|
fn file(&self, name: &str, text: &str) -> PathBuf {
|
|
let path = self.0.join(name);
|
|
fs::write(&path, text).unwrap();
|
|
path
|
|
}
|
|
|
|
fn editor(&self, text: &str) -> Editor {
|
|
let mut editor =
|
|
Editor::open(Some(self.file("buffer.txt", text)), vec!["text".into()]).unwrap();
|
|
editor.set_clipboard_mode("separate").unwrap();
|
|
editor
|
|
}
|
|
|
|
fn session(&self, text: &str, bindings: &str) -> EditorSession {
|
|
let source = self.file("buffer.txt", text);
|
|
let config = self.file(
|
|
"config.rb",
|
|
&format!(
|
|
"Vamp.configure do |config|\n config.clipboard_mode = 'separate'\n{bindings}\nend\n"
|
|
),
|
|
);
|
|
let session =
|
|
EditorSession::open(Some(source), vec!["text".into()], Some(&config)).unwrap();
|
|
assert!(session.editor().startup_diagnostic().is_none());
|
|
session
|
|
}
|
|
|
|
fn git(&self, args: &[&str]) -> String {
|
|
let output = Command::new("git")
|
|
.args(args)
|
|
.current_dir(self.0.join("repo"))
|
|
.env("GIT_CONFIG_NOSYSTEM", "1")
|
|
.env("GIT_CONFIG_GLOBAL", "/dev/null")
|
|
.env("GIT_AUTHOR_NAME", "Navigation Fixture")
|
|
.env("GIT_AUTHOR_EMAIL", "fixture@example.invalid")
|
|
.env("GIT_COMMITTER_NAME", "Navigation Fixture")
|
|
.env("GIT_COMMITTER_EMAIL", "fixture@example.invalid")
|
|
.env("GIT_AUTHOR_DATE", "2000-01-01T00:00:00Z")
|
|
.env("GIT_COMMITTER_DATE", "2000-01-01T00:00:00Z")
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
output.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
String::from_utf8(output.stdout).unwrap().trim().to_owned()
|
|
}
|
|
}
|
|
|
|
impl Drop for Fixture {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
|
|
fn keys(editor: &mut Editor, text: &str) {
|
|
for ch in text.chars() {
|
|
editor.handle(Input::Char(ch));
|
|
}
|
|
}
|
|
|
|
fn session_keys(session: &mut EditorSession, text: &str) {
|
|
for ch in text.chars() {
|
|
session.handle(Input::Char(ch));
|
|
}
|
|
}
|
|
|
|
fn palette_seek(session: &mut EditorSession, action: &str, target: char) {
|
|
session.handle(Input::Ctrl('p'));
|
|
session_keys(session, action);
|
|
session.handle(Input::Enter);
|
|
assert!(session.editor().palette().is_none());
|
|
session.handle(Input::Char(target));
|
|
}
|
|
|
|
fn wait_until(session: &mut EditorSession, ready: impl Fn(&EditorSession) -> bool) {
|
|
let deadline = Instant::now() + Duration::from_secs(10);
|
|
while !ready(session) {
|
|
session.poll();
|
|
assert!(
|
|
Instant::now() < deadline,
|
|
"diff state: {:?}",
|
|
session.editor().diff()
|
|
);
|
|
if let Some(diff) = session.editor().diff() {
|
|
assert!(diff.error.is_none(), "{:?}", diff.error);
|
|
}
|
|
thread::sleep(Duration::from_millis(1));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn counted_character_seeks_move_every_native_caret_independently() {
|
|
let fixture = Fixture::new();
|
|
let text = "xaxbx tail\nx---x----x tail\nx only\n";
|
|
let mut editor = fixture.editor(text);
|
|
editor.handle(Input::Ctrl('e'));
|
|
editor.handle(Input::Ctrl('e'));
|
|
assert_eq!(editor.selection_count(), 3);
|
|
|
|
keys(&mut editor, "2fx");
|
|
assert_eq!(
|
|
editor.carets(),
|
|
[
|
|
Cursor { row: 0, column: 4 },
|
|
Cursor { row: 1, column: 9 },
|
|
Cursor { row: 2, column: 0 },
|
|
]
|
|
);
|
|
keys(&mut editor, "2Fx");
|
|
assert_eq!(
|
|
editor.carets(),
|
|
[
|
|
Cursor { row: 0, column: 0 },
|
|
Cursor { row: 1, column: 0 },
|
|
Cursor { row: 2, column: 0 },
|
|
]
|
|
);
|
|
assert_eq!(editor.context().text, text);
|
|
assert!(!editor.dirty());
|
|
}
|
|
|
|
#[test]
|
|
fn visual_character_and_line_seeks_yank_the_selected_text_into_another_buffer() {
|
|
for (mode, expected) in [('v', "a x x"), ('V', "a x xx tail\n")] {
|
|
let fixture = Fixture::new();
|
|
let mut editor = fixture.editor("a x xx tail\n");
|
|
editor.handle(Input::Char(mode));
|
|
keys(&mut editor, "2fx");
|
|
assert_eq!(editor.mode(), Mode::Visual);
|
|
assert_eq!(editor.cursor(), Cursor { row: 0, column: 4 });
|
|
assert_eq!(editor.visual_linewise(), mode == 'V');
|
|
assert_eq!(
|
|
editor.selections(),
|
|
[Selection {
|
|
start: Cursor { row: 0, column: 0 },
|
|
end: if mode == 'V' {
|
|
Cursor { row: 1, column: 0 }
|
|
} else {
|
|
Cursor { row: 0, column: 5 }
|
|
},
|
|
}]
|
|
);
|
|
editor.handle(Input::Char('y'));
|
|
assert_eq!(editor.mode(), Mode::Normal);
|
|
assert!(!editor.dirty());
|
|
|
|
let destination = fixture.file("destination.txt", "");
|
|
editor.execute(&format!("edit {}", destination.display()));
|
|
editor.handle(Input::Char('P'));
|
|
assert_eq!(editor.context().text, expected, "{mode}");
|
|
assert_eq!(
|
|
fs::read_to_string(fixture.0.join("buffer.txt")).unwrap(),
|
|
"a x xx tail\n"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn typed_ruby_seek_remaps_and_palette_actions_preserve_normal_and_visual_navigation() {
|
|
let fixture = Fixture::new();
|
|
let text = "a x x tail\n";
|
|
let mut session = fixture.session(
|
|
text,
|
|
r#" config.bind 'leader s', config.core.seek_forward
|
|
config.bind 'leader r', config.core.seek_backward
|
|
config.bind 'leader s', config.core.seek_forward, mode: config.modes.visual
|
|
config.bind 'leader r', config.core.seek_backward, mode: config.modes.visual"#,
|
|
);
|
|
session_keys(&mut session, "2 sx");
|
|
assert_eq!(session.editor().cursor(), Cursor { row: 0, column: 4 });
|
|
session_keys(&mut session, " rx");
|
|
assert_eq!(session.editor().cursor(), Cursor { row: 0, column: 2 });
|
|
|
|
session_keys(&mut session, "v sx");
|
|
let expanded = [Selection {
|
|
start: Cursor { row: 0, column: 2 },
|
|
end: Cursor { row: 0, column: 5 },
|
|
}];
|
|
assert_eq!(session.editor().selections(), expanded);
|
|
session_keys(&mut session, " rx");
|
|
let contracted = [Selection {
|
|
start: Cursor { row: 0, column: 2 },
|
|
end: Cursor { row: 0, column: 3 },
|
|
}];
|
|
assert_eq!(session.editor().selections(), contracted);
|
|
palette_seek(&mut session, "core.seek_forward", 'x');
|
|
assert_eq!(session.editor().mode(), Mode::Visual);
|
|
assert_eq!(session.editor().selections(), expanded);
|
|
palette_seek(&mut session, "core.seek_backward", 'x');
|
|
assert_eq!(session.editor().selections(), contracted);
|
|
|
|
session.handle(Input::Escape);
|
|
palette_seek(&mut session, "core.seek_forward", 'x');
|
|
assert_eq!(session.editor().cursor(), Cursor { row: 0, column: 4 });
|
|
palette_seek(&mut session, "core.seek_backward", 'x');
|
|
assert_eq!(session.editor().cursor(), Cursor { row: 0, column: 2 });
|
|
assert_eq!(session.editor().mode(), Mode::Normal);
|
|
assert_eq!(session.editor().context().text, text);
|
|
assert!(!session.editor().dirty());
|
|
}
|
|
|
|
#[test]
|
|
fn repeating_a_visual_seek_change_resolves_the_counted_character_on_the_new_line() {
|
|
let fixture = Fixture::new();
|
|
let mut session = fixture.session(
|
|
"a x x tail\nlong x gap x tail\n",
|
|
" config.bind 'leader s', config.core.seek_forward, mode: config.modes.visual",
|
|
);
|
|
session_keys(&mut session, "v2 sxc!");
|
|
session.handle(Input::Escape);
|
|
assert_eq!(
|
|
session.editor().context().text,
|
|
"! tail\nlong x gap x tail\n"
|
|
);
|
|
|
|
session_keys(&mut session, "j0.");
|
|
assert_eq!(session.editor().context().text, "! tail\n! tail\n");
|
|
assert_eq!(session.editor().mode(), Mode::Normal);
|
|
session.handle(Input::Char('u'));
|
|
assert_eq!(
|
|
session.editor().context().text,
|
|
"! tail\nlong x gap x tail\n"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn readonly_before_and_historical_after_seeks_preserve_snapshots_and_folded_excerpts() {
|
|
let fixture = Fixture::new();
|
|
fs::create_dir_all(fixture.0.join("repo")).unwrap();
|
|
fixture.git(&["init", "-q"]);
|
|
let source = fixture.0.join("repo/context.txt");
|
|
let mut lines = (1..=40)
|
|
.map(|row| format!("stable_{row:03} [x] [x]"))
|
|
.collect::<Vec<_>>();
|
|
lines[19] = "before x x tail".into();
|
|
let before_text = lines.join("\n") + "\n";
|
|
fs::write(&source, &before_text).unwrap();
|
|
fixture.git(&["add", "context.txt"]);
|
|
fixture.git(&["commit", "-qm", "before"]);
|
|
let before = fixture.git(&["rev-parse", "HEAD"]);
|
|
lines[19] = "after x x tail".into();
|
|
let after_text = lines.join("\n") + "\n";
|
|
fs::write(&source, &after_text).unwrap();
|
|
fixture.git(&["commit", "-qam", "after"]);
|
|
let after = fixture.git(&["rev-parse", "HEAD"]);
|
|
fixture.git(&["checkout", "--detach", &before]);
|
|
lines[19] = "working x x tail".into();
|
|
let working_text = lines.join("\n") + "\n";
|
|
fs::write(&source, &working_text).unwrap();
|
|
let config = fixture.file(
|
|
"config.rb",
|
|
"Vamp.configure do |config|\n config.clipboard_mode = 'separate'\n config.diff_context_lines = 1\n config.diff_expand_lines = 2\nend\n",
|
|
);
|
|
let mut session =
|
|
EditorSession::open(Some(source.clone()), vec!["text".into()], Some(&config)).unwrap();
|
|
assert!(session.editor().startup_diagnostic().is_none());
|
|
session_keys(&mut session, "iunsaved ");
|
|
session.handle(Input::Escape);
|
|
let unsaved_text = session.editor().context().text;
|
|
let original_cursor = session.editor().cursor();
|
|
|
|
session.execute("diff");
|
|
wait_until(&mut session, |session| {
|
|
session
|
|
.editor()
|
|
.diff()
|
|
.is_some_and(|diff| diff.catalog.is_some() && !diff.loading)
|
|
});
|
|
for revision in [&before, &after] {
|
|
session_keys(&mut session, revision);
|
|
session.handle(Input::Down);
|
|
session.handle(Input::Enter);
|
|
}
|
|
wait_until(&mut session, |session| {
|
|
session
|
|
.editor()
|
|
.diff()
|
|
.is_some_and(|diff| diff.phase == Phase::View && diff.selected_file_loaded())
|
|
});
|
|
let excerpts = session.editor().diff().unwrap().display_rows.clone();
|
|
assert!(
|
|
excerpts
|
|
.iter()
|
|
.any(|row| matches!(row, DisplayRow::Fold { .. }))
|
|
);
|
|
|
|
for (focus, text, first_x, second_x) in [
|
|
(Focus::Before, &before_text, 7, 9),
|
|
(Focus::After, &after_text, 6, 8),
|
|
] {
|
|
for _ in 0..3 {
|
|
if session.editor().diff().unwrap().focus == focus {
|
|
break;
|
|
}
|
|
session.handle(Input::Char('\t'));
|
|
}
|
|
assert_eq!(session.editor().diff().unwrap().focus, focus);
|
|
let revision = session.editor().text_revision();
|
|
session_keys(&mut session, "gt02f[");
|
|
assert_eq!(
|
|
session.editor().cursor(),
|
|
Cursor {
|
|
row: 18,
|
|
column: 15
|
|
}
|
|
);
|
|
session_keys(&mut session, "F[f]");
|
|
assert_eq!(
|
|
session.editor().cursor(),
|
|
Cursor {
|
|
row: 18,
|
|
column: 13
|
|
}
|
|
);
|
|
session_keys(&mut session, "j02fx");
|
|
assert_eq!(
|
|
session.editor().cursor(),
|
|
Cursor {
|
|
row: 19,
|
|
column: second_x
|
|
}
|
|
);
|
|
session_keys(&mut session, "Fxvfx");
|
|
assert_eq!(
|
|
session.editor().selections(),
|
|
[Selection {
|
|
start: Cursor {
|
|
row: 19,
|
|
column: first_x
|
|
},
|
|
end: Cursor {
|
|
row: 19,
|
|
column: second_x + 1
|
|
},
|
|
}]
|
|
);
|
|
session_keys(&mut session, "yVfx");
|
|
assert!(session.editor().visual_linewise());
|
|
assert_eq!(
|
|
session.editor().cursor(),
|
|
Cursor {
|
|
row: 19,
|
|
column: second_x
|
|
}
|
|
);
|
|
session.handle(Input::Escape);
|
|
assert_eq!(session.editor().context().text, *text);
|
|
assert_eq!(session.editor().text_revision(), revision);
|
|
assert!(!session.editor().dirty());
|
|
assert_eq!(session.editor().diff().unwrap().display_rows, excerpts);
|
|
}
|
|
|
|
session.execute("diff.close");
|
|
assert_eq!(session.editor().context().text, unsaved_text);
|
|
assert_eq!(session.editor().cursor(), original_cursor);
|
|
assert!(session.editor().dirty());
|
|
assert_eq!(fs::read_to_string(&source).unwrap(), working_text);
|
|
assert_eq!(fixture.git(&["rev-parse", "HEAD"]), before);
|
|
}
|