//! `rust-analyzer` never mutates text itself and only sends diffs to clients,
//! so `TextEdit` is the ultimate representation of the work done by
//! rust-analyzer.
+
pub use text_size::{TextRange, TextSize};
/// `InsertDelete` -- a single "atomic" change to text
///
/// Must not overlap with other `InDel`s
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Indel {
pub insert: String,
/// Refers to offsets in the original text
#[derive(Default, Debug, Clone)]
pub struct TextEdit {
+ /// Invariant: disjoint and sorted by `delete`.
indels: Vec<Indel>,
}
}
let mut total_len = TextSize::of(&*text);
- for indel in self.indels.iter() {
+ for indel in &self.indels {
total_len += TextSize::of(&indel.insert);
total_len -= indel.delete.end() - indel.delete.start();
}
let mut buf = String::with_capacity(total_len.into());
let mut prev = 0;
- for indel in self.indels.iter() {
+ for indel in &self.indels {
let start: usize = indel.delete.start().into();
let end: usize = indel.delete.end().into();
if start > prev {
// FIXME: figure out a way to mutate the text in-place or reuse the
// memory in some other way
- *text = buf
+ *text = buf;
}
pub fn union(&mut self, other: TextEdit) -> Result<(), TextEdit> {
// FIXME: can be done without allocating intermediate vector
let mut all = self.iter().chain(other.iter()).collect::<Vec<_>>();
- if !check_disjoint(&mut all) {
+ if !check_disjoint_and_sort(&mut all) {
return Err(other);
}
+
self.indels.extend(other.indels);
- assert!(check_disjoint(&mut self.indels));
+ check_disjoint_and_sort(&mut self.indels);
+ // Only dedup deletions and replacements, keep all insertions
+ self.indels.dedup_by(|a, b| a == b && !a.delete.is_empty());
Ok(())
}
pub fn apply_to_offset(&self, offset: TextSize) -> Option<TextSize> {
let mut res = offset;
- for indel in self.indels.iter() {
+ for indel in &self.indels {
if indel.delete.start() >= offset {
break;
}
}
impl TextEditBuilder {
+ pub fn is_empty(&self) -> bool {
+ self.indels.is_empty()
+ }
pub fn replace(&mut self, range: TextRange, replace_with: String) {
- self.indels.push(Indel::replace(range, replace_with))
+ self.indel(Indel::replace(range, replace_with));
}
pub fn delete(&mut self, range: TextRange) {
- self.indels.push(Indel::delete(range))
+ self.indel(Indel::delete(range));
}
pub fn insert(&mut self, offset: TextSize, text: String) {
- self.indels.push(Indel::insert(offset, text))
+ self.indel(Indel::insert(offset, text));
}
pub fn finish(self) -> TextEdit {
let mut indels = self.indels;
- assert!(check_disjoint(&mut indels));
+ assert_disjoint_or_equal(&mut indels);
TextEdit { indels }
}
pub fn invalidates_offset(&self, offset: TextSize) -> bool {
self.indels.iter().any(|indel| indel.delete.contains_inclusive(offset))
}
+ fn indel(&mut self, indel: Indel) {
+ self.indels.push(indel);
+ if self.indels.len() <= 16 {
+ assert_disjoint_or_equal(&mut self.indels);
+ }
+ }
}
-fn check_disjoint(indels: &mut [impl std::borrow::Borrow<Indel>]) -> bool {
+fn assert_disjoint_or_equal(indels: &mut [Indel]) {
+ assert!(check_disjoint_and_sort(indels));
+}
+// FIXME: Remove the impl Bound here, it shouldn't be needed
+fn check_disjoint_and_sort(indels: &mut [impl std::borrow::Borrow<Indel>]) -> bool {
indels.sort_by_key(|indel| (indel.borrow().delete.start(), indel.borrow().delete.end()));
- indels
- .iter()
- .zip(indels.iter().skip(1))
- .all(|(l, r)| l.borrow().delete.end() <= r.borrow().delete.start())
+ indels.iter().zip(indels.iter().skip(1)).all(|(l, r)| {
+ let l = l.borrow();
+ let r = r.borrow();
+ l.delete.end() <= r.delete.start() || l == r
+ })
}
+
+#[test]
+fn test_apply() {
+ let mut text = "_11h1_2222_xx3333_4444_6666".to_string();
+ let mut builder = TextEditBuilder::default();
+ builder.replace(TextRange::new(3.into(), 4.into()), "1".to_string());
+ builder.delete(TextRange::new(11.into(), 13.into()));
+ builder.insert(22.into(), "_5555".to_string());
+
+ let text_edit = builder.finish();
+ text_edit.apply(&mut text);
+
+ assert_eq!(text, "_1111_2222_3333_4444_5555_6666")
+}
\ No newline at end of file