C# Support: Add treesitter and OmniSharp LSP support (#6908)

fminkowski created

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
<img width="984" alt="vscode"
src="https://github.com/zed-industries/zed/assets/6967829/1f6b4cb7-4e00-4d61-8e58-2867dc5c8ecf">

## Zed
<img width="1722" alt="zed"
src="https://github.com/zed-industries/zed/assets/6967829/88436c78-93de-4e26-be15-b0dea6590c55">

Change summary

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 
crates/zed/src/languages/csharp/highlights.scm | 254 ++++++++++++++++++++
crates/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(+)

Detailed changes

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",

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"}

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

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(),

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<Box<dyn 'static + Send + Any>> {
+        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<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().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,
+            <fs::Permissions as fs::unix::PermissionsExt>::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<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        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<LanguageServerBinary> {
+    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<OsString> {
+    vec!["-lsp".into()]
+}

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"] },
+]

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))

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

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)