python.rs

  1use crate::*;
  2use anyhow::{Context as _, anyhow};
  3use dap::{
  4    DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
  5    adapters::DebugTaskDefinition,
  6};
  7use gpui::{AsyncApp, SharedString};
  8use language::LanguageName;
  9use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock};
 10use util::ResultExt;
 11
 12#[derive(Default)]
 13pub(crate) struct PythonDebugAdapter {
 14    checked: OnceLock<()>,
 15}
 16
 17impl PythonDebugAdapter {
 18    const ADAPTER_NAME: &'static str = "Debugpy";
 19    const ADAPTER_PACKAGE_NAME: &'static str = "debugpy";
 20    const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
 21    const LANGUAGE_NAME: &'static str = "Python";
 22
 23    fn request_args(
 24        &self,
 25        task_definition: &DebugTaskDefinition,
 26    ) -> Result<StartDebuggingRequestArguments> {
 27        let request = self.validate_config(&task_definition.config)?;
 28
 29        Ok(StartDebuggingRequestArguments {
 30            configuration: task_definition.config.clone(),
 31            request,
 32        })
 33    }
 34    async fn fetch_latest_adapter_version(
 35        &self,
 36        delegate: &Arc<dyn DapDelegate>,
 37    ) -> Result<AdapterVersion> {
 38        let github_repo = GithubRepo {
 39            repo_name: Self::ADAPTER_PACKAGE_NAME.into(),
 40            repo_owner: "microsoft".into(),
 41        };
 42
 43        adapters::fetch_latest_adapter_version_from_github(github_repo, delegate.as_ref()).await
 44    }
 45
 46    async fn install_binary(
 47        &self,
 48        version: AdapterVersion,
 49        delegate: &Arc<dyn DapDelegate>,
 50    ) -> Result<()> {
 51        let version_path = adapters::download_adapter_from_github(
 52            self.name(),
 53            version,
 54            adapters::DownloadedFileType::Zip,
 55            delegate.as_ref(),
 56        )
 57        .await?;
 58
 59        // only needed when you install the latest version for the first time
 60        if let Some(debugpy_dir) =
 61            util::fs::find_file_name_in_dir(version_path.as_path(), |file_name| {
 62                file_name.starts_with("microsoft-debugpy-")
 63            })
 64            .await
 65        {
 66            // TODO Debugger: Rename folder instead of moving all files to another folder
 67            // We're doing unnecessary IO work right now
 68            util::fs::move_folder_files_to_folder(debugpy_dir.as_path(), version_path.as_path())
 69                .await?;
 70        }
 71
 72        Ok(())
 73    }
 74
 75    async fn get_installed_binary(
 76        &self,
 77        delegate: &Arc<dyn DapDelegate>,
 78        config: &DebugTaskDefinition,
 79        user_installed_path: Option<PathBuf>,
 80        cx: &mut AsyncApp,
 81    ) -> Result<DebugAdapterBinary> {
 82        const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
 83        let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
 84        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 85
 86        let debugpy_dir = if let Some(user_installed_path) = user_installed_path {
 87            user_installed_path
 88        } else {
 89            let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
 90            let file_name_prefix = format!("{}_", Self::ADAPTER_NAME);
 91
 92            util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
 93                file_name.starts_with(&file_name_prefix)
 94            })
 95            .await
 96            .context("Debugpy directory not found")?
 97        };
 98
 99        let toolchain = delegate
