Auto merge of #14533 - lowr:feat/text-edits-for-inlay-hints, r=Veykril

feat: make inlay hints insertable

Part of #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();
```
This commit is contained in:
bors 2023-04-12 14:11:20 +00:00
commit 1ee88db412
27 changed files with 374 additions and 49 deletions

View File

@ -150,6 +150,7 @@ fn display_source_code<'a>(
&'a self,
db: &'a dyn HirDatabase,
module_id: ModuleId,
allow_opaque: bool,
) -> Result<String, DisplaySourceCodeError> {
let mut result = String::new();
match self.hir_fmt(&mut HirFormatter {
@ -160,7 +161,7 @@ fn display_source_code<'a>(
max_size: None,
omit_verbose_types: false,
closure_style: ClosureStyle::ImplFn,
display_target: DisplayTarget::SourceCode { module_id },
display_target: DisplayTarget::SourceCode { module_id, allow_opaque },
}) {
Ok(()) => {}
Err(HirDisplayError::FmtError) => panic!("Writing to String can't fail!"),
@ -249,18 +250,26 @@ pub enum DisplayTarget {
Diagnostics,
/// Display types for inserting them in source files.
/// The generated code should compile, so paths need to be qualified.
SourceCode { module_id: ModuleId },
SourceCode { module_id: ModuleId, allow_opaque: bool },
/// Only for test purpose to keep real types
Test,
}
impl DisplayTarget {
fn is_source_code(&self) -> bool {
fn is_source_code(self) -> bool {
matches!(self, Self::SourceCode { .. })
}
fn is_test(&self) -> bool {
fn is_test(self) -> bool {
matches!(self, Self::Test)
}
fn allows_opaque(self) -> bool {
match self {
Self::SourceCode { allow_opaque, .. } => allow_opaque,
_ => true,
}
}
}
#[derive(Debug)]
@ -268,6 +277,7 @@ pub enum DisplaySourceCodeError {
PathNotFound,
UnknownType,
Generator,
OpaqueType,
}
pub enum HirDisplayError {
@ -768,7 +778,7 @@ fn hir_fmt(
};
write!(f, "{name}")?;
}
DisplayTarget::SourceCode { module_id } => {
DisplayTarget::SourceCode { module_id, allow_opaque: _ } => {
if let Some(path) = find_path::find_path(
db.upcast(),
ItemInNs::Types((*def_id).into()),
@ -906,6 +916,11 @@ fn should_show(
f.end_location_link();
}
TyKind::OpaqueType(opaque_ty_id, parameters) => {
if !f.display_target.allows_opaque() {
return Err(HirDisplayError::DisplaySourceCodeError(
DisplaySourceCodeError::OpaqueType,
));
}
let impl_trait_id = db.lookup_intern_impl_trait_id((*opaque_ty_id).into());
match impl_trait_id {
ImplTraitId::ReturnTypeImplTrait(func, idx) => {
@ -953,8 +968,14 @@ fn should_show(
}
}
TyKind::Closure(id, substs) => {
if f.display_target.is_source_code() && f.closure_style != ClosureStyle::ImplFn {
never!("Only `impl Fn` is valid for displaying closures in source code");
if f.display_target.is_source_code() {
if !f.display_target.allows_opaque() {
return Err(HirDisplayError::DisplaySourceCodeError(
DisplaySourceCodeError::OpaqueType,
));
} else if f.closure_style != ClosureStyle::ImplFn {
never!("Only `impl Fn` is valid for displaying closures in source code");
}
}
match f.closure_style {
ClosureStyle::Hide => return write!(f, "{TYPE_HINT_TRUNCATION}"),
@ -1053,6 +1074,11 @@ fn should_show(
}
TyKind::Alias(AliasTy::Projection(p_ty)) => p_ty.hir_fmt(f)?,
TyKind::Alias(AliasTy::Opaque(opaque_ty)) => {
if !f.display_target.allows_opaque() {
return Err(HirDisplayError::DisplaySourceCodeError(
DisplaySourceCodeError::OpaqueType,
));
}
let impl_trait_id = db.lookup_intern_impl_trait_id(opaque_ty.opaque_ty_id.into());
match impl_trait_id {
ImplTraitId::ReturnTypeImplTrait(func, idx) => {

View File

@ -159,7 +159,7 @@ fn check_impl(ra_fixture: &str, allow_none: bool, only_types: bool, display_sour
let range = node.as_ref().original_file_range(&db);
if let Some(expected) = types.remove(&range) {
let actual = if display_source {
ty.display_source_code(&db, def.module(&db)).unwrap()
ty.display_source_code(&db, def.module(&db), true).unwrap()
} else {
ty.display_test(&db).to_string()
};
@ -175,7 +175,7 @@ fn check_impl(ra_fixture: &str, allow_none: bool, only_types: bool, display_sour
let range = node.as_ref().original_file_range(&db);
if let Some(expected) = types.remove(&range) {
let actual = if display_source {
ty.display_source_code(&db, def.module(&db)).unwrap()
ty.display_source_code(&db, def.module(&db), true).unwrap()
} else {
ty.display_test(&db).to_string()
};

View File

@ -69,7 +69,7 @@ pub(crate) fn add_explicit_type(acc: &mut Assists, ctx: &AssistContext<'_>) -> O
return None;
}
let inferred_type = ty.display_source_code(ctx.db(), module.into()).ok()?;
let inferred_type = ty.display_source_code(ctx.db(), module.into(), false).ok()?;
acc.add(
AssistId("add_explicit_type", AssistKind::RefactorRewrite),
format!("Insert explicit type `{inferred_type}`"),

View File

@ -22,7 +22,7 @@ pub(crate) fn add_return_type(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opt
if ty.is_unit() {
return None;
}
let ty = ty.display_source_code(ctx.db(), module.into()).ok()?;
let ty = ty.display_source_code(ctx.db(), module.into(), true).ok()?;
acc.add(
AssistId("add_return_type", AssistKind::RefactorRewrite),

View File

@ -1884,7 +1884,7 @@ fn with_tail_expr(block: ast::BlockExpr, tail_expr: ast::Expr) -> ast::BlockExpr
}
fn format_type(ty: &hir::Type, ctx: &AssistContext<'_>, module: hir::Module) -> String {
ty.display_source_code(ctx.db(), module.into()).ok().unwrap_or_else(|| "_".to_string())
ty.display_source_code(ctx.db(), module.into(), true).ok().unwrap_or_else(|| "_".to_string())
}
fn make_ty(ty: &hir::Type, ctx: &AssistContext<'_>, module: hir::Module) -> ast::Type {

View File

@ -46,7 +46,8 @@ pub(crate) fn generate_constant(acc: &mut Assists, ctx: &AssistContext<'_>) -> O
let ty = ctx.sema.type_of_expr(&expr)?;
let scope = ctx.sema.scope(statement.syntax())?;
let constant_module = scope.module();
let type_name = ty.original().display_source_code(ctx.db(), constant_module.into()).ok()?;
let type_name =
ty.original().display_source_code(ctx.db(), constant_module.into(), false).ok()?;
let target = statement.syntax().parent()?.text_range();
let path = constant_token.syntax().ancestors().find_map(ast::Path::cast)?;

View File

@ -192,7 +192,7 @@ fn expr_ty(
scope: &hir::SemanticsScope<'_>,
) -> Option<ast::Type> {
let ty = ctx.sema.type_of_expr(&arg).map(|it| it.adjusted())?;
let text = ty.display_source_code(ctx.db(), scope.module().into()).ok()?;
let text = ty.display_source_code(ctx.db(), scope.module().into(), false).ok()?;
Some(make::ty(&text))
}

View File

@ -438,7 +438,7 @@ fn make_return_type(
Some(ty) if ty.is_unit() => (None, false),
Some(ty) => {
necessary_generic_params.extend(ty.generic_params(ctx.db()));
let rendered = ty.display_source_code(ctx.db(), target_module.into());
let rendered = ty.display_source_code(ctx.db(), target_module.into(), true);
match rendered {
Ok(rendered) => (Some(make::ty(&rendered)), false),
Err(_) => (Some(make::ty_placeholder()), true),
@ -992,9 +992,9 @@ fn maybe_displayed_type(
let famous_defs = &FamousDefs(&ctx.sema, ctx.sema.scope(fn_arg.syntax())?.krate());
convert_reference_type(ty.strip_references(), ctx.db(), famous_defs)
.map(|conversion| conversion.convert_type(ctx.db()))
.or_else(|| ty.display_source_code(ctx.db(), target_module.into()).ok())
.or_else(|| ty.display_source_code(ctx.db(), target_module.into(), true).ok())
} else {
ty.display_source_code(ctx.db(), target_module.into()).ok()
ty.display_source_code(ctx.db(), target_module.into(), true).ok()
}
}

View File

@ -57,11 +57,13 @@ pub(crate) fn promote_local_to_const(acc: &mut Assists, ctx: &AssistContext<'_>)
let local = ctx.sema.to_def(&pat)?;
let ty = ctx.sema.type_of_pat(&pat.into())?.original;
if ty.contains_unknown() || ty.is_closure() {
cov_mark::hit!(promote_lcoal_not_applicable_if_ty_not_inferred);
return None;
}
let ty = ty.display_source_code(ctx.db(), module.into()).ok()?;
let ty = match ty.display_source_code(ctx.db(), module.into(), false) {
Ok(ty) => ty,
Err(_) => {
cov_mark::hit!(promote_local_not_applicable_if_ty_not_inferred);
return None;
}
};
let initializer = let_stmt.initializer()?;
if !is_body_const(&ctx.sema, &initializer) {
@ -187,7 +189,7 @@ fn foo() {
#[test]
fn not_applicable_unknown_ty() {
cov_mark::check!(promote_lcoal_not_applicable_if_ty_not_inferred);
cov_mark::check!(promote_local_not_applicable_if_ty_not_inferred);
check_assist_not_applicable(
promote_local_to_const,
r"

View File

@ -55,7 +55,7 @@ pub(crate) fn replace_turbofish_with_explicit_type(
let returned_type = match ctx.sema.type_of_expr(&initializer) {
Some(returned_type) if !returned_type.original.contains_unknown() => {
let module = ctx.sema.scope(let_stmt.syntax())?.module();
returned_type.original.display_source_code(ctx.db(), module.into()).ok()?
returned_type.original.display_source_code(ctx.db(), module.into(), false).ok()?
}
_ => {
cov_mark::hit!(fallback_to_turbofish_type_if_type_info_not_available);

View File

@ -127,7 +127,7 @@ fn params_from_stmt_list_scope(
let module = scope.module().into();
scope.process_all_names(&mut |name, def| {
if let hir::ScopeDef::Local(local) = def {
if let Ok(ty) = local.ty(ctx.db).display_source_code(ctx.db, module) {
if let Ok(ty) = local.ty(ctx.db).display_source_code(ctx.db, module, true) {
cb(name, ty);
}
}

View File

@ -242,7 +242,7 @@ pub(crate) fn complete_ascribed_type(
}
}?
.adjusted();
let ty_string = x.display_source_code(ctx.db, ctx.module.into()).ok()?;
let ty_string = x.display_source_code(ctx.db, ctx.module.into(), true).ok()?;
acc.add(render_type_inference(ty_string, ctx));
None
}

View File

@ -116,7 +116,9 @@ fn build_ctx(&self) -> Ctx<'a> {
Some((
k,
ast::make::ty(
&default.display_source_code(db, source_module.into()).ok()?,
&default
.display_source_code(db, source_module.into(), false)
.ok()?,
),
))
}

View File

@ -176,7 +176,9 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Option<Vec<Ass
fn make_ty(ty: &hir::Type, db: &dyn HirDatabase, module: hir::Module) -> ast::Type {
let ty_str = match ty.as_adt() {
Some(adt) => adt.name(db).to_string(),
None => ty.display_source_code(db, module.into()).ok().unwrap_or_else(|| "_".to_string()),
None => {
ty.display_source_code(db, module.into(), false).ok().unwrap_or_else(|| "_".to_string())
}
};
make::ty(&ty_str)

View File

@ -69,7 +69,7 @@ fn missing_record_expr_field_fixes(
let new_field = make::record_field(
None,
make::name(record_expr_field.field_name()?.ident_token()?.text()),
make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
make::ty(&new_field_type.display_source_code(sema.db, module.into(), true).ok()?),
);
let last_field = record_fields.fields().last()?;

View File

@ -14,8 +14,9 @@
use stdx::never;
use syntax::{
ast::{self, AstNode},
match_ast, NodeOrToken, SyntaxNode, TextRange,
match_ast, NodeOrToken, SyntaxNode, TextRange, TextSize,
};
use text_edit::TextEdit;
use crate::{navigation_target::TryToNav, FileId};
@ -113,14 +114,26 @@ pub struct InlayHint {
pub kind: InlayKind,
/// The actual label to show in the inlay hint.
pub label: InlayHintLabel,
/// Text edit to apply when "accepting" this inlay hint.
pub text_edit: Option<TextEdit>,
}
impl InlayHint {
fn closing_paren(range: TextRange) -> InlayHint {
InlayHint { range, kind: InlayKind::ClosingParenthesis, label: InlayHintLabel::from(")") }
InlayHint {
range,
kind: InlayKind::ClosingParenthesis,
label: InlayHintLabel::from(")"),
text_edit: None,
}
}
fn opening_paren(range: TextRange) -> InlayHint {
InlayHint { range, kind: InlayKind::OpeningParenthesis, label: InlayHintLabel::from("(") }
InlayHint {
range,
kind: InlayKind::OpeningParenthesis,
label: InlayHintLabel::from("("),
text_edit: None,
}
}
}
@ -346,6 +359,23 @@ fn rec(
Some(r)
}
fn ty_to_text_edit(
sema: &Semantics<'_, RootDatabase>,
node_for_hint: &SyntaxNode,
ty: &hir::Type,
offset_to_insert: TextSize,
prefix: String,
) -> Option<TextEdit> {
let scope = sema.scope(node_for_hint)?;
// FIXME: Limit the length and bail out on excess somehow?
let rendered = ty.display_source_code(scope.db, scope.module().into(), false).ok()?;
let mut builder = TextEdit::builder();
builder.insert(offset_to_insert, prefix);
builder.insert(offset_to_insert, rendered);
Some(builder.finish())
}
// Feature: Inlay Hints
//
// rust-analyzer shows additional information inline with the source code.
@ -553,6 +583,37 @@ pub(super) fn check_expect(config: InlayHintsConfig, ra_fixture: &str, expect: E
expect.assert_debug_eq(&inlay_hints)
}
/// Computes inlay hints for the fixture, applies all the provided text edits and then runs
/// expect test.
#[track_caller]
pub(super) fn check_edit(config: InlayHintsConfig, ra_fixture: &str, expect: Expect) {
let (analysis, file_id) = fixture::file(ra_fixture);
let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
let edits = inlay_hints
.into_iter()
.filter_map(|hint| hint.text_edit)
.reduce(|mut acc, next| {
acc.union(next).expect("merging text edits failed");
acc
})
.expect("no edit returned");
let mut actual = analysis.file_text(file_id).unwrap().to_string();
edits.apply(&mut actual);
expect.assert_eq(&actual);
}
#[track_caller]
pub(super) fn check_no_edit(config: InlayHintsConfig, ra_fixture: &str) {
let (analysis, file_id) = fixture::file(ra_fixture);
let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
let edits: Vec<_> = inlay_hints.into_iter().filter_map(|hint| hint.text_edit).collect();
assert!(edits.is_empty(), "unexpected edits: {edits:?}");
}
#[test]
fn hints_disabled() {
check_with_config(

View File

@ -135,6 +135,7 @@ pub(super) fn hints(
))),
None,
),
text_edit: None,
});
}
if !postfix && needs_inner_parens {

View File

@ -12,9 +12,10 @@
match_ast,
};
use crate::{inlay_hints::closure_has_block_body, InlayHint, InlayHintsConfig, InlayKind};
use super::label_of_ty;
use crate::{
inlay_hints::{closure_has_block_body, label_of_ty, ty_to_text_edit},
InlayHint, InlayHintsConfig, InlayKind,
};
pub(super) fn hints(
acc: &mut Vec<InlayHint>,
@ -35,7 +36,7 @@ pub(super) fn hints(
return None;
}
let label = label_of_ty(famous_defs, config, ty)?;
let label = label_of_ty(famous_defs, config, ty.clone())?;
if config.hide_named_constructor_hints
&& is_named_constructor(sema, pat, &label.to_string()).is_some()
@ -43,6 +44,23 @@ pub(super) fn hints(
return None;
}
let type_annotation_is_valid = desc_pat
.syntax()
.parent()
.map(|it| ast::LetStmt::can_cast(it.kind()) || ast::Param::can_cast(it.kind()))
.unwrap_or(false);
let text_edit = if type_annotation_is_valid {
ty_to_text_edit(
sema,
desc_pat.syntax(),
&ty,
pat.syntax().text_range().end(),
String::from(": "),
)
} else {
None
};
acc.push(InlayHint {
range: match pat.name() {
Some(name) => name.syntax().text_range(),
@ -50,6 +68,7 @@ pub(super) fn hints(
},
kind: InlayKind::Type,
label,
text_edit,
});
Some(())
@ -176,14 +195,16 @@ fn pat_is_enum_variant(db: &RootDatabase, bind_pat: &ast::IdentPat, pat_ty: &hir
mod tests {
// This module also contains tests for super::closure_ret
use expect_test::expect;
use hir::ClosureStyle;
use syntax::{TextRange, TextSize};
use test_utils::extract_annotations;
use crate::{fixture, inlay_hints::InlayHintsConfig};
use crate::{fixture, inlay_hints::InlayHintsConfig, ClosureReturnTypeHints};
use crate::inlay_hints::tests::{check, check_with_config, DISABLED_CONFIG, TEST_CONFIG};
use crate::ClosureReturnTypeHints;
use crate::inlay_hints::tests::{
check, check_edit, check_no_edit, check_with_config, DISABLED_CONFIG, TEST_CONFIG,
};
#[track_caller]
fn check_types(ra_fixture: &str) {
@ -1012,4 +1033,160 @@ fn main() {
}"#,
);
}
#[test]
fn edit_for_let_stmt() {
check_edit(
TEST_CONFIG,
r#"
struct S<T>(T);
fn test<F>(v: S<(S<i32>, S<()>)>, f: F) {
let a = v;
let S((b, c)) = v;
let a @ S((b, c)) = v;
let a = f;
}
"#,
expect![[r#"
struct S<T>(T);
fn test<F>(v: S<(S<i32>, S<()>)>, f: F) {
let a: S<(S<i32>, S<()>)> = v;
let S((b, c)) = v;
let a @ S((b, c)): S<(S<i32>, S<()>)> = v;
let a: F = f;
}
"#]],
);
}
#[test]
fn edit_for_closure_param() {
check_edit(
TEST_CONFIG,
r#"
fn test<T>(t: T) {
let f = |a, b, c| {};
let result = f(42, "", t);
}
"#,
expect![[r#"
fn test<T>(t: T) {
let f = |a: i32, b: &str, c: T| {};
let result: () = f(42, "", t);
}
"#]],
);
}
#[test]
fn edit_for_closure_ret() {
check_edit(
TEST_CONFIG,
r#"
struct S<T>(T);
fn test() {
let f = || { 3 };
let f = |a: S<usize>| { S(a) };
}
"#,
expect![[r#"
struct S<T>(T);
fn test() {
let f = || -> i32 { 3 };
let f = |a: S<usize>| -> S<S<usize>> { S(a) };
}
"#]],
);
}
#[test]
fn edit_prefixes_paths() {
check_edit(
TEST_CONFIG,
r#"
pub struct S<T>(T);
mod middle {
pub struct S<T, U>(T, U);
pub fn make() -> S<inner::S<i64>, super::S<usize>> { loop {} }
mod inner {
pub struct S<T>(T);
}
fn test() {
let a = make();
}
}
"#,
expect![[r#"
pub struct S<T>(T);
mod middle {
pub struct S<T, U>(T, U);
pub fn make() -> S<inner::S<i64>, super::S<usize>> { loop {} }
mod inner {
pub struct S<T>(T);
}
fn test() {
let a: S<inner::S<i64>, crate::S<usize>> = make();
}
}
"#]],
);
}
#[test]
fn no_edit_for_top_pat_where_type_annotation_is_invalid() {
check_no_edit(
TEST_CONFIG,
r#"
fn test() {
if let a = 42 {}
while let a = 42 {}
match 42 {
a => (),
}
}
"#,
)
}
#[test]
fn no_edit_for_opaque_type() {
check_no_edit(
TEST_CONFIG,
r#"
trait Trait {}
struct S<T>(T);
fn foo() -> impl Trait {}
fn bar() -> S<impl Trait> {}
fn test() {
let a = foo();
let a = bar();
let f = || { foo() };
let f = || { bar() };
}
"#,
);
}
#[test]
fn no_edit_for_closure_return_without_body_block() {
// We can lift this limitation; see FIXME in closure_ret module.
let config = InlayHintsConfig {
closure_return_type_hints: ClosureReturnTypeHints::Always,
..TEST_CONFIG
};
check_no_edit(
config,
r#"
struct S<T>(T);
fn test() {
let f = || 3;
let f = |a: S<usize>| S(a);
}
"#,
);
}
}

View File

@ -49,7 +49,12 @@ pub(super) fn hints(
(true, false) => "&",
_ => return,
};
acc.push(InlayHint { range, kind: InlayKind::BindingMode, label: r.to_string().into() });
acc.push(InlayHint {
range,
kind: InlayKind::BindingMode,
label: r.to_string().into(),
text_edit: None,
});
});
match pat {
ast::Pat::IdentPat(pat) if pat.ref_token().is_none() && pat.mut_token().is_none() => {
@ -63,6 +68,7 @@ pub(super) fn hints(
range: pat.syntax().text_range(),
kind: InlayKind::BindingMode,
label: bm.to_string().into(),
text_edit: None,
});
}
ast::Pat::OrPat(pat) if !pattern_adjustments.is_empty() && outer_paren_pat.is_none() => {

View File

@ -61,6 +61,7 @@ pub(super) fn hints(
range: expr.syntax().text_range(),
kind: InlayKind::Chaining,
label: label_of_ty(famous_defs, config, ty)?,
text_edit: None,
});
}
}
@ -120,6 +121,7 @@ fn main() {
},
"",
],
text_edit: None,
},
InlayHint {
range: 147..154,
@ -140,6 +142,7 @@ fn main() {
},
"",
],
text_edit: None,
},
]
"#]],
@ -205,6 +208,7 @@ fn main() {
},
"",
],
text_edit: None,
},
InlayHint {
range: 143..179,
@ -225,6 +229,7 @@ fn main() {
},
"",
],
text_edit: None,
},
]
"#]],
@ -274,6 +279,7 @@ fn main() {
},
"",
],
text_edit: None,
},
InlayHint {
range: 143..179,
@ -294,6 +300,7 @@ fn main() {
},
"",
],
text_edit: None,
},
]
"#]],
@ -357,6 +364,7 @@ fn main() {
},
"<i32, bool>>",
],
text_edit: None,
},
InlayHint {
range: 246..265,
@ -390,6 +398,7 @@ fn main() {
},
"<i32, bool>>",
],
text_edit: None,
},
]
"#]],
@ -455,6 +464,7 @@ fn main() {
},
" = ()>",
],
text_edit: None,
},
InlayHint {
range: 174..224,
@ -488,6 +498,7 @@ fn main() {
},
" = ()>",
],
text_edit: None,
},
InlayHint {
range: 174..206,
@ -521,6 +532,7 @@ fn main() {
},
" = ()>",
],
text_edit: None,
},
InlayHint {
range: 174..189,
@ -541,6 +553,7 @@ fn main() {
},
"",
],
text_edit: None,
},
]
"#]],
@ -590,6 +603,16 @@ fn main() {
},
"",
],
text_edit: Some(
TextEdit {
indels: [
Indel {
insert: ": Struct",
delete: 130..130,
},
],
},
),
},
InlayHint {
range: 145..185,
@ -610,6 +633,7 @@ fn main() {
},
"",
],
text_edit: None,
},
InlayHint {
range: 145..168,
@ -630,6 +654,7 @@ fn main() {
},
"",
],
text_edit: None,
},
InlayHint {
range: 222..228,
@ -648,6 +673,7 @@ fn main() {
tooltip: "",
},
],
text_edit: None,
},
]
"#]],

View File

@ -112,6 +112,7 @@ pub(super) fn hints(
range: closing_token.text_range(),
kind: InlayKind::ClosingBrace,
label: InlayHintLabel::simple(label, None, linked_location),
text_edit: None,
});
None

View File

@ -1,14 +1,14 @@
//! Implementation of "closure return type" inlay hints.
//!
//! Tests live in [`bind_pat`][super::bind_pat] module.
use ide_db::{base_db::FileId, famous_defs::FamousDefs};
use syntax::ast::{self, AstNode};
use crate::{
inlay_hints::closure_has_block_body, ClosureReturnTypeHints, InlayHint, InlayHintsConfig,
InlayKind,
inlay_hints::{closure_has_block_body, label_of_ty, ty_to_text_edit},
ClosureReturnTypeHints, InlayHint, InlayHintsConfig, InlayKind,
};
use super::label_of_ty;
pub(super) fn hints(
acc: &mut Vec<InlayHint>,
famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,
@ -24,25 +24,39 @@ pub(super) fn hints(
return None;
}
if !closure_has_block_body(&closure)
&& config.closure_return_type_hints == ClosureReturnTypeHints::WithBlock
{
let has_block_body = closure_has_block_body(&closure);
if !has_block_body && config.closure_return_type_hints == ClosureReturnTypeHints::WithBlock {
return None;
}
let param_list = closure.param_list()?;
let closure = sema.descend_node_into_attributes(closure).pop()?;
let ty = sema.type_of_expr(&ast::Expr::ClosureExpr(closure))?.adjusted();
let ty = sema.type_of_expr(&ast::Expr::ClosureExpr(closure.clone()))?.adjusted();
let callable = ty.as_callable(sema.db)?;
let ty = callable.return_type();
if ty.is_unit() {
return None;
}
// FIXME?: We could provide text edit to insert braces for closures with non-block body.
let text_edit = if has_block_body {
ty_to_text_edit(
sema,
closure.syntax(),
&ty,
param_list.syntax().text_range().end(),
String::from(" -> "),
)
} else {
None
};
acc.push(InlayHint {
range: param_list.syntax().text_range(),
kind: InlayKind::ClosureReturnType,
label: label_of_ty(famous_defs, config, ty)?,
text_edit,
});
Some(())
}

View File

@ -75,6 +75,7 @@ fn variant_hints(
})),
None,
),
text_edit: None,
});
Some(())

View File

@ -25,6 +25,7 @@ pub(super) fn hints(
range: t.text_range(),
kind: InlayKind::Lifetime,
label: label.into(),
text_edit: None,
};
let param_list = func.param_list()?;
@ -189,12 +190,14 @@ pub(super) fn hints(
if is_empty { "" } else { ", " }
)
.into(),
text_edit: None,
});
}
(None, allocated_lifetimes) => acc.push(InlayHint {
range: func.name()?.syntax().text_range(),
kind: InlayKind::GenericParamList,
label: format!("<{}>", allocated_lifetimes.iter().format(", "),).into(),
text_edit: None,
}),
}
Some(())

View File

@ -34,6 +34,7 @@ pub(super) fn hints(
range: t.text_range(),
kind: InlayKind::Lifetime,
label: "'static".to_owned().into(),
text_edit: None,
});
}
}

View File

@ -57,6 +57,7 @@ pub(super) fn hints(
range,
kind: InlayKind::Parameter,
label: InlayHintLabel::simple(param_name, None, linked_location),
text_edit: None,
}
});

View File

@ -510,7 +510,7 @@ pub(crate) fn inlay_hint(
| InlayKind::AdjustmentPostfix
| InlayKind::ClosingBrace => None,
},
text_edits: None,
text_edits: inlay_hint.text_edit.map(|it| text_edit_vec(line_index, it)),
data: None,
tooltip,
label,