From 67615b968b32e7f62698bc896549bab87c8c3b0b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 26 Apr 2025 01:44:56 +0200 Subject: [PATCH] debugger/tasks: Remove TaskType enum (#29208) Closes #ISSUE Release Notes: - N/A --------- Co-authored-by: Cole Miller Co-authored-by: Anthony Eid Co-authored-by: Conrad Irwin Co-authored-by: Anthony Co-authored-by: Conrad --- crates/collab/src/db.rs | 2 + .../src/db/tables/worktree_settings_file.rs | 2 + crates/collab/src/tests/editor_tests.rs | 1 + crates/collab/src/tests/integration_tests.rs | 2 + crates/dap/src/adapters.rs | 122 +++++++- crates/dap/src/dap.rs | 2 +- crates/dap/src/registry.rs | 25 ++ crates/dap_adapters/src/codelldb.rs | 9 +- crates/dap_adapters/src/gdb.rs | 4 +- crates/dap_adapters/src/go.rs | 3 +- crates/dap_adapters/src/javascript.rs | 4 +- crates/dap_adapters/src/php.rs | 3 +- crates/dap_adapters/src/python.rs | 6 +- crates/debugger_ui/src/attach_modal.rs | 23 +- crates/debugger_ui/src/debugger_panel.rs | 191 +++++++----- crates/debugger_ui/src/new_session_modal.rs | 97 +++--- crates/debugger_ui/src/session.rs | 8 +- crates/debugger_ui/src/tests.rs | 15 +- crates/debugger_ui/src/tests/attach_modal.rs | 9 +- crates/editor/src/actions.rs | 4 + crates/editor/src/code_context_menus.rs | 165 +++++----- crates/editor/src/editor.rs | 112 ++++--- crates/editor/src/mouse_context_menu.rs | 1 + crates/language/src/task_context.rs | 3 + crates/languages/src/go.rs | 4 + crates/languages/src/python.rs | 4 + crates/languages/src/rust.rs | 68 ++--- crates/project/src/debugger/dap_store.rs | 157 +++++++--- crates/project/src/debugger/locator_store.rs | 34 --- crates/project/src/debugger/locators.rs | 9 - crates/project/src/debugger/locators/cargo.rs | 57 ++-- crates/project/src/debugger/session.rs | 30 +- crates/project/src/project.rs | 4 +- crates/project/src/project_settings.rs | 93 +++--- crates/project/src/project_tests.rs | 7 +- crates/project/src/task_inventory.rs | 288 +++++++++++++----- crates/project/src/task_store.rs | 24 +- crates/proto/proto/debugger.proto | 33 +- crates/proto/proto/worktree.proto | 1 + crates/proto/proto/zed.proto | 4 +- crates/proto/src/proto.rs | 8 +- crates/remote_server/src/headless_project.rs | 2 +- crates/settings/src/settings.rs | 2 +- crates/settings/src/settings_store.rs | 36 +-- crates/task/src/debug_format.rs | 226 +++++--------- crates/task/src/lib.rs | 104 +++---- crates/task/src/task_template.rs | 57 +--- crates/task/src/vscode_debug_format.rs | 96 +++--- crates/tasks_ui/src/modal.rs | 105 ++----- crates/tasks_ui/src/tasks_ui.rs | 14 +- crates/workspace/src/tasks.rs | 90 ++---- crates/workspace/src/workspace.rs | 14 +- crates/zed/src/zed.rs | 2 +- 53 files changed, 1272 insertions(+), 1114 deletions(-) delete mode 100644 crates/project/src/debugger/locator_store.rs diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 764eec401e..04b6547692 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -800,6 +800,7 @@ impl LocalSettingsKind { proto::LocalSettingsKind::Settings => Self::Settings, proto::LocalSettingsKind::Tasks => Self::Tasks, proto::LocalSettingsKind::Editorconfig => Self::Editorconfig, + proto::LocalSettingsKind::Debug => Self::Debug, } } @@ -808,6 +809,7 @@ impl LocalSettingsKind { Self::Settings => proto::LocalSettingsKind::Settings, Self::Tasks => proto::LocalSettingsKind::Tasks, Self::Editorconfig => proto::LocalSettingsKind::Editorconfig, + Self::Debug => proto::LocalSettingsKind::Debug, } } } diff --git a/crates/collab/src/db/tables/worktree_settings_file.rs b/crates/collab/src/db/tables/worktree_settings_file.rs index 71f7b73fc1..bed2de55ef 100644 --- a/crates/collab/src/db/tables/worktree_settings_file.rs +++ b/crates/collab/src/db/tables/worktree_settings_file.rs @@ -32,4 +32,6 @@ pub enum LocalSettingsKind { Tasks, #[sea_orm(string_value = "editorconfig")] Editorconfig, + #[sea_orm(string_value = "debug")] + Debug, } diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index ec66e593d5..a6ebfcb41d 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -680,6 +680,7 @@ async fn test_collaborating_with_code_actions( editor.toggle_code_actions( &ToggleCodeActions { deployed_from_indicator: None, + quick_launch: false, }, window, cx, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 941bd84ff9..9655fcbcb8 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -1824,6 +1824,8 @@ async fn test_active_call_events( server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + executor.run_until_parked(); + let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 1b927d0051..1e380f97f7 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -14,10 +14,16 @@ use serde::{Deserialize, Serialize}; use settings::WorktreeId; use smol::{self, fs::File, lock::Mutex}; use std::{ - borrow::Borrow, collections::HashSet, ffi::OsStr, fmt::Debug, net::Ipv4Addr, ops::Deref, - path::PathBuf, sync::Arc, + borrow::Borrow, + 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; #[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, + /// Whether to tell the debug adapter to stop on entry + pub stop_on_entry: Option, + /// 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, +} + +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 { + 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)] pub struct DebugAdapterBinary { pub command: String, diff --git a/crates/dap/src/dap.rs b/crates/dap/src/dap.rs index 9673d9a19d..8e06396d6b 100644 --- a/crates/dap/src/dap.rs +++ b/crates/dap/src/dap.rs @@ -6,7 +6,7 @@ mod registry; pub mod transport; pub use dap_types::*; -pub use registry::DapRegistry; +pub use registry::{DapLocator, DapRegistry}; pub use task::DebugRequest; pub type ScopeId = u64; diff --git a/crates/dap/src/registry.rs b/crates/dap/src/registry.rs index 4e08c5cce8..4a733109ae 100644 --- a/crates/dap/src/registry.rs +++ b/crates/dap/src/registry.rs @@ -1,12 +1,25 @@ +use anyhow::Result; +use async_trait::async_trait; +use collections::FxHashMap; use gpui::{App, Global}; use parking_lot::RwLock; +use task::{DebugRequest, SpawnInTerminal}; use crate::adapters::{DebugAdapter, DebugAdapterName}; 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; +} + #[derive(Default)] struct DapRegistryState { adapters: BTreeMap>, + locators: FxHashMap>, } #[derive(Clone, Default)] @@ -35,6 +48,18 @@ impl DapRegistry { ); } + pub fn add_locator(&self, name: String, locator: Arc) { + 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> { + self.0.read().locators.clone() + } + pub fn adapter(&self, name: &str) -> Option> { self.0.read().adapters.get(name).cloned() } diff --git a/crates/dap_adapters/src/codelldb.rs b/crates/dap_adapters/src/codelldb.rs index e6e27b25b6..78a376c3c0 100644 --- a/crates/dap_adapters/src/codelldb.rs +++ b/crates/dap_adapters/src/codelldb.rs @@ -2,9 +2,9 @@ use std::{collections::HashMap, path::PathBuf, sync::OnceLock}; use anyhow::{Result, bail}; use async_trait::async_trait; -use dap::adapters::{InlineValueProvider, latest_github_release}; +use dap::adapters::{DebugTaskDefinition, InlineValueProvider, latest_github_release}; use gpui::AsyncApp; -use task::{DebugRequest, DebugTaskDefinition}; +use task::DebugRequest; use crate::*; @@ -25,7 +25,10 @@ impl CodeLldbDebugAdapter { }); let map = configuration.as_object_mut().unwrap(); // 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(); match &config.request { DebugRequest::Attach(attach) => { diff --git a/crates/dap_adapters/src/gdb.rs b/crates/dap_adapters/src/gdb.rs index 257af7f136..028dc56081 100644 --- a/crates/dap_adapters/src/gdb.rs +++ b/crates/dap_adapters/src/gdb.rs @@ -2,9 +2,9 @@ use std::{collections::HashMap, ffi::OsStr}; use anyhow::{Result, bail}; use async_trait::async_trait; -use dap::StartDebuggingRequestArguments; +use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; use gpui::AsyncApp; -use task::{DebugRequest, DebugTaskDefinition}; +use task::DebugRequest; use crate::*; diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index e440020e20..ea69047e6c 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -1,7 +1,6 @@ -use dap::StartDebuggingRequestArguments; +use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; use gpui::AsyncApp; use std::{collections::HashMap, ffi::OsStr, path::PathBuf}; -use task::DebugTaskDefinition; use crate::*; diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index a83768ffdb..825c3661ae 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -1,8 +1,8 @@ use adapters::latest_github_release; -use dap::StartDebuggingRequestArguments; +use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; use gpui::AsyncApp; use std::{collections::HashMap, path::PathBuf}; -use task::{DebugRequest, DebugTaskDefinition}; +use task::DebugRequest; use crate::*; diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 8dd84ce28c..2024dea0bf 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -1,8 +1,7 @@ use adapters::latest_github_release; -use dap::adapters::TcpArguments; +use dap::adapters::{DebugTaskDefinition, TcpArguments}; use gpui::AsyncApp; use std::{collections::HashMap, path::PathBuf}; -use task::DebugTaskDefinition; use crate::*; diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index cde4df9109..8ab088b8c6 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,8 +1,10 @@ use crate::*; -use dap::{StartDebuggingRequestArguments, adapters::InlineValueProvider}; +use dap::{ + DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition, + adapters::InlineValueProvider, +}; use gpui::AsyncApp; use std::{collections::HashMap, ffi::OsStr, path::PathBuf}; -use task::DebugTaskDefinition; #[derive(Default)] pub(crate) struct PythonDebugAdapter; diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index d5ccf6c9ba..1afe8ba275 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -1,4 +1,5 @@ use dap::DebugRequest; +use dap::adapters::DebugTaskDefinition; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render}; use gpui::{Subscription, WeakEntity}; @@ -24,20 +25,20 @@ pub(crate) struct AttachModalDelegate { selected_index: usize, matches: Vec, placeholder_text: Arc, + pub(crate) definition: DebugTaskDefinition, workspace: WeakEntity, - pub(crate) debug_config: task::DebugTaskDefinition, candidates: Arc<[Candidate]>, } impl AttachModalDelegate { fn new( workspace: Entity, - debug_config: task::DebugTaskDefinition, + definition: DebugTaskDefinition, candidates: Arc<[Candidate]>, ) -> Self { Self { workspace: workspace.downgrade(), - debug_config, + definition, candidates, selected_index: 0, matches: Vec::default(), @@ -53,8 +54,8 @@ pub struct AttachModal { impl AttachModal { pub fn new( + definition: DebugTaskDefinition, workspace: Entity, - debug_config: task::DebugTaskDefinition, modal: bool, window: &mut Window, cx: &mut Context, @@ -77,12 +78,12 @@ impl AttachModal { .collect(); processes.sort_by_key(|k| k.name.clone()); 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( workspace: Entity, - debug_config: task::DebugTaskDefinition, + definition: DebugTaskDefinition, processes: Arc<[Candidate]>, modal: bool, window: &mut Window, @@ -90,7 +91,7 @@ impl AttachModal { ) -> Self { let picker = cx.new(|cx| { Picker::uniform_list( - AttachModalDelegate::new(workspace, debug_config, processes), + AttachModalDelegate::new(workspace, definition, processes), window, cx, ) @@ -217,7 +218,7 @@ impl PickerDelegate for AttachModalDelegate { return cx.emit(DismissEvent); }; - match &mut self.debug_config.request { + match &mut self.definition.request { DebugRequest::Attach(config) => { 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 .workspace .update(cx, |workspace, cx| workspace.panel::(cx)) @@ -235,9 +237,10 @@ impl PickerDelegate for AttachModalDelegate { .flatten(); if let Some(panel) = panel { panel.update(cx, |panel, cx| { - panel.start_session(definition, window, cx); + panel.start_session(scenario, Default::default(), None, window, cx); }); } + cx.emit(DismissEvent); } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 5736d7ee69..4e37162e06 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -9,11 +9,12 @@ use crate::{new_session_modal::NewSessionModal, session::DebugSession}; use anyhow::{Result, anyhow}; use collections::HashMap; use command_palette_hooks::CommandPaletteFilter; -use dap::StartDebuggingRequestArguments; +use dap::DebugRequest; use dap::{ ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent, client::SessionId, debugger_settings::DebuggerSettings, }; +use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; use futures::{SinkExt as _, channel::mpsc}; use gpui::{ Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter, @@ -21,6 +22,7 @@ use gpui::{ actions, anchored, deferred, }; +use language::Buffer; use project::debugger::session::{Session, SessionStateEvent}; use project::{ Project, @@ -35,9 +37,7 @@ use settings::Settings; use std::any::TypeId; use std::path::Path; use std::sync::Arc; -use task::{ - DebugTaskDefinition, DebugTaskTemplate, HideStrategy, RevealStrategy, RevealTarget, TaskId, -}; +use task::{DebugScenario, HideStrategy, RevealStrategy, RevealTarget, TaskContext, TaskId}; use terminal_view::TerminalView; use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*}; use workspace::SplitDirection; @@ -87,45 +87,8 @@ impl DebugPanel { let project = workspace.project().clone(); let dap_store = project.read(cx).dap_store(); - let weak = cx.weak_entity(); - - let modal_subscription = - cx.observe_new::(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 _subscriptions = + vec![cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event)]; let debug_panel = Self { size: px(300.), @@ -259,43 +222,16 @@ impl DebugPanel { }) } - pub fn start_session( + fn start_from_definition( &mut self, definition: DebugTaskDefinition, window: &mut Window, cx: &mut Context, - ) { - 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(); - + ) -> Task> { 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() - }; - + let dap_store = this.update(cx, |this, cx| this.project.read(cx).dap_store())?; let (session, task) = dap_store.update(cx, |dap_store, cx| { - let template = DebugTaskTemplate { - 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) - }; + let session = dap_store.new_session(definition, None, cx); (session.clone(), dap_store.boot_session(session, cx)) })?; @@ -318,6 +254,27 @@ impl DebugPanel { anyhow::Ok(()) }) + } + + pub fn start_session( + &mut self, + scenario: DebugScenario, + task_context: TaskContext, + active_buffer: Option>, + window: &mut Window, + cx: &mut Context, + ) { + 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); } @@ -343,13 +300,13 @@ impl DebugPanel { let definition = curr_session.update(cx, |session, _| session.definition()); let task = curr_session.update(cx, |session, cx| session.shutdown(cx)); - let definition = definition.clone(); cx.spawn_in(window, async move |this, cx| { task.await; 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); } @@ -503,6 +460,75 @@ impl DebugPanel { } } + pub fn resolve_scenario( + &self, + scenario: DebugScenario, + + task_context: TaskContext, + buffer: Option>, + window: &Window, + cx: &mut Context, + ) -> Task> { + 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( &self, session_id: SessionId, @@ -1409,10 +1435,17 @@ impl Render for DebugPanel { struct DebuggerProvider(Entity); 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>, + window: &mut Window, + cx: &mut App, + ) { self.0.update(cx, |_, cx| { cx.defer_in(window, |this, window, cx| { - this.start_session(definition, window, cx); + this.start_session(definition, context, buffer, window, cx); }) }) } diff --git a/crates/debugger_ui/src/new_session_modal.rs b/crates/debugger_ui/src/new_session_modal.rs index dc473bead5..516ca7edfa 100644 --- a/crates/debugger_ui/src/new_session_modal.rs +++ b/crates/debugger_ui/src/new_session_modal.rs @@ -4,14 +4,14 @@ use std::{ path::{Path, PathBuf}, }; -use dap::{DapRegistry, DebugRequest}; +use dap::{DapRegistry, DebugRequest, adapters::DebugTaskDefinition}; use editor::{Editor, EditorElement, EditorStyle}; use gpui::{ App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle, WeakEntity, }; use settings::Settings; -use task::{DebugTaskDefinition, DebugTaskTemplate, LaunchRequest}; +use task::{DebugScenario, LaunchRequest, TaskContext}; use theme::ThemeSettings; use ui::{ ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context, @@ -34,7 +34,7 @@ pub(super) struct NewSessionModal { last_selected_profile_name: Option, } -fn suggested_label(request: &DebugRequest, debugger: &str) -> String { +fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString { match request { DebugRequest::Launch(config) => { 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()) .unwrap_or_else(|| Cow::Borrowed(&config.program)); - format!("{} ({debugger})", last_path_component) + format!("{} ({debugger})", last_path_component).into() } DebugRequest::Attach(config) => format!( "pid: {} ({debugger})", config.process_id.unwrap_or(u32::MAX) - ), + ) + .into(), } } @@ -61,7 +62,7 @@ impl NewSessionModal { ) -> Self { let debugger = past_debug_definition .as_ref() - .map(|def| def.adapter.clone().into()); + .map(|def| def.adapter.clone()); let stop_on_entry = past_debug_definition .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); - DebugTaskDefinition { - adapter: debugger.to_owned(), - label: suggested_label(&request, debugger), - request, + let label = suggested_label(&request, debugger); + DebugScenario { + adapter: debugger.to_owned().into(), + label, + request: Some(request), initialize_args: self.initialize_args.clone(), tcp_connection: None, stop_on_entry: match self.stop_on_entry { ToggleState::Selected => Some(true), _ => None, }, + build: None, } } @@ -109,36 +112,9 @@ impl NewSessionModal { let config = self.debug_config(cx, debugger); 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| { - 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| { - let template = DebugTaskTemplate { - 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) - } + debug_panel.start_session(config, TaskContext::default(), None, window, cx) })?; this.update(cx, |_, cx| { cx.emit(DismissEvent); @@ -156,12 +132,13 @@ impl NewSessionModal { cx: &mut App, ) { attach.update(cx, |this, cx| { - if selected_debugger != this.debug_definition.adapter { - this.debug_definition.adapter = selected_debugger.into(); + if selected_debugger != this.definition.adapter.as_ref() { + let adapter: SharedString = selected_debugger.to_owned().into(); + this.definition.adapter = adapter.clone(); this.attach_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); }) }); @@ -224,22 +201,22 @@ impl NewSessionModal { "debug-config-menu", last_profile.unwrap_or_else(|| SELECT_SCENARIO_LABEL.clone()), ContextMenu::build(window, cx, move |mut menu, _, cx| { - let setter_for_name = |task: DebugTaskDefinition| { + let setter_for_name = |task: DebugScenario| { let weak = weak.clone(); move |window: &mut Window, cx: &mut App| { weak.update(cx, |this, cx| { 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(); match &task.request { - DebugRequest::Launch(launch_config) => { + Some(DebugRequest::Launch(launch_config)) => { this.mode = NewSessionMode::launch( Some(launch_config.clone()), window, cx, ); } - DebugRequest::Attach(_) => { + Some(DebugRequest::Attach(_)) => { let Some(workspace) = this.workspace.upgrade() else { return; }; @@ -256,6 +233,7 @@ impl NewSessionModal { Self::update_attach_picker(&attach, &debugger, window, cx); } } + _ => log::warn!("Selected debug scenario without either attach or launch request specified"), } cx.notify(); }) @@ -263,7 +241,7 @@ impl NewSessionModal { } }; - let available_adapters: Vec = workspace + let available_tasks: Vec = workspace .update(cx, |this, cx| { this.project() .read(cx) @@ -271,19 +249,19 @@ impl NewSessionModal { .read(cx) .task_inventory() .iter() - .flat_map(|task_inventory| task_inventory.read(cx).list_debug_tasks()) - .cloned() - .filter_map(|task| task.try_into().ok()) + .flat_map(|task_inventory| { + task_inventory.read(cx).list_debug_scenarios(None) + }) .collect() }) .ok() .unwrap_or_default(); - for debug_definition in available_adapters { + for debug_definition in available_tasks { menu = menu.entry( - debug_definition.definition.label.clone(), + debug_definition.label.clone(), None, - setter_for_name(debug_definition.definition), + setter_for_name(debug_definition), ); } menu @@ -332,13 +310,14 @@ impl LaunchMode { program: self.program.read(cx).text(cx), cwd: path.is_empty().not().then(|| PathBuf::from(path)), args: Default::default(), + env: Default::default(), } } } #[derive(Clone)] struct AttachMode { - debug_definition: DebugTaskDefinition, + definition: DebugTaskDefinition, attach_picker: Entity, } @@ -349,22 +328,22 @@ impl AttachMode { window: &mut Window, cx: &mut Context, ) -> Entity { - let debug_definition = DebugTaskDefinition { + let definition = DebugTaskDefinition { + adapter: debugger.clone().unwrap_or_default(), label: "Attach New Session Setup".into(), request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }), - tcp_connection: None, - adapter: debugger.clone().unwrap_or_default().into(), initialize_args: None, + tcp_connection: None, stop_on_entry: Some(false), }; 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)); modal }); cx.new(|_| Self { - debug_definition, + definition, attach_picker, }) } diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index ee70559ad7..974f205fa6 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -33,7 +33,7 @@ impl DebugSessionState { pub struct DebugSession { remote_id: Option, mode: DebugSessionState, - label: OnceLock, + label: OnceLock, dap_store: WeakEntity, _debug_panel: WeakEntity, _worktree_store: WeakEntity, @@ -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() { - return label.to_owned(); + return label.clone(); } let session_id = match &self.mode { @@ -123,7 +123,7 @@ impl DebugSession { .dap_store .read_with(cx, |store, _| store.session_by_id(session_id)) else { - return "".to_owned(); + return "".into(); }; self.label diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index 0305f09aad..27b43b1eee 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -1,11 +1,12 @@ use std::sync::Arc; use anyhow::{Result, anyhow}; +use dap::adapters::DebugTaskDefinition; use dap::{DebugRequest, client::DebugAdapterClient}; use gpui::{Entity, TestAppContext, WindowHandle}; use project::{Project, debugger::session::Session}; use settings::SettingsStore; -use task::DebugTaskDefinition; +use task::TaskContext; use terminal_view::terminal_panel::TerminalPanel; use workspace::Workspace; @@ -104,7 +105,13 @@ pub fn start_debug_session_with) + 'static>( ) -> Result> { let _subscription = project::debugger::test::intercept_debug_sessions(cx, configure); 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(); let session = workspace.read_with(cx, |workspace, cx| { @@ -128,9 +135,9 @@ pub fn start_debug_session) + 'static>( workspace, cx, DebugTaskDefinition { - adapter: "fake-adapter".to_string(), + adapter: "fake-adapter".into(), request: DebugRequest::Launch(Default::default()), - label: "test".to_string(), + label: "test".into(), initialize_args: None, tcp_connection: None, stop_on_entry: None, diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index bdcf98a5d9..1daabf0d44 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -1,11 +1,11 @@ use crate::{attach_modal::Candidate, tests::start_debug_session_with, *}; use attach_modal::AttachModal; -use dap::FakeAdapter; +use dap::{FakeAdapter, adapters::DebugTaskDefinition}; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use menu::Confirm; use project::{FakeFs, Project}; use serde_json::json; -use task::{AttachRequest, DebugTaskDefinition, TcpArgumentsTemplate}; +use task::{AttachRequest, TcpArgumentsTemplate}; use tests::{init_test, init_test_workspace}; #[gpui::test] @@ -30,11 +30,11 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te &workspace, cx, DebugTaskDefinition { - adapter: "fake-adapter".to_string(), + adapter: "fake-adapter".into(), request: dap::DebugRequest::Attach(AttachRequest { process_id: Some(10), }), - label: "label".to_string(), + label: "label".into(), initialize_args: None, tcp_connection: None, stop_on_entry: None, @@ -104,6 +104,7 @@ async fn test_show_attach_modal_and_select_process( workspace_handle, DebugTaskDefinition { adapter: FakeAdapter::ADAPTER_NAME.into(), + request: dap::DebugRequest::Attach(AttachRequest::default()), label: "attach example".into(), initialize_args: None, diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index d79461641d..d1f712b62e 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -78,6 +78,10 @@ pub struct ToggleCodeActions { #[serde(default)] #[serde(skip)] pub deployed_from_indicator: Option, + // Run first available task if there is only one. + #[serde(default)] + #[serde(skip)] + pub quick_launch: bool, } #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 37e1c9c7cd..e47d783638 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1,4 +1,3 @@ -use feature_flags::{DebuggerFeatureFlag, FeatureFlagAppExt as _}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ AnyElement, BackgroundExecutor, Entity, Focusable, FontWeight, ListSizingBehavior, @@ -13,6 +12,8 @@ use ordered_float::OrderedFloat; use project::CompletionSource; use project::lsp_store::CompletionDocumentation; use project::{CodeAction, Completion, TaskSourceKind}; +use task::DebugScenario; +use task::TaskContext; use std::{ 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_MAX_WIDTH: Pixels = px(500.); +#[allow(clippy::large_enum_variant)] pub enum CodeContextMenu { Completions(CompletionsMenu), CodeActions(CodeActionsMenu), @@ -819,28 +821,25 @@ pub struct AvailableCodeAction { } #[derive(Clone)] -pub struct CodeActionContents { +pub(crate) struct CodeActionContents { tasks: Option>, actions: Option>, + debug_scenarios: Vec, + pub(crate) context: TaskContext, } impl CodeActionContents { - pub fn new( - mut tasks: Option, + pub(crate) fn new( + tasks: Option, actions: Option>, - cx: &App, + debug_scenarios: Vec, + context: TaskContext, ) -> Self { - if !cx.has_flag::() { - if let Some(tasks) = &mut tasks { - tasks - .templates - .retain(|(_, task)| !matches!(task.task_type(), task::TaskType::Debug(_))); - } - } - Self { tasks: tasks.map(Rc::new), actions, + debug_scenarios, + context, } } @@ -849,21 +848,13 @@ impl CodeActionContents { } fn len(&self) -> usize { - match (&self.tasks, &self.actions) { - (Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(), - (Some(tasks), None) => tasks.templates.len(), - (None, Some(actions)) => actions.len(), - (None, None) => 0, - } + let tasks_len = self.tasks.as_ref().map_or(0, |tasks| tasks.templates.len()); + let code_actions_len = self.actions.as_ref().map_or(0, |actions| actions.len()); + tasks_len + code_actions_len + self.debug_scenarios.len() } fn is_empty(&self) -> bool { - match (&self.tasks, &self.actions) { - (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, - } + self.len() == 0 } fn iter(&self) -> impl Iterator + '_ { @@ -882,43 +873,38 @@ impl CodeActionContents { provider: available.provider.clone(), }) })) + .chain( + self.debug_scenarios + .iter() + .cloned() + .map(CodeActionsItem::DebugScenario), + ) } - pub fn get(&self, index: usize) -> Option { - match (&self.tasks, &self.actions) { - (Some(tasks), Some(actions)) => { - if index < tasks.templates.len() { - tasks - .templates - .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(), - } - }) - } + pub fn get(&self, mut index: usize) -> Option { + if let Some(tasks) = &self.tasks { + if let Some((kind, task)) = tasks.templates.get(index) { + return Some(CodeActionsItem::Task(kind.clone(), task.clone())); + } else { + index -= tasks.templates.len(); } - (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, provider: Rc, }, + DebugScenario(DebugScenario), } impl CodeActionsItem { @@ -947,16 +934,23 @@ impl CodeActionsItem { }; Some(action) } + fn as_debug_scenario(&self) -> Option<&DebugScenario> { + let Self::DebugScenario(scenario) = self else { + return None; + }; + Some(scenario) + } pub fn label(&self) -> String { match self { Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(), 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 buffer: Entity, pub selected_item: usize, @@ -1065,19 +1059,7 @@ impl CodeActionsMenu { .inset(true) .toggle_state(selected) .when_some(action.as_code_action(), |this, action| { - this.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) - } - })) - .child( + this.child( h_flex() .overflow_hidden() .child( @@ -1090,19 +1072,7 @@ impl CodeActionsMenu { ) }) .when_some(action.as_task(), |this, task| { - this.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) - } - })) - .child( + this.child( h_flex() .overflow_hidden() .child(task.resolved_label.replace("\n", "")) @@ -1110,7 +1080,29 @@ impl CodeActionsMenu { 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() @@ -1128,6 +1120,7 @@ impl CodeActionsMenu { CodeActionsItem::CodeAction { action, .. } => { action.lsp_action.title().chars().count() } + CodeActionsItem::DebugScenario(scenario) => scenario.label.chars().count(), }) .map(|(ix, _)| ix), ) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 74dfe1e3fa..8f43eeab61 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5089,6 +5089,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { + let quick_launch = action.quick_launch; let mut context_menu = self.context_menu.borrow_mut(); if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() { 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) }); - let debugger_flag = cx.has_flag::(); - Some(cx.spawn_in(window, async move |editor, cx| { let task_context = match task_context { Some(task_context) => task_context.await, @@ -5171,7 +5170,7 @@ impl Editor { }; let resolved_tasks = tasks - .zip(task_context) + .zip(task_context.clone()) .map(|(tasks, task_context)| ResolvedTasks { templates: tasks.resolve(&task_context).collect(), position: snapshot.buffer_snapshot.anchor_before(Point::new( @@ -5179,22 +5178,49 @@ impl Editor { tasks.column, )), }); - let spawn_straight_away = resolved_tasks.as_ref().map_or(false, |tasks| { - tasks - .templates - .iter() - .filter(|task| { - if matches!(task.1.task_type(), task::TaskType::Debug(_)) { - debugger_flag - } else { - true - } + let spawn_straight_away = quick_launch + && resolved_tasks + .as_ref() + .map_or(false, |tasks| tasks.templates.len() == 1) + && code_actions + .as_ref() + .map_or(true, |actions| actions.is_empty()); + let debug_scenarios = editor.update(cx, |editor, cx| { + if cx.has_flag::() { + 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() - == 1 - }) && code_actions - .as_ref() - .map_or(true, |actions| actions.is_empty()); + .unwrap_or_default() + } else { + vec![] + } + })?; if let Ok(task) = editor.update_in(cx, |editor, window, cx| { *editor.context_menu.borrow_mut() = Some(CodeContextMenu::CodeActions(CodeActionsMenu { @@ -5202,7 +5228,8 @@ impl Editor { actions: CodeActionContents::new( resolved_tasks, code_actions, - cx, + debug_scenarios, + task_context.unwrap_or_default(), ), selected_item: Default::default(), scroll_handle: UniformListScrollHandle::default(), @@ -5262,25 +5289,17 @@ impl Editor { match action { CodeActionsItem::Task(task_source_kind, resolved_task) => { - match resolved_task.task_type() { - task::TaskType::Script => workspace.update(cx, |workspace, cx| { - workspace.schedule_resolved_task( - task_source_kind, - resolved_task, - false, - window, - cx, - ); + workspace.update(cx, |workspace, cx| { + workspace.schedule_resolved_task( + task_source_kind, + resolved_task, + false, + window, + cx, + ); - Some(Task::ready(Ok(()))) - }), - task::TaskType::Debug(_) => { - workspace.update(cx, |workspace, cx| { - workspace.schedule_debug_task(resolved_task, window, cx); - }); - Some(Task::ready(Ok(()))) - } - } + Some(Task::ready(Ok(()))) + }) } CodeActionsItem::CodeAction { excerpt_id, @@ -5302,6 +5321,14 @@ impl Editor { .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", &ToggleCodeActions { deployed_from_indicator: None, + quick_launch: false, }, &focus_handle, 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)); editor.toggle_code_actions( &ToggleCodeActions { deployed_from_indicator: Some(row), + quick_launch, }, window, cx, @@ -7050,7 +7080,7 @@ impl Editor { let context = task_context.await?; 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; workspace @@ -7140,11 +7170,13 @@ impl Editor { .icon_size(IconSize::XSmall) .icon_color(color) .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)); editor.toggle_code_actions( &ToggleCodeActions { deployed_from_indicator: Some(row), + quick_launch, }, window, cx, diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 9b0017b7ce..a924be0545 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -211,6 +211,7 @@ pub fn deploy_context_menu( "Show Code Actions", Box::new(ToggleCodeActions { deployed_from_indicator: None, + quick_launch: false, }), ) .separator() diff --git a/crates/language/src/task_context.rs b/crates/language/src/task_context.rs index 37913884e6..c3faf10b81 100644 --- a/crates/language/src/task_context.rs +++ b/crates/language/src/task_context.rs @@ -47,4 +47,7 @@ pub trait ContextProvider: Send + Sync { fn lsp_task_source(&self) -> Option { None } + + /// Default debug adapter for a given language. + fn debug_adapter(&self) -> Option; } diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 72051feab2..3156685b33 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -630,6 +630,10 @@ impl ContextProvider for GoContextProvider { }, ])) } + + fn debug_adapter(&self) -> Option { + Some("Delve".into()) + } } fn extract_subtest_name(input: &str) -> Option { diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 28c82668af..bc822aa9a0 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -503,6 +503,10 @@ impl ContextProvider for PythonContextProvider { Some(TaskTemplates(tasks)) } + + fn debug_adapter(&self) -> Option { + Some("Debugpy".into()) + } } fn selected_test_runner(location: Option<&Arc>, cx: &App) -> TestRunner { diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 2188dfc9ef..b1769023eb 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -20,7 +20,7 @@ use std::{ path::{Path, PathBuf}, sync::{Arc, LazyLock}, }; -use task::{TaskTemplate, TaskTemplates, TaskType, TaskVariables, VariableName}; +use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName}; use util::merge_json_value_into; use util::{ResultExt, fs::remove_matching, maybe}; @@ -629,7 +629,7 @@ impl ContextProvider for RustContextProvider { } else { 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] } else { vec!["build".into()] @@ -675,32 +675,6 @@ impl ContextProvider for RustContextProvider { cwd: Some("$ZED_DIRNAME".to_owned()), ..TaskTemplate::default() }, - TaskTemplate { - label: format!( - "Debug Test '{}' (package: {})", - RUST_TEST_NAME_TASK_VARIABLE.template_value(), - RUST_PACKAGE_TASK_VARIABLE.template_value(), - ), - task_type: TaskType::Debug(task::DebugArgs { - adapter: "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 { label: format!( "Doc test '{}' (package: {})", @@ -780,31 +754,41 @@ impl ContextProvider for RustContextProvider { cwd: Some("$ZED_DIRNAME".to_owned()), ..TaskTemplate::default() }, + TaskTemplate { + label: "Clean".into(), + command: "cargo".into(), + args: vec!["clean".into()], + cwd: Some("$ZED_DIRNAME".to_owned()), + ..TaskTemplate::default() + }, TaskTemplate { label: format!( - "Debug {} {} (package: {})", + "Build {} {} (package: {})", RUST_BIN_KIND_TASK_VARIABLE.template_value(), RUST_BIN_NAME_TASK_VARIABLE.template_value(), RUST_PACKAGE_TASK_VARIABLE.template_value(), ), cwd: Some("$ZED_DIRNAME".to_owned()), command: "cargo".into(), - task_type: TaskType::Debug(task::DebugArgs { - 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, + args: build_task_args, tags: vec!["rust-main".to_owned()], ..TaskTemplate::default() }, 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(), - 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()), ..TaskTemplate::default() }, @@ -832,6 +816,10 @@ impl ContextProvider for RustContextProvider { fn lsp_task_source(&self) -> Option { Some(SERVER_NAME) } + + fn debug_adapter(&self) -> Option { + Some("CodeLLDB".to_owned()) + } } /// Part of the data structure of Cargo metadata diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 860892516f..b19445e85d 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -1,6 +1,6 @@ use super::{ breakpoint_store::BreakpointStore, - locators::DapLocator, + locators, session::{self, Session, SessionStateEvent}, }; use crate::{ @@ -13,10 +13,12 @@ use anyhow::{Result, anyhow}; use async_trait::async_trait; use collections::HashMap; use dap::{ - Capabilities, CompletionItem, CompletionsArguments, DapRegistry, EvaluateArguments, - EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments, Source, - StackFrameId, StartDebuggingRequestArguments, - adapters::{DapStatus, DebugAdapterBinary, DebugAdapterName, TcpArguments}, + Capabilities, CompletionItem, CompletionsArguments, DapRegistry, DebugRequest, + EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments, + Source, StackFrameId, StartDebuggingRequestArguments, + adapters::{ + DapStatus, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments, + }, client::SessionId, messages::Message, requests::{Completions, Evaluate, Request as _, RunInTerminal, StartDebugging}, @@ -49,9 +51,9 @@ use std::{ ffi::OsStr, net::Ipv4Addr, path::{Path, PathBuf}, - sync::Arc, + sync::{Arc, Once}, }; -use task::{DebugTaskDefinition, DebugTaskTemplate}; +use task::{DebugScenario, SpawnInTerminal}; use util::ResultExt as _; use worktree::Worktree; @@ -95,7 +97,6 @@ pub struct LocalDapStore { environment: Entity, language_registry: Arc, toolchain_store: Arc, - locators: HashMap>, } pub struct SshDapStore { @@ -118,9 +119,14 @@ pub struct DapStore { impl EventEmitter for 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_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)] @@ -135,11 +141,6 @@ impl DapStore { breakpoint_store: Entity, cx: &mut Context, ) -> Self { - let locators = HashMap::from_iter([( - "cargo".to_string(), - Arc::new(super::locators::cargo::CargoLocator {}) as _, - )]); - let mode = DapStoreMode::Local(LocalDapStore { fs, environment, @@ -147,7 +148,6 @@ impl DapStore { node_runtime, toolchain_store, language_registry, - locators, }); Self::new(mode, breakpoint_store, worktree_store, cx) @@ -273,7 +273,7 @@ impl DapStore { DapStoreMode::Ssh(ssh) => { let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary { project_id: ssh.upstream_project_id, - task: Some(definition.to_proto()), + definition: Some(definition.to_proto()), }); 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 { + 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( &mut self, - template: DebugTaskTemplate, + mut build_command: SpawnInTerminal, cx: &mut Context, - ) -> Task> { - let Some(locator_name) = template.locator else { - return Task::ready(Ok(template.definition)); - }; - + ) -> Task> { match &self.mode { - DapStoreMode::Local(local) => { - if let Some(locator) = local.locators.get(&locator_name).cloned() { - cx.background_spawn( - async move { locator.run_locator(template.definition).await }, - ) + DapStoreMode::Local(_) => { + // Pre-resolve args with existing environment. + build_command.args = build_command + .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::>(); + 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 { - 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) => { - let request = ssh.upstream_client.request(proto::RunDebugLocator { + let request = ssh.upstream_client.request(proto::RunDebugLocators { project_id: ssh.upstream_project_id, - locator: locator_name, - task: Some(template.definition.to_proto()), + build_command: Some(build_command.to_proto()), }); cx.background_spawn(async move { let response = request.await?; - DebugTaskDefinition::from_proto(response) + DebugRequest::from_proto(response) }) } DapStoreMode::Collab => { @@ -943,22 +1009,19 @@ impl DapStore { async fn handle_run_debug_locator( this: Entity, - envelope: TypedEnvelope, + envelope: TypedEnvelope, mut cx: AsyncApp, - ) -> Result { - let template = DebugTaskTemplate { - locator: Some(envelope.payload.locator), - definition: DebugTaskDefinition::from_proto( - envelope - .payload - .task - .ok_or_else(|| anyhow!("missing definition"))?, - )?, - }; - let definition = this - .update(&mut cx, |this, cx| this.run_debug_locator(template, cx))? + ) -> Result { + let task = envelope + .payload + .build_command + .ok_or_else(|| anyhow!("missing definition"))?; + let build_task = SpawnInTerminal::from_proto(task); + let request = this + .update(&mut cx, |this, cx| this.run_debug_locator(build_task, cx))? .await?; - Ok(definition.to_proto()) + + Ok(request.to_proto()) } async fn handle_get_debug_adapter_binary( @@ -969,7 +1032,7 @@ impl DapStore { let definition = DebugTaskDefinition::from_proto( envelope .payload - .task + .definition .ok_or_else(|| anyhow!("missing definition"))?, )?; let binary = this diff --git a/crates/project/src/debugger/locator_store.rs b/crates/project/src/debugger/locator_store.rs deleted file mode 100644 index d542537833..0000000000 --- a/crates/project/src/debugger/locator_store.rs +++ /dev/null @@ -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>, -} - -impl LocatorStore { - pub(super) fn new() -> Self { - Self { locators } - } - - pub(super) async fn resolve_debug_config( - &self, - template: DebugTaskTemplate, - ) -> Result { - 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)) - } - } -} diff --git a/crates/project/src/debugger/locators.rs b/crates/project/src/debugger/locators.rs index f8946f12df..a0108cf57b 100644 --- a/crates/project/src/debugger/locators.rs +++ b/crates/project/src/debugger/locators.rs @@ -1,10 +1 @@ -use anyhow::Result; -use async_trait::async_trait; -use task::DebugTaskDefinition; - pub(crate) mod cargo; - -#[async_trait] -pub(super) trait DapLocator: Send + Sync { - async fn run_locator(&self, debug_config: DebugTaskDefinition) -> Result; -} diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index d8de869db7..cd665562c5 100644 --- a/crates/project/src/debugger/locators/cargo.rs +++ b/crates/project/src/debugger/locators/cargo.rs @@ -1,12 +1,12 @@ -use super::DapLocator; use anyhow::{Result, anyhow}; use async_trait::async_trait; +use dap::{DapLocator, DebugRequest}; use serde_json::Value; use smol::{ io::AsyncReadExt, process::{Command, Stdio}, }; -use task::DebugTaskDefinition; +use task::SpawnInTerminal; pub(crate) struct CargoLocator; @@ -37,26 +37,31 @@ async fn find_best_executable(executables: &[String], test_name: &str) -> Option } #[async_trait] impl DapLocator for CargoLocator { - async fn run_locator( - &self, - mut debug_config: DebugTaskDefinition, - ) -> Result { - let Some(launch_config) = (match &mut debug_config.request { - task::DebugRequest::Launch(launch_config) => Some(launch_config), - _ => None, - }) else { - return Err(anyhow!("Couldn't get launch config in locator")); + fn accepts(&self, build_config: &SpawnInTerminal) -> bool { + if build_config.command != "cargo" { + return false; + } + let Some(command) = build_config.args.first().map(|s| s.as_str()) else { + return false; }; + 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 { + let Some(cwd) = build_config.cwd.clone() else { return Err(anyhow!( "Couldn't get cwd from debug config which is needed for locators" )); }; let mut child = Command::new("cargo") - .args(&launch_config.args) + .args(&build_config.args) .arg("--message-format=json") + .envs(build_config.env.iter().map(|(k, v)| (k.clone(), v.clone()))) .current_dir(cwd) .stdout(Stdio::piped()) .spawn()?; @@ -85,19 +90,16 @@ impl DapLocator for CargoLocator { return Err(anyhow!("Couldn't get executable in cargo locator")); }; - let is_test = launch_config - .args - .first() - .map_or(false, |arg| arg == "test"); + let is_test = build_config.args.first().map_or(false, |arg| arg == "test"); let mut test_name = None; if is_test { - if let Some(package_index) = launch_config + if let Some(package_index) = build_config .args .iter() .position(|arg| arg == "-p" || arg == "--package") { - test_name = launch_config + test_name = build_config .args .get(package_index + 2) .filter(|name| !name.starts_with("--")) @@ -116,12 +118,17 @@ impl DapLocator for CargoLocator { 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(); - if let Some(test_name) = test_name { - launch_config.args.push(test_name); - } - Ok(debug_config) + Ok(DebugRequest::Launch(task::LaunchRequest { + program: executable, + cwd: build_config.cwd.clone(), + args, + env: build_config + .env + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + })) } } diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index ac6f0ad2ec..cd6345e6f0 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -12,7 +12,7 @@ use super::dap_command::{ use super::dap_store::DapStore; use anyhow::{Context as _, Result, anyhow}; use collections::{HashMap, HashSet, IndexMap, IndexSet}; -use dap::adapters::DebugAdapterBinary; +use dap::adapters::{DebugAdapterBinary, DebugTaskDefinition}; use dap::messages::Response; use dap::{ Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, StackFrameId, @@ -42,7 +42,6 @@ use std::{ path::Path, sync::Arc, }; -use task::DebugTaskDefinition; use text::{PointUtf16, ToPointUtf16}; use util::{ResultExt, merge_json_value_into}; use worktree::Worktree; @@ -125,7 +124,6 @@ enum Mode { pub struct LocalMode { client: Arc, binary: DebugAdapterBinary, - root_binary: Option>, pub(crate) breakpoint_store: Entity, tmp_breakpoint: Option, worktree: WeakEntity, @@ -160,12 +158,6 @@ impl LocalMode { 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( if let Some(client) = parent_session .and_then(|session| cx.update(|cx| session.read(cx).adapter_client()).ok()) @@ -186,7 +178,6 @@ impl LocalMode { breakpoint_store, worktree, tmp_breakpoint: None, - root_binary, binary, }) } @@ -834,19 +825,6 @@ impl Session { &self.capabilities } - pub(crate) fn root_binary(&self) -> Arc { - 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 { let Mode::Running(local_mode) = &self.mode else { panic!("Session is not local"); @@ -855,10 +833,10 @@ impl Session { } 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() } @@ -889,7 +867,7 @@ impl Session { } pub(super) fn request_initialize(&mut self, cx: &mut Context) -> Task> { - let adapter_id = self.definition.adapter.clone(); + let adapter_id = String::from(self.definition.adapter.clone()); let request = Initialize { adapter_id }; match &self.mode { Mode::Running(local_mode) => { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1d950ed651..314634c63a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -826,7 +826,7 @@ impl Project { SettingsObserver::init(&client); TaskStore::init(Some(&client)); ToolchainStore::init(&client); - DapStore::init(&client); + DapStore::init(&client, cx); BreakpointStore::init(&client); } @@ -1159,7 +1159,7 @@ impl Project { SettingsObserver::init(&ssh_proto); TaskStore::init(Some(&ssh_proto)); ToolchainStore::init(&ssh_proto); - DapStore::init(&ssh_proto); + DapStore::init(&ssh_proto, cx); GitStore::init(&ssh_proto); this diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 7f95a53eb5..857293262f 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -8,7 +8,7 @@ use lsp::LanguageServerName; use paths::{ EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_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::{ AnyProtoClient, TypedEnvelope, @@ -18,7 +18,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{ InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources, - SettingsStore, TaskKind, parse_json_with_comments, watch_config_file, + SettingsStore, parse_json_with_comments, watch_config_file, }; use std::{ path::{Path, PathBuf}, @@ -377,7 +377,7 @@ pub struct SettingsObserver { worktree_store: Entity, project_id: u64, task_store: Entity, - _global_task_config_watchers: (Task<()>, Task<()>), + _global_task_config_watcher: Task<()>, } /// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees @@ -405,19 +405,10 @@ impl SettingsObserver { mode: SettingsObserverMode::Local(fs.clone()), downstream_client: None, project_id: 0, - _global_task_config_watchers: ( - Self::subscribe_to_global_task_file_changes( - fs.clone(), - TaskKind::Script, - paths::tasks_file().clone(), - cx, - ), - Self::subscribe_to_global_task_file_changes( - fs, - TaskKind::Debug, - paths::debug_tasks_file().clone(), - cx, - ), + _global_task_config_watcher: Self::subscribe_to_global_task_file_changes( + fs.clone(), + paths::tasks_file().clone(), + cx, ), } } @@ -434,19 +425,10 @@ impl SettingsObserver { mode: SettingsObserverMode::Remote, downstream_client: None, project_id: 0, - _global_task_config_watchers: ( - Self::subscribe_to_global_task_file_changes( - fs.clone(), - TaskKind::Script, - paths::tasks_file().clone(), - cx, - ), - Self::subscribe_to_global_task_file_changes( - fs.clone(), - TaskKind::Debug, - paths::debug_tasks_file().clone(), - cx, - ), + _global_task_config_watcher: Self::subscribe_to_global_task_file_changes( + fs.clone(), + paths::tasks_file().clone(), + cx, ), } } @@ -575,7 +557,7 @@ impl SettingsObserver { ) .unwrap(), ); - (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script)) + (settings_dir, LocalSettingsKind::Tasks) } else if path.ends_with(local_vscode_tasks_file_relative_path()) { let settings_dir = Arc::::from( path.ancestors() @@ -587,7 +569,7 @@ impl SettingsObserver { ) .unwrap(), ); - (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script)) + (settings_dir, LocalSettingsKind::Tasks) } else if path.ends_with(local_debug_file_relative_path()) { let settings_dir = Arc::::from( path.ancestors() @@ -599,7 +581,7 @@ impl SettingsObserver { ) .unwrap(), ); - (settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug)) + (settings_dir, LocalSettingsKind::Debug) } else if path.ends_with(local_vscode_launch_file_relative_path()) { let settings_dir = Arc::::from( path.ancestors() @@ -611,7 +593,7 @@ impl SettingsObserver { ) .unwrap(), ); - (settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug)) + (settings_dir, LocalSettingsKind::Debug) } else if path.ends_with(EDITORCONFIG_NAME) { let Some(settings_dir) = path.parent().map(Arc::from) else { continue; @@ -747,7 +729,7 @@ impl SettingsObserver { } } }), - LocalSettingsKind::Tasks(task_kind) => { + LocalSettingsKind::Tasks => { let result = task_store.update(cx, |task_store, cx| { task_store.update_user_tasks( TaskSettingsLocation::Worktree(SettingsLocation { @@ -755,7 +737,6 @@ impl SettingsObserver { path: directory.as_ref(), }), file_content.as_deref(), - task_kind, cx, ) }); @@ -772,7 +753,38 @@ impl SettingsObserver { } 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( fs: Arc, - task_kind: TaskKind, file_path: PathBuf, cx: &mut Context, ) -> Task<()> { @@ -815,7 +826,6 @@ impl SettingsObserver { .update_user_tasks( TaskSettingsLocation::Global(&file_path), Some(&user_tasks_content), - task_kind, cx, ) .log_err(); @@ -828,7 +838,6 @@ impl SettingsObserver { task_store.update_user_tasks( TaskSettingsLocation::Global(&file_path), Some(&user_tasks_content), - task_kind, cx, ) }) else { @@ -856,15 +865,17 @@ impl SettingsObserver { pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind { match kind { proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings, - proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks(TaskKind::Script), + proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks, proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig, + proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug, } } pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind { match kind { LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings, - LocalSettingsKind::Tasks(_) => proto::LocalSettingsKind::Tasks, + LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks, LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig, + LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug, } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 63168283a7..f923c15229 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -292,7 +292,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) }) .into_iter() .map(|(source_kind, task)| { - let resolved = task.resolved.unwrap(); + let resolved = task.resolved; ( source_kind, task.resolved_label, @@ -359,7 +359,6 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) }]) .to_string(), ), - settings::TaskKind::Script, ) .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)) .into_iter() .map(|(source_kind, task)| { - let resolved = task.resolved.unwrap(); + let resolved = task.resolved; ( source_kind, task.resolved_label, @@ -495,7 +494,7 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) { active_worktree_tasks .into_iter() .map(|(source_kind, task)| { - let resolved = task.resolved.unwrap(); + let resolved = task.resolved; (source_kind, resolved.command) }) .collect::>(), diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 3653ea4401..a93b8f9037 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -13,14 +13,15 @@ use collections::{HashMap, HashSet, VecDeque}; use gpui::{App, AppContext as _, Entity, SharedString, Task}; use itertools::Itertools; use language::{ - ContextProvider, File, Language, LanguageToolchainStore, Location, + Buffer, ContextProvider, File, Language, LanguageToolchainStore, Location, language_settings::language_settings, }; 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::{ - DebugTaskTemplate, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, - TaskVariables, VariableName, + DebugScenario, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, TaskVariables, + VariableName, }; use text::{BufferId, Point, ToPoint}; 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)] pub struct Inventory { last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>, - templates_from_settings: ParsedTemplates, + templates_from_settings: InventoryFor, + scenarios_from_settings: InventoryFor, } -#[derive(Debug, Default)] -struct ParsedTemplates { - global: HashMap>, - worktree: HashMap, TaskKind), Vec>>, +// Helper trait for better error messages in [InventoryFor] +trait InventoryContents: Clone { + const GLOBAL_SOURCE_FILE: &'static str; + 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 { + global: HashMap>, + worktree: HashMap, Vec>>, +} + +impl InventoryFor { + fn worktree_scenarios( + &self, + worktree: Option, + ) -> impl '_ + Iterator { + 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 { + 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 Default for InventoryFor { + 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. @@ -134,22 +206,40 @@ impl Inventory { cx.new(|_| Self::default()) } - pub fn list_debug_tasks(&self) -> Vec<&TaskTemplate> { - self.templates_from_settings - .worktree - .values() - .flat_map(|tasks| { - tasks.iter().filter_map(|(kind, tasks)| { - if matches!(kind.1, TaskKind::Debug) { - Some(tasks) - } else { - None - } - }) - }) - .flatten() + pub fn list_debug_scenarios(&self, worktree: Option) -> Vec { + let global_scenarios = self.global_debug_scenarios_from_settings(); + let worktree_scenarios = self.worktree_scenarios_from_settings(worktree); + + worktree_scenarios + .chain(global_scenarios) + .map(|(_, scenario)| scenario) .collect() } + + pub fn task_template_by_label( + &self, + buffer: Option>, + label: &str, + cx: &App, + ) -> Option { + 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, /// 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. @@ -160,10 +250,11 @@ impl Inventory { worktree: Option, cx: &App, ) -> 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 { name: language.name().into(), }); - let global_tasks = self.global_templates_from_settings(); let language_tasks = language .filter(|language| { 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)) .into_iter() .flat_map(|tasks| tasks.0.into_iter()) - .flat_map(|task| Some((task_source_kind.clone()?, task))) - .chain(global_tasks); + .flat_map(|task| Some((task_source_kind.clone()?, task))); - self.worktree_templates_from_settings(worktree) + worktree_tasks .chain(language_tasks) + .chain(global_tasks) .collect() } @@ -358,51 +449,27 @@ impl Inventory { fn global_templates_from_settings( &self, ) -> impl '_ + Iterator { - self.templates_from_settings - .global - .iter() - .flat_map(|(file_path, templates)| { - templates.into_iter().map(|template| { - ( - TaskSourceKind::AbsPath { - id_base: match template.task_type { - task::TaskType::Script => Cow::Borrowed("global tasks.json"), - task::TaskType::Debug(_) => Cow::Borrowed("global debug.json"), - }, - abs_path: file_path.clone(), - }, - template.clone(), - ) - }) - }) + self.templates_from_settings.global_scenarios() + } + + fn global_debug_scenarios_from_settings( + &self, + ) -> impl '_ + Iterator { + self.scenarios_from_settings.global_scenarios() + } + + fn worktree_scenarios_from_settings( + &self, + worktree: Option, + ) -> impl '_ + Iterator { + self.scenarios_from_settings.worktree_scenarios(worktree) } fn worktree_templates_from_settings( &self, worktree: Option, ) -> impl '_ + Iterator { - worktree.into_iter().flat_map(|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(), - ) - }) - }) + self.templates_from_settings.worktree_scenarios(worktree) } /// Updates in-memory task metadata from the JSON string given. @@ -413,7 +480,6 @@ impl Inventory { &mut self, location: TaskSettingsLocation<'_>, raw_tasks_json: Option<&str>, - task_kind: TaskKind, ) -> Result<(), InvalidSettingsError> { let raw_tasks = match parse_json_with_comments::>( raw_tasks_json.unwrap_or("[]"), @@ -424,21 +490,16 @@ impl Inventory { path: match location { TaskSettingsLocation::Global(path) => path.to_owned(), 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}"), }); } }; - let new_templates = raw_tasks - .into_iter() - .filter_map(|raw_template| match &task_kind { - TaskKind::Script => serde_json::from_value::(raw_template).log_err(), - TaskKind::Debug => serde_json::from_value::(raw_template) - .log_err() - .map(|content| content.to_zed_format()), - }); + let new_templates = raw_tasks.into_iter().filter_map(|raw_template| { + serde_json::from_value::(raw_template).log_err() + }); let parsed_templates = &mut self.templates_from_settings; match location { @@ -454,14 +515,72 @@ impl Inventory { if let Some(worktree_tasks) = parsed_templates.worktree.get_mut(&location.worktree_id) { - worktree_tasks.remove(&(Arc::from(location.path), task_kind)); + worktree_tasks.remove(location.path); } } else { parsed_templates .worktree .entry(location.worktree_id) .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::>( + 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::(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::>(); + 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)) } + + fn debug_adapter(&self) -> Option { + None + } } /// 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 { Some(self.templates.clone()) } + + fn debug_adapter(&self) -> Option { + None + } } #[cfg(test)] @@ -744,7 +871,6 @@ mod tests { Some(&mock_tasks_from_names( expected_initial_state.iter().map(|name| name.as_str()), )), - settings::TaskKind::Script, ) .unwrap(); }); @@ -800,7 +926,6 @@ mod tests { .into_iter() .chain(expected_initial_state.iter().map(|name| name.as_str())), )), - settings::TaskKind::Script, ) .unwrap(); }); @@ -925,7 +1050,6 @@ mod tests { .iter() .map(|(_, name)| name.as_str()), )), - settings::TaskKind::Script, ) .unwrap(); inventory @@ -937,7 +1061,6 @@ mod tests { Some(&mock_tasks_from_names( worktree_1_tasks.iter().map(|(_, name)| name.as_str()), )), - settings::TaskKind::Script, ) .unwrap(); inventory @@ -949,7 +1072,6 @@ mod tests { Some(&mock_tasks_from_names( worktree_2_tasks.iter().map(|(_, name)| name.as_str()), )), - settings::TaskKind::Script, ) .unwrap(); }); diff --git a/crates/project/src/task_store.rs b/crates/project/src/task_store.rs index 824cb7a9bb..331f956018 100644 --- a/crates/project/src/task_store.rs +++ b/crates/project/src/task_store.rs @@ -11,7 +11,7 @@ use language::{ proto::{deserialize_anchor, serialize_anchor}, }; use rpc::{AnyProtoClient, TypedEnvelope, proto}; -use settings::{InvalidSettingsError, SettingsLocation, TaskKind}; +use settings::{InvalidSettingsError, SettingsLocation}; use task::{TaskContext, TaskVariables, VariableName}; use text::{BufferId, OffsetRangeExt}; use util::ResultExt; @@ -264,7 +264,6 @@ impl TaskStore { &self, location: TaskSettingsLocation<'_>, raw_tasks_json: Option<&str>, - task_type: TaskKind, cx: &mut Context, ) -> Result<(), InvalidSettingsError> { let task_inventory = match self { @@ -276,7 +275,26 @@ impl TaskStore { .filter(|json| !json.is_empty()); 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, + ) -> 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) }) } } diff --git a/crates/proto/proto/debugger.proto b/crates/proto/proto/debugger.proto index 14751c8a38..9337d36f5f 100644 --- a/crates/proto/proto/debugger.proto +++ b/crates/proto/proto/debugger.proto @@ -543,6 +543,7 @@ message DebugLaunchRequest { string program = 1; optional string cwd = 2; repeated string args = 3; + map env = 4; } message DebugAttachRequest { @@ -558,7 +559,7 @@ message DapModuleId { message GetDebugAdapterBinary { uint64 project_id = 1; - DebugTaskDefinition task = 2; + DebugTaskDefinition definition = 2; } message DebugAdapterBinary { @@ -575,8 +576,32 @@ message DebugAdapterBinary { } } -message RunDebugLocator { +message RunDebugLocators { uint64 project_id = 1; - string locator = 2; - DebugTaskDefinition task = 3; + SpawnInTerminal build_command = 2; +} + +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 env = 4; + optional string cwd = 5; } diff --git a/crates/proto/proto/worktree.proto b/crates/proto/proto/worktree.proto index 0650e0a491..67bd1925b5 100644 --- a/crates/proto/proto/worktree.proto +++ b/crates/proto/proto/worktree.proto @@ -148,4 +148,5 @@ enum LocalSettingsKind { Settings = 0; Tasks = 1; Editorconfig = 2; + Debug = 3; } diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 74db3ce539..ebb6623c3f 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -377,8 +377,8 @@ message Envelope { GetDebugAdapterBinary get_debug_adapter_binary = 339; DebugAdapterBinary debug_adapter_binary = 340; - RunDebugLocator run_debug_locator = 341; - DebugTaskDefinition debug_task_definition = 342; // current max + RunDebugLocators run_debug_locators = 341; + DebugRequest debug_request = 342; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 03e0bda101..61c2b66ebe 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -298,8 +298,8 @@ messages!( (GitInit, Background), (GetDebugAdapterBinary, Background), (DebugAdapterBinary, Background), - (RunDebugLocator, Background), - (DebugTaskDefinition, Background), + (RunDebugLocators, Background), + (DebugRequest, Background), ); request_messages!( @@ -456,7 +456,7 @@ request_messages!( (GitInit, Ack), (ToggleBreakpoint, Ack), (GetDebugAdapterBinary, DebugAdapterBinary), - (RunDebugLocator, DebugTaskDefinition), + (RunDebugLocators, DebugRequest), ); entity_messages!( @@ -576,7 +576,7 @@ entity_messages!( GitInit, BreakpointsForFile, ToggleBreakpoint, - RunDebugLocator, + RunDebugLocators, GetDebugAdapterBinary, ); diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index c4e1f18852..9854f6f7e9 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -245,7 +245,7 @@ impl HeadlessProject { LspStore::init(&client); TaskStore::init(Some(&client)); ToolchainStore::init(&client); - DapStore::init(&client); + DapStore::init(&client, cx); // todo(debugger): Re init breakpoint store when we set it up for collab // BreakpointStore::init(&client); GitStore::init(&client); diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 1e234327db..8db108778f 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -20,7 +20,7 @@ pub use keymap_file::{ pub use settings_file::*; pub use settings_store::{ InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources, - SettingsStore, TaskKind, parse_json_with_comments, + SettingsStore, parse_json_with_comments, }; pub use vscode_import::VsCodeSettings; diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index d333d12951..3b845574a9 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -5,9 +5,7 @@ use fs::Fs; use futures::{FutureExt, StreamExt, channel::mpsc, future::LocalBoxFuture}; use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal}; -use paths::{ - EDITORCONFIG_NAME, debug_task_file_name, local_settings_file_relative_path, task_file_name, -}; +use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; use schemars::{JsonSchema, r#gen::SchemaGenerator, schema::RootSchema}; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde_json::Value; @@ -217,14 +215,9 @@ impl FromStr for Editorconfig { #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum LocalSettingsKind { Settings, - Tasks(TaskKind), + Tasks, Editorconfig, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub enum TaskKind { Debug, - Script, } impl Global for SettingsStore {} @@ -265,16 +258,6 @@ trait AnySettingValue: 'static + Send + Sync { struct DeserializedSetting(Box); -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 { pub fn new(cx: &App) -> Self { let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded(); @@ -684,10 +667,17 @@ impl SettingsStore { .map(|content| content.trim()) .filter(|content| !content.is_empty()), ) { - (LocalSettingsKind::Tasks(task_kind), _) => { + (LocalSettingsKind::Tasks, _) => { return Err(InvalidSettingsError::Tasks { 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) => { @@ -1085,6 +1075,7 @@ pub enum InvalidSettingsError { DefaultSettings { message: String }, Editorconfig { path: PathBuf, message: String }, Tasks { path: PathBuf, message: String }, + Debug { path: PathBuf, message: String }, } impl std::fmt::Display for InvalidSettingsError { @@ -1095,7 +1086,8 @@ impl std::fmt::Display for InvalidSettingsError { | InvalidSettingsError::ServerSettings { message } | InvalidSettingsError::DefaultSettings { message } | InvalidSettingsError::Tasks { message, .. } - | InvalidSettingsError::Editorconfig { message, .. } => { + | InvalidSettingsError::Editorconfig { message, .. } + | InvalidSettingsError::Debug { message, .. } => { write!(f, "{message}") } } diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 52c0e84c51..129db722c9 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -1,11 +1,11 @@ use anyhow::Result; +use collections::FxHashMap; +use gpui::SharedString; use schemars::{JsonSchema, r#gen::SchemaSettings}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::{net::Ipv4Addr, path::Path}; -use crate::{TaskTemplate, TaskType, task_template::DebugArgs}; - /// Represents the host information of the debug adapter #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] pub struct TcpArgumentsTemplate { @@ -63,6 +63,8 @@ pub struct LaunchRequest { /// Arguments to pass to a debuggee #[serde(default)] pub args: Vec, + #[serde(default)] + pub env: FxHashMap, } /// Represents the type that will determine which request to call on the debug adapter @@ -75,6 +77,64 @@ pub enum DebugRequest { 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 { + 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 for DebugRequest { fn from(launch_config: LaunchRequest) -> Self { DebugRequest::Launch(launch_config) @@ -87,180 +147,46 @@ impl From for DebugRequest { } } -impl TryFrom for DebugTaskTemplate { - type Error = (); - - fn try_from(value: TaskTemplate) -> Result { - 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, - #[serde(flatten)] - pub definition: DebugTaskDefinition, -} - /// This struct represent a user created debug task #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] -pub struct DebugTaskDefinition { - /// The adapter to run - pub adapter: String, - /// The type of request that should be called on the debug adapter - #[serde(flatten)] - pub request: DebugRequest, +pub struct DebugScenario { + pub adapter: SharedString, /// Name of the debug task - pub label: String, + pub label: SharedString, + /// A task to run prior to spawning the debuggee. + pub build: Option, + #[serde(flatten)] + pub request: Option, /// Additional initialization arguments to be sent on DAP initialization + #[serde(default)] pub initialize_args: Option, /// Optional TCP connection information /// /// 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 /// that is already running or is started by another process. + #[serde(default)] pub tcp_connection: Option, /// Whether to tell the debug adapter to stop on entry + #[serde(default)] pub stop_on_entry: Option, } -impl DebugTaskDefinition { +impl DebugScenario { pub fn cwd(&self) -> Option<&Path> { - if let DebugRequest::Launch(config) = &self.request { - config.cwd.as_deref() + if let Some(DebugRequest::Launch(config)) = &self.request { + config.cwd.as_ref().map(Path::new) } else { 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 { - 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. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(transparent)] -pub struct DebugTaskFile(pub Vec); +pub struct DebugTaskFile(pub Vec); impl DebugTaskFile { /// Generates JSON schema of Tasks JSON template format. diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index a55e89fdaf..45d9afbcb2 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -16,12 +16,10 @@ use std::path::PathBuf; use std::str::FromStr; pub use debug_format::{ - AttachRequest, DebugRequest, DebugTaskDefinition, DebugTaskFile, DebugTaskTemplate, - LaunchRequest, TcpArgumentsTemplate, + AttachRequest, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, TcpArgumentsTemplate, }; pub use task_template::{ - DebugArgs, DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate, - TaskTemplates, TaskType, + DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate, TaskTemplates, }; pub use vscode_debug_format::VsCodeDebugTaskFile; pub use vscode_format::VsCodeTaskFile; @@ -29,11 +27,11 @@ pub use zed_actions::RevealTarget; /// Task identifier, unique within the application. /// 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); /// 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 { /// Id of the task to use when determining task tab affinity. pub id: TaskId, @@ -72,6 +70,36 @@ pub struct SpawnInTerminal { 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. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ResolvedTask { @@ -89,7 +117,7 @@ pub struct ResolvedTask { substituted_variables: HashSet, /// Further actions that need to take place after the resolved task is spawned, /// with all task variables resolved. - pub resolved: Option, + pub resolved: SpawnInTerminal, } impl ResolvedTask { @@ -98,63 +126,6 @@ impl ResolvedTask { &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 { - 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. pub fn substituted_variables(&self) -> &HashSet { &self.substituted_variables @@ -162,10 +133,7 @@ impl ResolvedTask { /// A human-readable label to display in the UI. pub fn display_label(&self) -> &str { - self.resolved - .as_ref() - .map(|resolved| resolved.label.as_str()) - .unwrap_or_else(|| self.resolved_label.as_str()) + self.resolved.label.as_str() } } diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index b112d5b4a9..2032a24fe5 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -7,7 +7,6 @@ use std::path::PathBuf; use util::serde::default_true; use util::{ResultExt, truncate_and_remove_front}; -use crate::debug_format::TcpArgumentsTemplate; use crate::{ AttachRequest, ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TaskContext, TaskId, 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`. #[serde(default)] 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. /// 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")] @@ -87,34 +83,6 @@ pub enum DebugArgsRequest { 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, - /// Args to send to debug adapter - pub initialize_args: Option, - /// the locator to use - pub locator: Option, - /// Whether to tell the debug adapter to stop on entry - pub stop_on_entry: Option, -} - -/// 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)] /// The type of task modal to spawn 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), /// 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 { - if self.label.trim().is_empty() - || (self.command.trim().is_empty() && matches!(self.task_type, TaskType::Script)) - { + if self.label.trim().is_empty() || self.command.trim().is_empty() { return None; } @@ -285,7 +251,7 @@ impl TaskTemplate { substituted_variables, original_task: self.clone(), resolved_label: full_label.clone(), - resolved: Some(SpawnInTerminal { + resolved: SpawnInTerminal { id, cwd, full_label, @@ -310,7 +276,7 @@ impl TaskTemplate { show_summary: self.show_summary, show_command: self.show_command, show_rerun: true, - }), + }, }) } } @@ -474,12 +440,7 @@ mod tests { .resolve_task(TEST_ID_BASE, task_cx) .unwrap_or_else(|| panic!("failed to resolve task {task_without_cwd:?}")); assert_substituted_variables(&resolved_task, Vec::new()); - resolved_task - .resolved - .clone() - .unwrap_or_else(|| { - panic!("failed to get resolve data for resolved task. Template: {task_without_cwd:?} Resolved: {resolved_task:?}") - }) + resolved_task.resolved }; let cx = TaskContext { @@ -626,10 +587,7 @@ mod tests { all_variables.iter().map(|(name, _)| name.clone()).collect(), ); - let spawn_in_terminal = resolved_task - .resolved - .as_ref() - .expect("should have resolved a spawn in terminal task"); + let spawn_in_terminal = &resolved_task.resolved; assert_eq!( spawn_in_terminal.label, format!( @@ -713,7 +671,7 @@ mod tests { .resolve_task(TEST_ID_BASE, &TaskContext::default()) .unwrap(); 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.command, task.command); assert_eq!(resolved.args, task.args); @@ -882,8 +840,7 @@ mod tests { let resolved = template .resolve_task(TEST_ID_BASE, &context) .unwrap() - .resolved - .unwrap(); + .resolved; assert_eq!(resolved.env["TASK_ENV_VAR1"], "TASK_ENV_VAR1_VALUE"); assert_eq!(resolved.env["TASK_ENV_VAR2"], "env_var_2 1234 5678"); diff --git a/crates/task/src/vscode_debug_format.rs b/crates/task/src/vscode_debug_format.rs index 734064912f..694956c72c 100644 --- a/crates/task/src/vscode_debug_format.rs +++ b/crates/task/src/vscode_debug_format.rs @@ -2,12 +2,13 @@ use std::path::PathBuf; use anyhow::anyhow; use collections::HashMap; +use gpui::SharedString; use serde::Deserialize; use util::ResultExt as _; use crate::{ - AttachRequest, DebugRequest, DebugTaskDefinition, DebugTaskFile, DebugTaskTemplate, - EnvVariableReplacer, LaunchRequest, TcpArgumentsTemplate, VariableName, + AttachRequest, DebugRequest, DebugScenario, DebugTaskFile, EnvVariableReplacer, LaunchRequest, + TcpArgumentsTemplate, VariableName, }; #[derive(Clone, Debug, Deserialize, PartialEq)] @@ -43,11 +44,12 @@ struct VsCodeDebugTaskDefinition { } impl VsCodeDebugTaskDefinition { - fn try_to_zed(self, replacer: &EnvVariableReplacer) -> anyhow::Result { - let label = replacer.replace(&self.name); + fn try_to_zed(self, replacer: &EnvVariableReplacer) -> anyhow::Result { + let label = replacer.replace(&self.name).into(); // TODO based on grep.app results it seems that vscode supports whitespace-splitting this field (ugh) - let definition = DebugTaskDefinition { + let definition = DebugScenario { label, + build: None, request: match self.request { Request::Launch => { let cwd = self.cwd.map(|cwd| PathBuf::from(replacer.replace(&cwd))); @@ -60,11 +62,22 @@ impl VsCodeDebugTaskDefinition { .into_iter() .map(|arg| replacer.replace(&arg)) .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? tcp_connection: self.port.map(|port| TcpArgumentsTemplate { port: Some(port), @@ -75,11 +88,7 @@ impl VsCodeDebugTaskDefinition { // TODO initialize_args: None, }; - let template = DebugTaskTemplate { - locator: None, - definition, - }; - Ok(template) + Ok(definition) } } @@ -110,24 +119,26 @@ impl TryFrom for DebugTaskFile { } } -// TODO figure out how to make JsDebugAdapter::ADAPTER_NAME et al available here -fn task_type_to_adapter_name(task_type: String) -> String { - match task_type.as_str() { - "node" => "JavaScript".to_owned(), - "go" => "Delve".to_owned(), - "php" => "PHP".to_owned(), - "cppdbg" | "lldb" => "CodeLLDB".to_owned(), - "debugpy" => "Debugpy".to_owned(), +// todo(debugger) figure out how to make JsDebugAdapter::ADAPTER_NAME et al available here +fn task_type_to_adapter_name(task_type: &str) -> SharedString { + match task_type { + "node" => "JavaScript", + "go" => "Delve", + "php" => "PHP", + "cppdbg" | "lldb" => "CodeLLDB", + "debugpy" => "Debugpy", _ => task_type, } + .to_owned() + .into() } #[cfg(test)] mod tests { - use crate::{ - DebugRequest, DebugTaskDefinition, DebugTaskFile, DebugTaskTemplate, LaunchRequest, - TcpArgumentsTemplate, - }; + + use collections::FxHashMap; + + use crate::{DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, TcpArgumentsTemplate}; use super::VsCodeDebugTaskFile; @@ -159,24 +170,23 @@ mod tests { let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates"); pretty_assertions::assert_eq!( zed, - DebugTaskFile(vec![DebugTaskTemplate { - locator: None, - definition: DebugTaskDefinition { - label: "Debug my JS app".into(), - adapter: "JavaScript".into(), - stop_on_entry: Some(true), - initialize_args: None, - tcp_connection: Some(TcpArgumentsTemplate { - port: Some(17), - host: None, - timeout: None, - }), - request: DebugRequest::Launch(LaunchRequest { - program: "${ZED_WORKTREE_ROOT}/xyz.js".into(), - args: vec!["--foo".into(), "${ZED_WORKTREE_ROOT}/thing".into()], - cwd: Some("${ZED_WORKTREE_ROOT}/${FOO}/sub".into()), - }), - } + DebugTaskFile(vec![DebugScenario { + label: "Debug my JS app".into(), + adapter: "JavaScript".into(), + stop_on_entry: Some(true), + initialize_args: None, + tcp_connection: Some(TcpArgumentsTemplate { + port: Some(17), + host: None, + timeout: None, + }), + request: Some(DebugRequest::Launch(LaunchRequest { + program: "${ZED_WORKTREE_ROOT}/xyz.js".into(), + args: vec!["--foo".into(), "${ZED_WORKTREE_ROOT}/thing".into()], + cwd: Some("${ZED_WORKTREE_ROOT}/${FOO}/sub".into()), + env: FxHashMap::from_iter([("X".into(), "Y".into())]) + })), + build: None }]) ); } diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index a027db8774..5f34816854 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -10,10 +10,7 @@ use gpui::{ use itertools::Itertools; use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; use project::{TaskSourceKind, task_store::TaskStore}; -use task::{ - DebugRequest, DebugTaskDefinition, ResolvedTask, RevealTarget, TaskContext, TaskModal, - TaskTemplate, TaskType, -}; +use task::{DebugScenario, ResolvedTask, RevealTarget, TaskContext, TaskModal, TaskTemplate}; use ui::{ ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement, KeyBinding, Label, LabelSize, @@ -187,7 +184,7 @@ impl Render for TasksModal { } pub struct ShowAttachModal { - pub debug_config: DebugTaskDefinition, + pub debug_config: DebugScenario, } impl EventEmitter for TasksModal {} @@ -354,48 +351,20 @@ impl PickerDelegate for TasksModalDelegate { reveal_target: Some(reveal_target), }) = &self.task_overrides { - if let Some(resolved_task) = &mut task.resolved { - resolved_task.reveal_target = *reveal_target; - } + task.resolved.reveal_target = *reveal_target; } - match task.task_type() { - TaskType::Debug(_) => { - let Some(config) = task.resolved_debug_adapter_config() else { - return; - }; - let config = config.definition; - - match &config.request { - DebugRequest::Attach(attach_config) if attach_config.process_id.is_none() => { - cx.emit(ShowAttachModal { - debug_config: config.clone(), - }); - 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(); - } - }; + self.workspace + .update(cx, |workspace, cx| { + workspace.schedule_resolved_task( + task_source_kind, + task, + omit_history_entry, + window, + cx, + ); + }) + .ok(); cx.emit(DismissEvent); } @@ -422,16 +391,14 @@ impl PickerDelegate for TasksModalDelegate { } else { String::new() }; - if let Some(resolved) = resolved_task.resolved.as_ref() { - if resolved.command_label != display_label - && resolved.command_label != resolved_task.resolved_label - { - if !tooltip_label_text.trim().is_empty() { - tooltip_label_text.push('\n'); - } - tooltip_label_text.push_str(&resolved.command_label); + + if resolved_task.resolved.command_label != resolved_task.resolved_label { + if !tooltip_label_text.trim().is_empty() { + tooltip_label_text.push('\n'); } + tooltip_label_text.push_str(&resolved_task.resolved.command_label); } + if template.tags.len() > 0 { tooltip_label_text.push('\n'); 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 tasks = self.candidates.as_ref()?; let (_, task) = tasks.get(task_index)?; - Some(task.resolved.as_ref()?.command_label.clone()) + Some(task.resolved.command_label.clone()) } fn confirm_input( @@ -570,26 +537,17 @@ impl PickerDelegate for TasksModalDelegate { reveal_target: Some(reveal_target), }) = self.task_overrides { - if let Some(resolved_task) = &mut task.resolved { - resolved_task.reveal_target = reveal_target; - } + task.resolved.reveal_target = reveal_target; } self.workspace .update(cx, |workspace, cx| { - match task.task_type() { - TaskType::Script => workspace.schedule_resolved_task( - task_source_kind, - task, - omit_history_entry, - window, - 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); - } - }; + workspace.schedule_resolved_task( + task_source_kind, + task, + omit_history_entry, + window, + cx, + ) }) .ok(); cx.emit(DismissEvent); @@ -716,10 +674,7 @@ fn string_match_candidates<'a>( candidates .into_iter() .enumerate() - .filter(|(_, (_, candidate))| match candidate.task_type() { - TaskType::Script => task_modal_type == TaskModal::ScriptModal, - TaskType::Debug(_) => task_modal_type == TaskModal::DebugModal, - }) + .filter(|(_, (_, _))| task_modal_type == TaskModal::ScriptModal) .map(|(index, (_, candidate))| StringMatchCandidate::new(index, candidate.display_label())) .collect() } diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index 7bd60aac28..527dc12d4d 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -65,13 +65,13 @@ pub fn init(cx: &mut App) { }) .detach() } else { - if let Some(resolved) = last_scheduled_task.resolved.as_mut() { - 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; - } + 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(use_new_terminal) = action.use_new_terminal { + resolved.use_new_terminal = use_new_terminal; } workspace.schedule_resolved_task( diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs index 64f7d4d607..0012765125 100644 --- a/crates/workspace/src/tasks.rs +++ b/crates/workspace/src/tasks.rs @@ -1,10 +1,11 @@ use std::process::ExitStatus; use anyhow::{Result, anyhow}; -use gpui::{Context, Task}; +use gpui::{Context, Entity, Task}; +use language::Buffer; use project::TaskSourceKind; use remote::ConnectionState; -use task::{DebugTaskDefinition, ResolvedTask, SpawnInTerminal, TaskContext, TaskTemplate}; +use task::{DebugScenario, ResolvedTask, SpawnInTerminal, TaskContext, TaskTemplate}; use ui::Window; use crate::Workspace; @@ -48,84 +49,41 @@ impl Workspace { pub fn schedule_resolved_task( self: &mut Workspace, task_source_kind: TaskSourceKind, - mut resolved_task: ResolvedTask, + resolved_task: ResolvedTask, omit_history: bool, window: &mut Window, cx: &mut Context, ) { - if let Some(spawn_in_terminal) = resolved_task.resolved.take() { - if !omit_history { - resolved_task.resolved = Some(spawn_in_terminal.clone()); - self.project().update(cx, |project, cx| { - if let Some(task_inventory) = - project.task_store().read(cx).task_inventory().cloned() - { - task_inventory.update(cx, |inventory, _| { - 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, - ) { - 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 spawn_in_terminal = resolved_task.resolved.clone(); + if !omit_history { + self.project().update(cx, |project, cx| { + if let Some(task_inventory) = + project.task_store().read(cx).task_inventory().cloned() + { + task_inventory.update(cx, |inventory, _| { + inventory.task_scheduled(task_source_kind, resolved_task); + }) } - 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| { - workspace.start_debug_session(config, window, cx); - })?; - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + 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 start_debug_session( &mut self, - definition: DebugTaskDefinition, + scenario: DebugScenario, + task_context: TaskContext, + active_buffer: Option>, window: &mut Window, cx: &mut Context, ) { 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) } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index fa60d644fa..3ecc847bed 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -49,7 +49,7 @@ pub use item::{ ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle, }; use itertools::Itertools; -use language::{LanguageRegistry, Rope}; +use language::{Buffer, LanguageRegistry, Rope}; pub use modal_layer::*; use node_runtime::NodeRuntime; use notifications::{ @@ -96,7 +96,7 @@ use std::{ sync::{Arc, LazyLock, Weak, atomic::AtomicUsize}, time::Duration, }; -use task::{DebugTaskDefinition, SpawnInTerminal}; +use task::{DebugScenario, SpawnInTerminal, TaskContext}; use theme::{ActiveTheme, SystemAppearance, ThemeSettings}; pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub use ui; @@ -140,7 +140,15 @@ pub trait TerminalProvider { } 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>, + window: &mut Window, + cx: &mut App, + ); } actions!( diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c151d337a7..98c315e996 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4273,7 +4273,7 @@ mod tests { project::debugger::breakpoint_store::BreakpointStore::init( &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); initialize_workspace(app_state.clone(), prompt_builder, cx); search::init(cx);