debugger: Add console indicator and resolve debug configs from NewSessionModal (#28489)

Anthony Eid created

The debug console will now show an indicator when it's unopened and
there's unread messages.

`NewSessionModal` attempts to resolve debug configurations before using
the config to start debugging. This allows users to use zed's task
variables in the modal prompt.

I had to invert tasks_ui dependency on debugger_ui so `NewSessionModal`
could get the correct `TaskContexts` by calling tasks_ui functions. A
consequence of this workspace has a new event `ShowAttachModal` that I'm
not a big fan of. @osiewicz if you have time could you please take a
look to see if there's a way around adding the event. I'm open to pair
on it too.

Release Notes:

- N/A

Change summary

.zed/debug.json                                   |   6 
Cargo.lock                                        |   3 
crates/debugger_ui/Cargo.toml                     |   1 
crates/debugger_ui/src/debugger_panel.rs          |  41 ++++++
crates/debugger_ui/src/debugger_ui.rs             |  14 +
crates/debugger_ui/src/new_session_modal.rs       |  42 +++++
crates/debugger_ui/src/session/running.rs         |  40 +++++
crates/debugger_ui/src/session/running/console.rs |   4 
crates/project/src/debugger/session.rs            |   4 
crates/tasks_ui/Cargo.toml                        |   2 
crates/tasks_ui/src/modal.rs                      | 106 +++++++++-------
crates/tasks_ui/src/tasks_ui.rs                   |  25 +--
12 files changed, 205 insertions(+), 83 deletions(-)

Detailed changes

.zed/debug.json 🔗

@@ -1,13 +1,13 @@
 [
   {
-    "label": "Debug Zed with LLDB",
-    "adapter": "LLDB",
+    "label": "Debug Zed (CodeLLDB)",
+    "adapter": "CodeLLDB",
     "program": "$ZED_WORKTREE_ROOT/target/debug/zed",
     "request": "launch",
     "cwd": "$ZED_WORKTREE_ROOT"
   },
   {
-    "label": "Debug Zed with GDB",
+    "label": "Debug Zed (GDB)",
     "adapter": "GDB",
     "program": "$ZED_WORKTREE_ROOT/target/debug/zed",
     "request": "launch",

Cargo.lock 🔗

@@ -4232,6 +4232,7 @@ dependencies = [
  "settings",
  "sysinfo",
  "task",
+ "tasks_ui",
  "terminal_view",
  "theme",
  "ui",
@@ -14240,9 +14241,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "collections",
- "debugger_ui",
  "editor",
- "feature_flags",
  "file_icons",
  "fuzzy",
  "gpui",

crates/debugger_ui/Cargo.toml 🔗

@@ -45,6 +45,7 @@ serde_json.workspace = true
 settings.workspace = true
 sysinfo.workspace = true
 task.workspace = true
+tasks_ui.workspace = true
 terminal_view.workspace = true
 theme.workspace = true
 ui.workspace = true

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -77,8 +77,45 @@ impl DebugPanel {
             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 weak = cx.weak_entity();
+
+            let modal_subscription =
+                cx.observe_new::<tasks_ui::TasksModal>(move |_, window, cx| {
+                    let modal_entity = cx.entity();
+
+                    weak.update(cx, |_: &mut DebugPanel, cx| {
+                        let Some(window) = window else {
+                            log::error!("Debug panel couldn't subscribe to tasks modal because there was no window");
+                            return;
+                        };
+
+                        cx.subscribe_in(
+                            &modal_entity,
+                            window,
+                            |panel, _, event: &tasks_ui::ShowAttachModal, window, cx| {
+                                panel.workspace.update(cx, |workspace, cx| {
+                                    let project = workspace.project().clone();
+                                    workspace.toggle_modal(window, cx, |window, cx| {
+                                        crate::attach_modal::AttachModal::new(
+                                            project,
+                                            event.debug_config.clone(),
+                                            true,
+                                            window,
+                                            cx,
+                                        )
+                                    });
+                                }).ok();
+                            },
+                        )
+                        .detach();
+                    })
+                    .ok();
+                });
+
+            let _subscriptions = vec![
+                cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event),
+                modal_subscription,
+            ];
 
             let debug_panel = Self {
                 size: px(300.),

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -156,7 +156,17 @@ pub fn init(cx: &mut App) {
                             });
                         }
                     },
-                );
+                )
+                .register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
+                    tasks_ui::toggle_modal(
+                        workspace,
+                        None,
+                        task::TaskModal::DebugModal,
+                        window,
+                        cx,
+                    )
+                    .detach();
+                });
         })
     })
     .detach();
