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: &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).await
63 }
64
65 async fn install_binary(
66 &self,
67 version: AdapterVersion,
68 delegate: &dyn DapDelegate,
69 ) -> Result<()> {
70 let version_path = adapters::download_adapter_from_github(
71 self.name(),
72 version,
73 adapters::DownloadedFileType::Zip,
74 delegate,
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: &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 BINARY_NAMES
132 .iter()
133 .filter_map(|cmd| {
134 delegate
135 .which(OsStr::new(cmd))
136 .map(|path| path.to_string_lossy().to_string())
137 })
138 .find(|_| true)
139 };
140
141 Ok(DebugAdapterBinary {
142 command: python_path.ok_or(anyhow!("failed to find binary path for python"))?,
143 arguments: vec![
144 debugpy_dir
145 .join(Self::ADAPTER_PATH)
146 .to_string_lossy()
147 .to_string(),
148 format!("--port={}", port),
149 format!("--host={}", host),
150 ],
151 connection: Some(adapters::TcpArguments {
152 host,
153 port,
154 timeout,
155 }),
156 cwd: None,
157 envs: HashMap::default(),
158 request_args: self.request_args(config),
159 })
160 }
161}
162
163#[async_trait(?Send)]
164impl DebugAdapter for PythonDebugAdapter {
165 fn name(&self) -> DebugAdapterName {
166 DebugAdapterName(Self::ADAPTER_NAME.into())
167 }
168
169 fn adapter_language_name(&self) -> Option<LanguageName> {
170 Some(SharedString::new_static("Python").into())
171 }
172
173 async fn get_binary(
174 &self,
175 delegate: &dyn DapDelegate,
176 config: &DebugTaskDefinition,
177 user_installed_path: Option<PathBuf>,
178 cx: &mut AsyncApp,
179 ) -> Result<DebugAdapterBinary> {
180 if self.checked.set(()).is_ok() {
181 delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
182 if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
183 self.install_binary(version, delegate).await?;
184 }
185 }
186
187 self.get_installed_binary(delegate, &config, user_installed_path, cx)
188 .await
189 }
190}