use pushkin_core::edits::{apply_edits, EditError, LineSpan, Replacement};
const CONTENT: &str = "let a = 1;\nlet b = 2;\nlet a = 1;\n";
fn anchored(old: &str, new: &str, start: usize, end: usize) -> Replacement {
Replacement {
old: old.to_owned(),
new: new.to_owned(),
replace_all: false,
anchor: Some(LineSpan { start, end }),
}
}
fn plain(old: &str, new: &str) -> Replacement {
Replacement {
old: old.to_owned(),
new: new.to_owned(),
replace_all: false,
anchor: None,
}
}
#[test]
fn an_anchored_edit_replaces_inside_its_range() {
let out = apply_edits(CONTENT, &[anchored("let b = 2;", "let b = 9;", 2, 2)]);
assert_eq!(out.unwrap(), "let a = 1;\nlet b = 9;\nlet a = 1;\n");
}
#[test]
fn a_repeated_target_is_unambiguous_inside_its_anchor() {
let out = apply_edits(CONTENT, &[anchored("let a = 1;", "let a = 7;", 3, 3)]);
assert_eq!(
out.unwrap(),
"let a = 1;\nlet b = 2;\nlet a = 7;\n",
"the THIRD line was anchored; replacing the first would be a fabricated file"
);
}
#[test]
fn the_same_target_unanchored_is_still_refused_as_ambiguous() {
let error = apply_edits(CONTENT, &[plain("let a = 1;", "let a = 7;")]).unwrap_err();
assert!(
matches!(error, EditError::NotUnique { count: 2, .. }),
"{error:?}"
);
}
#[test]
fn an_anchor_may_span_several_lines() {
let out = apply_edits(CONTENT, &[anchored("= 1;\nlet b", "= 4;\nlet b", 1, 2)]);
assert_eq!(out.unwrap(), "let a = 4;\nlet b = 2;\nlet a = 1;\n");
}
#[test]
fn a_target_outside_its_anchor_is_not_found_even_though_it_exists() {
let error = apply_edits(CONTENT, &[anchored("let a = 1;", "let a = 7;", 2, 2)]).unwrap_err();
assert!(matches!(error, EditError::NotFound { .. }), "{error:?}");
}
#[test]
fn a_target_twice_inside_one_anchor_is_still_ambiguous() {
let error = apply_edits(CONTENT, &[anchored("let a = 1;", "let a = 7;", 1, 3)]).unwrap_err();
assert!(
matches!(error, EditError::NotUnique { count: 2, .. }),
"an anchor wide enough to contain both occurrences resolves nothing: {error:?}"
);
}
#[test]
fn an_anchor_past_the_last_line_is_refused() {
let error = apply_edits(CONTENT, &[anchored("let a = 1;", "x", 3, 9)]).unwrap_err();
assert_eq!(
error,
EditError::AnchorOutOfRange {
start: 3,
end: 9,
lines: 3
}
);
}
#[test]
fn a_zero_start_is_refused_because_line_numbering_is_one_based() {
let error = apply_edits(CONTENT, &[anchored("let a = 1;", "x", 0, 1)]).unwrap_err();
assert!(
matches!(error, EditError::AnchorOutOfRange { .. }),
"{error:?}"
);
}
#[test]
fn an_inverted_range_is_refused() {
let error = apply_edits(CONTENT, &[anchored("let a = 1;", "x", 3, 1)]).unwrap_err();
assert!(
matches!(error, EditError::AnchorOutOfRange { .. }),
"{error:?}"
);
}
#[test]
fn an_anchored_edit_after_a_line_count_change_is_refused() {
let error = apply_edits(
CONTENT,
&[
plain("let b = 2;", "let b = 2;\nlet c = 3;"),
anchored("let a = 1;", "let a = 7;", 3, 3),
],
)
.unwrap_err();
assert_eq!(error, EditError::AnchorShifted);
}
#[test]
fn an_anchored_edit_after_a_line_count_preserving_change_still_applies() {
let out = apply_edits(
CONTENT,
&[
plain("let b = 2;", "let b = 9;"),
anchored("let a = 1;", "let a = 7;", 3, 3),
],
);
assert_eq!(out.unwrap(), "let a = 1;\nlet b = 9;\nlet a = 7;\n");
}
#[test]
fn an_unanchored_edit_after_a_line_count_change_still_applies() {
let out = apply_edits(
CONTENT,
&[
anchored("let a = 1;", "let a = 1;\nlet c = 3;", 1, 1),
plain("let b = 2;", "let b = 9;"),
],
);
assert_eq!(
out.unwrap(),
"let a = 1;\nlet c = 3;\nlet b = 9;\nlet a = 1;\n"
);
}
#[test]
fn a_later_anchor_failure_discards_the_whole_application() {
let error = apply_edits(
CONTENT,
&[
anchored("let b = 2;", "let b = 9;", 2, 2),
anchored("nowhere", "x", 1, 1),
],
);
assert!(error.is_err(), "the application must not partially succeed");
}