From a525e830a62272d21fbb0fb1c20bfa865791512d Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Wed, 25 Sep 2019 17:57:12 +0300 Subject: [PATCH] add new editing API, suitable for modifying several nodes at once --- Cargo.lock | 2 + crates/ra_assists/Cargo.toml | 1 + crates/ra_assists/src/assists/move_bounds.rs | 35 +++++----- crates/ra_assists/src/ast_editor.rs | 68 ++++++++++++++++---- crates/ra_syntax/Cargo.toml | 1 + crates/ra_syntax/src/algo.rs | 32 +++++++++ crates/ra_syntax/src/ast/extensions.rs | 9 +++ 7 files changed, 119 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74570a9785..275b27775c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -903,6 +903,7 @@ dependencies = [ "ra_hir 0.1.0", "ra_syntax 0.1.0", "ra_text_edit 0.1.0", + "rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "test_utils 0.1.0", ] @@ -1088,6 +1089,7 @@ dependencies = [ "ra_parser 0.1.0", "ra_text_edit 0.1.0", "rowan 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_lexer 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "smol_str 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "test_utils 0.1.0", diff --git a/crates/ra_assists/Cargo.toml b/crates/ra_assists/Cargo.toml index 635d876116..02966bbda3 100644 --- a/crates/ra_assists/Cargo.toml +++ b/crates/ra_assists/Cargo.toml @@ -10,6 +10,7 @@ once_cell = "1.0.1" join_to_string = "0.1.3" itertools = "0.8.0" arrayvec = "0.4.10" +rustc-hash = "1.0.1" ra_syntax = { path = "../ra_syntax" } ra_text_edit = { path = "../ra_text_edit" } diff --git a/crates/ra_assists/src/assists/move_bounds.rs b/crates/ra_assists/src/assists/move_bounds.rs index 6fd2fb72bb..6718260137 100644 --- a/crates/ra_assists/src/assists/move_bounds.rs +++ b/crates/ra_assists/src/assists/move_bounds.rs @@ -3,10 +3,9 @@ use ra_syntax::{ ast::{self, AstNode, NameOwner, TypeBoundsOwner}, SyntaxElement, SyntaxKind::*, - TextRange, }; -use crate::{ast_builder::Make, Assist, AssistCtx, AssistId}; +use crate::{ast_builder::Make, ast_editor::AstEditor, Assist, AssistCtx, AssistId}; pub(crate) fn move_bounds_to_where_clause(mut ctx: AssistCtx) -> Option { let type_param_list = ctx.node_at_offset::()?; @@ -36,23 +35,23 @@ pub(crate) fn move_bounds_to_where_clause(mut ctx: AssistCtx) AssistId("move_bounds_to_where_clause"), "move_bounds_to_where_clause", |edit| { - let type_params = type_param_list.type_params().collect::>(); + let new_params = type_param_list + .type_params() + .filter(|it| it.type_bound_list().is_some()) + .map(|type_param| { + let without_bounds = + AstEditor::new(type_param.clone()).remove_bounds().ast().clone(); + (type_param, without_bounds) + }); - for param in &type_params { - if let Some(bounds) = param.type_bound_list() { - let colon = param - .syntax() - .children_with_tokens() - .find(|it| it.kind() == COLON) - .unwrap(); - let start = colon.text_range().start(); - let end = bounds.syntax().text_range().end(); - edit.delete(TextRange::from_to(start, end)); - } - } + let mut ast_editor = AstEditor::new(type_param_list.clone()); + ast_editor.replace_descendants(new_params); + ast_editor.into_text_edit(edit.text_edit_builder()); - let predicates = type_params.iter().filter_map(build_predicate); - let where_clause = Make::::from_predicates(predicates); + let where_clause = { + let predicates = type_param_list.type_params().filter_map(build_predicate); + Make::::from_predicates(predicates) + }; let to_insert = match anchor.prev_sibling_or_token() { Some(ref elem) if elem.kind() == WHITESPACE => { @@ -68,7 +67,7 @@ pub(crate) fn move_bounds_to_where_clause(mut ctx: AssistCtx) ctx.build() } -fn build_predicate(param: &ast::TypeParam) -> Option { +fn build_predicate(param: ast::TypeParam) -> Option { let path = Make::::from_name(param.name()?); let predicate = Make::::from(path, param.type_bound_list()?.bounds()); Some(predicate) diff --git a/crates/ra_assists/src/ast_editor.rs b/crates/ra_assists/src/ast_editor.rs index 55c0aa59f2..4e253f0a47 100644 --- a/crates/ra_assists/src/ast_editor.rs +++ b/crates/ra_assists/src/ast_editor.rs @@ -1,11 +1,13 @@ use std::{iter, ops::RangeInclusive}; use arrayvec::ArrayVec; +use rustc_hash::FxHashMap; use ra_fmt::leading_indent; use ra_syntax::{ - algo::{insert_children, replace_children}, - ast, AstNode, Direction, InsertPosition, SyntaxElement, + algo, + ast::{self, TypeBoundsOwner}, + AstNode, Direction, InsertPosition, NodeOrToken, SyntaxElement, SyntaxKind::*, T, }; @@ -27,26 +29,55 @@ impl AstEditor { } pub fn into_text_edit(self, builder: &mut TextEditBuilder) { - // FIXME: compute a more fine-grained diff here. - // If *you* know a nice algorithm to compute diff between two syntax - // tree, tell me about it! - builder.replace( - self.original_ast.syntax().text_range(), - self.ast().syntax().text().to_string(), - ); + // FIXME: this is both horrible inefficient and gives larger than + // necessary diff. I bet there's a cool algorithm to diff trees properly. + go(builder, self.original_ast.syntax().clone().into(), self.ast().syntax().clone().into()); + + fn go(buf: &mut TextEditBuilder, lhs: SyntaxElement, rhs: SyntaxElement) { + if lhs.kind() == rhs.kind() && lhs.text_range().len() == rhs.text_range().len() { + if match (&lhs, &rhs) { + (NodeOrToken::Node(lhs), NodeOrToken::Node(rhs)) => lhs.text() == rhs.text(), + (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) + } + return; + } + } + buf.replace(lhs.text_range(), rhs.to_string()) + } } pub fn ast(&self) -> &N { &self.ast } + pub fn replace_descendants( + &mut self, + replacement_map: impl Iterator, + ) -> &mut Self { + let map = replacement_map + .map(|(from, to)| (from.syntax().clone().into(), to.syntax().clone().into())) + .collect::>(); + let new_syntax = algo::replace_descendants(self.ast.syntax(), &map); + self.ast = N::cast(new_syntax).unwrap(); + self + } + #[must_use] fn insert_children( &self, position: InsertPosition, mut to_insert: impl Iterator, ) -> N { - let new_syntax = insert_children(self.ast().syntax(), position, &mut to_insert); + let new_syntax = algo::insert_children(self.ast().syntax(), position, &mut to_insert); N::cast(new_syntax).unwrap() } @@ -56,7 +87,7 @@ impl AstEditor { to_delete: RangeInclusive, mut to_insert: impl Iterator, ) -> N { - let new_syntax = replace_children(self.ast().syntax(), to_delete, &mut to_insert); + let new_syntax = algo::replace_children(self.ast().syntax(), to_delete, &mut to_insert); N::cast(new_syntax).unwrap() } @@ -240,3 +271,18 @@ impl AstEditor { self.ast = self.replace_children(replace_range, to_insert.into_iter()) } } + +impl AstEditor { + pub fn remove_bounds(&mut self) -> &mut Self { + let colon = match self.ast.colon_token() { + Some(it) => it, + None => return self, + }; + let end = match self.ast.type_bound_list() { + Some(it) => it.syntax().clone().into(), + None => colon.clone().into(), + }; + self.ast = self.replace_children(RangeInclusive::new(colon.into(), end), iter::empty()); + self + } +} diff --git a/crates/ra_syntax/Cargo.toml b/crates/ra_syntax/Cargo.toml index d3a8b516a2..724c38e175 100644 --- a/crates/ra_syntax/Cargo.toml +++ b/crates/ra_syntax/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/rust-analyzer/rust-analyzer" itertools = "0.8.0" rowan = "0.6.1" rustc_lexer = "0.1.0" +rustc-hash = "1.0.1" # ideally, `serde` should be enabled by `ra_lsp_server`, but we enable it here # to reduce number of compilations diff --git a/crates/ra_syntax/src/algo.rs b/crates/ra_syntax/src/algo.rs index 7ee5aa85b1..f0ed96a178 100644 --- a/crates/ra_syntax/src/algo.rs +++ b/crates/ra_syntax/src/algo.rs @@ -3,6 +3,7 @@ pub mod visit; use std::ops::RangeInclusive; use itertools::Itertools; +use rustc_hash::FxHashMap; use crate::{ AstNode, Direction, NodeOrToken, SyntaxElement, SyntaxNode, SyntaxNodePtr, TextRange, TextUnit, @@ -123,6 +124,37 @@ pub fn replace_children( with_children(parent, new_children) } +/// Replaces descendants in the node, according to the mapping. +/// +/// This is a type-unsafe low-level editing API, if you need to use it, prefer +/// to create a type-safe abstraction on top of it instead. +pub fn replace_descendants( + parent: &SyntaxNode, + map: &FxHashMap, +) -> SyntaxNode { + // FIXME: this could be made much faster. + let new_children = parent.children_with_tokens().map(|it| go(map, it)).collect::>(); + return with_children(parent, new_children); + + fn go( + map: &FxHashMap, + element: SyntaxElement, + ) -> NodeOrToken { + if let Some(replacement) = map.get(&element) { + return match replacement { + NodeOrToken::Node(it) => NodeOrToken::Node(it.green().clone()), + NodeOrToken::Token(it) => NodeOrToken::Token(it.green().clone()), + }; + } + match element { + NodeOrToken::Token(it) => NodeOrToken::Token(it.green().clone()), + NodeOrToken::Node(it) => { + NodeOrToken::Node(replace_descendants(&it, map).green().clone()) + } + } + } +} + fn with_children( parent: &SyntaxNode, new_children: Box<[NodeOrToken]>, diff --git a/crates/ra_syntax/src/ast/extensions.rs b/crates/ra_syntax/src/ast/extensions.rs index d3a375f879..5f7e9f5b10 100644 --- a/crates/ra_syntax/src/ast/extensions.rs +++ b/crates/ra_syntax/src/ast/extensions.rs @@ -373,6 +373,15 @@ impl ast::LifetimeParam { } } +impl ast::TypeParam { + pub fn colon_token(&self) -> Option { + self.syntax() + .children_with_tokens() + .filter_map(|it| it.into_token()) + .find(|it| it.kind() == T![:]) + } +} + impl ast::WherePred { pub fn lifetime_token(&self) -> Option { self.syntax()