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:  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.  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:
parent
76a81607de
commit
f31749c81b
5 changed files with 167 additions and 26 deletions
|
@ -1,12 +1,5 @@
|
||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g clip-path="url(#clip0_2131_1193)">
|
<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"/>
|
||||||
<circle cx="7" cy="7" r="6" 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="M6 10H7M8 10H7M7 10V7.1C7 7.04477 6.95523 7 6.9 7H6" 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"/>
|
||||||
<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>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 479 B After Width: | Height: | Size: 524 B |
4
assets/icons/zed_predict_error.svg
Normal file
4
assets/icons/zed_predict_error.svg
Normal 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 |
|
@ -85,8 +85,8 @@ use gpui::{
|
||||||
ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler,
|
ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler,
|
||||||
EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global,
|
EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global,
|
||||||
HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,
|
HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,
|
||||||
ParentElement, Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, Task,
|
ParentElement, Pixels, Render, SharedString, Size, Stateful, Styled, StyledText, Subscription,
|
||||||
TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
|
Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
|
||||||
WeakEntity, WeakFocusHandle, Window,
|
WeakEntity, WeakFocusHandle, Window,
|
||||||
};
|
};
|
||||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||||
|
@ -6294,6 +6294,9 @@ impl Editor {
|
||||||
|
|
||||||
const BORDER_WIDTH: Pixels = px(1.);
|
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()
|
let mut element = h_flex()
|
||||||
.items_start()
|
.items_start()
|
||||||
.child(
|
.child(
|
||||||
|
@ -6324,7 +6327,19 @@ impl Editor {
|
||||||
.border(BORDER_WIDTH)
|
.border(BORDER_WIDTH)
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
.rounded_r_lg()
|
.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();
|
.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_binding = self.accept_edit_prediction_keybind(window, cx);
|
||||||
let accept_keystroke = accept_binding.keystroke()?;
|
let accept_keystroke = accept_binding.keystroke()?;
|
||||||
|
|
||||||
|
@ -6458,6 +6477,7 @@ impl Editor {
|
||||||
.size(Some(IconSize::XSmall.rems().into())),
|
.size(Some(IconSize::XSmall.rems().into())),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.into_any()
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6467,10 +6487,14 @@ impl Editor {
|
||||||
icon: Option<IconName>,
|
icon: Option<IconName>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Option<Div> {
|
) -> Option<Stateful<Div>> {
|
||||||
let padding_right = if icon.is_some() { px(4.) } else { px(8.) };
|
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()
|
let result = h_flex()
|
||||||
|
.id("ep-line-popover")
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.pl_1()
|
.pl_1()
|
||||||
.pr(padding_right)
|
.pr(padding_right)
|
||||||
|
@ -6480,8 +6504,35 @@ impl Editor {
|
||||||
.bg(Self::edit_prediction_line_popover_bg_color(cx))
|
.bg(Self::edit_prediction_line_popover_bg_color(cx))
|
||||||
.border_color(Self::edit_prediction_callout_popover_border_color(cx))
|
.border_color(Self::edit_prediction_callout_popover_border_color(cx))
|
||||||
.shadow_sm()
|
.shadow_sm()
|
||||||
.children(self.render_edit_prediction_accept_keybind(window, cx))
|
.when(!has_keybind, |el| {
|
||||||
.child(Label::new(label).size(LabelSize::Small))
|
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| {
|
.when_some(icon, |element, icon| {
|
||||||
element.child(
|
element.child(
|
||||||
div()
|
div()
|
||||||
|
@ -6578,6 +6629,9 @@ impl Editor {
|
||||||
.elevation_2(cx)
|
.elevation_2(cx)
|
||||||
.border(BORDER_WIDTH)
|
.border(BORDER_WIDTH)
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
|
.when(accept_keystroke.is_none(), |el| {
|
||||||
|
el.border_color(cx.theme().status().error)
|
||||||
|
})
|
||||||
.rounded(RADIUS)
|
.rounded(RADIUS)
|
||||||
.rounded_tl(px(0.))
|
.rounded_tl(px(0.))
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
|
@ -6606,16 +6660,37 @@ impl Editor {
|
||||||
el.child(
|
el.child(
|
||||||
Label::new("Hold")
|
Label::new("Hold")
|
||||||
.size(LabelSize::Small)
|
.size(LabelSize::Small)
|
||||||
|
.when(accept_keystroke.is_none(), |el| {
|
||||||
|
el.strikethrough()
|
||||||
|
})
|
||||||
.line_height_style(LineHeightStyle::UiLabel),
|
.line_height_style(LineHeightStyle::UiLabel),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.child(h_flex().children(ui::render_modifiers(
|
.id("edit_prediction_cursor_popover_keybind")
|
||||||
&accept_keystroke?.modifiers,
|
.when(accept_keystroke.is_none(), |el| {
|
||||||
PlatformStyle::platform(),
|
let status_colors = cx.theme().status();
|
||||||
Some(Color::Default),
|
|
||||||
Some(IconSize::XSmall.rems().into()),
|
el.bg(status_colors.error_background)
|
||||||
false,
|
.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(),
|
.into_any(),
|
||||||
);
|
);
|
||||||
|
@ -18328,3 +18403,37 @@ fn all_edits_insertions_or_deletions(
|
||||||
}
|
}
|
||||||
all_insertions || all_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");
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -329,6 +329,7 @@ pub enum IconName {
|
||||||
ZedPredictUp,
|
ZedPredictUp,
|
||||||
ZedPredictDown,
|
ZedPredictDown,
|
||||||
ZedPredictDisabled,
|
ZedPredictDisabled,
|
||||||
|
ZedPredictError,
|
||||||
ZedXCopilot,
|
ZedXCopilot,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
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:
|
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
|
## Disabling Automatic Edit Prediction
|
||||||
|
|
||||||
To disable predictions that appear automatically as you type, set this within `settings.json`:
|
To disable predictions that appear automatically as you type, set this within `settings.json`:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue