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