debugger/tasks: Remove TaskType enum (#29208)

Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
Piotr Osiewicz 2025-04-26 01:44:56 +02:00 committed by GitHub
parent 053fafa90e
commit 67615b968b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 1272 additions and 1114 deletions

View file

@ -800,6 +800,7 @@ impl LocalSettingsKind {
proto::LocalSettingsKind::Settings => Self::Settings, proto::LocalSettingsKind::Settings => Self::Settings,
proto::LocalSettingsKind::Tasks => Self::Tasks, proto::LocalSettingsKind::Tasks => Self::Tasks,
proto::LocalSettingsKind::Editorconfig => Self::Editorconfig, proto::LocalSettingsKind::Editorconfig => Self::Editorconfig,
proto::LocalSettingsKind::Debug => Self::Debug,
} }
} }
@ -808,6 +809,7 @@ impl LocalSettingsKind {
Self::Settings => proto::LocalSettingsKind::Settings, Self::Settings => proto::LocalSettingsKind::Settings,
Self::Tasks => proto::LocalSettingsKind::Tasks, Self::Tasks => proto::LocalSettingsKind::Tasks,
Self::Editorconfig => proto::LocalSettingsKind::Editorconfig, Self::Editorconfig => proto::LocalSettingsKind::Editorconfig,
Self::Debug => proto::LocalSettingsKind::Debug,
} }
} }
} }

View file

@ -32,4 +32,6 @@ pub enum LocalSettingsKind {
Tasks, Tasks,
#[sea_orm(string_value = "editorconfig")] #[sea_orm(string_value = "editorconfig")]
Editorconfig, Editorconfig,
#[sea_orm(string_value = "debug")]
Debug,
} }

View file

@ -680,6 +680,7 @@ async fn test_collaborating_with_code_actions(
editor.toggle_code_actions( editor.toggle_code_actions(
&ToggleCodeActions { &ToggleCodeActions {
deployed_from_indicator: None, deployed_from_indicator: None,
quick_launch: false,
}, },
window, window,
cx, cx,

View file

@ -1824,6 +1824,8 @@ async fn test_active_call_events(
server server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await; .await;
executor.run_until_parked();
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global);

View file

@ -14,10 +14,16 @@ use serde::{Deserialize, Serialize};
use settings::WorktreeId; use settings::WorktreeId;
use smol::{self, fs::File, lock::Mutex}; use smol::{self, fs::File, lock::Mutex};
use std::{ use std::{
borrow::Borrow, collections::HashSet, ffi::OsStr, fmt::Debug, net::Ipv4Addr, ops::Deref, borrow::Borrow,
path::PathBuf, sync::Arc, collections::HashSet,
ffi::OsStr,
fmt::Debug,
net::Ipv4Addr,
ops::Deref,
path::{Path, PathBuf},
sync::Arc,
}; };
use task::{DebugTaskDefinition, TcpArgumentsTemplate}; use task::{AttachRequest, DebugRequest, DebugScenario, LaunchRequest, TcpArgumentsTemplate};
use util::ResultExt; use util::ResultExt;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -109,6 +115,116 @@ impl TcpArguments {
} }
} }
/// Represents a debuggable binary/process (what process is going to be debugged and with what arguments).
///
/// We start off with a [DebugScenario], a user-facing type that additionally defines how a debug target is built; once
/// an optional build step is completed, we turn it's result into a DebugTaskDefinition by running a locator (or using a user-provided task) and resolving task variables.
/// Finally, a [DebugTaskDefinition] has to be turned into a concrete debugger invocation ([DebugAdapterBinary]).
#[derive(Clone, Debug, PartialEq)]
pub struct DebugTaskDefinition {
pub label: SharedString,
pub adapter: SharedString,
pub request: DebugRequest,
/// Additional initialization arguments to be sent on DAP initialization
pub initialize_args: Option<serde_json::Value>,
/// Whether to tell the debug adapter to stop on entry
pub stop_on_entry: Option<bool>,
/// Optional TCP connection information
///
/// If provided, this will be used to connect to the debug adapter instead of
/// spawning a new debug adapter process. This is useful for connecting to a debug adapter
/// that is already running or is started by another process.
pub tcp_connection: Option<TcpArgumentsTemplate>,
}
impl DebugTaskDefinition {
pub fn cwd(&self) -> Option<&Path> {
if let DebugRequest::Launch(config) = &self.request {
config.cwd.as_ref().map(Path::new)
} else {
None
}
}
pub fn to_scenario(&self) -> DebugScenario {
DebugScenario {
label: self.label.clone(),
adapter: self.adapter.clone(),
build: None,
request: Some(self.request.clone()),
stop_on_entry: self.stop_on_entry,
tcp_connection: self.tcp_connection.clone(),
initialize_args: self.initialize_args.clone(),
}
}
pub fn to_proto(&self) -> proto::DebugTaskDefinition {
proto::DebugTaskDefinition {
adapter: self.adapter.to_string(),
request: Some(match &self.request {
DebugRequest::Launch(config) => {
proto::debug_task_definition::Request::DebugLaunchRequest(
proto::DebugLaunchRequest {
program: config.program.clone(),
cwd: config.cwd.as_ref().map(|c| c.to_string_lossy().to_string()),
args: config.args.clone(),
env: config
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
},
)
}
DebugRequest::Attach(attach_request) => {
proto::debug_task_definition::Request::DebugAttachRequest(
proto::DebugAttachRequest {
process_id: attach_request.process_id.unwrap_or_default(),
},
)
}
}),
label: self.label.to_string(),
initialize_args: self.initialize_args.as_ref().map(|v| v.to_string()),
tcp_connection: self.tcp_connection.as_ref().map(|t| t.to_proto()),
stop_on_entry: self.stop_on_entry,
}
}
pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> {
let request = proto
.request
.ok_or_else(|| anyhow::anyhow!("request is required"))?;
Ok(Self {
label: proto.label.into(),
initialize_args: proto.initialize_args.map(|v| v.into()),
tcp_connection: proto
.tcp_connection
.map(TcpArgumentsTemplate::from_proto)
.transpose()?,
stop_on_entry: proto.stop_on_entry,
adapter: proto.adapter.into(),
request: match request {
proto::debug_task_definition::Request::DebugAttachRequest(config) => {
DebugRequest::Attach(AttachRequest {
process_id: Some(config.process_id),
})
}
proto::debug_task_definition::Request::DebugLaunchRequest(config) => {
DebugRequest::Launch(LaunchRequest {
program: config.program,
cwd: config.cwd.map(|cwd| cwd.into()),
args: config.args,
env: Default::default(),
})
}
},
})
}
}
/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DebugAdapterBinary { pub struct DebugAdapterBinary {
pub command: String, pub command: String,

View file

@ -6,7 +6,7 @@ mod registry;
pub mod transport; pub mod transport;
pub use dap_types::*; pub use dap_types::*;
pub use registry::DapRegistry; pub use registry::{DapLocator, DapRegistry};
pub use task::DebugRequest; pub use task::DebugRequest;
pub type ScopeId = u64; pub type ScopeId = u64;

View file

@ -1,12 +1,25 @@
use anyhow::Result;
use async_trait::async_trait;
use collections::FxHashMap;
use gpui::{App, Global}; use gpui::{App, Global};
use parking_lot::RwLock; use parking_lot::RwLock;
use task::{DebugRequest, SpawnInTerminal};
use crate::adapters::{DebugAdapter, DebugAdapterName}; use crate::adapters::{DebugAdapter, DebugAdapterName};
use std::{collections::BTreeMap, sync::Arc}; use std::{collections::BTreeMap, sync::Arc};
/// Given a user build configuration, locator creates a fill-in debug target ([DebugRequest]) on behalf of the user.
#[async_trait]
pub trait DapLocator: Send + Sync {
/// Determines whether this locator can generate debug target for given task.
fn accepts(&self, build_config: &SpawnInTerminal) -> bool;
async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest>;
}
#[derive(Default)] #[derive(Default)]
struct DapRegistryState { struct DapRegistryState {
adapters: BTreeMap<DebugAdapterName, Arc<dyn DebugAdapter>>, adapters: BTreeMap<DebugAdapterName, Arc<dyn DebugAdapter>>,
locators: FxHashMap<String, Arc<dyn DapLocator>>,
} }
#[derive(Clone, Default)] #[derive(Clone, Default)]
@ -35,6 +48,18 @@ impl DapRegistry {
); );
} }
pub fn add_locator(&self, name: String, locator: Arc<dyn DapLocator>) {
let _previous_value = self.0.write().locators.insert(name, locator);
debug_assert!(
_previous_value.is_none(),
"Attempted to insert a new debug locator when one is already registered"
);
}
pub fn locators(&self) -> FxHashMap<String, Arc<dyn DapLocator>> {
self.0.read().locators.clone()
}
pub fn adapter(&self, name: &str) -> Option<Arc<dyn DebugAdapter>> { pub fn adapter(&self, name: &str) -> Option<Arc<dyn DebugAdapter>> {
self.0.read().adapters.get(name).cloned() self.0.read().adapters.get(name).cloned()
} }

View file

@ -2,9 +2,9 @@ use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use async_trait::async_trait; use async_trait::async_trait;
use dap::adapters::{InlineValueProvider, latest_github_release}; use dap::adapters::{DebugTaskDefinition, InlineValueProvider, latest_github_release};
use gpui::AsyncApp; use gpui::AsyncApp;
use task::{DebugRequest, DebugTaskDefinition}; use task::DebugRequest;
use crate::*; use crate::*;
@ -25,7 +25,10 @@ impl CodeLldbDebugAdapter {
}); });
let map = configuration.as_object_mut().unwrap(); let map = configuration.as_object_mut().unwrap();
// CodeLLDB uses `name` for a terminal label. // CodeLLDB uses `name` for a terminal label.
map.insert("name".into(), Value::String(config.label.clone())); map.insert(
"name".into(),
Value::String(String::from(config.label.as_ref())),
);
let request = config.request.to_dap(); let request = config.request.to_dap();
match &config.request { match &config.request {
DebugRequest::Attach(attach) => { DebugRequest::Attach(attach) => {

View file

@ -2,9 +2,9 @@ use std::{collections::HashMap, ffi::OsStr};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use async_trait::async_trait; use async_trait::async_trait;
use dap::StartDebuggingRequestArguments; use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp; use gpui::AsyncApp;
use task::{DebugRequest, DebugTaskDefinition}; use task::DebugRequest;
use crate::*; use crate::*;

View file

@ -1,7 +1,6 @@
use dap::StartDebuggingRequestArguments; use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp; use gpui::AsyncApp;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf}; use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
use task::DebugTaskDefinition;
use crate::*; use crate::*;

View file

@ -1,8 +1,8 @@
use adapters::latest_github_release; use adapters::latest_github_release;
use dap::StartDebuggingRequestArguments; use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp; use gpui::AsyncApp;
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use task::{DebugRequest, DebugTaskDefinition}; use task::DebugRequest;
use crate::*; use crate::*;

View file

@ -1,8 +1,7 @@
use adapters::latest_github_release; use adapters::latest_github_release;
use dap::adapters::TcpArguments; use dap::adapters::{DebugTaskDefinition, TcpArguments};
use gpui::AsyncApp; use gpui::AsyncApp;
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use task::DebugTaskDefinition;
use crate::*; use crate::*;

View file

@ -1,8 +1,10 @@
use crate::*; use crate::*;
use dap::{StartDebuggingRequestArguments, adapters::InlineValueProvider}; use dap::{
DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition,
adapters::InlineValueProvider,
};
use gpui::AsyncApp; use gpui::AsyncApp;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf}; use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
use task::DebugTaskDefinition;
#[derive(Default)] #[derive(Default)]
pub(crate) struct PythonDebugAdapter; pub(crate) struct PythonDebugAdapter;

View file

@ -1,4 +1,5 @@
use dap::DebugRequest; use dap::DebugRequest;
use dap::adapters::DebugTaskDefinition;
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render}; use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
use gpui::{Subscription, WeakEntity}; use gpui::{Subscription, WeakEntity};
@ -24,20 +25,20 @@ pub(crate) struct AttachModalDelegate {
selected_index: usize, selected_index: usize,
matches: Vec<StringMatch>, matches: Vec<StringMatch>,
placeholder_text: Arc<str>, placeholder_text: Arc<str>,
pub(crate) definition: DebugTaskDefinition,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
pub(crate) debug_config: task::DebugTaskDefinition,
candidates: Arc<[Candidate]>, candidates: Arc<[Candidate]>,
} }
impl AttachModalDelegate { impl AttachModalDelegate {
fn new( fn new(
workspace: Entity<Workspace>, workspace: Entity<Workspace>,
debug_config: task::DebugTaskDefinition, definition: DebugTaskDefinition,
candidates: Arc<[Candidate]>, candidates: Arc<[Candidate]>,
) -> Self { ) -> Self {
Self { Self {
workspace: workspace.downgrade(), workspace: workspace.downgrade(),
debug_config, definition,
candidates, candidates,
selected_index: 0, selected_index: 0,
matches: Vec::default(), matches: Vec::default(),
@ -53,8 +54,8 @@ pub struct AttachModal {
impl AttachModal { impl AttachModal {
pub fn new( pub fn new(
definition: DebugTaskDefinition,
workspace: Entity<Workspace>, workspace: Entity<Workspace>,
debug_config: task::DebugTaskDefinition,
modal: bool, modal: bool,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@ -77,12 +78,12 @@ impl AttachModal {
.collect(); .collect();
processes.sort_by_key(|k| k.name.clone()); processes.sort_by_key(|k| k.name.clone());
let processes = processes.into_iter().collect(); let processes = processes.into_iter().collect();
Self::with_processes(workspace, debug_config, processes, modal, window, cx) Self::with_processes(workspace, definition, processes, modal, window, cx)
} }
pub(super) fn with_processes( pub(super) fn with_processes(
workspace: Entity<Workspace>, workspace: Entity<Workspace>,
debug_config: task::DebugTaskDefinition, definition: DebugTaskDefinition,
processes: Arc<[Candidate]>, processes: Arc<[Candidate]>,
modal: bool, modal: bool,
window: &mut Window, window: &mut Window,
@ -90,7 +91,7 @@ impl AttachModal {
) -> Self { ) -> Self {
let picker = cx.new(|cx| { let picker = cx.new(|cx| {
Picker::uniform_list( Picker::uniform_list(
AttachModalDelegate::new(workspace, debug_config, processes), AttachModalDelegate::new(workspace, definition, processes),
window, window,
cx, cx,
) )
@ -217,7 +218,7 @@ impl PickerDelegate for AttachModalDelegate {
return cx.emit(DismissEvent); return cx.emit(DismissEvent);
}; };
match &mut self.debug_config.request { match &mut self.definition.request {
DebugRequest::Attach(config) => { DebugRequest::Attach(config) => {
config.process_id = Some(candidate.pid); config.process_id = Some(candidate.pid);
} }
@ -227,7 +228,8 @@ impl PickerDelegate for AttachModalDelegate {
} }
} }
let definition = self.debug_config.clone(); let scenario = self.definition.to_scenario();
let panel = self let panel = self
.workspace .workspace
.update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx)) .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
@ -235,9 +237,10 @@ impl PickerDelegate for AttachModalDelegate {
.flatten(); .flatten();
if let Some(panel) = panel { if let Some(panel) = panel {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.start_session(definition, window, cx); panel.start_session(scenario, Default::default(), None, window, cx);
}); });
} }
cx.emit(DismissEvent); cx.emit(DismissEvent);
} }

View file

@ -9,11 +9,12 @@ use crate::{new_session_modal::NewSessionModal, session::DebugSession};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use collections::HashMap; use collections::HashMap;
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;
use dap::StartDebuggingRequestArguments; use dap::DebugRequest;
use dap::{ use dap::{
ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent, ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
client::SessionId, debugger_settings::DebuggerSettings, client::SessionId, debugger_settings::DebuggerSettings,
}; };
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use futures::{SinkExt as _, channel::mpsc}; use futures::{SinkExt as _, channel::mpsc};
use gpui::{ use gpui::{
Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter, Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
@ -21,6 +22,7 @@ use gpui::{
actions, anchored, deferred, actions, anchored, deferred,
}; };
use language::Buffer;
use project::debugger::session::{Session, SessionStateEvent}; use project::debugger::session::{Session, SessionStateEvent};
use project::{ use project::{
Project, Project,
@ -35,9 +37,7 @@ use settings::Settings;
use std::any::TypeId; use std::any::TypeId;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use task::{ use task::{DebugScenario, HideStrategy, RevealStrategy, RevealTarget, TaskContext, TaskId};
DebugTaskDefinition, DebugTaskTemplate, HideStrategy, RevealStrategy, RevealTarget, TaskId,
};
use terminal_view::TerminalView; use terminal_view::TerminalView;
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*}; use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
use workspace::SplitDirection; use workspace::SplitDirection;
@ -87,45 +87,8 @@ impl DebugPanel {
let project = workspace.project().clone(); let project = workspace.project().clone();
let dap_store = project.read(cx).dap_store(); let dap_store = project.read(cx).dap_store();
let weak = cx.weak_entity(); let _subscriptions =
vec![cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event)];
let modal_subscription =
cx.observe_new::<tasks_ui::TasksModal>(move |_, window, cx| {
let modal_entity = cx.entity();
weak.update(cx, |_: &mut DebugPanel, cx| {
let Some(window) = window else {
log::error!("Debug panel couldn't subscribe to tasks modal because there was no window");
return;
};
cx.subscribe_in(
&modal_entity,
window,
|panel, _, event: &tasks_ui::ShowAttachModal, window, cx| {
panel.workspace.update(cx, |workspace, cx| {
let workspace_handle = cx.entity().clone();
workspace.toggle_modal(window, cx, |window, cx| {
crate::attach_modal::AttachModal::new(
workspace_handle,
event.debug_config.clone(),
true,
window,
cx,
)
});
}).ok();
},
)
.detach();
})
.ok();
});
let _subscriptions = vec![
cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event),
modal_subscription,
];
let debug_panel = Self { let debug_panel = Self {
size: px(300.), size: px(300.),
@ -259,43 +222,16 @@ impl DebugPanel {
}) })
} }
pub fn start_session( fn start_from_definition(
&mut self, &mut self,
definition: DebugTaskDefinition, definition: DebugTaskDefinition,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) -> Task<Result<()>> {
let task_contexts = self
.workspace
.update(cx, |workspace, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})
.ok();
let dap_store = self.project.read(cx).dap_store().clone();
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let task_context = if let Some(task) = task_contexts { let dap_store = this.update(cx, |this, cx| this.project.read(cx).dap_store())?;
task.await
.active_worktree_context
.map_or(task::TaskContext::default(), |context| context.1)
} else {
task::TaskContext::default()
};
let (session, task) = dap_store.update(cx, |dap_store, cx| { let (session, task) = dap_store.update(cx, |dap_store, cx| {
let template = DebugTaskTemplate { let session = dap_store.new_session(definition, None, cx);
locator: None,
definition: definition.clone(),
};
let session = if let Some(debug_config) = template
.to_zed_format()
.resolve_task("debug_task", &task_context)
.and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
{
dap_store.new_session(debug_config.definition, None, cx)
} else {
dap_store.new_session(definition.clone(), None, cx)
};
(session.clone(), dap_store.boot_session(session, cx)) (session.clone(), dap_store.boot_session(session, cx))
})?; })?;
@ -318,6 +254,27 @@ impl DebugPanel {
anyhow::Ok(()) anyhow::Ok(())
}) })
}
pub fn start_session(
&mut self,
scenario: DebugScenario,
task_context: TaskContext,
active_buffer: Option<Entity<Buffer>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
cx.spawn_in(window, async move |this, cx| {
let definition = this
.update_in(cx, |this, window, cx| {
this.resolve_scenario(scenario, task_context, active_buffer, window, cx)
})?
.await?;
this.update_in(cx, |this, window, cx| {
this.start_from_definition(definition, window, cx)
})?
.await
})
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
@ -343,13 +300,13 @@ impl DebugPanel {
let definition = curr_session.update(cx, |session, _| session.definition()); let definition = curr_session.update(cx, |session, _| session.definition());
let task = curr_session.update(cx, |session, cx| session.shutdown(cx)); let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
let definition = definition.clone();
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
task.await; task.await;
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
this.start_session(definition, window, cx) this.start_from_definition(definition, window, cx)
}) })?
.await
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
@ -503,6 +460,75 @@ impl DebugPanel {
} }
} }
pub fn resolve_scenario(
&self,
scenario: DebugScenario,
task_context: TaskContext,
buffer: Option<Entity<Buffer>>,
window: &Window,
cx: &mut Context<Self>,
) -> Task<Result<DebugTaskDefinition>> {
let project = self.project.read(cx);
let dap_store = project.dap_store().downgrade();
let task_store = project.task_store().downgrade();
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |_, cx| {
let DebugScenario {
adapter,
label,
build,
request,
initialize_args,
tcp_connection,
stop_on_entry,
} = scenario;
let request = if let Some(mut request) = request {
// Resolve task variables within the request.
if let DebugRequest::Launch(_) = &mut request {}
request
} else if let Some(build) = build {
let Some(task) = task_store.update(cx, |this, cx| {
this.task_inventory().and_then(|inventory| {
inventory
.read(cx)
.task_template_by_label(buffer, &build, cx)
})
})?
else {
anyhow::bail!("Couldn't find task template for {:?}", build)
};
let Some(task) = task.resolve_task("debug-build-task", &task_context) else {
anyhow::bail!("Could not resolve task variables within a debug scenario");
};
let run_build = workspace.update_in(cx, |workspace, window, cx| {
workspace.spawn_in_terminal(task.resolved.clone(), window, cx)
})?;
let exit_status = run_build.await?;
if !exit_status.success() {
anyhow::bail!("Build failed");
}
dap_store
.update(cx, |this, cx| this.run_debug_locator(task.resolved, cx))?
.await?
} else {
return Err(anyhow!("No request or build provided"));
};
Ok(DebugTaskDefinition {
label,
adapter,
request,
initialize_args,
stop_on_entry,
tcp_connection,
})
})
}
fn handle_run_in_terminal_request( fn handle_run_in_terminal_request(
&self, &self,
session_id: SessionId, session_id: SessionId,
@ -1409,10 +1435,17 @@ impl Render for DebugPanel {
struct DebuggerProvider(Entity<DebugPanel>); struct DebuggerProvider(Entity<DebugPanel>);
impl workspace::DebuggerProvider for DebuggerProvider { impl workspace::DebuggerProvider for DebuggerProvider {
fn start_session(&self, definition: DebugTaskDefinition, window: &mut Window, cx: &mut App) { fn start_session(
&self,
definition: DebugScenario,
context: TaskContext,
buffer: Option<Entity<Buffer>>,
window: &mut Window,
cx: &mut App,
) {
self.0.update(cx, |_, cx| { self.0.update(cx, |_, cx| {
cx.defer_in(window, |this, window, cx| { cx.defer_in(window, |this, window, cx| {
this.start_session(definition, window, cx); this.start_session(definition, context, buffer, window, cx);
}) })
}) })
} }

View file

@ -4,14 +4,14 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use dap::{DapRegistry, DebugRequest}; use dap::{DapRegistry, DebugRequest, adapters::DebugTaskDefinition};
use editor::{Editor, EditorElement, EditorStyle}; use editor::{Editor, EditorElement, EditorStyle};
use gpui::{ use gpui::{
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle,
WeakEntity, WeakEntity,
}; };
use settings::Settings; use settings::Settings;
use task::{DebugTaskDefinition, DebugTaskTemplate, LaunchRequest}; use task::{DebugScenario, LaunchRequest, TaskContext};
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context, ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
@ -34,7 +34,7 @@ pub(super) struct NewSessionModal {
last_selected_profile_name: Option<SharedString>, last_selected_profile_name: Option<SharedString>,
} }
fn suggested_label(request: &DebugRequest, debugger: &str) -> String { fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
match request { match request {
DebugRequest::Launch(config) => { DebugRequest::Launch(config) => {
let last_path_component = Path::new(&config.program) let last_path_component = Path::new(&config.program)
@ -42,12 +42,13 @@ fn suggested_label(request: &DebugRequest, debugger: &str) -> String {
.map(|name| name.to_string_lossy()) .map(|name| name.to_string_lossy())
.unwrap_or_else(|| Cow::Borrowed(&config.program)); .unwrap_or_else(|| Cow::Borrowed(&config.program));
format!("{} ({debugger})", last_path_component) format!("{} ({debugger})", last_path_component).into()
} }
DebugRequest::Attach(config) => format!( DebugRequest::Attach(config) => format!(
"pid: {} ({debugger})", "pid: {} ({debugger})",
config.process_id.unwrap_or(u32::MAX) config.process_id.unwrap_or(u32::MAX)
), )
.into(),
} }
} }
@ -61,7 +62,7 @@ impl NewSessionModal {
) -> Self { ) -> Self {
let debugger = past_debug_definition let debugger = past_debug_definition
.as_ref() .as_ref()
.map(|def| def.adapter.clone().into()); .map(|def| def.adapter.clone());
let stop_on_entry = past_debug_definition let stop_on_entry = past_debug_definition
.as_ref() .as_ref()
@ -85,18 +86,20 @@ impl NewSessionModal {
} }
} }
fn debug_config(&self, cx: &App, debugger: &str) -> DebugTaskDefinition { fn debug_config(&self, cx: &App, debugger: &str) -> DebugScenario {
let request = self.mode.debug_task(cx); let request = self.mode.debug_task(cx);
DebugTaskDefinition { let label = suggested_label(&request, debugger);
adapter: debugger.to_owned(), DebugScenario {
label: suggested_label(&request, debugger), adapter: debugger.to_owned().into(),
request, label,
request: Some(request),
initialize_args: self.initialize_args.clone(), initialize_args: self.initialize_args.clone(),
tcp_connection: None, tcp_connection: None,
stop_on_entry: match self.stop_on_entry { stop_on_entry: match self.stop_on_entry {
ToggleState::Selected => Some(true), ToggleState::Selected => Some(true),
_ => None, _ => None,
}, },
build: None,
} }
} }
@ -109,36 +112,9 @@ impl NewSessionModal {
let config = self.debug_config(cx, debugger); let config = self.debug_config(cx, debugger);
let debug_panel = self.debug_panel.clone(); let debug_panel = self.debug_panel.clone();
let task_contexts = self
.workspace
.update(cx, |workspace, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})
.ok();
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let task_context = if let Some(task) = task_contexts {
task.await
.active_worktree_context
.map_or(task::TaskContext::default(), |context| context.1)
} else {
task::TaskContext::default()
};
debug_panel.update_in(cx, |debug_panel, window, cx| { debug_panel.update_in(cx, |debug_panel, window, cx| {
let template = DebugTaskTemplate { debug_panel.start_session(config, TaskContext::default(), None, window, cx)
locator: None,
definition: config.clone(),
};
if let Some(debug_config) = template
.to_zed_format()
.resolve_task("debug_task", &task_context)
.and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
{
debug_panel.start_session(debug_config.definition, window, cx)
} else {
debug_panel.start_session(config, window, cx)
}
})?; })?;
this.update(cx, |_, cx| { this.update(cx, |_, cx| {
cx.emit(DismissEvent); cx.emit(DismissEvent);
@ -156,12 +132,13 @@ impl NewSessionModal {
cx: &mut App, cx: &mut App,
) { ) {
attach.update(cx, |this, cx| { attach.update(cx, |this, cx| {
if selected_debugger != this.debug_definition.adapter { if selected_debugger != this.definition.adapter.as_ref() {
this.debug_definition.adapter = selected_debugger.into(); let adapter: SharedString = selected_debugger.to_owned().into();
this.definition.adapter = adapter.clone();
this.attach_picker.update(cx, |this, cx| { this.attach_picker.update(cx, |this, cx| {
this.picker.update(cx, |this, cx| { this.picker.update(cx, |this, cx| {
this.delegate.debug_config.adapter = selected_debugger.into(); this.delegate.definition.adapter = adapter;
this.focus(window, cx); this.focus(window, cx);
}) })
}); });
@ -224,22 +201,22 @@ impl NewSessionModal {
"debug-config-menu", "debug-config-menu",
last_profile.unwrap_or_else(|| SELECT_SCENARIO_LABEL.clone()), last_profile.unwrap_or_else(|| SELECT_SCENARIO_LABEL.clone()),
ContextMenu::build(window, cx, move |mut menu, _, cx| { ContextMenu::build(window, cx, move |mut menu, _, cx| {
let setter_for_name = |task: DebugTaskDefinition| { let setter_for_name = |task: DebugScenario| {
let weak = weak.clone(); let weak = weak.clone();
move |window: &mut Window, cx: &mut App| { move |window: &mut Window, cx: &mut App| {
weak.update(cx, |this, cx| { weak.update(cx, |this, cx| {
this.last_selected_profile_name = Some(SharedString::from(&task.label)); this.last_selected_profile_name = Some(SharedString::from(&task.label));
this.debugger = Some(task.adapter.clone().into()); this.debugger = Some(task.adapter.clone());
this.initialize_args = task.initialize_args.clone(); this.initialize_args = task.initialize_args.clone();
match &task.request { match &task.request {
DebugRequest::Launch(launch_config) => { Some(DebugRequest::Launch(launch_config)) => {
this.mode = NewSessionMode::launch( this.mode = NewSessionMode::launch(
Some(launch_config.clone()), Some(launch_config.clone()),
window, window,
cx, cx,
); );
} }
DebugRequest::Attach(_) => { Some(DebugRequest::Attach(_)) => {
let Some(workspace) = this.workspace.upgrade() else { let Some(workspace) = this.workspace.upgrade() else {
return; return;
}; };
@ -256,6 +233,7 @@ impl NewSessionModal {
Self::update_attach_picker(&attach, &debugger, window, cx); Self::update_attach_picker(&attach, &debugger, window, cx);
} }
} }
_ => log::warn!("Selected debug scenario without either attach or launch request specified"),
} }
cx.notify(); cx.notify();
}) })
@ -263,7 +241,7 @@ impl NewSessionModal {
} }
}; };
let available_adapters: Vec<DebugTaskTemplate> = workspace let available_tasks: Vec<DebugScenario> = workspace
.update(cx, |this, cx| { .update(cx, |this, cx| {
this.project() this.project()
.read(cx) .read(cx)
@ -271,19 +249,19 @@ impl NewSessionModal {
.read(cx) .read(cx)
.task_inventory() .task_inventory()
.iter() .iter()
.flat_map(|task_inventory| task_inventory.read(cx).list_debug_tasks()) .flat_map(|task_inventory| {
.cloned() task_inventory.read(cx).list_debug_scenarios(None)
.filter_map(|task| task.try_into().ok()) })
.collect() .collect()
}) })
.ok() .ok()
.unwrap_or_default(); .unwrap_or_default();
for debug_definition in available_adapters { for debug_definition in available_tasks {
menu = menu.entry( menu = menu.entry(
debug_definition.definition.label.clone(), debug_definition.label.clone(),
None, None,
setter_for_name(debug_definition.definition), setter_for_name(debug_definition),
); );
} }
menu menu
@ -332,13 +310,14 @@ impl LaunchMode {
program: self.program.read(cx).text(cx), program: self.program.read(cx).text(cx),
cwd: path.is_empty().not().then(|| PathBuf::from(path)), cwd: path.is_empty().not().then(|| PathBuf::from(path)),
args: Default::default(), args: Default::default(),
env: Default::default(),
} }
} }
} }
#[derive(Clone)] #[derive(Clone)]
struct AttachMode { struct AttachMode {
debug_definition: DebugTaskDefinition, definition: DebugTaskDefinition,
attach_picker: Entity<AttachModal>, attach_picker: Entity<AttachModal>,
} }
@ -349,22 +328,22 @@ impl AttachMode {
window: &mut Window, window: &mut Window,
cx: &mut Context<NewSessionModal>, cx: &mut Context<NewSessionModal>,
) -> Entity<Self> { ) -> Entity<Self> {
let debug_definition = DebugTaskDefinition { let definition = DebugTaskDefinition {
adapter: debugger.clone().unwrap_or_default(),
label: "Attach New Session Setup".into(), label: "Attach New Session Setup".into(),
request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }), request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
tcp_connection: None,
adapter: debugger.clone().unwrap_or_default().into(),
initialize_args: None, initialize_args: None,
tcp_connection: None,
stop_on_entry: Some(false), stop_on_entry: Some(false),
}; };
let attach_picker = cx.new(|cx| { let attach_picker = cx.new(|cx| {
let modal = AttachModal::new(workspace, debug_definition.clone(), false, window, cx); let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
window.focus(&modal.focus_handle(cx)); window.focus(&modal.focus_handle(cx));
modal modal
}); });
cx.new(|_| Self { cx.new(|_| Self {
debug_definition, definition,
attach_picker, attach_picker,
}) })
} }

View file

@ -33,7 +33,7 @@ impl DebugSessionState {
pub struct DebugSession { pub struct DebugSession {
remote_id: Option<workspace::ViewId>, remote_id: Option<workspace::ViewId>,
mode: DebugSessionState, mode: DebugSessionState,
label: OnceLock<String>, label: OnceLock<SharedString>,
dap_store: WeakEntity<DapStore>, dap_store: WeakEntity<DapStore>,
_debug_panel: WeakEntity<DebugPanel>, _debug_panel: WeakEntity<DebugPanel>,
_worktree_store: WeakEntity<WorktreeStore>, _worktree_store: WeakEntity<WorktreeStore>,
@ -110,9 +110,9 @@ impl DebugSession {
} }
} }
pub(crate) fn label(&self, cx: &App) -> String { pub(crate) fn label(&self, cx: &App) -> SharedString {
if let Some(label) = self.label.get() { if let Some(label) = self.label.get() {
return label.to_owned(); return label.clone();
} }
let session_id = match &self.mode { let session_id = match &self.mode {
@ -123,7 +123,7 @@ impl DebugSession {
.dap_store .dap_store
.read_with(cx, |store, _| store.session_by_id(session_id)) .read_with(cx, |store, _| store.session_by_id(session_id))
else { else {
return "".to_owned(); return "".into();
}; };
self.label self.label

View file

@ -1,11 +1,12 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use dap::adapters::DebugTaskDefinition;
use dap::{DebugRequest, client::DebugAdapterClient}; use dap::{DebugRequest, client::DebugAdapterClient};
use gpui::{Entity, TestAppContext, WindowHandle}; use gpui::{Entity, TestAppContext, WindowHandle};
use project::{Project, debugger::session::Session}; use project::{Project, debugger::session::Session};
use settings::SettingsStore; use settings::SettingsStore;
use task::DebugTaskDefinition; use task::TaskContext;
use terminal_view::terminal_panel::TerminalPanel; use terminal_view::terminal_panel::TerminalPanel;
use workspace::Workspace; use workspace::Workspace;
@ -104,7 +105,13 @@ pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
) -> Result<Entity<Session>> { ) -> Result<Entity<Session>> {
let _subscription = project::debugger::test::intercept_debug_sessions(cx, configure); let _subscription = project::debugger::test::intercept_debug_sessions(cx, configure);
workspace.update(cx, |workspace, window, cx| { workspace.update(cx, |workspace, window, cx| {
workspace.start_debug_session(config, window, cx) workspace.start_debug_session(
config.to_scenario(),
TaskContext::default(),
None,
window,
cx,
)
})?; })?;
cx.run_until_parked(); cx.run_until_parked();
let session = workspace.read_with(cx, |workspace, cx| { let session = workspace.read_with(cx, |workspace, cx| {
@ -128,9 +135,9 @@ pub fn start_debug_session<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
workspace, workspace,
cx, cx,
DebugTaskDefinition { DebugTaskDefinition {
adapter: "fake-adapter".to_string(), adapter: "fake-adapter".into(),
request: DebugRequest::Launch(Default::default()), request: DebugRequest::Launch(Default::default()),
label: "test".to_string(), label: "test".into(),
initialize_args: None, initialize_args: None,
tcp_connection: None, tcp_connection: None,
stop_on_entry: None, stop_on_entry: None,

View file

@ -1,11 +1,11 @@
use crate::{attach_modal::Candidate, tests::start_debug_session_with, *}; use crate::{attach_modal::Candidate, tests::start_debug_session_with, *};
use attach_modal::AttachModal; use attach_modal::AttachModal;
use dap::FakeAdapter; use dap::{FakeAdapter, adapters::DebugTaskDefinition};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use menu::Confirm; use menu::Confirm;
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use serde_json::json; use serde_json::json;
use task::{AttachRequest, DebugTaskDefinition, TcpArgumentsTemplate}; use task::{AttachRequest, TcpArgumentsTemplate};
use tests::{init_test, init_test_workspace}; use tests::{init_test, init_test_workspace};
#[gpui::test] #[gpui::test]
@ -30,11 +30,11 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
&workspace, &workspace,
cx, cx,
DebugTaskDefinition { DebugTaskDefinition {
adapter: "fake-adapter".to_string(), adapter: "fake-adapter".into(),
request: dap::DebugRequest::Attach(AttachRequest { request: dap::DebugRequest::Attach(AttachRequest {
process_id: Some(10), process_id: Some(10),
}), }),
label: "label".to_string(), label: "label".into(),
initialize_args: None, initialize_args: None,
tcp_connection: None, tcp_connection: None,
stop_on_entry: None, stop_on_entry: None,
@ -104,6 +104,7 @@ async fn test_show_attach_modal_and_select_process(
workspace_handle, workspace_handle,
DebugTaskDefinition { DebugTaskDefinition {
adapter: FakeAdapter::ADAPTER_NAME.into(), adapter: FakeAdapter::ADAPTER_NAME.into(),
request: dap::DebugRequest::Attach(AttachRequest::default()), request: dap::DebugRequest::Attach(AttachRequest::default()),
label: "attach example".into(), label: "attach example".into(),
initialize_args: None, initialize_args: None,

View file

@ -78,6 +78,10 @@ pub struct ToggleCodeActions {
#[serde(default)] #[serde(default)]
#[serde(skip)] #[serde(skip)]
pub deployed_from_indicator: Option<DisplayRow>, pub deployed_from_indicator: Option<DisplayRow>,
// Run first available task if there is only one.
#[serde(default)]
#[serde(skip)]
pub quick_launch: bool,
} }
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]

View file

@ -1,4 +1,3 @@
use feature_flags::{DebuggerFeatureFlag, FeatureFlagAppExt as _};
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
AnyElement, BackgroundExecutor, Entity, Focusable, FontWeight, ListSizingBehavior, AnyElement, BackgroundExecutor, Entity, Focusable, FontWeight, ListSizingBehavior,
@ -13,6 +12,8 @@ use ordered_float::OrderedFloat;
use project::CompletionSource; use project::CompletionSource;
use project::lsp_store::CompletionDocumentation; use project::lsp_store::CompletionDocumentation;
use project::{CodeAction, Completion, TaskSourceKind}; use project::{CodeAction, Completion, TaskSourceKind};
use task::DebugScenario;
use task::TaskContext;
use std::{ use std::{
cell::RefCell, cell::RefCell,
@ -39,6 +40,7 @@ pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.); pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.); pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
#[allow(clippy::large_enum_variant)]
pub enum CodeContextMenu { pub enum CodeContextMenu {
Completions(CompletionsMenu), Completions(CompletionsMenu),
CodeActions(CodeActionsMenu), CodeActions(CodeActionsMenu),
@ -819,28 +821,25 @@ pub struct AvailableCodeAction {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct CodeActionContents { pub(crate) struct CodeActionContents {
tasks: Option<Rc<ResolvedTasks>>, tasks: Option<Rc<ResolvedTasks>>,
actions: Option<Rc<[AvailableCodeAction]>>, actions: Option<Rc<[AvailableCodeAction]>>,
debug_scenarios: Vec<DebugScenario>,
pub(crate) context: TaskContext,
} }
impl CodeActionContents { impl CodeActionContents {
pub fn new( pub(crate) fn new(
mut tasks: Option<ResolvedTasks>, tasks: Option<ResolvedTasks>,
actions: Option<Rc<[AvailableCodeAction]>>, actions: Option<Rc<[AvailableCodeAction]>>,
cx: &App, debug_scenarios: Vec<DebugScenario>,
context: TaskContext,
) -> Self { ) -> Self {
if !cx.has_flag::<DebuggerFeatureFlag>() {
if let Some(tasks) = &mut tasks {
tasks
.templates
.retain(|(_, task)| !matches!(task.task_type(), task::TaskType::Debug(_)));
}
}
Self { Self {
tasks: tasks.map(Rc::new), tasks: tasks.map(Rc::new),
actions, actions,
debug_scenarios,
context,
} }
} }
@ -849,21 +848,13 @@ impl CodeActionContents {
} }
fn len(&self) -> usize { fn len(&self) -> usize {
match (&self.tasks, &self.actions) { let tasks_len = self.tasks.as_ref().map_or(0, |tasks| tasks.templates.len());
(Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(), let code_actions_len = self.actions.as_ref().map_or(0, |actions| actions.len());
(Some(tasks), None) => tasks.templates.len(), tasks_len + code_actions_len + self.debug_scenarios.len()
(None, Some(actions)) => actions.len(),
(None, None) => 0,
}
} }
fn is_empty(&self) -> bool { fn is_empty(&self) -> bool {
match (&self.tasks, &self.actions) { self.len() == 0
(Some(tasks), Some(actions)) => actions.is_empty() && tasks.templates.is_empty(),
(Some(tasks), None) => tasks.templates.is_empty(),
(None, Some(actions)) => actions.is_empty(),
(None, None) => true,
}
} }
fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ { fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
@ -882,43 +873,38 @@ impl CodeActionContents {
provider: available.provider.clone(), provider: available.provider.clone(),
}) })
})) }))
.chain(
self.debug_scenarios
.iter()
.cloned()
.map(CodeActionsItem::DebugScenario),
)
} }
pub fn get(&self, index: usize) -> Option<CodeActionsItem> { pub fn get(&self, mut index: usize) -> Option<CodeActionsItem> {
match (&self.tasks, &self.actions) { if let Some(tasks) = &self.tasks {
(Some(tasks), Some(actions)) => { if let Some((kind, task)) = tasks.templates.get(index) {
if index < tasks.templates.len() { return Some(CodeActionsItem::Task(kind.clone(), task.clone()));
tasks } else {
.templates index -= tasks.templates.len();
.get(index)
.cloned()
.map(|(kind, task)| CodeActionsItem::Task(kind, task))
} else {
actions.get(index - tasks.templates.len()).map(|available| {
CodeActionsItem::CodeAction {
excerpt_id: available.excerpt_id,
action: available.action.clone(),
provider: available.provider.clone(),
}
})
}
} }
(Some(tasks), None) => tasks
.templates
.get(index)
.cloned()
.map(|(kind, task)| CodeActionsItem::Task(kind, task)),
(None, Some(actions)) => {
actions
.get(index)
.map(|available| CodeActionsItem::CodeAction {
excerpt_id: available.excerpt_id,
action: available.action.clone(),
provider: available.provider.clone(),
})
}
(None, None) => None,
} }
if let Some(actions) = &self.actions {
if let Some(available) = actions.get(index) {
return Some(CodeActionsItem::CodeAction {
excerpt_id: available.excerpt_id,
action: available.action.clone(),
provider: available.provider.clone(),
});
} else {
index -= actions.len();
}
}
self.debug_scenarios
.get(index)
.cloned()
.map(CodeActionsItem::DebugScenario)
} }
} }
@ -931,6 +917,7 @@ pub enum CodeActionsItem {
action: CodeAction, action: CodeAction,
provider: Rc<dyn CodeActionProvider>, provider: Rc<dyn CodeActionProvider>,
}, },
DebugScenario(DebugScenario),
} }
impl CodeActionsItem { impl CodeActionsItem {
@ -947,16 +934,23 @@ impl CodeActionsItem {
}; };
Some(action) Some(action)
} }
fn as_debug_scenario(&self) -> Option<&DebugScenario> {
let Self::DebugScenario(scenario) = self else {
return None;
};
Some(scenario)
}
pub fn label(&self) -> String { pub fn label(&self) -> String {
match self { match self {
Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(), Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
Self::Task(_, task) => task.resolved_label.clone(), Self::Task(_, task) => task.resolved_label.clone(),
Self::DebugScenario(scenario) => scenario.label.to_string(),
} }
} }
} }
pub struct CodeActionsMenu { pub(crate) struct CodeActionsMenu {
pub actions: CodeActionContents, pub actions: CodeActionContents,
pub buffer: Entity<Buffer>, pub buffer: Entity<Buffer>,
pub selected_item: usize, pub selected_item: usize,
@ -1065,19 +1059,7 @@ impl CodeActionsMenu {
.inset(true) .inset(true)
.toggle_state(selected) .toggle_state(selected)
.when_some(action.as_code_action(), |this, action| { .when_some(action.as_code_action(), |this, action| {
this.on_click(cx.listener(move |editor, _, window, cx| { this.child(
cx.stop_propagation();
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction {
item_ix: Some(item_ix),
},
window,
cx,
) {
task.detach_and_log_err(cx)
}
}))
.child(
h_flex() h_flex()
.overflow_hidden() .overflow_hidden()
.child( .child(
@ -1090,19 +1072,7 @@ impl CodeActionsMenu {
) )
}) })
.when_some(action.as_task(), |this, task| { .when_some(action.as_task(), |this, task| {
this.on_click(cx.listener(move |editor, _, window, cx| { this.child(
cx.stop_propagation();
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction {
item_ix: Some(item_ix),
},
window,
cx,
) {
task.detach_and_log_err(cx)
}
}))
.child(
h_flex() h_flex()
.overflow_hidden() .overflow_hidden()
.child(task.resolved_label.replace("\n", "")) .child(task.resolved_label.replace("\n", ""))
@ -1110,7 +1080,29 @@ impl CodeActionsMenu {
this.text_color(colors.text_accent) this.text_color(colors.text_accent)
}), }),
) )
}), })
.when_some(action.as_debug_scenario(), |this, scenario| {
this.child(
h_flex()
.overflow_hidden()
.child(scenario.label.clone())
.when(selected, |this| {
this.text_color(colors.text_accent)
}),
)
})
.on_click(cx.listener(move |editor, _, window, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction {
item_ix: Some(item_ix),
},
window,
cx,
) {
task.detach_and_log_err(cx)
}
})),
) )
}) })
.collect() .collect()
@ -1128,6 +1120,7 @@ impl CodeActionsMenu {
CodeActionsItem::CodeAction { action, .. } => { CodeActionsItem::CodeAction { action, .. } => {
action.lsp_action.title().chars().count() action.lsp_action.title().chars().count()
} }
CodeActionsItem::DebugScenario(scenario) => scenario.label.chars().count(),
}) })
.map(|(ix, _)| ix), .map(|(ix, _)| ix),
) )

