Unnamed repository; edit this file 'description' to name the repository.
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(); ```
bors 2023-04-12
parent 7501d3b · parent c978d4b · commit 1ee88db
-rw-r--r--crates/hir-ty/src/display.rs40
-rw-r--r--crates/hir-ty/src/tests.rs4
-rw-r--r--crates/ide-assists/src/handlers/add_explicit_type.rs2
-rw-r--r--crates/ide-assists/src/handlers/add_return_type.rs2
-rw-r--r--crates/ide-assists/src/handlers/extract_function.rs2
-rw-r--r--crates/ide-assists/src/handlers/generate_constant.rs3
-rw-r--r--crates/ide-assists/src/handlers/generate_enum_variant.rs2
-rw-r--r--crates/ide-assists/src/handlers/generate_function.rs6
-rw-r--r--crates/ide-assists/src/handlers/promote_local_to_const.rs14
-rw-r--r--crates/ide-assists/src/handlers/replace_turbofish_with_explicit_type.rs2
-rw-r--r--crates/ide-completion/src/completions/fn_param.rs2
-rw-r--r--crates/ide-completion/src/completions/type.rs2
-rw-r--r--crates/ide-db/src/path_transform.rs4
-rw-r--r--crates/ide-diagnostics/src/handlers/missing_fields.rs4
-rw-r--r--crates/ide-diagnostics/src/handlers/no_such_field.rs2
-rw-r--r--crates/ide/src/inlay_hints.rs67
-rw-r--r--crates/ide/src/inlay_hints/adjustment.rs1
-rw-r--r--crates/ide/src/inlay_hints/bind_pat.rs191
-rw-r--r--crates/ide/src/inlay_hints/binding_mode.rs8
-rw-r--r--crates/ide/src/inlay_hints/chaining.rs26
-rw-r--r--crates/ide/src/inlay_hints/closing_brace.rs1
-rw-r--r--crates/ide/src/inlay_hints/closure_ret.rs30
-rw-r--r--crates/ide/src/inlay_hints/discriminant.rs1
-rw-r--r--crates/ide/src/inlay_hints/fn_lifetime_fn.rs3
-rw-r--r--crates/ide/src/inlay_hints/implicit_static.rs1
-rw-r--r--crates/ide/src/inlay_hints/param_name.rs1
-rw-r--r--crates/rust-analyzer/src/to_proto.rs2
27 files changed, 374 insertions, 49 deletions
diff --git a/crates/hir-ty/src/display.rs b/crates/hir-ty/src/display.rs
index f892a81519..0eef25102e 100644
--- a/crates/hir-ty/src/display.rs
+++ b/crates/hir-ty/src/display.rs
@@ -150,6 +150,7 @@ pub trait HirDisplay {
&'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 @@ pub trait HirDisplay {
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 @@ impl HirDisplay for Ty {
};
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 @@ impl HirDisplay for Ty {
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 @@ impl HirDisplay for Ty {
}
}
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 @@ impl HirDisplay for Ty {
}
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) => {
diff --git a/crates/hir-ty/src/tests.rs b/crates/hir-ty/src/tests.rs
index 1e46bb1d04..245617ab82 100644
--- a/crates/hir-ty/src/tests.rs
+++ b/crates/hir-ty/src/tests.rs
@@ -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()
};
diff --git a/crates/ide-assists/src/handlers/add_explicit_type.rs b/crates/ide-assists/src/handlers/add_explicit_type.rs
index 785ae3d09c..8bc285614e 100644
--- a/crates/ide-assists/src/handlers/add_explicit_type.rs
+++ b/crates/ide-assists/src/handlers/add_explicit_type.rs
@@ -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}`"),
diff --git a/crates/ide-assists/src/handlers/add_return_type.rs b/crates/ide-assists/src/handlers/add_return_type.rs
index 879c478acf..9e1022d809 100644
--- a/crates/ide-assists/src/handlers/add_return_type.rs
+++ b/crates/ide-assists/src/handlers/add_return_type.rs
@@ -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),
diff --git a/crates/ide-assists/src/handlers/extract_function.rs b/crates/ide-assists/src/handlers/extract_function.rs
index bfa2890676..728018506d 100644
--- a/crates/ide-assists/src/handlers/extract_function.rs
+++ b/crates/ide-assists/src/handlers/extract_function.rs
@@ -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 {
diff --git a/crates/ide-assists/src/handlers/generate_constant.rs b/crates/ide-assists/src/handlers/generate_constant.rs
index 57bb679729..eccd7675fb 100644
--- a/crates/ide-assists/src/handlers/generate_constant.rs
+++ b/crates/ide-assists/src/handlers/generate_constant.rs
@@ -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)?;
diff --git a/crates/ide-assists/src/handlers/generate_enum_variant.rs b/crates/ide-assists/src/handlers/generate_enum_variant.rs
index cd037f7492..184f523e01 100644
--- a/crates/ide-assists/src/handlers/generate_enum_variant.rs
+++ b/crates/ide-assists/src/handlers/generate_enum_variant.rs
@@ -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))
}
diff --git a/crates/ide-assists/src/handlers/generate_function.rs b/crates/ide-assists/src/handlers/generate_function.rs
index 2372fe28e1..a5556878be 100644
--- a/crates/ide-assists/src/handlers/generate_function.rs
+++ b/crates/ide-assists/src/handlers/generate_function.rs
@@ -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 fn_arg_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()
}
}
diff --git a/crates/ide-assists/src/handlers/promote_local_to_const.rs b/crates/ide-assists/src/handlers/promote_local_to_const.rs
index cbbea6c1e6..23153b4c56 100644
--- a/crates/ide-assists/src/handlers/promote_local_to_const.rs
+++ b/crates/ide-assists/src/handlers/promote_local_to_const.rs
@@ -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"
diff --git a/crates/ide-assists/src/handlers/replace_turbofish_with_explicit_type.rs b/crates/ide-assists/src/handlers/replace_turbofish_with_explicit_type.rs
index 6626ce0795..43a97d7d3a 100644
--- a/crates/ide-assists/src/handlers/replace_turbofish_with_explicit_type.rs
+++ b/crates/ide-assists/src/handlers/replace_turbofish_with_explicit_type.rs
@@ -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);
diff --git a/crates/ide-completion/src/completions/fn_param.rs b/crates/ide-completion/src/completions/fn_param.rs
index d8b8a190eb..734e1bed8d 100644
--- a/crates/ide-completion/src/completions/fn_param.rs
+++ b/crates/ide-completion/src/completions/fn_param.rs
@@ -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);
}
}
diff --git a/crates/ide-completion/src/completions/type.rs b/crates/ide-completion/src/completions/type.rs
index 2ad9520cd6..e470547563 100644
--- a/crates/ide-completion/src/completions/type.rs
+++ b/crates/ide-completion/src/completions/type.rs
@@ -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
}
diff --git a/crates/ide-db/src/path_transform.rs b/crates/ide-db/src/path_transform.rs
index 6402a84a68..0ee627a44c 100644
--- a/crates/ide-db/src/path_transform.rs
+++ b/crates/ide-db/src/path_transform.rs
@@ -116,7 +116,9 @@ impl<'a> PathTransform<'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()?,
),
))
}
diff --git a/crates/ide-diagnostics/src/handlers/missing_fields.rs b/crates/ide-diagnostics/src/handlers/missing_fields.rs
index 5c4327ff93..a33a2cd85e 100644
--- a/crates/ide-diagnostics/src/handlers/missing_fields.rs
+++ b/crates/ide-diagnostics/src/handlers/missing_fields.rs
@@ -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)
diff --git a/crates/ide-diagnostics/src/handlers/no_such_field.rs b/crates/ide-diagnostics/src/handlers/no_such_field.rs
index 24c521ed1a..625c95ce20 100644
--- a/crates/ide-diagnostics/src/handlers/no_such_field.rs
+++ b/crates/ide-diagnostics/src/handlers/no_such_field.rs
@@ -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()?;
diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs
index e6360bc6ec..7a8edfea83 100644
--- a/crates/ide/src/inlay_hints.rs
+++ b/crates/ide/src/inlay_hints.rs
@@ -14,8 +14,9 @@ use smallvec::{smallvec, SmallVec};
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 label_of_ty(
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 @@ mod tests {
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(
diff --git a/crates/ide/src/inlay_hints/adjustment.rs b/crates/ide/src/inlay_hints/adjustment.rs
index 0b14609195..fea4b89b89 100644
--- a/crates/ide/src/inlay_hints/adjustment.rs
+++ b/crates/ide/src/inlay_hints/adjustment.rs
@@ -135,6 +135,7 @@ pub(super) fn hints(
))),
None,
),
+ text_edit: None,
});
}
if !postfix && needs_inner_parens {
diff --git a/crates/ide/src/inlay_hints/bind_pat.rs b/crates/ide/src/inlay_hints/bind_pat.rs
index 5f571d0448..a131427f5f 100644
--- a/crates/ide/src/inlay_hints/bind_pat.rs
+++ b/crates/ide/src/inlay_hints/bind_pat.rs
@@ -12,9 +12,10 @@ use syntax::{
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);
+}
+"#,
+ );
+ }
}
diff --git a/crates/ide/src/inlay_hints/binding_mode.rs b/crates/ide/src/inlay_hints/binding_mode.rs
index 5d9729263c..3d7f969aaa 100644
--- a/crates/ide/src/inlay_hints/binding_mode.rs
+++ b/crates/ide/src/inlay_hints/binding_mode.rs
@@ -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() => {
diff --git a/crates/ide/src/inlay_hints/chaining.rs b/crates/ide/src/inlay_hints/chaining.rs
index 11e6dc05fa..6db9b8b544 100644
--- a/crates/ide/src/inlay_hints/chaining.rs
+++ b/crates/ide/src/inlay_hints/chaining.rs
@@ -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,
},
]
"#]],
diff --git a/crates/ide/src/inlay_hints/closing_brace.rs b/crates/ide/src/inlay_hints/closing_brace.rs
index 14c11be54e..10b5acd064 100644
--- a/crates/ide/src/inlay_hints/closing_brace.rs
+++ b/crates/ide/src/inlay_hints/closing_brace.rs
@@ -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
diff --git a/crates/ide/src/inlay_hints/closure_ret.rs b/crates/ide/src/inlay_hints/closure_ret.rs
index f03a18b8e9..6214e9c8e7 100644
--- a/crates/ide/src/inlay_hints/closure_ret.rs
+++ b/crates/ide/src/inlay_hints/closure_ret.rs
@@ -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(())
}
diff --git a/crates/ide/src/inlay_hints/discriminant.rs b/crates/ide/src/inlay_hints/discriminant.rs
index 67eaa553ad..f9047efaf1 100644
--- a/crates/ide/src/inlay_hints/discriminant.rs
+++ b/crates/ide/src/inlay_hints/discriminant.rs
@@ -75,6 +75,7 @@ fn variant_hints(
})),
None,
),
+ text_edit: None,
});
Some(())
diff --git a/crates/ide/src/inlay_hints/fn_lifetime_fn.rs b/crates/ide/src/inlay_hints/fn_lifetime_fn.rs
index b7182085b3..34eb5eb94c 100644
--- a/crates/ide/src/inlay_hints/fn_lifetime_fn.rs
+++ b/crates/ide/src/inlay_hints/fn_lifetime_fn.rs
@@ -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(())
diff --git a/crates/ide/src/inlay_hints/implicit_static.rs b/crates/ide/src/inlay_hints/implicit_static.rs
index 1122ee2e39..ba875649f7 100644
--- a/crates/ide/src/inlay_hints/implicit_static.rs
+++ b/crates/ide/src/inlay_hints/implicit_static.rs
@@ -34,6 +34,7 @@ pub(super) fn hints(
range: t.text_range(),
kind: InlayKind::Lifetime,
label: "'static".to_owned().into(),
+ text_edit: None,
});
}
}
diff --git a/crates/ide/src/inlay_hints/param_name.rs b/crates/ide/src/inlay_hints/param_name.rs
index 9cdae63241..9729a43c22 100644
--- a/crates/ide/src/inlay_hints/param_name.rs
+++ b/crates/ide/src/inlay_hints/param_name.rs
@@ -57,6 +57,7 @@ pub(super) fn hints(
range,
kind: InlayKind::Parameter,
label: InlayHintLabel::simple(param_name, None, linked_location),
+ text_edit: None,
}
});
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index 2b9dfeccef..cc72c2e10b 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -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,