From 696591ca553893662cdb7215670acb2cee5c91fb Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 16 Jul 2024 18:39:13 -0400 Subject: [PATCH] php: Add Phpactor support (#14604) This PR extends the PHP extension with [Phpactor](https://github.com/phpactor/phpactor) support. Phpactor seems to provide a better feature set out-of-the-box for free, so it has been made the default PHP language server. Thank you to @xtrasmal for informing us of Phpactor's existence! Release Notes: - N/A --- assets/settings/default.json | 1 + docs/src/languages/php.md | 18 +++ extensions/php/extension.toml | 4 + extensions/php/src/language_servers.rs | 5 + .../php/src/language_servers/intelephense.rs | 64 +++++++++++ .../php/src/language_servers/phpactor.rs | 85 ++++++++++++++ extensions/php/src/php.rs | 104 +++++++----------- 7 files changed, 216 insertions(+), 65 deletions(-) create mode 100644 extensions/php/src/language_servers.rs create mode 100644 extensions/php/src/language_servers/intelephense.rs create mode 100644 extensions/php/src/language_servers/phpactor.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 62a299a4995f74425e63d13b4b801c5becac8c2a..693041cce5cfb5a4df364965c8275425ffe7e383 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -790,6 +790,7 @@ } }, "PHP": { + "language_servers": ["phpactor", "!intelephense", "..."], "prettier": { "allowed": true, "plugins": ["@prettier/plugin-php"], diff --git a/docs/src/languages/php.md b/docs/src/languages/php.md index 880efa67c48bac5ab8230bc247d4754884698112..e0ba5a4638762181ef6da0e7f4016160371a8de1 100644 --- a/docs/src/languages/php.md +++ b/docs/src/languages/php.md @@ -1,3 +1,21 @@ # PHP PHP support is available through the [PHP extension](https://github.com/zed-industries/zed/tree/main/extensions/php). + +## Choosing a language server + +The PHP extension offers both `phpactor` and `intelephense` language server support. + +`phpactor` is enabled by default. + +To switch to `intelephense`, add the following to your `settings.json`: + +```json +{ + "languages": { + "PHP": { + "language_servers": ["intelephense", "!phpactor", "..."] + } + } +} +``` diff --git a/extensions/php/extension.toml b/extensions/php/extension.toml index 0b1a4e49f11096d27ade89af776ced67a320bc61..267ee27db38977180354cb4d7a9f9bdaf88ea9fa 100644 --- a/extensions/php/extension.toml +++ b/extensions/php/extension.toml @@ -11,6 +11,10 @@ name = "Intelephense" language = "PHP" language_ids = { PHP = "php"} +[language_servers.phpactor] +name = "Phpactor" +language = "PHP" + [grammars.php] repository = "https://github.com/tree-sitter/tree-sitter-php" commit = "8ab93274065cbaf529ea15c24360cfa3348ec9e4" diff --git a/extensions/php/src/language_servers.rs b/extensions/php/src/language_servers.rs new file mode 100644 index 0000000000000000000000000000000000000000..f209d1c00ac2639a9933cc52e64fc23bc75ac971 --- /dev/null +++ b/extensions/php/src/language_servers.rs @@ -0,0 +1,5 @@ +mod intelephense; +mod phpactor; + +pub use intelephense::*; +pub use phpactor::*; diff --git a/extensions/php/src/language_servers/intelephense.rs b/extensions/php/src/language_servers/intelephense.rs new file mode 100644 index 0000000000000000000000000000000000000000..900571d9a439a64cc50bfc18619c12a26bd778f3 --- /dev/null +++ b/extensions/php/src/language_servers/intelephense.rs @@ -0,0 +1,64 @@ +use std::fs; + +use zed_extension_api::{self as zed, LanguageServerId, Result}; + +const SERVER_PATH: &str = "node_modules/intelephense/lib/intelephense.js"; +const PACKAGE_NAME: &str = "intelephense"; + +pub struct Intelephense { + did_find_server: bool, +} + +impl Intelephense { + pub const LANGUAGE_SERVER_ID: &'static str = "intelephense"; + + pub fn new() -> Self { + Self { + did_find_server: false, + } + } + + fn server_exists(&self) -> bool { + fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file()) + } + + pub fn server_script_path(&mut self, language_server_id: &LanguageServerId) -> Result { + let server_exists = self.server_exists(); + if self.did_find_server && server_exists { + return Ok(SERVER_PATH.to_string()); + } + + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let version = zed::npm_package_latest_version(PACKAGE_NAME)?; + + if !server_exists + || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version) + { + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + let result = zed::npm_install_package(PACKAGE_NAME, &version); + match result { + Ok(()) => { + if !self.server_exists() { + Err(format!( + "installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'", + ))?; + } + } + Err(error) => { + if !self.server_exists() { + Err(error)?; + } + } + } + } + + self.did_find_server = true; + Ok(SERVER_PATH.to_string()) + } +} diff --git a/extensions/php/src/language_servers/phpactor.rs b/extensions/php/src/language_servers/phpactor.rs new file mode 100644 index 0000000000000000000000000000000000000000..e633da8218b14fe2d2fb04defe15d0e7b40b2395 --- /dev/null +++ b/extensions/php/src/language_servers/phpactor.rs @@ -0,0 +1,85 @@ +use std::fs; + +use zed_extension_api::{self as zed, LanguageServerId, Result}; + +pub struct Phpactor { + cached_binary_path: Option, +} + +impl Phpactor { + pub const LANGUAGE_SERVER_ID: &'static str = "phpactor"; + + pub fn new() -> Self { + Self { + cached_binary_path: None, + } + } + + pub fn language_server_binary_path( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + if let Some(path) = worktree.which("phpactor") { + return Ok(path); + } + + if let Some(path) = &self.cached_binary_path { + if fs::metadata(path).map_or(false, |stat| stat.is_file()) { + return Ok(path.clone()); + } + } + + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let release = zed::latest_github_release( + "phpactor/phpactor", + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let asset_name = "phpactor.phar"; + 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!("phpactor-{}", release.version); + fs::create_dir_all(&version_dir).map_err(|e| format!("failed to create directory: {e}"))?; + + let binary_path = format!("{version_dir}/phpactor.phar"); + + 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, + &binary_path, + zed::DownloadedFileType::Uncompressed, + ) + .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(binary_path) + } +} diff --git a/extensions/php/src/php.rs b/extensions/php/src/php.rs index d588ed9d8b540aebf91c6430868b78c8369d6ddc..b9388f91871ee623afab2c843ac5360d2425cd47 100644 --- a/extensions/php/src/php.rs +++ b/extensions/php/src/php.rs @@ -1,84 +1,58 @@ -use std::{env, fs}; -use zed_extension_api::{self as zed, LanguageServerId, Result}; - -const SERVER_PATH: &str = "node_modules/intelephense/lib/intelephense.js"; -const PACKAGE_NAME: &str = "intelephense"; - -struct PhpExtension { - did_find_server: bool, -} - -impl PhpExtension { - fn server_exists(&self) -> bool { - fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file()) - } +mod language_servers; - fn server_script_path(&mut self, language_server_id: &LanguageServerId) -> Result { - let server_exists = self.server_exists(); - if self.did_find_server && server_exists { - return Ok(SERVER_PATH.to_string()); - } +use std::env; - zed::set_language_server_installation_status( - &language_server_id, - &zed::LanguageServerInstallationStatus::CheckingForUpdate, - ); - let version = zed::npm_package_latest_version(PACKAGE_NAME)?; +use zed_extension_api::{self as zed, LanguageServerId, Result}; - if !server_exists - || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version) - { - zed::set_language_server_installation_status( - &language_server_id, - &zed::LanguageServerInstallationStatus::Downloading, - ); - let result = zed::npm_install_package(PACKAGE_NAME, &version); - match result { - Ok(()) => { - if !self.server_exists() { - Err(format!( - "installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'", - ))?; - } - } - Err(error) => { - if !self.server_exists() { - Err(error)?; - } - } - } - } +use crate::language_servers::{Intelephense, Phpactor}; - self.did_find_server = true; - Ok(SERVER_PATH.to_string()) - } +struct PhpExtension { + intelephense: Option, + phpactor: Option, } impl zed::Extension for PhpExtension { fn new() -> Self { Self { - did_find_server: false, + intelephense: None, + phpactor: None, } } fn language_server_command( &mut self, language_server_id: &LanguageServerId, - _worktree: &zed::Worktree, + worktree: &zed::Worktree, ) -> Result { - let server_path = self.server_script_path(language_server_id)?; - Ok(zed::Command { - command: zed::node_binary_path()?, - args: vec![ - env::current_dir() - .unwrap() - .join(&server_path) - .to_string_lossy() - .to_string(), - "--stdio".to_string(), - ], - env: Default::default(), - }) + match language_server_id.as_ref() { + Intelephense::LANGUAGE_SERVER_ID => { + let intelephense = self.intelephense.get_or_insert_with(|| Intelephense::new()); + + let server_path = intelephense.server_script_path(language_server_id)?; + Ok(zed::Command { + command: zed::node_binary_path()?, + args: vec![ + env::current_dir() + .unwrap() + .join(&server_path) + .to_string_lossy() + .to_string(), + "--stdio".to_string(), + ], + env: Default::default(), + }) + } + Phpactor::LANGUAGE_SERVER_ID => { + let phpactor = self.phpactor.get_or_insert_with(|| Phpactor::new()); + + Ok(zed::Command { + command: phpactor.language_server_binary_path(language_server_id, worktree)?, + args: vec!["language-server".into()], + env: Default::default(), + }) + } + language_server_id => Err(format!("unknown language server: {language_server_id}")), + } } }