use anyhow::Result;use async_trait::async_trait;use collections::HashMap;use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};use gpui::SharedString;use serde::{Deserialize, Serialize};use task::{DebugScenario, SpawnInTerminal, TaskTemplate}; pub(crate) struct GoLocator; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]#[serde(rename_all = "camelCase")]struct DelveLaunchRequest { request: String, mode: String, program: String, #[serde(skip_serializing_if = "Option::is_none")] cwd: Option<String>, args: Vec<String>, build_flags: Vec<String>, env: HashMap<String, String>,} fn is_debug_flag(arg: &str) -> Option<bool> { let mut part = if let Some(suffix) = arg.strip_prefix("test.") { suffix } else { arg }; let mut might_have_arg = true; if let Some(idx) = part.find('=') { might_have_arg = false; part = &part[..idx]; } match part { "benchmem" | "failfast" | "fullpath" | "fuzzworker" | "json" | "short" | "v" | "paniconexit0" => Some(false), "bench" | "benchtime" | "blockprofile" | "blockprofilerate" | "count" | "coverprofile" | "cpu" | "cpuprofile" | "fuzz" | "fuzzcachedir" | "fuzzminimizetime" | "fuzztime" | "gocoverdir" | "list" | "memprofile" | "memprofilerate" | "mutexprofile" | "mutexprofilefraction" | "outputdir" | "parallel" | "run" | "shuffle" | "skip" | "testlogfile" | "timeout" | "trace" => Some(might_have_arg), _ if arg.starts_with("test.") => Some(false), _ => None, }} fn is_build_flag(mut arg: &str) -> Option<bool> { let mut might_have_arg = true; if let Some(idx) = arg.find('=') { might_have_arg = false; arg = &arg[..idx]; } match arg { "a" | "n" | "race" | "msan" | "asan" | "cover" | "work" | "x" | "v" | "buildvcs" | "json" | "linkshared" | "modcacherw" | "trimpath" => Some(false), "p" | "covermode" | "coverpkg" | "asmflags" | "buildmode" | "compiler" | "gccgoflags" | "gcflags" | "installsuffix" | "ldflags" | "mod" | "modfile" | "overlay" | "pgo" | "pkgdir" | "tags" | "toolexec" => Some(might_have_arg), _ => None, }} #[async_trait]impl DapLocator for GoLocator { fn name(&self) -> SharedString { SharedString::new_static("go-debug-locator") } async fn create_scenario( &self, build_config: &TaskTemplate, resolved_label: &str, adapter: &DebugAdapterName, ) -> Option<DebugScenario> { if build_config.command != "go" { return None; } let go_action = build_config.args.first()?; match go_action.as_str() { "test" => { let mut program = ".".to_string(); let mut args = Vec::default(); let mut build_flags = Vec::default(); let mut all_args_are_test = false; let mut next_arg_is_test = false; let mut next_arg_is_build = false; let mut seen_pkg = false; let mut seen_v = false; for arg in build_config.args.iter().skip(1) { if all_args_are_test || next_arg_is_test { // HACK: tasks assume that they are run in a shell context, // so the -run regex has escaped specials. Delve correctly // handles escaping, so we undo that here. if arg.starts_with("\\^") && arg.ends_with("\\$") { let mut arg = arg[1..arg.len() - 2].to_string(); arg.push('$'); args.push(arg); } else { args.push(arg.clone()); } next_arg_is_test = false; } else if next_arg_is_build { build_flags.push(arg.clone()); next_arg_is_build = false; } else if arg.starts_with('-') { let flag = arg.trim_start_matches('-'); if flag == "args" { all_args_are_test = true; } else if let Some(has_arg) = is_debug_flag(flag) { if flag == "v" || flag == "test.v" { seen_v = true; } if flag.starts_with("test.") { args.push(arg.clone()); } else { args.push(format!("-test.{flag}")) } next_arg_is_test = has_arg; } else if let Some(has_arg) = is_build_flag(flag) { build_flags.push(arg.clone()); next_arg_is_build = has_arg; } } else if !seen_pkg { program = arg.clone(); seen_pkg = true; } else { args.push(arg.clone()); } } if !seen_v { args.push("-test.v".to_string()); } let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest { request: "launch".to_string(), mode: "test".to_string(), program, args: args, build_flags, cwd: build_config.cwd.clone(), env: build_config.env.clone(), }) .unwrap(); Some(DebugScenario { label: resolved_label.to_string().into(), adapter: adapter.0.clone(), build: None, config: config, tcp_connection: None, }) } "run" => { let mut next_arg_is_build = false; let mut seen_pkg = false; let mut program = ".".to_string(); let mut args = Vec::default(); let mut build_flags = Vec::default(); for arg in build_config.args.iter().skip(1) { if seen_pkg { args.push(arg.clone()) } else if next_arg_is_build { build_flags.push(arg.clone()); next_arg_is_build = false; } else if arg.starts_with("-") { if let Some(has_arg) = is_build_flag(arg.trim_start_matches("-")) { next_arg_is_build = has_arg; } build_flags.push(arg.clone()) } else { program = arg.to_string(); seen_pkg = true; } } let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest { cwd: build_config.cwd.clone(), env: build_config.env.clone(), request: "launch".to_string(), mode: "debug".to_string(), program, args: args, build_flags, }) .unwrap(); Some(DebugScenario { label: resolved_label.to_string().into(), adapter: adapter.0.clone(), build: None, config, tcp_connection: None, }) } _ => None, } } async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> { unreachable!() }} #[cfg(test)]mod tests { use super::*; use gpui::TestAppContext; use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskTemplate}; #[gpui::test] async fn test_create_scenario_for_go_build(_: &mut TestAppContext) { 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())) .await; assert!(scenario.is_none()); } #[gpui::test] async fn test_skip_non_go_commands_with_non_delve_adapter(_: &mut TestAppContext) { 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()), ) .await; assert!(scenario.is_none()); let scenario = locator .create_scenario(&task, "test label", &DebugAdapterName("Delve".into())) .await; assert!(scenario.is_none()); } #[gpui::test] async fn test_go_locator_run(_: &mut TestAppContext) { let locator = GoLocator; let delve = DebugAdapterName("Delve".into()); let task = TaskTemplate { label: "go run with flags".into(), command: "go".into(), args: vec![ "run".to_string(), "-race".to_string(), "-ldflags".to_string(), "-X main.version=1.0".to_string(), "./cmd/myapp".to_string(), "--config".to_string(), "production.yaml".to_string(), "--verbose".to_string(), ], env: { let mut env = HashMap::default(); env.insert("GO_ENV".to_string(), "production".to_string()); env }, cwd: Some("/project/root".into()), ..Default::default() }; let scenario = locator .create_scenario(&task, "test run label", &delve) .await .unwrap(); let config: DelveLaunchRequest = serde_json::from_value(scenario.config).unwrap(); assert_eq!( config, DelveLaunchRequest { request: "launch".to_string(), mode: "debug".to_string(), program: "./cmd/myapp".to_string(), build_flags: vec![ "-race".to_string(), "-ldflags".to_string(), "-X main.version=1.0".to_string() ], args: vec![ "--config".to_string(), "production.yaml".to_string(), "--verbose".to_string(), ], env: { let mut env = HashMap::default(); env.insert("GO_ENV".to_string(), "production".to_string()); env }, cwd: Some("/project/root".to_string()), } ); } #[gpui::test] async fn test_go_locator_test(_: &mut TestAppContext) { let locator = GoLocator; let delve = DebugAdapterName("Delve".into()); // Test with tags and run flag let task_with_tags = TaskTemplate { label: "test".into(), command: "go".into(), args: vec![ "test".to_string(), "-tags".to_string(), "integration,unit".to_string(), "-run".to_string(), "Foo".to_string(), ".".to_string(), ], ..Default::default() }; let result = locator .create_scenario(&task_with_tags, "", &delve) .await .unwrap(); let config: DelveLaunchRequest = serde_json::from_value(result.config).unwrap(); assert_eq!( config, DelveLaunchRequest { request: "launch".to_string(), mode: "test".to_string(), program: ".".to_string(), build_flags: vec!["-tags".to_string(), "integration,unit".to_string(),], args: vec![ "-test.run".to_string(), "Foo".to_string(), "-test.v".to_string() ], env: HashMap::default(), cwd: None, } ); } #[gpui::test] async fn test_skip_unsupported_go_commands(_: &mut TestAppContext) { 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())) .await; assert!(scenario.is_none()); }}
use anyhow::Result;
use async_trait::async_trait;
use collections::HashMap;
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
use gpui::SharedString;
use serde::{Deserialize, Serialize};
use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
pub(crate) struct GoLocator;
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
struct DelveLaunchRequest {
request: String,
mode: String,
program: String,
#[serde(skip_serializing_if = "Option::is_none")]
cwd: Option<String>,
args: Vec<String>,
build_flags: Vec<String>,
env: HashMap<String, String>,
}
fn is_debug_flag(arg: &str) -> Option<bool> {
let mut part = if let Some(suffix) = arg.strip_prefix("test.") {
suffix
} else {
arg
};
let mut might_have_arg = true;
if let Some(idx) = part.find('=') {
might_have_arg = false;
part = &part[..idx];
}
match part {
"benchmem" | "failfast" | "fullpath" | "fuzzworker" | "json" | "short" | "v"
| "paniconexit0" => Some(false),
"bench"
| "benchtime"
| "blockprofile"
| "blockprofilerate"
| "count"
| "coverprofile"
| "cpu"
| "cpuprofile"
| "fuzz"
| "fuzzcachedir"
| "fuzzminimizetime"
| "fuzztime"
| "gocoverdir"
| "list"
| "memprofile"
| "memprofilerate"
| "mutexprofile"
| "mutexprofilefraction"
| "outputdir"
| "parallel"
| "run"
| "shuffle"
| "skip"
| "testlogfile"
| "timeout"
| "trace" => Some(might_have_arg),
_ if arg.starts_with("test.") => Some(false),
_ => None,
}
}
fn is_build_flag(mut arg: &str) -> Option<bool> {
let mut might_have_arg = true;
if let Some(idx) = arg.find('=') {
might_have_arg = false;
arg = &arg[..idx];
}
match arg {
"a" | "n" | "race" | "msan" | "asan" | "cover" | "work" | "x" | "v" | "buildvcs"
| "json" | "linkshared" | "modcacherw" | "trimpath" => Some(false),
"p" | "covermode" | "coverpkg" | "asmflags" | "buildmode" | "compiler" | "gccgoflags"
| "gcflags" | "installsuffix" | "ldflags" | "mod" | "modfile" | "overlay" | "pgo"
| "pkgdir" | "tags" | "toolexec" => Some(might_have_arg),
_ => None,
}
}
#[async_trait]
impl DapLocator for GoLocator {
fn name(&self) -> SharedString {
SharedString::new_static("go-debug-locator")
}
async fn create_scenario(
&self,
build_config: &TaskTemplate,
resolved_label: &str,
adapter: &DebugAdapterName,
) -> Option<DebugScenario> {
if build_config.command != "go" {
return None;
}
let go_action = build_config.args.first()?;
match go_action.as_str() {
"test" => {
let mut program = ".".to_string();
let mut args = Vec::default();
let mut build_flags = Vec::default();
let mut all_args_are_test = false;
let mut next_arg_is_test = false;
let mut next_arg_is_build = false;
let mut seen_pkg = false;
let mut seen_v = false;
for arg in build_config.args.iter().skip(1) {
if all_args_are_test || next_arg_is_test {
// HACK: tasks assume that they are run in a shell context,
// so the -run regex has escaped specials. Delve correctly
// handles escaping, so we undo that here.
if arg.starts_with("\\^") && arg.ends_with("\\$") {
let mut arg = arg[1..arg.len() - 2].to_string();
arg.push('$');
args.push(arg);
} else {
args.push(arg.clone());
}
next_arg_is_test = false;
} else if next_arg_is_build {
build_flags.push(arg.clone());
next_arg_is_build = false;
} else if arg.starts_with('-') {
let flag = arg.trim_start_matches('-');
if flag == "args" {
all_args_are_test = true;
} else if let Some(has_arg) = is_debug_flag(flag) {
if flag == "v" || flag == "test.v" {
seen_v = true;
}
if flag.starts_with("test.") {
args.push(arg.clone());
} else {
args.push(format!("-test.{flag}"))
}
next_arg_is_test = has_arg;
} else if let Some(has_arg) = is_build_flag(flag) {
build_flags.push(arg.clone());
next_arg_is_build = has_arg;
}
} else if !seen_pkg {
program = arg.clone();
seen_pkg = true;
} else {
args.push(arg.clone());
}
}
if !seen_v {
args.push("-test.v".to_string());
}
let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest {
request: "launch".to_string(),
mode: "test".to_string(),
program,
args: args,
build_flags,
cwd: build_config.cwd.clone(),
env: build_config.env.clone(),
})
.unwrap();
Some(DebugScenario {
label: resolved_label.to_string().into(),
adapter: adapter.0.clone(),
build: None,
config: config,
tcp_connection: None,
})
}
"run" => {
let mut next_arg_is_build = false;
let mut seen_pkg = false;
let mut program = ".".to_string();
let mut args = Vec::default();
let mut build_flags = Vec::default();
for arg in build_config.args.iter().skip(1) {
if seen_pkg {
args.push(arg.clone())
} else if next_arg_is_build {
build_flags.push(arg.clone());
next_arg_is_build = false;
} else if arg.starts_with("-") {
if let Some(has_arg) = is_build_flag(arg.trim_start_matches("-")) {
next_arg_is_build = has_arg;
}
build_flags.push(arg.clone())
} else {
program = arg.to_string();
seen_pkg = true;
}
}
let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest {
cwd: build_config.cwd.clone(),
env: build_config.env.clone(),
request: "launch".to_string(),
mode: "debug".to_string(),
program,
args: args,
build_flags,
})
.unwrap();
Some(DebugScenario {
label: resolved_label.to_string().into(),
adapter: adapter.0.clone(),
build: None,
config,
tcp_connection: None,
})
}
_ => None,
}
}
async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
unreachable!()
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskTemplate};
#[gpui::test]
async fn test_create_scenario_for_go_build(_: &mut TestAppContext) {
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()))
.await;
assert!(scenario.is_none());
}
#[gpui::test]
async fn test_skip_non_go_commands_with_non_delve_adapter(_: &mut TestAppContext) {
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()),
)
.await;
assert!(scenario.is_none());
let scenario = locator
.create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
.await;
assert!(scenario.is_none());
}
#[gpui::test]
async fn test_go_locator_run(_: &mut TestAppContext) {
let locator = GoLocator;
let delve = DebugAdapterName("Delve".into());
let task = TaskTemplate {
label: "go run with flags".into(),
command: "go".into(),
args: vec![
"run".to_string(),
"-race".to_string(),
"-ldflags".to_string(),
"-X main.version=1.0".to_string(),
"./cmd/myapp".to_string(),
"--config".to_string(),
"production.yaml".to_string(),
"--verbose".to_string(),
],
env: {
let mut env = HashMap::default();
env.insert("GO_ENV".to_string(), "production".to_string());
env
},
cwd: Some("/project/root".into()),
..Default::default()
};
let scenario = locator
.create_scenario(&task, "test run label", &delve)
.await
.unwrap();
let config: DelveLaunchRequest = serde_json::from_value(scenario.config).unwrap();
assert_eq!(
config,
DelveLaunchRequest {
request: "launch".to_string(),
mode: "debug".to_string(),
program: "./cmd/myapp".to_string(),
build_flags: vec![
"-race".to_string(),
"-ldflags".to_string(),
"-X main.version=1.0".to_string()
],
args: vec![
"--config".to_string(),
"production.yaml".to_string(),
"--verbose".to_string(),
],
env: {
let mut env = HashMap::default();
env.insert("GO_ENV".to_string(), "production".to_string());
env
},
cwd: Some("/project/root".to_string()),
}
);
}
#[gpui::test]
async fn test_go_locator_test(_: &mut TestAppContext) {
let locator = GoLocator;
let delve = DebugAdapterName("Delve".into());
// Test with tags and run flag
let task_with_tags = TaskTemplate {
label: "test".into(),
command: "go".into(),
args: vec![
"test".to_string(),
"-tags".to_string(),
"integration,unit".to_string(),
"-run".to_string(),
"Foo".to_string(),
".".to_string(),
],
..Default::default()
};
let result = locator
.create_scenario(&task_with_tags, "", &delve)
.await
.unwrap();
let config: DelveLaunchRequest = serde_json::from_value(result.config).unwrap();
assert_eq!(
config,
DelveLaunchRequest {
request: "launch".to_string(),
mode: "test".to_string(),
program: ".".to_string(),
build_flags: vec!["-tags".to_string(), "integration,unit".to_string(),],
args: vec![
"-test.run".to_string(),
"Foo".to_string(),
"-test.v".to_string()
],
env: HashMap::default(),
cwd: None,
}
);
}
#[gpui::test]
async fn test_skip_unsupported_go_commands(_: &mut TestAppContext) {
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()))
.await;
assert!(scenario.is_none());
}
}