proto: Add two language servers and change used grammar (#44440)

Finn Evers created

Closes #43784
Closes #44375
Closes #21057

This PR updates the Proto extension to include support for two new
language servers as well as an updated grammar for better highlighting.

Release Notes:

- Improved Proto support to work better out of the box.

Change summary

Cargo.lock                                                        |   2 
assets/settings/default.json                                      |   3 
extensions/proto/Cargo.toml                                       |   2 
extensions/proto/extension.toml                                   |  13 
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                                     |  86 
typos.toml                                                        |   3 
11 files changed, 358 insertions(+), 57 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -20828,7 +20828,7 @@ dependencies = [
 name = "zed_proto"
 version = "0.2.3"
 dependencies = [
- "zed_extension_api 0.1.0",
+ "zed_extension_api 0.7.0",
 ]
 
 [[package]]

assets/settings/default.json 🔗

@@ -1932,6 +1932,9 @@
         "words": "disabled",
       },
     },
+    "Proto": {
+      "language_servers": ["buf", "!protols", "!protobuf-language-server", "..."]
+    },
     "Python": {
       "code_actions_on_format": {
         "source.organizeImports.ruff": true,

extensions/proto/Cargo.toml 🔗

@@ -13,4 +13,4 @@ path = "src/proto.rs"
 crate-type = ["cdylib"]
 
 [dependencies]
-zed_extension_api = "0.1.0"
+zed_extension_api = "0.7.0"

extensions/proto/extension.toml 🔗

@@ -7,9 +7,18 @@ authors = ["Zed Industries <support@zed.dev>"]
 repository = "https://github.com/zed-industries/zed"
 
 [grammars.proto]
-repository = "https://github.com/zed-industries/tree-sitter-proto"
-commit = "0848bd30a64be48772e15fbb9d5ba8c0cc5772ad"
+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/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 🔗

@@ -1,48 +1,22 @@
 use zed_extension_api::{self as zed, Result, settings::LspSettings};
 
-const PROTOBUF_LANGUAGE_SERVER_NAME: &str = "protobuf-language-server";
+use crate::language_servers::{BufLsp, ProtoLs, ProtobufLanguageServer};
 
-struct ProtobufLanguageServerBinary {
-    path: String,
-    args: Option<Vec<String>>,
-}
-
-struct ProtobufExtension;
-
-impl ProtobufExtension {
-    fn language_server_binary(
-        &self,
-        _language_server_id: &zed::LanguageServerId,
-        worktree: &zed::Worktree,
-    ) -> Result<ProtobufLanguageServerBinary> {
-        let binary_settings = LspSettings::for_worktree("protobuf-language-server", worktree)
-            .ok()
-            .and_then(|lsp_settings| lsp_settings.binary);
-        let binary_args = binary_settings
-            .as_ref()
-            .and_then(|binary_settings| binary_settings.arguments.clone());
-
-        if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) {
-            return Ok(ProtobufLanguageServerBinary {
-                path,
-                args: binary_args,
-            });
-        }
-
-        if let Some(path) = worktree.which(PROTOBUF_LANGUAGE_SERVER_NAME) {
-            return Ok(ProtobufLanguageServerBinary {
-                path,
-                args: binary_args,
-            });
-        }
+mod language_servers;
 
-        Err(format!("{PROTOBUF_LANGUAGE_SERVER_NAME} not found in PATH",))
-    }
+struct ProtobufExtension {
+    protobuf_language_server: Option<ProtobufLanguageServer>,
+    protols: Option<ProtoLs>,
+    buf_lsp: Option<BufLsp>,
 }
 
 impl zed::Extension for ProtobufExtension {
     fn new() -> Self {
-        Self
+        Self {
+            protobuf_language_server: None,
+            protols: None,
+            buf_lsp: None,
+        }
     }
 
     fn language_server_command(
@@ -50,14 +24,24 @@ impl zed::Extension for ProtobufExtension {
         language_server_id: &zed_extension_api::LanguageServerId,
         worktree: &zed_extension_api::Worktree,
     ) -> zed_extension_api::Result<zed_extension_api::Command> {
-        let binary = self.language_server_binary(language_server_id, worktree)?;
-        Ok(zed::Command {
-            command: binary.path,
-            args: binary
-                .args
-                .unwrap_or_else(|| vec!["-logs".into(), "".into()]),
-            env: Default::default(),
-        })
+        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(
@@ -65,10 +49,8 @@ impl zed::Extension for ProtobufExtension {
         server_id: &zed::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);
-        Ok(settings)
+        LspSettings::for_worktree(server_id.as_ref(), worktree)
+            .map(|lsp_settings| lsp_settings.settings)
     }
 
     fn language_server_initialization_options(
@@ -76,10 +58,8 @@ impl zed::Extension for ProtobufExtension {
         server_id: &zed::LanguageServerId,
         worktree: &zed::Worktree,
     ) -> Result<Option<zed_extension_api::serde_json::Value>> {
-        let initialization_options = LspSettings::for_worktree(server_id.as_ref(), worktree)
-            .ok()
-            .and_then(|lsp_settings| lsp_settings.initialization_options);
-        Ok(initialization_options)
+        LspSettings::for_worktree(server_id.as_ref(), worktree)
+            .map(|lsp_settings| lsp_settings.initialization_options)
     }
 }
 

typos.toml 🔗

@@ -31,6 +31,9 @@ extend-exclude = [
     "crates/rpc/src/auth.rs",
     # glsl isn't recognized by this tool.
     "extensions/glsl/languages/glsl/",
+    # Protols is the name of the language server.
+    "extensions/proto/extension.toml",
+    "extensions/proto/src/language_servers/protols.rs",
     # Windows likes its abbreviations.
     "crates/gpui/src/platform/windows/directx_renderer.rs",
     "crates/gpui/src/platform/windows/events.rs",