feat: Highlight exit points of async blocks

Async blocks act similar to async functions in that the await keywords
are related, but also act like functions where the exit points are
related.
This commit is contained in:
Christopher Serr 2024-09-19 22:00:22 +02:00
parent 6dad8c5528
commit e1961b1078

View file

@ -281,99 +281,95 @@ fn highlight_references(
} }
} }
// If `file_id` is None, fn hl_exit_points(
pub(crate) fn highlight_exit_points(
sema: &Semantics<'_, RootDatabase>, sema: &Semantics<'_, RootDatabase>,
token: SyntaxToken, def_token: Option<SyntaxToken>,
) -> FxHashMap<EditionedFileId, Vec<HighlightedRange>> { body: ast::Expr,
fn hl( ) -> Option<FxHashMap<EditionedFileId, FxHashSet<HighlightedRange>>> {
sema: &Semantics<'_, RootDatabase>, let mut highlights: FxHashMap<EditionedFileId, FxHashSet<_>> = FxHashMap::default();
def_token: Option<SyntaxToken>,
body: ast::Expr,
) -> Option<FxHashMap<EditionedFileId, FxHashSet<HighlightedRange>>> {
let mut highlights: FxHashMap<EditionedFileId, FxHashSet<_>> = FxHashMap::default();
let mut push_to_highlights = |file_id, range| { let mut push_to_highlights = |file_id, range| {
if let Some(FileRange { file_id, range }) = original_frange(sema.db, file_id, range) { if let Some(FileRange { file_id, range }) = original_frange(sema.db, file_id, range) {
let hrange = HighlightedRange { category: ReferenceCategory::empty(), range }; let hrange = HighlightedRange { category: ReferenceCategory::empty(), range };
highlights.entry(file_id).or_default().insert(hrange); highlights.entry(file_id).or_default().insert(hrange);
}
};
if let Some(tok) = def_token {
let file_id = sema.hir_file_for(&tok.parent()?);
let range = Some(tok.text_range());
push_to_highlights(file_id, range);
}
WalkExpandedExprCtx::new(sema).walk(&body, &mut |_, expr| {
let file_id = sema.hir_file_for(expr.syntax());
let range = match &expr {
ast::Expr::TryExpr(try_) => try_.question_mark_token().map(|token| 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()) =>
{
Some(expr.syntax().text_range())
} }
_ => None,
}; };
if let Some(tok) = def_token { push_to_highlights(file_id, range);
let file_id = sema.hir_file_for(&tok.parent()?); });
let range = Some(tok.text_range());
push_to_highlights(file_id, range);
}
WalkExpandedExprCtx::new(sema).walk(&body, &mut |_, expr| { // We should handle `return` separately, because when it is used in a `try` block,
// it will exit the outside function instead of the block itself.
WalkExpandedExprCtx::new(sema)
.with_check_ctx(&WalkExpandedExprCtx::is_async_const_block_or_closure)
.walk(&body, &mut |_, expr| {
let file_id = sema.hir_file_for(expr.syntax()); let file_id = sema.hir_file_for(expr.syntax());
let range = match &expr { let range = match &expr {
ast::Expr::TryExpr(try_) => { ast::Expr::ReturnExpr(expr) => expr.return_token().map(|token| token.text_range()),
try_.question_mark_token().map(|token| 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()) =>
{
Some(expr.syntax().text_range())
}
_ => None, _ => None,
}; };
push_to_highlights(file_id, range); push_to_highlights(file_id, range);
}); });
// We should handle `return` separately, because when it is used in a `try` block, let tail = match body {
// it will exit the outside function instead of the block itself. ast::Expr::BlockExpr(b) => b.tail_expr(),
WalkExpandedExprCtx::new(sema) e => Some(e),
.with_check_ctx(&WalkExpandedExprCtx::is_async_const_block_or_closure) };
.walk(&body, &mut |_, expr| {
let file_id = sema.hir_file_for(expr.syntax());
let range = match &expr { if let Some(tail) = tail {
ast::Expr::ReturnExpr(expr) => { for_each_tail_expr(&tail, &mut |tail| {
expr.return_token().map(|token| token.text_range()) let file_id = sema.hir_file_for(tail.syntax());
} let range = match tail {
_ => None, ast::Expr::BreakExpr(b) => b
}; .break_token()
.map_or_else(|| tail.syntax().text_range(), |tok| tok.text_range()),
push_to_highlights(file_id, range); _ => tail.syntax().text_range(),
}); };
push_to_highlights(file_id, Some(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 file_id = sema.hir_file_for(tail.syntax());
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(),
};
push_to_highlights(file_id, Some(range));
});
}
Some(highlights)
} }
Some(highlights)
}
// If `file_id` is None,
pub(crate) fn highlight_exit_points(
sema: &Semantics<'_, RootDatabase>,
token: SyntaxToken,
) -> FxHashMap<EditionedFileId, Vec<HighlightedRange>> {
let mut res = FxHashMap::default(); let mut res = FxHashMap::default();
for def in goto_definition::find_fn_or_blocks(sema, &token) { for def in goto_definition::find_fn_or_blocks(sema, &token) {
let new_map = match_ast! { let new_map = match_ast! {
match def { match def {
ast::Fn(fn_) => fn_.body().and_then(|body| hl(sema, fn_.fn_token(), body.into())), ast::Fn(fn_) => fn_.body().and_then(|body| hl_exit_points(sema, fn_.fn_token(), body.into())),
ast::ClosureExpr(closure) => { ast::ClosureExpr(closure) => {
let pipe_tok = closure.param_list().and_then(|p| p.pipe_token()); let pipe_tok = closure.param_list().and_then(|p| p.pipe_token());
closure.body().and_then(|body| hl(sema, pipe_tok, body)) closure.body().and_then(|body| hl_exit_points(sema, pipe_tok, body))
}, },
ast::BlockExpr(blk) => match blk.modifier() { ast::BlockExpr(blk) => match blk.modifier() {
Some(ast::BlockModifier::Async(t)) => hl(sema, Some(t), blk.into()), Some(ast::BlockModifier::Async(t)) => hl_exit_points(sema, Some(t), blk.into()),
Some(ast::BlockModifier::Try(t)) if token.kind() != T![return] => { Some(ast::BlockModifier::Try(t)) if token.kind() != T![return] => {
hl(sema, Some(t), blk.into()) hl_exit_points(sema, Some(t), blk.into())
}, },
_ => continue, _ => continue,
}, },
@ -520,6 +516,12 @@ pub(crate) fn highlight_yield_points(
if block_expr.async_token().is_none() { if block_expr.async_token().is_none() {
continue; continue;
} }
// Async blocks act similar to closures. So we want to
// highlight their exit points too.
let exit_points = hl_exit_points(sema, block_expr.async_token(), block_expr.clone().into());
merge_map(&mut res, exit_points);
hl(sema, block_expr.async_token(), Some(block_expr.into())) hl(sema, block_expr.async_token(), Some(block_expr.into()))
}, },
ast::ClosureExpr(closure) => hl(sema, closure.async_token(), closure.body()), ast::ClosureExpr(closure) => hl(sema, closure.async_token(), closure.body()),
@ -876,6 +878,27 @@ pub async$0 fn foo() {
); );
} }
#[test]
fn test_hl_exit_points_of_async_blocks() {
check(
r#"
pub fn foo() {
let x = async$0 {
// ^^^^^
0.await;
// ^^^^^
0?;
// ^
return 0;
// ^^^^^^
0
// ^
};
}
"#,
);
}
#[test] #[test]
fn test_hl_let_else_yield_points() { fn test_hl_let_else_yield_points() {
check( check(
@ -925,11 +948,10 @@ async fn foo() {
async fn foo() { async fn foo() {
(async { (async {
// ^^^^^ // ^^^^^
(async { (async { 0.await }).await$0
0.await // ^^^^^^^^^^^^^^^^^^^^^^^^^
}).await$0 } // ^^^^^
// ^^^^^ }).await;
).await;
} }
"#, "#,
); );