diff --git a/assets/icons/info.svg b/assets/icons/info.svg index 7016cfe4d1..f3d2e6644f 100644 --- a/assets/icons/info.svg +++ b/assets/icons/info.svg @@ -1,12 +1,5 @@ - - - - - - - - - - - + + + + diff --git a/assets/icons/zed_predict_error.svg b/assets/icons/zed_predict_error.svg new file mode 100644 index 0000000000..b2dc339fe9 --- /dev/null +++ b/assets/icons/zed_predict_error.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 965e8a1123..8ce48e036c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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
{ + fn render_edit_prediction_accept_keybind( + &self, + window: &mut Window, + cx: &App, + ) -> Option { 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, window: &mut Window, cx: &App, - ) -> Option
{ + ) -> Option> { 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"); + })), + ) + }) + } +} diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 738d45ae26..9f655a686a 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -329,6 +329,7 @@ pub enum IconName { ZedPredictUp, ZedPredictDown, ZedPredictDisabled, + ZedPredictError, ZedXCopilot, } diff --git a/docs/src/completions.md b/docs/src/completions.md index a0830c566f..5328c68955 100644 --- a/docs/src/completions.md +++ b/docs/src/completions.md @@ -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`: