Add zig support

Allan Calix created

Change summary

Cargo.lock                                  |  10 
Cargo.toml                                  |   1 
crates/zed/Cargo.toml                       |   1 
crates/zed/src/languages.rs                 |   6 
crates/zed/src/languages/zig.rs             | 125 ++++++++++++
crates/zed/src/languages/zig/config.toml    |  10 
crates/zed/src/languages/zig/folds.scm      |  16 +
crates/zed/src/languages/zig/highlights.scm | 234 +++++++++++++++++++++++
crates/zed/src/languages/zig/indents.scm    |  22 ++
crates/zed/src/languages/zig/injections.scm |   5 
10 files changed, 430 insertions(+)

Detailed changes

Cargo.lock 🔗

@@ -8716,6 +8716,15 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-zig"
+version = "0.0.1"
+source = "git+https://github.com/maxxnino/tree-sitter-zig?rev=0d08703e4c3f426ec61695d7617415fff97029bd#0d08703e4c3f426ec61695d7617415fff97029bd"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "try-lock"
 version = "0.2.4"
@@ -9812,6 +9821,7 @@ dependencies = [
  "tree-sitter-uiua",
  "tree-sitter-vue",
  "tree-sitter-yaml",
+ "tree-sitter-zig",
  "unindent",
  "url",
  "urlencoding",

Cargo.toml 🔗

@@ -161,6 +161,7 @@ tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", re
 tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "26bbaecda0039df4067861ab38ea8ea169f7f5aa"}
 tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42"}
 tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"}
+tree-sitter-zig = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "0d08703e4c3f426ec61695d7617415fff97029bd" }
 
 [patch.crates-io]
 tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "31c40449749c4263a91a43593831b82229049a4c" }

crates/zed/Cargo.toml 🔗

@@ -142,6 +142,7 @@ tree-sitter-nix.workspace = true
 tree-sitter-nu.workspace = true
 tree-sitter-vue.workspace = true
 tree-sitter-uiua.workspace = true
+tree-sitter-zig.workspace = true
 
 url = "2.2"
 urlencoding = "2.1.2"

crates/zed/src/languages.rs 🔗

@@ -31,6 +31,7 @@ mod typescript;
 mod uiua;
 mod vue;
 mod yaml;
+mod zig;
 
 // 1. Add tree-sitter-{language} parser to zed crate
 // 2. Create a language directory in zed/crates/zed/src/languages and add the language to init function below
@@ -112,6 +113,11 @@ pub fn init(
         tree_sitter_go::language(),
         vec![Arc::new(go::GoLspAdapter)],
     );
+    language(
+        "zig",
+        tree_sitter_zig::language(),
+        vec![Arc::new(zig::ZlsAdapter)],
+    );
     language(
         "heex",
         tree_sitter_heex::language(),

crates/zed/src/languages/zig.rs 🔗

@@ -0,0 +1,125 @@
+use anyhow::{anyhow, Context, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_tar::Archive;
+use async_trait::async_trait;
+use futures::{io::BufReader, StreamExt};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use smol::fs;
+use std::env::consts::ARCH;
+use std::{any::Any, path::PathBuf};
+use util::async_maybe;
+use util::github::latest_github_release;
+use util::{github::GitHubLspBinaryVersion, ResultExt};
+
+pub struct ZlsAdapter;
+
+#[async_trait]
+impl LspAdapter for ZlsAdapter {
+    fn name(&self) -> LanguageServerName {
+        LanguageServerName("zls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "zls"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release = latest_github_release("zigtools/zls", false, delegate.http_client()).await?;
+        let asset_name = format!("zls-{}-macos.tar.gz", ARCH);
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+        let version = GitHubLspBinaryVersion {
+            name: release.name,
+            url: asset.browser_download_url.clone(),
+        };
+
+        Ok(Box::new(version) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+        let binary_path = container_dir.join("bin/zls");
+
+        if fs::metadata(&binary_path).await.is_err() {
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .context("error downloading release")?;
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let archive = Archive::new(decompressed_bytes);
+            archive.unpack(container_dir).await?;
+        }
+
+        fs::set_permissions(
+            &binary_path,
+            <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
+        )
+        .await?;
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: vec![],
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--help".into()];
+                binary
+            })
+    }
+}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async_maybe!({
+        let mut last_binary_path = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_file()
+                && entry
+                    .file_name()
+                    .to_str()
+                    .map_or(false, |name| name == "zls")
+            {
+                last_binary_path = Some(entry.path());
+            }
+        }
+
+        if let Some(path) = last_binary_path {
+            Ok(LanguageServerBinary {
+                path,
+                arguments: Vec::new(),
+            })
+        } else {
+            Err(anyhow!("no cached binary"))
+        }
+    })
+    .await
+    .log_err()
+}

crates/zed/src/languages/zig/config.toml 🔗

@@ -0,0 +1,10 @@
+name = "Zig"
+path_suffixes = ["zig"]
+line_comment = "// "
+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 = true },
+]

crates/zed/src/languages/zig/folds.scm 🔗

@@ -0,0 +1,16 @@
+[
+  (Block)
+  (ContainerDecl)
+  (SwitchExpr)
+  (InitList)
+  (AsmExpr)
+  (ErrorSetDecl)
+  (LINESTRING)
+  (
+    [
+      (IfPrefix)
+      (WhilePrefix)
+      (ForPrefix)
+    ]
+  )
+] @fold

