3 use hir
::{DescendPreference, Semantics}
;
5 base_db
::{FileId, FilePosition, FileRange}
,
6 defs
::{Definition, IdentClass}
,
7 helpers
::pick_best_token
,
8 search
::{FileReference, ReferenceCategory, SearchScope}
,
9 syntax_helpers
::node_ext
::{
10 for_each_break_and_continue_expr
, for_each_tail_expr
, full_path_of_name_ref
, walk_expr
,
12 FxHashSet
, RootDatabase
,
15 ast
::{self, HasLoopBody}
,
17 SyntaxKind
::{self, IDENT, INT_NUMBER}
,
18 SyntaxNode
, SyntaxToken
, TextRange
, T
,
21 use crate::{navigation_target::ToNav, references, NavigationTarget, TryToNav}
;
23 #[derive(PartialEq, Eq, Hash)]
24 pub struct HighlightedRange
{
26 // FIXME: This needs to be more precise. Reference category makes sense only
27 // for references, but we also have defs. And things like exit points are
29 pub category
: Option
<ReferenceCategory
>,
32 #[derive(Default, Clone)]
33 pub struct HighlightRelatedConfig
{
35 pub exit_points
: bool
,
36 pub break_points
: bool
,
37 pub closure_captures
: bool
,
38 pub yield_points
: bool
,
41 // Feature: Highlight Related
43 // Highlights constructs related to the thing under the cursor:
45 // . if on an identifier, highlights all references to that identifier in the current file
46 // .. 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
47 // . if on an `async` or `await` token, highlights all yield points for that async context
48 // . if on a `return` or `fn` keyword, `?` character or `->` return type arrow, highlights all exit points for that context
49 // . if on a `break`, `loop`, `while` or `for` token, highlights all break points for that loop or block context
50 // . if on a `move` or `|` token that belongs to a closure, highlights all captures of the closure.
52 // Note: `?`, `|` and `->` do not currently trigger this behavior in the VSCode editor.
53 pub(crate) fn highlight_related(
54 sema
: &Semantics
<'_
, RootDatabase
>,
55 config
: HighlightRelatedConfig
,
56 pos @ FilePosition { offset, file_id }
: FilePosition
,
57 ) -> Option
<Vec
<HighlightedRange
>> {
58 let _p
= profile
::span("highlight_related");
59 let syntax
= sema
.parse(file_id
).syntax().clone();
61 let token
= pick_best_token(syntax
.token_at_offset(offset
), |kind
| match kind
{
62 T
![?
] => 4, // prefer `?` when the cursor is sandwiched like in `await$0?`
64 kind
if kind
.is_keyword() => 3,
65 IDENT
| INT_NUMBER
=> 2,
69 // most if not all of these should be re-implemented with information seeded from hir
71 T
![?
] if config
.exit_points
&& token
.parent().and_then(ast
::TryExpr
::cast
).is_some() => {
72 highlight_exit_points(sema
, token
)
74 T
![fn] | T
![return] | T
![->] if config
.exit_points
=> highlight_exit_points(sema
, token
),
75 T
![await
] | T
![async
] if config
.yield_points
=> highlight_yield_points(token
),
76 T
![for] if config
.break_points
&& token
.parent().and_then(ast
::ForExpr
::cast
).is_some() => {
77 highlight_break_points(token
)
79 T
![break] | T
![loop] | T
![while] | T
![continue] if config
.break_points
=> {
80 highlight_break_points(token
)
82 T
![|] if config
.closure_captures
=> highlight_closure_captures(sema
, token
, file_id
),
83 T
![move] if config
.closure_captures
=> highlight_closure_captures(sema
, token
, file_id
),
84 _
if config
.references
=> highlight_references(sema
, &syntax
, token
, pos
),
89 fn highlight_closure_captures(
90 sema
: &Semantics
<'_
, RootDatabase
>,
93 ) -> Option
<Vec
<HighlightedRange
>> {
94 let closure
= token
.parent_ancestors().take(2).find_map(ast
::ClosureExpr
::cast
)?
;
95 let search_range
= closure
.body()?
.syntax().text_range();
96 let ty
= &sema
.type_of_expr(&closure
.into())?
.original
;
97 let c
= ty
.as_closure()?
;
99 c
.captured_items(sema
.db
)
101 .map(|capture
| capture
.local())
103 let usages
= Definition
::Local(local
)
105 .in_scope(&SearchScope
::file_range(FileRange { file_id, range: search_range }
))
112 .map(|FileReference { category, range, .. }
| HighlightedRange
{
116 let category
= local
.is_mut(sema
.db
).then_some(ReferenceCategory
::Write
);
120 .flat_map(|x
| x
.to_nav(sema
.db
))
121 .filter(|decl
| decl
.file_id
== file_id
)
122 .filter_map(|decl
| decl
.focus_range
)
123 .map(move |range
| HighlightedRange { range, category }
)
130 fn highlight_references(
131 sema
: &Semantics
<'_
, RootDatabase
>,
134 FilePosition { file_id, offset }
: FilePosition
,
135 ) -> Option
<Vec
<HighlightedRange
>> {
136 let defs
= if let Some((range
, resolution
)) =
137 sema
.check_for_format_args_template(token
.clone(), offset
)
139 match resolution
.map(Definition
::from
) {
140 Some(def
) => iter
::once(def
).collect(),
141 None
=> return Some(vec
![HighlightedRange { range, category: None }
]),
144 find_defs(sema
, token
.clone())
150 .in_scope(&SearchScope
::single_file(file_id
))
157 .map(|FileReference { category, range, .. }
| HighlightedRange { range, category }
);
158 let mut res
= FxHashSet
::default();
160 // highlight trait usages
161 if let Definition
::Trait(t
) = def
{
162 let trait_item_use_scope
= (|| {
163 let name_ref
= token
.parent().and_then(ast
::NameRef
::cast
)?
;
164 let path
= full_path_of_name_ref(&name_ref
)?
;
165 let parent
= path
.syntax().parent()?
;
168 ast
::UseTree(it
) => it
.syntax().ancestors().find(|it
| {
169 ast
::SourceFile
::can_cast(it
.kind()) || ast
::Module
::can_cast(it
.kind())
171 ast
::PathType(it
) => it
175 .and_then(ast
::TypeBoundList
::cast
)?
178 .filter(|it
| ast
::WhereClause
::can_cast(it
.kind()) || ast
::TypeParam
::can_cast(it
.kind()))?
181 ast
::Item
::can_cast(it
.kind())
187 if let Some(trait_item_use_scope
) = trait_item_use_scope
{
189 t
.items_with_supertraits(sema
.db
)
192 Definition
::from(item
)
194 .set_scope(Some(&SearchScope
::file_range(FileRange
{
196 range
: trait_item_use_scope
.text_range(),
204 .map(|FileReference { category, range, .. }
| HighlightedRange
{
212 // highlight the defs themselves
214 Definition
::Local(local
) => {
215 let category
= local
.is_mut(sema
.db
).then_some(ReferenceCategory
::Write
);
219 .flat_map(|x
| x
.to_nav(sema
.db
))
220 .filter(|decl
| decl
.file_id
== file_id
)
221 .filter_map(|decl
| decl
.focus_range
)
222 .map(|range
| HighlightedRange { range, category }
)
228 let navs
= match def
{
229 Definition
::Module(module
) => {
230 NavigationTarget
::from_module_to_decl(sema
.db
, module
)
232 def
=> match def
.try_to_nav(sema
.db
) {
238 if nav
.file_id
!= file_id
{
241 let hl_range
= nav
.focus_range
.map(|range
| {
242 let category
= references
::decl_mutability(&def
, node
, range
)
243 .then_some(ReferenceCategory
::Write
);
244 HighlightedRange { range, category }
246 if let Some(hl_range
) = hl_range
{
247 res
.insert(hl_range
);
258 Some(res
.into_iter().collect())
262 fn highlight_exit_points(
263 sema
: &Semantics
<'_
, RootDatabase
>,
265 ) -> Option
<Vec
<HighlightedRange
>> {
267 sema
: &Semantics
<'_
, RootDatabase
>,
268 def_ranges
: [Option
<TextRange
>; 2],
269 body
: Option
<ast
::Expr
>,
270 ) -> Option
<Vec
<HighlightedRange
>> {
271 let mut highlights
= Vec
::new();
276 .map(|range
| HighlightedRange { category: None, range }
),
279 walk_expr(&body
, &mut |expr
| match expr
{
280 ast
::Expr
::ReturnExpr(expr
) => {
281 if let Some(token
) = expr
.return_token() {
282 highlights
.push(HighlightedRange { category: None, range: token.text_range() }
);
285 ast
::Expr
::TryExpr(try_
) => {
286 if let Some(token
) = try_
.question_mark_token() {
287 highlights
.push(HighlightedRange { category: None, range: token.text_range() }
);
290 ast
::Expr
::MethodCallExpr(_
) | ast
::Expr
::CallExpr(_
) | ast
::Expr
::MacroExpr(_
) => {
291 if sema
.type_of_expr(&expr
).map_or(false, |ty
| ty
.original
.is_never()) {
292 highlights
.push(HighlightedRange
{
294 range
: expr
.syntax().text_range(),
300 let tail
= match body
{
301 ast
::Expr
::BlockExpr(b
) => b
.tail_expr(),
305 if let Some(tail
) = tail
{
306 for_each_tail_expr(&tail
, &mut |tail
| {
307 let range
= match tail
{
308 ast
::Expr
::BreakExpr(b
) => b
310 .map_or_else(|| tail
.syntax().text_range(), |tok
| tok
.text_range()),
311 _
=> tail
.syntax().text_range(),
313 highlights
.push(HighlightedRange { category: None, range }
)
318 for anc
in token
.parent_ancestors() {
321 ast
::Fn(fn_
) => hl(sema
, [fn_
.fn_token().map(|it
| it
.text_range()), None
], fn_
.body().map(ast
::Expr
::BlockExpr
)),
322 ast
::ClosureExpr(closure
) => hl(
324 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())]),
327 ast
::BlockExpr(block_expr
) => if matches
!(block_expr
.modifier(), Some(ast
::BlockModifier
::Async(_
) | ast
::BlockModifier
::Try(_
)| ast
::BlockModifier
::Const(_
))) {
330 [block_expr
.modifier().and_then(|modifier
| match modifier
{
331 ast
::BlockModifier
::Async(t
) | ast
::BlockModifier
::Try(t
) | ast
::BlockModifier
::Const(t
) => Some(t
.text_range()),
334 Some(block_expr
.into())
346 fn highlight_break_points(token
: SyntaxToken
) -> Option
<Vec
<HighlightedRange
>> {
348 cursor_token_kind
: SyntaxKind
,
349 token
: Option
<SyntaxToken
>,
350 label
: Option
<ast
::Label
>,
351 body
: Option
<ast
::StmtList
>,
352 ) -> Option
<Vec
<HighlightedRange
>> {
353 let mut highlights
= Vec
::new();
354 let range
= cover_range(
355 token
.map(|tok
| tok
.text_range()),
356 label
.as_ref().map(|it
| it
.syntax().text_range()),
358 highlights
.extend(range
.map(|range
| HighlightedRange { category: None, range }
));
359 for_each_break_and_continue_expr(label
, body
, &mut |expr
| {
360 let range
: Option
<TextRange
> = match (cursor_token_kind
, expr
) {
361 (T
![for] | T
![while] | T
![loop] | T
![break], ast
::Expr
::BreakExpr(break_
)) => {
363 break_
.break_token().map(|it
| it
.text_range()),
364 break_
.lifetime().map(|it
| it
.syntax().text_range()),
368 T
![for] | T
![while] | T
![loop] | T
![continue],
369 ast
::Expr
::ContinueExpr(continue_
),
371 continue_
.continue_token().map(|it
| it
.text_range()),
372 continue_
.lifetime().map(|it
| it
.syntax().text_range()),
376 highlights
.extend(range
.map(|range
| HighlightedRange { category: None, range }
));
380 let parent
= token
.parent()?
;
381 let lbl
= match_ast
! {
383 ast
::BreakExpr(b
) => b
.lifetime(),
384 ast
::ContinueExpr(c
) => c
.lifetime(),
385 ast
::LoopExpr(l
) => l
.label().and_then(|it
| it
.lifetime()),
386 ast
::ForExpr(f
) => f
.label().and_then(|it
| it
.lifetime()),
387 ast
::WhileExpr(w
) => w
.label().and_then(|it
| it
.lifetime()),
388 ast
::BlockExpr(b
) => Some(b
.label().and_then(|it
| it
.lifetime())?
),
392 let lbl
= lbl
.as_ref();
393 let label_matches
= |def_lbl
: Option
<ast
::Label
>| match lbl
{
395 Some(lbl
.text()) == def_lbl
.and_then(|it
| it
.lifetime()).as_ref().map(|it
| it
.text())
399 let token_kind
= token
.kind();
400 for anc
in token
.parent_ancestors().flat_map(ast
::Expr
::cast
) {
402 ast
::Expr
::LoopExpr(l
) if label_matches(l
.label()) => hl(
406 l
.loop_body().and_then(|it
| it
.stmt_list()),
408 ast
::Expr
::ForExpr(f
) if label_matches(f
.label()) => hl(
412 f
.loop_body().and_then(|it
| it
.stmt_list()),
414 ast
::Expr
::WhileExpr(w
) if label_matches(w
.label()) => hl(
418 w
.loop_body().and_then(|it
| it
.stmt_list()),
420 ast
::Expr
::BlockExpr(e
) if e
.label().is_some() && label_matches(e
.label()) => {
421 hl(token_kind
, None
, e
.label(), e
.stmt_list())
429 fn highlight_yield_points(token
: SyntaxToken
) -> Option
<Vec
<HighlightedRange
>> {
431 async_token
: Option
<SyntaxToken
>,
432 body
: Option
<ast
::Expr
>,
433 ) -> Option
<Vec
<HighlightedRange
>> {
435 vec
![HighlightedRange { category: None, range: async_token?.text_range() }
];
436 if let Some(body
) = body
{
437 walk_expr(&body
, &mut |expr
| {
438 if let ast
::Expr
::AwaitExpr(expr
) = expr
{
439 if let Some(token
) = expr
.await_token() {
441 .push(HighlightedRange { category: None, range: token.text_range() }
);
448 for anc
in token
.parent_ancestors() {
451 ast
::Fn(fn_
) => hl(fn_
.async_token(), fn_
.body().map(ast
::Expr
::BlockExpr
)),
452 ast
::BlockExpr(block_expr
) => {
453 if block_expr
.async_token().is_none() {
456 hl(block_expr
.async_token(), Some(block_expr
.into()))
458 ast
::ClosureExpr(closure
) => hl(closure
.async_token(), closure
.body()),
466 fn cover_range(r0
: Option
<TextRange
>, r1
: Option
<TextRange
>) -> Option
<TextRange
> {
468 (Some(r0
), Some(r1
)) => Some(r0
.cover(r1
)),
469 (Some(range
), None
) => Some(range
),
470 (None
, Some(range
)) => Some(range
),
471 (None
, None
) => None
,
475 fn find_defs(sema
: &Semantics
<'_
, RootDatabase
>, token
: SyntaxToken
) -> FxHashSet
<Definition
> {
476 sema
.descend_into_macros(DescendPreference
::None
, token
)
478 .filter_map(|token
| IdentClass
::classify_token(sema
, &token
))
479 .map(IdentClass
::definitions_no_ops
)
490 const ENABLED_CONFIG
: HighlightRelatedConfig
= HighlightRelatedConfig
{
494 closure_captures
: true,
499 fn check(ra_fixture
: &str) {
500 check_with_config(ra_fixture
, ENABLED_CONFIG
);
504 fn check_with_config(ra_fixture
: &str, config
: HighlightRelatedConfig
) {
505 let (analysis
, pos
, annotations
) = fixture
::annotations(ra_fixture
);
507 let hls
= analysis
.highlight_related(config
, pos
).unwrap().unwrap_or_default();
509 let mut expected
= annotations
511 .map(|(r
, access
)| (r
.range
, (!access
.is_empty()).then_some(access
)))
512 .collect
::<Vec
<_
>>();
519 hl
.category
.map(|it
| {
521 ReferenceCategory
::Read
=> "read",
522 ReferenceCategory
::Write
=> "write",
523 ReferenceCategory
::Import
=> "import",
529 .collect
::<Vec
<_
>>();
530 actual
.sort_by_key(|(range
, _
)| range
.start());
531 expected
.sort_by_key(|(range
, _
)| range
.start());
533 assert_eq
!(expected
, actual
);
537 fn test_hl_tuple_fields() {
540 struct Tuple(u32, u32);
553 fn test_hl_module() {
566 fn test_hl_self_in_crate_root() {
581 //- /main.rs crate:main deps:lib
584 //- /lib.rs crate:lib
590 fn test_hl_self_in_module() {
617 fn test_hl_local_in_attr() {
620 //- proc_macros: identity
621 #[proc_macros::identity]
633 fn test_multi_macro_usage() {
638 fn $ident() -> $ident { loop {} }
646 let bar: bar = bar();
656 fn $ident() -> $ident { loop {} }
664 let bar: bar$0 = bar();
672 fn test_hl_yield_points() {
683 (async { 0.await }).await
691 fn test_hl_yield_points2() {
694 pub async$0 fn foo() {
702 (async { 0.await }).await
710 fn test_hl_let_else_yield_points() {
721 let Some(_) = None else {
725 (async { 0.await }).await
733 fn test_hl_yield_nested_fn() {
752 fn test_hl_yield_nested_async_blocks() {
769 fn test_hl_exit_points() {
789 fn test_hl_exit_points2() {
809 fn test_hl_exit_points3() {
829 fn test_hl_let_else_exit_points() {
834 let Some(bar) = None else {
849 fn test_hl_prefer_ref_over_tail_exit() {
868 fn test_hl_never_call_is_exit_point() {
873 fn never(self) -> ! { loop {} }
878 fn never() -> ! { loop {} }
897 fn test_hl_inner_tail_exit_points() {
939 fn test_hl_inner_tail_exit_points_labeled_block() {
961 fn test_hl_inner_tail_exit_points_loops() {
966 'foo: while { return 0; true } {
979 fn test_hl_break_loop() {
1007 fn test_hl_break_loop2() {
1034 fn test_hl_break_for() {
1038 'outer: for _ in () {
1042 'inner: for _ in () {
1044 'innermost: for _ in () {
1062 fn test_hl_break_for_but_not_continue() {
1066 'outer: for _ in () {
1071 'inner: for _ in () {
1074 'innermost: for _ in () {
1097 fn test_hl_continue_for_but_not_break() {
1101 'outer: for _ in () {
1106 'inner: for _ in () {
1109 'innermost: for _ in () {
1132 fn test_hl_break_and_continue() {
1136 'outer: fo$0r _ in () {
1142 'inner: for _ in () {
1145 'innermost: for _ in () {
1171 fn test_hl_break_while() {
1175 'outer: while true {
1179 'inner: while true {
1181 'innermost: while true {
1199 fn test_hl_break_labeled_block() {
1227 fn test_hl_break_unlabeled_loop() {
1242 fn test_hl_break_unlabeled_block_in_loop() {
1259 fn test_hl_field_shorthand() {
1262 struct Struct { field: u32 }
1264 fn function(field: u32) {
1274 fn test_hl_disabled_ref_local() {
1275 let config
= HighlightRelatedConfig { references: false, ..ENABLED_CONFIG }
;
1289 fn test_hl_disabled_ref_local_preserved_break() {
1290 let config
= HighlightRelatedConfig { references: false, ..ENABLED_CONFIG }
;
1324 fn test_hl_disabled_ref_local_preserved_yield() {
1325 let config
= HighlightRelatedConfig { references: false, ..ENABLED_CONFIG }
;
1355 fn test_hl_disabled_ref_local_preserved_exit() {
1356 let config
= HighlightRelatedConfig { references: false, ..ENABLED_CONFIG }
;
1394 fn test_hl_disabled_break() {
1395 let config
= HighlightRelatedConfig { break_points: false, ..ENABLED_CONFIG }
;
1410 fn test_hl_disabled_yield() {
1411 let config
= HighlightRelatedConfig { yield_points: false, ..ENABLED_CONFIG }
;
1424 fn test_hl_disabled_exit() {
1425 let config
= HighlightRelatedConfig { exit_points: false, ..ENABLED_CONFIG }
;
1441 fn test_hl_multi_local() {
1493 fn test_hl_trait_impl_methods() {
1555 fn test_assoc_type_highlighting() {
1571 fn test_closure_capture_pipe() {
1577 let c = $0|y| x + y;
1585 fn test_closure_capture_move() {
1591 let c = move$0 |y| x + y;
1599 fn test_trait_highlights_assoc_item_uses() {
1616 fn f<T: Foo$0>(t: T) {
1628 fn f2<T: Foo>(t: T) {
1640 fn implicit_format_args() {
1647 format_args!("hello {a} {a$0} {}", a);