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;
12use smol::fs::{self, File};
13use std::{
14 any::Any,
15 env::consts,
16 ops::Deref,
17 path::{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 fn workspace_configuration(&self, _workspace_root: &Path, cx: &mut AppContext) -> Value {
282 let settings = ProjectSettings::get_global(cx)
283 .lsp
284 .get("elixir-ls")
285 .and_then(|s| s.settings.clone())
286 .unwrap_or_default();
287
288 serde_json::json!({
289 "elixirLS": settings
290 })
291 }
292}
293
294async fn get_cached_server_binary_elixir_ls(
295 container_dir: PathBuf,
296) -> Option<LanguageServerBinary> {
297 let server_path = container_dir.join("elixir-ls/language_server.sh");
298 if server_path.exists() {
299 Some(LanguageServerBinary {
300 path: server_path,
301 env: None,
302 arguments: vec![],
303 })
304 } else {
305 log::error!("missing executable in directory {:?}", server_path);
306 None
307 }
308}
309
310pub struct NextLspAdapter;
311
312#[async_trait(?Send)]
313impl LspAdapter for NextLspAdapter {
314 fn name(&self) -> LanguageServerName {
315 LanguageServerName("next-ls".into())
316 }
317
318 async fn fetch_latest_server_version(
319 &self,
320 delegate: &dyn LspAdapterDelegate,
321 ) -> Result<Box<dyn 'static + Send + Any>> {
322 let platform = match consts::ARCH {
323 "x86_64" => "darwin_amd64",
324 "aarch64" => "darwin_arm64",
325 other => bail!("Running on unsupported platform: {other}"),
326 };
327 let release =
328 latest_github_release("elixir-tools/next-ls", true, false, delegate.http_client())
329 .await?;
330 let version = release.tag_name;
331 let asset_name = format!("next_ls_{platform}");
332 let asset = release
333 .assets
334 .iter()
335 .find(|asset| asset.name == asset_name)
336 .with_context(|| format!("no asset found matching {asset_name:?}"))?;
337 let version = GitHubLspBinaryVersion {
338 name: version,
339 url: asset.browser_download_url.clone(),
340 };
341 Ok(Box::new(version) as Box<_>)
342 }
343
344 async fn fetch_server_binary(
345 &self,
346 version: Box<dyn 'static + Send + Any>,
347 container_dir: PathBuf,
348 delegate: &dyn LspAdapterDelegate,
349 ) -> Result<LanguageServerBinary> {
350 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
351
352 let binary_path = container_dir.join("next-ls");
353
354 if fs::metadata(&binary_path).await.is_err() {
355 let mut response = delegate
356 .http_client()
357 .get(&version.url, Default::default(), true)
358 .await
359 .map_err(|err| anyhow!("error downloading release: {}", err))?;
360
361 let mut file = smol::fs::File::create(&binary_path).await?;
362 if !response.status().is_success() {
363 Err(anyhow!(
364 "download failed with status {}",
365 response.status().to_string()
366 ))?;
367 }
368 futures::io::copy(response.body_mut(), &mut file).await?;
369
370 // todo("windows")
371 #[cfg(not(windows))]
372 {
373 fs::set_permissions(
374 &binary_path,
375 <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
376 )
377 .await?;
378 }
379 }
380
381 Ok(LanguageServerBinary {
382 path: binary_path,
383 env: None,
384 arguments: vec!["--stdio".into()],
385 })
386 }
387
388 async fn cached_server_binary(
389 &self,
390 container_dir: PathBuf,
391 _: &dyn LspAdapterDelegate,
392 ) -> Option<LanguageServerBinary> {
393 get_cached_server_binary_next(container_dir)
394 .await
395 .map(|mut binary| {
396 binary.arguments = vec!["--stdio".into()];
397 binary
398 })
399 }
400
401 async fn installation_test_binary(
402 &self,
403 container_dir: PathBuf,
404 ) -> Option<LanguageServerBinary> {
405 get_cached_server_binary_next(container_dir)
406 .await
407 .map(|mut binary| {
408 binary.arguments = vec!["--help".into()];
409 binary
410 })
411 }
412
413 async fn label_for_completion(
414 &self,
415 completion: &lsp::CompletionItem,
416 language: &Arc<Language>,
417 ) -> Option<CodeLabel> {
418 label_for_completion_elixir(completion, language)
419 }
420
421 async fn label_for_symbol(
422 &self,
423 name: &str,
424 symbol_kind: SymbolKind,
425 language: &Arc<Language>,
426 ) -> Option<CodeLabel> {
427 label_for_symbol_elixir(name, symbol_kind, language)
428 }
429}
430
431async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option<LanguageServerBinary> {
432 maybe!(async {
433 let mut last_binary_path = None;
434 let mut entries = fs::read_dir(&container_dir).await?;
435 while let Some(entry) = entries.next().await {
436 let entry = entry?;
437 if entry.file_type().await?.is_file()
438 && entry
439 .file_name()
440 .to_str()
441 .map_or(false, |name| name == "next-ls")
442 {
443 last_binary_path = Some(entry.path());
444 }
445 }
446
447 if let Some(path) = last_binary_path {
448 Ok(LanguageServerBinary {
449 path,
450 env: None,
451 arguments: Vec::new(),
452 })
453 } else {
454 Err(anyhow!("no cached binary"))
455 }
456 })
457 .await
458 .log_err()
459}
460
461pub struct LocalLspAdapter {
462 pub path: String,
463 pub arguments: Vec<String>,
464}
465
466#[async_trait(?Send)]
467impl LspAdapter for LocalLspAdapter {
468 fn name(&self) -> LanguageServerName {
469 LanguageServerName("local-ls".into())
470 }
471
472 async fn fetch_latest_server_version(
473 &self,
474 _: &dyn LspAdapterDelegate,
475 ) -> Result<Box<dyn 'static + Send + Any>> {
476 Ok(Box::new(()) as Box<_>)
477 }
478
479 async fn fetch_server_binary(
480 &self,
481 _: Box<dyn 'static + Send + Any>,
482 _: PathBuf,
483 _: &dyn LspAdapterDelegate,
484 ) -> Result<LanguageServerBinary> {
485 let path = shellexpand::full(&self.path)?;
486 Ok(LanguageServerBinary {
487 path: PathBuf::from(path.deref()),
488 env: None,
489 arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
490 })
491 }
492
493 async fn cached_server_binary(
494 &self,
495 _: PathBuf,
496 _: &dyn LspAdapterDelegate,
497 ) -> Option<LanguageServerBinary> {
498 let path = shellexpand::full(&self.path).ok()?;
499 Some(LanguageServerBinary {
500 path: PathBuf::from(path.deref()),
501 env: None,
502 arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
503 })
504 }
505
506 async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
507 let path = shellexpand::full(&self.path).ok()?;
508 Some(LanguageServerBinary {
509 path: PathBuf::from(path.deref()),
510 env: None,
511 arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
512 })
513 }
514
515 async fn label_for_completion(
516 &self,
517 completion: &lsp::CompletionItem,
518 language: &Arc<Language>,
519 ) -> Option<CodeLabel> {
520 label_for_completion_elixir(completion, language)
521 }
522
523 async fn label_for_symbol(
524 &self,
525 name: &str,
526 symbol: SymbolKind,
527 language: &Arc<Language>,
528 ) -> Option<CodeLabel> {
529 label_for_symbol_elixir(name, symbol, language)
530 }
531}
532
533fn label_for_completion_elixir(
534 completion: &lsp::CompletionItem,
535 language: &Arc<Language>,
536) -> Option<CodeLabel> {
537 return Some(CodeLabel {
538 runs: language.highlight_text(&completion.label.clone().into(), 0..completion.label.len()),
539 text: completion.label.clone(),
540 filter_range: 0..completion.label.len(),
541 });
542}
543
544fn label_for_symbol_elixir(
545 name: &str,
546 _: SymbolKind,
547 language: &Arc<Language>,
548) -> Option<CodeLabel> {
549 Some(CodeLabel {
550 runs: language.highlight_text(&name.into(), 0..name.len()),
551 text: name.to_string(),
552 filter_range: 0..name.len(),
553 })
554}
555
556pub(super) fn elixir_task_context() -> ContextProviderWithTasks {
557 // Taken from https://gist.github.com/josevalim/2e4f60a14ccd52728e3256571259d493#gistcomment-4995881
558 ContextProviderWithTasks::new(TaskDefinitions(vec![
559 Definition {
560 label: "Elixir: test suite".to_owned(),
561 command: "mix".to_owned(),
562 args: vec!["test".to_owned()],
563 ..Definition::default()
564 },
565 Definition {
566 label: "Elixir: failed tests suite".to_owned(),
567 command: "mix".to_owned(),
568 args: vec!["test".to_owned(), "--failed".to_owned()],
569 ..Definition::default()
570 },
571 Definition {
572 label: "Elixir: test file".to_owned(),
573 command: "mix".to_owned(),
574 args: vec!["test".to_owned(), VariableName::Symbol.template_value()],
575 ..Definition::default()
576 },
577 Definition {
578 label: "Elixir: test at current line".to_owned(),
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 ..Definition::default()
589 },
590 Definition {
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 ..Definition::default()
605 },
606 ]))
607}