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