100            .toolchain_store()
101            .active_toolchain(
102                delegate.worktree_id(),
103                Arc::from("".as_ref()),
104                language::LanguageName::new(Self::LANGUAGE_NAME),
105                cx,
106            )
107            .await;
108
109        let python_path = if let Some(toolchain) = toolchain {
110            Some(toolchain.path.to_string())
111        } else {
112            let mut name = None;
113
114            for cmd in BINARY_NAMES {
115                name = delegate
116                    .which(OsStr::new(cmd))
117                    .await
118                    .map(|path| path.to_string_lossy().to_string());
119                if name.is_some() {
120                    break;
121                }
122            }
123            name
124        };
125
126        Ok(DebugAdapterBinary {
127            command: python_path.context("failed to find binary path for Python")?,
128            arguments: vec![
129                debugpy_dir
130                    .join(Self::ADAPTER_PATH)
131                    .to_string_lossy()
132                    .to_string(),
133                format!("--port={}", port),
134                format!("--host={}", host),
135            ],
136            connection: Some(adapters::TcpArguments {
137                host,
138                port,
139                timeout,
140            }),
141            cwd: Some(delegate.worktree_root_path().to_path_buf()),
142            envs: HashMap::default(),
143            request_args: self.request_args(config)?,
144        })
145    }
146}
147
148#[async_trait(?Send)]
149impl DebugAdapter for PythonDebugAdapter {
150    fn name(&self) -> DebugAdapterName {
151        DebugAdapterName(Self::ADAPTER_NAME.into())
152    }
153
154    fn adapter_language_name(&self) -> Option<LanguageName> {
155        Some(SharedString::new_static("Python").into())
156    }
157
158    fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
159        let mut args = json!({
160            "request": match zed_scenario.request {
161                DebugRequest::Launch(_) => "launch",
162                DebugRequest::Attach(_) => "attach",
163            },
164            "subProcess": true,
165            "redirectOutput": true,
166        });
167
168        let map = args.as_object_mut().unwrap();
169        match &zed_scenario.request {
170            DebugRequest::Attach(attach) => {
171                map.insert("processId".into(), attach.process_id.into());
172            }
173            DebugRequest::Launch(launch) => {
174                map.insert("program".into(), launch.program.clone().into());
175                map.insert("args".into(), launch.args.clone().into());
176                if !launch.env.is_empty() {
177                    map.insert("env".into(), launch.env_json());
178                }
179
180                if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
181                    map.insert("stopOnEntry".into(), stop_on_entry.into());
182                }
183                if let Some(cwd) = launch.cwd.as_ref() {
184                    map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
185                }
186            }
187        }
188
189        Ok(DebugScenario {
190            adapter: zed_scenario.adapter,
191            label: zed_scenario.label,
192            config: args,
193            build: None,
194            tcp_connection: None,
195        })
196    }
197
198    fn validate_config(
199        &self,
200        config: &serde_json::Value,
201    ) -> Result<StartDebuggingRequestArgumentsRequest> {
202        let map = config.as_object().context("Config isn't an object")?;
203
204        let request_variant = map["request"].as_str().context("request is not valid")?;
205
206        match request_variant {
207            "launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
208            "attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
209            _ => Err(anyhow!("request must be either 'launch' or 'attach'")),
210        }
211    }
212
213    async fn dap_schema(&self) -> serde_json::Value {
214        json!({
215            "properties": {
216                "request": {
217                    "type": "string",
218                    "enum": ["attach", "launch"],
219                    "description": "Debug adapter request type"
220                },
221                "autoReload": {
222                    "default": {},
223                    "description": "Configures automatic reload of code on edit.",
224                    "properties": {
225                        "enable": {
226                            "default": false,
227                            "description": "Automatically reload code on edit.",
228                            "type": "boolean"
229                        },
230                        "exclude": {
231                            "default": [
232                                "**/.git/**",
233                                "**/.metadata/**",
234                                "**/__pycache__/**",
235                                "**/node_modules/**",
236                                "**/site-packages/**"
237                            ],
238                            "description": "Glob patterns of paths to exclude from auto reload.",
239                            "items": {
240                                "type": "string"
241                            },
242                            "type": "array"
243                        },
244                        "include": {
245                            "default": [
246                                "**/*.py",
247                                "**/*.pyw"
248                            ],
249                            "description": "Glob patterns of paths to include in auto reload.",
250                            "items": {
251                                "type": "string"
252                            },
253                            "type": "array"
254                        }
255                    },
256                    "type": "object"
257                },
258                "debugAdapterPath": {
259                    "description": "Path (fully qualified) to the python debug adapter executable.",
260                    "type": "string"
261                },
262                "django": {
263                    "default": false,
264                    "description": "Django debugging.",
265                    "type": "boolean"
266                },
267                "jinja": {
268                    "default": null,
269                    "description": "Jinja template debugging (e.g. Flask).",
270                    "enum": [
271                        false,
272                        null,
273                        true
274                    ]
275                },
276                "justMyCode": {
277                    "default": true,
278                    "description": "If true, show and debug only user-written code. If false, show and debug all code, including library calls.",
279                    "type": "boolean"
280                },
281                "logToFile": {
282                    "default": false,
283                    "description": "Enable logging of debugger events to a log file. This file can be found in the debugpy extension install folder.",
284                    "type": "boolean"
285                },
286                "pathMappings": {
287                    "default": [],
288                    "items": {
289                        "label": "Path mapping",
290                        "properties": {
291                            "localRoot": {
292                                "default": "${ZED_WORKTREE_ROOT}",
293                                "label": "Local source root.",
294                                "type": "string"
295                            },
296                            "remoteRoot": {
297                                "default": "",
298                                "label": "Remote source root.",
299                                "type": "string"
300                            }
301                        },
302                        "required": [
303                            "localRoot",
304                            "remoteRoot"
305                        ],
306                        "type": "object"
307                    },
308                    "label": "Path mappings.",
309                    "type": "array"
310                },
311                "redirectOutput": {
312                    "default": true,
313                    "description": "Redirect output.",
314                    "type": "boolean"
315                },
316                "showReturnValue": {
317                    "default": true,
318                    "description": "Show return value of functions when stepping.",
319                    "type": "boolean"
320                },
321                "subProcess": {
322                    "default": false,
323                    "description": "Whether to enable Sub Process debugging",
324                    "type": "boolean"
325                },
326                "consoleName": {
327                    "default": "Python Debug Console",
328                    "description": "Display name of the debug console or terminal",
329                    "type": "string"
330                },
331                "clientOS": {
332                    "default": null,
333                    "description": "OS that VS code is using.",
334                    "enum": [
335                        "windows",
336                        null,
337                        "unix"
338                    ]
339                }
340            },
341            "required": ["request"],
342            "allOf": [
343                {
344                    "if": {
345                        "properties": {
346                            "request": {
347                                "enum": ["attach"]
348                            }
349                        }
350                    },
351                    "then": {
352                        "properties": {
353                            "connect": {
354                                "label": "Attach by connecting to debugpy over a socket.",
355                                "properties": {
356                                    "host": {
357                                        "default": "127.0.0.1",
358                                        "description": "Hostname or IP address to connect to.",
359                                        "type": "string"
360                                    },
361                                    "port": {
362                                        "description": "Port to connect to.",
363                                        "type": [
364                                            "number",
365                                            "string"
366                                        ]
367                                    }
368                                },
369                                "required": [
370                                    "port"
371                                ],
372                                "type": "object"
373                            },
374                            "listen": {
375                                "label": "Attach by listening for incoming socket connection from debugpy",
376                                "properties": {
377                                    "host": {
378                                        "default": "127.0.0.1",
379                                        "description": "Hostname or IP address of the interface to listen on.",
380                                        "type": "string"
381                                    },
382                                    "port": {
383                                        "description": "Port to listen on.",
384                                        "type": [
385                                            "number",
386                                            "string"
387                                        ]
388                                    }
389                                },
390                                "required": [
391                                    "port"
392                                ],
393                                "type": "object"
394                            },
395                            "processId": {
396                                "anyOf": [
397                                    {
398                                        "default": "${command:pickProcess}",
399                                        "description": "Use process picker to select a process to attach, or Process ID as integer.",
400                                        "enum": [
401                                            "${command:pickProcess}"
402                                        ]
403                                    },
404                                    {
405                                        "description": "ID of the local process to attach to.",
406                                        "type": "integer"
407                                    }
408                                ]
409                            }
410                        }
411                    }
412                },
413                {
414                    "if": {
415                        "properties": {
416                            "request": {
417                                "enum": ["launch"]
418                            }
419                        }
420                    },
421                    "then": {
422                        "properties": {
423                            "args": {
424                                "default": [],
425                                "description": "Command line arguments passed to the program. For string type arguments, it will pass through the shell as is, and therefore all shell variable expansions will apply. But for the array type, the values will be shell-escaped.",
426                                "items": {
427                                    "type": "string"
428                                },
429                                "anyOf": [
430                                    {
431                                        "default": "${command:pickArgs}",
432                                        "enum": [
433                                            "${command:pickArgs}"
434                                        ]
435                                    },
436                                    {
437                                        "type": [
438                                            "array",
439                                            "string"
440                                        ]
441                                    }
442                                ]
443                            },
444                            "console": {
445                                "default": "integratedTerminal",
446                                "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.",
447                                "enum": [
448                                    "externalTerminal",
449                                    "integratedTerminal",
450                                    "internalConsole"
451                                ]
452                            },
453                            "cwd": {
454                                "default": "${ZED_WORKTREE_ROOT}",
455                                "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).",
456                                "type": "string"
457                            },
458                            "autoStartBrowser": {
459                                "default": false,
460                                "description": "Open external browser to launch the application",
461                                "type": "boolean"
462                            },
463                            "env": {
464                                "additionalProperties": {
465                                    "type": "string"
466                                },
467                                "default": {},
468                                "description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.",
469                                "type": "object"
470                            },
471                            "envFile": {
472                                "default": "${ZED_WORKTREE_ROOT}/.env",
473                                "description": "Absolute path to a file containing environment variable definitions.",
474                                "type": "string"
475                            },
476                            "gevent": {
477                                "default": false,
478                                "description": "Enable debugging of gevent monkey-patched code.",
479                                "type": "boolean"
480                            },
481                            "module": {
482                                "default": "",
483                                "description": "Name of the module to be debugged.",
484                                "type": "string"
485                            },
486                            "program": {
487                                "default": "${ZED_FILE}",
488                                "description": "Absolute path to the program.",
489                                "type": "string"
490                            },
491                            "purpose": {
492                                "default": [],
493                                "description": "Tells extension to use this configuration for test debugging, or when using debug-in-terminal command.",
494                                "items": {
495                                    "enum": [
496                                        "debug-test",
497                                        "debug-in-terminal"
498                                    ],
499                                    "enumDescriptions": [
500                                        "Use this configuration while debugging tests using test view or test debug commands.",
501                                        "Use this configuration while debugging a file using debug in terminal button in the editor."
502                                    ]
503                                },
504                                "type": "array"
505                            },
506                            "pyramid": {
507                                "default": false,
508                                "description": "Whether debugging Pyramid applications.",
509                                "type": "boolean"
510                            },
511                            "python": {
512                                "default": "${command:python.interpreterPath}",
513                                "description": "Absolute path to the Python interpreter executable; overrides workspace configuration if set.",
514                                "type": "string"
515                            },
516                            "pythonArgs": {
517                                "default": [],
518                                "description": "Command-line arguments passed to the Python interpreter. To pass arguments to the debug target, use \"args\".",
519                                "items": {
520                                    "type": "string"
521                                },
522                                "type": "array"
523                            },
524                            "stopOnEntry": {
525                                "default": false,
526                                "description": "Automatically stop after launch.",
527                                "type": "boolean"
528                            },
529                            "sudo": {
530                                "default": false,
531                                "description": "Running debug program under elevated permissions (on Unix).",
532                                "type": "boolean"
533                            },
534                            "guiEventLoop": {
535                                "default": "matplotlib",
536                                "description": "The GUI event loop that's going to run. Possible values: \"matplotlib\", \"wx\", \"qt\", \"none\", or a custom function that'll be imported and run.",
537                                "type": "string"
538                            }
539                        }
540                    }
541                }
542            ]
543        })
544    }
545
546    async fn get_binary(
547        &self,
548        delegate: &Arc<dyn DapDelegate>,
549        config: &DebugTaskDefinition,
550        user_installed_path: Option<PathBuf>,
551        cx: &mut AsyncApp,
552    ) -> Result<DebugAdapterBinary> {
553        if self.checked.set(()).is_ok() {
554            delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
555            if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
556                self.install_binary(version, delegate).await?;
557            }
558        }
559
560        self.get_installed_binary(delegate, &config, user_installed_path, cx)
561            .await
562    }
563}