Add PureScript LSP/Highlighting support (#6911)

Ivรกn Molina Rebolledo created

This PR adds basic support for the language using the
`purescript-language-server`.

Release Notes:

- Added support for PureScript.

Change summary

Cargo.lock                                         |  10 +
Cargo.toml                                         |   1 
crates/zed/Cargo.toml                              |   1 
crates/zed/src/languages.rs                        |   8 
crates/zed/src/languages/purescript.rs             | 139 +++++++++++++++
crates/zed/src/languages/purescript/brackets.scm   |   3 
crates/zed/src/languages/purescript/config.toml    |  13 +
crates/zed/src/languages/purescript/highlights.scm | 144 ++++++++++++++++
crates/zed/src/languages/purescript/indents.scm    |   3 
9 files changed, 322 insertions(+)

Detailed changes

Cargo.lock ๐Ÿ”—

@@ -8549,6 +8549,15 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-purescript"
+version = "1.0.0"
+source = "git+https://github.com/ivanmoreau/tree-sitter-purescript?rev=a37140f0c7034977b90faa73c94fcb8a5e45ed08#a37140f0c7034977b90faa73c94fcb8a5e45ed08"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-python"
 version = "0.20.4"
@@ -9739,6 +9748,7 @@ dependencies = [
  "tree-sitter-nix",
  "tree-sitter-nu",
  "tree-sitter-php",
+ "tree-sitter-purescript",
  "tree-sitter-python",
  "tree-sitter-racket",
  "tree-sitter-ruby",

Cargo.toml ๐Ÿ”—

@@ -148,6 +148,7 @@ tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", re
 tree-sitter-rust = "0.20.3"
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
 tree-sitter-php = "0.21.1"
+tree-sitter-purescript = { git = "https://github.com/ivanmoreau/tree-sitter-purescript", rev = "a37140f0c7034977b90faa73c94fcb8a5e45ed08" }
 tree-sitter-python = "0.20.2"
 tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
 tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }

crates/zed/Cargo.toml ๐Ÿ”—

@@ -134,6 +134,7 @@ tree-sitter-ruby.workspace = true
 tree-sitter-haskell.workspace = true
 tree-sitter-html.workspace = true
 tree-sitter-php.workspace = true
+tree-sitter-purescript.workspace = true
 tree-sitter-scheme.workspace = true
 tree-sitter-svelte.workspace = true
 tree-sitter-racket.workspace = true

crates/zed/src/languages.rs ๐Ÿ”—

@@ -23,6 +23,7 @@ mod language_plugin;
 mod lua;
 mod nu;
 mod php;
+mod purescript;
 mod python;
 mod ruby;
 mod rust;
@@ -258,6 +259,13 @@ pub fn init(
         ],
     );
 
+    language(
+        "purescript",
+        tree_sitter_purescript::language(),
+        vec![Arc::new(purescript::PurescriptLspAdapter::new(
+            node_runtime.clone(),
+        ))],
+    );
     language("elm", tree_sitter_elm::language(), vec![]);
     language("glsl", tree_sitter_glsl::language(), vec![]);
     language("nix", tree_sitter_nix::language(), vec![]);

crates/zed/src/languages/purescript.rs ๐Ÿ”—

@@ -0,0 +1,139 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use collections::HashMap;
+use futures::StreamExt;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use serde_json::json;
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::{async_maybe, ResultExt};
+
+const SERVER_PATH: &'static str = "node_modules/.bin/purescript-language-server";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct PurescriptLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl PurescriptLspAdapter {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        Self { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for PurescriptLspAdapter {
+    fn name(&self) -> LanguageServerName {
+        LanguageServerName("purescript-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "purescript"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("purescript-language-server")
+                .await?,
+        ) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("purescript-language-server", version.as_str())],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "purescript": {
+                "addSpagoSources": true
+            }
+        }))
+    }
+
+    fn language_ids(&self) -> HashMap<String, String> {
+        [("PureScript".into(), "purescript".into())]
+            .into_iter()
+            .collect()
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    async_maybe!({
+        let mut last_version_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_version_dir = Some(entry.path());
+            }
+        }
+
+        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let server_path = last_version_dir.join(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })
+    .await
+    .log_err()
+}

crates/zed/src/languages/purescript/config.toml ๐Ÿ”—

@@ -0,0 +1,13 @@
+name = "PureScript"
+path_suffixes = ["purs"]
+autoclose_before = ",=)}]"
+line_comment = "-- "
+block_comment = ["{- ", " -}"]
+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 },
+    { start = "'", end = "'", close = true, newline = false },
+    { start = "`", end = "`", close = true, newline = false },
+]

crates/zed/src/languages/purescript/highlights.scm ๐Ÿ”—

@@ -0,0 +1,144 @@
+;; Copyright 2022 nvim-treesitter
+;;
+;; 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.
+
+;; ----------------------------------------------------------------------------
+;; Literals and comments
+
+(integer) @number
+(exp_negation) @number
+(exp_literal (number)) @float
+(char) @character
+[
+  (string)
+  (triple_quote_string)
+] @string
+
+(comment) @comment
+
+
+;; ----------------------------------------------------------------------------
+;; Punctuation
+
+[
+  "("
+  ")"
+  "{"
+  "}"
+  "["
+  "]"
+] @punctuation.bracket
+
+[
+  (comma)
+  ";"
+] @punctuation.delimiter
+
+
+;; ----------------------------------------------------------------------------
+;; Keywords, operators, includes
+
+[
+  "forall"
+  "โˆ€"
+] @keyword
+
+;; (pragma) @constant
+
+[
+  "if"
+  "then"
+  "else"
+  "case"
+  "of"
+] @keyword
+
+[
+  "import"
+  "module"
+] @keyword
+
+[
+  (operator)
+  (constructor_operator)
+  (type_operator)
+  (qualified_module)  ; grabs the `.` (dot), ex: import System.IO
+  (all_names)
+  (wildcard)
+  "="
+  "|"
+  "::"
+  "=>"
+  "->"
+  "<-"
+  "\\"
+  "`"
+  "@"
+  "โˆท"
+  "โ‡’"
+  "<="
+  "โ‡"
+  "โ†’"
+  "โ†"
+] @operator
+
+(module) @title
+
+[
+  (where)
+  "let"
+  "in"
+  "class"
+  "instance"
+  "derive"
+  "foreign"
+  "data"
+  "newtype"
+  "type"
+  "as"
+  "hiding"
+  "do"
+  "ado"
+  "infix"
+  "infixl"
+  "infixr"
+] @keyword
+
+
+;; ----------------------------------------------------------------------------
+;; Functions and variables
+
+(variable) @variable
+(pat_wildcard) @variable
+
+(signature name: (variable) @type)
+(function
+  name: (variable) @function
+  patterns: (patterns))
+
+
+(exp_infix (exp_name) @function (#set! "priority" 101))
+(exp_apply . (exp_name (variable) @function))
+(exp_apply . (exp_name (qualified_variable (variable) @function)))
+
+
+;; ----------------------------------------------------------------------------
+;; Types
+
+(type) @type
+(type_variable) @type
+
+(constructor) @constructor
+
+; True or False
+((constructor) @_bool (#match? @_bool "(True|False)")) @boolean