diff --git a/extension.toml b/extension.toml index bf1909b..67c5f4f 100644 --- a/extension.toml +++ b/extension.toml @@ -15,6 +15,10 @@ language_ids = { PHP = "php" } name = "Phpactor" language = "PHP" +[language_servers.psalm] +name = "Psalm" +language = "PHP" + [grammars.php] repository = "https://github.com/tree-sitter/tree-sitter-php" commit = "8ab93274065cbaf529ea15c24360cfa3348ec9e4" diff --git a/src/language_servers.rs b/src/language_servers.rs index f209d1c..359a90b 100644 --- a/src/language_servers.rs +++ b/src/language_servers.rs @@ -1,5 +1,7 @@ mod intelephense; mod phpactor; +mod psalm; pub use intelephense::*; pub use phpactor::*; +pub use psalm::*; diff --git a/src/language_servers/psalm.rs b/src/language_servers/psalm.rs new file mode 100644 index 0000000..4283caa --- /dev/null +++ b/src/language_servers/psalm.rs @@ -0,0 +1,125 @@ +use std::fs; +use std::path::PathBuf; + +use zed_extension_api::settings::LspSettings; +use zed_extension_api::{self as zed, serde_json, LanguageServerId, Result}; + +pub struct Psalm; + +impl Psalm { + pub const LANGUAGE_SERVER_ID: &'static str = "psalm"; + + pub fn new() -> Self { + Self + } + + pub fn language_server_command( + &self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + let php_path = worktree.which("php").ok_or("PHP not found in PATH")?; + + let possible_commands = [ + "vendor/bin/psalm-language-server", + "psalm-language-server", + "vendor/bin/psalm", + "psalm" + ]; + + for command in &possible_commands { + if let Some(found_path) = worktree.which(command) { + let args = if command.contains("psalm-language-server") { + vec![] + } else { + vec!["--language-server".to_string()] + }; + + return Ok(zed::Command { + command: php_path.clone(), + args: { + let mut cmd_args = vec![found_path]; + cmd_args.extend(args); + cmd_args + }, + env: Default::default(), + }); + } + } + + let vendor_bin_path = PathBuf::from(worktree.root_path()).join("vendor/bin/psalm-language-server"); + if vendor_bin_path.exists() && vendor_bin_path.is_file() { + return Ok(zed::Command { + command: php_path.clone(), + args: vec![vendor_bin_path.to_string_lossy().to_string()], + env: Default::default(), + }); + } + + let psalm_vendor_path = PathBuf::from(worktree.root_path()).join("vendor/bin/psalm"); + if psalm_vendor_path.exists() && psalm_vendor_path.is_file() { + return Ok(zed::Command { + command: php_path.clone(), + args: vec![psalm_vendor_path.to_string_lossy().to_string(), "--language-server".to_string()], + env: Default::default(), + }); + } + + if let Ok(lsp_settings) = LspSettings::for_worktree("psalm", worktree) { + if let Some(initialization_options) = lsp_settings.initialization_options { + if let Some(command_array) = initialization_options.get("command").and_then(|v| v.as_array()) { + if let Some(command) = command_array.get(0).and_then(|v| v.as_str()) { + let command_path = PathBuf::from(worktree.root_path()).join(command); + if command_path.exists() && command_path.is_file() { + return Ok(zed::Command { + command: php_path.clone(), + args: vec![command_path.to_string_lossy().to_string()], + env: Default::default(), + }); + } + } + } + } + } + + + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::Failed( + "psalm-language-server not found. Install with: composer require --dev vimeo/psalm".to_string() + ), + ); + + Err("psalm-language-server not found. Please install it with: composer require --dev vimeo/psalm".to_string())? + } + + pub fn language_server_workspace_configuration( + &self, + worktree: &zed::Worktree, + ) -> Result> { + let settings = LspSettings::for_worktree("psalm", worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.settings.clone()) + .unwrap_or_default(); + + // Check if psalm.xml exists in the workspace + let psalm_config_path = PathBuf::from(worktree.root_path()).join("psalm.xml"); + let has_config = fs::metadata(&psalm_config_path).map_or(false, |stat| stat.is_file()); + + let mut config = serde_json::json!({ + "psalm": settings + }); + + // If psalm.xml exists, configure to use it + if has_config { + if let Some(psalm_settings) = config.get_mut("psalm").and_then(|v| v.as_object_mut()) { + psalm_settings.insert( + "configPaths".to_string(), + serde_json::json!([psalm_config_path.to_string_lossy().to_string()]) + ); + } + } + + Ok(Some(config)) + } +} \ No newline at end of file diff --git a/src/php.rs b/src/php.rs index 53b4c29..d53b91f 100644 --- a/src/php.rs +++ b/src/php.rs @@ -3,11 +3,12 @@ mod language_servers; use zed::CodeLabel; use zed_extension_api::{self as zed, serde_json, LanguageServerId, Result}; -use crate::language_servers::{Intelephense, Phpactor}; +use crate::language_servers::{Intelephense, Phpactor, Psalm}; struct PhpExtension { intelephense: Option, phpactor: Option, + psalm: Option, } impl zed::Extension for PhpExtension { @@ -15,6 +16,7 @@ impl zed::Extension for PhpExtension { Self { intelephense: None, phpactor: None, + psalm: None, } } @@ -37,6 +39,10 @@ impl zed::Extension for PhpExtension { env: Default::default(), }) } + Psalm::LANGUAGE_SERVER_ID => { + let psalm = self.psalm.get_or_insert_with(Psalm::new); + psalm.language_server_command(language_server_id, worktree) + } language_server_id => Err(format!("unknown language server: {language_server_id}")), } } @@ -46,10 +52,18 @@ impl zed::Extension for PhpExtension { language_server_id: &LanguageServerId, worktree: &zed::Worktree, ) -> Result> { - if language_server_id.as_ref() == Intelephense::LANGUAGE_SERVER_ID { - if let Some(intelephense) = self.intelephense.as_mut() { - return intelephense.language_server_workspace_configuration(worktree); + match language_server_id.as_ref() { + Intelephense::LANGUAGE_SERVER_ID => { + if let Some(intelephense) = self.intelephense.as_mut() { + return intelephense.language_server_workspace_configuration(worktree); + } + } + Psalm::LANGUAGE_SERVER_ID => { + if let Some(psalm) = self.psalm.as_ref() { + return psalm.language_server_workspace_configuration(worktree); + } } + _ => {} } Ok(None)