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