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}