diff --git a/crates/collab/src/db/tables/debug_clients.rs b/crates/collab/src/db/tables/debug_clients.rs index e27febacbba831..9c6d48346ace8a 100644 --- a/crates/collab/src/db/tables/debug_clients.rs +++ b/crates/collab/src/db/tables/debug_clients.rs @@ -11,6 +11,7 @@ const SUPPORTS_SINGLE_THREAD_EXECUTION_REQUESTS_BIT: u32 = 4; const SUPPORTS_STEP_BACK_BIT: u32 = 5; const SUPPORTS_STEPPING_GRANULARITY_BIT: u32 = 6; const SUPPORTS_TERMINATE_THREADS_REQUEST_BIT: u32 = 7; +const SUPPORTS_RESTART_FRAME_REQUEST_BIT: u32 = 8; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "debug_clients")] @@ -49,6 +50,9 @@ impl Model { supports_terminate_threads_request: (self.capabilities & (1 << SUPPORTS_TERMINATE_THREADS_REQUEST_BIT)) != 0, + supports_restart_frame_request: (self.capabilities + & (1 << SUPPORTS_RESTART_FRAME_REQUEST_BIT)) + != 0, } } @@ -69,6 +73,8 @@ impl Model { << SUPPORTS_STEPPING_GRANULARITY_BIT; capabilities_bit_mask |= (capabilities.supports_terminate_threads_request as i32) << SUPPORTS_TERMINATE_THREADS_REQUEST_BIT; + capabilities_bit_mask |= (capabilities.supports_restart_frame_request as i32) + << SUPPORTS_RESTART_FRAME_REQUEST_BIT; self.capabilities = capabilities_bit_mask; } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 357cbb0489c568..ef8789586546af 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -434,7 +434,10 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) - .add_message_handler(broadcast_project_message_from_host::); + .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler( + broadcast_project_message_from_host::, + ); Arc::new(server) } diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index c1d1425f2d6946..3c5a9e85ef7b3f 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -1,7 +1,14 @@ use call::ActiveCall; -use dap::requests::{Disconnect, Initialize, Launch, StackTrace}; +use dap::{ + requests::{Disconnect, Initialize, Launch, RestartFrame, StackTrace}, + StackFrame, +}; use debugger_ui::debugger_panel::DebugPanel; use gpui::{TestAppContext, View, VisualTestContext}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; use workspace::{dock::Panel, Workspace}; use super::TestServer; @@ -651,3 +658,172 @@ async fn test_debug_panel_remote_button_presses( }); }); } + +#[gpui::test] +async fn test_restart_stack_frame(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + init_test(cx_a); + init_test(cx_b); + + let called_restart_frame = Arc::new(AtomicBool::new(false)); + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + let (project_a, _worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + add_debugger_panel(&workspace_a, cx_a).await; + add_debugger_panel(&workspace_b, cx_b).await; + + let task = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_restart_frame: Some(true), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + let stack_frames = vec![StackFrame { + id: 1, + name: "Stack Frame 1".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some("/project/src/test.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 3, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }]; + + client + .on_request::({ + let stack_frames = Arc::new(stack_frames.clone()); + move |_, args| { + assert_eq!(1, args.thread_id); + + Ok(dap::StackTraceResponse { + stack_frames: (*stack_frames).clone(), + total_frames: None, + }) + } + }) + .await; + + client + .on_request::({ + let called_restart_frame = called_restart_frame.clone(); + move |_, args| { + assert_eq!(1, args.frame_id); + + called_restart_frame.store(true, Ordering::SeqCst); + + Ok(()) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + // try to restart stack frame 1 from the guest side + workspace_b.update(cx_b, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + debug_panel_item + .stack_frame_list() + .update(cx, |stack_frame_list, cx| { + stack_frame_list.restart_stack_frame(1, cx); + }); + }); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + assert!( + called_restart_frame.load(std::sync::atomic::Ordering::SeqCst), + "Restart stack frame was not called" + ); + + let shutdown_client = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_client.await.unwrap(); +} diff --git a/crates/dap/src/proto_conversions.rs b/crates/dap/src/proto_conversions.rs index 2532060e0cc822..15bf89638d0da9 100644 --- a/crates/dap/src/proto_conversions.rs +++ b/crates/dap/src/proto_conversions.rs @@ -344,6 +344,7 @@ pub fn capabilities_from_proto(payload: &SetDebugClientCapabilities) -> Capabili supports_step_back: Some(payload.supports_step_back), supports_stepping_granularity: Some(payload.supports_stepping_granularity), supports_terminate_threads_request: Some(payload.supports_terminate_threads_request), + supports_restart_frame: Some(payload.supports_restart_frame_request), ..Default::default() } } @@ -374,6 +375,7 @@ pub fn capabilities_to_proto( supports_terminate_threads_request: capabilities .supports_terminate_threads_request .unwrap_or_default(), + supports_restart_frame_request: capabilities.supports_restart_frame.unwrap_or_default(), } } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 437a3de8654d86..9a82798521adeb 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -232,7 +232,6 @@ impl DebugPanelItem { }); self.active_thread_item = ThreadItem::from_proto(state.active_thread_item()); - // self.update_thread_state_status(ThreadStatus::Stopped, cx); // This is a band aid fix for thread status not being sent correctly all the time if let Some(stack_frame_list) = state.stack_frame_list.as_ref() { self.stack_frame_list.update(cx, |this, cx| { diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 52e81ab9f822cf..8e53b73fc4b5a0 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -239,6 +239,14 @@ impl StackFrameList { .ok()? } + pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut ViewContext) { + self.dap_store.update(cx, |store, cx| { + store + .restart_stack_frame(&self.client_id, stack_frame_id, cx) + .detach_and_log_err(cx); + }); + } + fn render_entry(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { let stack_frame = &self.stack_frames[ix]; @@ -251,8 +259,16 @@ impl StackFrameList { stack_frame.line, ); - v_flex() + let supports_frame_restart = self + .dap_store + .read(cx) + .capabilities_by_id(&self.client_id) + .supports_restart_frame + .unwrap_or_default(); + + h_flex() .rounded_md() + .justify_between() .w_full() .group("") .id(("stack-frame", stack_frame.id)) @@ -271,20 +287,58 @@ impl StackFrameList { .detach_and_log_err(cx); } })) - .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) + .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer()) .child( - h_flex() - .gap_0p5() - .text_ui_sm(cx) - .child(stack_frame.name.clone()) - .child(formatted_path), - ) - .child( - h_flex() - .text_ui_xs(cx) - .text_color(cx.theme().colors().text_muted) - .when_some(source.and_then(|s| s.path), |this, path| this.child(path)), + v_flex() + .child( + h_flex() + .gap_0p5() + .text_ui_sm(cx) + .truncate() + .child(stack_frame.name.clone()) + .child(formatted_path), + ) + .child( + h_flex() + .text_ui_xs(cx) + .truncate() + .text_color(cx.theme().colors().text_muted) + .when_some(source.and_then(|s| s.path), |this, path| this.child(path)), + ), ) + .when(supports_frame_restart, |this| { + this.child( + h_flex() + .id(("restart-stack-frame", stack_frame.id)) + .visible_on_hover("") + .absolute() + .right_2() + .overflow_hidden() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().element_selected) + .bg(cx.theme().colors().element_background) + .hover(|style| { + style + .bg(cx.theme().colors().ghost_element_hover) + .cursor_pointer() + }) + .child( + IconButton::new( + ("restart-stack-frame", stack_frame.id), + IconName::DebugRestart, + ) + .icon_size(IconSize::Small) + .on_click(cx.listener({ + let stack_frame_id = stack_frame.id; + move |this, _, cx| { + this.restart_stack_frame(stack_frame_id, cx); + } + })) + .tooltip(move |cx| Tooltip::text("Restart Stack Frame", cx)), + ), + ) + }) .into_any() } } diff --git a/crates/project/src/dap_command.rs b/crates/project/src/dap_command.rs index cab4c3548acf17..386c2753f59ef4 100644 --- a/crates/project/src/dap_command.rs +++ b/crates/project/src/dap_command.rs @@ -798,3 +798,63 @@ impl DapCommand for RestartCommand { Ok(()) } } + +#[derive(Debug, Clone)] +pub(crate) struct RestartStackFrameCommand { + pub stack_frame_id: u64, +} + +impl DapCommand for RestartStackFrameCommand { + type Response = ::Response; + type DapRequest = dap::requests::RestartFrame; + type ProtoRequest = proto::DapRestartStackFrameRequest; + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + stack_frame_id: request.stack_frame_id, + } + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> proto::DapRestartStackFrameRequest { + proto::DapRestartStackFrameRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + stack_frame_id: self.stack_frame_id, + } + } + + fn response_to_proto( + _debug_client_id: &DebugAdapterClientId, + _message: Self::Response, + ) -> ::Response { + proto::Ack {} + } + + fn to_dap(&self) -> ::Arguments { + dap::RestartFrameArguments { + frame_id: self.stack_frame_id, + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } + + fn response_from_proto( + self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 175091a5c94442..9711f75a957cbb 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,8 +1,8 @@ use crate::{ dap_command::{ ContinueCommand, DapCommand, DisconnectCommand, NextCommand, PauseCommand, RestartCommand, - StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand, - TerminateThreadsCommand, + RestartStackFrameCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand, + TerminateCommand, TerminateThreadsCommand, }, project_settings::ProjectSettings, ProjectEnvironment, ProjectItem as _, ProjectPath, @@ -155,6 +155,7 @@ impl DapStore { client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_dap_command::); + client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_shutdown_session); } @@ -870,6 +871,23 @@ impl DapStore { }) } + pub fn restart_stack_frame( + &mut self, + client_id: &DebugAdapterClientId, + stack_frame_id: u64, + cx: &mut ModelContext, + ) -> Task> { + if !self + .capabilities_by_id(client_id) + .supports_restart_frame + .unwrap_or_default() + { + return Task::ready(Ok(())); + } + + self.request_dap(client_id, RestartStackFrameCommand { stack_frame_id }, cx) + } + pub fn scopes( &mut self, client_id: &DebugAdapterClientId, diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 3f7cca1d62caa4..8511b75f82e899 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -328,7 +328,8 @@ message Envelope { DapTerminateRequest dap_terminate_request = 307; DapRestartRequest dap_restart_request = 308; DapShutdownSession dap_shutdown_session = 309; - UpdateThreadStatus update_thread_status = 310; // current max + UpdateThreadStatus update_thread_status = 310; + DapRestartStackFrameRequest dap_restart_stack_frame_request = 311; // current max } reserved 87 to 88; @@ -2469,6 +2470,7 @@ message SetDebugClientCapabilities { bool supports_step_back = 9; bool supports_stepping_granularity = 10; bool supports_terminate_threads_request = 11; + bool supports_restart_frame_request = 12; } message Breakpoint { @@ -2649,6 +2651,11 @@ message DapRestartRequest { bytes raw_args = 3; } +message DapRestartStackFrameRequest { + uint64 project_id = 1; + uint64 client_id = 2; + uint64 stack_frame_id = 3; +} message DapShutdownSession { uint64 project_id = 1; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 5b55bdbb0ec885..f5985916823564 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -185,6 +185,7 @@ messages!( (DapNextRequest, Background), (DapPauseRequest, Background), (DapRestartRequest, Background), + (DapRestartStackFrameRequest, Background), (DapShutdownSession, Background), (DapStepBackRequest, Background), (DapStepInRequest, Background), @@ -531,6 +532,7 @@ request_messages!( (DapTerminateThreadsRequest, Ack), (DapTerminateRequest, Ack), (DapRestartRequest, Ack), + (DapRestartStackFrameRequest, Ack), (DapShutdownSession, Ack), ); @@ -635,6 +637,7 @@ entity_messages!( DapTerminateThreadsRequest, DapTerminateRequest, DapRestartRequest, + DapRestartStackFrameRequest, DapShutdownSession, UpdateThreadStatus, );