Skip to content

Commit b45e080

Browse files
jakubmisekosiewicz
andauthored
Add devsense-php-ls LSP support (#34)
This pull request adds support for [`devsense-php-ls`](https://www.npmjs.com/package/devsense-php-ls), the language server used by the [PHP Tools extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=DEVSENSE.phptools-vscode). This language server offers a comprehensive PHP development experience (within the limits of the LSP protocol), including: * Laravel IDE features * Support for PhpStan, Psalm, and other PHPDoc annotations * Code completion * Inlay hints * Code diagnostics * Code navigation * Code folding * And more #### Configuration Once the extension is installed, the language server can be enabled via settings. A separate PR will be submitted to update https://github.com/zed-industries/zed/blob/main/docs/src/languages/php.md after this PR is merged. ```json "languages": { "PHP": { "language_servers": ["phptools", "!intelephense", "!phpactor", "..."] } } ``` #### Next Steps After this is merged, I plan to: * Add more PHP-specific features * Document the freemium licensing model * Reference the extension on our website * Promote it via social media --------- Co-authored-by: Piotr Osiewicz <[email protected]>
1 parent afdbea1 commit b45e080

File tree

4 files changed

+147
-1
lines changed

4 files changed

+147
-1
lines changed

extension.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ schema_version = 1
66
authors = ["Piotr Osiewicz <[email protected]>"]
77
repository = "https://github.com/zed-extensions/php"
88

9+
[language_servers.phptools]
10+
name = "PhpTools"
11+
language = "PHP"
12+
language_ids = { PHP = "php" }
13+
914
[language_servers.intelephense]
1015
name = "Intelephense"
1116
language = "PHP"

src/language_servers.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
mod phptools;
12
mod intelephense;
23
mod phpactor;
34

5+
pub use phptools::*;
46
pub use intelephense::*;
57
pub use phpactor::*;

src/language_servers/phptools.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
use std::fs;
2+
use zed::{Architecture, Os};
3+
use zed_extension_api::settings::LspSettings;
4+
use zed_extension_api::{self as zed, serde_json, LanguageServerId, Result};
5+
6+
const PACKAGE_NAME: &str = "devsense-php-ls";
7+
8+
pub struct PhpTools {
9+
did_find_server: bool,
10+
}
11+
12+
impl PhpTools {
13+
pub const LANGUAGE_SERVER_ID: &'static str = "phptools";
14+
15+
pub fn new() -> Self {
16+
Self {
17+
did_find_server: false,
18+
}
19+
}
20+
21+
pub fn language_server_command(
22+
&mut self,
23+
language_server_id: &LanguageServerId,
24+
worktree: &zed::Worktree,
25+
) -> Result<zed::Command> {
26+
if let Some(path) = worktree.which("phptools") {
27+
return Ok(zed::Command {
28+
command: path,
29+
args: vec!["--stdio".to_string()],
30+
env: Default::default(),
31+
});
32+
}
33+
34+
let server_path = self.server_script_path(language_server_id)?;
35+
Ok(zed::Command {
36+
command: server_path,
37+
args: vec![
38+
"--composerNodes".into(),
39+
"false".into(), // disable /vendor/ caching
40+
],
41+
env: Default::default(),
42+
})
43+
}
44+
45+
fn server_file_path(&self) -> std::string::String {
46+
let (os, arch) = zed::current_platform();
47+
48+
let os_str = match os {
49+
Os::Mac => "darwin",
50+
Os::Linux => "linux",
51+
Os::Windows => "win32",
52+
};
53+
54+
let arch_str = match arch {
55+
Architecture::Aarch64 => "arm64",
56+
Architecture::X86 | Architecture::X8664 => "x64",
57+
};
58+
59+
let ext_str = match os {
60+
Os::Windows => ".exe",
61+
_ => "",
62+
};
63+
//
64+
format!(
65+
"node_modules/devsense-php-ls-{0}-{1}/dist/devsense.php.ls{2}",
66+
os_str, arch_str, ext_str
67+
)
68+
}
69+
70+
fn server_exists(&self) -> bool {
71+
fs::metadata(self.server_file_path()).map_or(false, |stat| stat.is_file())
72+
}
73+
74+
fn server_script_path(&mut self, language_server_id: &LanguageServerId) -> Result<String> {
75+
let server_exists = self.server_exists();
76+
let server_path = self.server_file_path();
77+
if self.did_find_server && server_exists {
78+
return Ok(server_path);
79+
}
80+
81+
zed::set_language_server_installation_status(
82+
language_server_id,
83+
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
84+
);
85+
let version = zed::npm_package_latest_version(PACKAGE_NAME)?;
86+
87+
if !server_exists
88+
|| zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
89+
{
90+
zed::set_language_server_installation_status(
91+
language_server_id,
92+
&zed::LanguageServerInstallationStatus::Downloading,
93+
);
94+
let result = zed::npm_install_package(PACKAGE_NAME, &version);
95+
match result {
96+
Ok(()) => {
97+
if !self.server_exists() {
98+
Err(format!(
99+
"installed package '{PACKAGE_NAME}' did not contain expected path '{server_path}'",
100+
))?;
101+
}
102+
}
103+
Err(error) => {
104+
if !self.server_exists() {
105+
Err(error)?;
106+
}
107+
}
108+
}
109+
}
110+
111+
self.did_find_server = true;
112+
Ok(server_path)
113+
}
114+
115+
pub fn language_server_workspace_configuration(
116+
&mut self,
117+
worktree: &zed::Worktree,
118+
) -> Result<Option<serde_json::Value>> {
119+
let settings = LspSettings::for_worktree("phptools", worktree)
120+
.ok()
121+
.and_then(|lsp_settings| lsp_settings.settings.clone())
122+
.unwrap_or_default();
123+
124+
Ok(Some(serde_json::json!({
125+
"phptools": settings
126+
})))
127+
}
128+
}

src/php.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ use zed_extension_api::{
88
};
99

1010
use crate::{
11-
language_servers::{Intelephense, Phpactor},
11+
language_servers::{PhpTools, Intelephense, Phpactor},
1212
xdebug::XDebug,
1313
};
1414

1515
struct PhpExtension {
16+
phptools: Option<PhpTools>,
1617
intelephense: Option<Intelephense>,
1718
phpactor: Option<Phpactor>,
1819
xdebug: XDebug,
@@ -21,6 +22,7 @@ struct PhpExtension {
2122
impl zed::Extension for PhpExtension {
2223
fn new() -> Self {
2324
Self {
25+
phptools: None,
2426
intelephense: None,
2527
phpactor: None,
2628
xdebug: XDebug::new(),
@@ -33,6 +35,10 @@ impl zed::Extension for PhpExtension {
3335
worktree: &zed::Worktree,
3436
) -> Result<zed::Command> {
3537
match language_server_id.as_ref() {
38+
PhpTools::LANGUAGE_SERVER_ID => {
39+
let phptools = self.phptools.get_or_insert_with(PhpTools::new);
40+
phptools.language_server_command(language_server_id, worktree)
41+
}
3642
Intelephense::LANGUAGE_SERVER_ID => {
3743
let intelephense = self.intelephense.get_or_insert_with(Intelephense::new);
3844
intelephense.language_server_command(language_server_id, worktree)
@@ -55,6 +61,11 @@ impl zed::Extension for PhpExtension {
5561
language_server_id: &LanguageServerId,
5662
worktree: &zed::Worktree,
5763
) -> Result<Option<serde_json::Value>> {
64+
if language_server_id.as_ref() == PhpTools::LANGUAGE_SERVER_ID {
65+
if let Some(phptools) = self.phptools.as_mut() {
66+
return phptools.language_server_workspace_configuration(worktree);
67+
}
68+
}
5869
if language_server_id.as_ref() == Intelephense::LANGUAGE_SERVER_ID {
5970
if let Some(intelephense) = self.intelephense.as_mut() {
6071
return intelephense.language_server_workspace_configuration(worktree);

0 commit comments

Comments
 (0)