diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-03-24 23:28:57 +0000 |
---|---|---|
committer | GitHub <[email protected]> | 2020-03-24 23:28:57 +0000 |
commit | 6ad1a0711631d8017791a6dfe85bbe205d6c7414 (patch) | |
tree | ce2d6448ff8770c8fe73086eefb85dab38d298d8 /crates/ra_ide | |
parent | fae627174aecae0b4f4d2c087a856eda1a97a1ac (diff) | |
parent | 7b35da04bf56a5461321a6dca515dcd29f44b57f (diff) |
Merge #3710
3710: Inlay hints for method chaining pattern r=matklad a=M-J-Hooper
This PR adds inlay hints on method call chains:
![image](https://user-images.githubusercontent.com/13765376/77472008-8dc2a880-6e13-11ea-9c18-2c2e2b809799.png)
It is not only explicit `MethodCall`s where this can be helpful. The heuristic used here is that whenever any expression is followed by a new line and then a dot, it resembles a call chain and type information can be #useful.
Changes:
- A new `InlayKind` for chaining.
- New option for disabling this type of hints.
- Tree traversal rules for identifying the chaining hints.
- VSCode decorators in the extension layer (and associated types).
Notes:
- IntelliJ has additional rules and configuration on this topic. Eg. minimum length of chain to start displaying hints and only displaying distinct types in the chain.
- I am checking for chaining on every `ast::Expr` in the tree; Are there performance concerns there?
This is my first contribution (to RA and to Rust in general) so would appreciate any feedback.
The only issue I can find the references this feature is #2741.
Co-authored-by: Matt Hooper <[email protected]>
Diffstat (limited to 'crates/ra_ide')
-rw-r--r-- | crates/ra_ide/src/inlay_hints.rs | 176 |
1 files changed, 171 insertions, 5 deletions
diff --git a/crates/ra_ide/src/inlay_hints.rs b/crates/ra_ide/src/inlay_hints.rs index ecd615cf4..f4f0751c0 100644 --- a/crates/ra_ide/src/inlay_hints.rs +++ b/crates/ra_ide/src/inlay_hints.rs | |||
@@ -5,7 +5,7 @@ use ra_ide_db::RootDatabase; | |||
5 | use ra_prof::profile; | 5 | use ra_prof::profile; |
6 | use ra_syntax::{ | 6 | use ra_syntax::{ |
7 | ast::{self, ArgListOwner, AstNode, TypeAscriptionOwner}, | 7 | ast::{self, ArgListOwner, AstNode, TypeAscriptionOwner}, |
8 | match_ast, SmolStr, TextRange, | 8 | match_ast, Direction, NodeOrToken, SmolStr, SyntaxKind, TextRange, |
9 | }; | 9 | }; |
10 | 10 | ||
11 | use crate::{FileId, FunctionSignature}; | 11 | use crate::{FileId, FunctionSignature}; |
@@ -14,12 +14,13 @@ use crate::{FileId, FunctionSignature}; | |||
14 | pub struct InlayHintsOptions { | 14 | pub struct InlayHintsOptions { |
15 | pub type_hints: bool, | 15 | pub type_hints: bool, |
16 | pub parameter_hints: bool, | 16 | pub parameter_hints: bool, |
17 | pub chaining_hints: bool, | ||
17 | pub max_length: Option<usize>, | 18 | pub max_length: Option<usize>, |
18 | } | 19 | } |
19 | 20 | ||
20 | impl Default for InlayHintsOptions { | 21 | impl Default for InlayHintsOptions { |
21 | fn default() -> Self { | 22 | fn default() -> Self { |
22 | Self { type_hints: true, parameter_hints: true, max_length: None } | 23 | Self { type_hints: true, parameter_hints: true, chaining_hints: true, max_length: None } |
23 | } | 24 | } |
24 | } | 25 | } |
25 | 26 | ||
@@ -27,6 +28,7 @@ impl Default for InlayHintsOptions { | |||
27 | pub enum InlayKind { | 28 | pub enum InlayKind { |
28 | TypeHint, | 29 | TypeHint, |
29 | ParameterHint, | 30 | ParameterHint, |
31 | ChainingHint, | ||
30 | } | 32 | } |
31 | 33 | ||
32 | #[derive(Debug)] | 34 | #[derive(Debug)] |
@@ -47,6 +49,10 @@ pub(crate) fn inlay_hints( | |||
47 | 49 | ||
48 | let mut res = Vec::new(); | 50 | let mut res = Vec::new(); |
49 | for node in file.syntax().descendants() { | 51 | for node in file.syntax().descendants() { |
52 | if let Some(expr) = ast::Expr::cast(node.clone()) { | ||
53 | get_chaining_hints(&mut res, &sema, options, expr); | ||
54 | } | ||
55 | |||
50 | match_ast! { | 56 | match_ast! { |
51 | match node { | 57 | match node { |
52 | ast::CallExpr(it) => { get_param_name_hints(&mut res, &sema, options, ast::Expr::from(it)); }, | 58 | ast::CallExpr(it) => { get_param_name_hints(&mut res, &sema, options, ast::Expr::from(it)); }, |
@@ -59,6 +65,46 @@ pub(crate) fn inlay_hints( | |||
59 | res | 65 | res |
60 | } | 66 | } |
61 | 67 | ||
68 | fn get_chaining_hints( | ||
69 | acc: &mut Vec<InlayHint>, | ||
70 | sema: &Semantics<RootDatabase>, | ||
71 | options: &InlayHintsOptions, | ||
72 | expr: ast::Expr, | ||
73 | ) -> Option<()> { | ||
74 | if !options.chaining_hints { | ||
75 | return None; | ||
76 | } | ||
77 | |||
78 | let ty = sema.type_of_expr(&expr)?; | ||
79 | if ty.is_unknown() { | ||
80 | return None; | ||
81 | } | ||
82 | |||
83 | let mut tokens = expr | ||
84 | .syntax() | ||
85 | .siblings_with_tokens(Direction::Next) | ||
86 | .filter_map(NodeOrToken::into_token) | ||
87 | .filter(|t| match t.kind() { | ||
88 | SyntaxKind::WHITESPACE if !t.text().contains('\n') => false, | ||
89 | SyntaxKind::COMMENT => false, | ||
90 | _ => true, | ||
91 | }); | ||
92 | |||
93 | // Chaining can be defined as an expression whose next sibling tokens are newline and dot | ||
94 | // Ignoring extra whitespace and comments | ||
95 | let next = tokens.next()?.kind(); | ||
96 | let next_next = tokens.next()?.kind(); | ||
97 | if next == SyntaxKind::WHITESPACE && next_next == SyntaxKind::DOT { | ||
98 | let label = ty.display_truncated(sema.db, options.max_length).to_string(); | ||
99 | acc.push(InlayHint { | ||
100 | range: expr.syntax().text_range(), | ||
101 | kind: InlayKind::ChainingHint, | ||
102 | label: label.into(), | ||
103 | }); | ||
104 | } | ||
105 | Some(()) | ||
106 | } | ||
107 | |||
62 | fn get_param_name_hints( | 108 | fn get_param_name_hints( |
63 | acc: &mut Vec<InlayHint>, | 109 | acc: &mut Vec<InlayHint>, |
64 | sema: &Semantics<RootDatabase>, | 110 | sema: &Semantics<RootDatabase>, |
@@ -238,7 +284,7 @@ mod tests { | |||
238 | let _x = foo(4, 4); | 284 | let _x = foo(4, 4); |
239 | }"#, | 285 | }"#, |
240 | ); | 286 | ); |
241 | assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ parameter_hints: true, type_hints: false, max_length: None}).unwrap(), @r###" | 287 | assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ parameter_hints: true, type_hints: false, chaining_hints: false, max_length: None}).unwrap(), @r###" |
242 | [ | 288 | [ |
243 | InlayHint { | 289 | InlayHint { |
244 | range: [106; 107), | 290 | range: [106; 107), |
@@ -262,7 +308,7 @@ mod tests { | |||
262 | let _x = foo(4, 4); | 308 | let _x = foo(4, 4); |
263 | }"#, | 309 | }"#, |
264 | ); | 310 | ); |
265 | assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ type_hints: false, parameter_hints: false, max_length: None}).unwrap(), @r###"[]"###); | 311 | assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ type_hints: false, parameter_hints: false, chaining_hints: false, max_length: None}).unwrap(), @r###"[]"###); |
266 | } | 312 | } |
267 | 313 | ||
268 | #[test] | 314 | #[test] |
@@ -274,7 +320,7 @@ mod tests { | |||
274 | let _x = foo(4, 4); | 320 | let _x = foo(4, 4); |
275 | }"#, | 321 | }"#, |
276 | ); | 322 | ); |
277 | assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ type_hints: true, parameter_hints: false, max_length: None}).unwrap(), @r###" | 323 | assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ type_hints: true, parameter_hints: false, chaining_hints: false, max_length: None}).unwrap(), @r###" |
278 | [ | 324 | [ |
279 | InlayHint { | 325 | InlayHint { |
280 | range: [97; 99), | 326 | range: [97; 99), |
@@ -1052,4 +1098,124 @@ fn main() { | |||
1052 | "### | 1098 | "### |
1053 | ); | 1099 | ); |
1054 | } | 1100 | } |
1101 | |||
1102 | #[test] | ||
1103 | fn chaining_hints_ignore_comments() { | ||
1104 | let (analysis, file_id) = single_file( | ||
1105 | r#" | ||
1106 | struct A(B); | ||
1107 | impl A { fn into_b(self) -> B { self.0 } } | ||
1108 | struct B(C); | ||
1109 | impl B { fn into_c(self) -> C { self.0 } } | ||
1110 | struct C; | ||
1111 | |||
1112 | fn main() { | ||
1113 | let c = A(B(C)) | ||
1114 | .into_b() // This is a comment | ||
1115 | .into_c(); | ||
1116 | }"#, | ||
1117 | ); | ||
1118 | assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ parameter_hints: false, type_hints: false, chaining_hints: true, max_length: None}).unwrap(), @r###" | ||
1119 | [ | ||
1120 | InlayHint { | ||
1121 | range: [232; 269), | ||
1122 | kind: ChainingHint, | ||
1123 | label: "B", | ||
1124 | }, | ||
1125 | InlayHint { | ||
1126 | range: [232; 239), | ||
1127 | kind: ChainingHint, | ||
1128 | label: "A", | ||
1129 | }, | ||
1130 | ]"###); | ||
1131 | } | ||
1132 | |||
1133 | #[test] | ||
1134 | fn chaining_hints_without_newlines() { | ||
1135 | let (analysis, file_id) = single_file( | ||
1136 | r#" | ||
1137 | struct A(B); | ||
1138 | impl A { fn into_b(self) -> B { self.0 } } | ||
1139 | struct B(C); | ||
1140 | impl B { fn into_c(self) -> C { self.0 } } | ||
1141 | struct C; | ||
1142 | |||
1143 | fn main() { | ||
1144 | let c = A(B(C)).into_b().into_c(); | ||
1145 | }"#, | ||
1146 | ); | ||
1147 | assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ parameter_hints: false, type_hints: false, chaining_hints: true, max_length: None}).unwrap(), @r###"[]"###); | ||
1148 | } | ||
1149 | |||
1150 | #[test] | ||
1151 | fn struct_access_chaining_hints() { | ||
1152 | let (analysis, file_id) = single_file( | ||
1153 | r#" | ||
1154 | struct A { pub b: B } | ||
1155 | struct B { pub c: C } | ||
1156 | struct C(pub bool); | ||
1157 | |||
1158 | fn main() { | ||
1159 | let x = A { b: B { c: C(true) } } | ||
1160 | .b | ||
1161 | .c | ||
1162 | .0; | ||
1163 | }"#, | ||
1164 | ); | ||
1165 | assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ parameter_hints: false, type_hints: false, chaining_hints: true, max_length: None}).unwrap(), @r###" | ||
1166 | [ | ||
1167 | InlayHint { | ||
1168 | range: [150; 221), | ||
1169 | kind: ChainingHint, | ||
1170 | label: "C", | ||
1171 | }, | ||
1172 | InlayHint { | ||
1173 | range: [150; 198), | ||
1174 | kind: ChainingHint, | ||
1175 | label: "B", | ||
1176 | }, | ||
1177 | InlayHint { | ||
1178 | range: [150; 175), | ||
1179 | kind: ChainingHint, | ||
1180 | label: "A", | ||
1181 | }, | ||
1182 | ]"###); | ||
1183 | } | ||
1184 | |||
1185 | #[test] | ||
1186 | fn generic_chaining_hints() { | ||
1187 | let (analysis, file_id) = single_file( | ||
1188 | r#" | ||
1189 | struct A<T>(T); | ||
1190 | struct B<T>(T); | ||
1191 | struct C<T>(T); | ||
1192 | struct X<T,R>(T, R); | ||
1193 | |||
1194 | impl<T> A<T> { | ||
1195 | fn new(t: T) -> Self { A(t) } | ||
1196 | fn into_b(self) -> B<T> { B(self.0) } | ||
1197 | } | ||
1198 | impl<T> B<T> { | ||
1199 | fn into_c(self) -> C<T> { C(self.0) } | ||
1200 | } | ||
1201 | fn main() { | ||
1202 | let c = A::new(X(42, true)) | ||
1203 | .into_b() | ||
1204 | .into_c(); | ||
1205 | }"#, | ||
1206 | ); | ||
1207 | assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsOptions{ parameter_hints: false, type_hints: false, chaining_hints: true, max_length: None}).unwrap(), @r###" | ||
1208 | [ | ||
1209 | InlayHint { | ||
1210 | range: [403; 452), | ||
1211 | kind: ChainingHint, | ||
1212 | label: "B<X<i32, bool>>", | ||
1213 | }, | ||
1214 | InlayHint { | ||
1215 | range: [403; 422), | ||
1216 | kind: ChainingHint, | ||
1217 | label: "A<X<i32, bool>>", | ||
1218 | }, | ||
1219 | ]"###); | ||
1220 | } | ||
1055 | } | 1221 | } |