use std::iter; use hir::{DescendPreference, Semantics}; use ide_db::{ base_db::{FileId, FilePosition, FileRange}, defs::{Definition, IdentClass}, helpers::pick_best_token, search::{FileReference, ReferenceCategory, SearchScope}, syntax_helpers::node_ext::{ for_each_break_and_continue_expr, for_each_tail_expr, full_path_of_name_ref, walk_expr, }, FxHashSet, RootDatabase, }; use syntax::{ ast::{self, HasLoopBody}, match_ast, AstNode, SyntaxKind::{self, IDENT, INT_NUMBER}, SyntaxToken, TextRange, T, }; use crate::{navigation_target::ToNav, NavigationTarget, TryToNav}; #[derive(PartialEq, Eq, Hash)] pub struct HighlightedRange { pub range: TextRange, // 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, } #[derive(Default, Clone)] pub struct HighlightRelatedConfig { pub references: bool, pub exit_points: bool, pub break_points: bool, pub closure_captures: 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 // .. additionally, if the identifier is a trait in a where clause, type parameter trait bound or use item, highlights all references to that trait's assoc items in the corresponding scope // . if on an `async` or `await` token, highlights all yield points for that async 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 // . if on a `move` or `|` token that belongs to a closure, highlights all captures of the closure. // // Note: `?`, `|` and `->` do not currently trigger this behavior in the VSCode editor. pub(crate) fn highlight_related( sema: &Semantics<'_, RootDatabase>, config: HighlightRelatedConfig, pos @ FilePosition { offset, file_id }: FilePosition, ) -> Option> { let _p = profile::span("highlight_related"); 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![->] => 4, kind if kind.is_keyword() => 3, IDENT | INT_NUMBER => 2, T![|] => 1, _ => 0, })?; // most if not all of these should be re-implemented with information seeded from hir match token.kind() { 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] | T![continue] if config.break_points => { highlight_break_points(token) } T![|] if config.closure_captures => highlight_closure_captures(sema, token, file_id), T![move] if config.closure_captures => highlight_closure_captures(sema, token, file_id), _ if config.references => highlight_references(sema, token, pos), _ => None, } } fn highlight_closure_captures( sema: &Semantics<'_, RootDatabase>, token: SyntaxToken, file_id: FileId, ) -> Option> { let closure = token.parent_ancestors().take(2).find_map(ast::ClosureExpr::cast)?; let search_range = closure.body()?.syntax().text_range(); let ty = &sema.type_of_expr(&closure.into())?.original; let c = ty.as_closure()?; Some( c.captured_items(sema.db) .into_iter() .map(|capture| capture.local()) .flat_map(|local| { let usages = Definition::Local(local) .usages(sema) .in_scope(&SearchScope::file_range(FileRange { file_id, range: search_range })) .include_self_refs() .all() .references .remove(&file_id) .into_iter() .flatten() .map(|FileReference { category, range, .. }| HighlightedRange { range, category, }); let category = local.is_mut(sema.db).then_some(ReferenceCategory::Write); local .sources(sema.db) .into_iter() .flat_map(|x| x.to_nav(sema.db)) .filter(|decl| decl.file_id == file_id) .filter_map(|decl| decl.focus_range) .map(move |range| HighlightedRange { range, category }) .chain(usages) }) .collect(), ) } fn highlight_references( sema: &Semantics<'_, RootDatabase>, token: SyntaxToken, FilePosition { file_id, offset }: FilePosition, ) -> Option> { let defs = if let Some((range, resolution)) = sema.check_for_format_args_template(token.clone(), offset) { match resolution.map(Definition::from) { Some(def) => iter::once(def).collect(), None => return Some(vec![HighlightedRange { range, category: None }]), } } else { find_defs(sema, token.clone()) }; let usages = defs .iter() .filter_map(|&d| { d.usages(sema) .in_scope(&SearchScope::single_file(file_id)) .include_self_refs() .all() .references .remove(&file_id) }) .flatten() .map(|FileReference { category, range, .. }| HighlightedRange { range, category }); let mut res = FxHashSet::default(); for &def in &defs { // highlight trait usages if let Definition::Trait(t) = def { let trait_item_use_scope = (|| { let name_ref = token.parent().and_then(ast::NameRef::cast)?; let path = full_path_of_name_ref(&name_ref)?; let parent = path.syntax().parent()?; match_ast! { match parent { ast::UseTree(it) => it.syntax().ancestors().find(|it| { ast::SourceFile::can_cast(it.kind()) || ast::Module::can_cast(it.kind()) }), ast::PathType(it) => it .syntax() .ancestors() .nth(2) .and_then(ast::TypeBoundList::cast)? .syntax() .parent() .filter(|it| ast::WhereClause::can_cast(it.kind()) || ast::TypeParam::can_cast(it.kind()))? .ancestors() .find(|it| { ast::Item::can_cast(it.kind()) }), _ => None, } } })(); if let Some(trait_item_use_scope) = trait_item_use_scope { res.extend( t.items_with_supertraits(sema.db) .into_iter() .filter_map(|item| { Definition::from(item) .usages(sema) .set_scope(Some(&SearchScope::file_range(FileRange { file_id, range: trait_item_use_scope.text_range(), }))) .include_self_refs() .all() .references .remove(&file_id) }) .flatten() .map(|FileReference { category, range, .. }| HighlightedRange { range, category, }), ); } } // highlight the defs themselves match def { Definition::Local(local) => { let category = local.is_mut(sema.db).then_some(ReferenceCategory::Write); local .sources(sema.db) .into_iter() .flat_map(|x| x.to_nav(sema.db)) .filter(|decl| decl.file_id == file_id) .filter_map(|decl| decl.focus_range) .map(|range| HighlightedRange { range, category }) .for_each(|x| { res.insert(x); }); } def => { let navs = match def { Definition::Module(module) => { NavigationTarget::from_module_to_decl(sema.db, module) } def => match def.try_to_nav(sema.db) { Some(it) => it, None => continue, }, }; for nav in navs { if nav.file_id != file_id { continue; } let hl_range = nav.focus_range.map(|range| { let category = matches!(def, Definition::Local(l) if l.is_mut(sema.db)) .then_some(ReferenceCategory::Write); HighlightedRange { range, category } }); if let Some(hl_range) = hl_range { res.insert(hl_range); } } } } } res.extend(usages); if res.is_empty() { None } else { Some(res.into_iter().collect()) } } fn highlight_exit_points( sema: &Semantics<'_, RootDatabase>, token: SyntaxToken, ) -> Option> { fn hl( sema: &Semantics<'_, RootDatabase>, def_ranges: [Option; 2], body: Option, ) -> Option> { let mut highlights = Vec::new(); highlights.extend( def_ranges .into_iter() .flatten() .map(|range| HighlightedRange { category: None, range }), ); let body = body?; walk_expr(&body, &mut |expr| match expr { ast::Expr::ReturnExpr(expr) => { if let Some(token) = expr.return_token() { highlights.push(HighlightedRange { category: None, range: token.text_range() }); } } ast::Expr::TryExpr(try_) => { if let Some(token) = try_.question_mark_token() { highlights.push(HighlightedRange { category: None, range: token.text_range() }); } } ast::Expr::MethodCallExpr(_) | ast::Expr::CallExpr(_) | ast::Expr::MacroExpr(_) => { if sema.type_of_expr(&expr).map_or(false, |ty| ty.original.is_never()) { highlights.push(HighlightedRange { category: None, range: expr.syntax().text_range(), }); } } _ => (), }); let tail = match body { ast::Expr::BlockExpr(b) => b.tail_expr(), e => Some(e), }; if let Some(tail) = 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 { category: None, range }) }); } Some(highlights) } for anc in token.parent_ancestors() { return match_ast! { match anc { ast::Fn(fn_) => hl(sema, [fn_.fn_token().map(|it| it.text_range()), None], fn_.body().map(ast::Expr::BlockExpr)), ast::ClosureExpr(closure) => hl( sema, closure.param_list().map_or([None; 2], |p| [p.l_paren_token().map(|it| it.text_range()), p.r_paren_token().map(|it| it.text_range())]), closure.body() ), ast::BlockExpr(block_expr) => if matches!(block_expr.modifier(), Some(ast::BlockModifier::Async(_) | ast::BlockModifier::Try(_)| ast::BlockModifier::Const(_))) { hl( sema, [block_expr.modifier().and_then(|modifier| match modifier { ast::BlockModifier::Async(t) | ast::BlockModifier::Try(t) | ast::BlockModifier::Const(t) => Some(t.text_range()), _ => None, }), None], Some(block_expr.into()) ) } else { continue; }, _ => continue, } }; } None } fn highlight_break_points(token: SyntaxToken) -> Option> { fn hl( cursor_token_kind: SyntaxKind, token: Option, label: Option, body: Option, ) -> Option> { 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 { category: None, range })); for_each_break_and_continue_expr(label, body, &mut |expr| { let range: Option = match (cursor_token_kind, expr) { (T![for] | T![while] | T![loop] | T![break], ast::Expr::BreakExpr(break_)) => { cover_range( break_.break_token().map(|it| it.text_range()), break_.lifetime().map(|it| it.syntax().text_range()), ) } ( T![for] | T![while] | T![loop] | T![continue], ast::Expr::ContinueExpr(continue_), ) => cover_range( continue_.continue_token().map(|it| it.text_range()), continue_.lifetime().map(|it| it.syntax().text_range()), ), _ => None, }; highlights.extend(range.map(|range| HighlightedRange { category: None, range })); }); Some(highlights) } let parent = token.parent()?; let lbl = match_ast! { match parent { ast::BreakExpr(b) => b.lifetime(), ast::ContinueExpr(c) => c.lifetime(), 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::BlockExpr(b) => Some(b.label().and_then(|it| it.lifetime())?), _ => return None, } }; let lbl = lbl.as_ref(); let label_matches = |def_lbl: Option| match lbl { Some(lbl) => { Some(lbl.text()) == def_lbl.and_then(|it| it.lifetime()).as_ref().map(|it| it.text()) } None => true, }; let token_kind = token.kind(); for anc in token.parent_ancestors().flat_map(ast::Expr::cast) { return match anc { ast::Expr::LoopExpr(l) if label_matches(l.label()) => hl( token_kind, l.loop_token(), l.label(), l.loop_body().and_then(|it| it.stmt_list()), ), ast::Expr::ForExpr(f) if label_matches(f.label()) => hl( token_kind, f.for_token(), f.label(), f.loop_body().and_then(|it| it.stmt_list()), ), ast::Expr::WhileExpr(w) if label_matches(w.label()) => hl( token_kind, w.while_token(), w.label(), w.loop_body().and_then(|it| it.stmt_list()), ), ast::Expr::BlockExpr(e) if e.label().is_some() && label_matches(e.label()) => { hl(token_kind, None, e.label(), e.stmt_list()) } _ => continue, }; } None } fn highlight_yield_points(token: SyntaxToken) -> Option> { fn hl( async_token: Option, body: Option, ) -> Option> { let mut highlights = vec![HighlightedRange { category: None, range: async_token?.text_range() }]; if let Some(body) = body { walk_expr(&body, &mut |expr| { if let ast::Expr::AwaitExpr(expr) = expr { if let Some(token) = expr.await_token() { highlights .push(HighlightedRange { category: None, range: token.text_range() }); } } }); } Some(highlights) } for anc in token.parent_ancestors() { return match_ast! { match anc { ast::Fn(fn_) => hl(fn_.async_token(), fn_.body().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 } fn cover_range(r0: Option, r1: Option) -> Option { match (r0, r1) { (Some(r0), Some(r1)) => Some(r0.cover(r1)), (Some(range), None) => Some(range), (None, Some(range)) => Some(range), (None, None) => None, } } fn find_defs(sema: &Semantics<'_, RootDatabase>, token: SyntaxToken) -> FxHashSet { sema.descend_into_macros(DescendPreference::None, token) .into_iter() .filter_map(|token| IdentClass::classify_token(sema, &token)) .flat_map(IdentClass::definitions_no_ops) .collect() } #[cfg(test)] mod tests { use crate::fixture; use super::*; const ENABLED_CONFIG: HighlightRelatedConfig = HighlightRelatedConfig { break_points: true, exit_points: true, references: true, closure_captures: true, yield_points: true, }; #[track_caller] fn check(ra_fixture: &str) { check_with_config(ra_fixture, ENABLED_CONFIG); } #[track_caller] fn check_with_config(ra_fixture: &str, config: HighlightRelatedConfig) { let (analysis, pos, annotations) = fixture::annotations(ra_fixture); let hls = analysis.highlight_related(config, pos).unwrap().unwrap_or_default(); let mut expected = annotations .into_iter() .map(|(r, access)| (r.range, (!access.is_empty()).then_some(access))) .collect::>(); let mut actual = hls .into_iter() .map(|hl| { ( hl.range, hl.category.map(|it| { match it { ReferenceCategory::Read => "read", ReferenceCategory::Write => "write", ReferenceCategory::Import => "import", } .to_string() }), ) }) .collect::>(); actual.sort_by_key(|(range, _)| range.start()); expected.sort_by_key(|(range, _)| range.start()); assert_eq!(expected, actual); } #[test] fn test_hl_tuple_fields() { check( r#" struct Tuple(u32, u32); fn foo(t: Tuple) { t.0$0; // ^ read t.0; // ^ read } "#, ); } #[test] fn test_hl_module() { check( r#" //- /lib.rs mod foo$0; // ^^^ //- /foo.rs struct Foo; "#, ); } #[test] fn test_hl_self_in_crate_root() { check( r#" use crate$0; //^^^^^ import use self; //^^^^ import mod __ { use super; //^^^^^ import } "#, ); check( r#" //- /main.rs crate:main deps:lib use lib$0; //^^^ import //- /lib.rs crate:lib "#, ); } #[test] fn test_hl_self_in_module() { check( r#" //- /lib.rs mod foo; //- /foo.rs use self$0; // ^^^^ import "#, ); } #[test] fn test_hl_local() { check( r#" fn foo() { let mut bar = 3; // ^^^ write bar$0; // ^^^ read } "#, ); } #[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( r#" pub async fn foo() { // ^^^^^ let x = foo() .await$0 // ^^^^^ .await; // ^^^^^ || { 0.await }; (async { 0.await }).await // ^^^^^ } "#, ); } #[test] fn test_hl_yield_points2() { check( r#" pub async$0 fn foo() { // ^^^^^ let x = foo() .await // ^^^^^ .await; // ^^^^^ || { 0.await }; (async { 0.await }).await // ^^^^^ } "#, ); } #[test] fn test_hl_let_else_yield_points() { check( r#" pub async fn foo() { // ^^^^^ let x = foo() .await$0 // ^^^^^ .await; // ^^^^^ || { 0.await }; let Some(_) = None else { foo().await // ^^^^^ }; (async { 0.await }).await // ^^^^^ } "#, ); } #[test] fn test_hl_yield_nested_fn() { check( r#" async fn foo() { async fn foo2() { // ^^^^^ async fn foo3() { 0.await } 0.await$0 // ^^^^^ } 0.await } "#, ); } #[test] fn test_hl_yield_nested_async_blocks() { check( r#" async fn foo() { (async { // ^^^^^ (async { 0.await }).await$0 } // ^^^^^ ).await; } "#, ); } #[test] fn test_hl_exit_points() { check( r#" fn foo() -> u32 { //^^ if true { return$0 0; // ^^^^^^ } 0?; // ^ 0xDEAD_BEEF // ^^^^^^^^^^^ } "#, ); } #[test] fn test_hl_exit_points2() { check( r#" fn foo() ->$0 u32 { //^^ if true { return 0; // ^^^^^^ } 0?; // ^ 0xDEAD_BEEF // ^^^^^^^^^^^ } "#, ); } #[test] fn test_hl_exit_points3() { check( r#" fn$0 foo() -> u32 { //^^ if true { return 0; // ^^^^^^ } 0?; // ^ 0xDEAD_BEEF // ^^^^^^^^^^^ } "#, ); } #[test] fn test_hl_let_else_exit_points() { check( r#" fn$0 foo() -> u32 { //^^ let Some(bar) = None else { return 0; // ^^^^^^ }; 0?; // ^ 0xDEAD_BEEF // ^^^^^^^^^^^ } "#, ); } #[test] fn test_hl_prefer_ref_over_tail_exit() { check( r#" fn foo() -> u32 { // ^^^ if true { return 0; } 0?; foo$0() // ^^^ } "#, ); } #[test] fn test_hl_never_call_is_exit_point() { check( r#" struct Never; impl Never { fn never(self) -> ! { loop {} } } macro_rules! never { () => { never() } } fn never() -> ! { loop {} } fn foo() ->$0 u32 { //^^ never(); // ^^^^^^^ never!(); // ^^^^^^^^ Never.never(); // ^^^^^^^^^^^^^ 0 // ^ } "#, ); } #[test] fn test_hl_inner_tail_exit_points() { check( r#" fn foo() ->$0 u32 { //^^ if true { unsafe { return 5; // ^^^^^^ 5 // ^ } } else if false { 0 // ^ } else { match 5 { 6 => 100, // ^^^ 7 => loop { break 5; // ^^^^^ } 8 => 'a: loop { 'b: loop { break 'a 5; // ^^^^^ break 'b 5; break 5; }; } // _ => 500, // ^^^ } } } "#, ); } #[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_inner_tail_exit_points_loops() { check( r#" fn foo() ->$0 u32 { //^^ 'foo: while { return 0; true } { // ^^^^^^ break 'foo 0; // ^^^^^ return 0; // ^^^^^^ } } "#, ); } #[test] fn test_hl_break_loop() { check( r#" fn foo() { 'outer: loop { // ^^^^^^^^^^^^ break; // ^^^^^ 'inner: loop { break; 'innermost: loop { break 'outer; // ^^^^^^^^^^^^ break 'inner; } break$0 'outer; // ^^^^^^^^^^^^ break; } break; // ^^^^^ } } "#, ); } #[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( r#" fn foo() { 'outer: for _ in () { // ^^^^^^^^^^^ break; // ^^^^^ 'inner: for _ in () { break; 'innermost: for _ in () { break 'outer; // ^^^^^^^^^^^^ break 'inner; } break$0 'outer; // ^^^^^^^^^^^^ break; } break; // ^^^^^ } } "#, ); } #[test] fn test_hl_break_for_but_not_continue() { check( r#" fn foo() { 'outer: for _ in () { // ^^^^^^^^^^^ break; // ^^^^^ continue; 'inner: for _ in () { break; continue; 'innermost: for _ in () { continue 'outer; break 'outer; // ^^^^^^^^^^^^ continue 'inner; break 'inner; } break$0 'outer; // ^^^^^^^^^^^^ continue 'outer; break; continue; } break; // ^^^^^ continue; } } "#, ); } #[test] fn test_hl_continue_for_but_not_break() { check( r#" fn foo() { 'outer: for _ in () { // ^^^^^^^^^^^ break; continue; // ^^^^^^^^ 'inner: for _ in () { break; continue; 'innermost: for _ in () { continue 'outer; // ^^^^^^^^^^^^^^^ break 'outer; continue 'inner; break 'inner; } break 'outer; continue$0 'outer; // ^^^^^^^^^^^^^^^ break; continue; } break; continue; // ^^^^^^^^ } } "#, ); } #[test] fn test_hl_break_and_continue() { check( r#" fn foo() { 'outer: fo$0r _ in () { // ^^^^^^^^^^^ break; // ^^^^^ continue; // ^^^^^^^^ 'inner: for _ in () { break; continue; 'innermost: for _ in () { continue 'outer; // ^^^^^^^^^^^^^^^ break 'outer; // ^^^^^^^^^^^^ continue 'inner; break 'inner; } break 'outer; // ^^^^^^^^^^^^ continue 'outer; // ^^^^^^^^^^^^^^^ break; continue; } break; // ^^^^^ continue; // ^^^^^^^^ } } "#, ); } #[test] fn test_hl_break_while() { check( r#" fn foo() { 'outer: while true { // ^^^^^^^^^^^^^ break; // ^^^^^ 'inner: while true { break; 'innermost: while true { break 'outer; // ^^^^^^^^^^^^ break 'inner; } break$0 'outer; // ^^^^^^^^^^^^ break; } break; // ^^^^^ } } "#, ); } #[test] fn test_hl_break_labeled_block() { check( r#" fn foo() { 'outer: { // ^^^^^^^ break; // ^^^^^ 'inner: { break; 'innermost: { break 'outer; // ^^^^^^^^^^^^ break 'inner; } break$0 'outer; // ^^^^^^^^^^^^ break; } break; // ^^^^^ } } "#, ); } #[test] fn test_hl_break_unlabeled_loop() { check( r#" fn foo() { loop { // ^^^^ break$0; // ^^^^^ } } "#, ); } #[test] fn test_hl_break_unlabeled_block_in_loop() { check( r#" fn foo() { loop { // ^^^^ { break$0; // ^^^^^ } } } "#, ); } #[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, ..ENABLED_CONFIG }; check_with_config( r#" fn foo() { let x$0 = 5; let y = x * 2; } "#, config, ); } #[test] fn test_hl_disabled_ref_local_preserved_break() { let config = HighlightRelatedConfig { references: false, ..ENABLED_CONFIG }; check_with_config( r#" fn foo() { let x$0 = 5; let y = x * 2; loop { break; } } "#, config.clone(), ); check_with_config( r#" fn foo() { let x = 5; let y = x * 2; loop$0 { // ^^^^ break; // ^^^^^ } } "#, config, ); } #[test] fn test_hl_disabled_ref_local_preserved_yield() { let config = HighlightRelatedConfig { references: false, ..ENABLED_CONFIG }; check_with_config( r#" async fn foo() { let x$0 = 5; let y = x * 2; 0.await; } "#, config.clone(), ); check_with_config( r#" async fn foo() { // ^^^^^ let x = 5; let y = x * 2; 0.await$0; // ^^^^^ } "#, config, ); } #[test] fn test_hl_disabled_ref_local_preserved_exit() { let config = HighlightRelatedConfig { references: false, ..ENABLED_CONFIG }; check_with_config( r#" fn foo() -> i32 { let x$0 = 5; let y = x * 2; if true { return y; } 0? } "#, config.clone(), ); check_with_config( r#" fn foo() ->$0 i32 { //^^ let x = 5; let y = x * 2; if true { return y; // ^^^^^^ } 0? // ^ "#, config, ); } #[test] fn test_hl_disabled_break() { let config = HighlightRelatedConfig { break_points: false, ..ENABLED_CONFIG }; check_with_config( r#" fn foo() { loop { break$0; } } "#, config, ); } #[test] fn test_hl_disabled_yield() { let config = HighlightRelatedConfig { yield_points: false, ..ENABLED_CONFIG }; check_with_config( r#" async$0 fn foo() { 0.await; } "#, config, ); } #[test] fn test_hl_disabled_exit() { let config = HighlightRelatedConfig { exit_points: false, ..ENABLED_CONFIG }; check_with_config( r#" fn foo() ->$0 i32 { if true { return -1; } 42 }"#, config, ); } #[test] fn test_hl_multi_local() { check( r#" fn foo(( foo$0 //^^^ | foo //^^^ | foo //^^^ ): ()) { foo; //^^^read let foo; } "#, ); check( r#" fn foo(( foo //^^^ | foo$0 //^^^ | foo //^^^ ): ()) { foo; //^^^read let foo; } "#, ); check( r#" fn foo(( foo //^^^ | foo //^^^ | foo //^^^ ): ()) { foo$0; //^^^read let foo; } "#, ); } #[test] fn test_hl_trait_impl_methods() { check( r#" trait Trait { fn func$0(self) {} //^^^^ } impl Trait for () { fn func(self) {} //^^^^ } fn main() { <()>::func(()); //^^^^ ().func(); //^^^^ } "#, ); check( r#" trait Trait { fn func(self) {} } impl Trait for () { fn func$0(self) {} //^^^^ } fn main() { <()>::func(()); //^^^^ ().func(); //^^^^ } "#, ); check( r#" trait Trait { fn func(self) {} } impl Trait for () { fn func(self) {} //^^^^ } fn main() { <()>::func(()); //^^^^ ().func$0(); //^^^^ } "#, ); } #[test] fn test_assoc_type_highlighting() { check( r#" trait Trait { type Output; // ^^^^^^ } impl Trait for () { type Output$0 = (); // ^^^^^^ } "#, ); } #[test] fn test_closure_capture_pipe() { check( r#" fn f() { let x = 1; // ^ let c = $0|y| x + y; // ^ read } "#, ); } #[test] fn test_closure_capture_move() { check( r#" fn f() { let x = 1; // ^ let c = move$0 |y| x + y; // ^ read } "#, ); } #[test] fn test_trait_highlights_assoc_item_uses() { check( r#" trait Foo { //^^^ type T; const C: usize; fn f() {} fn m(&self) {} } impl Foo for i32 { //^^^ type T = i32; const C: usize = 0; fn f() {} fn m(&self) {} } fn f(t: T) { //^^^ let _: T::T; //^ t.m(); //^ T::C; //^ T::f(); //^ } fn f2(t: T) { //^^^ let _: T::T; t.m(); T::C; T::f(); } "#, ); } #[test] fn implicit_format_args() { check( r#" //- minicore: fmt fn test() { let a = "foo"; // ^ format_args!("hello {a} {a$0} {}", a); // ^read // ^read // ^read } "#, ); } }