use lsp_types::TextDocumentContentChangeEvent;
use super::{Edit, LineIndex, PositionEncoding};
pub fn apply_content_changes(
text: &mut String,
changes: Vec<TextDocumentContentChangeEvent>,
encoding: PositionEncoding,
) -> Option<Vec<Edit>> {
let start = changes
.iter()
.rposition(|change| change.range.is_none())
.unwrap_or(0);
let mut edits = Vec::with_capacity(changes.len() - start);
let mut replaced = false;
for change in &changes[start..] {
match change.range {
Some(range) => {
let index = LineIndex::new(text);
let start = index.position_to_byte(range.start, encoding);
let end = index.position_to_byte(range.end, encoding);
text.replace_range(start..end, &change.text);
edits.push(Edit {
range: start..end,
insert: change.text.clone(),
});
}
None => {
text.clear();
text.push_str(&change.text);
replaced = true;
}
}
}
(!replaced).then_some(edits)
}
#[cfg(test)]
mod tests {
use lsp_types::{Position, Range};
use super::super::apply_edits;
use super::*;
fn ranged(start: (u32, u32), end: (u32, u32), text: &str) -> TextDocumentContentChangeEvent {
TextDocumentContentChangeEvent {
range: Some(Range::new(
Position::new(start.0, start.1),
Position::new(end.0, end.1),
)),
range_length: None,
text: text.to_string(),
}
}
fn full(text: &str) -> TextDocumentContentChangeEvent {
TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: text.to_string(),
}
}
fn apply(initial: &str, changes: Vec<TextDocumentContentChangeEvent>) -> String {
let mut text = initial.to_string();
let edits = apply_content_changes(&mut text, changes, PositionEncoding::Utf16);
if let Some(edits) = edits {
assert_eq!(
apply_edits(initial, &edits),
text,
"reported edits: {edits:?}"
);
}
text
}
fn edit(range: std::ops::Range<usize>, insert: &str) -> Edit {
Edit {
range,
insert: insert.to_string(),
}
}
#[test]
fn insert_delete_replace_on_one_line() {
assert_eq!(apply("ab", vec![ranged((0, 1), (0, 1), "x")]), "axb");
assert_eq!(apply("axb", vec![ranged((0, 1), (0, 2), "")]), "ab");
assert_eq!(apply("abc", vec![ranged((0, 1), (0, 2), "xy")]), "axyc");
}
#[test]
fn sequential_changes_see_prior_edits() {
let changes = vec![ranged((0, 0), (0, 0), "xyz"), ranged((0, 3), (0, 3), "!")];
assert_eq!(apply("ab", changes), "xyz!ab");
}
#[test]
fn edit_spanning_a_newline() {
assert_eq!(apply("ab\ncd", vec![ranged((0, 1), (1, 1), "-")]), "a-d");
}
#[test]
fn insert_adding_lines_shifts_later_ranges() {
let changes = vec![ranged((0, 2), (0, 2), "\nnew"), ranged((1, 3), (1, 3), "!")];
assert_eq!(apply("ab\ncd", changes), "ab\nnew!\ncd");
}
#[test]
fn utf16_offsets_after_surrogate_pair() {
assert_eq!(
apply("\u{1F600}x", vec![ranged((0, 2), (0, 3), "y")]),
"\u{1F600}y"
);
}
#[test]
fn utf8_offsets_after_surrogate_pair() {
let mut text = "\u{1F600}x".to_string();
apply_content_changes(
&mut text,
vec![ranged((0, 4), (0, 5), "y")],
PositionEncoding::Utf8,
);
assert_eq!(text, "\u{1F600}y");
}
#[test]
fn full_replacement() {
assert_eq!(apply("old", vec![full("new")]), "new");
}
#[test]
fn ranged_changes_report_their_byte_edits() {
let mut text = "ab\ncd".to_string();
let edits = apply_content_changes(
&mut text,
vec![ranged((0, 1), (0, 1), "x"), ranged((1, 1), (1, 1), "y")],
PositionEncoding::Utf16,
);
assert_eq!(edits, Some(vec![edit(1..1, "x"), edit(5..5, "y")]));
assert_eq!(text, "axb\ncyd");
}
#[test]
fn a_full_replacement_reports_no_edits() {
let mut text = "old".to_string();
assert_eq!(
apply_content_changes(
&mut text,
vec![full("base\n"), ranged((0, 4), (0, 4), "!")],
PositionEncoding::Utf16,
),
None,
);
assert_eq!(text, "base!\n");
}
#[test]
fn an_empty_batch_reports_an_empty_edit_slice() {
let mut text = "x = 1\n".to_string();
assert_eq!(
apply_content_changes(&mut text, vec![], PositionEncoding::Utf16),
Some(vec![]),
);
assert_eq!(text, "x = 1\n");
}
#[test]
fn changes_before_a_full_replacement_are_skipped() {
let changes = vec![
ranged((5, 0), (9, 0), "junk that must not apply"),
full("base\n"),
ranged((0, 4), (0, 4), "!"),
];
assert_eq!(apply("ab", changes), "base!\n");
}
#[test]
fn out_of_range_positions_clamp() {
assert_eq!(apply("ab\ncd", vec![ranged((0, 9), (9, 9), "!")]), "ab!");
assert_eq!(apply("", vec![ranged((3, 1), (4, 2), "x")]), "x");
}
}