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.
This commit is contained in:
bors 2024-01-07 10:21:39 +00:00
commit 6ce3f44597
14 changed files with 209 additions and 17 deletions

View file

@ -17,7 +17,7 @@ use smallvec::SmallVec;
use syntax::{ use syntax::{
ast::{ ast::{
self, ArrayExprKind, AstChildren, BlockExpr, HasArgList, HasAttrs, HasLoopBody, HasName, self, ArrayExprKind, AstChildren, BlockExpr, HasArgList, HasAttrs, HasLoopBody, HasName,
SlicePatComponents, RangeItem, SlicePatComponents,
}, },
AstNode, AstPtr, SyntaxNodePtr, AstNode, AstPtr, SyntaxNodePtr,
}; };

View file

@ -1,5 +1,6 @@
use hir::Semantics; use hir::Semantics;
use ide_db::RootDatabase; use ide_db::RootDatabase;
use syntax::ast::RangeItem;
use syntax::ast::{edit::AstNodeEdit, AstNode, HasName, LetStmt, Name, Pat}; use syntax::ast::{edit::AstNodeEdit, AstNode, HasName, LetStmt, Name, Pat};
use syntax::T; use syntax::T;

View file

@ -32,6 +32,7 @@ mod fn_lifetime_fn;
mod implicit_static; mod implicit_static;
mod param_name; mod param_name;
mod implicit_drop; mod implicit_drop;
mod range_exclusive;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct InlayHintsConfig { pub struct InlayHintsConfig {
@ -51,6 +52,7 @@ pub struct InlayHintsConfig {
pub param_names_for_lifetime_elision_hints: bool, pub param_names_for_lifetime_elision_hints: bool,
pub hide_named_constructor_hints: bool, pub hide_named_constructor_hints: bool,
pub hide_closure_initialization_hints: bool, pub hide_closure_initialization_hints: bool,
pub range_exclusive_hints: bool,
pub closure_style: ClosureStyle, pub closure_style: ClosureStyle,
pub max_length: Option<usize>, pub max_length: Option<usize>,
pub closing_brace_hints_min_lines: Option<usize>, pub closing_brace_hints_min_lines: Option<usize>,
@ -127,6 +129,7 @@ pub enum InlayKind {
Parameter, Parameter,
Type, Type,
Drop, Drop,
RangeExclusive,
} }
#[derive(Debug)] #[derive(Debug)]
@ -517,13 +520,20 @@ fn hints(
closure_captures::hints(hints, famous_defs, config, file_id, it.clone()); closure_captures::hints(hints, famous_defs, config, file_id, it.clone());
closure_ret::hints(hints, famous_defs, config, file_id, it) closure_ret::hints(hints, famous_defs, config, file_id, it)
}, },
ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, config, it),
_ => None, _ => None,
} }
}, },
ast::Pat(it) => { ast::Pat(it) => {
binding_mode::hints(hints, sema, config, &it); binding_mode::hints(hints, sema, config, &it);
if let ast::Pat::IdentPat(it) = it { match it {
bind_pat::hints(hints, famous_defs, config, file_id, &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(()) Some(())
}, },
@ -621,6 +631,7 @@ mod tests {
closing_brace_hints_min_lines: None, closing_brace_hints_min_lines: None,
fields_to_resolve: InlayFieldsToResolve::empty(), fields_to_resolve: InlayFieldsToResolve::empty(),
implicit_drop_hints: false, implicit_drop_hints: false,
range_exclusive_hints: false,
}; };
pub(super) const TEST_CONFIG: InlayHintsConfig = InlayHintsConfig { pub(super) const TEST_CONFIG: InlayHintsConfig = InlayHintsConfig {
type_hints: true, type_hints: true,

View file

@ -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 {}
}"#,
);
}
}

View file

@ -133,6 +133,7 @@ impl StaticIndex<'_> {
closure_capture_hints: false, closure_capture_hints: false,
closing_brace_hints_min_lines: Some(25), closing_brace_hints_min_lines: Some(25),
fields_to_resolve: InlayFieldsToResolve::empty(), fields_to_resolve: InlayFieldsToResolve::empty(),
range_exclusive_hints: false,
}, },
file_id, file_id,
None, None,

View file

@ -792,6 +792,7 @@ impl flags::AnalysisStats {
max_length: Some(25), max_length: Some(25),
closing_brace_hints_min_lines: Some(20), closing_brace_hints_min_lines: Some(20),
fields_to_resolve: InlayFieldsToResolve::empty(), fields_to_resolve: InlayFieldsToResolve::empty(),
range_exclusive_hints: true,
}, },
file_id, file_id,
None, None,

View file

