From 8081a654dac2e174c465263292616858e7f7ac1b Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 5 May 2023 13:34:55 +0200 Subject: [PATCH] feat: Closure capture inlay hints --- crates/hir-ty/src/infer/closure.rs | 6 +- crates/hir-ty/src/lib.rs | 5 +- crates/hir/src/lib.rs | 26 +++ crates/ide/src/inlay_hints.rs | 28 ++- .../ide/src/inlay_hints/closure_captures.rs | 193 ++++++++++++++++++ crates/ide/src/static_index.rs | 1 + crates/rust-analyzer/src/config.rs | 3 + crates/rust-analyzer/src/to_proto.rs | 6 + docs/user/generated_config.adoc | 5 + editors/code/package.json | 5 + 10 files changed, 264 insertions(+), 14 deletions(-) create mode 100644 crates/ide/src/inlay_hints/closure_captures.rs diff --git a/crates/hir-ty/src/infer/closure.rs b/crates/hir-ty/src/infer/closure.rs index df2ad7af343..a2c72e5751d 100644 --- a/crates/hir-ty/src/infer/closure.rs +++ b/crates/hir-ty/src/infer/closure.rs @@ -148,7 +148,7 @@ impl HirPlace { } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum CaptureKind { +pub enum CaptureKind { ByRef(BorrowKind), ByValue, } @@ -166,6 +166,10 @@ impl CapturedItem { self.place.local } + pub fn kind(&self) -> CaptureKind { + self.kind + } + pub fn display_kind(&self) -> &'static str { match self.kind { CaptureKind::ByRef(k) => match k { diff --git a/crates/hir-ty/src/lib.rs b/crates/hir-ty/src/lib.rs index 6e726042f6c..28a2bf2838d 100644 --- a/crates/hir-ty/src/lib.rs +++ b/crates/hir-ty/src/lib.rs @@ -61,8 +61,9 @@ pub use autoderef::autoderef; pub use builder::{ParamKind, TyBuilder}; pub use chalk_ext::*; pub use infer::{ - closure::CapturedItem, could_coerce, could_unify, Adjust, Adjustment, AutoBorrow, BindingMode, - InferenceDiagnostic, InferenceResult, OverloadedDeref, PointerCast, + closure::{CaptureKind, CapturedItem}, + could_coerce, could_unify, Adjust, Adjustment, AutoBorrow, BindingMode, InferenceDiagnostic, + InferenceResult, OverloadedDeref, PointerCast, }; pub use interner::Interner; pub use lower::{ diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index b3a8a33cac9..84608777054 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -2611,6 +2611,10 @@ impl LocalSource { self.source.file_id.original_file(db.upcast()) } + pub fn file(&self) -> HirFileId { + self.source.file_id + } + pub fn name(&self) -> Option { self.source.value.name() } @@ -3232,6 +3236,21 @@ impl ClosureCapture { Local { parent: self.owner, binding_id: self.capture.local() } } + pub fn kind(&self) -> CaptureKind { + 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_kind(&self) -> &'static str { self.capture.display_kind() } @@ -3241,6 +3260,13 @@ impl ClosureCapture { } } +pub enum CaptureKind { + SharedRef, + UniqueSharedRef, + MutableRef, + Move, +} + #[derive(Clone, PartialEq, Eq, Debug)] pub struct Type { env: Arc, diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs index 7a8edfea835..c326688ae6a 100644 --- a/crates/ide/src/inlay_hints.rs +++ b/crates/ide/src/inlay_hints.rs @@ -20,16 +20,17 @@ use text_edit::TextEdit; use crate::{navigation_target::TryToNav, FileId}; -mod closing_brace; -mod implicit_static; -mod fn_lifetime_fn; -mod closure_ret; mod adjustment; -mod chaining; -mod param_name; -mod binding_mode; mod bind_pat; +mod binding_mode; +mod chaining; +mod closing_brace; +mod closure_ret; +mod closure_captures; mod discriminant; +mod fn_lifetime_fn; +mod implicit_static; +mod param_name; #[derive(Clone, Debug, PartialEq, Eq)] pub struct InlayHintsConfig { @@ -42,6 +43,7 @@ pub struct InlayHintsConfig { pub adjustment_hints_mode: AdjustmentHintsMode, pub adjustment_hints_hide_outside_unsafe: bool, pub closure_return_type_hints: ClosureReturnTypeHints, + pub closure_capture_hints: bool, pub binding_mode_hints: bool, pub lifetime_elision_hints: LifetimeElisionHints, pub param_names_for_lifetime_elision_hints: bool, @@ -88,6 +90,8 @@ pub enum AdjustmentHintsMode { 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)] pub enum InlayKind { BindingMode, @@ -98,6 +102,7 @@ pub enum InlayKind { Adjustment, AdjustmentPostfix, Lifetime, + ClosureCapture, Parameter, Type, Discriminant, @@ -444,10 +449,10 @@ fn hints( ast::Expr::MethodCallExpr(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), - // We could show reborrows for all expressions, but usually that is just noise to the user - // and the main point here is to show why "moving" a mutable reference doesn't necessarily move it - // ast::Expr::PathExpr(_) => reborrow_hints(hints, sema, config, &expr), + ast::Expr::ClosureExpr(it) => { + closure_captures::hints(hints, famous_defs, config, file_id, it.clone()); + closure_ret::hints(hints, famous_defs, config, file_id, it) + }, _ => None, } }, @@ -535,6 +540,7 @@ mod tests { chaining_hints: false, lifetime_elision_hints: LifetimeElisionHints::Never, closure_return_type_hints: ClosureReturnTypeHints::Never, + closure_capture_hints: false, adjustment_hints: AdjustmentHints::Never, adjustment_hints_mode: AdjustmentHintsMode::Prefix, adjustment_hints_hide_outside_unsafe: false, diff --git a/crates/ide/src/inlay_hints/closure_captures.rs b/crates/ide/src/inlay_hints/closure_captures.rs new file mode 100644 index 00000000000..ddede5239e5 --- /dev/null +++ b/crates/ide/src/inlay_hints/closure_captures.rs @@ -0,0 +1,193 @@ +//! 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, + 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 => "", + }, + local.name(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; + }; + // FIXME: &mut qux should be &unique qux + || { +// ^ 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; + }; +} +"#, + ); + } +} diff --git a/crates/ide/src/static_index.rs b/crates/ide/src/static_index.rs index 7ce20e973bb..4b6e7da9a35 100644 --- a/crates/ide/src/static_index.rs +++ b/crates/ide/src/static_index.rs @@ -122,6 +122,7 @@ impl StaticIndex<'_> { param_names_for_lifetime_elision_hints: false, binding_mode_hints: false, max_length: Some(25), + closure_capture_hints: false, closing_brace_hints_min_lines: Some(25), }, file_id, diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 51874382a8b..251d09d0f65 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -338,6 +338,8 @@ config_data! { /// Minimum number of lines required before the `}` until the hint is shown (set to 0 or 1 /// to always show them). 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. inlayHints_closureReturnTypeHints_enable: ClosureReturnTypeHintsDef = "\"never\"", /// Closure notation in type and chaining inlay hints. @@ -1312,6 +1314,7 @@ impl Config { ClosureStyle::WithId => hir::ClosureStyle::ClosureWithId, ClosureStyle::Hide => hir::ClosureStyle::Hide, }, + closure_capture_hints: self.data.inlayHints_closureCaptureHints_enable, adjustment_hints: match self.data.inlayHints_expressionAdjustmentHints_enable { AdjustmentHintsDef::Always => ide::AdjustmentHints::Always, AdjustmentHintsDef::Never => match self.data.inlayHints_reborrowHints_enable { diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs index 60292d2ad18..1b7fd558906 100644 --- a/crates/rust-analyzer/src/to_proto.rs +++ b/crates/rust-analyzer/src/to_proto.rs @@ -456,6 +456,7 @@ pub(crate) fn inlay_hint( | InlayKind::BindingMode => position(line_index, inlay_hint.range.start()), // after annotated thing InlayKind::ClosureReturnType + | InlayKind::ClosureCapture | InlayKind::Type | InlayKind::Discriminant | InlayKind::Chaining @@ -469,6 +470,7 @@ pub(crate) fn inlay_hint( InlayKind::Type => !render_colons, InlayKind::Chaining | InlayKind::ClosingBrace => true, InlayKind::ClosingParenthesis + | InlayKind::ClosureCapture | InlayKind::Discriminant | InlayKind::OpeningParenthesis | InlayKind::BindingMode @@ -490,6 +492,9 @@ pub(crate) fn inlay_hint( | InlayKind::Type | InlayKind::Discriminant | InlayKind::ClosingBrace => false, + InlayKind::ClosureCapture => { + matches!(&label, lsp_types::InlayHintLabel::String(s) if s == ")") + } InlayKind::BindingMode => { matches!(&label, lsp_types::InlayHintLabel::String(s) if s != "&") } @@ -501,6 +506,7 @@ pub(crate) fn inlay_hint( Some(lsp_types::InlayHintKind::TYPE) } InlayKind::ClosingParenthesis + | InlayKind::ClosureCapture | InlayKind::Discriminant | InlayKind::OpeningParenthesis | InlayKind::BindingMode diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc index aad6969671c..187be26f175 100644 --- a/docs/user/generated_config.adoc +++ b/docs/user/generated_config.adoc @@ -474,6 +474,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 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"`):: + -- diff --git a/editors/code/package.json b/editors/code/package.json index ca00da9f361..7330cf18b4d 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -1032,6 +1032,11 @@ "type": "integer", "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": { "markdownDescription": "Whether to show inlay type hints for return types of closures.", "default": "never",