From 3f612d37c68a6e4c09e407b7cd2ad8a1d17ab4e6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 28 Nov 2020 16:30:39 +0200 Subject: Move the helpers into ide_db --- crates/assists/Cargo.toml | 1 - crates/assists/src/assist_config.rs | 2 +- crates/assists/src/ast_transform.rs | 2 +- crates/assists/src/handlers/auto_import.rs | 2 +- .../handlers/extract_struct_from_enum_variant.rs | 6 +- crates/assists/src/handlers/fill_match_arms.rs | 4 +- .../src/handlers/generate_from_impl_for_enum.rs | 2 +- crates/assists/src/handlers/merge_imports.rs | 2 +- crates/assists/src/handlers/qualify_path.rs | 2 +- .../handlers/replace_derive_with_manual_impl.rs | 2 +- .../handlers/replace_qualified_name_with_use.rs | 2 +- crates/completion/Cargo.toml | 1 - crates/completion/src/completions/record.rs | 4 +- .../completion/src/completions/unqualified_path.rs | 2 +- crates/completion/src/config.rs | 2 +- crates/completion/src/item.rs | 2 +- crates/completion/src/render.rs | 2 +- crates/ide/Cargo.toml | 1 - crates/ide/src/inlay_hints.rs | 4 +- crates/ide_db/Cargo.toml | 3 +- crates/ide_db/src/helpers.rs | 203 ++++ crates/ide_db/src/helpers/insert_use.rs | 1204 ++++++++++++++++++++ crates/ide_db/src/lib.rs | 1 + crates/ide_helpers/Cargo.toml | 23 - crates/ide_helpers/src/insert_use.rs | 1204 -------------------- crates/ide_helpers/src/lib.rs | 203 ---- crates/rust-analyzer/Cargo.toml | 3 +- crates/rust-analyzer/src/config.rs | 2 +- 28 files changed, 1433 insertions(+), 1458 deletions(-) create mode 100644 crates/ide_db/src/helpers.rs create mode 100644 crates/ide_db/src/helpers/insert_use.rs delete mode 100644 crates/ide_helpers/Cargo.toml delete mode 100644 crates/ide_helpers/src/insert_use.rs delete mode 100644 crates/ide_helpers/src/lib.rs (limited to 'crates') diff --git a/crates/assists/Cargo.toml b/crates/assists/Cargo.toml index 91d9b6e3f..3fd8327d6 100644 --- a/crates/assists/Cargo.toml +++ b/crates/assists/Cargo.toml @@ -21,4 +21,3 @@ profile = { path = "../profile", version = "0.0.0" } ide_db = { path = "../ide_db", version = "0.0.0" } hir = { path = "../hir", version = "0.0.0" } test_utils = { path = "../test_utils", version = "0.0.0" } -ide_helpers = { path = "../ide_helpers", version = "0.0.0" } diff --git a/crates/assists/src/assist_config.rs b/crates/assists/src/assist_config.rs index 6d9934e93..786224cfa 100644 --- a/crates/assists/src/assist_config.rs +++ b/crates/assists/src/assist_config.rs @@ -5,7 +5,7 @@ //! assists if we are allowed to. use hir::PrefixKind; -use ide_helpers::insert_use::MergeBehaviour; +use ide_db::helpers::insert_use::MergeBehaviour; use crate::AssistKind; diff --git a/crates/assists/src/ast_transform.rs b/crates/assists/src/ast_transform.rs index 95b060599..66e4634b1 100644 --- a/crates/assists/src/ast_transform.rs +++ b/crates/assists/src/ast_transform.rs @@ -1,6 +1,6 @@ //! `AstTransformer`s are functions that replace nodes in an AST and can be easily combined. use hir::{HirDisplay, PathResolution, SemanticsScope}; -use ide_helpers::mod_path_to_ast; +use ide_db::helpers::mod_path_to_ast; use rustc_hash::FxHashMap; use syntax::{ algo::SyntaxRewriter, diff --git a/crates/assists/src/handlers/auto_import.rs b/crates/assists/src/handlers/auto_import.rs index 0b2d508d5..bd5bba646 100644 --- a/crates/assists/src/handlers/auto_import.rs +++ b/crates/assists/src/handlers/auto_import.rs @@ -1,4 +1,4 @@ -use ide_helpers::{ +use ide_db::helpers::{ insert_use::{insert_use, ImportScope}, mod_path_to_ast, }; diff --git a/crates/assists/src/handlers/extract_struct_from_enum_variant.rs b/crates/assists/src/handlers/extract_struct_from_enum_variant.rs index fddd5354a..d85767b4e 100644 --- a/crates/assists/src/handlers/extract_struct_from_enum_variant.rs +++ b/crates/assists/src/handlers/extract_struct_from_enum_variant.rs @@ -2,11 +2,11 @@ use std::iter; use either::Either; use hir::{AsName, EnumVariant, Module, ModuleDef, Name}; -use ide_db::{defs::Definition, search::Reference, RootDatabase}; -use ide_helpers::{ +use ide_db::helpers::{ insert_use::{insert_use, ImportScope}, mod_path_to_ast, }; +use ide_db::{defs::Definition, search::Reference, RootDatabase}; use rustc_hash::{FxHashMap, FxHashSet}; use syntax::{ algo::{find_node_at_offset, SyntaxRewriter}, @@ -237,7 +237,7 @@ fn update_reference( #[cfg(test)] mod tests { - use ide_helpers::FamousDefs; + use ide_db::helpers::FamousDefs; use crate::tests::{check_assist, check_assist_not_applicable}; diff --git a/crates/assists/src/handlers/fill_match_arms.rs b/crates/assists/src/handlers/fill_match_arms.rs index bd42e0f16..ef12ef0cf 100644 --- a/crates/assists/src/handlers/fill_match_arms.rs +++ b/crates/assists/src/handlers/fill_match_arms.rs @@ -1,8 +1,8 @@ use std::iter; use hir::{Adt, HasSource, ModuleDef, Semantics}; +use ide_db::helpers::{mod_path_to_ast, FamousDefs}; use ide_db::RootDatabase; -use ide_helpers::{mod_path_to_ast, FamousDefs}; use itertools::Itertools; use syntax::ast::{self, make, AstNode, MatchArm, NameOwner, Pat}; use test_utils::mark; @@ -213,7 +213,7 @@ fn build_pat(db: &RootDatabase, module: hir::Module, var: hir::EnumVariant) -> O #[cfg(test)] mod tests { - use ide_helpers::FamousDefs; + use ide_db::helpers::FamousDefs; use test_utils::mark; use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target}; diff --git a/crates/assists/src/handlers/generate_from_impl_for_enum.rs b/crates/assists/src/handlers/generate_from_impl_for_enum.rs index 01b14d94d..3c374e5d9 100644 --- a/crates/assists/src/handlers/generate_from_impl_for_enum.rs +++ b/crates/assists/src/handlers/generate_from_impl_for_enum.rs @@ -1,5 +1,5 @@ +use ide_db::helpers::FamousDefs; use ide_db::RootDatabase; -use ide_helpers::FamousDefs; use syntax::ast::{self, AstNode, NameOwner}; use test_utils::mark; diff --git a/crates/assists/src/handlers/merge_imports.rs b/crates/assists/src/handlers/merge_imports.rs index 8207f0e6e..b7e853994 100644 --- a/crates/assists/src/handlers/merge_imports.rs +++ b/crates/assists/src/handlers/merge_imports.rs @@ -1,4 +1,4 @@ -use ide_helpers::insert_use::{try_merge_imports, try_merge_trees, MergeBehaviour}; +use ide_db::helpers::insert_use::{try_merge_imports, try_merge_trees, MergeBehaviour}; use syntax::{ algo::{neighbor, SyntaxRewriter}, ast, AstNode, diff --git a/crates/assists/src/handlers/qualify_path.rs b/crates/assists/src/handlers/qualify_path.rs index c0ee7ea0b..6f9810fe8 100644 --- a/crates/assists/src/handlers/qualify_path.rs +++ b/crates/assists/src/handlers/qualify_path.rs @@ -1,8 +1,8 @@ use std::iter; use hir::AsName; +use ide_db::helpers::mod_path_to_ast; use ide_db::RootDatabase; -use ide_helpers::mod_path_to_ast; use syntax::{ ast, ast::{make, ArgListOwner}, diff --git a/crates/assists/src/handlers/replace_derive_with_manual_impl.rs b/crates/assists/src/handlers/replace_derive_with_manual_impl.rs index fe262377c..4d6a1956b 100644 --- a/crates/assists/src/handlers/replace_derive_with_manual_impl.rs +++ b/crates/assists/src/handlers/replace_derive_with_manual_impl.rs @@ -1,5 +1,5 @@ +use ide_db::helpers::mod_path_to_ast; use ide_db::imports_locator; -use ide_helpers::mod_path_to_ast; use itertools::Itertools; use syntax::{ ast::{self, make, AstNode}, diff --git a/crates/assists/src/handlers/replace_qualified_name_with_use.rs b/crates/assists/src/handlers/replace_qualified_name_with_use.rs index 5e5c41a6f..8bdf9eea5 100644 --- a/crates/assists/src/handlers/replace_qualified_name_with_use.rs +++ b/crates/assists/src/handlers/replace_qualified_name_with_use.rs @@ -1,4 +1,4 @@ -use ide_helpers::insert_use::{insert_use, ImportScope}; +use ide_db::helpers::insert_use::{insert_use, ImportScope}; use syntax::{algo::SyntaxRewriter, ast, match_ast, AstNode, SyntaxNode}; use test_utils::mark; diff --git a/crates/completion/Cargo.toml b/crates/completion/Cargo.toml index 102de33f8..35e169a28 100644 --- a/crates/completion/Cargo.toml +++ b/crates/completion/Cargo.toml @@ -22,7 +22,6 @@ base_db = { path = "../base_db", version = "0.0.0" } ide_db = { path = "../ide_db", version = "0.0.0" } profile = { path = "../profile", version = "0.0.0" } test_utils = { path = "../test_utils", version = "0.0.0" } -ide_helpers = { path = "../ide_helpers", version = "0.0.0" } # completions crate should depend only on the top-level `hir` package. if you need # something from some `hir_xxx` subpackage, reexport the API via `hir`. diff --git a/crates/completion/src/completions/record.rs b/crates/completion/src/completions/record.rs index 218a1923c..eaa44c97d 100644 --- a/crates/completion/src/completions/record.rs +++ b/crates/completion/src/completions/record.rs @@ -1,5 +1,5 @@ //! Complete fields in record literals and patterns. -use ide_helpers::FamousDefs; +use ide_db::helpers::FamousDefs; use syntax::ast::Expr; use crate::{ @@ -46,7 +46,7 @@ pub(crate) fn complete_record(acc: &mut Completions, ctx: &CompletionContext) -> #[cfg(test)] mod tests { use expect_test::{expect, Expect}; - use ide_helpers::FamousDefs; + use ide_db::helpers::FamousDefs; use crate::{test_utils::completion_list, CompletionKind}; diff --git a/crates/completion/src/completions/unqualified_path.rs b/crates/completion/src/completions/unqualified_path.rs index db5dbb7dd..81691cd7f 100644 --- a/crates/completion/src/completions/unqualified_path.rs +++ b/crates/completion/src/completions/unqualified_path.rs @@ -2,8 +2,8 @@ use either::Either; use hir::{Adt, ModuleDef, ScopeDef, Type}; +use ide_db::helpers::insert_use::ImportScope; use ide_db::imports_locator; -use ide_helpers::insert_use::ImportScope; use syntax::AstNode; use test_utils::mark; diff --git a/crates/completion/src/config.rs b/crates/completion/src/config.rs index 1995b0754..654a76f7b 100644 --- a/crates/completion/src/config.rs +++ b/crates/completion/src/config.rs @@ -4,7 +4,7 @@ //! module, and we use to statically check that we only produce snippet //! completions if we are allowed to. -use ide_helpers::insert_use::MergeBehaviour; +use ide_db::helpers::insert_use::MergeBehaviour; #[derive(Clone, Debug, PartialEq, Eq)] pub struct CompletionConfig { diff --git a/crates/completion/src/item.rs b/crates/completion/src/item.rs index 3bfee1b3f..e85549fef 100644 --- a/crates/completion/src/item.rs +++ b/crates/completion/src/item.rs @@ -3,7 +3,7 @@ use std::fmt; use hir::{Documentation, ModPath, Mutability}; -use ide_helpers::{ +use ide_db::helpers::{ insert_use::{self, ImportScope, MergeBehaviour}, mod_path_to_ast, }; diff --git a/crates/completion/src/render.rs b/crates/completion/src/render.rs index e84aef0e4..504757a6a 100644 --- a/crates/completion/src/render.rs +++ b/crates/completion/src/render.rs @@ -10,8 +10,8 @@ pub(crate) mod type_alias; mod builder_ext; use hir::{Documentation, HasAttrs, HirDisplay, ModPath, Mutability, ScopeDef, Type}; +use ide_db::helpers::insert_use::{ImportScope, MergeBehaviour}; use ide_db::RootDatabase; -use ide_helpers::insert_use::{ImportScope, MergeBehaviour}; use syntax::TextRange; use test_utils::mark; diff --git a/crates/ide/Cargo.toml b/crates/ide/Cargo.toml index 92d4e5e9f..4d483580d 100644 --- a/crates/ide/Cargo.toml +++ b/crates/ide/Cargo.toml @@ -24,7 +24,6 @@ stdx = { path = "../stdx", version = "0.0.0" } syntax = { path = "../syntax", version = "0.0.0" } text_edit = { path = "../text_edit", version = "0.0.0" } ide_db = { path = "../ide_db", version = "0.0.0" } -ide_helpers = { path = "../ide_helpers", version = "0.0.0" } cfg = { path = "../cfg", version = "0.0.0" } profile = { path = "../profile", version = "0.0.0" } test_utils = { path = "../test_utils", version = "0.0.0" } diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs index 9c8bb7c45..65df7979c 100644 --- a/crates/ide/src/inlay_hints.rs +++ b/crates/ide/src/inlay_hints.rs @@ -1,7 +1,7 @@ use either::Either; use hir::{known, Callable, HirDisplay, Semantics}; +use ide_db::helpers::FamousDefs; use ide_db::RootDatabase; -use ide_helpers::FamousDefs; use stdx::to_lower_snake_case; use syntax::{ ast::{self, ArgListOwner, AstNode, NameOwner}, @@ -428,7 +428,7 @@ fn get_callable(sema: &Semantics, expr: &ast::Expr) -> Option ast::Path { + let _p = profile::span("mod_path_to_ast"); + + let mut segments = Vec::new(); + let mut is_abs = false; + match path.kind { + hir::PathKind::Plain => {} + hir::PathKind::Super(0) => segments.push(make::path_segment_self()), + hir::PathKind::Super(n) => segments.extend((0..n).map(|_| make::path_segment_super())), + hir::PathKind::DollarCrate(_) | hir::PathKind::Crate => { + segments.push(make::path_segment_crate()) + } + hir::PathKind::Abs => is_abs = true, + } + + segments.extend( + path.segments + .iter() + .map(|segment| make::path_segment(make::name_ref(&segment.to_string()))), + ); + make::path_from_segments(segments, is_abs) +} + +/// Helps with finding well-know things inside the standard library. This is +/// somewhat similar to the known paths infra inside hir, but it different; We +/// want to make sure that IDE specific paths don't become interesting inside +/// the compiler itself as well. +pub struct FamousDefs<'a, 'b>(pub &'a Semantics<'b, RootDatabase>, pub Option); + +#[allow(non_snake_case)] +impl FamousDefs<'_, '_> { + pub const FIXTURE: &'static str = r#"//- /libcore.rs crate:core +pub mod convert { + pub trait From { + fn from(t: T) -> Self; + } +} + +pub mod default { + pub trait Default { + fn default() -> Self; + } +} + +pub mod iter { + pub use self::traits::{collect::IntoIterator, iterator::Iterator}; + mod traits { + pub(crate) mod iterator { + use crate::option::Option; + pub trait Iterator { + type Item; + fn next(&mut self) -> Option; + fn by_ref(&mut self) -> &mut Self { + self + } + fn take(self, n: usize) -> crate::iter::Take { + crate::iter::Take { inner: self } + } + } + + impl Iterator for &mut I { + type Item = I::Item; + fn next(&mut self) -> Option { + (**self).next() + } + } + } + pub(crate) mod collect { + pub trait IntoIterator { + type Item; + } + } + } + + pub use self::sources::*; + pub(crate) mod sources { + use super::Iterator; + use crate::option::Option::{self, *}; + pub struct Repeat { + element: A, + } + + pub fn repeat(elt: T) -> Repeat { + Repeat { element: elt } + } + + impl Iterator for Repeat { + type Item = A; + + fn next(&mut self) -> Option { + None + } + } + } + + pub use self::adapters::*; + pub(crate) mod adapters { + use super::Iterator; + use crate::option::Option::{self, *}; + pub struct Take { pub(crate) inner: I } + impl Iterator for Take where I: Iterator { + type Item = ::Item; + fn next(&mut self) -> Option<::Item> { + None + } + } + } +} + +pub mod option { + pub enum Option { None, Some(T)} +} + +pub mod prelude { + pub use crate::{convert::From, iter::{IntoIterator, Iterator}, option::Option::{self, *}, default::Default}; +} +#[prelude_import] +pub use prelude::*; +"#; + + pub fn core(&self) -> Option { + self.find_crate("core") + } + + pub fn core_convert_From(&self) -> Option { + self.find_trait("core:convert:From") + } + + pub fn core_option_Option(&self) -> Option { + self.find_enum("core:option:Option") + } + + pub fn core_default_Default(&self) -> Option { + self.find_trait("core:default:Default") + } + + pub fn core_iter_Iterator(&self) -> Option { + self.find_trait("core:iter:traits:iterator:Iterator") + } + + pub fn core_iter(&self) -> Option { + self.find_module("core:iter") + } + + fn find_trait(&self, path: &str) -> Option { + match self.find_def(path)? { + hir::ScopeDef::ModuleDef(hir::ModuleDef::Trait(it)) => Some(it), + _ => None, + } + } + + fn find_enum(&self, path: &str) -> Option { + match self.find_def(path)? { + hir::ScopeDef::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Enum(it))) => Some(it), + _ => None, + } + } + + fn find_module(&self, path: &str) -> Option { + match self.find_def(path)? { + hir::ScopeDef::ModuleDef(hir::ModuleDef::Module(it)) => Some(it), + _ => None, + } + } + + fn find_crate(&self, name: &str) -> Option { + let krate = self.1?; + let db = self.0.db; + let res = + krate.dependencies(db).into_iter().find(|dep| dep.name.to_string() == name)?.krate; + Some(res) + } + + fn find_def(&self, path: &str) -> Option { + let db = self.0.db; + let mut path = path.split(':'); + let trait_ = path.next_back()?; + let std_crate = path.next()?; + let std_crate = self.find_crate(std_crate)?; + let mut module = std_crate.root_module(db); + for segment in path { + module = module.children(db).find_map(|child| { + let name = child.name(db)?; + if name.to_string() == segment { + Some(child) + } else { + None + } + })?; + } + let def = + module.scope(db, None).into_iter().find(|(name, _def)| name.to_string() == trait_)?.1; + Some(def) + } +} diff --git a/crates/ide_db/src/helpers/insert_use.rs b/crates/ide_db/src/helpers/insert_use.rs new file mode 100644 index 000000000..67e800fad --- /dev/null +++ b/crates/ide_db/src/helpers/insert_use.rs @@ -0,0 +1,1204 @@ +//! Handle syntactic aspects of inserting a new `use`. +use std::{cmp::Ordering, iter::successors}; + +use crate::RootDatabase; +use hir::Semantics; +use itertools::{EitherOrBoth, Itertools}; +use syntax::{ + algo::SyntaxRewriter, + ast::{ + self, + edit::{AstNodeEdit, IndentLevel}, + make, AstNode, PathSegmentKind, VisibilityOwner, + }, + AstToken, InsertPosition, NodeOrToken, SyntaxElement, SyntaxNode, SyntaxToken, +}; +use test_utils::mark; + +#[derive(Debug, Clone)] +pub enum ImportScope { + File(ast::SourceFile), + Module(ast::ItemList), +} + +impl ImportScope { + pub fn from(syntax: SyntaxNode) -> Option { + if let Some(module) = ast::Module::cast(syntax.clone()) { + module.item_list().map(ImportScope::Module) + } else if let this @ Some(_) = ast::SourceFile::cast(syntax.clone()) { + this.map(ImportScope::File) + } else { + ast::ItemList::cast(syntax).map(ImportScope::Module) + } + } + + /// Determines the containing syntax node in which to insert a `use` statement affecting `position`. + pub fn find_insert_use_container( + position: &SyntaxNode, + sema: &Semantics<'_, RootDatabase>, + ) -> Option { + sema.ancestors_with_macros(position.clone()).find_map(Self::from) + } + + pub fn as_syntax_node(&self) -> &SyntaxNode { + match self { + ImportScope::File(file) => file.syntax(), + ImportScope::Module(item_list) => item_list.syntax(), + } + } + + fn indent_level(&self) -> IndentLevel { + match self { + ImportScope::File(file) => file.indent_level(), + ImportScope::Module(item_list) => item_list.indent_level() + 1, + } + } + + fn first_insert_pos(&self) -> (InsertPosition, AddBlankLine) { + match self { + ImportScope::File(_) => (InsertPosition::First, AddBlankLine::AfterTwice), + // don't insert the imports before the item list's opening curly brace + ImportScope::Module(item_list) => item_list + .l_curly_token() + .map(|b| (InsertPosition::After(b.into()), AddBlankLine::Around)) + .unwrap_or((InsertPosition::First, AddBlankLine::AfterTwice)), + } + } + + fn insert_pos_after_last_inner_element(&self) -> (InsertPosition, AddBlankLine) { + self.as_syntax_node() + .children_with_tokens() + .filter(|child| match child { + NodeOrToken::Node(node) => is_inner_attribute(node.clone()), + NodeOrToken::Token(token) => is_inner_comment(token.clone()), + }) + .last() + .map(|last_inner_element| { + (InsertPosition::After(last_inner_element.into()), AddBlankLine::BeforeTwice) + }) + .unwrap_or_else(|| self.first_insert_pos()) + } +} + +fn is_inner_attribute(node: SyntaxNode) -> bool { + ast::Attr::cast(node).map(|attr| attr.kind()) == Some(ast::AttrKind::Inner) +} + +fn is_inner_comment(token: SyntaxToken) -> bool { + ast::Comment::cast(token).and_then(|comment| comment.kind().doc) + == Some(ast::CommentPlacement::Inner) +} + +/// Insert an import path into the given file/node. A `merge` value of none indicates that no import merging is allowed to occur. +pub fn insert_use<'a>( + scope: &ImportScope, + path: ast::Path, + merge: Option, +) -> SyntaxRewriter<'a> { + let _p = profile::span("insert_use"); + let mut rewriter = SyntaxRewriter::default(); + let use_item = make::use_(make::use_tree(path.clone(), None, None, false)); + // merge into existing imports if possible + if let Some(mb) = merge { + for existing_use in scope.as_syntax_node().children().filter_map(ast::Use::cast) { + if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) { + rewriter.replace(existing_use.syntax(), merged.syntax()); + return rewriter; + } + } + } + + // 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(scope, path); + + let indent = if let ident_level @ 1..=usize::MAX = scope.indent_level().0 as usize { + Some(make::tokens::whitespace(&" ".repeat(4 * ident_level)).into()) + } else { + None + }; + + let to_insert: Vec = { + let mut buf = Vec::new(); + + match add_blank { + AddBlankLine::Before | AddBlankLine::Around => { + buf.push(make::tokens::single_newline().into()) + } + AddBlankLine::BeforeTwice => buf.push(make::tokens::blank_line().into()), + _ => (), + } + + if add_blank.has_before() { + if let Some(indent) = indent.clone() { + mark::hit!(insert_use_indent_before); + buf.push(indent); + } + } + + buf.push(use_item.syntax().clone().into()); + + match add_blank { + AddBlankLine::After | AddBlankLine::Around => { + buf.push(make::tokens::single_newline().into()) + } + AddBlankLine::AfterTwice => buf.push(make::tokens::blank_line().into()), + _ => (), + } + + // only add indentation *after* our stuff if there's another node directly after it + if add_blank.has_after() && matches!(insert_position, InsertPosition::Before(_)) { + if let Some(indent) = indent { + mark::hit!(insert_use_indent_after); + buf.push(indent); + } + } else if add_blank.has_after() && matches!(insert_position, InsertPosition::After(_)) { + mark::hit!(insert_use_no_indent_after); + } + + buf + }; + + match insert_position { + InsertPosition::First => { + rewriter.insert_many_as_first_children(scope.as_syntax_node(), to_insert) + } + InsertPosition::Last => return rewriter, // actually unreachable + InsertPosition::Before(anchor) => rewriter.insert_many_before(&anchor, to_insert), + InsertPosition::After(anchor) => rewriter.insert_many_after(&anchor, to_insert), + } + rewriter +} + +fn eq_visibility(vis0: Option, vis1: Option) -> bool { + match (vis0, vis1) { + (None, None) => true, + // FIXME: Don't use the string representation to check for equality + // spaces inside of the node would break this comparison + (Some(vis0), Some(vis1)) => vis0.to_string() == vis1.to_string(), + _ => false, + } +} + +pub fn try_merge_imports( + lhs: &ast::Use, + rhs: &ast::Use, + merge_behaviour: MergeBehaviour, +) -> Option { + // don't merge imports with different visibilities + if !eq_visibility(lhs.visibility(), rhs.visibility()) { + return None; + } + let lhs_tree = lhs.use_tree()?; + let rhs_tree = rhs.use_tree()?; + let merged = try_merge_trees(&lhs_tree, &rhs_tree, merge_behaviour)?; + Some(lhs.with_use_tree(merged)) +} + +pub fn try_merge_trees( + lhs: &ast::UseTree, + rhs: &ast::UseTree, + merge: MergeBehaviour, +) -> Option { + let lhs_path = lhs.path()?; + let rhs_path = rhs.path()?; + + let (lhs_prefix, rhs_prefix) = common_prefix(&lhs_path, &rhs_path)?; + let (lhs, rhs) = if is_simple_path(lhs) + && is_simple_path(rhs) + && lhs_path == lhs_prefix + && rhs_path == rhs_prefix + { + (lhs.clone(), rhs.clone()) + } else { + (lhs.split_prefix(&lhs_prefix), rhs.split_prefix(&rhs_prefix)) + }; + recursive_merge(&lhs, &rhs, merge) +} + +/// Recursively "zips" together lhs and rhs. +fn recursive_merge( + lhs: &ast::UseTree, + rhs: &ast::UseTree, + merge: MergeBehaviour, +) -> Option { + let mut use_trees = lhs + .use_tree_list() + .into_iter() + .flat_map(|list| list.use_trees()) + // we use Option here to early return from this function(this is not the same as a `filter` op) + .map(|tree| match merge.is_tree_allowed(&tree) { + true => Some(tree), + false => None, + }) + .collect::>>()?; + use_trees.sort_unstable_by(|a, b| path_cmp_for_sort(a.path(), b.path())); + for rhs_t in rhs.use_tree_list().into_iter().flat_map(|list| list.use_trees()) { + if !merge.is_tree_allowed(&rhs_t) { + return None; + } + let rhs_path = rhs_t.path(); + match use_trees.binary_search_by(|lhs_t| { + let (lhs_t, rhs_t) = match lhs_t + .path() + .zip(rhs_path.clone()) + .and_then(|(lhs, rhs)| common_prefix(&lhs, &rhs)) + { + Some((lhs_p, rhs_p)) => (lhs_t.split_prefix(&lhs_p), rhs_t.split_prefix(&rhs_p)), + None => (lhs_t.clone(), rhs_t.clone()), + }; + + path_cmp_bin_search(lhs_t.path(), rhs_t.path()) + }) { + Ok(idx) => { + let lhs_t = &mut use_trees[idx]; + let lhs_path = lhs_t.path()?; + let rhs_path = rhs_path?; + let (lhs_prefix, rhs_prefix) = common_prefix(&lhs_path, &rhs_path)?; + if lhs_prefix == lhs_path && rhs_prefix == rhs_path { + let tree_is_self = |tree: ast::UseTree| { + tree.path().as_ref().map(path_is_self).unwrap_or(false) + }; + // check if only one of the two trees has a tree list, and whether that then contains `self` or not. + // If this is the case we can skip this iteration since the path without the list is already included in the other one via `self` + let tree_contains_self = |tree: &ast::UseTree| { + tree.use_tree_list() + .map(|tree_list| tree_list.use_trees().any(tree_is_self)) + .unwrap_or(false) + }; + match (tree_contains_self(&lhs_t), tree_contains_self(&rhs_t)) { + (true, false) => continue, + (false, true) => { + *lhs_t = rhs_t; + continue; + } + _ => (), + } + + // glob imports arent part of the use-tree lists so we need to special handle them here as well + // this special handling is only required for when we merge a module import into a glob import of said module + // see the `merge_self_glob` or `merge_mod_into_glob` tests + if lhs_t.star_token().is_some() || rhs_t.star_token().is_some() { + *lhs_t = make::use_tree( + make::path_unqualified(make::path_segment_self()), + None, + None, + false, + ); + use_trees.insert(idx, make::glob_use_tree()); + continue; + } + + if lhs_t.use_tree_list().is_none() && rhs_t.use_tree_list().is_none() { + continue; + } + } + let lhs = lhs_t.split_prefix(&lhs_prefix); + let rhs = rhs_t.split_prefix(&rhs_prefix); + match recursive_merge(&lhs, &rhs, merge) { + Some(use_tree) => use_trees[idx] = use_tree, + None => return None, + } + } + Err(_) + if merge == MergeBehaviour::Last + && use_trees.len() > 0 + && rhs_t.use_tree_list().is_some() => + { + return None + } + Err(idx) => { + use_trees.insert(idx, rhs_t); + } + } + } + Some(lhs.with_use_tree_list(make::use_tree_list(use_trees))) +} + +/// 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 { + match (lhs_curr.segment(), rhs_curr.segment()) { + (Some(lhs), Some(rhs)) if lhs.syntax().text() == rhs.syntax().text() => (), + _ => break res, + } + 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; + } + _ => break res, + } + } +} + +fn is_simple_path(use_tree: &ast::UseTree) -> bool { + use_tree.use_tree_list().is_none() && use_tree.star_token().is_none() +} + +fn path_is_self(path: &ast::Path) -> bool { + path.segment().and_then(|seg| seg.self_token()).is_some() && path.qualifier().is_none() +} + +#[inline] +fn first_segment(path: &ast::Path) -> Option { + first_path(path).segment() +} + +fn first_path(path: &ast::Path) -> ast::Path { + successors(Some(path.clone()), ast::Path::qualifier).last().unwrap() +} + +fn segment_iter(path: &ast::Path) -> impl Iterator + Clone { + // cant make use of SyntaxNode::siblings, because the returned Iterator is not clone + successors(first_segment(path), |p| p.parent_path().parent_path().and_then(|p| p.segment())) +} + +fn path_len(path: ast::Path) -> usize { + segment_iter(&path).count() +} + +/// Orders paths in the following way: +/// the sole self token comes first, after that come uppercase identifiers, then lowercase identifiers +// FIXME: rustfmt sorts lowercase idents before uppercase, in general we want to have the same ordering rustfmt has +// which is `self` and `super` first, then identifier imports with lowercase ones first, then glob imports and at last list imports. +// Example foo::{self, foo, baz, Baz, Qux, *, {Bar}} +fn path_cmp_for_sort(a: Option, b: Option) -> Ordering { + match (a, b) { + (None, None) => Ordering::Equal, + (None, Some(_)) => Ordering::Less, + (Some(_), None) => Ordering::Greater, + (Some(ref a), Some(ref b)) => match (path_is_self(a), path_is_self(b)) { + (true, true) => Ordering::Equal, + (true, false) => Ordering::Less, + (false, true) => Ordering::Greater, + (false, false) => path_cmp_short(a, b), + }, + } +} + +/// Path comparison func for binary searching for merging. +fn path_cmp_bin_search(lhs: Option, rhs: Option) -> Ordering { + match (lhs.and_then(|path| path.segment()), rhs.and_then(|path| path.segment())) { + (None, None) => Ordering::Equal, + (None, Some(_)) => Ordering::Less, + (Some(_), None) => Ordering::Greater, + (Some(ref a), Some(ref b)) => path_segment_cmp(a, b), + } +} + +/// Short circuiting comparison, if both paths are equal until one of them ends they are considered +/// equal +fn path_cmp_short(a: &ast::Path, b: &ast::Path) -> Ordering { + let a = segment_iter(a); + let b = segment_iter(b); + // cmp_by would be useful for us here but that is currently unstable + // cmp doesnt work due the lifetimes on text's return type + a.zip(b) + .find_map(|(a, b)| match path_segment_cmp(&a, &b) { + Ordering::Equal => None, + ord => Some(ord), + }) + .unwrap_or(Ordering::Equal) +} + +/// Compares to paths, if one ends earlier than the other the has_tl parameters decide which is +/// greater as a a path that has a tree list should be greater, while one that just ends without +/// a tree list should be considered less. +fn use_tree_path_cmp(a: &ast::Path, a_has_tl: bool, b: &ast::Path, b_has_tl: bool) -> Ordering { + let a_segments = segment_iter(a); + let b_segments = segment_iter(b); + // cmp_by would be useful for us here but that is currently unstable + // cmp doesnt work due the lifetimes on text's return type + a_segments + .zip_longest(b_segments) + .find_map(|zipped| match zipped { + EitherOrBoth::Both(ref a, ref b) => match path_segment_cmp(a, b) { + Ordering::Equal => None, + ord => Some(ord), + }, + EitherOrBoth::Left(_) if !b_has_tl => Some(Ordering::Greater), + EitherOrBoth::Left(_) => Some(Ordering::Less), + EitherOrBoth::Right(_) if !a_has_tl => Some(Ordering::Less), + EitherOrBoth::Right(_) => Some(Ordering::Greater), + }) + .unwrap_or(Ordering::Equal) +} + +fn path_segment_cmp(a: &ast::PathSegment, b: &ast::PathSegment) -> Ordering { + let a = a.name_ref(); + let b = b.name_ref(); + a.as_ref().map(ast::NameRef::text).cmp(&b.as_ref().map(ast::NameRef::text)) +} + +/// What type of merges are allowed. +#[derive(Copy, Clone, Debug, 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, +} + +impl MergeBehaviour { + #[inline] + fn is_tree_allowed(&self, tree: &ast::UseTree) -> bool { + match self { + MergeBehaviour::Full => true, + // only simple single segment paths are allowed + MergeBehaviour::Last => { + tree.use_tree_list().is_none() && tree.path().map(path_len) <= Some(1) + } + } + } +} + +#[derive(Eq, PartialEq, PartialOrd, Ord)] +enum ImportGroup { + // the order here defines the order of new group inserts + Std, + ExternCrate, + ThisCrate, + ThisModule, + SuperModule, +} + +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, + _ => ImportGroup::ExternCrate, + }, + PathSegmentKind::Type { .. } => unreachable!(), + } + } +} + +#[derive(PartialEq, Eq)] +enum AddBlankLine { + Before, + BeforeTwice, + Around, + After, + AfterTwice, +} + +impl AddBlankLine { + fn has_before(&self) -> bool { + matches!(self, AddBlankLine::Before | AddBlankLine::BeforeTwice | AddBlankLine::Around) + } + fn has_after(&self) -> bool { + matches!(self, AddBlankLine::After | AddBlankLine::AfterTwice | AddBlankLine::Around) + } +} + +fn find_insert_position( + scope: &ImportScope, + insert_path: ast::Path, +) -> (InsertPosition, AddBlankLine) { + let group = ImportGroup::new(&insert_path); + let path_node_iter = scope + .as_syntax_node() + .children() + .filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node))) + .flat_map(|(use_, node)| { + let tree = use_.use_tree()?; + let path = tree.path()?; + let has_tl = tree.use_tree_list().is_some(); + Some((path, has_tl, 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); + + // 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( + |&(ref path, has_tl, _)| { + use_tree_path_cmp(&insert_path, false, path, has_tl) != Ordering::Greater + }, + ); + 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::BeforeTwice) + } + // there are no imports in this file at all + None => scope.insert_pos_after_last_inner_element(), + }, + } + } + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use test_utils::assert_eq_text; + + #[test] + fn insert_existing() { + check_full("std::fs", "use std::fs;", "use std::fs;") + } + + #[test] + fn insert_start() { + check_none( + "std::bar::AA", + r" +use std::bar::B; +use std::bar::D; +use std::bar::F; +use std::bar::G;", + r" +use std::bar::AA; +use std::bar::B; +use std::bar::D; +use std::bar::F; +use std::bar::G;", + ) + } + + #[test] + fn insert_start_indent() { + mark::check!(insert_use_indent_after); + check_none( + "std::bar::AA", + r" + use std::bar::B; + use std::bar::D;", + r" + use std::bar::AA; + use std::bar::B; + use std::bar::D;", + ) + } + + #[test] + fn insert_middle() { + check_none( + "std::bar::EE", + 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::EE; +use std::bar::F; +use std::bar::G;", + ) + } + + #[test] + fn insert_middle_indent() { + check_none( + "std::bar::EE", + 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::EE; + use std::bar::F; + use std::bar::G;", + ) + } + + #[test] + fn insert_end() { + check_none( + "std::bar::ZZ", + 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::ZZ;", + ) + } + + #[test] + fn insert_end_indent() { + mark::check!(insert_use_indent_before); + check_none( + "std::bar::ZZ", + 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::ZZ;", + ) + } + + #[test] + fn insert_middle_nested() { + check_none( + "std::bar::EE", + 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::EE; +use std::bar::{D, Z}; // example of weird imports due to user +use std::bar::F; +use std::bar::G;", + ) + } + + #[test] + fn insert_middle_groups() { + check_none( + "foo::bar::GG", + 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::GG; + use foo::bar::H;", + ) + } + + #[test] + fn insert_first_matching_group() { + check_none( + "foo::bar::GG", + 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::GG; + + use std; + + use foo::bar::F; + use foo::bar::H;", + ) + } + + #[test] + fn insert_missing_group_std() { + 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;", + ) + } + + #[test] + fn insert_missing_group_self() { + check_none( + "self::fmt", + r" +use foo::bar::A; +use foo::bar::D;", + r" +use foo::bar::A; +use foo::bar::D; + +use self::fmt;", + ) + } + + #[test] + fn insert_no_imports() { + check_full( + "foo::bar", + "fn main() {}", + r"use foo::bar; + +fn main() {}", + ) + } + + #[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; + +", + ) + } + + #[test] + fn insert_empty_module() { + mark::check!(insert_use_no_indent_after); + check( + "foo::bar", + "mod x {}", + r"{ + use foo::bar; +}", + None, + true, + ) + } + + #[test] + fn insert_after_inner_attr() { + check_full( + "foo::bar", + r"#![allow(unused_imports)]", + r"#![allow(unused_imports)] + +use foo::bar;", + ) + } + + #[test] + fn insert_after_inner_attr2() { + check_full( + "foo::bar", + r"#![allow(unused_imports)] + +#![no_std] +fn main() {}", + r"#![allow(unused_imports)] + +#![no_std] + +use foo::bar; +fn main() {}", + ); + } + + #[test] + fn inserts_after_single_line_inner_comments() { + check_none( + "foo::bar::Baz", + "//! Single line inner comments do not allow any code before them.", + r#"//! Single line inner comments do not allow any code before them. + +use foo::bar::Baz;"#, + ); + } + + #[test] + fn inserts_after_multiline_inner_comments() { + check_none( + "foo::bar::Baz", + r#"/*! Multiline inner comments do not allow any code before them. */ + +/*! Still an inner comment, cannot place any code before. */ +fn main() {}"#, + r#"/*! Multiline inner comments do not allow any code before them. */ + +/*! Still an inner comment, cannot place any code before. */ + +use foo::bar::Baz; +fn main() {}"#, + ) + } + + #[test] + fn inserts_after_all_inner_items() { + check_none( + "foo::bar::Baz", + r#"#![allow(unused_imports)] +/*! Multiline line comment 2 */ + + +//! Single line comment 1 +#![no_std] +//! Single line comment 2 +fn main() {}"#, + r#"#![allow(unused_imports)] +/*! Multiline line comment 2 */ + + +//! Single line comment 1 +#![no_std] +//! Single line comment 2 + +use foo::bar::Baz; +fn main() {}"#, + ) + } + + #[test] + fn merge_groups() { + check_last("std::io", r"use std::fmt;", r"use std::{fmt, io};") + } + + #[test] + fn merge_groups_last() { + check_last( + "std::io", + r"use std::fmt::{Result, Display};", + r"use std::fmt::{Result, Display}; +use std::io;", + ) + } + + #[test] + fn merge_last_into_self() { + check_last("foo::bar::baz", r"use foo::bar;", r"use foo::bar::{self, baz};"); + } + + #[test] + fn merge_groups_full() { + check_full( + "std::io", + r"use std::fmt::{Result, Display};", + r"use std::{fmt::{Result, Display}, io};", + ) + } + + #[test] + fn merge_groups_long_full() { + check_full( + "std::foo::bar::Baz", + r"use std::foo::bar::Qux;", + r"use std::foo::bar::{Baz, Qux};", + ) + } + + #[test] + fn merge_groups_long_last() { + check_last( + "std::foo::bar::Baz", + r"use std::foo::bar::Qux;", + r"use std::foo::bar::{Baz, Qux};", + ) + } + + #[test] + fn merge_groups_long_full_list() { + check_full( + "std::foo::bar::Baz", + r"use std::foo::bar::{Qux, Quux};", + r"use std::foo::bar::{Baz, Quux, Qux};", + ) + } + + #[test] + fn merge_groups_long_last_list() { + check_last( + "std::foo::bar::Baz", + r"use std::foo::bar::{Qux, Quux};", + r"use std::foo::bar::{Baz, Quux, Qux};", + ) + } + + #[test] + fn merge_groups_long_full_nested() { + check_full( + "std::foo::bar::Baz", + r"use std::foo::bar::{Qux, quux::{Fez, Fizz}};", + r"use std::foo::bar::{Baz, Qux, quux::{Fez, Fizz}};", + ) + } + + #[test] + fn merge_groups_long_last_nested() { + check_last( + "std::foo::bar::Baz", + r"use std::foo::bar::{Qux, quux::{Fez, Fizz}};", + r"use std::foo::bar::Baz; +use std::foo::bar::{Qux, quux::{Fez, Fizz}};", + ) + } + + #[test] + fn merge_groups_full_nested_deep() { + check_full( + "std::foo::bar::quux::Baz", + r"use std::foo::bar::{Qux, quux::{Fez, Fizz}};", + r"use std::foo::bar::{Qux, quux::{Baz, Fez, Fizz}};", + ) + } + + #[test] + fn merge_groups_full_nested_long() { + check_full( + "std::foo::bar::Baz", + r"use std::{foo::bar::Qux};", + r"use std::{foo::bar::{Baz, Qux}};", + ); + } + + #[test] + fn merge_groups_last_nested_long() { + check_full( + "std::foo::bar::Baz", + r"use std::{foo::bar::Qux};", + r"use std::{foo::bar::{Baz, Qux}};", + ); + } + + #[test] + fn merge_groups_skip_pub() { + check_full( + "std::io", + r"pub use std::fmt::{Result, Display};", + r"pub use std::fmt::{Result, Display}; +use std::io;", + ) + } + + #[test] + fn merge_groups_skip_pub_crate() { + check_full( + "std::io", + r"pub(crate) use std::fmt::{Result, Display};", + r"pub(crate) use std::fmt::{Result, Display}; +use std::io;", + ) + } + + #[test] + #[ignore] // FIXME: Support this + fn split_out_merge() { + check_last( + "std::fmt::Result", + r"use std::{fmt, io};", + r"use std::fmt::{self, Result}; +use std::io;", + ) + } + + #[test] + fn merge_into_module_import() { + check_full( + "std::fmt::Result", + r"use std::{fmt, io};", + r"use std::{fmt::{self, Result}, io};", + ) + } + + #[test] + fn merge_groups_self() { + check_full("std::fmt::Debug", r"use std::fmt;", r"use std::fmt::{self, Debug};") + } + + #[test] + fn merge_mod_into_glob() { + check_full( + "token::TokenKind", + r"use token::TokenKind::*;", + r"use token::TokenKind::{*, self};", + ) + // FIXME: have it emit `use token::TokenKind::{self, *}`? + } + + #[test] + fn merge_self_glob() { + check_full("self", r"use self::*;", r"use self::{*, self};") + // FIXME: have it emit `use {self, *}`? + } + + #[test] + fn merge_glob_nested() { + check_full( + "foo::bar::quux::Fez", + r"use foo::bar::{Baz, quux::*};", + r"use foo::bar::{Baz, quux::{self::*, Fez}};", + ) + } + + #[test] + fn skip_merge_last_too_long() { + check_last( + "foo::bar", + r"use foo::bar::baz::Qux;", + r"use foo::bar; +use foo::bar::baz::Qux;", + ); + } + + #[test] + fn skip_merge_last_too_long2() { + check_last( + "foo::bar::baz::Qux", + r"use foo::bar;", + r"use foo::bar; +use foo::bar::baz::Qux;", + ); + } + + #[test] + fn insert_short_before_long() { + check_none( + "foo::bar", + r"use foo::bar::baz::Qux;", + r"use foo::bar; +use foo::bar::baz::Qux;", + ); + } + + #[test] + fn merge_last_fail() { + check_merge_only_fail( + r"use foo::bar::{baz::{Qux, Fez}};", + r"use foo::bar::{baaz::{Quux, Feez}};", + MergeBehaviour::Last, + ); + } + + #[test] + fn merge_last_fail1() { + check_merge_only_fail( + r"use foo::bar::{baz::{Qux, Fez}};", + r"use foo::bar::baaz::{Quux, Feez};", + MergeBehaviour::Last, + ); + } + + #[test] + fn merge_last_fail2() { + check_merge_only_fail( + r"use foo::bar::baz::{Qux, Fez};", + r"use foo::bar::{baaz::{Quux, Feez}};", + MergeBehaviour::Last, + ); + } + + #[test] + fn merge_last_fail3() { + check_merge_only_fail( + r"use foo::bar::baz::{Qux, Fez};", + r"use foo::bar::baaz::{Quux, Feez};", + MergeBehaviour::Last, + ); + } + + fn check( + path: &str, + ra_fixture_before: &str, + ra_fixture_after: &str, + mb: Option, + module: bool, + ) { + let mut syntax = ast::SourceFile::parse(ra_fixture_before).tree().syntax().clone(); + if module { + syntax = syntax.descendants().find_map(ast::Module::cast).unwrap().syntax().clone(); + } + let file = super::ImportScope::from(syntax).unwrap(); + let path = ast::SourceFile::parse(&format!("use {};", path)) + .tree() + .syntax() + .descendants() + .find_map(ast::Path::cast) + .unwrap(); + + let rewriter = insert_use(&file, path, mb); + let result = rewriter.rewrite(file.as_syntax_node()).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), false) + } + + fn check_last(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { + check(path, ra_fixture_before, ra_fixture_after, Some(MergeBehaviour::Last), false) + } + + fn check_none(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { + check(path, ra_fixture_before, ra_fixture_after, None, false) + } + + fn check_merge_only_fail(ra_fixture0: &str, ra_fixture1: &str, mb: MergeBehaviour) { + let use0 = ast::SourceFile::parse(ra_fixture0) + .tree() + .syntax() + .descendants() + .find_map(ast::Use::cast) + .unwrap(); + + let use1 = ast::SourceFile::parse(ra_fixture1) + .tree() + .syntax() + .descendants() + .find_map(ast::Use::cast) + .unwrap(); + + let result = try_merge_imports(&use0, &use1, mb); + assert_eq!(result.map(|u| u.to_string()), None); + } +} diff --git a/crates/ide_db/src/lib.rs b/crates/ide_db/src/lib.rs index 05139a651..fceaa089a 100644 --- a/crates/ide_db/src/lib.rs +++ b/crates/ide_db/src/lib.rs @@ -13,6 +13,7 @@ pub mod source_change; pub mod ty_filter; pub mod traits; pub mod call_info; +pub mod helpers; use std::{fmt, sync::Arc}; diff --git a/crates/ide_helpers/Cargo.toml b/crates/ide_helpers/Cargo.toml deleted file mode 100644 index c5065b22b..000000000 --- a/crates/ide_helpers/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "ide_helpers" -version = "0.0.0" -description = "A set of helper methods shared between various ide-level modules" -license = "MIT OR Apache-2.0" -authors = ["rust-analyzer developers"] -edition = "2018" - -[lib] -doctest = false - -[features] -wasm = [] - -[dependencies] -either = "1.6.1" -itertools = "0.9.0" - -syntax = { path = "../syntax", version = "0.0.0" } -ide_db = { path = "../ide_db", version = "0.0.0" } -hir = { path = "../hir", version = "0.0.0" } -profile = { path = "../profile", version = "0.0.0" } -test_utils = { path = "../test_utils", version = "0.0.0" } diff --git a/crates/ide_helpers/src/insert_use.rs b/crates/ide_helpers/src/insert_use.rs deleted file mode 100644 index 795009eb1..000000000 --- a/crates/ide_helpers/src/insert_use.rs +++ /dev/null @@ -1,1204 +0,0 @@ -//! Handle syntactic aspects of inserting a new `use`. -use std::{cmp::Ordering, iter::successors}; - -use hir::Semantics; -use ide_db::RootDatabase; -use itertools::{EitherOrBoth, Itertools}; -use syntax::{ - algo::SyntaxRewriter, - ast::{ - self, - edit::{AstNodeEdit, IndentLevel}, - make, AstNode, PathSegmentKind, VisibilityOwner, - }, - AstToken, InsertPosition, NodeOrToken, SyntaxElement, SyntaxNode, SyntaxToken, -}; -use test_utils::mark; - -#[derive(Debug, Clone)] -pub enum ImportScope { - File(ast::SourceFile), - Module(ast::ItemList), -} - -impl ImportScope { - pub fn from(syntax: SyntaxNode) -> Option { - if let Some(module) = ast::Module::cast(syntax.clone()) { - module.item_list().map(ImportScope::Module) - } else if let this @ Some(_) = ast::SourceFile::cast(syntax.clone()) { - this.map(ImportScope::File) - } else { - ast::ItemList::cast(syntax).map(ImportScope::Module) - } - } - - /// Determines the containing syntax node in which to insert a `use` statement affecting `position`. - pub fn find_insert_use_container( - position: &SyntaxNode, - sema: &Semantics<'_, RootDatabase>, - ) -> Option { - sema.ancestors_with_macros(position.clone()).find_map(Self::from) - } - - pub fn as_syntax_node(&self) -> &SyntaxNode { - match self { - ImportScope::File(file) => file.syntax(), - ImportScope::Module(item_list) => item_list.syntax(), - } - } - - fn indent_level(&self) -> IndentLevel { - match self { - ImportScope::File(file) => file.indent_level(), - ImportScope::Module(item_list) => item_list.indent_level() + 1, - } - } - - fn first_insert_pos(&self) -> (InsertPosition, AddBlankLine) { - match self { - ImportScope::File(_) => (InsertPosition::First, AddBlankLine::AfterTwice), - // don't insert the imports before the item list's opening curly brace - ImportScope::Module(item_list) => item_list - .l_curly_token() - .map(|b| (InsertPosition::After(b.into()), AddBlankLine::Around)) - .unwrap_or((InsertPosition::First, AddBlankLine::AfterTwice)), - } - } - - fn insert_pos_after_last_inner_element(&self) -> (InsertPosition, AddBlankLine) { - self.as_syntax_node() - .children_with_tokens() - .filter(|child| match child { - NodeOrToken::Node(node) => is_inner_attribute(node.clone()), - NodeOrToken::Token(token) => is_inner_comment(token.clone()), - }) - .last() - .map(|last_inner_element| { - (InsertPosition::After(last_inner_element.into()), AddBlankLine::BeforeTwice) - }) - .unwrap_or_else(|| self.first_insert_pos()) - } -} - -fn is_inner_attribute(node: SyntaxNode) -> bool { - ast::Attr::cast(node).map(|attr| attr.kind()) == Some(ast::AttrKind::Inner) -} - -fn is_inner_comment(token: SyntaxToken) -> bool { - ast::Comment::cast(token).and_then(|comment| comment.kind().doc) - == Some(ast::CommentPlacement::Inner) -} - -/// Insert an import path into the given file/node. A `merge` value of none indicates that no import merging is allowed to occur. -pub fn insert_use<'a>( - scope: &ImportScope, - path: ast::Path, - merge: Option, -) -> SyntaxRewriter<'a> { - let _p = profile::span("insert_use"); - let mut rewriter = SyntaxRewriter::default(); - let use_item = make::use_(make::use_tree(path.clone(), None, None, false)); - // merge into existing imports if possible - if let Some(mb) = merge { - for existing_use in scope.as_syntax_node().children().filter_map(ast::Use::cast) { - if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) { - rewriter.replace(existing_use.syntax(), merged.syntax()); - return rewriter; - } - } - } - - // 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(scope, path); - - let indent = if let ident_level @ 1..=usize::MAX = scope.indent_level().0 as usize { - Some(make::tokens::whitespace(&" ".repeat(4 * ident_level)).into()) - } else { - None - }; - - let to_insert: Vec = { - let mut buf = Vec::new(); - - match add_blank { - AddBlankLine::Before | AddBlankLine::Around => { - buf.push(make::tokens::single_newline().into()) - } - AddBlankLine::BeforeTwice => buf.push(make::tokens::blank_line().into()), - _ => (), - } - - if add_blank.has_before() { - if let Some(indent) = indent.clone() { - mark::hit!(insert_use_indent_before); - buf.push(indent); - } - } - - buf.push(use_item.syntax().clone().into()); - - match add_blank { - AddBlankLine::After | AddBlankLine::Around => { - buf.push(make::tokens::single_newline().into()) - } - AddBlankLine::AfterTwice => buf.push(make::tokens::blank_line().into()), - _ => (), - } - - // only add indentation *after* our stuff if there's another node directly after it - if add_blank.has_after() && matches!(insert_position, InsertPosition::Before(_)) { - if let Some(indent) = indent { - mark::hit!(insert_use_indent_after); - buf.push(indent); - } - } else if add_blank.has_after() && matches!(insert_position, InsertPosition::After(_)) { - mark::hit!(insert_use_no_indent_after); - } - - buf - }; - - match insert_position { - InsertPosition::First => { - rewriter.insert_many_as_first_children(scope.as_syntax_node(), to_insert) - } - InsertPosition::Last => return rewriter, // actually unreachable - InsertPosition::Before(anchor) => rewriter.insert_many_before(&anchor, to_insert), - InsertPosition::After(anchor) => rewriter.insert_many_after(&anchor, to_insert), - } - rewriter -} - -fn eq_visibility(vis0: Option, vis1: Option) -> bool { - match (vis0, vis1) { - (None, None) => true, - // FIXME: Don't use the string representation to check for equality - // spaces inside of the node would break this comparison - (Some(vis0), Some(vis1)) => vis0.to_string() == vis1.to_string(), - _ => false, - } -} - -pub fn try_merge_imports( - lhs: &ast::Use, - rhs: &ast::Use, - merge_behaviour: MergeBehaviour, -) -> Option { - // don't merge imports with different visibilities - if !eq_visibility(lhs.visibility(), rhs.visibility()) { - return None; - } - let lhs_tree = lhs.use_tree()?; - let rhs_tree = rhs.use_tree()?; - let merged = try_merge_trees(&lhs_tree, &rhs_tree, merge_behaviour)?; - Some(lhs.with_use_tree(merged)) -} - -pub fn try_merge_trees( - lhs: &ast::UseTree, - rhs: &ast::UseTree, - merge: MergeBehaviour, -) -> Option { - let lhs_path = lhs.path()?; - let rhs_path = rhs.path()?; - - let (lhs_prefix, rhs_prefix) = common_prefix(&lhs_path, &rhs_path)?; - let (lhs, rhs) = if is_simple_path(lhs) - && is_simple_path(rhs) - && lhs_path == lhs_prefix - && rhs_path == rhs_prefix - { - (lhs.clone(), rhs.clone()) - } else { - (lhs.split_prefix(&lhs_prefix), rhs.split_prefix(&rhs_prefix)) - }; - recursive_merge(&lhs, &rhs, merge) -} - -/// Recursively "zips" together lhs and rhs. -fn recursive_merge( - lhs: &ast::UseTree, - rhs: &ast::UseTree, - merge: MergeBehaviour, -) -> Option { - let mut use_trees = lhs - .use_tree_list() - .into_iter() - .flat_map(|list| list.use_trees()) - // we use Option here to early return from this function(this is not the same as a `filter` op) - .map(|tree| match merge.is_tree_allowed(&tree) { - true => Some(tree), - false => None, - }) - .collect::>>()?; - use_trees.sort_unstable_by(|a, b| path_cmp_for_sort(a.path(), b.path())); - for rhs_t in rhs.use_tree_list().into_iter().flat_map(|list| list.use_trees()) { - if !merge.is_tree_allowed(&rhs_t) { - return None; - } - let rhs_path = rhs_t.path(); - match use_trees.binary_search_by(|lhs_t| { - let (lhs_t, rhs_t) = match lhs_t - .path() - .zip(rhs_path.clone()) - .and_then(|(lhs, rhs)| common_prefix(&lhs, &rhs)) - { - Some((lhs_p, rhs_p)) => (lhs_t.split_prefix(&lhs_p), rhs_t.split_prefix(&rhs_p)), - None => (lhs_t.clone(), rhs_t.clone()), - }; - - path_cmp_bin_search(lhs_t.path(), rhs_t.path()) - }) { - Ok(idx) => { - let lhs_t = &mut use_trees[idx]; - let lhs_path = lhs_t.path()?; - let rhs_path = rhs_path?; - let (lhs_prefix, rhs_prefix) = common_prefix(&lhs_path, &rhs_path)?; - if lhs_prefix == lhs_path && rhs_prefix == rhs_path { - let tree_is_self = |tree: ast::UseTree| { - tree.path().as_ref().map(path_is_self).unwrap_or(false) - }; - // check if only one of the two trees has a tree list, and whether that then contains `self` or not. - // If this is the case we can skip this iteration since the path without the list is already included in the other one via `self` - let tree_contains_self = |tree: &ast::UseTree| { - tree.use_tree_list() - .map(|tree_list| tree_list.use_trees().any(tree_is_self)) - .unwrap_or(false) - }; - match (tree_contains_self(&lhs_t), tree_contains_self(&rhs_t)) { - (true, false) => continue, - (false, true) => { - *lhs_t = rhs_t; - continue; - } - _ => (), - } - - // glob imports arent part of the use-tree lists so we need to special handle them here as well - // this special handling is only required for when we merge a module import into a glob import of said module - // see the `merge_self_glob` or `merge_mod_into_glob` tests - if lhs_t.star_token().is_some() || rhs_t.star_token().is_some() { - *lhs_t = make::use_tree( - make::path_unqualified(make::path_segment_self()), - None, - None, - false, - ); - use_trees.insert(idx, make::glob_use_tree()); - continue; - } - - if lhs_t.use_tree_list().is_none() && rhs_t.use_tree_list().is_none() { - continue; - } - } - let lhs = lhs_t.split_prefix(&lhs_prefix); - let rhs = rhs_t.split_prefix(&rhs_prefix); - match recursive_merge(&lhs, &rhs, merge) { - Some(use_tree) => use_trees[idx] = use_tree, - None => return None, - } - } - Err(_) - if merge == MergeBehaviour::Last - && use_trees.len() > 0 - && rhs_t.use_tree_list().is_some() => - { - return None - } - Err(idx) => { - use_trees.insert(idx, rhs_t); - } - } - } - Some(lhs.with_use_tree_list(make::use_tree_list(use_trees))) -} - -/// 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 { - match (lhs_curr.segment(), rhs_curr.segment()) { - (Some(lhs), Some(rhs)) if lhs.syntax().text() == rhs.syntax().text() => (), - _ => break res, - } - 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; - } - _ => break res, - } - } -} - -fn is_simple_path(use_tree: &ast::UseTree) -> bool { - use_tree.use_tree_list().is_none() && use_tree.star_token().is_none() -} - -fn path_is_self(path: &ast::Path) -> bool { - path.segment().and_then(|seg| seg.self_token()).is_some() && path.qualifier().is_none() -} - -#[inline] -fn first_segment(path: &ast::Path) -> Option { - first_path(path).segment() -} - -fn first_path(path: &ast::Path) -> ast::Path { - successors(Some(path.clone()), ast::Path::qualifier).last().unwrap() -} - -fn segment_iter(path: &ast::Path) -> impl Iterator + Clone { - // cant make use of SyntaxNode::siblings, because the returned Iterator is not clone - successors(first_segment(path), |p| p.parent_path().parent_path().and_then(|p| p.segment())) -} - -fn path_len(path: ast::Path) -> usize { - segment_iter(&path).count() -} - -/// Orders paths in the following way: -/// the sole self token comes first, after that come uppercase identifiers, then lowercase identifiers -// FIXME: rustfmt sorts lowercase idents before uppercase, in general we want to have the same ordering rustfmt has -// which is `self` and `super` first, then identifier imports with lowercase ones first, then glob imports and at last list imports. -// Example foo::{self, foo, baz, Baz, Qux, *, {Bar}} -fn path_cmp_for_sort(a: Option, b: Option) -> Ordering { - match (a, b) { - (None, None) => Ordering::Equal, - (None, Some(_)) => Ordering::Less, - (Some(_), None) => Ordering::Greater, - (Some(ref a), Some(ref b)) => match (path_is_self(a), path_is_self(b)) { - (true, true) => Ordering::Equal, - (true, false) => Ordering::Less, - (false, true) => Ordering::Greater, - (false, false) => path_cmp_short(a, b), - }, - } -} - -/// Path comparison func for binary searching for merging. -fn path_cmp_bin_search(lhs: Option, rhs: Option) -> Ordering { - match (lhs.and_then(|path| path.segment()), rhs.and_then(|path| path.segment())) { - (None, None) => Ordering::Equal, - (None, Some(_)) => Ordering::Less, - (Some(_), None) => Ordering::Greater, - (Some(ref a), Some(ref b)) => path_segment_cmp(a, b), - } -} - -/// Short circuiting comparison, if both paths are equal until one of them ends they are considered -/// equal -fn path_cmp_short(a: &ast::Path, b: &ast::Path) -> Ordering { - let a = segment_iter(a); - let b = segment_iter(b); - // cmp_by would be useful for us here but that is currently unstable - // cmp doesnt work due the lifetimes on text's return type - a.zip(b) - .find_map(|(a, b)| match path_segment_cmp(&a, &b) { - Ordering::Equal => None, - ord => Some(ord), - }) - .unwrap_or(Ordering::Equal) -} - -/// Compares to paths, if one ends earlier than the other the has_tl parameters decide which is -/// greater as a a path that has a tree list should be greater, while one that just ends without -/// a tree list should be considered less. -fn use_tree_path_cmp(a: &ast::Path, a_has_tl: bool, b: &ast::Path, b_has_tl: bool) -> Ordering { - let a_segments = segment_iter(a); - let b_segments = segment_iter(b); - // cmp_by would be useful for us here but that is currently unstable - // cmp doesnt work due the lifetimes on text's return type - a_segments - .zip_longest(b_segments) - .find_map(|zipped| match zipped { - EitherOrBoth::Both(ref a, ref b) => match path_segment_cmp(a, b) { - Ordering::Equal => None, - ord => Some(ord), - }, - EitherOrBoth::Left(_) if !b_has_tl => Some(Ordering::Greater), - EitherOrBoth::Left(_) => Some(Ordering::Less), - EitherOrBoth::Right(_) if !a_has_tl => Some(Ordering::Less), - EitherOrBoth::Right(_) => Some(Ordering::Greater), - }) - .unwrap_or(Ordering::Equal) -} - -fn path_segment_cmp(a: &ast::PathSegment, b: &ast::PathSegment) -> Ordering { - let a = a.name_ref(); - let b = b.name_ref(); - a.as_ref().map(ast::NameRef::text).cmp(&b.as_ref().map(ast::NameRef::text)) -} - -/// What type of merges are allowed. -#[derive(Copy, Clone, Debug, 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, -} - -impl MergeBehaviour { - #[inline] - fn is_tree_allowed(&self, tree: &ast::UseTree) -> bool { - match self { - MergeBehaviour::Full => true, - // only simple single segment paths are allowed - MergeBehaviour::Last => { - tree.use_tree_list().is_none() && tree.path().map(path_len) <= Some(1) - } - } - } -} - -#[derive(Eq, PartialEq, PartialOrd, Ord)] -enum ImportGroup { - // the order here defines the order of new group inserts - Std, - ExternCrate, - ThisCrate, - ThisModule, - SuperModule, -} - -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, - _ => ImportGroup::ExternCrate, - }, - PathSegmentKind::Type { .. } => unreachable!(), - } - } -} - -#[derive(PartialEq, Eq)] -enum AddBlankLine { - Before, - BeforeTwice, - Around, - After, - AfterTwice, -} - -impl AddBlankLine { - fn has_before(&self) -> bool { - matches!(self, AddBlankLine::Before | AddBlankLine::BeforeTwice | AddBlankLine::Around) - } - fn has_after(&self) -> bool { - matches!(self, AddBlankLine::After | AddBlankLine::AfterTwice | AddBlankLine::Around) - } -} - -fn find_insert_position( - scope: &ImportScope, - insert_path: ast::Path, -) -> (InsertPosition, AddBlankLine) { - let group = ImportGroup::new(&insert_path); - let path_node_iter = scope - .as_syntax_node() - .children() - .filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node))) - .flat_map(|(use_, node)| { - let tree = use_.use_tree()?; - let path = tree.path()?; - let has_tl = tree.use_tree_list().is_some(); - Some((path, has_tl, 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); - - // 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( - |&(ref path, has_tl, _)| { - use_tree_path_cmp(&insert_path, false, path, has_tl) != Ordering::Greater - }, - ); - 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::BeforeTwice) - } - // there are no imports in this file at all - None => scope.insert_pos_after_last_inner_element(), - }, - } - } - }, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use test_utils::assert_eq_text; - - #[test] - fn insert_existing() { - check_full("std::fs", "use std::fs;", "use std::fs;") - } - - #[test] - fn insert_start() { - check_none( - "std::bar::AA", - r" -use std::bar::B; -use std::bar::D; -use std::bar::F; -use std::bar::G;", - r" -use std::bar::AA; -use std::bar::B; -use std::bar::D; -use std::bar::F; -use std::bar::G;", - ) - } - - #[test] - fn insert_start_indent() { - mark::check!(insert_use_indent_after); - check_none( - "std::bar::AA", - r" - use std::bar::B; - use std::bar::D;", - r" - use std::bar::AA; - use std::bar::B; - use std::bar::D;", - ) - } - - #[test] - fn insert_middle() { - check_none( - "std::bar::EE", - 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::EE; -use std::bar::F; -use std::bar::G;", - ) - } - - #[test] - fn insert_middle_indent() { - check_none( - "std::bar::EE", - 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::EE; - use std::bar::F; - use std::bar::G;", - ) - } - - #[test] - fn insert_end() { - check_none( - "std::bar::ZZ", - 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::ZZ;", - ) - } - - #[test] - fn insert_end_indent() { - mark::check!(insert_use_indent_before); - check_none( - "std::bar::ZZ", - 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::ZZ;", - ) - } - - #[test] - fn insert_middle_nested() { - check_none( - "std::bar::EE", - 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::EE; -use std::bar::{D, Z}; // example of weird imports due to user -use std::bar::F; -use std::bar::G;", - ) - } - - #[test] - fn insert_middle_groups() { - check_none( - "foo::bar::GG", - 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::GG; - use foo::bar::H;", - ) - } - - #[test] - fn insert_first_matching_group() { - check_none( - "foo::bar::GG", - 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::GG; - - use std; - - use foo::bar::F; - use foo::bar::H;", - ) - } - - #[test] - fn insert_missing_group_std() { - 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;", - ) - } - - #[test] - fn insert_missing_group_self() { - check_none( - "self::fmt", - r" -use foo::bar::A; -use foo::bar::D;", - r" -use foo::bar::A; -use foo::bar::D; - -use self::fmt;", - ) - } - - #[test] - fn insert_no_imports() { - check_full( - "foo::bar", - "fn main() {}", - r"use foo::bar; - -fn main() {}", - ) - } - - #[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; - -", - ) - } - - #[test] - fn insert_empty_module() { - mark::check!(insert_use_no_indent_after); - check( - "foo::bar", - "mod x {}", - r"{ - use foo::bar; -}", - None, - true, - ) - } - - #[test] - fn insert_after_inner_attr() { - check_full( - "foo::bar", - r"#![allow(unused_imports)]", - r"#![allow(unused_imports)] - -use foo::bar;", - ) - } - - #[test] - fn insert_after_inner_attr2() { - check_full( - "foo::bar", - r"#![allow(unused_imports)] - -#![no_std] -fn main() {}", - r"#![allow(unused_imports)] - -#![no_std] - -use foo::bar; -fn main() {}", - ); - } - - #[test] - fn inserts_after_single_line_inner_comments() { - check_none( - "foo::bar::Baz", - "//! Single line inner comments do not allow any code before them.", - r#"//! Single line inner comments do not allow any code before them. - -use foo::bar::Baz;"#, - ); - } - - #[test] - fn inserts_after_multiline_inner_comments() { - check_none( - "foo::bar::Baz", - r#"/*! Multiline inner comments do not allow any code before them. */ - -/*! Still an inner comment, cannot place any code before. */ -fn main() {}"#, - r#"/*! Multiline inner comments do not allow any code before them. */ - -/*! Still an inner comment, cannot place any code before. */ - -use foo::bar::Baz; -fn main() {}"#, - ) - } - - #[test] - fn inserts_after_all_inner_items() { - check_none( - "foo::bar::Baz", - r#"#![allow(unused_imports)] -/*! Multiline line comment 2 */ - - -//! Single line comment 1 -#![no_std] -//! Single line comment 2 -fn main() {}"#, - r#"#![allow(unused_imports)] -/*! Multiline line comment 2 */ - - -//! Single line comment 1 -#![no_std] -//! Single line comment 2 - -use foo::bar::Baz; -fn main() {}"#, - ) - } - - #[test] - fn merge_groups() { - check_last("std::io", r"use std::fmt;", r"use std::{fmt, io};") - } - - #[test] - fn merge_groups_last() { - check_last( - "std::io", - r"use std::fmt::{Result, Display};", - r"use std::fmt::{Result, Display}; -use std::io;", - ) - } - - #[test] - fn merge_last_into_self() { - check_last("foo::bar::baz", r"use foo::bar;", r"use foo::bar::{self, baz};"); - } - - #[test] - fn merge_groups_full() { - check_full( - "std::io", - r"use std::fmt::{Result, Display};", - r"use std::{fmt::{Result, Display}, io};", - ) - } - - #[test] - fn merge_groups_long_full() { - check_full( - "std::foo::bar::Baz", - r"use std::foo::bar::Qux;", - r"use std::foo::bar::{Baz, Qux};", - ) - } - - #[test] - fn merge_groups_long_last() { - check_last( - "std::foo::bar::Baz", - r"use std::foo::bar::Qux;", - r"use std::foo::bar::{Baz, Qux};", - ) - } - - #[test] - fn merge_groups_long_full_list() { - check_full( - "std::foo::bar::Baz", - r"use std::foo::bar::{Qux, Quux};", - r"use std::foo::bar::{Baz, Quux, Qux};", - ) - } - - #[test] - fn merge_groups_long_last_list() { - check_last( - "std::foo::bar::Baz", - r"use std::foo::bar::{Qux, Quux};", - r"use std::foo::bar::{Baz, Quux, Qux};", - ) - } - - #[test] - fn merge_groups_long_full_nested() { - check_full( - "std::foo::bar::Baz", - r"use std::foo::bar::{Qux, quux::{Fez, Fizz}};", - r"use std::foo::bar::{Baz, Qux, quux::{Fez, Fizz}};", - ) - } - - #[test] - fn merge_groups_long_last_nested() { - check_last( - "std::foo::bar::Baz", - r"use std::foo::bar::{Qux, quux::{Fez, Fizz}};", - r"use std::foo::bar::Baz; -use std::foo::bar::{Qux, quux::{Fez, Fizz}};", - ) - } - - #[test] - fn merge_groups_full_nested_deep() { - check_full( - "std::foo::bar::quux::Baz", - r"use std::foo::bar::{Qux, quux::{Fez, Fizz}};", - r"use std::foo::bar::{Qux, quux::{Baz, Fez, Fizz}};", - ) - } - - #[test] - fn merge_groups_full_nested_long() { - check_full( - "std::foo::bar::Baz", - r"use std::{foo::bar::Qux};", - r"use std::{foo::bar::{Baz, Qux}};", - ); - } - - #[test] - fn merge_groups_last_nested_long() { - check_full( - "std::foo::bar::Baz", - r"use std::{foo::bar::Qux};", - r"use std::{foo::bar::{Baz, Qux}};", - ); - } - - #[test] - fn merge_groups_skip_pub() { - check_full( - "std::io", - r"pub use std::fmt::{Result, Display};", - r"pub use std::fmt::{Result, Display}; -use std::io;", - ) - } - - #[test] - fn merge_groups_skip_pub_crate() { - check_full( - "std::io", - r"pub(crate) use std::fmt::{Result, Display};", - r"pub(crate) use std::fmt::{Result, Display}; -use std::io;", - ) - } - - #[test] - #[ignore] // FIXME: Support this - fn split_out_merge() { - check_last( - "std::fmt::Result", - r"use std::{fmt, io};", - r"use std::fmt::{self, Result}; -use std::io;", - ) - } - - #[test] - fn merge_into_module_import() { - check_full( - "std::fmt::Result", - r"use std::{fmt, io};", - r"use std::{fmt::{self, Result}, io};", - ) - } - - #[test] - fn merge_groups_self() { - check_full("std::fmt::Debug", r"use std::fmt;", r"use std::fmt::{self, Debug};") - } - - #[test] - fn merge_mod_into_glob() { - check_full( - "token::TokenKind", - r"use token::TokenKind::*;", - r"use token::TokenKind::{*, self};", - ) - // FIXME: have it emit `use token::TokenKind::{self, *}`? - } - - #[test] - fn merge_self_glob() { - check_full("self", r"use self::*;", r"use self::{*, self};") - // FIXME: have it emit `use {self, *}`? - } - - #[test] - fn merge_glob_nested() { - check_full( - "foo::bar::quux::Fez", - r"use foo::bar::{Baz, quux::*};", - r"use foo::bar::{Baz, quux::{self::*, Fez}};", - ) - } - - #[test] - fn skip_merge_last_too_long() { - check_last( - "foo::bar", - r"use foo::bar::baz::Qux;", - r"use foo::bar; -use foo::bar::baz::Qux;", - ); - } - - #[test] - fn skip_merge_last_too_long2() { - check_last( - "foo::bar::baz::Qux", - r"use foo::bar;", - r"use foo::bar; -use foo::bar::baz::Qux;", - ); - } - - #[test] - fn insert_short_before_long() { - check_none( - "foo::bar", - r"use foo::bar::baz::Qux;", - r"use foo::bar; -use foo::bar::baz::Qux;", - ); - } - - #[test] - fn merge_last_fail() { - check_merge_only_fail( - r"use foo::bar::{baz::{Qux, Fez}};", - r"use foo::bar::{baaz::{Quux, Feez}};", - MergeBehaviour::Last, - ); - } - - #[test] - fn merge_last_fail1() { - check_merge_only_fail( - r"use foo::bar::{baz::{Qux, Fez}};", - r"use foo::bar::baaz::{Quux, Feez};", - MergeBehaviour::Last, - ); - } - - #[test] - fn merge_last_fail2() { - check_merge_only_fail( - r"use foo::bar::baz::{Qux, Fez};", - r"use foo::bar::{baaz::{Quux, Feez}};", - MergeBehaviour::Last, - ); - } - - #[test] - fn merge_last_fail3() { - check_merge_only_fail( - r"use foo::bar::baz::{Qux, Fez};", - r"use foo::bar::baaz::{Quux, Feez};", - MergeBehaviour::Last, - ); - } - - fn check( - path: &str, - ra_fixture_before: &str, - ra_fixture_after: &str, - mb: Option, - module: bool, - ) { - let mut syntax = ast::SourceFile::parse(ra_fixture_before).tree().syntax().clone(); - if module { - syntax = syntax.descendants().find_map(ast::Module::cast).unwrap().syntax().clone(); - } - let file = super::ImportScope::from(syntax).unwrap(); - let path = ast::SourceFile::parse(&format!("use {};", path)) - .tree() - .syntax() - .descendants() - .find_map(ast::Path::cast) - .unwrap(); - - let rewriter = insert_use(&file, path, mb); - let result = rewriter.rewrite(file.as_syntax_node()).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), false) - } - - fn check_last(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { - check(path, ra_fixture_before, ra_fixture_after, Some(MergeBehaviour::Last), false) - } - - fn check_none(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { - check(path, ra_fixture_before, ra_fixture_after, None, false) - } - - fn check_merge_only_fail(ra_fixture0: &str, ra_fixture1: &str, mb: MergeBehaviour) { - let use0 = ast::SourceFile::parse(ra_fixture0) - .tree() - .syntax() - .descendants() - .find_map(ast::Use::cast) - .unwrap(); - - let use1 = ast::SourceFile::parse(ra_fixture1) - .tree() - .syntax() - .descendants() - .find_map(ast::Use::cast) - .unwrap(); - - let result = try_merge_imports(&use0, &use1, mb); - assert_eq!(result.map(|u| u.to_string()), None); - } -} diff --git a/crates/ide_helpers/src/lib.rs b/crates/ide_helpers/src/lib.rs deleted file mode 100644 index ad4404b19..000000000 --- a/crates/ide_helpers/src/lib.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! A module with ide helpers for high-level ide features. -use hir::{Crate, Enum, Module, ScopeDef, Semantics, Trait}; -use ide_db::RootDatabase; -use syntax::ast::{self, make}; - -pub mod insert_use; - -/// Converts the mod path struct into its ast representation. -pub fn mod_path_to_ast(path: &hir::ModPath) -> ast::Path { - let _p = profile::span("mod_path_to_ast"); - - let mut segments = Vec::new(); - let mut is_abs = false; - match path.kind { - hir::PathKind::Plain => {} - hir::PathKind::Super(0) => segments.push(make::path_segment_self()), - hir::PathKind::Super(n) => segments.extend((0..n).map(|_| make::path_segment_super())), - hir::PathKind::DollarCrate(_) | hir::PathKind::Crate => { - segments.push(make::path_segment_crate()) - } - hir::PathKind::Abs => is_abs = true, - } - - segments.extend( - path.segments - .iter() - .map(|segment| make::path_segment(make::name_ref(&segment.to_string()))), - ); - make::path_from_segments(segments, is_abs) -} - -/// Helps with finding well-know things inside the standard library. This is -/// somewhat similar to the known paths infra inside hir, but it different; We -/// want to make sure that IDE specific paths don't become interesting inside -/// the compiler itself as well. -pub struct FamousDefs<'a, 'b>(pub &'a Semantics<'b, RootDatabase>, pub Option); - -#[allow(non_snake_case)] -impl FamousDefs<'_, '_> { - pub const FIXTURE: &'static str = r#"//- /libcore.rs crate:core -pub mod convert { - pub trait From { - fn from(t: T) -> Self; - } -} - -pub mod default { - pub trait Default { - fn default() -> Self; - } -} - -pub mod iter { - pub use self::traits::{collect::IntoIterator, iterator::Iterator}; - mod traits { - pub(crate) mod iterator { - use crate::option::Option; - pub trait Iterator { - type Item; - fn next(&mut self) -> Option; - fn by_ref(&mut self) -> &mut Self { - self - } - fn take(self, n: usize) -> crate::iter::Take { - crate::iter::Take { inner: self } - } - } - - impl Iterator for &mut I { - type Item = I::Item; - fn next(&mut self) -> Option { - (**self).next() - } - } - } - pub(crate) mod collect { - pub trait IntoIterator { - type Item; - } - } - } - - pub use self::sources::*; - pub(crate) mod sources { - use super::Iterator; - use crate::option::Option::{self, *}; - pub struct Repeat { - element: A, - } - - pub fn repeat(elt: T) -> Repeat { - Repeat { element: elt } - } - - impl Iterator for Repeat { - type Item = A; - - fn next(&mut self) -> Option { - None - } - } - } - - pub use self::adapters::*; - pub(crate) mod adapters { - use super::Iterator; - use crate::option::Option::{self, *}; - pub struct Take { pub(crate) inner: I } - impl Iterator for Take where I: Iterator { - type Item = ::Item; - fn next(&mut self) -> Option<::Item> { - None - } - } - } -} - -pub mod option { - pub enum Option { None, Some(T)} -} - -pub mod prelude { - pub use crate::{convert::From, iter::{IntoIterator, Iterator}, option::Option::{self, *}, default::Default}; -} -#[prelude_import] -pub use prelude::*; -"#; - - pub fn core(&self) -> Option { - self.find_crate("core") - } - - pub fn core_convert_From(&self) -> Option { - self.find_trait("core:convert:From") - } - - pub fn core_option_Option(&self) -> Option { - self.find_enum("core:option:Option") - } - - pub fn core_default_Default(&self) -> Option { - self.find_trait("core:default:Default") - } - - pub fn core_iter_Iterator(&self) -> Option { - self.find_trait("core:iter:traits:iterator:Iterator") - } - - pub fn core_iter(&self) -> Option { - self.find_module("core:iter") - } - - fn find_trait(&self, path: &str) -> Option { - match self.find_def(path)? { - hir::ScopeDef::ModuleDef(hir::ModuleDef::Trait(it)) => Some(it), - _ => None, - } - } - - fn find_enum(&self, path: &str) -> Option { - match self.find_def(path)? { - hir::ScopeDef::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Enum(it))) => Some(it), - _ => None, - } - } - - fn find_module(&self, path: &str) -> Option { - match self.find_def(path)? { - hir::ScopeDef::ModuleDef(hir::ModuleDef::Module(it)) => Some(it), - _ => None, - } - } - - fn find_crate(&self, name: &str) -> Option { - let krate = self.1?; - let db = self.0.db; - let res = - krate.dependencies(db).into_iter().find(|dep| dep.name.to_string() == name)?.krate; - Some(res) - } - - fn find_def(&self, path: &str) -> Option { - let db = self.0.db; - let mut path = path.split(':'); - let trait_ = path.next_back()?; - let std_crate = path.next()?; - let std_crate = self.find_crate(std_crate)?; - let mut module = std_crate.root_module(db); - for segment in path { - module = module.children(db).find_map(|child| { - let name = child.name(db)?; - if name.to_string() == segment { - Some(child) - } else { - None - } - })?; - } - let def = - module.scope(db, None).into_iter().find(|(name, _def)| name.to_string() == trait_)?.1; - Some(def) - } -} diff --git a/crates/rust-analyzer/Cargo.toml b/crates/rust-analyzer/Cargo.toml index 3afcd2eda..08559b53a 100644 --- a/crates/rust-analyzer/Cargo.toml +++ b/crates/rust-analyzer/Cargo.toml @@ -39,7 +39,7 @@ tracing-tree = { version = "0.1.4" } stdx = { path = "../stdx", version = "0.0.0" } flycheck = { path = "../flycheck", version = "0.0.0" } ide = { path = "../ide", version = "0.0.0" } -ide_helpers = { path = "../ide_helpers", version = "0.0.0" } +ide_db = { path = "../ide_db", version = "0.0.0" } profile = { path = "../profile", version = "0.0.0" } project_model = { path = "../project_model", version = "0.0.0" } syntax = { path = "../syntax", version = "0.0.0" } @@ -50,7 +50,6 @@ cfg = { path = "../cfg", version = "0.0.0" } toolchain = { path = "../toolchain", version = "0.0.0" } # This should only be used in CLI -ide_db = { path = "../ide_db", version = "0.0.0" } ssr = { path = "../ssr", version = "0.0.0" } hir = { path = "../hir", version = "0.0.0" } hir_def = { path = "../hir_def", version = "0.0.0" } diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index d7b711f94..30299a465 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -12,7 +12,7 @@ use std::{convert::TryFrom, ffi::OsString, path::PathBuf}; use flycheck::FlycheckConfig; use hir::PrefixKind; use ide::{AssistConfig, CompletionConfig, DiagnosticsConfig, HoverConfig, InlayHintsConfig}; -use ide_helpers::insert_use::MergeBehaviour; +use ide_db::helpers::insert_use::MergeBehaviour; use lsp_types::{ClientCapabilities, MarkupKind}; use project_model::{CargoConfig, ProjectJson, ProjectJsonData, ProjectManifest}; use rustc_hash::FxHashSet; -- cgit v1.2.3