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