From 5b2437f5a0892d9aa379fc832ed0f2fea9484437 Mon Sep 17 00:00:00 2001 From: mizdra Date: Sun, 18 May 2025 18:42:33 +0900 Subject: [PATCH] add zed extension --- .gitignore | 29 ++++++++- Cargo.toml | 2 + crates/zed/Cargo.toml | 12 ++++ crates/zed/LICENSE | 21 +++++++ crates/zed/README.md | 18 ++++++ crates/zed/extension.toml | 11 ++++ crates/zed/src/lib.rs | 122 ++++++++++++++++++++++++++++++++++++++ docs/get-started.md | 2 +- 8 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 Cargo.toml create mode 100644 crates/zed/Cargo.toml create mode 100644 crates/zed/LICENSE create mode 100644 crates/zed/README.md create mode 100644 crates/zed/extension.toml create mode 100644 crates/zed/src/lib.rs diff --git a/.gitignore b/.gitignore index eb526e23..e8c9c77b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ ### Generated by gibo (https://github.com/simonwhitaker/gibo) -### https://raw.github.com/github/gitignore/ce5da10a3a43c4dd8bd9572eda17c0a37ee0eac1/Node.gitignore +### https://raw.github.com/github/gitignore/274c32303f1f86a42db55e2dab3107e560714010/Node.gitignore # Logs logs @@ -131,5 +131,30 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* +### Generated by gibo (https://github.com/simonwhitaker/gibo) +### https://raw.github.com/github/gitignore/274c32303f1f86a42db55e2dab3107e560714010/Rust.gitignore + +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ -# User +### User +/crates/zed/extension.wasm diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..c66a4d73 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["crates/*"] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml new file mode 100644 index 00000000..492c2995 --- /dev/null +++ b/crates/zed/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "css-modules-kit-zed" +version = "0.0.1" +edition = "2024" +license = "MIT" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +serde = { version = "1.0.219", features = ["derive"] } +zed_extension_api = "0.5.0" diff --git a/crates/zed/LICENSE b/crates/zed/LICENSE new file mode 100644 index 00000000..9ca7c7bc --- /dev/null +++ b/crates/zed/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 mizdra + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/zed/README.md b/crates/zed/README.md new file mode 100644 index 00000000..87596cf7 --- /dev/null +++ b/crates/zed/README.md @@ -0,0 +1,18 @@ +# css-modules-kit-zed + +The Zed extension for CSS Modules Kit + +## Installation + +1. Install "CSS Modules Kit" extension on Zed. +2. Add the following to your `~./config/zed/settings.json` file: + ```json + { + "languages": { + "CSS": { + "language_servers": ["vtsls", "..."] + } + } + } + ``` +3. Restart Zed. diff --git a/crates/zed/extension.toml b/crates/zed/extension.toml new file mode 100644 index 00000000..63a180c8 --- /dev/null +++ b/crates/zed/extension.toml @@ -0,0 +1,11 @@ +id = "css-modules-kit" +name = "CSS Modules Kit" +version = "0.0.1" +schema_version = 1 +authors = ["mizdra "] +description = "The Zed extension for CSS Modules Kit" +repository = "https://github.com/mizdra/css-modules-kit" + +[language_servers.css-modules-kit-language-server] +name = "CSS Modules Kit Language Server" +languages = ["TypeScript", "TSX", "JavaScript", "CSS"] diff --git a/crates/zed/src/lib.rs b/crates/zed/src/lib.rs new file mode 100644 index 00000000..2720d25a --- /dev/null +++ b/crates/zed/src/lib.rs @@ -0,0 +1,122 @@ +use std::collections::HashMap; +use std::env; + +use serde::Deserialize; +use zed_extension_api::{self as zed, Result, serde_json}; + +const TS_PLUGIN_PACKAGE_NAME: &str = "@css-modules-kit/ts-plugin"; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct PackageJson { + #[serde(default)] + dependencies: HashMap, + #[serde(default)] + dev_dependencies: HashMap, +} + +struct CSSModulesKitExtension {} + +impl CSSModulesKitExtension { + fn install_ts_plugin_if_needed(&self) -> Result<()> { + let installed_plugin_version = zed::npm_package_installed_version(TS_PLUGIN_PACKAGE_NAME)?; + let latest_plugin_version = zed::npm_package_latest_version(TS_PLUGIN_PACKAGE_NAME)?; + + if installed_plugin_version.as_ref() != Some(&latest_plugin_version) { + println!("installing {TS_PLUGIN_PACKAGE_NAME}@{latest_plugin_version}"); + zed::npm_install_package(TS_PLUGIN_PACKAGE_NAME, &latest_plugin_version)?; + } else { + println!("ts-plugin already installed"); + } + Ok(()) + } + + fn get_ts_plugin_root_path(&self, worktree: &zed::Worktree) -> Result> { + let package_json = worktree.read_text_file("package.json")?; + let package_json: PackageJson = serde_json::from_str(&package_json) + .map_err(|err| format!("failed to parse package.json: {err}"))?; + + let has_local_plugin = package_json + .dev_dependencies + .contains_key(TS_PLUGIN_PACKAGE_NAME) + || package_json + .dependencies + .contains_key(TS_PLUGIN_PACKAGE_NAME); + + if has_local_plugin { + println!("Using local installation of {TS_PLUGIN_PACKAGE_NAME}"); + return Ok(None); + } + + self.install_ts_plugin_if_needed()?; + + println!("Using global installation of {TS_PLUGIN_PACKAGE_NAME}"); + Ok(Some( + env::current_dir().unwrap().to_string_lossy().to_string(), + )) + } +} + +impl zed::Extension for CSSModulesKitExtension { + fn new() -> Self { + Self {} + } + + fn language_server_command( + &mut self, + _language_server_id: &zed::LanguageServerId, + _worktree: &zed::Worktree, + ) -> Result { + // A language server is not normally required. However, + // Zed does not seem to load ts-plugin unless a language server is configured. + // ref: https://github.com/zed-industries/zed/issues/22410#issuecomment-2815582825 + Ok(zed::Command { + command: zed::node_binary_path()?, + args: vec!["-e".into(), r#"import('data:text/javascript;base64,Y29uc3QgeyBzdGRpbiwgc3Rkb3V0IH0gPSBwcm9jZXNzOwoKc3RkaW4uc2V0RW5jb2RpbmcoInV0ZjgiKTsKCmZ1bmN0aW9uIHNlbmQobXNnKSB7CiAgY29uc3QganNvbiA9IEpTT04uc3RyaW5naWZ5KG1zZyk7CiAgY29uc3QgbGVuID0gQnVmZmVyLmJ5dGVMZW5ndGgoanNvbiwgInV0ZjgiKTsKICBzdGRvdXQud3JpdGUoYENvbnRlbnQtTGVuZ3RoOiAke2xlbn1cclxuXHJcbmApOwogIHN0ZG91dC53cml0ZShqc29uKTsKfQoKbGV0IGJ1ZmZlciA9ICIiOwpsZXQgY29udGVudExlbmd0aCA9IDA7CgpzdGRpbi5vbigiZGF0YSIsIChjaHVuaykgPT4gewogIGJ1ZmZlciArPSBjaHVuazsKCiAgd2hpbGUgKHRydWUpIHsKICAgIGlmIChjb250ZW50TGVuZ3RoID09PSAwKSB7CiAgICAgIGNvbnN0IGhlYWRlckVuZCA9IGJ1ZmZlci5pbmRleE9mKCJcclxuXHJcbiIpOwogICAgICBpZiAoaGVhZGVyRW5kID09PSAtMSkgcmV0dXJuOwoKICAgICAgY29uc3QgaGVhZGVyID0gYnVmZmVyLnNsaWNlKDAsIGhlYWRlckVuZCk7CiAgICAgIGNvbnN0IG1hdGNoID0gaGVhZGVyLm1hdGNoKC9Db250ZW50LUxlbmd0aDogKFxkKykvaSk7CiAgICAgIGlmICghbWF0Y2gpIHJldHVybjsgLy8gSWdub3JlIG1hbGZvcm1lZCBoZWFkZXJzCgogICAgICBjb250ZW50TGVuZ3RoID0gcGFyc2VJbnQobWF0Y2hbMV0sIDEwKTsKICAgICAgYnVmZmVyID0gYnVmZmVyLnNsaWNlKGhlYWRlckVuZCArIDQpOwogICAgfQoKICAgIC8vIFdhaXQgZm9yIHRoZSBmdWxsIG1lc3NhZ2UgYm9keQogICAgaWYgKGJ1ZmZlci5sZW5ndGggPCBjb250ZW50TGVuZ3RoKSByZXR1cm47CgogICAgY29uc3QgbWVzc2FnZUpzb24gPSBidWZmZXIuc2xpY2UoMCwgY29udGVudExlbmd0aCk7CiAgICBidWZmZXIgPSBidWZmZXIuc2xpY2UoY29udGVudExlbmd0aCk7CiAgICBjb250ZW50TGVuZ3RoID0gMDsgLy8gUmVzZXQgZm9yIHRoZSBuZXh0IG1lc3NhZ2UKCiAgICB0cnkgewogICAgICBjb25zdCByZXEgPSBKU09OLnBhcnNlKG1lc3NhZ2VKc29uKTsKCiAgICAgIGlmIChyZXEubWV0aG9kID09PSAiaW5pdGlhbGl6ZSIpIHsKICAgICAgICBzZW5kKHsKICAgICAgICAgIGpzb25ycGM6ICIyLjAiLAogICAgICAgICAgaWQ6IHJlcS5pZCwKICAgICAgICAgIHJlc3VsdDogewogICAgICAgICAgICBjYXBhYmlsaXRpZXM6IHt9LCAvLyBNaW5pbWFsIGNhcGFiaWxpdGllcwogICAgICAgICAgfSwKICAgICAgICB9KTsKICAgICAgfSBlbHNlIGlmIChyZXEubWV0aG9kID09PSAic2h1dGRvd24iKSB7CiAgICAgICAgLy8gUmVzcG9uZCB0byBzaHV0ZG93biBiZWZvcmUgZXhpdGluZwogICAgICAgIHNlbmQoewogICAgICAgICAganNvbnJwYzogIjIuMCIsCiAgICAgICAgICBpZDogcmVxLmlkLAogICAgICAgICAgcmVzdWx0OiBudWxsLAogICAgICAgIH0pOwogICAgICAgIC8vIFRoZSBjbGllbnQgc2hvdWxkIHNlbmQgJ2V4aXQnIGFmdGVyIHRoaXMsIGJ1dCB3ZSBjYW4gZXhpdCBoZXJlIGlmIG5lZWRlZCwKICAgICAgICAvLyB0aG91Z2ggdGVjaG5pY2FsbHkgTFNQIHNwZWMgc3VnZ2VzdHMgd2FpdGluZyBmb3IgJ2V4aXQnCiAgICAgICAgLy8gcHJvY2Vzcy5leGl0KDApOwogICAgICB9IGVsc2UgaWYgKHJlcS5tZXRob2QgPT09ICJleGl0IikgewogICAgICAgIHByb2Nlc3MuZXhpdCgwKTsgLy8gRXhpdCBjbGVhbmx5CiAgICAgIH0gZWxzZSBpZiAocmVxLmlkICE9PSB1bmRlZmluZWQpIHsKICAgICAgICAvLyBSZXNwb25kIHdpdGggYW4gZXJyb3IgZm9yIHVuc3VwcG9ydGVkIHJlcXVlc3RzIHRoYXQgaGF2ZSBhbiBJRAogICAgICAgIHNlbmQoewogICAgICAgICAganNvbnJwYzogIjIuMCIsCiAgICAgICAgICBpZDogcmVxLmlkLAogICAgICAgICAgZXJyb3I6IHsKICAgICAgICAgICAgY29kZTogLTMyNjAxLCAvLyBNZXRob2ROb3RGb3VuZAogICAgICAgICAgICBtZXNzYWdlOiBgTWV0aG9kIG5vdCBmb3VuZDogJHtyZXEubWV0aG9kfWAsCiAgICAgICAgICB9LAogICAgICAgIH0pOwogICAgICB9CiAgICAgIC8vIElnbm9yZSBub3RpZmljYXRpb25zIChyZXF1ZXN0cyB3aXRob3V0IGFuIElEKSBvdGhlciB0aGFuICdleGl0JwogICAgfSBjYXRjaCAoZXJyb3IpIHsKICAgICAgLy8gSGFuZGxlIEpTT04gcGFyc2luZyBlcnJvcnMgb3Igb3RoZXIgaXNzdWVzCiAgICAgIGNvbnNvbGUuZXJyb3IoIkVycm9yIHByb2Nlc3NpbmcgbWVzc2FnZToiLCBlcnJvcik7CiAgICAgIC8vIE9wdGlvbmFsbHkgc2VuZCBhIGdlbmVyaWMgZXJyb3IgcmVzcG9uc2UgaWYgcG9zc2libGUKICAgIH0KICB9Cn0pOwoKc3RkaW4ub24oImVuZCIsICgpID0+IHsKICAvLyBIYW5kbGUgc3RyZWFtIGVuZCBpZiBuZWNlc3NhcnkKICBwcm9jZXNzLmV4aXQoMCk7Cn0pOwoKLy8gSGFuZGxlIHBvdGVudGlhbCBlcnJvcnMgb24gc3RyZWFtcwpzdGRpbi5vbigiZXJyb3IiLCAoZXJyKSA9PiB7CiAgY29uc29sZS5lcnJvcigiU3RkaW4gRXJyb3I6IiwgZXJyKTsKICBwcm9jZXNzLmV4aXQoMSk7Cn0pOwoKc3Rkb3V0Lm9uKCJlcnJvciIsIChlcnIpID0+IHsKICBjb25zb2xlLmVycm9yKCJTdGRvdXQgRXJyb3I6IiwgZXJyKTsKICBwcm9jZXNzLmV4aXQoMSk7Cn0pOwo=')"#.to_string()], + env: Default::default(), + }) + } + + fn language_server_additional_initialization_options( + &mut self, + _language_server_id: &zed::LanguageServerId, + target_language_server_id: &zed::LanguageServerId, + worktree: &zed::Worktree, + ) -> Result> { + match target_language_server_id.as_ref() { + "typescript-language-server" => Ok(Some(serde_json::json!({ + "plugins": [{ + "name": TS_PLUGIN_PACKAGE_NAME, + "location": self.get_ts_plugin_root_path(worktree)?.unwrap_or_else(|| worktree.root_path()), + "languages": ["css"] + }], + }))), + _ => Ok(None), + } + } + + fn language_server_additional_workspace_configuration( + &mut self, + _language_server_id: &zed::LanguageServerId, + target_language_server_id: &zed::LanguageServerId, + worktree: &zed::Worktree, + ) -> Result> { + match target_language_server_id.as_ref() { + "vtsls" => Ok(Some(serde_json::json!({ + "vtsls": { + "tsserver": { + "globalPlugins": [{ + "name": TS_PLUGIN_PACKAGE_NAME, + "location": self.get_ts_plugin_root_path(worktree)?.unwrap_or_else(|| worktree.root_path()), + "enableForWorkspaceTypeScriptVersions": true, + "languages": ["css"] + }] + } + }, + }))), + _ => Ok(None), + } + } +} + +zed::register_extension!(CSSModulesKitExtension); diff --git a/docs/get-started.md b/docs/get-started.md index 2debb677..6733f7d0 100644 --- a/docs/get-started.md +++ b/docs/get-started.md @@ -11,7 +11,7 @@ To enable CSS Modules language features in your editor, you need to install [`@c - For Emacs: - Not yet supported - For Zed: - - Not yet supported + - See [crates/zed/README.md](../crates/zed/README.md) - For WebStorm: - Not yet supported - For StackBlitz Codeflow: