From 37093485fde5eb93c33c4844dbd83d20db3d13c1 Mon Sep 17 00:00:00 2001 From: Piotr Figiela <77412592+Draggu@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:27:02 +0100 Subject: [PATCH] LS: Proc macros support closes #6141 commit-id:7ea22adb --- Cargo.lock | 175 +++++++++++++-- crates/cairo-lang-language-server/Cargo.toml | 3 + .../cairo-lang-language-server/src/config.rs | 15 ++ .../src/lang/db/mod.rs | 78 ++++++- .../src/lang/db/swapper.rs | 21 ++ .../src/lang/mod.rs | 1 + .../src/lang/proc_macros/client/connection.rs | 76 +++++++ .../src/lang/proc_macros/client/controller.rs | 205 ++++++++++++++++++ .../lang/proc_macros/client/id_generator.rs | 16 ++ .../src/lang/proc_macros/client/mod.rs | 147 +++++++++++++ .../src/lang/proc_macros/client/status.rs | 51 +++++ .../src/lang/proc_macros/db.rs | 74 +++++++ .../src/lang/proc_macros/mod.rs | 3 + .../src/lang/proc_macros/plugins/mod.rs | 66 ++++++ crates/cairo-lang-language-server/src/lib.rs | 35 ++- .../cairo-lang-language-server/src/lsp/ext.rs | 9 + .../src/server/connection.rs | 9 +- .../cairo-lang-language-server/src/state.rs | 7 +- .../src/toolchain/scarb.rs | 19 +- 19 files changed, 981 insertions(+), 29 deletions(-) create mode 100644 crates/cairo-lang-language-server/src/lang/proc_macros/client/connection.rs create mode 100644 crates/cairo-lang-language-server/src/lang/proc_macros/client/controller.rs create mode 100644 crates/cairo-lang-language-server/src/lang/proc_macros/client/id_generator.rs create mode 100644 crates/cairo-lang-language-server/src/lang/proc_macros/client/mod.rs create mode 100644 crates/cairo-lang-language-server/src/lang/proc_macros/client/status.rs create mode 100644 crates/cairo-lang-language-server/src/lang/proc_macros/db.rs create mode 100644 crates/cairo-lang-language-server/src/lang/proc_macros/mod.rs create mode 100644 crates/cairo-lang-language-server/src/lang/proc_macros/plugins/mod.rs diff --git a/Cargo.lock b/Cargo.lock index d7befe01f4a..71da351a5f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -638,6 +638,7 @@ dependencies = [ "cairo-lang-formatter", "cairo-lang-language-server", "cairo-lang-lowering", + "cairo-lang-macro", "cairo-lang-parser", "cairo-lang-project", "cairo-lang-semantic", @@ -647,6 +648,7 @@ dependencies = [ "cairo-lang-test-utils", "cairo-lang-utils", "crossbeam", + "governor", "indent", "indoc", "itertools 0.12.1", @@ -659,6 +661,7 @@ dependencies = [ "rust-analyzer-salsa", "rustc-hash", "scarb-metadata", + "scarb-proc-macro-server-types", "serde", "serde_json", "smol_str", @@ -698,6 +701,34 @@ dependencies = [ "test-log", ] +[[package]] +name = "cairo-lang-macro" +version = "0.1.0" +source = "git+https://github.com/software-mansion/scarb?rev=fc99b54#fc99b54ccbcfc38419b395ae6ca633aa266639c9" +dependencies = [ + "cairo-lang-macro-attributes", + "cairo-lang-macro-stable", + "linkme", + "serde", +] + +[[package]] +name = "cairo-lang-macro-attributes" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e32e958decd95ae122ee64daa26721da2f76e83231047f947fd9cdc5d3c90cc6" +dependencies = [ + "quote", + "scarb-stable-hash", + "syn 2.0.72", +] + +[[package]] +name = "cairo-lang-macro-stable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c49906d6b1c215e5814be7c5c65ecf2328898b335bee8c2409ec07cfb5530daf" + [[package]] name = "cairo-lang-parser" version = "2.8.4" @@ -1501,6 +1532,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + [[package]] name = "deranged" version = "0.3.11" @@ -1751,9 +1788,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1761,9 +1798,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" @@ -1778,15 +1815,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -1795,15 +1832,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" @@ -1813,9 +1850,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1956,6 +1993,25 @@ dependencies = [ "minilp", ] +[[package]] +name = "governor" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0746aa765db78b521451ef74221663b57ba595bf83f75d0ce23cc09447c8139f" +dependencies = [ + "cfg-if", + "futures-sink", + "futures-timer", + "futures-util", + "no-std-compat", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "smallvec", + "spinning_top", +] + [[package]] name = "h2" version = "0.4.5" @@ -2417,6 +2473,26 @@ dependencies = [ "libc", ] +[[package]] +name = "linkme" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7dcd27dbec34243e167a18e8db8e58ff7d79c82851bcbc8c08aeaf673a1728" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc506a5de3d5b8f0a5ba36e2f93715d1f304969b18204054f379d297a993eb83" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -2576,6 +2652,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.3" @@ -2586,6 +2668,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3040,6 +3128,21 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" +[[package]] +name = "quanta" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quote" version = "1.0.36" @@ -3085,6 +3188,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "raw-cpuid" +version = "11.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "rawpointer" version = "0.2.1" @@ -3425,6 +3537,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "scarb-proc-macro-server-types" +version = "0.1.0" +source = "git+https://github.com/software-mansion/scarb?rev=fc99b54#fc99b54ccbcfc38419b395ae6ca633aa266639c9" +dependencies = [ + "cairo-lang-macro", + "serde", + "serde_json", +] + +[[package]] +name = "scarb-stable-hash" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1902536b23a05dd165d3992865870aaf1b0650317767cbf171ed2ca5903732a9" +dependencies = [ + "data-encoding", + "xxhash-rust", +] + [[package]] name = "schannel" version = "0.1.23" @@ -3681,6 +3813,15 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + [[package]] name = "sprs" version = "0.7.1" @@ -4778,6 +4919,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d422e8e38ec76e2f06ee439ccc765e9c6a9638b9e7c9f2e8255e4d41e8bd852" +[[package]] +name = "xxhash-rust" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984" + [[package]] name = "yansi" version = "0.5.1" diff --git a/crates/cairo-lang-language-server/Cargo.toml b/crates/cairo-lang-language-server/Cargo.toml index 8fa0ad21b0a..db4993781e8 100644 --- a/crates/cairo-lang-language-server/Cargo.toml +++ b/crates/cairo-lang-language-server/Cargo.toml @@ -25,7 +25,9 @@ cairo-lang-starknet = { path = "../cairo-lang-starknet", version = "~2.8.4" } cairo-lang-syntax = { path = "../cairo-lang-syntax", version = "~2.8.4" } cairo-lang-test-plugin = { path = "../cairo-lang-test-plugin", version = "~2.8.4" } cairo-lang-utils = { path = "../cairo-lang-utils", version = "~2.8.4" } +cairo-lang-macro = { git = "https://github.com/software-mansion/scarb", rev = "fc99b54" } # not registry version because conflict with scarb-proc-macro-server-types local version crossbeam = "0.8.4" +governor = { version = "0.7.0", default-features = false, features = ["std", "quanta"]} indent.workspace = true indoc.workspace = true itertools.workspace = true @@ -35,6 +37,7 @@ lsp-types = "=0.95.0" rustc-hash = "1.1.0" salsa.workspace = true scarb-metadata = "1.13" +scarb-proc-macro-server-types = { git = "https://github.com/software-mansion/scarb", rev = "fc99b54" } serde = { workspace = true, default-features = true } serde_json.workspace = true smol_str.workspace = true diff --git a/crates/cairo-lang-language-server/src/config.rs b/crates/cairo-lang-language-server/src/config.rs index a59e938307f..1ad6130114b 100644 --- a/crates/cairo-lang-language-server/src/config.rs +++ b/crates/cairo-lang-language-server/src/config.rs @@ -38,6 +38,14 @@ pub struct Config { /// The property is set by the user under the `cairo1.traceMacroDiagnostics` key in client /// configuration. pub trace_macro_diagnostics: bool, + /// Whether to resolve procedural macros or ignore them. + /// + /// The property is set by the user under the `cairo1.disableProcMacros` key in client + /// configuration. + /// [`None`] means that config has not yet been loaded from client. + /// We want to default it to `false`, but to prevent proc-macro-server from starting + /// immediately even if client want to set it to `true` we uses this 3rd value. + pub disable_proc_macros: Option, } impl Config { @@ -61,6 +69,10 @@ impl Config { scope_uri: None, section: Some("cairo1.traceMacroDiagnostics".to_owned()), }, + ConfigurationItem { + scope_uri: None, + section: Some("cairo1.disableProcMacros".to_owned()), + }, ]; let expected_len = items.len(); @@ -86,6 +98,9 @@ impl Config { .map(Into::into); state.config.trace_macro_diagnostics = response.pop_front().as_ref().and_then(Value::as_bool).unwrap_or_default(); + state.config.disable_proc_macros = Some( + response.pop_front().as_ref().and_then(Value::as_bool).unwrap_or_default(), + ); debug!("reloaded configuration: {:#?}", state.config); }) diff --git a/crates/cairo-lang-language-server/src/lang/db/mod.rs b/crates/cairo-lang-language-server/src/lang/db/mod.rs index e1f4dd9eade..9c8cb3d7d91 100644 --- a/crates/cairo-lang-language-server/src/lang/db/mod.rs +++ b/crates/cairo-lang-language-server/src/lang/db/mod.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use cairo_lang_defs::db::{DefsDatabase, DefsGroup, try_ext_as_virtual_impl}; use cairo_lang_doc::db::DocDatabase; use cairo_lang_filesystem::cfg::{Cfg, CfgSet}; @@ -13,12 +15,13 @@ use cairo_lang_semantic::inline_macros::get_default_plugin_suite; use cairo_lang_semantic::plugin::PluginSuite; use cairo_lang_starknet::starknet_plugin_suite; use cairo_lang_syntax::node::db::{SyntaxDatabase, SyntaxGroup}; -use cairo_lang_test_plugin::test_plugin_suite; +use cairo_lang_test_plugin::test_assert_suite; use cairo_lang_utils::Upcast; pub use self::semantic::*; pub use self::swapper::*; pub use self::syntax::*; +use super::proc_macros::db::{ProcMacroCacheDatabase, ProcMacroCacheGroup}; use crate::Tricks; mod semantic; @@ -33,7 +36,8 @@ mod syntax; ParserDatabase, SemanticDatabase, SyntaxDatabase, - DocDatabase + DocDatabase, + ProcMacroCacheDatabase )] pub struct AnalysisDatabase { storage: salsa::Storage, @@ -49,16 +53,28 @@ impl AnalysisDatabase { db.set_cfg_set(Self::initial_cfg_set().into()); - let plugin_suite = - [get_default_plugin_suite(), starknet_plugin_suite(), test_plugin_suite()] - .into_iter() - .chain(tricks.extra_plugin_suites.iter().flat_map(|f| f())) - .fold(PluginSuite::default(), |mut acc, suite| { - acc.add(suite); - acc - }); + let plugin_suite = [ + get_default_plugin_suite(), + starknet_plugin_suite(), + // test_plugin_suite() + test_assert_suite(), + ] + .into_iter() + .chain(tricks.extra_plugin_suites.iter().flat_map(|f| f())) + .fold(PluginSuite::default(), |mut acc, suite| { + acc.add(suite); + acc + }); db.apply_plugin_suite(plugin_suite); + // proc-macro-server can be restarted many times but we want to keep these data across + // multiple server starts, so init it once per database, not per server. + db.set_attribute_macro_resolution(Default::default()); + db.set_derive_macro_resolution(Default::default()); + db.set_inline_macro_resolution(Default::default()); + // We read this to check if client is available so it should be initialized. + db.set_proc_macro_client_status(Default::default()); + db } @@ -83,6 +99,48 @@ impl AnalysisDatabase { self.set_inline_macro_plugins(plugin_suite.inline_macro_plugins.into()); self.set_analyzer_plugins(plugin_suite.analyzer_plugins); } + + /// Removes all plugins from `previous_plugin_suite` if exists, plugins are considered + /// equal if uses same [`Arc`]. Then adds `new_plugin_suite`, unlike + /// [`AnalysisDatabase::apply_plugin_suite`] this will keep all other plugins in place. + pub(crate) fn replace_plugin_suite( + &mut self, + previous_plugin_suite: Option, + new_plugin_suite: PluginSuite, + ) { + let mut macro_plugins = self.macro_plugins(); + let mut analyzer_plugins = self.analyzer_plugins(); + let mut inline_macro_plugins = Arc::unwrap_or_clone(self.inline_macro_plugins()); + + if let Some(previous_plugin_suite) = previous_plugin_suite { + macro_plugins.retain(|plugin| { + previous_plugin_suite + .plugins + .iter() + .all(|previous_plugin| !Arc::ptr_eq(previous_plugin, plugin)) + }); + analyzer_plugins.retain(|plugin| { + previous_plugin_suite + .analyzer_plugins + .iter() + .all(|previous_plugin| !Arc::ptr_eq(previous_plugin, plugin)) + }); + inline_macro_plugins.retain(|_, plugin| { + previous_plugin_suite + .inline_macro_plugins + .iter() + .all(|(_, previous_plugin)| !Arc::ptr_eq(previous_plugin, plugin)) + }); + } + + macro_plugins.extend(new_plugin_suite.plugins); + analyzer_plugins.extend(new_plugin_suite.analyzer_plugins); + inline_macro_plugins.extend(new_plugin_suite.inline_macro_plugins); + + self.set_macro_plugins(macro_plugins); + self.set_analyzer_plugins(analyzer_plugins); + self.set_inline_macro_plugins(Arc::new(inline_macro_plugins)); + } } impl salsa::Database for AnalysisDatabase {} diff --git a/crates/cairo-lang-language-server/src/lang/db/swapper.rs b/crates/cairo-lang-language-server/src/lang/db/swapper.rs index a6fa7d71472..b52d49679d3 100644 --- a/crates/cairo-lang-language-server/src/lang/db/swapper.rs +++ b/crates/cairo-lang-language-server/src/lang/db/swapper.rs @@ -3,8 +3,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::Arc; use std::time::{Duration, SystemTime}; +use cairo_lang_defs::db::DefsGroup; use cairo_lang_filesystem::db::FilesGroup; use cairo_lang_filesystem::ids::FileId; +use cairo_lang_semantic::db::SemanticGroup; use cairo_lang_utils::ordered_hash_map::OrderedHashMap; use cairo_lang_utils::{Intern, LookupIntern}; use lsp_types::Url; @@ -13,6 +15,7 @@ use tracing::{error, warn}; use crate::config::Config; use crate::lang::db::AnalysisDatabase; use crate::lang::lsp::LsProtoGroup; +use crate::lang::proc_macros::db::ProcMacroCacheGroup; use crate::server::client::Notifier; use crate::toolchain::scarb::ScarbToolchain; use crate::{Backend, Tricks, env_config}; @@ -86,6 +89,7 @@ impl AnalysisDatabaseSwapper { ) { let Ok(new_db) = catch_unwind(AssertUnwindSafe(|| { let mut new_db = AnalysisDatabase::new(tricks); + self.migrate_proc_macro_state(&mut new_db, db); self.migrate_file_overrides(&mut new_db, db, open_files); self.detect_crates_for_open_files(&mut new_db, open_files, config, notifier); new_db @@ -99,6 +103,23 @@ impl AnalysisDatabaseSwapper { self.last_replace = SystemTime::now(); } + /// Copies current proc macro state into new db. + fn migrate_proc_macro_state(&self, new_db: &mut AnalysisDatabase, old_db: &AnalysisDatabase) { + new_db.set_macro_plugins(old_db.macro_plugins()); + new_db.set_inline_macro_plugins(old_db.inline_macro_plugins()); + // Currently there is no analyzer plugins here, but keep it because it would be hard to + // track it if we need it in future. + new_db.set_analyzer_plugins(old_db.analyzer_plugins()); + + new_db.set_proc_macro_client_status(old_db.proc_macro_client_status()); + + // TODO probably this should not be part of migration as it will be ever growing. + // But diagnostics going crazy every 5 minutes are no better. + new_db.set_attribute_macro_resolution(old_db.attribute_macro_resolution()); + new_db.set_derive_macro_resolution(old_db.derive_macro_resolution()); + new_db.set_inline_macro_resolution(old_db.inline_macro_resolution()); + } + /// Makes sure that all open files exist in the new db, with their current changes. fn migrate_file_overrides( &self, diff --git a/crates/cairo-lang-language-server/src/lang/mod.rs b/crates/cairo-lang-language-server/src/lang/mod.rs index ec6b506c07f..e0b4053e5fc 100644 --- a/crates/cairo-lang-language-server/src/lang/mod.rs +++ b/crates/cairo-lang-language-server/src/lang/mod.rs @@ -2,3 +2,4 @@ pub mod db; pub mod diagnostics; pub mod inspect; pub mod lsp; +pub mod proc_macros; diff --git a/crates/cairo-lang-language-server/src/lang/proc_macros/client/connection.rs b/crates/cairo-lang-language-server/src/lang/proc_macros/client/connection.rs new file mode 100644 index 00000000000..dd2f20c3998 --- /dev/null +++ b/crates/cairo-lang-language-server/src/lang/proc_macros/client/connection.rs @@ -0,0 +1,76 @@ +use std::io::{BufRead, BufReader, Write}; + +use crossbeam::channel::{Receiver, Sender}; +use scarb_proc_macro_server_types::jsonrpc::{RpcRequest, RpcResponse}; +use tracing::{error, info}; + +#[derive(Debug)] +pub struct ProcMacroServerConnection { + pub(super) requester: Sender, + pub(super) responder: Receiver, +} + +impl ProcMacroServerConnection { + pub fn new(mut proc_macro_server: std::process::Child) -> Self { + let server_output = proc_macro_server.stdout.take().unwrap(); + let mut server_input = proc_macro_server.stdin.take().unwrap(); + + let (sender, responder) = crossbeam::channel::unbounded(); + let (requester, receiver) = crossbeam::channel::bounded(0); + + std::thread::spawn(move || { + let mut line = String::new(); + let mut output = BufReader::new(server_output); + + loop { + line.clear(); + + let Ok(bytes) = output.read_line(&mut line) else { + error!("Error occurred while reading from proc-macro-server"); + break; + }; + + // This mean end of stream. + if bytes == 0 { + // Wait for process to exit as it will not produce any more data. + let _ = proc_macro_server.wait(); + + break; + } + + if line.trim().is_empty() { + continue; + } + + let Ok(response) = serde_json::from_str::(&line) else { + error!("Error occurred while deserializing response, used input:\n{line}"); + + break; + }; + + if sender.send(response).is_err() { + info!("Stopped reading from proc-macro-server"); + + // No receiver exists so stop reading and drop this thread. + break; + } + } + }); + + std::thread::spawn(move || { + for request in receiver { + let mut request = serde_json::to_vec(&request).unwrap(); + + request.push(b'\n'); + + if server_input.write_all(&request).is_err() { + error!("Error occurred while writing to proc-macro-server"); + + break; + } + } + }); + + Self { requester, responder } + } +} diff --git a/crates/cairo-lang-language-server/src/lang/proc_macros/client/controller.rs b/crates/cairo-lang-language-server/src/lang/proc_macros/client/controller.rs new file mode 100644 index 00000000000..7121873f563 --- /dev/null +++ b/crates/cairo-lang-language-server/src/lang/proc_macros/client/controller.rs @@ -0,0 +1,205 @@ +use std::num::NonZeroU32; +use std::time::Duration; + +use anyhow::{Context, Result, anyhow}; +use cairo_lang_semantic::plugin::PluginSuite; +use governor::clock::QuantaClock; +use governor::state::{InMemoryState, NotKeyed}; +use governor::{Quota, RateLimiter}; +use scarb_proc_macro_server_types::jsonrpc::RpcResponse; +use scarb_proc_macro_server_types::methods::ProcMacroResult; +use tracing::error; + +use super::connection::ProcMacroServerConnection; +use super::status::{ClientStatus, ProcMacroClientStatusChange}; +use super::{ProcMacroClient, RequestParams}; +use crate::config::Config; +use crate::lang::db::AnalysisDatabase; +use crate::lang::proc_macros::client::status::ClientStatusChange; +use crate::lang::proc_macros::db::ProcMacroCacheGroup; +use crate::lang::proc_macros::plugins::proc_macro_plugin_suite; +use crate::lsp::ext::ProcMacroServerFatalFailed; +use crate::server::client::Notifier; +use crate::toolchain::scarb::ScarbToolchain; + +/// Manages lifecycle of proc-macro-server client. +pub struct ProcMacroClientController { + status_change: ProcMacroClientStatusChange, + notifier: Notifier, + scarb: ScarbToolchain, + plugin_suite: Option, + initialization_retries: RateLimiter, +} + +impl ProcMacroClientController { + pub fn new(notifier: Notifier, scarb: ScarbToolchain) -> Self { + Self { + status_change: Default::default(), + notifier, + scarb, + plugin_suite: Default::default(), + initialization_retries: RateLimiter::direct( + Quota::with_period( + Duration::from_secs(180 / 5), // Across 3 minutes (180 seconds) / 5 retries. + ) + .unwrap() + .allow_burst( + NonZeroU32::new(5).unwrap(), // All 5 retries can be used as fast as possible. + ), + ), + } + } + + /// Runs in post sync task hook. Starts proc-macro-server after config reload. + /// Note that this will only try to go from `ClientStatus::Disabled` to + /// `ClientStatus::Initializing` if config now allows this. + pub fn try_initialize_if_disabled(&mut self, db: &mut AnalysisDatabase, config: &Config) { + if db.proc_macro_client_status().disabled() { + self.try_initialize(db, config); + } + } + + /// Tries starting proc-macro-server initialization process, if enabled. + /// + /// Returns value indicating if attempted to initialize. + pub fn try_initialize(&mut self, db: &mut AnalysisDatabase, config: &Config) -> bool { + // Do not initialize if not yet received client config (None) or received `true`. + // Also if disabled skip rate limiter check as this should not reduce burst. + let initialize = config.disable_proc_macros == Some(false) + && self.initialization_retries.check().is_ok(); + + if initialize { + self.spawn_server(db); + } + + initialize + } + + /// Spawns proc-macro-server. + fn spawn_server(&mut self, db: &mut AnalysisDatabase) { + match self.scarb.proc_macro_server() { + Ok(proc_macro_server) => { + db.set_proc_macro_client_status(ClientStatus::Initializing); + + let client = ProcMacroClient::new( + ProcMacroServerConnection::new(proc_macro_server), + self.status_change.clone(), + ); + + // `initialize` is blocking. + std::thread::spawn(move || client.initialize()); + } + Err(err) => { + error!("spawning proc-macro-server failed: {err:?}"); + + self.status_change.update(ClientStatusChange::FatalFailed); + } + } + } + + /// Check if there was status change reported and applies it. + /// If client is ready applies all available responses. + pub fn maybe_update_and_apply_responses(&mut self, db: &mut AnalysisDatabase, config: &Config) { + if let Some(change) = self.status_change.changed() { + self.update_status(db, config, change); + }; + + // TODO we should check here if there are live snapshots, but no idea if it is possible with + // salsa public api. if there are snapshots running we can skip this job, this will lead to + // more updates at once later and less cancellation. + if let Some(client) = db.proc_macro_client_status().ready() { + self.apply_responses(db, client); + }; + } + + /// Process status change update. + fn update_status( + &mut self, + db: &mut AnalysisDatabase, + config: &Config, + status: ClientStatusChange, + ) { + match status { + ClientStatusChange::Failed if self.try_initialize(db, config) => {} + ClientStatusChange::Failed | ClientStatusChange::FatalFailed => { + db.set_proc_macro_client_status(ClientStatus::InitializingFailed); + + self.notifier.notify::(()); + } + ClientStatusChange::Ready(defined_macros, client) => { + let new_plugin_suite = proc_macro_plugin_suite(defined_macros); + + // Store current plugins for identity comparison, so we can remove them if we + // restart proc-macro-server. + let previous_plugin_suite = self.plugin_suite.replace(new_plugin_suite.clone()); + + db.replace_plugin_suite(previous_plugin_suite, new_plugin_suite); + + db.set_proc_macro_client_status(ClientStatus::Ready(client)); + } + } + } + + /// Process proc-macro-server responses by updating resolutions. + pub fn apply_responses(&self, db: &mut AnalysisDatabase, client: &ProcMacroClient) { + let mut attribute_resolutions = db.attribute_macro_resolution(); + let mut attribute_resolutions_changed = false; + + let mut derive_resolutions = db.derive_macro_resolution(); + let mut derive_resolutions_changed = false; + + let mut inline_macro_resolutions = db.inline_macro_resolution(); + let mut inline_macro_resolutions_changed = false; + + let mut requests = client.requests_params.lock().unwrap(); + + for response in client.available_responses() { + // TODO investigate this as it somehow panicks if there is \0 problem. + let params = requests.remove(&response.id).unwrap(); + + match parse_proc_macro_response(response) { + Ok(result) => { + match params { + RequestParams::Attribute(params) => { + attribute_resolutions.insert(params, result); + attribute_resolutions_changed = true; + } + RequestParams::Derive(params) => { + derive_resolutions.insert(params, result); + derive_resolutions_changed = true; + } + RequestParams::Inline(params) => { + inline_macro_resolutions.insert(params, result); + inline_macro_resolutions_changed = true; + } + }; + } + Err(error) => { + error!("{error:#?}"); + + self.status_change.update(ClientStatusChange::Failed); + } + } + } + + // Set input only if resolution changed, this way we don't recompute queries if there were + // no updates. + if attribute_resolutions_changed { + db.set_attribute_macro_resolution(attribute_resolutions); + } + if derive_resolutions_changed { + db.set_derive_macro_resolution(derive_resolutions); + } + if inline_macro_resolutions_changed { + db.set_inline_macro_resolution(inline_macro_resolutions); + } + } +} + +fn parse_proc_macro_response(response: RpcResponse) -> Result { + let success = response + .into_result() + .map_err(|error| anyhow!("proc-macro-server responded with error: {error:?}"))?; + + serde_json::from_value(success).context("failed to deserialize response into `ProcMacroResult`") +} diff --git a/crates/cairo-lang-language-server/src/lang/proc_macros/client/id_generator.rs b/crates/cairo-lang-language-server/src/lang/proc_macros/client/id_generator.rs new file mode 100644 index 00000000000..d5290233c3e --- /dev/null +++ b/crates/cairo-lang-language-server/src/lang/proc_macros/client/id_generator.rs @@ -0,0 +1,16 @@ +use std::sync::atomic::{AtomicU64, Ordering}; + +use scarb_proc_macro_server_types::jsonrpc::RequestId; + +/// Atomic Id generator +#[derive(Debug, Default)] +pub struct IdGenerator { + counter: AtomicU64, +} + +impl IdGenerator { + /// Every call to this method returns different ID, but order is not guaranteed. + pub fn unique_id(&self) -> RequestId { + self.counter.fetch_add(1, Ordering::Relaxed) + } +} diff --git a/crates/cairo-lang-language-server/src/lang/proc_macros/client/mod.rs b/crates/cairo-lang-language-server/src/lang/proc_macros/client/mod.rs new file mode 100644 index 00000000000..93b477581c8 --- /dev/null +++ b/crates/cairo-lang-language-server/src/lang/proc_macros/client/mod.rs @@ -0,0 +1,147 @@ +use std::sync::{Arc, Mutex}; + +use anyhow::{Context, Result, anyhow, ensure}; +use connection::ProcMacroServerConnection; +use rustc_hash::FxHashMap; +use scarb_proc_macro_server_types::jsonrpc::{RequestId, RpcRequest, RpcResponse}; +use scarb_proc_macro_server_types::methods::Method; +use scarb_proc_macro_server_types::methods::defined_macros::{ + DefinedMacros, DefinedMacrosParams, DefinedMacrosResponse, +}; +use scarb_proc_macro_server_types::methods::expand::{ + ExpandAttribute, ExpandAttributeParams, ExpandDerive, ExpandDeriveParams, ExpandInline, + ExpandInlineMacroParams, +}; +pub use status::ClientStatus; +use status::{ClientStatusChange, ProcMacroClientStatusChange}; +use tracing::error; + +mod connection; +pub mod controller; +mod id_generator; +mod status; + +#[derive(Debug)] +pub enum RequestParams { + Attribute(ExpandAttributeParams), + Derive(ExpandDeriveParams), + Inline(ExpandInlineMacroParams), +} + +#[derive(Debug)] +pub struct ProcMacroClient { + connection: ProcMacroServerConnection, + status_change: ProcMacroClientStatusChange, + id_generator: id_generator::IdGenerator, + pub(super) requests_params: Mutex>, +} + +impl ProcMacroClient { + fn new( + connection: ProcMacroServerConnection, + status_change: ProcMacroClientStatusChange, + ) -> Self { + Self { + connection, + status_change, + id_generator: Default::default(), + requests_params: Default::default(), + } + } + + pub fn request_attribute(&self, params: ExpandAttributeParams) { + self.send_request_tracked::(params, RequestParams::Attribute) + } + + pub fn request_derives(&self, params: ExpandDeriveParams) { + self.send_request_tracked::(params, RequestParams::Derive) + } + + pub fn request_inline_macros(&self, params: ExpandInlineMacroParams) { + self.send_request_tracked::(params, RequestParams::Inline) + } + + /// Initlializes client by fetching defined macros. + /// Note: This is blocking! + fn initialize(self) { + match self.fetch_defined_macros() { + Ok(defined_macros) => { + self.status_change + .clone() + .update(ClientStatusChange::Ready(defined_macros, Arc::new(self))); + } + Err(err) => { + error!("failed to fetch defined macros: {err:?}"); + + self.status_change.update(ClientStatusChange::Failed); + } + } + } + + /// Reads all available responses without waiting for new ones. + fn available_responses(&self) -> impl Iterator + '_ { + self.connection.responder.try_iter() + } + + fn fetch_defined_macros(&self) -> Result { + let id = self.send_request::(&DefinedMacrosParams {})?; + + ensure!( + id == 0, + "fetching defined macros should be first sended request, it is {id} (zero counting)" + ); + + // This works because this it is first request we sends and we wait for response before + // sending any more requests. + let response = self + .connection + .responder + .recv() + .context("failed to read response for defined macros request")?; + + ensure!( + response.id == id, + "fetching defined macros should be waited before any other request is send, received \ + response for id: {} <- should be {id}", + response.id + ); + + let success = response + .into_result() + .map_err(|error| anyhow!("proc-macro-server responded with error: {error:?}"))?; + + serde_json::from_value(success) + .context("failed to deserialize response for defined macros request") + } + + fn send_request(&self, params: &M::Params) -> Result { + let id = self.id_generator.unique_id(); + + self.connection + .requester + .send(RpcRequest { + id, + method: M::METHOD.to_string(), + value: serde_json::to_value(params).unwrap(), + }) + .with_context(|| anyhow!("sending request {id} failed")) + .map(|_| id) + } + + fn send_request_tracked( + &self, + params: M::Params, + map: impl FnOnce(M::Params) -> RequestParams, + ) { + match self.send_request::(¶ms) { + Ok(id) => { + self.requests_params.lock().unwrap().insert(id, map(params)); + } + Err(err) => { + error!("Sending request to proc-macro-server failed: {err:?}"); + + self.status_change.update(ClientStatusChange::Failed); + } + } + } +} diff --git a/crates/cairo-lang-language-server/src/lang/proc_macros/client/status.rs b/crates/cairo-lang-language-server/src/lang/proc_macros/client/status.rs new file mode 100644 index 00000000000..8acab3bbb38 --- /dev/null +++ b/crates/cairo-lang-language-server/src/lang/proc_macros/client/status.rs @@ -0,0 +1,51 @@ +use std::sync::{Arc, Mutex}; + +use scarb_proc_macro_server_types::methods::defined_macros::DefinedMacrosResponse; + +use super::ProcMacroClient; + +#[derive(Debug, Clone, Default)] +pub struct ProcMacroClientStatusChange(Arc>>); + +impl ProcMacroClientStatusChange { + pub fn update(&self, change: ClientStatusChange) { + *self.0.lock().unwrap() = Some(change); + } + + pub fn changed(&self) -> Option { + self.0.lock().unwrap().take() + } +} + +#[derive(Debug, Default, Clone)] +pub enum ClientStatus { + // Disabled is default because it is initialized before receiving client config, though we + // should not start it if config can prohibit this. + #[default] + Disabled, + Initializing, + Ready(Arc), + // After retries it does not work. No more actions will be done. + InitializingFailed, +} + +impl ClientStatus { + pub fn ready(&self) -> Option<&ProcMacroClient> { + if let Self::Ready(client) = self { Some(client) } else { None } + } + + pub fn disabled(&self) -> bool { + matches!(self, Self::Disabled) + } +} + +/// Edges of [`ClientStatus`]. +/// Represents possible state transitions. +#[derive(Debug)] +pub enum ClientStatusChange { + Ready(DefinedMacrosResponse, Arc), + // We can retry. + Failed, + // Even if we retry it probably won't work anyway. + FatalFailed, +} diff --git a/crates/cairo-lang-language-server/src/lang/proc_macros/db.rs b/crates/cairo-lang-language-server/src/lang/proc_macros/db.rs new file mode 100644 index 00000000000..ce8654cddfc --- /dev/null +++ b/crates/cairo-lang-language-server/src/lang/proc_macros/db.rs @@ -0,0 +1,74 @@ +use cairo_lang_macro::TokenStream; +use rustc_hash::FxHashMap; +use scarb_proc_macro_server_types::methods::ProcMacroResult; +use scarb_proc_macro_server_types::methods::expand::{ + ExpandAttributeParams, ExpandDeriveParams, ExpandInlineMacroParams, +}; + +use super::client::ClientStatus; + +#[salsa::query_group(ProcMacroCacheDatabase)] +pub trait ProcMacroCacheGroup { + #[salsa::input] + fn attribute_macro_resolution(&self) -> FxHashMap; + #[salsa::input] + fn derive_macro_resolution(&self) -> FxHashMap; + #[salsa::input] + fn inline_macro_resolution(&self) -> FxHashMap; + + #[salsa::input] + fn proc_macro_client_status(&self) -> ClientStatus; + + /// Returns the expansion of attribute macro. + fn get_attribute_expansion(&self, params: ExpandAttributeParams) -> ProcMacroResult; + /// Returns the expansion of derive macros. + fn get_derive_expansion(&self, params: ExpandDeriveParams) -> ProcMacroResult; + /// Returns the expansion of inline macro. + fn get_inline_macros_expansion(&self, params: ExpandInlineMacroParams) -> ProcMacroResult; +} + +fn get_attribute_expansion( + db: &dyn ProcMacroCacheGroup, + params: ExpandAttributeParams, +) -> ProcMacroResult { + db.attribute_macro_resolution().get(¶ms).cloned().unwrap_or_else(|| { + let token_stream = params.item.clone(); + + if let Some(client) = db.proc_macro_client_status().ready() { + client.request_attribute(params); + } + + ProcMacroResult { token_stream, diagnostics: Default::default() } + }) +} + +fn get_derive_expansion( + db: &dyn ProcMacroCacheGroup, + params: ExpandDeriveParams, +) -> ProcMacroResult { + db.derive_macro_resolution().get(¶ms).cloned().unwrap_or_else(|| { + let token_stream = params.item.clone(); + + if let Some(client) = db.proc_macro_client_status().ready() { + client.request_derives(params); + } + + ProcMacroResult { token_stream, diagnostics: Default::default() } + }) +} + +fn get_inline_macros_expansion( + db: &dyn ProcMacroCacheGroup, + params: ExpandInlineMacroParams, +) -> ProcMacroResult { + db.inline_macro_resolution().get(¶ms).cloned().unwrap_or_else(|| { + // we can't return original node because it will make infinite recursive resolving. + let token_stream = TokenStream::new("()".to_string()); + + if let Some(client) = db.proc_macro_client_status().ready() { + client.request_inline_macros(params); + } + + ProcMacroResult { token_stream, diagnostics: Default::default() } + }) +} diff --git a/crates/cairo-lang-language-server/src/lang/proc_macros/mod.rs b/crates/cairo-lang-language-server/src/lang/proc_macros/mod.rs new file mode 100644 index 00000000000..1ae485a1c64 --- /dev/null +++ b/crates/cairo-lang-language-server/src/lang/proc_macros/mod.rs @@ -0,0 +1,3 @@ +pub mod client; +pub mod db; +pub mod plugins; diff --git a/crates/cairo-lang-language-server/src/lang/proc_macros/plugins/mod.rs b/crates/cairo-lang-language-server/src/lang/proc_macros/plugins/mod.rs new file mode 100644 index 00000000000..197015eb84f --- /dev/null +++ b/crates/cairo-lang-language-server/src/lang/proc_macros/plugins/mod.rs @@ -0,0 +1,66 @@ +use std::sync::Arc; + +use cairo_lang_defs::plugin::{InlineMacroExprPlugin, MacroPlugin}; +use cairo_lang_semantic::plugin::PluginSuite; +use scarb_proc_macro_server_types::methods::defined_macros::DefinedMacrosResponse; + +/// Important: NEVER make it pub outside this crate. +pub(crate) fn proc_macro_plugin_suite(defined_macros: DefinedMacrosResponse) -> PluginSuite { + let mut plugin_suite = PluginSuite::default(); + + plugin_suite.add_plugin_ex(Arc::new(ProcMacroPlugin { + defined_attributes: defined_macros.attributes, + defined_derives: defined_macros.derives, + defined_executable_attributes: defined_macros.executables, + })); + + let inline_plugin = Arc::new(InlineProcMacroPlugin); + + for inline_macro in defined_macros.inline_macros { + plugin_suite.add_inline_macro_plugin_ex(&inline_macro, inline_plugin.clone()); + } + + plugin_suite +} + +/// Important: NEVER make it public. +#[derive(Debug)] +struct ProcMacroPlugin { + defined_attributes: Vec, + defined_derives: Vec, + defined_executable_attributes: Vec, +} + +impl MacroPlugin for ProcMacroPlugin { + fn generate_code( + &self, + _db: &dyn cairo_lang_syntax::node::db::SyntaxGroup, + _item_ast: cairo_lang_syntax::node::ast::ModuleItem, + _metadata: &cairo_lang_defs::plugin::MacroPluginMetadata<'_>, + ) -> cairo_lang_defs::plugin::PluginResult { + todo!(); + } + + fn declared_attributes(&self) -> Vec { + [&self.defined_attributes[..], &self.defined_executable_attributes[..]].concat() + } + + fn declared_derives(&self) -> Vec { + self.defined_derives.clone() + } +} + +/// Important: NEVER make it public. +#[derive(Debug)] +struct InlineProcMacroPlugin; + +impl InlineMacroExprPlugin for InlineProcMacroPlugin { + fn generate_code( + &self, + _db: &dyn cairo_lang_syntax::node::db::SyntaxGroup, + _item_ast: &cairo_lang_syntax::node::ast::ExprInlineMacro, + _metadata: &cairo_lang_defs::plugin::MacroPluginMetadata<'_>, + ) -> cairo_lang_defs::plugin::InlinePluginResult { + todo!(); + } +} diff --git a/crates/cairo-lang-language-server/src/lib.rs b/crates/cairo-lang-language-server/src/lib.rs index afe838e48fa..7dcffa13fbd 100644 --- a/crates/cairo-lang-language-server/src/lib.rs +++ b/crates/cairo-lang-language-server/src/lib.rs @@ -38,10 +38,11 @@ //! } //! ``` +use std::num::NonZeroU32; use std::panic::RefUnwindSafe; use std::path::{Path, PathBuf}; use std::process::ExitCode; -use std::time::SystemTime; +use std::time::{Duration, SystemTime}; use std::{io, panic}; use anyhow::{Context, Result}; @@ -51,6 +52,7 @@ use cairo_lang_filesystem::db::FilesGroup; use cairo_lang_filesystem::ids::FileLongId; use cairo_lang_project::ProjectConfig; use cairo_lang_semantic::plugin::PluginSuite; +use governor::{Quota, RateLimiter}; use lsp_server::Message; use lsp_types::RegistrationParams; use salsa::{Database, Durability}; @@ -292,6 +294,9 @@ impl Backend { // we basically never hit such a case in CairoLS in happy paths. scheduler.on_sync_task(Self::refresh_diagnostics); + // Starts proc-macro-server after config has been changed, this counts first load. + scheduler.on_sync_task(Self::maybe_start_proc_macro_server); + let result = Self::event_loop(&connection, scheduler); // Trigger cancellation in any background tasks that might still be running. @@ -345,7 +350,21 @@ impl Backend { // | Commit: 46a457318d8d259376a2b458b3f814b9b795fe69 | // +--------------------------------------------------+ fn event_loop(connection: &Connection, mut scheduler: Scheduler<'_>) -> Result<()> { + // 1 per second. + let idle_job_limit = RateLimiter::direct(Quota::per_second(NonZeroU32::new(1).unwrap())); + for msg in connection.incoming() { + let Some(msg) = msg else { + if idle_job_limit.check().is_ok() { + scheduler.local(Self::run_idle_tasks); + } + + // Avoid busy-waiting, sleep for a short duration + std::thread::sleep(Duration::from_millis(1)); + + continue; + }; + if connection.handle_shutdown(&msg)? { break; } @@ -355,11 +374,20 @@ impl Backend { Message::Response(response) => scheduler.response(response), }; scheduler.dispatch(task); + + // Avoid busy-waiting, sleep for a short duration + std::thread::sleep(Duration::from_millis(1)); } Ok(()) } + /// Executes tasks when there is no incoming message. + /// Warning! Keep it very cheap! + fn run_idle_tasks(state: &mut State, _: Notifier, _: &mut Requester<'_>, _: Responder) { + state.proc_macro_controller.maybe_update_and_apply_responses(&mut state.db, &state.config); + } + /// Calls [`lang::db::AnalysisDatabaseSwapper::maybe_swap`] to do its work. fn maybe_swap_database(state: &mut State, notifier: Notifier) { state.db_swapper.maybe_swap( @@ -376,6 +404,11 @@ impl Backend { state.diagnostics_controller.refresh(state.snapshot(), notifier); } + /// Calls [`lang::proc_macros::client::controller::ProcMacroClientController::try_initialize_if_disabled`] to do its work. + fn maybe_start_proc_macro_server(state: &mut State, _notifier: Notifier) { + state.proc_macro_controller.try_initialize_if_disabled(&mut state.db, &state.config); + } + /// Tries to detect the crate root the config that contains a cairo file, and add it to the /// system. #[tracing::instrument(skip_all)] diff --git a/crates/cairo-lang-language-server/src/lsp/ext.rs b/crates/cairo-lang-language-server/src/lsp/ext.rs index 8a8d9db7865..0eeb7c4e873 100644 --- a/crates/cairo-lang-language-server/src/lsp/ext.rs +++ b/crates/cairo-lang-language-server/src/lsp/ext.rs @@ -59,3 +59,12 @@ impl Notification for ScarbMetadataFailed { type Params = (); const METHOD: &'static str = "cairo/scarb-metadata-failed"; } + +/// Notifies about `proc-macro-server` fatal fail. +#[derive(Debug)] +pub struct ProcMacroServerFatalFailed; + +impl Notification for ProcMacroServerFatalFailed { + type Params = (); + const METHOD: &'static str = "cairo/procMacroServerFatalFailed"; +} diff --git a/crates/cairo-lang-language-server/src/server/connection.rs b/crates/cairo-lang-language-server/src/server/connection.rs index 64240f0e3ee..d47708774fe 100644 --- a/crates/cairo-lang-language-server/src/server/connection.rs +++ b/crates/cairo-lang-language-server/src/server/connection.rs @@ -8,6 +8,7 @@ use std::sync::{Arc, Weak}; use anyhow::{Result, bail}; +use crossbeam::channel::TryRecvError; use lsp_server::{ Connection as LSPConnection, IoThreads, Message, Notification, Request, RequestId, Response, }; @@ -78,8 +79,12 @@ impl Connection { } /// An iterator over incoming messages from the client. - pub fn incoming(&self) -> crossbeam::channel::Iter<'_, Message> { - self.receiver.iter() + pub fn incoming(&self) -> impl Iterator> + '_ { + std::iter::from_fn(|| match self.receiver.try_recv() { + Ok(message) => Some(Some(message)), + Err(TryRecvError::Empty) => Some(None), + Err(TryRecvError::Disconnected) => None, + }) } /// Check and respond to any incoming shutdown requests; returns `true` if the server should be diff --git a/crates/cairo-lang-language-server/src/state.rs b/crates/cairo-lang-language-server/src/state.rs index c240e764507..7901fd6bd90 100644 --- a/crates/cairo-lang-language-server/src/state.rs +++ b/crates/cairo-lang-language-server/src/state.rs @@ -9,6 +9,7 @@ use crate::Tricks; use crate::config::Config; use crate::lang::db::{AnalysisDatabase, AnalysisDatabaseSwapper}; use crate::lang::diagnostics::DiagnosticsController; +use crate::lang::proc_macros::client::controller::ProcMacroClientController; use crate::server::client::Client; use crate::server::connection::ClientSender; use crate::toolchain::scarb::ScarbToolchain; @@ -23,6 +24,7 @@ pub struct State { pub db_swapper: AnalysisDatabaseSwapper, pub tricks: Owned, pub diagnostics_controller: DiagnosticsController, + pub proc_macro_controller: ProcMacroClientController, } impl State { @@ -32,8 +34,10 @@ impl State { tricks: Tricks, ) -> Self { let notifier = Client::new(sender).notifier(); - let scarb_toolchain = ScarbToolchain::new(notifier); + let scarb_toolchain = ScarbToolchain::new(notifier.clone()); let db_swapper = AnalysisDatabaseSwapper::new(scarb_toolchain.clone()); + let proc_macro_controller = + ProcMacroClientController::new(notifier, scarb_toolchain.clone()); Self { db: AnalysisDatabase::new(&tricks), @@ -44,6 +48,7 @@ impl State { db_swapper, tricks: Owned::new(tricks.into()), diagnostics_controller: DiagnosticsController::new(), + proc_macro_controller, } } diff --git a/crates/cairo-lang-language-server/src/toolchain/scarb.rs b/crates/cairo-lang-language-server/src/toolchain/scarb.rs index 4ac4f875c4d..b608e79ca25 100644 --- a/crates/cairo-lang-language-server/src/toolchain/scarb.rs +++ b/crates/cairo-lang-language-server/src/toolchain/scarb.rs @@ -1,7 +1,8 @@ use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; use std::sync::{Arc, OnceLock}; -use anyhow::{Context, Result, bail}; +use anyhow::{Context, Result, anyhow, bail}; use lsp_types::notification::Notification; use scarb_metadata::{Metadata, MetadataCommand}; use tracing::{error, warn}; @@ -129,6 +130,22 @@ impl ScarbToolchain { result } + + pub fn proc_macro_server(&self) -> Result { + let scarb_path = self.discover().ok_or(anyhow!("failed to get scarb path"))?; + + let proc_macro_server = Command::new(scarb_path) + .arg("--quiet") // If not set scarb will print all "Compiling ..." messages we don't need (and these can crash input parsing). + .arg("proc-macro-server") + .envs(std::env::var("RUST_BACKTRACE").map(|value| ("RUST_BACKTRACE", value))) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + // We uses this channel for debugging. + .stderr(Stdio::inherit()) + .spawn()?; + + Ok(proc_macro_server) + } } #[derive(Debug)]