1use crate::*;
2use anyhow::bail;
3use dap::DebugRequestType;
4use gpui::AsyncApp;
5use std::{ffi::OsStr, path::PathBuf};
6use task::DebugTaskDefinition;
7
8#[derive(Default)]
9pub(crate) struct PythonDebugAdapter;
10
11impl PythonDebugAdapter {
12 const ADAPTER_NAME: &'static str = "Debugpy";
13 const ADAPTER_PACKAGE_NAME: &'static str = "debugpy";
14 const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
15 const LANGUAGE_NAME: &'static str = "Python";
16}
17
18#[async_trait(?Send)]
19impl DebugAdapter for PythonDebugAdapter {
20 fn name(&self) -> DebugAdapterName {
21 DebugAdapterName(Self::ADAPTER_NAME.into())
22 }
23
24 async fn fetch_latest_adapter_version(
25 &self,
26 delegate: &dyn DapDelegate,
27 ) -> Result<AdapterVersion> {
28 let github_repo = GithubRepo {
29 repo_name: Self::ADAPTER_PACKAGE_NAME.into(),
30 repo_owner: "microsoft".into(),
31 };
32
33 adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await
34 }
35
36 async fn install_binary(
37 &self,
38 version: AdapterVersion,
39 delegate: &dyn DapDelegate,
40 ) -> Result<()> {
41 let version_path = adapters::download_adapter_from_github(
42 self.name(),
43 version,
44 adapters::DownloadedFileType::Zip,
45 delegate,
46 )
47 .await?;
48
49 // only needed when you install the latest version for the first time
50 if let Some(debugpy_dir) =
51 util::fs::find_file_name_in_dir(version_path.as_path(), |file_name| {
52 file_name.starts_with("microsoft-debugpy-")
53 })
54 .await
55 {
56 // TODO Debugger: Rename folder instead of moving all files to another folder
57 // We're doing unnecessary IO work right now
58 util::fs::move_folder_files_to_folder(debugpy_dir.as_path(), version_path.as_path())
59 .await?;
60 }
61
62 Ok(())
63 }
64
65 async fn get_installed_binary(
66 &self,
67 delegate: &dyn DapDelegate,
68 config: &DebugAdapterConfig,
69 user_installed_path: Option<PathBuf>,
70 cx: &mut AsyncApp,
71 ) -> Result<DebugAdapterBinary> {
72 const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
73 let Some(tcp_connection) = config.tcp_connection.clone() else {
74 bail!("Python Debug Adapter expects tcp connection arguments to be provided");
75 };
76 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
77
78 let debugpy_dir = if let Some(user_installed_path) = user_installed_path {
79 user_installed_path
80 } else {
81 let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
82 let file_name_prefix = format!("{}_", Self::ADAPTER_PACKAGE_NAME);
83
84 util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
85 file_name.starts_with(&file_name_prefix)
86 })
87 .await
88 .ok_or_else(|| anyhow!("Debugpy directory not found"))?
89 };
90
91 let toolchain = delegate
92 .toolchain_store()
93 .active_toolchain(
94 delegate.worktree_id(),
95 language::LanguageName::new(Self::LANGUAGE_NAME),
96 cx,
97 )
98 .await;
99
100 let python_path = if let Some(toolchain) = toolchain {
101 Some(toolchain.path.to_string())
102 } else {
103 BINARY_NAMES
104 .iter()
105 .filter_map(|cmd| {
106 delegate
107 .which(OsStr::new(cmd))
108 .map(|path| path.to_string_lossy().to_string())
109 })
110 .find(|_| true)
111 };
112
113 Ok(DebugAdapterBinary {
114 command: python_path.ok_or(anyhow!("failed to find binary path for python"))?,
115 arguments: Some(vec![
116 debugpy_dir.join(Self::ADAPTER_PATH).into(),
117 format!("--port={}", port).into(),
118 format!("--host={}", host).into(),
119 ]),
120 connection: Some(adapters::TcpArguments {
121 host,
122 port,
123 timeout,
124 }),
125 cwd: None,
126 envs: None,
127 })
128 }
129
130 fn request_args(&self, config: &DebugTaskDefinition) -> Value {
131 match &config.request {
132 DebugRequestType::Launch(launch_config) => {
133 json!({
134 "program": launch_config.program,
135 "subProcess": true,
136 "cwd": launch_config.cwd,
137 "redirectOutput": true,
138 })
139 }
140 dap::DebugRequestType::Attach(attach_config) => {
141 json!({
142 "subProcess": true,
143 "redirectOutput": true,
144 "processId": attach_config.process_id
145 })
146 }
147 }
148 }
149}