View file

@ -5089,6 +5089,7 @@ impl Editor {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let quick_launch = action.quick_launch;
let mut context_menu = self.context_menu.borrow_mut(); let mut context_menu = self.context_menu.borrow_mut();
if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() { if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() {
if code_actions.deployed_from_indicator == action.deployed_from_indicator { if code_actions.deployed_from_indicator == action.deployed_from_indicator {
@ -5162,8 +5163,6 @@ impl Editor {
Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx) Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx)
}); });
let debugger_flag = cx.has_flag::<DebuggerFeatureFlag>();
Some(cx.spawn_in(window, async move |editor, cx| { Some(cx.spawn_in(window, async move |editor, cx| {
let task_context = match task_context { let task_context = match task_context {
Some(task_context) => task_context.await, Some(task_context) => task_context.await,
@ -5171,7 +5170,7 @@ impl Editor {
}; };
let resolved_tasks = let resolved_tasks =
tasks tasks
.zip(task_context) .zip(task_context.clone())
.map(|(tasks, task_context)| ResolvedTasks { .map(|(tasks, task_context)| ResolvedTasks {
templates: tasks.resolve(&task_context).collect(), templates: tasks.resolve(&task_context).collect(),
position: snapshot.buffer_snapshot.anchor_before(Point::new( position: snapshot.buffer_snapshot.anchor_before(Point::new(
@ -5179,22 +5178,49 @@ impl Editor {
tasks.column, tasks.column,
)), )),
}); });
let spawn_straight_away = resolved_tasks.as_ref().map_or(false, |tasks| { let spawn_straight_away = quick_launch
tasks && resolved_tasks
.templates .as_ref()
.iter() .map_or(false, |tasks| tasks.templates.len() == 1)
.filter(|task| { && code_actions
if matches!(task.1.task_type(), task::TaskType::Debug(_)) { .as_ref()
debugger_flag .map_or(true, |actions| actions.is_empty());
} else { let debug_scenarios = editor.update(cx, |editor, cx| {
true if cx.has_flag::<DebuggerFeatureFlag>() {
} maybe!({
let project = editor.project.as_ref()?;
let dap_store = project.read(cx).dap_store();
let mut scenarios = vec![];
let resolved_tasks = resolved_tasks.as_ref()?;
let debug_adapter: SharedString = buffer
.read(cx)
.language()?
.context_provider()?
.debug_adapter()?
.into();
dap_store.update(cx, |this, cx| {
for (_, task) in &resolved_tasks.templates {
if let Some(scenario) = this
.debug_scenario_for_build_task(
task.resolved.clone(),
SharedString::from(
task.original_task().label.clone(),
),
debug_adapter.clone(),
cx,
)
{
scenarios.push(scenario);
}
}
});
Some(scenarios)
}) })
.count() .unwrap_or_default()
== 1 } else {
}) && code_actions vec![]
.as_ref() }
.map_or(true, |actions| actions.is_empty()); })?;
if let Ok(task) = editor.update_in(cx, |editor, window, cx| { if let Ok(task) = editor.update_in(cx, |editor, window, cx| {
*editor.context_menu.borrow_mut() = *editor.context_menu.borrow_mut() =
Some(CodeContextMenu::CodeActions(CodeActionsMenu { Some(CodeContextMenu::CodeActions(CodeActionsMenu {
@ -5202,7 +5228,8 @@ impl Editor {
actions: CodeActionContents::new( actions: CodeActionContents::new(
resolved_tasks, resolved_tasks,
code_actions, code_actions,
cx, debug_scenarios,
task_context.unwrap_or_default(),
), ),
selected_item: Default::default(), selected_item: Default::default(),
scroll_handle: UniformListScrollHandle::default(), scroll_handle: UniformListScrollHandle::default(),
@ -5262,25 +5289,17 @@ impl Editor {
match action { match action {
CodeActionsItem::Task(task_source_kind, resolved_task) => { CodeActionsItem::Task(task_source_kind, resolved_task) => {
match resolved_task.task_type() { workspace.update(cx, |workspace, cx| {
task::TaskType::Script => workspace.update(cx, |workspace, cx| { workspace.schedule_resolved_task(
workspace.schedule_resolved_task( task_source_kind,
task_source_kind, resolved_task,
resolved_task, false,
false, window,
window, cx,
cx, );
);
Some(Task::ready(Ok(()))) Some(Task::ready(Ok(())))
}), })
task::TaskType::Debug(_) => {
workspace.update(cx, |workspace, cx| {
workspace.schedule_debug_task(resolved_task, window, cx);
});
Some(Task::ready(Ok(())))
}
}
} }
CodeActionsItem::CodeAction { CodeActionsItem::CodeAction {
excerpt_id, excerpt_id,
@ -5302,6 +5321,14 @@ impl Editor {
.await .await
})) }))
} }
CodeActionsItem::DebugScenario(scenario) => {
let context = actions_menu.actions.context.clone();
workspace.update(cx, |workspace, cx| {
workspace.start_debug_session(scenario, context, Some(buffer), window, cx);
});
Some(Task::ready(Ok(())))
}
} }
} }
@ -6660,6 +6687,7 @@ impl Editor {
"Toggle Code Actions", "Toggle Code Actions",
&ToggleCodeActions { &ToggleCodeActions {
deployed_from_indicator: None, deployed_from_indicator: None,
quick_launch: false,
}, },
&focus_handle, &focus_handle,
window, window,
@ -6668,11 +6696,13 @@ impl Editor {
} }
}) })
}) })
.on_click(cx.listener(move |editor, _e, window, cx| { .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| {
let quick_launch = e.down.button == MouseButton::Left;
window.focus(&editor.focus_handle(cx)); window.focus(&editor.focus_handle(cx));
editor.toggle_code_actions( editor.toggle_code_actions(
&ToggleCodeActions { &ToggleCodeActions {
deployed_from_indicator: Some(row), deployed_from_indicator: Some(row),
quick_launch,
}, },
window, window,
cx, cx,
@ -7050,7 +7080,7 @@ impl Editor {
let context = task_context.await?; let context = task_context.await?;
let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?; let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?;
let resolved = resolved_task.resolved.as_mut()?; let resolved = &mut resolved_task.resolved;
resolved.reveal = reveal_strategy; resolved.reveal = reveal_strategy;
workspace workspace
@ -7140,11 +7170,13 @@ impl Editor {
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.icon_color(color) .icon_color(color)
.toggle_state(is_active) .toggle_state(is_active)
.on_click(cx.listener(move |editor, _e, window, cx| { .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| {
let quick_launch = e.down.button == MouseButton::Left;
window.focus(&editor.focus_handle(cx)); window.focus(&editor.focus_handle(cx));
editor.toggle_code_actions( editor.toggle_code_actions(
&ToggleCodeActions { &ToggleCodeActions {
deployed_from_indicator: Some(row), deployed_from_indicator: Some(row),
quick_launch,
}, },
window, window,
cx, cx,

View file

@ -211,6 +211,7 @@ pub fn deploy_context_menu(
"Show Code Actions", "Show Code Actions",
Box::new(ToggleCodeActions { Box::new(ToggleCodeActions {
deployed_from_indicator: None, deployed_from_indicator: None,
quick_launch: false,
}), }),
) )
.separator() .separator()

View file

@ -47,4 +47,7 @@ pub trait ContextProvider: Send + Sync {
fn lsp_task_source(&self) -> Option<LanguageServerName> { fn lsp_task_source(&self) -> Option<LanguageServerName> {
None None
} }
/// Default debug adapter for a given language.
fn debug_adapter(&self) -> Option<String>;
} }

View file

@ -630,6 +630,10 @@ impl ContextProvider for GoContextProvider {
}, },
])) ]))
} }
fn debug_adapter(&self) -> Option<String> {
Some("Delve".into())
}
} }
fn extract_subtest_name(input: &str) -> Option<String> { fn extract_subtest_name(input: &str) -> Option<String> {

View file

@ -503,6 +503,10 @@ impl ContextProvider for PythonContextProvider {
Some(TaskTemplates(tasks)) Some(TaskTemplates(tasks))
} }
fn debug_adapter(&self) -> Option<String> {
Some("Debugpy".into())
}
} }
fn selected_test_runner(location: Option<&Arc<dyn language::File>>, cx: &App) -> TestRunner { fn selected_test_runner(location: Option<&Arc<dyn language::File>>, cx: &App) -> TestRunner {

View file

@ -20,7 +20,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, LazyLock}, sync::{Arc, LazyLock},
}; };
use task::{TaskTemplate, TaskTemplates, TaskType, TaskVariables, VariableName}; use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use util::merge_json_value_into; use util::merge_json_value_into;
use util::{ResultExt, fs::remove_matching, maybe}; use util::{ResultExt, fs::remove_matching, maybe};
@ -629,7 +629,7 @@ impl ContextProvider for RustContextProvider {
} else { } else {
vec!["run".into()] vec!["run".into()]
}; };
let debug_task_args = if let Some(package_to_run) = package_to_run { let build_task_args = if let Some(package_to_run) = package_to_run {
vec!["build".into(), "-p".into(), package_to_run] vec!["build".into(), "-p".into(), package_to_run]
} else { } else {
vec!["build".into()] vec!["build".into()]
@ -675,32 +675,6 @@ impl ContextProvider for RustContextProvider {
cwd: Some("$ZED_DIRNAME".to_owned()), cwd: Some("$ZED_DIRNAME".to_owned()),
..TaskTemplate::default() ..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: "CodeLLDB".to_owned(),
request: task::DebugArgsRequest::Launch,
locator: Some("cargo".into()),
tcp_connection: None,
initialize_args: None,
stop_on_entry: 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 { TaskTemplate {
label: format!( label: format!(
"Doc test '{}' (package: {})", "Doc test '{}' (package: {})",
@ -780,31 +754,41 @@ impl ContextProvider for RustContextProvider {
cwd: Some("$ZED_DIRNAME".to_owned()), cwd: Some("$ZED_DIRNAME".to_owned()),
..TaskTemplate::default() ..TaskTemplate::default()
}, },
TaskTemplate {
label: "Clean".into(),
command: "cargo".into(),
args: vec!["clean".into()],
cwd: Some("$ZED_DIRNAME".to_owned()),
..TaskTemplate::default()
},
TaskTemplate { TaskTemplate {
label: format!( label: format!(
"Debug {} {} (package: {})", "Build {} {} (package: {})",
RUST_BIN_KIND_TASK_VARIABLE.template_value(), RUST_BIN_KIND_TASK_VARIABLE.template_value(),
RUST_BIN_NAME_TASK_VARIABLE.template_value(), RUST_BIN_NAME_TASK_VARIABLE.template_value(),
RUST_PACKAGE_TASK_VARIABLE.template_value(), RUST_PACKAGE_TASK_VARIABLE.template_value(),
), ),
cwd: Some("$ZED_DIRNAME".to_owned()), cwd: Some("$ZED_DIRNAME".to_owned()),
command: "cargo".into(), command: "cargo".into(),
task_type: TaskType::Debug(task::DebugArgs { args: build_task_args,
request: task::DebugArgsRequest::Launch,
adapter: "CodeLLDB".to_owned(),
initialize_args: None,
locator: Some("cargo".into()),
tcp_connection: None,
stop_on_entry: None,
}),
args: debug_task_args,
tags: vec!["rust-main".to_owned()], tags: vec!["rust-main".to_owned()],
..TaskTemplate::default() ..TaskTemplate::default()
}, },
TaskTemplate { TaskTemplate {
label: "Clean".into(), label: format!(
"Build Test '{}' (package: {})",
RUST_TEST_NAME_TASK_VARIABLE.template_value(),
RUST_PACKAGE_TASK_VARIABLE.template_value(),
),
command: "cargo".into(), command: "cargo".into(),
args: vec!["clean".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()), cwd: Some("$ZED_DIRNAME".to_owned()),
..TaskTemplate::default() ..TaskTemplate::default()
}, },
@ -832,6 +816,10 @@ impl ContextProvider for RustContextProvider {
fn lsp_task_source(&self) -> Option<LanguageServerName> { fn lsp_task_source(&self) -> Option<LanguageServerName> {
Some(SERVER_NAME) Some(SERVER_NAME)
} }
fn debug_adapter(&self) -> Option<String> {
Some("CodeLLDB".to_owned())
}
} }
/// Part of the data structure of Cargo metadata /// Part of the data structure of Cargo metadata

View file

@ -1,6 +1,6 @@
use super::{ use super::{
breakpoint_store::BreakpointStore, breakpoint_store::BreakpointStore,
locators::DapLocator, locators,
session::{self, Session, SessionStateEvent}, session::{self, Session, SessionStateEvent},
}; };
use crate::{ use crate::{
@ -13,10 +13,12 @@ use anyhow::{Result, anyhow};
use async_trait::async_trait; use async_trait::async_trait;
use collections::HashMap; use collections::HashMap;
use dap::{ use dap::{
Capabilities, CompletionItem, CompletionsArguments, DapRegistry, EvaluateArguments, Capabilities, CompletionItem, CompletionsArguments, DapRegistry, DebugRequest,
EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments, Source, EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments,
StackFrameId, StartDebuggingRequestArguments, Source, StackFrameId, StartDebuggingRequestArguments,
adapters::{DapStatus, DebugAdapterBinary, DebugAdapterName, TcpArguments}, adapters::{
DapStatus, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments,
},
client::SessionId, client::SessionId,
messages::Message, messages::Message,
requests::{Completions, Evaluate, Request as _, RunInTerminal, StartDebugging}, requests::{Completions, Evaluate, Request as _, RunInTerminal, StartDebugging},
@ -49,9 +51,9 @@ use std::{
ffi::OsStr, ffi::OsStr,
net::Ipv4Addr, net::Ipv4Addr,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::{Arc, Once},
}; };
use task::{DebugTaskDefinition, DebugTaskTemplate}; use task::{DebugScenario, SpawnInTerminal};
use util::ResultExt as _; use util::ResultExt as _;
use worktree::Worktree; use worktree::Worktree;
@ -95,7 +97,6 @@ pub struct LocalDapStore {
environment: Entity<ProjectEnvironment>, environment: Entity<ProjectEnvironment>,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
toolchain_store: Arc<dyn LanguageToolchainStore>, toolchain_store: Arc<dyn LanguageToolchainStore>,
locators: HashMap<String, Arc<dyn DapLocator>>,
} }
pub struct SshDapStore { pub struct SshDapStore {
@ -118,9 +119,14 @@ pub struct DapStore {
impl EventEmitter<DapStoreEvent> for DapStore {} impl EventEmitter<DapStoreEvent> for DapStore {}
impl DapStore { impl DapStore {
pub fn init(client: &AnyProtoClient) { pub fn init(client: &AnyProtoClient, cx: &mut App) {
static ADD_LOCATORS: Once = Once::new();
client.add_entity_request_handler(Self::handle_run_debug_locator); client.add_entity_request_handler(Self::handle_run_debug_locator);
client.add_entity_request_handler(Self::handle_get_debug_adapter_binary); client.add_entity_request_handler(Self::handle_get_debug_adapter_binary);
ADD_LOCATORS.call_once(|| {
DapRegistry::global(cx)
.add_locator("cargo".into(), Arc::new(locators::cargo::CargoLocator {}))
});
} }
#[expect(clippy::too_many_arguments)] #[expect(clippy::too_many_arguments)]
@ -135,11 +141,6 @@ impl DapStore {
breakpoint_store: Entity<BreakpointStore>, breakpoint_store: Entity<BreakpointStore>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let locators = HashMap::from_iter([(
"cargo".to_string(),
Arc::new(super::locators::cargo::CargoLocator {}) as _,
)]);
let mode = DapStoreMode::Local(LocalDapStore { let mode = DapStoreMode::Local(LocalDapStore {
fs, fs,
environment, environment,
@ -147,7 +148,6 @@ impl DapStore {
node_runtime, node_runtime,
toolchain_store, toolchain_store,
language_registry, language_registry,
locators,
}); });
Self::new(mode, breakpoint_store, worktree_store, cx) Self::new(mode, breakpoint_store, worktree_store, cx)
@ -273,7 +273,7 @@ impl DapStore {
DapStoreMode::Ssh(ssh) => { DapStoreMode::Ssh(ssh) => {
let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary { let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary {
project_id: ssh.upstream_project_id, project_id: ssh.upstream_project_id,
task: Some(definition.to_proto()), definition: Some(definition.to_proto()),
}); });
let ssh_client = ssh.ssh_client.clone(); let ssh_client = ssh.ssh_client.clone();
@ -326,34 +326,100 @@ impl DapStore {
} }
} }
pub fn debug_scenario_for_build_task(
&self,
mut build: SpawnInTerminal,
unresoved_label: SharedString,
adapter: SharedString,
cx: &mut App,
) -> Option<DebugScenario> {
build.args = build
.args
.into_iter()
.map(|arg| {
if arg.starts_with("$") {
arg.strip_prefix("$")
.and_then(|arg| build.env.get(arg).map(ToOwned::to_owned))
.unwrap_or_else(|| arg)
} else {
arg
}
})
.collect();
DapRegistry::global(cx)
.locators()
.values()
.find(|locator| locator.accepts(&build))
.map(|_| DebugScenario {
adapter,
label: format!("Debug `{}`", build.label).into(),
build: Some(unresoved_label),
request: None,
initialize_args: None,
tcp_connection: None,
stop_on_entry: None,
})
}
pub fn run_debug_locator( pub fn run_debug_locator(
&mut self, &mut self,
template: DebugTaskTemplate, mut build_command: SpawnInTerminal,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<DebugTaskDefinition>> { ) -> Task<Result<DebugRequest>> {
let Some(locator_name) = template.locator else {
return Task::ready(Ok(template.definition));
};
match &self.mode { match &self.mode {
DapStoreMode::Local(local) => { DapStoreMode::Local(_) => {
if let Some(locator) = local.locators.get(&locator_name).cloned() { // Pre-resolve args with existing environment.
cx.background_spawn( build_command.args = build_command
async move { locator.run_locator(template.definition).await }, .args
) .into_iter()
.map(|arg| {
if arg.starts_with("$") {
arg.strip_prefix("$")
.and_then(|arg| build_command.env.get(arg).map(ToOwned::to_owned))
.unwrap_or_else(|| arg)
} else {
arg
}
})
.collect();
let locators = DapRegistry::global(cx)
.locators()
.values()
.filter(|locator| locator.accepts(&build_command))
.cloned()
.collect::<Vec<_>>();
if !locators.is_empty() {
cx.background_spawn(async move {
for locator in locators {
let result = locator
.run(build_command.clone())
.await
.log_with_level(log::Level::Error);
if let Some(result) = result {
return Ok(result);
}
}
Err(anyhow!(
"None of the locators for task `{}` completed successfully",
build_command.label
))
})
} else { } else {
Task::ready(Err(anyhow!("Couldn't find locator {}", locator_name))) Task::ready(Err(anyhow!(
"Couldn't find any locator for task `{}`. Specify the `attach` or `launch` arguments in your debug scenario definition",
build_command.label
)))
} }
} }
DapStoreMode::Ssh(ssh) => { DapStoreMode::Ssh(ssh) => {
let request = ssh.upstream_client.request(proto::RunDebugLocator { let request = ssh.upstream_client.request(proto::RunDebugLocators {
project_id: ssh.upstream_project_id, project_id: ssh.upstream_project_id,
locator: locator_name, build_command: Some(build_command.to_proto()),
task: Some(template.definition.to_proto()),
}); });
cx.background_spawn(async move { cx.background_spawn(async move {
let response = request.await?; let response = request.await?;
DebugTaskDefinition::from_proto(response) DebugRequest::from_proto(response)
}) })
} }
DapStoreMode::Collab => { DapStoreMode::Collab => {
@ -943,22 +1009,19 @@ impl DapStore {
async fn handle_run_debug_locator( async fn handle_run_debug_locator(
this: Entity<Self>, this: Entity<Self>,
envelope: TypedEnvelope<proto::RunDebugLocator>, envelope: TypedEnvelope<proto::RunDebugLocators>,
mut cx: AsyncApp, mut cx: AsyncApp,
) -> Result<proto::DebugTaskDefinition> { ) -> Result<proto::DebugRequest> {
let template = DebugTaskTemplate { let task = envelope
locator: Some(envelope.payload.locator), .payload
definition: DebugTaskDefinition::from_proto( .build_command
envelope .ok_or_else(|| anyhow!("missing definition"))?;
.payload let build_task = SpawnInTerminal::from_proto(task);
.task let request = this
.ok_or_else(|| anyhow!("missing definition"))?, .update(&mut cx, |this, cx| this.run_debug_locator(build_task, cx))?
)?,
};
let definition = this
.update(&mut cx, |this, cx| this.run_debug_locator(template, cx))?
.await?; .await?;
Ok(definition.to_proto())
Ok(request.to_proto())
} }
async fn handle_get_debug_adapter_binary( async fn handle_get_debug_adapter_binary(
@ -969,7 +1032,7 @@ impl DapStore {
let definition = DebugTaskDefinition::from_proto( let definition = DebugTaskDefinition::from_proto(
envelope envelope
.payload .payload
.task .definition
.ok_or_else(|| anyhow!("missing definition"))?, .ok_or_else(|| anyhow!("missing definition"))?,
)?; )?;
let binary = this let binary = this

View file

@ -1,34 +0,0 @@
use anyhow::{Result, anyhow};
use cargo::CargoLocator;
use collections::HashMap;
use gpui::SharedString;
use locators::DapLocator;
use task::{DebugTaskDefinition, DebugTaskTemplate};
mod cargo;
pub mod locators;
pub(super) struct LocatorStore {
locators: HashMap<SharedString, Box<dyn DapLocator>>,
}
impl LocatorStore {
pub(super) fn new() -> Self {
Self { locators }
}
pub(super) async fn resolve_debug_config(
&self,
template: DebugTaskTemplate,
) -> Result<DebugTaskDefinition> {
let Some(locator_name) = &template.locator else {
return Ok(template.definition);
};
if let Some(locator) = self.locators.get(locator_name as &str) {
locator.run_locator(template.definition).await
} else {
Err(anyhow!("Couldn't find locator {}", locator_name))
}
}
}

View file

@ -1,10 +1 @@
use anyhow::Result;
use async_trait::async_trait;
use task::DebugTaskDefinition;
pub(crate) mod cargo; pub(crate) mod cargo;
#[async_trait]
pub(super) trait DapLocator: Send + Sync {
async fn run_locator(&self, debug_config: DebugTaskDefinition) -> Result<DebugTaskDefinition>;
}

View file

@ -1,12 +1,12 @@
use super::DapLocator;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use async_trait::async_trait; use async_trait::async_trait;
use dap::{DapLocator, DebugRequest};
use serde_json::Value; use serde_json::Value;
use smol::{ use smol::{
io::AsyncReadExt, io::AsyncReadExt,
process::{Command, Stdio}, process::{Command, Stdio},
}; };
use task::DebugTaskDefinition; use task::SpawnInTerminal;
pub(crate) struct CargoLocator; pub(crate) struct CargoLocator;
@ -37,26 +37,31 @@ async fn find_best_executable(executables: &[String], test_name: &str) -> Option
} }
#[async_trait] #[async_trait]
impl DapLocator for CargoLocator { impl DapLocator for CargoLocator {
async fn run_locator( fn accepts(&self, build_config: &SpawnInTerminal) -> bool {
&self, if build_config.command != "cargo" {
mut debug_config: DebugTaskDefinition, return false;
) -> Result<DebugTaskDefinition> { }
let Some(launch_config) = (match &mut debug_config.request { let Some(command) = build_config.args.first().map(|s| s.as_str()) else {
task::DebugRequest::Launch(launch_config) => Some(launch_config), return false;
_ => None,
}) else {
return Err(anyhow!("Couldn't get launch config in locator"));
}; };
if matches!(command, "check" | "run") {
return false;
}
!matches!(command, "test" | "bench")
|| build_config.args.iter().any(|arg| arg == "--no-run")
}
let Some(cwd) = launch_config.cwd.clone() else { async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
let Some(cwd) = build_config.cwd.clone() else {
return Err(anyhow!( return Err(anyhow!(
"Couldn't get cwd from debug config which is needed for locators" "Couldn't get cwd from debug config which is needed for locators"
)); ));
}; };
let mut child = Command::new("cargo") let mut child = Command::new("cargo")
.args(&launch_config.args) .args(&build_config.args)
.arg("--message-format=json") .arg("--message-format=json")
.envs(build_config.env.iter().map(|(k, v)| (k.clone(), v.clone())))
.current_dir(cwd) .current_dir(cwd)
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.spawn()?; .spawn()?;
@ -85,19 +90,16 @@ impl DapLocator for CargoLocator {
return Err(anyhow!("Couldn't get executable in cargo locator")); return Err(anyhow!("Couldn't get executable in cargo locator"));
}; };
let is_test = launch_config let is_test = build_config.args.first().map_or(false, |arg| arg == "test");
.args
.first()
.map_or(false, |arg| arg == "test");
let mut test_name = None; let mut test_name = None;
if is_test { if is_test {
if let Some(package_index) = launch_config if let Some(package_index) = build_config
.args .args
.iter() .iter()
.position(|arg| arg == "-p" || arg == "--package") .position(|arg| arg == "-p" || arg == "--package")
{ {
test_name = launch_config test_name = build_config
.args .args
.get(package_index + 2) .get(package_index + 2)
.filter(|name| !name.starts_with("--")) .filter(|name| !name.starts_with("--"))
@ -116,12 +118,17 @@ impl DapLocator for CargoLocator {
return Err(anyhow!("Couldn't get executable in cargo locator")); return Err(anyhow!("Couldn't get executable in cargo locator"));
}; };
launch_config.program = executable; let args = test_name.into_iter().collect();
launch_config.args.clear(); Ok(DebugRequest::Launch(task::LaunchRequest {
if let Some(test_name) = test_name { program: executable,
launch_config.args.push(test_name); cwd: build_config.cwd.clone(),
} args,
Ok(debug_config) env: build_config
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
}))
} }
} }

View file

@ -12,7 +12,7 @@ use super::dap_command::{
use super::dap_store::DapStore; use super::dap_store::DapStore;
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use collections::{HashMap, HashSet, IndexMap, IndexSet}; use collections::{HashMap, HashSet, IndexMap, IndexSet};
use dap::adapters::DebugAdapterBinary; use dap::adapters::{DebugAdapterBinary, DebugTaskDefinition};
use dap::messages::Response; use dap::messages::Response;
use dap::{ use dap::{
Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, StackFrameId, Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, StackFrameId,
@ -42,7 +42,6 @@ use std::{
path::Path, path::Path,
sync::Arc, sync::Arc,
}; };
use task::DebugTaskDefinition;
use text::{PointUtf16, ToPointUtf16}; use text::{PointUtf16, ToPointUtf16};
use util::{ResultExt, merge_json_value_into}; use util::{ResultExt, merge_json_value_into};
use worktree::Worktree; use worktree::Worktree;
@ -125,7 +124,6 @@ enum Mode {
pub struct LocalMode { pub struct LocalMode {
client: Arc<DebugAdapterClient>, client: Arc<DebugAdapterClient>,
binary: DebugAdapterBinary, binary: DebugAdapterBinary,
root_binary: Option<Arc<DebugAdapterBinary>>,
pub(crate) breakpoint_store: Entity<BreakpointStore>, pub(crate) breakpoint_store: Entity<BreakpointStore>,
tmp_breakpoint: Option<SourceBreakpoint>, tmp_breakpoint: Option<SourceBreakpoint>,
worktree: WeakEntity<Worktree>, worktree: WeakEntity<Worktree>,
@ -160,12 +158,6 @@ impl LocalMode {
messages_tx.unbounded_send(message).ok(); messages_tx.unbounded_send(message).ok();
}); });
let root_binary = if let Some(parent_session) = parent_session.as_ref() {
Some(parent_session.read_with(&cx, |session, _| session.root_binary().clone())?)
} else {
None
};
let client = Arc::new( let client = Arc::new(
if let Some(client) = parent_session if let Some(client) = parent_session
.and_then(|session| cx.update(|cx| session.read(cx).adapter_client()).ok()) .and_then(|session| cx.update(|cx| session.read(cx).adapter_client()).ok())
@ -186,7 +178,6 @@ impl LocalMode {
breakpoint_store, breakpoint_store,
worktree, worktree,
tmp_breakpoint: None, tmp_breakpoint: None,
root_binary,
binary, binary,
}) })
} }
@ -834,19 +825,6 @@ impl Session {
&self.capabilities &self.capabilities
} }
pub(crate) fn root_binary(&self) -> Arc<DebugAdapterBinary> {
match &self.mode {
Mode::Building => {
// todo(debugger): Implement root_binary for building mode
unimplemented!()
}
Mode::Running(running) => running
.root_binary
.clone()
.unwrap_or_else(|| Arc::new(running.binary.clone())),
}
}
pub fn binary(&self) -> &DebugAdapterBinary { pub fn binary(&self) -> &DebugAdapterBinary {
let Mode::Running(local_mode) = &self.mode else { let Mode::Running(local_mode) = &self.mode else {
panic!("Session is not local"); panic!("Session is not local");
@ -855,10 +833,10 @@ impl Session {
} }
pub fn adapter_name(&self) -> SharedString { pub fn adapter_name(&self) -> SharedString {
self.definition.adapter.clone().into() self.definition.adapter.clone()
} }
pub fn label(&self) -> String { pub fn label(&self) -> SharedString {
self.definition.label.clone() self.definition.label.clone()
} }
@ -889,7 +867,7 @@ impl Session {
} }
pub(super) fn request_initialize(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> { pub(super) fn request_initialize(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
let adapter_id = self.definition.adapter.clone(); let adapter_id = String::from(self.definition.adapter.clone());
let request = Initialize { adapter_id }; let request = Initialize { adapter_id };
match &self.mode { match &self.mode {
Mode::Running(local_mode) => { Mode::Running(local_mode) => {

View file

@ -826,7 +826,7 @@ impl Project {
SettingsObserver::init(&client); SettingsObserver::init(&client);
TaskStore::init(Some(&client)); TaskStore::init(Some(&client));
ToolchainStore::init(&client); ToolchainStore::init(&client);
DapStore::init(&client); DapStore::init(&client, cx);
BreakpointStore::init(&client); BreakpointStore::init(&client);
} }
@ -1159,7 +1159,7 @@ impl Project {
SettingsObserver::init(&ssh_proto); SettingsObserver::init(&ssh_proto);
TaskStore::init(Some(&ssh_proto)); TaskStore::init(Some(&ssh_proto));
ToolchainStore::init(&ssh_proto); ToolchainStore::init(&ssh_proto);
DapStore::init(&ssh_proto); DapStore::init(&ssh_proto, cx);
GitStore::init(&ssh_proto); GitStore::init(&ssh_proto);
this this

View file

@ -8,7 +8,7 @@ use lsp::LanguageServerName;
use paths::{ use paths::{
EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path, EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path,
local_tasks_file_relative_path, local_vscode_launch_file_relative_path, local_tasks_file_relative_path, local_vscode_launch_file_relative_path,
local_vscode_tasks_file_relative_path, local_vscode_tasks_file_relative_path, task_file_name,
}; };
use rpc::{ use rpc::{
AnyProtoClient, TypedEnvelope, AnyProtoClient, TypedEnvelope,
@ -18,7 +18,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{ use settings::{
InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
SettingsStore, TaskKind, parse_json_with_comments, watch_config_file, SettingsStore, parse_json_with_comments, watch_config_file,
}; };
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -377,7 +377,7 @@ pub struct SettingsObserver {
worktree_store: Entity<WorktreeStore>, worktree_store: Entity<WorktreeStore>,
project_id: u64, project_id: u64,
task_store: Entity<TaskStore>, task_store: Entity<TaskStore>,
_global_task_config_watchers: (Task<()>, Task<()>), _global_task_config_watcher: Task<()>,
} }
/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees /// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
@ -405,19 +405,10 @@ impl SettingsObserver {
mode: SettingsObserverMode::Local(fs.clone()), mode: SettingsObserverMode::Local(fs.clone()),
downstream_client: None, downstream_client: None,
project_id: 0, project_id: 0,
_global_task_config_watchers: ( _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
Self::subscribe_to_global_task_file_changes( fs.clone(),
fs.clone(), paths::tasks_file().clone(),
TaskKind::Script, cx,
paths::tasks_file().clone(),
cx,
),
Self::subscribe_to_global_task_file_changes(
fs,
TaskKind::Debug,
paths::debug_tasks_file().clone(),
cx,
),
), ),
} }
} }
@ -434,19 +425,10 @@ impl SettingsObserver {
mode: SettingsObserverMode::Remote, mode: SettingsObserverMode::Remote,
downstream_client: None, downstream_client: None,
project_id: 0, project_id: 0,
_global_task_config_watchers: ( _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
Self::subscribe_to_global_task_file_changes( fs.clone(),
fs.clone(), paths::tasks_file().clone(),
TaskKind::Script, cx,
paths::tasks_file().clone(),
cx,
),
Self::subscribe_to_global_task_file_changes(
fs.clone(),
TaskKind::Debug,
paths::debug_tasks_file().clone(),
cx,
),
), ),
} }
} }
@ -575,7 +557,7 @@ impl SettingsObserver {
) )
.unwrap(), .unwrap(),
); );
(settings_dir, LocalSettingsKind::Tasks(TaskKind::Script)) (settings_dir, LocalSettingsKind::Tasks)
} else if path.ends_with(local_vscode_tasks_file_relative_path()) { } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
let settings_dir = Arc::<Path>::from( let settings_dir = Arc::<Path>::from(
path.ancestors() path.ancestors()
@ -587,7 +569,7 @@ impl SettingsObserver {
) )
.unwrap(), .unwrap(),
); );
(settings_dir, LocalSettingsKind::Tasks(TaskKind::Script)) (settings_dir, LocalSettingsKind::Tasks)
} else if path.ends_with(local_debug_file_relative_path()) { } else if path.ends_with(local_debug_file_relative_path()) {
let settings_dir = Arc::<Path>::from( let settings_dir = Arc::<Path>::from(
path.ancestors() path.ancestors()
@ -599,7 +581,7 @@ impl SettingsObserver {
) )
.unwrap(), .unwrap(),
); );
(settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug)) (settings_dir, LocalSettingsKind::Debug)
} else if path.ends_with(local_vscode_launch_file_relative_path()) { } else if path.ends_with(local_vscode_launch_file_relative_path()) {
let settings_dir = Arc::<Path>::from( let settings_dir = Arc::<Path>::from(
path.ancestors() path.ancestors()
@ -611,7 +593,7 @@ impl SettingsObserver {
) )
.unwrap(), .unwrap(),
); );
(settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug)) (settings_dir, LocalSettingsKind::Debug)
} else if path.ends_with(EDITORCONFIG_NAME) { } else if path.ends_with(EDITORCONFIG_NAME) {
let Some(settings_dir) = path.parent().map(Arc::from) else { let Some(settings_dir) = path.parent().map(Arc::from) else {
continue; continue;
@ -747,7 +729,7 @@ impl SettingsObserver {
} }
} }
}), }),
LocalSettingsKind::Tasks(task_kind) => { LocalSettingsKind::Tasks => {
let result = task_store.update(cx, |task_store, cx| { let result = task_store.update(cx, |task_store, cx| {
task_store.update_user_tasks( task_store.update_user_tasks(
TaskSettingsLocation::Worktree(SettingsLocation { TaskSettingsLocation::Worktree(SettingsLocation {
@ -755,7 +737,6 @@ impl SettingsObserver {
path: directory.as_ref(), path: directory.as_ref(),
}), }),
file_content.as_deref(), file_content.as_deref(),
task_kind,
cx, cx,
) )
}); });
@ -772,7 +753,38 @@ impl SettingsObserver {
} }
Ok(()) => { Ok(()) => {
cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok( cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
task_kind.config_in_dir(&directory) directory.join(task_file_name())
)));
}
}
}
LocalSettingsKind::Debug => {
let result = task_store.update(cx, |task_store, cx| {
task_store.update_user_debug_scenarios(
TaskSettingsLocation::Worktree(SettingsLocation {
worktree_id,
path: directory.as_ref(),
}),
file_content.as_deref(),
cx,
)
});
match result {
Err(InvalidSettingsError::Debug { path, message }) => {
log::error!(
"Failed to set local debug scenarios in {path:?}: {message:?}"
);
cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
InvalidSettingsError::Debug { path, message },
)));
}
Err(e) => {
log::error!("Failed to set local tasks: {e}");
}
Ok(()) => {
cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
directory.join(task_file_name())
))); )));
} }
} }
@ -795,7 +807,6 @@ impl SettingsObserver {
fn subscribe_to_global_task_file_changes( fn subscribe_to_global_task_file_changes(
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
task_kind: TaskKind,
file_path: PathBuf, file_path: PathBuf,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<()> { ) -> Task<()> {
@ -815,7 +826,6 @@ impl SettingsObserver {
.update_user_tasks( .update_user_tasks(
TaskSettingsLocation::Global(&file_path), TaskSettingsLocation::Global(&file_path),
Some(&user_tasks_content), Some(&user_tasks_content),
task_kind,
cx, cx,
) )
.log_err(); .log_err();
@ -828,7 +838,6 @@ impl SettingsObserver {
task_store.update_user_tasks( task_store.update_user_tasks(
TaskSettingsLocation::Global(&file_path), TaskSettingsLocation::Global(&file_path),
Some(&user_tasks_content), Some(&user_tasks_content),
task_kind,
cx, cx,
) )
}) else { }) else {
@ -856,15 +865,17 @@ impl SettingsObserver {
pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind { pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
match kind { match kind {
proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings, proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks(TaskKind::Script), proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig, proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
} }
} }
pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind { pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
match kind { match kind {
LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings, LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
LocalSettingsKind::Tasks(_) => proto::LocalSettingsKind::Tasks, LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig, LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
} }
} }

View file

@ -292,7 +292,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
}) })
.into_iter() .into_iter()
.map(|(source_kind, task)| { .map(|(source_kind, task)| {
let resolved = task.resolved.unwrap(); let resolved = task.resolved;
( (
source_kind, source_kind,
task.resolved_label, task.resolved_label,
@ -359,7 +359,6 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
}]) }])
.to_string(), .to_string(),
), ),
settings::TaskKind::Script,
) )
.unwrap(); .unwrap();
}); });
@ -370,7 +369,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
.update(|cx| get_all_tasks(&project, &task_contexts, cx)) .update(|cx| get_all_tasks(&project, &task_contexts, cx))
.into_iter() .into_iter()
.map(|(source_kind, task)| { .map(|(source_kind, task)| {
let resolved = task.resolved.unwrap(); let resolved = task.resolved;
( (
source_kind, source_kind,
task.resolved_label, task.resolved_label,
@ -495,7 +494,7 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) {
active_worktree_tasks active_worktree_tasks
.into_iter() .into_iter()
.map(|(source_kind, task)| { .map(|(source_kind, task)| {
let resolved = task.resolved.unwrap(); let resolved = task.resolved;
(source_kind, resolved.command) (source_kind, resolved.command)
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),

View file

@ -13,14 +13,15 @@ use collections::{HashMap, HashSet, VecDeque};
use gpui::{App, AppContext as _, Entity, SharedString, Task}; use gpui::{App, AppContext as _, Entity, SharedString, Task};
use itertools::Itertools; use itertools::Itertools;
use language::{ use language::{
ContextProvider, File, Language, LanguageToolchainStore, Location, Buffer, ContextProvider, File, Language, LanguageToolchainStore, Location,
language_settings::language_settings, language_settings::language_settings,
}; };
use lsp::{LanguageServerId, LanguageServerName}; use lsp::{LanguageServerId, LanguageServerName};
use settings::{InvalidSettingsError, TaskKind, parse_json_with_comments}; use paths::{debug_task_file_name, task_file_name};
use settings::{InvalidSettingsError, parse_json_with_comments};
use task::{ use task::{
DebugTaskTemplate, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, DebugScenario, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, TaskVariables,
TaskVariables, VariableName, VariableName,
}; };
use text::{BufferId, Point, ToPoint}; use text::{BufferId, Point, ToPoint};
use util::{NumericPrefixWithSuffix, ResultExt as _, paths::PathExt as _, post_inc}; use util::{NumericPrefixWithSuffix, ResultExt as _, paths::PathExt as _, post_inc};
@ -32,13 +33,84 @@ use crate::{task_store::TaskSettingsLocation, worktree_store::WorktreeStore};
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Inventory { pub struct Inventory {
last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>, last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>,
templates_from_settings: ParsedTemplates, templates_from_settings: InventoryFor<TaskTemplate>,
scenarios_from_settings: InventoryFor<DebugScenario>,
} }
#[derive(Debug, Default)] // Helper trait for better error messages in [InventoryFor]
struct ParsedTemplates { trait InventoryContents: Clone {
global: HashMap<PathBuf, Vec<TaskTemplate>>, const GLOBAL_SOURCE_FILE: &'static str;
worktree: HashMap<WorktreeId, HashMap<(Arc<Path>, TaskKind), Vec<TaskTemplate>>>, const LABEL: &'static str;
}
impl InventoryContents for TaskTemplate {
const GLOBAL_SOURCE_FILE: &'static str = "tasks.json";
const LABEL: &'static str = "tasks";
}
impl InventoryContents for DebugScenario {
const GLOBAL_SOURCE_FILE: &'static str = "debug.json";
const LABEL: &'static str = "debug scenarios";
}
#[derive(Debug)]
struct InventoryFor<T> {
global: HashMap<PathBuf, Vec<T>>,
worktree: HashMap<WorktreeId, HashMap<Arc<Path>, Vec<T>>>,
}
impl<T: InventoryContents> InventoryFor<T> {
fn worktree_scenarios(
&self,
worktree: Option<WorktreeId>,
) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> {
worktree.into_iter().flat_map(|worktree| {
self.worktree
.get(&worktree)
.into_iter()
.flatten()
.flat_map(|(directory, templates)| {
templates.iter().map(move |template| (directory, template))
})
.map(move |(directory, template)| {
(
TaskSourceKind::Worktree {
id: worktree,
directory_in_worktree: directory.to_path_buf(),
id_base: Cow::Owned(format!(
"local worktree {} from directory {directory:?}",
T::LABEL
)),
},
template.clone(),
)
})
})
}
fn global_scenarios(&self) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> {
self.global.iter().flat_map(|(file_path, templates)| {
templates.into_iter().map(|template| {
(
TaskSourceKind::AbsPath {
id_base: Cow::Owned(format!("global {}", T::GLOBAL_SOURCE_FILE)),
abs_path: file_path.clone(),
},
template.clone(),
)
})
})
}
}
impl<T> Default for InventoryFor<T> {
fn default() -> Self {
Self {
global: HashMap::default(),
worktree: HashMap::default(),
}
}
} }
/// Kind of a source the tasks are fetched from, used to display more source information in the UI. /// Kind of a source the tasks are fetched from, used to display more source information in the UI.
@ -134,22 +206,40 @@ impl Inventory {
cx.new(|_| Self::default()) cx.new(|_| Self::default())
} }
pub fn list_debug_tasks(&self) -> Vec<&TaskTemplate> { pub fn list_debug_scenarios(&self, worktree: Option<WorktreeId>) -> Vec<DebugScenario> {
self.templates_from_settings let global_scenarios = self.global_debug_scenarios_from_settings();
.worktree let worktree_scenarios = self.worktree_scenarios_from_settings(worktree);
.values()
.flat_map(|tasks| { worktree_scenarios
tasks.iter().filter_map(|(kind, tasks)| { .chain(global_scenarios)
if matches!(kind.1, TaskKind::Debug) { .map(|(_, scenario)| scenario)
Some(tasks)
} else {
None
}
})
})
.flatten()
.collect() .collect()
} }
pub fn task_template_by_label(
&self,
buffer: Option<Entity<Buffer>>,
label: &str,
cx: &App,
) -> Option<TaskTemplate> {
let (worktree_id, file, language) = buffer
.map(|buffer| {
let buffer = buffer.read(cx);
let file = buffer.file().cloned();
(
file.as_ref().map(|file| file.worktree_id(cx)),
file,
buffer.language().cloned(),
)
})
.unwrap_or((None, None, None));
self.list_tasks(file, language, worktree_id, cx)
.iter()
.find(|(_, template)| template.label == label)
.map(|val| val.1.clone())
}
/// Pulls its task sources relevant to the worktree and the language given, /// Pulls its task sources relevant to the worktree and the language given,
/// returns all task templates with their source kinds, worktree tasks first, language tasks second /// returns all task templates with their source kinds, worktree tasks first, language tasks second
/// and global tasks last. No specific order inside source kinds groups. /// and global tasks last. No specific order inside source kinds groups.
@ -160,10 +250,11 @@ impl Inventory {
worktree: Option<WorktreeId>, worktree: Option<WorktreeId>,
cx: &App, cx: &App,
) -> Vec<(TaskSourceKind, TaskTemplate)> { ) -> Vec<(TaskSourceKind, TaskTemplate)> {
let global_tasks = self.global_templates_from_settings();
let worktree_tasks = self.worktree_templates_from_settings(worktree);
let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language { let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
name: language.name().into(), name: language.name().into(),
}); });
let global_tasks = self.global_templates_from_settings();
let language_tasks = language let language_tasks = language
.filter(|language| { .filter(|language| {
language_settings(Some(language.name()), file.as_ref(), cx) language_settings(Some(language.name()), file.as_ref(), cx)
@ -173,11 +264,11 @@ impl Inventory {
.and_then(|language| language.context_provider()?.associated_tasks(file, cx)) .and_then(|language| language.context_provider()?.associated_tasks(file, cx))
.into_iter() .into_iter()
.flat_map(|tasks| tasks.0.into_iter()) .flat_map(|tasks| tasks.0.into_iter())
.flat_map(|task| Some((task_source_kind.clone()?, task))) .flat_map(|task| Some((task_source_kind.clone()?, task)));
.chain(global_tasks);
self.worktree_templates_from_settings(worktree) worktree_tasks
.chain(language_tasks) .chain(language_tasks)
.chain(global_tasks)
.collect() .collect()
} }
@ -358,51 +449,27 @@ impl Inventory {
fn global_templates_from_settings( fn global_templates_from_settings(
&self, &self,
) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> { ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
self.templates_from_settings self.templates_from_settings.global_scenarios()
.global }
.iter()
.flat_map(|(file_path, templates)| { fn global_debug_scenarios_from_settings(
templates.into_iter().map(|template| { &self,
( ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
TaskSourceKind::AbsPath { self.scenarios_from_settings.global_scenarios()
id_base: match template.task_type { }
task::TaskType::Script => Cow::Borrowed("global tasks.json"),
task::TaskType::Debug(_) => Cow::Borrowed("global debug.json"), fn worktree_scenarios_from_settings(
}, &self,
abs_path: file_path.clone(), worktree: Option<WorktreeId>,
}, ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
template.clone(), self.scenarios_from_settings.worktree_scenarios(worktree)
)
})
})
} }
fn worktree_templates_from_settings( fn worktree_templates_from_settings(
&self, &self,
worktree: Option<WorktreeId>, worktree: Option<WorktreeId>,
) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> { ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
worktree.into_iter().flat_map(|worktree| { self.templates_from_settings.worktree_scenarios(worktree)
self.templates_from_settings
.worktree
.get(&worktree)
.into_iter()
.flatten()
.flat_map(|(directory, templates)| {
templates.iter().map(move |template| (directory, template))
})
.map(move |((directory, _task_kind), template)| {
(
TaskSourceKind::Worktree {
id: worktree,
directory_in_worktree: directory.to_path_buf(),
id_base: Cow::Owned(format!(
"local worktree tasks from directory {directory:?}"
)),
},
template.clone(),
)
})
})
} }
/// Updates in-memory task metadata from the JSON string given. /// Updates in-memory task metadata from the JSON string given.
@ -413,7 +480,6 @@ impl Inventory {
&mut self, &mut self,
location: TaskSettingsLocation<'_>, location: TaskSettingsLocation<'_>,
raw_tasks_json: Option<&str>, raw_tasks_json: Option<&str>,
task_kind: TaskKind,
) -> Result<(), InvalidSettingsError> { ) -> Result<(), InvalidSettingsError> {
let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>( let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
raw_tasks_json.unwrap_or("[]"), raw_tasks_json.unwrap_or("[]"),
@ -424,21 +490,16 @@ impl Inventory {
path: match location { path: match location {
TaskSettingsLocation::Global(path) => path.to_owned(), TaskSettingsLocation::Global(path) => path.to_owned(),
TaskSettingsLocation::Worktree(settings_location) => { TaskSettingsLocation::Worktree(settings_location) => {
task_kind.config_in_dir(settings_location.path) settings_location.path.join(task_file_name())
} }
}, },
message: format!("Failed to parse tasks file content as a JSON array: {e}"), message: format!("Failed to parse tasks file content as a JSON array: {e}"),
}); });
} }
}; };
let new_templates = raw_tasks let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
.into_iter() serde_json::from_value::<TaskTemplate>(raw_template).log_err()
.filter_map(|raw_template| match &task_kind { });
TaskKind::Script => serde_json::from_value::<TaskTemplate>(raw_template).log_err(),
TaskKind::Debug => serde_json::from_value::<DebugTaskTemplate>(raw_template)
.log_err()
.map(|content| content.to_zed_format()),
});
let parsed_templates = &mut self.templates_from_settings; let parsed_templates = &mut self.templates_from_settings;
match location { match location {
@ -454,14 +515,72 @@ impl Inventory {
if let Some(worktree_tasks) = if let Some(worktree_tasks) =
parsed_templates.worktree.get_mut(&location.worktree_id) parsed_templates.worktree.get_mut(&location.worktree_id)
{ {
worktree_tasks.remove(&(Arc::from(location.path), task_kind)); worktree_tasks.remove(location.path);
} }
} else { } else {
parsed_templates parsed_templates
.worktree .worktree
.entry(location.worktree_id) .entry(location.worktree_id)
.or_default() .or_default()
.insert((Arc::from(location.path), task_kind), new_templates); .insert(Arc::from(location.path), new_templates);
}
}
}
Ok(())
}
/// Updates in-memory task metadata from the JSON string given.
/// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
///
/// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
pub(crate) fn update_file_based_scenarios(
&mut self,
location: TaskSettingsLocation<'_>,
raw_tasks_json: Option<&str>,
) -> Result<(), InvalidSettingsError> {
let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
raw_tasks_json.unwrap_or("[]"),
) {
Ok(tasks) => tasks,
Err(e) => {
return Err(InvalidSettingsError::Debug {
path: match location {
TaskSettingsLocation::Global(path) => path.to_owned(),
TaskSettingsLocation::Worktree(settings_location) => {
settings_location.path.join(debug_task_file_name())
}
},
message: format!("Failed to parse tasks file content as a JSON array: {e}"),
});
}
};
let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
serde_json::from_value::<DebugScenario>(raw_template).log_err()
});
let parsed_scenarios = &mut self.scenarios_from_settings;
match location {
TaskSettingsLocation::Global(path) => {
parsed_scenarios
.global
.entry(path.to_owned())
.insert_entry(new_templates.collect());
}
TaskSettingsLocation::Worktree(location) => {
let new_templates = new_templates.collect::<Vec<_>>();
if new_templates.is_empty() {
if let Some(worktree_tasks) =
parsed_scenarios.worktree.get_mut(&location.worktree_id)
{
worktree_tasks.remove(location.path);
}
} else {
parsed_scenarios
.worktree
.entry(location.worktree_id)
.or_default()
.insert(Arc::from(location.path), new_templates);
} }
} }
} }
@ -677,6 +796,10 @@ impl ContextProvider for BasicContextProvider {
Task::ready(Ok(task_variables)) Task::ready(Ok(task_variables))
} }
fn debug_adapter(&self) -> Option<String> {
None
}
} }
/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks. /// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
@ -700,6 +823,10 @@ impl ContextProvider for ContextProviderWithTasks {
) -> Option<TaskTemplates> { ) -> Option<TaskTemplates> {
Some(self.templates.clone()) Some(self.templates.clone())
} }
fn debug_adapter(&self) -> Option<String> {
None
}
} }
#[cfg(test)] #[cfg(test)]
@ -744,7 +871,6 @@ mod tests {
Some(&mock_tasks_from_names( Some(&mock_tasks_from_names(
expected_initial_state.iter().map(|name| name.as_str()), expected_initial_state.iter().map(|name| name.as_str()),
)), )),
settings::TaskKind::Script,
) )
.unwrap(); .unwrap();
}); });
@ -800,7 +926,6 @@ mod tests {
.into_iter() .into_iter()
.chain(expected_initial_state.iter().map(|name| name.as_str())), .chain(expected_initial_state.iter().map(|name| name.as_str())),
)), )),
settings::TaskKind::Script,
) )
.unwrap(); .unwrap();
}); });
@ -925,7 +1050,6 @@ mod tests {
.iter() .iter()
.map(|(_, name)| name.as_str()), .map(|(_, name)| name.as_str()),
)), )),
settings::TaskKind::Script,
) )
.unwrap(); .unwrap();
inventory inventory
@ -937,7 +1061,6 @@ mod tests {
Some(&mock_tasks_from_names( Some(&mock_tasks_from_names(
worktree_1_tasks.iter().map(|(_, name)| name.as_str()), worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
)), )),
settings::TaskKind::Script,
) )
.unwrap(); .unwrap();
inventory inventory
@ -949,7 +1072,6 @@ mod tests {
Some(&mock_tasks_from_names( Some(&mock_tasks_from_names(
worktree_2_tasks.iter().map(|(_, name)| name.as_str()), worktree_2_tasks.iter().map(|(_, name)| name.as_str()),
)), )),
settings::TaskKind::Script,
) )
.unwrap(); .unwrap();
}); });

