debugger: Update docs with more examples (#31597)

Anthony Eid created

This PR also shows more completion items when defining a debug config in
a `debug.json` file. Mainly when using a pre build task argument.

### Follow ups
- Add docs for Go, JS, PHP
- Add attach docs

Release Notes:

- debugger beta: Show build task completions when editing a debug.json
configuration with a pre build task
- debugger beta: Add Python and Native Code debug config
[examples](https://zed.dev/docs/debugger)

Change summary

Cargo.lock                        |   1 
crates/dap_adapters/src/python.rs |   2 
crates/task/Cargo.toml            |   3 
crates/task/src/debug_format.rs   | 203 ++++++++++++++++++++++++++++++++
docs/src/debugger.md              | 123 +++++++++++++++++--
5 files changed, 310 insertions(+), 22 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -15588,6 +15588,7 @@ dependencies = [
  "futures 0.3.31",
  "gpui",
  "hex",
+ "log",
  "parking_lot",
  "pretty_assertions",
  "proto",

crates/dap_adapters/src/python.rs 🔗

@@ -660,7 +660,7 @@ impl DebugAdapter for PythonDebugAdapter {
             }
         }
 
-        self.get_installed_binary(delegate, &config, None, None, false)
+        self.get_installed_binary(delegate, &config, None, toolchain, false)
             .await
     }
 }

crates/task/Cargo.toml 🔗

@@ -20,6 +20,7 @@ collections.workspace = true
 futures.workspace = true
 gpui.workspace = true
 hex.workspace = true
+log.workspace = true
 parking_lot.workspace = true
 proto.workspace = true
 schemars.workspace = true
@@ -29,8 +30,8 @@ serde_json_lenient.workspace = true
 sha2.workspace = true
 shellexpand.workspace = true
 util.workspace = true
-zed_actions.workspace = true
 workspace-hack.workspace = true
+zed_actions.workspace = true
 
 [dev-dependencies]
 gpui = { workspace = true, features = ["test-support"] }

crates/task/src/debug_format.rs 🔗

@@ -1,10 +1,12 @@
 use anyhow::{Context as _, Result};
 use collections::FxHashMap;
 use gpui::SharedString;
+use log as _;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::net::Ipv4Addr;
 use std::path::PathBuf;
+use util::debug_panic;
 
 use crate::{TaskTemplate, adapter_schema::AdapterSchemas};
 
@@ -182,7 +184,7 @@ impl From<AttachRequest> for DebugRequest {
     }
 }
 
-#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
+#[derive(Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
 #[serde(untagged)]
 pub enum BuildTaskDefinition {
     ByName(SharedString),
@@ -194,6 +196,47 @@ pub enum BuildTaskDefinition {
     },
 }
 
