Detailed changes
@@ -775,15 +775,6 @@
"alt-j": "editor::AcceptNextLineEditPrediction",
},
},
- {
- "context": "Editor && edit_prediction_conflict",
- "bindings": {
- "alt-tab": "editor::AcceptEditPrediction",
- "alt-l": "editor::AcceptEditPrediction",
- "alt-k": "editor::AcceptNextWordEditPrediction",
- "alt-j": "editor::AcceptNextLineEditPrediction",
- },
- },
{
"context": "Editor && showing_code_actions",
"bindings": {
@@ -839,15 +839,6 @@
"ctrl-cmd-down": "editor::AcceptNextLineEditPrediction",
},
},
- {
- "context": "Editor && edit_prediction_conflict",
- "use_key_equivalents": true,
- "bindings": {
- "alt-tab": "editor::AcceptEditPrediction",
- "ctrl-cmd-right": "editor::AcceptNextWordEditPrediction",
- "ctrl-cmd-down": "editor::AcceptNextLineEditPrediction",
- },
- },
{
"context": "Editor && showing_code_actions",
"use_key_equivalents": true,
@@ -771,16 +771,6 @@
"alt-j": "editor::AcceptNextLineEditPrediction",
},
},
- {
- "context": "Editor && edit_prediction_conflict",
- "use_key_equivalents": true,
- "bindings": {
- "alt-tab": "editor::AcceptEditPrediction",
- "alt-l": "editor::AcceptEditPrediction",
- "alt-k": "editor::AcceptNextWordEditPrediction",
- "alt-j": "editor::AcceptNextLineEditPrediction",
- },
- },
{
"context": "Editor && showing_code_actions",
"use_key_equivalents": true,
@@ -1073,15 +1073,7 @@
"enter": "agent::Chat",
},
},
- {
- "context": "os != macos && Editor && edit_prediction_conflict",
- "bindings": {
- // alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This
- // is because alt-tab may not be available, as it is often used for window switching on Linux
- // and Windows.
- "alt-l": "editor::AcceptEditPrediction",
- },
- },
+
{
"context": "SettingsWindow > NavigationMenu && !search",
"bindings": {
@@ -3,6 +3,8 @@ use edit_prediction_types::{
};
use gpui::{Entity, KeyBinding, Modifiers, prelude::*};
use indoc::indoc;
+use language::Buffer;
+use language::EditPredictionsMode;
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
use std::{
ops::Range,
@@ -15,7 +17,9 @@ use text::{Point, ToOffset};
use ui::prelude::*;
use crate::{
- AcceptEditPrediction, EditPrediction, MenuEditPredictionsPolicy, editor_tests::init_test,
+ AcceptEditPrediction, EditPrediction, EditPredictionKeybindAction,
+ EditPredictionKeybindSurface, MenuEditPredictionsPolicy,
+ editor_tests::{init_test, update_test_language_settings},
test::editor_test_context::EditorTestContext,
};
use rpc::proto::PeerId;
@@ -478,6 +482,424 @@ async fn test_edit_prediction_preview_cleanup_on_toggle_off(cx: &mut gpui::TestA
});
}
+fn load_default_keymap(cx: &mut gpui::TestAppContext) {
+ cx.update(|cx| {
+ cx.bind_keys(
+ settings::KeymapFile::load_asset_allow_partial_failure(
+ settings::DEFAULT_KEYMAP_PATH,
+ cx,
+ )
+ .expect("failed to load default keymap"),
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_tab_is_preferred_accept_binding_over_alt_tab(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ load_default_keymap(cx);
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let provider = cx.new(|_| FakeEditPredictionDelegate::default());
+ assign_editor_completion_provider(provider.clone(), &mut cx);
+ cx.set_state("let x = Λ;");
+
+ propose_edits(&provider, vec![(8..8, "42")], &mut cx);
+ cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+
+ cx.update_editor(|editor, window, cx| {
+ assert!(editor.has_active_edit_prediction());
+ let keybind_display = editor.edit_prediction_keybind_display(
+ EditPredictionKeybindSurface::Inline,
+ window,
+ cx,
+ );
+ let keystroke = keybind_display
+ .accept_keystroke
+ .as_ref()
+ .expect("should have an accept binding");
+ assert!(
+ !keystroke.modifiers().modified(),
+ "preferred accept binding should be unmodified (tab), got modifiers: {:?}",
+ keystroke.modifiers()
+ );
+ assert_eq!(
+ keystroke.key(),
+ "tab",
+ "preferred accept binding should be tab"
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_subtle_in_code_indicator_prefers_preview_binding(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ load_default_keymap(cx);
+ update_test_language_settings(cx, &|settings| {
+ settings.edit_predictions.get_or_insert_default().mode = Some(EditPredictionsMode::Subtle);
+ });
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let provider = cx.new(|_| FakeEditPredictionDelegate::default());
+ assign_editor_completion_provider(provider.clone(), &mut cx);
+ cx.set_state("let x = Λ;");
+
+ propose_edits(&provider, vec![(8..8, "42")], &mut cx);
+ cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+
+ cx.update_editor(|editor, window, cx| {
+ assert!(editor.has_active_edit_prediction());
+ assert!(
+ editor.edit_prediction_requires_modifier(),
+ "subtle mode should require a modifier"
+ );
+
+ let inline_keybind_display = editor.edit_prediction_keybind_display(
+ EditPredictionKeybindSurface::Inline,
+ window,
+ cx,
+ );
+ let compact_keybind_display = editor.edit_prediction_keybind_display(
+ EditPredictionKeybindSurface::CursorPopoverCompact,
+ window,
+ cx,
+ );
+
+ let accept_keystroke = inline_keybind_display
+ .accept_keystroke
+ .as_ref()
+ .expect("should have an accept binding");
+ let preview_keystroke = inline_keybind_display
+ .preview_keystroke
+ .as_ref()
+ .expect("should have a preview binding");
+ let in_code_keystroke = inline_keybind_display
+ .displayed_keystroke
+ .as_ref()
+ .expect("should have an in-code binding");
+ let compact_cursor_popover_keystroke = compact_keybind_display
+ .displayed_keystroke
+ .as_ref()
+ .expect("should have a compact cursor popover binding");
+
+ assert_eq!(accept_keystroke.key(), "tab");
+ assert!(
+ !editor.has_visible_completions_menu(),
+ "compact cursor-popover branch should be used without a completions menu"
+ );
+ assert!(
+ preview_keystroke.modifiers().modified(),
+ "preview binding should use modifiers in subtle mode"
+ );
+ assert_eq!(
+ compact_cursor_popover_keystroke.key(),
+ preview_keystroke.key(),
+ "subtle compact cursor popover should prefer the preview binding"
+ );
+ assert_eq!(
+ compact_cursor_popover_keystroke.modifiers(),
+ preview_keystroke.modifiers(),
+ "subtle compact cursor popover should use the preview binding modifiers"
+ );
+ assert_eq!(
+ in_code_keystroke.key(),
+ preview_keystroke.key(),
+ "subtle in-code indicator should prefer the preview binding"
+ );
+ assert_eq!(
+ in_code_keystroke.modifiers(),
+ preview_keystroke.modifiers(),
+ "subtle in-code indicator should use the preview binding modifiers"
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_tab_accepts_edit_prediction_over_completion(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ load_default_keymap(cx);
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let provider = cx.new(|_| FakeEditPredictionDelegate::default());
+ assign_editor_completion_provider(provider.clone(), &mut cx);
+ cx.set_state("let x = Λ;");
+
+ propose_edits(&provider, vec![(8..8, "42")], &mut cx);
+ cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+
+ assert_editor_active_edit_completion(&mut cx, |_, edits| {
+ assert_eq!(edits.len(), 1);
+ assert_eq!(edits[0].1.as_ref(), "42");
+ });
+
+ cx.simulate_keystroke("tab");
+ cx.run_until_parked();
+
+ cx.assert_editor_state("let x = 42Λ;");
+}
+
+#[gpui::test]
+async fn test_single_line_prediction_uses_accept_cursor_popover_action(
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx, |_| {});
+ load_default_keymap(cx);
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let provider = cx.new(|_| FakeEditPredictionDelegate::default());
+ assign_editor_completion_provider(provider.clone(), &mut cx);
+ cx.set_state("let x = Λ;");
+
+ propose_edits(&provider, vec![(8..8, "42")], &mut cx);
+ cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+
+ cx.update_editor(|editor, window, cx| {
+ assert!(editor.has_active_edit_prediction());
+
+ let keybind_display = editor.edit_prediction_keybind_display(
+ EditPredictionKeybindSurface::CursorPopoverExpanded,
+ window,
+ cx,
+ );
+
+ let accept_keystroke = keybind_display
+ .accept_keystroke
+ .as_ref()
+ .expect("should have an accept binding");
+ let preview_keystroke = keybind_display
+ .preview_keystroke
+ .as_ref()
+ .expect("should have a preview binding");
+
+ assert_eq!(
+ keybind_display.action,
+ EditPredictionKeybindAction::Accept,
+ "single-line prediction should show the accept action"
+ );
+ assert_eq!(accept_keystroke.key(), "tab");
+ assert!(preview_keystroke.modifiers().modified());
+ });
+}
+
+#[gpui::test]
+async fn test_multi_line_prediction_uses_preview_cursor_popover_action(
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx, |_| {});
+ load_default_keymap(cx);
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let provider = cx.new(|_| FakeEditPredictionDelegate::default());
+ assign_editor_completion_provider(provider.clone(), &mut cx);
+ cx.set_state("let x = Λ;");
+
+ propose_edits(&provider, vec![(8..8, "42\n43")], &mut cx);
+ cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+
+ cx.update_editor(|editor, window, cx| {
+ assert!(editor.has_active_edit_prediction());
+
+ let keybind_display = editor.edit_prediction_keybind_display(
+ EditPredictionKeybindSurface::CursorPopoverExpanded,
+ window,
+ cx,
+ );
+ let preview_keystroke = keybind_display
+ .preview_keystroke
+ .as_ref()
+ .expect("should have a preview binding");
+
+ assert_eq!(
+ keybind_display.action,
+ EditPredictionKeybindAction::Preview,
+ "multi-line prediction should show the preview action"
+ );
+ assert!(preview_keystroke.modifiers().modified());
+ });
+}
+
+#[gpui::test]
+async fn test_single_line_prediction_with_preview_uses_accept_cursor_popover_action(
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx, |_| {});
+ load_default_keymap(cx);
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let provider = cx.new(|_| FakeEditPredictionDelegate::default());
+ assign_editor_completion_provider(provider.clone(), &mut cx);
+ cx.set_state("let x = Λ;");
+
+ propose_edits_with_preview(&provider, vec![(8..8, "42")], &mut cx).await;
+ cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+
+ cx.update_editor(|editor, window, cx| {
+ assert!(editor.has_active_edit_prediction());
+
+ let keybind_display = editor.edit_prediction_keybind_display(
+ EditPredictionKeybindSurface::CursorPopoverExpanded,
+ window,
+ cx,
+ );
+
+ let accept_keystroke = keybind_display
+ .accept_keystroke
+ .as_ref()
+ .expect("should have an accept binding");
+ let preview_keystroke = keybind_display
+ .preview_keystroke
+ .as_ref()
+ .expect("should have a preview binding");
+
+ assert_eq!(
+ keybind_display.action,
+ EditPredictionKeybindAction::Accept,
+ "single-line prediction should show the accept action even with edit_preview"
+ );
+ assert_eq!(accept_keystroke.key(), "tab");
+ assert!(preview_keystroke.modifiers().modified());
+ });
+}
+
+#[gpui::test]
+async fn test_multi_line_prediction_with_preview_uses_preview_cursor_popover_action(
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx, |_| {});
+ load_default_keymap(cx);
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let provider = cx.new(|_| FakeEditPredictionDelegate::default());
+ assign_editor_completion_provider(provider.clone(), &mut cx);
+ cx.set_state("let x = Λ;");
+
+ propose_edits_with_preview(&provider, vec![(8..8, "42\n43")], &mut cx).await;
+ cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+ cx.update_editor(|editor, window, cx| {
+ assert!(editor.has_active_edit_prediction());
+
+ let keybind_display = editor.edit_prediction_keybind_display(
+ EditPredictionKeybindSurface::CursorPopoverExpanded,
+ window,
+ cx,
+ );
+ let preview_keystroke = keybind_display
+ .preview_keystroke
+ .as_ref()
+ .expect("should have a preview binding");
+
+ assert_eq!(
+ keybind_display.action,
+ EditPredictionKeybindAction::Preview,
+ "multi-line prediction should show the preview action with edit_preview"
+ );
+ assert!(preview_keystroke.modifiers().modified());
+ });
+}
+
+#[gpui::test]
+async fn test_single_line_deletion_of_newline_uses_accept_cursor_popover_action(
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx, |_| {});
+ load_default_keymap(cx);
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let provider = cx.new(|_| FakeEditPredictionDelegate::default());
+ assign_editor_completion_provider(provider.clone(), &mut cx);
+ cx.set_state(indoc! {"
+ fn main() {
+ let value = 1;
+ Λprintln!(\"done\");
+ }
+ "});
+
+ propose_edits(
+ &provider,
+ vec![(Point::new(1, 18)..Point::new(2, 17), "")],
+ &mut cx,
+ );
+ cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+
+ cx.update_editor(|editor, window, cx| {
+ assert!(editor.has_active_edit_prediction());
+
+ let keybind_display = editor.edit_prediction_keybind_display(
+ EditPredictionKeybindSurface::CursorPopoverExpanded,
+ window,
+ cx,
+ );
+
+ let accept_keystroke = keybind_display
+ .accept_keystroke
+ .as_ref()
+ .expect("should have an accept binding");
+ let preview_keystroke = keybind_display
+ .preview_keystroke
+ .as_ref()
+ .expect("should have a preview binding");
+
+ assert_eq!(
+ keybind_display.action,
+ EditPredictionKeybindAction::Accept,
+ "deleting one newline plus adjacent text should show the accept action"
+ );
+ assert_eq!(accept_keystroke.key(), "tab");
+ assert!(preview_keystroke.modifiers().modified());
+ });
+}
+
+#[gpui::test]
+async fn test_stale_single_line_prediction_does_not_force_preview_cursor_popover_action(
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx, |_| {});
+ load_default_keymap(cx);
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let provider = cx.new(|_| FakeEditPredictionDelegate::default());
+ assign_editor_completion_provider(provider.clone(), &mut cx);
+ cx.set_state("let x = Λ;");
+
+ propose_edits(&provider, vec![(8..8, "42\n43")], &mut cx);
+ cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+ cx.update_editor(|editor, _window, cx| {
+ assert!(editor.active_edit_prediction.is_some());
+ assert!(editor.stale_edit_prediction_in_menu.is_none());
+ editor.take_active_edit_prediction(cx);
+ assert!(editor.active_edit_prediction.is_none());
+ assert!(editor.stale_edit_prediction_in_menu.is_some());
+ });
+
+ propose_edits(&provider, vec![(8..8, "42")], &mut cx);
+ cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+
+ cx.update_editor(|editor, window, cx| {
+ assert!(editor.has_active_edit_prediction());
+
+ let keybind_display = editor.edit_prediction_keybind_display(
+ EditPredictionKeybindSurface::CursorPopoverExpanded,
+ window,
+ cx,
+ );
+ let accept_keystroke = keybind_display
+ .accept_keystroke
+ .as_ref()
+ .expect("should have an accept binding");
+
+ assert_eq!(
+ keybind_display.action,
+ EditPredictionKeybindAction::Accept,
+ "single-line active prediction should show the accept action"
+ );
+ assert!(
+ editor.stale_edit_prediction_in_menu.is_none(),
+ "refreshing the visible prediction should clear stale menu state"
+ );
+ assert_eq!(accept_keystroke.key(), "tab");
+ });
+}
+
fn assert_editor_active_edit_completion(
cx: &mut EditorTestContext,
assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, Arc<str>)>),
@@ -528,6 +950,44 @@ fn propose_edits<T: ToOffset>(
propose_edits_with_cursor_position(provider, edits, None, cx);
}
+async fn propose_edits_with_preview<T: ToOffset + Clone>(
+ provider: &Entity<FakeEditPredictionDelegate>,
+ edits: Vec<(Range<T>, &str)>,
+ cx: &mut EditorTestContext,
+) {
+ let snapshot = cx.buffer_snapshot();
+ let edits = edits
+ .into_iter()
+ .map(|(range, text)| {
+ let anchor_range =
+ snapshot.anchor_after(range.start.clone())..snapshot.anchor_before(range.end);
+ (anchor_range, Arc::<str>::from(text))
+ })
+ .collect::<Vec<_>>();
+
+ let preview_edits = edits
+ .iter()
+ .map(|(range, text)| (range.clone(), text.clone()))
+ .collect::<Arc<[_]>>();
+
+ let edit_preview = cx
+ .buffer(|buffer: &Buffer, app| buffer.preview_edits(preview_edits, app))
+ .await;
+
+ let provider_edits = edits.into_iter().collect();
+
+ cx.update(|_, cx| {
+ provider.update(cx, |provider, _| {
+ provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
+ id: None,
+ edits: provider_edits,
+ cursor_position: None,
+ edit_preview: Some(edit_preview),
+ }))
+ })
+ });
+}
+
fn propose_edits_with_cursor_position<T: ToOffset>(
provider: &Entity<FakeEditPredictionDelegate>,
edits: Vec<(Range<T>, &str)>,
@@ -105,7 +105,7 @@ use edit_prediction_types::{
EditPredictionGranularity, SuggestionDisplayType,
};
use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings};
-use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line};
+use element::{LineWithInvisibles, PositionMap, layout_line};
use futures::{
FutureExt,
future::{self, Shared, join},
@@ -256,7 +256,6 @@ pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration:
pub const LSP_REQUEST_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(50);
pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction";
-pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict";
pub(crate) const MINIMAP_FONT_SIZE: AbsoluteLength = AbsoluteLength::Pixels(px(2.));
pub type RenderDiffHunkControlsFn = Arc<
@@ -701,6 +700,30 @@ pub enum EditPredictionPreview {
},
}
+#[derive(Copy, Clone, Eq, PartialEq)]
+enum EditPredictionKeybindSurface {
+ Inline,
+ CursorPopoverCompact,
+ CursorPopoverExpanded,
+}
+
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+enum EditPredictionKeybindAction {
+ Accept,
+ Preview,
+}
+
+struct EditPredictionKeybindDisplay {
+ #[cfg(test)]
+ accept_keystroke: Option<gpui::KeybindingKeystroke>,
+ #[cfg(test)]
+ preview_keystroke: Option<gpui::KeybindingKeystroke>,
+ displayed_keystroke: Option<gpui::KeybindingKeystroke>,
+ action: EditPredictionKeybindAction,
+ missing_accept_keystroke: bool,
+ show_hold_label: bool,
+}
+
impl EditPredictionPreview {
pub fn released_too_fast(&self) -> bool {
match self {
@@ -1225,8 +1248,7 @@ pub struct Editor {
show_completions_on_input_override: Option<bool>,
menu_edit_predictions_policy: MenuEditPredictionsPolicy,
edit_prediction_preview: EditPredictionPreview,
- edit_prediction_indent_conflict: bool,
- edit_prediction_requires_modifier_in_indent_conflict: bool,
+ in_leading_whitespace: bool,
next_inlay_id: usize,
next_color_inlay_id: usize,
_subscriptions: Vec<Subscription>,
@@ -2473,8 +2495,7 @@ impl Editor {
show_completions_on_input_override: None,
menu_edit_predictions_policy: MenuEditPredictionsPolicy::ByProvider,
edit_prediction_settings: EditPredictionSettings::Disabled,
- edit_prediction_indent_conflict: false,
- edit_prediction_requires_modifier_in_indent_conflict: true,
+ in_leading_whitespace: false,
custom_context_menu: None,
show_git_blame_gutter: false,
show_git_blame_inline: false,
@@ -2856,12 +2877,12 @@ impl Editor {
}
if has_active_edit_prediction {
- 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");
- }
+ key_context.add(EDIT_PREDICTION_KEY_CONTEXT);
+ key_context.add("copilot_suggestion");
+ }
+
+ if self.in_leading_whitespace {
+ key_context.add("in_leading_whitespace");
}
if self.selection_mark_mode {
@@ -2915,32 +2936,13 @@ impl Editor {
}
}
- pub fn edit_prediction_in_conflict(&self) -> bool {
- if !self.show_edit_predictions_in_menu() {
- return false;
- }
-
- let showing_completions = self
- .context_menu
- .borrow()
- .as_ref()
- .is_some_and(|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_indent_conflict && self.edit_prediction_indent_conflict)
- }
-
- pub fn accept_edit_prediction_keybind(
+ fn accept_edit_prediction_keystroke(
&self,
granularity: EditPredictionGranularity,
window: &mut Window,
cx: &mut App,
- ) -> AcceptEditPredictionBinding {
- let key_context = self.key_context_internal(true, window, cx);
- let in_conflict = self.edit_prediction_in_conflict();
+ ) -> Option<gpui::KeybindingKeystroke> {
+ let key_context = self.key_context_internal(self.has_active_edit_prediction(), window, cx);
let bindings =
match granularity {
@@ -2953,13 +2955,131 @@ impl Editor {
}
};
- AcceptEditPredictionBinding(bindings.into_iter().rev().find(|binding| {
- !in_conflict
- || binding
- .keystrokes()
- .first()
- .is_some_and(|keystroke| keystroke.modifiers().modified())
- }))
+ bindings
+ .into_iter()
+ .rev()
+ .find_map(|binding| match binding.keystrokes() {
+ [keystroke, ..] => Some(keystroke.clone()),
+ _ => None,
+ })
+ }
+
+ fn preview_edit_prediction_keystroke(
+ &self,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<gpui::KeybindingKeystroke> {
+ let key_context = self.key_context_internal(self.has_active_edit_prediction(), window, cx);
+ let bindings = window.bindings_for_action_in_context(&AcceptEditPrediction, key_context);
+ bindings
+ .into_iter()
+ .rev()
+ .find_map(|binding| match binding.keystrokes() {
+ [keystroke, ..] if keystroke.modifiers().modified() => Some(keystroke.clone()),
+ _ => None,
+ })
+ }
+
+ fn edit_prediction_cursor_popover_prefers_preview(
+ &self,
+ completion: &EditPredictionState,
+ ) -> bool {
+ match &completion.completion {
+ EditPrediction::Edit {
+ edits, snapshot, ..
+ } => {
+ let mut start_row: Option<u32> = None;
+ let mut end_row: Option<u32> = None;
+
+ for (range, text) in edits {
+ let edit_start_row = range.start.text_anchor.to_point(snapshot).row;
+ let old_end_row = range.end.text_anchor.to_point(snapshot).row;
+ let inserted_newline_count = text
+ .as_ref()
+ .chars()
+ .filter(|character| *character == '\n')
+ .count() as u32;
+ let deleted_newline_count = old_end_row - edit_start_row;
+ let preview_end_row = edit_start_row + inserted_newline_count;
+
+ start_row =
+ Some(start_row.map_or(edit_start_row, |row| row.min(edit_start_row)));
+ end_row = Some(end_row.map_or(preview_end_row, |row| row.max(preview_end_row)));
+
+ if deleted_newline_count > 1 {
+ end_row = Some(end_row.map_or(old_end_row, |row| row.max(old_end_row)));
+ }
+ }
+
+ start_row
+ .zip(end_row)
+ .is_some_and(|(start_row, end_row)| end_row > start_row)
+ }
+ EditPrediction::MoveWithin { .. } | EditPrediction::MoveOutside { .. } => false,
+ }
+ }
+
+ fn edit_prediction_keybind_display(
+ &self,
+ surface: EditPredictionKeybindSurface,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> EditPredictionKeybindDisplay {
+ let accept_keystroke =
+ self.accept_edit_prediction_keystroke(EditPredictionGranularity::Full, window, cx);
+ let preview_keystroke = self.preview_edit_prediction_keystroke(window, cx);
+
+ let action = match surface {
+ EditPredictionKeybindSurface::Inline
+ | EditPredictionKeybindSurface::CursorPopoverCompact => {
+ if self.edit_prediction_requires_modifier() {
+ EditPredictionKeybindAction::Preview
+ } else {
+ EditPredictionKeybindAction::Accept
+ }
+ }
+ EditPredictionKeybindSurface::CursorPopoverExpanded => self
+ .active_edit_prediction
+ .as_ref()
+ .filter(|completion| {
+ self.edit_prediction_cursor_popover_prefers_preview(completion)
+ })
+ .map_or(EditPredictionKeybindAction::Accept, |_| {
+ EditPredictionKeybindAction::Preview
+ }),
+ };
+ #[cfg(test)]
+ let preview_copy = preview_keystroke.clone();
+ #[cfg(test)]
+ let accept_copy = accept_keystroke.clone();
+
+ let displayed_keystroke = match surface {
+ EditPredictionKeybindSurface::Inline => match action {
+ EditPredictionKeybindAction::Accept => accept_keystroke,
+ EditPredictionKeybindAction::Preview => preview_keystroke,
+ },
+ EditPredictionKeybindSurface::CursorPopoverCompact
+ | EditPredictionKeybindSurface::CursorPopoverExpanded => match action {
+ EditPredictionKeybindAction::Accept => accept_keystroke,
+ EditPredictionKeybindAction::Preview => {
+ preview_keystroke.or_else(|| accept_keystroke.clone())
+ }
+ },
+ };
+
+ let missing_accept_keystroke = displayed_keystroke.is_none();
+
+ EditPredictionKeybindDisplay {
+ #[cfg(test)]
+ accept_keystroke: accept_copy,
+ #[cfg(test)]
+ preview_keystroke: preview_copy,
+ displayed_keystroke,
+ action,
+ missing_accept_keystroke,
+ show_hold_label: matches!(surface, EditPredictionKeybindSurface::CursorPopoverCompact)
+ && self.edit_prediction_preview.released_too_fast(),
+ }
}
pub fn new_file(
@@ -3596,7 +3716,6 @@ impl Editor {
self.refresh_matching_bracket_highlights(&display_map, cx);
self.refresh_outline_symbols_at_cursor(cx);
self.update_visible_edit_prediction(window, cx);
- self.edit_prediction_requires_modifier_in_indent_conflict = true;
self.inline_blame_popover.take();
if self.git_blame_inline_enabled {
self.start_inline_blame_timer(window, cx);
@@ -8216,8 +8335,6 @@ impl Editor {
}
}
}
-
- self.edit_prediction_requires_modifier_in_indent_conflict = false;
}
pub fn accept_next_word_edit_prediction(
@@ -8466,21 +8583,20 @@ impl Editor {
) {
let mut modifiers_held = false;
- // Check bindings for all granularities.
- // If the user holds the key for Word, Line, or Full, we want to show the preview.
- let granularities = [
- EditPredictionGranularity::Full,
- EditPredictionGranularity::Line,
- EditPredictionGranularity::Word,
+ let key_context = self.key_context_internal(self.has_active_edit_prediction(), window, cx);
+ let actions: [&dyn Action; 3] = [
+ &AcceptEditPrediction,
+ &AcceptNextWordEditPrediction,
+ &AcceptNextLineEditPrediction,
];
- for granularity in granularities {
- if let Some(keystroke) = self
- .accept_edit_prediction_keybind(granularity, window, cx)
- .keystroke()
- {
- modifiers_held = modifiers_held
- || (keystroke.modifiers() == modifiers && keystroke.modifiers().modified());
+ for action in actions {
+ let bindings = window.bindings_for_action_in_context(action, key_context.clone());
+ for binding in bindings {
+ if let Some(keystroke) = binding.keystrokes().first() {
+ modifiers_held = modifiers_held
+ || (keystroke.modifiers() == modifiers && keystroke.modifiers().modified());
+ }
}
}
@@ -8580,9 +8696,9 @@ impl Editor {
self.edit_prediction_settings =
self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx);
- self.edit_prediction_indent_conflict = multibuffer.is_line_whitespace_upto(cursor);
+ self.in_leading_whitespace = multibuffer.is_line_whitespace_upto(cursor);
- if self.edit_prediction_indent_conflict {
+ if self.in_leading_whitespace {
let cursor_point = cursor.to_point(&multibuffer);
let mut suggested_indent = None;
multibuffer.suggested_indents_callback(
@@ -8597,7 +8713,7 @@ impl Editor {
if let Some(indent) = suggested_indent
&& indent.len == cursor_point.column
{
- self.edit_prediction_indent_conflict = false;
+ self.in_leading_whitespace = false;
}
}
@@ -9610,7 +9726,7 @@ impl Editor {
const BORDER_WIDTH: Pixels = px(1.);
- let keybind = self.render_edit_prediction_accept_keybind(window, cx);
+ let keybind = self.render_edit_prediction_keybind(window, cx);
let has_keybind = keybind.is_some();
let mut element = h_flex()
@@ -9766,49 +9882,81 @@ impl Editor {
}
}
- fn render_edit_prediction_accept_keybind(
+ fn render_edit_prediction_inline_keystroke(
&self,
- window: &mut Window,
- cx: &mut App,
- ) -> Option<AnyElement> {
- let accept_binding =
- self.accept_edit_prediction_keybind(EditPredictionGranularity::Full, window, cx);
- let accept_keystroke = accept_binding.keystroke()?;
-
+ keystroke: &gpui::KeybindingKeystroke,
+ modifiers_color: Color,
+ cx: &App,
+ ) -> AnyElement {
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
- let modifiers_color = if *accept_keystroke.modifiers() == window.modifiers() {
- Color::Accent
- } else {
- Color::Muted
- };
-
h_flex()
.px_0p5()
.when(is_platform_style_mac, |parent| parent.gap_0p5())
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_size(TextSize::XSmall.rems(cx))
.child(h_flex().children(ui::render_modifiers(
- accept_keystroke.modifiers(),
+ keystroke.modifiers(),
PlatformStyle::platform(),
Some(modifiers_color),
Some(IconSize::XSmall.rems().into()),
true,
)))
.when(is_platform_style_mac, |parent| {
- parent.child(accept_keystroke.key().to_string())
+ parent.child(keystroke.key().to_string())
})
.when(!is_platform_style_mac, |parent| {
parent.child(
- Key::new(
- util::capitalize(accept_keystroke.key()),
- Some(Color::Default),
- )
- .size(Some(IconSize::XSmall.rems().into())),
+ Key::new(util::capitalize(keystroke.key()), Some(Color::Default))
+ .size(Some(IconSize::XSmall.rems().into())),
)
})
.into_any()
- .into()
+ }
+
+ fn render_edit_prediction_popover_keystroke(
+ &self,
+ keystroke: &gpui::KeybindingKeystroke,
+ color: Color,
+ cx: &App,
+ ) -> AnyElement {
+ let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
+
+ if keystroke.modifiers().modified() {
+ h_flex()
+ .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
+ .when(is_platform_style_mac, |parent| parent.gap_1())
+ .child(h_flex().children(ui::render_modifiers(
+ keystroke.modifiers(),
+ PlatformStyle::platform(),
+ Some(color),
+ None,
+ false,
+ )))
+ .into_any()
+ } else {
+ Key::new(util::capitalize(keystroke.key()), Some(color))
+ .size(Some(IconSize::XSmall.rems().into()))
+ .into_any_element()
+ }
+ }
+
+ fn render_edit_prediction_keybind(
+ &self,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<AnyElement> {
+ let keybind_display =
+ self.edit_prediction_keybind_display(EditPredictionKeybindSurface::Inline, window, cx);
+ let keystroke = keybind_display.displayed_keystroke.as_ref()?;
+
+ let modifiers_color = if *keystroke.modifiers() == window.modifiers() {
+ Color::Accent
+ } else {
+ Color::Muted
+ };
+
+ Some(self.render_edit_prediction_inline_keystroke(keystroke, modifiers_color, cx))
}
fn render_edit_prediction_line_popover(
@@ -9820,7 +9968,7 @@ impl Editor {
) -> 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 keybind = self.render_edit_prediction_keybind(window, cx);
let has_keybind = keybind.is_some();
let icons = Self::get_prediction_provider_icons(&self.edit_prediction_provider, cx);
@@ -9879,7 +10027,7 @@ impl Editor {
window: &mut Window,
cx: &mut App,
) -> Stateful<Div> {
- let keybind = self.render_edit_prediction_accept_keybind(window, cx);
+ let keybind = self.render_edit_prediction_keybind(window, cx);
let has_keybind = keybind.is_some();
let icons = Self::get_prediction_provider_icons(&self.edit_prediction_provider, cx);
@@ -9962,8 +10110,7 @@ impl Editor {
max_width: Pixels,
cursor_point: Point,
style: &EditorStyle,
- accept_keystroke: Option<&gpui::KeybindingKeystroke>,
- _window: &Window,
+ window: &mut Window,
cx: &mut Context<Editor>,
) -> Option<AnyElement> {
let provider = self.edit_prediction_provider.as_ref()?;
@@ -9980,13 +10127,18 @@ impl Editor {
if !self.has_visible_completions_menu() {
const RADIUS: Pixels = px(6.);
const BORDER_WIDTH: Pixels = px(1.);
+ let keybind_display = self.edit_prediction_keybind_display(
+ EditPredictionKeybindSurface::CursorPopoverCompact,
+ window,
+ cx,
+ );
return Some(
h_flex()
.elevation_2(cx)
.border(BORDER_WIDTH)
.border_color(cx.theme().colors().border)
- .when(accept_keystroke.is_none(), |el| {
+ .when(keybind_display.missing_accept_keystroke, |el| {
el.border_color(cx.theme().status().error)
})
.rounded(RADIUS)
@@ -10017,18 +10169,19 @@ impl Editor {
.border_l_1()
.border_color(cx.theme().colors().border)
.bg(Self::edit_prediction_line_popover_bg_color(cx))
- .when(self.edit_prediction_preview.released_too_fast(), |el| {
+ .when(keybind_display.show_hold_label, |el| {
el.child(
Label::new("Hold")
.size(LabelSize::Small)
- .when(accept_keystroke.is_none(), |el| {
- el.strikethrough()
- })
+ .when(
+ keybind_display.missing_accept_keystroke,
+ |el| el.strikethrough(),
+ )
.line_height_style(LineHeightStyle::UiLabel),
)
})
.id("edit_prediction_cursor_popover_keybind")
- .when(accept_keystroke.is_none(), |el| {
+ .when(keybind_display.missing_accept_keystroke, |el| {
let status_colors = cx.theme().status();
el.bg(status_colors.error_background)
@@ -10041,15 +10194,13 @@ impl Editor {
})
})
.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,
- )))
+ keybind_display.displayed_keystroke.as_ref(),
+ |el, compact_keystroke| {
+ el.child(self.render_edit_prediction_popover_keystroke(
+ compact_keystroke,
+ Color::Default,
+ cx,
+ ))
},
),
)
@@ -10096,8 +10247,12 @@ impl Editor {
};
let has_completion = self.active_edit_prediction.is_some();
+ let keybind_display = self.edit_prediction_keybind_display(
+ EditPredictionKeybindSurface::CursorPopoverExpanded,
+ window,
+ cx,
+ );
- let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
Some(
h_flex()
.min_w(min_width)
@@ -10113,41 +10268,51 @@ impl Editor {
.overflow_hidden()
.child(completion),
)
- .when_some(accept_keystroke, |el, accept_keystroke| {
- if !accept_keystroke.modifiers().modified() {
- return el;
- }
+ .when_some(
+ keybind_display.displayed_keystroke.as_ref(),
+ |el, keystroke| {
+ let key_color = if !has_completion {
+ Color::Muted
+ } else {
+ Color::Default
+ };
- el.child(
- h_flex()
- .h_full()
- .border_l_1()
- .rounded_r_lg()
- .border_color(cx.theme().colors().border)
- .bg(Self::edit_prediction_line_popover_bg_color(cx))
- .gap_1()
- .py_1()
- .px_2()
- .child(
+ if keybind_display.action == EditPredictionKeybindAction::Preview {
+ el.child(
h_flex()
- .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
- .when(is_platform_style_mac, |parent| parent.gap_1())
- .child(h_flex().children(ui::render_modifiers(
- accept_keystroke.modifiers(),
- PlatformStyle::platform(),
- Some(if !has_completion {
- Color::Muted
- } else {
- Color::Default
- }),
- None,
- false,
- ))),
+ .h_full()
+ .border_l_1()
+ .rounded_r_lg()
+ .border_color(cx.theme().colors().border)
+ .bg(Self::edit_prediction_line_popover_bg_color(cx))
+ .gap_1()
+ .py_1()
+ .px_2()
+ .child(self.render_edit_prediction_popover_keystroke(
+ keystroke, key_color, cx,
+ ))
+ .child(Label::new("Preview").into_any_element())
+ .opacity(if has_completion { 1.0 } else { 0.4 }),
)
- .child(Label::new("Preview").into_any_element())
- .opacity(if has_completion { 1.0 } else { 0.4 }),
- )
- })
+ } else {
+ el.child(
+ h_flex()
+ .h_full()
+ .border_l_1()
+ .rounded_r_lg()
+ .border_color(cx.theme().colors().border)
+ .bg(Self::edit_prediction_line_popover_bg_color(cx))
+ .gap_1()
+ .py_1()
+ .px_2()
+ .child(self.render_edit_prediction_popover_keystroke(
+ keystroke, key_color, cx,
+ ))
+ .opacity(if has_completion { 1.0 } else { 0.4 }),
+ )
+ }
+ },
+ )
.into_any(),
)
}
@@ -43,13 +43,12 @@ use gpui::{
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, Font, FontId,
FontWeight, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement,
- IsZero, KeybindingKeystroke, Length, Modifiers, ModifiersChangedEvent, MouseButton,
- MouseClickEvent, MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, PaintQuad,
- ParentElement, Pixels, PressureStage, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine,
- SharedString, Size, StatefulInteractiveElement, Style, Styled, StyledText, TextAlign, TextRun,
- TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop,
- linear_gradient, outline, pattern_slash, point, px, quad, relative, size, solid_background,
- transparent_black,
+ IsZero, Length, Modifiers, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent,
+ MouseMoveEvent, MousePressureEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels,
+ PressureStage, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size,
+ StatefulInteractiveElement, Style, Styled, StyledText, TextAlign, TextRun, TextStyleRefinement,
+ WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline,
+ pattern_slash, point, px, quad, relative, size, solid_background, transparent_black,
};
use itertools::Itertools;
use language::{HighlightedText, IndentGuideSettings, language_settings::ShowWhitespaceSetting};
@@ -59,8 +58,6 @@ use multi_buffer::{
MultiBufferRow, RowInfo,
};
-use edit_prediction_types::EditPredictionGranularity;
-
use project::{
DisableAiSettings, Entry, ProjectPath,
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
@@ -4838,17 +4835,11 @@ impl EditorElement {
let edit_prediction = if edit_prediction_popover_visible {
self.editor.update(cx, move |editor, cx| {
- let accept_binding = editor.accept_edit_prediction_keybind(
- EditPredictionGranularity::Full,
- window,
- cx,
- );
let mut element = editor.render_edit_prediction_cursor_popover(
min_width,
max_width,
cursor_point,
style,
- accept_binding.keystroke(),
window,
cx,
)?;
@@ -8618,21 +8609,6 @@ pub(crate) fn render_buffer_header(
})
}
-pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
-
-impl AcceptEditPredictionBinding {
- pub fn keystroke(&self) -> Option<&KeybindingKeystroke> {
- if let Some(binding) = self.0.as_ref() {
- match &binding.keystrokes() {
- [keystroke, ..] => Some(keystroke),
- _ => None,
- }
- } else {
- None
- }
- }
-}
-
fn prepaint_gutter_button(
mut button: AnyElement,
row: DisplayRow,