From c6a82b3b4538defccec9aac735d69b5e47440e7c Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:49:52 +0100 Subject: [PATCH] debugger: Open correct pane for breakpoints (#49390) Closes #40602 ### Summary This PR ensures that active debug lines only open in a single pane and new active debug lines are added to the most recent pane that contained an active debug line. This fixes a bug where Zed could go to the active debug line file and location in every pane a user had open, even if that pane was focused on a different file. I fixed this by storing the `entity_id` of the pane containing the most recently active debug line on `BreakpointStore`, this is consistent with where the selected stack frame is stored. I used an `entity_id` instead of a strong type to avoid circular dependencies. Whenever an active debug line is being set in the editor or by the debugger it now checks if there's a specific pane it should be set in, and after setting the line it updates `BreakpointStore` state. I also added a new method on the `workspace::Item` trait called `fn pane_changed(&mut self, new_pane_id: EntityId, cx: &mut Context)` To enable `Editor` to update `BreakpointStore`'s active debug line pane id whenever an `Editor` is moved to a new pane. ### PR review TODO list 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 - [x] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Release Notes: - debugger: Fix bug where active debug lines could be set in the wrong pane --- .../src/session/running/stack_frame_list.rs | 93 ++- .../debugger_ui/src/tests/debugger_panel.rs | 533 +++++++++++++++++- crates/editor/src/editor.rs | 28 +- crates/editor/src/items.rs | 19 +- .../project/src/debugger/breakpoint_store.rs | 25 +- crates/workspace/src/item.rs | 20 +- crates/workspace/src/workspace.rs | 13 +- 7 files changed, 689 insertions(+), 42 deletions(-) diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index b460f532548d8a71fafb031ff5c77323d60f046c..ccdfa22e89f449d2d40ae72f6b794b27ee6c8934 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -429,34 +429,58 @@ impl StackFrameList { let position = buffer.read_with(cx, |this, _| { this.snapshot().anchor_after(PointUtf16::new(row, 0)) }); - this.update_in(cx, |this, window, cx| { - this.workspace.update(cx, |workspace, cx| { - let project_path = buffer - .read(cx) - .project_path(cx) - .context("Could not select a stack frame for unnamed buffer")?; - - let open_preview = !workspace - .item_of_type::(cx) - .map(|viewer| { - workspace - .active_item(cx) - .is_some_and(|item| item.item_id() == viewer.item_id()) - }) - .unwrap_or_default(); - - anyhow::Ok(workspace.open_path_preview( - project_path, - None, - true, - true, - open_preview, - window, - cx, - )) - }) - })??? - .await?; + let opened_item = this + .update_in(cx, |this, window, cx| { + this.workspace.update(cx, |workspace, cx| { + let project_path = buffer + .read(cx) + .project_path(cx) + .context("Could not select a stack frame for unnamed buffer")?; + + let open_preview = !workspace + .item_of_type::(cx) + .map(|viewer| { + workspace + .active_item(cx) + .is_some_and(|item| item.item_id() == viewer.item_id()) + }) + .unwrap_or_default(); + + let active_debug_line_pane = workspace + .project() + .read(cx) + .breakpoint_store() + .read(cx) + .active_debug_line_pane_id() + .and_then(|id| workspace.pane_for_entity_id(id)); + + let debug_pane = if let Some(pane) = active_debug_line_pane { + Some(pane.downgrade()) + } else { + // No debug pane set yet. Find a pane where the target file + // is already the active tab so we don't disrupt other panes. + let pane_with_active_file = workspace.panes().iter().find(|pane| { + pane.read(cx) + .active_item() + .and_then(|item| item.project_path(cx)) + .is_some_and(|path| path == project_path) + }); + + pane_with_active_file.map(|pane| pane.downgrade()) + }; + + anyhow::Ok(workspace.open_path_preview( + project_path, + debug_pane, + true, + true, + open_preview, + window, + cx, + )) + }) + })??? + .await?; this.update(cx, |this, cx| { let thread_id = this.state.read_with(cx, |state, _| { @@ -464,6 +488,19 @@ impl StackFrameList { })??; this.workspace.update(cx, |workspace, cx| { + if let Some(pane_id) = workspace + .pane_for(&*opened_item) + .map(|pane| pane.entity_id()) + { + workspace + .project() + .read(cx) + .breakpoint_store() + .update(cx, |store, _cx| { + store.set_active_debug_pane_id(pane_id); + }); + } + let breakpoint_store = workspace.project().read(cx).breakpoint_store(); breakpoint_store.update(cx, |store, cx| { diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 32c0bf01c91a328734da64bd844b93ae3d9fd7a1..207e82b4958941e04ea04fc47c9471141e61a64d 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -34,7 +34,8 @@ use terminal_view::terminal_panel::TerminalPanel; use tests::{active_debug_session_panel, init_test, init_test_workspace}; use util::{path, rel_path::rel_path}; use workspace::item::SaveOptions; -use workspace::{Item, dock::Panel}; +use workspace::pane_group::SplitDirection; +use workspace::{Item, dock::Panel, move_active_item}; #[gpui::test] async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) { @@ -1813,6 +1814,536 @@ async fn test_debug_adapters_shutdown_on_app_quit( ); } +#[gpui::test] +async fn test_breakpoint_jumps_only_in_proper_split_view( + 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", + "second.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 project_path = Path::new(path!("/project")); + let worktree = project + .update(cx, |project, cx| project.find_worktree(project_path, cx)) + .expect("This worktree should exist in project") + .0; + + let worktree_id = workspace + .update(cx, |_, _, cx| worktree.read(cx).id()) + .unwrap(); + + // Open main.rs in pane A (the initial pane) + let pane_a = workspace + .update(cx, |multi, _window, cx| { + multi.workspace().read(cx).active_pane().clone() + }) + .unwrap(); + + let open_main = workspace + .update(cx, |multi, window, cx| { + multi.workspace().update(cx, |workspace, cx| { + workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx) + }) + }) + .unwrap(); + open_main.await.unwrap(); + + cx.run_until_parked(); + + // Split pane A to the right, creating pane B + let pane_b = workspace + .update(cx, |multi, window, cx| { + multi.workspace().update(cx, |workspace, cx| { + workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) + }) + }) + .unwrap(); + + cx.run_until_parked(); + + // Open main.rs in pane B + let weak_pane_b = pane_b.downgrade(); + let open_main_in_b = workspace + .update(cx, |multi, window, cx| { + multi.workspace().update(cx, |workspace, cx| { + workspace.open_path( + (worktree_id, rel_path("main.rs")), + Some(weak_pane_b), + true, + window, + cx, + ) + }) + }) + .unwrap(); + open_main_in_b.await.unwrap(); + + cx.run_until_parked(); + + // Also open second.rs in pane B as an inactive tab + let weak_pane_b = pane_b.downgrade(); + let open_second_in_b = workspace + .update(cx, |multi, window, cx| { + multi.workspace().update(cx, |workspace, cx| { + workspace.open_path( + (worktree_id, rel_path("second.rs")), + Some(weak_pane_b), + true, + window, + cx, + ) + }) + }) + .unwrap(); + open_second_in_b.await.unwrap(); + + cx.run_until_parked(); + + // Switch pane B back to main.rs so second.rs is inactive there + let weak_pane_b = pane_b.downgrade(); + let reactivate_main_in_b = workspace + .update(cx, |multi, window, cx| { + multi.workspace().update(cx, |workspace, cx| { + workspace.open_path( + (worktree_id, rel_path("main.rs")), + Some(weak_pane_b), + true, + window, + cx, + ) + }) + }) + .unwrap(); + reactivate_main_in_b.await.unwrap(); + + cx.run_until_parked(); + + // Now open second.rs in pane A, making main.rs an inactive tab there + let weak_pane_a = pane_a.downgrade(); + let open_second = workspace + .update(cx, |multi, window, cx| { + multi.workspace().update(cx, |workspace, cx| { + workspace.open_path( + (worktree_id, rel_path("second.rs")), + Some(weak_pane_a), + true, + window, + cx, + ) + }) + }) + .unwrap(); + open_second.await.unwrap(); + + cx.run_until_parked(); + + // Layout: + // Pane A: second.rs (active), main.rs (inactive tab) + // Pane B: main.rs (active), second.rs (inactive tab) + + // Verify pane A's active item is second.rs (main.rs is an inactive tab) + workspace + .read_with(cx, |_multi, cx| { + let active = pane_a.read(cx).active_item().unwrap(); + let editor = active.to_any_view().downcast::().unwrap(); + let path = editor.read(cx).project_path(cx).unwrap(); + assert_eq!( + path.path.file_name().unwrap(), + "second.rs", + "Pane A should have second.rs active", + ); + }) + .unwrap(); + + // Verify pane B's active item is main.rs + workspace + .read_with(cx, |_multi, cx| { + let active = pane_b.read(cx).active_item().unwrap(); + let editor = active.to_any_view().downcast::().unwrap(); + let path = editor.read(cx).project_path(cx).unwrap(); + assert_eq!( + path.path.file_name().unwrap(), + "main.rs", + "Pane B should have main.rs active", + ); + }) + .unwrap(); + + // Start a debug session and trigger a breakpoint stop on main.rs line 2 + let session = start_debug_session(&workspace, cx, |_| {}).unwrap(); + let client = session.update(cx, |session, _| session.adapter_client().unwrap()); + + client.on_request::(move |_, _| { + Ok(dap::ThreadsResponse { + threads: vec![dap::Thread { + id: 1, + name: "Thread 1".into(), + }], + }) + }); + + client.on_request::(move |_, _| { + Ok(dap::ScopesResponse { + scopes: Vec::default(), + }) + }); + + client.on_request::(move |_, args| { + assert_eq!(args.thread_id, 1); + + Ok(dap::StackTraceResponse { + stack_frames: vec![dap::StackFrame { + id: 1, + name: "frame 1".into(), + source: Some(dap::Source { + name: Some("main.rs".into()), + path: Some(path!("/project/main.rs").into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 2, + column: 0, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }], + total_frames: None, + }) + }); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Breakpoint, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + // After first breakpoint stop on main.rs: + // Pane A should still have second.rs as its active item because + // main.rs was only an inactive tab there. The debugger should have jumped + // to main.rs only in pane B where it was already the active tab. + workspace + .read_with(cx, |_multi, cx| { + let pane_a_active = pane_a.read(cx).active_item().unwrap(); + let pane_a_editor = pane_a_active.to_any_view().downcast::().unwrap(); + let pane_a_path = pane_a_editor.read(cx).project_path(cx).unwrap(); + assert_eq!( + pane_a_path.path.file_name().unwrap(), + "second.rs", + "Pane A should still have second.rs as active item. \ + The debugger should not switch active tabs in panes where the \ + breakpoint file is not the active tab (issue #40602)", + ); + }) + .unwrap(); + + // There should be exactly one active debug line across all editors in all panes + workspace + .read_with(cx, |_multi, cx| { + let mut total_active_debug_lines = 0; + for pane in [&pane_a, &pane_b] { + for item in pane.read(cx).items() { + if let Some(editor) = item.to_any_view().downcast::().ok() { + total_active_debug_lines += editor + .read(cx) + .highlighted_rows::() + .count(); + } + } + } + assert_eq!( + total_active_debug_lines, 1, + "There should be exactly one active debug line across all editors in all panes" + ); + }) + .unwrap(); + + // Pane B should show the debug highlight on main.rs + workspace + .read_with(cx, |_multi, cx| { + let pane_b_active = pane_b.read(cx).active_item().unwrap(); + let pane_b_editor = pane_b_active.to_any_view().downcast::().unwrap(); + + let active_debug_lines: Vec<_> = pane_b_editor + .read(cx) + .highlighted_rows::() + .collect(); + + assert_eq!( + active_debug_lines.len(), + 1, + "Pane B's main.rs editor should have the active debug line" + ); + }) + .unwrap(); + + // Second breakpoint stop: now on second.rs line 3. + // Even though pane A has second.rs as its active tab, the debug line + // should open in pane B (the persistent debug pane) because pane B + // had the last active debug line. + client.on_request::(move |_, args| { + assert_eq!(args.thread_id, 1); + + Ok(dap::StackTraceResponse { + stack_frames: vec![dap::StackFrame { + id: 2, + name: "frame 2".into(), + source: Some(dap::Source { + name: Some("second.rs".into()), + path: Some(path!("/project/second.rs").into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 3, + column: 0, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }], + total_frames: None, + }) + }); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Breakpoint, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + // Pane B should now have second.rs as the active tab with the debug line, + // because pane B was the last pane that had the debug line (persistent debug pane). + workspace + .read_with(cx, |_multi, cx| { + let pane_b_active = pane_b.read(cx).active_item().unwrap(); + let pane_b_editor = pane_b_active.to_any_view().downcast::().unwrap(); + let pane_b_path = pane_b_editor.read(cx).project_path(cx).unwrap(); + assert_eq!( + pane_b_path.path.file_name().unwrap(), + "second.rs", + "Pane B should have switched to second.rs because it is the persistent debug pane", + ); + + let active_debug_lines: Vec<_> = pane_b_editor + .read(cx) + .highlighted_rows::() + .collect(); + + assert_eq!( + active_debug_lines.len(), + 1, + "Pane B's second.rs editor should have the active debug line" + ); + }) + .unwrap(); + + // There should still be exactly one active debug line across all editors + workspace + .read_with(cx, |_multi, cx| { + let mut total_active_debug_lines = 0; + for pane in [&pane_a, &pane_b] { + for item in pane.read(cx).items() { + if let Some(editor) = item.to_any_view().downcast::().ok() { + total_active_debug_lines += editor + .read(cx) + .highlighted_rows::() + .count(); + } + } + } + assert_eq!( + total_active_debug_lines, 1, + "There should be exactly one active debug line across all editors after second stop" + ); + }) + .unwrap(); + + // === New case: Move the debug pane (pane B) active item to a new pane C === + // This simulates a user dragging the tab with the active debug line to a new split. + // The debugger should track that the debug line moved to pane C and use pane C + // for subsequent debug stops. + + // Split pane B to create pane C + let pane_c = workspace + .update(cx, |multi, window, cx| { + multi.workspace().update(cx, |workspace, cx| { + workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx) + }) + }) + .unwrap(); + + cx.run_until_parked(); + + // Move the active item (second.rs with debug line) from pane B to pane C + workspace + .update(cx, |_multi, window, cx| { + move_active_item(&pane_b, &pane_c, true, false, window, cx); + }) + .unwrap(); + + cx.run_until_parked(); + + // Verify pane C now has second.rs as active item + workspace + .read_with(cx, |_multi, cx| { + let pane_c_active = pane_c.read(cx).active_item().unwrap(); + let pane_c_editor = pane_c_active.to_any_view().downcast::().unwrap(); + let pane_c_path = pane_c_editor.read(cx).project_path(cx).unwrap(); + assert_eq!( + pane_c_path.path.file_name().unwrap(), + "second.rs", + "Pane C should have second.rs after moving it from pane B", + ); + }) + .unwrap(); + + // Third breakpoint stop: back on main.rs line 2. + // The debug line should appear in pane C because that's where the debug line + // was moved to. The debugger should track pane moves. + client.on_request::(move |_, args| { + assert_eq!(args.thread_id, 1); + + Ok(dap::StackTraceResponse { + stack_frames: vec![dap::StackFrame { + id: 3, + name: "frame 3".into(), + source: Some(dap::Source { + name: Some("main.rs".into()), + path: Some(path!("/project/main.rs").into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 2, + column: 0, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }], + total_frames: None, + }) + }); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Breakpoint, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + // Pane C should now have main.rs as the active tab with the debug line, + // because pane C is where the debug line was moved to from pane B. + workspace + .read_with(cx, |_multi, cx| { + let pane_c_active = pane_c.read(cx).active_item().unwrap(); + let pane_c_editor = pane_c_active.to_any_view().downcast::().unwrap(); + let pane_c_path = pane_c_editor.read(cx).project_path(cx).unwrap(); + assert_eq!( + pane_c_path.path.file_name().unwrap(), + "main.rs", + "Pane C should have switched to main.rs because it is now the persistent debug pane \ + (the debug line was moved here from pane B)", + ); + + let active_debug_lines: Vec<_> = pane_c_editor + .read(cx) + .highlighted_rows::() + .collect(); + + assert_eq!( + active_debug_lines.len(), + 1, + "Pane C's main.rs editor should have the active debug line" + ); + }) + .unwrap(); + + // There should still be exactly one active debug line across all editors + workspace + .read_with(cx, |_multi, cx| { + let mut total_active_debug_lines = 0; + for pane in [&pane_a, &pane_b, &pane_c] { + for item in pane.read(cx).items() { + if let Some(editor) = item.to_any_view().downcast::().ok() { + total_active_debug_lines += editor + .read(cx) + .highlighted_rows::() + .count(); + } + } + } + assert_eq!( + total_active_debug_lines, 1, + "There should be exactly one active debug line across all editors after third stop" + ); + }) + .unwrap(); + + // Clean up + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(session.read(cx).session_id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} + #[gpui::test] async fn test_adapter_shutdown_with_child_sessions_on_app_quit( executor: BackgroundExecutor, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 54e20d00cafebc209cec2bd10eb0cbb0007e3af8..3e734fdf1ab8254807a65c96bb98a0f804bc4dc4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -22801,12 +22801,36 @@ impl Editor { maybe!({ let breakpoint_store = self.breakpoint_store.as_ref()?; - let Some(active_stack_frame) = breakpoint_store.read(cx).active_position().cloned() - else { + let (active_stack_frame, debug_line_pane_id) = { + let store = breakpoint_store.read(cx); + let active_stack_frame = store.active_position().cloned(); + let debug_line_pane_id = store.active_debug_line_pane_id(); + (active_stack_frame, debug_line_pane_id) + }; + + let Some(active_stack_frame) = active_stack_frame else { self.clear_row_highlights::(); return None; }; + if let Some(debug_line_pane_id) = debug_line_pane_id { + if let Some(workspace) = self + .workspace + .as_ref() + .and_then(|(workspace, _)| workspace.upgrade()) + { + let editor_pane_id = workspace + .read(cx) + .pane_for_item_id(cx.entity_id()) + .map(|pane| pane.entity_id()); + + if editor_pane_id.is_some_and(|id| id != debug_line_pane_id) { + self.clear_row_highlights::(); + return None; + } + } + } + let position = active_stack_frame.position; let buffer_id = position.buffer_id?; let snapshot = self diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index afb296cff59369804cd28ebd85ced3d2f7649b7a..685387342caf8e705a3648cb07acaa1867db55d8 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,7 +1,7 @@ use crate::{ - Anchor, Autoscroll, BufferSerialization, Capability, Editor, EditorEvent, EditorSettings, - ExcerptId, ExcerptRange, FormatTarget, MultiBuffer, MultiBufferSnapshot, NavigationData, - ReportEditorEvent, SelectionEffects, ToPoint as _, + ActiveDebugLine, Anchor, Autoscroll, BufferSerialization, Capability, Editor, EditorEvent, + EditorSettings, ExcerptId, ExcerptRange, FormatTarget, MultiBuffer, MultiBufferSnapshot, + NavigationData, ReportEditorEvent, SelectionEffects, ToPoint as _, display_map::HighlightKey, editor_settings::SeedQuerySetting, persistence::{DB, SerializedEditor}, @@ -1027,6 +1027,19 @@ impl Item for Editor { } } + fn pane_changed(&mut self, new_pane_id: EntityId, cx: &mut Context) { + if self + .highlighted_rows + .get(&TypeId::of::()) + .is_some_and(|lines| !lines.is_empty()) + && let Some(breakpoint_store) = self.breakpoint_store.as_ref() + { + breakpoint_store.update(cx, |store, _cx| { + store.set_active_debug_pane_id(new_pane_id); + }); + } + } + fn to_item_events(event: &EditorEvent, f: &mut dyn FnMut(ItemEvent)) { match event { EditorEvent::Saved | EditorEvent::TitleChanged => { diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 54f884aa5704bd256620f35eb0ea73dc53feeab5..50df9ae3125d3db98df24280ebd1e5b14adfe557 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -6,7 +6,9 @@ pub use breakpoints_in_file::{BreakpointSessionState, BreakpointWithPosition}; use breakpoints_in_file::{BreakpointsInFile, StatefulBreakpoint}; use collections::{BTreeMap, HashMap}; use dap::{StackFrameId, client::SessionId}; -use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Subscription, Task}; +use gpui::{ + App, AppContext, AsyncApp, Context, Entity, EntityId, EventEmitter, Subscription, Task, +}; use itertools::Itertools; use language::{Buffer, BufferSnapshot, proto::serialize_anchor as serialize_text_anchor}; use rpc::{ @@ -154,6 +156,7 @@ pub struct BreakpointStore { breakpoints: BTreeMap, BreakpointsInFile>, downstream_client: Option<(AnyProtoClient, u64)>, active_stack_frame: Option, + active_debug_line_pane_id: Option, // E.g ssh mode: BreakpointStoreMode, } @@ -171,6 +174,7 @@ impl BreakpointStore { worktree_store, downstream_client: None, active_stack_frame: Default::default(), + active_debug_line_pane_id: None, } } @@ -190,6 +194,7 @@ impl BreakpointStore { worktree_store, downstream_client: None, active_stack_frame: Default::default(), + active_debug_line_pane_id: None, } } @@ -651,16 +656,30 @@ impl BreakpointStore { self.active_stack_frame.as_ref() } + pub fn active_debug_line_pane_id(&self) -> Option { + self.active_debug_line_pane_id + } + + pub fn set_active_debug_pane_id(&mut self, pane_id: EntityId) { + self.active_debug_line_pane_id = Some(pane_id); + } + pub fn remove_active_position( &mut self, session_id: Option, cx: &mut Context, ) { if let Some(session_id) = session_id { - self.active_stack_frame - .take_if(|active_stack_frame| active_stack_frame.session_id == session_id); + if self + .active_stack_frame + .take_if(|active_stack_frame| active_stack_frame.session_id == session_id) + .is_some() + { + self.active_debug_line_pane_id = None; + } } else { self.active_stack_frame.take(); + self.active_debug_line_pane_id = None; } cx.emit(BreakpointStoreEvent::ClearDebugLines); diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 4153373fdb0e107aa08c1fe643600635f63edafe..b29e02f05b367bab557403f3bb34f6ffa45caecc 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -219,6 +219,7 @@ pub trait Item: Focusable + EventEmitter + Render + Sized { fn discarded(&self, _project: Entity, _window: &mut Window, _cx: &mut Context) {} fn on_removed(&self, _cx: &mut Context) {} fn workspace_deactivated(&mut self, _window: &mut Window, _: &mut Context) {} + fn pane_changed(&mut self, _new_pane_id: EntityId, _cx: &mut Context) {} fn navigate( &mut self, _: Arc, @@ -737,11 +738,22 @@ impl ItemHandle for Entity { .log_err(); } - if workspace + let new_pane_id = pane.entity_id(); + let old_item_pane = workspace .panes_by_item - .insert(self.item_id(), pane.downgrade()) - .is_none() - { + .insert(self.item_id(), pane.downgrade()); + + if old_item_pane.as_ref().is_none_or(|old_pane| { + old_pane + .upgrade() + .is_some_and(|old_pane| old_pane.entity_id() != new_pane_id) + }) { + self.update(cx, |this, cx| { + this.pane_changed(new_pane_id, cx); + }); + } + + if old_item_pane.is_none() { let mut pending_autosave = DelayedDebouncedEditAction::new(); let (pending_update_tx, mut pending_update_rx) = mpsc::unbounded(); let pending_update = Rc::new(RefCell::new(None)); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ac83809f8d313e842e72d19fb98b8b5d1b69df0f..c12525bb2a5c6b46cd6b4fabc9599e3b6cdfd25d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4906,10 +4906,21 @@ impl Workspace { } pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option> { - let weak_pane = self.panes_by_item.get(&handle.item_id())?; + self.pane_for_item_id(handle.item_id()) + } + + pub fn pane_for_item_id(&self, item_id: EntityId) -> Option> { + let weak_pane = self.panes_by_item.get(&item_id)?; weak_pane.upgrade() } + pub fn pane_for_entity_id(&self, entity_id: EntityId) -> Option> { + self.panes + .iter() + .find(|pane| pane.entity_id() == entity_id) + .cloned() + } + fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context) { self.follower_states.retain(|leader_id, state| { if *leader_id == CollaboratorId::PeerId(peer_id) {