edit predictions: Improve UX when there's no keybinding for accepting predictions (#25815)

If the user already binds `tab`/`alt-tab`/`alt-l` to a different action
in a conflicting context and hasn't assigned a different keybinding for
`editor::AcceptEditPrediction`, we would show broken popovers with no
bindings:

![CleanShot 2025-02-28 at 12 46
13@2x](https://github.com/user-attachments/assets/a2c6a8ad-5e11-46ef-8031-62e1e6900244)

Instead, they will now see an error-variant of every popover which
includes a tooltip with a short description and buttons to open the
keymap, and open a new docs section explaining the issue in detail and
how to fix it.

![CleanShot 2025-02-28 at 12 48
11@2x](https://github.com/user-attachments/assets/36329b1f-6374-4735-9fbc-8fccab70e881)

Note: I included the docs change in this PR because it's ok to deploy
before the release, as it also applies to existing versions.

Release Notes:

- edit predictions: Improve UX when there's no keybinding for accepting
predictions

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Danilo <danilo@zed.dev>
This commit is contained in:
Agus Zubiaga 2025-03-04 11:28:36 -03:00 committed by GitHub
parent 76a81607de
commit f31749c81b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 167 additions and 26 deletions

View file

@ -1,12 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2131_1193)">
<circle cx="7" cy="7" r="6" stroke="black" stroke-width="1.5"/>
<path d="M6 10H7M8 10H7M7 10V7.1C7 7.04477 6.95523 7 6.9 7H6" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="7" cy="4.5" r="1" fill="black"/>
</g>
<defs>
<clipPath id="clip0_2131_1193">
<rect width="14" height="14" fill="white"/>
</clipPath>
</defs>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.5"/>
<path d="M7 11H8M8 11H9M8 11V8.1C8 8.04477 7.95523 8 7.9 8H7" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M8 6.5C8.55228 6.5 9 6.05228 9 5.5C9 4.94772 8.55228 4.5 8 4.5C7.44772 4.5 7 4.94772 7 5.5C7 6.05228 7.44772 6.5 8 6.5Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 479 B

After

Width:  |  Height:  |  Size: 524 B

Before After
Before After

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.6" d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
<path d="M14 8L10 12M14 12L10 8" stroke="black" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 296 B

View file

@ -85,8 +85,8 @@ use gpui::{
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,
TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
ParentElement, Pixels, Render, SharedString, Size, Stateful, Styled, StyledText, Subscription,
Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
WeakEntity, WeakFocusHandle, Window,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
@ -6294,6 +6294,9 @@ impl Editor {
const BORDER_WIDTH: Pixels = px(1.);
let keybind = self.render_edit_prediction_accept_keybind(window, cx);
let has_keybind = keybind.is_some();
let mut element = h_flex()
.items_start()
.child(
@ -6324,7 +6327,19 @@ impl Editor {
.border(BORDER_WIDTH)
.border_color(cx.theme().colors().border)
.rounded_r_lg()
.children(self.render_edit_prediction_accept_keybind(window, cx)),
.id("edit_prediction_diff_popover_keybind")
.when(!has_keybind, |el| {
let status_colors = cx.theme().status();
el.bg(status_colors.error_background)
.border_color(status_colors.error.opacity(0.6))
.child(Icon::new(IconName::Info).color(Color::Error))
.cursor_default()
.hoverable_tooltip(move |_window, cx| {
cx.new(|_| MissingEditPredictionKeybindingTooltip).into()
})
})
.children(keybind),
)
.into_any();
@ -6422,7 +6437,11 @@ impl Editor {
}
}
fn render_edit_prediction_accept_keybind(&self, window: &mut Window, cx: &App) -> Option<Div> {
fn render_edit_prediction_accept_keybind(
&self,
window: &mut Window,
cx: &App,
) -> Option<AnyElement> {
let accept_binding = self.accept_edit_prediction_keybind(window, cx);
let accept_keystroke = accept_binding.keystroke()?;
@ -6458,6 +6477,7 @@ impl Editor {
.size(Some(IconSize::XSmall.rems().into())),
)
})
.into_any()
.into()
}
@ -6467,10 +6487,14 @@ impl Editor {
icon: Option<IconName>,
window: &mut Window,
cx: &App,
) -> Option<Div> {
) -> Option<Stateful<Div>> {
let padding_right = if icon.is_some() { px(4.) } else { px(8.) };
let keybind = self.render_edit_prediction_accept_keybind(window, cx);
let has_keybind = keybind.is_some();
let result = h_flex()
.id("ep-line-popover")
.py_0p5()
.pl_1()
.pr(padding_right)
@ -6480,8 +6504,35 @@ impl Editor {
.bg(Self::edit_prediction_line_popover_bg_color(cx))
.border_color(Self::edit_prediction_callout_popover_border_color(cx))
.shadow_sm()
.children(self.render_edit_prediction_accept_keybind(window, cx))
.child(Label::new(label).size(LabelSize::Small))
.when(!has_keybind, |el| {
let status_colors = cx.theme().status();
el.bg(status_colors.error_background)
.border_color(status_colors.error.opacity(0.6))
.pl_2()
.child(Icon::new(IconName::ZedPredictError).color(Color::Error))
.cursor_default()
.hoverable_tooltip(move |_window, cx| {
cx.new(|_| MissingEditPredictionKeybindingTooltip).into()
})
})
.children(keybind)
.child(
Label::new(label)
.size(LabelSize::Small)
.when(!has_keybind, |el| {
el.color(cx.theme().status().error.into()).strikethrough()
}),
)
.when(!has_keybind, |el| {
el.child(
h_flex().ml_1().child(
Icon::new(IconName::Info)
.size(IconSize::Small)
.color(cx.theme().status().error.into()),
),
)
})
.when_some(icon, |element, icon| {
element.child(
div()
@ -6578,6 +6629,9 @@ impl Editor {
.elevation_2(cx)
.border(BORDER_WIDTH)
.border_color(cx.theme().colors().border)
.when(accept_keystroke.is_none(), |el| {
el.border_color(cx.theme().status().error)
})
.rounded(RADIUS)
.rounded_tl(px(0.))
.overflow_hidden()
@ -6606,16 +6660,37 @@ impl Editor {
el.child(
Label::new("Hold")
.size(LabelSize::Small)
.when(accept_keystroke.is_none(), |el| {
el.strikethrough()
})
.line_height_style(LineHeightStyle::UiLabel),
)
})
.child(h_flex().children(ui::render_modifiers(
&accept_keystroke?.modifiers,
PlatformStyle::platform(),
Some(Color::Default),
Some(IconSize::XSmall.rems().into()),
false,
))),
.id("edit_prediction_cursor_popover_keybind")
.when(accept_keystroke.is_none(), |el| {
let status_colors = cx.theme().status();
el.bg(status_colors.error_background)
.border_color(status_colors.error.opacity(0.6))
.child(Icon::new(IconName::Info).color(Color::Error))
.cursor_default()
.hoverable_tooltip(move |_window, cx| {
cx.new(|_| MissingEditPredictionKeybindingTooltip)
.into()
})
})
.when_some(
accept_keystroke.as_ref(),
|el, accept_keystroke| {
el.child(h_flex().children(ui::render_modifiers(
&accept_keystroke.modifiers,
PlatformStyle::platform(),
Some(Color::Default),
Some(IconSize::XSmall.rems().into()),
false,
)))
},
),
)
.into_any(),
);
@ -18328,3 +18403,37 @@ fn all_edits_insertions_or_deletions(
}
all_insertions || all_deletions
}
struct MissingEditPredictionKeybindingTooltip;
impl Render for MissingEditPredictionKeybindingTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
ui::tooltip_container(window, cx, |container, _, cx| {
container
.flex_shrink_0()
.max_w_80()
.min_h(rems_from_px(124.))
.justify_between()
.child(
v_flex()
.flex_1()
.text_ui_sm(cx)
.child(Label::new("Conflict with Accept Keybinding"))
.child("Your keymap currently overrides the default accept keybinding. To continue, assign one keybinding for the `editor::AcceptEditPrediction` action.")
)
.child(
h_flex()
.pb_1()
.gap_1()
.items_end()
.w_full()
.child(Button::new("open-keymap", "Assign Keybinding").size(ButtonSize::Compact).on_click(|_ev, window, cx| {
window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx)
}))
.child(Button::new("see-docs", "See Docs").size(ButtonSize::Compact).on_click(|_ev, _window, cx| {
cx.open_url("https://zed.dev/docs/completions#edit-predictions-missing-keybinding");
})),
)
})
}
}

View file

@ -329,6 +329,7 @@ pub enum IconName {
ZedPredictUp,
ZedPredictDown,
ZedPredictDisabled,
ZedPredictError,
ZedXCopilot,
}

View file

@ -32,7 +32,7 @@ On Linux, `alt-tab` is often used by the window manager for switching windows, s
See the [Configuring GitHub Copilot](#github-copilot) and [Configuring Supermaven](#supermaven) sections below for configuration of other providers. Only text insertions at the current cursor are supported for these providers, whereas the Zeta model provides multiple predictions including deletions.
## Configuring Edit Prediction Keybindings
## Configuring Edit Prediction Keybindings {#edit-predictions-keybinding}
By default, `tab` is used to accept edit predictions. You can use another keybinding by inserting this in your keymap:
@ -137,6 +137,40 @@ While `tab` and `alt-tab` are supported on Linux, `alt-l` is displayed instead.
},
```
### Missing keybind {#edit-predictions-missing-keybinding}
Zed requires at least one keybinding for the {#action editor::AcceptEditPrediction} action in both the `Editor && edit_prediction` and `Editor && edit_prediction_conflict` contexts ([learn more above](#edit-predictions-keybinding)).
If you have previously bound the default keybindings to different actions in the global context, you will not be able to preview or accept edit predictions. For example:
```json
[
// Your keymap
{
"bindings": {
// Binds `alt-tab` to a different action globally
"alt-tab": "menu::SelectNext"
}
}
]
```
To fix this, you can specify your own keybinding for accepting edit predictions:
```json
[
// ...
{
"context": "Editor && edit_prediction_conflict",
"bindings": {
"alt-l": "editor::AcceptEditPrediction"
}
}
]
```
If you would like to use the default keybinding, you can free it up by either moving yours to a more specific context or changing it to something else.
## Disabling Automatic Edit Prediction
To disable predictions that appear automatically as you type, set this within `settings.json`: