1use std::{env, fs};
2
3use zed::{CodeLabel, CodeLabelSpan};
4use zed_extension_api::settings::LspSettings;
5use zed_extension_api::{self as zed, serde_json, LanguageServerId, Result};
6
7const SERVER_PATH: &str = "node_modules/intelephense/lib/intelephense.js";
8const PACKAGE_NAME: &str = "intelephense";
9
10pub struct Intelephense {
11 did_find_server: bool,
12}
13
14impl Intelephense {
15 pub const LANGUAGE_SERVER_ID: &'static str = "intelephense";
16
17 pub fn new() -> Self {
18 Self {
19 did_find_server: false,
20 }
21 }
22
23 pub fn language_server_command(
24 &mut self,
25 language_server_id: &LanguageServerId,
26 worktree: &zed::Worktree,
27 ) -> Result<zed::Command> {
28 if let Some(path) = worktree.which("intelephense") {
29 return Ok(zed::Command {
30 command: path,
31 args: vec!["--stdio".to_string()],
32 env: Default::default(),
33 });
34 }
35
36 let server_path = self.server_script_path(language_server_id)?;
37 Ok(zed::Command {
38 command: zed::node_binary_path()?,
39 args: vec![
40 env::current_dir()
41 .unwrap()
42 .join(&server_path)
43 .to_string_lossy()
44 .to_string(),
45 "--stdio".to_string(),
46 ],
47 env: Default::default(),
48 })
49 }
50
51 fn server_exists(&self) -> bool {
52 fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
53 }
54
55 fn server_script_path(&mut self, language_server_id: &LanguageServerId) -> Result<String> {
56 let server_exists = self.server_exists();
57 if self.did_find_server && server_exists {
58 return Ok(SERVER_PATH.to_string());
59 }
60
61 zed::set_language_server_installation_status(
62 language_server_id,
63 &zed::LanguageServerInstallationStatus::CheckingForUpdate,
64 );
65 let version = zed::npm_package_latest_version(PACKAGE_NAME)?;
66
67 if !server_exists
68 || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
69 {
70 zed::set_language_server_installation_status(
71 language_server_id,
72 &zed::LanguageServerInstallationStatus::Downloading,
73 );
74 let result = zed::npm_install_package(PACKAGE_NAME, &version);
75 match result {
76 Ok(()) => {
77 if !self.server_exists() {
78 Err(format!(
79 "installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'",
80 ))?;
81 }
82 }
83 Err(error) => {
84 if !self.server_exists() {
85 Err(error)?;
86 }
87 }
88 }
89 }
90
91 self.did_find_server = true;
92 Ok(SERVER_PATH.to_string())
93 }
94
95 pub fn language_server_workspace_configuration(
96 &mut self,
97 worktree: &zed::Worktree,
98 ) -> Result<Option<serde_json::Value>> {
99 let settings = LspSettings::for_worktree("intelephense", worktree)
100 .ok()
101 .and_then(|lsp_settings| lsp_settings.settings.clone())
102 .unwrap_or_default();
103
104 Ok(Some(serde_json::json!({
105 "intelephense": settings
106 })))
107 }
108
109 pub fn label_for_completion(&self, completion: zed::lsp::Completion) -> Option<CodeLabel> {
110 let label = &completion.label;
111
112 match completion.kind? {
113 zed::lsp::CompletionKind::Method => {
114 // __construct method doesn't have a detail
115 if let Some(ref detail) = completion.detail {
116 if detail.is_empty() {
117 return Some(CodeLabel {
118 spans: vec![
119 CodeLabelSpan::literal(label, Some("function.method".to_string())),
120 CodeLabelSpan::literal("()", None),
121 ],
122 filter_range: (0..label.len()).into(),
123 code: completion.label,
124 });
125 }
126 }
127
128 let mut parts = completion.detail.as_ref()?.split(":");
129 // E.g., `foo(string $var)`
130 let name_and_params = parts.next()?;
131 let return_type = parts.next()?.trim();
132
133 let (_, params) = name_and_params.split_once("(")?;
134 let params = params.trim_end_matches(")");
135
136 Some(CodeLabel {
137 spans: vec![
138 CodeLabelSpan::literal(label, Some("function.method".to_string())),
139 CodeLabelSpan::literal("(", None),
140 CodeLabelSpan::literal(params, Some("comment".to_string())),
141 CodeLabelSpan::literal("): ", None),
142 CodeLabelSpan::literal(return_type, Some("type".to_string())),
143 ],
144 filter_range: (0..label.len()).into(),
145 code: completion.label,
146 })
147 }
148 zed::lsp::CompletionKind::Constant | zed::lsp::CompletionKind::EnumMember => {
149 if let Some(ref detail) = completion.detail {
150 if !detail.is_empty() {
151 return Some(CodeLabel {
152 spans: vec![
153 CodeLabelSpan::literal(label, Some("constant".to_string())),
154 CodeLabelSpan::literal(" ", None),
155 CodeLabelSpan::literal(detail, Some("comment".to_string())),
156 ],
157 filter_range: (0..label.len()).into(),
158 code: completion.label,
159 });
160 }
161 }
162
163 Some(CodeLabel {
164 spans: vec![CodeLabelSpan::literal(label, Some("constant".to_string()))],
165 filter_range: (0..label.len()).into(),
166 code: completion.label,
167 })
168 }
169 zed::lsp::CompletionKind::Property => {
170 let return_type = completion.detail?;
171 Some(CodeLabel {
172 spans: vec![
173 CodeLabelSpan::literal(label, Some("attribute".to_string())),
174 CodeLabelSpan::literal(": ", None),
175 CodeLabelSpan::literal(return_type, Some("type".to_string())),
176 ],
177 filter_range: (0..label.len()).into(),
178 code: completion.label,
179 })
180 }
181 zed::lsp::CompletionKind::Variable => {
182 // See https://www.php.net/manual/en/reserved.variables.php
183 const SYSTEM_VAR_NAMES: &[&str] =
184 &["argc", "argv", "php_errormsg", "http_response_header"];
185
186 let var_name = completion.label.trim_start_matches("$");
187 let is_uppercase = var_name
188 .chars()
189 .filter(|c| c.is_alphabetic())
190 .all(|c| c.is_uppercase());
191 let is_system_constant = var_name.starts_with("_");
192 let is_reserved = SYSTEM_VAR_NAMES.contains(&var_name);
193
194 let highlight = if is_uppercase || is_system_constant || is_reserved {
195 Some("comment".to_string())
196 } else {
197 None
198 };
199
200 Some(CodeLabel {
201 spans: vec![CodeLabelSpan::literal(label, highlight)],
202 filter_range: (0..label.len()).into(),
203 code: completion.label,
204 })
205 }
206 _ => None,
207 }
208 }
209}