Auto merge of #14742 - Veykril:closure-capture-inlays, r=Veykril

feat: Closure capture inlay hints

I opted for a fictional `move(foo, &bar, &mut qux)` syntax here, disabled by default as these are not correct rust syntax and hence could cause confusion.
![image](https://user-images.githubusercontent.com/3757771/236447484-649a4ea6-ad61-496e-bad8-765a5236150e.png)
This commit is contained in:
bors 2023-05-08 09:52:29 +00:00
commit d3ce333ec8
11 changed files with 278 additions and 37 deletions

View File

@ -148,7 +148,7 @@ impl HirPlace {
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum CaptureKind { pub enum CaptureKind {
ByRef(BorrowKind), ByRef(BorrowKind),
ByValue, ByValue,
} }
@ -166,23 +166,11 @@ impl CapturedItem {
self.place.local self.place.local
} }
pub fn display_kind(&self) -> &'static str { pub fn kind(&self) -> CaptureKind {
match self.kind { self.kind
CaptureKind::ByRef(k) => match k {
BorrowKind::Shared => "immutable borrow",
BorrowKind::Shallow => {
never!("shallow borrow should not happen in closure captures");
"shallow borrow"
},
BorrowKind::Unique => "unique immutable borrow ([read more](https://doc.rust-lang.org/stable/reference/types/closure.html#unique-immutable-borrows-in-captures))",
BorrowKind::Mut { .. } => "mutable borrow",
},
CaptureKind::ByValue => "move",
}
} }
pub fn display_place(&self, owner: ClosureId, db: &dyn HirDatabase) -> String { pub fn display_place(&self, owner: DefWithBodyId, db: &dyn HirDatabase) -> String {
let owner = db.lookup_intern_closure(owner.into()).0;
let body = db.body(owner); let body = db.body(owner);
let mut result = body[self.place.local].name.to_string(); let mut result = body[self.place.local].name.to_string();
let mut field_need_paren = false; let mut field_need_paren = false;

View File

@ -61,8 +61,9 @@ pub use autoderef::autoderef;
pub use builder::{ParamKind, TyBuilder}; pub use builder::{ParamKind, TyBuilder};
pub use chalk_ext::*; pub use chalk_ext::*;
pub use infer::{ pub use infer::{
closure::CapturedItem, could_coerce, could_unify, Adjust, Adjustment, AutoBorrow, BindingMode, closure::{CaptureKind, CapturedItem},
InferenceDiagnostic, InferenceResult, OverloadedDeref, PointerCast, could_coerce, could_unify, Adjust, Adjustment, AutoBorrow, BindingMode, InferenceDiagnostic,
InferenceResult, OverloadedDeref, PointerCast,
}; };
pub use interner::Interner; pub use interner::Interner;
pub use lower::{ pub use lower::{

View File

@ -2611,6 +2611,10 @@ impl LocalSource {
self.source.file_id.original_file(db.upcast()) self.source.file_id.original_file(db.upcast())
} }
pub fn file(&self) -> HirFileId {
self.source.file_id
}
pub fn name(&self) -> Option<ast::Name> { pub fn name(&self) -> Option<ast::Name> {
self.source.value.name() self.source.value.name()
} }
@ -3210,7 +3214,11 @@ impl Closure {
let owner = db.lookup_intern_closure((self.id).into()).0; let owner = db.lookup_intern_closure((self.id).into()).0;
let infer = &db.infer(owner); let infer = &db.infer(owner);
let info = infer.closure_info(&self.id); let info = infer.closure_info(&self.id);
info.0.iter().cloned().map(|capture| ClosureCapture { owner, capture }).collect() info.0
.iter()
.cloned()
.map(|capture| ClosureCapture { owner, closure: self.id, capture })
.collect()
} }
pub fn fn_trait(&self, db: &dyn HirDatabase) -> FnTrait { pub fn fn_trait(&self, db: &dyn HirDatabase) -> FnTrait {
@ -3224,6 +3232,7 @@ impl Closure {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClosureCapture { pub struct ClosureCapture {
owner: DefWithBodyId, owner: DefWithBodyId,
closure: ClosureId,
capture: hir_ty::CapturedItem, capture: hir_ty::CapturedItem,
} }
@ -3232,15 +3241,33 @@ impl ClosureCapture {
Local { parent: self.owner, binding_id: self.capture.local() } Local { parent: self.owner, binding_id: self.capture.local() }
} }
pub fn display_kind(&self) -> &'static str { pub fn kind(&self) -> CaptureKind {
self.capture.display_kind() match self.capture.kind() {
hir_ty::CaptureKind::ByRef(
hir_ty::mir::BorrowKind::Shallow | hir_ty::mir::BorrowKind::Shared,
) => CaptureKind::SharedRef,
hir_ty::CaptureKind::ByRef(hir_ty::mir::BorrowKind::Unique) => {
CaptureKind::UniqueSharedRef
}
hir_ty::CaptureKind::ByRef(hir_ty::mir::BorrowKind::Mut { .. }) => {
CaptureKind::MutableRef
}
hir_ty::CaptureKind::ByValue => CaptureKind::Move,
}
} }
pub fn display_place(&self, owner: ClosureId, db: &dyn HirDatabase) -> String { pub fn display_place(&self, db: &dyn HirDatabase) -> String {
self.capture.display_place(owner, db) self.capture.display_place(self.owner, db)
} }
} }
pub enum CaptureKind {
SharedRef,
UniqueSharedRef,
MutableRef,
Move,
}
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct Type { pub struct Type {
env: Arc<TraitEnvironment>, env: Arc<TraitEnvironment>,

View File

@ -3,7 +3,8 @@ use std::fmt::Display;
use either::Either; use either::Either;
use hir::{ use hir::{
Adt, AsAssocItem, AttributeTemplate, HasAttrs, HasSource, HirDisplay, Semantics, TypeInfo, Adt, AsAssocItem, AttributeTemplate, CaptureKind, HasAttrs, HasSource, HirDisplay, Semantics,
TypeInfo,
}; };
use ide_db::{ use ide_db::{
base_db::SourceDatabase, base_db::SourceDatabase,
@ -58,8 +59,14 @@ pub(super) fn closure_expr(
let mut captures = c let mut captures = c
.captured_items(sema.db) .captured_items(sema.db)
.into_iter() .into_iter()
.map(|x| { .map(|it| {
format!("* `{}` by {}", x.display_place(c.clone().into(), sema.db), x.display_kind()) let borrow_kind= match it.kind() {
CaptureKind::SharedRef => "immutable borrow",
CaptureKind::UniqueSharedRef => "unique immutable borrow ([read more](https://doc.rust-lang.org/stable/reference/types/closure.html#unique-immutable-borrows-in-captures))",
CaptureKind::MutableRef => "mutable borrow",
CaptureKind::Move => "move",
};
format!("* `{}` by {}", it.display_place(sema.db), borrow_kind)
}) })
.join("\n"); .join("\n");
if captures.trim().is_empty() { if captures.trim().is_empty() {

View File

@ -20,16 +20,17 @@ use text_edit::TextEdit;
use crate::{navigation_target::TryToNav, FileId}; use crate::{navigation_target::TryToNav, FileId};
mod closing_brace;
mod implicit_static;
mod fn_lifetime_fn;
mod closure_ret;
mod adjustment; mod adjustment;
mod chaining;
mod param_name;
mod binding_mode;
mod bind_pat; mod bind_pat;
mod binding_mode;
mod chaining;
mod closing_brace;
mod closure_ret;
mod closure_captures;
mod discriminant; mod discriminant;
mod fn_lifetime_fn;
mod implicit_static;
mod param_name;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct InlayHintsConfig { pub struct InlayHintsConfig {
@ -42,6 +43,7 @@ pub struct InlayHintsConfig {
pub adjustment_hints_mode: AdjustmentHintsMode, pub adjustment_hints_mode: AdjustmentHintsMode,
pub adjustment_hints_hide_outside_unsafe: bool, pub adjustment_hints_hide_outside_unsafe: bool,
pub closure_return_type_hints: ClosureReturnTypeHints, pub closure_return_type_hints: ClosureReturnTypeHints,
pub closure_capture_hints: bool,
pub binding_mode_hints: bool, pub binding_mode_hints: bool,
pub lifetime_elision_hints: LifetimeElisionHints, pub lifetime_elision_hints: LifetimeElisionHints,
pub param_names_for_lifetime_elision_hints: bool, pub param_names_for_lifetime_elision_hints: bool,
@ -88,6 +90,8 @@ pub enum AdjustmentHintsMode {
PreferPostfix, PreferPostfix,
} }
// FIXME: Clean up this mess, the kinds are mainly used for setting different rendering properties in the lsp layer
// We should probably turns this into such a property holding struct. Or clean this up in some other form.
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum InlayKind { pub enum InlayKind {
BindingMode, BindingMode,
@ -98,6 +102,7 @@ pub enum InlayKind {
Adjustment, Adjustment,
AdjustmentPostfix, AdjustmentPostfix,
Lifetime, Lifetime,
ClosureCapture,
Parameter, Parameter,
Type, Type,
Discriminant, Discriminant,
@ -444,10 +449,10 @@ fn hints(
ast::Expr::MethodCallExpr(it) => { ast::Expr::MethodCallExpr(it) => {
param_name::hints(hints, sema, config, ast::Expr::from(it)) param_name::hints(hints, sema, config, ast::Expr::from(it))
} }
ast::Expr::ClosureExpr(it) => closure_ret::hints(hints, famous_defs, config, file_id, it), ast::Expr::ClosureExpr(it) => {
// We could show reborrows for all expressions, but usually that is just noise to the user closure_captures::hints(hints, famous_defs, config, file_id, it.clone());
// and the main point here is to show why "moving" a mutable reference doesn't necessarily move it closure_ret::hints(hints, famous_defs, config, file_id, it)
// ast::Expr::PathExpr(_) => reborrow_hints(hints, sema, config, &expr), },
_ => None, _ => None,
} }
}, },
@ -535,6 +540,7 @@ mod tests {
chaining_hints: false, chaining_hints: false,
lifetime_elision_hints: LifetimeElisionHints::Never, lifetime_elision_hints: LifetimeElisionHints::Never,
closure_return_type_hints: ClosureReturnTypeHints::Never, closure_return_type_hints: ClosureReturnTypeHints::Never,
closure_capture_hints: false,
adjustment_hints: AdjustmentHints::Never, adjustment_hints: AdjustmentHints::Never,
adjustment_hints_mode: AdjustmentHintsMode::Prefix, adjustment_hints_mode: AdjustmentHintsMode::Prefix,
adjustment_hints_hide_outside_unsafe: false, adjustment_hints_hide_outside_unsafe: false,

View File

@ -0,0 +1,192 @@
//! 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 text_edit::{TextRange, TextSize};
use crate::{InlayHint, InlayHintLabel, InlayHintsConfig, InlayKind};
pub(super) fn hints(
acc: &mut Vec<InlayHint>,
FamousDefs(sema, _): &FamousDefs<'_, '_>,
config: &InlayHintsConfig,
_file_id: FileId,
closure: ast::ClosureExpr,
) -> Option<()> {
if !config.closure_capture_hints {
return None;
}
let ty = &sema.type_of_expr(&closure.clone().into())?.original;
let c = ty.as_closure()?;
let captures = c.captured_items(sema.db);
if captures.is_empty() {
return None;
}
let move_kw_range = match closure.move_token() {
Some(t) => t.text_range(),
None => {
let range = closure.syntax().first_token()?.prev_token()?.text_range();
let range = TextRange::new(range.end() - TextSize::from(1), range.end());
acc.push(InlayHint {
range,
kind: InlayKind::ClosureCapture,
label: InlayHintLabel::simple("move", None, None),
text_edit: None,
});
range
}
};
acc.push(InlayHint {
range: move_kw_range,
kind: InlayKind::ClosureCapture,
label: InlayHintLabel::from("("),
text_edit: None,
});
let last = captures.len() - 1;
for (idx, capture) in captures.into_iter().enumerate() {
let local = capture.local();
let source = local.primary_source(sema.db);
// force cache the source file, otherwise sema lookup will potentially panic
_ = sema.parse_or_expand(source.file());
acc.push(InlayHint {
range: move_kw_range,
kind: InlayKind::ClosureCapture,
label: InlayHintLabel::simple(
format!(
"{}{}",
match capture.kind() {
hir::CaptureKind::SharedRef => "&",
hir::CaptureKind::UniqueSharedRef => "&unique ",
hir::CaptureKind::MutableRef => "&mut ",
hir::CaptureKind::Move => "",
},
capture.display_place(sema.db)
),
None,
source.name().and_then(|name| sema.original_range_opt(name.syntax())),
),
text_edit: None,
});
if idx != last {
acc.push(InlayHint {
range: move_kw_range,
kind: InlayKind::ClosureCapture,
label: InlayHintLabel::simple(", ", None, None),
text_edit: None,
});
}
}
acc.push(InlayHint {
range: move_kw_range,
kind: InlayKind::ClosureCapture,
label: InlayHintLabel::from(")"),
text_edit: None,
});
Some(())
}
#[cfg(test)]
mod tests {
use crate::{
inlay_hints::tests::{check_with_config, DISABLED_CONFIG},
InlayHintsConfig,
};
#[test]
fn all_capture_kinds() {
check_with_config(
InlayHintsConfig { closure_capture_hints: true, ..DISABLED_CONFIG },
r#"
//- minicore: copy, derive
#[derive(Copy, Clone)]
struct Copy;
struct NonCopy;
fn main() {
let foo = Copy;
let bar = NonCopy;
let mut baz = NonCopy;
let qux = &mut NonCopy;
|| {
// ^ move
// ^ (
// ^ &foo
// ^ , $
// ^ bar
// ^ , $
// ^ baz
// ^ , $
// ^ qux
// ^ )
foo;
bar;
baz;
qux;
};
|| {
// ^ move
// ^ (
// ^ &foo
// ^ , $
// ^ &bar
// ^ , $
// ^ &baz
// ^ , $
// ^ &qux
// ^ )
&foo;
&bar;
&baz;
&qux;
};
|| {
// ^ move
// ^ (
// ^ &mut baz
// ^ )
&mut baz;
};
|| {
// ^ move
// ^ (
// ^ &mut baz
// ^ , $
// ^ &mut *qux
// ^ )
baz = NonCopy;
*qux = NonCopy;
};
}
"#,
);
}
#[test]
fn move_token() {
check_with_config(
InlayHintsConfig { closure_capture_hints: true, ..DISABLED_CONFIG },
r#"
//- minicore: copy, derive
fn main() {
let foo = u32;
move || {
// ^^^^ (
// ^^^^ foo
// ^^^^ )
foo;
};
}
"#,
);
}
}

View File

@ -122,6 +122,7 @@ impl StaticIndex<'_> {
param_names_for_lifetime_elision_hints: false, param_names_for_lifetime_elision_hints: false,
binding_mode_hints: false, binding_mode_hints: false,
max_length: Some(25), max_length: Some(25),
closure_capture_hints: false,
closing_brace_hints_min_lines: Some(25), closing_brace_hints_min_lines: Some(25),
}, },
file_id, file_id,

View File

@ -340,6 +340,8 @@ config_data! {
/// Minimum number of lines required before the `}` until the hint is shown (set to 0 or 1 /// Minimum number of lines required before the `}` until the hint is shown (set to 0 or 1
/// to always show them). /// to always show them).
inlayHints_closingBraceHints_minLines: usize = "25", inlayHints_closingBraceHints_minLines: usize = "25",
/// Whether to show inlay hints for closure captures.
inlayHints_closureCaptureHints_enable: bool = "false",
/// Whether to show inlay type hints for return types of closures. /// Whether to show inlay type hints for return types of closures.
inlayHints_closureReturnTypeHints_enable: ClosureReturnTypeHintsDef = "\"never\"", inlayHints_closureReturnTypeHints_enable: ClosureReturnTypeHintsDef = "\"never\"",
/// Closure notation in type and chaining inlay hints. /// Closure notation in type and chaining inlay hints.
@ -1314,6 +1316,7 @@ impl Config {
ClosureStyle::WithId => hir::ClosureStyle::ClosureWithId, ClosureStyle::WithId => hir::ClosureStyle::ClosureWithId,
ClosureStyle::Hide => hir::ClosureStyle::Hide, ClosureStyle::Hide => hir::ClosureStyle::Hide,
}, },
closure_capture_hints: self.data.inlayHints_closureCaptureHints_enable,
adjustment_hints: match self.data.inlayHints_expressionAdjustmentHints_enable { adjustment_hints: match self.data.inlayHints_expressionAdjustmentHints_enable {
AdjustmentHintsDef::Always => ide::AdjustmentHints::Always, AdjustmentHintsDef::Always => ide::AdjustmentHints::Always,
AdjustmentHintsDef::Never => match self.data.inlayHints_reborrowHints_enable { AdjustmentHintsDef::Never => match self.data.inlayHints_reborrowHints_enable {

View File

@ -456,6 +456,7 @@ pub(crate) fn inlay_hint(
| InlayKind::BindingMode => position(line_index, inlay_hint.range.start()), | InlayKind::BindingMode => position(line_index, inlay_hint.range.start()),
// after annotated thing // after annotated thing
InlayKind::ClosureReturnType InlayKind::ClosureReturnType
| InlayKind::ClosureCapture
| InlayKind::Type | InlayKind::Type
| InlayKind::Discriminant | InlayKind::Discriminant
| InlayKind::Chaining | InlayKind::Chaining
@ -469,6 +470,7 @@ pub(crate) fn inlay_hint(
InlayKind::Type => !render_colons, InlayKind::Type => !render_colons,
InlayKind::Chaining | InlayKind::ClosingBrace => true, InlayKind::Chaining | InlayKind::ClosingBrace => true,
InlayKind::ClosingParenthesis InlayKind::ClosingParenthesis
| InlayKind::ClosureCapture
| InlayKind::Discriminant | InlayKind::Discriminant
| InlayKind::OpeningParenthesis | InlayKind::OpeningParenthesis
| InlayKind::BindingMode | InlayKind::BindingMode
@ -490,6 +492,9 @@ pub(crate) fn inlay_hint(
| InlayKind::Type | InlayKind::Type
| InlayKind::Discriminant | InlayKind::Discriminant
| InlayKind::ClosingBrace => false, | InlayKind::ClosingBrace => false,
InlayKind::ClosureCapture => {
matches!(&label, lsp_types::InlayHintLabel::String(s) if s == ")")
}
InlayKind::BindingMode => { InlayKind::BindingMode => {
matches!(&label, lsp_types::InlayHintLabel::String(s) if s != "&") matches!(&label, lsp_types::InlayHintLabel::String(s) if s != "&")
} }
@ -501,6 +506,7 @@ pub(crate) fn inlay_hint(
Some(lsp_types::InlayHintKind::TYPE) Some(lsp_types::InlayHintKind::TYPE)
} }
InlayKind::ClosingParenthesis InlayKind::ClosingParenthesis
| InlayKind::ClosureCapture
| InlayKind::Discriminant | InlayKind::Discriminant
| InlayKind::OpeningParenthesis | InlayKind::OpeningParenthesis
| InlayKind::BindingMode | InlayKind::BindingMode

View File

@ -479,6 +479,11 @@ Whether to show inlay hints after a closing `}` to indicate what item it belongs
Minimum number of lines required before the `}` until the hint is shown (set to 0 or 1 Minimum number of lines required before the `}` until the hint is shown (set to 0 or 1
to always show them). to always show them).
-- --
[[rust-analyzer.inlayHints.closureCaptureHints.enable]]rust-analyzer.inlayHints.closureCaptureHints.enable (default: `false`)::
+
--
Whether to show inlay hints for closure captures.
--
[[rust-analyzer.inlayHints.closureReturnTypeHints.enable]]rust-analyzer.inlayHints.closureReturnTypeHints.enable (default: `"never"`):: [[rust-analyzer.inlayHints.closureReturnTypeHints.enable]]rust-analyzer.inlayHints.closureReturnTypeHints.enable (default: `"never"`)::
+ +
-- --

View File

@ -1037,6 +1037,11 @@
"type": "integer", "type": "integer",
"minimum": 0 "minimum": 0
}, },
"rust-analyzer.inlayHints.closureCaptureHints.enable": {
"markdownDescription": "Whether to show inlay hints for closure captures.",
"default": false,
"type": "boolean"
},
"rust-analyzer.inlayHints.closureReturnTypeHints.enable": { "rust-analyzer.inlayHints.closureReturnTypeHints.enable": {
"markdownDescription": "Whether to show inlay type hints for return types of closures.", "markdownDescription": "Whether to show inlay type hints for return types of closures.",
"default": "never", "default": "never",