Skip to content

Commit c24eb5e

Browse files
committed
Implement text edits for inlay hints
1 parent fd5b27b commit c24eb5e

File tree

4 files changed

+261
-15
lines changed

4 files changed

+261
-15
lines changed

crates/ide/src/inlay_hints.rs

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

@@ -353,6 +353,23 @@ fn label_of_ty(
353353
Some(r)
354354
}
355355

356+
fn ty_to_text_edit(
357+
sema: &Semantics<'_, RootDatabase>,
358+
node_for_hint: &SyntaxNode,
359+
ty: &hir::Type,
360+
offset_to_insert: TextSize,
361+
prefix: String,
362+
) -> Option<TextEdit> {
363+
let scope = sema.scope(node_for_hint)?;
364+
// FIXME: Limit the length and bail out on excess somehow?
365+
let rendered = ty.display_source_code(scope.db, scope.module().into(), false).ok()?;
366+
367+
let mut builder = TextEdit::builder();
368+
builder.insert(offset_to_insert, prefix);
369+
builder.insert(offset_to_insert, rendered);
370+
Some(builder.finish())
371+
}
372+
356373
// Feature: Inlay Hints
357374
//
358375
// rust-analyzer shows additional information inline with the source code.
@@ -558,6 +575,37 @@ mod tests {
558575
expect.assert_debug_eq(&inlay_hints)
559576
}
560577

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

crates/ide/src/inlay_hints/bind_pat.rs

Lines changed: 180 additions & 4 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,12 +195,15 @@ 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 syntax::{TextRange, TextSize};
182200
use test_utils::extract_annotations;
183201

184202
use crate::{fixture, inlay_hints::InlayHintsConfig};
185203

186-
use crate::inlay_hints::tests::{check, check_with_config, DISABLED_CONFIG, TEST_CONFIG};
204+
use crate::inlay_hints::tests::{
205+
check, check_edit, check_no_edit, check_with_config, DISABLED_CONFIG, TEST_CONFIG,
206+
};
187207
use crate::ClosureReturnTypeHints;
188208

189209
#[track_caller]
@@ -925,4 +945,160 @@ fn main() {
925945
}"#,
926946
);
927947
}
948+
949+
#[test]
950+
fn edit_for_let_stmt() {
951+
check_edit(
952+
TEST_CONFIG,
953+
r#"
954+
struct S<T>(T);
955+
fn test<F>(v: S<(S<i32>, S<()>)>, f: F) {
956+
let a = v;
957+
let S((b, c)) = v;
958+
let a @ S((b, c)) = v;
959+
let a = f;
960+
}
961+
"#,
962+
expect![[r#"
963+
struct S<T>(T);
964+
fn test<F>(v: S<(S<i32>, S<()>)>, f: F) {
965+
let a: S<(S<i32>, S<()>)> = v;
966+
let S((b, c)) = v;
967+
let a @ S((b, c)): S<(S<i32>, S<()>)> = v;
968+
let a: F = f;
969+
}
970+
"#]],
971+
);
972+
}
973+
974+
#[test]
975+
fn edit_for_closure_param() {
976+
check_edit(
977+
TEST_CONFIG,
978+
r#"
979+
fn test<T>(t: T) {
980+
let f = |a, b, c| {};
981+
let result = f(42, "", t);
982+
}
983+
"#,
984+
expect![[r#"
985+
fn test<T>(t: T) {
986+
let f = |a: i32, b: &str, c: T| {};
987+
let result: () = f(42, "", t);
988+
}
989+
"#]],
990+
);
991+
}
992+
993+
#[test]
994+
fn edit_for_closure_ret() {
995+
check_edit(
996+
TEST_CONFIG,
997+
r#"
998+
struct S<T>(T);
999+
fn test() {
1000+
let f = || { 3 };
1001+
let f = |a: S<usize>| { S(a) };
1002+
}
1003+
"#,
1004+
expect![[r#"
1005+
struct S<T>(T);
1006+
fn test() {
1007+
let f = || -> i32 { 3 };
1008+
let f = |a: S<usize>| -> S<S<usize>> { S(a) };
1009+
}
1010+
"#]],
1011+
);
1012+
}
1013+
1014+
#[test]
1015+
fn edit_prefixes_paths() {
1016+
check_edit(
1017+
TEST_CONFIG,
1018+
r#"
1019+
pub struct S<T>(T);
1020+
mod middle {
1021+
pub struct S<T, U>(T, U);
1022+
pub fn make() -> S<inner::S<i64>, super::S<usize>> { loop {} }
1023+
1024+
mod inner {
1025+
pub struct S<T>(T);
1026+
}
1027+
1028+
fn test() {
1029+
let a = make();
1030+
}
1031+
}
1032+
"#,
1033+
expect![[r#"
1034+
pub struct S<T>(T);
1035+
mod middle {
1036+
pub struct S<T, U>(T, U);
1037+
pub fn make() -> S<inner::S<i64>, super::S<usize>> { loop {} }
1038+
1039+
mod inner {
1040+
pub struct S<T>(T);
1041+
}
1042+
1043+
fn test() {
1044+
let a: S<inner::S<i64>, crate::S<usize>> = make();
1045+
}
1046+
}
1047+
"#]],
1048+
);
1049+
}
1050+
1051+
#[test]
1052+
fn no_edit_for_top_pat_where_type_annotation_is_invalid() {
1053+
check_no_edit(
1054+
TEST_CONFIG,
1055+
r#"
1056+
fn test() {
1057+
if let a = 42 {}
1058+
while let a = 42 {}
1059+
match 42 {
1060+
a => (),
1061+
}
1062+
}
1063+
"#,
1064+
)
1065+
}
1066+
1067+
#[test]
1068+
fn no_edit_for_opaque_type() {
1069+
check_no_edit(
1070+
TEST_CONFIG,
1071+
r#"
1072+
trait Trait {}
1073+
struct S<T>(T);
1074+
fn foo() -> impl Trait {}
1075+
fn bar() -> S<impl Trait> {}
1076+
fn test() {
1077+
let a = foo();
1078+
let a = bar();
1079+
let f = || { foo() };
1080+
let f = || { bar() };
1081+
}
1082+
"#,
1083+
);
1084+
}
1085+
1086+
#[test]
1087+
fn no_edit_for_closure_return_without_body_block() {
1088+
// We can lift this limitation; see FIXME in closure_ret module.
1089+
let config = InlayHintsConfig {
1090+
closure_return_type_hints: ClosureReturnTypeHints::Always,
1091+
..TEST_CONFIG
1092+
};
1093+
check_no_edit(
1094+
config,
1095+
r#"
1096+
struct S<T>(T);
1097+
fn test() {
1098+
let f = || 3;
1099+
let f = |a: S<usize>| S(a);
1100+
}
1101+
"#,
1102+
);
1103+
}
9281104
}

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)