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