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 @@ pub trait HirDisplay {
&'a self, &'a self,
db: &'a dyn HirDatabase, db: &'a dyn HirDatabase,
module_id: ModuleId, module_id: ModuleId,
allow_opaque: bool,
) -> Result<String, DisplaySourceCodeError> { ) -> Result<String, DisplaySourceCodeError> {
let mut result = String::new(); let mut result = String::new();
match self.hir_fmt(&mut HirFormatter { match self.hir_fmt(&mut HirFormatter {
@ -160,7 +161,7 @@ pub trait HirDisplay {
max_size: None, max_size: None,
omit_verbose_types: false, omit_verbose_types: false,
closure_style: ClosureStyle::ImplFn, closure_style: ClosureStyle::ImplFn,
display_target: DisplayTarget::SourceCode { module_id }, display_target: DisplayTarget::SourceCode { module_id, allow_opaque },
}) { }) {
Ok(()) => {} Ok(()) => {}
Err(HirDisplayError::FmtError) => panic!("Writing to String can't fail!"), Err(HirDisplayError::FmtError) => panic!("Writing to String can't fail!"),
@ -249,18 +250,26 @@ pub enum DisplayTarget {
Diagnostics, Diagnostics,
/// Display types for inserting them in source files. /// Display types for inserting them in source files.
/// The generated code should compile, so paths need to be qualified. /// 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 /// Only for test purpose to keep real types
Test, Test,
} }
impl DisplayTarget { impl DisplayTarget {
fn is_source_code(&self) -> bool { fn is_source_code(self) -> bool {
matches!(self, Self::SourceCode { .. }) matches!(self, Self::SourceCode { .. })
} }
fn is_test(&self) -> bool {
fn is_test(self) -> bool {
matches!(self, Self::Test) matches!(self, Self::Test)
} }
fn allows_opaque(self) -> bool {
match self {
Self::SourceCode { allow_opaque, .. } => allow_opaque,
_ => true,
}
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -268,6 +277,7 @@ pub enum DisplaySourceCodeError {
PathNotFound, PathNotFound,
UnknownType, UnknownType,
Generator, Generator,
OpaqueType,
} }
pub enum HirDisplayError { pub enum HirDisplayError {
@ -768,7 +778,7 @@ impl HirDisplay for Ty {
}; };
write!(f, "{name}")?; write!(f, "{name}")?;
} }
DisplayTarget::SourceCode { module_id } => { DisplayTarget::SourceCode { module_id, allow_opaque: _ } => {
if let Some(path) = find_path::find_path( if let Some(path) = find_path::find_path(
db.upcast(), db.upcast(),
ItemInNs::Types((*def_id).into()), ItemInNs::Types((*def_id).into()),
@ -906,6 +916,11 @@ impl HirDisplay for Ty {
f.end_location_link(); f.end_location_link();
} }
TyKind::OpaqueType(opaque_ty_id, parameters) => { 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()); let impl_trait_id = db.lookup_intern_impl_trait_id((*opaque_ty_id).into());
match impl_trait_id { match impl_trait_id {
ImplTraitId::ReturnTypeImplTrait(func, idx) => { ImplTraitId::ReturnTypeImplTrait(func, idx) => {
@ -953,8 +968,14 @@ impl HirDisplay for Ty {
} }
} }
TyKind::Closure(id, substs) => { TyKind::Closure(id, substs) => {
if f.display_target.is_source_code() && f.closure_style != ClosureStyle::ImplFn { if f.display_target.is_source_code() {
never!("Only `impl Fn` is valid for displaying closures in 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 { match f.closure_style {
ClosureStyle::Hide => return write!(f, "{TYPE_HINT_TRUNCATION}"), ClosureStyle::Hide => return write!(f, "{TYPE_HINT_TRUNCATION}"),
@ -1053,6 +1074,11 @@ impl HirDisplay for Ty {
} }
TyKind::Alias(AliasTy::Projection(p_ty)) => p_ty.hir_fmt(f)?, TyKind::Alias(AliasTy::Projection(p_ty)) => p_ty.hir_fmt(f)?,
TyKind::Alias(AliasTy::Opaque(opaque_ty)) => { 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()); let impl_trait_id = db.lookup_intern_impl_trait_id(opaque_ty.opaque_ty_id.into());
match impl_trait_id { match impl_trait_id {
ImplTraitId::ReturnTypeImplTrait(func, idx) => { 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); let range = node.as_ref().original_file_range(&db);
if let Some(expected) = types.remove(&range) { if let Some(expected) = types.remove(&range) {
let actual = if display_source { 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 { } else {
ty.display_test(&db).to_string() 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); let range = node.as_ref().original_file_range(&db);
if let Some(expected) = types.remove(&range) { if let Some(expected) = types.remove(&range) {
let actual = if display_source { 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 { } else {
ty.display_test(&db).to_string() 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; 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( acc.add(
AssistId("add_explicit_type", AssistKind::RefactorRewrite), AssistId("add_explicit_type", AssistKind::RefactorRewrite),
format!("Insert explicit type `{inferred_type}`"), 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() { if ty.is_unit() {
return None; 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( acc.add(
AssistId("add_return_type", AssistKind::RefactorRewrite), 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 { 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 { 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 ty = ctx.sema.type_of_expr(&expr)?;
let scope = ctx.sema.scope(statement.syntax())?; let scope = ctx.sema.scope(statement.syntax())?;
let constant_module = scope.module(); 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 target = statement.syntax().parent()?.text_range();
let path = constant_token.syntax().ancestors().find_map(ast::Path::cast)?; let path = constant_token.syntax().ancestors().find_map(ast::Path::cast)?;

View file

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

View file

@ -438,7 +438,7 @@ fn make_return_type(
Some(ty) if ty.is_unit() => (None, false), Some(ty) if ty.is_unit() => (None, false),
Some(ty) => { Some(ty) => {
necessary_generic_params.extend(ty.generic_params(ctx.db())); 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 { match rendered {
Ok(rendered) => (Some(make::ty(&rendered)), false), Ok(rendered) => (Some(make::ty(&rendered)), false),
Err(_) => (Some(make::ty_placeholder()), true), Err(_) => (Some(make::ty_placeholder()), true),
@ -992,9 +992,9 @@ fn fn_arg_type(
let famous_defs = &FamousDefs(&ctx.sema, ctx.sema.scope(fn_arg.syntax())?.krate()); let famous_defs = &FamousDefs(&ctx.sema, ctx.sema.scope(fn_arg.syntax())?.krate());
convert_reference_type(ty.strip_references(), ctx.db(), famous_defs) convert_reference_type(ty.strip_references(), ctx.db(), famous_defs)
.map(|conversion| conversion.convert_type(ctx.db())) .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 { } 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 local = ctx.sema.to_def(&pat)?;
let ty = ctx.sema.type_of_pat(&pat.into())?.original; let ty = ctx.sema.type_of_pat(&pat.into())?.original;
if ty.contains_unknown() || ty.is_closure() { let ty = match ty.display_source_code(ctx.db(), module.into(), false) {
cov_mark::hit!(promote_lcoal_not_applicable_if_ty_not_inferred); Ok(ty) => ty,
return None; Err(_) => {
} cov_mark::hit!(promote_local_not_applicable_if_ty_not_inferred);
let ty = ty.display_source_code(ctx.db(), module.into()).ok()?; return None;
}
};
let initializer = let_stmt.initializer()?; let initializer = let_stmt.initializer()?;
if !is_body_const(&ctx.sema, &initializer) { if !is_body_const(&ctx.sema, &initializer) {
@ -187,7 +189,7 @@ fn foo() {
#[test] #[test]
fn not_applicable_unknown_ty() { 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( check_assist_not_applicable(
promote_local_to_const, promote_local_to_const,
r" 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) { let returned_type = match ctx.sema.type_of_expr(&initializer) {
Some(returned_type) if !returned_type.original.contains_unknown() => { Some(returned_type) if !returned_type.original.contains_unknown() => {
let module = ctx.sema.scope(let_stmt.syntax())?.module(); 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); 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(); let module = scope.module().into();
scope.process_all_names(&mut |name, def| { scope.process_all_names(&mut |name, def| {
if let hir::ScopeDef::Local(local) = 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); cb(name, ty);
} }
} }

View file

@ -242,7 +242,7 @@ pub(crate) fn complete_ascribed_type(
} }
}? }?
.adjusted(); .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)); acc.add(render_type_inference(ty_string, ctx));
None None
} }

View file

@ -116,7 +116,9 @@ impl<'a> PathTransform<'a> {
Some(( Some((
k, k,
ast::make::ty( 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 { fn make_ty(ty: &hir::Type, db: &dyn HirDatabase, module: hir::Module) -> ast::Type {
let ty_str = match ty.as_adt() { let ty_str = match ty.as_adt() {
Some(adt) => adt.name(db).to_string(), 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) make::ty(&ty_str)

View file

@ -69,7 +69,7 @@ fn missing_record_expr_field_fixes(
let new_field = make::record_field( let new_field = make::record_field(
None, None,
make::name(record_expr_field.field_name()?.ident_token()?.text()), 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()?; let last_field = record_fields.fields().last()?;

View file

@ -14,8 +14,9 @@ use smallvec::{smallvec, SmallVec};
use stdx::never; use stdx::never;
use syntax::{ use syntax::{
ast::{self, AstNode}, ast::{self, AstNode},
match_ast, NodeOrToken, SyntaxNode, TextRange, match_ast, NodeOrToken, SyntaxNode, TextRange, TextSize,
}; };
use text_edit::TextEdit;
use crate::{navigation_target::TryToNav, FileId}; use crate::{navigation_target::TryToNav, FileId};
@ -113,14 +114,26 @@ pub struct InlayHint {
pub kind: InlayKind, pub kind: InlayKind,
/// The actual label to show in the inlay hint. /// The actual label to show in the inlay hint.
pub label: InlayHintLabel, pub label: InlayHintLabel,
/// Text edit to apply when "accepting" this inlay hint.
pub text_edit: Option<TextEdit>,
} }
impl InlayHint { impl InlayHint {
fn closing_paren(range: TextRange) -> 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 { 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 label_of_ty(
Some(r) 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 // Feature: Inlay Hints
// //
// rust-analyzer shows additional information inline with the source code. // rust-analyzer shows additional information inline with the source code.
@ -553,6 +583,37 @@ mod tests {
expect.assert_debug_eq(&inlay_hints) 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] #[test]
fn hints_disabled() { fn hints_disabled() {
check_with_config( check_with_config(

View file

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

View file

@ -12,9 +12,10 @@ use syntax::{
match_ast, match_ast,
}; };
use crate::{inlay_hints::closure_has_block_body, InlayHint, InlayHintsConfig, InlayKind}; use crate::{
inlay_hints::{closure_has_block_body, label_of_ty, ty_to_text_edit},
use super::label_of_ty; InlayHint, InlayHintsConfig, InlayKind,
};
pub(super) fn hints( pub(super) fn hints(
acc: &mut Vec<InlayHint>, acc: &mut Vec<InlayHint>,
@ -35,7 +36,7 @@ pub(super) fn hints(
return None; 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 if config.hide_named_constructor_hints
&& is_named_constructor(sema, pat, &label.to_string()).is_some() && is_named_constructor(sema, pat, &label.to_string()).is_some()
@ -43,6 +44,23 @@ pub(super) fn hints(
return None; 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 { acc.push(InlayHint {
range: match pat.name() { range: match pat.name() {
Some(name) => name.syntax().text_range(), Some(name) => name.syntax().text_range(),
@ -50,6 +68,7 @@ pub(super) fn hints(
}, },
kind: InlayKind::Type, kind: InlayKind::Type,
label, label,
text_edit,
}); });
Some(()) Some(())
@ -176,14 +195,16 @@ fn pat_is_enum_variant(db: &RootDatabase, bind_pat: &ast::IdentPat, pat_ty: &hir
mod tests { mod tests {
// This module also contains tests for super::closure_ret // This module also contains tests for super::closure_ret
use expect_test::expect;
use hir::ClosureStyle; use hir::ClosureStyle;
use syntax::{TextRange, TextSize}; use syntax::{TextRange, TextSize};
use test_utils::extract_annotations; 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::inlay_hints::tests::{
use crate::ClosureReturnTypeHints; check, check_edit, check_no_edit, check_with_config, DISABLED_CONFIG, TEST_CONFIG,
};
#[track_caller] #[track_caller]
fn check_types(ra_fixture: &str) { 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) => "&", (true, false) => "&",
_ => return, _ => 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 { match pat {
ast::Pat::IdentPat(pat) if pat.ref_token().is_none() && pat.mut_token().is_none() => { 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(), range: pat.syntax().text_range(),
kind: InlayKind::BindingMode, kind: InlayKind::BindingMode,
label: bm.to_string().into(), label: bm.to_string().into(),
text_edit: None,
}); });
} }
ast::Pat::OrPat(pat) if !pattern_adjustments.is_empty() && outer_paren_pat.is_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(), range: expr.syntax().text_range(),
kind: InlayKind::Chaining, kind: InlayKind::Chaining,
label: label_of_ty(famous_defs, config, ty)?, label: label_of_ty(famous_defs, config, ty)?,
text_edit: None,
}); });
} }
} }
@ -120,6 +121,7 @@ fn main() {
}, },
"", "",
], ],
text_edit: None,
}, },
InlayHint { InlayHint {
range: 147..154, range: 147..154,
@ -140,6 +142,7 @@ fn main() {
}, },
"", "",
], ],
text_edit: None,
}, },
] ]
"#]], "#]],
@ -205,6 +208,7 @@ fn main() {
}, },
"", "",
], ],
text_edit: None,
}, },
InlayHint { InlayHint {
range: 143..179, range: 143..179,
@ -225,6 +229,7 @@ fn main() {
}, },
"", "",
], ],
text_edit: None,
}, },
] ]
"#]], "#]],
@ -274,6 +279,7 @@ fn main() {
}, },
"", "",
], ],
text_edit: None,
}, },
InlayHint { InlayHint {
range: 143..179, range: 143..179,
@ -294,6 +300,7 @@ fn main() {
}, },
"", "",
], ],
text_edit: None,
}, },
] ]
"#]], "#]],
@ -357,6 +364,7 @@ fn main() {
}, },
"<i32, bool>>", "<i32, bool>>",
], ],
text_edit: None,
}, },
InlayHint { InlayHint {
range: 246..265, range: 246..265,
@ -390,6 +398,7 @@ fn main() {
}, },
"<i32, bool>>", "<i32, bool>>",
], ],
text_edit: None,
}, },
] ]
"#]], "#]],
@ -455,6 +464,7 @@ fn main() {
}, },
" = ()>", " = ()>",
], ],
text_edit: None,
}, },
InlayHint { InlayHint {
range: 174..224, range: 174..224,
@ -488,6 +498,7 @@ fn main() {
}, },
" = ()>", " = ()>",
], ],
text_edit: None,
}, },
InlayHint { InlayHint {
range: 174..206, range: 174..206,
@ -521,6 +532,7 @@ fn main() {
}, },
" = ()>", " = ()>",
], ],
text_edit: None,
}, },
InlayHint { InlayHint {
range: 174..189, 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 { InlayHint {
range: 145..185, range: 145..185,
@ -610,6 +633,7 @@ fn main() {
}, },
"", "",
], ],
text_edit: None,
}, },
InlayHint { InlayHint {
range: 145..168, range: 145..168,
@ -630,6 +654,7 @@ fn main() {
}, },
"", "",
], ],
text_edit: None,
}, },
InlayHint { InlayHint {
range: 222..228, range: 222..228,
@ -648,6 +673,7 @@ fn main() {
tooltip: "", tooltip: "",
}, },
], ],
text_edit: None,
}, },
] ]
"#]], "#]],

