mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-26 21:13:37 +00:00
Auto merge of #17258 - maxwase:code-assist-async-sugar, r=Veykril
Add `toggle_async_sugar` assist code action Implement code action for sugaring and de-sugaring asynchronous functions. This code action does not import `Future` trait when de-sugaring and does not touch function boby, I guess this can be implemented later if needed. This action also does not take into consideration other bounds because IMO it's usually "let me try to use sugared version here". Feel free to request changes, that's my first code action implementation 😄 Closes #17010 Relates to #16195
This commit is contained in:
commit
b32f181f47
4 changed files with 644 additions and 0 deletions
601
crates/ide-assists/src/handlers/toggle_async_sugar.rs
Normal file
601
crates/ide-assists/src/handlers/toggle_async_sugar.rs
Normal file
|
@ -0,0 +1,601 @@
|
|||
use hir::{ImportPathConfig, ModuleDef};
|
||||
use ide_db::{
|
||||
assists::{AssistId, AssistKind},
|
||||
famous_defs::FamousDefs,
|
||||
};
|
||||
use syntax::{
|
||||
ast::{self, HasVisibility},
|
||||
AstNode, NodeOrToken, SyntaxKind, SyntaxNode, SyntaxToken, TextRange,
|
||||
};
|
||||
|
||||
use crate::{AssistContext, Assists};
|
||||
|
||||
// Assist: sugar_impl_future_into_async
|
||||
//
|
||||
// Rewrites asynchronous function from `-> impl Future` into `async fn`.
|
||||
// This action does not touch the function body and therefore `async { 0 }`
|
||||
// block does not transform to just `0`.
|
||||
//
|
||||
// ```
|
||||
// # //- minicore: future
|
||||
// pub fn foo() -> impl core::future::F$0uture<Output = usize> {
|
||||
// async { 0 }
|
||||
// }
|
||||
// ```
|
||||
// ->
|
||||
// ```
|
||||
// pub async fn foo() -> usize {
|
||||
// async { 0 }
|
||||
// }
|
||||
// ```
|
||||
pub(crate) fn sugar_impl_future_into_async(
|
||||
acc: &mut Assists,
|
||||
ctx: &AssistContext<'_>,
|
||||
) -> Option<()> {
|
||||
let ret_type: ast::RetType = ctx.find_node_at_offset()?;
|
||||
let function = ret_type.syntax().parent().and_then(ast::Fn::cast)?;
|
||||
|
||||
if function.async_token().is_some() || function.const_token().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ast::Type::ImplTraitType(return_impl_trait) = ret_type.ty()? else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let main_trait_path = return_impl_trait
|
||||
.type_bound_list()?
|
||||
.bounds()
|
||||
.filter_map(|bound| match bound.ty() {
|
||||
Some(ast::Type::PathType(trait_path)) => trait_path.path(),
|
||||
_ => None,
|
||||
})
|
||||
.next()?;
|
||||
|
||||
let trait_type = ctx.sema.resolve_trait(&main_trait_path)?;
|
||||
let scope = ctx.sema.scope(main_trait_path.syntax())?;
|
||||
if trait_type != FamousDefs(&ctx.sema, scope.krate()).core_future_Future()? {
|
||||
return None;
|
||||
}
|
||||
let future_output = unwrap_future_output(main_trait_path)?;
|
||||
|
||||
acc.add(
|
||||
AssistId("sugar_impl_future_into_async", AssistKind::RefactorRewrite),
|
||||
"Convert `impl Future` into async",
|
||||
function.syntax().text_range(),
|
||||
|builder| {
|
||||
match future_output {
|
||||
// Empty tuple
|
||||
ast::Type::TupleType(t) if t.fields().next().is_none() => {
|
||||
let mut ret_type_range = ret_type.syntax().text_range();
|
||||
|
||||
// find leftover whitespace
|
||||
let whitespace_range = function
|
||||
.param_list()
|
||||
.as_ref()
|
||||
.map(|params| NodeOrToken::Node(params.syntax()))
|
||||
.and_then(following_whitespace);
|
||||
|
||||
if let Some(whitespace_range) = whitespace_range {
|
||||
ret_type_range =
|
||||
TextRange::new(whitespace_range.start(), ret_type_range.end());
|
||||
}
|
||||
|
||||
builder.delete(ret_type_range);
|
||||
}
|
||||
_ => {
|
||||
builder.replace(
|
||||
return_impl_trait.syntax().text_range(),
|
||||
future_output.syntax().text(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let (place_for_async, async_kw) = match function.visibility() {
|
||||
Some(vis) => (vis.syntax().text_range().end(), " async"),
|
||||
None => (function.syntax().text_range().start(), "async "),
|
||||
};
|
||||
builder.insert(place_for_async, async_kw);
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Assist: desugar_async_into_impl_future
|
||||
//
|
||||
// Rewrites asynchronous function from `async fn` into `-> impl Future`.
|
||||
// This action does not touch the function body and therefore `0`
|
||||
// block does not transform to `async { 0 }`.
|
||||
//
|
||||
// ```
|
||||
// # //- minicore: future
|
||||
// pub as$0ync fn foo() -> usize {
|
||||
// 0
|
||||
// }
|
||||
// ```
|
||||
// ->
|
||||
// ```
|
||||
// pub fn foo() -> impl core::future::Future<Output = usize> {
|
||||
// 0
|
||||
// }
|
||||
// ```
|
||||
pub(crate) fn desugar_async_into_impl_future(
|
||||
acc: &mut Assists,
|
||||
ctx: &AssistContext<'_>,
|
||||
) -> Option<()> {
|
||||
let async_token = ctx.find_token_syntax_at_offset(SyntaxKind::ASYNC_KW)?;
|
||||
let function = async_token.parent().and_then(ast::Fn::cast)?;
|
||||
|
||||
let rparen = function.param_list()?.r_paren_token()?;
|
||||
let return_type = match function.ret_type() {
|
||||
// unable to get a `ty` makes the action unapplicable
|
||||
Some(ret_type) => Some(ret_type.ty()?),
|
||||
// No type means `-> ()`
|
||||
None => None,
|
||||
};
|
||||
|
||||
let scope = ctx.sema.scope(function.syntax())?;
|
||||
let module = scope.module();
|
||||
let future_trait = FamousDefs(&ctx.sema, scope.krate()).core_future_Future()?;
|
||||
let trait_path = module.find_path(
|
||||
ctx.db(),
|
||||
ModuleDef::Trait(future_trait),
|
||||
ImportPathConfig {
|
||||
prefer_no_std: ctx.config.prefer_no_std,
|
||||
prefer_prelude: ctx.config.prefer_prelude,
|
||||
},
|
||||
)?;
|
||||
let trait_path = trait_path.display(ctx.db());
|
||||
|
||||
acc.add(
|
||||
AssistId("desugar_async_into_impl_future", AssistKind::RefactorRewrite),
|
||||
"Convert async into `impl Future`",
|
||||
function.syntax().text_range(),
|
||||
|builder| {
|
||||
let mut async_range = async_token.text_range();
|
||||
|
||||
if let Some(whitespace_range) = following_whitespace(NodeOrToken::Token(async_token)) {
|
||||
async_range = TextRange::new(async_range.start(), whitespace_range.end());
|
||||
}
|
||||
builder.delete(async_range);
|
||||
|
||||
match return_type {
|
||||
Some(ret_type) => builder.replace(
|
||||
ret_type.syntax().text_range(),
|
||||
format!("impl {trait_path}<Output = {ret_type}>"),
|
||||
),
|
||||
None => builder.insert(
|
||||
rparen.text_range().end(),
|
||||
format!(" -> impl {trait_path}<Output = ()>"),
|
||||
),
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn unwrap_future_output(path: ast::Path) -> Option<ast::Type> {
|
||||
let future_trait = path.segments().last()?;
|
||||
let assoc_list = future_trait.generic_arg_list()?;
|
||||
let future_assoc = assoc_list.generic_args().next()?;
|
||||
match future_assoc {
|
||||
ast::GenericArg::AssocTypeArg(output_type) => output_type.ty(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn following_whitespace(nt: NodeOrToken<&SyntaxNode, SyntaxToken>) -> Option<TextRange> {
|
||||
let next_token = match nt {
|
||||
NodeOrToken::Node(node) => node.next_sibling_or_token(),
|
||||
NodeOrToken::Token(token) => token.next_sibling_or_token(),
|
||||
}?;
|
||||
(next_token.kind() == SyntaxKind::WHITESPACE).then_some(next_token.text_range())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tests::{check_assist, check_assist_not_applicable};
|
||||
|
||||
#[test]
|
||||
fn sugar_with_use() {
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
use core::future::Future;
|
||||
fn foo() -> impl F$0uture<Output = ()> {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
use core::future::Future;
|
||||
async fn foo() {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
use core::future::Future;
|
||||
fn foo() -> impl F$0uture<Output = usize> {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
use core::future::Future;
|
||||
async fn foo() -> usize {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desugar_with_use() {
|
||||
check_assist(
|
||||
desugar_async_into_impl_future,
|
||||
r#"
|
||||
//- minicore: future
|
||||
use core::future::Future;
|
||||
as$0ync fn foo() {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
use core::future::Future;
|
||||
fn foo() -> impl Future<Output = ()> {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
desugar_async_into_impl_future,
|
||||
r#"
|
||||
//- minicore: future
|
||||
use core::future;
|
||||
as$0ync fn foo() {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
use core::future;
|
||||
fn foo() -> impl future::Future<Output = ()> {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
desugar_async_into_impl_future,
|
||||
r#"
|
||||
//- minicore: future
|
||||
use core::future::Future;
|
||||
as$0ync fn foo() -> usize {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
use core::future::Future;
|
||||
fn foo() -> impl Future<Output = usize> {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
desugar_async_into_impl_future,
|
||||
r#"
|
||||
//- minicore: future
|
||||
use core::future::Future;
|
||||
as$0ync fn foo() -> impl Future<Output = usize> {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
use core::future::Future;
|
||||
fn foo() -> impl Future<Output = impl Future<Output = usize>> {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sugar_without_use() {
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
fn foo() -> impl core::future::F$0uture<Output = ()> {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
async fn foo() {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
fn foo() -> impl core::future::F$0uture<Output = usize> {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
async fn foo() -> usize {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desugar_without_use() {
|
||||
check_assist(
|
||||
desugar_async_into_impl_future,
|
||||
r#"
|
||||
//- minicore: future
|
||||
as$0ync fn foo() {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
fn foo() -> impl core::future::Future<Output = ()> {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
desugar_async_into_impl_future,
|
||||
r#"
|
||||
//- minicore: future
|
||||
as$0ync fn foo() -> usize {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
fn foo() -> impl core::future::Future<Output = usize> {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_applicable() {
|
||||
check_assist_not_applicable(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
trait Future {
|
||||
type Output;
|
||||
}
|
||||
fn foo() -> impl F$0uture<Output = ()> {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist_not_applicable(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
trait Future {
|
||||
type Output;
|
||||
}
|
||||
fn foo() -> impl F$0uture<Output = usize> {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist_not_applicable(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
f$0n foo() -> impl core::future::Future<Output = usize> {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist_not_applicable(
|
||||
desugar_async_into_impl_future,
|
||||
r#"
|
||||
async f$0n foo() {
|
||||
todo!()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sugar_definition_with_use() {
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
use core::future::Future;
|
||||
fn foo() -> impl F$0uture<Output = ()>;
|
||||
"#,
|
||||
r#"
|
||||
use core::future::Future;
|
||||
async fn foo();
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
use core::future::Future;
|
||||
fn foo() -> impl F$0uture<Output = usize>;
|
||||
"#,
|
||||
r#"
|
||||
use core::future::Future;
|
||||
async fn foo() -> usize;
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sugar_definition_without_use() {
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
fn foo() -> impl core::future::F$0uture<Output = ()>;
|
||||
"#,
|
||||
r#"
|
||||
async fn foo();
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
fn foo() -> impl core::future::F$0uture<Output = usize>;
|
||||
"#,
|
||||
r#"
|
||||
async fn foo() -> usize;
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sugar_more_types() {
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
fn foo() -> impl core::future::F$0uture<Output = ()> + Send + Sync;
|
||||
"#,
|
||||
r#"
|
||||
async fn foo();
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
fn foo() -> impl core::future::F$0uture<Output = usize> + Debug;
|
||||
"#,
|
||||
r#"
|
||||
async fn foo() -> usize;
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
fn foo() -> impl core::future::F$0uture<Output = (usize)> + Debug;
|
||||
"#,
|
||||
r#"
|
||||
async fn foo() -> (usize);
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
fn foo() -> impl core::future::F$0uture<Output = (usize, usize)> + Debug;
|
||||
"#,
|
||||
r#"
|
||||
async fn foo() -> (usize, usize);
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
fn foo() -> impl core::future::Future<Output = impl core::future::F$0uture<Output = ()> + Send>;
|
||||
"#,
|
||||
r#"
|
||||
async fn foo() -> impl core::future::Future<Output = ()> + Send;
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sugar_with_modifiers() {
|
||||
check_assist_not_applicable(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
const fn foo() -> impl core::future::F$0uture<Output = ()>;
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
pub(crate) unsafe fn foo() -> impl core::future::F$0uture<Output = usize>;
|
||||
"#,
|
||||
r#"
|
||||
pub(crate) async unsafe fn foo() -> usize;
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
unsafe fn foo() -> impl core::future::F$0uture<Output = ()>;
|
||||
"#,
|
||||
r#"
|
||||
async unsafe fn foo();
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
unsafe extern "C" fn foo() -> impl core::future::F$0uture<Output = ()>;
|
||||
"#,
|
||||
r#"
|
||||
async unsafe extern "C" fn foo();
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
fn foo<T>() -> impl core::future::F$0uture<Output = T>;
|
||||
"#,
|
||||
r#"
|
||||
async fn foo<T>() -> T;
|
||||
"#,
|
||||
);
|
||||
|
||||
check_assist(
|
||||
sugar_impl_future_into_async,
|
||||
r#"
|
||||
//- minicore: future
|
||||
fn foo<T>() -> impl core::future::F$0uture<Output = T>
|
||||
where
|
||||
T: Sized;
|
||||
"#,
|
||||
r#"
|
||||
async fn foo<T>() -> T
|
||||
where
|
||||
T: Sized;
|
||||
"#,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -210,6 +210,7 @@ mod handlers {
|
|||
mod sort_items;
|
||||
mod split_import;
|
||||
mod term_search;
|
||||
mod toggle_async_sugar;
|
||||
mod toggle_ignore;
|
||||
mod unmerge_match_arm;
|
||||
mod unmerge_use;
|
||||
|
@ -239,6 +240,8 @@ mod handlers {
|
|||
change_visibility::change_visibility,
|
||||
convert_bool_then::convert_bool_then_to_if,
|
||||
convert_bool_then::convert_if_to_bool_then,
|
||||
toggle_async_sugar::desugar_async_into_impl_future,
|
||||
toggle_async_sugar::sugar_impl_future_into_async,
|
||||
convert_comment_block::convert_comment_block,
|
||||
convert_comment_from_or_to_doc::convert_comment_from_or_to_doc,
|
||||
convert_from_to_tryfrom::convert_from_to_tryfrom,
|
||||
|
|
|
@ -815,6 +815,24 @@ fn main() {
|
|||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doctest_desugar_async_into_impl_future() {
|
||||
check_doc_test(
|
||||
"desugar_async_into_impl_future",
|
||||
r#####"
|
||||
//- minicore: future
|
||||
pub as$0ync fn foo() -> usize {
|
||||
0
|
||||
}
|
||||
"#####,
|
||||
r#####"
|
||||
pub fn foo() -> impl core::future::Future<Output = usize> {
|
||||
0
|
||||
}
|
||||
"#####,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doctest_desugar_doc_comment() {
|
||||
check_doc_test(
|
||||
|
@ -3035,6 +3053,24 @@ use std::{collections::HashMap};
|
|||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doctest_sugar_impl_future_into_async() {
|
||||
check_doc_test(
|
||||
"sugar_impl_future_into_async",
|
||||
r#####"
|
||||
//- minicore: future
|
||||
pub fn foo() -> impl core::future::F$0uture<Output = usize> {
|
||||
async { 0 }
|
||||
}
|
||||
"#####,
|
||||
r#####"
|
||||
pub async fn foo() -> usize {
|
||||
async { 0 }
|
||||
}
|
||||
"#####,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doctest_toggle_ignore() {
|
||||
check_doc_test(
|
||||
|
|
|
@ -106,6 +106,10 @@ impl FamousDefs<'_, '_> {
|
|||
self.find_trait("core:marker:Copy")
|
||||
}
|
||||
|
||||
pub fn core_future_Future(&self) -> Option<Trait> {
|
||||
self.find_trait("core:future:Future")
|
||||
}
|
||||
|
||||
pub fn core_macros_builtin_derive(&self) -> Option<Macro> {
|
||||
self.find_macro("core:macros:builtin:derive")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue