]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/highlight_related.rs
Highlight label value block tails
[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) if depth == 0 || eq_label(b.lifetime()) => {
375                         cb(b);
376                     }
377                     _ => (),
378                 },
379                 WalkEvent::Leave(expr) => match expr {
380                     ast::Expr::LoopExpr(_) | ast::Expr::WhileExpr(_) | ast::Expr::ForExpr(_) => {
381                         depth -= 1
382                     }
383                     ast::Expr::EffectExpr(e) if e.label().is_some() => depth -= 1,
384                     _ => (),
385                 },
386             }
387         }
388     }
389 }
390
391 fn cover_range(r0: Option<TextRange>, r1: Option<TextRange>) -> Option<TextRange> {
392     match (r0, r1) {
393         (Some(r0), Some(r1)) => Some(r0.cover(r1)),
394         (Some(range), None) => Some(range),
395         (None, Some(range)) => Some(range),
396         (None, None) => None,
397     }
398 }
399
400 #[cfg(test)]
401 mod tests {
402     use crate::fixture;
403
404     use super::*;
405
406     fn check(ra_fixture: &str) {
407         let (analysis, pos, annotations) = fixture::annotations(ra_fixture);
408         let hls = analysis.highlight_related(pos).unwrap().unwrap();
409
410         let mut expected = annotations
411             .into_iter()
412             .map(|(r, access)| (r.range, (!access.is_empty()).then(|| access)))
413             .collect::<Vec<_>>();
414
415         let mut actual = hls
416             .into_iter()
417             .map(|hl| {
418                 (
419                     hl.range,
420                     hl.access.map(|it| {
421                         match it {
422                             ReferenceAccess::Read => "read",
423                             ReferenceAccess::Write => "write",
424                         }
425                         .to_string()
426                     }),
427                 )
428             })
429             .collect::<Vec<_>>();
430         actual.sort_by_key(|(range, _)| range.start());
431         expected.sort_by_key(|(range, _)| range.start());
432
433         assert_eq!(expected, actual);
434     }
435
436     #[test]
437     fn test_hl_module() {
438         check(
439             r#"
440 //- /lib.rs
441 mod foo$0;
442  // ^^^
443 //- /foo.rs
444 struct Foo;
445 "#,
446         );
447     }
448
449     #[test]
450     fn test_hl_self_in_crate_root() {
451         check(
452             r#"
453 use self$0;
454 "#,
455         );
456     }
457
458     #[test]
459     fn test_hl_self_in_module() {
460         check(
461             r#"
462 //- /lib.rs
463 mod foo;
464 //- /foo.rs
465 use self$0;
466 "#,
467         );
468     }
469
470     #[test]
471     fn test_hl_local() {
472         check(
473             r#"
474 fn foo() {
475     let mut bar = 3;
476          // ^^^ write
477     bar$0;
478  // ^^^ read
479 }
480 "#,
481         );
482     }
483
484     #[test]
485     fn test_hl_yield_points() {
486         check(
487             r#"
488 pub async fn foo() {
489  // ^^^^^
490     let x = foo()
491         .await$0
492       // ^^^^^
493         .await;
494       // ^^^^^
495     || { 0.await };
496     (async { 0.await }).await
497                      // ^^^^^
498 }
499 "#,
500         );
501     }
502
503     #[test]
504     fn test_hl_yield_points2() {
505         check(
506             r#"
507 pub async$0 fn foo() {
508  // ^^^^^
509     let x = foo()
510         .await
511       // ^^^^^
512         .await;
513       // ^^^^^
514     || { 0.await };
515     (async { 0.await }).await
516                      // ^^^^^
517 }
518 "#,
519         );
520     }
521
522     #[test]
523     fn test_hl_yield_nested_fn() {
524         check(
525             r#"
526 async fn foo() {
527     async fn foo2() {
528  // ^^^^^
529         async fn foo3() {
530             0.await
531         }
532         0.await$0
533        // ^^^^^
534     }
535     0.await
536 }
537 "#,
538         );
539     }
540
541     #[test]
542     fn test_hl_yield_nested_async_blocks() {
543         check(
544             r#"
545 async fn foo() {
546     (async {
547   // ^^^^^
548         (async {
549            0.await
550         }).await$0 }
551         // ^^^^^
552     ).await;
553 }
554 "#,
555         );
556     }
557
558     #[test]
559     fn test_hl_exit_points() {
560         check(
561             r#"
562 fn foo() -> u32 {
563     if true {
564         return$0 0;
565      // ^^^^^^
566     }
567
568     0?;
569   // ^
570     0xDEAD_BEEF
571  // ^^^^^^^^^^^
572 }
573 "#,
574         );
575     }
576
577     #[test]
578     fn test_hl_exit_points2() {
579         check(
580             r#"
581 fn foo() ->$0 u32 {
582     if true {
583         return 0;
584      // ^^^^^^
585     }
586
587     0?;
588   // ^
589     0xDEAD_BEEF
590  // ^^^^^^^^^^^
591 }
592 "#,
593         );
594     }
595
596     #[test]
597     fn test_hl_prefer_ref_over_tail_exit() {
598         check(
599             r#"
600 fn foo() -> u32 {
601 // ^^^
602     if true {
603         return 0;
604     }
605
606     0?;
607
608     foo$0()
609  // ^^^
610 }
611 "#,
612         );
613     }
614
615     #[test]
616     fn test_hl_never_call_is_exit_point() {
617         check(
618             r#"
619 struct Never;
620 impl Never {
621     fn never(self) -> ! { loop {} }
622 }
623 macro_rules! never {
624     () => { never() }
625 }
626 fn never() -> ! { loop {} }
627 fn foo() ->$0 u32 {
628     never();
629  // ^^^^^^^
630     never!();
631  // FIXME sema doesn't give us types for macrocalls
632
633     Never.never();
634  // ^^^^^^^^^^^^^
635
636     0
637  // ^
638 }
639 "#,
640         );
641     }
642
643     #[test]
644     fn test_hl_inner_tail_exit_points() {
645         check(
646             r#"
647 fn foo() ->$0 u32 {
648     if true {
649         unsafe {
650             return 5;
651          // ^^^^^^
652             5
653          // ^
654         }
655     } else {
656         match 5 {
657             6 => 100,
658               // ^^^
659             7 => loop {
660                 break 5;
661              // ^^^^^
662             }
663             8 => 'a: loop {
664                 'b: loop {
665                     break 'a 5;
666                  // ^^^^^
667                     break 'b 5;
668                     break 5;
669                 };
670             }
671             //
672             _ => 500,
673               // ^^^
674         }
675     }
676 }
677 "#,
678         );
679     }
680
681     #[test]
682     fn test_hl_inner_tail_exit_points_labeled_block() {
683         check(
684             r#"
685 fn foo() ->$0 u32 {
686     'foo: {
687         break 'foo 0;
688      // ^^^^^
689         loop {
690             break;
691             break 'foo 0;
692          // ^^^^^
693         }
694         0
695      // ^
696     }
697 }
698 "#,
699         );
700     }
701
702     #[test]
703     fn test_hl_break_loop() {
704         check(
705             r#"
706 fn foo() {
707     'outer: loop {
708  // ^^^^^^^^^^^^
709          break;
710       // ^^^^^
711          'inner: loop {
712             break;
713             'innermost: loop {
714                 break 'outer;
715              // ^^^^^^^^^^^^
716                 break 'inner;
717             }
718             break$0 'outer;
719          // ^^^^^^^^^^^^
720             break;
721         }
722         break;
723      // ^^^^^
724     }
725 }
726 "#,
727         );
728     }
729
730     #[test]
731     fn test_hl_break_for() {
732         check(
733             r#"
734 fn foo() {
735     'outer: for _ in () {
736  // ^^^^^^^^^^^
737          break;
738       // ^^^^^
739          'inner: for _ in () {
740             break;
741             'innermost: for _ in () {
742                 break 'outer;
743              // ^^^^^^^^^^^^
744                 break 'inner;
745             }
746             break$0 'outer;
747          // ^^^^^^^^^^^^
748             break;
749         }
750         break;
751      // ^^^^^
752     }
753 }
754 "#,
755         );
756     }
757
758     #[test]
759     fn test_hl_break_while() {
760         check(
761             r#"
762 fn foo() {
763     'outer: while true {
764  // ^^^^^^^^^^^^^
765          break;
766       // ^^^^^
767          'inner: while true {
768             break;
769             'innermost: while true {
770                 break 'outer;
771              // ^^^^^^^^^^^^
772                 break 'inner;
773             }
774             break$0 'outer;
775          // ^^^^^^^^^^^^
776             break;
777         }
778         break;
779      // ^^^^^
780     }
781 }
782 "#,
783         );
784     }
785
786     #[test]
787     fn test_hl_break_labeled_block() {
788         check(
789             r#"
790 fn foo() {
791     'outer: {
792  // ^^^^^^^
793          break;
794       // ^^^^^
795          'inner: {
796             break;
797             'innermost: {
798                 break 'outer;
799              // ^^^^^^^^^^^^
800                 break 'inner;
801             }
802             break$0 'outer;
803          // ^^^^^^^^^^^^
804             break;
805         }
806         break;
807      // ^^^^^
808     }
809 }
810 "#,
811         );
812     }
813
814     #[test]
815     fn test_hl_break_unlabeled_loop() {
816         check(
817             r#"
818 fn foo() {
819     loop {
820  // ^^^^
821         break$0;
822      // ^^^^^
823     }
824 }
825 "#,
826         );
827     }
828
829     #[test]
830     fn test_hl_break_unlabeled_block_in_loop() {
831         check(
832             r#"
833 fn foo() {
834     loop {
835  // ^^^^
836         {
837             break$0;
838          // ^^^^^
839         }
840     }
841 }
842 "#,
843         );
844     }
845 }