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`.
This commit is contained in:
parent
5a0a8ce30a
commit
625bf09830
6 changed files with 259 additions and 12 deletions
|
@ -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": {
|
||||
|
|
|
@ -1072,6 +1072,7 @@ pub struct EditorSnapshot {
|
|||
show_gutter: bool,
|
||||
show_line_numbers: Option<bool>,
|
||||
show_git_diff_gutter: Option<bool>,
|
||||
show_code_actions: Option<bool>,
|
||||
show_runnables: Option<bool>,
|
||||
show_breakpoints: Option<bool>,
|
||||
git_blame_gutter_max_author_length: Option<usize>,
|
||||
|
@ -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<Self>,
|
||||
) -> 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<Option<CodeContextMenu>> {
|
||||
&self.context_menu
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ pub struct EditorSettings {
|
|||
pub snippet_sort_order: SnippetSortOrder,
|
||||
#[serde(default)]
|
||||
pub diagnostics_max_severity: Option<DiagnosticSeverity>,
|
||||
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<DiagnosticSeverity>,
|
||||
|
||||
/// Whether to show code action button at start of buffer line.
|
||||
///
|
||||
/// Default: true
|
||||
pub inline_code_actions: Option<bool>,
|
||||
}
|
||||
|
||||
// Toolbar related settings
|
||||
|
@ -506,7 +512,7 @@ pub struct ToolbarContent {
|
|||
pub agent_review: Option<bool>,
|
||||
/// Whether to display code action buttons in the editor toolbar.
|
||||
///
|
||||
/// Default: true
|
||||
/// Default: false
|
||||
pub code_actions: Option<bool>,
|
||||
}
|
||||
|
||||
|
|
|
@ -1937,6 +1937,159 @@ impl EditorElement {
|
|||
elements
|
||||
}
|
||||
|
||||
fn layout_inline_code_actions(
|
||||
&self,
|
||||
display_point: DisplayPoint,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
line_height: Pixels,
|
||||
snapshot: &EditorSnapshot,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
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<Vec<AnyElement>>,
|
||||
inline_diagnostics: HashMap<DisplayRow, AnyElement>,
|
||||
inline_blame: Option<AnyElement>,
|
||||
inline_code_actions: Option<AnyElement>,
|
||||
blocks: Vec<BlockLayout>,
|
||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
|
||||
highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
},
|
||||
```
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue