diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 12:02:58 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 12:02:58 +0000 |
commit | 698f8c2f01ea549d77d7dc3338a12e04c11057b9 (patch) | |
tree | 173a775858bd501c378080a10dca74132f05bc50 /src/tools/rust-analyzer/crates/ide/src | |
parent | Initial commit. (diff) | |
download | rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.tar.xz rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.zip |
Adding upstream version 1.64.0+dfsg1.upstream/1.64.0+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/tools/rust-analyzer/crates/ide/src')
73 files changed, 37032 insertions, 0 deletions
diff --git a/src/tools/rust-analyzer/crates/ide/src/annotations.rs b/src/tools/rust-analyzer/crates/ide/src/annotations.rs new file mode 100644 index 000000000..210c5c7fa --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/annotations.rs @@ -0,0 +1,789 @@ +use hir::{HasSource, InFile, Semantics}; +use ide_db::{ + base_db::{FileId, FilePosition, FileRange}, + defs::Definition, + helpers::visit_file_defs, + RootDatabase, +}; +use syntax::{ast::HasName, AstNode, TextRange}; + +use crate::{ + fn_references::find_all_methods, + goto_implementation::goto_implementation, + references::find_all_refs, + runnables::{runnables, Runnable}, + NavigationTarget, RunnableKind, +}; + +// Feature: Annotations +// +// Provides user with annotations above items for looking up references or impl blocks +// and running/debugging binaries. +// +// image::https://user-images.githubusercontent.com/48062697/113020672-b7c34f00-917a-11eb-8f6e-858735660a0e.png[] +#[derive(Debug)] +pub struct Annotation { + pub range: TextRange, + pub kind: AnnotationKind, +} + +#[derive(Debug)] +pub enum AnnotationKind { + Runnable(Runnable), + HasImpls { file_id: FileId, data: Option<Vec<NavigationTarget>> }, + HasReferences { file_id: FileId, data: Option<Vec<FileRange>> }, +} + +pub struct AnnotationConfig { + pub binary_target: bool, + pub annotate_runnables: bool, + pub annotate_impls: bool, + pub annotate_references: bool, + pub annotate_method_references: bool, + pub annotate_enum_variant_references: bool, +} + +pub(crate) fn annotations( + db: &RootDatabase, + config: &AnnotationConfig, + file_id: FileId, +) -> Vec<Annotation> { + let mut annotations = Vec::default(); + + if config.annotate_runnables { + for runnable in runnables(db, file_id) { + if should_skip_runnable(&runnable.kind, config.binary_target) { + continue; + } + + let range = runnable.nav.focus_or_full_range(); + + annotations.push(Annotation { range, kind: AnnotationKind::Runnable(runnable) }); + } + } + + visit_file_defs(&Semantics::new(db), file_id, &mut |def| { + let range = match def { + Definition::Const(konst) if config.annotate_references => { + konst.source(db).and_then(|node| name_range(db, node, file_id)) + } + Definition::Trait(trait_) if config.annotate_references || config.annotate_impls => { + trait_.source(db).and_then(|node| name_range(db, node, file_id)) + } + Definition::Adt(adt) => match adt { + hir::Adt::Enum(enum_) => { + if config.annotate_enum_variant_references { + enum_ + .variants(db) + .into_iter() + .map(|variant| { + variant.source(db).and_then(|node| name_range(db, node, file_id)) + }) + .flatten() + .for_each(|range| { + annotations.push(Annotation { + range, + kind: AnnotationKind::HasReferences { file_id, data: None }, + }) + }) + } + if config.annotate_references || config.annotate_impls { + enum_.source(db).and_then(|node| name_range(db, node, file_id)) + } else { + None + } + } + _ => { + if config.annotate_references || config.annotate_impls { + adt.source(db).and_then(|node| name_range(db, node, file_id)) + } else { + None + } + } + }, + _ => None, + }; + + let range = match range { + Some(range) => range, + None => return, + }; + + if config.annotate_impls && !matches!(def, Definition::Const(_)) { + annotations + .push(Annotation { range, kind: AnnotationKind::HasImpls { file_id, data: None } }); + } + if config.annotate_references { + annotations.push(Annotation { + range, + kind: AnnotationKind::HasReferences { file_id, data: None }, + }); + } + + fn name_range<T: HasName>( + db: &RootDatabase, + node: InFile<T>, + source_file_id: FileId, + ) -> Option<TextRange> { + if let Some(InFile { file_id, value }) = node.original_ast_node(db) { + if file_id == source_file_id.into() { + return value.name().map(|it| it.syntax().text_range()); + } + } + None + } + }); + + if config.annotate_method_references { + annotations.extend(find_all_methods(db, file_id).into_iter().map( + |FileRange { file_id, range }| Annotation { + range, + kind: AnnotationKind::HasReferences { file_id, data: None }, + }, + )); + } + + annotations +} + +pub(crate) fn resolve_annotation(db: &RootDatabase, mut annotation: Annotation) -> Annotation { + match annotation.kind { + AnnotationKind::HasImpls { file_id, ref mut data } => { + *data = + goto_implementation(db, FilePosition { file_id, offset: annotation.range.start() }) + .map(|range| range.info); + } + AnnotationKind::HasReferences { file_id, ref mut data } => { + *data = find_all_refs( + &Semantics::new(db), + FilePosition { file_id, offset: annotation.range.start() }, + None, + ) + .map(|result| { + result + .into_iter() + .flat_map(|res| res.references) + .flat_map(|(file_id, access)| { + access.into_iter().map(move |(range, _)| FileRange { file_id, range }) + }) + .collect() + }); + } + _ => {} + }; + + annotation +} + +fn should_skip_runnable(kind: &RunnableKind, binary_target: bool) -> bool { + match kind { + RunnableKind::Bin => !binary_target, + _ => false, + } +} + +#[cfg(test)] +mod tests { + use expect_test::{expect, Expect}; + + use crate::{fixture, Annotation, AnnotationConfig}; + + fn check(ra_fixture: &str, expect: Expect) { + let (analysis, file_id) = fixture::file(ra_fixture); + + let annotations: Vec<Annotation> = analysis + .annotations( + &AnnotationConfig { + binary_target: true, + annotate_runnables: true, + annotate_impls: true, + annotate_references: true, + annotate_method_references: true, + annotate_enum_variant_references: true, + }, + file_id, + ) + .unwrap() + .into_iter() + .map(|annotation| analysis.resolve_annotation(annotation).unwrap()) + .collect(); + + expect.assert_debug_eq(&annotations); + } + + #[test] + fn const_annotations() { + check( + r#" +const DEMO: i32 = 123; + +const UNUSED: i32 = 123; + +fn main() { + let hello = DEMO; +} + "#, + expect![[r#" + [ + Annotation { + range: 53..57, + kind: Runnable( + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 50..85, + focus_range: 53..57, + name: "main", + kind: Function, + }, + kind: Bin, + cfg: None, + }, + ), + }, + Annotation { + range: 6..10, + kind: HasReferences { + file_id: FileId( + 0, + ), + data: Some( + [ + FileRange { + file_id: FileId( + 0, + ), + range: 78..82, + }, + ], + ), + }, + }, + Annotation { + range: 30..36, + kind: HasReferences { + file_id: FileId( + 0, + ), + data: Some( + [], + ), + }, + }, + Annotation { + range: 53..57, + kind: HasReferences { + file_id: FileId( + 0, + ), + data: Some( + [], + ), + }, + }, + ] + "#]], + ); + } + + #[test] + fn struct_references_annotations() { + check( + r#" +struct Test; + +fn main() { + let test = Test; +} + "#, + expect![[r#" + [ + Annotation { + range: 17..21, + kind: Runnable( + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 14..48, + focus_range: 17..21, + name: "main", + kind: Function, + }, + kind: Bin, + cfg: None, + }, + ), + }, + Annotation { + range: 7..11, + kind: HasImpls { + file_id: FileId( + 0, + ), + data: Some( + [], + ), + }, + }, + Annotation { + range: 7..11, + kind: HasReferences { + file_id: FileId( + 0, + ), + data: Some( + [ + FileRange { + file_id: FileId( + 0, + ), + range: 41..45, + }, + ], + ), + }, + }, + Annotation { + range: 17..21, + kind: HasReferences { + file_id: FileId( + 0, + ), + data: Some( + [], + ), + }, + }, + ] + "#]], + ); + } + + #[test] + fn struct_and_trait_impls_annotations() { + check( + r#" +struct Test; + +trait MyCoolTrait {} + +impl MyCoolTrait for Test {} + +fn main() { + let test = Test; +} + "#, + expect![[r#" + [ + Annotation { + range: 69..73, + kind: Runnable( + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 66..100, + focus_range: 69..73, + name: "main", + kind: Function, + }, + kind: Bin, + cfg: None, + }, + ), + }, + Annotation { + range: 7..11, + kind: HasImpls { + file_id: FileId( + 0, + ), + data: Some( + [ + NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 36..64, + focus_range: 57..61, + name: "impl", + kind: Impl, + }, + ], + ), + }, + }, + Annotation { + range: 7..11, + kind: HasReferences { + file_id: FileId( + 0, + ), + data: Some( + [ + FileRange { + file_id: FileId( + 0, + ), + range: 57..61, + }, + FileRange { + file_id: FileId( + 0, + ), + range: 93..97, + }, + ], + ), + }, + }, + Annotation { + range: 20..31, + kind: HasImpls { + file_id: FileId( + 0, + ), + data: Some( + [ + NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 36..64, + focus_range: 57..61, + name: "impl", + kind: Impl, + }, + ], + ), + }, + }, + Annotation { + range: 20..31, + kind: HasReferences { + file_id: FileId( + 0, + ), + data: Some( + [ + FileRange { + file_id: FileId( + 0, + ), + range: 41..52, + }, + ], + ), + }, + }, + Annotation { + range: 69..73, + kind: HasReferences { + file_id: FileId( + 0, + ), + data: Some( + [], + ), + }, + }, + ] + "#]], + ); + } + + #[test] + fn runnable_annotation() { + check( + r#" +fn main() {} + "#, + expect![[r#" + [ + Annotation { + range: 3..7, + kind: Runnable( + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..12, + focus_range: 3..7, + name: "main", + kind: Function, + }, + kind: Bin, + cfg: None, + }, + ), + }, + Annotation { + range: 3..7, + kind: HasReferences { + file_id: FileId( + 0, + ), + data: Some( + [], + ), + }, + }, + ] + "#]], + ); + } + + #[test] + fn method_annotations() { + check( + r#" +struct Test; + +impl Test { + fn self_by_ref(&self) {} +} + +fn main() { + Test.self_by_ref(); +} + "#, + expect![[r#" + [ + Annotation { + range: 61..65, + kind: Runnable( + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 58..95, + focus_range: 61..65, + name: "main", + kind: Function, + }, + kind: Bin, + cfg: None, + }, + ), + }, + Annotation { + range: 7..11, + kind: HasImpls { + file_id: FileId( + 0, + ), + data: Some( + [ + NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 14..56, + focus_range: 19..23, + name: "impl", + kind: Impl, + }, + ], + ), + }, + }, + Annotation { + range: 7..11, + kind: HasReferences { + file_id: FileId( + 0, + ), + data: Some( + [ + FileRange { + file_id: FileId( + 0, + ), + range: 19..23, + }, + FileRange { + file_id: FileId( + 0, + ), + range: 74..78, + }, + ], + ), + }, + }, + Annotation { + range: 33..44, + kind: HasReferences { + file_id: FileId( + 0, + ), + data: Some( + [ + FileRange { + file_id: FileId( + 0, + ), + range: 79..90, + }, + ], + ), + }, + }, + Annotation { + range: 61..65, + kind: HasReferences { + file_id: FileId( + 0, + ), + data: Some( + [], + ), + }, + }, + ] + "#]], + ); + } + + #[test] + fn test_annotations() { + check( + r#" +fn main() {} + +mod tests { + #[test] + fn my_cool_test() {} +} + "#, + expect![[r#" + [ + Annotation { + range: 3..7, + kind: Runnable( + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..12, + focus_range: 3..7, + name: "main", + kind: Function, + }, + kind: Bin, + cfg: None, + }, + ), + }, + Annotation { + range: 18..23, + kind: Runnable( + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 14..64, + focus_range: 18..23, + name: "tests", + kind: Module, + description: "mod tests", + }, + kind: TestMod { + path: "tests", + }, + cfg: None, + }, + ), + }, + Annotation { + range: 45..57, + kind: Runnable( + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 30..62, + focus_range: 45..57, + name: "my_cool_test", + kind: Function, + }, + kind: Test { + test_id: Path( + "tests::my_cool_test", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + ), + }, + Annotation { + range: 3..7, + kind: HasReferences { + file_id: FileId( + 0, + ), + data: Some( + [], + ), + }, + }, + ] + "#]], + ); + } + + #[test] + fn test_no_annotations_outside_module_tree() { + check( + r#" +//- /foo.rs +struct Foo; +//- /lib.rs +// this file comes last since `check` checks the first file only +"#, + expect![[r#" + [] + "#]], + ); + } + + #[test] + fn test_no_annotations_macro_struct_def() { + check( + r#" +//- /lib.rs +macro_rules! m { + () => { + struct A {} + }; +} + +m!(); +"#, + expect![[r#" + [] + "#]], + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/call_hierarchy.rs b/src/tools/rust-analyzer/crates/ide/src/call_hierarchy.rs new file mode 100644 index 000000000..a18a6bea9 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/call_hierarchy.rs @@ -0,0 +1,460 @@ +//! Entry point for call-hierarchy + +use hir::Semantics; +use ide_db::{ + defs::{Definition, NameClass, NameRefClass}, + helpers::pick_best_token, + search::FileReference, + FxIndexMap, RootDatabase, +}; +use syntax::{ast, AstNode, SyntaxKind::NAME, TextRange}; + +use crate::{goto_definition, FilePosition, NavigationTarget, RangeInfo, TryToNav}; + +#[derive(Debug, Clone)] +pub struct CallItem { + pub target: NavigationTarget, + pub ranges: Vec<TextRange>, +} + +impl CallItem { + #[cfg(test)] + pub(crate) fn debug_render(&self) -> String { + format!("{} : {:?}", self.target.debug_render(), self.ranges) + } +} + +pub(crate) fn call_hierarchy( + db: &RootDatabase, + position: FilePosition, +) -> Option<RangeInfo<Vec<NavigationTarget>>> { + goto_definition::goto_definition(db, position) +} + +pub(crate) fn incoming_calls( + db: &RootDatabase, + FilePosition { file_id, offset }: FilePosition, +) -> Option<Vec<CallItem>> { + let sema = &Semantics::new(db); + + let file = sema.parse(file_id); + let file = file.syntax(); + let mut calls = CallLocations::default(); + + let references = sema + .find_nodes_at_offset_with_descend(file, offset) + .filter_map(move |node| match node { + ast::NameLike::NameRef(name_ref) => match NameRefClass::classify(sema, &name_ref)? { + NameRefClass::Definition(def @ Definition::Function(_)) => Some(def), + _ => None, + }, + ast::NameLike::Name(name) => match NameClass::classify(sema, &name)? { + NameClass::Definition(def @ Definition::Function(_)) => Some(def), + _ => None, + }, + ast::NameLike::Lifetime(_) => None, + }) + .flat_map(|func| func.usages(sema).all()); + + for (_, references) in references { + let references = references.into_iter().map(|FileReference { name, .. }| name); + for name in references { + // This target is the containing function + let nav = sema.ancestors_with_macros(name.syntax().clone()).find_map(|node| { + let def = ast::Fn::cast(node).and_then(|fn_| sema.to_def(&fn_))?; + def.try_to_nav(sema.db) + }); + if let Some(nav) = nav { + calls.add(nav, sema.original_range(name.syntax()).range); + } + } + } + + Some(calls.into_items()) +} + +pub(crate) fn outgoing_calls(db: &RootDatabase, position: FilePosition) -> Option<Vec<CallItem>> { + let sema = Semantics::new(db); + let file_id = position.file_id; + let file = sema.parse(file_id); + let file = file.syntax(); + let token = pick_best_token(file.token_at_offset(position.offset), |kind| match kind { + NAME => 1, + _ => 0, + })?; + let mut calls = CallLocations::default(); + + sema.descend_into_macros(token) + .into_iter() + .filter_map(|it| it.parent_ancestors().nth(1).and_then(ast::Item::cast)) + .filter_map(|item| match item { + ast::Item::Const(c) => c.body().map(|it| it.syntax().descendants()), + ast::Item::Fn(f) => f.body().map(|it| it.syntax().descendants()), + ast::Item::Static(s) => s.body().map(|it| it.syntax().descendants()), + _ => None, + }) + .flatten() + .filter_map(ast::CallableExpr::cast) + .filter_map(|call_node| { + let (nav_target, range) = match call_node { + ast::CallableExpr::Call(call) => { + let expr = call.expr()?; + let callable = sema.type_of_expr(&expr)?.original.as_callable(db)?; + match callable.kind() { + hir::CallableKind::Function(it) => { + let range = expr.syntax().text_range(); + it.try_to_nav(db).zip(Some(range)) + } + _ => None, + } + } + ast::CallableExpr::MethodCall(expr) => { + let range = expr.name_ref()?.syntax().text_range(); + let function = sema.resolve_method_call(&expr)?; + function.try_to_nav(db).zip(Some(range)) + } + }?; + Some((nav_target, range)) + }) + .for_each(|(nav, range)| calls.add(nav, range)); + + Some(calls.into_items()) +} + +#[derive(Default)] +struct CallLocations { + funcs: FxIndexMap<NavigationTarget, Vec<TextRange>>, +} + +impl CallLocations { + fn add(&mut self, target: NavigationTarget, range: TextRange) { + self.funcs.entry(target).or_default().push(range); + } + + fn into_items(self) -> Vec<CallItem> { + self.funcs.into_iter().map(|(target, ranges)| CallItem { target, ranges }).collect() + } +} + +#[cfg(test)] +mod tests { + use expect_test::{expect, Expect}; + use ide_db::base_db::FilePosition; + use itertools::Itertools; + + use crate::fixture; + + fn check_hierarchy( + ra_fixture: &str, + expected: Expect, + expected_incoming: Expect, + expected_outgoing: Expect, + ) { + let (analysis, pos) = fixture::position(ra_fixture); + + let mut navs = analysis.call_hierarchy(pos).unwrap().unwrap().info; + assert_eq!(navs.len(), 1); + let nav = navs.pop().unwrap(); + expected.assert_eq(&nav.debug_render()); + + let item_pos = + FilePosition { file_id: nav.file_id, offset: nav.focus_or_full_range().start() }; + let incoming_calls = analysis.incoming_calls(item_pos).unwrap().unwrap(); + expected_incoming + .assert_eq(&incoming_calls.into_iter().map(|call| call.debug_render()).join("\n")); + + let outgoing_calls = analysis.outgoing_calls(item_pos).unwrap().unwrap(); + expected_outgoing + .assert_eq(&outgoing_calls.into_iter().map(|call| call.debug_render()).join("\n")); + } + + #[test] + fn test_call_hierarchy_on_ref() { + check_hierarchy( + r#" +//- /lib.rs +fn callee() {} +fn caller() { + call$0ee(); +} +"#, + expect![["callee Function FileId(0) 0..14 3..9"]], + expect![["caller Function FileId(0) 15..44 18..24 : [33..39]"]], + expect![[]], + ); + } + + #[test] + fn test_call_hierarchy_on_def() { + check_hierarchy( + r#" +//- /lib.rs +fn call$0ee() {} +fn caller() { + callee(); +} +"#, + expect![["callee Function FileId(0) 0..14 3..9"]], + expect![["caller Function FileId(0) 15..44 18..24 : [33..39]"]], + expect![[]], + ); + } + + #[test] + fn test_call_hierarchy_in_same_fn() { + check_hierarchy( + r#" +//- /lib.rs +fn callee() {} +fn caller() { + call$0ee(); + callee(); +} +"#, + expect![["callee Function FileId(0) 0..14 3..9"]], + expect![["caller Function FileId(0) 15..58 18..24 : [33..39, 47..53]"]], + expect![[]], + ); + } + + #[test] + fn test_call_hierarchy_in_different_fn() { + check_hierarchy( + r#" +//- /lib.rs +fn callee() {} +fn caller1() { + call$0ee(); +} + +fn caller2() { + callee(); +} +"#, + expect![["callee Function FileId(0) 0..14 3..9"]], + expect![[" + caller1 Function FileId(0) 15..45 18..25 : [34..40] + caller2 Function FileId(0) 47..77 50..57 : [66..72]"]], + expect![[]], + ); + } + + #[test] + fn test_call_hierarchy_in_tests_mod() { + check_hierarchy( + r#" +//- /lib.rs cfg:test +fn callee() {} +fn caller1() { + call$0ee(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_caller() { + callee(); + } +} +"#, + expect![["callee Function FileId(0) 0..14 3..9"]], + expect![[r#" + caller1 Function FileId(0) 15..45 18..25 : [34..40] + test_caller Function FileId(0) 95..149 110..121 : [134..140]"#]], + expect![[]], + ); + } + + #[test] + fn test_call_hierarchy_in_different_files() { + check_hierarchy( + r#" +//- /lib.rs +mod foo; +use foo::callee; + +fn caller() { + call$0ee(); +} + +//- /foo/mod.rs +pub fn callee() {} +"#, + expect![["callee Function FileId(1) 0..18 7..13"]], + expect![["caller Function FileId(0) 27..56 30..36 : [45..51]"]], + expect![[]], + ); + } + + #[test] + fn test_call_hierarchy_outgoing() { + check_hierarchy( + r#" +//- /lib.rs +fn callee() {} +fn call$0er() { + callee(); + callee(); +} +"#, + expect![["caller Function FileId(0) 15..58 18..24"]], + expect![[]], + expect![["callee Function FileId(0) 0..14 3..9 : [33..39, 47..53]"]], + ); + } + + #[test] + fn test_call_hierarchy_outgoing_in_different_files() { + check_hierarchy( + r#" +//- /lib.rs +mod foo; +use foo::callee; + +fn call$0er() { + callee(); +} + +//- /foo/mod.rs +pub fn callee() {} +"#, + expect![["caller Function FileId(0) 27..56 30..36"]], + expect![[]], + expect![["callee Function FileId(1) 0..18 7..13 : [45..51]"]], + ); + } + + #[test] + fn test_call_hierarchy_incoming_outgoing() { + check_hierarchy( + r#" +//- /lib.rs +fn caller1() { + call$0er2(); +} + +fn caller2() { + caller3(); +} + +fn caller3() { + +} +"#, + expect![["caller2 Function FileId(0) 33..64 36..43"]], + expect![["caller1 Function FileId(0) 0..31 3..10 : [19..26]"]], + expect![["caller3 Function FileId(0) 66..83 69..76 : [52..59]"]], + ); + } + + #[test] + fn test_call_hierarchy_issue_5103() { + check_hierarchy( + r#" +fn a() { + b() +} + +fn b() {} + +fn main() { + a$0() +} +"#, + expect![["a Function FileId(0) 0..18 3..4"]], + expect![["main Function FileId(0) 31..52 34..38 : [47..48]"]], + expect![["b Function FileId(0) 20..29 23..24 : [13..14]"]], + ); + + check_hierarchy( + r#" +fn a() { + b$0() +} + +fn b() {} + +fn main() { + a() +} +"#, + expect![["b Function FileId(0) 20..29 23..24"]], + expect![["a Function FileId(0) 0..18 3..4 : [13..14]"]], + expect![[]], + ); + } + + #[test] + fn test_call_hierarchy_in_macros_incoming() { + check_hierarchy( + r#" +macro_rules! define { + ($ident:ident) => { + fn $ident {} + } +} +macro_rules! call { + ($ident:ident) => { + $ident() + } +} +define!(callee) +fn caller() { + call!(call$0ee); +} +"#, + expect![[r#"callee Function FileId(0) 144..159 152..158"#]], + expect![[r#"caller Function FileId(0) 160..194 163..169 : [184..190]"#]], + expect![[]], + ); + check_hierarchy( + r#" +macro_rules! define { + ($ident:ident) => { + fn $ident {} + } +} +macro_rules! call { + ($ident:ident) => { + $ident() + } +} +define!(cal$0lee) +fn caller() { + call!(callee); +} +"#, + expect![[r#"callee Function FileId(0) 144..159 152..158"#]], + expect![[r#"caller Function FileId(0) 160..194 163..169 : [184..190]"#]], + expect![[]], + ); + } + + #[test] + fn test_call_hierarchy_in_macros_outgoing() { + check_hierarchy( + r#" +macro_rules! define { + ($ident:ident) => { + fn $ident {} + } +} +macro_rules! call { + ($ident:ident) => { + $ident() + } +} +define!(callee) +fn caller$0() { + call!(callee); +} +"#, + expect![[r#"caller Function FileId(0) 160..194 163..169"#]], + expect![[]], + // FIXME + expect![[]], + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/doc_links.rs b/src/tools/rust-analyzer/crates/ide/src/doc_links.rs new file mode 100644 index 000000000..582e9fe7e --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/doc_links.rs @@ -0,0 +1,549 @@ +//! Extracts, resolves and rewrites links and intra-doc links in markdown documentation. + +#[cfg(test)] +mod tests; + +mod intra_doc_links; + +use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag}; +use pulldown_cmark_to_cmark::{cmark_resume_with_options, Options as CMarkOptions}; +use stdx::format_to; +use url::Url; + +use hir::{db::HirDatabase, Adt, AsAssocItem, AssocItem, AssocItemContainer, HasAttrs}; +use ide_db::{ + base_db::{CrateOrigin, LangCrateOrigin, SourceDatabase}, + defs::{Definition, NameClass, NameRefClass}, + helpers::pick_best_token, + RootDatabase, +}; +use syntax::{ + ast::{self, IsString}, + match_ast, AstNode, AstToken, + SyntaxKind::*, + SyntaxNode, SyntaxToken, TextRange, TextSize, T, +}; + +use crate::{ + doc_links::intra_doc_links::{parse_intra_doc_link, strip_prefixes_suffixes}, + FilePosition, Semantics, +}; + +/// Weblink to an item's documentation. +pub(crate) type DocumentationLink = String; + +const MARKDOWN_OPTIONS: Options = + Options::ENABLE_FOOTNOTES.union(Options::ENABLE_TABLES).union(Options::ENABLE_TASKLISTS); + +/// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs) +pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: Definition) -> String { + let mut cb = broken_link_clone_cb; + let doc = Parser::new_with_broken_link_callback(markdown, MARKDOWN_OPTIONS, Some(&mut cb)); + + let doc = map_links(doc, |target, title| { + // This check is imperfect, there's some overlap between valid intra-doc links + // and valid URLs so we choose to be too eager to try to resolve what might be + // a URL. + if target.contains("://") { + (Some(LinkType::Inline), target.to_string(), title.to_string()) + } else { + // Two possibilities: + // * path-based links: `../../module/struct.MyStruct.html` + // * module-based links (AKA intra-doc links): `super::super::module::MyStruct` + if let Some((target, title)) = rewrite_intra_doc_link(db, definition, target, title) { + return (None, target, title); + } + if let Some(target) = rewrite_url_link(db, definition, target) { + return (Some(LinkType::Inline), target, title.to_string()); + } + + (None, target.to_string(), title.to_string()) + } + }); + let mut out = String::new(); + cmark_resume_with_options( + doc, + &mut out, + None, + CMarkOptions { code_block_token_count: 3, ..Default::default() }, + ) + .ok(); + out +} + +/// Remove all links in markdown documentation. +pub(crate) fn remove_links(markdown: &str) -> String { + let mut drop_link = false; + + let mut cb = |_: BrokenLink<'_>| { + let empty = InlineStr::try_from("").unwrap(); + Some((CowStr::Inlined(empty), CowStr::Inlined(empty))) + }; + let doc = Parser::new_with_broken_link_callback(markdown, MARKDOWN_OPTIONS, Some(&mut cb)); + let doc = doc.filter_map(move |evt| match evt { + Event::Start(Tag::Link(link_type, target, title)) => { + if link_type == LinkType::Inline && target.contains("://") { + Some(Event::Start(Tag::Link(link_type, target, title))) + } else { + drop_link = true; + None + } + } + Event::End(_) if drop_link => { + drop_link = false; + None + } + _ => Some(evt), + }); + + let mut out = String::new(); + cmark_resume_with_options( + doc, + &mut out, + None, + CMarkOptions { code_block_token_count: 3, ..Default::default() }, + ) + .ok(); + out +} + +/// Retrieve a link to documentation for the given symbol. +pub(crate) fn external_docs( + db: &RootDatabase, + position: &FilePosition, +) -> Option<DocumentationLink> { + let sema = &Semantics::new(db); + let file = sema.parse(position.file_id).syntax().clone(); + let token = pick_best_token(file.token_at_offset(position.offset), |kind| match kind { + IDENT | INT_NUMBER | T![self] => 3, + T!['('] | T![')'] => 2, + kind if kind.is_trivia() => 0, + _ => 1, + })?; + let token = sema.descend_into_macros_single(token); + + let node = token.parent()?; + let definition = match_ast! { + match node { + ast::NameRef(name_ref) => match NameRefClass::classify(sema, &name_ref)? { + NameRefClass::Definition(def) => def, + NameRefClass::FieldShorthand { local_ref: _, field_ref } => { + Definition::Field(field_ref) + } + }, + ast::Name(name) => match NameClass::classify(sema, &name)? { + NameClass::Definition(it) | NameClass::ConstReference(it) => it, + NameClass::PatFieldShorthand { local_def: _, field_ref } => Definition::Field(field_ref), + }, + _ => return None, + } + }; + + get_doc_link(db, definition) +} + +/// Extracts all links from a given markdown text returning the definition text range, link-text +/// and the namespace if known. +pub(crate) fn extract_definitions_from_docs( + docs: &hir::Documentation, +) -> Vec<(TextRange, String, Option<hir::Namespace>)> { + Parser::new_with_broken_link_callback( + docs.as_str(), + MARKDOWN_OPTIONS, + Some(&mut broken_link_clone_cb), + ) + .into_offset_iter() + .filter_map(|(event, range)| match event { + Event::Start(Tag::Link(_, target, _)) => { + let (link, ns) = parse_intra_doc_link(&target); + Some(( + TextRange::new(range.start.try_into().ok()?, range.end.try_into().ok()?), + link.to_string(), + ns, + )) + } + _ => None, + }) + .collect() +} + +pub(crate) fn resolve_doc_path_for_def( + db: &dyn HirDatabase, + def: Definition, + link: &str, + ns: Option<hir::Namespace>, +) -> Option<Definition> { + match def { + Definition::Module(it) => it.resolve_doc_path(db, link, ns), + Definition::Function(it) => it.resolve_doc_path(db, link, ns), + Definition::Adt(it) => it.resolve_doc_path(db, link, ns), + Definition::Variant(it) => it.resolve_doc_path(db, link, ns), + Definition::Const(it) => it.resolve_doc_path(db, link, ns), + Definition::Static(it) => it.resolve_doc_path(db, link, ns), + Definition::Trait(it) => it.resolve_doc_path(db, link, ns), + Definition::TypeAlias(it) => it.resolve_doc_path(db, link, ns), + Definition::Macro(it) => it.resolve_doc_path(db, link, ns), + Definition::Field(it) => it.resolve_doc_path(db, link, ns), + Definition::BuiltinAttr(_) + | Definition::ToolModule(_) + | Definition::BuiltinType(_) + | Definition::SelfType(_) + | Definition::Local(_) + | Definition::GenericParam(_) + | Definition::Label(_) + | Definition::DeriveHelper(_) => None, + } + .map(Definition::from) +} + +pub(crate) fn doc_attributes( + sema: &Semantics<'_, RootDatabase>, + node: &SyntaxNode, +) -> Option<(hir::AttrsWithOwner, Definition)> { + match_ast! { + match node { + ast::SourceFile(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Module(def))), + ast::Module(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Module(def))), + ast::Fn(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Function(def))), + ast::Struct(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Adt(hir::Adt::Struct(def)))), + ast::Union(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Adt(hir::Adt::Union(def)))), + ast::Enum(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Adt(hir::Adt::Enum(def)))), + ast::Variant(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Variant(def))), + ast::Trait(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Trait(def))), + ast::Static(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Static(def))), + ast::Const(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Const(def))), + ast::TypeAlias(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::TypeAlias(def))), + ast::Impl(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::SelfType(def))), + ast::RecordField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))), + ast::TupleField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))), + ast::Macro(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Macro(def))), + // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))), + _ => None + } + } +} + +pub(crate) struct DocCommentToken { + doc_token: SyntaxToken, + prefix_len: TextSize, +} + +pub(crate) fn token_as_doc_comment(doc_token: &SyntaxToken) -> Option<DocCommentToken> { + (match_ast! { + match doc_token { + ast::Comment(comment) => TextSize::try_from(comment.prefix().len()).ok(), + ast::String(string) => doc_token.parent_ancestors().find_map(ast::Attr::cast) + .filter(|attr| attr.simple_name().as_deref() == Some("doc")).and_then(|_| string.open_quote_text_range().map(|it| it.len())), + _ => None, + } + }).map(|prefix_len| DocCommentToken { prefix_len, doc_token: doc_token.clone() }) +} + +impl DocCommentToken { + pub(crate) fn get_definition_with_descend_at<T>( + self, + sema: &Semantics<'_, RootDatabase>, + offset: TextSize, + // Definition, CommentOwner, range of intra doc link in original file + mut cb: impl FnMut(Definition, SyntaxNode, TextRange) -> Option<T>, + ) -> Option<T> { + let DocCommentToken { prefix_len, doc_token } = self; + // offset relative to the comments contents + let original_start = doc_token.text_range().start(); + let relative_comment_offset = offset - original_start - prefix_len; + + sema.descend_into_macros(doc_token).into_iter().find_map(|t| { + let (node, descended_prefix_len) = match_ast! { + match t { + ast::Comment(comment) => (t.parent()?, TextSize::try_from(comment.prefix().len()).ok()?), + ast::String(string) => (t.parent_ancestors().skip_while(|n| n.kind() != ATTR).nth(1)?, string.open_quote_text_range()?.len()), + _ => return None, + } + }; + let token_start = t.text_range().start(); + let abs_in_expansion_offset = token_start + relative_comment_offset + descended_prefix_len; + + let (attributes, def) = doc_attributes(sema, &node)?; + let (docs, doc_mapping) = attributes.docs_with_rangemap(sema.db)?; + let (in_expansion_range, link, ns) = + extract_definitions_from_docs(&docs).into_iter().find_map(|(range, link, ns)| { + let mapped = doc_mapping.map(range)?; + (mapped.value.contains(abs_in_expansion_offset)).then(|| (mapped.value, link, ns)) + })?; + // get the relative range to the doc/attribute in the expansion + let in_expansion_relative_range = in_expansion_range - descended_prefix_len - token_start; + // Apply relative range to the original input comment + let absolute_range = in_expansion_relative_range + original_start + prefix_len; + let def = resolve_doc_path_for_def(sema.db, def, &link, ns)?; + cb(def, node, absolute_range) + }) + } +} + +fn broken_link_clone_cb<'a>(link: BrokenLink<'a>) -> Option<(CowStr<'a>, CowStr<'a>)> { + Some((/*url*/ link.reference.clone(), /*title*/ link.reference)) +} + +// FIXME: +// BUG: For Option::Some +// Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some +// Instead of https://doc.rust-lang.org/nightly/core/option/enum.Option.html +// +// This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented +// https://github.com/rust-lang/rfcs/pull/2988 +fn get_doc_link(db: &RootDatabase, def: Definition) -> Option<String> { + let (target, file, frag) = filename_and_frag_for_def(db, def)?; + + let mut url = get_doc_base_url(db, target)?; + + if let Some(path) = mod_path_of_def(db, target) { + url = url.join(&path).ok()?; + } + + url = url.join(&file).ok()?; + url.set_fragment(frag.as_deref()); + + Some(url.into()) +} + +fn rewrite_intra_doc_link( + db: &RootDatabase, + def: Definition, + target: &str, + title: &str, +) -> Option<(String, String)> { + let (link, ns) = parse_intra_doc_link(target); + + let resolved = resolve_doc_path_for_def(db, def, link, ns)?; + let mut url = get_doc_base_url(db, resolved)?; + + let (_, file, frag) = filename_and_frag_for_def(db, resolved)?; + if let Some(path) = mod_path_of_def(db, resolved) { + url = url.join(&path).ok()?; + } + + url = url.join(&file).ok()?; + url.set_fragment(frag.as_deref()); + + Some((url.into(), strip_prefixes_suffixes(title).to_string())) +} + +/// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`). +fn rewrite_url_link(db: &RootDatabase, def: Definition, target: &str) -> Option<String> { + if !(target.contains('#') || target.contains(".html")) { + return None; + } + + let mut url = get_doc_base_url(db, def)?; + let (def, file, frag) = filename_and_frag_for_def(db, def)?; + + if let Some(path) = mod_path_of_def(db, def) { + url = url.join(&path).ok()?; + } + + url = url.join(&file).ok()?; + url.set_fragment(frag.as_deref()); + url.join(target).ok().map(Into::into) +} + +fn mod_path_of_def(db: &RootDatabase, def: Definition) -> Option<String> { + def.canonical_module_path(db).map(|it| { + let mut path = String::new(); + it.flat_map(|it| it.name(db)).for_each(|name| format_to!(path, "{}/", name)); + path + }) +} + +/// Rewrites a markdown document, applying 'callback' to each link. +fn map_links<'e>( + events: impl Iterator<Item = Event<'e>>, + callback: impl Fn(&str, &str) -> (Option<LinkType>, String, String), +) -> impl Iterator<Item = Event<'e>> { + let mut in_link = false; + // holds the origin link target on start event and the rewritten one on end event + let mut end_link_target: Option<CowStr<'_>> = None; + // normally link's type is determined by the type of link tag in the end event, + // however in some cases we want to change the link type, for example, + // `Shortcut` type parsed from Start/End tags doesn't make sense for url links + let mut end_link_type: Option<LinkType> = None; + + events.map(move |evt| match evt { + Event::Start(Tag::Link(link_type, ref target, _)) => { + in_link = true; + end_link_target = Some(target.clone()); + end_link_type = Some(link_type); + evt + } + Event::End(Tag::Link(link_type, target, _)) => { + in_link = false; + Event::End(Tag::Link( + end_link_type.unwrap_or(link_type), + end_link_target.take().unwrap_or(target), + CowStr::Borrowed(""), + )) + } + Event::Text(s) if in_link => { + let (link_type, link_target_s, link_name) = + callback(&end_link_target.take().unwrap(), &s); + end_link_target = Some(CowStr::Boxed(link_target_s.into())); + if !matches!(end_link_type, Some(LinkType::Autolink)) { + end_link_type = link_type; + } + Event::Text(CowStr::Boxed(link_name.into())) + } + Event::Code(s) if in_link => { + let (link_type, link_target_s, link_name) = + callback(&end_link_target.take().unwrap(), &s); + end_link_target = Some(CowStr::Boxed(link_target_s.into())); + if !matches!(end_link_type, Some(LinkType::Autolink)) { + end_link_type = link_type; + } + Event::Code(CowStr::Boxed(link_name.into())) + } + _ => evt, + }) +} + +/// Get the root URL for the documentation of a definition. +/// +/// ```ignore +/// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next +/// ^^^^^^^^^^^^^^^^^^^^^^^^^^ +/// ``` +fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> { + // special case base url of `BuiltinType` to core + // https://github.com/rust-lang/rust-analyzer/issues/12250 + if let Definition::BuiltinType(..) = def { + return Url::parse("https://doc.rust-lang.org/nightly/core/").ok(); + }; + + let krate = def.krate(db)?; + let display_name = krate.display_name(db)?; + + let base = match db.crate_graph()[krate.into()].origin { + // std and co do not specify `html_root_url` any longer so we gotta handwrite this ourself. + // FIXME: Use the toolchains channel instead of nightly + CrateOrigin::Lang( + origin @ (LangCrateOrigin::Alloc + | LangCrateOrigin::Core + | LangCrateOrigin::ProcMacro + | LangCrateOrigin::Std + | LangCrateOrigin::Test), + ) => { + format!("https://doc.rust-lang.org/nightly/{origin}") + } + _ => { + krate.get_html_root_url(db).or_else(|| { + let version = krate.version(db); + // Fallback to docs.rs. This uses `display_name` and can never be + // correct, but that's what fallbacks are about. + // + // FIXME: clicking on the link should just open the file in the editor, + // instead of falling back to external urls. + Some(format!( + "https://docs.rs/{krate}/{version}/", + krate = display_name, + version = version.as_deref().unwrap_or("*") + )) + })? + } + }; + Url::parse(&base).ok()?.join(&format!("{}/", display_name)).ok() +} + +/// Get the filename and extension generated for a symbol by rustdoc. +/// +/// ```ignore +/// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next +/// ^^^^^^^^^^^^^^^^^^^ +/// ``` +fn filename_and_frag_for_def( + db: &dyn HirDatabase, + def: Definition, +) -> Option<(Definition, String, Option<String>)> { + if let Some(assoc_item) = def.as_assoc_item(db) { + let def = match assoc_item.container(db) { + AssocItemContainer::Trait(t) => t.into(), + AssocItemContainer::Impl(i) => i.self_ty(db).as_adt()?.into(), + }; + let (_, file, _) = filename_and_frag_for_def(db, def)?; + let frag = get_assoc_item_fragment(db, assoc_item)?; + return Some((def, file, Some(frag))); + } + + let res = match def { + Definition::Adt(adt) => match adt { + Adt::Struct(s) => format!("struct.{}.html", s.name(db)), + Adt::Enum(e) => format!("enum.{}.html", e.name(db)), + Adt::Union(u) => format!("union.{}.html", u.name(db)), + }, + Definition::Module(m) => match m.name(db) { + // `#[doc(keyword = "...")]` is internal used only by rust compiler + Some(name) => match m.attrs(db).by_key("doc").find_string_value_in_tt("keyword") { + Some(kw) => { + format!("keyword.{}.html", kw.trim_matches('"')) + } + None => format!("{}/index.html", name), + }, + None => String::from("index.html"), + }, + Definition::Trait(t) => format!("trait.{}.html", t.name(db)), + Definition::TypeAlias(t) => format!("type.{}.html", t.name(db)), + Definition::BuiltinType(t) => format!("primitive.{}.html", t.name()), + Definition::Function(f) => format!("fn.{}.html", f.name(db)), + Definition::Variant(ev) => { + format!("enum.{}.html#variant.{}", ev.parent_enum(db).name(db), ev.name(db)) + } + Definition::Const(c) => format!("const.{}.html", c.name(db)?), + Definition::Static(s) => format!("static.{}.html", s.name(db)), + Definition::Macro(mac) => format!("macro.{}.html", mac.name(db)), + Definition::Field(field) => { + let def = match field.parent_def(db) { + hir::VariantDef::Struct(it) => Definition::Adt(it.into()), + hir::VariantDef::Union(it) => Definition::Adt(it.into()), + hir::VariantDef::Variant(it) => Definition::Variant(it), + }; + let (_, file, _) = filename_and_frag_for_def(db, def)?; + return Some((def, file, Some(format!("structfield.{}", field.name(db))))); + } + Definition::SelfType(impl_) => { + let adt = impl_.self_ty(db).as_adt()?.into(); + let (_, file, _) = filename_and_frag_for_def(db, adt)?; + // FIXME fragment numbering + return Some((adt, file, Some(String::from("impl")))); + } + Definition::Local(_) + | Definition::GenericParam(_) + | Definition::Label(_) + | Definition::BuiltinAttr(_) + | Definition::ToolModule(_) + | Definition::DeriveHelper(_) => return None, + }; + + Some((def, res, None)) +} + +/// Get the fragment required to link to a specific field, method, associated type, or associated constant. +/// +/// ```ignore +/// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next +/// ^^^^^^^^^^^^^^ +/// ``` +fn get_assoc_item_fragment(db: &dyn HirDatabase, assoc_item: hir::AssocItem) -> Option<String> { + Some(match assoc_item { + AssocItem::Function(function) => { + let is_trait_method = + function.as_assoc_item(db).and_then(|assoc| assoc.containing_trait(db)).is_some(); + // This distinction may get more complicated when specialization is available. + // Rustdoc makes this decision based on whether a method 'has defaultness'. + // Currently this is only the case for provided trait methods. + if is_trait_method && !function.has_body(db) { + format!("tymethod.{}", function.name(db)) + } else { + format!("method.{}", function.name(db)) + } + } + AssocItem::Const(constant) => format!("associatedconstant.{}", constant.name(db)?), + AssocItem::TypeAlias(ty) => format!("associatedtype.{}", ty.name(db)), + }) +} diff --git a/src/tools/rust-analyzer/crates/ide/src/doc_links/intra_doc_links.rs b/src/tools/rust-analyzer/crates/ide/src/doc_links/intra_doc_links.rs new file mode 100644 index 000000000..1df9aaae2 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/doc_links/intra_doc_links.rs @@ -0,0 +1,77 @@ +//! Helper tools for intra doc links. + +const TYPES: ([&str; 9], [&str; 0]) = + (["type", "struct", "enum", "mod", "trait", "union", "module", "prim", "primitive"], []); +const VALUES: ([&str; 8], [&str; 1]) = + (["value", "function", "fn", "method", "const", "static", "mod", "module"], ["()"]); +const MACROS: ([&str; 2], [&str; 1]) = (["macro", "derive"], ["!"]); + +/// Extract the specified namespace from an intra-doc-link if one exists. +/// +/// # Examples +/// +/// * `struct MyStruct` -> ("MyStruct", `Namespace::Types`) +/// * `panic!` -> ("panic", `Namespace::Macros`) +/// * `fn@from_intra_spec` -> ("from_intra_spec", `Namespace::Values`) +pub(super) fn parse_intra_doc_link(s: &str) -> (&str, Option<hir::Namespace>) { + let s = s.trim_matches('`'); + + [ + (hir::Namespace::Types, (TYPES.0.iter(), TYPES.1.iter())), + (hir::Namespace::Values, (VALUES.0.iter(), VALUES.1.iter())), + (hir::Namespace::Macros, (MACROS.0.iter(), MACROS.1.iter())), + ] + .into_iter() + .find_map(|(ns, (mut prefixes, mut suffixes))| { + if let Some(prefix) = prefixes.find(|&&prefix| { + s.starts_with(prefix) + && s.chars().nth(prefix.len()).map_or(false, |c| c == '@' || c == ' ') + }) { + Some((&s[prefix.len() + 1..], ns)) + } else { + suffixes.find_map(|&suffix| s.strip_suffix(suffix).zip(Some(ns))) + } + }) + .map_or((s, None), |(s, ns)| (s, Some(ns))) +} + +pub(super) fn strip_prefixes_suffixes(s: &str) -> &str { + [ + (TYPES.0.iter(), TYPES.1.iter()), + (VALUES.0.iter(), VALUES.1.iter()), + (MACROS.0.iter(), MACROS.1.iter()), + ] + .into_iter() + .find_map(|(mut prefixes, mut suffixes)| { + if let Some(prefix) = prefixes.find(|&&prefix| { + s.starts_with(prefix) + && s.chars().nth(prefix.len()).map_or(false, |c| c == '@' || c == ' ') + }) { + Some(&s[prefix.len() + 1..]) + } else { + suffixes.find_map(|&suffix| s.strip_suffix(suffix)) + } + }) + .unwrap_or(s) +} + +#[cfg(test)] +mod tests { + use expect_test::{expect, Expect}; + + use super::*; + + fn check(link: &str, expected: Expect) { + let (l, a) = parse_intra_doc_link(link); + let a = a.map_or_else(String::new, |a| format!(" ({:?})", a)); + expected.assert_eq(&format!("{}{}", l, a)); + } + + #[test] + fn test_name() { + check("foo", expect![[r#"foo"#]]); + check("struct Struct", expect![[r#"Struct (Types)"#]]); + check("makro!", expect![[r#"makro (Macros)"#]]); + check("fn@function", expect![[r#"function (Values)"#]]); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/doc_links/tests.rs b/src/tools/rust-analyzer/crates/ide/src/doc_links/tests.rs new file mode 100644 index 000000000..c6bfb6b9d --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/doc_links/tests.rs @@ -0,0 +1,491 @@ +use expect_test::{expect, Expect}; +use hir::{HasAttrs, Semantics}; +use ide_db::{ + base_db::{FilePosition, FileRange}, + defs::Definition, + RootDatabase, +}; +use itertools::Itertools; +use syntax::{ast, match_ast, AstNode, SyntaxNode}; + +use crate::{ + doc_links::{extract_definitions_from_docs, resolve_doc_path_for_def, rewrite_links}, + fixture, TryToNav, +}; + +fn check_external_docs(ra_fixture: &str, expect: Expect) { + let (analysis, position) = fixture::position(ra_fixture); + let url = analysis.external_docs(position).unwrap().expect("could not find url for symbol"); + + expect.assert_eq(&url) +} + +fn check_rewrite(ra_fixture: &str, expect: Expect) { + let (analysis, position) = fixture::position(ra_fixture); + let sema = &Semantics::new(&*analysis.db); + let (cursor_def, docs) = def_under_cursor(sema, &position); + let res = rewrite_links(sema.db, docs.as_str(), cursor_def); + expect.assert_eq(&res) +} + +fn check_doc_links(ra_fixture: &str) { + let key_fn = |&(FileRange { file_id, range }, _): &_| (file_id, range.start()); + + let (analysis, position, mut expected) = fixture::annotations(ra_fixture); + expected.sort_by_key(key_fn); + let sema = &Semantics::new(&*analysis.db); + let (cursor_def, docs) = def_under_cursor(sema, &position); + let defs = extract_definitions_from_docs(&docs); + let actual: Vec<_> = defs + .into_iter() + .map(|(_, link, ns)| { + let def = resolve_doc_path_for_def(sema.db, cursor_def, &link, ns) + .unwrap_or_else(|| panic!("Failed to resolve {}", link)); + let nav_target = def.try_to_nav(sema.db).unwrap(); + let range = + FileRange { file_id: nav_target.file_id, range: nav_target.focus_or_full_range() }; + (range, link) + }) + .sorted_by_key(key_fn) + .collect(); + assert_eq!(expected, actual); +} + +fn def_under_cursor( + sema: &Semantics<'_, RootDatabase>, + position: &FilePosition, +) -> (Definition, hir::Documentation) { + let (docs, def) = sema + .parse(position.file_id) + .syntax() + .token_at_offset(position.offset) + .left_biased() + .unwrap() + .parent_ancestors() + .find_map(|it| node_to_def(sema, &it)) + .expect("no def found") + .unwrap(); + let docs = docs.expect("no docs found for cursor def"); + (def, docs) +} + +fn node_to_def( + sema: &Semantics<'_, RootDatabase>, + node: &SyntaxNode, +) -> Option<Option<(Option<hir::Documentation>, Definition)>> { + Some(match_ast! { + match node { + ast::SourceFile(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Module(def))), + ast::Module(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Module(def))), + ast::Fn(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Function(def))), + ast::Struct(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Adt(hir::Adt::Struct(def)))), + ast::Union(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Adt(hir::Adt::Union(def)))), + ast::Enum(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Adt(hir::Adt::Enum(def)))), + ast::Variant(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Variant(def))), + ast::Trait(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Trait(def))), + ast::Static(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Static(def))), + ast::Const(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Const(def))), + ast::TypeAlias(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::TypeAlias(def))), + ast::Impl(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::SelfType(def))), + ast::RecordField(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Field(def))), + ast::TupleField(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Field(def))), + ast::Macro(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Macro(def))), + // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))), + _ => return None, + } + }) +} + +#[test] +fn external_docs_doc_url_crate() { + check_external_docs( + r#" +//- /main.rs crate:main deps:foo +use foo$0::Foo; +//- /lib.rs crate:foo +pub struct Foo; +"#, + expect![[r#"https://docs.rs/foo/*/foo/index.html"#]], + ); +} + +#[test] +fn external_docs_doc_url_std_crate() { + check_external_docs( + r#" +//- /main.rs crate:std +use self$0; +"#, + expect![[r#"https://doc.rust-lang.org/nightly/std/index.html"#]], + ); +} + +#[test] +fn external_docs_doc_url_struct() { + check_external_docs( + r#" +//- /main.rs crate:foo +pub struct Fo$0o; +"#, + expect![[r#"https://docs.rs/foo/*/foo/struct.Foo.html"#]], + ); +} + +#[test] +fn external_docs_doc_url_struct_field() { + check_external_docs( + r#" +//- /main.rs crate:foo +pub struct Foo { + field$0: () +} +"#, + expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#structfield.field"##]], + ); +} + +#[test] +fn external_docs_doc_url_fn() { + check_external_docs( + r#" +//- /main.rs crate:foo +pub fn fo$0o() {} +"#, + expect![[r#"https://docs.rs/foo/*/foo/fn.foo.html"#]], + ); +} + +#[test] +fn external_docs_doc_url_impl_assoc() { + check_external_docs( + r#" +//- /main.rs crate:foo +pub struct Foo; +impl Foo { + pub fn method$0() {} +} +"#, + expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#method.method"##]], + ); + check_external_docs( + r#" +//- /main.rs crate:foo +pub struct Foo; +impl Foo { + const CONST$0: () = (); +} +"#, + expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#associatedconstant.CONST"##]], + ); +} + +#[test] +fn external_docs_doc_url_impl_trait_assoc() { + check_external_docs( + r#" +//- /main.rs crate:foo +pub struct Foo; +pub trait Trait { + fn method() {} +} +impl Trait for Foo { + pub fn method$0() {} +} +"#, + expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#method.method"##]], + ); + check_external_docs( + r#" +//- /main.rs crate:foo +pub struct Foo; +pub trait Trait { + const CONST: () = (); +} +impl Trait for Foo { + const CONST$0: () = (); +} +"#, + expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#associatedconstant.CONST"##]], + ); + check_external_docs( + r#" +//- /main.rs crate:foo +pub struct Foo; +pub trait Trait { + type Type; +} +impl Trait for Foo { + type Type$0 = (); +} +"#, + expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#associatedtype.Type"##]], + ); +} + +#[test] +fn external_docs_doc_url_trait_assoc() { + check_external_docs( + r#" +//- /main.rs crate:foo +pub trait Foo { + fn method$0(); +} +"#, + expect![[r##"https://docs.rs/foo/*/foo/trait.Foo.html#tymethod.method"##]], + ); + check_external_docs( + r#" +//- /main.rs crate:foo +pub trait Foo { + const CONST$0: (); +} +"#, + expect![[r##"https://docs.rs/foo/*/foo/trait.Foo.html#associatedconstant.CONST"##]], + ); + check_external_docs( + r#" +//- /main.rs crate:foo +pub trait Foo { + type Type$0; +} +"#, + expect![[r##"https://docs.rs/foo/*/foo/trait.Foo.html#associatedtype.Type"##]], + ); +} + +#[test] +fn external_docs_trait() { + check_external_docs( + r#" +//- /main.rs crate:foo +trait Trait$0 {} +"#, + expect![[r#"https://docs.rs/foo/*/foo/trait.Trait.html"#]], + ) +} + +#[test] +fn external_docs_module() { + check_external_docs( + r#" +//- /main.rs crate:foo +pub mod foo { + pub mod ba$0r {} +} +"#, + expect![[r#"https://docs.rs/foo/*/foo/foo/bar/index.html"#]], + ) +} + +#[test] +fn external_docs_reexport_order() { + check_external_docs( + r#" +//- /main.rs crate:foo +pub mod wrapper { + pub use module::Item; + + pub mod module { + pub struct Item; + } +} + +fn foo() { + let bar: wrapper::It$0em; +} + "#, + expect![[r#"https://docs.rs/foo/*/foo/wrapper/module/struct.Item.html"#]], + ) +} + +#[test] +fn doc_links_items_simple() { + check_doc_links( + r#" +//- /main.rs crate:main deps:krate +/// [`krate`] +//! [`Trait`] +//! [`function`] +//! [`CONST`] +//! [`STATIC`] +//! [`Struct`] +//! [`Enum`] +//! [`Union`] +//! [`Type`] +//! [`module`] +use self$0; + +const CONST: () = (); + // ^^^^^ CONST +static STATIC: () = (); + // ^^^^^^ STATIC +trait Trait { + // ^^^^^ Trait +} +fn function() {} +// ^^^^^^^^ function +struct Struct; + // ^^^^^^ Struct +enum Enum {} + // ^^^^ Enum +union Union {__: ()} + // ^^^^^ Union +type Type = (); + // ^^^^ Type +mod module {} + // ^^^^^^ module +//- /krate.rs crate:krate +// empty +//^file krate +"#, + ) +} + +#[test] +fn doc_links_inherent_impl_items() { + check_doc_links( + r#" +// /// [`Struct::CONST`] +// /// [`Struct::function`] +/// FIXME #9694 +struct Struct$0; + +impl Struct { + const CONST: () = (); + fn function() {} +} +"#, + ) +} + +#[test] +fn doc_links_trait_impl_items() { + check_doc_links( + r#" +trait Trait { + type Type; + const CONST: usize; + fn function(); +} +// /// [`Struct::Type`] +// /// [`Struct::CONST`] +// /// [`Struct::function`] +/// FIXME #9694 +struct Struct$0; + +impl Trait for Struct { + type Type = (); + const CONST: () = (); + fn function() {} +} +"#, + ) +} + +#[test] +fn doc_links_trait_items() { + check_doc_links( + r#" +/// [`Trait`] +/// [`Trait::Type`] +/// [`Trait::CONST`] +/// [`Trait::function`] +trait Trait$0 { + // ^^^^^ Trait +type Type; + // ^^^^ Trait::Type +const CONST: usize; + // ^^^^^ Trait::CONST +fn function(); +// ^^^^^^^^ Trait::function +} + "#, + ) +} + +#[test] +fn rewrite_html_root_url() { + check_rewrite( + r#" +//- /main.rs crate:foo +#![doc(arbitrary_attribute = "test", html_root_url = "https:/example.com", arbitrary_attribute2)] + +pub mod foo { + pub struct Foo; +} +/// [Foo](foo::Foo) +pub struct B$0ar +"#, + expect![[r#"[Foo](https://example.com/foo/foo/struct.Foo.html)"#]], + ); +} + +#[test] +fn rewrite_on_field() { + check_rewrite( + r#" +//- /main.rs crate:foo +pub struct Foo { + /// [Foo](struct.Foo.html) + fie$0ld: () +} +"#, + expect![[r#"[Foo](https://docs.rs/foo/*/foo/struct.Foo.html)"#]], + ); +} + +#[test] +fn rewrite_struct() { + check_rewrite( + r#" +//- /main.rs crate:foo +/// [Foo] +pub struct $0Foo; +"#, + expect![[r#"[Foo](https://docs.rs/foo/*/foo/struct.Foo.html)"#]], + ); + check_rewrite( + r#" +//- /main.rs crate:foo +/// [`Foo`] +pub struct $0Foo; +"#, + expect![[r#"[`Foo`](https://docs.rs/foo/*/foo/struct.Foo.html)"#]], + ); + check_rewrite( + r#" +//- /main.rs crate:foo +/// [Foo](struct.Foo.html) +pub struct $0Foo; +"#, + expect![[r#"[Foo](https://docs.rs/foo/*/foo/struct.Foo.html)"#]], + ); + check_rewrite( + r#" +//- /main.rs crate:foo +/// [struct Foo](struct.Foo.html) +pub struct $0Foo; +"#, + expect![[r#"[struct Foo](https://docs.rs/foo/*/foo/struct.Foo.html)"#]], + ); + check_rewrite( + r#" +//- /main.rs crate:foo +/// [my Foo][foo] +/// +/// [foo]: Foo +pub struct $0Foo; +"#, + expect![[r#"[my Foo](https://docs.rs/foo/*/foo/struct.Foo.html)"#]], + ); + check_rewrite( + r#" +//- /main.rs crate:foo +/// [`foo`] +/// +/// [`foo`]: Foo +pub struct $0Foo; +"#, + expect![["[`foo`]"]], + ); +} diff --git a/src/tools/rust-analyzer/crates/ide/src/expand_macro.rs b/src/tools/rust-analyzer/crates/ide/src/expand_macro.rs new file mode 100644 index 000000000..efa8551a0 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/expand_macro.rs @@ -0,0 +1,521 @@ +use hir::Semantics; +use ide_db::{ + base_db::FileId, helpers::pick_best_token, + syntax_helpers::insert_whitespace_into_node::insert_ws_into, RootDatabase, +}; +use syntax::{ast, ted, AstNode, NodeOrToken, SyntaxKind, SyntaxNode, T}; + +use crate::FilePosition; + +pub struct ExpandedMacro { + pub name: String, + pub expansion: String, +} + +// Feature: Expand Macro Recursively +// +// Shows the full macro expansion of the macro at current cursor. +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Expand macro recursively** +// |=== +// +// image::https://user-images.githubusercontent.com/48062697/113020648-b3973180-917a-11eb-84a9-ecb921293dc5.gif[] +pub(crate) fn expand_macro(db: &RootDatabase, position: FilePosition) -> Option<ExpandedMacro> { + let sema = Semantics::new(db); + let file = sema.parse(position.file_id); + + let tok = pick_best_token(file.syntax().token_at_offset(position.offset), |kind| match kind { + SyntaxKind::IDENT => 1, + _ => 0, + })?; + + // due to how Rust Analyzer works internally, we need to special case derive attributes, + // otherwise they might not get found, e.g. here with the cursor at $0 `#[attr]` would expand: + // ``` + // #[attr] + // #[derive($0Foo)] + // struct Bar; + // ``` + + let derive = sema.descend_into_macros(tok.clone()).into_iter().find_map(|descended| { + let hir_file = sema.hir_file_for(&descended.parent()?); + if !hir_file.is_derive_attr_pseudo_expansion(db) { + return None; + } + + let name = descended.parent_ancestors().filter_map(ast::Path::cast).last()?.to_string(); + // up map out of the #[derive] expansion + let token = hir::InFile::new(hir_file, descended).upmap(db)?.value; + let attr = token.parent_ancestors().find_map(ast::Attr::cast)?; + let expansions = sema.expand_derive_macro(&attr)?; + let idx = attr + .token_tree()? + .token_trees_and_tokens() + .filter_map(NodeOrToken::into_token) + .take_while(|it| it != &token) + .filter(|it| it.kind() == T![,]) + .count(); + let expansion = + format(db, SyntaxKind::MACRO_ITEMS, position.file_id, expansions.get(idx).cloned()?); + Some(ExpandedMacro { name, expansion }) + }); + + if derive.is_some() { + return derive; + } + + // FIXME: Intermix attribute and bang! expansions + // currently we only recursively expand one of the two types + let mut anc = tok.parent_ancestors(); + let (name, expanded, kind) = loop { + let node = anc.next()?; + + if let Some(item) = ast::Item::cast(node.clone()) { + if let Some(def) = sema.resolve_attr_macro_call(&item) { + break ( + def.name(db).to_string(), + expand_attr_macro_recur(&sema, &item)?, + SyntaxKind::MACRO_ITEMS, + ); + } + } + if let Some(mac) = ast::MacroCall::cast(node) { + break ( + mac.path()?.segment()?.name_ref()?.to_string(), + expand_macro_recur(&sema, &mac)?, + mac.syntax().parent().map(|it| it.kind()).unwrap_or(SyntaxKind::MACRO_ITEMS), + ); + } + }; + + // FIXME: + // macro expansion may lose all white space information + // But we hope someday we can use ra_fmt for that + let expansion = format(db, kind, position.file_id, expanded); + + Some(ExpandedMacro { name, expansion }) +} + +fn expand_macro_recur( + sema: &Semantics<'_, RootDatabase>, + macro_call: &ast::MacroCall, +) -> Option<SyntaxNode> { + let expanded = sema.expand(macro_call)?.clone_for_update(); + expand(sema, expanded, ast::MacroCall::cast, expand_macro_recur) +} + +fn expand_attr_macro_recur( + sema: &Semantics<'_, RootDatabase>, + item: &ast::Item, +) -> Option<SyntaxNode> { + let expanded = sema.expand_attr_macro(item)?.clone_for_update(); + expand(sema, expanded, ast::Item::cast, expand_attr_macro_recur) +} + +fn expand<T: AstNode>( + sema: &Semantics<'_, RootDatabase>, + expanded: SyntaxNode, + f: impl FnMut(SyntaxNode) -> Option<T>, + exp: impl Fn(&Semantics<'_, RootDatabase>, &T) -> Option<SyntaxNode>, +) -> Option<SyntaxNode> { + let children = expanded.descendants().filter_map(f); + let mut replacements = Vec::new(); + + for child in children { + if let Some(new_node) = exp(sema, &child) { + // check if the whole original syntax is replaced + if expanded == *child.syntax() { + return Some(new_node); + } + replacements.push((child, new_node)); + } + } + + replacements.into_iter().rev().for_each(|(old, new)| ted::replace(old.syntax(), new)); + Some(expanded) +} + +fn format(db: &RootDatabase, kind: SyntaxKind, file_id: FileId, expanded: SyntaxNode) -> String { + let expansion = insert_ws_into(expanded).to_string(); + + _format(db, kind, file_id, &expansion).unwrap_or(expansion) +} + +#[cfg(any(test, target_arch = "wasm32", target_os = "emscripten"))] +fn _format( + _db: &RootDatabase, + _kind: SyntaxKind, + _file_id: FileId, + _expansion: &str, +) -> Option<String> { + None +} + +#[cfg(not(any(test, target_arch = "wasm32", target_os = "emscripten")))] +fn _format( + db: &RootDatabase, + kind: SyntaxKind, + file_id: FileId, + expansion: &str, +) -> Option<String> { + use ide_db::base_db::{FileLoader, SourceDatabase}; + // hack until we get hygiene working (same character amount to preserve formatting as much as possible) + const DOLLAR_CRATE_REPLACE: &str = &"__r_a_"; + let expansion = expansion.replace("$crate", DOLLAR_CRATE_REPLACE); + let (prefix, suffix) = match kind { + SyntaxKind::MACRO_PAT => ("fn __(", ": u32);"), + SyntaxKind::MACRO_EXPR | SyntaxKind::MACRO_STMTS => ("fn __() {", "}"), + SyntaxKind::MACRO_TYPE => ("type __ =", ";"), + _ => ("", ""), + }; + let expansion = format!("{prefix}{expansion}{suffix}"); + + let &crate_id = db.relevant_crates(file_id).iter().next()?; + let edition = db.crate_graph()[crate_id].edition; + + let mut cmd = std::process::Command::new(toolchain::rustfmt()); + cmd.arg("--edition"); + cmd.arg(edition.to_string()); + + let mut rustfmt = cmd + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .ok()?; + + std::io::Write::write_all(&mut rustfmt.stdin.as_mut()?, expansion.as_bytes()).ok()?; + + let output = rustfmt.wait_with_output().ok()?; + let captured_stdout = String::from_utf8(output.stdout).ok()?; + + if output.status.success() && !captured_stdout.trim().is_empty() { + let output = captured_stdout.replace(DOLLAR_CRATE_REPLACE, "$crate"); + let output = output.trim().strip_prefix(prefix)?; + let output = match kind { + SyntaxKind::MACRO_PAT => { + output.strip_suffix(suffix).or_else(|| output.strip_suffix(": u32,\n);"))? + } + _ => output.strip_suffix(suffix)?, + }; + let trim_indent = stdx::trim_indent(output); + tracing::debug!("expand_macro: formatting succeeded"); + Some(trim_indent) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use expect_test::{expect, Expect}; + + use crate::fixture; + + #[track_caller] + fn check(ra_fixture: &str, expect: Expect) { + let (analysis, pos) = fixture::position(ra_fixture); + let expansion = analysis.expand_macro(pos).unwrap().unwrap(); + let actual = format!("{}\n{}", expansion.name, expansion.expansion); + expect.assert_eq(&actual); + } + + #[test] + fn macro_expand_as_keyword() { + check( + r#" +macro_rules! bar { + ($i:tt) => { $i as _ } +} +fn main() { + let x: u64 = ba$0r!(5i64); +} +"#, + expect![[r#" + bar + 5i64 as _"#]], + ); + } + + #[test] + fn macro_expand_underscore() { + check( + r#" +macro_rules! bar { + ($i:tt) => { for _ in 0..$i {} } +} +fn main() { + ba$0r!(42); +} +"#, + expect![[r#" + bar + for _ in 0..42{}"#]], + ); + } + + #[test] + fn macro_expand_recursive_expansion() { + check( + r#" +macro_rules! bar { + () => { fn b() {} } +} +macro_rules! foo { + () => { bar!(); } +} +macro_rules! baz { + () => { foo!(); } +} +f$0oo!(); +"#, + expect![[r#" + foo + fn b(){} + "#]], + ); + } + + #[test] + fn macro_expand_multiple_lines() { + check( + r#" +macro_rules! foo { + () => { + fn some_thing() -> u32 { + let a = 0; + a + 10 + } + } +} +f$0oo!(); + "#, + expect![[r#" + foo + fn some_thing() -> u32 { + let a = 0; + a+10 + }"#]], + ); + } + + #[test] + fn macro_expand_match_ast() { + check( + r#" +macro_rules! match_ast { + (match $node:ident { $($tt:tt)* }) => { match_ast!(match ($node) { $($tt)* }) }; + (match ($node:expr) { + $( ast::$ast:ident($it:ident) => $res:block, )* + _ => $catch_all:expr $(,)? + }) => {{ + $( if let Some($it) = ast::$ast::cast($node.clone()) $res else )* + { $catch_all } + }}; +} + +fn main() { + mat$0ch_ast! { + match container { + ast::TraitDef(it) => {}, + ast::ImplDef(it) => {}, + _ => { continue }, + } + } +} +"#, + expect![[r#" + match_ast + { + if let Some(it) = ast::TraitDef::cast(container.clone()){} + else if let Some(it) = ast::ImplDef::cast(container.clone()){} + else { + { + continue + } + } + }"#]], + ); + } + + #[test] + fn macro_expand_match_ast_inside_let_statement() { + check( + r#" +macro_rules! match_ast { + (match $node:ident { $($tt:tt)* }) => { match_ast!(match ($node) { $($tt)* }) }; + (match ($node:expr) {}) => {{}}; +} + +fn main() { + let p = f(|it| { + let res = mat$0ch_ast! { match c {}}; + Some(res) + })?; +} +"#, + expect![[r#" + match_ast + {}"#]], + ); + } + + #[test] + fn macro_expand_inner_macro_rules() { + check( + r#" +macro_rules! foo { + ($t:tt) => {{ + macro_rules! bar { + () => { + $t + } + } + bar!() + }}; +} + +fn main() { + foo$0!(42); +} + "#, + expect![[r#" + foo + { + macro_rules! bar { + () => { + 42 + } + } + 42 + }"#]], + ); + } + + #[test] + fn macro_expand_inner_macro_fail_to_expand() { + check( + r#" +macro_rules! bar { + (BAD) => {}; +} +macro_rules! foo { + () => {bar!()}; +} + +fn main() { + let res = fo$0o!(); +} +"#, + expect![[r#" + foo + "#]], + ); + } + + #[test] + fn macro_expand_with_dollar_crate() { + check( + r#" +#[macro_export] +macro_rules! bar { + () => {0}; +} +macro_rules! foo { + () => {$crate::bar!()}; +} + +fn main() { + let res = fo$0o!(); +} +"#, + expect![[r#" + foo + 0"#]], + ); + } + + #[test] + fn macro_expand_with_dyn_absolute_path() { + check( + r#" +macro_rules! foo { + () => {fn f<T>(_: &dyn ::std::marker::Copy) {}}; +} + +fn main() { + let res = fo$0o!(); +} +"#, + expect![[r#" + foo + fn f<T>(_: &dyn ::std::marker::Copy){}"#]], + ); + } + + #[test] + fn macro_expand_derive() { + check( + r#" +//- proc_macros: identity +//- minicore: clone, derive + +#[proc_macros::identity] +#[derive(C$0lone)] +struct Foo {} +"#, + expect![[r#" + Clone + impl < >core::clone::Clone for Foo< >{} + "#]], + ); + } + + #[test] + fn macro_expand_derive2() { + check( + r#" +//- minicore: copy, clone, derive + +#[derive(Cop$0y)] +#[derive(Clone)] +struct Foo {} +"#, + expect![[r#" + Copy + impl < >core::marker::Copy for Foo< >{} + "#]], + ); + } + + #[test] + fn macro_expand_derive_multi() { + check( + r#" +//- minicore: copy, clone, derive + +#[derive(Cop$0y, Clone)] +struct Foo {} +"#, + expect![[r#" + Copy + impl < >core::marker::Copy for Foo< >{} + "#]], + ); + check( + r#" +//- minicore: copy, clone, derive + +#[derive(Copy, Cl$0one)] +struct Foo {} +"#, + expect![[r#" + Clone + impl < >core::clone::Clone for Foo< >{} + "#]], + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/extend_selection.rs b/src/tools/rust-analyzer/crates/ide/src/extend_selection.rs new file mode 100644 index 000000000..45f1fd748 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/extend_selection.rs @@ -0,0 +1,662 @@ +use std::iter::successors; + +use hir::Semantics; +use ide_db::RootDatabase; +use syntax::{ + algo::{self, skip_trivia_token}, + ast::{self, AstNode, AstToken}, + Direction, NodeOrToken, + SyntaxKind::{self, *}, + SyntaxNode, SyntaxToken, TextRange, TextSize, TokenAtOffset, T, +}; + +use crate::FileRange; + +// Feature: Expand and Shrink Selection +// +// Extends or shrinks the current selection to the encompassing syntactic construct +// (expression, statement, item, module, etc). It works with multiple cursors. +// +// This is a standard LSP feature and not a protocol extension. +// +// |=== +// | Editor | Shortcut +// +// | VS Code | kbd:[Alt+Shift+→], kbd:[Alt+Shift+←] +// |=== +// +// image::https://user-images.githubusercontent.com/48062697/113020651-b42fc800-917a-11eb-8a4f-cf1a07859fac.gif[] +pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRange { + let sema = Semantics::new(db); + let src = sema.parse(frange.file_id); + try_extend_selection(&sema, src.syntax(), frange).unwrap_or(frange.range) +} + +fn try_extend_selection( + sema: &Semantics<'_, RootDatabase>, + root: &SyntaxNode, + frange: FileRange, +) -> Option<TextRange> { + let range = frange.range; + + let string_kinds = [COMMENT, STRING, BYTE_STRING]; + let list_kinds = [ + RECORD_PAT_FIELD_LIST, + MATCH_ARM_LIST, + RECORD_FIELD_LIST, + TUPLE_FIELD_LIST, + RECORD_EXPR_FIELD_LIST, + VARIANT_LIST, + USE_TREE_LIST, + GENERIC_PARAM_LIST, + GENERIC_ARG_LIST, + TYPE_BOUND_LIST, + PARAM_LIST, + ARG_LIST, + ARRAY_EXPR, + TUPLE_EXPR, + TUPLE_TYPE, + TUPLE_PAT, + WHERE_CLAUSE, + ]; + + if range.is_empty() { + let offset = range.start(); + let mut leaves = root.token_at_offset(offset); + if leaves.clone().all(|it| it.kind() == WHITESPACE) { + return Some(extend_ws(root, leaves.next()?, offset)); + } + let leaf_range = match leaves { + TokenAtOffset::None => return None, + TokenAtOffset::Single(l) => { + if string_kinds.contains(&l.kind()) { + extend_single_word_in_comment_or_string(&l, offset) + .unwrap_or_else(|| l.text_range()) + } else { + l.text_range() + } + } + TokenAtOffset::Between(l, r) => pick_best(l, r).text_range(), + }; + return Some(leaf_range); + }; + let node = match root.covering_element(range) { + NodeOrToken::Token(token) => { + if token.text_range() != range { + return Some(token.text_range()); + } + if let Some(comment) = ast::Comment::cast(token.clone()) { + if let Some(range) = extend_comments(comment) { + return Some(range); + } + } + token.parent()? + } + NodeOrToken::Node(node) => node, + }; + + // if we are in single token_tree, we maybe live in macro or attr + if node.kind() == TOKEN_TREE { + if let Some(macro_call) = node.ancestors().find_map(ast::MacroCall::cast) { + if let Some(range) = extend_tokens_from_range(sema, macro_call, range) { + return Some(range); + } + } + } + + if node.text_range() != range { + return Some(node.text_range()); + } + + let node = shallowest_node(&node); + + if node.parent().map(|n| list_kinds.contains(&n.kind())) == Some(true) { + if let Some(range) = extend_list_item(&node) { + return Some(range); + } + } + + node.parent().map(|it| it.text_range()) +} + +fn extend_tokens_from_range( + sema: &Semantics<'_, RootDatabase>, + macro_call: ast::MacroCall, + original_range: TextRange, +) -> Option<TextRange> { + let src = macro_call.syntax().covering_element(original_range); + let (first_token, last_token) = match src { + NodeOrToken::Node(it) => (it.first_token()?, it.last_token()?), + NodeOrToken::Token(it) => (it.clone(), it), + }; + + let mut first_token = skip_trivia_token(first_token, Direction::Next)?; + let mut last_token = skip_trivia_token(last_token, Direction::Prev)?; + + while !original_range.contains_range(first_token.text_range()) { + first_token = skip_trivia_token(first_token.next_token()?, Direction::Next)?; + } + while !original_range.contains_range(last_token.text_range()) { + last_token = skip_trivia_token(last_token.prev_token()?, Direction::Prev)?; + } + + // compute original mapped token range + let extended = { + let fst_expanded = sema.descend_into_macros_single(first_token.clone()); + let lst_expanded = sema.descend_into_macros_single(last_token.clone()); + let mut lca = + algo::least_common_ancestor(&fst_expanded.parent()?, &lst_expanded.parent()?)?; + lca = shallowest_node(&lca); + if lca.first_token() == Some(fst_expanded) && lca.last_token() == Some(lst_expanded) { + lca = lca.parent()?; + } + lca + }; + + // Compute parent node range + let validate = |token: &SyntaxToken| -> bool { + let expanded = sema.descend_into_macros_single(token.clone()); + let parent = match expanded.parent() { + Some(it) => it, + None => return false, + }; + algo::least_common_ancestor(&extended, &parent).as_ref() == Some(&extended) + }; + + // Find the first and last text range under expanded parent + let first = successors(Some(first_token), |token| { + let token = token.prev_token()?; + skip_trivia_token(token, Direction::Prev) + }) + .take_while(validate) + .last()?; + + let last = successors(Some(last_token), |token| { + let token = token.next_token()?; + skip_trivia_token(token, Direction::Next) + }) + .take_while(validate) + .last()?; + + let range = first.text_range().cover(last.text_range()); + if range.contains_range(original_range) && original_range != range { + Some(range) + } else { + None + } +} + +/// Find the shallowest node with same range, which allows us to traverse siblings. +fn shallowest_node(node: &SyntaxNode) -> SyntaxNode { + node.ancestors().take_while(|n| n.text_range() == node.text_range()).last().unwrap() +} + +fn extend_single_word_in_comment_or_string( + leaf: &SyntaxToken, + offset: TextSize, +) -> Option<TextRange> { + let text: &str = leaf.text(); + let cursor_position: u32 = (offset - leaf.text_range().start()).into(); + + let (before, after) = text.split_at(cursor_position as usize); + + fn non_word_char(c: char) -> bool { + !(c.is_alphanumeric() || c == '_') + } + + let start_idx = before.rfind(non_word_char)? as u32; + let end_idx = after.find(non_word_char).unwrap_or_else(|| after.len()) as u32; + + let from: TextSize = (start_idx + 1).into(); + let to: TextSize = (cursor_position + end_idx).into(); + + let range = TextRange::new(from, to); + if range.is_empty() { + None + } else { + Some(range + leaf.text_range().start()) + } +} + +fn extend_ws(root: &SyntaxNode, ws: SyntaxToken, offset: TextSize) -> TextRange { + let ws_text = ws.text(); + let suffix = TextRange::new(offset, ws.text_range().end()) - ws.text_range().start(); + let prefix = TextRange::new(ws.text_range().start(), offset) - ws.text_range().start(); + let ws_suffix = &ws_text[suffix]; + let ws_prefix = &ws_text[prefix]; + if ws_text.contains('\n') && !ws_suffix.contains('\n') { + if let Some(node) = ws.next_sibling_or_token() { + let start = match ws_prefix.rfind('\n') { + Some(idx) => ws.text_range().start() + TextSize::from((idx + 1) as u32), + None => node.text_range().start(), + }; + let end = if root.text().char_at(node.text_range().end()) == Some('\n') { + node.text_range().end() + TextSize::of('\n') + } else { + node.text_range().end() + }; + return TextRange::new(start, end); + } + } + ws.text_range() +} + +fn pick_best(l: SyntaxToken, r: SyntaxToken) -> SyntaxToken { + return if priority(&r) > priority(&l) { r } else { l }; + fn priority(n: &SyntaxToken) -> usize { + match n.kind() { + WHITESPACE => 0, + IDENT | T![self] | T![super] | T![crate] | T![Self] | LIFETIME_IDENT => 2, + _ => 1, + } + } +} + +/// Extend list item selection to include nearby delimiter and whitespace. +fn extend_list_item(node: &SyntaxNode) -> Option<TextRange> { + fn is_single_line_ws(node: &SyntaxToken) -> bool { + node.kind() == WHITESPACE && !node.text().contains('\n') + } + + fn nearby_delimiter( + delimiter_kind: SyntaxKind, + node: &SyntaxNode, + dir: Direction, + ) -> Option<SyntaxToken> { + node.siblings_with_tokens(dir) + .skip(1) + .find(|node| match node { + NodeOrToken::Node(_) => true, + NodeOrToken::Token(it) => !is_single_line_ws(it), + }) + .and_then(|it| it.into_token()) + .filter(|node| node.kind() == delimiter_kind) + } + + let delimiter = match node.kind() { + TYPE_BOUND => T![+], + _ => T![,], + }; + + if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Next) { + // Include any following whitespace when delimiter is after list item. + let final_node = delimiter_node + .next_sibling_or_token() + .and_then(|it| it.into_token()) + .filter(is_single_line_ws) + .unwrap_or(delimiter_node); + + return Some(TextRange::new(node.text_range().start(), final_node.text_range().end())); + } + if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Prev) { + return Some(TextRange::new(delimiter_node.text_range().start(), node.text_range().end())); + } + + None +} + +fn extend_comments(comment: ast::Comment) -> Option<TextRange> { + let prev = adj_comments(&comment, Direction::Prev); + let next = adj_comments(&comment, Direction::Next); + if prev != next { + Some(TextRange::new(prev.syntax().text_range().start(), next.syntax().text_range().end())) + } else { + None + } +} + +fn adj_comments(comment: &ast::Comment, dir: Direction) -> ast::Comment { + let mut res = comment.clone(); + for element in comment.syntax().siblings_with_tokens(dir) { + let token = match element.as_token() { + None => break, + Some(token) => token, + }; + if let Some(c) = ast::Comment::cast(token.clone()) { + res = c + } else if token.kind() != WHITESPACE || token.text().contains("\n\n") { + break; + } + } + res +} + +#[cfg(test)] +mod tests { + use crate::fixture; + + use super::*; + + fn do_check(before: &str, afters: &[&str]) { + let (analysis, position) = fixture::position(before); + let before = analysis.file_text(position.file_id).unwrap(); + let range = TextRange::empty(position.offset); + let mut frange = FileRange { file_id: position.file_id, range }; + + for &after in afters { + frange.range = analysis.extend_selection(frange).unwrap(); + let actual = &before[frange.range]; + assert_eq!(after, actual); + } + } + + #[test] + fn test_extend_selection_arith() { + do_check(r#"fn foo() { $01 + 1 }"#, &["1", "1 + 1", "{ 1 + 1 }"]); + } + + #[test] + fn test_extend_selection_list() { + do_check(r#"fn foo($0x: i32) {}"#, &["x", "x: i32"]); + do_check(r#"fn foo($0x: i32, y: i32) {}"#, &["x", "x: i32", "x: i32, "]); + do_check(r#"fn foo($0x: i32,y: i32) {}"#, &["x", "x: i32", "x: i32,", "(x: i32,y: i32)"]); + do_check(r#"fn foo(x: i32, $0y: i32) {}"#, &["y", "y: i32", ", y: i32"]); + do_check(r#"fn foo(x: i32, $0y: i32, ) {}"#, &["y", "y: i32", "y: i32, "]); + do_check(r#"fn foo(x: i32,$0y: i32) {}"#, &["y", "y: i32", ",y: i32"]); + + do_check(r#"const FOO: [usize; 2] = [ 22$0 , 33];"#, &["22", "22 , "]); + do_check(r#"const FOO: [usize; 2] = [ 22 , 33$0];"#, &["33", ", 33"]); + do_check(r#"const FOO: [usize; 2] = [ 22 , 33$0 ,];"#, &["33", "33 ,", "[ 22 , 33 ,]"]); + + do_check(r#"fn main() { (1, 2$0) }"#, &["2", ", 2", "(1, 2)"]); + + do_check( + r#" +const FOO: [usize; 2] = [ + 22, + $033, +]"#, + &["33", "33,"], + ); + + do_check( + r#" +const FOO: [usize; 2] = [ + 22 + , 33$0, +]"#, + &["33", "33,"], + ); + } + + #[test] + fn test_extend_selection_start_of_the_line() { + do_check( + r#" +impl S { +$0 fn foo() { + + } +}"#, + &[" fn foo() {\n\n }\n"], + ); + } + + #[test] + fn test_extend_selection_doc_comments() { + do_check( + r#" +struct A; + +/// bla +/// bla +struct B { + $0 +} + "#, + &["\n \n", "{\n \n}", "/// bla\n/// bla\nstruct B {\n \n}"], + ) + } + + #[test] + fn test_extend_selection_comments() { + do_check( + r#" +fn bar(){} + +// fn foo() { +// 1 + $01 +// } + +// fn foo(){} + "#, + &["1", "// 1 + 1", "// fn foo() {\n// 1 + 1\n// }"], + ); + + do_check( + r#" +// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +// pub enum Direction { +// $0 Next, +// Prev +// } +"#, + &[ + "// Next,", + "// #[derive(Debug, Clone, Copy, PartialEq, Eq)]\n// pub enum Direction {\n// Next,\n// Prev\n// }", + ], + ); + + do_check( + r#" +/* +foo +_bar1$0*/ +"#, + &["_bar1", "/*\nfoo\n_bar1*/"], + ); + + do_check(r#"//!$0foo_2 bar"#, &["foo_2", "//!foo_2 bar"]); + + do_check(r#"/$0/foo bar"#, &["//foo bar"]); + } + + #[test] + fn test_extend_selection_prefer_idents() { + do_check( + r#" +fn main() { foo$0+bar;} +"#, + &["foo", "foo+bar"], + ); + do_check( + r#" +fn main() { foo+$0bar;} +"#, + &["bar", "foo+bar"], + ); + } + + #[test] + fn test_extend_selection_prefer_lifetimes() { + do_check(r#"fn foo<$0'a>() {}"#, &["'a", "<'a>"]); + do_check(r#"fn foo<'a$0>() {}"#, &["'a", "<'a>"]); + } + + #[test] + fn test_extend_selection_select_first_word() { + do_check(r#"// foo bar b$0az quxx"#, &["baz", "// foo bar baz quxx"]); + do_check( + r#" +impl S { +fn foo() { +// hel$0lo world +} +} +"#, + &["hello", "// hello world"], + ); + } + + #[test] + fn test_extend_selection_string() { + do_check( + r#" +fn bar(){} + +" fn f$0oo() {" +"#, + &["foo", "\" fn foo() {\""], + ); + } + + #[test] + fn test_extend_trait_bounds_list_in_where_clause() { + do_check( + r#" +fn foo<R>() + where + R: req::Request + 'static, + R::Params: DeserializeOwned$0 + panic::UnwindSafe + 'static, + R::Result: Serialize + 'static, +"#, + &[ + "DeserializeOwned", + "DeserializeOwned + ", + "DeserializeOwned + panic::UnwindSafe + 'static", + "R::Params: DeserializeOwned + panic::UnwindSafe + 'static", + "R::Params: DeserializeOwned + panic::UnwindSafe + 'static,", + ], + ); + do_check(r#"fn foo<T>() where T: $0Copy"#, &["Copy"]); + do_check(r#"fn foo<T>() where T: $0Copy + Display"#, &["Copy", "Copy + "]); + do_check(r#"fn foo<T>() where T: $0Copy +Display"#, &["Copy", "Copy +"]); + do_check(r#"fn foo<T>() where T: $0Copy+Display"#, &["Copy", "Copy+"]); + do_check(r#"fn foo<T>() where T: Copy + $0Display"#, &["Display", "+ Display"]); + do_check(r#"fn foo<T>() where T: Copy + $0Display + Sync"#, &["Display", "Display + "]); + do_check(r#"fn foo<T>() where T: Copy +$0Display"#, &["Display", "+Display"]); + } + + #[test] + fn test_extend_trait_bounds_list_inline() { + do_check(r#"fn foo<T: $0Copy>() {}"#, &["Copy"]); + do_check(r#"fn foo<T: $0Copy + Display>() {}"#, &["Copy", "Copy + "]); + do_check(r#"fn foo<T: $0Copy +Display>() {}"#, &["Copy", "Copy +"]); + do_check(r#"fn foo<T: $0Copy+Display>() {}"#, &["Copy", "Copy+"]); + do_check(r#"fn foo<T: Copy + $0Display>() {}"#, &["Display", "+ Display"]); + do_check(r#"fn foo<T: Copy + $0Display + Sync>() {}"#, &["Display", "Display + "]); + do_check(r#"fn foo<T: Copy +$0Display>() {}"#, &["Display", "+Display"]); + do_check( + r#"fn foo<T: Copy$0 + Display, U: Copy>() {}"#, + &[ + "Copy", + "Copy + ", + "Copy + Display", + "T: Copy + Display", + "T: Copy + Display, ", + "<T: Copy + Display, U: Copy>", + ], + ); + } + + #[test] + fn test_extend_selection_on_tuple_in_type() { + do_check( + r#"fn main() { let _: (krate, $0_crate_def_map, module_id) = (); }"#, + &["_crate_def_map", "_crate_def_map, ", "(krate, _crate_def_map, module_id)"], + ); + // white space variations + do_check( + r#"fn main() { let _: (krate,$0_crate_def_map,module_id) = (); }"#, + &["_crate_def_map", "_crate_def_map,", "(krate,_crate_def_map,module_id)"], + ); + do_check( + r#" +fn main() { let _: ( + krate, + _crate$0_def_map, + module_id +) = (); }"#, + &[ + "_crate_def_map", + "_crate_def_map,", + "(\n krate,\n _crate_def_map,\n module_id\n)", + ], + ); + } + + #[test] + fn test_extend_selection_on_tuple_in_rvalue() { + do_check( + r#"fn main() { let var = (krate, _crate_def_map$0, module_id); }"#, + &["_crate_def_map", "_crate_def_map, ", "(krate, _crate_def_map, module_id)"], + ); + // white space variations + do_check( + r#"fn main() { let var = (krate,_crate$0_def_map,module_id); }"#, + &["_crate_def_map", "_crate_def_map,", "(krate,_crate_def_map,module_id)"], + ); + do_check( + r#" +fn main() { let var = ( + krate, + _crate_def_map$0, + module_id +); }"#, + &[ + "_crate_def_map", + "_crate_def_map,", + "(\n krate,\n _crate_def_map,\n module_id\n)", + ], + ); + } + + #[test] + fn test_extend_selection_on_tuple_pat() { + do_check( + r#"fn main() { let (krate, _crate_def_map$0, module_id) = var; }"#, + &["_crate_def_map", "_crate_def_map, ", "(krate, _crate_def_map, module_id)"], + ); + // white space variations + do_check( + r#"fn main() { let (krate,_crate$0_def_map,module_id) = var; }"#, + &["_crate_def_map", "_crate_def_map,", "(krate,_crate_def_map,module_id)"], + ); + do_check( + r#" +fn main() { let ( + krate, + _crate_def_map$0, + module_id +) = var; }"#, + &[ + "_crate_def_map", + "_crate_def_map,", + "(\n krate,\n _crate_def_map,\n module_id\n)", + ], + ); + } + + #[test] + fn extend_selection_inside_macros() { + do_check( + r#"macro_rules! foo { ($item:item) => {$item} } + foo!{fn hello(na$0me:usize){}}"#, + &[ + "name", + "name:usize", + "(name:usize)", + "fn hello(name:usize){}", + "{fn hello(name:usize){}}", + "foo!{fn hello(name:usize){}}", + ], + ); + } + + #[test] + fn extend_selection_inside_recur_macros() { + do_check( + r#" macro_rules! foo2 { ($item:item) => {$item} } + macro_rules! foo { ($item:item) => {foo2!($item);} } + foo!{fn hello(na$0me:usize){}}"#, + &[ + "name", + "name:usize", + "(name:usize)", + "fn hello(name:usize){}", + "{fn hello(name:usize){}}", + "foo!{fn hello(name:usize){}}", + ], + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/file_structure.rs b/src/tools/rust-analyzer/crates/ide/src/file_structure.rs new file mode 100644 index 000000000..68fd0952b --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/file_structure.rs @@ -0,0 +1,579 @@ +use ide_db::SymbolKind; +use syntax::{ + ast::{self, HasAttrs, HasGenericParams, HasName}, + match_ast, AstNode, AstToken, NodeOrToken, SourceFile, SyntaxNode, SyntaxToken, TextRange, + WalkEvent, +}; + +#[derive(Debug, Clone)] +pub struct StructureNode { + pub parent: Option<usize>, + pub label: String, + pub navigation_range: TextRange, + pub node_range: TextRange, + pub kind: StructureNodeKind, + pub detail: Option<String>, + pub deprecated: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum StructureNodeKind { + SymbolKind(SymbolKind), + Region, +} + +// Feature: File Structure +// +// Provides a tree of the symbols defined in the file. Can be used to +// +// * fuzzy search symbol in a file (super useful) +// * draw breadcrumbs to describe the context around the cursor +// * draw outline of the file +// +// |=== +// | Editor | Shortcut +// +// | VS Code | kbd:[Ctrl+Shift+O] +// |=== +// +// image::https://user-images.githubusercontent.com/48062697/113020654-b42fc800-917a-11eb-8388-e7dc4d92b02e.gif[] + +pub(crate) fn file_structure(file: &SourceFile) -> Vec<StructureNode> { + let mut res = Vec::new(); + let mut stack = Vec::new(); + + for event in file.syntax().preorder_with_tokens() { + match event { + WalkEvent::Enter(NodeOrToken::Node(node)) => { + if let Some(mut symbol) = structure_node(&node) { + symbol.parent = stack.last().copied(); + stack.push(res.len()); + res.push(symbol); + } + } + WalkEvent::Leave(NodeOrToken::Node(node)) => { + if structure_node(&node).is_some() { + stack.pop().unwrap(); + } + } + WalkEvent::Enter(NodeOrToken::Token(token)) => { + if let Some(mut symbol) = structure_token(token) { + symbol.parent = stack.last().copied(); + stack.push(res.len()); + res.push(symbol); + } + } + WalkEvent::Leave(NodeOrToken::Token(token)) => { + if structure_token(token).is_some() { + stack.pop().unwrap(); + } + } + } + } + res +} + +fn structure_node(node: &SyntaxNode) -> Option<StructureNode> { + fn decl<N: HasName + HasAttrs>(node: N, kind: StructureNodeKind) -> Option<StructureNode> { + decl_with_detail(&node, None, kind) + } + + fn decl_with_type_ref<N: HasName + HasAttrs>( + node: &N, + type_ref: Option<ast::Type>, + kind: StructureNodeKind, + ) -> Option<StructureNode> { + let detail = type_ref.map(|type_ref| { + let mut detail = String::new(); + collapse_ws(type_ref.syntax(), &mut detail); + detail + }); + decl_with_detail(node, detail, kind) + } + + fn decl_with_detail<N: HasName + HasAttrs>( + node: &N, + detail: Option<String>, + kind: StructureNodeKind, + ) -> Option<StructureNode> { + let name = node.name()?; + + Some(StructureNode { + parent: None, + label: name.text().to_string(), + navigation_range: name.syntax().text_range(), + node_range: node.syntax().text_range(), + kind, + detail, + deprecated: node.attrs().filter_map(|x| x.simple_name()).any(|x| x == "deprecated"), + }) + } + + fn collapse_ws(node: &SyntaxNode, output: &mut String) { + let mut can_insert_ws = false; + node.text().for_each_chunk(|chunk| { + for line in chunk.lines() { + let line = line.trim(); + if line.is_empty() { + if can_insert_ws { + output.push(' '); + can_insert_ws = false; + } + } else { + output.push_str(line); + can_insert_ws = true; + } + } + }) + } + + match_ast! { + match node { + ast::Fn(it) => { + let mut detail = String::from("fn"); + if let Some(type_param_list) = it.generic_param_list() { + collapse_ws(type_param_list.syntax(), &mut detail); + } + if let Some(param_list) = it.param_list() { + collapse_ws(param_list.syntax(), &mut detail); + } + if let Some(ret_type) = it.ret_type() { + detail.push(' '); + collapse_ws(ret_type.syntax(), &mut detail); + } + + decl_with_detail(&it, Some(detail), StructureNodeKind::SymbolKind(SymbolKind::Function)) + }, + ast::Struct(it) => decl(it, StructureNodeKind::SymbolKind(SymbolKind::Struct)), + ast::Union(it) => decl(it, StructureNodeKind::SymbolKind(SymbolKind::Union)), + ast::Enum(it) => decl(it, StructureNodeKind::SymbolKind(SymbolKind::Enum)), + ast::Variant(it) => decl(it, StructureNodeKind::SymbolKind(SymbolKind::Variant)), + ast::Trait(it) => decl(it, StructureNodeKind::SymbolKind(SymbolKind::Trait)), + ast::Module(it) => decl(it, StructureNodeKind::SymbolKind(SymbolKind::Module)), + ast::TypeAlias(it) => decl_with_type_ref(&it, it.ty(), StructureNodeKind::SymbolKind(SymbolKind::TypeAlias)), + ast::RecordField(it) => decl_with_type_ref(&it, it.ty(), StructureNodeKind::SymbolKind(SymbolKind::Field)), + ast::Const(it) => decl_with_type_ref(&it, it.ty(), StructureNodeKind::SymbolKind(SymbolKind::Const)), + ast::Static(it) => decl_with_type_ref(&it, it.ty(), StructureNodeKind::SymbolKind(SymbolKind::Static)), + ast::Impl(it) => { + let target_type = it.self_ty()?; + let target_trait = it.trait_(); + let label = match target_trait { + None => format!("impl {}", target_type.syntax().text()), + Some(t) => { + format!("impl {} for {}", t.syntax().text(), target_type.syntax().text(),) + } + }; + + let node = StructureNode { + parent: None, + label, + navigation_range: target_type.syntax().text_range(), + node_range: it.syntax().text_range(), + kind: StructureNodeKind::SymbolKind(SymbolKind::Impl), + detail: None, + deprecated: false, + }; + Some(node) + }, + ast::Macro(it) => decl(it, StructureNodeKind::SymbolKind(SymbolKind::Macro)), + _ => None, + } + } +} + +fn structure_token(token: SyntaxToken) -> Option<StructureNode> { + if let Some(comment) = ast::Comment::cast(token) { + let text = comment.text().trim(); + + if let Some(region_name) = text.strip_prefix("// region:").map(str::trim) { + return Some(StructureNode { + parent: None, + label: region_name.to_string(), + navigation_range: comment.syntax().text_range(), + node_range: comment.syntax().text_range(), + kind: StructureNodeKind::Region, + detail: None, + deprecated: false, + }); + } + } + + None +} + +#[cfg(test)] +mod tests { + use expect_test::{expect, Expect}; + + use super::*; + + fn check(ra_fixture: &str, expect: Expect) { + let file = SourceFile::parse(ra_fixture).ok().unwrap(); + let structure = file_structure(&file); + expect.assert_debug_eq(&structure) + } + + #[test] + fn test_file_structure() { + check( + r#" +struct Foo { + x: i32 +} + +mod m { + fn bar1() {} + fn bar2<T>(t: T) -> T {} + fn bar3<A, + B>(a: A, + b: B) -> Vec< + u32 + > {} +} + +enum E { X, Y(i32) } +type T = (); +static S: i32 = 92; +const C: i32 = 92; + +impl E {} + +impl fmt::Debug for E {} + +macro_rules! mc { + () => {} +} + +#[macro_export] +macro_rules! mcexp { + () => {} +} + +/// Doc comment +macro_rules! mcexp { + () => {} +} + +#[deprecated] +fn obsolete() {} + +#[deprecated(note = "for awhile")] +fn very_obsolete() {} + +// region: Some region name +// endregion + +// region: dontpanic +mod m { +fn f() {} +// endregion +fn g() {} +} +"#, + expect![[r#" + [ + StructureNode { + parent: None, + label: "Foo", + navigation_range: 8..11, + node_range: 1..26, + kind: SymbolKind( + Struct, + ), + detail: None, + deprecated: false, + }, + StructureNode { + parent: Some( + 0, + ), + label: "x", + navigation_range: 18..19, + node_range: 18..24, + kind: SymbolKind( + Field, + ), + detail: Some( + "i32", + ), + deprecated: false, + }, + StructureNode { + parent: None, + label: "m", + navigation_range: 32..33, + node_range: 28..158, + kind: SymbolKind( + Module, + ), + detail: None, + deprecated: false, + }, + StructureNode { + parent: Some( + 2, + ), + label: "bar1", + navigation_range: 43..47, + node_range: 40..52, + kind: SymbolKind( + Function, + ), + detail: Some( + "fn()", + ), + deprecated: false, + }, + StructureNode { + parent: Some( + 2, + ), + label: "bar2", + navigation_range: 60..64, + node_range: 57..81, + kind: SymbolKind( + Function, + ), + detail: Some( + "fn<T>(t: T) -> T", + ), + deprecated: false, + }, + StructureNode { + parent: Some( + 2, + ), + label: "bar3", + navigation_range: 89..93, + node_range: 86..156, + kind: SymbolKind( + Function, + ), + detail: Some( + "fn<A, B>(a: A, b: B) -> Vec< u32 >", + ), + deprecated: false, + }, + StructureNode { + parent: None, + label: "E", + navigation_range: 165..166, + node_range: 160..180, + kind: SymbolKind( + Enum, + ), + detail: None, + deprecated: false, + }, + StructureNode { + parent: Some( + 6, + ), + label: "X", + navigation_range: 169..170, + node_range: 169..170, + kind: SymbolKind( + Variant, + ), + detail: None, + deprecated: false, + }, + StructureNode { + parent: Some( + 6, + ), + label: "Y", + navigation_range: 172..173, + node_range: 172..178, + kind: SymbolKind( + Variant, + ), + detail: None, + deprecated: false, + }, + StructureNode { + parent: None, + label: "T", + navigation_range: 186..187, + node_range: 181..193, + kind: SymbolKind( + TypeAlias, + ), + detail: Some( + "()", + ), + deprecated: false, + }, + StructureNode { + parent: None, + label: "S", + navigation_range: 201..202, + node_range: 194..213, + kind: SymbolKind( + Static, + ), + detail: Some( + "i32", + ), + deprecated: false, + }, + StructureNode { + parent: None, + label: "C", + navigation_range: 220..221, + node_range: 214..232, + kind: SymbolKind( + Const, + ), + detail: Some( + "i32", + ), + deprecated: false, + }, + StructureNode { + parent: None, + label: "impl E", + navigation_range: 239..240, + node_range: 234..243, + kind: SymbolKind( + Impl, + ), + detail: None, + deprecated: false, + }, + StructureNode { + parent: None, + label: "impl fmt::Debug for E", + navigation_range: 265..266, + node_range: 245..269, + kind: SymbolKind( + Impl, + ), + detail: None, + deprecated: false, + }, + StructureNode { + parent: None, + label: "mc", + navigation_range: 284..286, + node_range: 271..303, + kind: SymbolKind( + Macro, + ), + detail: None, + deprecated: false, + }, + StructureNode { + parent: None, + label: "mcexp", + navigation_range: 334..339, + node_range: 305..356, + kind: SymbolKind( + Macro, + ), + detail: None, + deprecated: false, + }, + StructureNode { + parent: None, + label: "mcexp", + navigation_range: 387..392, + node_range: 358..409, + kind: SymbolKind( + Macro, + ), + detail: None, + deprecated: false, + }, + StructureNode { + parent: None, + label: "obsolete", + navigation_range: 428..436, + node_range: 411..441, + kind: SymbolKind( + Function, + ), + detail: Some( + "fn()", + ), + deprecated: true, + }, + StructureNode { + parent: None, + label: "very_obsolete", + navigation_range: 481..494, + node_range: 443..499, + kind: SymbolKind( + Function, + ), + detail: Some( + "fn()", + ), + deprecated: true, + }, + StructureNode { + parent: None, + label: "Some region name", + navigation_range: 501..528, + node_range: 501..528, + kind: Region, + detail: None, + deprecated: false, + }, + StructureNode { + parent: None, + label: "m", + navigation_range: 568..569, + node_range: 543..606, + kind: SymbolKind( + Module, + ), + detail: None, + deprecated: false, + }, + StructureNode { + parent: Some( + 20, + ), + label: "dontpanic", + navigation_range: 543..563, + node_range: 543..563, + kind: Region, + detail: None, + deprecated: false, + }, + StructureNode { + parent: Some( + 20, + ), + label: "f", + navigation_range: 575..576, + node_range: 572..581, + kind: SymbolKind( + Function, + ), + detail: Some( + "fn()", + ), + deprecated: false, + }, + StructureNode { + parent: Some( + 20, + ), + label: "g", + navigation_range: 598..599, + node_range: 582..604, + kind: SymbolKind( + Function, + ), + detail: Some( + "fn()", + ), + deprecated: false, + }, + ] + "#]], + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/fixture.rs b/src/tools/rust-analyzer/crates/ide/src/fixture.rs new file mode 100644 index 000000000..2ea6f6a9a --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/fixture.rs @@ -0,0 +1,87 @@ +//! Utilities for creating `Analysis` instances for tests. +use hir::db::DefDatabase; +use ide_db::base_db::fixture::ChangeFixture; +use test_utils::{extract_annotations, RangeOrOffset}; + +use crate::{Analysis, AnalysisHost, FileId, FilePosition, FileRange}; + +/// Creates analysis for a single file. +pub(crate) fn file(ra_fixture: &str) -> (Analysis, FileId) { + let mut host = AnalysisHost::default(); + let change_fixture = ChangeFixture::parse(ra_fixture); + host.db.set_enable_proc_attr_macros(true); + host.db.apply_change(change_fixture.change); + (host.analysis(), change_fixture.files[0]) +} + +/// Creates analysis from a multi-file fixture, returns positions marked with $0. +pub(crate) fn position(ra_fixture: &str) -> (Analysis, FilePosition) { + let mut host = AnalysisHost::default(); + let change_fixture = ChangeFixture::parse(ra_fixture); + host.db.set_enable_proc_attr_macros(true); + host.db.apply_change(change_fixture.change); + let (file_id, range_or_offset) = change_fixture.file_position.expect("expected a marker ($0)"); + let offset = range_or_offset.expect_offset(); + (host.analysis(), FilePosition { file_id, offset }) +} + +/// Creates analysis for a single file, returns range marked with a pair of $0. +pub(crate) fn range(ra_fixture: &str) -> (Analysis, FileRange) { + let mut host = AnalysisHost::default(); + let change_fixture = ChangeFixture::parse(ra_fixture); + host.db.set_enable_proc_attr_macros(true); + host.db.apply_change(change_fixture.change); + let (file_id, range_or_offset) = change_fixture.file_position.expect("expected a marker ($0)"); + let range = range_or_offset.expect_range(); + (host.analysis(), FileRange { file_id, range }) +} + +/// Creates analysis for a single file, returns range marked with a pair of $0 or a position marked with $0. +pub(crate) fn range_or_position(ra_fixture: &str) -> (Analysis, FileId, RangeOrOffset) { + let mut host = AnalysisHost::default(); + let change_fixture = ChangeFixture::parse(ra_fixture); + host.db.set_enable_proc_attr_macros(true); + host.db.apply_change(change_fixture.change); + let (file_id, range_or_offset) = change_fixture.file_position.expect("expected a marker ($0)"); + (host.analysis(), file_id, range_or_offset) +} + +/// Creates analysis from a multi-file fixture, returns positions marked with $0. +pub(crate) fn annotations(ra_fixture: &str) -> (Analysis, FilePosition, Vec<(FileRange, String)>) { + let mut host = AnalysisHost::default(); + let change_fixture = ChangeFixture::parse(ra_fixture); + host.db.set_enable_proc_attr_macros(true); + host.db.apply_change(change_fixture.change); + let (file_id, range_or_offset) = change_fixture.file_position.expect("expected a marker ($0)"); + let offset = range_or_offset.expect_offset(); + + let annotations = change_fixture + .files + .iter() + .flat_map(|&file_id| { + let file_text = host.analysis().file_text(file_id).unwrap(); + let annotations = extract_annotations(&file_text); + annotations.into_iter().map(move |(range, data)| (FileRange { file_id, range }, data)) + }) + .collect(); + (host.analysis(), FilePosition { file_id, offset }, annotations) +} + +/// Creates analysis from a multi-file fixture with annonations without $0 +pub(crate) fn annotations_without_marker(ra_fixture: &str) -> (Analysis, Vec<(FileRange, String)>) { + let mut host = AnalysisHost::default(); + let change_fixture = ChangeFixture::parse(ra_fixture); + host.db.set_enable_proc_attr_macros(true); + host.db.apply_change(change_fixture.change); + + let annotations = change_fixture + .files + .iter() + .flat_map(|&file_id| { + let file_text = host.analysis().file_text(file_id).unwrap(); + let annotations = extract_annotations(&file_text); + annotations.into_iter().map(move |(range, data)| (FileRange { file_id, range }, data)) + }) + .collect(); + (host.analysis(), annotations) +} diff --git a/src/tools/rust-analyzer/crates/ide/src/fn_references.rs b/src/tools/rust-analyzer/crates/ide/src/fn_references.rs new file mode 100644 index 000000000..63fb322ce --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/fn_references.rs @@ -0,0 +1,94 @@ +//! This module implements a methods and free functions search in the specified file. +//! We have to skip tests, so cannot reuse file_structure module. + +use hir::Semantics; +use ide_assists::utils::test_related_attribute; +use ide_db::RootDatabase; +use syntax::{ast, ast::HasName, AstNode, SyntaxNode}; + +use crate::{FileId, FileRange}; + +pub(crate) fn find_all_methods(db: &RootDatabase, file_id: FileId) -> Vec<FileRange> { + let sema = Semantics::new(db); + let source_file = sema.parse(file_id); + source_file.syntax().descendants().filter_map(|it| method_range(it, file_id)).collect() +} + +fn method_range(item: SyntaxNode, file_id: FileId) -> Option<FileRange> { + ast::Fn::cast(item).and_then(|fn_def| { + if test_related_attribute(&fn_def).is_some() { + None + } else { + fn_def.name().map(|name| FileRange { file_id, range: name.syntax().text_range() }) + } + }) +} + +#[cfg(test)] +mod tests { + use crate::fixture; + use crate::{FileRange, TextSize}; + use std::ops::RangeInclusive; + + #[test] + fn test_find_all_methods() { + let (analysis, pos) = fixture::position( + r#" + fn private_fn() {$0} + + pub fn pub_fn() {} + + pub fn generic_fn<T>(arg: T) {} + "#, + ); + + let refs = analysis.find_all_methods(pos.file_id).unwrap(); + check_result(&refs, &[3..=13, 27..=33, 47..=57]); + } + + #[test] + fn test_find_trait_methods() { + let (analysis, pos) = fixture::position( + r#" + trait Foo { + fn bar() {$0} + fn baz() {} + } + "#, + ); + + let refs = analysis.find_all_methods(pos.file_id).unwrap(); + check_result(&refs, &[19..=22, 35..=38]); + } + + #[test] + fn test_skip_tests() { + let (analysis, pos) = fixture::position( + r#" + //- /lib.rs + #[test] + fn foo() {$0} + + pub fn pub_fn() {} + + mod tests { + #[test] + fn bar() {} + } + "#, + ); + + let refs = analysis.find_all_methods(pos.file_id).unwrap(); + check_result(&refs, &[28..=34]); + } + + fn check_result(refs: &[FileRange], expected: &[RangeInclusive<u32>]) { + assert_eq!(refs.len(), expected.len()); + + for (i, item) in refs.iter().enumerate() { + let range = &expected[i]; + assert_eq!(TextSize::from(*range.start()), item.range.start()); + assert_eq!(TextSize::from(*range.end()), item.range.end()); + } + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/folding_ranges.rs b/src/tools/rust-analyzer/crates/ide/src/folding_ranges.rs new file mode 100755 index 000000000..c694d95d5 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/folding_ranges.rs @@ -0,0 +1,626 @@ +use ide_db::{syntax_helpers::node_ext::vis_eq, FxHashSet}; +use syntax::{ + ast::{self, AstNode, AstToken}, + match_ast, Direction, NodeOrToken, SourceFile, + SyntaxKind::{self, *}, + TextRange, TextSize, +}; + +use std::hash::Hash; + +const REGION_START: &str = "// region:"; +const REGION_END: &str = "// endregion"; + +#[derive(Debug, PartialEq, Eq)] +pub enum FoldKind { + Comment, + Imports, + Mods, + Block, + ArgList, + Region, + Consts, + Statics, + Array, + WhereClause, + ReturnType, + MatchArm, +} + +#[derive(Debug)] +pub struct Fold { + pub range: TextRange, + pub kind: FoldKind, +} + +// Feature: Folding +// +// Defines folding regions for curly braced blocks, runs of consecutive use, mod, const or static +// items, and `region` / `endregion` comment markers. +pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> { + let mut res = vec![]; + let mut visited_comments = FxHashSet::default(); + let mut visited_imports = FxHashSet::default(); + let mut visited_mods = FxHashSet::default(); + let mut visited_consts = FxHashSet::default(); + let mut visited_statics = FxHashSet::default(); + + // regions can be nested, here is a LIFO buffer + let mut region_starts: Vec<TextSize> = vec![]; + + for element in file.syntax().descendants_with_tokens() { + // Fold items that span multiple lines + if let Some(kind) = fold_kind(element.kind()) { + let is_multiline = match &element { + NodeOrToken::Node(node) => node.text().contains_char('\n'), + NodeOrToken::Token(token) => token.text().contains('\n'), + }; + if is_multiline { + res.push(Fold { range: element.text_range(), kind }); + continue; + } + } + + match element { + NodeOrToken::Token(token) => { + // Fold groups of comments + if let Some(comment) = ast::Comment::cast(token) { + if visited_comments.contains(&comment) { + continue; + } + let text = comment.text().trim_start(); + if text.starts_with(REGION_START) { + region_starts.push(comment.syntax().text_range().start()); + } else if text.starts_with(REGION_END) { + if let Some(region) = region_starts.pop() { + res.push(Fold { + range: TextRange::new(region, comment.syntax().text_range().end()), + kind: FoldKind::Region, + }) + } + } else if let Some(range) = + contiguous_range_for_comment(comment, &mut visited_comments) + { + res.push(Fold { range, kind: FoldKind::Comment }) + } + } + } + NodeOrToken::Node(node) => { + match_ast! { + match node { + ast::Module(module) => { + if module.item_list().is_none() { + if let Some(range) = contiguous_range_for_item_group( + module, + &mut visited_mods, + ) { + res.push(Fold { range, kind: FoldKind::Mods }) + } + } + }, + ast::Use(use_) => { + if let Some(range) = contiguous_range_for_item_group(use_, &mut visited_imports) { + res.push(Fold { range, kind: FoldKind::Imports }) + } + }, + ast::Const(konst) => { + if let Some(range) = contiguous_range_for_item_group(konst, &mut visited_consts) { + res.push(Fold { range, kind: FoldKind::Consts }) + } + }, + ast::Static(statik) => { + if let Some(range) = contiguous_range_for_item_group(statik, &mut visited_statics) { + res.push(Fold { range, kind: FoldKind::Statics }) + } + }, + ast::WhereClause(where_clause) => { + if let Some(range) = fold_range_for_where_clause(where_clause) { + res.push(Fold { range, kind: FoldKind::WhereClause }) + } + }, + ast::MatchArm(match_arm) => { + if let Some(range) = fold_range_for_multiline_match_arm(match_arm) { + res.push(Fold {range, kind: FoldKind::MatchArm}) + } + }, + _ => (), + } + } + } + } + } + + res +} + +fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> { + match kind { + COMMENT => Some(FoldKind::Comment), + ARG_LIST | PARAM_LIST => Some(FoldKind::ArgList), + ARRAY_EXPR => Some(FoldKind::Array), + RET_TYPE => Some(FoldKind::ReturnType), + ASSOC_ITEM_LIST + | RECORD_FIELD_LIST + | RECORD_PAT_FIELD_LIST + | RECORD_EXPR_FIELD_LIST + | ITEM_LIST + | EXTERN_ITEM_LIST + | USE_TREE_LIST + | BLOCK_EXPR + | MATCH_ARM_LIST + | VARIANT_LIST + | TOKEN_TREE => Some(FoldKind::Block), + _ => None, + } +} + +fn contiguous_range_for_item_group<N>(first: N, visited: &mut FxHashSet<N>) -> Option<TextRange> +where + N: ast::HasVisibility + Clone + Hash + Eq, +{ + if !visited.insert(first.clone()) { + return None; + } + + let (mut last, mut last_vis) = (first.clone(), first.visibility()); + for element in first.syntax().siblings_with_tokens(Direction::Next) { + let node = match element { + NodeOrToken::Token(token) => { + if let Some(ws) = ast::Whitespace::cast(token) { + if !ws.spans_multiple_lines() { + // Ignore whitespace without blank lines + continue; + } + } + // There is a blank line or another token, which means that the + // group ends here + break; + } + NodeOrToken::Node(node) => node, + }; + + if let Some(next) = N::cast(node) { + let next_vis = next.visibility(); + if eq_visibility(next_vis.clone(), last_vis) { + visited.insert(next.clone()); + last_vis = next_vis; + last = next; + continue; + } + } + // Stop if we find an item of a different kind or with a different visibility. + break; + } + + if first != last { + Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end())) + } else { + // The group consists of only one element, therefore it cannot be folded + None + } +} + +fn eq_visibility(vis0: Option<ast::Visibility>, vis1: Option<ast::Visibility>) -> bool { + match (vis0, vis1) { + (None, None) => true, + (Some(vis0), Some(vis1)) => vis_eq(&vis0, &vis1), + _ => false, + } +} + +fn contiguous_range_for_comment( + first: ast::Comment, + visited: &mut FxHashSet<ast::Comment>, +) -> Option<TextRange> { + visited.insert(first.clone()); + + // Only fold comments of the same flavor + let group_kind = first.kind(); + if !group_kind.shape.is_line() { + return None; + } + + let mut last = first.clone(); + for element in first.syntax().siblings_with_tokens(Direction::Next) { + match element { + NodeOrToken::Token(token) => { + if let Some(ws) = ast::Whitespace::cast(token.clone()) { + if !ws.spans_multiple_lines() { + // Ignore whitespace without blank lines + continue; + } + } + if let Some(c) = ast::Comment::cast(token) { + if c.kind() == group_kind { + let text = c.text().trim_start(); + // regions are not real comments + if !(text.starts_with(REGION_START) || text.starts_with(REGION_END)) { + visited.insert(c.clone()); + last = c; + continue; + } + } + } + // The comment group ends because either: + // * An element of a different kind was reached + // * A comment of a different flavor was reached + break; + } + NodeOrToken::Node(_) => break, + }; + } + + if first != last { + Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end())) + } else { + // The group consists of only one element, therefore it cannot be folded + None + } +} + +fn fold_range_for_where_clause(where_clause: ast::WhereClause) -> Option<TextRange> { + let first_where_pred = where_clause.predicates().next(); + let last_where_pred = where_clause.predicates().last(); + + if first_where_pred != last_where_pred { + let start = where_clause.where_token()?.text_range().end(); + let end = where_clause.syntax().text_range().end(); + return Some(TextRange::new(start, end)); + } + None +} + +fn fold_range_for_multiline_match_arm(match_arm: ast::MatchArm) -> Option<TextRange> { + if let Some(_) = fold_kind(match_arm.expr()?.syntax().kind()) { + return None; + } + if match_arm.expr()?.syntax().text().contains_char('\n') { + return Some(match_arm.expr()?.syntax().text_range()); + } + None +} + +#[cfg(test)] +mod tests { + use test_utils::extract_tags; + + use super::*; + + fn check(ra_fixture: &str) { + let (ranges, text) = extract_tags(ra_fixture, "fold"); + + let parse = SourceFile::parse(&text); + let mut folds = folding_ranges(&parse.tree()); + folds.sort_by_key(|fold| (fold.range.start(), fold.range.end())); + + assert_eq!( + folds.len(), + ranges.len(), + "The amount of folds is different than the expected amount" + ); + + for (fold, (range, attr)) in folds.iter().zip(ranges.into_iter()) { + assert_eq!(fold.range.start(), range.start(), "mismatched start of folding ranges"); + assert_eq!(fold.range.end(), range.end(), "mismatched end of folding ranges"); + + let kind = match fold.kind { + FoldKind::Comment => "comment", + FoldKind::Imports => "imports", + FoldKind::Mods => "mods", + FoldKind::Block => "block", + FoldKind::ArgList => "arglist", + FoldKind::Region => "region", + FoldKind::Consts => "consts", + FoldKind::Statics => "statics", + FoldKind::Array => "array", + FoldKind::WhereClause => "whereclause", + FoldKind::ReturnType => "returntype", + FoldKind::MatchArm => "matcharm", + }; + assert_eq!(kind, &attr.unwrap()); + } + } + + #[test] + fn test_fold_comments() { + check( + r#" +<fold comment>// Hello +// this is a multiline +// comment +//</fold> + +// But this is not + +fn main() <fold block>{ + <fold comment>// We should + // also + // fold + // this one.</fold> + <fold comment>//! But this one is different + //! because it has another flavor</fold> + <fold comment>/* As does this + multiline comment */</fold> +}</fold> +"#, + ); + } + + #[test] + fn test_fold_imports() { + check( + r#" +use std::<fold block>{ + str, + vec, + io as iop +}</fold>; +"#, + ); + } + + #[test] + fn test_fold_mods() { + check( + r#" + +pub mod foo; +<fold mods>mod after_pub; +mod after_pub_next;</fold> + +<fold mods>mod before_pub; +mod before_pub_next;</fold> +pub mod bar; + +mod not_folding_single; +pub mod foobar; +pub not_folding_single_next; + +<fold mods>#[cfg(test)] +mod with_attribute; +mod with_attribute_next;</fold> + +mod inline0 {} +mod inline1 {} + +mod inline2 <fold block>{ + +}</fold> +"#, + ); + } + + #[test] + fn test_fold_import_groups() { + check( + r#" +<fold imports>use std::str; +use std::vec; +use std::io as iop;</fold> + +<fold imports>use std::mem; +use std::f64;</fold> + +<fold imports>use std::collections::HashMap; +// Some random comment +use std::collections::VecDeque;</fold> +"#, + ); + } + + #[test] + fn test_fold_import_and_groups() { + check( + r#" +<fold imports>use std::str; +use std::vec; +use std::io as iop;</fold> + +<fold imports>use std::mem; +use std::f64;</fold> + +use std::collections::<fold block>{ + HashMap, + VecDeque, +}</fold>; +// Some random comment +"#, + ); + } + + #[test] + fn test_folds_structs() { + check( + r#" +struct Foo <fold block>{ +}</fold> +"#, + ); + } + + #[test] + fn test_folds_traits() { + check( + r#" +trait Foo <fold block>{ +}</fold> +"#, + ); + } + + #[test] + fn test_folds_macros() { + check( + r#" +macro_rules! foo <fold block>{ + ($($tt:tt)*) => { $($tt)* } +}</fold> +"#, + ); + } + + #[test] + fn test_fold_match_arms() { + check( + r#" +fn main() <fold block>{ + match 0 <fold block>{ + 0 => 0, + _ => 1, + }</fold> +}</fold> +"#, + ); + } + + #[test] + fn test_fold_multiline_non_block_match_arm() { + check( + r#" + fn main() <fold block>{ + match foo <fold block>{ + block => <fold block>{ + }</fold>, + matcharm => <fold matcharm>some. + call(). + chain()</fold>, + matcharm2 + => 0, + match_expr => <fold matcharm>match foo2 <fold block>{ + bar => (), + }</fold></fold>, + array_list => <fold array>[ + 1, + 2, + 3, + ]</fold>, + strustS => <fold matcharm>StructS <fold block>{ + a: 31, + }</fold></fold>, + }</fold> + }</fold> + "#, + ) + } + + #[test] + fn fold_big_calls() { + check( + r#" +fn main() <fold block>{ + frobnicate<fold arglist>( + 1, + 2, + 3, + )</fold> +}</fold> +"#, + ) + } + + #[test] + fn fold_record_literals() { + check( + r#" +const _: S = S <fold block>{ + +}</fold>; +"#, + ) + } + + #[test] + fn fold_multiline_params() { + check( + r#" +fn foo<fold arglist>( + x: i32, + y: String, +)</fold> {} +"#, + ) + } + + #[test] + fn fold_multiline_array() { + check( + r#" +const FOO: [usize; 4] = <fold array>[ + 1, + 2, + 3, + 4, +]</fold>; +"#, + ) + } + + #[test] + fn fold_region() { + check( + r#" +// 1. some normal comment +<fold region>// region: test +// 2. some normal comment +<fold region>// region: inner +fn f() {} +// endregion</fold> +fn f2() {} +// endregion: test</fold> +"#, + ) + } + + #[test] + fn fold_consecutive_const() { + check( + r#" +<fold consts>const FIRST_CONST: &str = "first"; +const SECOND_CONST: &str = "second";</fold> +"#, + ) + } + + #[test] + fn fold_consecutive_static() { + check( + r#" +<fold statics>static FIRST_STATIC: &str = "first"; +static SECOND_STATIC: &str = "second";</fold> +"#, + ) + } + + #[test] + fn fold_where_clause() { + // fold multi-line and don't fold single line. + check( + r#" +fn foo() +where<fold whereclause> + A: Foo, + B: Foo, + C: Foo, + D: Foo,</fold> {} + +fn bar() +where + A: Bar, {} +"#, + ) + } + + #[test] + fn fold_return_type() { + check( + r#" +fn foo()<fold returntype>-> ( + bool, + bool, +)</fold> { (true, true) } + +fn bar() -> (bool, bool) { (true, true) } +"#, + ) + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/goto_declaration.rs b/src/tools/rust-analyzer/crates/ide/src/goto_declaration.rs new file mode 100644 index 000000000..926292c9b --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/goto_declaration.rs @@ -0,0 +1,112 @@ +use hir::Semantics; +use ide_db::{ + defs::{Definition, NameClass, NameRefClass}, + RootDatabase, +}; +use syntax::{ast, match_ast, AstNode, SyntaxKind::*, T}; + +use crate::{FilePosition, NavigationTarget, RangeInfo}; + +// Feature: Go to Declaration +// +// Navigates to the declaration of an identifier. +// +// This is currently the same as `Go to Definition` with the exception of outline modules where it +// will navigate to the `mod name;` item declaration. +pub(crate) fn goto_declaration( + db: &RootDatabase, + position: FilePosition, +) -> Option<RangeInfo<Vec<NavigationTarget>>> { + let sema = Semantics::new(db); + let file = sema.parse(position.file_id).syntax().clone(); + let original_token = file + .token_at_offset(position.offset) + .find(|it| matches!(it.kind(), IDENT | T![self] | T![super] | T![crate] | T![Self]))?; + let range = original_token.text_range(); + let info: Vec<NavigationTarget> = sema + .descend_into_macros(original_token) + .iter() + .filter_map(|token| { + let parent = token.parent()?; + let def = match_ast! { + match parent { + ast::NameRef(name_ref) => match NameRefClass::classify(&sema, &name_ref)? { + NameRefClass::Definition(it) => Some(it), + _ => None + }, + ast::Name(name) => match NameClass::classify(&sema, &name)? { + NameClass::Definition(it) => Some(it), + _ => None + }, + _ => None + } + }; + match def? { + Definition::Module(module) => { + Some(NavigationTarget::from_module_to_decl(db, module)) + } + _ => None, + } + }) + .collect(); + + Some(RangeInfo::new(range, info)) +} + +#[cfg(test)] +mod tests { + use ide_db::base_db::FileRange; + use itertools::Itertools; + + use crate::fixture; + + fn check(ra_fixture: &str) { + let (analysis, position, expected) = fixture::annotations(ra_fixture); + let navs = analysis + .goto_declaration(position) + .unwrap() + .expect("no declaration or definition found") + .info; + if navs.is_empty() { + panic!("unresolved reference") + } + + let cmp = |&FileRange { file_id, range }: &_| (file_id, range.start()); + let navs = navs + .into_iter() + .map(|nav| FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() }) + .sorted_by_key(cmp) + .collect::<Vec<_>>(); + let expected = expected + .into_iter() + .map(|(FileRange { file_id, range }, _)| FileRange { file_id, range }) + .sorted_by_key(cmp) + .collect::<Vec<_>>(); + assert_eq!(expected, navs); + } + + #[test] + fn goto_decl_module_outline() { + check( + r#" +//- /main.rs +mod foo; + // ^^^ +//- /foo.rs +use self$0; +"#, + ) + } + + #[test] + fn goto_decl_module_inline() { + check( + r#" +mod foo { + // ^^^ + use self$0; +} +"#, + ) + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/goto_definition.rs b/src/tools/rust-analyzer/crates/ide/src/goto_definition.rs new file mode 100644 index 000000000..d9c97751c --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/goto_definition.rs @@ -0,0 +1,1634 @@ +use std::{convert::TryInto, mem::discriminant}; + +use crate::{doc_links::token_as_doc_comment, FilePosition, NavigationTarget, RangeInfo, TryToNav}; +use hir::{AsAssocItem, AssocItem, Semantics}; +use ide_db::{ + base_db::{AnchoredPath, FileId, FileLoader}, + defs::{Definition, IdentClass}, + helpers::pick_best_token, + RootDatabase, +}; +use itertools::Itertools; +use syntax::{ast, AstNode, AstToken, SyntaxKind::*, SyntaxToken, TextRange, T}; + +// Feature: Go to Definition +// +// Navigates to the definition of an identifier. +// +// For outline modules, this will navigate to the source file of the module. +// +// |=== +// | Editor | Shortcut +// +// | VS Code | kbd:[F12] +// |=== +// +// image::https://user-images.githubusercontent.com/48062697/113065563-025fbe00-91b1-11eb-83e4-a5a703610b23.gif[] +pub(crate) fn goto_definition( + db: &RootDatabase, + position: FilePosition, +) -> Option<RangeInfo<Vec<NavigationTarget>>> { + let sema = &Semantics::new(db); + let file = sema.parse(position.file_id).syntax().clone(); + let original_token = + pick_best_token(file.token_at_offset(position.offset), |kind| match kind { + IDENT + | INT_NUMBER + | LIFETIME_IDENT + | T![self] + | T![super] + | T![crate] + | T![Self] + | COMMENT => 2, + kind if kind.is_trivia() => 0, + _ => 1, + })?; + if let Some(doc_comment) = token_as_doc_comment(&original_token) { + return doc_comment.get_definition_with_descend_at(sema, position.offset, |def, _, _| { + let nav = def.try_to_nav(db)?; + Some(RangeInfo::new(original_token.text_range(), vec![nav])) + }); + } + let navs = sema + .descend_into_macros(original_token.clone()) + .into_iter() + .filter_map(|token| { + let parent = token.parent()?; + if let Some(tt) = ast::TokenTree::cast(parent) { + if let Some(x) = try_lookup_include_path(sema, tt, token.clone(), position.file_id) + { + return Some(vec![x]); + } + } + Some( + IdentClass::classify_token(sema, &token)? + .definitions() + .into_iter() + .flat_map(|def| { + try_filter_trait_item_definition(sema, &def) + .unwrap_or_else(|| def_to_nav(sema.db, def)) + }) + .collect(), + ) + }) + .flatten() + .unique() + .collect::<Vec<NavigationTarget>>(); + + Some(RangeInfo::new(original_token.text_range(), navs)) +} + +fn try_lookup_include_path( + sema: &Semantics<'_, RootDatabase>, + tt: ast::TokenTree, + token: SyntaxToken, + file_id: FileId, +) -> Option<NavigationTarget> { + let token = ast::String::cast(token)?; + let path = token.value()?.into_owned(); + let macro_call = tt.syntax().parent().and_then(ast::MacroCall::cast)?; + let name = macro_call.path()?.segment()?.name_ref()?; + if !matches!(&*name.text(), "include" | "include_str" | "include_bytes") { + return None; + } + let file_id = sema.db.resolve_path(AnchoredPath { anchor: file_id, path: &path })?; + let size = sema.db.file_text(file_id).len().try_into().ok()?; + Some(NavigationTarget { + file_id, + full_range: TextRange::new(0.into(), size), + name: path.into(), + focus_range: None, + kind: None, + container_name: None, + description: None, + docs: None, + }) +} +/// finds the trait definition of an impl'd item, except function +/// e.g. +/// ```rust +/// trait A { type a; } +/// struct S; +/// impl A for S { type a = i32; } // <-- on this associate type, will get the location of a in the trait +/// ``` +fn try_filter_trait_item_definition( + sema: &Semantics<'_, RootDatabase>, + def: &Definition, +) -> Option<Vec<NavigationTarget>> { + let db = sema.db; + let assoc = def.as_assoc_item(db)?; + match assoc { + AssocItem::Function(..) => None, + AssocItem::Const(..) | AssocItem::TypeAlias(..) => { + let imp = match assoc.container(db) { + hir::AssocItemContainer::Impl(imp) => imp, + _ => return None, + }; + let trait_ = imp.trait_(db)?; + let name = def.name(db)?; + let discri_value = discriminant(&assoc); + trait_ + .items(db) + .iter() + .filter(|itm| discriminant(*itm) == discri_value) + .find_map(|itm| (itm.name(db)? == name).then(|| itm.try_to_nav(db)).flatten()) + .map(|it| vec![it]) + } + } +} + +fn def_to_nav(db: &RootDatabase, def: Definition) -> Vec<NavigationTarget> { + def.try_to_nav(db).map(|it| vec![it]).unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use ide_db::base_db::FileRange; + use itertools::Itertools; + + use crate::fixture; + + #[track_caller] + fn check(ra_fixture: &str) { + let (analysis, position, expected) = fixture::annotations(ra_fixture); + let navs = analysis.goto_definition(position).unwrap().expect("no definition found").info; + if navs.is_empty() { + panic!("unresolved reference") + } + + let cmp = |&FileRange { file_id, range }: &_| (file_id, range.start()); + let navs = navs + .into_iter() + .map(|nav| FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() }) + .sorted_by_key(cmp) + .collect::<Vec<_>>(); + let expected = expected + .into_iter() + .map(|(FileRange { file_id, range }, _)| FileRange { file_id, range }) + .sorted_by_key(cmp) + .collect::<Vec<_>>(); + assert_eq!(expected, navs); + } + + fn check_unresolved(ra_fixture: &str) { + let (analysis, position) = fixture::position(ra_fixture); + let navs = analysis.goto_definition(position).unwrap().expect("no definition found").info; + + assert!(navs.is_empty(), "didn't expect this to resolve anywhere: {:?}", navs) + } + + #[test] + fn goto_def_if_items_same_name() { + check( + r#" +trait Trait { + type A; + const A: i32; + //^ +} + +struct T; +impl Trait for T { + type A = i32; + const A$0: i32 = -9; +}"#, + ); + } + #[test] + fn goto_def_in_mac_call_in_attr_invoc() { + check( + r#" +//- proc_macros: identity +pub struct Struct { + // ^^^^^^ + field: i32, +} + +macro_rules! identity { + ($($tt:tt)*) => {$($tt)*}; +} + +#[proc_macros::identity] +fn function() { + identity!(Struct$0 { field: 0 }); +} + +"#, + ) + } + + #[test] + fn goto_def_for_extern_crate() { + check( + r#" +//- /main.rs crate:main deps:std +extern crate std$0; +//- /std/lib.rs crate:std +// empty +//^file +"#, + ) + } + + #[test] + fn goto_def_for_renamed_extern_crate() { + check( + r#" +//- /main.rs crate:main deps:std +extern crate std as abc$0; +//- /std/lib.rs crate:std +// empty +//^file +"#, + ) + } + + #[test] + fn goto_def_in_items() { + check( + r#" +struct Foo; + //^^^ +enum E { X(Foo$0) } +"#, + ); + } + + #[test] + fn goto_def_at_start_of_item() { + check( + r#" +struct Foo; + //^^^ +enum E { X($0Foo) } +"#, + ); + } + + #[test] + fn goto_definition_resolves_correct_name() { + check( + r#" +//- /lib.rs +use a::Foo; +mod a; +mod b; +enum E { X(Foo$0) } + +//- /a.rs +struct Foo; + //^^^ +//- /b.rs +struct Foo; +"#, + ); + } + + #[test] + fn goto_def_for_module_declaration() { + check( + r#" +//- /lib.rs +mod $0foo; + +//- /foo.rs +// empty +//^file +"#, + ); + + check( + r#" +//- /lib.rs +mod $0foo; + +//- /foo/mod.rs +// empty +//^file +"#, + ); + } + + #[test] + fn goto_def_for_macros() { + check( + r#" +macro_rules! foo { () => { () } } + //^^^ +fn bar() { + $0foo!(); +} +"#, + ); + } + + #[test] + fn goto_def_for_macros_from_other_crates() { + check( + r#" +//- /lib.rs crate:main deps:foo +use foo::foo; +fn bar() { + $0foo!(); +} + +//- /foo/lib.rs crate:foo +#[macro_export] +macro_rules! foo { () => { () } } + //^^^ +"#, + ); + } + + #[test] + fn goto_def_for_macros_in_use_tree() { + check( + r#" +//- /lib.rs crate:main deps:foo +use foo::foo$0; + +//- /foo/lib.rs crate:foo +#[macro_export] +macro_rules! foo { () => { () } } + //^^^ +"#, + ); + } + + #[test] + fn goto_def_for_macro_defined_fn_with_arg() { + check( + r#" +//- /lib.rs +macro_rules! define_fn { + ($name:ident) => (fn $name() {}) +} + +define_fn!(foo); + //^^^ + +fn bar() { + $0foo(); +} +"#, + ); + } + + #[test] + fn goto_def_for_macro_defined_fn_no_arg() { + check( + r#" +//- /lib.rs +macro_rules! define_fn { + () => (fn foo() {}) +} + + define_fn!(); +//^^^^^^^^^^^^^ + +fn bar() { + $0foo(); +} +"#, + ); + } + + #[test] + fn goto_definition_works_for_macro_inside_pattern() { + check( + r#" +//- /lib.rs +macro_rules! foo {() => {0}} + //^^^ + +fn bar() { + match (0,1) { + ($0foo!(), _) => {} + } +} +"#, + ); + } + + #[test] + fn goto_definition_works_for_macro_inside_match_arm_lhs() { + check( + r#" +//- /lib.rs +macro_rules! foo {() => {0}} + //^^^ +fn bar() { + match 0 { + $0foo!() => {} + } +} +"#, + ); + } + + #[test] + fn goto_def_for_use_alias() { + check( + r#" +//- /lib.rs crate:main deps:foo +use foo as bar$0; + +//- /foo/lib.rs crate:foo +// empty +//^file +"#, + ); + } + + #[test] + fn goto_def_for_use_alias_foo_macro() { + check( + r#" +//- /lib.rs crate:main deps:foo +use foo::foo as bar$0; + +//- /foo/lib.rs crate:foo +#[macro_export] +macro_rules! foo { () => { () } } + //^^^ +"#, + ); + } + + #[test] + fn goto_def_for_methods() { + check( + r#" +struct Foo; +impl Foo { + fn frobnicate(&self) { } + //^^^^^^^^^^ +} + +fn bar(foo: &Foo) { + foo.frobnicate$0(); +} +"#, + ); + } + + #[test] + fn goto_def_for_fields() { + check( + r#" +struct Foo { + spam: u32, +} //^^^^ + +fn bar(foo: &Foo) { + foo.spam$0; +} +"#, + ); + } + + #[test] + fn goto_def_for_record_fields() { + check( + r#" +//- /lib.rs +struct Foo { + spam: u32, +} //^^^^ + +fn bar() -> Foo { + Foo { + spam$0: 0, + } +} +"#, + ); + } + + #[test] + fn goto_def_for_record_pat_fields() { + check( + r#" +//- /lib.rs +struct Foo { + spam: u32, +} //^^^^ + +fn bar(foo: Foo) -> Foo { + let Foo { spam$0: _, } = foo +} +"#, + ); + } + + #[test] + fn goto_def_for_record_fields_macros() { + check( + r" +macro_rules! m { () => { 92 };} +struct Foo { spam: u32 } + //^^^^ + +fn bar() -> Foo { + Foo { spam$0: m!() } +} +", + ); + } + + #[test] + fn goto_for_tuple_fields() { + check( + r#" +struct Foo(u32); + //^^^ + +fn bar() { + let foo = Foo(0); + foo.$00; +} +"#, + ); + } + + #[test] + fn goto_def_for_ufcs_inherent_methods() { + check( + r#" +struct Foo; +impl Foo { + fn frobnicate() { } +} //^^^^^^^^^^ + +fn bar(foo: &Foo) { + Foo::frobnicate$0(); +} +"#, + ); + } + + #[test] + fn goto_def_for_ufcs_trait_methods_through_traits() { + check( + r#" +trait Foo { + fn frobnicate(); +} //^^^^^^^^^^ + +fn bar() { + Foo::frobnicate$0(); +} +"#, + ); + } + + #[test] + fn goto_def_for_ufcs_trait_methods_through_self() { + check( + r#" +struct Foo; +trait Trait { + fn frobnicate(); +} //^^^^^^^^^^ +impl Trait for Foo {} + +fn bar() { + Foo::frobnicate$0(); +} +"#, + ); + } + + #[test] + fn goto_definition_on_self() { + check( + r#" +struct Foo; +impl Foo { + //^^^ + pub fn new() -> Self { + Self$0 {} + } +} +"#, + ); + check( + r#" +struct Foo; +impl Foo { + //^^^ + pub fn new() -> Self$0 { + Self {} + } +} +"#, + ); + + check( + r#" +enum Foo { A } +impl Foo { + //^^^ + pub fn new() -> Self$0 { + Foo::A + } +} +"#, + ); + + check( + r#" +enum Foo { A } +impl Foo { + //^^^ + pub fn thing(a: &Self$0) { + } +} +"#, + ); + } + + #[test] + fn goto_definition_on_self_in_trait_impl() { + check( + r#" +struct Foo; +trait Make { + fn new() -> Self; +} +impl Make for Foo { + //^^^ + fn new() -> Self { + Self$0 {} + } +} +"#, + ); + + check( + r#" +struct Foo; +trait Make { + fn new() -> Self; +} +impl Make for Foo { + //^^^ + fn new() -> Self$0 { + Self {} + } +} +"#, + ); + } + + #[test] + fn goto_def_when_used_on_definition_name_itself() { + check( + r#" +struct Foo$0 { value: u32 } + //^^^ + "#, + ); + + check( + r#" +struct Foo { + field$0: string, +} //^^^^^ +"#, + ); + + check( + r#" +fn foo_test$0() { } + //^^^^^^^^ +"#, + ); + + check( + r#" +enum Foo$0 { Variant } + //^^^ +"#, + ); + + check( + r#" +enum Foo { + Variant1, + Variant2$0, + //^^^^^^^^ + Variant3, +} +"#, + ); + + check( + r#" +static INNER$0: &str = ""; + //^^^^^ +"#, + ); + + check( + r#" +const INNER$0: &str = ""; + //^^^^^ +"#, + ); + + check( + r#" +type Thing$0 = Option<()>; + //^^^^^ +"#, + ); + + check( + r#" +trait Foo$0 { } + //^^^ +"#, + ); + + check( + r#" +mod bar$0 { } + //^^^ +"#, + ); + } + + #[test] + fn goto_from_macro() { + check( + r#" +macro_rules! id { + ($($tt:tt)*) => { $($tt)* } +} +fn foo() {} + //^^^ +id! { + fn bar() { + fo$0o(); + } +} +mod confuse_index { fn foo(); } +"#, + ); + } + + #[test] + fn goto_through_format() { + check( + r#" +#[macro_export] +macro_rules! format { + ($($arg:tt)*) => ($crate::fmt::format($crate::__export::format_args!($($arg)*))) +} +#[rustc_builtin_macro] +#[macro_export] +macro_rules! format_args { + ($fmt:expr) => ({ /* compiler built-in */ }); + ($fmt:expr, $($args:tt)*) => ({ /* compiler built-in */ }) +} +pub mod __export { + pub use crate::format_args; + fn foo() {} // for index confusion +} +fn foo() -> i8 {} + //^^^ +fn test() { + format!("{}", fo$0o()) +} +"#, + ); + } + + #[test] + fn goto_through_included_file() { + check( + r#" +//- /main.rs +#[rustc_builtin_macro] +macro_rules! include {} + + include!("foo.rs"); +//^^^^^^^^^^^^^^^^^^^ + +fn f() { + foo$0(); +} + +mod confuse_index { + pub fn foo() {} +} + +//- /foo.rs +fn foo() {} + "#, + ); + } + + #[test] + fn goto_for_type_param() { + check( + r#" +struct Foo<T: Clone> { t: $0T } + //^ +"#, + ); + } + + #[test] + fn goto_within_macro() { + check( + r#" +macro_rules! id { + ($($tt:tt)*) => ($($tt)*) +} + +fn foo() { + let x = 1; + //^ + id!({ + let y = $0x; + let z = y; + }); +} +"#, + ); + + check( + r#" +macro_rules! id { + ($($tt:tt)*) => ($($tt)*) +} + +fn foo() { + let x = 1; + id!({ + let y = x; + //^ + let z = $0y; + }); +} +"#, + ); + } + + #[test] + fn goto_def_in_local_fn() { + check( + r#" +fn main() { + fn foo() { + let x = 92; + //^ + $0x; + } +} +"#, + ); + } + + #[test] + fn goto_def_in_local_macro() { + check( + r#" +fn bar() { + macro_rules! foo { () => { () } } + //^^^ + $0foo!(); +} +"#, + ); + } + + #[test] + fn goto_def_for_field_init_shorthand() { + check( + r#" +struct Foo { x: i32 } + //^ +fn main() { + let x = 92; + //^ + Foo { x$0 }; +} +"#, + ) + } + + #[test] + fn goto_def_for_enum_variant_field() { + check( + r#" +enum Foo { + Bar { x: i32 } + //^ +} +fn baz(foo: Foo) { + match foo { + Foo::Bar { x$0 } => x + //^ + }; +} +"#, + ); + } + + #[test] + fn goto_def_for_enum_variant_self_pattern_const() { + check( + r#" +enum Foo { Bar } + //^^^ +impl Foo { + fn baz(self) { + match self { Self::Bar$0 => {} } + } +} +"#, + ); + } + + #[test] + fn goto_def_for_enum_variant_self_pattern_record() { + check( + r#" +enum Foo { Bar { val: i32 } } + //^^^ +impl Foo { + fn baz(self) -> i32 { + match self { Self::Bar$0 { val } => {} } + } +} +"#, + ); + } + + #[test] + fn goto_def_for_enum_variant_self_expr_const() { + check( + r#" +enum Foo { Bar } + //^^^ +impl Foo { + fn baz(self) { Self::Bar$0; } +} +"#, + ); + } + + #[test] + fn goto_def_for_enum_variant_self_expr_record() { + check( + r#" +enum Foo { Bar { val: i32 } } + //^^^ +impl Foo { + fn baz(self) { Self::Bar$0 {val: 4}; } +} +"#, + ); + } + + #[test] + fn goto_def_for_type_alias_generic_parameter() { + check( + r#" +type Alias<T> = T$0; + //^ +"#, + ) + } + + #[test] + fn goto_def_for_macro_container() { + check( + r#" +//- /lib.rs crate:main deps:foo +foo::module$0::mac!(); + +//- /foo/lib.rs crate:foo +pub mod module { + //^^^^^^ + #[macro_export] + macro_rules! _mac { () => { () } } + pub use crate::_mac as mac; +} +"#, + ); + } + + #[test] + fn goto_def_for_assoc_ty_in_path() { + check( + r#" +trait Iterator { + type Item; + //^^^^ +} + +fn f() -> impl Iterator<Item$0 = u8> {} +"#, + ); + } + + #[test] + fn goto_def_for_super_assoc_ty_in_path() { + check( + r#" +trait Super { + type Item; + //^^^^ +} + +trait Sub: Super {} + +fn f() -> impl Sub<Item$0 = u8> {} +"#, + ); + } + + #[test] + fn unknown_assoc_ty() { + check_unresolved( + r#" +trait Iterator { type Item; } +fn f() -> impl Iterator<Invalid$0 = u8> {} +"#, + ) + } + + #[test] + fn goto_def_for_assoc_ty_in_path_multiple() { + check( + r#" +trait Iterator { + type A; + //^ + type B; +} + +fn f() -> impl Iterator<A$0 = u8, B = ()> {} +"#, + ); + check( + r#" +trait Iterator { + type A; + type B; + //^ +} + +fn f() -> impl Iterator<A = u8, B$0 = ()> {} +"#, + ); + } + + #[test] + fn goto_def_for_assoc_ty_ufcs() { + check( + r#" +trait Iterator { + type Item; + //^^^^ +} + +fn g() -> <() as Iterator<Item$0 = ()>>::Item {} +"#, + ); + } + + #[test] + fn goto_def_for_assoc_ty_ufcs_multiple() { + check( + r#" +trait Iterator { + type A; + //^ + type B; +} + +fn g() -> <() as Iterator<A$0 = (), B = u8>>::B {} +"#, + ); + check( + r#" +trait Iterator { + type A; + type B; + //^ +} + +fn g() -> <() as Iterator<A = (), B$0 = u8>>::A {} +"#, + ); + } + + #[test] + fn goto_self_param_ty_specified() { + check( + r#" +struct Foo {} + +impl Foo { + fn bar(self: &Foo) { + //^^^^ + let foo = sel$0f; + } +}"#, + ) + } + + #[test] + fn goto_self_param_on_decl() { + check( + r#" +struct Foo {} + +impl Foo { + fn bar(&self$0) { + //^^^^ + } +}"#, + ) + } + + #[test] + fn goto_lifetime_param_on_decl() { + check( + r#" +fn foo<'foobar$0>(_: &'foobar ()) { + //^^^^^^^ +}"#, + ) + } + + #[test] + fn goto_lifetime_param_decl() { + check( + r#" +fn foo<'foobar>(_: &'foobar$0 ()) { + //^^^^^^^ +}"#, + ) + } + + #[test] + fn goto_lifetime_param_decl_nested() { + check( + r#" +fn foo<'foobar>(_: &'foobar ()) { + fn foo<'foobar>(_: &'foobar$0 ()) {} + //^^^^^^^ +}"#, + ) + } + + #[test] + fn goto_lifetime_hrtb() { + // FIXME: requires the HIR to somehow track these hrtb lifetimes + check_unresolved( + r#" +trait Foo<T> {} +fn foo<T>() where for<'a> T: Foo<&'a$0 (u8, u16)>, {} + //^^ +"#, + ); + check_unresolved( + r#" +trait Foo<T> {} +fn foo<T>() where for<'a$0> T: Foo<&'a (u8, u16)>, {} + //^^ +"#, + ); + } + + #[test] + fn goto_lifetime_hrtb_for_type() { + // FIXME: requires ForTypes to be implemented + check_unresolved( + r#"trait Foo<T> {} +fn foo<T>() where T: for<'a> Foo<&'a$0 (u8, u16)>, {} + //^^ +"#, + ); + } + + #[test] + fn goto_label() { + check( + r#" +fn foo<'foo>(_: &'foo ()) { + 'foo: { + //^^^^ + 'bar: loop { + break 'foo$0; + } + } +}"#, + ) + } + + #[test] + fn goto_def_for_intra_doc_link_same_file() { + check( + r#" +/// Blah, [`bar`](bar) .. [`foo`](foo$0) has [`bar`](bar) +pub fn bar() { } + +/// You might want to see [`std::fs::read()`] too. +pub fn foo() { } + //^^^ + +}"#, + ) + } + + #[test] + fn goto_def_for_intra_doc_link_inner() { + check( + r#" +//- /main.rs +mod m; +struct S; + //^ + +//- /m.rs +//! [`super::S$0`] +"#, + ) + } + + #[test] + fn goto_incomplete_field() { + check( + r#" +struct A { a: u32 } + //^ +fn foo() { A { a$0: }; } +"#, + ) + } + + #[test] + fn goto_proc_macro() { + check( + r#" +//- /main.rs crate:main deps:mac +use mac::fn_macro; + +fn_macro$0!(); + +//- /mac.rs crate:mac +#![crate_type="proc-macro"] +#[proc_macro] +fn fn_macro() {} + //^^^^^^^^ + "#, + ) + } + + #[test] + fn goto_intra_doc_links() { + check( + r#" + +pub mod theitem { + /// This is the item. Cool! + pub struct TheItem; + //^^^^^^^ +} + +/// Gives you a [`TheItem$0`]. +/// +/// [`TheItem`]: theitem::TheItem +pub fn gimme() -> theitem::TheItem { + theitem::TheItem +} +"#, + ); + } + + #[test] + fn goto_ident_from_pat_macro() { + check( + r#" +macro_rules! pat { + ($name:ident) => { Enum::Variant1($name) } +} + +enum Enum { + Variant1(u8), + Variant2, +} + +fn f(e: Enum) { + match e { + pat!(bind) => { + //^^^^ + bind$0 + } + Enum::Variant2 => {} + } +} +"#, + ); + } + + #[test] + fn goto_include() { + check( + r#" +//- /main.rs +fn main() { + let str = include_str!("foo.txt$0"); +} +//- /foo.txt +// empty +//^file +"#, + ); + } + #[cfg(test)] + mod goto_impl_of_trait_fn { + use super::check; + #[test] + fn cursor_on_impl() { + check( + r#" +trait Twait { + fn a(); +} + +struct Stwuct; + +impl Twait for Stwuct { + fn a$0(); + //^ +} + "#, + ); + } + #[test] + fn method_call() { + check( + r#" +trait Twait { + fn a(&self); +} + +struct Stwuct; + +impl Twait for Stwuct { + fn a(&self){}; + //^ +} +fn f() { + let s = Stwuct; + s.a$0(); +} + "#, + ); + } + #[test] + fn path_call() { + check( + r#" +trait Twait { + fn a(&self); +} + +struct Stwuct; + +impl Twait for Stwuct { + fn a(&self){}; + //^ +} +fn f() { + let s = Stwuct; + Stwuct::a$0(&s); +} + "#, + ); + } + #[test] + fn where_clause_can_work() { + check( + r#" +trait G { + fn g(&self); +} +trait Bound{} +trait EA{} +struct Gen<T>(T); +impl <T:EA> G for Gen<T> { + fn g(&self) { + } +} +impl <T> G for Gen<T> +where T : Bound +{ + fn g(&self){ + //^ + } +} +struct A; +impl Bound for A{} +fn f() { + let gen = Gen::<A>(A); + gen.g$0(); +} + "#, + ); + } + #[test] + fn wc_case_is_ok() { + check( + r#" +trait G { + fn g(&self); +} +trait BParent{} +trait Bound: BParent{} +struct Gen<T>(T); +impl <T> G for Gen<T> +where T : Bound +{ + fn g(&self){ + //^ + } +} +struct A; +impl Bound for A{} +fn f() { + let gen = Gen::<A>(A); + gen.g$0(); +} +"#, + ); + } + + #[test] + fn method_call_defaulted() { + check( + r#" +trait Twait { + fn a(&self) {} + //^ +} + +struct Stwuct; + +impl Twait for Stwuct { +} +fn f() { + let s = Stwuct; + s.a$0(); +} + "#, + ); + } + + #[test] + fn method_call_on_generic() { + check( + r#" +trait Twait { + fn a(&self) {} + //^ +} + +fn f<T: Twait>(s: T) { + s.a$0(); +} + "#, + ); + } + } + + #[test] + fn goto_def_of_trait_impl_const() { + check( + r#" +trait Twait { + const NOMS: bool; + // ^^^^ +} + +struct Stwuct; + +impl Twait for Stwuct { + const NOMS$0: bool = true; +} +"#, + ); + } + + #[test] + fn goto_def_of_trait_impl_type_alias() { + check( + r#" +trait Twait { + type IsBad; + // ^^^^^ +} + +struct Stwuct; + +impl Twait for Stwuct { + type IsBad$0 = !; +} +"#, + ); + } + + #[test] + fn goto_def_derive_input() { + check( + r#" + //- minicore:derive + #[rustc_builtin_macro] + pub macro Copy {} + // ^^^^ + #[derive(Copy$0)] + struct Foo; + "#, + ); + check( + r#" +//- minicore:derive +#[rustc_builtin_macro] +pub macro Copy {} + // ^^^^ +#[cfg_attr(feature = "false", derive)] +#[derive(Copy$0)] +struct Foo; + "#, + ); + check( + r#" +//- minicore:derive +mod foo { + #[rustc_builtin_macro] + pub macro Copy {} + // ^^^^ +} +#[derive(foo::Copy$0)] +struct Foo; + "#, + ); + check( + r#" +//- minicore:derive +mod foo { + // ^^^ + #[rustc_builtin_macro] + pub macro Copy {} +} +#[derive(foo$0::Copy)] +struct Foo; + "#, + ); + } + + #[test] + fn goto_def_in_macro_multi() { + check( + r#" +struct Foo { + foo: () + //^^^ +} +macro_rules! foo { + ($ident:ident) => { + fn $ident(Foo { $ident }: Foo) {} + } +} +foo!(foo$0); + //^^^ + //^^^ +"#, + ); + check( + r#" +fn bar() {} + //^^^ +struct bar; + //^^^ +macro_rules! foo { + ($ident:ident) => { + fn foo() { + let _: $ident = $ident; + } + } +} + +foo!(bar$0); +"#, + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/goto_implementation.rs b/src/tools/rust-analyzer/crates/ide/src/goto_implementation.rs new file mode 100644 index 000000000..04b51c839 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/goto_implementation.rs @@ -0,0 +1,344 @@ +use hir::{AsAssocItem, Impl, Semantics}; +use ide_db::{ + defs::{Definition, NameClass, NameRefClass}, + helpers::pick_best_token, + RootDatabase, +}; +use itertools::Itertools; +use syntax::{ast, AstNode, SyntaxKind::*, T}; + +use crate::{FilePosition, NavigationTarget, RangeInfo, TryToNav}; + +// Feature: Go to Implementation +// +// Navigates to the impl blocks of types. +// +// |=== +// | Editor | Shortcut +// +// | VS Code | kbd:[Ctrl+F12] +// |=== +// +// image::https://user-images.githubusercontent.com/48062697/113065566-02f85480-91b1-11eb-9288-aaad8abd8841.gif[] +pub(crate) fn goto_implementation( + db: &RootDatabase, + position: FilePosition, +) -> Option<RangeInfo<Vec<NavigationTarget>>> { + let sema = Semantics::new(db); + let source_file = sema.parse(position.file_id); + let syntax = source_file.syntax().clone(); + + let original_token = + pick_best_token(syntax.token_at_offset(position.offset), |kind| match kind { + IDENT | T![self] => 1, + _ => 0, + })?; + let range = original_token.text_range(); + let navs = sema + .descend_into_macros(original_token) + .into_iter() + .filter_map(|token| token.parent().and_then(ast::NameLike::cast)) + .filter_map(|node| match &node { + ast::NameLike::Name(name) => { + NameClass::classify(&sema, name).map(|class| match class { + NameClass::Definition(it) | NameClass::ConstReference(it) => it, + NameClass::PatFieldShorthand { local_def, field_ref: _ } => { + Definition::Local(local_def) + } + }) + } + ast::NameLike::NameRef(name_ref) => { + NameRefClass::classify(&sema, name_ref).map(|class| match class { + NameRefClass::Definition(def) => def, + NameRefClass::FieldShorthand { local_ref, field_ref: _ } => { + Definition::Local(local_ref) + } + }) + } + ast::NameLike::Lifetime(_) => None, + }) + .unique() + .filter_map(|def| { + let navs = match def { + Definition::Trait(trait_) => impls_for_trait(&sema, trait_), + Definition::Adt(adt) => impls_for_ty(&sema, adt.ty(sema.db)), + Definition::TypeAlias(alias) => impls_for_ty(&sema, alias.ty(sema.db)), + Definition::BuiltinType(builtin) => impls_for_ty(&sema, builtin.ty(sema.db)), + Definition::Function(f) => { + let assoc = f.as_assoc_item(sema.db)?; + let name = assoc.name(sema.db)?; + let trait_ = assoc.containing_trait_or_trait_impl(sema.db)?; + impls_for_trait_item(&sema, trait_, name) + } + Definition::Const(c) => { + let assoc = c.as_assoc_item(sema.db)?; + let name = assoc.name(sema.db)?; + let trait_ = assoc.containing_trait_or_trait_impl(sema.db)?; + impls_for_trait_item(&sema, trait_, name) + } + _ => return None, + }; + Some(navs) + }) + .flatten() + .collect(); + + Some(RangeInfo { range, info: navs }) +} + +fn impls_for_ty(sema: &Semantics<'_, RootDatabase>, ty: hir::Type) -> Vec<NavigationTarget> { + Impl::all_for_type(sema.db, ty).into_iter().filter_map(|imp| imp.try_to_nav(sema.db)).collect() +} + +fn impls_for_trait( + sema: &Semantics<'_, RootDatabase>, + trait_: hir::Trait, +) -> Vec<NavigationTarget> { + Impl::all_for_trait(sema.db, trait_) + .into_iter() + .filter_map(|imp| imp.try_to_nav(sema.db)) + .collect() +} + +fn impls_for_trait_item( + sema: &Semantics<'_, RootDatabase>, + trait_: hir::Trait, + fun_name: hir::Name, +) -> Vec<NavigationTarget> { + Impl::all_for_trait(sema.db, trait_) + .into_iter() + .filter_map(|imp| { + let item = imp.items(sema.db).iter().find_map(|itm| { + let itm_name = itm.name(sema.db)?; + (itm_name == fun_name).then(|| *itm) + })?; + item.try_to_nav(sema.db) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use ide_db::base_db::FileRange; + use itertools::Itertools; + + use crate::fixture; + + fn check(ra_fixture: &str) { + let (analysis, position, expected) = fixture::annotations(ra_fixture); + + let navs = analysis.goto_implementation(position).unwrap().unwrap().info; + + let cmp = |frange: &FileRange| (frange.file_id, frange.range.start()); + + let actual = navs + .into_iter() + .map(|nav| FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() }) + .sorted_by_key(cmp) + .collect::<Vec<_>>(); + let expected = + expected.into_iter().map(|(range, _)| range).sorted_by_key(cmp).collect::<Vec<_>>(); + assert_eq!(expected, actual); + } + + #[test] + fn goto_implementation_works() { + check( + r#" +struct Foo$0; +impl Foo {} + //^^^ +"#, + ); + } + + #[test] + fn goto_implementation_works_multiple_blocks() { + check( + r#" +struct Foo$0; +impl Foo {} + //^^^ +impl Foo {} + //^^^ +"#, + ); + } + + #[test] + fn goto_implementation_works_multiple_mods() { + check( + r#" +struct Foo$0; +mod a { + impl super::Foo {} + //^^^^^^^^^^ +} +mod b { + impl super::Foo {} + //^^^^^^^^^^ +} +"#, + ); + } + + #[test] + fn goto_implementation_works_multiple_files() { + check( + r#" +//- /lib.rs +struct Foo$0; +mod a; +mod b; +//- /a.rs +impl crate::Foo {} + //^^^^^^^^^^ +//- /b.rs +impl crate::Foo {} + //^^^^^^^^^^ +"#, + ); + } + + #[test] + fn goto_implementation_for_trait() { + check( + r#" +trait T$0 {} +struct Foo; +impl T for Foo {} + //^^^ +"#, + ); + } + + #[test] + fn goto_implementation_for_trait_multiple_files() { + check( + r#" +//- /lib.rs +trait T$0 {}; +struct Foo; +mod a; +mod b; +//- /a.rs +impl crate::T for crate::Foo {} + //^^^^^^^^^^ +//- /b.rs +impl crate::T for crate::Foo {} + //^^^^^^^^^^ + "#, + ); + } + + #[test] + fn goto_implementation_all_impls() { + check( + r#" +//- /lib.rs +trait T {} +struct Foo$0; +impl Foo {} + //^^^ +impl T for Foo {} + //^^^ +impl T for &Foo {} + //^^^^ +"#, + ); + } + + #[test] + fn goto_implementation_to_builtin_derive() { + check( + r#" +//- minicore: copy, derive + #[derive(Copy)] +//^^^^^^^^^^^^^^^ +struct Foo$0; +"#, + ); + } + + #[test] + fn goto_implementation_type_alias() { + check( + r#" +struct Foo; + +type Bar$0 = Foo; + +impl Foo {} + //^^^ +impl Bar {} + //^^^ +"#, + ); + } + + #[test] + fn goto_implementation_adt_generic() { + check( + r#" +struct Foo$0<T>; + +impl<T> Foo<T> {} + //^^^^^^ +impl Foo<str> {} + //^^^^^^^^ +"#, + ); + } + + #[test] + fn goto_implementation_builtin() { + check( + r#" +//- /lib.rs crate:main deps:core +fn foo(_: bool$0) {{}} +//- /libcore.rs crate:core +#[lang = "bool"] +impl bool {} + //^^^^ +"#, + ); + } + + #[test] + fn goto_implementation_trait_functions() { + check( + r#" +trait Tr { + fn f$0(); +} + +struct S; + +impl Tr for S { + fn f() { + //^ + println!("Hello, world!"); + } +} +"#, + ); + } + + #[test] + fn goto_implementation_trait_assoc_const() { + check( + r#" +trait Tr { + const C$0: usize; +} + +struct S; + +impl Tr for S { + const C: usize = 4; + //^ +} +"#, + ); + } +} 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 new file mode 100644 index 000000000..55cdb3200 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/goto_type_definition.rs @@ -0,0 +1,296 @@ +use ide_db::{base_db::Upcast, defs::Definition, helpers::pick_best_token, RootDatabase}; +use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, T}; + +use crate::{FilePosition, NavigationTarget, RangeInfo, TryToNav}; + +// Feature: Go to Type Definition +// +// Navigates to the type of an identifier. +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Go to Type Definition* +// |=== +// +// image::https://user-images.githubusercontent.com/48062697/113020657-b560f500-917a-11eb-9007-0f809733a338.gif[] +pub(crate) fn goto_type_definition( + db: &RootDatabase, + position: FilePosition, +) -> Option<RangeInfo<Vec<NavigationTarget>>> { + let sema = hir::Semantics::new(db); + + let file: ast::SourceFile = sema.parse(position.file_id); + let token: SyntaxToken = + pick_best_token(file.syntax().token_at_offset(position.offset), |kind| match kind { + IDENT | INT_NUMBER | T![self] => 2, + kind if kind.is_trivia() => 0, + _ => 1, + })?; + + let mut res = Vec::new(); + let mut push = |def: Definition| { + if let Some(nav) = def.try_to_nav(db) { + if !res.contains(&nav) { + res.push(nav); + } + } + }; + let range = token.text_range(); + sema.descend_into_macros(token) + .iter() + .filter_map(|token| { + let ty = sema.token_ancestors_with_macros(token.clone()).find_map(|node| { + let ty = match_ast! { + match node { + ast::Expr(it) => sema.type_of_expr(&it)?.original, + ast::Pat(it) => sema.type_of_pat(&it)?.original, + ast::SelfParam(it) => sema.type_of_self(&it)?, + ast::Type(it) => sema.resolve_type(&it)?, + ast::RecordField(it) => sema.to_def(&it).map(|d| d.ty(db.upcast()))?, + // can't match on RecordExprField directly as `ast::Expr` will match an iteration too early otherwise + ast::NameRef(it) => { + if let Some(record_field) = ast::RecordExprField::for_name_ref(&it) { + let (_, _, ty) = sema.resolve_record_field(&record_field)?; + ty + } else { + let record_field = ast::RecordPatField::for_field_name_ref(&it)?; + sema.resolve_record_pat_field(&record_field)?.ty(db) + } + }, + _ => return None, + } + }; + + Some(ty) + }); + ty + }) + .for_each(|ty| { + // collect from each `ty` into the `res` result vec + let ty = ty.strip_references(); + ty.walk(db, |t| { + if let Some(adt) = t.as_adt() { + push(adt.into()); + } else if let Some(trait_) = t.as_dyn_trait() { + push(trait_.into()); + } else if let Some(traits) = t.as_impl_traits(db) { + traits.for_each(|it| push(it.into())); + } else if let Some(trait_) = t.as_associated_type_parent_trait(db) { + push(trait_.into()); + } + }); + }); + Some(RangeInfo::new(range, res)) +} + +#[cfg(test)] +mod tests { + use ide_db::base_db::FileRange; + use itertools::Itertools; + + use crate::fixture; + + fn check(ra_fixture: &str) { + let (analysis, position, expected) = fixture::annotations(ra_fixture); + let navs = analysis.goto_type_definition(position).unwrap().unwrap().info; + assert_ne!(navs.len(), 0); + + let cmp = |&FileRange { file_id, range }: &_| (file_id, range.start()); + let navs = navs + .into_iter() + .map(|nav| FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() }) + .sorted_by_key(cmp) + .collect::<Vec<_>>(); + let expected = expected + .into_iter() + .map(|(FileRange { file_id, range }, _)| FileRange { file_id, range }) + .sorted_by_key(cmp) + .collect::<Vec<_>>(); + assert_eq!(expected, navs); + } + + #[test] + fn goto_type_definition_works_simple() { + check( + r#" +struct Foo; + //^^^ +fn foo() { + let f: Foo; f$0 +} +"#, + ); + } + + #[test] + fn goto_type_definition_record_expr_field() { + check( + r#" +struct Bar; + // ^^^ +struct Foo { foo: Bar } +fn foo() { + Foo { foo$0 } +} +"#, + ); + check( + r#" +struct Bar; + // ^^^ +struct Foo { foo: Bar } +fn foo() { + Foo { foo$0: Bar } +} +"#, + ); + } + + #[test] + fn goto_type_definition_record_pat_field() { + check( + r#" +struct Bar; + // ^^^ +struct Foo { foo: Bar } +fn foo() { + let Foo { foo$0 }; +} +"#, + ); + check( + r#" +struct Bar; + // ^^^ +struct Foo { foo: Bar } +fn foo() { + let Foo { foo$0: bar }; +} +"#, + ); + } + + #[test] + fn goto_type_definition_works_simple_ref() { + check( + r#" +struct Foo; + //^^^ +fn foo() { + let f: &Foo; f$0 +} +"#, + ); + } + + #[test] + fn goto_type_definition_works_through_macro() { + check( + r#" +macro_rules! id { ($($tt:tt)*) => { $($tt)* } } +struct Foo {} + //^^^ +id! { + fn bar() { let f$0 = Foo {}; } +} +"#, + ); + } + + #[test] + fn goto_type_definition_for_param() { + check( + r#" +struct Foo; + //^^^ +fn foo($0f: Foo) {} +"#, + ); + } + + #[test] + fn goto_type_definition_for_tuple_field() { + check( + r#" +struct Foo; + //^^^ +struct Bar(Foo); +fn foo() { + let bar = Bar(Foo); + bar.$00; +} +"#, + ); + } + + #[test] + fn goto_def_for_self_param() { + check( + r#" +struct Foo; + //^^^ +impl Foo { + fn f(&self$0) {} +} +"#, + ) + } + + #[test] + fn goto_def_for_type_fallback() { + check( + r#" +struct Foo; + //^^^ +impl Foo$0 {} +"#, + ) + } + + #[test] + fn goto_def_for_struct_field() { + check( + r#" +struct Bar; + //^^^ + +struct Foo { + bar$0: Bar, +} +"#, + ); + } + + #[test] + fn goto_def_for_enum_struct_field() { + check( + r#" +struct Bar; + //^^^ + +enum Foo { + Bar { + bar$0: Bar + }, +} +"#, + ); + } + + #[test] + fn goto_def_considers_generics() { + check( + r#" +struct Foo; + //^^^ +struct Bar<T, U>(T, U); + //^^^ +struct Baz<T>(T); + //^^^ + +fn foo(x$0: Bar<Baz<Foo>, Baz<usize>) {} +"#, + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/highlight_related.rs b/src/tools/rust-analyzer/crates/ide/src/highlight_related.rs new file mode 100644 index 000000000..f2d7029ea --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/highlight_related.rs @@ -0,0 +1,1377 @@ +use hir::Semantics; +use ide_db::{ + base_db::{FileId, FilePosition}, + defs::{Definition, IdentClass}, + helpers::pick_best_token, + search::{FileReference, ReferenceCategory, SearchScope}, + syntax_helpers::node_ext::{for_each_break_and_continue_expr, for_each_tail_expr, walk_expr}, + FxHashSet, RootDatabase, +}; +use syntax::{ + ast::{self, HasLoopBody}, + match_ast, AstNode, + SyntaxKind::{self, IDENT, INT_NUMBER}, + SyntaxNode, SyntaxToken, TextRange, T, +}; + +use crate::{references, NavigationTarget, TryToNav}; + +#[derive(PartialEq, Eq, Hash)] +pub struct HighlightedRange { + pub range: TextRange, + // FIXME: This needs to be more precise. Reference category makes sense only + // for references, but we also have defs. And things like exit points are + // neither. + pub category: Option<ReferenceCategory>, +} + +#[derive(Default, Clone)] +pub struct HighlightRelatedConfig { + pub references: bool, + pub exit_points: bool, + pub break_points: bool, + pub yield_points: bool, +} + +// Feature: Highlight Related +// +// Highlights constructs related to the thing under the cursor: +// +// . if on an identifier, highlights all references to that identifier in the current file +// . if on an `async` or `await token, highlights all yield points for that async context +// . if on a `return` or `fn` keyword, `?` character or `->` return type arrow, highlights all exit points for that context +// . if on a `break`, `loop`, `while` or `for` token, highlights all break points for that loop or block context +// +// Note: `?` and `->` do not currently trigger this behavior in the VSCode editor. +pub(crate) fn highlight_related( + sema: &Semantics<'_, RootDatabase>, + config: HighlightRelatedConfig, + FilePosition { offset, file_id }: FilePosition, +) -> Option<Vec<HighlightedRange>> { + let _p = profile::span("highlight_related"); + 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![->] => 3, + kind if kind.is_keyword() => 2, + IDENT | INT_NUMBER => 1, + _ => 0, + })?; + match token.kind() { + T![?] if config.exit_points && token.parent().and_then(ast::TryExpr::cast).is_some() => { + highlight_exit_points(sema, token) + } + T![fn] | T![return] | T![->] if config.exit_points => highlight_exit_points(sema, token), + T![await] | T![async] if config.yield_points => highlight_yield_points(token), + T![for] if config.break_points && token.parent().and_then(ast::ForExpr::cast).is_some() => { + highlight_break_points(token) + } + T![break] | T![loop] | T![while] | T![continue] if config.break_points => { + highlight_break_points(token) + } + _ if config.references => highlight_references(sema, &syntax, token, file_id), + _ => None, + } +} + +fn highlight_references( + sema: &Semantics<'_, RootDatabase>, + node: &SyntaxNode, + token: SyntaxToken, + file_id: FileId, +) -> Option<Vec<HighlightedRange>> { + let defs = find_defs(sema, token); + let usages = defs + .iter() + .filter_map(|&d| { + d.usages(sema) + .set_scope(Some(SearchScope::single_file(file_id))) + .include_self_refs() + .all() + .references + .remove(&file_id) + }) + .flatten() + .map(|FileReference { category: access, range, .. }| HighlightedRange { + range, + category: access, + }); + let mut res = FxHashSet::default(); + + let mut def_to_hl_range = |def| { + let hl_range = match def { + Definition::Module(module) => { + Some(NavigationTarget::from_module_to_decl(sema.db, module)) + } + def => def.try_to_nav(sema.db), + } + .filter(|decl| decl.file_id == file_id) + .and_then(|decl| decl.focus_range) + .map(|range| { + let category = + references::decl_mutability(&def, node, range).then(|| ReferenceCategory::Write); + HighlightedRange { range, category } + }); + if let Some(hl_range) = hl_range { + res.insert(hl_range); + } + }; + for &def in &defs { + match def { + Definition::Local(local) => local + .associated_locals(sema.db) + .iter() + .for_each(|&local| def_to_hl_range(Definition::Local(local))), + def => def_to_hl_range(def), + } + } + + res.extend(usages); + if res.is_empty() { + None + } else { + Some(res.into_iter().collect()) + } +} + +fn highlight_exit_points( + sema: &Semantics<'_, RootDatabase>, + token: SyntaxToken, +) -> Option<Vec<HighlightedRange>> { + fn hl( + sema: &Semantics<'_, RootDatabase>, + body: Option<ast::Expr>, + ) -> Option<Vec<HighlightedRange>> { + let mut highlights = Vec::new(); + let body = body?; + walk_expr(&body, &mut |expr| match expr { + ast::Expr::ReturnExpr(expr) => { + if let Some(token) = expr.return_token() { + highlights.push(HighlightedRange { category: None, range: token.text_range() }); + } + } + ast::Expr::TryExpr(try_) => { + if let Some(token) = try_.question_mark_token() { + highlights.push(HighlightedRange { category: None, range: token.text_range() }); + } + } + ast::Expr::MethodCallExpr(_) | ast::Expr::CallExpr(_) | ast::Expr::MacroExpr(_) => { + if sema.type_of_expr(&expr).map_or(false, |ty| ty.original.is_never()) { + highlights.push(HighlightedRange { + category: None, + range: expr.syntax().text_range(), + }); + } + } + _ => (), + }); + let tail = match body { + ast::Expr::BlockExpr(b) => b.tail_expr(), + e => Some(e), + }; + + if let Some(tail) = tail { + for_each_tail_expr(&tail, &mut |tail| { + let range = match tail { + ast::Expr::BreakExpr(b) => b + .break_token() + .map_or_else(|| tail.syntax().text_range(), |tok| tok.text_range()), + _ => tail.syntax().text_range(), + }; + highlights.push(HighlightedRange { category: None, range }) + }); + } + Some(highlights) + } + for anc in token.parent_ancestors() { + return match_ast! { + match anc { + ast::Fn(fn_) => hl(sema, fn_.body().map(ast::Expr::BlockExpr)), + ast::ClosureExpr(closure) => hl(sema, closure.body()), + ast::BlockExpr(block_expr) => if matches!(block_expr.modifier(), Some(ast::BlockModifier::Async(_) | ast::BlockModifier::Try(_)| ast::BlockModifier::Const(_))) { + hl(sema, Some(block_expr.into())) + } else { + continue; + }, + _ => continue, + } + }; + } + None +} + +fn highlight_break_points(token: SyntaxToken) -> Option<Vec<HighlightedRange>> { + fn hl( + cursor_token_kind: SyntaxKind, + token: Option<SyntaxToken>, + label: Option<ast::Label>, + body: Option<ast::StmtList>, + ) -> Option<Vec<HighlightedRange>> { + let mut highlights = Vec::new(); + let range = cover_range( + token.map(|tok| tok.text_range()), + label.as_ref().map(|it| it.syntax().text_range()), + ); + highlights.extend(range.map(|range| HighlightedRange { category: None, range })); + for_each_break_and_continue_expr(label, body, &mut |expr| { + let range: Option<TextRange> = match (cursor_token_kind, expr) { + (T![for] | T![while] | T![loop] | T![break], ast::Expr::BreakExpr(break_)) => { + cover_range( + break_.break_token().map(|it| it.text_range()), + break_.lifetime().map(|it| it.syntax().text_range()), + ) + } + ( + T![for] | T![while] | T![loop] | T![continue], + ast::Expr::ContinueExpr(continue_), + ) => cover_range( + continue_.continue_token().map(|it| it.text_range()), + continue_.lifetime().map(|it| it.syntax().text_range()), + ), + _ => None, + }; + highlights.extend(range.map(|range| HighlightedRange { category: None, range })); + }); + Some(highlights) + } + let parent = token.parent()?; + let lbl = match_ast! { + match parent { + ast::BreakExpr(b) => b.lifetime(), + ast::ContinueExpr(c) => c.lifetime(), + ast::LoopExpr(l) => l.label().and_then(|it| it.lifetime()), + ast::ForExpr(f) => f.label().and_then(|it| it.lifetime()), + ast::WhileExpr(w) => w.label().and_then(|it| it.lifetime()), + ast::BlockExpr(b) => Some(b.label().and_then(|it| it.lifetime())?), + _ => return None, + } + }; + let lbl = lbl.as_ref(); + let label_matches = |def_lbl: Option<ast::Label>| match lbl { + Some(lbl) => { + Some(lbl.text()) == def_lbl.and_then(|it| it.lifetime()).as_ref().map(|it| it.text()) + } + None => true, + }; + let token_kind = token.kind(); + for anc in token.parent_ancestors().flat_map(ast::Expr::cast) { + return match anc { + ast::Expr::LoopExpr(l) if label_matches(l.label()) => hl( + token_kind, + l.loop_token(), + l.label(), + l.loop_body().and_then(|it| it.stmt_list()), + ), + ast::Expr::ForExpr(f) if label_matches(f.label()) => hl( + token_kind, + f.for_token(), + f.label(), + f.loop_body().and_then(|it| it.stmt_list()), + ), + ast::Expr::WhileExpr(w) if label_matches(w.label()) => hl( + token_kind, + w.while_token(), + w.label(), + w.loop_body().and_then(|it| it.stmt_list()), + ), + ast::Expr::BlockExpr(e) if e.label().is_some() && label_matches(e.label()) => { + hl(token_kind, None, e.label(), e.stmt_list()) + } + _ => continue, + }; + } + None +} + +fn highlight_yield_points(token: SyntaxToken) -> Option<Vec<HighlightedRange>> { + fn hl( + async_token: Option<SyntaxToken>, + body: Option<ast::Expr>, + ) -> Option<Vec<HighlightedRange>> { + let mut highlights = + vec![HighlightedRange { category: None, range: async_token?.text_range() }]; + if let Some(body) = body { + walk_expr(&body, &mut |expr| { + if let ast::Expr::AwaitExpr(expr) = expr { + if let Some(token) = expr.await_token() { + highlights + .push(HighlightedRange { category: None, range: token.text_range() }); + } + } + }); + } + Some(highlights) + } + for anc in token.parent_ancestors() { + return match_ast! { + match anc { + ast::Fn(fn_) => hl(fn_.async_token(), fn_.body().map(ast::Expr::BlockExpr)), + ast::BlockExpr(block_expr) => { + if block_expr.async_token().is_none() { + continue; + } + hl(block_expr.async_token(), Some(block_expr.into())) + }, + ast::ClosureExpr(closure) => hl(closure.async_token(), closure.body()), + _ => continue, + } + }; + } + None +} + +fn cover_range(r0: Option<TextRange>, r1: Option<TextRange>) -> Option<TextRange> { + match (r0, r1) { + (Some(r0), Some(r1)) => Some(r0.cover(r1)), + (Some(range), None) => Some(range), + (None, Some(range)) => Some(range), + (None, None) => None, + } +} + +fn find_defs(sema: &Semantics<'_, RootDatabase>, token: SyntaxToken) -> FxHashSet<Definition> { + sema.descend_into_macros(token) + .into_iter() + .filter_map(|token| IdentClass::classify_token(sema, &token).map(IdentClass::definitions)) + .flatten() + .collect() +} + +#[cfg(test)] +mod tests { + use crate::fixture; + + use super::*; + + #[track_caller] + fn check(ra_fixture: &str) { + let config = HighlightRelatedConfig { + break_points: true, + exit_points: true, + references: true, + yield_points: true, + }; + + check_with_config(ra_fixture, config); + } + + #[track_caller] + fn check_with_config(ra_fixture: &str, config: HighlightRelatedConfig) { + let (analysis, pos, annotations) = fixture::annotations(ra_fixture); + + let hls = analysis.highlight_related(config, pos).unwrap().unwrap_or_default(); + + let mut expected = annotations + .into_iter() + .map(|(r, access)| (r.range, (!access.is_empty()).then(|| access))) + .collect::<Vec<_>>(); + + let mut actual = hls + .into_iter() + .map(|hl| { + ( + hl.range, + hl.category.map(|it| { + match it { + ReferenceCategory::Read => "read", + ReferenceCategory::Write => "write", + } + .to_string() + }), + ) + }) + .collect::<Vec<_>>(); + actual.sort_by_key(|(range, _)| range.start()); + expected.sort_by_key(|(range, _)| range.start()); + + assert_eq!(expected, actual); + } + + #[test] + fn test_hl_tuple_fields() { + check( + r#" +struct Tuple(u32, u32); + +fn foo(t: Tuple) { + t.0$0; + // ^ read + t.0; + // ^ read +} +"#, + ); + } + + #[test] + fn test_hl_module() { + check( + r#" +//- /lib.rs +mod foo$0; + // ^^^ +//- /foo.rs +struct Foo; +"#, + ); + } + + #[test] + fn test_hl_self_in_crate_root() { + check( + r#" +use crate$0; + //^^^^^ +use self; + //^^^^ +mod __ { + use super; + //^^^^^ +} +"#, + ); + check( + r#" +//- /main.rs crate:main deps:lib +use lib$0; + //^^^ +//- /lib.rs crate:lib +"#, + ); + } + + #[test] + fn test_hl_self_in_module() { + check( + r#" +//- /lib.rs +mod foo; +//- /foo.rs +use self$0; + // ^^^^ +"#, + ); + } + + #[test] + fn test_hl_local() { + check( + r#" +fn foo() { + let mut bar = 3; + // ^^^ write + bar$0; + // ^^^ read +} +"#, + ); + } + + #[test] + fn test_hl_local_in_attr() { + check( + r#" +//- proc_macros: identity +#[proc_macros::identity] +fn foo() { + let mut bar = 3; + // ^^^ write + bar$0; + // ^^^ read +} +"#, + ); + } + + #[test] + fn test_multi_macro_usage() { + check( + r#" +macro_rules! foo { + ($ident:ident) => { + fn $ident() -> $ident { loop {} } + struct $ident; + } +} + +foo!(bar$0); + // ^^^ +fn foo() { + let bar: bar = bar(); + // ^^^ + // ^^^ +} +"#, + ); + check( + r#" +macro_rules! foo { + ($ident:ident) => { + fn $ident() -> $ident { loop {} } + struct $ident; + } +} + +foo!(bar); + // ^^^ +fn foo() { + let bar: bar$0 = bar(); + // ^^^ +} +"#, + ); + } + + #[test] + fn test_hl_yield_points() { + check( + r#" +pub async fn foo() { + // ^^^^^ + let x = foo() + .await$0 + // ^^^^^ + .await; + // ^^^^^ + || { 0.await }; + (async { 0.await }).await + // ^^^^^ +} +"#, + ); + } + + #[test] + fn test_hl_yield_points2() { + check( + r#" +pub async$0 fn foo() { + // ^^^^^ + let x = foo() + .await + // ^^^^^ + .await; + // ^^^^^ + || { 0.await }; + (async { 0.await }).await + // ^^^^^ +} +"#, + ); + } + + #[test] + fn test_hl_yield_nested_fn() { + check( + r#" +async fn foo() { + async fn foo2() { + // ^^^^^ + async fn foo3() { + 0.await + } + 0.await$0 + // ^^^^^ + } + 0.await +} +"#, + ); + } + + #[test] + fn test_hl_yield_nested_async_blocks() { + check( + r#" +async fn foo() { + (async { + // ^^^^^ + (async { + 0.await + }).await$0 } + // ^^^^^ + ).await; +} +"#, + ); + } + + #[test] + fn test_hl_exit_points() { + check( + r#" +fn foo() -> u32 { + if true { + return$0 0; + // ^^^^^^ + } + + 0?; + // ^ + 0xDEAD_BEEF + // ^^^^^^^^^^^ +} +"#, + ); + } + + #[test] + fn test_hl_exit_points2() { + check( + r#" +fn foo() ->$0 u32 { + if true { + return 0; + // ^^^^^^ + } + + 0?; + // ^ + 0xDEAD_BEEF + // ^^^^^^^^^^^ +} +"#, + ); + } + + #[test] + fn test_hl_exit_points3() { + check( + r#" +fn$0 foo() -> u32 { + if true { + return 0; + // ^^^^^^ + } + + 0?; + // ^ + 0xDEAD_BEEF + // ^^^^^^^^^^^ +} +"#, + ); + } + + #[test] + fn test_hl_prefer_ref_over_tail_exit() { + check( + r#" +fn foo() -> u32 { +// ^^^ + if true { + return 0; + } + + 0?; + + foo$0() + // ^^^ +} +"#, + ); + } + + #[test] + fn test_hl_never_call_is_exit_point() { + check( + r#" +struct Never; +impl Never { + fn never(self) -> ! { loop {} } +} +macro_rules! never { + () => { never() } +} +fn never() -> ! { loop {} } +fn foo() ->$0 u32 { + never(); + // ^^^^^^^ + never!(); + // ^^^^^^^^ + + Never.never(); + // ^^^^^^^^^^^^^ + + 0 + // ^ +} +"#, + ); + } + + #[test] + fn test_hl_inner_tail_exit_points() { + check( + r#" +fn foo() ->$0 u32 { + if true { + unsafe { + return 5; + // ^^^^^^ + 5 + // ^ + } + } else if false { + 0 + // ^ + } else { + match 5 { + 6 => 100, + // ^^^ + 7 => loop { + break 5; + // ^^^^^ + } + 8 => 'a: loop { + 'b: loop { + break 'a 5; + // ^^^^^ + break 'b 5; + break 5; + }; + } + // + _ => 500, + // ^^^ + } + } +} +"#, + ); + } + + #[test] + fn test_hl_inner_tail_exit_points_labeled_block() { + check( + r#" +fn foo() ->$0 u32 { + 'foo: { + break 'foo 0; + // ^^^^^ + loop { + break; + break 'foo 0; + // ^^^^^ + } + 0 + // ^ + } +} +"#, + ); + } + + #[test] + fn test_hl_break_loop() { + check( + r#" +fn foo() { + 'outer: loop { + // ^^^^^^^^^^^^ + break; + // ^^^^^ + 'inner: loop { + break; + 'innermost: loop { + break 'outer; + // ^^^^^^^^^^^^ + break 'inner; + } + break$0 'outer; + // ^^^^^^^^^^^^ + break; + } + break; + // ^^^^^ + } +} +"#, + ); + } + + #[test] + fn test_hl_break_loop2() { + check( + r#" +fn foo() { + 'outer: loop { + break; + 'inner: loop { + // ^^^^^^^^^^^^ + break; + // ^^^^^ + 'innermost: loop { + break 'outer; + break 'inner; + // ^^^^^^^^^^^^ + } + break 'outer; + break$0; + // ^^^^^ + } + break; + } +} +"#, + ); + } + + #[test] + fn test_hl_break_for() { + check( + r#" +fn foo() { + 'outer: for _ in () { + // ^^^^^^^^^^^ + break; + // ^^^^^ + 'inner: for _ in () { + break; + 'innermost: for _ in () { + break 'outer; + // ^^^^^^^^^^^^ + break 'inner; + } + break$0 'outer; + // ^^^^^^^^^^^^ + break; + } + break; + // ^^^^^ + } +} +"#, + ); + } + + #[test] + fn test_hl_break_for_but_not_continue() { + check( + r#" +fn foo() { + 'outer: for _ in () { + // ^^^^^^^^^^^ + break; + // ^^^^^ + continue; + 'inner: for _ in () { + break; + continue; + 'innermost: for _ in () { + continue 'outer; + break 'outer; + // ^^^^^^^^^^^^ + continue 'inner; + break 'inner; + } + break$0 'outer; + // ^^^^^^^^^^^^ + continue 'outer; + break; + continue; + } + break; + // ^^^^^ + continue; + } +} +"#, + ); + } + + #[test] + fn test_hl_continue_for_but_not_break() { + check( + r#" +fn foo() { + 'outer: for _ in () { + // ^^^^^^^^^^^ + break; + continue; + // ^^^^^^^^ + 'inner: for _ in () { + break; + continue; + 'innermost: for _ in () { + continue 'outer; + // ^^^^^^^^^^^^^^^ + break 'outer; + continue 'inner; + break 'inner; + } + break 'outer; + continue$0 'outer; + // ^^^^^^^^^^^^^^^ + break; + continue; + } + break; + continue; + // ^^^^^^^^ + } +} +"#, + ); + } + + #[test] + fn test_hl_break_and_continue() { + check( + r#" +fn foo() { + 'outer: fo$0r _ in () { + // ^^^^^^^^^^^ + break; + // ^^^^^ + continue; + // ^^^^^^^^ + 'inner: for _ in () { + break; + continue; + 'innermost: for _ in () { + continue 'outer; + // ^^^^^^^^^^^^^^^ + break 'outer; + // ^^^^^^^^^^^^ + continue 'inner; + break 'inner; + } + break 'outer; + // ^^^^^^^^^^^^ + continue 'outer; + // ^^^^^^^^^^^^^^^ + break; + continue; + } + break; + // ^^^^^ + continue; + // ^^^^^^^^ + } +} +"#, + ); + } + + #[test] + fn test_hl_break_while() { + check( + r#" +fn foo() { + 'outer: while true { + // ^^^^^^^^^^^^^ + break; + // ^^^^^ + 'inner: while true { + break; + 'innermost: while true { + break 'outer; + // ^^^^^^^^^^^^ + break 'inner; + } + break$0 'outer; + // ^^^^^^^^^^^^ + break; + } + break; + // ^^^^^ + } +} +"#, + ); + } + + #[test] + fn test_hl_break_labeled_block() { + check( + r#" +fn foo() { + 'outer: { + // ^^^^^^^ + break; + // ^^^^^ + 'inner: { + break; + 'innermost: { + break 'outer; + // ^^^^^^^^^^^^ + break 'inner; + } + break$0 'outer; + // ^^^^^^^^^^^^ + break; + } + break; + // ^^^^^ + } +} +"#, + ); + } + + #[test] + fn test_hl_break_unlabeled_loop() { + check( + r#" +fn foo() { + loop { + // ^^^^ + break$0; + // ^^^^^ + } +} +"#, + ); + } + + #[test] + fn test_hl_break_unlabeled_block_in_loop() { + check( + r#" +fn foo() { + loop { + // ^^^^ + { + break$0; + // ^^^^^ + } + } +} +"#, + ); + } + + #[test] + fn test_hl_field_shorthand() { + check( + r#" +struct Struct { field: u32 } + //^^^^^ +fn function(field: u32) { + //^^^^^ + Struct { field$0 } + //^^^^^ read +} +"#, + ); + } + + #[test] + fn test_hl_disabled_ref_local() { + let config = HighlightRelatedConfig { + references: false, + break_points: true, + exit_points: true, + yield_points: true, + }; + + check_with_config( + r#" +fn foo() { + let x$0 = 5; + let y = x * 2; +} +"#, + config, + ); + } + + #[test] + fn test_hl_disabled_ref_local_preserved_break() { + let config = HighlightRelatedConfig { + references: false, + break_points: true, + exit_points: true, + yield_points: true, + }; + + check_with_config( + r#" +fn foo() { + let x$0 = 5; + let y = x * 2; + + loop { + break; + } +} +"#, + config.clone(), + ); + + check_with_config( + r#" +fn foo() { + let x = 5; + let y = x * 2; + + loop$0 { +// ^^^^ + break; +// ^^^^^ + } +} +"#, + config, + ); + } + + #[test] + fn test_hl_disabled_ref_local_preserved_yield() { + let config = HighlightRelatedConfig { + references: false, + break_points: true, + exit_points: true, + yield_points: true, + }; + + check_with_config( + r#" +async fn foo() { + let x$0 = 5; + let y = x * 2; + + 0.await; +} +"#, + config.clone(), + ); + + check_with_config( + r#" + async fn foo() { +// ^^^^^ + let x = 5; + let y = x * 2; + + 0.await$0; +// ^^^^^ +} +"#, + config, + ); + } + + #[test] + fn test_hl_disabled_ref_local_preserved_exit() { + let config = HighlightRelatedConfig { + references: false, + break_points: true, + exit_points: true, + yield_points: true, + }; + + check_with_config( + r#" +fn foo() -> i32 { + let x$0 = 5; + let y = x * 2; + + if true { + return y; + } + + 0? +} +"#, + config.clone(), + ); + + check_with_config( + r#" +fn foo() ->$0 i32 { + let x = 5; + let y = x * 2; + + if true { + return y; +// ^^^^^^ + } + + 0? +// ^ +"#, + config, + ); + } + + #[test] + fn test_hl_disabled_break() { + let config = HighlightRelatedConfig { + references: true, + break_points: false, + exit_points: true, + yield_points: true, + }; + + check_with_config( + r#" +fn foo() { + loop { + break$0; + } +} +"#, + config, + ); + } + + #[test] + fn test_hl_disabled_yield() { + let config = HighlightRelatedConfig { + references: true, + break_points: true, + exit_points: true, + yield_points: false, + }; + + check_with_config( + r#" +async$0 fn foo() { + 0.await; +} +"#, + config, + ); + } + + #[test] + fn test_hl_disabled_exit() { + let config = HighlightRelatedConfig { + references: true, + break_points: true, + exit_points: false, + yield_points: true, + }; + + check_with_config( + r#" +fn foo() ->$0 i32 { + if true { + return -1; + } + + 42 +}"#, + config, + ); + } + + #[test] + fn test_hl_multi_local() { + check( + r#" +fn foo(( + foo$0 + //^^^ + | foo + //^^^ + | foo + //^^^ +): ()) { + foo; + //^^^read + let foo; +} +"#, + ); + check( + r#" +fn foo(( + foo + //^^^ + | foo$0 + //^^^ + | foo + //^^^ +): ()) { + foo; + //^^^read + let foo; +} +"#, + ); + check( + r#" +fn foo(( + foo + //^^^ + | foo + //^^^ + | foo + //^^^ +): ()) { + foo$0; + //^^^read + let foo; +} +"#, + ); + } + + #[test] + fn test_hl_trait_impl_methods() { + check( + r#" +trait Trait { + fn func$0(self) {} + //^^^^ +} + +impl Trait for () { + fn func(self) {} + //^^^^ +} + +fn main() { + <()>::func(()); + //^^^^ + ().func(); + //^^^^ +} +"#, + ); + check( + r#" +trait Trait { + fn func(self) {} + //^^^^ +} + +impl Trait for () { + fn func$0(self) {} + //^^^^ +} + +fn main() { + <()>::func(()); + //^^^^ + ().func(); + //^^^^ +} +"#, + ); + check( + r#" +trait Trait { + fn func(self) {} + //^^^^ +} + +impl Trait for () { + fn func(self) {} + //^^^^ +} + +fn main() { + <()>::func(()); + //^^^^ + ().func$0(); + //^^^^ +} +"#, + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/hover.rs b/src/tools/rust-analyzer/crates/ide/src/hover.rs new file mode 100644 index 000000000..59c97f2dc --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/hover.rs @@ -0,0 +1,390 @@ +mod render; + +#[cfg(test)] +mod tests; + +use std::iter; + +use either::Either; +use hir::{HasSource, Semantics}; +use ide_db::{ + base_db::FileRange, + defs::{Definition, IdentClass}, + famous_defs::FamousDefs, + helpers::pick_best_token, + FxIndexSet, RootDatabase, +}; +use itertools::Itertools; +use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxNode, SyntaxToken, T}; + +use crate::{ + doc_links::token_as_doc_comment, + markup::Markup, + runnables::{runnable_fn, runnable_mod}, + FileId, FilePosition, NavigationTarget, RangeInfo, Runnable, TryToNav, +}; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HoverConfig { + pub links_in_hover: bool, + pub documentation: Option<HoverDocFormat>, +} + +impl HoverConfig { + fn markdown(&self) -> bool { + matches!(self.documentation, Some(HoverDocFormat::Markdown)) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum HoverDocFormat { + Markdown, + PlainText, +} + +#[derive(Debug, Clone)] +pub enum HoverAction { + Runnable(Runnable), + Implementation(FilePosition), + Reference(FilePosition), + GoToType(Vec<HoverGotoTypeData>), +} + +impl HoverAction { + fn goto_type_from_targets(db: &RootDatabase, targets: Vec<hir::ModuleDef>) -> Self { + let targets = targets + .into_iter() + .filter_map(|it| { + Some(HoverGotoTypeData { + mod_path: render::path( + db, + it.module(db)?, + it.name(db).map(|name| name.to_string()), + ), + nav: it.try_to_nav(db)?, + }) + }) + .collect(); + HoverAction::GoToType(targets) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct HoverGotoTypeData { + pub mod_path: String, + pub nav: NavigationTarget, +} + +/// Contains the results when hovering over an item +#[derive(Debug, Default)] +pub struct HoverResult { + pub markup: Markup, + pub actions: Vec<HoverAction>, +} + +// Feature: Hover +// +// Shows additional information, like the type of an expression or the documentation for a definition when "focusing" code. +// Focusing is usually hovering with a mouse, but can also be triggered with a shortcut. +// +// image::https://user-images.githubusercontent.com/48062697/113020658-b5f98b80-917a-11eb-9f88-3dbc27320c95.gif[] +pub(crate) fn hover( + db: &RootDatabase, + FileRange { file_id, range }: FileRange, + config: &HoverConfig, +) -> Option<RangeInfo<HoverResult>> { + let sema = &hir::Semantics::new(db); + let file = sema.parse(file_id).syntax().clone(); + + if !range.is_empty() { + return hover_ranged(&file, range, sema, config); + } + let offset = range.start(); + + let original_token = pick_best_token(file.token_at_offset(offset), |kind| match kind { + IDENT | INT_NUMBER | LIFETIME_IDENT | T![self] | T![super] | T![crate] | T![Self] => 3, + T!['('] | T![')'] => 2, + kind if kind.is_trivia() => 0, + _ => 1, + })?; + + if let Some(doc_comment) = token_as_doc_comment(&original_token) { + cov_mark::hit!(no_highlight_on_comment_hover); + return doc_comment.get_definition_with_descend_at(sema, offset, |def, node, range| { + let res = hover_for_definition(sema, file_id, def, &node, config)?; + Some(RangeInfo::new(range, res)) + }); + } + + let in_attr = matches!(original_token.parent().and_then(ast::TokenTree::cast), Some(tt) if tt.syntax().ancestors().any(|it| ast::Meta::can_cast(it.kind()))); + let descended = if in_attr { + [sema.descend_into_macros_with_kind_preference(original_token.clone())].into() + } else { + sema.descend_into_macros_with_same_text(original_token.clone()) + }; + + // FIXME: Definition should include known lints and the like instead of having this special case here + let hovered_lint = descended.iter().find_map(|token| { + let attr = token.parent_ancestors().find_map(ast::Attr::cast)?; + render::try_for_lint(&attr, token) + }); + if let Some(res) = hovered_lint { + return Some(RangeInfo::new(original_token.text_range(), res)); + } + + let result = descended + .iter() + .filter_map(|token| { + let node = token.parent()?; + let class = IdentClass::classify_token(sema, token)?; + Some(class.definitions().into_iter().zip(iter::once(node).cycle())) + }) + .flatten() + .unique_by(|&(def, _)| def) + .filter_map(|(def, node)| hover_for_definition(sema, file_id, def, &node, config)) + .reduce(|mut acc: HoverResult, HoverResult { markup, actions }| { + acc.actions.extend(actions); + acc.markup = Markup::from(format!("{}\n---\n{}", acc.markup, markup)); + acc + }); + + if result.is_none() { + // fallbacks, show keywords or types + + let res = descended.iter().find_map(|token| render::keyword(sema, config, token)); + if let Some(res) = res { + return Some(RangeInfo::new(original_token.text_range(), res)); + } + let res = descended + .iter() + .find_map(|token| hover_type_fallback(sema, config, token, &original_token)); + if let Some(_) = res { + return res; + } + } + result.map(|mut res: HoverResult| { + res.actions = dedupe_or_merge_hover_actions(res.actions); + RangeInfo::new(original_token.text_range(), res) + }) +} + +pub(crate) fn hover_for_definition( + sema: &Semantics<'_, RootDatabase>, + file_id: FileId, + definition: Definition, + node: &SyntaxNode, + config: &HoverConfig, +) -> Option<HoverResult> { + let famous_defs = match &definition { + Definition::BuiltinType(_) => Some(FamousDefs(sema, sema.scope(node)?.krate())), + _ => None, + }; + render::definition(sema.db, definition, famous_defs.as_ref(), config).map(|markup| { + HoverResult { + markup: render::process_markup(sema.db, definition, &markup, config), + actions: show_implementations_action(sema.db, definition) + .into_iter() + .chain(show_fn_references_action(sema.db, definition)) + .chain(runnable_action(sema, definition, file_id)) + .chain(goto_type_action_for_def(sema.db, definition)) + .collect(), + } + }) +} + +fn hover_ranged( + file: &SyntaxNode, + range: syntax::TextRange, + sema: &Semantics<'_, RootDatabase>, + config: &HoverConfig, +) -> Option<RangeInfo<HoverResult>> { + // FIXME: make this work in attributes + let expr_or_pat = file.covering_element(range).ancestors().find_map(|it| { + match_ast! { + match it { + ast::Expr(expr) => Some(Either::Left(expr)), + ast::Pat(pat) => Some(Either::Right(pat)), + _ => None, + } + } + })?; + let res = match &expr_or_pat { + Either::Left(ast::Expr::TryExpr(try_expr)) => render::try_expr(sema, config, try_expr), + Either::Left(ast::Expr::PrefixExpr(prefix_expr)) + if prefix_expr.op_kind() == Some(ast::UnaryOp::Deref) => + { + render::deref_expr(sema, config, prefix_expr) + } + _ => None, + }; + let res = res.or_else(|| render::type_info(sema, config, &expr_or_pat)); + res.map(|it| { + let range = match expr_or_pat { + Either::Left(it) => it.syntax().text_range(), + Either::Right(it) => it.syntax().text_range(), + }; + RangeInfo::new(range, it) + }) +} + +fn hover_type_fallback( + sema: &Semantics<'_, RootDatabase>, + config: &HoverConfig, + token: &SyntaxToken, + original_token: &SyntaxToken, +) -> Option<RangeInfo<HoverResult>> { + let node = token + .parent_ancestors() + .take_while(|it| !ast::Item::can_cast(it.kind())) + .find(|n| ast::Expr::can_cast(n.kind()) || ast::Pat::can_cast(n.kind()))?; + + let expr_or_pat = match_ast! { + match node { + ast::Expr(it) => Either::Left(it), + ast::Pat(it) => Either::Right(it), + // If this node is a MACRO_CALL, it means that `descend_into_macros_many` failed to resolve. + // (e.g expanding a builtin macro). So we give up here. + ast::MacroCall(_it) => return None, + _ => return None, + } + }; + + let res = render::type_info(sema, config, &expr_or_pat)?; + let range = sema + .original_range_opt(&node) + .map(|frange| frange.range) + .unwrap_or_else(|| original_token.text_range()); + Some(RangeInfo::new(range, res)) +} + +fn show_implementations_action(db: &RootDatabase, def: Definition) -> Option<HoverAction> { + fn to_action(nav_target: NavigationTarget) -> HoverAction { + HoverAction::Implementation(FilePosition { + file_id: nav_target.file_id, + offset: nav_target.focus_or_full_range().start(), + }) + } + + let adt = match def { + Definition::Trait(it) => return it.try_to_nav(db).map(to_action), + Definition::Adt(it) => Some(it), + Definition::SelfType(it) => it.self_ty(db).as_adt(), + _ => None, + }?; + adt.try_to_nav(db).map(to_action) +} + +fn show_fn_references_action(db: &RootDatabase, def: Definition) -> Option<HoverAction> { + match def { + Definition::Function(it) => it.try_to_nav(db).map(|nav_target| { + HoverAction::Reference(FilePosition { + file_id: nav_target.file_id, + offset: nav_target.focus_or_full_range().start(), + }) + }), + _ => None, + } +} + +fn runnable_action( + sema: &hir::Semantics<'_, RootDatabase>, + def: Definition, + file_id: FileId, +) -> Option<HoverAction> { + match def { + Definition::Module(it) => runnable_mod(sema, it).map(HoverAction::Runnable), + Definition::Function(func) => { + let src = func.source(sema.db)?; + if src.file_id != file_id.into() { + cov_mark::hit!(hover_macro_generated_struct_fn_doc_comment); + cov_mark::hit!(hover_macro_generated_struct_fn_doc_attr); + return None; + } + + runnable_fn(sema, func).map(HoverAction::Runnable) + } + _ => None, + } +} + +fn goto_type_action_for_def(db: &RootDatabase, def: Definition) -> Option<HoverAction> { + let mut targets: Vec<hir::ModuleDef> = Vec::new(); + let mut push_new_def = |item: hir::ModuleDef| { + if !targets.contains(&item) { + targets.push(item); + } + }; + + if let Definition::GenericParam(hir::GenericParam::TypeParam(it)) = def { + it.trait_bounds(db).into_iter().for_each(|it| push_new_def(it.into())); + } else { + let ty = match def { + Definition::Local(it) => it.ty(db), + Definition::GenericParam(hir::GenericParam::ConstParam(it)) => it.ty(db), + Definition::Field(field) => field.ty(db), + Definition::Function(function) => function.ret_type(db), + _ => return None, + }; + + walk_and_push_ty(db, &ty, &mut push_new_def); + } + + Some(HoverAction::goto_type_from_targets(db, targets)) +} + +fn walk_and_push_ty( + db: &RootDatabase, + ty: &hir::Type, + push_new_def: &mut dyn FnMut(hir::ModuleDef), +) { + ty.walk(db, |t| { + if let Some(adt) = t.as_adt() { + push_new_def(adt.into()); + } else if let Some(trait_) = t.as_dyn_trait() { + push_new_def(trait_.into()); + } else if let Some(traits) = t.as_impl_traits(db) { + traits.for_each(|it| push_new_def(it.into())); + } else if let Some(trait_) = t.as_associated_type_parent_trait(db) { + push_new_def(trait_.into()); + } + }); +} + +fn dedupe_or_merge_hover_actions(actions: Vec<HoverAction>) -> Vec<HoverAction> { + let mut deduped_actions = Vec::with_capacity(actions.len()); + let mut go_to_type_targets = FxIndexSet::default(); + + let mut seen_implementation = false; + let mut seen_reference = false; + let mut seen_runnable = false; + for action in actions { + match action { + HoverAction::GoToType(targets) => { + go_to_type_targets.extend(targets); + } + HoverAction::Implementation(..) => { + if !seen_implementation { + seen_implementation = true; + deduped_actions.push(action); + } + } + HoverAction::Reference(..) => { + if !seen_reference { + seen_reference = true; + deduped_actions.push(action); + } + } + HoverAction::Runnable(..) => { + if !seen_runnable { + seen_runnable = true; + deduped_actions.push(action); + } + } + }; + } + + if !go_to_type_targets.is_empty() { + deduped_actions.push(HoverAction::GoToType(go_to_type_targets.into_iter().collect())); + } + + deduped_actions +} diff --git a/src/tools/rust-analyzer/crates/ide/src/hover/render.rs b/src/tools/rust-analyzer/crates/ide/src/hover/render.rs new file mode 100644 index 000000000..6c50a4e6a --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/hover/render.rs @@ -0,0 +1,563 @@ +//! Logic for rendering the different hover messages +use std::fmt::Display; + +use either::Either; +use hir::{AsAssocItem, AttributeTemplate, HasAttrs, HirDisplay, Semantics, TypeInfo}; +use ide_db::{ + base_db::SourceDatabase, + defs::Definition, + famous_defs::FamousDefs, + generated::lints::{CLIPPY_LINTS, DEFAULT_LINTS, FEATURES}, + RootDatabase, +}; +use itertools::Itertools; +use stdx::format_to; +use syntax::{ + algo, ast, match_ast, AstNode, Direction, + SyntaxKind::{LET_EXPR, LET_STMT}, + SyntaxToken, T, +}; + +use crate::{ + doc_links::{remove_links, rewrite_links}, + hover::walk_and_push_ty, + markdown_remove::remove_markdown, + HoverAction, HoverConfig, HoverResult, Markup, +}; + +pub(super) fn type_info( + sema: &Semantics<'_, RootDatabase>, + config: &HoverConfig, + expr_or_pat: &Either<ast::Expr, ast::Pat>, +) -> Option<HoverResult> { + let TypeInfo { original, adjusted } = match expr_or_pat { + Either::Left(expr) => sema.type_of_expr(expr)?, + Either::Right(pat) => sema.type_of_pat(pat)?, + }; + + let mut res = HoverResult::default(); + let mut targets: Vec<hir::ModuleDef> = Vec::new(); + let mut push_new_def = |item: hir::ModuleDef| { + if !targets.contains(&item) { + targets.push(item); + } + }; + walk_and_push_ty(sema.db, &original, &mut push_new_def); + + res.markup = if let Some(adjusted_ty) = adjusted { + walk_and_push_ty(sema.db, &adjusted_ty, &mut push_new_def); + let original = original.display(sema.db).to_string(); + let adjusted = adjusted_ty.display(sema.db).to_string(); + let static_text_diff_len = "Coerced to: ".len() - "Type: ".len(); + format!( + "{bt_start}Type: {:>apad$}\nCoerced to: {:>opad$}\n{bt_end}", + original, + adjusted, + apad = static_text_diff_len + adjusted.len().max(original.len()), + opad = original.len(), + bt_start = if config.markdown() { "```text\n" } else { "" }, + bt_end = if config.markdown() { "```\n" } else { "" } + ) + .into() + } else { + if config.markdown() { + Markup::fenced_block(&original.display(sema.db)) + } else { + original.display(sema.db).to_string().into() + } + }; + res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets)); + Some(res) +} + +pub(super) fn try_expr( + sema: &Semantics<'_, RootDatabase>, + config: &HoverConfig, + try_expr: &ast::TryExpr, +) -> Option<HoverResult> { + let inner_ty = sema.type_of_expr(&try_expr.expr()?)?.original; + let mut ancestors = try_expr.syntax().ancestors(); + let mut body_ty = loop { + let next = ancestors.next()?; + break match_ast! { + match next { + ast::Fn(fn_) => sema.to_def(&fn_)?.ret_type(sema.db), + ast::Item(__) => return None, + ast::ClosureExpr(closure) => sema.type_of_expr(&closure.body()?)?.original, + ast::BlockExpr(block_expr) => if matches!(block_expr.modifier(), Some(ast::BlockModifier::Async(_) | ast::BlockModifier::Try(_)| ast::BlockModifier::Const(_))) { + sema.type_of_expr(&block_expr.into())?.original + } else { + continue; + }, + _ => continue, + } + }; + }; + + if inner_ty == body_ty { + return None; + } + + let mut inner_ty = inner_ty; + let mut s = "Try Target".to_owned(); + + let adts = inner_ty.as_adt().zip(body_ty.as_adt()); + if let Some((hir::Adt::Enum(inner), hir::Adt::Enum(body))) = adts { + let famous_defs = FamousDefs(sema, sema.scope(try_expr.syntax())?.krate()); + // special case for two options, there is no value in showing them + if let Some(option_enum) = famous_defs.core_option_Option() { + if inner == option_enum && body == option_enum { + cov_mark::hit!(hover_try_expr_opt_opt); + return None; + } + } + + // special case two results to show the error variants only + if let Some(result_enum) = famous_defs.core_result_Result() { + if inner == result_enum && body == result_enum { + let error_type_args = + inner_ty.type_arguments().nth(1).zip(body_ty.type_arguments().nth(1)); + if let Some((inner, body)) = error_type_args { + inner_ty = inner; + body_ty = body; + s = "Try Error".to_owned(); + } + } + } + } + + let mut res = HoverResult::default(); + + let mut targets: Vec<hir::ModuleDef> = Vec::new(); + let mut push_new_def = |item: hir::ModuleDef| { + if !targets.contains(&item) { + targets.push(item); + } + }; + walk_and_push_ty(sema.db, &inner_ty, &mut push_new_def); + walk_and_push_ty(sema.db, &body_ty, &mut push_new_def); + res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets)); + + let inner_ty = inner_ty.display(sema.db).to_string(); + let body_ty = body_ty.display(sema.db).to_string(); + let ty_len_max = inner_ty.len().max(body_ty.len()); + + let l = "Propagated as: ".len() - " Type: ".len(); + let static_text_len_diff = l as isize - s.len() as isize; + let tpad = static_text_len_diff.max(0) as usize; + let ppad = static_text_len_diff.min(0).abs() as usize; + + res.markup = format!( + "{bt_start}{} Type: {:>pad0$}\nPropagated as: {:>pad1$}\n{bt_end}", + s, + inner_ty, + body_ty, + pad0 = ty_len_max + tpad, + pad1 = ty_len_max + ppad, + bt_start = if config.markdown() { "```text\n" } else { "" }, + bt_end = if config.markdown() { "```\n" } else { "" } + ) + .into(); + Some(res) +} + +pub(super) fn deref_expr( + sema: &Semantics<'_, RootDatabase>, + config: &HoverConfig, + deref_expr: &ast::PrefixExpr, +) -> Option<HoverResult> { + let inner_ty = sema.type_of_expr(&deref_expr.expr()?)?.original; + let TypeInfo { original, adjusted } = + sema.type_of_expr(&ast::Expr::from(deref_expr.clone()))?; + + let mut res = HoverResult::default(); + let mut targets: Vec<hir::ModuleDef> = Vec::new(); + let mut push_new_def = |item: hir::ModuleDef| { + if !targets.contains(&item) { + targets.push(item); + } + }; + walk_and_push_ty(sema.db, &inner_ty, &mut push_new_def); + walk_and_push_ty(sema.db, &original, &mut push_new_def); + + res.markup = if let Some(adjusted_ty) = adjusted { + walk_and_push_ty(sema.db, &adjusted_ty, &mut push_new_def); + let original = original.display(sema.db).to_string(); + let adjusted = adjusted_ty.display(sema.db).to_string(); + let inner = inner_ty.display(sema.db).to_string(); + let type_len = "To type: ".len(); + let coerced_len = "Coerced to: ".len(); + let deref_len = "Dereferenced from: ".len(); + let max_len = (original.len() + type_len) + .max(adjusted.len() + coerced_len) + .max(inner.len() + deref_len); + format!( + "{bt_start}Dereferenced from: {:>ipad$}\nTo type: {:>apad$}\nCoerced to: {:>opad$}\n{bt_end}", + inner, + original, + adjusted, + ipad = max_len - deref_len, + apad = max_len - type_len, + opad = max_len - coerced_len, + bt_start = if config.markdown() { "```text\n" } else { "" }, + bt_end = if config.markdown() { "```\n" } else { "" } + ) + .into() + } else { + let original = original.display(sema.db).to_string(); + let inner = inner_ty.display(sema.db).to_string(); + let type_len = "To type: ".len(); + let deref_len = "Dereferenced from: ".len(); + let max_len = (original.len() + type_len).max(inner.len() + deref_len); + format!( + "{bt_start}Dereferenced from: {:>ipad$}\nTo type: {:>apad$}\n{bt_end}", + inner, + original, + ipad = max_len - deref_len, + apad = max_len - type_len, + bt_start = if config.markdown() { "```text\n" } else { "" }, + bt_end = if config.markdown() { "```\n" } else { "" } + ) + .into() + }; + res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets)); + + Some(res) +} + +pub(super) fn keyword( + sema: &Semantics<'_, RootDatabase>, + config: &HoverConfig, + token: &SyntaxToken, +) -> Option<HoverResult> { + if !token.kind().is_keyword() || !config.documentation.is_some() { + return None; + } + let parent = token.parent()?; + let famous_defs = FamousDefs(sema, sema.scope(&parent)?.krate()); + + let KeywordHint { description, keyword_mod, actions } = keyword_hints(sema, token, parent); + + let doc_owner = find_std_module(&famous_defs, &keyword_mod)?; + let docs = doc_owner.attrs(sema.db).docs()?; + let markup = process_markup( + sema.db, + Definition::Module(doc_owner), + &markup(Some(docs.into()), description, None)?, + config, + ); + Some(HoverResult { markup, actions }) +} + +pub(super) fn try_for_lint(attr: &ast::Attr, token: &SyntaxToken) -> Option<HoverResult> { + let (path, tt) = attr.as_simple_call()?; + if !tt.syntax().text_range().contains(token.text_range().start()) { + return None; + } + let (is_clippy, lints) = match &*path { + "feature" => (false, FEATURES), + "allow" | "deny" | "forbid" | "warn" => { + let is_clippy = algo::non_trivia_sibling(token.clone().into(), Direction::Prev) + .filter(|t| t.kind() == T![:]) + .and_then(|t| algo::non_trivia_sibling(t, Direction::Prev)) + .filter(|t| t.kind() == T![:]) + .and_then(|t| algo::non_trivia_sibling(t, Direction::Prev)) + .map_or(false, |t| { + t.kind() == T![ident] && t.into_token().map_or(false, |t| t.text() == "clippy") + }); + if is_clippy { + (true, CLIPPY_LINTS) + } else { + (false, DEFAULT_LINTS) + } + } + _ => return None, + }; + + let tmp; + let needle = if is_clippy { + tmp = format!("clippy::{}", token.text()); + &tmp + } else { + &*token.text() + }; + + let lint = + lints.binary_search_by_key(&needle, |lint| lint.label).ok().map(|idx| &lints[idx])?; + Some(HoverResult { + markup: Markup::from(format!("```\n{}\n```\n___\n\n{}", lint.label, lint.description)), + ..Default::default() + }) +} + +pub(super) fn process_markup( + db: &RootDatabase, + def: Definition, + markup: &Markup, + config: &HoverConfig, +) -> Markup { + let markup = markup.as_str(); + let markup = if !config.markdown() { + remove_markdown(markup) + } else if config.links_in_hover { + rewrite_links(db, markup, def) + } else { + remove_links(markup) + }; + Markup::from(markup) +} + +fn definition_owner_name(db: &RootDatabase, def: &Definition) -> Option<String> { + match def { + Definition::Field(f) => Some(f.parent_def(db).name(db)), + Definition::Local(l) => l.parent(db).name(db), + Definition::Function(f) => match f.as_assoc_item(db)?.container(db) { + hir::AssocItemContainer::Trait(t) => Some(t.name(db)), + hir::AssocItemContainer::Impl(i) => i.self_ty(db).as_adt().map(|adt| adt.name(db)), + }, + Definition::Variant(e) => Some(e.parent_enum(db).name(db)), + _ => None, + } + .map(|name| name.to_string()) +} + +pub(super) fn path(db: &RootDatabase, module: hir::Module, item_name: Option<String>) -> String { + let crate_name = + db.crate_graph()[module.krate().into()].display_name.as_ref().map(|it| it.to_string()); + let module_path = module + .path_to_root(db) + .into_iter() + .rev() + .flat_map(|it| it.name(db).map(|name| name.to_string())); + crate_name.into_iter().chain(module_path).chain(item_name).join("::") +} + +pub(super) fn definition( + db: &RootDatabase, + def: Definition, + famous_defs: Option<&FamousDefs<'_, '_>>, + config: &HoverConfig, +) -> Option<Markup> { + let mod_path = definition_mod_path(db, &def); + let (label, docs) = match def { + Definition::Macro(it) => label_and_docs(db, it), + Definition::Field(it) => label_and_docs(db, it), + Definition::Module(it) => label_and_docs(db, it), + Definition::Function(it) => label_and_docs(db, it), + Definition::Adt(it) => label_and_docs(db, it), + Definition::Variant(it) => label_and_docs(db, it), + Definition::Const(it) => label_value_and_docs(db, it, |it| { + let body = it.eval(db); + match body { + Ok(x) => Some(format!("{}", x)), + Err(_) => it.value(db).map(|x| format!("{}", x)), + } + }), + Definition::Static(it) => label_value_and_docs(db, it, |it| it.value(db)), + Definition::Trait(it) => label_and_docs(db, it), + Definition::TypeAlias(it) => label_and_docs(db, it), + Definition::BuiltinType(it) => { + return famous_defs + .and_then(|fd| builtin(fd, it)) + .or_else(|| Some(Markup::fenced_block(&it.name()))) + } + Definition::Local(it) => return local(db, it), + Definition::SelfType(impl_def) => { + impl_def.self_ty(db).as_adt().map(|adt| label_and_docs(db, adt))? + } + Definition::GenericParam(it) => label_and_docs(db, it), + Definition::Label(it) => return Some(Markup::fenced_block(&it.name(db))), + // FIXME: We should be able to show more info about these + Definition::BuiltinAttr(it) => return render_builtin_attr(db, it), + Definition::ToolModule(it) => return Some(Markup::fenced_block(&it.name(db))), + Definition::DeriveHelper(it) => (format!("derive_helper {}", it.name(db)), None), + }; + + let docs = match config.documentation { + Some(_) => docs.or_else(|| { + // docs are missing, for assoc items of trait impls try to fall back to the docs of the + // original item of the trait + let assoc = def.as_assoc_item(db)?; + let trait_ = assoc.containing_trait_impl(db)?; + let name = Some(assoc.name(db)?); + let item = trait_.items(db).into_iter().find(|it| it.name(db) == name)?; + item.docs(db) + }), + None => None, + }; + let docs = docs.filter(|_| config.documentation.is_some()).map(Into::into); + markup(docs, label, mod_path) +} + +fn render_builtin_attr(db: &RootDatabase, attr: hir::BuiltinAttr) -> Option<Markup> { + let name = attr.name(db); + let desc = format!("#[{}]", name); + + let AttributeTemplate { word, list, name_value_str } = match attr.template(db) { + Some(template) => template, + None => return Some(Markup::fenced_block(&attr.name(db))), + }; + let mut docs = "Valid forms are:".to_owned(); + if word { + format_to!(docs, "\n - #\\[{}]", name); + } + if let Some(list) = list { + format_to!(docs, "\n - #\\[{}({})]", name, list); + } + if let Some(name_value_str) = name_value_str { + format_to!(docs, "\n - #\\[{} = {}]", name, name_value_str); + } + markup(Some(docs.replace('*', "\\*")), desc, None) +} + +fn label_and_docs<D>(db: &RootDatabase, def: D) -> (String, Option<hir::Documentation>) +where + D: HasAttrs + HirDisplay, +{ + let label = def.display(db).to_string(); + let docs = def.attrs(db).docs(); + (label, docs) +} + +fn label_value_and_docs<D, E, V>( + db: &RootDatabase, + def: D, + value_extractor: E, +) -> (String, Option<hir::Documentation>) +where + D: HasAttrs + HirDisplay, + E: Fn(&D) -> Option<V>, + V: Display, +{ + let label = if let Some(value) = value_extractor(&def) { + format!("{} = {}", def.display(db), value) + } else { + def.display(db).to_string() + }; + let docs = def.attrs(db).docs(); + (label, docs) +} + +fn definition_mod_path(db: &RootDatabase, def: &Definition) -> Option<String> { + if let Definition::GenericParam(_) = def { + return None; + } + def.module(db).map(|module| path(db, module, definition_owner_name(db, def))) +} + +fn markup(docs: Option<String>, desc: String, mod_path: Option<String>) -> Option<Markup> { + let mut buf = String::new(); + + if let Some(mod_path) = mod_path { + if !mod_path.is_empty() { + format_to!(buf, "```rust\n{}\n```\n\n", mod_path); + } + } + format_to!(buf, "```rust\n{}\n```", desc); + + if let Some(doc) = docs { + format_to!(buf, "\n___\n\n{}", doc); + } + Some(buf.into()) +} + +fn builtin(famous_defs: &FamousDefs<'_, '_>, builtin: hir::BuiltinType) -> Option<Markup> { + // std exposes prim_{} modules with docstrings on the root to document the builtins + let primitive_mod = format!("prim_{}", builtin.name()); + let doc_owner = find_std_module(famous_defs, &primitive_mod)?; + let docs = doc_owner.attrs(famous_defs.0.db).docs()?; + markup(Some(docs.into()), builtin.name().to_string(), None) +} + +fn find_std_module(famous_defs: &FamousDefs<'_, '_>, name: &str) -> Option<hir::Module> { + let db = famous_defs.0.db; + let std_crate = famous_defs.std()?; + let std_root_module = std_crate.root_module(db); + std_root_module + .children(db) + .find(|module| module.name(db).map_or(false, |module| module.to_string() == name)) +} + +fn local(db: &RootDatabase, it: hir::Local) -> Option<Markup> { + let ty = it.ty(db); + let ty = ty.display_truncated(db, None); + let is_mut = if it.is_mut(db) { "mut " } else { "" }; + let desc = match it.source(db).value { + Either::Left(ident) => { + let name = it.name(db); + let let_kw = if ident + .syntax() + .parent() + .map_or(false, |p| p.kind() == LET_STMT || p.kind() == LET_EXPR) + { + "let " + } else { + "" + }; + format!("{}{}{}: {}", let_kw, is_mut, name, ty) + } + Either::Right(_) => format!("{}self: {}", is_mut, ty), + }; + markup(None, desc, None) +} + +struct KeywordHint { + description: String, + keyword_mod: String, + actions: Vec<HoverAction>, +} + +impl KeywordHint { + fn new(description: String, keyword_mod: String) -> Self { + Self { description, keyword_mod, actions: Vec::default() } + } +} + +fn keyword_hints( + sema: &Semantics<'_, RootDatabase>, + token: &SyntaxToken, + parent: syntax::SyntaxNode, +) -> KeywordHint { + match token.kind() { + T![await] | T![loop] | T![match] | T![unsafe] | T![as] | T![try] | T![if] | T![else] => { + let keyword_mod = format!("{}_keyword", token.text()); + + match ast::Expr::cast(parent).and_then(|site| sema.type_of_expr(&site)) { + // ignore the unit type () + Some(ty) if !ty.adjusted.as_ref().unwrap_or(&ty.original).is_unit() => { + let mut targets: Vec<hir::ModuleDef> = Vec::new(); + let mut push_new_def = |item: hir::ModuleDef| { + if !targets.contains(&item) { + targets.push(item); + } + }; + walk_and_push_ty(sema.db, &ty.original, &mut push_new_def); + + let ty = ty.adjusted(); + let description = format!("{}: {}", token.text(), ty.display(sema.db)); + + KeywordHint { + description, + keyword_mod, + actions: vec![HoverAction::goto_type_from_targets(sema.db, targets)], + } + } + _ => KeywordHint { + description: token.text().to_string(), + keyword_mod, + actions: Vec::new(), + }, + } + } + T![fn] => { + let module = match ast::FnPtrType::cast(parent) { + // treat fn keyword inside function pointer type as primitive + Some(_) => format!("prim_{}", token.text()), + None => format!("{}_keyword", token.text()), + }; + KeywordHint::new(token.text().to_string(), module) + } + T![Self] => KeywordHint::new(token.text().to_string(), "self_upper_keyword".into()), + _ => KeywordHint::new(token.text().to_string(), format!("{}_keyword", token.text())), + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs b/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs new file mode 100644 index 000000000..867d1f54d --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/hover/tests.rs @@ -0,0 +1,5053 @@ +use expect_test::{expect, Expect}; +use ide_db::base_db::{FileLoader, FileRange}; +use syntax::TextRange; + +use crate::{fixture, hover::HoverDocFormat, HoverConfig}; + +fn check_hover_no_result(ra_fixture: &str) { + let (analysis, position) = fixture::position(ra_fixture); + let hover = analysis + .hover( + &HoverConfig { links_in_hover: true, documentation: Some(HoverDocFormat::Markdown) }, + FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) }, + ) + .unwrap(); + assert!(hover.is_none(), "hover not expected but found: {:?}", hover.unwrap()); +} + +#[track_caller] +fn check(ra_fixture: &str, expect: Expect) { + let (analysis, position) = fixture::position(ra_fixture); + let hover = analysis + .hover( + &HoverConfig { links_in_hover: true, documentation: Some(HoverDocFormat::Markdown) }, + FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) }, + ) + .unwrap() + .unwrap(); + + let content = analysis.db.file_text(position.file_id); + let hovered_element = &content[hover.range]; + + let actual = format!("*{}*\n{}\n", hovered_element, hover.info.markup); + expect.assert_eq(&actual) +} + +fn check_hover_no_links(ra_fixture: &str, expect: Expect) { + let (analysis, position) = fixture::position(ra_fixture); + let hover = analysis + .hover( + &HoverConfig { links_in_hover: false, documentation: Some(HoverDocFormat::Markdown) }, + FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) }, + ) + .unwrap() + .unwrap(); + + let content = analysis.db.file_text(position.file_id); + let hovered_element = &content[hover.range]; + + let actual = format!("*{}*\n{}\n", hovered_element, hover.info.markup); + expect.assert_eq(&actual) +} + +fn check_hover_no_markdown(ra_fixture: &str, expect: Expect) { + let (analysis, position) = fixture::position(ra_fixture); + let hover = analysis + .hover( + &HoverConfig { links_in_hover: true, documentation: Some(HoverDocFormat::PlainText) }, + FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) }, + ) + .unwrap() + .unwrap(); + + let content = analysis.db.file_text(position.file_id); + let hovered_element = &content[hover.range]; + + let actual = format!("*{}*\n{}\n", hovered_element, hover.info.markup); + expect.assert_eq(&actual) +} + +fn check_actions(ra_fixture: &str, expect: Expect) { + let (analysis, file_id, position) = fixture::range_or_position(ra_fixture); + let hover = analysis + .hover( + &HoverConfig { links_in_hover: true, documentation: Some(HoverDocFormat::Markdown) }, + FileRange { file_id, range: position.range_or_empty() }, + ) + .unwrap() + .unwrap(); + expect.assert_debug_eq(&hover.info.actions) +} + +fn check_hover_range(ra_fixture: &str, expect: Expect) { + let (analysis, range) = fixture::range(ra_fixture); + let hover = analysis + .hover( + &HoverConfig { links_in_hover: false, documentation: Some(HoverDocFormat::Markdown) }, + range, + ) + .unwrap() + .unwrap(); + expect.assert_eq(hover.info.markup.as_str()) +} + +fn check_hover_range_no_results(ra_fixture: &str) { + let (analysis, range) = fixture::range(ra_fixture); + let hover = analysis + .hover( + &HoverConfig { links_in_hover: false, documentation: Some(HoverDocFormat::Markdown) }, + range, + ) + .unwrap(); + assert!(hover.is_none()); +} + +#[test] +fn hover_descend_macros_avoids_duplicates() { + check( + r#" +macro_rules! dupe_use { + ($local:ident) => { + { + $local; + $local; + } + } +} +fn foo() { + let local = 0; + dupe_use!(local$0); +} +"#, + expect![[r#" + *local* + + ```rust + let local: i32 + ``` + "#]], + ); +} + +#[test] +fn hover_shows_all_macro_descends() { + check( + r#" +macro_rules! m { + ($name:ident) => { + /// Outer + fn $name() {} + + mod module { + /// Inner + fn $name() {} + } + }; +} + +m!(ab$0c); + "#, + expect![[r#" + *abc* + + ```rust + test::module + ``` + + ```rust + fn abc() + ``` + + --- + + Inner + --- + + ```rust + test + ``` + + ```rust + fn abc() + ``` + + --- + + Outer + "#]], + ); +} + +#[test] +fn hover_shows_type_of_an_expression() { + check( + r#" +pub fn foo() -> u32 { 1 } + +fn main() { + let foo_test = foo()$0; +} +"#, + expect![[r#" + *foo()* + ```rust + u32 + ``` + "#]], + ); +} + +#[test] +fn hover_remove_markdown_if_configured() { + check_hover_no_markdown( + r#" +pub fn foo() -> u32 { 1 } + +fn main() { + let foo_test = foo()$0; +} +"#, + expect![[r#" + *foo()* + u32 + "#]], + ); +} + +#[test] +fn hover_shows_long_type_of_an_expression() { + check( + r#" +struct Scan<A, B, C> { a: A, b: B, c: C } +struct Iter<I> { inner: I } +enum Option<T> { Some(T), None } + +struct OtherStruct<T> { i: T } + +fn scan<A, B, C>(a: A, b: B, c: C) -> Iter<Scan<OtherStruct<A>, B, C>> { + Iter { inner: Scan { a, b, c } } +} + +fn main() { + let num: i32 = 55; + let closure = |memo: &mut u32, value: &u32, _another: &mut u32| -> Option<u32> { + Option::Some(*memo + value) + }; + let number = 5u32; + let mut iter$0 = scan(OtherStruct { i: num }, closure, number); +} +"#, + expect![[r#" + *iter* + + ```rust + let mut iter: Iter<Scan<OtherStruct<OtherStruct<i32>>, |&mut u32, &u32, &mut u32| -> Option<u32>, u32>> + ``` + "#]], + ); +} + +#[test] +fn hover_shows_fn_signature() { + // Single file with result + check( + r#" +pub fn foo() -> u32 { 1 } + +fn main() { let foo_test = fo$0o(); } +"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + pub fn foo() -> u32 + ``` + "#]], + ); + + // Multiple candidates but results are ambiguous. + check( + r#" +//- /a.rs +pub fn foo() -> u32 { 1 } + +//- /b.rs +pub fn foo() -> &str { "" } + +//- /c.rs +pub fn foo(a: u32, b: u32) {} + +//- /main.rs +mod a; +mod b; +mod c; + +fn main() { let foo_test = fo$0o(); } + "#, + expect![[r#" + *foo* + ```rust + {unknown} + ``` + "#]], + ); + + // Use literal `crate` in path + check( + r#" +pub struct X; + +fn foo() -> crate::X { X } + +fn main() { f$0oo(); } + "#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + fn foo() -> crate::X + ``` + "#]], + ); + + // Check `super` in path + check( + r#" +pub struct X; + +mod m { pub fn foo() -> super::X { super::X } } + +fn main() { m::f$0oo(); } + "#, + expect![[r#" + *foo* + + ```rust + test::m + ``` + + ```rust + pub fn foo() -> super::X + ``` + "#]], + ); +} + +#[test] +fn hover_omits_unnamed_where_preds() { + check( + r#" +pub fn foo(bar: impl T) { } + +fn main() { fo$0o(); } + "#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + pub fn foo(bar: impl T) + ``` + "#]], + ); + check( + r#" +pub fn foo<V: AsRef<str>>(bar: impl T, baz: V) { } + +fn main() { fo$0o(); } + "#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + pub fn foo<V>(bar: impl T, baz: V) + where + V: AsRef<str>, + ``` + "#]], + ); +} + +#[test] +fn hover_shows_fn_signature_with_type_params() { + check( + r#" +pub fn foo<'a, T: AsRef<str>>(b: &'a T) -> &'a str { } + +fn main() { let foo_test = fo$0o(); } + "#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + pub fn foo<'a, T>(b: &'a T) -> &'a str + where + T: AsRef<str>, + ``` + "#]], + ); +} + +#[test] +fn hover_shows_fn_signature_on_fn_name() { + check( + r#" +pub fn foo$0(a: u32, b: u32) -> u32 {} + +fn main() { } +"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + pub fn foo(a: u32, b: u32) -> u32 + ``` + "#]], + ); +} + +#[test] +fn hover_shows_fn_doc() { + check( + r#" +/// # Example +/// ``` +/// # use std::path::Path; +/// # +/// foo(Path::new("hello, world!")) +/// ``` +pub fn foo$0(_: &Path) {} + +fn main() { } +"#, + expect![[r##" + *foo* + + ```rust + test + ``` + + ```rust + pub fn foo(_: &Path) + ``` + + --- + + # Example + + ``` + # use std::path::Path; + # + foo(Path::new("hello, world!")) + ``` + "##]], + ); +} + +#[test] +fn hover_shows_fn_doc_attr_raw_string() { + check( + r##" +#[doc = r#"Raw string doc attr"#] +pub fn foo$0(_: &Path) {} + +fn main() { } +"##, + expect![[r##" + *foo* + + ```rust + test + ``` + + ```rust + pub fn foo(_: &Path) + ``` + + --- + + Raw string doc attr + "##]], + ); +} + +#[test] +fn hover_shows_struct_field_info() { + // Hovering over the field when instantiating + check( + r#" +struct Foo { field_a: u32 } + +fn main() { + let foo = Foo { field_a$0: 0, }; +} +"#, + expect![[r#" + *field_a* + + ```rust + test::Foo + ``` + + ```rust + field_a: u32 + ``` + "#]], + ); + + // Hovering over the field in the definition + check( + r#" +struct Foo { field_a$0: u32 } + +fn main() { + let foo = Foo { field_a: 0 }; +} +"#, + expect![[r#" + *field_a* + + ```rust + test::Foo + ``` + + ```rust + field_a: u32 + ``` + "#]], + ); +} + +#[test] +fn hover_const_static() { + check( + r#"const foo$0: u32 = 123;"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + const foo: u32 = 123 (0x7B) + ``` + "#]], + ); + check( + r#" +const foo$0: u32 = { + let x = foo(); + x + 100 +};"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + const foo: u32 = { + let x = foo(); + x + 100 + } + ``` + "#]], + ); + + check( + r#"static foo$0: u32 = 456;"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + static foo: u32 = 456 + ``` + "#]], + ); +} + +#[test] +fn hover_default_generic_types() { + check( + r#" +struct Test<K, T = u8> { k: K, t: T } + +fn main() { + let zz$0 = Test { t: 23u8, k: 33 }; +}"#, + expect![[r#" + *zz* + + ```rust + let zz: Test<i32> + ``` + "#]], + ); + check_hover_range( + r#" +struct Test<K, T = u8> { k: K, t: T } + +fn main() { + let $0zz$0 = Test { t: 23u8, k: 33 }; +}"#, + expect![[r#" + ```rust + Test<i32, u8> + ```"#]], + ); +} + +#[test] +fn hover_some() { + check( + r#" +enum Option<T> { Some(T) } +use Option::Some; + +fn main() { So$0me(12); } +"#, + expect![[r#" + *Some* + + ```rust + test::Option + ``` + + ```rust + Some(T) + ``` + "#]], + ); + + check( + r#" +enum Option<T> { Some(T) } +use Option::Some; + +fn main() { let b$0ar = Some(12); } +"#, + expect![[r#" + *bar* + + ```rust + let bar: Option<i32> + ``` + "#]], + ); +} + +#[test] +fn hover_enum_variant() { + check( + r#" +enum Option<T> { + /// The None variant + Non$0e +} +"#, + expect![[r#" + *None* + + ```rust + test::Option + ``` + + ```rust + None + ``` + + --- + + The None variant + "#]], + ); + + check( + r#" +enum Option<T> { + /// The Some variant + Some(T) +} +fn main() { + let s = Option::Som$0e(12); +} +"#, + expect![[r#" + *Some* + + ```rust + test::Option + ``` + + ```rust + Some(T) + ``` + + --- + + The Some variant + "#]], + ); +} + +#[test] +fn hover_for_local_variable() { + check( + r#"fn func(foo: i32) { fo$0o; }"#, + expect![[r#" + *foo* + + ```rust + foo: i32 + ``` + "#]], + ) +} + +#[test] +fn hover_for_local_variable_pat() { + check( + r#"fn func(fo$0o: i32) {}"#, + expect![[r#" + *foo* + + ```rust + foo: i32 + ``` + "#]], + ) +} + +#[test] +fn hover_local_var_edge() { + check( + r#"fn func(foo: i32) { if true { $0foo; }; }"#, + expect![[r#" + *foo* + + ```rust + foo: i32 + ``` + "#]], + ) +} + +#[test] +fn hover_for_param_edge() { + check( + r#"fn func($0foo: i32) {}"#, + expect![[r#" + *foo* + + ```rust + foo: i32 + ``` + "#]], + ) +} + +#[test] +fn hover_for_param_with_multiple_traits() { + check( + r#" + //- minicore: sized + trait Deref { + type Target: ?Sized; + } + trait DerefMut { + type Target: ?Sized; + } + fn f(_x$0: impl Deref<Target=u8> + DerefMut<Target=u8>) {}"#, + expect![[r#" + *_x* + + ```rust + _x: impl Deref<Target = u8> + DerefMut<Target = u8> + ``` + "#]], + ) +} + +#[test] +fn test_hover_infer_associated_method_result() { + check( + r#" +struct Thing { x: u32 } + +impl Thing { + fn new() -> Thing { Thing { x: 0 } } +} + +fn main() { let foo_$0test = Thing::new(); } +"#, + expect![[r#" + *foo_test* + + ```rust + let foo_test: Thing + ``` + "#]], + ) +} + +#[test] +fn test_hover_infer_associated_method_exact() { + check( + r#" +mod wrapper { + pub struct Thing { x: u32 } + + impl Thing { + pub fn new() -> Thing { Thing { x: 0 } } + } +} + +fn main() { let foo_test = wrapper::Thing::new$0(); } +"#, + expect![[r#" + *new* + + ```rust + test::wrapper::Thing + ``` + + ```rust + pub fn new() -> Thing + ``` + "#]], + ) +} + +#[test] +fn test_hover_infer_associated_const_in_pattern() { + check( + r#" +struct X; +impl X { + const C: u32 = 1; +} + +fn main() { + match 1 { + X::C$0 => {}, + 2 => {}, + _ => {} + }; +} +"#, + expect![[r#" + *C* + + ```rust + test + ``` + + ```rust + const C: u32 = 1 + ``` + "#]], + ) +} + +#[test] +fn test_hover_self() { + check( + r#" +struct Thing { x: u32 } +impl Thing { + fn new() -> Self { Self$0 { x: 0 } } +} +"#, + expect![[r#" + *Self* + + ```rust + test + ``` + + ```rust + struct Thing + ``` + "#]], + ); + check( + r#" +struct Thing { x: u32 } +impl Thing { + fn new() -> Self$0 { Self { x: 0 } } +} +"#, + expect![[r#" + *Self* + + ```rust + test + ``` + + ```rust + struct Thing + ``` + "#]], + ); + check( + r#" +enum Thing { A } +impl Thing { + pub fn new() -> Self$0 { Thing::A } +} +"#, + expect![[r#" + *Self* + + ```rust + test + ``` + + ```rust + enum Thing + ``` + "#]], + ); + check( + r#" + enum Thing { A } + impl Thing { + pub fn thing(a: Self$0) {} + } + "#, + expect![[r#" + *Self* + + ```rust + test + ``` + + ```rust + enum Thing + ``` + "#]], + ); +} + +#[test] +fn test_hover_shadowing_pat() { + check( + r#" +fn x() {} + +fn y() { + let x = 0i32; + x$0; +} +"#, + expect![[r#" + *x* + + ```rust + let x: i32 + ``` + "#]], + ) +} + +#[test] +fn test_hover_macro_invocation() { + check( + r#" +macro_rules! foo { () => {} } + +fn f() { fo$0o!(); } +"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + macro_rules! foo + ``` + "#]], + ) +} + +#[test] +fn test_hover_macro2_invocation() { + check( + r#" +/// foo bar +/// +/// foo bar baz +macro foo() {} + +fn f() { fo$0o!(); } +"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + macro foo + ``` + + --- + + foo bar + + foo bar baz + "#]], + ) +} + +#[test] +fn test_hover_tuple_field() { + check( + r#"struct TS(String, i32$0);"#, + expect![[r#" + *i32* + + ```rust + i32 + ``` + "#]], + ) +} + +#[test] +fn test_hover_through_macro() { + check( + r#" +macro_rules! id { ($($tt:tt)*) => { $($tt)* } } +fn foo() {} +id! { + fn bar() { fo$0o(); } +} +"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + fn foo() + ``` + "#]], + ); +} + +#[test] +fn test_hover_through_attr() { + check( + r#" +//- proc_macros: identity +#[proc_macros::identity] +fn foo$0() {} +"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + fn foo() + ``` + "#]], + ); +} + +#[test] +fn test_hover_through_expr_in_macro() { + check( + r#" +macro_rules! id { ($($tt:tt)*) => { $($tt)* } } +fn foo(bar:u32) { let a = id!(ba$0r); } +"#, + expect![[r#" + *bar* + + ```rust + bar: u32 + ``` + "#]], + ); +} + +#[test] +fn test_hover_through_expr_in_macro_recursive() { + check( + r#" +macro_rules! id_deep { ($($tt:tt)*) => { $($tt)* } } +macro_rules! id { ($($tt:tt)*) => { id_deep!($($tt)*) } } +fn foo(bar:u32) { let a = id!(ba$0r); } +"#, + expect![[r#" + *bar* + + ```rust + bar: u32 + ``` + "#]], + ); +} + +#[test] +fn test_hover_through_func_in_macro_recursive() { + check( + r#" +macro_rules! id_deep { ($($tt:tt)*) => { $($tt)* } } +macro_rules! id { ($($tt:tt)*) => { id_deep!($($tt)*) } } +fn bar() -> u32 { 0 } +fn foo() { let a = id!([0u32, bar($0)] ); } +"#, + expect![[r#" + *bar()* + ```rust + u32 + ``` + "#]], + ); +} + +#[test] +fn test_hover_through_literal_string_in_macro() { + check( + r#" +macro_rules! arr { ($($tt:tt)*) => { [$($tt)*] } } +fn foo() { + let mastered_for_itunes = ""; + let _ = arr!("Tr$0acks", &mastered_for_itunes); +} +"#, + expect![[r#" + *"Tracks"* + ```rust + &str + ``` + "#]], + ); +} + +#[test] +fn test_hover_through_assert_macro() { + check( + r#" +#[rustc_builtin_macro] +macro_rules! assert {} + +fn bar() -> bool { true } +fn foo() { + assert!(ba$0r()); +} +"#, + expect![[r#" + *bar* + + ```rust + test + ``` + + ```rust + fn bar() -> bool + ``` + "#]], + ); +} + +#[test] +fn test_hover_multiple_actions() { + check_actions( + r#" +struct Bar; +struct Foo { bar: Bar } + +fn foo(Foo { b$0ar }: &Foo) {} + "#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::Bar", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..11, + focus_range: 7..10, + name: "Bar", + kind: Struct, + description: "struct Bar", + }, + }, + ], + ), + ] + "#]], + ) +} + +#[test] +fn test_hover_through_literal_string_in_builtin_macro() { + check_hover_no_result( + r#" + #[rustc_builtin_macro] + macro_rules! format {} + + fn foo() { + format!("hel$0lo {}", 0); + } +"#, + ); +} + +#[test] +fn test_hover_non_ascii_space_doc() { + check( + " +/// <- `\u{3000}` here +fn foo() { } + +fn bar() { fo$0o(); } +", + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + fn foo() + ``` + + --- + + \<- ` ` here + "#]], + ); +} + +#[test] +fn test_hover_function_show_qualifiers() { + check( + r#"async fn foo$0() {}"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + async fn foo() + ``` + "#]], + ); + check( + r#"pub const unsafe fn foo$0() {}"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + pub const unsafe fn foo() + ``` + "#]], + ); + // Top level `pub(crate)` will be displayed as no visibility. + check( + r#"mod m { pub(crate) async unsafe extern "C" fn foo$0() {} }"#, + expect![[r#" + *foo* + + ```rust + test::m + ``` + + ```rust + pub(crate) async unsafe extern "C" fn foo() + ``` + "#]], + ); +} + +#[test] +fn test_hover_function_show_types() { + check( + r#"fn foo$0(a: i32, b:i32) -> i32 { 0 }"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + fn foo(a: i32, b: i32) -> i32 + ``` + "#]], + ); +} + +#[test] +fn test_hover_function_pointer_show_identifiers() { + check( + r#"type foo$0 = fn(a: i32, b: i32) -> i32;"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + type foo = fn(a: i32, b: i32) -> i32 + ``` + "#]], + ); +} + +#[test] +fn test_hover_function_pointer_no_identifier() { + check( + r#"type foo$0 = fn(i32, _: i32) -> i32;"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + type foo = fn(i32, i32) -> i32 + ``` + "#]], + ); +} + +#[test] +fn test_hover_trait_show_qualifiers() { + check_actions( + r"unsafe trait foo$0() {}", + expect![[r#" + [ + Implementation( + FilePosition { + file_id: FileId( + 0, + ), + offset: 13, + }, + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_extern_crate() { + check( + r#" +//- /main.rs crate:main deps:std +extern crate st$0d; +//- /std/lib.rs crate:std +//! Standard library for this test +//! +//! Printed? +//! abc123 +"#, + expect![[r#" + *std* + + ```rust + extern crate std + ``` + + --- + + Standard library for this test + + Printed? + abc123 + "#]], + ); + check( + r#" +//- /main.rs crate:main deps:std +extern crate std as ab$0c; +//- /std/lib.rs crate:std +//! Standard library for this test +//! +//! Printed? +//! abc123 +"#, + expect![[r#" + *abc* + + ```rust + extern crate std + ``` + + --- + + Standard library for this test + + Printed? + abc123 + "#]], + ); +} + +#[test] +fn test_hover_mod_with_same_name_as_function() { + check( + r#" +use self::m$0y::Bar; +mod my { pub struct Bar; } + +fn my() {} +"#, + expect![[r#" + *my* + + ```rust + test + ``` + + ```rust + mod my + ``` + "#]], + ); +} + +#[test] +fn test_hover_struct_doc_comment() { + check( + r#" +/// This is an example +/// multiline doc +/// +/// # Example +/// +/// ``` +/// let five = 5; +/// +/// assert_eq!(6, my_crate::add_one(5)); +/// ``` +struct Bar; + +fn foo() { let bar = Ba$0r; } +"#, + expect![[r##" + *Bar* + + ```rust + test + ``` + + ```rust + struct Bar + ``` + + --- + + This is an example + multiline doc + + # Example + + ``` + let five = 5; + + assert_eq!(6, my_crate::add_one(5)); + ``` + "##]], + ); +} + +#[test] +fn test_hover_struct_doc_attr() { + check( + r#" +#[doc = "bar docs"] +struct Bar; + +fn foo() { let bar = Ba$0r; } +"#, + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + struct Bar + ``` + + --- + + bar docs + "#]], + ); +} + +#[test] +fn test_hover_struct_doc_attr_multiple_and_mixed() { + check( + r#" +/// bar docs 0 +#[doc = "bar docs 1"] +#[doc = "bar docs 2"] +struct Bar; + +fn foo() { let bar = Ba$0r; } +"#, + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + struct Bar + ``` + + --- + + bar docs 0 + bar docs 1 + bar docs 2 + "#]], + ); +} + +#[test] +fn test_hover_external_url() { + check( + r#" +pub struct Foo; +/// [external](https://www.google.com) +pub struct B$0ar +"#, + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + pub struct Bar + ``` + + --- + + [external](https://www.google.com) + "#]], + ); +} + +// Check that we don't rewrite links which we can't identify +#[test] +fn test_hover_unknown_target() { + check( + r#" +pub struct Foo; +/// [baz](Baz) +pub struct B$0ar +"#, + expect![[r#" + *Bar* + + ```rust + test + ``` + + ```rust + pub struct Bar + ``` + + --- + + [baz](Baz) + "#]], + ); +} + +#[test] +fn test_hover_no_links() { + check_hover_no_links( + r#" +/// Test cases: +/// case 1. bare URL: https://www.example.com/ +/// case 2. inline URL with title: [example](https://www.example.com/) +/// case 3. code reference: [`Result`] +/// case 4. code reference but miss footnote: [`String`] +/// case 5. autolink: <http://www.example.com/> +/// case 6. email address: <test@example.com> +/// case 7. reference: [example][example] +/// case 8. collapsed link: [example][] +/// case 9. shortcut link: [example] +/// case 10. inline without URL: [example]() +/// case 11. reference: [foo][foo] +/// case 12. reference: [foo][bar] +/// case 13. collapsed link: [foo][] +/// case 14. shortcut link: [foo] +/// case 15. inline without URL: [foo]() +/// case 16. just escaped text: \[foo] +/// case 17. inline link: [Foo](foo::Foo) +/// +/// [`Result`]: ../../std/result/enum.Result.html +/// [^example]: https://www.example.com/ +pub fn fo$0o() {} +"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + pub fn foo() + ``` + + --- + + Test cases: + case 1. bare URL: https://www.example.com/ + case 2. inline URL with title: [example](https://www.example.com/) + case 3. code reference: `Result` + case 4. code reference but miss footnote: `String` + case 5. autolink: http://www.example.com/ + case 6. email address: test@example.com + case 7. reference: example + case 8. collapsed link: example + case 9. shortcut link: example + case 10. inline without URL: example + case 11. reference: foo + case 12. reference: foo + case 13. collapsed link: foo + case 14. shortcut link: foo + case 15. inline without URL: foo + case 16. just escaped text: \[foo\] + case 17. inline link: Foo + + [^example]: https://www.example.com/ + "#]], + ); +} + +#[test] +fn test_hover_macro_generated_struct_fn_doc_comment() { + cov_mark::check!(hover_macro_generated_struct_fn_doc_comment); + + check( + r#" +macro_rules! bar { + () => { + struct Bar; + impl Bar { + /// Do the foo + fn foo(&self) {} + } + } +} + +bar!(); + +fn foo() { let bar = Bar; bar.fo$0o(); } +"#, + expect![[r#" + *foo* + + ```rust + test::Bar + ``` + + ```rust + fn foo(&self) + ``` + + --- + + Do the foo + "#]], + ); +} + +#[test] +fn test_hover_macro_generated_struct_fn_doc_attr() { + cov_mark::check!(hover_macro_generated_struct_fn_doc_attr); + + check( + r#" +macro_rules! bar { + () => { + struct Bar; + impl Bar { + #[doc = "Do the foo"] + fn foo(&self) {} + } + } +} + +bar!(); + +fn foo() { let bar = Bar; bar.fo$0o(); } +"#, + expect![[r#" + *foo* + + ```rust + test::Bar + ``` + + ```rust + fn foo(&self) + ``` + + --- + + Do the foo + "#]], + ); +} + +#[test] +fn test_hover_variadic_function() { + check( + r#" +extern "C" { + pub fn foo(bar: i32, ...) -> i32; +} + +fn main() { let foo_test = unsafe { fo$0o(1, 2, 3); } } +"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + pub unsafe fn foo(bar: i32, ...) -> i32 + ``` + "#]], + ); +} + +#[test] +fn test_hover_trait_has_impl_action() { + check_actions( + r#"trait foo$0() {}"#, + expect![[r#" + [ + Implementation( + FilePosition { + file_id: FileId( + 0, + ), + offset: 6, + }, + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_struct_has_impl_action() { + check_actions( + r"struct foo$0() {}", + expect![[r#" + [ + Implementation( + FilePosition { + file_id: FileId( + 0, + ), + offset: 7, + }, + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_union_has_impl_action() { + check_actions( + r#"union foo$0() {}"#, + expect![[r#" + [ + Implementation( + FilePosition { + file_id: FileId( + 0, + ), + offset: 6, + }, + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_enum_has_impl_action() { + check_actions( + r"enum foo$0() { A, B }", + expect![[r#" + [ + Implementation( + FilePosition { + file_id: FileId( + 0, + ), + offset: 5, + }, + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_self_has_impl_action() { + check_actions( + r#"struct foo where Self$0:;"#, + expect![[r#" + [ + Implementation( + FilePosition { + file_id: FileId( + 0, + ), + offset: 7, + }, + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_test_has_action() { + check_actions( + r#" +#[test] +fn foo_$0test() {} +"#, + expect![[r#" + [ + Reference( + FilePosition { + file_id: FileId( + 0, + ), + offset: 11, + }, + ), + Runnable( + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..24, + focus_range: 11..19, + name: "foo_test", + kind: Function, + }, + kind: Test { + test_id: Path( + "foo_test", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_test_mod_has_action() { + check_actions( + r#" +mod tests$0 { + #[test] + fn foo_test() {} +} +"#, + expect![[r#" + [ + Runnable( + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..46, + focus_range: 4..9, + name: "tests", + kind: Module, + description: "mod tests", + }, + kind: TestMod { + path: "tests", + }, + cfg: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_struct_has_goto_type_action() { + check_actions( + r#" +struct S{ f1: u32 } + +fn main() { let s$0t = S{ f1:0 }; } +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::S", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..19, + focus_range: 7..8, + name: "S", + kind: Struct, + description: "struct S", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_generic_struct_has_goto_type_actions() { + check_actions( + r#" +struct Arg(u32); +struct S<T>{ f1: T } + +fn main() { let s$0t = S{ f1:Arg(0) }; } +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::S", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 17..37, + focus_range: 24..25, + name: "S", + kind: Struct, + description: "struct S<T>", + }, + }, + HoverGotoTypeData { + mod_path: "test::Arg", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..16, + focus_range: 7..10, + name: "Arg", + kind: Struct, + description: "struct Arg", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_generic_struct_has_flattened_goto_type_actions() { + check_actions( + r#" +struct Arg(u32); +struct S<T>{ f1: T } + +fn main() { let s$0t = S{ f1: S{ f1: Arg(0) } }; } +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::S", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 17..37, + focus_range: 24..25, + name: "S", + kind: Struct, + description: "struct S<T>", + }, + }, + HoverGotoTypeData { + mod_path: "test::Arg", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..16, + focus_range: 7..10, + name: "Arg", + kind: Struct, + description: "struct Arg", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_tuple_has_goto_type_actions() { + check_actions( + r#" +struct A(u32); +struct B(u32); +mod M { + pub struct C(u32); +} + +fn main() { let s$0t = (A(1), B(2), M::C(3) ); } +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::A", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..14, + focus_range: 7..8, + name: "A", + kind: Struct, + description: "struct A", + }, + }, + HoverGotoTypeData { + mod_path: "test::B", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 15..29, + focus_range: 22..23, + name: "B", + kind: Struct, + description: "struct B", + }, + }, + HoverGotoTypeData { + mod_path: "test::M::C", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 42..60, + focus_range: 53..54, + name: "C", + kind: Struct, + description: "pub struct C", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_return_impl_trait_has_goto_type_action() { + check_actions( + r#" +trait Foo {} +fn foo() -> impl Foo {} + +fn main() { let s$0t = foo(); } +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::Foo", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..12, + focus_range: 6..9, + name: "Foo", + kind: Trait, + description: "trait Foo", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_generic_return_impl_trait_has_goto_type_action() { + check_actions( + r#" +trait Foo<T> {} +struct S; +fn foo() -> impl Foo<S> {} + +fn main() { let s$0t = foo(); } +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::Foo", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..15, + focus_range: 6..9, + name: "Foo", + kind: Trait, + description: "trait Foo<T>", + }, + }, + HoverGotoTypeData { + mod_path: "test::S", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 16..25, + focus_range: 23..24, + name: "S", + kind: Struct, + description: "struct S", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_return_impl_traits_has_goto_type_action() { + check_actions( + r#" +trait Foo {} +trait Bar {} +fn foo() -> impl Foo + Bar {} + +fn main() { let s$0t = foo(); } +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::Foo", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..12, + focus_range: 6..9, + name: "Foo", + kind: Trait, + description: "trait Foo", + }, + }, + HoverGotoTypeData { + mod_path: "test::Bar", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 13..25, + focus_range: 19..22, + name: "Bar", + kind: Trait, + description: "trait Bar", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_generic_return_impl_traits_has_goto_type_action() { + check_actions( + r#" +trait Foo<T> {} +trait Bar<T> {} +struct S1 {} +struct S2 {} + +fn foo() -> impl Foo<S1> + Bar<S2> {} + +fn main() { let s$0t = foo(); } +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::Foo", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..15, + focus_range: 6..9, + name: "Foo", + kind: Trait, + description: "trait Foo<T>", + }, + }, + HoverGotoTypeData { + mod_path: "test::Bar", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 16..31, + focus_range: 22..25, + name: "Bar", + kind: Trait, + description: "trait Bar<T>", + }, + }, + HoverGotoTypeData { + mod_path: "test::S1", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 32..44, + focus_range: 39..41, + name: "S1", + kind: Struct, + description: "struct S1", + }, + }, + HoverGotoTypeData { + mod_path: "test::S2", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 45..57, + focus_range: 52..54, + name: "S2", + kind: Struct, + description: "struct S2", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_arg_impl_trait_has_goto_type_action() { + check_actions( + r#" +trait Foo {} +fn foo(ar$0g: &impl Foo) {} +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::Foo", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..12, + focus_range: 6..9, + name: "Foo", + kind: Trait, + description: "trait Foo", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_arg_impl_traits_has_goto_type_action() { + check_actions( + r#" +trait Foo {} +trait Bar<T> {} +struct S{} + +fn foo(ar$0g: &impl Foo + Bar<S>) {} +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::Foo", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..12, + focus_range: 6..9, + name: "Foo", + kind: Trait, + description: "trait Foo", + }, + }, + HoverGotoTypeData { + mod_path: "test::Bar", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 13..28, + focus_range: 19..22, + name: "Bar", + kind: Trait, + description: "trait Bar<T>", + }, + }, + HoverGotoTypeData { + mod_path: "test::S", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 29..39, + focus_range: 36..37, + name: "S", + kind: Struct, + description: "struct S", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_async_block_impl_trait_has_goto_type_action() { + check_actions( + r#" +//- /main.rs crate:main deps:core +// we don't use minicore here so that this test doesn't randomly fail +// when someone edits minicore +struct S; +fn foo() { + let fo$0o = async { S }; +} +//- /core.rs crate:core +pub mod future { + #[lang = "future_trait"] + pub trait Future {} +} +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "core::future::Future", + nav: NavigationTarget { + file_id: FileId( + 1, + ), + full_range: 21..69, + focus_range: 60..66, + name: "Future", + kind: Trait, + description: "pub trait Future", + }, + }, + HoverGotoTypeData { + mod_path: "main::S", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..110, + focus_range: 108..109, + name: "S", + kind: Struct, + description: "struct S", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_arg_generic_impl_trait_has_goto_type_action() { + check_actions( + r#" +trait Foo<T> {} +struct S {} +fn foo(ar$0g: &impl Foo<S>) {} +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::Foo", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..15, + focus_range: 6..9, + name: "Foo", + kind: Trait, + description: "trait Foo<T>", + }, + }, + HoverGotoTypeData { + mod_path: "test::S", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 16..27, + focus_range: 23..24, + name: "S", + kind: Struct, + description: "struct S", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_dyn_return_has_goto_type_action() { + check_actions( + r#" +trait Foo {} +struct S; +impl Foo for S {} + +struct B<T>{} +fn foo() -> B<dyn Foo> {} + +fn main() { let s$0t = foo(); } +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::B", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 42..55, + focus_range: 49..50, + name: "B", + kind: Struct, + description: "struct B<T>", + }, + }, + HoverGotoTypeData { + mod_path: "test::Foo", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..12, + focus_range: 6..9, + name: "Foo", + kind: Trait, + description: "trait Foo", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_dyn_arg_has_goto_type_action() { + check_actions( + r#" +trait Foo {} +fn foo(ar$0g: &dyn Foo) {} +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::Foo", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..12, + focus_range: 6..9, + name: "Foo", + kind: Trait, + description: "trait Foo", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_generic_dyn_arg_has_goto_type_action() { + check_actions( + r#" +trait Foo<T> {} +struct S {} +fn foo(ar$0g: &dyn Foo<S>) {} +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::Foo", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..15, + focus_range: 6..9, + name: "Foo", + kind: Trait, + description: "trait Foo<T>", + }, + }, + HoverGotoTypeData { + mod_path: "test::S", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 16..27, + focus_range: 23..24, + name: "S", + kind: Struct, + description: "struct S", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_goto_type_action_links_order() { + check_actions( + r#" +trait ImplTrait<T> {} +trait DynTrait<T> {} +struct B<T> {} +struct S {} + +fn foo(a$0rg: &impl ImplTrait<B<dyn DynTrait<B<S>>>>) {} +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::ImplTrait", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..21, + focus_range: 6..15, + name: "ImplTrait", + kind: Trait, + description: "trait ImplTrait<T>", + }, + }, + HoverGotoTypeData { + mod_path: "test::B", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 43..57, + focus_range: 50..51, + name: "B", + kind: Struct, + description: "struct B<T>", + }, + }, + HoverGotoTypeData { + mod_path: "test::DynTrait", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 22..42, + focus_range: 28..36, + name: "DynTrait", + kind: Trait, + description: "trait DynTrait<T>", + }, + }, + HoverGotoTypeData { + mod_path: "test::S", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 58..69, + focus_range: 65..66, + name: "S", + kind: Struct, + description: "struct S", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_associated_type_has_goto_type_action() { + check_actions( + r#" +trait Foo { + type Item; + fn get(self) -> Self::Item {} +} + +struct Bar{} +struct S{} + +impl Foo for S { type Item = Bar; } + +fn test() -> impl Foo { S {} } + +fn main() { let s$0t = test().get(); } +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::Foo", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..62, + focus_range: 6..9, + name: "Foo", + kind: Trait, + description: "trait Foo", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_const_param_has_goto_type_action() { + check_actions( + r#" +struct Bar; +struct Foo<const BAR: Bar>; + +impl<const BAR: Bar> Foo<BAR$0> {} +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::Bar", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..11, + focus_range: 7..10, + name: "Bar", + kind: Struct, + description: "struct Bar", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_type_param_has_goto_type_action() { + check_actions( + r#" +trait Foo {} + +fn foo<T: Foo>(t: T$0){} +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::Foo", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..12, + focus_range: 6..9, + name: "Foo", + kind: Trait, + description: "trait Foo", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn test_hover_self_has_go_to_type() { + check_actions( + r#" +struct Foo; +impl Foo { + fn foo(&self$0) {} +} +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::Foo", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..11, + focus_range: 7..10, + name: "Foo", + kind: Struct, + description: "struct Foo", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn hover_displays_normalized_crate_names() { + check( + r#" +//- /lib.rs crate:name-with-dashes +pub mod wrapper { + pub struct Thing { x: u32 } + + impl Thing { + pub fn new() -> Thing { Thing { x: 0 } } + } +} + +//- /main.rs crate:main deps:name-with-dashes +fn main() { let foo_test = name_with_dashes::wrapper::Thing::new$0(); } +"#, + expect![[r#" + *new* + + ```rust + name_with_dashes::wrapper::Thing + ``` + + ```rust + pub fn new() -> Thing + ``` + "#]], + ) +} + +#[test] +fn hover_field_pat_shorthand_ref_match_ergonomics() { + check( + r#" +struct S { + f: i32, +} + +fn main() { + let s = S { f: 0 }; + let S { f$0 } = &s; +} +"#, + expect![[r#" + *f* + + ```rust + f: &i32 + ``` + --- + + ```rust + test::S + ``` + + ```rust + f: i32 + ``` + "#]], + ); +} + +#[test] +fn const_generic_order() { + check( + r#" +struct Foo; +struct S$0T<const C: usize = 1, T = Foo>(T); +"#, + expect![[r#" + *ST* + + ```rust + test + ``` + + ```rust + struct ST<const C: usize, T = Foo> + ``` + "#]], + ); +} + +#[test] +fn const_generic_positive_i8_literal() { + check( + r#" +struct Const<const N: i8>; + +fn main() { + let v$0alue = Const::<1>; +} +"#, + expect![[r#" + *value* + + ```rust + let value: Const<1> + ``` + "#]], + ); +} + +#[test] +fn const_generic_zero_i8_literal() { + check( + r#" +struct Const<const N: i8>; + +fn main() { + let v$0alue = Const::<0>; +} +"#, + expect![[r#" + *value* + + ```rust + let value: Const<0> + ``` + "#]], + ); +} + +#[test] +fn const_generic_negative_i8_literal() { + check( + r#" +struct Const<const N: i8>; + +fn main() { + let v$0alue = Const::<-1>; +} +"#, + expect![[r#" + *value* + + ```rust + let value: Const<-1> + ``` + "#]], + ); +} + +#[test] +fn const_generic_bool_literal() { + check( + r#" +struct Const<const F: bool>; + +fn main() { + let v$0alue = Const::<true>; +} +"#, + expect![[r#" + *value* + + ```rust + let value: Const<true> + ``` + "#]], + ); +} + +#[test] +fn const_generic_char_literal() { + check( + r#" +struct Const<const C: char>; + +fn main() { + let v$0alue = Const::<'🦀'>; +} +"#, + expect![[r#" + *value* + + ```rust + let value: Const<'🦀'> + ``` + "#]], + ); +} + +#[test] +fn hover_self_param_shows_type() { + check( + r#" +struct Foo {} +impl Foo { + fn bar(&sel$0f) {} +} +"#, + expect![[r#" + *self* + + ```rust + self: &Foo + ``` + "#]], + ); +} + +#[test] +fn hover_self_param_shows_type_for_arbitrary_self_type() { + check( + r#" +struct Arc<T>(T); +struct Foo {} +impl Foo { + fn bar(sel$0f: Arc<Foo>) {} +} +"#, + expect![[r#" + *self* + + ```rust + self: Arc<Foo> + ``` + "#]], + ); +} + +#[test] +fn hover_doc_outer_inner() { + check( + r#" +/// Be quick; +mod Foo$0 { + //! time is mana + + /// This comment belongs to the function + fn foo() {} +} +"#, + expect![[r#" + *Foo* + + ```rust + test + ``` + + ```rust + mod Foo + ``` + + --- + + Be quick; + time is mana + "#]], + ); +} + +#[test] +fn hover_doc_outer_inner_attribue() { + check( + r#" +#[doc = "Be quick;"] +mod Foo$0 { + #![doc = "time is mana"] + + #[doc = "This comment belongs to the function"] + fn foo() {} +} +"#, + expect![[r#" + *Foo* + + ```rust + test + ``` + + ```rust + mod Foo + ``` + + --- + + Be quick; + time is mana + "#]], + ); +} + +#[test] +fn hover_doc_block_style_indentend() { + check( + r#" +/** + foo + ```rust + let x = 3; + ``` +*/ +fn foo$0() {} +"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + fn foo() + ``` + + --- + + foo + + ```rust + let x = 3; + ``` + "#]], + ); +} + +#[test] +fn hover_comments_dont_highlight_parent() { + cov_mark::check!(no_highlight_on_comment_hover); + check_hover_no_result( + r#" +fn no_hover() { + // no$0hover +} +"#, + ); +} + +#[test] +fn hover_label() { + check( + r#" +fn foo() { + 'label$0: loop {} +} +"#, + expect![[r#" + *'label* + + ```rust + 'label + ``` + "#]], + ); +} + +#[test] +fn hover_lifetime() { + check( + r#"fn foo<'lifetime>(_: &'lifetime$0 ()) {}"#, + expect![[r#" + *'lifetime* + + ```rust + 'lifetime + ``` + "#]], + ); +} + +#[test] +fn hover_type_param() { + check( + r#" +//- minicore: sized +struct Foo<T>(T); +trait TraitA {} +trait TraitB {} +impl<T: TraitA + TraitB> Foo<T$0> where T: Sized {} +"#, + expect![[r#" + *T* + + ```rust + T: TraitA + TraitB + ``` + "#]], + ); + check( + r#" +//- minicore: sized +struct Foo<T>(T); +impl<T> Foo<T$0> {} +"#, + expect![[r#" + *T* + + ```rust + T + ``` + "#]], + ); + // lifetimes bounds arent being tracked yet + check( + r#" +//- minicore: sized +struct Foo<T>(T); +impl<T: 'static> Foo<T$0> {} +"#, + expect![[r#" + *T* + + ```rust + T + ``` + "#]], + ); +} + +#[test] +fn hover_type_param_sized_bounds() { + // implicit `: Sized` bound + check( + r#" +//- minicore: sized +trait Trait {} +struct Foo<T>(T); +impl<T: Trait> Foo<T$0> {} +"#, + expect![[r#" + *T* + + ```rust + T: Trait + ``` + "#]], + ); + check( + r#" +//- minicore: sized +trait Trait {} +struct Foo<T>(T); +impl<T: Trait + ?Sized> Foo<T$0> {} +"#, + expect![[r#" + *T* + + ```rust + T: Trait + ?Sized + ``` + "#]], + ); +} + +mod type_param_sized_bounds { + use super::*; + + #[test] + fn single_implicit() { + check( + r#" +//- minicore: sized +fn foo<T$0>() {} +"#, + expect![[r#" + *T* + + ```rust + T + ``` + "#]], + ); + } + + #[test] + fn single_explicit() { + check( + r#" +//- minicore: sized +fn foo<T$0: Sized>() {} +"#, + expect![[r#" + *T* + + ```rust + T + ``` + "#]], + ); + } + + #[test] + fn single_relaxed() { + check( + r#" +//- minicore: sized +fn foo<T$0: ?Sized>() {} +"#, + expect![[r#" + *T* + + ```rust + T: ?Sized + ``` + "#]], + ); + } + + #[test] + fn multiple_implicit() { + check( + r#" +//- minicore: sized +trait Trait {} +fn foo<T$0: Trait>() {} +"#, + expect![[r#" + *T* + + ```rust + T: Trait + ``` + "#]], + ); + } + + #[test] + fn multiple_explicit() { + check( + r#" +//- minicore: sized +trait Trait {} +fn foo<T$0: Trait + Sized>() {} +"#, + expect![[r#" + *T* + + ```rust + T: Trait + ``` + "#]], + ); + } + + #[test] + fn multiple_relaxed() { + check( + r#" +//- minicore: sized +trait Trait {} +fn foo<T$0: Trait + ?Sized>() {} +"#, + expect![[r#" + *T* + + ```rust + T: Trait + ?Sized + ``` + "#]], + ); + } + + #[test] + fn mixed() { + check( + r#" +//- minicore: sized +fn foo<T$0: ?Sized + Sized + Sized>() {} +"#, + expect![[r#" + *T* + + ```rust + T + ``` + "#]], + ); + check( + r#" +//- minicore: sized +trait Trait {} +fn foo<T$0: Sized + ?Sized + Sized + Trait>() {} +"#, + expect![[r#" + *T* + + ```rust + T: Trait + ``` + "#]], + ); + } +} + +#[test] +fn hover_const_generic_type_alias() { + check( + r#" +struct Foo<const LEN: usize>; +type Fo$0o2 = Foo<2>; +"#, + expect![[r#" + *Foo2* + + ```rust + test + ``` + + ```rust + type Foo2 = Foo<2> + ``` + "#]], + ); +} + +#[test] +fn hover_const_param() { + check( + r#" +struct Foo<const LEN: usize>; +impl<const LEN: usize> Foo<LEN$0> {} +"#, + expect![[r#" + *LEN* + + ```rust + const LEN: usize + ``` + "#]], + ); +} + +#[test] +fn hover_const_eval() { + // show hex for <10 + check( + r#" +/// This is a doc +const FOO$0: usize = 1 << 3; +"#, + expect![[r#" + *FOO* + + ```rust + test + ``` + + ```rust + const FOO: usize = 8 + ``` + + --- + + This is a doc + "#]], + ); + // show hex for >10 + check( + r#" +/// This is a doc +const FOO$0: usize = (1 << 3) + (1 << 2); +"#, + expect![[r#" + *FOO* + + ```rust + test + ``` + + ```rust + const FOO: usize = 12 (0xC) + ``` + + --- + + This is a doc + "#]], + ); + // show original body when const eval fails + check( + r#" +/// This is a doc +const FOO$0: usize = 2 - 3; +"#, + expect![[r#" + *FOO* + + ```rust + test + ``` + + ```rust + const FOO: usize = 2 - 3 + ``` + + --- + + This is a doc + "#]], + ); + // don't show hex for negatives + check( + r#" +/// This is a doc +const FOO$0: i32 = 2 - 3; +"#, + expect![[r#" + *FOO* + + ```rust + test + ``` + + ```rust + const FOO: i32 = -1 + ``` + + --- + + This is a doc + "#]], + ); + check( + r#" +/// This is a doc +const FOO$0: &str = "bar"; +"#, + expect![[r#" + *FOO* + + ```rust + test + ``` + + ```rust + const FOO: &str = "bar" + ``` + + --- + + This is a doc + "#]], + ); + // show char literal + check( + r#" +/// This is a doc +const FOO$0: char = 'a'; +"#, + expect![[r#" + *FOO* + + ```rust + test + ``` + + ```rust + const FOO: char = 'a' + ``` + + --- + + This is a doc + "#]], + ); + // show escaped char literal + check( + r#" +/// This is a doc +const FOO$0: char = '\x61'; +"#, + expect![[r#" + *FOO* + + ```rust + test + ``` + + ```rust + const FOO: char = 'a' + ``` + + --- + + This is a doc + "#]], + ); + // show byte literal + check( + r#" +/// This is a doc +const FOO$0: u8 = b'a'; +"#, + expect![[r#" + *FOO* + + ```rust + test + ``` + + ```rust + const FOO: u8 = 97 (0x61) + ``` + + --- + + This is a doc + "#]], + ); + // show escaped byte literal + check( + r#" +/// This is a doc +const FOO$0: u8 = b'\x61'; +"#, + expect![[r#" + *FOO* + + ```rust + test + ``` + + ```rust + const FOO: u8 = 97 (0x61) + ``` + + --- + + This is a doc + "#]], + ); + // show float literal + check( + r#" + /// This is a doc + const FOO$0: f64 = 1.0234; + "#, + expect![[r#" + *FOO* + + ```rust + test + ``` + + ```rust + const FOO: f64 = 1.0234 + ``` + + --- + + This is a doc + "#]], + ); + //show float typecasted from int + check( + r#" +/// This is a doc +const FOO$0: f32 = 1f32; +"#, + expect![[r#" + *FOO* + + ```rust + test + ``` + + ```rust + const FOO: f32 = 1.0 + ``` + + --- + + This is a doc + "#]], + ); + //show f64 typecasted from float + check( + r#" +/// This is a doc +const FOO$0: f64 = 1.0f64; +"#, + expect![[r#" + *FOO* + + ```rust + test + ``` + + ```rust + const FOO: f64 = 1.0 + ``` + + --- + + This is a doc + "#]], + ); +} + +#[test] +fn hover_const_pat() { + check( + r#" +/// This is a doc +const FOO: usize = 3; +fn foo() { + match 5 { + FOO$0 => (), + _ => () + } +} +"#, + expect![[r#" + *FOO* + + ```rust + test + ``` + + ```rust + const FOO: usize = 3 + ``` + + --- + + This is a doc + "#]], + ); +} + +#[test] +fn array_repeat_exp() { + check( + r#" +fn main() { + let til$0e4 = [0_u32; (4 * 8 * 8) / 32]; +} + "#, + expect![[r#" + *tile4* + + ```rust + let tile4: [u32; 8] + ``` + "#]], + ); +} + +#[test] +fn hover_mod_def() { + check( + r#" +//- /main.rs +mod foo$0; +//- /foo.rs +//! For the horde! +"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + mod foo + ``` + + --- + + For the horde! + "#]], + ); +} + +#[test] +fn hover_self_in_use() { + check( + r#" +//! This should not appear +mod foo { + /// But this should appear + pub mod bar {} +} +use foo::bar::{self$0}; +"#, + expect![[r#" + *self* + + ```rust + test::foo + ``` + + ```rust + mod bar + ``` + + --- + + But this should appear + "#]], + ) +} + +#[test] +fn hover_keyword() { + check( + r#" +//- /main.rs crate:main deps:std +fn f() { retur$0n; } +//- /libstd.rs crate:std +/// Docs for return_keyword +mod return_keyword {} +"#, + expect![[r#" + *return* + + ```rust + return + ``` + + --- + + Docs for return_keyword + "#]], + ); +} + +#[test] +fn hover_keyword_doc() { + check( + r#" +//- /main.rs crate:main deps:std +fn foo() { + let bar = mov$0e || {}; +} +//- /libstd.rs crate:std +#[doc(keyword = "move")] +/// [closure] +/// [closures][closure] +/// [threads] +/// <https://doc.rust-lang.org/nightly/book/ch13-01-closures.html> +/// +/// [closure]: ../book/ch13-01-closures.html +/// [threads]: ../book/ch16-01-threads.html#using-move-closures-with-threads +mod move_keyword {} +"#, + expect![[r##" + *move* + + ```rust + move + ``` + + --- + + [closure](https://doc.rust-lang.org/nightly/book/ch13-01-closures.html) + [closures](https://doc.rust-lang.org/nightly/book/ch13-01-closures.html) + [threads](https://doc.rust-lang.org/nightly/book/ch16-01-threads.html#using-move-closures-with-threads) + <https://doc.rust-lang.org/nightly/book/ch13-01-closures.html> + "##]], + ); +} + +#[test] +fn hover_keyword_as_primitive() { + check( + r#" +//- /main.rs crate:main deps:std +type F = f$0n(i32) -> i32; +//- /libstd.rs crate:std +/// Docs for prim_fn +mod prim_fn {} +"#, + expect![[r#" + *fn* + + ```rust + fn + ``` + + --- + + Docs for prim_fn + "#]], + ); +} + +#[test] +fn hover_builtin() { + check( + r#" +//- /main.rs crate:main deps:std +cosnt _: &str$0 = ""; } + +//- /libstd.rs crate:std +/// Docs for prim_str +/// [`foo`](../std/keyword.foo.html) +mod prim_str {} +"#, + expect![[r#" + *str* + + ```rust + str + ``` + + --- + + Docs for prim_str + [`foo`](https://doc.rust-lang.org/nightly/std/keyword.foo.html) + "#]], + ); +} + +#[test] +fn hover_macro_expanded_function() { + check( + r#" +struct S<'a, T>(&'a T); +trait Clone {} +macro_rules! foo { + () => { + fn bar<'t, T: Clone + 't>(s: &mut S<'t, T>, t: u32) -> *mut u32 where + 't: 't + 't, + for<'a> T: Clone + 'a + { 0 as _ } + }; +} + +foo!(); + +fn main() { + bar$0; +} +"#, + expect![[r#" + *bar* + + ```rust + test + ``` + + ```rust + fn bar<'t, T>(s: &mut S<'t, T>, t: u32) -> *mut u32 + where + T: Clone + 't, + 't: 't + 't, + for<'a> T: Clone + 'a, + ``` + "#]], + ) +} + +#[test] +fn hover_intra_doc_links() { + check( + r#" + +pub mod theitem { + /// This is the item. Cool! + pub struct TheItem; +} + +/// Gives you a [`TheItem$0`]. +/// +/// [`TheItem`]: theitem::TheItem +pub fn gimme() -> theitem::TheItem { + theitem::TheItem +} +"#, + expect![[r#" + *[`TheItem`]* + + ```rust + test::theitem + ``` + + ```rust + pub struct TheItem + ``` + + --- + + This is the item. Cool! + "#]], + ); +} + +#[test] +fn test_hover_trait_assoc_typealias() { + check( + r#" + fn main() {} + +trait T1 { + type Bar; + type Baz; +} + +struct Foo; + +mod t2 { + pub trait T2 { + type Bar; + } +} + +use t2::T2; + +impl T2 for Foo { + type Bar = String; +} + +impl T1 for Foo { + type Bar = <Foo as t2::T2>::Ba$0r; + // ^^^ unresolvedReference +} + "#, + expect![[r#" +*Bar* + +```rust +test::t2 +``` + +```rust +pub type Bar +``` +"#]], + ); +} +#[test] +fn hover_generic_assoc() { + check( + r#" +fn foo<T: A>() where T::Assoc$0: {} + +trait A { + type Assoc; +}"#, + expect![[r#" + *Assoc* + + ```rust + test + ``` + + ```rust + type Assoc + ``` + "#]], + ); + check( + r#" +fn foo<T: A>() { + let _: <T>::Assoc$0; +} + +trait A { + type Assoc; +}"#, + expect![[r#" + *Assoc* + + ```rust + test + ``` + + ```rust + type Assoc + ``` + "#]], + ); + check( + r#" +trait A where + Self::Assoc$0: , +{ + type Assoc; +}"#, + expect![[r#" + *Assoc* + + ```rust + test + ``` + + ```rust + type Assoc + ``` + "#]], + ); +} + +#[test] +fn string_shadowed_with_inner_items() { + check( + r#" +//- /main.rs crate:main deps:alloc + +/// Custom `String` type. +struct String; + +fn f() { + let _: String$0; + + fn inner() {} +} + +//- /alloc.rs crate:alloc +#[prelude_import] +pub use string::*; + +mod string { + /// This is `alloc::String`. + pub struct String; +} +"#, + expect![[r#" + *String* + + ```rust + main + ``` + + ```rust + struct String + ``` + + --- + + Custom `String` type. + "#]], + ) +} + +#[test] +fn function_doesnt_shadow_crate_in_use_tree() { + check( + r#" +//- /main.rs crate:main deps:foo +use foo$0::{foo}; + +//- /foo.rs crate:foo +pub fn foo() {} +"#, + expect![[r#" + *foo* + + ```rust + extern crate foo + ``` + "#]], + ) +} + +#[test] +fn hover_feature() { + check( + r#"#![feature(box_syntax$0)]"#, + expect![[r##" + *box_syntax* + ``` + box_syntax + ``` + ___ + + # `box_syntax` + + The tracking issue for this feature is: [#49733] + + [#49733]: https://github.com/rust-lang/rust/issues/49733 + + See also [`box_patterns`](box-patterns.md) + + ------------------------ + + Currently the only stable way to create a `Box` is via the `Box::new` method. + Also it is not possible in stable Rust to destructure a `Box` in a match + pattern. The unstable `box` keyword can be used to create a `Box`. An example + usage would be: + + ```rust + #![feature(box_syntax)] + + fn main() { + let b = box 5; + } + ``` + + "##]], + ) +} + +#[test] +fn hover_lint() { + check( + r#"#![allow(arithmetic_overflow$0)]"#, + expect![[r#" + *arithmetic_overflow* + ``` + arithmetic_overflow + ``` + ___ + + arithmetic operation overflows + "#]], + ) +} + +#[test] +fn hover_clippy_lint() { + check( + r#"#![allow(clippy::almost_swapped$0)]"#, + expect![[r#" + *almost_swapped* + ``` + clippy::almost_swapped + ``` + ___ + + Checks for `foo = bar; bar = foo` sequences. + "#]], + ) +} + +#[test] +fn hover_attr_path_qualifier() { + check( + r#" +//- /foo.rs crate:foo + +//- /lib.rs crate:main.rs deps:foo +#[fo$0o::bar()] +struct Foo; +"#, + expect![[r#" + *foo* + + ```rust + extern crate foo + ``` + "#]], + ) +} + +#[test] +fn hover_rename() { + check( + r#" +use self as foo$0; +"#, + expect![[r#" + *foo* + + ```rust + extern crate test + ``` + "#]], + ); + check( + r#" +mod bar {} +use bar::{self as foo$0}; +"#, + expect![[r#" + *foo* + + ```rust + test + ``` + + ```rust + mod bar + ``` + "#]], + ); + check( + r#" +mod bar { + use super as foo$0; +} +"#, + expect![[r#" + *foo* + + ```rust + extern crate test + ``` + "#]], + ); + check( + r#" +use crate as foo$0; +"#, + expect![[r#" + *foo* + + ```rust + extern crate test + ``` + "#]], + ); +} + +#[test] +fn hover_attribute_in_macro() { + check( + r#" +//- minicore:derive +macro_rules! identity { + ($struct:item) => { + $struct + }; +} +#[rustc_builtin_macro] +pub macro Copy {} +identity!{ + #[derive(Copy$0)] + struct Foo; +} +"#, + expect![[r#" + *Copy* + + ```rust + test + ``` + + ```rust + macro Copy + ``` + "#]], + ); +} + +#[test] +fn hover_derive_input() { + check( + r#" +//- minicore:derive +#[rustc_builtin_macro] +pub macro Copy {} +#[derive(Copy$0)] +struct Foo; +"#, + expect![[r#" + *Copy* + + ```rust + test + ``` + + ```rust + macro Copy + ``` + "#]], + ); + check( + r#" +//- minicore:derive +mod foo { + #[rustc_builtin_macro] + pub macro Copy {} +} +#[derive(foo::Copy$0)] +struct Foo; +"#, + expect![[r#" + *Copy* + + ```rust + test::foo + ``` + + ```rust + macro Copy + ``` + "#]], + ); +} + +#[test] +fn hover_range_math() { + check_hover_range( + r#" +fn f() { let expr = $01 + 2 * 3$0 } +"#, + expect![[r#" + ```rust + i32 + ```"#]], + ); + + check_hover_range( + r#" +fn f() { let expr = 1 $0+ 2 * $03 } +"#, + expect![[r#" + ```rust + i32 + ```"#]], + ); + + check_hover_range( + r#" +fn f() { let expr = 1 + $02 * 3$0 } +"#, + expect![[r#" + ```rust + i32 + ```"#]], + ); +} + +#[test] +fn hover_range_arrays() { + check_hover_range( + r#" +fn f() { let expr = $0[1, 2, 3, 4]$0 } +"#, + expect![[r#" + ```rust + [i32; 4] + ```"#]], + ); + + check_hover_range( + r#" +fn f() { let expr = [1, 2, $03, 4]$0 } +"#, + expect![[r#" + ```rust + [i32; 4] + ```"#]], + ); + + check_hover_range( + r#" +fn f() { let expr = [1, 2, $03$0, 4] } +"#, + expect![[r#" + ```rust + i32 + ```"#]], + ); +} + +#[test] +fn hover_range_functions() { + check_hover_range( + r#" +fn f<T>(a: &[T]) { } +fn b() { $0f$0(&[1, 2, 3, 4, 5]); } +"#, + expect![[r#" + ```rust + fn f<i32>(&[i32]) + ```"#]], + ); + + check_hover_range( + r#" +fn f<T>(a: &[T]) { } +fn b() { f($0&[1, 2, 3, 4, 5]$0); } +"#, + expect![[r#" + ```rust + &[i32; 5] + ```"#]], + ); +} + +#[test] +fn hover_range_shows_nothing_when_invalid() { + check_hover_range_no_results( + r#" +fn f<T>(a: &[T]) { } +fn b()$0 { f(&[1, 2, 3, 4, 5]); }$0 +"#, + ); + + check_hover_range_no_results( + r#" +fn f<T>$0(a: &[T]) { } +fn b() { f(&[1, 2, 3,$0 4, 5]); } +"#, + ); + + check_hover_range_no_results( + r#" +fn $0f() { let expr = [1, 2, 3, 4]$0 } +"#, + ); +} + +#[test] +fn hover_range_shows_unit_for_statements() { + check_hover_range( + r#" +fn f<T>(a: &[T]) { } +fn b() { $0f(&[1, 2, 3, 4, 5]); }$0 +"#, + expect![[r#" + ```rust + () + ```"#]], + ); + + check_hover_range( + r#" +fn f() { let expr$0 = $0[1, 2, 3, 4] } +"#, + expect![[r#" + ```rust + () + ```"#]], + ); +} + +#[test] +fn hover_range_for_pat() { + check_hover_range( + r#" +fn foo() { + let $0x$0 = 0; +} +"#, + expect![[r#" + ```rust + i32 + ```"#]], + ); + + check_hover_range( + r#" +fn foo() { + let $0x$0 = ""; +} +"#, + expect![[r#" + ```rust + &str + ```"#]], + ); +} + +#[test] +fn hover_range_shows_coercions_if_applicable_expr() { + check_hover_range( + r#" +fn foo() { + let x: &u32 = $0&&&&&0$0; +} +"#, + expect![[r#" + ```text + Type: &&&&&u32 + Coerced to: &u32 + ``` + "#]], + ); + check_hover_range( + r#" +fn foo() { + let x: *const u32 = $0&0$0; +} +"#, + expect![[r#" + ```text + Type: &u32 + Coerced to: *const u32 + ``` + "#]], + ); +} + +#[test] +fn hover_range_shows_type_actions() { + check_actions( + r#" +struct Foo; +fn foo() { + let x: &Foo = $0&&&&&Foo$0; +} +"#, + expect![[r#" + [ + GoToType( + [ + HoverGotoTypeData { + mod_path: "test::Foo", + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..11, + focus_range: 7..10, + name: "Foo", + kind: Struct, + description: "struct Foo", + }, + }, + ], + ), + ] + "#]], + ); +} + +#[test] +fn hover_try_expr_res() { + check_hover_range( + r#" +//- minicore:result +struct FooError; + +fn foo() -> Result<(), FooError> { + Ok($0Result::<(), FooError>::Ok(())?$0) +} +"#, + expect![[r#" + ```rust + () + ```"#]], + ); + check_hover_range( + r#" +//- minicore:result +struct FooError; +struct BarError; + +fn foo() -> Result<(), FooError> { + Ok($0Result::<(), BarError>::Ok(())?$0) +} +"#, + expect![[r#" + ```text + Try Error Type: BarError + Propagated as: FooError + ``` + "#]], + ); +} + +#[test] +fn hover_try_expr() { + check_hover_range( + r#" +struct NotResult<T, U>(T, U); +struct Short; +struct Looooong; + +fn foo() -> NotResult<(), Looooong> { + $0NotResult((), Short)?$0; +} +"#, + expect![[r#" + ```text + Try Target Type: NotResult<(), Short> + Propagated as: NotResult<(), Looooong> + ``` + "#]], + ); + check_hover_range( + r#" +struct NotResult<T, U>(T, U); +struct Short; +struct Looooong; + +fn foo() -> NotResult<(), Short> { + $0NotResult((), Looooong)?$0; +} +"#, + expect![[r#" + ```text + Try Target Type: NotResult<(), Looooong> + Propagated as: NotResult<(), Short> + ``` + "#]], + ); +} + +#[test] +fn hover_try_expr_option() { + cov_mark::check!(hover_try_expr_opt_opt); + check_hover_range( + r#" +//- minicore: option, try + +fn foo() -> Option<()> { + $0Some(0)?$0; + None +} +"#, + expect![[r#" + ```rust + <Option<i32> as Try>::Output + ```"#]], + ); +} + +#[test] +fn hover_deref_expr() { + check_hover_range( + r#" +//- minicore: deref +use core::ops::Deref; + +struct DerefExample<T> { + value: T +} + +impl<T> Deref for DerefExample<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +fn foo() { + let x = DerefExample { value: 0 }; + let y: i32 = $0*x$0; +} +"#, + expect![[r#" + ```text + Dereferenced from: DerefExample<i32> + To type: i32 + ``` + "#]], + ); +} + +#[test] +fn hover_deref_expr_with_coercion() { + check_hover_range( + r#" +//- minicore: deref +use core::ops::Deref; + +struct DerefExample<T> { + value: T +} + +impl<T> Deref for DerefExample<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +fn foo() { + let x = DerefExample { value: &&&&&0 }; + let y: &i32 = $0*x$0; +} +"#, + expect![[r#" + ```text + Dereferenced from: DerefExample<&&&&&i32> + To type: &&&&&i32 + Coerced to: &i32 + ``` + "#]], + ); +} + +#[test] +fn hover_intra_in_macro() { + check( + r#" +macro_rules! foo_macro { + ($(#[$attr:meta])* $name:ident) => { + $(#[$attr])* + pub struct $name; + } +} + +foo_macro!( + /// Doc comment for [`Foo$0`] + Foo +); +"#, + expect![[r#" + *[`Foo`]* + + ```rust + test + ``` + + ```rust + pub struct Foo + ``` + + --- + + Doc comment for [`Foo`](https://docs.rs/test/*/test/struct.Foo.html) + "#]], + ); +} + +#[test] +fn hover_intra_in_attr() { + check( + r#" +#[doc = "Doc comment for [`Foo$0`]"] +pub struct Foo; +"#, + expect![[r#" + *[`Foo`]* + + ```rust + test + ``` + + ```rust + pub struct Foo + ``` + + --- + + Doc comment for [`Foo`](https://docs.rs/test/*/test/struct.Foo.html) + "#]], + ); +} + +#[test] +fn hover_inert_attr() { + check( + r#" +#[doc$0 = ""] +pub struct Foo; +"#, + expect![[r##" + *doc* + + ```rust + #[doc] + ``` + + --- + + Valid forms are: + + * \#\[doc(hidden|inline|...)\] + * \#\[doc = string\] + "##]], + ); + check( + r#" +#[allow$0()] +pub struct Foo; +"#, + expect![[r##" + *allow* + + ```rust + #[allow] + ``` + + --- + + Valid forms are: + + * \#\[allow(lint1, lint2, ..., /\*opt\*/ reason = "...")\] + "##]], + ); +} + +#[test] +fn hover_dollar_crate() { + // $crate should be resolved to the right crate name. + + check( + r#" +//- /main.rs crate:main deps:dep +dep::m!(KONST$0); +//- /dep.rs crate:dep +#[macro_export] +macro_rules! m { + ( $name:ident ) => { const $name: $crate::Type = $crate::Type; }; +} + +pub struct Type; +"#, + expect![[r#" + *KONST* + + ```rust + main + ``` + + ```rust + const KONST: dep::Type = $crate::Type + ``` + "#]], + ); +} + +#[test] +fn hover_record_variant() { + check( + r#" +enum Enum { + RecordV$0 { field: u32 } +} +"#, + expect![[r#" + *RecordV* + + ```rust + test::Enum + ``` + + ```rust + RecordV { field: u32 } + ``` + "#]], + ); +} + +#[test] +fn hover_trait_impl_assoc_item_def_doc_forwarding() { + check( + r#" +trait T { + /// Trait docs + fn func() {} +} +impl T for () { + fn func$0() {} +} +"#, + expect![[r#" + *func* + + ```rust + test + ``` + + ```rust + fn func() + ``` + + --- + + Trait docs + "#]], + ); +} + +#[test] +fn hover_ranged_macro_call() { + check_hover_range( + r#" +macro_rules! __rust_force_expr { + ($e:expr) => { + $e + }; +} +macro_rules! vec { + ($elem:expr) => { + __rust_force_expr!($elem) + }; +} + +struct Struct; +impl Struct { + fn foo(self) {} +} + +fn f() { + $0vec![Struct]$0; +} +"#, + expect![[r#" + ```rust + Struct + ```"#]], + ); +} diff --git a/src/tools/rust-analyzer/crates/ide/src/inlay_hints.rs b/src/tools/rust-analyzer/crates/ide/src/inlay_hints.rs new file mode 100644 index 000000000..5aae669aa --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/inlay_hints.rs @@ -0,0 +1,2818 @@ +use either::Either; +use hir::{known, Callable, HasVisibility, HirDisplay, Mutability, Semantics, TypeInfo}; +use ide_db::{ + base_db::FileRange, famous_defs::FamousDefs, syntax_helpers::node_ext::walk_ty, FxHashMap, + RootDatabase, +}; +use itertools::Itertools; +use stdx::to_lower_snake_case; +use syntax::{ + ast::{self, AstNode, HasArgList, HasGenericParams, HasName, UnaryOp}, + match_ast, Direction, NodeOrToken, SmolStr, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, + TextSize, T, +}; + +use crate::FileId; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct InlayHintsConfig { + pub render_colons: bool, + pub type_hints: bool, + pub parameter_hints: bool, + pub chaining_hints: bool, + pub reborrow_hints: ReborrowHints, + pub closure_return_type_hints: ClosureReturnTypeHints, + pub binding_mode_hints: bool, + pub lifetime_elision_hints: LifetimeElisionHints, + pub param_names_for_lifetime_elision_hints: bool, + pub hide_named_constructor_hints: bool, + pub hide_closure_initialization_hints: bool, + pub max_length: Option<usize>, + pub closing_brace_hints_min_lines: Option<usize>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ClosureReturnTypeHints { + Always, + WithBlock, + Never, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LifetimeElisionHints { + Always, + SkipTrivial, + Never, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ReborrowHints { + Always, + MutableOnly, + Never, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum InlayKind { + BindingModeHint, + ChainingHint, + ClosingBraceHint, + ClosureReturnTypeHint, + GenericParamListHint, + ImplicitReborrowHint, + LifetimeHint, + ParameterHint, + TypeHint, +} + +#[derive(Debug)] +pub struct InlayHint { + pub range: TextRange, + pub kind: InlayKind, + pub label: String, + pub tooltip: Option<InlayTooltip>, +} + +#[derive(Debug)] +pub enum InlayTooltip { + String(String), + HoverRanged(FileId, TextRange), + HoverOffset(FileId, TextSize), +} + +// Feature: Inlay Hints +// +// rust-analyzer shows additional information inline with the source code. +// Editors usually render this using read-only virtual text snippets interspersed with code. +// +// rust-analyzer by default shows hints for +// +// * types of local variables +// * names of function arguments +// * types of chained expressions +// +// Optionally, one can enable additional hints for +// +// * return types of closure expressions +// * elided lifetimes +// * compiler inserted reborrows +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Toggle inlay hints* +// |=== +// +// image::https://user-images.githubusercontent.com/48062697/113020660-b5f98b80-917a-11eb-8d70-3be3fd558cdd.png[] +pub(crate) fn inlay_hints( + db: &RootDatabase, + file_id: FileId, + range_limit: Option<FileRange>, + config: &InlayHintsConfig, +) -> Vec<InlayHint> { + let _p = profile::span("inlay_hints"); + let sema = Semantics::new(db); + let file = sema.parse(file_id); + let file = file.syntax(); + + let mut acc = Vec::new(); + + if let Some(scope) = sema.scope(&file) { + let famous_defs = FamousDefs(&sema, scope.krate()); + + let hints = |node| hints(&mut acc, &famous_defs, config, file_id, node); + match range_limit { + Some(FileRange { range, .. }) => match file.covering_element(range) { + NodeOrToken::Token(_) => return acc, + NodeOrToken::Node(n) => n + .descendants() + .filter(|descendant| range.intersect(descendant.text_range()).is_some()) + .for_each(hints), + }, + None => file.descendants().for_each(hints), + }; + } + + acc +} + +fn hints( + hints: &mut Vec<InlayHint>, + famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>, + config: &InlayHintsConfig, + file_id: FileId, + node: SyntaxNode, +) { + closing_brace_hints(hints, sema, config, file_id, node.clone()); + match_ast! { + match node { + ast::Expr(expr) => { + chaining_hints(hints, sema, &famous_defs, config, file_id, &expr); + match expr { + ast::Expr::CallExpr(it) => param_name_hints(hints, sema, config, ast::Expr::from(it)), + ast::Expr::MethodCallExpr(it) => { + param_name_hints(hints, sema, config, ast::Expr::from(it)) + } + ast::Expr::ClosureExpr(it) => closure_ret_hints(hints, sema, &famous_defs, config, file_id, it), + // We could show reborrows for all expressions, but usually that is just noise to the user + // and the main point here is to show why "moving" a mutable reference doesn't necessarily move it + ast::Expr::PathExpr(_) => reborrow_hints(hints, sema, config, &expr), + _ => None, + } + }, + ast::Pat(it) => { + binding_mode_hints(hints, sema, config, &it); + if let ast::Pat::IdentPat(it) = it { + bind_pat_hints(hints, sema, config, file_id, &it); + } + Some(()) + }, + ast::Item(it) => match it { + // FIXME: record impl lifetimes so they aren't being reused in assoc item lifetime inlay hints + ast::Item::Impl(_) => None, + ast::Item::Fn(it) => fn_lifetime_fn_hints(hints, config, it), + // static type elisions + ast::Item::Static(it) => implicit_static_hints(hints, config, Either::Left(it)), + ast::Item::Const(it) => implicit_static_hints(hints, config, Either::Right(it)), + _ => None, + }, + // FIXME: fn-ptr type, dyn fn type, and trait object type elisions + ast::Type(_) => None, + _ => None, + } + }; +} + +fn closing_brace_hints( + acc: &mut Vec<InlayHint>, + sema: &Semantics<'_, RootDatabase>, + config: &InlayHintsConfig, + file_id: FileId, + node: SyntaxNode, +) -> Option<()> { + let min_lines = config.closing_brace_hints_min_lines?; + + let name = |it: ast::Name| it.syntax().text_range().start(); + + let mut closing_token; + let (label, name_offset) = if let Some(item_list) = ast::AssocItemList::cast(node.clone()) { + closing_token = item_list.r_curly_token()?; + + let parent = item_list.syntax().parent()?; + match_ast! { + match parent { + ast::Impl(imp) => { + let imp = sema.to_def(&imp)?; + let ty = imp.self_ty(sema.db); + let trait_ = imp.trait_(sema.db); + + (match trait_ { + Some(tr) => format!("impl {} for {}", tr.name(sema.db), ty.display_truncated(sema.db, config.max_length)), + None => format!("impl {}", ty.display_truncated(sema.db, config.max_length)), + }, None) + }, + ast::Trait(tr) => { + (format!("trait {}", tr.name()?), tr.name().map(name)) + }, + _ => return None, + } + } + } else if let Some(list) = ast::ItemList::cast(node.clone()) { + closing_token = list.r_curly_token()?; + + let module = ast::Module::cast(list.syntax().parent()?)?; + (format!("mod {}", module.name()?), module.name().map(name)) + } else if let Some(block) = ast::BlockExpr::cast(node.clone()) { + closing_token = block.stmt_list()?.r_curly_token()?; + + let parent = block.syntax().parent()?; + match_ast! { + match parent { + ast::Fn(it) => { + // FIXME: this could include parameters, but `HirDisplay` prints too much info + // and doesn't respect the max length either, so the hints end up way too long + (format!("fn {}", it.name()?), it.name().map(name)) + }, + ast::Static(it) => (format!("static {}", it.name()?), it.name().map(name)), + ast::Const(it) => { + if it.underscore_token().is_some() { + ("const _".into(), None) + } else { + (format!("const {}", it.name()?), it.name().map(name)) + } + }, + _ => return None, + } + } + } else if let Some(mac) = ast::MacroCall::cast(node.clone()) { + let last_token = mac.syntax().last_token()?; + if last_token.kind() != T![;] && last_token.kind() != SyntaxKind::R_CURLY { + return None; + } + closing_token = last_token; + + ( + format!("{}!", mac.path()?), + mac.path().and_then(|it| it.segment()).map(|it| it.syntax().text_range().start()), + ) + } else { + return None; + }; + + if let Some(mut next) = closing_token.next_token() { + if next.kind() == T![;] { + if let Some(tok) = next.next_token() { + closing_token = next; + next = tok; + } + } + if !(next.kind() == SyntaxKind::WHITESPACE && next.text().contains('\n')) { + // Only display the hint if the `}` is the last token on the line + return None; + } + } + + let mut lines = 1; + node.text().for_each_chunk(|s| lines += s.matches('\n').count()); + if lines < min_lines { + return None; + } + + acc.push(InlayHint { + range: closing_token.text_range(), + kind: InlayKind::ClosingBraceHint, + label, + tooltip: name_offset.map(|it| InlayTooltip::HoverOffset(file_id, it)), + }); + + None +} + +fn implicit_static_hints( + acc: &mut Vec<InlayHint>, + config: &InlayHintsConfig, + statik_or_const: Either<ast::Static, ast::Const>, +) -> Option<()> { + if config.lifetime_elision_hints != LifetimeElisionHints::Always { + return None; + } + + if let Either::Right(it) = &statik_or_const { + if ast::AssocItemList::can_cast( + it.syntax().parent().map_or(SyntaxKind::EOF, |it| it.kind()), + ) { + return None; + } + } + + if let Some(ast::Type::RefType(ty)) = statik_or_const.either(|it| it.ty(), |it| it.ty()) { + if ty.lifetime().is_none() { + let t = ty.amp_token()?; + acc.push(InlayHint { + range: t.text_range(), + kind: InlayKind::LifetimeHint, + label: "'static".to_owned(), + tooltip: Some(InlayTooltip::String("Elided static lifetime".into())), + }); + } + } + + Some(()) +} + +fn fn_lifetime_fn_hints( + acc: &mut Vec<InlayHint>, + config: &InlayHintsConfig, + func: ast::Fn, +) -> Option<()> { + if config.lifetime_elision_hints == LifetimeElisionHints::Never { + return None; + } + + let mk_lt_hint = |t: SyntaxToken, label| InlayHint { + range: t.text_range(), + kind: InlayKind::LifetimeHint, + label, + tooltip: Some(InlayTooltip::String("Elided lifetime".into())), + }; + + let param_list = func.param_list()?; + let generic_param_list = func.generic_param_list(); + let ret_type = func.ret_type(); + let self_param = param_list.self_param().filter(|it| it.amp_token().is_some()); + + let is_elided = |lt: &Option<ast::Lifetime>| match lt { + Some(lt) => matches!(lt.text().as_str(), "'_"), + None => true, + }; + + let potential_lt_refs = { + let mut acc: Vec<_> = vec![]; + if let Some(self_param) = &self_param { + let lifetime = self_param.lifetime(); + let is_elided = is_elided(&lifetime); + acc.push((None, self_param.amp_token(), lifetime, is_elided)); + } + param_list.params().filter_map(|it| Some((it.pat(), it.ty()?))).for_each(|(pat, ty)| { + // FIXME: check path types + walk_ty(&ty, &mut |ty| match ty { + ast::Type::RefType(r) => { + let lifetime = r.lifetime(); + let is_elided = is_elided(&lifetime); + acc.push(( + pat.as_ref().and_then(|it| match it { + ast::Pat::IdentPat(p) => p.name(), + _ => None, + }), + r.amp_token(), + lifetime, + is_elided, + )) + } + _ => (), + }) + }); + acc + }; + + // allocate names + let mut gen_idx_name = { + let mut gen = (0u8..).map(|idx| match idx { + idx if idx < 10 => SmolStr::from_iter(['\'', (idx + 48) as char]), + idx => format!("'{idx}").into(), + }); + move || gen.next().unwrap_or_default() + }; + let mut allocated_lifetimes = vec![]; + + let mut used_names: FxHashMap<SmolStr, usize> = + match config.param_names_for_lifetime_elision_hints { + true => generic_param_list + .iter() + .flat_map(|gpl| gpl.lifetime_params()) + .filter_map(|param| param.lifetime()) + .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0))) + .collect(), + false => Default::default(), + }; + { + let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided); + if let Some(_) = &self_param { + if let Some(_) = potential_lt_refs.next() { + allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints { + // self can't be used as a lifetime, so no need to check for collisions + "'self".into() + } else { + gen_idx_name() + }); + } + } + potential_lt_refs.for_each(|(name, ..)| { + let name = match name { + Some(it) if config.param_names_for_lifetime_elision_hints => { + if let Some(c) = used_names.get_mut(it.text().as_str()) { + *c += 1; + SmolStr::from(format!("'{text}{c}", text = it.text().as_str())) + } else { + used_names.insert(it.text().as_str().into(), 0); + SmolStr::from_iter(["\'", it.text().as_str()]) + } + } + _ => gen_idx_name(), + }; + allocated_lifetimes.push(name); + }); + } + + // fetch output lifetime if elision rule applies + let output = match potential_lt_refs.as_slice() { + [(_, _, lifetime, _), ..] if self_param.is_some() || potential_lt_refs.len() == 1 => { + match lifetime { + Some(lt) => match lt.text().as_str() { + "'_" => allocated_lifetimes.get(0).cloned(), + "'static" => None, + name => Some(name.into()), + }, + None => allocated_lifetimes.get(0).cloned(), + } + } + [..] => None, + }; + + if allocated_lifetimes.is_empty() && output.is_none() { + return None; + } + + // apply hints + // apply output if required + let mut is_trivial = true; + if let (Some(output_lt), Some(r)) = (&output, ret_type) { + if let Some(ty) = r.ty() { + walk_ty(&ty, &mut |ty| match ty { + ast::Type::RefType(ty) if ty.lifetime().is_none() => { + if let Some(amp) = ty.amp_token() { + is_trivial = false; + acc.push(mk_lt_hint(amp, output_lt.to_string())); + } + } + _ => (), + }) + } + } + + if config.lifetime_elision_hints == LifetimeElisionHints::SkipTrivial && is_trivial { + return None; + } + + let mut a = allocated_lifetimes.iter(); + for (_, amp_token, _, is_elided) in potential_lt_refs { + if is_elided { + let t = amp_token?; + let lt = a.next()?; + acc.push(mk_lt_hint(t, lt.to_string())); + } + } + + // generate generic param list things + match (generic_param_list, allocated_lifetimes.as_slice()) { + (_, []) => (), + (Some(gpl), allocated_lifetimes) => { + let angle_tok = gpl.l_angle_token()?; + let is_empty = gpl.generic_params().next().is_none(); + acc.push(InlayHint { + range: angle_tok.text_range(), + kind: InlayKind::LifetimeHint, + label: format!( + "{}{}", + allocated_lifetimes.iter().format(", "), + if is_empty { "" } else { ", " } + ), + tooltip: Some(InlayTooltip::String("Elided lifetimes".into())), + }); + } + (None, allocated_lifetimes) => acc.push(InlayHint { + range: func.name()?.syntax().text_range(), + kind: InlayKind::GenericParamListHint, + label: format!("<{}>", allocated_lifetimes.iter().format(", "),).into(), + tooltip: Some(InlayTooltip::String("Elided lifetimes".into())), + }), + } + Some(()) +} + +fn closure_ret_hints( + acc: &mut Vec<InlayHint>, + sema: &Semantics<'_, RootDatabase>, + famous_defs: &FamousDefs<'_, '_>, + config: &InlayHintsConfig, + file_id: FileId, + closure: ast::ClosureExpr, +) -> Option<()> { + if config.closure_return_type_hints == ClosureReturnTypeHints::Never { + return None; + } + + if closure.ret_type().is_some() { + return None; + } + + if !closure_has_block_body(&closure) + && config.closure_return_type_hints == ClosureReturnTypeHints::WithBlock + { + return None; + } + + let param_list = closure.param_list()?; + + let closure = sema.descend_node_into_attributes(closure.clone()).pop()?; + let ty = sema.type_of_expr(&ast::Expr::ClosureExpr(closure))?.adjusted(); + let callable = ty.as_callable(sema.db)?; + let ty = callable.return_type(); + if ty.is_unit() { + return None; + } + acc.push(InlayHint { + range: param_list.syntax().text_range(), + kind: InlayKind::ClosureReturnTypeHint, + label: hint_iterator(sema, &famous_defs, config, &ty) + .unwrap_or_else(|| ty.display_truncated(sema.db, config.max_length).to_string()), + tooltip: Some(InlayTooltip::HoverRanged(file_id, param_list.syntax().text_range())), + }); + Some(()) +} + +fn reborrow_hints( + acc: &mut Vec<InlayHint>, + sema: &Semantics<'_, RootDatabase>, + config: &InlayHintsConfig, + expr: &ast::Expr, +) -> Option<()> { + if config.reborrow_hints == ReborrowHints::Never { + return None; + } + + let descended = sema.descend_node_into_attributes(expr.clone()).pop(); + let desc_expr = descended.as_ref().unwrap_or(expr); + let mutability = sema.is_implicit_reborrow(desc_expr)?; + let label = match mutability { + hir::Mutability::Shared if config.reborrow_hints != ReborrowHints::MutableOnly => "&*", + hir::Mutability::Mut => "&mut *", + _ => return None, + }; + acc.push(InlayHint { + range: expr.syntax().text_range(), + kind: InlayKind::ImplicitReborrowHint, + label: label.to_string(), + tooltip: Some(InlayTooltip::String("Compiler inserted reborrow".into())), + }); + Some(()) +} + +fn chaining_hints( + acc: &mut Vec<InlayHint>, + sema: &Semantics<'_, RootDatabase>, + famous_defs: &FamousDefs<'_, '_>, + config: &InlayHintsConfig, + file_id: FileId, + expr: &ast::Expr, +) -> Option<()> { + if !config.chaining_hints { + return None; + } + + if matches!(expr, ast::Expr::RecordExpr(_)) { + return None; + } + + let descended = sema.descend_node_into_attributes(expr.clone()).pop(); + let desc_expr = descended.as_ref().unwrap_or(expr); + + let mut tokens = expr + .syntax() + .siblings_with_tokens(Direction::Next) + .filter_map(NodeOrToken::into_token) + .filter(|t| match t.kind() { + SyntaxKind::WHITESPACE if !t.text().contains('\n') => false, + SyntaxKind::COMMENT => false, + _ => true, + }); + + // Chaining can be defined as an expression whose next sibling tokens are newline and dot + // Ignoring extra whitespace and comments + let next = tokens.next()?.kind(); + if next == SyntaxKind::WHITESPACE { + let mut next_next = tokens.next()?.kind(); + while next_next == SyntaxKind::WHITESPACE { + next_next = tokens.next()?.kind(); + } + if next_next == T![.] { + let ty = sema.type_of_expr(desc_expr)?.original; + if ty.is_unknown() { + return None; + } + if matches!(expr, ast::Expr::PathExpr(_)) { + if let Some(hir::Adt::Struct(st)) = ty.as_adt() { + if st.fields(sema.db).is_empty() { + return None; + } + } + } + acc.push(InlayHint { + range: expr.syntax().text_range(), + kind: InlayKind::ChainingHint, + label: hint_iterator(sema, &famous_defs, config, &ty).unwrap_or_else(|| { + ty.display_truncated(sema.db, config.max_length).to_string() + }), + tooltip: Some(InlayTooltip::HoverRanged(file_id, expr.syntax().text_range())), + }); + } + } + Some(()) +} + +fn param_name_hints( + acc: &mut Vec<InlayHint>, + sema: &Semantics<'_, RootDatabase>, + config: &InlayHintsConfig, + expr: ast::Expr, +) -> Option<()> { + if !config.parameter_hints { + return None; + } + + let (callable, arg_list) = get_callable(sema, &expr)?; + let hints = callable + .params(sema.db) + .into_iter() + .zip(arg_list.args()) + .filter_map(|((param, _ty), arg)| { + // Only annotate hints for expressions that exist in the original file + let range = sema.original_range_opt(arg.syntax())?; + let (param_name, name_syntax) = match param.as_ref()? { + Either::Left(pat) => ("self".to_string(), pat.name()), + Either::Right(pat) => match pat { + ast::Pat::IdentPat(it) => (it.name()?.to_string(), it.name()), + _ => return None, + }, + }; + Some((name_syntax, param_name, arg, range)) + }) + .filter(|(_, param_name, arg, _)| { + !should_hide_param_name_hint(sema, &callable, param_name, arg) + }) + .map(|(param, param_name, _, FileRange { range, .. })| { + let mut tooltip = None; + if let Some(name) = param { + if let hir::CallableKind::Function(f) = callable.kind() { + // assert the file is cached so we can map out of macros + if let Some(_) = sema.source(f) { + tooltip = sema.original_range_opt(name.syntax()); + } + } + } + + InlayHint { + range, + kind: InlayKind::ParameterHint, + label: param_name, + tooltip: tooltip.map(|it| InlayTooltip::HoverOffset(it.file_id, it.range.start())), + } + }); + + acc.extend(hints); + Some(()) +} + +fn binding_mode_hints( + acc: &mut Vec<InlayHint>, + sema: &Semantics<'_, RootDatabase>, + config: &InlayHintsConfig, + pat: &ast::Pat, +) -> Option<()> { + if !config.binding_mode_hints { + return None; + } + + let range = pat.syntax().text_range(); + sema.pattern_adjustments(&pat).iter().for_each(|ty| { + let reference = ty.is_reference(); + let mut_reference = ty.is_mutable_reference(); + let r = match (reference, mut_reference) { + (true, true) => "&mut", + (true, false) => "&", + _ => return, + }; + acc.push(InlayHint { + range, + kind: InlayKind::BindingModeHint, + label: r.to_string(), + tooltip: Some(InlayTooltip::String("Inferred binding mode".into())), + }); + }); + match pat { + ast::Pat::IdentPat(pat) if pat.ref_token().is_none() && pat.mut_token().is_none() => { + let bm = sema.binding_mode_of_pat(pat)?; + let bm = match bm { + hir::BindingMode::Move => return None, + hir::BindingMode::Ref(Mutability::Mut) => "ref mut", + hir::BindingMode::Ref(Mutability::Shared) => "ref", + }; + acc.push(InlayHint { + range, + kind: InlayKind::BindingModeHint, + label: bm.to_string(), + tooltip: Some(InlayTooltip::String("Inferred binding mode".into())), + }); + } + _ => (), + } + + Some(()) +} + +fn bind_pat_hints( + acc: &mut Vec<InlayHint>, + sema: &Semantics<'_, RootDatabase>, + config: &InlayHintsConfig, + file_id: FileId, + pat: &ast::IdentPat, +) -> Option<()> { + if !config.type_hints { + return None; + } + + let descended = sema.descend_node_into_attributes(pat.clone()).pop(); + let desc_pat = descended.as_ref().unwrap_or(pat); + let ty = sema.type_of_pat(&desc_pat.clone().into())?.original; + + if should_not_display_type_hint(sema, config, pat, &ty) { + return None; + } + + let krate = sema.scope(desc_pat.syntax())?.krate(); + let famous_defs = FamousDefs(sema, krate); + let label = hint_iterator(sema, &famous_defs, config, &ty); + + let label = match label { + Some(label) => label, + None => { + let ty_name = ty.display_truncated(sema.db, config.max_length).to_string(); + if config.hide_named_constructor_hints + && is_named_constructor(sema, pat, &ty_name).is_some() + { + return None; + } + ty_name + } + }; + + acc.push(InlayHint { + range: match pat.name() { + Some(name) => name.syntax().text_range(), + None => pat.syntax().text_range(), + }, + kind: InlayKind::TypeHint, + label, + tooltip: pat + .name() + .map(|it| it.syntax().text_range()) + .map(|it| InlayTooltip::HoverRanged(file_id, it)), + }); + + Some(()) +} + +fn is_named_constructor( + sema: &Semantics<'_, RootDatabase>, + pat: &ast::IdentPat, + ty_name: &str, +) -> Option<()> { + let let_node = pat.syntax().parent()?; + let expr = match_ast! { + match let_node { + ast::LetStmt(it) => it.initializer(), + ast::LetExpr(it) => it.expr(), + _ => None, + } + }?; + + let expr = sema.descend_node_into_attributes(expr.clone()).pop().unwrap_or(expr); + // unwrap postfix expressions + let expr = match expr { + ast::Expr::TryExpr(it) => it.expr(), + ast::Expr::AwaitExpr(it) => it.expr(), + expr => Some(expr), + }?; + let expr = match expr { + ast::Expr::CallExpr(call) => match call.expr()? { + ast::Expr::PathExpr(path) => path, + _ => return None, + }, + ast::Expr::PathExpr(path) => path, + _ => return None, + }; + let path = expr.path()?; + + let callable = sema.type_of_expr(&ast::Expr::PathExpr(expr))?.original.as_callable(sema.db); + let callable_kind = callable.map(|it| it.kind()); + let qual_seg = match callable_kind { + Some(hir::CallableKind::Function(_) | hir::CallableKind::TupleEnumVariant(_)) => { + path.qualifier()?.segment() + } + _ => path.segment(), + }?; + + let ctor_name = match qual_seg.kind()? { + ast::PathSegmentKind::Name(name_ref) => { + match qual_seg.generic_arg_list().map(|it| it.generic_args()) { + Some(generics) => format!("{}<{}>", name_ref, generics.format(", ")), + None => name_ref.to_string(), + } + } + ast::PathSegmentKind::Type { type_ref: Some(ty), trait_ref: None } => ty.to_string(), + _ => return None, + }; + (ctor_name == ty_name).then(|| ()) +} + +/// Checks if the type is an Iterator from std::iter and replaces its hint with an `impl Iterator<Item = Ty>`. +fn hint_iterator( + sema: &Semantics<'_, RootDatabase>, + famous_defs: &FamousDefs<'_, '_>, + config: &InlayHintsConfig, + ty: &hir::Type, +) -> Option<String> { + let db = sema.db; + let strukt = ty.strip_references().as_adt()?; + let krate = strukt.module(db).krate(); + if krate != famous_defs.core()? { + return None; + } + let iter_trait = famous_defs.core_iter_Iterator()?; + let iter_mod = famous_defs.core_iter()?; + + // Assert that this struct comes from `core::iter`. + if !(strukt.visibility(db) == hir::Visibility::Public + && strukt.module(db).path_to_root(db).contains(&iter_mod)) + { + return None; + } + + if ty.impls_trait(db, iter_trait, &[]) { + let assoc_type_item = iter_trait.items(db).into_iter().find_map(|item| match item { + hir::AssocItem::TypeAlias(alias) if alias.name(db) == known::Item => Some(alias), + _ => None, + })?; + if let Some(ty) = ty.normalize_trait_assoc_type(db, &[], assoc_type_item) { + const LABEL_START: &str = "impl Iterator<Item = "; + const LABEL_END: &str = ">"; + + let ty_display = hint_iterator(sema, famous_defs, config, &ty) + .map(|assoc_type_impl| assoc_type_impl.to_string()) + .unwrap_or_else(|| { + ty.display_truncated( + db, + config + .max_length + .map(|len| len.saturating_sub(LABEL_START.len() + LABEL_END.len())), + ) + .to_string() + }); + return Some(format!("{}{}{}", LABEL_START, ty_display, LABEL_END)); + } + } + + None +} + +fn pat_is_enum_variant(db: &RootDatabase, bind_pat: &ast::IdentPat, pat_ty: &hir::Type) -> bool { + if let Some(hir::Adt::Enum(enum_data)) = pat_ty.as_adt() { + let pat_text = bind_pat.to_string(); + enum_data + .variants(db) + .into_iter() + .map(|variant| variant.name(db).to_smol_str()) + .any(|enum_name| enum_name == pat_text) + } else { + false + } +} + +fn should_not_display_type_hint( + sema: &Semantics<'_, RootDatabase>, + config: &InlayHintsConfig, + bind_pat: &ast::IdentPat, + pat_ty: &hir::Type, +) -> bool { + let db = sema.db; + + if pat_ty.is_unknown() { + return true; + } + + if let Some(hir::Adt::Struct(s)) = pat_ty.as_adt() { + if s.fields(db).is_empty() && s.name(db).to_smol_str() == bind_pat.to_string() { + return true; + } + } + + if config.hide_closure_initialization_hints { + if let Some(parent) = bind_pat.syntax().parent() { + if let Some(it) = ast::LetStmt::cast(parent.clone()) { + if let Some(ast::Expr::ClosureExpr(closure)) = it.initializer() { + if closure_has_block_body(&closure) { + return true; + } + } + } + } + } + + for node in bind_pat.syntax().ancestors() { + match_ast! { + match node { + ast::LetStmt(it) => return it.ty().is_some(), + // FIXME: We might wanna show type hints in parameters for non-top level patterns as well + ast::Param(it) => return it.ty().is_some(), + ast::MatchArm(_) => return pat_is_enum_variant(db, bind_pat, pat_ty), + ast::LetExpr(_) => return pat_is_enum_variant(db, bind_pat, pat_ty), + ast::IfExpr(_) => return false, + ast::WhileExpr(_) => return false, + ast::ForExpr(it) => { + // We *should* display hint only if user provided "in {expr}" and we know the type of expr (and it's not unit). + // Type of expr should be iterable. + return it.in_token().is_none() || + it.iterable() + .and_then(|iterable_expr| sema.type_of_expr(&iterable_expr)) + .map(TypeInfo::original) + .map_or(true, |iterable_ty| iterable_ty.is_unknown() || iterable_ty.is_unit()) + }, + _ => (), + } + } + } + false +} + +fn closure_has_block_body(closure: &ast::ClosureExpr) -> bool { + matches!(closure.body(), Some(ast::Expr::BlockExpr(_))) +} + +fn should_hide_param_name_hint( + sema: &Semantics<'_, RootDatabase>, + callable: &hir::Callable, + param_name: &str, + argument: &ast::Expr, +) -> bool { + // These are to be tested in the `parameter_hint_heuristics` test + // hide when: + // - the parameter name is a suffix of the function's name + // - the argument is a qualified constructing or call expression where the qualifier is an ADT + // - exact argument<->parameter match(ignoring leading underscore) or parameter is a prefix/suffix + // of argument with _ splitting it off + // - param starts with `ra_fixture` + // - param is a well known name in a unary function + + let param_name = param_name.trim_start_matches('_'); + if param_name.is_empty() { + return true; + } + + if matches!(argument, ast::Expr::PrefixExpr(prefix) if prefix.op_kind() == Some(UnaryOp::Not)) { + return false; + } + + let fn_name = match callable.kind() { + hir::CallableKind::Function(it) => Some(it.name(sema.db).to_smol_str()), + _ => None, + }; + let fn_name = fn_name.as_deref(); + is_param_name_suffix_of_fn_name(param_name, callable, fn_name) + || is_argument_similar_to_param_name(argument, param_name) + || param_name.starts_with("ra_fixture") + || (callable.n_params() == 1 && is_obvious_param(param_name)) + || is_adt_constructor_similar_to_param_name(sema, argument, param_name) +} + +fn is_argument_similar_to_param_name(argument: &ast::Expr, param_name: &str) -> bool { + // check whether param_name and argument are the same or + // whether param_name is a prefix/suffix of argument(split at `_`) + let argument = match get_string_representation(argument) { + Some(argument) => argument, + None => return false, + }; + + // std is honestly too panic happy... + let str_split_at = |str: &str, at| str.is_char_boundary(at).then(|| argument.split_at(at)); + + let param_name = param_name.trim_start_matches('_'); + let argument = argument.trim_start_matches('_'); + + match str_split_at(argument, param_name.len()) { + Some((prefix, rest)) if prefix.eq_ignore_ascii_case(param_name) => { + return rest.is_empty() || rest.starts_with('_'); + } + _ => (), + } + match argument.len().checked_sub(param_name.len()).and_then(|at| str_split_at(argument, at)) { + Some((rest, suffix)) if param_name.eq_ignore_ascii_case(suffix) => { + return rest.is_empty() || rest.ends_with('_'); + } + _ => (), + } + false +} + +/// Hide the parameter name of a unary function if it is a `_` - prefixed suffix of the function's name, or equal. +/// +/// `fn strip_suffix(suffix)` will be hidden. +/// `fn stripsuffix(suffix)` will not be hidden. +fn is_param_name_suffix_of_fn_name( + param_name: &str, + callable: &Callable, + fn_name: Option<&str>, +) -> bool { + match (callable.n_params(), fn_name) { + (1, Some(function)) => { + function == param_name + || function + .len() + .checked_sub(param_name.len()) + .and_then(|at| function.is_char_boundary(at).then(|| function.split_at(at))) + .map_or(false, |(prefix, suffix)| { + suffix.eq_ignore_ascii_case(param_name) && prefix.ends_with('_') + }) + } + _ => false, + } +} + +fn is_adt_constructor_similar_to_param_name( + sema: &Semantics<'_, RootDatabase>, + argument: &ast::Expr, + param_name: &str, +) -> bool { + let path = match argument { + ast::Expr::CallExpr(c) => c.expr().and_then(|e| match e { + ast::Expr::PathExpr(p) => p.path(), + _ => None, + }), + ast::Expr::PathExpr(p) => p.path(), + ast::Expr::RecordExpr(r) => r.path(), + _ => return false, + }; + let path = match path { + Some(it) => it, + None => return false, + }; + (|| match sema.resolve_path(&path)? { + hir::PathResolution::Def(hir::ModuleDef::Adt(_)) => { + Some(to_lower_snake_case(&path.segment()?.name_ref()?.text()) == param_name) + } + hir::PathResolution::Def(hir::ModuleDef::Function(_) | hir::ModuleDef::Variant(_)) => { + if to_lower_snake_case(&path.segment()?.name_ref()?.text()) == param_name { + return Some(true); + } + let qual = path.qualifier()?; + match sema.resolve_path(&qual)? { + hir::PathResolution::Def(hir::ModuleDef::Adt(_)) => { + Some(to_lower_snake_case(&qual.segment()?.name_ref()?.text()) == param_name) + } + _ => None, + } + } + _ => None, + })() + .unwrap_or(false) +} + +fn get_string_representation(expr: &ast::Expr) -> Option<String> { + match expr { + ast::Expr::MethodCallExpr(method_call_expr) => { + let name_ref = method_call_expr.name_ref()?; + match name_ref.text().as_str() { + "clone" | "as_ref" => method_call_expr.receiver().map(|rec| rec.to_string()), + name_ref => Some(name_ref.to_owned()), + } + } + ast::Expr::MacroExpr(macro_expr) => { + Some(macro_expr.macro_call()?.path()?.segment()?.to_string()) + } + ast::Expr::FieldExpr(field_expr) => Some(field_expr.name_ref()?.to_string()), + ast::Expr::PathExpr(path_expr) => Some(path_expr.path()?.segment()?.to_string()), + ast::Expr::PrefixExpr(prefix_expr) => get_string_representation(&prefix_expr.expr()?), + ast::Expr::RefExpr(ref_expr) => get_string_representation(&ref_expr.expr()?), + ast::Expr::CastExpr(cast_expr) => get_string_representation(&cast_expr.expr()?), + _ => None, + } +} + +fn is_obvious_param(param_name: &str) -> bool { + // avoid displaying hints for common functions like map, filter, etc. + // or other obvious words used in std + let is_obvious_param_name = + matches!(param_name, "predicate" | "value" | "pat" | "rhs" | "other"); + param_name.len() == 1 || is_obvious_param_name +} + +fn get_callable( + sema: &Semantics<'_, RootDatabase>, + expr: &ast::Expr, +) -> Option<(hir::Callable, ast::ArgList)> { + match expr { + ast::Expr::CallExpr(expr) => { + let descended = sema.descend_node_into_attributes(expr.clone()).pop(); + let expr = descended.as_ref().unwrap_or(expr); + sema.type_of_expr(&expr.expr()?)?.original.as_callable(sema.db).zip(expr.arg_list()) + } + ast::Expr::MethodCallExpr(expr) => { + let descended = sema.descend_node_into_attributes(expr.clone()).pop(); + let expr = descended.as_ref().unwrap_or(expr); + sema.resolve_method_call_as_callable(expr).zip(expr.arg_list()) + } + _ => None, + } +} + +#[cfg(test)] +mod tests { + use expect_test::{expect, Expect}; + use ide_db::base_db::FileRange; + use itertools::Itertools; + use syntax::{TextRange, TextSize}; + use test_utils::extract_annotations; + + use crate::inlay_hints::ReborrowHints; + use crate::{fixture, inlay_hints::InlayHintsConfig, LifetimeElisionHints}; + + use super::ClosureReturnTypeHints; + + const DISABLED_CONFIG: InlayHintsConfig = InlayHintsConfig { + render_colons: false, + type_hints: false, + parameter_hints: false, + chaining_hints: false, + lifetime_elision_hints: LifetimeElisionHints::Never, + closure_return_type_hints: ClosureReturnTypeHints::Never, + reborrow_hints: ReborrowHints::Always, + binding_mode_hints: false, + hide_named_constructor_hints: false, + hide_closure_initialization_hints: false, + param_names_for_lifetime_elision_hints: false, + max_length: None, + closing_brace_hints_min_lines: None, + }; + const TEST_CONFIG: InlayHintsConfig = InlayHintsConfig { + type_hints: true, + parameter_hints: true, + chaining_hints: true, + reborrow_hints: ReborrowHints::Always, + closure_return_type_hints: ClosureReturnTypeHints::WithBlock, + binding_mode_hints: true, + lifetime_elision_hints: LifetimeElisionHints::Always, + ..DISABLED_CONFIG + }; + + #[track_caller] + fn check(ra_fixture: &str) { + check_with_config(TEST_CONFIG, ra_fixture); + } + + #[track_caller] + fn check_params(ra_fixture: &str) { + check_with_config( + InlayHintsConfig { parameter_hints: true, ..DISABLED_CONFIG }, + ra_fixture, + ); + } + + #[track_caller] + fn check_types(ra_fixture: &str) { + check_with_config(InlayHintsConfig { type_hints: true, ..DISABLED_CONFIG }, ra_fixture); + } + + #[track_caller] + fn check_chains(ra_fixture: &str) { + check_with_config(InlayHintsConfig { chaining_hints: true, ..DISABLED_CONFIG }, ra_fixture); + } + + #[track_caller] + fn check_with_config(config: InlayHintsConfig, ra_fixture: &str) { + let (analysis, file_id) = fixture::file(ra_fixture); + let mut expected = extract_annotations(&*analysis.file_text(file_id).unwrap()); + let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap(); + let actual = inlay_hints + .into_iter() + .map(|it| (it.range, it.label.to_string())) + .sorted_by_key(|(range, _)| range.start()) + .collect::<Vec<_>>(); + expected.sort_by_key(|(range, _)| range.start()); + + assert_eq!(expected, actual, "\nExpected:\n{:#?}\n\nActual:\n{:#?}", expected, actual); + } + + #[track_caller] + fn check_expect(config: InlayHintsConfig, ra_fixture: &str, expect: Expect) { + let (analysis, file_id) = fixture::file(ra_fixture); + let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap(); + expect.assert_debug_eq(&inlay_hints) + } + + #[test] + fn hints_disabled() { + check_with_config( + InlayHintsConfig { render_colons: true, ..DISABLED_CONFIG }, + r#" +fn foo(a: i32, b: i32) -> i32 { a + b } +fn main() { + let _x = foo(4, 4); +}"#, + ); + } + + // Parameter hint tests + + #[test] + fn param_hints_only() { + check_params( + r#" +fn foo(a: i32, b: i32) -> i32 { a + b } +fn main() { + let _x = foo( + 4, + //^ a + 4, + //^ b + ); +}"#, + ); + } + + #[test] + fn param_hints_on_closure() { + check_params( + r#" +fn main() { + let clo = |a: u8, b: u8| a + b; + clo( + 1, + //^ a + 2, + //^ b + ); +} + "#, + ); + } + + #[test] + fn param_name_similar_to_fn_name_still_hints() { + check_params( + r#" +fn max(x: i32, y: i32) -> i32 { x + y } +fn main() { + let _x = max( + 4, + //^ x + 4, + //^ y + ); +}"#, + ); + } + + #[test] + fn param_name_similar_to_fn_name() { + check_params( + r#" +fn param_with_underscore(with_underscore: i32) -> i32 { with_underscore } +fn main() { + let _x = param_with_underscore( + 4, + ); +}"#, + ); + check_params( + r#" +fn param_with_underscore(underscore: i32) -> i32 { underscore } +fn main() { + let _x = param_with_underscore( + 4, + ); +}"#, + ); + } + + #[test] + fn param_name_same_as_fn_name() { + check_params( + r#" +fn foo(foo: i32) -> i32 { foo } +fn main() { + let _x = foo( + 4, + ); +}"#, + ); + } + + #[test] + fn never_hide_param_when_multiple_params() { + check_params( + r#" +fn foo(foo: i32, bar: i32) -> i32 { bar + baz } +fn main() { + let _x = foo( + 4, + //^ foo + 8, + //^ bar + ); +}"#, + ); + } + + #[test] + fn param_hints_look_through_as_ref_and_clone() { + check_params( + r#" +fn foo(bar: i32, baz: f32) {} + +fn main() { + let bar = 3; + let baz = &"baz"; + let fez = 1.0; + foo(bar.clone(), bar.clone()); + //^^^^^^^^^^^ baz + foo(bar.as_ref(), bar.as_ref()); + //^^^^^^^^^^^^ baz +} +"#, + ); + } + + #[test] + fn self_param_hints() { + check_params( + r#" +struct Foo; + +impl Foo { + fn foo(self: Self) {} + fn bar(self: &Self) {} +} + +fn main() { + Foo::foo(Foo); + //^^^ self + Foo::bar(&Foo); + //^^^^ self +} +"#, + ) + } + + #[test] + fn param_name_hints_show_for_literals() { + check_params( + r#"pub fn test(a: i32, b: i32) -> [i32; 2] { [a, b] } +fn main() { + test( + 0xa_b, + //^^^^^ a + 0xa_b, + //^^^^^ b + ); +}"#, + ) + } + + #[test] + fn function_call_parameter_hint() { + check_params( + r#" +//- minicore: option +struct FileId {} +struct SmolStr {} + +struct TextRange {} +struct SyntaxKind {} +struct NavigationTarget {} + +struct Test {} + +impl Test { + fn method(&self, mut param: i32) -> i32 { param * 2 } + + fn from_syntax( + file_id: FileId, + name: SmolStr, + focus_range: Option<TextRange>, + full_range: TextRange, + kind: SyntaxKind, + docs: Option<String>, + ) -> NavigationTarget { + NavigationTarget {} + } +} + +fn test_func(mut foo: i32, bar: i32, msg: &str, _: i32, last: i32) -> i32 { + foo + bar +} + +fn main() { + let not_literal = 1; + let _: i32 = test_func(1, 2, "hello", 3, not_literal); + //^ foo ^ bar ^^^^^^^ msg ^^^^^^^^^^^ last + let t: Test = Test {}; + t.method(123); + //^^^ param + Test::method(&t, 3456); + //^^ self ^^^^ param + Test::from_syntax( + FileId {}, + "impl".into(), + //^^^^^^^^^^^^^ name + None, + //^^^^ focus_range + TextRange {}, + //^^^^^^^^^^^^ full_range + SyntaxKind {}, + //^^^^^^^^^^^^^ kind + None, + //^^^^ docs + ); +}"#, + ); + } + + #[test] + fn parameter_hint_heuristics() { + check_params( + r#" +fn check(ra_fixture_thing: &str) {} + +fn map(f: i32) {} +fn filter(predicate: i32) {} + +fn strip_suffix(suffix: &str) {} +fn stripsuffix(suffix: &str) {} +fn same(same: u32) {} +fn same2(_same2: u32) {} + +fn enum_matches_param_name(completion_kind: CompletionKind) {} + +fn foo(param: u32) {} +fn bar(param_eter: u32) {} + +enum CompletionKind { + Keyword, +} + +fn non_ident_pat((a, b): (u32, u32)) {} + +fn main() { + const PARAM: u32 = 0; + foo(PARAM); + foo(!PARAM); + // ^^^^^^ param + check(""); + + map(0); + filter(0); + + strip_suffix(""); + stripsuffix(""); + //^^ suffix + same(0); + same2(0); + + enum_matches_param_name(CompletionKind::Keyword); + + let param = 0; + foo(param); + foo(param as _); + let param_end = 0; + foo(param_end); + let start_param = 0; + foo(start_param); + let param2 = 0; + foo(param2); + //^^^^^^ param + + macro_rules! param { + () => {}; + }; + foo(param!()); + + let param_eter = 0; + bar(param_eter); + let param_eter_end = 0; + bar(param_eter_end); + let start_param_eter = 0; + bar(start_param_eter); + let param_eter2 = 0; + bar(param_eter2); + //^^^^^^^^^^^ param_eter + + non_ident_pat((0, 0)); +}"#, + ); + } + + // Type-Hint tests + + #[test] + fn type_hints_only() { + check_types( + r#" +fn foo(a: i32, b: i32) -> i32 { a + b } +fn main() { + let _x = foo(4, 4); + //^^ i32 +}"#, + ); + } + + #[test] + fn type_hints_bindings_after_at() { + check_types( + r#" +//- minicore: option +fn main() { + let ref foo @ bar @ ref mut baz = 0; + //^^^ &i32 + //^^^ i32 + //^^^ &mut i32 + let [x @ ..] = [0]; + //^ [i32; 1] + if let x @ Some(_) = Some(0) {} + //^ Option<i32> + let foo @ (bar, baz) = (3, 3); + //^^^ (i32, i32) + //^^^ i32 + //^^^ i32 +}"#, + ); + } + + #[test] + fn default_generic_types_should_not_be_displayed() { + check( + r#" +struct Test<K, T = u8> { k: K, t: T } + +fn main() { + let zz = Test { t: 23u8, k: 33 }; + //^^ Test<i32> + let zz_ref = &zz; + //^^^^^^ &Test<i32> + let test = || zz; + //^^^^ || -> Test<i32> +}"#, + ); + } + + #[test] + fn shorten_iterators_in_associated_params() { + check_types( + r#" +//- minicore: iterators +use core::iter; + +pub struct SomeIter<T> {} + +impl<T> SomeIter<T> { + pub fn new() -> Self { SomeIter {} } + pub fn push(&mut self, t: T) {} +} + +impl<T> Iterator for SomeIter<T> { + type Item = T; + fn next(&mut self) -> Option<Self::Item> { + None + } +} + +fn main() { + let mut some_iter = SomeIter::new(); + //^^^^^^^^^ SomeIter<Take<Repeat<i32>>> + some_iter.push(iter::repeat(2).take(2)); + let iter_of_iters = some_iter.take(2); + //^^^^^^^^^^^^^ impl Iterator<Item = impl Iterator<Item = i32>> +} +"#, + ); + } + + #[test] + fn infer_call_method_return_associated_types_with_generic() { + check_types( + r#" + pub trait Default { + fn default() -> Self; + } + pub trait Foo { + type Bar: Default; + } + + pub fn quux<T: Foo>() -> T::Bar { + let y = Default::default(); + //^ <T as Foo>::Bar + + y + } + "#, + ); + } + + #[test] + fn fn_hints() { + check_types( + r#" +//- minicore: fn, sized +fn foo() -> impl Fn() { loop {} } +fn foo1() -> impl Fn(f64) { loop {} } +fn foo2() -> impl Fn(f64, f64) { loop {} } +fn foo3() -> impl Fn(f64, f64) -> u32 { loop {} } +fn foo4() -> &'static dyn Fn(f64, f64) -> u32 { loop {} } +fn foo5() -> &'static dyn Fn(&'static dyn Fn(f64, f64) -> u32, f64) -> u32 { loop {} } +fn foo6() -> impl Fn(f64, f64) -> u32 + Sized { loop {} } +fn foo7() -> *const (impl Fn(f64, f64) -> u32 + Sized) { loop {} } + +fn main() { + let foo = foo(); + // ^^^ impl Fn() + let foo = foo1(); + // ^^^ impl Fn(f64) + let foo = foo2(); + // ^^^ impl Fn(f64, f64) + let foo = foo3(); + // ^^^ impl Fn(f64, f64) -> u32 + let foo = foo4(); + // ^^^ &dyn Fn(f64, f64) -> u32 + let foo = foo5(); + // ^^^ &dyn Fn(&dyn Fn(f64, f64) -> u32, f64) -> u32 + let foo = foo6(); + // ^^^ impl Fn(f64, f64) -> u32 + let foo = foo7(); + // ^^^ *const impl Fn(f64, f64) -> u32 +} +"#, + ) + } + + #[test] + fn check_hint_range_limit() { + let fixture = r#" + //- minicore: fn, sized + fn foo() -> impl Fn() { loop {} } + fn foo1() -> impl Fn(f64) { loop {} } + fn foo2() -> impl Fn(f64, f64) { loop {} } + fn foo3() -> impl Fn(f64, f64) -> u32 { loop {} } + fn foo4() -> &'static dyn Fn(f64, f64) -> u32 { loop {} } + fn foo5() -> &'static dyn Fn(&'static dyn Fn(f64, f64) -> u32, f64) -> u32 { loop {} } + fn foo6() -> impl Fn(f64, f64) -> u32 + Sized { loop {} } + fn foo7() -> *const (impl Fn(f64, f64) -> u32 + Sized) { loop {} } + + fn main() { + let foo = foo(); + let foo = foo1(); + let foo = foo2(); + // ^^^ impl Fn(f64, f64) + let foo = foo3(); + // ^^^ impl Fn(f64, f64) -> u32 + let foo = foo4(); + let foo = foo5(); + let foo = foo6(); + let foo = foo7(); + } + "#; + let (analysis, file_id) = fixture::file(fixture); + let expected = extract_annotations(&*analysis.file_text(file_id).unwrap()); + let inlay_hints = analysis + .inlay_hints( + &InlayHintsConfig { type_hints: true, ..DISABLED_CONFIG }, + file_id, + Some(FileRange { + file_id, + range: TextRange::new(TextSize::from(500), TextSize::from(600)), + }), + ) + .unwrap(); + let actual = + inlay_hints.into_iter().map(|it| (it.range, it.label.to_string())).collect::<Vec<_>>(); + assert_eq!(expected, actual, "\nExpected:\n{:#?}\n\nActual:\n{:#?}", expected, actual); + } + + #[test] + fn fn_hints_ptr_rpit_fn_parentheses() { + check_types( + r#" +//- minicore: fn, sized +trait Trait {} + +fn foo1() -> *const impl Fn() { loop {} } +fn foo2() -> *const (impl Fn() + Sized) { loop {} } +fn foo3() -> *const (impl Fn() + ?Sized) { loop {} } +fn foo4() -> *const (impl Sized + Fn()) { loop {} } +fn foo5() -> *const (impl ?Sized + Fn()) { loop {} } +fn foo6() -> *const (impl Fn() + Trait) { loop {} } +fn foo7() -> *const (impl Fn() + Sized + Trait) { loop {} } +fn foo8() -> *const (impl Fn() + ?Sized + Trait) { loop {} } +fn foo9() -> *const (impl Fn() -> u8 + ?Sized) { loop {} } +fn foo10() -> *const (impl Fn() + Sized + ?Sized) { loop {} } + +fn main() { + let foo = foo1(); + // ^^^ *const impl Fn() + let foo = foo2(); + // ^^^ *const impl Fn() + let foo = foo3(); + // ^^^ *const (impl Fn() + ?Sized) + let foo = foo4(); + // ^^^ *const impl Fn() + let foo = foo5(); + // ^^^ *const (impl Fn() + ?Sized) + let foo = foo6(); + // ^^^ *const (impl Fn() + Trait) + let foo = foo7(); + // ^^^ *const (impl Fn() + Trait) + let foo = foo8(); + // ^^^ *const (impl Fn() + Trait + ?Sized) + let foo = foo9(); + // ^^^ *const (impl Fn() -> u8 + ?Sized) + let foo = foo10(); + // ^^^ *const impl Fn() +} +"#, + ) + } + + #[test] + fn unit_structs_have_no_type_hints() { + check_types( + r#" +//- minicore: result +struct SyntheticSyntax; + +fn main() { + match Ok(()) { + Ok(_) => (), + Err(SyntheticSyntax) => (), + } +}"#, + ); + } + + #[test] + fn let_statement() { + check_types( + r#" +#[derive(PartialEq)] +enum Option<T> { None, Some(T) } + +#[derive(PartialEq)] +struct Test { a: Option<u32>, b: u8 } + +fn main() { + struct InnerStruct {} + + let test = 54; + //^^^^ i32 + let test: i32 = 33; + let mut test = 33; + //^^^^ i32 + let _ = 22; + let test = "test"; + //^^^^ &str + let test = InnerStruct {}; + //^^^^ InnerStruct + + let test = unresolved(); + + let test = (42, 'a'); + //^^^^ (i32, char) + let (a, (b, (c,)) = (2, (3, (9.2,)); + //^ i32 ^ i32 ^ f64 + let &x = &92; + //^ i32 +}"#, + ); + } + + #[test] + fn if_expr() { + check_types( + r#" +//- minicore: option +struct Test { a: Option<u32>, b: u8 } + +fn main() { + let test = Some(Test { a: Some(3), b: 1 }); + //^^^^ Option<Test> + if let None = &test {}; + if let test = &test {}; + //^^^^ &Option<Test> + if let Some(test) = &test {}; + //^^^^ &Test + if let Some(Test { a, b }) = &test {}; + //^ &Option<u32> ^ &u8 + if let Some(Test { a: x, b: y }) = &test {}; + //^ &Option<u32> ^ &u8 + if let Some(Test { a: Some(x), b: y }) = &test {}; + //^ &u32 ^ &u8 + if let Some(Test { a: None, b: y }) = &test {}; + //^ &u8 + if let Some(Test { b: y, .. }) = &test {}; + //^ &u8 + if test == None {} +}"#, + ); + } + + #[test] + fn while_expr() { + check_types( + r#" +//- minicore: option +struct Test { a: Option<u32>, b: u8 } + +fn main() { + let test = Some(Test { a: Some(3), b: 1 }); + //^^^^ Option<Test> + while let Some(Test { a: Some(x), b: y }) = &test {}; + //^ &u32 ^ &u8 +}"#, + ); + } + + #[test] + fn match_arm_list() { + check_types( + r#" +//- minicore: option +struct Test { a: Option<u32>, b: u8 } + +fn main() { + match Some(Test { a: Some(3), b: 1 }) { + None => (), + test => (), + //^^^^ Option<Test> + Some(Test { a: Some(x), b: y }) => (), + //^ u32 ^ u8 + _ => {} + } +}"#, + ); + } + + #[test] + fn complete_for_hint() { + check_types( + r#" +//- minicore: iterator +pub struct Vec<T> {} + +impl<T> Vec<T> { + pub fn new() -> Self { Vec {} } + pub fn push(&mut self, t: T) {} +} + +impl<T> IntoIterator for Vec<T> { + type Item=T; +} + +fn main() { + let mut data = Vec::new(); + //^^^^ Vec<&str> + data.push("foo"); + for i in data { + //^ &str + let z = i; + //^ &str + } +} +"#, + ); + } + + #[test] + fn multi_dyn_trait_bounds() { + check_types( + r#" +pub struct Vec<T> {} + +impl<T> Vec<T> { + pub fn new() -> Self { Vec {} } +} + +pub struct Box<T> {} + +trait Display {} +trait Sync {} + +fn main() { + // The block expression wrapping disables the constructor hint hiding logic + let _v = { Vec::<Box<&(dyn Display + Sync)>>::new() }; + //^^ Vec<Box<&(dyn Display + Sync)>> + let _v = { Vec::<Box<*const (dyn Display + Sync)>>::new() }; + //^^ Vec<Box<*const (dyn Display + Sync)>> + let _v = { Vec::<Box<dyn Display + Sync>>::new() }; + //^^ Vec<Box<dyn Display + Sync>> +} +"#, + ); + } + + #[test] + fn shorten_iterator_hints() { + check_types( + r#" +//- minicore: iterators +use core::iter; + +struct MyIter; + +impl Iterator for MyIter { + type Item = (); + fn next(&mut self) -> Option<Self::Item> { + None + } +} + +fn main() { + let _x = MyIter; + //^^ MyIter + let _x = iter::repeat(0); + //^^ impl Iterator<Item = i32> + fn generic<T: Clone>(t: T) { + let _x = iter::repeat(t); + //^^ impl Iterator<Item = T> + let _chained = iter::repeat(t).take(10); + //^^^^^^^^ impl Iterator<Item = T> + } +} +"#, + ); + } + + #[test] + fn skip_constructor_and_enum_type_hints() { + check_with_config( + InlayHintsConfig { + type_hints: true, + hide_named_constructor_hints: true, + ..DISABLED_CONFIG + }, + r#" +//- minicore: try, option +use core::ops::ControlFlow; + +mod x { + pub mod y { pub struct Foo; } + pub struct Foo; + pub enum AnotherEnum { + Variant() + }; +} +struct Struct; +struct TupleStruct(); + +impl Struct { + fn new() -> Self { + Struct + } + fn try_new() -> ControlFlow<(), Self> { + ControlFlow::Continue(Struct) + } +} + +struct Generic<T>(T); +impl Generic<i32> { + fn new() -> Self { + Generic(0) + } +} + +enum Enum { + Variant(u32) +} + +fn times2(value: i32) -> i32 { + 2 * value +} + +fn main() { + let enumb = Enum::Variant(0); + + let strukt = x::Foo; + let strukt = x::y::Foo; + let strukt = Struct; + let strukt = Struct::new(); + + let tuple_struct = TupleStruct(); + + let generic0 = Generic::new(); + // ^^^^^^^^ Generic<i32> + let generic1 = Generic(0); + // ^^^^^^^^ Generic<i32> + let generic2 = Generic::<i32>::new(); + let generic3 = <Generic<i32>>::new(); + let generic4 = Generic::<i32>(0); + + + let option = Some(0); + // ^^^^^^ Option<i32> + let func = times2; + // ^^^^ fn times2(i32) -> i32 + let closure = |x: i32| x * 2; + // ^^^^^^^ |i32| -> i32 +} + +fn fallible() -> ControlFlow<()> { + let strukt = Struct::try_new()?; +} +"#, + ); + } + + #[test] + fn shows_constructor_type_hints_when_enabled() { + check_types( + r#" +//- minicore: try +use core::ops::ControlFlow; + +struct Struct; +struct TupleStruct(); + +impl Struct { + fn new() -> Self { + Struct + } + fn try_new() -> ControlFlow<(), Self> { + ControlFlow::Continue(Struct) + } +} + +struct Generic<T>(T); +impl Generic<i32> { + fn new() -> Self { + Generic(0) + } +} + +fn main() { + let strukt = Struct::new(); + // ^^^^^^ Struct + let tuple_struct = TupleStruct(); + // ^^^^^^^^^^^^ TupleStruct + let generic0 = Generic::new(); + // ^^^^^^^^ Generic<i32> + let generic1 = Generic::<i32>::new(); + // ^^^^^^^^ Generic<i32> + let generic2 = <Generic<i32>>::new(); + // ^^^^^^^^ Generic<i32> +} + +fn fallible() -> ControlFlow<()> { + let strukt = Struct::try_new()?; + // ^^^^^^ Struct +} +"#, + ); + } + + #[test] + fn closures() { + check( + r#" +fn main() { + let mut start = 0; + //^^^^^ i32 + (0..2).for_each(|increment | { start += increment; }); + //^^^^^^^^^ i32 + + let multiply = + //^^^^^^^^ |i32, i32| -> i32 + | a, b| a * b + //^ i32 ^ i32 + + ; + + let _: i32 = multiply(1, 2); + //^ a ^ b + let multiply_ref = &multiply; + //^^^^^^^^^^^^ &|i32, i32| -> i32 + + let return_42 = || 42; + //^^^^^^^^^ || -> i32 + || { 42 }; + //^^ i32 +}"#, + ); + } + + #[test] + fn return_type_hints_for_closure_without_block() { + check_with_config( + InlayHintsConfig { + closure_return_type_hints: ClosureReturnTypeHints::Always, + ..DISABLED_CONFIG + }, + r#" +fn main() { + let a = || { 0 }; + //^^ i32 + let b = || 0; + //^^ i32 +}"#, + ); + } + + #[test] + fn skip_closure_type_hints() { + check_with_config( + InlayHintsConfig { + type_hints: true, + hide_closure_initialization_hints: true, + ..DISABLED_CONFIG + }, + r#" +//- minicore: fn +fn main() { + let multiple_2 = |x: i32| { x * 2 }; + + let multiple_2 = |x: i32| x * 2; + // ^^^^^^^^^^ |i32| -> i32 + + let (not) = (|x: bool| { !x }); + // ^^^ |bool| -> bool + + let (is_zero, _b) = (|x: usize| { x == 0 }, false); + // ^^^^^^^ |usize| -> bool + // ^^ bool + + let plus_one = |x| { x + 1 }; + // ^ u8 + foo(plus_one); + + let add_mul = bar(|x: u8| { x + 1 }); + // ^^^^^^^ impl FnOnce(u8) -> u8 + ?Sized + + let closure = if let Some(6) = add_mul(2).checked_sub(1) { + // ^^^^^^^ fn(i32) -> i32 + |x: i32| { x * 2 } + } else { + |x: i32| { x * 3 } + }; +} + +fn foo(f: impl FnOnce(u8) -> u8) {} + +fn bar(f: impl FnOnce(u8) -> u8) -> impl FnOnce(u8) -> u8 { + move |x: u8| f(x) * 2 +} +"#, + ); + } + + #[test] + fn hint_truncation() { + check_with_config( + InlayHintsConfig { max_length: Some(8), ..TEST_CONFIG }, + r#" +struct Smol<T>(T); + +struct VeryLongOuterName<T>(T); + +fn main() { + let a = Smol(0u32); + //^ Smol<u32> + let b = VeryLongOuterName(0usize); + //^ VeryLongOuterName<…> + let c = Smol(Smol(0u32)) + //^ Smol<Smol<…>> +}"#, + ); + } + + // Chaining hint tests + + #[test] + fn chaining_hints_ignore_comments() { + check_expect( + InlayHintsConfig { type_hints: false, chaining_hints: true, ..DISABLED_CONFIG }, + r#" +struct A(B); +impl A { fn into_b(self) -> B { self.0 } } +struct B(C); +impl B { fn into_c(self) -> C { self.0 } } +struct C; + +fn main() { + let c = A(B(C)) + .into_b() // This is a comment + // This is another comment + .into_c(); +} +"#, + expect![[r#" + [ + InlayHint { + range: 147..172, + kind: ChainingHint, + label: "B", + tooltip: Some( + HoverRanged( + FileId( + 0, + ), + 147..172, + ), + ), + }, + InlayHint { + range: 147..154, + kind: ChainingHint, + label: "A", + tooltip: Some( + HoverRanged( + FileId( + 0, + ), + 147..154, + ), + ), + }, + ] + "#]], + ); + } + + #[test] + fn chaining_hints_without_newlines() { + check_chains( + r#" +struct A(B); +impl A { fn into_b(self) -> B { self.0 } } +struct B(C); +impl B { fn into_c(self) -> C { self.0 } } +struct C; + +fn main() { + let c = A(B(C)).into_b().into_c(); +}"#, + ); + } + + #[test] + fn struct_access_chaining_hints() { + check_expect( + InlayHintsConfig { chaining_hints: true, ..DISABLED_CONFIG }, + r#" +struct A { pub b: B } +struct B { pub c: C } +struct C(pub bool); +struct D; + +impl D { + fn foo(&self) -> i32 { 42 } +} + +fn main() { + let x = A { b: B { c: C(true) } } + .b + .c + .0; + let x = D + .foo(); +}"#, + expect![[r#" + [ + InlayHint { + range: 143..190, + kind: ChainingHint, + label: "C", + tooltip: Some( + HoverRanged( + FileId( + 0, + ), + 143..190, + ), + ), + }, + InlayHint { + range: 143..179, + kind: ChainingHint, + label: "B", + tooltip: Some( + HoverRanged( + FileId( + 0, + ), + 143..179, + ), + ), + }, + ] + "#]], + ); + } + + #[test] + fn generic_chaining_hints() { + check_expect( + InlayHintsConfig { chaining_hints: true, ..DISABLED_CONFIG }, + r#" +struct A<T>(T); +struct B<T>(T); +struct C<T>(T); +struct X<T,R>(T, R); + +impl<T> A<T> { + fn new(t: T) -> Self { A(t) } + fn into_b(self) -> B<T> { B(self.0) } +} +impl<T> B<T> { + fn into_c(self) -> C<T> { C(self.0) } +} +fn main() { + let c = A::new(X(42, true)) + .into_b() + .into_c(); +} +"#, + expect![[r#" + [ + InlayHint { + range: 246..283, + kind: ChainingHint, + label: "B<X<i32, bool>>", + tooltip: Some( + HoverRanged( + FileId( + 0, + ), + 246..283, + ), + ), + }, + InlayHint { + range: 246..265, + kind: ChainingHint, + label: "A<X<i32, bool>>", + tooltip: Some( + HoverRanged( + FileId( + 0, + ), + 246..265, + ), + ), + }, + ] + "#]], + ); + } + + #[test] + fn shorten_iterator_chaining_hints() { + check_expect( + InlayHintsConfig { chaining_hints: true, ..DISABLED_CONFIG }, + r#" +//- minicore: iterators +use core::iter; + +struct MyIter; + +impl Iterator for MyIter { + type Item = (); + fn next(&mut self) -> Option<Self::Item> { + None + } +} + +fn main() { + let _x = MyIter.by_ref() + .take(5) + .by_ref() + .take(5) + .by_ref(); +} +"#, + expect![[r#" + [ + InlayHint { + range: 174..241, + kind: ChainingHint, + label: "impl Iterator<Item = ()>", + tooltip: Some( + HoverRanged( + FileId( + 0, + ), + 174..241, + ), + ), + }, + InlayHint { + range: 174..224, + kind: ChainingHint, + label: "impl Iterator<Item = ()>", + tooltip: Some( + HoverRanged( + FileId( + 0, + ), + 174..224, + ), + ), + }, + InlayHint { + range: 174..206, + kind: ChainingHint, + label: "impl Iterator<Item = ()>", + tooltip: Some( + HoverRanged( + FileId( + 0, + ), + 174..206, + ), + ), + }, + InlayHint { + range: 174..189, + kind: ChainingHint, + label: "&mut MyIter", + tooltip: Some( + HoverRanged( + FileId( + 0, + ), + 174..189, + ), + ), + }, + ] + "#]], + ); + } + + #[test] + fn hints_in_attr_call() { + check_expect( + TEST_CONFIG, + r#" +//- proc_macros: identity, input_replace +struct Struct; +impl Struct { + fn chain(self) -> Self { + self + } +} +#[proc_macros::identity] +fn main() { + let strukt = Struct; + strukt + .chain() + .chain() + .chain(); + Struct::chain(strukt); +} +"#, + expect![[r#" + [ + InlayHint { + range: 124..130, + kind: TypeHint, + label: "Struct", + tooltip: Some( + HoverRanged( + FileId( + 0, + ), + 124..130, + ), + ), + }, + InlayHint { + range: 145..185, + kind: ChainingHint, + label: "Struct", + tooltip: Some( + HoverRanged( + FileId( + 0, + ), + 145..185, + ), + ), + }, + InlayHint { + range: 145..168, + kind: ChainingHint, + label: "Struct", + tooltip: Some( + HoverRanged( + FileId( + 0, + ), + 145..168, + ), + ), + }, + InlayHint { + range: 222..228, + kind: ParameterHint, + label: "self", + tooltip: Some( + HoverOffset( + FileId( + 0, + ), + 42, + ), + ), + }, + ] + "#]], + ); + } + + #[test] + fn hints_lifetimes() { + check( + r#" +fn empty() {} + +fn no_gpl(a: &()) {} + //^^^^^^<'0> + // ^'0 +fn empty_gpl<>(a: &()) {} + // ^'0 ^'0 +fn partial<'b>(a: &(), b: &'b ()) {} +// ^'0, $ ^'0 +fn partial<'a>(a: &'a (), b: &()) {} +// ^'0, $ ^'0 + +fn single_ret(a: &()) -> &() {} +// ^^^^^^^^^^<'0> + // ^'0 ^'0 +fn full_mul(a: &(), b: &()) {} +// ^^^^^^^^<'0, '1> + // ^'0 ^'1 + +fn foo<'c>(a: &'c ()) -> &() {} + // ^'c + +fn nested_in(a: & &X< &()>) {} +// ^^^^^^^^^<'0, '1, '2> + //^'0 ^'1 ^'2 +fn nested_out(a: &()) -> & &X< &()>{} +// ^^^^^^^^^^<'0> + //^'0 ^'0 ^'0 ^'0 + +impl () { + fn foo(&self) {} + // ^^^<'0> + // ^'0 + fn foo(&self) -> &() {} + // ^^^<'0> + // ^'0 ^'0 + fn foo(&self, a: &()) -> &() {} + // ^^^<'0, '1> + // ^'0 ^'1 ^'0 +} +"#, + ); + } + + #[test] + fn hints_lifetimes_named() { + check_with_config( + InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }, + r#" +fn nested_in<'named>(named: & &X< &()>) {} +// ^'named1, 'named2, 'named3, $ + //^'named1 ^'named2 ^'named3 +"#, + ); + } + + #[test] + fn hints_lifetimes_trivial_skip() { + check_with_config( + InlayHintsConfig { + lifetime_elision_hints: LifetimeElisionHints::SkipTrivial, + ..TEST_CONFIG + }, + r#" +fn no_gpl(a: &()) {} +fn empty_gpl<>(a: &()) {} +fn partial<'b>(a: &(), b: &'b ()) {} +fn partial<'a>(a: &'a (), b: &()) {} + +fn single_ret(a: &()) -> &() {} +// ^^^^^^^^^^<'0> + // ^'0 ^'0 +fn full_mul(a: &(), b: &()) {} + +fn foo<'c>(a: &'c ()) -> &() {} + // ^'c + +fn nested_in(a: & &X< &()>) {} +fn nested_out(a: &()) -> & &X< &()>{} +// ^^^^^^^^^^<'0> + //^'0 ^'0 ^'0 ^'0 + +impl () { + fn foo(&self) {} + fn foo(&self) -> &() {} + // ^^^<'0> + // ^'0 ^'0 + fn foo(&self, a: &()) -> &() {} + // ^^^<'0, '1> + // ^'0 ^'1 ^'0 +} +"#, + ); + } + + #[test] + fn hints_lifetimes_static() { + check_with_config( + InlayHintsConfig { + lifetime_elision_hints: LifetimeElisionHints::Always, + ..TEST_CONFIG + }, + r#" +trait Trait {} +static S: &str = ""; +// ^'static +const C: &str = ""; +// ^'static +const C: &dyn Trait = panic!(); +// ^'static + +impl () { + const C: &str = ""; + const C: &dyn Trait = panic!(); +} +"#, + ); + } + + #[test] + fn hints_implicit_reborrow() { + check_with_config( + InlayHintsConfig { + reborrow_hints: ReborrowHints::Always, + parameter_hints: true, + ..DISABLED_CONFIG + }, + r#" +fn __() { + let unique = &mut (); + let r_mov = unique; + let foo: &mut _ = unique; + //^^^^^^ &mut * + ref_mut_id(unique); + //^^^^^^ mut_ref + //^^^^^^ &mut * + let shared = ref_id(unique); + //^^^^^^ shared_ref + //^^^^^^ &* + let mov = shared; + let r_mov: &_ = shared; + ref_id(shared); + //^^^^^^ shared_ref + + identity(unique); + identity(shared); +} +fn identity<T>(t: T) -> T { + t +} +fn ref_mut_id(mut_ref: &mut ()) -> &mut () { + mut_ref + //^^^^^^^ &mut * +} +fn ref_id(shared_ref: &()) -> &() { + shared_ref +} +"#, + ); + } + + #[test] + fn hints_binding_modes() { + check_with_config( + InlayHintsConfig { binding_mode_hints: true, ..DISABLED_CONFIG }, + r#" +fn __( + (x,): (u32,), + (x,): &(u32,), + //^^^^& + //^ ref + (x,): &mut (u32,) + //^^^^&mut + //^ ref mut +) { + let (x,) = (0,); + let (x,) = &(0,); + //^^^^ & + //^ ref + let (x,) = &mut (0,); + //^^^^ &mut + //^ ref mut + let &mut (x,) = &mut (0,); + let (ref mut x,) = &mut (0,); + //^^^^^^^^^^^^ &mut + let &mut (ref mut x,) = &mut (0,); + let (mut x,) = &mut (0,); + //^^^^^^^^ &mut + match (0,) { + (x,) => () + } + match &(0,) { + (x,) => () + //^^^^ & + //^ ref + } + match &mut (0,) { + (x,) => () + //^^^^ &mut + //^ ref mut + } +}"#, + ); + } + + #[test] + fn hints_closing_brace() { + check_with_config( + InlayHintsConfig { closing_brace_hints_min_lines: Some(2), ..DISABLED_CONFIG }, + r#" +fn a() {} + +fn f() { +} // no hint unless `}` is the last token on the line + +fn g() { + } +//^ fn g + +fn h<T>(with: T, arguments: u8, ...) { + } +//^ fn h + +trait Tr { + fn f(); + fn g() { + } + //^ fn g + } +//^ trait Tr +impl Tr for () { + } +//^ impl Tr for () +impl dyn Tr { + } +//^ impl dyn Tr + +static S0: () = 0; +static S1: () = {}; +static S2: () = { + }; +//^ static S2 +const _: () = { + }; +//^ const _ + +mod m { + } +//^ mod m + +m! {} +m!(); +m!( + ); +//^ m! + +m! { + } +//^ m! + +fn f() { + let v = vec![ + ]; + } +//^ fn f +"#, + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/join_lines.rs b/src/tools/rust-analyzer/crates/ide/src/join_lines.rs new file mode 100644 index 000000000..08621adde --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/join_lines.rs @@ -0,0 +1,1087 @@ +use ide_assists::utils::extract_trivial_expression; +use ide_db::syntax_helpers::node_ext::expr_as_name_ref; +use itertools::Itertools; +use syntax::{ + ast::{self, AstNode, AstToken, IsString}, + NodeOrToken, SourceFile, SyntaxElement, + SyntaxKind::{self, USE_TREE, WHITESPACE}, + SyntaxToken, TextRange, TextSize, T, +}; + +use text_edit::{TextEdit, TextEditBuilder}; + +pub struct JoinLinesConfig { + pub join_else_if: bool, + pub remove_trailing_comma: bool, + pub unwrap_trivial_blocks: bool, + pub join_assignments: bool, +} + +// Feature: Join Lines +// +// Join selected lines into one, smartly fixing up whitespace, trailing commas, and braces. +// +// See +// https://user-images.githubusercontent.com/1711539/124515923-4504e800-dde9-11eb-8d58-d97945a1a785.gif[this gif] +// for the cases handled specially by joined lines. +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Join lines** +// |=== +// +// image::https://user-images.githubusercontent.com/48062697/113020661-b6922200-917a-11eb-87c4-b75acc028f11.gif[] +pub(crate) fn join_lines( + config: &JoinLinesConfig, + file: &SourceFile, + range: TextRange, +) -> TextEdit { + let range = if range.is_empty() { + let syntax = file.syntax(); + let text = syntax.text().slice(range.start()..); + let pos = match text.find_char('\n') { + None => return TextEdit::builder().finish(), + Some(pos) => pos, + }; + TextRange::at(range.start() + pos, TextSize::of('\n')) + } else { + range + }; + + let mut edit = TextEdit::builder(); + match file.syntax().covering_element(range) { + NodeOrToken::Node(node) => { + for token in node.descendants_with_tokens().filter_map(|it| it.into_token()) { + remove_newlines(config, &mut edit, &token, range) + } + } + NodeOrToken::Token(token) => remove_newlines(config, &mut edit, &token, range), + }; + edit.finish() +} + +fn remove_newlines( + config: &JoinLinesConfig, + edit: &mut TextEditBuilder, + token: &SyntaxToken, + range: TextRange, +) { + let intersection = match range.intersect(token.text_range()) { + Some(range) => range, + None => return, + }; + + let range = intersection - token.text_range().start(); + let text = token.text(); + for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') { + let pos: TextSize = (pos as u32).into(); + let offset = token.text_range().start() + range.start() + pos; + if !edit.invalidates_offset(offset) { + remove_newline(config, edit, token, offset); + } + } +} + +fn remove_newline( + config: &JoinLinesConfig, + edit: &mut TextEditBuilder, + token: &SyntaxToken, + offset: TextSize, +) { + if token.kind() != WHITESPACE || token.text().bytes().filter(|&b| b == b'\n').count() != 1 { + let n_spaces_after_line_break = { + let suff = &token.text()[TextRange::new( + offset - token.text_range().start() + TextSize::of('\n'), + TextSize::of(token.text()), + )]; + suff.bytes().take_while(|&b| b == b' ').count() + }; + + let mut no_space = false; + if let Some(string) = ast::String::cast(token.clone()) { + if let Some(range) = string.open_quote_text_range() { + cov_mark::hit!(join_string_literal_open_quote); + no_space |= range.end() == offset; + } + if let Some(range) = string.close_quote_text_range() { + cov_mark::hit!(join_string_literal_close_quote); + no_space |= range.start() + == offset + + TextSize::of('\n') + + TextSize::try_from(n_spaces_after_line_break).unwrap(); + } + } + + let range = TextRange::at(offset, ((n_spaces_after_line_break + 1) as u32).into()); + let replace_with = if no_space { "" } else { " " }; + edit.replace(range, replace_with.to_string()); + return; + } + + // The node is between two other nodes + let (prev, next) = match (token.prev_sibling_or_token(), token.next_sibling_or_token()) { + (Some(prev), Some(next)) => (prev, next), + _ => return, + }; + + if config.remove_trailing_comma && prev.kind() == T![,] { + match next.kind() { + T![')'] | T![']'] => { + // Removes: trailing comma, newline (incl. surrounding whitespace) + edit.delete(TextRange::new(prev.text_range().start(), token.text_range().end())); + return; + } + T!['}'] => { + // Removes: comma, newline (incl. surrounding whitespace) + let space = match prev.prev_sibling_or_token() { + Some(left) => compute_ws(left.kind(), next.kind()), + None => " ", + }; + edit.replace( + TextRange::new(prev.text_range().start(), token.text_range().end()), + space.to_string(), + ); + return; + } + _ => (), + } + } + + if config.join_else_if { + if let (Some(prev), Some(_next)) = (as_if_expr(&prev), as_if_expr(&next)) { + match prev.else_token() { + Some(_) => cov_mark::hit!(join_two_ifs_with_existing_else), + None => { + cov_mark::hit!(join_two_ifs); + edit.replace(token.text_range(), " else ".to_string()); + return; + } + } + } + } + + if config.join_assignments { + if join_assignments(edit, &prev, &next).is_some() { + return; + } + } + + if config.unwrap_trivial_blocks { + // Special case that turns something like: + // + // ``` + // my_function({$0 + // <some-expr> + // }) + // ``` + // + // into `my_function(<some-expr>)` + if join_single_expr_block(edit, token).is_some() { + return; + } + // ditto for + // + // ``` + // use foo::{$0 + // bar + // }; + // ``` + if join_single_use_tree(edit, token).is_some() { + return; + } + } + + if let (Some(_), Some(next)) = ( + prev.as_token().cloned().and_then(ast::Comment::cast), + next.as_token().cloned().and_then(ast::Comment::cast), + ) { + // Removes: newline (incl. surrounding whitespace), start of the next comment + edit.delete(TextRange::new( + token.text_range().start(), + next.syntax().text_range().start() + TextSize::of(next.prefix()), + )); + return; + } + + // Remove newline but add a computed amount of whitespace characters + edit.replace(token.text_range(), compute_ws(prev.kind(), next.kind()).to_string()); +} + +fn join_single_expr_block(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> { + let block_expr = ast::BlockExpr::cast(token.parent_ancestors().nth(1)?)?; + if !block_expr.is_standalone() { + return None; + } + let expr = extract_trivial_expression(&block_expr)?; + + let block_range = block_expr.syntax().text_range(); + let mut buf = expr.syntax().text().to_string(); + + // Match block needs to have a comma after the block + if let Some(match_arm) = block_expr.syntax().parent().and_then(ast::MatchArm::cast) { + if match_arm.comma_token().is_none() { + buf.push(','); + } + } + + edit.replace(block_range, buf); + + Some(()) +} + +fn join_single_use_tree(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> { + let use_tree_list = ast::UseTreeList::cast(token.parent()?)?; + let (tree,) = use_tree_list.use_trees().collect_tuple()?; + edit.replace(use_tree_list.syntax().text_range(), tree.syntax().text().to_string()); + Some(()) +} + +fn join_assignments( + edit: &mut TextEditBuilder, + prev: &SyntaxElement, + next: &SyntaxElement, +) -> Option<()> { + let let_stmt = ast::LetStmt::cast(prev.as_node()?.clone())?; + if let_stmt.eq_token().is_some() { + cov_mark::hit!(join_assignments_already_initialized); + return None; + } + let let_ident_pat = match let_stmt.pat()? { + ast::Pat::IdentPat(it) => it, + _ => return None, + }; + + let expr_stmt = ast::ExprStmt::cast(next.as_node()?.clone())?; + let bin_expr = match expr_stmt.expr()? { + ast::Expr::BinExpr(it) => it, + _ => return None, + }; + if !matches!(bin_expr.op_kind()?, ast::BinaryOp::Assignment { op: None }) { + return None; + } + let lhs = bin_expr.lhs()?; + let name_ref = expr_as_name_ref(&lhs)?; + + if name_ref.to_string() != let_ident_pat.syntax().to_string() { + cov_mark::hit!(join_assignments_mismatch); + return None; + } + + edit.delete(let_stmt.semicolon_token()?.text_range().cover(lhs.syntax().text_range())); + Some(()) +} + +fn as_if_expr(element: &SyntaxElement) -> Option<ast::IfExpr> { + let mut node = element.as_node()?.clone(); + if let Some(stmt) = ast::ExprStmt::cast(node.clone()) { + node = stmt.expr()?.syntax().clone(); + } + ast::IfExpr::cast(node) +} + +fn compute_ws(left: SyntaxKind, right: SyntaxKind) -> &'static str { + match left { + T!['('] | T!['['] => return "", + T!['{'] => { + if let USE_TREE = right { + return ""; + } + } + _ => (), + } + match right { + T![')'] | T![']'] => return "", + T!['}'] => { + if let USE_TREE = left { + return ""; + } + } + T![.] => return "", + _ => (), + } + " " +} + +#[cfg(test)] +mod tests { + use syntax::SourceFile; + use test_utils::{add_cursor, assert_eq_text, extract_offset, extract_range}; + + use super::*; + + fn check_join_lines(ra_fixture_before: &str, ra_fixture_after: &str) { + let config = JoinLinesConfig { + join_else_if: true, + remove_trailing_comma: true, + unwrap_trivial_blocks: true, + join_assignments: true, + }; + + let (before_cursor_pos, before) = extract_offset(ra_fixture_before); + let file = SourceFile::parse(&before).ok().unwrap(); + + let range = TextRange::empty(before_cursor_pos); + let result = join_lines(&config, &file, range); + + let actual = { + let mut actual = before; + result.apply(&mut actual); + actual + }; + let actual_cursor_pos = result + .apply_to_offset(before_cursor_pos) + .expect("cursor position is affected by the edit"); + let actual = add_cursor(&actual, actual_cursor_pos); + assert_eq_text!(ra_fixture_after, &actual); + } + + fn check_join_lines_sel(ra_fixture_before: &str, ra_fixture_after: &str) { + let config = JoinLinesConfig { + join_else_if: true, + remove_trailing_comma: true, + unwrap_trivial_blocks: true, + join_assignments: true, + }; + + let (sel, before) = extract_range(ra_fixture_before); + let parse = SourceFile::parse(&before); + let result = join_lines(&config, &parse.tree(), sel); + let actual = { + let mut actual = before; + result.apply(&mut actual); + actual + }; + assert_eq_text!(ra_fixture_after, &actual); + } + + #[test] + fn test_join_lines_comma() { + check_join_lines( + r" +fn foo() { + $0foo(1, + ) +} +", + r" +fn foo() { + $0foo(1) +} +", + ); + } + + #[test] + fn test_join_lines_lambda_block() { + check_join_lines( + r" +pub fn reparse(&self, edit: &AtomTextEdit) -> File { + $0self.incremental_reparse(edit).unwrap_or_else(|| { + self.full_reparse(edit) + }) +} +", + r" +pub fn reparse(&self, edit: &AtomTextEdit) -> File { + $0self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit)) +} +", + ); + } + + #[test] + fn test_join_lines_block() { + check_join_lines( + r" +fn foo() { + foo($0{ + 92 + }) +}", + r" +fn foo() { + foo($092) +}", + ); + } + + #[test] + fn test_join_lines_diverging_block() { + check_join_lines( + r" +fn foo() { + loop { + match x { + 92 => $0{ + continue; + } + } + } +} + ", + r" +fn foo() { + loop { + match x { + 92 => $0continue, + } + } +} + ", + ); + } + + #[test] + fn join_lines_adds_comma_for_block_in_match_arm() { + check_join_lines( + r" +fn foo(e: Result<U, V>) { + match e { + Ok(u) => $0{ + u.foo() + } + Err(v) => v, + } +}", + r" +fn foo(e: Result<U, V>) { + match e { + Ok(u) => $0u.foo(), + Err(v) => v, + } +}", + ); + } + + #[test] + fn join_lines_multiline_in_block() { + check_join_lines( + r" +fn foo() { + match ty { + $0 Some(ty) => { + match ty { + _ => false, + } + } + _ => true, + } +} +", + r" +fn foo() { + match ty { + $0 Some(ty) => match ty { + _ => false, + }, + _ => true, + } +} +", + ); + } + + #[test] + fn join_lines_keeps_comma_for_block_in_match_arm() { + // We already have a comma + check_join_lines( + r" +fn foo(e: Result<U, V>) { + match e { + Ok(u) => $0{ + u.foo() + }, + Err(v) => v, + } +}", + r" +fn foo(e: Result<U, V>) { + match e { + Ok(u) => $0u.foo(), + Err(v) => v, + } +}", + ); + + // comma with whitespace between brace and , + check_join_lines( + r" +fn foo(e: Result<U, V>) { + match e { + Ok(u) => $0{ + u.foo() + } , + Err(v) => v, + } +}", + r" +fn foo(e: Result<U, V>) { + match e { + Ok(u) => $0u.foo() , + Err(v) => v, + } +}", + ); + + // comma with newline between brace and , + check_join_lines( + r" +fn foo(e: Result<U, V>) { + match e { + Ok(u) => $0{ + u.foo() + } + , + Err(v) => v, + } +}", + r" +fn foo(e: Result<U, V>) { + match e { + Ok(u) => $0u.foo() + , + Err(v) => v, + } +}", + ); + } + + #[test] + fn join_lines_keeps_comma_with_single_arg_tuple() { + // A single arg tuple + check_join_lines( + r" +fn foo() { + let x = ($0{ + 4 + },); +}", + r" +fn foo() { + let x = ($04,); +}", + ); + + // single arg tuple with whitespace between brace and comma + check_join_lines( + r" +fn foo() { + let x = ($0{ + 4 + } ,); +}", + r" +fn foo() { + let x = ($04 ,); +}", + ); + + // single arg tuple with newline between brace and comma + check_join_lines( + r" +fn foo() { + let x = ($0{ + 4 + } + ,); +}", + r" +fn foo() { + let x = ($04 + ,); +}", + ); + } + + #[test] + fn test_join_lines_use_items_left() { + // No space after the '{' + check_join_lines( + r" +$0use syntax::{ + TextSize, TextRange, +};", + r" +$0use syntax::{TextSize, TextRange, +};", + ); + } + + #[test] + fn test_join_lines_use_items_right() { + // No space after the '}' + check_join_lines( + r" +use syntax::{ +$0 TextSize, TextRange +};", + r" +use syntax::{ +$0 TextSize, TextRange};", + ); + } + + #[test] + fn test_join_lines_use_items_right_comma() { + // No space after the '}' + check_join_lines( + r" +use syntax::{ +$0 TextSize, TextRange, +};", + r" +use syntax::{ +$0 TextSize, TextRange};", + ); + } + + #[test] + fn test_join_lines_use_tree() { + check_join_lines( + r" +use syntax::{ + algo::$0{ + find_token_at_offset, + }, + ast, +};", + r" +use syntax::{ + algo::$0find_token_at_offset, + ast, +};", + ); + } + + #[test] + fn test_join_lines_normal_comments() { + check_join_lines( + r" +fn foo() { + // Hello$0 + // world! +} +", + r" +fn foo() { + // Hello$0 world! +} +", + ); + } + + #[test] + fn test_join_lines_doc_comments() { + check_join_lines( + r" +fn foo() { + /// Hello$0 + /// world! +} +", + r" +fn foo() { + /// Hello$0 world! +} +", + ); + } + + #[test] + fn test_join_lines_mod_comments() { + check_join_lines( + r" +fn foo() { + //! Hello$0 + //! world! +} +", + r" +fn foo() { + //! Hello$0 world! +} +", + ); + } + + #[test] + fn test_join_lines_multiline_comments_1() { + check_join_lines( + r" +fn foo() { + // Hello$0 + /* world! */ +} +", + r" +fn foo() { + // Hello$0 world! */ +} +", + ); + } + + #[test] + fn test_join_lines_multiline_comments_2() { + check_join_lines( + r" +fn foo() { + // The$0 + /* quick + brown + fox! */ +} +", + r" +fn foo() { + // The$0 quick + brown + fox! */ +} +", + ); + } + + #[test] + fn test_join_lines_selection_fn_args() { + check_join_lines_sel( + r" +fn foo() { + $0foo(1, + 2, + 3, + $0) +} + ", + r" +fn foo() { + foo(1, 2, 3) +} + ", + ); + } + + #[test] + fn test_join_lines_selection_struct() { + check_join_lines_sel( + r" +struct Foo $0{ + f: u32, +}$0 + ", + r" +struct Foo { f: u32 } + ", + ); + } + + #[test] + fn test_join_lines_selection_dot_chain() { + check_join_lines_sel( + r" +fn foo() { + join($0type_params.type_params() + .filter_map(|it| it.name()) + .map(|it| it.text())$0) +}", + r" +fn foo() { + join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text())) +}", + ); + } + + #[test] + fn test_join_lines_selection_lambda_block_body() { + check_join_lines_sel( + r" +pub fn handle_find_matching_brace() { + params.offsets + .map(|offset| $0{ + world.analysis().matching_brace(&file, offset).unwrap_or(offset) + }$0) + .collect(); +}", + r" +pub fn handle_find_matching_brace() { + params.offsets + .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset)) + .collect(); +}", + ); + } + + #[test] + fn test_join_lines_commented_block() { + check_join_lines( + r" +fn main() { + let _ = { + // $0foo + // bar + 92 + }; +} + ", + r" +fn main() { + let _ = { + // $0foo bar + 92 + }; +} + ", + ) + } + + #[test] + fn join_lines_mandatory_blocks_block() { + check_join_lines( + r" +$0fn foo() { + 92 +} + ", + r" +$0fn foo() { 92 +} + ", + ); + + check_join_lines( + r" +fn foo() { + $0if true { + 92 + } +} + ", + r" +fn foo() { + $0if true { 92 + } +} + ", + ); + + check_join_lines( + r" +fn foo() { + $0loop { + 92 + } +} + ", + r" +fn foo() { + $0loop { 92 + } +} + ", + ); + + check_join_lines( + r" +fn foo() { + $0unsafe { + 92 + } +} + ", + r" +fn foo() { + $0unsafe { 92 + } +} + ", + ); + } + + #[test] + fn join_string_literal() { + { + cov_mark::check!(join_string_literal_open_quote); + check_join_lines( + r#" +fn main() { + $0" +hello +"; +} +"#, + r#" +fn main() { + $0"hello +"; +} +"#, + ); + } + + { + cov_mark::check!(join_string_literal_close_quote); + check_join_lines( + r#" +fn main() { + $0"hello +"; +} +"#, + r#" +fn main() { + $0"hello"; +} +"#, + ); + check_join_lines( + r#" +fn main() { + $0r"hello + "; +} +"#, + r#" +fn main() { + $0r"hello"; +} +"#, + ); + } + + check_join_lines( + r#" +fn main() { + " +$0hello +world +"; +} +"#, + r#" +fn main() { + " +$0hello world +"; +} +"#, + ); + } + + #[test] + fn join_last_line_empty() { + check_join_lines( + r#" +fn main() {$0} +"#, + r#" +fn main() {$0} +"#, + ); + } + + #[test] + fn join_two_ifs() { + cov_mark::check!(join_two_ifs); + check_join_lines( + r#" +fn main() { + if foo { + + }$0 + if bar { + + } +} +"#, + r#" +fn main() { + if foo { + + }$0 else if bar { + + } +} +"#, + ); + } + + #[test] + fn join_two_ifs_with_existing_else() { + cov_mark::check!(join_two_ifs_with_existing_else); + check_join_lines( + r#" +fn main() { + if foo { + + } else { + + }$0 + if bar { + + } +} +"#, + r#" +fn main() { + if foo { + + } else { + + }$0 if bar { + + } +} +"#, + ); + } + + #[test] + fn join_assignments() { + check_join_lines( + r#" +fn foo() { + $0let foo; + foo = "bar"; +} +"#, + r#" +fn foo() { + $0let foo = "bar"; +} +"#, + ); + + cov_mark::check!(join_assignments_mismatch); + check_join_lines( + r#" +fn foo() { + let foo; + let qux;$0 + foo = "bar"; +} +"#, + r#" +fn foo() { + let foo; + let qux;$0 foo = "bar"; +} +"#, + ); + + cov_mark::check!(join_assignments_already_initialized); + check_join_lines( + r#" +fn foo() { + let foo = "bar";$0 + foo = "bar"; +} +"#, + r#" +fn foo() { + let foo = "bar";$0 foo = "bar"; +} +"#, + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/lib.rs b/src/tools/rust-analyzer/crates/ide/src/lib.rs new file mode 100644 index 000000000..dd108fa79 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/lib.rs @@ -0,0 +1,702 @@ +//! ide crate provides "ide-centric" APIs for the rust-analyzer. That is, +//! it generally operates with files and text ranges, and returns results as +//! Strings, suitable for displaying to the human. +//! +//! What powers this API are the `RootDatabase` struct, which defines a `salsa` +//! database, and the `hir` crate, where majority of the analysis happens. +//! However, IDE specific bits of the analysis (most notably completion) happen +//! in this crate. + +// For proving that RootDatabase is RefUnwindSafe. +#![recursion_limit = "128"] +#![warn(rust_2018_idioms, unused_lifetimes, semicolon_in_expressions_from_macros)] + +#[allow(unused)] +macro_rules! eprintln { + ($($tt:tt)*) => { stdx::eprintln!($($tt)*) }; +} + +#[cfg(test)] +mod fixture; + +mod markup; +mod prime_caches; +mod navigation_target; + +mod annotations; +mod call_hierarchy; +mod signature_help; +mod doc_links; +mod highlight_related; +mod expand_macro; +mod extend_selection; +mod file_structure; +mod fn_references; +mod folding_ranges; +mod goto_declaration; +mod goto_definition; +mod goto_implementation; +mod goto_type_definition; +mod hover; +mod inlay_hints; +mod join_lines; +mod markdown_remove; +mod matching_brace; +mod moniker; +mod move_item; +mod parent_module; +mod references; +mod rename; +mod runnables; +mod ssr; +mod static_index; +mod status; +mod syntax_highlighting; +mod syntax_tree; +mod typing; +mod view_crate_graph; +mod view_hir; +mod view_item_tree; +mod shuffle_crate_graph; + +use std::sync::Arc; + +use cfg::CfgOptions; +use ide_db::{ + base_db::{ + salsa::{self, ParallelDatabase}, + CrateOrigin, Env, FileLoader, FileSet, SourceDatabase, VfsPath, + }, + symbol_index, LineIndexDatabase, +}; +use syntax::SourceFile; + +use crate::navigation_target::{ToNav, TryToNav}; + +pub use crate::{ + annotations::{Annotation, AnnotationConfig, AnnotationKind}, + call_hierarchy::CallItem, + expand_macro::ExpandedMacro, + file_structure::{StructureNode, StructureNodeKind}, + folding_ranges::{Fold, FoldKind}, + highlight_related::{HighlightRelatedConfig, HighlightedRange}, + hover::{HoverAction, HoverConfig, HoverDocFormat, HoverGotoTypeData, HoverResult}, + inlay_hints::{ + ClosureReturnTypeHints, InlayHint, InlayHintsConfig, InlayKind, InlayTooltip, + LifetimeElisionHints, ReborrowHints, + }, + join_lines::JoinLinesConfig, + markup::Markup, + moniker::{MonikerKind, MonikerResult, PackageInformation}, + move_item::Direction, + navigation_target::NavigationTarget, + prime_caches::ParallelPrimeCachesProgress, + references::ReferenceSearchResult, + rename::RenameError, + runnables::{Runnable, RunnableKind, TestId}, + signature_help::SignatureHelp, + static_index::{StaticIndex, StaticIndexedFile, TokenId, TokenStaticData}, + syntax_highlighting::{ + tags::{Highlight, HlMod, HlMods, HlOperator, HlPunct, HlTag}, + HlRange, + }, +}; +pub use hir::{Documentation, Semantics}; +pub use ide_assists::{ + Assist, AssistConfig, AssistId, AssistKind, AssistResolveStrategy, SingleResolve, +}; +pub use ide_completion::{ + CallableSnippets, CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, + Snippet, SnippetScope, +}; +pub use ide_db::{ + base_db::{ + Cancelled, Change, CrateGraph, CrateId, Edition, FileId, FilePosition, FileRange, + SourceRoot, SourceRootId, + }, + label::Label, + line_index::{LineCol, LineColUtf16, LineIndex}, + search::{ReferenceCategory, SearchScope}, + source_change::{FileSystemEdit, SourceChange}, + symbol_index::Query, + RootDatabase, SymbolKind, +}; +pub use ide_diagnostics::{Diagnostic, DiagnosticsConfig, ExprFillDefaultMode, Severity}; +pub use ide_ssr::SsrError; +pub use syntax::{TextRange, TextSize}; +pub use text_edit::{Indel, TextEdit}; + +pub type Cancellable<T> = Result<T, Cancelled>; + +/// Info associated with a text range. +#[derive(Debug)] +pub struct RangeInfo<T> { + pub range: TextRange, + pub info: T, +} + +impl<T> RangeInfo<T> { + pub fn new(range: TextRange, info: T) -> RangeInfo<T> { + RangeInfo { range, info } + } +} + +/// `AnalysisHost` stores the current state of the world. +#[derive(Debug)] +pub struct AnalysisHost { + db: RootDatabase, +} + +impl AnalysisHost { + pub fn new(lru_capacity: Option<usize>) -> AnalysisHost { + AnalysisHost { db: RootDatabase::new(lru_capacity) } + } + + pub fn update_lru_capacity(&mut self, lru_capacity: Option<usize>) { + self.db.update_lru_capacity(lru_capacity); + } + + /// Returns a snapshot of the current state, which you can query for + /// semantic information. + pub fn analysis(&self) -> Analysis { + Analysis { db: self.db.snapshot() } + } + + /// Applies changes to the current state of the world. If there are + /// outstanding snapshots, they will be canceled. + pub fn apply_change(&mut self, change: Change) { + self.db.apply_change(change) + } + + /// NB: this clears the database + pub fn per_query_memory_usage(&mut self) -> Vec<(String, profile::Bytes)> { + self.db.per_query_memory_usage() + } + pub fn request_cancellation(&mut self) { + self.db.request_cancellation(); + } + pub fn raw_database(&self) -> &RootDatabase { + &self.db + } + pub fn raw_database_mut(&mut self) -> &mut RootDatabase { + &mut self.db + } + + pub fn shuffle_crate_graph(&mut self) { + shuffle_crate_graph::shuffle_crate_graph(&mut self.db); + } +} + +impl Default for AnalysisHost { + fn default() -> AnalysisHost { + AnalysisHost::new(None) + } +} + +/// Analysis is a snapshot of a world state at a moment in time. It is the main +/// entry point for asking semantic information about the world. When the world +/// state is advanced using `AnalysisHost::apply_change` method, all existing +/// `Analysis` are canceled (most method return `Err(Canceled)`). +#[derive(Debug)] +pub struct Analysis { + db: salsa::Snapshot<RootDatabase>, +} + +// As a general design guideline, `Analysis` API are intended to be independent +// from the language server protocol. That is, when exposing some functionality +// we should think in terms of "what API makes most sense" and not in terms of +// "what types LSP uses". Although currently LSP is the only consumer of the +// API, the API should in theory be usable as a library, or via a different +// protocol. +impl Analysis { + // Creates an analysis instance for a single file, without any external + // dependencies, stdlib support or ability to apply changes. See + // `AnalysisHost` for creating a fully-featured analysis. + pub fn from_single_file(text: String) -> (Analysis, FileId) { + let mut host = AnalysisHost::default(); + let file_id = FileId(0); + let mut file_set = FileSet::default(); + file_set.insert(file_id, VfsPath::new_virtual_path("/main.rs".to_string())); + let source_root = SourceRoot::new_local(file_set); + + let mut change = Change::new(); + change.set_roots(vec![source_root]); + let mut crate_graph = CrateGraph::default(); + // FIXME: cfg options + // Default to enable test for single file. + let mut cfg_options = CfgOptions::default(); + cfg_options.insert_atom("test".into()); + crate_graph.add_crate_root( + file_id, + Edition::CURRENT, + None, + None, + cfg_options.clone(), + cfg_options, + Env::default(), + Ok(Vec::new()), + false, + CrateOrigin::CratesIo { repo: None }, + ); + change.change_file(file_id, Some(Arc::new(text))); + change.set_crate_graph(crate_graph); + host.apply_change(change); + (host.analysis(), file_id) + } + + /// Debug info about the current state of the analysis. + pub fn status(&self, file_id: Option<FileId>) -> Cancellable<String> { + self.with_db(|db| status::status(&*db, file_id)) + } + + pub fn parallel_prime_caches<F>(&self, num_worker_threads: u8, cb: F) -> Cancellable<()> + where + F: Fn(ParallelPrimeCachesProgress) + Sync + std::panic::UnwindSafe, + { + self.with_db(move |db| prime_caches::parallel_prime_caches(db, num_worker_threads, &cb)) + } + + /// Gets the text of the source file. + pub fn file_text(&self, file_id: FileId) -> Cancellable<Arc<String>> { + self.with_db(|db| db.file_text(file_id)) + } + + /// Gets the syntax tree of the file. + pub fn parse(&self, file_id: FileId) -> Cancellable<SourceFile> { + self.with_db(|db| db.parse(file_id).tree()) + } + + /// Returns true if this file belongs to an immutable library. + pub fn is_library_file(&self, file_id: FileId) -> Cancellable<bool> { + use ide_db::base_db::SourceDatabaseExt; + self.with_db(|db| db.source_root(db.file_source_root(file_id)).is_library) + } + + /// Gets the file's `LineIndex`: data structure to convert between absolute + /// offsets and line/column representation. + pub fn file_line_index(&self, file_id: FileId) -> Cancellable<Arc<LineIndex>> { + self.with_db(|db| db.line_index(file_id)) + } + + /// Selects the next syntactic nodes encompassing the range. + pub fn extend_selection(&self, frange: FileRange) -> Cancellable<TextRange> { + self.with_db(|db| extend_selection::extend_selection(db, frange)) + } + + /// Returns position of the matching brace (all types of braces are + /// supported). + pub fn matching_brace(&self, position: FilePosition) -> Cancellable<Option<TextSize>> { + self.with_db(|db| { + let parse = db.parse(position.file_id); + let file = parse.tree(); + matching_brace::matching_brace(&file, position.offset) + }) + } + + /// Returns a syntax tree represented as `String`, for debug purposes. + // FIXME: use a better name here. + pub fn syntax_tree( + &self, + file_id: FileId, + text_range: Option<TextRange>, + ) -> Cancellable<String> { + self.with_db(|db| syntax_tree::syntax_tree(db, file_id, text_range)) + } + + pub fn view_hir(&self, position: FilePosition) -> Cancellable<String> { + self.with_db(|db| view_hir::view_hir(db, position)) + } + + pub fn view_item_tree(&self, file_id: FileId) -> Cancellable<String> { + self.with_db(|db| view_item_tree::view_item_tree(db, file_id)) + } + + /// Renders the crate graph to GraphViz "dot" syntax. + pub fn view_crate_graph(&self, full: bool) -> Cancellable<Result<String, String>> { + self.with_db(|db| view_crate_graph::view_crate_graph(db, full)) + } + + pub fn expand_macro(&self, position: FilePosition) -> Cancellable<Option<ExpandedMacro>> { + self.with_db(|db| expand_macro::expand_macro(db, position)) + } + + /// Returns an edit to remove all newlines in the range, cleaning up minor + /// stuff like trailing commas. + pub fn join_lines(&self, config: &JoinLinesConfig, frange: FileRange) -> Cancellable<TextEdit> { + self.with_db(|db| { + let parse = db.parse(frange.file_id); + join_lines::join_lines(config, &parse.tree(), frange.range) + }) + } + + /// Returns an edit which should be applied when opening a new line, fixing + /// up minor stuff like continuing the comment. + /// The edit will be a snippet (with `$0`). + pub fn on_enter(&self, position: FilePosition) -> Cancellable<Option<TextEdit>> { + self.with_db(|db| typing::on_enter(db, position)) + } + + /// Returns an edit which should be applied after a character was typed. + /// + /// This is useful for some on-the-fly fixups, like adding `;` to `let =` + /// automatically. + pub fn on_char_typed( + &self, + position: FilePosition, + char_typed: char, + autoclose: bool, + ) -> Cancellable<Option<SourceChange>> { + // Fast path to not even parse the file. + if !typing::TRIGGER_CHARS.contains(char_typed) { + return Ok(None); + } + if char_typed == '<' && !autoclose { + return Ok(None); + } + + self.with_db(|db| typing::on_char_typed(db, position, char_typed)) + } + + /// Returns a tree representation of symbols in the file. Useful to draw a + /// file outline. + pub fn file_structure(&self, file_id: FileId) -> Cancellable<Vec<StructureNode>> { + self.with_db(|db| file_structure::file_structure(&db.parse(file_id).tree())) + } + + /// Returns a list of the places in the file where type hints can be displayed. + pub fn inlay_hints( + &self, + config: &InlayHintsConfig, + file_id: FileId, + range: Option<FileRange>, + ) -> Cancellable<Vec<InlayHint>> { + self.with_db(|db| inlay_hints::inlay_hints(db, file_id, range, config)) + } + + /// Returns the set of folding ranges. + pub fn folding_ranges(&self, file_id: FileId) -> Cancellable<Vec<Fold>> { + self.with_db(|db| folding_ranges::folding_ranges(&db.parse(file_id).tree())) + } + + /// Fuzzy searches for a symbol. + pub fn symbol_search(&self, query: Query) -> Cancellable<Vec<NavigationTarget>> { + self.with_db(|db| { + symbol_index::world_symbols(db, query) + .into_iter() // xx: should we make this a par iter? + .filter_map(|s| s.try_to_nav(db)) + .collect::<Vec<_>>() + }) + } + + /// Returns the definitions from the symbol at `position`. + pub fn goto_definition( + &self, + position: FilePosition, + ) -> Cancellable<Option<RangeInfo<Vec<NavigationTarget>>>> { + self.with_db(|db| goto_definition::goto_definition(db, position)) + } + + /// Returns the declaration from the symbol at `position`. + pub fn goto_declaration( + &self, + position: FilePosition, + ) -> Cancellable<Option<RangeInfo<Vec<NavigationTarget>>>> { + self.with_db(|db| goto_declaration::goto_declaration(db, position)) + } + + /// Returns the impls from the symbol at `position`. + pub fn goto_implementation( + &self, + position: FilePosition, + ) -> Cancellable<Option<RangeInfo<Vec<NavigationTarget>>>> { + self.with_db(|db| goto_implementation::goto_implementation(db, position)) + } + + /// Returns the type definitions for the symbol at `position`. + pub fn goto_type_definition( + &self, + position: FilePosition, + ) -> Cancellable<Option<RangeInfo<Vec<NavigationTarget>>>> { + 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, + search_scope: Option<SearchScope>, + ) -> Cancellable<Option<Vec<ReferenceSearchResult>>> { + self.with_db(|db| references::find_all_refs(&Semantics::new(db), position, search_scope)) + } + + /// Finds all methods and free functions for the file. Does not return tests! + pub fn find_all_methods(&self, file_id: FileId) -> Cancellable<Vec<FileRange>> { + self.with_db(|db| fn_references::find_all_methods(db, file_id)) + } + + /// Returns a short text describing element at position. + pub fn hover( + &self, + config: &HoverConfig, + range: FileRange, + ) -> Cancellable<Option<RangeInfo<HoverResult>>> { + self.with_db(|db| hover::hover(db, range, config)) + } + + /// Returns moniker of symbol at position. + pub fn moniker( + &self, + position: FilePosition, + ) -> Cancellable<Option<RangeInfo<Vec<moniker::MonikerResult>>>> { + self.with_db(|db| moniker::moniker(db, position)) + } + + /// Return URL(s) for the documentation of the symbol under the cursor. + pub fn external_docs( + &self, + position: FilePosition, + ) -> Cancellable<Option<doc_links::DocumentationLink>> { + self.with_db(|db| doc_links::external_docs(db, &position)) + } + + /// Computes parameter information at the given position. + pub fn signature_help(&self, position: FilePosition) -> Cancellable<Option<SignatureHelp>> { + self.with_db(|db| signature_help::signature_help(db, position)) + } + + /// Computes call hierarchy candidates for the given file position. + pub fn call_hierarchy( + &self, + position: FilePosition, + ) -> Cancellable<Option<RangeInfo<Vec<NavigationTarget>>>> { + self.with_db(|db| call_hierarchy::call_hierarchy(db, position)) + } + + /// Computes incoming calls for the given file position. + pub fn incoming_calls(&self, position: FilePosition) -> Cancellable<Option<Vec<CallItem>>> { + self.with_db(|db| call_hierarchy::incoming_calls(db, position)) + } + + /// Computes outgoing calls for the given file position. + pub fn outgoing_calls(&self, position: FilePosition) -> Cancellable<Option<Vec<CallItem>>> { + self.with_db(|db| call_hierarchy::outgoing_calls(db, position)) + } + + /// Returns a `mod name;` declaration which created the current module. + pub fn parent_module(&self, position: FilePosition) -> Cancellable<Vec<NavigationTarget>> { + self.with_db(|db| parent_module::parent_module(db, position)) + } + + /// Returns crates this file belongs too. + pub fn crate_for(&self, file_id: FileId) -> Cancellable<Vec<CrateId>> { + self.with_db(|db| parent_module::crate_for(db, file_id)) + } + + /// Returns the edition of the given crate. + pub fn crate_edition(&self, crate_id: CrateId) -> Cancellable<Edition> { + self.with_db(|db| db.crate_graph()[crate_id].edition) + } + + /// Returns the root file of the given crate. + pub fn crate_root(&self, crate_id: CrateId) -> Cancellable<FileId> { + self.with_db(|db| db.crate_graph()[crate_id].root_file_id) + } + + /// Returns the set of possible targets to run for the current file. + pub fn runnables(&self, file_id: FileId) -> Cancellable<Vec<Runnable>> { + self.with_db(|db| runnables::runnables(db, file_id)) + } + + /// Returns the set of tests for the given file position. + pub fn related_tests( + &self, + position: FilePosition, + search_scope: Option<SearchScope>, + ) -> Cancellable<Vec<Runnable>> { + self.with_db(|db| runnables::related_tests(db, position, search_scope)) + } + + /// Computes syntax highlighting for the given file + pub fn highlight(&self, file_id: FileId) -> Cancellable<Vec<HlRange>> { + self.with_db(|db| syntax_highlighting::highlight(db, file_id, None, false)) + } + + /// Computes all ranges to highlight for a given item in a file. + pub fn highlight_related( + &self, + config: HighlightRelatedConfig, + position: FilePosition, + ) -> Cancellable<Option<Vec<HighlightedRange>>> { + self.with_db(|db| { + highlight_related::highlight_related(&Semantics::new(db), config, position) + }) + } + + /// Computes syntax highlighting for the given file range. + pub fn highlight_range(&self, frange: FileRange) -> Cancellable<Vec<HlRange>> { + self.with_db(|db| { + syntax_highlighting::highlight(db, frange.file_id, Some(frange.range), false) + }) + } + + /// Computes syntax highlighting for the given file. + pub fn highlight_as_html(&self, file_id: FileId, rainbow: bool) -> Cancellable<String> { + self.with_db(|db| syntax_highlighting::highlight_as_html(db, file_id, rainbow)) + } + + /// Computes completions at the given position. + pub fn completions( + &self, + config: &CompletionConfig, + position: FilePosition, + trigger_character: Option<char>, + ) -> Cancellable<Option<Vec<CompletionItem>>> { + self.with_db(|db| { + ide_completion::completions(db, config, position, trigger_character).map(Into::into) + }) + } + + /// Resolves additional completion data at the position given. + pub fn resolve_completion_edits( + &self, + config: &CompletionConfig, + position: FilePosition, + imports: impl IntoIterator<Item = (String, String)> + std::panic::UnwindSafe, + ) -> Cancellable<Vec<TextEdit>> { + Ok(self + .with_db(|db| ide_completion::resolve_completion_edits(db, config, position, imports))? + .unwrap_or_default()) + } + + /// Computes the set of diagnostics for the given file. + pub fn diagnostics( + &self, + config: &DiagnosticsConfig, + resolve: AssistResolveStrategy, + file_id: FileId, + ) -> Cancellable<Vec<Diagnostic>> { + self.with_db(|db| ide_diagnostics::diagnostics(db, config, &resolve, file_id)) + } + + /// Convenience function to return assists + quick fixes for diagnostics + pub fn assists_with_fixes( + &self, + assist_config: &AssistConfig, + diagnostics_config: &DiagnosticsConfig, + resolve: AssistResolveStrategy, + frange: FileRange, + ) -> Cancellable<Vec<Assist>> { + let include_fixes = match &assist_config.allowed { + Some(it) => it.iter().any(|&it| it == AssistKind::None || it == AssistKind::QuickFix), + None => true, + }; + + self.with_db(|db| { + let diagnostic_assists = if include_fixes { + ide_diagnostics::diagnostics(db, diagnostics_config, &resolve, frange.file_id) + .into_iter() + .flat_map(|it| it.fixes.unwrap_or_default()) + .filter(|it| it.target.intersect(frange.range).is_some()) + .collect() + } else { + Vec::new() + }; + let ssr_assists = ssr::ssr_assists(db, &resolve, frange); + let assists = ide_assists::assists(db, assist_config, resolve, frange); + + let mut res = diagnostic_assists; + res.extend(ssr_assists.into_iter()); + res.extend(assists.into_iter()); + + res + }) + } + + /// Returns the edit required to rename reference at the position to the new + /// name. + pub fn rename( + &self, + position: FilePosition, + new_name: &str, + ) -> Cancellable<Result<SourceChange, RenameError>> { + self.with_db(|db| rename::rename(db, position, new_name)) + } + + pub fn prepare_rename( + &self, + position: FilePosition, + ) -> Cancellable<Result<RangeInfo<()>, RenameError>> { + self.with_db(|db| rename::prepare_rename(db, position)) + } + + pub fn will_rename_file( + &self, + file_id: FileId, + new_name_stem: &str, + ) -> Cancellable<Option<SourceChange>> { + self.with_db(|db| rename::will_rename_file(db, file_id, new_name_stem)) + } + + pub fn structural_search_replace( + &self, + query: &str, + parse_only: bool, + resolve_context: FilePosition, + selections: Vec<FileRange>, + ) -> Cancellable<Result<SourceChange, SsrError>> { + self.with_db(|db| { + let rule: ide_ssr::SsrRule = query.parse()?; + let mut match_finder = + ide_ssr::MatchFinder::in_context(db, resolve_context, selections)?; + match_finder.add_rule(rule)?; + let edits = if parse_only { Default::default() } else { match_finder.edits() }; + Ok(SourceChange::from(edits)) + }) + } + + pub fn annotations( + &self, + config: &AnnotationConfig, + file_id: FileId, + ) -> Cancellable<Vec<Annotation>> { + self.with_db(|db| annotations::annotations(db, config, file_id)) + } + + pub fn resolve_annotation(&self, annotation: Annotation) -> Cancellable<Annotation> { + self.with_db(|db| annotations::resolve_annotation(db, annotation)) + } + + pub fn move_item( + &self, + range: FileRange, + direction: Direction, + ) -> Cancellable<Option<TextEdit>> { + self.with_db(|db| move_item::move_item(db, range, direction)) + } + + /// Performs an operation on the database that may be canceled. + /// + /// rust-analyzer needs to be able to answer semantic questions about the + /// code while the code is being modified. A common problem is that a + /// long-running query is being calculated when a new change arrives. + /// + /// We can't just apply the change immediately: this will cause the pending + /// query to see inconsistent state (it will observe an absence of + /// repeatable read). So what we do is we **cancel** all pending queries + /// before applying the change. + /// + /// Salsa implements cancellation by unwinding with a special value and + /// catching it on the API boundary. + fn with_db<F, T>(&self, f: F) -> Cancellable<T> + where + F: FnOnce(&RootDatabase) -> T + std::panic::UnwindSafe, + { + Cancelled::catch(|| f(&self.db)) + } +} + +#[test] +fn analysis_is_send() { + fn is_send<T: Send>() {} + is_send::<Analysis>(); +} diff --git a/src/tools/rust-analyzer/crates/ide/src/markdown_remove.rs b/src/tools/rust-analyzer/crates/ide/src/markdown_remove.rs new file mode 100644 index 000000000..3ec5c629e --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/markdown_remove.rs @@ -0,0 +1,22 @@ +//! Removes markdown from strings. +use pulldown_cmark::{Event, Parser, Tag}; + +/// Removes all markdown, keeping the text and code blocks +/// +/// Currently limited in styling, i.e. no ascii tables or lists +pub(crate) fn remove_markdown(markdown: &str) -> String { + let mut out = String::new(); + let parser = Parser::new(markdown); + + for event in parser { + match event { + Event::Text(text) | Event::Code(text) => out.push_str(&text), + Event::SoftBreak | Event::HardBreak | Event::Rule | Event::End(Tag::CodeBlock(_)) => { + out.push('\n') + } + _ => {} + } + } + + out +} diff --git a/src/tools/rust-analyzer/crates/ide/src/markup.rs b/src/tools/rust-analyzer/crates/ide/src/markup.rs new file mode 100644 index 000000000..60c193c40 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/markup.rs @@ -0,0 +1,38 @@ +//! Markdown formatting. +//! +//! Sometimes, we want to display a "rich text" in the UI. At the moment, we use +//! markdown for this purpose. It doesn't feel like a right option, but that's +//! what is used by LSP, so let's keep it simple. +use std::fmt; + +#[derive(Default, Debug)] +pub struct Markup { + text: String, +} + +impl From<Markup> for String { + fn from(markup: Markup) -> Self { + markup.text + } +} + +impl From<String> for Markup { + fn from(text: String) -> Self { + Markup { text } + } +} + +impl fmt::Display for Markup { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.text, f) + } +} + +impl Markup { + pub fn as_str(&self) -> &str { + self.text.as_str() + } + pub fn fenced_block(contents: &impl fmt::Display) -> Markup { + format!("```rust\n{}\n```", contents).into() + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/matching_brace.rs b/src/tools/rust-analyzer/crates/ide/src/matching_brace.rs new file mode 100644 index 000000000..da70cecdd --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/matching_brace.rs @@ -0,0 +1,78 @@ +use syntax::{ + ast::{self, AstNode}, + SourceFile, SyntaxKind, TextSize, T, +}; + +// Feature: Matching Brace +// +// If the cursor is on any brace (`<>(){}[]||`) which is a part of a brace-pair, +// moves cursor to the matching brace. It uses the actual parser to determine +// braces, so it won't confuse generics with comparisons. +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Find matching brace** +// |=== +// +// image::https://user-images.githubusercontent.com/48062697/113065573-04298180-91b1-11eb-8dec-d4e2a202f304.gif[] +pub(crate) fn matching_brace(file: &SourceFile, offset: TextSize) -> Option<TextSize> { + const BRACES: &[SyntaxKind] = + &[T!['{'], T!['}'], T!['['], T![']'], T!['('], T![')'], T![<], T![>], T![|], T![|]]; + let (brace_token, brace_idx) = file + .syntax() + .token_at_offset(offset) + .filter_map(|node| { + let idx = BRACES.iter().position(|&brace| brace == node.kind())?; + Some((node, idx)) + }) + .last()?; + let parent = brace_token.parent()?; + if brace_token.kind() == T![|] && !ast::ParamList::can_cast(parent.kind()) { + cov_mark::hit!(pipes_not_braces); + return None; + } + let matching_kind = BRACES[brace_idx ^ 1]; + let matching_node = parent + .children_with_tokens() + .filter_map(|it| it.into_token()) + .find(|node| node.kind() == matching_kind && node != &brace_token)?; + Some(matching_node.text_range().start()) +} + +#[cfg(test)] +mod tests { + use test_utils::{add_cursor, assert_eq_text, extract_offset}; + + use super::*; + + #[test] + fn test_matching_brace() { + fn do_check(before: &str, after: &str) { + let (pos, before) = extract_offset(before); + let parse = SourceFile::parse(&before); + let new_pos = match matching_brace(&parse.tree(), pos) { + None => pos, + Some(pos) => pos, + }; + let actual = add_cursor(&before, new_pos); + assert_eq_text!(after, &actual); + } + + do_check("struct Foo { a: i32, }$0", "struct Foo $0{ a: i32, }"); + do_check("fn main() { |x: i32|$0 x * 2;}", "fn main() { $0|x: i32| x * 2;}"); + do_check("fn main() { $0|x: i32| x * 2;}", "fn main() { |x: i32$0| x * 2;}"); + do_check( + "fn func(x) { return (2 * (x + 3)$0) + 5;}", + "fn func(x) { return $0(2 * (x + 3)) + 5;}", + ); + + { + cov_mark::check!(pipes_not_braces); + do_check( + "fn main() { match 92 { 1 | 2 |$0 3 => 92 } }", + "fn main() { match 92 { 1 | 2 |$0 3 => 92 } }", + ); + } + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/moniker.rs b/src/tools/rust-analyzer/crates/ide/src/moniker.rs new file mode 100644 index 000000000..6bab9fa1e --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/moniker.rs @@ -0,0 +1,342 @@ +//! This module generates [moniker](https://microsoft.github.io/language-server-protocol/specifications/lsif/0.6.0/specification/#exportsImports) +//! for LSIF and LSP. + +use hir::{db::DefDatabase, AsAssocItem, AssocItemContainer, Crate, Name, Semantics}; +use ide_db::{ + base_db::{CrateOrigin, FileId, FileLoader, FilePosition, LangCrateOrigin}, + defs::{Definition, IdentClass}, + helpers::pick_best_token, + RootDatabase, +}; +use itertools::Itertools; +use syntax::{AstNode, SyntaxKind::*, T}; + +use crate::{doc_links::token_as_doc_comment, RangeInfo}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct MonikerIdentifier { + crate_name: String, + path: Vec<Name>, +} + +impl ToString for MonikerIdentifier { + fn to_string(&self) -> String { + match self { + MonikerIdentifier { path, crate_name } => { + format!("{}::{}", crate_name, path.iter().map(|x| x.to_string()).join("::")) + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MonikerKind { + Import, + Export, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MonikerResult { + pub identifier: MonikerIdentifier, + pub kind: MonikerKind, + pub package_information: PackageInformation, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PackageInformation { + pub name: String, + pub repo: String, + pub version: String, +} + +pub(crate) fn crate_for_file(db: &RootDatabase, file_id: FileId) -> Option<Crate> { + for &krate in db.relevant_crates(file_id).iter() { + let crate_def_map = db.crate_def_map(krate); + for (_, data) in crate_def_map.modules() { + if data.origin.file_id() == Some(file_id) { + return Some(krate.into()); + } + } + } + None +} + +pub(crate) fn moniker( + db: &RootDatabase, + FilePosition { file_id, offset }: FilePosition, +) -> Option<RangeInfo<Vec<MonikerResult>>> { + let sema = &Semantics::new(db); + let file = sema.parse(file_id).syntax().clone(); + let current_crate = crate_for_file(db, file_id)?; + let original_token = pick_best_token(file.token_at_offset(offset), |kind| match kind { + IDENT + | INT_NUMBER + | LIFETIME_IDENT + | T![self] + | T![super] + | T![crate] + | T![Self] + | COMMENT => 2, + kind if kind.is_trivia() => 0, + _ => 1, + })?; + if let Some(doc_comment) = token_as_doc_comment(&original_token) { + return doc_comment.get_definition_with_descend_at(sema, offset, |def, _, _| { + let m = def_to_moniker(db, def, current_crate)?; + Some(RangeInfo::new(original_token.text_range(), vec![m])) + }); + } + let navs = sema + .descend_into_macros(original_token.clone()) + .into_iter() + .filter_map(|token| { + IdentClass::classify_token(sema, &token).map(IdentClass::definitions).map(|it| { + it.into_iter().flat_map(|def| def_to_moniker(sema.db, def, current_crate)) + }) + }) + .flatten() + .unique() + .collect::<Vec<_>>(); + Some(RangeInfo::new(original_token.text_range(), navs)) +} + +pub(crate) fn def_to_moniker( + db: &RootDatabase, + def: Definition, + from_crate: Crate, +) -> Option<MonikerResult> { + if matches!(def, Definition::GenericParam(_) | Definition::SelfType(_) | Definition::Local(_)) { + return None; + } + let module = def.module(db)?; + let krate = module.krate(); + let mut path = vec![]; + path.extend(module.path_to_root(db).into_iter().filter_map(|x| x.name(db))); + + // Handle associated items within a trait + if let Some(assoc) = def.as_assoc_item(db) { + let container = assoc.container(db); + match container { + AssocItemContainer::Trait(trait_) => { + // Because different traits can have functions with the same name, + // we have to include the trait name as part of the moniker for uniqueness. + path.push(trait_.name(db)); + } + AssocItemContainer::Impl(impl_) => { + // Because a struct can implement multiple traits, for implementations + // we add both the struct name and the trait name to the path + if let Some(adt) = impl_.self_ty(db).as_adt() { + path.push(adt.name(db)); + } + + if let Some(trait_) = impl_.trait_(db) { + path.push(trait_.name(db)); + } + } + } + } + + if let Definition::Field(it) = def { + path.push(it.parent_def(db).name(db)); + } + + path.push(def.name(db)?); + Some(MonikerResult { + identifier: MonikerIdentifier { + crate_name: krate.display_name(db)?.crate_name().to_string(), + path, + }, + kind: if krate == from_crate { MonikerKind::Export } else { MonikerKind::Import }, + package_information: { + let name = krate.display_name(db)?.to_string(); + let (repo, version) = match krate.origin(db) { + CrateOrigin::CratesIo { repo } => (repo?, krate.version(db)?), + CrateOrigin::Lang(lang) => ( + "https://github.com/rust-lang/rust/".to_string(), + match lang { + LangCrateOrigin::Other => { + "https://github.com/rust-lang/rust/library/".into() + } + lang => format!("https://github.com/rust-lang/rust/library/{lang}",), + }, + ), + }; + PackageInformation { name, repo, version } + }, + }) +} + +#[cfg(test)] +mod tests { + use crate::fixture; + + use super::MonikerKind; + + #[track_caller] + fn no_moniker(ra_fixture: &str) { + let (analysis, position) = fixture::position(ra_fixture); + if let Some(x) = analysis.moniker(position).unwrap() { + assert_eq!(x.info.len(), 0, "Moniker founded but no moniker expected: {:?}", x); + } + } + + #[track_caller] + fn check_moniker(ra_fixture: &str, identifier: &str, package: &str, kind: MonikerKind) { + let (analysis, position) = fixture::position(ra_fixture); + let x = analysis.moniker(position).unwrap().expect("no moniker found").info; + assert_eq!(x.len(), 1); + let x = x.into_iter().next().unwrap(); + assert_eq!(identifier, x.identifier.to_string()); + assert_eq!(package, format!("{:?}", x.package_information)); + assert_eq!(kind, x.kind); + } + + #[test] + fn basic() { + check_moniker( + r#" +//- /lib.rs crate:main deps:foo +use foo::module::func; +fn main() { + func$0(); +} +//- /foo/lib.rs crate:foo@CratesIo:0.1.0,https://a.b/foo.git +pub mod module { + pub fn func() {} +} +"#, + "foo::module::func", + r#"PackageInformation { name: "foo", repo: "https://a.b/foo.git", version: "0.1.0" }"#, + MonikerKind::Import, + ); + check_moniker( + r#" +//- /lib.rs crate:main deps:foo +use foo::module::func; +fn main() { + func(); +} +//- /foo/lib.rs crate:foo@CratesIo:0.1.0,https://a.b/foo.git +pub mod module { + pub fn func$0() {} +} +"#, + "foo::module::func", + r#"PackageInformation { name: "foo", repo: "https://a.b/foo.git", version: "0.1.0" }"#, + MonikerKind::Export, + ); + } + + #[test] + fn moniker_for_trait() { + check_moniker( + r#" +//- /foo/lib.rs crate:foo@CratesIo:0.1.0,https://a.b/foo.git +pub mod module { + pub trait MyTrait { + pub fn func$0() {} + } +} +"#, + "foo::module::MyTrait::func", + r#"PackageInformation { name: "foo", repo: "https://a.b/foo.git", version: "0.1.0" }"#, + MonikerKind::Export, + ); + } + + #[test] + fn moniker_for_trait_constant() { + check_moniker( + r#" +//- /foo/lib.rs crate:foo@CratesIo:0.1.0,https://a.b/foo.git +pub mod module { + pub trait MyTrait { + const MY_CONST$0: u8; + } +} +"#, + "foo::module::MyTrait::MY_CONST", + r#"PackageInformation { name: "foo", repo: "https://a.b/foo.git", version: "0.1.0" }"#, + MonikerKind::Export, + ); + } + + #[test] + fn moniker_for_trait_type() { + check_moniker( + r#" +//- /foo/lib.rs crate:foo@CratesIo:0.1.0,https://a.b/foo.git +pub mod module { + pub trait MyTrait { + type MyType$0; + } +} +"#, + "foo::module::MyTrait::MyType", + r#"PackageInformation { name: "foo", repo: "https://a.b/foo.git", version: "0.1.0" }"#, + MonikerKind::Export, + ); + } + + #[test] + fn moniker_for_trait_impl_function() { + check_moniker( + r#" +//- /foo/lib.rs crate:foo@CratesIo:0.1.0,https://a.b/foo.git +pub mod module { + pub trait MyTrait { + pub fn func() {} + } + + struct MyStruct {} + + impl MyTrait for MyStruct { + pub fn func$0() {} + } +} +"#, + "foo::module::MyStruct::MyTrait::func", + r#"PackageInformation { name: "foo", repo: "https://a.b/foo.git", version: "0.1.0" }"#, + MonikerKind::Export, + ); + } + + #[test] + fn moniker_for_field() { + check_moniker( + r#" +//- /lib.rs crate:main deps:foo +use foo::St; +fn main() { + let x = St { a$0: 2 }; +} +//- /foo/lib.rs crate:foo@CratesIo:0.1.0,https://a.b/foo.git +pub struct St { + pub a: i32, +} +"#, + "foo::St::a", + r#"PackageInformation { name: "foo", repo: "https://a.b/foo.git", version: "0.1.0" }"#, + MonikerKind::Import, + ); + } + + #[test] + fn no_moniker_for_local() { + no_moniker( + r#" +//- /lib.rs crate:main deps:foo +use foo::module::func; +fn main() { + func(); +} +//- /foo/lib.rs crate:foo@CratesIo:0.1.0,https://a.b/foo.git +pub mod module { + pub fn func() { + let x$0 = 2; + } +} +"#, + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/move_item.rs b/src/tools/rust-analyzer/crates/ide/src/move_item.rs new file mode 100644 index 000000000..02e9fb8b5 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/move_item.rs @@ -0,0 +1,890 @@ +use std::{iter::once, mem}; + +use hir::Semantics; +use ide_db::{base_db::FileRange, helpers::pick_best_token, RootDatabase}; +use itertools::Itertools; +use syntax::{algo, ast, match_ast, AstNode, SyntaxElement, SyntaxKind, SyntaxNode, TextRange}; +use text_edit::{TextEdit, TextEditBuilder}; + +#[derive(Copy, Clone, Debug)] +pub enum Direction { + Up, + Down, +} + +// Feature: Move Item +// +// Move item under cursor or selection up and down. +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Move item up** +// | VS Code | **Rust Analyzer: Move item down** +// |=== +// +// image::https://user-images.githubusercontent.com/48062697/113065576-04298180-91b1-11eb-91ce-4505e99ed598.gif[] +pub(crate) fn move_item( + db: &RootDatabase, + range: FileRange, + direction: Direction, +) -> Option<TextEdit> { + let sema = Semantics::new(db); + let file = sema.parse(range.file_id); + + let item = if range.range.is_empty() { + SyntaxElement::Token(pick_best_token( + file.syntax().token_at_offset(range.range.start()), + |kind| match kind { + SyntaxKind::IDENT | SyntaxKind::LIFETIME_IDENT => 2, + kind if kind.is_trivia() => 0, + _ => 1, + }, + )?) + } else { + file.syntax().covering_element(range.range) + }; + + find_ancestors(item, direction, range.range) +} + +fn find_ancestors(item: SyntaxElement, direction: Direction, range: TextRange) -> Option<TextEdit> { + let root = match item { + SyntaxElement::Node(node) => node, + SyntaxElement::Token(token) => token.parent()?, + }; + + let movable = [ + SyntaxKind::ARG_LIST, + SyntaxKind::GENERIC_PARAM_LIST, + SyntaxKind::GENERIC_ARG_LIST, + SyntaxKind::VARIANT_LIST, + SyntaxKind::TYPE_BOUND_LIST, + SyntaxKind::MATCH_ARM, + SyntaxKind::PARAM, + SyntaxKind::LET_STMT, + SyntaxKind::EXPR_STMT, + SyntaxKind::IF_EXPR, + SyntaxKind::FOR_EXPR, + SyntaxKind::LOOP_EXPR, + SyntaxKind::WHILE_EXPR, + SyntaxKind::RETURN_EXPR, + SyntaxKind::MATCH_EXPR, + SyntaxKind::MACRO_CALL, + SyntaxKind::TYPE_ALIAS, + SyntaxKind::TRAIT, + SyntaxKind::IMPL, + SyntaxKind::MACRO_DEF, + SyntaxKind::STRUCT, + SyntaxKind::UNION, + SyntaxKind::ENUM, + SyntaxKind::FN, + SyntaxKind::MODULE, + SyntaxKind::USE, + SyntaxKind::STATIC, + SyntaxKind::CONST, + SyntaxKind::MACRO_RULES, + SyntaxKind::MACRO_DEF, + ]; + + let ancestor = once(root.clone()) + .chain(root.ancestors()) + .find(|ancestor| movable.contains(&ancestor.kind()))?; + + move_in_direction(&ancestor, direction, range) +} + +fn move_in_direction( + node: &SyntaxNode, + direction: Direction, + range: TextRange, +) -> Option<TextEdit> { + match_ast! { + match node { + ast::ArgList(it) => swap_sibling_in_list(node, it.args(), range, direction), + ast::GenericParamList(it) => swap_sibling_in_list(node, it.generic_params(), range, direction), + ast::GenericArgList(it) => swap_sibling_in_list(node, it.generic_args(), range, direction), + ast::VariantList(it) => swap_sibling_in_list(node, it.variants(), range, direction), + ast::TypeBoundList(it) => swap_sibling_in_list(node, it.bounds(), range, direction), + _ => Some(replace_nodes(range, node, &match direction { + Direction::Up => node.prev_sibling(), + Direction::Down => node.next_sibling(), + }?)) + } + } +} + +fn swap_sibling_in_list<A: AstNode + Clone, I: Iterator<Item = A>>( + node: &SyntaxNode, + list: I, + range: TextRange, + direction: Direction, +) -> Option<TextEdit> { + let list_lookup = list.tuple_windows().find(|(l, r)| match direction { + Direction::Up => r.syntax().text_range().contains_range(range), + Direction::Down => l.syntax().text_range().contains_range(range), + }); + + if let Some((l, r)) = list_lookup { + Some(replace_nodes(range, l.syntax(), r.syntax())) + } else { + // Cursor is beyond any movable list item (for example, on curly brace in enum). + // It's not necessary, that parent of list is movable (arg list's parent is not, for example), + // and we have to continue tree traversal to find suitable node. + find_ancestors(SyntaxElement::Node(node.parent()?), direction, range) + } +} + +fn replace_nodes<'a>( + range: TextRange, + mut first: &'a SyntaxNode, + mut second: &'a SyntaxNode, +) -> TextEdit { + let cursor_offset = if range.is_empty() { + // FIXME: `applySnippetTextEdits` does not support non-empty selection ranges + if first.text_range().contains_range(range) { + Some(range.start() - first.text_range().start()) + } else if second.text_range().contains_range(range) { + mem::swap(&mut first, &mut second); + Some(range.start() - first.text_range().start()) + } else { + None + } + } else { + None + }; + + let first_with_cursor = match cursor_offset { + Some(offset) => { + let mut item_text = first.text().to_string(); + item_text.insert_str(offset.into(), "$0"); + item_text + } + None => first.text().to_string(), + }; + + let mut edit = TextEditBuilder::default(); + + algo::diff(first, second).into_text_edit(&mut edit); + edit.replace(second.text_range(), first_with_cursor); + + edit.finish() +} + +#[cfg(test)] +mod tests { + use crate::fixture; + use expect_test::{expect, Expect}; + + use crate::Direction; + + fn check(ra_fixture: &str, expect: Expect, direction: Direction) { + let (analysis, range) = fixture::range(ra_fixture); + let edit = analysis.move_item(range, direction).unwrap().unwrap_or_default(); + let mut file = analysis.file_text(range.file_id).unwrap().to_string(); + edit.apply(&mut file); + expect.assert_eq(&file); + } + + #[test] + fn test_moves_match_arm_up() { + check( + r#" +fn main() { + match true { + true => { + println!("Hello, world"); + }, + false =>$0$0 { + println!("Test"); + } + }; +} +"#, + expect![[r#" + fn main() { + match true { + false =>$0 { + println!("Test"); + } + true => { + println!("Hello, world"); + }, + }; + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_match_arm_down() { + check( + r#" +fn main() { + match true { + true =>$0$0 { + println!("Hello, world"); + }, + false => { + println!("Test"); + } + }; +} +"#, + expect![[r#" + fn main() { + match true { + false => { + println!("Test"); + } + true =>$0 { + println!("Hello, world"); + }, + }; + } + "#]], + Direction::Down, + ); + } + + #[test] + fn test_nowhere_to_move() { + check( + r#" +fn main() { + match true { + true =>$0$0 { + println!("Hello, world"); + }, + false => { + println!("Test"); + } + }; +} +"#, + expect![[r#" + fn main() { + match true { + true => { + println!("Hello, world"); + }, + false => { + println!("Test"); + } + }; + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_let_stmt_up() { + check( + r#" +fn main() { + let test = 123; + let test2$0$0 = 456; +} +"#, + expect![[r#" + fn main() { + let test2$0 = 456; + let test = 123; + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_expr_up() { + check( + r#" +fn main() { + println!("Hello, world"); + println!("All I want to say is...");$0$0 +} +"#, + expect![[r#" + fn main() { + println!("All I want to say is...");$0 + println!("Hello, world"); + } + "#]], + Direction::Up, + ); + check( + r#" +fn main() { + println!("Hello, world"); + + if true { + println!("Test"); + }$0$0 +} +"#, + expect![[r#" + fn main() { + if true { + println!("Test"); + }$0 + + println!("Hello, world"); + } + "#]], + Direction::Up, + ); + check( + r#" +fn main() { + println!("Hello, world"); + + for i in 0..10 { + println!("Test"); + }$0$0 +} +"#, + expect![[r#" + fn main() { + for i in 0..10 { + println!("Test"); + }$0 + + println!("Hello, world"); + } + "#]], + Direction::Up, + ); + check( + r#" +fn main() { + println!("Hello, world"); + + loop { + println!("Test"); + }$0$0 +} +"#, + expect![[r#" + fn main() { + loop { + println!("Test"); + }$0 + + println!("Hello, world"); + } + "#]], + Direction::Up, + ); + check( + r#" +fn main() { + println!("Hello, world"); + + while true { + println!("Test"); + }$0$0 +} +"#, + expect![[r#" + fn main() { + while true { + println!("Test"); + }$0 + + println!("Hello, world"); + } + "#]], + Direction::Up, + ); + check( + r#" +fn main() { + println!("Hello, world"); + + return 123;$0$0 +} +"#, + expect![[r#" + fn main() { + return 123;$0 + + println!("Hello, world"); + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_nowhere_to_move_stmt() { + check( + r#" +fn main() { + println!("All I want to say is...");$0$0 + println!("Hello, world"); +} +"#, + expect![[r#" + fn main() { + println!("All I want to say is..."); + println!("Hello, world"); + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_move_item() { + check( + r#" +fn main() {} + +fn foo() {}$0$0 +"#, + expect![[r#" + fn foo() {}$0 + + fn main() {} + "#]], + Direction::Up, + ); + } + + #[test] + fn test_move_impl_up() { + check( + r#" +struct Yay; + +trait Wow {} + +impl Wow for Yay $0$0{} +"#, + expect![[r#" + struct Yay; + + impl Wow for Yay $0{} + + trait Wow {} + "#]], + Direction::Up, + ); + } + + #[test] + fn test_move_use_up() { + check( + r#" +use std::vec::Vec; +use std::collections::HashMap$0$0; +"#, + expect![[r#" + use std::collections::HashMap$0; + use std::vec::Vec; + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_match_expr_up() { + check( + r#" +fn main() { + let test = 123; + + $0match test { + 456 => {}, + _ => {} + };$0 +} +"#, + expect![[r#" + fn main() { + match test { + 456 => {}, + _ => {} + }; + + let test = 123; + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_param() { + check( + r#" +fn test(one: i32, two$0$0: u32) {} + +fn main() { + test(123, 456); +} +"#, + expect![[r#" + fn test(two$0: u32, one: i32) {} + + fn main() { + test(123, 456); + } + "#]], + Direction::Up, + ); + check( + r#" +fn f($0$0arg: u8, arg2: u16) {} +"#, + expect![[r#" + fn f(arg2: u16, $0arg: u8) {} + "#]], + Direction::Down, + ); + } + + #[test] + fn test_moves_arg_up() { + check( + r#" +fn test(one: i32, two: u32) {} + +fn main() { + test(123, 456$0$0); +} +"#, + expect![[r#" + fn test(one: i32, two: u32) {} + + fn main() { + test(456$0, 123); + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_arg_down() { + check( + r#" +fn test(one: i32, two: u32) {} + +fn main() { + test(123$0$0, 456); +} +"#, + expect![[r#" + fn test(one: i32, two: u32) {} + + fn main() { + test(456, 123$0); + } + "#]], + Direction::Down, + ); + } + + #[test] + fn test_nowhere_to_move_arg() { + check( + r#" +fn test(one: i32, two: u32) {} + +fn main() { + test(123$0$0, 456); +} +"#, + expect![[r#" + fn test(one: i32, two: u32) {} + + fn main() { + test(123, 456); + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_generic_param_up() { + check( + r#" +struct Test<A, B$0$0>(A, B); + +fn main() {} +"#, + expect![[r#" + struct Test<B$0, A>(A, B); + + fn main() {} + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_generic_arg_up() { + check( + r#" +struct Test<A, B>(A, B); + +fn main() { + let t = Test::<i32, &str$0$0>(123, "yay"); +} +"#, + expect![[r#" + struct Test<A, B>(A, B); + + fn main() { + let t = Test::<&str$0, i32>(123, "yay"); + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_variant_up() { + check( + r#" +enum Hello { + One, + Two$0$0 +} + +fn main() {} +"#, + expect![[r#" + enum Hello { + Two$0, + One + } + + fn main() {} + "#]], + Direction::Up, + ); + } + + #[test] + fn test_moves_type_bound_up() { + check( + r#" +trait One {} + +trait Two {} + +fn test<T: One + Two$0$0>(t: T) {} + +fn main() {} +"#, + expect![[r#" + trait One {} + + trait Two {} + + fn test<T: Two$0 + One>(t: T) {} + + fn main() {} + "#]], + Direction::Up, + ); + } + + #[test] + fn test_prioritizes_trait_items() { + check( + r#" +struct Test; + +trait Yay { + type One; + + type Two; + + fn inner(); +} + +impl Yay for Test { + type One = i32; + + type Two = u32; + + fn inner() {$0$0 + println!("Mmmm"); + } +} +"#, + expect![[r#" + struct Test; + + trait Yay { + type One; + + type Two; + + fn inner(); + } + + impl Yay for Test { + type One = i32; + + fn inner() {$0 + println!("Mmmm"); + } + + type Two = u32; + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_weird_nesting() { + check( + r#" +fn test() { + mod hello { + fn inner() {} + } + + mod hi {$0$0 + fn inner() {} + } +} +"#, + expect![[r#" + fn test() { + mod hi {$0 + fn inner() {} + } + + mod hello { + fn inner() {} + } + } + "#]], + Direction::Up, + ); + } + + #[test] + fn test_cursor_at_item_start() { + check( + r#" +$0$0#[derive(Debug)] +enum FooBar { + Foo, + Bar, +} + +fn main() {} +"#, + expect![[r##" + fn main() {} + + $0#[derive(Debug)] + enum FooBar { + Foo, + Bar, + } + "##]], + Direction::Down, + ); + check( + r#" +$0$0enum FooBar { + Foo, + Bar, +} + +fn main() {} +"#, + expect![[r#" + fn main() {} + + $0enum FooBar { + Foo, + Bar, + } + "#]], + Direction::Down, + ); + check( + r#" +struct Test; + +trait SomeTrait {} + +$0$0impl SomeTrait for Test {} + +fn main() {} +"#, + expect![[r#" + struct Test; + + $0impl SomeTrait for Test {} + + trait SomeTrait {} + + fn main() {} + "#]], + Direction::Up, + ); + } + + #[test] + fn test_cursor_at_item_end() { + check( + r#" +enum FooBar { + Foo, + Bar, +}$0$0 + +fn main() {} +"#, + expect![[r#" + fn main() {} + + enum FooBar { + Foo, + Bar, + }$0 + "#]], + Direction::Down, + ); + check( + r#" +struct Test; + +trait SomeTrait {} + +impl SomeTrait for Test {}$0$0 + +fn main() {} +"#, + expect![[r#" + struct Test; + + impl SomeTrait for Test {}$0 + + trait SomeTrait {} + + fn main() {} + "#]], + Direction::Up, + ); + } + + #[test] + fn handles_empty_file() { + check(r#"$0$0"#, expect![[r#""#]], Direction::Up); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/navigation_target.rs b/src/tools/rust-analyzer/crates/ide/src/navigation_target.rs new file mode 100644 index 000000000..9f049e298 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/navigation_target.rs @@ -0,0 +1,623 @@ +//! See [`NavigationTarget`]. + +use std::fmt; + +use either::Either; +use hir::{ + symbols::FileSymbol, AssocItem, Documentation, FieldSource, HasAttrs, HasSource, HirDisplay, + InFile, ModuleSource, Semantics, +}; +use ide_db::{ + base_db::{FileId, FileRange}, + SymbolKind, +}; +use ide_db::{defs::Definition, RootDatabase}; +use stdx::never; +use syntax::{ + ast::{self, HasName}, + match_ast, AstNode, SmolStr, SyntaxNode, TextRange, +}; + +/// `NavigationTarget` represents an element in the editor's UI which you can +/// click on to navigate to a particular piece of code. +/// +/// Typically, a `NavigationTarget` corresponds to some element in the source +/// code, like a function or a struct, but this is not strictly required. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct NavigationTarget { + pub file_id: FileId, + /// Range which encompasses the whole element. + /// + /// Should include body, doc comments, attributes, etc. + /// + /// Clients should use this range to answer "is the cursor inside the + /// element?" question. + pub full_range: TextRange, + /// A "most interesting" range within the `full_range`. + /// + /// Typically, `full_range` is the whole syntax node, including doc + /// comments, and `focus_range` is the range of the identifier. + /// + /// Clients should place the cursor on this range when navigating to this target. + pub focus_range: Option<TextRange>, + pub name: SmolStr, + pub kind: Option<SymbolKind>, + pub container_name: Option<SmolStr>, + pub description: Option<String>, + pub docs: Option<Documentation>, +} + +impl fmt::Debug for NavigationTarget { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut f = f.debug_struct("NavigationTarget"); + macro_rules! opt { + ($($name:ident)*) => {$( + if let Some(it) = &self.$name { + f.field(stringify!($name), it); + } + )*} + } + f.field("file_id", &self.file_id).field("full_range", &self.full_range); + opt!(focus_range); + f.field("name", &self.name); + opt!(kind container_name description docs); + f.finish() + } +} + +pub(crate) trait ToNav { + fn to_nav(&self, db: &RootDatabase) -> NavigationTarget; +} + +pub(crate) trait TryToNav { + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget>; +} + +impl<T: TryToNav, U: TryToNav> TryToNav for Either<T, U> { + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { + match self { + Either::Left(it) => it.try_to_nav(db), + Either::Right(it) => it.try_to_nav(db), + } + } +} + +impl NavigationTarget { + pub fn focus_or_full_range(&self) -> TextRange { + self.focus_range.unwrap_or(self.full_range) + } + + pub(crate) fn from_module_to_decl(db: &RootDatabase, module: hir::Module) -> NavigationTarget { + let name = module.name(db).map(|it| it.to_smol_str()).unwrap_or_default(); + if let Some(src @ InFile { value, .. }) = &module.declaration_source(db) { + let FileRange { file_id, range: full_range } = src.syntax().original_file_range(db); + let focus_range = + value.name().and_then(|it| orig_focus_range(db, src.file_id, it.syntax())); + let mut res = NavigationTarget::from_syntax( + file_id, + name, + focus_range, + full_range, + SymbolKind::Module, + ); + res.docs = module.attrs(db).docs(); + res.description = Some(module.display(db).to_string()); + return res; + } + module.to_nav(db) + } + + #[cfg(test)] + pub(crate) fn debug_render(&self) -> String { + let mut buf = format!( + "{} {:?} {:?} {:?}", + self.name, + self.kind.unwrap(), + self.file_id, + self.full_range + ); + if let Some(focus_range) = self.focus_range { + buf.push_str(&format!(" {:?}", focus_range)) + } + if let Some(container_name) = &self.container_name { + buf.push_str(&format!(" {}", container_name)) + } + buf + } + + /// Allows `NavigationTarget` to be created from a `NameOwner` + pub(crate) fn from_named( + db: &RootDatabase, + node @ InFile { file_id, value }: InFile<&dyn ast::HasName>, + kind: SymbolKind, + ) -> NavigationTarget { + let name = value.name().map(|it| it.text().into()).unwrap_or_else(|| "_".into()); + let focus_range = value.name().and_then(|it| orig_focus_range(db, file_id, it.syntax())); + let FileRange { file_id, range } = node.map(|it| it.syntax()).original_file_range(db); + + NavigationTarget::from_syntax(file_id, name, focus_range, range, kind) + } + + fn from_syntax( + file_id: FileId, + name: SmolStr, + focus_range: Option<TextRange>, + full_range: TextRange, + kind: SymbolKind, + ) -> NavigationTarget { + NavigationTarget { + file_id, + name, + kind: Some(kind), + full_range, + focus_range, + container_name: None, + description: None, + docs: None, + } + } +} + +impl TryToNav for FileSymbol { + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { + let full_range = self.loc.original_range(db)?; + let name_range = self.loc.original_name_range(db)?; + + Some(NavigationTarget { + file_id: full_range.file_id, + name: self.name.clone(), + kind: Some(self.kind.into()), + full_range: full_range.range, + focus_range: Some(name_range.range), + container_name: self.container_name.clone(), + description: description_from_symbol(db, self), + docs: None, + }) + } +} + +impl TryToNav for Definition { + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { + match self { + Definition::Local(it) => Some(it.to_nav(db)), + Definition::Label(it) => Some(it.to_nav(db)), + Definition::Module(it) => Some(it.to_nav(db)), + Definition::Macro(it) => it.try_to_nav(db), + Definition::Field(it) => it.try_to_nav(db), + Definition::SelfType(it) => it.try_to_nav(db), + Definition::GenericParam(it) => it.try_to_nav(db), + Definition::Function(it) => it.try_to_nav(db), + Definition::Adt(it) => it.try_to_nav(db), + Definition::Variant(it) => it.try_to_nav(db), + Definition::Const(it) => it.try_to_nav(db), + Definition::Static(it) => it.try_to_nav(db), + Definition::Trait(it) => it.try_to_nav(db), + Definition::TypeAlias(it) => it.try_to_nav(db), + Definition::BuiltinType(_) => None, + Definition::ToolModule(_) => None, + Definition::BuiltinAttr(_) => None, + // FIXME: The focus range should be set to the helper declaration + Definition::DeriveHelper(it) => it.derive().try_to_nav(db), + } + } +} + +impl TryToNav for hir::ModuleDef { + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { + match self { + hir::ModuleDef::Module(it) => Some(it.to_nav(db)), + hir::ModuleDef::Function(it) => it.try_to_nav(db), + hir::ModuleDef::Adt(it) => it.try_to_nav(db), + hir::ModuleDef::Variant(it) => it.try_to_nav(db), + hir::ModuleDef::Const(it) => it.try_to_nav(db), + hir::ModuleDef::Static(it) => it.try_to_nav(db), + hir::ModuleDef::Trait(it) => it.try_to_nav(db), + hir::ModuleDef::TypeAlias(it) => it.try_to_nav(db), + hir::ModuleDef::Macro(it) => it.try_to_nav(db), + hir::ModuleDef::BuiltinType(_) => None, + } + } +} + +pub(crate) trait ToNavFromAst { + const KIND: SymbolKind; +} +impl ToNavFromAst for hir::Function { + const KIND: SymbolKind = SymbolKind::Function; +} +impl ToNavFromAst for hir::Const { + const KIND: SymbolKind = SymbolKind::Const; +} +impl ToNavFromAst for hir::Static { + const KIND: SymbolKind = SymbolKind::Static; +} +impl ToNavFromAst for hir::Struct { + const KIND: SymbolKind = SymbolKind::Struct; +} +impl ToNavFromAst for hir::Enum { + const KIND: SymbolKind = SymbolKind::Enum; +} +impl ToNavFromAst for hir::Variant { + const KIND: SymbolKind = SymbolKind::Variant; +} +impl ToNavFromAst for hir::Union { + const KIND: SymbolKind = SymbolKind::Union; +} +impl ToNavFromAst for hir::TypeAlias { + const KIND: SymbolKind = SymbolKind::TypeAlias; +} +impl ToNavFromAst for hir::Trait { + const KIND: SymbolKind = SymbolKind::Trait; +} + +impl<D> TryToNav for D +where + D: HasSource + ToNavFromAst + Copy + HasAttrs + HirDisplay, + D::Ast: ast::HasName, +{ + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { + let src = self.source(db)?; + let mut res = NavigationTarget::from_named( + db, + src.as_ref().map(|it| it as &dyn ast::HasName), + D::KIND, + ); + res.docs = self.docs(db); + res.description = Some(self.display(db).to_string()); + Some(res) + } +} + +impl ToNav for hir::Module { + fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { + let InFile { file_id, value } = self.definition_source(db); + + let name = self.name(db).map(|it| it.to_smol_str()).unwrap_or_default(); + let (syntax, focus) = match &value { + ModuleSource::SourceFile(node) => (node.syntax(), None), + ModuleSource::Module(node) => ( + node.syntax(), + node.name().and_then(|it| orig_focus_range(db, file_id, it.syntax())), + ), + ModuleSource::BlockExpr(node) => (node.syntax(), None), + }; + let FileRange { file_id, range: full_range } = + InFile::new(file_id, syntax).original_file_range(db); + NavigationTarget::from_syntax(file_id, name, focus, full_range, SymbolKind::Module) + } +} + +impl TryToNav for hir::Impl { + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { + let InFile { file_id, value } = self.source(db)?; + let derive_attr = self.is_builtin_derive(db); + + let focus_range = if derive_attr.is_some() { + None + } else { + value.self_ty().and_then(|ty| orig_focus_range(db, file_id, ty.syntax())) + }; + + let FileRange { file_id, range: full_range } = match &derive_attr { + Some(attr) => attr.syntax().original_file_range(db), + None => InFile::new(file_id, value.syntax()).original_file_range(db), + }; + + Some(NavigationTarget::from_syntax( + file_id, + "impl".into(), + focus_range, + full_range, + SymbolKind::Impl, + )) + } +} + +impl TryToNav for hir::Field { + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { + let src = self.source(db)?; + + let field_source = match &src.value { + FieldSource::Named(it) => { + let mut res = + NavigationTarget::from_named(db, src.with_value(it), SymbolKind::Field); + res.docs = self.docs(db); + res.description = Some(self.display(db).to_string()); + res + } + FieldSource::Pos(it) => { + let FileRange { file_id, range } = + src.with_value(it.syntax()).original_file_range(db); + NavigationTarget::from_syntax(file_id, "".into(), None, range, SymbolKind::Field) + } + }; + Some(field_source) + } +} + +impl TryToNav for hir::Macro { + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { + let src = self.source(db)?; + let name_owner: &dyn ast::HasName = match &src.value { + Either::Left(it) => it, + Either::Right(it) => it, + }; + let mut res = NavigationTarget::from_named( + db, + src.as_ref().with_value(name_owner), + self.kind(db).into(), + ); + res.docs = self.docs(db); + Some(res) + } +} + +impl TryToNav for hir::Adt { + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { + match self { + hir::Adt::Struct(it) => it.try_to_nav(db), + hir::Adt::Union(it) => it.try_to_nav(db), + hir::Adt::Enum(it) => it.try_to_nav(db), + } + } +} + +impl TryToNav for hir::AssocItem { + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { + match self { + AssocItem::Function(it) => it.try_to_nav(db), + AssocItem::Const(it) => it.try_to_nav(db), + AssocItem::TypeAlias(it) => it.try_to_nav(db), + } + } +} + +impl TryToNav for hir::GenericParam { + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { + match self { + hir::GenericParam::TypeParam(it) => it.try_to_nav(db), + hir::GenericParam::ConstParam(it) => it.try_to_nav(db), + hir::GenericParam::LifetimeParam(it) => it.try_to_nav(db), + } + } +} + +impl ToNav for hir::Local { + fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { + let InFile { file_id, value } = self.source(db); + let (node, name) = match &value { + Either::Left(bind_pat) => (bind_pat.syntax(), bind_pat.name()), + Either::Right(it) => (it.syntax(), it.name()), + }; + let focus_range = name.and_then(|it| orig_focus_range(db, file_id, it.syntax())); + let FileRange { file_id, range: full_range } = + InFile::new(file_id, node).original_file_range(db); + + let name = self.name(db).to_smol_str(); + let kind = if self.is_self(db) { + SymbolKind::SelfParam + } else if self.is_param(db) { + SymbolKind::ValueParam + } else { + SymbolKind::Local + }; + NavigationTarget { + file_id, + name, + kind: Some(kind), + full_range, + focus_range, + container_name: None, + description: None, + docs: None, + } + } +} + +impl ToNav for hir::Label { + fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { + let InFile { file_id, value } = self.source(db); + let name = self.name(db).to_smol_str(); + + let range = |syntax: &_| InFile::new(file_id, syntax).original_file_range(db); + let FileRange { file_id, range: full_range } = range(value.syntax()); + let focus_range = value.lifetime().map(|lt| range(lt.syntax()).range); + + NavigationTarget { + file_id, + name, + kind: Some(SymbolKind::Label), + full_range, + focus_range, + container_name: None, + description: None, + docs: None, + } + } +} + +impl TryToNav for hir::TypeParam { + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { + let InFile { file_id, value } = self.merge().source(db)?; + let name = self.name(db).to_smol_str(); + + let value = match value { + Either::Left(ast::TypeOrConstParam::Type(x)) => Either::Left(x), + Either::Left(ast::TypeOrConstParam::Const(_)) => { + never!(); + return None; + } + Either::Right(x) => Either::Right(x), + }; + + let range = |syntax: &_| InFile::new(file_id, syntax).original_file_range(db); + let focus_range = |syntax: &_| InFile::new(file_id, syntax).original_file_range_opt(db); + let FileRange { file_id, range: full_range } = match &value { + Either::Left(type_param) => range(type_param.syntax()), + Either::Right(trait_) => trait_ + .name() + .and_then(|name| focus_range(name.syntax())) + .unwrap_or_else(|| range(trait_.syntax())), + }; + let focus_range = value + .either(|it| it.name(), |it| it.name()) + .and_then(|it| focus_range(it.syntax())) + .map(|it| it.range); + Some(NavigationTarget { + file_id, + name, + kind: Some(SymbolKind::TypeParam), + full_range, + focus_range, + container_name: None, + description: None, + docs: None, + }) + } +} + +impl TryToNav for hir::TypeOrConstParam { + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { + self.split(db).try_to_nav(db) + } +} + +impl TryToNav for hir::LifetimeParam { + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { + let InFile { file_id, value } = self.source(db)?; + let name = self.name(db).to_smol_str(); + + let FileRange { file_id, range: full_range } = + InFile::new(file_id, value.syntax()).original_file_range(db); + Some(NavigationTarget { + file_id, + name, + kind: Some(SymbolKind::LifetimeParam), + full_range, + focus_range: Some(full_range), + container_name: None, + description: None, + docs: None, + }) + } +} + +impl TryToNav for hir::ConstParam { + fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { + let InFile { file_id, value } = self.merge().source(db)?; + let name = self.name(db).to_smol_str(); + + let value = match value { + Either::Left(ast::TypeOrConstParam::Const(x)) => x, + _ => { + never!(); + return None; + } + }; + + let focus_range = value.name().and_then(|it| orig_focus_range(db, file_id, it.syntax())); + let FileRange { file_id, range: full_range } = + InFile::new(file_id, value.syntax()).original_file_range(db); + Some(NavigationTarget { + file_id, + name, + kind: Some(SymbolKind::ConstParam), + full_range, + focus_range, + container_name: None, + description: None, + docs: None, + }) + } +} + +/// Get a description of a symbol. +/// +/// e.g. `struct Name`, `enum Name`, `fn Name` +pub(crate) fn description_from_symbol(db: &RootDatabase, symbol: &FileSymbol) -> Option<String> { + let sema = Semantics::new(db); + let node = symbol.loc.syntax(&sema)?; + + match_ast! { + match node { + ast::Fn(it) => sema.to_def(&it).map(|it| it.display(db).to_string()), + ast::Struct(it) => sema.to_def(&it).map(|it| it.display(db).to_string()), + ast::Enum(it) => sema.to_def(&it).map(|it| it.display(db).to_string()), + ast::Trait(it) => sema.to_def(&it).map(|it| it.display(db).to_string()), + ast::Module(it) => sema.to_def(&it).map(|it| it.display(db).to_string()), + ast::TypeAlias(it) => sema.to_def(&it).map(|it| it.display(db).to_string()), + ast::Const(it) => sema.to_def(&it).map(|it| it.display(db).to_string()), + ast::Static(it) => sema.to_def(&it).map(|it| it.display(db).to_string()), + ast::RecordField(it) => sema.to_def(&it).map(|it| it.display(db).to_string()), + ast::Variant(it) => sema.to_def(&it).map(|it| it.display(db).to_string()), + ast::Union(it) => sema.to_def(&it).map(|it| it.display(db).to_string()), + _ => None, + } + } +} + +fn orig_focus_range( + db: &RootDatabase, + file_id: hir::HirFileId, + syntax: &SyntaxNode, +) -> Option<TextRange> { + InFile::new(file_id, syntax).original_file_range_opt(db).map(|it| it.range) +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use crate::{fixture, Query}; + + #[test] + fn test_nav_for_symbol() { + let (analysis, _) = fixture::file( + r#" +enum FooInner { } +fn foo() { enum FooInner { } } +"#, + ); + + let navs = analysis.symbol_search(Query::new("FooInner".to_string())).unwrap(); + expect![[r#" + [ + NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..17, + focus_range: 5..13, + name: "FooInner", + kind: Enum, + description: "enum FooInner", + }, + NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 29..46, + focus_range: 34..42, + name: "FooInner", + kind: Enum, + container_name: "foo", + description: "enum FooInner", + }, + ] + "#]] + .assert_debug_eq(&navs); + } + + #[test] + fn test_world_symbols_are_case_sensitive() { + let (analysis, _) = fixture::file( + r#" +fn foo() {} +struct Foo; +"#, + ); + + let navs = analysis.symbol_search(Query::new("foo".to_string())).unwrap(); + assert_eq!(navs.len(), 2) + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/parent_module.rs b/src/tools/rust-analyzer/crates/ide/src/parent_module.rs new file mode 100644 index 000000000..9b1f48044 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/parent_module.rs @@ -0,0 +1,167 @@ +use hir::Semantics; +use ide_db::{ + base_db::{CrateId, FileId, FilePosition}, + RootDatabase, +}; +use itertools::Itertools; +use syntax::{ + algo::find_node_at_offset, + ast::{self, AstNode}, +}; + +use crate::NavigationTarget; + +// Feature: Parent Module +// +// Navigates to the parent module of the current module. +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Locate parent module** +// |=== +// +// image::https://user-images.githubusercontent.com/48062697/113065580-04c21800-91b1-11eb-9a32-00086161c0bd.gif[] + +/// This returns `Vec` because a module may be included from several places. +pub(crate) fn parent_module(db: &RootDatabase, position: FilePosition) -> Vec<NavigationTarget> { + let sema = Semantics::new(db); + let source_file = sema.parse(position.file_id); + + let mut module = find_node_at_offset::<ast::Module>(source_file.syntax(), position.offset); + + // If cursor is literally on `mod foo`, go to the grandpa. + if let Some(m) = &module { + if !m + .item_list() + .map_or(false, |it| it.syntax().text_range().contains_inclusive(position.offset)) + { + cov_mark::hit!(test_resolve_parent_module_on_module_decl); + module = m.syntax().ancestors().skip(1).find_map(ast::Module::cast); + } + } + + match module { + Some(module) => sema + .to_def(&module) + .into_iter() + .map(|module| NavigationTarget::from_module_to_decl(db, module)) + .collect(), + None => sema + .to_module_defs(position.file_id) + .map(|module| NavigationTarget::from_module_to_decl(db, module)) + .collect(), + } +} + +/// Returns `Vec` for the same reason as `parent_module` +pub(crate) fn crate_for(db: &RootDatabase, file_id: FileId) -> Vec<CrateId> { + let sema = Semantics::new(db); + sema.to_module_defs(file_id).map(|module| module.krate().into()).unique().collect() +} + +#[cfg(test)] +mod tests { + use ide_db::base_db::FileRange; + + use crate::fixture; + + fn check(ra_fixture: &str) { + let (analysis, position, expected) = fixture::annotations(ra_fixture); + let navs = analysis.parent_module(position).unwrap(); + let navs = navs + .iter() + .map(|nav| FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() }) + .collect::<Vec<_>>(); + assert_eq!(expected.into_iter().map(|(fr, _)| fr).collect::<Vec<_>>(), navs); + } + + #[test] + fn test_resolve_parent_module() { + check( + r#" +//- /lib.rs +mod foo; + //^^^ + +//- /foo.rs +$0// empty +"#, + ); + } + + #[test] + fn test_resolve_parent_module_on_module_decl() { + cov_mark::check!(test_resolve_parent_module_on_module_decl); + check( + r#" +//- /lib.rs +mod foo; + //^^^ +//- /foo.rs +mod $0bar; + +//- /foo/bar.rs +// empty +"#, + ); + } + + #[test] + fn test_resolve_parent_module_for_inline() { + check( + r#" +//- /lib.rs +mod foo { + mod bar { + mod baz { $0 } + } //^^^ +} +"#, + ); + } + + #[test] + fn test_resolve_multi_parent_module() { + check( + r#" +//- /main.rs +mod foo; + //^^^ +#[path = "foo.rs"] +mod bar; + //^^^ +//- /foo.rs +$0 +"#, + ); + } + + #[test] + fn test_resolve_crate_root() { + let (analysis, file_id) = fixture::file( + r#" +//- /foo.rs +$0 +//- /main.rs +mod foo; +"#, + ); + assert_eq!(analysis.crate_for(file_id).unwrap().len(), 1); + } + + #[test] + fn test_resolve_multi_parent_crate() { + let (analysis, file_id) = fixture::file( + r#" +//- /baz.rs +$0 +//- /foo.rs crate:foo +mod baz; +//- /bar.rs crate:bar +mod baz; +"#, + ); + assert_eq!(analysis.crate_for(file_id).unwrap().len(), 2); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/prime_caches.rs b/src/tools/rust-analyzer/crates/ide/src/prime_caches.rs new file mode 100644 index 000000000..296270036 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/prime_caches.rs @@ -0,0 +1,158 @@ +//! rust-analyzer is lazy and doesn't compute anything unless asked. This +//! sometimes is counter productive when, for example, the first goto definition +//! request takes longer to compute. This modules implemented prepopulation of +//! various caches, it's not really advanced at the moment. +mod topologic_sort; + +use std::time::Duration; + +use hir::db::DefDatabase; +use ide_db::{ + base_db::{ + salsa::{Database, ParallelDatabase, Snapshot}, + Cancelled, CrateGraph, CrateId, SourceDatabase, SourceDatabaseExt, + }, + FxHashSet, FxIndexMap, +}; + +use crate::RootDatabase; + +/// We're indexing many crates. +#[derive(Debug)] +pub struct ParallelPrimeCachesProgress { + /// the crates that we are currently priming. + pub crates_currently_indexing: Vec<String>, + /// the total number of crates we want to prime. + pub crates_total: usize, + /// the total number of crates that have finished priming + pub crates_done: usize, +} + +pub(crate) fn parallel_prime_caches( + db: &RootDatabase, + num_worker_threads: u8, + cb: &(dyn Fn(ParallelPrimeCachesProgress) + Sync), +) { + let _p = profile::span("prime_caches"); + + let graph = db.crate_graph(); + let mut crates_to_prime = { + let crate_ids = compute_crates_to_prime(db, &graph); + + let mut builder = topologic_sort::TopologicalSortIter::builder(); + + for &crate_id in &crate_ids { + let crate_data = &graph[crate_id]; + let dependencies = crate_data + .dependencies + .iter() + .map(|d| d.crate_id) + .filter(|i| crate_ids.contains(i)); + + builder.add(crate_id, dependencies); + } + + builder.build() + }; + + enum ParallelPrimeCacheWorkerProgress { + BeginCrate { crate_id: CrateId, crate_name: String }, + EndCrate { crate_id: CrateId }, + } + + let (work_sender, progress_receiver) = { + let (progress_sender, progress_receiver) = crossbeam_channel::unbounded(); + let (work_sender, work_receiver) = crossbeam_channel::unbounded(); + let prime_caches_worker = move |db: Snapshot<RootDatabase>| { + while let Ok((crate_id, crate_name)) = work_receiver.recv() { + progress_sender + .send(ParallelPrimeCacheWorkerProgress::BeginCrate { crate_id, crate_name })?; + + // This also computes the DefMap + db.import_map(crate_id); + + progress_sender.send(ParallelPrimeCacheWorkerProgress::EndCrate { crate_id })?; + } + + Ok::<_, crossbeam_channel::SendError<_>>(()) + }; + + for _ in 0..num_worker_threads { + let worker = prime_caches_worker.clone(); + let db = db.snapshot(); + std::thread::spawn(move || Cancelled::catch(|| worker(db))); + } + + (work_sender, progress_receiver) + }; + + let crates_total = crates_to_prime.pending(); + let mut crates_done = 0; + + // an index map is used to preserve ordering so we can sort the progress report in order of + // "longest crate to index" first + let mut crates_currently_indexing = + FxIndexMap::with_capacity_and_hasher(num_worker_threads as _, Default::default()); + + while crates_done < crates_total { + db.unwind_if_cancelled(); + + for crate_id in &mut crates_to_prime { + work_sender + .send(( + crate_id, + graph[crate_id].display_name.as_deref().unwrap_or_default().to_string(), + )) + .ok(); + } + + // recv_timeout is somewhat a hack, we need a way to from this thread check to see if the current salsa revision + // is cancelled on a regular basis. workers will only exit if they are processing a task that is cancelled, or + // if this thread exits, and closes the work channel. + let worker_progress = match progress_receiver.recv_timeout(Duration::from_millis(10)) { + Ok(p) => p, + Err(crossbeam_channel::RecvTimeoutError::Timeout) => { + continue; + } + Err(crossbeam_channel::RecvTimeoutError::Disconnected) => { + // our workers may have died from a cancelled task, so we'll check and re-raise here. + db.unwind_if_cancelled(); + break; + } + }; + match worker_progress { + ParallelPrimeCacheWorkerProgress::BeginCrate { crate_id, crate_name } => { + crates_currently_indexing.insert(crate_id, crate_name); + } + ParallelPrimeCacheWorkerProgress::EndCrate { crate_id } => { + crates_currently_indexing.remove(&crate_id); + crates_to_prime.mark_done(crate_id); + crates_done += 1; + } + }; + + let progress = ParallelPrimeCachesProgress { + crates_currently_indexing: crates_currently_indexing.values().cloned().collect(), + crates_done, + crates_total, + }; + + cb(progress); + } +} + +fn compute_crates_to_prime(db: &RootDatabase, graph: &CrateGraph) -> FxHashSet<CrateId> { + // We're only interested in the workspace crates and the `ImportMap`s of their direct + // dependencies, though in practice the latter also compute the `DefMap`s. + // We don't prime transitive dependencies because they're generally not visible in + // the current workspace. + graph + .iter() + .filter(|&id| { + let file_id = graph[id].root_file_id; + let root_id = db.file_source_root(file_id); + !db.source_root(root_id).is_library + }) + .flat_map(|id| graph[id].dependencies.iter().map(|krate| krate.crate_id)) + .collect() +} diff --git a/src/tools/rust-analyzer/crates/ide/src/prime_caches/topologic_sort.rs b/src/tools/rust-analyzer/crates/ide/src/prime_caches/topologic_sort.rs new file mode 100644 index 000000000..9c3ceedbb --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/prime_caches/topologic_sort.rs @@ -0,0 +1,98 @@ +//! helper data structure to schedule work for parallel prime caches. +use std::{collections::VecDeque, hash::Hash}; + +use ide_db::FxHashMap; + +pub(crate) struct TopologicSortIterBuilder<T> { + nodes: FxHashMap<T, Entry<T>>, +} + +impl<T> TopologicSortIterBuilder<T> +where + T: Copy + Eq + PartialEq + Hash, +{ + fn new() -> Self { + Self { nodes: Default::default() } + } + + fn get_or_create_entry(&mut self, item: T) -> &mut Entry<T> { + self.nodes.entry(item).or_default() + } + + pub(crate) fn add(&mut self, item: T, predecessors: impl IntoIterator<Item = T>) { + let mut num_predecessors = 0; + + for predecessor in predecessors.into_iter() { + self.get_or_create_entry(predecessor).successors.push(item); + num_predecessors += 1; + } + + let entry = self.get_or_create_entry(item); + entry.num_predecessors += num_predecessors; + } + + pub(crate) fn build(self) -> TopologicalSortIter<T> { + let ready = self + .nodes + .iter() + .filter_map( + |(item, entry)| if entry.num_predecessors == 0 { Some(*item) } else { None }, + ) + .collect(); + + TopologicalSortIter { nodes: self.nodes, ready } + } +} + +pub(crate) struct TopologicalSortIter<T> { + ready: VecDeque<T>, + nodes: FxHashMap<T, Entry<T>>, +} + +impl<T> TopologicalSortIter<T> +where + T: Copy + Eq + PartialEq + Hash, +{ + pub(crate) fn builder() -> TopologicSortIterBuilder<T> { + TopologicSortIterBuilder::new() + } + + pub(crate) fn pending(&self) -> usize { + self.nodes.len() + } + + pub(crate) fn mark_done(&mut self, item: T) { + let entry = self.nodes.remove(&item).expect("invariant: unknown item marked as done"); + + for successor in entry.successors { + let succ_entry = self + .nodes + .get_mut(&successor) + .expect("invariant: unknown successor referenced by entry"); + + succ_entry.num_predecessors -= 1; + if succ_entry.num_predecessors == 0 { + self.ready.push_back(successor); + } + } + } +} + +impl<T> Iterator for TopologicalSortIter<T> { + type Item = T; + + fn next(&mut self) -> Option<Self::Item> { + self.ready.pop_front() + } +} + +struct Entry<T> { + successors: Vec<T>, + num_predecessors: usize, +} + +impl<T> Default for Entry<T> { + fn default() -> Self { + Self { successors: Default::default(), num_predecessors: 0 } + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/references.rs b/src/tools/rust-analyzer/crates/ide/src/references.rs new file mode 100644 index 000000000..1a6beec18 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/references.rs @@ -0,0 +1,1636 @@ +//! This module implements a reference search. +//! First, the element at the cursor position must be either an `ast::Name` +//! or `ast::NameRef`. If it's an `ast::NameRef`, at the classification step we +//! try to resolve the direct tree parent of this element, otherwise we +//! already have a definition and just need to get its HIR together with +//! some information that is needed for further steps of searching. +//! After that, we collect files that might contain references and look +//! 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. + +use hir::{PathResolution, Semantics}; +use ide_db::{ + base_db::FileId, + defs::{Definition, NameClass, NameRefClass}, + search::{ReferenceCategory, SearchScope, UsageSearchResult}, + FxHashMap, RootDatabase, +}; +use syntax::{ + algo::find_node_at_offset, + ast::{self, HasName}, + match_ast, AstNode, + SyntaxKind::*, + SyntaxNode, TextRange, TextSize, T, +}; + +use crate::{FilePosition, NavigationTarget, TryToNav}; + +#[derive(Debug, Clone)] +pub struct ReferenceSearchResult { + pub declaration: Option<Declaration>, + pub references: FxHashMap<FileId, Vec<(TextRange, Option<ReferenceCategory>)>>, +} + +#[derive(Debug, Clone)] +pub struct Declaration { + pub nav: NavigationTarget, + pub is_mut: bool, +} + +// Feature: Find All References +// +// Shows all references of the item at the cursor location +// +// |=== +// | Editor | Shortcut +// +// | VS Code | kbd:[Shift+Alt+F12] +// |=== +// +// image::https://user-images.githubusercontent.com/48062697/113020670-b7c34f00-917a-11eb-8003-370ac5f2b3cb.gif[] +pub(crate) fn find_all_refs( + sema: &Semantics<'_, RootDatabase>, + position: FilePosition, + search_scope: Option<SearchScope>, +) -> Option<Vec<ReferenceSearchResult>> { + let _p = profile::span("find_all_refs"); + let syntax = sema.parse(position.file_id).syntax().clone(); + let make_searcher = |literal_search: bool| { + move |def: Definition| { + let declaration = match def { + Definition::Module(module) => { + Some(NavigationTarget::from_module_to_decl(sema.db, module)) + } + def => def.try_to_nav(sema.db), + } + .map(|nav| { + let decl_range = nav.focus_or_full_range(); + Declaration { + is_mut: decl_mutability(&def, sema.parse(nav.file_id).syntax(), decl_range), + nav, + } + }); + let mut usages = + def.usages(sema).set_scope(search_scope.clone()).include_self_refs().all(); + + if literal_search { + retain_adt_literal_usages(&mut usages, def, sema); + } + + let references = usages + .into_iter() + .map(|(file_id, refs)| { + ( + file_id, + refs.into_iter() + .map(|file_ref| (file_ref.range, file_ref.category)) + .collect(), + ) + }) + .collect(); + + ReferenceSearchResult { declaration, references } + } + }; + + match name_for_constructor_search(&syntax, position) { + Some(name) => { + let def = match NameClass::classify(sema, &name)? { + NameClass::Definition(it) | NameClass::ConstReference(it) => it, + NameClass::PatFieldShorthand { local_def: _, field_ref } => { + Definition::Field(field_ref) + } + }; + Some(vec![make_searcher(true)(def)]) + } + None => { + let search = make_searcher(false); + Some(find_defs(sema, &syntax, position.offset)?.map(search).collect()) + } + } +} + +pub(crate) fn find_defs<'a>( + sema: &'a Semantics<'_, RootDatabase>, + syntax: &SyntaxNode, + offset: TextSize, +) -> Option<impl Iterator<Item = Definition> + 'a> { + let token = syntax.token_at_offset(offset).find(|t| { + matches!( + t.kind(), + IDENT | INT_NUMBER | LIFETIME_IDENT | T![self] | T![super] | T![crate] | T![Self] + ) + }); + token.map(|token| { + sema.descend_into_macros_with_same_text(token) + .into_iter() + .filter_map(|it| ast::NameLike::cast(it.parent()?)) + .filter_map(move |name_like| { + let def = match name_like { + ast::NameLike::NameRef(name_ref) => { + match NameRefClass::classify(sema, &name_ref)? { + NameRefClass::Definition(def) => def, + NameRefClass::FieldShorthand { local_ref, field_ref: _ } => { + Definition::Local(local_ref) + } + } + } + ast::NameLike::Name(name) => match NameClass::classify(sema, &name)? { + NameClass::Definition(it) | NameClass::ConstReference(it) => it, + NameClass::PatFieldShorthand { local_def, field_ref: _ } => { + Definition::Local(local_def) + } + }, + ast::NameLike::Lifetime(lifetime) => { + NameRefClass::classify_lifetime(sema, &lifetime) + .and_then(|class| match class { + NameRefClass::Definition(it) => Some(it), + _ => None, + }) + .or_else(|| { + NameClass::classify_lifetime(sema, &lifetime) + .and_then(NameClass::defined) + })? + } + }; + Some(def) + }) + }) +} + +pub(crate) fn decl_mutability(def: &Definition, syntax: &SyntaxNode, range: TextRange) -> bool { + match def { + Definition::Local(_) | Definition::Field(_) => {} + _ => return false, + }; + + match find_node_at_offset::<ast::LetStmt>(syntax, range.start()) { + Some(stmt) if stmt.initializer().is_some() => match stmt.pat() { + Some(ast::Pat::IdentPat(it)) => it.mut_token().is_some(), + _ => false, + }, + _ => false, + } +} + +/// Filter out all non-literal usages for adt-defs +fn retain_adt_literal_usages( + usages: &mut UsageSearchResult, + def: Definition, + sema: &Semantics<'_, RootDatabase>, +) { + let refs = usages.references.values_mut(); + match def { + Definition::Adt(hir::Adt::Enum(enum_)) => { + refs.for_each(|it| { + it.retain(|reference| { + reference + .name + .as_name_ref() + .map_or(false, |name_ref| is_enum_lit_name_ref(sema, enum_, name_ref)) + }) + }); + usages.references.retain(|_, it| !it.is_empty()); + } + Definition::Adt(_) | Definition::Variant(_) => { + refs.for_each(|it| { + it.retain(|reference| reference.name.as_name_ref().map_or(false, is_lit_name_ref)) + }); + usages.references.retain(|_, it| !it.is_empty()); + } + _ => {} + } +} + +/// Returns `Some` if the cursor is at a position for an item to search for all its constructor/literal usages +fn name_for_constructor_search(syntax: &SyntaxNode, position: FilePosition) -> Option<ast::Name> { + let token = syntax.token_at_offset(position.offset).right_biased()?; + let token_parent = token.parent()?; + let kind = token.kind(); + if kind == T![;] { + ast::Struct::cast(token_parent) + .filter(|struct_| struct_.field_list().is_none()) + .and_then(|struct_| struct_.name()) + } else if kind == T!['{'] { + match_ast! { + match token_parent { + ast::RecordFieldList(rfl) => match_ast! { + match (rfl.syntax().parent()?) { + ast::Variant(it) => it.name(), + ast::Struct(it) => it.name(), + ast::Union(it) => it.name(), + _ => None, + } + }, + ast::VariantList(vl) => ast::Enum::cast(vl.syntax().parent()?)?.name(), + _ => None, + } + } + } else if kind == T!['('] { + let tfl = ast::TupleFieldList::cast(token_parent)?; + match_ast! { + match (tfl.syntax().parent()?) { + ast::Variant(it) => it.name(), + ast::Struct(it) => it.name(), + _ => None, + } + } + } else { + None + } +} + +fn is_enum_lit_name_ref( + sema: &Semantics<'_, RootDatabase>, + enum_: hir::Enum, + name_ref: &ast::NameRef, +) -> bool { + let path_is_variant_of_enum = |path: ast::Path| { + matches!( + sema.resolve_path(&path), + Some(PathResolution::Def(hir::ModuleDef::Variant(variant))) + if variant.parent_enum(sema.db) == enum_ + ) + }; + name_ref + .syntax() + .ancestors() + .find_map(|ancestor| { + match_ast! { + match ancestor { + ast::PathExpr(path_expr) => path_expr.path().map(path_is_variant_of_enum), + ast::RecordExpr(record_expr) => record_expr.path().map(path_is_variant_of_enum), + _ => None, + } + } + }) + .unwrap_or(false) +} + +fn path_ends_with(path: Option<ast::Path>, name_ref: &ast::NameRef) -> bool { + path.and_then(|path| path.segment()) + .and_then(|segment| segment.name_ref()) + .map_or(false, |segment| segment == *name_ref) +} + +fn is_lit_name_ref(name_ref: &ast::NameRef) -> bool { + name_ref.syntax().ancestors().find_map(|ancestor| { + match_ast! { + match ancestor { + ast::PathExpr(path_expr) => Some(path_ends_with(path_expr.path(), name_ref)), + ast::RecordExpr(record_expr) => Some(path_ends_with(record_expr.path(), name_ref)), + _ => None, + } + } + }).unwrap_or(false) +} + +#[cfg(test)] +mod tests { + use expect_test::{expect, Expect}; + use ide_db::{base_db::FileId, search::ReferenceCategory}; + use stdx::format_to; + + use crate::{fixture, SearchScope}; + + #[test] + fn test_struct_literal_after_space() { + check( + r#" +struct Foo $0{ + a: i32, +} +impl Foo { + fn f() -> i32 { 42 } +} +fn main() { + let f: Foo; + f = Foo {a: Foo::f()}; +} +"#, + expect![[r#" + Foo Struct FileId(0) 0..26 7..10 + + FileId(0) 101..104 + "#]], + ); + } + + #[test] + fn test_struct_literal_before_space() { + check( + r#" +struct Foo$0 {} + fn main() { + let f: Foo; + f = Foo {}; +} +"#, + expect![[r#" + Foo Struct FileId(0) 0..13 7..10 + + FileId(0) 41..44 + FileId(0) 54..57 + "#]], + ); + } + + #[test] + fn test_struct_literal_with_generic_type() { + check( + r#" +struct Foo<T> $0{} + fn main() { + let f: Foo::<i32>; + f = Foo {}; +} +"#, + expect![[r#" + Foo Struct FileId(0) 0..16 7..10 + + FileId(0) 64..67 + "#]], + ); + } + + #[test] + fn test_struct_literal_for_tuple() { + check( + r#" +struct Foo$0(i32); + +fn main() { + let f: Foo; + f = Foo(1); +} +"#, + expect![[r#" + Foo Struct FileId(0) 0..16 7..10 + + FileId(0) 54..57 + "#]], + ); + } + + #[test] + fn test_struct_literal_for_union() { + check( + r#" +union Foo $0{ + x: u32 +} + +fn main() { + let f: Foo; + f = Foo { x: 1 }; +} +"#, + expect![[r#" + Foo Union FileId(0) 0..24 6..9 + + FileId(0) 62..65 + "#]], + ); + } + + #[test] + fn test_enum_after_space() { + check( + r#" +enum Foo $0{ + A, + B(), + C{}, +} +fn main() { + let f: Foo; + f = Foo::A; + f = Foo::B(); + f = Foo::C{}; +} +"#, + expect![[r#" + Foo Enum FileId(0) 0..37 5..8 + + FileId(0) 74..77 + FileId(0) 90..93 + FileId(0) 108..111 + "#]], + ); + } + + #[test] + fn test_variant_record_after_space() { + check( + r#" +enum Foo { + A $0{ n: i32 }, + B, +} +fn main() { + let f: Foo; + f = Foo::B; + f = Foo::A { n: 92 }; +} +"#, + expect![[r#" + A Variant FileId(0) 15..27 15..16 + + FileId(0) 95..96 + "#]], + ); + } + #[test] + fn test_variant_tuple_before_paren() { + check( + r#" +enum Foo { + A$0(i32), + B, +} +fn main() { + let f: Foo; + f = Foo::B; + f = Foo::A(92); +} +"#, + expect![[r#" + A Variant FileId(0) 15..21 15..16 + + FileId(0) 89..90 + "#]], + ); + } + + #[test] + fn test_enum_before_space() { + check( + r#" +enum Foo$0 { + A, + B, +} +fn main() { + let f: Foo; + f = Foo::A; +} +"#, + expect![[r#" + Foo Enum FileId(0) 0..26 5..8 + + FileId(0) 50..53 + FileId(0) 63..66 + "#]], + ); + } + + #[test] + fn test_enum_with_generic_type() { + check( + r#" +enum Foo<T> $0{ + A(T), + B, +} +fn main() { + let f: Foo<i8>; + f = Foo::A(1); +} +"#, + expect![[r#" + Foo Enum FileId(0) 0..32 5..8 + + FileId(0) 73..76 + "#]], + ); + } + + #[test] + fn test_enum_for_tuple() { + check( + r#" +enum Foo$0{ + A(i8), + B(i8), +} +fn main() { + let f: Foo; + f = Foo::A(1); +} +"#, + expect![[r#" + Foo Enum FileId(0) 0..33 5..8 + + FileId(0) 70..73 + "#]], + ); + } + + #[test] + fn test_find_all_refs_for_local() { + check( + r#" +fn main() { + let mut i = 1; + let j = 1; + i = i$0 + j; + + { + i = 0; + } + + i = 5; +}"#, + expect![[r#" + i Local FileId(0) 20..25 24..25 Write + + FileId(0) 50..51 Write + FileId(0) 54..55 Read + FileId(0) 76..77 Write + FileId(0) 94..95 Write + "#]], + ); + } + + #[test] + fn search_filters_by_range() { + check( + r#" +fn foo() { + let spam$0 = 92; + spam + spam +} +fn bar() { + let spam = 92; + spam + spam +} +"#, + expect![[r#" + spam Local FileId(0) 19..23 19..23 + + FileId(0) 34..38 Read + FileId(0) 41..45 Read + "#]], + ); + } + + #[test] + fn test_find_all_refs_for_param_inside() { + check( + r#" +fn foo(i : u32) -> u32 { i$0 } +"#, + expect![[r#" + i ValueParam FileId(0) 7..8 7..8 + + FileId(0) 25..26 Read + "#]], + ); + } + + #[test] + fn test_find_all_refs_for_fn_param() { + check( + r#" +fn foo(i$0 : u32) -> u32 { i } +"#, + expect![[r#" + i ValueParam FileId(0) 7..8 7..8 + + FileId(0) 25..26 Read + "#]], + ); + } + + #[test] + fn test_find_all_refs_field_name() { + check( + r#" +//- /lib.rs +struct Foo { + pub spam$0: u32, +} + +fn main(s: Foo) { + let f = s.spam; +} +"#, + expect![[r#" + spam Field FileId(0) 17..30 21..25 + + FileId(0) 67..71 Read + "#]], + ); + } + + #[test] + fn test_find_all_refs_impl_item_name() { + check( + r#" +struct Foo; +impl Foo { + fn f$0(&self) { } +} +"#, + expect![[r#" + f Function FileId(0) 27..43 30..31 + + (no references) + "#]], + ); + } + + #[test] + fn test_find_all_refs_enum_var_name() { + check( + r#" +enum Foo { + A, + B$0, + C, +} +"#, + expect![[r#" + B Variant FileId(0) 22..23 22..23 + + (no references) + "#]], + ); + } + + #[test] + fn test_find_all_refs_enum_var_field() { + check( + r#" +enum Foo { + A, + B { field$0: u8 }, + C, +} +"#, + expect![[r#" + field Field FileId(0) 26..35 26..31 + + (no references) + "#]], + ); + } + + #[test] + fn test_find_all_refs_two_modules() { + check( + r#" +//- /lib.rs +pub mod foo; +pub mod bar; + +fn f() { + let i = foo::Foo { n: 5 }; +} + +//- /foo.rs +use crate::bar; + +pub struct Foo { + pub n: u32, +} + +fn f() { + let i = bar::Bar { n: 5 }; +} + +//- /bar.rs +use crate::foo; + +pub struct Bar { + pub n: u32, +} + +fn f() { + let i = foo::Foo$0 { n: 5 }; +} +"#, + expect![[r#" + Foo Struct FileId(1) 17..51 28..31 + + FileId(0) 53..56 + FileId(2) 79..82 + "#]], + ); + } + + #[test] + fn test_find_all_refs_decl_module() { + check( + r#" +//- /lib.rs +mod foo$0; + +use foo::Foo; + +fn f() { + let i = Foo { n: 5 }; +} + +//- /foo.rs +pub struct Foo { + pub n: u32, +} +"#, + expect![[r#" + foo Module FileId(0) 0..8 4..7 + + FileId(0) 14..17 + "#]], + ); + } + + #[test] + fn test_find_all_refs_decl_module_on_self() { + check( + r#" +//- /lib.rs +mod foo; + +//- /foo.rs +use self$0; +"#, + expect![[r#" + foo Module FileId(0) 0..8 4..7 + + FileId(1) 4..8 + "#]], + ); + } + + #[test] + fn test_find_all_refs_decl_module_on_self_crate_root() { + check( + r#" +//- /lib.rs +use self$0; +"#, + expect![[r#" + Module FileId(0) 0..10 + + FileId(0) 4..8 + "#]], + ); + } + + #[test] + fn test_find_all_refs_super_mod_vis() { + check( + r#" +//- /lib.rs +mod foo; + +//- /foo.rs +mod some; +use some::Foo; + +fn f() { + let i = Foo { n: 5 }; +} + +//- /foo/some.rs +pub(super) struct Foo$0 { + pub n: u32, +} +"#, + expect![[r#" + Foo Struct FileId(2) 0..41 18..21 + + FileId(1) 20..23 + FileId(1) 47..50 + "#]], + ); + } + + #[test] + fn test_find_all_refs_with_scope() { + let code = r#" + //- /lib.rs + mod foo; + mod bar; + + pub fn quux$0() {} + + //- /foo.rs + fn f() { super::quux(); } + + //- /bar.rs + fn f() { super::quux(); } + "#; + + check_with_scope( + code, + None, + expect![[r#" + quux Function FileId(0) 19..35 26..30 + + FileId(1) 16..20 + FileId(2) 16..20 + "#]], + ); + + check_with_scope( + code, + Some(SearchScope::single_file(FileId(2))), + expect![[r#" + quux Function FileId(0) 19..35 26..30 + + FileId(2) 16..20 + "#]], + ); + } + + #[test] + fn test_find_all_refs_macro_def() { + check( + r#" +#[macro_export] +macro_rules! m1$0 { () => (()) } + +fn foo() { + m1(); + m1(); +} +"#, + expect![[r#" + m1 Macro FileId(0) 0..46 29..31 + + FileId(0) 63..65 + FileId(0) 73..75 + "#]], + ); + } + + #[test] + fn test_basic_highlight_read_write() { + check( + r#" +fn foo() { + let mut i$0 = 0; + i = i + 1; +} +"#, + expect![[r#" + i Local FileId(0) 19..24 23..24 Write + + FileId(0) 34..35 Write + FileId(0) 38..39 Read + "#]], + ); + } + + #[test] + fn test_basic_highlight_field_read_write() { + check( + r#" +struct S { + f: u32, +} + +fn foo() { + let mut s = S{f: 0}; + s.f$0 = 0; +} +"#, + expect![[r#" + f Field FileId(0) 15..21 15..16 + + FileId(0) 55..56 Read + FileId(0) 68..69 Write + "#]], + ); + } + + #[test] + fn test_basic_highlight_decl_no_write() { + check( + r#" +fn foo() { + let i$0; + i = 1; +} +"#, + expect![[r#" + i Local FileId(0) 19..20 19..20 + + FileId(0) 26..27 Write + "#]], + ); + } + + #[test] + fn test_find_struct_function_refs_outside_module() { + check( + r#" +mod foo { + pub struct Foo; + + impl Foo { + pub fn new$0() -> Foo { Foo } + } +} + +fn main() { + let _f = foo::Foo::new(); +} +"#, + expect![[r#" + new Function FileId(0) 54..81 61..64 + + FileId(0) 126..129 + "#]], + ); + } + + #[test] + fn test_find_all_refs_nested_module() { + check( + r#" +//- /lib.rs +mod foo { mod bar; } + +fn f$0() {} + +//- /foo/bar.rs +use crate::f; + +fn g() { f(); } +"#, + expect![[r#" + f Function FileId(0) 22..31 25..26 + + FileId(1) 11..12 + FileId(1) 24..25 + "#]], + ); + } + + #[test] + fn test_find_all_refs_struct_pat() { + check( + r#" +struct S { + field$0: u8, +} + +fn f(s: S) { + match s { + S { field } => {} + } +} +"#, + expect![[r#" + field Field FileId(0) 15..24 15..20 + + FileId(0) 68..73 Read + "#]], + ); + } + + #[test] + fn test_find_all_refs_enum_var_pat() { + check( + r#" +enum En { + Variant { + field$0: u8, + } +} + +fn f(e: En) { + match e { + En::Variant { field } => {} + } +} +"#, + expect![[r#" + field Field FileId(0) 32..41 32..37 + + FileId(0) 102..107 Read + "#]], + ); + } + + #[test] + fn test_find_all_refs_enum_var_privacy() { + check( + r#" +mod m { + pub enum En { + Variant { + field$0: u8, + } + } +} + +fn f() -> m::En { + m::En::Variant { field: 0 } +} +"#, + expect![[r#" + field Field FileId(0) 56..65 56..61 + + FileId(0) 125..130 Read + "#]], + ); + } + + #[test] + fn test_find_self_refs() { + check( + r#" +struct Foo { bar: i32 } + +impl Foo { + fn foo(self) { + let x = self$0.bar; + if true { + let _ = match () { + () => self, + }; + } + } +} +"#, + expect![[r#" + self SelfParam FileId(0) 47..51 47..51 + + FileId(0) 71..75 Read + FileId(0) 152..156 Read + "#]], + ); + } + + #[test] + fn test_find_self_refs_decl() { + check( + r#" +struct Foo { bar: i32 } + +impl Foo { + fn foo(self$0) { + self; + } +} +"#, + expect![[r#" + self SelfParam FileId(0) 47..51 47..51 + + FileId(0) 63..67 Read + "#]], + ); + } + + fn check(ra_fixture: &str, expect: Expect) { + check_with_scope(ra_fixture, None, expect) + } + + fn check_with_scope(ra_fixture: &str, search_scope: Option<SearchScope>, expect: Expect) { + let (analysis, pos) = fixture::position(ra_fixture); + let refs = analysis.find_all_refs(pos, search_scope).unwrap().unwrap(); + + let mut actual = String::new(); + for refs in refs { + actual += "\n\n"; + + if let Some(decl) = refs.declaration { + format_to!(actual, "{}", decl.nav.debug_render()); + if decl.is_mut { + format_to!(actual, " {:?}", ReferenceCategory::Write) + } + actual += "\n\n"; + } + + for (file_id, references) in &refs.references { + for (range, access) in references { + format_to!(actual, "{:?} {:?}", file_id, range); + if let Some(access) = access { + format_to!(actual, " {:?}", access); + } + actual += "\n"; + } + } + + if refs.references.is_empty() { + actual += "(no references)\n"; + } + } + expect.assert_eq(actual.trim_start()) + } + + #[test] + fn test_find_lifetimes_function() { + check( + r#" +trait Foo<'a> {} +impl<'a> Foo<'a> for &'a () {} +fn foo<'a, 'b: 'a>(x: &'a$0 ()) -> &'a () where &'a (): Foo<'a> { + fn bar<'a>(_: &'a ()) {} + x +} +"#, + expect![[r#" + 'a LifetimeParam FileId(0) 55..57 55..57 + + FileId(0) 63..65 + FileId(0) 71..73 + FileId(0) 82..84 + FileId(0) 95..97 + FileId(0) 106..108 + "#]], + ); + } + + #[test] + fn test_find_lifetimes_type_alias() { + check( + r#" +type Foo<'a, T> where T: 'a$0 = &'a T; +"#, + expect![[r#" + 'a LifetimeParam FileId(0) 9..11 9..11 + + FileId(0) 25..27 + FileId(0) 31..33 + "#]], + ); + } + + #[test] + fn test_find_lifetimes_trait_impl() { + check( + r#" +trait Foo<'a> { + fn foo() -> &'a (); +} +impl<'a> Foo<'a> for &'a () { + fn foo() -> &'a$0 () { + unimplemented!() + } +} +"#, + expect![[r#" + 'a LifetimeParam FileId(0) 47..49 47..49 + + FileId(0) 55..57 + FileId(0) 64..66 + FileId(0) 89..91 + "#]], + ); + } + + #[test] + fn test_map_range_to_original() { + check( + r#" +macro_rules! foo {($i:ident) => {$i} } +fn main() { + let a$0 = "test"; + foo!(a); +} +"#, + expect![[r#" + a Local FileId(0) 59..60 59..60 + + FileId(0) 80..81 Read + "#]], + ); + } + + #[test] + fn test_map_range_to_original_ref() { + check( + r#" +macro_rules! foo {($i:ident) => {$i} } +fn main() { + let a = "test"; + foo!(a$0); +} +"#, + expect![[r#" + a Local FileId(0) 59..60 59..60 + + FileId(0) 80..81 Read + "#]], + ); + } + + #[test] + fn test_find_labels() { + check( + r#" +fn foo<'a>() -> &'a () { + 'a: loop { + 'b: loop { + continue 'a$0; + } + break 'a; + } +} +"#, + expect![[r#" + 'a Label FileId(0) 29..32 29..31 + + FileId(0) 80..82 + FileId(0) 108..110 + "#]], + ); + } + + #[test] + fn test_find_const_param() { + check( + r#" +fn foo<const FOO$0: usize>() -> usize { + FOO +} +"#, + expect![[r#" + FOO ConstParam FileId(0) 7..23 13..16 + + FileId(0) 42..45 + "#]], + ); + } + + #[test] + fn test_trait() { + check( + r#" +trait Foo$0 where Self: {} + +impl Foo for () {} +"#, + expect![[r#" + Foo Trait FileId(0) 0..24 6..9 + + FileId(0) 31..34 + "#]], + ); + } + + #[test] + fn test_trait_self() { + check( + r#" +trait Foo where Self$0 { + fn f() -> Self; +} + +impl Foo for () {} +"#, + expect![[r#" + Self TypeParam FileId(0) 6..9 6..9 + + FileId(0) 16..20 + FileId(0) 37..41 + "#]], + ); + } + + #[test] + fn test_self_ty() { + check( + r#" + struct $0Foo; + + impl Foo where Self: { + fn f() -> Self; + } + "#, + expect![[r#" + Foo Struct FileId(0) 0..11 7..10 + + FileId(0) 18..21 + FileId(0) 28..32 + FileId(0) 50..54 + "#]], + ); + check( + r#" +struct Foo; + +impl Foo where Self: { + fn f() -> Self$0; +} +"#, + expect![[r#" + impl Impl FileId(0) 13..57 18..21 + + FileId(0) 18..21 + FileId(0) 28..32 + FileId(0) 50..54 + "#]], + ); + } + #[test] + fn test_self_variant_with_payload() { + check( + r#" +enum Foo { Bar() } + +impl Foo { + fn foo(self) { + match self { + Self::Bar$0() => (), + } + } +} + +"#, + expect![[r#" + Bar Variant FileId(0) 11..16 11..14 + + FileId(0) 89..92 + "#]], + ); + } + + #[test] + fn test_attr_differs_from_fn_with_same_name() { + check( + r#" +#[test] +fn test$0() { + test(); +} +"#, + expect![[r#" + test Function FileId(0) 0..33 11..15 + + FileId(0) 24..28 + "#]], + ); + } + + #[test] + fn test_const_in_pattern() { + check( + r#" +const A$0: i32 = 42; + +fn main() { + match A { + A => (), + _ => (), + } + if let A = A {} +} +"#, + expect![[r#" + A Const FileId(0) 0..18 6..7 + + FileId(0) 42..43 + FileId(0) 54..55 + FileId(0) 97..98 + FileId(0) 101..102 + "#]], + ); + } + + #[test] + fn test_primitives() { + check( + r#" +fn foo(_: bool) -> bo$0ol { true } +"#, + expect![[r#" + FileId(0) 10..14 + FileId(0) 19..23 + "#]], + ); + } + + #[test] + fn test_transitive() { + check( + r#" +//- /level3.rs new_source_root:local crate:level3 +pub struct Fo$0o; +//- /level2.rs new_source_root:local crate:level2 deps:level3 +pub use level3::Foo; +//- /level1.rs new_source_root:local crate:level1 deps:level2 +pub use level2::Foo; +//- /level0.rs new_source_root:local crate:level0 deps:level1 +pub use level1::Foo; +"#, + expect![[r#" + Foo Struct FileId(0) 0..15 11..14 + + FileId(1) 16..19 + FileId(2) 16..19 + FileId(3) 16..19 + "#]], + ); + } + + #[test] + fn test_decl_macro_references() { + check( + r#" +//- /lib.rs crate:lib +#[macro_use] +mod qux; +mod bar; + +pub use self::foo; +//- /qux.rs +#[macro_export] +macro_rules! foo$0 { + () => {struct Foo;}; +} +//- /bar.rs +foo!(); +//- /other.rs crate:other deps:lib new_source_root:local +lib::foo!(); +"#, + expect![[r#" + foo Macro FileId(1) 0..61 29..32 + + FileId(0) 46..49 + FileId(2) 0..3 + FileId(3) 5..8 + "#]], + ); + } + + #[test] + fn macro_doesnt_reference_attribute_on_call() { + check( + r#" +macro_rules! m { + () => {}; +} + +#[proc_macro_test::attr_noop] +m$0!(); + +"#, + expect![[r#" + m Macro FileId(0) 0..32 13..14 + + FileId(0) 64..65 + "#]], + ); + } + + #[test] + fn multi_def() { + check( + r#" +macro_rules! m { + ($name:ident) => { + mod module { + pub fn $name() {} + } + + pub fn $name() {} + } +} + +m!(func$0); + +fn f() { + func(); + module::func(); +} + "#, + expect![[r#" + func Function FileId(0) 137..146 140..144 + + FileId(0) 161..165 + + + func Function FileId(0) 137..146 140..144 + + FileId(0) 181..185 + "#]], + ) + } + + #[test] + fn attr_expanded() { + check( + r#" +//- proc_macros: identity +#[proc_macros::identity] +fn func$0() { + func(); +} +"#, + expect![[r#" + func Function FileId(0) 25..50 28..32 + + FileId(0) 41..45 + "#]], + ) + } + + #[test] + fn attr_assoc_item() { + check( + r#" +//- proc_macros: identity + +trait Trait { + #[proc_macros::identity] + fn func() { + Self::func$0(); + } +} +"#, + expect![[r#" + func Function FileId(0) 48..87 51..55 + + FileId(0) 74..78 + "#]], + ) + } + + // FIXME: import is classified as function + #[test] + fn attr() { + check( + r#" +//- proc_macros: identity +use proc_macros::identity; + +#[proc_macros::$0identity] +fn func() {} +"#, + expect![[r#" + identity Attribute FileId(1) 1..107 32..40 + + FileId(0) 43..51 + "#]], + ); + check( + r#" +#![crate_type="proc-macro"] +#[proc_macro_attribute] +fn func$0() {} +"#, + expect![[r#" + func Attribute FileId(0) 28..64 55..59 + + (no references) + "#]], + ); + } + + // FIXME: import is classified as function + #[test] + fn proc_macro() { + check( + r#" +//- proc_macros: mirror +use proc_macros::mirror; + +mirror$0! {} +"#, + expect![[r#" + mirror Macro FileId(1) 1..77 22..28 + + FileId(0) 26..32 + "#]], + ) + } + + #[test] + fn derive() { + check( + r#" +//- proc_macros: derive_identity +//- minicore: derive +use proc_macros::DeriveIdentity; + +#[derive(proc_macros::DeriveIdentity$0)] +struct Foo; +"#, + expect![[r#" + derive_identity Derive FileId(2) 1..107 45..60 + + FileId(0) 17..31 + FileId(0) 56..70 + "#]], + ); + check( + r#" +#![crate_type="proc-macro"] +#[proc_macro_derive(Derive, attributes(x))] +pub fn deri$0ve(_stream: TokenStream) -> TokenStream {} +"#, + expect![[r#" + derive Derive FileId(0) 28..125 79..85 + + (no references) + "#]], + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/rename.rs b/src/tools/rust-analyzer/crates/ide/src/rename.rs new file mode 100644 index 000000000..fe44856dc --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/rename.rs @@ -0,0 +1,2252 @@ +//! Renaming functionality. +//! +//! This is mostly front-end for [`ide_db::rename`], but it also includes the +//! tests. This module also implements a couple of magic tricks, like renaming +//! `self` and to `self` (to switch between associated function and method). + +use hir::{AsAssocItem, InFile, Semantics}; +use ide_db::{ + base_db::FileId, + defs::{Definition, NameClass, NameRefClass}, + rename::{bail, format_err, source_edit_from_references, IdentifierKind}, + RootDatabase, +}; +use itertools::Itertools; +use stdx::{always, never}; +use syntax::{ast, AstNode, SyntaxNode}; + +use text_edit::TextEdit; + +use crate::{FilePosition, RangeInfo, SourceChange}; + +pub use ide_db::rename::RenameError; + +type RenameResult<T> = Result<T, RenameError>; + +/// Prepares a rename. The sole job of this function is to return the TextRange of the thing that is +/// being targeted for a rename. +pub(crate) fn prepare_rename( + db: &RootDatabase, + position: FilePosition, +) -> RenameResult<RangeInfo<()>> { + let sema = Semantics::new(db); + let source_file = sema.parse(position.file_id); + let syntax = source_file.syntax(); + + let res = find_definitions(&sema, syntax, position)? + .map(|(name_like, def)| { + // ensure all ranges are valid + + if def.range_for_rename(&sema).is_none() { + bail!("No references found at position") + } + let frange = sema.original_range(name_like.syntax()); + + always!( + frange.range.contains_inclusive(position.offset) + && frange.file_id == position.file_id + ); + Ok(frange.range) + }) + .reduce(|acc, cur| match (acc, cur) { + // ensure all ranges are the same + (Ok(acc_inner), Ok(cur_inner)) if acc_inner == cur_inner => Ok(acc_inner), + (Err(e), _) => Err(e), + _ => bail!("inconsistent text range"), + }); + + match res { + // ensure at least one definition was found + Some(res) => res.map(|range| RangeInfo::new(range, ())), + None => bail!("No references found at position"), + } +} + +// Feature: Rename +// +// Renames the item below the cursor and all of its references +// +// |=== +// | Editor | Shortcut +// +// | VS Code | kbd:[F2] +// |=== +// +// image::https://user-images.githubusercontent.com/48062697/113065582-055aae80-91b1-11eb-8ade-2b58e6d81883.gif[] +pub(crate) fn rename( + db: &RootDatabase, + position: FilePosition, + new_name: &str, +) -> RenameResult<SourceChange> { + let sema = Semantics::new(db); + let source_file = sema.parse(position.file_id); + let syntax = source_file.syntax(); + + let defs = find_definitions(&sema, syntax, position)?; + + let ops: RenameResult<Vec<SourceChange>> = defs + .map(|(_namelike, def)| { + if let Definition::Local(local) = def { + if let Some(self_param) = local.as_self_param(sema.db) { + cov_mark::hit!(rename_self_to_param); + return rename_self_to_param(&sema, local, self_param, new_name); + } + if new_name == "self" { + cov_mark::hit!(rename_to_self); + return rename_to_self(&sema, local); + } + } + def.rename(&sema, new_name) + }) + .collect(); + + ops?.into_iter() + .reduce(|acc, elem| acc.merge(elem)) + .ok_or_else(|| format_err!("No references found at position")) +} + +/// Called by the client when it is about to rename a file. +pub(crate) fn will_rename_file( + db: &RootDatabase, + file_id: FileId, + new_name_stem: &str, +) -> Option<SourceChange> { + let sema = Semantics::new(db); + let module = sema.to_module_def(file_id)?; + let def = Definition::Module(module); + let mut change = def.rename(&sema, new_name_stem).ok()?; + change.file_system_edits.clear(); + Some(change) +} + +fn find_definitions( + sema: &Semantics<'_, RootDatabase>, + syntax: &SyntaxNode, + position: FilePosition, +) -> RenameResult<impl Iterator<Item = (ast::NameLike, Definition)>> { + let symbols = sema + .find_nodes_at_offset_with_descend::<ast::NameLike>(syntax, position.offset) + .map(|name_like| { + let res = match &name_like { + // renaming aliases would rename the item being aliased as the HIR doesn't track aliases yet + ast::NameLike::Name(name) + if name + .syntax() + .parent() + .map_or(false, |it| ast::Rename::can_cast(it.kind())) => + { + bail!("Renaming aliases is currently unsupported") + } + ast::NameLike::Name(name) => NameClass::classify(sema, name) + .map(|class| match class { + NameClass::Definition(it) | NameClass::ConstReference(it) => it, + NameClass::PatFieldShorthand { local_def, field_ref: _ } => { + Definition::Local(local_def) + } + }) + .map(|def| (name_like.clone(), def)) + .ok_or_else(|| format_err!("No references found at position")), + ast::NameLike::NameRef(name_ref) => { + NameRefClass::classify(sema, name_ref) + .map(|class| match class { + NameRefClass::Definition(def) => def, + NameRefClass::FieldShorthand { local_ref, field_ref: _ } => { + Definition::Local(local_ref) + } + }) + .ok_or_else(|| format_err!("No references found at position")) + .and_then(|def| { + // if the name differs from the definitions name it has to be an alias + if def + .name(sema.db) + .map_or(false, |it| it.to_smol_str() != name_ref.text().as_str()) + { + Err(format_err!("Renaming aliases is currently unsupported")) + } else { + Ok((name_like.clone(), def)) + } + }) + } + ast::NameLike::Lifetime(lifetime) => { + NameRefClass::classify_lifetime(sema, lifetime) + .and_then(|class| match class { + NameRefClass::Definition(def) => Some(def), + _ => None, + }) + .or_else(|| { + NameClass::classify_lifetime(sema, lifetime).and_then(|it| match it { + NameClass::Definition(it) => Some(it), + _ => None, + }) + }) + .map(|def| (name_like, def)) + .ok_or_else(|| format_err!("No references found at position")) + } + }; + res + }); + + let res: RenameResult<Vec<_>> = symbols.collect(); + match res { + Ok(v) => { + if v.is_empty() { + // FIXME: some semantic duplication between "empty vec" and "Err()" + Err(format_err!("No references found at position")) + } else { + // remove duplicates, comparing `Definition`s + Ok(v.into_iter().unique_by(|t| t.1)) + } + } + Err(e) => Err(e), + } +} + +fn rename_to_self( + sema: &Semantics<'_, RootDatabase>, + local: hir::Local, +) -> RenameResult<SourceChange> { + if never!(local.is_self(sema.db)) { + bail!("rename_to_self invoked on self"); + } + + let fn_def = match local.parent(sema.db) { + hir::DefWithBody::Function(func) => func, + _ => bail!("Cannot rename local to self outside of function"), + }; + + if fn_def.self_param(sema.db).is_some() { + bail!("Method already has a self parameter"); + } + + let params = fn_def.assoc_fn_params(sema.db); + let first_param = params + .first() + .ok_or_else(|| format_err!("Cannot rename local to self unless it is a parameter"))?; + match first_param.as_local(sema.db) { + Some(plocal) => { + if plocal != local { + bail!("Only the first parameter may be renamed to self"); + } + } + None => bail!("rename_to_self invoked on destructuring parameter"), + } + + let assoc_item = fn_def + .as_assoc_item(sema.db) + .ok_or_else(|| format_err!("Cannot rename parameter to self for free function"))?; + let impl_ = match assoc_item.container(sema.db) { + hir::AssocItemContainer::Trait(_) => { + bail!("Cannot rename parameter to self for trait functions"); + } + hir::AssocItemContainer::Impl(impl_) => impl_, + }; + let first_param_ty = first_param.ty(); + let impl_ty = impl_.self_ty(sema.db); + let (ty, self_param) = if impl_ty.remove_ref().is_some() { + // if the impl is a ref to the type we can just match the `&T` with self directly + (first_param_ty.clone(), "self") + } else { + first_param_ty.remove_ref().map_or((first_param_ty.clone(), "self"), |ty| { + (ty, if first_param_ty.is_mutable_reference() { "&mut self" } else { "&self" }) + }) + }; + + if ty != impl_ty { + bail!("Parameter type differs from impl block type"); + } + + let InFile { file_id, value: param_source } = + first_param.source(sema.db).ok_or_else(|| format_err!("No source for parameter found"))?; + + let def = Definition::Local(local); + let usages = def.usages(sema).all(); + let mut source_change = SourceChange::default(); + source_change.extend(usages.iter().map(|(&file_id, references)| { + (file_id, source_edit_from_references(references, def, "self")) + })); + source_change.insert_source_edit( + file_id.original_file(sema.db), + TextEdit::replace(param_source.syntax().text_range(), String::from(self_param)), + ); + Ok(source_change) +} + +fn rename_self_to_param( + sema: &Semantics<'_, RootDatabase>, + local: hir::Local, + self_param: hir::SelfParam, + new_name: &str, +) -> RenameResult<SourceChange> { + if new_name == "self" { + // Let's do nothing rather than complain. + cov_mark::hit!(rename_self_to_self); + return Ok(SourceChange::default()); + } + + let identifier_kind = IdentifierKind::classify(new_name)?; + + let InFile { file_id, value: self_param } = + self_param.source(sema.db).ok_or_else(|| format_err!("cannot find function source"))?; + + let def = Definition::Local(local); + let usages = def.usages(sema).all(); + let edit = text_edit_from_self_param(&self_param, new_name) + .ok_or_else(|| format_err!("No target type found"))?; + if usages.len() > 1 && identifier_kind == IdentifierKind::Underscore { + bail!("Cannot rename reference to `_` as it is being referenced multiple times"); + } + let mut source_change = SourceChange::default(); + source_change.insert_source_edit(file_id.original_file(sema.db), edit); + source_change.extend(usages.iter().map(|(&file_id, references)| { + (file_id, source_edit_from_references(references, def, new_name)) + })); + Ok(source_change) +} + +fn text_edit_from_self_param(self_param: &ast::SelfParam, new_name: &str) -> Option<TextEdit> { + fn target_type_name(impl_def: &ast::Impl) -> Option<String> { + if let Some(ast::Type::PathType(p)) = impl_def.self_ty() { + return Some(p.path()?.segment()?.name_ref()?.text().to_string()); + } + None + } + + let impl_def = self_param.syntax().ancestors().find_map(ast::Impl::cast)?; + let type_name = target_type_name(&impl_def)?; + + let mut replacement_text = String::from(new_name); + replacement_text.push_str(": "); + match (self_param.amp_token(), self_param.mut_token()) { + (Some(_), None) => replacement_text.push('&'), + (Some(_), Some(_)) => replacement_text.push_str("&mut "), + (_, _) => (), + }; + replacement_text.push_str(type_name.as_str()); + + Some(TextEdit::replace(self_param.syntax().text_range(), replacement_text)) +} + +#[cfg(test)] +mod tests { + use expect_test::{expect, Expect}; + use stdx::trim_indent; + use test_utils::assert_eq_text; + use text_edit::TextEdit; + + use crate::{fixture, FileId}; + + use super::{RangeInfo, RenameError}; + + #[track_caller] + fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) { + let ra_fixture_after = &trim_indent(ra_fixture_after); + let (analysis, position) = fixture::position(ra_fixture_before); + let rename_result = analysis + .rename(position, new_name) + .unwrap_or_else(|err| panic!("Rename to '{}' was cancelled: {}", new_name, err)); + match rename_result { + Ok(source_change) => { + let mut text_edit_builder = TextEdit::builder(); + let mut file_id: Option<FileId> = None; + for edit in source_change.source_file_edits { + file_id = Some(edit.0); + for indel in edit.1.into_iter() { + text_edit_builder.replace(indel.delete, indel.insert); + } + } + if let Some(file_id) = file_id { + let mut result = analysis.file_text(file_id).unwrap().to_string(); + text_edit_builder.finish().apply(&mut result); + assert_eq_text!(ra_fixture_after, &*result); + } + } + Err(err) => { + if ra_fixture_after.starts_with("error:") { + let error_message = ra_fixture_after + .chars() + .into_iter() + .skip("error:".len()) + .collect::<String>(); + assert_eq!(error_message.trim(), err.to_string()); + } else { + panic!("Rename to '{}' failed unexpectedly: {}", new_name, err) + } + } + }; + } + + fn check_expect(new_name: &str, ra_fixture: &str, expect: Expect) { + let (analysis, position) = fixture::position(ra_fixture); + let source_change = + analysis.rename(position, new_name).unwrap().expect("Expect returned a RenameError"); + expect.assert_debug_eq(&source_change) + } + + fn check_expect_will_rename_file(new_name: &str, ra_fixture: &str, expect: Expect) { + let (analysis, position) = fixture::position(ra_fixture); + let source_change = analysis + .will_rename_file(position.file_id, new_name) + .unwrap() + .expect("Expect returned a RenameError"); + expect.assert_debug_eq(&source_change) + } + + fn check_prepare(ra_fixture: &str, expect: Expect) { + let (analysis, position) = fixture::position(ra_fixture); + let result = analysis + .prepare_rename(position) + .unwrap_or_else(|err| panic!("PrepareRename was cancelled: {}", err)); + match result { + Ok(RangeInfo { range, info: () }) => { + let source = analysis.file_text(position.file_id).unwrap(); + expect.assert_eq(&format!("{:?}: {}", range, &source[range])) + } + Err(RenameError(err)) => expect.assert_eq(&err), + }; + } + + #[test] + fn test_prepare_rename_namelikes() { + check_prepare(r"fn name$0<'lifetime>() {}", expect![[r#"3..7: name"#]]); + check_prepare(r"fn name<'lifetime$0>() {}", expect![[r#"8..17: 'lifetime"#]]); + check_prepare(r"fn name<'lifetime>() { name$0(); }", expect![[r#"23..27: name"#]]); + } + + #[test] + fn test_prepare_rename_in_macro() { + check_prepare( + r"macro_rules! foo { + ($ident:ident) => { + pub struct $ident; + } +} +foo!(Foo$0);", + expect![[r#"83..86: Foo"#]], + ); + } + + #[test] + fn test_prepare_rename_keyword() { + check_prepare(r"struct$0 Foo;", expect![[r#"No references found at position"#]]); + } + + #[test] + fn test_prepare_rename_tuple_field() { + check_prepare( + r#" +struct Foo(i32); + +fn baz() { + let mut x = Foo(4); + x.0$0 = 5; +} +"#, + expect![[r#"No references found at position"#]], + ); + } + + #[test] + fn test_prepare_rename_builtin() { + check_prepare( + r#" +fn foo() { + let x: i32$0 = 0; +} +"#, + expect![[r#"No references found at position"#]], + ); + } + + #[test] + fn test_prepare_rename_self() { + check_prepare( + r#" +struct Foo {} + +impl Foo { + fn foo(self) -> Self$0 { + self + } +} +"#, + expect![[r#"No references found at position"#]], + ); + } + + #[test] + fn test_rename_to_underscore() { + check("_", r#"fn main() { let i$0 = 1; }"#, r#"fn main() { let _ = 1; }"#); + } + + #[test] + fn test_rename_to_raw_identifier() { + check("r#fn", r#"fn main() { let i$0 = 1; }"#, r#"fn main() { let r#fn = 1; }"#); + } + + #[test] + fn test_rename_to_invalid_identifier1() { + check( + "invalid!", + r#"fn main() { let i$0 = 1; }"#, + "error: Invalid name `invalid!`: not an identifier", + ); + } + + #[test] + fn test_rename_to_invalid_identifier2() { + check( + "multiple tokens", + r#"fn main() { let i$0 = 1; }"#, + "error: Invalid name `multiple tokens`: not an identifier", + ); + } + + #[test] + fn test_rename_to_invalid_identifier3() { + check( + "let", + r#"fn main() { let i$0 = 1; }"#, + "error: Invalid name `let`: not an identifier", + ); + } + + #[test] + fn test_rename_to_invalid_identifier_lifetime() { + cov_mark::check!(rename_not_an_ident_ref); + check( + "'foo", + r#"fn main() { let i$0 = 1; }"#, + "error: Invalid name `'foo`: not an identifier", + ); + } + + #[test] + fn test_rename_to_invalid_identifier_lifetime2() { + cov_mark::check!(rename_not_a_lifetime_ident_ref); + check( + "foo", + r#"fn main<'a>(_: &'a$0 ()) {}"#, + "error: Invalid name `foo`: not a lifetime identifier", + ); + } + + #[test] + fn test_rename_to_underscore_invalid() { + cov_mark::check!(rename_underscore_multiple); + check( + "_", + r#"fn main(foo$0: ()) {foo;}"#, + "error: Cannot rename reference to `_` as it is being referenced multiple times", + ); + } + + #[test] + fn test_rename_mod_invalid() { + check( + "'foo", + r#"mod foo$0 {}"#, + "error: Invalid name `'foo`: cannot rename module to 'foo", + ); + } + + #[test] + fn test_rename_for_local() { + check( + "k", + r#" +fn main() { + let mut i = 1; + let j = 1; + i = i$0 + j; + + { i = 0; } + + i = 5; +} +"#, + r#" +fn main() { + let mut k = 1; + let j = 1; + k = k + j; + + { k = 0; } + + k = 5; +} +"#, + ); + } + + #[test] + fn test_rename_unresolved_reference() { + check( + "new_name", + r#"fn main() { let _ = unresolved_ref$0; }"#, + "error: No references found at position", + ); + } + + #[test] + fn test_rename_macro_multiple_occurrences() { + check( + "Baaah", + r#"macro_rules! foo { + ($ident:ident) => { + const $ident: () = (); + struct $ident {} + }; +} + +foo!($0Foo); +const _: () = Foo; +const _: Foo = Foo {}; + "#, + r#" +macro_rules! foo { + ($ident:ident) => { + const $ident: () = (); + struct $ident {} + }; +} + +foo!(Baaah); +const _: () = Baaah; +const _: Baaah = Baaah {}; + "#, + ) + } + + #[test] + fn test_rename_for_macro_args() { + check( + "b", + r#" +macro_rules! foo {($i:ident) => {$i} } +fn main() { + let a$0 = "test"; + foo!(a); +} +"#, + r#" +macro_rules! foo {($i:ident) => {$i} } +fn main() { + let b = "test"; + foo!(b); +} +"#, + ); + } + + #[test] + fn test_rename_for_macro_args_rev() { + check( + "b", + r#" +macro_rules! foo {($i:ident) => {$i} } +fn main() { + let a = "test"; + foo!(a$0); +} +"#, + r#" +macro_rules! foo {($i:ident) => {$i} } +fn main() { + let b = "test"; + foo!(b); +} +"#, + ); + } + + #[test] + fn test_rename_for_macro_define_fn() { + check( + "bar", + r#" +macro_rules! define_fn {($id:ident) => { fn $id{} }} +define_fn!(foo); +fn main() { + fo$0o(); +} +"#, + r#" +macro_rules! define_fn {($id:ident) => { fn $id{} }} +define_fn!(bar); +fn main() { + bar(); +} +"#, + ); + } + + #[test] + fn test_rename_for_macro_define_fn_rev() { + check( + "bar", + r#" +macro_rules! define_fn {($id:ident) => { fn $id{} }} +define_fn!(fo$0o); +fn main() { + foo(); +} +"#, + r#" +macro_rules! define_fn {($id:ident) => { fn $id{} }} +define_fn!(bar); +fn main() { + bar(); +} +"#, + ); + } + + #[test] + fn test_rename_for_param_inside() { + check("j", r#"fn foo(i : u32) -> u32 { i$0 }"#, r#"fn foo(j : u32) -> u32 { j }"#); + } + + #[test] + fn test_rename_refs_for_fn_param() { + check("j", r#"fn foo(i$0 : u32) -> u32 { i }"#, r#"fn foo(j : u32) -> u32 { j }"#); + } + + #[test] + fn test_rename_for_mut_param() { + check("j", r#"fn foo(mut i$0 : u32) -> u32 { i }"#, r#"fn foo(mut j : u32) -> u32 { j }"#); + } + + #[test] + fn test_rename_struct_field() { + check( + "foo", + r#" +struct Foo { field$0: i32 } + +impl Foo { + fn new(i: i32) -> Self { + Self { field: i } + } +} +"#, + r#" +struct Foo { foo: i32 } + +impl Foo { + fn new(i: i32) -> Self { + Self { foo: i } + } +} +"#, + ); + } + + #[test] + fn test_rename_field_in_field_shorthand() { + cov_mark::check!(test_rename_field_in_field_shorthand); + check( + "field", + r#" +struct Foo { foo$0: i32 } + +impl Foo { + fn new(foo: i32) -> Self { + Self { foo } + } +} +"#, + r#" +struct Foo { field: i32 } + +impl Foo { + fn new(foo: i32) -> Self { + Self { field: foo } + } +} +"#, + ); + } + + #[test] + fn test_rename_local_in_field_shorthand() { + cov_mark::check!(test_rename_local_in_field_shorthand); + check( + "j", + r#" +struct Foo { i: i32 } + +impl Foo { + fn new(i$0: i32) -> Self { + Self { i } + } +} +"#, + r#" +struct Foo { i: i32 } + +impl Foo { + fn new(j: i32) -> Self { + Self { i: j } + } +} +"#, + ); + } + + #[test] + fn test_field_shorthand_correct_struct() { + check( + "j", + r#" +struct Foo { i$0: i32 } +struct Bar { i: i32 } + +impl Bar { + fn new(i: i32) -> Self { + Self { i } + } +} +"#, + r#" +struct Foo { j: i32 } +struct Bar { i: i32 } + +impl Bar { + fn new(i: i32) -> Self { + Self { i } + } +} +"#, + ); + } + + #[test] + fn test_shadow_local_for_struct_shorthand() { + check( + "j", + r#" +struct Foo { i: i32 } + +fn baz(i$0: i32) -> Self { + let x = Foo { i }; + { + let i = 0; + Foo { i } + } +} +"#, + r#" +struct Foo { i: i32 } + +fn baz(j: i32) -> Self { + let x = Foo { i: j }; + { + let i = 0; + Foo { i } + } +} +"#, + ); + } + + #[test] + fn test_rename_mod() { + check_expect( + "foo2", + r#" +//- /lib.rs +mod bar; + +//- /bar.rs +mod foo$0; + +//- /bar/foo.rs +// empty +"#, + expect![[r#" + SourceChange { + source_file_edits: { + FileId( + 1, + ): TextEdit { + indels: [ + Indel { + insert: "foo2", + delete: 4..7, + }, + ], + }, + }, + file_system_edits: [ + MoveFile { + src: FileId( + 2, + ), + dst: AnchoredPathBuf { + anchor: FileId( + 2, + ), + path: "foo2.rs", + }, + }, + ], + is_snippet: false, + } + "#]], + ); + } + + #[test] + fn test_rename_mod_in_use_tree() { + check_expect( + "quux", + r#" +//- /main.rs +pub mod foo; +pub mod bar; +fn main() {} + +//- /foo.rs +pub struct FooContent; + +//- /bar.rs +use crate::foo$0::FooContent; +"#, + expect![[r#" + SourceChange { + source_file_edits: { + FileId( + 0, + ): TextEdit { + indels: [ + Indel { + insert: "quux", + delete: 8..11, + }, + ], + }, + FileId( + 2, + ): TextEdit { + indels: [ + Indel { + insert: "quux", + delete: 11..14, + }, + ], + }, + }, + file_system_edits: [ + MoveFile { + src: FileId( + 1, + ), + dst: AnchoredPathBuf { + anchor: FileId( + 1, + ), + path: "quux.rs", + }, + }, + ], + is_snippet: false, + } + "#]], + ); + } + + #[test] + fn test_rename_mod_in_dir() { + check_expect( + "foo2", + r#" +//- /lib.rs +mod fo$0o; +//- /foo/mod.rs +// empty +"#, + expect![[r#" + SourceChange { + source_file_edits: { + FileId( + 0, + ): TextEdit { + indels: [ + Indel { + insert: "foo2", + delete: 4..7, + }, + ], + }, + }, + file_system_edits: [ + MoveDir { + src: AnchoredPathBuf { + anchor: FileId( + 1, + ), + path: "../foo", + }, + src_id: FileId( + 1, + ), + dst: AnchoredPathBuf { + anchor: FileId( + 1, + ), + path: "../foo2", + }, + }, + ], + is_snippet: false, + } + "#]], + ); + } + + #[test] + fn test_rename_unusually_nested_mod() { + check_expect( + "bar", + r#" +//- /lib.rs +mod outer { mod fo$0o; } + +//- /outer/foo.rs +// empty +"#, + expect![[r#" + SourceChange { + source_file_edits: { + FileId( + 0, + ): TextEdit { + indels: [ + Indel { + insert: "bar", + delete: 16..19, + }, + ], + }, + }, + file_system_edits: [ + MoveFile { + src: FileId( + 1, + ), + dst: AnchoredPathBuf { + anchor: FileId( + 1, + ), + path: "bar.rs", + }, + }, + ], + is_snippet: false, + } + "#]], + ); + } + + #[test] + fn test_module_rename_in_path() { + check( + "baz", + r#" +mod $0foo { + pub use self::bar as qux; + pub fn bar() {} +} + +fn main() { foo::bar(); } +"#, + r#" +mod baz { + pub use self::bar as qux; + pub fn bar() {} +} + +fn main() { baz::bar(); } +"#, + ); + } + + #[test] + fn test_rename_mod_filename_and_path() { + check_expect( + "foo2", + r#" +//- /lib.rs +mod bar; +fn f() { + bar::foo::fun() +} + +//- /bar.rs +pub mod foo$0; + +//- /bar/foo.rs +// pub fn fun() {} +"#, + expect![[r#" + SourceChange { + source_file_edits: { + FileId( + 0, + ): TextEdit { + indels: [ + Indel { + insert: "foo2", + delete: 27..30, + }, + ], + }, + FileId( + 1, + ): TextEdit { + indels: [ + Indel { + insert: "foo2", + delete: 8..11, + }, + ], + }, + }, + file_system_edits: [ + MoveFile { + src: FileId( + 2, + ), + dst: AnchoredPathBuf { + anchor: FileId( + 2, + ), + path: "foo2.rs", + }, + }, + ], + is_snippet: false, + } + "#]], + ); + } + + #[test] + fn test_rename_mod_recursive() { + check_expect( + "foo2", + r#" +//- /lib.rs +mod foo$0; + +//- /foo.rs +mod bar; +mod corge; + +//- /foo/bar.rs +mod qux; + +//- /foo/bar/qux.rs +mod quux; + +//- /foo/bar/qux/quux/mod.rs +// empty + +//- /foo/corge.rs +// empty +"#, + expect![[r#" + SourceChange { + source_file_edits: { + FileId( + 0, + ): TextEdit { + indels: [ + Indel { + insert: "foo2", + delete: 4..7, + }, + ], + }, + }, + file_system_edits: [ + MoveFile { + src: FileId( + 1, + ), + dst: AnchoredPathBuf { + anchor: FileId( + 1, + ), + path: "foo2.rs", + }, + }, + MoveDir { + src: AnchoredPathBuf { + anchor: FileId( + 1, + ), + path: "foo", + }, + src_id: FileId( + 1, + ), + dst: AnchoredPathBuf { + anchor: FileId( + 1, + ), + path: "foo2", + }, + }, + ], + is_snippet: false, + } + "#]], + ) + } + #[test] + fn test_rename_mod_ref_by_super() { + check( + "baz", + r#" + mod $0foo { + struct X; + + mod bar { + use super::X; + } + } + "#, + r#" + mod baz { + struct X; + + mod bar { + use super::X; + } + } + "#, + ) + } + + #[test] + fn test_rename_mod_in_macro() { + check( + "bar", + r#" +//- /foo.rs + +//- /lib.rs +macro_rules! submodule { + ($name:ident) => { + mod $name; + }; +} + +submodule!($0foo); +"#, + r#" +macro_rules! submodule { + ($name:ident) => { + mod $name; + }; +} + +submodule!(bar); +"#, + ) + } + + #[test] + fn test_rename_mod_for_crate_root() { + check_expect_will_rename_file( + "main", + r#" +//- /lib.rs +use crate::foo as bar; +fn foo() {} +mod bar$0; +"#, + expect![[r#" + SourceChange { + source_file_edits: {}, + file_system_edits: [], + is_snippet: false, + } + "#]], + ) + } + + #[test] + fn test_enum_variant_from_module_1() { + cov_mark::check!(rename_non_local); + check( + "Baz", + r#" +mod foo { + pub enum Foo { Bar$0 } +} + +fn func(f: foo::Foo) { + match f { + foo::Foo::Bar => {} + } +} +"#, + r#" +mod foo { + pub enum Foo { Baz } +} + +fn func(f: foo::Foo) { + match f { + foo::Foo::Baz => {} + } +} +"#, + ); + } + + #[test] + fn test_enum_variant_from_module_2() { + check( + "baz", + r#" +mod foo { + pub struct Foo { pub bar$0: uint } +} + +fn foo(f: foo::Foo) { + let _ = f.bar; +} +"#, + r#" +mod foo { + pub struct Foo { pub baz: uint } +} + +fn foo(f: foo::Foo) { + let _ = f.baz; +} +"#, + ); + } + + #[test] + fn test_parameter_to_self() { + cov_mark::check!(rename_to_self); + check( + "self", + r#" +struct Foo { i: i32 } + +impl Foo { + fn f(foo$0: &mut Foo) -> i32 { + foo.i + } +} +"#, + r#" +struct Foo { i: i32 } + +impl Foo { + fn f(&mut self) -> i32 { + self.i + } +} +"#, + ); + check( + "self", + r#" +struct Foo { i: i32 } + +impl Foo { + fn f(foo$0: Foo) -> i32 { + foo.i + } +} +"#, + r#" +struct Foo { i: i32 } + +impl Foo { + fn f(self) -> i32 { + self.i + } +} +"#, + ); + } + + #[test] + fn test_parameter_to_self_error_no_impl() { + check( + "self", + r#" +struct Foo { i: i32 } + +fn f(foo$0: &mut Foo) -> i32 { + foo.i +} +"#, + "error: Cannot rename parameter to self for free function", + ); + check( + "self", + r#" +struct Foo { i: i32 } +struct Bar; + +impl Bar { + fn f(foo$0: &mut Foo) -> i32 { + foo.i + } +} +"#, + "error: Parameter type differs from impl block type", + ); + } + + #[test] + fn test_parameter_to_self_error_not_first() { + check( + "self", + r#" +struct Foo { i: i32 } +impl Foo { + fn f(x: (), foo$0: &mut Foo) -> i32 { + foo.i + } +} +"#, + "error: Only the first parameter may be renamed to self", + ); + } + + #[test] + fn test_parameter_to_self_impl_ref() { + check( + "self", + r#" +struct Foo { i: i32 } +impl &Foo { + fn f(foo$0: &Foo) -> i32 { + foo.i + } +} +"#, + r#" +struct Foo { i: i32 } +impl &Foo { + fn f(self) -> i32 { + self.i + } +} +"#, + ); + } + + #[test] + fn test_self_to_parameter() { + check( + "foo", + r#" +struct Foo { i: i32 } + +impl Foo { + fn f(&mut $0self) -> i32 { + self.i + } +} +"#, + r#" +struct Foo { i: i32 } + +impl Foo { + fn f(foo: &mut Foo) -> i32 { + foo.i + } +} +"#, + ); + } + + #[test] + fn test_owned_self_to_parameter() { + cov_mark::check!(rename_self_to_param); + check( + "foo", + r#" +struct Foo { i: i32 } + +impl Foo { + fn f($0self) -> i32 { + self.i + } +} +"#, + r#" +struct Foo { i: i32 } + +impl Foo { + fn f(foo: Foo) -> i32 { + foo.i + } +} +"#, + ); + } + + #[test] + fn test_self_in_path_to_parameter() { + check( + "foo", + r#" +struct Foo { i: i32 } + +impl Foo { + fn f(&self) -> i32 { + let self_var = 1; + self$0.i + } +} +"#, + r#" +struct Foo { i: i32 } + +impl Foo { + fn f(foo: &Foo) -> i32 { + let self_var = 1; + foo.i + } +} +"#, + ); + } + + #[test] + fn test_rename_field_put_init_shorthand() { + cov_mark::check!(test_rename_field_put_init_shorthand); + check( + "bar", + r#" +struct Foo { i$0: i32 } + +fn foo(bar: i32) -> Foo { + Foo { i: bar } +} +"#, + r#" +struct Foo { bar: i32 } + +fn foo(bar: i32) -> Foo { + Foo { bar } +} +"#, + ); + } + + #[test] + fn test_rename_local_put_init_shorthand() { + cov_mark::check!(test_rename_local_put_init_shorthand); + check( + "i", + r#" +struct Foo { i: i32 } + +fn foo(bar$0: i32) -> Foo { + Foo { i: bar } +} +"#, + r#" +struct Foo { i: i32 } + +fn foo(i: i32) -> Foo { + Foo { i } +} +"#, + ); + } + + #[test] + fn test_struct_field_pat_into_shorthand() { + cov_mark::check!(test_rename_field_put_init_shorthand_pat); + check( + "baz", + r#" +struct Foo { i$0: i32 } + +fn foo(foo: Foo) { + let Foo { i: ref baz @ qux } = foo; + let _ = qux; +} +"#, + r#" +struct Foo { baz: i32 } + +fn foo(foo: Foo) { + let Foo { baz: ref baz @ qux } = foo; + let _ = qux; +} +"#, + ); + check( + "baz", + r#" +struct Foo { i$0: i32 } + +fn foo(foo: Foo) { + let Foo { i: ref baz } = foo; + let _ = qux; +} +"#, + r#" +struct Foo { baz: i32 } + +fn foo(foo: Foo) { + let Foo { ref baz } = foo; + let _ = qux; +} +"#, + ); + } + + #[test] + fn test_struct_local_pat_into_shorthand() { + cov_mark::check!(test_rename_local_put_init_shorthand_pat); + check( + "field", + r#" +struct Foo { field: i32 } + +fn foo(foo: Foo) { + let Foo { field: qux$0 } = foo; + let _ = qux; +} +"#, + r#" +struct Foo { field: i32 } + +fn foo(foo: Foo) { + let Foo { field } = foo; + let _ = field; +} +"#, + ); + check( + "field", + r#" +struct Foo { field: i32 } + +fn foo(foo: Foo) { + let Foo { field: x @ qux$0 } = foo; + let _ = qux; +} +"#, + r#" +struct Foo { field: i32 } + +fn foo(foo: Foo) { + let Foo { field: x @ field } = foo; + let _ = field; +} +"#, + ); + } + + #[test] + fn test_rename_binding_in_destructure_pat() { + let expected_fixture = r#" +struct Foo { + i: i32, +} + +fn foo(foo: Foo) { + let Foo { i: bar } = foo; + let _ = bar; +} +"#; + check( + "bar", + r#" +struct Foo { + i: i32, +} + +fn foo(foo: Foo) { + let Foo { i: b } = foo; + let _ = b$0; +} +"#, + expected_fixture, + ); + check( + "bar", + r#" +struct Foo { + i: i32, +} + +fn foo(foo: Foo) { + let Foo { i } = foo; + let _ = i$0; +} +"#, + expected_fixture, + ); + } + + #[test] + fn test_rename_binding_in_destructure_param_pat() { + check( + "bar", + r#" +struct Foo { + i: i32 +} + +fn foo(Foo { i }: Foo) -> i32 { + i$0 +} +"#, + r#" +struct Foo { + i: i32 +} + +fn foo(Foo { i: bar }: Foo) -> i32 { + bar +} +"#, + ) + } + + #[test] + fn test_struct_field_complex_ident_pat() { + cov_mark::check!(rename_record_pat_field_name_split); + check( + "baz", + r#" +struct Foo { i$0: i32 } + +fn foo(foo: Foo) { + let Foo { ref i } = foo; +} +"#, + r#" +struct Foo { baz: i32 } + +fn foo(foo: Foo) { + let Foo { baz: ref i } = foo; +} +"#, + ); + } + + #[test] + fn test_rename_lifetimes() { + cov_mark::check!(rename_lifetime); + check( + "'yeeee", + r#" +trait Foo<'a> { + fn foo() -> &'a (); +} +impl<'a> Foo<'a> for &'a () { + fn foo() -> &'a$0 () { + unimplemented!() + } +} +"#, + r#" +trait Foo<'a> { + fn foo() -> &'a (); +} +impl<'yeeee> Foo<'yeeee> for &'yeeee () { + fn foo() -> &'yeeee () { + unimplemented!() + } +} +"#, + ) + } + + #[test] + fn test_rename_bind_pat() { + check( + "new_name", + r#" +fn main() { + enum CustomOption<T> { + None, + Some(T), + } + + let test_variable = CustomOption::Some(22); + + match test_variable { + CustomOption::Some(foo$0) if foo == 11 => {} + _ => (), + } +}"#, + r#" +fn main() { + enum CustomOption<T> { + None, + Some(T), + } + + let test_variable = CustomOption::Some(22); + + match test_variable { + CustomOption::Some(new_name) if new_name == 11 => {} + _ => (), + } +}"#, + ); + } + + #[test] + fn test_rename_label() { + check( + "'foo", + r#" +fn foo<'a>() -> &'a () { + 'a: { + 'b: loop { + break 'a$0; + } + } +} +"#, + r#" +fn foo<'a>() -> &'a () { + 'foo: { + 'b: loop { + break 'foo; + } + } +} +"#, + ) + } + + #[test] + fn test_self_to_self() { + cov_mark::check!(rename_self_to_self); + check( + "self", + r#" +struct Foo; +impl Foo { + fn foo(self$0) {} +} +"#, + r#" +struct Foo; +impl Foo { + fn foo(self) {} +} +"#, + ) + } + + #[test] + fn test_rename_field_in_pat_in_macro_doesnt_shorthand() { + // ideally we would be able to make this emit a short hand, but I doubt this is easily possible + check( + "baz", + r#" +macro_rules! foo { + ($pattern:pat) => { + let $pattern = loop {}; + }; +} +struct Foo { + bar$0: u32, +} +fn foo() { + foo!(Foo { bar: baz }); +} +"#, + r#" +macro_rules! foo { + ($pattern:pat) => { + let $pattern = loop {}; + }; +} +struct Foo { + baz: u32, +} +fn foo() { + foo!(Foo { baz: baz }); +} +"#, + ) + } + + #[test] + fn test_rename_tuple_field() { + check( + "foo", + r#" +struct Foo(i32); + +fn baz() { + let mut x = Foo(4); + x.0$0 = 5; +} +"#, + "error: No identifier available to rename", + ); + } + + #[test] + fn test_rename_builtin() { + check( + "foo", + r#" +fn foo() { + let x: i32$0 = 0; +} +"#, + "error: Cannot rename builtin type", + ); + } + + #[test] + fn test_rename_self() { + check( + "foo", + r#" +struct Foo {} + +impl Foo { + fn foo(self) -> Self$0 { + self + } +} +"#, + "error: Cannot rename `Self`", + ); + } + + #[test] + fn test_rename_ignores_self_ty() { + check( + "Fo0", + r#" +struct $0Foo; + +impl Foo where Self: {} +"#, + r#" +struct Fo0; + +impl Fo0 where Self: {} +"#, + ); + } + + #[test] + fn test_rename_fails_on_aliases() { + check( + "Baz", + r#" +struct Foo; +use Foo as Bar$0; +"#, + "error: Renaming aliases is currently unsupported", + ); + check( + "Baz", + r#" +struct Foo; +use Foo as Bar; +use Bar$0; +"#, + "error: Renaming aliases is currently unsupported", + ); + } + + #[test] + fn test_rename_trait_method() { + let res = r" +trait Foo { + fn foo(&self) { + self.foo(); + } +} + +impl Foo for () { + fn foo(&self) { + self.foo(); + } +}"; + check( + "foo", + r#" +trait Foo { + fn bar$0(&self) { + self.bar(); + } +} + +impl Foo for () { + fn bar(&self) { + self.bar(); + } +}"#, + res, + ); + check( + "foo", + r#" +trait Foo { + fn bar(&self) { + self.bar$0(); + } +} + +impl Foo for () { + fn bar(&self) { + self.bar(); + } +}"#, + res, + ); + check( + "foo", + r#" +trait Foo { + fn bar(&self) { + self.bar(); + } +} + +impl Foo for () { + fn bar$0(&self) { + self.bar(); + } +}"#, + res, + ); + check( + "foo", + r#" +trait Foo { + fn bar(&self) { + self.bar(); + } +} + +impl Foo for () { + fn bar(&self) { + self.bar$0(); + } +}"#, + res, + ); + } + + #[test] + fn test_rename_trait_method_prefix_of_second() { + check( + "qux", + r#" +trait Foo { + fn foo$0() {} + fn foobar() {} +} +"#, + r#" +trait Foo { + fn qux() {} + fn foobar() {} +} +"#, + ); + } + + #[test] + fn test_rename_trait_const() { + let res = r" +trait Foo { + const FOO: (); +} + +impl Foo for () { + const FOO: (); +} +fn f() { <()>::FOO; }"; + check( + "FOO", + r#" +trait Foo { + const BAR$0: (); +} + +impl Foo for () { + const BAR: (); +} +fn f() { <()>::BAR; }"#, + res, + ); + check( + "FOO", + r#" +trait Foo { + const BAR: (); +} + +impl Foo for () { + const BAR$0: (); +} +fn f() { <()>::BAR; }"#, + res, + ); + check( + "FOO", + r#" +trait Foo { + const BAR: (); +} + +impl Foo for () { + const BAR: (); +} +fn f() { <()>::BAR$0; }"#, + res, + ); + } + + #[test] + fn defs_from_macros_arent_renamed() { + check( + "lol", + r#" +macro_rules! m { () => { fn f() {} } } +m!(); +fn main() { f$0() } +"#, + "error: No identifier available to rename", + ) + } + + #[test] + fn attributed_item() { + check( + "function", + r#" +//- proc_macros: identity + +#[proc_macros::identity] +fn func$0() { + func(); +} +"#, + r#" + +#[proc_macros::identity] +fn function() { + function(); +} +"#, + ) + } + + #[test] + fn in_macro_multi_mapping() { + check( + "a", + r#" +fn foo() { + macro_rules! match_ast2 { + ($node:ident { + $( $res:expr, )* + }) => {{ + $( if $node { $res } else )* + { loop {} } + }}; + } + let $0d = 3; + match_ast2! { + d { + d, + d, + } + }; +} +"#, + r#" +fn foo() { + macro_rules! match_ast2 { + ($node:ident { + $( $res:expr, )* + }) => {{ + $( if $node { $res } else )* + { loop {} } + }}; + } + let a = 3; + match_ast2! { + a { + a, + a, + } + }; +} +"#, + ) + } + + #[test] + fn rename_multi_local() { + check( + "bar", + r#" +fn foo((foo$0 | foo | foo): ()) { + foo; + let foo; +} +"#, + r#" +fn foo((bar | bar | bar): ()) { + bar; + let foo; +} +"#, + ); + check( + "bar", + r#" +fn foo((foo | foo$0 | foo): ()) { + foo; + let foo; +} +"#, + r#" +fn foo((bar | bar | bar): ()) { + bar; + let foo; +} +"#, + ); + check( + "bar", + r#" +fn foo((foo | foo | foo): ()) { + foo$0; + let foo; +} +"#, + r#" +fn foo((bar | bar | bar): ()) { + bar; + let foo; +} +"#, + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/runnables.rs b/src/tools/rust-analyzer/crates/ide/src/runnables.rs new file mode 100644 index 000000000..bec770ed9 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/runnables.rs @@ -0,0 +1,2163 @@ +use std::fmt; + +use ast::HasName; +use cfg::CfgExpr; +use hir::{AsAssocItem, HasAttrs, HasSource, HirDisplay, Semantics}; +use ide_assists::utils::test_related_attribute; +use ide_db::{ + base_db::{FilePosition, FileRange}, + defs::Definition, + helpers::visit_file_defs, + search::SearchScope, + FxHashMap, FxHashSet, RootDatabase, SymbolKind, +}; +use itertools::Itertools; +use stdx::{always, format_to}; +use syntax::{ + ast::{self, AstNode, HasAttrs as _}, + SmolStr, SyntaxNode, +}; + +use crate::{references, FileId, NavigationTarget, ToNav, TryToNav}; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Runnable { + pub use_name_in_title: bool, + pub nav: NavigationTarget, + pub kind: RunnableKind, + pub cfg: Option<CfgExpr>, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum TestId { + Name(SmolStr), + Path(String), +} + +impl fmt::Display for TestId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TestId::Name(name) => name.fmt(f), + TestId::Path(path) => path.fmt(f), + } + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum RunnableKind { + Test { test_id: TestId, attr: TestAttr }, + TestMod { path: String }, + Bench { test_id: TestId }, + DocTest { test_id: TestId }, + Bin, +} + +#[cfg(test)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +enum RunnableTestKind { + Test, + TestMod, + DocTest, + Bench, + Bin, +} + +impl Runnable { + // test package::module::testname + pub fn label(&self, target: Option<String>) -> String { + match &self.kind { + RunnableKind::Test { test_id, .. } => format!("test {}", test_id), + RunnableKind::TestMod { path } => format!("test-mod {}", path), + RunnableKind::Bench { test_id } => format!("bench {}", test_id), + RunnableKind::DocTest { test_id, .. } => format!("doctest {}", test_id), + RunnableKind::Bin => { + target.map_or_else(|| "run binary".to_string(), |t| format!("run {}", t)) + } + } + } + + pub fn title(&self) -> String { + let mut s = String::from("▶\u{fe0e} Run "); + if self.use_name_in_title { + format_to!(s, "{}", self.nav.name); + if !matches!(self.kind, RunnableKind::Bin) { + s.push(' '); + } + } + let suffix = match &self.kind { + RunnableKind::TestMod { .. } => "Tests", + RunnableKind::Test { .. } => "Test", + RunnableKind::DocTest { .. } => "Doctest", + RunnableKind::Bench { .. } => "Bench", + RunnableKind::Bin => return s, + }; + s.push_str(suffix); + s + } + + #[cfg(test)] + fn test_kind(&self) -> RunnableTestKind { + match &self.kind { + RunnableKind::TestMod { .. } => RunnableTestKind::TestMod, + RunnableKind::Test { .. } => RunnableTestKind::Test, + RunnableKind::DocTest { .. } => RunnableTestKind::DocTest, + RunnableKind::Bench { .. } => RunnableTestKind::Bench, + RunnableKind::Bin => RunnableTestKind::Bin, + } + } +} + +// Feature: Run +// +// Shows a popup suggesting to run a test/benchmark/binary **at the current cursor +// location**. Super useful for repeatedly running just a single test. Do bind this +// to a shortcut! +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Run** +// |=== +// image::https://user-images.githubusercontent.com/48062697/113065583-055aae80-91b1-11eb-958f-d67efcaf6a2f.gif[] +pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> { + let sema = Semantics::new(db); + + let mut res = Vec::new(); + // Record all runnables that come from macro expansions here instead. + // In case an expansion creates multiple runnables we want to name them to avoid emitting a bunch of equally named runnables. + let mut in_macro_expansion = FxHashMap::<hir::HirFileId, Vec<Runnable>>::default(); + let mut add_opt = |runnable: Option<Runnable>, def| { + if let Some(runnable) = runnable.filter(|runnable| { + always!( + runnable.nav.file_id == file_id, + "tried adding a runnable pointing to a different file: {:?} for {:?}", + runnable.kind, + file_id + ) + }) { + if let Some(def) = def { + let file_id = match def { + Definition::Module(it) => it.declaration_source(db).map(|src| src.file_id), + Definition::Function(it) => it.source(db).map(|src| src.file_id), + _ => None, + }; + if let Some(file_id) = file_id.filter(|file| file.call_node(db).is_some()) { + in_macro_expansion.entry(file_id).or_default().push(runnable); + return; + } + } + res.push(runnable); + } + }; + visit_file_defs(&sema, file_id, &mut |def| { + let runnable = match def { + Definition::Module(it) => runnable_mod(&sema, it), + Definition::Function(it) => runnable_fn(&sema, it), + Definition::SelfType(impl_) => runnable_impl(&sema, &impl_), + _ => None, + }; + add_opt( + runnable + .or_else(|| module_def_doctest(sema.db, def)) + // #[macro_export] mbe macros are declared in the root, while their definition may reside in a different module + .filter(|it| it.nav.file_id == file_id), + Some(def), + ); + if let Definition::SelfType(impl_) = def { + impl_.items(db).into_iter().for_each(|assoc| { + let runnable = match assoc { + hir::AssocItem::Function(it) => { + runnable_fn(&sema, it).or_else(|| module_def_doctest(sema.db, it.into())) + } + hir::AssocItem::Const(it) => module_def_doctest(sema.db, it.into()), + hir::AssocItem::TypeAlias(it) => module_def_doctest(sema.db, it.into()), + }; + add_opt(runnable, Some(assoc.into())) + }); + } + }); + + sema.to_module_defs(file_id) + .map(|it| runnable_mod_outline_definition(&sema, it)) + .for_each(|it| add_opt(it, None)); + + res.extend(in_macro_expansion.into_iter().flat_map(|(_, runnables)| { + let use_name_in_title = runnables.len() != 1; + runnables.into_iter().map(move |mut r| { + r.use_name_in_title = use_name_in_title; + r + }) + })); + res +} + +// Feature: Related Tests +// +// Provides a sneak peek of all tests where the current item is used. +// +// The simplest way to use this feature is via the context menu: +// - Right-click on the selected item. The context menu opens. +// - Select **Peek related tests** +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Peek related tests** +// |=== +pub(crate) fn related_tests( + db: &RootDatabase, + position: FilePosition, + search_scope: Option<SearchScope>, +) -> Vec<Runnable> { + let sema = Semantics::new(db); + let mut res: FxHashSet<Runnable> = FxHashSet::default(); + let syntax = sema.parse(position.file_id).syntax().clone(); + + find_related_tests(&sema, &syntax, position, search_scope, &mut res); + + res.into_iter().collect() +} + +fn find_related_tests( + sema: &Semantics<'_, RootDatabase>, + syntax: &SyntaxNode, + position: FilePosition, + search_scope: Option<SearchScope>, + tests: &mut FxHashSet<Runnable>, +) { + // FIXME: why is this using references::find_defs, this should use ide_db::search + let defs = match references::find_defs(sema, syntax, position.offset) { + Some(defs) => defs, + None => return, + }; + for def in defs { + let defs = def + .usages(sema) + .set_scope(search_scope.clone()) + .all() + .references + .into_values() + .flatten(); + for ref_ in defs { + let name_ref = match ref_.name { + ast::NameLike::NameRef(name_ref) => name_ref, + _ => continue, + }; + if let Some(fn_def) = + sema.ancestors_with_macros(name_ref.syntax().clone()).find_map(ast::Fn::cast) + { + if let Some(runnable) = as_test_runnable(sema, &fn_def) { + // direct test + tests.insert(runnable); + } else if let Some(module) = parent_test_module(sema, &fn_def) { + // indirect test + find_related_tests_in_module(sema, syntax, &fn_def, &module, tests); + } + } + } + } +} + +fn find_related_tests_in_module( + sema: &Semantics<'_, RootDatabase>, + syntax: &SyntaxNode, + fn_def: &ast::Fn, + parent_module: &hir::Module, + tests: &mut FxHashSet<Runnable>, +) { + let fn_name = match fn_def.name() { + Some(it) => it, + _ => return, + }; + let mod_source = parent_module.definition_source(sema.db); + let range = match &mod_source.value { + hir::ModuleSource::Module(m) => m.syntax().text_range(), + hir::ModuleSource::BlockExpr(b) => b.syntax().text_range(), + hir::ModuleSource::SourceFile(f) => f.syntax().text_range(), + }; + + let file_id = mod_source.file_id.original_file(sema.db); + let mod_scope = SearchScope::file_range(FileRange { file_id, range }); + let fn_pos = FilePosition { file_id, offset: fn_name.syntax().text_range().start() }; + find_related_tests(sema, syntax, fn_pos, Some(mod_scope), tests) +} + +fn as_test_runnable(sema: &Semantics<'_, RootDatabase>, fn_def: &ast::Fn) -> Option<Runnable> { + if test_related_attribute(fn_def).is_some() { + let function = sema.to_def(fn_def)?; + runnable_fn(sema, function) + } else { + None + } +} + +fn parent_test_module(sema: &Semantics<'_, RootDatabase>, fn_def: &ast::Fn) -> Option<hir::Module> { + fn_def.syntax().ancestors().find_map(|node| { + let module = ast::Module::cast(node)?; + let module = sema.to_def(&module)?; + + if has_test_function_or_multiple_test_submodules(sema, &module) { + Some(module) + } else { + None + } + }) +} + +pub(crate) fn runnable_fn( + sema: &Semantics<'_, RootDatabase>, + def: hir::Function, +) -> Option<Runnable> { + let func = def.source(sema.db)?; + let name = def.name(sema.db).to_smol_str(); + + let root = def.module(sema.db).krate().root_module(sema.db); + + let kind = if name == "main" && def.module(sema.db) == root { + RunnableKind::Bin + } else { + let test_id = || { + let canonical_path = { + let def: hir::ModuleDef = def.into(); + def.canonical_path(sema.db) + }; + canonical_path.map(TestId::Path).unwrap_or(TestId::Name(name)) + }; + + if test_related_attribute(&func.value).is_some() { + let attr = TestAttr::from_fn(&func.value); + RunnableKind::Test { test_id: test_id(), attr } + } else if func.value.has_atom_attr("bench") { + RunnableKind::Bench { test_id: test_id() } + } else { + return None; + } + }; + + let nav = NavigationTarget::from_named( + sema.db, + func.as_ref().map(|it| it as &dyn ast::HasName), + SymbolKind::Function, + ); + let cfg = def.attrs(sema.db).cfg(); + Some(Runnable { use_name_in_title: false, nav, kind, cfg }) +} + +pub(crate) fn runnable_mod( + sema: &Semantics<'_, RootDatabase>, + def: hir::Module, +) -> Option<Runnable> { + if !has_test_function_or_multiple_test_submodules(sema, &def) { + return None; + } + let path = + def.path_to_root(sema.db).into_iter().rev().filter_map(|it| it.name(sema.db)).join("::"); + + let attrs = def.attrs(sema.db); + let cfg = attrs.cfg(); + let nav = NavigationTarget::from_module_to_decl(sema.db, def); + Some(Runnable { use_name_in_title: false, nav, kind: RunnableKind::TestMod { path }, cfg }) +} + +pub(crate) fn runnable_impl( + sema: &Semantics<'_, RootDatabase>, + def: &hir::Impl, +) -> Option<Runnable> { + let attrs = def.attrs(sema.db); + if !has_runnable_doc_test(&attrs) { + return None; + } + let cfg = attrs.cfg(); + let nav = def.try_to_nav(sema.db)?; + let ty = def.self_ty(sema.db); + let adt_name = ty.as_adt()?.name(sema.db); + let mut ty_args = ty.type_arguments().peekable(); + let params = if ty_args.peek().is_some() { + format!("<{}>", ty_args.format_with(", ", |ty, cb| cb(&ty.display(sema.db)))) + } else { + String::new() + }; + let test_id = TestId::Path(format!("{}{}", adt_name, params)); + + Some(Runnable { use_name_in_title: false, nav, kind: RunnableKind::DocTest { test_id }, cfg }) +} + +/// Creates a test mod runnable for outline modules at the top of their definition. +fn runnable_mod_outline_definition( + sema: &Semantics<'_, RootDatabase>, + def: hir::Module, +) -> Option<Runnable> { + if !has_test_function_or_multiple_test_submodules(sema, &def) { + return None; + } + let path = + def.path_to_root(sema.db).into_iter().rev().filter_map(|it| it.name(sema.db)).join("::"); + + let attrs = def.attrs(sema.db); + let cfg = attrs.cfg(); + match def.definition_source(sema.db).value { + hir::ModuleSource::SourceFile(_) => Some(Runnable { + use_name_in_title: false, + nav: def.to_nav(sema.db), + kind: RunnableKind::TestMod { path }, + cfg, + }), + _ => None, + } +} + +fn module_def_doctest(db: &RootDatabase, def: Definition) -> Option<Runnable> { + let attrs = match def { + Definition::Module(it) => it.attrs(db), + Definition::Function(it) => it.attrs(db), + Definition::Adt(it) => it.attrs(db), + Definition::Variant(it) => it.attrs(db), + Definition::Const(it) => it.attrs(db), + Definition::Static(it) => it.attrs(db), + Definition::Trait(it) => it.attrs(db), + Definition::TypeAlias(it) => it.attrs(db), + Definition::Macro(it) => it.attrs(db), + Definition::SelfType(it) => it.attrs(db), + _ => return None, + }; + if !has_runnable_doc_test(&attrs) { + return None; + } + let def_name = def.name(db)?; + let path = (|| { + let mut path = String::new(); + def.canonical_module_path(db)? + .flat_map(|it| it.name(db)) + .for_each(|name| format_to!(path, "{}::", name)); + // This probably belongs to canonical_path? + if let Some(assoc_item) = def.as_assoc_item(db) { + if let hir::AssocItemContainer::Impl(imp) = assoc_item.container(db) { + let ty = imp.self_ty(db); + if let Some(adt) = ty.as_adt() { + let name = adt.name(db); + let mut ty_args = ty.type_arguments().peekable(); + format_to!(path, "{}", name); + if ty_args.peek().is_some() { + format_to!( + path, + "<{}>", + ty_args.format_with(", ", |ty, cb| cb(&ty.display(db))) + ); + } + format_to!(path, "::{}", def_name); + return Some(path); + } + } + } + format_to!(path, "{}", def_name); + Some(path) + })(); + + let test_id = path.map_or_else(|| TestId::Name(def_name.to_smol_str()), TestId::Path); + + let mut nav = match def { + Definition::Module(def) => NavigationTarget::from_module_to_decl(db, def), + def => def.try_to_nav(db)?, + }; + nav.focus_range = None; + nav.description = None; + nav.docs = None; + nav.kind = None; + let res = Runnable { + use_name_in_title: false, + nav, + kind: RunnableKind::DocTest { test_id }, + cfg: attrs.cfg(), + }; + Some(res) +} + +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub struct TestAttr { + pub ignore: bool, +} + +impl TestAttr { + fn from_fn(fn_def: &ast::Fn) -> TestAttr { + let ignore = fn_def + .attrs() + .filter_map(|attr| attr.simple_name()) + .any(|attribute_text| attribute_text == "ignore"); + TestAttr { ignore } + } +} + +const RUSTDOC_FENCES: [&str; 2] = ["```", "~~~"]; +const RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE: &[&str] = + &["", "rust", "should_panic", "edition2015", "edition2018", "edition2021"]; + +fn has_runnable_doc_test(attrs: &hir::Attrs) -> bool { + attrs.docs().map_or(false, |doc| { + let mut in_code_block = false; + + for line in String::from(doc).lines() { + if let Some(header) = + RUSTDOC_FENCES.into_iter().find_map(|fence| line.strip_prefix(fence)) + { + in_code_block = !in_code_block; + + if in_code_block + && header + .split(',') + .all(|sub| RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE.contains(&sub.trim())) + { + return true; + } + } + } + + false + }) +} + +// We could create runnables for modules with number_of_test_submodules > 0, +// but that bloats the runnables for no real benefit, since all tests can be run by the submodule already +fn has_test_function_or_multiple_test_submodules( + sema: &Semantics<'_, RootDatabase>, + module: &hir::Module, +) -> bool { + let mut number_of_test_submodules = 0; + + for item in module.declarations(sema.db) { + match item { + hir::ModuleDef::Function(f) => { + if let Some(it) = f.source(sema.db) { + if test_related_attribute(&it.value).is_some() { + return true; + } + } + } + hir::ModuleDef::Module(submodule) => { + if has_test_function_or_multiple_test_submodules(sema, &submodule) { + number_of_test_submodules += 1; + } + } + _ => (), + } + } + + number_of_test_submodules > 1 +} + +#[cfg(test)] +mod tests { + use expect_test::{expect, Expect}; + + use crate::fixture; + + use super::{RunnableTestKind::*, *}; + + fn check( + ra_fixture: &str, + // FIXME: fold this into `expect` as well + actions: &[RunnableTestKind], + expect: Expect, + ) { + let (analysis, position) = fixture::position(ra_fixture); + let mut runnables = analysis.runnables(position.file_id).unwrap(); + runnables.sort_by_key(|it| (it.nav.full_range.start(), it.nav.name.clone())); + expect.assert_debug_eq(&runnables); + assert_eq!( + actions, + runnables.into_iter().map(|it| it.test_kind()).collect::<Vec<_>>().as_slice() + ); + } + + fn check_tests(ra_fixture: &str, expect: Expect) { + let (analysis, position) = fixture::position(ra_fixture); + let tests = analysis.related_tests(position, None).unwrap(); + expect.assert_debug_eq(&tests); + } + + #[test] + fn test_runnables() { + check( + r#" +//- /lib.rs +$0 +fn main() {} + +#[test] +fn test_foo() {} + +#[test] +#[ignore] +fn test_foo() {} + +#[bench] +fn bench() {} + +mod not_a_root { + fn main() {} +} +"#, + &[TestMod, Bin, Test, Test, Bench], + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..137, + name: "", + kind: Module, + }, + kind: TestMod { + path: "", + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 1..13, + focus_range: 4..8, + name: "main", + kind: Function, + }, + kind: Bin, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 15..39, + focus_range: 26..34, + name: "test_foo", + kind: Function, + }, + kind: Test { + test_id: Path( + "test_foo", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 41..75, + focus_range: 62..70, + name: "test_foo", + kind: Function, + }, + kind: Test { + test_id: Path( + "test_foo", + ), + attr: TestAttr { + ignore: true, + }, + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 77..99, + focus_range: 89..94, + name: "bench", + kind: Function, + }, + kind: Bench { + test_id: Path( + "bench", + ), + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn test_runnables_doc_test() { + check( + r#" +//- /lib.rs +$0 +fn main() {} + +/// ``` +/// let x = 5; +/// ``` +fn should_have_runnable() {} + +/// ```edition2018 +/// let x = 5; +/// ``` +fn should_have_runnable_1() {} + +/// ``` +/// let z = 55; +/// ``` +/// +/// ```ignore +/// let z = 56; +/// ``` +fn should_have_runnable_2() {} + +/** +```rust +let z = 55; +``` +*/ +fn should_have_no_runnable_3() {} + +/** + ```rust + let z = 55; + ``` +*/ +fn should_have_no_runnable_4() {} + +/// ```no_run +/// let z = 55; +/// ``` +fn should_have_no_runnable() {} + +/// ```ignore +/// let z = 55; +/// ``` +fn should_have_no_runnable_2() {} + +/// ```compile_fail +/// let z = 55; +/// ``` +fn should_have_no_runnable_3() {} + +/// ```text +/// arbitrary plain text +/// ``` +fn should_have_no_runnable_4() {} + +/// ```text +/// arbitrary plain text +/// ``` +/// +/// ```sh +/// $ shell code +/// ``` +fn should_have_no_runnable_5() {} + +/// ```rust,no_run +/// let z = 55; +/// ``` +fn should_have_no_runnable_6() {} + +/// ``` +/// let x = 5; +/// ``` +struct StructWithRunnable(String); + +/// ``` +/// let x = 5; +/// ``` +impl StructWithRunnable {} + +trait Test { + fn test() -> usize { + 5usize + } +} + +/// ``` +/// let x = 5; +/// ``` +impl Test for StructWithRunnable {} +"#, + &[Bin, DocTest, DocTest, DocTest, DocTest, DocTest, DocTest, DocTest, DocTest], + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 1..13, + focus_range: 4..8, + name: "main", + kind: Function, + }, + kind: Bin, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 15..74, + name: "should_have_runnable", + }, + kind: DocTest { + test_id: Path( + "should_have_runnable", + ), + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 76..148, + name: "should_have_runnable_1", + }, + kind: DocTest { + test_id: Path( + "should_have_runnable_1", + ), + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 150..254, + name: "should_have_runnable_2", + }, + kind: DocTest { + test_id: Path( + "should_have_runnable_2", + ), + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 256..320, + name: "should_have_no_runnable_3", + }, + kind: DocTest { + test_id: Path( + "should_have_no_runnable_3", + ), + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 322..398, + name: "should_have_no_runnable_4", + }, + kind: DocTest { + test_id: Path( + "should_have_no_runnable_4", + ), + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 900..965, + name: "StructWithRunnable", + }, + kind: DocTest { + test_id: Path( + "StructWithRunnable", + ), + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 967..1024, + focus_range: 1003..1021, + name: "impl", + kind: Impl, + }, + kind: DocTest { + test_id: Path( + "StructWithRunnable", + ), + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 1088..1154, + focus_range: 1133..1151, + name: "impl", + kind: Impl, + }, + kind: DocTest { + test_id: Path( + "StructWithRunnable", + ), + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn test_runnables_doc_test_in_impl() { + check( + r#" +//- /lib.rs +$0 +fn main() {} + +struct Data; +impl Data { + /// ``` + /// let x = 5; + /// ``` + fn foo() {} +} +"#, + &[Bin, DocTest], + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 1..13, + focus_range: 4..8, + name: "main", + kind: Function, + }, + kind: Bin, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 44..98, + name: "foo", + }, + kind: DocTest { + test_id: Path( + "Data::foo", + ), + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn test_runnables_module() { + check( + r#" +//- /lib.rs +$0 +mod test_mod { + #[test] + fn test_foo1() {} +} +"#, + &[TestMod, Test], + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 1..51, + focus_range: 5..13, + name: "test_mod", + kind: Module, + description: "mod test_mod", + }, + kind: TestMod { + path: "test_mod", + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 20..49, + focus_range: 35..44, + name: "test_foo1", + kind: Function, + }, + kind: Test { + test_id: Path( + "test_mod::test_foo1", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn only_modules_with_test_functions_or_more_than_one_test_submodule_have_runners() { + check( + r#" +//- /lib.rs +$0 +mod root_tests { + mod nested_tests_0 { + mod nested_tests_1 { + #[test] + fn nested_test_11() {} + + #[test] + fn nested_test_12() {} + } + + mod nested_tests_2 { + #[test] + fn nested_test_2() {} + } + + mod nested_tests_3 {} + } + + mod nested_tests_4 {} +} +"#, + &[TestMod, TestMod, Test, Test, TestMod, Test], + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 22..323, + focus_range: 26..40, + name: "nested_tests_0", + kind: Module, + description: "mod nested_tests_0", + }, + kind: TestMod { + path: "root_tests::nested_tests_0", + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 51..192, + focus_range: 55..69, + name: "nested_tests_1", + kind: Module, + description: "mod nested_tests_1", + }, + kind: TestMod { + path: "root_tests::nested_tests_0::nested_tests_1", + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 84..126, + focus_range: 107..121, + name: "nested_test_11", + kind: Function, + }, + kind: Test { + test_id: Path( + "root_tests::nested_tests_0::nested_tests_1::nested_test_11", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 140..182, + focus_range: 163..177, + name: "nested_test_12", + kind: Function, + }, + kind: Test { + test_id: Path( + "root_tests::nested_tests_0::nested_tests_1::nested_test_12", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 202..286, + focus_range: 206..220, + name: "nested_tests_2", + kind: Module, + description: "mod nested_tests_2", + }, + kind: TestMod { + path: "root_tests::nested_tests_0::nested_tests_2", + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 235..276, + focus_range: 258..271, + name: "nested_test_2", + kind: Function, + }, + kind: Test { + test_id: Path( + "root_tests::nested_tests_0::nested_tests_2::nested_test_2", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn test_runnables_with_feature() { + check( + r#" +//- /lib.rs crate:foo cfg:feature=foo +$0 +#[test] +#[cfg(feature = "foo")] +fn test_foo1() {} +"#, + &[TestMod, Test], + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..51, + name: "", + kind: Module, + }, + kind: TestMod { + path: "", + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 1..50, + focus_range: 36..45, + name: "test_foo1", + kind: Function, + }, + kind: Test { + test_id: Path( + "test_foo1", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: Some( + Atom( + KeyValue { + key: "feature", + value: "foo", + }, + ), + ), + }, + ] + "#]], + ); + } + + #[test] + fn test_runnables_with_features() { + check( + r#" +//- /lib.rs crate:foo cfg:feature=foo,feature=bar +$0 +#[test] +#[cfg(all(feature = "foo", feature = "bar"))] +fn test_foo1() {} +"#, + &[TestMod, Test], + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..73, + name: "", + kind: Module, + }, + kind: TestMod { + path: "", + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 1..72, + focus_range: 58..67, + name: "test_foo1", + kind: Function, + }, + kind: Test { + test_id: Path( + "test_foo1", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: Some( + All( + [ + Atom( + KeyValue { + key: "feature", + value: "foo", + }, + ), + Atom( + KeyValue { + key: "feature", + value: "bar", + }, + ), + ], + ), + ), + }, + ] + "#]], + ); + } + + #[test] + fn test_runnables_no_test_function_in_module() { + check( + r#" +//- /lib.rs +$0 +mod test_mod { + fn foo1() {} +} +"#, + &[], + expect![[r#" + [] + "#]], + ); + } + + #[test] + fn test_doc_runnables_impl_mod() { + check( + r#" +//- /lib.rs +mod foo; +//- /foo.rs +struct Foo;$0 +impl Foo { + /// ``` + /// let x = 5; + /// ``` + fn foo() {} +} + "#, + &[DocTest], + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 1, + ), + full_range: 27..81, + name: "foo", + }, + kind: DocTest { + test_id: Path( + "foo::Foo::foo", + ), + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn test_runnables_in_macro() { + check( + r#" +//- /lib.rs +$0 +macro_rules! gen { + () => { + #[test] + fn foo_test() {} + } +} +macro_rules! gen2 { + () => { + mod tests2 { + #[test] + fn foo_test2() {} + } + } +} +mod tests { + gen!(); +} +gen2!(); +"#, + &[TestMod, TestMod, Test, Test, TestMod], + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..237, + name: "", + kind: Module, + }, + kind: TestMod { + path: "", + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 202..227, + focus_range: 206..211, + name: "tests", + kind: Module, + description: "mod tests", + }, + kind: TestMod { + path: "tests", + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 218..225, + name: "foo_test", + kind: Function, + }, + kind: Test { + test_id: Path( + "tests::foo_test", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + Runnable { + use_name_in_title: true, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 228..236, + name: "foo_test2", + kind: Function, + }, + kind: Test { + test_id: Path( + "tests2::foo_test2", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + Runnable { + use_name_in_title: true, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 228..236, + name: "tests2", + kind: Module, + description: "mod tests2", + }, + kind: TestMod { + path: "tests2", + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn big_mac() { + check( + r#" +//- /lib.rs +$0 +macro_rules! foo { + () => { + mod foo_tests { + #[test] + fn foo0() {} + #[test] + fn foo1() {} + #[test] + fn foo2() {} + } + }; +} +foo!(); +"#, + &[Test, Test, Test, TestMod], + expect![[r#" + [ + Runnable { + use_name_in_title: true, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 210..217, + name: "foo0", + kind: Function, + }, + kind: Test { + test_id: Path( + "foo_tests::foo0", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + Runnable { + use_name_in_title: true, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 210..217, + name: "foo1", + kind: Function, + }, + kind: Test { + test_id: Path( + "foo_tests::foo1", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + Runnable { + use_name_in_title: true, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 210..217, + name: "foo2", + kind: Function, + }, + kind: Test { + test_id: Path( + "foo_tests::foo2", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + Runnable { + use_name_in_title: true, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 210..217, + name: "foo_tests", + kind: Module, + description: "mod foo_tests", + }, + kind: TestMod { + path: "foo_tests", + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn dont_recurse_in_outline_submodules() { + check( + r#" +//- /lib.rs +$0 +mod m; +//- /m.rs +mod tests { + #[test] + fn t() {} +} +"#, + &[], + expect![[r#" + [] + "#]], + ); + } + + #[test] + fn outline_submodule1() { + check( + r#" +//- /lib.rs +$0 +mod m; +//- /m.rs +#[test] +fn t0() {} +#[test] +fn t1() {} +"#, + &[TestMod], + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 1..7, + focus_range: 5..6, + name: "m", + kind: Module, + description: "mod m", + }, + kind: TestMod { + path: "m", + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn outline_submodule2() { + check( + r#" +//- /lib.rs +mod m; +//- /m.rs +$0 +#[test] +fn t0() {} +#[test] +fn t1() {} +"#, + &[TestMod, Test, Test], + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 1, + ), + full_range: 0..39, + name: "m", + kind: Module, + }, + kind: TestMod { + path: "m", + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 1, + ), + full_range: 1..19, + focus_range: 12..14, + name: "t0", + kind: Function, + }, + kind: Test { + test_id: Path( + "m::t0", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 1, + ), + full_range: 20..38, + focus_range: 31..33, + name: "t1", + kind: Function, + }, + kind: Test { + test_id: Path( + "m::t1", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn attributed_module() { + check( + r#" +//- proc_macros: identity +//- /lib.rs +$0 +#[proc_macros::identity] +mod module { + #[test] + fn t0() {} + #[test] + fn t1() {} +} +"#, + &[TestMod, Test, Test], + expect![[r#" + [ + Runnable { + use_name_in_title: true, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 26..94, + focus_range: 30..36, + name: "module", + kind: Module, + description: "mod module", + }, + kind: TestMod { + path: "module", + }, + cfg: None, + }, + Runnable { + use_name_in_title: true, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 43..65, + focus_range: 58..60, + name: "t0", + kind: Function, + }, + kind: Test { + test_id: Path( + "module::t0", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + Runnable { + use_name_in_title: true, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 70..92, + focus_range: 85..87, + name: "t1", + kind: Function, + }, + kind: Test { + test_id: Path( + "module::t1", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn find_no_tests() { + check_tests( + r#" +//- /lib.rs +fn foo$0() { }; +"#, + expect![[r#" + [] + "#]], + ); + } + + #[test] + fn find_direct_fn_test() { + check_tests( + r#" +//- /lib.rs +fn foo$0() { }; + +mod tests { + #[test] + fn foo_test() { + super::foo() + } +} +"#, + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 31..85, + focus_range: 46..54, + name: "foo_test", + kind: Function, + }, + kind: Test { + test_id: Path( + "tests::foo_test", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn find_direct_struct_test() { + check_tests( + r#" +//- /lib.rs +struct Fo$0o; +fn foo(arg: &Foo) { }; + +mod tests { + use super::*; + + #[test] + fn foo_test() { + foo(Foo); + } +} +"#, + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 71..122, + focus_range: 86..94, + name: "foo_test", + kind: Function, + }, + kind: Test { + test_id: Path( + "tests::foo_test", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn find_indirect_fn_test() { + check_tests( + r#" +//- /lib.rs +fn foo$0() { }; + +mod tests { + use super::foo; + + fn check1() { + check2() + } + + fn check2() { + foo() + } + + #[test] + fn foo_test() { + check1() + } +} +"#, + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 133..183, + focus_range: 148..156, + name: "foo_test", + kind: Function, + }, + kind: Test { + test_id: Path( + "tests::foo_test", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn tests_are_unique() { + check_tests( + r#" +//- /lib.rs +fn foo$0() { }; + +mod tests { + use super::foo; + + #[test] + fn foo_test() { + foo(); + foo(); + } + + #[test] + fn foo2_test() { + foo(); + foo(); + } + +} +"#, + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 52..115, + focus_range: 67..75, + name: "foo_test", + kind: Function, + }, + kind: Test { + test_id: Path( + "tests::foo_test", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 121..185, + focus_range: 136..145, + name: "foo2_test", + kind: Function, + }, + kind: Test { + test_id: Path( + "tests::foo2_test", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn doc_test_type_params() { + check( + r#" +//- /lib.rs +$0 +struct Foo<T, U>; + +impl<T, U> Foo<T, U> { + /// ```rust + /// ```` + fn t() {} +} +"#, + &[DocTest], + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 47..85, + name: "t", + }, + kind: DocTest { + test_id: Path( + "Foo<T, U>::t", + ), + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn doc_test_macro_export_mbe() { + check( + r#" +//- /lib.rs +$0 +mod foo; + +//- /foo.rs +/// ``` +/// fn foo() { +/// } +/// ``` +#[macro_export] +macro_rules! foo { + () => { + + }; +} +"#, + &[], + expect![[r#" + [] + "#]], + ); + check( + r#" +//- /lib.rs +$0 +/// ``` +/// fn foo() { +/// } +/// ``` +#[macro_export] +macro_rules! foo { + () => { + + }; +} +"#, + &[DocTest], + expect![[r#" + [ + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 1..94, + name: "foo", + }, + kind: DocTest { + test_id: Path( + "foo", + ), + }, + cfg: None, + }, + ] + "#]], + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/shuffle_crate_graph.rs b/src/tools/rust-analyzer/crates/ide/src/shuffle_crate_graph.rs new file mode 100644 index 000000000..15cb89dcc --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/shuffle_crate_graph.rs @@ -0,0 +1,71 @@ +use std::sync::Arc; + +use ide_db::{ + base_db::{salsa::Durability, CrateGraph, SourceDatabase}, + FxHashMap, RootDatabase, +}; + +// Feature: Shuffle Crate Graph +// +// Randomizes all crate IDs in the crate graph, for debugging. +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Shuffle Crate Graph** +// |=== +pub(crate) fn shuffle_crate_graph(db: &mut RootDatabase) { + let crate_graph = db.crate_graph(); + + let mut shuffled_ids = crate_graph.iter().collect::<Vec<_>>(); + shuffle(&mut shuffled_ids); + + let mut new_graph = CrateGraph::default(); + + let mut map = FxHashMap::default(); + for old_id in shuffled_ids.iter().copied() { + let data = &crate_graph[old_id]; + let new_id = new_graph.add_crate_root( + data.root_file_id, + data.edition, + data.display_name.clone(), + data.version.clone(), + data.cfg_options.clone(), + data.potential_cfg_options.clone(), + data.env.clone(), + data.proc_macro.clone(), + data.is_proc_macro, + data.origin.clone(), + ); + map.insert(old_id, new_id); + } + + for old_id in shuffled_ids.iter().copied() { + let data = &crate_graph[old_id]; + for dep in &data.dependencies { + let mut new_dep = dep.clone(); + new_dep.crate_id = map[&dep.crate_id]; + new_graph.add_dep(map[&old_id], new_dep).unwrap(); + } + } + + db.set_crate_graph_with_durability(Arc::new(new_graph), Durability::HIGH); +} + +fn shuffle<T>(slice: &mut [T]) { + let mut rng = oorandom::Rand32::new(seed()); + + let mut remaining = slice.len() - 1; + while remaining > 0 { + let index = rng.rand_range(0..remaining as u32); + slice.swap(remaining, index as usize); + remaining -= 1; + } +} + +fn seed() -> u64 { + use std::collections::hash_map::RandomState; + use std::hash::{BuildHasher, Hasher}; + + RandomState::new().build_hasher().finish() +} diff --git a/src/tools/rust-analyzer/crates/ide/src/signature_help.rs b/src/tools/rust-analyzer/crates/ide/src/signature_help.rs new file mode 100644 index 000000000..fedc1a435 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/signature_help.rs @@ -0,0 +1,1334 @@ +//! This module provides primitives for showing type and function parameter information when editing +//! a call or use-site. + +use std::collections::BTreeSet; + +use either::Either; +use hir::{AssocItem, GenericParam, HasAttrs, HirDisplay, Semantics, Trait}; +use ide_db::{active_parameter::callable_for_node, base_db::FilePosition}; +use stdx::format_to; +use syntax::{ + algo, + ast::{self, HasArgList}, + match_ast, AstNode, Direction, SyntaxToken, TextRange, TextSize, +}; + +use crate::RootDatabase; + +/// Contains information about an item signature as seen from a use site. +/// +/// This includes the "active parameter", which is the parameter whose value is currently being +/// edited. +#[derive(Debug)] +pub struct SignatureHelp { + pub doc: Option<String>, + pub signature: String, + pub active_parameter: Option<usize>, + parameters: Vec<TextRange>, +} + +impl SignatureHelp { + pub fn parameter_labels(&self) -> impl Iterator<Item = &str> + '_ { + self.parameters.iter().map(move |&it| &self.signature[it]) + } + + pub fn parameter_ranges(&self) -> &[TextRange] { + &self.parameters + } + + fn push_call_param(&mut self, param: &str) { + self.push_param('(', param); + } + + fn push_generic_param(&mut self, param: &str) { + self.push_param('<', param); + } + + fn push_param(&mut self, opening_delim: char, param: &str) { + if !self.signature.ends_with(opening_delim) { + self.signature.push_str(", "); + } + let start = TextSize::of(&self.signature); + self.signature.push_str(param); + let end = TextSize::of(&self.signature); + self.parameters.push(TextRange::new(start, end)) + } +} + +/// Computes parameter information for the given position. +pub(crate) fn signature_help(db: &RootDatabase, position: FilePosition) -> Option<SignatureHelp> { + let sema = Semantics::new(db); + let file = sema.parse(position.file_id); + let file = file.syntax(); + let token = file + .token_at_offset(position.offset) + .left_biased() + // if the cursor is sandwiched between two space tokens and the call is unclosed + // this prevents us from leaving the CallExpression + .and_then(|tok| algo::skip_trivia_token(tok, Direction::Prev))?; + let token = sema.descend_into_macros_single(token); + + for node in token.parent_ancestors() { + match_ast! { + match node { + ast::ArgList(arg_list) => { + let cursor_outside = arg_list.r_paren_token().as_ref() == Some(&token); + if cursor_outside { + return None; + } + return signature_help_for_call(&sema, token); + }, + ast::GenericArgList(garg_list) => { + let cursor_outside = garg_list.r_angle_token().as_ref() == Some(&token); + if cursor_outside { + return None; + } + return signature_help_for_generics(&sema, token); + }, + _ => (), + } + } + } + + None +} + +fn signature_help_for_call( + sema: &Semantics<'_, RootDatabase>, + token: SyntaxToken, +) -> Option<SignatureHelp> { + // Find the calling expression and its NameRef + let mut node = token.parent()?; + let calling_node = loop { + if let Some(callable) = ast::CallableExpr::cast(node.clone()) { + if callable + .arg_list() + .map_or(false, |it| it.syntax().text_range().contains(token.text_range().start())) + { + break callable; + } + } + + // Stop at multi-line expressions, since the signature of the outer call is not very + // helpful inside them. + if let Some(expr) = ast::Expr::cast(node.clone()) { + if expr.syntax().text().contains_char('\n') { + return None; + } + } + + node = node.parent()?; + }; + + let (callable, active_parameter) = callable_for_node(sema, &calling_node, &token)?; + + let mut res = + SignatureHelp { doc: None, signature: String::new(), parameters: vec![], active_parameter }; + + let db = sema.db; + let mut fn_params = None; + match callable.kind() { + hir::CallableKind::Function(func) => { + res.doc = func.docs(db).map(|it| it.into()); + format_to!(res.signature, "fn {}", func.name(db)); + fn_params = Some(match callable.receiver_param(db) { + Some(_self) => func.params_without_self(db), + None => func.assoc_fn_params(db), + }); + } + hir::CallableKind::TupleStruct(strukt) => { + res.doc = strukt.docs(db).map(|it| it.into()); + format_to!(res.signature, "struct {}", strukt.name(db)); + } + hir::CallableKind::TupleEnumVariant(variant) => { + res.doc = variant.docs(db).map(|it| it.into()); + format_to!( + res.signature, + "enum {}::{}", + variant.parent_enum(db).name(db), + variant.name(db) + ); + } + hir::CallableKind::Closure | hir::CallableKind::FnPtr => (), + } + + res.signature.push('('); + { + if let Some(self_param) = callable.receiver_param(db) { + format_to!(res.signature, "{}", self_param) + } + let mut buf = String::new(); + for (idx, (pat, ty)) in callable.params(db).into_iter().enumerate() { + buf.clear(); + if let Some(pat) = pat { + match pat { + Either::Left(_self) => format_to!(buf, "self: "), + Either::Right(pat) => format_to!(buf, "{}: ", pat), + } + } + // APITs (argument position `impl Trait`s) are inferred as {unknown} as the user is + // in the middle of entering call arguments. + // In that case, fall back to render definitions of the respective parameters. + // This is overly conservative: we do not substitute known type vars + // (see FIXME in tests::impl_trait) and falling back on any unknowns. + match (ty.contains_unknown(), fn_params.as_deref()) { + (true, Some(fn_params)) => format_to!(buf, "{}", fn_params[idx].ty().display(db)), + _ => format_to!(buf, "{}", ty.display(db)), + } + res.push_call_param(&buf); + } + } + res.signature.push(')'); + + let mut render = |ret_type: hir::Type| { + if !ret_type.is_unit() { + format_to!(res.signature, " -> {}", ret_type.display(db)); + } + }; + match callable.kind() { + hir::CallableKind::Function(func) if callable.return_type().contains_unknown() => { + render(func.ret_type(db)) + } + hir::CallableKind::Function(_) | hir::CallableKind::Closure | hir::CallableKind::FnPtr => { + render(callable.return_type()) + } + hir::CallableKind::TupleStruct(_) | hir::CallableKind::TupleEnumVariant(_) => {} + } + Some(res) +} + +fn signature_help_for_generics( + sema: &Semantics<'_, RootDatabase>, + token: SyntaxToken, +) -> Option<SignatureHelp> { + let parent = token.parent()?; + let arg_list = parent + .ancestors() + .filter_map(ast::GenericArgList::cast) + .find(|list| list.syntax().text_range().contains(token.text_range().start()))?; + + let mut active_parameter = arg_list + .generic_args() + .take_while(|arg| arg.syntax().text_range().end() <= token.text_range().start()) + .count(); + + let first_arg_is_non_lifetime = arg_list + .generic_args() + .next() + .map_or(false, |arg| !matches!(arg, ast::GenericArg::LifetimeArg(_))); + + let mut generics_def = if let Some(path) = + arg_list.syntax().ancestors().find_map(ast::Path::cast) + { + let res = sema.resolve_path(&path)?; + let generic_def: hir::GenericDef = match res { + hir::PathResolution::Def(hir::ModuleDef::Adt(it)) => it.into(), + hir::PathResolution::Def(hir::ModuleDef::Function(it)) => it.into(), + hir::PathResolution::Def(hir::ModuleDef::Trait(it)) => it.into(), + hir::PathResolution::Def(hir::ModuleDef::TypeAlias(it)) => it.into(), + hir::PathResolution::Def(hir::ModuleDef::Variant(it)) => it.into(), + hir::PathResolution::Def(hir::ModuleDef::BuiltinType(_)) + | hir::PathResolution::Def(hir::ModuleDef::Const(_)) + | hir::PathResolution::Def(hir::ModuleDef::Macro(_)) + | hir::PathResolution::Def(hir::ModuleDef::Module(_)) + | hir::PathResolution::Def(hir::ModuleDef::Static(_)) => return None, + hir::PathResolution::BuiltinAttr(_) + | hir::PathResolution::ToolModule(_) + | hir::PathResolution::Local(_) + | hir::PathResolution::TypeParam(_) + | hir::PathResolution::ConstParam(_) + | hir::PathResolution::SelfType(_) + | hir::PathResolution::DeriveHelper(_) => return None, + }; + + generic_def + } else if let Some(method_call) = arg_list.syntax().parent().and_then(ast::MethodCallExpr::cast) + { + // recv.method::<$0>() + let method = sema.resolve_method_call(&method_call)?; + method.into() + } else { + return None; + }; + + let mut res = SignatureHelp { + doc: None, + signature: String::new(), + parameters: vec![], + active_parameter: None, + }; + + let db = sema.db; + match generics_def { + hir::GenericDef::Function(it) => { + res.doc = it.docs(db).map(|it| it.into()); + format_to!(res.signature, "fn {}", it.name(db)); + } + hir::GenericDef::Adt(hir::Adt::Enum(it)) => { + res.doc = it.docs(db).map(|it| it.into()); + format_to!(res.signature, "enum {}", it.name(db)); + } + hir::GenericDef::Adt(hir::Adt::Struct(it)) => { + res.doc = it.docs(db).map(|it| it.into()); + format_to!(res.signature, "struct {}", it.name(db)); + } + hir::GenericDef::Adt(hir::Adt::Union(it)) => { + res.doc = it.docs(db).map(|it| it.into()); + format_to!(res.signature, "union {}", it.name(db)); + } + hir::GenericDef::Trait(it) => { + res.doc = it.docs(db).map(|it| it.into()); + format_to!(res.signature, "trait {}", it.name(db)); + } + hir::GenericDef::TypeAlias(it) => { + res.doc = it.docs(db).map(|it| it.into()); + format_to!(res.signature, "type {}", it.name(db)); + } + hir::GenericDef::Variant(it) => { + // In paths, generics of an enum can be specified *after* one of its variants. + // eg. `None::<u8>` + // We'll use the signature of the enum, but include the docs of the variant. + res.doc = it.docs(db).map(|it| it.into()); + let it = it.parent_enum(db); + format_to!(res.signature, "enum {}", it.name(db)); + generics_def = it.into(); + } + // These don't have generic args that can be specified + hir::GenericDef::Impl(_) | hir::GenericDef::Const(_) => return None, + } + + let params = generics_def.params(sema.db); + let num_lifetime_params = + params.iter().take_while(|param| matches!(param, GenericParam::LifetimeParam(_))).count(); + if first_arg_is_non_lifetime { + // Lifetime parameters were omitted. + active_parameter += num_lifetime_params; + } + res.active_parameter = Some(active_parameter); + + res.signature.push('<'); + let mut buf = String::new(); + for param in params { + if let hir::GenericParam::TypeParam(ty) = param { + if ty.is_implicit(db) { + continue; + } + } + + buf.clear(); + format_to!(buf, "{}", param.display(db)); + res.push_generic_param(&buf); + } + if let hir::GenericDef::Trait(tr) = generics_def { + add_assoc_type_bindings(db, &mut res, tr, arg_list); + } + res.signature.push('>'); + + Some(res) +} + +fn add_assoc_type_bindings( + db: &RootDatabase, + res: &mut SignatureHelp, + tr: Trait, + args: ast::GenericArgList, +) { + if args.syntax().ancestors().find_map(ast::TypeBound::cast).is_none() { + // Assoc type bindings are only valid in type bound position. + return; + } + + let present_bindings = args + .generic_args() + .filter_map(|arg| match arg { + ast::GenericArg::AssocTypeArg(arg) => arg.name_ref().map(|n| n.to_string()), + _ => None, + }) + .collect::<BTreeSet<_>>(); + + let mut buf = String::new(); + for binding in &present_bindings { + buf.clear(); + format_to!(buf, "{} = …", binding); + res.push_generic_param(&buf); + } + + for item in tr.items_with_supertraits(db) { + if let AssocItem::TypeAlias(ty) = item { + let name = ty.name(db).to_smol_str(); + if !present_bindings.contains(&*name) { + buf.clear(); + format_to!(buf, "{} = …", name); + res.push_generic_param(&buf); + } + } + } +} + +#[cfg(test)] +mod tests { + use std::iter; + + use expect_test::{expect, Expect}; + use ide_db::base_db::{fixture::ChangeFixture, FilePosition}; + use stdx::format_to; + + use crate::RootDatabase; + + /// Creates analysis from a multi-file fixture, returns positions marked with $0. + pub(crate) fn position(ra_fixture: &str) -> (RootDatabase, FilePosition) { + let change_fixture = ChangeFixture::parse(ra_fixture); + let mut database = RootDatabase::default(); + database.apply_change(change_fixture.change); + let (file_id, range_or_offset) = + change_fixture.file_position.expect("expected a marker ($0)"); + let offset = range_or_offset.expect_offset(); + (database, FilePosition { file_id, offset }) + } + + fn check(ra_fixture: &str, expect: Expect) { + // Implicitly add `Sized` to avoid noisy `T: ?Sized` in the results. + let fixture = format!( + r#" +#[lang = "sized"] trait Sized {{}} +{ra_fixture} + "# + ); + let (db, position) = position(&fixture); + let sig_help = crate::signature_help::signature_help(&db, position); + let actual = match sig_help { + Some(sig_help) => { + let mut rendered = String::new(); + if let Some(docs) = &sig_help.doc { + format_to!(rendered, "{}\n------\n", docs.as_str()); + } + format_to!(rendered, "{}\n", sig_help.signature); + let mut offset = 0; + for (i, range) in sig_help.parameter_ranges().iter().enumerate() { + let is_active = sig_help.active_parameter == Some(i); + + let start = u32::from(range.start()); + let gap = start.checked_sub(offset).unwrap_or_else(|| { + panic!("parameter ranges out of order: {:?}", sig_help.parameter_ranges()) + }); + rendered.extend(iter::repeat(' ').take(gap as usize)); + let param_text = &sig_help.signature[*range]; + let width = param_text.chars().count(); // … + let marker = if is_active { '^' } else { '-' }; + rendered.extend(iter::repeat(marker).take(width)); + offset += gap + u32::from(range.len()); + } + if !sig_help.parameter_ranges().is_empty() { + format_to!(rendered, "\n"); + } + rendered + } + None => String::new(), + }; + expect.assert_eq(&actual); + } + + #[test] + fn test_fn_signature_two_args() { + check( + r#" +fn foo(x: u32, y: u32) -> u32 {x + y} +fn bar() { foo($03, ); } +"#, + expect![[r#" + fn foo(x: u32, y: u32) -> u32 + ^^^^^^ ------ + "#]], + ); + check( + r#" +fn foo(x: u32, y: u32) -> u32 {x + y} +fn bar() { foo(3$0, ); } +"#, + expect![[r#" + fn foo(x: u32, y: u32) -> u32 + ^^^^^^ ------ + "#]], + ); + check( + r#" +fn foo(x: u32, y: u32) -> u32 {x + y} +fn bar() { foo(3,$0 ); } +"#, + expect![[r#" + fn foo(x: u32, y: u32) -> u32 + ------ ^^^^^^ + "#]], + ); + check( + r#" +fn foo(x: u32, y: u32) -> u32 {x + y} +fn bar() { foo(3, $0); } +"#, + expect![[r#" + fn foo(x: u32, y: u32) -> u32 + ------ ^^^^^^ + "#]], + ); + } + + #[test] + fn test_fn_signature_two_args_empty() { + check( + r#" +fn foo(x: u32, y: u32) -> u32 {x + y} +fn bar() { foo($0); } +"#, + expect![[r#" + fn foo(x: u32, y: u32) -> u32 + ^^^^^^ ------ + "#]], + ); + } + + #[test] + fn test_fn_signature_two_args_first_generics() { + check( + r#" +fn foo<T, U: Copy + Display>(x: T, y: U) -> u32 + where T: Copy + Display, U: Debug +{ x + y } + +fn bar() { foo($03, ); } +"#, + expect![[r#" + fn foo(x: i32, y: U) -> u32 + ^^^^^^ ---- + "#]], + ); + } + + #[test] + fn test_fn_signature_no_params() { + check( + r#" +fn foo<T>() -> T where T: Copy + Display {} +fn bar() { foo($0); } +"#, + expect![[r#" + fn foo() -> T + "#]], + ); + } + + #[test] + fn test_fn_signature_for_impl() { + check( + r#" +struct F; +impl F { pub fn new() { } } +fn bar() { + let _ : F = F::new($0); +} +"#, + expect![[r#" + fn new() + "#]], + ); + } + + #[test] + fn test_fn_signature_for_method_self() { + check( + r#" +struct S; +impl S { pub fn do_it(&self) {} } + +fn bar() { + let s: S = S; + s.do_it($0); +} +"#, + expect![[r#" + fn do_it(&self) + "#]], + ); + } + + #[test] + fn test_fn_signature_for_method_with_arg() { + check( + r#" +struct S; +impl S { + fn foo(&self, x: i32) {} +} + +fn main() { S.foo($0); } +"#, + expect![[r#" + fn foo(&self, x: i32) + ^^^^^^ + "#]], + ); + } + + #[test] + fn test_fn_signature_for_generic_method() { + check( + r#" +struct S<T>(T); +impl<T> S<T> { + fn foo(&self, x: T) {} +} + +fn main() { S(1u32).foo($0); } +"#, + expect![[r#" + fn foo(&self, x: u32) + ^^^^^^ + "#]], + ); + } + + #[test] + fn test_fn_signature_for_method_with_arg_as_assoc_fn() { + check( + r#" +struct S; +impl S { + fn foo(&self, x: i32) {} +} + +fn main() { S::foo($0); } +"#, + expect![[r#" + fn foo(self: &S, x: i32) + ^^^^^^^^ ------ + "#]], + ); + } + + #[test] + fn test_fn_signature_with_docs_simple() { + check( + r#" +/// test +// non-doc-comment +fn foo(j: u32) -> u32 { + j +} + +fn bar() { + let _ = foo($0); +} +"#, + expect![[r#" + test + ------ + fn foo(j: u32) -> u32 + ^^^^^^ + "#]], + ); + } + + #[test] + fn test_fn_signature_with_docs() { + check( + r#" +/// Adds one to the number given. +/// +/// # Examples +/// +/// ``` +/// let five = 5; +/// +/// assert_eq!(6, my_crate::add_one(5)); +/// ``` +pub fn add_one(x: i32) -> i32 { + x + 1 +} + +pub fn do() { + add_one($0 +}"#, + expect![[r##" + Adds one to the number given. + + # Examples + + ``` + let five = 5; + + assert_eq!(6, my_crate::add_one(5)); + ``` + ------ + fn add_one(x: i32) -> i32 + ^^^^^^ + "##]], + ); + } + + #[test] + fn test_fn_signature_with_docs_impl() { + check( + r#" +struct addr; +impl addr { + /// Adds one to the number given. + /// + /// # Examples + /// + /// ``` + /// let five = 5; + /// + /// assert_eq!(6, my_crate::add_one(5)); + /// ``` + pub fn add_one(x: i32) -> i32 { + x + 1 + } +} + +pub fn do_it() { + addr {}; + addr::add_one($0); +} +"#, + expect![[r##" + Adds one to the number given. + + # Examples + + ``` + let five = 5; + + assert_eq!(6, my_crate::add_one(5)); + ``` + ------ + fn add_one(x: i32) -> i32 + ^^^^^^ + "##]], + ); + } + + #[test] + fn test_fn_signature_with_docs_from_actix() { + check( + r#" +trait Actor { + /// Actor execution context type + type Context; +} +trait WriteHandler<E> +where + Self: Actor +{ + /// Method is called when writer finishes. + /// + /// By default this method stops actor's `Context`. + fn finished(&mut self, ctx: &mut Self::Context) {} +} + +fn foo(mut r: impl WriteHandler<()>) { + r.finished($0); +} +"#, + expect![[r#" + Method is called when writer finishes. + + By default this method stops actor's `Context`. + ------ + fn finished(&mut self, ctx: &mut <impl WriteHandler<()> as Actor>::Context) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + "#]], + ); + } + + #[test] + fn call_info_bad_offset() { + check( + r#" +fn foo(x: u32, y: u32) -> u32 {x + y} +fn bar() { foo $0 (3, ); } +"#, + expect![[""]], + ); + } + + #[test] + fn outside_of_arg_list() { + check( + r#" +fn foo(a: u8) {} +fn f() { + foo(123)$0 +} +"#, + expect![[]], + ); + check( + r#" +fn foo<T>(a: u8) {} +fn f() { + foo::<u32>$0() +} +"#, + expect![[]], + ); + } + + #[test] + fn test_nested_method_in_lambda() { + check( + r#" +struct Foo; +impl Foo { fn bar(&self, _: u32) { } } + +fn bar(_: u32) { } + +fn main() { + let foo = Foo; + std::thread::spawn(move || foo.bar($0)); +} +"#, + expect![[r#" + fn bar(&self, _: u32) + ^^^^^^ + "#]], + ); + } + + #[test] + fn works_for_tuple_structs() { + check( + r#" +/// A cool tuple struct +struct S(u32, i32); +fn main() { + let s = S(0, $0); +} +"#, + expect![[r#" + A cool tuple struct + ------ + struct S(u32, i32) + --- ^^^ + "#]], + ); + } + + #[test] + fn generic_struct() { + check( + r#" +struct S<T>(T); +fn main() { + let s = S($0); +} +"#, + expect![[r#" + struct S({unknown}) + ^^^^^^^^^ + "#]], + ); + } + + #[test] + fn works_for_enum_variants() { + check( + r#" +enum E { + /// A Variant + A(i32), + /// Another + B, + /// And C + C { a: i32, b: i32 } +} + +fn main() { + let a = E::A($0); +} +"#, + expect![[r#" + A Variant + ------ + enum E::A(i32) + ^^^ + "#]], + ); + } + + #[test] + fn cant_call_struct_record() { + check( + r#" +struct S { x: u32, y: i32 } +fn main() { + let s = S($0); +} +"#, + expect![[""]], + ); + } + + #[test] + fn cant_call_enum_record() { + check( + r#" +enum E { + /// A Variant + A(i32), + /// Another + B, + /// And C + C { a: i32, b: i32 } +} + +fn main() { + let a = E::C($0); +} +"#, + expect![[""]], + ); + } + + #[test] + fn fn_signature_for_call_in_macro() { + check( + r#" +macro_rules! id { ($($tt:tt)*) => { $($tt)* } } +fn foo() { } +id! { + fn bar() { foo($0); } +} +"#, + expect![[r#" + fn foo() + "#]], + ); + } + + #[test] + fn call_info_for_lambdas() { + check( + r#" +struct S; +fn foo(s: S) -> i32 { 92 } +fn main() { + (|s| foo(s))($0) +} + "#, + expect![[r#" + (s: S) -> i32 + ^^^^ + "#]], + ) + } + + #[test] + fn call_info_for_fn_ptr() { + check( + r#" +fn main(f: fn(i32, f64) -> char) { + f(0, $0) +} + "#, + expect![[r#" + (i32, f64) -> char + --- ^^^ + "#]], + ) + } + + #[test] + fn call_info_for_unclosed_call() { + check( + r#" +fn foo(foo: u32, bar: u32) {} +fn main() { + foo($0 +}"#, + expect![[r#" + fn foo(foo: u32, bar: u32) + ^^^^^^^^ -------- + "#]], + ); + // check with surrounding space + check( + r#" +fn foo(foo: u32, bar: u32) {} +fn main() { + foo( $0 +}"#, + expect![[r#" + fn foo(foo: u32, bar: u32) + ^^^^^^^^ -------- + "#]], + ) + } + + #[test] + fn test_multiline_argument() { + check( + r#" +fn callee(a: u8, b: u8) {} +fn main() { + callee(match 0 { + 0 => 1,$0 + }) +}"#, + expect![[r#""#]], + ); + check( + r#" +fn callee(a: u8, b: u8) {} +fn main() { + callee(match 0 { + 0 => 1, + },$0) +}"#, + expect![[r#" + fn callee(a: u8, b: u8) + ----- ^^^^^ + "#]], + ); + check( + r#" +fn callee(a: u8, b: u8) {} +fn main() { + callee($0match 0 { + 0 => 1, + }) +}"#, + expect![[r#" + fn callee(a: u8, b: u8) + ^^^^^ ----- + "#]], + ); + } + + #[test] + fn test_generics_simple() { + check( + r#" +/// Option docs. +enum Option<T> { + Some(T), + None, +} + +fn f() { + let opt: Option<$0 +} + "#, + expect![[r#" + Option docs. + ------ + enum Option<T> + ^ + "#]], + ); + } + + #[test] + fn test_generics_on_variant() { + check( + r#" +/// Option docs. +enum Option<T> { + /// Some docs. + Some(T), + /// None docs. + None, +} + +use Option::*; + +fn f() { + None::<$0 +} + "#, + expect![[r#" + None docs. + ------ + enum Option<T> + ^ + "#]], + ); + } + + #[test] + fn test_lots_of_generics() { + check( + r#" +trait Tr<T> {} + +struct S<T>(T); + +impl<T> S<T> { + fn f<G, H>(g: G, h: impl Tr<G>) where G: Tr<()> {} +} + +fn f() { + S::<u8>::f::<(), $0 +} + "#, + expect![[r#" + fn f<G: Tr<()>, H> + --------- ^ + "#]], + ); + } + + #[test] + fn test_generics_in_trait_ufcs() { + check( + r#" +trait Tr { + fn f<T: Tr, U>() {} +} + +struct S; + +impl Tr for S {} + +fn f() { + <S as Tr>::f::<$0 +} + "#, + expect![[r#" + fn f<T: Tr, U> + ^^^^^ - + "#]], + ); + } + + #[test] + fn test_generics_in_method_call() { + check( + r#" +struct S; + +impl S { + fn f<T>(&self) {} +} + +fn f() { + S.f::<$0 +} + "#, + expect![[r#" + fn f<T> + ^ + "#]], + ); + } + + #[test] + fn test_generic_param_in_method_call() { + check( + r#" +struct Foo; +impl Foo { + fn test<V>(&mut self, val: V) {} +} +fn sup() { + Foo.test($0) +} +"#, + expect![[r#" + fn test(&mut self, val: V) + ^^^^^^ + "#]], + ); + } + + #[test] + fn test_generic_kinds() { + check( + r#" +fn callee<'a, const A: u8, T, const C: u8>() {} + +fn f() { + callee::<'static, $0 +} + "#, + expect![[r#" + fn callee<'a, const A: u8, T, const C: u8> + -- ^^^^^^^^^^^ - ----------- + "#]], + ); + check( + r#" +fn callee<'a, const A: u8, T, const C: u8>() {} + +fn f() { + callee::<NON_LIFETIME$0 +} + "#, + expect![[r#" + fn callee<'a, const A: u8, T, const C: u8> + -- ^^^^^^^^^^^ - ----------- + "#]], + ); + } + + #[test] + fn test_trait_assoc_types() { + check( + r#" +trait Trait<'a, T> { + type Assoc; +} +fn f() -> impl Trait<(), $0 + "#, + expect![[r#" + trait Trait<'a, T, Assoc = …> + -- - ^^^^^^^^^ + "#]], + ); + check( + r#" +trait Iterator { + type Item; +} +fn f() -> impl Iterator<$0 + "#, + expect![[r#" + trait Iterator<Item = …> + ^^^^^^^^ + "#]], + ); + check( + r#" +trait Iterator { + type Item; +} +fn f() -> impl Iterator<Item = $0 + "#, + expect![[r#" + trait Iterator<Item = …> + ^^^^^^^^ + "#]], + ); + check( + r#" +trait Tr { + type A; + type B; +} +fn f() -> impl Tr<$0 + "#, + expect![[r#" + trait Tr<A = …, B = …> + ^^^^^ ----- + "#]], + ); + check( + r#" +trait Tr { + type A; + type B; +} +fn f() -> impl Tr<B$0 + "#, + expect![[r#" + trait Tr<A = …, B = …> + ^^^^^ ----- + "#]], + ); + check( + r#" +trait Tr { + type A; + type B; +} +fn f() -> impl Tr<B = $0 + "#, + expect![[r#" + trait Tr<B = …, A = …> + ^^^^^ ----- + "#]], + ); + check( + r#" +trait Tr { + type A; + type B; +} +fn f() -> impl Tr<B = (), $0 + "#, + expect![[r#" + trait Tr<B = …, A = …> + ----- ^^^^^ + "#]], + ); + } + + #[test] + fn test_supertrait_assoc() { + check( + r#" +trait Super { + type SuperTy; +} +trait Sub: Super + Super { + type SubTy; +} +fn f() -> impl Sub<$0 + "#, + expect![[r#" + trait Sub<SubTy = …, SuperTy = …> + ^^^^^^^^^ ----------- + "#]], + ); + } + + #[test] + fn no_assoc_types_outside_type_bounds() { + check( + r#" +trait Tr<T> { + type Assoc; +} + +impl Tr<$0 + "#, + expect![[r#" + trait Tr<T> + ^ + "#]], + ); + } + + #[test] + fn impl_trait() { + // FIXME: Substitute type vars in impl trait (`U` -> `i8`) + check( + r#" +trait Trait<T> {} +struct Wrap<T>(T); +fn foo<U>(x: Wrap<impl Trait<U>>) {} +fn f() { + foo::<i8>($0) +} +"#, + expect![[r#" + fn foo(x: Wrap<impl Trait<U>>) + ^^^^^^^^^^^^^^^^^^^^^^ + "#]], + ); + } + + #[test] + fn fully_qualified_syntax() { + check( + r#" +fn f() { + trait A { fn foo(&self, other: Self); } + A::foo(&self$0, other); +} +"#, + expect![[r#" + fn foo(self: &Self, other: Self) + ^^^^^^^^^^^ ----------- + "#]], + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/ssr.rs b/src/tools/rust-analyzer/crates/ide/src/ssr.rs new file mode 100644 index 000000000..497eb1cc1 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/ssr.rs @@ -0,0 +1,255 @@ +//! This module provides an SSR assist. It is not desirable to include this +//! assist in ide_assists because that would require the ide_assists crate +//! depend on the ide_ssr crate. + +use ide_assists::{Assist, AssistId, AssistKind, AssistResolveStrategy, GroupLabel}; +use ide_db::{base_db::FileRange, label::Label, source_change::SourceChange, RootDatabase}; + +pub(crate) fn ssr_assists( + db: &RootDatabase, + resolve: &AssistResolveStrategy, + frange: FileRange, +) -> Vec<Assist> { + let mut ssr_assists = Vec::with_capacity(2); + + let (match_finder, comment_range) = match ide_ssr::ssr_from_comment(db, frange) { + Some(ssr_data) => ssr_data, + None => return ssr_assists, + }; + let id = AssistId("ssr", AssistKind::RefactorRewrite); + + let (source_change_for_file, source_change_for_workspace) = if resolve.should_resolve(&id) { + let edits = match_finder.edits(); + + let source_change_for_file = { + let text_edit_for_file = edits.get(&frange.file_id).cloned().unwrap_or_default(); + SourceChange::from_text_edit(frange.file_id, text_edit_for_file) + }; + + let source_change_for_workspace = SourceChange::from(match_finder.edits()); + + (Some(source_change_for_file), Some(source_change_for_workspace)) + } else { + (None, None) + }; + + let assists = vec![ + ("Apply SSR in file", source_change_for_file), + ("Apply SSR in workspace", source_change_for_workspace), + ]; + + for (label, source_change) in assists.into_iter() { + let assist = Assist { + id, + label: Label::new(label.to_string()), + group: Some(GroupLabel("Apply SSR".into())), + target: comment_range, + source_change, + trigger_signature_help: false, + }; + + ssr_assists.push(assist); + } + + ssr_assists +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use expect_test::expect; + use ide_assists::{Assist, AssistResolveStrategy}; + use ide_db::{ + base_db::{fixture::WithFixture, salsa::Durability, FileRange}, + symbol_index::SymbolsDatabase, + FxHashSet, RootDatabase, + }; + + use super::ssr_assists; + + fn get_assists(ra_fixture: &str, resolve: AssistResolveStrategy) -> Vec<Assist> { + let (mut db, file_id, range_or_offset) = RootDatabase::with_range_or_offset(ra_fixture); + let mut local_roots = FxHashSet::default(); + local_roots.insert(ide_db::base_db::fixture::WORKSPACE); + db.set_local_roots_with_durability(Arc::new(local_roots), Durability::HIGH); + ssr_assists(&db, &resolve, FileRange { file_id, range: range_or_offset.into() }) + } + + #[test] + fn not_applicable_comment_not_ssr() { + let ra_fixture = r#" + //- /lib.rs + + // This is foo $0 + fn foo() {} + "#; + let assists = get_assists(ra_fixture, AssistResolveStrategy::All); + + assert_eq!(0, assists.len()); + } + + #[test] + fn resolve_edits_true() { + let assists = get_assists( + r#" + //- /lib.rs + mod bar; + + // 2 ==>> 3$0 + fn foo() { 2 } + + //- /bar.rs + fn bar() { 2 } + "#, + AssistResolveStrategy::All, + ); + + assert_eq!(2, assists.len()); + let mut assists = assists.into_iter(); + + let apply_in_file_assist = assists.next().unwrap(); + expect![[r#" + Assist { + id: AssistId( + "ssr", + RefactorRewrite, + ), + label: "Apply SSR in file", + group: Some( + GroupLabel( + "Apply SSR", + ), + ), + target: 10..21, + source_change: Some( + SourceChange { + source_file_edits: { + FileId( + 0, + ): TextEdit { + indels: [ + Indel { + insert: "3", + delete: 33..34, + }, + ], + }, + }, + file_system_edits: [], + is_snippet: false, + }, + ), + trigger_signature_help: false, + } + "#]] + .assert_debug_eq(&apply_in_file_assist); + + let apply_in_workspace_assist = assists.next().unwrap(); + expect![[r#" + Assist { + id: AssistId( + "ssr", + RefactorRewrite, + ), + label: "Apply SSR in workspace", + group: Some( + GroupLabel( + "Apply SSR", + ), + ), + target: 10..21, + source_change: Some( + SourceChange { + source_file_edits: { + FileId( + 0, + ): TextEdit { + indels: [ + Indel { + insert: "3", + delete: 33..34, + }, + ], + }, + FileId( + 1, + ): TextEdit { + indels: [ + Indel { + insert: "3", + delete: 11..12, + }, + ], + }, + }, + file_system_edits: [], + is_snippet: false, + }, + ), + trigger_signature_help: false, + } + "#]] + .assert_debug_eq(&apply_in_workspace_assist); + } + + #[test] + fn resolve_edits_false() { + let assists = get_assists( + r#" + //- /lib.rs + mod bar; + + // 2 ==>> 3$0 + fn foo() { 2 } + + //- /bar.rs + fn bar() { 2 } + "#, + AssistResolveStrategy::None, + ); + + assert_eq!(2, assists.len()); + let mut assists = assists.into_iter(); + + let apply_in_file_assist = assists.next().unwrap(); + expect![[r#" + Assist { + id: AssistId( + "ssr", + RefactorRewrite, + ), + label: "Apply SSR in file", + group: Some( + GroupLabel( + "Apply SSR", + ), + ), + target: 10..21, + source_change: None, + trigger_signature_help: false, + } + "#]] + .assert_debug_eq(&apply_in_file_assist); + + let apply_in_workspace_assist = assists.next().unwrap(); + expect![[r#" + Assist { + id: AssistId( + "ssr", + RefactorRewrite, + ), + label: "Apply SSR in workspace", + group: Some( + GroupLabel( + "Apply SSR", + ), + ), + target: 10..21, + source_change: None, + trigger_signature_help: false, + } + "#]] + .assert_debug_eq(&apply_in_workspace_assist); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/static_index.rs b/src/tools/rust-analyzer/crates/ide/src/static_index.rs new file mode 100644 index 000000000..d74b64041 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/static_index.rs @@ -0,0 +1,321 @@ +//! This module provides `StaticIndex` which is used for powering +//! read-only code browsers and emitting LSIF + +use std::collections::HashMap; + +use hir::{db::HirDatabase, Crate, Module, Semantics}; +use ide_db::{ + base_db::{FileId, FileRange, SourceDatabaseExt}, + defs::{Definition, IdentClass}, + FxHashSet, RootDatabase, +}; +use syntax::{AstNode, SyntaxKind::*, SyntaxToken, TextRange, T}; + +use crate::{ + hover::hover_for_definition, + moniker::{crate_for_file, def_to_moniker, MonikerResult}, + Analysis, Fold, HoverConfig, HoverDocFormat, HoverResult, InlayHint, InlayHintsConfig, + TryToNav, +}; + +/// A static representation of fully analyzed source code. +/// +/// The intended use-case is powering read-only code browsers and emitting LSIF +#[derive(Debug)] +pub struct StaticIndex<'a> { + pub files: Vec<StaticIndexedFile>, + pub tokens: TokenStore, + analysis: &'a Analysis, + db: &'a RootDatabase, + def_map: HashMap<Definition, TokenId>, +} + +#[derive(Debug)] +pub struct ReferenceData { + pub range: FileRange, + pub is_definition: bool, +} + +#[derive(Debug)] +pub struct TokenStaticData { + pub hover: Option<HoverResult>, + pub definition: Option<FileRange>, + pub references: Vec<ReferenceData>, + pub moniker: Option<MonikerResult>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TokenId(usize); + +impl TokenId { + pub fn raw(self) -> usize { + self.0 + } +} + +#[derive(Default, Debug)] +pub struct TokenStore(Vec<TokenStaticData>); + +impl TokenStore { + pub fn insert(&mut self, data: TokenStaticData) -> TokenId { + let id = TokenId(self.0.len()); + self.0.push(data); + id + } + + pub fn get_mut(&mut self, id: TokenId) -> Option<&mut TokenStaticData> { + self.0.get_mut(id.0) + } + + pub fn get(&self, id: TokenId) -> Option<&TokenStaticData> { + self.0.get(id.0) + } + + pub fn iter(self) -> impl Iterator<Item = (TokenId, TokenStaticData)> { + self.0.into_iter().enumerate().map(|(i, x)| (TokenId(i), x)) + } +} + +#[derive(Debug)] +pub struct StaticIndexedFile { + pub file_id: FileId, + pub folds: Vec<Fold>, + pub inlay_hints: Vec<InlayHint>, + pub tokens: Vec<(TextRange, TokenId)>, +} + +fn all_modules(db: &dyn HirDatabase) -> Vec<Module> { + let mut worklist: Vec<_> = + Crate::all(db).into_iter().map(|krate| krate.root_module(db)).collect(); + let mut modules = Vec::new(); + + while let Some(module) = worklist.pop() { + modules.push(module); + worklist.extend(module.children(db)); + } + + modules +} + +impl StaticIndex<'_> { + fn add_file(&mut self, file_id: FileId) { + let current_crate = crate_for_file(self.db, file_id); + let folds = self.analysis.folding_ranges(file_id).unwrap(); + let inlay_hints = self + .analysis + .inlay_hints( + &InlayHintsConfig { + render_colons: true, + type_hints: true, + parameter_hints: true, + chaining_hints: true, + closure_return_type_hints: crate::ClosureReturnTypeHints::WithBlock, + lifetime_elision_hints: crate::LifetimeElisionHints::Never, + reborrow_hints: crate::ReborrowHints::Never, + hide_named_constructor_hints: false, + hide_closure_initialization_hints: false, + param_names_for_lifetime_elision_hints: false, + binding_mode_hints: false, + max_length: Some(25), + closing_brace_hints_min_lines: Some(25), + }, + file_id, + None, + ) + .unwrap(); + // hovers + let sema = hir::Semantics::new(self.db); + let tokens_or_nodes = sema.parse(file_id).syntax().clone(); + let tokens = tokens_or_nodes.descendants_with_tokens().filter_map(|x| match x { + syntax::NodeOrToken::Node(_) => None, + syntax::NodeOrToken::Token(x) => Some(x), + }); + let hover_config = + HoverConfig { links_in_hover: true, documentation: Some(HoverDocFormat::Markdown) }; + let tokens = tokens.filter(|token| { + matches!( + token.kind(), + IDENT | INT_NUMBER | LIFETIME_IDENT | T![self] | T![super] | T![crate] | T![Self] + ) + }); + let mut result = StaticIndexedFile { file_id, inlay_hints, folds, tokens: vec![] }; + for token in tokens { + let range = token.text_range(); + let node = token.parent().unwrap(); + let def = match get_definition(&sema, token.clone()) { + Some(x) => x, + None => continue, + }; + let id = if let Some(x) = self.def_map.get(&def) { + *x + } else { + let x = self.tokens.insert(TokenStaticData { + hover: hover_for_definition(&sema, file_id, def, &node, &hover_config), + definition: def + .try_to_nav(self.db) + .map(|x| FileRange { file_id: x.file_id, range: x.focus_or_full_range() }), + references: vec![], + moniker: current_crate.and_then(|cc| def_to_moniker(self.db, def, cc)), + }); + self.def_map.insert(def, x); + x + }; + let token = self.tokens.get_mut(id).unwrap(); + token.references.push(ReferenceData { + range: FileRange { range, file_id }, + is_definition: match def.try_to_nav(self.db) { + Some(x) => x.file_id == file_id && x.focus_or_full_range() == range, + None => false, + }, + }); + result.tokens.push((range, id)); + } + self.files.push(result); + } + + pub fn compute(analysis: &Analysis) -> StaticIndex<'_> { + let db = &*analysis.db; + let work = all_modules(db).into_iter().filter(|module| { + let file_id = module.definition_source(db).file_id.original_file(db); + let source_root = db.file_source_root(file_id); + let source_root = db.source_root(source_root); + !source_root.is_library + }); + let mut this = StaticIndex { + files: vec![], + tokens: Default::default(), + analysis, + db, + def_map: Default::default(), + }; + let mut visited_files = FxHashSet::default(); + for module in work { + let file_id = module.definition_source(db).file_id.original_file(db); + if visited_files.contains(&file_id) { + continue; + } + this.add_file(file_id); + // mark the file + visited_files.insert(file_id); + } + this + } +} + +fn get_definition(sema: &Semantics<'_, RootDatabase>, token: SyntaxToken) -> Option<Definition> { + for token in sema.descend_into_macros(token) { + let def = IdentClass::classify_token(sema, &token).map(IdentClass::definitions); + if let Some(&[x]) = def.as_deref() { + return Some(x); + } else { + continue; + }; + } + None +} + +#[cfg(test)] +mod tests { + use crate::{fixture, StaticIndex}; + use ide_db::base_db::FileRange; + use std::collections::HashSet; + use syntax::TextSize; + + fn check_all_ranges(ra_fixture: &str) { + let (analysis, ranges) = fixture::annotations_without_marker(ra_fixture); + let s = StaticIndex::compute(&analysis); + let mut range_set: HashSet<_> = ranges.iter().map(|x| x.0).collect(); + for f in s.files { + for (range, _) in f.tokens { + let x = FileRange { file_id: f.file_id, range }; + if !range_set.contains(&x) { + panic!("additional range {:?}", x); + } + range_set.remove(&x); + } + } + if !range_set.is_empty() { + panic!("unfound ranges {:?}", range_set); + } + } + + fn check_definitions(ra_fixture: &str) { + let (analysis, ranges) = fixture::annotations_without_marker(ra_fixture); + let s = StaticIndex::compute(&analysis); + let mut range_set: HashSet<_> = ranges.iter().map(|x| x.0).collect(); + for (_, t) in s.tokens.iter() { + if let Some(x) = t.definition { + if x.range.start() == TextSize::from(0) { + // ignore definitions that are whole of file + continue; + } + if !range_set.contains(&x) { + panic!("additional definition {:?}", x); + } + range_set.remove(&x); + } + } + if !range_set.is_empty() { + panic!("unfound definitions {:?}", range_set); + } + } + + #[test] + fn struct_and_enum() { + check_all_ranges( + r#" +struct Foo; + //^^^ +enum E { X(Foo) } + //^ ^ ^^^ +"#, + ); + check_definitions( + r#" +struct Foo; + //^^^ +enum E { X(Foo) } + //^ ^ +"#, + ); + } + + #[test] + fn multi_crate() { + check_definitions( + r#" +//- /main.rs crate:main deps:foo + + +use foo::func; + +fn main() { + //^^^^ + func(); +} +//- /foo/lib.rs crate:foo + +pub func() { + +} +"#, + ); + } + + #[test] + fn derives() { + check_all_ranges( + r#" +//- minicore:derive +#[rustc_builtin_macro] +//^^^^^^^^^^^^^^^^^^^ +pub macro Copy {} + //^^^^ +#[derive(Copy)] +//^^^^^^ ^^^^ +struct Hello(i32); + //^^^^^ ^^^ +"#, + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/status.rs b/src/tools/rust-analyzer/crates/ide/src/status.rs new file mode 100644 index 000000000..3191870eb --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/status.rs @@ -0,0 +1,164 @@ +use std::{fmt, iter::FromIterator, sync::Arc}; + +use hir::{ExpandResult, MacroFile}; +use ide_db::base_db::{ + salsa::debug::{DebugQueryTable, TableEntry}, + CrateId, FileId, FileTextQuery, SourceDatabase, SourceRootId, +}; +use ide_db::{ + symbol_index::{LibrarySymbolsQuery, SymbolIndex}, + RootDatabase, +}; +use itertools::Itertools; +use profile::{memory_usage, Bytes}; +use std::env; +use stdx::format_to; +use syntax::{ast, Parse, SyntaxNode}; + +fn syntax_tree_stats(db: &RootDatabase) -> SyntaxTreeStats { + ide_db::base_db::ParseQuery.in_db(db).entries::<SyntaxTreeStats>() +} +fn macro_syntax_tree_stats(db: &RootDatabase) -> SyntaxTreeStats { + hir::db::ParseMacroExpansionQuery.in_db(db).entries::<SyntaxTreeStats>() +} + +// Feature: Status +// +// Shows internal statistic about memory usage of rust-analyzer. +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Status** +// |=== +// image::https://user-images.githubusercontent.com/48062697/113065584-05f34500-91b1-11eb-98cc-5c196f76be7f.gif[] +pub(crate) fn status(db: &RootDatabase, file_id: Option<FileId>) -> String { + let mut buf = String::new(); + format_to!(buf, "{}\n", FileTextQuery.in_db(db).entries::<FilesStats>()); + format_to!(buf, "{}\n", LibrarySymbolsQuery.in_db(db).entries::<LibrarySymbolsStats>()); + format_to!(buf, "{}\n", syntax_tree_stats(db)); + format_to!(buf, "{} (Macros)\n", macro_syntax_tree_stats(db)); + format_to!(buf, "{} in total\n", memory_usage()); + if env::var("RA_COUNT").is_ok() { + format_to!(buf, "\nCounts:\n{}", profile::countme::get_all()); + } + + if let Some(file_id) = file_id { + format_to!(buf, "\nFile info:\n"); + let crates = crate::parent_module::crate_for(db, file_id); + if crates.is_empty() { + format_to!(buf, "Does not belong to any crate"); + } + let crate_graph = db.crate_graph(); + for krate in crates { + let display_crate = |krate: CrateId| match &crate_graph[krate].display_name { + Some(it) => format!("{}({:?})", it, krate), + None => format!("{:?}", krate), + }; + format_to!(buf, "Crate: {}\n", display_crate(krate)); + let deps = crate_graph[krate] + .dependencies + .iter() + .map(|dep| format!("{}={:?}", dep.name, dep.crate_id)) + .format(", "); + format_to!(buf, "Dependencies: {}\n", deps); + } + } + + buf.trim().to_string() +} + +#[derive(Default)] +struct FilesStats { + total: usize, + size: Bytes, +} + +impl fmt::Display for FilesStats { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(fmt, "{} of files", self.size) + } +} + +impl FromIterator<TableEntry<FileId, Arc<String>>> for FilesStats { + fn from_iter<T>(iter: T) -> FilesStats + where + T: IntoIterator<Item = TableEntry<FileId, Arc<String>>>, + { + let mut res = FilesStats::default(); + for entry in iter { + res.total += 1; + res.size += entry.value.unwrap().len(); + } + res + } +} + +#[derive(Default)] +pub(crate) struct SyntaxTreeStats { + total: usize, + pub(crate) retained: usize, +} + +impl fmt::Display for SyntaxTreeStats { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(fmt, "{} trees, {} preserved", self.total, self.retained) + } +} + +impl FromIterator<TableEntry<FileId, Parse<ast::SourceFile>>> for SyntaxTreeStats { + fn from_iter<T>(iter: T) -> SyntaxTreeStats + where + T: IntoIterator<Item = TableEntry<FileId, Parse<ast::SourceFile>>>, + { + let mut res = SyntaxTreeStats::default(); + for entry in iter { + res.total += 1; + res.retained += entry.value.is_some() as usize; + } + res + } +} + +impl<M> FromIterator<TableEntry<MacroFile, ExpandResult<Option<(Parse<SyntaxNode>, M)>>>> + for SyntaxTreeStats +{ + fn from_iter<T>(iter: T) -> SyntaxTreeStats + where + T: IntoIterator<Item = TableEntry<MacroFile, ExpandResult<Option<(Parse<SyntaxNode>, M)>>>>, + { + let mut res = SyntaxTreeStats::default(); + for entry in iter { + res.total += 1; + res.retained += entry.value.is_some() as usize; + } + res + } +} + +#[derive(Default)] +struct LibrarySymbolsStats { + total: usize, + size: Bytes, +} + +impl fmt::Display for LibrarySymbolsStats { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(fmt, "{} of index symbols ({})", self.size, self.total) + } +} + +impl FromIterator<TableEntry<SourceRootId, Arc<SymbolIndex>>> for LibrarySymbolsStats { + fn from_iter<T>(iter: T) -> LibrarySymbolsStats + where + T: IntoIterator<Item = TableEntry<SourceRootId, Arc<SymbolIndex>>>, + { + let mut res = LibrarySymbolsStats::default(); + for entry in iter { + let symbols = entry.value.unwrap(); + res.total += symbols.len(); + res.size += symbols.memory_size(); + } + res + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting.rs b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting.rs new file mode 100644 index 000000000..3fb49b45d --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting.rs @@ -0,0 +1,449 @@ +pub(crate) mod tags; + +mod highlights; +mod injector; + +mod highlight; +mod format; +mod macro_; +mod inject; +mod escape; + +mod html; +#[cfg(test)] +mod tests; + +use hir::{Name, Semantics}; +use ide_db::{FxHashMap, RootDatabase}; +use syntax::{ + ast, AstNode, AstToken, NodeOrToken, SyntaxKind::*, SyntaxNode, TextRange, WalkEvent, T, +}; + +use crate::{ + syntax_highlighting::{ + escape::highlight_escape_string, format::highlight_format_string, highlights::Highlights, + macro_::MacroHighlighter, tags::Highlight, + }, + FileId, HlMod, HlTag, +}; + +pub(crate) use html::highlight_as_html; + +#[derive(Debug, Clone, Copy)] +pub struct HlRange { + pub range: TextRange, + pub highlight: Highlight, + pub binding_hash: Option<u64>, +} + +// Feature: Semantic Syntax Highlighting +// +// rust-analyzer highlights the code semantically. +// For example, `Bar` in `foo::Bar` might be colored differently depending on whether `Bar` is an enum or a trait. +// rust-analyzer does not specify colors directly, instead it assigns a tag (like `struct`) and a set of modifiers (like `declaration`) to each token. +// It's up to the client to map those to specific colors. +// +// The general rule is that a reference to an entity gets colored the same way as the entity itself. +// We also give special modifier for `mut` and `&mut` local variables. +// +// +// .Token Tags +// +// Rust-analyzer currently emits the following token tags: +// +// - For items: +// + +// [horizontal] +// attribute:: Emitted for attribute macros. +// enum:: Emitted for enums. +// function:: Emitted for free-standing functions. +// derive:: Emitted for derive macros. +// macro:: Emitted for function-like macros. +// method:: Emitted for associated functions, also knowns as methods. +// namespace:: Emitted for modules. +// struct:: Emitted for structs. +// trait:: Emitted for traits. +// typeAlias:: Emitted for type aliases and `Self` in `impl`s. +// union:: Emitted for unions. +// +// - For literals: +// + +// [horizontal] +// boolean:: Emitted for the boolean literals `true` and `false`. +// character:: Emitted for character literals. +// number:: Emitted for numeric literals. +// string:: Emitted for string literals. +// escapeSequence:: Emitted for escaped sequences inside strings like `\n`. +// formatSpecifier:: Emitted for format specifiers `{:?}` in `format!`-like macros. +// +// - For operators: +// + +// [horizontal] +// operator:: Emitted for general operators. +// arithmetic:: Emitted for the arithmetic operators `+`, `-`, `*`, `/`, `+=`, `-=`, `*=`, `/=`. +// bitwise:: Emitted for the bitwise operators `|`, `&`, `!`, `^`, `|=`, `&=`, `^=`. +// comparison:: Emitted for the comparison operators `>`, `<`, `==`, `>=`, `<=`, `!=`. +// logical:: Emitted for the logical operators `||`, `&&`, `!`. +// +// - For punctuation: +// + +// [horizontal] +// punctuation:: Emitted for general punctuation. +// attributeBracket:: Emitted for attribute invocation brackets, that is the `#[` and `]` tokens. +// angle:: Emitted for `<>` angle brackets. +// brace:: Emitted for `{}` braces. +// bracket:: Emitted for `[]` brackets. +// parenthesis:: Emitted for `()` parentheses. +// colon:: Emitted for the `:` token. +// comma:: Emitted for the `,` token. +// dot:: Emitted for the `.` token. +// semi:: Emitted for the `;` token. +// macroBang:: Emitted for the `!` token in macro calls. +// +// //- +// +// [horizontal] +// builtinAttribute:: Emitted for names to builtin attributes in attribute path, the `repr` in `#[repr(u8)]` for example. +// builtinType:: Emitted for builtin types like `u32`, `str` and `f32`. +// comment:: Emitted for comments. +// constParameter:: Emitted for const parameters. +// deriveHelper:: Emitted for derive helper attributes. +// enumMember:: Emitted for enum variants. +// generic:: Emitted for generic tokens that have no mapping. +// keyword:: Emitted for keywords. +// label:: Emitted for labels. +// lifetime:: Emitted for lifetimes. +// parameter:: Emitted for non-self function parameters. +// property:: Emitted for struct and union fields. +// selfKeyword:: Emitted for the self function parameter and self path-specifier. +// selfTypeKeyword:: Emitted for the Self type parameter. +// toolModule:: Emitted for tool modules. +// typeParameter:: Emitted for type parameters. +// unresolvedReference:: Emitted for unresolved references, names that rust-analyzer can't find the definition of. +// variable:: Emitted for locals, constants and statics. +// +// +// .Token Modifiers +// +// Token modifiers allow to style some elements in the source code more precisely. +// +// Rust-analyzer currently emits the following token modifiers: +// +// [horizontal] +// async:: Emitted for async functions and the `async` and `await` keywords. +// attribute:: Emitted for tokens inside attributes. +// callable:: Emitted for locals whose types implements one of the `Fn*` traits. +// constant:: Emitted for consts. +// consuming:: Emitted for locals that are being consumed when use in a function call. +// controlFlow:: Emitted for control-flow related tokens, this includes the `?` operator. +// crateRoot:: Emitted for crate names, like `serde` and `crate`. +// declaration:: Emitted for names of definitions, like `foo` in `fn foo() {}`. +// defaultLibrary:: Emitted for items from built-in crates (std, core, alloc, test and proc_macro). +// documentation:: Emitted for documentation comments. +// injected:: Emitted for doc-string injected highlighting like rust source blocks in documentation. +// intraDocLink:: Emitted for intra doc links in doc-strings. +// library:: Emitted for items that are defined outside of the current crate. +// mutable:: Emitted for mutable locals and statics as well as functions taking `&mut self`. +// public:: Emitted for items that are from the current crate and are `pub`. +// reference:: Emitted for locals behind a reference and functions taking `self` by reference. +// static:: Emitted for "static" functions, also known as functions that do not take a `self` param, as well as statics and consts. +// trait:: Emitted for associated trait items. +// unsafe:: Emitted for unsafe operations, like unsafe function calls, as well as the `unsafe` token. +// +// +// image::https://user-images.githubusercontent.com/48062697/113164457-06cfb980-9239-11eb-819b-0f93e646acf8.png[] +// image::https://user-images.githubusercontent.com/48062697/113187625-f7f50100-9250-11eb-825e-91c58f236071.png[] +pub(crate) fn highlight( + db: &RootDatabase, + file_id: FileId, + range_to_highlight: Option<TextRange>, + syntactic_name_ref_highlighting: bool, +) -> Vec<HlRange> { + let _p = profile::span("highlight"); + let sema = Semantics::new(db); + + // Determine the root based on the given range. + let (root, range_to_highlight) = { + let source_file = sema.parse(file_id); + let source_file = source_file.syntax(); + match range_to_highlight { + Some(range) => { + let node = match source_file.covering_element(range) { + NodeOrToken::Node(it) => it, + NodeOrToken::Token(it) => it.parent().unwrap_or_else(|| source_file.clone()), + }; + (node, range) + } + None => (source_file.clone(), source_file.text_range()), + } + }; + + let mut hl = highlights::Highlights::new(root.text_range()); + let krate = match sema.scope(&root) { + Some(it) => it.krate(), + None => return hl.to_vec(), + }; + traverse( + &mut hl, + &sema, + file_id, + &root, + krate, + range_to_highlight, + syntactic_name_ref_highlighting, + ); + hl.to_vec() +} + +fn traverse( + hl: &mut Highlights, + sema: &Semantics<'_, RootDatabase>, + file_id: FileId, + root: &SyntaxNode, + krate: hir::Crate, + range_to_highlight: TextRange, + syntactic_name_ref_highlighting: bool, +) { + let is_unlinked = sema.to_module_def(file_id).is_none(); + let mut bindings_shadow_count: FxHashMap<Name, u32> = FxHashMap::default(); + + enum AttrOrDerive { + Attr(ast::Item), + Derive(ast::Item), + } + + impl AttrOrDerive { + fn item(&self) -> &ast::Item { + match self { + AttrOrDerive::Attr(item) | AttrOrDerive::Derive(item) => item, + } + } + } + + let mut tt_level = 0; + let mut attr_or_derive_item = None; + let mut current_macro: Option<ast::Macro> = None; + let mut macro_highlighter = MacroHighlighter::default(); + let mut inside_attribute = false; + + // Walk all nodes, keeping track of whether we are inside a macro or not. + // If in macro, expand it first and highlight the expanded code. + for event in root.preorder_with_tokens() { + use WalkEvent::{Enter, Leave}; + + let range = match &event { + Enter(it) | Leave(it) => it.text_range(), + }; + + // Element outside of the viewport, no need to highlight + if range_to_highlight.intersect(range).is_none() { + continue; + } + + // set macro and attribute highlighting states + match event.clone() { + Enter(NodeOrToken::Node(node)) if ast::TokenTree::can_cast(node.kind()) => { + tt_level += 1; + } + Leave(NodeOrToken::Node(node)) if ast::TokenTree::can_cast(node.kind()) => { + tt_level -= 1; + } + Enter(NodeOrToken::Node(node)) if ast::Attr::can_cast(node.kind()) => { + inside_attribute = true + } + Leave(NodeOrToken::Node(node)) if ast::Attr::can_cast(node.kind()) => { + inside_attribute = false + } + + Enter(NodeOrToken::Node(node)) if ast::Item::can_cast(node.kind()) => { + match ast::Item::cast(node.clone()) { + Some(ast::Item::MacroRules(mac)) => { + macro_highlighter.init(); + current_macro = Some(mac.into()); + continue; + } + Some(ast::Item::MacroDef(mac)) => { + macro_highlighter.init(); + current_macro = Some(mac.into()); + continue; + } + Some(item) => { + if matches!(node.kind(), FN | CONST | STATIC) { + bindings_shadow_count.clear(); + } + + if attr_or_derive_item.is_none() { + if sema.is_attr_macro_call(&item) { + attr_or_derive_item = Some(AttrOrDerive::Attr(item)); + } else { + let adt = match item { + ast::Item::Enum(it) => Some(ast::Adt::Enum(it)), + ast::Item::Struct(it) => Some(ast::Adt::Struct(it)), + ast::Item::Union(it) => Some(ast::Adt::Union(it)), + _ => None, + }; + match adt { + Some(adt) if sema.is_derive_annotated(&adt) => { + attr_or_derive_item = + Some(AttrOrDerive::Derive(ast::Item::from(adt))); + } + _ => (), + } + } + } + } + _ => (), + } + } + Leave(NodeOrToken::Node(node)) if ast::Item::can_cast(node.kind()) => { + match ast::Item::cast(node.clone()) { + Some(ast::Item::MacroRules(mac)) => { + assert_eq!(current_macro, Some(mac.into())); + current_macro = None; + macro_highlighter = MacroHighlighter::default(); + } + Some(ast::Item::MacroDef(mac)) => { + assert_eq!(current_macro, Some(mac.into())); + current_macro = None; + macro_highlighter = MacroHighlighter::default(); + } + Some(item) + if attr_or_derive_item.as_ref().map_or(false, |it| *it.item() == item) => + { + attr_or_derive_item = None; + } + _ => (), + } + } + _ => (), + } + + let element = match event { + Enter(NodeOrToken::Token(tok)) if tok.kind() == WHITESPACE => continue, + Enter(it) => it, + Leave(NodeOrToken::Token(_)) => continue, + Leave(NodeOrToken::Node(node)) => { + // Doc comment highlighting injection, we do this when leaving the node + // so that we overwrite the highlighting of the doc comment itself. + inject::doc_comment(hl, sema, file_id, &node); + continue; + } + }; + + if current_macro.is_some() { + if let Some(tok) = element.as_token() { + macro_highlighter.advance(tok); + } + } + + let element = match element.clone() { + NodeOrToken::Node(n) => match ast::NameLike::cast(n) { + Some(n) => NodeOrToken::Node(n), + None => continue, + }, + NodeOrToken::Token(t) => NodeOrToken::Token(t), + }; + let token = element.as_token().cloned(); + + // Descending tokens into macros is expensive even if no descending occurs, so make sure + // that we actually are in a position where descending is possible. + let in_macro = tt_level > 0 + || match attr_or_derive_item { + Some(AttrOrDerive::Attr(_)) => true, + Some(AttrOrDerive::Derive(_)) => inside_attribute, + None => false, + }; + let descended_element = if in_macro { + // Attempt to descend tokens into macro-calls. + match element { + NodeOrToken::Token(token) if token.kind() != COMMENT => { + let token = match attr_or_derive_item { + Some(AttrOrDerive::Attr(_)) => { + sema.descend_into_macros_with_kind_preference(token) + } + Some(AttrOrDerive::Derive(_)) | None => { + sema.descend_into_macros_single(token) + } + }; + match token.parent().and_then(ast::NameLike::cast) { + // Remap the token into the wrapping single token nodes + Some(parent) => match (token.kind(), parent.syntax().kind()) { + (T![self] | T![ident], NAME | NAME_REF) => NodeOrToken::Node(parent), + (T![self] | T![super] | T![crate] | T![Self], NAME_REF) => { + NodeOrToken::Node(parent) + } + (INT_NUMBER, NAME_REF) => NodeOrToken::Node(parent), + (LIFETIME_IDENT, LIFETIME) => NodeOrToken::Node(parent), + _ => NodeOrToken::Token(token), + }, + None => NodeOrToken::Token(token), + } + } + e => e, + } + } else { + element + }; + + // FIXME: do proper macro def highlighting https://github.com/rust-lang/rust-analyzer/issues/6232 + // Skip metavariables from being highlighted to prevent keyword highlighting in them + if descended_element.as_token().and_then(|t| macro_highlighter.highlight(t)).is_some() { + continue; + } + + // string highlight injections, note this does not use the descended element as proc-macros + // can rewrite string literals which invalidates our indices + if let (Some(token), Some(descended_token)) = (token, descended_element.as_token()) { + if ast::String::can_cast(token.kind()) && ast::String::can_cast(descended_token.kind()) + { + let string = ast::String::cast(token); + let string_to_highlight = ast::String::cast(descended_token.clone()); + if let Some((string, expanded_string)) = string.zip(string_to_highlight) { + if string.is_raw() { + if inject::ra_fixture(hl, sema, &string, &expanded_string).is_some() { + continue; + } + } + highlight_format_string(hl, &string, &expanded_string, range); + highlight_escape_string(hl, &string, range.start()); + } + } else if ast::ByteString::can_cast(token.kind()) + && ast::ByteString::can_cast(descended_token.kind()) + { + if let Some(byte_string) = ast::ByteString::cast(token) { + highlight_escape_string(hl, &byte_string, range.start()); + } + } + } + + let element = match descended_element { + NodeOrToken::Node(name_like) => highlight::name_like( + sema, + krate, + &mut bindings_shadow_count, + syntactic_name_ref_highlighting, + name_like, + ), + NodeOrToken::Token(token) => highlight::token(sema, token).zip(Some(None)), + }; + if let Some((mut highlight, binding_hash)) = element { + if is_unlinked && highlight.tag == HlTag::UnresolvedReference { + // do not emit unresolved references if the file is unlinked + // let the editor do its highlighting for these tokens instead + continue; + } + if highlight.tag == HlTag::UnresolvedReference + && matches!(attr_or_derive_item, Some(AttrOrDerive::Derive(_)) if inside_attribute) + { + // do not emit unresolved references in derive helpers if the token mapping maps to + // something unresolvable. FIXME: There should be a way to prevent that + continue; + } + if inside_attribute { + highlight |= HlMod::Attribute + } + + hl.add(HlRange { range, highlight, binding_hash }); + } + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/escape.rs b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/escape.rs new file mode 100644 index 000000000..6a1236c79 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/escape.rs @@ -0,0 +1,25 @@ +//! Syntax highlighting for escape sequences +use crate::syntax_highlighting::highlights::Highlights; +use crate::{HlRange, HlTag}; +use syntax::ast::IsString; +use syntax::TextSize; + +pub(super) fn highlight_escape_string<T: IsString>( + stack: &mut Highlights, + string: &T, + start: TextSize, +) { + string.escaped_char_ranges(&mut |piece_range, char| { + if char.is_err() { + return; + } + + if string.text()[piece_range.start().into()..].starts_with('\\') { + stack.add(HlRange { + range: piece_range + start, + highlight: HlTag::EscapeSequence.into(), + binding_hash: None, + }); + } + }); +} diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/format.rs b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/format.rs new file mode 100644 index 000000000..2ed57e201 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/format.rs @@ -0,0 +1,50 @@ +//! Syntax highlighting for format macro strings. +use ide_db::{ + syntax_helpers::format_string::{is_format_string, lex_format_specifiers, FormatSpecifier}, + SymbolKind, +}; +use syntax::{ast, TextRange}; + +use crate::{syntax_highlighting::highlights::Highlights, HlRange, HlTag}; + +pub(super) fn highlight_format_string( + stack: &mut Highlights, + string: &ast::String, + expanded_string: &ast::String, + range: TextRange, +) { + if !is_format_string(expanded_string) { + return; + } + + lex_format_specifiers(string, &mut |piece_range, kind| { + if let Some(highlight) = highlight_format_specifier(kind) { + stack.add(HlRange { + range: piece_range + range.start(), + highlight: highlight.into(), + binding_hash: None, + }); + } + }); +} + +fn highlight_format_specifier(kind: FormatSpecifier) -> Option<HlTag> { + Some(match kind { + FormatSpecifier::Open + | FormatSpecifier::Close + | FormatSpecifier::Colon + | FormatSpecifier::Fill + | FormatSpecifier::Align + | FormatSpecifier::Sign + | FormatSpecifier::NumberSign + | FormatSpecifier::DollarSign + | FormatSpecifier::Dot + | FormatSpecifier::Asterisk + | FormatSpecifier::QuestionMark => HlTag::FormatSpecifier, + + FormatSpecifier::Integer | FormatSpecifier::Zero => HlTag::NumericLiteral, + + FormatSpecifier::Identifier => HlTag::Symbol(SymbolKind::Local), + FormatSpecifier::Escape => HlTag::EscapeSequence, + }) +} diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/highlight.rs b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/highlight.rs new file mode 100644 index 000000000..9395e914c --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/highlight.rs @@ -0,0 +1,690 @@ +//! Computes color for a single element. + +use hir::{AsAssocItem, HasVisibility, Semantics}; +use ide_db::{ + defs::{Definition, IdentClass, NameClass, NameRefClass}, + FxHashMap, RootDatabase, SymbolKind, +}; +use syntax::{ + ast, match_ast, AstNode, AstToken, NodeOrToken, + SyntaxKind::{self, *}, + SyntaxNode, SyntaxToken, T, +}; + +use crate::{ + syntax_highlighting::tags::{HlOperator, HlPunct}, + Highlight, HlMod, HlTag, +}; + +pub(super) fn token(sema: &Semantics<'_, RootDatabase>, token: SyntaxToken) -> Option<Highlight> { + if let Some(comment) = ast::Comment::cast(token.clone()) { + let h = HlTag::Comment; + return Some(match comment.kind().doc { + Some(_) => h | HlMod::Documentation, + None => h.into(), + }); + } + + let highlight: Highlight = match token.kind() { + STRING | BYTE_STRING => HlTag::StringLiteral.into(), + INT_NUMBER if token.parent_ancestors().nth(1).map(|it| it.kind()) == Some(FIELD_EXPR) => { + SymbolKind::Field.into() + } + INT_NUMBER | FLOAT_NUMBER => HlTag::NumericLiteral.into(), + BYTE => HlTag::ByteLiteral.into(), + CHAR => HlTag::CharLiteral.into(), + IDENT if token.parent().and_then(ast::TokenTree::cast).is_some() => { + // from this point on we are inside a token tree, this only happens for identifiers + // that were not mapped down into macro invocations + HlTag::None.into() + } + p if p.is_punct() => punctuation(sema, token, p), + k if k.is_keyword() => keyword(sema, token, k)?, + _ => return None, + }; + Some(highlight) +} + +pub(super) fn name_like( + sema: &Semantics<'_, RootDatabase>, + krate: hir::Crate, + bindings_shadow_count: &mut FxHashMap<hir::Name, u32>, + syntactic_name_ref_highlighting: bool, + name_like: ast::NameLike, +) -> Option<(Highlight, Option<u64>)> { + let mut binding_hash = None; + let highlight = match name_like { + ast::NameLike::NameRef(name_ref) => highlight_name_ref( + sema, + krate, + bindings_shadow_count, + &mut binding_hash, + syntactic_name_ref_highlighting, + name_ref, + ), + ast::NameLike::Name(name) => { + highlight_name(sema, bindings_shadow_count, &mut binding_hash, krate, name) + } + ast::NameLike::Lifetime(lifetime) => match IdentClass::classify_lifetime(sema, &lifetime) { + Some(IdentClass::NameClass(NameClass::Definition(def))) => { + highlight_def(sema, krate, def) | HlMod::Definition + } + Some(IdentClass::NameRefClass(NameRefClass::Definition(def))) => { + highlight_def(sema, krate, def) + } + // FIXME: Fallback for 'static and '_, as we do not resolve these yet + _ => SymbolKind::LifetimeParam.into(), + }, + }; + Some((highlight, binding_hash)) +} + +fn punctuation( + sema: &Semantics<'_, RootDatabase>, + token: SyntaxToken, + kind: SyntaxKind, +) -> Highlight { + let parent = token.parent(); + let parent_kind = parent.as_ref().map_or(EOF, SyntaxNode::kind); + match (kind, parent_kind) { + (T![?], _) => HlTag::Operator(HlOperator::Other) | HlMod::ControlFlow, + (T![&], BIN_EXPR) => HlOperator::Bitwise.into(), + (T![&], _) => { + let h = HlTag::Operator(HlOperator::Other).into(); + let is_unsafe = parent + .and_then(ast::RefExpr::cast) + .map(|ref_expr| sema.is_unsafe_ref_expr(&ref_expr)); + if let Some(true) = is_unsafe { + h | HlMod::Unsafe + } else { + h + } + } + (T![::] | T![->] | T![=>] | T![..] | T![=] | T![@] | T![.], _) => HlOperator::Other.into(), + (T![!], MACRO_CALL | MACRO_RULES) => HlPunct::MacroBang.into(), + (T![!], NEVER_TYPE) => HlTag::BuiltinType.into(), + (T![!], PREFIX_EXPR) => HlOperator::Logical.into(), + (T![*], PTR_TYPE) => HlTag::Keyword.into(), + (T![*], PREFIX_EXPR) => { + let is_raw_ptr = (|| { + let prefix_expr = parent.and_then(ast::PrefixExpr::cast)?; + let expr = prefix_expr.expr()?; + sema.type_of_expr(&expr)?.original.is_raw_ptr().then(|| ()) + })(); + if let Some(()) = is_raw_ptr { + HlTag::Operator(HlOperator::Other) | HlMod::Unsafe + } else { + HlOperator::Other.into() + } + } + (T![-], PREFIX_EXPR) => { + let prefix_expr = parent.and_then(ast::PrefixExpr::cast).and_then(|e| e.expr()); + match prefix_expr { + Some(ast::Expr::Literal(_)) => HlTag::NumericLiteral, + _ => HlTag::Operator(HlOperator::Other), + } + .into() + } + (T![+] | T![-] | T![*] | T![/] | T![%], BIN_EXPR) => HlOperator::Arithmetic.into(), + (T![+=] | T![-=] | T![*=] | T![/=] | T![%=], BIN_EXPR) => { + Highlight::from(HlOperator::Arithmetic) | HlMod::Mutable + } + (T![|] | T![&] | T![!] | T![^] | T![>>] | T![<<], BIN_EXPR) => HlOperator::Bitwise.into(), + (T![|=] | T![&=] | T![^=] | T![>>=] | T![<<=], BIN_EXPR) => { + Highlight::from(HlOperator::Bitwise) | HlMod::Mutable + } + (T![&&] | T![||], BIN_EXPR) => HlOperator::Logical.into(), + (T![>] | T![<] | T![==] | T![>=] | T![<=] | T![!=], BIN_EXPR) => { + HlOperator::Comparison.into() + } + (_, PREFIX_EXPR | BIN_EXPR | RANGE_EXPR | RANGE_PAT | REST_PAT) => HlOperator::Other.into(), + (_, ATTR) => HlTag::AttributeBracket.into(), + (kind, _) => match kind { + T!['['] | T![']'] => HlPunct::Bracket, + T!['{'] | T!['}'] => HlPunct::Brace, + T!['('] | T![')'] => HlPunct::Parenthesis, + T![<] | T![>] => HlPunct::Angle, + T![,] => HlPunct::Comma, + T![:] => HlPunct::Colon, + T![;] => HlPunct::Semi, + T![.] => HlPunct::Dot, + _ => HlPunct::Other, + } + .into(), + } +} + +fn keyword( + sema: &Semantics<'_, RootDatabase>, + token: SyntaxToken, + kind: SyntaxKind, +) -> Option<Highlight> { + let h = Highlight::new(HlTag::Keyword); + let h = match kind { + T![await] => h | HlMod::Async | HlMod::ControlFlow, + T![async] => h | HlMod::Async, + T![break] + | T![continue] + | T![else] + | T![if] + | T![in] + | T![loop] + | T![match] + | T![return] + | T![while] + | T![yield] => h | HlMod::ControlFlow, + T![for] if parent_matches::<ast::ForExpr>(&token) => h | HlMod::ControlFlow, + T![unsafe] => h | HlMod::Unsafe, + T![true] | T![false] => HlTag::BoolLiteral.into(), + // crate is handled just as a token if it's in an `extern crate` + T![crate] if parent_matches::<ast::ExternCrate>(&token) => h, + // self, crate, super and `Self` are handled as either a Name or NameRef already, unless they + // are inside unmapped token trees + T![self] | T![crate] | T![super] | T![Self] if parent_matches::<ast::NameRef>(&token) => { + return None + } + T![self] if parent_matches::<ast::Name>(&token) => return None, + T![ref] => match token.parent().and_then(ast::IdentPat::cast) { + Some(ident) if sema.is_unsafe_ident_pat(&ident) => h | HlMod::Unsafe, + _ => h, + }, + _ => h, + }; + Some(h) +} + +fn highlight_name_ref( + sema: &Semantics<'_, RootDatabase>, + krate: hir::Crate, + bindings_shadow_count: &mut FxHashMap<hir::Name, u32>, + binding_hash: &mut Option<u64>, + syntactic_name_ref_highlighting: bool, + name_ref: ast::NameRef, +) -> Highlight { + let db = sema.db; + if let Some(res) = highlight_method_call_by_name_ref(sema, krate, &name_ref) { + return res; + } + + let name_class = match NameRefClass::classify(sema, &name_ref) { + Some(name_kind) => name_kind, + None if syntactic_name_ref_highlighting => { + return highlight_name_ref_by_syntax(name_ref, sema, krate) + } + // FIXME: This is required for helper attributes used by proc-macros, as those do not map down + // to anything when used. + // We can fix this for derive attributes since derive helpers are recorded, but not for + // general attributes. + None if name_ref.syntax().ancestors().any(|it| it.kind() == ATTR) => { + return HlTag::Symbol(SymbolKind::Attribute).into(); + } + None => return HlTag::UnresolvedReference.into(), + }; + let mut h = match name_class { + NameRefClass::Definition(def) => { + if let Definition::Local(local) = &def { + let name = local.name(db); + let shadow_count = bindings_shadow_count.entry(name.clone()).or_default(); + *binding_hash = Some(calc_binding_hash(&name, *shadow_count)) + }; + + let mut h = highlight_def(sema, krate, def); + + match def { + Definition::Local(local) if is_consumed_lvalue(name_ref.syntax(), &local, db) => { + h |= HlMod::Consuming; + } + Definition::Trait(trait_) if trait_.is_unsafe(db) => { + if ast::Impl::for_trait_name_ref(&name_ref) + .map_or(false, |impl_| impl_.unsafe_token().is_some()) + { + h |= HlMod::Unsafe; + } + } + Definition::Field(field) => { + if let Some(parent) = name_ref.syntax().parent() { + if matches!(parent.kind(), FIELD_EXPR | RECORD_PAT_FIELD) { + if let hir::VariantDef::Union(_) = field.parent_def(db) { + h |= HlMod::Unsafe; + } + } + } + } + Definition::Macro(_) => { + if let Some(macro_call) = + ide_db::syntax_helpers::node_ext::full_path_of_name_ref(&name_ref) + .and_then(|it| it.syntax().parent().and_then(ast::MacroCall::cast)) + { + if sema.is_unsafe_macro_call(¯o_call) { + h |= HlMod::Unsafe; + } + } + } + _ => (), + } + + h + } + NameRefClass::FieldShorthand { .. } => SymbolKind::Field.into(), + }; + + h.tag = match name_ref.token_kind() { + T![Self] => HlTag::Symbol(SymbolKind::SelfType), + T![self] => HlTag::Symbol(SymbolKind::SelfParam), + T![super] | T![crate] => HlTag::Keyword, + _ => h.tag, + }; + h +} + +fn highlight_name( + sema: &Semantics<'_, RootDatabase>, + bindings_shadow_count: &mut FxHashMap<hir::Name, u32>, + binding_hash: &mut Option<u64>, + krate: hir::Crate, + name: ast::Name, +) -> Highlight { + let name_kind = NameClass::classify(sema, &name); + if let Some(NameClass::Definition(Definition::Local(local))) = &name_kind { + let name = local.name(sema.db); + let shadow_count = bindings_shadow_count.entry(name.clone()).or_default(); + *shadow_count += 1; + *binding_hash = Some(calc_binding_hash(&name, *shadow_count)) + }; + match name_kind { + Some(NameClass::Definition(def)) => { + let mut h = highlight_def(sema, krate, def) | HlMod::Definition; + if let Definition::Trait(trait_) = &def { + if trait_.is_unsafe(sema.db) { + h |= HlMod::Unsafe; + } + } + h + } + Some(NameClass::ConstReference(def)) => highlight_def(sema, krate, def), + Some(NameClass::PatFieldShorthand { field_ref, .. }) => { + let mut h = HlTag::Symbol(SymbolKind::Field).into(); + if let hir::VariantDef::Union(_) = field_ref.parent_def(sema.db) { + h |= HlMod::Unsafe; + } + h + } + None => highlight_name_by_syntax(name) | HlMod::Definition, + } +} + +fn calc_binding_hash(name: &hir::Name, shadow_count: u32) -> u64 { + fn hash<T: std::hash::Hash + std::fmt::Debug>(x: T) -> u64 { + use std::{collections::hash_map::DefaultHasher, hash::Hasher}; + + let mut hasher = DefaultHasher::new(); + x.hash(&mut hasher); + hasher.finish() + } + + hash((name, shadow_count)) +} + +fn highlight_def( + sema: &Semantics<'_, RootDatabase>, + krate: hir::Crate, + def: Definition, +) -> Highlight { + let db = sema.db; + let mut h = match def { + Definition::Macro(m) => Highlight::new(HlTag::Symbol(m.kind(sema.db).into())), + Definition::Field(_) => Highlight::new(HlTag::Symbol(SymbolKind::Field)), + Definition::Module(module) => { + let mut h = Highlight::new(HlTag::Symbol(SymbolKind::Module)); + if module.is_crate_root(db) { + h |= HlMod::CrateRoot; + } + h + } + Definition::Function(func) => { + let mut h = Highlight::new(HlTag::Symbol(SymbolKind::Function)); + if let Some(item) = func.as_assoc_item(db) { + h |= HlMod::Associated; + match func.self_param(db) { + Some(sp) => match sp.access(db) { + hir::Access::Exclusive => { + h |= HlMod::Mutable; + h |= HlMod::Reference; + } + hir::Access::Shared => h |= HlMod::Reference, + hir::Access::Owned => h |= HlMod::Consuming, + }, + None => h |= HlMod::Static, + } + + match item.container(db) { + hir::AssocItemContainer::Impl(i) => { + if i.trait_(db).is_some() { + h |= HlMod::Trait; + } + } + hir::AssocItemContainer::Trait(_t) => { + h |= HlMod::Trait; + } + } + } + + if func.is_unsafe_to_call(db) { + h |= HlMod::Unsafe; + } + if func.is_async(db) { + h |= HlMod::Async; + } + + h + } + Definition::Adt(adt) => { + let h = match adt { + hir::Adt::Struct(_) => HlTag::Symbol(SymbolKind::Struct), + hir::Adt::Enum(_) => HlTag::Symbol(SymbolKind::Enum), + hir::Adt::Union(_) => HlTag::Symbol(SymbolKind::Union), + }; + + Highlight::new(h) + } + Definition::Variant(_) => Highlight::new(HlTag::Symbol(SymbolKind::Variant)), + Definition::Const(konst) => { + let mut h = Highlight::new(HlTag::Symbol(SymbolKind::Const)); + + if let Some(item) = konst.as_assoc_item(db) { + h |= HlMod::Associated; + match item.container(db) { + hir::AssocItemContainer::Impl(i) => { + if i.trait_(db).is_some() { + h |= HlMod::Trait; + } + } + hir::AssocItemContainer::Trait(_t) => { + h |= HlMod::Trait; + } + } + } + + h + } + Definition::Trait(_) => Highlight::new(HlTag::Symbol(SymbolKind::Trait)), + Definition::TypeAlias(type_) => { + let mut h = Highlight::new(HlTag::Symbol(SymbolKind::TypeAlias)); + + if let Some(item) = type_.as_assoc_item(db) { + h |= HlMod::Associated; + match item.container(db) { + hir::AssocItemContainer::Impl(i) => { + if i.trait_(db).is_some() { + h |= HlMod::Trait; + } + } + hir::AssocItemContainer::Trait(_t) => { + h |= HlMod::Trait; + } + } + } + + h + } + Definition::BuiltinType(_) => Highlight::new(HlTag::BuiltinType), + Definition::Static(s) => { + let mut h = Highlight::new(HlTag::Symbol(SymbolKind::Static)); + + if s.is_mut(db) { + h |= HlMod::Mutable; + h |= HlMod::Unsafe; + } + + h + } + Definition::SelfType(_) => Highlight::new(HlTag::Symbol(SymbolKind::Impl)), + Definition::GenericParam(it) => match it { + hir::GenericParam::TypeParam(_) => Highlight::new(HlTag::Symbol(SymbolKind::TypeParam)), + hir::GenericParam::ConstParam(_) => { + Highlight::new(HlTag::Symbol(SymbolKind::ConstParam)) + } + hir::GenericParam::LifetimeParam(_) => { + Highlight::new(HlTag::Symbol(SymbolKind::LifetimeParam)) + } + }, + Definition::Local(local) => { + let tag = if local.is_self(db) { + HlTag::Symbol(SymbolKind::SelfParam) + } else if local.is_param(db) { + HlTag::Symbol(SymbolKind::ValueParam) + } else { + HlTag::Symbol(SymbolKind::Local) + }; + let mut h = Highlight::new(tag); + let ty = local.ty(db); + if local.is_mut(db) || ty.is_mutable_reference() { + h |= HlMod::Mutable; + } + if local.is_ref(db) || ty.is_reference() { + h |= HlMod::Reference; + } + if ty.as_callable(db).is_some() || ty.impls_fnonce(db) { + h |= HlMod::Callable; + } + h + } + Definition::Label(_) => Highlight::new(HlTag::Symbol(SymbolKind::Label)), + Definition::BuiltinAttr(_) => Highlight::new(HlTag::Symbol(SymbolKind::BuiltinAttr)), + Definition::ToolModule(_) => Highlight::new(HlTag::Symbol(SymbolKind::ToolModule)), + Definition::DeriveHelper(_) => Highlight::new(HlTag::Symbol(SymbolKind::DeriveHelper)), + }; + + let def_crate = def.krate(db); + let is_from_other_crate = def_crate != Some(krate); + let is_from_builtin_crate = def_crate.map_or(false, |def_crate| def_crate.is_builtin(db)); + let is_builtin_type = matches!(def, Definition::BuiltinType(_)); + let is_public = def.visibility(db) == Some(hir::Visibility::Public); + + match (is_from_other_crate, is_builtin_type, is_public) { + (true, false, _) => h |= HlMod::Library, + (false, _, true) => h |= HlMod::Public, + _ => {} + } + + if is_from_builtin_crate { + h |= HlMod::DefaultLibrary; + } + + h +} + +fn highlight_method_call_by_name_ref( + sema: &Semantics<'_, RootDatabase>, + krate: hir::Crate, + name_ref: &ast::NameRef, +) -> Option<Highlight> { + let mc = name_ref.syntax().parent().and_then(ast::MethodCallExpr::cast)?; + highlight_method_call(sema, krate, &mc) +} + +fn highlight_method_call( + sema: &Semantics<'_, RootDatabase>, + krate: hir::Crate, + method_call: &ast::MethodCallExpr, +) -> Option<Highlight> { + let func = sema.resolve_method_call(method_call)?; + + let mut h = SymbolKind::Function.into(); + h |= HlMod::Associated; + + if func.is_unsafe_to_call(sema.db) || sema.is_unsafe_method_call(method_call) { + h |= HlMod::Unsafe; + } + if func.is_async(sema.db) { + h |= HlMod::Async; + } + if func + .as_assoc_item(sema.db) + .and_then(|it| it.containing_trait_or_trait_impl(sema.db)) + .is_some() + { + h |= HlMod::Trait; + } + + let def_crate = func.module(sema.db).krate(); + let is_from_other_crate = def_crate != krate; + let is_from_builtin_crate = def_crate.is_builtin(sema.db); + let is_public = func.visibility(sema.db) == hir::Visibility::Public; + + if is_from_other_crate { + h |= HlMod::Library; + } else if is_public { + h |= HlMod::Public; + } + + if is_from_builtin_crate { + h |= HlMod::DefaultLibrary; + } + + if let Some(self_param) = func.self_param(sema.db) { + match self_param.access(sema.db) { + hir::Access::Shared => h |= HlMod::Reference, + hir::Access::Exclusive => { + h |= HlMod::Mutable; + h |= HlMod::Reference; + } + hir::Access::Owned => { + if let Some(receiver_ty) = + method_call.receiver().and_then(|it| sema.type_of_expr(&it)) + { + if !receiver_ty.adjusted().is_copy(sema.db) { + h |= HlMod::Consuming + } + } + } + } + } + Some(h) +} + +fn highlight_name_by_syntax(name: ast::Name) -> Highlight { + let default = HlTag::UnresolvedReference; + + let parent = match name.syntax().parent() { + Some(it) => it, + _ => return default.into(), + }; + + let tag = match parent.kind() { + STRUCT => SymbolKind::Struct, + ENUM => SymbolKind::Enum, + VARIANT => SymbolKind::Variant, + UNION => SymbolKind::Union, + TRAIT => SymbolKind::Trait, + TYPE_ALIAS => SymbolKind::TypeAlias, + TYPE_PARAM => SymbolKind::TypeParam, + RECORD_FIELD => SymbolKind::Field, + MODULE => SymbolKind::Module, + FN => SymbolKind::Function, + CONST => SymbolKind::Const, + STATIC => SymbolKind::Static, + IDENT_PAT => SymbolKind::Local, + _ => return default.into(), + }; + + tag.into() +} + +fn highlight_name_ref_by_syntax( + name: ast::NameRef, + sema: &Semantics<'_, RootDatabase>, + krate: hir::Crate, +) -> Highlight { + let default = HlTag::UnresolvedReference; + + let parent = match name.syntax().parent() { + Some(it) => it, + _ => return default.into(), + }; + + match parent.kind() { + METHOD_CALL_EXPR => ast::MethodCallExpr::cast(parent) + .and_then(|it| highlight_method_call(sema, krate, &it)) + .unwrap_or_else(|| SymbolKind::Function.into()), + FIELD_EXPR => { + let h = HlTag::Symbol(SymbolKind::Field); + let is_union = ast::FieldExpr::cast(parent) + .and_then(|field_expr| sema.resolve_field(&field_expr)) + .map_or(false, |field| { + matches!(field.parent_def(sema.db), hir::VariantDef::Union(_)) + }); + if is_union { + h | HlMod::Unsafe + } else { + h.into() + } + } + PATH_SEGMENT => { + let name_based_fallback = || { + if name.text().chars().next().unwrap_or_default().is_uppercase() { + SymbolKind::Struct.into() + } else { + SymbolKind::Module.into() + } + }; + let path = match parent.parent().and_then(ast::Path::cast) { + Some(it) => it, + _ => return name_based_fallback(), + }; + let expr = match path.syntax().parent() { + Some(parent) => match_ast! { + match parent { + ast::PathExpr(path) => path, + ast::MacroCall(_) => return SymbolKind::Macro.into(), + _ => return name_based_fallback(), + } + }, + // within path, decide whether it is module or adt by checking for uppercase name + None => return name_based_fallback(), + }; + let parent = match expr.syntax().parent() { + Some(it) => it, + None => return default.into(), + }; + + match parent.kind() { + CALL_EXPR => SymbolKind::Function.into(), + _ => if name.text().chars().next().unwrap_or_default().is_uppercase() { + SymbolKind::Struct + } else { + SymbolKind::Const + } + .into(), + } + } + _ => default.into(), + } +} + +fn is_consumed_lvalue(node: &SyntaxNode, local: &hir::Local, db: &RootDatabase) -> bool { + // When lvalues are passed as arguments and they're not Copy, then mark them as Consuming. + parents_match(node.clone().into(), &[PATH_SEGMENT, PATH, PATH_EXPR, ARG_LIST]) + && !local.ty(db).is_copy(db) +} + +/// Returns true if the parent nodes of `node` all match the `SyntaxKind`s in `kinds` exactly. +fn parents_match(mut node: NodeOrToken<SyntaxNode, SyntaxToken>, mut kinds: &[SyntaxKind]) -> bool { + while let (Some(parent), [kind, rest @ ..]) = (&node.parent(), kinds) { + if parent.kind() != *kind { + return false; + } + + // FIXME: Would be nice to get parent out of the match, but binding by-move and by-value + // in the same pattern is unstable: rust-lang/rust#68354. + node = node.parent().unwrap().into(); + kinds = rest; + } + + // Only true if we matched all expected kinds + kinds.is_empty() +} + +fn parent_matches<N: AstNode>(token: &SyntaxToken) -> bool { + token.parent().map_or(false, |it| N::can_cast(it.kind())) +} diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/highlights.rs b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/highlights.rs new file mode 100644 index 000000000..340290eaf --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/highlights.rs @@ -0,0 +1,92 @@ +//! Collects a tree of highlighted ranges and flattens it. +use std::iter; + +use stdx::equal_range_by; +use syntax::TextRange; + +use crate::{HlRange, HlTag}; + +pub(super) struct Highlights { + root: Node, +} + +struct Node { + hl_range: HlRange, + nested: Vec<Node>, +} + +impl Highlights { + pub(super) fn new(range: TextRange) -> Highlights { + Highlights { + root: Node::new(HlRange { range, highlight: HlTag::None.into(), binding_hash: None }), + } + } + + pub(super) fn add(&mut self, hl_range: HlRange) { + self.root.add(hl_range); + } + + pub(super) fn to_vec(&self) -> Vec<HlRange> { + let mut res = Vec::new(); + self.root.flatten(&mut res); + res + } +} + +impl Node { + fn new(hl_range: HlRange) -> Node { + Node { hl_range, nested: Vec::new() } + } + + fn add(&mut self, hl_range: HlRange) { + assert!(self.hl_range.range.contains_range(hl_range.range)); + + // Fast path + if let Some(last) = self.nested.last_mut() { + if last.hl_range.range.contains_range(hl_range.range) { + return last.add(hl_range); + } + if last.hl_range.range.end() <= hl_range.range.start() { + return self.nested.push(Node::new(hl_range)); + } + } + + let overlapping = + equal_range_by(&self.nested, |n| TextRange::ordering(n.hl_range.range, hl_range.range)); + + if overlapping.len() == 1 + && self.nested[overlapping.start].hl_range.range.contains_range(hl_range.range) + { + return self.nested[overlapping.start].add(hl_range); + } + + let nested = self + .nested + .splice(overlapping.clone(), iter::once(Node::new(hl_range))) + .collect::<Vec<_>>(); + self.nested[overlapping.start].nested = nested; + } + + fn flatten(&self, acc: &mut Vec<HlRange>) { + let mut start = self.hl_range.range.start(); + let mut nested = self.nested.iter(); + loop { + let next = nested.next(); + let end = next.map_or(self.hl_range.range.end(), |it| it.hl_range.range.start()); + if start < end { + acc.push(HlRange { + range: TextRange::new(start, end), + highlight: self.hl_range.highlight, + binding_hash: self.hl_range.binding_hash, + }); + } + start = match next { + Some(child) => { + child.flatten(acc); + child.hl_range.range.end() + } + None => break, + } + } + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/html.rs b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/html.rs new file mode 100644 index 000000000..9777c014c --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/html.rs @@ -0,0 +1,97 @@ +//! Renders a bit of code as HTML. + +use ide_db::base_db::SourceDatabase; +use oorandom::Rand32; +use stdx::format_to; +use syntax::AstNode; + +use crate::{syntax_highlighting::highlight, FileId, RootDatabase}; + +pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId, rainbow: bool) -> String { + let parse = db.parse(file_id); + + fn rainbowify(seed: u64) -> String { + let mut rng = Rand32::new(seed); + format!( + "hsl({h},{s}%,{l}%)", + h = rng.rand_range(0..361), + s = rng.rand_range(42..99), + l = rng.rand_range(40..91), + ) + } + + let hl_ranges = highlight(db, file_id, None, false); + let text = parse.tree().syntax().to_string(); + let mut buf = String::new(); + buf.push_str(STYLE); + buf.push_str("<pre><code>"); + for r in &hl_ranges { + let chunk = html_escape(&text[r.range]); + if r.highlight.is_empty() { + format_to!(buf, "{}", chunk); + continue; + } + + let class = r.highlight.to_string().replace('.', " "); + let color = match (rainbow, r.binding_hash) { + (true, Some(hash)) => { + format!(" data-binding-hash=\"{}\" style=\"color: {};\"", hash, rainbowify(hash)) + } + _ => "".into(), + }; + format_to!(buf, "<span class=\"{}\"{}>{}</span>", class, color, chunk); + } + buf.push_str("</code></pre>"); + buf +} + +//FIXME: like, real html escaping +fn html_escape(text: &str) -> String { + text.replace('<', "<").replace('>', ">") +} + +const STYLE: &str = " +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +"; diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/inject.rs b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/inject.rs new file mode 100644 index 000000000..f376f9fda --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/inject.rs @@ -0,0 +1,279 @@ +//! "Recursive" Syntax highlighting for code in doctests and fixtures. + +use std::mem; + +use either::Either; +use hir::{InFile, Semantics}; +use ide_db::{ + active_parameter::ActiveParameter, base_db::FileId, defs::Definition, rust_doc::is_rust_fence, + SymbolKind, +}; +use syntax::{ + ast::{self, AstNode, IsString, QuoteOffsets}, + AstToken, NodeOrToken, SyntaxNode, TextRange, TextSize, +}; + +use crate::{ + doc_links::{doc_attributes, extract_definitions_from_docs, resolve_doc_path_for_def}, + syntax_highlighting::{highlights::Highlights, injector::Injector}, + Analysis, HlMod, HlRange, HlTag, RootDatabase, +}; + +pub(super) fn ra_fixture( + hl: &mut Highlights, + sema: &Semantics<'_, RootDatabase>, + literal: &ast::String, + expanded: &ast::String, +) -> Option<()> { + let active_parameter = ActiveParameter::at_token(sema, expanded.syntax().clone())?; + if !active_parameter.ident().map_or(false, |name| name.text().starts_with("ra_fixture")) { + return None; + } + let value = literal.value()?; + + if let Some(range) = literal.open_quote_text_range() { + hl.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None }) + } + + let mut inj = Injector::default(); + + let mut text = &*value; + let mut offset: TextSize = 0.into(); + + while !text.is_empty() { + let marker = "$0"; + let idx = text.find(marker).unwrap_or(text.len()); + let (chunk, next) = text.split_at(idx); + inj.add(chunk, TextRange::at(offset, TextSize::of(chunk))); + + text = next; + offset += TextSize::of(chunk); + + if let Some(next) = text.strip_prefix(marker) { + if let Some(range) = literal.map_range_up(TextRange::at(offset, TextSize::of(marker))) { + hl.add(HlRange { range, highlight: HlTag::Keyword.into(), binding_hash: None }); + } + + text = next; + + let marker_len = TextSize::of(marker); + offset += marker_len; + } + } + + let (analysis, tmp_file_id) = Analysis::from_single_file(inj.take_text()); + + for mut hl_range in analysis.highlight(tmp_file_id).unwrap() { + for range in inj.map_range_up(hl_range.range) { + if let Some(range) = literal.map_range_up(range) { + hl_range.range = range; + hl.add(hl_range); + } + } + } + + if let Some(range) = literal.close_quote_text_range() { + hl.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None }) + } + + Some(()) +} + +const RUSTDOC_FENCE_LENGTH: usize = 3; +const RUSTDOC_FENCES: [&str; 2] = ["```", "~~~"]; + +/// Injection of syntax highlighting of doctests and intra doc links. +pub(super) fn doc_comment( + hl: &mut Highlights, + sema: &Semantics<'_, RootDatabase>, + src_file_id: FileId, + node: &SyntaxNode, +) { + let (attributes, def) = match doc_attributes(sema, node) { + Some(it) => it, + None => return, + }; + let src_file_id = src_file_id.into(); + + // Extract intra-doc links and emit highlights for them. + if let Some((docs, doc_mapping)) = attributes.docs_with_rangemap(sema.db) { + extract_definitions_from_docs(&docs) + .into_iter() + .filter_map(|(range, link, ns)| { + doc_mapping.map(range).filter(|mapping| mapping.file_id == src_file_id).and_then( + |InFile { value: mapped_range, .. }| { + Some(mapped_range).zip(resolve_doc_path_for_def(sema.db, def, &link, ns)) + }, + ) + }) + .for_each(|(range, def)| { + hl.add(HlRange { + range, + highlight: module_def_to_hl_tag(def) + | HlMod::Documentation + | HlMod::Injected + | HlMod::IntraDocLink, + binding_hash: None, + }) + }); + } + + // Extract doc-test sources from the docs and calculate highlighting for them. + + let mut inj = Injector::default(); + inj.add_unmapped("fn doctest() {\n"); + + let attrs_source_map = attributes.source_map(sema.db); + + let mut is_codeblock = false; + let mut is_doctest = false; + + let mut new_comments = Vec::new(); + let mut string; + + for attr in attributes.by_key("doc").attrs() { + let InFile { file_id, value: src } = attrs_source_map.source_of(attr); + if file_id != src_file_id { + continue; + } + let (line, range) = match &src { + Either::Left(it) => { + string = match find_doc_string_in_attr(attr, it) { + Some(it) => it, + None => continue, + }; + let text = string.text(); + let text_range = string.syntax().text_range(); + match string.quote_offsets() { + Some(QuoteOffsets { contents, .. }) => { + (&text[contents - text_range.start()], contents) + } + None => (text, text_range), + } + } + Either::Right(comment) => { + let value = comment.prefix().len(); + let range = comment.syntax().text_range(); + ( + &comment.text()[value..], + TextRange::new(range.start() + TextSize::try_from(value).unwrap(), range.end()), + ) + } + }; + + let mut range_start = range.start(); + for line in line.split('\n') { + let line_len = TextSize::from(line.len() as u32); + let prev_range_start = { + let next_range_start = range_start + line_len + TextSize::from(1); + mem::replace(&mut range_start, next_range_start) + }; + let mut pos = TextSize::from(0); + + match RUSTDOC_FENCES.into_iter().find_map(|fence| line.find(fence)) { + Some(idx) => { + is_codeblock = !is_codeblock; + // Check whether code is rust by inspecting fence guards + let guards = &line[idx + RUSTDOC_FENCE_LENGTH..]; + let is_rust = is_rust_fence(guards); + is_doctest = is_codeblock && is_rust; + continue; + } + None if !is_doctest => continue, + None => (), + } + + // whitespace after comment is ignored + if let Some(ws) = line[pos.into()..].chars().next().filter(|c| c.is_whitespace()) { + pos += TextSize::of(ws); + } + // lines marked with `#` should be ignored in output, we skip the `#` char + if line[pos.into()..].starts_with('#') { + pos += TextSize::of('#'); + } + + new_comments.push(TextRange::at(prev_range_start, pos)); + inj.add(&line[pos.into()..], TextRange::new(pos, line_len) + prev_range_start); + inj.add_unmapped("\n"); + } + } + + if new_comments.is_empty() { + return; // no need to run an analysis on an empty file + } + + inj.add_unmapped("\n}"); + + let (analysis, tmp_file_id) = Analysis::from_single_file(inj.take_text()); + + if let Ok(ranges) = analysis.with_db(|db| super::highlight(db, tmp_file_id, None, true)) { + for HlRange { range, highlight, binding_hash } in ranges { + for range in inj.map_range_up(range) { + hl.add(HlRange { range, highlight: highlight | HlMod::Injected, binding_hash }); + } + } + } + + for range in new_comments { + hl.add(HlRange { + range, + highlight: HlTag::Comment | HlMod::Documentation, + binding_hash: None, + }); + } +} + +fn find_doc_string_in_attr(attr: &hir::Attr, it: &ast::Attr) -> Option<ast::String> { + match it.expr() { + // #[doc = lit] + Some(ast::Expr::Literal(lit)) => match lit.kind() { + ast::LiteralKind::String(it) => Some(it), + _ => None, + }, + // #[cfg_attr(..., doc = "", ...)] + None => { + // We gotta hunt the string token manually here + let text = attr.string_value()?; + // FIXME: We just pick the first string literal that has the same text as the doc attribute + // This means technically we might highlight the wrong one + it.syntax() + .descendants_with_tokens() + .filter_map(NodeOrToken::into_token) + .filter_map(ast::String::cast) + .find(|string| { + string.text().get(1..string.text().len() - 1).map_or(false, |it| it == text) + }) + } + _ => None, + } +} + +fn module_def_to_hl_tag(def: Definition) -> HlTag { + let symbol = match def { + Definition::Module(_) => SymbolKind::Module, + Definition::Function(_) => SymbolKind::Function, + Definition::Adt(hir::Adt::Struct(_)) => SymbolKind::Struct, + Definition::Adt(hir::Adt::Enum(_)) => SymbolKind::Enum, + Definition::Adt(hir::Adt::Union(_)) => SymbolKind::Union, + Definition::Variant(_) => SymbolKind::Variant, + Definition::Const(_) => SymbolKind::Const, + Definition::Static(_) => SymbolKind::Static, + Definition::Trait(_) => SymbolKind::Trait, + Definition::TypeAlias(_) => SymbolKind::TypeAlias, + Definition::BuiltinType(_) => return HlTag::BuiltinType, + Definition::Macro(_) => SymbolKind::Macro, + Definition::Field(_) => SymbolKind::Field, + Definition::SelfType(_) => SymbolKind::Impl, + Definition::Local(_) => SymbolKind::Local, + Definition::GenericParam(gp) => match gp { + hir::GenericParam::TypeParam(_) => SymbolKind::TypeParam, + hir::GenericParam::ConstParam(_) => SymbolKind::ConstParam, + hir::GenericParam::LifetimeParam(_) => SymbolKind::LifetimeParam, + }, + Definition::Label(_) => SymbolKind::Label, + Definition::BuiltinAttr(_) => SymbolKind::BuiltinAttr, + Definition::ToolModule(_) => SymbolKind::ToolModule, + Definition::DeriveHelper(_) => SymbolKind::DeriveHelper, + }; + HlTag::Symbol(symbol) +} diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/injector.rs b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/injector.rs new file mode 100644 index 000000000..a902fd717 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/injector.rs @@ -0,0 +1,81 @@ +//! Extracts a subsequence of a text document, remembering the mapping of ranges +//! between original and extracted texts. +use std::ops::{self, Sub}; + +use stdx::equal_range_by; +use syntax::{TextRange, TextSize}; + +#[derive(Default)] +pub(super) struct Injector { + buf: String, + ranges: Vec<(TextRange, Option<Delta<TextSize>>)>, +} + +impl Injector { + pub(super) fn add(&mut self, text: &str, source_range: TextRange) { + let len = TextSize::of(text); + assert_eq!(len, source_range.len()); + self.add_impl(text, Some(source_range.start())); + } + + pub(super) fn add_unmapped(&mut self, text: &str) { + self.add_impl(text, None); + } + + fn add_impl(&mut self, text: &str, source: Option<TextSize>) { + let len = TextSize::of(text); + let target_range = TextRange::at(TextSize::of(&self.buf), len); + self.ranges.push((target_range, source.map(|it| Delta::new(target_range.start(), it)))); + self.buf.push_str(text); + } + + pub(super) fn take_text(&mut self) -> String { + std::mem::take(&mut self.buf) + } + + pub(super) fn map_range_up(&self, range: TextRange) -> impl Iterator<Item = TextRange> + '_ { + equal_range_by(&self.ranges, |&(r, _)| TextRange::ordering(r, range)).filter_map(move |i| { + let (target_range, delta) = self.ranges[i]; + let intersection = target_range.intersect(range).unwrap(); + Some(intersection + delta?) + }) + } +} + +#[derive(Clone, Copy)] +enum Delta<T> { + Add(T), + Sub(T), +} + +impl<T> Delta<T> { + fn new(from: T, to: T) -> Delta<T> + where + T: Ord + Sub<Output = T>, + { + if to >= from { + Delta::Add(to - from) + } else { + Delta::Sub(from - to) + } + } +} + +impl ops::Add<Delta<TextSize>> for TextSize { + type Output = TextSize; + + fn add(self, rhs: Delta<TextSize>) -> TextSize { + match rhs { + Delta::Add(it) => self + it, + Delta::Sub(it) => self - it, + } + } +} + +impl ops::Add<Delta<TextSize>> for TextRange { + type Output = TextRange; + + fn add(self, rhs: Delta<TextSize>) -> TextRange { + TextRange::at(self.start() + rhs, self.len()) + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/macro_.rs b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/macro_.rs new file mode 100644 index 000000000..1099d9c23 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/macro_.rs @@ -0,0 +1,128 @@ +//! Syntax highlighting for macro_rules!. +use syntax::{SyntaxKind, SyntaxToken, TextRange, T}; + +use crate::{HlRange, HlTag}; + +#[derive(Default)] +pub(super) struct MacroHighlighter { + state: Option<MacroMatcherParseState>, +} + +impl MacroHighlighter { + pub(super) fn init(&mut self) { + self.state = Some(MacroMatcherParseState::default()); + } + + pub(super) fn advance(&mut self, token: &SyntaxToken) { + if let Some(state) = self.state.as_mut() { + update_macro_state(state, token); + } + } + + pub(super) fn highlight(&self, token: &SyntaxToken) -> Option<HlRange> { + if let Some(state) = self.state.as_ref() { + if matches!(state.rule_state, RuleState::Matcher | RuleState::Expander) { + if let Some(range) = is_metavariable(token) { + return Some(HlRange { + range, + highlight: HlTag::UnresolvedReference.into(), + binding_hash: None, + }); + } + } + } + None + } +} + +struct MacroMatcherParseState { + /// Opening and corresponding closing bracket of the matcher or expander of the current rule + paren_ty: Option<(SyntaxKind, SyntaxKind)>, + paren_level: usize, + rule_state: RuleState, + /// Whether we are inside the outer `{` `}` macro block that holds the rules + in_invoc_body: bool, +} + +impl Default for MacroMatcherParseState { + fn default() -> Self { + MacroMatcherParseState { + paren_ty: None, + paren_level: 0, + in_invoc_body: false, + rule_state: RuleState::None, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +enum RuleState { + Matcher, + Expander, + Between, + None, +} + +impl RuleState { + fn transition(&mut self) { + *self = match self { + RuleState::Matcher => RuleState::Between, + RuleState::Expander => RuleState::None, + RuleState::Between => RuleState::Expander, + RuleState::None => RuleState::Matcher, + }; + } +} + +fn update_macro_state(state: &mut MacroMatcherParseState, tok: &SyntaxToken) { + if !state.in_invoc_body { + if tok.kind() == T!['{'] || tok.kind() == T!['('] { + state.in_invoc_body = true; + } + return; + } + + match state.paren_ty { + Some((open, close)) => { + if tok.kind() == open { + state.paren_level += 1; + } else if tok.kind() == close { + state.paren_level -= 1; + if state.paren_level == 0 { + state.rule_state.transition(); + state.paren_ty = None; + } + } + } + None => { + match tok.kind() { + T!['('] => { + state.paren_ty = Some((T!['('], T![')'])); + } + T!['{'] => { + state.paren_ty = Some((T!['{'], T!['}'])); + } + T!['['] => { + state.paren_ty = Some((T!['['], T![']'])); + } + _ => (), + } + if state.paren_ty.is_some() { + state.paren_level = 1; + state.rule_state.transition(); + } + } + } +} + +fn is_metavariable(token: &SyntaxToken) -> Option<TextRange> { + match token.kind() { + kind if kind == SyntaxKind::IDENT || kind.is_keyword() => { + if let Some(_dollar) = token.prev_token().filter(|t| t.kind() == T![$]) { + return Some(token.text_range()); + } + } + _ => (), + }; + None +} diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/tags.rs b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/tags.rs new file mode 100644 index 000000000..5262770f3 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/tags.rs @@ -0,0 +1,340 @@ +//! Defines token tags we use for syntax highlighting. +//! A tag is not unlike a CSS class. + +use std::{ + fmt::{self, Write}, + ops, +}; + +use ide_db::SymbolKind; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Highlight { + pub tag: HlTag, + pub mods: HlMods, +} + +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct HlMods(u32); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum HlTag { + Symbol(SymbolKind), + + AttributeBracket, + BoolLiteral, + BuiltinType, + ByteLiteral, + CharLiteral, + Comment, + EscapeSequence, + FormatSpecifier, + Keyword, + NumericLiteral, + Operator(HlOperator), + Punctuation(HlPunct), + StringLiteral, + UnresolvedReference, + + // For things which don't have a specific highlight. + None, +} + +// Don't forget to adjust the feature description in crates/ide/src/syntax_highlighting.rs. +// And make sure to use the lsp strings used when converting to the protocol in crates\rust-analyzer\src\semantic_tokens.rs, not the names of the variants here. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[repr(u8)] +pub enum HlMod { + /// Used for items in traits and impls. + Associated = 0, + /// Used with keywords like `async` and `await`. + Async, + /// Used to differentiate individual elements within attributes. + Attribute, + /// Callable item or value. + Callable, + /// Value that is being consumed in a function call + Consuming, + /// Used with keywords like `if` and `break`. + ControlFlow, + /// Used for crate names, like `serde`. + CrateRoot, + /// Used for items from built-in crates (std, core, alloc, test and proc_macro). + DefaultLibrary, + /// `foo` in `fn foo(x: i32)` is a definition, `foo` in `foo(90 + 2)` is + /// not. + Definition, + /// Doc-strings like this one. + Documentation, + /// Highlighting injection like rust code in doc strings or ra_fixture. + Injected, + /// Used for intra doc links in doc injection. + IntraDocLink, + /// Used for items from other crates. + Library, + /// Mutable binding. + Mutable, + /// Used for public items. + Public, + /// Immutable reference. + Reference, + /// Used for associated functions. + Static, + /// Used for items in traits and trait impls. + Trait, + // Keep this last! + /// Used for unsafe functions, unsafe traits, mutable statics, union accesses and unsafe operations. + Unsafe, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum HlPunct { + /// [] + Bracket, + /// {} + Brace, + /// () + Parenthesis, + /// <> + Angle, + /// , + Comma, + /// . + Dot, + /// : + Colon, + /// ; + Semi, + /// ! (only for macro calls) + MacroBang, + /// + Other, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum HlOperator { + /// |, &, !, ^, |=, &=, ^= + Bitwise, + /// +, -, *, /, +=, -=, *=, /= + Arithmetic, + /// &&, ||, ! + Logical, + /// >, <, ==, >=, <=, != + Comparison, + /// + Other, +} + +impl HlTag { + fn as_str(self) -> &'static str { + match self { + HlTag::Symbol(symbol) => match symbol { + SymbolKind::Attribute => "attribute", + SymbolKind::BuiltinAttr => "builtin_attr", + SymbolKind::Const => "constant", + SymbolKind::ConstParam => "const_param", + SymbolKind::Derive => "derive", + SymbolKind::DeriveHelper => "derive_helper", + SymbolKind::Enum => "enum", + SymbolKind::Field => "field", + SymbolKind::Function => "function", + SymbolKind::Impl => "self_type", + SymbolKind::Label => "label", + SymbolKind::LifetimeParam => "lifetime", + SymbolKind::Local => "variable", + SymbolKind::Macro => "macro", + SymbolKind::Module => "module", + SymbolKind::SelfParam => "self_keyword", + SymbolKind::SelfType => "self_type_keyword", + SymbolKind::Static => "static", + SymbolKind::Struct => "struct", + SymbolKind::ToolModule => "tool_module", + SymbolKind::Trait => "trait", + SymbolKind::TypeAlias => "type_alias", + SymbolKind::TypeParam => "type_param", + SymbolKind::Union => "union", + SymbolKind::ValueParam => "value_param", + SymbolKind::Variant => "enum_variant", + }, + HlTag::AttributeBracket => "attribute_bracket", + HlTag::BoolLiteral => "bool_literal", + HlTag::BuiltinType => "builtin_type", + HlTag::ByteLiteral => "byte_literal", + HlTag::CharLiteral => "char_literal", + HlTag::Comment => "comment", + HlTag::EscapeSequence => "escape_sequence", + HlTag::FormatSpecifier => "format_specifier", + HlTag::Keyword => "keyword", + HlTag::Punctuation(punct) => match punct { + HlPunct::Bracket => "bracket", + HlPunct::Brace => "brace", + HlPunct::Parenthesis => "parenthesis", + HlPunct::Angle => "angle", + HlPunct::Comma => "comma", + HlPunct::Dot => "dot", + HlPunct::Colon => "colon", + HlPunct::Semi => "semicolon", + HlPunct::MacroBang => "macro_bang", + HlPunct::Other => "punctuation", + }, + HlTag::NumericLiteral => "numeric_literal", + HlTag::Operator(op) => match op { + HlOperator::Bitwise => "bitwise", + HlOperator::Arithmetic => "arithmetic", + HlOperator::Logical => "logical", + HlOperator::Comparison => "comparison", + HlOperator::Other => "operator", + }, + HlTag::StringLiteral => "string_literal", + HlTag::UnresolvedReference => "unresolved_reference", + HlTag::None => "none", + } + } +} + +impl fmt::Display for HlTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self.as_str(), f) + } +} + +impl HlMod { + const ALL: &'static [HlMod; HlMod::Unsafe as u8 as usize + 1] = &[ + HlMod::Associated, + HlMod::Async, + HlMod::Attribute, + HlMod::Callable, + HlMod::Consuming, + HlMod::ControlFlow, + HlMod::CrateRoot, + HlMod::DefaultLibrary, + HlMod::Definition, + HlMod::Documentation, + HlMod::Injected, + HlMod::IntraDocLink, + HlMod::Library, + HlMod::Mutable, + HlMod::Public, + HlMod::Reference, + HlMod::Static, + HlMod::Trait, + HlMod::Unsafe, + ]; + + fn as_str(self) -> &'static str { + match self { + HlMod::Associated => "associated", + HlMod::Async => "async", + HlMod::Attribute => "attribute", + HlMod::Callable => "callable", + HlMod::Consuming => "consuming", + HlMod::ControlFlow => "control", + HlMod::CrateRoot => "crate_root", + HlMod::DefaultLibrary => "default_library", + HlMod::Definition => "declaration", + HlMod::Documentation => "documentation", + HlMod::Injected => "injected", + HlMod::IntraDocLink => "intra_doc_link", + HlMod::Library => "library", + HlMod::Mutable => "mutable", + HlMod::Public => "public", + HlMod::Reference => "reference", + HlMod::Static => "static", + HlMod::Trait => "trait", + HlMod::Unsafe => "unsafe", + } + } + + fn mask(self) -> u32 { + 1 << (self as u32) + } +} + +impl fmt::Display for HlMod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self.as_str(), f) + } +} + +impl fmt::Display for Highlight { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.tag.fmt(f)?; + for modifier in self.mods.iter() { + f.write_char('.')?; + modifier.fmt(f)?; + } + Ok(()) + } +} + +impl From<HlTag> for Highlight { + fn from(tag: HlTag) -> Highlight { + Highlight::new(tag) + } +} + +impl From<HlOperator> for Highlight { + fn from(op: HlOperator) -> Highlight { + Highlight::new(HlTag::Operator(op)) + } +} + +impl From<HlPunct> for Highlight { + fn from(punct: HlPunct) -> Highlight { + Highlight::new(HlTag::Punctuation(punct)) + } +} + +impl From<SymbolKind> for Highlight { + fn from(sym: SymbolKind) -> Highlight { + Highlight::new(HlTag::Symbol(sym)) + } +} + +impl Highlight { + pub(crate) fn new(tag: HlTag) -> Highlight { + Highlight { tag, mods: HlMods::default() } + } + pub fn is_empty(&self) -> bool { + self.tag == HlTag::None && self.mods == HlMods::default() + } +} + +impl ops::BitOr<HlMod> for HlTag { + type Output = Highlight; + + fn bitor(self, rhs: HlMod) -> Highlight { + Highlight::new(self) | rhs + } +} + +impl ops::BitOrAssign<HlMod> for HlMods { + fn bitor_assign(&mut self, rhs: HlMod) { + self.0 |= rhs.mask(); + } +} + +impl ops::BitOrAssign<HlMod> for Highlight { + fn bitor_assign(&mut self, rhs: HlMod) { + self.mods |= rhs; + } +} + +impl ops::BitOr<HlMod> for Highlight { + type Output = Highlight; + + fn bitor(mut self, rhs: HlMod) -> Highlight { + self |= rhs; + self + } +} + +impl HlMods { + pub fn contains(self, m: HlMod) -> bool { + self.0 & m.mask() == m.mask() + } + + pub fn iter(self) -> impl Iterator<Item = HlMod> { + HlMod::ALL.iter().copied().filter(move |it| self.0 & it.mask() == it.mask()) + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_assoc_functions.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_assoc_functions.html new file mode 100644 index 000000000..e07fd3925 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_assoc_functions.html @@ -0,0 +1,62 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="keyword">fn</span> <span class="function declaration">not_static</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> + +<span class="keyword">struct</span> <span class="struct declaration">foo</span> <span class="brace">{</span><span class="brace">}</span> + +<span class="keyword">impl</span> <span class="struct">foo</span> <span class="brace">{</span> + <span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function associated declaration public static">is_static</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> + <span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function associated declaration public reference">is_not_static</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration reference">self</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> +<span class="brace">}</span> + +<span class="keyword">trait</span> <span class="trait declaration">t</span> <span class="brace">{</span> + <span class="keyword">fn</span> <span class="function associated declaration static trait">t_is_static</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> + <span class="keyword">fn</span> <span class="function associated declaration reference trait">t_is_not_static</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration reference">self</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> +<span class="brace">}</span> + +<span class="keyword">impl</span> <span class="trait">t</span> <span class="keyword">for</span> <span class="struct">foo</span> <span class="brace">{</span> + <span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function associated declaration public static trait">is_static</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> + <span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function associated declaration public reference trait">is_not_static</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration reference">self</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> +<span class="brace">}</span></code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_attributes.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_attributes.html new file mode 100644 index 000000000..1a4398814 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_attributes.html @@ -0,0 +1,58 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">allow</span><span class="parenthesis attribute">(</span><span class="none attribute">dead_code</span><span class="parenthesis attribute">)</span><span class="attribute_bracket attribute">]</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="tool_module attribute library">rustfmt</span><span class="operator attribute">::</span><span class="tool_module attribute library">skip</span><span class="attribute_bracket attribute">]</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="module attribute crate_root library">proc_macros</span><span class="operator attribute">::</span><span class="attribute attribute library">identity</span><span class="attribute_bracket attribute">]</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="attribute attribute default_library library">derive</span><span class="parenthesis attribute">(</span><span class="derive attribute default_library library">Copy</span><span class="parenthesis attribute">)</span><span class="attribute_bracket attribute">]</span> +<span class="comment documentation">/// This is a doc comment</span> +<span class="comment">// This is a normal comment</span> +<span class="comment documentation">/// This is a doc comment</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="attribute attribute default_library library">derive</span><span class="parenthesis attribute">(</span><span class="derive attribute default_library library">Copy</span><span class="parenthesis attribute">)</span><span class="attribute_bracket attribute">]</span> +<span class="comment">// This is another normal comment</span> +<span class="comment documentation">/// This is another doc comment</span> +<span class="comment">// This is another normal comment</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="attribute attribute default_library library">derive</span><span class="parenthesis attribute">(</span><span class="derive attribute default_library library">Copy</span><span class="parenthesis attribute">)</span><span class="attribute_bracket attribute">]</span> +<span class="comment">// The reason for these being here is to test AttrIds</span> +<span class="keyword">struct</span> <span class="struct declaration">Foo</span><span class="semicolon">;</span></code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_crate_root.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_crate_root.html new file mode 100644 index 000000000..1e4c06df7 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_crate_root.html @@ -0,0 +1,66 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="keyword">extern</span> <span class="keyword">crate</span> <span class="module crate_root library">foo</span><span class="semicolon">;</span> +<span class="keyword">use</span> <span class="module crate_root default_library library">core</span><span class="operator">::</span><span class="module default_library library">iter</span><span class="semicolon">;</span> + +<span class="keyword">pub</span> <span class="keyword">const</span> <span class="constant declaration public">NINETY_TWO</span><span class="colon">:</span> <span class="builtin_type">u8</span> <span class="operator">=</span> <span class="numeric_literal">92</span><span class="semicolon">;</span> + +<span class="keyword">use</span> <span class="module crate_root library">foo</span> <span class="keyword">as</span> <span class="module crate_root declaration library">foooo</span><span class="semicolon">;</span> + +<span class="keyword">pub</span><span class="parenthesis">(</span><span class="keyword crate_root public">crate</span><span class="parenthesis">)</span> <span class="keyword">fn</span> <span class="function declaration">main</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="keyword">let</span> <span class="variable declaration">baz</span> <span class="operator">=</span> <span class="module default_library library">iter</span><span class="operator">::</span><span class="function default_library library">repeat</span><span class="parenthesis">(</span><span class="numeric_literal">92</span><span class="parenthesis">)</span><span class="semicolon">;</span> +<span class="brace">}</span> + +<span class="keyword">mod</span> <span class="module declaration">bar</span> <span class="brace">{</span> + <span class="keyword">pub</span><span class="parenthesis">(</span><span class="keyword control">in</span> <span class="keyword crate_root public">super</span><span class="parenthesis">)</span> <span class="keyword">const</span> <span class="constant declaration">FORTY_TWO</span><span class="colon">:</span> <span class="builtin_type">u8</span> <span class="operator">=</span> <span class="numeric_literal">42</span><span class="semicolon">;</span> + + <span class="keyword">mod</span> <span class="module declaration">baz</span> <span class="brace">{</span> + <span class="keyword">use</span> <span class="keyword">super</span><span class="operator">::</span><span class="keyword crate_root public">super</span><span class="operator">::</span><span class="constant public">NINETY_TWO</span><span class="semicolon">;</span> + <span class="keyword">use</span> <span class="keyword crate_root public">crate</span><span class="operator">::</span><span class="module crate_root library">foooo</span><span class="operator">::</span><span class="struct library">Point</span><span class="semicolon">;</span> + + <span class="keyword">pub</span><span class="parenthesis">(</span><span class="keyword control">in</span> <span class="keyword">super</span><span class="operator">::</span><span class="keyword crate_root public">super</span><span class="parenthesis">)</span> <span class="keyword">const</span> <span class="constant declaration">TWENTY_NINE</span><span class="colon">:</span> <span class="builtin_type">u8</span> <span class="operator">=</span> <span class="numeric_literal">29</span><span class="semicolon">;</span> + <span class="brace">}</span> +<span class="brace">}</span> +</code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_default_library.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_default_library.html new file mode 100644 index 000000000..5d66f832d --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_default_library.html @@ -0,0 +1,50 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="keyword">use</span> <span class="module crate_root default_library library">core</span><span class="operator">::</span><span class="module default_library library">iter</span><span class="semicolon">;</span> + +<span class="keyword">fn</span> <span class="function declaration">main</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="keyword">let</span> <span class="variable declaration">foo</span> <span class="operator">=</span> <span class="enum_variant default_library library">Some</span><span class="parenthesis">(</span><span class="numeric_literal">92</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="variable declaration">nums</span> <span class="operator">=</span> <span class="module default_library library">iter</span><span class="operator">::</span><span class="function default_library library">repeat</span><span class="parenthesis">(</span><span class="variable">foo</span><span class="operator">.</span><span class="function associated consuming default_library library">unwrap</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="parenthesis">)</span><span class="semicolon">;</span> +<span class="brace">}</span></code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html new file mode 100644 index 000000000..a747b4bc1 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html @@ -0,0 +1,190 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="comment documentation">//! This is a module to test doc injection.</span> +<span class="comment documentation">//! ```</span> +<span class="comment documentation">//!</span><span class="comment documentation"> </span><span class="keyword injected">fn</span><span class="none injected"> </span><span class="function declaration injected">test</span><span class="parenthesis injected">(</span><span class="parenthesis injected">)</span><span class="none injected"> </span><span class="brace injected">{</span><span class="brace injected">}</span> +<span class="comment documentation">//! ```</span> + +<span class="keyword">mod</span> <span class="module declaration">outline_module</span><span class="semicolon">;</span> + +<span class="comment documentation">/// ```</span> +<span class="comment documentation">///</span><span class="comment documentation"> </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="punctuation injected">_</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="string_literal injected">"early doctests should not go boom"</span><span class="semicolon injected">;</span> +<span class="comment documentation">/// ```</span> +<span class="keyword">struct</span> <span class="struct declaration">Foo</span> <span class="brace">{</span> + <span class="field declaration">bar</span><span class="colon">:</span> <span class="builtin_type">bool</span><span class="comma">,</span> +<span class="brace">}</span> + +<span class="comment documentation">/// This is an impl with a code block.</span> +<span class="comment documentation">///</span> +<span class="comment documentation">/// ```</span> +<span class="comment documentation">///</span><span class="comment documentation"> </span><span class="keyword injected">fn</span><span class="none injected"> </span><span class="function declaration injected">foo</span><span class="parenthesis injected">(</span><span class="parenthesis injected">)</span><span class="none injected"> </span><span class="brace injected">{</span> +<span class="comment documentation">///</span> +<span class="comment documentation">///</span><span class="comment documentation"> </span><span class="brace injected">}</span> +<span class="comment documentation">/// ```</span> +<span class="keyword">impl</span> <span class="struct">Foo</span> <span class="brace">{</span> + <span class="comment documentation">/// ```</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="punctuation injected">_</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="string_literal injected">"Call me</span> + <span class="comment">// KILLER WHALE</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="string_literal injected"> Ishmael."</span><span class="semicolon injected">;</span> + <span class="comment documentation">/// ```</span> + <span class="keyword">pub</span> <span class="keyword">const</span> <span class="constant associated declaration public">bar</span><span class="colon">:</span> <span class="builtin_type">bool</span> <span class="operator">=</span> <span class="bool_literal">true</span><span class="semicolon">;</span> + + <span class="comment documentation">/// Constructs a new `Foo`.</span> + <span class="comment documentation">///</span> + <span class="comment documentation">/// # Examples</span> + <span class="comment documentation">///</span> + <span class="comment documentation">/// ```</span> + <span class="comment documentation">///</span><span class="comment documentation"> #</span><span class="none injected"> </span><span class="attribute_bracket attribute injected">#</span><span class="attribute_bracket attribute injected">!</span><span class="attribute_bracket attribute injected">[</span><span class="builtin_attr attribute injected library">allow</span><span class="parenthesis attribute injected">(</span><span class="none attribute injected">unused_mut</span><span class="parenthesis attribute injected">)</span><span class="attribute_bracket attribute injected">]</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="keyword injected">mut</span><span class="none injected"> </span><span class="variable declaration injected mutable">foo</span><span class="colon injected">:</span><span class="none injected"> </span><span class="struct injected">Foo</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="struct injected">Foo</span><span class="operator injected">::</span><span class="function injected">new</span><span class="parenthesis injected">(</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span> + <span class="comment documentation">/// ```</span> + <span class="keyword">pub</span> <span class="keyword">const</span> <span class="keyword">fn</span> <span class="function associated declaration public static">new</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="struct">Foo</span> <span class="brace">{</span> + <span class="struct">Foo</span> <span class="brace">{</span> <span class="field">bar</span><span class="colon">:</span> <span class="bool_literal">true</span> <span class="brace">}</span> + <span class="brace">}</span> + + <span class="comment documentation">/// `bar` method on `Foo`.</span> + <span class="comment documentation">///</span> + <span class="comment documentation">/// # Examples</span> + <span class="comment documentation">///</span> + <span class="comment documentation">/// ```</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="keyword injected">use</span><span class="none injected"> </span><span class="module injected">x</span><span class="operator injected">::</span><span class="module injected">y</span><span class="semicolon injected">;</span> + <span class="comment documentation">///</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="variable declaration injected">foo</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="struct injected">Foo</span><span class="operator injected">::</span><span class="function injected">new</span><span class="parenthesis injected">(</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span> + <span class="comment documentation">///</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="comment injected">// calls bar on foo</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="macro injected">assert</span><span class="macro_bang injected">!</span><span class="parenthesis injected">(</span><span class="none injected">foo</span><span class="operator injected">.</span><span class="none injected">bar</span><span class="parenthesis injected">(</span><span class="parenthesis injected">)</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span> + <span class="comment documentation">///</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="variable declaration injected">bar</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="variable injected">foo</span><span class="operator injected">.</span><span class="field injected">bar</span><span class="none injected"> </span><span class="logical injected">||</span><span class="none injected"> </span><span class="struct injected">Foo</span><span class="operator injected">::</span><span class="constant injected">bar</span><span class="semicolon injected">;</span> + <span class="comment documentation">///</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="comment injected">/* multi-line</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="comment injected"> comment */</span> + <span class="comment documentation">///</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="variable declaration injected reference">multi_line_string</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="string_literal injected">"Foo</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="string_literal injected"> bar</span><span class="escape_sequence injected">\n</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="string_literal injected"> "</span><span class="semicolon injected">;</span> + <span class="comment documentation">///</span> + <span class="comment documentation">/// ```</span> + <span class="comment documentation">///</span> + <span class="comment documentation">/// ```rust,no_run</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="variable declaration injected">foobar</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="struct injected">Foo</span><span class="operator injected">::</span><span class="function injected">new</span><span class="parenthesis injected">(</span><span class="parenthesis injected">)</span><span class="operator injected">.</span><span class="function injected">bar</span><span class="parenthesis injected">(</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span> + <span class="comment documentation">/// ```</span> + <span class="comment documentation">///</span> + <span class="comment documentation">/// ~~~rust,no_run</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="comment injected">// code block with tilde.</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="variable declaration injected">foobar</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="struct injected">Foo</span><span class="operator injected">::</span><span class="function injected">new</span><span class="parenthesis injected">(</span><span class="parenthesis injected">)</span><span class="operator injected">.</span><span class="function injected">bar</span><span class="parenthesis injected">(</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span> + <span class="comment documentation">/// ~~~</span> + <span class="comment documentation">///</span> + <span class="comment documentation">/// ```</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="comment injected">// functions</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="keyword injected">fn</span><span class="none injected"> </span><span class="function declaration injected">foo</span><span class="angle injected"><</span><span class="type_param declaration injected">T</span><span class="comma injected">,</span><span class="none injected"> </span><span class="keyword injected">const</span><span class="none injected"> </span><span class="const_param declaration injected">X</span><span class="colon injected">:</span><span class="none injected"> </span><span class="builtin_type injected">usize</span><span class="angle injected">></span><span class="parenthesis injected">(</span><span class="value_param declaration injected">arg</span><span class="colon injected">:</span><span class="none injected"> </span><span class="builtin_type injected">i32</span><span class="parenthesis injected">)</span><span class="none injected"> </span><span class="brace injected">{</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="none injected"> </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="variable declaration injected">x</span><span class="colon injected">:</span><span class="none injected"> </span><span class="type_param injected">T</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="const_param injected">X</span><span class="semicolon injected">;</span> + <span class="comment documentation">///</span><span class="comment documentation"> </span><span class="brace injected">}</span> + <span class="comment documentation">/// ```</span> + <span class="comment documentation">///</span> + <span class="comment documentation">/// ```sh</span> + <span class="comment documentation">/// echo 1</span> + <span class="comment documentation">/// ```</span> + <span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function associated declaration public reference">foo</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration reference">self</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="builtin_type">bool</span> <span class="brace">{</span> + <span class="bool_literal">true</span> + <span class="brace">}</span> +<span class="brace">}</span> + +<span class="comment documentation">/// </span><span class="struct documentation injected intra_doc_link">[`Foo`](Foo)</span><span class="comment documentation"> is a struct</span> +<span class="comment documentation">/// This function is > </span><span class="function documentation injected intra_doc_link">[`all_the_links`](all_the_links)</span><span class="comment documentation"> <</span> +<span class="comment documentation">/// </span><span class="macro documentation injected intra_doc_link">[`noop`](noop)</span><span class="comment documentation"> is a macro below</span> +<span class="comment documentation">/// </span><span class="struct documentation injected intra_doc_link">[`Item`]</span><span class="comment documentation"> is a struct in the module </span><span class="module documentation injected intra_doc_link">[`module`]</span> +<span class="comment documentation">///</span> +<span class="comment documentation">/// [`Item`]: module::Item</span> +<span class="comment documentation">/// [mix_and_match]: ThisShouldntResolve</span> +<span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function declaration public">all_the_links</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> + +<span class="keyword">pub</span> <span class="keyword">mod</span> <span class="module declaration public">module</span> <span class="brace">{</span> + <span class="keyword">pub</span> <span class="keyword">struct</span> <span class="struct declaration public">Item</span><span class="semicolon">;</span> +<span class="brace">}</span> + +<span class="comment documentation">/// ```</span> +<span class="comment documentation">///</span><span class="comment documentation"> </span><span class="keyword injected">macro_rules</span><span class="macro_bang injected">!</span><span class="none injected"> </span><span class="macro declaration injected">noop</span><span class="none injected"> </span><span class="brace injected">{</span><span class="none injected"> </span><span class="parenthesis injected">(</span><span class="punctuation injected">$</span><span class="none injected">expr</span><span class="colon injected">:</span><span class="none injected">expr</span><span class="parenthesis injected">)</span><span class="none injected"> </span><span class="operator injected">=</span><span class="angle injected">></span><span class="none injected"> </span><span class="brace injected">{</span><span class="none injected"> </span><span class="punctuation injected">$</span><span class="none injected">expr </span><span class="brace injected">}</span><span class="brace injected">}</span> +<span class="comment documentation">///</span><span class="comment documentation"> </span><span class="macro injected">noop</span><span class="macro_bang injected">!</span><span class="parenthesis injected">(</span><span class="numeric_literal injected">1</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span> +<span class="comment documentation">/// ```</span> +<span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">noop</span> <span class="brace">{</span> + <span class="parenthesis">(</span><span class="punctuation">$</span>expr<span class="colon">:</span>expr<span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="brace">{</span> + <span class="punctuation">$</span>expr + <span class="brace">}</span> +<span class="brace">}</span> + +<span class="comment documentation">/// ```rust</span> +<span class="comment documentation">///</span><span class="comment documentation"> </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="punctuation injected">_</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="function injected">example</span><span class="parenthesis injected">(</span><span class="operator injected">&</span><span class="bracket injected">[</span><span class="numeric_literal injected">1</span><span class="comma injected">,</span><span class="none injected"> </span><span class="numeric_literal injected">2</span><span class="comma injected">,</span><span class="none injected"> </span><span class="numeric_literal injected">3</span><span class="bracket injected">]</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span> +<span class="comment documentation">/// ```</span> +<span class="comment documentation">///</span> +<span class="comment documentation">/// ```</span> +<span class="comment documentation">///</span><span class="comment documentation"> </span><span class="keyword control injected">loop</span><span class="none injected"> </span><span class="brace injected">{</span><span class="brace injected">}</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">cfg_attr</span><span class="parenthesis attribute">(</span><span class="none attribute">not</span><span class="parenthesis attribute">(</span><span class="none attribute">feature</span> <span class="operator attribute">=</span> <span class="string_literal attribute">"false"</span><span class="parenthesis attribute">)</span><span class="comma attribute">,</span> <span class="none attribute">doc</span> <span class="operator attribute">=</span> <span class="string_literal attribute">"</span><span class="keyword control injected">loop</span><span class="none injected"> </span><span class="brace injected">{</span><span class="brace injected">}</span><span class="string_literal attribute">"</span><span class="parenthesis attribute">)</span><span class="attribute_bracket attribute">]</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">doc</span> <span class="operator attribute">=</span> <span class="string_literal attribute">"</span><span class="keyword control injected">loop</span><span class="none injected"> </span><span class="brace injected">{</span><span class="brace injected">}</span><span class="string_literal attribute">"</span><span class="attribute_bracket attribute">]</span> +<span class="comment documentation">/// ```</span> +<span class="comment documentation">///</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">cfg_attr</span><span class="parenthesis attribute">(</span><span class="none attribute">feature</span> <span class="operator attribute">=</span> <span class="string_literal attribute">"alloc"</span><span class="comma attribute">,</span> <span class="none attribute">doc</span> <span class="operator attribute">=</span> <span class="string_literal attribute">"```rust"</span><span class="parenthesis attribute">)</span><span class="attribute_bracket attribute">]</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">cfg_attr</span><span class="parenthesis attribute">(</span><span class="none attribute">not</span><span class="parenthesis attribute">(</span><span class="none attribute">feature</span> <span class="operator attribute">=</span> <span class="string_literal attribute">"alloc"</span><span class="parenthesis attribute">)</span><span class="comma attribute">,</span> <span class="none attribute">doc</span> <span class="operator attribute">=</span> <span class="string_literal attribute">"```ignore"</span><span class="parenthesis attribute">)</span><span class="attribute_bracket attribute">]</span> +<span class="comment documentation">///</span><span class="comment documentation"> </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="punctuation injected">_</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="function injected">example</span><span class="parenthesis injected">(</span><span class="operator injected">&</span><span class="module injected">alloc</span><span class="operator injected">::</span><span class="macro injected">vec</span><span class="macro_bang injected">!</span><span class="bracket injected">[</span><span class="numeric_literal injected">1</span><span class="comma injected">,</span><span class="none injected"> </span><span class="numeric_literal injected">2</span><span class="comma injected">,</span><span class="none injected"> </span><span class="numeric_literal injected">3</span><span class="bracket injected">]</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span> +<span class="comment documentation">/// ```</span> +<span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function declaration public">mix_and_match</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> + +<span class="comment documentation">/** +It is beyond me why you'd use these when you got /// +```rust +</span><span class="keyword injected">let</span><span class="none injected"> </span><span class="punctuation injected">_</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="function injected">example</span><span class="parenthesis injected">(</span><span class="operator injected">&</span><span class="bracket injected">[</span><span class="numeric_literal injected">1</span><span class="comma injected">,</span><span class="none injected"> </span><span class="numeric_literal injected">2</span><span class="comma injected">,</span><span class="none injected"> </span><span class="numeric_literal injected">3</span><span class="bracket injected">]</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span><span class="comment documentation"> +``` +</span><span class="function documentation injected intra_doc_link">[`block_comments2`]</span><span class="comment documentation"> tests these with indentation + */</span> +<span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function declaration public">block_comments</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> + +<span class="comment documentation">/** + Really, I don't get it + ```rust +</span><span class="comment documentation"> </span><span class="none injected"> </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="punctuation injected">_</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="function injected">example</span><span class="parenthesis injected">(</span><span class="operator injected">&</span><span class="bracket injected">[</span><span class="numeric_literal injected">1</span><span class="comma injected">,</span><span class="none injected"> </span><span class="numeric_literal injected">2</span><span class="comma injected">,</span><span class="none injected"> </span><span class="numeric_literal injected">3</span><span class="bracket injected">]</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span><span class="comment documentation"> + ``` + </span><span class="function documentation injected intra_doc_link">[`block_comments`]</span><span class="comment documentation"> tests these without indentation +*/</span> +<span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function declaration public">block_comments2</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> + +</code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_extern_crate.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_extern_crate.html new file mode 100644 index 000000000..af41796e2 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_extern_crate.html @@ -0,0 +1,47 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="keyword">extern</span> <span class="keyword">crate</span> <span class="module crate_root default_library library">std</span><span class="semicolon">;</span> +<span class="keyword">extern</span> <span class="keyword">crate</span> <span class="module crate_root library">alloc</span> <span class="keyword">as</span> <span class="module crate_root declaration library">abc</span><span class="semicolon">;</span> +</code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_general.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_general.html new file mode 100644 index 000000000..a97802cbb --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_general.html @@ -0,0 +1,233 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="keyword">use</span> <span class="module">inner</span><span class="operator">::</span><span class="brace">{</span><span class="self_keyword">self</span> <span class="keyword">as</span> <span class="module declaration">inner_mod</span><span class="brace">}</span><span class="semicolon">;</span> +<span class="keyword">mod</span> <span class="module declaration">inner</span> <span class="brace">{</span><span class="brace">}</span> + +<span class="keyword">pub</span> <span class="keyword">mod</span> <span class="module declaration public">ops</span> <span class="brace">{</span> + <span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">lang</span> <span class="operator attribute">=</span> <span class="string_literal attribute">"fn_once"</span><span class="attribute_bracket attribute">]</span> + <span class="keyword">pub</span> <span class="keyword">trait</span> <span class="trait declaration public">FnOnce</span><span class="angle"><</span><span class="type_param declaration">Args</span><span class="angle">></span> <span class="brace">{</span><span class="brace">}</span> + + <span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">lang</span> <span class="operator attribute">=</span> <span class="string_literal attribute">"fn_mut"</span><span class="attribute_bracket attribute">]</span> + <span class="keyword">pub</span> <span class="keyword">trait</span> <span class="trait declaration public">FnMut</span><span class="angle"><</span><span class="type_param declaration">Args</span><span class="angle">></span><span class="colon">:</span> <span class="trait public">FnOnce</span><span class="angle"><</span><span class="type_param">Args</span><span class="angle">></span> <span class="brace">{</span><span class="brace">}</span> + + <span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">lang</span> <span class="operator attribute">=</span> <span class="string_literal attribute">"fn"</span><span class="attribute_bracket attribute">]</span> + <span class="keyword">pub</span> <span class="keyword">trait</span> <span class="trait declaration public">Fn</span><span class="angle"><</span><span class="type_param declaration">Args</span><span class="angle">></span><span class="colon">:</span> <span class="trait public">FnMut</span><span class="angle"><</span><span class="type_param">Args</span><span class="angle">></span> <span class="brace">{</span><span class="brace">}</span> +<span class="brace">}</span> + +<span class="keyword">struct</span> <span class="struct declaration">Foo</span> <span class="brace">{</span> + <span class="field declaration">x</span><span class="colon">:</span> <span class="builtin_type">u32</span><span class="comma">,</span> +<span class="brace">}</span> + +<span class="keyword">trait</span> <span class="trait declaration">Bar</span> <span class="brace">{</span> + <span class="keyword">fn</span> <span class="function associated declaration reference trait">bar</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration reference">self</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="builtin_type">i32</span><span class="semicolon">;</span> +<span class="brace">}</span> + +<span class="keyword">impl</span> <span class="trait">Bar</span> <span class="keyword">for</span> <span class="struct">Foo</span> <span class="brace">{</span> + <span class="keyword">fn</span> <span class="function associated declaration reference trait">bar</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration reference">self</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="builtin_type">i32</span> <span class="brace">{</span> + <span class="self_keyword reference">self</span><span class="operator">.</span><span class="field">x</span> + <span class="brace">}</span> +<span class="brace">}</span> + +<span class="keyword">impl</span> <span class="struct">Foo</span> <span class="brace">{</span> + <span class="keyword">fn</span> <span class="function associated consuming declaration">baz</span><span class="parenthesis">(</span><span class="keyword">mut</span> <span class="self_keyword declaration mutable">self</span><span class="comma">,</span> <span class="value_param declaration">f</span><span class="colon">:</span> <span class="struct">Foo</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="builtin_type">i32</span> <span class="brace">{</span> + <span class="value_param">f</span><span class="operator">.</span><span class="function associated consuming">baz</span><span class="parenthesis">(</span><span class="self_keyword consuming mutable">self</span><span class="parenthesis">)</span> + <span class="brace">}</span> + + <span class="keyword">fn</span> <span class="function associated declaration mutable reference">qux</span><span class="parenthesis">(</span><span class="operator">&</span><span class="keyword">mut</span> <span class="self_keyword declaration mutable reference">self</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="self_keyword mutable reference">self</span><span class="operator">.</span><span class="field">x</span> <span class="operator">=</span> <span class="numeric_literal">0</span><span class="semicolon">;</span> + <span class="brace">}</span> + + <span class="keyword">fn</span> <span class="function associated declaration reference">quop</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration reference">self</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="builtin_type">i32</span> <span class="brace">{</span> + <span class="self_keyword reference">self</span><span class="operator">.</span><span class="field">x</span> + <span class="brace">}</span> +<span class="brace">}</span> + +<span class="keyword">use</span> <span class="self_keyword crate_root public">self</span><span class="operator">::</span><span class="struct">FooCopy</span><span class="operator">::</span><span class="brace">{</span><span class="self_keyword">self</span> <span class="keyword">as</span> <span class="struct declaration">BarCopy</span><span class="brace">}</span><span class="semicolon">;</span> + +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="attribute attribute default_library library">derive</span><span class="parenthesis attribute">(</span><span class="derive attribute default_library library">Copy</span><span class="parenthesis attribute">)</span><span class="attribute_bracket attribute">]</span> +<span class="keyword">struct</span> <span class="struct declaration">FooCopy</span> <span class="brace">{</span> + <span class="field declaration">x</span><span class="colon">:</span> <span class="builtin_type">u32</span><span class="comma">,</span> +<span class="brace">}</span> + +<span class="keyword">impl</span> <span class="struct">FooCopy</span> <span class="brace">{</span> + <span class="keyword">fn</span> <span class="function associated consuming declaration">baz</span><span class="parenthesis">(</span><span class="self_keyword declaration">self</span><span class="comma">,</span> <span class="value_param declaration">f</span><span class="colon">:</span> <span class="struct">FooCopy</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="builtin_type">u32</span> <span class="brace">{</span> + <span class="value_param">f</span><span class="operator">.</span><span class="function associated">baz</span><span class="parenthesis">(</span><span class="self_keyword">self</span><span class="parenthesis">)</span> + <span class="brace">}</span> + + <span class="keyword">fn</span> <span class="function associated declaration mutable reference">qux</span><span class="parenthesis">(</span><span class="operator">&</span><span class="keyword">mut</span> <span class="self_keyword declaration mutable reference">self</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="self_keyword mutable reference">self</span><span class="operator">.</span><span class="field">x</span> <span class="operator">=</span> <span class="numeric_literal">0</span><span class="semicolon">;</span> + <span class="brace">}</span> + + <span class="keyword">fn</span> <span class="function associated declaration reference">quop</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration reference">self</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="builtin_type">u32</span> <span class="brace">{</span> + <span class="self_keyword reference">self</span><span class="operator">.</span><span class="field">x</span> + <span class="brace">}</span> +<span class="brace">}</span> + +<span class="keyword">fn</span> <span class="function declaration">str</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="function">str</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> +<span class="brace">}</span> + +<span class="keyword">fn</span> <span class="function declaration">foo</span><span class="angle"><</span><span class="lifetime declaration">'a</span><span class="comma">,</span> <span class="type_param declaration">T</span><span class="angle">></span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="type_param">T</span> <span class="brace">{</span> + <span class="function">foo</span><span class="operator">::</span><span class="angle"><</span><span class="lifetime">'a</span><span class="comma">,</span> <span class="builtin_type">i32</span><span class="angle">></span><span class="parenthesis">(</span><span class="parenthesis">)</span> +<span class="brace">}</span> + +<span class="keyword">fn</span> <span class="function declaration">never</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="builtin_type">!</span> <span class="brace">{</span> + <span class="keyword control">loop</span> <span class="brace">{</span><span class="brace">}</span> +<span class="brace">}</span> + +<span class="keyword">fn</span> <span class="function declaration">const_param</span><span class="angle"><</span><span class="keyword">const</span> <span class="const_param declaration">FOO</span><span class="colon">:</span> <span class="builtin_type">usize</span><span class="angle">></span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="builtin_type">usize</span> <span class="brace">{</span> + <span class="function">const_param</span><span class="operator">::</span><span class="angle"><</span><span class="brace">{</span> <span class="const_param">FOO</span> <span class="brace">}</span><span class="angle">></span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="const_param">FOO</span> +<span class="brace">}</span> + +<span class="keyword">use</span> <span class="module public">ops</span><span class="operator">::</span><span class="trait public">Fn</span><span class="semicolon">;</span> +<span class="keyword">fn</span> <span class="function declaration">baz</span><span class="angle"><</span><span class="type_param declaration">F</span><span class="colon">:</span> <span class="trait public">Fn</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="parenthesis">(</span><span class="parenthesis">)</span><span class="angle">></span><span class="parenthesis">(</span><span class="value_param callable declaration">f</span><span class="colon">:</span> <span class="type_param">F</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="value_param callable">f</span><span class="parenthesis">(</span><span class="parenthesis">)</span> +<span class="brace">}</span> + +<span class="keyword">fn</span> <span class="function declaration">foobar</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="keyword">impl</span> <span class="trait default_library library">Copy</span> <span class="brace">{</span><span class="brace">}</span> + +<span class="keyword">fn</span> <span class="function declaration">foo</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="keyword">let</span> <span class="variable declaration">bar</span> <span class="operator">=</span> <span class="function">foobar</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> +<span class="brace">}</span> + +<span class="comment">// comment</span> +<span class="keyword">fn</span> <span class="function declaration">main</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="keyword">let</span> <span class="keyword">mut</span> <span class="variable declaration mutable">x</span> <span class="operator">=</span> <span class="numeric_literal">42</span><span class="semicolon">;</span> + <span class="variable mutable">x</span> <span class="arithmetic mutable">+=</span> <span class="numeric_literal">1</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="variable declaration mutable reference">y</span> <span class="operator">=</span> <span class="operator">&</span><span class="keyword">mut</span> <span class="variable mutable">x</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="variable declaration reference">z</span> <span class="operator">=</span> <span class="operator">&</span><span class="variable mutable reference">y</span><span class="semicolon">;</span> + + <span class="keyword">let</span> <span class="struct">Foo</span> <span class="brace">{</span> <span class="field">x</span><span class="colon">:</span> <span class="variable declaration">z</span><span class="comma">,</span> <span class="variable declaration">y</span> <span class="brace">}</span> <span class="operator">=</span> <span class="struct">Foo</span> <span class="brace">{</span> <span class="field">x</span><span class="colon">:</span> <span class="variable reference">z</span><span class="comma">,</span> <span class="variable mutable reference">y</span> <span class="brace">}</span><span class="semicolon">;</span> + + <span class="variable">y</span><span class="semicolon">;</span> + + <span class="keyword">let</span> <span class="keyword">mut</span> <span class="variable declaration mutable">foo</span> <span class="operator">=</span> <span class="struct">Foo</span> <span class="brace">{</span> <span class="field">x</span><span class="comma">,</span> <span class="unresolved_reference">y</span><span class="colon">:</span> <span class="variable mutable">x</span> <span class="brace">}</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="variable declaration">foo2</span> <span class="operator">=</span> <span class="struct">Foo</span> <span class="brace">{</span> <span class="field">x</span><span class="comma">,</span> <span class="unresolved_reference">y</span><span class="colon">:</span> <span class="variable mutable">x</span> <span class="brace">}</span><span class="semicolon">;</span> + <span class="variable mutable">foo</span><span class="operator">.</span><span class="function associated reference">quop</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="variable mutable">foo</span><span class="operator">.</span><span class="function associated mutable reference">qux</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="variable mutable">foo</span><span class="operator">.</span><span class="function associated consuming">baz</span><span class="parenthesis">(</span><span class="variable consuming">foo2</span><span class="parenthesis">)</span><span class="semicolon">;</span> + + <span class="keyword">let</span> <span class="keyword">mut</span> <span class="variable declaration mutable">copy</span> <span class="operator">=</span> <span class="struct">FooCopy</span> <span class="brace">{</span> <span class="field">x</span> <span class="brace">}</span><span class="semicolon">;</span> + <span class="variable mutable">copy</span><span class="operator">.</span><span class="function associated reference">quop</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="variable mutable">copy</span><span class="operator">.</span><span class="function associated mutable reference">qux</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="variable mutable">copy</span><span class="operator">.</span><span class="function associated">baz</span><span class="parenthesis">(</span><span class="variable mutable">copy</span><span class="parenthesis">)</span><span class="semicolon">;</span> + + <span class="keyword">let</span> <span class="variable callable declaration">a</span> <span class="operator">=</span> <span class="punctuation">|</span><span class="value_param declaration">x</span><span class="punctuation">|</span> <span class="value_param">x</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="variable callable declaration">bar</span> <span class="operator">=</span> <span class="struct">Foo</span><span class="operator">::</span><span class="function associated consuming">baz</span><span class="semicolon">;</span> + + <span class="keyword">let</span> <span class="variable declaration">baz</span> <span class="operator">=</span> <span class="parenthesis">(</span><span class="numeric_literal">-</span><span class="numeric_literal">42</span><span class="comma">,</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="variable declaration">baz</span> <span class="operator">=</span> <span class="operator">-</span><span class="variable">baz</span><span class="operator">.</span><span class="field">0</span><span class="semicolon">;</span> + + <span class="keyword">let</span> <span class="punctuation">_</span> <span class="operator">=</span> <span class="logical">!</span><span class="bool_literal">true</span><span class="semicolon">;</span> + + <span class="label declaration">'foo</span><span class="colon">:</span> <span class="keyword control">loop</span> <span class="brace">{</span> + <span class="keyword control">break</span> <span class="label">'foo</span><span class="semicolon">;</span> + <span class="keyword control">continue</span> <span class="label">'foo</span><span class="semicolon">;</span> + <span class="brace">}</span> +<span class="brace">}</span> + +<span class="keyword">enum</span> <span class="enum declaration">Option</span><span class="angle"><</span><span class="type_param declaration">T</span><span class="angle">></span> <span class="brace">{</span> + <span class="enum_variant declaration">Some</span><span class="parenthesis">(</span><span class="type_param">T</span><span class="parenthesis">)</span><span class="comma">,</span> + <span class="enum_variant declaration">None</span><span class="comma">,</span> +<span class="brace">}</span> +<span class="keyword">use</span> <span class="enum">Option</span><span class="operator">::</span><span class="punctuation">*</span><span class="semicolon">;</span> + +<span class="keyword">impl</span><span class="angle"><</span><span class="type_param declaration">T</span><span class="angle">></span> <span class="enum">Option</span><span class="angle"><</span><span class="type_param">T</span><span class="angle">></span> <span class="brace">{</span> + <span class="keyword">fn</span> <span class="function associated consuming declaration">and</span><span class="angle"><</span><span class="type_param declaration">U</span><span class="angle">></span><span class="parenthesis">(</span><span class="self_keyword declaration">self</span><span class="comma">,</span> <span class="value_param declaration">other</span><span class="colon">:</span> <span class="enum">Option</span><span class="angle"><</span><span class="type_param">U</span><span class="angle">></span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="enum">Option</span><span class="angle"><</span><span class="parenthesis">(</span><span class="type_param">T</span><span class="comma">,</span> <span class="type_param">U</span><span class="parenthesis">)</span><span class="angle">></span> <span class="brace">{</span> + <span class="keyword control">match</span> <span class="value_param">other</span> <span class="brace">{</span> + <span class="enum_variant">None</span> <span class="operator">=></span> <span class="unresolved_reference">unimplemented</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="comma">,</span> + <span class="variable declaration">Nope</span> <span class="operator">=></span> <span class="variable">Nope</span><span class="comma">,</span> + <span class="brace">}</span> + <span class="brace">}</span> +<span class="brace">}</span> + +<span class="keyword async">async</span> <span class="keyword">fn</span> <span class="function async declaration">learn_and_sing</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="keyword">let</span> <span class="variable declaration">song</span> <span class="operator">=</span> <span class="unresolved_reference">learn_song</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="operator">.</span><span class="keyword async control">await</span><span class="semicolon">;</span> + <span class="unresolved_reference">sing_song</span><span class="parenthesis">(</span><span class="variable consuming">song</span><span class="parenthesis">)</span><span class="operator">.</span><span class="keyword async control">await</span><span class="semicolon">;</span> +<span class="brace">}</span> + +<span class="keyword async">async</span> <span class="keyword">fn</span> <span class="function async declaration">async_main</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="keyword">let</span> <span class="variable declaration">f1</span> <span class="operator">=</span> <span class="function async">learn_and_sing</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="variable declaration">f2</span> <span class="operator">=</span> <span class="unresolved_reference">dance</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="unresolved_reference">futures</span><span class="operator">::</span><span class="unresolved_reference">join</span><span class="macro_bang">!</span><span class="parenthesis">(</span>f1<span class="comma">,</span> f2<span class="parenthesis">)</span><span class="semicolon">;</span> +<span class="brace">}</span> + +<span class="keyword">fn</span> <span class="function declaration">use_foo_items</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="keyword">let</span> <span class="variable declaration">bob</span> <span class="operator">=</span> <span class="module crate_root library">foo</span><span class="operator">::</span><span class="struct library">Person</span> <span class="brace">{</span> + <span class="field library">name</span><span class="colon">:</span> <span class="string_literal">"Bob"</span><span class="comma">,</span> + <span class="field library">age</span><span class="colon">:</span> <span class="module crate_root library">foo</span><span class="operator">::</span><span class="module library">consts</span><span class="operator">::</span><span class="constant library">NUMBER</span><span class="comma">,</span> + <span class="brace">}</span><span class="semicolon">;</span> + + <span class="keyword">let</span> <span class="variable declaration">control_flow</span> <span class="operator">=</span> <span class="module crate_root library">foo</span><span class="operator">::</span><span class="function library">identity</span><span class="parenthesis">(</span><span class="module crate_root library">foo</span><span class="operator">::</span><span class="enum library">ControlFlow</span><span class="operator">::</span><span class="enum_variant library">Continue</span><span class="parenthesis">)</span><span class="semicolon">;</span> + + <span class="keyword control">if</span> <span class="variable">control_flow</span><span class="operator">.</span><span class="function associated consuming library">should_die</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="module crate_root library">foo</span><span class="operator">::</span><span class="unresolved_reference">die</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="brace">}</span> +<span class="brace">}</span> + +<span class="keyword">pub</span> <span class="keyword">enum</span> <span class="enum declaration public">Bool</span> <span class="brace">{</span> <span class="enum_variant declaration public">True</span><span class="comma">,</span> <span class="enum_variant declaration public">False</span> <span class="brace">}</span> + +<span class="keyword">impl</span> <span class="enum public">Bool</span> <span class="brace">{</span> + <span class="keyword">pub</span> <span class="keyword">const</span> <span class="keyword">fn</span> <span class="function associated consuming declaration public">to_primitive</span><span class="parenthesis">(</span><span class="self_keyword declaration">self</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="builtin_type">bool</span> <span class="brace">{</span> + <span class="bool_literal">true</span> + <span class="brace">}</span> +<span class="brace">}</span> +<span class="keyword">const</span> <span class="constant declaration">USAGE_OF_BOOL</span><span class="colon">:</span><span class="builtin_type">bool</span> <span class="operator">=</span> <span class="enum public">Bool</span><span class="operator">::</span><span class="enum_variant public">True</span><span class="operator">.</span><span class="function associated consuming public">to_primitive</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + +<span class="keyword">trait</span> <span class="trait declaration">Baz</span> <span class="brace">{</span> + <span class="keyword">type</span> <span class="type_alias associated declaration trait">Qux</span><span class="semicolon">;</span> +<span class="brace">}</span> + +<span class="keyword">fn</span> <span class="function declaration">baz</span><span class="angle"><</span><span class="type_param declaration">T</span><span class="angle">></span><span class="parenthesis">(</span><span class="value_param declaration">t</span><span class="colon">:</span> <span class="type_param">T</span><span class="parenthesis">)</span> +<span class="keyword">where</span> + <span class="type_param">T</span><span class="colon">:</span> <span class="trait">Baz</span><span class="comma">,</span> + <span class="angle"><</span><span class="type_param">T</span> <span class="keyword">as</span> <span class="trait">Baz</span><span class="angle">></span><span class="operator">::</span><span class="type_alias associated trait">Qux</span><span class="colon">:</span> <span class="trait">Bar</span> <span class="brace">{</span><span class="brace">}</span> + +<span class="keyword">fn</span> <span class="function declaration">gp_shadows_trait</span><span class="angle"><</span><span class="type_param declaration">Baz</span><span class="colon">:</span> <span class="trait">Bar</span><span class="angle">></span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="type_param">Baz</span><span class="operator">::</span><span class="function associated reference trait">bar</span><span class="semicolon">;</span> +<span class="brace">}</span> + +</code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_injection.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_injection.html new file mode 100644 index 000000000..ced7d22f0 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_injection.html @@ -0,0 +1,62 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="keyword">fn</span> <span class="function declaration">fixture</span><span class="parenthesis">(</span><span class="value_param declaration reference">ra_fixture</span><span class="colon">:</span> <span class="operator">&</span><span class="builtin_type">str</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> + +<span class="keyword">fn</span> <span class="function declaration">main</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="function">fixture</span><span class="parenthesis">(</span><span class="string_literal">r#"</span> +<span class="keyword">trait</span> <span class="trait declaration">Foo</span> <span class="brace">{</span> + <span class="keyword">fn</span> <span class="function associated declaration static trait">foo</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="unresolved_reference">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"2 + 2 = {}"</span><span class="comma">,</span> <span class="numeric_literal">4</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="brace">}</span> +<span class="brace">}</span><span class="string_literal">"#</span> + <span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="function">fixture</span><span class="parenthesis">(</span><span class="string_literal">r"</span> +<span class="keyword">fn</span> <span class="function declaration">foo</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="function">foo</span><span class="parenthesis">(</span><span class="keyword">$0</span><span class="brace">{</span> + <span class="numeric_literal">92</span> + <span class="brace">}</span><span class="keyword">$0</span><span class="parenthesis">)</span> +<span class="brace">}</span><span class="string_literal">"</span> + <span class="parenthesis">)</span><span class="semicolon">;</span> +<span class="brace">}</span></code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_keywords.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_keywords.html new file mode 100644 index 000000000..66f9ede96 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_keywords.html @@ -0,0 +1,58 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="keyword">extern</span> <span class="keyword">crate</span> <span class="self_keyword crate_root public">self</span><span class="semicolon">;</span> + +<span class="keyword">use</span> <span class="keyword crate_root public">crate</span><span class="semicolon">;</span> +<span class="keyword">use</span> <span class="self_keyword crate_root public">self</span><span class="semicolon">;</span> +<span class="keyword">mod</span> <span class="module declaration">__</span> <span class="brace">{</span> + <span class="keyword">use</span> <span class="keyword crate_root public">super</span><span class="operator">::</span><span class="punctuation">*</span><span class="semicolon">;</span> +<span class="brace">}</span> + +<span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">void</span> <span class="brace">{</span> + <span class="parenthesis">(</span><span class="punctuation">$</span><span class="parenthesis">(</span><span class="punctuation">$</span>tt<span class="colon">:</span>tt<span class="parenthesis">)</span><span class="punctuation">*</span><span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="brace">{</span><span class="brace">}</span> +<span class="brace">}</span> +<span class="macro">void</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="keyword">Self</span><span class="parenthesis">)</span><span class="semicolon">;</span> +<span class="keyword">struct</span> <span class="struct declaration">__</span> <span class="keyword">where</span> <span class="self_type_keyword">Self</span><span class="colon">:</span><span class="semicolon">;</span> +<span class="keyword">fn</span> <span class="function declaration">__</span><span class="parenthesis">(</span><span class="punctuation">_</span><span class="colon">:</span> <span class="unresolved_reference">Self</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span></code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_lifetimes.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_lifetimes.html new file mode 100644 index 000000000..2d85fc8c9 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_lifetimes.html @@ -0,0 +1,55 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="attribute attribute default_library library">derive</span><span class="parenthesis attribute">(</span><span class="parenthesis attribute">)</span><span class="attribute_bracket attribute">]</span> +<span class="keyword">struct</span> <span class="struct declaration">Foo</span><span class="angle"><</span><span class="lifetime declaration">'a</span><span class="comma">,</span> <span class="lifetime declaration">'b</span><span class="comma">,</span> <span class="lifetime declaration">'c</span><span class="angle">></span> <span class="keyword">where</span> <span class="lifetime">'a</span><span class="colon">:</span> <span class="lifetime">'a</span><span class="comma">,</span> <span class="lifetime">'static</span><span class="colon">:</span> <span class="lifetime">'static</span> <span class="brace">{</span> + <span class="field declaration">field</span><span class="colon">:</span> <span class="operator">&</span><span class="lifetime">'a</span> <span class="parenthesis">(</span><span class="parenthesis">)</span><span class="comma">,</span> + <span class="field declaration">field2</span><span class="colon">:</span> <span class="operator">&</span><span class="lifetime">'static</span> <span class="parenthesis">(</span><span class="parenthesis">)</span><span class="comma">,</span> +<span class="brace">}</span> +<span class="keyword">impl</span><span class="angle"><</span><span class="lifetime declaration">'a</span><span class="angle">></span> <span class="struct">Foo</span><span class="angle"><</span><span class="lifetime">'_</span><span class="comma">,</span> <span class="lifetime">'a</span><span class="comma">,</span> <span class="lifetime">'static</span><span class="angle">></span> +<span class="keyword">where</span> + <span class="lifetime">'a</span><span class="colon">:</span> <span class="lifetime">'a</span><span class="comma">,</span> + <span class="lifetime">'static</span><span class="colon">:</span> <span class="lifetime">'static</span> +<span class="brace">{</span><span class="brace">}</span></code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_macros.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_macros.html new file mode 100644 index 000000000..54d427952 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_macros.html @@ -0,0 +1,96 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="module crate_root library">proc_macros</span><span class="operator">::</span><span class="macro library">mirror</span><span class="macro_bang">!</span> <span class="brace">{</span> + <span class="brace">{</span> + <span class="comma">,</span><span class="builtin_type">i32</span> <span class="colon">:</span><span class="field declaration public">x</span> <span class="keyword">pub</span> + <span class="comma">,</span><span class="builtin_type">i32</span> <span class="colon">:</span><span class="field declaration public">y</span> <span class="keyword">pub</span> + <span class="brace">}</span> <span class="struct declaration">Foo</span> <span class="keyword">struct</span> +<span class="brace">}</span> +<span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">def_fn</span> <span class="brace">{</span> + <span class="parenthesis">(</span><span class="punctuation">$</span><span class="parenthesis">(</span><span class="punctuation">$</span>tt<span class="colon">:</span>tt<span class="parenthesis">)</span><span class="punctuation">*</span><span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="brace">{</span><span class="punctuation">$</span><span class="parenthesis">(</span><span class="punctuation">$</span>tt<span class="parenthesis">)</span><span class="punctuation">*</span><span class="brace">}</span> +<span class="brace">}</span> + +<span class="macro">def_fn</span><span class="macro_bang">!</span> <span class="brace">{</span> + <span class="keyword">fn</span> <span class="function declaration">bar</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="operator">-</span><span class="operator">></span> <span class="builtin_type">u32</span> <span class="brace">{</span> + <span class="numeric_literal">100</span> + <span class="brace">}</span> +<span class="brace">}</span> + +<span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">dont_color_me_braces</span> <span class="brace">{</span> + <span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="brace">{</span><span class="numeric_literal">0</span><span class="brace">}</span> +<span class="brace">}</span> + +<span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">noop</span> <span class="brace">{</span> + <span class="parenthesis">(</span><span class="punctuation">$</span>expr<span class="colon">:</span>expr<span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="brace">{</span> + <span class="punctuation">$</span>expr + <span class="brace">}</span> +<span class="brace">}</span> + +<span class="comment documentation">/// textually shadow previous definition</span> +<span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">noop</span> <span class="brace">{</span> + <span class="parenthesis">(</span><span class="punctuation">$</span>expr<span class="colon">:</span>expr<span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="brace">{</span> + <span class="punctuation">$</span>expr + <span class="brace">}</span> +<span class="brace">}</span> + +<span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">keyword_frag</span> <span class="brace">{</span> + <span class="parenthesis">(</span><span class="punctuation">$</span>type<span class="colon">:</span>ty<span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="parenthesis">(</span><span class="punctuation">$</span>type<span class="parenthesis">)</span> +<span class="brace">}</span> + +<span class="keyword">macro</span> <span class="macro declaration">with_args</span><span class="parenthesis">(</span><span class="punctuation">$</span>i<span class="colon">:</span>ident<span class="parenthesis">)</span> <span class="brace">{</span> + <span class="punctuation">$</span>i +<span class="brace">}</span> + +<span class="keyword">macro</span> <span class="macro declaration">without_args</span> <span class="brace">{</span> + <span class="parenthesis">(</span><span class="punctuation">$</span>i<span class="colon">:</span>ident<span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="brace">{</span> + <span class="punctuation">$</span>i + <span class="brace">}</span> +<span class="brace">}</span> + +<span class="keyword">fn</span> <span class="function declaration">main</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="unresolved_reference">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello, {}!"</span><span class="comma">,</span> <span class="numeric_literal">92</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">dont_color_me_braces</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">noop</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="macro">noop</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="numeric_literal">1</span><span class="parenthesis">)</span><span class="parenthesis">)</span><span class="semicolon">;</span> +<span class="brace">}</span></code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_module_docs_inline.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_module_docs_inline.html new file mode 100644 index 000000000..8a1d69816 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_module_docs_inline.html @@ -0,0 +1,51 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="comment documentation">//! </span><span class="struct documentation injected intra_doc_link">[Struct]</span> +<span class="comment documentation">//! This is an intra doc injection test for modules</span> +<span class="comment documentation">//! </span><span class="struct documentation injected intra_doc_link">[Struct]</span> +<span class="comment documentation">//! This is an intra doc injection test for modules</span> + +<span class="keyword">pub</span> <span class="keyword">struct</span> <span class="struct declaration public">Struct</span><span class="semicolon">;</span> +</code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_module_docs_outline.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_module_docs_outline.html new file mode 100644 index 000000000..c4c3e3dc2 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_module_docs_outline.html @@ -0,0 +1,50 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="comment documentation">/// </span><span class="struct documentation injected intra_doc_link">[crate::foo::Struct]</span> +<span class="comment documentation">/// This is an intra doc injection test for modules</span> +<span class="comment documentation">/// </span><span class="struct documentation injected intra_doc_link">[crate::foo::Struct]</span> +<span class="comment documentation">/// This is an intra doc injection test for modules</span> +<span class="keyword">mod</span> <span class="module declaration">foo</span><span class="semicolon">;</span> +</code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_operators.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_operators.html new file mode 100644 index 000000000..2369071ae --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_operators.html @@ -0,0 +1,58 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="keyword">fn</span> <span class="function declaration">main</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="numeric_literal">1</span> <span class="arithmetic">+</span> <span class="numeric_literal">1</span> <span class="arithmetic">-</span> <span class="numeric_literal">1</span> <span class="arithmetic">*</span> <span class="numeric_literal">1</span> <span class="arithmetic">/</span> <span class="numeric_literal">1</span> <span class="arithmetic">%</span> <span class="numeric_literal">1</span> <span class="bitwise">|</span> <span class="numeric_literal">1</span> <span class="bitwise">&</span> <span class="numeric_literal">1</span> <span class="logical">!</span> <span class="numeric_literal">1</span> <span class="bitwise">^</span> <span class="numeric_literal">1</span> <span class="bitwise">>></span> <span class="numeric_literal">1</span> <span class="bitwise"><<</span> <span class="numeric_literal">1</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="keyword">mut</span> <span class="variable declaration mutable">a</span> <span class="operator">=</span> <span class="numeric_literal">0</span><span class="semicolon">;</span> + <span class="variable mutable">a</span> <span class="arithmetic mutable">+=</span> <span class="numeric_literal">1</span><span class="semicolon">;</span> + <span class="variable mutable">a</span> <span class="arithmetic mutable">-=</span> <span class="numeric_literal">1</span><span class="semicolon">;</span> + <span class="variable mutable">a</span> <span class="arithmetic mutable">*=</span> <span class="numeric_literal">1</span><span class="semicolon">;</span> + <span class="variable mutable">a</span> <span class="arithmetic mutable">/=</span> <span class="numeric_literal">1</span><span class="semicolon">;</span> + <span class="variable mutable">a</span> <span class="arithmetic mutable">%=</span> <span class="numeric_literal">1</span><span class="semicolon">;</span> + <span class="variable mutable">a</span> <span class="bitwise mutable">|=</span> <span class="numeric_literal">1</span><span class="semicolon">;</span> + <span class="variable mutable">a</span> <span class="bitwise mutable">&=</span> <span class="numeric_literal">1</span><span class="semicolon">;</span> + <span class="variable mutable">a</span> <span class="bitwise mutable">^=</span> <span class="numeric_literal">1</span><span class="semicolon">;</span> + <span class="variable mutable">a</span> <span class="bitwise mutable">>>=</span> <span class="numeric_literal">1</span><span class="semicolon">;</span> + <span class="variable mutable">a</span> <span class="bitwise mutable"><<=</span> <span class="numeric_literal">1</span><span class="semicolon">;</span> +<span class="brace">}</span></code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_rainbow.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_rainbow.html new file mode 100644 index 000000000..bff35c897 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_rainbow.html @@ -0,0 +1,56 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="keyword">fn</span> <span class="function declaration">main</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="keyword">let</span> <span class="variable declaration reference" data-binding-hash="8121853618659664005" style="color: hsl(273,88%,88%);">hello</span> <span class="operator">=</span> <span class="string_literal">"hello"</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="variable declaration" data-binding-hash="2705725358298919760" style="color: hsl(76,47%,83%);">x</span> <span class="operator">=</span> <span class="variable reference" data-binding-hash="8121853618659664005" style="color: hsl(273,88%,88%);">hello</span><span class="operator">.</span><span class="unresolved_reference">to_string</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="variable declaration" data-binding-hash="3365759661443752373" style="color: hsl(15,86%,51%);">y</span> <span class="operator">=</span> <span class="variable reference" data-binding-hash="8121853618659664005" style="color: hsl(273,88%,88%);">hello</span><span class="operator">.</span><span class="unresolved_reference">to_string</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + + <span class="keyword">let</span> <span class="variable declaration reference" data-binding-hash="794745962933817518" style="color: hsl(127,71%,87%);">x</span> <span class="operator">=</span> <span class="string_literal">"other color please!"</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="variable declaration" data-binding-hash="6717528807933952652" style="color: hsl(90,74%,79%);">y</span> <span class="operator">=</span> <span class="variable reference" data-binding-hash="794745962933817518" style="color: hsl(127,71%,87%);">x</span><span class="operator">.</span><span class="unresolved_reference">to_string</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> +<span class="brace">}</span> + +<span class="keyword">fn</span> <span class="function declaration">bar</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="keyword">let</span> <span class="keyword">mut</span> <span class="variable declaration mutable reference" data-binding-hash="8121853618659664005" style="color: hsl(273,88%,88%);">hello</span> <span class="operator">=</span> <span class="string_literal">"hello"</span><span class="semicolon">;</span> +<span class="brace">}</span></code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_strings.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_strings.html new file mode 100644 index 000000000..c627bc9b0 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_strings.html @@ -0,0 +1,164 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">println</span> <span class="brace">{</span> + <span class="parenthesis">(</span><span class="punctuation">$</span><span class="parenthesis">(</span><span class="punctuation">$</span>arg<span class="colon">:</span>tt<span class="parenthesis">)</span><span class="punctuation">*</span><span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="parenthesis">(</span><span class="brace">{</span> + <span class="punctuation">$</span>crate<span class="colon">:</span><span class="colon">:</span>io<span class="colon">:</span><span class="colon">:</span>_print<span class="parenthesis">(</span><span class="punctuation">$</span>crate<span class="colon">:</span><span class="colon">:</span>format_args_nl<span class="punctuation">!</span><span class="parenthesis">(</span><span class="punctuation">$</span><span class="parenthesis">(</span><span class="punctuation">$</span>arg<span class="parenthesis">)</span><span class="punctuation">*</span><span class="parenthesis">)</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="brace">}</span><span class="parenthesis">)</span> +<span class="brace">}</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">rustc_builtin_macro</span><span class="attribute_bracket attribute">]</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">macro_export</span><span class="attribute_bracket attribute">]</span> +<span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">format_args</span> <span class="brace">{</span><span class="brace">}</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">rustc_builtin_macro</span><span class="attribute_bracket attribute">]</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">macro_export</span><span class="attribute_bracket attribute">]</span> +<span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">const_format_args</span> <span class="brace">{</span><span class="brace">}</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">rustc_builtin_macro</span><span class="attribute_bracket attribute">]</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">macro_export</span><span class="attribute_bracket attribute">]</span> +<span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">format_args_nl</span> <span class="brace">{</span><span class="brace">}</span> + +<span class="keyword">mod</span> <span class="module declaration">panic</span> <span class="brace">{</span> + <span class="keyword">pub</span> <span class="keyword">macro</span> <span class="macro declaration">panic_2015</span> <span class="brace">{</span> + <span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="parenthesis">(</span> + <span class="punctuation">$</span>crate<span class="colon">:</span><span class="colon">:</span>panicking<span class="colon">:</span><span class="colon">:</span>panic<span class="parenthesis">(</span><span class="string_literal">"explicit panic"</span><span class="parenthesis">)</span> + <span class="parenthesis">)</span><span class="comma">,</span> + <span class="parenthesis">(</span><span class="punctuation">$</span>msg<span class="colon">:</span>literal <span class="punctuation">$</span><span class="parenthesis">(</span><span class="comma">,</span><span class="parenthesis">)</span><span class="operator control">?</span><span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="parenthesis">(</span> + <span class="punctuation">$</span>crate<span class="colon">:</span><span class="colon">:</span>panicking<span class="colon">:</span><span class="colon">:</span>panic<span class="parenthesis">(</span><span class="punctuation">$</span>msg<span class="parenthesis">)</span> + <span class="parenthesis">)</span><span class="comma">,</span> + <span class="comment">// Use `panic_str` instead of `panic_display::<&str>` for non_fmt_panic lint.</span> + <span class="parenthesis">(</span><span class="punctuation">$</span>msg<span class="colon">:</span>expr <span class="punctuation">$</span><span class="parenthesis">(</span><span class="comma">,</span><span class="parenthesis">)</span><span class="operator control">?</span><span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="parenthesis">(</span> + <span class="punctuation">$</span>crate<span class="colon">:</span><span class="colon">:</span>panicking<span class="colon">:</span><span class="colon">:</span>panic_str<span class="parenthesis">(</span><span class="punctuation">$</span>msg<span class="parenthesis">)</span> + <span class="parenthesis">)</span><span class="comma">,</span> + <span class="comment">// Special-case the single-argument case for const_panic.</span> + <span class="parenthesis">(</span><span class="string_literal">"{}"</span><span class="comma">,</span> <span class="punctuation">$</span>arg<span class="colon">:</span>expr <span class="punctuation">$</span><span class="parenthesis">(</span><span class="comma">,</span><span class="parenthesis">)</span><span class="operator control">?</span><span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="parenthesis">(</span> + <span class="punctuation">$</span>crate<span class="colon">:</span><span class="colon">:</span>panicking<span class="colon">:</span><span class="colon">:</span>panic_display<span class="parenthesis">(</span><span class="operator">&</span><span class="punctuation">$</span>arg<span class="parenthesis">)</span> + <span class="parenthesis">)</span><span class="comma">,</span> + <span class="parenthesis">(</span><span class="punctuation">$</span>fmt<span class="colon">:</span>expr<span class="comma">,</span> <span class="punctuation">$</span><span class="parenthesis">(</span><span class="punctuation">$</span>arg<span class="colon">:</span>tt<span class="parenthesis">)</span><span class="punctuation">+</span><span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="parenthesis">(</span> + <span class="punctuation">$</span>crate<span class="colon">:</span><span class="colon">:</span>panicking<span class="colon">:</span><span class="colon">:</span>panic_fmt<span class="parenthesis">(</span><span class="punctuation">$</span>crate<span class="colon">:</span><span class="colon">:</span>const_format_args<span class="punctuation">!</span><span class="parenthesis">(</span><span class="punctuation">$</span>fmt<span class="comma">,</span> <span class="punctuation">$</span><span class="parenthesis">(</span><span class="punctuation">$</span>arg<span class="parenthesis">)</span><span class="punctuation">+</span><span class="parenthesis">)</span><span class="parenthesis">)</span> + <span class="parenthesis">)</span><span class="comma">,</span> + <span class="brace">}</span> +<span class="brace">}</span> + +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">rustc_builtin_macro</span><span class="parenthesis attribute">(</span><span class="none attribute">std_panic</span><span class="parenthesis attribute">)</span><span class="attribute_bracket attribute">]</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">macro_export</span><span class="attribute_bracket attribute">]</span> +<span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">panic</span> <span class="brace">{</span><span class="brace">}</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">rustc_builtin_macro</span><span class="attribute_bracket attribute">]</span> +<span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">assert</span> <span class="brace">{</span><span class="brace">}</span> +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">rustc_builtin_macro</span><span class="attribute_bracket attribute">]</span> +<span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">asm</span> <span class="brace">{</span><span class="brace">}</span> + +<span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">toho</span> <span class="brace">{</span> + <span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="parenthesis">(</span><span class="punctuation">$</span>crate<span class="colon">:</span><span class="colon">:</span>panic<span class="punctuation">!</span><span class="parenthesis">(</span><span class="string_literal">"not yet implemented"</span><span class="parenthesis">)</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="parenthesis">(</span><span class="punctuation">$</span><span class="parenthesis">(</span><span class="punctuation">$</span>arg<span class="colon">:</span>tt<span class="parenthesis">)</span><span class="punctuation">+</span><span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="parenthesis">(</span><span class="punctuation">$</span>crate<span class="colon">:</span><span class="colon">:</span>panic<span class="punctuation">!</span><span class="parenthesis">(</span><span class="string_literal">"not yet implemented: {}"</span><span class="comma">,</span> <span class="punctuation">$</span>crate<span class="colon">:</span><span class="colon">:</span>format_args<span class="punctuation">!</span><span class="parenthesis">(</span><span class="punctuation">$</span><span class="parenthesis">(</span><span class="punctuation">$</span>arg<span class="parenthesis">)</span><span class="punctuation">+</span><span class="parenthesis">)</span><span class="parenthesis">)</span><span class="parenthesis">)</span><span class="semicolon">;</span> +<span class="brace">}</span> + +<span class="keyword">fn</span> <span class="function declaration">main</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="escape_sequence">{{</span><span class="string_literal">Hello</span><span class="escape_sequence">}}</span><span class="string_literal">"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="comment">// from https://doc.rust-lang.org/std/fmt/index.html</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello"</span><span class="parenthesis">)</span><span class="semicolon">;</span> <span class="comment">// => "Hello"</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello, </span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal">!"</span><span class="comma">,</span> <span class="string_literal">"world"</span><span class="parenthesis">)</span><span class="semicolon">;</span> <span class="comment">// => "Hello, world!"</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"The number is </span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="numeric_literal">1</span><span class="parenthesis">)</span><span class="semicolon">;</span> <span class="comment">// => "The number is 1"</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="format_specifier">?</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="parenthesis">(</span><span class="numeric_literal">3</span><span class="comma">,</span> <span class="numeric_literal">4</span><span class="parenthesis">)</span><span class="parenthesis">)</span><span class="semicolon">;</span> <span class="comment">// => "(3, 4)"</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="variable">value</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> value<span class="operator">=</span><span class="numeric_literal">4</span><span class="parenthesis">)</span><span class="semicolon">;</span> <span class="comment">// => "4"</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal"> </span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="numeric_literal">1</span><span class="comma">,</span> <span class="numeric_literal">2</span><span class="parenthesis">)</span><span class="semicolon">;</span> <span class="comment">// => "1 2"</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="numeric_literal">0</span><span class="numeric_literal">4</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="numeric_literal">42</span><span class="parenthesis">)</span><span class="semicolon">;</span> <span class="comment">// => "0042" with leading zerosV</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="numeric_literal">1</span><span class="format_specifier">}</span><span class="string_literal"> </span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal"> </span><span class="format_specifier">{</span><span class="numeric_literal">0</span><span class="format_specifier">}</span><span class="string_literal"> </span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="numeric_literal">1</span><span class="comma">,</span> <span class="numeric_literal">2</span><span class="parenthesis">)</span><span class="semicolon">;</span> <span class="comment">// => "2 1 1 2"</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="variable">argument</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> argument <span class="operator">=</span> <span class="string_literal">"test"</span><span class="parenthesis">)</span><span class="semicolon">;</span> <span class="comment">// => "test"</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="variable">name</span><span class="format_specifier">}</span><span class="string_literal"> </span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="numeric_literal">1</span><span class="comma">,</span> name <span class="operator">=</span> <span class="numeric_literal">2</span><span class="parenthesis">)</span><span class="semicolon">;</span> <span class="comment">// => "2 1"</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="variable">a</span><span class="format_specifier">}</span><span class="string_literal"> </span><span class="format_specifier">{</span><span class="variable">c</span><span class="format_specifier">}</span><span class="string_literal"> </span><span class="format_specifier">{</span><span class="variable">b</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> a<span class="operator">=</span><span class="string_literal">"a"</span><span class="comma">,</span> b<span class="operator">=</span><span class="char_literal">'b'</span><span class="comma">,</span> c<span class="operator">=</span><span class="numeric_literal">3</span><span class="parenthesis">)</span><span class="semicolon">;</span> <span class="comment">// => "a 3 b"</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="escape_sequence">{{</span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="escape_sequence">}}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="numeric_literal">2</span><span class="parenthesis">)</span><span class="semicolon">;</span> <span class="comment">// => "{2}"</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="numeric_literal">5</span><span class="format_specifier">}</span><span class="string_literal">!"</span><span class="comma">,</span> <span class="string_literal">"x"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="numeric_literal">1</span><span class="format_specifier">$</span><span class="format_specifier">}</span><span class="string_literal">!"</span><span class="comma">,</span> <span class="string_literal">"x"</span><span class="comma">,</span> <span class="numeric_literal">5</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="numeric_literal">1</span><span class="format_specifier">:</span><span class="numeric_literal">0</span><span class="format_specifier">$</span><span class="format_specifier">}</span><span class="string_literal">!"</span><span class="comma">,</span> <span class="numeric_literal">5</span><span class="comma">,</span> <span class="string_literal">"x"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="variable">width</span><span class="format_specifier">$</span><span class="format_specifier">}</span><span class="string_literal">!"</span><span class="comma">,</span> <span class="string_literal">"x"</span><span class="comma">,</span> width <span class="operator">=</span> <span class="numeric_literal">5</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="format_specifier"><</span><span class="numeric_literal">5</span><span class="format_specifier">}</span><span class="string_literal">!"</span><span class="comma">,</span> <span class="string_literal">"x"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="format_specifier">-</span><span class="format_specifier"><</span><span class="numeric_literal">5</span><span class="format_specifier">}</span><span class="string_literal">!"</span><span class="comma">,</span> <span class="string_literal">"x"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="format_specifier">^</span><span class="numeric_literal">5</span><span class="format_specifier">}</span><span class="string_literal">!"</span><span class="comma">,</span> <span class="string_literal">"x"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="format_specifier">></span><span class="numeric_literal">5</span><span class="format_specifier">}</span><span class="string_literal">!"</span><span class="comma">,</span> <span class="string_literal">"x"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="format_specifier">+</span><span class="format_specifier">}</span><span class="string_literal">!"</span><span class="comma">,</span> <span class="numeric_literal">5</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="format_specifier">#</span><span class="variable">x</span><span class="format_specifier">}</span><span class="string_literal">!"</span><span class="comma">,</span> <span class="numeric_literal">27</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="numeric_literal">0</span><span class="numeric_literal">5</span><span class="format_specifier">}</span><span class="string_literal">!"</span><span class="comma">,</span> <span class="numeric_literal">5</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="numeric_literal">0</span><span class="numeric_literal">5</span><span class="format_specifier">}</span><span class="string_literal">!"</span><span class="comma">,</span> <span class="numeric_literal">-</span><span class="numeric_literal">5</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="format_specifier">#</span><span class="numeric_literal">0</span><span class="numeric_literal">10</span><span class="variable">x</span><span class="format_specifier">}</span><span class="string_literal">!"</span><span class="comma">,</span> <span class="numeric_literal">27</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="numeric_literal">0</span><span class="format_specifier">}</span><span class="string_literal"> is </span><span class="format_specifier">{</span><span class="numeric_literal">1</span><span class="format_specifier">:</span><span class="format_specifier">.</span><span class="numeric_literal">5</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="string_literal">"x"</span><span class="comma">,</span> <span class="numeric_literal">0.01</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="numeric_literal">1</span><span class="format_specifier">}</span><span class="string_literal"> is </span><span class="format_specifier">{</span><span class="numeric_literal">2</span><span class="format_specifier">:</span><span class="format_specifier">.</span><span class="numeric_literal">0</span><span class="format_specifier">$</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="numeric_literal">5</span><span class="comma">,</span> <span class="string_literal">"x"</span><span class="comma">,</span> <span class="numeric_literal">0.01</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="numeric_literal">0</span><span class="format_specifier">}</span><span class="string_literal"> is </span><span class="format_specifier">{</span><span class="numeric_literal">2</span><span class="format_specifier">:</span><span class="format_specifier">.</span><span class="numeric_literal">1</span><span class="format_specifier">$</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="string_literal">"x"</span><span class="comma">,</span> <span class="numeric_literal">5</span><span class="comma">,</span> <span class="numeric_literal">0.01</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal"> is </span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="format_specifier">.</span><span class="format_specifier">*</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="string_literal">"x"</span><span class="comma">,</span> <span class="numeric_literal">5</span><span class="comma">,</span> <span class="numeric_literal">0.01</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal"> is </span><span class="format_specifier">{</span><span class="numeric_literal">2</span><span class="format_specifier">:</span><span class="format_specifier">.</span><span class="format_specifier">*</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="string_literal">"x"</span><span class="comma">,</span> <span class="numeric_literal">5</span><span class="comma">,</span> <span class="numeric_literal">0.01</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal"> is </span><span class="format_specifier">{</span><span class="variable">number</span><span class="format_specifier">:</span><span class="format_specifier">.</span><span class="variable">prec</span><span class="format_specifier">$</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="string_literal">"x"</span><span class="comma">,</span> prec <span class="operator">=</span> <span class="numeric_literal">5</span><span class="comma">,</span> number <span class="operator">=</span> <span class="numeric_literal">0.01</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal">, `</span><span class="format_specifier">{</span><span class="variable">name</span><span class="format_specifier">:</span><span class="format_specifier">.</span><span class="format_specifier">*</span><span class="format_specifier">}</span><span class="string_literal">` has 3 fractional digits"</span><span class="comma">,</span> <span class="string_literal">"Hello"</span><span class="comma">,</span> <span class="numeric_literal">3</span><span class="comma">,</span> name<span class="operator">=</span><span class="numeric_literal">1234.56</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal">, `</span><span class="format_specifier">{</span><span class="variable">name</span><span class="format_specifier">:</span><span class="format_specifier">.</span><span class="format_specifier">*</span><span class="format_specifier">}</span><span class="string_literal">` has 3 characters"</span><span class="comma">,</span> <span class="string_literal">"Hello"</span><span class="comma">,</span> <span class="numeric_literal">3</span><span class="comma">,</span> name<span class="operator">=</span><span class="string_literal">"1234.56"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal">, `</span><span class="format_specifier">{</span><span class="variable">name</span><span class="format_specifier">:</span><span class="format_specifier">></span><span class="numeric_literal">8</span><span class="format_specifier">.</span><span class="format_specifier">*</span><span class="format_specifier">}</span><span class="string_literal">` has 3 right-aligned characters"</span><span class="comma">,</span> <span class="string_literal">"Hello"</span><span class="comma">,</span> <span class="numeric_literal">3</span><span class="comma">,</span> name<span class="operator">=</span><span class="string_literal">"1234.56"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + + <span class="keyword">let</span> <span class="punctuation">_</span> <span class="operator">=</span> <span class="string_literal">"{}"</span> + <span class="keyword">let</span> <span class="punctuation">_</span> <span class="operator">=</span> <span class="string_literal">"{{}}"</span><span class="semicolon">;</span> + + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="escape_sequence">{{</span><span class="escape_sequence">}}</span><span class="string_literal">"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="escape_sequence">{{</span><span class="string_literal"> Hello"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello </span><span class="escape_sequence">}}</span><span class="string_literal">"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="escape_sequence">{{</span><span class="string_literal">Hello</span><span class="escape_sequence">}}</span><span class="string_literal">"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="escape_sequence">{{</span><span class="string_literal"> Hello </span><span class="escape_sequence">}}</span><span class="string_literal">"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="escape_sequence">{{</span><span class="string_literal">Hello </span><span class="escape_sequence">}}</span><span class="string_literal">"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="escape_sequence">{{</span><span class="string_literal"> Hello</span><span class="escape_sequence">}}</span><span class="string_literal">"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">r"Hello, </span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal">!"</span><span class="comma">,</span> <span class="string_literal">"world"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + + <span class="comment">// escape sequences</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"Hello</span><span class="escape_sequence">\n</span><span class="string_literal">World"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="escape_sequence">\u{48}</span><span class="escape_sequence">\x65</span><span class="escape_sequence">\x6C</span><span class="escape_sequence">\x6C</span><span class="escape_sequence">\x6F</span><span class="string_literal"> World"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + + <span class="keyword">let</span> <span class="punctuation">_</span> <span class="operator">=</span> <span class="string_literal">"</span><span class="escape_sequence">\x28</span><span class="escape_sequence">\x28</span><span class="escape_sequence">\x00</span><span class="escape_sequence">\x63</span><span class="escape_sequence">\n</span><span class="string_literal">"</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="punctuation">_</span> <span class="operator">=</span> <span class="string_literal">b"</span><span class="escape_sequence">\x28</span><span class="escape_sequence">\x28</span><span class="escape_sequence">\x00</span><span class="escape_sequence">\x63</span><span class="escape_sequence">\n</span><span class="string_literal">"</span><span class="semicolon">;</span> + + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="escape_sequence">\x41</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> A <span class="operator">=</span> <span class="numeric_literal">92</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="variable">ничоси</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> ничоси <span class="operator">=</span> <span class="numeric_literal">92</span><span class="parenthesis">)</span><span class="semicolon">;</span> + + <span class="macro">println</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="format_specifier">:</span><span class="variable">x</span><span class="format_specifier">?</span><span class="format_specifier">}</span><span class="string_literal"> </span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal"> "</span><span class="comma">,</span> <span class="unresolved_reference">thingy</span><span class="comma">,</span> <span class="unresolved_reference">n2</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">panic</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="numeric_literal">0</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">panic</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"more </span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="numeric_literal">1</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">assert</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="bool_literal">true</span><span class="comma">,</span> <span class="string_literal">"</span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="comma">,</span> <span class="numeric_literal">1</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">assert</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="bool_literal">true</span><span class="comma">,</span> <span class="string_literal">"</span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal"> asdasd"</span><span class="comma">,</span> <span class="numeric_literal">1</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">toho</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal">fmt"</span><span class="comma">,</span> <span class="numeric_literal">0</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro unsafe">asm</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="string_literal">"mov eax, </span><span class="format_specifier">{</span><span class="numeric_literal">0</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro">format_args</span><span class="macro_bang">!</span><span class="parenthesis">(</span>concat<span class="punctuation">!</span><span class="parenthesis">(</span><span class="string_literal">"</span><span class="format_specifier">{</span><span class="format_specifier">}</span><span class="string_literal">"</span><span class="parenthesis">)</span><span class="comma">,</span> <span class="string_literal">"{}"</span><span class="parenthesis">)</span><span class="semicolon">;</span> +<span class="brace">}</span></code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_unsafe.html b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_unsafe.html new file mode 100644 index 000000000..0716bae75 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_unsafe.html @@ -0,0 +1,126 @@ + +<style> +body { margin: 0; } +pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } + +.lifetime { color: #DFAF8F; font-style: italic; } +.label { color: #DFAF8F; font-style: italic; } +.comment { color: #7F9F7F; } +.documentation { color: #629755; } +.intra_doc_link { font-style: italic; } +.injected { opacity: 0.65 ; } +.struct, .enum { color: #7CB8BB; } +.enum_variant { color: #BDE0F3; } +.string_literal { color: #CC9393; } +.field { color: #94BFF3; } +.function { color: #93E0E3; } +.function.unsafe { color: #BC8383; } +.trait.unsafe { color: #BC8383; } +.operator.unsafe { color: #BC8383; } +.mutable.unsafe { color: #BC8383; text-decoration: underline; } +.keyword.unsafe { color: #BC8383; font-weight: bold; } +.macro.unsafe { color: #BC8383; } +.parameter { color: #94BFF3; } +.text { color: #DCDCCC; } +.type { color: #7CB8BB; } +.builtin_type { color: #8CD0D3; } +.type_param { color: #DFAF8F; } +.attribute { color: #94BFF3; } +.numeric_literal { color: #BFEBBF; } +.bool_literal { color: #BFE6EB; } +.macro { color: #94BFF3; } +.derive { color: #94BFF3; font-style: italic; } +.module { color: #AFD8AF; } +.value_param { color: #DCDCCC; } +.variable { color: #DCDCCC; } +.format_specifier { color: #CC696B; } +.mutable { text-decoration: underline; } +.escape_sequence { color: #94BFF3; } +.keyword { color: #F0DFAF; font-weight: bold; } +.control { font-style: italic; } +.reference { font-style: italic; font-weight: bold; } + +.unresolved_reference { color: #FC5555; text-decoration: wavy underline; } +</style> +<pre><code><span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">id</span> <span class="brace">{</span> + <span class="parenthesis">(</span><span class="punctuation">$</span><span class="parenthesis">(</span><span class="punctuation">$</span>tt<span class="colon">:</span>tt<span class="parenthesis">)</span><span class="punctuation">*</span><span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="brace">{</span> + <span class="punctuation">$</span><span class="parenthesis">(</span><span class="punctuation">$</span>tt<span class="parenthesis">)</span><span class="punctuation">*</span> + <span class="brace">}</span><span class="semicolon">;</span> +<span class="brace">}</span> +<span class="keyword">macro_rules</span><span class="macro_bang">!</span> <span class="macro declaration">unsafe_deref</span> <span class="brace">{</span> + <span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="operator">=</span><span class="angle">></span> <span class="brace">{</span> + <span class="punctuation">*</span><span class="parenthesis">(</span><span class="operator">&</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="keyword">as</span> <span class="punctuation">*</span><span class="keyword">const</span> <span class="parenthesis">(</span><span class="parenthesis">)</span><span class="parenthesis">)</span> + <span class="brace">}</span><span class="semicolon">;</span> +<span class="brace">}</span> +<span class="keyword">static</span> <span class="keyword">mut</span> <span class="static declaration mutable unsafe">MUT_GLOBAL</span><span class="colon">:</span> <span class="struct">Struct</span> <span class="operator">=</span> <span class="struct">Struct</span> <span class="brace">{</span> <span class="field">field</span><span class="colon">:</span> <span class="numeric_literal">0</span> <span class="brace">}</span><span class="semicolon">;</span> +<span class="keyword">static</span> <span class="static declaration">GLOBAL</span><span class="colon">:</span> <span class="struct">Struct</span> <span class="operator">=</span> <span class="struct">Struct</span> <span class="brace">{</span> <span class="field">field</span><span class="colon">:</span> <span class="numeric_literal">0</span> <span class="brace">}</span><span class="semicolon">;</span> +<span class="keyword unsafe">unsafe</span> <span class="keyword">fn</span> <span class="function declaration unsafe">unsafe_fn</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> + +<span class="keyword">union</span> <span class="union declaration">Union</span> <span class="brace">{</span> + <span class="field declaration">a</span><span class="colon">:</span> <span class="builtin_type">u32</span><span class="comma">,</span> + <span class="field declaration">b</span><span class="colon">:</span> <span class="builtin_type">f32</span><span class="comma">,</span> +<span class="brace">}</span> + +<span class="keyword">struct</span> <span class="struct declaration">Struct</span> <span class="brace">{</span> <span class="field declaration">field</span><span class="colon">:</span> <span class="builtin_type">i32</span> <span class="brace">}</span> +<span class="keyword">impl</span> <span class="struct">Struct</span> <span class="brace">{</span> + <span class="keyword unsafe">unsafe</span> <span class="keyword">fn</span> <span class="function associated declaration reference unsafe">unsafe_method</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration reference">self</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> +<span class="brace">}</span> + +<span class="attribute_bracket attribute">#</span><span class="attribute_bracket attribute">[</span><span class="builtin_attr attribute library">repr</span><span class="parenthesis attribute">(</span><span class="none attribute">packed</span><span class="parenthesis attribute">)</span><span class="attribute_bracket attribute">]</span> +<span class="keyword">struct</span> <span class="struct declaration">Packed</span> <span class="brace">{</span> + <span class="field declaration">a</span><span class="colon">:</span> <span class="builtin_type">u16</span><span class="comma">,</span> +<span class="brace">}</span> + +<span class="keyword unsafe">unsafe</span> <span class="keyword">trait</span> <span class="trait declaration unsafe">UnsafeTrait</span> <span class="brace">{</span><span class="brace">}</span> +<span class="keyword unsafe">unsafe</span> <span class="keyword">impl</span> <span class="trait unsafe">UnsafeTrait</span> <span class="keyword">for</span> <span class="struct">Packed</span> <span class="brace">{</span><span class="brace">}</span> +<span class="keyword">impl</span> <span class="punctuation">!</span><span class="trait">UnsafeTrait</span> <span class="keyword">for</span> <span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> + +<span class="keyword">fn</span> <span class="function declaration">unsafe_trait_bound</span><span class="angle"><</span><span class="type_param declaration">T</span><span class="colon">:</span> <span class="trait">UnsafeTrait</span><span class="angle">></span><span class="parenthesis">(</span><span class="punctuation">_</span><span class="colon">:</span> <span class="type_param">T</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> + +<span class="keyword">trait</span> <span class="trait declaration">DoTheAutoref</span> <span class="brace">{</span> + <span class="keyword">fn</span> <span class="function associated declaration reference trait">calls_autoref</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration reference">self</span><span class="parenthesis">)</span><span class="semicolon">;</span> +<span class="brace">}</span> + +<span class="keyword">impl</span> <span class="trait">DoTheAutoref</span> <span class="keyword">for</span> <span class="builtin_type">u16</span> <span class="brace">{</span> + <span class="keyword">fn</span> <span class="function associated declaration reference trait">calls_autoref</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration reference">self</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> +<span class="brace">}</span> + +<span class="keyword">fn</span> <span class="function declaration">main</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> + <span class="keyword">let</span> <span class="variable declaration">x</span> <span class="operator">=</span> <span class="operator">&</span><span class="numeric_literal">5</span> <span class="keyword">as</span> <span class="keyword">*</span><span class="keyword">const</span> <span class="punctuation">_</span> <span class="keyword">as</span> <span class="keyword">*</span><span class="keyword">const</span> <span class="builtin_type">usize</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="variable declaration">u</span> <span class="operator">=</span> <span class="union">Union</span> <span class="brace">{</span> <span class="field">b</span><span class="colon">:</span> <span class="numeric_literal">0</span> <span class="brace">}</span><span class="semicolon">;</span> + + <span class="macro">id</span><span class="macro_bang">!</span> <span class="brace">{</span> + <span class="keyword unsafe">unsafe</span> <span class="brace">{</span> <span class="macro unsafe">unsafe_deref</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">}</span> + <span class="brace">}</span><span class="semicolon">;</span> + + <span class="keyword unsafe">unsafe</span> <span class="brace">{</span> + <span class="macro unsafe">unsafe_deref</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="macro unsafe">id</span><span class="macro_bang">!</span> <span class="brace">{</span> <span class="macro unsafe">unsafe_deref</span><span class="macro_bang">!</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">}</span><span class="semicolon">;</span> + + <span class="comment">// unsafe fn and method calls</span> + <span class="function unsafe">unsafe_fn</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="variable declaration">b</span> <span class="operator">=</span> <span class="variable">u</span><span class="operator">.</span><span class="field unsafe">b</span><span class="semicolon">;</span> + <span class="keyword control">match</span> <span class="variable">u</span> <span class="brace">{</span> + <span class="union">Union</span> <span class="brace">{</span> <span class="field unsafe">b</span><span class="colon">:</span> <span class="numeric_literal">0</span> <span class="brace">}</span> <span class="operator">=></span> <span class="parenthesis">(</span><span class="parenthesis">)</span><span class="comma">,</span> + <span class="union">Union</span> <span class="brace">{</span> <span class="field unsafe">a</span> <span class="brace">}</span> <span class="operator">=></span> <span class="parenthesis">(</span><span class="parenthesis">)</span><span class="comma">,</span> + <span class="brace">}</span> + <span class="struct">Struct</span> <span class="brace">{</span> <span class="field">field</span><span class="colon">:</span> <span class="numeric_literal">0</span> <span class="brace">}</span><span class="operator">.</span><span class="function associated reference unsafe">unsafe_method</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + + <span class="comment">// unsafe deref</span> + <span class="operator unsafe">*</span><span class="variable">x</span><span class="semicolon">;</span> + + <span class="comment">// unsafe access to a static mut</span> + <span class="static mutable unsafe">MUT_GLOBAL</span><span class="operator">.</span><span class="field">field</span><span class="semicolon">;</span> + <span class="static">GLOBAL</span><span class="operator">.</span><span class="field">field</span><span class="semicolon">;</span> + + <span class="comment">// unsafe ref of packed fields</span> + <span class="keyword">let</span> <span class="variable declaration">packed</span> <span class="operator">=</span> <span class="struct">Packed</span> <span class="brace">{</span> <span class="field">a</span><span class="colon">:</span> <span class="numeric_literal">0</span> <span class="brace">}</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="variable declaration reference">a</span> <span class="operator">=</span> <span class="operator unsafe">&</span><span class="variable">packed</span><span class="operator">.</span><span class="field">a</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="keyword unsafe">ref</span> <span class="variable declaration reference">a</span> <span class="operator">=</span> <span class="variable">packed</span><span class="operator">.</span><span class="field">a</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="struct">Packed</span> <span class="brace">{</span> <span class="keyword unsafe">ref</span> <span class="field">a</span> <span class="brace">}</span> <span class="operator">=</span> <span class="variable">packed</span><span class="semicolon">;</span> + <span class="keyword">let</span> <span class="struct">Packed</span> <span class="brace">{</span> <span class="field">a</span><span class="colon">:</span> <span class="keyword unsafe">ref</span> <span class="variable declaration reference">_a</span> <span class="brace">}</span> <span class="operator">=</span> <span class="variable">packed</span><span class="semicolon">;</span> + + <span class="comment">// unsafe auto ref of packed field</span> + <span class="variable">packed</span><span class="operator">.</span><span class="field">a</span><span class="operator">.</span><span class="function associated reference trait unsafe">calls_autoref</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> + <span class="brace">}</span> +<span class="brace">}</span></code></pre>
\ No newline at end of file diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/tests.rs b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/tests.rs new file mode 100644 index 000000000..99be7c664 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/tests.rs @@ -0,0 +1,1096 @@ +use std::time::Instant; + +use expect_test::{expect_file, ExpectFile}; +use ide_db::SymbolKind; +use test_utils::{bench, bench_fixture, skip_slow_tests, AssertLinear}; + +use crate::{fixture, FileRange, HlTag, TextRange}; + +#[test] +fn attributes() { + check_highlighting( + r#" +//- proc_macros: identity +//- minicore: derive, copy +#[allow(dead_code)] +#[rustfmt::skip] +#[proc_macros::identity] +#[derive(Copy)] +/// This is a doc comment +// This is a normal comment +/// This is a doc comment +#[derive(Copy)] +// This is another normal comment +/// This is another doc comment +// This is another normal comment +#[derive(Copy)] +// The reason for these being here is to test AttrIds +struct Foo; +"#, + expect_file!["./test_data/highlight_attributes.html"], + false, + ); +} + +#[test] +fn macros() { + check_highlighting( + r#" +//- proc_macros: mirror +proc_macros::mirror! { + { + ,i32 :x pub + ,i32 :y pub + } Foo struct +} +macro_rules! def_fn { + ($($tt:tt)*) => {$($tt)*} +} + +def_fn! { + fn bar() -> u32 { + 100 + } +} + +macro_rules! dont_color_me_braces { + () => {0} +} + +macro_rules! noop { + ($expr:expr) => { + $expr + } +} + +/// textually shadow previous definition +macro_rules! noop { + ($expr:expr) => { + $expr + } +} + +macro_rules! keyword_frag { + ($type:ty) => ($type) +} + +macro with_args($i:ident) { + $i +} + +macro without_args { + ($i:ident) => { + $i + } +} + +fn main() { + println!("Hello, {}!", 92); + dont_color_me_braces!(); + noop!(noop!(1)); +} +"#, + expect_file!["./test_data/highlight_macros.html"], + false, + ); +} + +/// If what you want to test feels like a specific entity consider making a new test instead, +/// this test fixture here in fact should shrink instead of grow ideally. +#[test] +fn test_highlighting() { + check_highlighting( + r#" +//- minicore: derive, copy +//- /main.rs crate:main deps:foo +use inner::{self as inner_mod}; +mod inner {} + +pub mod ops { + #[lang = "fn_once"] + pub trait FnOnce<Args> {} + + #[lang = "fn_mut"] + pub trait FnMut<Args>: FnOnce<Args> {} + + #[lang = "fn"] + pub trait Fn<Args>: FnMut<Args> {} +} + +struct Foo { + x: u32, +} + +trait Bar { + fn bar(&self) -> i32; +} + +impl Bar for Foo { + fn bar(&self) -> i32 { + self.x + } +} + +impl Foo { + fn baz(mut self, f: Foo) -> i32 { + f.baz(self) + } + + fn qux(&mut self) { + self.x = 0; + } + + fn quop(&self) -> i32 { + self.x + } +} + +use self::FooCopy::{self as BarCopy}; + +#[derive(Copy)] +struct FooCopy { + x: u32, +} + +impl FooCopy { + fn baz(self, f: FooCopy) -> u32 { + f.baz(self) + } + + fn qux(&mut self) { + self.x = 0; + } + + fn quop(&self) -> u32 { + self.x + } +} + +fn str() { + str(); +} + +fn foo<'a, T>() -> T { + foo::<'a, i32>() +} + +fn never() -> ! { + loop {} +} + +fn const_param<const FOO: usize>() -> usize { + const_param::<{ FOO }>(); + FOO +} + +use ops::Fn; +fn baz<F: Fn() -> ()>(f: F) { + f() +} + +fn foobar() -> impl Copy {} + +fn foo() { + let bar = foobar(); +} + +// comment +fn main() { + let mut x = 42; + x += 1; + let y = &mut x; + let z = &y; + + let Foo { x: z, y } = Foo { x: z, y }; + + y; + + let mut foo = Foo { x, y: x }; + let foo2 = Foo { x, y: x }; + foo.quop(); + foo.qux(); + foo.baz(foo2); + + let mut copy = FooCopy { x }; + copy.quop(); + copy.qux(); + copy.baz(copy); + + let a = |x| x; + let bar = Foo::baz; + + let baz = (-42,); + let baz = -baz.0; + + let _ = !true; + + 'foo: loop { + break 'foo; + continue 'foo; + } +} + +enum Option<T> { + Some(T), + None, +} +use Option::*; + +impl<T> Option<T> { + fn and<U>(self, other: Option<U>) -> Option<(T, U)> { + match other { + None => unimplemented!(), + Nope => Nope, + } + } +} + +async fn learn_and_sing() { + let song = learn_song().await; + sing_song(song).await; +} + +async fn async_main() { + let f1 = learn_and_sing(); + let f2 = dance(); + futures::join!(f1, f2); +} + +fn use_foo_items() { + let bob = foo::Person { + name: "Bob", + age: foo::consts::NUMBER, + }; + + let control_flow = foo::identity(foo::ControlFlow::Continue); + + if control_flow.should_die() { + foo::die!(); + } +} + +pub enum Bool { True, False } + +impl Bool { + pub const fn to_primitive(self) -> bool { + true + } +} +const USAGE_OF_BOOL:bool = Bool::True.to_primitive(); + +trait Baz { + type Qux; +} + +fn baz<T>(t: T) +where + T: Baz, + <T as Baz>::Qux: Bar {} + +fn gp_shadows_trait<Baz: Bar>() { + Baz::bar; +} + +//- /foo.rs crate:foo +pub struct Person { + pub name: &'static str, + pub age: u8, +} + +pub enum ControlFlow { + Continue, + Die, +} + +impl ControlFlow { + pub fn should_die(self) -> bool { + matches!(self, ControlFlow::Die) + } +} + +pub fn identity<T>(x: T) -> T { x } + +pub mod consts { + pub const NUMBER: i64 = 92; +} + +macro_rules! die { + () => { + panic!(); + }; +} +"#, + expect_file!["./test_data/highlight_general.html"], + false, + ); +} + +#[test] +fn test_lifetime_highlighting() { + check_highlighting( + r#" +//- minicore: derive + +#[derive()] +struct Foo<'a, 'b, 'c> where 'a: 'a, 'static: 'static { + field: &'a (), + field2: &'static (), +} +impl<'a> Foo<'_, 'a, 'static> +where + 'a: 'a, + 'static: 'static +{} +"#, + expect_file!["./test_data/highlight_lifetimes.html"], + false, + ); +} + +#[test] +fn test_keyword_highlighting() { + check_highlighting( + r#" +extern crate self; + +use crate; +use self; +mod __ { + use super::*; +} + +macro_rules! void { + ($($tt:tt)*) => {} +} +void!(Self); +struct __ where Self:; +fn __(_: Self) {} +"#, + expect_file!["./test_data/highlight_keywords.html"], + false, + ); +} + +#[test] +fn test_string_highlighting() { + // The format string detection is based on macro-expansion, + // thus, we have to copy the macro definition from `std` + check_highlighting( + r#" +macro_rules! println { + ($($arg:tt)*) => ({ + $crate::io::_print($crate::format_args_nl!($($arg)*)); + }) +} +#[rustc_builtin_macro] +#[macro_export] +macro_rules! format_args {} +#[rustc_builtin_macro] +#[macro_export] +macro_rules! const_format_args {} +#[rustc_builtin_macro] +#[macro_export] +macro_rules! format_args_nl {} + +mod panic { + pub macro panic_2015 { + () => ( + $crate::panicking::panic("explicit panic") + ), + ($msg:literal $(,)?) => ( + $crate::panicking::panic($msg) + ), + // Use `panic_str` instead of `panic_display::<&str>` for non_fmt_panic lint. + ($msg:expr $(,)?) => ( + $crate::panicking::panic_str($msg) + ), + // Special-case the single-argument case for const_panic. + ("{}", $arg:expr $(,)?) => ( + $crate::panicking::panic_display(&$arg) + ), + ($fmt:expr, $($arg:tt)+) => ( + $crate::panicking::panic_fmt($crate::const_format_args!($fmt, $($arg)+)) + ), + } +} + +#[rustc_builtin_macro(std_panic)] +#[macro_export] +macro_rules! panic {} +#[rustc_builtin_macro] +macro_rules! assert {} +#[rustc_builtin_macro] +macro_rules! asm {} + +macro_rules! toho { + () => ($crate::panic!("not yet implemented")); + ($($arg:tt)+) => ($crate::panic!("not yet implemented: {}", $crate::format_args!($($arg)+))); +} + +fn main() { + println!("Hello {{Hello}}"); + // from https://doc.rust-lang.org/std/fmt/index.html + println!("Hello"); // => "Hello" + println!("Hello, {}!", "world"); // => "Hello, world!" + println!("The number is {}", 1); // => "The number is 1" + println!("{:?}", (3, 4)); // => "(3, 4)" + println!("{value}", value=4); // => "4" + println!("{} {}", 1, 2); // => "1 2" + println!("{:04}", 42); // => "0042" with leading zerosV + println!("{1} {} {0} {}", 1, 2); // => "2 1 1 2" + println!("{argument}", argument = "test"); // => "test" + println!("{name} {}", 1, name = 2); // => "2 1" + println!("{a} {c} {b}", a="a", b='b', c=3); // => "a 3 b" + println!("{{{}}}", 2); // => "{2}" + println!("Hello {:5}!", "x"); + println!("Hello {:1$}!", "x", 5); + println!("Hello {1:0$}!", 5, "x"); + println!("Hello {:width$}!", "x", width = 5); + println!("Hello {:<5}!", "x"); + println!("Hello {:-<5}!", "x"); + println!("Hello {:^5}!", "x"); + println!("Hello {:>5}!", "x"); + println!("Hello {:+}!", 5); + println!("{:#x}!", 27); + println!("Hello {:05}!", 5); + println!("Hello {:05}!", -5); + println!("{:#010x}!", 27); + println!("Hello {0} is {1:.5}", "x", 0.01); + println!("Hello {1} is {2:.0$}", 5, "x", 0.01); + println!("Hello {0} is {2:.1$}", "x", 5, 0.01); + println!("Hello {} is {:.*}", "x", 5, 0.01); + println!("Hello {} is {2:.*}", "x", 5, 0.01); + println!("Hello {} is {number:.prec$}", "x", prec = 5, number = 0.01); + println!("{}, `{name:.*}` has 3 fractional digits", "Hello", 3, name=1234.56); + println!("{}, `{name:.*}` has 3 characters", "Hello", 3, name="1234.56"); + println!("{}, `{name:>8.*}` has 3 right-aligned characters", "Hello", 3, name="1234.56"); + + let _ = "{}" + let _ = "{{}}"; + + println!("Hello {{}}"); + println!("{{ Hello"); + println!("Hello }}"); + println!("{{Hello}}"); + println!("{{ Hello }}"); + println!("{{Hello }}"); + println!("{{ Hello}}"); + + println!(r"Hello, {}!", "world"); + + // escape sequences + println!("Hello\nWorld"); + println!("\u{48}\x65\x6C\x6C\x6F World"); + + let _ = "\x28\x28\x00\x63\n"; + let _ = b"\x28\x28\x00\x63\n"; + + println!("{\x41}", A = 92); + println!("{ничоси}", ничоси = 92); + + println!("{:x?} {} ", thingy, n2); + panic!("{}", 0); + panic!("more {}", 1); + assert!(true, "{}", 1); + assert!(true, "{} asdasd", 1); + toho!("{}fmt", 0); + asm!("mov eax, {0}"); + format_args!(concat!("{}"), "{}"); +}"#, + expect_file!["./test_data/highlight_strings.html"], + false, + ); +} + +#[test] +fn test_unsafe_highlighting() { + check_highlighting( + r#" +macro_rules! id { + ($($tt:tt)*) => { + $($tt)* + }; +} +macro_rules! unsafe_deref { + () => { + *(&() as *const ()) + }; +} +static mut MUT_GLOBAL: Struct = Struct { field: 0 }; +static GLOBAL: Struct = Struct { field: 0 }; +unsafe fn unsafe_fn() {} + +union Union { + a: u32, + b: f32, +} + +struct Struct { field: i32 } +impl Struct { + unsafe fn unsafe_method(&self) {} +} + +#[repr(packed)] +struct Packed { + a: u16, +} + +unsafe trait UnsafeTrait {} +unsafe impl UnsafeTrait for Packed {} +impl !UnsafeTrait for () {} + +fn unsafe_trait_bound<T: UnsafeTrait>(_: T) {} + +trait DoTheAutoref { + fn calls_autoref(&self); +} + +impl DoTheAutoref for u16 { + fn calls_autoref(&self) {} +} + +fn main() { + let x = &5 as *const _ as *const usize; + let u = Union { b: 0 }; + + id! { + unsafe { unsafe_deref!() } + }; + + unsafe { + unsafe_deref!(); + id! { unsafe_deref!() }; + + // unsafe fn and method calls + unsafe_fn(); + let b = u.b; + match u { + Union { b: 0 } => (), + Union { a } => (), + } + Struct { field: 0 }.unsafe_method(); + + // unsafe deref + *x; + + // unsafe access to a static mut + MUT_GLOBAL.field; + GLOBAL.field; + + // unsafe ref of packed fields + let packed = Packed { a: 0 }; + let a = &packed.a; + let ref a = packed.a; + let Packed { ref a } = packed; + let Packed { a: ref _a } = packed; + + // unsafe auto ref of packed field + packed.a.calls_autoref(); + } +} +"#, + expect_file!["./test_data/highlight_unsafe.html"], + false, + ); +} + +#[test] +fn test_highlight_doc_comment() { + check_highlighting( + r#" +//- /main.rs +//! This is a module to test doc injection. +//! ``` +//! fn test() {} +//! ``` + +mod outline_module; + +/// ``` +/// let _ = "early doctests should not go boom"; +/// ``` +struct Foo { + bar: bool, +} + +/// This is an impl with a code block. +/// +/// ``` +/// fn foo() { +/// +/// } +/// ``` +impl Foo { + /// ``` + /// let _ = "Call me + // KILLER WHALE + /// Ishmael."; + /// ``` + pub const bar: bool = true; + + /// Constructs a new `Foo`. + /// + /// # Examples + /// + /// ``` + /// # #![allow(unused_mut)] + /// let mut foo: Foo = Foo::new(); + /// ``` + pub const fn new() -> Foo { + Foo { bar: true } + } + + /// `bar` method on `Foo`. + /// + /// # Examples + /// + /// ``` + /// use x::y; + /// + /// let foo = Foo::new(); + /// + /// // calls bar on foo + /// assert!(foo.bar()); + /// + /// let bar = foo.bar || Foo::bar; + /// + /// /* multi-line + /// comment */ + /// + /// let multi_line_string = "Foo + /// bar\n + /// "; + /// + /// ``` + /// + /// ```rust,no_run + /// let foobar = Foo::new().bar(); + /// ``` + /// + /// ~~~rust,no_run + /// // code block with tilde. + /// let foobar = Foo::new().bar(); + /// ~~~ + /// + /// ``` + /// // functions + /// fn foo<T, const X: usize>(arg: i32) { + /// let x: T = X; + /// } + /// ``` + /// + /// ```sh + /// echo 1 + /// ``` + pub fn foo(&self) -> bool { + true + } +} + +/// [`Foo`](Foo) is a struct +/// This function is > [`all_the_links`](all_the_links) < +/// [`noop`](noop) is a macro below +/// [`Item`] is a struct in the module [`module`] +/// +/// [`Item`]: module::Item +/// [mix_and_match]: ThisShouldntResolve +pub fn all_the_links() {} + +pub mod module { + pub struct Item; +} + +/// ``` +/// macro_rules! noop { ($expr:expr) => { $expr }} +/// noop!(1); +/// ``` +macro_rules! noop { + ($expr:expr) => { + $expr + } +} + +/// ```rust +/// let _ = example(&[1, 2, 3]); +/// ``` +/// +/// ``` +/// loop {} +#[cfg_attr(not(feature = "false"), doc = "loop {}")] +#[doc = "loop {}"] +/// ``` +/// +#[cfg_attr(feature = "alloc", doc = "```rust")] +#[cfg_attr(not(feature = "alloc"), doc = "```ignore")] +/// let _ = example(&alloc::vec![1, 2, 3]); +/// ``` +pub fn mix_and_match() {} + +/** +It is beyond me why you'd use these when you got /// +```rust +let _ = example(&[1, 2, 3]); +``` +[`block_comments2`] tests these with indentation + */ +pub fn block_comments() {} + +/** + Really, I don't get it + ```rust + let _ = example(&[1, 2, 3]); + ``` + [`block_comments`] tests these without indentation +*/ +pub fn block_comments2() {} + +//- /outline_module.rs +//! This is an outline module whose purpose is to test that its inline attribute injection does not +//! spill into its parent. +//! ``` +//! fn test() {} +//! ``` +"#, + expect_file!["./test_data/highlight_doctest.html"], + false, + ); +} + +#[test] +fn test_extern_crate() { + check_highlighting( + r#" +//- /main.rs crate:main deps:std,alloc +extern crate std; +extern crate alloc as abc; +//- /std/lib.rs crate:std +pub struct S; +//- /alloc/lib.rs crate:alloc +pub struct A +"#, + expect_file!["./test_data/highlight_extern_crate.html"], + false, + ); +} + +#[test] +fn test_crate_root() { + check_highlighting( + r#" +//- minicore: iterators +//- /main.rs crate:main deps:foo +extern crate foo; +use core::iter; + +pub const NINETY_TWO: u8 = 92; + +use foo as foooo; + +pub(crate) fn main() { + let baz = iter::repeat(92); +} + +mod bar { + pub(in super) const FORTY_TWO: u8 = 42; + + mod baz { + use super::super::NINETY_TWO; + use crate::foooo::Point; + + pub(in super::super) const TWENTY_NINE: u8 = 29; + } +} +//- /foo.rs crate:foo +struct Point { + x: u8, + y: u8, +} + +mod inner { + pub(super) fn swap(p: crate::Point) -> crate::Point { + crate::Point { x: p.y, y: p.x } + } +} +"#, + expect_file!["./test_data/highlight_crate_root.html"], + false, + ); +} + +#[test] +fn test_default_library() { + check_highlighting( + r#" +//- minicore: option, iterators +use core::iter; + +fn main() { + let foo = Some(92); + let nums = iter::repeat(foo.unwrap()); +} +"#, + expect_file!["./test_data/highlight_default_library.html"], + false, + ); +} + +#[test] +fn test_associated_function() { + check_highlighting( + r#" +fn not_static() {} + +struct foo {} + +impl foo { + pub fn is_static() {} + pub fn is_not_static(&self) {} +} + +trait t { + fn t_is_static() {} + fn t_is_not_static(&self) {} +} + +impl t for foo { + pub fn is_static() {} + pub fn is_not_static(&self) {} +} +"#, + expect_file!["./test_data/highlight_assoc_functions.html"], + false, + ) +} + +#[test] +fn test_injection() { + check_highlighting( + r##" +fn fixture(ra_fixture: &str) {} + +fn main() { + fixture(r#" +trait Foo { + fn foo() { + println!("2 + 2 = {}", 4); + } +}"# + ); + fixture(r" +fn foo() { + foo(\$0{ + 92 + }\$0) +}" + ); +} +"##, + expect_file!["./test_data/highlight_injection.html"], + false, + ); +} + +#[test] +fn test_operators() { + check_highlighting( + r##" +fn main() { + 1 + 1 - 1 * 1 / 1 % 1 | 1 & 1 ! 1 ^ 1 >> 1 << 1; + let mut a = 0; + a += 1; + a -= 1; + a *= 1; + a /= 1; + a %= 1; + a |= 1; + a &= 1; + a ^= 1; + a >>= 1; + a <<= 1; +} +"##, + expect_file!["./test_data/highlight_operators.html"], + false, + ); +} + +#[test] +fn test_mod_hl_injection() { + check_highlighting( + r##" +//- /foo.rs +//! [Struct] +//! This is an intra doc injection test for modules +//! [Struct] +//! This is an intra doc injection test for modules + +pub struct Struct; +//- /lib.rs crate:foo +/// [crate::foo::Struct] +/// This is an intra doc injection test for modules +/// [crate::foo::Struct] +/// This is an intra doc injection test for modules +mod foo; +"##, + expect_file!["./test_data/highlight_module_docs_inline.html"], + false, + ); + check_highlighting( + r##" +//- /lib.rs crate:foo +/// [crate::foo::Struct] +/// This is an intra doc injection test for modules +/// [crate::foo::Struct] +/// This is an intra doc injection test for modules +mod foo; +//- /foo.rs +//! [Struct] +//! This is an intra doc injection test for modules +//! [Struct] +//! This is an intra doc injection test for modules + +pub struct Struct; +"##, + expect_file!["./test_data/highlight_module_docs_outline.html"], + false, + ); +} + +#[test] +#[cfg_attr( + all(unix, not(target_pointer_width = "64")), + ignore = "depends on `DefaultHasher` outputs" +)] +fn test_rainbow_highlighting() { + check_highlighting( + r#" +fn main() { + let hello = "hello"; + let x = hello.to_string(); + let y = hello.to_string(); + + let x = "other color please!"; + let y = x.to_string(); +} + +fn bar() { + let mut hello = "hello"; +} +"#, + expect_file!["./test_data/highlight_rainbow.html"], + true, + ); +} + +#[test] +fn test_ranges() { + let (analysis, file_id) = fixture::file( + r#" +#[derive(Clone, Debug)] +struct Foo { + pub x: i32, + pub y: i32, +} +"#, + ); + + // The "x" + let highlights = &analysis + .highlight_range(FileRange { file_id, range: TextRange::at(45.into(), 1.into()) }) + .unwrap(); + + assert_eq!(&highlights[0].highlight.to_string(), "field.declaration.public"); +} + +#[test] +fn ranges_sorted() { + let (analysis, file_id) = fixture::file( + r#" +#[foo(bar = "bar")] +macro_rules! test {} +}"# + .trim(), + ); + let _ = analysis.highlight(file_id).unwrap(); +} + +/// Highlights the code given by the `ra_fixture` argument, renders the +/// result as HTML, and compares it with the HTML file given as `snapshot`. +/// Note that the `snapshot` file is overwritten by the rendered HTML. +fn check_highlighting(ra_fixture: &str, expect: ExpectFile, rainbow: bool) { + let (analysis, file_id) = fixture::file(ra_fixture.trim()); + let actual_html = &analysis.highlight_as_html(file_id, rainbow).unwrap(); + expect.assert_eq(actual_html) +} + +#[test] +fn benchmark_syntax_highlighting_long_struct() { + if skip_slow_tests() { + return; + } + + let fixture = bench_fixture::big_struct(); + let (analysis, file_id) = fixture::file(&fixture); + + let hash = { + let _pt = bench("syntax highlighting long struct"); + analysis + .highlight(file_id) + .unwrap() + .iter() + .filter(|it| it.highlight.tag == HlTag::Symbol(SymbolKind::Struct)) + .count() + }; + assert_eq!(hash, 2001); +} + +#[test] +fn syntax_highlighting_not_quadratic() { + if skip_slow_tests() { + return; + } + + let mut al = AssertLinear::default(); + while al.next_round() { + for i in 6..=10 { + let n = 1 << i; + + let fixture = bench_fixture::big_struct_n(n); + let (analysis, file_id) = fixture::file(&fixture); + + let time = Instant::now(); + + let hash = analysis + .highlight(file_id) + .unwrap() + .iter() + .filter(|it| it.highlight.tag == HlTag::Symbol(SymbolKind::Struct)) + .count(); + assert!(hash > n as usize); + + let elapsed = time.elapsed(); + al.sample(n as f64, elapsed.as_millis() as f64); + } + } +} + +#[test] +fn benchmark_syntax_highlighting_parser() { + if skip_slow_tests() { + return; + } + + let fixture = bench_fixture::glorious_old_parser(); + let (analysis, file_id) = fixture::file(&fixture); + + let hash = { + let _pt = bench("syntax highlighting parser"); + analysis + .highlight(file_id) + .unwrap() + .iter() + .filter(|it| it.highlight.tag == HlTag::Symbol(SymbolKind::Function)) + .count() + }; + assert_eq!(hash, 1609); +} diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_tree.rs b/src/tools/rust-analyzer/crates/ide/src/syntax_tree.rs new file mode 100644 index 000000000..9003e7cd3 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/syntax_tree.rs @@ -0,0 +1,339 @@ +use ide_db::base_db::{FileId, SourceDatabase}; +use ide_db::RootDatabase; +use syntax::{ + AstNode, NodeOrToken, SourceFile, SyntaxKind::STRING, SyntaxToken, TextRange, TextSize, +}; + +// Feature: Show Syntax Tree +// +// Shows the parse tree of the current file. It exists mostly for debugging +// rust-analyzer itself. +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Show Syntax Tree** +// |=== +// image::https://user-images.githubusercontent.com/48062697/113065586-068bdb80-91b1-11eb-9507-fee67f9f45a0.gif[] +pub(crate) fn syntax_tree( + db: &RootDatabase, + file_id: FileId, + text_range: Option<TextRange>, +) -> String { + let parse = db.parse(file_id); + if let Some(text_range) = text_range { + let node = match parse.tree().syntax().covering_element(text_range) { + NodeOrToken::Node(node) => node, + NodeOrToken::Token(token) => { + if let Some(tree) = syntax_tree_for_string(&token, text_range) { + return tree; + } + token.parent().unwrap() + } + }; + + format!("{:#?}", node) + } else { + format!("{:#?}", parse.tree().syntax()) + } +} + +/// Attempts parsing the selected contents of a string literal +/// as rust syntax and returns its syntax tree +fn syntax_tree_for_string(token: &SyntaxToken, text_range: TextRange) -> Option<String> { + // When the range is inside a string + // we'll attempt parsing it as rust syntax + // to provide the syntax tree of the contents of the string + match token.kind() { + STRING => syntax_tree_for_token(token, text_range), + _ => None, + } +} + +fn syntax_tree_for_token(node: &SyntaxToken, text_range: TextRange) -> Option<String> { + // Range of the full node + let node_range = node.text_range(); + let text = node.text().to_string(); + + // We start at some point inside the node + // Either we have selected the whole string + // or our selection is inside it + let start = text_range.start() - node_range.start(); + + // how many characters we have selected + let len = text_range.len(); + + let node_len = node_range.len(); + + let start = start; + + // We want to cap our length + let len = len.min(node_len); + + // Ensure our slice is inside the actual string + let end = + if start + len < TextSize::of(&text) { start + len } else { TextSize::of(&text) - start }; + + let text = &text[TextRange::new(start, end)]; + + // Remove possible extra string quotes from the start + // and the end of the string + let text = text + .trim_start_matches('r') + .trim_start_matches('#') + .trim_start_matches('"') + .trim_end_matches('#') + .trim_end_matches('"') + .trim() + // Remove custom markers + .replace("$0", ""); + + let parsed = SourceFile::parse(&text); + + // If the "file" parsed without errors, + // return its syntax + if parsed.errors().is_empty() { + return Some(format!("{:#?}", parsed.tree().syntax())); + } + + None +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use crate::fixture; + + fn check(ra_fixture: &str, expect: expect_test::Expect) { + let (analysis, file_id) = fixture::file(ra_fixture); + let syn = analysis.syntax_tree(file_id, None).unwrap(); + expect.assert_eq(&syn) + } + fn check_range(ra_fixture: &str, expect: expect_test::Expect) { + let (analysis, frange) = fixture::range(ra_fixture); + let syn = analysis.syntax_tree(frange.file_id, Some(frange.range)).unwrap(); + expect.assert_eq(&syn) + } + + #[test] + fn test_syntax_tree_without_range() { + // Basic syntax + check( + r#"fn foo() {}"#, + expect![[r#" + SOURCE_FILE@0..11 + FN@0..11 + FN_KW@0..2 "fn" + WHITESPACE@2..3 " " + NAME@3..6 + IDENT@3..6 "foo" + PARAM_LIST@6..8 + L_PAREN@6..7 "(" + R_PAREN@7..8 ")" + WHITESPACE@8..9 " " + BLOCK_EXPR@9..11 + STMT_LIST@9..11 + L_CURLY@9..10 "{" + R_CURLY@10..11 "}" + "#]], + ); + + check( + r#" +fn test() { + assert!(" + fn foo() { + } + ", ""); +}"#, + expect![[r#" + SOURCE_FILE@0..60 + FN@0..60 + FN_KW@0..2 "fn" + WHITESPACE@2..3 " " + NAME@3..7 + IDENT@3..7 "test" + PARAM_LIST@7..9 + L_PAREN@7..8 "(" + R_PAREN@8..9 ")" + WHITESPACE@9..10 " " + BLOCK_EXPR@10..60 + STMT_LIST@10..60 + L_CURLY@10..11 "{" + WHITESPACE@11..16 "\n " + EXPR_STMT@16..58 + MACRO_EXPR@16..57 + MACRO_CALL@16..57 + PATH@16..22 + PATH_SEGMENT@16..22 + NAME_REF@16..22 + IDENT@16..22 "assert" + BANG@22..23 "!" + TOKEN_TREE@23..57 + L_PAREN@23..24 "(" + STRING@24..52 "\"\n fn foo() {\n ..." + COMMA@52..53 "," + WHITESPACE@53..54 " " + STRING@54..56 "\"\"" + R_PAREN@56..57 ")" + SEMICOLON@57..58 ";" + WHITESPACE@58..59 "\n" + R_CURLY@59..60 "}" + "#]], + ) + } + + #[test] + fn test_syntax_tree_with_range() { + check_range( + r#"$0fn foo() {}$0"#, + expect![[r#" + FN@0..11 + FN_KW@0..2 "fn" + WHITESPACE@2..3 " " + NAME@3..6 + IDENT@3..6 "foo" + PARAM_LIST@6..8 + L_PAREN@6..7 "(" + R_PAREN@7..8 ")" + WHITESPACE@8..9 " " + BLOCK_EXPR@9..11 + STMT_LIST@9..11 + L_CURLY@9..10 "{" + R_CURLY@10..11 "}" + "#]], + ); + + check_range( + r#" +fn test() { + $0assert!(" + fn foo() { + } + ", "");$0 +}"#, + expect![[r#" + EXPR_STMT@16..58 + MACRO_EXPR@16..57 + MACRO_CALL@16..57 + PATH@16..22 + PATH_SEGMENT@16..22 + NAME_REF@16..22 + IDENT@16..22 "assert" + BANG@22..23 "!" + TOKEN_TREE@23..57 + L_PAREN@23..24 "(" + STRING@24..52 "\"\n fn foo() {\n ..." + COMMA@52..53 "," + WHITESPACE@53..54 " " + STRING@54..56 "\"\"" + R_PAREN@56..57 ")" + SEMICOLON@57..58 ";" + "#]], + ); + } + + #[test] + fn test_syntax_tree_inside_string() { + check_range( + r#"fn test() { + assert!(" +$0fn foo() { +}$0 +fn bar() { +} + ", ""); +}"#, + expect![[r#" + SOURCE_FILE@0..12 + FN@0..12 + FN_KW@0..2 "fn" + WHITESPACE@2..3 " " + NAME@3..6 + IDENT@3..6 "foo" + PARAM_LIST@6..8 + L_PAREN@6..7 "(" + R_PAREN@7..8 ")" + WHITESPACE@8..9 " " + BLOCK_EXPR@9..12 + STMT_LIST@9..12 + L_CURLY@9..10 "{" + WHITESPACE@10..11 "\n" + R_CURLY@11..12 "}" + "#]], + ); + + // With a raw string + check_range( + r###"fn test() { + assert!(r#" +$0fn foo() { +}$0 +fn bar() { +} + "#, ""); +}"###, + expect![[r#" + SOURCE_FILE@0..12 + FN@0..12 + FN_KW@0..2 "fn" + WHITESPACE@2..3 " " + NAME@3..6 + IDENT@3..6 "foo" + PARAM_LIST@6..8 + L_PAREN@6..7 "(" + R_PAREN@7..8 ")" + WHITESPACE@8..9 " " + BLOCK_EXPR@9..12 + STMT_LIST@9..12 + L_CURLY@9..10 "{" + WHITESPACE@10..11 "\n" + R_CURLY@11..12 "}" + "#]], + ); + + // With a raw string + check_range( + r###"fn test() { + assert!(r$0#" +fn foo() { +} +fn bar() { +}"$0#, ""); +}"###, + expect![[r#" + SOURCE_FILE@0..25 + FN@0..12 + FN_KW@0..2 "fn" + WHITESPACE@2..3 " " + NAME@3..6 + IDENT@3..6 "foo" + PARAM_LIST@6..8 + L_PAREN@6..7 "(" + R_PAREN@7..8 ")" + WHITESPACE@8..9 " " + BLOCK_EXPR@9..12 + STMT_LIST@9..12 + L_CURLY@9..10 "{" + WHITESPACE@10..11 "\n" + R_CURLY@11..12 "}" + WHITESPACE@12..13 "\n" + FN@13..25 + FN_KW@13..15 "fn" + WHITESPACE@15..16 " " + NAME@16..19 + IDENT@16..19 "bar" + PARAM_LIST@19..21 + L_PAREN@19..20 "(" + R_PAREN@20..21 ")" + WHITESPACE@21..22 " " + BLOCK_EXPR@22..25 + STMT_LIST@22..25 + L_CURLY@22..23 "{" + WHITESPACE@23..24 "\n" + R_CURLY@24..25 "}" + "#]], + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/typing.rs b/src/tools/rust-analyzer/crates/ide/src/typing.rs new file mode 100644 index 000000000..9118f3c69 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/typing.rs @@ -0,0 +1,1210 @@ +//! This module handles auto-magic editing actions applied together with users +//! edits. For example, if the user typed +//! +//! ```text +//! foo +//! .bar() +//! .baz() +//! | // <- cursor is here +//! ``` +//! +//! and types `.` next, we want to indent the dot. +//! +//! Language server executes such typing assists synchronously. That is, they +//! block user's typing and should be pretty fast for this reason! + +mod on_enter; + +use ide_db::{ + base_db::{FilePosition, SourceDatabase}, + RootDatabase, +}; +use syntax::{ + algo::{ancestors_at_offset, find_node_at_offset}, + ast::{self, edit::IndentLevel, AstToken}, + AstNode, Parse, SourceFile, SyntaxKind, TextRange, TextSize, T, +}; + +use text_edit::{Indel, TextEdit}; + +use crate::SourceChange; + +pub(crate) use on_enter::on_enter; + +// Don't forget to add new trigger characters to `server_capabilities` in `caps.rs`. +pub(crate) const TRIGGER_CHARS: &str = ".=<>{"; + +struct ExtendedTextEdit { + edit: TextEdit, + is_snippet: bool, +} + +// Feature: On Typing Assists +// +// Some features trigger on typing certain characters: +// +// - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression +// - typing `=` between two expressions adds `;` when in statement position +// - typing `=` to turn an assignment into an equality comparison removes `;` when in expression position +// - typing `.` in a chain method call auto-indents +// - typing `{` in front of an expression inserts a closing `}` after the expression +// - typing `{` in a use item adds a closing `}` in the right place +// +// VS Code:: +// +// Add the following to `settings.json`: +// [source,json] +// ---- +// "editor.formatOnType": true, +// ---- +// +// image::https://user-images.githubusercontent.com/48062697/113166163-69758500-923a-11eb-81ee-eb33ec380399.gif[] +// image::https://user-images.githubusercontent.com/48062697/113171066-105c2000-923f-11eb-87ab-f4a263346567.gif[] +pub(crate) fn on_char_typed( + db: &RootDatabase, + position: FilePosition, + char_typed: char, +) -> Option<SourceChange> { + if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) { + return None; + } + let file = &db.parse(position.file_id); + if !stdx::always!(file.tree().syntax().text().char_at(position.offset) == Some(char_typed)) { + return None; + } + let edit = on_char_typed_inner(file, position.offset, char_typed)?; + let mut sc = SourceChange::from_text_edit(position.file_id, edit.edit); + sc.is_snippet = edit.is_snippet; + Some(sc) +} + +fn on_char_typed_inner( + file: &Parse<SourceFile>, + offset: TextSize, + char_typed: char, +) -> Option<ExtendedTextEdit> { + if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) { + return None; + } + return match char_typed { + '.' => conv(on_dot_typed(&file.tree(), offset)), + '=' => conv(on_eq_typed(&file.tree(), offset)), + '<' => on_left_angle_typed(&file.tree(), offset), + '>' => conv(on_right_angle_typed(&file.tree(), offset)), + '{' => conv(on_opening_brace_typed(file, offset)), + _ => return None, + }; + + fn conv(text_edit: Option<TextEdit>) -> Option<ExtendedTextEdit> { + Some(ExtendedTextEdit { edit: text_edit?, is_snippet: false }) + } +} + +/// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a +/// block, or a part of a `use` item. +fn on_opening_brace_typed(file: &Parse<SourceFile>, offset: TextSize) -> Option<TextEdit> { + if !stdx::always!(file.tree().syntax().text().char_at(offset) == Some('{')) { + return None; + } + + let brace_token = file.tree().syntax().token_at_offset(offset).right_biased()?; + if brace_token.kind() != SyntaxKind::L_CURLY { + return None; + } + + // Remove the `{` to get a better parse tree, and reparse. + let range = brace_token.text_range(); + if !stdx::always!(range.len() == TextSize::of('{')) { + return None; + } + let file = file.reparse(&Indel::delete(range)); + + if let Some(edit) = brace_expr(&file.tree(), offset) { + return Some(edit); + } + + if let Some(edit) = brace_use_path(&file.tree(), offset) { + return Some(edit); + } + + return None; + + fn brace_use_path(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { + let segment: ast::PathSegment = find_node_at_offset(file.syntax(), offset)?; + if segment.syntax().text_range().start() != offset { + return None; + } + + let tree: ast::UseTree = find_node_at_offset(file.syntax(), offset)?; + + Some(TextEdit::insert( + tree.syntax().text_range().end() + TextSize::of("{"), + "}".to_string(), + )) + } + + fn brace_expr(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { + let mut expr: ast::Expr = find_node_at_offset(file.syntax(), offset)?; + if expr.syntax().text_range().start() != offset { + return None; + } + + // Enclose the outermost expression starting at `offset` + while let Some(parent) = expr.syntax().parent() { + if parent.text_range().start() != expr.syntax().text_range().start() { + break; + } + + match ast::Expr::cast(parent) { + Some(parent) => expr = parent, + None => break, + } + } + + // If it's a statement in a block, we don't know how many statements should be included + if ast::ExprStmt::can_cast(expr.syntax().parent()?.kind()) { + return None; + } + + // Insert `}` right after the expression. + Some(TextEdit::insert( + expr.syntax().text_range().end() + TextSize::of("{"), + "}".to_string(), + )) + } +} + +/// Returns an edit which should be applied after `=` was typed. Primarily, +/// this works when adding `let =`. +// FIXME: use a snippet completion instead of this hack here. +fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { + if !stdx::always!(file.syntax().text().char_at(offset) == Some('=')) { + return None; + } + + if let Some(edit) = let_stmt(file, offset) { + return Some(edit); + } + if let Some(edit) = assign_expr(file, offset) { + return Some(edit); + } + if let Some(edit) = assign_to_eq(file, offset) { + return Some(edit); + } + + return None; + + fn assign_expr(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { + let binop: ast::BinExpr = find_node_at_offset(file.syntax(), offset)?; + if !matches!(binop.op_kind(), Some(ast::BinaryOp::Assignment { op: None })) { + return None; + } + + // Parent must be `ExprStmt` or `StmtList` for `;` to be valid. + if let Some(expr_stmt) = ast::ExprStmt::cast(binop.syntax().parent()?) { + if expr_stmt.semicolon_token().is_some() { + return None; + } + } else { + if !ast::StmtList::can_cast(binop.syntax().parent()?.kind()) { + return None; + } + } + + let expr = binop.rhs()?; + let expr_range = expr.syntax().text_range(); + if expr_range.contains(offset) && offset != expr_range.start() { + return None; + } + if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') { + return None; + } + let offset = expr.syntax().text_range().end(); + Some(TextEdit::insert(offset, ";".to_string())) + } + + /// `a =$0 b;` removes the semicolon if an expression is valid in this context. + fn assign_to_eq(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { + let binop: ast::BinExpr = find_node_at_offset(file.syntax(), offset)?; + if !matches!(binop.op_kind(), Some(ast::BinaryOp::CmpOp(ast::CmpOp::Eq { negated: false }))) + { + return None; + } + + let expr_stmt = ast::ExprStmt::cast(binop.syntax().parent()?)?; + let semi = expr_stmt.semicolon_token()?; + + if expr_stmt.syntax().next_sibling().is_some() { + // Not the last statement in the list. + return None; + } + + Some(TextEdit::delete(semi.text_range())) + } + + fn let_stmt(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { + let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; + if let_stmt.semicolon_token().is_some() { + return None; + } + let expr = let_stmt.initializer()?; + let expr_range = expr.syntax().text_range(); + if expr_range.contains(offset) && offset != expr_range.start() { + return None; + } + if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') { + return None; + } + let offset = let_stmt.syntax().text_range().end(); + Some(TextEdit::insert(offset, ";".to_string())) + } +} + +/// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. +fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { + if !stdx::always!(file.syntax().text().char_at(offset) == Some('.')) { + return None; + } + let whitespace = + file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; + + // if prior is fn call over multiple lines dont indent + // or if previous is method call over multiples lines keep that indent + let current_indent = { + let text = whitespace.text(); + let (_prefix, suffix) = text.rsplit_once('\n')?; + suffix + }; + let current_indent_len = TextSize::of(current_indent); + + let parent = whitespace.syntax().parent()?; + // Make sure dot is a part of call chain + let receiver = if let Some(field_expr) = ast::FieldExpr::cast(parent.clone()) { + field_expr.expr()? + } else if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent.clone()) { + method_call_expr.receiver()? + } else { + return None; + }; + + let receiver_is_multiline = receiver.syntax().text().find_char('\n').is_some(); + let target_indent = match (receiver, receiver_is_multiline) { + // if receiver is multiline field or method call, just take the previous `.` indentation + (ast::Expr::MethodCallExpr(expr), true) => { + expr.dot_token().as_ref().map(IndentLevel::from_token) + } + (ast::Expr::FieldExpr(expr), true) => { + expr.dot_token().as_ref().map(IndentLevel::from_token) + } + // if receiver is multiline expression, just keeps its indentation + (_, true) => Some(IndentLevel::from_node(&parent)), + _ => None, + }; + let target_indent = match target_indent { + Some(x) => x, + // in all other cases, take previous indentation and indent once + None => IndentLevel::from_node(&parent) + 1, + } + .to_string(); + + if current_indent_len == TextSize::of(&target_indent) { + return None; + } + + Some(TextEdit::replace(TextRange::new(offset - current_indent_len, offset), target_indent)) +} + +/// Add closing `>` for generic arguments/parameters. +fn on_left_angle_typed(file: &SourceFile, offset: TextSize) -> Option<ExtendedTextEdit> { + let file_text = file.syntax().text(); + if !stdx::always!(file_text.char_at(offset) == Some('<')) { + return None; + } + + // Find the next non-whitespace char in the line. + let mut next_offset = offset + TextSize::of('<'); + while file_text.char_at(next_offset) == Some(' ') { + next_offset += TextSize::of(' ') + } + if file_text.char_at(next_offset) == Some('>') { + return None; + } + + let range = TextRange::at(offset, TextSize::of('<')); + if let Some(t) = file.syntax().token_at_offset(offset).left_biased() { + if T![impl] == t.kind() { + return Some(ExtendedTextEdit { + edit: TextEdit::replace(range, "<$0>".to_string()), + is_snippet: true, + }); + } + } + + if ancestors_at_offset(file.syntax(), offset) + .find(|n| { + ast::GenericParamList::can_cast(n.kind()) || ast::GenericArgList::can_cast(n.kind()) + }) + .is_some() + { + return Some(ExtendedTextEdit { + edit: TextEdit::replace(range, "<$0>".to_string()), + is_snippet: true, + }); + } + + None +} + +/// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` +fn on_right_angle_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { + let file_text = file.syntax().text(); + if !stdx::always!(file_text.char_at(offset) == Some('>')) { + return None; + } + let after_arrow = offset + TextSize::of('>'); + if file_text.char_at(after_arrow) != Some('{') { + return None; + } + if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() { + return None; + } + + Some(TextEdit::insert(after_arrow, " ".to_string())) +} + +#[cfg(test)] +mod tests { + use test_utils::{assert_eq_text, extract_offset}; + + use super::*; + + impl ExtendedTextEdit { + fn apply(&self, text: &mut String) { + self.edit.apply(text); + } + } + + fn do_type_char(char_typed: char, before: &str) -> Option<String> { + let (offset, mut before) = extract_offset(before); + let edit = TextEdit::insert(offset, char_typed.to_string()); + edit.apply(&mut before); + let parse = SourceFile::parse(&before); + on_char_typed_inner(&parse, offset, char_typed).map(|it| { + it.apply(&mut before); + before.to_string() + }) + } + + fn type_char(char_typed: char, ra_fixture_before: &str, ra_fixture_after: &str) { + let actual = do_type_char(char_typed, ra_fixture_before) + .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed)); + + assert_eq_text!(ra_fixture_after, &actual); + } + + fn type_char_noop(char_typed: char, ra_fixture_before: &str) { + let file_change = do_type_char(char_typed, ra_fixture_before); + assert!(file_change.is_none()) + } + + #[test] + fn test_semi_after_let() { + // do_check(r" + // fn foo() { + // let foo =$0 + // } + // ", r" + // fn foo() { + // let foo =; + // } + // "); + type_char( + '=', + r#" +fn foo() { + let foo $0 1 + 1 +} +"#, + r#" +fn foo() { + let foo = 1 + 1; +} +"#, + ); + // do_check(r" + // fn foo() { + // let foo =$0 + // let bar = 1; + // } + // ", r" + // fn foo() { + // let foo =; + // let bar = 1; + // } + // "); + } + + #[test] + fn test_semi_after_assign() { + type_char( + '=', + r#" +fn f() { + i $0 0 +} +"#, + r#" +fn f() { + i = 0; +} +"#, + ); + type_char( + '=', + r#" +fn f() { + i $0 0 + i +} +"#, + r#" +fn f() { + i = 0; + i +} +"#, + ); + type_char_noop( + '=', + r#" +fn f(x: u8) { + if x $0 +} +"#, + ); + type_char_noop( + '=', + r#" +fn f(x: u8) { + if x $0 {} +} +"#, + ); + type_char_noop( + '=', + r#" +fn f(x: u8) { + if x $0 0 {} +} +"#, + ); + type_char_noop( + '=', + r#" +fn f() { + g(i $0 0); +} +"#, + ); + } + + #[test] + fn assign_to_eq() { + type_char( + '=', + r#" +fn f(a: u8) { + a =$0 0; +} +"#, + r#" +fn f(a: u8) { + a == 0 +} +"#, + ); + type_char( + '=', + r#" +fn f(a: u8) { + a $0= 0; +} +"#, + r#" +fn f(a: u8) { + a == 0 +} +"#, + ); + type_char_noop( + '=', + r#" +fn f(a: u8) { + let e = a =$0 0; +} +"#, + ); + type_char_noop( + '=', + r#" +fn f(a: u8) { + let e = a =$0 0; + e +} +"#, + ); + } + + #[test] + fn indents_new_chain_call() { + type_char( + '.', + r#" +fn main() { + xs.foo() + $0 +} + "#, + r#" +fn main() { + xs.foo() + . +} + "#, + ); + type_char_noop( + '.', + r#" +fn main() { + xs.foo() + $0 +} + "#, + ) + } + + #[test] + fn indents_new_chain_call_with_semi() { + type_char( + '.', + r" +fn main() { + xs.foo() + $0; +} + ", + r#" +fn main() { + xs.foo() + .; +} + "#, + ); + type_char_noop( + '.', + r#" +fn main() { + xs.foo() + $0; +} + "#, + ) + } + + #[test] + fn indents_new_chain_call_with_let() { + type_char( + '.', + r#" +fn main() { + let _ = foo + $0 + bar() +} +"#, + r#" +fn main() { + let _ = foo + . + bar() +} +"#, + ); + } + + #[test] + fn indents_continued_chain_call() { + type_char( + '.', + r#" +fn main() { + xs.foo() + .first() + $0 +} + "#, + r#" +fn main() { + xs.foo() + .first() + . +} + "#, + ); + type_char_noop( + '.', + r#" +fn main() { + xs.foo() + .first() + $0 +} + "#, + ); + } + + #[test] + fn indents_middle_of_chain_call() { + type_char( + '.', + r#" +fn source_impl() { + let var = enum_defvariant_list().unwrap() + $0 + .nth(92) + .unwrap(); +} + "#, + r#" +fn source_impl() { + let var = enum_defvariant_list().unwrap() + . + .nth(92) + .unwrap(); +} + "#, + ); + type_char_noop( + '.', + r#" +fn source_impl() { + let var = enum_defvariant_list().unwrap() + $0 + .nth(92) + .unwrap(); +} + "#, + ); + } + + #[test] + fn dont_indent_freestanding_dot() { + type_char_noop( + '.', + r#" +fn main() { + $0 +} + "#, + ); + type_char_noop( + '.', + r#" +fn main() { +$0 +} + "#, + ); + } + + #[test] + fn adds_space_after_return_type() { + type_char( + '>', + r#" +fn foo() -$0{ 92 } +"#, + r#" +fn foo() -> { 92 } +"#, + ); + } + + #[test] + fn adds_closing_brace_for_expr() { + type_char( + '{', + r#" +fn f() { match () { _ => $0() } } + "#, + r#" +fn f() { match () { _ => {()} } } + "#, + ); + type_char( + '{', + r#" +fn f() { $0() } + "#, + r#" +fn f() { {()} } + "#, + ); + type_char( + '{', + r#" +fn f() { let x = $0(); } + "#, + r#" +fn f() { let x = {()}; } + "#, + ); + type_char( + '{', + r#" +fn f() { let x = $0a.b(); } + "#, + r#" +fn f() { let x = {a.b()}; } + "#, + ); + type_char( + '{', + r#" +const S: () = $0(); +fn f() {} + "#, + r#" +const S: () = {()}; +fn f() {} + "#, + ); + type_char( + '{', + r#" +const S: () = $0a.b(); +fn f() {} + "#, + r#" +const S: () = {a.b()}; +fn f() {} + "#, + ); + type_char( + '{', + r#" +fn f() { + match x { + 0 => $0(), + 1 => (), + } +} + "#, + r#" +fn f() { + match x { + 0 => {()}, + 1 => (), + } +} + "#, + ); + } + + #[test] + fn noop_in_string_literal() { + // Regression test for #9351 + type_char_noop( + '{', + r##" +fn check_with(ra_fixture: &str, expect: Expect) { + let base = r#" +enum E { T(), R$0, C } +use self::E::X; +const Z: E = E::C; +mod m {} +asdasdasdasdasdasda +sdasdasdasdasdasda +sdasdasdasdasd +"#; + let actual = completion_list(&format!("{}\n{}", base, ra_fixture)); + expect.assert_eq(&actual) +} + "##, + ); + } + + #[test] + fn noop_in_item_position_with_macro() { + type_char_noop('{', r#"$0println!();"#); + type_char_noop( + '{', + r#" +fn main() $0println!("hello"); +}"#, + ); + } + + #[test] + fn adds_closing_brace_for_use_tree() { + type_char( + '{', + r#" +use some::$0Path; + "#, + r#" +use some::{Path}; + "#, + ); + type_char( + '{', + r#" +use some::{Path, $0Other}; + "#, + r#" +use some::{Path, {Other}}; + "#, + ); + type_char( + '{', + r#" +use some::{$0Path, Other}; + "#, + r#" +use some::{{Path}, Other}; + "#, + ); + type_char( + '{', + r#" +use some::path::$0to::Item; + "#, + r#" +use some::path::{to::Item}; + "#, + ); + type_char( + '{', + r#" +use some::$0path::to::Item; + "#, + r#" +use some::{path::to::Item}; + "#, + ); + type_char( + '{', + r#" +use $0some::path::to::Item; + "#, + r#" +use {some::path::to::Item}; + "#, + ); + type_char( + '{', + r#" +use some::path::$0to::{Item}; + "#, + r#" +use some::path::{to::{Item}}; + "#, + ); + type_char( + '{', + r#" +use $0Thing as _; + "#, + r#" +use {Thing as _}; + "#, + ); + + type_char_noop( + '{', + r#" +use some::pa$0th::to::Item; + "#, + ); + } + + #[test] + fn adds_closing_angle_bracket_for_generic_args() { + type_char( + '<', + r#" +fn foo() { + bar::$0 +} + "#, + r#" +fn foo() { + bar::<$0> +} + "#, + ); + + type_char( + '<', + r#" +fn foo(bar: &[u64]) { + bar.iter().collect::$0(); +} + "#, + r#" +fn foo(bar: &[u64]) { + bar.iter().collect::<$0>(); +} + "#, + ); + } + + #[test] + fn adds_closing_angle_bracket_for_generic_params() { + type_char( + '<', + r#" +fn foo$0() {} + "#, + r#" +fn foo<$0>() {} + "#, + ); + type_char( + '<', + r#" +fn foo$0 + "#, + r#" +fn foo<$0> + "#, + ); + type_char( + '<', + r#" +struct Foo$0 {} + "#, + r#" +struct Foo<$0> {} + "#, + ); + type_char( + '<', + r#" +struct Foo$0(); + "#, + r#" +struct Foo<$0>(); + "#, + ); + type_char( + '<', + r#" +struct Foo$0 + "#, + r#" +struct Foo<$0> + "#, + ); + type_char( + '<', + r#" +enum Foo$0 + "#, + r#" +enum Foo<$0> + "#, + ); + type_char( + '<', + r#" +trait Foo$0 + "#, + r#" +trait Foo<$0> + "#, + ); + type_char( + '<', + r#" +type Foo$0 = Bar; + "#, + r#" +type Foo<$0> = Bar; + "#, + ); + type_char( + '<', + r#" +impl$0 Foo {} + "#, + r#" +impl<$0> Foo {} + "#, + ); + type_char( + '<', + r#" +impl<T> Foo$0 {} + "#, + r#" +impl<T> Foo<$0> {} + "#, + ); + type_char( + '<', + r#" +impl Foo$0 {} + "#, + r#" +impl Foo<$0> {} + "#, + ); + } + + #[test] + fn dont_add_closing_angle_bracket_for_comparison() { + type_char_noop( + '<', + r#" +fn main() { + 42$0 +} + "#, + ); + type_char_noop( + '<', + r#" +fn main() { + 42 $0 +} + "#, + ); + type_char_noop( + '<', + r#" +fn main() { + let foo = 42; + foo $0 +} + "#, + ); + } + + #[test] + fn dont_add_closing_angle_bracket_if_it_is_already_there() { + type_char_noop( + '<', + r#" +fn foo() { + bar::$0> +} + "#, + ); + type_char_noop( + '<', + r#" +fn foo(bar: &[u64]) { + bar.iter().collect::$0 >(); +} + "#, + ); + type_char_noop( + '<', + r#" +fn foo$0>() {} + "#, + ); + type_char_noop( + '<', + r#" +fn foo$0> + "#, + ); + type_char_noop( + '<', + r#" +struct Foo$0> {} + "#, + ); + type_char_noop( + '<', + r#" +struct Foo$0>(); + "#, + ); + type_char_noop( + '<', + r#" +struct Foo$0> + "#, + ); + type_char_noop( + '<', + r#" +enum Foo$0> + "#, + ); + type_char_noop( + '<', + r#" +trait Foo$0> + "#, + ); + type_char_noop( + '<', + r#" +type Foo$0> = Bar; + "#, + ); + type_char_noop( + '<', + r#" +impl$0> Foo {} + "#, + ); + type_char_noop( + '<', + r#" +impl<T> Foo$0> {} + "#, + ); + type_char_noop( + '<', + r#" +impl Foo$0> {} + "#, + ); + } + + #[test] + fn regression_629() { + type_char_noop( + '.', + r#" +fn foo() { + CompletionItem::new( + CompletionKind::Reference, + ctx.source_range(), + field.name().to_string(), + ) + .foo() + $0 +} +"#, + ); + type_char_noop( + '.', + r#" +fn foo() { + CompletionItem::new( + CompletionKind::Reference, + ctx.source_range(), + field.name().to_string(), + ) + $0 +} +"#, + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/typing/on_enter.rs b/src/tools/rust-analyzer/crates/ide/src/typing/on_enter.rs new file mode 100644 index 000000000..48c171327 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/typing/on_enter.rs @@ -0,0 +1,616 @@ +//! Handles the `Enter` key press. At the momently, this only continues +//! comments, but should handle indent some time in the future as well. + +use ide_db::base_db::{FilePosition, SourceDatabase}; +use ide_db::RootDatabase; +use syntax::{ + algo::find_node_at_offset, + ast::{self, edit::IndentLevel, AstToken}, + AstNode, SmolStr, SourceFile, + SyntaxKind::*, + SyntaxNode, SyntaxToken, TextRange, TextSize, TokenAtOffset, +}; + +use text_edit::TextEdit; + +// Feature: On Enter +// +// rust-analyzer can override kbd:[Enter] key to make it smarter: +// +// - kbd:[Enter] inside triple-slash comments automatically inserts `///` +// - kbd:[Enter] in the middle or after a trailing space in `//` inserts `//` +// - kbd:[Enter] inside `//!` doc comments automatically inserts `//!` +// - kbd:[Enter] after `{` indents contents and closing `}` of single-line block +// +// This action needs to be assigned to shortcut explicitly. +// +// Note that, depending on the other installed extensions, this feature can visibly slow down typing. +// Similarly, if rust-analyzer crashes or stops responding, `Enter` might not work. +// In that case, you can still press `Shift-Enter` to insert a newline. +// +// VS Code:: +// +// Add the following to `keybindings.json`: +// [source,json] +// ---- +// { +// "key": "Enter", +// "command": "rust-analyzer.onEnter", +// "when": "editorTextFocus && !suggestWidgetVisible && editorLangId == rust" +// } +// ---- +// +// When using the Vim plugin: +// [source,json] +// ---- +// { +// "key": "Enter", +// "command": "rust-analyzer.onEnter", +// "when": "editorTextFocus && !suggestWidgetVisible && editorLangId == rust && vim.mode == 'Insert'" +// } +// ---- +// +// image::https://user-images.githubusercontent.com/48062697/113065578-04c21800-91b1-11eb-82b8-22b8c481e645.gif[] +pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> { + let parse = db.parse(position.file_id); + let file = parse.tree(); + let token = file.syntax().token_at_offset(position.offset).left_biased()?; + + if let Some(comment) = ast::Comment::cast(token.clone()) { + return on_enter_in_comment(&comment, &file, position.offset); + } + + if token.kind() == L_CURLY { + // Typing enter after the `{` of a block expression, where the `}` is on the same line + if let Some(edit) = find_node_at_offset(file.syntax(), position.offset - TextSize::of('{')) + .and_then(|block| on_enter_in_block(block, position)) + { + cov_mark::hit!(indent_block_contents); + return Some(edit); + } + + // Typing enter after the `{` of a use tree list. + if let Some(edit) = find_node_at_offset(file.syntax(), position.offset - TextSize::of('{')) + .and_then(|list| on_enter_in_use_tree_list(list, position)) + { + cov_mark::hit!(indent_block_contents); + return Some(edit); + } + } + + None +} + +fn on_enter_in_comment( + comment: &ast::Comment, + file: &ast::SourceFile, + offset: TextSize, +) -> Option<TextEdit> { + if comment.kind().shape.is_block() { + return None; + } + + let prefix = comment.prefix(); + let comment_range = comment.syntax().text_range(); + if offset < comment_range.start() + TextSize::of(prefix) { + return None; + } + + let mut remove_trailing_whitespace = false; + // Continuing single-line non-doc comments (like this one :) ) is annoying + if prefix == "//" && comment_range.end() == offset { + if comment.text().ends_with(' ') { + cov_mark::hit!(continues_end_of_line_comment_with_space); + remove_trailing_whitespace = true; + } else if !followed_by_comment(comment) { + return None; + } + } + + let indent = node_indent(file, comment.syntax())?; + let inserted = format!("\n{}{} $0", indent, prefix); + let delete = if remove_trailing_whitespace { + let trimmed_len = comment.text().trim_end().len() as u32; + let trailing_whitespace_len = comment.text().len() as u32 - trimmed_len; + TextRange::new(offset - TextSize::from(trailing_whitespace_len), offset) + } else { + TextRange::empty(offset) + }; + let edit = TextEdit::replace(delete, inserted); + Some(edit) +} + +fn on_enter_in_block(block: ast::BlockExpr, position: FilePosition) -> Option<TextEdit> { + let contents = block_contents(&block)?; + + if block.syntax().text().contains_char('\n') { + return None; + } + + let indent = IndentLevel::from_node(block.syntax()); + let mut edit = TextEdit::insert(position.offset, format!("\n{}$0", indent + 1)); + edit.union(TextEdit::insert(contents.text_range().end(), format!("\n{}", indent))).ok()?; + Some(edit) +} + +fn on_enter_in_use_tree_list(list: ast::UseTreeList, position: FilePosition) -> Option<TextEdit> { + if list.syntax().text().contains_char('\n') { + return None; + } + + let indent = IndentLevel::from_node(list.syntax()); + let mut edit = TextEdit::insert(position.offset, format!("\n{}$0", indent + 1)); + edit.union(TextEdit::insert( + list.r_curly_token()?.text_range().start(), + format!("\n{}", indent), + )) + .ok()?; + Some(edit) +} + +fn block_contents(block: &ast::BlockExpr) -> Option<SyntaxNode> { + let mut node = block.tail_expr().map(|e| e.syntax().clone()); + + for stmt in block.statements() { + if node.is_some() { + // More than 1 node in the block + return None; + } + + node = Some(stmt.syntax().clone()); + } + + node +} + +fn followed_by_comment(comment: &ast::Comment) -> bool { + let ws = match comment.syntax().next_token().and_then(ast::Whitespace::cast) { + Some(it) => it, + None => return false, + }; + if ws.spans_multiple_lines() { + return false; + } + ws.syntax().next_token().and_then(ast::Comment::cast).is_some() +} + +fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> { + let ws = match file.syntax().token_at_offset(token.text_range().start()) { + TokenAtOffset::Between(l, r) => { + assert!(r == *token); + l + } + TokenAtOffset::Single(n) => { + assert!(n == *token); + return Some("".into()); + } + TokenAtOffset::None => unreachable!(), + }; + if ws.kind() != WHITESPACE { + return None; + } + let text = ws.text(); + let pos = text.rfind('\n').map(|it| it + 1).unwrap_or(0); + Some(text[pos..].into()) +} + +#[cfg(test)] +mod tests { + use stdx::trim_indent; + use test_utils::assert_eq_text; + + use crate::fixture; + + fn apply_on_enter(before: &str) -> Option<String> { + let (analysis, position) = fixture::position(before); + let result = analysis.on_enter(position).unwrap()?; + + let mut actual = analysis.file_text(position.file_id).unwrap().to_string(); + result.apply(&mut actual); + Some(actual) + } + + fn do_check(ra_fixture_before: &str, ra_fixture_after: &str) { + let ra_fixture_after = &trim_indent(ra_fixture_after); + let actual = apply_on_enter(ra_fixture_before).unwrap(); + assert_eq_text!(ra_fixture_after, &actual); + } + + fn do_check_noop(ra_fixture_text: &str) { + assert!(apply_on_enter(ra_fixture_text).is_none()) + } + + #[test] + fn continues_doc_comment() { + do_check( + r" +/// Some docs$0 +fn foo() { +} +", + r" +/// Some docs +/// $0 +fn foo() { +} +", + ); + + do_check( + r" +impl S { + /// Some$0 docs. + fn foo() {} +} +", + r" +impl S { + /// Some + /// $0 docs. + fn foo() {} +} +", + ); + + do_check( + r" +///$0 Some docs +fn foo() { +} +", + r" +/// +/// $0 Some docs +fn foo() { +} +", + ); + } + + #[test] + fn does_not_continue_before_doc_comment() { + do_check_noop(r"$0//! docz"); + } + + #[test] + fn continues_another_doc_comment() { + do_check( + r#" +fn main() { + //! Documentation for$0 on enter + let x = 1 + 1; +} +"#, + r#" +fn main() { + //! Documentation for + //! $0 on enter + let x = 1 + 1; +} +"#, + ); + } + + #[test] + fn continues_code_comment_in_the_middle_of_line() { + do_check( + r" +fn main() { + // Fix$0 me + let x = 1 + 1; +} +", + r" +fn main() { + // Fix + // $0 me + let x = 1 + 1; +} +", + ); + } + + #[test] + fn continues_code_comment_in_the_middle_several_lines() { + do_check( + r" +fn main() { + // Fix$0 + // me + let x = 1 + 1; +} +", + r" +fn main() { + // Fix + // $0 + // me + let x = 1 + 1; +} +", + ); + } + + #[test] + fn does_not_continue_end_of_line_comment() { + do_check_noop( + r" +fn main() { + // Fix me$0 + let x = 1 + 1; +} +", + ); + } + + #[test] + fn continues_end_of_line_comment_with_space() { + cov_mark::check!(continues_end_of_line_comment_with_space); + do_check( + r#" +fn main() { + // Fix me $0 + let x = 1 + 1; +} +"#, + r#" +fn main() { + // Fix me + // $0 + let x = 1 + 1; +} +"#, + ); + } + + #[test] + fn trims_all_trailing_whitespace() { + do_check( + " +fn main() { + // Fix me \t\t $0 + let x = 1 + 1; +} +", + " +fn main() { + // Fix me + // $0 + let x = 1 + 1; +} +", + ); + } + + #[test] + fn indents_fn_body_block() { + cov_mark::check!(indent_block_contents); + do_check( + r#" +fn f() {$0()} + "#, + r#" +fn f() { + $0() +} + "#, + ); + } + + #[test] + fn indents_block_expr() { + do_check( + r#" +fn f() { + let x = {$0()}; +} + "#, + r#" +fn f() { + let x = { + $0() + }; +} + "#, + ); + } + + #[test] + fn indents_match_arm() { + do_check( + r#" +fn f() { + match 6 { + 1 => {$0f()}, + _ => (), + } +} + "#, + r#" +fn f() { + match 6 { + 1 => { + $0f() + }, + _ => (), + } +} + "#, + ); + } + + #[test] + fn indents_block_with_statement() { + do_check( + r#" +fn f() {$0a = b} + "#, + r#" +fn f() { + $0a = b +} + "#, + ); + do_check( + r#" +fn f() {$0fn f() {}} + "#, + r#" +fn f() { + $0fn f() {} +} + "#, + ); + } + + #[test] + fn indents_nested_blocks() { + do_check( + r#" +fn f() {$0{}} + "#, + r#" +fn f() { + $0{} +} + "#, + ); + } + + #[test] + fn does_not_indent_empty_block() { + do_check_noop( + r#" +fn f() {$0} + "#, + ); + do_check_noop( + r#" +fn f() {{$0}} + "#, + ); + } + + #[test] + fn does_not_indent_block_with_too_much_content() { + do_check_noop( + r#" +fn f() {$0 a = b; ()} + "#, + ); + do_check_noop( + r#" +fn f() {$0 a = b; a = b; } + "#, + ); + } + + #[test] + fn does_not_indent_multiline_block() { + do_check_noop( + r#" +fn f() {$0 +} + "#, + ); + do_check_noop( + r#" +fn f() {$0 + +} + "#, + ); + } + + #[test] + fn indents_use_tree_list() { + do_check( + r#" +use crate::{$0}; + "#, + r#" +use crate::{ + $0 +}; + "#, + ); + do_check( + r#" +use crate::{$0Object, path::to::OtherThing}; + "#, + r#" +use crate::{ + $0Object, path::to::OtherThing +}; + "#, + ); + do_check( + r#" +use {crate::{$0Object, path::to::OtherThing}}; + "#, + r#" +use {crate::{ + $0Object, path::to::OtherThing +}}; + "#, + ); + do_check( + r#" +use { + crate::{$0Object, path::to::OtherThing} +}; + "#, + r#" +use { + crate::{ + $0Object, path::to::OtherThing + } +}; + "#, + ); + } + + #[test] + fn does_not_indent_use_tree_list_when_not_at_curly_brace() { + do_check_noop( + r#" +use path::{Thing$0}; + "#, + ); + } + + #[test] + fn does_not_indent_use_tree_list_without_curly_braces() { + do_check_noop( + r#" +use path::Thing$0; + "#, + ); + do_check_noop( + r#" +use path::$0Thing; + "#, + ); + do_check_noop( + r#" +use path::Thing$0}; + "#, + ); + do_check_noop( + r#" +use path::{$0Thing; + "#, + ); + } + + #[test] + fn does_not_indent_multiline_use_tree_list() { + do_check_noop( + r#" +use path::{$0 + Thing +}; + "#, + ); + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/view_crate_graph.rs b/src/tools/rust-analyzer/crates/ide/src/view_crate_graph.rs new file mode 100644 index 000000000..51291a645 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/view_crate_graph.rs @@ -0,0 +1,93 @@ +use std::sync::Arc; + +use dot::{Id, LabelText}; +use ide_db::{ + base_db::{CrateGraph, CrateId, Dependency, SourceDatabase, SourceDatabaseExt}, + FxHashSet, RootDatabase, +}; + +// Feature: View Crate Graph +// +// Renders the currently loaded crate graph as an SVG graphic. Requires the `dot` tool, which +// is part of graphviz, to be installed. +// +// Only workspace crates are included, no crates.io dependencies or sysroot crates. +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: View Crate Graph** +// |=== +pub(crate) fn view_crate_graph(db: &RootDatabase, full: bool) -> Result<String, String> { + let crate_graph = db.crate_graph(); + let crates_to_render = crate_graph + .iter() + .filter(|krate| { + if full { + true + } else { + // Only render workspace crates + let root_id = db.file_source_root(crate_graph[*krate].root_file_id); + !db.source_root(root_id).is_library + } + }) + .collect(); + let graph = DotCrateGraph { graph: crate_graph, crates_to_render }; + + let mut dot = Vec::new(); + dot::render(&graph, &mut dot).unwrap(); + Ok(String::from_utf8(dot).unwrap()) +} + +struct DotCrateGraph { + graph: Arc<CrateGraph>, + crates_to_render: FxHashSet<CrateId>, +} + +type Edge<'a> = (CrateId, &'a Dependency); + +impl<'a> dot::GraphWalk<'a, CrateId, Edge<'a>> for DotCrateGraph { + fn nodes(&'a self) -> dot::Nodes<'a, CrateId> { + self.crates_to_render.iter().copied().collect() + } + + fn edges(&'a self) -> dot::Edges<'a, Edge<'a>> { + self.crates_to_render + .iter() + .flat_map(|krate| { + self.graph[*krate] + .dependencies + .iter() + .filter(|dep| self.crates_to_render.contains(&dep.crate_id)) + .map(move |dep| (*krate, dep)) + }) + .collect() + } + + fn source(&'a self, edge: &Edge<'a>) -> CrateId { + edge.0 + } + + fn target(&'a self, edge: &Edge<'a>) -> CrateId { + edge.1.crate_id + } +} + +impl<'a> dot::Labeller<'a, CrateId, Edge<'a>> for DotCrateGraph { + fn graph_id(&'a self) -> Id<'a> { + Id::new("rust_analyzer_crate_graph").unwrap() + } + + fn node_id(&'a self, n: &CrateId) -> Id<'a> { + Id::new(format!("_{}", n.0)).unwrap() + } + + fn node_shape(&'a self, _node: &CrateId) -> Option<LabelText<'a>> { + Some(LabelText::LabelStr("box".into())) + } + + fn node_label(&'a self, n: &CrateId) -> LabelText<'a> { + let name = self.graph[*n].display_name.as_ref().map_or("(unnamed crate)", |name| &*name); + LabelText::LabelStr(name.into()) + } +} diff --git a/src/tools/rust-analyzer/crates/ide/src/view_hir.rs b/src/tools/rust-analyzer/crates/ide/src/view_hir.rs new file mode 100644 index 000000000..7312afe53 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/view_hir.rs @@ -0,0 +1,26 @@ +use hir::{Function, Semantics}; +use ide_db::base_db::FilePosition; +use ide_db::RootDatabase; +use syntax::{algo::find_node_at_offset, ast, AstNode}; + +// Feature: View Hir +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: View Hir** +// |=== +// image::https://user-images.githubusercontent.com/48062697/113065588-068bdb80-91b1-11eb-9a78-0b4ef1e972fb.gif[] +pub(crate) fn view_hir(db: &RootDatabase, position: FilePosition) -> String { + body_hir(db, position).unwrap_or_else(|| "Not inside a function body".to_string()) +} + +fn body_hir(db: &RootDatabase, position: FilePosition) -> Option<String> { + let sema = Semantics::new(db); + let source_file = sema.parse(position.file_id); + + let function = find_node_at_offset::<ast::Fn>(source_file.syntax(), position.offset)?; + + let function: Function = sema.to_def(&function)?; + Some(function.debug_hir(db)) +} diff --git a/src/tools/rust-analyzer/crates/ide/src/view_item_tree.rs b/src/tools/rust-analyzer/crates/ide/src/view_item_tree.rs new file mode 100644 index 000000000..3dc03085d --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide/src/view_item_tree.rs @@ -0,0 +1,16 @@ +use hir::db::DefDatabase; +use ide_db::base_db::FileId; +use ide_db::RootDatabase; + +// Feature: Debug ItemTree +// +// Displays the ItemTree of the currently open file, for debugging. +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Debug ItemTree** +// |=== +pub(crate) fn view_item_tree(db: &RootDatabase, file_id: FileId) -> String { + db.file_item_tree(file_id.into()).pretty_print() +} |