Unnamed repository; edit this file 'description' to name the repository.
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.
bors 2024-01-07
parent 1c5fa44 · parent 3c378b9 · commit 6ce3f44
-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 9e4542f7fc..c728570d98 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 5f7056b9c1..79c34c14da 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 6d8d103205..f466b8e938 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 0000000000..50ab15c504
--- /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 47fe2472a5..5b7094e6bc 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 1908c73b3b..e69302dbac 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 88fb370844..fe009f82a7 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 1e691befff..cc90d2dd1d 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 36980b146e..18a56e2823 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 a7e4899fb7..8618018c0b 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 8e04ab8bed..9ddf5a0a98 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 2b1bbac08e..eabbda2c39 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 c3f249e0ce..ecc90abff1 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 27ed8ac502..8307f6833e 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",