From 89daaa19c8d8f2eedc4188a2beb5aa9a2e6b404d Mon Sep 17 00:00:00 2001 From: Richard Boisvert Date: Tue, 5 May 2026 10:59:45 -0400 Subject: [PATCH 1/2] feat(language server): add csharp-ls as a new option --- extension.toml | 4 + src/csharp.rs | 16 ++-- src/language_servers/csharp_ls.rs | 137 ++++++++++++++++++++++++++++++ src/language_servers/mod.rs | 2 + 4 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 src/language_servers/csharp_ls.rs diff --git a/extension.toml b/extension.toml index cbed3a0..807d3f6 100644 --- a/extension.toml +++ b/extension.toml @@ -17,6 +17,10 @@ language = "CSharp" name = "Roslyn" language = "CSharp" +[language_servers.csharp-ls] +name = "csharp-ls" +language = "CSharp" + [grammars.c_sharp] repository = "https://github.com/tree-sitter/tree-sitter-c-sharp" commit = "485f0bae0274ac9114797fc10db6f7034e4086e3" diff --git a/src/csharp.rs b/src/csharp.rs index c39bc2c..3ee0e71 100644 --- a/src/csharp.rs +++ b/src/csharp.rs @@ -1,13 +1,13 @@ mod language_servers; -use language_servers::Roslyn; use zed_extension_api::{self as zed, Result}; -use crate::language_servers::Omnisharp; +use crate::language_servers::{CsharpLs, Omnisharp, Roslyn}; struct CsharpExtension { omnisharp: Option, roslyn: Option, + csharp_ls: Option, } impl CsharpExtension {} @@ -17,6 +17,7 @@ impl zed::Extension for CsharpExtension { Self { omnisharp: None, roslyn: None, + csharp_ls: None, } } @@ -41,6 +42,10 @@ impl zed::Extension for CsharpExtension { let roslyn = self.roslyn.get_or_insert_with(Roslyn::new); roslyn.language_server_cmd(language_server_id, worktree) } + CsharpLs::LANGUAGE_SERVER_ID => { + let csharp_ls = self.csharp_ls.get_or_insert_with(CsharpLs::new); + csharp_ls.language_server_cmd(language_server_id, worktree) + } language_server_id => Err(format!("unknown language server: {language_server_id}")), } } @@ -50,10 +55,11 @@ impl zed::Extension for CsharpExtension { language_server_id: &zed::LanguageServerId, worktree: &zed::Worktree, ) -> Result> { - if language_server_id.as_ref() == Roslyn::LANGUAGE_SERVER_ID { - return Roslyn::configuration_options(worktree); + match language_server_id.as_ref() { + Roslyn::LANGUAGE_SERVER_ID => Roslyn::configuration_options(worktree), + CsharpLs::LANGUAGE_SERVER_ID => CsharpLs::configuration_options(worktree), + _ => Ok(None), } - Ok(None) } } diff --git a/src/language_servers/csharp_ls.rs b/src/language_servers/csharp_ls.rs new file mode 100644 index 0000000..7bd400f --- /dev/null +++ b/src/language_servers/csharp_ls.rs @@ -0,0 +1,137 @@ +use std::fs; + +use zed_extension_api::{self as zed, settings::LspSettings, LanguageServerId, Result}; + +use crate::language_servers::{nuget::NuGetClient, util}; + +const PACKAGE_ID: &str = "csharp-ls"; +const SERVER_DLL: &str = "CSharpLanguageServer.dll"; +const DOTNET_HINT: &str = "csharp-ls requires the .NET SDK on PATH; install .NET 10+ \ +or set `lsp.\"csharp-ls\".binary.path` to a working `csharp-ls` binary."; + +pub struct CsharpLs { + cached_dll_path: Option, + nuget: NuGetClient, +} + +impl CsharpLs { + pub const LANGUAGE_SERVER_ID: &'static str = "csharp-ls"; + const BINARY_NAME: &'static str = "csharp-ls"; + + pub fn new() -> Self { + Self { + cached_dll_path: None, + nuget: NuGetClient::new(), + } + } + + pub fn language_server_cmd( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + let binary_settings = LspSettings::for_worktree(Self::LANGUAGE_SERVER_ID, worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.binary); + let binary_args = binary_settings.as_ref().and_then(|b| b.arguments.clone()); + + if let Some(path) = binary_settings.and_then(|b| b.path) { + return Ok(zed::Command { + command: path, + args: binary_args.unwrap_or_default(), + env: Default::default(), + }); + } + + if let Some(path) = worktree.which(Self::BINARY_NAME) { + return Ok(zed::Command { + command: path, + args: binary_args.unwrap_or_default(), + env: Default::default(), + }); + } + + if let Some(ref dll_path) = self.cached_dll_path { + if fs::metadata(dll_path).is_ok_and(|s| s.is_file()) { + return Self::dotnet_exec(worktree, dll_path, binary_args); + } + } + + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + + let version = self.nuget.get_latest_version(PACKAGE_ID)?; + let version_dir = format!("{}-{}", Self::LANGUAGE_SERVER_ID, version); + + if Self::find_dll(&version_dir).is_err() { + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + + self.nuget + .download_and_extract(PACKAGE_ID, &version, &version_dir)?; + + util::remove_outdated_versions(Self::LANGUAGE_SERVER_ID, &version_dir)?; + } + + let dll_path = Self::find_dll(&version_dir)?; + let command = Self::dotnet_exec(worktree, &dll_path, binary_args)?; + self.cached_dll_path = Some(dll_path); + Ok(command) + } + + fn dotnet_exec( + worktree: &zed::Worktree, + dll_path: &str, + user_args: Option>, + ) -> Result { + let dotnet = worktree + .which("dotnet") + .ok_or_else(|| DOTNET_HINT.to_string())?; + let mut args = vec!["exec".to_string(), dll_path.to_string()]; + if let Some(user) = user_args { + args.extend(user); + } + Ok(zed::Command { + command: dotnet, + args, + env: Default::default(), + }) + } + + fn find_dll(version_dir: &str) -> Result { + let tools_dir = format!("{version_dir}/tools"); + let tfm = fs::read_dir(&tools_dir) + .map_err(|e| format!("failed to read tools directory '{tools_dir}': {e}"))? + .filter_map(|entry| { + let entry = entry.ok()?; + if entry.file_type().ok()?.is_dir() { + entry.file_name().into_string().ok() + } else { + None + } + }) + .next() + .ok_or_else(|| format!("no TFM directory found inside '{tools_dir}'"))?; + + let dll_path = format!("{tools_dir}/{tfm}/any/{SERVER_DLL}"); + if !fs::metadata(&dll_path).is_ok_and(|s| s.is_file()) { + return Err(format!( + "csharp-ls package layout unexpected: missing entry DLL at '{dll_path}'" + )); + } + Ok(dll_path) + } + + pub fn configuration_options( + worktree: &zed::Worktree, + ) -> Result> { + let settings = LspSettings::for_worktree(Self::LANGUAGE_SERVER_ID, worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.settings); + Ok(settings.map(|s| zed::serde_json::json!({ "csharp": s }))) + } +} diff --git a/src/language_servers/mod.rs b/src/language_servers/mod.rs index 2a34de7..e76d216 100644 --- a/src/language_servers/mod.rs +++ b/src/language_servers/mod.rs @@ -1,7 +1,9 @@ +pub mod csharp_ls; pub mod nuget; pub mod omnisharp; pub mod roslyn; pub mod util; +pub use csharp_ls::*; pub use omnisharp::*; pub use roslyn::*; From a49531a87ee8c76ba6e586ee9dfe394ac91ead74 Mon Sep 17 00:00:00 2001 From: MrSubidubi Date: Thu, 4 Jun 2026 09:30:12 +0200 Subject: [PATCH 2/2] Minor changes --- src/language_servers/csharp_ls.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/language_servers/csharp_ls.rs b/src/language_servers/csharp_ls.rs index 7bd400f..94df1bd 100644 --- a/src/language_servers/csharp_ls.rs +++ b/src/language_servers/csharp_ls.rs @@ -6,8 +6,8 @@ use crate::language_servers::{nuget::NuGetClient, util}; const PACKAGE_ID: &str = "csharp-ls"; const SERVER_DLL: &str = "CSharpLanguageServer.dll"; -const DOTNET_HINT: &str = "csharp-ls requires the .NET SDK on PATH; install .NET 10+ \ -or set `lsp.\"csharp-ls\".binary.path` to a working `csharp-ls` binary."; +const DOTNET_HINT: &str = "csharp-ls requires the .NET SDK on PATH. Install .NET 10+ \ +or set `lsp.csharp-ls.binary.path` to a working `csharp-ls` binary."; pub struct CsharpLs { cached_dll_path: Option, @@ -118,12 +118,14 @@ impl CsharpLs { .ok_or_else(|| format!("no TFM directory found inside '{tools_dir}'"))?; let dll_path = format!("{tools_dir}/{tfm}/any/{SERVER_DLL}"); - if !fs::metadata(&dll_path).is_ok_and(|s| s.is_file()) { - return Err(format!( + + if fs::metadata(&dll_path).is_ok_and(|s| s.is_file()) { + Ok(dll_path) + } else { + Err(format!( "csharp-ls package layout unexpected: missing entry DLL at '{dll_path}'" - )); + )) } - Ok(dll_path) } pub fn configuration_options(