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