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 if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
23 return Ok(path.clone());
24 }
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).map_or(false, |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: Vec<_> = fs::read_dir(".")
78 .map_err(|e| format!("failed to list working directory {e}"))?
79 .collect();
80 for entry in entries {
81 let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
82 if entry.file_name().to_str() != Some(&version_dir) {
83 fs::remove_dir_all(entry.path()).ok();
84 }
85 }
86 }
87
88 self.cached_binary_path = Some(binary_path.clone());
89 Ok(binary_path)
90 }
91}
92
93impl zed::Extension for TestExtension {
94 fn new() -> Self {
95 Self {
96 cached_binary_path: None,
97 }
98 }
99
100 fn language_server_command(
101 &mut self,
102 language_server_id: &LanguageServerId,
103 worktree: &zed::Worktree,
104 ) -> Result<zed::Command> {
105 Ok(zed::Command {
106 command: self.language_server_binary_path(language_server_id, worktree)?,
107 args: vec!["lsp".to_string()],
108 env: Default::default(),
109 })
110 }
111
112 fn label_for_completion(
113 &self,
114 _language_server_id: &LanguageServerId,
115 completion: zed::lsp::Completion,
116 ) -> Option<zed::CodeLabel> {
117 let name = &completion.label;
118 let ty = strip_newlines_from_detail(&completion.detail?);
119 let let_binding = "let a";
120 let colon = ": ";
121 let assignment = " = ";
122 let call = match completion.kind? {
123 CompletionKind::Function | CompletionKind::Constructor => "()",
124 _ => "",
125 };
126 let code = format!("{let_binding}{colon}{ty}{assignment}{name}{call}");
127
128 Some(CodeLabel {
129 spans: vec![
130 CodeLabelSpan::code_range({
131 let start = let_binding.len() + colon.len() + ty.len() + assignment.len();
132 start..start + name.len()
133 }),
134 CodeLabelSpan::code_range({
135 let start = let_binding.len();
136 start..start + colon.len()
137 }),
138 CodeLabelSpan::code_range({
139 let start = let_binding.len() + colon.len();
140 start..start + ty.len()
141 }),
142 ],
143 filter_range: (0..name.len()).into(),
144 code,
145 })
146 }
147}
148
149zed::register_extension!(TestExtension);
150
151/// Removes newlines from the completion detail.
152///
153/// The Gleam LSP can return types containing newlines, which causes formatting
154/// issues within the Zed completions menu.
155fn strip_newlines_from_detail(detail: &str) -> String {
156 let without_newlines = detail
157 .replace("->\n ", "-> ")
158 .replace("\n ", "")
159 .replace(",\n", "");
160
161 let comma_delimited_parts = without_newlines.split(',');
162 comma_delimited_parts
163 .map(|part| part.trim())
164 .collect::<Vec<_>>()
165 .join(", ")
166}