1use std::fs;
2
3use zed::lsp::{Completion, CompletionKind, Symbol, SymbolKind};
4use zed::{CodeLabel, CodeLabelSpan, LanguageServerId};
5use zed_extension_api::{self as zed, Result};
6
7pub struct ElixirLs {
8 cached_binary_path: Option<String>,
9}
10
11impl ElixirLs {
12 pub const LANGUAGE_SERVER_ID: &'static str = "elixir-ls";
13
14 pub fn new() -> Self {
15 Self {
16 cached_binary_path: None,
17 }
18 }
19
20 pub fn language_server_binary_path(
21 &mut self,
22 language_server_id: &LanguageServerId,
23 worktree: &zed::Worktree,
24 ) -> Result<String> {
25 if let Some(path) = worktree.which("elixir-ls") {
26 return Ok(path);
27 }
28
29 if let Some(path) = &self.cached_binary_path {
30 if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
31 return Ok(path.clone());
32 }
33 }
34
35 zed::set_language_server_installation_status(
36 &language_server_id,
37 &zed::LanguageServerInstallationStatus::CheckingForUpdate,
38 );
39 let release = zed::latest_github_release(
40 "elixir-lsp/elixir-ls",
41 zed::GithubReleaseOptions {
42 require_assets: true,
43 pre_release: false,
44 },
45 )?;
46
47 let asset_name = format!("elixir-ls-{version}.zip", version = release.version,);
48
49 let asset = release
50 .assets
51 .iter()
52 .find(|asset| asset.name == asset_name)
53 .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
54
55 let (platform, _arch) = zed::current_platform();
56 let version_dir = format!("elixir-ls-{}", release.version);
57 let binary_path = format!(
58 "{version_dir}/language_server.{extension}",
59 extension = match platform {
60 zed::Os::Mac | zed::Os::Linux => "sh",
61 zed::Os::Windows => "bat",
62 }
63 );
64
65 if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
66 zed::set_language_server_installation_status(
67 &language_server_id,
68 &zed::LanguageServerInstallationStatus::Downloading,
69 );
70
71 zed::download_file(
72 &asset.download_url,
73 &version_dir,
74 zed::DownloadedFileType::Zip,
75 )
76 .map_err(|e| format!("failed to download file: {e}"))?;
77
78 let entries =
79 fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
80 for entry in entries {
81 let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
82 if entry.file_name().to_str() != Some(&version_dir) {
83 fs::remove_dir_all(&entry.path()).ok();
84 }
85 }
86 }
87
88 self.cached_binary_path = Some(binary_path.clone());
89 Ok(binary_path)
90 }
91
92 pub fn label_for_completion(&self, completion: Completion) -> Option<CodeLabel> {
93 match completion.kind? {
94 CompletionKind::Module
95 | CompletionKind::Class
96 | CompletionKind::Interface
97 | CompletionKind::Struct => {
98 let name = completion.label;
99 let defmodule = "defmodule ";
100 let code = format!("{defmodule}{name}");
101
102 Some(CodeLabel {
103 code,
104 spans: vec![CodeLabelSpan::code_range(
105 defmodule.len()..defmodule.len() + name.len(),
106 )],
107 filter_range: (0..name.len()).into(),
108 })
109 }
110 CompletionKind::Function | CompletionKind::Constant => {
111 let name = completion.label;
112 let def = "def ";
113 let code = format!("{def}{name}");
114
115 Some(CodeLabel {
116 code,
117 spans: vec![CodeLabelSpan::code_range(def.len()..def.len() + name.len())],
118 filter_range: (0..name.len()).into(),
119 })
120 }
121 CompletionKind::Operator => {
122 let name = completion.label;
123 let def_a = "def a ";
124 let code = format!("{def_a}{name} b");
125
126 Some(CodeLabel {
127 code,
128 spans: vec![CodeLabelSpan::code_range(
129 def_a.len()..def_a.len() + name.len(),
130 )],
131 filter_range: (0..name.len()).into(),
132 })
133 }
134 _ => None,
135 }
136 }
137
138 pub fn label_for_symbol(&self, symbol: Symbol) -> Option<CodeLabel> {
139 let name = &symbol.name;
140
141 let (code, filter_range, display_range) = match symbol.kind {
142 SymbolKind::Module | SymbolKind::Class | SymbolKind::Interface | SymbolKind::Struct => {
143 let defmodule = "defmodule ";
144 let code = format!("{defmodule}{name}");
145 let filter_range = 0..name.len();
146 let display_range = defmodule.len()..defmodule.len() + name.len();
147 (code, filter_range, display_range)
148 }
149 SymbolKind::Function | SymbolKind::Constant => {
150 let def = "def ";
151 let code = format!("{def}{name}");
152 let filter_range = 0..name.len();
153 let display_range = def.len()..def.len() + name.len();
154 (code, filter_range, display_range)
155 }
156 _ => return None,
157 };
158
159 Some(CodeLabel {
160 spans: vec![CodeLabelSpan::code_range(display_range)],
161 filter_range: filter_range.into(),
162 code,
163 })
164 }
165}