zeta2: Allow provider to suggest edits in different files (#39110)
Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga <agus@zed.dev>
This commit is contained in:
co-authored by
Agus Zubiaga
parent
b7f9fd7d74
commit
cda48a3a1c
@@ -2,7 +2,6 @@ use edit_prediction::EditPredictionProvider;
|
||||
use gpui::{Entity, prelude::*};
|
||||
use indoc::indoc;
|
||||
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
|
||||
use project::Project;
|
||||
use std::ops::Range;
|
||||
use text::{Point, ToOffset};
|
||||
|
||||
@@ -261,7 +260,7 @@ async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui:
|
||||
EditPrediction::Edit { .. } => {
|
||||
// This is expected for non-Zed providers
|
||||
}
|
||||
EditPrediction::Move { .. } => {
|
||||
EditPrediction::MoveWithin { .. } | EditPrediction::MoveOutside { .. } => {
|
||||
panic!(
|
||||
"Non-Zed providers should not show Move predictions (jump functionality)"
|
||||
);
|
||||
@@ -299,7 +298,7 @@ fn assert_editor_active_move_completion(
|
||||
.as_ref()
|
||||
.expect("editor has no active completion");
|
||||
|
||||
if let EditPrediction::Move { target, .. } = &completion_state.completion {
|
||||
if let EditPrediction::MoveWithin { target, .. } = &completion_state.completion {
|
||||
assert(editor.buffer().read(cx).snapshot(cx), *target);
|
||||
} else {
|
||||
panic!("expected move completion");
|
||||
@@ -326,7 +325,7 @@ fn propose_edits<T: ToOffset>(
|
||||
|
||||
cx.update(|_, cx| {
|
||||
provider.update(cx, |provider, _| {
|
||||
provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
|
||||
provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
|
||||
id: None,
|
||||
edits: edits.collect(),
|
||||
edit_preview: None,
|
||||
@@ -357,7 +356,7 @@ fn propose_edits_non_zed<T: ToOffset>(
|
||||
|
||||
cx.update(|_, cx| {
|
||||
provider.update(cx, |provider, _| {
|
||||
provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
|
||||
provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
|
||||
id: None,
|
||||
edits: edits.collect(),
|
||||
edit_preview: None,
|
||||
@@ -418,7 +417,6 @@ impl EditPredictionProvider for FakeEditPredictionProvider {
|
||||
|
||||
fn refresh(
|
||||
&mut self,
|
||||
_project: Option<Entity<Project>>,
|
||||
_buffer: gpui::Entity<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_debounce: bool,
|
||||
@@ -492,7 +490,6 @@ impl EditPredictionProvider for FakeNonZedEditPredictionProvider {
|
||||
|
||||
fn refresh(
|
||||
&mut self,
|
||||
_project: Option<Entity<Project>>,
|
||||
_buffer: gpui::Entity<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_debounce: bool,
|
||||
|
||||
+152
-39
@@ -638,17 +638,23 @@ enum EditPrediction {
|
||||
display_mode: EditDisplayMode,
|
||||
snapshot: BufferSnapshot,
|
||||
},
|
||||
Move {
|
||||
/// Move to a specific location in the active editor
|
||||
MoveWithin {
|
||||
target: Anchor,
|
||||
snapshot: BufferSnapshot,
|
||||
},
|
||||
/// Move to a specific location in a different editor (not the active one)
|
||||
MoveOutside {
|
||||
target: language::Anchor,
|
||||
snapshot: BufferSnapshot,
|
||||
},
|
||||
}
|
||||
|
||||
struct EditPredictionState {
|
||||
inlay_ids: Vec<InlayId>,
|
||||
completion: EditPrediction,
|
||||
completion_id: Option<SharedString>,
|
||||
invalidation_range: Range<Anchor>,
|
||||
invalidation_range: Option<Range<Anchor>>,
|
||||
}
|
||||
|
||||
enum EditPredictionSettings {
|
||||
@@ -7175,13 +7181,7 @@ impl Editor {
|
||||
return None;
|
||||
}
|
||||
|
||||
provider.refresh(
|
||||
self.project.clone(),
|
||||
buffer,
|
||||
cursor_buffer_position,
|
||||
debounce,
|
||||
cx,
|
||||
);
|
||||
provider.refresh(buffer, cursor_buffer_position, debounce, cx);
|
||||
Some(())
|
||||
}
|
||||
|
||||
@@ -7424,10 +7424,8 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
|
||||
self.report_edit_prediction_event(active_edit_prediction.completion_id.clone(), true, cx);
|
||||
|
||||
match &active_edit_prediction.completion {
|
||||
EditPrediction::Move { target, .. } => {
|
||||
EditPrediction::MoveWithin { target, .. } => {
|
||||
let target = *target;
|
||||
|
||||
if let Some(position_map) = &self.last_position_map {
|
||||
@@ -7469,7 +7467,19 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
);
|
||||
|
||||
if let Some(provider) = self.edit_prediction_provider() {
|
||||
provider.accept(cx);
|
||||
}
|
||||
@@ -7522,10 +7532,8 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.report_edit_prediction_event(active_edit_prediction.completion_id.clone(), true, cx);
|
||||
|
||||
match &active_edit_prediction.completion {
|
||||
EditPrediction::Move { target, .. } => {
|
||||
EditPrediction::MoveWithin { target, .. } => {
|
||||
let target = *target;
|
||||
self.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::newest()),
|
||||
@@ -7536,7 +7544,19 @@ impl Editor {
|
||||
},
|
||||
);
|
||||
}
|
||||
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,
|
||||
);
|
||||
|
||||
// Find an insertion that starts at the cursor position.
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let cursor_offset = self.selections.newest::<usize>(cx).head();
|
||||
@@ -7631,6 +7651,36 @@ impl Editor {
|
||||
);
|
||||
}
|
||||
|
||||
fn open_editor_at_anchor(
|
||||
snapshot: &language::BufferSnapshot,
|
||||
target: language::Anchor,
|
||||
workspace: &Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let path = snapshot.file().map(|file| file.full_path(cx));
|
||||
let Some(path) =
|
||||
path.and_then(|path| workspace.project().read(cx).find_project_path(path, cx))
|
||||
else {
|
||||
return Task::ready(Err(anyhow::anyhow!("Project path not found")));
|
||||
};
|
||||
let target = text::ToPoint::to_point(&target, snapshot);
|
||||
let item = workspace.open_path(path, None, true, window, cx);
|
||||
window.spawn(cx, async move |cx| {
|
||||
let Some(editor) = item.await?.downcast::<Editor>() else {
|
||||
return Ok(());
|
||||
};
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.go_to_singleton_buffer_point(target, window, cx);
|
||||
})
|
||||
.ok();
|
||||
anyhow::Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_active_edit_prediction(&self) -> bool {
|
||||
self.active_edit_prediction.is_some()
|
||||
}
|
||||
@@ -7846,7 +7896,10 @@ impl Editor {
|
||||
.active_edit_prediction
|
||||
.as_ref()
|
||||
.is_some_and(|completion| {
|
||||
let invalidation_range = completion.invalidation_range.to_offset(&multibuffer);
|
||||
let Some(invalidation_range) = completion.invalidation_range.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
let invalidation_range = invalidation_range.to_offset(&multibuffer);
|
||||
let invalidation_range = invalidation_range.start..=invalidation_range.end;
|
||||
!invalidation_range.contains(&offset_selection.head())
|
||||
})
|
||||
@@ -7882,8 +7935,31 @@ impl Editor {
|
||||
}
|
||||
|
||||
let edit_prediction = provider.suggest(&buffer, cursor_buffer_position, cx)?;
|
||||
let edits = edit_prediction
|
||||
.edits
|
||||
|
||||
let (completion_id, edits, edit_preview) = match edit_prediction {
|
||||
edit_prediction::EditPrediction::Local {
|
||||
id,
|
||||
edits,
|
||||
edit_preview,
|
||||
} => (id, edits, edit_preview),
|
||||
edit_prediction::EditPrediction::Jump {
|
||||
id,
|
||||
snapshot,
|
||||
target,
|
||||
} => {
|
||||
self.stale_edit_prediction_in_menu = None;
|
||||
self.active_edit_prediction = Some(EditPredictionState {
|
||||
inlay_ids: vec![],
|
||||
completion: EditPrediction::MoveOutside { snapshot, target },
|
||||
completion_id: id,
|
||||
invalidation_range: None,
|
||||
});
|
||||
cx.notify();
|
||||
return Some(());
|
||||
}
|
||||
};
|
||||
|
||||
let edits = edits
|
||||
.into_iter()
|
||||
.flat_map(|(range, new_text)| {
|
||||
let start = multibuffer.anchor_in_excerpt(excerpt_id, range.start)?;
|
||||
@@ -7928,7 +8004,7 @@ impl Editor {
|
||||
invalidation_row_range =
|
||||
move_invalidation_row_range.unwrap_or(edit_start_row..edit_end_row);
|
||||
let target = first_edit_start;
|
||||
EditPrediction::Move { target, snapshot }
|
||||
EditPrediction::MoveWithin { target, snapshot }
|
||||
} else {
|
||||
let show_completions_in_buffer = !self.edit_prediction_visible_in_cursor_popover(true)
|
||||
&& !self.edit_predictions_hidden_for_vim_mode;
|
||||
@@ -7977,7 +8053,7 @@ impl Editor {
|
||||
|
||||
EditPrediction::Edit {
|
||||
edits,
|
||||
edit_preview: edit_prediction.edit_preview,
|
||||
edit_preview,
|
||||
display_mode,
|
||||
snapshot,
|
||||
}
|
||||
@@ -7994,8 +8070,8 @@ impl Editor {
|
||||
self.active_edit_prediction = Some(EditPredictionState {
|
||||
inlay_ids,
|
||||
completion,
|
||||
completion_id: edit_prediction.id,
|
||||
invalidation_range,
|
||||
completion_id,
|
||||
invalidation_range: Some(invalidation_range),
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
@@ -8581,7 +8657,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
match &active_edit_prediction.completion {
|
||||
EditPrediction::Move { target, .. } => {
|
||||
EditPrediction::MoveWithin { target, .. } => {
|
||||
let target_display_point = target.to_display_point(editor_snapshot);
|
||||
|
||||
if self.edit_prediction_requires_modifier() {
|
||||
@@ -8666,6 +8742,28 @@ impl Editor {
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
EditPrediction::MoveOutside { snapshot, .. } => {
|
||||
let file_name = snapshot
|
||||
.file()
|
||||
.map(|file| file.file_name(cx))
|
||||
.unwrap_or("untitled");
|
||||
let mut element = self
|
||||
.render_edit_prediction_line_popover(
|
||||
format!("Jump to {file_name}"),
|
||||
Some(IconName::ZedPredict),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
let origin_x = text_bounds.size.width / 2. - size.width / 2.;
|
||||
let origin_y = text_bounds.size.height - size.height - px(30.);
|
||||
let origin = text_bounds.origin + gpui::Point::new(origin_x, origin_y);
|
||||
element.prepaint_at(origin, window, cx);
|
||||
|
||||
Some((element, origin))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8730,13 +8828,13 @@ impl Editor {
|
||||
.items_end()
|
||||
.when(flag_on_right, |el| el.items_start())
|
||||
.child(if flag_on_right {
|
||||
self.render_edit_prediction_line_popover("Jump", None, window, cx)?
|
||||
self.render_edit_prediction_line_popover("Jump", None, window, cx)
|
||||
.rounded_bl(px(0.))
|
||||
.rounded_tl(px(0.))
|
||||
.border_l_2()
|
||||
.border_color(border_color)
|
||||
} else {
|
||||
self.render_edit_prediction_line_popover("Jump", None, window, cx)?
|
||||
self.render_edit_prediction_line_popover("Jump", None, window, cx)
|
||||
.rounded_br(px(0.))
|
||||
.rounded_tr(px(0.))
|
||||
.border_r_2()
|
||||
@@ -8776,7 +8874,7 @@ impl Editor {
|
||||
cx: &mut App,
|
||||
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
|
||||
let mut element = self
|
||||
.render_edit_prediction_line_popover("Scroll", Some(scroll_icon), window, cx)?
|
||||
.render_edit_prediction_line_popover("Scroll", Some(scroll_icon), window, cx)
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
@@ -8816,7 +8914,7 @@ impl Editor {
|
||||
Some(IconName::ArrowUp),
|
||||
window,
|
||||
cx,
|
||||
)?
|
||||
)
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
@@ -8835,7 +8933,7 @@ impl Editor {
|
||||
Some(IconName::ArrowDown),
|
||||
window,
|
||||
cx,
|
||||
)?
|
||||
)
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
@@ -8882,7 +8980,7 @@ impl Editor {
|
||||
);
|
||||
|
||||
let mut element = self
|
||||
.render_edit_prediction_line_popover(label, None, window, cx)?
|
||||
.render_edit_prediction_line_popover(label, None, window, cx)
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
@@ -8909,7 +9007,7 @@ impl Editor {
|
||||
};
|
||||
|
||||
element = self
|
||||
.render_edit_prediction_line_popover(label, Some(icon), window, cx)?
|
||||
.render_edit_prediction_line_popover(label, Some(icon), window, cx)
|
||||
.into_any();
|
||||
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
@@ -9163,13 +9261,13 @@ impl Editor {
|
||||
icon: Option<IconName>,
|
||||
window: &mut Window,
|
||||
cx: &App,
|
||||
) -> Option<Stateful<Div>> {
|
||||
) -> 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 has_keybind = keybind.is_some();
|
||||
|
||||
let result = h_flex()
|
||||
h_flex()
|
||||
.id("ep-line-popover")
|
||||
.py_0p5()
|
||||
.pl_1()
|
||||
@@ -9215,9 +9313,7 @@ impl Editor {
|
||||
.mt(px(1.5))
|
||||
.child(Icon::new(icon).size(IconSize::Small)),
|
||||
)
|
||||
});
|
||||
|
||||
Some(result)
|
||||
})
|
||||
}
|
||||
|
||||
fn edit_prediction_line_popover_bg_color(cx: &App) -> Hsla {
|
||||
@@ -9281,7 +9377,7 @@ impl Editor {
|
||||
.rounded_tl(px(0.))
|
||||
.overflow_hidden()
|
||||
.child(div().px_1p5().child(match &prediction.completion {
|
||||
EditPrediction::Move { target, snapshot } => {
|
||||
EditPrediction::MoveWithin { target, snapshot } => {
|
||||
use text::ToPoint as _;
|
||||
if target.text_anchor.to_point(snapshot).row > cursor_point.row
|
||||
{
|
||||
@@ -9290,6 +9386,10 @@ impl Editor {
|
||||
Icon::new(IconName::ZedPredictUp)
|
||||
}
|
||||
}
|
||||
EditPrediction::MoveOutside { .. } => {
|
||||
// TODO [zeta2] custom icon for external jump?
|
||||
Icon::new(provider_icon)
|
||||
}
|
||||
EditPrediction::Edit { .. } => Icon::new(provider_icon),
|
||||
}))
|
||||
.child(
|
||||
@@ -9472,7 +9572,7 @@ impl Editor {
|
||||
.unwrap_or(true);
|
||||
|
||||
match &completion.completion {
|
||||
EditPrediction::Move {
|
||||
EditPrediction::MoveWithin {
|
||||
target, snapshot, ..
|
||||
} => {
|
||||
if !supports_jump {
|
||||
@@ -9494,7 +9594,20 @@ impl Editor {
|
||||
.child(Label::new("Jump to Edit")),
|
||||
)
|
||||
}
|
||||
|
||||
EditPrediction::MoveOutside { snapshot, .. } => {
|
||||
let file_name = snapshot
|
||||
.file()
|
||||
.map(|file| file.file_name(cx))
|
||||
.unwrap_or("untitled");
|
||||
Some(
|
||||
h_flex()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.child(Icon::new(IconName::ZedPredict))
|
||||
.child(Label::new(format!("Jump to {file_name}"))),
|
||||
)
|
||||
}
|
||||
EditPrediction::Edit {
|
||||
edits,
|
||||
edit_preview,
|
||||
@@ -21418,7 +21531,7 @@ impl Editor {
|
||||
{
|
||||
self.hide_context_menu(window, cx);
|
||||
}
|
||||
self.discard_edit_prediction(false, cx);
|
||||
self.take_active_edit_prediction(cx);
|
||||
cx.emit(EditorEvent::Blurred);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -8272,7 +8272,7 @@ async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext)
|
||||
|
||||
cx.update(|_, cx| {
|
||||
provider.update(cx, |provider, _| {
|
||||
provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
|
||||
provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
|
||||
id: None,
|
||||
edits: vec![(edit_position..edit_position, "X".into())],
|
||||
edit_preview: None,
|
||||
|
||||
Reference in New Issue
Block a user