summaryrefslogtreecommitdiffstats
path: root/src/tools/rust-analyzer/crates/ide/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
commit698f8c2f01ea549d77d7dc3338a12e04c11057b9 (patch)
tree173a775858bd501c378080a10dca74132f05bc50 /src/tools/rust-analyzer/crates/ide/src
parentInitial commit. (diff)
downloadrustc-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')
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/annotations.rs789
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/call_hierarchy.rs460
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/doc_links.rs549
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/doc_links/intra_doc_links.rs77
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/doc_links/tests.rs491
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/expand_macro.rs521
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/extend_selection.rs662
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/file_structure.rs579
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/fixture.rs87
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/fn_references.rs94
-rwxr-xr-xsrc/tools/rust-analyzer/crates/ide/src/folding_ranges.rs626
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/goto_declaration.rs112
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/goto_definition.rs1634
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/goto_implementation.rs344
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/goto_type_definition.rs296
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/highlight_related.rs1377
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/hover.rs390
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/hover/render.rs563
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/hover/tests.rs5053
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/inlay_hints.rs2818
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/join_lines.rs1087
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/lib.rs702
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/markdown_remove.rs22
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/markup.rs38
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/matching_brace.rs78
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/moniker.rs342
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/move_item.rs890
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/navigation_target.rs623
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/parent_module.rs167
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/prime_caches.rs158
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/prime_caches/topologic_sort.rs98
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/references.rs1636
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/rename.rs2252
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/runnables.rs2163
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/shuffle_crate_graph.rs71
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/signature_help.rs1334
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/ssr.rs255
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/static_index.rs321
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/status.rs164
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting.rs449
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/escape.rs25
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/format.rs50
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/highlight.rs690
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/highlights.rs92
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/html.rs97
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/inject.rs279
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/injector.rs81
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/macro_.rs128
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/tags.rs340
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_assoc_functions.html62
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_attributes.html58
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_crate_root.html66
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_default_library.html50
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html190
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_extern_crate.html47
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_general.html233
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_injection.html62
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_keywords.html58
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_lifetimes.html55
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_macros.html96
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_module_docs_inline.html51
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_module_docs_outline.html50
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_operators.html58
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_rainbow.html56
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_strings.html164
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/test_data/highlight_unsafe.html126
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_highlighting/tests.rs1096
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_tree.rs339
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/typing.rs1210
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/typing/on_enter.rs616
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/view_crate_graph.rs93
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/view_hir.rs26
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/view_item_tree.rs16
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(&macro_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('<', "&lt;").replace('>', "&gt;")
+}
+
+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">-&gt;</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">&lt;</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">&gt;</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">-&gt;</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 &gt; </span><span class="function documentation injected intra_doc_link">[`all_the_links`](all_the_links)</span><span class="comment documentation"> &lt;</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">&gt;</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">&gt;</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">&lt;</span><span class="type_param declaration">Args</span><span class="angle">&gt;</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">&lt;</span><span class="type_param declaration">Args</span><span class="angle">&gt;</span><span class="colon">:</span> <span class="trait public">FnOnce</span><span class="angle">&lt;</span><span class="type_param">Args</span><span class="angle">&gt;</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">&lt;</span><span class="type_param declaration">Args</span><span class="angle">&gt;</span><span class="colon">:</span> <span class="trait public">FnMut</span><span class="angle">&lt;</span><span class="type_param">Args</span><span class="angle">&gt;</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">-&gt;</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">-&gt;</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">-&gt;</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">-&gt;</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">-&gt;</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">-&gt;</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">&lt;</span><span class="lifetime declaration">'a</span><span class="comma">,</span> <span class="type_param declaration">T</span><span class="angle">&gt;</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="operator">-&gt;</span> <span class="type_param">T</span> <span class="brace">{</span>
+ <span class="function">foo</span><span class="operator">::</span><span class="angle">&lt;</span><span class="lifetime">'a</span><span class="comma">,</span> <span class="builtin_type">i32</span><span class="angle">&gt;</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">-&gt;</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">&lt;</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">&gt;</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="operator">-&gt;</span> <span class="builtin_type">usize</span> <span class="brace">{</span>
+ <span class="function">const_param</span><span class="operator">::</span><span class="angle">&lt;</span><span class="brace">{</span> <span class="const_param">FOO</span> <span class="brace">}</span><span class="angle">&gt;</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">&lt;</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">-&gt;</span> <span class="parenthesis">(</span><span class="parenthesis">)</span><span class="angle">&gt;</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">-&gt;</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">&lt;</span><span class="type_param declaration">T</span><span class="angle">&gt;</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">&lt;</span><span class="type_param declaration">T</span><span class="angle">&gt;</span> <span class="enum">Option</span><span class="angle">&lt;</span><span class="type_param">T</span><span class="angle">&gt;</span> <span class="brace">{</span>
+ <span class="keyword">fn</span> <span class="function associated consuming declaration">and</span><span class="angle">&lt;</span><span class="type_param declaration">U</span><span class="angle">&gt;</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">&lt;</span><span class="type_param">U</span><span class="angle">&gt;</span><span class="parenthesis">)</span> <span class="operator">-&gt;</span> <span class="enum">Option</span><span class="angle">&lt;</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">&gt;</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">=&gt;</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">=&gt;</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">-&gt;</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">&lt;</span><span class="type_param declaration">T</span><span class="angle">&gt;</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">&lt;</span><span class="type_param">T</span> <span class="keyword">as</span> <span class="trait">Baz</span><span class="angle">&gt;</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">&lt;</span><span class="type_param declaration">Baz</span><span class="colon">:</span> <span class="trait">Bar</span><span class="angle">&gt;</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">&gt;</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">&lt;</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">&gt;</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">&lt;</span><span class="lifetime declaration">'a</span><span class="angle">&gt;</span> <span class="struct">Foo</span><span class="angle">&lt;</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">&gt;</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">&gt;</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">&gt;</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">&gt;</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">&gt;</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">&gt;</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">&gt;</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">&gt;</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">&gt;&gt;</span> <span class="numeric_literal">1</span> <span class="bitwise">&lt;&lt;</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">&gt;&gt;=</span> <span class="numeric_literal">1</span><span class="semicolon">;</span>
+ <span class="variable mutable">a</span> <span class="bitwise mutable">&lt;&lt;=</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">&gt;</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">&gt;</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">&gt;</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::&lt;&str&gt;` 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">&gt;</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">&gt;</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">&gt;</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">&gt;</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">&gt;</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">// =&gt; "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">// =&gt; "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">// =&gt; "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">// =&gt; "(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">// =&gt; "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">// =&gt; "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">// =&gt; "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">// =&gt; "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">// =&gt; "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">// =&gt; "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">// =&gt; "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">// =&gt; "{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">&lt;</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">&lt;</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">&gt;</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">&gt;</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">&gt;</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">&gt;</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">&lt;</span><span class="type_param declaration">T</span><span class="colon">:</span> <span class="trait">UnsafeTrait</span><span class="angle">&gt;</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">=&gt;</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">=&gt;</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()
+}