diff --git a/crates/ide-assists/src/assist_context.rs b/crates/ide-assists/src/assist_context.rs index afb2229d3e2f..3fec383d9aef 100644 --- a/crates/ide-assists/src/assist_context.rs +++ b/crates/ide-assists/src/assist_context.rs @@ -1,8 +1,11 @@ //! See [`AssistContext`]. +use std::sync::{Arc, Mutex}; + use hir::{FileRange, Semantics}; use ide_db::EditionedFileId; use ide_db::base_db::salsa::AsDynDatabase; +use ide_db::source_change::{ChoiceAction, ChoiceCallBack, IndependentChoiceCallback, SequentialChoiceCallback, SourceChange}; use ide_db::{FileId, RootDatabase, label::Label}; use syntax::Edition; use syntax::{ @@ -203,6 +206,78 @@ impl Assists { self.add_impl(Some(group), id, label.into(), target, &mut |it| f.take().unwrap()(it)) } + /// For scenarios where the user needs to select options from multiple predefined lists + /// + /// `choices_list` is a list of choice lists, where each choice is a string + /// + /// `f` is a function that takes a `SourceChangeBuilder` and a slice of strings + /// which is the choice being made, each one from corrseponding choice list in `choices_list` + /// or if no choice is provided, `None` is passed + pub(crate) fn add_independent_choices_assist( + &mut self, + group: Option<&GroupLabel>, + id: AssistId, + label: String, + target: TextRange, + choices_list: Vec>, + f: IndependentChoiceCallback, + ) -> Option<()> { + if !self.is_allowed(&id) { + return None; + } + let file_id = self.file; + + let callback = ChoiceCallBack::Independent { choices_list, f }; + let action = ChoiceAction::new(file_id, Arc::new(Mutex::new(callback))); + + let source_change = Some(SourceChange::from_choice_action(file_id, action)); + + let label = Label::new(label); + let group = group.cloned(); + self.buf.push(Assist { id, label, group, target, source_change, command: None }); + Some(()) + } + + /// For scenarios requiring sequential, stateful interactions where choices influence subsequent steps. + /// + /// `first_choice_list` is a list of choices, where each choice is a string + /// + /// `f` is a function that takes a `SourceChangeBuilder` and a usize indicate which question this is, and a string + /// which is the choice being made, return next choice list + /// + /// for example, the first time `f` is called, it is called with `0` and a string in `first_choice_list` and returns + /// the next choice list, the second time `f` is called, it is called with `1` and a string in the second choice list + /// and returns the next choice list, and so on + /// + /// in conclusion, `f` is like rewrite a proper async function into a state machine with each await point being a choice + /// being made by user + /// + /// TODO(discord9): maybe consider async closure for `f` + pub(crate) fn add_sequential_choice_assist( + &mut self, + group: Option<&GroupLabel>, + id: AssistId, + label: String, + target: TextRange, + first_choice_list: Vec, + f: SequentialChoiceCallback, + ) -> Option<()> { + if !self.is_allowed(&id) { + return None; + } + let file_id = self.file; + + let callback = ChoiceCallBack::Sequential { first_choice_list, f }; + let action = ChoiceAction::new(file_id, Arc::new(Mutex::new(callback))); + + let source_change = Some(SourceChange::from_choice_action(file_id, action)); + + let label = Label::new(label); + let group = group.cloned(); + self.buf.push(Assist { id, label, group, target, source_change, command: None }); + Some(()) + } + fn add_impl( &mut self, group: Option<&GroupLabel>, diff --git a/crates/ide-assists/src/handlers/auto_import.rs b/crates/ide-assists/src/handlers/auto_import.rs index 2c7532d919ad..904177eede4e 100644 --- a/crates/ide-assists/src/handlers/auto_import.rs +++ b/crates/ide-assists/src/handlers/auto_import.rs @@ -2,13 +2,16 @@ use std::cmp::Reverse; use hir::{Module, db::HirDatabase}; use ide_db::{ + FxHashSet, helpers::mod_path_to_ast, imports::{ import_assets::{ImportAssets, ImportCandidate, LocatedImport}, - insert_use::{ImportScope, insert_use, insert_use_as_alias}, + insert_use::{ + ImportScope, insert_multiple_use_with_alias_option, insert_use, insert_use_as_alias, + }, }, }; -use syntax::{AstNode, Edition, NodeOrToken, SyntaxElement, ast}; +use syntax::{AstNode, Edition, NodeOrToken, SyntaxElement, TextRange, WalkEvent, ast}; use crate::{AssistContext, AssistId, Assists, GroupLabel}; @@ -201,6 +204,170 @@ pub(super) fn find_importable_node( } } +/// Represents an import action to be performed +#[derive(Clone)] +struct ImportAction { + import: LocatedImport, + scope: ImportScope, + range: TextRange, + edition: Edition, +} + +/// Feature: Auto Import All +/// auto import all missing imports in the current file. Share the same configuration as `auto_import`. +/// and basically is just calling `auto_import` multiple times. But if multiple proposed imports are provided for one importable node, +/// then this assist will automatically choose the most relevant one(as api forbid us from proposing multiple choices **multiple times** to user), also for trait import it default to import as alias `_` +/// +/// Another limitation is that if a `Foo::bar()` where `bar()` is a trait method and both struct `Foo` and trait `Bar` are not in scope, two pass of `auto_import_all` will be required, as in the first pass, there is no way to know whether `bar()` is a trait method or not. +pub(crate) fn auto_import_all(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { + let cfg = ctx.config.import_path_config(); + + let importable_nodes = find_all_importable_nodes(ctx); + // dedup all proposed_imports first, so we wouldn't propose the same import multiple times + let mut all_proposed_import_paths = FxHashSet::default(); + let mut all_insert_use_actions = Vec::with_capacity(importable_nodes.len()); + for (import_assets, syntax_under_caret) in &importable_nodes { + let mut proposed_imports: Vec<_> = import_assets + .search_for_imports(&ctx.sema, cfg, ctx.config.insert_use.prefix_kind) + .collect(); + // remove duplicates + proposed_imports.retain(|import| !all_proposed_import_paths.contains(&import.import_path)); + if proposed_imports.is_empty() { + continue; + } + all_proposed_import_paths + .extend(proposed_imports.iter().map(|import| import.import_path.clone())); + + let range = match &syntax_under_caret { + NodeOrToken::Node(node) => ctx.sema.original_range(node).range, + NodeOrToken::Token(token) => token.text_range(), + }; + let scope = ImportScope::find_insert_use_container( + &match syntax_under_caret { + NodeOrToken::Node(it) => it.clone(), + NodeOrToken::Token(it) => it.parent()?, + }, + &ctx.sema, + )?; + + // we aren't interested in different namespaces + proposed_imports.sort_by(|a, b| a.import_path.cmp(&b.import_path)); + proposed_imports.dedup_by(|a, b| a.import_path == b.import_path); + + let current_module = ctx.sema.scope(scope.as_syntax_node()).map(|scope| scope.module()); + // prioritize more relevant imports + proposed_imports + .sort_by_key(|import| Reverse(relevance_score(ctx, import, current_module.as_ref()))); + let edition = + current_module.map(|it| it.krate().edition(ctx.db())).unwrap_or(Edition::CURRENT); + + let chosen_import = + proposed_imports.first().expect("should have at least one available import"); + + all_insert_use_actions.push(ImportAction { + import: chosen_import.clone(), + scope, + range, + edition, + }); + } + + let group_label = GroupLabel("Import all missing items".to_owned()); + let assist_id = AssistId::quick_fix("auto_import_all"); + let label = "Import all missing items"; + + // add the same import all action in all the places where we need import + for action in &all_insert_use_actions { + acc.add_group(&group_label, assist_id, label, action.range, |builder| { + let path_alias = gen_insert_args( + &action.scope, + importable_nodes.iter().map(|(import_assets, _)| import_assets), + &all_insert_use_actions, + ); + + let scope = match action.scope.clone() { + ImportScope::File(it) => ImportScope::File(builder.make_mut(it)), + ImportScope::Module(it) => ImportScope::Module(builder.make_mut(it)), + ImportScope::Block(it) => ImportScope::Block(builder.make_mut(it)), + }; + + insert_multiple_use_with_alias_option(&scope, &path_alias, &ctx.config.insert_use); + }); + } + Some(()) +} + +/// Find imports in the given scope +fn gen_insert_args<'a>( + given_scope: &ImportScope, + import_assets: impl Iterator, + insert_uses: &[ImportAction], +) -> Vec<(ast::Path, Option)> { + let mut path_alias = Vec::new(); + for (import_assets, action) in import_assets.zip(insert_uses.iter()) { + if action.scope.as_syntax_node() != given_scope.as_syntax_node() { + continue; + } + + match import_assets.import_candidate() { + ImportCandidate::TraitAssocItem(_name) | ImportCandidate::TraitMethod(_name) => { + // get a ast node `Rename` from `use foo as _`(the same code from `insert_use_as_alias`) + let text: &str = "use foo as _"; + let parse = syntax::SourceFile::parse(text, action.edition); + let node = parse + .tree() + .syntax() + .descendants() + .find_map(ast::UseTree::cast) + .expect("Failed to make ast node `Rename`"); + let alias = node.rename(); + let ast_path = mod_path_to_ast(&action.import.import_path, action.edition); + path_alias.push((ast_path, alias)); + } + _ => { + path_alias + .push((mod_path_to_ast(&action.import.import_path, action.edition), None)); + } + } + } + + path_alias +} + +pub(super) fn find_all_importable_nodes( + ctx: &AssistContext<'_>, +) -> Vec<(ImportAssets, SyntaxElement)> { + // walk from root to find all importable nodes + let mut result: Vec<(ImportAssets, SyntaxElement)> = Vec::new(); + + for syntax_node in ctx.source_file().syntax().preorder() { + let WalkEvent::Enter(syntax_node) = syntax_node else { continue }; + let node = if ast::Path::can_cast(syntax_node.kind()) { + let path_under_caret = + ast::Path::cast(syntax_node).expect("Should be able to cast to Path"); + ImportAssets::for_exact_path(&path_under_caret, &ctx.sema) + .zip(Some(path_under_caret.syntax().clone().into())) + } else if ast::MethodCallExpr::can_cast(syntax_node.kind()) { + let method_under_caret = ast::MethodCallExpr::cast(syntax_node) + .expect("Should be able to cast to MethodCallExpr"); + ImportAssets::for_method_call(&method_under_caret, &ctx.sema) + .zip(Some(method_under_caret.syntax().clone().into())) + } else if ast::Param::can_cast(syntax_node.kind()) { + None + } else if let Some(pat) = + ast::IdentPat::cast(syntax_node).filter(ast::IdentPat::is_simple_ident) + { + ImportAssets::for_ident_pat(&ctx.sema, &pat).zip(Some(pat.syntax().clone().into())) + } else { + None + }; + if let Some(node) = node { + result.push(node); + } + } + result +} + fn group_label(import_candidate: &ImportCandidate) -> GroupLabel { let name = match import_candidate { ImportCandidate::Path(candidate) => format!("Import {}", candidate.name.text()), @@ -1722,4 +1889,189 @@ mod foo { ", ); } + + #[test] + fn basic_auto_import_all() { + check_assist( + auto_import_all, + r" +mod foo { pub struct Foo; } +mod bar { pub struct Bar; } + +fn main() { + Foo$0; + Bar; +} +", + r" +use {foo::Foo, bar::Bar}; + +mod foo { pub struct Foo; } +mod bar { pub struct Bar; } + +fn main() { + Foo; + Bar; +} +", + ) + } + + /// only import in current scope + #[test] + fn auto_import_all_scope() { + check_assist_by_label( + auto_import_all, + r" +mod foo { pub struct Foo; } +mod bar { + pub struct Bar; + fn use_foo(){ + Foo$0; + } +} + +fn main() { + Foo; + Bar; +} +", + r" +mod foo { pub struct Foo; } +mod bar { + use {crate::foo::Foo}; + + pub struct Bar; + fn use_foo(){ + Foo; + } +} + +fn main() { + Foo; + Bar; +} +", + "Import all missing items", + ) + } + + /// notice that only after struct is imported, + /// the `ImportAssets` can search and found + /// the trait. + #[test] + fn auto_import_all_two_step_struct_trait() { + check_assist( + auto_import_all, + r" +mod foo { pub struct Foo; } +mod bar { pub trait Bar{fn foo() -> bool{true}} } + +impl bar::Bar for foo::Foo{} + +fn main() { +Foo::foo()$0; +} +", + r" +use {foo::Foo}; + +mod foo { pub struct Foo; } +mod bar { pub trait Bar{fn foo() -> bool{true}} } + +impl bar::Bar for foo::Foo{} + +fn main() { +Foo::foo(); +} +", + ); + check_assist( + auto_import_all, + r" +use foo::Foo; +mod foo { pub struct Foo; } +mod bar { pub trait Bar{fn foo() -> bool{true}} } + +impl bar::Bar for foo::Foo{} + +fn main() { + Foo::foo()$0; +} +", + r" +use foo::Foo; + +use {bar::Bar as _}; +mod foo { pub struct Foo; } +mod bar { pub trait Bar{fn foo() -> bool{true}} } + +impl bar::Bar for foo::Foo{} + +fn main() { + Foo::foo(); +} +", + ); + } + + #[test] + fn auto_import_all_multiple_scopes() { + // This test verifies that the ImportAction struct correctly handles + // multiple scopes and different import types + check_assist_by_label( + auto_import_all, + r" +mod foo { + pub struct Foo; + pub trait FooTrait { fn foo_method() {} } +} +mod bar { + pub struct Bar; + pub mod nested { pub struct Nested; } +} + +fn main() { + // Multiple imports in the same scope + Foo$0; + Bar; + + // Nested import + nested::Nested; + + // Block scope with trait method + { + Foo::foo_method(); + } +} +", + r" +use {foo::Foo, bar::Bar, bar::nested}; + +mod foo { + pub struct Foo; + pub trait FooTrait { fn foo_method() {} } +} +mod bar { + pub struct Bar; + pub mod nested { pub struct Nested; } +} + +fn main() { + // Multiple imports in the same scope + Foo; + Bar; + + // Nested import + nested::Nested; + + // Block scope with trait method + { + Foo::foo_method(); + } +} +", + "Import all missing items", + ); + } } diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs index 7e9d59661481..2fa536589849 100644 --- a/crates/ide-assists/src/lib.rs +++ b/crates/ide-assists/src/lib.rs @@ -245,6 +245,7 @@ mod handlers { apply_demorgan::apply_demorgan_iterator, apply_demorgan::apply_demorgan, auto_import::auto_import, + auto_import::auto_import_all, bind_unused_param::bind_unused_param, change_visibility::change_visibility, convert_bool_then::convert_bool_then_to_if, diff --git a/crates/ide-db/src/imports/insert_use.rs b/crates/ide-db/src/imports/insert_use.rs index d26e5d62ced5..de000e51311c 100644 --- a/crates/ide-db/src/imports/insert_use.rs +++ b/crates/ide-db/src/imports/insert_use.rs @@ -235,6 +235,31 @@ fn insert_use_with_alias_option( insert_use_(scope, use_item, cfg.group); } +/// Insert multiple uses with optional alias, notice this will not merge existing imports, and only happen within the same scope +pub fn insert_multiple_use_with_alias_option( + scope: &ImportScope, + path_alias: &[(ast::Path, Option)], + cfg: &InsertUseConfig, +) { + let _p = tracing::info_span!("insert_multiple_use_with_alias_option").entered(); + + let use_trees = path_alias + .iter() + .map(|(path, alias)| make::use_tree(path.clone(), None, alias.clone(), false)) + .collect::>(); + let use_tree_list = make::use_tree_list(use_trees); + + // make a `use ::{use_tree_list}` + let use_tree = make::use_tree_from_tree_list(use_tree_list); + + let use_item = make::use_(None, use_tree).clone_for_update(); + + // not merge into existing imports, as those multiple use might need to be revised by user + // and having them in one place might help user to revise them + // so look for the place we have to insert to + insert_use_(scope, use_item, cfg.group); +} + pub fn ast_to_remove_for_path_in_use_stmt(path: &ast::Path) -> Option> { // FIXME: improve this if path.parent_path().is_some() { diff --git a/crates/ide-db/src/source_change.rs b/crates/ide-db/src/source_change.rs index 741dc6bb3c88..814346156416 100644 --- a/crates/ide-db/src/source_change.rs +++ b/crates/ide-db/src/source_change.rs @@ -3,6 +3,7 @@ //! //! It can be viewed as a dual for `Change`. +use std::sync::{Arc, Mutex}; use std::{collections::hash_map::Entry, fmt, iter, mem}; use crate::text_edit::{TextEdit, TextEditBuilder}; @@ -38,6 +39,7 @@ pub struct ChangeAnnotation { #[derive(Default, Debug, Clone)] pub struct SourceChange { pub source_file_edits: IntMap)>, + pub choice_action_callbacks: IntMap, pub file_system_edits: Vec, pub is_snippet: bool, pub annotations: FxHashMap, @@ -45,6 +47,13 @@ pub struct SourceChange { } impl SourceChange { + pub fn from_choice_action(file_id: impl Into, action: ChoiceAction) -> Self { + SourceChange { + choice_action_callbacks: iter::once((file_id.into(), action)).collect(), + ..Default::default() + } + } + pub fn from_text_edit(file_id: impl Into, edit: TextEdit) -> Self { SourceChange { source_file_edits: iter::once((file_id.into(), (edit, None))).collect(), @@ -557,3 +566,65 @@ impl PlaceSnippet { } } } + +/// Choice Action to perform +#[derive(Clone)] +pub struct ChoiceAction { + /// The file to apply the choice to + file: FileId, + callback: Arc>, +} + +impl ChoiceAction { + pub fn new(file: FileId, callback: Arc>) -> Self { + Self { file, callback } + } +} + +impl std::fmt::Debug for ChoiceAction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ChoiceAction").field("file", &self.file).finish() + } +} +/// a function that takes a `SourceChangeBuilder` and a slice of strings +/// which is the choice being made, each one from corrseponding choice list in `choices_list` +/// or if no choice is provided, `None` is passed in corresponding position +pub type IndependentChoiceCallback = + Box]) + Send + 'static>; + +/// function that takes a `SourceChangeBuilder` and a usize indicate which question this is, and a string +/// which is the choice being made, return next choice list +/// +/// for example, the first time `f` is called, it is called with `0` and a string in `first_choice_list` and returns +/// the next choice list, the second time `f` is called, it is called with `1` and a string in the second choice list +/// and returns the next choice list, and so on +/// +/// in conclusion, `f` is like rewrite a proper async function into a state machine with each await point being a choice +/// being made by user +pub type SequentialChoiceCallback = + Box) -> Vec + Send + 'static>; + +pub enum ChoiceCallBack { + /// where the user needs to select one or more options from a predefined list + /// + /// `choices_list` is a list of choice lists, where each choice is a string + /// + /// `f` is a function that takes a `SourceChangeBuilder` and a slice of strings + /// which is the choice being made, each one from corrseponding choice list in `choices_list` + /// or if no choice is provided, `None` is passed in corresponding position + Independent { choices_list: Vec>, f: IndependentChoiceCallback }, + /// for sequential, stateful interactions where choices influence subsequent steps. + /// + /// `first_choice_list` is a list of choices, where each choice is a string + /// + /// `f` is a function that takes a `SourceChangeBuilder` and a usize indicate which question this is, and a string + /// which is the choice being made, return next choice list + /// + /// for example, the first time `f` is called, it is called with `0` and a string in `first_choice_list` and returns + /// the next choice list, the second time `f` is called, it is called with `1` and a string in the second choice list + /// and returns the next choice list, and so on + /// + /// in conclusion, `f` is like rewrite a proper async function into a state machine with each await point being a choice + /// being made by user + Sequential { first_choice_list: Vec, f: SequentialChoiceCallback }, +} diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index 47fcb5ac02b7..ec9e0822a3f0 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -1175,6 +1175,7 @@ impl GlobalState { .on::(handlers::handle_runnables) .on::(handlers::handle_related_tests) .on::(handlers::handle_code_action) + // TODO(discord9): move this to on_sync_mut to .on_identity::(handlers::handle_code_action_resolve) .on::(handlers::handle_hover) .on::(handlers::handle_open_docs) diff --git a/crates/syntax/src/ast/make.rs b/crates/syntax/src/ast/make.rs index d5a5119ff68d..1a34d1ec64f1 100644 --- a/crates/syntax/src/ast/make.rs +++ b/crates/syntax/src/ast/make.rs @@ -437,6 +437,12 @@ pub fn use_tree( } ast_from_text(&buf) } +pub fn use_tree_from_tree_list(use_tree_list: ast::UseTreeList) -> ast::UseTree { + let mut buf = "use ".to_owned(); + format_to!(buf, "{use_tree_list}"); + + ast_from_text(&buf) +} pub fn use_tree_list(use_trees: impl IntoIterator) -> ast::UseTreeList { let use_trees = use_trees.into_iter().map(|it| it.syntax().clone()).join(", ");