mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-09-27 12:29:21 +00:00
Implement text edits for inlay hints
This commit is contained in:
parent
fcbc250723
commit
c978d4bf0c
4 changed files with 262 additions and 17 deletions
|
@ -14,7 +14,7 @@ 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 text_edit::TextEdit;
|
||||||
|
|
||||||
|
@ -359,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.
|
||||||
|
@ -566,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(
|
||||||
|
|
|
@ -13,7 +13,7 @@ use syntax::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
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,
|
InlayHint, InlayHintsConfig, InlayKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,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()
|
||||||
|
@ -44,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(),
|
||||||
|
@ -51,7 +68,7 @@ pub(super) fn hints(
|
||||||
},
|
},
|
||||||
kind: InlayKind::Type,
|
kind: InlayKind::Type,
|
||||||
label,
|
label,
|
||||||
text_edit: None,
|
text_edit,
|
||||||
});
|
});
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
|
@ -178,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) {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -603,7 +603,16 @@ fn main() {
|
||||||
},
|
},
|
||||||
"",
|
"",
|
||||||
],
|
],
|
||||||
text_edit: None,
|
text_edit: Some(
|
||||||
|
TextEdit {
|
||||||
|
indels: [
|
||||||
|
Indel {
|
||||||
|
insert: ": Struct",
|
||||||
|
delete: 130..130,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
InlayHint {
|
InlayHint {
|
||||||
range: 145..185,
|
range: 145..185,
|
||||||
|
|
|
@ -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,26 +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: None,
|
text_edit,
|
||||||
});
|
});
|
||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue