]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/highlight_related.rs
Fix break point highlighting not considering outer labels
[rust.git] / crates / ide / src / highlight_related.rs
1 use hir::Semantics;
2 use ide_db::{
3     base_db::FilePosition,
4     defs::Definition,
5     helpers::pick_best_token,
6     search::{FileReference, ReferenceAccess, SearchScope},
7     RootDatabase,
8 };
9 use syntax::{
10     ast::{self, LoopBodyOwner},
11     match_ast, AstNode, SyntaxNode, SyntaxToken, TextRange, WalkEvent, T,
12 };
13
14 use crate::{display::TryToNav, references, NavigationTarget};
15
16 pub struct HighlightedRange {
17     pub range: TextRange,
18     pub access: Option<ReferenceAccess>,
19 }
20
21 // Feature: Highlight Related
22 //
23 // Highlights constructs related to the thing under the cursor:
24 // - if on an identifier, highlights all references to that identifier in the current file
25 // - if on an `async` or `await token, highlights all yield points for that async context
26 // - if on a `return` token, `?` character or `->` return type arrow, highlights all exit points for that context
27 // - if on a `break`, `loop`, `while` or `for` token, highlights all break points for that loop or block context
28 pub(crate) fn highlight_related(
29     sema: &Semantics<RootDatabase>,
30     position: FilePosition,
31 ) -> Option<Vec<HighlightedRange>> {
32     let _p = profile::span("highlight_related");
33     let syntax = sema.parse(position.file_id).syntax().clone();
34
35     let token = pick_best_token(syntax.token_at_offset(position.offset), |kind| match kind {
36         T![?] => 2, // prefer `?` when the cursor is sandwiched like `await$0?`
37         T![await]
38         | T![async]
39         | T![return]
40         | T![break]
41         | T![loop]
42         | T![for]
43         | T![while]
44         | T![->] => 1,
45         _ => 0,
46     })?;
47
48     match token.kind() {
49         T![return] | T![?] | T![->] => highlight_exit_points(sema, token),
50         T![await] | T![async] => highlight_yield_points(token),
51         T![break] | T![loop] | T![for] | T![while] => highlight_break_points(token),
52         _ => highlight_references(sema, &syntax, position),
53     }
54 }
55
56 fn highlight_references(
57     sema: &Semantics<RootDatabase>,
58     syntax: &SyntaxNode,
59     FilePosition { offset, file_id }: FilePosition,
60 ) -> Option<Vec<HighlightedRange>> {
61     let def = references::find_def(sema, syntax, offset)?;
62     let usages = def.usages(sema).set_scope(Some(SearchScope::single_file(file_id))).all();
63
64     let declaration = match def {
65         Definition::ModuleDef(hir::ModuleDef::Module(module)) => {
66             Some(NavigationTarget::from_module_to_decl(sema.db, module))
67         }
68         def => def.try_to_nav(sema.db),
69     }
70     .filter(|decl| decl.file_id == file_id)
71     .and_then(|decl| {
72         let range = decl.focus_range?;
73         let access = references::decl_access(&def, syntax, range);
74         Some(HighlightedRange { range, access })
75     });
76
77     let file_refs = usages.references.get(&file_id).map_or(&[][..], Vec::as_slice);
78     let mut res = Vec::with_capacity(file_refs.len() + 1);
79     res.extend(declaration);
80     res.extend(
81         file_refs
82             .iter()
83             .map(|&FileReference { access, range, .. }| HighlightedRange { range, access }),
84     );
85     Some(res)
86 }
87
88 fn highlight_exit_points(
89     sema: &Semantics<RootDatabase>,
90     token: SyntaxToken,
91 ) -> Option<Vec<HighlightedRange>> {
92     fn hl(
93         sema: &Semantics<RootDatabase>,
94         body: Option<ast::Expr>,
95     ) -> Option<Vec<HighlightedRange>> {
96         let mut highlights = Vec::new();
97         let body = body?;
98         walk(&body, &mut |expr| match expr {
99             ast::Expr::ReturnExpr(expr) => {
100                 if let Some(token) = expr.return_token() {
101                     highlights.push(HighlightedRange { access: None, range: token.text_range() });
102                 }
103             }
104             ast::Expr::TryExpr(try_) => {
105                 if let Some(token) = try_.question_mark_token() {
106                     highlights.push(HighlightedRange { access: None, range: token.text_range() });
107                 }
108             }
109             ast::Expr::MethodCallExpr(_) | ast::Expr::CallExpr(_) | ast::Expr::MacroCall(_) => {
110                 if sema.type_of_expr(&expr).map_or(false, |ty| ty.is_never()) {
111                     highlights
112                         .push(HighlightedRange { access: None, range: expr.syntax().text_range() });
113                 }
114             }
115             _ => (),
116         });
117         let tail = match body {
118             ast::Expr::BlockExpr(b) => b.tail_expr(),
119             e => Some(e),
120         };
121
122         if let Some(tail) = tail {
123             for_each_inner_tail(&tail, &mut |tail| {
124                 let range = match tail {
125                     ast::Expr::BreakExpr(b) => b
126                         .break_token()
127                         .map_or_else(|| tail.syntax().text_range(), |tok| tok.text_range()),
128                     _ => tail.syntax().text_range(),
129                 };
130                 highlights.push(HighlightedRange { access: None, range })
131             });
132         }
133         Some(highlights)
134     }
135     for anc in token.ancestors() {
136         return match_ast! {
137             match anc {
138                 ast::Fn(fn_) => hl(sema, fn_.body().map(ast::Expr::BlockExpr)),
139                 ast::ClosureExpr(closure) => hl(sema, closure.body()),
140                 ast::EffectExpr(effect) => if matches!(effect.effect(), ast::Effect::Async(_) | ast::Effect::Try(_)| ast::Effect::Const(_)) {
141                     hl(sema, effect.block_expr().map(ast::Expr::BlockExpr))
142                 } else {
143                     continue;
144                 },
145                 _ => continue,
146             }
147         };
148     }
149     None
150 }
151
152 fn highlight_break_points(token: SyntaxToken) -> Option<Vec<HighlightedRange>> {
153     fn hl(
154         token: Option<SyntaxToken>,
155         label: Option<ast::Label>,
156         body: Option<ast::BlockExpr>,
157     ) -> Option<Vec<HighlightedRange>> {
158         let mut highlights = Vec::new();
159         let range = cover_range(
160             token.map(|tok| tok.text_range()),
161             label.as_ref().map(|it| it.syntax().text_range()),
162         );
163         highlights.extend(range.map(|range| HighlightedRange { access: None, range }));
164         for_each_break(label, body, &mut |break_| {
165             let range = cover_range(
166                 break_.break_token().map(|it| it.text_range()),
167                 break_.lifetime().map(|it| it.syntax().text_range()),
168             );
169             highlights.extend(range.map(|range| HighlightedRange { access: None, range }));
170         });
171         Some(highlights)
172     }
173     let parent = token.parent()?;
174     let lbl = match_ast! {
175         match parent {
176             ast::BreakExpr(b) => b.lifetime(),
177             ast::LoopExpr(l) => l.label().and_then(|it| it.lifetime()),
178             ast::ForExpr(f) => f.label().and_then(|it| it.lifetime()),
179             ast::WhileExpr(w) => w.label().and_then(|it| it.lifetime()),
180             ast::EffectExpr(b) => Some(b.label().and_then(|it| it.lifetime())?),
181             _ => return None,
182         }
183     };
184     let lbl = lbl.as_ref();
185     let label_matches = |def_lbl: Option<ast::Label>| match lbl {
186         Some(lbl) => {
187             Some(lbl.text()) == def_lbl.and_then(|it| it.lifetime()).as_ref().map(|it| it.text())
188         }
189         None => true,
190     };
191     for anc in token.ancestors().flat_map(ast::Expr::cast) {
192         return match anc {
193             ast::Expr::LoopExpr(l) if label_matches(l.label()) => {
194                 hl(l.loop_token(), l.label(), l.loop_body())
195             }
196             ast::Expr::ForExpr(f) if label_matches(f.label()) => {
197                 hl(f.for_token(), f.label(), f.loop_body())
198             }
199             ast::Expr::WhileExpr(w) if label_matches(w.label()) => {
200                 hl(w.while_token(), w.label(), w.loop_body())
201             }
202             ast::Expr::EffectExpr(e) if e.label().is_some() && label_matches(e.label()) => {
203                 hl(None, e.label(), e.block_expr())
204             }
205             _ => continue,
206         };
207     }
208     None
209 }
210
211 fn highlight_yield_points(token: SyntaxToken) -> Option<Vec<HighlightedRange>> {
212     fn hl(
213         async_token: Option<SyntaxToken>,
214         body: Option<ast::Expr>,
215     ) -> Option<Vec<HighlightedRange>> {
216         let mut highlights = Vec::new();
217         highlights.push(HighlightedRange { access: None, range: async_token?.text_range() });
218         if let Some(body) = body {
219             walk(&body, &mut |expr| {
220                 if let ast::Expr::AwaitExpr(expr) = expr {
221                     if let Some(token) = expr.await_token() {
222                         highlights
223                             .push(HighlightedRange { access: None, range: token.text_range() });
224                     }
225                 }
226             });
227         }
228         Some(highlights)
229     }
230     for anc in token.ancestors() {
231         return match_ast! {
232             match anc {
233                 ast::Fn(fn_) => hl(fn_.async_token(), fn_.body().map(ast::Expr::BlockExpr)),
234                 ast::EffectExpr(effect) => hl(effect.async_token(), effect.block_expr().map(ast::Expr::BlockExpr)),
235                 ast::ClosureExpr(closure) => hl(closure.async_token(), closure.body()),
236                 _ => continue,
237             }
238         };
239     }
240     None
241 }
242
243 /// Preorder walk all the expression's child expressions
244 fn walk(expr: &ast::Expr, cb: &mut dyn FnMut(ast::Expr)) {
245     let mut preorder = expr.syntax().preorder();
246     while let Some(event) = preorder.next() {
247         let node = match event {
248             WalkEvent::Enter(node) => node,
249             WalkEvent::Leave(_) => continue,
250         };
251         match ast::Stmt::cast(node.clone()) {
252             // recursively walk the initializer, skipping potential const pat expressions
253             // lets statements aren't usually nested too deeply so this is fine to recurse on
254             Some(ast::Stmt::LetStmt(l)) => {
255                 if let Some(expr) = l.initializer() {
256                     walk(&expr, cb);
257                 }
258                 preorder.skip_subtree();
259             }
260             // Don't skip subtree since we want to process the expression child next
261             Some(ast::Stmt::ExprStmt(_)) => (),
262             // skip inner items which might have their own expressions
263             Some(ast::Stmt::Item(_)) => preorder.skip_subtree(),
264             None => {
265                 if let Some(expr) = ast::Expr::cast(node) {
266                     let is_different_context = match &expr {
267                         ast::Expr::EffectExpr(effect) => {
268                             matches!(
269                                 effect.effect(),
270                                 ast::Effect::Async(_) | ast::Effect::Try(_) | ast::Effect::Const(_)
271                             )
272                         }
273                         ast::Expr::ClosureExpr(__) => true,
274                         _ => false,
275                     };
276                     cb(expr);
277                     if is_different_context {
278                         preorder.skip_subtree();
279                     }
280                 } else {
281                     preorder.skip_subtree();
282                 }
283             }
284         }
285     }
286 }
287
288 // FIXME: doesn't account for labeled breaks in labeled blocks
289 fn for_each_inner_tail(expr: &ast::Expr, cb: &mut dyn FnMut(&ast::Expr)) {
290     match expr {
291         ast::Expr::BlockExpr(b) => {
292             if let Some(e) = b.tail_expr() {
293                 for_each_inner_tail(&e, cb);
294             }
295         }
296         ast::Expr::EffectExpr(e) => match e.effect() {
297             ast::Effect::Label(label) => {
298                 for_each_break(Some(label), e.block_expr(), &mut |b| cb(&ast::Expr::BreakExpr(b)));
299                 if let Some(b) = e.block_expr() {
300                     for_each_inner_tail(&ast::Expr::BlockExpr(b), cb);
301                 }
302             }
303             ast::Effect::Unsafe(_) => {
304                 if let Some(e) = e.block_expr().and_then(|b| b.tail_expr()) {
305                     for_each_inner_tail(&e, cb);
306                 }
307             }
308             ast::Effect::Async(_) | ast::Effect::Try(_) | ast::Effect::Const(_) => cb(expr),
309         },
310         ast::Expr::IfExpr(if_) => {
311             if_.blocks().for_each(|block| for_each_inner_tail(&ast::Expr::BlockExpr(block), cb))
312         }
313         ast::Expr::LoopExpr(l) => {
314             for_each_break(l.label(), l.loop_body(), &mut |b| cb(&ast::Expr::BreakExpr(b)))
315         }
316         ast::Expr::MatchExpr(m) => {
317             if let Some(arms) = m.match_arm_list() {
318                 arms.arms().filter_map(|arm| arm.expr()).for_each(|e| for_each_inner_tail(&e, cb));
319             }
320         }
321         ast::Expr::ArrayExpr(_)
322         | ast::Expr::AwaitExpr(_)
323         | ast::Expr::BinExpr(_)
324         | ast::Expr::BoxExpr(_)
325         | ast::Expr::BreakExpr(_)
326         | ast::Expr::CallExpr(_)
327         | ast::Expr::CastExpr(_)
328         | ast::Expr::ClosureExpr(_)
329         | ast::Expr::ContinueExpr(_)
330         | ast::Expr::FieldExpr(_)
331         | ast::Expr::ForExpr(_)
332         | ast::Expr::IndexExpr(_)
333         | ast::Expr::Literal(_)
334         | ast::Expr::MacroCall(_)
335         | ast::Expr::MacroStmts(_)
336         | ast::Expr::MethodCallExpr(_)
337         | ast::Expr::ParenExpr(_)
338         | ast::Expr::PathExpr(_)
339         | ast::Expr::PrefixExpr(_)
340         | ast::Expr::RangeExpr(_)
341         | ast::Expr::RecordExpr(_)
342         | ast::Expr::RefExpr(_)
343         | ast::Expr::ReturnExpr(_)
344         | ast::Expr::TryExpr(_)
345         | ast::Expr::TupleExpr(_)
346         | ast::Expr::WhileExpr(_)
347         | ast::Expr::YieldExpr(_) => cb(expr),
348     }
349 }
350
351 fn for_each_break(
352     label: Option<ast::Label>,
353     body: Option<ast::BlockExpr>,
354     cb: &mut dyn FnMut(ast::BreakExpr),
355 ) {
356     let label = label.and_then(|lbl| lbl.lifetime());
357     let mut depth = 0;
358     if let Some(b) = body {
359         let preorder = &mut b.syntax().preorder();
360         let ev_as_expr = |ev| match ev {
361             WalkEvent::Enter(it) => Some(WalkEvent::Enter(ast::Expr::cast(it)?)),
362             WalkEvent::Leave(it) => Some(WalkEvent::Leave(ast::Expr::cast(it)?)),
363         };
364         let eq_label = |lt: Option<ast::Lifetime>| {
365             lt.zip(label.as_ref()).map_or(false, |(lt, lbl)| lt.text() == lbl.text())
366         };
367         while let Some(node) = preorder.find_map(ev_as_expr) {
368             match node {
369                 WalkEvent::Enter(expr) => match expr {
370                     ast::Expr::LoopExpr(_) | ast::Expr::WhileExpr(_) | ast::Expr::ForExpr(_) => {
371                         depth += 1
372                     }
373                     ast::Expr::EffectExpr(e) if e.label().is_some() => depth += 1,
374                     ast::Expr::BreakExpr(b)
375                         if (depth == 0 && b.lifetime().is_none()) || eq_label(b.lifetime()) =>
376                     {
377                         cb(b);
378                     }
379                     _ => (),
380                 },
381                 WalkEvent::Leave(expr) => match expr {
382                     ast::Expr::LoopExpr(_) | ast::Expr::WhileExpr(_) | ast::Expr::ForExpr(_) => {
383                         depth -= 1
384                     }
385                     ast::Expr::EffectExpr(e) if e.label().is_some() => depth -= 1,
386                     _ => (),
387                 },
388             }
389         }
390     }
391 }
392
393 fn cover_range(r0: Option<TextRange>, r1: Option<TextRange>) -> Option<TextRange> {
394     match (r0, r1) {
395         (Some(r0), Some(r1)) => Some(r0.cover(r1)),
396         (Some(range), None) => Some(range),
397         (None, Some(range)) => Some(range),
398         (None, None) => None,
399     }
400 }
401
402 #[cfg(test)]
403 mod tests {
404     use crate::fixture;
405
406     use super::*;
407
408     fn check(ra_fixture: &str) {
409         let (analysis, pos, annotations) = fixture::annotations(ra_fixture);
410         let hls = analysis.highlight_related(pos).unwrap().unwrap();
411
412         let mut expected = annotations
413             .into_iter()
414             .map(|(r, access)| (r.range, (!access.is_empty()).then(|| access)))
415             .collect::<Vec<_>>();
416
417         let mut actual = hls
418             .into_iter()
419             .map(|hl| {
420                 (
421                     hl.range,
422                     hl.access.map(|it| {
423                         match it {
424                             ReferenceAccess::Read => "read",
425                             ReferenceAccess::Write => "write",
426                         }
427                         .to_string()
428                     }),
429                 )
430             })
431             .collect::<Vec<_>>();
432         actual.sort_by_key(|(range, _)| range.start());
433         expected.sort_by_key(|(range, _)| range.start());
434
435         assert_eq!(expected, actual);
436     }
437
438     #[test]
439     fn test_hl_module() {
440         check(
441             r#"
442 //- /lib.rs
443 mod foo$0;
444  // ^^^
445 //- /foo.rs
446 struct Foo;
447 "#,
448         );
449     }
450
451     #[test]
452     fn test_hl_self_in_crate_root() {
453         check(
454             r#"
455 use self$0;
456 "#,
457         );
458     }
459
460     #[test]
461     fn test_hl_self_in_module() {
462         check(
463             r#"
464 //- /lib.rs
465 mod foo;
466 //- /foo.rs
467 use self$0;
468 "#,
469         );
470     }
471
472     #[test]
473     fn test_hl_local() {
474         check(
475             r#"
476 fn foo() {
477     let mut bar = 3;
478          // ^^^ write
479     bar$0;
480  // ^^^ read
481 }
482 "#,
483         );
484     }
485
486     #[test]
487     fn test_hl_yield_points() {
488         check(
489             r#"
490 pub async fn foo() {
491  // ^^^^^
492     let x = foo()
493         .await$0
494       // ^^^^^
495         .await;
496       // ^^^^^
497     || { 0.await };
498     (async { 0.await }).await
499                      // ^^^^^
500 }
501 "#,
502         );
503     }
504
505     #[test]
506     fn test_hl_yield_points2() {
507         check(
508             r#"
509 pub async$0 fn foo() {
510  // ^^^^^
511     let x = foo()
512         .await
513       // ^^^^^
514         .await;
515       // ^^^^^
516     || { 0.await };
517     (async { 0.await }).await
518                      // ^^^^^
519 }
520 "#,
521         );
522     }
523
524     #[test]
525     fn test_hl_yield_nested_fn() {
526         check(
527             r#"
528 async fn foo() {
529     async fn foo2() {
530  // ^^^^^
531         async fn foo3() {
532             0.await
533         }
534         0.await$0
535        // ^^^^^
536     }
537     0.await
538 }
539 "#,
540         );
541     }
542
543     #[test]
544     fn test_hl_yield_nested_async_blocks() {
545         check(
546             r#"
547 async fn foo() {
548     (async {
549   // ^^^^^
550         (async {
551            0.await
552         }).await$0 }
553         // ^^^^^
554     ).await;
555 }
556 "#,
557         );
558     }
559
560     #[test]
561     fn test_hl_exit_points() {
562         check(
563             r#"
564 fn foo() -> u32 {
565     if true {
566         return$0 0;
567      // ^^^^^^
568     }
569
570     0?;
571   // ^
572     0xDEAD_BEEF
573  // ^^^^^^^^^^^
574 }
575 "#,
576         );
577     }
578
579     #[test]
580     fn test_hl_exit_points2() {
581         check(
582             r#"
583 fn foo() ->$0 u32 {
584     if true {
585         return 0;
586      // ^^^^^^
587     }
588
589     0?;
590   // ^
591     0xDEAD_BEEF
592  // ^^^^^^^^^^^
593 }
594 "#,
595         );
596     }
597
598     #[test]
599     fn test_hl_prefer_ref_over_tail_exit() {
600         check(
601             r#"
602 fn foo() -> u32 {
603 // ^^^
604     if true {
605         return 0;
606     }
607
608     0?;
609
610     foo$0()
611  // ^^^
612 }
613 "#,
614         );
615     }
616
617     #[test]
618     fn test_hl_never_call_is_exit_point() {
619         check(
620             r#"
621 struct Never;
622 impl Never {
623     fn never(self) -> ! { loop {} }
624 }
625 macro_rules! never {
626     () => { never() }
627 }
628 fn never() -> ! { loop {} }
629 fn foo() ->$0 u32 {
630     never();
631  // ^^^^^^^
632     never!();
633  // FIXME sema doesn't give us types for macrocalls
634
635     Never.never();
636  // ^^^^^^^^^^^^^
637
638     0
639  // ^
640 }
641 "#,
642         );
643     }
644
645     #[test]
646     fn test_hl_inner_tail_exit_points() {
647         check(
648             r#"
649 fn foo() ->$0 u32 {
650     if true {
651         unsafe {
652             return 5;
653          // ^^^^^^
654             5
655          // ^
656         }
657     } else {
658         match 5 {
659             6 => 100,
660               // ^^^
661             7 => loop {
662                 break 5;
663              // ^^^^^
664             }
665             8 => 'a: loop {
666                 'b: loop {
667                     break 'a 5;
668                  // ^^^^^
669                     break 'b 5;
670                     break 5;
671                 };
672             }
673             //
674             _ => 500,
675               // ^^^
676         }
677     }
678 }
679 "#,
680         );
681     }
682
683     #[test]
684     fn test_hl_inner_tail_exit_points_labeled_block() {
685         check(
686             r#"
687 fn foo() ->$0 u32 {
688     'foo: {
689         break 'foo 0;
690      // ^^^^^
691         loop {
692             break;
693             break 'foo 0;
694          // ^^^^^
695         }
696         0
697      // ^
698     }
699 }
700 "#,
701         );
702     }
703
704     #[test]
705     fn test_hl_break_loop() {
706         check(
707             r#"
708 fn foo() {
709     'outer: loop {
710  // ^^^^^^^^^^^^
711          break;
712       // ^^^^^
713          'inner: loop {
714             break;
715             'innermost: loop {
716                 break 'outer;
717              // ^^^^^^^^^^^^
718                 break 'inner;
719             }
720             break$0 'outer;
721          // ^^^^^^^^^^^^
722             break;
723         }
724         break;
725      // ^^^^^
726     }
727 }
728 "#,
729         );
730     }
731
732     #[test]
733     fn test_hl_break_loop2() {
734         check(
735             r#"
736 fn foo() {
737     'outer: loop {
738         break;
739         'inner: loop {
740      // ^^^^^^^^^^^^
741             break;
742          // ^^^^^
743             'innermost: loop {
744                 break 'outer;
745                 break 'inner;
746              // ^^^^^^^^^^^^
747             }
748             break 'outer;
749             break$0;
750          // ^^^^^
751         }
752         break;
753     }
754 }
755 "#,
756         );
757     }
758
759     #[test]
760     fn test_hl_break_for() {
761         check(
762             r#"
763 fn foo() {
764     'outer: for _ in () {
765  // ^^^^^^^^^^^
766          break;
767       // ^^^^^
768          'inner: for _ in () {
769             break;
770             'innermost: for _ in () {
771                 break 'outer;
772              // ^^^^^^^^^^^^
773                 break 'inner;
774             }
775             break$0 'outer;
776          // ^^^^^^^^^^^^
777             break;
778         }
779         break;
780      // ^^^^^
781     }
782 }
783 "#,
784         );
785     }
786
787     #[test]
788     fn test_hl_break_while() {
789         check(
790             r#"
791 fn foo() {
792     'outer: while true {
793  // ^^^^^^^^^^^^^
794          break;
795       // ^^^^^
796          'inner: while true {
797             break;
798             'innermost: while true {
799                 break 'outer;
800              // ^^^^^^^^^^^^
801                 break 'inner;
802             }
803             break$0 'outer;
804          // ^^^^^^^^^^^^
805             break;
806         }
807         break;
808      // ^^^^^
809     }
810 }
811 "#,
812         );
813     }
814
815     #[test]
816     fn test_hl_break_labeled_block() {
817         check(
818             r#"
819 fn foo() {
820     'outer: {
821  // ^^^^^^^
822          break;
823       // ^^^^^
824          'inner: {
825             break;
826             'innermost: {
827                 break 'outer;
828              // ^^^^^^^^^^^^
829                 break 'inner;
830             }
831             break$0 'outer;
832          // ^^^^^^^^^^^^
833             break;
834         }
835         break;
836      // ^^^^^
837     }
838 }
839 "#,
840         );
841     }
842
843     #[test]
844     fn test_hl_break_unlabeled_loop() {
845         check(
846             r#"
847 fn foo() {
848     loop {
849  // ^^^^
850         break$0;
851      // ^^^^^
852     }
853 }
854 "#,
855         );
856     }
857
858     #[test]
859     fn test_hl_break_unlabeled_block_in_loop() {
860         check(
861             r#"
862 fn foo() {
863     loop {
864  // ^^^^
865         {
866             break$0;
867          // ^^^^^
868         }
869     }
870 }
871 "#,
872         );
873     }
874 }