1use crate::*;
   2use anyhow::{Context as _, bail};
   3use collections::HashMap;
   4use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
   5use fs::RemoveOptions;
   6use futures::{StreamExt, TryStreamExt};
   7use gpui::http_client::AsyncBody;
   8use gpui::{AsyncApp, SharedString};
   9use json_dotpath::DotPaths;
  10use language::{LanguageName, Toolchain};
  11use paths::debug_adapters_dir;
  12use serde_json::Value;
  13use smol::fs::File;
  14use smol::io::AsyncReadExt;
  15use smol::lock::OnceCell;
  16use std::ffi::OsString;
  17use std::net::Ipv4Addr;
  18use std::str::FromStr;
  19use std::{
  20    ffi::OsStr,
  21    path::{Path, PathBuf},
  22};
  23use util::command::new_smol_command;
  24use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
  25
  26enum DebugpyLaunchMode<'a> {
  27    Normal,
  28    AttachWithConnect { host: Option<&'a str> },
  29}
  30
  31#[derive(Default)]
  32pub(crate) struct PythonDebugAdapter {
  33    base_venv_path: OnceCell<Result<Arc<Path>, String>>,
  34    debugpy_whl_base_path: OnceCell<Result<Arc<Path>, String>>,
  35}
  36
  37impl PythonDebugAdapter {
  38    const ADAPTER_NAME: &'static str = "Debugpy";
  39    const DEBUG_ADAPTER_NAME: DebugAdapterName =
  40        DebugAdapterName(SharedString::new_static(Self::ADAPTER_NAME));
  41
  42    const LANGUAGE_NAME: &'static str = "Python";
  43
  44    async fn generate_debugpy_arguments<'a>(
  45        host: &'a Ipv4Addr,
  46        port: u16,
  47        launch_mode: DebugpyLaunchMode<'a>,
  48        user_installed_path: Option<&'a Path>,
  49        user_args: Option<Vec<String>>,
  50    ) -> Result<Vec<String>> {
  51        let mut args = if let Some(user_installed_path) = user_installed_path {
  52            log::debug!(
  53                "Using user-installed debugpy adapter from: {}",
  54                user_installed_path.display()
  55            );
  56            vec![user_installed_path.to_string_lossy().into_owned()]
  57        } else {
  58            let adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref());
  59            let path = adapter_path
  60                .join("debugpy")
  61                .join("adapter")
  62                .to_string_lossy()
  63                .into_owned();
  64            log::debug!("Using pip debugpy adapter from: {path}");
  65            vec![path]
  66        };
  67
  68        args.extend(if let Some(args) = user_args {
  69            args
  70        } else {
  71            match launch_mode {
  72                DebugpyLaunchMode::Normal => {
  73                    vec![format!("--host={}", host), format!("--port={}", port)]
  74                }
  75                DebugpyLaunchMode::AttachWithConnect { host } => {
  76                    let mut args = vec!["connect".to_string()];
  77
  78                    if let Some(host) = host {
  79                        args.push(format!("{host}:"));
  80                    }
  81                    args.push(format!("{port}"));
  82                    args
  83                }
  84            }
  85        });
  86        Ok(args)
  87    }
  88
  89    async fn request_args(
  90        &self,
  91        delegate: &Arc<dyn DapDelegate>,
  92        task_definition: &DebugTaskDefinition,
  93    ) -> Result<StartDebuggingRequestArguments> {
  94        let request = self.request_kind(&task_definition.config).await?;
  95
  96        let mut configuration = task_definition.config.clone();
  97        if let Ok(console) = configuration.dot_get_mut("console") {
  98            // Use built-in Zed terminal if user did not explicitly provide a setting for console.
  99            if console.is_null() {
 100                *console = Value::String("integratedTerminal".into());
 101            }
 102        }
 103
 104        if let Some(obj) = configuration.as_object_mut() {
 105            obj.entry("cwd")
 106                .or_insert(delegate.worktree_root_path().to_string_lossy().into());
 107        }
 108
 109        Ok(StartDebuggingRequestArguments {
 110            configuration,
 111            request,
 112        })
 113    }
 114
 115    async fn fetch_wheel(
 116        &self,
 117        toolchain: Option<Toolchain>,
 118        delegate: &Arc<dyn DapDelegate>,
 119    ) -> Result<Arc<Path>> {
 120        let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME).join("wheels");
 121        std::fs::create_dir_all(&download_dir)?;
 122        let venv_python = self.base_venv_path(toolchain, delegate).await?;
 123
 124        let installation_succeeded = util::command::new_smol_command(venv_python.as_ref())
 125            .args([
 126                "-m",
 127                "pip",
 128                "download",
 129                "debugpy",
 130                "--only-binary=:all:",
 131                "-d",
 132                download_dir.to_string_lossy().as_ref(),
 133            ])
 134            .output()
 135            .await
 136            .context("spawn system python")?
 137            .status
 138            .success();
 139        if !installation_succeeded {
 140            bail!("debugpy installation failed (could not fetch Debugpy's wheel)");
 141        }
 142
 143        let wheel_path = std::fs::read_dir(&download_dir)?
 144            .find_map(|entry| {
 145                entry.ok().filter(|e| {
 146                    e.file_type().is_ok_and(|typ| typ.is_file())
 147                        && Path::new(&e.file_name()).extension() == Some("whl".as_ref())
 148                })
 149            })
 150            .with_context(|| format!("Did not find a .whl in {download_dir:?}"))?;
 151
 152        util::archive::extract_zip(
 153            &debug_adapters_dir().join(Self::ADAPTER_NAME),
 154            File::open(&wheel_path.path()).await?,
 155        )
 156        .await?;
 157
 158        Ok(Arc::from(wheel_path.path()))
 159    }
 160
 161    async fn maybe_fetch_new_wheel(
 162        &self,
 163        toolchain: Option<Toolchain>,
 164        delegate: &Arc<dyn DapDelegate>,
 165    ) -> Result<()> {
 166        let latest_release = delegate
 167            .http_client()
 168            .get(
 169                "https://pypi.org/pypi/debugpy/json",
 170                AsyncBody::empty(),
 171                false,
 172            )
 173            .await
 174            .log_err();
 175        let response = latest_release
 176            .filter(|response| response.status().is_success())
 177            .context("getting latest release")?;
 178
 179        let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
 180        std::fs::create_dir_all(&download_dir)?;
 181
 182        let mut output = String::new();
 183        response.into_body().read_to_string(&mut output).await?;
 184        let as_json = serde_json::Value::from_str(&output)?;
 185        let latest_version = as_json
 186            .get("info")
 187            .and_then(|info| {
 188                info.get("version")
 189                    .and_then(|version| version.as_str())
 190                    .map(ToOwned::to_owned)
 191            })
 192            .context("parsing latest release information")?;
 193        let dist_info_dirname: OsString = format!("debugpy-{latest_version}.dist-info").into();
 194        let is_up_to_date = delegate
 195            .fs()
 196            .read_dir(&debug_adapters_dir().join(Self::ADAPTER_NAME))
 197            .await?
 198            .into_stream()
 199            .any(async |entry| {
 200                entry.is_ok_and(|e| e.file_name().is_some_and(|name| name == dist_info_dirname))
 201            })
 202            .await;
 203
 204        if !is_up_to_date {
 205            delegate
 206                .fs()
 207                .remove_dir(
 208                    &debug_adapters_dir().join(Self::ADAPTER_NAME),
 209                    RemoveOptions {
 210                        recursive: true,
 211                        ignore_if_not_exists: true,
 212                    },
 213                )
 214                .await?;
 215            self.fetch_wheel(toolchain, delegate).await?;
 216        }
 217        anyhow::Ok(())
 218    }
 219
 220    async fn fetch_debugpy_whl(
 221        &self,
 222        toolchain: Option<Toolchain>,
 223        delegate: &Arc<dyn DapDelegate>,
 224    ) -> Result<Arc<Path>, String> {
 225        self.debugpy_whl_base_path
 226            .get_or_init(|| async move {
 227                self.maybe_fetch_new_wheel(toolchain, delegate)
 228                    .await
 229                    .map_err(|e| format!("{e}"))?;
 230                Ok(Arc::from(
 231                    debug_adapters_dir()
 232                        .join(Self::ADAPTER_NAME)
 233                        .join("debugpy")
 234                        .join("adapter")
 235                        .as_ref(),
 236                ))
 237            })
 238            .await
 239            .clone()
 240    }
 241
 242    async fn base_venv_path(
 243        &self,
 244        toolchain: Option<Toolchain>,
 245        delegate: &Arc<dyn DapDelegate>,
 246    ) -> Result<Arc<Path>> {
 247        let result = self.base_venv_path
 248            .get_or_init(|| async {
 249                let base_python = if let Some(toolchain) = toolchain {
 250                    toolchain.path.to_string()
 251                } else {
 252                    Self::system_python_name(delegate).await.ok_or_else(|| {
 253                        let mut message = "Could not find a Python installation".to_owned();
 254                        if cfg!(windows){
 255                            message.push_str(". Install Python from the Microsoft Store, or manually from https://www.python.org/downloads/windows.")
 256                        }
 257                        message
 258                    })?
 259                };
 260
 261                let debug_adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref());
 262                let output = util::command::new_smol_command(&base_python)
 263                    .args(["-m", "venv", "zed_base_venv"])
 264                    .current_dir(
 265                        &debug_adapter_path,
 266                    )
 267                    .spawn()
 268                    .map_err(|e| format!("{e:#?}"))?
 269                    .output()
 270                    .await
 271                    .map_err(|e| format!("{e:#?}"))?;
 272
 273                if !output.status.success() {
 274                    let stderr = String::from_utf8_lossy(&output.stderr);
 275                    let stdout = String::from_utf8_lossy(&output.stdout);
 276                    let debug_adapter_path = debug_adapter_path.display();
 277                    return Err(format!("Failed to create base virtual environment with {base_python} in:\n{debug_adapter_path}\nstderr:\n{stderr}\nstdout:\n{stdout}\n"));
 278                }
 279
 280                const PYTHON_PATH: &str = if cfg!(target_os = "windows") {
 281                    "Scripts/python.exe"
 282                } else {
 283                    "bin/python3"
 284                };
 285                Ok(Arc::from(
 286                    paths::debug_adapters_dir()
 287                        .join(Self::DEBUG_ADAPTER_NAME.as_ref())
 288                        .join("zed_base_venv")
 289                        .join(PYTHON_PATH)
 290                        .as_ref(),
 291                ))
 292            })
 293            .await
 294            .clone();
 295        match result {
 296            Ok(path) => Ok(path),
 297            Err(e) => Err(anyhow::anyhow!("{e}")),
 298        }
 299    }
 300    async fn system_python_name(delegate: &Arc<dyn DapDelegate>) -> Option<String> {
 301        const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
 302        let mut name = None;
 303
 304        for cmd in BINARY_NAMES {
 305            let Some(path) = delegate.which(OsStr::new(cmd)).await else {
 306                continue;
 307            };
 308            // Try to detect situations where `python3` exists but is not a real Python interpreter.
 309            // Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app
 310            // when run with no arguments, and just fails otherwise.
 311            let Some(output) = new_smol_command(&path)
 312                .args(["-c", "print(1 + 2)"])
 313                .output()
 314                .await
 315                .ok()
 316            else {
 317                continue;
 318            };
 319            if output.stdout.trim_ascii() != b"3" {
 320                continue;
 321            }
 322            name = Some(path.to_string_lossy().into_owned());
 323            break;
 324        }
 325        name
 326    }
 327
 328    async fn get_installed_binary(
 329        &self,
 330        delegate: &Arc<dyn DapDelegate>,
 331        config: &DebugTaskDefinition,
 332        user_installed_path: Option<PathBuf>,
 333        user_args: Option<Vec<String>>,
 334        user_env: Option<HashMap<String, String>>,
 335        python_from_toolchain: Option<String>,
 336    ) -> Result<DebugAdapterBinary> {
 337        let mut tcp_connection = config.tcp_connection.clone().unwrap_or_default();
 338
 339        let (config_port, config_host) = config
 340            .config
 341            .get("connect")
 342            .map(|value| {
 343                (
 344                    value
 345                        .get("port")
 346                        .and_then(|val| val.as_u64().map(|p| p as u16)),
 347                    value.get("host").and_then(|val| val.as_str()),
 348                )
 349            })
 350            .unwrap_or_else(|| {
 351                (
 352                    config
 353                        .config
 354                        .get("port")
 355                        .and_then(|port| port.as_u64().map(|p| p as u16)),
 356                    config.config.get("host").and_then(|host| host.as_str()),
 357                )
 358            });
 359
 360        let is_attach_with_connect = if config
 361            .config
 362            .get("request")
 363            .is_some_and(|val| val.as_str().is_some_and(|request| request == "attach"))
 364        {
 365            if tcp_connection.host.is_some() && config_host.is_some() {
 366                bail!("Cannot have two different hosts in debug configuration")
 367            } else if tcp_connection.port.is_some() && config_port.is_some() {
 368                bail!("Cannot have two different ports in debug configuration")
 369            }
 370
 371            tcp_connection.port = config_port;
 372            DebugpyLaunchMode::AttachWithConnect { host: config_host }
 373        } else {
 374            DebugpyLaunchMode::Normal
 375        };
 376
 377        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 378
 379        let python_path = if let Some(toolchain) = python_from_toolchain {
 380            Some(toolchain)
 381        } else {
 382            Self::system_python_name(delegate).await
 383        };
 384
 385        let python_command = python_path.context("failed to find binary path for Python")?;
 386        log::debug!("Using Python executable: {}", python_command);
 387
 388        let arguments = Self::generate_debugpy_arguments(
 389            &host,
 390            port,
 391            is_attach_with_connect,
 392            user_installed_path.as_deref(),
 393            user_args,
 394        )
 395        .await?;
 396
 397        log::debug!(
 398            "Starting debugpy adapter with command: {} {}",
 399            python_command,
 400            arguments.join(" ")
 401        );
 402
 403        Ok(DebugAdapterBinary {
 404            command: Some(python_command),
 405            arguments,
 406            connection: Some(adapters::TcpArguments {
 407                host,
 408                port,
 409                timeout,
 410            }),
 411            cwd: Some(delegate.worktree_root_path().to_path_buf()),
 412            envs: user_env.unwrap_or_default(),
 413            request_args: self.request_args(delegate, config).await?,
 414        })
 415    }
 416}
 417
 418#[async_trait(?Send)]
 419impl DebugAdapter for PythonDebugAdapter {
 420    fn name(&self) -> DebugAdapterName {
 421        Self::DEBUG_ADAPTER_NAME
 422    }
 423
 424    fn adapter_language_name(&self) -> Option<LanguageName> {
 425        Some(SharedString::new_static("Python").into())
 426    }
 427
 428    async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
 429        let mut args = json!({
 430            "request": match zed_scenario.request {
 431                DebugRequest::Launch(_) => "launch",
 432                DebugRequest::Attach(_) => "attach",
 433            },
 434            "subProcess": true,
 435            "redirectOutput": true,
 436        });
 437
 438        let map = args.as_object_mut().unwrap();
 439        match &zed_scenario.request {
 440            DebugRequest::Attach(attach) => {
 441                map.insert("processId".into(), attach.process_id.into());
 442            }
 443            DebugRequest::Launch(launch) => {
 444                map.insert("program".into(), launch.program.clone().into());
 445                map.insert("args".into(), launch.args.clone().into());
 446                if !launch.env.is_empty() {
 447                    map.insert("env".into(), launch.env_json());
 448                }
 449
 450                if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
 451                    map.insert("stopOnEntry".into(), stop_on_entry.into());
 452                }
 453                if let Some(cwd) = launch.cwd.as_ref() {
 454                    map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
 455                }
 456            }
 457        }
 458
 459        Ok(DebugScenario {
 460            adapter: zed_scenario.adapter,
 461            label: zed_scenario.label,
 462            config: args,
 463            build: None,
 464            tcp_connection: None,
 465        })
 466    }
 467
 468    fn dap_schema(&self) -> serde_json::Value {
 469        json!({
 470            "properties": {
 471                "request": {
 472                    "type": "string",
 473                    "enum": ["attach", "launch"],
 474                    "description": "Debug adapter request type"
 475                },
 476                "autoReload": {
 477                    "default": {},
 478                    "description": "Configures automatic reload of code on edit.",
 479                    "properties": {
 480                        "enable": {
 481                            "default": false,
 482                            "description": "Automatically reload code on edit.",
 483                            "type": "boolean"
 484                        },
 485                        "exclude": {
 486                            "default": [
 487                                "**/.git/**",
 488                                "**/.metadata/**",
 489                                "**/__pycache__/**",
 490                                "**/node_modules/**",
 491                                "**/site-packages/**"
 492                            ],
 493                            "description": "Glob patterns of paths to exclude from auto reload.",
 494                            "items": {
 495                                "type": "string"
 496                            },
 497                            "type": "array"
 498                        },
 499                        "include": {
 500                            "default": [
 501                                "**/*.py",
 502                                "**/*.pyw"
 503                            ],
 504                            "description": "Glob patterns of paths to include in auto reload.",
 505                            "items": {
 506                                "type": "string"
 507                            },
 508                            "type": "array"
 509                        }
 510                    },
 511                    "type": "object"
 512                },
 513                "debugAdapterPath": {
 514                    "description": "Path (fully qualified) to the python debug adapter executable.",
 515                    "type": "string"
 516                },
 517                "django": {
 518                    "default": false,
 519                    "description": "Django debugging.",
 520                    "type": "boolean"
 521                },
 522                "jinja": {
 523                    "default": null,
 524                    "description": "Jinja template debugging (e.g. Flask).",
 525                    "enum": [
 526                        false,
 527                        null,
 528                        true
 529                    ]
 530                },
 531                "justMyCode": {
 532                    "default": true,
 533                    "description": "If true, show and debug only user-written code. If false, show and debug all code, including library calls.",
 534                    "type": "boolean"
 535                },
 536                "logToFile": {
 537                    "default": false,
 538                    "description": "Enable logging of debugger events to a log file. This file can be found in the debugpy extension install folder.",
 539                    "type": "boolean"
 540                },
 541                "pathMappings": {
 542                    "default": [],
 543                    "items": {
 544                        "label": "Path mapping",
 545                        "properties": {
 546                            "localRoot": {
 547                                "default": "${ZED_WORKTREE_ROOT}",
 548                                "label": "Local source root.",
 549                                "type": "string"
 550                            },
 551                            "remoteRoot": {
 552                                "default": "",
 553                                "label": "Remote source root.",
 554                                "type": "string"
 555                            }
 556                        },
 557                        "required": [
 558                            "localRoot",
 559                            "remoteRoot"
 560                        ],
 561                        "type": "object"
 562                    },
 563                    "label": "Path mappings.",
 564                    "type": "array"
 565                },
 566                "redirectOutput": {
 567                    "default": true,
 568                    "description": "Redirect output.",
 569                    "type": "boolean"
 570                },
 571                "showReturnValue": {
 572                    "default": true,
 573                    "description": "Show return value of functions when stepping.",
 574                    "type": "boolean"
 575                },
 576                "subProcess": {
 577                    "default": false,
 578                    "description": "Whether to enable Sub Process debugging",
 579                    "type": "boolean"
 580                },
 581                "consoleName": {
 582                    "default": "Python Debug Console",
 583                    "description": "Display name of the debug console or terminal",
 584                    "type": "string"
 585                },
 586                "clientOS": {
 587                    "default": null,
 588                    "description": "OS that VS code is using.",
 589                    "enum": [
 590                        "windows",
 591                        null,
 592                        "unix"
 593                    ]
 594                }
 595            },
 596            "required": ["request"],
 597            "allOf": [
 598                {
 599                    "if": {
 600                        "properties": {
 601                            "request": {
 602                                "enum": ["attach"]
 603                            }
 604                        }
 605                    },
 606                    "then": {
 607                        "properties": {
 608                            "connect": {
 609                                "label": "Attach by connecting to debugpy over a socket.",
 610                                "properties": {
 611                                    "host": {
 612                                        "default": "127.0.0.1",
 613                                        "description": "Hostname or IP address to connect to.",
 614                                        "type": "string"
 615                                    },
 616                                    "port": {
 617                                        "description": "Port to connect to.",
 618                                        "type": [
 619                                            "number",
 620                                            "string"
 621                                        ]
 622                                    }
 623                                },
 624                                "required": [
 625                                    "port"
 626                                ],
 627                                "type": "object"
 628                            },
 629                            "listen": {
 630                                "label": "Attach by listening for incoming socket connection from debugpy",
 631                                "properties": {
 632                                    "host": {
 633                                        "default": "127.0.0.1",
 634                                        "description": "Hostname or IP address of the interface to listen on.",
 635                                        "type": "string"
 636                                    },
 637                                    "port": {
 638                                        "description": "Port to listen on.",
 639                                        "type": [
 640                                            "number",
 641                                            "string"
 642                                        ]
 643                                    }
 644                                },
 645                                "required": [
 646                                    "port"
 647                                ],
 648                                "type": "object"
 649                            },
 650                            "processId": {
 651                                "anyOf": [
 652                                    {
 653                                        "default": "${command:pickProcess}",
 654                                        "description": "Use process picker to select a process to attach, or Process ID as integer.",
 655                                        "enum": [
 656                                            "${command:pickProcess}"
 657                                        ]
 658                                    },
 659                                    {
 660                                        "description": "ID of the local process to attach to.",
 661                                        "type": "integer"
 662                                    }
 663                                ]
 664                            }
 665                        }
 666                    }
 667                },
 668                {
 669                    "if": {
 670                        "properties": {
 671                            "request": {
 672                                "enum": ["launch"]
 673                            }
 674                        }
 675                    },
 676                    "then": {
 677                        "properties": {
 678                            "args": {
 679                                "default": [],
 680                                "description": "Command line arguments passed to the program. For string type arguments, it will pass through the shell as is, and therefore all shell variable expansions will apply. But for the array type, the values will be shell-escaped.",
 681                                "items": {
 682                                    "type": "string"
 683                                },
 684                                "anyOf": [
 685                                    {
 686                                        "default": "${command:pickArgs}",
 687                                        "enum": [
 688                                            "${command:pickArgs}"
 689                                        ]
 690                                    },
 691                                    {
 692                                        "type": [
 693                                            "array",
 694                                            "string"
 695                                        ]
 696                                    }
 697                                ]
 698                            },
 699                            "console": {
 700                                "default": "integratedTerminal",
 701                                "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.",
 702                                "enum": [
 703                                    "externalTerminal",
 704                                    "integratedTerminal",
 705                                    "internalConsole"
 706                                ]
 707                            },
 708                            "cwd": {
 709                                "default": "${ZED_WORKTREE_ROOT}",
 710                                "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).",
 711                                "type": "string"
 712                            },
 713                            "autoStartBrowser": {
 714                                "default": false,
 715                                "description": "Open external browser to launch the application",
 716                                "type": "boolean"
 717                            },
 718                            "env": {
 719                                "additionalProperties": {
 720                                    "type": "string"
 721                                },
 722                                "default": {},
 723                                "description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.",
 724                                "type": "object"
 725                            },
 726                            "envFile": {
 727                                "default": "${ZED_WORKTREE_ROOT}/.env",
 728                                "description": "Absolute path to a file containing environment variable definitions.",
 729                                "type": "string"
 730                            },
 731                            "gevent": {
 732                                "default": false,
 733                                "description": "Enable debugging of gevent monkey-patched code.",
 734                                "type": "boolean"
 735                            },
 736                            "module": {
 737                                "default": "",
 738                                "description": "Name of the module to be debugged.",
 739                                "type": "string"
 740                            },
 741                            "program": {
 742                                "default": "${ZED_FILE}",
 743                                "description": "Absolute path to the program.",
 744                                "type": "string"
 745                            },
 746                            "purpose": {
 747                                "default": [],
 748                                "description": "Tells extension to use this configuration for test debugging, or when using debug-in-terminal command.",
 749                                "items": {
 750                                    "enum": [
 751                                        "debug-test",
 752                                        "debug-in-terminal"
 753                                    ],
 754                                    "enumDescriptions": [
 755                                        "Use this configuration while debugging tests using test view or test debug commands.",
 756                                        "Use this configuration while debugging a file using debug in terminal button in the editor."
 757                                    ]
 758                                },
 759                                "type": "array"
 760                            },
 761                            "pyramid": {
 762                                "default": false,
 763                                "description": "Whether debugging Pyramid applications.",
 764                                "type": "boolean"
 765                            },
 766                            "python": {
 767                                "default": "${command:python.interpreterPath}",
 768                                "description": "Absolute path to the Python interpreter executable; overrides workspace configuration if set.",
 769                                "type": "string"
 770                            },
 771                            "pythonArgs": {
 772                                "default": [],
 773                                "description": "Command-line arguments passed to the Python interpreter. To pass arguments to the debug target, use \"args\".",
 774                                "items": {
 775                                    "type": "string"
 776                                },
 777                                "type": "array"
 778                            },
 779                            "stopOnEntry": {
 780                                "default": false,
 781                                "description": "Automatically stop after launch.",
 782                                "type": "boolean"
 783                            },
 784                            "sudo": {
 785                                "default": false,
 786                                "description": "Running debug program under elevated permissions (on Unix).",
 787                                "type": "boolean"
 788                            },
 789                            "guiEventLoop": {
 790                                "default": "matplotlib",
 791                                "description": "The GUI event loop that's going to run. Possible values: \"matplotlib\", \"wx\", \"qt\", \"none\", or a custom function that'll be imported and run.",
 792                                "type": "string"
 793                            }
 794                        }
 795                    }
 796                }
 797            ]
 798        })
 799    }
 800
 801    async fn get_binary(
 802        &self,
 803        delegate: &Arc<dyn DapDelegate>,
 804        config: &DebugTaskDefinition,
 805        user_installed_path: Option<PathBuf>,
 806        user_args: Option<Vec<String>>,
 807        user_env: Option<HashMap<String, String>>,
 808        cx: &mut AsyncApp,
 809    ) -> Result<DebugAdapterBinary> {
 810        if let Some(local_path) = &user_installed_path {
 811            log::debug!(
 812                "Using user-installed debugpy adapter from: {}",
 813                local_path.display()
 814            );
 815            return self
 816                .get_installed_binary(
 817                    delegate,
 818                    config,
 819                    Some(local_path.clone()),
 820                    user_args,
 821                    user_env,
 822                    None,
 823                )
 824                .await;
 825        }
 826
 827        let base_paths = ["cwd", "program", "module"]
 828            .into_iter()
 829            .filter_map(|key| {
 830                config.config.get(key).and_then(|cwd| {
 831                    RelPath::new(
 832                        cwd.as_str()
 833                            .map(Path::new)?
 834                            .strip_prefix(delegate.worktree_root_path())
 835                            .ok()?,
 836                        PathStyle::local(),
 837                    )
 838                    .ok()
 839                })
 840            })
 841            .chain(
 842                // While Debugpy's wiki saids absolute paths are required, but it actually supports relative paths when cwd is passed in.
 843                // (Which should always be the case because Zed defaults to the cwd worktree root)
 844                // So we want to check that these relative paths find toolchains as well. Otherwise, they won't be checked
 845                // because the strip prefix in the iteration above will return an error
 846                config
 847                    .config
 848                    .get("cwd")
 849                    .map(|_| {
 850                        ["program", "module"].into_iter().filter_map(|key| {
 851                            config.config.get(key).and_then(|value| {
 852                                let path = Path::new(value.as_str()?);
 853                                RelPath::new(path, PathStyle::local()).ok()
 854                            })
 855                        })
 856                    })
 857                    .into_iter()
 858                    .flatten(),
 859            )
 860            .chain([RelPath::empty().into()]);
 861
 862        let mut toolchain = None;
 863
 864        for base_path in base_paths {
 865            if let Some(found_toolchain) = delegate
 866                .toolchain_store()
 867                .active_toolchain(
 868                    delegate.worktree_id(),
 869                    base_path.into_arc(),
 870                    language::LanguageName::new(Self::LANGUAGE_NAME),
 871                    cx,
 872                )
 873                .await
 874            {
 875                toolchain = Some(found_toolchain);
 876                break;
 877            }
 878        }
 879
 880        self.fetch_debugpy_whl(toolchain.clone(), delegate)
 881            .await
 882            .map_err(|e| anyhow::anyhow!("{e}"))?;
 883        if let Some(toolchain) = &toolchain {
 884            return self
 885                .get_installed_binary(
 886                    delegate,
 887                    config,
 888                    None,
 889                    user_args,
 890                    user_env,
 891                    Some(toolchain.path.to_string()),
 892                )
 893                .await;
 894        }
 895
 896        self.get_installed_binary(delegate, config, None, user_args, user_env, None)
 897            .await
 898    }
 899
 900    fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
 901        let label = args
 902            .configuration
 903            .get("name")?
 904            .as_str()
 905            .filter(|label| !label.is_empty())?;
 906        Some(label.to_owned())
 907    }
 908}
 909
 910#[cfg(test)]
 911mod tests {
 912    use util::path;
 913
 914    use super::*;
 915    use task::TcpArgumentsTemplate;
 916
 917    #[gpui::test]
 918    async fn test_tcp_connection_conflict_with_connect_args() {
 919        let adapter = PythonDebugAdapter {
 920            base_venv_path: OnceCell::new(),
 921            debugpy_whl_base_path: OnceCell::new(),
 922        };
 923
 924        let config_with_port_conflict = json!({
 925            "request": "attach",
 926            "connect": {
 927                "port": 5679
 928            }
 929        });
 930
 931        let tcp_connection = TcpArgumentsTemplate {
 932            host: None,
 933            port: Some(5678),
 934            timeout: None,
 935        };
 936
 937        let task_def = DebugTaskDefinition {
 938            label: "test".into(),
 939            adapter: PythonDebugAdapter::ADAPTER_NAME.into(),
 940            config: config_with_port_conflict,
 941            tcp_connection: Some(tcp_connection.clone()),
 942        };
 943
 944        let result = adapter
 945            .get_installed_binary(
 946                &test_mocks::MockDelegate::new(),
 947                &task_def,
 948                None,
 949                None,
 950                None,
 951                Some("python3".to_string()),
 952            )
 953            .await;
 954
 955        assert!(result.is_err());
 956        assert!(
 957            result
 958                .unwrap_err()
 959                .to_string()
 960                .contains("Cannot have two different ports")
 961        );
 962
 963        let host = Ipv4Addr::new(127, 0, 0, 1);
 964        let config_with_host_conflict = json!({
 965            "request": "attach",
 966            "connect": {
 967                "host": "192.168.1.1",
 968                "port": 5678
 969            }
 970        });
 971
 972        let tcp_connection_with_host = TcpArgumentsTemplate {
 973            host: Some(host),
 974            port: None,
 975            timeout: None,
 976        };
 977
 978        let task_def_host = DebugTaskDefinition {
 979            label: "test".into(),
 980            adapter: PythonDebugAdapter::ADAPTER_NAME.into(),
 981            config: config_with_host_conflict,
 982            tcp_connection: Some(tcp_connection_with_host),
 983        };
 984
 985        let result_host = adapter
 986            .get_installed_binary(
 987                &test_mocks::MockDelegate::new(),
 988                &task_def_host,
 989                None,
 990                None,
 991                None,
 992                Some("python3".to_string()),
 993            )
 994            .await;
 995
 996        assert!(result_host.is_err());
 997        assert!(
 998            result_host
 999                .unwrap_err()
1000                .to_string()
1001                .contains("Cannot have two different hosts")
1002        );
1003    }
1004
1005    #[gpui::test]
1006    async fn test_attach_with_connect_mode_generates_correct_arguments() {
1007        let host = Ipv4Addr::new(127, 0, 0, 1);
1008        let port = 5678;
1009
1010        let args_without_host = PythonDebugAdapter::generate_debugpy_arguments(
1011            &host,
1012            port,
1013            DebugpyLaunchMode::AttachWithConnect { host: None },
1014            None,
1015            None,
1016        )
1017        .await
1018        .unwrap();
1019
1020        let expected_suffix = path!("debug_adapters/Debugpy/debugpy/adapter");
1021        assert!(args_without_host[0].ends_with(expected_suffix));
1022        assert_eq!(args_without_host[1], "connect");
1023        assert_eq!(args_without_host[2], "5678");
1024
1025        let args_with_host = PythonDebugAdapter::generate_debugpy_arguments(
1026            &host,
1027            port,
1028            DebugpyLaunchMode::AttachWithConnect {
1029                host: Some("192.168.1.100"),
1030            },
1031            None,
1032            None,
1033        )
1034        .await
1035        .unwrap();
1036
1037        assert!(args_with_host[0].ends_with(expected_suffix));
1038        assert_eq!(args_with_host[1], "connect");
1039        assert_eq!(args_with_host[2], "192.168.1.100:");
1040        assert_eq!(args_with_host[3], "5678");
1041
1042        let args_normal = PythonDebugAdapter::generate_debugpy_arguments(
1043            &host,
1044            port,
1045            DebugpyLaunchMode::Normal,
1046            None,
1047            None,
1048        )
1049        .await
1050        .unwrap();
1051
1052        assert!(args_normal[0].ends_with(expected_suffix));
1053        assert_eq!(args_normal[1], "--host=127.0.0.1");
1054        assert_eq!(args_normal[2], "--port=5678");
1055        assert!(!args_normal.contains(&"connect".to_string()));
1056    }
1057
1058    #[gpui::test]
1059    async fn test_debugpy_install_path_cases() {
1060        let host = Ipv4Addr::new(127, 0, 0, 1);
1061        let port = 5678;
1062
1063        // Case 1: User-defined debugpy path (highest precedence)
1064        let user_path = PathBuf::from("/custom/path/to/debugpy/src/debugpy/adapter");
1065        let user_args = PythonDebugAdapter::generate_debugpy_arguments(
1066            &host,
1067            port,
1068            DebugpyLaunchMode::Normal,
1069            Some(&user_path),
1070            None,
1071        )
1072        .await
1073        .unwrap();
1074
1075        let venv_args = PythonDebugAdapter::generate_debugpy_arguments(
1076            &host,
1077            port,
1078            DebugpyLaunchMode::Normal,
1079            None,
1080            None,
1081        )
1082        .await
1083        .unwrap();
1084
1085        assert_eq!(user_args[0], "/custom/path/to/debugpy/src/debugpy/adapter");
1086        assert_eq!(user_args[1], "--host=127.0.0.1");
1087        assert_eq!(user_args[2], "--port=5678");
1088
1089        let expected_suffix = path!("debug_adapters/Debugpy/debugpy/adapter");
1090        assert!(venv_args[0].ends_with(expected_suffix));
1091        assert_eq!(venv_args[1], "--host=127.0.0.1");
1092        assert_eq!(venv_args[2], "--port=5678");
1093
1094        // The same cases, with arguments overridden by the user
1095        let user_args = PythonDebugAdapter::generate_debugpy_arguments(
1096            &host,
1097            port,
1098            DebugpyLaunchMode::Normal,
1099            Some(&user_path),
1100            Some(vec!["foo".into()]),
1101        )
1102        .await
1103        .unwrap();
1104        let venv_args = PythonDebugAdapter::generate_debugpy_arguments(
1105            &host,
1106            port,
1107            DebugpyLaunchMode::Normal,
1108            None,
1109            Some(vec!["foo".into()]),
1110        )
1111        .await
1112        .unwrap();
1113
1114        assert!(user_args[0].ends_with("src/debugpy/adapter"));
1115        assert_eq!(user_args[1], "foo");
1116
1117        assert!(venv_args[0].ends_with(expected_suffix));
1118        assert_eq!(venv_args[1], "foo");
1119
1120        // Note: Case 3 (GitHub-downloaded debugpy) is not tested since this requires mocking the Github API.
1121    }
1122}