1use anyhow::{anyhow, Context, Result};
2use async_trait::async_trait;
3use futures::StreamExt;
4use gpui::{AsyncAppContext, Task};
5pub use language::*;
6use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
7use smol::fs::{self, File};
8use std::{
9 any::Any,
10 path::PathBuf,
11 sync::{
12 atomic::{AtomicBool, Ordering::SeqCst},
13 Arc,
14 },
15};
16use util::{
17 fs::remove_matching,
18 github::{latest_github_release, GitHubLspBinaryVersion},
19 ResultExt,
20};
21
22pub struct ElixirLspAdapter;
23
24#[async_trait]
25impl LspAdapter for ElixirLspAdapter {
26 async fn name(&self) -> LanguageServerName {
27 LanguageServerName("elixir-ls".into())
28 }
29
30 fn short_name(&self) -> &'static str {
31 "elixir-ls"
32 }
33
34 fn will_start_server(
35 &self,
36 delegate: &Arc<dyn LspAdapterDelegate>,
37 cx: &mut AsyncAppContext,
38 ) -> Option<Task<Result<()>>> {
39 static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
40
41 const NOTIFICATION_MESSAGE: &str = "Could not run the elixir language server, `elixir-ls`, because `elixir` was not found.";
42
43 let delegate = delegate.clone();
44 Some(cx.spawn(|mut cx| async move {
45 let elixir_output = smol::process::Command::new("elixir")
46 .args(["--version"])
47 .output()
48 .await;
49 if elixir_output.is_err() {
50 if DID_SHOW_NOTIFICATION
51 .compare_exchange(false, true, SeqCst, SeqCst)
52 .is_ok()
53 {
54 cx.update(|cx| {
55 delegate.show_notification(NOTIFICATION_MESSAGE, cx);
56 })
57 }
58 return Err(anyhow!("cannot run elixir-ls"));
59 }
60
61 Ok(())
62 }))
63 }
64
65 async fn fetch_latest_server_version(
66 &self,
67 delegate: &dyn LspAdapterDelegate,
68 ) -> Result<Box<dyn 'static + Send + Any>> {
69 let http = delegate.http_client();
70 let release = latest_github_release("elixir-lsp/elixir-ls", false, http).await?;
71 let version_name = release
72 .name
73 .strip_prefix("Release ")
74 .context("Elixir-ls release name does not start with prefix")?
75 .to_owned();
76
77 let asset_name = format!("elixir-ls-{}.zip", &version_name);
78 let asset = release
79 .assets
80 .iter()
81 .find(|asset| asset.name == asset_name)
82 .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
83
84 let version = GitHubLspBinaryVersion {
85 name: version_name,
86 url: asset.browser_download_url.clone(),
87 };
88 Ok(Box::new(version) as Box<_>)
89 }
90
91 async fn fetch_server_binary(
92 &self,
93 version: Box<dyn 'static + Send + Any>,
94 container_dir: PathBuf,
95 delegate: &dyn LspAdapterDelegate,
96 ) -> Result<LanguageServerBinary> {
97 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
98 let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
99 let version_dir = container_dir.join(format!("elixir-ls_{}", version.name));
100 let binary_path = version_dir.join("language_server.sh");
101
102 if fs::metadata(&binary_path).await.is_err() {
103 let mut response = delegate
104 .http_client()
105 .get(&version.url, Default::default(), true)
106 .await
107 .context("error downloading release")?;
108 let mut file = File::create(&zip_path)
109 .await
110 .with_context(|| format!("failed to create file {}", zip_path.display()))?;
111 if !response.status().is_success() {
112 Err(anyhow!(
113 "download failed with status {}",
114 response.status().to_string()
115 ))?;
116 }
117 futures::io::copy(response.body_mut(), &mut file).await?;
118
119 fs::create_dir_all(&version_dir)
120 .await
121 .with_context(|| format!("failed to create directory {}", version_dir.display()))?;
122 let unzip_status = smol::process::Command::new("unzip")
123 .arg(&zip_path)
124 .arg("-d")
125 .arg(&version_dir)
126 .output()
127 .await?
128 .status;
129 if !unzip_status.success() {
130 Err(anyhow!("failed to unzip elixir-ls archive"))?;
131 }
132
133 remove_matching(&container_dir, |entry| entry != version_dir).await;
134 }
135
136 Ok(LanguageServerBinary {
137 path: binary_path,
138 arguments: vec![],
139 })
140 }
141
142 async fn cached_server_binary(
143 &self,
144 container_dir: PathBuf,
145 _: &dyn LspAdapterDelegate,
146 ) -> Option<LanguageServerBinary> {
147 get_cached_server_binary(container_dir).await
148 }
149
150 async fn installation_test_binary(
151 &self,
152 container_dir: PathBuf,
153 ) -> Option<LanguageServerBinary> {
154 get_cached_server_binary(container_dir).await
155 }
156
157 async fn label_for_completion(
158 &self,
159 completion: &lsp::CompletionItem,
160 language: &Arc<Language>,
161 ) -> Option<CodeLabel> {
162 match completion.kind.zip(completion.detail.as_ref()) {
163 Some((_, detail)) if detail.starts_with("(function)") => {
164 let text = detail.strip_prefix("(function) ")?;
165 let filter_range = 0..text.find('(').unwrap_or(text.len());
166 let source = Rope::from(format!("def {text}").as_str());
167 let runs = language.highlight_text(&source, 4..4 + text.len());
168 return Some(CodeLabel {
169 text: text.to_string(),
170 runs,
171 filter_range,
172 });
173 }
174 Some((_, detail)) if detail.starts_with("(macro)") => {
175 let text = detail.strip_prefix("(macro) ")?;
176 let filter_range = 0..text.find('(').unwrap_or(text.len());
177 let source = Rope::from(format!("defmacro {text}").as_str());
178 let runs = language.highlight_text(&source, 9..9 + text.len());
179 return Some(CodeLabel {
180 text: text.to_string(),
181 runs,
182 filter_range,
183 });
184 }
185 Some((
186 CompletionItemKind::CLASS
187 | CompletionItemKind::MODULE
188 | CompletionItemKind::INTERFACE
189 | CompletionItemKind::STRUCT,
190 _,
191 )) => {
192 let filter_range = 0..completion
193 .label
194 .find(" (")
195 .unwrap_or(completion.label.len());
196 let text = &completion.label[filter_range.clone()];
197 let source = Rope::from(format!("defmodule {text}").as_str());
198 let runs = language.highlight_text(&source, 10..10 + text.len());
199 return Some(CodeLabel {
200 text: completion.label.clone(),
201 runs,
202 filter_range,
203 });
204 }
205 _ => {}
206 }
207
208 None
209 }
210
211 async fn label_for_symbol(
212 &self,
213 name: &str,
214 kind: SymbolKind,
215 language: &Arc<Language>,
216 ) -> Option<CodeLabel> {
217 let (text, filter_range, display_range) = match kind {
218 SymbolKind::METHOD | SymbolKind::FUNCTION => {
219 let text = format!("def {}", name);
220 let filter_range = 4..4 + name.len();
221 let display_range = 0..filter_range.end;
222 (text, filter_range, display_range)
223 }
224 SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => {
225 let text = format!("defmodule {}", name);
226 let filter_range = 10..10 + name.len();
227 let display_range = 0..filter_range.end;
228 (text, filter_range, display_range)
229 }
230 _ => return None,
231 };
232
233 Some(CodeLabel {
234 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
235 text: text[display_range].to_string(),
236 filter_range,
237 })
238 }
239}
240
241async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
242 (|| async move {
243 let mut last = None;
244 let mut entries = fs::read_dir(&container_dir).await?;
245 while let Some(entry) = entries.next().await {
246 last = Some(entry?.path());
247 }
248 last.map(|path| LanguageServerBinary {
249 path,
250 arguments: vec![],
251 })
252 .ok_or_else(|| anyhow!("no cached binary"))
253 })()
254 .await
255 .log_err()
256}