Cargo.lock 🔗
@@ -4031,6 +4031,8 @@ dependencies = [
"smol",
"task",
"telemetry",
+ "tree-sitter",
+ "tree-sitter-go",
"util",
"workspace-hack",
"zlog",
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
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(-)
@@ -4031,6 +4031,8 @@ dependencies = [
"smol",
"task",
"telemetry",
+ "tree-sitter",
+ "tree-sitter-go",
"util",
"workspace-hack",
"zlog",
@@ -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
@@ -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"));
+ }
+}
@@ -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));
})
}
@@ -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();
@@ -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);
@@ -1,2 +1,3 @@
pub(crate) mod cargo;
+pub(crate) mod go;
pub(crate) mod python;
@@ -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());
+ }
+}