diff --git a/assets/settings/default.json b/assets/settings/default.json index 22cc6a753e..431a6f3869 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -213,6 +213,8 @@ // Whether to show the signature help after completion or a bracket pair inserted. // If `auto_signature_help` is enabled, this setting will be treated as enabled also. "show_signature_help_after_edits": false, + // Whether to show code action button at start of buffer line. + "inline_code_actions": true, // What to do when go to definition yields no results. // // 1. Do nothing: `none` @@ -324,7 +326,7 @@ // Whether to show agent review buttons in the editor toolbar. "agent_review": true, // Whether to show code action buttons in the editor toolbar. - "code_actions": true + "code_actions": false }, // Titlebar related settings "title_bar": { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 366df3b97d..ef6e743942 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1072,6 +1072,7 @@ pub struct EditorSnapshot { show_gutter: bool, show_line_numbers: Option, show_git_diff_gutter: Option, + show_code_actions: Option, show_runnables: Option, show_breakpoints: Option, git_blame_gutter_max_author_length: Option, @@ -2307,6 +2308,7 @@ impl Editor { show_gutter: self.show_gutter, show_line_numbers: self.show_line_numbers, show_git_diff_gutter: self.show_git_diff_gutter, + show_code_actions: self.show_code_actions, show_runnables: self.show_runnables, show_breakpoints: self.show_breakpoints, git_blame_gutter_max_author_length, @@ -5755,7 +5757,7 @@ impl Editor { self.refresh_code_actions(window, cx); } - pub fn code_actions_enabled(&self, cx: &App) -> bool { + pub fn code_actions_enabled_for_toolbar(&self, cx: &App) -> bool { !self.code_action_providers.is_empty() && EditorSettings::get_global(cx).toolbar.code_actions } @@ -5766,6 +5768,53 @@ impl Editor { .is_some_and(|(_, actions)| !actions.is_empty()) } + fn render_inline_code_actions( + &self, + icon_size: ui::IconSize, + display_row: DisplayRow, + is_active: bool, + cx: &mut Context, + ) -> AnyElement { + let show_tooltip = !self.context_menu_visible(); + IconButton::new("inline_code_actions", ui::IconName::BoltFilled) + .icon_size(icon_size) + .shape(ui::IconButtonShape::Square) + .style(ButtonStyle::Transparent) + .icon_color(ui::Color::Hidden) + .toggle_state(is_active) + .when(show_tooltip, |this| { + this.tooltip({ + let focus_handle = self.focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Toggle Code Actions", + &ToggleCodeActions { + deployed_from: None, + quick_launch: false, + }, + &focus_handle, + window, + cx, + ) + } + }) + }) + .on_click(cx.listener(move |editor, _: &ClickEvent, window, cx| { + window.focus(&editor.focus_handle(cx)); + editor.toggle_code_actions( + &crate::actions::ToggleCodeActions { + deployed_from: Some(crate::actions::CodeActionSource::Indicator( + display_row, + )), + quick_launch: false, + }, + window, + cx, + ); + })) + .into_any_element() + } + pub fn context_menu(&self) -> &RefCell> { &self.context_menu } diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index bbccbb3bf7..080c070c5d 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -47,6 +47,7 @@ pub struct EditorSettings { pub snippet_sort_order: SnippetSortOrder, #[serde(default)] pub diagnostics_max_severity: Option, + pub inline_code_actions: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -482,6 +483,11 @@ pub struct EditorSettingsContent { /// Default: warning #[serde(default)] pub diagnostics_max_severity: Option, + + /// Whether to show code action button at start of buffer line. + /// + /// Default: true + pub inline_code_actions: Option, } // Toolbar related settings @@ -506,7 +512,7 @@ pub struct ToolbarContent { pub agent_review: Option, /// Whether to display code action buttons in the editor toolbar. /// - /// Default: true + /// Default: false pub code_actions: Option, } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index be29ff624c..ecddfc24b4 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1937,6 +1937,159 @@ impl EditorElement { elements } + fn layout_inline_code_actions( + &self, + display_point: DisplayPoint, + content_origin: gpui::Point, + scroll_pixel_position: gpui::Point, + line_height: Pixels, + snapshot: &EditorSnapshot, + window: &mut Window, + cx: &mut App, + ) -> Option { + if !snapshot + .show_code_actions + .unwrap_or(EditorSettings::get_global(cx).inline_code_actions) + { + return None; + } + + let icon_size = ui::IconSize::XSmall; + let mut button = self.editor.update(cx, |editor, cx| { + editor.available_code_actions.as_ref()?; + let active = editor + .context_menu + .borrow() + .as_ref() + .and_then(|menu| { + if let crate::CodeContextMenu::CodeActions(CodeActionsMenu { + deployed_from, + .. + }) = menu + { + deployed_from.as_ref() + } else { + None + } + }) + .map_or(false, |source| { + matches!(source, CodeActionSource::Indicator(..)) + }); + Some(editor.render_inline_code_actions(icon_size, display_point.row(), active, cx)) + })?; + + let buffer_point = display_point.to_point(&snapshot.display_snapshot); + + // do not show code action for folded line + if snapshot.is_line_folded(MultiBufferRow(buffer_point.row)) { + return None; + } + + // do not show code action for blank line with cursor + let line_indent = snapshot + .display_snapshot + .buffer_snapshot + .line_indent_for_row(MultiBufferRow(buffer_point.row)); + if line_indent.is_line_blank() { + return None; + } + + const INLINE_SLOT_CHAR_LIMIT: u32 = 4; + const MAX_ALTERNATE_DISTANCE: u32 = 8; + + let excerpt_id = snapshot + .display_snapshot + .buffer_snapshot + .excerpt_containing(buffer_point..buffer_point) + .map(|excerpt| excerpt.id()); + + let is_valid_row = |row_candidate: u32| -> bool { + // move to other row if folded row + if snapshot.is_line_folded(MultiBufferRow(row_candidate)) { + return false; + } + if buffer_point.row == row_candidate { + // move to other row if cursor is in slot + if buffer_point.column < INLINE_SLOT_CHAR_LIMIT { + return false; + } + } else { + let candidate_point = MultiBufferPoint { + row: row_candidate, + column: 0, + }; + let candidate_excerpt_id = snapshot + .display_snapshot + .buffer_snapshot + .excerpt_containing(candidate_point..candidate_point) + .map(|excerpt| excerpt.id()); + // move to other row if different excerpt + if excerpt_id != candidate_excerpt_id { + return false; + } + } + let line_indent = snapshot + .display_snapshot + .buffer_snapshot + .line_indent_for_row(MultiBufferRow(row_candidate)); + // use this row if it's blank + if line_indent.is_line_blank() { + true + } else { + // use this row if code starts after slot + let indent_size = snapshot + .display_snapshot + .buffer_snapshot + .indent_size_for_line(MultiBufferRow(row_candidate)); + indent_size.len >= INLINE_SLOT_CHAR_LIMIT + } + }; + + let new_buffer_row = if is_valid_row(buffer_point.row) { + Some(buffer_point.row) + } else { + let max_row = snapshot.display_snapshot.buffer_snapshot.max_point().row; + (1..=MAX_ALTERNATE_DISTANCE).find_map(|offset| { + let row_above = buffer_point.row.saturating_sub(offset); + let row_below = buffer_point.row + offset; + if row_above != buffer_point.row && is_valid_row(row_above) { + Some(row_above) + } else if row_below <= max_row && is_valid_row(row_below) { + Some(row_below) + } else { + None + } + }) + }?; + + let new_display_row = snapshot + .display_snapshot + .point_to_display_point( + Point { + row: new_buffer_row, + column: buffer_point.column, + }, + text::Bias::Left, + ) + .row(); + + let start_y = content_origin.y + + ((new_display_row.as_f32() - (scroll_pixel_position.y / line_height)) * line_height) + + (line_height / 2.0) + - (icon_size.square(window, cx) / 2.); + let start_x = content_origin.x - scroll_pixel_position.x + (window.rem_size() * 0.1); + + let absolute_offset = gpui::point(start_x, start_y); + button.layout_as_root(gpui::AvailableSpace::min_size(), window, cx); + button.prepaint_as_root( + absolute_offset, + gpui::AvailableSpace::min_size(), + window, + cx, + ); + Some(button) + } + fn layout_inline_blame( &self, display_row: DisplayRow, @@ -5304,6 +5457,7 @@ impl EditorElement { self.paint_cursors(layout, window, cx); self.paint_inline_diagnostics(layout, window, cx); self.paint_inline_blame(layout, window, cx); + self.paint_inline_code_actions(layout, window, cx); self.paint_diff_hunk_controls(layout, window, cx); window.with_element_namespace("crease_trailers", |window| { for trailer in layout.crease_trailers.iter_mut().flatten() { @@ -5929,6 +6083,19 @@ impl EditorElement { } } + fn paint_inline_code_actions( + &mut self, + layout: &mut EditorLayout, + window: &mut Window, + cx: &mut App, + ) { + if let Some(mut inline_code_actions) = layout.inline_code_actions.take() { + window.paint_layer(layout.position_map.text_hitbox.bounds, |window| { + inline_code_actions.paint(window, cx); + }) + } + } + fn paint_diff_hunk_controls( &mut self, layout: &mut EditorLayout, @@ -7984,15 +8151,27 @@ impl Element for EditorElement { ); let mut inline_blame = None; + let mut inline_code_actions = None; if let Some(newest_selection_head) = newest_selection_head { let display_row = newest_selection_head.row(); if (start_row..end_row).contains(&display_row) && !row_block_types.contains_key(&display_row) { + inline_code_actions = self.layout_inline_code_actions( + newest_selection_head, + content_origin, + scroll_pixel_position, + line_height, + &snapshot, + window, + cx, + ); + let line_ix = display_row.minus(start_row) as usize; let row_info = &row_infos[line_ix]; let line_layout = &line_layouts[line_ix]; let crease_trailer_layout = crease_trailers[line_ix].as_ref(); + inline_blame = self.layout_inline_blame( display_row, row_info, @@ -8336,6 +8515,7 @@ impl Element for EditorElement { blamed_display_rows, inline_diagnostics, inline_blame, + inline_code_actions, blocks, cursors, visible_cursors, @@ -8516,6 +8696,7 @@ pub struct EditorLayout { blamed_display_rows: Option>, inline_diagnostics: HashMap, inline_blame: Option, + inline_code_actions: Option, blocks: Vec, highlighted_ranges: Vec<(Range, Hsla)>, highlighted_gutter_ranges: Vec<(Range, Hsla)>, diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 9b1b8620a1..71b17abab4 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -111,7 +111,7 @@ impl Render for QuickActionBar { 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 code_action_enabled = editor_value.code_actions_enabled_for_toolbar(cx); let focus_handle = editor_value.focus_handle(cx); let search_button = editor.is_singleton(cx).then(|| { @@ -147,17 +147,16 @@ 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 is_deployed = { 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| { + 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 { + let code_action_element = if is_deployed { editor.update(cx, |editor, cx| { if let Some(style) = editor.style() { editor.render_context_menu(&style, MAX_CODE_ACTION_MENU_LINES, window, cx) @@ -174,8 +173,8 @@ impl Render for QuickActionBar { .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .disabled(!has_available_code_actions) - .toggle_state(code_action_menu_active) - .when(!code_action_menu_active, |this| { + .toggle_state(is_deployed) + .when(!is_deployed, |this| { this.when(has_available_code_actions, |this| { this.tooltip(Tooltip::for_action_title( "Code Actions", diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 0a986e928b..91cb60a396 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1203,6 +1203,16 @@ or } ``` +### Show Inline Code Actions + +- Description: Whether to show code action button at start of buffer line. +- Setting: `inline_code_actions` +- Default: `true` + +**Options** + +`boolean` values + ## Editor Toolbar - Description: Whether or not to show various elements in the editor toolbar. @@ -1215,7 +1225,7 @@ or "quick_actions": true, "selections_menu": true, "agent_review": true, - "code_actions": true + "code_actions": false }, ```