From 4923a02e17e00bc3b06e31ba0dc5a5758ae5534a Mon Sep 17 00:00:00 2001 From: Christian Pieczewski Date: Mon, 15 Jun 2026 10:19:57 +0200 Subject: [PATCH 1/5] feat: add java-task-helper for build tool execution - Implement a new `java-task-helper` binary to handle build tool commands - Add support for Maven and Gradle build tool detection and execution - Integrate `java-task-helper` into the Zed Java extension tasks - Add automated download logic for the task helper binary - Update `justfile` with new recipes for building and installing the helper - Update `Cargo.lock` and `Cargo.toml` with new dependencies --- Cargo.lock | 300 +++++++++--------- Cargo.toml | 3 +- README.md | 8 +- justfile | 33 +- languages/java/tasks.json | 45 +-- src/download.rs | 147 +++++++++ src/java.rs | 425 ++++++++++++++++++++++++-- src/task.rs | 21 ++ task_helper/Cargo.toml | 14 + task_helper/src/build_tool/gradle.rs | 154 ++++++++++ task_helper/src/build_tool/maven.rs | 205 +++++++++++++ task_helper/src/build_tool/mod.rs | 175 +++++++++++ task_helper/src/build_tool/vanilla.rs | 86 ++++++ task_helper/src/command.rs | 34 +++ task_helper/src/main.rs | 107 +++++++ tests/task_verification_test.rs | 31 ++ 16 files changed, 1573 insertions(+), 215 deletions(-) create mode 100644 src/download.rs create mode 100644 src/task.rs create mode 100644 task_helper/Cargo.toml create mode 100644 task_helper/src/build_tool/gradle.rs create mode 100644 task_helper/src/build_tool/maven.rs create mode 100644 task_helper/src/build_tool/mod.rs create mode 100644 task_helper/src/build_tool/vanilla.rs create mode 100644 task_helper/src/command.rs create mode 100644 task_helper/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index cf77ed6..38a56e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,18 +10,18 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "auditable-serde" @@ -37,9 +37,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.9.3" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "console" @@ -97,9 +97,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -145,7 +145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -162,9 +162,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -187,9 +187,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -202,9 +202,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -212,15 +212,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -229,15 +229,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -246,21 +246,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -270,7 +270,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -306,6 +305,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" @@ -320,12 +325,13 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -333,9 +339,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -346,11 +352,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -361,42 +366,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -406,9 +407,9 @@ dependencies = [ [[package]] name = "id-arena" -version = "2.2.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "idna" @@ -423,9 +424,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -433,13 +434,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", "serde", + "serde_core", ] [[package]] @@ -457,9 +459,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "java-lsp-proxy" @@ -471,6 +473,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "java-task-helper" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tempfile", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -479,33 +490,33 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miniz_oxide" @@ -514,13 +525,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "percent-encoding" @@ -530,21 +542,15 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -561,18 +567,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -585,9 +591,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -597,9 +603,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "722166aa0d7438abbaa4d5cc2c649dac844e8c56d82fb3d33e9c34b5cd268fc6" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -608,36 +614,31 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.7" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3160422bbd54dd5ecfdca71e5fd59b7b8fe2b1697ab2baf64f6d05dcc66d298" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "semver" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", + "serde_core", ] [[package]] @@ -672,16 +673,16 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "indexmap", "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -701,6 +702,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "similar" version = "2.7.0" @@ -709,9 +716,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -730,9 +737,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "streaming-iterator" @@ -742,9 +749,9 @@ checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -764,22 +771,22 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.25.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -793,9 +800,9 @@ checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" [[package]] name = "tree-sitter" -version = "0.26.8" +version = "0.26.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "887bd495d0582c5e3e0d8ece2233666169fa56a9644d172fc22ad179ab2d0538" +checksum = "4dab76d0b724ba557954125188cf0633a1ca43199ced82d95c7b9c32cc3de1f3" dependencies = [ "cc", "regex", @@ -823,15 +830,15 @@ checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-xid" @@ -841,9 +848,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -939,7 +946,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" dependencies = [ "bitflags", - "hashbrown", + "hashbrown 0.15.5", "indexmap", "semver", ] @@ -951,7 +958,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", - "hashbrown", + "hashbrown 0.15.5", "indexmap", "semver", ] @@ -1240,17 +1247,16 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -1258,9 +1264,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -1297,18 +1303,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -1318,9 +1324,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -1329,9 +1335,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -1340,11 +1346,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index d247174..c419bd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [workspace] members = [ ".", - "proxy" + "proxy", + "task_helper" ] [package] diff --git a/README.md b/README.md index eaaf3d7..8c600b2 100644 --- a/README.md +++ b/README.md @@ -606,13 +606,15 @@ The project includes a `justfile` with common development tasks: | Recipe | Description | |--------|-------------| | `just proxy-build` | Build the proxy binary in debug mode | -| `just proxy-release` | Build the proxy binary in release mode | | `just proxy-install` | Build release proxy and copy it to the extension workdir | +| `just task-build` | Build the task helper binary in debug mode | +| `just task-install` | Build release task helper and copy it to the extension workdir | +| `just task-test` | Run task helper tests | | `just ext-build` | Build the WASM extension in release mode | | `just fmt` | Format all code (Rust + tree-sitter queries) | -| `just clippy` | Run clippy on both crates | +| `just clippy` | Run clippy on all crates | | `just lint` | Format and lint all code | -| `just all` | Lint, build extension, and install proxy | +| `just all` | Lint, build extension, and install proxy & task helper | ### Updating the `java-lsp-proxy` Binary diff --git a/justfile b/justfile index 806a8f3..910bd75 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,7 @@ native_target := `rustc -vV | grep host | awk '{print $2}'` ext_dir := if os() == "macos" { env("HOME") / "Library/Application Support/Zed/extensions/work/java" } else if os() == "linux" { env("HOME") / ".local/share/zed/extensions/work/java" } else { env("LOCALAPPDATA") / "Zed/extensions/work/java" } proxy_bin := ext_dir / "proxy-bin" / "java-lsp-proxy" +tasks_bin := ext_dir / "proxy-bin" / "java-task-helper" # Build proxy in debug mode proxy-build: @@ -14,7 +15,33 @@ proxy-release: proxy-install: proxy-release mkdir -p "{{ ext_dir }}/proxy-bin" cp "target/{{ native_target }}/release/java-lsp-proxy" "{{ proxy_bin }}" - @echo "Installed to {{ proxy_bin }}" + @echo "Installed to {{ ext_dir }}" + +# --- Task helper recipes --- + +# Build task helper in debug mode +task-build: + cd task_helper && cargo build --target {{ native_target }} + +# Build task helper in release mode +task-release: + cd task_helper && cargo build --release --target {{ native_target }} + +# Build task helper release and install to extension workdir for testing +task-install: task-release + mkdir -p "{{ ext_dir }}/proxy-bin" + cp "target/{{ native_target }}/release/java-task-helper" "{{ tasks_bin }}" + @echo "Installed to {{ ext_dir }}" + +# Run task helper tests +task-test: + cd task_helper && cargo test + +# Clean task helper build +task-clean: + cd task_helper && cargo clean + +# --- Core recipes --- # Build WASM extension in release mode ext-build: @@ -33,5 +60,5 @@ clippy: # Format and lint all code lint: fmt clippy -# Build everything: lint, extension, proxy install -all: lint ext-build proxy-install +# Build everything: lint, extension, and install proxy & task helper +all: lint ext-build proxy-install task-install diff --git a/languages/java/tasks.json b/languages/java/tasks.json index 41c8cae..71139ff 100644 --- a/languages/java/tasks.json +++ b/languages/java/tasks.json @@ -1,7 +1,7 @@ [ { "label": "Run $ZED_CUSTOM_java_class_name", - "command": "pkg=\"${ZED_CUSTOM_java_package_name:-}\"; cls=\"$ZED_CUSTOM_java_class_name\"; if [ -n \"$pkg\" ]; then c=\"$pkg.$cls\"; else c=\"$cls\"; fi; f=\"$ZED_FILE\"; p=\"$PWD\"; d=$(dirname \"${f#$p/}\"); if [ -f pom.xml ]; then m=\".\"; md=\"$d\"; while [ \"$md\" != \".\" ] && [ \"$md\" != \"/\" ]; do if [ -f \"$md/pom.xml\" ]; then m=\"$md\"; break; fi; md=$(dirname \"$md\"); done; [ -f ./mvnw ] && CMD=\"./mvnw\" || CMD=\"mvn\"; case \"$f\" in *\"/src/test/\"*) COMPILE_GOAL=\"test-compile\"; CLASSPATH_SCOPE=\"test\";; *) COMPILE_GOAL=\"compile\"; CLASSPATH_SCOPE=\"runtime\";; esac; if [ \"$m\" = \".\" ]; then $CMD clean $COMPILE_GOAL exec:java -Dexec.mainClass=\"$c\" -Dexec.classpathScope=$CLASSPATH_SCOPE; else $CMD clean $COMPILE_GOAL -pl \"$m\" -am && $CMD exec:java -pl \"$m\" -Dexec.mainClass=\"$c\" -Dexec.classpathScope=$CLASSPATH_SCOPE; fi; elif [ -f build.gradle ] || [ -f build.gradle.kts ] || [ -f settings.gradle ] || [ -f settings.gradle.kts ]; then m=\".\"; md=\"$d\"; while [ \"$md\" != \".\" ] && [ \"$md\" != \"/\" ]; do if [ -f \"$md/build.gradle\" ] || [ -f \"$md/build.gradle.kts\" ]; then m=\"$md\"; break; fi; md=$(dirname \"$md\"); done; if [ \"$m\" = \".\" ]; then mp=\"\"; else mp=\":$(echo \"$m\" | tr '/' ':')\"; fi; [ -f ./gradlew ] && CMD=\"./gradlew\" || CMD=\"gradle\"; $CMD ${mp}:run -PmainClass=\"$c\"; else find . -name '*.java' -not -path './bin/*' -not -path './target/*' -not -path './build/*' -print0 | xargs -0 javac -d bin && java -cp bin \"$c\"; fi;", + "command": "EXT=\"${XDG_DATA_HOME:-$HOME/.local/share}/zed/extensions/work/java\"; [ \"$(uname 2>/dev/null)\" = \"Darwin\" ] && EXT=\"$HOME/Library/Application Support/Zed/extensions/work/java\"; [ -n \"$LOCALAPPDATA\" ] && EXT=\"$(cygpath -u \"$LOCALAPPDATA\" 2>/dev/null || echo \"$LOCALAPPDATA\")/zed/extensions/work/java\"; \"$EXT/proxy-bin/java-task-helper\" run-class \"$ZED_FILE\" \"$ZED_CUSTOM_java_package_name\" \"$ZED_CUSTOM_java_class_name\" \"${ZED_CUSTOM_java_outer_class_name:-}\"", "use_new_terminal": false, "reveal": "always", "tags": ["java-main"], @@ -13,8 +13,8 @@ } }, { - "label": "$ZED_CUSTOM_java_class_name.${ZED_CUSTOM_java_outer_class_name:}.$ZED_CUSTOM_java_method_name", - "command": "package=\"${ZED_CUSTOM_java_package_name:-}\"; outer=\"${ZED_CUSTOM_java_outer_class_name:-}\"; inner=\"$ZED_CUSTOM_java_class_name\"; method=\"$ZED_CUSTOM_java_method_name\"; sep=\"$\"; if [ -n \"$outer\" ]; then c=\"$outer$sep$inner\"; else c=\"$inner\"; fi; if [ -n \"$package\" ]; then fqc=\"$package.$c\"; else fqc=\"$c\"; fi; f=\"$ZED_FILE\"; p=\"$PWD\"; d=$(dirname \"${f#$p/}\"); if [ -f pom.xml ]; then m=\".\"; md=\"$d\"; while [ \"$md\" != \".\" ] && [ \"$md\" != \"/\" ]; do if [ -f \"$md/pom.xml\" ]; then m=\"$md\"; break; fi; md=$(dirname \"$md\"); done; [ -f ./mvnw ] && CMD=\"./mvnw\" || CMD=\"mvn\"; if [ \"$m\" = \".\" ]; then $CMD clean test -Dtest=\"$fqc#$method\"; else $CMD clean test-compile -pl \"$m\" -am && $CMD test -pl \"$m\" -Dtest=\"$fqc#$method\"; fi; elif [ -f build.gradle ] || [ -f build.gradle.kts ] || [ -f settings.gradle ] || [ -f settings.gradle.kts ]; then m=\".\"; md=\"$d\"; while [ \"$md\" != \".\" ] && [ \"$md\" != \"/\" ]; do if [ -f \"$md/build.gradle\" ] || [ -f \"$md/build.gradle.kts\" ]; then m=\"$md\"; break; fi; md=$(dirname \"$md\"); done; if [ \"$m\" = \".\" ]; then mp=\"\"; else mp=\":$(echo \"$m\" | tr '/' ':')\"; fi; [ -f ./gradlew ] && CMD=\"./gradlew\" || CMD=\"gradle\"; $CMD ${mp}:test --tests \"$fqc.$method\"; else >&2 echo 'No build tool found'; exit 1; fi;", + "label": "Run $ZED_CUSTOM_java_class_name.${ZED_CUSTOM_java_outer_class_name:}.$ZED_CUSTOM_java_method_name", + "command": "EXT=\"${XDG_DATA_HOME:-$HOME/.local/share}/zed/extensions/work/java\"; [ \"$(uname 2>/dev/null)\" = \"Darwin\" ] && EXT=\"$HOME/Library/Application Support/Zed/extensions/work/java\"; [ -n \"$LOCALAPPDATA\" ] && EXT=\"$(cygpath -u \"$LOCALAPPDATA\" 2>/dev/null || echo \"$LOCALAPPDATA\")/zed/extensions/work/java\"; \"$EXT/proxy-bin/java-task-helper\" run-test-method \"$ZED_FILE\" \"$ZED_CUSTOM_java_package_name\" \"$ZED_CUSTOM_java_class_name\" \"${ZED_CUSTOM_java_outer_class_name:-}\" \"$ZED_CUSTOM_java_method_name\"", "use_new_terminal": false, "reveal": "always", "tags": ["java-test-method", "java-test-method-nested"], @@ -24,44 +24,5 @@ "args": ["-c"] } } - }, - { - "label": "Test class $ZED_CUSTOM_java_class_name", - "command": "package=\"${ZED_CUSTOM_java_package_name:-}\"; outer=\"${ZED_CUSTOM_java_outer_class_name:-}\"; inner=\"$ZED_CUSTOM_java_class_name\"; sep=\"$\"; if [ -n \"$outer\" ]; then c=\"$outer$sep$inner\"; else c=\"$inner\"; fi; if [ -n \"$package\" ]; then fqc=\"$package.$c\"; else fqc=\"$c\"; fi; f=\"$ZED_FILE\"; p=\"$PWD\"; d=$(dirname \"${f#$p/}\"); if [ -f pom.xml ]; then m=\".\"; md=\"$d\"; while [ \"$md\" != \".\" ] && [ \"$md\" != \"/\" ]; do if [ -f \"$md/pom.xml\" ]; then m=\"$md\"; break; fi; md=$(dirname \"$md\"); done; [ -f ./mvnw ] && CMD=\"./mvnw\" || CMD=\"mvn\"; if [ \"$m\" = \".\" ]; then $CMD clean test -Dtest=\"$fqc\"; else $CMD clean test-compile -pl \"$m\" -am && $CMD test -pl \"$m\" -Dtest=\"$fqc\"; fi; elif [ -f build.gradle ] || [ -f build.gradle.kts ] || [ -f settings.gradle ] || [ -f settings.gradle.kts ]; then m=\".\"; md=\"$d\"; while [ \"$md\" != \".\" ] && [ \"$md\" != \"/\" ]; do if [ -f \"$md/build.gradle\" ] || [ -f \"$md/build.gradle.kts\" ]; then m=\"$md\"; break; fi; md=$(dirname \"$md\"); done; if [ \"$m\" = \".\" ]; then mp=\"\"; else mp=\":$(echo \"$m\" | tr '/' ':')\"; fi; [ -f ./gradlew ] && CMD=\"./gradlew\" || CMD=\"gradle\"; $CMD ${mp}:test --tests \"$fqc\"; else >&2 echo 'No build tool found'; exit 1; fi;", - "use_new_terminal": false, - "reveal": "always", - "tags": ["java-test-class", "java-test-class-nested"], - "shell": { - "with_arguments": { - "program": "/bin/sh", - "args": ["-c"] - } - } - }, - { - "label": "Run tests", - "command": "f=\"$ZED_FILE\"; p=\"$PWD\"; d=$(dirname \"${f#$p/}\"); if [ -f pom.xml ]; then m=\".\"; md=\"$d\"; while [ \"$md\" != \".\" ] && [ \"$md\" != \"/\" ]; do if [ -f \"$md/pom.xml\" ]; then m=\"$md\"; break; fi; md=$(dirname \"$md\"); done; [ -f ./mvnw ] && CMD=\"./mvnw\" || CMD=\"mvn\"; if [ \"$m\" = \".\" ]; then $CMD clean test; else $CMD clean test-compile -pl \"$m\" -am && $CMD test -pl \"$m\"; fi; elif [ -f build.gradle ] || [ -f build.gradle.kts ] || [ -f settings.gradle ] || [ -f settings.gradle.kts ]; then m=\".\"; md=\"$d\"; while [ \"$md\" != \".\" ] && [ \"$md\" != \"/\" ]; do if [ -f \"$md/build.gradle\" ] || [ -f \"$md/build.gradle.kts\" ]; then m=\"$md\"; break; fi; md=$(dirname \"$md\"); done; if [ \"$m\" = \".\" ]; then mp=\"\"; else mp=\":$(echo \"$m\" | tr '/' ':')\"; fi; [ -f ./gradlew ] && CMD=\"./gradlew\" || CMD=\"gradle\"; $CMD ${mp}:test; else >&2 echo 'No build tool found'; exit 1; fi;", - "use_new_terminal": false, - "reveal": "always", - "tags": ["java-test-all"], - "shell": { - "with_arguments": { - "program": "/bin/sh", - "args": ["-c"] - } - } - }, - { - "label": "Clear JDTLS cache", - "command": "cache_dir=\"\"; if [ -n \"$XDG_CACHE_HOME\" ]; then cache_dir=\"$XDG_CACHE_HOME\"; elif [ \"$(uname)\" = \"Darwin\" ]; then cache_dir=\"$HOME/Library/Caches\"; else cache_dir=\"$HOME/.cache\"; fi; found=$(find \"$cache_dir\" -maxdepth 1 -type d -name 'jdtls-*' 2>/dev/null); if [ -n \"$found\" ]; then echo \"$found\" | xargs rm -rf && echo 'JDTLS cache cleared. Restart the language server'; else echo 'No JDTLS cache found'; fi", - "use_new_terminal": false, - "reveal": "always", - "tags": ["java-clear-cache"], - "shell": { - "with_arguments": { - "program": "/bin/sh", - "args": ["-c"] - } - } } ] diff --git a/src/download.rs b/src/download.rs new file mode 100644 index 0000000..25ea1e7 --- /dev/null +++ b/src/download.rs @@ -0,0 +1,147 @@ +use std::{fs::metadata, path::PathBuf}; + +use serde_json::Value; +use zed_extension_api::{ + self as zed, DownloadedFileType, GithubReleaseOptions, LanguageServerId, + LanguageServerInstallationStatus, Worktree, serde_json, + set_language_server_installation_status, +}; + +use crate::util::{mark_checked_once, should_use_local_or_download}; + +pub(crate) const PROXY_INSTALL_PATH: &str = "proxy-bin"; +pub(crate) const GITHUB_REPO: &str = "zed-extensions/java"; + +pub(crate) fn asset_name(binary: &str) -> zed::Result<(String, DownloadedFileType)> { + let (os, arch) = zed::current_platform(); + let (os_str, file_type) = match os { + zed::Os::Mac => ("darwin", DownloadedFileType::GzipTar), + zed::Os::Linux => ("linux", DownloadedFileType::GzipTar), + zed::Os::Windows => ("windows", DownloadedFileType::Zip), + }; + let arch_str = match arch { + zed::Architecture::Aarch64 => "aarch64", + zed::Architecture::X8664 => "x86_64", + _ => return Err("Unsupported architecture".into()), + }; + let ext = if matches!(file_type, DownloadedFileType::Zip) { + "zip" + } else { + "tar.gz" + }; + Ok((format!("{binary}-{os_str}-{arch_str}.{ext}"), file_type)) +} + +pub(crate) fn binary_exec(binary: &str) -> String { + let (os, _arch) = zed::current_platform(); + + match os { + zed::Os::Linux | zed::Os::Mac => binary.to_string(), + zed::Os::Windows => format!("{binary}.exe"), + } +} + +pub(crate) fn find_latest_local(binary: &str) -> Option { + let exec = binary_exec(binary); + let local_binary = PathBuf::from(PROXY_INSTALL_PATH).join(&exec); + if metadata(&local_binary).is_ok_and(|m| m.is_file()) { + return Some(local_binary); + } + + // Check versioned downloads (e.g. proxy-bin/v6.8.12/java-lsp-proxy) + std::fs::read_dir(PROXY_INSTALL_PATH) + .ok()? + .filter_map(Result::ok) + .map(|e| e.path().join(&exec)) + .filter(|p| metadata(p).is_ok_and(|m| m.is_file())) + .last() +} + +pub(crate) fn download_binary( + cached: &mut Option, + configuration: &Option, + language_server_id: &LanguageServerId, + worktree: &Worktree, + binary: &str, +) -> zed::Result { + // 1. Respect check_updates setting (Never/Once/Always) + match should_use_local_or_download(configuration, find_latest_local(binary), PROXY_INSTALL_PATH) + { + Ok(Some(path)) => { + let s = path.to_string_lossy().to_string(); + *cached = Some(s.clone()); + return Ok(s); + } + Ok(None) => { /* policy allows download, continue */ } + Err(_) => { + // Never/Once with no managed install — fall through to PATH as last resort + } + } + + // 2. Auto-download from GitHub releases + if let Ok((name, file_type)) = asset_name(binary) + && let Ok(release) = zed::latest_github_release( + GITHUB_REPO, + GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + ) + { + let bin_path = format!( + "{}/{}/{}", + PROXY_INSTALL_PATH, + release.version, + binary_exec(binary) + ); + + if metadata(&bin_path).is_ok() { + *cached = Some(bin_path.clone()); + return Ok(bin_path); + } + + if let Some(asset) = release.assets.iter().find(|a| a.name == name) { + let version_dir = format!("{PROXY_INSTALL_PATH}/{}", release.version); + + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + + if zed::download_file(&asset.download_url, &version_dir, file_type).is_ok() { + let _ = zed::make_file_executable(&bin_path); + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::None, + ); + // Do not remove other files if we are downloading one of multiple binaries + // but for now they are in the same version dir. + // let _ = remove_all_files_except(PROXY_INSTALL_PATH, &release.version); + let _ = mark_checked_once(PROXY_INSTALL_PATH, &release.version); + *cached = Some(bin_path.clone()); + return Ok(bin_path); + } + } + } + + // 3. Fallback: local install (covers "always" mode when download fails) + if let Some(path) = find_latest_local(binary) { + let s = path.to_string_lossy().to_string(); + *cached = Some(s.clone()); + return Ok(s); + } + + // 4. Fallback: binary on $PATH + if let Some(path) = worktree.which(binary_exec(binary).as_str()) { + return Ok(path); + } + + // 5. Stale cache fallback + if let Some(path) = cached.as_deref() + && metadata(path).is_ok() + { + return Ok(path.to_string()); + } + + Err(format!("'{binary}' not found")) +} diff --git a/src/java.rs b/src/java.rs index bfc9b39..007e82f 100644 --- a/src/java.rs +++ b/src/java.rs @@ -1,12 +1,14 @@ mod config; mod debugger; mod downloadable; +mod download; mod jdk; mod jdtls; mod jdtls_server; mod language_server; mod lsp; mod proxy; +mod task; mod util; use std::str::FromStr; @@ -26,7 +28,11 @@ use crate::{ const DEBUG_ADAPTER_NAME: &str = "Java"; struct Java { - jdtls_server: JdtlsServer, + cached_binary_path: Option, + cached_lombok_path: Option, + cached_proxy_path: Option, + cached_task_helper_path: Option, + integrations: Option<(LspWrapper, Debugger)>, } impl Extension for Java { @@ -74,31 +80,42 @@ impl Extension for Java { .workspace_configuration(language_server_id, worktree), _ => Ok(None), } - } - fn label_for_completion( - &self, - language_server_id: &LanguageServerId, - completion: Completion, - ) -> Option { - match language_server_id.as_ref() { - JdtlsServer::SERVER_ID => self - .jdtls_server - .label_for_completion(language_server_id, completion), - _ => None, + // Use cached path if exists + if let Some(path) = &self.cached_lombok_path + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) + { + return Ok(path.clone()); + } + + match try_to_fetch_and_install_latest_lombok(language_server_id, configuration) { + Ok(path) => { + self.cached_lombok_path = Some(path.clone()); + Ok(path) + } + Err(e) => { + if let Some(local_version) = find_latest_local_lombok() { + self.cached_lombok_path = Some(local_version.clone()); + Ok(local_version) + } else { + Err(e) + } + } } } +} - fn label_for_symbol( - &self, - language_server_id: &LanguageServerId, - symbol: Symbol, - ) -> Option { - match language_server_id.as_ref() { - JdtlsServer::SERVER_ID => self - .jdtls_server - .label_for_symbol(language_server_id, symbol), - _ => None, +impl Extension for Java { + fn new() -> Self + where + Self: Sized, + { + Self { + cached_binary_path: None, + cached_lombok_path: None, + cached_proxy_path: None, + cached_task_helper_path: None, + integrations: None, } } @@ -221,6 +238,370 @@ impl Extension for Java { } } } + + fn language_server_command( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> zed::Result { + let current_dir = + env::current_dir().map_err(|err| format!("Failed to get current directory: {err}"))?; + + let configuration = + self.language_server_workspace_configuration(language_server_id, worktree)?; + + let mut env = Vec::new(); + + if let Some(java_home) = get_java_home(&configuration, worktree) { + env.push(("JAVA_HOME".to_string(), java_home)); + } + + let proxy_path = proxy::binary_path( + &mut self.cached_proxy_path, + &configuration, + language_server_id, + worktree, + ) + .map_err(|err| format!("Failed to get proxy binary path: {err}"))?; + + let _ = task::task_helper_binary_path( + &mut self.cached_task_helper_path, + &configuration, + language_server_id, + worktree, + ); + + // proxy takes: workdir, bin, [args...] + let mut args = vec![ + path_to_string(current_dir.clone()) + .map_err(|err| format!("Failed to convert current directory to string: {err}"))?, + ]; + + // Add lombok as javaagent if settings.java.jdt.ls.lombokSupport.enabled is true + let lombok_jvm_arg = if is_lombok_enabled(&configuration) { + let lombok_jar_path = self + .lombok_jar_path(language_server_id, &configuration, worktree) + .map_err(|err| format!("Failed to get Lombok jar path: {err}"))?; + let canonical_lombok_jar_path = path_to_string(current_dir.join(lombok_jar_path)) + .map_err(|err| format!("Failed to convert Lombok jar path to string: {err}"))?; + + Some(format!("-javaagent:{canonical_lombok_jar_path}")) + } else { + None + }; + + self.init(worktree); + + // Check for user-configured JDTLS launcher first + if let Some(launcher) = get_jdtls_launcher(&configuration, worktree) { + args.push(launcher); + if let Some(lombok_jvm_arg) = lombok_jvm_arg { + args.push(format!("--jvm-arg={lombok_jvm_arg}")); + } + } else if let Some(launcher) = get_jdtls_launcher_from_path(worktree) { + // if the user has `jdtls(.bat)` on their PATH, we use that + args.push(launcher); + if let Some(lombok_jvm_arg) = lombok_jvm_arg { + args.push(format!("--jvm-arg={lombok_jvm_arg}")); + } + } else { + // otherwise we launch ourselves + args.extend( + build_jdtls_launch_args( + &self + .language_server_binary_path(language_server_id, &configuration) + .map_err(|err| format!("Failed to get JDTLS binary path: {err}"))?, + &configuration, + worktree, + lombok_jvm_arg.into_iter().collect(), + language_server_id, + ) + .map_err(|err| format!("Failed to build JDTLS launch arguments: {err}"))?, + ); + } + + // download debugger if not exists + if let Err(err) = + self.debugger()? + .get_or_download(language_server_id, &configuration, worktree) + { + println!("Failed to download debugger: {err}"); + }; + + self.lsp()? + .switch_workspace(worktree.root_path()) + .map_err(|err| format!("Failed to switch LSP workspace: {err}"))?; + + Ok(zed::Command { + command: proxy_path, + args, + env, + }) + } + + fn language_server_initialization_options( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> zed::Result> { + if self.integrations.is_some() { + self.lsp()? + .switch_workspace(worktree.root_path()) + .map_err(|err| { + format!("Failed to switch LSP workspace for initialization: {err}") + })?; + } + + let mut options = LspSettings::for_worktree(language_server_id.as_ref(), worktree) + .map(|lsp_settings| lsp_settings.initialization_options) + .map_err(|err| format!("Failed to get LSP settings for worktree: {err}"))? + .unwrap_or_else(|| json!({})); + + // Inject workspaceFolders default if not already set by the user + let options_obj = options + .as_object_mut() + .ok_or_else(|| "initialization_options is not a JSON object".to_string())?; + if !options_obj.contains_key("workspaceFolders") { + let uri = util::path_to_file_uri(&worktree.root_path()); + options_obj.insert("workspaceFolders".to_string(), json!([uri])); + } + + // Inject extendedClientCapabilities defaults if not already set by the user + let caps = options_obj + .entry("extendedClientCapabilities") + .or_insert_with(|| json!({})); + let caps_obj = caps + .as_object_mut() + .ok_or_else(|| "extendedClientCapabilities is not a JSON object".to_string())?; + caps_obj + .entry("classFileContentsSupport") + .or_insert(json!(true)); + caps_obj + .entry("resolveAdditionalTextEditsSupport") + .or_insert(json!(true)); + + if self.debugger().is_ok_and(|v| v.loaded()) { + return Ok(Some( + self.debugger()? + .inject_plugin_into_options(Some(options)) + .map_err(|err| { + format!("Failed to inject debugger plugin into options: {err}") + })?, + )); + } + + Ok(Some(options)) + } + + fn language_server_workspace_configuration( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> zed::Result> { + if let Ok(Some(settings)) = LspSettings::for_worktree(language_server_id.as_ref(), worktree) + .map(|lsp_settings| lsp_settings.settings) + { + Ok(Some(settings)) + } else { + self.language_server_initialization_options(language_server_id, worktree) + .map(|init_options| { + init_options.and_then(|init_options| init_options.get("settings").cloned()) + }) + } + } + + fn label_for_completion( + &self, + _language_server_id: &LanguageServerId, + completion: Completion, + ) -> Option { + // uncomment when debugging completions + // println!("Java completion: {completion:#?}"); + + completion.kind.and_then(|kind| match kind { + CompletionKind::Field | CompletionKind::Constant => { + let modifiers = match kind { + CompletionKind::Field => "", + CompletionKind::Constant => "static final ", + _ => return None, + }; + let property_type = completion.detail.as_ref().and_then(|detail| { + detail + .split_once(" : ") + .map(|(_, property_type)| format!("{property_type} ")) + })?; + let semicolon = ";"; + let code = format!("{modifiers}{property_type}{}{semicolon}", completion.label); + + Some(CodeLabel { + spans: vec![ + CodeLabelSpan::code_range( + modifiers.len() + property_type.len()..code.len() - semicolon.len(), + ), + CodeLabelSpan::literal(" : ", None), + CodeLabelSpan::code_range( + modifiers.len()..modifiers.len() + property_type.len(), + ), + ], + code, + filter_range: (0..completion.label.len()).into(), + }) + } + CompletionKind::Method => { + let detail = completion.detail?; + let (left, return_type) = detail + .split_once(" : ") + .map(|(left, return_type)| (left, format!("{return_type} "))) + .unwrap_or((&detail, "void".to_string())); + let parameters = left + .find('(') + .map(|parameters_start| &left[parameters_start..]); + let name_and_parameters = + format!("{}{}", completion.label, parameters.unwrap_or("()")); + let braces = " {}"; + let code = format!("{return_type}{name_and_parameters}{braces}"); + let mut spans = vec![CodeLabelSpan::code_range( + return_type.len()..code.len() - braces.len(), + )]; + + if parameters.is_some() { + spans.push(CodeLabelSpan::literal(" : ", None)); + spans.push(CodeLabelSpan::code_range(0..return_type.len())); + } else { + spans.push(CodeLabelSpan::literal(" - ", None)); + spans.push(CodeLabelSpan::literal(detail, None)); + } + + Some(CodeLabel { + spans, + code, + filter_range: (0..completion.label.len()).into(), + }) + } + CompletionKind::Class | CompletionKind::Interface | CompletionKind::Enum => { + let keyword = match kind { + CompletionKind::Class => "class ", + CompletionKind::Interface => "interface ", + CompletionKind::Enum => "enum ", + _ => return None, + }; + let braces = " {}"; + let code = format!("{keyword}{}{braces}", completion.label); + let namespace = completion.detail.and_then(|detail| { + if detail.len() > completion.label.len() { + let prefix_len = detail.len() - completion.label.len() - 1; + Some(detail[..prefix_len].to_string()) + } else { + None + } + }); + let mut spans = vec![CodeLabelSpan::code_range( + keyword.len()..code.len() - braces.len(), + )]; + + if let Some(namespace) = namespace { + spans.push(CodeLabelSpan::literal(format!(" ({namespace})"), None)); + } + + Some(CodeLabel { + spans, + code, + filter_range: (0..completion.label.len()).into(), + }) + } + CompletionKind::Snippet => Some(CodeLabel { + code: String::new(), + spans: vec![CodeLabelSpan::literal( + format!("{} - {}", completion.label, completion.detail?), + None, + )], + filter_range: (0..completion.label.len()).into(), + }), + CompletionKind::Keyword | CompletionKind::Variable => Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range(0..completion.label.len())], + filter_range: (0..completion.label.len()).into(), + code: completion.label, + }), + CompletionKind::Constructor => { + let detail = completion.detail?; + let parameters = &detail[detail.find('(')?..]; + let braces = " {}"; + let code = format!("{}{parameters}{braces}", completion.label); + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range(0..code.len() - braces.len())], + code, + filter_range: (0..completion.label.len()).into(), + }) + } + _ => None, + }) + } + + fn label_for_symbol( + &self, + _language_server_id: &LanguageServerId, + symbol: Symbol, + ) -> Option { + let name = &symbol.name; + + match symbol.kind { + SymbolKind::Class | SymbolKind::Interface | SymbolKind::Enum => { + let keyword = match symbol.kind { + SymbolKind::Class => "class ", + SymbolKind::Interface => "interface ", + SymbolKind::Enum => "enum ", + _ => return None, + }; + let code = format!("{keyword}{name} {{}}"); + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())], + filter_range: (keyword.len()..keyword.len() + name.len()).into(), + code, + }) + } + SymbolKind::Method | SymbolKind::Function => { + // jdtls: "methodName(Type, Type) : ReturnType" or "methodName(Type)" + // display: "ReturnType methodName(Type, Type)" (Java declaration order) + let method_name = name.split('(').next().unwrap_or(name); + let after_name = &name[method_name.len()..]; + + let (params, return_type) = if let Some((p, r)) = after_name.split_once(" : ") { + (p, Some(r)) + } else { + (after_name, None) + }; + + let ret = return_type.unwrap_or("void"); + let class_open = "class _ { "; + let code = format!("{class_open}{ret} {method_name}() {{}} }}"); + + let ret_start = class_open.len(); + let name_start = ret_start + ret.len() + 1; + + // Display: "void methodName(String, int)" + let mut spans = vec![ + CodeLabelSpan::code_range(ret_start..ret_start + ret.len()), + CodeLabelSpan::literal(" ".to_string(), None), + CodeLabelSpan::code_range(name_start..name_start + method_name.len()), + ]; + if !params.is_empty() { + spans.push(CodeLabelSpan::literal(params.to_string(), None)); + } + + // filter on "methodName(params)" portion of displayed text + let type_prefix_len = ret.len() + 1; // "void " + let filter_end = type_prefix_len + method_name.len() + params.len(); + Some(CodeLabel { + spans, + filter_range: (type_prefix_len..filter_end).into(), + code, + }) + } + _ => None, + } + } } register_extension!(Java); diff --git a/src/task.rs b/src/task.rs new file mode 100644 index 0000000..c522d25 --- /dev/null +++ b/src/task.rs @@ -0,0 +1,21 @@ +use serde_json::Value; +use zed_extension_api::{self as zed, LanguageServerId, Worktree}; + +use crate::download; + +const TASK_HELPER_BINARY: &str = "java-task-helper"; + +pub fn task_helper_binary_path( + cached: &mut Option, + configuration: &Option, + language_server_id: &LanguageServerId, + worktree: &Worktree, +) -> zed::Result { + download::download_binary( + cached, + configuration, + language_server_id, + worktree, + TASK_HELPER_BINARY, + ) +} diff --git a/task_helper/Cargo.toml b/task_helper/Cargo.toml new file mode 100644 index 0000000..334b85b --- /dev/null +++ b/task_helper/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "java-task-helper" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" +description = "Task management helper for the Zed Java extension" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[dev-dependencies] +tempfile = "3.17.1" diff --git a/task_helper/src/build_tool/gradle.rs b/task_helper/src/build_tool/gradle.rs new file mode 100644 index 0000000..0514792 --- /dev/null +++ b/task_helper/src/build_tool/gradle.rs @@ -0,0 +1,154 @@ +use crate::build_tool::{find_closest_module, which_wrapper, BuildTool}; +use crate::command::TaskCommand; +use crate::is_debug; +use std::path::PathBuf; + +pub struct Gradle { + root: PathBuf, +} + +impl Gradle { + pub fn new(root: PathBuf) -> Self { + Self { root } + } + + fn find_module(&self, file: &str) -> Option { + find_closest_module(file, &self.root, &["build.gradle", "build.gradle.kts"]) + } +} + +impl BuildTool for Gradle { + fn run_class( + &self, + file: &str, + package: &str, + class: &str, + outer: Option<&str>, + ) -> TaskCommand { + let command = which_wrapper(&self.root, "gradle"); + let module = self.find_module(file); + let full_class = match outer { + Some(o) => format!("{}${}", o, class), + None => class.to_string(), + }; + let full_name = if package.is_empty() { + full_class + } else { + format!("{}.{}", package, full_class) + }; + + let task = if let Some(m) = module { + format!(":{}:run", m.to_string_lossy().replace("/", ":")) + } else { + ":run".to_string() + }; + + let mut args = vec![task, format!("-PmainClass={}", full_name)]; + if is_debug() { + args.push("--debug-jvm".to_string()); + } + + TaskCommand { + command, + args, + cwd: self.root.to_string_lossy().to_string(), + } + } + + fn run_test_method( + &self, + _file: &str, + package: &str, + class: &str, + outer: Option<&str>, + method: &str, + ) -> TaskCommand { + let command = which_wrapper(&self.root, "gradle"); + let module = self.find_module(_file); + let full_class = match outer { + Some(o) => format!("{}${}", o, class), + None => class.to_string(), + }; + let test_filter = if package.is_empty() { + format!("{}.{}", full_class, method) + } else { + format!("{}.{}.{}", package, full_class, method) + }; + + let task = if let Some(m) = module { + format!(":{}:test", m.to_string_lossy().replace("/", ":")) + } else { + ":test".to_string() + }; + + let mut args = vec![task, "--tests".to_string(), test_filter]; + if is_debug() { + args.push("--debug-jvm".to_string()); + } + + TaskCommand { + command, + args, + cwd: self.root.to_string_lossy().to_string(), + } + } + + fn run_test_class( + &self, + _file: &str, + package: &str, + class: &str, + outer: Option<&str>, + ) -> TaskCommand { + let command = which_wrapper(&self.root, "gradle"); + let module = self.find_module(_file); + let full_class = match outer { + Some(o) => format!("{}${}", o, class), + None => class.to_string(), + }; + let test_filter = if package.is_empty() { + full_class + } else { + format!("{}.{}", package, full_class) + }; + + let task = if let Some(m) = module { + format!(":{}:test", m.to_string_lossy().replace("/", ":")) + } else { + ":test".to_string() + }; + + let mut args = vec![task, "--tests".to_string(), test_filter]; + if is_debug() { + args.push("--debug-jvm".to_string()); + } + + TaskCommand { + command, + args, + cwd: self.root.to_string_lossy().to_string(), + } + } + + fn run_all_tests(&self, _file: &str) -> TaskCommand { + let command = which_wrapper(&self.root, "gradle"); + let module = self.find_module(_file); + + let task = if let Some(m) = module { + format!(":{}:test", m.to_string_lossy().replace("/", ":")) + } else { + ":test".to_string() + }; + + let mut args = vec![task]; + if is_debug() { + args.push("--debug-jvm".to_string()); + } + + TaskCommand { + command, + args, + cwd: self.root.to_string_lossy().to_string(), + } + } +} diff --git a/task_helper/src/build_tool/maven.rs b/task_helper/src/build_tool/maven.rs new file mode 100644 index 0000000..97f5991 --- /dev/null +++ b/task_helper/src/build_tool/maven.rs @@ -0,0 +1,205 @@ +use crate::build_tool::{find_closest_module, which_wrapper, BuildTool}; +use crate::command::TaskCommand; +use crate::{get_jdwp_args, is_debug}; +use std::path::PathBuf; + +pub struct Maven { + root: PathBuf, +} + +impl Maven { + pub fn new(root: PathBuf) -> Self { + Self { root } + } + + fn find_module(&self, file: &str) -> Option { + find_closest_module(file, &self.root, &["pom.xml"]) + } + + fn full_class_name(&self, package: &str, class: &str, outer: Option<&str>) -> String { + let full_class = match outer { + Some(o) => format!("{}${}", o, class), + None => class.to_string(), + }; + if package.is_empty() { + full_class + } else { + format!("{}.{}", package, full_class) + } + } +} + +impl BuildTool for Maven { + fn run_class( + &self, + file: &str, + package: &str, + class: &str, + outer: Option<&str>, + ) -> TaskCommand { + let command = which_wrapper(&self.root, "mvn"); + let module = self.find_module(file); + let full_name = self.full_class_name(package, class, outer); + let is_test = file.contains("/src/test/"); + let compile_goal = if is_test { "test-compile" } else { "compile" }; + let classpath_scope = if is_test { "test" } else { "runtime" }; + + let env_prefix = if is_debug() { + format!("MAVEN_OPTS=\"{}\" ", get_jdwp_args()) + } else { + "".to_string() + }; + + if let Some(m) = module { + let m_str = m.to_string_lossy().to_string(); + let shell_cmd = format!( + "{}{} clean {} -pl \"{}\" -am && {}{} exec:java -pl \"{}\" -Dexec.mainClass=\"{}\" -Dexec.classpathScope={}", + env_prefix, command, compile_goal, m_str, env_prefix, command, m_str, full_name, classpath_scope + ); + TaskCommand { + command: "sh".to_string(), + args: vec!["-c".to_string(), shell_cmd], + cwd: self.root.to_string_lossy().to_string(), + } + } else { + let shell_cmd = format!( + "{}{} clean {} exec:java -Dexec.mainClass=\"{}\" -Dexec.classpathScope={}", + env_prefix, command, compile_goal, full_name, classpath_scope + ); + TaskCommand { + command: "sh".to_string(), + args: vec!["-c".to_string(), shell_cmd], + cwd: self.root.to_string_lossy().to_string(), + } + } + } + + fn run_test_method( + &self, + file: &str, + package: &str, + class: &str, + outer: Option<&str>, + method: &str, + ) -> TaskCommand { + let command = which_wrapper(&self.root, "mvn"); + let module = self.find_module(file); + let full_class = match outer { + Some(o) => format!("{}${}", o, class), + None => class.to_string(), + }; + let test_filter = if package.is_empty() { + format!("{}#{}", full_class, method) + } else { + format!("{}.{}#{}", package, full_class, method) + }; + + let debug_arg = if is_debug() { + " -Dmaven.surefire.debug" + } else { + "" + }; + + if let Some(m) = module { + let m_str = m.to_string_lossy().to_string(); + let shell_cmd = format!( + "{} clean test-compile -pl \"{}\" -am && {} test -pl \"{}\" -Dtest='{}'{}", + command, m_str, command, m_str, test_filter, debug_arg + ); + TaskCommand { + command: "sh".to_string(), + args: vec!["-c".to_string(), shell_cmd], + cwd: self.root.to_string_lossy().to_string(), + } + } else { + let shell_cmd = format!( + "{} clean test -Dtest='{}'{}", + command, test_filter, debug_arg + ); + TaskCommand { + command: "sh".to_string(), + args: vec!["-c".to_string(), shell_cmd], + cwd: self.root.to_string_lossy().to_string(), + } + } + } + + fn run_test_class( + &self, + file: &str, + package: &str, + class: &str, + outer: Option<&str>, + ) -> TaskCommand { + let command = which_wrapper(&self.root, "mvn"); + let module = self.find_module(file); + let full_class = match outer { + Some(o) => format!("{}${}", o, class), + None => class.to_string(), + }; + let test_filter = if package.is_empty() { + full_class + } else { + format!("{}.{}", package, full_class) + }; + + let debug_arg = if is_debug() { + " -Dmaven.surefire.debug" + } else { + "" + }; + + if let Some(m) = module { + let m_str = m.to_string_lossy().to_string(); + let shell_cmd = format!( + "{} clean test-compile -pl \"{}\" -am && {} test -pl \"{}\" -Dtest='{}'{}", + command, m_str, command, m_str, test_filter, debug_arg + ); + TaskCommand { + command: "sh".to_string(), + args: vec!["-c".to_string(), shell_cmd], + cwd: self.root.to_string_lossy().to_string(), + } + } else { + let shell_cmd = format!( + "{} clean test -Dtest='{}'{}", + command, test_filter, debug_arg + ); + TaskCommand { + command: "sh".to_string(), + args: vec!["-c".to_string(), shell_cmd], + cwd: self.root.to_string_lossy().to_string(), + } + } + } + + fn run_all_tests(&self, file: &str) -> TaskCommand { + let command = which_wrapper(&self.root, "mvn"); + let module = self.find_module(file); + let debug_arg = if is_debug() { + " -Dmaven.surefire.debug" + } else { + "" + }; + + if let Some(m) = module { + let m_str = m.to_string_lossy().to_string(); + let shell_cmd = format!( + "{} clean test-compile -pl \"{}\" -am && {} test -pl \"{}\"{}", + command, m_str, command, m_str, debug_arg + ); + TaskCommand { + command: "sh".to_string(), + args: vec!["-c".to_string(), shell_cmd], + cwd: self.root.to_string_lossy().to_string(), + } + } else { + let shell_cmd = format!("{} clean test{}", command, debug_arg); + TaskCommand { + command: "sh".to_string(), + args: vec!["-c".to_string(), shell_cmd], + cwd: self.root.to_string_lossy().to_string(), + } + } + } +} diff --git a/task_helper/src/build_tool/mod.rs b/task_helper/src/build_tool/mod.rs new file mode 100644 index 0000000..c74bb0a --- /dev/null +++ b/task_helper/src/build_tool/mod.rs @@ -0,0 +1,175 @@ +use crate::command::TaskCommand; +use std::env; +use std::path::{Path, PathBuf}; + +pub mod gradle; +pub mod maven; +pub mod vanilla; + +pub trait BuildTool { + fn run_class(&self, file: &str, package: &str, class: &str, outer: Option<&str>) + -> TaskCommand; + fn run_test_method( + &self, + file: &str, + package: &str, + class: &str, + outer: Option<&str>, + method: &str, + ) -> TaskCommand; + fn run_test_class( + &self, + file: &str, + package: &str, + class: &str, + outer: Option<&str>, + ) -> TaskCommand; + fn run_all_tests(&self, file: &str) -> TaskCommand; +} + +pub fn detect_build_tool(cwd: &Path) -> (Box, PathBuf) { + let cwd = cwd.canonicalize().unwrap_or_else(|_| cwd.to_path_buf()); + if cwd.join("pom.xml").exists() { + return (Box::new(maven::Maven::new(cwd.clone())), cwd); + } + if cwd.join("build.gradle").exists() + || cwd.join("build.gradle.kts").exists() + || cwd.join("settings.gradle").exists() + || cwd.join("settings.gradle.kts").exists() + { + return (Box::new(gradle::Gradle::new(cwd.clone())), cwd); + } + (Box::new(vanilla::Vanilla::new(cwd.clone())), cwd) +} + +pub fn get_workspace_root() -> (Box, PathBuf) { + let cwd = env::current_dir().unwrap_or_default(); + detect_build_tool(&cwd) +} + +pub fn find_closest_module(file_path: &str, root: &Path, marker_files: &[&str]) -> Option { + let file_path = Path::new(file_path); + let current_abs = if file_path.is_absolute() { + if file_path.is_file() { + file_path.parent().map(|p| p.to_path_buf()) + } else { + Some(file_path.to_path_buf()) + } + } else { + let abs = root.join(file_path); + if abs.is_file() { + abs.parent().map(|p| p.to_path_buf()) + } else { + Some(abs) + } + }; + + let abs_root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf()); + + let mut current_opt = current_abs; + while let Some(current) = current_opt { + let current_canonical = current.canonicalize().unwrap_or_else(|_| current.clone()); + if !current_canonical.starts_with(&abs_root) { + break; + } + + for marker in marker_files { + if current.join(marker).exists() { + let rel = current_canonical + .strip_prefix(&abs_root) + .ok() + .map(|p| p.to_path_buf()); + if let Some(ref p) = rel { + if p.as_os_str().is_empty() { + return None; // Root module + } + } + return rel; + } + } + + if current_canonical == abs_root { + break; + } + + current_opt = current.parent().map(|p| p.to_path_buf()); + } + None +} + +pub fn which_wrapper(root: &Path, tool_name: &str) -> String { + let wrapper_name = if tool_name == "mvn" { + if cfg!(windows) { + "mvnw.cmd" + } else { + "./mvnw" + } + } else { + if cfg!(windows) { + "gradlew.bat" + } else { + "./gradlew" + } + }; + + if root.join(wrapper_name.trim_start_matches("./")).exists() { + wrapper_name.to_string() + } else { + tool_name.to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::{self, File}; + use tempfile::tempdir; + + #[test] + fn test_detect_maven() { + let dir = tempdir().unwrap(); + File::create(dir.path().join("pom.xml")).unwrap(); + + let (_tool, root) = detect_build_tool(dir.path()); + // Since we return Box, we can't easily assert type, + // but we can check the root. + assert_eq!(root, dir.path().canonicalize().unwrap()); + } + + #[test] + fn test_find_closest_module_maven() { + let root_dir = tempdir().unwrap(); + let root_path = root_dir.path().canonicalize().unwrap(); + File::create(root_path.join("pom.xml")).unwrap(); + + let sub_dir = root_path.join("module-a"); + fs::create_dir(&sub_dir).unwrap(); + File::create(sub_dir.join("pom.xml")).unwrap(); + + let file_path = sub_dir.join("src/main/java/App.java"); + fs::create_dir_all(file_path.parent().unwrap()).unwrap(); + File::create(&file_path).unwrap(); + + let module = find_closest_module(file_path.to_str().unwrap(), &root_path, &["pom.xml"]); + assert_eq!(module, Some(PathBuf::from("module-a"))); + } + + #[test] + fn test_find_closest_module_gradle() { + let root_dir = tempdir().unwrap(); + let root_path = root_dir.path().canonicalize().unwrap(); + File::create(root_path.join("settings.gradle")).unwrap(); + + let sub_dir = root_path.join("module-b"); + fs::create_dir(&sub_dir).unwrap(); + File::create(sub_dir.join("build.gradle")).unwrap(); + + let file_path = sub_dir.join("src/main/java/App.java"); + fs::create_dir_all(file_path.parent().unwrap()).unwrap(); + File::create(&file_path).unwrap(); + + let module = + find_closest_module(file_path.to_str().unwrap(), &root_path, &["build.gradle"]); + assert_eq!(module, Some(PathBuf::from("module-b"))); + } +} diff --git a/task_helper/src/build_tool/vanilla.rs b/task_helper/src/build_tool/vanilla.rs new file mode 100644 index 0000000..40acdf1 --- /dev/null +++ b/task_helper/src/build_tool/vanilla.rs @@ -0,0 +1,86 @@ +use crate::build_tool::BuildTool; +use crate::command::TaskCommand; +use crate::{get_jdwp_args, is_debug}; +use std::path::PathBuf; + +pub struct Vanilla { + root: PathBuf, +} + +impl Vanilla { + pub fn new(root: PathBuf) -> Self { + Self { root } + } +} + +impl BuildTool for Vanilla { + fn run_class( + &self, + _file: &str, + package: &str, + class: &str, + outer: Option<&str>, + ) -> TaskCommand { + let full_class = match outer { + Some(o) => format!("{}${}", o, class), + None => class.to_string(), + }; + let full_name = if package.is_empty() { + full_class + } else { + format!("{}.{}", package, full_class) + }; + + let debug_args = if is_debug() { + format!("{} ", get_jdwp_args()) + } else { + "".to_string() + }; + + TaskCommand { + command: "sh".to_string(), + args: vec![ + "-c".to_string(), + format!("find . -name '*.java' -not -path './bin/*' -not -path './target/*' -not -path './build/*' -print0 | xargs -0 javac -d bin && java {} -cp bin \"{}\"", debug_args, full_name), + ], + cwd: self.root.to_string_lossy().to_string(), + } + } + + fn run_test_method( + &self, + _file: &str, + _package: &str, + _class: &str, + _outer: Option<&str>, + _method: &str, + ) -> TaskCommand { + TaskCommand { + command: "echo".to_string(), + args: vec!["No build tool found".to_string()], + cwd: self.root.to_string_lossy().to_string(), + } + } + + fn run_test_class( + &self, + _file: &str, + _package: &str, + _class: &str, + _outer: Option<&str>, + ) -> TaskCommand { + TaskCommand { + command: "echo".to_string(), + args: vec!["No build tool found".to_string()], + cwd: self.root.to_string_lossy().to_string(), + } + } + + fn run_all_tests(&self, _file: &str) -> TaskCommand { + TaskCommand { + command: "echo".to_string(), + args: vec!["No build tool found".to_string()], + cwd: self.root.to_string_lossy().to_string(), + } + } +} diff --git a/task_helper/src/command.rs b/task_helper/src/command.rs new file mode 100644 index 0000000..57713d8 --- /dev/null +++ b/task_helper/src/command.rs @@ -0,0 +1,34 @@ +use serde::Serialize; +use std::process::{self, Command}; + +#[derive(Serialize)] +pub struct TaskCommand { + pub command: String, + pub args: Vec, + pub cwd: String, +} + +impl TaskCommand { + pub fn execute(self) { + let mut cmd = Command::new(&self.command); + cmd.args(&self.args); + cmd.current_dir(&self.cwd); + + // Inherit stdin/stdout/stderr + cmd.stdin(process::Stdio::inherit()); + cmd.stdout(process::Stdio::inherit()); + cmd.stderr(process::Stdio::inherit()); + + let mut child = cmd.spawn().unwrap_or_else(|e| { + eprintln!("Failed to execute {}: {}", self.command, e); + process::exit(1); + }); + + let status = child.wait().unwrap_or_else(|e| { + eprintln!("Failed to wait for {}: {}", self.command, e); + process::exit(1); + }); + + process::exit(status.code().unwrap_or(0)); + } +} diff --git a/task_helper/src/main.rs b/task_helper/src/main.rs new file mode 100644 index 0000000..110c5c6 --- /dev/null +++ b/task_helper/src/main.rs @@ -0,0 +1,107 @@ +mod build_tool; +mod command; + +use crate::build_tool::get_workspace_root; +use std::env; +use std::path::{Path, PathBuf}; + +fn main() { + let args: Vec = env::args().skip(1).collect(); + if args.is_empty() { + return; + } + + let subcommand = &args[0]; + let (tool, _root) = get_workspace_root(); + + let result = match subcommand.as_str() { + "run-class" => { + if args.len() < 4 { + return; + } + let file = &args[1]; + let package = &args[2]; + let class = &args[3]; + let outer = args.get(4).filter(|s| !s.is_empty()).map(|s| s.as_str()); + Some(tool.run_class(file, package, class, outer)) + } + "run-test-method" => { + if args.len() < 6 { + return; + } + let file = &args[1]; + let package = &args[2]; + let class = &args[3]; + let outer = args.get(4).filter(|s| !s.is_empty()).map(|s| s.as_str()); + let method = &args[5]; + Some(tool.run_test_method(file, package, class, outer, method)) + } + "run-test-class" => { + if args.len() < 4 { + return; + } + let file = &args[1]; + let package = &args[2]; + let class = &args[3]; + let outer = args.get(4).filter(|s| !s.is_empty()).map(|s| s.as_str()); + Some(tool.run_test_class(file, package, class, outer)) + } + "run-all-tests" => { + if args.len() < 2 { + return; + } + let file = &args[1]; + Some(tool.run_all_tests(file)) + } + "clear-cache" => Some(task_clear_cache()), + _ => None, + }; + + if let Some(cmd) = result { + // Output JSON for transparency/debugging + eprintln!("{}", serde_json::to_string(&cmd).unwrap()); + // Execute the task + cmd.execute(); + } +} + +pub fn is_debug() -> bool { + env::var("ZED_JAVA_DEBUG").unwrap_or_default() == "1" +} + +pub fn get_debug_port() -> String { + env::var("ZED_JAVA_DEBUG_PORT").unwrap_or_else(|_| "5005".to_string()) +} + +pub fn get_jdwp_args() -> String { + format!( + "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address={}", + get_debug_port() + ) +} + +fn task_clear_cache() -> crate::command::TaskCommand { + let cache_dir = if let Ok(xdg) = env::var("XDG_CACHE_HOME") { + PathBuf::from(xdg) + } else if cfg!(target_os = "macos") { + env::var("HOME") + .map(|h| Path::new(&h).join("Library/Caches")) + .unwrap_or_default() + } else { + env::var("HOME") + .map(|h| Path::new(&h).join(".cache")) + .unwrap_or_default() + }; + + crate::command::TaskCommand { + command: "sh".to_string(), + args: vec![ + "-c".to_string(), + format!("find \"{}\" -maxdepth 1 -type d -name 'jdtls-*' -exec rm -rf {{}} + && echo 'JDTLS cache cleared. Restart the language server'", cache_dir.to_string_lossy()), + ], + cwd: env::current_dir() + .unwrap_or_default() + .to_string_lossy() + .to_string(), + } +} diff --git a/tests/task_verification_test.rs b/tests/task_verification_test.rs index 20f1a40..e7fb780 100644 --- a/tests/task_verification_test.rs +++ b/tests/task_verification_test.rs @@ -170,6 +170,36 @@ impl<'a> TaskRunner<'a> { fn run(self) -> String { let mut cmd = std::process::Command::new("sh"); + + // Find the built java-task-helper binary + let task_helper_bin = std::env::current_dir().unwrap(); + + // Try common target paths + let paths = [ + "target/debug/java-task-helper", + "target/x86_64-unknown-linux-gnu/debug/java-task-helper", + "target/aarch64-unknown-linux-gnu/debug/java-task-helper", + ]; + + let mut found_bin = None; + for path in paths { + let p = task_helper_bin.join(path); + if p.exists() { + found_bin = Some(p); + break; + } + } + + let found_bin = found_bin.expect( + "Could not find java-task-helper binary in target directory. Run 'cargo build' first.", + ); + + // Create a temporary ZED_EXT directory structure matching what tasks.json expects + let zed_ext_base = self.project.temp_dir.join("mock_zed_ext"); + let zed_ext_dir = zed_ext_base.join("zed/extensions/work/java/proxy-bin"); + fs::create_dir_all(&zed_ext_dir).unwrap(); + fs::copy(&found_bin, zed_ext_dir.join("java-task-helper")).unwrap(); + cmd.arg("-c") .arg(&self.command) .env("ZED_FILE", self.zed_file.to_string_lossy().to_string()) @@ -177,6 +207,7 @@ impl<'a> TaskRunner<'a> { .env("ZED_CUSTOM_java_package_name", &self.package) .env("ZED_CUSTOM_java_class_name", &self.class) .env("PATH", &self.project.new_path) + .env("XDG_DATA_HOME", zed_ext_base.to_string_lossy().to_string()) .current_dir(&self.project.temp_dir); for (k, v) in self.extra_env { From 49d928d6587fe5963863856fd387e3caf8fe724e Mon Sep 17 00:00:00 2001 From: Christian Pieczewski Date: Mon, 15 Jun 2026 14:39:39 +0200 Subject: [PATCH 2/5] feat: add java test tasks and refactor binary download - add test class, run all tests, and clear cache tasks to tasks.json - refactor binary download logic by moving it from proxy.rs to download.rs - update download logic to use allow_download flag and handle status updates - implement remove_all_files_except in the download flow --- languages/java/tasks.json | 39 ++++ src/download.rs | 147 ------------- src/java.rs | 424 ++------------------------------------ src/jdtls_server.rs | 10 + src/task.rs | 186 +++++++++++++++-- 5 files changed, 241 insertions(+), 565 deletions(-) delete mode 100644 src/download.rs diff --git a/languages/java/tasks.json b/languages/java/tasks.json index 71139ff..292f9a8 100644 --- a/languages/java/tasks.json +++ b/languages/java/tasks.json @@ -24,5 +24,44 @@ "args": ["-c"] } } + }, + { + "label": "Test class $ZED_CUSTOM_java_class_name", + "command": "EXT=\"${XDG_DATA_HOME:-$HOME/.local/share}/zed/extensions/work/java\"; [ \"$(uname 2>/dev/null)\" = \"Darwin\" ] && EXT=\"$HOME/Library/Application Support/Zed/extensions/work/java\"; [ -n \"$LOCALAPPDATA\" ] && EXT=\"$(cygpath -u \"$LOCALAPPDATA\" 2>/dev/null || echo \"$LOCALAPPDATA\")/zed/extensions/work/java\"; \"$EXT/proxy-bin/java-task-helper\" run-test-class \"$ZED_FILE\" \"$ZED_CUSTOM_java_package_name\" \"$ZED_CUSTOM_java_class_name\" \"${ZED_CUSTOM_java_outer_class_name:-}\"", + "use_new_terminal": false, + "reveal": "always", + "tags": ["java-test-class", "java-test-class-nested"], + "shell": { + "with_arguments": { + "program": "/bin/sh", + "args": ["-c"] + } + } + }, + { + "label": "Run tests", + "command": "EXT=\"${XDG_DATA_HOME:-$HOME/.local/share}/zed/extensions/work/java\"; [ \"$(uname 2>/dev/null)\" = \"Darwin\" ] && EXT=\"$HOME/Library/Application Support/Zed/extensions/work/java\"; [ -n \"$LOCALAPPDATA\" ] && EXT=\"$(cygpath -u \"$LOCALAPPDATA\" 2>/dev/null || echo \"$LOCALAPPDATA\")/zed/extensions/work/java\"; \"$EXT/proxy-bin/java-task-helper\" run-all-tests \"$ZED_FILE\"", + "use_new_terminal": false, + "reveal": "always", + "tags": ["java-test-all"], + "shell": { + "with_arguments": { + "program": "/bin/sh", + "args": ["-c"] + } + } + }, + { + "label": "Clear JDTLS cache", + "command": "EXT=\"${XDG_DATA_HOME:-$HOME/.local/share}/zed/extensions/work/java\"; [ \"$(uname 2>/dev/null)\" = \"Darwin\" ] && EXT=\"$HOME/Library/Application Support/Zed/extensions/work/java\"; [ -n \"$LOCALAPPDATA\" ] && EXT=\"$(cygpath -u \"$LOCALAPPDATA\" 2>/dev/null || echo \"$LOCALAPPDATA\")/zed/extensions/work/java\"; \"$EXT/proxy-bin/java-task-helper\" clear-cache", + "use_new_terminal": false, + "reveal": "always", + "tags": [], + "shell": { + "with_arguments": { + "program": "/bin/sh", + "args": ["-c"] + } + } } ] diff --git a/src/download.rs b/src/download.rs deleted file mode 100644 index 25ea1e7..0000000 --- a/src/download.rs +++ /dev/null @@ -1,147 +0,0 @@ -use std::{fs::metadata, path::PathBuf}; - -use serde_json::Value; -use zed_extension_api::{ - self as zed, DownloadedFileType, GithubReleaseOptions, LanguageServerId, - LanguageServerInstallationStatus, Worktree, serde_json, - set_language_server_installation_status, -}; - -use crate::util::{mark_checked_once, should_use_local_or_download}; - -pub(crate) const PROXY_INSTALL_PATH: &str = "proxy-bin"; -pub(crate) const GITHUB_REPO: &str = "zed-extensions/java"; - -pub(crate) fn asset_name(binary: &str) -> zed::Result<(String, DownloadedFileType)> { - let (os, arch) = zed::current_platform(); - let (os_str, file_type) = match os { - zed::Os::Mac => ("darwin", DownloadedFileType::GzipTar), - zed::Os::Linux => ("linux", DownloadedFileType::GzipTar), - zed::Os::Windows => ("windows", DownloadedFileType::Zip), - }; - let arch_str = match arch { - zed::Architecture::Aarch64 => "aarch64", - zed::Architecture::X8664 => "x86_64", - _ => return Err("Unsupported architecture".into()), - }; - let ext = if matches!(file_type, DownloadedFileType::Zip) { - "zip" - } else { - "tar.gz" - }; - Ok((format!("{binary}-{os_str}-{arch_str}.{ext}"), file_type)) -} - -pub(crate) fn binary_exec(binary: &str) -> String { - let (os, _arch) = zed::current_platform(); - - match os { - zed::Os::Linux | zed::Os::Mac => binary.to_string(), - zed::Os::Windows => format!("{binary}.exe"), - } -} - -pub(crate) fn find_latest_local(binary: &str) -> Option { - let exec = binary_exec(binary); - let local_binary = PathBuf::from(PROXY_INSTALL_PATH).join(&exec); - if metadata(&local_binary).is_ok_and(|m| m.is_file()) { - return Some(local_binary); - } - - // Check versioned downloads (e.g. proxy-bin/v6.8.12/java-lsp-proxy) - std::fs::read_dir(PROXY_INSTALL_PATH) - .ok()? - .filter_map(Result::ok) - .map(|e| e.path().join(&exec)) - .filter(|p| metadata(p).is_ok_and(|m| m.is_file())) - .last() -} - -pub(crate) fn download_binary( - cached: &mut Option, - configuration: &Option, - language_server_id: &LanguageServerId, - worktree: &Worktree, - binary: &str, -) -> zed::Result { - // 1. Respect check_updates setting (Never/Once/Always) - match should_use_local_or_download(configuration, find_latest_local(binary), PROXY_INSTALL_PATH) - { - Ok(Some(path)) => { - let s = path.to_string_lossy().to_string(); - *cached = Some(s.clone()); - return Ok(s); - } - Ok(None) => { /* policy allows download, continue */ } - Err(_) => { - // Never/Once with no managed install — fall through to PATH as last resort - } - } - - // 2. Auto-download from GitHub releases - if let Ok((name, file_type)) = asset_name(binary) - && let Ok(release) = zed::latest_github_release( - GITHUB_REPO, - GithubReleaseOptions { - require_assets: true, - pre_release: false, - }, - ) - { - let bin_path = format!( - "{}/{}/{}", - PROXY_INSTALL_PATH, - release.version, - binary_exec(binary) - ); - - if metadata(&bin_path).is_ok() { - *cached = Some(bin_path.clone()); - return Ok(bin_path); - } - - if let Some(asset) = release.assets.iter().find(|a| a.name == name) { - let version_dir = format!("{PROXY_INSTALL_PATH}/{}", release.version); - - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::Downloading, - ); - - if zed::download_file(&asset.download_url, &version_dir, file_type).is_ok() { - let _ = zed::make_file_executable(&bin_path); - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::None, - ); - // Do not remove other files if we are downloading one of multiple binaries - // but for now they are in the same version dir. - // let _ = remove_all_files_except(PROXY_INSTALL_PATH, &release.version); - let _ = mark_checked_once(PROXY_INSTALL_PATH, &release.version); - *cached = Some(bin_path.clone()); - return Ok(bin_path); - } - } - } - - // 3. Fallback: local install (covers "always" mode when download fails) - if let Some(path) = find_latest_local(binary) { - let s = path.to_string_lossy().to_string(); - *cached = Some(s.clone()); - return Ok(s); - } - - // 4. Fallback: binary on $PATH - if let Some(path) = worktree.which(binary_exec(binary).as_str()) { - return Ok(path); - } - - // 5. Stale cache fallback - if let Some(path) = cached.as_deref() - && metadata(path).is_ok() - { - return Ok(path.to_string()); - } - - Err(format!("'{binary}' not found")) -} diff --git a/src/java.rs b/src/java.rs index 007e82f..fa28272 100644 --- a/src/java.rs +++ b/src/java.rs @@ -1,7 +1,6 @@ mod config; mod debugger; mod downloadable; -mod download; mod jdk; mod jdtls; mod jdtls_server; @@ -28,11 +27,7 @@ use crate::{ const DEBUG_ADAPTER_NAME: &str = "Java"; struct Java { - cached_binary_path: Option, - cached_lombok_path: Option, - cached_proxy_path: Option, - cached_task_helper_path: Option, - integrations: Option<(LspWrapper, Debugger)>, + jdtls_server: JdtlsServer, } impl Extension for Java { @@ -80,42 +75,31 @@ impl Extension for Java { .workspace_configuration(language_server_id, worktree), _ => Ok(None), } + } - // Use cached path if exists - if let Some(path) = &self.cached_lombok_path - && fs::metadata(path).is_ok_and(|stat| stat.is_file()) - { - return Ok(path.clone()); - } - - match try_to_fetch_and_install_latest_lombok(language_server_id, configuration) { - Ok(path) => { - self.cached_lombok_path = Some(path.clone()); - Ok(path) - } - Err(e) => { - if let Some(local_version) = find_latest_local_lombok() { - self.cached_lombok_path = Some(local_version.clone()); - Ok(local_version) - } else { - Err(e) - } - } + fn label_for_completion( + &self, + language_server_id: &LanguageServerId, + completion: Completion, + ) -> Option { + match language_server_id.as_ref() { + JdtlsServer::SERVER_ID => self + .jdtls_server + .label_for_completion(language_server_id, completion), + _ => None, } } -} -impl Extension for Java { - fn new() -> Self - where - Self: Sized, - { - Self { - cached_binary_path: None, - cached_lombok_path: None, - cached_proxy_path: None, - cached_task_helper_path: None, - integrations: None, + fn label_for_symbol( + &self, + language_server_id: &LanguageServerId, + symbol: Symbol, + ) -> Option { + match language_server_id.as_ref() { + JdtlsServer::SERVER_ID => self + .jdtls_server + .label_for_symbol(language_server_id, symbol), + _ => None, } } @@ -238,370 +222,6 @@ impl Extension for Java { } } } - - fn language_server_command( - &mut self, - language_server_id: &LanguageServerId, - worktree: &Worktree, - ) -> zed::Result { - let current_dir = - env::current_dir().map_err(|err| format!("Failed to get current directory: {err}"))?; - - let configuration = - self.language_server_workspace_configuration(language_server_id, worktree)?; - - let mut env = Vec::new(); - - if let Some(java_home) = get_java_home(&configuration, worktree) { - env.push(("JAVA_HOME".to_string(), java_home)); - } - - let proxy_path = proxy::binary_path( - &mut self.cached_proxy_path, - &configuration, - language_server_id, - worktree, - ) - .map_err(|err| format!("Failed to get proxy binary path: {err}"))?; - - let _ = task::task_helper_binary_path( - &mut self.cached_task_helper_path, - &configuration, - language_server_id, - worktree, - ); - - // proxy takes: workdir, bin, [args...] - let mut args = vec![ - path_to_string(current_dir.clone()) - .map_err(|err| format!("Failed to convert current directory to string: {err}"))?, - ]; - - // Add lombok as javaagent if settings.java.jdt.ls.lombokSupport.enabled is true - let lombok_jvm_arg = if is_lombok_enabled(&configuration) { - let lombok_jar_path = self - .lombok_jar_path(language_server_id, &configuration, worktree) - .map_err(|err| format!("Failed to get Lombok jar path: {err}"))?; - let canonical_lombok_jar_path = path_to_string(current_dir.join(lombok_jar_path)) - .map_err(|err| format!("Failed to convert Lombok jar path to string: {err}"))?; - - Some(format!("-javaagent:{canonical_lombok_jar_path}")) - } else { - None - }; - - self.init(worktree); - - // Check for user-configured JDTLS launcher first - if let Some(launcher) = get_jdtls_launcher(&configuration, worktree) { - args.push(launcher); - if let Some(lombok_jvm_arg) = lombok_jvm_arg { - args.push(format!("--jvm-arg={lombok_jvm_arg}")); - } - } else if let Some(launcher) = get_jdtls_launcher_from_path(worktree) { - // if the user has `jdtls(.bat)` on their PATH, we use that - args.push(launcher); - if let Some(lombok_jvm_arg) = lombok_jvm_arg { - args.push(format!("--jvm-arg={lombok_jvm_arg}")); - } - } else { - // otherwise we launch ourselves - args.extend( - build_jdtls_launch_args( - &self - .language_server_binary_path(language_server_id, &configuration) - .map_err(|err| format!("Failed to get JDTLS binary path: {err}"))?, - &configuration, - worktree, - lombok_jvm_arg.into_iter().collect(), - language_server_id, - ) - .map_err(|err| format!("Failed to build JDTLS launch arguments: {err}"))?, - ); - } - - // download debugger if not exists - if let Err(err) = - self.debugger()? - .get_or_download(language_server_id, &configuration, worktree) - { - println!("Failed to download debugger: {err}"); - }; - - self.lsp()? - .switch_workspace(worktree.root_path()) - .map_err(|err| format!("Failed to switch LSP workspace: {err}"))?; - - Ok(zed::Command { - command: proxy_path, - args, - env, - }) - } - - fn language_server_initialization_options( - &mut self, - language_server_id: &LanguageServerId, - worktree: &Worktree, - ) -> zed::Result> { - if self.integrations.is_some() { - self.lsp()? - .switch_workspace(worktree.root_path()) - .map_err(|err| { - format!("Failed to switch LSP workspace for initialization: {err}") - })?; - } - - let mut options = LspSettings::for_worktree(language_server_id.as_ref(), worktree) - .map(|lsp_settings| lsp_settings.initialization_options) - .map_err(|err| format!("Failed to get LSP settings for worktree: {err}"))? - .unwrap_or_else(|| json!({})); - - // Inject workspaceFolders default if not already set by the user - let options_obj = options - .as_object_mut() - .ok_or_else(|| "initialization_options is not a JSON object".to_string())?; - if !options_obj.contains_key("workspaceFolders") { - let uri = util::path_to_file_uri(&worktree.root_path()); - options_obj.insert("workspaceFolders".to_string(), json!([uri])); - } - - // Inject extendedClientCapabilities defaults if not already set by the user - let caps = options_obj - .entry("extendedClientCapabilities") - .or_insert_with(|| json!({})); - let caps_obj = caps - .as_object_mut() - .ok_or_else(|| "extendedClientCapabilities is not a JSON object".to_string())?; - caps_obj - .entry("classFileContentsSupport") - .or_insert(json!(true)); - caps_obj - .entry("resolveAdditionalTextEditsSupport") - .or_insert(json!(true)); - - if self.debugger().is_ok_and(|v| v.loaded()) { - return Ok(Some( - self.debugger()? - .inject_plugin_into_options(Some(options)) - .map_err(|err| { - format!("Failed to inject debugger plugin into options: {err}") - })?, - )); - } - - Ok(Some(options)) - } - - fn language_server_workspace_configuration( - &mut self, - language_server_id: &LanguageServerId, - worktree: &Worktree, - ) -> zed::Result> { - if let Ok(Some(settings)) = LspSettings::for_worktree(language_server_id.as_ref(), worktree) - .map(|lsp_settings| lsp_settings.settings) - { - Ok(Some(settings)) - } else { - self.language_server_initialization_options(language_server_id, worktree) - .map(|init_options| { - init_options.and_then(|init_options| init_options.get("settings").cloned()) - }) - } - } - - fn label_for_completion( - &self, - _language_server_id: &LanguageServerId, - completion: Completion, - ) -> Option { - // uncomment when debugging completions - // println!("Java completion: {completion:#?}"); - - completion.kind.and_then(|kind| match kind { - CompletionKind::Field | CompletionKind::Constant => { - let modifiers = match kind { - CompletionKind::Field => "", - CompletionKind::Constant => "static final ", - _ => return None, - }; - let property_type = completion.detail.as_ref().and_then(|detail| { - detail - .split_once(" : ") - .map(|(_, property_type)| format!("{property_type} ")) - })?; - let semicolon = ";"; - let code = format!("{modifiers}{property_type}{}{semicolon}", completion.label); - - Some(CodeLabel { - spans: vec![ - CodeLabelSpan::code_range( - modifiers.len() + property_type.len()..code.len() - semicolon.len(), - ), - CodeLabelSpan::literal(" : ", None), - CodeLabelSpan::code_range( - modifiers.len()..modifiers.len() + property_type.len(), - ), - ], - code, - filter_range: (0..completion.label.len()).into(), - }) - } - CompletionKind::Method => { - let detail = completion.detail?; - let (left, return_type) = detail - .split_once(" : ") - .map(|(left, return_type)| (left, format!("{return_type} "))) - .unwrap_or((&detail, "void".to_string())); - let parameters = left - .find('(') - .map(|parameters_start| &left[parameters_start..]); - let name_and_parameters = - format!("{}{}", completion.label, parameters.unwrap_or("()")); - let braces = " {}"; - let code = format!("{return_type}{name_and_parameters}{braces}"); - let mut spans = vec![CodeLabelSpan::code_range( - return_type.len()..code.len() - braces.len(), - )]; - - if parameters.is_some() { - spans.push(CodeLabelSpan::literal(" : ", None)); - spans.push(CodeLabelSpan::code_range(0..return_type.len())); - } else { - spans.push(CodeLabelSpan::literal(" - ", None)); - spans.push(CodeLabelSpan::literal(detail, None)); - } - - Some(CodeLabel { - spans, - code, - filter_range: (0..completion.label.len()).into(), - }) - } - CompletionKind::Class | CompletionKind::Interface | CompletionKind::Enum => { - let keyword = match kind { - CompletionKind::Class => "class ", - CompletionKind::Interface => "interface ", - CompletionKind::Enum => "enum ", - _ => return None, - }; - let braces = " {}"; - let code = format!("{keyword}{}{braces}", completion.label); - let namespace = completion.detail.and_then(|detail| { - if detail.len() > completion.label.len() { - let prefix_len = detail.len() - completion.label.len() - 1; - Some(detail[..prefix_len].to_string()) - } else { - None - } - }); - let mut spans = vec![CodeLabelSpan::code_range( - keyword.len()..code.len() - braces.len(), - )]; - - if let Some(namespace) = namespace { - spans.push(CodeLabelSpan::literal(format!(" ({namespace})"), None)); - } - - Some(CodeLabel { - spans, - code, - filter_range: (0..completion.label.len()).into(), - }) - } - CompletionKind::Snippet => Some(CodeLabel { - code: String::new(), - spans: vec![CodeLabelSpan::literal( - format!("{} - {}", completion.label, completion.detail?), - None, - )], - filter_range: (0..completion.label.len()).into(), - }), - CompletionKind::Keyword | CompletionKind::Variable => Some(CodeLabel { - spans: vec![CodeLabelSpan::code_range(0..completion.label.len())], - filter_range: (0..completion.label.len()).into(), - code: completion.label, - }), - CompletionKind::Constructor => { - let detail = completion.detail?; - let parameters = &detail[detail.find('(')?..]; - let braces = " {}"; - let code = format!("{}{parameters}{braces}", completion.label); - - Some(CodeLabel { - spans: vec![CodeLabelSpan::code_range(0..code.len() - braces.len())], - code, - filter_range: (0..completion.label.len()).into(), - }) - } - _ => None, - }) - } - - fn label_for_symbol( - &self, - _language_server_id: &LanguageServerId, - symbol: Symbol, - ) -> Option { - let name = &symbol.name; - - match symbol.kind { - SymbolKind::Class | SymbolKind::Interface | SymbolKind::Enum => { - let keyword = match symbol.kind { - SymbolKind::Class => "class ", - SymbolKind::Interface => "interface ", - SymbolKind::Enum => "enum ", - _ => return None, - }; - let code = format!("{keyword}{name} {{}}"); - - Some(CodeLabel { - spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())], - filter_range: (keyword.len()..keyword.len() + name.len()).into(), - code, - }) - } - SymbolKind::Method | SymbolKind::Function => { - // jdtls: "methodName(Type, Type) : ReturnType" or "methodName(Type)" - // display: "ReturnType methodName(Type, Type)" (Java declaration order) - let method_name = name.split('(').next().unwrap_or(name); - let after_name = &name[method_name.len()..]; - - let (params, return_type) = if let Some((p, r)) = after_name.split_once(" : ") { - (p, Some(r)) - } else { - (after_name, None) - }; - - let ret = return_type.unwrap_or("void"); - let class_open = "class _ { "; - let code = format!("{class_open}{ret} {method_name}() {{}} }}"); - - let ret_start = class_open.len(); - let name_start = ret_start + ret.len() + 1; - - // Display: "void methodName(String, int)" - let mut spans = vec![ - CodeLabelSpan::code_range(ret_start..ret_start + ret.len()), - CodeLabelSpan::literal(" ".to_string(), None), - CodeLabelSpan::code_range(name_start..name_start + method_name.len()), - ]; - if !params.is_empty() { - spans.push(CodeLabelSpan::literal(params.to_string(), None)); - } - - // filter on "methodName(params)" portion of displayed text - let type_prefix_len = ret.len() + 1; // "void " - let filter_end = type_prefix_len + method_name.len() + params.len(); - Some(CodeLabel { - spans, - filter_range: (type_prefix_len..filter_end).into(), - code, - }) - } - _ => None, - } - } } register_extension!(Java); diff --git a/src/jdtls_server.rs b/src/jdtls_server.rs index 8578a7b..d77c970 100644 --- a/src/jdtls_server.rs +++ b/src/jdtls_server.rs @@ -15,6 +15,7 @@ use crate::{ jdtls::{Jdtls, Lombok, build_jdtls_launch_args, get_jdtls_launcher_from_path}, language_server::LanguageServer, proxy::Proxy, + task::TaskHelper, util::{path_to_file_uri, path_to_string}, }; @@ -24,6 +25,7 @@ pub struct JdtlsServer { pub proxy: Proxy, pub jdk: Jdk, pub debugger: Debugger, + pub task_helper: TaskHelper, pub cached_workspace: Option, } @@ -35,6 +37,7 @@ impl JdtlsServer { proxy: Proxy::new(), jdk: Jdk::new(), debugger: Debugger::new(), + task_helper: TaskHelper::new(), cached_workspace: None, } } @@ -117,6 +120,13 @@ impl LanguageServer for JdtlsServer { println!("Failed to download debugger: {err}"); }; + if let Err(err) = + self.task_helper + .get_or_download(language_server_id, &configuration, worktree) + { + println!("Failed to download task helper: {err}"); + } + self.cached_workspace = Some(worktree.root_path()); Ok(zed::Command { diff --git a/src/task.rs b/src/task.rs index c522d25..61af07d 100644 --- a/src/task.rs +++ b/src/task.rs @@ -1,21 +1,175 @@ -use serde_json::Value; -use zed_extension_api::{self as zed, LanguageServerId, Worktree}; +use std::{fs::metadata, path::PathBuf}; -use crate::download; +use zed_extension_api::{ + self as zed, DownloadedFileType, GithubReleaseOptions, LanguageServerId, + LanguageServerInstallationStatus, Worktree, serde_json::Value, + set_language_server_installation_status, +}; + +use crate::{ + downloadable::Downloadable, + util::{mark_checked_once, remove_all_files_except, should_use_local_or_download}, +}; const TASK_HELPER_BINARY: &str = "java-task-helper"; +const GITHUB_REPO: &str = "zed-extensions/java"; + +pub struct TaskHelper { + cached_path: Option, +} + +impl TaskHelper { + pub fn new() -> Self { + Self { cached_path: None } + } +} + +impl Downloadable for TaskHelper { + const INSTALL_PATH: &'static str = "proxy-bin"; + + fn find_local(&self) -> Option { + let local_binary = PathBuf::from(Self::INSTALL_PATH).join(task_helper_exec()); + if metadata(&local_binary).is_ok_and(|m| m.is_file()) { + return Some(local_binary); + } + + std::fs::read_dir(Self::INSTALL_PATH) + .ok()? + .filter_map(Result::ok) + .map(|e| e.path().join(task_helper_exec())) + .filter(|p| metadata(p).is_ok_and(|m| m.is_file())) + .last() + } + + fn loaded(&self) -> bool { + self.cached_path.is_some() + } + + fn fetch_latest_version(&self) -> zed::Result { + Ok(zed::latest_github_release( + GITHUB_REPO, + GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + ) + .map_err(|err| { + format!("Failed to fetch latest task helper release from {GITHUB_REPO}: {err}") + })? + .version) + } + + fn download( + &mut self, + version: &str, + language_server_id: &LanguageServerId, + ) -> zed::Result { + let (name, file_type) = asset_name()?; + let bin_path = format!("{}/{version}/{}", Self::INSTALL_PATH, task_helper_exec()); + + if metadata(&bin_path).is_ok() { + self.cached_path = Some(bin_path.clone()); + return Ok(PathBuf::from(bin_path)); + } + + let release = zed::latest_github_release( + GITHUB_REPO, + GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + ) + .map_err(|err| format!("Failed to fetch task helper release: {err}"))?; + + let asset = release + .assets + .iter() + .find(|a| a.name == name) + .ok_or_else(|| format!("No asset found matching {name:?}"))?; + + let version_dir = format!("{}/{version}", Self::INSTALL_PATH); + + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + + zed::download_file(&asset.download_url, &version_dir, file_type) + .map_err(|err| format!("Failed to download task helper: {err}"))?; + + let _ = zed::make_file_executable(&bin_path); + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::None, + ); + let _ = remove_all_files_except(Self::INSTALL_PATH, version); + let _ = mark_checked_once(Self::INSTALL_PATH, version); + + self.cached_path = Some(bin_path.clone()); + Ok(PathBuf::from(bin_path)) + } + + fn get_or_download( + &mut self, + language_server_id: &LanguageServerId, + configuration: &Option, + worktree: &Worktree, + ) -> zed::Result { + if let Some(path) = self.user_configured_path(configuration, worktree) { + self.cached_path = Some(path.clone()); + return Ok(PathBuf::from(path)); + } + + if let Some(path) = + should_use_local_or_download(configuration, self.find_local(), Self::INSTALL_PATH) + .unwrap_or(None) + { + let s = path.to_string_lossy().to_string(); + self.cached_path = Some(s); + return Ok(path); + } + + if let Ok(version) = self.fetch_latest_version() + && let Ok(path) = self.download(&version, language_server_id) + { + return Ok(path); + } + + if let Some(path) = worktree.which(task_helper_exec().as_str()) { + return Ok(PathBuf::from(path)); + } + + Err(format!("'{}' not found", task_helper_exec())) + } +} + +fn asset_name() -> zed::Result<(String, DownloadedFileType)> { + let (os, arch) = zed::current_platform(); + let (os_str, file_type) = match os { + zed::Os::Mac => ("darwin", DownloadedFileType::GzipTar), + zed::Os::Linux => ("linux", DownloadedFileType::GzipTar), + zed::Os::Windows => ("windows", DownloadedFileType::Zip), + }; + let arch_str = match arch { + zed::Architecture::Aarch64 => "aarch64", + zed::Architecture::X8664 => "x86_64", + _ => return Err("Unsupported architecture".into()), + }; + let ext = if matches!(file_type, DownloadedFileType::Zip) { + "zip" + } else { + "tar.gz" + }; + Ok(( + format!("{TASK_HELPER_BINARY}-{os_str}-{arch_str}.{ext}"), + file_type, + )) +} -pub fn task_helper_binary_path( - cached: &mut Option, - configuration: &Option, - language_server_id: &LanguageServerId, - worktree: &Worktree, -) -> zed::Result { - download::download_binary( - cached, - configuration, - language_server_id, - worktree, - TASK_HELPER_BINARY, - ) +fn task_helper_exec() -> String { + let (os, _arch) = zed::current_platform(); + match os { + zed::Os::Linux | zed::Os::Mac => TASK_HELPER_BINARY.to_string(), + zed::Os::Windows => format!("{TASK_HELPER_BINARY}.exe"), + } } From bcd39a322a7ecafcc77bbce30f11e8e40982451e Mon Sep 17 00:00:00 2001 From: Christian Pieczewski Date: Wed, 17 Jun 2026 10:57:55 +0200 Subject: [PATCH 3/5] refactor: update gradle test methods to use file argument - update run_test_method to use file argument for module lookup - update run_test_class to use file argument for module lookup - update run_all_tests to use file argument for module lookup - refactor task_clear_cache to execute directly instead of returning TaskCommand - update main entry point to handle clear-cache subcommand --- src/task.rs | 18 ++++++----- task_helper/src/build_tool/gradle.rs | 12 ++++---- task_helper/src/main.rs | 46 ++++++++++++++++++++-------- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/task.rs b/src/task.rs index 61af07d..daf79a1 100644 --- a/src/task.rs +++ b/src/task.rs @@ -12,6 +12,7 @@ use crate::{ }; const TASK_HELPER_BINARY: &str = "java-task-helper"; +const TASK_HELPER_INSTALL_PATH: &str = "proxy-bin"; const GITHUB_REPO: &str = "zed-extensions/java"; pub struct TaskHelper { @@ -25,15 +26,15 @@ impl TaskHelper { } impl Downloadable for TaskHelper { - const INSTALL_PATH: &'static str = "proxy-bin"; + const INSTALL_PATH: &'static str = TASK_HELPER_INSTALL_PATH; fn find_local(&self) -> Option { - let local_binary = PathBuf::from(Self::INSTALL_PATH).join(task_helper_exec()); + let local_binary = PathBuf::from(TASK_HELPER_INSTALL_PATH).join(task_helper_exec()); if metadata(&local_binary).is_ok_and(|m| m.is_file()) { return Some(local_binary); } - std::fs::read_dir(Self::INSTALL_PATH) + std::fs::read_dir(TASK_HELPER_INSTALL_PATH) .ok()? .filter_map(Result::ok) .map(|e| e.path().join(task_helper_exec())) @@ -65,7 +66,10 @@ impl Downloadable for TaskHelper { language_server_id: &LanguageServerId, ) -> zed::Result { let (name, file_type) = asset_name()?; - let bin_path = format!("{}/{version}/{}", Self::INSTALL_PATH, task_helper_exec()); + let bin_path = format!( + "{TASK_HELPER_INSTALL_PATH}/{version}/{}", + task_helper_exec() + ); if metadata(&bin_path).is_ok() { self.cached_path = Some(bin_path.clone()); @@ -87,7 +91,7 @@ impl Downloadable for TaskHelper { .find(|a| a.name == name) .ok_or_else(|| format!("No asset found matching {name:?}"))?; - let version_dir = format!("{}/{version}", Self::INSTALL_PATH); + let version_dir = format!("{TASK_HELPER_INSTALL_PATH}/{version}"); set_language_server_installation_status( language_server_id, @@ -102,8 +106,8 @@ impl Downloadable for TaskHelper { language_server_id, &LanguageServerInstallationStatus::None, ); - let _ = remove_all_files_except(Self::INSTALL_PATH, version); - let _ = mark_checked_once(Self::INSTALL_PATH, version); + let _ = remove_all_files_except(TASK_HELPER_INSTALL_PATH, version); + let _ = mark_checked_once(TASK_HELPER_INSTALL_PATH, version); self.cached_path = Some(bin_path.clone()); Ok(PathBuf::from(bin_path)) diff --git a/task_helper/src/build_tool/gradle.rs b/task_helper/src/build_tool/gradle.rs index 0514792..61512d0 100644 --- a/task_helper/src/build_tool/gradle.rs +++ b/task_helper/src/build_tool/gradle.rs @@ -57,14 +57,14 @@ impl BuildTool for Gradle { fn run_test_method( &self, - _file: &str, + file: &str, package: &str, class: &str, outer: Option<&str>, method: &str, ) -> TaskCommand { let command = which_wrapper(&self.root, "gradle"); - let module = self.find_module(_file); + let module = self.find_module(file); let full_class = match outer { Some(o) => format!("{}${}", o, class), None => class.to_string(), @@ -95,13 +95,13 @@ impl BuildTool for Gradle { fn run_test_class( &self, - _file: &str, + file: &str, package: &str, class: &str, outer: Option<&str>, ) -> TaskCommand { let command = which_wrapper(&self.root, "gradle"); - let module = self.find_module(_file); + let module = self.find_module(file); let full_class = match outer { Some(o) => format!("{}${}", o, class), None => class.to_string(), @@ -130,9 +130,9 @@ impl BuildTool for Gradle { } } - fn run_all_tests(&self, _file: &str) -> TaskCommand { + fn run_all_tests(&self, file: &str) -> TaskCommand { let command = which_wrapper(&self.root, "gradle"); - let module = self.find_module(_file); + let module = self.find_module(file); let task = if let Some(m) = module { format!(":{}:test", m.to_string_lossy().replace("/", ":")) diff --git a/task_helper/src/main.rs b/task_helper/src/main.rs index 110c5c6..8f2dc47 100644 --- a/task_helper/src/main.rs +++ b/task_helper/src/main.rs @@ -12,6 +12,11 @@ fn main() { } let subcommand = &args[0]; + if subcommand == "clear-cache" { + task_clear_cache(); + return; + } + let (tool, _root) = get_workspace_root(); let result = match subcommand.as_str() { @@ -53,7 +58,6 @@ fn main() { let file = &args[1]; Some(tool.run_all_tests(file)) } - "clear-cache" => Some(task_clear_cache()), _ => None, }; @@ -80,7 +84,7 @@ pub fn get_jdwp_args() -> String { ) } -fn task_clear_cache() -> crate::command::TaskCommand { +fn task_clear_cache() { let cache_dir = if let Ok(xdg) = env::var("XDG_CACHE_HOME") { PathBuf::from(xdg) } else if cfg!(target_os = "macos") { @@ -93,15 +97,33 @@ fn task_clear_cache() -> crate::command::TaskCommand { .unwrap_or_default() }; - crate::command::TaskCommand { - command: "sh".to_string(), - args: vec![ - "-c".to_string(), - format!("find \"{}\" -maxdepth 1 -type d -name 'jdtls-*' -exec rm -rf {{}} + && echo 'JDTLS cache cleared. Restart the language server'", cache_dir.to_string_lossy()), - ], - cwd: env::current_dir() - .unwrap_or_default() - .to_string_lossy() - .to_string(), + if !cache_dir.exists() { + println!("No JDTLS cache found"); + return; + } + + let mut cleared = false; + if let Ok(entries) = std::fs::read_dir(&cache_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { + if name.starts_with("jdtls-") { + if let Err(e) = std::fs::remove_dir_all(&path) { + eprintln!("Failed to remove cache directory {:?}: {}", path, e); + } else { + println!("Removed cache directory {:?}", path); + cleared = true; + } + } + } + } + } + } + + if cleared { + println!("JDTLS cache cleared. Restart the language server"); + } else { + println!("No JDTLS cache found"); } } From d966d3bfcba2cdd9942cfff4a73794d6aaed66e1 Mon Sep 17 00:00:00 2001 From: Christian Pieczewski Date: Wed, 17 Jun 2026 13:21:13 +0200 Subject: [PATCH 4/5] build: add java-task-helper to release workflow and tests - update release-proxy.yml to build and package java-task-helper - update task_verification_test.rs to dynamically locate java-task-helper binary - remove hardcoded paths for task helper in tests --- .github/workflows/release-proxy.yml | 31 +++++++-- tests/task_verification_test.rs | 100 +++++++++++++++++++++------- 2 files changed, 101 insertions(+), 30 deletions(-) diff --git a/.github/workflows/release-proxy.yml b/.github/workflows/release-proxy.yml index fdf9dc6..7902cca 100644 --- a/.github/workflows/release-proxy.yml +++ b/.github/workflows/release-proxy.yml @@ -1,4 +1,4 @@ -name: Release Proxy Binary +name: Release Binaries on: release: @@ -9,7 +9,7 @@ permissions: jobs: build: - name: Build ${{ matrix.asset_name }} + name: Build binaries for ${{ matrix.target }} runs-on: ${{ matrix.runner }} strategy: fail-fast: true @@ -18,21 +18,27 @@ jobs: - target: aarch64-apple-darwin runner: macos-15 asset_name: java-lsp-proxy-darwin-aarch64.tar.gz + helper_asset_name: java-task-helper-darwin-aarch64.tar.gz - target: x86_64-apple-darwin runner: macos-15-intel asset_name: java-lsp-proxy-darwin-x86_64.tar.gz + helper_asset_name: java-task-helper-darwin-x86_64.tar.gz - target: x86_64-unknown-linux-gnu runner: ubuntu-22.04 asset_name: java-lsp-proxy-linux-x86_64.tar.gz + helper_asset_name: java-task-helper-linux-x86_64.tar.gz - target: aarch64-unknown-linux-gnu runner: ubuntu-22.04-arm asset_name: java-lsp-proxy-linux-aarch64.tar.gz + helper_asset_name: java-task-helper-linux-aarch64.tar.gz - target: x86_64-pc-windows-msvc runner: windows-latest asset_name: java-lsp-proxy-windows-x86_64.zip + helper_asset_name: java-task-helper-windows-x86_64.zip - target: aarch64-pc-windows-msvc runner: windows-11-arm asset_name: java-lsp-proxy-windows-aarch64.zip + helper_asset_name: java-task-helper-windows-aarch64.zip steps: - name: Checkout @@ -48,24 +54,35 @@ jobs: run: cargo build --release --target ${{ matrix.target }} shell: bash - - name: Package binary (Unix) + - name: Build task helper binary + working-directory: task_helper + run: cargo build --release --target ${{ matrix.target }} + shell: bash + + - name: Package binaries (Unix) if: runner.os != 'Windows' run: | tar -czf ${{ matrix.asset_name }} \ -C target/${{ matrix.target }}/release \ java-lsp-proxy + tar -czf ${{ matrix.helper_asset_name }} \ + -C target/${{ matrix.target }}/release \ + java-task-helper - - name: Package binary (Windows) + - name: Package binaries (Windows) if: runner.os == 'Windows' shell: pwsh run: | Compress-Archive -Path target/${{ matrix.target }}/release/java-lsp-proxy.exe -DestinationPath ${{ matrix.asset_name }} + Compress-Archive -Path target/${{ matrix.target }}/release/java-task-helper.exe -DestinationPath ${{ matrix.helper_asset_name }} - - name: Upload artifact + - name: Upload artifacts uses: actions/upload-artifact@v7 with: - name: ${{ matrix.asset_name }} - path: ${{ matrix.asset_name }} + name: binaries-${{ matrix.target }} + path: | + ${{ matrix.asset_name }} + ${{ matrix.helper_asset_name }} retention-days: 1 release: diff --git a/tests/task_verification_test.rs b/tests/task_verification_test.rs index e7fb780..494bbbd 100644 --- a/tests/task_verification_test.rs +++ b/tests/task_verification_test.rs @@ -135,6 +135,67 @@ impl Drop for TestProject { let _ = fs::remove_dir_all(&self.temp_dir); } } + +static BUILD_HELPER: std::sync::OnceLock = std::sync::OnceLock::new(); + +fn ensure_task_helper_built() -> PathBuf { + BUILD_HELPER + .get_or_init(|| { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| std::env::current_dir().unwrap()); + + let mut cmd = std::process::Command::new("cargo"); + cmd.current_dir(&manifest_dir); + cmd.env_remove("CARGO_BUILD_TARGET"); + cmd.args([ + "build", + "-p", + "java-task-helper", + "--bin", + "java-task-helper", + "--message-format=json", + ]); + if !cfg!(debug_assertions) { + cmd.arg("--release"); + } + + let output = cmd + .output() + .expect("failed to run cargo build for java-task-helper"); + assert!( + output.status.success(), + "cargo build -p java-task-helper failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + let mut found_path = None; + for line in stdout.lines() { + if let Ok(val) = serde_json::from_str::(line) { + let is_target_bin = val["target"]["kind"] + .as_array() + .is_some_and(|k| k.iter().any(|kind| kind == "bin")); + if val["reason"] == "compiler-artifact" + && val["target"]["name"] == "java-task-helper" + && is_target_bin + { + found_path = val["filenames"] + .as_array() + .and_then(|files| files.first()) + .and_then(|f| f.as_str()) + .map(PathBuf::from); + } + } + } + + found_path.expect( + "Could not find compiler artifact filename for java-task-helper in cargo build output", + ) + }) + .clone() +} + struct TaskRunner<'a> { project: &'a TestProject, command: String, @@ -171,34 +232,19 @@ impl<'a> TaskRunner<'a> { fn run(self) -> String { let mut cmd = std::process::Command::new("sh"); - // Find the built java-task-helper binary - let task_helper_bin = std::env::current_dir().unwrap(); - - // Try common target paths - let paths = [ - "target/debug/java-task-helper", - "target/x86_64-unknown-linux-gnu/debug/java-task-helper", - "target/aarch64-unknown-linux-gnu/debug/java-task-helper", - ]; - - let mut found_bin = None; - for path in paths { - let p = task_helper_bin.join(path); - if p.exists() { - found_bin = Some(p); - break; - } - } - - let found_bin = found_bin.expect( - "Could not find java-task-helper binary in target directory. Run 'cargo build' first.", - ); + let found_bin = ensure_task_helper_built(); // Create a temporary ZED_EXT directory structure matching what tasks.json expects let zed_ext_base = self.project.temp_dir.join("mock_zed_ext"); let zed_ext_dir = zed_ext_base.join("zed/extensions/work/java/proxy-bin"); fs::create_dir_all(&zed_ext_dir).unwrap(); - fs::copy(&found_bin, zed_ext_dir.join("java-task-helper")).unwrap(); + let dest_bin = zed_ext_dir.join("java-task-helper"); + fs::copy(&found_bin, &dest_bin).unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&dest_bin, fs::Permissions::from_mode(0o755)).unwrap(); + } cmd.arg("-c") .arg(&self.command) @@ -215,6 +261,14 @@ impl<'a> TaskRunner<'a> { } let output = cmd.output().expect("Failed to execute shell command"); + if !output.status.success() { + panic!( + "Shell command failed with status: {:?}\nStdout: {}\nStderr: {}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } String::from_utf8_lossy(&output.stdout).to_string() } } From 020e8e7779e9fa9188e9feccb7c0f182a31af88c Mon Sep 17 00:00:00 2001 From: Christian Pieczewski Date: Wed, 17 Jun 2026 14:09:28 +0200 Subject: [PATCH 5/5] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8c600b2..ad2e48f 100644 --- a/README.md +++ b/README.md @@ -606,8 +606,10 @@ The project includes a `justfile` with common development tasks: | Recipe | Description | |--------|-------------| | `just proxy-build` | Build the proxy binary in debug mode | +| `just proxy-release` | Build the proxy binary in release mode | | `just proxy-install` | Build release proxy and copy it to the extension workdir | | `just task-build` | Build the task helper binary in debug mode | +| `just task-release` | Build the task helper binary in release mode | | `just task-install` | Build release task helper and copy it to the extension workdir | | `just task-test` | Run task helper tests | | `just ext-build` | Build the WASM extension in release mode |