debugger: Improve Go support (#31559)

Alex created

Supersedes https://github.com/zed-industries/zed/pull/31345 
This PR does not have any terminal/console related stuff so that it can
be solved separately.

Introduces inline hints in debugger:
<img width="1141" alt="image"
src="https://github.com/user-attachments/assets/b0575f1e-ddf8-41fe-8958-2da6d4974912"
/>
Adds locators for go, so that you can your app in debug mode:
<img width="706" alt="image"
src="https://github.com/user-attachments/assets/df29bba5-8264-4bea-976f-686c32a5605b"
/>
As well is allows you to specify an existing compiled binary:
<img width="604" alt="image"
src="https://github.com/user-attachments/assets/548f2ab5-88c1-41fb-af84-115a19e685ea"
/>

Release Notes:

- Added inline value hints for Go debugging, displaying variable values
directly in the editor during debug sessions
- Added Go debug locator support, enabling debugging of Go applications
through task templates
- Improved Go debug adapter to support both source debugging (mode:
"debug") and binary execution (mode: "exec") based on program path

cc @osiewicz, @Anthony-Eid

Change summary

Cargo.lock                                 |   2 
crates/dap/Cargo.toml                      |   2 
crates/dap/src/inline_value.rs             | 383 ++++++++++++++++++++++++
crates/dap_adapters/src/dap_adapters.rs    |   3 
crates/dap_adapters/src/go.rs              |  24 +
crates/project/src/debugger/dap_store.rs   |   1 
crates/project/src/debugger/locators.rs    |   1 
crates/project/src/debugger/locators/go.rs | 244 +++++++++++++++
8 files changed, 651 insertions(+), 9 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4031,6 +4031,8 @@ dependencies = [
  "smol",
  "task",
  "telemetry",
+ "tree-sitter",
+ "tree-sitter-go",
  "util",
  "workspace-hack",
  "zlog",

crates/dap/Cargo.toml 🔗

@@ -56,5 +56,7 @@ async-pipe.workspace = true
 gpui = { workspace = true, features = ["test-support"] }
 settings = { workspace = true, features = ["test-support"] }
 task = { workspace = true, features = ["test-support"] }
+tree-sitter.workspace = true
+tree-sitter-go.workspace = true
 util = { workspace = true, features = ["test-support"] }
 zlog.workspace = true

crates/dap/src/inline_value.rs 🔗

@@ -275,3 +275,386 @@ impl InlineValueProvider for PythonInlineValueProvider {
         variables
     }
 }
+
+pub struct GoInlineValueProvider;
+
+impl InlineValueProvider for GoInlineValueProvider {
+    fn provide(
+        &self,
+        mut node: language::Node,
+        source: &str,
+        max_row: usize,
+    ) -> Vec<InlineValueLocation> {
+        let mut variables = Vec::new();
+        let mut variable_names = HashSet::new();
+        let mut scope = VariableScope::Local;
+
+        loop {
+            let mut variable_names_in_scope = HashMap::new();
+            for child in node.named_children(&mut node.walk()) {
+                if child.start_position().row >= max_row {
+                    break;
+                }
+
+                if scope == VariableScope::Local {
+                    match child.kind() {
+                        "var_declaration" => {
+                            for var_spec in child.named_children(&mut child.walk()) {
+                                if var_spec.kind() == "var_spec" {
+                                    if let Some(name_node) = var_spec.child_by_field_name("name") {
+                                        let variable_name =
+                                            source[name_node.byte_range()].to_string();
+
+                                        if variable_names.contains(&variable_name) {
+                                            continue;
+                                        }
+
+                                        if let Some(index) =
+                                            variable_names_in_scope.get(&variable_name)
+                                        {
+                                            variables.remove(*index);
+                                        }
+
+                                        variable_names_in_scope
+                                            .insert(variable_name.clone(), variables.len());
+                                        variables.push(InlineValueLocation {
+                                            variable_name,
+                                            scope: VariableScope::Local,
+                                            lookup: VariableLookupKind::Variable,
+                                            row: name_node.end_position().row,
+                                            column: name_node.end_position().column,
+                                        });
+                                    }
+                                }
+                            }
+                        }
+                        "short_var_declaration" => {
+                            if let Some(left_side) = child.child_by_field_name("left") {
+                                for identifier in left_side.named_children(&mut left_side.walk()) {
+                                    if identifier.kind() == "identifier" {
+                                        let variable_name =
+                                            source[identifier.byte_range()].to_string();
+
+                                        if variable_names.contains(&variable_name) {
+                                            continue;
+                                        }
+
+                                        if let Some(index) =
+                                            variable_names_in_scope.get(&variable_name)
+                                        {
+                                            variables.remove(*index);
+                                        }
+
+                                        variable_names_in_scope
+                                            .insert(variable_name.clone(), variables.len());
+                                        variables.push(InlineValueLocation {
+                                            variable_name,
+                                            scope: VariableScope::Local,
+                                            lookup: VariableLookupKind::Variable,
+                                            row: identifier.end_position().row,
+                                            column: identifier.end_position().column,
+                                        });
+                                    }
+                                }
+                            }
+                        }
+                        "assignment_statement" => {
+                            if let Some(left_side) = child.child_by_field_name("left") {
+                                for identifier in left_side.named_children(&mut left_side.walk()) {
+                                    if identifier.kind() == "identifier" {
+                                        let variable_name =
+                                            source[identifier.byte_range()].to_string();
+
+                                        if variable_names.contains(&variable_name) {
+                                            continue;
+                                        }
+
+                                        if let Some(index) =
+                                            variable_names_in_scope.get(&variable_name)
+                                        {
+                                            variables.remove(*index);
+                                        }
+
+                                        variable_names_in_scope
+                                            .insert(variable_name.clone(), variables.len());
+                                        variables.push(InlineValueLocation {
+                                            variable_name,
+                                            scope: VariableScope::Local,
+                                            lookup: VariableLookupKind::Variable,
+                                            row: identifier.end_position().row,
+                                            column: identifier.end_position().column,
+                                        });
+                                    }
+                                }
+                            }
+                        }
+                        "function_declaration" | "method_declaration" => {
+                            if let Some(params) = child.child_by_field_name("parameters") {
+                                for param in params.named_children(&mut params.walk()) {
+                                    if param.kind() == "parameter_declaration" {
+                                        if let Some(name_node) = param.child_by_field_name("name") {
+                                            let variable_name =
+                                                source[name_node.byte_range()].to_string();
+
+                                            if variable_names.contains(&variable_name) {
+                                                continue;
+                                            }
+
+                                            if let Some(index) =
+                                                variable_names_in_scope.get(&variable_name)
+                                            {
+                                                variables.remove(*index);
+                                            }
+
+                                            variable_names_in_scope
+                                                .insert(variable_name.clone(), variables.len());
+                                            variables.push(InlineValueLocation {
+                                                variable_name,
+                                                scope: VariableScope::Local,
+                                                lookup: VariableLookupKind::Variable,
+                                                row: name_node.end_position().row,
+                                                column: name_node.end_position().column,
+                                            });
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                        "for_statement" => {
+                            if let Some(clause) = child.named_child(0) {
+                                if clause.kind() == "for_clause" {
+                                    if let Some(init) = clause.named_child(0) {
+                                        if init.kind() == "short_var_declaration" {
+                                            if let Some(left_side) =
+                                                init.child_by_field_name("left")
+                                            {
+                                                if left_side.kind() == "expression_list" {
+                                                    for identifier in left_side
+                                                        .named_children(&mut left_side.walk())
+                                                    {
+                                                        if identifier.kind() == "identifier" {
+                                                            let variable_name = source
+                                                                [identifier.byte_range()]
+                                                            .to_string();
+
+                                                            if variable_names
+                                                                .contains(&variable_name)
+                                                            {
+                                                                continue;
+                                                            }
+
+                                                            if let Some(index) =
+                                                                variable_names_in_scope
+                                                                    .get(&variable_name)
+                                                            {
+                                                                variables.remove(*index);
+                                                            }
+
+                                                            variable_names_in_scope.insert(
+                                                                variable_name.clone(),
+                                                                variables.len(),
+                                                            );
+                                                            variables.push(InlineValueLocation {
+                                                                variable_name,
+                                                                scope: VariableScope::Local,
+                                                                lookup:
+                                                                    VariableLookupKind::Variable,
+                                                                row: identifier.end_position().row,
+                                                                column: identifier
+                                                                    .end_position()
+                                                                    .column,
+                                                            });
+                                                        }
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                } else if clause.kind() == "range_clause" {
+                                    if let Some(left) = clause.child_by_field_name("left") {
+                                        if left.kind() == "expression_list" {
+                                            for identifier in left.named_children(&mut left.walk())
+                                            {
+                                                if identifier.kind() == "identifier" {
+                                                    let variable_name =
+                                                        source[identifier.byte_range()].to_string();
+
+                                                    if variable_name == "_" {
+                                                        continue;
+                                                    }
+
+                                                    if variable_names.contains(&variable_name) {
+                                                        continue;
+                                                    }
+
+                                                    if let Some(index) =
+                                                        variable_names_in_scope.get(&variable_name)
+                                                    {
+                                                        variables.remove(*index);
+                                                    }
+                                                    variable_names_in_scope.insert(
+                                                        variable_name.clone(),
+                                                        variables.len(),
+                                                    );
+                                                    variables.push(InlineValueLocation {
+                                                        variable_name,
+                                                        scope: VariableScope::Local,
+                                                        lookup: VariableLookupKind::Variable,
+                                                        row: identifier.end_position().row,
+                                                        column: identifier.end_position().column,
+                                                    });
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                        _ => {}
+                    }
+                } else if child.kind() == "var_declaration" {
+                    for var_spec in child.named_children(&mut child.walk()) {
+                        if var_spec.kind() == "var_spec" {
+                            if let Some(name_node) = var_spec.child_by_field_name("name") {
+                                let variable_name = source[name_node.byte_range()].to_string();
+                                variables.push(InlineValueLocation {
+                                    variable_name,
+                                    scope: VariableScope::Global,
+                                    lookup: VariableLookupKind::Expression,
+                                    row: name_node.end_position().row,
+                                    column: name_node.end_position().column,
+                                });
+                            }
+                        }
+                    }
+                }
+            }
+
+            variable_names.extend(variable_names_in_scope.keys().cloned());
+
+            if matches!(node.kind(), "function_declaration" | "method_declaration") {
+                scope = VariableScope::Global;
+            }
+
+            if let Some(parent) = node.parent() {
+                node = parent;
+            } else {
+                break;
+            }
+        }
+
+        variables
+    }
+}
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use tree_sitter::Parser;
+
+    #[test]
+    fn test_go_inline_value_provider() {
+        let provider = GoInlineValueProvider;
+        let source = r#"
+package main
+
+func main() {
+    items := []int{1, 2, 3, 4, 5}
+    for i, v := range items {
+        println(i, v)
+    }
+    for j := 0; j < 10; j++ {
+        println(j)
+    }
+}
+"#;
+
+        let mut parser = Parser::new();
+        if parser
+            .set_language(&tree_sitter_go::LANGUAGE.into())
+            .is_err()
+        {
+            return;
+        }
+        let Some(tree) = parser.parse(source, None) else {
+            return;
+        };
+        let root_node = tree.root_node();
+
+        let mut main_body = None;
+        for child in root_node.named_children(&mut root_node.walk()) {
+            if child.kind() == "function_declaration" {
+                if let Some(name) = child.child_by_field_name("name") {
+                    if &source[name.byte_range()] == "main" {
+                        if let Some(body) = child.child_by_field_name("body") {
+                            main_body = Some(body);
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        let Some(main_body) = main_body else {
+            return;
+        };
+
+        let variables = provider.provide(main_body, source, 100);
+        assert!(variables.len() >= 2);
+
+        let variable_names: Vec<&str> =
+            variables.iter().map(|v| v.variable_name.as_str()).collect();
+        assert!(variable_names.contains(&"items"));
+        assert!(variable_names.contains(&"j"));
+    }
+
+    #[test]
+    fn test_go_inline_value_provider_counter_pattern() {
+        let provider = GoInlineValueProvider;
+        let source = r#"
+package main
+
+func main() {
+    N := 10
+    for i := range N {
+        println(i)
+    }
+}
+"#;
+
+        let mut parser = Parser::new();
+        if parser
+            .set_language(&tree_sitter_go::LANGUAGE.into())
+            .is_err()
+        {
+            return;
+        }
+        let Some(tree) = parser.parse(source, None) else {
+            return;
+        };
+        let root_node = tree.root_node();
+
+        let mut main_body = None;
+        for child in root_node.named_children(&mut root_node.walk()) {
+            if child.kind() == "function_declaration" {
+                if let Some(name) = child.child_by_field_name("name") {
+                    if &source[name.byte_range()] == "main" {
+                        if let Some(body) = child.child_by_field_name("body") {
+                            main_body = Some(body);
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        let Some(main_body) = main_body else {
+            return;
+        };
+        let variables = provider.provide(main_body, source, 100);
+
+        let variable_names: Vec<&str> =
+            variables.iter().map(|v| v.variable_name.as_str()).collect();
+        assert!(variable_names.contains(&"N"));
+        assert!(variable_names.contains(&"i"));
+    }
+}

crates/dap_adapters/src/dap_adapters.rs 🔗

@@ -18,7 +18,7 @@ use dap::{
         GithubRepo,
     },
     configure_tcp_connection,
-    inline_value::{PythonInlineValueProvider, RustInlineValueProvider},
+    inline_value::{GoInlineValueProvider, PythonInlineValueProvider, RustInlineValueProvider},
 };
 use gdb::GdbDebugAdapter;
 use go::GoDebugAdapter;
@@ -48,5 +48,6 @@ pub fn init(cx: &mut App) {
         registry.add_inline_value_provider("Rust".to_string(), Arc::from(RustInlineValueProvider));
         registry
             .add_inline_value_provider("Python".to_string(), Arc::from(PythonInlineValueProvider));
+        registry.add_inline_value_provider("Go".to_string(), Arc::from(GoInlineValueProvider));
     })
 }

crates/dap_adapters/src/go.rs 🔗

@@ -312,14 +312,22 @@ impl DebugAdapter for GoDebugAdapter {
                     "processId": attach_config.process_id,
                 })
             }
-            dap::DebugRequest::Launch(launch_config) => json!({
-                "request": "launch",
-                "mode": "debug",
-                "program": launch_config.program,
-                "cwd": launch_config.cwd,
-                "args": launch_config.args,
-                "env": launch_config.env_json()
-            }),
+            dap::DebugRequest::Launch(launch_config) => {
+                let mode = if launch_config.program != "." {
+                    "exec"
+                } else {
+                    "debug"
+                };
+
+                json!({
+                    "request": "launch",
+                    "mode": mode,
+                    "program": launch_config.program,
+                    "cwd": launch_config.cwd,
+                    "args": launch_config.args,
+                    "env": launch_config.env_json()
+                })
+            }
         };
 
         let map = args.as_object_mut().unwrap();

crates/project/src/debugger/dap_store.rs 🔗

@@ -104,6 +104,7 @@ impl DapStore {
             let registry = DapRegistry::global(cx);
             registry.add_locator(Arc::new(locators::cargo::CargoLocator {}));
             registry.add_locator(Arc::new(locators::python::PythonLocator));
+            registry.add_locator(Arc::new(locators::go::GoLocator {}));
         });
         client.add_entity_request_handler(Self::handle_run_debug_locator);
         client.add_entity_request_handler(Self::handle_get_debug_adapter_binary);

crates/project/src/debugger/locators/go.rs 🔗

@@ -0,0 +1,244 @@
+use anyhow::Result;
+use async_trait::async_trait;
+use collections::FxHashMap;
+use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
+use gpui::SharedString;
+use std::path::PathBuf;
+use task::{
+    BuildTaskDefinition, DebugScenario, RevealStrategy, RevealTarget, Shell, SpawnInTerminal,
+    TaskTemplate,
+};
+
+pub(crate) struct GoLocator;
+
+#[async_trait]
+impl DapLocator for GoLocator {
+    fn name(&self) -> SharedString {
+        SharedString::new_static("go-debug-locator")
+    }
+
+    fn create_scenario(
+        &self,
+        build_config: &TaskTemplate,
+        resolved_label: &str,
+        adapter: DebugAdapterName,
+    ) -> Option<DebugScenario> {
+        let go_action = build_config.args.first()?;
+
+        match go_action.as_str() {
+            "run" => {
+                let program = build_config
+                    .args
+                    .get(1)
+                    .cloned()
+                    .unwrap_or_else(|| ".".to_string());
+
+                let build_task = TaskTemplate {
+                    label: "go build debug".into(),
+                    command: "go".into(),
+                    args: vec![
+                        "build".into(),
+                        "-gcflags \"all=-N -l\"".into(),
+                        program.clone(),
+                    ],
+                    env: build_config.env.clone(),
+                    cwd: build_config.cwd.clone(),
+                    use_new_terminal: false,
+                    allow_concurrent_runs: false,
+                    reveal: RevealStrategy::Always,
+                    reveal_target: RevealTarget::Dock,
+                    hide: task::HideStrategy::Never,
+                    shell: Shell::System,
+                    tags: vec![],
+                    show_summary: true,
+                    show_command: true,
+                };
+
+                Some(DebugScenario {
+                    label: resolved_label.to_string().into(),
+                    adapter: adapter.0,
+                    build: Some(BuildTaskDefinition::Template {
+                        task_template: build_task,
+                        locator_name: Some(self.name()),
+                    }),
+                    config: serde_json::Value::Null,
+                    tcp_connection: None,
+                })
+            }
+            _ => None,
+        }
+    }
+
+    async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
+        if build_config.args.is_empty() {
+            return Err(anyhow::anyhow!("Invalid Go command"));
+        }
+
+        let go_action = &build_config.args[0];
+        let cwd = build_config
+            .cwd
+            .as_ref()
+            .map(|p| p.to_string_lossy().to_string())
+            .unwrap_or_else(|| ".".to_string());
+
+        let mut env = FxHashMap::default();
+        for (key, value) in &build_config.env {
+            env.insert(key.clone(), value.clone());
+        }
+
+        match go_action.as_str() {
+            "build" => {
+                let package = build_config
+                    .args
+                    .get(2)
+                    .cloned()
+                    .unwrap_or_else(|| ".".to_string());
+
+                Ok(DebugRequest::Launch(task::LaunchRequest {
+                    program: package,
+                    cwd: Some(PathBuf::from(&cwd)),
+                    args: vec![],
+                    env,
+                }))
+            }
+            _ => Err(anyhow::anyhow!("Unsupported Go command: {}", go_action)),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskTemplate};
+
+    #[test]
+    fn test_create_scenario_for_go_run() {
+        let locator = GoLocator;
+        let task = TaskTemplate {
+            label: "go run main.go".into(),
+            command: "go".into(),
+            args: vec!["run".into(), "main.go".into()],
+            env: Default::default(),
+            cwd: Some("${ZED_WORKTREE_ROOT}".into()),
+            use_new_terminal: false,
+            allow_concurrent_runs: false,
+            reveal: RevealStrategy::Always,
+            reveal_target: RevealTarget::Dock,
+            hide: HideStrategy::Never,
+            shell: Shell::System,
+            tags: vec![],
+            show_summary: true,
+            show_command: true,
+        };
+
+        let scenario =
+            locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
+
+        assert!(scenario.is_some());
+        let scenario = scenario.unwrap();
+        assert_eq!(scenario.adapter, "Delve");
+        assert_eq!(scenario.label, "test label");
+        assert!(scenario.build.is_some());
+
+        if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
+            assert_eq!(task_template.command, "go");
+            assert!(task_template.args.contains(&"build".into()));
+            assert!(
+                task_template
+                    .args
+                    .contains(&"-gcflags \"all=-N -l\"".into())
+            );
+            assert!(task_template.args.contains(&"main.go".into()));
+        } else {
+            panic!("Expected BuildTaskDefinition::Template");
+        }
+
+        assert!(
+            scenario.config.is_null(),
+            "Initial config should be null to ensure it's invalid"
+        );
+    }
+
+    #[test]
+    fn test_create_scenario_for_go_build() {
+        let locator = GoLocator;
+        let task = TaskTemplate {
+            label: "go build".into(),
+            command: "go".into(),
+            args: vec!["build".into(), ".".into()],
+            env: Default::default(),
+            cwd: Some("${ZED_WORKTREE_ROOT}".into()),
+            use_new_terminal: false,
+            allow_concurrent_runs: false,
+            reveal: RevealStrategy::Always,
+            reveal_target: RevealTarget::Dock,
+            hide: HideStrategy::Never,
+            shell: Shell::System,
+            tags: vec![],
+            show_summary: true,
+            show_command: true,
+        };
+
+        let scenario =
+            locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
+
+        assert!(scenario.is_none());
+    }
+
+    #[test]
+    fn test_skip_non_go_commands_with_non_delve_adapter() {
+        let locator = GoLocator;
+        let task = TaskTemplate {
+            label: "cargo build".into(),
+            command: "cargo".into(),
+            args: vec!["build".into()],
+            env: Default::default(),
+            cwd: Some("${ZED_WORKTREE_ROOT}".into()),
+            use_new_terminal: false,
+            allow_concurrent_runs: false,
+            reveal: RevealStrategy::Always,
+            reveal_target: RevealTarget::Dock,
+            hide: HideStrategy::Never,
+            shell: Shell::System,
+            tags: vec![],
+            show_summary: true,
+            show_command: true,
+        };
+
+        let scenario = locator.create_scenario(
+            &task,
+            "test label",
+            DebugAdapterName("SomeOtherAdapter".into()),
+        );
+        assert!(scenario.is_none());
+
+        let scenario =
+            locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
+        assert!(scenario.is_none());
+    }
+
+    #[test]
+    fn test_skip_unsupported_go_commands() {
+        let locator = GoLocator;
+        let task = TaskTemplate {
+            label: "go clean".into(),
+            command: "go".into(),
+            args: vec!["clean".into()],
+            env: Default::default(),
+            cwd: Some("${ZED_WORKTREE_ROOT}".into()),
+            use_new_terminal: false,
+            allow_concurrent_runs: false,
+            reveal: RevealStrategy::Always,
+            reveal_target: RevealTarget::Dock,
+            hide: HideStrategy::Never,
+            shell: Shell::System,
+            tags: vec![],
+            show_summary: true,
+            show_command: true,
+        };
+
+        let scenario =
+            locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
+        assert!(scenario.is_none());
+    }
+}