diff --git a/src/tools/rust-analyzer/crates/hir-expand/src/files.rs b/src/tools/rust-analyzer/crates/hir-expand/src/files.rs index 321ee8feb9a8c..8024823cbc5cb 100644 --- a/src/tools/rust-analyzer/crates/hir-expand/src/files.rs +++ b/src/tools/rust-analyzer/crates/hir-expand/src/files.rs @@ -42,6 +42,49 @@ impl FilePosition { FilePositionWrapper { file_id: self.file_id.file_id(db), offset: self.offset } } } + +impl From for HirFileRange { + fn from(value: FileRange) -> Self { + HirFileRange { file_id: value.file_id.into(), range: value.range } + } +} + +impl From for HirFilePosition { + fn from(value: FilePosition) -> Self { + HirFilePosition { file_id: value.file_id.into(), offset: value.offset } + } +} + +impl FilePositionWrapper { + pub fn with_edition(self, db: &dyn ExpandDatabase, edition: span::Edition) -> FilePosition { + FilePositionWrapper { + file_id: EditionedFileId::new(db, self.file_id, edition), + offset: self.offset, + } + } +} + +impl FileRangeWrapper { + pub fn with_edition(self, db: &dyn ExpandDatabase, edition: span::Edition) -> FileRange { + FileRangeWrapper { + file_id: EditionedFileId::new(db, self.file_id, edition), + range: self.range, + } + } +} + +impl InFileWrapper { + pub fn with_edition(self, db: &dyn ExpandDatabase, edition: span::Edition) -> InRealFile { + InRealFile { file_id: EditionedFileId::new(db, self.file_id, edition), value: self.value } + } +} + +impl HirFileRange { + pub fn file_range(self) -> Option { + Some(FileRange { file_id: self.file_id.file_id()?, range: self.range }) + } +} + #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub struct FileRangeWrapper { pub file_id: FileKind, @@ -194,6 +237,9 @@ impl InFileWrapper { pub fn syntax(&self) -> InFileWrapper { self.with_value(self.value.syntax()) } + pub fn node_file_range(&self) -> FileRangeWrapper { + FileRangeWrapper { file_id: self.file_id, range: self.value.syntax().text_range() } + } } impl InFileWrapper { @@ -204,9 +250,9 @@ impl InFileWrapper { } // region:specific impls -impl> InRealFile { - pub fn file_range(&self) -> FileRange { - FileRange { file_id: self.file_id, range: self.value.borrow().text_range() } +impl> InFileWrapper { + pub fn file_range(&self) -> FileRangeWrapper { + FileRangeWrapper { file_id: self.file_id, range: self.value.borrow().text_range() } } } diff --git a/src/tools/rust-analyzer/crates/hir-expand/src/lib.rs b/src/tools/rust-analyzer/crates/hir-expand/src/lib.rs index d844d8f41eeff..6ecac1463f5c4 100644 --- a/src/tools/rust-analyzer/crates/hir-expand/src/lib.rs +++ b/src/tools/rust-analyzer/crates/hir-expand/src/lib.rs @@ -392,6 +392,10 @@ impl HirFileId { } } + pub fn call_node(self, db: &dyn ExpandDatabase) -> Option> { + Some(db.lookup_intern_macro_call(self.macro_file()?).to_node(db)) + } + pub fn as_builtin_derive_attr_node( &self, db: &dyn ExpandDatabase, @@ -848,7 +852,10 @@ impl ExpansionInfo { map_node_range_up(db, &self.exp_map, range) } - /// Maps up the text range out of the expansion into is macro call. + /// Maps up the text range out of the expansion into its macro call. + /// + /// Note that this may return multiple ranges as we lose the precise association between input to output + /// and as such we may consider inputs that are unrelated. pub fn map_range_up_once( &self, db: &dyn ExpandDatabase, @@ -864,11 +871,10 @@ impl ExpansionInfo { InFile { file_id, value: smallvec::smallvec![span.range + anchor_offset] } } SpanMap::ExpansionSpanMap(arg_map) => { - let arg_range = self - .arg - .value - .as_ref() - .map_or_else(|| TextRange::empty(TextSize::from(0)), |it| it.text_range()); + let Some(arg_node) = &self.arg.value else { + return InFile::new(self.arg.file_id, smallvec::smallvec![]); + }; + let arg_range = arg_node.text_range(); InFile::new( self.arg.file_id, arg_map diff --git a/src/tools/rust-analyzer/crates/hir-expand/src/prettify_macro_expansion_.rs b/src/tools/rust-analyzer/crates/hir-expand/src/prettify_macro_expansion_.rs index 11cc434c2d826..6134c3a36b931 100644 --- a/src/tools/rust-analyzer/crates/hir-expand/src/prettify_macro_expansion_.rs +++ b/src/tools/rust-analyzer/crates/hir-expand/src/prettify_macro_expansion_.rs @@ -20,42 +20,46 @@ pub fn prettify_macro_expansion( let span_offset = syn.text_range().start(); let target_crate = target_crate_id.data(db); let mut syntax_ctx_id_to_dollar_crate_replacement = FxHashMap::default(); - syntax_bridge::prettify_macro_expansion::prettify_macro_expansion(syn, &mut |dollar_crate| { - let ctx = span_map.span_at(dollar_crate.text_range().start() + span_offset).ctx; - let replacement = - syntax_ctx_id_to_dollar_crate_replacement.entry(ctx).or_insert_with(|| { - let macro_call_id = - ctx.outer_expn(db).expect("`$crate` cannot come from `SyntaxContextId::ROOT`"); - let macro_call = db.lookup_intern_macro_call(macro_call_id.into()); - let macro_def_crate = macro_call.def.krate; - // First, if this is the same crate as the macro, nothing will work but `crate`. - // If not, if the target trait has the macro's crate as a dependency, using the dependency name - // will work in inserted code and match the user's expectation. - // If not, the crate's display name is what the dependency name is likely to be once such dependency - // is inserted, and also understandable to the user. - // Lastly, if nothing else found, resort to leaving `$crate`. - if target_crate_id == macro_def_crate { - make::tokens::crate_kw() - } else if let Some(dep) = - target_crate.dependencies.iter().find(|dep| dep.crate_id == macro_def_crate) - { - make::tokens::ident(dep.name.as_str()) - } else if let Some(crate_name) = ¯o_def_crate.extra_data(db).display_name { - make::tokens::ident(crate_name.crate_name().as_str()) - } else { - return dollar_crate.clone(); - } - }); - if replacement.text() == "$crate" { - // The parent may have many children, and looking for the token may yield incorrect results. - return dollar_crate.clone(); - } - // We need to `clone_subtree()` but rowan doesn't provide such operation for tokens. - let parent = replacement.parent().unwrap().clone_subtree().clone_for_update(); - parent - .children_with_tokens() - .filter_map(NodeOrToken::into_token) - .find(|it| it.kind() == replacement.kind()) - .unwrap() - }) + syntax_bridge::prettify_macro_expansion::prettify_macro_expansion( + syn, + &mut |dollar_crate| { + let ctx = span_map.span_at(dollar_crate.text_range().start() + span_offset).ctx; + let replacement = + syntax_ctx_id_to_dollar_crate_replacement.entry(ctx).or_insert_with(|| { + let macro_call_id = ctx + .outer_expn(db) + .expect("`$crate` cannot come from `SyntaxContextId::ROOT`"); + let macro_call = db.lookup_intern_macro_call(macro_call_id.into()); + let macro_def_crate = macro_call.def.krate; + // First, if this is the same crate as the macro, nothing will work but `crate`. + // If not, if the target trait has the macro's crate as a dependency, using the dependency name + // will work in inserted code and match the user's expectation. + // If not, the crate's display name is what the dependency name is likely to be once such dependency + // is inserted, and also understandable to the user. + // Lastly, if nothing else found, resort to leaving `$crate`. + if target_crate_id == macro_def_crate { + make::tokens::crate_kw() + } else if let Some(dep) = + target_crate.dependencies.iter().find(|dep| dep.crate_id == macro_def_crate) + { + make::tokens::ident(dep.name.as_str()) + } else if let Some(crate_name) = ¯o_def_crate.extra_data(db).display_name { + make::tokens::ident(crate_name.crate_name().as_str()) + } else { + return dollar_crate.clone(); + } + }); + if replacement.text() == "$crate" { + // The parent may have many children, and looking for the token may yield incorrect results. + return None; + } + // We need to `clone_subtree()` but rowan doesn't provide such operation for tokens. + let parent = replacement.parent().unwrap().clone_subtree().clone_for_update(); + parent + .children_with_tokens() + .filter_map(NodeOrToken::into_token) + .find(|it| it.kind() == replacement.kind()) + }, + |_| (), + ) } diff --git a/src/tools/rust-analyzer/crates/hir/src/semantics.rs b/src/tools/rust-analyzer/crates/hir/src/semantics.rs index aea22545ed5d5..e01774650b1f8 100644 --- a/src/tools/rust-analyzer/crates/hir/src/semantics.rs +++ b/src/tools/rust-analyzer/crates/hir/src/semantics.rs @@ -24,7 +24,7 @@ use hir_expand::{ attrs::collect_attrs, builtin::{BuiltinFnLikeExpander, EagerExpander}, db::ExpandDatabase, - files::{FileRangeWrapper, InRealFile}, + files::{FileRangeWrapper, HirFileRange, InRealFile}, inert_attr_macro::find_builtin_attr_idx, mod_path::{ModPath, PathKind}, name::AsName, @@ -262,6 +262,17 @@ impl Semantics<'_, DB> { self.imp.file_to_module_defs(file.into()) } + pub fn hir_file_to_module_def(&self, file: impl Into) -> Option { + self.imp.hir_file_to_module_defs(file.into()).next() + } + + pub fn hir_file_to_module_defs( + &self, + file: impl Into, + ) -> impl Iterator { + self.imp.hir_file_to_module_defs(file.into()) + } + pub fn to_adt_def(&self, a: &ast::Adt) -> Option { self.imp.to_def(a) } @@ -357,6 +368,15 @@ impl<'db> SemanticsImpl<'db> { tree } + pub fn adjust_edition(&self, file_id: HirFileId) -> HirFileId { + if let Some(editioned_file_id) = file_id.file_id() { + self.attach_first_edition(editioned_file_id.file_id(self.db)) + .map_or(file_id, Into::into) + } else { + file_id + } + } + pub fn find_parent_file(&self, file_id: HirFileId) -> Option> { match file_id { HirFileId::FileId(file_id) => { @@ -653,7 +673,7 @@ impl<'db> SemanticsImpl<'db> { string: &ast::String, ) -> Option>)>> { let string_start = string.syntax().text_range().start(); - let token = self.wrap_token_infile(string.syntax().clone()).into_real_file().ok()?; + let token = self.wrap_token_infile(string.syntax().clone()); self.descend_into_macros_breakable(token, |token, _| { (|| { let token = token.value; @@ -693,50 +713,95 @@ impl<'db> SemanticsImpl<'db> { } /// Retrieves the formatting part of the format_args! template string at the given offset. + /// + // FIXME: Type the return type + /// Returns the range (pre-expansion) in the string literal corresponding to the resolution, + /// absolute file range (post-expansion) + /// of the part in the format string, the corresponding string token and the resolution if it + /// exists. + // FIXME: Remove this in favor of `check_for_format_args_template_with_file` pub fn check_for_format_args_template( &self, original_token: SyntaxToken, offset: TextSize, - ) -> Option<(TextRange, Option>)> { - let string_start = original_token.text_range().start(); - let original_token = self.wrap_token_infile(original_token).into_real_file().ok()?; - self.descend_into_macros_breakable(original_token, |token, _| { - (|| { - let token = token.value; - self.resolve_offset_in_format_args( - ast::String::cast(token)?, - offset.checked_sub(string_start)?, - ) - .map(|(range, res)| (range + string_start, res)) - })() - .map_or(ControlFlow::Continue(()), ControlFlow::Break) - }) + ) -> Option<( + TextRange, + HirFileRange, + ast::String, + Option>, + )> { + let original_token = + self.wrap_token_infile(original_token).map(ast::String::cast).transpose()?; + self.check_for_format_args_template_with_file(original_token, offset) + } + + /// Retrieves the formatting part of the format_args! template string at the given offset. + /// + // FIXME: Type the return type + /// Returns the range (pre-expansion) in the string literal corresponding to the resolution, + /// absolute file range (post-expansion) + /// of the part in the format string, the corresponding string token and the resolution if it + /// exists. + pub fn check_for_format_args_template_with_file( + &self, + original_token: InFile, + offset: TextSize, + ) -> Option<( + TextRange, + HirFileRange, + ast::String, + Option>, + )> { + let relative_offset = + offset.checked_sub(original_token.value.syntax().text_range().start())?; + self.descend_into_macros_breakable( + original_token.as_ref().map(|it| it.syntax().clone()), + |token, _| { + (|| { + let token = token.map(ast::String::cast).transpose()?; + self.resolve_offset_in_format_args(token.as_ref(), relative_offset).map( + |(range, res)| { + ( + range + original_token.value.syntax().text_range().start(), + HirFileRange { + file_id: token.file_id, + range: range + token.value.syntax().text_range().start(), + }, + token.value, + res, + ) + }, + ) + })() + .map_or(ControlFlow::Continue(()), ControlFlow::Break) + }, + ) } fn resolve_offset_in_format_args( &self, - string: ast::String, + InFile { value: string, file_id }: InFile<&ast::String>, offset: TextSize, ) -> Option<(TextRange, Option>)> { debug_assert!(offset <= string.syntax().text_range().len()); let literal = string.syntax().parent().filter(|it| it.kind() == SyntaxKind::LITERAL)?; let parent = literal.parent()?; if let Some(format_args) = ast::FormatArgsExpr::cast(parent.clone()) { - let source_analyzer = &self.analyze_no_infer(format_args.syntax())?; - let format_args = self.wrap_node_infile(format_args); + let source_analyzer = + &self.analyze_impl(InFile::new(file_id, format_args.syntax()), None, false)?; source_analyzer - .resolve_offset_in_format_args(self.db, format_args.as_ref(), offset) + .resolve_offset_in_format_args(self.db, InFile::new(file_id, &format_args), offset) .map(|(range, res)| (range, res.map(Either::Left))) } else { let asm = ast::AsmExpr::cast(parent)?; - let source_analyzer = &self.analyze_no_infer(asm.syntax())?; + let source_analyzer = + self.analyze_impl(InFile::new(file_id, asm.syntax()), None, false)?; let line = asm.template().position(|it| *it.syntax() == literal)?; - let asm = self.wrap_node_infile(asm); - source_analyzer.resolve_offset_in_asm_template(asm.as_ref(), line, offset).map( - |(owner, (expr, range, index))| { + source_analyzer + .resolve_offset_in_asm_template(InFile::new(file_id, &asm), line, offset) + .map(|(owner, (expr, range, index))| { (range, Some(Either::Right(InlineAsmOperand { owner, expr, index }))) - }, - ) + }) } } @@ -809,14 +874,11 @@ impl<'db> SemanticsImpl<'db> { None => return res, }; let file = self.find_file(node.syntax()); - let Some(file_id) = file.file_id.file_id() else { - return res; - }; if first == last { // node is just the token, so descend the token self.descend_into_macros_impl( - InRealFile::new(file_id, first), + InFile::new(file.file_id, first), &mut |InFile { value, .. }, _ctx| { if let Some(node) = value .parent_ancestors() @@ -831,14 +893,14 @@ impl<'db> SemanticsImpl<'db> { } else { // Descend first and last token, then zip them to look for the node they belong to let mut scratch: SmallVec<[_; 1]> = smallvec![]; - self.descend_into_macros_impl(InRealFile::new(file_id, first), &mut |token, _ctx| { + self.descend_into_macros_impl(InFile::new(file.file_id, first), &mut |token, _ctx| { scratch.push(token); CONTINUE_NO_BREAKS }); let mut scratch = scratch.into_iter(); self.descend_into_macros_impl( - InRealFile::new(file_id, last), + InFile::new(file.file_id, last), &mut |InFile { value: last, file_id: last_fid }, _ctx| { if let Some(InFile { value: first, file_id: first_fid }) = scratch.next() { if first_fid == last_fid { @@ -900,22 +962,18 @@ impl<'db> SemanticsImpl<'db> { token: SyntaxToken, mut cb: impl FnMut(InFile, SyntaxContext), ) { - if let Ok(token) = self.wrap_token_infile(token).into_real_file() { - self.descend_into_macros_impl(token, &mut |t, ctx| { - cb(t, ctx); - CONTINUE_NO_BREAKS - }); - } + self.descend_into_macros_impl(self.wrap_token_infile(token), &mut |t, ctx| { + cb(t, ctx); + CONTINUE_NO_BREAKS + }); } pub fn descend_into_macros(&self, token: SyntaxToken) -> SmallVec<[SyntaxToken; 1]> { let mut res = smallvec![]; - if let Ok(token) = self.wrap_token_infile(token.clone()).into_real_file() { - self.descend_into_macros_impl(token, &mut |t, _ctx| { - res.push(t.value); - CONTINUE_NO_BREAKS - }); - } + self.descend_into_macros_impl(self.wrap_token_infile(token.clone()), &mut |t, _ctx| { + res.push(t.value); + CONTINUE_NO_BREAKS + }); if res.is_empty() { res.push(token); } @@ -928,15 +986,13 @@ impl<'db> SemanticsImpl<'db> { ) -> SmallVec<[InFile; 1]> { let mut res = smallvec![]; let token = self.wrap_token_infile(token); - if let Ok(token) = token.clone().into_real_file() { - self.descend_into_macros_impl(token, &mut |t, ctx| { - if !ctx.is_opaque(self.db) { - // Don't descend into opaque contexts - res.push(t); - } - CONTINUE_NO_BREAKS - }); - } + self.descend_into_macros_impl(token.clone(), &mut |t, ctx| { + if !ctx.is_opaque(self.db) { + // Don't descend into opaque contexts + res.push(t); + } + CONTINUE_NO_BREAKS + }); if res.is_empty() { res.push(token); } @@ -945,7 +1001,7 @@ impl<'db> SemanticsImpl<'db> { pub fn descend_into_macros_breakable( &self, - token: InRealFile, + token: InFile, mut cb: impl FnMut(InFile, SyntaxContext) -> ControlFlow, ) -> Option { self.descend_into_macros_impl(token, &mut cb) @@ -974,33 +1030,58 @@ impl<'db> SemanticsImpl<'db> { r } + /// Descends the token into expansions, returning the tokens that matches the input + /// token's [`SyntaxKind`] and text. + pub fn descend_into_macros_exact_with_file( + &self, + token: SyntaxToken, + ) -> SmallVec<[InFile; 1]> { + let mut r = smallvec![]; + let text = token.text(); + let kind = token.kind(); + + self.descend_into_macros_cb(token.clone(), |InFile { value, file_id }, ctx| { + let mapped_kind = value.kind(); + let any_ident_match = || kind.is_any_identifier() && value.kind().is_any_identifier(); + let matches = (kind == mapped_kind || any_ident_match()) + && text == value.text() + && !ctx.is_opaque(self.db); + if matches { + r.push(InFile { value, file_id }); + } + }); + if r.is_empty() { + r.push(self.wrap_token_infile(token)); + } + r + } + /// Descends the token into expansions, returning the first token that matches the input /// token's [`SyntaxKind`] and text. pub fn descend_into_macros_single_exact(&self, token: SyntaxToken) -> SyntaxToken { let text = token.text(); let kind = token.kind(); - if let Ok(token) = self.wrap_token_infile(token.clone()).into_real_file() { - self.descend_into_macros_breakable(token, |InFile { value, file_id: _ }, _ctx| { + self.descend_into_macros_breakable( + self.wrap_token_infile(token.clone()), + |InFile { value, file_id: _ }, _ctx| { let mapped_kind = value.kind(); let any_ident_match = || kind.is_any_identifier() && value.kind().is_any_identifier(); let matches = (kind == mapped_kind || any_ident_match()) && text == value.text(); if matches { ControlFlow::Break(value) } else { ControlFlow::Continue(()) } - }) - } else { - None - } + }, + ) .unwrap_or(token) } fn descend_into_macros_impl( &self, - InRealFile { value: token, file_id }: InRealFile, + InFile { value: token, file_id }: InFile, f: &mut dyn FnMut(InFile, SyntaxContext) -> ControlFlow, ) -> Option { let _p = tracing::info_span!("descend_into_macros_impl").entered(); - let span = self.db.real_span_map(file_id).span_for_range(token.text_range()); + let span = self.db.span_map(file_id).span_for_range(token.text_range()); // Process the expansion of a call, pushing all tokens with our span in the expansion back onto our stack let process_expansion_for_token = |stack: &mut Vec<_>, macro_file| { @@ -1024,17 +1105,16 @@ impl<'db> SemanticsImpl<'db> { // the tokens themselves aren't that interesting as the span that is being used to map // things down never changes. let mut stack: Vec<(_, SmallVec<[_; 2]>)> = vec![]; - let include = self.s2d_cache.borrow_mut().get_or_insert_include_for(self.db, file_id); + let include = file_id.file_id().and_then(|file_id| { + self.s2d_cache.borrow_mut().get_or_insert_include_for(self.db, file_id) + }); match include { Some(include) => { // include! inputs are always from real files, so they only need to be handled once upfront process_expansion_for_token(&mut stack, include)?; } None => { - stack.push(( - file_id.into(), - smallvec![(token, SyntaxContext::root(file_id.edition(self.db)))], - )); + stack.push((file_id, smallvec![(token, span.ctx)])); } } @@ -1678,6 +1758,11 @@ impl<'db> SemanticsImpl<'db> { self.with_ctx(|ctx| ctx.file_to_def(file).to_owned()).into_iter().map(Module::from) } + fn hir_file_to_module_defs(&self, file: HirFileId) -> impl Iterator { + // FIXME: Do we need to care about inline modules for macro expansions? + self.file_to_module_defs(file.original_file_respecting_includes(self.db).file_id(self.db)) + } + pub fn scope(&self, node: &SyntaxNode) -> Option> { self.analyze_no_infer(node).map(|SourceAnalyzer { file_id, resolver, .. }| SemanticsScope { db: self.db, diff --git a/src/tools/rust-analyzer/crates/hir/src/source_analyzer.rs b/src/tools/rust-analyzer/crates/hir/src/source_analyzer.rs index d22812d3c692e..ec2ccf8cba0be 100644 --- a/src/tools/rust-analyzer/crates/hir/src/source_analyzer.rs +++ b/src/tools/rust-analyzer/crates/hir/src/source_analyzer.rs @@ -1303,6 +1303,7 @@ impl<'db> SourceAnalyzer<'db> { false } + /// Returns the range of the implicit template argument and its resolution at the given `offset` pub(crate) fn resolve_offset_in_format_args( &self, db: &'db dyn HirDatabase, diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/auto_import.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/auto_import.rs index d310e11011be1..f3243d369a0bf 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/auto_import.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/auto_import.rs @@ -128,11 +128,7 @@ pub(crate) fn auto_import(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option< format!("Import `{import_name}`"), range, |builder| { - let scope = match 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)), - }; + let scope = builder.make_import_scope_mut(scope.clone()); insert_use(&scope, mod_path_to_ast(&import_path, edition), &ctx.config.insert_use); }, ); @@ -153,11 +149,7 @@ pub(crate) fn auto_import(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option< format!("Import `{import_name} as _`"), range, |builder| { - let scope = match 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)), - }; + let scope = builder.make_import_scope_mut(scope.clone()); insert_use_as_alias( &scope, mod_path_to_ast(&import_path, edition), @@ -1877,4 +1869,30 @@ fn main() { ", ); } + + #[test] + fn carries_cfg_attr() { + check_assist( + auto_import, + r#" +mod m { + pub struct S; +} + +#[cfg(test)] +fn foo(_: S$0) {} +"#, + r#" +#[cfg(test)] +use m::S; + +mod m { + pub struct S; +} + +#[cfg(test)] +fn foo(_: S) {} +"#, + ); + } } diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_bool_to_enum.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_bool_to_enum.rs index 00e9fdf124d16..f73b8c4fd0f19 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_bool_to_enum.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_bool_to_enum.rs @@ -312,12 +312,8 @@ fn replace_usages( } // add imports across modules where needed - if let Some((import_scope, path)) = import_data { - let scope = match import_scope { - ImportScope::File(it) => ImportScope::File(edit.make_mut(it)), - ImportScope::Module(it) => ImportScope::Module(edit.make_mut(it)), - ImportScope::Block(it) => ImportScope::Block(edit.make_mut(it)), - }; + if let Some((scope, path)) = import_data { + let scope = edit.make_import_scope_mut(scope); delayed_mutations.push((scope, path)); } }, diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs index ed8aad7b2c605..5d75e445861eb 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs @@ -996,7 +996,8 @@ pub struct $0Foo { } "#, r#" -pub struct Foo(#[my_custom_attr] u32); +pub struct Foo(#[my_custom_attr] +u32); "#, ); } diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs index 777e366da956b..0c0b93bcfbc11 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs @@ -923,7 +923,8 @@ where pub struct $0Foo(#[my_custom_attr] u32); "#, r#" -pub struct Foo { #[my_custom_attr] field1: u32 } +pub struct Foo { #[my_custom_attr] +field1: u32 } "#, ); } diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_function.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_function.rs index e977798c4fd01..cf45ea0a30d00 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_function.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_function.rs @@ -204,12 +204,7 @@ pub(crate) fn extract_function(acc: &mut Assists, ctx: &AssistContext<'_>) -> Op .kind .is_some_and(|kind| matches!(kind, FlowKind::Break(_, _) | FlowKind::Continue(_))) { - let scope = match scope { - 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)), - }; - + let scope = builder.make_import_scope_mut(scope); let control_flow_enum = FamousDefs(&ctx.sema, module.krate()).core_ops_ControlFlow(); diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_qualified_name_with_use.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_qualified_name_with_use.rs index c067747bc1bb1..fa005a411d361 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_qualified_name_with_use.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_qualified_name_with_use.rs @@ -81,11 +81,7 @@ pub(crate) fn replace_qualified_name_with_use( |builder| { // Now that we've brought the name into scope, re-qualify all paths that could be // affected (that is, all paths inside the node we added the `use` to). - let scope = match scope { - 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)), - }; + let scope = builder.make_import_scope_mut(scope); shorten_paths(scope.as_syntax_node(), &original_path); let path = drop_generic_args(&original_path); let edition = ctx diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/unqualify_method_call.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/unqualify_method_call.rs index ebb8ef99100e7..1f89a3d5f17c9 100644 --- a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/unqualify_method_call.rs +++ b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/unqualify_method_call.rs @@ -1,4 +1,3 @@ -use ide_db::imports::insert_use::ImportScope; use syntax::{ TextRange, ast::{self, AstNode, HasArgList, prec::ExprPrecedence}, @@ -114,11 +113,7 @@ fn add_import( ); if let Some(scope) = scope { - let scope = match scope { - ImportScope::File(it) => ImportScope::File(edit.make_mut(it)), - ImportScope::Module(it) => ImportScope::Module(edit.make_mut(it)), - ImportScope::Block(it) => ImportScope::Block(edit.make_mut(it)), - }; + let scope = edit.make_import_scope_mut(scope); ide_db::imports::insert_use::insert_use(&scope, import, &ctx.config.insert_use); } } diff --git a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs index d26e5d62ced51..813f38380f69d 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs @@ -60,107 +60,87 @@ pub struct InsertUseConfig { } #[derive(Debug, Clone)] -pub enum ImportScope { +pub struct ImportScope { + pub kind: ImportScopeKind, + pub required_cfgs: Vec, +} + +#[derive(Debug, Clone)] +pub enum ImportScopeKind { File(ast::SourceFile), Module(ast::ItemList), Block(ast::StmtList), } impl ImportScope { - // FIXME: Remove this? - #[cfg(test)] - fn from(syntax: SyntaxNode) -> Option { - use syntax::match_ast; - fn contains_cfg_attr(attrs: &dyn HasAttrs) -> bool { - attrs.attrs().any(|attr| attr.as_simple_call().is_some_and(|(ident, _)| ident == "cfg")) - } - match_ast! { - match syntax { - ast::Module(module) => module.item_list().map(ImportScope::Module), - ast::SourceFile(file) => Some(ImportScope::File(file)), - ast::Fn(func) => contains_cfg_attr(&func).then(|| func.body().and_then(|it| it.stmt_list().map(ImportScope::Block))).flatten(), - ast::Const(konst) => contains_cfg_attr(&konst).then(|| match konst.body()? { - ast::Expr::BlockExpr(block) => Some(block), - _ => None, - }).flatten().and_then(|it| it.stmt_list().map(ImportScope::Block)), - ast::Static(statik) => contains_cfg_attr(&statik).then(|| match statik.body()? { - ast::Expr::BlockExpr(block) => Some(block), - _ => None, - }).flatten().and_then(|it| it.stmt_list().map(ImportScope::Block)), - _ => None, - - } - } - } - /// Determines the containing syntax node in which to insert a `use` statement affecting `position`. /// Returns the original source node inside attributes. pub fn find_insert_use_container( position: &SyntaxNode, sema: &Semantics<'_, RootDatabase>, ) -> Option { - fn contains_cfg_attr(attrs: &dyn HasAttrs) -> bool { - attrs.attrs().any(|attr| attr.as_simple_call().is_some_and(|(ident, _)| ident == "cfg")) - } - + // The closest block expression ancestor + let mut block = None; + let mut required_cfgs = Vec::new(); // Walk up the ancestor tree searching for a suitable node to do insertions on // with special handling on cfg-gated items, in which case we want to insert imports locally // or FIXME: annotate inserted imports with the same cfg for syntax in sema.ancestors_with_macros(position.clone()) { if let Some(file) = ast::SourceFile::cast(syntax.clone()) { - return Some(ImportScope::File(file)); - } else if let Some(item) = ast::Item::cast(syntax) { - return match item { - ast::Item::Const(konst) if contains_cfg_attr(&konst) => { - // FIXME: Instead of bailing out with None, we should note down that - // this import needs an attribute added - match sema.original_ast_node(konst)?.body()? { - ast::Expr::BlockExpr(block) => block, - _ => return None, + return Some(ImportScope { kind: ImportScopeKind::File(file), required_cfgs }); + } else if let Some(module) = ast::Module::cast(syntax.clone()) { + // early return is important here, if we can't find the original module + // in the input there is no way for us to insert an import anywhere. + return sema + .original_ast_node(module)? + .item_list() + .map(ImportScopeKind::Module) + .map(|kind| ImportScope { kind, required_cfgs }); + } else if let Some(has_attrs) = ast::AnyHasAttrs::cast(syntax) { + if block.is_none() { + if let Some(b) = ast::BlockExpr::cast(has_attrs.syntax().clone()) { + if let Some(b) = sema.original_ast_node(b) { + block = b.stmt_list(); } - .stmt_list() - .map(ImportScope::Block) } - ast::Item::Fn(func) if contains_cfg_attr(&func) => { - // FIXME: Instead of bailing out with None, we should note down that - // this import needs an attribute added - sema.original_ast_node(func)?.body()?.stmt_list().map(ImportScope::Block) - } - ast::Item::Static(statik) if contains_cfg_attr(&statik) => { - // FIXME: Instead of bailing out with None, we should note down that - // this import needs an attribute added - match sema.original_ast_node(statik)?.body()? { - ast::Expr::BlockExpr(block) => block, - _ => return None, - } - .stmt_list() - .map(ImportScope::Block) - } - ast::Item::Module(module) => { - // early return is important here, if we can't find the original module - // in the input there is no way for us to insert an import anywhere. - sema.original_ast_node(module)?.item_list().map(ImportScope::Module) + } + if has_attrs + .attrs() + .any(|attr| attr.as_simple_call().is_some_and(|(ident, _)| ident == "cfg")) + { + if let Some(b) = block { + return Some(ImportScope { + kind: ImportScopeKind::Block(b), + required_cfgs, + }); } - _ => continue, - }; + required_cfgs.extend(has_attrs.attrs().filter(|attr| { + attr.as_simple_call().is_some_and(|(ident, _)| ident == "cfg") + })); + } } } None } pub fn as_syntax_node(&self) -> &SyntaxNode { - match self { - ImportScope::File(file) => file.syntax(), - ImportScope::Module(item_list) => item_list.syntax(), - ImportScope::Block(block) => block.syntax(), + match &self.kind { + ImportScopeKind::File(file) => file.syntax(), + ImportScopeKind::Module(item_list) => item_list.syntax(), + ImportScopeKind::Block(block) => block.syntax(), } } pub fn clone_for_update(&self) -> Self { - match self { - ImportScope::File(file) => ImportScope::File(file.clone_for_update()), - ImportScope::Module(item_list) => ImportScope::Module(item_list.clone_for_update()), - ImportScope::Block(block) => ImportScope::Block(block.clone_for_update()), + Self { + kind: match &self.kind { + ImportScopeKind::File(file) => ImportScopeKind::File(file.clone_for_update()), + ImportScopeKind::Module(item_list) => { + ImportScopeKind::Module(item_list.clone_for_update()) + } + ImportScopeKind::Block(block) => ImportScopeKind::Block(block.clone_for_update()), + }, + required_cfgs: self.required_cfgs.iter().map(|attr| attr.clone_for_update()).collect(), } } } @@ -216,6 +196,11 @@ fn insert_use_with_alias_option( use_tree.wrap_in_tree_list(); } let use_item = make::use_(None, use_tree).clone_for_update(); + for attr in + scope.required_cfgs.iter().map(|attr| attr.syntax().clone_subtree().clone_for_update()) + { + ted::insert(ted::Position::first_child_of(use_item.syntax()), attr); + } // merge into existing imports if possible if let Some(mb) = mb { @@ -229,7 +214,6 @@ fn insert_use_with_alias_option( } } } - // 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 insert_use_(scope, use_item, cfg.group); @@ -316,10 +300,10 @@ fn guess_granularity_from_scope(scope: &ImportScope) -> ImportGranularityGuess { } _ => None, }; - let mut use_stmts = match scope { - ImportScope::File(f) => f.items(), - ImportScope::Module(m) => m.items(), - ImportScope::Block(b) => b.items(), + let mut use_stmts = match &scope.kind { + ImportScopeKind::File(f) => f.items(), + ImportScopeKind::Module(m) => m.items(), + ImportScopeKind::Block(b) => b.items(), } .filter_map(use_stmt); let mut res = ImportGranularityGuess::Unknown; @@ -463,12 +447,12 @@ fn insert_use_(scope: &ImportScope, use_item: ast::Use, group_imports: bool) { } } - let l_curly = match scope { - ImportScope::File(_) => None, + let l_curly = match &scope.kind { + ImportScopeKind::File(_) => None, // don't insert the imports before the item list/block expr's opening curly brace - ImportScope::Module(item_list) => item_list.l_curly_token(), + ImportScopeKind::Module(item_list) => item_list.l_curly_token(), // don't insert the imports before the item list's opening curly brace - ImportScope::Block(block) => block.l_curly_token(), + ImportScopeKind::Block(block) => block.l_curly_token(), }; // there are no imports in this file at all // so put the import after all inner module attributes and possible license header comments diff --git a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs index 428ba1d511897..4a00854f01a5f 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use/tests.rs @@ -23,7 +23,7 @@ struct Struct; } #[test] -fn respects_cfg_attr_fn() { +fn respects_cfg_attr_fn_body() { check( r"bar::Bar", r#" @@ -40,6 +40,25 @@ fn foo() { ); } +#[test] +fn respects_cfg_attr_fn_sig() { + check( + r"bar::Bar", + r#" +#[cfg(test)] +fn foo($0) {} +"#, + r#" +#[cfg(test)] +use bar::Bar; + +#[cfg(test)] +fn foo() {} +"#, + ImportGranularity::Crate, + ); +} + #[test] fn respects_cfg_attr_const() { check( @@ -58,6 +77,51 @@ const FOO: Bar = { ); } +#[test] +fn respects_cfg_attr_impl() { + check( + r"bar::Bar", + r#" +#[cfg(test)] +impl () {$0} +"#, + r#" +#[cfg(test)] +use bar::Bar; + +#[cfg(test)] +impl () {} +"#, + ImportGranularity::Crate, + ); +} + +#[test] +fn respects_cfg_attr_multiple_layers() { + check( + r"bar::Bar", + r#" +#[cfg(test)] +impl () { + #[cfg(test2)] + fn f($0) {} +} +"#, + r#" +#[cfg(test)] +#[cfg(test2)] +use bar::Bar; + +#[cfg(test)] +impl () { + #[cfg(test2)] + fn f() {} +} +"#, + ImportGranularity::Crate, + ); +} + #[test] fn insert_skips_lone_glob_imports() { check( @@ -813,7 +877,7 @@ use {std::io};", } #[test] -fn merge_groups_skip_attributed() { +fn merge_groups_cfg_vs_no_cfg() { check_crate( "std::io", r#" @@ -836,6 +900,25 @@ use {std::io}; ); } +#[test] +fn merge_groups_cfg_matching() { + check_crate( + "std::io", + r#" +#[cfg(feature = "gated")] use std::fmt::{Result, Display}; + +#[cfg(feature = "gated")] +fn f($0) {} +"#, + r#" +#[cfg(feature = "gated")] use std::{fmt::{Display, Result}, io}; + +#[cfg(feature = "gated")] +fn f() {} +"#, + ); +} + #[test] fn split_out_merge() { // FIXME: This is suboptimal, we want to get `use std::fmt::{self, Result}` @@ -1259,12 +1342,14 @@ fn check_with_config( }; let sema = &Semantics::new(&db); let source_file = sema.parse(file_id); - let syntax = source_file.syntax().clone_for_update(); let file = pos - .and_then(|pos| syntax.token_at_offset(pos.expect_offset()).next()?.parent()) + .and_then(|pos| source_file.syntax().token_at_offset(pos.expect_offset()).next()?.parent()) .and_then(|it| ImportScope::find_insert_use_container(&it, sema)) - .or_else(|| ImportScope::from(syntax)) - .unwrap(); + .unwrap_or_else(|| ImportScope { + kind: ImportScopeKind::File(source_file), + required_cfgs: vec![], + }) + .clone_for_update(); let path = ast::SourceFile::parse(&format!("use {path};"), span::Edition::CURRENT) .tree() .syntax() @@ -1349,7 +1434,7 @@ fn check_merge_only_fail(ra_fixture0: &str, ra_fixture1: &str, mb: MergeBehavior } fn check_guess(#[rust_analyzer::rust_fixture] ra_fixture: &str, expected: ImportGranularityGuess) { - let syntax = ast::SourceFile::parse(ra_fixture, span::Edition::CURRENT).tree().syntax().clone(); - let file = ImportScope::from(syntax).unwrap(); + let syntax = ast::SourceFile::parse(ra_fixture, span::Edition::CURRENT).tree(); + let file = ImportScope { kind: ImportScopeKind::File(syntax), required_cfgs: vec![] }; assert_eq!(super::guess_granularity_from_scope(&file), expected); } diff --git a/src/tools/rust-analyzer/crates/ide-db/src/search.rs b/src/tools/rust-analyzer/crates/ide-db/src/search.rs index d4ab75929279d..c5ad64ed59412 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/search.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/search.rs @@ -961,12 +961,16 @@ impl<'a> FindUsages<'a> { // Search for occurrences of the items name for offset in Self::match_indices(&text, finder, search_range) { let ret = tree.token_at_offset(offset).any(|token| { - let Some(str_token) = ast::String::cast(token.clone()) else { return false }; - if let Some((range, Some(nameres))) = - sema.check_for_format_args_template(token, offset) + if let Some((range, _frange, string_token, Some(nameres))) = + sema.check_for_format_args_template(token.clone(), offset) { - return self - .found_format_args_ref(file_id, range, str_token, nameres, sink); + return self.found_format_args_ref( + file_id, + range, + string_token, + nameres, + sink, + ); } false }); diff --git a/src/tools/rust-analyzer/crates/ide-db/src/source_change.rs b/src/tools/rust-analyzer/crates/ide-db/src/source_change.rs index b1b58d6568cb5..16c0d8d97a7db 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/source_change.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/source_change.rs @@ -5,6 +5,7 @@ use std::{collections::hash_map::Entry, fmt, iter, mem}; +use crate::imports::insert_use::{ImportScope, ImportScopeKind}; use crate::text_edit::{TextEdit, TextEditBuilder}; use crate::{SnippetCap, assists::Command, syntax_helpers::tree_diff::diff}; use base_db::AnchoredPathBuf; @@ -367,6 +368,17 @@ impl SourceChangeBuilder { pub fn make_mut(&mut self, node: N) -> N { self.mutated_tree.get_or_insert_with(|| TreeMutator::new(node.syntax())).make_mut(&node) } + + pub fn make_import_scope_mut(&mut self, scope: ImportScope) -> ImportScope { + ImportScope { + kind: match scope.kind.clone() { + ImportScopeKind::File(it) => ImportScopeKind::File(self.make_mut(it)), + ImportScopeKind::Module(it) => ImportScopeKind::Module(self.make_mut(it)), + ImportScopeKind::Block(it) => ImportScopeKind::Block(self.make_mut(it)), + }, + required_cfgs: scope.required_cfgs.iter().map(|it| self.make_mut(it.clone())).collect(), + } + } /// Returns a copy of the `node`, suitable for mutation. /// /// Syntax trees in rust-analyzer are typically immutable, and mutating diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/json_is_not_rust.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/json_is_not_rust.rs index ac1b599c49e2a..87c9397fb771d 100644 --- a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/json_is_not_rust.rs +++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/json_is_not_rust.rs @@ -137,11 +137,7 @@ pub(crate) fn json_in_items( ) .with_fixes(Some(vec![{ let mut scb = SourceChangeBuilder::new(vfs_file_id); - let scope = match import_scope { - ImportScope::File(it) => ImportScope::File(scb.make_mut(it)), - ImportScope::Module(it) => ImportScope::Module(scb.make_mut(it)), - ImportScope::Block(it) => ImportScope::Block(scb.make_mut(it)), - }; + let scope = scb.make_import_scope_mut(import_scope); let current_module = semantics_scope.module(); let cfg = ImportPathConfig { diff --git a/src/tools/rust-analyzer/crates/ide/src/goto_definition.rs b/src/tools/rust-analyzer/crates/ide/src/goto_definition.rs index c60ca3562f663..7917aab8bf782 100644 --- a/src/tools/rust-analyzer/crates/ide/src/goto_definition.rs +++ b/src/tools/rust-analyzer/crates/ide/src/goto_definition.rs @@ -67,7 +67,7 @@ pub(crate) fn goto_definition( }); } - if let Some((range, resolution)) = + if let Some((range, _, _, resolution)) = sema.check_for_format_args_template(original_token.clone(), offset) { return Some(RangeInfo::new( diff --git a/src/tools/rust-analyzer/crates/ide/src/goto_type_definition.rs b/src/tools/rust-analyzer/crates/ide/src/goto_type_definition.rs index a78f5cdc9d0e6..a6c7ea29b09e6 100644 --- a/src/tools/rust-analyzer/crates/ide/src/goto_type_definition.rs +++ b/src/tools/rust-analyzer/crates/ide/src/goto_type_definition.rs @@ -53,7 +53,9 @@ pub(crate) fn goto_type_definition( } }); }; - if let Some((range, resolution)) = sema.check_for_format_args_template(token.clone(), offset) { + if let Some((range, _, _, resolution)) = + sema.check_for_format_args_template(token.clone(), offset) + { if let Some(ty) = resolution.and_then(|res| match Definition::from(res) { Definition::Const(it) => Some(it.ty(db)), Definition::Static(it) => Some(it.ty(db)), diff --git a/src/tools/rust-analyzer/crates/ide/src/highlight_related.rs b/src/tools/rust-analyzer/crates/ide/src/highlight_related.rs index 520ba39a238b7..aa947921a9bb8 100644 --- a/src/tools/rust-analyzer/crates/ide/src/highlight_related.rs +++ b/src/tools/rust-analyzer/crates/ide/src/highlight_related.rs @@ -11,7 +11,6 @@ use ide_db::{ preorder_expr_with_ctx_checker, }, }; -use span::FileId; use syntax::{ AstNode, SyntaxKind::{self, IDENT, INT_NUMBER}, @@ -61,13 +60,12 @@ pub(crate) fn highlight_related( let file_id = sema .attach_first_edition(file_id) .unwrap_or_else(|| EditionedFileId::current_edition(sema.db, file_id)); - let span_file_id = file_id.editioned_file_id(sema.db); let syntax = sema.parse(file_id).syntax().clone(); let token = pick_best_token(syntax.token_at_offset(offset), |kind| match kind { T![?] => 4, // prefer `?` when the cursor is sandwiched like in `await$0?` T![->] => 4, - kind if kind.is_keyword(span_file_id.edition()) => 3, + kind if kind.is_keyword(file_id.edition(sema.db)) => 3, IDENT | INT_NUMBER => 2, T![|] => 1, _ => 0, @@ -92,18 +90,11 @@ pub(crate) fn highlight_related( T![unsafe] if token.parent().and_then(ast::BlockExpr::cast).is_some() => { highlight_unsafe_points(sema, token).remove(&file_id) } - T![|] if config.closure_captures => { - highlight_closure_captures(sema, token, file_id, span_file_id.file_id()) + T![|] if config.closure_captures => highlight_closure_captures(sema, token, file_id), + T![move] if config.closure_captures => highlight_closure_captures(sema, token, file_id), + _ if config.references => { + highlight_references(sema, token, FilePosition { file_id, offset }) } - T![move] if config.closure_captures => { - highlight_closure_captures(sema, token, file_id, span_file_id.file_id()) - } - _ if config.references => highlight_references( - sema, - token, - FilePosition { file_id, offset }, - span_file_id.file_id(), - ), _ => None, } } @@ -112,7 +103,6 @@ fn highlight_closure_captures( sema: &Semantics<'_, RootDatabase>, token: SyntaxToken, file_id: EditionedFileId, - vfs_file_id: FileId, ) -> Option> { let closure = token.parent_ancestors().take(2).find_map(ast::ClosureExpr::cast)?; let search_range = closure.body()?.syntax().text_range(); @@ -145,7 +135,7 @@ fn highlight_closure_captures( .sources(sema.db) .into_iter() .flat_map(|x| x.to_nav(sema.db)) - .filter(|decl| decl.file_id == vfs_file_id) + .filter(|decl| decl.file_id == file_id.file_id(sema.db)) .filter_map(|decl| decl.focus_range) .map(move |range| HighlightedRange { range, category }) .chain(usages) @@ -158,9 +148,8 @@ fn highlight_references( sema: &Semantics<'_, RootDatabase>, token: SyntaxToken, FilePosition { file_id, offset }: FilePosition, - vfs_file_id: FileId, ) -> Option> { - let defs = if let Some((range, resolution)) = + let defs = if let Some((range, _, _, resolution)) = sema.check_for_format_args_template(token.clone(), offset) { match resolution.map(Definition::from) { @@ -270,7 +259,7 @@ fn highlight_references( .sources(sema.db) .into_iter() .flat_map(|x| x.to_nav(sema.db)) - .filter(|decl| decl.file_id == vfs_file_id) + .filter(|decl| decl.file_id == file_id.file_id(sema.db)) .filter_map(|decl| decl.focus_range) .map(|range| HighlightedRange { range, category }) .for_each(|x| { @@ -288,7 +277,7 @@ fn highlight_references( }, }; for nav in navs { - if nav.file_id != vfs_file_id { + if nav.file_id != file_id.file_id(sema.db) { continue; } let hl_range = nav.focus_range.map(|range| { diff --git a/src/tools/rust-analyzer/crates/ide/src/hover.rs b/src/tools/rust-analyzer/crates/ide/src/hover.rs index 8bb1c708e25d8..5404a9dc2cece 100644 --- a/src/tools/rust-analyzer/crates/ide/src/hover.rs +++ b/src/tools/rust-analyzer/crates/ide/src/hover.rs @@ -200,7 +200,7 @@ fn hover_offset( }); } - if let Some((range, resolution)) = + if let Some((range, _, _, resolution)) = sema.check_for_format_args_template(original_token.clone(), offset) { let res = hover_for_definition( diff --git a/src/tools/rust-analyzer/crates/ide/src/lib.rs b/src/tools/rust-analyzer/crates/ide/src/lib.rs index d649dffbd90c2..82dbcde4c06fe 100644 --- a/src/tools/rust-analyzer/crates/ide/src/lib.rs +++ b/src/tools/rust-analyzer/crates/ide/src/lib.rs @@ -514,7 +514,6 @@ impl Analysis { self.with_db(|db| goto_type_definition::goto_type_definition(db, position)) } - /// Finds all usages of the reference at point. pub fn find_all_refs( &self, position: FilePosition, diff --git a/src/tools/rust-analyzer/crates/ide/src/references.rs b/src/tools/rust-analyzer/crates/ide/src/references.rs index 4fa116444b7ff..c6a323d40815a 100644 --- a/src/tools/rust-analyzer/crates/ide/src/references.rs +++ b/src/tools/rust-analyzer/crates/ide/src/references.rs @@ -8,6 +8,14 @@ //! for text occurrences of the identifier. If there's an `ast::NameRef` //! at the index that the match starts at and its tree parent is //! resolved to the search element definition, we get a reference. +//! +//! Special handling for constructors/initializations: +//! When searching for references to a struct/enum/variant, if the cursor is positioned on: +//! - `{` after a struct/enum/variant definition +//! - `(` for tuple structs/variants +//! - `;` for unit structs +//! - The type name in a struct/enum/variant definition +//! Then only constructor/initialization usages will be shown, filtering out other references. use hir::{PathResolution, Semantics}; use ide_db::{ @@ -28,27 +36,76 @@ use syntax::{ use crate::{FilePosition, HighlightedRange, NavigationTarget, TryToNav, highlight_related}; +/// Result of a reference search operation. #[derive(Debug, Clone)] pub struct ReferenceSearchResult { + /// Information about the declaration site of the searched item. + /// For ADTs (structs/enums), this points to the type definition. + /// May be None for primitives or items without clear declaration sites. pub declaration: Option, + /// All references found, grouped by file. + /// For ADTs when searching from a constructor position (e.g. on '{', '(', ';'), + /// this only includes constructor/initialization usages. + /// The map key is the file ID, and the value is a vector of (range, category) pairs. + /// - range: The text range of the reference in the file + /// - category: Metadata about how the reference is used (read/write/etc) pub references: IntMap>, } +/// Information about the declaration site of a searched item. #[derive(Debug, Clone)] pub struct Declaration { + /// Navigation information to jump to the declaration pub nav: NavigationTarget, + /// Whether the declared item is mutable (relevant for variables) pub is_mut: bool, } // Feature: Find All References // -// Shows all references of the item at the cursor location +// Shows all references of the item at the cursor location. This includes: +// - Direct references to variables, functions, types, etc. +// - Constructor/initialization references when cursor is on struct/enum definition tokens +// - References in patterns and type contexts +// - References through dereferencing and borrowing +// - References in macro expansions +// +// Special handling for constructors: +// - When the cursor is on `{`, `(`, or `;` in a struct/enum definition +// - When the cursor is on the type name in a struct/enum definition +// These cases will show only constructor/initialization usages of the type // // | Editor | Shortcut | // |---------|----------| // | VS Code | Shift+Alt+F12 | // // ![Find All References](https://user-images.githubusercontent.com/48062697/113020670-b7c34f00-917a-11eb-8003-370ac5f2b3cb.gif) + +/// Find all references to the item at the given position. +/// +/// # Arguments +/// * `sema` - Semantic analysis context +/// * `position` - Position in the file where to look for the item +/// * `search_scope` - Optional scope to limit the search (e.g. current crate only) +/// +/// # Returns +/// Returns `None` if no valid item is found at the position. +/// Otherwise returns a vector of `ReferenceSearchResult`, usually with one element. +/// Multiple results can occur in case of ambiguity or when searching for trait items. +/// +/// # Special cases +/// - Control flow keywords (break, continue, etc): Shows all related jump points +/// - Constructor search: When on struct/enum definition tokens (`{`, `(`, `;`), shows only initialization sites +/// - Format string arguments: Shows template parameter usages +/// - Lifetime parameters: Shows lifetime constraint usages +/// +/// # Constructor search +/// When the cursor is on specific tokens in a struct/enum definition: +/// - `{` after struct/enum/variant: Shows record literal initializations +/// - `(` after tuple struct/variant: Shows tuple literal initializations +/// - `;` after unit struct: Shows unit literal initializations +/// - Type name in definition: Shows all initialization usages +/// In these cases, other kinds of references (like type references) are filtered out. pub(crate) fn find_all_refs( sema: &Semantics<'_, RootDatabase>, position: FilePosition, @@ -143,7 +200,7 @@ pub(crate) fn find_defs( ) })?; - if let Some((_, resolution)) = sema.check_for_format_args_template(token.clone(), offset) { + if let Some((.., resolution)) = sema.check_for_format_args_template(token.clone(), offset) { return resolution.map(Definition::from).map(|it| vec![it]); } @@ -219,7 +276,19 @@ fn retain_adt_literal_usages( } } -/// Returns `Some` if the cursor is at a position for an item to search for all its constructor/literal usages +/// Returns `Some` if the cursor is at a position where we should search for constructor/initialization usages. +/// This is used to implement the special constructor search behavior when the cursor is on specific tokens +/// in a struct/enum/variant definition. +/// +/// # Returns +/// - `Some(name)` if the cursor is on: +/// - `{` after a struct/enum/variant definition +/// - `(` for tuple structs/variants +/// - `;` for unit structs +/// - The type name in a struct/enum/variant definition +/// - `None` otherwise +/// +/// The returned name is the name of the type whose constructor usages should be searched for. fn name_for_constructor_search(syntax: &SyntaxNode, position: FilePosition) -> Option { let token = syntax.token_at_offset(position.offset).right_biased()?; let token_parent = token.parent()?; @@ -257,6 +326,16 @@ fn name_for_constructor_search(syntax: &SyntaxNode, position: FilePosition) -> O } } +/// Checks if a name reference is part of an enum variant literal expression. +/// Used to filter references when searching for enum variant constructors. +/// +/// # Arguments +/// * `sema` - Semantic analysis context +/// * `enum_` - The enum type to check against +/// * `name_ref` - The name reference to check +/// +/// # Returns +/// `true` if the name reference is used as part of constructing a variant of the given enum. fn is_enum_lit_name_ref( sema: &Semantics<'_, RootDatabase>, enum_: hir::Enum, @@ -284,12 +363,19 @@ fn is_enum_lit_name_ref( .unwrap_or(false) } +/// Checks if a path ends with the given name reference. +/// Helper function for checking constructor usage patterns. fn path_ends_with(path: Option, name_ref: &ast::NameRef) -> bool { path.and_then(|path| path.segment()) .and_then(|segment| segment.name_ref()) .map_or(false, |segment| segment == *name_ref) } +/// Checks if a name reference is used in a literal (constructor) context. +/// Used to filter references when searching for struct/variant constructors. +/// +/// # Returns +/// `true` if the name reference is used as part of a struct/variant literal expression. fn is_lit_name_ref(name_ref: &ast::NameRef) -> bool { name_ref.syntax().ancestors().find_map(|ancestor| { match_ast! { diff --git a/src/tools/rust-analyzer/crates/ide/src/rename.rs b/src/tools/rust-analyzer/crates/ide/src/rename.rs index e6cda60cd95b5..0423e3da2c899 100644 --- a/src/tools/rust-analyzer/crates/ide/src/rename.rs +++ b/src/tools/rust-analyzer/crates/ide/src/rename.rs @@ -203,7 +203,7 @@ fn find_definitions( ) -> RenameResult> { let token = syntax.token_at_offset(offset).find(|t| matches!(t.kind(), SyntaxKind::STRING)); - if let Some((range, Some(resolution))) = + if let Some((range, _, _, Some(resolution))) = token.and_then(|token| sema.check_for_format_args_template(token, offset)) { return Ok(vec![( diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting.rs b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting.rs index e1bc76318f8b9..3ca172977cb9e 100644 --- a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting.rs +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting.rs @@ -542,7 +542,7 @@ fn descend_token( let mut t = None; let mut r = 0; - sema.descend_into_macros_breakable(token.clone(), |tok, _ctx| { + sema.descend_into_macros_breakable(token.clone().into(), |tok, _ctx| { // FIXME: Consider checking ctx transparency for being opaque? let my_rank = ranker.rank_token(&tok.value); diff --git a/src/tools/rust-analyzer/crates/mbe/src/tests.rs b/src/tools/rust-analyzer/crates/mbe/src/tests.rs index 3369dfff2816d..769455faac041 100644 --- a/src/tools/rust-analyzer/crates/mbe/src/tests.rs +++ b/src/tools/rust-analyzer/crates/mbe/src/tests.rs @@ -74,7 +74,8 @@ fn check_( "{}", syntax_bridge::prettify_macro_expansion::prettify_macro_expansion( node.syntax_node(), - &mut |it| it.clone() + &mut |_| None, + |_| () ) ); expect.assert_eq(&expect_res); diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/handlers/dispatch.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/handlers/dispatch.rs index f04ada38893a8..40d05567fccd1 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/handlers/dispatch.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/handlers/dispatch.rs @@ -6,7 +6,7 @@ use std::{ use ide_db::base_db::{ DbPanicContext, - salsa::{self, Cancelled}, + salsa::{self, Cancelled, UnexpectedCycle}, }; use lsp_server::{ExtractError, Response, ResponseError}; use serde::{Serialize, de::DeserializeOwned}; @@ -349,11 +349,14 @@ where let mut message = "request handler panicked".to_owned(); if let Some(panic_message) = panic_message { message.push_str(": "); - message.push_str(panic_message) + message.push_str(panic_message); + } else if let Some(cycle) = panic.downcast_ref::() { + tracing::error!("{cycle}"); + message.push_str(": unexpected cycle"); } else if let Ok(cancelled) = panic.downcast::() { tracing::error!("Cancellation propagated out of salsa! This is a bug"); return Err(HandlerCancelledError::Inner(*cancelled)); - } + }; Ok(lsp_server::Response::new_err( id, diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/lsp/from_proto.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/lsp/from_proto.rs index fb8a98382905c..02757616d4ffd 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/lsp/from_proto.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/lsp/from_proto.rs @@ -103,6 +103,7 @@ pub(crate) fn file_range_uri( pub(crate) fn assist_kind(kind: lsp_types::CodeActionKind) -> Option { let assist_kind = match &kind { + k if k == &lsp_types::CodeActionKind::EMPTY => AssistKind::Generate, k if k == &lsp_types::CodeActionKind::QUICKFIX => AssistKind::QuickFix, k if k == &lsp_types::CodeActionKind::REFACTOR => AssistKind::Refactor, k if k == &lsp_types::CodeActionKind::REFACTOR_EXTRACT => AssistKind::RefactorExtract, diff --git a/src/tools/rust-analyzer/crates/span/src/lib.rs b/src/tools/rust-analyzer/crates/span/src/lib.rs index 54f90908f3672..f81648ac42c58 100644 --- a/src/tools/rust-analyzer/crates/span/src/lib.rs +++ b/src/tools/rust-analyzer/crates/span/src/lib.rs @@ -112,7 +112,10 @@ pub struct EditionedFileId(u32); impl fmt::Debug for EditionedFileId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("EditionedFileId").field(&self.file_id()).field(&self.edition()).finish() + f.debug_tuple("EditionedFileId") + .field(&self.file_id().index()) + .field(&self.edition()) + .finish() } } diff --git a/src/tools/rust-analyzer/crates/syntax-bridge/src/prettify_macro_expansion.rs b/src/tools/rust-analyzer/crates/syntax-bridge/src/prettify_macro_expansion.rs index e815e07d80a4e..0a5c8df0d0aef 100644 --- a/src/tools/rust-analyzer/crates/syntax-bridge/src/prettify_macro_expansion.rs +++ b/src/tools/rust-analyzer/crates/syntax-bridge/src/prettify_macro_expansion.rs @@ -7,6 +7,13 @@ use syntax::{ ted::{self, Position}, }; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PrettifyWsKind { + Space, + Indent(usize), + Newline, +} + /// Renders a [`SyntaxNode`] with whitespace inserted between tokens that require them. /// /// This is an internal API that is only exported because `mbe` needs it for tests and cannot depend @@ -15,7 +22,8 @@ use syntax::{ #[deprecated = "use `hir_expand::prettify_macro_expansion()` instead"] pub fn prettify_macro_expansion( syn: SyntaxNode, - dollar_crate_replacement: &mut dyn FnMut(&SyntaxToken) -> SyntaxToken, + dollar_crate_replacement: &mut dyn FnMut(&SyntaxToken) -> Option, + inspect_mods: impl FnOnce(&[(Position, PrettifyWsKind)]), ) -> SyntaxNode { let mut indent = 0; let mut last: Option = None; @@ -27,14 +35,12 @@ pub fn prettify_macro_expansion( let after = Position::after; let do_indent = |pos: fn(_) -> Position, token: &SyntaxToken, indent| { - (pos(token.clone()), make::tokens::whitespace(&" ".repeat(4 * indent))) - }; - let do_ws = |pos: fn(_) -> Position, token: &SyntaxToken| { - (pos(token.clone()), make::tokens::single_space()) - }; - let do_nl = |pos: fn(_) -> Position, token: &SyntaxToken| { - (pos(token.clone()), make::tokens::single_newline()) + (pos(token.clone()), PrettifyWsKind::Indent(indent)) }; + let do_ws = + |pos: fn(_) -> Position, token: &SyntaxToken| (pos(token.clone()), PrettifyWsKind::Space); + let do_nl = + |pos: fn(_) -> Position, token: &SyntaxToken| (pos(token.clone()), PrettifyWsKind::Newline); for event in syn.preorder_with_tokens() { let token = match event { @@ -46,20 +52,19 @@ pub fn prettify_macro_expansion( ) => { if indent > 0 { - mods.push(( - Position::after(node.clone()), - make::tokens::whitespace(&" ".repeat(4 * indent)), - )); + mods.push((Position::after(node.clone()), PrettifyWsKind::Indent(indent))); } if node.parent().is_some() { - mods.push((Position::after(node), make::tokens::single_newline())); + mods.push((Position::after(node), PrettifyWsKind::Newline)); } continue; } _ => continue, }; if token.kind() == SyntaxKind::IDENT && token.text() == "$crate" { - dollar_crate_replacements.push((token.clone(), dollar_crate_replacement(&token))); + if let Some(replacement) = dollar_crate_replacement(&token) { + dollar_crate_replacements.push((token.clone(), replacement)); + } } let tok = &token; @@ -129,8 +134,16 @@ pub fn prettify_macro_expansion( last = Some(tok.kind()); } + inspect_mods(&mods); for (pos, insert) in mods { - ted::insert(pos, insert); + ted::insert_raw( + pos, + match insert { + PrettifyWsKind::Space => make::tokens::single_space(), + PrettifyWsKind::Indent(indent) => make::tokens::whitespace(&" ".repeat(4 * indent)), + PrettifyWsKind::Newline => make::tokens::single_newline(), + }, + ); } for (old, new) in dollar_crate_replacements { ted::replace(old, new); diff --git a/src/tools/rust-analyzer/crates/syntax/src/ted.rs b/src/tools/rust-analyzer/crates/syntax/src/ted.rs index 64d5ea084c1b0..6fcbdd006c244 100644 --- a/src/tools/rust-analyzer/crates/syntax/src/ted.rs +++ b/src/tools/rust-analyzer/crates/syntax/src/ted.rs @@ -5,6 +5,7 @@ use std::{mem, ops::RangeInclusive}; use parser::T; +use rowan::TextSize; use crate::{ SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, @@ -74,6 +75,12 @@ impl Position { }; Position { repr } } + pub fn offset(&self) -> TextSize { + match &self.repr { + PositionRepr::FirstChild(node) => node.text_range().start(), + PositionRepr::After(elem) => elem.text_range().end(), + } + } } pub fn insert(position: Position, elem: impl Element) { @@ -207,5 +214,12 @@ fn ws_between(left: &SyntaxElement, right: &SyntaxElement) -> Option Option { + match self.0 { + VfsPathRepr::PathBuf(it) => Some(it), + VfsPathRepr::VirtualPath(_) => None, + } + } + /// Creates a new `VfsPath` with `path` adjoined to `self`. pub fn join(&self, path: &str) -> Option { match &self.0 { diff --git a/src/tools/rust-analyzer/xtask/src/codegen.rs b/src/tools/rust-analyzer/xtask/src/codegen.rs index bba7ad73f3895..19ca62e8a3290 100644 --- a/src/tools/rust-analyzer/xtask/src/codegen.rs +++ b/src/tools/rust-analyzer/xtask/src/codegen.rs @@ -24,8 +24,8 @@ impl flags::Codegen { grammar::generate(self.check); assists_doc_tests::generate(self.check); parser_inline_tests::generate(self.check); - feature_docs::generate(self.check) - // diagnostics_docs::generate(self.check) doesn't generate any tests + feature_docs::generate(self.check); + diagnostics_docs::generate(self.check); // lints::generate(self.check) Updating clones the rust repo, so don't run it unless // explicitly asked for }