1936: cleanup editor r=matklad a=matklad



Co-authored-by: Aleksey Kladov <aleksey.kladov@gmail.com>
This commit is contained in:
bors[bot] 2019-09-30 07:16:23 +00:00 committed by GitHub
commit c913b48928
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 279 additions and 285 deletions

View file

@ -2,7 +2,7 @@ use hir::db::HirDatabase;
use ra_db::FileRange;
use ra_fmt::{leading_indent, reindent};
use ra_syntax::{
algo::{find_covering_element, find_node_at_offset},
algo::{self, find_covering_element, find_node_at_offset},
AstNode, SourceFile, SyntaxElement, SyntaxNode, SyntaxToken, TextRange, TextUnit,
TokenAtOffset,
};
@ -177,6 +177,10 @@ impl AssistBuilder {
&mut self.edit
}
pub(crate) fn replace_ast<N: AstNode>(&mut self, old: N, new: N) {
algo::diff(old.syntax(), new.syntax()).into_text_edit(&mut self.edit)
}
fn build(self) -> AssistAction {
AssistAction {
edit: self.edit.finish(),

View file

@ -1,10 +1,10 @@
use hir::{db::HirDatabase, HasSource};
use ra_syntax::{
ast::{self, make, AstNode, NameOwner},
ast::{self, edit, make, AstNode, NameOwner},
SmolStr,
};
use crate::{ast_editor::AstEditor, Assist, AssistCtx, AssistId};
use crate::{Assist, AssistCtx, AssistId};
#[derive(PartialEq)]
enum AddMissingImplMembersMode {
@ -75,30 +75,26 @@ fn add_missing_impl_members_inner(
ctx.add_action(AssistId(assist_id), label, |edit| {
let n_existing_items = impl_item_list.impl_items().count();
let items = missing_items.into_iter().map(|it| match it {
ast::ImplItem::FnDef(def) => strip_docstring(add_body(def).into()),
_ => strip_docstring(it),
});
let mut ast_editor = AstEditor::new(impl_item_list);
ast_editor.append_items(items);
let first_new_item = ast_editor.ast().impl_items().nth(n_existing_items).unwrap();
let cursor_position = first_new_item.syntax().text_range().start();
ast_editor.into_text_edit(edit.text_edit_builder());
let items = missing_items
.into_iter()
.map(|it| match it {
ast::ImplItem::FnDef(def) => ast::ImplItem::FnDef(add_body(def)),
_ => it,
})
.map(|it| edit::strip_attrs_and_docs(&it));
let new_impl_item_list = impl_item_list.append_items(items);
let cursor_position = {
let first_new_item = new_impl_item_list.impl_items().nth(n_existing_items).unwrap();
first_new_item.syntax().text_range().start()
};
edit.replace_ast(impl_item_list, new_impl_item_list);
edit.set_cursor(cursor_position);
});
ctx.build()
}
fn strip_docstring(item: ast::ImplItem) -> ast::ImplItem {
let mut ast_editor = AstEditor::new(item);
ast_editor.strip_attrs_and_docs();
ast_editor.ast().to_owned()
}
fn add_body(fn_def: ast::FnDef) -> ast::FnDef {
if fn_def.body().is_none() {
fn_def.with_body(make::block_from_expr(make::expr_unimplemented()))

View file

@ -1,11 +1,11 @@
use hir::db::HirDatabase;
use ra_syntax::{
ast::{self, make, AstNode, NameOwner, TypeBoundsOwner},
ast::{self, edit, make, AstNode, NameOwner, TypeBoundsOwner},
SyntaxElement,
SyntaxKind::*,
};
use crate::{ast_editor::AstEditor, Assist, AssistCtx, AssistId};
use crate::{Assist, AssistCtx, AssistId};
pub(crate) fn move_bounds_to_where_clause(mut ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> {
let type_param_list = ctx.node_at_offset::<ast::TypeParamList>()?;
@ -39,14 +39,12 @@ pub(crate) fn move_bounds_to_where_clause(mut ctx: AssistCtx<impl HirDatabase>)
.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();
let without_bounds = type_param.remove_bounds();
(type_param, without_bounds)
});
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 new_type_param_list = edit::replace_descendants(&type_param_list, new_params);
edit.replace_ast(type_param_list.clone(), new_type_param_list);
let where_clause = {
let predicates = type_param_list.type_params().filter_map(build_predicate);

View file

@ -1,245 +0,0 @@
use std::{iter, ops::RangeInclusive};
use arrayvec::ArrayVec;
use rustc_hash::FxHashMap;
use ra_fmt::leading_indent;
use ra_syntax::{
algo,
ast::{self, make::tokens, TypeBoundsOwner},
AstNode, Direction, InsertPosition, SyntaxElement,
SyntaxKind::*,
T,
};
use ra_text_edit::TextEditBuilder;
pub struct AstEditor<N: AstNode> {
original_ast: N,
ast: N,
}
impl<N: AstNode> AstEditor<N> {
pub fn new(node: N) -> AstEditor<N>
where
N: Clone,
{
AstEditor { original_ast: node.clone(), ast: node }
}
pub fn into_text_edit(self, builder: &mut TextEditBuilder) {
for (from, to) in algo::diff(&self.original_ast.syntax(), self.ast().syntax()) {
builder.replace(from.text_range(), to.to_string())
}
}
pub fn ast(&self) -> &N {
&self.ast
}
pub fn replace_descendants<T: AstNode>(
&mut self,
replacement_map: impl Iterator<Item = (T, T)>,
) -> &mut Self {
let map = replacement_map
.map(|(from, to)| (from.syntax().clone().into(), to.syntax().clone().into()))
.collect::<FxHashMap<_, _>>();
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<SyntaxElement>,
mut to_insert: impl Iterator<Item = SyntaxElement>,
) -> N {
let new_syntax = algo::insert_children(self.ast().syntax(), position, &mut to_insert);
N::cast(new_syntax).unwrap()
}
#[must_use]
fn replace_children(
&self,
to_delete: RangeInclusive<SyntaxElement>,
mut to_insert: impl Iterator<Item = SyntaxElement>,
) -> N {
let new_syntax = algo::replace_children(self.ast().syntax(), to_delete, &mut to_insert);
N::cast(new_syntax).unwrap()
}
fn do_make_multiline(&mut self) {
let l_curly =
match self.ast().syntax().children_with_tokens().find(|it| it.kind() == T!['{']) {
Some(it) => it,
None => return,
};
let sibling = match l_curly.next_sibling_or_token() {
Some(it) => it,
None => return,
};
let existing_ws = match sibling.as_token() {
None => None,
Some(tok) if tok.kind() != WHITESPACE => None,
Some(ws) => {
if ws.text().contains('\n') {
return;
}
Some(ws.clone())
}
};
let indent = leading_indent(self.ast().syntax()).unwrap_or("".into());
let ws = tokens::WsBuilder::new(&format!("\n{}", indent));
let to_insert = iter::once(ws.ws().into());
self.ast = match existing_ws {
None => self.insert_children(InsertPosition::After(l_curly), to_insert),
Some(ws) => {
self.replace_children(RangeInclusive::new(ws.clone().into(), ws.into()), to_insert)
}
};
}
}
impl AstEditor<ast::RecordFieldList> {
pub fn append_field(&mut self, field: &ast::RecordField) {
self.insert_field(InsertPosition::Last, field)
}
pub fn insert_field(
&mut self,
position: InsertPosition<&'_ ast::RecordField>,
field: &ast::RecordField,
) {
let is_multiline = self.ast().syntax().text().contains_char('\n');
let ws;
let space = if is_multiline {
ws = tokens::WsBuilder::new(&format!(
"\n{} ",
leading_indent(self.ast().syntax()).unwrap_or("".into())
));
ws.ws()
} else {
tokens::single_space()
};
let mut to_insert: ArrayVec<[SyntaxElement; 4]> = ArrayVec::new();
to_insert.push(space.into());
to_insert.push(field.syntax().clone().into());
to_insert.push(tokens::comma().into());
macro_rules! after_l_curly {
() => {{
let anchor = match self.l_curly() {
Some(it) => it,
None => return,
};
InsertPosition::After(anchor)
}};
}
macro_rules! after_field {
($anchor:expr) => {
if let Some(comma) = $anchor
.syntax()
.siblings_with_tokens(Direction::Next)
.find(|it| it.kind() == T![,])
{
InsertPosition::After(comma)
} else {
to_insert.insert(0, tokens::comma().into());
InsertPosition::After($anchor.syntax().clone().into())
}
};
};
let position = match position {
InsertPosition::First => after_l_curly!(),
InsertPosition::Last => {
if !is_multiline {
// don't insert comma before curly
to_insert.pop();
}
match self.ast().fields().last() {
Some(it) => after_field!(it),
None => after_l_curly!(),
}
}
InsertPosition::Before(anchor) => {
InsertPosition::Before(anchor.syntax().clone().into())
}
InsertPosition::After(anchor) => after_field!(anchor),
};
self.ast = self.insert_children(position, to_insert.iter().cloned());
}
fn l_curly(&self) -> Option<SyntaxElement> {
self.ast().syntax().children_with_tokens().find(|it| it.kind() == T!['{'])
}
}
impl AstEditor<ast::ItemList> {
pub fn append_items(&mut self, items: impl Iterator<Item = ast::ImplItem>) {
if !self.ast().syntax().text().contains_char('\n') {
self.do_make_multiline();
}
items.for_each(|it| self.append_item(it));
}
pub fn append_item(&mut self, item: ast::ImplItem) {
let (indent, position) = match self.ast().impl_items().last() {
Some(it) => (
leading_indent(it.syntax()).unwrap_or_default().to_string(),
InsertPosition::After(it.syntax().clone().into()),
),
None => match self.l_curly() {
Some(it) => (
" ".to_string() + &leading_indent(self.ast().syntax()).unwrap_or_default(),
InsertPosition::After(it),
),
None => return,
},
};
let ws = tokens::WsBuilder::new(&format!("\n{}", indent));
let to_insert: ArrayVec<[SyntaxElement; 2]> =
[ws.ws().into(), item.syntax().clone().into()].into();
self.ast = self.insert_children(position, to_insert.into_iter());
}
fn l_curly(&self) -> Option<SyntaxElement> {
self.ast().syntax().children_with_tokens().find(|it| it.kind() == T!['{'])
}
}
impl AstEditor<ast::ImplItem> {
pub fn strip_attrs_and_docs(&mut self) {
while let Some(start) = self
.ast()
.syntax()
.children_with_tokens()
.find(|it| it.kind() == ATTR || it.kind() == COMMENT)
{
let end = match &start.next_sibling_or_token() {
Some(el) if el.kind() == WHITESPACE => el.clone(),
Some(_) | None => start.clone(),
};
self.ast = self.replace_children(RangeInclusive::new(start, end), iter::empty());
}
}
}
impl AstEditor<ast::TypeParam> {
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
}
}

View file

@ -7,7 +7,6 @@
mod assist_ctx;
mod marks;
pub mod ast_editor;
use hir::db::HirDatabase;
use itertools::Itertools;

View file

@ -2,10 +2,10 @@ use std::cell::RefCell;
use hir::diagnostics::{AstDiagnostic, Diagnostic as _, DiagnosticSink};
use itertools::Itertools;
use ra_assists::ast_editor::AstEditor;
use ra_db::SourceDatabase;
use ra_prof::profile;
use ra_syntax::{
algo,
ast::{self, make, AstNode},
Location, SyntaxNode, TextRange, T,
};
@ -56,15 +56,15 @@ pub(crate) fn diagnostics(db: &RootDatabase, file_id: FileId) -> Vec<Diagnostic>
})
})
.on::<hir::diagnostics::MissingFields, _>(|d| {
let node = d.ast(db);
let mut ast_editor = AstEditor::new(node);
let mut field_list = d.ast(db);
for f in d.missed_fields.iter() {
let field = make::record_field(make::name_ref(&f.to_string()), Some(make::expr_unit()));
ast_editor.append_field(&field);
field_list = field_list.append_field(&field);
}
let mut builder = TextEditBuilder::default();
ast_editor.into_text_edit(&mut builder);
algo::diff(&d.ast(db).syntax(), &field_list.syntax()).into_text_edit(&mut builder);
let fix =
SourceChange::source_file_edit_from("fill struct fields", file_id, builder.finish());
res.borrow_mut().push(Diagnostic {

View file

@ -3,6 +3,7 @@ pub mod visit;
use std::ops::RangeInclusive;
use itertools::Itertools;
use ra_text_edit::TextEditBuilder;
use rustc_hash::FxHashMap;
use crate::{
@ -63,6 +64,18 @@ pub enum InsertPosition<T> {
After(T),
}
pub struct TreeDiff {
replacements: FxHashMap<SyntaxElement, SyntaxElement>,
}
impl TreeDiff {
pub fn into_text_edit(&self, builder: &mut TextEditBuilder) {
for (from, to) in self.replacements.iter() {
builder.replace(from.text_range(), to.to_string())
}
}
}
/// Finds minimal the diff, which, applied to `from`, will result in `to`.
///
/// Specifically, returns a map whose keys are descendants of `from` and values
@ -70,12 +83,12 @@ pub enum InsertPosition<T> {
///
/// A trivial solution is a singletom map `{ from: to }`, but this function
/// tries to find a more fine-grained diff.
pub fn diff(from: &SyntaxNode, to: &SyntaxNode) -> FxHashMap<SyntaxElement, SyntaxElement> {
pub fn diff(from: &SyntaxNode, to: &SyntaxNode) -> TreeDiff {
let mut buf = FxHashMap::default();
// 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 buf;
return TreeDiff { replacements: buf };
fn go(
buf: &mut FxHashMap<SyntaxElement, SyntaxElement>,

View file

@ -5,7 +5,7 @@ mod traits;
mod tokens;
mod extensions;
mod expr_extensions;
mod edit;
pub mod edit;
pub mod make;
use std::marker::PhantomData;

View file

@ -1,14 +1,21 @@
//! This module contains functions for editing syntax trees. As the trees are
//! immutable, all function here return a fresh copy of the tree, instead of
//! doing an in-place modification.
use std::{iter, ops::RangeInclusive};
use arrayvec::ArrayVec;
use std::ops::RangeInclusive;
use rustc_hash::FxHashMap;
use crate::{
algo,
ast::{self, make, AstNode},
InsertPosition, SyntaxElement,
ast::{
self,
make::{self, tokens},
AstNode, TypeBoundsOwner,
},
AstToken, Direction, InsertPosition, SmolStr, SyntaxElement,
SyntaxKind::{ATTR, COMMENT, WHITESPACE},
SyntaxNode, T,
};
impl ast::FnDef {
@ -31,6 +38,218 @@ impl ast::FnDef {
}
}
impl ast::ItemList {
#[must_use]
pub fn append_items(&self, items: impl Iterator<Item = ast::ImplItem>) -> ast::ItemList {
let mut res = self.clone();
if !self.syntax().text().contains_char('\n') {
res = res.make_multiline();
}
items.for_each(|it| res = res.append_item(it));
res
}
#[must_use]
pub fn append_item(&self, item: ast::ImplItem) -> ast::ItemList {
let (indent, position) = match self.impl_items().last() {
Some(it) => (
leading_indent(it.syntax()).unwrap_or_default().to_string(),
InsertPosition::After(it.syntax().clone().into()),
),
None => match self.l_curly() {
Some(it) => (
" ".to_string() + &leading_indent(self.syntax()).unwrap_or_default(),
InsertPosition::After(it),
),
None => return self.clone(),
},
};
let ws = tokens::WsBuilder::new(&format!("\n{}", indent));
let to_insert: ArrayVec<[SyntaxElement; 2]> =
[ws.ws().into(), item.syntax().clone().into()].into();
insert_children(self, position, to_insert.into_iter())
}
fn l_curly(&self) -> Option<SyntaxElement> {
self.syntax().children_with_tokens().find(|it| it.kind() == T!['{'])
}
fn make_multiline(&self) -> ast::ItemList {
let l_curly = match self.syntax().children_with_tokens().find(|it| it.kind() == T!['{']) {
Some(it) => it,
None => return self.clone(),
};
let sibling = match l_curly.next_sibling_or_token() {
Some(it) => it,
None => return self.clone(),
};
let existing_ws = match sibling.as_token() {
None => None,
Some(tok) if tok.kind() != WHITESPACE => None,
Some(ws) => {
if ws.text().contains('\n') {
return self.clone();
}
Some(ws.clone())
}
};
let indent = leading_indent(self.syntax()).unwrap_or("".into());
let ws = tokens::WsBuilder::new(&format!("\n{}", indent));
let to_insert = iter::once(ws.ws().into());
match existing_ws {
None => insert_children(self, InsertPosition::After(l_curly), to_insert),
Some(ws) => {
replace_children(self, RangeInclusive::new(ws.clone().into(), ws.into()), to_insert)
}
}
}
}
impl ast::RecordFieldList {
#[must_use]
pub fn append_field(&self, field: &ast::RecordField) -> ast::RecordFieldList {
self.insert_field(InsertPosition::Last, field)
}
#[must_use]
pub fn insert_field(
&self,
position: InsertPosition<&'_ ast::RecordField>,
field: &ast::RecordField,
) -> ast::RecordFieldList {
let is_multiline = self.syntax().text().contains_char('\n');
let ws;
let space = if is_multiline {
ws = tokens::WsBuilder::new(&format!(
"\n{} ",
leading_indent(self.syntax()).unwrap_or("".into())
));
ws.ws()
} else {
tokens::single_space()
};
let mut to_insert: ArrayVec<[SyntaxElement; 4]> = ArrayVec::new();
to_insert.push(space.into());
to_insert.push(field.syntax().clone().into());
to_insert.push(tokens::comma().into());
macro_rules! after_l_curly {
() => {{
let anchor = match self.l_curly() {
Some(it) => it,
None => return self.clone(),
};
InsertPosition::After(anchor)
}};
}
macro_rules! after_field {
($anchor:expr) => {
if let Some(comma) = $anchor
.syntax()
.siblings_with_tokens(Direction::Next)
.find(|it| it.kind() == T![,])
{
InsertPosition::After(comma)
} else {
to_insert.insert(0, tokens::comma().into());
InsertPosition::After($anchor.syntax().clone().into())
}
};
};
let position = match position {
InsertPosition::First => after_l_curly!(),
InsertPosition::Last => {
if !is_multiline {
// don't insert comma before curly
to_insert.pop();
}
match self.fields().last() {
Some(it) => after_field!(it),
None => after_l_curly!(),
}
}
InsertPosition::Before(anchor) => {
InsertPosition::Before(anchor.syntax().clone().into())
}
InsertPosition::After(anchor) => after_field!(anchor),
};
insert_children(self, position, to_insert.iter().cloned())
}
fn l_curly(&self) -> Option<SyntaxElement> {
self.syntax().children_with_tokens().find(|it| it.kind() == T!['{'])
}
}
impl ast::TypeParam {
#[must_use]
pub fn remove_bounds(&self) -> ast::TypeParam {
let colon = match self.colon_token() {
Some(it) => it,
None => return self.clone(),
};
let end = match self.type_bound_list() {
Some(it) => it.syntax().clone().into(),
None => colon.clone().into(),
};
replace_children(self, RangeInclusive::new(colon.into(), end), iter::empty())
}
}
#[must_use]
pub fn strip_attrs_and_docs<N: ast::AttrsOwner>(node: &N) -> N {
N::cast(strip_attrs_and_docs_inner(node.syntax().clone())).unwrap()
}
fn strip_attrs_and_docs_inner(mut node: SyntaxNode) -> SyntaxNode {
while let Some(start) =
node.children_with_tokens().find(|it| it.kind() == ATTR || it.kind() == COMMENT)
{
let end = match &start.next_sibling_or_token() {
Some(el) if el.kind() == WHITESPACE => el.clone(),
Some(_) | None => start.clone(),
};
node = algo::replace_children(&node, RangeInclusive::new(start, end), &mut iter::empty());
}
node
}
#[must_use]
pub fn replace_descendants<N: AstNode, D: AstNode>(
parent: &N,
replacement_map: impl Iterator<Item = (D, D)>,
) -> N {
let map = replacement_map
.map(|(from, to)| (from.syntax().clone().into(), to.syntax().clone().into()))
.collect::<FxHashMap<_, _>>();
let new_syntax = algo::replace_descendants(parent.syntax(), &map);
N::cast(new_syntax).unwrap()
}
// Note this is copy-pasted from fmt. It seems like fmt should be a separate
// crate, but basic tree building should be this crate. However, tree building
// might want to call into fmt...
fn leading_indent(node: &SyntaxNode) -> Option<SmolStr> {
let prev_tokens = std::iter::successors(node.first_token(), |token| token.prev_token());
for token in prev_tokens {
if let Some(ws) = ast::Whitespace::cast(token.clone()) {
let ws_text = ws.text();
if let Some(pos) = ws_text.rfind('\n') {
return Some(ws_text[pos + 1..].into());
}
}
if token.text().contains('\n') {
break;
}
}
None
}
#[must_use]
fn insert_children<N: AstNode>(
parent: &N,

View file

@ -4,7 +4,7 @@
use itertools::Itertools;
use crate::{
ast::{self, child_opt, children, AstNode, SyntaxNode},
ast::{self, child_opt, children, AstChildren, AstNode, SyntaxNode},
SmolStr, SyntaxElement,
SyntaxKind::*,
SyntaxToken, T,
@ -203,6 +203,16 @@ impl ast::ImplBlock {
}
}
impl ast::AttrsOwner for ast::ImplItem {
fn attrs(&self) -> AstChildren<ast::Attr> {
match self {
ast::ImplItem::FnDef(it) => it.attrs(),
ast::ImplItem::TypeAliasDef(it) => it.attrs(),
ast::ImplItem::ConstDef(it) => it.attrs(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StructKind {
Tuple(ast::TupleFieldDefList),