php.rs

  1use anyhow::{anyhow, Result};
  2use async_compression::futures::bufread::GzipDecoder;
  3use async_tar::Archive;
  4use async_trait::async_trait;
  5use collections::HashMap;
  6use futures::{future::BoxFuture, FutureExt};
  7use gpui::AppContext;
  8use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
  9use lsp::{CodeActionKind, LanguageServerBinary};
 10use node_runtime::NodeRuntime;
 11use serde_json::{json, Value};
 12use smol::{fs, io::BufReader, stream::StreamExt};
 13use std::{
 14    any::Any,
 15    ffi::OsString,
 16    future,
 17    path::{Path, PathBuf},
 18    sync::Arc,
 19};
 20use util::ResultExt;
 21
 22fn intelephense_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 23    vec![server_path.into(), "--stdio".into()]
 24}
 25
 26pub struct IntelephenseVersion(String);
 27
 28pub struct IntelephenseLspAdapter {
 29    node: Arc<NodeRuntime>,
 30}
 31
 32impl IntelephenseLspAdapter {
 33    const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js";
 34
 35    #[allow(unused)]
 36    pub fn new(node: Arc<NodeRuntime>) -> Self {
 37        Self { node }
 38    }
 39}
 40
 41#[async_trait]
 42impl LspAdapter for IntelephenseLspAdapter {
 43    async fn name(&self) -> LanguageServerName {
 44        LanguageServerName("intelephense".into())
 45    }
 46
 47    async fn fetch_latest_server_version(
 48        &self,
 49        _delegate: &dyn LspAdapterDelegate,
 50    ) -> Result<Box<dyn 'static + Send + Any>> {
 51        // At the time of writing the latest vscode-eslint release was released in 2020 and requires
 52        // special custom LSP protocol extensions be handled to fully initialize. Download the latest
 53        // prerelease instead to sidestep this issue
 54        Ok(Box::new(IntelephenseVersion(
 55            self.node.npm_package_latest_version("intelephense").await?,
 56        )) as Box<_>)
 57    }
 58
 59    async fn fetch_server_binary(
 60        &self,
 61        version: Box<dyn 'static + Send + Any>,
 62        container_dir: PathBuf,
 63        _delegate: &dyn LspAdapterDelegate,
 64    ) -> Result<LanguageServerBinary> {
 65        let version = version.downcast::<IntelephenseVersion>().unwrap();
 66        let server_path = container_dir.join(Self::SERVER_PATH);
 67
 68        if fs::metadata(&server_path).await.is_err() {
 69            self.node
 70                .npm_install_packages(&container_dir, [("intelephense", version.0.as_str())])
 71                .await?;
 72        }
 73        Ok(LanguageServerBinary {
 74            path: self.node.binary_path().await?,
 75            arguments: intelephense_server_binary_arguments(&server_path),
 76        })
 77    }
 78
 79    async fn cached_server_binary(
 80        &self,
 81        container_dir: PathBuf,
 82        _: &dyn LspAdapterDelegate,
 83    ) -> Option<LanguageServerBinary> {
 84        get_cached_server_binary(container_dir, &self.node).await
 85    }
 86
 87    async fn installation_test_binary(
 88        &self,
 89        container_dir: PathBuf,
 90    ) -> Option<LanguageServerBinary> {
 91        get_cached_server_binary(container_dir, &self.node).await
 92    }
 93
 94    async fn label_for_completion(
 95        &self,
 96        _item: &lsp::CompletionItem,
 97        _language: &Arc<language::Language>,
 98    ) -> Option<language::CodeLabel> {
 99        None
100    }
101
102    async fn initialization_options(&self) -> Option<serde_json::Value> {
103        None
104    }
105    async fn language_ids(&self) -> HashMap<String, String> {
106        HashMap::from_iter([("PHP".into(), "php".into())])
107    }
108}
109
110async fn get_cached_server_binary(
111    container_dir: PathBuf,
112    node: &NodeRuntime,
113) -> Option<LanguageServerBinary> {
114    (|| async move {
115        let mut last_version_dir = None;
116        let mut entries = fs::read_dir(&container_dir).await?;
117        while let Some(entry) = entries.next().await {
118            let entry = entry?;
119            if entry.file_type().await?.is_dir() {
120                last_version_dir = Some(entry.path());
121            }
122        }
123        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
124        let server_path = last_version_dir.join(IntelephenseLspAdapter::SERVER_PATH);
125        if server_path.exists() {
126            Ok(LanguageServerBinary {
127                path: node.binary_path().await?,
128                arguments: intelephense_server_binary_arguments(&server_path),
129            })
130        } else {
131            Err(anyhow!(
132                "missing executable in directory {:?}",
133                last_version_dir
134            ))
135        }
136    })()
137    .await
138    .log_err()
139}
140
141#[cfg(test)]
142mod tests {
143    use gpui::TestAppContext;
144    use unindent::Unindent;
145
146    #[gpui::test]
147    async fn test_outline(cx: &mut TestAppContext) {
148        let language = crate::languages::language("php", tree_sitter_php::language(), None).await;
149
150        /*let text = r#"
151            function a() {
152              // local variables are omitted
153              let a1 = 1;
154              // all functions are included
155              async function a2() {}
156            }
157            // top-level variables are included
158            let b: C
159            function getB() {}
160            // exported variables are included
161            export const d = e;
162        "#
163        .unindent();*/
164        let text = r#"
165            function a() {
166              // local variables are omitted
167              $a1 = 1;
168              // all functions are included
169              function a2() {}
170            }
171            class Foo {}
172        "#
173        .unindent();
174        let buffer =
175            cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx));
176        let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
177        panic!(
178            "{:?}",
179            outline
180                .items
181                .iter()
182                .map(|item| (item.text.as_str(), item.depth))
183                .collect::<Vec<_>>()
184        );
185        assert_eq!(
186            outline
187                .items
188                .iter()
189                .map(|item| (item.text.as_str(), item.depth))
190                .collect::<Vec<_>>(),
191            &[
192                ("function a()", 0),
193                ("async function a2()", 1),
194                ("let b", 0),
195                ("function getB()", 0),
196                ("const d", 0),
197            ]
198        );
199    }
200}