Add syntax highlighting and LSP (erlang_lsp) for Erlang (#7093)

Dairon M and Thanabodee Charoenpiriyakij created

This pull request implements support for the [Erlang
Language](https://erlang.org/).

**It adds:**

* [tree-sitter-erlang](https://github.com/WhatsApp/tree-sitter-erlang)
grammar
highlights (Licensed under Apache-2 from WhatsApp which is compatible
with Zed licensing model), folds and indents
* Erlang file icon based on the [official
one](https://www.erlang.org/doc/erlang-logo.png)
* [erlang_ls](https://github.com/erlang-ls/erlang_ls) support

Fixes https://github.com/zed-industries/zed/issues/4939, possibly a
duplicate of https://github.com/zed-industries/zed/pull/7085 with more
features. Suppose @wingyplus wants to join efforts here.

**To complete (out of scope for this PR):**

* Support for the ELP language server from WhatsApp. CC @robertoaloi
* Better indentation handling, need something like
`indentNextLinePattern` in VS Code

**Screenshots:**

![Screenshot 2024-01-30 at 11 03 51
AM](https://github.com/zed-industries/zed/assets/168440/5289c245-9edd-46b8-b443-d7b3210f6510)
![Screenshot 2024-01-30 at 11 01 19
AM](https://github.com/zed-industries/zed/assets/168440/bd22b322-5344-44e6-b5f7-6e352fb3deef)
![Screenshot 2024-01-30 at 11 01 37
AM](https://github.com/zed-industries/zed/assets/168440/f28f6a15-383e-4719-8a87-fceae5062436)
![Screenshot 2024-01-30 at 11 02 03
AM](https://github.com/zed-industries/zed/assets/168440/980d5213-0367-4a08-86eb-5743dfa628eb)
![Screenshot 2024-01-30 at 11 02 19
AM](https://github.com/zed-industries/zed/assets/168440/ea998891-604d-48d6-929f-ae4c1bb3fae1)

Outline: 
![Screenshot 2024-01-31 at 9 09 36
AM](https://github.com/zed-industries/zed/assets/168440/46d56d94-21c3-414d-84fb-9251fa2506ab)



**Release Notes:**

* Added Erlang Support
([7093](https://github.com/zed-industries/zed/pull/7093)).

---------

Signed-off-by: Thanabodee Charoenpiriyakij <wingyminus@gmail.com>
Co-authored-by: Thanabodee Charoenpiriyakij <wingyminus@gmail.com>

Change summary

Cargo.lock                                     |  11 
Cargo.toml                                     |   1 
assets/icons/file_icons/erlang.svg             |   0 
assets/icons/file_icons/file_types.json        |  19 +
crates/zed/Cargo.toml                          |   1 
crates/zed/src/languages.rs                    |   7 
crates/zed/src/languages/erlang.rs             |  58 +++++
crates/zed/src/languages/erlang/brackets.scm   |   3 
crates/zed/src/languages/erlang/config.toml    |  23 +
crates/zed/src/languages/erlang/folds.scm      |   9 
crates/zed/src/languages/erlang/highlights.scm | 231 ++++++++++++++++++++
crates/zed/src/languages/erlang/indents.scm    |   3 
crates/zed/src/languages/erlang/outline.scm    |  31 ++
docs/src/languages/erlang.md                   |   4 
14 files changed, 397 insertions(+), 4 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -8812,6 +8812,16 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-erlang"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93ced5145ebb17f83243bf055b74e108da7cc129e12faab4166df03f59b287f4"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-gitcommit"
 version = "0.3.3"
@@ -10392,6 +10402,7 @@ dependencies = [
  "tree-sitter-elixir",
  "tree-sitter-elm",
  "tree-sitter-embedded-template",
+ "tree-sitter-erlang",
  "tree-sitter-gitcommit",
  "tree-sitter-gleam",
  "tree-sitter-glsl",

Cargo.toml 🔗

@@ -142,6 +142,7 @@ tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev
 tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" }
 tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40" }
 tree-sitter-embedded-template = "0.20.0"
+tree-sitter-erlang = "0.4.0"
 tree-sitter-gitcommit = { git = "https://github.com/gbprod/tree-sitter-gitcommit" }
 tree-sitter-gleam = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "58b7cac8fc14c92b0677c542610d8738c373fa81" }
 tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" }

assets/icons/file_icons/file_types.json 🔗

@@ -1,7 +1,9 @@
 {
     "suffixes": {
+        "Emakefile": "erlang",
         "aac": "audio",
         "accdb": "storage",
+        "app.src": "erlang",
         "avif": "image",
         "bak": "backup",
         "bash": "terminal",
@@ -23,6 +25,8 @@
         "doc": "document",
         "docx": "document",
         "eex": "elixir",
+        "erl": "erlang",
+        "escript": "erlang",
         "eslintrc": "eslint",
         "eslintrc.js": "eslint",
         "eslintrc.json": "eslint",
@@ -37,17 +41,18 @@
         "gif": "image",
         "gitattributes": "vcs",
         "gitignore": "vcs",
-        "gitmodules": "vcs",
         "gitkeep": "vcs",
+        "gitmodules": "vcs",
         "go": "go",
         "h": "code",
         "handlebars": "code",
         "hbs": "template",
         "heex": "elixir",
         "heif": "image",
+        "hrl": "erlang",
+        "hs": "haskell",
         "htm": "template",
         "html": "template",
-        "hs": "haskell",
         "ib": "storage",
         "ico": "image",
         "ini": "settings",
@@ -85,6 +90,7 @@
         "psd": "image",
         "py": "python",
         "rb": "ruby",
+        "rebar.config": "erlang",
         "rkt": "code",
         "rs": "rust",
         "rtf": "document",
@@ -104,13 +110,15 @@
         "txt": "document",
         "vue": "vue",
         "wav": "audio",
-        "webp": "image",
         "webm": "video",
+        "webp": "image",
         "xls": "document",
         "xlsx": "document",
         "xml": "template",
+        "xrl": "erlang",
         "yaml": "yaml",
         "yml": "yaml",
+        "yrl": "erlang",
         "zlogin": "terminal",
         "zsh": "terminal",
         "zsh_aliases": "terminal",
@@ -133,7 +141,7 @@
             "icon": "icons/file_icons/folder.svg"
         },
         "css": {
-          "icon": "icons/file_icons/css.svg"
+            "icon": "icons/file_icons/css.svg"
         },
         "default": {
             "icon": "icons/file_icons/file.svg"
@@ -144,6 +152,9 @@
         "elixir": {
             "icon": "icons/file_icons/elixir.svg"
         },
+        "erlang": {
+            "icon": "icons/file_icons/erlang.svg"
+        },
         "eslint": {
             "icon": "icons/file_icons/eslint.svg"
         },

crates/zed/Cargo.toml 🔗

@@ -114,6 +114,7 @@ tree-sitter-css.workspace = true
 tree-sitter-elixir.workspace = true
 tree-sitter-elm.workspace = true
 tree-sitter-embedded-template.workspace = true
+tree-sitter-erlang.workspace = true
 tree-sitter-gitcommit.workspace = true
 tree-sitter-gleam.workspace = true
 tree-sitter-glsl.workspace = true

crates/zed/src/languages.rs 🔗

@@ -15,6 +15,7 @@ mod css;
 mod deno;
 mod elixir;
 mod elm;
+mod erlang;
 mod gleam;
 mod go;
 mod haskell;
@@ -113,6 +114,12 @@ pub fn init(
         ),
     }
     language("gitcommit", tree_sitter_gitcommit::language(), vec![]);
+    language(
+        "erlang",
+        tree_sitter_erlang::language(),
+        vec![Arc::new(erlang::ErlangLspAdapter)],
+    );
+
     language(
         "gleam",
         tree_sitter_gleam::language(),

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

@@ -0,0 +1,58 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use std::{any::Any, path::PathBuf};
+
+pub struct ErlangLspAdapter;
+
+#[async_trait]
+impl LspAdapter for ErlangLspAdapter {
+    fn name(&self) -> LanguageServerName {
+        LanguageServerName("erlang_ls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "erlang_ls"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(()) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _version: Box<dyn 'static + Send + Any>,
+        _container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        Err(anyhow!(
+            "erlang_ls must be installed and available in your $PATH"
+        ))
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "erlang_ls".into(),
+            arguments: vec![],
+        })
+    }
+
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "erlang_ls".into(),
+            arguments: vec!["--version".into()],
+        })
+    }
+}

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

@@ -0,0 +1,23 @@
+name = "Erlang"
+# TODO: support parsing rebar.config files
+# # https://github.com/WhatsApp/tree-sitter-erlang/issues/3
+path_suffixes = ["erl", "hrl", "app.src", "escript", "xrl", "yrl", "Emakefile", "rebar.config"]
+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"] },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+]
+# Indent if a line ends brackets, "->" or most keywords. Also if prefixed
+# with "||". This should work with most formatting models.
+# The ([^%]).* is to ensure this doesn't match inside comments.
+increase_indent_pattern = "^([^%]).*([{(\\[]]|\\->|after|begin|case|catch|fun|if|of|try|when|maybe|else|(\\|\\|.*))\\s*$"
+
+# Dedent after brackets, end or lone "->". The latter happens in a spec
+# with indented types, typically after "when". Only do this if it's _only_
+# preceded by whitespace.
+decrease_indent_pattern = "^\\s*([)}\\]]|end|else|\\->\\s*$)"

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

@@ -0,0 +1,231 @@
+;; Copyright (c) Facebook, Inc. and its affiliates.
+;;
+;; Licensed under the Apache License, Version 2.0 (the "License");
+;; you may not use this file except in compliance with the License.
+;; You may obtain a copy of the License at
+;;
+;;     http://www.apache.org/licenses/LICENSE-2.0
+;;
+;; Unless required by applicable law or agreed to in writing, software
+;; distributed under the License is distributed on an "AS IS" BASIS,
+;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+;; See the License for the specific language governing permissions and
+;; limitations under the License.
+;; ---------------------------------------------------------------------
+
+;; Based initially on the contents of https://github.com/WhatsApp/tree-sitter-erlang/issues/2 by @Wilfred
+;; and https://github.com/the-mikedavis/tree-sitter-erlang/blob/main/queries/highlights.scm
+;;
+;; The tests are also based on those in
+;; https://github.com/the-mikedavis/tree-sitter-erlang/tree/main/test/highlight
+;;
+
+
+;; First match wins in this file
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Attributes
+
+;; module attribute
+(module_attribute
+  name: (atom) @module)
+
+;; behaviour
+(behaviour_attribute name: (atom) @module)
+
+;; export
+
+;; Import attribute
+(import_attribute
+    module: (atom) @module)
+
+;; export_type
+
+;; optional_callbacks
+
+;; compile
+(compile_options_attribute
+    options: (tuple
+      expr: (atom)
+      expr: (list
+        exprs: (binary_op_expr
+          lhs: (atom)
+          rhs: (integer)))))
+
+;; file attribute
+
+;; record
+(record_decl name: (atom) @type)
+(record_decl name: (macro_call_expr name: (var) @constant))
+(record_field name: (atom) @property)
+
+;; type alias
+
+;; opaque
+
+;; Spec attribute
+(spec fun: (atom) @function)
+(spec
+  module: (module name: (atom) @module)
+  fun: (atom) @function)
+
+;; callback
+(callback fun: (atom) @function)
+
+;; fun decl
+
+;; include/include_lib
+
+;; ifdef/ifndef
+(pp_ifdef name: (_) @keyword.directive)
+(pp_ifndef name: (_) @keyword.directive)
+
+;; define
+(pp_define
+    lhs: (macro_lhs
+      name: (_) @keyword.directive
+      args: (var_args args: (var))))
+(pp_define
+    lhs: (macro_lhs
+      name: (var) @constant))
+
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Functions
+(fa fun: (atom) @function)
+(type_name name: (atom) @function)
+(call expr: (atom) @function)
+(function_clause name: (atom) @function)
+(internal_fun fun: (atom) @function)
+
+;; This is a fudge, we should check that the operator is '/'
+;; But our grammar does not (currently) provide it
+(binary_op_expr lhs: (atom) @function rhs: (integer))
+
+;; Others
+(remote_module module: (atom) @module)
+(remote fun: (atom) @function)
+(macro_call_expr name: (var) @keyword.directive args: (_) )
+(macro_call_expr name: (var) @constant)
+(macro_call_expr name: (atom) @keyword.directive)
+(record_field_name name: (atom) @property)
+(record_name name: (atom) @type)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Reserved words
+[ "after"
+  "and"
+  "band"
+  "begin"
+  "behavior"
+  "behaviour"
+  "bnot"
+  "bor"
+  "bsl"
+  "bsr"
+  "bxor"
+  "callback"
+  "case"
+  "catch"
+  "compile"
+  "define"
+  "deprecated"
+  "div"
+  "elif"
+  "else"
+  "end"
+  "endif"
+  "export"
+  "export_type"
+  "file"
+  "fun"
+  "if"
+  "ifdef"
+  "ifndef"
+  "import"
+  "include"
+  "include_lib"
+  "maybe"
+  "module"
+  "of"
+  "opaque"
+  "optional_callbacks"
+  "or"
+  "receive"
+  "record"
+  "spec"
+  "try"
+  "type"
+  "undef"
+  "unit"
+  "when"
+  "xor"] @keyword
+
+["andalso" "orelse"] @keyword.operator
+
+;; Punctuation
+["," "." ";"] @punctuation.delimiter
+["(" ")" "{" "}" "[" "]" "<<" ">>"] @punctuation.bracket
+
+;; Operators
+["!"
+ "->"
+ "<-"
+ "#"
+ "::"
+ "|"
+ ":"
+ "="
+ "||"
+
+ "+"
+ "-"
+ "bnot"
+ "not"
+
+ "/"
+ "*"
+ "div"
+ "rem"
+ "band"
+ "and"
+
+ "+"
+ "-"
+ "bor"
+ "bxor"
+ "bsl"
+ "bsr"
+ "or"
+ "xor"
+
+ "++"
+ "--"
+
+ "=="
+ "/="
+ "=<"
+ "<"
+ ">="
+ ">"
+ "=:="
+ "=/="
+ ] @operator
+
+;;; Comments
+((var) @comment.discard
+ (#match? @comment.discard "^_"))
+
+(dotdotdot) @comment.discard
+(comment) @comment
+
+;; Primitive types
+(string) @string
+(char) @constant
+(integer) @number
+(var) @variable
+(atom) @string.special.symbol
+
+;; wild attribute (Should take precedence over atoms, otherwise they are highlighted as atoms)
+(wild_attribute name: (attr_name name: (_) @keyword))

crates/zed/src/languages/erlang/outline.scm 🔗

@@ -0,0 +1,31 @@
+(module_attribute
+    "module" @context
+    name: (_) @name) @item
+
+(behaviour_attribute
+    "behaviour" @context
+    (atom) @name) @item
+
+(type_alias
+    "type" @context
+    name: (_) @name) @item
+
+(opaque
+    "opaque" @context
+    name: (_) @name) @item
+
+(pp_define
+    "define" @context
+    lhs: (_) @name) @item
+
+(record_decl
+    "record" @context
+    name: (_) @name) @item
+
+(callback
+    "callback" @context
+    fun: (_) @function ( (_) @name)) @item
+
+(fun_decl (function_clause
+    name: (_) @name
+    args: (_) @context)) @item

docs/src/languages/erlang.md 🔗

@@ -0,0 +1,4 @@
+# Erlang
+
+- Tree Sitter: [tree-sitter-erlang](https://github.com/WhatsApp/tree-sitter-erlang)
+- Language Server: [erlang_ls](https://github.com/erlang-ls/erlang_ls)