Reset extensions dir to origin/main

Richard Feldman created

Change summary

extensions/glsl/Cargo.toml                                        |  16 
extensions/glsl/LICENSE-APACHE                                    |   1 
extensions/glsl/extension.toml                                    |  15 
extensions/glsl/languages/glsl/config.toml                        |  20 
extensions/glsl/languages/glsl/highlights.scm                     | 117 
extensions/glsl/src/glsl.rs                                       | 131 
extensions/html/Cargo.toml                                        |  16 
extensions/html/LICENSE-APACHE                                    |   1 
extensions/html/extension.toml                                    |  19 
extensions/html/languages/html/brackets.scm                       |   5 
extensions/html/languages/html/config.toml                        |  19 
extensions/html/languages/html/highlights.scm                     |  19 
extensions/html/languages/html/indents.scm                        |   6 
extensions/html/languages/html/injections.scm                     |  21 
extensions/html/languages/html/outline.scm                        |   5 
extensions/html/languages/html/overrides.scm                      |   7 
extensions/html/src/html.rs                                       | 115 
extensions/proto/Cargo.toml                                       |  16 
extensions/proto/LICENSE-APACHE                                   |   1 
extensions/proto/extension.toml                                   |  24 
extensions/proto/languages/proto/config.toml                      |  13 
extensions/proto/languages/proto/highlights.scm                   |  61 
extensions/proto/languages/proto/indents.scm                      |   3 
extensions/proto/languages/proto/outline.scm                      |  19 
extensions/proto/languages/proto/textobjects.scm                  |  18 
extensions/proto/src/language_servers.rs                          |   8 
extensions/proto/src/language_servers/buf.rs                      | 114 
extensions/proto/src/language_servers/protobuf_language_server.rs |  52 
extensions/proto/src/language_servers/protols.rs                  | 113 
extensions/proto/src/language_servers/util.rs                     |  19 
extensions/proto/src/proto.rs                                     |  66 
extensions/slash-commands-example/Cargo.toml                      |  16 
extensions/slash-commands-example/LICENSE-APACHE                  |   1 
extensions/slash-commands-example/README.md                       |  84 
extensions/slash-commands-example/extension.toml                  |  15 
extensions/slash-commands-example/src/slash_commands_example.rs   |  90 
extensions/test-extension/Cargo.toml                              |  16 
extensions/test-extension/LICENSE-APACHE                          |   1 
extensions/test-extension/README.md                               |   5 
extensions/test-extension/extension.toml                          |  25 
extensions/test-extension/languages/gleam/config.toml             |  12 
extensions/test-extension/languages/gleam/highlights.scm          | 130 
extensions/test-extension/languages/gleam/indents.scm             |   3 
extensions/test-extension/languages/gleam/outline.scm             |  31 
extensions/test-extension/src/test_extension.rs                   | 209 +
extensions/workflows/bump_version.yml                             |  52 
extensions/workflows/release_version.yml                          |  13 
extensions/workflows/run_tests.yml                                |  16 
48 files changed, 1,779 insertions(+)

Detailed changes

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"

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 <mikayla@zed.dev>"]
+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"

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 },
+]

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_")
+)

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<String>,
+}
+
+impl GlslExtension {
+    fn language_server_binary_path(
+        &mut self,
+        language_server_id: &LanguageServerId,
+        worktree: &zed::Worktree,
+    ) -> Result<String> {
+        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<zed::Command> {
+        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<Option<serde_json::Value>> {
+        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);

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"

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 <slightknack@gmail.com>"]
+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"

extensions/html/languages/html/brackets.scm 🔗

@@ -0,0 +1,5 @@
+("<" @open "/>" @close)
+("</" @open ">" @close)
+("<" @open ">" @close)
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
+((element (start_tag) @open (end_tag) @close) (#set! newline.only) (#set! rainbow.exclude))

extensions/html/languages/html/config.toml 🔗

@@ -0,0 +1,19 @@
+name = "HTML"
+grammar = "html"
+path_suffixes = ["html", "htm", "shtml"]
+autoclose_before = ">})"
+block_comment = { start = "<!--", prefix = "", end = "-->", tab_size = 0 }
+wrap_characters = { start_prefix = "<", start_suffix = ">", end_prefix = "</", end_suffix = ">" }
+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 = ["-"]

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

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"))

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<String>,
+}
+
+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<String> {
+        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<zed::Command> {
+        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<Option<zed::serde_json::Value>> {
+        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<Option<zed_extension_api::serde_json::Value>> {
+        let initialization_options = json!({"provideFormatter": true });
+        Ok(Some(initialization_options))
+    }
+}
+
+zed::register_extension!(HtmlExtension);

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"

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 <support@zed.dev>"]
+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"]

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"] },
+]

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

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

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

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::*;

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<String>,
+}
+
+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<zed::Command> {
+        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(),
+        })
+    }
+}

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<String>,
+}
+
+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<zed::Command> {
+        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))
+        }
+    }
+}

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<String>,
+}
+
+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<zed::Command> {
+        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,
+        })
+    }
+}

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(())
+}

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<ProtobufLanguageServer>,
+    protols: Option<ProtoLs>,
+    buf_lsp: Option<BufLsp>,
+}
+
+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<zed_extension_api::Command> {
+        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<Option<zed::serde_json::Value>> {
+        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<Option<zed_extension_api::serde_json::Value>> {
+        LspSettings::for_worktree(server_id.as_ref(), worktree)
+            .map(|lsp_settings| lsp_settings.initialization_options)
+    }
+}
+
+zed::register_extension!(ProtobufExtension);

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"

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)

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 <hi@zed.dev>"]
+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

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<String>,
+    ) -> Result<Vec<zed_extension_api::SlashCommandArgumentCompletion>, 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<String>,
+        _worktree: Option<&Worktree>,
+    ) -> Result<SlashCommandOutput, String> {
+        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);

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" }

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.

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 <elliott.codes@gmail.com>"]
+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!"]

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

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

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

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<String>,
+}
+
+impl TestExtension {
+    fn language_server_binary_path(
+        &mut self,
+        language_server_id: &LanguageServerId,
+        _worktree: &zed::Worktree,
+    ) -> Result<String> {
+        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<zed::Command> {
+        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<zed::CodeLabel> {
+        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::<Vec<_>>()
+        .join(", ")
+}

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

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 }}

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