Add a run menu (#32505)

Conrad Irwin created

As part of this I refactored the logic that enabled/disabled actions in
the debugger to happen at action registration time instead of using
command palette filters. This allows the menu to grey out actions correctly.

Release Notes:

- Add a "Run" menu to contain tasks and debugger

Change summary

Cargo.lock                               |   1 
assets/settings/initial_tasks.json       |   2 
crates/debugger_ui/src/debugger_panel.rs |  94 -----
crates/debugger_ui/src/debugger_ui.rs    | 425 +++++++++++++------------
crates/editor/Cargo.toml                 |   1 
crates/editor/src/actions.rs             |   4 
crates/editor/src/editor.rs              |  27 +
crates/editor/src/element.rs             |   2 
crates/editor/src/mouse_context_menu.rs  |  15 
crates/workspace/src/workspace.rs        |  13 
crates/zed/src/zed/app_menus.rs          |  24 +
crates/zed/src/zed/quick_action_bar.rs   |  41 --
12 files changed, 288 insertions(+), 361 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4695,7 +4695,6 @@ dependencies = [
  "client",
  "clock",
  "collections",
- "command_palette_hooks",
  "convert_case 0.8.0",
  "ctor",
  "dap",

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -4,12 +4,10 @@ use crate::session::running::RunningState;
 use crate::{
     ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
     FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, NewProcessModal,
-    NewProcessMode, Pause, Restart, ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop,
-    ToggleExpandItem, ToggleIgnoreBreakpoints, ToggleSessionPicker, ToggleThreadPicker,
-    persistence, spawn_task_or_modal,
+    NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop, ToggleExpandItem,
+    ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
 };
 use anyhow::Result;
-use command_palette_hooks::CommandPaletteFilter;
 use dap::adapters::DebugAdapterName;
 use dap::debugger_settings::DebugPanelDockPosition;
 use dap::{
@@ -29,7 +27,6 @@ use project::{Fs, WorktreeId};
 use project::{Project, debugger::session::ThreadStatus};
 use rpc::proto::{self};
 use settings::Settings;
-use std::any::TypeId;
 use std::sync::Arc;
 use task::{DebugScenario, TaskContext};
 use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
@@ -140,82 +137,6 @@ impl DebugPanel {
             .map(|session| session.read(cx).running_state().clone())
     }
 
-    pub(crate) fn filter_action_types(&self, cx: &mut App) {
-        let (has_active_session, supports_restart, support_step_back, status) = self
-            .active_session()
-            .map(|item| {
-                let running = item.read(cx).running_state().clone();
-                let caps = running.read(cx).capabilities(cx);
-                (
-                    !running.read(cx).session().read(cx).is_terminated(),
-                    caps.supports_restart_request.unwrap_or_default(),
-                    caps.supports_step_back.unwrap_or_default(),
-                    running.read(cx).thread_status(cx),
-                )
-            })
-            .unwrap_or((false, false, false, None));
-
-        let filter = CommandPaletteFilter::global_mut(cx);
-        let debugger_action_types = [
-            TypeId::of::<Detach>(),
-            TypeId::of::<Stop>(),
-            TypeId::of::<ToggleIgnoreBreakpoints>(),
-        ];
-
-        let running_action_types = [TypeId::of::<Pause>()];
-
-        let stopped_action_type = [
-            TypeId::of::<Continue>(),
-            TypeId::of::<StepOver>(),
-            TypeId::of::<StepInto>(),
-            TypeId::of::<StepOut>(),
-            TypeId::of::<ShowStackTrace>(),
-            TypeId::of::<editor::actions::DebuggerRunToCursor>(),
-            TypeId::of::<editor::actions::DebuggerEvaluateSelectedText>(),
-        ];
-
-        let step_back_action_type = [TypeId::of::<StepBack>()];
-        let restart_action_type = [TypeId::of::<Restart>()];
-
-        if has_active_session {
-            filter.show_action_types(debugger_action_types.iter());
-
-            if supports_restart {
-                filter.show_action_types(restart_action_type.iter());
-            } else {
-                filter.hide_action_types(&restart_action_type);
-            }
-
-            if support_step_back {
-                filter.show_action_types(step_back_action_type.iter());
-            } else {
-                filter.hide_action_types(&step_back_action_type);
-            }
-
-            match status {
-                Some(ThreadStatus::Running) => {
-                    filter.show_action_types(running_action_types.iter());
-                    filter.hide_action_types(&stopped_action_type);
-                }
-                Some(ThreadStatus::Stopped) => {
-                    filter.show_action_types(stopped_action_type.iter());
-                    filter.hide_action_types(&running_action_types);
-                }
-                _ => {
-                    filter.hide_action_types(&running_action_types);
-                    filter.hide_action_types(&stopped_action_type);
-                }
-            }
-        } else {
-            // show only the `debug: start`
-            filter.hide_action_types(&debugger_action_types);
-            filter.hide_action_types(&step_back_action_type);
-            filter.hide_action_types(&restart_action_type);
-            filter.hide_action_types(&running_action_types);
-            filter.hide_action_types(&stopped_action_type);
-        }
-    }
-
     pub fn load(
         workspace: WeakEntity<Workspace>,
         cx: &mut AsyncWindowContext,
@@ -233,17 +154,6 @@ impl DebugPanel {
                     )
                 });
 
-                cx.observe_new::<DebugPanel>(|debug_panel, _, cx| {
-                    Self::filter_action_types(debug_panel, cx);
-                })
-                .detach();
-
-                cx.observe(&debug_panel, |_, debug_panel, cx| {
-                    debug_panel.update(cx, |debug_panel, cx| {
-                        Self::filter_action_types(debug_panel, cx);
-                    });
-                })
-                .detach();
                 workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
 
                 debug_panel

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -1,14 +1,17 @@
+use std::any::TypeId;
+
 use dap::debugger_settings::DebuggerSettings;
 use debugger_panel::{DebugPanel, ToggleFocus};
 use editor::Editor;
 use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
-use gpui::{App, EntityInputHandler, actions};
+use gpui::{App, DispatchPhase, EntityInputHandler, actions};
 use new_process_modal::{NewProcessModal, NewProcessMode};
-use project::debugger::{self, breakpoint_store::SourceBreakpoint};
+use project::debugger::{self, breakpoint_store::SourceBreakpoint, session::ThreadStatus};
 use session::DebugSession;
 use settings::Settings;
 use stack_trace_view::StackTraceView;
 use tasks_ui::{Spawn, TaskOverrides};
+use ui::{FluentBuilder, InteractiveElement};
 use util::maybe;
 use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
 
@@ -68,107 +71,20 @@ pub fn init(cx: &mut App) {
                 .register_action(|workspace, _: &ToggleFocus, window, cx| {
                     workspace.toggle_panel_focus::<DebugPanel>(window, cx);
                 })
-                .register_action(|workspace, _: &Pause, _, cx| {
-                    if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
-                        if let Some(active_item) = debug_panel
-                            .read(cx)
-                            .active_session()
-                            .map(|session| session.read(cx).running_state().clone())
-                        {
-                            active_item.update(cx, |item, cx| item.pause_thread(cx))
-                        }
-                    }
-                })
-                .register_action(|workspace, _: &Restart, _, cx| {
-                    if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
-                        if let Some(active_item) = debug_panel
-                            .read(cx)
-                            .active_session()
-                            .map(|session| session.read(cx).running_state().clone())
-                        {
-                            active_item.update(cx, |item, cx| item.restart_session(cx))
-                        }
-                    }
-                })
-                .register_action(|workspace, _: &Continue, _, cx| {
-                    if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
-                        if let Some(active_item) = debug_panel
-                            .read(cx)
-                            .active_session()
-                            .map(|session| session.read(cx).running_state().clone())
-                        {
-                            active_item.update(cx, |item, cx| item.continue_thread(cx))
-                        }
-                    }
-                })
-                .register_action(|workspace, _: &StepInto, _, cx| {
-                    if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
-                        if let Some(active_item) = debug_panel
-                            .read(cx)
-                            .active_session()
-                            .map(|session| session.read(cx).running_state().clone())
-                        {
-                            active_item.update(cx, |item, cx| item.step_in(cx))
-                        }
-                    }
-                })
-                .register_action(|workspace, _: &StepOver, _, cx| {
-                    if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
-                        if let Some(active_item) = debug_panel
-                            .read(cx)
-                            .active_session()
-                            .map(|session| session.read(cx).running_state().clone())
-                        {
-                            active_item.update(cx, |item, cx| item.step_over(cx))
-                        }
-                    }
-                })
-                .register_action(|workspace, _: &StepOut, _, cx| {
-                    if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
-                        if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
-                            panel
-                                .active_session()
-                                .map(|session| session.read(cx).running_state().clone())
-                        }) {
-                            active_item.update(cx, |item, cx| item.step_out(cx))
-                        }
-                    }
-                })
-                .register_action(|workspace, _: &StepBack, _, cx| {
-                    if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
-                        if let Some(active_item) = debug_panel
-                            .read(cx)
-                            .active_session()
-                            .map(|session| session.read(cx).running_state().clone())
-                        {
-                            active_item.update(cx, |item, cx| item.step_back(cx))
-                        }
-                    }
-                })
-                .register_action(|workspace, _: &Stop, _, cx| {
-                    if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
-                        if let Some(active_item) = debug_panel
-                            .read(cx)
-                            .active_session()
-                            .map(|session| session.read(cx).running_state().clone())
-                        {
-                            cx.defer(move |cx| {
-                                active_item.update(cx, |item, cx| item.stop_thread(cx))
-                            })
-                        }
-                    }
-                })
-                .register_action(|workspace, _: &ToggleIgnoreBreakpoints, _, cx| {
-                    if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
-                        if let Some(active_item) = debug_panel
-                            .read(cx)
-                            .active_session()
-                            .map(|session| session.read(cx).running_state().clone())
-                        {
-                            active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
-                        }
-                    }
+                .register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
+                    NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
                 })
