editor: Fix inline blame show/hide not working until buffer interaction (#32683)

We recently fixed the issue of `cx.notify` on every mouse move event
https://github.com/zed-industries/zed/pull/32408. As this perf bug was
there for a long time, we made some not-optimal choices for checking
things like if the mouse is hovering over an element in the prepaint
phase rather than the `mouse_move` listener.

After the mentioned fix, it regressed these code paths as prepaint is
not being called for every other frame, and hence the mouse hovering
logic never triggers. This bug is directly noticeable when the
"cursor_blink" setting is turned off, which notifies the editor on every
second.

This PR fixes that for git inline blame popover by moving logic to
show/hide in `mouse_move` instead of prepaint phase. `cx.notify` is only
get called only when popover is shown or hidden.

Release Notes:

- Fixed git inline blame not correctly showing in Editor on hover when
`cursor_blink` is `false`.
This commit is contained in:
Smit Barmase 2025-06-13 17:53:13 +05:30 committed by GitHub
parent d5b8c21a75
commit aa1cb9c1e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 171 additions and 175 deletions

View file

@ -899,7 +899,6 @@ struct InlineBlamePopoverState {
struct InlineBlamePopover { struct InlineBlamePopover {
position: gpui::Point<Pixels>, position: gpui::Point<Pixels>,
show_task: Option<Task<()>>,
hide_task: Option<Task<()>>, hide_task: Option<Task<()>>,
popover_bounds: Option<Bounds<Pixels>>, popover_bounds: Option<Bounds<Pixels>>,
popover_state: InlineBlamePopoverState, popover_state: InlineBlamePopoverState,
@ -1007,6 +1006,7 @@ pub struct Editor {
mouse_context_menu: Option<MouseContextMenu>, mouse_context_menu: Option<MouseContextMenu>,
completion_tasks: Vec<(CompletionId, Task<()>)>, completion_tasks: Vec<(CompletionId, Task<()>)>,
inline_blame_popover: Option<InlineBlamePopover>, inline_blame_popover: Option<InlineBlamePopover>,
inline_blame_popover_show_task: Option<Task<()>>,
signature_help_state: SignatureHelpState, signature_help_state: SignatureHelpState,
auto_signature_help: Option<bool>, auto_signature_help: Option<bool>,
find_all_references_task_sources: Vec<Anchor>, find_all_references_task_sources: Vec<Anchor>,
@ -1938,6 +1938,7 @@ impl Editor {
mouse_context_menu: None, mouse_context_menu: None,
completion_tasks: Vec::new(), completion_tasks: Vec::new(),
inline_blame_popover: None, inline_blame_popover: None,
inline_blame_popover_show_task: None,
signature_help_state: SignatureHelpState::default(), signature_help_state: SignatureHelpState::default(),
auto_signature_help: None, auto_signature_help: None,
find_all_references_task_sources: Vec::new(), find_all_references_task_sources: Vec::new(),
@ -6276,71 +6277,65 @@ impl Editor {
) { ) {
if let Some(state) = &mut self.inline_blame_popover { if let Some(state) = &mut self.inline_blame_popover {
state.hide_task.take(); state.hide_task.take();
cx.notify();
} else { } else {
let delay = EditorSettings::get_global(cx).hover_popover_delay; let delay = EditorSettings::get_global(cx).hover_popover_delay;
let blame_entry = blame_entry.clone();
let show_task = cx.spawn(async move |editor, cx| { let show_task = cx.spawn(async move |editor, cx| {
cx.background_executor() cx.background_executor()
.timer(std::time::Duration::from_millis(delay)) .timer(std::time::Duration::from_millis(delay))
.await; .await;
editor editor
.update(cx, |editor, cx| { .update(cx, |editor, cx| {
if let Some(state) = &mut editor.inline_blame_popover { editor.inline_blame_popover_show_task.take();
state.show_task = None; let Some(blame) = editor.blame.as_ref() else {
cx.notify(); return;
} };
let blame = blame.read(cx);
let details = blame.details_for_entry(&blame_entry);
let markdown = cx.new(|cx| {
Markdown::new(
details
.as_ref()
.map(|message| message.message.clone())
.unwrap_or_default(),
None,
None,
cx,
)
});
editor.inline_blame_popover = Some(InlineBlamePopover {
position,
hide_task: None,
popover_bounds: None,
popover_state: InlineBlamePopoverState {
scroll_handle: ScrollHandle::new(),
commit_message: details,
markdown,
},
});
cx.notify();
}) })
.ok(); .ok();
}); });
let Some(blame) = self.blame.as_ref() else { self.inline_blame_popover_show_task = Some(show_task);
return;
};
let blame = blame.read(cx);
let details = blame.details_for_entry(&blame_entry);
let markdown = cx.new(|cx| {
Markdown::new(
details
.as_ref()
.map(|message| message.message.clone())
.unwrap_or_default(),
None,
None,
cx,
)
});
self.inline_blame_popover = Some(InlineBlamePopover {
position,
show_task: Some(show_task),
hide_task: None,
popover_bounds: None,
popover_state: InlineBlamePopoverState {
scroll_handle: ScrollHandle::new(),
commit_message: details,
markdown,
},
});
} }
} }
fn hide_blame_popover(&mut self, cx: &mut Context<Self>) { fn hide_blame_popover(&mut self, cx: &mut Context<Self>) {
self.inline_blame_popover_show_task.take();
if let Some(state) = &mut self.inline_blame_popover { if let Some(state) = &mut self.inline_blame_popover {
if state.show_task.is_some() { let hide_task = cx.spawn(async move |editor, cx| {
self.inline_blame_popover.take(); cx.background_executor()
cx.notify(); .timer(std::time::Duration::from_millis(100))
} else { .await;
let hide_task = cx.spawn(async move |editor, cx| { editor
cx.background_executor() .update(cx, |editor, cx| {
.timer(std::time::Duration::from_millis(100)) editor.inline_blame_popover.take();
.await; cx.notify();
editor })
.update(cx, |editor, cx| { .ok();
editor.inline_blame_popover.take(); });
cx.notify(); state.hide_task = Some(hide_task);
})
.ok();
});
state.hide_task = Some(hide_task);
}
} }
} }

View file

@ -108,6 +108,12 @@ struct SelectionLayout {
user_name: Option<SharedString>, user_name: Option<SharedString>,
} }
struct InlineBlameLayout {
element: AnyElement,
bounds: Bounds<Pixels>,
entry: BlameEntry,
}
impl SelectionLayout { impl SelectionLayout {
fn new<T: ToPoint + ToDisplayPoint + Clone>( fn new<T: ToPoint + ToDisplayPoint + Clone>(
selection: Selection<T>, selection: Selection<T>,
@ -950,40 +956,43 @@ impl EditorElement {
return; return;
} }
let text_hitbox = &position_map.text_hitbox;
let text_bounds = text_hitbox.bounds;
let point_for_position = position_map.point_for_position(event.position); let point_for_position = position_map.point_for_position(event.position);
let text_hitbox = &position_map.text_hitbox;
let mut scroll_delta = gpui::Point::<f32>::default(); let scroll_delta = {
let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0); let text_bounds = text_hitbox.bounds;
let top = text_bounds.origin.y + vertical_margin; let mut scroll_delta = gpui::Point::<f32>::default();
let bottom = text_bounds.bottom_left().y - vertical_margin; let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0);
if event.position.y < top { let top = text_bounds.origin.y + vertical_margin;
scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y); let bottom = text_bounds.bottom_left().y - vertical_margin;
} if event.position.y < top {
if event.position.y > bottom { scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y);
scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom); }
} if event.position.y > bottom {
scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom);
}
// We need horizontal width of text // We need horizontal width of text
let style = editor.style.clone().unwrap_or_default(); let style = editor.style.clone().unwrap_or_default();
let font_id = window.text_system().resolve_font(&style.text.font()); let font_id = window.text_system().resolve_font(&style.text.font());
let font_size = style.text.font_size.to_pixels(window.rem_size()); let font_size = style.text.font_size.to_pixels(window.rem_size());
let em_width = window.text_system().em_width(font_id, font_size).unwrap(); let em_width = window.text_system().em_width(font_id, font_size).unwrap();
let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin; let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin;
let scroll_space: Pixels = scroll_margin_x * em_width; let scroll_space: Pixels = scroll_margin_x * em_width;
let left = text_bounds.origin.x + scroll_space; let left = text_bounds.origin.x + scroll_space;
let right = text_bounds.top_right().x - scroll_space; let right = text_bounds.top_right().x - scroll_space;
if event.position.x < left { if event.position.x < left {
scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x); scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x);
} }
if event.position.x > right { if event.position.x > right {
scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right); scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right);
} }
scroll_delta
};
if !editor.has_pending_selection() { if !editor.has_pending_selection() {
let drop_anchor = position_map let drop_anchor = position_map
@ -1063,6 +1072,7 @@ impl EditorElement {
editor: &mut Editor, editor: &mut Editor,
event: &MouseMoveEvent, event: &MouseMoveEvent,
position_map: &PositionMap, position_map: &PositionMap,
inline_blame_bounds: &Option<(Bounds<Pixels>, BlameEntry)>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) { ) {
@ -1073,6 +1083,23 @@ impl EditorElement {
editor.set_gutter_hovered(gutter_hovered, cx); editor.set_gutter_hovered(gutter_hovered, cx);
editor.mouse_cursor_hidden = false; editor.mouse_cursor_hidden = false;
if let Some((bounds, blame_entry)) = inline_blame_bounds {
let mouse_over_inline_blame = bounds.contains(&event.position);
let mouse_over_popover = editor
.inline_blame_popover
.as_ref()
.and_then(|state| state.popover_bounds)
.map_or(false, |bounds| bounds.contains(&event.position));
if mouse_over_inline_blame || mouse_over_popover {
editor.show_blame_popover(&blame_entry, event.position, cx);
} else {
editor.hide_blame_popover(cx);
}
} else {
editor.hide_blame_popover(cx);
}
let breakpoint_indicator = if gutter_hovered { let breakpoint_indicator = if gutter_hovered {
let new_point = position_map let new_point = position_map
.point_for_position(event.position) .point_for_position(event.position)
@ -2278,7 +2305,7 @@ impl EditorElement {
text_hitbox: &Hitbox, text_hitbox: &Hitbox,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Option<AnyElement> { ) -> Option<InlineBlameLayout> {
if !self if !self
.editor .editor
.update(cx, |editor, cx| editor.render_git_blame_inline(window, cx)) .update(cx, |editor, cx| editor.render_git_blame_inline(window, cx))
@ -2307,13 +2334,13 @@ impl EditorElement {
padding * em_width padding * em_width
}; };
let blame_entry = blame let entry = blame
.update(cx, |blame, cx| { .update(cx, |blame, cx| {
blame.blame_for_rows(&[*row_info], cx).next() blame.blame_for_rows(&[*row_info], cx).next()
}) })
.flatten()?; .flatten()?;
let mut element = render_inline_blame_entry(blame_entry.clone(), &self.style, cx)?; let mut element = render_inline_blame_entry(entry.clone(), &self.style, cx)?;
let start_y = content_origin.y let start_y = content_origin.y
+ line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height); + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
@ -2342,24 +2369,19 @@ impl EditorElement {
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let bounds = Bounds::new(absolute_offset, size); let bounds = Bounds::new(absolute_offset, size);
self.layout_blame_entry_popover( self.layout_blame_entry_popover(entry.clone(), blame, line_height, text_hitbox, window, cx);
bounds,
blame_entry,
blame,
line_height,
text_hitbox,
window,
cx,
);
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx); element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx);
Some(element) Some(InlineBlameLayout {
element,
bounds,
entry,
})
} }
fn layout_blame_entry_popover( fn layout_blame_entry_popover(
&self, &self,
parent_bounds: Bounds<Pixels>,
blame_entry: BlameEntry, blame_entry: BlameEntry,
blame: Entity<GitBlame>, blame: Entity<GitBlame>,
line_height: Pixels, line_height: Pixels,
@ -2367,91 +2389,59 @@ impl EditorElement {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) { ) {
let mouse_position = window.mouse_position(); let Some((popover_state, target_point)) = self.editor.read_with(cx, |editor, _| {
let mouse_over_inline_blame = parent_bounds.contains(&mouse_position);
let mouse_over_popover = self.editor.read_with(cx, |editor, _| {
editor editor
.inline_blame_popover .inline_blame_popover
.as_ref() .as_ref()
.and_then(|state| state.popover_bounds) .map(|state| (state.popover_state.clone(), state.position))
.map_or(false, |bounds| bounds.contains(&mouse_position)) }) else {
return;
};
let workspace = self
.editor
.read_with(cx, |editor, _| editor.workspace().map(|w| w.downgrade()));
let maybe_element = workspace.and_then(|workspace| {
render_blame_entry_popover(
blame_entry,
popover_state.scroll_handle,
popover_state.commit_message,
popover_state.markdown,
workspace,
&blame,
window,
cx,
)
}); });
self.editor.update(cx, |editor, cx| { if let Some(mut element) = maybe_element {
if mouse_over_inline_blame || mouse_over_popover { let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
editor.show_blame_popover(&blame_entry, mouse_position, cx); let overall_height = size.height + HOVER_POPOVER_GAP;
let popover_origin = if target_point.y > overall_height {
point(target_point.x, target_point.y - size.height)
} else { } else {
editor.hide_blame_popover(cx); point(
} target_point.x,
}); target_point.y + line_height + HOVER_POPOVER_GAP,
)
};
let should_draw = self.editor.read_with(cx, |editor, _| { let horizontal_offset = (text_hitbox.top_right().x
editor - POPOVER_RIGHT_OFFSET
.inline_blame_popover - (popover_origin.x + size.width))
.as_ref() .min(Pixels::ZERO);
.map_or(false, |state| state.show_task.is_none())
});
if should_draw { let origin = point(popover_origin.x + horizontal_offset, popover_origin.y);
let maybe_element = self.editor.update(cx, |editor, cx| { let popover_bounds = Bounds::new(origin, size);
editor
.workspace() self.editor.update(cx, |editor, _| {
.map(|workspace| workspace.downgrade()) if let Some(state) = &mut editor.inline_blame_popover {
.zip( state.popover_bounds = Some(popover_bounds);
editor }
.inline_blame_popover
.as_ref()
.map(|p| p.popover_state.clone()),
)
.and_then(|(workspace, popover_state)| {
render_blame_entry_popover(
blame_entry,
popover_state.scroll_handle,
popover_state.commit_message,
popover_state.markdown,
workspace,
&blame,
window,
cx,
)
})
}); });
if let Some(mut element) = maybe_element { window.defer_draw(element, origin, 2);
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let origin = self.editor.read_with(cx, |editor, _| {
let target_point = editor
.inline_blame_popover
.as_ref()
.map_or(mouse_position, |state| state.position);
let overall_height = size.height + HOVER_POPOVER_GAP;
let popover_origin = if target_point.y > overall_height {
point(target_point.x, target_point.y - size.height)
} else {
point(
target_point.x,
target_point.y + line_height + HOVER_POPOVER_GAP,
)
};
let horizontal_offset = (text_hitbox.top_right().x
- POPOVER_RIGHT_OFFSET
- (popover_origin.x + size.width))
.min(Pixels::ZERO);
point(popover_origin.x + horizontal_offset, popover_origin.y)
});
let popover_bounds = Bounds::new(origin, size);
self.editor.update(cx, |editor, _| {
if let Some(state) = &mut editor.inline_blame_popover {
state.popover_bounds = Some(popover_bounds);
}
});
window.defer_draw(element, origin, 2);
}
} }
} }
@ -6258,9 +6248,9 @@ impl EditorElement {
} }
fn paint_inline_blame(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) { fn paint_inline_blame(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
if let Some(mut inline_blame) = layout.inline_blame.take() { if let Some(mut blame_layout) = layout.inline_blame_layout.take() {
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| { window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
inline_blame.paint(window, cx); blame_layout.element.paint(window, cx);
}) })
} }
} }
@ -6709,6 +6699,10 @@ impl EditorElement {
window.on_mouse_event({ window.on_mouse_event({
let position_map = layout.position_map.clone(); let position_map = layout.position_map.clone();
let editor = self.editor.clone(); let editor = self.editor.clone();
let inline_blame_bounds = layout
.inline_blame_layout
.as_ref()
.map(|layout| (layout.bounds, layout.entry.clone()));
move |event: &MouseMoveEvent, phase, window, cx| { move |event: &MouseMoveEvent, phase, window, cx| {
if phase == DispatchPhase::Bubble { if phase == DispatchPhase::Bubble {
@ -6722,7 +6716,14 @@ impl EditorElement {
Self::mouse_dragged(editor, event, &position_map, window, cx) Self::mouse_dragged(editor, event, &position_map, window, cx)
} }
Self::mouse_moved(editor, event, &position_map, window, cx) Self::mouse_moved(
editor,
event,
&position_map,
&inline_blame_bounds,
window,
cx,
)
}); });
} }
} }
@ -8392,7 +8393,7 @@ impl Element for EditorElement {
cx, cx,
); );
let mut inline_blame = None; let mut inline_blame_layout = None;
let mut inline_code_actions = None; let mut inline_code_actions = None;
if let Some(newest_selection_head) = newest_selection_head { if let Some(newest_selection_head) = newest_selection_head {
let display_row = newest_selection_head.row(); let display_row = newest_selection_head.row();
@ -8414,7 +8415,7 @@ impl Element for EditorElement {
let line_layout = &line_layouts[line_ix]; let line_layout = &line_layouts[line_ix];
let crease_trailer_layout = crease_trailers[line_ix].as_ref(); let crease_trailer_layout = crease_trailers[line_ix].as_ref();
inline_blame = self.layout_inline_blame( if let Some(layout) = self.layout_inline_blame(
display_row, display_row,
row_info, row_info,
line_layout, line_layout,
@ -8426,8 +8427,8 @@ impl Element for EditorElement {
&text_hitbox, &text_hitbox,
window, window,
cx, cx,
); ) {
if inline_blame.is_some() { inline_blame_layout = Some(layout);
// Blame overrides inline diagnostics // Blame overrides inline diagnostics
inline_diagnostics.remove(&display_row); inline_diagnostics.remove(&display_row);
} }
@ -8756,7 +8757,7 @@ impl Element for EditorElement {
line_numbers, line_numbers,
blamed_display_rows, blamed_display_rows,
inline_diagnostics, inline_diagnostics,
inline_blame, inline_blame_layout,
inline_code_actions, inline_code_actions,
blocks, blocks,
cursors, cursors,
@ -8937,7 +8938,7 @@ pub struct EditorLayout {
display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>, display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
blamed_display_rows: Option<Vec<AnyElement>>, blamed_display_rows: Option<Vec<AnyElement>>,
inline_diagnostics: HashMap<DisplayRow, AnyElement>, inline_diagnostics: HashMap<DisplayRow, AnyElement>,
inline_blame: Option<AnyElement>, inline_blame_layout: Option<InlineBlameLayout>,
inline_code_actions: Option<AnyElement>, inline_code_actions: Option<AnyElement>,
blocks: Vec<BlockLayout>, blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>, highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,