zeta: Improve UX for simultaneous LSP and prediction completions (#24024)

Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <michael@zed.dev>
Co-authored-by: Danilo <danilo@zed.dev>
Co-authored-by: Richard <richard@zed.dev>
This commit is contained in:
Agus Zubiaga 2025-02-03 21:47:11 -03:00 committed by GitHub
parent b6e680ea3d
commit 4c29e1ff07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1196 additions and 762 deletions

View file

@ -73,17 +73,18 @@ use zed_predict_onboarding::ZedPredictModal;
use code_context_menus::{
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
CompletionEntry, CompletionsMenu, ContextMenuOrigin,
CompletionsMenu, ContextMenuOrigin,
};
use git::blame::GitBlame;
use gpui::{
div, impl_actions, point, prelude::*, px, relative, size, Action, AnyElement, App,
AsyncWindowContext, AvailableSpace, Bounds, ClipboardEntry, ClipboardItem, Context,
DispatchPhase, ElementId, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent,
Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext,
MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size,
Styled, StyledText, Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection,
UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window,
div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation,
AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Bounds, ClipboardEntry,
ClipboardItem, Context, DispatchPhase, ElementId, Entity, EntityInputHandler, EventEmitter,
FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla,
InteractiveText, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, ParentElement,
Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, Task, TextStyle,
TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity,
WeakFocusHandle, Window,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@ -107,7 +108,7 @@ pub use proposed_changes_editor::{
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
};
use similar::{ChangeTag, TextDiff};
use std::iter::Peekable;
use std::iter::{self, Peekable};
use task::{ResolvedTask, TaskTemplate, TaskVariables};
use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
@ -163,7 +164,7 @@ use ui::{
h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize,
Tooltip,
};
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TakeUntilExt, TryFutureExt};
use workspace::item::{ItemHandle, PreviewTabsSettings};
use workspace::notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt};
use workspace::{
@ -465,32 +466,6 @@ pub fn make_suggestion_styles(cx: &mut App) -> InlineCompletionStyles {
type CompletionId = usize;
#[derive(Debug, Clone)]
enum InlineCompletionMenuHint {
Loading,
Loaded { text: InlineCompletionText },
PendingTermsAcceptance,
None,
}
impl InlineCompletionMenuHint {
pub fn label(&self) -> &'static str {
match self {
InlineCompletionMenuHint::Loading | InlineCompletionMenuHint::Loaded { .. } => {
"Edit Prediction"
}
InlineCompletionMenuHint::PendingTermsAcceptance => "Accept Terms of Service",
InlineCompletionMenuHint::None => "No Prediction",
}
}
}
#[derive(Clone, Debug)]
enum InlineCompletionText {
Move(SharedString),
Edit(HighlightedText),
}
pub(crate) enum EditDisplayMode {
TabAccept,
DiffPopover,
@ -504,7 +479,11 @@ enum InlineCompletion {
display_mode: EditDisplayMode,
snapshot: BufferSnapshot,
},
Move(Anchor),
Move {
target: Anchor,
range_around_target: Range<text::Anchor>,
snapshot: BufferSnapshot,
},
}
struct InlineCompletionState {
@ -513,6 +492,15 @@ struct InlineCompletionState {
invalidation_range: Range<Anchor>,
}
impl InlineCompletionState {
pub fn is_move(&self) -> bool {
match &self.completion {
InlineCompletion::Move { .. } => true,
_ => false,
}
}
}
enum InlineCompletionHighlight {}
pub enum MenuInlineCompletionsPolicy {
@ -687,6 +675,8 @@ pub struct Editor {
inline_completion_provider: Option<RegisteredInlineCompletionProvider>,
code_action_providers: Vec<Rc<dyn CodeActionProvider>>,
active_inline_completion: Option<InlineCompletionState>,
/// Used to prevent flickering as the user types while the menu is open
stale_inline_completion_in_menu: Option<InlineCompletionState>,
// enable_inline_completions is a switch that Vim can use to disable
// inline completions based on its mode.
enable_inline_completions: bool,
@ -1381,6 +1371,7 @@ impl Editor {
hovered_link_state: Default::default(),
inline_completion_provider: None,
active_inline_completion: None,
stale_inline_completion_in_menu: None,
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
@ -1496,7 +1487,7 @@ impl Editor {
match self.context_menu.borrow().as_ref() {
Some(CodeContextMenu::Completions(_)) => {
key_context.add("menu");
key_context.add("showing_completions")
key_context.add("showing_completions");
}
Some(CodeContextMenu::CodeActions(_)) => {
key_context.add("menu");
@ -2611,9 +2602,6 @@ impl Editor {
}
if self.hide_context_menu(window, cx).is_some() {
if self.show_inline_completions_in_menu(cx) && self.has_active_inline_completion() {
self.update_visible_inline_completion(window, cx);
}
return true;
}
@ -3601,10 +3589,11 @@ impl Editor {
} else {
self.inlay_hint_cache.clear();
self.splice_inlays(
self.visible_inlay_hints(cx)
&self
.visible_inlay_hints(cx)
.iter()
.map(|inlay| inlay.id)
.collect(),
.collect::<Vec<InlayId>>(),
Vec::new(),
cx,
);
@ -3622,7 +3611,7 @@ impl Editor {
to_remove,
to_insert,
})) => {
self.splice_inlays(to_remove, to_insert, cx);
self.splice_inlays(&to_remove, to_insert, cx);
return;
}
ControlFlow::Break(None) => return,
@ -3635,7 +3624,7 @@ impl Editor {
to_insert,
}) = self.inlay_hint_cache.remove_excerpts(excerpts_removed)
{
self.splice_inlays(to_remove, to_insert, cx);
self.splice_inlays(&to_remove, to_insert, cx);
}
return;
}
@ -3658,7 +3647,7 @@ impl Editor {
ignore_debounce,
cx,
) {
self.splice_inlays(to_remove, to_insert, cx);
self.splice_inlays(&to_remove, to_insert, cx);
}
}
@ -3738,7 +3727,7 @@ impl Editor {
pub fn splice_inlays(
&self,
to_remove: Vec<InlayId>,
to_remove: &[InlayId],
to_insert: Vec<Inlay>,
cx: &mut Context<Self>,
) {
@ -3905,17 +3894,15 @@ impl Editor {
let mut menu = menu.unwrap();
menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx);
*editor.context_menu.borrow_mut() =
Some(CodeContextMenu::Completions(menu));
if editor.show_inline_completions_in_menu(cx) {
if let Some(hint) = editor.inline_completion_menu_hint(window, cx) {
menu.show_inline_completion_hint(hint);
}
editor.update_visible_inline_completion(window, cx);
} else {
editor.discard_inline_completion(false, cx);
}
*editor.context_menu.borrow_mut() =
Some(CodeContextMenu::Completions(menu));
cx.notify();
} else if editor.completion_tasks.len() <= 1 {
// If there are no more completion tasks and the last menu was
@ -3982,34 +3969,6 @@ impl Editor {
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
use language::ToOffset as _;
{
let context_menu = self.context_menu.borrow();
if let CodeContextMenu::Completions(menu) = context_menu.as_ref()? {
let entries = menu.entries.borrow();
let entry = entries.get(item_ix.unwrap_or(menu.selected_item));
match entry {
Some(CompletionEntry::InlineCompletionHint(
InlineCompletionMenuHint::Loading,
)) => return Some(Task::ready(Ok(()))),
Some(CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::None)) => {
drop(entries);
drop(context_menu);
self.context_menu_next(&Default::default(), window, cx);
return Some(Task::ready(Ok(())));
}
Some(CompletionEntry::InlineCompletionHint(
InlineCompletionMenuHint::PendingTermsAcceptance,
)) => {
drop(entries);
drop(context_menu);
self.toggle_zed_predict_onboarding(window, cx);
return Some(Task::ready(Ok(())));
}
_ => {}
}
}
}
let completions_menu =
if let CodeContextMenu::Completions(menu) = self.hide_context_menu(window, cx)? {
menu
@ -4019,19 +3978,9 @@ impl Editor {
let entries = completions_menu.entries.borrow();
let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?;
let mat = match mat {
CompletionEntry::InlineCompletionHint(_) => {
self.accept_inline_completion(&AcceptInlineCompletion, window, cx);
cx.stop_propagation();
return Some(Task::ready(Ok(())));
}
CompletionEntry::Match(mat) => {
if self.show_inline_completions_in_menu(cx) {
self.discard_inline_completion(true, cx);
}
mat
}
};
if self.show_inline_completions_in_menu(cx) {
self.discard_inline_completion(true, cx);
}
let candidate_id = mat.candidate_id;
drop(entries);
@ -4863,10 +4812,10 @@ impl Editor {
self.report_inline_completion_event(true, cx);
match &active_inline_completion.completion {
InlineCompletion::Move(position) => {
let position = *position;
InlineCompletion::Move { target, .. } => {
let target = *target;
self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
selections.select_anchor_ranges([position..position]);
selections.select_anchor_ranges([target..target]);
});
}
InlineCompletion::Edit { edits, .. } => {
@ -4911,10 +4860,10 @@ impl Editor {
self.report_inline_completion_event(true, cx);
match &active_inline_completion.completion {
InlineCompletion::Move(position) => {
let position = *position;
InlineCompletion::Move { target, .. } => {
let target = *target;
self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
selections.select_anchor_ranges([position..position]);
selections.select_anchor_ranges([target..target]);
});
}
InlineCompletion::Edit { edits, .. } => {
@ -4973,7 +4922,7 @@ impl Editor {
provider.discard(cx);
}
self.take_active_inline_completion(cx).is_some()
self.take_active_inline_completion(cx)
}
fn report_inline_completion_event(&self, accepted: bool, cx: &App) {
@ -5010,19 +4959,58 @@ impl Editor {
self.active_inline_completion.is_some()
}
fn take_active_inline_completion(
&mut self,
cx: &mut Context<Self>,
) -> Option<InlineCompletion> {
let active_inline_completion = self.active_inline_completion.take()?;
self.splice_inlays(active_inline_completion.inlay_ids, Default::default(), cx);
fn take_active_inline_completion(&mut self, cx: &mut Context<Self>) -> bool {
let Some(active_inline_completion) = self.active_inline_completion.take() else {
return false;
};
self.splice_inlays(&active_inline_completion.inlay_ids, Default::default(), cx);
self.clear_highlights::<InlineCompletionHighlight>(cx);
Some(active_inline_completion.completion)
self.stale_inline_completion_in_menu = Some(active_inline_completion);
true
}
fn update_inline_completion_preview(
&mut self,
modifiers: &Modifiers,
window: &mut Window,
cx: &mut Context<Self>,
) {
// Moves jump directly with a preview step
if self
.active_inline_completion
.as_ref()
.map_or(true, |c| c.is_move())
{
cx.notify();
return;
}
if !self.show_inline_completions_in_menu(cx) {
return;
}
let mut menu_borrow = self.context_menu.borrow_mut();
let Some(CodeContextMenu::Completions(completions_menu)) = menu_borrow.as_mut() else {
return;
};
if completions_menu.is_empty()
|| completions_menu.previewing_inline_completion == modifiers.alt
{
return;
}
completions_menu.set_previewing_inline_completion(modifiers.alt);
drop(menu_borrow);
self.update_visible_inline_completion(window, cx);
}
fn update_visible_inline_completion(
&mut self,
window: &mut Window,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<()> {
let selection = self.selections.newest_anchor();
@ -5031,7 +5019,8 @@ impl Editor {
let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer));
let excerpt_id = cursor.excerpt_id;
let completions_menu_has_precedence = !self.show_inline_completions_in_menu(cx)
let show_in_menu = self.show_inline_completions_in_menu(cx);
let completions_menu_has_precedence = !show_in_menu
&& (self.context_menu.borrow().is_some()
|| (!self.completion_tasks.is_empty() && !self.has_active_inline_completion()));
if completions_menu_has_precedence
@ -5080,50 +5069,73 @@ impl Editor {
let cursor_row = cursor.to_point(&multibuffer).row;
let snapshot = multibuffer.buffer_for_excerpt(excerpt_id).cloned()?;
let mut inlay_ids = Vec::new();
let invalidation_row_range;
let completion = if cursor_row < edit_start_row {
invalidation_row_range = cursor_row..edit_end_row;
InlineCompletion::Move(first_edit_start)
let move_invalidation_row_range = if cursor_row < edit_start_row {
Some(cursor_row..edit_end_row)
} else if cursor_row > edit_end_row {
invalidation_row_range = edit_start_row..cursor_row;
InlineCompletion::Move(first_edit_start)
Some(edit_start_row..cursor_row)
} else {
if edits
.iter()
.all(|(range, _)| range.to_offset(&multibuffer).is_empty())
{
let mut inlays = Vec::new();
for (range, new_text) in &edits {
let inlay = Inlay::inline_completion(
post_inc(&mut self.next_inlay_id),
range.start,
new_text.as_str(),
);
inlay_ids.push(inlay.id);
inlays.push(inlay);
}
None
};
let completion = if let Some(move_invalidation_row_range) = move_invalidation_row_range {
invalidation_row_range = move_invalidation_row_range;
let target = first_edit_start;
let target_point = text::ToPoint::to_point(&target.text_anchor, &snapshot);
// TODO: Base this off of TreeSitter or word boundaries?
let target_excerpt_begin = snapshot.anchor_before(snapshot.clip_point(
Point::new(target_point.row, target_point.column.saturating_sub(10)),
Bias::Left,
));
let target_excerpt_end = snapshot.anchor_after(snapshot.clip_point(
Point::new(target_point.row, target_point.column + 10),
Bias::Right,
));
// TODO: Extend this to be before the jump target, and draw a cursor at the jump target
// (using Editor::current_user_player_color).
let range_around_target = target_excerpt_begin..target_excerpt_end;
InlineCompletion::Move {
target,
range_around_target,
snapshot,
}
} else {
if !show_in_menu || !self.has_active_completions_menu() {
if edits
.iter()
.all(|(range, _)| range.to_offset(&multibuffer).is_empty())
{
let mut inlays = Vec::new();
for (range, new_text) in &edits {
let inlay = Inlay::inline_completion(
post_inc(&mut self.next_inlay_id),
range.start,
new_text.as_str(),
);
inlay_ids.push(inlay.id);
inlays.push(inlay);
}
self.splice_inlays(vec![], inlays, cx);
} else {
let background_color = cx.theme().status().deleted_background;
self.highlight_text::<InlineCompletionHighlight>(
edits.iter().map(|(range, _)| range.clone()).collect(),
HighlightStyle {
background_color: Some(background_color),
..Default::default()
},
cx,
);
self.splice_inlays(&[], inlays, cx);
} else {
let background_color = cx.theme().status().deleted_background;
self.highlight_text::<InlineCompletionHighlight>(
edits.iter().map(|(range, _)| range.clone()).collect(),
HighlightStyle {
background_color: Some(background_color),
..Default::default()
},
cx,
);
}
}
invalidation_row_range = edit_start_row..edit_end_row;
let display_mode = if all_edits_insertions_or_deletions(&edits, &multibuffer) {
if provider.show_tab_accept_marker()
&& first_edit_start_point.row == last_edit_end_point.row
&& !edits.iter().any(|(_, edit)| edit.contains('\n'))
{
if provider.show_tab_accept_marker() {
EditDisplayMode::TabAccept
} else {
EditDisplayMode::Inline
@ -5132,8 +5144,6 @@ impl Editor {
EditDisplayMode::DiffPopover
};
let snapshot = multibuffer.buffer_for_excerpt(excerpt_id).cloned()?;
InlineCompletion::Edit {
edits,
edit_preview: inline_completion.edit_preview,
@ -5149,69 +5159,18 @@ impl Editor {
multibuffer.line_len(MultiBufferRow(invalidation_row_range.end)),
));
self.stale_inline_completion_in_menu = None;
self.active_inline_completion = Some(InlineCompletionState {
inlay_ids,
completion,
invalidation_range,
});
if self.show_inline_completions_in_menu(cx) && self.has_active_completions_menu() {
if let Some(hint) = self.inline_completion_menu_hint(window, cx) {
match self.context_menu.borrow_mut().as_mut() {
Some(CodeContextMenu::Completions(menu)) => {
menu.show_inline_completion_hint(hint);
}
_ => {}
}
}
}
cx.notify();
Some(())
}
fn inline_completion_menu_hint(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<InlineCompletionMenuHint> {
let provider = self.inline_completion_provider()?;
if self.has_active_inline_completion() {
let editor_snapshot = self.snapshot(window, cx);
let text = match &self.active_inline_completion.as_ref()?.completion {
InlineCompletion::Edit {
edits,
edit_preview,
display_mode: _,
snapshot,
} => edit_preview
.as_ref()
.and_then(|edit_preview| {
inline_completion_edit_text(&snapshot, &edits, edit_preview, true, cx)
})
.map(InlineCompletionText::Edit),
InlineCompletion::Move(target) => {
let target_point =
target.to_point(&editor_snapshot.display_snapshot.buffer_snapshot);
let target_line = target_point.row + 1;
Some(InlineCompletionText::Move(
format!("Jump to edit in line {}", target_line).into(),
))
}
};
Some(InlineCompletionMenuHint::Loaded { text: text? })
} else if provider.is_refreshing(cx) {
Some(InlineCompletionMenuHint::Loading)
} else if provider.needs_terms_acceptance(cx) {
Some(InlineCompletionMenuHint::PendingTermsAcceptance)
} else {
Some(InlineCompletionMenuHint::None)
}
}
pub fn inline_completion_provider(&self) -> Option<Arc<dyn InlineCompletionProviderHandle>> {
Some(self.inline_completion_provider.as_ref()?.provider.clone())
}
@ -5439,7 +5398,6 @@ impl Editor {
}))
}
#[cfg(any(test, feature = "test-support"))]
pub fn context_menu_visible(&self) -> bool {
self.context_menu
.borrow()
@ -5447,26 +5405,300 @@ impl Editor {
.map_or(false, |menu| menu.visible())
}
#[cfg(feature = "test-support")]
pub fn context_menu_contains_inline_completion(&self) -> bool {
fn context_menu_origin(&self) -> Option<ContextMenuOrigin> {
self.context_menu
.borrow()
.as_ref()
.map_or(false, |menu| match menu {
CodeContextMenu::Completions(menu) => {
menu.entries.borrow().first().map_or(false, |entry| {
matches!(entry, CompletionEntry::InlineCompletionHint(_))
})
}
CodeContextMenu::CodeActions(_) => false,
})
.map(|menu| menu.origin())
}
fn context_menu_origin(&self, cursor_position: DisplayPoint) -> Option<ContextMenuOrigin> {
self.context_menu
.borrow()
.as_ref()
.map(|menu| menu.origin(cursor_position))
fn edit_prediction_cursor_popover_height(&self) -> Pixels {
px(32.)
}
fn current_user_player_color(&self, cx: &mut App) -> PlayerColor {
if self.read_only(cx) {
cx.theme().players().read_only()
} else {
self.style.as_ref().unwrap().local_player
}
}
fn render_edit_prediction_cursor_popover(
&self,
max_width: Pixels,
cursor_point: Point,
style: &EditorStyle,
accept_keystroke: &gpui::Keystroke,
window: &Window,
cx: &mut Context<Editor>,
) -> Option<AnyElement> {
let provider = self.inline_completion_provider.as_ref()?;
if provider.provider.needs_terms_acceptance(cx) {
return Some(
h_flex()
.h(self.edit_prediction_cursor_popover_height())
.flex_1()
.px_2()
.gap_3()
.elevation_2(cx)
.hover(|style| style.bg(cx.theme().colors().element_hover))
.id("accept-terms")
.cursor_pointer()
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
.on_click(cx.listener(|this, _event, window, cx| {
cx.stop_propagation();
this.toggle_zed_predict_onboarding(window, cx)
}))
.child(
h_flex()
.w_full()
.gap_2()
.child(Icon::new(IconName::ZedPredict))
.child(Label::new("Accept Terms of Service"))
.child(div().w_full())
.child(Icon::new(IconName::ArrowUpRight))
.into_any_element(),
)
.into_any(),
);
}
let is_refreshing = provider.provider.is_refreshing(cx);
fn pending_completion_container() -> Div {
h_flex().gap_3().child(Icon::new(IconName::ZedPredict))
}
let completion = match &self.active_inline_completion {
Some(completion) => self.render_edit_prediction_cursor_popover_preview(
completion,
cursor_point,
style,
cx,
)?,
None if is_refreshing => match &self.stale_inline_completion_in_menu {
Some(stale_completion) => self.render_edit_prediction_cursor_popover_preview(
stale_completion,
cursor_point,
style,
cx,
)?,
None => {
pending_completion_container().child(Label::new("...").size(LabelSize::Small))
}
},
None => pending_completion_container().child(Label::new("No Prediction")),
};
let buffer_font = theme::ThemeSettings::get_global(cx).buffer_font.clone();
let completion = completion.font(buffer_font.clone());
let completion = if is_refreshing {
completion
.with_animation(
"loading-completion",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.opacity(delta),
)
.into_any_element()
} else {
completion.into_any_element()
};
let has_completion = self.active_inline_completion.is_some();
Some(
h_flex()
.h(self.edit_prediction_cursor_popover_height())
.max_w(max_width)
.flex_1()
.px_2()
.gap_3()
.elevation_2(cx)
.child(completion)
.child(div().w_full())
.child(
h_flex()
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.pl_2()
.child(
h_flex()
.font(buffer_font.clone())
.p_1()
.rounded_sm()
.children(ui::render_modifiers(
&accept_keystroke.modifiers,
PlatformStyle::platform(),
if window.modifiers() == accept_keystroke.modifiers {
Some(Color::Accent)
} else {
None
},
)),
)
.opacity(if has_completion { 1.0 } else { 0.1 })
.child(
if self
.active_inline_completion
.as_ref()
.map_or(false, |c| c.is_move())
{
div()
.child(ui::Key::new(&accept_keystroke.key, None))
.font(buffer_font.clone())
.into_any()
} else {
Label::new("Preview").color(Color::Muted).into_any_element()
},
),
)
.into_any(),
)
}
fn render_edit_prediction_cursor_popover_preview(
&self,
completion: &InlineCompletionState,
cursor_point: Point,
style: &EditorStyle,
cx: &mut Context<Editor>,
) -> Option<Div> {
use text::ToPoint as _;
fn render_relative_row_jump(
prefix: impl Into<String>,
current_row: u32,
target_row: u32,
) -> Div {
let (row_diff, arrow) = if target_row < current_row {
(current_row - target_row, IconName::ArrowUp)
} else {
(target_row - current_row, IconName::ArrowDown)
};
h_flex()
.child(
Label::new(format!("{}{}", prefix.into(), row_diff))
.color(Color::Muted)
.size(LabelSize::Small),
)
.child(Icon::new(arrow).color(Color::Muted).size(IconSize::Small))
}
match &completion.completion {
InlineCompletion::Edit {
edits,
edit_preview,
snapshot,
display_mode: _,
} => {
let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row;
let highlighted_edits = crate::inline_completion_edit_text(
&snapshot,
&edits,
edit_preview.as_ref()?,
true,
cx,
);
let len_total = highlighted_edits.text.len();
let first_line = &highlighted_edits.text
[..highlighted_edits.text.find('\n').unwrap_or(len_total)];
let first_line_len = first_line.len();
let first_highlight_start = highlighted_edits
.highlights
.first()
.map_or(0, |(range, _)| range.start);
let drop_prefix_len = first_line
.char_indices()
.find(|(_, c)| !c.is_whitespace())
.map_or(first_highlight_start, |(ix, _)| {
ix.min(first_highlight_start)
});
let preview_text = &first_line[drop_prefix_len..];
let preview_len = preview_text.len();
let highlights = highlighted_edits
.highlights
.into_iter()
.take_until(|(range, _)| range.start > first_line_len)
.map(|(range, style)| {
(
range.start - drop_prefix_len
..(range.end - drop_prefix_len).min(preview_len),
style,
)
});
let styled_text = gpui::StyledText::new(SharedString::new(preview_text))
.with_highlights(&style.text, highlights);
let preview = h_flex()
.gap_1()
.child(styled_text)
.when(len_total > first_line_len, |parent| parent.child(""));
let left = if first_edit_row != cursor_point.row {
render_relative_row_jump("", cursor_point.row, first_edit_row)
.into_any_element()
} else {
Icon::new(IconName::ZedPredict).into_any_element()
};
Some(h_flex().gap_3().child(left).child(preview))
}
InlineCompletion::Move {
target,
range_around_target,
snapshot,
} => {
let mut highlighted_text = snapshot.highlighted_text_for_range(
range_around_target.clone(),
None,
&style.syntax,
);
let cursor_color = self.current_user_player_color(cx).cursor;
let target_offset =
text::ToOffset::to_offset(&target.text_anchor, &snapshot).saturating_sub(
text::ToOffset::to_offset(&range_around_target.start, &snapshot),
);
highlighted_text.highlights = gpui::combine_highlights(
highlighted_text.highlights,
iter::once((
target_offset..target_offset + 1,
HighlightStyle {
background_color: Some(cursor_color),
..Default::default()
},
)),
)
.collect::<Vec<_>>();
Some(
h_flex()
.gap_3()
.child(render_relative_row_jump(
"Jump ",
cursor_point.row,
target.text_anchor.to_point(&snapshot).row,
))
.when(!highlighted_text.text.is_empty(), |parent| {
parent.child(highlighted_text.to_styled_text(&style.text))
}),
)
}
}
}
fn render_context_menu(
@ -5477,13 +5709,12 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Editor>,
) -> Option<AnyElement> {
self.context_menu.borrow().as_ref().and_then(|menu| {
if menu.visible() {
Some(menu.render(style, max_height_in_lines, y_flipped, window, cx))
} else {
None
}
})
let menu = self.context_menu.borrow();
let menu = menu.as_ref()?;
if !menu.visible() {
return None;
};
Some(menu.render(style, max_height_in_lines, y_flipped, window, cx))
}
fn render_context_menu_aside(
@ -5514,7 +5745,8 @@ impl Editor {
cx.notify();
self.completion_tasks.clear();
let context_menu = self.context_menu.borrow_mut().take();
if context_menu.is_some() && !self.show_inline_completions_in_menu(cx) {
self.stale_inline_completion_in_menu.take();
if context_menu.is_some() {
self.update_visible_inline_completion(window, cx);
}
context_menu
@ -15859,7 +16091,7 @@ fn inline_completion_edit_text(
edit_preview: &EditPreview,
include_deletions: bool,
cx: &App,
) -> Option<HighlightedText> {
) -> HighlightedText {
let edits = edits
.iter()
.map(|(anchor, text)| {
@ -15870,7 +16102,7 @@ fn inline_completion_edit_text(
})
.collect::<Vec<_>>();
Some(edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx))
edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx)
}
pub fn highlight_diagnostic_message(