Cargo.lock π
@@ -4058,10 +4058,10 @@ dependencies = [
"gpui",
"json_dotpath",
"language",
+ "log",
"paths",
"serde",
"serde_json",
- "smol",
"task",
"util",
"workspace-hack",
Raphael LΓΌthy created
Based on my report on discord when chatting with Anthony and Remco:
https://discord.com/channels/869392257814519848/1375129714645012530
Root Cause: Zed was incorrectly trying to execute a directory path
instead of properly invoking the debugpy module when debugpy was
installed via package managers (pip, conda, etc.) rather than downloaded
from GitHub releases.
Solution:
- Automatic Detection: Zed now automatically detects whether debugpy is
installed via pip/conda or downloaded from GitHub
- Correct Invocation: For pip-installed debugpy, Zed now uses python -m
debugpy.adapter instead of trying to execute file paths
- Added a `installed_in_venv` flag to differentiate the setup properly
- Backward Compatibility: GitHub-downloaded debugpy releases continue to
work as before
- Enhanced Logging: Added logging to show which debugpy installation
method is being used (I had to verify it somehow)
I verified with the following setups (can be confirmed with the debug
logs):
- `conda` with installed debugpy, went to installed instance
- `uv` with installed debugpy, went to installed instance
- `uv` without installed debugpy, went to github releases
- Homebrew global python install, went to github releases
Release Notes:
- Fix issue where debugpy from different environments won't load as
intended
Cargo.lock | 2
crates/dap_adapters/Cargo.toml | 2
crates/dap_adapters/src/python.rs | 161 +++++++++++++++++++++++++++-----
3 files changed, 138 insertions(+), 27 deletions(-)
@@ -4058,10 +4058,10 @@ dependencies = [
"gpui",
"json_dotpath",
"language",
+ "log",
"paths",
"serde",
"serde_json",
- "smol",
"task",
"util",
"workspace-hack",
@@ -28,10 +28,10 @@ futures.workspace = true
gpui.workspace = true
json_dotpath.workspace = true
language.workspace = true
+log.workspace = true
paths.workspace = true
serde.workspace = true
serde_json.workspace = true
-smol.workspace = true
task.workspace = true
util.workspace = true
workspace-hack.workspace = true
@@ -8,6 +8,7 @@ use gpui::{AsyncApp, SharedString};
use json_dotpath::DotPaths;
use language::{LanguageName, Toolchain};
use serde_json::Value;
+use std::net::Ipv4Addr;
use std::{
collections::HashMap,
ffi::OsStr,
@@ -27,6 +28,60 @@ impl PythonDebugAdapter {
const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
const LANGUAGE_NAME: &'static str = "Python";
+ async fn generate_debugpy_arguments(
+ &self,
+ host: &Ipv4Addr,
+ port: u16,
+ user_installed_path: Option<&Path>,
+ installed_in_venv: bool,
+ ) -> Result<Vec<String>> {
+ if let Some(user_installed_path) = user_installed_path {
+ log::debug!(
+ "Using user-installed debugpy adapter from: {}",
+ user_installed_path.display()
+ );
+ Ok(vec![
+ user_installed_path
+ .join(Self::ADAPTER_PATH)
+ .to_string_lossy()
+ .to_string(),
+ format!("--host={}", host),
+ format!("--port={}", port),
+ ])
+ } else if installed_in_venv {
+ log::debug!("Using venv-installed debugpy");
+ Ok(vec![
+ "-m".to_string(),
+ "debugpy.adapter".to_string(),
+ format!("--host={}", host),
+ format!("--port={}", port),
+ ])
+ } else {
+ let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
+ let file_name_prefix = format!("{}_", Self::ADAPTER_NAME);
+
+ let debugpy_dir =
+ util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
+ file_name.starts_with(&file_name_prefix)
+ })
+ .await
+ .context("Debugpy directory not found")?;
+
+ log::debug!(
+ "Using GitHub-downloaded debugpy adapter from: {}",
+ debugpy_dir.display()
+ );
+ Ok(vec![
+ debugpy_dir
+ .join(Self::ADAPTER_PATH)
+ .to_string_lossy()
+ .to_string(),
+ format!("--host={}", host),
+ format!("--port={}", port),
+ ])
+ }
+ }
+
fn request_args(
&self,
task_definition: &DebugTaskDefinition,
@@ -93,24 +148,12 @@ impl PythonDebugAdapter {
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
toolchain: Option<Toolchain>,
+ installed_in_venv: bool,
) -> Result<DebugAdapterBinary> {
const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
- let debugpy_dir = if let Some(user_installed_path) = user_installed_path {
- user_installed_path
- } else {
- let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
- let file_name_prefix = format!("{}_", Self::ADAPTER_NAME);
-
- util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
- file_name.starts_with(&file_name_prefix)
- })
- .await
- .context("Debugpy directory not found")?
- };
-
let python_path = if let Some(toolchain) = toolchain {
Some(toolchain.path.to_string())
} else {
@@ -128,16 +171,27 @@ impl PythonDebugAdapter {
name
};
+ let python_command = python_path.context("failed to find binary path for Python")?;
+ log::debug!("Using Python executable: {}", python_command);
+
+ let arguments = self
+ .generate_debugpy_arguments(
+ &host,
+ port,
+ user_installed_path.as_deref(),
+ installed_in_venv,
+ )
+ .await?;
+
+ log::debug!(
+ "Starting debugpy adapter with command: {} {}",
+ python_command,
+ arguments.join(" ")
+ );
+
Ok(DebugAdapterBinary {
- command: python_path.context("failed to find binary path for Python")?,
- arguments: vec![
- debugpy_dir
- .join(Self::ADAPTER_PATH)
- .to_string_lossy()
- .to_string(),
- format!("--port={}", port),
- format!("--host={}", host),
- ],
+ command: python_command,
+ arguments,
connection: Some(adapters::TcpArguments {
host,
port,
@@ -558,6 +612,16 @@ impl DebugAdapter for PythonDebugAdapter {
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
+ if let Some(local_path) = &user_installed_path {
+ log::debug!(
+ "Using user-installed debugpy adapter from: {}",
+ local_path.display()
+ );
+ return self
+ .get_installed_binary(delegate, &config, Some(local_path.clone()), None, false)
+ .await;
+ }
+
let toolchain = delegate
.toolchain_store()
.active_toolchain(
@@ -571,13 +635,18 @@ impl DebugAdapter for PythonDebugAdapter {
if let Some(toolchain) = &toolchain {
if let Some(path) = Path::new(&toolchain.path.to_string()).parent() {
let debugpy_path = path.join("debugpy");
- if smol::fs::metadata(&debugpy_path).await.is_ok() {
+ if delegate.fs().is_file(&debugpy_path).await {
+ log::debug!(
+ "Found debugpy in toolchain environment: {}",
+ debugpy_path.display()
+ );
return self
.get_installed_binary(
delegate,
&config,
- Some(debugpy_path.to_path_buf()),
+ None,
Some(toolchain.clone()),
+ true,
)
.await;
}
@@ -591,7 +660,49 @@ impl DebugAdapter for PythonDebugAdapter {
}
}
- self.get_installed_binary(delegate, &config, user_installed_path, toolchain)
+ self.get_installed_binary(delegate, &config, None, None, false)
+ .await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::{net::Ipv4Addr, path::PathBuf};
+
+ #[gpui::test]
+ async fn test_debugpy_install_path_cases() {
+ let adapter = PythonDebugAdapter::default();
+ let host = Ipv4Addr::new(127, 0, 0, 1);
+ let port = 5678;
+
+ // Case 1: User-defined debugpy path (highest precedence)
+ let user_path = PathBuf::from("/custom/path/to/debugpy");
+ let user_args = adapter
+ .generate_debugpy_arguments(&host, port, Some(&user_path), false)
.await
+ .unwrap();
+
+ // Case 2: Venv-installed debugpy (uses -m debugpy.adapter)
+ let venv_args = adapter
+ .generate_debugpy_arguments(&host, port, None, true)
+ .await
+ .unwrap();
+
+ assert!(user_args[0].ends_with("src/debugpy/adapter"));
+ assert_eq!(user_args[1], "--host=127.0.0.1");
+ assert_eq!(user_args[2], "--port=5678");
+
+ assert_eq!(venv_args[0], "-m");
+ assert_eq!(venv_args[1], "debugpy.adapter");
+ assert_eq!(venv_args[2], "--host=127.0.0.1");
+ assert_eq!(venv_args[3], "--port=5678");
+
+ // Note: Case 3 (GitHub-downloaded debugpy) is not tested since this requires mocking the Github API.
+ }
+
+ #[test]
+ fn test_adapter_path_constant() {
+ assert_eq!(PythonDebugAdapter::ADAPTER_PATH, "src/debugpy/adapter");
}
}