1use std::fs;
2
3use zed::lsp::{Completion, CompletionKind, Symbol, SymbolKind};
4use zed::settings::LspSettings;
5use zed::{serde_json, CodeLabel, CodeLabelSpan, LanguageServerId};
6use zed_extension_api::{self as zed, Result};
7
8pub struct ElixirLs {
9 cached_binary_path: Option<String>,
10}
11
12impl ElixirLs {
13 pub const LANGUAGE_SERVER_ID: &'static str = "elixir-ls";
14
15 pub fn new() -> Self {
16 Self {
17 cached_binary_path: None,
18 }
19 }
20
21 pub fn language_server_binary_path(
22 &mut self,
23 language_server_id: &LanguageServerId,
24 worktree: &zed::Worktree,
25 ) -> Result<String> {
26 if let Some(path) = worktree.which("elixir-ls") {
27 return Ok(path);
28 }
29
30 if let Some(path) = &self.cached_binary_path {
31 if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
32 return Ok(path.clone());
33 }
34 }
35
36 zed::set_language_server_installation_status(
37 language_server_id,
38 &zed::LanguageServerInstallationStatus::CheckingForUpdate,
39 );
40 let release = zed::latest_github_release(
41 "elixir-lsp/elixir-ls",
42 zed::GithubReleaseOptions {
43 require_assets: true,
44 pre_release: false,
45 },
46 )?;
47
48 let asset_name = format!("elixir-ls-{version}.zip", version = release.version,);
49
50 let asset = release
51 .assets
52 .iter()
53 .find(|asset| asset.name == asset_name)
54 .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
55
56 let (platform, _arch) = zed::current_platform();
57 let version_dir = format!("elixir-ls-{}", release.version);
58 let extension = match platform {
59 zed::Os::Mac | zed::Os::Linux => "sh",
60 zed::Os::Windows => "bat",
61 };
62 let binary_path = format!("{version_dir}/language_server.{extension}");
63
64 if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
65 zed::set_language_server_installation_status(
66 language_server_id,
67 &zed::LanguageServerInstallationStatus::Downloading,
68 );
69
70 zed::download_file(
71 &asset.download_url,
72 &version_dir,
73 zed::DownloadedFileType::Zip,
74 )
75 .map_err(|e| format!("failed to download file: {e}"))?;
76
77 zed::make_file_executable(&binary_path)?;
78 zed::make_file_executable(&format!("{version_dir}/launch.{extension}"))?;
79 zed::make_file_executable(&format!("{version_dir}/debug_adapter.{extension}"))?;
80
81 let entries =
82 fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
83 for entry in entries {
84 let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
85 if entry.file_name().to_str() != Some(&version_dir) {
86 fs::remove_dir_all(entry.path()).ok();
87 }
88 }
89 }
90
91 self.cached_binary_path = Some(binary_path.clone());
92 Ok(binary_path)
93 }
94
95 pub fn language_server_workspace_configuration(
96 &mut self,
97 worktree: &zed::Worktree,
98 ) -> Result<Option<serde_json::Value>> {
99 let settings = LspSettings::for_worktree("elixir-ls", worktree)
100 .ok()
101 .and_then(|lsp_settings| lsp_settings.settings.clone())
102 .unwrap_or_default();
103
104 Ok(Some(serde_json::json!({
105 "elixirLS": settings
106 })))
107 }
108
109 pub fn label_for_completion(&self, completion: Completion) -> Option<CodeLabel> {
110 let name = &completion.label;
111 let detail = completion
112 .detail
113 .filter(|detail| detail != "alias")
114 .map(|detail| format!(": {detail}"))
115 .unwrap_or("".to_string());
116
117 let detail_span = CodeLabelSpan::literal(detail, Some("comment.unused".to_string()));
118
119 match completion.kind? {
120 CompletionKind::Module | CompletionKind::Class | CompletionKind::Struct => {
121 let defmodule = "defmodule ";
122 let alias = completion
123 .label_details
124 .and_then(|details| details.description)
125 .filter(|description| description.starts_with("alias"))
126 .map(|description| format!(" ({description})"))
127 .unwrap_or("".to_string());
128
129 let code = format!("{defmodule}{name}{alias}");
130 let name_start = defmodule.len();
131 let name_end = name_start + name.len();
132
133 Some(CodeLabel {
134 code,
135 spans: vec![
136 CodeLabelSpan::code_range(name_start..name_end),
137 detail_span,
138 CodeLabelSpan::code_range(name_end..(name_end + alias.len())),
139 ],
140 filter_range: (0..name.len()).into(),
141 })
142 }
143 CompletionKind::Interface => Some(CodeLabel {
144 code: name.to_string(),
145 spans: vec![CodeLabelSpan::code_range(0..name.len()), detail_span],
146 filter_range: (0..name.len()).into(),
147 }),
148 CompletionKind::Field => Some(CodeLabel {
149 code: name.to_string(),
150 spans: vec![
151 CodeLabelSpan::literal(name, Some("function".to_string())),
152 detail_span,
153 ],
154 filter_range: (0..name.len()).into(),
155 }),
156 CompletionKind::Function | CompletionKind::Constant => {
157 let detail = completion
158 .label_details
159 .clone()
160 .and_then(|details| details.detail)
161 .unwrap_or("".to_string());
162
163 let description = completion
164 .label_details
165 .clone()
166 .and_then(|details| details.description)
167 .map(|description| format!(" ({description})"))
168 .unwrap_or("".to_string());
169
170 let def = "def ";
171 let code = format!("{def}{name}{detail}{description}");
172
173 let name_start = def.len();
174 let name_end = name_start + name.len();
175 let detail_end = name_end + detail.len();
176 let description_end = detail_end + description.len();
177
178 Some(CodeLabel {
179 code,
180 spans: vec![
181 CodeLabelSpan::code_range(name_start..name_end),
182 CodeLabelSpan::code_range(name_end..detail_end),
183 CodeLabelSpan::code_range(detail_end..description_end),
184 ],
185 filter_range: (0..name.len()).into(),
186 })
187 }
188 CompletionKind::Operator => {
189 let def_a = "def a ";
190 let code = format!("{def_a}{name} b");
191
192 Some(CodeLabel {
193 code,
194 spans: vec![CodeLabelSpan::code_range(
195 def_a.len()..def_a.len() + name.len(),
196 )],
197 filter_range: (0..name.len()).into(),
198 })
199 }
200 _ => None,
201 }
202 }
203
204 pub fn label_for_symbol(&self, symbol: Symbol) -> Option<CodeLabel> {
205 let name = &symbol.name;
206
207 let (code, filter_range, display_range) = match symbol.kind {
208 SymbolKind::Module | SymbolKind::Class | SymbolKind::Interface | SymbolKind::Struct => {
209 let defmodule = "defmodule ";
210 let code = format!("{defmodule}{name}");
211 let filter_range = 0..name.len();
212 let display_range = defmodule.len()..defmodule.len() + name.len();
213 (code, filter_range, display_range)
214 }
215 SymbolKind::Function | SymbolKind::Constant => {
216 let def = "def ";
217 let code = format!("{def}{name}");
218 let filter_range = 0..name.len();
219 let display_range = def.len()..def.len() + name.len();
220 (code, filter_range, display_range)
221 }
222 _ => return None,
223 };
224
225 Some(CodeLabel {
226 spans: vec![CodeLabelSpan::code_range(display_range)],
227 filter_range: filter_range.into(),
228 code,
229 })
230 }
231}