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