about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2024-01-07 10:21:39 +0000
committerbors <bors@rust-lang.org>2024-01-07 10:21:39 +0000
commit6ce3f44597a1fb78a6ec67d087c2f725c006515c (patch)
tree604a5df8544b34103e2c75c219c9f510b53c6830
parent1c5fa447d951e054218dcb9781effbfe776e2c9e (diff)
parent3c378b9c706c41d8f07adff9efd91ccb23e1c529 (diff)
downloadrust-6ce3f44597a1fb78a6ec67d087c2f725c006515c.tar.gz
rust-6ce3f44597a1fb78a6ec67d087c2f725c006515c.zip
Auto merge of #16298 - riverbl:exclusive-range-hint, r=Veykril
feat: Add inlay hint for exclusive ranges

Adds an inlay hint containing a '<' character to exclusive range expressions and patterns that specify an upper bound.

![2024-01-07-095056_257x415_scrot](https://github.com/rust-lang/rust-analyzer/assets/94326797/d6bbc0de-52a5-4af4-b53c-a034749b6cab)

Inspired by [this comment](https://github.com/rust-lang/rust/issues/37854#issuecomment-1865124907) noting that IntelliJ Rust has this feature.
-rw-r--r--crates/hir-def/src/body/lower.rs2
-rw-r--r--crates/ide-assists/src/handlers/convert_let_else_to_match.rs1
-rw-r--r--crates/ide/src/inlay_hints.rs15
-rw-r--r--crates/ide/src/inlay_hints/range_exclusive.rs121
-rw-r--r--crates/ide/src/static_index.rs1
-rw-r--r--crates/rust-analyzer/src/cli/analysis_stats.rs1
-rw-r--r--crates/rust-analyzer/src/config.rs3
-rw-r--r--crates/syntax/src/ast.rs10
-rw-r--r--crates/syntax/src/ast/expr_ext.rs24
-rw-r--r--crates/syntax/src/ast/node_ext.rs34
-rw-r--r--crates/syntax/src/ast/prec.rs2
-rw-r--r--crates/syntax/src/validation.rs2
-rw-r--r--docs/user/generated_config.adoc5
-rw-r--r--editors/code/package.json5
14 files changed, 209 insertions, 17 deletions
diff --git a/crates/hir-def/src/body/lower.rs b/crates/hir-def/src/body/lower.rs
index 9e4542f7fca..c728570d986 100644
--- a/crates/hir-def/src/body/lower.rs
+++ b/crates/hir-def/src/body/lower.rs
@@ -17,7 +17,7 @@ use smallvec::SmallVec;
 use syntax::{
     ast::{
         self, ArrayExprKind, AstChildren, BlockExpr, HasArgList, HasAttrs, HasLoopBody, HasName,
-        SlicePatComponents,
+        RangeItem, SlicePatComponents,
     },
     AstNode, AstPtr, SyntaxNodePtr,
 };
diff --git a/crates/ide-assists/src/handlers/convert_let_else_to_match.rs b/crates/ide-assists/src/handlers/convert_let_else_to_match.rs
index 5f7056b9c1c..79c34c14da7 100644
--- a/crates/ide-assists/src/handlers/convert_let_else_to_match.rs
+++ b/crates/ide-assists/src/handlers/convert_let_else_to_match.rs
@@ -1,5 +1,6 @@
 use hir::Semantics;
 use ide_db::RootDatabase;
+use syntax::ast::RangeItem;
 use syntax::ast::{edit::AstNodeEdit, AstNode, HasName, LetStmt, Name, Pat};
 use syntax::T;
 
diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs
index 6d8d1032051..f466b8e938f 100644
--- a/crates/ide/src/inlay_hints.rs
+++ b/crates/ide/src/inlay_hints.rs
@@ -32,6 +32,7 @@ mod fn_lifetime_fn;
 mod implicit_static;
 mod param_name;
 mod implicit_drop;
+mod range_exclusive;
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct InlayHintsConfig {
@@ -51,6 +52,7 @@ pub struct InlayHintsConfig {
     pub param_names_for_lifetime_elision_hints: bool,
     pub hide_named_constructor_hints: bool,
     pub hide_closure_initialization_hints: bool,
+    pub range_exclusive_hints: bool,
     pub closure_style: ClosureStyle,
     pub max_length: Option<usize>,
     pub closing_brace_hints_min_lines: Option<usize>,
@@ -127,6 +129,7 @@ pub enum InlayKind {
     Parameter,
     Type,
     Drop,
+    RangeExclusive,
 }
 
 #[derive(Debug)]
@@ -517,13 +520,20 @@ fn hints(
                         closure_captures::hints(hints, famous_defs, config, file_id, it.clone());
                         closure_ret::hints(hints, famous_defs, config, file_id, it)
                     },
+                    ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, config, it),
                     _ => None,
                 }
             },
             ast::Pat(it) => {
                 binding_mode::hints(hints, sema, config, &it);
-                if let ast::Pat::IdentPat(it) = it {
-                    bind_pat::hints(hints, famous_defs, config, file_id, &it);
+                match it {
+                    ast::Pat::IdentPat(it) => {
+                        bind_pat::hints(hints, famous_defs, config, file_id, &it);
+                    }
+                    ast::Pat::RangePat(it) => {
+                        range_exclusive::hints(hints, config, it);
+                    }
+                    _ => {}
                 }
                 Some(())
             },
@@ -621,6 +631,7 @@ mod tests {
         closing_brace_hints_min_lines: None,
         fields_to_resolve: InlayFieldsToResolve::empty(),
         implicit_drop_hints: false,
+        range_exclusive_hints: false,
     };
     pub(super) const TEST_CONFIG: InlayHintsConfig = InlayHintsConfig {
         type_hints: true,
diff --git a/crates/ide/src/inlay_hints/range_exclusive.rs b/crates/ide/src/inlay_hints/range_exclusive.rs
new file mode 100644
index 00000000000..50ab15c504f
--- /dev/null
+++ b/crates/ide/src/inlay_hints/range_exclusive.rs
@@ -0,0 +1,121 @@
+//! Implementation of "range exclusive" inlay hints:
+//! ```no_run
+//! for i in 0../* < */10 {}
+//! if let ../* < */100 = 50 {}
+//! ```
+use syntax::{ast, SyntaxToken, T};
+
+use crate::{InlayHint, InlayHintsConfig};
+
+pub(super) fn hints(
+    acc: &mut Vec<InlayHint>,
+    config: &InlayHintsConfig,
+    range: impl ast::RangeItem,
+) -> Option<()> {
+    (config.range_exclusive_hints && range.end().is_some())
+        .then(|| {
+            range.op_token().filter(|token| token.kind() == T![..]).map(|token| {
+                acc.push(inlay_hint(token));
+            })
+        })
+        .flatten()
+}
+
+fn inlay_hint(token: SyntaxToken) -> InlayHint {
+    InlayHint {
+        range: token.text_range(),
+        position: crate::InlayHintPosition::After,
+        pad_left: false,
+        pad_right: false,
+        kind: crate::InlayKind::RangeExclusive,
+        label: crate::InlayHintLabel::from("<"),
+        text_edit: None,
+        needs_resolve: false,
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::{
+        inlay_hints::tests::{check_with_config, DISABLED_CONFIG},
+        InlayHintsConfig,
+    };
+
+    #[test]
+    fn range_exclusive_expression_bounded_above_hints() {
+        check_with_config(
+            InlayHintsConfig { range_exclusive_hints: true, ..DISABLED_CONFIG },
+            r#"
+fn main() {
+    let a = 0..10;
+           //^^<
+    let b = ..100;
+          //^^<
+    let c = (2 - 1)..(7 * 8)
+                 //^^<
+}"#,
+        );
+    }
+
+    #[test]
+    fn range_exclusive_expression_unbounded_above_no_hints() {
+        check_with_config(
+            InlayHintsConfig { range_exclusive_hints: true, ..DISABLED_CONFIG },
+            r#"
+fn main() {
+    let a = 0..;
+    let b = ..;
+}"#,
+        );
+    }
+
+    #[test]
+    fn range_inclusive_expression_no_hints() {
+        check_with_config(
+            InlayHintsConfig { range_exclusive_hints: true, ..DISABLED_CONFIG },
+            r#"
+fn main() {
+    let a = 0..=10;
+    let b = ..=100;
+}"#,
+        );
+    }
+
+    #[test]
+    fn range_exclusive_pattern_bounded_above_hints() {
+        check_with_config(
+            InlayHintsConfig { range_exclusive_hints: true, ..DISABLED_CONFIG },
+            r#"
+fn main() {
+    if let 0..10 = 0 {}
+          //^^<
+    if let ..100 = 0 {}
+         //^^<
+}"#,
+        );
+    }
+
+    #[test]
+    fn range_exclusive_pattern_unbounded_above_no_hints() {
+        check_with_config(
+            InlayHintsConfig { range_exclusive_hints: true, ..DISABLED_CONFIG },
+            r#"
+fn main() {
+    if let 0.. = 0 {}
+    if let .. = 0 {}
+}"#,
+        );
+    }
+
+    #[test]
+    fn range_inclusive_pattern_no_hints() {
+        check_with_config(
+            InlayHintsConfig { range_exclusive_hints: true, ..DISABLED_CONFIG },
+            r#"
+fn main() {
+    if let 0..=10 = 0 {}
+    if let ..=100 = 0 {}
+}"#,
+        );
+    }
+}
diff --git a/crates/ide/src/static_index.rs b/crates/ide/src/static_index.rs
index 47fe2472a5e..5b7094e6bcc 100644
--- a/crates/ide/src/static_index.rs
+++ b/crates/ide/src/static_index.rs
@@ -133,6 +133,7 @@ impl StaticIndex<'_> {
                     closure_capture_hints: false,
                     closing_brace_hints_min_lines: Some(25),
                     fields_to_resolve: InlayFieldsToResolve::empty(),
+                    range_exclusive_hints: false,
                 },
                 file_id,
                 None,
diff --git a/crates/rust-analyzer/src/cli/analysis_stats.rs b/crates/rust-analyzer/src/cli/analysis_stats.rs
index 1908c73b3b4..e69302dbacc 100644
--- a/crates/rust-analyzer/src/cli/analysis_stats.rs
+++ b/crates/rust-analyzer/src/cli/analysis_stats.rs
@@ -792,6 +792,7 @@ impl flags::AnalysisStats {
                     max_length: Some(25),
                     closing_brace_hints_min_lines: Some(20),
                     fields_to_resolve: InlayFieldsToResolve::empty(),
+                    range_exclusive_hints: true,
                 },
                 file_id,
                 None,
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index 88fb3708449..fe009f82a71 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -399,6 +399,8 @@ config_data! {
         /// Whether to show function parameter name inlay hints at the call
         /// site.
         inlayHints_parameterHints_enable: bool                     = "true",
+        /// Whether to show exclusive range inlay hints.
+        inlayHints_rangeExclusiveHints_enable: bool                = "false",
         /// Whether to show inlay hints for compiler inserted reborrows.
         /// This setting is deprecated in favor of #rust-analyzer.inlayHints.expressionAdjustmentHints.enable#.
         inlayHints_reborrowHints_enable: ReborrowHintsDef          = "\"never\"",
@@ -1464,6 +1466,7 @@ impl Config {
             } else {
                 None
             },
+            range_exclusive_hints: self.data.inlayHints_rangeExclusiveHints_enable,
             fields_to_resolve: InlayFieldsToResolve {
                 resolve_text_edits: client_capability_fields.contains("textEdits"),
                 resolve_hint_tooltip: client_capability_fields.contains("tooltip"),
diff --git a/crates/syntax/src/ast.rs b/crates/syntax/src/ast.rs
index 1e691befff6..cc90d2dd1d5 100644
--- a/crates/syntax/src/ast.rs
+++ b/crates/syntax/src/ast.rs
@@ -136,6 +136,16 @@ where
 {
 }
 
+/// Trait to describe operations common to both `RangeExpr` and `RangePat`.
+pub trait RangeItem {
+    type Bound;
+
+    fn start(&self) -> Option<Self::Bound>;
+    fn end(&self) -> Option<Self::Bound>;
+    fn op_kind(&self) -> Option<RangeOp>;
+    fn op_token(&self) -> Option<SyntaxToken>;
+}
+
 mod support {
     use super::{AstChildren, AstNode, SyntaxKind, SyntaxNode, SyntaxToken};
 
diff --git a/crates/syntax/src/ast/expr_ext.rs b/crates/syntax/src/ast/expr_ext.rs
index 36980b146ef..18a56e2823a 100644
--- a/crates/syntax/src/ast/expr_ext.rs
+++ b/crates/syntax/src/ast/expr_ext.rs
@@ -13,6 +13,8 @@ use crate::{
     SyntaxNode, SyntaxToken, T,
 };
 
+use super::RangeItem;
+
 impl ast::HasAttrs for ast::Expr {}
 
 impl ast::Expr {
@@ -227,16 +229,12 @@ impl ast::RangeExpr {
             Some((ix, token, bin_op))
         })
     }
+}
 
-    pub fn op_kind(&self) -> Option<RangeOp> {
-        self.op_details().map(|t| t.2)
-    }
-
-    pub fn op_token(&self) -> Option<SyntaxToken> {
-        self.op_details().map(|t| t.1)
-    }
+impl RangeItem for ast::RangeExpr {
+    type Bound = ast::Expr;
 
-    pub fn start(&self) -> Option<ast::Expr> {
+    fn start(&self) -> Option<ast::Expr> {
         let op_ix = self.op_details()?.0;
         self.syntax()
             .children_with_tokens()
@@ -244,13 +242,21 @@ impl ast::RangeExpr {
             .find_map(|it| ast::Expr::cast(it.into_node()?))
     }
 
-    pub fn end(&self) -> Option<ast::Expr> {
+    fn end(&self) -> Option<ast::Expr> {
         let op_ix = self.op_details()?.0;
         self.syntax()
             .children_with_tokens()
             .skip(op_ix + 1)
             .find_map(|it| ast::Expr::cast(it.into_node()?))
     }
+
+    fn op_token(&self) -> Option<SyntaxToken> {
+        self.op_details().map(|t| t.1)
+    }
+
+    fn op_kind(&self) -> Option<RangeOp> {
+        self.op_details().map(|t| t.2)
+    }
 }
 
 impl ast::IndexExpr {
diff --git a/crates/syntax/src/ast/node_ext.rs b/crates/syntax/src/ast/node_ext.rs
index a7e4899fb7e..8618018c0b6 100644
--- a/crates/syntax/src/ast/node_ext.rs
+++ b/crates/syntax/src/ast/node_ext.rs
@@ -14,6 +14,8 @@ use crate::{
     ted, NodeOrToken, SmolStr, SyntaxElement, SyntaxToken, TokenText, T,
 };
 
+use super::{RangeItem, RangeOp};
+
 impl ast::Lifetime {
     pub fn text(&self) -> TokenText<'_> {
         text_of_first_token(self.syntax())
@@ -875,8 +877,10 @@ impl ast::Module {
     }
 }
 
-impl ast::RangePat {
-    pub fn start(&self) -> Option<ast::Pat> {
+impl RangeItem for ast::RangePat {
+    type Bound = ast::Pat;
+
+    fn start(&self) -> Option<ast::Pat> {
         self.syntax()
             .children_with_tokens()
             .take_while(|it| !(it.kind() == T![..] || it.kind() == T![..=]))
@@ -884,13 +888,37 @@ impl ast::RangePat {
             .find_map(ast::Pat::cast)
     }
 
-    pub fn end(&self) -> Option<ast::Pat> {
+    fn end(&self) -> Option<ast::Pat> {
         self.syntax()
             .children_with_tokens()
             .skip_while(|it| !(it.kind() == T![..] || it.kind() == T![..=]))
             .filter_map(|it| it.into_node())
             .find_map(ast::Pat::cast)
     }
+
+    fn op_token(&self) -> Option<SyntaxToken> {
+        self.syntax().children_with_tokens().find_map(|it| {
+            let token = it.into_token()?;
+
+            match token.kind() {
+                T![..] => Some(token),
+                T![..=] => Some(token),
+                _ => None,
+            }
+        })
+    }
+
+    fn op_kind(&self) -> Option<RangeOp> {
+        self.syntax().children_with_tokens().find_map(|it| {
+            let token = it.into_token()?;
+
+            match token.kind() {
+                T![..] => Some(RangeOp::Exclusive),
+                T![..=] => Some(RangeOp::Inclusive),
+                _ => None,
+            }
+        })
+    }
 }
 
 impl ast::TokenTree {
diff --git a/crates/syntax/src/ast/prec.rs b/crates/syntax/src/ast/prec.rs
index 8e04ab8bedc..9ddf5a0a980 100644
--- a/crates/syntax/src/ast/prec.rs
+++ b/crates/syntax/src/ast/prec.rs
@@ -1,7 +1,7 @@
 //! Precedence representation.
 
 use crate::{
-    ast::{self, BinaryOp, Expr, HasArgList},
+    ast::{self, BinaryOp, Expr, HasArgList, RangeItem},
     match_ast, AstNode, SyntaxNode,
 };
 
diff --git a/crates/syntax/src/validation.rs b/crates/syntax/src/validation.rs
index 2b1bbac08e5..eabbda2c398 100644
--- a/crates/syntax/src/validation.rs
+++ b/crates/syntax/src/validation.rs
@@ -9,7 +9,7 @@ use rustc_dependencies::lexer::unescape::{self, unescape_literal, Mode};
 
 use crate::{
     algo,
-    ast::{self, HasAttrs, HasVisibility, IsString},
+    ast::{self, HasAttrs, HasVisibility, IsString, RangeItem},
     match_ast, AstNode, SyntaxError,
     SyntaxKind::{CONST, FN, INT_NUMBER, TYPE_ALIAS},
     SyntaxNode, SyntaxToken, TextSize, T,
diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc
index c3f249e0ce2..ecc90abff13 100644
--- a/docs/user/generated_config.adoc
+++ b/docs/user/generated_config.adoc
@@ -596,6 +596,11 @@ Maximum length for inlay hints. Set to null to have an unlimited length.
 Whether to show function parameter name inlay hints at the call
 site.
 --
+[[rust-analyzer.inlayHints.rangeExclusiveHints.enable]]rust-analyzer.inlayHints.rangeExclusiveHints.enable (default: `false`)::
++
+--
+Whether to show exclusive range inlay hints.
+--
 [[rust-analyzer.inlayHints.reborrowHints.enable]]rust-analyzer.inlayHints.reborrowHints.enable (default: `"never"`)::
 +
 --
diff --git a/editors/code/package.json b/editors/code/package.json
index 27ed8ac502b..8307f6833e6 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -1308,6 +1308,11 @@
                     "default": true,
                     "type": "boolean"
                 },
+                "rust-analyzer.inlayHints.rangeExclusiveHints.enable": {
+                    "markdownDescription": "Whether to show exclusive range inlay hints.",
+                    "default": false,
+                    "type": "boolean"
+                },
                 "rust-analyzer.inlayHints.reborrowHints.enable": {
                     "markdownDescription": "Whether to show inlay hints for compiler inserted reborrows.\nThis setting is deprecated in favor of #rust-analyzer.inlayHints.expressionAdjustmentHints.enable#.",
                     "default": "never",