From 50b9e5d8d291dadbf8a30355b5f10145247b83c1 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 25 Jan 2024 18:44:35 -0500 Subject: [PATCH] Add Gleam support (#6733) This PR adds support for [Gleam](https://gleam.run/). Screenshot 2024-01-25 at 6 39 18 PM Screenshot 2024-01-25 at 6 39 37 PM Screenshot 2024-01-25 at 6 39 55 PM 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)). --- 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(+) create mode 100644 crates/zed/src/languages/gleam.rs create mode 100644 crates/zed/src/languages/gleam/config.toml create mode 100644 crates/zed/src/languages/gleam/highlights.scm create mode 100644 crates/zed/src/languages/gleam/outline.scm diff --git a/Cargo.lock b/Cargo.lock index 41987a70b8..fc1217161a 100644 --- a/Cargo.lock +++ b/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", diff --git a/Cargo.toml b/Cargo.toml index 121d42223b..4c3658b1a3 100644 --- a/Cargo.toml +++ b/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" } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 683a7c6fed..d1a83e0b3f 100644 --- a/crates/zed/Cargo.toml +++ b/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 diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 3fdcad46fe..0ad95b9fde 100644 --- a/crates/zed/src/languages.rs +++ b/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(), diff --git a/crates/zed/src/languages/gleam.rs b/crates/zed/src/languages/gleam.rs new file mode 100644 index 0000000000..90b10a3657 --- /dev/null +++ b/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 { + 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> { + 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, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().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 { + get_cached_server_binary(container_dir).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + 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 { + 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() +} diff --git a/crates/zed/src/languages/gleam/config.toml b/crates/zed/src/languages/gleam/config.toml new file mode 100644 index 0000000000..a841862709 --- /dev/null +++ b/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"] }, +] diff --git a/crates/zed/src/languages/gleam/highlights.scm b/crates/zed/src/languages/gleam/highlights.scm new file mode 100644 index 0000000000..a95f6cb031 --- /dev/null +++ b/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 diff --git a/crates/zed/src/languages/gleam/outline.scm b/crates/zed/src/languages/gleam/outline.scm new file mode 100644 index 0000000000..7d983f5947 --- /dev/null +++ b/crates/zed/src/languages/gleam/outline.scm @@ -0,0 +1,4 @@ +(function + (visibility_modifier)? @context + "fn" @context + name: (_) @name) @item