diff --git a/Cargo.lock b/Cargo.lock index ed428585bf..7b3a5ba6f3 100644 --- a/Cargo.lock +++ b/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", diff --git a/Cargo.toml b/Cargo.toml index 00d69c8786..f46d32a53c 100644 --- a/Cargo.toml +++ b/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" } diff --git a/assets/icons/file_icons/erlang.svg b/assets/icons/file_icons/erlang.svg new file mode 100644 index 0000000000..2dd57910b8 --- /dev/null +++ b/assets/icons/file_icons/erlang.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 993dfe53b0..8cf7b314c9 100644 --- a/assets/icons/file_icons/file_types.json +++ b/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" }, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index f17d3053a0..b87b79e2f0 100644 --- a/crates/zed/Cargo.toml +++ b/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 diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index f0217af1d9..add9f9c192 100644 --- a/crates/zed/src/languages.rs +++ b/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(), diff --git a/crates/zed/src/languages/erlang.rs b/crates/zed/src/languages/erlang.rs new file mode 100644 index 0000000000..b50b6e7564 --- /dev/null +++ b/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> { + Ok(Box::new(()) as Box<_>) + } + + async fn fetch_server_binary( + &self, + _version: Box, + _container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + Err(anyhow!( + "erlang_ls must be installed and available in your $PATH" + )) + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + Some(LanguageServerBinary { + path: "erlang_ls".into(), + arguments: vec![], + }) + } + + fn can_be_reinstalled(&self) -> bool { + false + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + Some(LanguageServerBinary { + path: "erlang_ls".into(), + arguments: vec!["--version".into()], + }) + } +} diff --git a/crates/zed/src/languages/erlang/brackets.scm b/crates/zed/src/languages/erlang/brackets.scm new file mode 100644 index 0000000000..191fd9c084 --- /dev/null +++ b/crates/zed/src/languages/erlang/brackets.scm @@ -0,0 +1,3 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) diff --git a/crates/zed/src/languages/erlang/config.toml b/crates/zed/src/languages/erlang/config.toml new file mode 100644 index 0000000000..5f92c0fe27 --- /dev/null +++ b/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*$)" diff --git a/crates/zed/src/languages/erlang/folds.scm b/crates/zed/src/languages/erlang/folds.scm new file mode 100644 index 0000000000..65c2d8ed19 --- /dev/null +++ b/crates/zed/src/languages/erlang/folds.scm @@ -0,0 +1,9 @@ +[ + (fun_decl) + (anonymous_fun) + (case_expr) + (maybe_expr) + (map_expr) + (export_attribute) + (export_type_attribute) +] @fold diff --git a/crates/zed/src/languages/erlang/highlights.scm b/crates/zed/src/languages/erlang/highlights.scm new file mode 100644 index 0000000000..c4abf04776 --- /dev/null +++ b/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)) diff --git a/crates/zed/src/languages/erlang/indents.scm b/crates/zed/src/languages/erlang/indents.scm new file mode 100644 index 0000000000..112b414aa4 --- /dev/null +++ b/crates/zed/src/languages/erlang/indents.scm @@ -0,0 +1,3 @@ +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed/src/languages/erlang/outline.scm b/crates/zed/src/languages/erlang/outline.scm new file mode 100644 index 0000000000..294f109702 --- /dev/null +++ b/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 diff --git a/docs/src/languages/erlang.md b/docs/src/languages/erlang.md new file mode 100644 index 0000000000..3343168faf --- /dev/null +++ b/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)