Make edit prediction bindings backwards compatible with existing user keymaps (#24802)

Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
This commit is contained in:
Agus Zubiaga 2025-02-13 11:22:08 -03:00 committed by GitHub
parent d57f5937d4
commit c3afeda80b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 185 additions and 106 deletions

View file

@ -49,7 +49,6 @@ gpui.workspace = true
http_client.workspace = true
indoc.workspace = true
inline_completion.workspace = true
inventory.workspace = true
itertools.workspace = true
language.workspace = true
linkify.workspace = true

View file

@ -194,8 +194,7 @@ pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction";
pub(crate) const EDIT_PREDICTION_REQUIRES_MODIFIER_KEY_CONTEXT: &str =
"edit_prediction_requires_modifier";
pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict";
pub fn render_parsed_markdown(
element_id: impl Into<ElementId>,
@ -1545,13 +1544,10 @@ impl Editor {
key_context.add("renaming");
}
let mut showing_completions = false;
match self.context_menu.borrow().as_ref() {
Some(CodeContextMenu::Completions(_)) => {
key_context.add("menu");
key_context.add("showing_completions");
showing_completions = true;
}
Some(CodeContextMenu::CodeActions(_)) => {
key_context.add("menu");
@ -1579,15 +1575,11 @@ impl Editor {
}
if has_active_edit_prediction {
key_context.add("copilot_suggestion");
key_context.add(EDIT_PREDICTION_KEY_CONTEXT);
if showing_completions
|| self.edit_prediction_requires_modifier()
// Require modifier key when the cursor is on leading whitespace, to allow `tab`
// bindings to insert tab characters.
|| (self.edit_prediction_requires_modifier_in_leading_space && self.edit_prediction_cursor_on_leading_whitespace)
{
key_context.add(EDIT_PREDICTION_REQUIRES_MODIFIER_KEY_CONTEXT);
if self.edit_prediction_in_conflict() {
key_context.add(EDIT_PREDICTION_CONFLICT_KEY_CONTEXT);
} else {
key_context.add(EDIT_PREDICTION_KEY_CONTEXT);
key_context.add("copilot_suggestion");
}
}
@ -1598,16 +1590,40 @@ impl Editor {
key_context
}
pub fn edit_prediction_in_conflict(&self) -> bool {
let showing_completions = self
.context_menu
.borrow()
.as_ref()
.map_or(false, |context| {
matches!(context, CodeContextMenu::Completions(_))
});
showing_completions
|| self.edit_prediction_requires_modifier()
// Require modifier key when the cursor is on leading whitespace, to allow `tab`
// bindings to insert tab characters.
|| (self.edit_prediction_requires_modifier_in_leading_space && self.edit_prediction_cursor_on_leading_whitespace)
}
pub fn accept_edit_prediction_keybind(
&self,
window: &Window,
cx: &App,
) -> AcceptEditPredictionBinding {
let key_context = self.key_context_internal(true, window, cx);
let in_conflict = self.edit_prediction_in_conflict();
AcceptEditPredictionBinding(
window
.bindings_for_action_in_context(&AcceptEditPrediction, key_context)
.into_iter()
.filter(|binding| {
!in_conflict
|| binding
.keystrokes()
.first()
.map_or(false, |keystroke| keystroke.modifiers.modified())
})
.rev()
.next(),
)

View file

@ -15,14 +15,13 @@ use crate::{
items::BufferSearchHighlights,
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair},
AcceptEditPrediction, BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayPoint,
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayPoint, DisplayRow,
DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
GoToPrevHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point,
RevertSelectedHunks, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap,
StickyHeaderExcerpt, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR,
EDIT_PREDICTION_REQUIRES_MODIFIER_KEY_CONTEXT, FILE_HEADER_HEIGHT,
StickyHeaderExcerpt, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT,
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
};
use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus};
@ -35,11 +34,11 @@ use gpui::{
point, px, quad, relative, size, svg, transparent_black, Action, AnyElement, App,
AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners,
CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _,
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement,
KeyBindingContextPredicate, Keystroke, Length, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
Subscription, TextRun, TextStyleRefinement, WeakEntity, Window,
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Keystroke, Length,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement,
WeakEntity, Window,
};
use itertools::Itertools;
use language::{
@ -55,7 +54,7 @@ use multi_buffer::{
RowInfo, ToOffset,
};
use project::project_settings::{GitGutterSetting, ProjectSettings};
use settings::{KeyBindingValidator, KeyBindingValidatorRegistration, Settings};
use settings::Settings;
use smallvec::{smallvec, SmallVec};
use std::{
any::TypeId,
@ -75,7 +74,7 @@ use ui::{
POPOVER_Y_PADDING,
};
use unicode_segmentation::UnicodeSegmentation;
use util::{markdown::MarkdownString, RangeExt, ResultExt};
use util::{RangeExt, ResultExt};
use workspace::{item::Item, notifications::NotifyTaskExt, Workspace};
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
@ -5834,50 +5833,6 @@ impl AcceptEditPredictionBinding {
}
}
struct AcceptEditPredictionsBindingValidator;
inventory::submit! { KeyBindingValidatorRegistration(|| Box::new(AcceptEditPredictionsBindingValidator)) }
impl KeyBindingValidator for AcceptEditPredictionsBindingValidator {
fn action_type_id(&self) -> TypeId {
TypeId::of::<AcceptEditPrediction>()
}
fn validate(&self, binding: &gpui::KeyBinding) -> Result<(), MarkdownString> {
use KeyBindingContextPredicate::*;
if binding.keystrokes().len() == 1 && binding.keystrokes()[0].modifiers.modified() {
return Ok(());
}
let required_predicate =
Not(Identifier(EDIT_PREDICTION_REQUIRES_MODIFIER_KEY_CONTEXT.into()).into());
match binding.predicate() {
Some(predicate) if required_predicate.is_superset(&predicate) => {
return Ok(());
}
_ => {}
}
let negated_requires_modifier_key_context = MarkdownString::inline_code(&format!(
"!{}",
EDIT_PREDICTION_REQUIRES_MODIFIER_KEY_CONTEXT
));
Err(MarkdownString(format!(
"{} can only be bound to a single keystroke with modifiers, so \
that pressing these modifiers can be used for prediction \
preview.\n\n\
This restriction does not apply when the context requires {}, \
since these bindings are not used for prediction preview. For \
example, in the default keymap `tab` requires {}, and `alt-tab` \
is used otherwise.\n\n\
See [the documentation]({}) for more details.",
MarkdownString::inline_code(AcceptEditPrediction.name()),
negated_requires_modifier_key_context.clone(),
negated_requires_modifier_key_context,
"https://zed.dev/docs/completions#edit-predictions",
)))
}
}
#[allow(clippy::too_many_arguments)]
fn prepaint_gutter_button(
button: IconButton,