From c390e92fdd25ced46c589bfbff94e4b0bc4d9c38 Mon Sep 17 00:00:00 2001 From: imtsuki Date: Wed, 15 Jan 2020 01:02:01 +0800 Subject: [PATCH] Add inlay parameter name hints for function calls Signed-off-by: imtsuki --- .../ra_ide/src/display/function_signature.rs | 22 +++ crates/ra_ide/src/inlay_hints.rs | 141 +++++++++++++++++- .../ra_lsp_server/src/main_loop/handlers.rs | 1 + crates/ra_lsp_server/src/req.rs | 1 + editors/code/package.json | 2 +- editors/code/src/inlay_hints.ts | 51 +++++-- 6 files changed, 205 insertions(+), 13 deletions(-) diff --git a/crates/ra_ide/src/display/function_signature.rs b/crates/ra_ide/src/display/function_signature.rs index 324ad95526..b0eb1f1941 100644 --- a/crates/ra_ide/src/display/function_signature.rs +++ b/crates/ra_ide/src/display/function_signature.rs @@ -34,6 +34,8 @@ pub struct FunctionSignature { pub generic_parameters: Vec, /// Parameters of the function pub parameters: Vec, + /// Parameter names of the function + pub parameter_names: Vec, /// Optional return type pub ret_type: Option, /// Where predicates @@ -75,6 +77,7 @@ impl FunctionSignature { name: node.name().map(|n| n.text().to_string()), ret_type: node.name().map(|n| n.text().to_string()), parameters: params, + parameter_names: vec![], generic_parameters: generic_parameters(&node), where_predicates: where_predicates(&node), doc: None, @@ -114,6 +117,7 @@ impl FunctionSignature { name: Some(name), ret_type: None, parameters: params, + parameter_names: vec![], generic_parameters: vec![], where_predicates: vec![], doc: None, @@ -134,6 +138,7 @@ impl FunctionSignature { name: node.name().map(|n| n.text().to_string()), ret_type: None, parameters: params, + parameter_names: vec![], generic_parameters: vec![], where_predicates: vec![], doc: None, @@ -157,6 +162,22 @@ impl From<&'_ ast::FnDef> for FunctionSignature { res } + fn param_name_list(node: &ast::FnDef) -> Vec { + let mut res = vec![]; + if let Some(param_list) = node.param_list() { + if let Some(self_param) = param_list.self_param() { + res.push(self_param.syntax().text().to_string()) + } + + res.extend( + param_list + .params() + .map(|param| param.pat().unwrap().syntax().text().to_string()), + ); + } + res + } + FunctionSignature { kind: CallableKind::Function, visibility: node.visibility().map(|n| n.syntax().text().to_string()), @@ -166,6 +187,7 @@ impl From<&'_ ast::FnDef> for FunctionSignature { .and_then(|r| r.type_ref()) .map(|n| n.syntax().text().to_string()), parameters: param_list(node), + parameter_names: param_name_list(node), generic_parameters: generic_parameters(node), where_predicates: where_predicates(node), // docs are processed separately diff --git a/crates/ra_ide/src/inlay_hints.rs b/crates/ra_ide/src/inlay_hints.rs index 977aafc51f..83e4588c11 100644 --- a/crates/ra_ide/src/inlay_hints.rs +++ b/crates/ra_ide/src/inlay_hints.rs @@ -4,15 +4,16 @@ use hir::{HirDisplay, SourceAnalyzer}; use once_cell::unsync::Lazy; use ra_prof::profile; use ra_syntax::{ - ast::{self, AstNode, TypeAscriptionOwner}, + ast::{self, ArgListOwner, AstNode, TypeAscriptionOwner}, match_ast, SmolStr, SourceFile, SyntaxKind, SyntaxNode, TextRange, }; -use crate::{db::RootDatabase, FileId}; +use crate::{db::RootDatabase, FileId, FunctionSignature}; #[derive(Debug, PartialEq, Eq)] pub enum InlayKind { TypeHint, + ParameterHint, } #[derive(Debug)] @@ -87,10 +88,79 @@ fn get_inlay_hints( .collect(), ) }, + ast::CallExpr(it) => { + get_param_name_hints(db, &analyzer, ast::Expr::from(it)) + }, + ast::MethodCallExpr(it) => { + get_param_name_hints(db, &analyzer, ast::Expr::from(it)) + }, _ => None, } } } +fn get_param_name_hints( + db: &RootDatabase, + analyzer: &SourceAnalyzer, + expr: ast::Expr, +) -> Option> { + let args = match &expr { + ast::Expr::CallExpr(expr) => Some(expr.arg_list()?.args()), + ast::Expr::MethodCallExpr(expr) => Some(expr.arg_list()?.args()), + _ => None, + }?; + + let mut parameters = get_fn_signature(db, analyzer, &expr)?.parameter_names.into_iter(); + + if let ast::Expr::MethodCallExpr(_) = &expr { + parameters.next(); + }; + + let hints = parameters + .zip(args) + .filter_map(|(param, arg)| { + if arg.syntax().kind() == SyntaxKind::LITERAL { + Some((arg.syntax().text_range(), param)) + } else { + None + } + }) + .map(|(range, param_name)| InlayHint { + range, + kind: InlayKind::ParameterHint, + label: param_name.into(), + }) + .collect(); + + Some(hints) +} + +fn get_fn_signature( + db: &RootDatabase, + analyzer: &SourceAnalyzer, + expr: &ast::Expr, +) -> Option { + match expr { + ast::Expr::CallExpr(expr) => { + // FIXME: Type::as_callable is broken for closures + let callable_def = analyzer.type_of(db, &expr.expr()?)?.as_callable()?; + match callable_def { + hir::CallableDef::FunctionId(it) => { + let fn_def = it.into(); + Some(FunctionSignature::from_hir(db, fn_def)) + } + hir::CallableDef::StructId(it) => FunctionSignature::from_struct(db, it.into()), + hir::CallableDef::EnumVariantId(it) => { + FunctionSignature::from_enum_variant(db, it.into()) + } + } + } + ast::Expr::MethodCallExpr(expr) => { + let fn_def = analyzer.resolve_method_call(&expr)?; + Some(FunctionSignature::from_hir(db, fn_def)) + } + _ => None, + } +} fn get_pat_type_hints( db: &RootDatabase, @@ -605,4 +675,71 @@ fn main() { "### ); } + + #[test] + fn function_call_parameter_hint() { + let (analysis, file_id) = single_file( + r#" +struct Test {} + +impl Test { + fn method(&self, param: i32) -> i32 { + param * 2 + } +} + +fn test_func(foo: i32, bar: i32, msg: &str, _: i32, last: i32) -> i32 { + foo + bar +} + +fn main() { + let not_literal = 1; + let _: i32 = test_func(1, 2, "hello", 3, not_literal); + let t: Test = Test {}; + t.method(123); + Test::method(&t, 3456); +}"#, + ); + + assert_debug_snapshot!(analysis.inlay_hints(file_id, None).unwrap(), @r###" + [ + InlayHint { + range: [207; 218), + kind: TypeHint, + label: "i32", + }, + InlayHint { + range: [251; 252), + kind: ParameterHint, + label: "foo", + }, + InlayHint { + range: [254; 255), + kind: ParameterHint, + label: "bar", + }, + InlayHint { + range: [257; 264), + kind: ParameterHint, + label: "msg", + }, + InlayHint { + range: [266; 267), + kind: ParameterHint, + label: "_", + }, + InlayHint { + range: [322; 325), + kind: ParameterHint, + label: "param", + }, + InlayHint { + range: [349; 353), + kind: ParameterHint, + label: "param", + }, + ] + "### + ); + } } diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs index a592f0a126..a9a8538b75 100644 --- a/crates/ra_lsp_server/src/main_loop/handlers.rs +++ b/crates/ra_lsp_server/src/main_loop/handlers.rs @@ -952,6 +952,7 @@ pub fn handle_inlay_hints( range: api_type.range.conv_with(&line_index), kind: match api_type.kind { ra_ide::InlayKind::TypeHint => InlayKind::TypeHint, + ra_ide::InlayKind::ParameterHint => InlayKind::ParameterHint, }, }) .collect()) diff --git a/crates/ra_lsp_server/src/req.rs b/crates/ra_lsp_server/src/req.rs index 8098ff31d1..dc327f53d2 100644 --- a/crates/ra_lsp_server/src/req.rs +++ b/crates/ra_lsp_server/src/req.rs @@ -197,6 +197,7 @@ pub struct InlayHintsParams { #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum InlayKind { TypeHint, + ParameterHint, } #[derive(Debug, Deserialize, Serialize)] diff --git a/editors/code/package.json b/editors/code/package.json index 7c22d21d35..ed637d114a 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -228,7 +228,7 @@ "rust-analyzer.displayInlayHints": { "type": "boolean", "default": true, - "description": "Display additional type information in the editor" + "description": "Display additional type and parameter information in the editor" }, "rust-analyzer.maxInlayHintLength": { "type": "number", diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts index 078d18f0fb..c4206cf5b2 100644 --- a/editors/code/src/inlay_hints.ts +++ b/editors/code/src/inlay_hints.ts @@ -38,6 +38,12 @@ const typeHintDecorationType = vscode.window.createTextEditorDecorationType({ }, }); +const parameterHintDecorationType = vscode.window.createTextEditorDecorationType({ + before: { + color: new vscode.ThemeColor('rust_analyzer.inlayHint'), + } +}) + class HintsUpdater { private pending: Map = new Map(); private ctx: Ctx; @@ -55,7 +61,10 @@ class HintsUpdater { if (this.enabled) { await this.refresh(); } else { - this.allEditors.forEach(it => this.setDecorations(it, [])); + this.allEditors.forEach(it => { + this.setTypeDecorations(it, []); + this.setParameterDecorations(it, []); + }); } } @@ -68,15 +77,27 @@ class HintsUpdater { private async refreshEditor(editor: vscode.TextEditor): Promise { const newHints = await this.queryHints(editor.document.uri.toString()); if (newHints == null) return; - const newDecorations = newHints.map(hint => ({ - range: hint.range, - renderOptions: { - after: { - contentText: `: ${hint.label}`, + const newTypeDecorations = newHints.filter(hint => hint.kind === 'TypeHint') + .map(hint => ({ + range: hint.range, + renderOptions: { + after: { + contentText: `: ${hint.label}`, + }, }, - }, - })); - this.setDecorations(editor, newDecorations); + })); + this.setTypeDecorations(editor, newTypeDecorations); + + const newParameterDecorations = newHints.filter(hint => hint.kind === 'ParameterHint') + .map(hint => ({ + range: hint.range, + renderOptions: { + before: { + contentText: `${hint.label}: `, + }, + }, + })); + this.setParameterDecorations(editor, newParameterDecorations); } private get allEditors(): vscode.TextEditor[] { @@ -85,7 +106,7 @@ class HintsUpdater { ); } - private setDecorations( + private setTypeDecorations( editor: vscode.TextEditor, decorations: vscode.DecorationOptions[], ) { @@ -95,6 +116,16 @@ class HintsUpdater { ); } + private setParameterDecorations( + editor: vscode.TextEditor, + decorations: vscode.DecorationOptions[], + ) { + editor.setDecorations( + parameterHintDecorationType, + this.enabled ? decorations : [], + ); + } + private async queryHints(documentUri: string): Promise { let client = this.ctx.client; if (!client) return null;