rust-analyzer/crates/ide/src/annotations.rs

411 lines
12 KiB
Rust
Raw Normal View History

2021-02-13 11:07:47 +00:00
use hir::Semantics;
use ide_db::{
base_db::{FileId, FilePosition, FileRange, SourceDatabase},
RootDatabase, SymbolKind,
};
use syntax::TextRange;
use crate::{
file_structure::file_structure,
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.
2021-02-13 13:27:04 +00:00
#[derive(Debug)]
2021-02-13 11:07:47 +00:00
pub struct Annotation {
pub range: TextRange,
pub kind: AnnotationKind,
}
2021-02-13 13:27:04 +00:00
#[derive(Debug)]
2021-02-13 11:07:47 +00:00
pub enum AnnotationKind {
Runnable { debug: bool, runnable: Runnable },
HasImpls { position: FilePosition, data: Option<Vec<NavigationTarget>> },
HasReferences { position: FilePosition, 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 run: bool,
pub debug: bool,
}
pub(crate) fn annotations(
db: &RootDatabase,
file_id: FileId,
config: AnnotationConfig,
) -> Vec<Annotation> {
let mut annotations = Vec::default();
if config.annotate_runnables {
for runnable in runnables(db, file_id) {
if !matches!(runnable.kind, RunnableKind::Bin) || !config.binary_target {
continue;
}
let action = runnable.action();
let range = runnable.nav.full_range;
if config.run {
annotations.push(Annotation {
range,
// FIXME: This one allocates without reason if run is enabled, but debug is disabled
kind: AnnotationKind::Runnable { debug: false, runnable: runnable.clone() },
});
}
if action.debugee && config.debug {
annotations.push(Annotation {
range,
kind: AnnotationKind::Runnable { debug: true, runnable },
});
}
}
}
file_structure(&db.parse(file_id).tree())
.into_iter()
.filter(|node| {
matches!(
node.kind,
SymbolKind::Trait
| SymbolKind::Struct
| SymbolKind::Enum
| SymbolKind::Union
| SymbolKind::Const
)
})
.for_each(|node| {
if config.annotate_impls && node.kind != SymbolKind::Const {
annotations.push(Annotation {
range: node.node_range,
kind: AnnotationKind::HasImpls {
position: FilePosition { file_id, offset: node.navigation_range.start() },
data: None,
},
});
}
if config.annotate_references {
annotations.push(Annotation {
range: node.node_range,
kind: AnnotationKind::HasReferences {
position: FilePosition { file_id, offset: node.navigation_range.start() },
data: None,
},
});
}
});
if config.annotate_method_references {
annotations.extend(find_all_methods(db, file_id).into_iter().map(|method| Annotation {
range: method.range,
kind: AnnotationKind::HasReferences {
position: FilePosition { file_id, offset: method.range.start() },
data: None,
},
}));
}
annotations
}
pub(crate) fn resolve_annotation(db: &RootDatabase, mut annotation: Annotation) -> Annotation {
match annotation.kind {
AnnotationKind::HasImpls { position, ref mut data } => {
*data = goto_implementation(db, position).map(|range| range.info);
}
AnnotationKind::HasReferences { position, ref mut data } => {
*data = find_all_refs(&Semantics::new(db), position, None).map(|result| {
result
.references
.into_iter()
2021-02-13 11:22:12 +00:00
.map(|(file_id, access)| {
access.into_iter().map(move |(range, _)| FileRange { file_id, range })
})
2021-02-13 11:07:47 +00:00
.flatten()
.collect()
});
}
_ => {}
};
annotation
}
2021-02-13 13:27:04 +00:00
#[cfg(test)]
mod tests {
use ide_db::base_db::{FileId, FileRange};
use syntax::{TextRange, TextSize};
use crate::{fixture, Annotation, AnnotationConfig, AnnotationKind, RunnableKind};
fn get_annotations(
ra_fixture: &str,
annotation_config: AnnotationConfig,
) -> (FileId, Vec<Annotation>) {
let (analysis, file_id) = fixture::file(ra_fixture);
let annotations: Vec<Annotation> = analysis
.annotations(file_id, annotation_config)
.unwrap()
.into_iter()
.map(move |annotation| analysis.resolve_annotation(annotation).unwrap())
.collect();
if annotations.len() == 0 {
panic!("unresolved annotations")
}
(file_id, annotations)
}
macro_rules! check_annotation {
( $ra_fixture:expr, $config:expr, $item_positions:expr, $pattern:pat, $checker:expr ) => {
let (file_id, annotations) = get_annotations($ra_fixture, $config);
annotations.into_iter().for_each(|annotation| {
assert!($item_positions.contains(&annotation.range));
match annotation.kind {
$pattern => $checker(file_id),
_ => panic!("Unexpected annotation kind"),
}
});
};
}
#[test]
fn const_annotations() {
check_annotation!(
r#"
const DEMO: i32 = 123;
fn main() {
let hello = DEMO;
}
"#,
AnnotationConfig {
binary_target: false,
annotate_runnables: false,
annotate_impls: false,
annotate_references: true,
annotate_method_references: false,
run: false,
debug: false,
},
&[TextRange::new(TextSize::from(0), TextSize::from(22))],
AnnotationKind::HasReferences { data: Some(ranges), .. },
|file_id| assert_eq!(
*ranges.first().unwrap(),
FileRange {
file_id,
range: TextRange::new(TextSize::from(52), TextSize::from(56))
}
)
);
}
#[test]
fn unused_const_annotations() {
check_annotation!(
r#"
const DEMO: i32 = 123;
fn main() {}
"#,
AnnotationConfig {
binary_target: false,
annotate_runnables: false,
annotate_impls: false,
annotate_references: true,
annotate_method_references: false,
run: false,
debug: false,
},
&[TextRange::new(TextSize::from(0), TextSize::from(22))],
AnnotationKind::HasReferences { data: Some(ranges), .. },
|_| assert_eq!(ranges.len(), 0)
);
}
#[test]
fn struct_references_annotations() {
check_annotation!(
r#"
struct Test;
fn main() {
let test = Test;
}
"#,
AnnotationConfig {
binary_target: false,
annotate_runnables: false,
annotate_impls: false,
annotate_references: true,
annotate_method_references: false,
run: false,
debug: false,
},
&[TextRange::new(TextSize::from(0), TextSize::from(12))],
AnnotationKind::HasReferences { data: Some(ranges), .. },
|file_id| assert_eq!(
*ranges.first().unwrap(),
FileRange {
file_id,
range: TextRange::new(TextSize::from(41), TextSize::from(45))
}
)
);
}
#[test]
fn struct_and_trait_impls_annotations() {
check_annotation!(
r#"
struct Test;
trait MyCoolTrait {}
impl MyCoolTrait for Test {}
fn main() {
let test = Test;
}
"#,
AnnotationConfig {
binary_target: false,
annotate_runnables: false,
annotate_impls: true,
annotate_references: false,
annotate_method_references: false,
run: false,
debug: false,
},
&[
TextRange::new(TextSize::from(0), TextSize::from(12)),
TextRange::new(TextSize::from(14), TextSize::from(34))
],
AnnotationKind::HasImpls { data: Some(ranges), .. },
|_| assert_eq!(
ranges.first().unwrap().full_range,
TextRange::new(TextSize::from(36), TextSize::from(64))
)
);
}
#[test]
fn run_annotation() {
check_annotation!(
r#"
fn main() {}
"#,
AnnotationConfig {
binary_target: true,
annotate_runnables: true,
annotate_impls: false,
annotate_references: false,
annotate_method_references: false,
run: true,
debug: false,
},
&[TextRange::new(TextSize::from(0), TextSize::from(12))],
AnnotationKind::Runnable { debug: false, runnable },
|_| {
assert!(matches!(runnable.kind, RunnableKind::Bin));
assert!(runnable.action().run_title.contains("Run"));
}
);
}
#[test]
fn debug_annotation() {
check_annotation!(
r#"
fn main() {}
"#,
AnnotationConfig {
binary_target: true,
annotate_runnables: true,
annotate_impls: false,
annotate_references: false,
annotate_method_references: false,
run: false,
debug: true,
},
&[TextRange::new(TextSize::from(0), TextSize::from(12))],
AnnotationKind::Runnable { debug: true, runnable },
|_| {
assert!(matches!(runnable.kind, RunnableKind::Bin));
assert!(runnable.action().debugee);
}
);
}
#[test]
fn method_annotations() {
// We actually want to skip `fn main` annotation, as it has no references in it
// but just ignoring empty reference slices would lead to false-positive if something
// goes wrong in annotation resolving mechanism. By tracking if we iterated before finding
// an empty slice we can track if everything is settled.
let mut iterated_once = false;
check_annotation!(
r#"
struct Test;
impl Test {
fn self_by_ref(&self) {}
}
fn main() {
Test.self_by_ref();
}
"#,
AnnotationConfig {
binary_target: false,
annotate_runnables: false,
annotate_impls: false,
annotate_references: false,
annotate_method_references: true,
run: false,
debug: false,
},
&[
TextRange::new(TextSize::from(33), TextSize::from(44)),
TextRange::new(TextSize::from(61), TextSize::from(65))
],
AnnotationKind::HasReferences { data: Some(ranges), .. },
|file_id| {
match ranges.as_slice() {
[first, ..] => {
assert_eq!(
*first,
FileRange {
file_id,
range: TextRange::new(TextSize::from(79), TextSize::from(90))
}
);
iterated_once = true;
}
[] if iterated_once => {}
[] => panic!("One reference was expected but not found"),
}
}
);
}
}