+impl<'de> Deserialize<'de> for BuildTaskDefinition {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        #[derive(Deserialize)]
+        struct TemplateHelper {
+            #[serde(default)]
+            label: Option<String>,
+            #[serde(flatten)]
+            rest: serde_json::Value,
+        }
+
+        let value = serde_json::Value::deserialize(deserializer)?;
+
+        if let Ok(name) = serde_json::from_value::<SharedString>(value.clone()) {
+            return Ok(BuildTaskDefinition::ByName(name));
+        }
+
+        let helper: TemplateHelper =
+            serde_json::from_value(value).map_err(serde::de::Error::custom)?;
+
+        let mut template_value = helper.rest;
+        if let serde_json::Value::Object(ref mut map) = template_value {
+            map.insert(
+                "label".to_string(),
+                serde_json::to_value(helper.label.unwrap_or_else(|| "debug-build".to_owned()))
+                    .map_err(serde::de::Error::custom)?,
+            );
+        }
+
+        let task_template: TaskTemplate =
+            serde_json::from_value(template_value).map_err(serde::de::Error::custom)?;
+
+        Ok(BuildTaskDefinition::Template {
+            task_template,
+            locator_name: None,
+        })
+    }
+}
+
 #[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
 pub enum Request {
     Launch,
@@ -243,9 +286,96 @@ pub struct DebugScenario {
 pub struct DebugTaskFile(pub Vec<DebugScenario>);
 
 impl DebugTaskFile {
-    /// Generates JSON schema of Tasks JSON template format.
     pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json_lenient::Value {
-        schemas.generate_json_schema().unwrap_or_default()
+        let build_task_schema = schemars::schema_for!(BuildTaskDefinition);
+        let mut build_task_value =
+            serde_json_lenient::to_value(&build_task_schema).unwrap_or_default();
+
+        if let Some(template_object) = build_task_value
+            .get_mut("anyOf")
+            .and_then(|array| array.as_array_mut())
+            .and_then(|array| array.get_mut(1))
+        {
+            if let Some(properties) = template_object
+                .get_mut("properties")
+                .and_then(|value| value.as_object_mut())
+            {
+                properties.remove("label");
+            }
+
+            if let Some(arr) = template_object
+                .get_mut("required")
+                .and_then(|array| array.as_array_mut())
+            {
+                arr.retain(|v| v.as_str() != Some("label"));
+            }
+        } else {
+            debug_panic!("Task Template schema in debug scenario's needs to be updated");
+        }
+
+        let task_definitions = build_task_value
+            .get("definitions")
+            .cloned()
+            .unwrap_or_default();
+
+        let adapter_conditions = schemas
+            .0
+            .iter()
+            .map(|adapter_schema| {
+                let adapter_name = adapter_schema.adapter.to_string();
+                serde_json::json!({
+                    "if": {
+                        "properties": {
+                            "adapter": { "const": adapter_name }
+                        }
+                    },
+                    "then": adapter_schema.schema
+                })
+            })
+            .collect::<Vec<_>>();
+
+        serde_json_lenient::json!({
+            "$schema": "http://json-schema.org/draft-07/schema#",
+            "title": "Debug Configurations",
+            "description": "Configuration for debug scenarios",
+            "type": "array",
+            "items": {
+                "type": "object",
+                "required": ["adapter", "label"],
+                "properties": {
+                    "adapter": {
+                        "type": "string",
+                        "description": "The name of the debug adapter"
+                    },
+                    "label": {
+                        "type": "string",
+                        "description": "The name of the debug configuration"
+                    },
+                    "build": build_task_value,
+                    "tcp_connection": {
+                        "type": "object",
+                        "description": "Optional TCP connection information for connecting to an already running debug adapter",
+                        "properties": {
+                            "port": {
+                                "type": "integer",
+                                "description": "The port that the debug adapter is listening on (default: auto-find open port)"
+                            },
+                            "host": {
+                                "type": "string",
+                                "pattern": "^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$",
+                                "description": "The host that the debug adapter is listening to (default: 127.0.0.1)"
+                            },
+                            "timeout": {
+                                "type": "integer",
+                                "description": "The max amount of time in milliseconds to connect to a tcp DAP before returning an error (default: 2000ms)"
+                            }
+                        }
+                    }
+                },
+                "allOf": adapter_conditions
+            },
+            "definitions": task_definitions
+        })
     }
 }
 
@@ -254,6 +384,32 @@ mod tests {
     use crate::DebugScenario;
     use serde_json::json;
 
+    #[test]
+    fn test_just_build_args() {
+        let json = r#"{
+            "label": "Build & debug rust",
+            "adapter": "CodeLLDB",
+            "build": {
+                "command": "rust",
+                "args": ["build"]
+            }
+        }"#;
+
+        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
+        assert!(deserialized.build.is_some());
+        match deserialized.build.as_ref().unwrap() {
+            crate::BuildTaskDefinition::Template { task_template, .. } => {
+                assert_eq!("debug-build", task_template.label);
+                assert_eq!("rust", task_template.command);
+                assert_eq!(vec!["build"], task_template.args);
+            }
+            _ => panic!("Expected Template variant"),
+        }
+        assert_eq!(json!({}), deserialized.config);
+        assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
+        assert_eq!("Build & debug rust", deserialized.label.as_ref());
+    }
+
     #[test]
     fn test_empty_scenario_has_none_request() {
         let json = r#"{
@@ -307,4 +463,45 @@ mod tests {
         assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
         assert_eq!("Attach to process", deserialized.label.as_ref());
     }
+
+    #[test]
+    fn test_build_task_definition_without_label() {
+        use crate::BuildTaskDefinition;
+
+        let json = r#""my_build_task""#;
+        let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
+        match deserialized {
+            BuildTaskDefinition::ByName(name) => assert_eq!("my_build_task", name.as_ref()),
+            _ => panic!("Expected ByName variant"),
+        }
+
+        let json = r#"{
+            "command": "cargo",
+            "args": ["build", "--release"]
+        }"#;
+        let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
+        match deserialized {
+            BuildTaskDefinition::Template { task_template, .. } => {
+                assert_eq!("debug-build", task_template.label);
+                assert_eq!("cargo", task_template.command);
+                assert_eq!(vec!["build", "--release"], task_template.args);
+            }
+            _ => panic!("Expected Template variant"),
+        }
+
+        let json = r#"{
+            "label": "Build Release",
+            "command": "cargo",
+            "args": ["build", "--release"]
+        }"#;
+        let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
+        match deserialized {
+            BuildTaskDefinition::Template { task_template, .. } => {
+                assert_eq!("Build Release", task_template.label);
+                assert_eq!("cargo", task_template.command);
+                assert_eq!(vec!["build", "--release"], task_template.args);
+            }
+            _ => panic!("Expected Template variant"),
+        }
+    }
 }

