From e30ee62e45111dd6ccdcda7ffe3b00343e41841a Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 26 May 2025 19:41:19 +0530 Subject: [PATCH] editor: Inline Code Actions Indicator (#31432) Follow up to https://github.com/zed-industries/zed/pull/30140 and https://github.com/zed-industries/zed/pull/31236 This PR introduces an inline code action indicator that shows up at the start of a buffer line when there's enough space. If space is tight, it adjusts to lines above or below instead. It also adjusts when cursor is near indicator. The indicator won't appear if there's no space within about 8 rows in either direction, and it also stays hidden for folded ranges. It also won't show up in case there is not space in multi buffer excerpt. These cases account for very little because practically all languages do have indents. https://github.com/user-attachments/assets/1363ee8a-3178-4665-89a7-c86c733f2885 This PR also sets the existing `toolbar.code_actions` setting to `false` in favor of this. Release Notes: - Added code action indicator which shows up inline at the start of the row. This can be disabled by setting `inline_code_actions` to `false`. --- assets/settings/default.json | 4 +- crates/editor/src/editor.rs | 51 ++++++- crates/editor/src/editor_settings.rs | 8 +- crates/editor/src/element.rs | 181 +++++++++++++++++++++++++ crates/zed/src/zed/quick_action_bar.rs | 15 +- docs/src/configuring-zed.md | 12 +- 6 files changed, 259 insertions(+), 12 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index e9032e9c19b456a26d79a25878a13e0a2dc934d5..da7d31e9334eeef36c0cabaf09d473ef4a52a58a 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 569698f04cabac0499081ef75ce50373e5c7ddbe..3219e93caba6bfdc66a5a2d1f3babad93e66221f 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, @@ -2306,6 +2307,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, @@ -5754,7 +5756,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 } @@ -5765,6 +5767,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 bbccbb3bf728bcb45bf284679dd16172003440c7..080c070c5d22c1aebdcb0d4f778ba1f2fc11ed43 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 8d9bf468d3e565ba6606df0b328ac0c81fd76808..4d3941c58ae3423f8b7eb77f704f067b250918e1 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1930,6 +1930,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, @@ -5297,6 +5450,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() { @@ -5920,6 +6074,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, @@ -7969,15 +8136,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, @@ -8321,6 +8500,7 @@ impl Element for EditorElement { blamed_display_rows, inline_diagnostics, inline_blame, + inline_code_actions, blocks, cursors, visible_cursors, @@ -8500,6 +8680,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 9b1b8620a1911a7508537a8985a74983e2be20a7..71b17abab4d020cdfd354e3e23ee43e56f7158ce 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 0a986e928b8c65c3d1065e5051700f8495810aae..91cb60a396396c901d224f7d2aa601b326fce101 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 }, ```