1use std::fs;
2use zed_extension_api::{self as zed, serde_json, settings::LspSettings, LanguageServerId, Result};
3
4struct ZigExtension {
5 cached_binary_path: Option<String>,
6}
7
8#[derive(Clone)]
9struct ZlsBinary {
10 path: String,
11 args: Option<Vec<String>>,
12 environment: Option<Vec<(String, String)>>,
13}
14
15impl ZigExtension {
16 fn language_server_binary(
17 &mut self,
18 language_server_id: &LanguageServerId,
19 worktree: &zed::Worktree,
20 ) -> Result<ZlsBinary> {
21 let mut args: Option<Vec<String>> = None;
22
23 let (platform, arch) = zed::current_platform();
24 let environment = match platform {
25 zed::Os::Mac | zed::Os::Linux => Some(worktree.shell_env()),
26 zed::Os::Windows => None,
27 };
28
29 if let Ok(lsp_settings) = LspSettings::for_worktree("zls", worktree) {
30 if let Some(binary) = lsp_settings.binary {
31 args = binary.arguments;
32 if let Some(path) = binary.path {
33 return Ok(ZlsBinary {
34 path: path.clone(),
35 args,
36 environment,
37 });
38 }
39 }
40 }
41
42 if let Some(path) = worktree.which("zls") {
43 return Ok(ZlsBinary {
44 path,
45 args,
46 environment,
47 });
48 }
49
50 if let Some(path) = &self.cached_binary_path {
51 if fs::metadata(&path).map_or(false, |stat| stat.is_file()) {
52 return Ok(ZlsBinary {
53 path: path.clone(),
54 args,
55 environment,
56 });
57 }
58 }
59
60 zed::set_language_server_installation_status(
61 &language_server_id,
62 &zed::LanguageServerInstallationStatus::CheckingForUpdate,
63 );
64 // TODO: Once we're ready to release v0.0.7 of the Zed extension API we want to pin
65 // ZLS to a specific version with `zed::github_release_by_tag_name`.
66 // We're pinning ZLS to a release that has `.tar.gz` assets, since the latest release does not have
67 // them, at time of writing.
68 //
69 // ZLS tracking issue: https://github.com/zigtools/zls/issues/1879
70 // let release = zed::github_release_by_tag_name("zigtools/zls", "0.11.0")?;
71
72 let release = zed::latest_github_release(
73 "zigtools/zls",
74 zed::GithubReleaseOptions {
75 require_assets: true,
76 pre_release: false,
77 },
78 )?;
79
80 let asset_name = format!(
81 "zls-{arch}-{os}.{extension}",
82 arch = match arch {
83 zed::Architecture::Aarch64 => "aarch64",
84 zed::Architecture::X86 => "x86",
85 zed::Architecture::X8664 => "x86_64",
86 },
87 os = match platform {
88 zed::Os::Mac => "macos",
89 zed::Os::Linux => "linux",
90 zed::Os::Windows => "windows",
91 },
92 extension = match platform {
93 zed::Os::Mac | zed::Os::Linux => "tar.gz",
94 zed::Os::Windows => "zip",
95 }
96 );
97
98 let asset = release
99 .assets
100 .iter()
101 .find(|asset| asset.name == asset_name)
102 .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
103
104 let version_dir = format!("zls-{}", release.version);
105 let binary_path = match platform {
106 zed::Os::Mac | zed::Os::Linux => format!("{version_dir}/bin/zls"),
107 zed::Os::Windows => format!("{version_dir}/zls.exe"),
108 };
109
110 if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
111 zed::set_language_server_installation_status(
112 &language_server_id,
113 &zed::LanguageServerInstallationStatus::Downloading,
114 );
115
116 zed::download_file(
117 &asset.download_url,
118 &version_dir,
119 match platform {
120 zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::GzipTar,
121 zed::Os::Windows => zed::DownloadedFileType::Zip,
122 },
123 )
124 .map_err(|e| format!("failed to download file: {e}"))?;
125
126 zed::make_file_executable(&binary_path)?;
127
128 let entries =
129 fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
130 for entry in entries {
131 let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
132 if entry.file_name().to_str() != Some(&version_dir) {
133 fs::remove_dir_all(&entry.path()).ok();
134 }
135 }
136 }
137
138 self.cached_binary_path = Some(binary_path.clone());
139 Ok(ZlsBinary {
140 path: binary_path,
141 args,
142 environment,
143 })
144 }
145}
146
147impl zed::Extension for ZigExtension {
148 fn new() -> Self {
149 Self {
150 cached_binary_path: None,
151 }
152 }
153
154 fn language_server_command(
155 &mut self,
156 language_server_id: &LanguageServerId,
157 worktree: &zed::Worktree,
158 ) -> Result<zed::Command> {
159 let zls_binary = self.language_server_binary(language_server_id, worktree)?;
160 Ok(zed::Command {
161 command: zls_binary.path,
162 args: zls_binary.args.unwrap_or_default(),
163 env: zls_binary.environment.unwrap_or_default(),
164 })
165 }
166
167 fn language_server_workspace_configuration(
168 &mut self,
169 _language_server_id: &zed::LanguageServerId,
170 worktree: &zed::Worktree,
171 ) -> Result<Option<serde_json::Value>> {
172 let settings = LspSettings::for_worktree("zls", worktree)
173 .ok()
174 .and_then(|lsp_settings| lsp_settings.settings.clone())
175 .unwrap_or_default();
176 Ok(Some(settings))
177 }
178}
179
180zed::register_extension!(ZigExtension);