diff --git a/Cargo.lock b/Cargo.lock index 8eb872514a..3558c39bb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -216,16 +216,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "command-group" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5080df6b0f0ecb76cab30808f00d937ba725cebe266a3da8cd89dff92f2a9916" -dependencies = [ - "nix 0.26.4", - "winapi", -] - [[package]] name = "countme" version = "3.0.1" @@ -292,7 +282,7 @@ version = "3.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" dependencies = [ - "nix 0.28.0", + "nix", "windows-sys 0.52.0", ] @@ -432,9 +422,9 @@ name = "flycheck" version = "0.0.0" dependencies = [ "cargo_metadata", - "command-group", "crossbeam-channel", "paths", + "process-wrap", "rustc-hash", "serde", "serde_json", @@ -1121,17 +1111,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", -] - [[package]] name = "nix" version = "0.28.0" @@ -1397,6 +1376,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "process-wrap" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ee68ae331824036479c84060534b18254c864fa73366c58d86db3b7b811619" +dependencies = [ + "indexmap", + "nix", + "tracing", + "windows", +] + [[package]] name = "profile" version = "0.0.0" @@ -2374,22 +2365,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" version = "0.1.8" @@ -2400,10 +2375,57 @@ dependencies = [ ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +dependencies = [ + "windows-core", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-result" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b" +dependencies = [ + "windows-targets 0.52.5", +] [[package]] name = "windows-sys" diff --git a/Cargo.toml b/Cargo.toml index 3108c1b3df..ccc27e2133 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,7 +111,6 @@ chalk-solve = { version = "0.97.0", default-features = false } chalk-ir = "0.97.0" chalk-recursive = { version = "0.97.0", default-features = false } chalk-derive = "0.97.0" -command-group = "2.0.1" crossbeam-channel = "0.5.8" dissimilar = "1.0.7" dot = "0.1.4" @@ -132,6 +131,7 @@ object = { version = "0.33.0", default-features = false, features = [ "macho", "pe", ] } +process-wrap = { version = "8.0.2", features = ["std"] } pulldown-cmark-to-cmark = "10.0.4" pulldown-cmark = { version = "0.9.0", default-features = false } rayon = "1.8.0" diff --git a/crates/flycheck/Cargo.toml b/crates/flycheck/Cargo.toml index b8c10da1b6..d81a5fe340 100644 --- a/crates/flycheck/Cargo.toml +++ b/crates/flycheck/Cargo.toml @@ -18,7 +18,7 @@ tracing.workspace = true rustc-hash.workspace = true serde_json.workspace = true serde.workspace = true -command-group.workspace = true +process-wrap.workspace = true # local deps paths.workspace = true diff --git a/crates/flycheck/src/command.rs b/crates/flycheck/src/command.rs index 8ba7018316..38c7c81f57 100644 --- a/crates/flycheck/src/command.rs +++ b/crates/flycheck/src/command.rs @@ -9,8 +9,8 @@ use std::{ process::{ChildStderr, ChildStdout, Command, Stdio}, }; -use command_group::{CommandGroup, GroupChild}; use crossbeam_channel::Sender; +use process_wrap::std::{StdChildWrapper, StdCommandWrap}; use stdx::process::streaming_output; /// Cargo output is structured as a one JSON per line. This trait abstracts parsing one line of @@ -85,7 +85,7 @@ impl CargoActor { } } -struct JodGroupChild(GroupChild); +struct JodGroupChild(Box); impl Drop for JodGroupChild { fn drop(&mut self) { @@ -119,14 +119,20 @@ impl fmt::Debug for CommandHandle { impl CommandHandle { pub(crate) fn spawn(mut command: Command, sender: Sender) -> std::io::Result { command.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null()); - let mut child = command.group_spawn().map(JodGroupChild)?; let program = command.get_program().into(); let arguments = command.get_args().map(|arg| arg.into()).collect::>(); let current_dir = command.get_current_dir().map(|arg| arg.to_path_buf()); - let stdout = child.0.inner().stdout.take().unwrap(); - let stderr = child.0.inner().stderr.take().unwrap(); + let mut child = StdCommandWrap::from(command); + #[cfg(unix)] + child.wrap(process_wrap::std::ProcessSession); + #[cfg(windows)] + child.wrap(process_wrap::std::JobObject); + let mut child = child.spawn().map(JodGroupChild)?; + + let stdout = child.0.stdout().take().unwrap(); + let stderr = child.0.stderr().take().unwrap(); let actor = CargoActor::::new(sender, stdout, stderr); let thread = stdx::thread::Builder::new(stdx::thread::ThreadIntent::Worker) diff --git a/crates/flycheck/src/lib.rs b/crates/flycheck/src/lib.rs index 6d5ca8321e..afdc3e389b 100644 --- a/crates/flycheck/src/lib.rs +++ b/crates/flycheck/src/lib.rs @@ -163,6 +163,9 @@ pub enum Message { /// Request adding a diagnostic with fixes included to a file AddDiagnostic { id: usize, workspace_root: AbsPathBuf, diagnostic: Diagnostic }, + /// Request clearing all previous diagnostics + ClearDiagnostics { id: usize }, + /// Request check progress notification to client Progress { /// Flycheck instance ID @@ -180,6 +183,9 @@ impl fmt::Debug for Message { .field("workspace_root", workspace_root) .field("diagnostic_code", &diagnostic.code.as_ref().map(|it| &it.code)) .finish(), + Message::ClearDiagnostics { id } => { + f.debug_struct("ClearDiagnostics").field("id", id).finish() + } Message::Progress { id, progress } => { f.debug_struct("Progress").field("id", id).field("progress", progress).finish() } @@ -220,6 +226,8 @@ struct FlycheckActor { command_handle: Option>, /// The receiver side of the channel mentioned above. command_receiver: Option>, + + status: FlycheckStatus, } enum Event { @@ -227,6 +235,13 @@ enum Event { CheckEvent(Option), } +#[derive(PartialEq)] +enum FlycheckStatus { + Started, + DiagnosticSent, + Finished, +} + const SAVED_FILE_PLACEHOLDER: &str = "$saved_file"; impl FlycheckActor { @@ -248,6 +263,7 @@ impl FlycheckActor { manifest_path, command_handle: None, command_receiver: None, + status: FlycheckStatus::Finished, } } @@ -298,12 +314,14 @@ impl FlycheckActor { self.command_handle = Some(command_handle); self.command_receiver = Some(receiver); self.report_progress(Progress::DidStart); + self.status = FlycheckStatus::Started; } Err(error) => { self.report_progress(Progress::DidFailToRestart(format!( "Failed to run the following command: {} error={}", formatted_command, error ))); + self.status = FlycheckStatus::Finished; } } } @@ -323,7 +341,11 @@ impl FlycheckActor { error ); } + if self.status == FlycheckStatus::Started { + self.send(Message::ClearDiagnostics { id: self.id }); + } self.report_progress(Progress::DidFinish(res)); + self.status = FlycheckStatus::Finished; } Event::CheckEvent(Some(message)) => match message { CargoCheckMessage::CompilerArtifact(msg) => { @@ -341,11 +363,15 @@ impl FlycheckActor { message = msg.message, "diagnostic received" ); + if self.status == FlycheckStatus::Started { + self.send(Message::ClearDiagnostics { id: self.id }); + } self.send(Message::AddDiagnostic { id: self.id, workspace_root: self.root.clone(), diagnostic: msg, }); + self.status = FlycheckStatus::DiagnosticSent; } }, } @@ -362,6 +388,7 @@ impl FlycheckActor { ); command_handle.cancel(); self.report_progress(Progress::DidCancel); + self.status = FlycheckStatus::Finished; } } diff --git a/crates/hir-expand/src/db.rs b/crates/hir-expand/src/db.rs index 2e5fa6131a..12421bbe70 100644 --- a/crates/hir-expand/src/db.rs +++ b/crates/hir-expand/src/db.rs @@ -298,7 +298,7 @@ pub fn expand_speculative( // prefer tokens of the same kind and text // Note the inversion of the score here, as we want to prefer the first token in case // of all tokens having the same score - (t.kind() != token_to_map.kind()) as u8 + (t.text() != token_to_map.text()) as u8 + (t.kind() != token_to_map.kind()) as u8 + 2 * ((t.text() != token_to_map.text()) as u8) })?; Some((node.syntax_node(), token)) } diff --git a/crates/hir-expand/src/files.rs b/crates/hir-expand/src/files.rs index 04a4851ddb..1ba85c5c7e 100644 --- a/crates/hir-expand/src/files.rs +++ b/crates/hir-expand/src/files.rs @@ -153,24 +153,20 @@ impl InFileWrapper { // region:specific impls impl InFile<&SyntaxNode> { - /// Skips the attributed item that caused the macro invocation we are climbing up - pub fn ancestors_with_macros_skip_attr_item( + /// Traverse up macro calls and skips the macro invocation node + pub fn ancestors_with_macros( self, db: &dyn db::ExpandDatabase, ) -> impl Iterator> + '_ { let succ = move |node: &InFile| match node.value.parent() { Some(parent) => Some(node.with_value(parent)), - None => { - let macro_file_id = node.file_id.macro_file()?; - let parent_node = macro_file_id.call_node(db); - if macro_file_id.is_attr_macro(db) { - // macro call was an attributed item, skip it - // FIXME: does this fail if this is a direct expansion of another macro? - parent_node.map(|node| node.parent()).transpose() - } else { - Some(parent_node) - } - } + None => db + .lookup_intern_macro_call(node.file_id.macro_file()?.macro_call_id) + .to_node_item(db) + .syntax() + .cloned() + .map(|node| node.parent()) + .transpose(), }; iter::successors(succ(&self.cloned()), succ) } diff --git a/crates/hir-expand/src/lib.rs b/crates/hir-expand/src/lib.rs index 4ab989bec2..83e92565f4 100644 --- a/crates/hir-expand/src/lib.rs +++ b/crates/hir-expand/src/lib.rs @@ -33,8 +33,8 @@ use std::{fmt, hash::Hash}; use base_db::{salsa::impl_intern_value_trivial, CrateId, FileId}; use either::Either; use span::{ - Edition, ErasedFileAstId, FileRange, HirFileIdRepr, Span, SpanAnchor, SyntaxContextData, - SyntaxContextId, + Edition, ErasedFileAstId, FileAstId, FileRange, HirFileIdRepr, Span, SpanAnchor, + SyntaxContextData, SyntaxContextId, }; use syntax::{ ast::{self, AstNode}, @@ -546,6 +546,18 @@ impl MacroCallLoc { } } + pub fn to_node_item(&self, db: &dyn ExpandDatabase) -> InFile { + match self.kind { + MacroCallKind::FnLike { ast_id, .. } => { + InFile::new(ast_id.file_id, ast_id.map(FileAstId::upcast).to_node(db)) + } + MacroCallKind::Derive { ast_id, .. } => { + InFile::new(ast_id.file_id, ast_id.map(FileAstId::upcast).to_node(db)) + } + MacroCallKind::Attr { ast_id, .. } => InFile::new(ast_id.file_id, ast_id.to_node(db)), + } + } + fn expand_to(&self) -> ExpandTo { match self.kind { MacroCallKind::FnLike { expand_to, .. } => expand_to, diff --git a/crates/hir/src/semantics.rs b/crates/hir/src/semantics.rs index 6c70cc4baf..43de2a6ee7 100644 --- a/crates/hir/src/semantics.rs +++ b/crates/hir/src/semantics.rs @@ -380,6 +380,27 @@ impl<'db> SemanticsImpl<'db> { self.with_ctx(|ctx| ctx.has_derives(adt)) } + pub fn derive_helper(&self, attr: &ast::Attr) -> Option> { + let adt = attr.syntax().ancestors().find_map(ast::Item::cast).and_then(|it| match it { + ast::Item::Struct(it) => Some(ast::Adt::Struct(it)), + ast::Item::Enum(it) => Some(ast::Adt::Enum(it)), + ast::Item::Union(it) => Some(ast::Adt::Union(it)), + _ => None, + })?; + let attr_name = attr.path().and_then(|it| it.as_single_name_ref())?.as_name(); + let sa = self.analyze_no_infer(adt.syntax())?; + let id = self.db.ast_id_map(sa.file_id).ast_id(&adt); + let res: Vec<_> = sa + .resolver + .def_map() + .derive_helpers_in_scope(InFile::new(sa.file_id, id))? + .iter() + .filter(|&(name, _, _)| *name == attr_name) + .map(|&(_, macro_, call)| (macro_.into(), call.as_macro_file())) + .collect(); + res.is_empty().not().then_some(res) + } + pub fn is_attr_macro_call(&self, item: &ast::Item) -> bool { let file_id = self.find_file(item.syntax()).file_id; let src = InFile::new(file_id, item.clone()); @@ -409,6 +430,20 @@ impl<'db> SemanticsImpl<'db> { ) } + pub fn speculative_expand_raw( + &self, + macro_file: MacroFileId, + speculative_args: &SyntaxNode, + token_to_map: SyntaxToken, + ) -> Option<(SyntaxNode, SyntaxToken)> { + hir_expand::db::expand_speculative( + self.db.upcast(), + macro_file.macro_call_id, + speculative_args, + token_to_map, + ) + } + /// Expand the macro call with a different item as the input, mapping the `token_to_map` down into the /// expansion. `token_to_map` should be a token from the `speculative args` node. pub fn speculative_expand_attr_macro( @@ -826,107 +861,109 @@ impl<'db> SemanticsImpl<'db> { // Then check for token trees, that means we are either in a function-like macro or // secondary attribute inputs - let tt = token.parent_ancestors().map_while(ast::TokenTree::cast).last()?; - let parent = tt.syntax().parent()?; - - if tt.left_delimiter_token().map_or(false, |it| it == token) { - return None; - } - if tt.right_delimiter_token().map_or(false, |it| it == token) { - return None; - } - - if let Some(macro_call) = ast::MacroCall::cast(parent.clone()) { - let mcall: hir_expand::files::InFileWrapper = - InFile::new(file_id, macro_call); - let file_id = match mcache.get(&mcall) { - Some(&it) => it, - None => { - let it = sa.expand(self.db, mcall.as_ref())?; - mcache.insert(mcall, it); - it + let tt = token + .parent_ancestors() + .map_while(Either::::cast) + .last()?; + match tt { + Either::Left(tt) => { + if tt.left_delimiter_token().map_or(false, |it| it == token) { + return None; } - }; - let text_range = tt.syntax().text_range(); - // remove any other token in this macro input, all their mappings are the - // same as this one - tokens.retain(|t| !text_range.contains_range(t.text_range())); - - process_expansion_for_token(&mut stack, file_id).or(file_id - .eager_arg(self.db.upcast()) - .and_then(|arg| { - // also descend into eager expansions - process_expansion_for_token(&mut stack, arg.as_macro_file()) - })) - } else if let Some(meta) = ast::Meta::cast(parent) { - // attribute we failed expansion for earlier, this might be a derive invocation - // or derive helper attribute - let attr = meta.parent_attr()?; - - let adt = if let Some(adt) = attr.syntax().parent().and_then(ast::Adt::cast) - { - // this might be a derive, or a derive helper on an ADT - let derive_call = self.with_ctx(|ctx| { - // so try downmapping the token into the pseudo derive expansion - // see [hir_expand::builtin_attr_macro] for how the pseudo derive expansion works - ctx.attr_to_derive_macro_call( - InFile::new(file_id, &adt), - InFile::new(file_id, attr.clone()), - ) - .map(|(_, call_id, _)| call_id) - }); - - match derive_call { - Some(call_id) => { - // resolved to a derive - let file_id = call_id.as_macro_file(); - let text_range = attr.syntax().text_range(); - // remove any other token in this macro input, all their mappings are the - // same as this one - tokens.retain(|t| !text_range.contains_range(t.text_range())); - return process_expansion_for_token(&mut stack, file_id); + if tt.right_delimiter_token().map_or(false, |it| it == token) { + return None; + } + let macro_call = tt.syntax().parent().and_then(ast::MacroCall::cast)?; + let mcall: hir_expand::files::InFileWrapper = + InFile::new(file_id, macro_call); + let file_id = match mcache.get(&mcall) { + Some(&it) => it, + None => { + let it = sa.expand(self.db, mcall.as_ref())?; + mcache.insert(mcall, it); + it } - None => Some(adt), - } - } else { - // Otherwise this could be a derive helper on a variant or field - if let Some(field) = - attr.syntax().parent().and_then(ast::RecordField::cast) - { - field.syntax().ancestors().take(4).find_map(ast::Adt::cast) - } else if let Some(field) = - attr.syntax().parent().and_then(ast::TupleField::cast) - { - field.syntax().ancestors().take(4).find_map(ast::Adt::cast) - } else if let Some(variant) = - attr.syntax().parent().and_then(ast::Variant::cast) - { - variant.syntax().ancestors().nth(2).and_then(ast::Adt::cast) - } else { - None - } - }?; - if !self.with_ctx(|ctx| ctx.has_derives(InFile::new(file_id, &adt))) { - return None; + }; + let text_range = tt.syntax().text_range(); + // remove any other token in this macro input, all their mappings are the + // same as this one + tokens.retain(|t| !text_range.contains_range(t.text_range())); + + process_expansion_for_token(&mut stack, file_id).or(file_id + .eager_arg(self.db.upcast()) + .and_then(|arg| { + // also descend into eager expansions + process_expansion_for_token(&mut stack, arg.as_macro_file()) + })) } - // Not an attribute, nor a derive, so it's either a builtin or a derive helper - // Try to resolve to a derive helper and downmap - let attr_name = - attr.path().and_then(|it| it.as_single_name_ref())?.as_name(); - let id = self.db.ast_id_map(file_id).ast_id(&adt); - let helpers = def_map.derive_helpers_in_scope(InFile::new(file_id, id))?; - let mut res = None; - for (.., derive) in - helpers.iter().filter(|(helper, ..)| *helper == attr_name) - { - res = res.or(process_expansion_for_token( - &mut stack, - derive.as_macro_file(), - )); + Either::Right(meta) => { + // attribute we failed expansion for earlier, this might be a derive invocation + // or derive helper attribute + let attr = meta.parent_attr()?; + let adt = match attr.syntax().parent().and_then(ast::Adt::cast) { + Some(adt) => { + // this might be a derive on an ADT + let derive_call = self.with_ctx(|ctx| { + // so try downmapping the token into the pseudo derive expansion + // see [hir_expand::builtin_attr_macro] for how the pseudo derive expansion works + ctx.attr_to_derive_macro_call( + InFile::new(file_id, &adt), + InFile::new(file_id, attr.clone()), + ) + .map(|(_, call_id, _)| call_id) + }); + + match derive_call { + Some(call_id) => { + // resolved to a derive + let file_id = call_id.as_macro_file(); + let text_range = attr.syntax().text_range(); + // remove any other token in this macro input, all their mappings are the + // same as this + tokens.retain(|t| { + !text_range.contains_range(t.text_range()) + }); + return process_expansion_for_token( + &mut stack, file_id, + ); + } + None => Some(adt), + } + } + None => { + // Otherwise this could be a derive helper on a variant or field + attr.syntax().ancestors().find_map(ast::Item::cast).and_then( + |it| match it { + ast::Item::Struct(it) => Some(ast::Adt::Struct(it)), + ast::Item::Enum(it) => Some(ast::Adt::Enum(it)), + ast::Item::Union(it) => Some(ast::Adt::Union(it)), + _ => None, + }, + ) + } + }?; + if !self.with_ctx(|ctx| ctx.has_derives(InFile::new(file_id, &adt))) { + return None; + } + let attr_name = + attr.path().and_then(|it| it.as_single_name_ref())?.as_name(); + // Not an attribute, nor a derive, so it's either a builtin or a derive helper + // Try to resolve to a derive helper and downmap + let id = self.db.ast_id_map(file_id).ast_id(&adt); + let helpers = + def_map.derive_helpers_in_scope(InFile::new(file_id, id))?; + + let mut res = None; + for (.., derive) in + helpers.iter().filter(|(helper, ..)| *helper == attr_name) + { + res = res.or(process_expansion_for_token( + &mut stack, + derive.as_macro_file(), + )); + } + res } - res - } else { - None } })() .is_none(); diff --git a/crates/hir/src/semantics/source_to_def.rs b/crates/hir/src/semantics/source_to_def.rs index d2bd8b0e79..77e7cdb58a 100644 --- a/crates/hir/src/semantics/source_to_def.rs +++ b/crates/hir/src/semantics/source_to_def.rs @@ -139,7 +139,7 @@ impl SourceToDefCtx<'_, '_> { let _p = tracing::span!(tracing::Level::INFO, "module_to_def").entered(); let parent_declaration = src .syntax() - .ancestors_with_macros_skip_attr_item(self.db.upcast()) + .ancestors_with_macros(self.db.upcast()) .find_map(|it| it.map(Either::::cast).transpose()) .map(|it| it.transpose()); @@ -366,7 +366,7 @@ impl SourceToDefCtx<'_, '_> { } pub(super) fn find_container(&mut self, src: InFile<&SyntaxNode>) -> Option { - for container in src.ancestors_with_macros_skip_attr_item(self.db.upcast()) { + for container in src.ancestors_with_macros(self.db.upcast()) { if let Some(res) = self.container_to_def(container) { return Some(res); } @@ -420,7 +420,7 @@ impl SourceToDefCtx<'_, '_> { } fn find_generic_param_container(&mut self, src: InFile<&SyntaxNode>) -> Option { - let ancestors = src.ancestors_with_macros_skip_attr_item(self.db.upcast()); + let ancestors = src.ancestors_with_macros(self.db.upcast()); for InFile { file_id, value } in ancestors { let item = match ast::Item::cast(value) { Some(it) => it, @@ -429,6 +429,7 @@ impl SourceToDefCtx<'_, '_> { let res: GenericDefId = match item { ast::Item::Fn(it) => self.fn_to_def(InFile::new(file_id, it))?.into(), ast::Item::Struct(it) => self.struct_to_def(InFile::new(file_id, it))?.into(), + ast::Item::Union(it) => self.union_to_def(InFile::new(file_id, it))?.into(), ast::Item::Enum(it) => self.enum_to_def(InFile::new(file_id, it))?.into(), ast::Item::Trait(it) => self.trait_to_def(InFile::new(file_id, it))?.into(), ast::Item::TraitAlias(it) => { @@ -446,11 +447,18 @@ impl SourceToDefCtx<'_, '_> { } fn find_pat_or_label_container(&mut self, src: InFile<&SyntaxNode>) -> Option { - let ancestors = src.ancestors_with_macros_skip_attr_item(self.db.upcast()); + let ancestors = src.ancestors_with_macros(self.db.upcast()); for InFile { file_id, value } in ancestors { - let item = match ast::Item::cast(value) { + let item = match ast::Item::cast(value.clone()) { Some(it) => it, - None => continue, + None => { + if let Some(variant) = ast::Variant::cast(value.clone()) { + return self + .enum_variant_to_def(InFile::new(file_id, variant)) + .map(Into::into); + } + continue; + } }; let res: DefWithBodyId = match item { ast::Item::Const(it) => self.const_to_def(InFile::new(file_id, it))?.into(), diff --git a/crates/hir/src/term_search.rs b/crates/hir/src/term_search.rs index 5c5ddae19e..7b70cdf459 100644 --- a/crates/hir/src/term_search.rs +++ b/crates/hir/src/term_search.rs @@ -329,7 +329,7 @@ pub fn term_search(ctx: &TermSearchCtx<'_, DB>) -> Vec { while should_continue() { lookup.new_round(); - solutions.extend(tactics::type_constructor(ctx, &defs, &mut lookup, should_continue)); + solutions.extend(tactics::data_constructor(ctx, &defs, &mut lookup, should_continue)); solutions.extend(tactics::free_function(ctx, &defs, &mut lookup, should_continue)); solutions.extend(tactics::impl_method(ctx, &defs, &mut lookup, should_continue)); solutions.extend(tactics::struct_projection(ctx, &defs, &mut lookup, should_continue)); diff --git a/crates/hir/src/term_search/tactics.rs b/crates/hir/src/term_search/tactics.rs index a26728272d..f95ff1dc0f 100644 --- a/crates/hir/src/term_search/tactics.rs +++ b/crates/hir/src/term_search/tactics.rs @@ -87,9 +87,9 @@ pub(super) fn trivial<'a, DB: HirDatabase>( }) } -/// # Type constructor tactic +/// # Data constructor tactic /// -/// Attempts different type constructors for enums and structs in scope +/// Attempts different data constructors for enums and structs in scope /// /// Updates lookup by new types reached and returns iterator that yields /// elements that unify with `goal`. @@ -99,7 +99,7 @@ pub(super) fn trivial<'a, DB: HirDatabase>( /// * `defs` - Set of items in scope at term search target location /// * `lookup` - Lookup table for types /// * `should_continue` - Function that indicates when to stop iterating -pub(super) fn type_constructor<'a, DB: HirDatabase>( +pub(super) fn data_constructor<'a, DB: HirDatabase>( ctx: &'a TermSearchCtx<'a, DB>, defs: &'a FxHashSet, lookup: &'a mut LookupTable, @@ -308,7 +308,9 @@ pub(super) fn type_constructor<'a, DB: HirDatabase>( // Early exit if some param cannot be filled from lookup let param_exprs: Vec> = fields .into_iter() - .map(|field| lookup.find(db, &field.ty(db))) + .map(|field| { + lookup.find(db, &field.ty_with_args(db, generics.iter().cloned())) + }) .collect::>()?; // Note that we need special case for 0 param constructors because of multi cartesian diff --git a/crates/ide-assists/src/handlers/convert_comment_from_or_to_doc.rs b/crates/ide-assists/src/handlers/convert_comment_from_or_to_doc.rs new file mode 100644 index 0000000000..953119fd1f --- /dev/null +++ b/crates/ide-assists/src/handlers/convert_comment_from_or_to_doc.rs @@ -0,0 +1,685 @@ +use itertools::Itertools; +use syntax::{ + ast::{self, edit::IndentLevel, Comment, CommentPlacement, Whitespace}, + AstToken, Direction, SyntaxElement, TextRange, +}; + +use crate::{AssistContext, AssistId, AssistKind, Assists}; + +// Assist: comment_to_doc +// +// Converts comments to documentation. +// +// ``` +// // Wow what $0a nice module +// // I sure hope this shows up when I hover over it +// ``` +// -> +// ``` +// //! Wow what a nice module +// //! I sure hope this shows up when I hover over it +// ``` +pub(crate) fn convert_comment_from_or_to_doc( + acc: &mut Assists, + ctx: &AssistContext<'_>, +) -> Option<()> { + let comment = ctx.find_token_at_offset::()?; + + match comment.kind().doc { + Some(_) => doc_to_comment(acc, comment), + None => can_be_doc_comment(&comment).and_then(|style| comment_to_doc(acc, comment, style)), + } +} + +fn doc_to_comment(acc: &mut Assists, comment: ast::Comment) -> Option<()> { + let target = if comment.kind().shape.is_line() { + line_comments_text_range(&comment)? + } else { + comment.syntax().text_range() + }; + + acc.add( + AssistId("doc_to_comment", AssistKind::RefactorRewrite), + "Replace comment with doc comment", + target, + |edit| { + // We need to either replace the first occurrence of /* with /***, or we need to replace + // the occurrences // at the start of each line with /// + let output = match comment.kind().shape { + ast::CommentShape::Line => { + let indentation = IndentLevel::from_token(comment.syntax()); + let line_start = comment.prefix(); + let prefix = format!("{indentation}//"); + relevant_line_comments(&comment) + .iter() + .map(|comment| comment.text()) + .flat_map(|text| text.lines()) + .map(|line| line.replacen(line_start, &prefix, 1)) + .join("\n") + } + ast::CommentShape::Block => { + let block_start = comment.prefix(); + comment + .text() + .lines() + .enumerate() + .map(|(idx, line)| { + if idx == 0 { + line.replacen(block_start, "/*", 1) + } else { + line.replacen("* ", "* ", 1) + } + }) + .join("\n") + } + }; + edit.replace(target, output) + }, + ) +} + +fn comment_to_doc(acc: &mut Assists, comment: ast::Comment, style: CommentPlacement) -> Option<()> { + let target = if comment.kind().shape.is_line() { + line_comments_text_range(&comment)? + } else { + comment.syntax().text_range() + }; + + acc.add( + AssistId("comment_to_doc", AssistKind::RefactorRewrite), + "Replace doc comment with comment", + target, + |edit| { + // We need to either replace the first occurrence of /* with /***, or we need to replace + // the occurrences // at the start of each line with /// + let output = match comment.kind().shape { + ast::CommentShape::Line => { + let indentation = IndentLevel::from_token(comment.syntax()); + let line_start = match style { + CommentPlacement::Inner => format!("{indentation}//!"), + CommentPlacement::Outer => format!("{indentation}///"), + }; + relevant_line_comments(&comment) + .iter() + .map(|comment| comment.text()) + .flat_map(|text| text.lines()) + .map(|line| line.replacen("//", &line_start, 1)) + .join("\n") + } + ast::CommentShape::Block => { + let block_start = match style { + CommentPlacement::Inner => "/*!", + CommentPlacement::Outer => "/**", + }; + comment + .text() + .lines() + .enumerate() + .map(|(idx, line)| { + if idx == 0 { + // On the first line we replace the comment start with a doc comment + // start. + line.replacen("/*", block_start, 1) + } else { + // put one extra space after each * since we moved the first line to + // the right by one column as well. + line.replacen("* ", "* ", 1) + } + }) + .join("\n") + } + }; + edit.replace(target, output) + }, + ) +} + +/// Not all comments are valid candidates for conversion into doc comments. For example, the +/// comments in the code: +/// ```rust +/// // Brilliant module right here +/// +/// // Really good right +/// fn good_function(foo: Foo) -> Bar { +/// foo.into_bar() +/// } +/// +/// // So nice +/// mod nice_module {} +/// ``` +/// can be converted to doc comments. However, the comments in this example: +/// ```rust +/// fn foo_bar(foo: Foo /* not bar yet */) -> Bar { +/// foo.into_bar() +/// // Nicely done +/// } +/// // end of function +/// +/// struct S { +/// // The S struct +/// } +/// ``` +/// are not allowed to become doc comments. Moreover, some comments _are_ allowed, but aren't common +/// style in Rust. For example, the following comments are allowed to be doc comments, but it is not +/// common style for them to be: +/// ```rust +/// fn foo_bar(foo: Foo) -> Bar { +/// // this could be an inner comment with //! +/// foo.into_bar() +/// } +/// +/// trait T { +/// // The T struct could also be documented from within +/// } +/// +/// mod mymod { +/// // Modules only normally get inner documentation when they are defined as a separate file. +/// } +/// ``` +fn can_be_doc_comment(comment: &ast::Comment) -> Option { + use syntax::SyntaxKind::*; + + // if the comment is not on its own line, then we do not propose anything. + match comment.syntax().prev_token() { + Some(prev) => { + // There was a previous token, now check if it was a newline + Whitespace::cast(prev).filter(|w| w.text().contains('\n'))?; + } + // There is no previous token, this is the start of the file. + None => return Some(CommentPlacement::Inner), + } + + // check if comment is followed by: `struct`, `trait`, `mod`, `fn`, `type`, `extern crate`, + // `use` or `const`. + let parent = comment.syntax().parent(); + let par_kind = parent.as_ref().map(|parent| parent.kind()); + matches!(par_kind, Some(STRUCT | TRAIT | MODULE | FN | TYPE_ALIAS | EXTERN_CRATE | USE | CONST)) + .then_some(CommentPlacement::Outer) +} + +/// The line -> block assist can be invoked from anywhere within a sequence of line comments. +/// relevant_line_comments crawls backwards and forwards finding the complete sequence of comments that will +/// be joined. +pub(crate) fn relevant_line_comments(comment: &ast::Comment) -> Vec { + // The prefix identifies the kind of comment we're dealing with + let prefix = comment.prefix(); + let same_prefix = |c: &ast::Comment| c.prefix() == prefix; + + // These tokens are allowed to exist between comments + let skippable = |not: &SyntaxElement| { + not.clone() + .into_token() + .and_then(Whitespace::cast) + .map(|w| !w.spans_multiple_lines()) + .unwrap_or(false) + }; + + // Find all preceding comments (in reverse order) that have the same prefix + let prev_comments = comment + .syntax() + .siblings_with_tokens(Direction::Prev) + .filter(|s| !skippable(s)) + .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix)) + .take_while(|opt_com| opt_com.is_some()) + .flatten() + .skip(1); // skip the first element so we don't duplicate it in next_comments + + let next_comments = comment + .syntax() + .siblings_with_tokens(Direction::Next) + .filter(|s| !skippable(s)) + .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix)) + .take_while(|opt_com| opt_com.is_some()) + .flatten(); + + let mut comments: Vec<_> = prev_comments.collect(); + comments.reverse(); + comments.extend(next_comments); + comments +} + +fn line_comments_text_range(comment: &ast::Comment) -> Option { + let comments = relevant_line_comments(comment); + let first = comments.first()?; + let indentation = IndentLevel::from_token(first.syntax()); + let start = + first.syntax().text_range().start().checked_sub((indentation.0 as u32 * 4).into())?; + let end = comments.last()?.syntax().text_range().end(); + Some(TextRange::new(start, end)) +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_assist, check_assist_not_applicable}; + + use super::*; + + #[test] + fn module_comment_to_doc() { + check_assist( + convert_comment_from_or_to_doc, + r#" + // such a nice module$0 + fn main() { + foo(); + } + "#, + r#" + //! such a nice module + fn main() { + foo(); + } + "#, + ); + } + + #[test] + fn single_line_comment_to_doc() { + check_assist( + convert_comment_from_or_to_doc, + r#" + + // unseen$0 docs + fn main() { + foo(); + } + "#, + r#" + + /// unseen docs + fn main() { + foo(); + } + "#, + ); + } + + #[test] + fn multi_line_comment_to_doc() { + check_assist( + convert_comment_from_or_to_doc, + r#" + + // unseen$0 docs + // make me seen! + fn main() { + foo(); + } + "#, + r#" + + /// unseen docs + /// make me seen! + fn main() { + foo(); + } + "#, + ); + } + + #[test] + fn single_line_doc_to_comment() { + check_assist( + convert_comment_from_or_to_doc, + r#" + + /// visible$0 docs + fn main() { + foo(); + } + "#, + r#" + + // visible docs + fn main() { + foo(); + } + "#, + ); + } + + #[test] + fn multi_line_doc_to_comment() { + check_assist( + convert_comment_from_or_to_doc, + r#" + + /// visible$0 docs + /// Hide me! + fn main() { + foo(); + } + "#, + r#" + + // visible docs + // Hide me! + fn main() { + foo(); + } + "#, + ); + } + + #[test] + fn single_line_block_comment_to_doc() { + check_assist( + convert_comment_from_or_to_doc, + r#" + + /* unseen$0 docs */ + fn main() { + foo(); + } + "#, + r#" + + /** unseen docs */ + fn main() { + foo(); + } + "#, + ); + } + + #[test] + fn multi_line_block_comment_to_doc() { + check_assist( + convert_comment_from_or_to_doc, + r#" + + /* unseen$0 docs + * make me seen! + */ + fn main() { + foo(); + } + "#, + r#" + + /** unseen docs + * make me seen! + */ + fn main() { + foo(); + } + "#, + ); + } + + #[test] + fn single_line_block_doc_to_comment() { + check_assist( + convert_comment_from_or_to_doc, + r#" + + /** visible$0 docs */ + fn main() { + foo(); + } + "#, + r#" + + /* visible docs */ + fn main() { + foo(); + } + "#, + ); + } + + #[test] + fn multi_line_block_doc_to_comment() { + check_assist( + convert_comment_from_or_to_doc, + r#" + + /** visible$0 docs + * Hide me! + */ + fn main() { + foo(); + } + "#, + r#" + + /* visible docs + * Hide me! + */ + fn main() { + foo(); + } + "#, + ); + } + + #[test] + fn single_inner_line_comment_to_doc() { + check_assist_not_applicable( + convert_comment_from_or_to_doc, + r#" + mod mymod { + // unseen$0 docs + foo(); + } + "#, + ); + } + + #[test] + fn single_inner_line_doc_to_comment() { + check_assist( + convert_comment_from_or_to_doc, + r#" + mod mymod { + //! visible$0 docs + foo(); + } + "#, + r#" + mod mymod { + // visible docs + foo(); + } + "#, + ); + } + + #[test] + fn multi_inner_line_doc_to_comment() { + check_assist( + convert_comment_from_or_to_doc, + r#" + mod mymod { + //! visible$0 docs + //! Hide me! + foo(); + } + "#, + r#" + mod mymod { + // visible docs + // Hide me! + foo(); + } + "#, + ); + check_assist( + convert_comment_from_or_to_doc, + r#" + mod mymod { + /// visible$0 docs + /// Hide me! + foo(); + } + "#, + r#" + mod mymod { + // visible docs + // Hide me! + foo(); + } + "#, + ); + } + + #[test] + fn single_inner_line_block_doc_to_comment() { + check_assist( + convert_comment_from_or_to_doc, + r#" + mod mymod { + /*! visible$0 docs */ + type Int = i32; + } + "#, + r#" + mod mymod { + /* visible docs */ + type Int = i32; + } + "#, + ); + } + + #[test] + fn multi_inner_line_block_doc_to_comment() { + check_assist( + convert_comment_from_or_to_doc, + r#" + mod mymod { + /*! visible$0 docs + * Hide me! + */ + type Int = i32; + } + "#, + r#" + mod mymod { + /* visible docs + * Hide me! + */ + type Int = i32; + } + "#, + ); + } + + #[test] + fn not_overeager() { + check_assist_not_applicable( + convert_comment_from_or_to_doc, + r#" + fn main() { + foo(); + // $0well that settles main + } + // $1 nicely done + "#, + ); + } + + #[test] + fn all_possible_items() { + check_assist( + convert_comment_from_or_to_doc, + r#"mod m { + /* Nice struct$0 */ + struct S {} + }"#, + r#"mod m { + /** Nice struct */ + struct S {} + }"#, + ); + check_assist( + convert_comment_from_or_to_doc, + r#"mod m { + /* Nice trait$0 */ + trait T {} + }"#, + r#"mod m { + /** Nice trait */ + trait T {} + }"#, + ); + check_assist( + convert_comment_from_or_to_doc, + r#"mod m { + /* Nice module$0 */ + mod module {} + }"#, + r#"mod m { + /** Nice module */ + mod module {} + }"#, + ); + check_assist( + convert_comment_from_or_to_doc, + r#"mod m { + /* Nice function$0 */ + fn function() {} + }"#, + r#"mod m { + /** Nice function */ + fn function() {} + }"#, + ); + check_assist( + convert_comment_from_or_to_doc, + r#"mod m { + /* Nice type$0 */ + type Type Int = i32; + }"#, + r#"mod m { + /** Nice type */ + type Type Int = i32; + }"#, + ); + check_assist( + convert_comment_from_or_to_doc, + r#"mod m { + /* Nice crate$0 */ + extern crate rust_analyzer; + }"#, + r#"mod m { + /** Nice crate */ + extern crate rust_analyzer; + }"#, + ); + check_assist( + convert_comment_from_or_to_doc, + r#"mod m { + /* Nice import$0 */ + use ide_assists::convert_comment_from_or_to_doc::tests + }"#, + r#"mod m { + /** Nice import */ + use ide_assists::convert_comment_from_or_to_doc::tests + }"#, + ); + check_assist( + convert_comment_from_or_to_doc, + r#"mod m { + /* Nice constant$0 */ + const CONST: &str = "very const"; + }"#, + r#"mod m { + /** Nice constant */ + const CONST: &str = "very const"; + }"#, + ); + } + + #[test] + fn no_inner_comments() { + check_assist_not_applicable( + convert_comment_from_or_to_doc, + r#" + mod mymod { + // aaa$0aa + } + "#, + ); + } +} diff --git a/crates/ide-assists/src/handlers/term_search.rs b/crates/ide-assists/src/handlers/term_search.rs index ffd1508ccb..94e0519cba 100644 --- a/crates/ide-assists/src/handlers/term_search.rs +++ b/crates/ide-assists/src/handlers/term_search.rs @@ -278,4 +278,16 @@ fn f() { let a = 1; let b = 0.0; let c: (i32, (i32, f64)) = todo$0!(); }"#, r#"fn f() { let a = 1; let b = 0.0; let c: (i32, (i32, f64)) = (a, (a, b)); }"#, ) } + + #[test] + fn test_tuple_struct_with_generics() { + check_assist( + term_search, + r#"//- minicore: todo, unimplemented +struct Foo(T); +fn f() { let a = 1; let b: Foo = todo$0!(); }"#, + r#"struct Foo(T); +fn f() { let a = 1; let b: Foo = Foo(a); }"#, + ) + } } 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 0df5e913a5..abebdec1d1 100644 --- a/crates/ide-assists/src/lib.rs +++ b/crates/ide-assists/src/lib.rs @@ -116,6 +116,7 @@ mod handlers { mod change_visibility; mod convert_bool_then; mod convert_comment_block; + mod convert_comment_from_or_to_doc; mod convert_from_to_tryfrom; mod convert_integer_literal; mod convert_into_to_from; @@ -209,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; @@ -238,7 +240,10 @@ 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, convert_integer_literal::convert_integer_literal, convert_into_to_from::convert_into_to_from, diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index 937e78f8d7..eec1087e2d 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -345,6 +345,21 @@ pub(crate) fn frobnicate() {} ) } +#[test] +fn doctest_comment_to_doc() { + check_doc_test( + "comment_to_doc", + r#####" +// Wow what $0a nice module +// I sure hope this shows up when I hover over it +"#####, + r#####" +//! Wow what a nice module +//! I sure hope this shows up when I hover over it +"#####, + ) +} + #[test] fn doctest_convert_bool_then_to_if() { check_doc_test( @@ -800,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( @@ -3020,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-completion/src/context/analysis.rs b/crates/ide-completion/src/context/analysis.rs index 79c503e0a1..f0c6e7a63b 100644 --- a/crates/ide-completion/src/context/analysis.rs +++ b/crates/ide-completion/src/context/analysis.rs @@ -3,8 +3,9 @@ use std::iter; use hir::{Semantics, Type, TypeInfo, Variant}; use ide_db::{active_parameter::ActiveParameter, RootDatabase}; +use itertools::Either; use syntax::{ - algo::{find_node_at_offset, non_trivia_sibling}, + algo::{ancestors_at_offset, find_node_at_offset, non_trivia_sibling}, ast::{self, AttrKind, HasArgList, HasGenericParams, HasLoopBody, HasName, NameOrNameRef}, match_ast, AstNode, AstToken, Direction, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, TextSize, T, @@ -119,20 +120,45 @@ fn expand( } // No attributes have been expanded, so look for macro_call! token trees or derive token trees - let orig_tt = match find_node_at_offset::(&original_file, offset) { + let orig_tt = match ancestors_at_offset(&original_file, offset) + .map_while(Either::::cast) + .last() + { Some(it) => it, None => break 'expansion, }; - let spec_tt = match find_node_at_offset::(&speculative_file, offset) { + let spec_tt = match ancestors_at_offset(&speculative_file, offset) + .map_while(Either::::cast) + .last() + { Some(it) => it, None => break 'expansion, }; - // Expand pseudo-derive expansion - if let (Some(orig_attr), Some(spec_attr)) = ( - orig_tt.syntax().parent().and_then(ast::Meta::cast).and_then(|it| it.parent_attr()), - spec_tt.syntax().parent().and_then(ast::Meta::cast).and_then(|it| it.parent_attr()), - ) { + let (tts, attrs) = match (orig_tt, spec_tt) { + (Either::Left(orig_tt), Either::Left(spec_tt)) => { + let attrs = orig_tt + .syntax() + .parent() + .and_then(ast::Meta::cast) + .and_then(|it| it.parent_attr()) + .zip( + spec_tt + .syntax() + .parent() + .and_then(ast::Meta::cast) + .and_then(|it| it.parent_attr()), + ); + (Some((orig_tt, spec_tt)), attrs) + } + (Either::Right(orig_path), Either::Right(spec_path)) => { + (None, orig_path.parent_attr().zip(spec_path.parent_attr())) + } + _ => break 'expansion, + }; + + // Expand pseudo-derive expansion aka `derive(Debug$0)` + if let Some((orig_attr, spec_attr)) = attrs { if let (Some(actual_expansion), Some((fake_expansion, fake_mapped_token))) = ( sema.expand_derive_as_pseudo_attr_macro(&orig_attr), sema.speculative_expand_derive_as_pseudo_attr_macro( @@ -147,15 +173,54 @@ fn expand( fake_mapped_token.text_range().start(), orig_attr, )); + break 'expansion; + } + + if let Some(spec_adt) = + spec_attr.syntax().ancestors().find_map(ast::Item::cast).and_then(|it| match it { + ast::Item::Struct(it) => Some(ast::Adt::Struct(it)), + ast::Item::Enum(it) => Some(ast::Adt::Enum(it)), + ast::Item::Union(it) => Some(ast::Adt::Union(it)), + _ => None, + }) + { + // might be the path of derive helper or a token tree inside of one + if let Some(helpers) = sema.derive_helper(&orig_attr) { + for (_mac, file) in helpers { + if let Some((fake_expansion, fake_mapped_token)) = sema + .speculative_expand_raw( + file, + spec_adt.syntax(), + fake_ident_token.clone(), + ) + { + // we are inside a derive helper token tree, treat this as being inside + // the derive expansion + let actual_expansion = sema.parse_or_expand(file.into()); + let new_offset = fake_mapped_token.text_range().start(); + if new_offset + relative_offset > actual_expansion.text_range().end() { + // offset outside of bounds from the original expansion, + // stop here to prevent problems from happening + break 'expansion; + } + original_file = actual_expansion; + speculative_file = fake_expansion; + fake_ident_token = fake_mapped_token; + offset = new_offset; + continue 'expansion; + } + } + } } // at this point we won't have any more successful expansions, so stop break 'expansion; } // Expand fn-like macro calls + let Some((orig_tt, spec_tt)) = tts else { break 'expansion }; if let (Some(actual_macro_call), Some(macro_call_with_fake_ident)) = ( - orig_tt.syntax().ancestors().find_map(ast::MacroCall::cast), - spec_tt.syntax().ancestors().find_map(ast::MacroCall::cast), + orig_tt.syntax().parent().and_then(ast::MacroCall::cast), + spec_tt.syntax().parent().and_then(ast::MacroCall::cast), ) { let mac_call_path0 = actual_macro_call.path().as_ref().map(|s| s.syntax().text()); let mac_call_path1 = @@ -201,6 +266,7 @@ fn expand( // none of our states have changed so stop the loop break 'expansion; } + ExpansionResult { original_file, speculative_file, offset, fake_ident_token, derive_ctx } } 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") } diff --git a/crates/ide/src/goto_declaration.rs b/crates/ide/src/goto_declaration.rs index fab62e95d1..c19f19803f 100644 --- a/crates/ide/src/goto_declaration.rs +++ b/crates/ide/src/goto_declaration.rs @@ -16,7 +16,7 @@ use crate::{ // // This is the same as `Go to Definition` with the following exceptions: // - outline modules will navigate to the `mod name;` item declaration -// - trait assoc items will navigate to the assoc item of the trait declaration opposed to the trait impl +// - trait assoc items will navigate to the assoc item of the trait declaration as opposed to the trait impl // - fields in patterns will navigate to the field declaration of the struct, union or variant pub(crate) fn goto_declaration( db: &RootDatabase, diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs index 79b87ecd58..f64e66183d 100644 --- a/crates/rust-analyzer/src/global_state.rs +++ b/crates/rust-analyzer/src/global_state.rs @@ -87,7 +87,6 @@ pub(crate) struct GlobalState { pub(crate) flycheck_sender: Sender, pub(crate) flycheck_receiver: Receiver, pub(crate) last_flycheck_error: Option, - pub(crate) diagnostics_received: bool, // Test explorer pub(crate) test_run_session: Option>, @@ -225,7 +224,6 @@ impl GlobalState { flycheck_sender, flycheck_receiver, last_flycheck_error: None, - diagnostics_received: false, test_run_session: None, test_run_sender, diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index 7acd302867..193b3fdd4a 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -804,10 +804,6 @@ impl GlobalState { fn handle_flycheck_msg(&mut self, message: flycheck::Message) { match message { flycheck::Message::AddDiagnostic { id, workspace_root, diagnostic } => { - if !self.diagnostics_received { - self.diagnostics.clear_check(id); - self.diagnostics_received = true; - } let snap = self.snapshot(); let diagnostics = crate::diagnostics::to_proto::map_rust_diagnostic_to_lsp( &self.config.diagnostics_map(), @@ -833,12 +829,11 @@ impl GlobalState { } } + flycheck::Message::ClearDiagnostics { id } => self.diagnostics.clear_check(id), + flycheck::Message::Progress { id, progress } => { let (state, message) = match progress { - flycheck::Progress::DidStart => { - self.diagnostics_received = false; - (Progress::Begin, None) - } + flycheck::Progress::DidStart => (Progress::Begin, None), flycheck::Progress::DidCheckCrate(target) => (Progress::Report, Some(target)), flycheck::Progress::DidCancel => { self.last_flycheck_error = None; @@ -852,9 +847,6 @@ impl GlobalState { flycheck::Progress::DidFinish(result) => { self.last_flycheck_error = result.err().map(|err| format!("cargo check failed to start: {err}")); - if !self.diagnostics_received { - self.diagnostics.clear_check(id); - } (Progress::End, None) } };