diff --git a/crates/hir_def/src/test_db.rs b/crates/hir_def/src/test_db.rs index 2635b556e652..baf71731f497 100644 --- a/crates/hir_def/src/test_db.rs +++ b/crates/hir_def/src/test_db.rs @@ -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))) diff --git a/crates/hir_ty/src/test_db.rs b/crates/hir_ty/src/test_db.rs index b99a03492b9e..ee6c2fa4f8c6 100644 --- a/crates/hir_ty/src/test_db.rs +++ b/crates/hir_ty/src/test_db.rs @@ -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))) diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 35601f2efc7a..a2df3b895f6f 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -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> { 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::(&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 #[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(a: &[T]) { } +fn b() { $0f$0(&[1, 2, 3, 4, 5]); } + "#, + expect![[r#" + ```rust + fn f(&[i32]) + ```"#]], + ); + + check_hover_range( + r#" +fn f(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(a: &[T]) { } +fn b()$0 { f(&[1, 2, 3, 4, 5]); }$0 + "#, + ); + + check_hover_range_no_results( + r#" +fn f$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(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 + () + ```"#]], + ); + } } diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 83173e1c6b8a..d717c46057f1 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -418,9 +418,9 @@ impl Analysis { pub fn hover( &self, config: &HoverConfig, - position: FilePosition, + range: FileRange, ) -> Cancellable>> { - 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. diff --git a/crates/rust-analyzer/src/caps.rs b/crates/rust-analyzer/src/caps.rs index 17d0ed1b6594..9469776db8af 100644 --- a/crates/rust-analyzer/src/caps.rs +++ b/crates/rust-analyzer/src/caps.rs @@ -118,6 +118,7 @@ pub fn server_capabilities(config: &Config) -> ServerCapabilities { "ssr": true, "onEnter": true, "parentModule": true, + "hoverRange": true, "runnables": { "kinds": [ "cargo" ], }, diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index 52b557f15688..967ef31e45dc 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs @@ -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> { 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 { diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs index f11ad396e702..d697ec44d155 100644 --- a/crates/rust-analyzer/src/lsp_ext.rs +++ b/crates/rust-analyzer/src/lsp_ext.rs @@ -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; 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)] diff --git a/crates/syntax/src/algo.rs b/crates/syntax/src/algo.rs index 8d7a77b507d8..91ab01ca4e46 100644 --- a/crates/syntax/src/algo.rs +++ b/crates/syntax/src/algo.rs @@ -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), diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md index ffc94b178a54..e617153a6c78 100644 --- a/docs/dev/lsp-extensions.md +++ b/docs/dev/lsp-extensions.md @@ -1,5 +1,5 @@