python.rs

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