@@ -237,7 +247,7 @@ pub fn init(cx: &mut App) {
 
                                     state.session().update(cx, |session, cx| {
                                         session.evaluate(text, None, stack_id, None, cx);
-                                    })
+                                    });
                                 });
                             Some(())
                         });

crates/debugger_ui/src/new_session_modal.rs 🔗

@@ -102,7 +102,8 @@ impl NewSessionModal {
             },
         })
     }
-    fn start_new_session(&self, cx: &mut Context<Self>) -> Result<()> {
+
+    fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) -> Result<()> {
         let workspace = self.workspace.clone();
         let config = self
             .debug_config(cx)
@@ -112,10 +113,41 @@ impl NewSessionModal {
             panel.past_debug_definition = Some(config.clone());
         });
 
+        let task_contexts = workspace
+            .update(cx, |workspace, cx| {
+                tasks_ui::task_contexts(workspace, window, cx)
+            })
+            .ok();
+
         cx.spawn(async move |this, cx| {
+            let task_context = if let Some(task) = task_contexts {
+                task.await
+                    .active_worktree_context
+                    .map_or(task::TaskContext::default(), |context| context.1)
+            } else {
+                task::TaskContext::default()
+            };
             let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
-            let task =
-                project.update(cx, |this, cx| this.start_debug_session(config.into(), cx))?;
+
+            let task = project.update(cx, |this, cx| {
+                if let Some(debug_config) =
+                    config
+                        .clone()
+                        .to_zed_format()
+                        .ok()
+                        .and_then(|task_template| {
+                            task_template
+                                .resolve_task("debug_task", &task_context)
+                                .and_then(|resolved_task| {
+                                    resolved_task.resolved_debug_adapter_config()
+                                })
+                        })
+                {
+                    this.start_debug_session(debug_config, cx)
+                } else {
+                    this.start_debug_session(config.into(), cx)
+                }
+            })?;
             let spawn_result = task.await;
             if spawn_result.is_ok() {
                 this.update(cx, |_, cx| {
@@ -614,8 +646,8 @@ impl Render for NewSessionModal {
                             })
                             .child(
                                 Button::new("debugger-spawn", "Start")
-                                    .on_click(cx.listener(|this, _, _, cx| {
-                                        this.start_new_session(cx).log_err();
+                                    .on_click(cx.listener(|this, _, window, cx| {
+                                        this.start_new_session(window, cx).log_err();
                                     }))
                                     .disabled(self.debugger.is_none()),
                             ),

crates/debugger_ui/src/session/running.rs 🔗

@@ -26,8 +26,8 @@ use rpc::proto::ViewId;
 use settings::Settings;
 use stack_frame_list::StackFrameList;
 use ui::{
-    App, Context, ContextMenu, DropdownMenu, InteractiveElement, IntoElement, ParentElement,
-    Render, SharedString, Styled, Window, div, h_flex, v_flex,
+    AnyElement, App, Context, ContextMenu, DropdownMenu, InteractiveElement, IntoElement, Label,
+    LabelCommon as _, ParentElement, Render, SharedString, Styled, Window, div, h_flex, v_flex,
 };
 use util::ResultExt;
 use variable_list::VariableList;
@@ -86,6 +86,7 @@ struct SubView {
     inner: AnyView,
     pane_focus_handle: FocusHandle,
     tab_name: SharedString,
+    show_indicator: Box<dyn Fn(&App) -> bool>,
 }
 
 impl SubView {
@@ -93,12 +94,14 @@ impl SubView {
         pane_focus_handle: FocusHandle,
         view: AnyView,
         tab_name: SharedString,
+        show_indicator: Option<Box<dyn Fn(&App) -> bool>>,
         cx: &mut App,
     ) -> Entity<Self> {
         cx.new(|_| Self {
             tab_name,
             inner: view,
             pane_focus_handle,
+            show_indicator: show_indicator.unwrap_or(Box::new(|_| false)),
         })
     }
 }
@@ -110,8 +113,27 @@ impl Focusable for SubView {
 impl EventEmitter<()> for SubView {}
 impl Item for SubView {
     type Event = ();
-    fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
-        Some(self.tab_name.clone())
+
+    fn tab_content(
+        &self,
+        params: workspace::item::TabContentParams,
+        _: &Window,
+        cx: &App,
+    ) -> AnyElement {
+        let label = Label::new(self.tab_name.clone())
+            .color(params.text_color())
+            .into_any_element();
+
+        if !params.selected && self.show_indicator.as_ref()(cx) {
+            return h_flex()
+                .justify_between()
+                .child(ui::Indicator::dot())
+                .gap_2()
+                .child(label)
+                .into_any_element();
+        }
+
+        label
     }
 }
 
@@ -315,6 +337,7 @@ impl RunningState {
                     this.focus_handle(cx),
                     stack_frame_list.clone().into(),
                     SharedString::new_static("Frames"),
+                    None,
                     cx,
                 )),
                 true,
@@ -329,6 +352,7 @@ impl RunningState {
                     breakpoints.focus_handle(cx),
                     breakpoints.into(),
                     SharedString::new_static("Breakpoints"),
+                    None,
                     cx,
                 )),
                 true,
@@ -346,6 +370,7 @@ impl RunningState {
                     variable_list.focus_handle(cx),
                     variable_list.clone().into(),
                     SharedString::new_static("Variables"),
+                    None,
                     cx,
                 )),
                 true,
@@ -359,6 +384,7 @@ impl RunningState {
                     this.focus_handle(cx),
                     module_list.clone().into(),
                     SharedString::new_static("Modules"),
+                    None,
                     cx,
                 )),
                 false,
@@ -371,11 +397,17 @@ impl RunningState {
         });
         let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
         rightmost_pane.update(cx, |this, cx| {
+            let weak_console = console.downgrade();
             this.add_item(
                 Box::new(SubView::new(
                     this.focus_handle(cx),
                     console.clone().into(),
                     SharedString::new_static("Console"),
+                    Some(Box::new(move |cx| {
+                        weak_console
+                            .read_with(cx, |console, cx| console.show_indicator(cx))
+                            .unwrap_or_default()
+                    })),
                     cx,
                 )),
                 true,

crates/debugger_ui/src/session/running/console.rs 🔗

@@ -105,6 +105,10 @@ impl Console {
         }
     }
 
+    pub(crate) fn show_indicator(&self, cx: &App) -> bool {
+        self.session.read(cx).has_new_output(self.last_token)
+    }
+
     pub fn add_messages<'a>(
         &mut self,
         events: impl Iterator<Item = &'a OutputEvent>,

crates/project/src/debugger/session.rs 🔗

@@ -1152,6 +1152,10 @@ impl Session {
         }
     }
 
+    pub fn has_new_output(&self, last_update: OutputToken) -> bool {
+        self.output_token.0.checked_sub(last_update.0).unwrap_or(0) != 0
+    }
+
     pub fn output(
         &self,
         since: OutputToken,

crates/tasks_ui/Cargo.toml 🔗

@@ -14,11 +14,9 @@ path = "src/tasks_ui.rs"
 [dependencies]
 anyhow.workspace = true
 collections.workspace = true
-debugger_ui.workspace = true
 editor.workspace = true
 file_icons.workspace = true
 fuzzy.workspace = true
-feature_flags.workspace = true
 itertools.workspace = true
 gpui.workspace = true
 menu.workspace = true

crates/tasks_ui/src/modal.rs 🔗

@@ -128,9 +128,9 @@ impl TasksModalDelegate {
     }
 }
 
-pub(crate) struct TasksModal {
+pub struct TasksModal {
     picker: Entity<Picker<TasksModalDelegate>>,
-    _subscription: Subscription,
+    _subscription: [Subscription; 2],
 }
 
 impl TasksModal {
@@ -156,9 +156,16 @@ impl TasksModal {
                 cx,
             )
         });
-        let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
-            cx.emit(DismissEvent);
-        });
+        let _subscription = [
+            cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| {
+                cx.emit(DismissEvent);
+            }),
+            cx.subscribe(&picker, |_, _, event: &ShowAttachModal, cx| {
+                cx.emit(ShowAttachModal {
+                    debug_config: event.debug_config.clone(),
+                });
+            }),
+        ];
         Self {
             picker,
             _subscription,
@@ -179,7 +186,13 @@ impl Render for TasksModal {
     }
 }
 
+pub struct ShowAttachModal {
+    pub debug_config: DebugTaskDefinition,
+}
+
 impl EventEmitter<DismissEvent> for TasksModal {}
+impl EventEmitter<ShowAttachModal> for TasksModal {}
+impl EventEmitter<ShowAttachModal> for Picker<TasksModalDelegate> {}
 
 impl Focusable for TasksModal {
     fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle {
@@ -321,7 +334,7 @@ impl PickerDelegate for TasksModalDelegate {
     fn confirm(
         &mut self,
         omit_history_entry: bool,
-        window: &mut Window,
+        _: &mut Window,
         cx: &mut Context<picker::Picker<Self>>,
     ) {
         let current_match_index = self.selected_index();
@@ -346,51 +359,52 @@ impl PickerDelegate for TasksModalDelegate {
             }
         }
 
-        self.workspace
-            .update(cx, |workspace, cx| {
-                match task.task_type() {
-                    TaskType::Debug(config) if config.locator.is_none() => {
-                        let Some(config): Option<DebugTaskDefinition> = task
-                            .resolved_debug_adapter_config()
-                            .and_then(|config| config.try_into().ok())
-                        else {
-                            return;
-                        };
-                        let project = workspace.project().clone();
-
-                        match &config.request {
-                            DebugRequestType::Attach(attach_config)
-                                if attach_config.process_id.is_none() =>
-                            {
-                                workspace.toggle_modal(window, cx, |window, cx| {
-                                    debugger_ui::attach_modal::AttachModal::new(
-                                        project,
-                                        config.clone(),
-                                        true,
-                                        window,
-                                        cx,
-                                    )
-                                });
-                            }
-                            _ => {
-                                project.update(cx, |project, cx| {
+        match task.task_type() {
+            TaskType::Debug(config) if config.locator.is_none() => {
+                let Some(config): Option<DebugTaskDefinition> = task
+                    .resolved_debug_adapter_config()
+                    .and_then(|config| config.try_into().ok())
+                else {
+                    return;
+                };
+
+                match &config.request {
+                    DebugRequestType::Attach(attach_config)
+                        if attach_config.process_id.is_none() =>
+                    {
+                        cx.emit(ShowAttachModal {
+                            debug_config: config.clone(),
+                        });
+                        return;
+                    }
+                    _ => {
+                        self.workspace
+                            .update(cx, |workspace, cx| {
+                                workspace.project().update(cx, |project, cx| {
                                     project
                                         .start_debug_session(config.into(), cx)
                                         .detach_and_log_err(cx);
                                 });
-                            }
-                        }
+                            })
+                            .ok();
                     }
-                    _ => schedule_resolved_task(
-                        workspace,
-                        task_source_kind,
-                        task,
-                        omit_history_entry,
-                        cx,
-                    ),
-                };
-            })
-            .ok();
+                }
+            }
+            _ => {
+                self.workspace
+                    .update(cx, |workspace, cx| {
+                        schedule_resolved_task(
+                            workspace,
+                            task_source_kind,
+                            task,
+                            omit_history_entry,
+                            cx,
+                        );
+                    })
+                    .ok();
+            }
+        };
+
         cx.emit(DismissEvent);
     }
 

crates/tasks_ui/src/tasks_ui.rs 🔗

@@ -1,11 +1,9 @@
 use std::path::Path;
 
 use collections::HashMap;
-use debugger_ui::Start;
 use editor::Editor;
-use feature_flags::{Debugger, FeatureFlagViewExt};
 use gpui::{App, AppContext as _, Context, Entity, Task, Window};
-use modal::{TaskOverrides, TasksModal};
+use modal::TaskOverrides;
 use project::{Location, TaskContexts, TaskSourceKind, Worktree};
 use task::{
     RevealTarget, TaskContext, TaskId, TaskModal, TaskTemplate, TaskVariables, VariableName,
@@ -15,11 +13,11 @@ use workspace::{Workspace, tasks::schedule_resolved_task};
 
 mod modal;
 
-pub use modal::{Rerun, Spawn};
+pub use modal::{Rerun, ShowAttachModal, Spawn, TasksModal};
 
 pub fn init(cx: &mut App) {
     cx.observe_new(
-        |workspace: &mut Workspace, window: Option<&mut Window>, cx: &mut Context<Workspace>| {
+        |workspace: &mut Workspace, _: Option<&mut Window>, _: &mut Context<Workspace>| {
             workspace
                 .register_action(spawn_task_or_modal)
                 .register_action(move |workspace, action: &modal::Rerun, window, cx| {
@@ -89,17 +87,6 @@ pub fn init(cx: &mut App) {
                         toggle_modal(workspace, None, TaskModal::ScriptModal, window, cx).detach();
                     };
                 });
-
-            let Some(window) = window else {
-                return;
-            };
-
-            cx.when_flag_enabled::<Debugger>(window, |workspace, _, _| {
-                workspace.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
-                    crate::toggle_modal(workspace, None, task::TaskModal::DebugModal, window, cx)
-                        .detach();
-                });
-            });
         },
     )
     .detach();
@@ -277,7 +264,11 @@ where
     })
 }
 
-fn task_contexts(workspace: &Workspace, window: &mut Window, cx: &mut App) -> Task<TaskContexts> {
+pub fn task_contexts(
+    workspace: &Workspace,
+    window: &mut Window,
+    cx: &mut App,
+) -> Task<TaskContexts> {
     let active_item = workspace.active_item(cx);
     let active_worktree = active_item
         .as_ref()