editor: Accept next line prediction (#44411)
Closes [#20574](https://github.com/zed-industries/zed/issues/20574) Release Notes: - Replaced editor action editor::AcceptPartialEditPrediction with editor::AcceptNextLineEditPrediction and editor::AcceptNextWordEditPrediction Tested manually on windows, attaching screen cap. https://github.com/user-attachments/assets/fea04499-fd16-4b7d-a6aa-3661bb85cf4f Updated existing test for accepting word prediction in copilot - it is already marked as flaky, not sure what to do about it and I'm not really confident creating new one without a working example. Added migration of keymaps and new defaults for windows, linux, macos in defaults and in cursor. This should alleviate [#21645](https://github.com/zed-industries/zed/issues/21645) I used some work done in stale PR https://github.com/zed-industries/zed/pull/25274, hopefully this one makes it through! --------- Co-authored-by: Agus Zubiaga <agus@zed.dev>
This commit is contained in:
1 parent
b8a7b2981b
commit
ff08911ffe
14 files changed
+280
-183
No files matched your search
@@ -746,7 +746,8 @@
|
||||
"alt-tab": "editor::AcceptEditPrediction",
|
||||
"alt-l": "editor::AcceptEditPrediction",
|
||||
"tab": "editor::AcceptEditPrediction",
|
||||
"alt-right": "editor::AcceptPartialEditPrediction",
|
||||
"alt-right": "editor::AcceptNextWordEditPrediction",
|
||||
"alt-down": "editor::AcceptNextLineEditPrediction",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -754,7 +755,8 @@
|
||||
"bindings": {
|
||||
"alt-tab": "editor::AcceptEditPrediction",
|
||||
"alt-l": "editor::AcceptEditPrediction",
|
||||
"alt-right": "editor::AcceptPartialEditPrediction",
|
||||
"alt-right": "editor::AcceptNextWordEditPrediction",
|
||||
"alt-down": "editor::AcceptNextLineEditPrediction",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -810,7 +810,8 @@
|
||||
"bindings": {
|
||||
"alt-tab": "editor::AcceptEditPrediction",
|
||||
"tab": "editor::AcceptEditPrediction",
|
||||
"ctrl-cmd-right": "editor::AcceptPartialEditPrediction",
|
||||
"ctrl-cmd-right": "editor::AcceptNextWordEditPrediction",
|
||||
"ctrl-cmd-down": "editor::AcceptNextLineEditPrediction",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -818,7 +819,8 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-tab": "editor::AcceptEditPrediction",
|
||||
"ctrl-cmd-right": "editor::AcceptPartialEditPrediction",
|
||||
"ctrl-cmd-right": "editor::AcceptNextWordEditPrediction",
|
||||
"ctrl-cmd-down": "editor::AcceptNextLineEditPrediction",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -747,7 +747,8 @@
|
||||
"alt-tab": "editor::AcceptEditPrediction",
|
||||
"alt-l": "editor::AcceptEditPrediction",
|
||||
"tab": "editor::AcceptEditPrediction",
|
||||
"alt-right": "editor::AcceptPartialEditPrediction",
|
||||
"alt-right": "editor::AcceptNextWordEditPrediction",
|
||||
"alt-down": "editor::AcceptNextLineEditPrediction",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -756,7 +757,8 @@
|
||||
"bindings": {
|
||||
"alt-tab": "editor::AcceptEditPrediction",
|
||||
"alt-l": "editor::AcceptEditPrediction",
|
||||
"alt-right": "editor::AcceptPartialEditPrediction",
|
||||
"alt-right": "editor::AcceptNextWordEditPrediction",
|
||||
"alt-down": "editor::AcceptNextLineEditPrediction",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
"context": "Editor && mode == full && edit_prediction",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-right": "editor::AcceptPartialEditPrediction",
|
||||
"cmd-right": "editor::AcceptNextWordEditPrediction",
|
||||
"cmd-down": "editor::AcceptNextLineEditPrediction",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -261,12 +261,14 @@ fn update_command_palette_filter(cx: &mut App) {
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _| {
|
||||
use editor::actions::{
|
||||
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
|
||||
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
||||
AcceptEditPrediction, AcceptNextLineEditPrediction, AcceptNextWordEditPrediction,
|
||||
NextEditPrediction, PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
||||
};
|
||||
let edit_prediction_actions = [
|
||||
TypeId::of::<AcceptEditPrediction>(),
|
||||
TypeId::of::<AcceptPartialEditPrediction>(),
|
||||
TypeId::of::<AcceptNextWordEditPrediction>(),
|
||||
TypeId::of::<AcceptNextLineEditPrediction>(),
|
||||
TypeId::of::<AcceptEditPrediction>(),
|
||||
TypeId::of::<ShowEditPrediction>(),
|
||||
TypeId::of::<NextEditPrediction>(),
|
||||
TypeId::of::<PreviousEditPrediction>(),
|
||||
|
||||
@@ -269,6 +269,7 @@ fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b:
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use edit_prediction_types::EditPredictionGranularity;
|
||||
use editor::{
|
||||
Editor, ExcerptRange, MultiBuffer, MultiBufferOffset, SelectionEffects,
|
||||
test::editor_lsp_test_context::EditorLspTestContext,
|
||||
@@ -581,13 +582,15 @@ mod tests {
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
|
||||
// Accepting the first word of the suggestion should only accept the first word and still show the rest.
|
||||
editor.accept_partial_edit_prediction(&Default::default(), window, cx);
|
||||
editor.accept_partial_edit_prediction(EditPredictionGranularity::Word, window, cx);
|
||||
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n");
|
||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
|
||||
// Accepting next word should accept the non-word and copilot suggestion should be gone
|
||||
editor.accept_partial_edit_prediction(&Default::default(), window, cx);
|
||||
editor.accept_partial_edit_prediction(EditPredictionGranularity::Word, window, cx);
|
||||
|
||||
assert!(!editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
@@ -623,7 +626,7 @@ mod tests {
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
|
||||
// Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest.
|
||||
editor.accept_partial_edit_prediction(&Default::default(), window, cx);
|
||||
editor.accept_partial_edit_prediction(EditPredictionGranularity::Word, window, cx);
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n");
|
||||
assert_eq!(
|
||||
@@ -632,7 +635,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Accepting next word should accept the next word and copilot suggestion should still exist
|
||||
editor.accept_partial_edit_prediction(&Default::default(), window, cx);
|
||||
editor.accept_partial_edit_prediction(EditPredictionGranularity::Word, window, cx);
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n");
|
||||
assert_eq!(
|
||||
@@ -641,7 +644,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone
|
||||
editor.accept_partial_edit_prediction(&Default::default(), window, cx);
|
||||
editor.accept_partial_edit_prediction(EditPredictionGranularity::Word, window, cx);
|
||||
assert!(!editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n");
|
||||
assert_eq!(
|
||||
|
||||
@@ -249,6 +249,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum EditPredictionGranularity {
|
||||
Word,
|
||||
Line,
|
||||
Full,
|
||||
}
|
||||
/// Returns edits updated based on user edits since the old snapshot. None is returned if any user
|
||||
/// edit is not a prefix of a predicted insertion.
|
||||
pub fn interpolate_edits(
|
||||
|
||||
@@ -370,7 +370,8 @@ actions!(
|
||||
AcceptEditPrediction,
|
||||
/// Accepts a partial edit prediction.
|
||||
#[action(deprecated_aliases = ["editor::AcceptPartialCopilotSuggestion"])]
|
||||
AcceptPartialEditPrediction,
|
||||
AcceptNextWordEditPrediction,
|
||||
AcceptNextLineEditPrediction,
|
||||
/// Applies all diff hunks in the editor.
|
||||
ApplyAllDiffHunks,
|
||||
/// Applies the diff hunk at the current position.
|
||||
|
||||
+188
-163
@@ -92,7 +92,9 @@ use collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
||||
use convert_case::{Case, Casing};
|
||||
use dap::TelemetrySpawnLocation;
|
||||
use display_map::*;
|
||||
use edit_prediction_types::{EditPredictionDelegate, EditPredictionDelegateHandle};
|
||||
use edit_prediction_types::{
|
||||
EditPredictionDelegate, EditPredictionDelegateHandle, EditPredictionGranularity,
|
||||
};
|
||||
use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings};
|
||||
use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line};
|
||||
use futures::{
|
||||
@@ -2778,21 +2780,24 @@ impl Editor {
|
||||
|
||||
pub fn accept_edit_prediction_keybind(
|
||||
&self,
|
||||
accept_partial: bool,
|
||||
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();
|
||||
|
||||
let bindings = if accept_partial {
|
||||
window.bindings_for_action_in_context(&AcceptPartialEditPrediction, key_context)
|
||||
} else {
|
||||
window.bindings_for_action_in_context(&AcceptEditPrediction, key_context)
|
||||
};
|
||||
let bindings =
|
||||
match granularity {
|
||||
EditPredictionGranularity::Word => window
|
||||
.bindings_for_action_in_context(&AcceptNextWordEditPrediction, key_context),
|
||||
EditPredictionGranularity::Line => window
|
||||
.bindings_for_action_in_context(&AcceptNextLineEditPrediction, key_context),
|
||||
EditPredictionGranularity::Full => {
|
||||
window.bindings_for_action_in_context(&AcceptEditPrediction, key_context)
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: if the binding contains multiple keystrokes, display all of them, not
|
||||
// just the first one.
|
||||
AcceptEditPredictionBinding(bindings.into_iter().rev().find(|binding| {
|
||||
!in_conflict
|
||||
|| binding
|
||||
@@ -7633,9 +7638,9 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn accept_edit_prediction(
|
||||
pub fn accept_partial_edit_prediction(
|
||||
&mut self,
|
||||
_: &AcceptEditPrediction,
|
||||
granularity: EditPredictionGranularity,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -7647,47 +7652,59 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
|
||||
if !matches!(granularity, EditPredictionGranularity::Full) && self.selections.count() != 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
match &active_edit_prediction.completion {
|
||||
EditPrediction::MoveWithin { target, .. } => {
|
||||
let target = *target;
|
||||
|
||||
if let Some(position_map) = &self.last_position_map {
|
||||
if position_map
|
||||
.visible_row_range
|
||||
.contains(&target.to_display_point(&position_map.snapshot).row())
|
||||
|| !self.edit_prediction_requires_modifier()
|
||||
{
|
||||
self.unfold_ranges(&[target..target], true, false, cx);
|
||||
// Note that this is also done in vim's handler of the Tab action.
|
||||
self.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::newest()),
|
||||
window,
|
||||
cx,
|
||||
|selections| {
|
||||
selections.select_anchor_ranges([target..target]);
|
||||
},
|
||||
);
|
||||
self.clear_row_highlights::<EditPredictionPreview>();
|
||||
if matches!(granularity, EditPredictionGranularity::Full) {
|
||||
if let Some(position_map) = &self.last_position_map {
|
||||
let target_row = target.to_display_point(&position_map.snapshot).row();
|
||||
let is_visible = position_map.visible_row_range.contains(&target_row);
|
||||
|
||||
self.edit_prediction_preview
|
||||
.set_previous_scroll_position(None);
|
||||
} else {
|
||||
self.edit_prediction_preview
|
||||
.set_previous_scroll_position(Some(
|
||||
position_map.snapshot.scroll_anchor,
|
||||
));
|
||||
|
||||
self.highlight_rows::<EditPredictionPreview>(
|
||||
target..target,
|
||||
cx.theme().colors().editor_highlighted_line_background,
|
||||
RowHighlightOptions {
|
||||
autoscroll: true,
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
if is_visible || !self.edit_prediction_requires_modifier() {
|
||||
self.unfold_ranges(&[target..target], true, false, cx);
|
||||
self.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::newest()),
|
||||
window,
|
||||
cx,
|
||||
|selections| {
|
||||
selections.select_anchor_ranges([target..target]);
|
||||
},
|
||||
);
|
||||
self.clear_row_highlights::<EditPredictionPreview>();
|
||||
self.edit_prediction_preview
|
||||
.set_previous_scroll_position(None);
|
||||
} else {
|
||||
// Highlight and request scroll
|
||||
self.edit_prediction_preview
|
||||
.set_previous_scroll_position(Some(
|
||||
position_map.snapshot.scroll_anchor,
|
||||
));
|
||||
self.highlight_rows::<EditPredictionPreview>(
|
||||
target..target,
|
||||
cx.theme().colors().editor_highlighted_line_background,
|
||||
RowHighlightOptions {
|
||||
autoscroll: true,
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::newest()),
|
||||
window,
|
||||
cx,
|
||||
|selections| {
|
||||
selections.select_anchor_ranges([target..target]);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
EditPrediction::MoveOutside { snapshot, target } => {
|
||||
@@ -7703,126 +7720,131 @@ impl Editor {
|
||||
cx,
|
||||
);
|
||||
|
||||
if let Some(provider) = self.edit_prediction_provider() {
|
||||
provider.accept(cx);
|
||||
}
|
||||
match granularity {
|
||||
EditPredictionGranularity::Full => {
|
||||
if let Some(provider) = self.edit_prediction_provider() {
|
||||
provider.accept(cx);
|
||||
}
|
||||
|
||||
// Store the transaction ID and selections before applying the edit
|
||||
let transaction_id_prev = self.buffer.read(cx).last_transaction_id(cx);
|
||||
let transaction_id_prev = self.buffer.read(cx).last_transaction_id(cx);
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let last_edit_end = edits.last().unwrap().0.end.bias_right(&snapshot);
|
||||
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let last_edit_end = edits.last().unwrap().0.end.bias_right(&snapshot);
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits.iter().cloned(), None, cx)
|
||||
});
|
||||
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits.iter().cloned(), None, cx)
|
||||
});
|
||||
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_anchor_ranges([last_edit_end..last_edit_end]);
|
||||
});
|
||||
|
||||
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_anchor_ranges([last_edit_end..last_edit_end]);
|
||||
});
|
||||
let selections = self.selections.disjoint_anchors_arc();
|
||||
if let Some(transaction_id_now) =
|
||||
self.buffer.read(cx).last_transaction_id(cx)
|
||||
{
|
||||
if transaction_id_prev != Some(transaction_id_now) {
|
||||
self.selection_history
|
||||
.insert_transaction(transaction_id_now, selections);
|
||||
}
|
||||
}
|
||||
|
||||
let selections = self.selections.disjoint_anchors_arc();
|
||||
if let Some(transaction_id_now) = self.buffer.read(cx).last_transaction_id(cx) {
|
||||
let has_new_transaction = transaction_id_prev != Some(transaction_id_now);
|
||||
if has_new_transaction {
|
||||
self.selection_history
|
||||
.insert_transaction(transaction_id_now, selections);
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
if self.active_edit_prediction.is_none() {
|
||||
self.refresh_edit_prediction(true, true, window, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
_ => {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let cursor_offset = self
|
||||
.selections
|
||||
.newest::<MultiBufferOffset>(&self.display_snapshot(cx))
|
||||
.head();
|
||||
|
||||
let insertion = edits.iter().find_map(|(range, text)| {
|
||||
let range = range.to_offset(&snapshot);
|
||||
if range.is_empty() && range.start == cursor_offset {
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(text) = insertion {
|
||||
let text_to_insert = match granularity {
|
||||
EditPredictionGranularity::Word => {
|
||||
let mut partial = text
|
||||
.chars()
|
||||
.by_ref()
|
||||
.take_while(|c| c.is_alphabetic())
|
||||
.collect::<String>();
|
||||
if partial.is_empty() {
|
||||
partial = text
|
||||
.chars()
|
||||
.by_ref()
|
||||
.take_while(|c| c.is_whitespace() || !c.is_alphabetic())
|
||||
.collect::<String>();
|
||||
}
|
||||
partial
|
||||
}
|
||||
EditPredictionGranularity::Line => {
|
||||
if let Some(line) = text.split_inclusive('\n').next() {
|
||||
line.to_string()
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
EditPredictionGranularity::Full => unreachable!(),
|
||||
};
|
||||
|
||||
cx.emit(EditorEvent::InputHandled {
|
||||
utf16_range_to_replace: None,
|
||||
text: text_to_insert.clone().into(),
|
||||
});
|
||||
|
||||
self.insert_with_autoindent_mode(&text_to_insert, None, window, cx);
|
||||
self.refresh_edit_prediction(true, true, window, cx);
|
||||
cx.notify();
|
||||
} else {
|
||||
self.accept_partial_edit_prediction(
|
||||
EditPredictionGranularity::Full,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
if self.active_edit_prediction.is_none() {
|
||||
self.refresh_edit_prediction(true, true, window, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
self.edit_prediction_requires_modifier_in_indent_conflict = false;
|
||||
}
|
||||
|
||||
pub fn accept_partial_edit_prediction(
|
||||
pub fn accept_next_word_edit_prediction(
|
||||
&mut self,
|
||||
_: &AcceptPartialEditPrediction,
|
||||
_: &AcceptNextWordEditPrediction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(active_edit_prediction) = self.active_edit_prediction.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if self.selections.count() != 1 {
|
||||
return;
|
||||
}
|
||||
self.accept_partial_edit_prediction(EditPredictionGranularity::Word, window, cx);
|
||||
}
|
||||
|
||||
match &active_edit_prediction.completion {
|
||||
EditPrediction::MoveWithin { target, .. } => {
|
||||
let target = *target;
|
||||
self.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::newest()),
|
||||
window,
|
||||
cx,
|
||||
|selections| {
|
||||
selections.select_anchor_ranges([target..target]);
|
||||
},
|
||||
);
|
||||
}
|
||||
EditPrediction::MoveOutside { snapshot, target } => {
|
||||
if let Some(workspace) = self.workspace() {
|
||||
Self::open_editor_at_anchor(snapshot, *target, &workspace, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
EditPrediction::Edit { edits, .. } => {
|
||||
self.report_edit_prediction_event(
|
||||
active_edit_prediction.completion_id.clone(),
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
pub fn accept_next_line_edit_prediction(
|
||||
&mut self,
|
||||
_: &AcceptNextLineEditPrediction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.accept_partial_edit_prediction(EditPredictionGranularity::Line, window, cx);
|
||||
}
|
||||
|
||||
// Find an insertion that starts at the cursor position.
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let cursor_offset = self
|
||||
.selections
|
||||
.newest::<MultiBufferOffset>(&self.display_snapshot(cx))
|
||||
.head();
|
||||
let insertion = edits.iter().find_map(|(range, text)| {
|
||||
let range = range.to_offset(&snapshot);
|
||||
if range.is_empty() && range.start == cursor_offset {
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(text) = insertion {
|
||||
let mut partial_completion = text
|
||||
.chars()
|
||||
.by_ref()
|
||||
.take_while(|c| c.is_alphabetic())
|
||||
.collect::<String>();
|
||||
if partial_completion.is_empty() {
|
||||
partial_completion = text
|
||||
.chars()
|
||||
.by_ref()
|
||||
.take_while(|c| c.is_whitespace() || !c.is_alphabetic())
|
||||
.collect::<String>();
|
||||
}
|
||||
|
||||
cx.emit(EditorEvent::InputHandled {
|
||||
utf16_range_to_replace: None,
|
||||
text: partial_completion.clone().into(),
|
||||
});
|
||||
|
||||
self.insert_with_autoindent_mode(&partial_completion, None, window, cx);
|
||||
|
||||
self.refresh_edit_prediction(true, true, window, cx);
|
||||
cx.notify();
|
||||
} else {
|
||||
self.accept_edit_prediction(&Default::default(), window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn accept_edit_prediction(
|
||||
&mut self,
|
||||
_: &AcceptEditPrediction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.accept_partial_edit_prediction(EditPredictionGranularity::Full, window, cx);
|
||||
}
|
||||
|
||||
fn discard_edit_prediction(
|
||||
@@ -8042,21 +8064,23 @@ impl Editor {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut modifiers_held = false;
|
||||
if let Some(accept_keystroke) = self
|
||||
.accept_edit_prediction_keybind(false, window, cx)
|
||||
.keystroke()
|
||||
{
|
||||
modifiers_held = modifiers_held
|
||||
|| (accept_keystroke.modifiers() == modifiers
|
||||
&& accept_keystroke.modifiers().modified());
|
||||
};
|
||||
if let Some(accept_partial_keystroke) = self
|
||||
.accept_edit_prediction_keybind(true, window, cx)
|
||||
.keystroke()
|
||||
{
|
||||
modifiers_held = modifiers_held
|
||||
|| (accept_partial_keystroke.modifiers() == modifiers
|
||||
&& accept_partial_keystroke.modifiers().modified());
|
||||
|
||||
// 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,
|
||||
];
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
if modifiers_held {
|
||||
@@ -9476,7 +9500,8 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
let accept_binding = self.accept_edit_prediction_keybind(false, window, cx);
|
||||
let accept_binding =
|
||||
self.accept_edit_prediction_keybind(EditPredictionGranularity::Full, window, cx);
|
||||
let accept_keystroke = accept_binding.keystroke()?;
|
||||
|
||||
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
|
||||
|
||||
@@ -62,6 +62,7 @@ use multi_buffer::{
|
||||
MultiBufferRow, RowInfo,
|
||||
};
|
||||
|
||||
use edit_prediction_types::EditPredictionGranularity;
|
||||
use project::{
|
||||
Entry, ProjectPath,
|
||||
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
|
||||
@@ -603,7 +604,8 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::display_cursor_names);
|
||||
register_action(editor, window, Editor::unique_lines_case_insensitive);
|
||||
register_action(editor, window, Editor::unique_lines_case_sensitive);
|
||||
register_action(editor, window, Editor::accept_partial_edit_prediction);
|
||||
register_action(editor, window, Editor::accept_next_word_edit_prediction);
|
||||
register_action(editor, window, Editor::accept_next_line_edit_prediction);
|
||||
register_action(editor, window, Editor::accept_edit_prediction);
|
||||
register_action(editor, window, Editor::restore_file);
|
||||
register_action(editor, window, Editor::git_restore);
|
||||
@@ -4900,8 +4902,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(false, window, 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,
|
||||
|
||||
@@ -159,3 +159,9 @@ pub(crate) mod m_2025_12_01 {
|
||||
|
||||
pub(crate) use settings::SETTINGS_PATTERNS;
|
||||
}
|
||||
|
||||
pub(crate) mod m_2025_12_08 {
|
||||
mod keymap;
|
||||
|
||||
pub(crate) use keymap::KEYMAP_PATTERNS;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use collections::HashMap;
|
||||
use std::{ops::Range, sync::LazyLock};
|
||||
use tree_sitter::{Query, QueryMatch};
|
||||
|
||||
use crate::MigrationPatterns;
|
||||
use crate::patterns::KEYMAP_ACTION_STRING_PATTERN;
|
||||
|
||||
pub const KEYMAP_PATTERNS: MigrationPatterns =
|
||||
&[(KEYMAP_ACTION_STRING_PATTERN, replace_string_action)];
|
||||
|
||||
fn replace_string_action(
|
||||
contents: &str,
|
||||
mat: &QueryMatch,
|
||||
query: &Query,
|
||||
) -> Option<(Range<usize>, String)> {
|
||||
let action_name_ix = query.capture_index_for_name("action_name")?;
|
||||
let action_name_node = mat.nodes_for_capture_index(action_name_ix).next()?;
|
||||
let action_name_range = action_name_node.byte_range();
|
||||
let action_name = contents.get(action_name_range.clone())?;
|
||||
|
||||
if let Some(new_action_name) = STRING_REPLACE.get(&action_name) {
|
||||
return Some((action_name_range, new_action_name.to_string()));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
|
||||
HashMap::from_iter([(
|
||||
"editor::AcceptPartialEditPrediction",
|
||||
"editor::AcceptNextWordEditPrediction",
|
||||
)])
|
||||
});
|
||||
@@ -139,6 +139,10 @@ pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
|
||||
migrations::m_2025_04_15::KEYMAP_PATTERNS,
|
||||
&KEYMAP_QUERY_2025_04_15,
|
||||
),
|
||||
MigrationType::TreeSitter(
|
||||
migrations::m_2025_12_08::KEYMAP_PATTERNS,
|
||||
&KEYMAP_QUERY_2025_12_08,
|
||||
),
|
||||
];
|
||||
run_migrations(text, migrations)
|
||||
}
|
||||
@@ -358,6 +362,10 @@ define_query!(
|
||||
SETTINGS_QUERY_2025_11_20,
|
||||
migrations::m_2025_11_20::SETTINGS_PATTERNS
|
||||
);
|
||||
define_query!(
|
||||
KEYMAP_QUERY_2025_12_08,
|
||||
migrations::m_2025_12_08::KEYMAP_PATTERNS
|
||||
);
|
||||
|
||||
// custom query
|
||||
static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
|
||||
|
||||
@@ -58,7 +58,8 @@ In these cases, `alt-tab` is used instead to accept the prediction. When the lan
|
||||
|
||||
On Linux, `alt-tab` is often used by the window manager for switching windows, so `alt-l` is provided as the default binding for accepting predictions. `tab` and `alt-tab` also work, but aren't displayed by default.
|
||||
|
||||
{#action editor::AcceptPartialEditPrediction} ({#kb editor::AcceptPartialEditPrediction}) can be used to accept the current edit prediction up to the next word boundary.
|
||||
{#action editor::AcceptNextWordEditPrediction} ({#kb editor::AcceptNextWordEditPrediction}) can be used to accept the current edit prediction up to the next word boundary.
|
||||
{#action editor::AcceptNextLineEditPrediction} ({#kb editor::AcceptNextLineEditPrediction}) can be used to accept the current edit prediction up to the new line boundary.
|
||||
|
||||
## Configuring Edit Prediction Keybindings {#edit-predictions-keybinding}
|
||||
|
||||
|
||||
Reference in new issue
Block a user