crates/zed/src/languages/zig/highlights.scm 🔗

@@ -0,0 +1,234 @@
+[
+  (container_doc_comment)
+  (doc_comment)
+  (line_comment)
+] @comment
+
+[
+  variable: (IDENTIFIER)
+  variable_type_function: (IDENTIFIER)
+] @variable
+
+parameter: (IDENTIFIER) @parameter
+
+[
+  field_member: (IDENTIFIER)
+  field_access: (IDENTIFIER)
+] @field
+
+;; assume TitleCase is a type
+(
+  [
+    variable_type_function: (IDENTIFIER)
+    field_access: (IDENTIFIER)
+    parameter: (IDENTIFIER)
+  ] @type
+  (#match? @type "^[A-Z]([a-z]+[A-Za-z0-9]*)*$")
+)
+;; assume camelCase is a function
+(
+  [
+    variable_type_function: (IDENTIFIER)
+    field_access: (IDENTIFIER)
+    parameter: (IDENTIFIER)
+  ] @function
+  (#match? @function "^[a-z]+([A-Z][a-z0-9]*)+$")
+)
+
+;; assume all CAPS_1 is a constant
+(
+  [
+    variable_type_function: (IDENTIFIER)
+    field_access: (IDENTIFIER)
+  ] @constant
+  (#match? @constant "^[A-Z][A-Z_0-9]+$")
+)
+
+[
+  function_call: (IDENTIFIER)
+  function: (IDENTIFIER)
+] @function
+
+exception: "!" @exception
+
+(
+  (IDENTIFIER) @variable.builtin
+  (#eq? @variable.builtin "_")
+)
+
+(PtrTypeStart "c" @variable.builtin)
+
+(
+  (ContainerDeclType
+    [
+      (ErrorUnionExpr)
+      "enum"
+    ]
+  )
+  (ContainerField (IDENTIFIER) @constant)
+)
+
+field_constant: (IDENTIFIER) @constant
+
+(BUILTINIDENTIFIER) @keyword
+
+; No idea why this doesnt work
+; ((BUILTINIDENTIFIER) @include
+;   (#any-of? @include "@import" "@cImport"))
+
+(INTEGER) @number
+
+(FLOAT) @float
+
+[
+  "true"
+  "false"
+] @boolean
+
+[
+  (LINESTRING)
+  (STRINGLITERALSINGLE)
+] @string
+
+(CHAR_LITERAL) @character
+(EscapeSequence) @string.escape
+(FormatSequence) @string.special
+
+(BreakLabel (IDENTIFIER) @label)
+(BlockLabel (IDENTIFIER) @label)
+
+[
+  "asm"
+  "defer"
+  "errdefer"
+  "test"
+  "struct"
+  "union"
+  "enum"
+  "opaque"
+  "error"
+] @keyword
+
+[
+  "async"
+  "await"
+  "suspend"
+  "nosuspend"
+  "resume"
+] @keyword.coroutine
+
+[
+  "fn"
+] @keyword.function
+
+[
+  "and"
+  "or"
+  "orelse"
+] @keyword.operator
+
+[
+  "return"
+] @keyword.return
+
+[
+  "if"
+  "else"
+  "switch"
+] @conditional
+
+[
+  "for"
+  "while"
+  "break"
+  "continue"
+] @keyword
+
+[
+  "usingnamespace"
+] @include
+
+[
+  "try"
+  "catch"
+] @keyword
+
+[
+  "anytype"
+  (BuildinTypeExpr)
+] @type.builtin
+
+[
+  "const"
+  "var"
+  "volatile"
+  "allowzero"
+  "noalias"
+] @type.qualifier
+
+[
+  "addrspace"
+  "align"
+  "callconv"
+  "linksection"
+] @storageclass
+
+[
+  "comptime"
+  "export"
+  "extern"
+  "inline"
+  "noinline"
+  "packed"
+  "pub"
+  "threadlocal"
+] @attribute
+
+[
+  "null"
+  "unreachable"
+  "undefined"
+] @constant.builtin
+
+[
+  (CompareOp)
+  (BitwiseOp)
+  (BitShiftOp)
+  (AdditionOp)
+  (AssignOp)
+  (MultiplyOp)
+  (PrefixOp)
+  "*"
+  "**"
+  "->"
+  ".?"
+  ".*"
+  "?"
+] @operator
+
+[
+  ";"
+  "."
+  ","
+  ":"
+] @punctuation.delimiter
+
+[
+  ".."
+  "..."
+] @punctuation.special
+
+[
+  "["
+  "]"
+  "("
+  ")"
+  "{"
+  "}"
+  (Payload "|")
+  (PtrPayload "|")
+  (PtrIndexPayload "|")
+] @punctuation.bracket
+
+; Error
+(ERROR) @error

crates/zed/src/languages/zig/indents.scm 🔗

@@ -0,0 +1,22 @@
+[
+  (Block)
+  (ContainerDecl)
+  (SwitchExpr)
+  (InitList)
+] @indent
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+] @branch
+
+[
+  (line_comment)
+  (container_doc_comment)
+  (doc_comment)
+  (LINESTRING)
+] @ignore