Implement text edits for inlay hints

This commit is contained in:
Ryo Yoshida 2023-04-08 04:06:32 +09:00
parent fcbc250723
commit c978d4bf0c
No known key found for this signature in database
GPG Key ID: E25698A930586171
4 changed files with 262 additions and 17 deletions

View File

@ -14,7 +14,7 @@ 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;
@ -359,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.
@ -566,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(

View File

@ -13,7 +13,7 @@ use syntax::{
};
use crate::{
inlay_hints::{closure_has_block_body, label_of_ty},
inlay_hints::{closure_has_block_body, label_of_ty, ty_to_text_edit},
InlayHint, InlayHintsConfig, InlayKind,
};
@ -36,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()
@ -44,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(),
@ -51,7 +68,7 @@ pub(super) fn hints(
},
kind: InlayKind::Type,
label,
text_edit: None,
text_edit,
});
Some(())
@ -178,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) {
@ -1014,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

@ -603,7 +603,16 @@ fn main() {
},
"",
],
text_edit: None,
text_edit: Some(
TextEdit {
indels: [
Indel {
insert: ": Struct",
delete: 130..130,
},
],
},
),
},
InlayHint {
range: 145..185,

View File

@ -1,14 +1,14 @@
//! Implementation of "closure return type" inlay hints.
//!
//! Tests live in [`bind_pat`][super::bind_pat] module.
use ide_db::{base_db::FileId, famous_defs::FamousDefs};
use syntax::ast::{self, AstNode};
use crate::{
inlay_hints::closure_has_block_body, ClosureReturnTypeHints, InlayHint, InlayHintsConfig,
InlayKind,
inlay_hints::{closure_has_block_body, label_of_ty, ty_to_text_edit},
ClosureReturnTypeHints, InlayHint, InlayHintsConfig, InlayKind,
};
use super::label_of_ty;
pub(super) fn hints(
acc: &mut Vec<InlayHint>,
famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,
@ -24,26 +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: None,
text_edit,
});
Some(())
}