Unnamed repository; edit this file 'description' to name the repository.
Implement text edits for inlay hints
Ryo Yoshida 2023-04-12
parent fcbc250 · commit c978d4b
-rw-r--r--crates/ide/src/inlay_hints.rs50
-rw-r--r--crates/ide/src/inlay_hints/bind_pat.rs187
-rw-r--r--crates/ide/src/inlay_hints/chaining.rs11
-rw-r--r--crates/ide/src/inlay_hints/closure_ret.rs31
4 files changed, 262 insertions, 17 deletions
diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs
index 42009ea649..7a8edfea83 100644
--- a/crates/ide/src/inlay_hints.rs
+++ b/crates/ide/src/inlay_hints.rs
@@ -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(
diff --git a/crates/ide/src/inlay_hints/bind_pat.rs b/crates/ide/src/inlay_hints/bind_pat.rs
index 4cd1fce8c9..a131427f5f 100644
--- a/crates/ide/src/inlay_hints/bind_pat.rs
+++ b/crates/ide/src/inlay_hints/bind_pat.rs
@@ -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);
+}
+"#,
+ );
+ }
}
diff --git a/crates/ide/src/inlay_hints/chaining.rs b/crates/ide/src/inlay_hints/chaining.rs
index eed5a2554c..6db9b8b544 100644
--- a/crates/ide/src/inlay_hints/chaining.rs
+++ b/crates/ide/src/inlay_hints/chaining.rs
@@ -603,7 +603,16 @@ fn main() {
},
"",
],
- text_edit: None,
+ text_edit: Some(
+ TextEdit {
+ indels: [
+ Indel {
+ insert: ": Struct",
+ delete: 130..130,
+ },
+ ],
+ },
+ ),
},
InlayHint {
range: 145..185,
diff --git a/crates/ide/src/inlay_hints/closure_ret.rs b/crates/ide/src/inlay_hints/closure_ret.rs
index eb5a464e7c..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,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(())
}