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