9693: feat: Add the Hover Range capability which enables showing the type of an expression r=matklad a=alexfertel

Closes https://github.com/rust-analyzer/rust-analyzer/issues/389

This PR extends the `textDocument/hover` method to allow getting the type of an expression. It looks like this:

![type_of_expression](https://user-images.githubusercontent.com/22298999/126914293-0ce49a92-545d-4005-a59e-9294fa2330d6.gif)

Edit: One thing I noticed is that when hovering a selection that includes a macro it doesn't work, so maybe this would need a follow-up issue discussing what problem that may have.

(PS: What a great project! I am learning a lot! 🚀)

Co-authored-by: Alexander Gonzalez <alexfertel97@gmail.com>
Co-authored-by: Alexander González <alexfertel97@gmail.com>
This commit is contained in:
bors[bot] 2021-07-28 11:21:33 +00:00 committed by GitHub
commit 068ede0991
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 302 additions and 34 deletions

View file

@ -233,7 +233,7 @@ impl TestDB {
events
.into_iter()
.filter_map(|e| match e.kind {
// This pretty horrible, but `Debug` is the only way to inspect
// This is pretty horrible, but `Debug` is the only way to inspect
// QueryDescriptor at the moment.
salsa::EventKind::WillExecute { database_key } => {
Some(format!("{:?}", database_key.debug(self)))

View file

@ -138,7 +138,7 @@ impl TestDB {
events
.into_iter()
.filter_map(|e| match e.kind {
// This pretty horrible, but `Debug` is the only way to inspect
// This is pretty horrible, but `Debug` is the only way to inspect
// QueryDescriptor at the moment.
salsa::EventKind::WillExecute { database_key } => {
Some(format!("{:?}", database_key.debug(self)))

View file

@ -1,7 +1,7 @@
use either::Either;
use hir::{AsAssocItem, HasAttrs, HasSource, HirDisplay, Semantics};
use ide_db::{
base_db::SourceDatabase,
base_db::{FileRange, SourceDatabase},
defs::{Definition, NameClass, NameRefClass},
helpers::{
generated_lints::{CLIPPY_LINTS, DEFAULT_LINTS, FEATURES},
@ -12,8 +12,12 @@ use ide_db::{
use itertools::Itertools;
use stdx::format_to;
use syntax::{
algo, ast, display::fn_as_proc_macro_label, match_ast, AstNode, AstToken, Direction,
SyntaxKind::*, SyntaxToken, T,
algo::{self, find_node_at_range},
ast,
display::fn_as_proc_macro_label,
match_ast, AstNode, AstToken, Direction,
SyntaxKind::*,
SyntaxToken, T,
};
use crate::{
@ -69,17 +73,39 @@ pub struct HoverResult {
// Feature: Hover
//
// Shows additional information, like type of an expression or documentation for definition when "focusing" code.
// 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,
position: FilePosition,
range: FileRange,
config: &HoverConfig,
) -> Option<RangeInfo<HoverResult>> {
let sema = hir::Semantics::new(db);
let file = sema.parse(position.file_id).syntax().clone();
let file = sema.parse(range.file_id).syntax().clone();
// This means we're hovering over a range.
if !range.range.is_empty() {
let expr = find_node_at_range::<ast::Expr>(&file, range.range)?;
let ty = sema.type_of_expr(&expr)?;
if ty.is_unknown() {
return None;
}
let mut res = HoverResult::default();
res.markup = if config.markdown() {
Markup::fenced_block(&ty.display(db))
} else {
ty.display(db).to_string().into()
};
return Some(RangeInfo::new(range.range, res));
}
let position = FilePosition { file_id: range.file_id, offset: range.range.start() };
let token = pick_best_token(file.token_at_offset(position.offset), |kind| match kind {
IDENT | INT_NUMBER | LIFETIME_IDENT | T![self] | T![super] | T![crate] => 3,
T!['('] | T![')'] => 2,
@ -94,8 +120,8 @@ pub(crate) fn hover(
let mut range = None;
let definition = match_ast! {
match node {
// we don't use NameClass::referenced_or_defined here as we do not want to resolve
// field pattern shorthands to their definition
// We don't use NameClass::referenced_or_defined here as we do not want to resolve
// field pattern shorthands to their definition.
ast::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),
@ -193,6 +219,7 @@ pub(crate) fn hover(
} else {
ty.display(db).to_string().into()
};
let range = sema.original_range(&node).range;
Some(RangeInfo::new(range, res))
}
@ -530,7 +557,8 @@ fn find_std_module(famous_defs: &FamousDefs, name: &str) -> Option<hir::Module>
#[cfg(test)]
mod tests {
use expect_test::{expect, Expect};
use ide_db::base_db::FileLoader;
use ide_db::base_db::{FileLoader, FileRange};
use syntax::TextRange;
use crate::{fixture, hover::HoverDocFormat, HoverConfig};
@ -542,7 +570,7 @@ mod tests {
links_in_hover: true,
documentation: Some(HoverDocFormat::Markdown),
},
position,
FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) },
)
.unwrap();
assert!(hover.is_none());
@ -556,7 +584,7 @@ mod tests {
links_in_hover: true,
documentation: Some(HoverDocFormat::Markdown),
},
position,
FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) },
)
.unwrap()
.unwrap();
@ -576,7 +604,7 @@ mod tests {
links_in_hover: false,
documentation: Some(HoverDocFormat::Markdown),
},
position,
FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) },
)
.unwrap()
.unwrap();
@ -596,7 +624,7 @@ mod tests {
links_in_hover: true,
documentation: Some(HoverDocFormat::PlainText),
},
position,
FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) },
)
.unwrap()
.unwrap();
@ -616,13 +644,42 @@ mod tests {
links_in_hover: true,
documentation: Some(HoverDocFormat::Markdown),
},
position,
FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) },
)
.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_shows_type_of_an_expression() {
check(
@ -3882,4 +3939,142 @@ struct Foo;
"#]],
);
}
#[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
()
```"#]],
);
}
}

View file

@ -418,9 +418,9 @@ impl Analysis {
pub fn hover(
&self,
config: &HoverConfig,
position: FilePosition,
range: FileRange,
) -> Cancellable<Option<RangeInfo<HoverResult>>> {
self.with_db(|db| hover::hover(db, position, config))
self.with_db(|db| hover::hover(db, range, config))
}
/// Return URL(s) for the documentation of the symbol under the cursor.

View file

@ -118,6 +118,7 @@ pub fn server_capabilities(config: &Config) -> ServerCapabilities {
"ssr": true,
"onEnter": true,
"parentModule": true,
"hoverRange": true,
"runnables": {
"kinds": [ "cargo" ],
},

View file

@ -36,7 +36,10 @@ use crate::{
from_proto,
global_state::{GlobalState, GlobalStateSnapshot},
line_index::LineEndings,
lsp_ext::{self, InlayHint, InlayHintsParams, ViewCrateGraphParams, WorkspaceSymbolParams},
lsp_ext::{
self, InlayHint, InlayHintsParams, PositionOrRange, ViewCrateGraphParams,
WorkspaceSymbolParams,
},
lsp_utils::all_edits_are_disjoint,
to_proto, LspError, Result,
};
@ -867,15 +870,21 @@ pub(crate) fn handle_signature_help(
pub(crate) fn handle_hover(
snap: GlobalStateSnapshot,
params: lsp_types::HoverParams,
params: lsp_ext::HoverParams,
) -> Result<Option<lsp_ext::Hover>> {
let _p = profile::span("handle_hover");
let position = from_proto::file_position(&snap, params.text_document_position_params)?;
let info = match snap.analysis.hover(&snap.config.hover(), position)? {
let range = match params.position {
PositionOrRange::Position(position) => Range::new(position, position),
PositionOrRange::Range(range) => range,
};
let file_range = from_proto::file_range(&snap, params.text_document, range)?;
let info = match snap.analysis.hover(&snap.config.hover(), file_range)? {
None => return Ok(None),
Some(info) => info,
};
let line_index = snap.file_line_index(position.file_id)?;
let line_index = snap.file_line_index(file_range.file_id)?;
let range = to_proto::range(&line_index, info.range);
let hover = lsp_ext::Hover {
hover: lsp_types::Hover {

View file

@ -376,11 +376,28 @@ pub struct SnippetTextEdit {
pub enum HoverRequest {}
impl Request for HoverRequest {
type Params = lsp_types::HoverParams;
type Params = HoverParams;
type Result = Option<Hover>;
const METHOD: &'static str = "textDocument/hover";
}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HoverParams {
pub text_document: TextDocumentIdentifier,
pub position: PositionOrRange,
#[serde(flatten)]
pub work_done_progress_params: WorkDoneProgressParams,
}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum PositionOrRange {
Position(lsp_types::Position),
Range(lsp_types::Range),
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
pub struct Hover {
#[serde(flatten)]

View file

@ -173,7 +173,7 @@ pub fn diff(from: &SyntaxNode, to: &SyntaxNode) -> TreeDiff {
}
}
// FIXME: this is horrible inefficient. I bet there's a cool algorithm to diff trees properly.
// FIXME: this is horribly inefficient. I bet there's a cool algorithm to diff trees properly.
fn go(diff: &mut TreeDiff, lhs: SyntaxElement, rhs: SyntaxElement) {
let (lhs, rhs) = match lhs.as_node().zip(rhs.as_node()) {
Some((lhs, rhs)) => (lhs, rhs),