test_extension.rs

  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}