elixir.rs

  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}