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