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 events
.into_iter() .into_iter()
.filter_map(|e| match e.kind { .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. // QueryDescriptor at the moment.
salsa::EventKind::WillExecute { database_key } => { salsa::EventKind::WillExecute { database_key } => {
Some(format!("{:?}", database_key.debug(self))) Some(format!("{:?}", database_key.debug(self)))

View file

@ -138,7 +138,7 @@ impl TestDB {
events events
.into_iter() .into_iter()
.filter_map(|e| match e.kind { .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. // QueryDescriptor at the moment.
salsa::EventKind::WillExecute { database_key } => { salsa::EventKind::WillExecute { database_key } => {
Some(format!("{:?}", database_key.debug(self))) Some(format!("{:?}", database_key.debug(self)))

View file

@ -1,7 +1,7 @@
use either::Either; use either::Either;
use hir::{AsAssocItem, HasAttrs, HasSource, HirDisplay, Semantics}; use hir::{AsAssocItem, HasAttrs, HasSource, HirDisplay, Semantics};
use ide_db::{ use ide_db::{
base_db::SourceDatabase, base_db::{FileRange, SourceDatabase},
defs::{Definition, NameClass, NameRefClass}, defs::{Definition, NameClass, NameRefClass},
helpers::{ helpers::{
generated_lints::{CLIPPY_LINTS, DEFAULT_LINTS, FEATURES}, generated_lints::{CLIPPY_LINTS, DEFAULT_LINTS, FEATURES},
@ -12,8 +12,12 @@ use ide_db::{
use itertools::Itertools; 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::{self, find_node_at_range},
SyntaxKind::*, SyntaxToken, T, ast,
display::fn_as_proc_macro_label,
match_ast, AstNode, AstToken, Direction,
SyntaxKind::*,
SyntaxToken, T,
}; };
use crate::{ use crate::{
@ -69,17 +73,39 @@ pub struct HoverResult {
// Feature: Hover // 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. // 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[] // image::https://user-images.githubusercontent.com/48062697/113020658-b5f98b80-917a-11eb-9f88-3dbc27320c95.gif[]
pub(crate) fn hover( pub(crate) fn hover(
db: &RootDatabase, db: &RootDatabase,
position: FilePosition, range: FileRange,
config: &HoverConfig, config: &HoverConfig,
) -> Option<RangeInfo<HoverResult>> { ) -> Option<RangeInfo<HoverResult>> {
let sema = hir::Semantics::new(db); 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 { 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, IDENT | INT_NUMBER | LIFETIME_IDENT | T![self] | T![super] | T![crate] => 3,
T!['('] | T![')'] => 2, T!['('] | T![')'] => 2,
@ -94,8 +120,8 @@ pub(crate) fn hover(
let mut range = None; 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
// field pattern shorthands to their definition // field pattern shorthands to their definition.
ast::Name(name) => NameClass::classify(&sema, &name).map(|class| match class { ast::Name(name) => NameClass::classify(&sema, &name).map(|class| match class {
NameClass::Definition(it) | NameClass::ConstReference(it) => it, NameClass::Definition(it) | NameClass::ConstReference(it) => it,
NameClass::PatFieldShorthand { local_def, field_ref: _ } => Definition::Local(local_def), NameClass::PatFieldShorthand { local_def, field_ref: _ } => Definition::Local(local_def),
@ -193,6 +219,7 @@ pub(crate) fn hover(
} else { } else {
ty.display(db).to_string().into() ty.display(db).to_string().into()
}; };
let range = sema.original_range(&node).range; let range = sema.original_range(&node).range;
Some(RangeInfo::new(range, res)) Some(RangeInfo::new(range, res))
} }
@ -530,7 +557,8 @@ fn find_std_module(famous_defs: &FamousDefs, name: &str) -> Option<hir::Module>
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use expect_test::{expect, Expect}; 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}; use crate::{fixture, hover::HoverDocFormat, HoverConfig};
@ -542,7 +570,7 @@ mod tests {
links_in_hover: true, links_in_hover: true,
documentation: Some(HoverDocFormat::Markdown), documentation: Some(HoverDocFormat::Markdown),
}, },
position, FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) },
) )
.unwrap(); .unwrap();
assert!(hover.is_none()); assert!(hover.is_none());
@ -556,7 +584,7 @@ mod tests {
links_in_hover: true, links_in_hover: true,
documentation: Some(HoverDocFormat::Markdown), documentation: Some(HoverDocFormat::Markdown),
}, },
position, FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) },
) )
.unwrap() .unwrap()
.unwrap(); .unwrap();
@ -576,7 +604,7 @@ mod tests {
links_in_hover: false, links_in_hover: false,
documentation: Some(HoverDocFormat::Markdown), documentation: Some(HoverDocFormat::Markdown),
}, },
position, FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) },
) )
.unwrap() .unwrap()
.unwrap(); .unwrap();
@ -596,7 +624,7 @@ mod tests {
links_in_hover: true, links_in_hover: true,
documentation: Some(HoverDocFormat::PlainText), documentation: Some(HoverDocFormat::PlainText),
}, },
position, FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) },
) )
.unwrap() .unwrap()
.unwrap(); .unwrap();
@ -616,13 +644,42 @@ mod tests {
links_in_hover: true, links_in_hover: true,
documentation: Some(HoverDocFormat::Markdown), documentation: Some(HoverDocFormat::Markdown),
}, },
position, FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) },
) )
.unwrap() .unwrap()
.unwrap(); .unwrap();
expect.assert_debug_eq(&hover.info.actions) 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] #[test]
fn hover_shows_type_of_an_expression() { fn hover_shows_type_of_an_expression() {
check( 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( pub fn hover(
&self, &self,
config: &HoverConfig, config: &HoverConfig,
position: FilePosition, range: FileRange,
) -> Cancellable<Option<RangeInfo<HoverResult>>> { ) -> 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. /// 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, "ssr": true,
"onEnter": true, "onEnter": true,
"parentModule": true, "parentModule": true,
"hoverRange": true,
"runnables": { "runnables": {
"kinds": [ "cargo" ], "kinds": [ "cargo" ],
}, },

View file

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

View file

@ -376,11 +376,28 @@ pub struct SnippetTextEdit {
pub enum HoverRequest {} pub enum HoverRequest {}
impl Request for HoverRequest { impl Request for HoverRequest {
type Params = lsp_types::HoverParams; type Params = HoverParams;
type Result = Option<Hover>; type Result = Option<Hover>;
const METHOD: &'static str = "textDocument/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)] #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
pub struct Hover { pub struct Hover {
#[serde(flatten)] #[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) { fn go(diff: &mut TreeDiff, lhs: SyntaxElement, rhs: SyntaxElement) {
let (lhs, rhs) = match lhs.as_node().zip(rhs.as_node()) { let (lhs, rhs) = match lhs.as_node().zip(rhs.as_node()) {
Some((lhs, rhs)) => (lhs, rhs), Some((lhs, rhs)) => (lhs, rhs),

View file

@ -1,5 +1,5 @@
<!--- <!---
lsp_ext.rs hash: 3b2931972b33198b lsp_ext.rs hash: 5f96a69eb3a5ebc3
If you need to change the above hash to make the test pass, please check if you If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue: need to adjust this doc as well and ping this issue:
@ -13,7 +13,7 @@ need to adjust this doc as well and ping this issue:
This document describes LSP extensions used by rust-analyzer. This document describes LSP extensions used by rust-analyzer.
It's a best effort document, when in doubt, consult the source (and send a PR with clarification ;-) ). It's a best effort document, when in doubt, consult the source (and send a PR with clarification ;-) ).
We aim to upstream all non Rust-specific extensions to the protocol, but this is not a top priority. We aim to upstream all non Rust-specific extensions to the protocol, but this is not a top priority.
All capabilities are enabled via `experimental` field of `ClientCapabilities` or `ServerCapabilities`. All capabilities are enabled via the `experimental` field of `ClientCapabilities` or `ServerCapabilities`.
Requests which we hope to upstream live under `experimental/` namespace. Requests which we hope to upstream live under `experimental/` namespace.
Requests, which are likely to always remain specific to `rust-analyzer` are under `rust-analyzer/` namespace. Requests, which are likely to always remain specific to `rust-analyzer` are under `rust-analyzer/` namespace.
@ -29,7 +29,7 @@ https://clangd.llvm.org/extensions.html#utf-8-offsets
**Issue:** https://github.com/microsoft/language-server-protocol/issues/567 **Issue:** https://github.com/microsoft/language-server-protocol/issues/567
The `initializationOptions` filed of the `InitializeParams` of the initialization request should contain `"rust-analyzer"` section of the configuration. The `initializationOptions` field of the `InitializeParams` of the initialization request should contain the `"rust-analyzer"` section of the configuration.
`rust-analyzer` normally sends a `"workspace/configuration"` request with `{ "items": ["rust-analyzer"] }` payload. `rust-analyzer` normally sends a `"workspace/configuration"` request with `{ "items": ["rust-analyzer"] }` payload.
However, the server can't do this during initialization. However, the server can't do this during initialization.
@ -81,7 +81,7 @@ At the moment, rust-analyzer guarantees that only a single edit will have `Inser
**Experimental Client Capability:** `{ "codeActionGroup": boolean }` **Experimental Client Capability:** `{ "codeActionGroup": boolean }`
If this capability is set, `CodeAction` returned from the server contain an additional field, `group`: If this capability is set, `CodeAction`s returned from the server contain an additional field, `group`:
```typescript ```typescript
interface CodeAction { interface CodeAction {
@ -209,7 +209,7 @@ fn main() {
**Experimental Server Capability:** `{ "onEnter": boolean }` **Experimental Server Capability:** `{ "onEnter": boolean }`
This request is sent from client to server to handle <kbd>Enter</kbd> keypress. This request is sent from client to server to handle the <kbd>Enter</kbd> key press.
**Method:** `experimental/onEnter` **Method:** `experimental/onEnter`
@ -658,6 +658,33 @@ interface TestInfo {
} }
``` ```
## Hover Range
**Issue:** https://github.com/microsoft/language-server-protocol/issues/377
**Experimental Server Capability:** { "hoverRange": boolean }
This request build upon the current `textDocument/hover` to show the type of the expression currently selected.
```typescript
interface HoverParams extends lc.WorkDoneProgressParams {
textDocument: lc.TextDocumentIdentifier;
position: lc.Range | lc.Position;
}
```
Whenever the client sends a `Range`, it is understood as the current selection and any hover included in the range will show the type of the expression if possible.
### Example
```rust
fn main() {
let expression = $01 + 2 * 3$0;
}
```
Triggering a hover inside the selection above will show a result of `i32`.
## Move Item ## Move Item
**Issue:** https://github.com/rust-analyzer/rust-analyzer/issues/6823 **Issue:** https://github.com/rust-analyzer/rust-analyzer/issues/6823

View file

@ -56,9 +56,15 @@ export function createClient(serverPath: string, workspace: Workspace, extraEnv:
traceOutputChannel, traceOutputChannel,
middleware: { middleware: {
async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, _next: lc.ProvideHoverSignature) { 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( const editor = vscode.window.activeTextEditor;
const positionOrRange = editor?.selection?.contains(position) ? client.code2ProtocolConverter.asRange(editor.selection) : client.code2ProtocolConverter.asPosition(position);
return client.sendRequest(ra.hover, {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
position: positionOrRange
}, token).then(
(result) => { (result) => {
const hover = client.protocol2CodeConverter.asHover(result); const hover =
client.protocol2CodeConverter.asHover(result);
if (hover) { if (hover) {
const actions = (<any>result).actions; const actions = (<any>result).actions;
if (actions) { if (actions) {
@ -68,9 +74,15 @@ export function createClient(serverPath: string, workspace: Workspace, extraEnv:
return hover; return hover;
}, },
(error) => { (error) => {
client.handleFailedRequest(lc.HoverRequest.type, token, error, null); client.handleFailedRequest(
lc.HoverRequest.type,
token,
error,
null
);
return Promise.resolve(null); return Promise.resolve(null);
}); }
);
}, },
// Using custom handling of CodeActions to support action groups and snippet edits. // Using custom handling of CodeActions to support action groups and snippet edits.
// Note that this means we have to re-implement lazy edit resolving ourselves as well. // Note that this means we have to re-implement lazy edit resolving ourselves as well.

View file

@ -19,6 +19,13 @@ export const serverStatus = new lc.NotificationType<ServerStatusParams>("experim
export const reloadWorkspace = new lc.RequestType0<null, void>("rust-analyzer/reloadWorkspace"); export const reloadWorkspace = new lc.RequestType0<null, void>("rust-analyzer/reloadWorkspace");
export const hover = new lc.RequestType<HoverParams, lc.Hover | null, void>("textDocument/hover");
export interface HoverParams extends lc.WorkDoneProgressParams {
textDocument: lc.TextDocumentIdentifier;
position: lc.Range | lc.Position;
}
export interface SyntaxTreeParams { export interface SyntaxTreeParams {
textDocument: lc.TextDocumentIdentifier; textDocument: lc.TextDocumentIdentifier;
range: lc.Range | null; range: lc.Range | null;