diff --git a/assets/icons/bolt.svg b/assets/icons/bolt.svg index 543e72adf8..2688ede2a5 100644 --- a/assets/icons/bolt.svg +++ b/assets/icons/bolt.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/bolt_filled.svg b/assets/icons/bolt_filled.svg new file mode 100644 index 0000000000..543e72adf8 --- /dev/null +++ b/assets/icons/bolt_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/settings/default.json b/assets/settings/default.json index ca236b0f7f..e9032e9c19 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -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": { diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index c69f428148..da37904f0c 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -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, diff --git a/crates/debugger_ui/src/new_session_modal.rs b/crates/debugger_ui/src/new_session_modal.rs index 4c9b0c2067..f388e91d46 100644 --- a/crates/debugger_ui/src/new_session_modal.rs +++ b/crates/debugger_ui/src/new_session_modal.rs @@ -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)), diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 8952f3ce10..79ef7b2733 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -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, + pub deployed_from: Option, // 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 { diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 74498c55b8..57cbd3c24e 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -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>, actions: Option>, debug_scenarios: Vec, @@ -968,12 +970,12 @@ impl CodeActionsItem { } } -pub(crate) struct CodeActionsMenu { +pub struct CodeActionsMenu { pub actions: CodeActionContents, pub buffer: Entity, pub selected_item: usize, pub scroll_handle: UniformListScrollHandle, - pub deployed_from_indicator: Option, + pub deployed_from: Option, } 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, } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5835529fa6..779dfeb2fb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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::(cx).head()); + let multibuffer_point = match &action.deployed_from { + Some(CodeActionSource::Indicator(row)) => { + DisplayPoint::new(*row, 0).to_point(&snapshot) + } + _ => editor.selections.newest::(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> { + &self.context_menu + } + fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context) -> 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 { + pub fn context_menu_origin(&self) -> Option { 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, diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 40e5a37bdb..bbccbb3bf7 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -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, + /// Whether to display code action buttons in the editor toolbar. + /// + /// Default: true + pub code_actions: Option, } /// Scrollbar related settings diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index d0af01f884..fcf5676b37 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 648c8fee3b..8d9bf468d3 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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 }; diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 88327e4347..441a3821c6 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -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, }), ) diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index fc6d46c68e..3f51383f21 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -41,6 +41,7 @@ pub enum IconName { Binary, Blocks, Bolt, + BoltFilled, Book, BookCopy, BookPlus, diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index aa6f8c77a0..ece3ba78d4 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -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)), diff --git a/crates/ui/src/components/stories/list_header.rs b/crates/ui/src/components/stories/list_header.rs index 6109c18794..f7fa068d5a 100644 --- a/crates/ui/src/components/stories/list_header.rs +++ b/crates/ui/src/components/stories/list_header.rs @@ -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)), ) diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 26947ceb7e..9b1b8620a1 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -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, active_item: Option>, @@ -83,7 +86,7 @@ impl QuickActionBar { } impl Render for QuickActionBar { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> 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) } diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index dde9ecf4ce..0a986e928b 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1214,7 +1214,8 @@ or "breadcrumbs": true, "quick_actions": true, "selections_menu": true, - "agent_review": true + "agent_review": true, + "code_actions": true }, ```