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