From 51645a88c9b9c0d3e13a47da429ab9f7ef28d67f Mon Sep 17 00:00:00 2001 From: Dominik Nakamura Date: Tue, 12 Dec 2023 23:48:04 +0900 Subject: [PATCH] refactor(lsp): replace tower-lsp with lsp-server The `tower-lsp` crate appears to be somewhat unmaintained in the recent months and the `lsp-server` crate is made by the people behind rust-analyzer. In addition, the LSP server doesn't need a full async runtime as LSP communication is only with a single client and done in serial. This change cuts down on several dependencies, reducing both build time and binary size. --- Cargo.lock | 528 ++++++++++----------------------- crates/stef-lsp/Cargo.toml | 9 +- crates/stef-lsp/src/compile.rs | 2 +- crates/stef-lsp/src/logging.rs | 300 ++++++++++++++----- crates/stef-lsp/src/main.rs | 384 ++++++++++++++++-------- crates/stef-parser/Cargo.toml | 2 +- 6 files changed, 651 insertions(+), 574 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52cdd20..5b275c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,17 +86,6 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" -[[package]] -name = "async-trait" -version = "0.1.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.40", -] - [[package]] name = "atty" version = "0.2.14" @@ -108,18 +97,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "auto_impl" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee3da8ef1276b0bee5dd1c7258010d8fffd31801447323115a25560e1327b89" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -281,19 +258,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "deranged" version = "0.3.10" @@ -359,6 +323,15 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "erased-serde" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +dependencies = [ + "serde", +] + [[package]] name = "errno" version = "0.3.8" @@ -388,83 +361,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futures" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" - -[[package]] -name = "futures-io" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" - -[[package]] -name = "futures-macro" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.40", -] - -[[package]] -name = "futures-sink" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" - -[[package]] -name = "futures-task" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" - -[[package]] -name = "futures-util" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - [[package]] name = "getrandom" version = "0.2.11" @@ -501,12 +397,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "hashbrown" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" - [[package]] name = "heck" version = "0.4.1" @@ -528,12 +418,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - [[package]] name = "idna" version = "0.5.0" @@ -663,6 +547,21 @@ name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "value-bag", +] + +[[package]] +name = "lsp-server" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb69ba934913ebf0ef3b3dd762f0149bf993decd571d094b646de09c2e456732" +dependencies = [ + "crossbeam-channel", + "log", + "serde", + "serde_json", +] [[package]] name = "lsp-types" @@ -731,26 +630,6 @@ dependencies = [ "adler", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.3", - "libc", -] - [[package]] name = "num_threads" version = "0.1.6" @@ -806,12 +685,6 @@ dependencies = [ "syn 2.0.40", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "owo-colors" version = "3.5.0" @@ -821,6 +694,16 @@ dependencies = [ "supports-color 1.3.1", ] +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + [[package]] name = "parking_lot_core" version = "0.9.9" @@ -846,38 +729,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pin-project" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.40", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "powerfmt" version = "0.2.0" @@ -1039,6 +890,15 @@ dependencies = [ "syn 2.0.40", ] +[[package]] +name = "serde_fmt" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" +dependencies = [ + "serde", +] + [[package]] name = "serde_json" version = "1.0.108" @@ -1061,30 +921,12 @@ dependencies = [ "syn 2.0.40", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "similar" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - [[package]] name = "smallvec" version = "1.11.2" @@ -1190,17 +1032,16 @@ dependencies = [ "anyhow", "clap", "directories", + "log", + "lsp-server", + "lsp-types", "ouroboros", + "parking_lot", "serde", "serde_json", "stef-compiler", "stef-parser", "time", - "tokio", - "tower-lsp", - "tracing", - "tracing-appender", - "tracing-subscriber", ] [[package]] @@ -1267,6 +1108,74 @@ dependencies = [ "is-terminal", ] +[[package]] +name = "sval" +version = "2.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15df12a8db7c216a04b4b438f90d50d5335cd38f161b56389c9f5c9d96d0873" + +[[package]] +name = "sval_buffer" +version = "2.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e80556bc8acea0446e574ce542ad6114a76a0237f28a842bc01ca3ea98f479" +dependencies = [ + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d93d2259edb1d7b4316179f0a98c62e3ffc726f47ab200e07cfe382771f57b8" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532f7f882226f7a5a4656f5151224aaebf8217e0d539cb1595b831bace921343" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e03bd8aa0ae6ee018f7ae95c9714577687a4415bd1a5f19b26e34695f7e072" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_ref" +version = "2.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ed054f2fb8c2a0ab5d36c1ec57b412919700099fc5e32ad8e7a38b23e1a9e1" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff191c4ff05b67e3844c161021427646cde5d6624597958be158357d9200586" +dependencies = [ + "serde", + "sval", + "sval_buffer", + "sval_fmt", +] + [[package]] name = "syn" version = "1.0.109" @@ -1274,7 +1183,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote", "unicode-ident", ] @@ -1340,16 +1248,6 @@ dependencies = [ "syn 2.0.40", ] -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - [[package]] name = "time" version = "0.3.30" @@ -1396,162 +1294,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "tokio" -version = "1.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" -dependencies = [ - "backtrace", - "bytes", - "num_cpus", - "pin-project-lite", -] - -[[package]] -name = "tokio-util" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" - -[[package]] -name = "tower-lsp" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ba052b54a6627628d9b3c34c176e7eda8359b7da9acd497b9f20998d118508" -dependencies = [ - "async-trait", - "auto_impl", - "bytes", - "dashmap", - "futures", - "httparse", - "lsp-types", - "memchr", - "serde", - "serde_json", - "tokio", - "tokio-util", - "tower", - "tower-lsp-macros", - "tracing", -] - -[[package]] -name = "tower-lsp-macros" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.40", -] - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-appender" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" -dependencies = [ - "crossbeam-channel", - "thiserror", - "time", - "tracing-subscriber", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.40", -] - -[[package]] -name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "time", - "tracing-core", - "tracing-log", -] - [[package]] name = "unicode-bidi" version = "0.3.14" @@ -1604,10 +1346,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] -name = "valuable" -version = "0.1.0" +name = "value-bag" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "07ba39dc791ecb35baad371a3fc04c6eab688c04937d2e0ac6c22b612c0357bf" +dependencies = [ + "erased-serde", + "serde", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e06c10810a57bbf45778d023d432a50a1daa7d185991ae06bcfb6c654d0945" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] [[package]] name = "version_check" @@ -1862,9 +1634,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.26" +version = "0.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67b5f0a4e7a27a64c651977932b9dc5667ca7fc31ac44b03ed37a0cf42fdfff" +checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2" dependencies = [ "memchr", ] diff --git a/crates/stef-lsp/Cargo.toml b/crates/stef-lsp/Cargo.toml index cffc8fd..901c38d 100644 --- a/crates/stef-lsp/Cargo.toml +++ b/crates/stef-lsp/Cargo.toml @@ -13,17 +13,16 @@ license.workspace = true anyhow = "1.0.75" clap.workspace = true directories = "5.0.1" +log = { version = "0.4.20", features = ["kv_unstable_std", "std"] } +lsp-server = "0.7.5" +lsp-types = { version = "0.94.1", features = ["proposed"] } ouroboros = "0.18.1" +parking_lot = "0.12.1" serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" stef-compiler = { path = "../stef-compiler" } stef-parser = { path = "../stef-parser" } time = { version = "0.3.30", features = ["formatting", "local-offset", "macros"] } -tokio = { version = "1.35.0", features = ["fs", "io-std", "io-util", "rt-multi-thread"] } -tower-lsp = { version = "0.20.0", features = ["proposed"] } -tracing = "0.1.40" -tracing-appender = "0.2.3" -tracing-subscriber = { version = "0.3.18", features = ["time"] } [lints] workspace = true diff --git a/crates/stef-lsp/src/compile.rs b/crates/stef-lsp/src/compile.rs index 9563bd9..aa73741 100644 --- a/crates/stef-lsp/src/compile.rs +++ b/crates/stef-lsp/src/compile.rs @@ -1,5 +1,6 @@ use std::ops::Range; +use lsp_types::{self as lsp, Diagnostic, Url}; use stef_compiler::validate; use stef_parser::{ error::{ @@ -10,7 +11,6 @@ use stef_parser::{ }, Schema, }; -use tower_lsp::lsp_types::{self as lsp, Diagnostic, Url}; use crate::utf16; diff --git a/crates/stef-lsp/src/logging.rs b/crates/stef-lsp/src/logging.rs index dc1778c..80f13cc 100644 --- a/crates/stef-lsp/src/logging.rs +++ b/crates/stef-lsp/src/logging.rs @@ -1,104 +1,262 @@ -use std::io::Write; +use std::{fs::File, io::Write, path::PathBuf}; use anyhow::{Context, Result}; use directories::ProjectDirs; -use time::{format_description::FormatItem, macros::format_description, UtcOffset}; -use tower_lsp::{lsp_types::MessageType, Client}; -use tracing::Level; -use tracing_appender::non_blocking::NonBlocking; -use tracing_subscriber::{ - filter::Targets, - fmt::{time::OffsetTime, MakeWriter}, - prelude::*, +use log::{kv::Visitor, Level, LevelFilter, Metadata, Record}; +use lsp_server::{Connection, Message, Notification}; +use lsp_types::{ + notification::{LogMessage, Notification as _}, + LogMessageParams, MessageType, }; +use parking_lot::Mutex; +use time::{format_description::FormatItem, macros::format_description, OffsetDateTime, UtcOffset}; -pub fn prepare() -> Result<(Options, Guard)> { - let timer = OffsetTime::new( - UtcOffset::current_local_offset().context("failed retrieving local UTC offset")?, - format_description!("[hour]:[minute]:[second]"), - ); +static FORMAT_HMS: &[FormatItem<'_>] = format_description!("[hour]:[minute]:[second]"); + +pub fn init(conn: Option) -> Result<()> { + let offset = UtcOffset::current_local_offset()?; let dirs = ProjectDirs::from("rocks", "dnaka91", env!("CARGO_PKG_NAME")) .context("failed locating project directories")?; - let file_appender = tracing_appender::rolling::daily(dirs.cache_dir(), "log"); - let (file_appender, guard) = tracing_appender::non_blocking(file_appender); + log::set_max_level(LevelFilter::Trace); + log::set_boxed_logger(Box::new(CombinedLogger { + client: conn.map(|conn| ClientLogger::new(conn, offset)), + file: FileLogger::new(dirs.cache_dir().join("lsp.log"), offset)?, + stderr: StderrLogger::new(offset), + })) + .map_err(Into::into) +} + +fn write_message<'a>( + f: &mut impl std::io::Write, + record: &'a Record<'a>, + offset: UtcOffset, +) -> Result<()> { + OffsetDateTime::now_utc() + .to_offset(offset) + .format_into(f, FORMAT_HMS)?; + + write!( + f, + " {:5} {}: {}", + record.level(), + record.target(), + record.args() + )?; + + record.key_values().visit(&mut FormatVisitor(f))?; + + writeln!(f).map_err(Into::into) +} + +struct FormatVisitor<'a, T>(&'a mut T); + +impl Visitor<'_> for FormatVisitor<'_, T> { + fn visit_pair( + &mut self, + key: log::kv::Key<'_>, + value: log::kv::Value<'_>, + ) -> Result<(), log::kv::Error> { + write!(self.0, " {}=", key.as_str())?; + value.visit(self) + } +} + +impl log::kv::value::Visit<'_> for FormatVisitor<'_, T> { + fn visit_any(&mut self, value: log::kv::Value<'_>) -> Result<(), log::kv::Error> { + write!(self.0, "{value}").map_err(Into::into) + } + + fn visit_u64(&mut self, value: u64) -> Result<(), log::kv::Error> { + write!(self.0, "{value}").map_err(Into::into) + } + + fn visit_i64(&mut self, value: i64) -> Result<(), log::kv::Error> { + write!(self.0, "{value}").map_err(Into::into) + } + + fn visit_u128(&mut self, value: u128) -> Result<(), log::kv::Error> { + write!(self.0, "{value}").map_err(Into::into) + } + + fn visit_i128(&mut self, value: i128) -> Result<(), log::kv::Error> { + write!(self.0, "{value}").map_err(Into::into) + } + + fn visit_f64(&mut self, value: f64) -> Result<(), log::kv::Error> { + write!(self.0, "{value}").map_err(Into::into) + } + + fn visit_bool(&mut self, value: bool) -> Result<(), log::kv::Error> { + write!(self.0, "{value}").map_err(Into::into) + } + + fn visit_str(&mut self, value: &str) -> Result<(), log::kv::Error> { + write!(self.0, "{value}").map_err(Into::into) + } + + fn visit_borrowed_str(&mut self, value: &str) -> Result<(), log::kv::Error> { + write!(self.0, "{value}").map_err(Into::into) + } + + fn visit_char(&mut self, value: char) -> Result<(), log::kv::Error> { + write!(self.0, "{value}").map_err(Into::into) + } + + fn visit_error( + &mut self, + err: &(dyn std::error::Error + 'static), + ) -> Result<(), log::kv::Error> { + write!(self.0, "{err}").map_err(Into::into) + } + + fn visit_borrowed_error( + &mut self, + err: &(dyn std::error::Error + 'static), + ) -> Result<(), log::kv::Error> { + write!(self.0, "{err}").map_err(Into::into) + } +} - Ok(( - Options { - timer, - file_appender, - }, - Guard(guard), - )) +struct ClientLogger { + conn: Connection, + offset: UtcOffset, } -pub fn init( - Options { - timer, - file_appender, - }: Options, - client: Client, -) { - tracing_subscriber::registry() - .with( - tracing_subscriber::fmt::layer() - .with_ansi(false) - .with_timer(timer.clone()) - .with_writer(ClientLogWriter::new(client)), - ) - .with( - tracing_subscriber::fmt::layer() - .with_timer(timer) - .with_writer(file_appender), - ) - .with(Targets::new().with_default(Level::WARN).with_targets([ - (env!("CARGO_CRATE_NAME"), Level::TRACE), - ("stef_compiler", Level::TRACE), - ("stef_parser", Level::TRACE), - ("tower_lsp", Level::DEBUG), - ])) - .init(); +impl ClientLogger { + fn new(conn: Connection, offset: UtcOffset) -> Self { + Self { conn, offset } + } } -pub struct Options { - timer: OffsetTime<&'static [FormatItem<'static>]>, - file_appender: NonBlocking, +impl log::Log for ClientLogger { + fn enabled(&self, _metadata: &Metadata<'_>) -> bool { + true + } + + fn log(&self, record: &Record<'_>) { + let message = { + let mut buf = Vec::new(); + if let Err(e) = write_message(&mut buf, record, self.offset) { + eprintln!("failed formatting log message: {e:?}"); + return; + } + String::from_utf8_lossy(&buf).into_owned() + }; + + let params = match serde_json::to_value(LogMessageParams { + typ: MessageType::LOG, + message, + }) { + Ok(params) => params, + Err(e) => { + eprintln!("failed serializing log message params: {e:?}"); + return; + } + }; + + if let Err(e) = self.conn.sender.send(Message::Notification(Notification { + method: LogMessage::METHOD.to_owned(), + params, + })) { + eprintln!("failed sending log message to client: {e:?}"); + } + } + + fn flush(&self) {} } -pub struct Guard(tracing_appender::non_blocking::WorkerGuard); +struct FileLogger { + file: Mutex, + offset: UtcOffset, +} -struct ClientLogWriter { - client: Client, +impl FileLogger { + fn new(file: PathBuf, offset: UtcOffset) -> Result { + Ok(Self { + file: File::options().create(true).append(true).open(file)?.into(), + offset, + }) + } } -impl ClientLogWriter { - fn new(client: Client) -> Self { - Self { client } +impl log::Log for FileLogger { + fn enabled(&self, _metadata: &Metadata<'_>) -> bool { + true + } + + fn log(&self, record: &Record<'_>) { + if let Err(e) = write_message(&mut *self.file.lock(), record, self.offset) { + eprintln!("failed writing log message to file: {e:?}"); + } + } + + fn flush(&self) { + if let Err(e) = self.file.lock().flush() { + eprintln!("failed flushing log file: {e:?}"); + } } } -impl Write for ClientLogWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - let client = self.client.clone(); - let message = String::from_utf8_lossy(buf).trim().to_owned(); +struct StderrLogger { + offset: UtcOffset, +} + +impl StderrLogger { + fn new(offset: UtcOffset) -> Self { + Self { offset } + } +} - tokio::spawn(async move { client.log_message(MessageType::LOG, message).await }); +impl log::Log for StderrLogger { + fn enabled(&self, _metadata: &Metadata<'_>) -> bool { + true + } - Ok(buf.len()) + fn log(&self, record: &Record<'_>) { + if let Err(e) = write_message(&mut std::io::stderr(), record, self.offset) { + eprintln!("failed formatting log message: {e:?}"); + } } - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) + fn flush(&self) { + std::io::stderr().flush().ok(); } } -impl MakeWriter<'_> for ClientLogWriter { - type Writer = Self; +struct CombinedLogger { + client: Option, + file: FileLogger, + stderr: StderrLogger, +} + +impl log::Log for CombinedLogger { + fn enabled(&self, metadata: &Metadata<'_>) -> bool { + metadata.level() <= Level::Warn + || (metadata.target().starts_with(env!("CARGO_CRATE_NAME")) + && metadata.level() <= Level::Trace) + || (metadata.target().starts_with("stef_compiler") && metadata.level() <= Level::Trace) + || (metadata.target().starts_with("stef_parser") && metadata.level() <= Level::Trace) + || (metadata.target().starts_with("lsp_server::msg") + && metadata.level() <= Level::Debug) + } + + fn log(&self, record: &Record<'_>) { + if self.enabled(record.metadata()) { + if let Some(client) = &self.client { + client.log(record); + } + self.file.log(record); + self.stderr.log(record); + } + } - fn make_writer(&self) -> Self::Writer { - Self { - client: self.client.clone(), + fn flush(&self) { + if let Some(client) = &self.client { + client.flush(); } + self.file.flush(); + self.stderr.flush(); } } diff --git a/crates/stef-lsp/src/main.rs b/crates/stef-lsp/src/main.rs index f94aece..b02ba4d 100644 --- a/crates/stef-lsp/src/main.rs +++ b/crates/stef-lsp/src/main.rs @@ -1,27 +1,30 @@ #![warn(clippy::expect_used, clippy::unwrap_used)] #![allow(missing_docs)] -use std::collections::HashMap; - -use anyhow::{ensure, Context, Result}; -use ouroboros::self_referencing; -use stef_parser::Schema; -use tokio::sync::{Mutex, RwLock}; -use tower_lsp::{ - async_trait, - jsonrpc::Result as LspResult, - lsp_types::{ - ConfigurationItem, Diagnostic, DidChangeConfigurationParams, DidChangeTextDocumentParams, - DidCloseTextDocumentParams, DidOpenTextDocumentParams, InitializeParams, InitializeResult, - InitializedParams, Registration, SemanticTokenModifier, SemanticTokenType, - SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, - SemanticTokensParams, SemanticTokensResult, SemanticTokensServerCapabilities, - ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, Url, - WorkDoneProgressOptions, +use std::{collections::HashMap, net::Ipv4Addr, time::Duration}; + +use anyhow::{bail, ensure, Context, Result}; +use log::{as_debug, as_display, debug, error, info}; +use lsp_server::{Connection, Message, Notification, Request, RequestId, Response}; +use lsp_types::{ + notification::{ + DidChangeConfiguration, DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, + Initialized, Notification as LspNotification, PublishDiagnostics, }, - Client, LanguageServer, LspService, Server, + request::{ + RegisterCapability, Request as LspRequest, SemanticTokensFullRequest, Shutdown, + WorkspaceConfiguration, + }, + ConfigurationItem, ConfigurationParams, Diagnostic, DidChangeConfigurationParams, + DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, + InitializeParams, InitializeResult, InitializedParams, PublishDiagnosticsParams, Registration, + RegistrationParams, SemanticTokenModifier, SemanticTokenType, SemanticTokens, + SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, SemanticTokensParams, + SemanticTokensResult, SemanticTokensServerCapabilities, ServerCapabilities, ServerInfo, + TextDocumentSyncCapability, TextDocumentSyncKind, Url, WorkDoneProgressOptions, }; -use tracing::{debug, error}; +use ouroboros::self_referencing; +use stef_parser::Schema; use self::cli::Cli; @@ -31,11 +34,11 @@ mod config; mod logging; mod utf16; -#[derive(Debug)] struct Backend { - client: Client, - files: Mutex>, - settings: RwLock, + conn: Connection, + files: HashMap, + settings: config::Global, + next_id: i32, } #[self_referencing] @@ -48,14 +51,76 @@ struct File { } impl Backend { - async fn reload_settings(&self) -> Result<()> { + fn send_notification(&self, params: T::Params) -> Result<()> + where + T: LspNotification, + { + self.conn + .sender + .send_timeout( + Notification::new(T::METHOD.to_owned(), params).into(), + Duration::from_secs(2), + ) + .map_err(Into::into) + } + + fn send_request(&mut self, params: T::Params) -> Result + where + T: LspRequest, + { + let next_id = self.next_id.wrapping_add(1); + self.next_id = next_id; + + self.conn.sender.send_timeout( + Request::new(next_id.into(), T::METHOD.to_owned(), params).into(), + Duration::from_secs(2), + )?; + + match self.conn.receiver.recv_timeout(Duration::from_secs(2))? { + Message::Response(Response { + id, + result: Some(result), + error: None, + }) => { + ensure!(id == next_id.into(), "invalid ID"); + serde_json::from_value(result).map_err(Into::into) + } + Message::Response(Response { + id, + result: None, + error: Some(error), + }) => bail!("request {id} failed: {error:?}"), + _ => bail!("invalid message type"), + } + } + + fn publish_diagnostics( + &self, + uri: Url, + diagnostics: Vec, + version: Option, + ) -> Result<()> { + self.send_notification::(PublishDiagnosticsParams { + uri, + diagnostics, + version, + }) + } + + fn configuration(&mut self, items: Vec) -> Result> { + self.send_request::(ConfigurationParams { items }) + } + + fn register_capability(&mut self, registrations: Vec) -> Result<()> { + self.send_request::(RegistrationParams { registrations }) + } + + fn reload_settings(&mut self) -> Result<()> { let mut settings = self - .client .configuration(vec![ConfigurationItem { scope_uri: None, section: Some("stef".to_owned()), }]) - .await .context("failed getting configuration from client")?; ensure!( @@ -68,15 +133,28 @@ impl Backend { debug!("configuration loaded: {settings:#?}"); - *self.settings.write().await = settings; + self.settings = settings; Ok(()) } } -#[async_trait] +trait LanguageServer { + fn initialize(&mut self, params: InitializeParams) -> Result; + fn initialized(&mut self, params: InitializedParams); + fn shutdown(&mut self) -> Result<()>; + fn did_open(&mut self, params: DidOpenTextDocumentParams); + fn did_change(&mut self, params: DidChangeTextDocumentParams); + fn did_close(&mut self, params: DidCloseTextDocumentParams); + fn semantic_tokens_full( + &mut self, + params: SemanticTokensParams, + ) -> Result>; + fn did_change_configuration(&mut self, params: DidChangeConfigurationParams); +} + impl LanguageServer for Backend { - async fn initialize(&self, _params: InitializeParams) -> LspResult { + fn initialize(&mut self, _params: InitializeParams) -> Result { Ok(InitializeResult { server_info: Some(ServerInfo { name: env!("CARGO_CRATE_NAME").to_owned(), @@ -142,32 +220,29 @@ impl LanguageServer for Backend { }) } - async fn initialized(&self, _params: InitializedParams) { - if let Err(e) = self.reload_settings().await { - error!(error = ?e, "failed loading initial settings"); + fn initialized(&mut self, _params: InitializedParams) { + if let Err(e) = self.reload_settings() { + error!(error = as_debug!(e); "failed loading initial settings"); } - if let Err(e) = self - .client - .register_capability(vec![Registration { - id: "1".to_owned(), - method: "workspace/didChangeConfiguration".to_owned(), - register_options: None, - }]) - .await - { - error!(error = ?e, "failed registering for configuration changes"); + if let Err(e) = self.register_capability(vec![Registration { + id: "1".to_owned(), + method: "workspace/didChangeConfiguration".to_owned(), + register_options: None, + }]) { + error!(error = as_debug!(e); "failed registering for configuration changes"); } debug!("initialized"); } - async fn shutdown(&self) -> LspResult<()> { + fn shutdown(&mut self) -> Result<()> { + debug!("got shutdown request"); Ok(()) } - async fn did_open(&self, params: DidOpenTextDocumentParams) { - debug!(uri = %params.text_document.uri, "schema opened"); + fn did_open(&mut self, params: DidOpenTextDocumentParams) { + debug!(uri = as_display!(params.text_document.uri); "schema opened"); let file = FileBuilder { content: params.text_document.text, @@ -175,26 +250,23 @@ impl LanguageServer for Backend { } .build(); - self.client - .publish_diagnostics( - params.text_document.uri.clone(), - file.borrow_schema() - .as_ref() - .err() - .map(|diag| vec![diag.clone()]) - .unwrap_or_default(), - None, - ) - .await; + if let Err(e) = self.publish_diagnostics( + params.text_document.uri.clone(), + file.borrow_schema() + .as_ref() + .err() + .map(|diag| vec![diag.clone()]) + .unwrap_or_default(), + None, + ) { + error!(error = as_debug!(e); "failed publishing diagnostics"); + } - self.files - .lock() - .await - .insert(params.text_document.uri, file); + self.files.insert(params.text_document.uri, file); } - async fn did_change(&self, mut params: DidChangeTextDocumentParams) { - debug!(uri = %params.text_document.uri, "schema changed"); + fn did_change(&mut self, mut params: DidChangeTextDocumentParams) { + debug!(uri = as_display!(params.text_document.uri); "schema changed"); let file = FileBuilder { content: params.content_changes.remove(0).text, @@ -202,81 +274,157 @@ impl LanguageServer for Backend { } .build(); - self.client - .publish_diagnostics( - params.text_document.uri.clone(), - file.borrow_schema() - .as_ref() - .err() - .map(|diag| vec![diag.clone()]) - .unwrap_or_default(), - None, - ) - .await; + if let Err(e) = self.publish_diagnostics( + params.text_document.uri.clone(), + file.borrow_schema() + .as_ref() + .err() + .map(|diag| vec![diag.clone()]) + .unwrap_or_default(), + None, + ) { + error!(error = as_debug!(e); "failed publishing diagnostics"); + } - self.files - .lock() - .await - .insert(params.text_document.uri, file); + self.files.insert(params.text_document.uri, file); } - async fn did_close(&self, params: DidCloseTextDocumentParams) { - debug!(uri = %params.text_document.uri, "schema closed"); - self.files.lock().await.remove(¶ms.text_document.uri); + fn did_close(&mut self, params: DidCloseTextDocumentParams) { + debug!(uri = as_display!(params.text_document.uri); "schema closed"); + self.files.remove(¶ms.text_document.uri); } - async fn semantic_tokens_full( - &self, + fn semantic_tokens_full( + &mut self, params: SemanticTokensParams, - ) -> LspResult> { - debug!(uri = %params.text_document.uri, "requested semantic tokens"); + ) -> Result> { + debug!(uri = as_display!(params.text_document.uri); "requested semantic tokens"); Ok(None) } - async fn did_change_configuration(&self, _: DidChangeConfigurationParams) { + fn did_change_configuration(&mut self, _params: DidChangeConfigurationParams) { debug!("configuration changed"); - if let Err(e) = self.reload_settings().await { - error!(error = ?e, "failed loading changed settings"); + if let Err(e) = self.reload_settings() { + error!(error = as_debug!(e); "failed loading changed settings"); } } } fn main() -> Result<()> { let cli = Cli::parse(); - let (log_options, _guard) = logging::prepare()?; + logging::init(None)?; + + let (connection, _io_threads) = if cli.stdio { + Connection::stdio() + } else if let Some(file) = cli.pipe { + unimplemented!("open connection on pipe/socket {file:?}"); + } else if let Some(port) = cli.socket { + Connection::connect((Ipv4Addr::LOCALHOST, port))? + } else { + bail!("no connection method provided") + }; + + let mut server = Backend { + conn: Connection { + sender: connection.sender.clone(), + receiver: connection.receiver.clone(), + }, + files: HashMap::default(), + settings: config::Global::default(), + next_id: 0, + }; + + let (id, params) = connection.initialize_start()?; + let init_params = serde_json::from_value::(params)?; + let init_result = server.initialize(init_params)?; + connection.initialize_finish(id, serde_json::to_value(init_result)?)?; + + info!("server initialized"); + + if let Err(e) = main_loop(&connection, server) { + error!(error = as_debug!(e); "error in main loop"); + return Err(e); + } - let (service, socket) = LspService::new(|client| { - logging::init(log_options, client.clone()); + // TODO: investigate why this hangs + // io_threads.join()?; - Backend { - client, - files: Mutex::default(), - settings: RwLock::default(), - } - }); - - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build()? - .block_on(async move { - if cli.stdio { - let (stdin, stdout) = (tokio::io::stdin(), tokio::io::stdout()); - Server::new(stdin, stdout, socket).serve(service).await; - } else if let Some(file) = cli.pipe { - let file = tokio::fs::File::options() - .read(true) - .write(true) - .open(file) - .await - .context("failed to open provided pipe/socket")?; - - let (read, write) = tokio::io::split(file); - Server::new(read, write, socket).serve(service).await; - } else if let Some(port) = cli.socket { - unimplemented!("open TCP connection on port {port}"); + info!("goodbye!"); + + Ok(()) +} + +fn main_loop(conn: &Connection, mut server: impl LanguageServer) -> Result<()> { + for msg in &conn.receiver { + match msg { + lsp_server::Message::Request(req) => { + if conn.handle_shutdown(&req)? { + info!("shutting down"); + return Ok(()); + } + + match req.method.as_str() { + Shutdown::METHOD => { + server.shutdown()?; + } + SemanticTokensFullRequest::METHOD => { + let (id, params) = cast_req::(req)?; + let result = server.semantic_tokens_full(params)?; + + conn.sender.send( + Response::new_ok( + id, + result.unwrap_or(SemanticTokensResult::Tokens( + SemanticTokens::default(), + )), + ) + .into(), + )?; + } + + _ => debug!("got request: {req:?}"), + } + } + lsp_server::Message::Response(resp) => { + debug!("got response: {resp:?}"); } + lsp_server::Message::Notification(notif) => match notif.method.as_str() { + Initialized::METHOD => { + server.initialized(cast_notify::(notif)?); + } + DidOpenTextDocument::METHOD => { + server.did_open(cast_notify::(notif)?); + } + DidChangeTextDocument::METHOD => { + server.did_change(cast_notify::(notif)?); + } + DidCloseTextDocument::METHOD => { + server.did_close(cast_notify::(notif)?); + } + DidChangeConfiguration::METHOD => { + server.did_change_configuration(cast_notify::(notif)?); + } + _ => debug!("got unknown notification: {notif:?}"), + }, + } + } - anyhow::Ok(()) - }) + Ok(()) +} + +fn cast_req(req: Request) -> Result<(RequestId, R::Params)> +where + R: LspRequest, + R::Params: serde::de::DeserializeOwned, +{ + req.extract(R::METHOD).map_err(Into::into) +} + +fn cast_notify(notif: Notification) -> Result +where + R: LspNotification, + R::Params: serde::de::DeserializeOwned, +{ + notif.extract(R::METHOD).map_err(Into::into) } diff --git a/crates/stef-parser/Cargo.toml b/crates/stef-parser/Cargo.toml index 524ca52..c049f7f 100644 --- a/crates/stef-parser/Cargo.toml +++ b/crates/stef-parser/Cargo.toml @@ -16,7 +16,7 @@ rustc-args = ["--cfg", "docsrs"] miette.workspace = true owo-colors.workspace = true stef-derive = { path = "../stef-derive" } -winnow = "0.5.26" +winnow = "0.5.28" [dev-dependencies] indoc.workspace = true