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