1use anyhow::{anyhow, bail, Context, Result};
2use async_trait::async_trait;
3use futures::StreamExt;
4use gpui2::{AsyncAppContext, Task};
5pub use language2::*;
6use lsp2::{CompletionItemKind, LanguageServerBinary, SymbolKind};
7use schemars::JsonSchema;
8use serde_derive::{Deserialize, Serialize};
9use settings2::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 gpui2::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 version_dir = container_dir.join(format!("elixir-ls_{}", version.name));
144 let binary_path = version_dir.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(&version_dir)
164 .await
165 .with_context(|| format!("failed to create directory {}", version_dir.display()))?;
166 let unzip_status = smol::process::Command::new("unzip")
167 .arg(&zip_path)
168 .arg("-d")
169 .arg(&version_dir)
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 != version_dir).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: &lsp2::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 (|| async move {
289 let mut last = None;
290 let mut entries = fs::read_dir(&container_dir).await?;
291 while let Some(entry) = entries.next().await {
292 last = Some(entry?.path());
293 }
294 last.map(|path| LanguageServerBinary {
295 path,
296 arguments: vec![],
297 })
298 .ok_or_else(|| anyhow!("no cached binary"))
299 })()
300 .await
301 .log_err()
302}
303
304pub struct NextLspAdapter;
305
306#[async_trait]
307impl LspAdapter for NextLspAdapter {
308 async fn name(&self) -> LanguageServerName {
309 LanguageServerName("next-ls".into())
310 }
311
312 fn short_name(&self) -> &'static str {
313 "next-ls"
314 }
315
316 async fn fetch_latest_server_version(
317 &self,
318 delegate: &dyn LspAdapterDelegate,
319 ) -> Result<Box<dyn 'static + Send + Any>> {
320 let release =
321 latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?;
322 let version = release.name.clone();
323 let platform = match consts::ARCH {
324 "x86_64" => "darwin_amd64",
325 "aarch64" => "darwin_arm64",
326 other => bail!("Running on unsupported platform: {other}"),
327 };
328 let asset_name = format!("next_ls_{}", platform);
329 let asset = release
330 .assets
331 .iter()
332 .find(|asset| asset.name == asset_name)
333 .ok_or_else(|| anyhow!("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 fs::set_permissions(
368 &binary_path,
369 <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
370 )
371 .await?;
372 }
373
374 Ok(LanguageServerBinary {
375 path: binary_path,
376 arguments: vec!["--stdio".into()],
377 })
378 }
379
380 async fn cached_server_binary(
381 &self,
382 container_dir: PathBuf,
383 _: &dyn LspAdapterDelegate,
384 ) -> Option<LanguageServerBinary> {
385 get_cached_server_binary_next(container_dir)
386 .await
387 .map(|mut binary| {
388 binary.arguments = vec!["--stdio".into()];
389 binary
390 })
391 }
392
393 async fn installation_test_binary(
394 &self,
395 container_dir: PathBuf,
396 ) -> Option<LanguageServerBinary> {
397 get_cached_server_binary_next(container_dir)
398 .await
399 .map(|mut binary| {
400 binary.arguments = vec!["--help".into()];
401 binary
402 })
403 }
404
405 async fn label_for_completion(
406 &self,
407 completion: &lsp2::CompletionItem,
408 language: &Arc<Language>,
409 ) -> Option<CodeLabel> {
410 label_for_completion_elixir(completion, language)
411 }
412
413 async fn label_for_symbol(
414 &self,
415 name: &str,
416 symbol_kind: SymbolKind,
417 language: &Arc<Language>,
418 ) -> Option<CodeLabel> {
419 label_for_symbol_elixir(name, symbol_kind, language)
420 }
421}
422
423async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option<LanguageServerBinary> {
424 async_maybe!({
425 let mut last_binary_path = None;
426 let mut entries = fs::read_dir(&container_dir).await?;
427 while let Some(entry) = entries.next().await {
428 let entry = entry?;
429 if entry.file_type().await?.is_file()
430 && entry
431 .file_name()
432 .to_str()
433 .map_or(false, |name| name == "next-ls")
434 {
435 last_binary_path = Some(entry.path());
436 }
437 }
438
439 if let Some(path) = last_binary_path {
440 Ok(LanguageServerBinary {
441 path,
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 async 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 arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
484 })
485 }
486
487 async fn cached_server_binary(
488 &self,
489 _: PathBuf,
490 _: &dyn LspAdapterDelegate,
491 ) -> Option<LanguageServerBinary> {
492 let path = shellexpand::full(&self.path).ok()?;
493 Some(LanguageServerBinary {
494 path: PathBuf::from(path.deref()),
495 arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
496 })
497 }
498
499 async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
500 let path = shellexpand::full(&self.path).ok()?;
501 Some(LanguageServerBinary {
502 path: PathBuf::from(path.deref()),
503 arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
504 })
505 }
506
507 async fn label_for_completion(
508 &self,
509 completion: &lsp2::CompletionItem,
510 language: &Arc<Language>,
511 ) -> Option<CodeLabel> {
512 label_for_completion_elixir(completion, language)
513 }
514
515 async fn label_for_symbol(
516 &self,
517 name: &str,
518 symbol: SymbolKind,
519 language: &Arc<Language>,
520 ) -> Option<CodeLabel> {
521 label_for_symbol_elixir(name, symbol, language)
522 }
523}
524
525fn label_for_completion_elixir(
526 completion: &lsp2::CompletionItem,
527 language: &Arc<Language>,
528) -> Option<CodeLabel> {
529 return Some(CodeLabel {
530 runs: language.highlight_text(&completion.label.clone().into(), 0..completion.label.len()),
531 text: completion.label.clone(),
532 filter_range: 0..completion.label.len(),
533 });
534}
535
536fn label_for_symbol_elixir(
537 name: &str,
538 _: SymbolKind,
539 language: &Arc<Language>,
540) -> Option<CodeLabel> {
541 Some(CodeLabel {
542 runs: language.highlight_text(&name.into(), 0..name.len()),
543 text: name.to_string(),
544 filter_range: 0..name.len(),
545 })
546}