mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-11-10 07:04:22 +00:00
Rewrite algo::diff to support insertion and deletion
This commit is contained in:
parent
cc63f153f0
commit
d86863aeb4
5 changed files with 68 additions and 25 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1607,6 +1607,7 @@ version = "0.0.0"
|
|||
dependencies = [
|
||||
"arrayvec",
|
||||
"expect-test",
|
||||
"indexmap",
|
||||
"itertools",
|
||||
"once_cell",
|
||||
"parser",
|
||||
|
|
|
@ -11,7 +11,7 @@ doctest = false
|
|||
|
||||
[dependencies]
|
||||
either = "1.5.3"
|
||||
indexmap = "1.3.2"
|
||||
indexmap = "1.4.0"
|
||||
itertools = "0.9.0"
|
||||
log = "0.4.8"
|
||||
rustc-hash = "1.1.0"
|
||||
|
|
|
@ -613,7 +613,7 @@ fn main() {
|
|||
pub struct Foo { pub a: i32, pub b: i32 }
|
||||
"#,
|
||||
r#"
|
||||
fn {a:42, b: ()} {}
|
||||
fn some(, b: ()} {}
|
||||
fn items() {}
|
||||
fn here() {}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ rustc_lexer = { version = "683.0.0", package = "rustc-ap-rustc_lexer" }
|
|||
rustc-hash = "1.1.0"
|
||||
arrayvec = "0.5.1"
|
||||
once_cell = "1.3.1"
|
||||
indexmap = "1.4.0"
|
||||
# This crate transitively depends on `smol_str` via `rowan`.
|
||||
# ideally, `serde` should be enabled by `rust-analyzer`, but we enable it here
|
||||
# to reduce number of compilations
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
use std::{
|
||||
fmt,
|
||||
hash::BuildHasherDefault,
|
||||
ops::{self, RangeInclusive},
|
||||
};
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use itertools::Itertools;
|
||||
use rustc_hash::FxHashMap;
|
||||
use text_edit::TextEditBuilder;
|
||||
|
@ -106,42 +108,56 @@ pub enum InsertPosition<T> {
|
|||
After(T),
|
||||
}
|
||||
|
||||
type FxIndexMap<K, V> = IndexMap<K, V, BuildHasherDefault<rustc_hash::FxHasher>>;
|
||||
|
||||
pub struct TreeDiff {
|
||||
replacements: FxHashMap<SyntaxElement, SyntaxElement>,
|
||||
deletions: Vec<SyntaxElement>,
|
||||
// the vec as well as the indexmap are both here to preserve order
|
||||
insertions: FxIndexMap<SyntaxElement, Vec<SyntaxElement>>,
|
||||
}
|
||||
|
||||
impl TreeDiff {
|
||||
pub fn into_text_edit(&self, builder: &mut TextEditBuilder) {
|
||||
for (anchor, to) in self.insertions.iter() {
|
||||
to.iter().for_each(|to| builder.insert(anchor.text_range().end(), to.to_string()));
|
||||
}
|
||||
for (from, to) in self.replacements.iter() {
|
||||
builder.replace(from.text_range(), to.to_string())
|
||||
}
|
||||
for text_range in self.deletions.iter().map(SyntaxElement::text_range) {
|
||||
builder.delete(text_range);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.replacements.is_empty()
|
||||
self.replacements.is_empty() && self.deletions.is_empty() && self.insertions.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds minimal the diff, which, applied to `from`, will result in `to`.
|
||||
///
|
||||
/// Specifically, returns a map whose keys are descendants of `from` and values
|
||||
/// are descendants of `to`, such that `replace_descendants(from, map) == to`.
|
||||
/// Specifically, returns a structure that consists of a replacements, insertions and deletions
|
||||
/// such that applying this map on `from` will result in `to`.
|
||||
///
|
||||
/// A trivial solution is a singleton map `{ from: to }`, but this function
|
||||
/// tries to find a more fine-grained diff.
|
||||
/// This function tries to find a fine-grained diff.
|
||||
pub fn diff(from: &SyntaxNode, to: &SyntaxNode) -> TreeDiff {
|
||||
let mut buf = FxHashMap::default();
|
||||
let mut diff = TreeDiff {
|
||||
replacements: FxHashMap::default(),
|
||||
insertions: FxIndexMap::default(),
|
||||
deletions: Vec::new(),
|
||||
};
|
||||
let (from, to) = (from.clone().into(), to.clone().into());
|
||||
|
||||
// FIXME: this is both horrible inefficient and gives larger than
|
||||
// necessary diff. I bet there's a cool algorithm to diff trees properly.
|
||||
go(&mut buf, from.clone().into(), to.clone().into());
|
||||
return TreeDiff { replacements: buf };
|
||||
if !syntax_element_eq(&from, &to) {
|
||||
go(&mut diff, from, to);
|
||||
}
|
||||
return diff;
|
||||
|
||||
fn go(
|
||||
buf: &mut FxHashMap<SyntaxElement, SyntaxElement>,
|
||||
lhs: SyntaxElement,
|
||||
rhs: SyntaxElement,
|
||||
) {
|
||||
if lhs.kind() == rhs.kind()
|
||||
fn syntax_element_eq(lhs: &SyntaxElement, rhs: &SyntaxElement) -> bool {
|
||||
lhs.kind() == rhs.kind()
|
||||
&& lhs.text_range().len() == rhs.text_range().len()
|
||||
&& match (&lhs, &rhs) {
|
||||
(NodeOrToken::Node(lhs), NodeOrToken::Node(rhs)) => {
|
||||
|
@ -150,18 +166,43 @@ pub fn diff(from: &SyntaxNode, to: &SyntaxNode) -> TreeDiff {
|
|||
(NodeOrToken::Token(lhs), NodeOrToken::Token(rhs)) => lhs.text() == rhs.text(),
|
||||
_ => false,
|
||||
}
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let (Some(lhs), Some(rhs)) = (lhs.as_node(), rhs.as_node()) {
|
||||
if lhs.children_with_tokens().count() == rhs.children_with_tokens().count() {
|
||||
for (lhs, rhs) in lhs.children_with_tokens().zip(rhs.children_with_tokens()) {
|
||||
go(buf, lhs, rhs)
|
||||
}
|
||||
}
|
||||
|
||||
fn go(diff: &mut TreeDiff, lhs: SyntaxElement, rhs: SyntaxElement) {
|
||||
let (lhs, rhs) = match lhs.as_node().zip(rhs.as_node()) {
|
||||
Some((lhs, rhs)) => (lhs, rhs),
|
||||
_ => {
|
||||
diff.replacements.insert(lhs, rhs);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut rhs_children = rhs.children_with_tokens();
|
||||
let mut lhs_children = lhs.children_with_tokens();
|
||||
let mut last_lhs = None;
|
||||
loop {
|
||||
let lhs_child = lhs_children.next();
|
||||
match (lhs_child.clone(), rhs_children.next()) {
|
||||
(None, None) => break,
|
||||
(None, Some(element)) => match last_lhs.clone() {
|
||||
Some(prev) => {
|
||||
diff.insertions.entry(prev).or_insert_with(Vec::new).push(element);
|
||||
}
|
||||
// first iteration, this means we got no anchor element to insert after
|
||||
// therefor replace the parent node instead
|
||||
None => {
|
||||
diff.replacements.insert(lhs.clone().into(), rhs.clone().into());
|
||||
break;
|
||||
}
|
||||
},
|
||||
(Some(element), None) => {
|
||||
diff.deletions.push(element);
|
||||
}
|
||||
(Some(ref lhs_ele), Some(ref rhs_ele)) if syntax_element_eq(lhs_ele, rhs_ele) => {}
|
||||
(Some(lhs_ele), Some(rhs_ele)) => go(diff, lhs_ele, rhs_ele),
|
||||
}
|
||||
last_lhs = lhs_child.or(last_lhs);
|
||||
}
|
||||
buf.insert(lhs, rhs);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue