From a159183f52726527782bed97aeca0fd673cbacb8 Mon Sep 17 00:00:00 2001 From: Paulo Roberto de Oliveira Castro Date: Sat, 10 Feb 2024 18:28:48 -0300 Subject: [PATCH] Add Clojure language support with tree-sitter and LSP (#6988) Current limitations: * Not able to navigate into JAR files Release Notes: - Added Clojure language support --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- Cargo.lock | 10 ++ Cargo.toml | 1 + crates/copilot/src/copilot.rs | 1 - crates/project/src/project.rs | 66 ++++++++- crates/workspace/src/workspace.rs | 21 +++ crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 3 + crates/zed/src/languages/clojure.rs | 136 ++++++++++++++++++ crates/zed/src/languages/clojure/brackets.scm | 3 + crates/zed/src/languages/clojure/config.toml | 12 ++ .../zed/src/languages/clojure/highlights.scm | 41 ++++++ crates/zed/src/languages/clojure/indents.scm | 3 + crates/zed/src/languages/clojure/outline.scm | 1 + docs/src/languages/clojure.md | 4 + 14 files changed, 300 insertions(+), 3 deletions(-) create mode 100644 crates/zed/src/languages/clojure.rs create mode 100644 crates/zed/src/languages/clojure/brackets.scm create mode 100644 crates/zed/src/languages/clojure/config.toml create mode 100644 crates/zed/src/languages/clojure/highlights.scm create mode 100644 crates/zed/src/languages/clojure/indents.scm create mode 100644 crates/zed/src/languages/clojure/outline.scm create mode 100644 docs/src/languages/clojure.md diff --git a/Cargo.lock b/Cargo.lock index d05fc4ae93..38ecaa8f52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9149,6 +9149,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-clojure" +version = "0.0.9" +source = "git+https://github.com/prcastro/tree-sitter-clojure?branch=update-ts#38b4f8d264248b2fd09575fbce66f7c22e8929d5" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-cpp" version = "0.20.0" @@ -10841,6 +10850,7 @@ dependencies = [ "tree-sitter-beancount", "tree-sitter-c", "tree-sitter-c-sharp", + "tree-sitter-clojure", "tree-sitter-cpp", "tree-sitter-css", "tree-sitter-elixir", diff --git a/Cargo.toml b/Cargo.toml index 84627885e0..4b936c4cfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -226,6 +226,7 @@ tree-sitter = { version = "0.20", features = ["wasm"] } tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" } tree-sitter-beancount = { git = "https://github.com/polarmutex/tree-sitter-beancount", rev = "da1bf8c6eb0ae7a97588affde7227630bcd678b6" } tree-sitter-c = "0.20.1" +tree-sitter-clojure = { git = "https://github.com/prcastro/tree-sitter-clojure", branch = "update-ts"} tree-sitter-c-sharp = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", rev = "dd5e59721a5f8dae34604060833902b882023aaf" } tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "f44509141e7e483323d2ec178f2d2e6c0fc041c1" } tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 6b619e4979..bce829fa59 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -444,7 +444,6 @@ impl Copilot { |_, _| { /* Silence the notification */ }, ) .detach(); - let server = cx.update(|cx| server.initialize(None, cx))?.await?; let status = server diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index cfd2f6412c..ce9b64a1d7 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -28,7 +28,7 @@ use futures::{ use globset::{Glob, GlobSet, GlobSetBuilder}; use gpui::{ AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, Context, Entity, EventEmitter, - Model, ModelContext, Task, WeakModel, + Model, ModelContext, PromptLevel, Task, WeakModel, }; use itertools::Itertools; use language::{ @@ -47,7 +47,8 @@ use language::{ use log::error; use lsp::{ DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, - DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf, + DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, + MessageActionItem, OneOf, }; use lsp_command::*; use node_runtime::NodeRuntime; @@ -213,12 +214,37 @@ enum ProjectClientState { }, } +/// A prompt requested by LSP server. +#[derive(Clone, Debug)] +pub struct LanguageServerPromptRequest { + pub level: PromptLevel, + pub message: String, + pub actions: Vec, + response_channel: Sender, +} + +impl LanguageServerPromptRequest { + pub async fn respond(self, index: usize) -> Option<()> { + if let Some(response) = self.actions.into_iter().nth(index) { + self.response_channel.send(response).await.ok() + } else { + None + } + } +} +impl PartialEq for LanguageServerPromptRequest { + fn eq(&self, other: &Self) -> bool { + self.message == other.message && self.actions == other.actions + } +} + #[derive(Clone, Debug, PartialEq)] pub enum Event { LanguageServerAdded(LanguageServerId), LanguageServerRemoved(LanguageServerId), LanguageServerLog(LanguageServerId, String), Notification(String), + LanguageServerPrompt(LanguageServerPromptRequest), ActiveEntryChanged(Option), ActivateProjectPanel, WorktreeAdded, @@ -3105,6 +3131,42 @@ impl Project { }) .detach(); + language_server + .on_request::({ + let this = this.clone(); + move |params, mut cx| { + let this = this.clone(); + async move { + if let Some(actions) = params.actions { + let (tx, mut rx) = smol::channel::bounded(1); + let request = LanguageServerPromptRequest { + level: match params.typ { + lsp::MessageType::ERROR => PromptLevel::Critical, + lsp::MessageType::WARNING => PromptLevel::Warning, + _ => PromptLevel::Info, + }, + message: params.message, + actions, + response_channel: tx, + }; + + if let Ok(_) = this.update(&mut cx, |_, cx| { + cx.emit(Event::LanguageServerPrompt(request)); + }) { + let response = rx.next().await; + + Ok(response) + } else { + Ok(None) + } + } else { + Ok(None) + } + } + } + }) + .detach(); + let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token.clone(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a4af8170aa..7562de9094 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -571,6 +571,27 @@ impl Workspace { cx.new_view(|_| MessageNotification::new(message.clone())) }), + project::Event::LanguageServerPrompt(request) => { + let request = request.clone(); + + cx.spawn(|_, mut cx| async move { + let messages = request + .actions + .iter() + .map(|action| action.title.as_str()) + .collect::>(); + let index = cx + .update(|cx| { + cx.prompt(request.level, "", Some(&request.message), &messages) + })? + .await?; + request.respond(index).await; + + Result::<(), anyhow::Error>::Ok(()) + }) + .detach() + } + _ => {} } cx.notify() diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 05b74515d5..fc957ad559 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -112,6 +112,7 @@ tree-sitter-bash.workspace = true tree-sitter-beancount.workspace = true tree-sitter-c-sharp.workspace = true tree-sitter-c.workspace = true +tree-sitter-clojure.workspace = true tree-sitter-cpp.workspace = true tree-sitter-css.workspace = true tree-sitter-elixir.workspace = true diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 3b78b23225..59e4be70d7 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -10,6 +10,7 @@ use util::asset_str; use self::{deno::DenoSettings, elixir::ElixirSettings}; mod c; +mod clojure; mod csharp; mod css; mod deno; @@ -68,6 +69,7 @@ pub fn init( ("beancount", tree_sitter_beancount::language()), ("c", tree_sitter_c::language()), ("c_sharp", tree_sitter_c_sharp::language()), + ("clojure", tree_sitter_clojure::language()), ("cpp", tree_sitter_cpp::language()), ("css", tree_sitter_css::language()), ("elixir", tree_sitter_elixir::language()), @@ -131,6 +133,7 @@ pub fn init( language("bash", vec![]); language("beancount", vec![]); language("c", vec![Arc::new(c::CLspAdapter) as Arc]); + language("clojure", vec![Arc::new(clojure::ClojureLspAdapter)]); language("cpp", vec![Arc::new(c::CLspAdapter)]); language("csharp", vec![Arc::new(csharp::OmniSharpAdapter {})]); language( diff --git a/crates/zed/src/languages/clojure.rs b/crates/zed/src/languages/clojure.rs new file mode 100644 index 0000000000..e200b9c6a0 --- /dev/null +++ b/crates/zed/src/languages/clojure.rs @@ -0,0 +1,136 @@ +use anyhow::{anyhow, bail, Context, Result}; +use async_trait::async_trait; +pub use language::*; +use lsp::LanguageServerBinary; +use smol::fs::{self, File}; +use std::{any::Any, env::consts, path::PathBuf}; +use util::{ + fs::remove_matching, + github::{latest_github_release, GitHubLspBinaryVersion}, +}; + +#[derive(Copy, Clone)] +pub struct ClojureLspAdapter; + +#[async_trait] +impl super::LspAdapter for ClojureLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("clojure-lsp".into()) + } + + fn short_name(&self) -> &'static str { + "clojure" + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result> { + let release = latest_github_release( + "clojure-lsp/clojure-lsp", + true, + false, + delegate.http_client(), + ) + .await?; + let platform = match consts::ARCH { + "x86_64" => "amd64", + "aarch64" => "aarch64", + other => bail!("Running on unsupported platform: {other}"), + }; + let asset_name = format!("clojure-lsp-native-macos-{platform}.zip"); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + let version = GitHubLspBinaryVersion { + name: release.tag_name.clone(), + url: asset.browser_download_url.clone(), + }; + Ok(Box::new(version) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let zip_path = container_dir.join(format!("clojure-lsp_{}.zip", version.name)); + let folder_path = container_dir.join("bin"); + let binary_path = folder_path.join("clojure-lsp"); + + if fs::metadata(&binary_path).await.is_err() { + let mut response = delegate + .http_client() + .get(&version.url, Default::default(), true) + .await + .context("error downloading release")?; + let mut file = File::create(&zip_path) + .await + .with_context(|| format!("failed to create file {}", zip_path.display()))?; + if !response.status().is_success() { + return Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + futures::io::copy(response.body_mut(), &mut file).await?; + + fs::create_dir_all(&folder_path) + .await + .with_context(|| format!("failed to create directory {}", folder_path.display()))?; + + let unzip_status = smol::process::Command::new("unzip") + .arg(&zip_path) + .arg("-d") + .arg(&folder_path) + .output() + .await? + .status; + if !unzip_status.success() { + return Err(anyhow!("failed to unzip elixir-ls archive"))?; + } + + remove_matching(&container_dir, |entry| entry != folder_path).await; + } + + Ok(LanguageServerBinary { + path: binary_path, + arguments: vec![], + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + let binary_path = container_dir.join("bin").join("clojure-lsp"); + if binary_path.exists() { + Some(LanguageServerBinary { + path: binary_path, + arguments: vec![], + }) + } else { + None + } + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + let binary_path = container_dir.join("bin").join("clojure-lsp"); + if binary_path.exists() { + Some(LanguageServerBinary { + path: binary_path, + arguments: vec!["--version".into()], + }) + } else { + None + } + } +} diff --git a/crates/zed/src/languages/clojure/brackets.scm b/crates/zed/src/languages/clojure/brackets.scm new file mode 100644 index 0000000000..191fd9c084 --- /dev/null +++ b/crates/zed/src/languages/clojure/brackets.scm @@ -0,0 +1,3 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) diff --git a/crates/zed/src/languages/clojure/config.toml b/crates/zed/src/languages/clojure/config.toml new file mode 100644 index 0000000000..b773e88341 --- /dev/null +++ b/crates/zed/src/languages/clojure/config.toml @@ -0,0 +1,12 @@ +name = "Clojure" +grammar = "clojure" +path_suffixes = ["clj", "cljs"] +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"] }, +] +word_characters = ["-"] diff --git a/crates/zed/src/languages/clojure/highlights.scm b/crates/zed/src/languages/clojure/highlights.scm new file mode 100644 index 0000000000..f4abe3bcdd --- /dev/null +++ b/crates/zed/src/languages/clojure/highlights.scm @@ -0,0 +1,41 @@ +;; Literals + +(num_lit) @number + +[ + (char_lit) + (str_lit) +] @string + +[ + (bool_lit) + (nil_lit) +] @constant.builtin + +(kwd_lit) @constant + +;; Comments + +(comment) @comment + +;; Treat quasiquotation as operators for the purpose of highlighting. + +[ + "'" + "`" + "~" + "@" + "~@" +] @operator + + +(list_lit + . + (sym_lit) @function) + +(list_lit + . + (sym_lit) @keyword + (#match? @keyword + "^(do|if|let|var|fn|fn*|loop*|recur|throw|try|catch|finally|set!|new|quote|->|->>)$" + )) diff --git a/crates/zed/src/languages/clojure/indents.scm b/crates/zed/src/languages/clojure/indents.scm new file mode 100644 index 0000000000..9a1cbad161 --- /dev/null +++ b/crates/zed/src/languages/clojure/indents.scm @@ -0,0 +1,3 @@ +(_ "[" "]") @indent +(_ "{" "}") @indent +(_ "(" ")") @indent diff --git a/crates/zed/src/languages/clojure/outline.scm b/crates/zed/src/languages/clojure/outline.scm new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/zed/src/languages/clojure/outline.scm @@ -0,0 +1 @@ + diff --git a/docs/src/languages/clojure.md b/docs/src/languages/clojure.md new file mode 100644 index 0000000000..58ff9790e0 --- /dev/null +++ b/docs/src/languages/clojure.md @@ -0,0 +1,4 @@ +# Clojure + +- Tree Sitter: [tree-sitter-clojure](https://github.com/sogaiu/tree-sitter-clojure) +- Language Server: [clojure-lsp](https://github.com/clojure-lsp/clojure-lsp)