1use anyhow::{anyhow, Context, Result};
2use async_trait::async_trait;
3use futures::StreamExt;
4use gpui::{AsyncAppContext, Task};
5pub use language::*;
6use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
7use smol::fs::{self, File};
8use std::{
9 any::Any,
10 path::PathBuf,
11 sync::{
12 atomic::{AtomicBool, Ordering::SeqCst},
13 Arc,
14 },
15};
16use util::{
17 fs::remove_matching,
18 github::{latest_github_release, GitHubLspBinaryVersion},
19 ResultExt,
20};
21
22pub struct ElixirLspAdapter;
23
24#[async_trait]
25impl LspAdapter for ElixirLspAdapter {
26 async fn name(&self) -> LanguageServerName {
27 LanguageServerName("elixir-ls".into())
28 }
29
30 fn will_start_server(
31 &self,
32 delegate: &Arc<dyn LspAdapterDelegate>,
33 cx: &mut AsyncAppContext,
34 ) -> Option<Task<Result<()>>> {
35 static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
36
37 const NOTIFICATION_MESSAGE: &str = "Could not run the elixir language server, `elixir-ls`, because `elixir` was not found.";
38
39 let delegate = delegate.clone();
40 Some(cx.spawn(|mut cx| async move {
41 let elixir_output = smol::process::Command::new("elixir")
42 .args(["--version"])
43 .output()
44 .await;
45 if elixir_output.is_err() {
46 if DID_SHOW_NOTIFICATION
47 .compare_exchange(false, true, SeqCst, SeqCst)
48 .is_ok()
49 {
50 cx.update(|cx| {
51 delegate.show_notification(NOTIFICATION_MESSAGE, cx);
52 })
53 }
54 return Err(anyhow!("cannot run elixir-ls"));
55 }
56
57 Ok(())
58 }))
59 }
60
61 async fn fetch_latest_server_version(
62 &self,
63 delegate: &dyn LspAdapterDelegate,
64 ) -> Result<Box<dyn 'static + Send + Any>> {
65 let http = delegate.http_client();
66 let release = latest_github_release("elixir-lsp/elixir-ls", false, http).await?;
67 let version_name = release
68 .name
69 .strip_prefix("Release ")
70 .context("Elixir-ls release name does not start with prefix")?
71 .to_owned();
72
73 let asset_name = format!("elixir-ls-{}.zip", &version_name);
74 let asset = release
75 .assets
76 .iter()
77 .find(|asset| asset.name == asset_name)
78 .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
79
80 let version = GitHubLspBinaryVersion {
81 name: version_name,
82 url: asset.browser_download_url.clone(),
83 };
84 Ok(Box::new(version) as Box<_>)
85 }
86
87 async fn fetch_server_binary(
88 &self,
89 version: Box<dyn 'static + Send + Any>,
90 container_dir: PathBuf,
91 delegate: &dyn LspAdapterDelegate,
92 ) -> Result<LanguageServerBinary> {
93 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
94 let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
95 let version_dir = container_dir.join(format!("elixir-ls_{}", version.name));
96 let binary_path = version_dir.join("language_server.sh");
97
98 if fs::metadata(&binary_path).await.is_err() {
99 let mut response = delegate
100 .http_client()
101 .get(&version.url, Default::default(), true)
102 .await
103 .context("error downloading release")?;
104 let mut file = File::create(&zip_path)
105 .await
106 .with_context(|| format!("failed to create file {}", zip_path.display()))?;
107 if !response.status().is_success() {
108 Err(anyhow!(
109 "download failed with status {}",
110 response.status().to_string()
111 ))?;
112 }
113 futures::io::copy(response.body_mut(), &mut file).await?;
114
115 fs::create_dir_all(&version_dir)
116 .await
117 .with_context(|| format!("failed to create directory {}", version_dir.display()))?;
118 let unzip_status = smol::process::Command::new("unzip")
119 .arg(&zip_path)
120 .arg("-d")
121 .arg(&version_dir)
122 .output()
123 .await?
124 .status;
125 if !unzip_status.success() {
126 Err(anyhow!("failed to unzip elixir-ls archive"))?;
127 }
128
129 remove_matching(&container_dir, |entry| entry != version_dir).await;
130 }
131
132 Ok(LanguageServerBinary {
133 path: binary_path,
134 arguments: vec![],
135 })
136 }
137
138 async fn cached_server_binary(
139 &self,
140 container_dir: PathBuf,
141 _: &dyn LspAdapterDelegate,
142 ) -> Option<LanguageServerBinary> {
143 get_cached_server_binary(container_dir).await
144 }
145
146 async fn installation_test_binary(
147 &self,
148 container_dir: PathBuf,
149 ) -> Option<LanguageServerBinary> {
150 get_cached_server_binary(container_dir).await
151 }
152
153 async fn label_for_completion(
154 &self,
155 completion: &lsp::CompletionItem,
156 language: &Arc<Language>,
157 ) -> Option<CodeLabel> {
158 match completion.kind.zip(completion.detail.as_ref()) {
159 Some((_, detail)) if detail.starts_with("(function)") => {
160 let text = detail.strip_prefix("(function) ")?;
161 let filter_range = 0..text.find('(').unwrap_or(text.len());
162 let source = Rope::from(format!("def {text}").as_str());
163 let runs = language.highlight_text(&source, 4..4 + text.len());
164 return Some(CodeLabel {
165 text: text.to_string(),
166 runs,
167 filter_range,
168 });
169 }
170 Some((_, detail)) if detail.starts_with("(macro)") => {
171 let text = detail.strip_prefix("(macro) ")?;
172 let filter_range = 0..text.find('(').unwrap_or(text.len());
173 let source = Rope::from(format!("defmacro {text}").as_str());
174 let runs = language.highlight_text(&source, 9..9 + text.len());
175 return Some(CodeLabel {
176 text: text.to_string(),
177 runs,
178 filter_range,
179 });
180 }
181 Some((
182 CompletionItemKind::CLASS
183 | CompletionItemKind::MODULE
184 | CompletionItemKind::INTERFACE
185 | CompletionItemKind::STRUCT,
186 _,
187 )) => {
188 let filter_range = 0..completion
189 .label
190 .find(" (")
191 .unwrap_or(completion.label.len());
192 let text = &completion.label[filter_range.clone()];
193 let source = Rope::from(format!("defmodule {text}").as_str());
194 let runs = language.highlight_text(&source, 10..10 + text.len());
195 return Some(CodeLabel {
196 text: completion.label.clone(),
197 runs,
198 filter_range,
199 });
200 }
201 _ => {}
202 }
203
204 None
205 }
206
207 async fn label_for_symbol(
208 &self,
209 name: &str,
210 kind: SymbolKind,
211 language: &Arc<Language>,
212 ) -> Option<CodeLabel> {
213 let (text, filter_range, display_range) = match kind {
214 SymbolKind::METHOD | SymbolKind::FUNCTION => {
215 let text = format!("def {}", name);
216 let filter_range = 4..4 + name.len();
217 let display_range = 0..filter_range.end;
218 (text, filter_range, display_range)
219 }
220 SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => {
221 let text = format!("defmodule {}", name);
222 let filter_range = 10..10 + name.len();
223 let display_range = 0..filter_range.end;
224 (text, filter_range, display_range)
225 }
226 _ => return None,
227 };
228
229 Some(CodeLabel {
230 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
231 text: text[display_range].to_string(),
232 filter_range,
233 })
234 }
235}
236
237async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
238 (|| async move {
239 let mut last = None;
240 let mut entries = fs::read_dir(&container_dir).await?;
241 while let Some(entry) = entries.next().await {
242 last = Some(entry?.path());
243 }
244 last.map(|path| LanguageServerBinary {
245 path,
246 arguments: vec![],
247 })
248 .ok_or_else(|| anyhow!("no cached binary"))
249 })()
250 .await
251 .log_err()
252}