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