diff --git a/extensions/glsl/Cargo.toml b/extensions/glsl/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..6dddeea35813c662e57ae5f5c16f89fe921fc223 --- /dev/null +++ b/extensions/glsl/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "zed_glsl" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/glsl.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = "0.1.0" diff --git a/extensions/glsl/LICENSE-APACHE b/extensions/glsl/LICENSE-APACHE new file mode 120000 index 0000000000000000000000000000000000000000..1cd601d0a3affae83854be02a0afdec3b7a9ec4d --- /dev/null +++ b/extensions/glsl/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/glsl/extension.toml b/extensions/glsl/extension.toml new file mode 100644 index 0000000000000000000000000000000000000000..7cf9e218c80dd39b2f4f1eb3d0fe75d6de8615f1 --- /dev/null +++ b/extensions/glsl/extension.toml @@ -0,0 +1,15 @@ +id = "glsl" +name = "GLSL" +description = "GLSL support." +version = "0.1.0" +schema_version = 1 +authors = ["Mikayla Maki "] +repository = "https://github.com/zed-industries/zed" + +[language_servers.glsl_analyzer] +name = "GLSL Analyzer LSP" +language = "GLSL" + +[grammars.glsl] +repository = "https://github.com/theHamsta/tree-sitter-glsl" +commit = "31064ce53385150f894a6c72d61b94076adf640a" diff --git a/extensions/glsl/languages/glsl/config.toml b/extensions/glsl/languages/glsl/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..0c71419c91e40f4b5fc65c10c882ac5c542a080c --- /dev/null +++ b/extensions/glsl/languages/glsl/config.toml @@ -0,0 +1,20 @@ +name = "GLSL" +grammar = "glsl" +path_suffixes = [ + # Traditional rasterization pipeline shaders + "vert", "frag", "tesc", "tese", "geom", + # Compute shaders + "comp", + # Ray tracing pipeline shaders + "rgen", "rint", "rahit", "rchit", "rmiss", "rcall", + # Other + "glsl" + ] +first_line_pattern = '^#version \d+' +line_comments = ["// "] +block_comment = { start = "/* ", prefix = "* ", end = "*/", tab_size = 1 } +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, +] diff --git a/extensions/glsl/languages/glsl/highlights.scm b/extensions/glsl/languages/glsl/highlights.scm new file mode 100644 index 0000000000000000000000000000000000000000..09f94d4fb587963254c9bc31ec25b66a0e1e4323 --- /dev/null +++ b/extensions/glsl/languages/glsl/highlights.scm @@ -0,0 +1,117 @@ +"break" @keyword +"case" @keyword +"const" @keyword +"continue" @keyword +"default" @keyword +"do" @keyword +"else" @keyword +"enum" @keyword +"extern" @keyword +"for" @keyword +"if" @keyword +"inline" @keyword +"return" @keyword +"sizeof" @keyword +"static" @keyword +"struct" @keyword +"switch" @keyword +"typedef" @keyword +"union" @keyword +"volatile" @keyword +"while" @keyword + +"#define" @keyword +"#elif" @keyword +"#else" @keyword +"#endif" @keyword +"#if" @keyword +"#ifdef" @keyword +"#ifndef" @keyword +"#include" @keyword +(preproc_directive) @keyword + +"--" @operator +"-" @operator +"-=" @operator +"->" @operator +"=" @operator +"!=" @operator +"*" @operator +"&" @operator +"&&" @operator +"+" @operator +"++" @operator +"+=" @operator +"<" @operator +"==" @operator +">" @operator +"||" @operator + +"." @delimiter +";" @delimiter + +(string_literal) @string +(system_lib_string) @string + +(null) @constant +(number_literal) @number +(char_literal) @number + +(identifier) @variable + +(field_identifier) @property +(statement_identifier) @label +(type_identifier) @type +(primitive_type) @type +(sized_type_specifier) @type + +(call_expression + function: (identifier) @function) +(call_expression + function: (field_expression + field: (field_identifier) @function)) +(function_declarator + declarator: (identifier) @function) +(preproc_function_def + name: (identifier) @function.special) + +((identifier) @constant + (#match? @constant "^[A-Z][A-Z\\d_]*$")) + +(comment) @comment + +[ + "in" + "out" + "inout" + "uniform" + "shared" + "layout" + "attribute" + "varying" + "buffer" + "coherent" + "readonly" + "writeonly" + "precision" + "highp" + "mediump" + "lowp" + "centroid" + "sample" + "patch" + "smooth" + "flat" + "noperspective" + "invariant" + "precise" +] @type.qualifier + +"subroutine" @keyword.function + +(extension_storage_class) @storageclass + +( + (identifier) @variable.builtin + (#match? @variable.builtin "^gl_") +) diff --git a/extensions/glsl/src/glsl.rs b/extensions/glsl/src/glsl.rs new file mode 100644 index 0000000000000000000000000000000000000000..77865564cc1efead12327715aa77c5a0df2965af --- /dev/null +++ b/extensions/glsl/src/glsl.rs @@ -0,0 +1,131 @@ +use std::fs; +use zed::settings::LspSettings; +use zed_extension_api::{self as zed, LanguageServerId, Result, serde_json}; + +struct GlslExtension { + cached_binary_path: Option, +} + +impl GlslExtension { + fn language_server_binary_path( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + if let Some(path) = worktree.which("glsl_analyzer") { + return Ok(path); + } + + if let Some(path) = &self.cached_binary_path + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) + { + return Ok(path.clone()); + } + + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let release = zed::latest_github_release( + "nolanderc/glsl_analyzer", + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let (platform, arch) = zed::current_platform(); + let asset_name = format!( + "{arch}-{os}.zip", + arch = match arch { + zed::Architecture::Aarch64 => "aarch64", + zed::Architecture::X86 => "x86", + zed::Architecture::X8664 => "x86_64", + }, + os = match platform { + zed::Os::Mac => "macos", + zed::Os::Linux => "linux-musl", + zed::Os::Windows => "windows", + } + ); + + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; + + let version_dir = format!("glsl_analyzer-{}", release.version); + fs::create_dir_all(&version_dir) + .map_err(|err| format!("failed to create directory '{version_dir}': {err}"))?; + let binary_path = format!("{version_dir}/bin/glsl_analyzer"); + + if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + + zed::download_file( + &asset.download_url, + &version_dir, + match platform { + zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::Zip, + zed::Os::Windows => zed::DownloadedFileType::Zip, + }, + ) + .map_err(|e| format!("failed to download file: {e}"))?; + + zed::make_file_executable(&binary_path)?; + + let entries = + fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; + for entry in entries { + let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; + if entry.file_name().to_str() != Some(&version_dir) { + fs::remove_dir_all(entry.path()).ok(); + } + } + } + + self.cached_binary_path = Some(binary_path.clone()); + Ok(binary_path) + } +} + +impl zed::Extension for GlslExtension { + fn new() -> Self { + Self { + cached_binary_path: None, + } + } + + fn language_server_command( + &mut self, + language_server_id: &zed::LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + Ok(zed::Command { + command: self.language_server_binary_path(language_server_id, worktree)?, + args: vec![], + env: Default::default(), + }) + } + + fn language_server_workspace_configuration( + &mut self, + _language_server_id: &zed::LanguageServerId, + worktree: &zed::Worktree, + ) -> Result> { + let settings = LspSettings::for_worktree("glsl_analyzer", worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.settings) + .unwrap_or_default(); + + Ok(Some(serde_json::json!({ + "glsl_analyzer": settings + }))) + } +} + +zed::register_extension!(GlslExtension); diff --git a/extensions/html/Cargo.toml b/extensions/html/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..2c89f86cb450b7ea8476bffdff003a94b137d213 --- /dev/null +++ b/extensions/html/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "zed_html" +version = "0.3.0" +edition.workspace = true +publish.workspace = true +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/html.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = "0.7.0" diff --git a/extensions/html/LICENSE-APACHE b/extensions/html/LICENSE-APACHE new file mode 120000 index 0000000000000000000000000000000000000000..1cd601d0a3affae83854be02a0afdec3b7a9ec4d --- /dev/null +++ b/extensions/html/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/html/extension.toml b/extensions/html/extension.toml new file mode 100644 index 0000000000000000000000000000000000000000..68ab0e4b9d3f56fca17cbd518d5990edc2ec711a --- /dev/null +++ b/extensions/html/extension.toml @@ -0,0 +1,19 @@ +id = "html" +name = "HTML" +description = "HTML support." +version = "0.3.0" +schema_version = 1 +authors = ["Isaac Clayton "] +repository = "https://github.com/zed-industries/zed" + +[language_servers.vscode-html-language-server] +name = "vscode-html-language-server" +language = "HTML" + +[language_servers.vscode-html-language-server.language_ids] +"HTML" = "html" +"CSS" = "css" + +[grammars.html] +repository = "https://github.com/tree-sitter/tree-sitter-html" +commit = "bfa075d83c6b97cd48440b3829ab8d24a2319809" diff --git a/extensions/html/languages/html/brackets.scm b/extensions/html/languages/html/brackets.scm new file mode 100644 index 0000000000000000000000000000000000000000..53d6a6bb234e28db21581906ea42e6384f872c9a --- /dev/null +++ b/extensions/html/languages/html/brackets.scm @@ -0,0 +1,5 @@ +("<" @open "/>" @close) +("" @close) +("<" @open ">" @close) +(("\"" @open "\"" @close) (#set! rainbow.exclude)) +((element (start_tag) @open (end_tag) @close) (#set! newline.only) (#set! rainbow.exclude)) diff --git a/extensions/html/languages/html/config.toml b/extensions/html/languages/html/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..fc7d5571981e99fdb8bde68441821f07a1a94889 --- /dev/null +++ b/extensions/html/languages/html/config.toml @@ -0,0 +1,19 @@ +name = "HTML" +grammar = "html" +path_suffixes = ["html", "htm", "shtml"] +autoclose_before = ">})" +block_comment = { start = "", tab_size = 0 } +wrap_characters = { start_prefix = "<", start_suffix = ">", end_prefix = "" } +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, + { start = "<", end = ">", close = false, newline = true, not_in = ["comment", "string"] }, + { start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] }, +] +completion_query_characters = ["-"] +prettier_parser_name = "html" + +[overrides.default] +linked_edit_characters = ["-"] diff --git a/extensions/html/languages/html/highlights.scm b/extensions/html/languages/html/highlights.scm new file mode 100644 index 0000000000000000000000000000000000000000..1cc0601b764554c37ac28a45e988997b267b32fc --- /dev/null +++ b/extensions/html/languages/html/highlights.scm @@ -0,0 +1,19 @@ +(tag_name) @tag +(doctype) @tag.doctype +(attribute_name) @attribute +[ + "\"" + "'" + (attribute_value) +] @string +(comment) @comment + +"=" @punctuation.delimiter.html + +[ + "<" + ">" + "" +] @punctuation.bracket.html diff --git a/extensions/html/languages/html/indents.scm b/extensions/html/languages/html/indents.scm new file mode 100644 index 0000000000000000000000000000000000000000..436663dba3e1993c84e151f09c581844fdcb977a --- /dev/null +++ b/extensions/html/languages/html/indents.scm @@ -0,0 +1,6 @@ +(start_tag ">" @end) @indent +(self_closing_tag "/>" @end) @indent + +(element + (start_tag) @start + (end_tag)? @end) @indent diff --git a/extensions/html/languages/html/injections.scm b/extensions/html/languages/html/injections.scm new file mode 100644 index 0000000000000000000000000000000000000000..525b3efe29dca541afc8829dd41ff217f48439c3 --- /dev/null +++ b/extensions/html/languages/html/injections.scm @@ -0,0 +1,21 @@ +((comment) @injection.content + (#set! injection.language "comment") +) + +(script_element + (raw_text) @injection.content + (#set! injection.language "javascript")) + +(style_element + (raw_text) @injection.content + (#set! injection.language "css")) + +(attribute + (attribute_name) @_attribute_name (#match? @_attribute_name "^style$") + (quoted_attribute_value (attribute_value) @injection.content) + (#set! injection.language "css")) + +(attribute + (attribute_name) @_attribute_name (#match? @_attribute_name "^on[a-z]+$") + (quoted_attribute_value (attribute_value) @injection.content) + (#set! injection.language "javascript")) diff --git a/extensions/html/languages/html/outline.scm b/extensions/html/languages/html/outline.scm new file mode 100644 index 0000000000000000000000000000000000000000..e7f9dc4fab01b89e68a2b668425fc7655b7d275e --- /dev/null +++ b/extensions/html/languages/html/outline.scm @@ -0,0 +1,5 @@ +(comment) @annotation + +(element + (start_tag + (tag_name) @name)) @item diff --git a/extensions/html/languages/html/overrides.scm b/extensions/html/languages/html/overrides.scm new file mode 100644 index 0000000000000000000000000000000000000000..434f610e70242be8589a9f58cc7fd4704d5d9296 --- /dev/null +++ b/extensions/html/languages/html/overrides.scm @@ -0,0 +1,7 @@ +(comment) @comment +(quoted_attribute_value) @string + +[ + (start_tag) + (end_tag) +] @default diff --git a/extensions/html/src/html.rs b/extensions/html/src/html.rs new file mode 100644 index 0000000000000000000000000000000000000000..337689ebddd427769ab985ad82512f76b601e67c --- /dev/null +++ b/extensions/html/src/html.rs @@ -0,0 +1,115 @@ +use std::{env, fs}; +use zed::settings::LspSettings; +use zed_extension_api::{self as zed, LanguageServerId, Result, serde_json::json}; + +const BINARY_NAME: &str = "vscode-html-language-server"; +const SERVER_PATH: &str = + "node_modules/@zed-industries/vscode-langservers-extracted/bin/vscode-html-language-server"; +const PACKAGE_NAME: &str = "@zed-industries/vscode-langservers-extracted"; + +struct HtmlExtension { + cached_binary_path: Option, +} + +impl HtmlExtension { + fn server_exists(&self) -> bool { + fs::metadata(SERVER_PATH).is_ok_and(|stat| stat.is_file()) + } + + fn server_script_path(&mut self, language_server_id: &LanguageServerId) -> Result { + let server_exists = self.server_exists(); + if self.cached_binary_path.is_some() && server_exists { + return Ok(SERVER_PATH.to_string()); + } + + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let version = zed::npm_package_latest_version(PACKAGE_NAME)?; + + if !server_exists + || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version) + { + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + let result = zed::npm_install_package(PACKAGE_NAME, &version); + match result { + Ok(()) => { + if !self.server_exists() { + Err(format!( + "installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'", + ))?; + } + } + Err(error) => { + if !self.server_exists() { + Err(error)?; + } + } + } + } + Ok(SERVER_PATH.to_string()) + } +} + +impl zed::Extension for HtmlExtension { + fn new() -> Self { + Self { + cached_binary_path: None, + } + } + + fn language_server_command( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + let server_path = if let Some(path) = worktree.which(BINARY_NAME) { + return Ok(zed::Command { + command: path, + args: vec!["--stdio".to_string()], + env: Default::default(), + }); + } else { + let server_path = self.server_script_path(language_server_id)?; + env::current_dir() + .unwrap() + .join(&server_path) + .to_string_lossy() + .to_string() + }; + self.cached_binary_path = Some(server_path.clone()); + + Ok(zed::Command { + command: zed::node_binary_path()?, + args: vec![server_path, "--stdio".to_string()], + env: Default::default(), + }) + } + + fn language_server_workspace_configuration( + &mut self, + server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result> { + let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.settings) + .unwrap_or_default(); + Ok(Some(settings)) + } + + fn language_server_initialization_options( + &mut self, + _server_id: &LanguageServerId, + _worktree: &zed_extension_api::Worktree, + ) -> Result> { + let initialization_options = json!({"provideFormatter": true }); + Ok(Some(initialization_options)) + } +} + +zed::register_extension!(HtmlExtension); diff --git a/extensions/proto/Cargo.toml b/extensions/proto/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..c3606f668aa01d7a8baa20d54d073a7004a6f8c0 --- /dev/null +++ b/extensions/proto/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "zed_proto" +version = "0.3.0" +edition.workspace = true +publish.workspace = true +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/proto.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = "0.7.0" diff --git a/extensions/proto/LICENSE-APACHE b/extensions/proto/LICENSE-APACHE new file mode 120000 index 0000000000000000000000000000000000000000..1cd601d0a3affae83854be02a0afdec3b7a9ec4d --- /dev/null +++ b/extensions/proto/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/proto/extension.toml b/extensions/proto/extension.toml new file mode 100644 index 0000000000000000000000000000000000000000..13c4054eef083e131ab311b1ec6e5a63aff545d8 --- /dev/null +++ b/extensions/proto/extension.toml @@ -0,0 +1,24 @@ +id = "proto" +name = "Proto" +description = "Protocol Buffers support." +version = "0.3.0" +schema_version = 1 +authors = ["Zed Industries "] +repository = "https://github.com/zed-industries/zed" + +[grammars.proto] +repository = "https://github.com/coder3101/tree-sitter-proto" +commit = "a6caac94b5aa36b322b5b70040d5b67132f109d0" + + +[language_servers.buf] +name = "Buf" +languages = ["Proto"] + +[language_servers.protobuf-language-server] +name = "Protobuf Language Server" +languages = ["Proto"] + +[language_servers.protols] +name = "Protols" +languages = ["Proto"] diff --git a/extensions/proto/languages/proto/config.toml b/extensions/proto/languages/proto/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..6d25c23da5dfaaf9c4baf36bba916d7d4d001b8e --- /dev/null +++ b/extensions/proto/languages/proto/config.toml @@ -0,0 +1,13 @@ +name = "Proto" +grammar = "proto" +path_suffixes = ["proto"] +line_comments = ["// "] +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] }, + { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] }, +] diff --git a/extensions/proto/languages/proto/highlights.scm b/extensions/proto/languages/proto/highlights.scm new file mode 100644 index 0000000000000000000000000000000000000000..5d0a513bee1ca4597c649fbe8e6ca31a00fe9dff --- /dev/null +++ b/extensions/proto/languages/proto/highlights.scm @@ -0,0 +1,61 @@ +[ + "syntax" + "package" + "option" + "optional" + "import" + "service" + "rpc" + "returns" + "message" + "enum" + "oneof" + "repeated" + "reserved" + "to" +] @keyword + +[ + (key_type) + (type) + (message_name) + (enum_name) + (service_name) + (rpc_name) + (message_or_enum_type) +] @type + +(enum_field + (identifier) @constant) + +[ + (string) + "\"proto3\"" +] @string + +(int_lit) @number + +[ + (true) + (false) +] @boolean + +(comment) @comment + +[ + "(" + ")" + "[" + "]" + "{" + "}" + "<" + ">" +] @punctuation.bracket + +[ + ";" + "," +] @punctuation.delimiter + +"=" @operator diff --git a/extensions/proto/languages/proto/indents.scm b/extensions/proto/languages/proto/indents.scm new file mode 100644 index 0000000000000000000000000000000000000000..acb44a5e1e617cc0d735228af022129c0b39d561 --- /dev/null +++ b/extensions/proto/languages/proto/indents.scm @@ -0,0 +1,3 @@ +(_ "{" "}" @end) @indent +(_ "[" "]" @end) @indent +(_ "(" ")" @end) @indent diff --git a/extensions/proto/languages/proto/outline.scm b/extensions/proto/languages/proto/outline.scm new file mode 100644 index 0000000000000000000000000000000000000000..f90b1bae33effade920bf8f2c76d7f2d187f1d8e --- /dev/null +++ b/extensions/proto/languages/proto/outline.scm @@ -0,0 +1,19 @@ +(message + "message" @context + (message_name + (identifier) @name)) @item + +(service + "service" @context + (service_name + (identifier) @name)) @item + +(rpc + "rpc" @context + (rpc_name + (identifier) @name)) @item + +(enum + "enum" @context + (enum_name + (identifier) @name)) @item diff --git a/extensions/proto/languages/proto/textobjects.scm b/extensions/proto/languages/proto/textobjects.scm new file mode 100644 index 0000000000000000000000000000000000000000..90ea84282da39df8a2023108c367c3ef76a0ef9a --- /dev/null +++ b/extensions/proto/languages/proto/textobjects.scm @@ -0,0 +1,18 @@ +(message (message_body + "{" + (_)* @class.inside + "}")) @class.around +(enum (enum_body + "{" + (_)* @class.inside + "}")) @class.around +(service + "service" + (_) + "{" + (_)* @class.inside + "}") @class.around + +(rpc) @function.around + +(comment)+ @comment.around diff --git a/extensions/proto/src/language_servers.rs b/extensions/proto/src/language_servers.rs new file mode 100644 index 0000000000000000000000000000000000000000..47a5e72d8aadf5d0286667148f0a7dd95fea10ba --- /dev/null +++ b/extensions/proto/src/language_servers.rs @@ -0,0 +1,8 @@ +mod buf; +mod protobuf_language_server; +mod protols; +mod util; + +pub(crate) use buf::*; +pub(crate) use protobuf_language_server::*; +pub(crate) use protols::*; diff --git a/extensions/proto/src/language_servers/buf.rs b/extensions/proto/src/language_servers/buf.rs new file mode 100644 index 0000000000000000000000000000000000000000..92106298d3d1deb6ed2b0f4194ab09321fa09552 --- /dev/null +++ b/extensions/proto/src/language_servers/buf.rs @@ -0,0 +1,114 @@ +use std::fs; + +use zed_extension_api::{ + self as zed, Architecture, DownloadedFileType, GithubReleaseOptions, Os, Result, + settings::LspSettings, +}; + +use crate::language_servers::util; + +pub(crate) struct BufLsp { + cached_binary_path: Option, +} + +impl BufLsp { + pub(crate) const SERVER_NAME: &str = "buf"; + + pub(crate) fn new() -> Self { + BufLsp { + cached_binary_path: None, + } + } + + pub(crate) fn language_server_binary( + &mut self, + worktree: &zed::Worktree, + ) -> Result { + let binary_settings = LspSettings::for_worktree(Self::SERVER_NAME, worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.binary); + + let args = binary_settings + .as_ref() + .and_then(|binary_settings| binary_settings.arguments.clone()) + .unwrap_or_else(|| ["lsp", "serve"].map(ToOwned::to_owned).into()); + + if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) { + return Ok(zed::Command { + command: path, + args, + env: Default::default(), + }); + } else if let Some(path) = self.cached_binary_path.clone() { + return Ok(zed::Command { + command: path, + args, + env: Default::default(), + }); + } else if let Some(path) = worktree.which(Self::SERVER_NAME) { + self.cached_binary_path = Some(path.clone()); + return Ok(zed::Command { + command: path, + args, + env: Default::default(), + }); + } + + let latest_release = zed::latest_github_release( + "bufbuild/buf", + GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let (os, arch) = zed::current_platform(); + + let release_suffix = match (os, arch) { + (Os::Mac, Architecture::Aarch64) => "Darwin-arm64", + (Os::Mac, Architecture::X8664) => "Darwin-x86_64", + (Os::Linux, Architecture::Aarch64) => "Linux-aarch64", + (Os::Linux, Architecture::X8664) => "Linux-x86_64", + (Os::Windows, Architecture::Aarch64) => "Windows-arm64.exe", + (Os::Windows, Architecture::X8664) => "Windows-x86_64.exe", + _ => { + return Err("Platform and architecture not supported by buf CLI".to_string()); + } + }; + + let release_name = format!("buf-{release_suffix}"); + + let version_dir = format!("{}-{}", Self::SERVER_NAME, latest_release.version); + fs::create_dir_all(&version_dir).map_err(|_| "Could not create directory")?; + + let binary_path = format!("{version_dir}/buf"); + + let download_target = latest_release + .assets + .into_iter() + .find(|asset| asset.name == release_name) + .ok_or_else(|| { + format!( + "Could not find asset with name {} in buf CLI release", + &release_name + ) + })?; + + zed::download_file( + &download_target.download_url, + &binary_path, + DownloadedFileType::Uncompressed, + )?; + zed::make_file_executable(&binary_path)?; + + util::remove_outdated_versions(Self::SERVER_NAME, &version_dir)?; + + self.cached_binary_path = Some(binary_path.clone()); + + Ok(zed::Command { + command: binary_path, + args, + env: Default::default(), + }) + } +} diff --git a/extensions/proto/src/language_servers/protobuf_language_server.rs b/extensions/proto/src/language_servers/protobuf_language_server.rs new file mode 100644 index 0000000000000000000000000000000000000000..f4b13077f73182dd0c30486ee274ade26ec1e40e --- /dev/null +++ b/extensions/proto/src/language_servers/protobuf_language_server.rs @@ -0,0 +1,52 @@ +use zed_extension_api::{self as zed, Result, settings::LspSettings}; + +pub(crate) struct ProtobufLanguageServer { + cached_binary_path: Option, +} + +impl ProtobufLanguageServer { + pub(crate) const SERVER_NAME: &str = "protobuf-language-server"; + + pub(crate) fn new() -> Self { + ProtobufLanguageServer { + cached_binary_path: None, + } + } + + pub(crate) fn language_server_binary( + &mut self, + worktree: &zed::Worktree, + ) -> Result { + let binary_settings = LspSettings::for_worktree(Self::SERVER_NAME, worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.binary); + + let args = binary_settings + .as_ref() + .and_then(|binary_settings| binary_settings.arguments.clone()) + .unwrap_or_else(|| vec!["-logs".into(), "".into()]); + + if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) { + Ok(zed::Command { + command: path, + args, + env: Default::default(), + }) + } else if let Some(path) = self.cached_binary_path.clone() { + Ok(zed::Command { + command: path, + args, + env: Default::default(), + }) + } else if let Some(path) = worktree.which(Self::SERVER_NAME) { + self.cached_binary_path = Some(path.clone()); + Ok(zed::Command { + command: path, + args, + env: Default::default(), + }) + } else { + Err(format!("{} not found in PATH", Self::SERVER_NAME)) + } + } +} diff --git a/extensions/proto/src/language_servers/protols.rs b/extensions/proto/src/language_servers/protols.rs new file mode 100644 index 0000000000000000000000000000000000000000..90d365eae7d99ccb27d60f774ed700b47323d8d0 --- /dev/null +++ b/extensions/proto/src/language_servers/protols.rs @@ -0,0 +1,113 @@ +use zed_extension_api::{ + self as zed, Architecture, DownloadedFileType, GithubReleaseOptions, Os, Result, + settings::LspSettings, +}; + +use crate::language_servers::util; + +pub(crate) struct ProtoLs { + cached_binary_path: Option, +} + +impl ProtoLs { + pub(crate) const SERVER_NAME: &str = "protols"; + + pub(crate) fn new() -> Self { + ProtoLs { + cached_binary_path: None, + } + } + + pub(crate) fn language_server_binary( + &mut self, + worktree: &zed::Worktree, + ) -> Result { + let binary_settings = LspSettings::for_worktree(Self::SERVER_NAME, worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.binary); + + let args = binary_settings + .as_ref() + .and_then(|binary_settings| binary_settings.arguments.clone()) + .unwrap_or_default(); + + let env = worktree.shell_env(); + + if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) { + return Ok(zed::Command { + command: path, + args, + env, + }); + } else if let Some(path) = self.cached_binary_path.clone() { + return Ok(zed::Command { + command: path, + args, + env, + }); + } else if let Some(path) = worktree.which(Self::SERVER_NAME) { + self.cached_binary_path = Some(path.clone()); + return Ok(zed::Command { + command: path, + args, + env, + }); + } + + let latest_release = zed::latest_github_release( + "coder3101/protols", + GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let (os, arch) = zed::current_platform(); + + let release_suffix = match (os, arch) { + (Os::Mac, Architecture::Aarch64) => "aarch64-apple-darwin.tar.gz", + (Os::Mac, Architecture::X8664) => "x86_64-apple-darwin.tar.gz", + (Os::Linux, Architecture::Aarch64) => "aarch64-unknown-linux-gnu.tar.gz", + (Os::Linux, Architecture::X8664) => "x86_64-unknown-linux-gnu.tar.gz", + (Os::Windows, Architecture::X8664) => "x86_64-pc-windows-msvc.zip", + _ => { + return Err("Platform and architecture not supported by Protols".to_string()); + } + }; + + let release_name = format!("protols-{release_suffix}"); + + let file_type = if os == Os::Windows { + DownloadedFileType::Zip + } else { + DownloadedFileType::GzipTar + }; + + let version_dir = format!("{}-{}", Self::SERVER_NAME, latest_release.version); + let binary_path = format!("{version_dir}/protols"); + + let download_target = latest_release + .assets + .into_iter() + .find(|asset| asset.name == release_name) + .ok_or_else(|| { + format!( + "Could not find asset with name {} in Protols release", + &release_name + ) + })?; + + zed::download_file(&download_target.download_url, &version_dir, file_type)?; + zed::make_file_executable(&binary_path)?; + + util::remove_outdated_versions(Self::SERVER_NAME, &version_dir)?; + + self.cached_binary_path = Some(binary_path.clone()); + + Ok(zed::Command { + command: binary_path, + args, + env, + }) + } +} diff --git a/extensions/proto/src/language_servers/util.rs b/extensions/proto/src/language_servers/util.rs new file mode 100644 index 0000000000000000000000000000000000000000..3036c9bc3aaf9cc3fccd462fe0ad70aa31892012 --- /dev/null +++ b/extensions/proto/src/language_servers/util.rs @@ -0,0 +1,19 @@ +use std::fs; + +use zed_extension_api::Result; + +pub(super) fn remove_outdated_versions( + language_server_id: &'static str, + version_dir: &str, +) -> Result<()> { + let entries = fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; + for entry in entries { + let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; + if entry.file_name().to_str().is_none_or(|file_name| { + file_name.starts_with(language_server_id) && file_name != version_dir + }) { + fs::remove_dir_all(entry.path()).ok(); + } + } + Ok(()) +} diff --git a/extensions/proto/src/proto.rs b/extensions/proto/src/proto.rs new file mode 100644 index 0000000000000000000000000000000000000000..07e0ccedcee287f037576db56d5a9d7958ea83f9 --- /dev/null +++ b/extensions/proto/src/proto.rs @@ -0,0 +1,66 @@ +use zed_extension_api::{self as zed, Result, settings::LspSettings}; + +use crate::language_servers::{BufLsp, ProtoLs, ProtobufLanguageServer}; + +mod language_servers; + +struct ProtobufExtension { + protobuf_language_server: Option, + protols: Option, + buf_lsp: Option, +} + +impl zed::Extension for ProtobufExtension { + fn new() -> Self { + Self { + protobuf_language_server: None, + protols: None, + buf_lsp: None, + } + } + + fn language_server_command( + &mut self, + language_server_id: &zed_extension_api::LanguageServerId, + worktree: &zed_extension_api::Worktree, + ) -> zed_extension_api::Result { + match language_server_id.as_ref() { + ProtobufLanguageServer::SERVER_NAME => self + .protobuf_language_server + .get_or_insert_with(ProtobufLanguageServer::new) + .language_server_binary(worktree), + + ProtoLs::SERVER_NAME => self + .protols + .get_or_insert_with(ProtoLs::new) + .language_server_binary(worktree), + + BufLsp::SERVER_NAME => self + .buf_lsp + .get_or_insert_with(BufLsp::new) + .language_server_binary(worktree), + + _ => Err(format!("Unknown language server ID {}", language_server_id)), + } + } + + fn language_server_workspace_configuration( + &mut self, + server_id: &zed::LanguageServerId, + worktree: &zed::Worktree, + ) -> Result> { + LspSettings::for_worktree(server_id.as_ref(), worktree) + .map(|lsp_settings| lsp_settings.settings) + } + + fn language_server_initialization_options( + &mut self, + server_id: &zed::LanguageServerId, + worktree: &zed::Worktree, + ) -> Result> { + LspSettings::for_worktree(server_id.as_ref(), worktree) + .map(|lsp_settings| lsp_settings.initialization_options) + } +} + +zed::register_extension!(ProtobufExtension); diff --git a/extensions/slash-commands-example/Cargo.toml b/extensions/slash-commands-example/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..03b22af254ab3190f2dbfca04976c89b9a37e995 --- /dev/null +++ b/extensions/slash-commands-example/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "slash_commands_example" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/slash_commands_example.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = "0.1.0" diff --git a/extensions/slash-commands-example/LICENSE-APACHE b/extensions/slash-commands-example/LICENSE-APACHE new file mode 120000 index 0000000000000000000000000000000000000000..1cd601d0a3affae83854be02a0afdec3b7a9ec4d --- /dev/null +++ b/extensions/slash-commands-example/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/slash-commands-example/README.md b/extensions/slash-commands-example/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8c16a4e168a3334d3197090837eeaf21c956b3c3 --- /dev/null +++ b/extensions/slash-commands-example/README.md @@ -0,0 +1,84 @@ +# Slash Commands Example Extension + +This is an example extension showcasing how to write slash commands. + +See: [Extensions: Slash Commands](https://zed.dev/docs/extensions/slash-commands) in the Zed Docs. + +## Pre-requisites + +[Install Rust Toolchain](https://www.rust-lang.org/tools/install): + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +## Setup + +```sh +git clone https://github.com/zed-industries/zed.git +cp -RL zed/extensions/slash-commands-example . + +cd slash-commands-example/ + +# Update Cargo.toml to make it standalone +cat > Cargo.toml << EOF +[package] +name = "slash_commands_example" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +path = "src/slash_commands_example.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = "0.1.0" +EOF + +curl -O https://raw.githubusercontent.com/rust-lang/rust/master/LICENSE-APACHE +echo "# Zed Slash Commands Example Extension" > README.md +echo "Cargo.lock" > .gitignore +echo "target/" >> .gitignore +echo "*.wasm" >> .gitignore + +git init +git add . +git commit -m "Initial commit" + +cd .. +mv slash-commands-example MY-SUPER-COOL-ZED-EXTENSION +zed $_ +``` + +## Installation + +1. Open the command palette (`cmd-shift-p` or `ctrl-shift-p`). +2. Launch `zed: install dev extension` +3. Select the extension folder created above + +## Test + +Open the assistant and type `/echo` and `/pick-one` at the beginning of a line. + +## Customization + +Open the `extensions.toml` file and set the `id`, `name`, `description`, `authors` and `repository` fields. + +Rename `slash-commands-example.rs` you'll also have to update `Cargo.toml` + +## Rebuild + +Rebuild to see these changes reflected: + +1. Open Zed Extensions (`cmd-shift-x` or `ctrl-shift-x`). +2. Click `Rebuild` next to your Dev Extension (formerly "Slash Command Example") + +## Troubleshooting / Logs + +- [zed.dev docs: Troubleshooting](https://zed.dev/docs/troubleshooting) + +## Documentation + +- [zed.dev docs: Extensions: Developing Extensions](https://zed.dev/docs/extensions/developing-extensions) +- [zed.dev docs: Extensions: Slash Commands](https://zed.dev/docs/extensions/slash-commands) diff --git a/extensions/slash-commands-example/extension.toml b/extensions/slash-commands-example/extension.toml new file mode 100644 index 0000000000000000000000000000000000000000..888c776d0111bdc5f99e87967f0cff6e0c91b2b3 --- /dev/null +++ b/extensions/slash-commands-example/extension.toml @@ -0,0 +1,15 @@ +id = "slash-commands-example" +name = "Slash Commands Example" +description = "An example extension showcasing slash commands." +version = "0.1.0" +schema_version = 1 +authors = ["Zed Industries "] +repository = "https://github.com/zed-industries/zed" + +[slash_commands.echo] +description = "echoes the provided input" +requires_argument = true + +[slash_commands.pick-one] +description = "pick one of three options" +requires_argument = true diff --git a/extensions/slash-commands-example/src/slash_commands_example.rs b/extensions/slash-commands-example/src/slash_commands_example.rs new file mode 100644 index 0000000000000000000000000000000000000000..5b170d63ee38c4dd173ddc2856c858755150490e --- /dev/null +++ b/extensions/slash-commands-example/src/slash_commands_example.rs @@ -0,0 +1,90 @@ +use zed_extension_api::{ + self as zed, SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, + SlashCommandOutputSection, Worktree, +}; + +struct SlashCommandsExampleExtension; + +impl zed::Extension for SlashCommandsExampleExtension { + fn new() -> Self { + SlashCommandsExampleExtension + } + + fn complete_slash_command_argument( + &self, + command: SlashCommand, + _args: Vec, + ) -> Result, String> { + match command.name.as_str() { + "echo" => Ok(vec![]), + "pick-one" => Ok(vec![ + SlashCommandArgumentCompletion { + label: "Option One".to_string(), + new_text: "option-1".to_string(), + run_command: true, + }, + SlashCommandArgumentCompletion { + label: "Option Two".to_string(), + new_text: "option-2".to_string(), + run_command: true, + }, + SlashCommandArgumentCompletion { + label: "Option Three".to_string(), + new_text: "option-3".to_string(), + run_command: true, + }, + ]), + command => Err(format!("unknown slash command: \"{command}\"")), + } + } + + fn run_slash_command( + &self, + command: SlashCommand, + args: Vec, + _worktree: Option<&Worktree>, + ) -> Result { + match command.name.as_str() { + "echo" => { + if args.is_empty() { + return Err("nothing to echo".to_string()); + } + + let text = args.join(" "); + + Ok(SlashCommandOutput { + sections: vec![SlashCommandOutputSection { + range: (0..text.len()).into(), + label: "Echo".to_string(), + }], + text, + }) + } + "pick-one" => { + let Some(selection) = args.first() else { + return Err("no option selected".to_string()); + }; + + match selection.as_str() { + "option-1" | "option-2" | "option-3" => {} + invalid_option => { + return Err(format!("{invalid_option} is not a valid option")); + } + } + + let text = format!("You chose {selection}."); + + Ok(SlashCommandOutput { + sections: vec![SlashCommandOutputSection { + range: (0..text.len()).into(), + label: format!("Pick One: {selection}"), + }], + text, + }) + } + command => Err(format!("unknown slash command: \"{command}\"")), + } + } +} + +zed::register_extension!(SlashCommandsExampleExtension); diff --git a/extensions/test-extension/Cargo.toml b/extensions/test-extension/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..7d2412b98f3829a97e9cae7e59883a0e3ebb068d --- /dev/null +++ b/extensions/test-extension/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "zed_test_extension" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/test_extension.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = { path = "../../crates/extension_api" } diff --git a/extensions/test-extension/LICENSE-APACHE b/extensions/test-extension/LICENSE-APACHE new file mode 120000 index 0000000000000000000000000000000000000000..1cd601d0a3affae83854be02a0afdec3b7a9ec4d --- /dev/null +++ b/extensions/test-extension/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/test-extension/README.md b/extensions/test-extension/README.md new file mode 100644 index 0000000000000000000000000000000000000000..5941f23ec46716bc4bed992ca00bda500cda366a --- /dev/null +++ b/extensions/test-extension/README.md @@ -0,0 +1,5 @@ +# Test Extension + +This is a test extension that we use in the tests for the `extension` crate. + +Originally based off the Gleam extension. diff --git a/extensions/test-extension/extension.toml b/extensions/test-extension/extension.toml new file mode 100644 index 0000000000000000000000000000000000000000..0cb5afac7f7031c7f2eb9db2fbbcef9d2e107b68 --- /dev/null +++ b/extensions/test-extension/extension.toml @@ -0,0 +1,25 @@ +id = "test-extension" +name = "Test Extension" +description = "An extension for use in tests." +version = "0.1.0" +schema_version = 1 +authors = ["Marshall Bowers "] +repository = "https://github.com/zed-industries/zed" + +[language_servers.gleam] +name = "Gleam LSP" +language = "Gleam" + +[grammars.gleam] +repository = "https://github.com/gleam-lang/tree-sitter-gleam" +commit = "8432ffe32ccd360534837256747beb5b1c82fca1" + +[[capabilities]] +kind = "process:exec" +command = "echo" +args = ["hello from a child process!"] + +[[capabilities]] +kind = "process:exec" +command = "cmd" +args = ["/C", "echo", "hello from a child process!"] diff --git a/extensions/test-extension/languages/gleam/config.toml b/extensions/test-extension/languages/gleam/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..51874945e2de6b520a83f76c9302d1d95c824980 --- /dev/null +++ b/extensions/test-extension/languages/gleam/config.toml @@ -0,0 +1,12 @@ +name = "Gleam" +grammar = "gleam" +path_suffixes = ["gleam"] +line_comments = ["// ", "/// "] +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] }, +] +tab_size = 2 diff --git a/extensions/test-extension/languages/gleam/highlights.scm b/extensions/test-extension/languages/gleam/highlights.scm new file mode 100644 index 0000000000000000000000000000000000000000..4b85b88d0151a1bfe9018f0c526497261d6e1801 --- /dev/null +++ b/extensions/test-extension/languages/gleam/highlights.scm @@ -0,0 +1,130 @@ +; Comments +(module_comment) @comment +(statement_comment) @comment +(comment) @comment + +; Constants +(constant + name: (identifier) @constant) + +; Variables +(identifier) @variable +(discard) @comment.unused + +; Modules +(module) @module +(import alias: (identifier) @module) +(remote_type_identifier + module: (identifier) @module) +(remote_constructor_name + module: (identifier) @module) +((field_access + record: (identifier) @module + field: (label) @function) + (#is-not? local)) + +; Functions +(unqualified_import (identifier) @function) +(unqualified_import "type" (type_identifier) @type) +(unqualified_import (type_identifier) @constructor) +(function + name: (identifier) @function) +(external_function + name: (identifier) @function) +(function_parameter + name: (identifier) @variable.parameter) +((function_call + function: (identifier) @function) + (#is-not? local)) +((binary_expression + operator: "|>" + right: (identifier) @function) + (#is-not? local)) + +; "Properties" +; Assumed to be intended to refer to a name for a field; something that comes +; before ":" or after "." +; e.g. record field names, tuple indices, names for named arguments, etc +(label) @property +(tuple_access + index: (integer) @property) + +; Attributes +(attribute + "@" @attribute + name: (identifier) @attribute) + +(attribute_value (identifier) @constant) + +; Type names +(remote_type_identifier) @type +(type_identifier) @type + +; Data constructors +(constructor_name) @constructor + +; Literals +(string) @string +((escape_sequence) @warning + ; Deprecated in v0.33.0-rc2: + (#eq? @warning "\\e")) +(escape_sequence) @string.escape +(bit_string_segment_option) @function.builtin +(integer) @number +(float) @number + +; Reserved identifiers +; TODO: when tree-sitter supports `#any-of?` in the Rust bindings, +; refactor this to use `#any-of?` rather than `#match?` +((identifier) @warning + (#match? @warning "^(auto|delegate|derive|else|implement|macro|test|echo)$")) + +; Keywords +[ + (visibility_modifier) ; "pub" + (opacity_modifier) ; "opaque" + "as" + "assert" + "case" + "const" + ; DEPRECATED: 'external' was removed in v0.30. + "external" + "fn" + "if" + "import" + "let" + "panic" + "todo" + "type" + "use" +] @keyword + +; Operators +(binary_expression + operator: _ @operator) +(boolean_negation "!" @operator) +(integer_negation "-" @operator) + +; Punctuation +[ + "(" + ")" + "[" + "]" + "{" + "}" + "<<" + ">>" +] @punctuation.bracket +[ + "." + "," + ;; Controversial -- maybe some are operators? + ":" + "#" + "=" + "->" + ".." + "-" + "<-" +] @punctuation.delimiter diff --git a/extensions/test-extension/languages/gleam/indents.scm b/extensions/test-extension/languages/gleam/indents.scm new file mode 100644 index 0000000000000000000000000000000000000000..112b414aa45f277138d0c681851129a608ee96e0 --- /dev/null +++ b/extensions/test-extension/languages/gleam/indents.scm @@ -0,0 +1,3 @@ +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/extensions/test-extension/languages/gleam/outline.scm b/extensions/test-extension/languages/gleam/outline.scm new file mode 100644 index 0000000000000000000000000000000000000000..5df7a6af800e8e3c9f0b00834576f2e059bd12b0 --- /dev/null +++ b/extensions/test-extension/languages/gleam/outline.scm @@ -0,0 +1,31 @@ +(external_type + (visibility_modifier)? @context + "type" @context + (type_name) @name) @item + +(type_definition + (visibility_modifier)? @context + (opacity_modifier)? @context + "type" @context + (type_name) @name) @item + +(data_constructor + (constructor_name) @name) @item + +(data_constructor_argument + (label) @name) @item + +(type_alias + (visibility_modifier)? @context + "type" @context + (type_name) @name) @item + +(function + (visibility_modifier)? @context + "fn" @context + name: (_) @name) @item + +(constant + (visibility_modifier)? @context + "const" @context + name: (_) @name) @item diff --git a/extensions/test-extension/src/test_extension.rs b/extensions/test-extension/src/test_extension.rs new file mode 100644 index 0000000000000000000000000000000000000000..0b96f470386fcc08a62b487cc36a448c4514854b --- /dev/null +++ b/extensions/test-extension/src/test_extension.rs @@ -0,0 +1,209 @@ +use std::fs; +use zed::lsp::CompletionKind; +use zed::{CodeLabel, CodeLabelSpan, LanguageServerId}; +use zed_extension_api::process::Command; +use zed_extension_api::{self as zed, Result}; + +struct TestExtension { + cached_binary_path: Option, +} + +impl TestExtension { + fn language_server_binary_path( + &mut self, + language_server_id: &LanguageServerId, + _worktree: &zed::Worktree, + ) -> Result { + let (platform, arch) = zed::current_platform(); + + let current_dir = std::env::current_dir().unwrap(); + println!("current_dir: {}", current_dir.display()); + assert_eq!( + current_dir.file_name().unwrap().to_str().unwrap(), + "test-extension" + ); + + fs::create_dir_all(current_dir.join("dir-created-with-abs-path")).unwrap(); + fs::create_dir_all("./dir-created-with-rel-path").unwrap(); + fs::write("file-created-with-rel-path", b"contents 1").unwrap(); + fs::write( + current_dir.join("file-created-with-abs-path"), + b"contents 2", + ) + .unwrap(); + assert_eq!( + fs::read("file-created-with-rel-path").unwrap(), + b"contents 1" + ); + assert_eq!( + fs::read("file-created-with-abs-path").unwrap(), + b"contents 2" + ); + + let command = match platform { + zed::Os::Linux | zed::Os::Mac => Command::new("echo"), + zed::Os::Windows => Command::new("cmd").args(["/C", "echo"]), + }; + let output = command.arg("hello from a child process!").output()?; + println!( + "command output: {}", + String::from_utf8_lossy(&output.stdout).trim() + ); + + if let Some(path) = &self.cached_binary_path + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) + { + return Ok(path.clone()); + } + + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let release = zed::latest_github_release( + "gleam-lang/gleam", + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let ext = "tar.gz"; + let download_type = zed::DownloadedFileType::GzipTar; + + // Do this if you want to actually run this extension - + // the actual asset is a .zip. But the integration test is simpler + // if every platform uses .tar.gz. + // + // ext = "zip"; + // download_type = zed::DownloadedFileType::Zip; + + let asset_name = format!( + "gleam-{version}-{arch}-{os}.{ext}", + version = release.version, + arch = match arch { + zed::Architecture::Aarch64 => "aarch64", + zed::Architecture::X86 => "x86", + zed::Architecture::X8664 => "x86_64", + }, + os = match platform { + zed::Os::Mac => "apple-darwin", + zed::Os::Linux => "unknown-linux-musl", + zed::Os::Windows => "pc-windows-msvc", + }, + ); + + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; + + let version_dir = format!("gleam-{}", release.version); + let binary_path = format!("{version_dir}/gleam"); + + if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + + zed::download_file(&asset.download_url, &version_dir, download_type) + .map_err(|e| format!("failed to download file: {e}"))?; + + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::None, + ); + + let entries = + fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; + for entry in entries { + let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; + let filename = entry.file_name(); + let filename = filename.to_str().unwrap(); + if filename.starts_with("gleam-") && filename != version_dir { + fs::remove_dir_all(entry.path()).ok(); + } + } + } + + self.cached_binary_path = Some(binary_path.clone()); + Ok(binary_path) + } +} + +impl zed::Extension for TestExtension { + fn new() -> Self { + Self { + cached_binary_path: None, + } + } + + fn language_server_command( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + Ok(zed::Command { + command: self.language_server_binary_path(language_server_id, worktree)?, + args: vec!["lsp".to_string()], + env: Default::default(), + }) + } + + fn label_for_completion( + &self, + _language_server_id: &LanguageServerId, + completion: zed::lsp::Completion, + ) -> Option { + let name = &completion.label; + let ty = strip_newlines_from_detail(&completion.detail?); + let let_binding = "let a"; + let colon = ": "; + let assignment = " = "; + let call = match completion.kind? { + CompletionKind::Function | CompletionKind::Constructor => "()", + _ => "", + }; + let code = format!("{let_binding}{colon}{ty}{assignment}{name}{call}"); + + Some(CodeLabel { + spans: vec![ + CodeLabelSpan::code_range({ + let start = let_binding.len() + colon.len() + ty.len() + assignment.len(); + start..start + name.len() + }), + CodeLabelSpan::code_range({ + let start = let_binding.len(); + start..start + colon.len() + }), + CodeLabelSpan::code_range({ + let start = let_binding.len() + colon.len(); + start..start + ty.len() + }), + ], + filter_range: (0..name.len()).into(), + code, + }) + } +} + +zed::register_extension!(TestExtension); + +/// Removes newlines from the completion detail. +/// +/// The Gleam LSP can return types containing newlines, which causes formatting +/// issues within the Zed completions menu. +fn strip_newlines_from_detail(detail: &str) -> String { + let without_newlines = detail + .replace("->\n ", "-> ") + .replace("\n ", "") + .replace(",\n", ""); + + let comma_delimited_parts = without_newlines.split(','); + comma_delimited_parts + .map(|part| part.trim()) + .collect::>() + .join(", ") +} diff --git a/extensions/workflows/bump_version.yml b/extensions/workflows/bump_version.yml new file mode 100644 index 0000000000000000000000000000000000000000..7f4318dcf54ad8c9360ae622354530b2b54c6a03 --- /dev/null +++ b/extensions/workflows/bump_version.yml @@ -0,0 +1,52 @@ +# Generated from xtask::workflows::extensions::bump_version within the Zed repository. +# Rebuild with `cargo xtask workflows`. +name: extensions::bump_version +on: + pull_request: + types: + - labeled + push: + branches: + - main + paths-ignore: + - .github/** + workflow_dispatch: {} +jobs: + determine_bump_type: + runs-on: namespace-profile-16x32-ubuntu-2204 + steps: + - id: get-bump-type + name: extensions::bump_version::get_bump_type + run: | + if [ "$HAS_MAJOR_LABEL" = "true" ]; then + bump_type="major" + elif [ "$HAS_MINOR_LABEL" = "true" ]; then + bump_type="minor" + else + bump_type="patch" + fi + echo "bump_type=$bump_type" >> $GITHUB_OUTPUT + shell: bash -euxo pipefail {0} + env: + HAS_MAJOR_LABEL: |- + ${{ (github.event.action == 'labeled' && github.event.label.name == 'major') || + (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'major')) }} + HAS_MINOR_LABEL: |- + ${{ (github.event.action == 'labeled' && github.event.label.name == 'minor') || + (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'minor')) }} + outputs: + bump_type: ${{ steps.get-bump-type.outputs.bump_type }} + call_bump_version: + needs: + - determine_bump_type + if: github.event.action != 'labeled' || needs.determine_bump_type.outputs.bump_type != 'patch' + uses: zed-industries/zed/.github/workflows/extension_bump.yml@main + secrets: + app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} + app-secret: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }} + with: + bump-type: ${{ needs.determine_bump_type.outputs.bump_type }} + force-bump: true +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}labels + cancel-in-progress: true diff --git a/extensions/workflows/release_version.yml b/extensions/workflows/release_version.yml new file mode 100644 index 0000000000000000000000000000000000000000..f752931917292110580a74198d3e1231098539db --- /dev/null +++ b/extensions/workflows/release_version.yml @@ -0,0 +1,13 @@ +# Generated from xtask::workflows::extensions::release_version within the Zed repository. +# Rebuild with `cargo xtask workflows`. +name: extensions::release_version +on: + push: + tags: + - v** +jobs: + call_release_version: + uses: zed-industries/zed/.github/workflows/extension_release.yml@main + secrets: + app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} + app-secret: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }} diff --git a/extensions/workflows/run_tests.yml b/extensions/workflows/run_tests.yml new file mode 100644 index 0000000000000000000000000000000000000000..81ba76c483479ed827f0a91181557a2387b40722 --- /dev/null +++ b/extensions/workflows/run_tests.yml @@ -0,0 +1,16 @@ +# Generated from xtask::workflows::extensions::run_tests within the Zed repository. +# Rebuild with `cargo xtask workflows`. +name: extensions::run_tests +on: + pull_request: + branches: + - '**' + push: + branches: + - main +jobs: + call_extension_tests: + uses: zed-industries/zed/.github/workflows/extension_tests.yml@main +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}pr + cancel-in-progress: true