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