mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-28 14:03:35 +00:00
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:
commit
1ee88db412
27 changed files with 374 additions and 49 deletions
|
@ -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) => {
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
|
|
@ -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}`"),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)?;
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()?,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()?;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -135,6 +135,7 @@ pub(super) fn hints(
|
|||
))),
|
||||
None,
|
||||
),
|
||||
text_edit: None,
|
||||
});
|
||||
}
|
||||
if !postfix && needs_inner_parens {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() => {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]
|
||||
"#]],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ fn variant_hints(
|
|||
})),
|
||||
None,
|
||||
),
|
||||
text_edit: None,
|
||||
});
|
||||
|
||||
Some(())
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -34,6 +34,7 @@ pub(super) fn hints(
|
|||
range: t.text_range(),
|
||||
kind: InlayKind::Lifetime,
|
||||
label: "'static".to_owned().into(),
|
||||
text_edit: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ pub(super) fn hints(
|
|||
range,
|
||||
kind: InlayKind::Parameter,
|
||||
label: InlayHintLabel::simple(param_name, None, linked_location),
|
||||
text_edit: None,
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue