diff --git a/extension.toml b/extension.toml index 46490c7..5c3bdae 100644 --- a/extension.toml +++ b/extension.toml @@ -3,13 +3,20 @@ name = "C#" description = "C# support." version = "0.1.2" schema_version = 1 -authors = ["fminkowski "] +authors = [ + "fminkowski ", + "Fabian Freimueller ", +] repository = "https://github.com/zed-extensions/csharp" [language_servers.omnisharp] name = "OmniSharp" language = "CSharp" +[language_servers.roslyn] +name = "Roslyn" +language = "CSharp" + [grammars.c_sharp] repository = "https://github.com/tree-sitter/tree-sitter-c-sharp" commit = "dd5e59721a5f8dae34604060833902b882023aaf" diff --git a/src/csharp.rs b/src/csharp.rs index f9da890..4f10051 100644 --- a/src/csharp.rs +++ b/src/csharp.rs @@ -1,129 +1,22 @@ -use std::fs; -use zed_extension_api::{self as zed, settings::LspSettings, LanguageServerId, Result}; +mod language_servers; -struct OmnisharpBinary { - path: String, - args: Option>, -} +use language_servers::Roslyn; +use zed_extension_api::{self as zed, Result}; + +use crate::language_servers::Omnisharp; struct CsharpExtension { - cached_binary_path: Option, + omnisharp: Option, + roslyn: Option, } -impl CsharpExtension { - fn language_server_binary( - &mut self, - language_server_id: &LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - let binary_settings = LspSettings::for_worktree("omnisharp", worktree) - .ok() - .and_then(|lsp_settings| lsp_settings.binary); - let binary_args = binary_settings - .as_ref() - .and_then(|binary_settings| binary_settings.arguments.clone()); - - if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) { - return Ok(OmnisharpBinary { - path, - args: binary_args, - }); - } - - if let Some(path) = worktree.which("OmniSharp") { - return Ok(OmnisharpBinary { - path, - args: binary_args, - }); - } - - if let Some(path) = &self.cached_binary_path { - if fs::metadata(path).map_or(false, |stat| stat.is_file()) { - return Ok(OmnisharpBinary { - path: path.clone(), - args: binary_args, - }); - } - } - - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::CheckingForUpdate, - ); - let release = zed::latest_github_release( - "OmniSharp/omnisharp-roslyn", - zed::GithubReleaseOptions { - require_assets: true, - pre_release: false, - }, - )?; - - let (platform, arch) = zed::current_platform(); - let asset_name = format!( - "omnisharp-{os}-{arch}-net6.0.{extension}", - os = match platform { - zed::Os::Mac => "osx", - zed::Os::Linux => "linux", - zed::Os::Windows => "win", - }, - arch = match arch { - zed::Architecture::Aarch64 => "arm64", - zed::Architecture::X86 => "x86", - zed::Architecture::X8664 => "x64", - }, - extension = match platform { - zed::Os::Mac | zed::Os::Linux => "tar.gz", - zed::Os::Windows => "zip", - } - ); - - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; - - let version_dir = format!("omnisharp-{}", release.version); - let binary_path = format!("{version_dir}/OmniSharp"); - - if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::Downloading, - ); - - zed::download_file( - &asset.download_url, - &version_dir, - match platform { - zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::GzipTar, - zed::Os::Windows => zed::DownloadedFileType::Zip, - }, - ) - .map_err(|e| format!("failed to download file: {e}"))?; - - let entries = - fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; - for entry in entries { - let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; - if entry.file_name().to_str() != Some(&version_dir) { - fs::remove_dir_all(entry.path()).ok(); - } - } - } - - self.cached_binary_path = Some(binary_path.clone()); - Ok(OmnisharpBinary { - path: binary_path, - args: binary_args, - }) - } -} +impl CsharpExtension {} impl zed::Extension for CsharpExtension { fn new() -> Self { Self { - cached_binary_path: None, + omnisharp: None, + roslyn: None, } } @@ -132,12 +25,37 @@ impl zed::Extension for CsharpExtension { language_server_id: &zed::LanguageServerId, worktree: &zed::Worktree, ) -> Result { - let omnisharp_binary = self.language_server_binary(language_server_id, worktree)?; - Ok(zed::Command { - command: omnisharp_binary.path, - args: omnisharp_binary.args.unwrap_or_else(|| vec!["-lsp".into()]), - env: Default::default(), - }) + match language_server_id.as_ref() { + Omnisharp::LANGUAGE_SERVER_ID => { + let omnisharp = self.omnisharp.get_or_insert_with(Omnisharp::new); + let omnisharp_binary = + omnisharp.language_server_binary(language_server_id, worktree)?; + Ok(zed::Command { + command: omnisharp_binary.path, + args: omnisharp_binary.args.unwrap_or_else(|| vec!["-lsp".into()]), + env: Default::default(), + }) + } + Roslyn::LANGUAGE_SERVER_ID => { + // Add Roslyn Server + let roslyn = self.roslyn.get_or_insert_with(Roslyn::new); + roslyn.language_server_cmd(language_server_id, worktree) + } + language_server_id => Err(format!("unknown language server: {language_server_id}")), + } + } + + fn language_server_workspace_configuration( + &mut self, + language_server_id: &zed::LanguageServerId, + worktree: &zed::Worktree, + ) -> Result> { + if language_server_id.as_ref() == Roslyn::LANGUAGE_SERVER_ID { + if let Some(roslyn) = self.roslyn.as_mut() { + return roslyn.configuration_options(worktree); + } + } + Ok(None) } } diff --git a/src/language_servers/mod.rs b/src/language_servers/mod.rs new file mode 100644 index 0000000..4d8ce91 --- /dev/null +++ b/src/language_servers/mod.rs @@ -0,0 +1,5 @@ +pub mod omnisharp; +pub mod roslyn; + +pub use omnisharp::*; +pub use roslyn::*; diff --git a/src/language_servers/omnisharp.rs b/src/language_servers/omnisharp.rs new file mode 100644 index 0000000..3b0a923 --- /dev/null +++ b/src/language_servers/omnisharp.rs @@ -0,0 +1,129 @@ +use std::fs; +use zed_extension_api::{self as zed, settings::LspSettings, LanguageServerId, Result}; + +pub struct Omnisharp { + cached_binary_path: Option, +} + +pub struct OmnisharpBinary { + pub path: String, + pub args: Option>, +} + +impl Omnisharp { + pub const LANGUAGE_SERVER_ID: &'static str = "omnisharp"; + + pub fn new() -> Self { + Self { + cached_binary_path: None, + } + } + + pub fn language_server_binary( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + let binary_settings = LspSettings::for_worktree("omnisharp", worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.binary); + let binary_args = binary_settings + .as_ref() + .and_then(|binary_settings| binary_settings.arguments.clone()); + + if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) { + return Ok(OmnisharpBinary { + path, + args: binary_args, + }); + } + + if let Some(path) = worktree.which("OmniSharp") { + return Ok(OmnisharpBinary { + path, + args: binary_args, + }); + } + + if let Some(path) = &self.cached_binary_path { + if fs::metadata(path).map_or(false, |stat| stat.is_file()) { + return Ok(OmnisharpBinary { + path: path.clone(), + args: binary_args, + }); + } + } + + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let release = zed::latest_github_release( + "OmniSharp/omnisharp-roslyn", + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let (platform, arch) = zed::current_platform(); + let asset_name = format!( + "omnisharp-{os}-{arch}-net6.0.{extension}", + os = match platform { + zed::Os::Mac => "osx", + zed::Os::Linux => "linux", + zed::Os::Windows => "win", + }, + arch = match arch { + zed::Architecture::Aarch64 => "arm64", + zed::Architecture::X86 => "x86", + zed::Architecture::X8664 => "x64", + }, + extension = match platform { + zed::Os::Mac | zed::Os::Linux => "tar.gz", + zed::Os::Windows => "zip", + } + ); + + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; + + let version_dir = format!("omnisharp-{}", release.version); + let binary_path = format!("{version_dir}/OmniSharp"); + + if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + + zed::download_file( + &asset.download_url, + &version_dir, + match platform { + zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::GzipTar, + zed::Os::Windows => zed::DownloadedFileType::Zip, + }, + ) + .map_err(|e| format!("failed to download file: {e}"))?; + + let entries = + fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; + for entry in entries { + let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; + if entry.file_name().to_str() != Some(&version_dir) { + fs::remove_dir_all(entry.path()).ok(); + } + } + } + + self.cached_binary_path = Some(binary_path.clone()); + Ok(OmnisharpBinary { + path: binary_path, + args: binary_args, + }) + } +} diff --git a/src/language_servers/roslyn.rs b/src/language_servers/roslyn.rs new file mode 100644 index 0000000..8d0da21 --- /dev/null +++ b/src/language_servers/roslyn.rs @@ -0,0 +1,169 @@ +use std::fs; + +use zed_extension_api::{ + self as zed, serde_json::Map, settings::LspSettings, LanguageServerId, Result, +}; + +const REPO: &str = "SofusA/csharp-language-server"; + +pub struct Roslyn { + cached_binary_path: Option, +} + +impl Roslyn { + pub const LANGUAGE_SERVER_ID: &'static str = "roslyn"; + + pub fn new() -> Self { + Roslyn { + cached_binary_path: None, + } + } + + 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(|binary_settings| binary_settings.arguments.clone()); + + if let Some(path) = binary_settings + .and_then(|binary_settings| binary_settings.path) + .or_else(|| { + self.cached_binary_path + .as_ref() + .filter(|path| fs::metadata(path).map_or(false, |stat| stat.is_file())) + .cloned() + }) + { + return Ok(zed::Command { + command: path, + args: binary_args.unwrap_or_default(), + env: Default::default(), + }); + } + + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let release = zed::latest_github_release( + REPO, + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let (platform, arch) = zed::current_platform(); + + let arch_str = match arch { + zed::Architecture::Aarch64 => "aarch64", + zed::Architecture::X8664 => "x86_64", + zed::Architecture::X86 => { + return Err(format!("The roslyn lsp server wrapper does not support the following processor architecture: {:?}", arch)); + } + }; + + let asset_name = format!( + "csharp-language-server-{arch}-{os}.{extension}", + os = match platform { + zed::Os::Mac => "apple-darwin", + zed::Os::Linux => "unknown-linux-gnu", + zed::Os::Windows => "pc-windows-msvc", + }, + arch = arch_str, + extension = match platform { + zed::Os::Mac | zed::Os::Linux => "tar.gz", + zed::Os::Windows => "zip", + } + ); + + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; + + let version_dir = format!("roslyn-{}", release.version); + let binary_path = format!("{version_dir}/csharp-language-server"); + + if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + + zed::download_file( + &asset.download_url, + &version_dir, + match platform { + zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::GzipTar, + zed::Os::Windows => zed::DownloadedFileType::Zip, + }, + ) + .map_err(|e| format!("failed to download file: {e}"))?; + + zed::make_file_executable(&binary_path)?; + + let entries = + fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; + for entry in entries { + let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; + if entry.file_name().to_str() != Some(&version_dir) { + fs::remove_dir_all(entry.path()).ok(); + } + } + } + + self.cached_binary_path = Some(binary_path.clone()); + Ok(zed::Command { + command: binary_path, + args: binary_args.unwrap_or_default(), + env: Default::default(), + }) + } + + pub fn configuration_options( + &self, + 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(|user_settings| self.transform_settings_for_roslyn(user_settings))) + } + + fn transform_settings_for_roslyn( + &self, + settings: zed::serde_json::Value, + ) -> zed::serde_json::Value { + let mut roslyn_config = Map::new(); + + if let zed::serde_json::Value::Object(settings_map) = settings { + for (key, value) in &settings_map { + if key.contains('|') { + // This is already in the language|category format + if let zed::serde_json::Value::Object(nested_settings) = value { + for (nested_key, nested_value) in nested_settings { + // The key already contains the proper format, just add the setting + let roslyn_key = format!("{}.{}", key, nested_key); + roslyn_config.insert(roslyn_key, nested_value.clone()); + } + } + } + // Handle direct roslyn-format settings (fallback for any other format) + else if key.contains('.') { + roslyn_config.insert(key.clone(), value.clone()); + } + } + } + + zed::serde_json::Value::Object(roslyn_config) + } +}