View file

@ -11,7 +11,7 @@ use language::{
proto::{deserialize_anchor, serialize_anchor}, proto::{deserialize_anchor, serialize_anchor},
}; };
use rpc::{AnyProtoClient, TypedEnvelope, proto}; use rpc::{AnyProtoClient, TypedEnvelope, proto};
use settings::{InvalidSettingsError, SettingsLocation, TaskKind}; use settings::{InvalidSettingsError, SettingsLocation};
use task::{TaskContext, TaskVariables, VariableName}; use task::{TaskContext, TaskVariables, VariableName};
use text::{BufferId, OffsetRangeExt}; use text::{BufferId, OffsetRangeExt};
use util::ResultExt; use util::ResultExt;
@ -264,7 +264,6 @@ impl TaskStore {
&self, &self,
location: TaskSettingsLocation<'_>, location: TaskSettingsLocation<'_>,
raw_tasks_json: Option<&str>, raw_tasks_json: Option<&str>,
task_type: TaskKind,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Result<(), InvalidSettingsError> { ) -> Result<(), InvalidSettingsError> {
let task_inventory = match self { let task_inventory = match self {
@ -276,7 +275,26 @@ impl TaskStore {
.filter(|json| !json.is_empty()); .filter(|json| !json.is_empty());
task_inventory.update(cx, |inventory, _| { task_inventory.update(cx, |inventory, _| {
inventory.update_file_based_tasks(location, raw_tasks_json, task_type) inventory.update_file_based_tasks(location, raw_tasks_json)
})
}
pub(super) fn update_user_debug_scenarios(
&self,
location: TaskSettingsLocation<'_>,
raw_tasks_json: Option<&str>,
cx: &mut Context<Self>,
) -> Result<(), InvalidSettingsError> {
let task_inventory = match self {
TaskStore::Functional(state) => &state.task_inventory,
TaskStore::Noop => return Ok(()),
};
let raw_tasks_json = raw_tasks_json
.map(|json| json.trim())
.filter(|json| !json.is_empty());
task_inventory.update(cx, |inventory, _| {
inventory.update_file_based_scenarios(location, raw_tasks_json)
}) })
} }
} }

View file

@ -543,6 +543,7 @@ message DebugLaunchRequest {
string program = 1; string program = 1;
optional string cwd = 2; optional string cwd = 2;
repeated string args = 3; repeated string args = 3;
map<string, string> env = 4;
} }
message DebugAttachRequest { message DebugAttachRequest {
@ -558,7 +559,7 @@ message DapModuleId {
message GetDebugAdapterBinary { message GetDebugAdapterBinary {
uint64 project_id = 1; uint64 project_id = 1;
DebugTaskDefinition task = 2; DebugTaskDefinition definition = 2;
} }
message DebugAdapterBinary { message DebugAdapterBinary {
@ -575,8 +576,32 @@ message DebugAdapterBinary {
} }
} }
message RunDebugLocator { message RunDebugLocators {
uint64 project_id = 1; uint64 project_id = 1;
string locator = 2; SpawnInTerminal build_command = 2;
DebugTaskDefinition task = 3; }
message DebugRequest {
oneof request {
DebugLaunchRequest debug_launch_request = 1;
DebugAttachRequest debug_attach_request = 2;
}
}
message DebugScenario {
string label = 1;
string adapter = 2;
reserved 3;
DebugRequest request = 4;
optional TcpHost connection = 5;
optional bool stop_on_entry = 6;
optional string configuration = 7;
}
message SpawnInTerminal {
string label = 1;
string command = 2;
repeated string args = 3;
map<string, string> env = 4;
optional string cwd = 5;
} }

View file

@ -148,4 +148,5 @@ enum LocalSettingsKind {
Settings = 0; Settings = 0;
Tasks = 1; Tasks = 1;
Editorconfig = 2; Editorconfig = 2;
Debug = 3;
} }

View file

@ -377,8 +377,8 @@ message Envelope {
GetDebugAdapterBinary get_debug_adapter_binary = 339; GetDebugAdapterBinary get_debug_adapter_binary = 339;
DebugAdapterBinary debug_adapter_binary = 340; DebugAdapterBinary debug_adapter_binary = 340;
RunDebugLocator run_debug_locator = 341; RunDebugLocators run_debug_locators = 341;
DebugTaskDefinition debug_task_definition = 342; // current max DebugRequest debug_request = 342; // current max
} }
reserved 87 to 88; reserved 87 to 88;

