From 94a5fe265d5bb8e6ff1fd6fa5e114553fc553ad2 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 28 May 2025 12:59:05 +0200 Subject: [PATCH] debugger: Improve Go support (#31559) 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: image Adds locators for go, so that you can your app in debug mode: image As well is allows you to specify an existing compiled binary: image 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 --- 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(-) create mode 100644 crates/project/src/debugger/locators/go.rs diff --git a/Cargo.lock b/Cargo.lock index b80340e689..711122d5a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4031,6 +4031,8 @@ dependencies = [ "smol", "task", "telemetry", + "tree-sitter", + "tree-sitter-go", "util", "workspace-hack", "zlog", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 01516353a9..162c17d6f0 100644 --- a/crates/dap/Cargo.toml +++ b/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 diff --git a/crates/dap/src/inline_value.rs b/crates/dap/src/inline_value.rs index 16562a52b4..881797e20f 100644 --- a/crates/dap/src/inline_value.rs +++ b/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 { + 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")); + } +} diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index b0450461e6..5dbcb7058d 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/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)); }) } diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index 1f7faf206f..dc63201be9 100644 --- a/crates/dap_adapters/src/go.rs +++ b/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(); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index bdcd2c53e3..382efd108b 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/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); diff --git a/crates/project/src/debugger/locators.rs b/crates/project/src/debugger/locators.rs index d4a64118d7..a845f1759c 100644 --- a/crates/project/src/debugger/locators.rs +++ b/crates/project/src/debugger/locators.rs @@ -1,2 +1,3 @@ pub(crate) mod cargo; +pub(crate) mod go; pub(crate) mod python; diff --git a/crates/project/src/debugger/locators/go.rs b/crates/project/src/debugger/locators/go.rs new file mode 100644 index 0000000000..3b905cce91 --- /dev/null +++ b/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 { + 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 { + 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()); + } +}