+                .register_action(
+                    |workspace: &mut Workspace, _: &RerunLastSession, window, cx| {
+                        let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
+                            return;
+                        };
+
+                        debug_panel.update(cx, |debug_panel, cx| {
+                            debug_panel.rerun_last_session(workspace, window, cx);
+                        })
+                    },
+                )
                 .register_action(
                     |workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
                         workspace.project().update(cx, |project, cx| {
@@ -178,135 +94,236 @@ pub fn init(cx: &mut App) {
                         })
                     },
                 )
-                .register_action(
-                    |workspace: &mut Workspace, _: &ShowStackTrace, window, cx| {
-                        let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
-                            return;
-                        };
+                .register_action_renderer(|div, workspace, _, cx| {
+                    let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
+                        return div;
+                    };
+                    let Some(active_item) = debug_panel
+                        .read(cx)
+                        .active_session()
+                        .map(|session| session.read(cx).running_state().clone())
+                    else {
+                        return div;
+                    };
+                    let running_state = active_item.read(cx);
+                    if running_state.session().read(cx).is_terminated() {
+                        return div;
+                    }
 
-                        if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx) {
-                            let is_active = workspace
-                                .active_item(cx)
-                                .is_some_and(|item| item.item_id() == existing.item_id());
-                            workspace.activate_item(&existing, true, !is_active, window, cx);
-                        } else {
-                            let Some(active_session) = debug_panel.read(cx).active_session() else {
-                                return;
-                            };
+                    let caps = running_state.capabilities(cx);
+                    let supports_restart = caps.supports_restart_request.unwrap_or_default();
+                    let supports_step_back = caps.supports_step_back.unwrap_or_default();
+                    let status = running_state.thread_status(cx);
 
-                            let project = workspace.project();
+                    let active_item = active_item.downgrade();
+                    div.when(status == Some(ThreadStatus::Running), |div| {
+                        let active_item = active_item.clone();
+                        div.on_action(move |_: &Pause, _, cx| {
+                            active_item
+                                .update(cx, |item, cx| item.pause_thread(cx))
+                                .ok();
+                        })
+                    })
+                    .when(status == Some(ThreadStatus::Stopped), |div| {
+                        div.on_action({
+                            let active_item = active_item.clone();
+                            move |_: &StepInto, _, cx| {
+                                active_item.update(cx, |item, cx| item.step_in(cx)).ok();
+                            }
+                        })
+                        .on_action({
+                            let active_item = active_item.clone();
+                            move |_: &StepOver, _, cx| {
+                                active_item.update(cx, |item, cx| item.step_over(cx)).ok();
+                            }
+                        })
+                        .on_action({
+                            let active_item = active_item.clone();
+                            move |_: &StepOut, _, cx| {
+                                active_item.update(cx, |item, cx| item.step_out(cx)).ok();
+                            }
+                        })
+                        .when(supports_step_back, |div| {
+                            let active_item = active_item.clone();
+                            div.on_action(move |_: &StepBack, _, cx| {
+                                active_item.update(cx, |item, cx| item.step_back(cx)).ok();
+                            })
+                        })
+                        .on_action({
+                            let active_item = active_item.clone();
+                            move |_: &Continue, _, cx| {
+                                active_item
+                                    .update(cx, |item, cx| item.continue_thread(cx))
+                                    .ok();
+                            }
+                        })
+                        .on_action(cx.listener(
+                            |workspace, _: &ShowStackTrace, window, cx| {
+                                let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
+                                    return;
+                                };
 
-                            let stack_trace_view = active_session.update(cx, |session, cx| {
-                                session.stack_trace_view(project, window, cx).clone()
-                            });
+                                if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx)
+                                {
+                                    let is_active = workspace
+                                        .active_item(cx)
+                                        .is_some_and(|item| item.item_id() == existing.item_id());
+                                    workspace
+                                        .activate_item(&existing, true, !is_active, window, cx);
+                                } else {
+                                    let Some(active_session) =
+                                        debug_panel.read(cx).active_session()
+                                    else {
+                                        return;
+                                    };
 
-                            workspace.add_item_to_active_pane(
-                                Box::new(stack_trace_view),
-                                None,
-                                true,
-                                window,
-                                cx,
-                            );
-                        }
-                    },
-                )
-                .register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
-                    NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
-                })
-                .register_action(
-                    |workspace: &mut Workspace, _: &RerunLastSession, window, cx| {
-                        let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
-                            return;
-                        };
+                                    let project = workspace.project();
 
-                        debug_panel.update(cx, |debug_panel, cx| {
-                            debug_panel.rerun_last_session(workspace, window, cx);
+                                    let stack_trace_view =
+                                        active_session.update(cx, |session, cx| {
+                                            session.stack_trace_view(project, window, cx).clone()
+                                        });
+
+                                    workspace.add_item_to_active_pane(
+                                        Box::new(stack_trace_view),
+                                        None,
+                                        true,
+                                        window,
+                                        cx,
+                                    );
+                                }
+                            },
+                        ))
+                    })
+                    .when(supports_restart, |div| {
+                        let active_item = active_item.clone();
+                        div.on_action(move |_: &Restart, _, cx| {
+                            active_item
+                                .update(cx, |item, cx| item.restart_session(cx))
+                                .ok();
                         })
-                    },
-                );
+                    })
+                    .on_action({
+                        let active_item = active_item.clone();
+                        move |_: &Stop, _, cx| {
+                            active_item.update(cx, |item, cx| item.stop_thread(cx)).ok();
+                        }
+                    })
+                    .on_action({
+                        let active_item = active_item.clone();
+                        move |_: &ToggleIgnoreBreakpoints, _, cx| {
+                            active_item
+                                .update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
+                                .ok();
+                        }
+                    })
+                });
         })
     })
     .detach();
 
     cx.observe_new({
-        move |editor: &mut Editor, _, cx| {
+        move |editor: &mut Editor, _, _| {
             editor
-                .register_action(cx.listener(
-                    move |editor, _: &editor::actions::DebuggerRunToCursor, _, cx| {
-                        maybe!({
-                            let debug_panel =
-                                editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
-                            let cursor_point: language::Point = editor.selections.newest(cx).head();
-                            let active_session = debug_panel.read(cx).active_session()?;
+                .register_action_renderer(move |editor, window, cx| {
+                    let Some(workspace) = editor.workspace() else {
+                        return;
+                    };
+                    let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else {
+                        return;
+                    };
+                    let Some(active_session) = debug_panel
+                        .clone()
+                        .update(cx, |panel, _| panel.active_session())
+                    else {
+                        return;
+                    };
+                    let editor = cx.entity().downgrade();
+                    window.on_action(TypeId::of::<editor::actions::RunToCursor>(), {
+                        let editor = editor.clone();
+                        let active_session = active_session.clone();
+                        move |_, phase, _, cx| {
+                            if phase != DispatchPhase::Bubble {
+                                return;
+                            }
+                            maybe!({
+                                let (buffer, position, _) = editor
+                                    .update(cx, |editor, cx| {
+                                        let cursor_point: language::Point =
+                                            editor.selections.newest(cx).head();
 
-                            let (buffer, position, _) = editor
-                                .buffer()
-                                .read(cx)
-                                .point_to_buffer_point(cursor_point, cx)?;
+                                        editor
+                                            .buffer()
+                                            .read(cx)
+                                            .point_to_buffer_point(cursor_point, cx)
+                                    })
+                                    .ok()??;
 
-                            let path =
+                                let path =
                                 debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer(
                                     &buffer, cx,
                                 )?;
 
-                            let source_breakpoint = SourceBreakpoint {
-                                row: position.row,
-                                path,
-                                message: None,
-                                condition: None,
-                                hit_condition: None,
-                                state: debugger::breakpoint_store::BreakpointState::Enabled,
-                            };
+                                let source_breakpoint = SourceBreakpoint {
+                                    row: position.row,
+                                    path,
+                                    message: None,
+                                    condition: None,
+                                    hit_condition: None,
+                                    state: debugger::breakpoint_store::BreakpointState::Enabled,
+                                };
 
-                            active_session.update(cx, |session, cx| {
-                                session.running_state().update(cx, |state, cx| {
-                                    if let Some(thread_id) = state.selected_thread_id() {
-                                        state.session().update(cx, |session, cx| {
-                                            session.run_to_position(
-                                                source_breakpoint,
-                                                thread_id,
-                                                cx,
-                                            );
-                                        })
-                                    }
+                                active_session.update(cx, |session, cx| {
+                                    session.running_state().update(cx, |state, cx| {
+                                        if let Some(thread_id) = state.selected_thread_id() {
+                                            state.session().update(cx, |session, cx| {
+                                                session.run_to_position(
+                                                    source_breakpoint,
+                                                    thread_id,
+                                                    cx,
+                                                );
+                                            })
+                                        }
+                                    });
                                 });
-                            });
 
-                            Some(())
-                        });
-                    },
-                ))
-                .detach();
-
-            editor
-                .register_action(cx.listener(
-                    move |editor, _: &editor::actions::DebuggerEvaluateSelectedText, window, cx| {
-                        maybe!({
-                            let debug_panel =
-                                editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
-                            let active_session = debug_panel.read(cx).active_session()?;
+                                Some(())
+                            });
+                        }
+                    });
 
-                            let text = editor.text_for_range(
-                                editor.selections.newest(cx).range(),
-                                &mut None,
-                                window,
-                                cx,
-                            )?;
+                    window.on_action(
+                        TypeId::of::<editor::actions::EvaluateSelectedText>(),
+                        move |_, _, window, cx| {
+                            maybe!({
+                                let text = editor
+                                    .update(cx, |editor, cx| {
+                                        editor.text_for_range(
+                                            editor.selections.newest(cx).range(),
+                                            &mut None,
+                                            window,
+                                            cx,
+                                        )
+                                    })
+                                    .ok()??;
 
-                            active_session.update(cx, |session, cx| {
-                                session.running_state().update(cx, |state, cx| {
-                                    let stack_id = state.selected_stack_frame_id(cx);
+                                active_session.update(cx, |session, cx| {
+                                    session.running_state().update(cx, |state, cx| {
+                                        let stack_id = state.selected_stack_frame_id(cx);
 
-                                    state.session().update(cx, |session, cx| {
-                                        session.evaluate(text, None, stack_id, None, cx).detach();
+                                        state.session().update(cx, |session, cx| {
+                                            session
+                                                .evaluate(text, None, stack_id, None, cx)
+                                                .detach();
+                                        });
                                     });
                                 });
-                            });
 
-                            Some(())
-                        });
-                    },
-                ))
+                                Some(())
+                            });
+                        },
+                    );
+                })
                 .detach();
         }
     })

crates/editor/Cargo.toml 🔗

@@ -35,7 +35,6 @@ assets.workspace = true
 client.workspace = true
 clock.workspace = true
 collections.workspace = true
-command_palette_hooks.workspace = true
 convert_case.workspace = true
 dap.workspace = true
 db.workspace = true

crates/editor/src/actions.rs 🔗

@@ -243,6 +243,8 @@ impl_actions!(
     ]
 );
 
+actions!(debugger, [RunToCursor, EvaluateSelectedText]);
+
 actions!(
     editor,
     [
@@ -426,8 +428,6 @@ actions!(
         DisableBreakpoint,
         EnableBreakpoint,
         EditLogBreakpoint,
-        DebuggerRunToCursor,
-        DebuggerEvaluateSelectedText,
         ToggleAutoSignatureHelp,
         ToggleGitBlameInline,
         OpenGitBlameCommit,

crates/editor/src/editor.rs 🔗

@@ -1054,8 +1054,9 @@ pub struct Editor {
     style: Option<EditorStyle>,
     text_style_refinement: Option<TextStyleRefinement>,
     next_editor_action_id: EditorActionId,
-    editor_actions:
-        Rc<RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&mut Window, &mut Context<Self>)>>>>,
+    editor_actions: Rc<
+        RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&Editor, &mut Window, &mut Context<Self>)>>>,
+    >,
     use_autoclose: bool,
     use_auto_surround: bool,
     auto_replace_emoji_shortcode: bool,
@@ -7541,8 +7542,7 @@ impl Editor {
             "Set Breakpoint"
         };
 
-        let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx)
-            .map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor));
+        let run_to_cursor = window.is_action_available(&RunToCursor, cx);
 
         let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state {
             BreakpointState::Enabled => Some("Disable"),
@@ -7566,7 +7566,7 @@ impl Editor {
                             })
                             .ok();
 
-                        window.dispatch_action(Box::new(DebuggerRunToCursor), cx);
+                        window.dispatch_action(Box::new(RunToCursor), cx);
                     })
                     .separator()
                 })
@@ -19819,6 +19819,21 @@ impl Editor {
         }
     }
 
