Skip to content

Commit d86863a

Browse files
committed
Rewrite algo::diff to support insertion and deletion
1 parent cc63f15 commit d86863a

File tree

5 files changed

+68
-25
lines changed

5 files changed

+68
-25
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ide/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ doctest = false
1111

1212
[dependencies]
1313
either = "1.5.3"
14-
indexmap = "1.3.2"
14+
indexmap = "1.4.0"
1515
itertools = "0.9.0"
1616
log = "0.4.8"
1717
rustc-hash = "1.1.0"

crates/ide/src/diagnostics.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,7 @@ fn main() {
613613
pub struct Foo { pub a: i32, pub b: i32 }
614614
"#,
615615
r#"
616-
fn {a:42, b: ()} {}
616+
fn some(, b: ()} {}
617617
fn items() {}
618618
fn here() {}
619619

crates/syntax/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ rustc_lexer = { version = "683.0.0", package = "rustc-ap-rustc_lexer" }
1717
rustc-hash = "1.1.0"
1818
arrayvec = "0.5.1"
1919
once_cell = "1.3.1"
20+
indexmap = "1.4.0"
2021
# This crate transitively depends on `smol_str` via `rowan`.
2122
# ideally, `serde` should be enabled by `rust-analyzer`, but we enable it here
2223
# to reduce number of compilations

crates/syntax/src/algo.rs

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
33
use std::{
44
fmt,
5+
hash::BuildHasherDefault,
56
ops::{self, RangeInclusive},
67
};
78

9+
use indexmap::IndexMap;
810
use itertools::Itertools;
911
use rustc_hash::FxHashMap;
1012
use text_edit::TextEditBuilder;
@@ -106,42 +108,56 @@ pub enum InsertPosition<T> {
106108
After(T),
107109
}
108110

111+
type FxIndexMap<K, V> = IndexMap<K, V, BuildHasherDefault<rustc_hash::FxHasher>>;
112+
109113
pub struct TreeDiff {
110114
replacements: FxHashMap<SyntaxElement, SyntaxElement>,
115+
deletions: Vec<SyntaxElement>,
116+
// the vec as well as the indexmap are both here to preserve order
117+
insertions: FxIndexMap<SyntaxElement, Vec<SyntaxElement>>,
111118
}
112119

113120
impl TreeDiff {
114121
pub fn into_text_edit(&self, builder: &mut TextEditBuilder) {
122+
for (anchor, to) in self.insertions.iter() {
123+
to.iter().for_each(|to| builder.insert(anchor.text_range().end(), to.to_string()));
124+
}
115125
for (from, to) in self.replacements.iter() {
116126
builder.replace(from.text_range(), to.to_string())
117127
}
128+
for text_range in self.deletions.iter().map(SyntaxElement::text_range) {
129+
builder.delete(text_range);
130+
}
118131
}
119132

120133
pub fn is_empty(&self) -> bool {
121-
self.replacements.is_empty()
134+
self.replacements.is_empty() && self.deletions.is_empty() && self.insertions.is_empty()
122135
}
123136
}
124137

125138
/// Finds minimal the diff, which, applied to `from`, will result in `to`.
126139
///
127-
/// Specifically, returns a map whose keys are descendants of `from` and values
128-
/// are descendants of `to`, such that `replace_descendants(from, map) == to`.
140+
/// Specifically, returns a structure that consists of a replacements, insertions and deletions
141+
/// such that applying this map on `from` will result in `to`.
129142
///
130-
/// A trivial solution is a singleton map `{ from: to }`, but this function
131-
/// tries to find a more fine-grained diff.
143+
/// This function tries to find a fine-grained diff.
132144
pub fn diff(from: &SyntaxNode, to: &SyntaxNode) -> TreeDiff {
133-
let mut buf = FxHashMap::default();
145+
let mut diff = TreeDiff {
146+
replacements: FxHashMap::default(),
147+
insertions: FxIndexMap::default(),
148+
deletions: Vec::new(),
149+
};
150+
let (from, to) = (from.clone().into(), to.clone().into());
151+
134152
// FIXME: this is both horrible inefficient and gives larger than
135153
// necessary diff. I bet there's a cool algorithm to diff trees properly.
136-
go(&mut buf, from.clone().into(), to.clone().into());
137-
return TreeDiff { replacements: buf };
154+
if !syntax_element_eq(&from, &to) {
155+
go(&mut diff, from, to);
156+
}
157+
return diff;
138158

139-
fn go(
140-
buf: &mut FxHashMap<SyntaxElement, SyntaxElement>,
141-
lhs: SyntaxElement,
142-
rhs: SyntaxElement,
143-
) {
144-
if lhs.kind() == rhs.kind()
159+
fn syntax_element_eq(lhs: &SyntaxElement, rhs: &SyntaxElement) -> bool {
160+
lhs.kind() == rhs.kind()
145161
&& lhs.text_range().len() == rhs.text_range().len()
146162
&& match (&lhs, &rhs) {
147163
(NodeOrToken::Node(lhs), NodeOrToken::Node(rhs)) => {
@@ -150,18 +166,43 @@ pub fn diff(from: &SyntaxNode, to: &SyntaxNode) -> TreeDiff {
150166
(NodeOrToken::Token(lhs), NodeOrToken::Token(rhs)) => lhs.text() == rhs.text(),
151167
_ => false,
152168
}
153-
{
154-
return;
155-
}
156-
if let (Some(lhs), Some(rhs)) = (lhs.as_node(), rhs.as_node()) {
157-
if lhs.children_with_tokens().count() == rhs.children_with_tokens().count() {
158-
for (lhs, rhs) in lhs.children_with_tokens().zip(rhs.children_with_tokens()) {
159-
go(buf, lhs, rhs)
160-
}
169+
}
170+
171+
fn go(diff: &mut TreeDiff, lhs: SyntaxElement, rhs: SyntaxElement) {
172+
let (lhs, rhs) = match lhs.as_node().zip(rhs.as_node()) {
173+
Some((lhs, rhs)) => (lhs, rhs),
174+
_ => {
175+
diff.replacements.insert(lhs, rhs);
161176
return;
162177
}
178+
};
179+
180+
let mut rhs_children = rhs.children_with_tokens();
181+
let mut lhs_children = lhs.children_with_tokens();
182+
let mut last_lhs = None;
183+
loop {
184+
let lhs_child = lhs_children.next();
185+
match (lhs_child.clone(), rhs_children.next()) {
186+
(None, None) => break,
187+
(None, Some(element)) => match last_lhs.clone() {
188+
Some(prev) => {
189+
diff.insertions.entry(prev).or_insert_with(Vec::new).push(element);
190+
}
191+
// first iteration, this means we got no anchor element to insert after
192+
// therefor replace the parent node instead
193+
None => {
194+
diff.replacements.insert(lhs.clone().into(), rhs.clone().into());
195+
break;
196+
}
197+
},
198+
(Some(element), None) => {
199+
diff.deletions.push(element);
200+
}
201+
(Some(ref lhs_ele), Some(ref rhs_ele)) if syntax_element_eq(lhs_ele, rhs_ele) => {}
202+
(Some(lhs_ele), Some(rhs_ele)) => go(diff, lhs_ele, rhs_ele),
203+
}
204+
last_lhs = lhs_child.or(last_lhs);
163205
}
164-
buf.insert(lhs, rhs);
165206
}
166207
}
167208

0 commit comments

Comments
 (0)