1use crate::*;
2use dap::{
3 DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition,
4 adapters::InlineValueProvider,
5};
6use gpui::AsyncApp;
7use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
8
9#[derive(Default)]
10pub(crate) struct PythonDebugAdapter;
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}
50
51#[async_trait(?Send)]
52impl DebugAdapter for PythonDebugAdapter {
53 fn name(&self) -> DebugAdapterName {
54 DebugAdapterName(Self::ADAPTER_NAME.into())
55 }
56
57 async fn fetch_latest_adapter_version(
58 &self,
59 delegate: &dyn DapDelegate,
60 ) -> Result<AdapterVersion> {
61 let github_repo = GithubRepo {
62 repo_name: Self::ADAPTER_PACKAGE_NAME.into(),
63 repo_owner: "microsoft".into(),
64 };
65
66 adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await
67 }
68
69 async fn install_binary(
70 &self,
71 version: AdapterVersion,
72 delegate: &dyn DapDelegate,
73 ) -> Result<()> {
74 let version_path = adapters::download_adapter_from_github(
75 self.name(),
76 version,
77 adapters::DownloadedFileType::Zip,
78 delegate,
79 )
80 .await?;
81
82 // only needed when you install the latest version for the first time
83 if let Some(debugpy_dir) =
84 util::fs::find_file_name_in_dir(version_path.as_path(), |file_name| {
85 file_name.starts_with("microsoft-debugpy-")
86 })
87 .await
88 {
89 // TODO Debugger: Rename folder instead of moving all files to another folder
90 // We're doing unnecessary IO work right now
91 util::fs::move_folder_files_to_folder(debugpy_dir.as_path(), version_path.as_path())
92 .await?;
93 }
94
95 Ok(())
96 }
97
98 async fn get_installed_binary(
99 &self,
100 delegate: &dyn DapDelegate,
101 config: &DebugTaskDefinition,
102 user_installed_path: Option<PathBuf>,
103 cx: &mut AsyncApp,
104 ) -> Result<DebugAdapterBinary> {
105 const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
106 let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
107 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
108
109 let debugpy_dir = if let Some(user_installed_path) = user_installed_path {
110 user_installed_path
111 } else {
112 let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
113 let file_name_prefix = format!("{}_", Self::ADAPTER_NAME);
114
115 util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
116 file_name.starts_with(&file_name_prefix)
117 })
118 .await
119 .ok_or_else(|| anyhow!("Debugpy directory not found"))?
120 };
121
122 let toolchain = delegate
123 .toolchain_store()
124 .active_toolchain(
125 delegate.worktree_id(),
126 Arc::from("".as_ref()),
127 language::LanguageName::new(Self::LANGUAGE_NAME),
128 cx,
129 )
130 .await;
131
132 let python_path = if let Some(toolchain) = toolchain {
133 Some(toolchain.path.to_string())
134 } else {
135 BINARY_NAMES
136 .iter()
137 .filter_map(|cmd| {
138 delegate
139 .which(OsStr::new(cmd))
140 .map(|path| path.to_string_lossy().to_string())
141 })
142 .find(|_| true)
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 fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
167 Some(Box::new(PythonInlineValueProvider))
168 }
169}
170
171struct PythonInlineValueProvider;
172
173impl InlineValueProvider for PythonInlineValueProvider {
174 fn provide(&self, variables: Vec<(String, lsp_types::Range)>) -> Vec<lsp_types::InlineValue> {
175 variables
176 .into_iter()
177 .map(|(variable, range)| {
178 if variable.contains(".") || variable.contains("[") {
179 lsp_types::InlineValue::EvaluatableExpression(
180 lsp_types::InlineValueEvaluatableExpression {
181 range,
182 expression: Some(variable),
183 },
184 )
185 } else {
186 lsp_types::InlineValue::VariableLookup(lsp_types::InlineValueVariableLookup {
187 range,
188 variable_name: Some(variable),
189 case_sensitive_lookup: true,
190 })
191 }
192 })
193 .collect()
194 }
195}