elixir.rs

  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}