From 314b7e55fb3fb2d8277a38217a64d335668ef473 Mon Sep 17 00:00:00 2001 From: Nelson Campos <60667230+nelsoncampos-cloudwalk@users.noreply.github.com> Date: Thu, 12 Mar 2026 10:05:52 -0300 Subject: [PATCH] debugger: Fix restart only working once per session (#51247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Session::restart_task` is set to `Some` when a restart is initiated but never cleared back to `None`. The guard at the top of `restart()` checks `self.restart_task.is_some()` and returns early, so only the first restart attempt succeeds. This primarily affects debug adapters that advertise `supportsRestartRequest` dynamically via a `CapabilitiesEvent` after launch, such as the Flutter debug adapter. Related: https://github.com/zed-extensions/dart/issues/45 Before you mark this PR as ready for review, make sure that you have: - [x] Added a solid test coverage and/or screenshots from doing manual testing - [x] Done a self-review taking into account security and performance aspects - [ ] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) (N/A — no UI changes) Release Notes: - debugger: Fixed debug session restart only working once when the adapter supports DAP restart requests. --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Anthony Eid --- crates/debugger_ui/src/tests.rs | 8 +- .../debugger_ui/src/tests/debugger_panel.rs | 74 ++++++++++++++++++- crates/project/src/debugger/session.rs | 28 ++++--- 3 files changed, 97 insertions(+), 13 deletions(-) diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index c183f8941c3f30cb43ffaa638eae4e6b387e226d..cc407dfd810ceedb11c4d8030c46a6f17065b34b 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -132,7 +132,13 @@ pub fn start_debug_session_with) + 'static>( .workspace() .read(cx) .panel::(cx) - .and_then(|panel| panel.read(cx).active_session()) + .and_then(|panel| { + panel + .read(cx) + .sessions_with_children + .keys() + .max_by_key(|session| session.read(cx).session_id(cx)) + }) .map(|session| session.read(cx).running_state().read(cx).session()) .cloned() .context("Failed to get active session") diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 207e82b4958941e04ea04fc47c9471141e61a64d..e4c258a8d2af0b865f13c28430c44a66117a11cd 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -27,7 +27,7 @@ use std::{ path::Path, sync::{ Arc, - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, }, }; use terminal_view::terminal_panel::TerminalPanel; @@ -2481,3 +2481,75 @@ async fn test_adapter_shutdown_with_child_sessions_on_app_quit( "Child session should have received disconnect request" ); } + +#[gpui::test] +async fn test_restart_request_is_not_sent_more_than_once_until_response( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + fs.insert_tree( + path!("/project"), + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, [path!("/project").as_ref()], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let session = start_debug_session(&workspace, cx, move |client| { + client.on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_restart_request: Some(true), + ..Default::default() + }) + }); + }) + .unwrap(); + + let client = session.update(cx, |session, _| session.adapter_client().unwrap()); + + let restart_count = Arc::new(AtomicUsize::new(0)); + + client.on_request::({ + let restart_count = restart_count.clone(); + move |_, _| { + restart_count.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + }); + + // This works because the restart request sender is on the foreground thread + // so it will start running after the gpui update stack is cleared + session.update(cx, |session, cx| { + session.restart(None, cx); + session.restart(None, cx); + session.restart(None, cx); + }); + + cx.run_until_parked(); + + assert_eq!( + restart_count.load(Ordering::SeqCst), + 1, + "Only one restart request should be sent while a restart is in-flight" + ); + + session.update(cx, |session, cx| { + session.restart(None, cx); + }); + + cx.run_until_parked(); + + assert_eq!( + restart_count.load(Ordering::SeqCst), + 2, + "A second restart should be allowed after the first one completes" + ); +} diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index a6c3f52b17a4a6cf241aa49329f3f14f0b5cefbc..87e11cfd97a2f63bba3cefca671e4413deb6765f 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -2187,21 +2187,27 @@ impl Session { self.capabilities.supports_restart_request.unwrap_or(false) && !self.is_terminated(); self.restart_task = Some(cx.spawn(async move |this, cx| { - let _ = this.update(cx, |session, cx| { + this.update(cx, |session, cx| { if supports_dap_restart { - session - .request( - RestartCommand { - raw: args.unwrap_or(Value::Null), - }, - Self::fallback_to_manual_restart, - cx, - ) - .detach(); + session.request( + RestartCommand { + raw: args.unwrap_or(Value::Null), + }, + Self::fallback_to_manual_restart, + cx, + ) } else { cx.emit(SessionStateEvent::Restart); + Task::ready(None) } - }); + }) + .unwrap_or_else(|_| Task::ready(None)) + .await; + + this.update(cx, |session, _cx| { + session.restart_task = None; + }) + .ok(); })); }