Set up Rust debugger code runner tasks (#27571)

## Summary 
This PR starts the process of adding debug task locators to Zed's
debugger system. A task locator is a secondary resolution phase that
allows a debug task to run a command before starting a debug session and
then uses the output of the run command to configure itself.

Locators are most applicable when debugging a compiled language but will
be helpful for any language as well.

## Architecture

At a high level, this works by adding a debug task queue to `Workspace`.
Which add's a debug configuration associated with a `TaskId` whenever a
resolved task with a debug config is added to `TaskInventory`'s queue.
Then, when the `SpawnInTerminal` task finishes running, it emits its
task_id and the result of the ran task.

When a ran task exits successfully, `Workspace` tells `Project` to start
a debug session using its stored debug config, then `DapStore` queries
the `LocatorStore` to configure the debug configuration if it has a
valid locator argument.

Release Notes:

- N/A
This commit is contained in:
Anthony Eid 2025-03-29 02:10:40 -04:00 committed by GitHub
parent 141a6c3915
commit 8add90d7cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 441 additions and 168 deletions

View file

@ -1,4 +1,3 @@
use anyhow::bail;
use gpui::AsyncApp;
use std::{ffi::OsStr, path::PathBuf};
use task::DebugTaskDefinition;
@ -63,9 +62,7 @@ impl DebugAdapter for GoDebugAdapter {
.and_then(|p| p.to_str().map(|p| p.to_string()))
.ok_or(anyhow!("Dlv not found in path"))?;
let Some(tcp_connection) = config.tcp_connection.clone() else {
bail!("Go Debug Adapter expects tcp connection arguments to be provided");
};
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {

View file

@ -78,11 +78,7 @@ impl DebugAdapter for JsDebugAdapter {
.ok_or_else(|| anyhow!("Couldn't find JavaScript dap directory"))?
};
let Some(tcp_connection) = config.tcp_connection.clone() else {
anyhow::bail!(
"Javascript Debug Adapter expects tcp connection arguments to be provided"
);
};
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {

View file

@ -1,5 +1,4 @@
use adapters::latest_github_release;
use anyhow::bail;
use dap::adapters::TcpArguments;
use gpui::AsyncApp;
use std::path::PathBuf;
@ -69,9 +68,7 @@ impl DebugAdapter for PhpDebugAdapter {
.ok_or_else(|| anyhow!("Couldn't find PHP dap directory"))?
};
let Some(tcp_connection) = config.tcp_connection.clone() else {
bail!("PHP Debug Adapter expects tcp connection arguments to be provided");
};
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {

View file

@ -1,5 +1,4 @@
use crate::*;
use anyhow::bail;
use dap::DebugRequestType;
use gpui::AsyncApp;
use std::{ffi::OsStr, path::PathBuf};
@ -70,9 +69,7 @@ impl DebugAdapter for PythonDebugAdapter {
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
let Some(tcp_connection) = config.tcp_connection.clone() else {
bail!("Python Debug Adapter expects tcp connection arguments to be provided");
};
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let debugpy_dir = if let Some(user_installed_path) = user_installed_path {

View file

@ -159,6 +159,8 @@ impl Render for InertState {
}),
tcp_connection: Some(TCPHost::default()),
initialize_args: None,
args: Default::default(),
locator: None,
},
});
} else {
@ -319,6 +321,8 @@ impl InertState {
adapter: kind,
request: DebugRequestType::Attach(task::AttachConfig { process_id: None }),
initialize_args: None,
args: Default::default(),
locator: None,
tcp_connection: Some(TCPHost::default()),
};

View file

@ -89,6 +89,8 @@ async fn test_show_attach_modal_and_select_process(
label: "attach example".into(),
initialize_args: None,
tcp_connection: Some(TCPHost::default()),
locator: None,
args: Default::default(),
},
vec![
Candidate {

View file

@ -4850,6 +4850,7 @@ impl Editor {
} else {
return None;
};
let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item);
let action = actions_menu.actions.get(action_ix)?;
let title = action.label();
@ -4858,7 +4859,8 @@ impl Editor {
match action {
CodeActionsItem::Task(task_source_kind, resolved_task) => {
workspace.update(cx, |workspace, cx| {
match resolved_task.task_type() {
task::TaskType::Script => workspace.update(cx, |workspace, cx| {
workspace::tasks::schedule_resolved_task(
workspace,
task_source_kind,
@ -4868,7 +4870,37 @@ impl Editor {
);
Some(Task::ready(Ok(())))
}),
task::TaskType::Debug(debug_args) => {
if debug_args.locator.is_some() {
workspace.update(cx, |workspace, cx| {
workspace::tasks::schedule_resolved_task(
workspace,
task_source_kind,
resolved_task,
false,
cx,
);
});
return Some(Task::ready(Ok(())));
}
if let Some(project) = self.project.as_ref() {
project
.update(cx, |project, cx| {
project.start_debug_session(
resolved_task.resolved_debug_adapter_config().unwrap(),
cx,
)
})
.detach_and_log_err(cx);
Some(Task::ready(Ok(())))
} else {
Some(Task::ready(Ok(())))
}
}
}
}
CodeActionsItem::CodeAction {
excerpt_id,
@ -8600,7 +8632,7 @@ impl Editor {
let line_len = snapshot.buffer_snapshot.line_len(MultiBufferRow(row));
let anchor_end = snapshot
.buffer_snapshot
.anchor_before(Point::new(row, line_len));
.anchor_after(Point::new(row, line_len));
let bp = self
.breakpoint_store

View file

@ -17,7 +17,7 @@ use std::{
path::{Path, PathBuf},
sync::{Arc, LazyLock},
};
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use task::{TaskTemplate, TaskTemplates, TaskType, TaskVariables, VariableName};
use util::{fs::remove_matching, maybe, ResultExt};
use crate::language_settings::language_settings;
@ -574,11 +574,16 @@ impl ContextProvider for RustContextProvider {
.variables
.get(CUSTOM_TARGET_DIR)
.cloned();
let run_task_args = if let Some(package_to_run) = package_to_run {
let run_task_args = if let Some(package_to_run) = package_to_run.clone() {
vec!["run".into(), "-p".into(), package_to_run]
} else {
vec!["run".into()]
};
let debug_task_args = if let Some(package_to_run) = package_to_run {
vec!["build".into(), "-p".into(), package_to_run]
} else {
vec!["build".into()]
};
let mut task_templates = vec![
TaskTemplate {
label: format!(
@ -620,6 +625,31 @@ impl ContextProvider for RustContextProvider {
cwd: Some("$ZED_DIRNAME".to_owned()),
..TaskTemplate::default()
},
TaskTemplate {
label: format!(
"Debug Test '{}' (package: {})",
RUST_TEST_NAME_TASK_VARIABLE.template_value(),
RUST_PACKAGE_TASK_VARIABLE.template_value(),
),
task_type: TaskType::Debug(task::DebugArgs {
adapter: "LLDB".to_owned(),
request: task::DebugArgsRequest::Launch,
locator: Some("cargo".into()),
tcp_connection: None,
initialize_args: None,
}),
command: "cargo".into(),
args: vec![
"test".into(),
"-p".into(),
RUST_PACKAGE_TASK_VARIABLE.template_value(),
RUST_TEST_NAME_TASK_VARIABLE.template_value(),
"--no-run".into(),
],
tags: vec!["rust-test".to_owned()],
cwd: Some("$ZED_DIRNAME".to_owned()),
..TaskTemplate::default()
},
TaskTemplate {
label: format!(
"Doc test '{}' (package: {})",
@ -697,6 +727,21 @@ impl ContextProvider for RustContextProvider {
cwd: Some("$ZED_DIRNAME".to_owned()),
..TaskTemplate::default()
},
TaskTemplate {
label: "Debug".into(),
cwd: Some("$ZED_DIRNAME".to_owned()),
command: "cargo".into(),
task_type: TaskType::Debug(task::DebugArgs {
request: task::DebugArgsRequest::Launch,
adapter: "LLDB".to_owned(),
initialize_args: None,
locator: Some("cargo".into()),
tcp_connection: None,
}),
args: debug_task_args,
tags: vec!["rust-main".to_owned()],
..TaskTemplate::default()
},
TaskTemplate {
label: "Clean".into(),
command: "cargo".into(),

View file

@ -14,4 +14,5 @@
pub mod breakpoint_store;
pub mod dap_command;
pub mod dap_store;
mod locator_store;
pub mod session;

View file

@ -12,7 +12,7 @@ use rpc::{
AnyProtoClient, TypedEnvelope,
};
use std::{hash::Hash, ops::Range, path::Path, sync::Arc};
use text::{Point, PointUtf16};
use text::PointUtf16;
use crate::{buffer_store::BufferStore, worktree_store::WorktreeStore, Project, ProjectPath};
@ -494,7 +494,7 @@ impl BreakpointStore {
this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
for bp in bps {
let position = snapshot.anchor_after(Point::new(bp.row, 0));
let position = snapshot.anchor_after(PointUtf16::new(bp.row, 0));
breakpoints_for_file.breakpoints.push((
position,
Breakpoint {

View file

@ -1,11 +1,6 @@
use super::{
breakpoint_store::BreakpointStore,
// Will need to uncomment this once we implement rpc message handler again
// dap_command::{
// ContinueCommand, DapCommand, DisconnectCommand, NextCommand, PauseCommand, RestartCommand,
// RestartStackFrameCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand,
// TerminateCommand, TerminateThreadsCommand, VariablesCommand,
// },
locator_store::LocatorStore,
session::{self, Session},
};
use crate::{debugger, worktree_store::WorktreeStore, ProjectEnvironment};
@ -87,6 +82,7 @@ pub struct LocalDapStore {
language_registry: Arc<LanguageRegistry>,
debug_adapters: Arc<DapRegistry>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
locator_store: Arc<LocatorStore>,
start_debugging_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
_start_debugging_task: Task<()>,
}
@ -179,6 +175,7 @@ impl DapStore {
debug_adapters,
start_debugging_tx,
_start_debugging_task,
locator_store: Arc::from(LocatorStore::new()),
next_session_id: Default::default(),
}),
downstream_client: None,
@ -324,7 +321,7 @@ impl DapStore {
pub fn new_session(
&mut self,
config: DebugAdapterConfig,
mut config: DebugAdapterConfig,
worktree: &Entity<Worktree>,
parent_session: Option<Entity<Session>>,
cx: &mut Context<Self>,
@ -354,22 +351,39 @@ impl DapStore {
}
let (initialized_tx, initialized_rx) = oneshot::channel();
let locator_store = local_store.locator_store.clone();
let debug_adapters = local_store.debug_adapters.clone();
let start_client_task = Session::local(
self.breakpoint_store.clone(),
let start_debugging_tx = local_store.start_debugging_tx.clone();
let task = cx.spawn(async move |this, cx| {
if config.locator.is_some() {
locator_store.resolve_debug_config(&mut config).await?;
}
let start_client_task = this.update(cx, |this, cx| {
Session::local(
this.breakpoint_store.clone(),
session_id,
parent_session,
delegate,
config,
local_store.start_debugging_tx.clone(),
start_debugging_tx.clone(),
initialized_tx,
local_store.debug_adapters.clone(),
debug_adapters,
cx,
);
)
})?;
this.update(cx, |_, cx| {
create_new_session(session_id, initialized_rx, start_client_task, cx)
})?
.await
});
let task = create_new_session(session_id, initialized_rx, start_client_task, cx);
(session_id, task)
}
#[cfg(any(test, feature = "test-support"))]
pub fn new_fake_session(
&mut self,
@ -456,7 +470,10 @@ impl DapStore {
request: DebugRequestDisposition::ReverseRequest(args),
initialize_args: config.initialize_args.clone(),
tcp_connection: config.tcp_connection.clone(),
locator: None,
args: Default::default(),
};
#[cfg(any(test, feature = "test-support"))]
let new_session_task = {
let caps = parent_session.read(cx).capabilities.clone();

View file

@ -0,0 +1,39 @@
use anyhow::{anyhow, Result};
use cargo::CargoLocator;
use collections::HashMap;
use dap::DebugAdapterConfig;
use gpui::SharedString;
use locators::DapLocator;
mod cargo;
mod locators;
pub(super) struct LocatorStore {
locators: HashMap<SharedString, Box<dyn DapLocator>>,
}
impl LocatorStore {
pub(super) fn new() -> Self {
let locators = HashMap::from_iter([(
SharedString::new("cargo"),
Box::new(CargoLocator {}) as Box<dyn DapLocator>,
)]);
Self { locators }
}
pub(super) async fn resolve_debug_config(
&self,
debug_config: &mut DebugAdapterConfig,
) -> Result<()> {
let Some(ref locator_name) = &debug_config.locator else {
log::debug!("Attempted to resolve debug config without a locator field");
return Ok(());
};
if let Some(locator) = self.locators.get(locator_name as &str) {
locator.run_locator(debug_config).await
} else {
Err(anyhow!("Couldn't find locator {}", locator_name))
}
}
}

View file

@ -0,0 +1,65 @@
use super::DapLocator;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use dap::DebugAdapterConfig;
use serde_json::Value;
use smol::{
io::AsyncReadExt,
process::{Command, Stdio},
};
pub(super) struct CargoLocator {}
#[async_trait]
impl DapLocator for CargoLocator {
async fn run_locator(&self, debug_config: &mut DebugAdapterConfig) -> Result<()> {
let Some(launch_config) = (match &mut debug_config.request {
task::DebugRequestDisposition::UserConfigured(task::DebugRequestType::Launch(
launch_config,
)) => Some(launch_config),
_ => None,
}) else {
return Err(anyhow!("Couldn't get launch config in locator"));
};
let Some(cwd) = launch_config.cwd.clone() else {
return Err(anyhow!(
"Couldn't get cwd from debug config which is needed for locators"
));
};
let mut child = Command::new("cargo")
.args(&debug_config.args)
.arg("--message-format=json")
.current_dir(cwd)
.stdout(Stdio::piped())
.spawn()?;
let mut output = String::new();
if let Some(mut stdout) = child.stdout.take() {
stdout.read_to_string(&mut output).await?;
}
let status = child.status().await?;
if !status.success() {
return Err(anyhow::anyhow!("Cargo command failed"));
}
let Some(executable) = output
.lines()
.filter(|line| !line.trim().is_empty())
.filter_map(|line| serde_json::from_str(line).ok())
.find_map(|json: Value| {
json.get("executable")
.and_then(Value::as_str)
.map(String::from)
})
else {
return Err(anyhow!("Couldn't get executable in cargo locator"));
};
launch_config.program = executable;
debug_config.args.clear();
Ok(())
}
}

View file

@ -0,0 +1,8 @@
use anyhow::Result;
use async_trait::async_trait;
use dap::DebugAdapterConfig;
#[async_trait]
pub(super) trait DapLocator {
async fn run_locator(&self, debug_config: &mut DebugAdapterConfig) -> Result<()>;
}

View file

@ -165,6 +165,7 @@ pub struct Project {
languages: Arc<LanguageRegistry>,
debug_adapters: Arc<DapRegistry>,
dap_store: Entity<DapStore>,
breakpoint_store: Entity<BreakpointStore>,
client: Arc<client::Client>,
join_project_response_message_id: u32,
@ -952,6 +953,7 @@ impl Project {
ssh_client: None,
breakpoint_store,
dap_store,
buffers_needing_diff: Default::default(),
git_diff_debouncer: DebouncedDelay::new(),
terminals: Terminals {
@ -1450,6 +1452,12 @@ impl Project {
}
}
pub fn queue_debug_session(&mut self, config: DebugAdapterConfig, cx: &mut Context<Self>) {
if config.locator.is_none() {
self.start_debug_session(config, cx).detach_and_log_err(cx);
}
}
pub fn start_debug_session(
&mut self,
config: DebugAdapterConfig,
@ -1490,6 +1498,8 @@ impl Project {
request: DebugRequestDisposition::UserConfigured(request),
initialize_args: None,
tcp_connection: None,
locator: None,
args: Default::default(),
};
let caps = caps.unwrap_or(Capabilities {
supports_step_back: Some(false),

View file

@ -5,7 +5,7 @@ use std::net::Ipv4Addr;
use std::path::PathBuf;
use util::ResultExt;
use crate::{TaskTemplate, TaskTemplates, TaskType};
use crate::{task_template::DebugArgs, TaskTemplate, TaskTemplates, TaskType};
impl Default for DebugConnectionType {
fn default() -> Self {
@ -102,6 +102,10 @@ pub struct DebugAdapterConfig {
/// spawning a new process. This is useful for connecting to a debug adapter
/// that is already running or is started by another process.
pub tcp_connection: Option<TCPHost>,
/// What Locator to use to configure the debug task
pub locator: Option<String>,
/// Args to pass to a debug adapter (only used in locator right now)
pub args: Vec<String>,
}
impl From<DebugTaskDefinition> for DebugAdapterConfig {
@ -112,6 +116,8 @@ impl From<DebugTaskDefinition> for DebugAdapterConfig {
request: DebugRequestDisposition::UserConfigured(def.request),
initialize_args: def.initialize_args,
tcp_connection: def.tcp_connection,
locator: def.locator,
args: def.args,
}
}
}
@ -130,6 +136,8 @@ impl TryFrom<DebugAdapterConfig> for DebugTaskDefinition {
request,
initialize_args: def.initialize_args,
tcp_connection: def.tcp_connection,
locator: def.locator,
args: def.args,
})
}
}
@ -137,18 +145,30 @@ impl TryFrom<DebugAdapterConfig> for DebugTaskDefinition {
impl DebugTaskDefinition {
/// Translate from debug definition to a task template
pub fn to_zed_format(self) -> anyhow::Result<TaskTemplate> {
let command = "".to_string();
let cwd = if let DebugRequestType::Launch(ref launch) = self.request {
launch
let (command, cwd, request) = match self.request {
DebugRequestType::Launch(launch_config) => (
launch_config.program,
launch_config
.cwd
.as_ref()
.map(|path| path.to_string_lossy().into_owned())
} else {
None
.map(|cwd| cwd.to_string_lossy().to_string()),
crate::task_template::DebugArgsRequest::Launch,
),
DebugRequestType::Attach(attach_config) => (
"".to_owned(),
None,
crate::task_template::DebugArgsRequest::Attach(attach_config),
),
};
let task_type = TaskType::Debug(DebugArgs {
adapter: self.adapter,
request,
initialize_args: self.initialize_args,
locator: self.locator,
tcp_connection: self.tcp_connection,
});
let label = self.label.clone();
let task_type = TaskType::Debug(self);
Ok(TaskTemplate {
label,
@ -189,6 +209,12 @@ pub struct DebugTaskDefinition {
/// spawning a new process. This is useful for connecting to a debug adapter
/// that is already running or is started by another process.
pub tcp_connection: Option<TCPHost>,
/// Locator to use
/// -- cargo
pub locator: Option<String>,
/// Args to pass to a debug adapter (only used in locator right now)
#[serde(skip)]
pub args: Vec<String>,
}
/// A group of Debug Tasks defined in a JSON file.

View file

@ -19,7 +19,8 @@ pub use debug_format::{
DebugRequestType, DebugTaskDefinition, DebugTaskFile, LaunchConfig, TCPHost,
};
pub use task_template::{
HideStrategy, RevealStrategy, TaskModal, TaskTemplate, TaskTemplates, TaskType,
DebugArgs, DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate,
TaskTemplates, TaskType,
};
pub use vscode_format::VsCodeTaskFile;
pub use zed_actions::RevealTarget;
@ -61,8 +62,6 @@ pub struct SpawnInTerminal {
pub hide: HideStrategy,
/// Which shell to use when spawning the task.
pub shell: Shell,
/// Tells debug tasks which program to debug
pub program: Option<String>,
/// Whether to show the task summary line in the task output (sucess/failure).
pub show_summary: bool,
/// Whether to show the command line in the task output.
@ -104,24 +103,50 @@ impl ResolvedTask {
}
/// Get the configuration for the debug adapter that should be used for this task.
pub fn resolved_debug_adapter_config(&self) -> Option<DebugTaskDefinition> {
pub fn resolved_debug_adapter_config(&self) -> Option<DebugAdapterConfig> {
match self.original_task.task_type.clone() {
TaskType::Script => None,
TaskType::Debug(mut adapter_config) => {
if let Some(resolved) = &self.resolved {
adapter_config.label = resolved.label.clone();
if let DebugRequestType::Launch(ref mut launch) = adapter_config.request {
if let Some(program) = resolved.program.clone() {
launch.program = program;
}
if let Some(cwd) = resolved.cwd.clone() {
launch.cwd = Some(cwd);
}
}
}
TaskType::Debug(debug_args) if self.resolved.is_some() => {
let resolved = self
.resolved
.as_ref()
.expect("We just checked if this was some");
Some(adapter_config)
let args = resolved
.args
.iter()
.cloned()
.map(|arg| {
if arg.starts_with("$") {
arg.strip_prefix("$")
.and_then(|arg| resolved.env.get(arg).map(ToOwned::to_owned))
.unwrap_or_else(|| arg)
} else {
arg
}
})
.collect();
Some(DebugAdapterConfig {
label: resolved.label.clone(),
adapter: debug_args.adapter.clone(),
request: DebugRequestDisposition::UserConfigured(match debug_args.request {
crate::task_template::DebugArgsRequest::Launch => {
DebugRequestType::Launch(LaunchConfig {
program: resolved.command.clone(),
cwd: resolved.cwd.clone(),
})
}
crate::task_template::DebugArgsRequest::Attach(attach_config) => {
DebugRequestType::Attach(attach_config)
}
}),
initialize_args: debug_args.initialize_args,
tcp_connection: debug_args.tcp_connection,
args,
locator: debug_args.locator.clone(),
})
}
_ => None,
}
}

View file

@ -9,8 +9,8 @@ use sha2::{Digest, Sha256};
use util::{truncate_and_remove_front, ResultExt};
use crate::{
DebugRequestType, DebugTaskDefinition, ResolvedTask, RevealTarget, Shell, SpawnInTerminal,
TaskContext, TaskId, VariableName, ZED_VARIABLE_NAME_PREFIX,
AttachConfig, ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TCPHost, TaskContext, TaskId,
VariableName, ZED_VARIABLE_NAME_PREFIX,
};
/// A template definition of a Zed task to run.
@ -75,62 +75,39 @@ pub struct TaskTemplate {
pub show_command: bool,
}
#[derive(Deserialize, Eq, PartialEq, Clone, Debug)]
/// Use to represent debug request type
pub enum DebugArgsRequest {
/// launch (program, cwd) are stored in TaskTemplate as (command, cwd)
Launch,
/// Attach
Attach(AttachConfig),
}
#[derive(Deserialize, Eq, PartialEq, Clone, Debug)]
/// This represents the arguments for the debug task.
pub struct DebugArgs {
/// The launch type
pub request: DebugArgsRequest,
/// Adapter choice
pub adapter: String,
/// TCP connection to make with debug adapter
pub tcp_connection: Option<TCPHost>,
/// Args to send to debug adapter
pub initialize_args: Option<serde_json::value::Value>,
/// the locator to use
pub locator: Option<String>,
}
/// Represents the type of task that is being ran
#[derive(Default, Deserialize, Serialize, Eq, PartialEq, JsonSchema, Clone, Debug)]
#[serde(rename_all = "snake_case", tag = "type")]
#[derive(Default, Eq, PartialEq, Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum TaskType {
/// Act like a typically task that runs commands
#[default]
Script,
/// This task starts the debugger for a language
Debug(DebugTaskDefinition),
}
#[cfg(test)]
mod deserialization_tests {
use crate::LaunchConfig;
use super::*;
use serde_json::json;
#[test]
fn deserialize_task_type_script() {
let json = json!({"type": "script"});
let task_type: TaskType =
serde_json::from_value(json).expect("Failed to deserialize TaskType::Script");
assert_eq!(task_type, TaskType::Script);
}
#[test]
fn deserialize_task_type_debug() {
let adapter_config = DebugTaskDefinition {
label: "test config".into(),
adapter: "Debugpy".into(),
request: crate::DebugRequestType::Launch(LaunchConfig {
program: "main".to_string(),
cwd: None,
}),
initialize_args: None,
tcp_connection: None,
};
let json = json!({
"label": "test config",
"type": "debug",
"adapter": "Debugpy",
"program": "main",
"supports_attach": false,
});
let task_type: TaskType =
serde_json::from_value(json).expect("Failed to deserialize TaskType::Debug");
if let TaskType::Debug(config) = task_type {
assert_eq!(config, adapter_config);
} else {
panic!("Expected TaskType::Debug");
}
}
Debug(DebugArgs),
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -270,22 +247,6 @@ impl TaskTemplate {
&mut substituted_variables,
)?;
let program = match &self.task_type {
TaskType::Script => None,
TaskType::Debug(adapter_config) => {
if let DebugRequestType::Launch(ref launch) = &adapter_config.request {
Some(substitute_all_template_variables_in_str(
&launch.program,
&task_variables,
&variable_names,
&mut substituted_variables,
)?)
} else {
None
}
}
};
let task_hash = to_hex_hash(self)
.context("hashing task template")
.log_err()?;
@ -341,7 +302,6 @@ impl TaskTemplate {
reveal_target: self.reveal_target,
hide: self.hide,
shell: self.shell.clone(),
program,
show_summary: self.show_summary,
show_command: self.show_command,
show_rerun: true,

View file

@ -10,7 +10,8 @@ use gpui::{
use picker::{highlighted_match_with_paths::HighlightedMatch, Picker, PickerDelegate};
use project::{task_store::TaskStore, TaskSourceKind};
use task::{
DebugRequestType, ResolvedTask, RevealTarget, TaskContext, TaskModal, TaskTemplate, TaskType,
DebugRequestType, DebugTaskDefinition, ResolvedTask, RevealTarget, TaskContext, TaskModal,
TaskTemplate, TaskType,
};
use ui::{
div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color,
@ -320,15 +321,11 @@ impl PickerDelegate for TasksModalDelegate {
self.workspace
.update(cx, |workspace, cx| {
match task.task_type() {
TaskType::Script => schedule_resolved_task(
workspace,
task_source_kind,
task,
omit_history_entry,
cx,
),
TaskType::Debug(_) => {
let Some(config) = task.resolved_debug_adapter_config() else {
TaskType::Debug(config) if config.locator.is_none() => {
let Some(config): Option<DebugTaskDefinition> = task
.resolved_debug_adapter_config()
.and_then(|config| config.try_into().ok())
else {
return;
};
let project = workspace.project().clone();
@ -355,6 +352,13 @@ impl PickerDelegate for TasksModalDelegate {
}
}
}
_ => schedule_resolved_task(
workspace,
task_source_kind,
task,
omit_history_entry,
cx,
),
};
})
.ok();
@ -517,16 +521,30 @@ impl PickerDelegate for TasksModalDelegate {
omit_history_entry,
cx,
),
// TODO: Should create a schedule_resolved_debug_task function
// todo(debugger): Should create a schedule_resolved_debug_task function
// This would allow users to access to debug history and other issues
TaskType::Debug(_) => workspace.project().update(cx, |project, cx| {
project
.start_debug_session(
task.resolved_debug_adapter_config().unwrap().into(),
TaskType::Debug(debug_args) => {
let Some(debug_config) = task.resolved_debug_adapter_config() else {
// todo(debugger) log an error, this should never happen
return;
};
if debug_args.locator.is_some() {
schedule_resolved_task(
workspace,
task_source_kind,
task,
omit_history_entry,
cx,
)
);
} else {
workspace.project().update(cx, |project, cx| {
project
.start_debug_session(debug_config, cx)
.detach_and_log_err(cx);
}),
});
}
}
};
})
.ok();

View file

@ -110,6 +110,7 @@ pub enum Event {
SelectionsChanged,
NewNavigationTarget(Option<MaybeNavigationTarget>),
Open(MaybeNavigationTarget),
TaskLocatorReady { task_id: TaskId, success: bool },
}
#[derive(Clone, Debug)]
@ -1899,6 +1900,11 @@ impl Terminal {
unsafe { append_text_to_term(&mut self.term.lock(), &lines_to_show) };
}
cx.emit(Event::TaskLocatorReady {
task_id: task.id.clone(),
success: finished_successfully,
});
match task.hide {
HideStrategy::Never => {}
HideStrategy::Always => {

View file

@ -982,6 +982,15 @@ fn subscribe_for_terminal_events(
window.invalidate_character_coordinates();
cx.emit(SearchEvent::ActiveMatchChanged)
}
Event::TaskLocatorReady { task_id, success } => {
if *success {
workspace
.update(cx, |workspace, cx| {
workspace.debug_task_ready(task_id, cx);
})
.log_err();
}
}
},
);
vec![terminal_subscription, terminal_events_subscription]

View file

@ -1440,7 +1440,6 @@ impl ShellExec {
reveal_target: RevealTarget::Dock,
hide: HideStrategy::Never,
shell,
program: None,
show_summary: false,
show_command: false,
show_rerun: false,

View file

@ -46,7 +46,15 @@ pub fn schedule_resolved_task(
omit_history: bool,
cx: &mut Context<Workspace>,
) {
let debug_config = resolved_task.resolved_debug_adapter_config();
if let Some(spawn_in_terminal) = resolved_task.resolved.take() {
if let Some(debug_config) = debug_config {
workspace
.debug_task_queue
.insert(resolved_task.id.clone(), debug_config);
}
if !omit_history {
resolved_task.resolved = Some(spawn_in_terminal.clone());
workspace.project().update(cx, |project, cx| {

View file

@ -94,7 +94,7 @@ use std::{
sync::{atomic::AtomicUsize, Arc, LazyLock, Weak},
time::Duration,
};
use task::SpawnInTerminal;
use task::{DebugAdapterConfig, SpawnInTerminal, TaskId};
use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub use ui;
@ -874,6 +874,7 @@ pub struct Workspace {
serialized_ssh_project: Option<SerializedSshProject>,
_items_serializer: Task<Result<()>>,
session_id: Option<String>,
debug_task_queue: HashMap<task::TaskId, DebugAdapterConfig>,
}
impl EventEmitter<Event> for Workspace {}
@ -1185,6 +1186,7 @@ impl Workspace {
_items_serializer,
session_id: Some(session_id),
serialized_ssh_project: None,
debug_task_queue: Default::default(),
}
}
@ -5191,6 +5193,16 @@ impl Workspace {
.update(cx, |_, window, _| window.activate_window())
.ok();
}
pub fn debug_task_ready(&mut self, task_id: &TaskId, cx: &mut App) {
if let Some(debug_config) = self.debug_task_queue.remove(task_id) {
self.project.update(cx, |project, cx| {
project
.start_debug_session(debug_config, cx)
.detach_and_log_err(cx);
})
}
}
}
fn leader_border_for_pane(