Add Gleam support (#6733)

Marshall Bowers created

This PR adds support for [Gleam](https://gleam.run/).

<img width="1320" alt="Screenshot 2024-01-25 at 6 39 18 PM"
src="https://github.com/zed-industries/zed/assets/1486634/7891b6e9-d7dc-46a0-b7c5-8aa7854c1f35">

<img width="757" alt="Screenshot 2024-01-25 at 6 39 37 PM"
src="https://github.com/zed-industries/zed/assets/1486634/f7ce6b3f-6175-45cb-8547-cfd286d918c6">

<img width="694" alt="Screenshot 2024-01-25 at 6 39 55 PM"
src="https://github.com/zed-industries/zed/assets/1486634/b0838027-c377-47e6-bdd1-bdc9b67a8672">

There are still some areas of improvement, like extending what
constructs we support in the outline view, but this is a good start.

Release Notes:

- Added Gleam support
([#5162](https://github.com/zed-industries/zed/issues/5162)).

Change summary

Cargo.lock                                    |  10 +
Cargo.toml                                    |   1 
crates/zed/Cargo.toml                         |   1 
crates/zed/src/languages.rs                   |   6 
crates/zed/src/languages/gleam.rs             | 118 +++++++++++++++++++
crates/zed/src/languages/gleam/config.toml    |  10 +
crates/zed/src/languages/gleam/highlights.scm | 130 +++++++++++++++++++++
crates/zed/src/languages/gleam/outline.scm    |   4 
8 files changed, 280 insertions(+)

Detailed changes

Cargo.lock 🔗

@@ -8503,6 +8503,15 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-gleam"
+version = "0.34.0"
+source = "git+https://github.com/gleam-lang/tree-sitter-gleam?rev=58b7cac8fc14c92b0677c542610d8738c373fa81#58b7cac8fc14c92b0677c542610d8738c373fa81"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-glsl"
 version = "0.1.4"
@@ -9781,6 +9790,7 @@ dependencies = [
  "tree-sitter-elixir",
  "tree-sitter-elm",
  "tree-sitter-embedded-template",
+ "tree-sitter-gleam",
  "tree-sitter-glsl",
  "tree-sitter-go",
  "tree-sitter-heex",

Cargo.toml 🔗

@@ -140,6 +140,7 @@ tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir"
 tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40"}
 tree-sitter-embedded-template = "0.20.0"
 tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" }
+tree-sitter-gleam = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "58b7cac8fc14c92b0677c542610d8738c373fa81" }
 tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
 tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
 tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }

crates/zed/Cargo.toml 🔗

@@ -121,6 +121,7 @@ tree-sitter-elixir.workspace = true
 tree-sitter-elm.workspace = true
 tree-sitter-embedded-template.workspace = true
 tree-sitter-glsl.workspace = true
+tree-sitter-gleam.workspace = true
 tree-sitter-go.workspace = true
 tree-sitter-heex.workspace = true
 tree-sitter-json.workspace = true

crates/zed/src/languages.rs 🔗

@@ -12,6 +12,7 @@ use self::elixir::ElixirSettings;
 mod c;
 mod css;
 mod elixir;
+mod gleam;
 mod go;
 mod html;
 mod json;
@@ -99,6 +100,11 @@ pub fn init(
         ),
     }
 
+    language(
+        "gleam",
+        tree_sitter_gleam::language(),
+        vec![Arc::new(gleam::GleamLspAdapter)],
+    );
     language(
         "go",
         tree_sitter_go::language(),

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

@@ -0,0 +1,118 @@
+use std::any::Any;
+use std::ffi::OsString;
+use std::path::PathBuf;
+
+use anyhow::{anyhow, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_tar::Archive;
+use async_trait::async_trait;
+use futures::io::BufReader;
+use futures::StreamExt;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use smol::fs;
+use util::github::{latest_github_release, GitHubLspBinaryVersion};
+use util::{async_maybe, ResultExt};
+
+fn server_binary_arguments() -> Vec<OsString> {
+    vec!["lsp".into()]
+}
+
+pub struct GleamLspAdapter;
+
+#[async_trait]
+impl LspAdapter for GleamLspAdapter {
+    fn name(&self) -> LanguageServerName {
+        LanguageServerName("gleam".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "gleam"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release =
+            latest_github_release("gleam-lang/gleam", false, delegate.http_client()).await?;
+
+        let asset_name = format!(
+            "gleam-{version}-{arch}-apple-darwin.tar.gz",
+            version = release.name,
+            arch = std::env::consts::ARCH
+        );
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+        Ok(Box::new(GitHubLspBinaryVersion {
+            name: release.name,
+            url: asset.browser_download_url.clone(),
+        }))
+    }
+
+    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("gleam");
+
+        if fs::metadata(&binary_path).await.is_err() {
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let archive = Archive::new(decompressed_bytes);
+            archive.unpack(container_dir).await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: server_binary_arguments(),
+        })
+    }
+
+    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!["--version".into()];
+                binary
+            })
+    }
+}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async_maybe!({
+        let mut last = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            last = Some(entry?.path());
+        }
+
+        anyhow::Ok(LanguageServerBinary {
+            path: last.ok_or_else(|| anyhow!("no cached binary"))?,
+            arguments: server_binary_arguments(),
+        })
+    })
+    .await
+    .log_err()
+}

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

@@ -0,0 +1,10 @@
+name = "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"] },
+]

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

@@ -0,0 +1,130 @@
+; Comments
+(module_comment) @comment
+(statement_comment) @comment
+(comment) @comment
+
+; Constants
+(constant
+  name: (identifier) @constant)
+
+; 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)$"))
+
+; Variables
+(identifier) @variable
+(discard) @comment.unused
+
+; 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