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