+    pub fn register_action_renderer(
+        &mut self,
+        listener: impl Fn(&Editor, &mut Window, &mut Context<Editor>) + 'static,
+    ) -> Subscription {
+        let id = self.next_editor_action_id.post_inc();
+        self.editor_actions
+            .borrow_mut()
+            .insert(id, Box::new(listener));
+
+        let editor_actions = self.editor_actions.clone();
+        Subscription::new(move || {
+            editor_actions.borrow_mut().remove(&id);
+        })
+    }
+
     pub fn register_action<A: Action>(
         &mut self,
         listener: impl Fn(&A, &mut Window, &mut App) + 'static,
@@ -19827,7 +19842,7 @@ impl Editor {
         let listener = Arc::new(listener);
         self.editor_actions.borrow_mut().insert(
             id,
-            Box::new(move |window, _| {
+            Box::new(move |_, window, _| {
                 let listener = listener.clone();
                 window.on_action(TypeId::of::<A>(), move |action, phase, window, cx| {
                     let action = action.downcast_ref().unwrap();

crates/editor/src/element.rs 🔗

@@ -187,7 +187,7 @@ impl EditorElement {
         let editor = &self.editor;
         editor.update(cx, |editor, cx| {
             for action in editor.editor_actions.borrow().values() {
-                (action)(window, cx)
+                (action)(editor, window, cx)
             }
         });
 

crates/editor/src/mouse_context_menu.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
-    Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DebuggerEvaluateSelectedText, DisplayPoint,
-    DisplaySnapshot, Editor, FindAllReferences, GoToDeclaration, GoToDefinition,
-    GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode,
-    SelectionExt, ToDisplayPoint, ToggleCodeActions,
+    Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor,
+    EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation,
+    GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionExt,
+    ToDisplayPoint, ToggleCodeActions,
     actions::{Format, FormatSelections},
     selections_collection::SelectionsCollection,
 };
@@ -199,17 +199,14 @@ pub fn deploy_context_menu(
                 .is_some()
         });
 
-        let evaluate_selection = command_palette_hooks::CommandPaletteFilter::try_global(cx)
-            .map_or(false, |filter| {
-                !filter.is_hidden(&DebuggerEvaluateSelectedText)
-            });
+        let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx);
 
         ui::ContextMenu::build(window, cx, |menu, _window, _cx| {
             let builder = menu
                 .on_blur_subscription(Subscription::new(|| {}))
                 .when(evaluate_selection && has_selections, |builder| {
                     builder
-                        .action("Evaluate Selection", Box::new(DebuggerEvaluateSelectedText))
+                        .action("Evaluate Selection", Box::new(EvaluateSelectedText))
                         .separator()
                 })
                 .action("Go to Definition", Box::new(GoToDefinition))

crates/workspace/src/workspace.rs 🔗

@@ -922,7 +922,7 @@ type PromptForOpenPath = Box<
 /// that can be used to register a global action to be triggered from any place in the window.
 pub struct Workspace {
     weak_self: WeakEntity<Self>,
-    workspace_actions: Vec<Box<dyn Fn(Div, &mut Window, &mut Context<Self>) -> Div>>,
+    workspace_actions: Vec<Box<dyn Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div>>,
     zoomed: Option<AnyWeakView>,
     previous_dock_drag_coordinates: Option<Point<Pixels>>,
     zoomed_position: Option<DockPosition>,
@@ -5436,7 +5436,7 @@ impl Workspace {
     ) -> &mut Self {
         let callback = Arc::new(callback);
 
-        self.workspace_actions.push(Box::new(move |div, _, cx| {
+        self.workspace_actions.push(Box::new(move |div, _, _, cx| {
             let callback = callback.clone();
             div.on_action(cx.listener(move |workspace, event, window, cx| {
                 (callback)(workspace, event, window, cx)
@@ -5444,6 +5444,13 @@ impl Workspace {
         }));
         self
     }
+    pub fn register_action_renderer(
+        &mut self,
+        callback: impl Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div + 'static,
+    ) -> &mut Self {
+        self.workspace_actions.push(Box::new(callback));
+        self
+    }
 
     fn add_workspace_actions_listeners(
         &self,
@@ -5452,7 +5459,7 @@ impl Workspace {
         cx: &mut Context<Self>,
     ) -> Div {
         for action in self.workspace_actions.iter() {
-            div = (action)(div, window, cx)
+            div = (action)(div, self, window, cx)
         }
         div
     }

crates/zed/src/zed/app_menus.rs 🔗

@@ -203,6 +203,30 @@ pub fn app_menus() -> Vec<Menu> {
                 MenuItem::action("Previous Problem", editor::actions::GoToPreviousDiagnostic),
             ],
         },
+        Menu {
+            name: "Run".into(),
+            items: vec![
+                MenuItem::action(
+                    "Spawn Task",
+                    zed_actions::Spawn::ViaModal {
+                        reveal_target: None,
+                    },
+                ),
+                MenuItem::action("Start Debugger", debugger_ui::Start),
+                MenuItem::separator(),
+                MenuItem::action("Edit tasks.json...", crate::zed::OpenProjectTasks),
+                MenuItem::action("Edit debug.json...", crate::zed::OpenProjectDebugTasks),
+                MenuItem::separator(),
+                MenuItem::action("Continue", debugger_ui::Continue),
+                MenuItem::action("Step Over", debugger_ui::StepOver),
+                MenuItem::action("Step Into", debugger_ui::StepInto),
+                MenuItem::action("Step Out", debugger_ui::StepOut),
+                MenuItem::separator(),
+                MenuItem::action("Toggle Breakpoint", editor::actions::ToggleBreakpoint),
+                MenuItem::action("Edit Breakpoint", editor::actions::EditLogBreakpoint),
+                MenuItem::action("Clear all Breakpoints", debugger_ui::ClearAllBreakpoints),
+            ],
+        },
         Menu {
             name: "Window".into(),
             items: vec![

crates/zed/src/zed/quick_action_bar.rs 🔗

@@ -133,46 +133,6 @@ impl Render for QuickActionBar {
             )
         });
 
-        let last_run_debug = self
-            .workspace
-            .read_with(cx, |workspace, cx| {
-                workspace
-                    .debugger_provider()
-                    .map(|provider| provider.debug_scenario_scheduled_last(cx))
-                    .unwrap_or_default()
-            })
-            .ok()
-            .unwrap_or_default();
-
-        let run_button = if last_run_debug {
-            QuickActionBarButton::new(
-                "debug",
-                IconName::PlayBug,
-                false,
-                Box::new(debugger_ui::Start),
-                focus_handle.clone(),
-                "Debug",
-                move |_, window, cx| {
-                    window.dispatch_action(Box::new(debugger_ui::Start), cx);
-                },
-            )
-        } else {
-            let action = Box::new(tasks_ui::Spawn::ViaModal {
-                reveal_target: None,
-            });
-            QuickActionBarButton::new(
-                "run",
-                IconName::PlayAlt,
-                false,
-                action.boxed_clone(),
-                focus_handle.clone(),
-                "Spawn Task",
-                move |_, window, cx| {
-                    window.dispatch_action(action.boxed_clone(), cx);
-                },
-            )
-        };
-
         let assistant_button = QuickActionBarButton::new(
             "toggle inline assistant",
             IconName::ZedAssistant,
@@ -601,7 +561,6 @@ impl Render for QuickActionBar {
                 AgentSettings::get_global(cx).enabled && AgentSettings::get_global(cx).button,
                 |bar| bar.child(assistant_button),
             )
-            .child(run_button)
             .children(code_actions_dropdown)
             .children(editor_selections_dropdown)
             .child(editor_settings_dropdown)