glsl: Add glsl_analyzer (LSP) (#10694)

jansol and Marshall Bowers created

<img width="691" alt="image"
src="https://github.com/zed-industries/zed/assets/2588851/c5e02d12-d1e4-4407-971c-72de7e6599f0">

@mikayla-maki the extension lists you as the original author.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>

Change summary

Cargo.lock                                 |   7 +
Cargo.toml                                 |   1 
extensions/glsl/Cargo.toml                 |  16 ++
extensions/glsl/extension.toml             |   4 
extensions/glsl/languages/glsl/config.toml |  11 +
extensions/glsl/src/glsl.rs                | 131 ++++++++++++++++++++++++
6 files changed, 169 insertions(+), 1 deletion(-)

Detailed changes

Cargo.lock 🔗

@@ -12699,6 +12699,13 @@ dependencies = [
  "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "zed_glsl"
+version = "0.0.1"
+dependencies = [
+ "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "zed_haskell"
 version = "0.1.0"

Cargo.toml 🔗

@@ -110,6 +110,7 @@ members = [
     "extensions/emmet",
     "extensions/erlang",
     "extensions/gleam",
+    "extensions/glsl",
     "extensions/haskell",
     "extensions/html",
     "extensions/lua",

extensions/glsl/Cargo.toml 🔗

@@ -0,0 +1,16 @@
+[package]
+name = "zed_glsl"
+version = "0.0.1"
+edition = "2021"
+publish = false
+license = "Apache-2.0"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/glsl.rs"
+crate-type = ["cdylib"]
+
+[dependencies]
+zed_extension_api = "0.0.6"

extensions/glsl/extension.toml 🔗

@@ -6,6 +6,10 @@ 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 🔗

@@ -1,6 +1,15 @@
 name = "GLSL"
 grammar = "glsl"
-path_suffixes = ["vert", "frag", "tesc", "tese", "geom", "comp"]
+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"
+    ]
 line_comments = ["// "]
 block_comment = ["/* ", " */"]
 brackets = [

extensions/glsl/src/glsl.rs 🔗

@@ -0,0 +1,131 @@
+use std::fs;
+use zed::settings::LspSettings;
+use zed_extension_api::{self as zed, serde_json, LanguageServerId, Result};
+
+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 {
+            if fs::metadata(path).map_or(false, |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).map_or(false, |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.clone())
+            .unwrap_or_default();
+
+        Ok(Some(serde_json::json!({
+            "glsl_analyzer": settings
+        })))
+    }
+}
+
+zed::register_extension!(GlslExtension);