use hir::Semantics;
use ide_db::{
- base_db::FilePosition,
+ base_db::{FileId, FilePosition},
defs::Definition,
- helpers::pick_best_token,
- search::{FileReference, ReferenceAccess, SearchScope},
+ helpers::{for_each_break_expr, for_each_tail_expr, node_ext::walk_expr, pick_best_token},
+ search::{FileReference, ReferenceCategory, SearchScope},
RootDatabase,
};
+use rustc_hash::FxHashSet;
use syntax::{
- ast::{self, LoopBodyOwner},
- match_ast, AstNode, SyntaxNode, SyntaxToken, TextRange, WalkEvent, T,
+ ast::{self, HasLoopBody},
+ match_ast, AstNode,
+ SyntaxKind::IDENT,
+ SyntaxNode, SyntaxToken, TextRange, T,
};
-use crate::{display::TryToNav, references, NavigationTarget};
+use crate::{references, NavigationTarget, TryToNav};
+#[derive(PartialEq, Eq, Hash)]
pub struct HighlightedRange {
pub range: TextRange,
- pub access: Option<ReferenceAccess>,
+ // FIXME: This needs to be more precise. Reference category makes sense only
+ // for references, but we also have defs. And things like exit points are
+ // neither.
+ pub category: Option<ReferenceCategory>,
+}
+
+#[derive(Default, Clone)]
+pub struct HighlightRelatedConfig {
+ pub references: bool,
+ pub exit_points: bool,
+ pub break_points: bool,
+ pub yield_points: bool,
}
// Feature: Highlight Related
// Highlights constructs related to the thing under the cursor:
// - if on an identifier, highlights all references to that identifier in the current file
// - if on an `async` or `await token, highlights all yield points for that async context
-// - if on a `return` token, `?` character or `->` return type arrow, highlights all exit points for that context
+// - if on a `return` or `fn` keyword, `?` character or `->` return type arrow, highlights all exit points for that context
// - if on a `break`, `loop`, `while` or `for` token, highlights all break points for that loop or block context
+//
+// Note: `?` and `->` do not currently trigger this behavior in the VSCode editor.
pub(crate) fn highlight_related(
sema: &Semantics<RootDatabase>,
- position: FilePosition,
+ config: HighlightRelatedConfig,
+ FilePosition { offset, file_id }: FilePosition,
) -> Option<Vec<HighlightedRange>> {
let _p = profile::span("highlight_related");
- let syntax = sema.parse(position.file_id).syntax().clone();
-
- let token = pick_best_token(syntax.token_at_offset(position.offset), |kind| match kind {
- T![?] => 2, // prefer `?` when the cursor is sandwiched like `await$0?`
- T![await]
- | T![async]
- | T![return]
- | T![break]
- | T![loop]
- | T![for]
- | T![while]
- | T![->] => 1,
+ let syntax = sema.parse(file_id).syntax().clone();
+
+ let token = pick_best_token(syntax.token_at_offset(offset), |kind| match kind {
+ T![?] => 4, // prefer `?` when the cursor is sandwiched like in `await$0?`
+ T![->] => 3,
+ kind if kind.is_keyword() => 2,
+ IDENT => 1,
_ => 0,
})?;
match token.kind() {
- T![return] | T![?] | T![->] => highlight_exit_points(sema, token),
- T![await] | T![async] => highlight_yield_points(token),
- T![break] | T![loop] | T![for] | T![while] => highlight_break_points(token),
- _ => highlight_references(sema, &syntax, position),
+ T![?] if config.exit_points && token.parent().and_then(ast::TryExpr::cast).is_some() => {
+ highlight_exit_points(sema, token)
+ }
+ T![fn] | T![return] | T![->] if config.exit_points => highlight_exit_points(sema, token),
+ T![await] | T![async] if config.yield_points => highlight_yield_points(token),
+ T![for] if config.break_points && token.parent().and_then(ast::ForExpr::cast).is_some() => {
+ highlight_break_points(token)
+ }
+ T![break] | T![loop] | T![while] if config.break_points => highlight_break_points(token),
+ _ if config.references => highlight_references(sema, &syntax, token, file_id),
+ _ => None,
}
}
fn highlight_references(
sema: &Semantics<RootDatabase>,
- syntax: &SyntaxNode,
- FilePosition { offset, file_id }: FilePosition,
+ node: &SyntaxNode,
+ token: SyntaxToken,
+ file_id: FileId,
) -> Option<Vec<HighlightedRange>> {
- let def = references::find_def(sema, syntax, offset)?;
- let usages = def.usages(sema).set_scope(Some(SearchScope::single_file(file_id))).all();
+ let defs = find_defs(sema, token.clone());
+ let usages = defs
+ .iter()
+ .filter_map(|&d| {
+ d.usages(sema)
+ .set_scope(Some(SearchScope::single_file(file_id)))
+ .include_self_refs()
+ .all()
+ .references
+ .remove(&file_id)
+ })
+ .flatten()
+ .map(|FileReference { category: access, range, .. }| HighlightedRange {
+ range,
+ category: access,
+ });
- let declaration = match def {
- Definition::ModuleDef(hir::ModuleDef::Module(module)) => {
- Some(NavigationTarget::from_module_to_decl(sema.db, module))
+ let declarations = defs.iter().flat_map(|def| {
+ match def {
+ &Definition::Module(module) => {
+ Some(NavigationTarget::from_module_to_decl(sema.db, module))
+ }
+ def => def.try_to_nav(sema.db),
}
- def => def.try_to_nav(sema.db),
- }
- .filter(|decl| decl.file_id == file_id)
- .and_then(|decl| {
- let range = decl.focus_range?;
- let access = references::decl_access(&def, syntax, range);
- Some(HighlightedRange { range, access })
+ .filter(|decl| decl.file_id == file_id)
+ .and_then(|decl| {
+ let range = decl.focus_range?;
+ let category =
+ references::decl_mutability(&def, node, range).then(|| ReferenceCategory::Write);
+ Some(HighlightedRange { range, category })
+ })
});
- let file_refs = usages.references.get(&file_id).map_or(&[][..], Vec::as_slice);
- let mut res = Vec::with_capacity(file_refs.len() + 1);
- res.extend(declaration);
- res.extend(
- file_refs
- .iter()
- .map(|&FileReference { access, range, .. }| HighlightedRange { range, access }),
- );
- Some(res)
+ let res: FxHashSet<_> = declarations.chain(usages).collect();
+ if res.is_empty() {
+ None
+ } else {
+ Some(res.into_iter().collect())
+ }
}
fn highlight_exit_points(
) -> Option<Vec<HighlightedRange>> {
let mut highlights = Vec::new();
let body = body?;
- walk(&body, &mut |expr| match expr {
+ walk_expr(&body, &mut |expr| match expr {
ast::Expr::ReturnExpr(expr) => {
if let Some(token) = expr.return_token() {
- highlights.push(HighlightedRange { access: None, range: token.text_range() });
+ highlights.push(HighlightedRange { category: None, range: token.text_range() });
}
}
ast::Expr::TryExpr(try_) => {
if let Some(token) = try_.question_mark_token() {
- highlights.push(HighlightedRange { access: None, range: token.text_range() });
+ highlights.push(HighlightedRange { category: None, range: token.text_range() });
}
}
ast::Expr::MethodCallExpr(_) | ast::Expr::CallExpr(_) | ast::Expr::MacroCall(_) => {
- if sema.type_of_expr(&expr).map_or(false, |ty| ty.is_never()) {
- highlights
- .push(HighlightedRange { access: None, range: expr.syntax().text_range() });
+ if sema.type_of_expr(&expr).map_or(false, |ty| ty.original.is_never()) {
+ highlights.push(HighlightedRange {
+ category: None,
+ range: expr.syntax().text_range(),
+ });
}
}
_ => (),
};
if let Some(tail) = tail {
- for_each_inner_tail(&tail, &mut |tail| {
+ for_each_tail_expr(&tail, &mut |tail| {
let range = match tail {
ast::Expr::BreakExpr(b) => b
.break_token()
.map_or_else(|| tail.syntax().text_range(), |tok| tok.text_range()),
_ => tail.syntax().text_range(),
};
- highlights.push(HighlightedRange { access: None, range })
+ highlights.push(HighlightedRange { category: None, range })
});
}
Some(highlights)
match anc {
ast::Fn(fn_) => hl(sema, fn_.body().map(ast::Expr::BlockExpr)),
ast::ClosureExpr(closure) => hl(sema, closure.body()),
- ast::EffectExpr(effect) => if matches!(effect.effect(), ast::Effect::Async(_) | ast::Effect::Try(_)| ast::Effect::Const(_)) {
- hl(sema, effect.block_expr().map(ast::Expr::BlockExpr))
+ ast::BlockExpr(block_expr) => if matches!(block_expr.modifier(), Some(ast::BlockModifier::Async(_) | ast::BlockModifier::Try(_)| ast::BlockModifier::Const(_))) {
+ hl(sema, Some(block_expr.into()))
} else {
continue;
},
fn hl(
token: Option<SyntaxToken>,
label: Option<ast::Label>,
- body: Option<ast::BlockExpr>,
+ body: Option<ast::StmtList>,
) -> Option<Vec<HighlightedRange>> {
let mut highlights = Vec::new();
let range = cover_range(
token.map(|tok| tok.text_range()),
label.as_ref().map(|it| it.syntax().text_range()),
);
- highlights.extend(range.map(|range| HighlightedRange { access: None, range }));
- for_each_break(label, body, &mut |break_| {
+ highlights.extend(range.map(|range| HighlightedRange { category: None, range }));
+ for_each_break_expr(label, body, &mut |break_| {
let range = cover_range(
break_.break_token().map(|it| it.text_range()),
break_.lifetime().map(|it| it.syntax().text_range()),
);
- highlights.extend(range.map(|range| HighlightedRange { access: None, range }));
+ highlights.extend(range.map(|range| HighlightedRange { category: None, range }));
});
Some(highlights)
}
ast::LoopExpr(l) => l.label().and_then(|it| it.lifetime()),
ast::ForExpr(f) => f.label().and_then(|it| it.lifetime()),
ast::WhileExpr(w) => w.label().and_then(|it| it.lifetime()),
- ast::EffectExpr(b) => Some(b.label().and_then(|it| it.lifetime())?),
+ ast::BlockExpr(b) => Some(b.label().and_then(|it| it.lifetime())?),
_ => return None,
}
};
for anc in token.ancestors().flat_map(ast::Expr::cast) {
return match anc {
ast::Expr::LoopExpr(l) if label_matches(l.label()) => {
- hl(l.loop_token(), l.label(), l.loop_body())
+ hl(l.loop_token(), l.label(), l.loop_body().and_then(|it| it.stmt_list()))
}
ast::Expr::ForExpr(f) if label_matches(f.label()) => {
- hl(f.for_token(), f.label(), f.loop_body())
+ hl(f.for_token(), f.label(), f.loop_body().and_then(|it| it.stmt_list()))
}
ast::Expr::WhileExpr(w) if label_matches(w.label()) => {
- hl(w.while_token(), w.label(), w.loop_body())
+ hl(w.while_token(), w.label(), w.loop_body().and_then(|it| it.stmt_list()))
}
- ast::Expr::EffectExpr(e) if e.label().is_some() && label_matches(e.label()) => {
- hl(None, e.label(), e.block_expr())
+ ast::Expr::BlockExpr(e) if e.label().is_some() && label_matches(e.label()) => {
+ hl(None, e.label(), e.stmt_list())
}
_ => continue,
};
async_token: Option<SyntaxToken>,
body: Option<ast::Expr>,
) -> Option<Vec<HighlightedRange>> {
- let mut highlights = Vec::new();
- highlights.push(HighlightedRange { access: None, range: async_token?.text_range() });
+ let mut highlights =
+ vec![HighlightedRange { category: None, range: async_token?.text_range() }];
if let Some(body) = body {
- walk(&body, &mut |expr| {
+ walk_expr(&body, &mut |expr| {
if let ast::Expr::AwaitExpr(expr) = expr {
if let Some(token) = expr.await_token() {
highlights
- .push(HighlightedRange { access: None, range: token.text_range() });
+ .push(HighlightedRange { category: None, range: token.text_range() });
}
}
});
return match_ast! {
match anc {
ast::Fn(fn_) => hl(fn_.async_token(), fn_.body().map(ast::Expr::BlockExpr)),
- ast::EffectExpr(effect) => hl(effect.async_token(), effect.block_expr().map(ast::Expr::BlockExpr)),
+ ast::BlockExpr(block_expr) => {
+ if block_expr.async_token().is_none() {
+ continue;
+ }
+ hl(block_expr.async_token(), Some(block_expr.into()))
+ },
ast::ClosureExpr(closure) => hl(closure.async_token(), closure.body()),
_ => continue,
}
None
}
-/// Preorder walk all the expression's child expressions
-fn walk(expr: &ast::Expr, cb: &mut dyn FnMut(ast::Expr)) {
- let mut preorder = expr.syntax().preorder();
- while let Some(event) = preorder.next() {
- let node = match event {
- WalkEvent::Enter(node) => node,
- WalkEvent::Leave(_) => continue,
- };
- match ast::Stmt::cast(node.clone()) {
- // recursively walk the initializer, skipping potential const pat expressions
- // lets statements aren't usually nested too deeply so this is fine to recurse on
- Some(ast::Stmt::LetStmt(l)) => {
- if let Some(expr) = l.initializer() {
- walk(&expr, cb);
- }
- preorder.skip_subtree();
- }
- // Don't skip subtree since we want to process the expression child next
- Some(ast::Stmt::ExprStmt(_)) => (),
- // skip inner items which might have their own expressions
- Some(ast::Stmt::Item(_)) => preorder.skip_subtree(),
- None => {
- if let Some(expr) = ast::Expr::cast(node) {
- let is_different_context = match &expr {
- ast::Expr::EffectExpr(effect) => {
- matches!(
- effect.effect(),
- ast::Effect::Async(_) | ast::Effect::Try(_) | ast::Effect::Const(_)
- )
- }
- ast::Expr::ClosureExpr(__) => true,
- _ => false,
- };
- cb(expr);
- if is_different_context {
- preorder.skip_subtree();
- }
- } else {
- preorder.skip_subtree();
- }
- }
- }
- }
-}
-
-// FIXME: doesn't account for labeled breaks in labeled blocks
-fn for_each_inner_tail(expr: &ast::Expr, cb: &mut dyn FnMut(&ast::Expr)) {
- match expr {
- ast::Expr::BlockExpr(b) => {
- if let Some(e) = b.tail_expr() {
- for_each_inner_tail(&e, cb);
- }
- }
- ast::Expr::EffectExpr(e) => match e.effect() {
- ast::Effect::Label(_) | ast::Effect::Unsafe(_) => {
- if let Some(e) = e.block_expr().and_then(|b| b.tail_expr()) {
- for_each_inner_tail(&e, cb);
- }
- }
- ast::Effect::Async(_) | ast::Effect::Try(_) | ast::Effect::Const(_) => cb(expr),
- },
- ast::Expr::IfExpr(if_) => {
- if_.blocks().for_each(|block| for_each_inner_tail(&ast::Expr::BlockExpr(block), cb))
- }
- ast::Expr::LoopExpr(l) => {
- for_each_break(l.label(), l.loop_body(), &mut |b| cb(&ast::Expr::BreakExpr(b)))
- }
- ast::Expr::MatchExpr(m) => {
- if let Some(arms) = m.match_arm_list() {
- arms.arms().filter_map(|arm| arm.expr()).for_each(|e| for_each_inner_tail(&e, cb));
- }
- }
- ast::Expr::ArrayExpr(_)
- | ast::Expr::AwaitExpr(_)
- | ast::Expr::BinExpr(_)
- | ast::Expr::BoxExpr(_)
- | ast::Expr::BreakExpr(_)
- | ast::Expr::CallExpr(_)
- | ast::Expr::CastExpr(_)
- | ast::Expr::ClosureExpr(_)
- | ast::Expr::ContinueExpr(_)
- | ast::Expr::FieldExpr(_)
- | ast::Expr::ForExpr(_)
- | ast::Expr::IndexExpr(_)
- | ast::Expr::Literal(_)
- | ast::Expr::MacroCall(_)
- | ast::Expr::MacroStmts(_)
- | ast::Expr::MethodCallExpr(_)
- | ast::Expr::ParenExpr(_)
- | ast::Expr::PathExpr(_)
- | ast::Expr::PrefixExpr(_)
- | ast::Expr::RangeExpr(_)
- | ast::Expr::RecordExpr(_)
- | ast::Expr::RefExpr(_)
- | ast::Expr::ReturnExpr(_)
- | ast::Expr::TryExpr(_)
- | ast::Expr::TupleExpr(_)
- | ast::Expr::WhileExpr(_)
- | ast::Expr::YieldExpr(_) => cb(expr),
- }
-}
-
-fn for_each_break(
- label: Option<ast::Label>,
- body: Option<ast::BlockExpr>,
- cb: &mut dyn FnMut(ast::BreakExpr),
-) {
- let label = label.and_then(|lbl| lbl.lifetime());
- let mut depth = 0;
- if let Some(b) = body {
- let preorder = &mut b.syntax().preorder();
- let ev_as_expr = |ev| match ev {
- WalkEvent::Enter(it) => Some(WalkEvent::Enter(ast::Expr::cast(it)?)),
- WalkEvent::Leave(it) => Some(WalkEvent::Leave(ast::Expr::cast(it)?)),
- };
- let eq_label = |lt: Option<ast::Lifetime>| {
- lt.zip(label.as_ref()).map_or(false, |(lt, lbl)| lt.text() == lbl.text())
- };
- while let Some(node) = preorder.find_map(ev_as_expr) {
- match node {
- WalkEvent::Enter(expr) => match expr {
- ast::Expr::LoopExpr(_) | ast::Expr::WhileExpr(_) | ast::Expr::ForExpr(_) => {
- depth += 1
- }
- ast::Expr::EffectExpr(e) if e.label().is_some() => depth += 1,
- ast::Expr::BreakExpr(b) if depth == 0 || eq_label(b.lifetime()) => {
- cb(b);
- }
- _ => (),
- },
- WalkEvent::Leave(expr) => match expr {
- ast::Expr::LoopExpr(_) | ast::Expr::WhileExpr(_) | ast::Expr::ForExpr(_) => {
- depth -= 1
- }
- ast::Expr::EffectExpr(e) if e.label().is_some() => depth -= 1,
- _ => (),
- },
- }
- }
- }
-}
-
fn cover_range(r0: Option<TextRange>, r1: Option<TextRange>) -> Option<TextRange> {
match (r0, r1) {
(Some(r0), Some(r1)) => Some(r0.cover(r1)),
}
}
+fn find_defs(sema: &Semantics<RootDatabase>, token: SyntaxToken) -> FxHashSet<Definition> {
+ sema.descend_into_macros(token)
+ .into_iter()
+ .flat_map(|token| Definition::from_token(sema, &token))
+ .collect()
+}
+
#[cfg(test)]
mod tests {
use crate::fixture;
use super::*;
fn check(ra_fixture: &str) {
+ let config = HighlightRelatedConfig {
+ break_points: true,
+ exit_points: true,
+ references: true,
+ yield_points: true,
+ };
+
+ check_with_config(ra_fixture, config);
+ }
+
+ fn check_with_config(ra_fixture: &str, config: HighlightRelatedConfig) {
let (analysis, pos, annotations) = fixture::annotations(ra_fixture);
- let hls = analysis.highlight_related(pos).unwrap().unwrap();
+
+ let hls = analysis.highlight_related(config, pos).unwrap().unwrap_or_default();
let mut expected = annotations
.into_iter()
.map(|hl| {
(
hl.range,
- hl.access.map(|it| {
+ hl.category.map(|it| {
match it {
- ReferenceAccess::Read => "read",
- ReferenceAccess::Write => "write",
+ ReferenceCategory::Read => "read",
+ ReferenceCategory::Write => "write",
}
.to_string()
}),
mod foo;
//- /foo.rs
use self$0;
+ // ^^^^
"#,
);
}
);
}
+ #[test]
+ fn test_hl_local_in_attr() {
+ check(
+ r#"
+//- proc_macros: identity
+#[proc_macros::identity]
+fn foo() {
+ let mut bar = 3;
+ // ^^^ write
+ bar$0;
+ // ^^^ read
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_multi_macro_usage() {
+ check(
+ r#"
+macro_rules! foo {
+ ($ident:ident) => {
+ fn $ident() -> $ident { loop {} }
+ struct $ident;
+ }
+}
+
+foo!(bar$0);
+ // ^^^
+fn foo() {
+ let bar: bar = bar();
+ // ^^^
+ // ^^^
+}
+"#,
+ );
+ check(
+ r#"
+macro_rules! foo {
+ ($ident:ident) => {
+ fn $ident() -> $ident { loop {} }
+ struct $ident;
+ }
+}
+
+foo!(bar);
+ // ^^^
+fn foo() {
+ let bar: bar$0 = bar();
+ // ^^^
+}
+"#,
+ );
+ }
+
#[test]
fn test_hl_yield_points() {
check(
);
}
+ #[test]
+ fn test_hl_exit_points3() {
+ check(
+ r#"
+fn$0 foo() -> u32 {
+ if true {
+ return 0;
+ // ^^^^^^
+ }
+
+ 0?;
+ // ^
+ 0xDEAD_BEEF
+ // ^^^^^^^^^^^
+}
+"#,
+ );
+ }
+
#[test]
fn test_hl_prefer_ref_over_tail_exit() {
check(
5
// ^
}
+ } else if false {
+ 0
+ // ^
} else {
match 5 {
6 => 100,
);
}
+ #[test]
+ fn test_hl_inner_tail_exit_points_labeled_block() {
+ check(
+ r#"
+fn foo() ->$0 u32 {
+ 'foo: {
+ break 'foo 0;
+ // ^^^^^
+ loop {
+ break;
+ break 'foo 0;
+ // ^^^^^
+ }
+ 0
+ // ^
+ }
+}
+"#,
+ );
+ }
+
#[test]
fn test_hl_break_loop() {
check(
);
}
+ #[test]
+ fn test_hl_break_loop2() {
+ check(
+ r#"
+fn foo() {
+ 'outer: loop {
+ break;
+ 'inner: loop {
+ // ^^^^^^^^^^^^
+ break;
+ // ^^^^^
+ 'innermost: loop {
+ break 'outer;
+ break 'inner;
+ // ^^^^^^^^^^^^
+ }
+ break 'outer;
+ break$0;
+ // ^^^^^
+ }
+ break;
+ }
+}
+"#,
+ );
+ }
+
#[test]
fn test_hl_break_for() {
check(
"#,
);
}
+
+ #[test]
+ fn test_hl_field_shorthand() {
+ check(
+ r#"
+struct Struct { field: u32 }
+ //^^^^^
+fn function(field: u32) {
+ //^^^^^
+ Struct { field$0 }
+ //^^^^^ read
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_hl_disabled_ref_local() {
+ let config = HighlightRelatedConfig {
+ references: false,
+ break_points: true,
+ exit_points: true,
+ yield_points: true,
+ };
+
+ let ra_fixture = r#"
+fn foo() {
+ let x$0 = 5;
+ let y = x * 2;
+}"#;
+
+ check_with_config(ra_fixture, config);
+ }
+
+ #[test]
+ fn test_hl_disabled_ref_local_preserved_break() {
+ let config = HighlightRelatedConfig {
+ references: false,
+ break_points: true,
+ exit_points: true,
+ yield_points: true,
+ };
+
+ let ra_fixture = r#"
+fn foo() {
+ let x$0 = 5;
+ let y = x * 2;
+
+ loop {
+ break;
+ }
+}"#;
+
+ check_with_config(ra_fixture, config.clone());
+
+ let ra_fixture = r#"
+fn foo() {
+ let x = 5;
+ let y = x * 2;
+
+ loop$0 {
+// ^^^^
+ break;
+// ^^^^^
+ }
+}"#;
+
+ check_with_config(ra_fixture, config);
+ }
+
+ #[test]
+ fn test_hl_disabled_ref_local_preserved_yield() {
+ let config = HighlightRelatedConfig {
+ references: false,
+ break_points: true,
+ exit_points: true,
+ yield_points: true,
+ };
+
+ let ra_fixture = r#"
+async fn foo() {
+ let x$0 = 5;
+ let y = x * 2;
+
+ 0.await;
+}"#;
+
+ check_with_config(ra_fixture, config.clone());
+
+ let ra_fixture = r#"
+ async fn foo() {
+// ^^^^^
+ let x = 5;
+ let y = x * 2;
+
+ 0.await$0;
+// ^^^^^
+}"#;
+
+ check_with_config(ra_fixture, config);
+ }
+
+ #[test]
+ fn test_hl_disabled_ref_local_preserved_exit() {
+ let config = HighlightRelatedConfig {
+ references: false,
+ break_points: true,
+ exit_points: true,
+ yield_points: true,
+ };
+
+ let ra_fixture = r#"
+fn foo() -> i32 {
+ let x$0 = 5;
+ let y = x * 2;
+
+ if true {
+ return y;
+ }
+
+ 0?
+}"#;
+
+ check_with_config(ra_fixture, config.clone());
+
+ let ra_fixture = r#"
+fn foo() ->$0 i32 {
+ let x = 5;
+ let y = x * 2;
+
+ if true {
+ return y;
+// ^^^^^^
+ }
+
+ 0?
+// ^
+"#;
+
+ check_with_config(ra_fixture, config);
+ }
+
+ #[test]
+ fn test_hl_disabled_break() {
+ let config = HighlightRelatedConfig {
+ references: true,
+ break_points: false,
+ exit_points: true,
+ yield_points: true,
+ };
+
+ let ra_fixture = r#"
+fn foo() {
+ loop {
+ break$0;
+ }
+}"#;
+
+ check_with_config(ra_fixture, config);
+ }
+
+ #[test]
+ fn test_hl_disabled_yield() {
+ let config = HighlightRelatedConfig {
+ references: true,
+ break_points: true,
+ exit_points: true,
+ yield_points: false,
+ };
+
+ let ra_fixture = r#"
+async$0 fn foo() {
+ 0.await;
+}"#;
+
+ check_with_config(ra_fixture, config);
+ }
+
+ #[test]
+ fn test_hl_disabled_exit() {
+ let config = HighlightRelatedConfig {
+ references: true,
+ break_points: true,
+ exit_points: false,
+ yield_points: true,
+ };
+
+ let ra_fixture = r#"
+fn foo() ->$0 i32 {
+ if true {
+ return -1;
+ }
+
+ 42
+}"#;
+
+ check_with_config(ra_fixture, config);
+ }
}