1use crate::*;
2use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
3use gpui::{AsyncApp, SharedString};
4use language::LanguageName;
5use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock};
6use util::ResultExt;
7
8#[derive(Default)]
9pub(crate) struct PythonDebugAdapter {
10 checked: OnceLock<()>,
11}
12
13impl PythonDebugAdapter {
14 const ADAPTER_NAME: &'static str = "Debugpy";
15 const ADAPTER_PACKAGE_NAME: &'static str = "debugpy";
16 const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
17 const LANGUAGE_NAME: &'static str = "Python";
18
19 fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
20 let mut args = json!({
21 "request": match config.request {
22 DebugRequest::Launch(_) => "launch",
23 DebugRequest::Attach(_) => "attach",
24 },
25 "subProcess": true,
26 "redirectOutput": true,
27 });
28 let map = args.as_object_mut().unwrap();
29 match &config.request {
30 DebugRequest::Attach(attach) => {
31 map.insert("processId".into(), attach.process_id.into());
32 }
33 DebugRequest::Launch(launch) => {
34 map.insert("program".into(), launch.program.clone().into());
35 map.insert("args".into(), launch.args.clone().into());
36 if !launch.env.is_empty() {
37 map.insert("env".into(), launch.env_json());
38 }
39
40 if let Some(stop_on_entry) = config.stop_on_entry {
41 map.insert("stopOnEntry".into(), stop_on_entry.into());
42 }
43 if let Some(cwd) = launch.cwd.as_ref() {
44 map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
45 }
46 }
47 }
48 StartDebuggingRequestArguments {
49 configuration: args,
50 request: config.request.to_dap(),
51 }
52 }
53 async fn fetch_latest_adapter_version(
54 &self,
55 delegate: &Arc<dyn DapDelegate>,
56 ) -> Result<AdapterVersion> {
57 let github_repo = GithubRepo {
58 repo_name: Self::ADAPTER_PACKAGE_NAME.into(),
59 repo_owner: "microsoft".into(),
60 };
61
62 adapters::fetch_latest_adapter_version_from_github(github_repo, delegate.as_ref()).await
63 }
64
65 async fn install_binary(
66 &self,
67 version: AdapterVersion,
68 delegate: &Arc<dyn DapDelegate>,
69 ) -> Result<()> {
70 let version_path = adapters::download_adapter_from_github(
71 self.name(),
72 version,
73 adapters::DownloadedFileType::Zip,
74 delegate.as_ref(),
75 )
76 .await?;
77
78 // only needed when you install the latest version for the first time
79 if let Some(debugpy_dir) =
80 util::fs::find_file_name_in_dir(version_path.as_path(), |file_name| {
81 file_name.starts_with("microsoft-debugpy-")
82 })
83 .await
84 {
85 // TODO Debugger: Rename folder instead of moving all files to another folder
86 // We're doing unnecessary IO work right now
87 util::fs::move_folder_files_to_folder(debugpy_dir.as_path(), version_path.as_path())
88 .await?;
89 }
90
91 Ok(())
92 }
93
94 async fn get_installed_binary(
95 &self,
96 delegate: &Arc<dyn DapDelegate>,
97 config: &DebugTaskDefinition,
98 user_installed_path: Option<PathBuf>,
99 cx: &mut AsyncApp,
100 ) -> Result<DebugAdapterBinary> {
101 const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
102 let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
103 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
104
105 let debugpy_dir = if let Some(user_installed_path) = user_installed_path {
106 user_installed_path
107 } else {
108 let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
109 let file_name_prefix = format!("{}_", Self::ADAPTER_NAME);
110
111 util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
112 file_name.starts_with(&file_name_prefix)
113 })
114 .await
115 .ok_or_else(|| anyhow!("Debugpy directory not found"))?
116 };
117
118 let toolchain = delegate
119 .toolchain_store()
120 .active_toolchain(
121 delegate.worktree_id(),
122 Arc::from("".as_ref()),
123 language::LanguageName::new(Self::LANGUAGE_NAME),
124 cx,
125 )
126 .await;
127
128 let python_path = if let Some(toolchain) = toolchain {
129 Some(toolchain.path.to_string())
130 } else {
131 let mut name = None;
132
133 for cmd in BINARY_NAMES {
134 name = delegate
135 .which(OsStr::new(cmd))
136 .await
137 .map(|path| path.to_string_lossy().to_string());
138 if name.is_some() {
139 break;
140 }
141 }
142 name
143 };
144
145 Ok(DebugAdapterBinary {
146 command: python_path.ok_or(anyhow!("failed to find binary path for python"))?,
147 arguments: vec![
148 debugpy_dir
149 .join(Self::ADAPTER_PATH)
150 .to_string_lossy()
151 .to_string(),
152 format!("--port={}", port),
153 format!("--host={}", host),
154 ],
155 connection: Some(adapters::TcpArguments {
156 host,
157 port,
158 timeout,
159 }),
160 cwd: None,
161 envs: HashMap::default(),
162 request_args: self.request_args(config),
163 })
164 }
165}
166
167#[async_trait(?Send)]
168impl DebugAdapter for PythonDebugAdapter {
169 fn name(&self) -> DebugAdapterName {
170 DebugAdapterName(Self::ADAPTER_NAME.into())
171 }
172
173 fn adapter_language_name(&self) -> Option<LanguageName> {
174 Some(SharedString::new_static("Python").into())
175 }
176
177 async fn get_binary(
178 &self,
179 delegate: &Arc<dyn DapDelegate>,
180 config: &DebugTaskDefinition,
181 user_installed_path: Option<PathBuf>,
182 cx: &mut AsyncApp,
183 ) -> Result<DebugAdapterBinary> {
184 if self.checked.set(()).is_ok() {
185 delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
186 if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
187 self.install_binary(version, delegate).await?;
188 }
189 }
190
191 self.get_installed_binary(delegate, &config, user_installed_path, cx)
192 .await
193 }
194}