Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide/src/inlay_hints.rs')
| -rw-r--r-- | crates/ide/src/inlay_hints.rs | 146 |
1 files changed, 115 insertions, 31 deletions
diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs index ac477339ec..2925916741 100644 --- a/crates/ide/src/inlay_hints.rs +++ b/crates/ide/src/inlay_hints.rs @@ -5,7 +5,8 @@ use std::{ use either::Either; use hir::{ - known, HasVisibility, HirDisplay, HirDisplayError, HirWrite, ModuleDef, ModuleDefId, Semantics, + known, ClosureStyle, HasVisibility, HirDisplay, HirDisplayError, HirWrite, ModuleDef, + ModuleDefId, Semantics, }; use ide_db::{base_db::FileRange, famous_defs::FamousDefs, RootDatabase}; use itertools::Itertools; @@ -13,21 +14,23 @@ 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; 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 { @@ -40,11 +43,13 @@ 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, pub hide_named_constructor_hints: bool, pub hide_closure_initialization_hints: bool, + pub closure_style: ClosureStyle, pub max_length: Option<usize>, pub closing_brace_hints_min_lines: Option<usize>, } @@ -87,38 +92,61 @@ pub enum AdjustmentHintsMode { #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum InlayKind { + Adjustment, BindingMode, Chaining, ClosingBrace, - ClosureReturnType, + ClosureCapture, + Discriminant, GenericParamList, - Adjustment, - AdjustmentPostfix, Lifetime, Parameter, Type, - Discriminant, - OpeningParenthesis, - ClosingParenthesis, +} + +#[derive(Debug)] +pub enum InlayHintPosition { + Before, + After, } #[derive(Debug)] pub struct InlayHint { /// The text range this inlay hint applies to. pub range: TextRange, - /// The kind of this inlay hint. This is used to determine side and padding of the hint for - /// rendering purposes. + pub position: InlayHintPosition, + pub pad_left: bool, + pub pad_right: bool, + /// The kind of this inlay hint. pub kind: InlayKind, /// The actual label to show in the inlay hint. pub label: InlayHintLabel, + /// Text edit to apply when "accepting" this inlay hint. + pub text_edit: Option<TextEdit>, } impl InlayHint { - fn closing_paren(range: TextRange) -> InlayHint { - InlayHint { range, kind: InlayKind::ClosingParenthesis, label: InlayHintLabel::from(")") } + fn closing_paren_after(kind: InlayKind, range: TextRange) -> InlayHint { + InlayHint { + range, + kind, + label: InlayHintLabel::from(")"), + text_edit: None, + position: InlayHintPosition::After, + pad_left: false, + pad_right: false, + } } - fn opening_paren(range: TextRange) -> InlayHint { - InlayHint { range, kind: InlayKind::OpeningParenthesis, label: InlayHintLabel::from("(") } + fn opening_paren_before(kind: InlayKind, range: TextRange) -> InlayHint { + InlayHint { + range, + kind, + label: InlayHintLabel::from("("), + text_edit: None, + position: InlayHintPosition::Before, + pad_left: false, + pad_right: false, + } } } @@ -283,14 +311,15 @@ impl InlayHintLabelBuilder<'_> { fn label_of_ty( famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>, config: &InlayHintsConfig, - ty: hir::Type, + ty: &hir::Type, ) -> Option<InlayHintLabel> { fn rec( sema: &Semantics<'_, RootDatabase>, famous_defs: &FamousDefs<'_, '_>, mut max_length: Option<usize>, - ty: hir::Type, + ty: &hir::Type, label_builder: &mut InlayHintLabelBuilder<'_>, + config: &InlayHintsConfig, ) -> Result<(), HirDisplayError> { let iter_item_type = hint_iterator(sema, famous_defs, &ty); match iter_item_type { @@ -321,11 +350,14 @@ fn label_of_ty( label_builder.write_str(LABEL_ITEM)?; label_builder.end_location_link(); label_builder.write_str(LABEL_MIDDLE2)?; - rec(sema, famous_defs, max_length, ty, label_builder)?; + rec(sema, famous_defs, max_length, &ty, label_builder, config)?; label_builder.write_str(LABEL_END)?; Ok(()) } - None => ty.display_truncated(sema.db, max_length).write_to(label_builder), + None => ty + .display_truncated(sema.db, max_length) + .with_closure_style(config.closure_style) + .write_to(label_builder), } } @@ -335,11 +367,28 @@ fn label_of_ty( location: None, result: InlayHintLabel::default(), }; - let _ = rec(sema, famous_defs, config.max_length, ty, &mut label_builder); + let _ = rec(sema, famous_defs, config.max_length, ty, &mut label_builder, config); let r = label_builder.finish(); 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. @@ -408,10 +457,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, } }, @@ -481,6 +530,7 @@ fn closure_has_block_body(closure: &ast::ClosureExpr) -> bool { #[cfg(test)] mod tests { use expect_test::Expect; + use hir::ClosureStyle; use itertools::Itertools; use test_utils::extract_annotations; @@ -498,12 +548,14 @@ 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, binding_mode_hints: false, hide_named_constructor_hints: false, hide_closure_initialization_hints: false, + closure_style: ClosureStyle::ImplFn, param_names_for_lifetime_elision_hints: false, max_length: None, closing_brace_hints_min_lines: None, @@ -530,7 +582,8 @@ mod tests { let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap(); let actual = inlay_hints .into_iter() - .map(|it| (it.range, it.label.to_string())) + // FIXME: We trim the start because some inlay produces leading whitespace which is not properly supported by our annotation extraction + .map(|it| (it.range, it.label.to_string().trim_start().to_owned())) .sorted_by_key(|(range, _)| range.start()) .collect::<Vec<_>>(); expected.sort_by_key(|(range, _)| range.start()); @@ -545,6 +598,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( |