From ed37335c012a66f5d028a160221b1ca152a61325 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 2 Sep 2020 15:21:10 +0200 Subject: Begin refactor of import insertion --- crates/assists/src/utils/insert_use.rs | 908 ++++++++++++++++----------------- 1 file changed, 440 insertions(+), 468 deletions(-) diff --git a/crates/assists/src/utils/insert_use.rs b/crates/assists/src/utils/insert_use.rs index 49096a67c..8ee5e0c9c 100644 --- a/crates/assists/src/utils/insert_use.rs +++ b/crates/assists/src/utils/insert_use.rs @@ -1,17 +1,13 @@ -//! Handle syntactic aspects of inserting a new `use`. -// FIXME: rewrite according to the plan, outlined in -// https://github.com/rust-analyzer/rust-analyzer/issues/3301#issuecomment-592931553 - -use std::iter::successors; +use std::iter::{self, successors}; +use algo::skip_trivia_token; +use ast::{edit::AstNodeEdit, PathSegmentKind, VisibilityOwner}; use either::Either; use syntax::{ - ast::{self, NameOwner, VisibilityOwner}, - AstNode, AstToken, Direction, SmolStr, - SyntaxKind::{PATH, PATH_SEGMENT}, - SyntaxNode, SyntaxToken, T, + algo, + ast::{self, make, AstNode}, + Direction, InsertPosition, SyntaxElement, SyntaxNode, T, }; -use text_edit::TextEditBuilder; use crate::assist_context::AssistContext; @@ -22,525 +18,501 @@ pub(crate) fn find_insert_use_container( ) -> Option> { ctx.sema.ancestors_with_macros(position.clone()).find_map(|n| { if let Some(module) = ast::Module::cast(n.clone()) { - return module.item_list().map(|it| Either::Left(it)); + return module.item_list().map(Either::Left); } Some(Either::Right(ast::SourceFile::cast(n)?)) }) } -/// Creates and inserts a use statement for the given path to import. -/// The use statement is inserted in the scope most appropriate to the -/// the cursor position given, additionally merged with the existing use imports. pub(crate) fn insert_use_statement( // Ideally the position of the cursor, used to position: &SyntaxNode, path_to_import: &str, - ctx: &AssistContext, - builder: &mut TextEditBuilder, + ctx: &crate::assist_context::AssistContext, + builder: &mut text_edit::TextEditBuilder, ) { - let target = path_to_import.split("::").map(SmolStr::new).collect::>(); - let container = find_insert_use_container(position, ctx); + insert_use(position.clone(), make::path_from_text(path_to_import), Some(MergeBehaviour::Full)); +} + +pub fn insert_use( + where_: SyntaxNode, + path: ast::Path, + merge_behaviour: Option, +) -> SyntaxNode { + let use_item = make::use_(make::use_tree(path.clone(), None, None, false)); + // merge into existing imports if possible + if let Some(mb) = merge_behaviour { + for existing_use in where_.children().filter_map(ast::Use::cast) { + if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) { + let to_delete: SyntaxElement = existing_use.syntax().clone().into(); + let to_delete = to_delete.clone()..=to_delete; + let to_insert = iter::once(merged.syntax().clone().into()); + return algo::replace_children(&where_, to_delete, to_insert); + } + } + } + + // either we weren't allowed to merge or there is no import that fits the merge conditions + // so look for the place we have to insert to + let (insert_position, add_blank) = find_insert_position(&where_, path); + + let to_insert: Vec = { + let mut buf = Vec::new(); + + if add_blank == AddBlankLine::Before { + buf.push(make::tokens::single_newline().into()); + } + + buf.push(use_item.syntax().clone().into()); + + if add_blank == AddBlankLine::After { + buf.push(make::tokens::single_newline().into()); + } else if add_blank == AddBlankLine::AfterTwice { + buf.push(make::tokens::single_newline().into()); + buf.push(make::tokens::single_newline().into()); + } - if let Some(container) = container { - let syntax = container.either(|l| l.syntax().clone(), |r| r.syntax().clone()); - let action = best_action_for_target(syntax, position.clone(), &target); - make_assist(&action, &target, builder); + buf + }; + + algo::insert_children(&where_, insert_position, to_insert) +} + +fn try_merge_imports( + old: &ast::Use, + new: &ast::Use, + merge_behaviour: MergeBehaviour, +) -> Option { + // dont merge into re-exports + if old.visibility().map(|vis| vis.pub_token()).is_some() { + return None; + } + let old_tree = old.use_tree()?; + let new_tree = new.use_tree()?; + let merged = try_merge_trees(&old_tree, &new_tree, merge_behaviour)?; + Some(old.with_use_tree(merged)) +} + +/// Simple function that checks if a UseTreeList is deeper than one level +fn use_tree_list_is_nested(tl: &ast::UseTreeList) -> bool { + tl.use_trees().any(|use_tree| { + use_tree.use_tree_list().is_some() || use_tree.path().and_then(|p| p.qualifier()).is_some() + }) +} + +pub fn try_merge_trees( + old: &ast::UseTree, + new: &ast::UseTree, + merge_behaviour: MergeBehaviour, +) -> Option { + let lhs_path = old.path()?; + let rhs_path = new.path()?; + + let (lhs_prefix, rhs_prefix) = common_prefix(&lhs_path, &rhs_path)?; + let lhs = old.split_prefix(&lhs_prefix); + let rhs = new.split_prefix(&rhs_prefix); + let lhs_tl = lhs.use_tree_list()?; + let rhs_tl = rhs.use_tree_list()?; + + // if we are only allowed to merge the last level check if the paths are only one level deep + // FIXME: This shouldn't work yet i think + if merge_behaviour == MergeBehaviour::Last && use_tree_list_is_nested(&lhs_tl) + || use_tree_list_is_nested(&rhs_tl) + { + return None; } + + let should_insert_comma = lhs_tl + .r_curly_token() + .and_then(|it| skip_trivia_token(it.prev_token()?, Direction::Prev)) + .map(|it| it.kind() != T![,]) + .unwrap_or(true); + let mut to_insert: Vec = Vec::new(); + if should_insert_comma { + to_insert.push(make::token(T![,]).into()); + to_insert.push(make::tokens::single_space().into()); + } + to_insert.extend( + rhs_tl + .syntax() + .children_with_tokens() + .filter(|it| it.kind() != T!['{'] && it.kind() != T!['}']), + ); + let pos = InsertPosition::Before(lhs_tl.r_curly_token()?.into()); + let use_tree_list = lhs_tl.insert_children(pos, to_insert); + Some(lhs.with_use_tree_list(use_tree_list)) } -fn collect_path_segments_raw( - segments: &mut Vec, - mut path: ast::Path, -) -> Option { - let oldlen = segments.len(); +/// Traverses both paths until they differ, returning the common prefix of both. +fn common_prefix(lhs: &ast::Path, rhs: &ast::Path) -> Option<(ast::Path, ast::Path)> { + let mut res = None; + let mut lhs_curr = first_path(&lhs); + let mut rhs_curr = first_path(&rhs); loop { - let mut children = path.syntax().children_with_tokens(); - let (first, second, third) = ( - children.next().map(|n| (n.clone(), n.kind())), - children.next().map(|n| (n.clone(), n.kind())), - children.next().map(|n| (n.clone(), n.kind())), - ); - match (first, second, third) { - (Some((subpath, PATH)), Some((_, T![::])), Some((segment, PATH_SEGMENT))) => { - path = ast::Path::cast(subpath.as_node()?.clone())?; - segments.push(ast::PathSegment::cast(segment.as_node()?.clone())?); - } - (Some((segment, PATH_SEGMENT)), _, _) => { - segments.push(ast::PathSegment::cast(segment.as_node()?.clone())?); - break; + match (lhs_curr.segment(), rhs_curr.segment()) { + (Some(lhs), Some(rhs)) if lhs.syntax().text() == rhs.syntax().text() => (), + _ => break, + } + res = Some((lhs_curr.clone(), rhs_curr.clone())); + + match lhs_curr.parent_path().zip(rhs_curr.parent_path()) { + Some((lhs, rhs)) => { + lhs_curr = lhs; + rhs_curr = rhs; } - (_, _, _) => return None, + _ => break, } } - // We need to reverse only the new added segments - let only_new_segments = segments.split_at_mut(oldlen).1; - only_new_segments.reverse(); - Some(segments.len() - oldlen) + + res } -fn fmt_segments_raw(segments: &[SmolStr], buf: &mut String) { - let mut iter = segments.iter(); - if let Some(s) = iter.next() { - buf.push_str(s); - } - for s in iter { - buf.push_str("::"); - buf.push_str(s); - } +/// What type of merges are allowed. +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum MergeBehaviour { + /// Merge everything together creating deeply nested imports. + Full, + /// Only merge the last import level, doesn't allow import nesting. + Last, } -/// Returns the number of common segments. -fn compare_path_segments(left: &[SmolStr], right: &[ast::PathSegment]) -> usize { - left.iter().zip(right).take_while(|(l, r)| compare_path_segment(l, r)).count() +#[derive(Eq, PartialEq, PartialOrd, Ord)] +enum ImportGroup { + // the order here defines the order of new group inserts + Std, + ExternCrate, + ThisCrate, + ThisModule, + SuperModule, } -fn compare_path_segment(a: &SmolStr, b: &ast::PathSegment) -> bool { - if let Some(kb) = b.kind() { - match kb { - ast::PathSegmentKind::Name(nameref_b) => a == nameref_b.text(), - ast::PathSegmentKind::SelfKw => a == "self", - ast::PathSegmentKind::SuperKw => a == "super", - ast::PathSegmentKind::CrateKw => a == "crate", - ast::PathSegmentKind::Type { .. } => false, // not allowed in imports +impl ImportGroup { + fn new(path: &ast::Path) -> ImportGroup { + let default = ImportGroup::ExternCrate; + + let first_segment = match first_segment(path) { + Some(it) => it, + None => return default, + }; + + let kind = first_segment.kind().unwrap_or(PathSegmentKind::SelfKw); + match kind { + PathSegmentKind::SelfKw => ImportGroup::ThisModule, + PathSegmentKind::SuperKw => ImportGroup::SuperModule, + PathSegmentKind::CrateKw => ImportGroup::ThisCrate, + PathSegmentKind::Name(name) => match name.text().as_str() { + "std" => ImportGroup::Std, + "core" => ImportGroup::Std, + // FIXME: can be ThisModule as well + _ => ImportGroup::ExternCrate, + }, + PathSegmentKind::Type { .. } => unreachable!(), } - } else { - false } } -fn compare_path_segment_with_name(a: &SmolStr, b: &ast::Name) -> bool { - a == b.text() +fn first_segment(path: &ast::Path) -> Option { + first_path(path).segment() } -#[derive(Clone, Debug)] -enum ImportAction { - Nothing, - // Add a brand new use statement. - AddNewUse { - anchor: Option, // anchor node - add_after_anchor: bool, - }, - - // To split an existing use statement creating a nested import. - AddNestedImport { - // how may segments matched with the target path - common_segments: usize, - path_to_split: ast::Path, - // the first segment of path_to_split we want to add into the new nested list - first_segment_to_split: Option, - // Wether to add 'self' in addition to the target path - add_self: bool, - }, - // To add the target path to an existing nested import tree list. - AddInTreeList { - common_segments: usize, - // The UseTreeList where to add the target path - tree_list: ast::UseTreeList, - add_self: bool, - }, +fn first_path(path: &ast::Path) -> ast::Path { + successors(Some(path.clone()), ast::Path::qualifier).last().unwrap() } -impl ImportAction { - fn add_new_use(anchor: Option, add_after_anchor: bool) -> Self { - ImportAction::AddNewUse { anchor, add_after_anchor } +fn segment_iter(path: &ast::Path) -> impl Iterator + Clone { + path.syntax().children().flat_map(ast::PathSegment::cast) +} + +#[derive(PartialEq, Eq)] +enum AddBlankLine { + Before, + After, + AfterTwice, +} + +fn find_insert_position( + scope: &SyntaxNode, + insert_path: ast::Path, +) -> (InsertPosition, AddBlankLine) { + let group = ImportGroup::new(&insert_path); + let path_node_iter = scope + .children() + .filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node))) + .flat_map(|(use_, node)| use_.use_tree().and_then(|tree| tree.path()).zip(Some(node))); + // Iterator that discards anything thats not in the required grouping + // This implementation allows the user to rearrange their import groups as this only takes the first group that fits + let group_iter = path_node_iter + .clone() + .skip_while(|(path, _)| ImportGroup::new(path) != group) + .take_while(|(path, _)| ImportGroup::new(path) == group); + + let segments = segment_iter(&insert_path); + // track the last element we iterated over, if this is still None after the iteration then that means we never iterated in the first place + let mut last = None; + // find the element that would come directly after our new import + let post_insert = + group_iter.inspect(|(_, node)| last = Some(node.clone())).find(|(path, _)| { + let check_segments = segment_iter(&path); + segments + .clone() + .zip(check_segments) + .flat_map(|(seg, seg2)| seg.name_ref().zip(seg2.name_ref())) + .all(|(l, r)| l.text() <= r.text()) + }); + match post_insert { + // insert our import before that element + Some((_, node)) => (InsertPosition::Before(node.into()), AddBlankLine::After), + // there is no element after our new import, so append it to the end of the group + None => match last { + Some(node) => (InsertPosition::After(node.into()), AddBlankLine::Before), + // the group we were looking for actually doesnt exist, so insert + None => { + // similar concept here to the `last` from above + let mut last = None; + // find the group that comes after where we want to insert + let post_group = path_node_iter + .inspect(|(_, node)| last = Some(node.clone())) + .find(|(p, _)| ImportGroup::new(p) > group); + match post_group { + Some((_, node)) => { + (InsertPosition::Before(node.into()), AddBlankLine::AfterTwice) + } + // there is no such group, so append after the last one + None => match last { + Some(node) => (InsertPosition::After(node.into()), AddBlankLine::Before), + // there are no imports in this file at all + None => (InsertPosition::First, AddBlankLine::AfterTwice), + }, + } + } + }, } +} - fn add_nested_import( - common_segments: usize, - path_to_split: ast::Path, - first_segment_to_split: Option, - add_self: bool, - ) -> Self { - ImportAction::AddNestedImport { - common_segments, - path_to_split, - first_segment_to_split, - add_self, - } +#[cfg(test)] +mod tests { + use super::*; + + use test_utils::assert_eq_text; + + #[test] + fn insert_start() { + check_none( + "std::bar::A", + r"use std::bar::B; +use std::bar::D; +use std::bar::F; +use std::bar::G;", + r"use std::bar::A; +use std::bar::B; +use std::bar::D; +use std::bar::F; +use std::bar::G;", + ) } - fn add_in_tree_list( - common_segments: usize, - tree_list: ast::UseTreeList, - add_self: bool, - ) -> Self { - ImportAction::AddInTreeList { common_segments, tree_list, add_self } + #[test] + fn insert_middle() { + check_none( + "std::bar::E", + r"use std::bar::A; +use std::bar::D; +use std::bar::F; +use std::bar::G;", + r"use std::bar::A; +use std::bar::D; +use std::bar::E; +use std::bar::F; +use std::bar::G;", + ) } - fn better(left: ImportAction, right: ImportAction) -> ImportAction { - if left.is_better(&right) { - left - } else { - right - } + #[test] + fn insert_end() { + check_none( + "std::bar::Z", + r"use std::bar::A; +use std::bar::D; +use std::bar::F; +use std::bar::G;", + r"use std::bar::A; +use std::bar::D; +use std::bar::F; +use std::bar::G; +use std::bar::Z;", + ) } - fn is_better(&self, other: &ImportAction) -> bool { - match (self, other) { - (ImportAction::Nothing, _) => true, - (ImportAction::AddInTreeList { .. }, ImportAction::Nothing) => false, - ( - ImportAction::AddNestedImport { common_segments: n, .. }, - ImportAction::AddInTreeList { common_segments: m, .. }, - ) - | ( - ImportAction::AddInTreeList { common_segments: n, .. }, - ImportAction::AddNestedImport { common_segments: m, .. }, - ) - | ( - ImportAction::AddInTreeList { common_segments: n, .. }, - ImportAction::AddInTreeList { common_segments: m, .. }, - ) - | ( - ImportAction::AddNestedImport { common_segments: n, .. }, - ImportAction::AddNestedImport { common_segments: m, .. }, - ) => n > m, - (ImportAction::AddInTreeList { .. }, _) => true, - (ImportAction::AddNestedImport { .. }, ImportAction::Nothing) => false, - (ImportAction::AddNestedImport { .. }, _) => true, - (ImportAction::AddNewUse { .. }, _) => false, - } + #[test] + fn insert_middle_pnested() { + check_none( + "std::bar::E", + r"use std::bar::A; +use std::bar::{D, Z}; // example of weird imports due to user +use std::bar::F; +use std::bar::G;", + r"use std::bar::A; +use std::bar::E; +use std::bar::{D, Z}; // example of weird imports due to user +use std::bar::F; +use std::bar::G;", + ) } -} -// Find out the best ImportAction to import target path against current_use_tree. -// If current_use_tree has a nested import the function gets called recursively on every UseTree inside a UseTreeList. -fn walk_use_tree_for_best_action( - current_path_segments: &mut Vec, // buffer containing path segments - current_parent_use_tree_list: Option, // will be Some value if we are in a nested import - current_use_tree: ast::UseTree, // the use tree we are currently examinating - target: &[SmolStr], // the path we want to import -) -> ImportAction { - // We save the number of segments in the buffer so we can restore the correct segments - // before returning. Recursive call will add segments so we need to delete them. - let prev_len = current_path_segments.len(); - - let tree_list = current_use_tree.use_tree_list(); - let alias = current_use_tree.rename(); - - let path = match current_use_tree.path() { - Some(path) => path, - None => { - // If the use item don't have a path, it means it's broken (syntax error) - return ImportAction::add_new_use( - current_use_tree - .syntax() - .ancestors() - .find_map(ast::Use::cast) - .map(|it| it.syntax().clone()), - true, - ); - } - }; + #[test] + fn insert_middle_groups() { + check_none( + "foo::bar::G", + r"use std::bar::A; +use std::bar::D; + +use foo::bar::F; +use foo::bar::H;", + r"use std::bar::A; +use std::bar::D; + +use foo::bar::F; +use foo::bar::G; +use foo::bar::H;", + ) + } - // This can happen only if current_use_tree is a direct child of a UseItem - if let Some(name) = alias.and_then(|it| it.name()) { - if compare_path_segment_with_name(&target[0], &name) { - return ImportAction::Nothing; - } + #[test] + fn insert_first_matching_group() { + check_none( + "foo::bar::G", + r"use foo::bar::A; +use foo::bar::D; + +use std; + +use foo::bar::F; +use foo::bar::H;", + r"use foo::bar::A; +use foo::bar::D; +use foo::bar::G; + +use std; + +use foo::bar::F; +use foo::bar::H;", + ) } - collect_path_segments_raw(current_path_segments, path.clone()); - - // We compare only the new segments added in the line just above. - // The first prev_len segments were already compared in 'parent' recursive calls. - let left = target.split_at(prev_len).1; - let right = current_path_segments.split_at(prev_len).1; - let common = compare_path_segments(left, &right); - let mut action = match common { - 0 => ImportAction::add_new_use( - // e.g: target is std::fmt and we can have - // use foo::bar - // We add a brand new use statement - current_use_tree - .syntax() - .ancestors() - .find_map(ast::Use::cast) - .map(|it| it.syntax().clone()), - true, - ), - common if common == left.len() && left.len() == right.len() => { - // e.g: target is std::fmt and we can have - // 1- use std::fmt; - // 2- use std::fmt::{ ... } - if let Some(list) = tree_list { - // In case 2 we need to add self to the nested list - // unless it's already there - let has_self = list.use_trees().map(|it| it.path()).any(|p| { - p.and_then(|it| it.segment()) - .and_then(|it| it.kind()) - .filter(|k| *k == ast::PathSegmentKind::SelfKw) - .is_some() - }); - - if has_self { - ImportAction::Nothing - } else { - ImportAction::add_in_tree_list(current_path_segments.len(), list, true) - } - } else { - // Case 1 - ImportAction::Nothing - } - } - common if common != left.len() && left.len() == right.len() => { - // e.g: target is std::fmt and we have - // use std::io; - // We need to split. - let segments_to_split = current_path_segments.split_at(prev_len + common).1; - ImportAction::add_nested_import( - prev_len + common, - path, - Some(segments_to_split[0].clone()), - false, - ) - } - common if common == right.len() && left.len() > right.len() => { - // e.g: target is std::fmt and we can have - // 1- use std; - // 2- use std::{ ... }; - - // fallback action - let mut better_action = ImportAction::add_new_use( - current_use_tree - .syntax() - .ancestors() - .find_map(ast::Use::cast) - .map(|it| it.syntax().clone()), - true, - ); - if let Some(list) = tree_list { - // Case 2, check recursively if the path is already imported in the nested list - for u in list.use_trees() { - let child_action = walk_use_tree_for_best_action( - current_path_segments, - Some(list.clone()), - u, - target, - ); - if child_action.is_better(&better_action) { - better_action = child_action; - if let ImportAction::Nothing = better_action { - return better_action; - } - } - } - } else { - // Case 1, split adding self - better_action = ImportAction::add_nested_import(prev_len + common, path, None, true) - } - better_action - } - common if common == left.len() && left.len() < right.len() => { - // e.g: target is std::fmt and we can have - // use std::fmt::Debug; - let segments_to_split = current_path_segments.split_at(prev_len + common).1; - ImportAction::add_nested_import( - prev_len + common, - path, - Some(segments_to_split[0].clone()), - true, - ) - } - common if common < left.len() && common < right.len() => { - // e.g: target is std::fmt::nested::Debug - // use std::fmt::Display - let segments_to_split = current_path_segments.split_at(prev_len + common).1; - ImportAction::add_nested_import( - prev_len + common, - path, - Some(segments_to_split[0].clone()), - false, - ) - } - _ => unreachable!(), - }; + #[test] + fn insert_missing_group() { + check_none( + "std::fmt", + r"use foo::bar::A; +use foo::bar::D;", + r"use std::fmt; + +use foo::bar::A; +use foo::bar::D;", + ) + } - // If we are inside a UseTreeList adding a use statement become adding to the existing - // tree list. - action = match (current_parent_use_tree_list, action.clone()) { - (Some(use_tree_list), ImportAction::AddNewUse { .. }) => { - ImportAction::add_in_tree_list(prev_len, use_tree_list, false) - } - (_, _) => action, - }; + #[test] + fn insert_no_imports() { + check_full( + "foo::bar", + "fn main() {}", + r"use foo::bar; - // We remove the segments added - current_path_segments.truncate(prev_len); - action -} +fn main() {}", + ) + } -fn best_action_for_target( - container: SyntaxNode, - anchor: SyntaxNode, - target: &[SmolStr], -) -> ImportAction { - let mut storage = Vec::with_capacity(16); // this should be the only allocation - let best_action = container - .children() - .filter_map(ast::Use::cast) - .filter(|u| u.visibility().is_none()) - .filter_map(|it| it.use_tree()) - .map(|u| walk_use_tree_for_best_action(&mut storage, None, u, target)) - .fold(None, |best, a| match best { - Some(best) => Some(ImportAction::better(best, a)), - None => Some(a), - }); + #[test] + fn insert_empty_file() { + // empty files will get two trailing newlines + // this is due to the test case insert_no_imports above + check_full( + "foo::bar", + "", + r"use foo::bar; + +", + ) + } - match best_action { - Some(action) => action, - None => { - // We have no action and no UseItem was found in container so we find - // another item and we use it as anchor. - // If there are no items above, we choose the target path itself as anchor. - // todo: we should include even whitespace blocks as anchor candidates - let anchor = container.children().next().or_else(|| Some(anchor)); + #[test] + fn adds_std_group() { + check_full( + "std::fmt::Debug", + r"use stdx;", + r"use std::fmt::Debug; - let add_after_anchor = anchor - .clone() - .and_then(ast::Attr::cast) - .map(|attr| attr.kind() == ast::AttrKind::Inner) - .unwrap_or(false); - ImportAction::add_new_use(anchor, add_after_anchor) - } +use stdx;", + ) } -} -fn make_assist(action: &ImportAction, target: &[SmolStr], edit: &mut TextEditBuilder) { - match action { - ImportAction::AddNewUse { anchor, add_after_anchor } => { - make_assist_add_new_use(anchor, *add_after_anchor, target, edit) - } - ImportAction::AddInTreeList { common_segments, tree_list, add_self } => { - // We know that the fist n segments already exists in the use statement we want - // to modify, so we want to add only the last target.len() - n segments. - let segments_to_add = target.split_at(*common_segments).1; - make_assist_add_in_tree_list(tree_list, segments_to_add, *add_self, edit) - } - ImportAction::AddNestedImport { - common_segments, - path_to_split, - first_segment_to_split, - add_self, - } => { - let segments_to_add = target.split_at(*common_segments).1; - make_assist_add_nested_import( - path_to_split, - first_segment_to_split, - segments_to_add, - *add_self, - edit, - ) - } - _ => {} + #[test] + fn merges_groups() { + check_last("std::io", r"use std::fmt;", r"use std::{fmt, io};") } -} -fn make_assist_add_new_use( - anchor: &Option, - after: bool, - target: &[SmolStr], - edit: &mut TextEditBuilder, -) { - if let Some(anchor) = anchor { - let indent = leading_indent(anchor); - let mut buf = String::new(); - if after { - buf.push_str("\n"); - if let Some(spaces) = &indent { - buf.push_str(spaces); - } - } - buf.push_str("use "); - fmt_segments_raw(target, &mut buf); - buf.push_str(";"); - if !after { - buf.push_str("\n\n"); - if let Some(spaces) = &indent { - buf.push_str(&spaces); - } - } - let position = if after { anchor.text_range().end() } else { anchor.text_range().start() }; - edit.insert(position, buf); + #[test] + fn merges_groups_last() { + check_last( + "std::io", + r"use std::fmt::{Result, Display};", + r"use std::fmt::{Result, Display}; +use std::io;", + ) } -} -fn make_assist_add_in_tree_list( - tree_list: &ast::UseTreeList, - target: &[SmolStr], - add_self: bool, - edit: &mut TextEditBuilder, -) { - let last = tree_list.use_trees().last(); - if let Some(last) = last { - let mut buf = String::new(); - let comma = last.syntax().siblings(Direction::Next).find(|n| n.kind() == T![,]); - let offset = if let Some(comma) = comma { - comma.text_range().end() - } else { - buf.push_str(","); - last.syntax().text_range().end() - }; - if add_self { - buf.push_str(" self") - } else { - buf.push_str(" "); - } - fmt_segments_raw(target, &mut buf); - edit.insert(offset, buf); - } else { + #[test] + fn merges_groups2() { + check_full( + "std::io", + r"use std::fmt::{Result, Display};", + r"use std::{fmt::{Result, Display}, io};", + ) } -} -fn make_assist_add_nested_import( - path: &ast::Path, - first_segment_to_split: &Option, - target: &[SmolStr], - add_self: bool, - edit: &mut TextEditBuilder, -) { - let use_tree = path.syntax().ancestors().find_map(ast::UseTree::cast); - if let Some(use_tree) = use_tree { - let (start, add_colon_colon) = if let Some(first_segment_to_split) = first_segment_to_split - { - (first_segment_to_split.syntax().text_range().start(), false) - } else { - (use_tree.syntax().text_range().end(), true) - }; - let end = use_tree.syntax().text_range().end(); + #[test] + fn skip_merges_groups_pub() { + check_full( + "std::io", + r"pub use std::fmt::{Result, Display};", + r"pub use std::fmt::{Result, Display}; +use std::io;", + ) + } - let mut buf = String::new(); - if add_colon_colon { - buf.push_str("::"); - } - buf.push_str("{"); - if add_self { - buf.push_str("self, "); - } - fmt_segments_raw(target, &mut buf); - if !target.is_empty() { - buf.push_str(", "); - } - edit.insert(start, buf); - edit.insert(end, "}".to_string()); + #[test] + fn merges_groups_self() { + check_full("std::fmt::Debug", r"use std::fmt;", r"use std::fmt::{self, Debug};") } -} -/// If the node is on the beginning of the line, calculate indent. -fn leading_indent(node: &SyntaxNode) -> Option { - for token in prev_tokens(node.first_token()?) { - 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; - } + fn check( + path: &str, + ra_fixture_before: &str, + ra_fixture_after: &str, + mb: Option, + ) { + let file = ast::SourceFile::parse(ra_fixture_before).tree().syntax().clone(); + let path = ast::SourceFile::parse(&format!("use {};", path)) + .tree() + .syntax() + .descendants() + .find_map(ast::Path::cast) + .unwrap(); + + let result = insert_use(file, path, mb).to_string(); + assert_eq_text!(&result, ra_fixture_after); + } + + fn check_full(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { + check(path, ra_fixture_before, ra_fixture_after, Some(MergeBehaviour::Full)) } - return None; - fn prev_tokens(token: SyntaxToken) -> impl Iterator { - successors(token.prev_token(), |token| token.prev_token()) + + fn check_last(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { + check(path, ra_fixture_before, ra_fixture_after, Some(MergeBehaviour::Last)) + } + + fn check_none(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { + check(path, ra_fixture_before, ra_fixture_after, None) } } -- cgit v1.2.3