Skip to content

Commit c978d4b

Browse files
committed
Implement text edits for inlay hints
1 parent fcbc250 commit c978d4b

File tree

4 files changed

+262
-17
lines changed

4 files changed

+262
-17
lines changed

crates/ide/src/inlay_hints.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use smallvec::{smallvec, SmallVec};
1414
use stdx::never;
1515
use syntax::{
1616
ast::{self, AstNode},
17-
match_ast, NodeOrToken, SyntaxNode, TextRange,
17+
match_ast, NodeOrToken, SyntaxNode, TextRange, TextSize,
1818
};
1919
use text_edit::TextEdit;
2020

@@ -359,6 +359,23 @@ fn label_of_ty(
359359
Some(r)
360360
}
361361

362+
fn ty_to_text_edit(
363+
sema: &Semantics<'_, RootDatabase>,
364+
node_for_hint: &SyntaxNode,
365+
ty: &hir::Type,
366+
offset_to_insert: TextSize,
367+
prefix: String,
368+
) -> Option<TextEdit> {
369+
let scope = sema.scope(node_for_hint)?;
370+
// FIXME: Limit the length and bail out on excess somehow?
371+
let rendered = ty.display_source_code(scope.db, scope.module().into(), false).ok()?;
372+
373+
let mut builder = TextEdit::builder();
374+
builder.insert(offset_to_insert, prefix);
375+
builder.insert(offset_to_insert, rendered);
376+
Some(builder.finish())
377+
}
378+
362379
// Feature: Inlay Hints
363380
//
364381
// rust-analyzer shows additional information inline with the source code.
@@ -566,6 +583,37 @@ mod tests {
566583
expect.assert_debug_eq(&inlay_hints)
567584
}
568585

