From 843ff74d5d9f31d70b5716516650e88ac5d08b52 Mon Sep 17 00:00:00 2001 From: Piettro <2058578+pcastro93@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:37:26 -0500 Subject: [PATCH] fix(tools): name uv as the installer for Python servers when it makes the virtualenv (#936) Tools and the install log named "pip (virtualenv)" for Python language servers (basedpyright, Pyright, ty, Ruff) even when the virtualenv is made with uv, which `install_venv` prefers whenever uv is on `PATH`. The label now comes from the installer that will actually run: "uv (virtualenv)" when uv is on `PATH`, unchanged otherwise. debugpy's private copy still says pip, since it is always installed with `python -m pip`. Co-authored-by: pcastro93 <2058578+pcastro93@users.noreply.github.com> --- crates/ide-tools/src/lib.rs | 15 +++++++++++++-- crates/ide-tools/src/tests.rs | 15 +++++++++++++++ src/features/tools/backend.ts | 2 +- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/crates/ide-tools/src/lib.rs b/crates/ide-tools/src/lib.rs index 1822367fe..af2daf933 100644 --- a/crates/ide-tools/src/lib.rs +++ b/crates/ide-tools/src/lib.rs @@ -540,6 +540,15 @@ impl Tools { if managed || self.method_missing(spec.method).is_some() { fallback } else { spec.method } } + /// [`Method::label`], naming the installer that will actually run: a virtualenv is + /// made with uv when it is on `PATH` (see `install_venv`). + fn method_label(&self, method: Method) -> &'static str { + match method { + Method::PipVenv { .. } if self.which("uv").is_some() => "uv (virtualenv)", + m => m.label(), + } + } + /// Requirement check for installing `spec` right now. fn unavailable_reason(&self, spec: &ToolSpec) -> Option { self.method_missing(self.install_method(spec)) @@ -624,7 +633,7 @@ impl Tools { status, path: path.map(Into::into), version, - install_method: Some(self.install_method(spec).label().to_string()), + install_method: Some(self.method_label(self.install_method(spec)).to_string()), can_install: unavailable_reason.is_none() && !bundled, unavailable_reason, can_uninstall, @@ -796,7 +805,9 @@ impl Tools { let result = (|| { std::fs::create_dir_all(&self.root)?; let method = self.install_method(spec); - emit(InstallEvent::Line { text: format!("Installing {} via {}…", spec.display, method.label()) }); + emit(InstallEvent::Line { + text: format!("Installing {} via {}…", spec.display, self.method_label(method)), + }); match method { Method::Bundled { .. } => { bundled::install(&self.root, spec)?; diff --git a/crates/ide-tools/src/tests.rs b/crates/ide-tools/src/tests.rs index ccb35e2be..40112196f 100644 --- a/crates/ide-tools/src/tests.rs +++ b/crates/ide-tools/src/tests.rs @@ -161,6 +161,21 @@ fn interchangeable_servers_list_their_alternatives() { assert!(find(&list, "rust-analyzer").alternatives.is_empty()); } +#[cfg(unix)] +#[test] +fn python_servers_name_uv_when_it_makes_the_virtualenv() { + let env = Env::new(); + env.script(&env.sys, "uv", "exit 0"); + let list = env.tools().list(&langs()); + for name in ["basedpyright", "ty", "ruff"] { + let tool = find(&list, name); + assert_eq!(tool.install_method.as_deref(), Some("uv (virtualenv)"), "{name}"); + assert!(tool.can_install, "{name}: uv alone is enough"); + } + // debugpy's private copy is always made by `python -m pip`. + assert_eq!(find(&list, "debugpy").install_method.as_deref(), Some("pip (private copy)")); +} + #[cfg(unix)] #[test] fn detects_tools_on_path_with_versions() { diff --git a/src/features/tools/backend.ts b/src/features/tools/backend.ts index f0ae57560..f472418a3 100644 --- a/src/features/tools/backend.ts +++ b/src/features/tools/backend.ts @@ -99,7 +99,7 @@ export function sampleTools(): ToolInfo[] { name, displayName: name === "pyright" ? "Pyright" : name === "ty" ? "ty (Python)" : name, languages: ["Python"], - installMethod: "pip (virtualenv)", + installMethod: "uv (virtualenv)", alternatives: ["basedpyright", "pyright", "ty"], }), ),