4729: Hover actions r=matklad a=vsrs

This PR adds a `hoverActions` LSP extension and a `Go to Implementations` action as an example:
![hover_actions_impl](https://user-images.githubusercontent.com/62505555/83335732-6d9de280-a2b7-11ea-8cc3-75253d062fe0.gif)



4748: Add an `ImportMap` and use it to resolve item paths in `find_path` r=matklad a=jonas-schievink

Removes the "go faster" queries I added in https://github.com/rust-analyzer/rust-analyzer/pull/4501 and https://github.com/rust-analyzer/rust-analyzer/pull/4506. I've checked this PR on the rustc code base and the assists are still fast.

This should fix https://github.com/rust-analyzer/rust-analyzer/issues/4515.

Note that this does introduce a change in behavior: We now always refer to items defined in external crates using paths through the external crate. Previously we could also use a local path (if for example the extern crate was reexported locally), as seen in the changed test. If that is undesired I can fix that, but the test didn't say why the previous behavior would be preferable.

Co-authored-by: vsrs <vit@conrlab.com>
Co-authored-by: Jonas Schievink <jonasschievink@gmail.com>
Co-authored-by: Jonas Schievink <jonas.schievink@ferrous-systems.com>
This commit is contained in:
bors[bot] 2020-06-05 15:14:35 +00:00 committed by GitHub
commit 4029628f15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 899 additions and 169 deletions

View file

@ -11,8 +11,8 @@ use ra_syntax::{ast, Parse, SourceFile, TextRange, TextSize};
pub use crate::{
cancellation::Canceled,
input::{
CrateGraph, CrateId, CrateName, Dependency, Edition, Env, ExternSource, ExternSourceId,
FileId, ProcMacroId, SourceRoot, SourceRootId,
CrateData, CrateGraph, CrateId, CrateName, Dependency, Edition, Env, ExternSource,
ExternSourceId, FileId, ProcMacroId, SourceRoot, SourceRootId,
},
};
pub use relative_path::{RelativePath, RelativePathBuf};

View file

@ -3,11 +3,11 @@
pub use hir_def::db::{
AttrsQuery, BodyQuery, BodyWithSourceMapQuery, ConstDataQuery, CrateDefMapQueryQuery,
CrateLangItemsQuery, DefDatabase, DefDatabaseStorage, DocumentationQuery, EnumDataQuery,
ExprScopesQuery, FunctionDataQuery, GenericParamsQuery, ImplDataQuery, InternConstQuery,
InternDatabase, InternDatabaseStorage, InternEnumQuery, InternFunctionQuery, InternImplQuery,
InternStaticQuery, InternStructQuery, InternTraitQuery, InternTypeAliasQuery, InternUnionQuery,
LangItemQuery, ModuleLangItemsQuery, RawItemsQuery, StaticDataQuery, StructDataQuery,
TraitDataQuery, TypeAliasDataQuery, UnionDataQuery,
ExprScopesQuery, FunctionDataQuery, GenericParamsQuery, ImplDataQuery, ImportMapQuery,
InternConstQuery, InternDatabase, InternDatabaseStorage, InternEnumQuery, InternFunctionQuery,
InternImplQuery, InternStaticQuery, InternStructQuery, InternTraitQuery, InternTypeAliasQuery,
InternUnionQuery, LangItemQuery, ModuleLangItemsQuery, RawItemsQuery, StaticDataQuery,
StructDataQuery, TraitDataQuery, TypeAliasDataQuery, UnionDataQuery,
};
pub use hir_expand::db::{
AstDatabase, AstDatabaseStorage, AstIdMapQuery, InternEagerExpansionQuery, InternMacroQuery,

View file

@ -1,7 +1,7 @@
//! Defines database & queries for name resolution.
use std::sync::Arc;
use hir_expand::{db::AstDatabase, name::Name, HirFileId};
use hir_expand::{db::AstDatabase, HirFileId};
use ra_db::{salsa, CrateId, SourceDatabase, Upcast};
use ra_prof::profile;
use ra_syntax::SmolStr;
@ -12,13 +12,10 @@ use crate::{
body::{scope::ExprScopes, Body, BodySourceMap},
data::{ConstData, FunctionData, ImplData, StaticData, TraitData, TypeAliasData},
docs::Documentation,
find_path,
generics::GenericParams,
item_scope::ItemInNs,
import_map::ImportMap,
lang_item::{LangItemTarget, LangItems},
nameres::{raw::RawItems, CrateDefMap},
path::ModPath,
visibility::Visibility,
AttrDefId, ConstId, ConstLoc, DefWithBodyId, EnumId, EnumLoc, FunctionId, FunctionLoc,
GenericDefId, ImplId, ImplLoc, ModuleId, StaticId, StaticLoc, StructId, StructLoc, TraitId,
TraitLoc, TypeAliasId, TypeAliasLoc, UnionId, UnionLoc,
@ -113,15 +110,8 @@ pub trait DefDatabase: InternDatabase + AstDatabase + Upcast<dyn AstDatabase> {
#[salsa::invoke(Documentation::documentation_query)]
fn documentation(&self, def: AttrDefId) -> Option<Documentation>;
#[salsa::invoke(find_path::importable_locations_of_query)]
fn importable_locations_of(
&self,
item: ItemInNs,
krate: CrateId,
) -> Arc<[(ModuleId, Name, Visibility)]>;
#[salsa::invoke(find_path::find_path_inner_query)]
fn find_path_inner(&self, item: ItemInNs, from: ModuleId, max_len: usize) -> Option<ModPath>;
#[salsa::invoke(ImportMap::import_map_query)]
fn import_map(&self, krate: CrateId) -> Arc<ImportMap>;
}
fn crate_def_map_wait(db: &impl DefDatabase, krate: CrateId) -> Arc<CrateDefMap> {

View file

@ -1,9 +1,8 @@
//! An algorithm to find a path to refer to a certain item.
use std::sync::Arc;
use hir_expand::name::{known, AsName, Name};
use ra_prof::profile;
use rustc_hash::FxHashSet;
use test_utils::mark;
use crate::{
@ -11,7 +10,7 @@ use crate::{
item_scope::ItemInNs,
path::{ModPath, PathKind},
visibility::Visibility,
CrateId, ModuleDefId, ModuleId,
ModuleDefId, ModuleId,
};
// FIXME: handle local items
@ -20,7 +19,7 @@ use crate::{
/// *from where* you're referring to the item, hence the `from` parameter.
pub fn find_path(db: &dyn DefDatabase, item: ItemInNs, from: ModuleId) -> Option<ModPath> {
let _p = profile("find_path");
db.find_path_inner(item, from, MAX_PATH_LEN)
find_path_inner(db, item, from, MAX_PATH_LEN)
}
const MAX_PATH_LEN: usize = 15;
@ -36,20 +35,9 @@ impl ModPath {
let first_segment = self.segments.first();
first_segment == Some(&known::alloc) || first_segment == Some(&known::core)
}
fn len(&self) -> usize {
self.segments.len()
+ match self.kind {
PathKind::Plain => 0,
PathKind::Super(i) => i as usize,
PathKind::Crate => 1,
PathKind::Abs => 0,
PathKind::DollarCrate(_) => 1,
}
}
}
pub(crate) fn find_path_inner_query(
fn find_path_inner(
db: &dyn DefDatabase,
item: ItemInNs,
from: ModuleId,
@ -133,31 +121,61 @@ pub(crate) fn find_path_inner_query(
}
// - otherwise, look for modules containing (reexporting) it and import it from one of those
let crate_root = ModuleId { local_id: def_map.root, krate: from.krate };
let crate_attrs = db.attrs(crate_root.into());
let prefer_no_std = crate_attrs.by_key("no_std").exists();
let importable_locations = find_importable_locations(db, item, from);
let mut best_path = None;
let mut best_path_len = max_len;
for (module_id, name) in importable_locations {
let mut path = match db.find_path_inner(
ItemInNs::Types(ModuleDefId::ModuleId(module_id)),
from,
best_path_len - 1,
) {
None => continue,
Some(path) => path,
};
path.segments.push(name);
let new_path = if let Some(best_path) = best_path {
select_best_path(best_path, path, prefer_no_std)
} else {
path
};
best_path_len = new_path.len();
best_path = Some(new_path);
if item.krate(db) == Some(from.krate) {
// Item was defined in the same crate that wants to import it. It cannot be found in any
// dependency in this case.
let local_imports = find_local_import_locations(db, item, from);
for (module_id, name) in local_imports {
if let Some(mut path) = find_path_inner(
db,
ItemInNs::Types(ModuleDefId::ModuleId(module_id)),
from,
best_path_len - 1,
) {
path.segments.push(name);
let new_path = if let Some(best_path) = best_path {
select_best_path(best_path, path, prefer_no_std)
} else {
path
};
best_path_len = new_path.len();
best_path = Some(new_path);
}
}
} else {
// Item was defined in some upstream crate. This means that it must be exported from one,
// too (unless we can't name it at all). It could *also* be (re)exported by the same crate
// that wants to import it here, but we always prefer to use the external path here.
let crate_graph = db.crate_graph();
let extern_paths = crate_graph[from.krate].dependencies.iter().filter_map(|dep| {
let import_map = db.import_map(dep.crate_id);
import_map.path_of(item).map(|modpath| {
let mut modpath = modpath.clone();
modpath.segments.insert(0, dep.as_name());
modpath
})
});
for path in extern_paths {
let new_path = if let Some(best_path) = best_path {
select_best_path(best_path, path, prefer_no_std)
} else {
path
};
best_path = Some(new_path);
}
}
best_path
}
@ -185,69 +203,86 @@ fn select_best_path(old_path: ModPath, new_path: ModPath, prefer_no_std: bool) -
}
}
fn find_importable_locations(
/// Finds locations in `from.krate` from which `item` can be imported by `from`.
fn find_local_import_locations(
db: &dyn DefDatabase,
item: ItemInNs,
from: ModuleId,
) -> Vec<(ModuleId, Name)> {
let crate_graph = db.crate_graph();
let mut result = Vec::new();
// We only look in the crate from which we are importing, and the direct
// dependencies. We cannot refer to names from transitive dependencies
// directly (only through reexports in direct dependencies).
for krate in Some(from.krate)
.into_iter()
.chain(crate_graph[from.krate].dependencies.iter().map(|dep| dep.crate_id))
{
result.extend(
db.importable_locations_of(item, krate)
.iter()
.filter(|(_, _, vis)| vis.is_visible_from(db, from))
.map(|(m, n, _)| (*m, n.clone())),
);
}
result
}
let _p = profile("find_local_import_locations");
// `from` can import anything below `from` with visibility of at least `from`, and anything
// above `from` with any visibility. That means we do not need to descend into private siblings
// of `from` (and similar).
let def_map = db.crate_def_map(from.krate);
// Compute the initial worklist. We start with all direct child modules of `from` as well as all
// of its (recursive) parent modules.
let data = &def_map.modules[from.local_id];
let mut worklist = data
.children
.values()
.map(|child| ModuleId { krate: from.krate, local_id: *child })
.collect::<Vec<_>>();
let mut parent = data.parent;
while let Some(p) = parent {
worklist.push(ModuleId { krate: from.krate, local_id: p });
parent = def_map.modules[p].parent;
}
let mut seen: FxHashSet<_> = FxHashSet::default();
let mut locations = Vec::new();
while let Some(module) = worklist.pop() {
if !seen.insert(module) {
continue; // already processed this module
}
let ext_def_map;
let data = if module.krate == from.krate {
&def_map[module.local_id]
} else {
// The crate might reexport a module defined in another crate.
ext_def_map = db.crate_def_map(module.krate);
&ext_def_map[module.local_id]
};
/// Collects all locations from which we might import the item in a particular
/// crate. These include the original definition of the item, and any
/// non-private `use`s.
///
/// Note that the crate doesn't need to be the one in which the item is defined;
/// it might be re-exported in other crates.
pub(crate) fn importable_locations_of_query(
db: &dyn DefDatabase,
item: ItemInNs,
krate: CrateId,
) -> Arc<[(ModuleId, Name, Visibility)]> {
let _p = profile("importable_locations_of_query");
let def_map = db.crate_def_map(krate);
let mut result = Vec::new();
for (local_id, data) in def_map.modules.iter() {
if let Some((name, vis)) = data.scope.name_of(item) {
let is_private = if let Visibility::Module(private_to) = vis {
private_to.local_id == local_id
} else {
false
};
let is_original_def = if let Some(module_def_id) = item.as_module_def_id() {
data.scope.declarations().any(|it| it == module_def_id)
} else {
false
};
if is_private && !is_original_def {
if vis.is_visible_from(db, from) {
let is_private = if let Visibility::Module(private_to) = vis {
private_to.local_id == module.local_id
} else {
false
};
let is_original_def = if let Some(module_def_id) = item.as_module_def_id() {
data.scope.declarations().any(|it| it == module_def_id)
} else {
false
};
// Ignore private imports. these could be used if we are
// in a submodule of this module, but that's usually not
// what the user wants; and if this module can import
// the item and we're a submodule of it, so can we.
// Also this keeps the cached data smaller.
continue;
if !is_private || is_original_def {
locations.push((module, name.clone()));
}
}
}
// Descend into all modules visible from `from`.
for (_, per_ns) in data.scope.entries() {
if let Some((ModuleDefId::ModuleId(module), vis)) = per_ns.take_types_vis() {
if vis.is_visible_from(db, from) {
worklist.push(module);
}
}
result.push((ModuleId { krate, local_id }, name.clone(), vis));
}
}
Arc::from(result)
locations
}
#[cfg(test)]
@ -385,6 +420,7 @@ mod tests {
#[test]
fn different_crate_renamed() {
// Even if a local path exists, if the item is defined externally, prefer an external path.
let code = r#"
//- /main.rs crate:main deps:std
extern crate std as std_renamed;
@ -392,7 +428,7 @@ mod tests {
//- /std.rs crate:std
pub struct S;
"#;
check_found_path(code, "std_renamed::S");
check_found_path(code, "std::S");
}
#[test]

View file

@ -0,0 +1,331 @@
//! A map of all publicly exported items in a crate.
use std::{collections::hash_map::Entry, fmt, sync::Arc};
use ra_db::CrateId;
use rustc_hash::FxHashMap;
use crate::{
db::DefDatabase,
item_scope::ItemInNs,
path::{ModPath, PathKind},
visibility::Visibility,
ModuleDefId, ModuleId,
};
/// A map from publicly exported items to the path needed to import/name them from a downstream
/// crate.
///
/// Reexports of items are taken into account, ie. if something is exported under multiple
/// names, the one with the shortest import path will be used.
///
/// Note that all paths are relative to the containing crate's root, so the crate name still needs
/// to be prepended to the `ModPath` before the path is valid.
#[derive(Eq, PartialEq)]
pub struct ImportMap {
map: FxHashMap<ItemInNs, ModPath>,
}
impl ImportMap {
pub fn import_map_query(db: &dyn DefDatabase, krate: CrateId) -> Arc<Self> {
let _p = ra_prof::profile("import_map_query");
let def_map = db.crate_def_map(krate);
let mut import_map = FxHashMap::with_capacity_and_hasher(64, Default::default());
// We look only into modules that are public(ly reexported), starting with the crate root.
let empty = ModPath { kind: PathKind::Plain, segments: vec![] };
let root = ModuleId { krate, local_id: def_map.root };
let mut worklist = vec![(root, empty)];
while let Some((module, mod_path)) = worklist.pop() {
let ext_def_map;
let mod_data = if module.krate == krate {
&def_map[module.local_id]
} else {
// The crate might reexport a module defined in another crate.
ext_def_map = db.crate_def_map(module.krate);
&ext_def_map[module.local_id]
};
let visible_items = mod_data.scope.entries().filter_map(|(name, per_ns)| {
let per_ns = per_ns.filter_visibility(|vis| vis == Visibility::Public);
if per_ns.is_none() {
None
} else {
Some((name, per_ns))
}
});
for (name, per_ns) in visible_items {
let mk_path = || {
let mut path = mod_path.clone();
path.segments.push(name.clone());
path
};
for item in per_ns.iter_items() {
let path = mk_path();
match import_map.entry(item) {
Entry::Vacant(entry) => {
entry.insert(path);
}
Entry::Occupied(mut entry) => {
// If the new path is shorter, prefer that one.
if path.len() < entry.get().len() {
*entry.get_mut() = path;
} else {
continue;
}
}
}
// If we've just added a path to a module, descend into it. We might traverse
// modules multiple times, but only if the new path to it is shorter than the
// first (else we `continue` above).
if let Some(ModuleDefId::ModuleId(mod_id)) = item.as_module_def_id() {
worklist.push((mod_id, mk_path()));
}
}
}
}
Arc::new(Self { map: import_map })
}
/// Returns the `ModPath` needed to import/mention `item`, relative to this crate's root.
pub fn path_of(&self, item: ItemInNs) -> Option<&ModPath> {
self.map.get(&item)
}
}
impl fmt::Debug for ImportMap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut importable_paths: Vec<_> = self
.map
.iter()
.map(|(item, modpath)| {
let ns = match item {
ItemInNs::Types(_) => "t",
ItemInNs::Values(_) => "v",
ItemInNs::Macros(_) => "m",
};
format!("- {} ({})", modpath, ns)
})
.collect();
importable_paths.sort();
f.write_str(&importable_paths.join("\n"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_db::TestDB;
use insta::assert_snapshot;
use ra_db::fixture::WithFixture;
use ra_db::SourceDatabase;
fn import_map(ra_fixture: &str) -> String {
let db = TestDB::with_files(ra_fixture);
let crate_graph = db.crate_graph();
let import_maps: Vec<_> = crate_graph
.iter()
.filter_map(|krate| {
let cdata = &crate_graph[krate];
let name = cdata.display_name.as_ref()?;
let map = db.import_map(krate);
Some(format!("{}:\n{:?}", name, map))
})
.collect();
import_maps.join("\n")
}
#[test]
fn smoke() {
let map = import_map(
r"
//- /main.rs crate:main deps:lib
mod private {
pub use lib::Pub;
pub struct InPrivateModule;
}
pub mod publ1 {
use lib::Pub;
}
pub mod real_pub {
pub use lib::Pub;
}
pub mod real_pu2 { // same path length as above
pub use lib::Pub;
}
//- /lib.rs crate:lib
pub struct Pub {}
pub struct Pub2; // t + v
struct Priv;
",
);
assert_snapshot!(map, @r###"
main:
- publ1 (t)
- real_pu2 (t)
- real_pub (t)
- real_pub::Pub (t)
lib:
- Pub (t)
- Pub2 (t)
- Pub2 (v)
"###);
}
#[test]
fn prefers_shortest_path() {
let map = import_map(
r"
//- /main.rs crate:main
pub mod sub {
pub mod subsub {
pub struct Def {}
}
pub use super::sub::subsub::Def;
}
",
);
assert_snapshot!(map, @r###"
main:
- sub (t)
- sub::Def (t)
- sub::subsub (t)
"###);
}
#[test]
fn type_reexport_cross_crate() {
// Reexports need to be visible from a crate, even if the original crate exports the item
// at a shorter path.
let map = import_map(
r"
//- /main.rs crate:main deps:lib
pub mod m {
pub use lib::S;
}
//- /lib.rs crate:lib
pub struct S;
",
);
assert_snapshot!(map, @r###"
main:
- m (t)
- m::S (t)
- m::S (v)
lib:
- S (t)
- S (v)
"###);
}
#[test]
fn macro_reexport() {
let map = import_map(
r"
//- /main.rs crate:main deps:lib
pub mod m {
pub use lib::pub_macro;
}
//- /lib.rs crate:lib
#[macro_export]
macro_rules! pub_macro {
() => {};
}
",
);
assert_snapshot!(map, @r###"
main:
- m (t)
- m::pub_macro (m)
lib:
- pub_macro (m)
"###);
}
#[test]
fn module_reexport() {
// Reexporting modules from a dependency adds all contents to the import map.
let map = import_map(
r"
//- /main.rs crate:main deps:lib
pub use lib::module as reexported_module;
//- /lib.rs crate:lib
pub mod module {
pub struct S;
}
",
);
assert_snapshot!(map, @r###"
main:
- reexported_module (t)
- reexported_module::S (t)
- reexported_module::S (v)
lib:
- module (t)
- module::S (t)
- module::S (v)
"###);
}
#[test]
fn cyclic_module_reexport() {
// A cyclic reexport does not hang.
let map = import_map(
r"
//- /lib.rs crate:lib
pub mod module {
pub struct S;
pub use super::sub::*;
}
pub mod sub {
pub use super::module;
}
",
);
assert_snapshot!(map, @r###"
lib:
- module (t)
- module::S (t)
- module::S (v)
- sub (t)
"###);
}
#[test]
fn private_macro() {
let map = import_map(
r"
//- /lib.rs crate:lib
macro_rules! private_macro {
() => {};
}
",
);
assert_snapshot!(map, @r###"
lib:
"###);
}
}

View file

@ -3,11 +3,12 @@
use hir_expand::name::Name;
use once_cell::sync::Lazy;
use ra_db::CrateId;
use rustc_hash::FxHashMap;
use crate::{
per_ns::PerNs, visibility::Visibility, AdtId, BuiltinType, ImplId, MacroDefId, ModuleDefId,
TraitId,
db::DefDatabase, per_ns::PerNs, visibility::Visibility, AdtId, BuiltinType, HasModule, ImplId,
Lookup, MacroDefId, ModuleDefId, TraitId,
};
#[derive(Debug, Default, PartialEq, Eq)]
@ -203,4 +204,22 @@ impl ItemInNs {
ItemInNs::Macros(_) => None,
}
}
/// Returns the crate defining this item (or `None` if `self` is built-in).
pub fn krate(&self, db: &dyn DefDatabase) -> Option<CrateId> {
Some(match self {
ItemInNs::Types(did) | ItemInNs::Values(did) => match did {
ModuleDefId::ModuleId(id) => id.krate,
ModuleDefId::FunctionId(id) => id.lookup(db).module(db).krate,
ModuleDefId::AdtId(id) => id.module(db).krate,
ModuleDefId::EnumVariantId(id) => id.parent.lookup(db).container.module(db).krate,
ModuleDefId::ConstId(id) => id.lookup(db).container.module(db).krate,
ModuleDefId::StaticId(id) => id.lookup(db).container.module(db).krate,
ModuleDefId::TraitId(id) => id.lookup(db).container.module(db).krate,
ModuleDefId::TypeAliasId(id) => id.lookup(db).module(db).krate,
ModuleDefId::BuiltinType(_) => return None,
},
ItemInNs::Macros(id) => return id.krate,
})
}
}

View file

@ -43,6 +43,7 @@ pub mod child_by_source;
pub mod visibility;
pub mod find_path;
pub mod import_map;
#[cfg(test)]
mod test_db;

View file

@ -76,6 +76,19 @@ impl ModPath {
}
}
/// Returns the number of segments in the path (counting special segments like `$crate` and
/// `super`).
pub fn len(&self) -> usize {
self.segments.len()
+ match self.kind {
PathKind::Plain => 0,
PathKind::Super(i) => i as usize,
PathKind::Crate => 1,
PathKind::Abs => 0,
PathKind::DollarCrate(_) => 1,
}
}
pub fn is_ident(&self) -> bool {
self.kind == PathKind::Plain && self.segments.len() == 1
}

View file

@ -5,7 +5,7 @@
use hir_expand::MacroDefId;
use crate::{visibility::Visibility, ModuleDefId};
use crate::{item_scope::ItemInNs, visibility::Visibility, ModuleDefId};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct PerNs {
@ -84,4 +84,12 @@ impl PerNs {
macros: self.macros.or(other.macros),
}
}
pub fn iter_items(self) -> impl Iterator<Item = ItemInNs> {
self.types
.map(|it| ItemInNs::Types(it.0))
.into_iter()
.chain(self.values.map(|it| ItemInNs::Values(it.0)).into_iter())
.chain(self.macros.map(|it| ItemInNs::Macros(it.0)).into_iter())
}
}

View file

@ -13,14 +13,43 @@ use ra_ide_db::{
use ra_syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset};
use crate::{
display::{macro_label, rust_code_markup, rust_code_markup_with_doc, ShortLabel},
FilePosition, RangeInfo,
display::{macro_label, rust_code_markup, rust_code_markup_with_doc, ShortLabel, ToNav},
FilePosition, NavigationTarget, RangeInfo,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HoverConfig {
pub implementations: bool,
}
impl Default for HoverConfig {
fn default() -> Self {
Self { implementations: true }
}
}
impl HoverConfig {
pub const NO_ACTIONS: Self = Self { implementations: false };
pub fn any(&self) -> bool {
self.implementations
}
pub fn none(&self) -> bool {
!self.any()
}
}
#[derive(Debug, Clone)]
pub enum HoverAction {
Implementaion(FilePosition),
}
/// Contains the results when hovering over an item
#[derive(Debug, Default)]
pub struct HoverResult {
results: Vec<String>,
actions: Vec<HoverAction>,
}
impl HoverResult {
@ -48,10 +77,20 @@ impl HoverResult {
&self.results
}
pub fn actions(&self) -> &[HoverAction] {
&self.actions
}
pub fn push_action(&mut self, action: HoverAction) {
self.actions.push(action);
}
/// Returns the results converted into markup
/// for displaying in a UI
///
/// Does not process actions!
pub fn to_markup(&self) -> String {
self.results.join("\n\n---\n")
self.results.join("\n\n___\n")
}
}
@ -82,6 +121,10 @@ pub(crate) fn hover(db: &RootDatabase, position: FilePosition) -> Option<RangeIn
res.extend(hover_text_from_name_kind(db, name_kind));
if !res.is_empty() {
if let Some(action) = show_implementations_action(db, name_kind) {
res.push_action(action);
}
return Some(RangeInfo::new(range, res));
}
}
@ -112,6 +155,26 @@ pub(crate) fn hover(db: &RootDatabase, position: FilePosition) -> Option<RangeIn
Some(RangeInfo::new(range, res))
}
fn show_implementations_action(db: &RootDatabase, def: Definition) -> Option<HoverAction> {
fn to_action(nav_target: NavigationTarget) -> HoverAction {
HoverAction::Implementaion(FilePosition {
file_id: nav_target.file_id(),
offset: nav_target.range().start(),
})
}
match def {
Definition::ModuleDef(it) => match it {
ModuleDef::Adt(Adt::Struct(it)) => Some(to_action(it.to_nav(db))),
ModuleDef::Adt(Adt::Union(it)) => Some(to_action(it.to_nav(db))),
ModuleDef::Adt(Adt::Enum(it)) => Some(to_action(it.to_nav(db))),
ModuleDef::Trait(it) => Some(to_action(it.to_nav(db))),
_ => None,
},
_ => None,
}
}
fn hover_text(
docs: Option<String>,
desc: Option<String>,
@ -228,6 +291,8 @@ fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> {
#[cfg(test)]
mod tests {
use super::*;
use ra_db::FileLoader;
use ra_syntax::TextRange;
@ -241,7 +306,14 @@ mod tests {
s.map(trim_markup)
}
fn check_hover_result(fixture: &str, expected: &[&str]) -> String {
fn assert_impl_action(action: &HoverAction, position: u32) {
let offset = match action {
HoverAction::Implementaion(pos) => pos.offset,
};
assert_eq!(offset, position.into());
}
fn check_hover_result(fixture: &str, expected: &[&str]) -> (String, Vec<HoverAction>) {
let (analysis, position) = analysis_and_position(fixture);
let hover = analysis.hover(position).unwrap().unwrap();
let mut results = Vec::from(hover.info.results());
@ -256,7 +328,7 @@ mod tests {
assert_eq!(hover.info.len(), expected.len());
let content = analysis.db.file_text(position.file_id);
content[hover.range].to_string()
(content[hover.range].to_string(), hover.info.actions().to_vec())
}
fn check_hover_no_result(fixture: &str) {
@ -746,7 +818,7 @@ fn func(foo: i32) { if true { <|>foo; }; }
#[test]
fn test_hover_through_macro() {
let hover_on = check_hover_result(
let (hover_on, _) = check_hover_result(
"
//- /lib.rs
macro_rules! id {
@ -767,7 +839,7 @@ fn func(foo: i32) { if true { <|>foo; }; }
#[test]
fn test_hover_through_expr_in_macro() {
let hover_on = check_hover_result(
let (hover_on, _) = check_hover_result(
"
//- /lib.rs
macro_rules! id {
@ -785,7 +857,7 @@ fn func(foo: i32) { if true { <|>foo; }; }
#[test]
fn test_hover_through_expr_in_macro_recursive() {
let hover_on = check_hover_result(
let (hover_on, _) = check_hover_result(
"
//- /lib.rs
macro_rules! id_deep {
@ -806,7 +878,7 @@ fn func(foo: i32) { if true { <|>foo; }; }
#[test]
fn test_hover_through_func_in_macro_recursive() {
let hover_on = check_hover_result(
let (hover_on, _) = check_hover_result(
"
//- /lib.rs
macro_rules! id_deep {
@ -830,7 +902,7 @@ fn func(foo: i32) { if true { <|>foo; }; }
#[test]
fn test_hover_through_literal_string_in_macro() {
let hover_on = check_hover_result(
let (hover_on, _) = check_hover_result(
r#"
//- /lib.rs
macro_rules! arr {
@ -849,7 +921,7 @@ fn func(foo: i32) { if true { <|>foo; }; }
#[test]
fn test_hover_through_assert_macro() {
let hover_on = check_hover_result(
let (hover_on, _) = check_hover_result(
r#"
//- /lib.rs
#[rustc_builtin_macro]
@ -925,13 +997,14 @@ fn func(foo: i32) { if true { <|>foo; }; }
#[test]
fn test_hover_trait_show_qualifiers() {
check_hover_result(
let (_, actions) = check_hover_result(
"
//- /lib.rs
unsafe trait foo<|>() {}
",
&["unsafe trait foo"],
);
assert_impl_action(&actions[0], 13);
}
#[test]
@ -1052,4 +1125,55 @@ fn func(foo: i32) { if true { <|>foo; }; }
&["Bar\n```\n\n```rust\nfn foo(&self)\n```\n___\n\nDo the foo"],
);
}
#[test]
fn test_hover_trait_has_impl_action() {
let (_, actions) = check_hover_result(
"
//- /lib.rs
trait foo<|>() {}
",
&["trait foo"],
);
assert_impl_action(&actions[0], 6);
}
#[test]
fn test_hover_struct_has_impl_action() {
let (_, actions) = check_hover_result(
"
//- /lib.rs
struct foo<|>() {}
",
&["struct foo"],
);
assert_impl_action(&actions[0], 7);
}
#[test]
fn test_hover_union_has_impl_action() {
let (_, actions) = check_hover_result(
"
//- /lib.rs
union foo<|>() {}
",
&["union foo"],
);
assert_impl_action(&actions[0], 6);
}
#[test]
fn test_hover_enum_has_impl_action() {
let (_, actions) = check_hover_result(
"
//- /lib.rs
enum foo<|>() {
A,
B
}
",
&["enum foo"],
);
assert_impl_action(&actions[0], 5);
}
}

View file

@ -66,7 +66,7 @@ pub use crate::{
display::{file_structure, FunctionSignature, NavigationTarget, StructureNode},
expand_macro::ExpandedMacro,
folding_ranges::{Fold, FoldKind},
hover::HoverResult,
hover::{HoverAction, HoverConfig, HoverResult},
inlay_hints::{InlayHint, InlayHintsConfig, InlayKind},
references::{Declaration, Reference, ReferenceAccess, ReferenceKind, ReferenceSearchResult},
runnables::{Runnable, RunnableKind, TestId},

View file

@ -334,6 +334,7 @@ impl RootDatabase {
hir::db::CrateLangItemsQuery
hir::db::LangItemQuery
hir::db::DocumentationQuery
hir::db::ImportMapQuery
// InternDatabase
hir::db::InternFunctionQuery

View file

@ -18,7 +18,7 @@ use ra_syntax::{
use crate::RootDatabase;
// FIXME: a more precise name would probably be `Symbol`?
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Definition {
Macro(MacroDef),
Field(Field),

View file

@ -11,7 +11,7 @@ use std::{ffi::OsString, path::PathBuf};
use lsp_types::ClientCapabilities;
use ra_flycheck::FlycheckConfig;
use ra_ide::{AssistConfig, CompletionConfig, InlayHintsConfig};
use ra_ide::{AssistConfig, CompletionConfig, HoverConfig, InlayHintsConfig};
use ra_project_model::{CargoConfig, JsonProject, ProjectManifest};
use serde::Deserialize;
@ -34,6 +34,7 @@ pub struct Config {
pub assist: AssistConfig,
pub call_info_full: bool,
pub lens: LensConfig,
pub hover: HoverConfig,
pub with_sysroot: bool,
pub linked_projects: Vec<LinkedProject>,
@ -124,6 +125,7 @@ pub struct ClientCapsConfig {
pub work_done_progress: bool,
pub code_action_group: bool,
pub resolve_code_action: bool,
pub hover_actions: bool,
}
impl Default for Config {
@ -162,6 +164,7 @@ impl Default for Config {
assist: AssistConfig::default(),
call_info_full: true,
lens: LensConfig::default(),
hover: HoverConfig::default(),
linked_projects: Vec::new(),
}
}
@ -278,6 +281,14 @@ impl Config {
}
}
let mut use_hover_actions = false;
set(value, "/hoverActions/enable", &mut use_hover_actions);
if use_hover_actions {
set(value, "/hoverActions/implementations", &mut self.hover.implementations);
} else {
self.hover = HoverConfig::NO_ACTIONS;
}
log::info!("Config::update() = {:#?}", self);
fn get<'a, T: Deserialize<'a>>(value: &'a serde_json::Value, pointer: &str) -> Option<T> {
@ -331,17 +342,15 @@ impl Config {
self.assist.allow_snippets(false);
if let Some(experimental) = &caps.experimental {
let snippet_text_edit =
experimental.get("snippetTextEdit").and_then(|it| it.as_bool()) == Some(true);
let get_bool =
|index: &str| experimental.get(index).and_then(|it| it.as_bool()) == Some(true);
let snippet_text_edit = get_bool("snippetTextEdit");
self.assist.allow_snippets(snippet_text_edit);
let code_action_group =
experimental.get("codeActionGroup").and_then(|it| it.as_bool()) == Some(true);
self.client_caps.code_action_group = code_action_group;
let resolve_code_action =
experimental.get("resolveCodeAction").and_then(|it| it.as_bool()) == Some(true);
self.client_caps.resolve_code_action = resolve_code_action;
self.client_caps.code_action_group = get_bool("codeActionGroup");
self.client_caps.resolve_code_action = get_bool("resolveCodeAction");
self.client_caps.hover_actions = get_bool("hoverActions");
}
}
}

View file

@ -260,3 +260,35 @@ pub struct SnippetTextEdit {
#[serde(skip_serializing_if = "Option::is_none")]
pub insert_text_format: Option<lsp_types::InsertTextFormat>,
}
pub enum HoverRequest {}
impl Request for HoverRequest {
type Params = lsp_types::HoverParams;
type Result = Option<Hover>;
const METHOD: &'static str = "textDocument/hover";
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
pub struct Hover {
#[serde(flatten)]
pub hover: lsp_types::Hover,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub actions: Vec<CommandLinkGroup>,
}
#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
pub struct CommandLinkGroup {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub commands: Vec<CommandLink>,
}
// LSP v3.15 Command does not have a `tooltip` field, vscode supports one.
#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
pub struct CommandLink {
#[serde(flatten)]
pub command: lsp_types::Command,
#[serde(skip_serializing_if = "Option::is_none")]
pub tooltip: Option<String>,
}

View file

@ -510,6 +510,7 @@ fn on_request(
.on::<lsp_ext::InlayHints>(handlers::handle_inlay_hints)?
.on::<lsp_ext::CodeActionRequest>(handlers::handle_code_action)?
.on::<lsp_ext::ResolveCodeActionRequest>(handlers::handle_resolve_code_action)?
.on::<lsp_ext::HoverRequest>(handlers::handle_hover)?
.on::<lsp_types::request::OnTypeFormatting>(handlers::handle_on_type_formatting)?
.on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol)?
.on::<lsp_types::request::WorkspaceSymbol>(handlers::handle_workspace_symbol)?
@ -521,7 +522,6 @@ fn on_request(
.on::<lsp_types::request::CodeLensResolve>(handlers::handle_code_lens_resolve)?
.on::<lsp_types::request::FoldingRangeRequest>(handlers::handle_folding_range)?
.on::<lsp_types::request::SignatureHelpRequest>(handlers::handle_signature_help)?
.on::<lsp_types::request::HoverRequest>(handlers::handle_hover)?
.on::<lsp_types::request::PrepareRenameRequest>(handlers::handle_prepare_rename)?
.on::<lsp_types::request::Rename>(handlers::handle_rename)?
.on::<lsp_types::request::References>(handlers::handle_references)?

View file

@ -12,13 +12,14 @@ use lsp_types::{
CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams,
CodeLens, Command, CompletionItem, Diagnostic, DocumentFormattingParams, DocumentHighlight,
DocumentSymbol, FoldingRange, FoldingRangeParams, Hover, HoverContents, Location,
MarkupContent, MarkupKind, Position, PrepareRenameResponse, Range, RenameParams,
SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRangeResult,
SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, Url, WorkspaceEdit,
DocumentSymbol, FoldingRange, FoldingRangeParams, HoverContents, Location, MarkupContent,
MarkupKind, Position, PrepareRenameResponse, Range, RenameParams, SemanticTokensParams,
SemanticTokensRangeParams, SemanticTokensRangeResult, SemanticTokensResult, SymbolInformation,
TextDocumentIdentifier, Url, WorkspaceEdit,
};
use ra_ide::{
FileId, FilePosition, FileRange, Query, RangeInfo, RunnableKind, SearchScope, TextEdit,
FileId, FilePosition, FileRange, HoverAction, Query, RangeInfo, RunnableKind, SearchScope,
TextEdit,
};
use ra_prof::profile;
use ra_project_model::TargetKind;
@ -537,7 +538,7 @@ pub fn handle_signature_help(
pub fn handle_hover(
snap: GlobalStateSnapshot,
params: lsp_types::HoverParams,
) -> Result<Option<Hover>> {
) -> Result<Option<lsp_ext::Hover>> {
let _p = profile("handle_hover");
let position = from_proto::file_position(&snap, params.text_document_position_params)?;
let info = match snap.analysis().hover(position)? {
@ -546,14 +547,18 @@ pub fn handle_hover(
};
let line_index = snap.analysis.file_line_index(position.file_id)?;
let range = to_proto::range(&line_index, info.range);
let res = Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: crate::markdown::format_docs(&info.info.to_markup()),
}),
range: Some(range),
let hover = lsp_ext::Hover {
hover: lsp_types::Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: crate::markdown::format_docs(&info.info.to_markup()),
}),
range: Some(range),
},
actions: prepare_hover_actions(&snap, info.info.actions()),
};
Ok(Some(res))
Ok(Some(hover))
}
pub fn handle_prepare_rename(
@ -924,24 +929,13 @@ pub fn handle_code_lens_resolve(
_ => vec![],
};
let title = if locations.len() == 1 {
"1 implementation".into()
} else {
format!("{} implementations", locations.len())
};
// We cannot use the 'editor.action.showReferences' command directly
// because that command requires vscode types which we convert in the handler
// on the client side.
let cmd = Command {
let title = implementation_title(locations.len());
let cmd = show_references_command(
title,
command: "rust-analyzer.showReferences".into(),
arguments: Some(vec![
to_value(&lens_params.text_document_position_params.text_document.uri).unwrap(),
to_value(code_lens.range.start).unwrap(),
to_value(locations).unwrap(),
]),
};
&lens_params.text_document_position_params.text_document.uri,
code_lens.range.start,
locations,
);
Ok(CodeLens { range: code_lens.range, command: Some(cmd), data: None })
}
None => Ok(CodeLens {
@ -1145,3 +1139,78 @@ pub fn handle_semantic_tokens_range(
let semantic_tokens = to_proto::semantic_tokens(&text, &line_index, highlights);
Ok(Some(semantic_tokens.into()))
}
fn implementation_title(count: usize) -> String {
if count == 1 {
"1 implementation".into()
} else {
format!("{} implementations", count)
}
}
fn show_references_command(
title: String,
uri: &lsp_types::Url,
position: lsp_types::Position,
locations: Vec<lsp_types::Location>,
) -> Command {
// We cannot use the 'editor.action.showReferences' command directly
// because that command requires vscode types which we convert in the handler
// on the client side.
Command {
title,
command: "rust-analyzer.showReferences".into(),
arguments: Some(vec![
to_value(uri).unwrap(),
to_value(position).unwrap(),
to_value(locations).unwrap(),
]),
}
}
fn to_command_link(command: Command, tooltip: String) -> lsp_ext::CommandLink {
lsp_ext::CommandLink { tooltip: Some(tooltip), command }
}
fn show_impl_command_link(
snap: &GlobalStateSnapshot,
position: &FilePosition,
) -> Option<lsp_ext::CommandLinkGroup> {
if snap.config.hover.implementations {
if let Some(nav_data) = snap.analysis().goto_implementation(*position).unwrap_or(None) {
let uri = to_proto::url(snap, position.file_id).ok()?;
let line_index = snap.analysis().file_line_index(position.file_id).ok()?;
let position = to_proto::position(&line_index, position.offset);
let locations: Vec<_> = nav_data
.info
.iter()
.filter_map(|it| to_proto::location(snap, it.file_range()).ok())
.collect();
let title = implementation_title(locations.len());
let command = show_references_command(title, &uri, position, locations);
return Some(lsp_ext::CommandLinkGroup {
commands: vec![to_command_link(command, "Go to implementations".into())],
..Default::default()
});
}
}
None
}
fn prepare_hover_actions(
snap: &GlobalStateSnapshot,
actions: &[HoverAction],
) -> Vec<lsp_ext::CommandLinkGroup> {
if snap.config.hover.none() || !snap.config.client_caps.hover_actions {
return Vec::new();
}
actions
.iter()
.filter_map(|it| match it {
HoverAction::Implementaion(position) => show_impl_command_link(snap, position),
})
.collect()
}

View file

@ -467,3 +467,41 @@ interface InlayHint {
label: string,
}
```
## Hover Actions
**Client Capability:** `{ "hoverActions": boolean }`
If this capability is set, `Hover` request returned from the server might contain an additional field, `actions`:
```typescript
interface Hover {
...
actions?: CommandLinkGroup[];
}
interface CommandLink extends Command {
/**
* A tooltip for the command, when represented in the UI.
*/
tooltip?: string;
}
interface CommandLinkGroup {
title?: string;
commands: CommandLink[];
}
```
Such actions on the client side are appended to a hover bottom as command links:
```
+-----------------------------+
| Hover content |
| |
+-----------------------------+
| _Action1_ | _Action2_ | <- first group, no TITLE
+-----------------------------+
| TITLE _Action1_ | _Action2_ | <- second group
+-----------------------------+
...
```

View file

@ -462,17 +462,27 @@
"default": true
},
"rust-analyzer.lens.run": {
"markdownDescription": "Whether to show Run lens. Only applies when `#rust-analyzer.lens.enable#` is set.",
"markdownDescription": "Whether to show `Run` lens. Only applies when `#rust-analyzer.lens.enable#` is set.",
"type": "boolean",
"default": true
},
"rust-analyzer.lens.debug": {
"markdownDescription": "Whether to show Debug lens. Only applies when `#rust-analyzer.lens.enable#` is set.",
"markdownDescription": "Whether to show `Debug` lens. Only applies when `#rust-analyzer.lens.enable#` is set.",
"type": "boolean",
"default": true
},
"rust-analyzer.lens.implementations": {
"markdownDescription": "Whether to show Implementations lens. Only applies when `#rust-analyzer.lens.enable#` is set.",
"markdownDescription": "Whether to show `Implementations` lens. Only applies when `#rust-analyzer.lens.enable#` is set.",
"type": "boolean",
"default": true
},
"rust-analyzer.hoverActions.enable": {
"description": "Whether to show HoverActions in Rust files.",
"type": "boolean",
"default": true
},
"rust-analyzer.hoverActions.implementations": {
"markdownDescription": "Whether to show `Implementations` action. Only applies when `#rust-analyzer.hoverActions.enable#` is set.",
"type": "boolean",
"default": true
},

View file

@ -7,6 +7,20 @@ import { CallHierarchyFeature } from 'vscode-languageclient/lib/callHierarchy.pr
import { SemanticTokensFeature, DocumentSemanticsTokensSignature } from 'vscode-languageclient/lib/semanticTokens.proposed';
import { assert } from './util';
function renderCommand(cmd: ra.CommandLink) {
return `[${cmd.title}](command:${cmd.command}?${encodeURIComponent(JSON.stringify(cmd.arguments))} '${cmd.tooltip!}')`;
}
function renderHoverActions(actions: ra.CommandLinkGroup[]): vscode.MarkdownString {
const text = actions.map(group =>
(group.title ? (group.title + " ") : "") + group.commands.map(renderCommand).join(' | ')
).join('___');
const result = new vscode.MarkdownString(text);
result.isTrusted = true;
return result;
}
export function createClient(serverPath: string, cwd: string): lc.LanguageClient {
// '.' Is the fallback if no folder is open
// TODO?: Workspace folders support Uri's (eg: file://test.txt).
@ -35,6 +49,23 @@ export function createClient(serverPath: string, cwd: string): lc.LanguageClient
if (res === undefined) throw new Error('busy');
return res;
},
async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, _next: lc.ProvideHoverSignature) {
return client.sendRequest(lc.HoverRequest.type, client.code2ProtocolConverter.asTextDocumentPositionParams(document, position), token).then(
(result) => {
const hover = client.protocol2CodeConverter.asHover(result);
if (hover) {
const actions = (<any>result).actions;
if (actions) {
hover.contents.push(renderHoverActions(actions));
}
}
return hover;
},
(error) => {
client.logFailedRequest(lc.HoverRequest.type, error);
return Promise.resolve(null);
});
},
// Using custom handling of CodeActions where each code action is resloved lazily
// That's why we are not waiting for any command or edits
async provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken, _next: lc.ProvideCodeActionsSignature) {
@ -129,6 +160,7 @@ class ExperimentalFeatures implements lc.StaticFeature {
caps.snippetTextEdit = true;
caps.codeActionGroup = true;
caps.resolveCodeAction = true;
caps.hoverActions = true;
capabilities.experimental = caps;
}
initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void {

View file

@ -16,10 +16,8 @@ export class Config {
"files",
"highlighting",
"updates.channel",
"lens.enable",
"lens.run",
"lens.debug",
"lens.implementations",
"lens", // works as lens.*
"hoverActions", // works as hoverActions.*
]
.map(opt => `${this.rootSection}.${opt}`);
@ -132,4 +130,11 @@ export class Config {
implementations: this.get<boolean>("lens.implementations"),
};
}
get hoverActions() {
return {
enable: this.get<boolean>("hoverActions.enable"),
implementations: this.get<boolean>("hoverActions.implementations"),
};
}
}

View file

@ -90,3 +90,15 @@ export interface SsrParams {
parseOnly: boolean;
}
export const ssr = new lc.RequestType<SsrParams, lc.WorkspaceEdit, void>('experimental/ssr');
export interface CommandLink extends lc.Command {
/**
* A tooltip for the command, when represented in the UI.
*/
tooltip?: string;
}
export interface CommandLinkGroup {
title?: string;
commands: CommandLink[];
}