about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2023-01-09 13:47:46 +0000
committerbors <bors@rust-lang.org>2023-01-09 13:47:46 +0000
commitec968198b98c6291aac6a6aac41c1e194b14e95b (patch)
treedb052830452614e4125958923b43b0ed213f78f4
parentb0214d81e85c5bd82ae470f1291472a9eb8a0068 (diff)
parentb6169c2a2e5a24e9905a2614e6fe469fb37f50c9 (diff)
Auto merge of #13816 - WaffleLapkin:postfix_adjustment_hints, r=Veykril
Postfix adjustment hints

# Basic Description

This PR implements "postfix" adjustment hints:
![2022-12-21_19-27](https://user-images.githubusercontent.com/38225716/208941721-d48d316f-a918-408a-9757-8d4e2b402a66.png)

They are identical to normal adjustment hints, but are rendered _after_ the expression. E.g. `expr.*` instead of `*expr`. ~~This mirrors "postfix deref" feature that I'm planning to eventually propose to the compiler.~~

# Motivation

The advantage of being postfix is that you need to add parentheses less often:

![2022-12-21_19-38](https://user-images.githubusercontent.com/38225716/208944302-16718112-14a4-4438-8aed-797766391c63.png)
![2022-12-21_19-37](https://user-images.githubusercontent.com/38225716/208944281-d9614888-6597-41ee-bf5d-a081d8048f94.png)

This is because a lot of "reborrow" hints are caused by field access or method calls, both of which are postfix and have higher "precedence" than prefix `&` and `*`.

Also IMHO it just looks nicer and it's more clear what is happening (order of operations).

# Modes

However, there are some cases where postfix hints need parentheses but prefix don't (for example `&x` being turned into `(&x).*.*.&` or `&**&x`).

This PR allows users to choose which look they like more. There are 4 options (`rust-analyzer.inlayHints.expressionAdjustmentHints.mode` setting):
- `prefix` — always use prefix hints (default, what was used before that PR)
- `postfix` — always use postfix hints
- `prefer_prefix` — try to minimize number of parentheses, breaking ties in favor of prefix
- `prefer_postfix` — try to minimize number of parentheses, breaking ties in favor of postfix

Comparison of all modes:

![2022-12-21_19-53](https://user-images.githubusercontent.com/38225716/208947482-26357c82-2b42-47d9-acec-835f5f03f6b4.png)
![2022-12-21_19-49](https://user-images.githubusercontent.com/38225716/208946731-fe566d3b-52b2-4846-994d-c2cecc769e0f.png)
![2022-12-21_19-48](https://user-images.githubusercontent.com/38225716/208946742-6e237f44-805e-469a-a3db-03d8f76e1317.png)
![2022-12-21_19-47](https://user-images.githubusercontent.com/38225716/208946747-79f25fae-e3ea-47d2-8d27-cb4eeac034fe.png)

# Edge cases

Where are some rare cases where chain hints weirdly interact with adjustment hints, for example (note `SourceAnalyzer.&`):

![image](https://user-images.githubusercontent.com/38225716/208947958-41c12971-f1f0-4a41-a930-47939cce9f58.png)

This is pre-existing, you can get the same effect with prefix hints (`SourceAnalyzer)`).

----

Another weird thing is this:

![2022-12-21_20-00](https://user-images.githubusercontent.com/38225716/208948590-ea26d325-2108-4b35-abaa-716a65a1ae99.png)

Here `.&` is a hint and `?` is written in the source code. It looks like `?` is part of the hint because `?.` is ligature in my font. IMO this is a bug in vscode, but still worth mentioning (I'm also too lazy to report it there...).

# Fixed bugs

I've used the "needs parens" API and this accidentally fixed a bug with parens around `as`, see the test diff:
```diff,rust
     let _: *const u32  = &mut 0u32 as *mut u32;
                        //^^^^^^^^^^^^^^^^^^^^^<mut-ptr-to-const-ptr>
+                       //^^^^^^^^^^^^^^^^^^^^^(
+                       //^^^^^^^^^^^^^^^^^^^^^)
...
     let _: *const u32  = &mut 0u32 as *mut u32;
                        //^^^^^^^^^^^^^^^^^^^^^<mut-ptr-to-const-ptr>
+                       //^^^^^^^^^^^^^^^^^^^^^(
+                       //^^^^^^^^^^^^^^^^^^^^^)
```

# Changelog

changelog feature Add an option to make adjustment hints (aka reborrow hints) postfix
changelog fix Fix placement of parentheses around `as` casts for adjustment hints
-rw-r--r--crates/ide/src/inlay_hints.rs13
-rw-r--r--crates/ide/src/inlay_hints/adjustment.rs357
-rw-r--r--crates/ide/src/lib.rs4
-rw-r--r--crates/ide/src/static_index.rs2
-rw-r--r--crates/rust-analyzer/src/config.rs32
-rw-r--r--crates/rust-analyzer/src/to_proto.rs4
-rw-r--r--docs/user/generated_config.adoc5
-rw-r--r--editors/code/package.json17
8 files changed, 402 insertions, 32 deletions
diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs
index 86d25e2f5ad..48a7bbfecff 100644
--- a/crates/ide/src/inlay_hints.rs
+++ b/crates/ide/src/inlay_hints.rs
@@ -35,6 +35,7 @@ pub struct InlayHintsConfig {
     pub parameter_hints: bool,
     pub chaining_hints: bool,
     pub adjustment_hints: AdjustmentHints,
+    pub adjustment_hints_mode: AdjustmentHintsMode,
     pub adjustment_hints_hide_outside_unsafe: bool,
     pub closure_return_type_hints: ClosureReturnTypeHints,
     pub binding_mode_hints: bool,
@@ -74,6 +75,14 @@ pub enum AdjustmentHints {
     Never,
 }
 
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum AdjustmentHintsMode {
+    Prefix,
+    Postfix,
+    PreferPrefix,
+    PreferPostfix,
+}
+
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum InlayKind {
     BindingModeHint,
@@ -82,6 +91,7 @@ pub enum InlayKind {
     ClosureReturnTypeHint,
     GenericParamListHint,
     AdjustmentHint,
+    AdjustmentHintPostfix,
     LifetimeHint,
     ParameterHint,
     TypeHint,
@@ -430,7 +440,7 @@ mod tests {
     use itertools::Itertools;
     use test_utils::extract_annotations;
 
-    use crate::inlay_hints::AdjustmentHints;
+    use crate::inlay_hints::{AdjustmentHints, AdjustmentHintsMode};
     use crate::DiscriminantHints;
     use crate::{fixture, inlay_hints::InlayHintsConfig, LifetimeElisionHints};
 
@@ -446,6 +456,7 @@ mod tests {
         lifetime_elision_hints: LifetimeElisionHints::Never,
         closure_return_type_hints: ClosureReturnTypeHints::Never,
         adjustment_hints: AdjustmentHints::Never,
+        adjustment_hints_mode: AdjustmentHintsMode::Prefix,
         adjustment_hints_hide_outside_unsafe: false,
         binding_mode_hints: false,
         hide_named_constructor_hints: false,
diff --git a/crates/ide/src/inlay_hints/adjustment.rs b/crates/ide/src/inlay_hints/adjustment.rs
index 1c13f31cf24..bdd7c05e008 100644
--- a/crates/ide/src/inlay_hints/adjustment.rs
+++ b/crates/ide/src/inlay_hints/adjustment.rs
@@ -5,9 +5,13 @@
 //! ```
 use hir::{Adjust, AutoBorrow, Mutability, OverloadedDeref, PointerCast, Safety, Semantics};
 use ide_db::RootDatabase;
-use syntax::ast::{self, AstNode};
 
-use crate::{AdjustmentHints, InlayHint, InlayHintsConfig, InlayKind};
+use syntax::{
+    ast::{self, make, AstNode},
+    ted,
+};
+
+use crate::{AdjustmentHints, AdjustmentHintsMode, InlayHint, InlayHintsConfig, InlayKind};
 
 pub(super) fn hints(
     acc: &mut Vec<InlayHint>,
@@ -32,36 +36,47 @@ pub(super) fn hints(
         return None;
     }
 
-    let parent = expr.syntax().parent().and_then(ast::Expr::cast);
     let descended = sema.descend_node_into_attributes(expr.clone()).pop();
     let desc_expr = descended.as_ref().unwrap_or(expr);
     let adjustments = sema.expr_adjustments(desc_expr).filter(|it| !it.is_empty())?;
-    let needs_parens = match parent {
-        Some(parent) => {
-            match parent {
-                ast::Expr::AwaitExpr(_)
-                | ast::Expr::CallExpr(_)
-                | ast::Expr::CastExpr(_)
-                | ast::Expr::FieldExpr(_)
-                | ast::Expr::MethodCallExpr(_)
-                | ast::Expr::TryExpr(_) => true,
-                // FIXME: shorthands need special casing, though not sure if adjustments are even valid there
-                ast::Expr::RecordExpr(_) => false,
-                ast::Expr::IndexExpr(index) => index.base().as_ref() == Some(expr),
-                _ => false,
-            }
-        }
-        None => false,
-    };
-    if needs_parens {
+
+    let (postfix, needs_outer_parens, needs_inner_parens) =
+        mode_and_needs_parens_for_adjustment_hints(expr, config.adjustment_hints_mode);
+
+    if needs_outer_parens {
+        acc.push(InlayHint {
+            range: expr.syntax().text_range(),
+            kind: InlayKind::OpeningParenthesis,
+            label: "(".into(),
+            tooltip: None,
+        });
+    }
+
+    if postfix && needs_inner_parens {
         acc.push(InlayHint {
             range: expr.syntax().text_range(),
             kind: InlayKind::OpeningParenthesis,
             label: "(".into(),
             tooltip: None,
         });
+        acc.push(InlayHint {
+            range: expr.syntax().text_range(),
+            kind: InlayKind::ClosingParenthesis,
+            label: ")".into(),
+            tooltip: None,
+        });
     }
-    for adjustment in adjustments.into_iter().rev() {
+
+    let (mut tmp0, mut tmp1);
+    let iter: &mut dyn Iterator<Item = _> = if postfix {
+        tmp0 = adjustments.into_iter();
+        &mut tmp0
+    } else {
+        tmp1 = adjustments.into_iter().rev();
+        &mut tmp1
+    };
+
+    for adjustment in iter {
         if adjustment.source == adjustment.target {
             continue;
         }
@@ -97,12 +112,30 @@ pub(super) fn hints(
         };
         acc.push(InlayHint {
             range: expr.syntax().text_range(),
-            kind: InlayKind::AdjustmentHint,
-            label: text.into(),
+            kind: if postfix {
+                InlayKind::AdjustmentHintPostfix
+            } else {
+                InlayKind::AdjustmentHint
+            },
+            label: if postfix { format!(".{}", text.trim_end()).into() } else { text.into() },
+            tooltip: None,
+        });
+    }
+    if !postfix && needs_inner_parens {
+        acc.push(InlayHint {
+            range: expr.syntax().text_range(),
+            kind: InlayKind::OpeningParenthesis,
+            label: "(".into(),
+            tooltip: None,
+        });
+        acc.push(InlayHint {
+            range: expr.syntax().text_range(),
+            kind: InlayKind::ClosingParenthesis,
+            label: ")".into(),
             tooltip: None,
         });
     }
-    if needs_parens {
+    if needs_outer_parens {
         acc.push(InlayHint {
             range: expr.syntax().text_range(),
             kind: InlayKind::ClosingParenthesis,
@@ -113,11 +146,111 @@ pub(super) fn hints(
     Some(())
 }
 
+/// Returns whatever the hint should be postfix and if we need to add paretheses on the inside and/or outside of `expr`,
+/// if we are going to add (`postfix`) adjustments hints to it.
+fn mode_and_needs_parens_for_adjustment_hints(
+    expr: &ast::Expr,
+    mode: AdjustmentHintsMode,
+) -> (bool, bool, bool) {
+    use {std::cmp::Ordering::*, AdjustmentHintsMode::*};
+
+    match mode {
+        Prefix | Postfix => {
+            let postfix = matches!(mode, Postfix);
+            let (inside, outside) = needs_parens_for_adjustment_hints(expr, postfix);
+            (postfix, inside, outside)
+        }
+        PreferPrefix | PreferPostfix => {
+            let prefer_postfix = matches!(mode, PreferPostfix);
+
+            let (pre_inside, pre_outside) = needs_parens_for_adjustment_hints(expr, false);
+            let prefix = (false, pre_inside, pre_outside);
+            let pre_count = pre_inside as u8 + pre_outside as u8;
+
+            let (post_inside, post_outside) = needs_parens_for_adjustment_hints(expr, true);
+            let postfix = (true, post_inside, post_outside);
+            let post_count = post_inside as u8 + post_outside as u8;
+
+            match pre_count.cmp(&post_count) {
+                Less => prefix,
+                Greater => postfix,
+                Equal if prefer_postfix => postfix,
+                Equal => prefix,
+            }
+        }
+    }
+}
+
+/// Returns whatever we need to add paretheses on the inside and/or outside of `expr`,
+/// if we are going to add (`postfix`) adjustments hints to it.
+fn needs_parens_for_adjustment_hints(expr: &ast::Expr, postfix: bool) -> (bool, bool) {
+    // This is a very miserable pile of hacks...
+    //
+    // `Expr::needs_parens_in` requires that the expression is the child of the other expression,
+    // that is supposed to be its parent.
+    //
+    // But we want to check what would happen if we add `*`/`.*` to the inner expression.
+    // To check for inner we need `` expr.needs_parens_in(`*expr`) ``,
+    // to check for outer we need `` `*expr`.needs_parens_in(parent) ``,
+    // where "expr" is the `expr` parameter, `*expr` is the editted `expr`,
+    // and "parent" is the parent of the original expression...
+    //
+    // For this we utilize mutable mutable trees, which is a HACK, but it works.
+    //
+    // FIXME: comeup with a better API for `needs_parens_in`, so that we don't have to do *this*
+
+    // Make `&expr`/`expr?`
+    let dummy_expr = {
+        // `make::*` function go through a string, so they parse wrongly.
+        // for example `` make::expr_try(`|| a`) `` would result in a
+        // `|| (a?)` and not `(|| a)?`.
+        //
+        // Thus we need dummy parens to preserve the relationship we want.
+        // The parens are then simply ignored by the following code.
+        let dummy_paren = make::expr_paren(expr.clone());
+        if postfix {
+            make::expr_try(dummy_paren)
+        } else {
+            make::expr_ref(dummy_paren, false)
+        }
+    };
+
+    // Do the dark mutable tree magic.
+    // This essentially makes `dummy_expr` and `expr` switch places (families),
+    // so that `expr`'s parent is not `dummy_expr`'s parent.
+    let dummy_expr = dummy_expr.clone_for_update();
+    let expr = expr.clone_for_update();
+    ted::replace(expr.syntax(), dummy_expr.syntax());
+
+    let parent = dummy_expr.syntax().parent();
+    let expr = if postfix {
+        let ast::Expr::TryExpr(e) = &dummy_expr else { unreachable!() };
+        let Some(ast::Expr::ParenExpr(e)) = e.expr() else { unreachable!() };
+
+        e.expr().unwrap()
+    } else {
+        let ast::Expr::RefExpr(e) = &dummy_expr else { unreachable!() };
+        let Some(ast::Expr::ParenExpr(e)) = e.expr() else { unreachable!() };
+
+        e.expr().unwrap()
+    };
+
+    // At this point
+    // - `parent`     is the parrent of the original expression
+    // - `dummy_expr` is the original expression wrapped in the operator we want (`*`/`.*`)
+    // - `expr`       is the clone of the original expression (with `dummy_expr` as the parent)
+
+    let needs_outer_parens = parent.map_or(false, |p| dummy_expr.needs_parens_in(p));
+    let needs_inner_parens = expr.needs_parens_in(dummy_expr.syntax().clone());
+
+    (needs_outer_parens, needs_inner_parens)
+}
+
 #[cfg(test)]
 mod tests {
     use crate::{
         inlay_hints::tests::{check_with_config, DISABLED_CONFIG},
-        AdjustmentHints, InlayHintsConfig,
+        AdjustmentHints, AdjustmentHintsMode, InlayHintsConfig,
     };
 
     #[test]
@@ -125,7 +258,7 @@ mod tests {
         check_with_config(
             InlayHintsConfig { adjustment_hints: AdjustmentHints::Always, ..DISABLED_CONFIG },
             r#"
-//- minicore: coerce_unsized
+//- minicore: coerce_unsized, fn
 fn main() {
     let _: u32         = loop {};
                        //^^^^^^^<never-to-any>
@@ -148,12 +281,16 @@ fn main() {
                        //^^^^<fn-item-to-fn-pointer>
     let _: unsafe fn() = main as fn();
                        //^^^^^^^^^^^^<safe-fn-pointer-to-unsafe-fn-pointer>
+                       //^^^^^^^^^^^^(
+                       //^^^^^^^^^^^^)
     let _: fn()        = || {};
                        //^^^^^<closure-to-fn-pointer>
     let _: unsafe fn() = || {};
                        //^^^^^<closure-to-unsafe-fn-pointer>
     let _: *const u32  = &mut 0u32 as *mut u32;
                        //^^^^^^^^^^^^^^^^^^^^^<mut-ptr-to-const-ptr>
+                       //^^^^^^^^^^^^^^^^^^^^^(
+                       //^^^^^^^^^^^^^^^^^^^^^)
     let _: &mut [_]    = &mut [0; 0];
                        //^^^^^^^^^^^<unsize>
                        //^^^^^^^^^^^&mut $
@@ -206,6 +343,11 @@ fn main() {
                                        //^^^^^^^<unsize>
                                        //^^^^^^^&mut $
                                        //^^^^^^^*
+
+    let _: &mut dyn Fn() = &mut || ();
+                         //^^^^^^^^^^<unsize>
+                         //^^^^^^^^^^&mut $
+                         //^^^^^^^^^^*
 }
 
 #[derive(Copy, Clone)]
@@ -215,13 +357,154 @@ impl Struct {
     fn by_ref(&self) {}
     fn by_ref_mut(&mut self) {}
 }
-trait Trait {}
-impl Trait for Struct {}
 "#,
         )
     }
 
     #[test]
+    fn adjustment_hints_postfix() {
+        check_with_config(
+            InlayHintsConfig {
+                adjustment_hints: AdjustmentHints::Always,
+                adjustment_hints_mode: AdjustmentHintsMode::Postfix,
+                ..DISABLED_CONFIG
+            },
+            r#"
+//- minicore: coerce_unsized, fn
+fn main() {
+
+    Struct.consume();
+    Struct.by_ref();
+  //^^^^^^.&
+    Struct.by_ref_mut();
+  //^^^^^^.&mut
+
+    (&Struct).consume();
+   //^^^^^^^(
+   //^^^^^^^)
+   //^^^^^^^.*
+    (&Struct).by_ref();
+
+    (&mut Struct).consume();
+   //^^^^^^^^^^^(
+   //^^^^^^^^^^^)
+   //^^^^^^^^^^^.*
+    (&mut Struct).by_ref();
+   //^^^^^^^^^^^(
+   //^^^^^^^^^^^)
+   //^^^^^^^^^^^.*
+   //^^^^^^^^^^^.&
+    (&mut Struct).by_ref_mut();
+
+    // Check that block-like expressions don't duplicate hints
+    let _: &mut [u32] = (&mut []);
+                       //^^^^^^^(
+                       //^^^^^^^)
+                       //^^^^^^^.*
+                       //^^^^^^^.&mut
+                       //^^^^^^^.<unsize>
+    let _: &mut [u32] = { &mut [] };
+                        //^^^^^^^(
+                        //^^^^^^^)
+                        //^^^^^^^.*
+                        //^^^^^^^.&mut
+                        //^^^^^^^.<unsize>
+    let _: &mut [u32] = unsafe { &mut [] };
+                               //^^^^^^^(
+                               //^^^^^^^)
+                               //^^^^^^^.*
+                               //^^^^^^^.&mut
+                               //^^^^^^^.<unsize>
+    let _: &mut [u32] = if true {
+        &mut []
+      //^^^^^^^(
+      //^^^^^^^)
+      //^^^^^^^.*
+      //^^^^^^^.&mut
+      //^^^^^^^.<unsize>
+    } else {
+        loop {}
+      //^^^^^^^.<never-to-any>
+    };
+    let _: &mut [u32] = match () { () => &mut [] }
+                                       //^^^^^^^(
+                                       //^^^^^^^)
+                                       //^^^^^^^.*
+                                       //^^^^^^^.&mut
+                                       //^^^^^^^.<unsize>
+
+    let _: &mut dyn Fn() = &mut || ();
+                         //^^^^^^^^^^(
+                         //^^^^^^^^^^)
+                         //^^^^^^^^^^.*
+                         //^^^^^^^^^^.&mut
+                         //^^^^^^^^^^.<unsize>
+}
+
+#[derive(Copy, Clone)]
+struct Struct;
+impl Struct {
+    fn consume(self) {}
+    fn by_ref(&self) {}
+    fn by_ref_mut(&mut self) {}
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn adjustment_hints_prefer_prefix() {
+        check_with_config(
+            InlayHintsConfig {
+                adjustment_hints: AdjustmentHints::Always,
+                adjustment_hints_mode: AdjustmentHintsMode::PreferPrefix,
+                ..DISABLED_CONFIG
+            },
+            r#"
+fn main() {
+    let _: u32         = loop {};
+                       //^^^^^^^<never-to-any>
+
+    Struct.by_ref();
+  //^^^^^^.&
+
+    let (): () = return ();
+               //^^^^^^^^^<never-to-any>
+
+    struct Struct;
+    impl Struct { fn by_ref(&self) {} }
+}
+            "#,
+        )
+    }
+
+    #[test]
+    fn adjustment_hints_prefer_postfix() {
+        check_with_config(
+            InlayHintsConfig {
+                adjustment_hints: AdjustmentHints::Always,
+                adjustment_hints_mode: AdjustmentHintsMode::PreferPostfix,
+                ..DISABLED_CONFIG
+            },
+            r#"
+fn main() {
+    let _: u32         = loop {};
+                       //^^^^^^^.<never-to-any>
+
+    Struct.by_ref();
+  //^^^^^^.&
+
+    let (): () = return ();
+               //^^^^^^^^^<never-to-any>
+
+    struct Struct;
+    impl Struct { fn by_ref(&self) {} }
+}
+            "#,
+        )
+    }
+
+    #[test]
     fn never_to_never_is_never_shown() {
         check_with_config(
             InlayHintsConfig { adjustment_hints: AdjustmentHints::Always, ..DISABLED_CONFIG },
@@ -328,4 +611,20 @@ fn a() {
             "#,
         );
     }
+
+    #[test]
+    fn bug() {
+        check_with_config(
+            InlayHintsConfig { adjustment_hints: AdjustmentHints::Always, ..DISABLED_CONFIG },
+            r#"
+fn main() {
+    // These should be identical, but they are not...
+
+    let () = return;
+    let (): () = return;
+               //^^^^^^<never-to-any>
+}
+            "#,
+        )
+    }
 }
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 200958a4330..239456cb281 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -81,8 +81,8 @@ pub use crate::{
     highlight_related::{HighlightRelatedConfig, HighlightedRange},
     hover::{HoverAction, HoverConfig, HoverDocFormat, HoverGotoTypeData, HoverResult},
     inlay_hints::{
-        AdjustmentHints, ClosureReturnTypeHints, DiscriminantHints, InlayHint, InlayHintLabel,
-        InlayHintsConfig, InlayKind, InlayTooltip, LifetimeElisionHints,
+        AdjustmentHints, AdjustmentHintsMode, ClosureReturnTypeHints, DiscriminantHints, InlayHint,
+        InlayHintLabel, InlayHintsConfig, InlayKind, InlayTooltip, LifetimeElisionHints,
     },
     join_lines::JoinLinesConfig,
     markup::Markup,
diff --git a/crates/ide/src/static_index.rs b/crates/ide/src/static_index.rs
index 6e31a1420ea..a6b30ba1396 100644
--- a/crates/ide/src/static_index.rs
+++ b/crates/ide/src/static_index.rs
@@ -13,6 +13,7 @@ use syntax::{AstNode, SyntaxKind::*, SyntaxToken, TextRange, T};
 
 use crate::{
     hover::hover_for_definition,
+    inlay_hints::AdjustmentHintsMode,
     moniker::{def_to_moniker, MonikerResult},
     parent_module::crates_for,
     Analysis, Fold, HoverConfig, HoverDocFormat, HoverResult, InlayHint, InlayHintsConfig,
@@ -115,6 +116,7 @@ impl StaticIndex<'_> {
                     closure_return_type_hints: crate::ClosureReturnTypeHints::WithBlock,
                     lifetime_elision_hints: crate::LifetimeElisionHints::Never,
                     adjustment_hints: crate::AdjustmentHints::Never,
+                    adjustment_hints_mode: AdjustmentHintsMode::Prefix,
                     adjustment_hints_hide_outside_unsafe: false,
                     hide_named_constructor_hints: false,
                     hide_closure_initialization_hints: false,
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index 0f515a6d5cb..27a86db382d 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -333,6 +333,8 @@ config_data! {
         inlayHints_expressionAdjustmentHints_enable: AdjustmentHintsDef = "\"never\"",
         /// Whether to hide inlay hints for type adjustments outside of `unsafe` blocks.
         inlayHints_expressionAdjustmentHints_hideOutsideUnsafe: bool = "false",
+        /// Whether to show inlay hints as postfix ops (`.*` instead of `*`, etc).
+        inlayHints_expressionAdjustmentHints_mode: AdjustmentHintsModeDef = "\"prefix\"",
         /// Whether to show inlay type hints for elided lifetimes in function signatures.
         inlayHints_lifetimeElisionHints_enable: LifetimeElisionDef = "\"never\"",
         /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
@@ -1252,6 +1254,12 @@ impl Config {
                 },
                 AdjustmentHintsDef::Reborrow => ide::AdjustmentHints::ReborrowOnly,
             },
+            adjustment_hints_mode: match self.data.inlayHints_expressionAdjustmentHints_mode {
+                AdjustmentHintsModeDef::Prefix => ide::AdjustmentHintsMode::Prefix,
+                AdjustmentHintsModeDef::Postfix => ide::AdjustmentHintsMode::Postfix,
+                AdjustmentHintsModeDef::PreferPrefix => ide::AdjustmentHintsMode::PreferPrefix,
+                AdjustmentHintsModeDef::PreferPostfix => ide::AdjustmentHintsMode::PreferPostfix,
+            },
             adjustment_hints_hide_outside_unsafe: self
                 .data
                 .inlayHints_expressionAdjustmentHints_hideOutsideUnsafe,
@@ -1770,6 +1778,15 @@ enum DiscriminantHintsDef {
 
 #[derive(Deserialize, Debug, Clone)]
 #[serde(rename_all = "snake_case")]
+enum AdjustmentHintsModeDef {
+    Prefix,
+    Postfix,
+    PreferPrefix,
+    PreferPostfix,
+}
+
+#[derive(Deserialize, Debug, Clone)]
+#[serde(rename_all = "snake_case")]
 enum FilesWatcherDef {
     Client,
     Notify,
@@ -2101,6 +2118,21 @@ fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json
                 "Only show discriminant hints on fieldless enum variants."
             ]
         },
+        "AdjustmentHintsModeDef" => set! {
+            "type": "string",
+            "enum": [
+                "prefix",
+                "postfix",
+                "prefer_prefix",
+                "prefer_postfix",
+            ],
+            "enumDescriptions": [
+                "Always show adjustment hints as prefix (`*expr`).",
+                "Always show adjustment hints as postfix (`expr.*`).",
+                "Show prefix or postfix depending on which uses less parenthesis, prefering prefix.",
+                "Show prefix or postfix depending on which uses less parenthesis, prefering postfix.",
+            ]
+        },
         "CargoFeaturesDef" => set! {
             "anyOf": [
                 {
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index a12bd3952cc..e736b2ff9a3 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -452,6 +452,7 @@ pub(crate) fn inlay_hint(
             | InlayKind::ChainingHint
             | InlayKind::GenericParamListHint
             | InlayKind::ClosingParenthesis
+            | InlayKind::AdjustmentHintPostfix
             | InlayKind::LifetimeHint
             | InlayKind::ClosingBraceHint => position(line_index, inlay_hint.range.end()),
         },
@@ -465,6 +466,7 @@ pub(crate) fn inlay_hint(
             | InlayKind::ClosureReturnTypeHint
             | InlayKind::GenericParamListHint
             | InlayKind::AdjustmentHint
+            | InlayKind::AdjustmentHintPostfix
             | InlayKind::LifetimeHint
             | InlayKind::ParameterHint => false,
         }),
@@ -475,6 +477,7 @@ pub(crate) fn inlay_hint(
             | InlayKind::ClosureReturnTypeHint
             | InlayKind::GenericParamListHint
             | InlayKind::AdjustmentHint
+            | InlayKind::AdjustmentHintPostfix
             | InlayKind::TypeHint
             | InlayKind::DiscriminantHint
             | InlayKind::ClosingBraceHint => false,
@@ -493,6 +496,7 @@ pub(crate) fn inlay_hint(
             | InlayKind::GenericParamListHint
             | InlayKind::LifetimeHint
             | InlayKind::AdjustmentHint
+            | InlayKind::AdjustmentHintPostfix
             | InlayKind::ClosingBraceHint => None,
         },
         text_edits: None,
diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc
index d9794e7052a..0aaf07ebf38 100644
--- a/docs/user/generated_config.adoc
+++ b/docs/user/generated_config.adoc
@@ -469,6 +469,11 @@ Whether to show inlay hints for type adjustments.
 --
 Whether to hide inlay hints for type adjustments outside of `unsafe` blocks.
 --
+[[rust-analyzer.inlayHints.expressionAdjustmentHints.mode]]rust-analyzer.inlayHints.expressionAdjustmentHints.mode (default: `"prefix"`)::
++
+--
+Whether to show inlay hints as postfix ops (`.*` instead of `*`, etc).
+--
 [[rust-analyzer.inlayHints.lifetimeElisionHints.enable]]rust-analyzer.inlayHints.lifetimeElisionHints.enable (default: `"never"`)::
 +
 --
diff --git a/editors/code/package.json b/editors/code/package.json
index f508dde4f60..5ffce2f5536 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -1000,6 +1000,23 @@
                     "default": false,
                     "type": "boolean"
                 },
+                "rust-analyzer.inlayHints.expressionAdjustmentHints.mode": {
+                    "markdownDescription": "Whether to show inlay hints as postfix ops (`.*` instead of `*`, etc).",
+                    "default": "prefix",
+                    "type": "string",
+                    "enum": [
+                        "prefix",
+                        "postfix",
+                        "prefer_prefix",
+                        "prefer_postfix"
+                    ],
+                    "enumDescriptions": [
+                        "Always show adjustment hints as prefix (`*expr`).",
+                        "Always show adjustment hints as postfix (`expr.*`).",
+                        "Show prefix or postfix depending on which uses less parenthesis, prefering prefix.",
+                        "Show prefix or postfix depending on which uses less parenthesis, prefering postfix."
+                    ]
+                },
                 "rust-analyzer.inlayHints.lifetimeElisionHints.enable": {
                     "markdownDescription": "Whether to show inlay type hints for elided lifetimes in function signatures.",
                     "default": "never",