From e5c4c8522b78a702430b618a9292958505e041cb Mon Sep 17 00:00:00 2001 From: fminkowski Date: Tue, 30 Jan 2024 12:54:39 -0500 Subject: [PATCH] C# Support: Add treesitter and OmniSharp LSP support (#6908) This PR adds the C# tree-sitter grammar. It also adds OmniSharp-Roslyn for LSP support. Resolves issue [#5299](https://github.com/zed-industries/zed/issues/5299) Release Notes: - Added C# support ## VSCode vscode ## Zed zed --- Cargo.lock | 10 + Cargo.toml | 1 + crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 6 + crates/zed/src/languages/csharp.rs | 144 ++++++++++ crates/zed/src/languages/csharp/config.toml | 12 + .../zed/src/languages/csharp/highlights.scm | 254 ++++++++++++++++++ .../zed/src/languages/csharp/injections.scm | 2 + crates/zed/src/languages/csharp/outline.scm | 38 +++ docs/src/languages/csharp.md | 4 + 10 files changed, 472 insertions(+) create mode 100644 crates/zed/src/languages/csharp.rs create mode 100644 crates/zed/src/languages/csharp/config.toml create mode 100644 crates/zed/src/languages/csharp/highlights.scm create mode 100644 crates/zed/src/languages/csharp/injections.scm create mode 100644 crates/zed/src/languages/csharp/outline.scm create mode 100644 docs/src/languages/csharp.md diff --git a/Cargo.lock b/Cargo.lock index 3817ba633c..462c7ef795 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8455,6 +8455,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-c-sharp" +version = "0.20.0" +source = "git+https://github.com/tree-sitter/tree-sitter-c-sharp?rev=dd5e59721a5f8dae34604060833902b882023aaf#dd5e59721a5f8dae34604060833902b882023aaf" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-cpp" version = "0.20.0" @@ -9813,6 +9822,7 @@ dependencies = [ "tree-sitter", "tree-sitter-bash", "tree-sitter-c", + "tree-sitter-c-sharp", "tree-sitter-cpp", "tree-sitter-css", "tree-sitter-elixir", diff --git a/Cargo.toml b/Cargo.toml index eaa09c4c8f..0d49975536 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,6 +136,7 @@ uuid = { version = "1.1.2", features = ["v4"] } tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" } tree-sitter-c = "0.20.1" tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" } +tree-sitter-c-sharp = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", rev = "dd5e59721a5f8dae34604060833902b882023aaf" } tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } 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"} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 7736f7a97b..ec0241dac8 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -115,6 +115,7 @@ tree-sitter.workspace = true tree-sitter-bash.workspace = true tree-sitter-c.workspace = true tree-sitter-cpp.workspace = true +tree-sitter-c-sharp.workspace = true tree-sitter-css.workspace = true tree-sitter-elixir.workspace = true tree-sitter-elm.workspace = true diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index b487210050..0faeb358fd 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -10,6 +10,7 @@ use util::{asset_str, paths::PLUGINS_DIR}; use self::{deno::DenoSettings, elixir::ElixirSettings}; mod c; +mod csharp; mod css; mod deno; mod elixir; @@ -73,6 +74,11 @@ pub fn init( tree_sitter_cpp::language(), vec![Arc::new(c::CLspAdapter)], ); + language( + "csharp", + tree_sitter_c_sharp::language(), + vec![Arc::new(csharp::OmniSharpAdapter {})], + ); language( "css", tree_sitter_css::language(), diff --git a/crates/zed/src/languages/csharp.rs b/crates/zed/src/languages/csharp.rs new file mode 100644 index 0000000000..e70bc78279 --- /dev/null +++ b/crates/zed/src/languages/csharp.rs @@ -0,0 +1,144 @@ +use anyhow::{anyhow, Context, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; +use async_trait::async_trait; +use futures::{io::BufReader, StreamExt}; +use language::{LanguageServerName, LspAdapterDelegate}; +use lsp::LanguageServerBinary; +use smol::fs; +use std::env::consts::ARCH; +use std::ffi::OsString; +use std::{any::Any, path::PathBuf}; +use util::async_maybe; +use util::github::latest_github_release; +use util::{github::GitHubLspBinaryVersion, ResultExt}; + +pub struct OmniSharpAdapter; + +#[async_trait] +impl super::LspAdapter for OmniSharpAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("OmniSharp".into()) + } + + fn short_name(&self) -> &'static str { + "OmniSharp" + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result> { + let release = + latest_github_release("OmniSharp/omnisharp-roslyn", false, delegate.http_client()) + .await?; + + let mapped_arch = match ARCH { + "aarch64" => Some("arm64"), + "x86_64" => Some("x64"), + _ => None, + }; + + match mapped_arch { + None => Ok(Box::new(())), + Some(arch) => { + let asset_name = format!("omnisharp-osx-{}-net6.0.tar.gz", arch); + 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.name, + 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 binary_path = container_dir.join("omnisharp"); + + 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 decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); + let archive = Archive::new(decompressed_bytes); + archive.unpack(container_dir).await?; + } + + fs::set_permissions( + &binary_path, + ::from_mode(0o755), + ) + .await?; + Ok(LanguageServerBinary { + path: binary_path, + arguments: server_binary_arguments(), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--help".into()]; + binary + }) + } +} + +async fn get_cached_server_binary(container_dir: PathBuf) -> Option { + async_maybe!({ + let mut last_binary_path = 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_file() + && entry + .file_name() + .to_str() + .map_or(false, |name| name == "omnisharp") + { + last_binary_path = Some(entry.path()); + } + } + + if let Some(path) = last_binary_path { + Ok(LanguageServerBinary { + path, + arguments: server_binary_arguments(), + }) + } else { + Err(anyhow!("no cached binary")) + } + }) + .await + .log_err() +} + +fn server_binary_arguments() -> Vec { + vec!["-lsp".into()] +} diff --git a/crates/zed/src/languages/csharp/config.toml b/crates/zed/src/languages/csharp/config.toml new file mode 100644 index 0000000000..7835283df4 --- /dev/null +++ b/crates/zed/src/languages/csharp/config.toml @@ -0,0 +1,12 @@ +name = "CSharp" +path_suffixes = ["cs"] +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", "comment"] }, + { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, +] diff --git a/crates/zed/src/languages/csharp/highlights.scm b/crates/zed/src/languages/csharp/highlights.scm new file mode 100644 index 0000000000..cb3c1ad4ad --- /dev/null +++ b/crates/zed/src/languages/csharp/highlights.scm @@ -0,0 +1,254 @@ +;; Methods +(method_declaration name: (identifier) @function) +(local_function_statement name: (identifier) @function) + +;; Types +(interface_declaration name: (identifier) @type) +(class_declaration name: (identifier) @type) +(enum_declaration name: (identifier) @type) +(struct_declaration (identifier) @type) +(record_declaration (identifier) @type) +(record_struct_declaration (identifier) @type) +(namespace_declaration name: (identifier) @type) + +(constructor_declaration name: (identifier) @constructor) +(destructor_declaration name: (identifier) @constructor) + +[ + (implicit_type) + (predefined_type) +] @type.builtin + +(_ type: (identifier) @type) + +;; Enum +(enum_member_declaration (identifier) @property) + +;; Literals +[ + (real_literal) + (integer_literal) +] @number + +[ + (character_literal) + (string_literal) + (verbatim_string_literal) + (interpolated_string_text) + (interpolated_verbatim_string_text) + "\"" + "$\"" + "@$\"" + "$@\"" + ] @string + +[ + (boolean_literal) + (null_literal) +] @constant + +;; Comments +(comment) @comment + +;; Tokens +[ + ";" + "." + "," +] @punctuation.delimiter + +[ + "--" + "-" + "-=" + "&" + "&=" + "&&" + "+" + "++" + "+=" + "<" + "<=" + "<<" + "<<=" + "=" + "==" + "!" + "!=" + "=>" + ">" + ">=" + ">>" + ">>=" + ">>>" + ">>>=" + "|" + "|=" + "||" + "?" + "??" + "??=" + "^" + "^=" + "~" + "*" + "*=" + "/" + "/=" + "%" + "%=" + ":" +] @operator + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +;; Keywords +(modifier) @keyword +(this_expression) @keyword +(escape_sequence) @keyword + +[ + "add" + "alias" + "as" + "base" + "break" + "case" + "catch" + "checked" + "class" + "continue" + "default" + "delegate" + "do" + "else" + "enum" + "event" + "explicit" + "extern" + "finally" + "for" + "foreach" + "global" + "goto" + "if" + "implicit" + "interface" + "is" + "lock" + "namespace" + "notnull" + "operator" + "params" + "return" + "remove" + "sizeof" + "stackalloc" + "static" + "struct" + "switch" + "throw" + "try" + "typeof" + "unchecked" + "using" + "while" + "new" + "await" + "in" + "yield" + "get" + "set" + "when" + "out" + "ref" + "from" + "where" + "select" + "record" + "init" + "with" + "let" +] @keyword + + +;; Linq +(from_clause (identifier) @variable) +(group_clause (identifier) @variable) +(order_by_clause (identifier) @variable) +(join_clause (identifier) @variable) +(select_clause (identifier) @variable) +(query_continuation (identifier) @variable) @keyword + +;; Record +(with_expression + (with_initializer_expression + (simple_assignment_expression + (identifier) @variable))) + +;; Exprs +(binary_expression (identifier) @variable (identifier) @variable) +(binary_expression (identifier)* @variable) +(conditional_expression (identifier) @variable) +(prefix_unary_expression (identifier) @variable) +(postfix_unary_expression (identifier)* @variable) +(assignment_expression (identifier) @variable) +(cast_expression (_) (identifier) @variable) + +;; Class +(base_list (identifier) @type) ;; applies to record_base too +(property_declaration (generic_name)) +(property_declaration + name: (identifier) @variable) +(property_declaration + name: (identifier) @variable) +(property_declaration + name: (identifier) @variable) + +;; Lambda +(lambda_expression) @variable + +;; Attribute +(attribute) @attribute + +;; Parameter +(parameter + name: (identifier) @variable) +(parameter (identifier) @variable) +(parameter_modifier) @keyword + +;; Variable declarations +(variable_declarator (identifier) @variable) +(for_each_statement left: (identifier) @variable) +(catch_declaration (_) (identifier) @variable) + +;; Return +(return_statement (identifier) @variable) +(yield_statement (identifier) @variable) + +;; Type +(generic_name (identifier) @type) +(type_parameter (identifier) @property) +(type_argument_list (identifier) @type) +(as_expression right: (identifier) @type) +(is_expression right: (identifier) @type) + +;; Type constraints +(type_parameter_constraints_clause (identifier) @property) + +;; Switch +(switch_statement (identifier) @variable) +(switch_expression (identifier) @variable) + +;; Lock statement +(lock_statement (identifier) @variable) + +;; Method calls +(invocation_expression (member_access_expression name: (identifier) @function)) diff --git a/crates/zed/src/languages/csharp/injections.scm b/crates/zed/src/languages/csharp/injections.scm new file mode 100644 index 0000000000..2f0e58eb64 --- /dev/null +++ b/crates/zed/src/languages/csharp/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/crates/zed/src/languages/csharp/outline.scm b/crates/zed/src/languages/csharp/outline.scm new file mode 100644 index 0000000000..aed899b37b --- /dev/null +++ b/crates/zed/src/languages/csharp/outline.scm @@ -0,0 +1,38 @@ +(class_declaration + "class" @context + name: (identifier) @name +) @item + +(constructor_declaration + name: (identifier) @name +) @item + +(property_declaration + type: (identifier)? @context + type: (predefined_type)? @context + name: (identifier) @name +) @item + +(field_declaration + (variable_declaration) @context +) @item + +(method_declaration + name: (identifier) @name + parameters: (parameter_list) @context +) @item + +(enum_declaration + "enum" @context + name: (identifier) @name +) @item + +(namespace_declaration + "namespace" @context + name: (qualified_name) @name +) @item + +(interface_declaration + "interface" @context + name: (identifier) @name +) @item diff --git a/docs/src/languages/csharp.md b/docs/src/languages/csharp.md new file mode 100644 index 0000000000..1de49c503c --- /dev/null +++ b/docs/src/languages/csharp.md @@ -0,0 +1,4 @@ +# C# + +- Tree Sitter: [tree-sitter-c-sharp](https://github.com/tree-sitter/tree-sitter-c-sharp) +- Language Server: [OmniSharp](https://github.com/OmniSharp/omnisharp-roslyn)