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 async 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", false, http).await?;
115 let version_name = release
116 .name
117 .strip_prefix("Release ")
118 .context("Elixir-ls release name does not start with prefix")?
119 .to_owned();
120
121 let asset_name = format!("elixir-ls-{}.zip", &version_name);
122 let asset = release
123 .assets
124 .iter()
125 .find(|asset| asset.name == asset_name)
126 .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
127
128 let version = GitHubLspBinaryVersion {
129 name: version_name,
130 url: asset.browser_download_url.clone(),
131 };
132 Ok(Box::new(version) as Box<_>)
133 }
134
135 async fn fetch_server_binary(
136 &self,
137 version: Box<dyn 'static + Send + Any>,
138 container_dir: PathBuf,
139 delegate: &dyn LspAdapterDelegate,
140 ) -> Result<LanguageServerBinary> {
141 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
142 let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
143 let folder_path = container_dir.join("elixir-ls");
144 let binary_path = folder_path.join("language_server.sh");
145
146 if fs::metadata(&binary_path).await.is_err() {
147 let mut response = delegate
148 .http_client()
149 .get(&version.url, Default::default(), true)
150 .await
151 .context("error downloading release")?;
152 let mut file = File::create(&zip_path)
153 .await
154 .with_context(|| format!("failed to create file {}", zip_path.display()))?;
155 if !response.status().is_success() {
156 Err(anyhow!(
157 "download failed with status {}",
158 response.status().to_string()
159 ))?;
160 }
161 futures::io::copy(response.body_mut(), &mut file).await?;
162
163 fs::create_dir_all(&folder_path)
164 .await
165 .with_context(|| format!("failed to create directory {}", folder_path.display()))?;
166 let unzip_status = smol::process::Command::new("unzip")
167 .arg(&zip_path)
168 .arg("-d")
169 .arg(&folder_path)
170 .output()
171 .await?
172 .status;
173 if !unzip_status.success() {
174 Err(anyhow!("failed to unzip elixir-ls archive"))?;
175 }
176
177 remove_matching(&container_dir, |entry| entry != folder_path).await;
178 }
179
180 Ok(LanguageServerBinary {
181 path: binary_path,
182 arguments: vec![],
183 })
184 }
185
186 async fn cached_server_binary(
187 &self,
188 container_dir: PathBuf,
189 _: &dyn LspAdapterDelegate,
190 ) -> Option<LanguageServerBinary> {
191 get_cached_server_binary_elixir_ls(container_dir).await
192 }
193
194 async fn installation_test_binary(
195 &self,
196 container_dir: PathBuf,
197 ) -> Option<LanguageServerBinary> {
198 get_cached_server_binary_elixir_ls(container_dir).await
199 }
200
201 async fn label_for_completion(
202 &self,
203 completion: &lsp::CompletionItem,
204 language: &Arc<Language>,
205 ) -> Option<CodeLabel> {
206 match completion.kind.zip(completion.detail.as_ref()) {
207 Some((_, detail)) if detail.starts_with("(function)") => {
208 let text = detail.strip_prefix("(function) ")?;
209 let filter_range = 0..text.find('(').unwrap_or(text.len());
210 let source = Rope::from(format!("def {text}").as_str());
211 let runs = language.highlight_text(&source, 4..4 + text.len());
212 return Some(CodeLabel {
213 text: text.to_string(),
214 runs,
215 filter_range,
216 });
217 }
218 Some((_, detail)) if detail.starts_with("(macro)") => {
219 let text = detail.strip_prefix("(macro) ")?;
220 let filter_range = 0..text.find('(').unwrap_or(text.len());
221 let source = Rope::from(format!("defmacro {text}").as_str());
222 let runs = language.highlight_text(&source, 9..9 + text.len());
223 return Some(CodeLabel {
224 text: text.to_string(),
225 runs,
226 filter_range,
227 });
228 }
229 Some((
230 CompletionItemKind::CLASS
231 | CompletionItemKind::MODULE
232 | CompletionItemKind::INTERFACE
233 | CompletionItemKind::STRUCT,
234 _,
235 )) => {
236 let filter_range = 0..completion
237 .label
238 .find(" (")
239 .unwrap_or(completion.label.len());
240 let text = &completion.label[filter_range.clone()];
241 let source = Rope::from(format!("defmodule {text}").as_str());
242 let runs = language.highlight_text(&source, 10..10 + text.len());
243 return Some(CodeLabel {
244 text: completion.label.clone(),
245 runs,
246 filter_range,
247 });
248 }
249 _ => {}
250 }
251
252 None
253 }
254
255 async fn label_for_symbol(
256 &self,
257 name: &str,
258 kind: SymbolKind,
259 language: &Arc<Language>,
260 ) -> Option<CodeLabel> {
261 let (text, filter_range, display_range) = match kind {
262 SymbolKind::METHOD | SymbolKind::FUNCTION => {
263 let text = format!("def {}", name);
264 let filter_range = 4..4 + name.len();
265 let display_range = 0..filter_range.end;
266 (text, filter_range, display_range)
267 }
268 SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => {
269 let text = format!("defmodule {}", name);
270 let filter_range = 10..10 + name.len();
271 let display_range = 0..filter_range.end;
272 (text, filter_range, display_range)
273 }
274 _ => return None,
275 };
276
277 Some(CodeLabel {
278 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
279 text: text[display_range].to_string(),
280 filter_range,
281 })
282 }
283}
284
285async fn get_cached_server_binary_elixir_ls(
286 container_dir: PathBuf,
287) -> Option<LanguageServerBinary> {
288 let server_path = container_dir.join("elixir-ls/language_server.sh");
289 if server_path.exists() {
290 Some(LanguageServerBinary {
291 path: server_path,
292 arguments: vec![],
293 })
294 } else {
295 log::error!("missing executable in directory {:?}", server_path);
296 None
297 }
298}
299
300pub struct NextLspAdapter;
301
302#[async_trait]
303impl LspAdapter for NextLspAdapter {
304 async fn name(&self) -> LanguageServerName {
305 LanguageServerName("next-ls".into())
306 }
307
308 fn short_name(&self) -> &'static str {
309 "next-ls"
310 }
311
312 async fn fetch_latest_server_version(
313 &self,
314 delegate: &dyn LspAdapterDelegate,
315 ) -> Result<Box<dyn 'static + Send + Any>> {
316 let release =
317 latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?;
318 let version = release.name.clone();
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 asset_name = format!("next_ls_{}", platform);
325 let asset = release
326 .assets
327 .iter()
328 .find(|asset| asset.name == asset_name)
329 .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
330 let version = GitHubLspBinaryVersion {
331 name: version,
332 url: asset.browser_download_url.clone(),
333 };
334 Ok(Box::new(version) as Box<_>)
335 }
336
337 async fn fetch_server_binary(
338 &self,
339 version: Box<dyn 'static + Send + Any>,
340 container_dir: PathBuf,
341 delegate: &dyn LspAdapterDelegate,
342 ) -> Result<LanguageServerBinary> {
343 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
344
345 let binary_path = container_dir.join("next-ls");
346
347 if fs::metadata(&binary_path).await.is_err() {
348 let mut response = delegate
349 .http_client()
350 .get(&version.url, Default::default(), true)
351 .await
352 .map_err(|err| anyhow!("error downloading release: {}", err))?;
353
354 let mut file = smol::fs::File::create(&binary_path).await?;
355 if !response.status().is_success() {
356 Err(anyhow!(
357 "download failed with status {}",
358 response.status().to_string()
359 ))?;
360 }
361 futures::io::copy(response.body_mut(), &mut file).await?;
362
363 fs::set_permissions(
364 &binary_path,
365 <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
366 )
367 .await?;
368 }
369
370 Ok(LanguageServerBinary {
371 path: binary_path,
372 arguments: vec!["--stdio".into()],
373 })
374 }
375
376 async fn cached_server_binary(
377 &self,
378 container_dir: PathBuf,
379 _: &dyn LspAdapterDelegate,
380 ) -> Option<LanguageServerBinary> {
381 get_cached_server_binary_next(container_dir)
382 .await
383 .map(|mut binary| {
384 binary.arguments = vec!["--stdio".into()];
385 binary
386 })
387 }
388
389 async fn installation_test_binary(
390 &self,
391 container_dir: PathBuf,
392 ) -> Option<LanguageServerBinary> {
393 get_cached_server_binary_next(container_dir)
394 .await
395 .map(|mut binary| {
396 binary.arguments = vec!["--help".into()];
397 binary
398 })
399 }
400
401 async fn label_for_completion(
402 &self,
403 completion: &lsp::CompletionItem,
404 language: &Arc<Language>,
405 ) -> Option<CodeLabel> {
406 label_for_completion_elixir(completion, language)
407 }
408
409 async fn label_for_symbol(
410 &self,
411 name: &str,
412 symbol_kind: SymbolKind,
413 language: &Arc<Language>,
414 ) -> Option<CodeLabel> {
415 label_for_symbol_elixir(name, symbol_kind, language)
416 }
417}
418
419async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option<LanguageServerBinary> {
420 async_maybe!({
421 let mut last_binary_path = None;
422 let mut entries = fs::read_dir(&container_dir).await?;
423 while let Some(entry) = entries.next().await {
424 let entry = entry?;
425 if entry.file_type().await?.is_file()
426 && entry
427 .file_name()
428 .to_str()
429 .map_or(false, |name| name == "next-ls")
430 {
431 last_binary_path = Some(entry.path());
432 }
433 }
434
435 if let Some(path) = last_binary_path {
436 Ok(LanguageServerBinary {
437 path,
438 arguments: Vec::new(),
439 })
440 } else {
441 Err(anyhow!("no cached binary"))
442 }
443 })
444 .await
445 .log_err()
446}
447
448pub struct LocalLspAdapter {
449 pub path: String,
450 pub arguments: Vec<String>,
451}
452
453#[async_trait]
454impl LspAdapter for LocalLspAdapter {
455 async fn name(&self) -> LanguageServerName {
456 LanguageServerName("local-ls".into())
457 }
458
459 fn short_name(&self) -> &'static str {
460 "local-ls"
461 }
462
463 async fn fetch_latest_server_version(
464 &self,
465 _: &dyn LspAdapterDelegate,
466 ) -> Result<Box<dyn 'static + Send + Any>> {
467 Ok(Box::new(()) as Box<_>)
468 }
469
470 async fn fetch_server_binary(
471 &self,
472 _: Box<dyn 'static + Send + Any>,
473 _: PathBuf,
474 _: &dyn LspAdapterDelegate,
475 ) -> Result<LanguageServerBinary> {
476 let path = shellexpand::full(&self.path)?;
477 Ok(LanguageServerBinary {
478 path: PathBuf::from(path.deref()),
479 arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
480 })
481 }
482
483 async fn cached_server_binary(
484 &self,
485 _: PathBuf,
486 _: &dyn LspAdapterDelegate,
487 ) -> Option<LanguageServerBinary> {
488 let path = shellexpand::full(&self.path).ok()?;
489 Some(LanguageServerBinary {
490 path: PathBuf::from(path.deref()),
491 arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
492 })
493 }
494
495 async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
496 let path = shellexpand::full(&self.path).ok()?;
497 Some(LanguageServerBinary {
498 path: PathBuf::from(path.deref()),
499 arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
500 })
501 }
502
503 async fn label_for_completion(
504 &self,
505 completion: &lsp::CompletionItem,
506 language: &Arc<Language>,
507 ) -> Option<CodeLabel> {
508 label_for_completion_elixir(completion, language)
509 }
510
511 async fn label_for_symbol(
512 &self,
513 name: &str,
514 symbol: SymbolKind,
515 language: &Arc<Language>,
516 ) -> Option<CodeLabel> {
517 label_for_symbol_elixir(name, symbol, language)
518 }
519}
520
521fn label_for_completion_elixir(
522 completion: &lsp::CompletionItem,
523 language: &Arc<Language>,
524) -> Option<CodeLabel> {
525 return Some(CodeLabel {
526 runs: language.highlight_text(&completion.label.clone().into(), 0..completion.label.len()),
527 text: completion.label.clone(),
528 filter_range: 0..completion.label.len(),
529 });
530}
531
532fn label_for_symbol_elixir(
533 name: &str,
534 _: SymbolKind,
535 language: &Arc<Language>,
536) -> Option<CodeLabel> {
537 Some(CodeLabel {
538 runs: language.highlight_text(&name.into(), 0..name.len()),
539 text: name.to_string(),
540 filter_range: 0..name.len(),
541 })
542}