use super::super::ChangeProducer;
use super::*;
use std::path::PathBuf;
use strop_core::id::LineIndex;
use strop_lsp::{PositionEncoding, ServerColumn, ServerEdit, ServerId, ServerPosition};
use strop_workspace::ResourceLocation;
fn at(line: usize, col: usize) -> ServerPosition {
ServerPosition {
line: LineIndex::new(line),
column: ServerColumn::new(col),
}
}
fn edit(sl: usize, sc: usize, el: usize, ec: usize, text: &str) -> ServerEdit {
ServerEdit {
start: at(sl, sc),
end: at(el, ec),
new_text: text.into(),
}
}
fn file_editor(dir: &tempfile::TempDir, name: &str, text: &str) -> (Editor, DocumentId) {
std::fs::write(dir.path().join(name), text).unwrap();
let mut e = Editor::new_in(Buffer::from_text("scratch\n"), dir.path().to_path_buf());
e.open_fixture(&dir.path().join(name)).unwrap();
let document = e.current();
(e, document)
}
fn open_second(e: &mut Editor, dir: &tempfile::TempDir, name: &str, text: &str) -> DocumentId {
std::fs::write(dir.path().join(name), text).unwrap();
e.open_fixture(&dir.path().join(name)).unwrap();
e.current()
}
fn bind(e: &mut Editor, document: DocumentId) {
let (revision, path) = {
let doc = e.docs.get(document).unwrap();
(doc.buf.revision(), doc.buf.path.clone().unwrap())
};
e.lsp_state.bindings.insert(
document,
crate::editor::lsp::state::Binding {
server: ServerId::new(7),
revision,
path,
root: PathBuf::from("/workspace"),
language: "rust".into(),
target: strop_workspace::Filesystem::Local,
},
);
}
fn text_of(e: &Editor, document: DocumentId) -> String {
e.docs.get(document).unwrap().buf.text().to_string()
}
fn two_file_editor() -> (Editor, tempfile::TempDir, DocumentId, DocumentId) {
let dir = tempfile::tempdir().unwrap();
let (mut e, first) = file_editor(&dir, "a.txt", "alpha here\n");
let second = open_second(&mut e, &dir, "b.txt", "alpha there\n");
bind(&mut e, first);
bind(&mut e, second);
let plan = e.build_change_plan(
ChangeProducer::Rename,
vec![
(
ResourceLocation::local(dir.path().join("a.txt")),
vec![edit(0, 0, 0, 5, "omega")],
),
(
ResourceLocation::local(dir.path().join("b.txt")),
vec![edit(0, 0, 0, 5, "omega")],
),
(
ResourceLocation::local(dir.path().join("ghost.txt")),
vec![edit(0, 0, 0, 3, "x")],
),
],
PositionEncoding::Utf8,
);
e.present_change_plan(plan);
(e, dir, first, second)
}
#[test]
fn multi_target_plan_opens_a_diff_review_without_mutating() {
let (e, _dir, first, _second) = two_file_editor();
assert_eq!(e.buf().name.as_deref(), Some("change proposal 1"));
let review = e.current();
assert_ne!(review, first, "the review buffer takes focus");
assert!(e.buf().readonly, "the review is a real read-only buffer");
let text = e.buf().text().to_string();
assert!(text.contains("strop change proposal 1: rename"), "{text}");
assert!(text.contains(APPLY_COMMAND), "{text}");
assert!(text.contains(CANCEL_COMMAND), "{text}");
assert!(text.contains("--- a/"), "{text}");
assert!(text.contains("+++ b/"), "{text}");
assert!(text.contains("@@ -1,1 +1,1 @@"), "{text}");
assert!(text.contains("-alpha here"), "{text}");
assert!(text.contains("+omega here"), "{text}");
assert!(text.contains("-alpha there"), "{text}");
assert!(text.contains("+omega there"), "{text}");
assert!(text.contains("ghost.txt"), "{text}");
assert!(text.contains("not open on the server"), "{text}");
assert_eq!(text_of(&e, first), "alpha here\n");
assert!(e.message.contains("proposal 1"), "{}", e.message);
}
#[test]
fn apply_mutates_exactly_the_planned_sources_and_leaves_a_receipt() {
let (mut e, _dir, first, second) = two_file_editor();
let review = e.current();
e.review_apply_pub();
assert_eq!(text_of(&e, first), "omega here\n");
assert_eq!(text_of(&e, second), "omega there\n");
let receipt = text_of(&e, review);
assert!(receipt.contains("applied: "), "{receipt}");
assert!(receipt.contains("a.txt"), "{receipt}");
assert!(receipt.contains("b.txt"), "{receipt}");
assert!(receipt.contains("refused: "), "{receipt}");
assert!(receipt.contains("ghost.txt"), "{receipt}");
assert!(receipt.contains("not open on the server"), "{receipt}");
e.feed_text(":undo-change\r");
assert_eq!(text_of(&e, first), "alpha here\n");
assert_eq!(text_of(&e, second), "alpha there\n");
}
#[test]
fn cancel_mutates_nothing_and_leaves_a_cancelled_receipt() {
let (mut e, _dir, first, second) = two_file_editor();
let review = e.current();
e.review_cancel_pub();
assert_eq!(text_of(&e, first), "alpha here\n");
assert_eq!(text_of(&e, second), "alpha there\n");
let receipt = text_of(&e, review);
assert!(receipt.contains("— CANCELLED"), "{receipt}");
assert!(receipt.contains("nothing applied"), "{receipt}");
assert!(receipt.contains("ghost.txt"), "{receipt}");
assert!(e.message.contains("cancelled"), "{}", e.message);
e.feed_text(":undo-change\r");
assert_eq!(e.message, "no change to undo");
e.review_cancel_pub();
assert_eq!(e.message, "no change proposal awaiting review");
}
#[test]
fn a_source_edited_since_the_proposal_is_refused_by_name() {
let (mut e, _dir, first, second) = two_file_editor();
let review = e.current();
e.switch_to(first);
e.feed_text("0rx"); e.review_apply_pub();
assert_eq!(text_of(&e, first), "xlpha here\n");
assert_eq!(text_of(&e, second), "omega there\n");
assert_eq!(
e.message,
"rename: 1 buffer(s) applied, 2 target(s) refused"
);
let receipt = text_of(&e, review);
assert!(receipt.contains("a.txt"), "{receipt}");
assert!(receipt.contains("edited since the proposal"), "{receipt}");
assert!(receipt.contains("re-run the rename"), "{receipt}");
}
#[test]
fn a_source_closed_since_the_proposal_is_refused_by_name() {
let (mut e, _dir, first, second) = two_file_editor();
let review = e.current();
e.switch_to(second);
e.close_buffer(true); assert_eq!(text_of(&e, first), "alpha here\n");
e.review_apply_pub();
assert_eq!(text_of(&e, first), "omega here\n");
let receipt = text_of(&e, review);
assert!(receipt.contains("closed since the proposal"), "{receipt}");
assert_eq!(
e.message,
"rename: 1 buffer(s) applied, 2 target(s) refused"
);
}
#[test]
fn a_single_clean_document_applies_directly_with_the_existing_receipt() {
let dir = tempfile::tempdir().unwrap();
let (mut e, document) = file_editor(&dir, "a.txt", "alpha here\n");
bind(&mut e, document);
let plan = e.build_change_plan(
ChangeProducer::Rename,
vec![(
ResourceLocation::local(dir.path().join("a.txt")),
vec![edit(0, 0, 0, 5, "omega")],
)],
PositionEncoding::Utf8,
);
e.present_change_plan(plan);
assert_eq!(e.current(), document, "focus stays on the edited file");
assert_eq!(text_of(&e, document), "omega here\n");
assert_eq!(e.message, "rename: applied to 1 buffer(s)");
}
#[test]
fn a_newer_proposal_supersedes_the_older_buffer_visibly() {
let (mut e, dir, first, _second) = two_file_editor();
let stale = e.current();
let plan = e.build_change_plan(
ChangeProducer::CodeAction,
vec![
(
ResourceLocation::local(dir.path().join("a.txt")),
vec![edit(0, 0, 0, 5, "sigma")],
),
(
ResourceLocation::local(dir.path().join("b.txt")),
vec![edit(0, 0, 0, 5, "sigma")],
),
],
PositionEncoding::Utf8,
);
e.present_change_plan(plan);
assert_ne!(e.current(), stale);
let retired = text_of(&e, stale);
assert!(retired.contains("SUPERSEDED"), "{retired}");
assert!(e.buf().text().to_string().contains("change proposal 2"));
e.review_apply_pub();
assert_eq!(text_of(&e, first), "sigma here\n");
}
#[test]
fn nearby_edits_share_one_hunk_with_context_on_both_sides() {
let dir = tempfile::tempdir().unwrap();
let (mut e, document) = file_editor(
&dir,
"a.txt",
"one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n",
);
bind(&mut e, document);
let plan = e.build_change_plan(
ChangeProducer::CodeAction,
vec![
(
ResourceLocation::local(dir.path().join("a.txt")),
vec![edit(1, 0, 1, 3, "TWO"), edit(2, 0, 2, 5, "THREE")],
),
(
ResourceLocation::local(dir.path().join("ghost.txt")),
vec![edit(0, 0, 0, 1, "x")],
),
],
PositionEncoding::Utf8,
);
e.present_change_plan(plan);
let text = e.buf().text().to_string();
assert!(text.contains("@@ -1,6 +1,6 @@"), "{text}");
assert!(
text.contains(" one\n-two\n+TWO\n-three\n+THREE\n four\n five\n six\n"),
"{text}"
);
assert_eq!(
text.lines().filter(|line| line.starts_with("@@")).count(),
1,
"{text}"
);
e.review_apply_pub();
assert_eq!(
text_of(&e, document),
"one\nTWO\nTHREE\nfour\nfive\nsix\nseven\neight\nnine\nten\n"
);
}
#[test]
fn editing_a_review_cannot_apply_the_hidden_original_plan() {
let (mut editor, _dir, first, second) = two_file_editor();
editor.feed_text(":set noro<cr>ggIchanged review<esc>:apply-change<cr>");
assert_eq!(text_of(&editor, first), "alpha here\n");
assert_eq!(text_of(&editor, second), "alpha there\n");
assert!(
editor.message.contains("review changed"),
"{}",
editor.message
);
}
#[test]
fn save_change_retains_each_file_outcome_after_focus_moves() {
let (mut editor, dir, first, second) = two_file_editor();
editor.review_apply_pub();
editor.doc_mut(second).buf.readonly = true;
editor.save_changed_files_pub();
let report = editor.current();
editor.switch_to(first);
editor.wait_io().unwrap();
assert_eq!(editor.current(), first, "completion must not steal focus");
assert_eq!(
std::fs::read_to_string(dir.path().join("a.txt")).unwrap(),
"omega here\n"
);
assert_eq!(
std::fs::read_to_string(dir.path().join("b.txt")).unwrap(),
"alpha there\n"
);
assert!(editor.docs.get(second).unwrap().buf.dirty);
let results = text_of(&editor, report);
assert!(
results
.lines()
.any(|line| line.contains("a.txt") && line.contains("written")),
"{results}"
);
assert!(
results
.lines()
.any(|line| line.contains("b.txt") && line.contains("read")),
"{results}"
);
}