1use std::fs;
2use zed::lsp::CompletionKind;
3use zed::{CodeLabel, CodeLabelSpan, LanguageServerId};
4use zed_extension_api::process::Command;
5use zed_extension_api::{self as zed, Result};
6
7struct TestExtension {
8 cached_binary_path: Option<String>,
9}
10
11impl TestExtension {
12 fn language_server_binary_path(
13 &mut self,
14 language_server_id: &LanguageServerId,
15 _worktree: &zed::Worktree,
16 ) -> Result<String> {
17 let (platform, arch) = zed::current_platform();
18
19 let current_dir = std::env::current_dir().unwrap();
20 println!("current_dir: {}", current_dir.display());
21 assert_eq!(
22 current_dir.file_name().unwrap().to_str().unwrap(),
23 "test-extension"
24 );
25
26 fs::create_dir_all(current_dir.join("dir-created-with-abs-path")).unwrap();
27 fs::create_dir_all("./dir-created-with-rel-path").unwrap();
28 fs::write("file-created-with-rel-path", b"contents 1").unwrap();
29 fs::write(
30 current_dir.join("file-created-with-abs-path"),
31 b"contents 2",
32 )
33 .unwrap();
34 assert_eq!(
35 fs::read("file-created-with-rel-path").unwrap(),
36 b"contents 1"
37 );
38 assert_eq!(
39 fs::read("file-created-with-abs-path").unwrap(),
40 b"contents 2"
41 );
42
43 let command = match platform {
44 zed::Os::Linux | zed::Os::Mac => Command::new("echo"),
45 zed::Os::Windows => Command::new("cmd").args(["/C", "echo"]),
46 };
47 let output = command.arg("hello from a child process!").output()?;
48 println!(
49 "command output: {}",
50 String::from_utf8_lossy(&output.stdout).trim()
51 );
52
53 if let Some(path) = &self.cached_binary_path
54 && fs::metadata(path).is_ok_and(|stat| stat.is_file())
55 {
56 return Ok(path.clone());
57 }
58
59 zed::set_language_server_installation_status(
60 language_server_id,
61 &zed::LanguageServerInstallationStatus::CheckingForUpdate,
62 );
63 let release = zed::latest_github_release(
64 "gleam-lang/gleam",
65 zed::GithubReleaseOptions {
66 require_assets: true,
67 pre_release: false,
68 },
69 )?;
70
71 let ext = "tar.gz";
72 let download_type = zed::DownloadedFileType::GzipTar;
73
74 // Do this if you want to actually run this extension -
75 // the actual asset is a .zip. But the integration test is simpler
76 // if every platform uses .tar.gz.
77 //
78 // ext = "zip";
79 // download_type = zed::DownloadedFileType::Zip;
80
81 let asset_name = format!(
82 "gleam-{version}-{arch}-{os}.{ext}",
83 version = release.version,
84 arch = match arch {
85 zed::Architecture::Aarch64 => "aarch64",
86 zed::Architecture::X86 => "x86",
87 zed::Architecture::X8664 => "x86_64",
88 },
89 os = match platform {
90 zed::Os::Mac => "apple-darwin",
91 zed::Os::Linux => "unknown-linux-musl",
92 zed::Os::Windows => "pc-windows-msvc",
93 },
94 );
95
96 let asset = release
97 .assets
98 .iter()
99 .find(|asset| asset.name == asset_name)
100 .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
101
102 let version_dir = format!("gleam-{}", release.version);
103 let binary_path = format!("{version_dir}/gleam");
104
105 if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) {
106 zed::set_language_server_installation_status(
107 language_server_id,
108 &zed::LanguageServerInstallationStatus::Downloading,
109 );
110
111 zed::download_file(&asset.download_url, &version_dir, download_type)
112 .map_err(|e| format!("failed to download file: {e}"))?;
113
114 zed::set_language_server_installation_status(
115 language_server_id,
116 &zed::LanguageServerInstallationStatus::None,
117 );
118
119 let entries =
120 fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
121 for entry in entries {
122 let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
123 let filename = entry.file_name();
124 let filename = filename.to_str().unwrap();
125 if filename.starts_with("gleam-") && filename != version_dir {
126 fs::remove_dir_all(entry.path()).ok();
127 }
128 }
129 }
130
131 self.cached_binary_path = Some(binary_path.clone());
132 Ok(binary_path)
133 }
134}
135
136impl zed::Extension for TestExtension {
137 fn new() -> Self {
138 Self {
139 cached_binary_path: None,
140 }
141 }
142
143 fn language_server_command(
144 &mut self,
145 language_server_id: &LanguageServerId,
146 worktree: &zed::Worktree,
147 ) -> Result<zed::Command> {
148 Ok(zed::Command {
149 command: self.language_server_binary_path(language_server_id, worktree)?,
150 args: vec!["lsp".to_string()],
151 env: Default::default(),
152 })
153 }
154
155 fn label_for_completion(
156 &self,
157 _language_server_id: &LanguageServerId,
158 completion: zed::lsp::Completion,
159 ) -> Option<zed::CodeLabel> {
160 let name = &completion.label;
161 let ty = strip_newlines_from_detail(&completion.detail?);
162 let let_binding = "let a";
163 let colon = ": ";
164 let assignment = " = ";
165 let call = match completion.kind? {
166 CompletionKind::Function | CompletionKind::Constructor => "()",
167 _ => "",
168 };
169 let code = format!("{let_binding}{colon}{ty}{assignment}{name}{call}");
170
171 Some(CodeLabel {
172 spans: vec![
173 CodeLabelSpan::code_range({
174 let start = let_binding.len() + colon.len() + ty.len() + assignment.len();
175 start..start + name.len()
176 }),
177 CodeLabelSpan::code_range({
178 let start = let_binding.len();
179 start..start + colon.len()
180 }),
181 CodeLabelSpan::code_range({
182 let start = let_binding.len() + colon.len();
183 start..start + ty.len()
184 }),
185 ],
186 filter_range: (0..name.len()).into(),
187 code,
188 })
189 }
190}
191
192zed::register_extension!(TestExtension);
193
194/// Removes newlines from the completion detail.
195///
196/// The Gleam LSP can return types containing newlines, which causes formatting
197/// issues within the Zed completions menu.
198fn strip_newlines_from_detail(detail: &str) -> String {
199 let without_newlines = detail
200 .replace("->\n ", "-> ")
201 .replace("\n ", "")
202 .replace(",\n", "");
203
204 let comma_delimited_parts = without_newlines.split(',');
205 comma_delimited_parts
206 .map(|part| part.trim())
207 .collect::<Vec<_>>()
208 .join(", ")
209}