edit predictions: Split layout_edit_prediction popover (#25463)

This function has grown a lot and it was getting really hard to
navigate. This PR splits it into smaller methods and moves it into
`Editor` with the rest of the edit prediction popovers' code.

I think there are opportunities to consolidate the many popovers we
have, but we'll do that separately.

Release Notes:

- N/A
This commit is contained in:
Agus Zubiaga 2025-02-24 16:16:19 -03:00 committed by GitHub
parent d8694510b5
commit ceb7fc2cb2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 542 additions and 465 deletions

View file

@ -63,7 +63,7 @@ pub use editor_settings::{
CurrentLineHighlight, EditorSettings, ScrollBeyondLastLine, SearchSettings, ShowScrollbar,
};
pub use editor_settings_controls::*;
use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap};
use element::{layout_line, AcceptEditPredictionBinding, LineWithInvisibles, PositionMap};
pub use element::{
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
};
@ -82,7 +82,7 @@ use git::blame::GitBlame;
use gpui::{
div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation,
AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Background, Bounds,
ClipboardEntry, ClipboardItem, Context, DispatchPhase, Entity, EntityInputHandler,
ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler,
EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global,
HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,
ParentElement, Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, Task,
@ -113,6 +113,7 @@ use persistence::DB;
pub use proposed_changes_editor::{
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
};
use smallvec::smallvec;
use std::iter::Peekable;
use task::{ResolvedTask, TaskTemplate, TaskVariables};
@ -5782,6 +5783,524 @@ impl Editor {
.map(|menu| menu.origin())
}
const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.);
const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.);
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_popover(
&mut self,
text_bounds: &Bounds<Pixels>,
content_origin: gpui::Point<Pixels>,
editor_snapshot: &EditorSnapshot,
visible_row_range: Range<DisplayRow>,
scroll_top: f32,
scroll_bottom: f32,
line_layouts: &[LineWithInvisibles],
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
newest_selection_head: Option<DisplayPoint>,
editor_width: Pixels,
style: &EditorStyle,
window: &mut Window,
cx: &mut App,
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
let active_inline_completion = self.active_inline_completion.as_ref()?;
if self.edit_prediction_visible_in_cursor_popover(true) {
return None;
}
match &active_inline_completion.completion {
InlineCompletion::Move { target, .. } => {
let target_display_point = target.to_display_point(editor_snapshot);
if self.edit_prediction_requires_modifier() {
if !self.edit_prediction_preview_is_active() {
return None;
}
self.render_edit_prediction_modifier_jump_popover(
text_bounds,
content_origin,
visible_row_range,
line_layouts,
line_height,
scroll_pixel_position,
newest_selection_head,
target_display_point,
window,
cx,
)
} else {
self.render_edit_prediction_eager_jump_popover(
text_bounds,
content_origin,
editor_snapshot,
visible_row_range,
scroll_top,
scroll_bottom,
line_height,
scroll_pixel_position,
target_display_point,
editor_width,
window,
cx,
)
}
}
InlineCompletion::Edit {
display_mode: EditDisplayMode::Inline,
..
} => None,
InlineCompletion::Edit {
display_mode: EditDisplayMode::TabAccept,
edits,
..
} => {
let range = &edits.first()?.0;
let target_display_point = range.end.to_display_point(editor_snapshot);
self.render_edit_prediction_end_of_line_popover(
"Accept",
editor_snapshot,
visible_row_range,
target_display_point,
line_height,
scroll_pixel_position,
content_origin,
editor_width,
window,
cx,
)
}
InlineCompletion::Edit {
edits,
edit_preview,
display_mode: EditDisplayMode::DiffPopover,
snapshot,
} => self.render_edit_prediction_diff_popover(
text_bounds,
content_origin,
editor_snapshot,
visible_row_range,
line_layouts,
line_height,
scroll_pixel_position,
newest_selection_head,
editor_width,
style,
edits,
edit_preview,
snapshot,
window,
cx,
),
}
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_modifier_jump_popover(
&mut self,
text_bounds: &Bounds<Pixels>,
content_origin: gpui::Point<Pixels>,
visible_row_range: Range<DisplayRow>,
line_layouts: &[LineWithInvisibles],
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
newest_selection_head: Option<DisplayPoint>,
target_display_point: DisplayPoint,
window: &mut Window,
cx: &mut App,
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
let scrolled_content_origin =
content_origin - gpui::Point::new(scroll_pixel_position.x, Pixels(0.0));
const SCROLL_PADDING_Y: Pixels = px(12.);
if target_display_point.row() < visible_row_range.start {
return self.render_edit_prediction_scroll_popover(
|_| SCROLL_PADDING_Y,
IconName::ArrowUp,
visible_row_range,
line_layouts,
newest_selection_head,
scrolled_content_origin,
window,
cx,
);
} else if target_display_point.row() >= visible_row_range.end {
return self.render_edit_prediction_scroll_popover(
|size| text_bounds.size.height - size.height - SCROLL_PADDING_Y,
IconName::ArrowDown,
visible_row_range,
line_layouts,
newest_selection_head,
scrolled_content_origin,
window,
cx,
);
}
const POLE_WIDTH: Pixels = px(2.);
let mut element = v_flex()
.items_end()
.child(
self.render_edit_prediction_line_popover("Jump", None, window, cx)?
.rounded_br(px(0.))
.rounded_tr(px(0.))
.border_r_2(),
)
.child(
div()
.w(POLE_WIDTH)
.bg(Editor::edit_prediction_callout_popover_border_color(cx))
.h(line_height),
)
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let line_layout =
line_layouts.get(target_display_point.row().minus(visible_row_range.start) as usize)?;
let target_column = target_display_point.column() as usize;
let target_x = line_layout.x_for_index(target_column);
let target_y =
(target_display_point.row().as_f32() * line_height) - scroll_pixel_position.y;
let mut origin = scrolled_content_origin + point(target_x, target_y)
- point(size.width - POLE_WIDTH, size.height - line_height);
origin.x = origin.x.max(content_origin.x);
element.prepaint_at(origin, window, cx);
Some((element, origin))
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_scroll_popover(
&mut self,
to_y: impl Fn(Size<Pixels>) -> Pixels,
scroll_icon: IconName,
visible_row_range: Range<DisplayRow>,
line_layouts: &[LineWithInvisibles],
newest_selection_head: Option<DisplayPoint>,
scrolled_content_origin: gpui::Point<Pixels>,
window: &mut Window,
cx: &mut App,
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
let mut element = self
.render_edit_prediction_line_popover("Scroll", Some(scroll_icon), window, cx)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let cursor = newest_selection_head?;
let cursor_row_layout =
line_layouts.get(cursor.row().minus(visible_row_range.start) as usize)?;
let cursor_column = cursor.column() as usize;
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
let origin = scrolled_content_origin + point(cursor_character_x, to_y(size));
element.prepaint_at(origin, window, cx);
Some((element, origin))
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_eager_jump_popover(
&mut self,
text_bounds: &Bounds<Pixels>,
content_origin: gpui::Point<Pixels>,
editor_snapshot: &EditorSnapshot,
visible_row_range: Range<DisplayRow>,
scroll_top: f32,
scroll_bottom: f32,
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
target_display_point: DisplayPoint,
editor_width: Pixels,
window: &mut Window,
cx: &mut App,
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
if target_display_point.row().as_f32() < scroll_top {
let mut element = self
.render_edit_prediction_line_popover(
"Jump to Edit",
Some(IconName::ArrowUp),
window,
cx,
)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let offset = point(
(text_bounds.size.width - size.width) / 2.,
Self::EDIT_PREDICTION_POPOVER_PADDING_Y,
);
let origin = text_bounds.origin + offset;
element.prepaint_at(origin, window, cx);
Some((element, origin))
} else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
let mut element = self
.render_edit_prediction_line_popover(
"Jump to Edit",
Some(IconName::ArrowDown),
window,
cx,
)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let offset = point(
(text_bounds.size.width - size.width) / 2.,
text_bounds.size.height - size.height - Self::EDIT_PREDICTION_POPOVER_PADDING_Y,
);
let origin = text_bounds.origin + offset;
element.prepaint_at(origin, window, cx);
Some((element, origin))
} else {
self.render_edit_prediction_end_of_line_popover(
"Jump to Edit",
editor_snapshot,
visible_row_range,
target_display_point,
line_height,
scroll_pixel_position,
content_origin,
editor_width,
window,
cx,
)
}
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_end_of_line_popover(
self: &mut Editor,
label: &'static str,
editor_snapshot: &EditorSnapshot,
visible_row_range: Range<DisplayRow>,
target_display_point: DisplayPoint,
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
content_origin: gpui::Point<Pixels>,
editor_width: Pixels,
window: &mut Window,
cx: &mut App,
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
let target_line_end = DisplayPoint::new(
target_display_point.row(),
editor_snapshot.line_len(target_display_point.row()),
);
let mut element = self
.render_edit_prediction_line_popover(label, None, window, cx)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let line_origin = self.display_to_pixel_point(target_line_end, editor_snapshot, window)?;
let start_point = content_origin - point(scroll_pixel_position.x, Pixels::ZERO);
let mut origin = start_point
+ line_origin
+ point(Self::EDIT_PREDICTION_POPOVER_PADDING_X, Pixels::ZERO);
origin.x = origin.x.max(content_origin.x);
let max_x = content_origin.x + editor_width - size.width;
if origin.x > max_x {
let offset = line_height + Self::EDIT_PREDICTION_POPOVER_PADDING_Y;
let icon = if visible_row_range.contains(&(target_display_point.row() + 2)) {
origin.y += offset;
IconName::ArrowUp
} else {
origin.y -= offset;
IconName::ArrowDown
};
element = self
.render_edit_prediction_line_popover(label, Some(icon), window, cx)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
origin.x = content_origin.x + editor_width - size.width - px(2.);
}
element.prepaint_at(origin, window, cx);
Some((element, origin))
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_diff_popover(
self: &Editor,
text_bounds: &Bounds<Pixels>,
content_origin: gpui::Point<Pixels>,
editor_snapshot: &EditorSnapshot,
visible_row_range: Range<DisplayRow>,
line_layouts: &[LineWithInvisibles],
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
newest_selection_head: Option<DisplayPoint>,
editor_width: Pixels,
style: &EditorStyle,
edits: &Vec<(Range<Anchor>, String)>,
edit_preview: &Option<language::EditPreview>,
snapshot: &language::BufferSnapshot,
window: &mut Window,
cx: &mut App,
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
let edit_start = edits
.first()
.unwrap()
.0
.start
.to_display_point(editor_snapshot);
let edit_end = edits
.last()
.unwrap()
.0
.end
.to_display_point(editor_snapshot);
let is_visible = visible_row_range.contains(&edit_start.row())
|| visible_row_range.contains(&edit_end.row());
if !is_visible {
return None;
}
let highlighted_edits =
crate::inline_completion_edit_text(&snapshot, edits, edit_preview.as_ref()?, false, cx);
let styled_text = highlighted_edits.to_styled_text(&style.text);
let line_count = highlighted_edits.text.lines().count();
const BORDER_WIDTH: Pixels = px(1.);
let mut element = h_flex()
.items_start()
.child(
h_flex()
.bg(cx.theme().colors().editor_background)
.border(BORDER_WIDTH)
.shadow_sm()
.border_color(cx.theme().colors().border)
.rounded_l_lg()
.when(line_count > 1, |el| el.rounded_br_lg())
.pr_1()
.child(styled_text),
)
.child(
h_flex()
.h(line_height + BORDER_WIDTH * px(2.))
.px_1p5()
.gap_1()
// Workaround: For some reason, there's a gap if we don't do this
.ml(-BORDER_WIDTH)
.shadow(smallvec![gpui::BoxShadow {
color: gpui::black().opacity(0.05),
offset: point(px(1.), px(1.)),
blur_radius: px(2.),
spread_radius: px(0.),
}])
.bg(Editor::edit_prediction_line_popover_bg_color(cx))
.border(BORDER_WIDTH)
.border_color(cx.theme().colors().border)
.rounded_r_lg()
.children(self.render_edit_prediction_accept_keybind(window, cx)),
)
.into_any();
let longest_row =
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
let longest_line_width = if visible_row_range.contains(&longest_row) {
line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width
} else {
layout_line(
longest_row,
editor_snapshot,
style,
editor_width,
|_| false,
window,
cx,
)
.width
};
let viewport_bounds =
Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
right: -EditorElement::SCROLLBAR_WIDTH,
..Default::default()
});
let x_after_longest =
text_bounds.origin.x + longest_line_width + Self::EDIT_PREDICTION_POPOVER_PADDING_X
- scroll_pixel_position.x;
let element_bounds = element.layout_as_root(AvailableSpace::min_size(), window, cx);
// Fully visible if it can be displayed within the window (allow overlapping other
// panes). However, this is only allowed if the popover starts within text_bounds.
let can_position_to_the_right = x_after_longest < text_bounds.right()
&& x_after_longest + element_bounds.width < viewport_bounds.right();
let mut origin = if can_position_to_the_right {
point(
x_after_longest,
text_bounds.origin.y + edit_start.row().as_f32() * line_height
- scroll_pixel_position.y,
)
} else {
let cursor_row = newest_selection_head.map(|head| head.row());
let above_edit = edit_start
.row()
.0
.checked_sub(line_count as u32)
.map(DisplayRow);
let below_edit = Some(edit_end.row() + 1);
let above_cursor =
cursor_row.and_then(|row| row.0.checked_sub(line_count as u32).map(DisplayRow));
let below_cursor = cursor_row.map(|cursor_row| cursor_row + 1);
// Place the edit popover adjacent to the edit if there is a location
// available that is onscreen and does not obscure the cursor. Otherwise,
// place it adjacent to the cursor.
let row_target = [above_edit, below_edit, above_cursor, below_cursor]
.into_iter()
.flatten()
.find(|&start_row| {
let end_row = start_row + line_count as u32;
visible_row_range.contains(&start_row)
&& visible_row_range.contains(&end_row)
&& cursor_row.map_or(true, |cursor_row| {
!((start_row..end_row).contains(&cursor_row))
})
})?;
content_origin
+ point(
-scroll_pixel_position.x,
row_target.as_f32() * line_height - scroll_pixel_position.y,
)
};
origin.x -= BORDER_WIDTH;
window.defer_draw(element, origin, 1);
// Do not return an element, since it will already be drawn due to defer_draw.
None
}
fn edit_prediction_cursor_popover_height(&self) -> Pixels {
px(30.)
}

View file

@ -3711,451 +3711,6 @@ impl EditorElement {
}
}
const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.);
const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.);
#[allow(clippy::too_many_arguments)]
fn layout_edit_prediction_popover(
&self,
text_bounds: &Bounds<Pixels>,
content_origin: gpui::Point<Pixels>,
editor_snapshot: &EditorSnapshot,
visible_row_range: Range<DisplayRow>,
scroll_top: f32,
scroll_bottom: f32,
line_layouts: &[LineWithInvisibles],
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
newest_selection_head: Option<DisplayPoint>,
editor_width: Pixels,
style: &EditorStyle,
window: &mut Window,
cx: &mut App,
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
let editor = self.editor.read(cx);
let active_inline_completion = editor.active_inline_completion.as_ref()?;
if editor.edit_prediction_visible_in_cursor_popover(true) {
return None;
}
// Adjust text origin for horizontal scrolling (in some cases here)
let start_point = content_origin - gpui::Point::new(scroll_pixel_position.x, Pixels(0.0));
// Clamp left offset after extreme scrollings
let clamp_start = |point: gpui::Point<Pixels>| gpui::Point {
x: point.x.max(content_origin.x),
y: point.y,
};
match &active_inline_completion.completion {
InlineCompletion::Move { target, .. } => {
let target_display_point = target.to_display_point(editor_snapshot);
if editor.edit_prediction_requires_modifier() {
if !editor.edit_prediction_preview_is_active() {
return None;
}
if target_display_point.row() < visible_row_range.start {
let mut element = editor
.render_edit_prediction_line_popover(
"Scroll",
Some(IconName::ArrowUp),
window,
cx,
)?
.into_any();
element.layout_as_root(AvailableSpace::min_size(), window, cx);
let cursor = newest_selection_head?;
let cursor_row_layout = line_layouts
.get(cursor.row().minus(visible_row_range.start) as usize)?;
let cursor_column = cursor.column() as usize;
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
const PADDING_Y: Pixels = px(12.);
let origin = start_point + point(cursor_character_x, PADDING_Y);
element.prepaint_at(origin, window, cx);
return Some((element, origin));
} else if target_display_point.row() >= visible_row_range.end {
let mut element = editor
.render_edit_prediction_line_popover(
"Scroll",
Some(IconName::ArrowDown),
window,
cx,
)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let cursor = newest_selection_head?;
let cursor_row_layout = line_layouts
.get(cursor.row().minus(visible_row_range.start) as usize)?;
let cursor_column = cursor.column() as usize;
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
const PADDING_Y: Pixels = px(12.);
let origin = start_point
+ point(
cursor_character_x,
text_bounds.size.height - size.height - PADDING_Y,
);
element.prepaint_at(origin, window, cx);
return Some((element, origin));
} else {
const POLE_WIDTH: Pixels = px(2.);
let mut element = v_flex()
.items_end()
.child(
editor
.render_edit_prediction_line_popover("Jump", None, window, cx)?
.rounded_br(px(0.))
.rounded_tr(px(0.))
.border_r_2(),
)
.child(
div()
.w(POLE_WIDTH)
.bg(Editor::edit_prediction_callout_popover_border_color(cx))
.h(line_height),
)
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let line_layout =
line_layouts
.get(target_display_point.row().minus(visible_row_range.start)
as usize)?;
let target_column = target_display_point.column() as usize;
let target_x = line_layout.x_for_index(target_column);
let target_y = (target_display_point.row().as_f32() * line_height)
- scroll_pixel_position.y;
let origin = clamp_start(
start_point + point(target_x, target_y)
- point(size.width - POLE_WIDTH, size.height - line_height),
);
element.prepaint_at(origin, window, cx);
return Some((element, origin));
}
}
if target_display_point.row().as_f32() < scroll_top {
let mut element = editor
.render_edit_prediction_line_popover(
"Jump to Edit",
Some(IconName::ArrowUp),
window,
cx,
)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let offset = point(
(text_bounds.size.width - size.width) / 2.,
Self::EDIT_PREDICTION_POPOVER_PADDING_Y,
);
let origin = text_bounds.origin + offset;
element.prepaint_at(origin, window, cx);
Some((element, origin))
} else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
let mut element = editor
.render_edit_prediction_line_popover(
"Jump to Edit",
Some(IconName::ArrowDown),
window,
cx,
)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let offset = point(
(text_bounds.size.width - size.width) / 2.,
text_bounds.size.height
- size.height
- Self::EDIT_PREDICTION_POPOVER_PADDING_Y,
);
let origin = text_bounds.origin + offset;
element.prepaint_at(origin, window, cx);
Some((element, origin))
} else {
return self.layout_edit_prediction_end_of_line_popover(
"Jump to Edit",
editor_snapshot,
visible_row_range,
target_display_point,
line_height,
scroll_pixel_position,
content_origin,
editor_width,
window,
cx,
);
}
}
InlineCompletion::Edit {
edits,
edit_preview,
display_mode,
snapshot,
} => {
if self.editor.read(cx).has_visible_completions_menu() {
return None;
}
let edit_start = edits
.first()
.unwrap()
.0
.start
.to_display_point(editor_snapshot);
let edit_end = edits
.last()
.unwrap()
.0
.end
.to_display_point(editor_snapshot);
let is_visible = visible_row_range.contains(&edit_start.row())
|| visible_row_range.contains(&edit_end.row());
if !is_visible {
return None;
}
match display_mode {
EditDisplayMode::TabAccept => {
let range = &edits.first()?.0;
let target_display_point = range.end.to_display_point(editor_snapshot);
return self.layout_edit_prediction_end_of_line_popover(
"Accept",
editor_snapshot,
visible_row_range,
target_display_point,
line_height,
scroll_pixel_position,
content_origin,
editor_width,
window,
cx,
);
}
EditDisplayMode::Inline => return None,
EditDisplayMode::DiffPopover => {}
}
let highlighted_edits = crate::inline_completion_edit_text(
&snapshot,
edits,
edit_preview.as_ref()?,
false,
cx,
);
let styled_text = highlighted_edits.to_styled_text(&style.text);
let line_count = highlighted_edits.text.lines().count();
const BORDER_WIDTH: Pixels = px(1.);
let mut element = h_flex()
.items_start()
.child(
h_flex()
.bg(cx.theme().colors().editor_background)
.border(BORDER_WIDTH)
.shadow_sm()
.border_color(cx.theme().colors().border)
.rounded_l_lg()
.when(line_count > 1, |el| el.rounded_br_lg())
.pr_1()
.child(styled_text),
)
.child(
h_flex()
.h(line_height + BORDER_WIDTH * px(2.))
.px_1p5()
.gap_1()
// Workaround: For some reason, there's a gap if we don't do this
.ml(-BORDER_WIDTH)
.shadow(smallvec![gpui::BoxShadow {
color: gpui::black().opacity(0.05),
offset: point(px(1.), px(1.)),
blur_radius: px(2.),
spread_radius: px(0.),
}])
.bg(Editor::edit_prediction_line_popover_bg_color(cx))
.border(BORDER_WIDTH)
.border_color(cx.theme().colors().border)
.rounded_r_lg()
.children(editor.render_edit_prediction_accept_keybind(window, cx)),
)
.into_any();
let longest_row =
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
let longest_line_width = if visible_row_range.contains(&longest_row) {
line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width
} else {
layout_line(
longest_row,
editor_snapshot,
style,
editor_width,
|_| false,
window,
cx,
)
.width
};
let viewport_bounds = Bounds::new(Default::default(), window.viewport_size())
.extend(Edges {
right: -Self::SCROLLBAR_WIDTH,
..Default::default()
});
let x_after_longest = text_bounds.origin.x
+ longest_line_width
+ Self::EDIT_PREDICTION_POPOVER_PADDING_X
- scroll_pixel_position.x;
let element_bounds = element.layout_as_root(AvailableSpace::min_size(), window, cx);
// Fully visible if it can be displayed within the window (allow overlapping other
// panes). However, this is only allowed if the popover starts within text_bounds.
let can_position_to_the_right = x_after_longest < text_bounds.right()
&& x_after_longest + element_bounds.width < viewport_bounds.right();
let mut origin = if can_position_to_the_right {
point(
x_after_longest,
text_bounds.origin.y + edit_start.row().as_f32() * line_height
- scroll_pixel_position.y,
)
} else {
let cursor_row = newest_selection_head.map(|head| head.row());
let above_edit = edit_start
.row()
.0
.checked_sub(line_count as u32)
.map(DisplayRow);
let below_edit = Some(edit_end.row() + 1);
let above_cursor = cursor_row
.and_then(|row| row.0.checked_sub(line_count as u32).map(DisplayRow));
let below_cursor = cursor_row.map(|cursor_row| cursor_row + 1);
// Place the edit popover adjacent to the edit if there is a location
// available that is onscreen and does not obscure the cursor. Otherwise,
// place it adjacent to the cursor.
let row_target = [above_edit, below_edit, above_cursor, below_cursor]
.into_iter()
.flatten()
.find(|&start_row| {
let end_row = start_row + line_count as u32;
visible_row_range.contains(&start_row)
&& visible_row_range.contains(&end_row)
&& cursor_row.map_or(true, |cursor_row| {
!((start_row..end_row).contains(&cursor_row))
})
})?;
content_origin
+ point(
-scroll_pixel_position.x,
row_target.as_f32() * line_height - scroll_pixel_position.y,
)
};
origin.x -= BORDER_WIDTH;
window.defer_draw(element, origin, 1);
// Do not return an element, since it will already be drawn due to defer_draw.
None
}
}
}
#[allow(clippy::too_many_arguments)]
fn layout_edit_prediction_end_of_line_popover(
&self,
label: &'static str,
editor_snapshot: &EditorSnapshot,
visible_row_range: Range<DisplayRow>,
target_display_point: DisplayPoint,
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
content_origin: gpui::Point<Pixels>,
editor_width: Pixels,
window: &mut Window,
cx: &mut App,
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
let target_line_end = DisplayPoint::new(
target_display_point.row(),
editor_snapshot.line_len(target_display_point.row()),
);
let mut element = self
.editor
.read(cx)
.render_edit_prediction_line_popover(label, None, window, cx)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let line_origin = self.editor.update(cx, |editor, _cx| {
editor.display_to_pixel_point(target_line_end, editor_snapshot, window)
})?;
let start_point = content_origin - point(scroll_pixel_position.x, Pixels::ZERO);
let mut origin = start_point
+ line_origin
+ point(Self::EDIT_PREDICTION_POPOVER_PADDING_X, Pixels::ZERO);
origin.x = origin.x.max(content_origin.x);
let max_x = content_origin.x + editor_width - size.width;
if origin.x > max_x {
let offset = line_height + Self::EDIT_PREDICTION_POPOVER_PADDING_Y;
let icon = if visible_row_range.contains(&(target_display_point.row() + 2)) {
origin.y += offset;
IconName::ArrowUp
} else {
origin.y -= offset;
IconName::ArrowDown
};
element = self
.editor
.read(cx)
.render_edit_prediction_line_popover(label, Some(icon), window, cx)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
origin.x = content_origin.x + editor_width - size.width - px(2.);
}
element.prepaint_at(origin, window, cx);
Some((element, origin))
}
fn layout_mouse_context_menu(
&self,
editor_snapshot: &EditorSnapshot,
@ -6298,7 +5853,7 @@ pub(crate) struct LineWithInvisibles {
fragments: SmallVec<[LineFragment; 1]>,
invisibles: Vec<Invisible>,
len: usize,
width: Pixels,
pub(crate) width: Pixels,
font_size: Pixels,
}
@ -7446,22 +7001,25 @@ impl Element for EditorElement {
});
let (inline_completion_popover, inline_completion_popover_origin) = self
.layout_edit_prediction_popover(
&text_hitbox.bounds,
content_origin,
&snapshot,
start_row..end_row,
scroll_position.y,
scroll_position.y + height_in_lines,
&line_layouts,
line_height,
scroll_pixel_position,
newest_selection_head,
editor_width,
&style,
window,
cx,
)
.editor
.update(cx, |editor, cx| {
editor.render_edit_prediction_popover(
&text_hitbox.bounds,
content_origin,
&snapshot,
start_row..end_row,
scroll_position.y,
scroll_position.y + height_in_lines,
&line_layouts,
line_height,
scroll_pixel_position,
newest_selection_head,
editor_width,
&style,
window,
cx,
)
})
.unzip();
let mut inline_diagnostics = self.layout_inline_diagnostics(
@ -8267,7 +7825,7 @@ struct BlockLayout {
style: BlockStyle,
}
fn layout_line(
pub fn layout_line(
row: DisplayRow,
snapshot: &EditorSnapshot,
style: &EditorStyle,