docs/src/debugger.md 🔗

@@ -28,15 +28,15 @@ These adapters enable Zed to provide a consistent debugging experience across mu
 
 ## Getting Started
 
-For basic debugging you can set up a new configuration by opening the `New Session Modal` either via the `debugger: start` (default: f4) or clicking the plus icon at the top right of the debug panel.
+For basic debugging, you can set up a new configuration by opening the `New Session Modal` either via the `debugger: start` (default: f4) or by clicking the plus icon at the top right of the debug panel.
 
-For more advanced use cases you can create debug configurations by directly editing the `.zed/debug.json` file in your project root directory.
+For more advanced use cases, you can create debug configurations by directly editing the `.zed/debug.json` file in your project root directory.
 
-You can then use the `New Session Modal` to select a configuration then start debugging.
+You can then use the `New Session Modal` to select a configuration and start debugging.
 
 ### Configuration
 
-While configuration fields are debug adapter dependent, most adapters support the following fields.
+While configuration fields are debug adapter-dependent, most adapters support the following fields:
 
 ```json
 [
@@ -58,22 +58,114 @@ While configuration fields are debug adapter dependent, most adapters support th
 ]
 ```
 
-#### Task Variables
+#### Tasks
 
-All configuration fields support task variables. See [Tasks](./tasks.md)
+All configuration fields support task variables. See [Tasks Variables](./tasks.md#variables)
+
+Zed also allows embedding a task that is run before the debugger starts. This is useful for setting up the environment or running any necessary setup steps before the debugger starts.
+
+See an example [here](#build-binary-then-debug)
+
+#### Python Examples
+
+##### Python Active File
+
+```json
+[
+  {
+    "label": "Active File",
+    "adapter": "Debugpy",
+    "program": "$ZED_FILE",
+    "request": "launch"
+  }
+]
+```
+
+##### Flask App
+
+For a common Flask Application with a file structure similar to the following:
+
+- .venv/
+- app/
+  - **init**.py
+  - **main**.py
+  - routes.py
+- templates/
+  - index.html
+- static/
+  - style.css
+- requirements.txt
+
+```json
+[
+  {
+    "label": "Python: Flask",
+    "adapter": "Debugpy",
+    "request": "launch",
+    "module": "app",
+    "cwd": "$ZED_WORKTREE_ROOT",
+    "env": {
+      "FLASK_APP": "app",
+      "FLASK_DEBUG": "1"
+    },
+    "args": [
+      "run",
+      "--reload", // Enables Flask reloader that watches for file changes
+      "--debugger" // Enables Flask debugger
+    ],
+    "autoReload": {
+      "enable": true
+    },
+    "jinja": true,
+    "justMyCode": true
+  }
+]
+```
+
+#### Rust/C++/C
+
+##### Using pre-built binary
+
+```json
+[
+  {
+    "label": "Debug native binary",
+    "program": "$ZED_WORKTREE_ROOT/build/binary",
+    "request": "launch",
+    "adapter": "CodeLLDB" // GDB is available on non arm macs as well as linux
+  }
+]
+```
+
+##### Build binary then debug
+
+```json
+[
+  {
+    "label": "Build & Debug native binary",
+    "build": {
+      "command": "cargo",
+      "args": ["build"]
+    },
+    "program": "$ZED_WORKTREE_ROOT/target/debug/binary",
+    "request": "launch",
+    "adapter": "CodeLLDB" // GDB is available on non arm macs as well as linux
+  }
+]
+```
 
 ## Breakpoints
 
-Zed currently supports these types of breakpoints
+Zed currently supports these types of breakpoints:
 
 - Standard Breakpoints: Stop at the breakpoint when it's hit
 - Log Breakpoints: Output a log message instead of stopping at the breakpoint when it's hit
 - Conditional Breakpoints: Stop at the breakpoint when it's hit if the condition is met
 - Hit Breakpoints: Stop at the breakpoint when it's hit a certain number of times
 
-Standard breakpoints can be toggled by left clicking on the editor gutter or using the Toggle Breakpoint action. Right clicking on a breakpoint or on a code runner symbol brings up the breakpoint context menu. This has options for toggling breakpoints and editing log breakpoints.
+Standard breakpoints can be toggled by left-clicking on the editor gutter or using the Toggle Breakpoint action. Right-clicking on a breakpoint or on a code runner symbol brings up the breakpoint context menu. This has options for toggling breakpoints and editing log breakpoints.
 
-Other kinds of breakpoints can be toggled/edited by right clicking on the breakpoint icon in the gutter and selecting the desired option.
+Other kinds of breakpoints can be toggled/edited by right-clicking on the breakpoint icon in the gutter and selecting the desired option.
 
 ## Settings
 
@@ -81,8 +173,8 @@ Other kinds of breakpoints can be toggled/edited by right clicking on the breakp
 - `save_breakpoints`: Whether the breakpoints should be reused across Zed sessions.
 - `button`: Whether to show the debug button in the status bar.
 - `timeout`: Time in milliseconds until timeout error when connecting to a TCP debug adapter.
-- `log_dap_communications`: Whether to log messages between active debug adapters and Zed
-- `format_dap_log_messages`: Whether to format dap messages in when adding them to debug adapter logger
+- `log_dap_communications`: Whether to log messages between active debug adapters and Zed.
+- `format_dap_log_messages`: Whether to format DAP messages when adding them to the debug adapter logger.
 
 ### Stepping granularity
 
@@ -163,7 +255,7 @@ Other kinds of breakpoints can be toggled/edited by right clicking on the breakp
 ### Timeout
 
 - Description: Time in milliseconds until timeout error when connecting to a TCP debug adapter.
-- Default: 2000ms
+- Default: 2000
 - Setting: debugger.timeout
 
 **Options**
@@ -198,7 +290,7 @@ Other kinds of breakpoints can be toggled/edited by right clicking on the breakp
 
 ### Format Dap Log Messages
 
-- Description: Whether to format dap messages in when adding them to debug adapter logger. (Used for DAP development)
+- Description: Whether to format DAP messages when adding them to the debug adapter logger. (Used for DAP development)
 - Default: false
 - Setting: debugger.format_dap_log_messages
 
@@ -218,8 +310,5 @@ Other kinds of breakpoints can be toggled/edited by right clicking on the breakp
 
 The Debugger supports the following theme options:
 
-    /// Color used to accent some of the debugger's elements
-    /// Only accents breakpoint & breakpoint related symbols right now
-
-**debugger.accent**: Color used to accent breakpoint & breakpoint related symbols
+**debugger.accent**: Color used to accent breakpoint & breakpoint-related symbols
 **editor.debugger_active_line.background**: Background color of active debug line