From 625bf09830ecde892152c0620512c76c50aa2469 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 22cc6a753e3a07927001c32bd1a6cc27a6f546e7..431a6f3869926c60ca8275f19fbfaafe8a0e5097 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 366df3b97d2fe619386a99eb879406443a5beca1..ef6e743942a78689eec5e4e21db183e23766d2aa 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 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 be29ff624c4eaaab3b39483b2e7ea3c0e8ba3290..ecddfc24b4595e8dc309300bdee526dd048a7d0e 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 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 }, ```