intelephense.rs

  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}