Files
oak-gpui/crates/project/src/debugger/locators/go.rs
T
Piotr Osiewicz d5b8c21a75 debugger: Mark DapLocator::create_scenario as an async function (#32680)
Paves way for locators in extensions.

Release Notes:

- N/A
2025-06-13 13:19:03 +02:00

429 lines
14 KiB
Rust

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());
}
}