Show type actions on ranged type hover

This commit is contained in:
Lukas Wirth 2021-08-11 13:39:36 +02:00
parent f2246fecef
commit ec443886ea
3 changed files with 143 additions and 58 deletions

View file

@ -1,6 +1,6 @@
//! Utilities for creating `Analysis` instances for tests. //! Utilities for creating `Analysis` instances for tests.
use ide_db::base_db::fixture::ChangeFixture; use ide_db::base_db::fixture::ChangeFixture;
use test_utils::extract_annotations; use test_utils::{extract_annotations, RangeOrOffset};
use crate::{Analysis, AnalysisHost, FileId, FilePosition, FileRange}; use crate::{Analysis, AnalysisHost, FileId, FilePosition, FileRange};
@ -32,6 +32,15 @@ pub(crate) fn range(ra_fixture: &str) -> (Analysis, FileRange) {
(host.analysis(), FileRange { file_id, 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.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. /// Creates analysis from a multi-file fixture, returns positions marked with $0.
pub(crate) fn annotations(ra_fixture: &str) -> (Analysis, FilePosition, Vec<(FileRange, String)>) { pub(crate) fn annotations(ra_fixture: &str) -> (Analysis, FilePosition, Vec<(FileRange, String)>) {
let mut host = AnalysisHost::default(); let mut host = AnalysisHost::default();

View file

@ -13,7 +13,7 @@ use itertools::Itertools;
use stdx::format_to; use stdx::format_to;
use syntax::{ use syntax::{
algo, ast, display::fn_as_proc_macro_label, match_ast, AstNode, AstToken, Direction, algo, ast, display::fn_as_proc_macro_label, match_ast, AstNode, AstToken, Direction,
SyntaxKind::*, SyntaxToken, T, SyntaxKind::*, SyntaxNode, SyntaxToken, T,
}; };
use crate::{ use crate::{
@ -54,6 +54,25 @@ pub enum HoverAction {
GoToType(Vec<HoverGotoTypeData>), 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)] #[derive(Debug, Clone, Eq, PartialEq)]
pub struct HoverGotoTypeData { pub struct HoverGotoTypeData {
pub mod_path: String, pub mod_path: String,
@ -81,28 +100,10 @@ pub(crate) fn hover(
let sema = hir::Semantics::new(db); let sema = hir::Semantics::new(db);
let file = sema.parse(file_id).syntax().clone(); let file = sema.parse(file_id).syntax().clone();
let offset = if range.is_empty() { if !range.is_empty() {
range.start() return hover_ranged(&file, range, &sema, config);
} else { }
let expr = file.covering_element(range).ancestors().find_map(|it| { let offset = range.start();
match_ast! {
match it {
ast::Expr(expr) => Some(Either::Left(expr)),
ast::Pat(pat) => Some(Either::Right(pat)),
_ => None,
}
}
})?;
return hover_type_info(&sema, config, &expr).map(|it| {
RangeInfo::new(
match expr {
Either::Left(it) => it.syntax().text_range(),
Either::Right(it) => it.syntax().text_range(),
},
it,
)
});
};
let token = pick_best_token(file.token_at_offset(offset), |kind| match kind { let token = pick_best_token(file.token_at_offset(offset), |kind| match kind {
IDENT | INT_NUMBER | LIFETIME_IDENT | T![self] | T![super] | T![crate] => 3, IDENT | INT_NUMBER | LIFETIME_IDENT | T![self] | T![super] | T![crate] => 3,
@ -112,8 +113,8 @@ pub(crate) fn hover(
})?; })?;
let token = sema.descend_into_macros(token); let token = sema.descend_into_macros(token);
let mut range_override = None;
let node = token.parent()?; let node = token.parent()?;
let mut range = None;
let definition = match_ast! { let definition = match_ast! {
match node { match node {
// We don't use NameClass::referenced_or_defined here as we do not want to resolve // We don't use NameClass::referenced_or_defined here as we do not want to resolve
@ -129,11 +130,13 @@ pub(crate) fn hover(
} }
}), }),
ast::Lifetime(lifetime) => NameClass::classify_lifetime(&sema, &lifetime).map_or_else( ast::Lifetime(lifetime) => NameClass::classify_lifetime(&sema, &lifetime).map_or_else(
|| NameRefClass::classify_lifetime(&sema, &lifetime).and_then(|class| match class { || {
NameRefClass::Definition(it) => Some(it), NameRefClass::classify_lifetime(&sema, &lifetime).and_then(|class| match class {
_ => None, NameRefClass::Definition(it) => Some(it),
}), _ => None,
|d| d.defined(), })
},
NameClass::defined,
), ),
_ => { _ => {
if ast::Comment::cast(token.clone()).is_some() { if ast::Comment::cast(token.clone()).is_some() {
@ -145,7 +148,7 @@ pub(crate) fn hover(
let mapped = doc_mapping.map(range)?; let mapped = doc_mapping.map(range)?;
(mapped.file_id == file_id.into() && mapped.value.contains(offset)).then(||(mapped.value, link, ns)) (mapped.file_id == file_id.into() && mapped.value.contains(offset)).then(||(mapped.value, link, ns))
})?; })?;
range = Some(idl_range); range_override = Some(idl_range);
Some(match resolve_doc_path_for_def(db,def, &link,ns)? { Some(match resolve_doc_path_for_def(db,def, &link,ns)? {
Either::Left(it) => Definition::ModuleDef(it), Either::Left(it) => Definition::ModuleDef(it),
Either::Right(it) => Definition::Macro(it), Either::Right(it) => Definition::Macro(it),
@ -154,7 +157,7 @@ pub(crate) fn hover(
if let res@Some(_) = try_hover_for_lint(&attr, &token) { if let res@Some(_) = try_hover_for_lint(&attr, &token) {
return res; return res;
} else { } else {
range = Some(token.text_range()); range_override = Some(token.text_range());
try_resolve_derive_input_at(&sema, &attr, &token).map(Definition::Macro) try_resolve_derive_input_at(&sema, &attr, &token).map(Definition::Macro)
} }
} else { } else {
@ -186,11 +189,11 @@ pub(crate) fn hover(
res.actions.push(action); res.actions.push(action);
} }
if let Some(action) = goto_type_action(db, definition) { if let Some(action) = goto_type_action_for_def(db, definition) {
res.actions.push(action); res.actions.push(action);
} }
let range = range.unwrap_or_else(|| sema.original_range(&node).range); let range = range_override.unwrap_or_else(|| sema.original_range(&node).range);
return Some(RangeInfo::new(range, res)); return Some(RangeInfo::new(range, res));
} }
} }
@ -199,6 +202,8 @@ pub(crate) fn hover(
return res; return res;
} }
// No definition below cursor, fall back to showing type hovers.
let node = token let node = token
.ancestors() .ancestors()
.take_while(|it| !ast::Item::can_cast(it.kind())) .take_while(|it| !ast::Item::can_cast(it.kind()))
@ -220,6 +225,30 @@ pub(crate) fn hover(
Some(RangeInfo::new(range, res)) Some(RangeInfo::new(range, res))
} }
fn hover_ranged(
file: &SyntaxNode,
range: syntax::TextRange,
sema: &Semantics<RootDatabase>,
config: &HoverConfig,
) -> Option<RangeInfo<HoverResult>> {
let expr = 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,
}
}
})?;
hover_type_info(sema, config, &expr).map(|it| {
let range = match expr {
Either::Left(it) => it.syntax().text_range(),
Either::Right(it) => it.syntax().text_range(),
};
RangeInfo::new(range, it)
})
}
fn hover_type_info( fn hover_type_info(
sema: &Semantics<RootDatabase>, sema: &Semantics<RootDatabase>,
config: &HoverConfig, config: &HoverConfig,
@ -231,7 +260,16 @@ fn hover_type_info(
}; };
let mut res = HoverResult::default(); 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 { 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 original = original.display(sema.db).to_string();
let adjusted = adjusted_ty.display(sema.db).to_string(); let adjusted = adjusted_ty.display(sema.db).to_string();
format!( format!(
@ -250,6 +288,7 @@ fn hover_type_info(
original.display(sema.db).to_string().into() original.display(sema.db).to_string().into()
} }
}; };
res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets));
Some(res) Some(res)
} }
@ -354,7 +393,7 @@ fn runnable_action(
} }
} }
fn goto_type_action(db: &RootDatabase, def: Definition) -> Option<HoverAction> { fn goto_type_action_for_def(db: &RootDatabase, def: Definition) -> Option<HoverAction> {
let mut targets: Vec<hir::ModuleDef> = Vec::new(); let mut targets: Vec<hir::ModuleDef> = Vec::new();
let mut push_new_def = |item: hir::ModuleDef| { let mut push_new_def = |item: hir::ModuleDef| {
if !targets.contains(&item) { if !targets.contains(&item) {
@ -372,30 +411,28 @@ fn goto_type_action(db: &RootDatabase, def: Definition) -> Option<HoverAction> {
_ => return None, _ => return None,
}; };
ty.walk(db, |t| { walk_and_push_ty(db, &ty, &mut push_new_def);
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.into_iter().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());
}
});
} }
let targets = targets Some(HoverAction::goto_type_from_targets(db, 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();
Some(HoverAction::GoToType(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.into_iter().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 hover_markup(docs: Option<String>, desc: String, mod_path: Option<String>) -> Option<Markup> { fn hover_markup(docs: Option<String>, desc: String, mod_path: Option<String>) -> Option<Markup> {
@ -666,14 +703,14 @@ mod tests {
} }
fn check_actions(ra_fixture: &str, expect: Expect) { fn check_actions(ra_fixture: &str, expect: Expect) {
let (analysis, position) = fixture::position(ra_fixture); let (analysis, file_id, position) = fixture::range_or_position(ra_fixture);
let hover = analysis let hover = analysis
.hover( .hover(
&HoverConfig { &HoverConfig {
links_in_hover: true, links_in_hover: true,
documentation: Some(HoverDocFormat::Markdown), documentation: Some(HoverDocFormat::Markdown),
}, },
FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) }, FileRange { file_id, range: position.range_or_empty() },
) )
.unwrap() .unwrap()
.unwrap(); .unwrap();
@ -4163,4 +4200,37 @@ fn foo() {
"#]], "#]],
); );
} }
#[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",
},
},
],
),
]
"#]],
);
}
} }

View file

@ -113,6 +113,12 @@ impl RangeOrOffset {
RangeOrOffset::Offset(_) => panic!("expected a range but got an offset"), RangeOrOffset::Offset(_) => panic!("expected a range but got an offset"),
} }
} }
pub fn range_or_empty(self) -> TextRange {
match self {
RangeOrOffset::Range(range) => range,
RangeOrOffset::Offset(offset) => TextRange::empty(offset),
}
}
} }
impl From<RangeOrOffset> for TextRange { impl From<RangeOrOffset> for TextRange {