diff --git a/crates/ide-tools/src/lib.rs b/crates/ide-tools/src/lib.rs index 66b67a8c..317eea3e 100644 --- a/crates/ide-tools/src/lib.rs +++ b/crates/ide-tools/src/lib.rs @@ -138,7 +138,7 @@ fn bin_dirs_in(root: &Path, platform: Platform) -> Vec { } fn managed_bin_dir(root: &Path, spec: &ToolSpec, platform: Platform) -> Option { - match spec.method { + match spec.fallback.unwrap_or(spec.method) { Method::Npm { .. } => Some(root.join("node_modules").join(".bin")), Method::Go { .. } | Method::Gem { .. } | Method::DotnetTool { .. } | Method::Release { .. } => { Some(root.join(spec.name).join("bin")) @@ -520,16 +520,26 @@ impl Tools { parse_version(&out) } + /// The installer that owns this tool: a managed copy is refreshed and removed by the + /// fallback that made it, even if the primary installer has appeared since. + fn install_method(&self, spec: &ToolSpec) -> Method { + let Some(fallback) = spec.fallback else { return spec.method }; + let managed = managed_bin_dir(&self.root, spec, self.platform) + .and_then(|dir| self.exe_in(&dir, spec.commands[0])) + .is_some(); + if managed || self.method_missing(spec.method).is_some() { fallback } else { spec.method } + } + /// Requirement check for installing `spec` right now. fn unavailable_reason(&self, spec: &ToolSpec) -> Option { - self.installer_missing(spec) + self.method_missing(self.install_method(spec)) .or_else(|| spec.needs.iter().find(|(cmd, _)| self.which(cmd).is_none()).map(|(_, why)| why.to_string())) } /// What the install method itself lacks (npm, a build for this platform, …). - fn installer_missing(&self, spec: &ToolSpec) -> Option { + fn method_missing(&self, method: Method) -> Option { let need = |ok: bool, msg: &str| (!ok).then(|| msg.to_string()); - match spec.method { + match method { Method::Rustup { .. } => need(self.which("rustup").is_some(), "rustup is required (https://rustup.rs)"), Method::Npm { .. } => need( self.which_any(&["npm", "pnpm"]).is_some(), @@ -602,7 +612,7 @@ impl Tools { status, path: path.map(Into::into), version, - install_method: Some(spec.method.label().to_string()), + install_method: Some(self.install_method(spec).label().to_string()), can_install: unavailable_reason.is_none(), unavailable_reason, can_uninstall, @@ -643,7 +653,16 @@ impl Tools { fn detect(&self, spec: &ToolSpec) -> (ToolStatus, Option, Option) { let main = spec.commands[0]; - match spec.method { + let managed = managed_bin_dir(&self.root, spec, self.platform).and_then(|d| self.exe_in(&d, main)); + if let Some(path) = managed { + let version = match spec.method { + Method::Npm { packages } => self.npm_package_version(npm_name(packages[0])), + _ => self.manifest(spec.name).and_then(|m| m.version), + } + .or_else(|| self.query_version(spec, &path)); + return (ToolStatus::Installed, Some(path), version); + } + match self.install_method(spec) { Method::Rustup { .. } => { let Some(path) = self.which(main) else { return (ToolStatus::Missing, None, None) }; // The rustup proxy exists even without the component; it only answers when installed. @@ -678,24 +697,13 @@ impl Tools { _ => (ToolStatus::Missing, None, None), } } - _ => { - let managed = managed_bin_dir(&self.root, spec, self.platform).and_then(|d| self.exe_in(&d, main)); - if let Some(path) = managed { - let version = match spec.method { - Method::Npm { packages } => self.npm_package_version(npm_name(packages[0])), - _ => self.manifest(spec.name).and_then(|m| m.version), - } - .or_else(|| self.query_version(spec, &path)); - return (ToolStatus::Installed, Some(path), version); + _ => match self.which(main) { + Some(path) => { + let version = self.query_version(spec, &path); + (ToolStatus::OnPath, Some(path), version) } - match self.which(main) { - Some(path) => { - let version = self.query_version(spec, &path); - (ToolStatus::OnPath, Some(path), version) - } - None => (ToolStatus::Missing, None, None), - } - } + None => (ToolStatus::Missing, None, None), + }, } } @@ -769,8 +777,9 @@ impl Tools { let emit = &fan as Emit; let result = (|| { std::fs::create_dir_all(&self.root)?; - emit(InstallEvent::Line { text: format!("Installing {} via {}…", spec.display, spec.method.label()) }); - match spec.method { + let method = self.install_method(spec); + emit(InstallEvent::Line { text: format!("Installing {} via {}…", spec.display, method.label()) }); + match method { Method::Rustup { component } => { let mut c = self.command(self.which("rustup").expect("checked")); c.args(["component", "add", component]); @@ -1072,6 +1081,10 @@ impl Tools { } ReleaseSource::OpenVsx { namespace, extension } => { let json = http.get_text(&sources::openvsx_latest_url(namespace, extension), cancel)?; + let json = match sources::openvsx_platform_url(&json, assets, self.platform)? { + Some(url) => http.get_text(&url, cancel)?, + None => json, + }; sources::openvsx_pick(&json, assets, self.platform) } ReleaseSource::MavenCentral { group, artifact } => { @@ -1172,7 +1185,7 @@ impl Tools { let _guard = InstallGuard::acquire(&format!("uninstall-{name}"))?; let cancel = CancellationToken::new(); let quiet = |_: InstallEvent| {}; - match spec.method { + match self.install_method(spec) { Method::Rustup { component } => { let rustup = self.which("rustup").ok_or_else(|| IdeError::unavailable("rustup not found"))?; let mut c = self.command(rustup); diff --git a/crates/ide-tools/src/registry.rs b/crates/ide-tools/src/registry.rs index b571b054..161d9dc8 100644 --- a/crates/ide-tools/src/registry.rs +++ b/crates/ide-tools/src/registry.rs @@ -82,6 +82,8 @@ pub struct ToolSpec { /// is a plugin bundle loaded by another tool (copied to `bin/` under this name). pub commands: &'static [&'static str], pub method: Method, + /// Used when the primary installer is unavailable. + pub fallback: Option, /// Version query: a command (looked up next to the main binary first) and its args. pub version: Option<(&'static str, &'static [&'static str])>, pub homepage: &'static str, @@ -123,6 +125,18 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["rust-analyzer"], method: Method::Rustup { component: "rust-analyzer" }, + fallback: Some(Method::Release { + source: ReleaseSource::OpenVsx { namespace: "rust-lang", extension: "rust-analyzer" }, + assets: &[ + rule(Mac, Arm64, "rust-lang.rust-analyzer-*@darwin-arm64.vsix"), + rule(Mac, X64, "rust-lang.rust-analyzer-*@darwin-x64.vsix"), + rule(Linux, Arm64, "rust-lang.rust-analyzer-*@linux-arm64.vsix"), + rule(Linux, X64, "rust-lang.rust-analyzer-*@linux-x64.vsix"), + rule(Windows, Arm64, "rust-lang.rust-analyzer-*@win32-arm64.vsix"), + rule(Windows, X64, "rust-lang.rust-analyzer-*@win32-x64.vsix"), + ], + locate: &["rust-analyzer"], + }), version: ver("rust-analyzer"), homepage: "https://rust-analyzer.github.io", needs: &[], @@ -133,6 +147,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["vtsls"], method: Method::Npm { packages: &["@vtsls/language-server"] }, + fallback: None, version: V, homepage: "https://github.com/yioneko/vtsls", needs: &[], @@ -143,6 +158,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["typescript-language-server"], method: Method::Npm { packages: &["typescript-language-server", TYPESCRIPT] }, + fallback: None, version: ver("typescript-language-server"), homepage: "https://github.com/typescript-language-server/typescript-language-server", needs: &[], @@ -153,6 +169,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["pyright-langserver", "pyright"], method: Method::Npm { packages: &["pyright"] }, + fallback: None, version: ver("pyright"), homepage: "https://github.com/microsoft/pyright", needs: &[], @@ -163,6 +180,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["basedpyright-langserver", "basedpyright"], method: Method::PipVenv { package: "basedpyright" }, + fallback: None, version: ver("basedpyright"), homepage: "https://docs.basedpyright.com", needs: &[], @@ -173,6 +191,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["ty"], method: Method::PipVenv { package: "ty" }, + fallback: None, version: ver("ty"), homepage: "https://docs.astral.sh/ty/", needs: &[], @@ -183,6 +202,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["bash-language-server"], method: Method::Npm { packages: &["bash-language-server"] }, + fallback: None, version: ver("bash-language-server"), homepage: "https://github.com/bash-lsp/bash-language-server", needs: &[], @@ -193,6 +213,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["yaml-language-server"], method: Method::Npm { packages: &["yaml-language-server"] }, + fallback: None, version: V, homepage: "https://github.com/redhat-developer/yaml-language-server", needs: &[], @@ -203,6 +224,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["vscode-json-language-server"], method: Method::Npm { packages: &["vscode-langservers-extracted"] }, + fallback: None, version: V, homepage: "https://github.com/hrsh7th/vscode-langservers-extracted", needs: &[], @@ -213,6 +235,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["vscode-css-language-server"], method: Method::Npm { packages: &["vscode-langservers-extracted"] }, + fallback: None, version: V, homepage: "https://github.com/hrsh7th/vscode-langservers-extracted", needs: &[], @@ -223,6 +246,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["vscode-html-language-server"], method: Method::Npm { packages: &["vscode-langservers-extracted"] }, + fallback: None, version: V, homepage: "https://github.com/hrsh7th/vscode-langservers-extracted", needs: &[], @@ -233,6 +257,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["docker-langserver"], method: Method::Npm { packages: &["dockerfile-language-server-nodejs"] }, + fallback: None, version: V, homepage: "https://github.com/rcjsuen/dockerfile-language-server", needs: &[], @@ -243,6 +268,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["svelteserver"], method: Method::Npm { packages: &["svelte-language-server", TYPESCRIPT] }, + fallback: None, version: V, homepage: "https://github.com/sveltejs/language-tools", needs: &[], @@ -253,6 +279,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["vue-language-server"], method: Method::Npm { packages: &["@vue/language-server", TYPESCRIPT] }, + fallback: None, version: V, homepage: "https://github.com/vuejs/language-tools", needs: &[], @@ -264,6 +291,7 @@ pub static TOOLS: &[ToolSpec] = &[ commands: &["ngserver"], // Brings the TypeScript and `@angular/language-service` it loads. method: Method::Npm { packages: &["@angular/language-server"] }, + fallback: None, version: V, homepage: "https://angular.dev/tools/language-service", needs: &[], @@ -274,6 +302,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["graphql-lsp"], method: Method::Npm { packages: &["graphql-language-service-cli"] }, + fallback: None, version: V, homepage: "https://github.com/graphql/graphiql", needs: &[], @@ -284,6 +313,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["intelephense"], method: Method::Npm { packages: &["intelephense"] }, + fallback: None, version: V, homepage: "https://intelephense.com", needs: &[], @@ -294,6 +324,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["gopls"], method: Method::Go { module: "golang.org/x/tools/gopls@latest" }, + fallback: None, version: Some(("gopls", &["version"])), homepage: "https://pkg.go.dev/golang.org/x/tools/gopls", needs: &[], @@ -304,6 +335,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["sqls"], method: Method::Go { module: "github.com/sqls-server/sqls@latest" }, + fallback: None, version: Some(("sqls", &["--version"])), homepage: "https://github.com/sqls-server/sqls", needs: &[], @@ -314,6 +346,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: Debugger, commands: &["dlv"], method: Method::Go { module: "github.com/go-delve/delve/cmd/dlv@latest" }, + fallback: None, version: Some(("dlv", &["version"])), homepage: "https://github.com/go-delve/delve", needs: &[], @@ -324,6 +357,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: Debugger, commands: &["python3"], method: Method::PipTarget { package: "debugpy", module: "debugpy" }, + fallback: None, version: V, homepage: "https://github.com/microsoft/debugpy", needs: &[], @@ -342,6 +376,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["clangd"], }, + fallback: None, version: ver("clangd"), homepage: "https://clangd.llvm.org", needs: &[], @@ -362,6 +397,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["lua-language-server"], }, + fallback: None, version: ver("lua-language-server"), homepage: "https://luals.github.io", needs: &[], @@ -381,6 +417,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["marksman"], }, + fallback: None, version: ver("marksman"), homepage: "https://github.com/artempyanykh/marksman", needs: &[], @@ -401,6 +438,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["taplo"], }, + fallback: None, version: ver("taplo"), homepage: "https://taplo.tamasfe.dev", needs: &[], @@ -421,6 +459,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["zls"], }, + fallback: None, version: ver("zls"), homepage: "https://github.com/zigtools/zls", needs: &[], @@ -441,6 +480,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["terraform-ls"], }, + fallback: None, version: Some(("terraform-ls", &["version"])), homepage: "https://github.com/hashicorp/terraform-ls", needs: &[], @@ -466,6 +506,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["codelldb"], }, + fallback: None, version: V, homepage: "https://github.com/vadimcn/codelldb", needs: &[], @@ -485,6 +526,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["dapDebugServer.js"], }, + fallback: None, version: V, homepage: "https://github.com/microsoft/vscode-js-debug", needs: &[], @@ -503,6 +545,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["jdtls", "jdtls.bat"], }, + fallback: None, version: V, homepage: "https://github.com/eclipse-jdtls/eclipse.jdt.ls", needs: &[], @@ -522,6 +565,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["com.microsoft.java.debug.plugin-*.jar"], }, + fallback: None, version: V, homepage: "https://github.com/microsoft/java-debug", needs: &[], @@ -541,6 +585,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["lombok.jar"], }, + fallback: None, version: V, homepage: "https://projectlombok.org", needs: &[], @@ -565,6 +610,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["kotlin-lsp", "kotlin-lsp.sh", "kotlin-lsp.cmd"], }, + fallback: None, version: V, homepage: "https://github.com/Kotlin/kotlin-lsp", needs: &[], @@ -581,6 +627,7 @@ pub static TOOLS: &[ToolSpec] = &[ assets: ELIXIR_LS_ASSETS, locate: &["language_server.sh", "language_server.bat"], }, + fallback: None, version: V, homepage: "https://github.com/elixir-lsp/elixir-ls", needs: ELIXIR, @@ -595,6 +642,7 @@ pub static TOOLS: &[ToolSpec] = &[ assets: ELIXIR_LS_ASSETS, locate: &["debug_adapter.sh", "debug_adapter.bat"], }, + fallback: None, version: V, homepage: "https://github.com/elixir-lsp/elixir-ls", needs: ELIXIR, @@ -616,6 +664,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["buf"], }, + fallback: None, version: ver("buf"), homepage: "https://buf.build", needs: &[], @@ -636,6 +685,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["clojure-lsp"], }, + fallback: None, version: V, homepage: "https://clojure-lsp.io", needs: &[], @@ -657,6 +707,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["texlab"], }, + fallback: None, version: ver("texlab"), homepage: "https://github.com/latex-lsp/texlab", needs: &[], @@ -677,6 +728,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["neocmakelsp"], }, + fallback: None, version: ver("neocmakelsp"), homepage: "https://github.com/Decodetalkers/neocmakelsp", needs: &[], @@ -697,6 +749,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["lemminx-*"], }, + fallback: None, version: V, homepage: "https://github.com/eclipse/lemminx", needs: &[], @@ -715,6 +768,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["perlnavigator"], }, + fallback: None, version: V, homepage: "https://github.com/bscan/PerlNavigator", needs: &[("perl", "Perl is required (https://www.perl.org/get.html)")], @@ -729,6 +783,7 @@ pub static TOOLS: &[ToolSpec] = &[ assets: &[any_arch(Mac, "server.zip"), any_arch(Linux, "server.zip"), any_arch(Windows, "server.zip")], locate: &["kotlin-language-server"], }, + fallback: None, version: V, homepage: "https://github.com/fwcd/kotlin-language-server", needs: JAVA, @@ -743,6 +798,7 @@ pub static TOOLS: &[ToolSpec] = &[ assets: &[any_arch(Mac, "adapter.zip"), any_arch(Linux, "adapter.zip"), any_arch(Windows, "adapter.zip")], locate: &["kotlin-debug-adapter"], }, + fallback: None, version: V, homepage: "https://github.com/fwcd/kotlin-debug-adapter", needs: JAVA, @@ -762,6 +818,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["netcoredbg"], }, + fallback: None, version: V, homepage: "https://github.com/Samsung/netcoredbg", needs: &[], @@ -780,6 +837,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["bashDebug.js"], }, + fallback: None, version: V, homepage: "https://github.com/rogalmic/vscode-bash-debug", needs: NODE, @@ -798,6 +856,7 @@ pub static TOOLS: &[ToolSpec] = &[ ], locate: &["phpDebug.js"], }, + fallback: None, version: V, homepage: "https://github.com/xdebug/vscode-php-debug", needs: NODE, @@ -808,6 +867,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["ruby-lsp"], method: Method::Gem { gem: "ruby-lsp" }, + fallback: None, version: V, homepage: "https://shopify.github.io/ruby-lsp", needs: &[], @@ -818,6 +878,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["solargraph"], method: Method::Gem { gem: "solargraph" }, + fallback: None, version: ver("solargraph"), homepage: "https://solargraph.org", needs: &[], @@ -828,6 +889,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["csharp-ls"], method: Method::DotnetTool { package: "csharp-ls" }, + fallback: None, version: V, homepage: "https://github.com/razzmatazz/csharp-language-server", needs: &[], @@ -838,6 +900,7 @@ pub static TOOLS: &[ToolSpec] = &[ kind: LanguageServer, commands: &["fsautocomplete"], method: Method::DotnetTool { package: "fsautocomplete" }, + fallback: None, version: V, homepage: "https://github.com/ionide/FsAutoComplete", needs: &[], diff --git a/crates/ide-tools/src/sources.rs b/crates/ide-tools/src/sources.rs index aa10a985..09ec12a4 100644 --- a/crates/ide-tools/src/sources.rs +++ b/crates/ide-tools/src/sources.rs @@ -177,6 +177,8 @@ pub fn jdtls_download(version: &str, latest_txt: &str) -> IdeResult { #[derive(Deserialize)] struct VsxExtension { version: String, + #[serde(default)] + downloads: std::collections::BTreeMap, files: VsxFiles, } @@ -191,7 +193,22 @@ pub fn openvsx_latest_url(namespace: &str, extension: &str) -> String { format!("https://open-vsx.org/api/{namespace}/{extension}/latest") } -/// The `.vsix` of an Open VSX `…/latest` response (platform-independent extensions only). +/// Select platform metadata when the universal package does not match the asset rules. +pub fn openvsx_platform_url(json: &str, rules: &[AssetRule], platform: Platform) -> IdeResult> { + let ext: VsxExtension = + serde_json::from_str(json).map_err(|e| proto(format!("unexpected Open VSX response: {e}")))?; + if select_asset(&[file_name(&ext.files.download)], rules, platform).is_some() { + return Ok(None); + } + let names: Vec<&str> = ext.downloads.values().map(|url| file_name(url)).collect(); + let name = select_asset(&names, rules, platform) + .ok_or_else(|| IdeError::unavailable("No Open VSX package for this platform"))?; + let url = ext.downloads.values().find(|url| file_name(url) == name).expect("selected download"); + let (metadata, _) = url.rsplit_once("/file/").ok_or_else(|| proto("unexpected Open VSX download URL"))?; + Ok(Some(metadata.to_string())) +} + +/// The `.vsix` of an Open VSX `…/latest` response. pub fn openvsx_pick(json: &str, rules: &[AssetRule], platform: Platform) -> IdeResult { let ext: VsxExtension = serde_json::from_str(json).map_err(|e| proto(format!("unexpected Open VSX response: {e}")))?; @@ -355,6 +372,56 @@ mod tests { assert!(openvsx_pick(&other, rules, LINUX).is_err()); } + #[test] + fn openvsx_rust_analyzer_platform_packages() { + use crate::platform::{Arch, Os}; + let crate::registry::Method::Release { assets, .. } = find("rust-analyzer").unwrap().fallback.unwrap() else { + panic!("expected release fallback") + }; + let targets = [ + (Os::Mac, Arch::Arm64, "darwin-arm64"), + (Os::Mac, Arch::X64, "darwin-x64"), + (Os::Linux, Arch::Arm64, "linux-arm64"), + (Os::Linux, Arch::X64, "linux-x64"), + (Os::Windows, Arch::Arm64, "win32-arm64"), + (Os::Windows, Arch::X64, "win32-x64"), + ]; + let base = "https://open-vsx.org/api/rust-lang/rust-analyzer"; + let downloads: serde_json::Map = targets + .iter() + .map(|(_, _, target)| { + ( + target.to_string(), + format!("{base}/{target}/1.0.0/file/rust-lang.rust-analyzer-1.0.0@{target}.vsix").into(), + ) + }) + .collect(); + let universal = serde_json::json!({ + "version": "1.0.0", "downloads": downloads, + "files": {"download": format!("{base}/1.0.0/file/rust-lang.rust-analyzer-1.0.0.vsix")} + }) + .to_string(); + for (os, arch, target) in targets { + let platform = Platform { os, arch }; + assert_eq!( + openvsx_platform_url(&universal, assets, platform).unwrap(), + Some(format!("{base}/{target}/1.0.0")) + ); + let json = serde_json::json!({ + "version": "1.0.0", + "files": { + "download": downloads[target], + "sha256": format!("{base}/{target}/1.0.0/file/rust-lang.rust-analyzer-1.0.0@{target}.sha256") + } + }) + .to_string(); + let download = openvsx_pick(&json, assets, platform).unwrap(); + assert!(download.name.ends_with(&format!("@{target}.vsix"))); + assert!(download.checksum_url.unwrap().ends_with(&format!("@{target}.sha256"))); + } + assert!(openvsx_platform_url(&universal.replace("downloads", "unused"), assets, MAC_ARM).is_err()); + } + #[test] fn maven_central() { let xml = diff --git a/crates/ide-tools/src/tests.rs b/crates/ide-tools/src/tests.rs index b5cba665..e6aa29c2 100644 --- a/crates/ide-tools/src/tests.rs +++ b/crates/ide-tools/src/tests.rs @@ -65,8 +65,8 @@ fn everything_missing_on_an_empty_system() { let ra = find(&list, "rust-analyzer"); assert_eq!(ra.status, ToolStatus::Missing); assert_eq!(ra.languages, vec!["Rust".to_string()]); - assert!(!ra.can_install, "rustup is not on the fake PATH"); - assert!(ra.unavailable_reason.as_deref().unwrap().contains("rustup")); + assert!(!ra.can_install, "curl is not on the fake PATH"); + assert!(ra.unavailable_reason.as_deref().unwrap().contains("curl")); let vtsls = find(&list, "vtsls"); assert!(vtsls.languages.iter().any(|l| l == "TypeScript")); assert!(vtsls.unavailable_reason.as_deref().unwrap().contains("npm")); @@ -552,3 +552,74 @@ fn the_installing_request_can_still_cancel_its_shared_install() { assert!(slot.cancel.is_cancelled()); release_install_slot("taplo", &slot); } + +#[cfg(unix)] +#[test] +fn rust_analyzer_selects_openvsx_without_rustup() { + let env = Env::new(); + env.script(&env.sys, "curl", "exit 0"); + let tools = env.tools(); + let spec = registry::find("rust-analyzer").unwrap(); + assert!(matches!( + tools.install_method(spec), + Method::Release { source: ReleaseSource::OpenVsx { namespace: "rust-lang", extension: "rust-analyzer" }, .. } + )); + let info = tools.info("rust-analyzer", &langs()).unwrap(); + assert_eq!(info.status, ToolStatus::Missing); + assert_eq!(info.install_method.as_deref(), Some("Open VSX")); + assert!(info.can_install); + assert!(info.unavailable_reason.is_none()); + assert!(bin_dirs_in(&env.root, Platform::current()).contains(&env.root.join("rust-analyzer/bin"))); +} + +#[cfg(unix)] +static RUST_ANALYZER_INSTALL: parking_lot::Mutex<()> = parking_lot::Mutex::new(()); + +#[cfg(unix)] +#[test] +fn rust_analyzer_prefers_rustup_and_installs_component() { + let _guard = RUST_ANALYZER_INSTALL.lock(); + let env = Env::new(); + env.script(&env.sys, "rustup", "test \"$*\" = 'component add rust-analyzer'"); + env.script(&env.sys, "rust-analyzer", "echo 'rust-analyzer 1.0.0'"); + let tools = env.tools(); + assert!(matches!( + tools.install_method(registry::find("rust-analyzer").unwrap()), + Method::Rustup { component: "rust-analyzer" } + )); + let info = tools.info("rust-analyzer", &langs()).unwrap(); + assert_eq!(info.install_method.as_deref(), Some("rustup")); + assert!(info.can_install, "rustup does not require curl"); + let mut lines = Vec::new(); + let emit = parking_lot::Mutex::new(&mut lines); + tools + .install("rust-analyzer", &langs(), &CancellationToken::new(), &|event| { + if let InstallEvent::Line { text } = event { + emit.lock().push(text); + } + }) + .unwrap(); + assert!(lines.iter().any(|line| line.contains("via rustup"))); +} + +#[cfg(unix)] +#[test] +fn rust_analyzer_managed_copy_survives_rustup_becoming_available() { + let _guard = RUST_ANALYZER_INSTALL.lock(); + let env = Env::new(); + let bin = env.root.join("rust-analyzer/bin"); + env.script(&bin, "rust-analyzer", "echo 'rust-analyzer 1.0.0'"); + for with_rustup in [false, true] { + if with_rustup { + env.script(&env.sys, "rustup", "exit 1"); + env.script(&env.sys, "rust-analyzer", "exit 1"); + } + let info = env.tools().info("rust-analyzer", &langs()).unwrap(); + assert_eq!(info.status, ToolStatus::Installed); + assert_eq!(info.path.as_deref(), Some(bin.join("rust-analyzer").as_path())); + assert_eq!(info.install_method.as_deref(), Some("Open VSX"), "Reinstall refreshes the copy in use"); + assert!(info.can_uninstall); + } + env.tools().uninstall("rust-analyzer", &langs()).unwrap(); + assert!(!env.root.join("rust-analyzer").exists()); +}