@ -399,6 +399,8 @@ config_data! {
/// Whether to show function parameter name inlay hints at the call /// Whether to show function parameter name inlay hints at the call
/// site. /// site.
inlayHints_parameterHints_enable: bool = "true", 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. /// Whether to show inlay hints for compiler inserted reborrows.
/// This setting is deprecated in favor of #rust-analyzer.inlayHints.expressionAdjustmentHints.enable#. /// This setting is deprecated in favor of #rust-analyzer.inlayHints.expressionAdjustmentHints.enable#.
inlayHints_reborrowHints_enable: ReborrowHintsDef = "\"never\"", inlayHints_reborrowHints_enable: ReborrowHintsDef = "\"never\"",
@ -1464,6 +1466,7 @@ impl Config {
} else { } else {
None None
}, },
range_exclusive_hints: self.data.inlayHints_rangeExclusiveHints_enable,
fields_to_resolve: InlayFieldsToResolve { fields_to_resolve: InlayFieldsToResolve {
resolve_text_edits: client_capability_fields.contains("textEdits"), resolve_text_edits: client_capability_fields.contains("textEdits"),
resolve_hint_tooltip: client_capability_fields.contains("tooltip"), resolve_hint_tooltip: client_capability_fields.contains("tooltip"),

View file

@ -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 { mod support {
use super::{AstChildren, AstNode, SyntaxKind, SyntaxNode, SyntaxToken}; use super::{AstChildren, AstNode, SyntaxKind, SyntaxNode, SyntaxToken};

View file

@ -13,6 +13,8 @@ use crate::{
SyntaxNode, SyntaxToken, T, SyntaxNode, SyntaxToken, T,
}; };
use super::RangeItem;
impl ast::HasAttrs for ast::Expr {} impl ast::HasAttrs for ast::Expr {}
impl ast::Expr { impl ast::Expr {
@ -227,16 +229,12 @@ impl ast::RangeExpr {
Some((ix, token, bin_op)) Some((ix, token, bin_op))
}) })
} }
}
pub fn op_kind(&self) -> Option<RangeOp> { impl RangeItem for ast::RangeExpr {
self.op_details().map(|t| t.2) type Bound = ast::Expr;
}
pub fn op_token(&self) -> Option<SyntaxToken> { fn start(&self) -> Option<ast::Expr> {
self.op_details().map(|t| t.1)
}
pub fn start(&self) -> Option<ast::Expr> {
let op_ix = self.op_details()?.0; let op_ix = self.op_details()?.0;
self.syntax() self.syntax()
.children_with_tokens() .children_with_tokens()
@ -244,13 +242,21 @@ impl ast::RangeExpr {
.find_map(|it| ast::Expr::cast(it.into_node()?)) .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; let op_ix = self.op_details()?.0;
self.syntax() self.syntax()
.children_with_tokens() .children_with_tokens()
.skip(op_ix + 1) .skip(op_ix + 1)
.find_map(|it| ast::Expr::cast(it.into_node()?)) .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 { impl ast::IndexExpr {

View file

@ -14,6 +14,8 @@ use crate::{
ted, NodeOrToken, SmolStr, SyntaxElement, SyntaxToken, TokenText, T, ted, NodeOrToken, SmolStr, SyntaxElement, SyntaxToken, TokenText, T,
}; };
use super::{RangeItem, RangeOp};
impl ast::Lifetime { impl ast::Lifetime {
pub fn text(&self) -> TokenText<'_> { pub fn text(&self) -> TokenText<'_> {
text_of_first_token(self.syntax()) text_of_first_token(self.syntax())
@ -875,8 +877,10 @@ impl ast::Module {
} }
} }
impl ast::RangePat { impl RangeItem for ast::RangePat {
pub fn start(&self) -> Option<ast::Pat> { type Bound = ast::Pat;
fn start(&self) -> Option<ast::Pat> {
self.syntax() self.syntax()
.children_with_tokens() .children_with_tokens()
.take_while(|it| !(it.kind() == T![..] || it.kind() == T![..=])) .take_while(|it| !(it.kind() == T![..] || it.kind() == T![..=]))
@ -884,13 +888,37 @@ impl ast::RangePat {
.find_map(ast::Pat::cast) .find_map(ast::Pat::cast)
} }
pub fn end(&self) -> Option<ast::Pat> { fn end(&self) -> Option<ast::Pat> {
self.syntax() self.syntax()
.children_with_tokens() .children_with_tokens()
.skip_while(|it| !(it.kind() == T![..] || it.kind() == T![..=])) .skip_while(|it| !(it.kind() == T![..] || it.kind() == T![..=]))
.filter_map(|it| it.into_node()) .filter_map(|it| it.into_node())
.find_map(ast::Pat::cast) .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 { impl ast::TokenTree {

View file

@ -1,7 +1,7 @@
//! Precedence representation. //! Precedence representation.
use crate::{ use crate::{
ast::{self, BinaryOp, Expr, HasArgList}, ast::{self, BinaryOp, Expr, HasArgList, RangeItem},
match_ast, AstNode, SyntaxNode, match_ast, AstNode, SyntaxNode,
}; };

View file

@ -9,7 +9,7 @@ use rustc_dependencies::lexer::unescape::{self, unescape_literal, Mode};
use crate::{ use crate::{
algo, algo,
ast::{self, HasAttrs, HasVisibility, IsString}, ast::{self, HasAttrs, HasVisibility, IsString, RangeItem},
match_ast, AstNode, SyntaxError, match_ast, AstNode, SyntaxError,
SyntaxKind::{CONST, FN, INT_NUMBER, TYPE_ALIAS}, SyntaxKind::{CONST, FN, INT_NUMBER, TYPE_ALIAS},
SyntaxNode, SyntaxToken, TextSize, T, SyntaxNode, SyntaxToken, TextSize, T,

View file

@ -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 Whether to show function parameter name inlay hints at the call
site. 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"`):: [[rust-analyzer.inlayHints.reborrowHints.enable]]rust-analyzer.inlayHints.reborrowHints.enable (default: `"never"`)::
+ +
-- --

View file

@ -1308,6 +1308,11 @@
"default": true, "default": true,
"type": "boolean" "type": "boolean"
}, },
"rust-analyzer.inlayHints.rangeExclusiveHints.enable": {
"markdownDescription": "Whether to show exclusive range inlay hints.",
"default": false,
"type": "boolean"
},
"rust-analyzer.inlayHints.reborrowHints.enable": { "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#.", "markdownDescription": "Whether to show inlay hints for compiler inserted reborrows.\nThis setting is deprecated in favor of #rust-analyzer.inlayHints.expressionAdjustmentHints.enable#.",
"default": "never", "default": "never",