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
 26#[derive(Default)]
 27pub(crate) struct PythonDebugAdapter {
 28    base_venv_path: OnceCell<Result<Arc<Path>, String>>,
 29    debugpy_whl_base_path: OnceCell<Result<Arc<Path>, String>>,
 30}
 31
 32impl PythonDebugAdapter {
 33    const ADAPTER_NAME: &'static str = "Debugpy";
 34    const DEBUG_ADAPTER_NAME: DebugAdapterName =
 35        DebugAdapterName(SharedString::new_static(Self::ADAPTER_NAME));
 36
 37    const LANGUAGE_NAME: &'static str = "Python";
 38
 39    async fn generate_debugpy_arguments(
 40        host: &Ipv4Addr,
 41        port: u16,
 42        user_installed_path: Option<&Path>,
 43        user_args: Option<Vec<String>>,
 44    ) -> Result<Vec<String>> {
 45        let mut args = if let Some(user_installed_path) = user_installed_path {
 46            log::debug!(
 47                "Using user-installed debugpy adapter from: {}",
 48                user_installed_path.display()
 49            );
 50            vec![user_installed_path.to_string_lossy().into_owned()]
 51        } else {
 52            let adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref());
 53            let path = adapter_path
 54                .join("debugpy")
 55                .join("adapter")
 56                .to_string_lossy()
 57                .into_owned();
 58            log::debug!("Using pip debugpy adapter from: {path}");
 59            vec![path]
 60        };
 61
 62        args.extend(if let Some(args) = user_args {
 63            args
 64        } else {
 65            vec![format!("--host={}", host), format!("--port={}", port)]
 66        });
 67        Ok(args)
 68    }
 69
 70    async fn request_args(
 71        &self,
 72        delegate: &Arc<dyn DapDelegate>,
 73        task_definition: &DebugTaskDefinition,
 74    ) -> Result<StartDebuggingRequestArguments> {
 75        let request = self.request_kind(&task_definition.config).await?;
 76
 77        let mut configuration = task_definition.config.clone();
 78        if let Ok(console) = configuration.dot_get_mut("console") {
 79            // Use built-in Zed terminal if user did not explicitly provide a setting for console.
 80            if console.is_null() {
 81                *console = Value::String("integratedTerminal".into());
 82            }
 83        }
 84
 85        if let Some(obj) = configuration.as_object_mut() {
 86            obj.entry("cwd")
 87                .or_insert(delegate.worktree_root_path().to_string_lossy().into());
 88        }
 89
 90        Ok(StartDebuggingRequestArguments {
 91            configuration,
 92            request,
 93        })
 94    }
 95
 96    async fn fetch_wheel(
 97        &self,
 98        toolchain: Option<Toolchain>,
 99        delegate: &Arc<dyn DapDelegate>,
100    ) -> Result<Arc<Path>> {
101        let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME).join("wheels");
102        std::fs::create_dir_all(&download_dir)?;
103        let venv_python = self.base_venv_path(toolchain, delegate).await?;
104
105        let installation_succeeded = util::command::new_smol_command(venv_python.as_ref())
106            .args([
107                "-m",
108                "pip",
109                "download",
110                "debugpy",
111                "--only-binary=:all:",
112                "-d",
113                download_dir.to_string_lossy().as_ref(),
114            ])
115            .output()
116            .await
117            .context("spawn system python")?
118            .status
119            .success();
120        if !installation_succeeded {
121            bail!("debugpy installation failed (could not fetch Debugpy's wheel)");
122        }
123
124        let wheel_path = std::fs::read_dir(&download_dir)?
125            .find_map(|entry| {
126                entry.ok().filter(|e| {
127                    e.file_type().is_ok_and(|typ| typ.is_file())
128                        && Path::new(&e.file_name()).extension() == Some("whl".as_ref())
129                })
130            })
131            .with_context(|| format!("Did not find a .whl in {download_dir:?}"))?;
132
133        util::archive::extract_zip(
134            &debug_adapters_dir().join(Self::ADAPTER_NAME),
135            File::open(&wheel_path.path()).await?,
136        )
137        .await?;
138
139        Ok(Arc::from(wheel_path.path()))
140    }
141
142    async fn maybe_fetch_new_wheel(
143        &self,
144        toolchain: Option<Toolchain>,
145        delegate: &Arc<dyn DapDelegate>,
146    ) -> Result<()> {
147        let latest_release = delegate
148            .http_client()
149            .get(
150                "https://pypi.org/pypi/debugpy/json",
151                AsyncBody::empty(),
152                false,
153            )
154            .await
155            .log_err();
156        let response = latest_release
157            .filter(|response| response.status().is_success())
158            .context("getting latest release")?;
159
160        let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
161        std::fs::create_dir_all(&download_dir)?;
162
163        let mut output = String::new();
164        response.into_body().read_to_string(&mut output).await?;
165        let as_json = serde_json::Value::from_str(&output)?;
166        let latest_version = as_json
167            .get("info")
168            .and_then(|info| {
169                info.get("version")
170                    .and_then(|version| version.as_str())
171                    .map(ToOwned::to_owned)
172            })
173            .context("parsing latest release information")?;
174        let dist_info_dirname: OsString = format!("debugpy-{latest_version}.dist-info").into();
175        let is_up_to_date = delegate
176            .fs()
177            .read_dir(&debug_adapters_dir().join(Self::ADAPTER_NAME))
178            .await?
179            .into_stream()
180            .any(async |entry| {
181                entry.is_ok_and(|e| e.file_name().is_some_and(|name| name == dist_info_dirname))
182            })
183            .await;
184
185        if !is_up_to_date {
186            delegate
187                .fs()
188                .remove_dir(
189                    &debug_adapters_dir().join(Self::ADAPTER_NAME),
190                    RemoveOptions {
191                        recursive: true,
192                        ignore_if_not_exists: true,
193                    },
194                )
195                .await?;
196            self.fetch_wheel(toolchain, delegate).await?;
197        }
198        anyhow::Ok(())
199    }
200
201    async fn fetch_debugpy_whl(
202        &self,
203        toolchain: Option<Toolchain>,
204        delegate: &Arc<dyn DapDelegate>,
205    ) -> Result<Arc<Path>, String> {
206        self.debugpy_whl_base_path
207            .get_or_init(|| async move {
208                self.maybe_fetch_new_wheel(toolchain, delegate)
209                    .await
210                    .map_err(|e| format!("{e}"))?;
211                Ok(Arc::from(
212                    debug_adapters_dir()
213                        .join(Self::ADAPTER_NAME)
214                        .join("debugpy")
215                        .join("adapter")
216                        .as_ref(),
217                ))
218            })
219            .await
220            .clone()
221    }
222
223    async fn base_venv_path(
224        &self,
225        toolchain: Option<Toolchain>,
226        delegate: &Arc<dyn DapDelegate>,
227    ) -> Result<Arc<Path>> {
228        let result = self.base_venv_path
229            .get_or_init(|| async {
230                let base_python = if let Some(toolchain) = toolchain {
231                    toolchain.path.to_string()
232                } else {
233                    Self::system_python_name(delegate).await.ok_or_else(|| {
234                        let mut message = "Could not find a Python installation".to_owned();
235                        if cfg!(windows){
236                            message.push_str(". Install Python from the Microsoft Store, or manually from https://www.python.org/downloads/windows.")
237                        }
238                        message
239                    })?
240                };
241
242                let debug_adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref());
243                let output = util::command::new_smol_command(&base_python)
244                    .args(["-m", "venv", "zed_base_venv"])
245                    .current_dir(
246                        &debug_adapter_path,
247                    )
248                    .spawn()
249                    .map_err(|e| format!("{e:#?}"))?
250                    .output()
251                    .await
252                    .map_err(|e| format!("{e:#?}"))?;
253
254                if !output.status.success() {
255                    let stderr = String::from_utf8_lossy(&output.stderr);
256                    let stdout = String::from_utf8_lossy(&output.stdout);
257                    let debug_adapter_path = debug_adapter_path.display();
258                    return Err(format!("Failed to create base virtual environment with {base_python} in:\n{debug_adapter_path}\nstderr:\n{stderr}\nstdout:\n{stdout}\n"));
259                }
260
261                const PYTHON_PATH: &str = if cfg!(target_os = "windows") {
262                    "Scripts/python.exe"
263                } else {
264                    "bin/python3"
265                };
266                Ok(Arc::from(
267                    paths::debug_adapters_dir()
268                        .join(Self::DEBUG_ADAPTER_NAME.as_ref())
269                        .join("zed_base_venv")
270                        .join(PYTHON_PATH)
271                        .as_ref(),
272                ))
273            })
274            .await
275            .clone();
276        match result {
277            Ok(path) => Ok(path),
278            Err(e) => Err(anyhow::anyhow!("{e}")),
279        }
280    }
281    async fn system_python_name(delegate: &Arc<dyn DapDelegate>) -> Option<String> {
282        const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
283        let mut name = None;
284
285        for cmd in BINARY_NAMES {
286            let Some(path) = delegate.which(OsStr::new(cmd)).await else {
287                continue;
288            };
289            // Try to detect situations where `python3` exists but is not a real Python interpreter.
290            // Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app
291            // when run with no arguments, and just fails otherwise.
292            let Some(output) = new_smol_command(&path)
293                .args(["-c", "print(1 + 2)"])
294                .output()
295                .await
296                .ok()
297            else {
298                continue;
299            };
300            if output.stdout.trim_ascii() != b"3" {
301                continue;
302            }
303            name = Some(path.to_string_lossy().into_owned());
304            break;
305        }
306        name
307    }
308
309    async fn get_installed_binary(
310        &self,
311        delegate: &Arc<dyn DapDelegate>,
312        config: &DebugTaskDefinition,
313        user_installed_path: Option<PathBuf>,
314        user_args: Option<Vec<String>>,
315        user_env: Option<HashMap<String, String>>,
316        python_from_toolchain: Option<String>,
317    ) -> Result<DebugAdapterBinary> {
318        let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
319        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
320
321        let python_path = if let Some(toolchain) = python_from_toolchain {
322            Some(toolchain)
323        } else {
324            Self::system_python_name(delegate).await
325        };
326
327        let python_command = python_path.context("failed to find binary path for Python")?;
328        log::debug!("Using Python executable: {}", python_command);
329
330        let arguments = Self::generate_debugpy_arguments(
331            &host,
332            port,
333            user_installed_path.as_deref(),
334            user_args,
335        )
336        .await?;
337
338        log::debug!(
339            "Starting debugpy adapter with command: {} {}",
340            python_command,
341            arguments.join(" ")
342        );
343
344        Ok(DebugAdapterBinary {
345            command: Some(python_command),
346            arguments,
347            connection: Some(adapters::TcpArguments {
348                host,
349                port,
350                timeout,
351            }),
352            cwd: Some(delegate.worktree_root_path().to_path_buf()),
353            envs: user_env.unwrap_or_default(),
354            request_args: self.request_args(delegate, config).await?,
355        })
356    }
357}
358
359#[async_trait(?Send)]
360impl DebugAdapter for PythonDebugAdapter {
361    fn name(&self) -> DebugAdapterName {
362        Self::DEBUG_ADAPTER_NAME
363    }
364
365    fn adapter_language_name(&self) -> Option<LanguageName> {
366        Some(SharedString::new_static("Python").into())
367    }
368
369    async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
370        let mut args = json!({
371            "request": match zed_scenario.request {
372                DebugRequest::Launch(_) => "launch",
373                DebugRequest::Attach(_) => "attach",
374            },
375            "subProcess": true,
376            "redirectOutput": true,
377        });
378
379        let map = args.as_object_mut().unwrap();
380        match &zed_scenario.request {
381            DebugRequest::Attach(attach) => {
382                map.insert("processId".into(), attach.process_id.into());
383            }
384            DebugRequest::Launch(launch) => {
385                map.insert("program".into(), launch.program.clone().into());
386                map.insert("args".into(), launch.args.clone().into());
387                if !launch.env.is_empty() {
388                    map.insert("env".into(), launch.env_json());
389                }
390
391                if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
392                    map.insert("stopOnEntry".into(), stop_on_entry.into());
393                }
394                if let Some(cwd) = launch.cwd.as_ref() {
395                    map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
396                }
397            }
398        }
399
400        Ok(DebugScenario {
401            adapter: zed_scenario.adapter,
402            label: zed_scenario.label,
403            config: args,
404            build: None,
405            tcp_connection: None,
406        })
407    }
408
409    fn dap_schema(&self) -> serde_json::Value {
410        json!({
411            "properties": {
412                "request": {
413                    "type": "string",
414                    "enum": ["attach", "launch"],
415                    "description": "Debug adapter request type"
416                },
417                "autoReload": {
418                    "default": {},
419                    "description": "Configures automatic reload of code on edit.",
420                    "properties": {
421                        "enable": {
422                            "default": false,
423                            "description": "Automatically reload code on edit.",
424                            "type": "boolean"
425                        },
426                        "exclude": {
427                            "default": [
428                                "**/.git/**",
429                                "**/.metadata/**",
430                                "**/__pycache__/**",
431                                "**/node_modules/**",
432                                "**/site-packages/**"
433                            ],
434                            "description": "Glob patterns of paths to exclude from auto reload.",
435                            "items": {
436                                "type": "string"
437                            },
438                            "type": "array"
439                        },
440                        "include": {
441                            "default": [
442                                "**/*.py",
443                                "**/*.pyw"
444                            ],
445                            "description": "Glob patterns of paths to include in auto reload.",
446                            "items": {
447                                "type": "string"
448                            },
449                            "type": "array"
450                        }
451                    },
452                    "type": "object"
453                },
454                "debugAdapterPath": {
455                    "description": "Path (fully qualified) to the python debug adapter executable.",
456                    "type": "string"
457                },
458                "django": {
459                    "default": false,
460                    "description": "Django debugging.",
461                    "type": "boolean"
462                },
463                "jinja": {
464                    "default": null,
465                    "description": "Jinja template debugging (e.g. Flask).",
466                    "enum": [
467                        false,
468                        null,
469                        true
470                    ]
471                },
472                "justMyCode": {
473                    "default": true,
474                    "description": "If true, show and debug only user-written code. If false, show and debug all code, including library calls.",
475                    "type": "boolean"
476                },
477                "logToFile": {
478                    "default": false,
479                    "description": "Enable logging of debugger events to a log file. This file can be found in the debugpy extension install folder.",
480                    "type": "boolean"
481                },
482                "pathMappings": {
483                    "default": [],
484                    "items": {
485                        "label": "Path mapping",
486                        "properties": {
487                            "localRoot": {
488                                "default": "${ZED_WORKTREE_ROOT}",
489                                "label": "Local source root.",
490                                "type": "string"
491                            },
492                            "remoteRoot": {
493                                "default": "",
494                                "label": "Remote source root.",
495                                "type": "string"
496                            }
497                        },
498                        "required": [
499                            "localRoot",
500                            "remoteRoot"
501                        ],
502                        "type": "object"
503                    },
504                    "label": "Path mappings.",
505                    "type": "array"
506                },
507                "redirectOutput": {
508                    "default": true,
509                    "description": "Redirect output.",
510                    "type": "boolean"
511                },
512                "showReturnValue": {
513                    "default": true,
514                    "description": "Show return value of functions when stepping.",
515                    "type": "boolean"
516                },
517                "subProcess": {
518                    "default": false,
519                    "description": "Whether to enable Sub Process debugging",
520                    "type": "boolean"
521                },
522                "consoleName": {
523                    "default": "Python Debug Console",
524                    "description": "Display name of the debug console or terminal",
525                    "type": "string"
526                },
527                "clientOS": {
528                    "default": null,
529                    "description": "OS that VS code is using.",
530                    "enum": [
531                        "windows",
532                        null,
533                        "unix"
534                    ]
535                }
536            },
537            "required": ["request"],
538            "allOf": [
539                {
540                    "if": {
541                        "properties": {
542                            "request": {
543                                "enum": ["attach"]
544                            }
545                        }
546                    },
547                    "then": {
548                        "properties": {
549                            "connect": {
550                                "label": "Attach by connecting to debugpy over a socket.",
551                                "properties": {
552                                    "host": {
553                                        "default": "127.0.0.1",
554                                        "description": "Hostname or IP address to connect to.",
555                                        "type": "string"
556                                    },
557                                    "port": {
558                                        "description": "Port to connect to.",
559                                        "type": [
560                                            "number",
561                                            "string"
562                                        ]
563                                    }
564                                },
565                                "required": [
566                                    "port"
567                                ],
568                                "type": "object"
569                            },
570                            "listen": {
571                                "label": "Attach by listening for incoming socket connection from debugpy",
572                                "properties": {
573                                    "host": {
574                                        "default": "127.0.0.1",
575                                        "description": "Hostname or IP address of the interface to listen on.",
576                                        "type": "string"
577                                    },
578                                    "port": {
579                                        "description": "Port to listen on.",
580                                        "type": [
581                                            "number",
582                                            "string"
583                                        ]
584                                    }
585                                },
586                                "required": [
587                                    "port"
588                                ],
589                                "type": "object"
590                            },
591                            "processId": {
592                                "anyOf": [
593                                    {
594                                        "default": "${command:pickProcess}",
595                                        "description": "Use process picker to select a process to attach, or Process ID as integer.",
596                                        "enum": [
597                                            "${command:pickProcess}"
598                                        ]
599                                    },
600                                    {
601                                        "description": "ID of the local process to attach to.",
602                                        "type": "integer"
603                                    }
604                                ]
605                            }
606                        }
607                    }
608                },
609                {
610                    "if": {
611                        "properties": {
612                            "request": {
613                                "enum": ["launch"]
614                            }
615                        }
616                    },
617                    "then": {
618                        "properties": {
619                            "args": {
620                                "default": [],
621                                "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.",
622                                "items": {
623                                    "type": "string"
624                                },
625                                "anyOf": [
626                                    {
627                                        "default": "${command:pickArgs}",
628                                        "enum": [
629                                            "${command:pickArgs}"
630                                        ]
631                                    },
632                                    {
633                                        "type": [
634                                            "array",
635                                            "string"
636                                        ]
637                                    }
638                                ]
639                            },
640                            "console": {
641                                "default": "integratedTerminal",
642                                "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.",
643                                "enum": [
644                                    "externalTerminal",
645                                    "integratedTerminal",
646                                    "internalConsole"
647                                ]
648                            },
649                            "cwd": {
650                                "default": "${ZED_WORKTREE_ROOT}",
651                                "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).",
652                                "type": "string"
653                            },
654                            "autoStartBrowser": {
655                                "default": false,
656                                "description": "Open external browser to launch the application",
657                                "type": "boolean"
658                            },
659                            "env": {
660                                "additionalProperties": {
661                                    "type": "string"
662                                },
663                                "default": {},
664                                "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.",
665                                "type": "object"
666                            },
667                            "envFile": {
668                                "default": "${ZED_WORKTREE_ROOT}/.env",
669                                "description": "Absolute path to a file containing environment variable definitions.",
670                                "type": "string"
671                            },
672                            "gevent": {
673                                "default": false,
674                                "description": "Enable debugging of gevent monkey-patched code.",
675                                "type": "boolean"
676                            },
677                            "module": {
678                                "default": "",
679                                "description": "Name of the module to be debugged.",
680                                "type": "string"
681                            },
682                            "program": {
683                                "default": "${ZED_FILE}",
684                                "description": "Absolute path to the program.",
685                                "type": "string"
686                            },
687                            "purpose": {
688                                "default": [],
689                                "description": "Tells extension to use this configuration for test debugging, or when using debug-in-terminal command.",
690                                "items": {
691                                    "enum": [
692                                        "debug-test",
693                                        "debug-in-terminal"
694                                    ],
695                                    "enumDescriptions": [
696                                        "Use this configuration while debugging tests using test view or test debug commands.",
697                                        "Use this configuration while debugging a file using debug in terminal button in the editor."
698                                    ]
699                                },
700                                "type": "array"
701                            },
702                            "pyramid": {
703                                "default": false,
704                                "description": "Whether debugging Pyramid applications.",
705                                "type": "boolean"
706                            },
707                            "python": {
708                                "default": "${command:python.interpreterPath}",
709                                "description": "Absolute path to the Python interpreter executable; overrides workspace configuration if set.",
710                                "type": "string"
711                            },
712                            "pythonArgs": {
713                                "default": [],
714                                "description": "Command-line arguments passed to the Python interpreter. To pass arguments to the debug target, use \"args\".",
715                                "items": {
716                                    "type": "string"
717                                },
718                                "type": "array"
719                            },
720                            "stopOnEntry": {
721                                "default": false,
722                                "description": "Automatically stop after launch.",
723                                "type": "boolean"
724                            },
725                            "sudo": {
726                                "default": false,
727                                "description": "Running debug program under elevated permissions (on Unix).",
728                                "type": "boolean"
729                            },
730                            "guiEventLoop": {
731                                "default": "matplotlib",
732                                "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.",
733                                "type": "string"
734                            }
735                        }
736                    }
737                }
738            ]
739        })
740    }
741
742    async fn get_binary(
743        &self,
744        delegate: &Arc<dyn DapDelegate>,
745        config: &DebugTaskDefinition,
746        user_installed_path: Option<PathBuf>,
747        user_args: Option<Vec<String>>,
748        user_env: Option<HashMap<String, String>>,
749        cx: &mut AsyncApp,
750    ) -> Result<DebugAdapterBinary> {
751        if let Some(local_path) = &user_installed_path {
752            log::debug!(
753                "Using user-installed debugpy adapter from: {}",
754                local_path.display()
755            );
756            return self
757                .get_installed_binary(
758                    delegate,
759                    config,
760                    Some(local_path.clone()),
761                    user_args,
762                    user_env,
763                    None,
764                )
765                .await;
766        }
767
768        let base_path = config
769            .config
770            .get("cwd")
771            .and_then(|cwd| {
772                RelPath::new(
773                    cwd.as_str()
774                        .map(Path::new)?
775                        .strip_prefix(delegate.worktree_root_path())
776                        .ok()?,
777                    PathStyle::local(),
778                )
779                .ok()
780            })
781            .unwrap_or_else(|| RelPath::empty().into());
782        let toolchain = delegate
783            .toolchain_store()
784            .active_toolchain(
785                delegate.worktree_id(),
786                base_path.into_arc(),
787                language::LanguageName::new(Self::LANGUAGE_NAME),
788                cx,
789            )
790            .await;
791
792        self.fetch_debugpy_whl(toolchain.clone(), delegate)
793            .await
794            .map_err(|e| anyhow::anyhow!("{e}"))?;
795        if let Some(toolchain) = &toolchain {
796            return self
797                .get_installed_binary(
798                    delegate,
799                    config,
800                    None,
801                    user_args,
802                    user_env,
803                    Some(toolchain.path.to_string()),
804                )
805                .await;
806        }
807
808        self.get_installed_binary(delegate, config, None, user_args, user_env, None)
809            .await
810    }
811
812    fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
813        let label = args
814            .configuration
815            .get("name")?
816            .as_str()
817            .filter(|label| !label.is_empty())?;
818        Some(label.to_owned())
819    }
820}
821
822#[cfg(test)]
823mod tests {
824    use util::path;
825
826    use super::*;
827    use std::{net::Ipv4Addr, path::PathBuf};
828
829    #[gpui::test]
830    async fn test_debugpy_install_path_cases() {
831        let host = Ipv4Addr::new(127, 0, 0, 1);
832        let port = 5678;
833
834        // Case 1: User-defined debugpy path (highest precedence)
835        let user_path = PathBuf::from("/custom/path/to/debugpy/src/debugpy/adapter");
836        let user_args =
837            PythonDebugAdapter::generate_debugpy_arguments(&host, port, Some(&user_path), None)
838                .await
839                .unwrap();
840
841        // Case 2: Venv-installed debugpy (uses -m debugpy.adapter)
842        let venv_args = PythonDebugAdapter::generate_debugpy_arguments(&host, port, None, None)
843            .await
844            .unwrap();
845
846        assert_eq!(user_args[0], "/custom/path/to/debugpy/src/debugpy/adapter");
847        assert_eq!(user_args[1], "--host=127.0.0.1");
848        assert_eq!(user_args[2], "--port=5678");
849
850        let expected_suffix = path!("debug_adapters/Debugpy/debugpy/adapter");
851        assert!(venv_args[0].ends_with(expected_suffix));
852        assert_eq!(venv_args[1], "--host=127.0.0.1");
853        assert_eq!(venv_args[2], "--port=5678");
854
855        // The same cases, with arguments overridden by the user
856        let user_args = PythonDebugAdapter::generate_debugpy_arguments(
857            &host,
858            port,
859            Some(&user_path),
860            Some(vec!["foo".into()]),
861        )
862        .await
863        .unwrap();
864        let venv_args = PythonDebugAdapter::generate_debugpy_arguments(
865            &host,
866            port,
867            None,
868            Some(vec!["foo".into()]),
869        )
870        .await
871        .unwrap();
872
873        assert!(user_args[0].ends_with("src/debugpy/adapter"));
874        assert_eq!(user_args[1], "foo");
875
876        assert!(venv_args[0].ends_with(expected_suffix));
877        assert_eq!(venv_args[1], "foo");
878
879        // Note: Case 3 (GitHub-downloaded debugpy) is not tested since this requires mocking the Github API.
880    }
881}