Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit 1ee88db

Browse files
committed
Auto merge of rust-lang#14533 - lowr:feat/text-edits-for-inlay-hints, r=Veykril
feat: make inlay hints insertable Part of rust-lang#13812 This PR implements text edit for inlay hints. When an inlay hint contain text edit, user can "accept" it (e.g. by double-clicking in VS Code) to make the hint actual code (effectively deprecating the hint itself). This PR does not implement auto import despite the original request; text edits only insert qualified types along with necessary punctuation. I feel there are some missing pieces to implement efficient auto import (in particular, type traversal function with early exit) so left it for future work. Even without it, user can use `replace_qualified_name_with_use` assist after accepting the edit to achieve the same result. I implemented for the following inlay hints: - top-level identifier pattern in let statements - top-level identifier pattern in closure parameters - closure return type when its has block body One somewhat strange interaction can be observed when top-level identifier pattern has subpattern: text edit inserts type annotation in different place than the inlay hint. Do we want to allow it or should we not provide text edits for these cases at all? ```rust let a /* inlay hint shown here */ @ (b, c) = foo(); let a @ (b, c) /* text edit inserts types here */ = foo(); ```
2 parents 7501d3b + c978d4b commit 1ee88db

27 files changed

+374
-49
lines changed

crates/hir-ty/src/display.rs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ pub trait HirDisplay {
150150
&'a self,
151151
db: &'a dyn HirDatabase,
152152
module_id: ModuleId,
153+
allow_opaque: bool,
153154
) -> Result<String, DisplaySourceCodeError> {
154155
let mut result = String::new();
155156
match self.hir_fmt(&mut HirFormatter {
@@ -160,7 +161,7 @@ pub trait HirDisplay {
160161
max_size: None,
161162
omit_verbose_types: false,
162163
closure_style: ClosureStyle::ImplFn,
163-
display_target: DisplayTarget::SourceCode { module_id },
164+
display_target: DisplayTarget::SourceCode { module_id, allow_opaque },
164165
}) {
165166
Ok(()) => {}
166167
Err(HirDisplayError::FmtError) => panic!("Writing to String can't fail!"),
@@ -249,25 +250,34 @@ pub enum DisplayTarget {
249250
Diagnostics,
250251
/// Display types for inserting them in source files.
251252
/// The generated code should compile, so paths need to be qualified.
252-
SourceCode { module_id: ModuleId },
253+
SourceCode { module_id: ModuleId, allow_opaque: bool },
253254
/// Only for test purpose to keep real types
254255
Test,
255256
}
256257

257258
impl DisplayTarget {
258-
fn is_source_code(&self) -> bool {
259+
fn is_source_code(self) -> bool {
259260
matches!(self, Self::SourceCode { .. })
260261
}
261-
fn is_test(&self) -> bool {
262+
263+
fn is_test(self) -> bool {
262264
matches!(self, Self::Test)
263265
}
266+
267+
fn allows_opaque(self) -> bool {
268+
match self {
269+
Self::SourceCode { allow_opaque, .. } => allow_opaque,
270+
_ => true,
271+
}
272+
}
264273
}
265274

266275
#[derive(Debug)]
267276
pub enum DisplaySourceCodeError {
268277
PathNotFound,
269278
UnknownType,
270279
Generator,
280+
OpaqueType,
271281
}
272282

273283
pub enum HirDisplayError {
@@ -768,7 +778,7 @@ impl HirDisplay for Ty {
768778
};
769779
write!(f, "{name}")?;
770780
}
771-
DisplayTarget::SourceCode { module_id } => {
781+
DisplayTarget::SourceCode { module_id, allow_opaque: _ } => {
772782
if let Some(path) = find_path::find_path(
773783
db.upcast(),
774784
ItemInNs::Types((*def_id).into()),
@@ -906,6 +916,11 @@ impl HirDisplay for Ty {
906916
f.end_location_link();
907917
}
908918
TyKind::OpaqueType(opaque_ty_id, parameters) => {
919+
if !f.display_target.allows_opaque() {
920+
return Err(HirDisplayError::DisplaySourceCodeError(
921+
DisplaySourceCodeError::OpaqueType,
922+
));
923+
}
909924
let impl_trait_id = db.lookup_intern_impl_trait_id((*opaque_ty_id).into());
910925
match impl_trait_id {
911926
ImplTraitId::ReturnTypeImplTrait(func, idx) => {
@@ -953,8 +968,14 @@ impl HirDisplay for Ty {
953968
}
954969
}
955970
TyKind::Closure(id, substs) => {
956-
if f.display_target.is_source_code() && f.closure_style != ClosureStyle::ImplFn {
957-
never!("Only `impl Fn` is valid for displaying closures in source code");
971+
if f.display_target.is_source_code() {
972+
if !f.display_target.allows_opaque() {
973+
return Err(HirDisplayError::DisplaySourceCodeError(
974+
DisplaySourceCodeError::OpaqueType,
975+
));
976+
} else if f.closure_style != ClosureStyle::ImplFn {
977+
never!("Only `impl Fn` is valid for displaying closures in source code");
978+
}
958979
}
959980
match f.closure_style {
960981
ClosureStyle::Hide => return write!(f, "{TYPE_HINT_TRUNCATION}"),
@@ -1053,6 +1074,11 @@ impl HirDisplay for Ty {
10531074
}
10541075
TyKind::Alias(AliasTy::Projection(p_ty)) => p_ty.hir_fmt(f)?,
10551076
TyKind::Alias(AliasTy::Opaque(opaque_ty)) => {
1077+
if !f.display_target.allows_opaque() {
1078+
return Err(HirDisplayError::DisplaySourceCodeError(
1079+
DisplaySourceCodeError::OpaqueType,
1080+
));
1081+
}
10561082
let impl_trait_id = db.lookup_intern_impl_trait_id(opaque_ty.opaque_ty_id.into());
10571083
match impl_trait_id {
10581084
ImplTraitId::ReturnTypeImplTrait(func, idx) => {

crates/hir-ty/src/tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ fn check_impl(ra_fixture: &str, allow_none: bool, only_types: bool, display_sour
159159
let range = node.as_ref().original_file_range(&db);
160160
if let Some(expected) = types.remove(&range) {
161161
let actual = if display_source {
162-
ty.display_source_code(&db, def.module(&db)).unwrap()
162+
ty.display_source_code(&db, def.module(&db), true).unwrap()
163163
} else {
164164
ty.display_test(&db).to_string()
165165
};
@@ -175,7 +175,7 @@ fn check_impl(ra_fixture: &str, allow_none: bool, only_types: bool, display_sour
175175
let range = node.as_ref().original_file_range(&db);
176176
if let Some(expected) = types.remove(&range) {
177177
let actual = if display_source {
178-
ty.display_source_code(&db, def.module(&db)).unwrap()
178+
ty.display_source_code(&db, def.module(&db), true).unwrap()
179179
} else {
180180
ty.display_test(&db).to_string()
181181
};

crates/ide-assists/src/handlers/add_explicit_type.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ pub(crate) fn add_explicit_type(acc: &mut Assists, ctx: &AssistContext<'_>) -> O
6969
return None;
7070
}
7171

72-
let inferred_type = ty.display_source_code(ctx.db(), module.into()).ok()?;
72+
let inferred_type = ty.display_source_code(ctx.db(), module.into(), false).ok()?;
7373
acc.add(
7474
AssistId("add_explicit_type", AssistKind::RefactorRewrite),
7575
format!("Insert explicit type `{inferred_type}`"),

crates/ide-assists/src/handlers/add_return_type.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub(crate) fn add_return_type(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opt
2222
if ty.is_unit() {
2323
return None;
2424
}
25-
let ty = ty.display_source_code(ctx.db(), module.into()).ok()?;
25+
let ty = ty.display_source_code(ctx.db(), module.into(), true).ok()?;
2626

2727
acc.add(
2828
AssistId("add_return_type", AssistKind::RefactorRewrite),

crates/ide-assists/src/handlers/extract_function.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1884,7 +1884,7 @@ fn with_tail_expr(block: ast::BlockExpr, tail_expr: ast::Expr) -> ast::BlockExpr
18841884
}
18851885

18861886
fn format_type(ty: &hir::Type, ctx: &AssistContext<'_>, module: hir::Module) -> String {
1887-
ty.display_source_code(ctx.db(), module.into()).ok().unwrap_or_else(|| "_".to_string())
1887+
ty.display_source_code(ctx.db(), module.into(), true).ok().unwrap_or_else(|| "_".to_string())
18881888
}
18891889

18901890
fn make_ty(ty: &hir::Type, ctx: &AssistContext<'_>, module: hir::Module) -> ast::Type {

crates/ide-assists/src/handlers/generate_constant.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ pub(crate) fn generate_constant(acc: &mut Assists, ctx: &AssistContext<'_>) -> O
4646
let ty = ctx.sema.type_of_expr(&expr)?;
4747
let scope = ctx.sema.scope(statement.syntax())?;
4848
let constant_module = scope.module();
49-
let type_name = ty.original().display_source_code(ctx.db(), constant_module.into()).ok()?;
49+
let type_name =
50+
ty.original().display_source_code(ctx.db(), constant_module.into(), false).ok()?;
5051
let target = statement.syntax().parent()?.text_range();
5152
let path = constant_token.syntax().ancestors().find_map(ast::Path::cast)?;
5253

crates/ide-assists/src/handlers/generate_enum_variant.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ fn expr_ty(
192192
scope: &hir::SemanticsScope<'_>,
193193
) -> Option<ast::Type> {
194194
let ty = ctx.sema.type_of_expr(&arg).map(|it| it.adjusted())?;
195-
let text = ty.display_source_code(ctx.db(), scope.module().into()).ok()?;
195+
let text = ty.display_source_code(ctx.db(), scope.module().into(), false).ok()?;
196196
Some(make::ty(&text))
197197
}
198198

crates/ide-assists/src/handlers/generate_function.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ fn make_return_type(
438438
Some(ty) if ty.is_unit() => (None, false),
439439
Some(ty) => {
440440
necessary_generic_params.extend(ty.generic_params(ctx.db()));
441-
let rendered = ty.display_source_code(ctx.db(), target_module.into());
441+
let rendered = ty.display_source_code(ctx.db(), target_module.into(), true);
442442
match rendered {
443443
Ok(rendered) => (Some(make::ty(&rendered)), false),
444444
Err(_) => (Some(make::ty_placeholder()), true),
@@ -992,9 +992,9 @@ fn fn_arg_type(
992992
let famous_defs = &FamousDefs(&ctx.sema, ctx.sema.scope(fn_arg.syntax())?.krate());
993993
convert_reference_type(ty.strip_references(), ctx.db(), famous_defs)
994994
.map(|conversion| conversion.convert_type(ctx.db()))
995-
.or_else(|| ty.display_source_code(ctx.db(), target_module.into()).ok())
995+
.or_else(|| ty.display_source_code(ctx.db(), target_module.into(), true).ok())
996996
} else {
997-
ty.display_source_code(ctx.db(), target_module.into()).ok()
997+
ty.display_source_code(ctx.db(), target_module.into(), true).ok()
998998
}
999999
}
10001000

crates/ide-assists/src/handlers/promote_local_to_const.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,13 @@ pub(crate) fn promote_local_to_const(acc: &mut Assists, ctx: &AssistContext<'_>)
5757
let local = ctx.sema.to_def(&pat)?;
5858
let ty = ctx.sema.type_of_pat(&pat.into())?.original;
5959

60-
if ty.contains_unknown() || ty.is_closure() {
61-
cov_mark::hit!(promote_lcoal_not_applicable_if_ty_not_inferred);
62-
return None;
63-
}
64-
let ty = ty.display_source_code(ctx.db(), module.into()).ok()?;
60+
let ty = match ty.display_source_code(ctx.db(), module.into(), false) {
61+
Ok(ty) => ty,
62+
Err(_) => {
63+
cov_mark::hit!(promote_local_not_applicable_if_ty_not_inferred);
64+
return None;
65+
}
66+
};
6567

6668
let initializer = let_stmt.initializer()?;
6769
if !is_body_const(&ctx.sema, &initializer) {
@@ -187,7 +189,7 @@ fn foo() {
187189

188190
#[test]
189191
fn not_applicable_unknown_ty() {
190-
cov_mark::check!(promote_lcoal_not_applicable_if_ty_not_inferred);
192+
cov_mark::check!(promote_local_not_applicable_if_ty_not_inferred);
191193
check_assist_not_applicable(
192194
promote_local_to_const,
193195
r"

crates/ide-assists/src/handlers/replace_turbofish_with_explicit_type.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ pub(crate) fn replace_turbofish_with_explicit_type(
5555
let returned_type = match ctx.sema.type_of_expr(&initializer) {
5656
Some(returned_type) if !returned_type.original.contains_unknown() => {
5757
let module = ctx.sema.scope(let_stmt.syntax())?.module();
58-
returned_type.original.display_source_code(ctx.db(), module.into()).ok()?
58+
returned_type.original.display_source_code(ctx.db(), module.into(), false).ok()?
5959
}
6060
_ => {
6161
cov_mark::hit!(fallback_to_turbofish_type_if_type_info_not_available);

crates/ide-completion/src/completions/fn_param.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ fn params_from_stmt_list_scope(
127127
let module = scope.module().into();
128128
scope.process_all_names(&mut |name, def| {
129129
if let hir::ScopeDef::Local(local) = def {
130-
if let Ok(ty) = local.ty(ctx.db).display_source_code(ctx.db, module) {
130+
if let Ok(ty) = local.ty(ctx.db).display_source_code(ctx.db, module, true) {
131131
cb(name, ty);
132132
}
133133
}

crates/ide-completion/src/completions/type.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ pub(crate) fn complete_ascribed_type(
242242
}
243243
}?
244244
.adjusted();
245-
let ty_string = x.display_source_code(ctx.db, ctx.module.into()).ok()?;
245+
let ty_string = x.display_source_code(ctx.db, ctx.module.into(), true).ok()?;
246246
acc.add(render_type_inference(ty_string, ctx));
247247
None
248248
}

crates/ide-db/src/path_transform.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ impl<'a> PathTransform<'a> {
116116
Some((
117117
k,
118118
ast::make::ty(
119-
&default.display_source_code(db, source_module.into()).ok()?,
119+
&default
120+
.display_source_code(db, source_module.into(), false)
121+
.ok()?,
120122
),
121123
))
122124
}

crates/ide-diagnostics/src/handlers/missing_fields.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,9 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Option<Vec<Ass
176176
fn make_ty(ty: &hir::Type, db: &dyn HirDatabase, module: hir::Module) -> ast::Type {
177177
let ty_str = match ty.as_adt() {
178178
Some(adt) => adt.name(db).to_string(),
179-
None => ty.display_source_code(db, module.into()).ok().unwrap_or_else(|| "_".to_string()),
179+
None => {
180+
ty.display_source_code(db, module.into(), false).ok().unwrap_or_else(|| "_".to_string())
181+
}
180182
};
181183

182184
make::ty(&ty_str)

crates/ide-diagnostics/src/handlers/no_such_field.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ fn missing_record_expr_field_fixes(
6969
let new_field = make::record_field(
7070
None,
7171
make::name(record_expr_field.field_name()?.ident_token()?.text()),
72-
make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
72+
make::ty(&new_field_type.display_source_code(sema.db, module.into(), true).ok()?),
7373
);
7474

7575
let last_field = record_fields.fields().last()?;

crates/ide/src/inlay_hints.rs

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ 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
};
19+
use text_edit::TextEdit;
1920

2021
use crate::{navigation_target::TryToNav, FileId};
2122

@@ -113,14 +114,26 @@ pub struct InlayHint {
113114
pub kind: InlayKind,
114115
/// The actual label to show in the inlay hint.
115116
pub label: InlayHintLabel,
117+
/// Text edit to apply when "accepting" this inlay hint.
118+
pub text_edit: Option<TextEdit>,
116119
}
117120

118121
impl InlayHint {
119122
fn closing_paren(range: TextRange) -> InlayHint {
120-
InlayHint { range, kind: InlayKind::ClosingParenthesis, label: InlayHintLabel::from(")") }
123+
InlayHint {
124+
range,
125+
kind: InlayKind::ClosingParenthesis,
126+
label: InlayHintLabel::from(")"),
127+
text_edit: None,
128+
}
121129
}
122130
fn opening_paren(range: TextRange) -> InlayHint {
123-
InlayHint { range, kind: InlayKind::OpeningParenthesis, label: InlayHintLabel::from("(") }
131+
InlayHint {
132+
range,
133+
kind: InlayKind::OpeningParenthesis,
134+
label: InlayHintLabel::from("("),
135+
text_edit: None,
136+
}
124137
}
125138
}
126139

@@ -346,6 +359,23 @@ fn label_of_ty(
346359
Some(r)
347360
}
348361

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+
349379
// Feature: Inlay Hints
350380
//
351381
// rust-analyzer shows additional information inline with the source code.
@@ -553,6 +583,37 @@ mod tests {
553583
expect.assert_debug_eq(&inlay_hints)
554584
}
555585

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+
556617
#[test]
557618
fn hints_disabled() {
558619
check_with_config(

crates/ide/src/inlay_hints/adjustment.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ pub(super) fn hints(
135135
))),
136136
None,
137137
),
138+
text_edit: None,
138139
});
139140
}
140141
if !postfix && needs_inner_parens {

0 commit comments

Comments
 (0)