View file

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

View file

@ -1,14 +1,14 @@
//! Implementation of "closure return type" inlay hints. //! 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 ide_db::{base_db::FileId, famous_defs::FamousDefs};
use syntax::ast::{self, AstNode}; use syntax::ast::{self, AstNode};
use crate::{ use crate::{
inlay_hints::closure_has_block_body, ClosureReturnTypeHints, InlayHint, InlayHintsConfig, inlay_hints::{closure_has_block_body, label_of_ty, ty_to_text_edit},
InlayKind, ClosureReturnTypeHints, InlayHint, InlayHintsConfig, InlayKind,
}; };
use super::label_of_ty;
pub(super) fn hints( pub(super) fn hints(
acc: &mut Vec<InlayHint>, acc: &mut Vec<InlayHint>,
famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>, famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,
@ -24,25 +24,39 @@ pub(super) fn hints(
return None; return None;
} }
if !closure_has_block_body(&closure) let has_block_body = closure_has_block_body(&closure);
&& config.closure_return_type_hints == ClosureReturnTypeHints::WithBlock if !has_block_body && config.closure_return_type_hints == ClosureReturnTypeHints::WithBlock {
{
return None; return None;
} }
let param_list = closure.param_list()?; let param_list = closure.param_list()?;
let closure = sema.descend_node_into_attributes(closure).pop()?; 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 callable = ty.as_callable(sema.db)?;
let ty = callable.return_type(); let ty = callable.return_type();
if ty.is_unit() { if ty.is_unit() {
return None; 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 { acc.push(InlayHint {
range: param_list.syntax().text_range(), range: param_list.syntax().text_range(),
kind: InlayKind::ClosureReturnType, kind: InlayKind::ClosureReturnType,
label: label_of_ty(famous_defs, config, ty)?, label: label_of_ty(famous_defs, config, ty)?,
text_edit,
}); });
Some(()) Some(())
} }

View file

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

View file

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

View file

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

View file

@ -57,6 +57,7 @@ pub(super) fn hints(
range, range,
kind: InlayKind::Parameter, kind: InlayKind::Parameter,
label: InlayHintLabel::simple(param_name, None, linked_location), 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::AdjustmentPostfix
| InlayKind::ClosingBrace => None, | InlayKind::ClosingBrace => None,
}, },
text_edits: None, text_edits: inlay_hint.text_edit.map(|it| text_edit_vec(line_index, it)),
data: None, data: None,
tooltip, tooltip,
label, label,