586+
/// Computes inlay hints for the fixture, applies all the provided text edits and then runs
587+
/// expect test.
588+
#[track_caller]
589+
pub(super) fn check_edit(config: InlayHintsConfig, ra_fixture: &str, expect: Expect) {
590+
let (analysis, file_id) = fixture::file(ra_fixture);
591+
let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
592+
593+
let edits = inlay_hints
594+
.into_iter()
595+
.filter_map(|hint| hint.text_edit)
596+
.reduce(|mut acc, next| {
597+
acc.union(next).expect("merging text edits failed");
598+
acc
599+
})
600+
.expect("no edit returned");
601+
602+
let mut actual = analysis.file_text(file_id).unwrap().to_string();
603+
edits.apply(&mut actual);
604+
expect.assert_eq(&actual);
605+
}
606+
607+
#[track_caller]
608+
pub(super) fn check_no_edit(config: InlayHintsConfig, ra_fixture: &str) {
609+
let (analysis, file_id) = fixture::file(ra_fixture);
610+
let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
611+
612+
let edits: Vec<_> = inlay_hints.into_iter().filter_map(|hint| hint.text_edit).collect();
613+
614+
assert!(edits.is_empty(), "unexpected edits: {edits:?}");
615+
}
616+
569617
#[test]
570618
fn hints_disabled() {
571619
check_with_config(

crates/ide/src/inlay_hints/bind_pat.rs

Lines changed: 181 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use syntax::{
1313
};
1414

1515
use crate::{
16-
inlay_hints::{closure_has_block_body, label_of_ty},
16+
inlay_hints::{closure_has_block_body, label_of_ty, ty_to_text_edit},
1717
InlayHint, InlayHintsConfig, InlayKind,
1818
};
1919

@@ -36,22 +36,39 @@ pub(super) fn hints(
3636
return None;
3737
}
3838

39-
let label = label_of_ty(famous_defs, config, ty)?;
39+
let label = label_of_ty(famous_defs, config, ty.clone())?;
4040

4141
if config.hide_named_constructor_hints
4242
&& is_named_constructor(sema, pat, &label.to_string()).is_some()
4343
{
4444
return None;
4545
}
4646

47+
let type_annotation_is_valid = desc_pat
48+
.syntax()
49+
.parent()
50+
.map(|it| ast::LetStmt::can_cast(it.kind()) || ast::Param::can_cast(it.kind()))
51+
.unwrap_or(false);
52+
let text_edit = if type_annotation_is_valid {
53+
ty_to_text_edit(
54+
sema,
55+
desc_pat.syntax(),
56+
&ty,
57+
pat.syntax().text_range().end(),
58+
String::from(": "),
59+
)
60+
} else {
61+
None
62+
};
63+
4764
acc.push(InlayHint {
4865
range: match pat.name() {
4966
Some(name) => name.syntax().text_range(),
5067
None => pat.syntax().text_range(),
5168
},
5269
kind: InlayKind::Type,
5370
label,
54-
text_edit: None,
71+
text_edit,
5572
});
5673

5774
Some(())
@@ -178,14 +195,16 @@ fn pat_is_enum_variant(db: &RootDatabase, bind_pat: &ast::IdentPat, pat_ty: &hir
178195
mod tests {
179196
// This module also contains tests for super::closure_ret
180197

198+
use expect_test::expect;
181199
use hir::ClosureStyle;
182200
use syntax::{TextRange, TextSize};
183201
use test_utils::extract_annotations;
184202

185-
use crate::{fixture, inlay_hints::InlayHintsConfig};
203+
use crate::{fixture, inlay_hints::InlayHintsConfig, ClosureReturnTypeHints};
186204

187-
use crate::inlay_hints::tests::{check, check_with_config, DISABLED_CONFIG, TEST_CONFIG};
188-
use crate::ClosureReturnTypeHints;
205+
use crate::inlay_hints::tests::{
206+
check, check_edit, check_no_edit, check_with_config, DISABLED_CONFIG, TEST_CONFIG,
207+
};
189208

190209
#[track_caller]
191210
fn check_types(ra_fixture: &str) {
@@ -1014,4 +1033,160 @@ fn main() {
10141033
}"#,
10151034
);
10161035
}
1036+
1037+
#[test]
1038+
fn edit_for_let_stmt() {
1039+
check_edit(
1040+
TEST_CONFIG,
1041+
r#"
1042+
struct S<T>(T);
1043+
fn test<F>(v: S<(S<i32>, S<()>)>, f: F) {
1044+
let a = v;
1045+
let S((b, c)) = v;
1046+
let a @ S((b, c)) = v;
1047+
let a = f;
1048+
}
1049+
"#,
1050+
expect![[r#"
1051+
struct S<T>(T);
1052+
fn test<F>(v: S<(S<i32>, S<()>)>, f: F) {
1053+
let a: S<(S<i32>, S<()>)> = v;
1054+
let S((b, c)) = v;
1055+
let a @ S((b, c)): S<(S<i32>, S<()>)> = v;
1056+
let a: F = f;
1057+
}
1058+
"#]],
1059+
);
1060+
}
1061+
1062+
#[test]
1063+
fn edit_for_closure_param() {
1064+
check_edit(
1065+
TEST_CONFIG,
1066+
r#"
1067+
fn test<T>(t: T) {
1068+
let f = |a, b, c| {};
1069+
let result = f(42, "", t);
1070+
}
1071+
"#,
1072+
expect![[r#"
1073+
fn test<T>(t: T) {
1074+
let f = |a: i32, b: &str, c: T| {};
1075+
let result: () = f(42, "", t);
1076+
}
1077+
"#]],
1078+
);
1079+
}
1080+
1081+
#[test]
1082+
fn edit_for_closure_ret() {
1083+
check_edit(
1084+
TEST_CONFIG,
1085+
r#"
1086+
struct S<T>(T);
1087+
fn test() {
1088+
let f = || { 3 };
1089+
let f = |a: S<usize>| { S(a) };
1090+
}
1091+
"#,
1092+
expect![[r#"
1093+
struct S<T>(T);
1094+
fn test() {
1095+
let f = || -> i32 { 3 };
1096+
let f = |a: S<usize>| -> S<S<usize>> { S(a) };
1097+
}
1098+
"#]],
1099+
);
1100+
}
1101+
1102+
#[test]
1103+
fn edit_prefixes_paths() {
1104+
check_edit(
1105+
TEST_CONFIG,
1106+
r#"
1107+
pub struct S<T>(T);
1108+
mod middle {
1109+
pub struct S<T, U>(T, U);
1110+
pub fn make() -> S<inner::S<i64>, super::S<usize>> { loop {} }
1111+
1112+
mod inner {
1113+
pub struct S<T>(T);
1114+
}
1115+
1116+
fn test() {
1117+
let a = make();
1118+
}
1119+
}
1120+
"#,
1121+
expect![[r#"
1122+
pub struct S<T>(T);
1123+
mod middle {
1124+
pub struct S<T, U>(T, U);
1125+
pub fn make() -> S<inner::S<i64>, super::S<usize>> { loop {} }
1126+
1127+
mod inner {
1128+
pub struct S<T>(T);
1129+
}
1130+
1131+
fn test() {
1132+
let a: S<inner::S<i64>, crate::S<usize>> = make();
1133+
}
1134+
}
1135+
"#]],
1136+
);
1137+
}
1138+
1139+
#[test]
1140+
fn no_edit_for_top_pat_where_type_annotation_is_invalid() {
1141+
check_no_edit(
1142+
TEST_CONFIG,
1143+
r#"
1144+
fn test() {
1145+
if let a = 42 {}
1146+
while let a = 42 {}
1147+
match 42 {
1148+
a => (),
1149+
}
1150+
}
1151+
"#,
1152+
)
1153+
}
1154+
1155+
#[test]
1156+
fn no_edit_for_opaque_type() {
1157+
check_no_edit(
1158+
TEST_CONFIG,
1159+
r#"
1160+
trait Trait {}
1161+
struct S<T>(T);
1162+
fn foo() -> impl Trait {}
1163+
fn bar() -> S<impl Trait> {}
1164+
fn test() {
1165+
let a = foo();
1166+
let a = bar();
1167+
let f = || { foo() };
1168+
let f = || { bar() };
1169+
}
1170+
"#,
1171+
);
1172+
}
1173+
1174+
#[test]
1175+
fn no_edit_for_closure_return_without_body_block() {
1176+
// We can lift this limitation; see FIXME in closure_ret module.
1177+
let config = InlayHintsConfig {
1178+
closure_return_type_hints: ClosureReturnTypeHints::Always,
1179+
..TEST_CONFIG
1180+
};
1181+
check_no_edit(
1182+
config,
1183+
r#"
1184+
struct S<T>(T);
1185+
fn test() {
1186+
let f = || 3;
1187+
let f = |a: S<usize>| S(a);
1188+
}
1189+
"#,
1190+
);
1191+
}
10171192
}

crates/ide/src/inlay_hints/chaining.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,16 @@ fn main() {
603603
},
604604
"",
605605
],
606-
text_edit: None,
606+
text_edit: Some(
607+
TextEdit {
608+
indels: [
609+
Indel {
610+
insert: ": Struct",
611+
delete: 130..130,
612+
},
613+
],
614+
},
615+
),
607616
},
608617
InlayHint {
609618
range: 145..185,
Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
//! Implementation of "closure return type" inlay hints.
2+
//!
3+
//! Tests live in [`bind_pat`][super::bind_pat] module.
24
use ide_db::{base_db::FileId, famous_defs::FamousDefs};
35
use syntax::ast::{self, AstNode};
46

57
use crate::{
6-
inlay_hints::closure_has_block_body, ClosureReturnTypeHints, InlayHint, InlayHintsConfig,
7-
InlayKind,
8+
inlay_hints::{closure_has_block_body, label_of_ty, ty_to_text_edit},
9+
ClosureReturnTypeHints, InlayHint, InlayHintsConfig, InlayKind,
810
};
911

10-
use super::label_of_ty;
11-
1212
pub(super) fn hints(
1313
acc: &mut Vec<InlayHint>,
1414
famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,
@@ -24,26 +24,39 @@ pub(super) fn hints(
2424
return None;
2525
}
2626

27-
if !closure_has_block_body(&closure)
28-
&& config.closure_return_type_hints == ClosureReturnTypeHints::WithBlock
29-
{
27+
let has_block_body = closure_has_block_body(&closure);
28+
if !has_block_body && config.closure_return_type_hints == ClosureReturnTypeHints::WithBlock {
3029
return None;
3130
}
3231

3332
let param_list = closure.param_list()?;
3433

3534
let closure = sema.descend_node_into_attributes(closure).pop()?;
36-
let ty = sema.type_of_expr(&ast::Expr::ClosureExpr(closure))?.adjusted();
35+
let ty = sema.type_of_expr(&ast::Expr::ClosureExpr(closure.clone()))?.adjusted();
3736
let callable = ty.as_callable(sema.db)?;
3837
let ty = callable.return_type();
3938
if ty.is_unit() {
4039
return None;
4140
}
41+
42+
// FIXME?: We could provide text edit to insert braces for closures with non-block body.
43+
let text_edit = if has_block_body {
44+
ty_to_text_edit(
45+
sema,
46+
closure.syntax(),
47+
&ty,
48+
param_list.syntax().text_range().end(),
49+
String::from(" -> "),
50+
)
51+
} else {
52+
None
53+
};
54+
4255
acc.push(InlayHint {
4356
range: param_list.syntax().text_range(),
4457
kind: InlayKind::ClosureReturnType,
4558
label: label_of_ty(famous_defs, config, ty)?,
46-
text_edit: None,
59+
text_edit,
4760
});
4861
Some(())
4962
}

0 commit comments

Comments
 (0)