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 match completion.kind? {
111 CompletionKind::Module
112 | CompletionKind::Class
113 | CompletionKind::Interface
114 | CompletionKind::Struct => {
115 let name = completion.label;
116 let defmodule = "defmodule ";
117 let code = format!("{defmodule}{name}");
118
119 Some(CodeLabel {
120 code,
121 spans: vec![CodeLabelSpan::code_range(
122 defmodule.len()..defmodule.len() + name.len(),
123 )],
124 filter_range: (0..name.len()).into(),
125 })
126 }
127 CompletionKind::Function | CompletionKind::Constant => {
128 let name = completion.label;
129 let def = "def ";
130 let code = format!("{def}{name}");
131
132 Some(CodeLabel {
133 code,
134 spans: vec![CodeLabelSpan::code_range(def.len()..def.len() + name.len())],
135 filter_range: (0..name.len()).into(),
136 })
137 }
138 CompletionKind::Operator => {
139 let name = completion.label;
140 let def_a = "def a ";
141 let code = format!("{def_a}{name} b");
142
143 Some(CodeLabel {
144 code,
145 spans: vec![CodeLabelSpan::code_range(
146 def_a.len()..def_a.len() + name.len(),
147 )],
148 filter_range: (0..name.len()).into(),
149 })
150 }
151 _ => None,
152 }
153 }
154
155 pub fn label_for_symbol(&self, symbol: Symbol) -> Option<CodeLabel> {
156 let name = &symbol.name;
157
158 let (code, filter_range, display_range) = match symbol.kind {
159 SymbolKind::Module | SymbolKind::Class | SymbolKind::Interface | SymbolKind::Struct => {
160 let defmodule = "defmodule ";
161 let code = format!("{defmodule}{name}");
162 let filter_range = 0..name.len();
163 let display_range = defmodule.len()..defmodule.len() + name.len();
164 (code, filter_range, display_range)
165 }
166 SymbolKind::Function | SymbolKind::Constant => {
167 let def = "def ";
168 let code = format!("{def}{name}");
169 let filter_range = 0..name.len();
170 let display_range = def.len()..def.len() + name.len();
171 (code, filter_range, display_range)
172 }
173 _ => return None,
174 };
175
176 Some(CodeLabel {
177 spans: vec![CodeLabelSpan::code_range(display_range)],
178 filter_range: filter_range.into(),
179 code,
180 })
181 }
182}