View file

@ -298,8 +298,8 @@ messages!(
(GitInit, Background), (GitInit, Background),
(GetDebugAdapterBinary, Background), (GetDebugAdapterBinary, Background),
(DebugAdapterBinary, Background), (DebugAdapterBinary, Background),
(RunDebugLocator, Background), (RunDebugLocators, Background),
(DebugTaskDefinition, Background), (DebugRequest, Background),
); );
request_messages!( request_messages!(
@ -456,7 +456,7 @@ request_messages!(
(GitInit, Ack), (GitInit, Ack),
(ToggleBreakpoint, Ack), (ToggleBreakpoint, Ack),
(GetDebugAdapterBinary, DebugAdapterBinary), (GetDebugAdapterBinary, DebugAdapterBinary),
(RunDebugLocator, DebugTaskDefinition), (RunDebugLocators, DebugRequest),
); );
entity_messages!( entity_messages!(
@ -576,7 +576,7 @@ entity_messages!(
GitInit, GitInit,
BreakpointsForFile, BreakpointsForFile,
ToggleBreakpoint, ToggleBreakpoint,
RunDebugLocator, RunDebugLocators,
GetDebugAdapterBinary, GetDebugAdapterBinary,
); );

View file

@ -245,7 +245,7 @@ impl HeadlessProject {
LspStore::init(&client); LspStore::init(&client);
TaskStore::init(Some(&client)); TaskStore::init(Some(&client));
ToolchainStore::init(&client); ToolchainStore::init(&client);
DapStore::init(&client); DapStore::init(&client, cx);
// todo(debugger): Re init breakpoint store when we set it up for collab // todo(debugger): Re init breakpoint store when we set it up for collab
// BreakpointStore::init(&client); // BreakpointStore::init(&client);
GitStore::init(&client); GitStore::init(&client);

View file

@ -20,7 +20,7 @@ pub use keymap_file::{
pub use settings_file::*; pub use settings_file::*;
pub use settings_store::{ pub use settings_store::{
InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
SettingsStore, TaskKind, parse_json_with_comments, SettingsStore, parse_json_with_comments,
}; };
pub use vscode_import::VsCodeSettings; pub use vscode_import::VsCodeSettings;

View file

@ -5,9 +5,7 @@ use fs::Fs;
use futures::{FutureExt, StreamExt, channel::mpsc, future::LocalBoxFuture}; use futures::{FutureExt, StreamExt, channel::mpsc, future::LocalBoxFuture};
use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal}; use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal};
use paths::{ use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name};
EDITORCONFIG_NAME, debug_task_file_name, local_settings_file_relative_path, task_file_name,
};
use schemars::{JsonSchema, r#gen::SchemaGenerator, schema::RootSchema}; use schemars::{JsonSchema, r#gen::SchemaGenerator, schema::RootSchema};
use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::Value; use serde_json::Value;
@ -217,14 +215,9 @@ impl FromStr for Editorconfig {
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum LocalSettingsKind { pub enum LocalSettingsKind {
Settings, Settings,
Tasks(TaskKind), Tasks,
Editorconfig, Editorconfig,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum TaskKind {
Debug, Debug,
Script,
} }
impl Global for SettingsStore {} impl Global for SettingsStore {}
@ -265,16 +258,6 @@ trait AnySettingValue: 'static + Send + Sync {
struct DeserializedSetting(Box<dyn Any>); struct DeserializedSetting(Box<dyn Any>);
impl TaskKind {
/// Returns a file path of a task configuration file of this kind within the given directory.
pub fn config_in_dir(&self, dir: &Path) -> PathBuf {
dir.join(match self {
Self::Debug => debug_task_file_name(),
Self::Script => task_file_name(),
})
}
}
impl SettingsStore { impl SettingsStore {
pub fn new(cx: &App) -> Self { pub fn new(cx: &App) -> Self {
let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded(); let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded();
@ -684,10 +667,17 @@ impl SettingsStore {
.map(|content| content.trim()) .map(|content| content.trim())
.filter(|content| !content.is_empty()), .filter(|content| !content.is_empty()),
) { ) {
(LocalSettingsKind::Tasks(task_kind), _) => { (LocalSettingsKind::Tasks, _) => {
return Err(InvalidSettingsError::Tasks { return Err(InvalidSettingsError::Tasks {
message: "Attempted to submit tasks into the settings store".to_string(), message: "Attempted to submit tasks into the settings store".to_string(),
path: task_kind.config_in_dir(&directory_path), path: directory_path.join(task_file_name()),
});
}
(LocalSettingsKind::Debug, _) => {
return Err(InvalidSettingsError::Debug {
message: "Attempted to submit debugger config into the settings store"
.to_string(),
path: directory_path.join(task_file_name()),
}); });
} }
(LocalSettingsKind::Settings, None) => { (LocalSettingsKind::Settings, None) => {
@ -1085,6 +1075,7 @@ pub enum InvalidSettingsError {
DefaultSettings { message: String }, DefaultSettings { message: String },
Editorconfig { path: PathBuf, message: String }, Editorconfig { path: PathBuf, message: String },
Tasks { path: PathBuf, message: String }, Tasks { path: PathBuf, message: String },
Debug { path: PathBuf, message: String },
} }
impl std::fmt::Display for InvalidSettingsError { impl std::fmt::Display for InvalidSettingsError {
@ -1095,7 +1086,8 @@ impl std::fmt::Display for InvalidSettingsError {
| InvalidSettingsError::ServerSettings { message } | InvalidSettingsError::ServerSettings { message }
| InvalidSettingsError::DefaultSettings { message } | InvalidSettingsError::DefaultSettings { message }
| InvalidSettingsError::Tasks { message, .. } | InvalidSettingsError::Tasks { message, .. }
| InvalidSettingsError::Editorconfig { message, .. } => { | InvalidSettingsError::Editorconfig { message, .. }
| InvalidSettingsError::Debug { message, .. } => {
write!(f, "{message}") write!(f, "{message}")
} }
} }

View file

@ -1,11 +1,11 @@
use anyhow::Result; use anyhow::Result;
use collections::FxHashMap;
use gpui::SharedString;
use schemars::{JsonSchema, r#gen::SchemaSettings}; use schemars::{JsonSchema, r#gen::SchemaSettings};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
use std::{net::Ipv4Addr, path::Path}; use std::{net::Ipv4Addr, path::Path};
use crate::{TaskTemplate, TaskType, task_template::DebugArgs};
/// Represents the host information of the debug adapter /// Represents the host information of the debug adapter
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
pub struct TcpArgumentsTemplate { pub struct TcpArgumentsTemplate {
@ -63,6 +63,8 @@ pub struct LaunchRequest {
/// Arguments to pass to a debuggee /// Arguments to pass to a debuggee
#[serde(default)] #[serde(default)]
pub args: Vec<String>, pub args: Vec<String>,
#[serde(default)]
pub env: FxHashMap<String, String>,
} }
/// Represents the type that will determine which request to call on the debug adapter /// Represents the type that will determine which request to call on the debug adapter
@ -75,6 +77,64 @@ pub enum DebugRequest {
Attach(AttachRequest), Attach(AttachRequest),
} }
impl DebugRequest {
pub fn to_proto(&self) -> proto::DebugRequest {
match self {
DebugRequest::Launch(launch_request) => proto::DebugRequest {
request: Some(proto::debug_request::Request::DebugLaunchRequest(
proto::DebugLaunchRequest {
program: launch_request.program.clone(),
cwd: launch_request
.cwd
.as_ref()
.map(|cwd| cwd.to_string_lossy().into_owned()),
args: launch_request.args.clone(),
env: launch_request
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
},
)),
},
DebugRequest::Attach(attach_request) => proto::DebugRequest {
request: Some(proto::debug_request::Request::DebugAttachRequest(
proto::DebugAttachRequest {
process_id: attach_request
.process_id
.expect("The process ID to be already filled out."),
},
)),
},
}
}
pub fn from_proto(val: proto::DebugRequest) -> Result<DebugRequest> {
let request = val
.request
.ok_or_else(|| anyhow::anyhow!("Missing debug request"))?;
match request {
proto::debug_request::Request::DebugLaunchRequest(proto::DebugLaunchRequest {
program,
cwd,
args,
env,
}) => Ok(DebugRequest::Launch(LaunchRequest {
program,
cwd: cwd.map(From::from),
args,
env: env.into_iter().collect(),
})),
proto::debug_request::Request::DebugAttachRequest(proto::DebugAttachRequest {
process_id,
}) => Ok(DebugRequest::Attach(AttachRequest {
process_id: Some(process_id),
})),
}
}
}
impl From<LaunchRequest> for DebugRequest { impl From<LaunchRequest> for DebugRequest {
fn from(launch_config: LaunchRequest) -> Self { fn from(launch_config: LaunchRequest) -> Self {
DebugRequest::Launch(launch_config) DebugRequest::Launch(launch_config)
@ -87,180 +147,46 @@ impl From<AttachRequest> for DebugRequest {
} }
} }
impl TryFrom<TaskTemplate> for DebugTaskTemplate {
type Error = ();
fn try_from(value: TaskTemplate) -> Result<Self, Self::Error> {
let TaskType::Debug(debug_args) = value.task_type else {
return Err(());
};
let request = match debug_args.request {
crate::DebugArgsRequest::Launch => DebugRequest::Launch(LaunchRequest {
program: value.command,
cwd: value.cwd.map(PathBuf::from),
args: value.args,
}),
crate::DebugArgsRequest::Attach(attach_config) => DebugRequest::Attach(attach_config),
};
Ok(DebugTaskTemplate {
locator: debug_args.locator,
definition: DebugTaskDefinition {
adapter: debug_args.adapter,
request,
label: value.label,
initialize_args: debug_args.initialize_args,
tcp_connection: debug_args.tcp_connection,
stop_on_entry: debug_args.stop_on_entry,
},
})
}
}
impl DebugTaskTemplate {
/// Translate from debug definition to a task template
pub fn to_zed_format(self) -> TaskTemplate {
let (command, cwd, request) = match self.definition.request {
DebugRequest::Launch(launch_config) => (
launch_config.program,
launch_config
.cwd
.map(|cwd| cwd.to_string_lossy().to_string()),
crate::task_template::DebugArgsRequest::Launch,
),
DebugRequest::Attach(attach_config) => (
"".to_owned(),
None,
crate::task_template::DebugArgsRequest::Attach(attach_config),
),
};
let task_type = TaskType::Debug(DebugArgs {
adapter: self.definition.adapter,
request,
initialize_args: self.definition.initialize_args,
locator: self.locator,
tcp_connection: self.definition.tcp_connection,
stop_on_entry: self.definition.stop_on_entry,
});
let label = self.definition.label.clone();
TaskTemplate {
label,
command,
args: vec![],
task_type,
cwd,
..Default::default()
}
}
}
#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub struct DebugTaskTemplate {
pub locator: Option<String>,
#[serde(flatten)]
pub definition: DebugTaskDefinition,
}
/// This struct represent a user created debug task /// This struct represent a user created debug task
#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct DebugTaskDefinition { pub struct DebugScenario {
/// The adapter to run pub adapter: SharedString,
pub adapter: String,
/// The type of request that should be called on the debug adapter
#[serde(flatten)]
pub request: DebugRequest,
/// Name of the debug task /// Name of the debug task
pub label: String, pub label: SharedString,
/// A task to run prior to spawning the debuggee.
pub build: Option<SharedString>,
#[serde(flatten)]
pub request: Option<DebugRequest>,
/// Additional initialization arguments to be sent on DAP initialization /// Additional initialization arguments to be sent on DAP initialization
#[serde(default)]
pub initialize_args: Option<serde_json::Value>, pub initialize_args: Option<serde_json::Value>,
/// Optional TCP connection information /// Optional TCP connection information
/// ///
/// If provided, this will be used to connect to the debug adapter instead of /// If provided, this will be used to connect to the debug adapter instead of
/// spawning a new process. This is useful for connecting to a debug adapter /// spawning a new process. This is useful for connecting to a debug adapter
/// that is already running or is started by another process. /// that is already running or is started by another process.
#[serde(default)]
pub tcp_connection: Option<TcpArgumentsTemplate>, pub tcp_connection: Option<TcpArgumentsTemplate>,
/// Whether to tell the debug adapter to stop on entry /// Whether to tell the debug adapter to stop on entry
#[serde(default)]
pub stop_on_entry: Option<bool>, pub stop_on_entry: Option<bool>,
} }
impl DebugTaskDefinition { impl DebugScenario {
pub fn cwd(&self) -> Option<&Path> { pub fn cwd(&self) -> Option<&Path> {
if let DebugRequest::Launch(config) = &self.request { if let Some(DebugRequest::Launch(config)) = &self.request {
config.cwd.as_deref() config.cwd.as_ref().map(Path::new)
} else { } else {
None None
} }
} }
pub fn to_proto(&self) -> proto::DebugTaskDefinition {
proto::DebugTaskDefinition {
adapter: self.adapter.clone(),
request: Some(match &self.request {
DebugRequest::Launch(config) => {
proto::debug_task_definition::Request::DebugLaunchRequest(
proto::DebugLaunchRequest {
program: config.program.clone(),
cwd: config.cwd.as_ref().map(|c| c.to_string_lossy().to_string()),
args: config.args.clone(),
},
)
}
DebugRequest::Attach(attach_request) => {
proto::debug_task_definition::Request::DebugAttachRequest(
proto::DebugAttachRequest {
process_id: attach_request.process_id.unwrap_or_default(),
},
)
}
}),
label: self.label.clone(),
initialize_args: self.initialize_args.as_ref().map(|v| v.to_string()),
tcp_connection: self.tcp_connection.as_ref().map(|t| t.to_proto()),
stop_on_entry: self.stop_on_entry,
}
}
pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> {
let request = proto
.request
.ok_or_else(|| anyhow::anyhow!("request is required"))?;
Ok(Self {
label: proto.label,
initialize_args: proto.initialize_args.map(|v| v.into()),
tcp_connection: proto
.tcp_connection
.map(TcpArgumentsTemplate::from_proto)
.transpose()?,
stop_on_entry: proto.stop_on_entry,
adapter: proto.adapter.clone(),
request: match request {
proto::debug_task_definition::Request::DebugAttachRequest(config) => {
DebugRequest::Attach(AttachRequest {
process_id: Some(config.process_id),
})
}
proto::debug_task_definition::Request::DebugLaunchRequest(config) => {
DebugRequest::Launch(LaunchRequest {
program: config.program,
cwd: config.cwd.map(|cwd| cwd.into()),
args: config.args,
})
}
},
})
}
} }
/// A group of Debug Tasks defined in a JSON file. /// A group of Debug Tasks defined in a JSON file.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)] #[serde(transparent)]
pub struct DebugTaskFile(pub Vec<DebugTaskTemplate>); pub struct DebugTaskFile(pub Vec<DebugScenario>);
impl DebugTaskFile { impl DebugTaskFile {
/// Generates JSON schema of Tasks JSON template format. /// Generates JSON schema of Tasks JSON template format.

View file

@ -16,12 +16,10 @@ use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
pub use debug_format::{ pub use debug_format::{
AttachRequest, DebugRequest, DebugTaskDefinition, DebugTaskFile, DebugTaskTemplate, AttachRequest, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, TcpArgumentsTemplate,
LaunchRequest, TcpArgumentsTemplate,
}; };
pub use task_template::{ pub use task_template::{
DebugArgs, DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate, DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate, TaskTemplates,
TaskTemplates, TaskType,
}; };
pub use vscode_debug_format::VsCodeDebugTaskFile; pub use vscode_debug_format::VsCodeDebugTaskFile;
pub use vscode_format::VsCodeTaskFile; pub use vscode_format::VsCodeTaskFile;
@ -29,11 +27,11 @@ pub use zed_actions::RevealTarget;
/// Task identifier, unique within the application. /// Task identifier, unique within the application.
/// Based on it, task reruns and terminal tabs are managed. /// Based on it, task reruns and terminal tabs are managed.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)] #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)]
pub struct TaskId(pub String); pub struct TaskId(pub String);
/// Contains all information needed by Zed to spawn a new terminal tab for the given task. /// Contains all information needed by Zed to spawn a new terminal tab for the given task.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct SpawnInTerminal { pub struct SpawnInTerminal {
/// Id of the task to use when determining task tab affinity. /// Id of the task to use when determining task tab affinity.
pub id: TaskId, pub id: TaskId,
@ -72,6 +70,36 @@ pub struct SpawnInTerminal {
pub show_rerun: bool, pub show_rerun: bool,
} }
impl SpawnInTerminal {
pub fn to_proto(&self) -> proto::SpawnInTerminal {
proto::SpawnInTerminal {
label: self.label.clone(),
command: self.command.clone(),
args: self.args.clone(),
env: self
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
cwd: self
.cwd
.clone()
.map(|cwd| cwd.to_string_lossy().into_owned()),
}
}
pub fn from_proto(proto: proto::SpawnInTerminal) -> Self {
Self {
label: proto.label.clone(),
command: proto.command.clone(),
args: proto.args.clone(),
env: proto.env.into_iter().collect(),
cwd: proto.cwd.map(PathBuf::from).clone(),
..Default::default()
}
}
}
/// A final form of the [`TaskTemplate`], that got resolved with a particular [`TaskContext`] and now is ready to spawn the actual task. /// A final form of the [`TaskTemplate`], that got resolved with a particular [`TaskContext`] and now is ready to spawn the actual task.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedTask { pub struct ResolvedTask {
@ -89,7 +117,7 @@ pub struct ResolvedTask {
substituted_variables: HashSet<VariableName>, substituted_variables: HashSet<VariableName>,
/// Further actions that need to take place after the resolved task is spawned, /// Further actions that need to take place after the resolved task is spawned,
/// with all task variables resolved. /// with all task variables resolved.
pub resolved: Option<SpawnInTerminal>, pub resolved: SpawnInTerminal,
} }
impl ResolvedTask { impl ResolvedTask {
@ -98,63 +126,6 @@ impl ResolvedTask {
&self.original_task &self.original_task
} }
/// Get the task type that determines what this task is used for
/// And where is it shown in the UI
pub fn task_type(&self) -> TaskType {
self.original_task.task_type.clone()
}
/// Get the configuration for the debug adapter that should be used for this task.
pub fn resolved_debug_adapter_config(&self) -> Option<DebugTaskTemplate> {
match self.original_task.task_type.clone() {
TaskType::Debug(debug_args) if self.resolved.is_some() => {
let resolved = self
.resolved
.as_ref()
.expect("We just checked if this was some");
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(DebugTaskTemplate {
locator: debug_args.locator.clone(),
definition: DebugTaskDefinition {
label: resolved.label.clone(),
adapter: debug_args.adapter.clone(),
request: match debug_args.request {
crate::task_template::DebugArgsRequest::Launch => {
DebugRequest::Launch(LaunchRequest {
program: resolved.command.clone(),
cwd: resolved.cwd.clone(),
args,
})
}
crate::task_template::DebugArgsRequest::Attach(attach_config) => {
DebugRequest::Attach(attach_config)
}
},
initialize_args: debug_args.initialize_args,
tcp_connection: debug_args.tcp_connection,
stop_on_entry: debug_args.stop_on_entry,
},
})
}
_ => None,
}
}
/// Variables that were substituted during the task template resolution. /// Variables that were substituted during the task template resolution.
pub fn substituted_variables(&self) -> &HashSet<VariableName> { pub fn substituted_variables(&self) -> &HashSet<VariableName> {
&self.substituted_variables &self.substituted_variables
@ -162,10 +133,7 @@ impl ResolvedTask {
/// A human-readable label to display in the UI. /// A human-readable label to display in the UI.
pub fn display_label(&self) -> &str { pub fn display_label(&self) -> &str {
self.resolved self.resolved.label.as_str()
.as_ref()
.map(|resolved| resolved.label.as_str())
.unwrap_or_else(|| self.resolved_label.as_str())
} }
} }

View file

@ -7,7 +7,6 @@ use std::path::PathBuf;
use util::serde::default_true; use util::serde::default_true;
use util::{ResultExt, truncate_and_remove_front}; use util::{ResultExt, truncate_and_remove_front};
use crate::debug_format::TcpArgumentsTemplate;
use crate::{ use crate::{
AttachRequest, ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TaskContext, TaskId, AttachRequest, ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TaskContext, TaskId,
VariableName, ZED_VARIABLE_NAME_PREFIX, VariableName, ZED_VARIABLE_NAME_PREFIX,
@ -59,9 +58,6 @@ pub struct TaskTemplate {
/// * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always`. /// * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always`.
#[serde(default)] #[serde(default)]
pub hide: HideStrategy, pub hide: HideStrategy,
/// If this task should start a debugger or not
#[serde(default, skip)]
pub task_type: TaskType,
/// Represents the tags which this template attaches to. /// Represents the tags which this template attaches to.
/// Adding this removes this task from other UI and gives you ability to run it by tag. /// Adding this removes this task from other UI and gives you ability to run it by tag.
#[serde(default, deserialize_with = "non_empty_string_vec")] #[serde(default, deserialize_with = "non_empty_string_vec")]
@ -87,34 +83,6 @@ pub enum DebugArgsRequest {
Attach(AttachRequest), Attach(AttachRequest),
} }
#[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<TcpArgumentsTemplate>,
/// Args to send to debug adapter
pub initialize_args: Option<serde_json::value::Value>,
/// the locator to use
pub locator: Option<String>,
/// Whether to tell the debug adapter to stop on entry
pub stop_on_entry: Option<bool>,
}
/// Represents the type of task that is being ran
#[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(DebugArgs),
}
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
/// The type of task modal to spawn /// The type of task modal to spawn
pub enum TaskModal { pub enum TaskModal {
@ -174,9 +142,7 @@ impl TaskTemplate {
/// Every [`ResolvedTask`] gets a [`TaskId`], based on the `id_base` (to avoid collision with various task sources), /// Every [`ResolvedTask`] gets a [`TaskId`], based on the `id_base` (to avoid collision with various task sources),
/// and hashes of its template and [`TaskContext`], see [`ResolvedTask`] fields' documentation for more details. /// and hashes of its template and [`TaskContext`], see [`ResolvedTask`] fields' documentation for more details.
pub fn resolve_task(&self, id_base: &str, cx: &TaskContext) -> Option<ResolvedTask> { pub fn resolve_task(&self, id_base: &str, cx: &TaskContext) -> Option<ResolvedTask> {
if self.label.trim().is_empty() if self.label.trim().is_empty() || self.command.trim().is_empty() {
|| (self.command.trim().is_empty() && matches!(self.task_type, TaskType::Script))
{
return None; return None;
} }
@ -285,7 +251,7 @@ impl TaskTemplate {
substituted_variables, substituted_variables,
original_task: self.clone(), original_task: self.clone(),
resolved_label: full_label.clone(), resolved_label: full_label.clone(),
resolved: Some(SpawnInTerminal { resolved: SpawnInTerminal {
id, id,
cwd, cwd,
full_label, full_label,
@ -310,7 +276,7 @@ impl TaskTemplate {
show_summary: self.show_summary, show_summary: self.show_summary,
show_command: self.show_command, show_command: self.show_command,
show_rerun: true, show_rerun: true,
}), },
}) })
} }
} }
@ -474,12 +440,7 @@ mod tests {
.resolve_task(TEST_ID_BASE, task_cx) .resolve_task(TEST_ID_BASE, task_cx)
.unwrap_or_else(|| panic!("failed to resolve task {task_without_cwd:?}")); .unwrap_or_else(|| panic!("failed to resolve task {task_without_cwd:?}"));
assert_substituted_variables(&resolved_task, Vec::new()); assert_substituted_variables(&resolved_task, Vec::new());
resolved_task resolved_task.resolved
.resolved
.clone()
.unwrap_or_else(|| {
panic!("failed to get resolve data for resolved task. Template: {task_without_cwd:?} Resolved: {resolved_task:?}")
})
}; };
let cx = TaskContext { let cx = TaskContext {
@ -626,10 +587,7 @@ mod tests {
all_variables.iter().map(|(name, _)| name.clone()).collect(), all_variables.iter().map(|(name, _)| name.clone()).collect(),
); );
let spawn_in_terminal = resolved_task let spawn_in_terminal = &resolved_task.resolved;
.resolved
.as_ref()
.expect("should have resolved a spawn in terminal task");
assert_eq!( assert_eq!(
spawn_in_terminal.label, spawn_in_terminal.label,
format!( format!(
@ -713,7 +671,7 @@ mod tests {
.resolve_task(TEST_ID_BASE, &TaskContext::default()) .resolve_task(TEST_ID_BASE, &TaskContext::default())
.unwrap(); .unwrap();
assert_substituted_variables(&resolved_task, Vec::new()); assert_substituted_variables(&resolved_task, Vec::new());
let resolved = resolved_task.resolved.unwrap(); let resolved = resolved_task.resolved;
assert_eq!(resolved.label, task.label); assert_eq!(resolved.label, task.label);
assert_eq!(resolved.command, task.command); assert_eq!(resolved.command, task.command);
assert_eq!(resolved.args, task.args); assert_eq!(resolved.args, task.args);
@ -882,8 +840,7 @@ mod tests {
let resolved = template let resolved = template
.resolve_task(TEST_ID_BASE, &context) .resolve_task(TEST_ID_BASE, &context)
.unwrap() .unwrap()
.resolved .resolved;
.unwrap();
assert_eq!(resolved.env["TASK_ENV_VAR1"], "TASK_ENV_VAR1_VALUE"); assert_eq!(resolved.env["TASK_ENV_VAR1"], "TASK_ENV_VAR1_VALUE");
assert_eq!(resolved.env["TASK_ENV_VAR2"], "env_var_2 1234 5678"); assert_eq!(resolved.env["TASK_ENV_VAR2"], "env_var_2 1234 5678");

View file

@ -2,12 +2,13 @@ use std::path::PathBuf;
use anyhow::anyhow; use anyhow::anyhow;
use collections::HashMap; use collections::HashMap;
use gpui::SharedString;
use serde::Deserialize; use serde::Deserialize;
use util::ResultExt as _; use util::ResultExt as _;
use crate::{ use crate::{
AttachRequest, DebugRequest, DebugTaskDefinition, DebugTaskFile, DebugTaskTemplate, AttachRequest, DebugRequest, DebugScenario, DebugTaskFile, EnvVariableReplacer, LaunchRequest,
EnvVariableReplacer, LaunchRequest, TcpArgumentsTemplate, VariableName, TcpArgumentsTemplate, VariableName,
}; };
#[derive(Clone, Debug, Deserialize, PartialEq)] #[derive(Clone, Debug, Deserialize, PartialEq)]
@ -43,11 +44,12 @@ struct VsCodeDebugTaskDefinition {
} }
impl VsCodeDebugTaskDefinition { impl VsCodeDebugTaskDefinition {
fn try_to_zed(self, replacer: &EnvVariableReplacer) -> anyhow::Result<DebugTaskTemplate> { fn try_to_zed(self, replacer: &EnvVariableReplacer) -> anyhow::Result<DebugScenario> {
let label = replacer.replace(&self.name); let label = replacer.replace(&self.name).into();
// TODO based on grep.app results it seems that vscode supports whitespace-splitting this field (ugh) // TODO based on grep.app results it seems that vscode supports whitespace-splitting this field (ugh)
let definition = DebugTaskDefinition { let definition = DebugScenario {
label, label,
build: None,
request: match self.request { request: match self.request {
Request::Launch => { Request::Launch => {
let cwd = self.cwd.map(|cwd| PathBuf::from(replacer.replace(&cwd))); let cwd = self.cwd.map(|cwd| PathBuf::from(replacer.replace(&cwd)));
@ -60,11 +62,22 @@ impl VsCodeDebugTaskDefinition {
.into_iter() .into_iter()
.map(|arg| replacer.replace(&arg)) .map(|arg| replacer.replace(&arg))
.collect(); .collect();
DebugRequest::Launch(LaunchRequest { program, cwd, args }) let env = self
.env
.into_iter()
.filter_map(|(k, v)| v.map(|v| (k, v)))
.collect();
DebugRequest::Launch(LaunchRequest {
program,
cwd,
args,
env,
})
.into()
} }
Request::Attach => DebugRequest::Attach(AttachRequest { process_id: None }), Request::Attach => DebugRequest::Attach(AttachRequest { process_id: None }).into(),
}, },
adapter: task_type_to_adapter_name(self.r#type), adapter: task_type_to_adapter_name(&self.r#type),
// TODO host? // TODO host?
tcp_connection: self.port.map(|port| TcpArgumentsTemplate { tcp_connection: self.port.map(|port| TcpArgumentsTemplate {
port: Some(port), port: Some(port),
@ -75,11 +88,7 @@ impl VsCodeDebugTaskDefinition {
// TODO // TODO
initialize_args: None, initialize_args: None,
}; };
let template = DebugTaskTemplate { Ok(definition)
locator: None,
definition,
};
Ok(template)
} }
} }
@ -110,24 +119,26 @@ impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
} }
} }
// TODO figure out how to make JsDebugAdapter::ADAPTER_NAME et al available here // todo(debugger) figure out how to make JsDebugAdapter::ADAPTER_NAME et al available here
fn task_type_to_adapter_name(task_type: String) -> String { fn task_type_to_adapter_name(task_type: &str) -> SharedString {
match task_type.as_str() { match task_type {
"node" => "JavaScript".to_owned(), "node" => "JavaScript",
"go" => "Delve".to_owned(), "go" => "Delve",
"php" => "PHP".to_owned(), "php" => "PHP",
"cppdbg" | "lldb" => "CodeLLDB".to_owned(), "cppdbg" | "lldb" => "CodeLLDB",
"debugpy" => "Debugpy".to_owned(), "debugpy" => "Debugpy",
_ => task_type, _ => task_type,
} }
.to_owned()
.into()
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{
DebugRequest, DebugTaskDefinition, DebugTaskFile, DebugTaskTemplate, LaunchRequest, use collections::FxHashMap;
TcpArgumentsTemplate,
}; use crate::{DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, TcpArgumentsTemplate};
use super::VsCodeDebugTaskFile; use super::VsCodeDebugTaskFile;
@ -159,24 +170,23 @@ mod tests {
let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates"); let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates");
pretty_assertions::assert_eq!( pretty_assertions::assert_eq!(
zed, zed,
DebugTaskFile(vec![DebugTaskTemplate { DebugTaskFile(vec![DebugScenario {
locator: None, label: "Debug my JS app".into(),
definition: DebugTaskDefinition { adapter: "JavaScript".into(),
label: "Debug my JS app".into(), stop_on_entry: Some(true),
adapter: "JavaScript".into(), initialize_args: None,
stop_on_entry: Some(true), tcp_connection: Some(TcpArgumentsTemplate {
initialize_args: None, port: Some(17),
tcp_connection: Some(TcpArgumentsTemplate { host: None,
port: Some(17), timeout: None,
host: None, }),
timeout: None, request: Some(DebugRequest::Launch(LaunchRequest {
}), program: "${ZED_WORKTREE_ROOT}/xyz.js".into(),
request: DebugRequest::Launch(LaunchRequest { args: vec!["--foo".into(), "${ZED_WORKTREE_ROOT}/thing".into()],
program: "${ZED_WORKTREE_ROOT}/xyz.js".into(), cwd: Some("${ZED_WORKTREE_ROOT}/${FOO}/sub".into()),
args: vec!["--foo".into(), "${ZED_WORKTREE_ROOT}/thing".into()], env: FxHashMap::from_iter([("X".into(), "Y".into())])
cwd: Some("${ZED_WORKTREE_ROOT}/${FOO}/sub".into()), })),
}), build: None
}
}]) }])
); );
} }

View file

@ -10,10 +10,7 @@ use gpui::{
use itertools::Itertools; use itertools::Itertools;
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{TaskSourceKind, task_store::TaskStore}; use project::{TaskSourceKind, task_store::TaskStore};
use task::{ use task::{DebugScenario, ResolvedTask, RevealTarget, TaskContext, TaskModal, TaskTemplate};
DebugRequest, DebugTaskDefinition, ResolvedTask, RevealTarget, TaskContext, TaskModal,
TaskTemplate, TaskType,
};
use ui::{ use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon,
IconButton, IconButtonShape, IconName, IconSize, IntoElement, KeyBinding, Label, LabelSize, IconButton, IconButtonShape, IconName, IconSize, IntoElement, KeyBinding, Label, LabelSize,
@ -187,7 +184,7 @@ impl Render for TasksModal {
} }
pub struct ShowAttachModal { pub struct ShowAttachModal {
pub debug_config: DebugTaskDefinition, pub debug_config: DebugScenario,
} }
impl EventEmitter<DismissEvent> for TasksModal {} impl EventEmitter<DismissEvent> for TasksModal {}
@ -354,48 +351,20 @@ impl PickerDelegate for TasksModalDelegate {
reveal_target: Some(reveal_target), reveal_target: Some(reveal_target),
}) = &self.task_overrides }) = &self.task_overrides
{ {
if let Some(resolved_task) = &mut task.resolved { task.resolved.reveal_target = *reveal_target;
resolved_task.reveal_target = *reveal_target;
}
} }
match task.task_type() { self.workspace
TaskType::Debug(_) => { .update(cx, |workspace, cx| {
let Some(config) = task.resolved_debug_adapter_config() else { workspace.schedule_resolved_task(
return; task_source_kind,
}; task,
let config = config.definition; omit_history_entry,
window,
match &config.request { cx,
DebugRequest::Attach(attach_config) if attach_config.process_id.is_none() => { );
cx.emit(ShowAttachModal { })
debug_config: config.clone(), .ok();
});
return;
}
_ => {
self.workspace
.update(cx, |workspace, cx| {
workspace.schedule_debug_task(task, window, cx);
})
.ok();
}
}
}
TaskType::Script => {
self.workspace
.update(cx, |workspace, cx| {
workspace.schedule_resolved_task(
task_source_kind,
task,
omit_history_entry,
window,
cx,
);
})
.ok();
}
};
cx.emit(DismissEvent); cx.emit(DismissEvent);
} }
@ -422,16 +391,14 @@ impl PickerDelegate for TasksModalDelegate {
} else { } else {
String::new() String::new()
}; };
if let Some(resolved) = resolved_task.resolved.as_ref() {
if resolved.command_label != display_label if resolved_task.resolved.command_label != resolved_task.resolved_label {
&& resolved.command_label != resolved_task.resolved_label if !tooltip_label_text.trim().is_empty() {
{ tooltip_label_text.push('\n');
if !tooltip_label_text.trim().is_empty() {
tooltip_label_text.push('\n');
}
tooltip_label_text.push_str(&resolved.command_label);
} }
tooltip_label_text.push_str(&resolved_task.resolved.command_label);
} }
if template.tags.len() > 0 { if template.tags.len() > 0 {
tooltip_label_text.push('\n'); tooltip_label_text.push('\n');
tooltip_label_text.push_str( tooltip_label_text.push_str(
@ -553,7 +520,7 @@ impl PickerDelegate for TasksModalDelegate {
let task_index = self.matches.get(self.selected_index())?.candidate_id; let task_index = self.matches.get(self.selected_index())?.candidate_id;
let tasks = self.candidates.as_ref()?; let tasks = self.candidates.as_ref()?;
let (_, task) = tasks.get(task_index)?; let (_, task) = tasks.get(task_index)?;
Some(task.resolved.as_ref()?.command_label.clone()) Some(task.resolved.command_label.clone())
} }
fn confirm_input( fn confirm_input(
@ -570,26 +537,17 @@ impl PickerDelegate for TasksModalDelegate {
reveal_target: Some(reveal_target), reveal_target: Some(reveal_target),
}) = self.task_overrides }) = self.task_overrides
{ {
if let Some(resolved_task) = &mut task.resolved { task.resolved.reveal_target = reveal_target;
resolved_task.reveal_target = reveal_target;
}
} }
self.workspace self.workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
match task.task_type() { workspace.schedule_resolved_task(
TaskType::Script => workspace.schedule_resolved_task( task_source_kind,
task_source_kind, task,
task, omit_history_entry,
omit_history_entry, window,
window, cx,
cx, )
),
// 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.schedule_debug_task(task, window, cx);
}
};
}) })
.ok(); .ok();
cx.emit(DismissEvent); cx.emit(DismissEvent);
@ -716,10 +674,7 @@ fn string_match_candidates<'a>(
candidates candidates
.into_iter() .into_iter()
.enumerate() .enumerate()
.filter(|(_, (_, candidate))| match candidate.task_type() { .filter(|(_, (_, _))| task_modal_type == TaskModal::ScriptModal)
TaskType::Script => task_modal_type == TaskModal::ScriptModal,
TaskType::Debug(_) => task_modal_type == TaskModal::DebugModal,
})
.map(|(index, (_, candidate))| StringMatchCandidate::new(index, candidate.display_label())) .map(|(index, (_, candidate))| StringMatchCandidate::new(index, candidate.display_label()))
.collect() .collect()
} }

View file

@ -65,13 +65,13 @@ pub fn init(cx: &mut App) {
}) })
.detach() .detach()
} else { } else {
if let Some(resolved) = last_scheduled_task.resolved.as_mut() { let resolved = &mut last_scheduled_task.resolved;
if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
resolved.allow_concurrent_runs = allow_concurrent_runs; if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
} resolved.allow_concurrent_runs = allow_concurrent_runs;
if let Some(use_new_terminal) = action.use_new_terminal { }
resolved.use_new_terminal = use_new_terminal; if let Some(use_new_terminal) = action.use_new_terminal {
} resolved.use_new_terminal = use_new_terminal;
} }
workspace.schedule_resolved_task( workspace.schedule_resolved_task(

View file

@ -1,10 +1,11 @@
use std::process::ExitStatus; use std::process::ExitStatus;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use gpui::{Context, Task}; use gpui::{Context, Entity, Task};
use language::Buffer;
use project::TaskSourceKind; use project::TaskSourceKind;
use remote::ConnectionState; use remote::ConnectionState;
use task::{DebugTaskDefinition, ResolvedTask, SpawnInTerminal, TaskContext, TaskTemplate}; use task::{DebugScenario, ResolvedTask, SpawnInTerminal, TaskContext, TaskTemplate};
use ui::Window; use ui::Window;
use crate::Workspace; use crate::Workspace;
@ -48,84 +49,41 @@ impl Workspace {
pub fn schedule_resolved_task( pub fn schedule_resolved_task(
self: &mut Workspace, self: &mut Workspace,
task_source_kind: TaskSourceKind, task_source_kind: TaskSourceKind,
mut resolved_task: ResolvedTask, resolved_task: ResolvedTask,
omit_history: bool, omit_history: bool,
window: &mut Window, window: &mut Window,
cx: &mut Context<Workspace>, cx: &mut Context<Workspace>,
) { ) {
if let Some(spawn_in_terminal) = resolved_task.resolved.take() { let spawn_in_terminal = resolved_task.resolved.clone();
if !omit_history { if !omit_history {
resolved_task.resolved = Some(spawn_in_terminal.clone()); self.project().update(cx, |project, cx| {
self.project().update(cx, |project, cx| { if let Some(task_inventory) =
if let Some(task_inventory) = project.task_store().read(cx).task_inventory().cloned()
project.task_store().read(cx).task_inventory().cloned() {
{ task_inventory.update(cx, |inventory, _| {
task_inventory.update(cx, |inventory, _| { inventory.task_scheduled(task_source_kind, resolved_task);
inventory.task_scheduled(task_source_kind, resolved_task); })
})
}
});
}
if let Some(terminal_provider) = self.terminal_provider.as_ref() {
terminal_provider
.spawn(spawn_in_terminal, window, cx)
.detach_and_log_err(cx);
}
}
}
pub fn schedule_debug_task(
&mut self,
task: ResolvedTask,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let Some(debug_config) = task.resolved_debug_adapter_config() else {
log::error!("Debug task has no debug adapter config");
return;
};
let project = self.project().clone();
cx.spawn_in(window, async move |workspace, cx| {
let config = if debug_config.locator.is_some() {
let task = workspace.update_in(cx, |workspace, window, cx| {
workspace.spawn_in_terminal(task.resolved.unwrap(), window, cx)
})?;
let exit_code = task.await?;
if !exit_code.success() {
return anyhow::Ok(());
} }
let ret = project });
.update(cx, |project, cx| { }
project.dap_store().update(cx, |dap_store, cx| {
dap_store.run_debug_locator(debug_config, cx)
})
})?
.await?;
ret
} else {
debug_config.definition
};
workspace.update_in(cx, |workspace, window, cx| { if let Some(terminal_provider) = self.terminal_provider.as_ref() {
workspace.start_debug_session(config, window, cx); terminal_provider
})?; .spawn(spawn_in_terminal, window, cx)
.detach_and_log_err(cx);
anyhow::Ok(()) }
})
.detach_and_log_err(cx);
} }
pub fn start_debug_session( pub fn start_debug_session(
&mut self, &mut self,
definition: DebugTaskDefinition, scenario: DebugScenario,
task_context: TaskContext,
active_buffer: Option<Entity<Buffer>>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if let Some(provider) = self.debugger_provider.as_mut() { if let Some(provider) = self.debugger_provider.as_mut() {
provider.start_session(definition, window, cx) provider.start_session(scenario, task_context, active_buffer, window, cx)
} }
} }

View file

@ -49,7 +49,7 @@ pub use item::{
ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle, ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
}; };
use itertools::Itertools; use itertools::Itertools;
use language::{LanguageRegistry, Rope}; use language::{Buffer, LanguageRegistry, Rope};
pub use modal_layer::*; pub use modal_layer::*;
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use notifications::{ use notifications::{
@ -96,7 +96,7 @@ use std::{
sync::{Arc, LazyLock, Weak, atomic::AtomicUsize}, sync::{Arc, LazyLock, Weak, atomic::AtomicUsize},
time::Duration, time::Duration,
}; };
use task::{DebugTaskDefinition, SpawnInTerminal}; use task::{DebugScenario, SpawnInTerminal, TaskContext};
use theme::{ActiveTheme, SystemAppearance, ThemeSettings}; use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub use ui; pub use ui;
@ -140,7 +140,15 @@ pub trait TerminalProvider {
} }
pub trait DebuggerProvider { pub trait DebuggerProvider {
fn start_session(&self, definition: DebugTaskDefinition, window: &mut Window, cx: &mut App); // `active_buffer` is used to resolve build task's name against language-specific tasks.
fn start_session(
&self,
definition: DebugScenario,
task_context: TaskContext,
active_buffer: Option<Entity<Buffer>>,
window: &mut Window,
cx: &mut App,
);
} }
actions!( actions!(

View file

@ -4273,7 +4273,7 @@ mod tests {
project::debugger::breakpoint_store::BreakpointStore::init( project::debugger::breakpoint_store::BreakpointStore::init(
&app_state.client.clone().into(), &app_state.client.clone().into(),
); );
project::debugger::dap_store::DapStore::init(&app_state.client.clone().into()); project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx);
debugger_ui::init(cx); debugger_ui::init(cx);
initialize_workspace(app_state.clone(), prompt_builder, cx); initialize_workspace(app_state.clone(), prompt_builder, cx);
search::init(cx); search::init(cx);