Skip to content

Commit 6ad1a07

Browse files
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 <matthewjhooper94@gmail.com>
2 parents fae6271 + 7b35da0 commit 6ad1a07

File tree

11 files changed

+218
-9
lines changed

11 files changed

+218
-9
lines changed

crates/ra_ide/src/inlay_hints.rs

Lines changed: 171 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use ra_ide_db::RootDatabase;
55
use ra_prof::profile;
66
use ra_syntax::{
77
ast::{self, ArgListOwner, AstNode, TypeAscriptionOwner},
8-
match_ast, SmolStr, TextRange,
8+
match_ast, Direction, NodeOrToken, SmolStr, SyntaxKind, TextRange,
99
};
1010

1111
use crate::{FileId, FunctionSignature};
@@ -14,19 +14,21 @@ use crate::{FileId, FunctionSignature};
1414
pub struct InlayHintsOptions {
1515
pub type_hints: bool,
1616
pub parameter_hints: bool,
17+
pub chaining_hints: bool,
1718
pub max_length: Option<usize>,
1819
}
1920

2021
impl Default for InlayHintsOptions {
2122
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 }
2324
}
2425
}
2526

2627
#[derive(Clone, Debug, PartialEq, Eq)]
2728
pub enum InlayKind {
2829
TypeHint,
2930
ParameterHint,
31+
ChainingHint,
3032
}
3133

3234
#[derive(Debug)]
@@ -47,6 +49,10 @@ pub(crate) fn inlay_hints(
4749

4850
let mut res = Vec::new();
4951
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+
5056
match_ast! {
5157
match node {
5258
ast::CallExpr(it) => { get_param_name_hints(&mut res, &sema, options, ast::Expr::from(it)); },
@@ -59,6 +65,46 @@ pub(crate) fn inlay_hints(
5965
res
6066
}
6167

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+
62108
fn get_param_name_hints(
63109
acc: &mut Vec<InlayHint>,
64110
sema: &Semantics<RootDatabase>,
@@ -238,7 +284,7 @@ mod tests {
238284
let _x = foo(4, 4);
239285
}"#,
240286
);
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###"
242288
[
243289
InlayHint {
244290
range: [106; 107),
@@ -262,7 +308,7 @@ mod tests {
262308
let _x = foo(4, 4);
263309
}"#,
264310
);
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###"[]"###);
266312
}
267313

268314
#[test]
@@ -274,7 +320,7 @@ mod tests {
274320
let _x = foo(4, 4);
275321
}"#,
276322
);
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###"
278324
[
279325
InlayHint {
280326
range: [97; 99),
@@ -1052,4 +1098,124 @@ fn main() {
10521098
"###
10531099
);
10541100
}
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+
}
10551221
}

crates/rust-analyzer/src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ pub struct ServerConfig {
3434
pub inlay_hints_type: bool,
3535
#[serde(deserialize_with = "nullable_bool_true")]
3636
pub inlay_hints_parameter: bool,
37+
#[serde(deserialize_with = "nullable_bool_true")]
38+
pub inlay_hints_chaining: bool,
3739
pub inlay_hints_max_length: Option<usize>,
3840

3941
pub cargo_watch_enable: bool,
@@ -66,6 +68,7 @@ impl Default for ServerConfig {
6668
lru_capacity: None,
6769
inlay_hints_type: true,
6870
inlay_hints_parameter: true,
71+
inlay_hints_chaining: true,
6972
inlay_hints_max_length: None,
7073
cargo_watch_enable: true,
7174
cargo_watch_args: Vec::new(),

crates/rust-analyzer/src/conv.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ impl ConvWith<&LineIndex> for InlayHint {
332332
kind: match self.kind {
333333
InlayKind::ParameterHint => req::InlayKind::ParameterHint,
334334
InlayKind::TypeHint => req::InlayKind::TypeHint,
335+
InlayKind::ChainingHint => req::InlayKind::ChainingHint,
335336
},
336337
}
337338
}

crates/rust-analyzer/src/main_loop.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ pub fn main_loop(
183183
inlay_hints: InlayHintsOptions {
184184
type_hints: config.inlay_hints_type,
185185
parameter_hints: config.inlay_hints_parameter,
186+
chaining_hints: config.inlay_hints_chaining,
186187
max_length: config.inlay_hints_max_length,
187188
},
188189
cargo_watch: CheckOptions {

crates/rust-analyzer/src/req.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ pub struct InlayHintsParams {
200200
pub enum InlayKind {
201201
TypeHint,
202202
ParameterHint,
203+
ChainingHint,
203204
}
204205

205206
#[derive(Debug, Deserialize, Serialize)]

docs/user/features.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,15 @@ These contain extended information on the hovered language item.
185185
Two types of inlay hints are displayed currently:
186186

187187
* type hints, displaying the minimal information on the type of the expression (if the information is available)
188+
* method chaining hints, type information for multi-line method chains
188189
* parameter name hints, displaying the names of the parameters in the corresponding methods
189190

190191
#### VS Code
191192

192193
In VS Code, the following settings can be used to configure the inlay hints:
193194

194195
* `rust-analyzer.inlayHints.typeHints` - enable hints for inferred types.
196+
* `rust-analyzer.inlayHints.chainingHints` - enable hints for inferred types on method chains.
195197
* `rust-analyzer.inlayHints.parameterHints` - enable hints for function parameters.
196198
* `rust-analyzer.inlayHints.maxLength` — shortens the hints if their length exceeds the value specified. If no value is specified (`null`), no shortening is applied.
197199

editors/code/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,11 @@
333333
"default": true,
334334
"description": "Whether to show inlay type hints"
335335
},
336+
"rust-analyzer.inlayHints.chainingHints": {
337+
"type": "boolean",
338+
"default": true,
339+
"description": "Whether to show inlay type hints for method chains"
340+
},
336341
"rust-analyzer.inlayHints.parameterHints": {
337342
"type": "boolean",
338343
"default": true,

editors/code/src/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export async function createClient(config: Config, serverPath: string): Promise<
3232

3333
inlayHintsType: config.inlayHints.typeHints,
3434
inlayHintsParameter: config.inlayHints.parameterHints,
35+
inlayHintsChaining: config.inlayHints.chainingHints,
3536
inlayHintsMaxLength: config.inlayHints.maxLength,
3637

3738
cargoWatchEnable: cargoWatchOpts.enable,

editors/code/src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export class Config {
8888
return {
8989
typeHints: this.cfg.get<boolean>("inlayHints.typeHints")!,
9090
parameterHints: this.cfg.get<boolean>("inlayHints.parameterHints")!,
91+
chainingHints: this.cfg.get<boolean>("inlayHints.chainingHints")!,
9192
maxLength: this.cfg.get<null | number>("inlayHints.maxLength")!,
9293
};
9394
}

0 commit comments

Comments
 (0)