diff --git a/crates/ide-assists/src/handlers/toggle_async_sugar.rs b/crates/ide-assists/src/handlers/toggle_async_sugar.rs new file mode 100644 index 0000000000..30e09648ea --- /dev/null +++ b/crates/ide-assists/src/handlers/toggle_async_sugar.rs @@ -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 { +// 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 { +// 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}"), + ), + None => builder.insert( + rparen.text_range().end(), + format!(" -> impl {trait_path}"), + ), + } + }, + ) +} + +fn unwrap_future_output(path: ast::Path) -> Option { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + todo!() + } + "#, + ); + + check_assist( + desugar_async_into_impl_future, + r#" + //- minicore: future + use core::future::Future; + as$0ync fn foo() -> impl Future { + todo!() + } + "#, + r#" + use core::future::Future; + fn foo() -> impl Future> { + todo!() + } + "#, + ); + } + + #[test] + fn sugar_without_use() { + check_assist( + sugar_impl_future_into_async, + r#" + //- minicore: future + fn foo() -> impl core::future::F$0uture { + todo!() + } + "#, + r#" + async fn foo() { + todo!() + } + "#, + ); + + check_assist( + sugar_impl_future_into_async, + r#" + //- minicore: future + fn foo() -> impl core::future::F$0uture { + 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 { + todo!() + } + "#, + ); + + check_assist( + desugar_async_into_impl_future, + r#" + //- minicore: future + as$0ync fn foo() -> usize { + todo!() + } + "#, + r#" + fn foo() -> impl core::future::Future { + 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 { + todo!() + } + "#, + ); + + check_assist_not_applicable( + sugar_impl_future_into_async, + r#" + //- minicore: future + trait Future { + type Output; + } + fn foo() -> impl F$0uture { + todo!() + } + "#, + ); + + check_assist_not_applicable( + sugar_impl_future_into_async, + r#" + //- minicore: future + f$0n foo() -> impl core::future::Future { + 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; + "#, + 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; + "#, + 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; + "#, + r#" + async fn foo(); + "#, + ); + + check_assist( + sugar_impl_future_into_async, + r#" + //- minicore: future + fn foo() -> impl core::future::F$0uture; + "#, + 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 + Send + Sync; + "#, + r#" + async fn foo(); + "#, + ); + + check_assist( + sugar_impl_future_into_async, + r#" + //- minicore: future + fn foo() -> impl core::future::F$0uture + Debug; + "#, + r#" + async fn foo() -> usize; + "#, + ); + + check_assist( + sugar_impl_future_into_async, + r#" + //- minicore: future + fn foo() -> impl core::future::F$0uture + Debug; + "#, + r#" + async fn foo() -> (usize); + "#, + ); + + check_assist( + sugar_impl_future_into_async, + r#" + //- minicore: future + fn foo() -> impl core::future::F$0uture + Debug; + "#, + r#" + async fn foo() -> (usize, usize); + "#, + ); + + check_assist( + sugar_impl_future_into_async, + r#" + //- minicore: future + fn foo() -> impl core::future::Future + Send>; + "#, + r#" + async fn foo() -> impl core::future::Future + 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; + "#, + ); + + check_assist( + sugar_impl_future_into_async, + r#" + //- minicore: future + pub(crate) unsafe fn foo() -> impl core::future::F$0uture; + "#, + 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; + "#, + 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; + "#, + r#" + async unsafe extern "C" fn foo(); + "#, + ); + + check_assist( + sugar_impl_future_into_async, + r#" + //- minicore: future + fn foo() -> impl core::future::F$0uture; + "#, + r#" + async fn foo() -> T; + "#, + ); + + check_assist( + sugar_impl_future_into_async, + r#" + //- minicore: future + fn foo() -> impl core::future::F$0uture + where + T: Sized; + "#, + r#" + async fn foo() -> T + where + T: Sized; + "#, + ); + } +} diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs index cbaf03e4d1..abebdec1d1 100644 --- a/crates/ide-assists/src/lib.rs +++ b/crates/ide-assists/src/lib.rs @@ -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, diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index 5ecce3cbb6..eec1087e2d 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -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 { + 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 { + async { 0 } +} +"#####, + r#####" +pub async fn foo() -> usize { + async { 0 } +} +"#####, + ) +} + #[test] fn doctest_toggle_ignore() { check_doc_test( diff --git a/crates/ide-db/src/famous_defs.rs b/crates/ide-db/src/famous_defs.rs index 3106772e63..e445e9fb68 100644 --- a/crates/ide-db/src/famous_defs.rs +++ b/crates/ide-db/src/famous_defs.rs @@ -106,6 +106,10 @@ impl FamousDefs<'_, '_> { self.find_trait("core:marker:Copy") } + pub fn core_future_Future(&self) -> Option { + self.find_trait("core:future:Future") + } + pub fn core_macros_builtin_derive(&self) -> Option { self.find_macro("core:macros:builtin:derive") }