1use anyhow::{anyhow, Context, Result};
2use async_trait::async_trait;
3use futures::StreamExt;
4pub use language::*;
5use lsp::{CompletionItemKind, SymbolKind};
6use smol::fs::{self, File};
7use std::{any::Any, path::PathBuf, sync::Arc};
8use util::fs::remove_matching;
9use util::github::latest_github_release;
10use util::http::HttpClient;
11use util::ResultExt;
12
13use super::github::GitHubLspBinaryVersion;
14
15pub struct ElixirLspAdapter;
16
17#[async_trait]
18impl LspAdapter for ElixirLspAdapter {
19 async fn name(&self) -> LanguageServerName {
20 LanguageServerName("elixir-ls".into())
21 }
22
23 async fn fetch_latest_server_version(
24 &self,
25 http: Arc<dyn HttpClient>,
26 ) -> Result<Box<dyn 'static + Send + Any>> {
27 let release = latest_github_release("elixir-lsp/elixir-ls", http).await?;
28 let asset_name = "elixir-ls.zip";
29 let asset = release
30 .assets
31 .iter()
32 .find(|asset| asset.name == asset_name)
33 .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
34 let version = GitHubLspBinaryVersion {
35 name: release.name,
36 url: asset.browser_download_url.clone(),
37 };
38 Ok(Box::new(version) as Box<_>)
39 }
40
41 async fn fetch_server_binary(
42 &self,
43 version: Box<dyn 'static + Send + Any>,
44 http: Arc<dyn HttpClient>,
45 container_dir: PathBuf,
46 ) -> Result<LanguageServerBinary> {
47 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
48 let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
49 let version_dir = container_dir.join(format!("elixir-ls_{}", version.name));
50 let binary_path = version_dir.join("language_server.sh");
51
52 if fs::metadata(&binary_path).await.is_err() {
53 let mut response = http
54 .get(&version.url, Default::default(), true)
55 .await
56 .context("error downloading release")?;
57 let mut file = File::create(&zip_path)
58 .await
59 .with_context(|| format!("failed to create file {}", zip_path.display()))?;
60 if !response.status().is_success() {
61 Err(anyhow!(
62 "download failed with status {}",
63 response.status().to_string()
64 ))?;
65 }
66 futures::io::copy(response.body_mut(), &mut file).await?;
67
68 fs::create_dir_all(&version_dir)
69 .await
70 .with_context(|| format!("failed to create directory {}", version_dir.display()))?;
71 let unzip_status = smol::process::Command::new("unzip")
72 .arg(&zip_path)
73 .arg("-d")
74 .arg(&version_dir)
75 .output()
76 .await?
77 .status;
78 if !unzip_status.success() {
79 Err(anyhow!("failed to unzip clangd archive"))?;
80 }
81
82 remove_matching(&container_dir, |entry| entry != version_dir).await;
83 }
84
85 Ok(LanguageServerBinary {
86 path: binary_path,
87 arguments: vec![],
88 })
89 }
90
91 async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
92 (|| async move {
93 let mut last = None;
94 let mut entries = fs::read_dir(&container_dir).await?;
95 while let Some(entry) = entries.next().await {
96 last = Some(entry?.path());
97 }
98 last.map(|path| LanguageServerBinary {
99 path,
100 arguments: vec![],
101 })
102 .ok_or_else(|| anyhow!("no cached binary"))
103 })()
104 .await
105 .log_err()
106 }
107
108 async fn label_for_completion(
109 &self,
110 completion: &lsp::CompletionItem,
111 language: &Arc<Language>,
112 ) -> Option<CodeLabel> {
113 match completion.kind.zip(completion.detail.as_ref()) {
114 Some((_, detail)) if detail.starts_with("(function)") => {
115 let text = detail.strip_prefix("(function) ")?;
116 let filter_range = 0..text.find('(').unwrap_or(text.len());
117 let source = Rope::from(format!("def {text}").as_str());
118 let runs = language.highlight_text(&source, 4..4 + text.len());
119 return Some(CodeLabel {
120 text: text.to_string(),
121 runs,
122 filter_range,
123 });
124 }
125 Some((_, detail)) if detail.starts_with("(macro)") => {
126 let text = detail.strip_prefix("(macro) ")?;
127 let filter_range = 0..text.find('(').unwrap_or(text.len());
128 let source = Rope::from(format!("defmacro {text}").as_str());
129 let runs = language.highlight_text(&source, 9..9 + text.len());
130 return Some(CodeLabel {
131 text: text.to_string(),
132 runs,
133 filter_range,
134 });
135 }
136 Some((
137 CompletionItemKind::CLASS
138 | CompletionItemKind::MODULE
139 | CompletionItemKind::INTERFACE
140 | CompletionItemKind::STRUCT,
141 _,
142 )) => {
143 let filter_range = 0..completion
144 .label
145 .find(" (")
146 .unwrap_or(completion.label.len());
147 let text = &completion.label[filter_range.clone()];
148 let source = Rope::from(format!("defmodule {text}").as_str());
149 let runs = language.highlight_text(&source, 10..10 + text.len());
150 return Some(CodeLabel {
151 text: completion.label.clone(),
152 runs,
153 filter_range,
154 });
155 }
156 _ => {}
157 }
158
159 None
160 }
161
162 async fn label_for_symbol(
163 &self,
164 name: &str,
165 kind: SymbolKind,
166 language: &Arc<Language>,
167 ) -> Option<CodeLabel> {
168 let (text, filter_range, display_range) = match kind {
169 SymbolKind::METHOD | SymbolKind::FUNCTION => {
170 let text = format!("def {}", name);
171 let filter_range = 4..4 + name.len();
172 let display_range = 0..filter_range.end;
173 (text, filter_range, display_range)
174 }
175 SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => {
176 let text = format!("defmodule {}", name);
177 let filter_range = 10..10 + name.len();
178 let display_range = 0..filter_range.end;
179 (text, filter_range, display_range)
180 }
181 _ => return None,
182 };
183
184 Some(CodeLabel {
185 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
186 text: text[display_range].to_string(),
187 filter_range,
188 })
189 }
190}