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 binary_path = format!(
59 "{version_dir}/language_server.{extension}",
60 extension = match platform {
61 zed::Os::Mac | zed::Os::Linux => "sh",
62 zed::Os::Windows => "bat",
63 }
64 );
65
66 if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
67 zed::set_language_server_installation_status(
68 &language_server_id,
69 &zed::LanguageServerInstallationStatus::Downloading,
70 );
71
72 zed::download_file(
73 &asset.download_url,
74 &version_dir,
75 zed::DownloadedFileType::Zip,
76 )
77 .map_err(|e| format!("failed to download file: {e}"))?;
78
79 let entries =
80 fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
81 for entry in entries {
82 let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
83 if entry.file_name().to_str() != Some(&version_dir) {
84 fs::remove_dir_all(&entry.path()).ok();
85 }
86 }
87 }
88
89 self.cached_binary_path = Some(binary_path.clone());
90 Ok(binary_path)
91 }
92
93 pub fn language_server_workspace_configuration(
94 &mut self,
95 worktree: &zed::Worktree,
96 ) -> Result<Option<serde_json::Value>> {
97 let settings = LspSettings::for_worktree("elixir-ls", worktree)
98 .ok()
99 .and_then(|lsp_settings| lsp_settings.settings.clone())
100 .unwrap_or_default();
101
102 Ok(Some(serde_json::json!({
103 "elixirLS": settings
104 })))
105 }
106
107 pub fn label_for_completion(&self, completion: Completion) -> Option<CodeLabel> {
108 match completion.kind? {
109 CompletionKind::Module
110 | CompletionKind::Class
111 | CompletionKind::Interface
112 | CompletionKind::Struct => {
113 let name = completion.label;
114 let defmodule = "defmodule ";
115 let code = format!("{defmodule}{name}");
116
117 Some(CodeLabel {
118 code,
119 spans: vec![CodeLabelSpan::code_range(
120 defmodule.len()..defmodule.len() + name.len(),
121 )],
122 filter_range: (0..name.len()).into(),
123 })
124 }
125 CompletionKind::Function | CompletionKind::Constant => {
126 let name = completion.label;
127 let def = "def ";
128 let code = format!("{def}{name}");
129
130 Some(CodeLabel {
131 code,
132 spans: vec![CodeLabelSpan::code_range(def.len()..def.len() + name.len())],
133 filter_range: (0..name.len()).into(),
134 })
135 }
136 CompletionKind::Operator => {
137 let name = completion.label;
138 let def_a = "def a ";
139 let code = format!("{def_a}{name} b");
140
141 Some(CodeLabel {
142 code,
143 spans: vec![CodeLabelSpan::code_range(
144 def_a.len()..def_a.len() + name.len(),
145 )],
146 filter_range: (0..name.len()).into(),
147 })
148 }
149 _ => None,
150 }
151 }
152
153 pub fn label_for_symbol(&self, symbol: Symbol) -> Option<CodeLabel> {
154 let name = &symbol.name;
155
156 let (code, filter_range, display_range) = match symbol.kind {
157 SymbolKind::Module | SymbolKind::Class | SymbolKind::Interface | SymbolKind::Struct => {
158 let defmodule = "defmodule ";
159 let code = format!("{defmodule}{name}");
160 let filter_range = 0..name.len();
161 let display_range = defmodule.len()..defmodule.len() + name.len();
162 (code, filter_range, display_range)
163 }
164 SymbolKind::Function | SymbolKind::Constant => {
165 let def = "def ";
166 let code = format!("{def}{name}");
167 let filter_range = 0..name.len();
168 let display_range = def.len()..def.len() + name.len();
169 (code, filter_range, display_range)
170 }
171 _ => return None,
172 };
173
174 Some(CodeLabel {
175 spans: vec![CodeLabelSpan::code_range(display_range)],
176 filter_range: filter_range.into(),
177 code,
178 })
179 }
180}