diff --git a/Cargo.lock b/Cargo.lock index 7b72a9d736f745..d2e3261dfc20df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3370,6 +3370,7 @@ dependencies = [ "settings", "smallvec", "smol", + "sysinfo", "task", "util", ] @@ -3392,8 +3393,10 @@ dependencies = [ "async-trait", "dap", "paths", + "regex", "serde", "serde_json", + "sysinfo", "task", "util", ] @@ -3499,10 +3502,12 @@ dependencies = [ "language", "menu", "parking_lot", + "picker", "project", "serde", "serde_json", "settings", + "sysinfo", "task", "tasks_ui", "terminal_view", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index f3a955859869a8..c2f57bf6abeca1 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -26,5 +26,6 @@ serde_json.workspace = true settings.workspace = true smallvec.workspace = true smol.workspace = true +sysinfo.workspace = true task.workspace = true util.workspace = true diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 1c2f7ab148b0ad..542c245402799d 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -15,6 +15,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use sysinfo::{Pid, Process}; use task::DebugAdapterConfig; #[derive(Clone, Debug, PartialEq, Eq)] @@ -210,7 +211,7 @@ pub trait DebugAdapter: 'static + Send + Sync { ); let binary = self - .get_installed_binary(delegate, config, Some(adapter_path)) + .get_installed_binary(delegate, &config, Some(adapter_path)) .await; if binary.is_ok() { @@ -240,14 +241,14 @@ pub trait DebugAdapter: 'static + Send + Sync { { log::info!("Using cached debug adapter binary {}", self.name()); - return self.get_installed_binary(delegate, config, None).await; + return self.get_installed_binary(delegate, &config, None).await; } log::info!("Getting latest version of debug adapter {}", self.name()); delegate.update_status(self.name(), DapStatus::CheckingForUpdate); let version = self.fetch_latest_adapter_version(delegate).await.ok(); - let mut binary = self.get_installed_binary(delegate, config, None).await; + let mut binary = self.get_installed_binary(delegate, &config, None).await; if let Some(version) = version { if binary @@ -265,7 +266,8 @@ pub trait DebugAdapter: 'static + Send + Sync { delegate.update_status(self.name(), DapStatus::Downloading); self.install_binary(version, delegate).await?; - binary = self.get_installed_binary(delegate, config, None).await; + + binary = self.get_installed_binary(delegate, &config, None).await; } else { log::error!( "Failed getting latest version of debug adapter {}", @@ -309,4 +311,18 @@ pub trait DebugAdapter: 'static + Send + Sync { /// Should return base configuration to make the debug adapter work fn request_args(&self, config: &DebugAdapterConfig) -> Value; + + /// Whether the adapter supports `attach` request, + /// if not support and the request is selected we will show an error message + fn supports_attach(&self) -> bool { + false + } + + /// Filters out the processes that the adapter can attach to for debugging + fn attach_processes<'a>( + &self, + _: &'a HashMap, + ) -> Option> { + None + } } diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 672fa17c817c6c..068a5a91e07eb6 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -3,19 +3,17 @@ use crate::{ transport::{IoKind, LogKind, TransportDelegate}, }; use anyhow::{anyhow, Result}; - use dap_types::{ messages::{Message, Response}, requests::Request, }; use gpui::{AppContext, AsyncAppContext}; -use serde_json::Value; use smol::channel::{bounded, Receiver, Sender}; use std::{ hash::Hash, sync::{ atomic::{AtomicU64, Ordering}, - Arc, + Arc, Mutex, }, }; use task::{DebugAdapterConfig, DebugRequestType}; @@ -35,27 +33,26 @@ pub struct DebugAdapterClientId(pub usize); pub struct DebugAdapterClient { id: DebugAdapterClientId, - adapter_id: String, - request_args: Value, sequence_count: AtomicU64, - config: DebugAdapterConfig, + adapter: Arc>, transport_delegate: TransportDelegate, + config: Arc>, } impl DebugAdapterClient { pub fn new( id: DebugAdapterClientId, - request_args: Value, config: DebugAdapterConfig, adapter: Arc>, ) -> Self { + let transport_delegate = TransportDelegate::new(adapter.transport()); + Self { id, - config, - request_args, + adapter, + transport_delegate, sequence_count: AtomicU64::new(1), - adapter_id: adapter.name().to_string(), - transport_delegate: TransportDelegate::new(adapter.transport()), + config: Arc::new(Mutex::new(config)), } } @@ -141,19 +138,23 @@ impl DebugAdapterClient { } pub fn config(&self) -> DebugAdapterConfig { - self.config.clone() + self.config.lock().unwrap().clone() } - pub fn adapter_id(&self) -> String { - self.adapter_id.clone() + pub fn adapter(&self) -> &Arc> { + &self.adapter } - pub fn request_args(&self) -> Value { - self.request_args.clone() + pub fn adapter_id(&self) -> String { + self.adapter.name().to_string() } - pub fn request_type(&self) -> DebugRequestType { - self.config.request.clone() + pub fn set_process_id(&self, process_id: u32) { + let mut config = self.config.lock().unwrap(); + + config.request = DebugRequestType::Attach(task::AttachConfig { + process_id: Some(process_id), + }); } /// Get the next sequence id to be used in a request diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index 73d5e8168f5a98..6f390f4e66f46c 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -17,7 +17,9 @@ anyhow.workspace = true async-trait.workspace = true dap.workspace = true paths.workspace = true +regex.workspace = true serde.workspace = true serde_json.workspace = true +sysinfo.workspace = true task.workspace = true util.workspace = true diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 9a305d4a7b142c..3c9949d0ec4caa 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -19,13 +19,13 @@ use serde_json::{json, Value}; use std::path::PathBuf; use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost}; -pub async fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result> { - match &adapter_config.kind { +pub async fn build_adapter(kind: &DebugAdapterKind) -> Result> { + match &kind { DebugAdapterKind::Custom(start_args) => { Ok(Box::new(CustomDebugAdapter::new(start_args.clone()).await?)) } DebugAdapterKind::Python(host) => Ok(Box::new(PythonDebugAdapter::new(host).await?)), - DebugAdapterKind::PHP(host) => Ok(Box::new(PhpDebugAdapter::new(host.clone()).await?)), + DebugAdapterKind::Php(host) => Ok(Box::new(PhpDebugAdapter::new(host.clone()).await?)), DebugAdapterKind::Javascript(host) => { Ok(Box::new(JsDebugAdapter::new(host.clone()).await?)) } diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 708fe650809cc8..6e99e1dd4d50ad 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -1,5 +1,8 @@ use dap::transport::{TcpTransport, Transport}; -use std::net::Ipv4Addr; +use regex::Regex; +use std::{collections::HashMap, net::Ipv4Addr}; +use sysinfo::{Pid, Process}; +use task::DebugRequestType; use util::maybe; use crate::*; @@ -124,10 +127,39 @@ impl DebugAdapter for JsDebugAdapter { } fn request_args(&self, config: &DebugAdapterConfig) -> Value { + let pid = if let DebugRequestType::Attach(attach_config) = &config.request { + attach_config.process_id + } else { + None + }; + json!({ "program": config.program, "type": "pwa-node", + "request": match config.request { + DebugRequestType::Launch => "launch", + DebugRequestType::Attach(_) => "attach", + }, + "processId": pid, "cwd": config.cwd, }) } + + fn supports_attach(&self) -> bool { + true + } + + fn attach_processes<'a>( + &self, + processes: &'a HashMap, + ) -> Option> { + let regex = Regex::new(r"(?i)^(?:node|bun|iojs)(?:$|\b)").unwrap(); + + Some( + processes + .iter() + .filter(|(_, process)| regex.is_match(&process.name().to_string_lossy())) + .collect::>(), + ) + } } diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index c54df216d15177..65f1074d888cef 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -50,7 +50,6 @@ impl DebugAdapter for LldbDebugAdapter { async fn get_binary( &self, _: &dyn DapDelegate, - _: &DebugAdapterConfig, _: Option, ) -> Result { Err(anyhow::anyhow!( diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 370b1e1595c57d..427881ef1af3f2 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -10,8 +10,8 @@ workspace = true [dependencies] anyhow.workspace = true -command_palette_hooks.workspace = true collections.workspace = true +command_palette_hooks.workspace = true dap.workspace = true editor.workspace = true futures.workspace = true @@ -20,10 +20,12 @@ gpui.workspace = true language.workspace = true menu.workspace = true parking_lot.workspace = true +picker.workspace = true project.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true +sysinfo.workspace = true task.workspace = true tasks_ui.workspace = true terminal_view.workspace = true diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs new file mode 100644 index 00000000000000..df036e8ccbd832 --- /dev/null +++ b/crates/debugger_ui/src/attach_modal.rs @@ -0,0 +1,250 @@ +use dap::client::DebugAdapterClientId; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{DismissEvent, EventEmitter, FocusableView, Render, View}; +use gpui::{Model, Subscription}; +use picker::{Picker, PickerDelegate}; +use project::dap_store::DapStore; +use std::sync::Arc; +use sysinfo::System; +use ui::{prelude::*, ViewContext}; +use ui::{ListItem, ListItemSpacing}; +use workspace::ModalView; + +#[derive(Debug, Clone)] +struct Candidate { + pid: u32, + name: String, + command: String, +} + +struct AttachModalDelegate { + selected_index: usize, + matches: Vec, + placeholder_text: Arc, + dap_store: Model, + client_id: DebugAdapterClientId, + candidates: Option>, +} + +impl AttachModalDelegate { + pub fn new(client_id: DebugAdapterClientId, dap_store: Model) -> Self { + Self { + client_id, + dap_store, + candidates: None, + selected_index: 0, + matches: Vec::default(), + placeholder_text: Arc::from("Select the process you want to attach the debugger to"), + } + } +} + +pub(crate) struct AttachModal { + _subscription: Subscription, + picker: View>, +} + +impl AttachModal { + pub fn new( + client_id: &DebugAdapterClientId, + dap_store: Model, + cx: &mut ViewContext, + ) -> Self { + let picker = cx.new_view(|cx| { + Picker::uniform_list(AttachModalDelegate::new(*client_id, dap_store), cx) + }); + let _subscription = cx.subscribe(&picker, |_, _, _, cx| { + cx.emit(DismissEvent); + }); + Self { + picker, + _subscription, + } + } +} + +impl Render for AttachModal { + fn render(&mut self, _: &mut ViewContext) -> impl ui::IntoElement { + v_flex() + .key_context("AttachModal") + .w(rems(34.)) + .child(self.picker.clone()) + } +} + +impl EventEmitter for AttachModal {} + +impl FocusableView for AttachModal { + fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle { + self.picker.read(cx).focus_handle(cx) + } +} + +impl ModalView for AttachModal {} + +impl PickerDelegate for AttachModalDelegate { + type ListItem = ListItem; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { + self.selected_index = ix; + } + + fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> std::sync::Arc { + self.placeholder_text.clone() + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> gpui::Task<()> { + cx.spawn(|this, mut cx| async move { + let Some(processes) = this + .update(&mut cx, |this, cx| { + if let Some(processes) = this.delegate.candidates.clone() { + processes + } else { + let Some(client) = this + .delegate + .dap_store + .read(cx) + .client_by_id(&this.delegate.client_id) + else { + return Vec::new(); + }; + + let system = System::new_all(); + let Some(processes) = + client.adapter().attach_processes(&system.processes()) + else { + return Vec::new(); + }; + + let processes = processes + .into_iter() + .map(|(pid, process)| Candidate { + pid: pid.as_u32(), + name: process.name().to_string_lossy().into_owned(), + command: process + .cmd() + .iter() + .map(|s| s.to_string_lossy()) + .collect::>() + .join(" "), + }) + .collect::>(); + + let _ = this.delegate.candidates.insert(processes.clone()); + + processes + } + }) + .ok() + else { + return; + }; + + let matches = fuzzy::match_strings( + &processes + .iter() + .enumerate() + .map(|(id, candidate)| { + StringMatchCandidate::new( + id, + format!("{} {} {}", candidate.command, candidate.pid, candidate.name), + ) + }) + .collect::>(), + &query, + true, + 100, + &Default::default(), + cx.background_executor().clone(), + ) + .await; + + this.update(&mut cx, |this, _| { + let delegate = &mut this.delegate; + + delegate.matches = matches; + delegate.candidates = Some(processes); + + if delegate.matches.is_empty() { + delegate.selected_index = 0; + } else { + delegate.selected_index = + delegate.selected_index.min(delegate.matches.len() - 1); + } + }) + .ok(); + }) + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + let candidate = self + .matches + .get(self.selected_index()) + .and_then(|current_match| { + let ix = current_match.candidate_id; + self.candidates.as_ref().map(|candidates| &candidates[ix]) + }); + let Some(candidate) = candidate else { + return cx.emit(DismissEvent); + }; + + self.dap_store.update(cx, |store, cx| { + store + .attach(&self.client_id, candidate.pid, cx) + .detach_and_log_err(cx); + }); + + cx.emit(DismissEvent); + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.selected_index = 0; + self.candidates.take(); + + self.dap_store.update(cx, |store, cx| { + store.shutdown_client(&self.client_id, cx).detach(); + }); + + cx.emit(DismissEvent); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _: &mut ViewContext>, + ) -> Option { + let candidates = self.candidates.as_ref()?; + let hit = &self.matches[ix]; + let candidate = &candidates.get(hit.candidate_id)?; + + Some( + ListItem::new(SharedString::from(format!("attach-modal-{ix}"))) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .selected(selected) + .child( + v_flex() + .items_start() + .child(Label::new(candidate.command.clone())) + .child( + Label::new(format!("Pid: {}, name: {}", candidate.pid, candidate.name)) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ), + ) + } +} diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 8fe9ccb2d741e9..47014ccfe8bd86 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,3 +1,4 @@ +use crate::attach_modal::AttachModal; use crate::debugger_panel_item::DebugPanelItem; use anyhow::Result; use collections::{BTreeMap, HashMap}; @@ -22,6 +23,7 @@ use settings::Settings; use std::any::TypeId; use std::path::PathBuf; use std::u64; +use task::DebugRequestType; use terminal_view::terminal_panel::TerminalPanel; use ui::prelude::*; use workspace::{ @@ -98,6 +100,9 @@ impl DebugPanel { cx.subscribe(&pane, Self::handle_pane_event), cx.subscribe(&project, { move |this: &mut Self, _, event, cx| match event { + project::Event::DebugClientStarted(client_id) => { + this.handle_debug_client_started(client_id, cx); + } project::Event::DebugClientEvent { message, client_id } => match message { Message::Event(event) => { this.handle_debug_client_events(client_id, event, cx); @@ -255,7 +260,11 @@ impl DebugPanel { let args = if let Some(args) = request_args { serde_json::from_value(args.clone()).ok() } else { - None + return; + }; + + let Some(args) = args else { + return; }; self.dap_store.update(cx, |store, cx| { @@ -375,6 +384,58 @@ impl DebugPanel { .detach_and_log_err(cx); } + fn handle_debug_client_started( + &self, + client_id: &DebugAdapterClientId, + cx: &mut ViewContext, + ) { + let Some(client) = self.dap_store.read(cx).client_by_id(&client_id) else { + return; + }; + + let client_id = *client_id; + let workspace = self.workspace.clone(); + let request_type = client.config().request; + cx.spawn(|this, mut cx| async move { + let task = this.update(&mut cx, |this, cx| { + this.dap_store + .update(cx, |store, cx| store.initialize(&client_id, cx)) + })?; + + task.await?; + + match request_type { + DebugRequestType::Launch => { + let task = this.update(&mut cx, |this, cx| { + this.dap_store + .update(cx, |store, cx| store.launch(&client_id, cx)) + }); + + task?.await + } + DebugRequestType::Attach(config) => { + if let Some(process_id) = config.process_id { + let task = this.update(&mut cx, |this, cx| { + this.dap_store + .update(cx, |store, cx| store.attach(&client_id, process_id, cx)) + })?; + + task.await + } else { + this.update(&mut cx, |this, cx| { + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, |cx| { + AttachModal::new(&client_id, this.dap_store.clone(), cx) + }) + }) + })? + } + } + } + }) + .detach_and_log_err(cx); + } + fn handle_debug_client_events( &mut self, client_id: &DebugAdapterClientId, @@ -461,7 +522,7 @@ impl DebugPanel { .dap_store .read(cx) .client_by_id(client_id) - .map(|c| c.config().kind) + .map(|client| client.config().kind) else { return; // this can never happen }; @@ -600,7 +661,10 @@ impl DebugPanel { } self.dap_store.update(cx, |store, cx| { - if restart_args.is_some() { + if restart_args + .as_ref() + .is_some_and(|v| v.as_bool().unwrap_or(true)) + { store .restart(&client_id, restart_args, cx) .detach_and_log_err(cx); diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index d7e3e59b4f8b05..f9dbe505f1420e 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -4,10 +4,11 @@ use gpui::AppContext; use settings::Settings; use ui::ViewContext; use workspace::{ - Continue, Pause, Restart, Start, StepInto, StepOut, StepOver, Stop, StopDebugAdapters, + Continue, Pause, Restart, ShutdownDebugAdapters, Start, StepInto, StepOut, StepOver, Stop, Workspace, }; +mod attach_modal; mod console; pub mod debugger_panel; mod debugger_panel_item; @@ -28,7 +29,7 @@ pub fn init(cx: &mut AppContext) { .register_action(|workspace: &mut Workspace, _: &Start, cx| { tasks_ui::toggle_modal(workspace, task::TaskModal::DebugModal, cx).detach(); }) - .register_action(|workspace: &mut Workspace, _: &StopDebugAdapters, cx| { + .register_action(|workspace: &mut Workspace, _: &ShutdownDebugAdapters, cx| { workspace.project().update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { store.shutdown_clients(cx).detach(); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index c9a2fda15cef58..ef53b347d465b4 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -6,20 +6,20 @@ use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use dap::messages::{Message, Response}; use dap::requests::{ Attach, Completions, ConfigurationDone, Continue, Disconnect, Evaluate, Initialize, Launch, - LoadedSources, Modules, Next, Pause, Request as _, RunInTerminal, Scopes, SetBreakpoints, - SetExpression, SetVariable, StackTrace, StartDebugging, StepIn, StepOut, Terminate, - TerminateThreads, Variables, + LoadedSources, Modules, Next, Pause, Request as _, Restart, RunInTerminal, Scopes, + SetBreakpoints, SetExpression, SetVariable, StackTrace, StartDebugging, StepIn, StepOut, + Terminate, TerminateThreads, Variables, }; use dap::{ AttachRequestArguments, Capabilities, CompletionItem, CompletionsArguments, ConfigurationDoneArguments, ContinueArguments, DisconnectArguments, ErrorResponse, EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, InitializeRequestArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments, LoadedSourcesArguments, Module, - ModulesArguments, NextArguments, PauseArguments, RunInTerminalResponse, Scope, ScopesArguments, - SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, Source, - SourceBreakpoint, StackFrame, StackTraceArguments, StartDebuggingRequestArguments, - StepInArguments, StepOutArguments, SteppingGranularity, TerminateArguments, - TerminateThreadsArguments, Variable, VariablesArguments, + ModulesArguments, NextArguments, PauseArguments, RestartArguments, RunInTerminalResponse, + Scope, ScopesArguments, SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, + Source, SourceBreakpoint, StackFrame, StackTraceArguments, StartDebuggingRequestArguments, + StartDebuggingRequestArgumentsRequest, StepInArguments, StepOutArguments, SteppingGranularity, + TerminateArguments, TerminateThreadsArguments, Variable, VariablesArguments, }; use dap_adapters::build_adapter; use fs::Fs; @@ -29,7 +29,7 @@ use language::{ Buffer, BufferSnapshot, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName, }; use node_runtime::NodeRuntime; -use serde_json::{json, Value}; +use serde_json::Value; use settings::{Settings, WorktreeId}; use smol::lock::Mutex; use std::{ @@ -41,9 +41,9 @@ use std::{ Arc, }, }; -use task::{DebugAdapterConfig, DebugRequestType}; +use task::{AttachConfig, DebugAdapterConfig, DebugRequestType}; use text::Point; -use util::{maybe, merge_json_value_into, ResultExt}; +use util::{maybe, merge_json_value_into, ResultExt as _}; pub enum DapStoreEvent { DebugClientStarted(DebugAdapterClientId), @@ -238,12 +238,7 @@ impl DapStore { } } - pub fn start_client( - &mut self, - config: DebugAdapterConfig, - args: Option, - cx: &mut ModelContext, - ) { + pub fn start_client(&mut self, config: DebugAdapterConfig, cx: &mut ModelContext) { let client_id = self.next_client_id(); let adapter_delegate = self.delegate.clone(); @@ -251,11 +246,17 @@ impl DapStore { let dap_store = this.clone(); let client = maybe!(async { let adapter = Arc::new( - build_adapter(&config) + build_adapter(&config.kind) .await .context("Creating debug adapter")?, ); + if !adapter.supports_attach() + && matches!(config.request, DebugRequestType::Attach(_)) + { + return Err(anyhow!("Debug adapter does not support `attach` request")); + } + let path = cx.update(|cx| { let name = LanguageServerName::from(adapter.name().as_ref()); @@ -286,18 +287,7 @@ impl DapStore { } }; - let mut request_args = json!({}); - if let Some(config_args) = config.initialize_args.clone() { - merge_json_value_into(config_args, &mut request_args); - } - - merge_json_value_into(adapter.request_args(&config), &mut request_args); - - if let Some(args) = args { - merge_json_value_into(args.configuration, &mut request_args); - } - - let mut client = DebugAdapterClient::new(client_id, request_args, config, adapter); + let mut client = DebugAdapterClient::new(client_id, config, adapter); client .start( @@ -387,27 +377,58 @@ impl DapStore { store.capabilities.insert(client.id(), capabilities); cx.notify(); - })?; - - // send correct request based on adapter config - match client.config().request { - DebugRequestType::Launch => { - client - .request::(LaunchRequestArguments { - raw: client.request_args(), - }) - .await? - } - DebugRequestType::Attach => { - client - .request::(AttachRequestArguments { - raw: client.request_args(), - }) - .await? - } - } + }) + }) + } - Ok(()) + pub fn launch( + &mut self, + client_id: &DebugAdapterClientId, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Client was not found"))); + }; + + let mut adapter_args = client.adapter().request_args(&client.config()); + + if let Some(args) = client.config().initialize_args.clone() { + merge_json_value_into(args, &mut adapter_args); + } + + cx.background_executor().spawn(async move { + client + .request::(LaunchRequestArguments { raw: adapter_args }) + .await + }) + } + + pub fn attach( + &mut self, + client_id: &DebugAdapterClientId, + pid: u32, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Client was not found"))); + }; + + // update the process id on the config, so when the `startDebugging` reverse request + // comes in we send another `attach` request with the already selected PID + // If we don't do this the user has to select the process twice if the adapter sends a `startDebugging` request + client.set_process_id(pid); + + let config = client.config(); + let mut adapter_args = client.adapter().request_args(&config); + + if let Some(args) = config.initialize_args.clone() { + merge_json_value_into(args, &mut adapter_args); + } + + cx.background_executor().spawn(async move { + client + .request::(AttachRequestArguments { raw: adapter_args }) + .await }) } @@ -426,7 +447,7 @@ impl DapStore { return Task::ready(Ok(Vec::default())); } - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { Ok(client .request::(ModulesArguments { start_module: None, @@ -455,7 +476,7 @@ impl DapStore { return Task::ready(Ok(Vec::default())); } - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { Ok(client .request::(LoadedSourcesArguments {}) .await? @@ -473,7 +494,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Client was not found"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { Ok(client .request::(StackTraceArguments { thread_id, @@ -496,7 +517,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Client was not found"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { Ok(client .request::(ScopesArguments { frame_id: stack_frame_id, @@ -517,7 +538,7 @@ impl DapStore { let capabilities = self.capabilities_by_id(client_id); - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { let support_configuration_done_request = capabilities .supports_configuration_done_request .unwrap_or_default(); @@ -536,7 +557,7 @@ impl DapStore { &self, client_id: &DebugAdapterClientId, seq: u64, - args: Option, + args: StartDebuggingRequestArguments, cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { @@ -554,8 +575,33 @@ impl DapStore { })) .await?; + let config = client.config(); + this.update(&mut cx, |store, cx| { - store.start_client(client.config(), args, cx); + store.start_client( + DebugAdapterConfig { + kind: config.kind.clone(), + request: match args.request { + StartDebuggingRequestArgumentsRequest::Launch => { + DebugRequestType::Launch + } + StartDebuggingRequestArgumentsRequest::Attach => { + DebugRequestType::Attach( + if let DebugRequestType::Attach(attach_config) = config.request + { + attach_config + } else { + AttachConfig::default() + }, + ) + } + }, + program: config.program.clone(), + cwd: config.cwd.clone(), + initialize_args: Some(args.configuration), + }, + cx, + ); }) }) } @@ -572,31 +618,22 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { - if success { - client - .send_message(Message::Response(Response { - seq, - request_seq: seq, - success: true, - command: RunInTerminal::COMMAND.to_string(), - body: Some(serde_json::to_value(RunInTerminalResponse { + cx.background_executor().spawn(async move { + client + .send_message(Message::Response(Response { + seq, + request_seq: seq, + success, + command: RunInTerminal::COMMAND.to_string(), + body: match success { + true => Some(serde_json::to_value(RunInTerminalResponse { process_id: Some(std::process::id() as u64), shell_process_id: shell_pid, })?), - })) - .await - } else { - client - .send_message(Message::Response(Response { - seq, - request_seq: seq, - success: false, - command: RunInTerminal::COMMAND.to_string(), - body: Some(serde_json::to_value(ErrorResponse { error: None })?), - })) - .await - } + false => Some(serde_json::to_value(ErrorResponse { error: None })?), + }, + })) + .await }) } @@ -610,7 +647,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(ContinueArguments { thread_id, @@ -642,7 +679,7 @@ impl DapStore { .supports_stepping_granularity .unwrap_or_default(); - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(NextArguments { thread_id, @@ -673,7 +710,7 @@ impl DapStore { .supports_stepping_granularity .unwrap_or_default(); - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(StepInArguments { thread_id, @@ -705,7 +742,7 @@ impl DapStore { .supports_stepping_granularity .unwrap_or_default(); - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(StepOutArguments { thread_id, @@ -726,7 +763,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { Ok(client .request::(VariablesArguments { variables_reference, @@ -752,7 +789,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(EvaluateArguments { expression: expression.clone(), @@ -779,7 +816,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { Ok(client .request::(CompletionsArguments { frame_id: Some(stack_frame_id), @@ -812,7 +849,7 @@ impl DapStore { .supports_set_expression .unwrap_or_default(); - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { if let Some(evaluate_name) = supports_set_expression.then(|| evaluate_name).flatten() { client .request::(SetExpressionArguments { @@ -847,7 +884,8 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { client.request::(PauseArguments { thread_id }).await }) + cx.background_executor() + .spawn(async move { client.request::(PauseArguments { thread_id }).await }) } pub fn terminate_threads( @@ -866,7 +904,7 @@ impl DapStore { .supports_terminate_threads_request .unwrap_or_default() { - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(TerminateThreadsArguments { thread_ids }) .await @@ -885,7 +923,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(DisconnectArguments { restart: Some(false), @@ -906,28 +944,24 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - let restart_args = args.unwrap_or(Value::Null); + let supports_restart = self + .capabilities_by_id(client_id) + .supports_restart_request + .unwrap_or_default(); - cx.spawn(|_, _| async move { - client - .request::(DisconnectArguments { - restart: Some(true), - terminate_debuggee: Some(false), - suspend_debuggee: Some(false), - }) - .await?; + let raw = args.unwrap_or(Value::Null); - match client.request_type() { - DebugRequestType::Launch => { - client - .request::(LaunchRequestArguments { raw: restart_args }) - .await? - } - DebugRequestType::Attach => { - client - .request::(AttachRequestArguments { raw: restart_args }) - .await? - } + cx.background_executor().spawn(async move { + if supports_restart { + client.request::(RestartArguments { raw }).await?; + } else { + client + .request::(DisconnectArguments { + restart: Some(false), + terminate_debuggee: Some(true), + suspend_debuggee: Some(false), + }) + .await?; } Ok(()) @@ -959,9 +993,7 @@ impl DapStore { let capabilities = self.capabilities.remove(client_id); - cx.notify(); - - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { let client = match client { DebugAdapterClientState::Starting(task) => task.await, DebugAdapterClientState::Running(client) => Some(client), @@ -1026,7 +1058,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(SetBreakpointsArguments { source: Source { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 85e0fa4faa844a..6367191365db67 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -85,6 +85,7 @@ use std::{ sync::Arc, time::Duration, }; + use task_store::TaskStore; use terminals::Terminals; use text::{Anchor, BufferId}; @@ -1224,8 +1225,9 @@ impl Project { cx: &mut ModelContext, ) { if let Some(adapter_config) = debug_task.debug_adapter_config() { - self.dap_store - .update(cx, |store, cx| store.start_client(adapter_config, None, cx)); + self.dap_store.update(cx, |store, cx| { + store.start_client(adapter_config, cx); + }); } } @@ -2270,10 +2272,6 @@ impl Project { match event { DapStoreEvent::DebugClientStarted(client_id) => { cx.emit(Event::DebugClientStarted(*client_id)); - - self.dap_store.update(cx, |store, cx| { - store.initialize(client_id, cx).detach_and_log_err(cx); - }); } DapStoreEvent::DebugClientStopped(client_id) => { cx.emit(Event::DebugClientStopped(*client_id)); diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index c71b0ee15416bb..f590e865f32934 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -37,20 +37,28 @@ impl TCPHost { } } +/// Represents the attach request information of the debug adapter +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +pub struct AttachConfig { + /// The processId to attach to, if left empty we will show a process picker + #[serde(default)] + pub process_id: Option, +} + /// Represents the type that will determine which request to call on the debug adapter #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(rename_all = "lowercase")] +#[serde(rename_all = "lowercase", tag = "request")] pub enum DebugRequestType { /// Call the `launch` request on the debug adapter #[default] Launch, /// Call the `attach` request on the debug adapter - Attach, + Attach(AttachConfig), } /// The Debug adapter to use #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(rename_all = "lowercase", tag = "kind")] +#[serde(rename_all = "lowercase", tag = "adapter")] pub enum DebugAdapterKind { /// Manually setup starting a debug adapter /// The argument within is used to start the DAP @@ -58,7 +66,7 @@ pub enum DebugAdapterKind { /// Use debugpy Python(TCPHost), /// Use vscode-php-debug - PHP(TCPHost), + Php(TCPHost), /// Use vscode-js-debug Javascript(TCPHost), /// Use lldb @@ -71,7 +79,7 @@ impl DebugAdapterKind { match self { Self::Custom(_) => "Custom", Self::Python(_) => "Python", - Self::PHP(_) => "PHP", + Self::Php(_) => "PHP", Self::Javascript(_) => "JavaScript", Self::Lldb => "LLDB", } @@ -96,13 +104,11 @@ pub struct CustomArgs { #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub struct DebugAdapterConfig { - /// Unique id of for the debug adapter, - /// that will be send with the `initialize` request + /// The type of adapter you want to use #[serde(flatten)] pub kind: DebugAdapterKind, - /// The type of connection the adapter should use /// The type of request that should be called on the debug adapter - #[serde(default)] + #[serde(default, flatten)] pub request: DebugRequestType, /// The program that you trying to debug pub program: Option, @@ -125,17 +131,18 @@ pub enum DebugConnectionType { #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub struct DebugTaskDefinition { - /// Name of the debug tasks + /// Name of the debug task label: String, /// Program to run the debugger on program: Option, /// The current working directory of your project cwd: Option, - /// Launch | Request depending on the session the adapter should be ran as - #[serde(default)] - session_type: DebugRequestType, + /// The type of request that should be called on the debug adapter + #[serde(default, flatten)] + request: DebugRequestType, /// The adapter to run - adapter: DebugAdapterKind, + #[serde(flatten)] + kind: DebugAdapterKind, /// Additional initialization arguments to be sent on DAP initialization initialize_args: Option, } @@ -146,8 +153,8 @@ impl DebugTaskDefinition { let cwd = self.cwd.clone().map(PathBuf::from).take_if(|p| p.exists()); let task_type = TaskType::Debug(DebugAdapterConfig { - kind: self.adapter, - request: self.session_type, + kind: self.kind, + request: self.request, program: self.program, cwd: cwd.clone(), initialize_args: self.initialize_args, diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index ad02b367c09359..e7e05bd5666aec 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -15,8 +15,8 @@ use std::path::PathBuf; use std::str::FromStr; pub use debug_format::{ - CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, DebugRequestType, - DebugTaskFile, TCPHost, + AttachConfig, CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, + DebugRequestType, DebugTaskFile, TCPHost, }; pub use task_template::{ HideStrategy, RevealStrategy, TaskModal, TaskTemplate, TaskTemplates, TaskType, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 876306175c1e0d..b4308fbfb88281 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -158,7 +158,7 @@ actions!( ReloadActiveItem, SaveAs, SaveWithoutFormat, - StopDebugAdapters, + ShutdownDebugAdapters, ToggleBottomDock, ToggleCenteredLayout, ToggleLeftDock,