Add Code Actions to the Toolbar (#31236)

Closes issue #31120.


https://github.com/user-attachments/assets/a4b3c86d-7358-49ac-b8d9-e9af50daf671

Release Notes:

- Added a code actions icon to the toolbar. This icon can be disabled by
setting `toolbar.code_actions` to `false`.
This commit is contained in:
smit 2025-05-23 16:55:29 +05:30 committed by GitHub
parent fbc922ad46
commit 1cad1cbbfc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 167 additions and 50 deletions

View file

@ -1,3 +1,3 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.76019 3.50003H6.50231C6.71012 3.50003 6.89761 3.62971 6.95698 3.82346C7.04292 4.01876 6.98823 4.23906 6.83199 4.37656L2.83214 7.87643C2.65558 8.02954 2.39731 8.04204 2.20857 7.90455C2.01967 7.76705 1.95092 7.51706 2.04295 7.30301L3.24462 4.49999H1.48844C1.29423 4.49999 1.10767 4.37031 1.0344 4.17657C0.961132 3.98126 1.01643 3.76096 1.17323 3.62346L5.17261 0.123753C5.34917 -0.0299914 5.60697 -0.0417097 5.79603 0.0954726C5.98508 0.232749 6.05383 0.482177 5.96165 0.69695L4.76013 3.49981L4.76019 3.50003Z" fill="white"/>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.3 1.75L3 7.35H5.8L4.7 12.25L11 6.65H8.2L9.3 1.75Z" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 633 B

After

Width:  |  Height:  |  Size: 227 B

Before After
Before After

View file

@ -0,0 +1,3 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.76019 3.50003H6.50231C6.71012 3.50003 6.89761 3.62971 6.95698 3.82346C7.04292 4.01876 6.98823 4.23906 6.83199 4.37656L2.83214 7.87643C2.65558 8.02954 2.39731 8.04204 2.20857 7.90455C2.01967 7.76705 1.95092 7.51706 2.04295 7.30301L3.24462 4.49999H1.48844C1.29423 4.49999 1.10767 4.37031 1.0344 4.17657C0.961132 3.98126 1.01643 3.76096 1.17323 3.62346L5.17261 0.123753C5.34917 -0.0299914 5.60697 -0.0417097 5.79603 0.0954726C5.98508 0.232749 6.05383 0.482177 5.96165 0.69695L4.76013 3.49981L4.76019 3.50003Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 633 B

View file

@ -322,7 +322,9 @@
// Whether to show the Selections menu in the editor toolbar.
"selections_menu": true,
// Whether to show agent review buttons in the editor toolbar.
"agent_review": true
"agent_review": true,
// Whether to show code action buttons in the editor toolbar.
"code_actions": true
},
// Titlebar related settings
"title_bar": {

View file

@ -679,7 +679,7 @@ async fn test_collaborating_with_code_actions(
editor_b.update_in(cx_b, |editor, window, cx| {
editor.toggle_code_actions(
&ToggleCodeActions {
deployed_from_indicator: None,
deployed_from: None,
quick_launch: false,
},
window,

View file

@ -1123,7 +1123,7 @@ impl PickerDelegate for DebugScenarioDelegate {
let task_kind = &self.candidates[hit.candidate_id].0;
let icon = match task_kind {
Some(TaskSourceKind::Lsp(..)) => Some(Icon::new(IconName::Bolt)),
Some(TaskSourceKind::Lsp(..)) => Some(Icon::new(IconName::BoltFilled)),
Some(TaskSourceKind::UserInput) => Some(Icon::new(IconName::Terminal)),
Some(TaskSourceKind::AbsPath { .. }) => Some(Icon::new(IconName::Settings)),
Some(TaskSourceKind::Worktree { .. }) => Some(Icon::new(IconName::FileTree)),

View file

@ -74,16 +74,22 @@ pub struct SelectToEndOfLine {
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ToggleCodeActions {
// Display row from which the action was deployed.
// Source from which the action was deployed.
#[serde(default)]
#[serde(skip)]
pub deployed_from_indicator: Option<DisplayRow>,
pub deployed_from: Option<CodeActionSource>,
// Run first available task if there is only one.
#[serde(default)]
#[serde(skip)]
pub quick_launch: bool,
}
#[derive(PartialEq, Clone, Debug)]
pub enum CodeActionSource {
Indicator(DisplayRow),
QuickActionBar,
}
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ConfirmCompletion {

View file

@ -26,6 +26,7 @@ use task::ResolvedTask;
use ui::{Color, IntoElement, ListItem, Pixels, Popover, Styled, prelude::*};
use util::ResultExt;
use crate::CodeActionSource;
use crate::editor_settings::SnippetSortOrder;
use crate::hover_popover::{hover_markdown_style, open_markdown_url};
use crate::{
@ -168,6 +169,7 @@ impl CodeContextMenu {
pub enum ContextMenuOrigin {
Cursor,
GutterIndicator(DisplayRow),
QuickActionBar,
}
#[derive(Clone, Debug)]
@ -840,7 +842,7 @@ pub struct AvailableCodeAction {
}
#[derive(Clone)]
pub(crate) struct CodeActionContents {
pub struct CodeActionContents {
tasks: Option<Rc<ResolvedTasks>>,
actions: Option<Rc<[AvailableCodeAction]>>,
debug_scenarios: Vec<DebugScenario>,
@ -968,12 +970,12 @@ impl CodeActionsItem {
}
}
pub(crate) struct CodeActionsMenu {
pub struct CodeActionsMenu {
pub actions: CodeActionContents,
pub buffer: Entity<Buffer>,
pub selected_item: usize,
pub scroll_handle: UniformListScrollHandle,
pub deployed_from_indicator: Option<DisplayRow>,
pub deployed_from: Option<CodeActionSource>,
}
impl CodeActionsMenu {
@ -1042,10 +1044,10 @@ impl CodeActionsMenu {
}
fn origin(&self) -> ContextMenuOrigin {
if let Some(row) = self.deployed_from_indicator {
ContextMenuOrigin::GutterIndicator(row)
} else {
ContextMenuOrigin::Cursor
match &self.deployed_from {
Some(CodeActionSource::Indicator(row)) => ContextMenuOrigin::GutterIndicator(*row),
Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
None => ContextMenuOrigin::Cursor,
}
}

View file

@ -15,7 +15,7 @@
pub mod actions;
mod blink_manager;
mod clangd_ext;
mod code_context_menus;
pub mod code_context_menus;
pub mod display_map;
mod editor_settings;
mod editor_settings_controls;
@ -777,7 +777,7 @@ impl RunnableTasks {
}
#[derive(Clone)]
struct ResolvedTasks {
pub struct ResolvedTasks {
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
position: Anchor,
}
@ -5375,7 +5375,7 @@ impl Editor {
let quick_launch = action.quick_launch;
let mut context_menu = self.context_menu.borrow_mut();
if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() {
if code_actions.deployed_from_indicator == action.deployed_from_indicator {
if code_actions.deployed_from == action.deployed_from {
// Toggle if we're selecting the same one
*context_menu = None;
cx.notify();
@ -5388,7 +5388,7 @@ impl Editor {
}
drop(context_menu);
let snapshot = self.snapshot(window, cx);
let deployed_from_indicator = action.deployed_from_indicator;
let deployed_from = action.deployed_from.clone();
let mut task = self.code_actions_task.take();
let action = action.clone();
cx.spawn_in(window, async move |editor, cx| {
@ -5399,10 +5399,12 @@ impl Editor {
let spawned_test_task = editor.update_in(cx, |editor, window, cx| {
if editor.focus_handle.is_focused(window) {
let multibuffer_point = action
.deployed_from_indicator
.map(|row| DisplayPoint::new(row, 0).to_point(&snapshot))
.unwrap_or_else(|| editor.selections.newest::<Point>(cx).head());
let multibuffer_point = match &action.deployed_from {
Some(CodeActionSource::Indicator(row)) => {
DisplayPoint::new(*row, 0).to_point(&snapshot)
}
_ => editor.selections.newest::<Point>(cx).head(),
};
let (buffer, buffer_row) = snapshot
.buffer_snapshot
.buffer_line_for_row(MultiBufferRow(multibuffer_point.row))
@ -5526,7 +5528,7 @@ impl Editor {
),
selected_item: Default::default(),
scroll_handle: UniformListScrollHandle::default(),
deployed_from_indicator,
deployed_from,
}));
if spawn_straight_away {
if let Some(task) = editor.confirm_code_action(
@ -5746,6 +5748,21 @@ impl Editor {
self.refresh_code_actions(window, cx);
}
pub fn code_actions_enabled(&self, cx: &App) -> bool {
!self.code_action_providers.is_empty()
&& EditorSettings::get_global(cx).toolbar.code_actions
}
pub fn has_available_code_actions(&self) -> bool {
self.available_code_actions
.as_ref()
.is_some_and(|(_, actions)| !actions.is_empty())
}
pub fn context_menu(&self) -> &RefCell<Option<CodeContextMenu>> {
&self.context_menu
}
fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<()> {
let newest_selection = self.selections.newest_anchor().clone();
let newest_selection_adjusted = self.selections.newest_adjusted(cx).clone();
@ -7498,7 +7515,7 @@ impl Editor {
window.focus(&editor.focus_handle(cx));
editor.toggle_code_actions(
&ToggleCodeActions {
deployed_from_indicator: Some(row),
deployed_from: Some(CodeActionSource::Indicator(row)),
quick_launch,
},
window,
@ -7519,7 +7536,7 @@ impl Editor {
.map_or(false, |menu| menu.visible())
}
fn context_menu_origin(&self) -> Option<ContextMenuOrigin> {
pub fn context_menu_origin(&self) -> Option<ContextMenuOrigin> {
self.context_menu
.borrow()
.as_ref()
@ -8538,7 +8555,7 @@ impl Editor {
}
}
fn render_context_menu(
pub fn render_context_menu(
&self,
style: &EditorStyle,
max_height_in_lines: u32,

View file

@ -109,6 +109,7 @@ pub struct Toolbar {
pub quick_actions: bool,
pub selections_menu: bool,
pub agent_review: bool,
pub code_actions: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@ -503,6 +504,10 @@ pub struct ToolbarContent {
///
/// Default: true
pub agent_review: Option<bool>,
/// Whether to display code action buttons in the editor toolbar.
///
/// Default: true
pub code_actions: Option<bool>,
}
/// Scrollbar related settings

View file

@ -14243,7 +14243,7 @@ async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.toggle_code_actions(
&ToggleCodeActions {
deployed_from_indicator: None,
deployed_from: None,
quick_launch: false,
},
window,

View file

@ -1,12 +1,12 @@
use crate::{
ActiveDiagnostic, BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
ChunkRendererContext, ChunkReplacement, ConflictsOurs, ConflictsOursMarker, ConflictsOuter,
ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId,
DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite,
EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight,
LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE,
ChunkRendererContext, ChunkReplacement, CodeActionSource, ConflictsOurs, ConflictsOursMarker,
ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape,
CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead,
DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot,
EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp,
HandleInput, HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown,
LineHighlight, LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator,
Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
StickyHeaderExcerpt, ToPoint, ToggleFold,
@ -2385,7 +2385,7 @@ impl EditorElement {
self.editor.update(cx, |editor, cx| {
let active_task_indicator_row =
if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
deployed_from_indicator,
deployed_from,
actions,
..
})) = editor.context_menu.borrow().as_ref()
@ -2393,7 +2393,10 @@ impl EditorElement {
actions
.tasks()
.map(|tasks| tasks.position.to_display_point(snapshot).row())
.or(*deployed_from_indicator)
.or_else(|| match deployed_from {
Some(CodeActionSource::Indicator(row)) => Some(*row),
_ => None,
})
} else {
None
};

View file

@ -226,7 +226,7 @@ pub fn deploy_context_menu(
.action(
"Show Code Actions",
Box::new(ToggleCodeActions {
deployed_from_indicator: None,
deployed_from: None,
quick_launch: false,
}),
)

View file

@ -41,6 +41,7 @@ pub enum IconName {
Binary,
Blocks,
Bolt,
BoltFilled,
Book,
BookCopy,
BookPlus,

View file

@ -411,7 +411,7 @@ impl PickerDelegate for TasksModalDelegate {
color: Color::Default,
};
let icon = match source_kind {
TaskSourceKind::Lsp(..) => Some(Icon::new(IconName::Bolt)),
TaskSourceKind::Lsp(..) => Some(Icon::new(IconName::BoltFilled)),
TaskSourceKind::UserInput => Some(Icon::new(IconName::Terminal)),
TaskSourceKind::AbsPath { .. } => Some(Icon::new(IconName::Settings)),
TaskSourceKind::Worktree { .. } => Some(Icon::new(IconName::FileTree)),

View file

@ -18,12 +18,12 @@ impl Render for ListHeaderStory {
.child(
ListHeader::new("Section 3")
.start_slot(Icon::new(IconName::BellOff))
.end_slot(IconButton::new("action_1", IconName::Bolt)),
.end_slot(IconButton::new("action_1", IconName::BoltFilled)),
)
.child(Story::label("With multiple meta", cx))
.child(
ListHeader::new("Section 4")
.end_slot(IconButton::new("action_1", IconName::Bolt))
.end_slot(IconButton::new("action_1", IconName::BoltFilled))
.end_slot(IconButton::new("action_2", IconName::Warning))
.end_slot(IconButton::new("action_3", IconName::Plus)),
)

View file

@ -1,17 +1,18 @@
mod markdown_preview;
mod repl_menu;
use assistant_settings::AssistantSettings;
use editor::actions::{
AddSelectionAbove, AddSelectionBelow, DuplicateLineDown, GoToDiagnostic, GoToHunk,
GoToPreviousDiagnostic, GoToPreviousHunk, MoveLineDown, MoveLineUp, SelectAll,
SelectLargerSyntaxNode, SelectNext, SelectSmallerSyntaxNode, ToggleDiagnostics, ToggleGoToLine,
ToggleInlineDiagnostics,
AddSelectionAbove, AddSelectionBelow, CodeActionSource, DuplicateLineDown, GoToDiagnostic,
GoToHunk, GoToPreviousDiagnostic, GoToPreviousHunk, MoveLineDown, MoveLineUp, SelectAll,
SelectLargerSyntaxNode, SelectNext, SelectSmallerSyntaxNode, ToggleCodeActions,
ToggleDiagnostics, ToggleGoToLine, ToggleInlineDiagnostics,
};
use editor::code_context_menus::{CodeContextMenu, ContextMenuOrigin};
use editor::{Editor, EditorSettings};
use gpui::{
Action, ClickEvent, Context, Corner, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, ParentElement, Render, Styled, Subscription, WeakEntity, Window,
Action, AnchoredPositionMode, ClickEvent, Context, Corner, ElementId, Entity, EventEmitter,
FocusHandle, Focusable, InteractiveElement, ParentElement, Render, Styled, Subscription,
WeakEntity, Window, anchored, deferred, point,
};
use project::project_settings::DiagnosticSeverity;
use search::{BufferSearchBar, buffer_search};
@ -26,6 +27,8 @@ use workspace::{
};
use zed_actions::{assistant::InlineAssist, outline::ToggleOutline};
const MAX_CODE_ACTION_MENU_LINES: u32 = 16;
pub struct QuickActionBar {
_inlay_hints_enabled_subscription: Option<Subscription>,
active_item: Option<Box<dyn ItemHandle>>,
@ -83,7 +86,7 @@ impl QuickActionBar {
}
impl Render for QuickActionBar {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let Some(editor) = self.active_editor() else {
return div().id("empty quick action bar");
};
@ -107,7 +110,8 @@ impl Render for QuickActionBar {
editor_value.edit_predictions_enabled_at_cursor(cx);
let supports_minimap = editor_value.supports_minimap(cx);
let minimap_enabled = supports_minimap && editor_value.minimap().is_some();
let has_available_code_actions = editor_value.has_available_code_actions();
let code_action_enabled = editor_value.code_actions_enabled(cx);
let focus_handle = editor_value.focus_handle(cx);
let search_button = editor.is_singleton(cx).then(|| {
@ -141,6 +145,78 @@ impl Render for QuickActionBar {
},
);
let code_actions_dropdown = code_action_enabled.then(|| {
let focus = editor.focus_handle(cx);
let (code_action_menu_active, is_deployed_from_quick_action) = {
let menu_ref = editor.read(cx).context_menu().borrow();
let code_action_menu = menu_ref
.as_ref()
.filter(|menu| matches!(menu, CodeContextMenu::CodeActions(..)));
let is_deployed = code_action_menu.as_ref().map_or(false, |menu| {
matches!(menu.origin(), ContextMenuOrigin::QuickActionBar)
});
(code_action_menu.is_some(), is_deployed)
};
let code_action_element = if is_deployed_from_quick_action {
editor.update(cx, |editor, cx| {
if let Some(style) = editor.style() {
editor.render_context_menu(&style, MAX_CODE_ACTION_MENU_LINES, window, cx)
} else {
None
}
})
} else {
None
};
v_flex()
.child(
IconButton::new("toggle_code_actions_icon", IconName::Bolt)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.disabled(!has_available_code_actions)
.toggle_state(code_action_menu_active)
.when(!code_action_menu_active, |this| {
this.when(has_available_code_actions, |this| {
this.tooltip(Tooltip::for_action_title(
"Code Actions",
&ToggleCodeActions::default(),
))
})
.when(
!has_available_code_actions,
|this| {
this.tooltip(Tooltip::for_action_title(
"No Code Actions Available",
&ToggleCodeActions::default(),
))
},
)
})
.on_click({
let focus = focus.clone();
move |_, window, cx| {
focus.dispatch_action(
&ToggleCodeActions {
deployed_from: Some(CodeActionSource::QuickActionBar),
quick_launch: false,
},
window,
cx,
);
}
}),
)
.children(code_action_element.map(|menu| {
deferred(
anchored()
.position_mode(AnchoredPositionMode::Local)
.position(point(px(20.), px(20.)))
.anchor(Corner::TopRight)
.child(menu),
)
}))
});
let editor_selections_dropdown = selection_menu_enabled.then(|| {
let focus = editor.focus_handle(cx);
@ -487,6 +563,7 @@ impl Render for QuickActionBar {
&& AssistantSettings::get_global(cx).button,
|bar| bar.child(assistant_button),
)
.children(code_actions_dropdown)
.children(editor_selections_dropdown)
.child(editor_settings_dropdown)
}

View file

@ -1214,7 +1214,8 @@ or
"breadcrumbs": true,
"quick_actions": true,
"selections_menu": true,
"agent_review": true
"agent_review": true,
"code_actions": true
},
```