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 // We're pinning ZLS to a release that has `.tar.gz` assets, since the latest release does not have
65 // them, at time of writing.
66 //
67 // ZLS tracking issue: https://github.com/zigtools/zls/issues/1879
68 let release = zed::github_release_by_tag_name("zigtools/zls", "0.11.0")?;
69
70 let asset_name = format!(
71 "zls-{arch}-{os}.{extension}",
72 arch = match arch {
73 zed::Architecture::Aarch64 => "aarch64",
74 zed::Architecture::X86 => "x86",
75 zed::Architecture::X8664 => "x86_64",
76 },
77 os = match platform {
78 zed::Os::Mac => "macos",
79 zed::Os::Linux => "linux",
80 zed::Os::Windows => "windows",
81 },
82 extension = match platform {
83 zed::Os::Mac | zed::Os::Linux => "tar.gz",
84 zed::Os::Windows => "zip",
85 }
86 );
87
88 let asset = release
89 .assets
90 .iter()
91 .find(|asset| asset.name == asset_name)
92 .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
93
94 let version_dir = format!("zls-{}", release.version);
95 let binary_path = match platform {
96 zed::Os::Mac | zed::Os::Linux => format!("{version_dir}/bin/zls"),
97 zed::Os::Windows => format!("{version_dir}/zls.exe"),
98 };
99
100 if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
101 zed::set_language_server_installation_status(
102 &language_server_id,
103 &zed::LanguageServerInstallationStatus::Downloading,
104 );
105
106 zed::download_file(
107 &asset.download_url,
108 &version_dir,
109 match platform {
110 zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::GzipTar,
111 zed::Os::Windows => zed::DownloadedFileType::Zip,
112 },
113 )
114 .map_err(|e| format!("failed to download file: {e}"))?;
115
116 zed::make_file_executable(&binary_path)?;
117
118 let entries =
119 fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
120 for entry in entries {
121 let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
122 if entry.file_name().to_str() != Some(&version_dir) {
123 fs::remove_dir_all(&entry.path()).ok();
124 }
125 }
126 }
127
128 self.cached_binary_path = Some(binary_path.clone());
129 Ok(ZlsBinary {
130 path: binary_path,
131 args,
132 environment,
133 })
134 }
135}
136
137impl zed::Extension for ZigExtension {
138 fn new() -> Self {
139 Self {
140 cached_binary_path: None,
141 }
142 }
143
144 fn language_server_command(
145 &mut self,
146 language_server_id: &LanguageServerId,
147 worktree: &zed::Worktree,
148 ) -> Result<zed::Command> {
149 let zls_binary = self.language_server_binary(language_server_id, worktree)?;
150 Ok(zed::Command {
151 command: zls_binary.path,
152 args: zls_binary.args.unwrap_or_default(),
153 env: zls_binary.environment.unwrap_or_default(),
154 })
155 }
156
157 fn language_server_workspace_configuration(
158 &mut self,
159 _language_server_id: &zed::LanguageServerId,
160 worktree: &zed::Worktree,
161 ) -> Result<Option<serde_json::Value>> {
162 let settings = LspSettings::for_worktree("zls", worktree)
163 .ok()
164 .and_then(|lsp_settings| lsp_settings.settings.clone())
165 .unwrap_or_default();
166 Ok(Some(settings))
167 }
168}
169
170zed::register_extension!(ZigExtension);