1use anyhow::{anyhow, bail, Context, Result};
2use async_trait::async_trait;
3use futures::StreamExt;
4use gpui::{AsyncAppContext, Task};
5pub use language::*;
6use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
7use schemars::JsonSchema;
8use serde_derive::{Deserialize, Serialize};
9use settings::Settings;
10use smol::fs::{self, File};
11use std::{
12 any::Any,
13 env::consts,
14 ops::Deref,
15 path::PathBuf,
16 sync::{
17 atomic::{AtomicBool, Ordering::SeqCst},
18 Arc,
19 },
20};
21use util::{
22 async_maybe,
23 fs::remove_matching,
24 github::{latest_github_release, GitHubLspBinaryVersion},
25 ResultExt,
26};
27
28#[derive(Clone, Serialize, Deserialize, JsonSchema)]
29pub struct ElixirSettings {
30 pub lsp: ElixirLspSetting,
31}
32
33#[derive(Clone, Serialize, Deserialize, JsonSchema)]
34#[serde(rename_all = "snake_case")]
35pub enum ElixirLspSetting {
36 ElixirLs,
37 NextLs,
38 Local {
39 path: String,
40 arguments: Vec<String>,
41 },
42}
43
44#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
45pub struct ElixirSettingsContent {
46 lsp: Option<ElixirLspSetting>,
47}
48
49impl Settings for ElixirSettings {
50 const KEY: Option<&'static str> = Some("elixir");
51
52 type FileContent = ElixirSettingsContent;
53
54 fn load(
55 default_value: &Self::FileContent,
56 user_values: &[&Self::FileContent],
57 _: &mut gpui::AppContext,
58 ) -> Result<Self>
59 where
60 Self: Sized,
61 {
62 Self::load_via_json_merge(default_value, user_values)
63 }
64}
65
66pub struct ElixirLspAdapter;
67
68#[async_trait]
69impl LspAdapter for ElixirLspAdapter {
70 fn name(&self) -> LanguageServerName {
71 LanguageServerName("elixir-ls".into())
72 }
73
74 fn will_start_server(
75 &self,
76 delegate: &Arc<dyn LspAdapterDelegate>,
77 cx: &mut AsyncAppContext,
78 ) -> Option<Task<Result<()>>> {
79 static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
80
81 const NOTIFICATION_MESSAGE: &str = "Could not run the elixir language server, `elixir-ls`, because `elixir` was not found.";
82
83 let delegate = delegate.clone();
84 Some(cx.spawn(|cx| async move {
85 let elixir_output = smol::process::Command::new("elixir")
86 .args(["--version"])
87 .output()
88 .await;
89 if elixir_output.is_err() {
90 if DID_SHOW_NOTIFICATION
91 .compare_exchange(false, true, SeqCst, SeqCst)
92 .is_ok()
93 {
94 cx.update(|cx| {
95 delegate.show_notification(NOTIFICATION_MESSAGE, cx);
96 })?
97 }
98 return Err(anyhow!("cannot run elixir-ls"));
99 }
100
101 Ok(())
102 }))
103 }
104
105 async fn fetch_latest_server_version(
106 &self,
107 delegate: &dyn LspAdapterDelegate,
108 ) -> Result<Box<dyn 'static + Send + Any>> {
109 let http = delegate.http_client();
110 let release = latest_github_release("elixir-lsp/elixir-ls", true, false, http).await?;
111
112 let asset_name = format!("elixir-ls-{}.zip", &release.tag_name);
113 let asset = release
114 .assets
115 .iter()
116 .find(|asset| asset.name == asset_name)
117 .ok_or_else(|| anyhow!("no asset found matching {asset_name:?}"))?;
118
119 let version = GitHubLspBinaryVersion {
120 name: release.tag_name.clone(),
121 url: asset.browser_download_url.clone(),
122 };
123 Ok(Box::new(version) as Box<_>)
124 }
125
126 async fn fetch_server_binary(
127 &self,
128 version: Box<dyn 'static + Send + Any>,
129 container_dir: PathBuf,
130 delegate: &dyn LspAdapterDelegate,
131 ) -> Result<LanguageServerBinary> {
132 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
133 let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
134 let folder_path = container_dir.join("elixir-ls");
135 let binary_path = folder_path.join("language_server.sh");
136
137 if fs::metadata(&binary_path).await.is_err() {
138 let mut response = delegate
139 .http_client()
140 .get(&version.url, Default::default(), true)
141 .await
142 .context("error downloading release")?;
143 let mut file = File::create(&zip_path)
144 .await
145 .with_context(|| format!("failed to create file {}", zip_path.display()))?;
146 if !response.status().is_success() {
147 Err(anyhow!(
148 "download failed with status {}",
149 response.status().to_string()
150 ))?;
151 }
152 futures::io::copy(response.body_mut(), &mut file).await?;
153
154 fs::create_dir_all(&folder_path)
155 .await
156 .with_context(|| format!("failed to create directory {}", folder_path.display()))?;
157 let unzip_status = smol::process::Command::new("unzip")
158 .arg(&zip_path)
159 .arg("-d")
160 .arg(&folder_path)
161 .output()
162 .await?
163 .status;
164 if !unzip_status.success() {
165 Err(anyhow!("failed to unzip elixir-ls archive"))?;
166 }
167
168 remove_matching(&container_dir, |entry| entry != folder_path).await;
169 }
170
171 Ok(LanguageServerBinary {
172 path: binary_path,
173 env: None,
174 arguments: vec![],
175 })
176 }
177
178 async fn cached_server_binary(
179 &self,
180 container_dir: PathBuf,
181 _: &dyn LspAdapterDelegate,
182 ) -> Option<LanguageServerBinary> {
183 get_cached_server_binary_elixir_ls(container_dir).await
184 }
185
186 async fn installation_test_binary(
187 &self,
188 container_dir: PathBuf,
189 ) -> Option<LanguageServerBinary> {
190 get_cached_server_binary_elixir_ls(container_dir).await
191 }
192
193 async fn label_for_completion(
194 &self,
195 completion: &lsp::CompletionItem,
196 language: &Arc<Language>,
197 ) -> Option<CodeLabel> {
198 match completion.kind.zip(completion.detail.as_ref()) {
199 Some((_, detail)) if detail.starts_with("(function)") => {
200 let text = detail.strip_prefix("(function) ")?;
201 let filter_range = 0..text.find('(').unwrap_or(text.len());
202 let source = Rope::from(format!("def {text}").as_str());
203 let runs = language.highlight_text(&source, 4..4 + text.len());
204 return Some(CodeLabel {
205 text: text.to_string(),
206 runs,
207 filter_range,
208 });
209 }
210 Some((_, detail)) if detail.starts_with("(macro)") => {
211 let text = detail.strip_prefix("(macro) ")?;
212 let filter_range = 0..text.find('(').unwrap_or(text.len());
213 let source = Rope::from(format!("defmacro {text}").as_str());
214 let runs = language.highlight_text(&source, 9..9 + text.len());
215 return Some(CodeLabel {
216 text: text.to_string(),
217 runs,
218 filter_range,
219 });
220 }
221 Some((
222 CompletionItemKind::CLASS
223 | CompletionItemKind::MODULE
224 | CompletionItemKind::INTERFACE
225 | CompletionItemKind::STRUCT,
226 _,
227 )) => {
228 let filter_range = 0..completion
229 .label
230 .find(" (")
231 .unwrap_or(completion.label.len());
232 let text = &completion.label[filter_range.clone()];
233 let source = Rope::from(format!("defmodule {text}").as_str());
234 let runs = language.highlight_text(&source, 10..10 + text.len());
235 return Some(CodeLabel {
236 text: completion.label.clone(),
237 runs,
238 filter_range,
239 });
240 }
241 _ => {}
242 }
243
244 None
245 }
246
247 async fn label_for_symbol(
248 &self,
249 name: &str,
250 kind: SymbolKind,
251 language: &Arc<Language>,
252 ) -> Option<CodeLabel> {
253 let (text, filter_range, display_range) = match kind {
254 SymbolKind::METHOD | SymbolKind::FUNCTION => {
255 let text = format!("def {}", name);
256 let filter_range = 4..4 + name.len();
257 let display_range = 0..filter_range.end;
258 (text, filter_range, display_range)
259 }
260 SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => {
261 let text = format!("defmodule {}", name);
262 let filter_range = 10..10 + name.len();
263 let display_range = 0..filter_range.end;
264 (text, filter_range, display_range)
265 }
266 _ => return None,
267 };
268
269 Some(CodeLabel {
270 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
271 text: text[display_range].to_string(),
272 filter_range,
273 })
274 }
275}
276
277async fn get_cached_server_binary_elixir_ls(
278 container_dir: PathBuf,
279) -> Option<LanguageServerBinary> {
280 let server_path = container_dir.join("elixir-ls/language_server.sh");
281 if server_path.exists() {
282 Some(LanguageServerBinary {
283 path: server_path,
284 env: None,
285 arguments: vec![],
286 })
287 } else {
288 log::error!("missing executable in directory {:?}", server_path);
289 None
290 }
291}
292
293pub struct NextLspAdapter;
294
295#[async_trait]
296impl LspAdapter for NextLspAdapter {
297 fn name(&self) -> LanguageServerName {
298 LanguageServerName("next-ls".into())
299 }
300
301 async fn fetch_latest_server_version(
302 &self,
303 delegate: &dyn LspAdapterDelegate,
304 ) -> Result<Box<dyn 'static + Send + Any>> {
305 let platform = match consts::ARCH {
306 "x86_64" => "darwin_amd64",
307 "aarch64" => "darwin_arm64",
308 other => bail!("Running on unsupported platform: {other}"),
309 };
310 let release =
311 latest_github_release("elixir-tools/next-ls", true, false, delegate.http_client())
312 .await?;
313 let version = release.tag_name;
314 let asset_name = format!("next_ls_{platform}");
315 let asset = release
316 .assets
317 .iter()
318 .find(|asset| asset.name == asset_name)
319 .with_context(|| format!("no asset found matching {asset_name:?}"))?;
320 let version = GitHubLspBinaryVersion {
321 name: version,
322 url: asset.browser_download_url.clone(),
323 };
324 Ok(Box::new(version) as Box<_>)
325 }
326
327 async fn fetch_server_binary(
328 &self,
329 version: Box<dyn 'static + Send + Any>,
330 container_dir: PathBuf,
331 delegate: &dyn LspAdapterDelegate,
332 ) -> Result<LanguageServerBinary> {
333 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
334
335 let binary_path = container_dir.join("next-ls");
336
337 if fs::metadata(&binary_path).await.is_err() {
338 let mut response = delegate
339 .http_client()
340 .get(&version.url, Default::default(), true)
341 .await
342 .map_err(|err| anyhow!("error downloading release: {}", err))?;
343
344 let mut file = smol::fs::File::create(&binary_path).await?;
345 if !response.status().is_success() {
346 Err(anyhow!(
347 "download failed with status {}",
348 response.status().to_string()
349 ))?;
350 }
351 futures::io::copy(response.body_mut(), &mut file).await?;
352
353 // todo(windows)
354 #[cfg(not(windows))]
355 {
356 fs::set_permissions(
357 &binary_path,
358 <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
359 )
360 .await?;
361 }
362 }
363
364 Ok(LanguageServerBinary {
365 path: binary_path,
366 env: None,
367 arguments: vec!["--stdio".into()],
368 })
369 }
370
371 async fn cached_server_binary(
372 &self,
373 container_dir: PathBuf,
374 _: &dyn LspAdapterDelegate,
375 ) -> Option<LanguageServerBinary> {
376 get_cached_server_binary_next(container_dir)
377 .await
378 .map(|mut binary| {
379 binary.arguments = vec!["--stdio".into()];
380 binary
381 })
382 }
383
384 async fn installation_test_binary(
385 &self,
386 container_dir: PathBuf,
387 ) -> Option<LanguageServerBinary> {
388 get_cached_server_binary_next(container_dir)
389 .await
390 .map(|mut binary| {
391 binary.arguments = vec!["--help".into()];
392 binary
393 })
394 }
395
396 async fn label_for_completion(
397 &self,
398 completion: &lsp::CompletionItem,
399 language: &Arc<Language>,
400 ) -> Option<CodeLabel> {
401 label_for_completion_elixir(completion, language)
402 }
403
404 async fn label_for_symbol(
405 &self,
406 name: &str,
407 symbol_kind: SymbolKind,
408 language: &Arc<Language>,
409 ) -> Option<CodeLabel> {
410 label_for_symbol_elixir(name, symbol_kind, language)
411 }
412}
413
414async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option<LanguageServerBinary> {
415 async_maybe!({
416 let mut last_binary_path = None;
417 let mut entries = fs::read_dir(&container_dir).await?;
418 while let Some(entry) = entries.next().await {
419 let entry = entry?;
420 if entry.file_type().await?.is_file()
421 && entry
422 .file_name()
423 .to_str()
424 .map_or(false, |name| name == "next-ls")
425 {
426 last_binary_path = Some(entry.path());
427 }
428 }
429
430 if let Some(path) = last_binary_path {
431 Ok(LanguageServerBinary {
432 path,
433 env: None,
434 arguments: Vec::new(),
435 })
436 } else {
437 Err(anyhow!("no cached binary"))
438 }
439 })
440 .await
441 .log_err()
442}
443
444pub struct LocalLspAdapter {
445 pub path: String,
446 pub arguments: Vec<String>,
447}
448
449#[async_trait]
450impl LspAdapter for LocalLspAdapter {
451 fn name(&self) -> LanguageServerName {
452 LanguageServerName("local-ls".into())
453 }
454
455 async fn fetch_latest_server_version(
456 &self,
457 _: &dyn LspAdapterDelegate,
458 ) -> Result<Box<dyn 'static + Send + Any>> {
459 Ok(Box::new(()) as Box<_>)
460 }
461
462 async fn fetch_server_binary(
463 &self,
464 _: Box<dyn 'static + Send + Any>,
465 _: PathBuf,
466 _: &dyn LspAdapterDelegate,
467 ) -> Result<LanguageServerBinary> {
468 let path = shellexpand::full(&self.path)?;
469 Ok(LanguageServerBinary {
470 path: PathBuf::from(path.deref()),
471 env: None,
472 arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
473 })
474 }
475
476 async fn cached_server_binary(
477 &self,
478 _: PathBuf,
479 _: &dyn LspAdapterDelegate,
480 ) -> Option<LanguageServerBinary> {
481 let path = shellexpand::full(&self.path).ok()?;
482 Some(LanguageServerBinary {
483 path: PathBuf::from(path.deref()),
484 env: None,
485 arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
486 })
487 }
488
489 async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
490 let path = shellexpand::full(&self.path).ok()?;
491 Some(LanguageServerBinary {
492 path: PathBuf::from(path.deref()),
493 env: None,
494 arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
495 })
496 }
497
498 async fn label_for_completion(
499 &self,
500 completion: &lsp::CompletionItem,
501 language: &Arc<Language>,
502 ) -> Option<CodeLabel> {
503 label_for_completion_elixir(completion, language)
504 }
505
506 async fn label_for_symbol(
507 &self,
508 name: &str,
509 symbol: SymbolKind,
510 language: &Arc<Language>,
511 ) -> Option<CodeLabel> {
512 label_for_symbol_elixir(name, symbol, language)
513 }
514}
515
516fn label_for_completion_elixir(
517 completion: &lsp::CompletionItem,
518 language: &Arc<Language>,
519) -> Option<CodeLabel> {
520 return Some(CodeLabel {
521 runs: language.highlight_text(&completion.label.clone().into(), 0..completion.label.len()),
522 text: completion.label.clone(),
523 filter_range: 0..completion.label.len(),
524 });
525}
526
527fn label_for_symbol_elixir(
528 name: &str,
529 _: SymbolKind,
530 language: &Arc<Language>,
531) -> Option<CodeLabel> {
532 Some(CodeLabel {
533 runs: language.highlight_text(&name.into(), 0..name.len()),
534 text: name.to_string(),
535 filter_range: 0..name.len(),
536 })
537}