use super::*;
#[derive(Clone, Debug, PartialEq)]
pub struct Position {
pub byte: usize,
pub char: usize,
pub point: Point,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Edit<'a> {
pub end_char: usize,
pub input_edit: InputEdit,
pub start_char: usize,
pub text: &'a str,
}
pub trait RopeExt {
fn apply_edit(&mut self, edit: &Edit);
fn build_edit<'a>(
&self,
change: &'a lsp::TextDocumentContentChangeEvent,
) -> Edit<'a>;
fn byte_to_lsp_position(&self, byte: usize) -> lsp::Position;
fn lsp_position_to_position(&self, position: lsp::Position) -> Position;
}
impl RopeExt for Rope {
fn apply_edit(&mut self, edit: &Edit) {
self.remove(edit.start_char..edit.end_char);
if !edit.text.is_empty() {
self.insert(edit.start_char, edit.text);
}
}
fn build_edit<'a>(
&self,
change: &'a lsp::TextDocumentContentChangeEvent,
) -> Edit<'a> {
let text = change.text.as_str();
let text_end_bytes = text.len();
let range = change.range.unwrap_or_else(|| lsp::Range {
start: self.byte_to_lsp_position(0),
end: self.byte_to_lsp_position(self.len_bytes()),
});
let (start, old_end) = (
self.lsp_position_to_position(range.start),
self.lsp_position_to_position(range.end),
);
let input_edit = InputEdit {
new_end_byte: start.byte + text_end_bytes,
new_end_position: start.point.advance(text.point_delta()),
old_end_byte: old_end.byte,
old_end_position: old_end.point,
start_byte: start.byte,
start_position: start.point,
};
Edit {
end_char: old_end.char,
input_edit,
start_char: start.char,
text,
}
}
fn byte_to_lsp_position(&self, byte: usize) -> lsp::Position {
let line = self.byte_to_line(byte);
let line_char = self.line_to_char(line);
let line_utf16_cu = self.char_to_utf16_cu(line_char);
let char = self.byte_to_char(byte);
let char_utf16_cu = self.char_to_utf16_cu(char);
lsp::Position::new(
u32::try_from(line).expect("line index exceeds u32::MAX"),
u32::try_from(char_utf16_cu - line_utf16_cu)
.expect("character offset exceeds u32::MAX"),
)
}
fn lsp_position_to_position(&self, position: lsp::Position) -> Position {
let row = position.line as usize;
let row_char = self.line_to_char(row);
let row_byte = self.line_to_byte(row);
let col_char = self.utf16_cu_to_char(
self.char_to_utf16_cu(row_char) + position.character as usize,
);
let col_byte = self.char_to_byte(col_char);
Position {
byte: col_byte,
char: col_char,
point: Point::new(row, col_byte - row_byte),
}
}
}
#[cfg(test)]
mod tests {
use {super::*, pretty_assertions::assert_eq, ropey::Rope};
fn change(
text: &str,
range: lsp::Range,
) -> lsp::TextDocumentContentChangeEvent {
lsp::TextDocumentContentChangeEvent {
range: Some(range),
range_length: None,
text: text.into(),
}
}
#[test]
fn apply_insert_into_empty_document() {
let mut rope = Rope::from_str("");
let change = change("🧪\nnew", lsp::Range::at(0, 0, 0, 0));
let edit = rope.build_edit(&change);
assert_eq!(
edit,
Edit {
start_char: 0,
end_char: 0,
input_edit: InputEdit {
start_byte: 0,
old_end_byte: 0,
new_end_byte: "🧪\nnew".len(),
start_position: Point::new(0, 0),
old_end_position: Point::new(0, 0),
new_end_position: Point::new(1, 3),
},
text: "🧪\nnew",
}
);
rope.apply_edit(&edit);
assert_eq!(rope.to_string(), "🧪\nnew");
}
#[test]
fn apply_insert_edit_updates_rope_contents() {
let mut rope = Rope::from_str("hello world");
let change = change("rope", lsp::Range::at(0, 6, 0, 11));
let edit = rope.build_edit(&change);
assert_eq!(
edit,
Edit {
start_char: 6,
end_char: 11,
input_edit: InputEdit {
new_end_byte: 10,
new_end_position: Point::new(0, 10),
old_end_byte: 11,
old_end_position: Point::new(0, 11),
start_byte: 6,
start_position: Point::new(0, 6),
},
text: "rope",
}
);
rope.apply_edit(&edit);
assert_eq!(rope.to_string(), "hello rope");
}
#[test]
fn apply_insert_edit_respects_utf16_columns() {
let mut rope = Rope::from_str("ab");
let change = change("🧪", lsp::Range::at(0, 1, 0, 1));
let edit = rope.build_edit(&change);
assert_eq!(
edit,
Edit {
start_char: 1,
end_char: 1,
input_edit: InputEdit {
new_end_byte: 5,
new_end_position: Point::new(0, 5),
old_end_byte: 1,
old_end_position: Point::new(0, 1),
start_byte: 1,
start_position: Point::new(0, 1),
},
text: "🧪",
}
);
rope.apply_edit(&edit);
assert_eq!(rope.to_string(), "a🧪b");
}
#[test]
fn apply_delete_edit_respects_utf16_columns() {
let mut rope = Rope::from_str("a😊b");
let change = change("", lsp::Range::at(0, 1, 0, 3));
let edit = rope.build_edit(&change);
assert_eq!(
edit,
Edit {
start_char: 1,
end_char: 2,
input_edit: InputEdit {
new_end_byte: 1,
new_end_position: Point::new(0, 1),
old_end_byte: 5,
old_end_position: Point::new(0, 5),
start_byte: 1,
start_position: Point::new(0, 1),
},
text: "",
}
);
rope.apply_edit(&edit);
assert_eq!(rope.to_string(), "ab");
}
#[test]
fn lsp_round_trip_handles_utf16_columns() {
let rope = Rope::from_str("a😊b\nsecond");
let position = rope.byte_to_lsp_position(5);
assert_eq!(position, lsp::Position::new(0, 3));
assert_eq!(
rope.lsp_position_to_position(position),
Position {
byte: 5,
char: 2,
point: Point::new(0, 5),
}
);
}
#[test]
fn replacement_across_surrogates_is_consistent() {
let mut rope = Rope::from_str("foo😊bar");
let change = change("🧪", lsp::Range::at(0, 3, 0, 5));
let edit = rope.build_edit(&change);
assert_eq!(
edit,
Edit {
start_char: 3,
end_char: 4,
input_edit: InputEdit {
start_byte: 3,
old_end_byte: 7,
new_end_byte: 7,
start_position: Point::new(0, 3),
old_end_position: Point::new(0, 7),
new_end_position: Point::new(0, 7),
},
text: "🧪",
}
);
rope.apply_edit(&edit);
assert_eq!(rope.to_string(), "foo🧪bar");
}
#[test]
fn multiline_edit_handles_utf16_offsets() {
let mut rope = Rope::from_str("foo😊\nbar");
let change = change("XX", lsp::Range::at(0, 2, 1, 1));
let edit = rope.build_edit(&change);
assert_eq!(
edit,
Edit {
start_char: 2,
end_char: 6,
input_edit: InputEdit {
start_byte: 2,
old_end_byte: 9,
new_end_byte: 4,
start_position: Point::new(0, 2),
old_end_position: Point::new(1, 1),
new_end_position: Point::new(0, 4),
},
text: "XX",
}
);
rope.apply_edit(&edit);
assert_eq!(rope.to_string(), "foXXar");
}
#[test]
fn append_beyond_eof_updates_point() {
let mut rope = Rope::from_str("hi");
let change = change("🧪\nnew", lsp::Range::at(0, 2, 0, 2));
let edit = rope.build_edit(&change);
assert_eq!(
edit,
Edit {
start_char: 2,
end_char: 2,
input_edit: InputEdit {
start_byte: 2,
old_end_byte: 2,
new_end_byte: 10,
start_position: Point::new(0, 2),
old_end_position: Point::new(0, 2),
new_end_position: Point::new(1, 3),
},
text: "🧪\nnew",
}
);
rope.apply_edit(&edit);
assert_eq!(rope.to_string(), "hi🧪\nnew");
}
#[test]
fn replace_entire_document_via_full_range() {
let mut rope = Rope::from_str("foo😊bar");
let change = lsp::TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "🧪baz".into(),
};
let edit = rope.build_edit(&change);
assert_eq!(
edit,
Edit {
start_char: 0,
end_char: 7,
input_edit: InputEdit {
start_byte: 0,
old_end_byte: 10,
new_end_byte: 7,
start_position: Point::new(0, 0),
old_end_position: Point::new(0, 10),
new_end_position: Point::new(0, 7),
},
text: "🧪baz",
}
);
rope.apply_edit(&edit);
assert_eq!(rope.to_string(), "🧪baz");
}
}