diff --git a/crates/ra_ide_api/src/inlay_hints.rs b/crates/ra_ide_api/src/inlay_hints.rs new file mode 100644 index 00000000000..174662beb57 --- /dev/null +++ b/crates/ra_ide_api/src/inlay_hints.rs @@ -0,0 +1,180 @@ +use crate::{db::RootDatabase, FileId}; +use hir::{HirDisplay, Ty}; +use ra_syntax::ast::Pat; +use ra_syntax::{ + algo::visit::{visitor, Visitor}, + ast::{self, PatKind, TypeAscriptionOwner}, + AstNode, SmolStr, SourceFile, SyntaxNode, TextRange, +}; + +#[derive(Debug, PartialEq, Eq)] +pub enum InlayKind { + LetBindingType, + ClosureParameterType, +} + +#[derive(Debug)] +pub struct InlayHint { + pub range: TextRange, + pub kind: InlayKind, + pub label: SmolStr, +} + +pub(crate) fn inlay_hints(db: &RootDatabase, file_id: FileId, file: &SourceFile) -> Vec { + file.syntax() + .descendants() + .map(|node| get_inlay_hints(db, file_id, &node).unwrap_or_default()) + .flatten() + .collect() +} + +fn get_inlay_hints( + db: &RootDatabase, + file_id: FileId, + node: &SyntaxNode, +) -> Option> { + visitor() + .visit(|let_statement: ast::LetStmt| { + let let_syntax = let_statement.syntax(); + + if let_statement.ascribed_type().is_some() { + return None; + } + + let let_pat = let_statement.pat()?; + let inlay_type_string = get_node_displayable_type(db, file_id, let_syntax, &let_pat)? + .display(db) + .to_string() + .into(); + + let pat_range = match let_pat.kind() { + PatKind::BindPat(bind_pat) => bind_pat.syntax().text_range(), + PatKind::TuplePat(tuple_pat) => tuple_pat.syntax().text_range(), + _ => return None, + }; + + Some(vec![InlayHint { + range: pat_range, + kind: InlayKind::LetBindingType, + label: inlay_type_string, + }]) + }) + .visit(|closure_parameter: ast::LambdaExpr| match closure_parameter.param_list() { + Some(param_list) => Some( + param_list + .params() + .filter(|closure_param| closure_param.ascribed_type().is_none()) + .filter_map(|closure_param| { + let closure_param_syntax = closure_param.syntax(); + let inlay_type_string = get_node_displayable_type( + db, + file_id, + closure_param_syntax, + &closure_param.pat()?, + )? + .display(db) + .to_string() + .into(); + + Some(InlayHint { + range: closure_param_syntax.text_range(), + kind: InlayKind::ClosureParameterType, + label: inlay_type_string, + }) + }) + .collect(), + ), + None => None, + }) + .accept(&node)? +} + +fn get_node_displayable_type( + db: &RootDatabase, + file_id: FileId, + node_syntax: &SyntaxNode, + node_pat: &Pat, +) -> Option { + let analyzer = hir::SourceAnalyzer::new(db, file_id, node_syntax, None); + analyzer.type_of_pat(db, node_pat).and_then(|resolved_type| { + if let Ty::Apply(_) = resolved_type { + Some(resolved_type) + } else { + None + } + }) +} + +#[cfg(test)] +mod tests { + use crate::mock_analysis::single_file; + use insta::assert_debug_snapshot_matches; + + #[test] + fn test_inlay_hints() { + let (analysis, file_id) = single_file( + r#" +struct OuterStruct {} + +fn main() { + struct InnerStruct {} + + let test = 54; + let test = InnerStruct {}; + let test = OuterStruct {}; + let test = vec![222]; + let mut test = Vec::new(); + test.push(333); + let test = test.into_iter().map(|i| i * i).collect::>(); + let mut test = 33; + let _ = 22; + let test: Vec<_> = (0..3).collect(); + + let _ = (0..23).map(|i: u32| { + let i_squared = i * i; + i_squared + }); + + let test: i32 = 33; + + let (x, c) = (42, 'a'); + let test = (42, 'a'); +} +"#, + ); + + assert_debug_snapshot_matches!(analysis.inlay_hints(file_id).unwrap(), @r#"[ + InlayHint { + range: [71; 75), + kind: LetBindingType, + label: "i32", + }, + InlayHint { + range: [121; 125), + kind: LetBindingType, + label: "OuterStruct", + }, + InlayHint { + range: [297; 305), + kind: LetBindingType, + label: "i32", + }, + InlayHint { + range: [417; 426), + kind: LetBindingType, + label: "u32", + }, + InlayHint { + range: [496; 502), + kind: LetBindingType, + label: "(i32, char)", + }, + InlayHint { + range: [524; 528), + kind: LetBindingType, + label: "(i32, char)", + }, +]"# + ); + } +} diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs index c54d574bcc5..16ffb03ce92 100644 --- a/crates/ra_ide_api/src/lib.rs +++ b/crates/ra_ide_api/src/lib.rs @@ -38,6 +38,7 @@ mod join_lines; mod typing; mod matching_brace; mod display; +mod inlay_hints; #[cfg(test)] mod marks; @@ -64,6 +65,7 @@ pub use crate::{ display::{file_structure, FunctionSignature, NavigationTarget, StructureNode}, folding_ranges::{Fold, FoldKind}, hover::HoverResult, + inlay_hints::{InlayHint, InlayKind}, line_index::{LineCol, LineIndex}, line_index_utils::translate_offset_with_edit, references::ReferenceSearchResult, @@ -396,6 +398,11 @@ impl Analysis { file_structure(&parse.tree()) } + /// Returns a list of the places in the file where type hints can be displayed. + pub fn inlay_hints(&self, file_id: FileId) -> Cancelable> { + self.with_db(|db| inlay_hints::inlay_hints(db, file_id, &db.parse(file_id).tree())) + } + /// Returns the set of folding ranges. pub fn folding_ranges(&self, file_id: FileId) -> Vec { let parse = self.db.parse(file_id); diff --git a/crates/ra_lsp_server/src/main_loop.rs b/crates/ra_lsp_server/src/main_loop.rs index 668d2fd729d..8e830c8b858 100644 --- a/crates/ra_lsp_server/src/main_loop.rs +++ b/crates/ra_lsp_server/src/main_loop.rs @@ -362,6 +362,7 @@ fn on_request( .on::(handlers::handle_references)? .on::(handlers::handle_formatting)? .on::(handlers::handle_document_highlight)? + .on::(handlers::handle_inlay_hints)? .finish(); Ok(()) } diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs index 68865b75550..5bf950a5378 100644 --- a/crates/ra_lsp_server/src/main_loop/handlers.rs +++ b/crates/ra_lsp_server/src/main_loop/handlers.rs @@ -21,7 +21,7 @@ use url_serde::Ser; use crate::{ cargo_target_spec::{runnable_args, CargoTargetSpec}, conv::{to_location, Conv, ConvWith, MapConvWith, TryConvWith, TryConvWithToVec}, - req::{self, Decoration}, + req::{self, Decoration, InlayHint, InlayHintsParams, InlayKind}, world::WorldSnapshot, LspError, Result, }; @@ -874,3 +874,24 @@ fn to_diagnostic_severity(severity: Severity) -> DiagnosticSeverity { WeakWarning => DiagnosticSeverity::Hint, } } + +pub fn handle_inlay_hints( + world: WorldSnapshot, + params: InlayHintsParams, +) -> Result> { + let file_id = params.text_document.try_conv_with(&world)?; + let analysis = world.analysis(); + let line_index = analysis.file_line_index(file_id); + Ok(analysis + .inlay_hints(file_id)? + .into_iter() + .map(|api_type| InlayHint { + label: api_type.label.to_string(), + range: api_type.range.conv_with(&line_index), + kind: match api_type.kind { + ra_ide_api::InlayKind::LetBindingType => InlayKind::LetBindingType, + ra_ide_api::InlayKind::ClosureParameterType => InlayKind::ClosureParameterType, + }, + }) + .collect()) +} diff --git a/crates/ra_lsp_server/src/req.rs b/crates/ra_lsp_server/src/req.rs index 8d39b04a748..916185f99d1 100644 --- a/crates/ra_lsp_server/src/req.rs +++ b/crates/ra_lsp_server/src/req.rs @@ -196,3 +196,30 @@ pub struct SourceChange { pub workspace_edit: WorkspaceEdit, pub cursor_position: Option, } + +pub enum InlayHints {} + +impl Request for InlayHints { + type Params = InlayHintsParams; + type Result = Vec; + const METHOD: &'static str = "rust-analyzer/inlayHints"; +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintsParams { + pub text_document: TextDocumentIdentifier, +} + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +pub enum InlayKind { + LetBindingType, + ClosureParameterType, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct InlayHint { + pub range: Range, + pub kind: InlayKind, + pub label: String, +}