From ce7f7dc038fdb2ca6284e4ff8048fa3c7372dfde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20P=C5=99=C3=ADhoda?= Date: Sat, 10 Feb 2024 16:58:40 +0100 Subject: [PATCH] Add minimal Scala support - tree-sitter, Metals Adds - Scala syntax highlighting using `tree-sitter-scala` - basic Metals LSP support, without automatic installation Most of the tree-sitter queries have been picked up and repurposed from [tree-sitter-scala](https://github.com/tree-sitter/tree-sitter-scala) and [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter/tree/master/queries/scala) projects. --- Cargo.lock | 10 + Cargo.toml | 1 + crates/languages/Cargo.toml | 1 + crates/languages/src/lib.rs | 3 + crates/languages/src/scala.rs | 58 +++++ crates/languages/src/scala/brackets.scm | 3 + crates/languages/src/scala/config.toml | 15 ++ crates/languages/src/scala/highlights.scm | 265 ++++++++++++++++++++++ crates/languages/src/scala/indents.scm | 20 ++ crates/languages/src/scala/injections.scm | 15 ++ crates/languages/src/scala/outline.scm | 27 +++ 11 files changed, 418 insertions(+) create mode 100644 crates/languages/src/scala.rs create mode 100644 crates/languages/src/scala/brackets.scm create mode 100644 crates/languages/src/scala/config.toml create mode 100644 crates/languages/src/scala/highlights.scm create mode 100644 crates/languages/src/scala/indents.scm create mode 100644 crates/languages/src/scala/injections.scm create mode 100644 crates/languages/src/scala/outline.scm diff --git a/Cargo.lock b/Cargo.lock index d5f5377f05ec5..da429d81e2c39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5377,6 +5377,7 @@ dependencies = [ "tree-sitter-racket", "tree-sitter-ruby", "tree-sitter-rust", + "tree-sitter-scala", "tree-sitter-scheme", "tree-sitter-svelte", "tree-sitter-toml", @@ -10776,6 +10777,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-scala" +version = "0.20.0" +source = "git+https://github.com/tree-sitter/tree-sitter-scala.git?rev=45b5ba0e749a8477a8fd2666f082f352859bdc3c#45b5ba0e749a8477a8fd2666f082f352859bdc3c" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-scheme" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 9c0f65e1a28eb..49a6501a00dbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -303,6 +303,7 @@ tree-sitter-python = "0.20.2" tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a" } tree-sitter-ruby = "0.20.0" tree-sitter-rust = "0.20.3" +tree-sitter-scala = { git = "https://github.com/tree-sitter/tree-sitter-scala.git", rev = "45b5ba0e749a8477a8fd2666f082f352859bdc3c" } tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9" } tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "bd60db7d3d06f89b6ec3b287c9a6e9190b5564bd" } tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" } diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index facb1460c216c..e9c962df6e07c 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -72,6 +72,7 @@ tree-sitter-python.workspace = true tree-sitter-racket.workspace = true tree-sitter-ruby.workspace = true tree-sitter-rust.workspace = true +tree-sitter-scala.workspace = true tree-sitter-scheme.workspace = true tree-sitter-svelte.workspace = true tree-sitter-toml.workspace = true diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 5cd46edaea920..a750c30683319 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -34,6 +34,7 @@ mod purescript; mod python; mod ruby; mod rust; +mod scala; mod svelte; mod tailwind; mod terraform; @@ -109,6 +110,7 @@ pub fn init( ("racket", tree_sitter_racket::language()), ("ruby", tree_sitter_ruby::language()), ("rust", tree_sitter_rust::language()), + ("scala", tree_sitter_scala::language()), ("scheme", tree_sitter_scheme::language()), ("svelte", tree_sitter_svelte::language()), ("toml", tree_sitter_toml::language()), @@ -359,6 +361,7 @@ pub fn init( ))] ); language!("dart", vec![Arc::new(dart::DartLanguageServer {})]); + language!("scala", vec![Arc::new(scala::MetalsLspAdapter {})]); } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/languages/src/scala.rs b/crates/languages/src/scala.rs new file mode 100644 index 0000000000000..66066d704baf3 --- /dev/null +++ b/crates/languages/src/scala.rs @@ -0,0 +1,58 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +pub use language::*; +use lsp::LanguageServerBinary; +use std::{any::Any, path::PathBuf}; + +pub struct MetalsLspAdapter; + +#[async_trait] +impl LspAdapter for MetalsLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("metals".into()) + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(())) + } + + async fn fetch_server_binary( + &self, + _version: Box, + _container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + Err(anyhow!( + "metals must be installed and available in your $PATH" + )) + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + Some(LanguageServerBinary { + path: "metals".into(), + env: None, + arguments: vec![], + }) + } + + fn can_be_reinstalled(&self) -> bool { + false + } + + async fn installation_test_binary( + &self, + _container_dir: PathBuf, + ) -> Option { + None + } +} + +#[cfg(test)] +mod tests {} diff --git a/crates/languages/src/scala/brackets.scm b/crates/languages/src/scala/brackets.scm new file mode 100644 index 0000000000000..191fd9c084a52 --- /dev/null +++ b/crates/languages/src/scala/brackets.scm @@ -0,0 +1,3 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) diff --git a/crates/languages/src/scala/config.toml b/crates/languages/src/scala/config.toml new file mode 100644 index 0000000000000..ccb298b4d204e --- /dev/null +++ b/crates/languages/src/scala/config.toml @@ -0,0 +1,15 @@ +name = "Scala" +grammar = "scala" +path_suffixes = ["scala", "sbt", "sc"] +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 = ["comment", "string"] }, +{ start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] }, +{ start = "`", end = "`", close = true, newline = false, not_in = ["comment", "string"] }, +{ start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] } +] +collapsed_placeholder = " /* ... */ " diff --git a/crates/languages/src/scala/highlights.scm b/crates/languages/src/scala/highlights.scm new file mode 100644 index 0000000000000..36fa0a4c9b496 --- /dev/null +++ b/crates/languages/src/scala/highlights.scm @@ -0,0 +1,265 @@ +; CREDITS @stumash (stuart.mashaal@gmail.com) + +(class_definition + name: (identifier) @type) + +(enum_definition + name: (identifier) @enum) + +(object_definition + name: (identifier) @type) + +(trait_definition + name: (identifier) @type) + +(full_enum_case + name: (identifier) @type) + +(simple_enum_case + name: (identifier) @type) + +;; variables + +(class_parameter + name: (identifier) @property) + +(self_type (identifier) @property) + +(interpolation (identifier) @none) +(interpolation (block) @none) + +;; types + +(type_definition + name: (type_identifier) @type.definition) + +(type_identifier) @type + +;; val/var definitions/declarations + +(val_definition + pattern: (identifier) @variable) + +(var_definition + pattern: (identifier) @variable) + +(val_declaration + name: (identifier) @variable) + +(var_declaration + name: (identifier) @variable) + +; method definition + +(function_declaration + name: (identifier) @function.method) + +(function_definition + name: (identifier) @function.method) + +; imports/exports +((import_declaration + path: (identifier) @type) (#match? @type "^[A-Z]")) + +(import_declaration + path: (identifier) @namespace) + +((stable_identifier (identifier) @type) (#match? @type "^[A-Z]")) + +((stable_identifier (identifier) @namespace)) + +(export_declaration + path: (identifier) @namespace) +((stable_identifier (identifier) @namespace)) + +((export_declaration + path: (identifier) @type) (#match? @type "^[A-Z]")) +((stable_identifier (identifier) @type) (#match? @type "^[A-Z]")) + +((namespace_selectors (identifier) @type) (#match? @type "^[A-Z]")) + +; method invocation + +(call_expression + function: (identifier) @function) + +(call_expression + function: (operator_identifier) @function) + +(call_expression + function: (field_expression + field: (identifier) @function.method)) + +((call_expression + function: (identifier) @constructor) + (#match? @constructor "^[A-Z]")) + +(generic_function + function: (identifier) @function) + +(interpolated_string_expression + interpolator: (identifier) @function) + +; function definitions + +(function_definition + name: (identifier) @function) + +(parameter + name: (identifier) @parameter) + +(binding + name: (identifier) @parameter) + +; expressions + +(field_expression field: (identifier) @property) +(field_expression value: (identifier) @type + (#match? @type "^[A-Z]")) + +(infix_expression operator: (identifier) @operator) +(infix_expression operator: (operator_identifier) @operator) +(infix_type operator: (operator_identifier) @operator) +(infix_type operator: (operator_identifier) @operator) + +; literals + +(boolean_literal) @boolean +(integer_literal) @number +(floating_point_literal) @float + +[ + (symbol_literal) + (string) + (character_literal) + (interpolated_string_expression) +] @string + +(interpolation "$" @punctuation.special) + +;; keywords + +(opaque_modifier) @type.qualifier +(infix_modifier) @keyword +(transparent_modifier) @type.qualifier +(open_modifier) @type.qualifier + +[ + "case" + "class" + "enum" + "extends" + "derives" + "finally" +;; `forSome` existential types not implemented yet +;; `macro` not implemented yet + "object" + "override" + "package" + "trait" + "type" + "val" + "var" + "with" + "given" + "using" + "end" + "implicit" + "extension" + "with" +] @keyword + +[ + "abstract" + "final" + "lazy" + "sealed" + "private" + "protected" +] @type.qualifier + +(inline_modifier) @label + +(null_literal) @constant + +(wildcard) @parameter + +(annotation) @attribute + +;; special keywords + +"new" @operator + +[ + "else" + "if" + "match" + "then" +] @keyword + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "." + "," +] @punctuation.delimiter + +[ + "do" + "for" + "while" + "yield" +] @keyword + +"def" @keyword + +[ + "=>" + "<-" + "@" +] @operator + +["import" "export"] @keyword ; @include + +[ + "try" + "catch" + "throw" +] @keyword + +"return" @keyword + +[ + (comment) + (block_comment) + "_end_ident" +] @comment + +;; `case` is a conditional keyword in case_block + +(case_block + (case_clause ("case") @keyword)) +(indented_cases + (case_clause ("case") @keyword)) + +(operator_identifier) @operator + +((identifier) @type (#match? @type "^[A-Z]")) +((identifier) @variable.special + (#match? @variable.special "^this$")) + +( + (identifier) @function + (#match? @function "^super$") +) + +;; Scala CLI using directives +(using_directive_key) @parameter +(using_directive_value) @string diff --git a/crates/languages/src/scala/indents.scm b/crates/languages/src/scala/indents.scm new file mode 100644 index 0000000000000..e41027077ed42 --- /dev/null +++ b/crates/languages/src/scala/indents.scm @@ -0,0 +1,20 @@ +[ + (block) + (arguments) + (parameter) + (class_definition) + (trait_definition) + (object_definition) + (function_definition) + (val_definition) + (import_declaration) + (while_expression) + (do_while_expression) + (for_expression) + (try_expression) + (match_expression) +] @indent + +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/languages/src/scala/injections.scm b/crates/languages/src/scala/injections.scm new file mode 100644 index 0000000000000..482c7a31f5080 --- /dev/null +++ b/crates/languages/src/scala/injections.scm @@ -0,0 +1,15 @@ +([(comment) (block_comment)] @injection.content + (#set! injection.language "comment")) + + +; TODO for some reason multiline string (triple quotes) interpolation works only if it contains interpolated value +; Matches these SQL interpolators: +; - Doobie: 'sql', 'fr' +; - Quill: 'sql', 'infix' +; - Slick: 'sql', 'sqlu' +(interpolated_string_expression + interpolator: + ((identifier) @interpolator + (#any-of? @interpolator "fr" "infix" "sql" "sqlu")) + (interpolated_string) @injection.content + (#set! injection.language "sql")) diff --git a/crates/languages/src/scala/outline.scm b/crates/languages/src/scala/outline.scm new file mode 100644 index 0000000000000..64ced7bd81c13 --- /dev/null +++ b/crates/languages/src/scala/outline.scm @@ -0,0 +1,27 @@ +(class_definition + "class" @context + name: (_) @name) @item + +(enum_definition + "enum" @context + name: (_) @name) @item + +(object_definition + "object" @context + name: (_) @name) @item + +(trait_definition + "trait" @context + name: (_) @name) @item + +(type_definition + "type" @context + name: (_) @name) @item + +(function_definition + "def" @context + name: (_) @name) @item + +(val_definition + "val" @context + pattern: (identifier) @name) @item