diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 4e37162e06..afef80ce00 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -7,7 +7,6 @@ use crate::{ }; use crate::{new_session_modal::NewSessionModal, session::DebugSession}; use anyhow::{Result, anyhow}; -use collections::HashMap; use command_palette_hooks::CommandPaletteFilter; use dap::DebugRequest; use dap::{ @@ -15,7 +14,6 @@ use dap::{ 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, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity, @@ -24,21 +22,11 @@ use gpui::{ use language::Buffer; use project::debugger::session::{Session, SessionStateEvent}; -use project::{ - Project, - debugger::{ - dap_store::{self, DapStore}, - session::ThreadStatus, - }, - terminals::TerminalKind, -}; +use project::{Project, debugger::session::ThreadStatus}; use rpc::proto::{self}; use settings::Settings; use std::any::TypeId; -use std::path::Path; -use std::sync::Arc; -use task::{DebugScenario, HideStrategy, RevealStrategy, RevealTarget, TaskContext, TaskId}; -use terminal_view::TerminalView; +use task::{DebugScenario, TaskContext}; use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*}; use workspace::SplitDirection; use workspace::{ @@ -74,27 +62,21 @@ pub struct DebugPanel { workspace: WeakEntity, focus_handle: FocusHandle, context_menu: Option<(Entity, Point, Subscription)>, - _subscriptions: Vec, } impl DebugPanel { pub fn new( workspace: &Workspace, - window: &mut Window, + _window: &mut Window, cx: &mut Context, ) -> Entity { cx.new(|cx| { let project = workspace.project().clone(); - let dap_store = project.read(cx).dap_store(); - - let _subscriptions = - vec![cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event)]; let debug_panel = Self { size: px(300.), sessions: vec![], active_session: None, - _subscriptions, past_debug_definition: None, focus_handle: cx.focus_handle(), project, @@ -288,7 +270,7 @@ impl DebugPanel { cx.subscribe_in( &session, window, - move |_, session, event: &SessionStateEvent, window, cx| match event { + move |this, session, event: &SessionStateEvent, window, cx| match event { SessionStateEvent::Restart => { let mut curr_session = session.clone(); while let Some(parent_session) = curr_session @@ -310,6 +292,9 @@ impl DebugPanel { }) .detach_and_log_err(cx); } + SessionStateEvent::SpawnChildSession { request } => { + this.handle_start_debugging_request(request, session.clone(), window, cx); + } _ => {} }, ) @@ -357,7 +342,7 @@ impl DebugPanel { Ok(()) } - pub fn start_child_session( + pub fn handle_start_debugging_request( &mut self, request: &StartDebuggingRequestArguments, parent_session: Entity, @@ -419,47 +404,6 @@ impl DebugPanel { self.active_session.clone() } - fn handle_dap_store_event( - &mut self, - _dap_store: &Entity, - event: &dap_store::DapStoreEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - dap_store::DapStoreEvent::RunInTerminal { - session_id, - title, - cwd, - command, - args, - envs, - sender, - .. - } => { - self.handle_run_in_terminal_request( - *session_id, - title.clone(), - cwd.clone(), - command.clone(), - args.clone(), - envs.clone(), - sender.clone(), - window, - cx, - ) - .detach_and_log_err(cx); - } - dap_store::DapStoreEvent::SpawnChildSession { - request, - parent_session, - } => { - self.start_child_session(request, parent_session.clone(), window, cx); - } - _ => {} - } - } - pub fn resolve_scenario( &self, scenario: DebugScenario, @@ -529,101 +473,6 @@ impl DebugPanel { }) } - fn handle_run_in_terminal_request( - &self, - session_id: SessionId, - title: Option, - cwd: Option>, - command: Option, - args: Vec, - envs: HashMap, - mut sender: mpsc::Sender>, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let Some(session) = self - .sessions - .iter() - .find(|s| s.read(cx).session_id(cx) == session_id) - else { - return Task::ready(Err(anyhow!("no session {:?} found", session_id))); - }; - let running = session.read(cx).running_state(); - let cwd = cwd.map(|p| p.to_path_buf()); - let shell = self - .project - .read(cx) - .terminal_settings(&cwd, cx) - .shell - .clone(); - let kind = if let Some(command) = command { - let title = title.clone().unwrap_or(command.clone()); - TerminalKind::Task(task::SpawnInTerminal { - id: TaskId("debug".to_string()), - full_label: title.clone(), - label: title.clone(), - command: command.clone(), - args, - command_label: title.clone(), - cwd, - env: envs, - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: RevealStrategy::NoFocus, - reveal_target: RevealTarget::Dock, - hide: HideStrategy::Never, - shell, - show_summary: false, - show_command: false, - show_rerun: false, - }) - } else { - TerminalKind::Shell(cwd.map(|c| c.to_path_buf())) - }; - - let workspace = self.workspace.clone(); - let project = self.project.downgrade(); - - let terminal_task = self.project.update(cx, |project, cx| { - project.create_terminal(kind, window.window_handle(), cx) - }); - let terminal_task = cx.spawn_in(window, async move |_, cx| { - let terminal = terminal_task.await?; - - let terminal_view = cx.new_window_entity(|window, cx| { - TerminalView::new(terminal.clone(), workspace, None, project, window, cx) - })?; - - running.update_in(cx, |running, window, cx| { - running.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx); - running.debug_terminal.update(cx, |debug_terminal, cx| { - debug_terminal.terminal = Some(terminal_view); - cx.notify(); - }); - })?; - - anyhow::Ok(terminal.read_with(cx, |terminal, _| terminal.pty_info.pid())?) - }); - - cx.background_spawn(async move { - match terminal_task.await { - Ok(pid_task) => match pid_task { - Some(pid) => sender.send(Ok(pid.as_u32())).await?, - None => { - sender - .send(Err(anyhow!( - "Terminal was spawned but PID was not available" - ))) - .await? - } - }, - Err(error) => sender.send(Err(anyhow!(error))).await?, - }; - - Ok(()) - }) - } - fn close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context) { let Some(session) = self .sessions diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 756f866bf1..ece2b69407 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -104,12 +104,6 @@ impl DebugSession { &self.mode } - pub(crate) fn running_state(&self) -> Entity { - match &self.mode { - DebugSessionState::Running(running_state) => running_state.clone(), - } - } - pub(crate) fn label(&self, cx: &App) -> SharedString { if let Some(label) = self.label.get() { return label.clone(); @@ -131,6 +125,13 @@ impl DebugSession { .to_owned() } + #[allow(unused)] + pub(crate) fn running_state(&self) -> Entity { + match &self.mode { + DebugSessionState::Running(running_state) => running_state.clone(), + } + } + pub(crate) fn label_element(&self, cx: &App) -> AnyElement { let label = self.label(cx); diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index a48edd9507..f22fe77afe 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -5,15 +5,20 @@ pub(crate) mod module_list; pub mod stack_frame_list; pub mod variable_list; -use std::{any::Any, ops::ControlFlow, sync::Arc, time::Duration}; +use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration}; use crate::persistence::{self, DebuggerPaneItem, SerializedPaneLayout}; use super::DebugPanelItemEvent; +use anyhow::{Result, anyhow}; use breakpoint_list::BreakpointList; use collections::{HashMap, IndexMap}; use console::Console; -use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings}; +use dap::{ + Capabilities, RunInTerminalRequestArguments, Thread, client::SessionId, + debugger_settings::DebuggerSettings, +}; +use futures::{SinkExt, channel::mpsc}; use gpui::{ Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable, NoAction, Pixels, Point, Subscription, Task, WeakEntity, @@ -23,8 +28,10 @@ use module_list::ModuleList; use project::{ Project, debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus}, + terminals::TerminalKind, }; use rpc::proto::ViewId; +use serde_json::Value; use settings::Settings; use stack_frame_list::StackFrameList; use terminal_view::TerminalView; @@ -32,7 +39,7 @@ use ui::{ ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, ContextMenu, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon as _, ParentElement, Render, SharedString, StatefulInteractiveElement, - Styled, Tab, Tooltip, VisibleOnHover, Window, div, h_flex, v_flex, + Styled, Tab, Tooltip, VisibleOnHover, VisualContext, Window, div, h_flex, v_flex, }; use util::ResultExt; use variable_list::VariableList; @@ -559,6 +566,9 @@ impl RunningState { this.remove_pane_item(DebuggerPaneItem::LoadedSources, window, cx); } } + SessionEvent::RunInTerminal { request, sender } => this + .handle_run_in_terminal(request, sender.clone(), window, cx) + .detach_and_log_err(cx), _ => {} } @@ -657,6 +667,111 @@ impl RunningState { self.panes.pane_at_pixel_position(position).is_some() } + fn handle_run_in_terminal( + &self, + request: &RunInTerminalRequestArguments, + mut sender: mpsc::Sender>, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let running = cx.entity(); + let Ok(project) = self + .workspace + .update(cx, |workspace, _| workspace.project().clone()) + else { + return Task::ready(Err(anyhow!("no workspace"))); + }; + let session = self.session.read(cx); + + let cwd = Some(&request.cwd) + .filter(|cwd| cwd.len() > 0) + .map(PathBuf::from) + .or_else(|| session.binary().cwd.clone()); + + let mut args = request.args.clone(); + + // Handle special case for NodeJS debug adapter + // If only the Node binary path is provided, we set the command to None + // This prevents the NodeJS REPL from appearing, which is not the desired behavior + // The expected usage is for users to provide their own Node command, e.g., `node test.js` + // This allows the NodeJS debug client to attach correctly + let command = if args.len() > 1 { + Some(args.remove(0)) + } else { + None + }; + + let mut envs: HashMap = Default::default(); + if let Some(Value::Object(env)) = &request.env { + for (key, value) in env { + let value_str = match (key.as_str(), value) { + (_, Value::String(value)) => value, + _ => continue, + }; + + envs.insert(key.clone(), value_str.clone()); + } + } + + let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone(); + let kind = if let Some(command) = command { + let title = request.title.clone().unwrap_or(command.clone()); + TerminalKind::Task(task::SpawnInTerminal { + id: task::TaskId("debug".to_string()), + full_label: title.clone(), + label: title.clone(), + command: command.clone(), + args, + command_label: title.clone(), + cwd, + env: envs, + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: task::RevealStrategy::NoFocus, + reveal_target: task::RevealTarget::Dock, + hide: task::HideStrategy::Never, + shell, + show_summary: false, + show_command: false, + show_rerun: false, + }) + } else { + TerminalKind::Shell(cwd.map(|c| c.to_path_buf())) + }; + + let workspace = self.workspace.clone(); + let weak_project = project.downgrade(); + + let terminal_task = project.update(cx, |project, cx| { + project.create_terminal(kind, window.window_handle(), cx) + }); + let terminal_task = cx.spawn_in(window, async move |_, cx| { + let terminal = terminal_task.await?; + + let terminal_view = cx.new_window_entity(|window, cx| { + TerminalView::new(terminal.clone(), workspace, None, weak_project, window, cx) + })?; + + running.update_in(cx, |running, window, cx| { + running.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx); + running.debug_terminal.update(cx, |debug_terminal, cx| { + debug_terminal.terminal = Some(terminal_view); + cx.notify(); + }); + })?; + + terminal.read_with(cx, |terminal, _| { + terminal + .pty_info + .pid() + .map(|pid| pid.as_u32()) + .ok_or_else(|| anyhow!("Terminal was spawned but PID was not available")) + })? + }); + + cx.background_spawn(async move { anyhow::Ok(sender.send(terminal_task.await).await?) }) + } + fn create_sub_view( &self, item_kind: DebuggerPaneItem, diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index d193b8046a..b98a601205 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -523,8 +523,8 @@ async fn test_handle_error_run_in_terminal_reverse_request( .fake_reverse_request::(RunInTerminalRequestArguments { kind: None, title: None, - cwd: path!("/non-existing/path").into(), // invalid/non-existing path will cause the terminal spawn to fail - args: vec![], + cwd: "".into(), + args: vec!["oops".into(), "oops".into()], env: None, args_can_be_interpreted_by_shell: None, }) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 807e0a8689..f0c90c63a7 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -15,20 +15,16 @@ use async_trait::async_trait; use collections::HashMap; use dap::{ Capabilities, CompletionItem, CompletionsArguments, DapRegistry, DebugRequest, - EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments, - Source, StackFrameId, StartDebuggingRequestArguments, + EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, Source, StackFrameId, adapters::{ DapStatus, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments, }, client::SessionId, messages::Message, - requests::{Completions, Evaluate, Request as _, RunInTerminal, StartDebugging}, + requests::{Completions, Evaluate}, }; use fs::Fs; -use futures::{ - channel::mpsc, - future::{Shared, join_all}, -}; +use futures::future::{Shared, join_all}; use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task}; use http_client::HttpClient; use language::{ @@ -43,9 +39,8 @@ use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self}, }; -use serde_json::Value; use settings::{Settings, WorktreeId}; -use smol::{lock::Mutex, stream::StreamExt}; +use smol::lock::Mutex; use std::{ borrow::Borrow, collections::{BTreeMap, HashSet}, @@ -67,19 +62,6 @@ pub enum DapStoreEvent { session_id: SessionId, message: Message, }, - RunInTerminal { - session_id: SessionId, - title: Option, - cwd: Option>, - command: Option, - args: Vec, - envs: HashMap, - sender: mpsc::Sender>, - }, - SpawnChildSession { - request: StartDebuggingRequestArguments, - parent_session: Entity, - }, Notification(String), RemoteHasInitialized, } @@ -113,8 +95,6 @@ pub struct DapStore { worktree_store: Entity, sessions: BTreeMap>, next_session_id: u32, - start_debugging_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>, - _start_debugging_task: Task<()>, } impl EventEmitter for DapStore {} @@ -184,35 +164,10 @@ impl DapStore { mode: DapStoreMode, breakpoint_store: Entity, worktree_store: Entity, - cx: &mut Context, + _cx: &mut Context, ) -> Self { - let (start_debugging_tx, mut message_rx) = - futures::channel::mpsc::unbounded::<(SessionId, Message)>(); - let task = cx.spawn(async move |this, cx| { - while let Some((session_id, message)) = message_rx.next().await { - match message { - Message::Request(request) => { - let _ = this - .update(cx, |this, cx| { - if request.command == StartDebugging::COMMAND { - this.handle_start_debugging_request(session_id, request, cx) - .detach_and_log_err(cx); - } else if request.command == RunInTerminal::COMMAND { - this.handle_run_in_terminal_request(session_id, request, cx) - .detach_and_log_err(cx); - } - }) - .log_err(); - } - _ => {} - } - } - }); - Self { mode, - _start_debugging_task: task, - start_debugging_tx, next_session_id: 0, downstream_client: None, breakpoint_store, @@ -450,14 +405,11 @@ impl DapStore { }); } - let start_debugging_tx = self.start_debugging_tx.clone(); - let session = Session::new( self.breakpoint_store.clone(), session_id, parent_session, template.clone(), - start_debugging_tx, cx, ); @@ -469,7 +421,7 @@ impl DapStore { SessionStateEvent::Shutdown => { this.shutdown_session(session_id, cx).detach_and_log_err(cx); } - SessionStateEvent::Restart => {} + SessionStateEvent::Restart | SessionStateEvent::SpawnChildSession { .. } => {} SessionStateEvent::Running => { cx.emit(DapStoreEvent::DebugClientStarted(session_id)); } @@ -583,196 +535,6 @@ impl DapStore { ) } - fn handle_start_debugging_request( - &mut self, - session_id: SessionId, - request: dap::messages::Request, - cx: &mut Context, - ) -> Task> { - let Some(parent_session) = self.session_by_id(session_id) else { - return Task::ready(Err(anyhow!("Session not found"))); - }; - let request_seq = request.seq; - - let launch_request: Option> = request - .arguments - .as_ref() - .map(|value| serde_json::from_value(value.clone())); - - let mut success = true; - if let Some(Ok(request)) = launch_request { - cx.emit(DapStoreEvent::SpawnChildSession { - request, - parent_session: parent_session.clone(), - }); - } else { - log::error!( - "Failed to parse launch request arguments: {:?}", - request.arguments - ); - success = false; - } - - cx.spawn(async move |_, cx| { - parent_session - .update(cx, |session, cx| { - session.respond_to_client( - request_seq, - success, - StartDebugging::COMMAND.to_string(), - None, - cx, - ) - })? - .await - }) - } - - fn handle_run_in_terminal_request( - &mut self, - session_id: SessionId, - request: dap::messages::Request, - cx: &mut Context, - ) -> Task> { - let Some(session) = self.session_by_id(session_id) else { - return Task::ready(Err(anyhow!("Session not found"))); - }; - - let request_args = serde_json::from_value::( - request.arguments.unwrap_or_default(), - ) - .expect("To parse StartDebuggingRequestArguments"); - - let seq = request.seq; - - let cwd = Path::new(&request_args.cwd); - - match cwd.try_exists() { - Ok(false) | Err(_) if !request_args.cwd.is_empty() => { - return session.update(cx, |session, cx| { - session.respond_to_client( - seq, - false, - RunInTerminal::COMMAND.to_string(), - serde_json::to_value(dap::ErrorResponse { - error: Some(dap::Message { - id: seq, - format: format!("Received invalid/unknown cwd: {cwd:?}"), - variables: None, - send_telemetry: None, - show_user: None, - url: None, - url_label: None, - }), - }) - .ok(), - cx, - ) - }); - } - _ => (), - } - let mut args = request_args.args.clone(); - - // Handle special case for NodeJS debug adapter - // If only the Node binary path is provided, we set the command to None - // This prevents the NodeJS REPL from appearing, which is not the desired behavior - // The expected usage is for users to provide their own Node command, e.g., `node test.js` - // This allows the NodeJS debug client to attach correctly - let command = if args.len() > 1 { - Some(args.remove(0)) - } else { - None - }; - - let mut envs: HashMap = Default::default(); - if let Some(Value::Object(env)) = request_args.env { - for (key, value) in env { - let value_str = match (key.as_str(), value) { - (_, Value::String(value)) => value, - _ => continue, - }; - - envs.insert(key, value_str); - } - } - - let (tx, mut rx) = mpsc::channel::>(1); - let cwd = Some(cwd) - .filter(|cwd| cwd.as_os_str().len() > 0) - .map(Arc::from) - .or_else(|| { - self.session_by_id(session_id) - .and_then(|session| session.read(cx).binary().cwd.as_deref().map(Arc::from)) - }); - cx.emit(DapStoreEvent::RunInTerminal { - session_id, - title: request_args.title, - cwd, - command, - args, - envs, - sender: tx, - }); - cx.notify(); - - let session = session.downgrade(); - cx.spawn(async move |_, cx| { - let (success, body) = match rx.next().await { - Some(Ok(pid)) => ( - true, - serde_json::to_value(dap::RunInTerminalResponse { - process_id: None, - shell_process_id: Some(pid as u64), - }) - .ok(), - ), - Some(Err(error)) => ( - false, - serde_json::to_value(dap::ErrorResponse { - error: Some(dap::Message { - id: seq, - format: error.to_string(), - variables: None, - send_telemetry: None, - show_user: None, - url: None, - url_label: None, - }), - }) - .ok(), - ), - None => ( - false, - serde_json::to_value(dap::ErrorResponse { - error: Some(dap::Message { - id: seq, - format: "failed to receive response from spawn terminal".to_string(), - variables: None, - send_telemetry: None, - show_user: None, - url: None, - url_label: None, - }), - }) - .ok(), - ), - }; - - session - .update(cx, |session, cx| { - session.respond_to_client( - seq, - success, - RunInTerminal::COMMAND.to_string(), - body, - cx, - ) - })? - .await - }) - } - pub fn evaluate( &self, session_id: &SessionId, diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index d831abc10e..a602cb5e76 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -14,14 +14,18 @@ use anyhow::{Context as _, Result, anyhow}; use collections::{HashMap, HashSet, IndexMap, IndexSet}; use dap::adapters::{DebugAdapterBinary, DebugTaskDefinition}; use dap::messages::Response; +use dap::requests::{Request, RunInTerminal, StartDebugging}; use dap::{ Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, StackFrameId, SteppingGranularity, StoppedEvent, VariableReference, client::{DebugAdapterClient, SessionId}, messages::{Events, Message}, }; -use dap::{ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEventCategory}; -use futures::channel::oneshot; +use dap::{ + ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEventCategory, + RunInTerminalRequestArguments, StartDebuggingRequestArguments, +}; +use futures::channel::{mpsc, oneshot}; use futures::{FutureExt, future::Shared}; use gpui::{ App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, SharedString, @@ -522,7 +526,6 @@ pub struct Session { is_session_terminated: bool, requests: HashMap>>>>, exception_breakpoints: BTreeMap, - start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>, background_tasks: Vec>, } @@ -608,13 +611,20 @@ pub enum SessionEvent { Threads, InvalidateInlineValue, CapabilitiesLoaded, + RunInTerminal { + request: RunInTerminalRequestArguments, + sender: mpsc::Sender>, + }, } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum SessionStateEvent { Running, Shutdown, Restart, + SpawnChildSession { + request: StartDebuggingRequestArguments, + }, } impl EventEmitter for Session {} @@ -629,7 +639,6 @@ impl Session { session_id: SessionId, parent_session: Option>, template: DebugTaskDefinition, - start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>, cx: &mut App, ) -> Entity { cx.new::(|cx| { @@ -678,7 +687,6 @@ impl Session { is_session_terminated: false, exception_breakpoints: Default::default(), definition: template, - start_debugging_requests_tx, }; this @@ -702,7 +710,6 @@ impl Session { ) -> Task> { let (message_tx, mut message_rx) = futures::channel::mpsc::unbounded(); let (initialized_tx, initialized_rx) = futures::channel::oneshot::channel(); - let session_id = self.session_id(); let background_tasks = vec![cx.spawn(async move |this: WeakEntity, cx| { let mut initialized_tx = Some(initialized_tx); @@ -719,10 +726,15 @@ impl Session { break; }; } - } else { - let Ok(Ok(_)) = this.update(cx, |this, _| { - this.start_debugging_requests_tx - .unbounded_send((session_id, message)) + } else if let Message::Request(request) = message { + let Ok(_) = this.update(cx, |this, cx| { + if request.command == StartDebugging::COMMAND { + this.handle_start_debugging_request(request, cx) + .detach_and_log_err(cx); + } else if request.command == RunInTerminal::COMMAND { + this.handle_run_in_terminal_request(request, cx) + .detach_and_log_err(cx); + } }) else { break; }; @@ -830,6 +842,109 @@ impl Session { } } + fn handle_start_debugging_request( + &mut self, + request: dap::messages::Request, + cx: &mut Context, + ) -> Task> { + let request_seq = request.seq; + + let launch_request: Option> = request + .arguments + .as_ref() + .map(|value| serde_json::from_value(value.clone())); + + let mut success = true; + if let Some(Ok(request)) = launch_request { + cx.emit(SessionStateEvent::SpawnChildSession { request }); + } else { + log::error!( + "Failed to parse launch request arguments: {:?}", + request.arguments + ); + success = false; + } + + cx.spawn(async move |this, cx| { + this.update(cx, |this, cx| { + this.respond_to_client( + request_seq, + success, + StartDebugging::COMMAND.to_string(), + None, + cx, + ) + })? + .await + }) + } + + fn handle_run_in_terminal_request( + &mut self, + request: dap::messages::Request, + cx: &mut Context, + ) -> Task> { + let request_args = serde_json::from_value::( + request.arguments.unwrap_or_default(), + ) + .expect("To parse StartDebuggingRequestArguments"); + + let seq = request.seq; + + let (tx, mut rx) = mpsc::channel::>(1); + cx.emit(SessionEvent::RunInTerminal { + request: request_args, + sender: tx, + }); + cx.notify(); + + cx.spawn(async move |session, cx| { + let result = util::maybe!(async move { + rx.next().await.ok_or_else(|| { + anyhow!("failed to receive response from spawn terminal".to_string()) + })? + }) + .await; + let (success, body) = match result { + Ok(pid) => ( + true, + serde_json::to_value(dap::RunInTerminalResponse { + process_id: None, + shell_process_id: Some(pid as u64), + }) + .ok(), + ), + Err(error) => ( + false, + serde_json::to_value(dap::ErrorResponse { + error: Some(dap::Message { + id: seq, + format: error.to_string(), + variables: None, + send_telemetry: None, + show_user: None, + url: None, + url_label: None, + }), + }) + .ok(), + ), + }; + + session + .update(cx, |session, cx| { + session.respond_to_client( + seq, + success, + RunInTerminal::COMMAND.to_string(), + body, + cx, + ) + })? + .await + }) + } + pub(super) fn request_initialize(&mut self, cx: &mut Context) -> Task> { let adapter_id = String::from(self.definition.adapter.clone()); let request = Initialize { adapter_id };