Split interpolate failure rejection reason (#60499)

## Summary

- Added `InterpolateFailed` as a distinct edit prediction rejection
reason.
- Kept `InterpolatedEmpty` for successful interpolation that leaves no
edits.
- Updated interpolation callers to classify failed interpolation
separately from empty edit vectors.

Release Notes:

- N/A
This commit is contained in:
Ben Kunkle authored and GitHub committed 2026-07-06 22:12:33 +00:00
1 parent fadf7e6164
commit 8d11484a35
6 files changed
+112 -24

No files matched your search

@@ -196,6 +196,8 @@ pub enum EditPredictionRejectReason {
Empty,
/// Edits returned, but none remained after interpolation
InterpolatedEmpty,
/// Edits returned, but could not be interpolated after buffer changes
InterpolateFailed,
/// The new prediction was preferred over the current one
Replaced,
/// The current prediction was preferred over the new one
+2 -1
View File
@@ -82,9 +82,10 @@ struct CurrentCompletion {
impl CurrentCompletion {
/// Attempts to adjust the edits based on changes made to the buffer since the completion was generated.
/// Returns None if the user's edits conflict with the predicted edits.
/// Returns None if no predicted edits remain or the user's edits conflict with the predicted edits.
fn interpolate(&self, new_snapshot: &BufferSnapshot) -> Option<Vec<(Range<Anchor>, Arc<str>)>> {
edit_prediction_types::interpolate_edits(&self.snapshot, new_snapshot, &self.edits)
.filter(|edits| !edits.is_empty())
}
}
@@ -1389,7 +1389,7 @@ async fn test_interpolated_empty(cx: &mut TestAppContext) {
let (request, respond_tx) = requests.predict.next().await.unwrap();
buffer.update(cx, |buffer, cx| {
buffer.set_text("Hello!\nHow are you?\nBye", cx);
buffer.edit([(10..10, " are you?")], None, cx);
});
let mut response = model_response(&request, SIMPLE_DIFF);
@@ -1412,7 +1412,6 @@ async fn test_interpolated_empty(cx: &mut TestAppContext) {
assert!(shown_predictions[0].editable_range.is_some());
});
// prediction is reported as rejected
let (reject_request, _) = requests.reject.next().await.unwrap();
assert_eq!(
@@ -1427,6 +1426,75 @@ async fn test_interpolated_empty(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn test_interpolate_failed(cx: &mut TestAppContext) {
let (ep_store, mut requests) = init_test_with_fake_client(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"foo.md": "Hello!\nHow\nBye\n"
}),
)
.await;
let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
let buffer = project
.update(cx, |project, cx| {
let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
project.open_buffer(path, cx)
})
.await
.unwrap();
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let position = snapshot.anchor_before(language::Point::new(1, 3));
ep_store.update(cx, |ep_store, cx| {
ep_store.refresh_prediction_from_buffer(
project.clone(),
buffer.clone(),
position,
EditPredictionRequestTrigger::Other,
cx,
);
});
let (request, respond_tx) = requests.predict.next().await.unwrap();
buffer.update(cx, |buffer, cx| {
buffer.edit([(10..10, " is it?")], None, cx);
});
let mut response = model_response(&request, SIMPLE_DIFF);
response.model_version = Some("zeta2:test-interpolate-failed".to_string());
let id = response.request_id.clone();
respond_tx.send(response).unwrap();
cx.run_until_parked();
ep_store.update(cx, |ep_store, cx| {
assert!(
ep_store
.prediction_at(&buffer, None, &project, cx)
.is_none()
);
assert!(ep_store.rateable_predictions().next().is_none());
});
let (reject_request, _) = requests.reject.next().await.unwrap();
assert_eq!(
&reject_request.rejections,
&[EditPredictionRejection {
request_id: id,
reason: EditPredictionRejectReason::InterpolateFailed,
was_shown: false,
model_version: Some("zeta2:test-interpolate-failed".to_string()),
e2e_latency_ms: Some(0),
}]
);
}
const SIMPLE_DIFF: &str = indoc! { r"
--- a/root/foo.md
+++ b/root/foo.md
+27 -19
View File
@@ -49,27 +49,35 @@ impl EditPredictionResult {
e2e_latency: std::time::Duration,
cx: &mut AsyncApp,
) -> Self {
let (edits, new_snapshot) = (!edits.is_empty())
.then(|| {
edited_buffer.read_with(cx, |buffer, _cx| {
let new_snapshot = buffer.snapshot();
let edits: Arc<[(Range<Anchor>, Arc<str>)]> =
interpolate_edits(&edited_buffer_snapshot, &new_snapshot, &edits)
.map(Arc::from)
.unwrap_or_default();
let snapshot = (!edits.is_empty()).then_some(new_snapshot);
(Some(edits), snapshot)
})
let (edits, reject_reason, new_snapshot): (
Arc<[(Range<Anchor>, Arc<str>)]>,
Option<EditPredictionRejectReason>,
Option<BufferSnapshot>,
) = if edits.is_empty() {
(
Arc::default(),
Some(EditPredictionRejectReason::Empty),
None,
)
} else {
edited_buffer.read_with(cx, |buffer, _cx| {
let new_snapshot = buffer.snapshot();
match interpolate_edits(&edited_buffer_snapshot, &new_snapshot, &edits) {
Some(edits) if edits.is_empty() => (
Arc::default(),
Some(EditPredictionRejectReason::InterpolatedEmpty),
None,
),
Some(edits) => (Arc::from(edits), None, Some(new_snapshot)),
None => (
Arc::default(),
Some(EditPredictionRejectReason::InterpolateFailed),
None,
),
}
})
.unwrap_or_default();
let snapshot = new_snapshot.unwrap_or_else(|| edited_buffer_snapshot.clone());
let reject_reason = match edits.as_ref() {
None => Some(EditPredictionRejectReason::Empty),
Some(edits) if edits.is_empty() => Some(EditPredictionRejectReason::InterpolatedEmpty),
Some(_) => None,
};
let edits = edits.unwrap_or_default();
let snapshot = new_snapshot.unwrap_or_else(|| edited_buffer_snapshot.clone());
let edit_preview = if !edits.is_empty() {
edited_buffer
@@ -222,13 +222,22 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
let Some(edits) = prediction.interpolate(&snapshot) else {
store.reject_current_prediction(
EditPredictionRejectReason::InterpolatedEmpty,
EditPredictionRejectReason::InterpolateFailed,
&self.project,
cx,
);
return None;
};
if edits.is_empty() {
store.reject_current_prediction(
EditPredictionRejectReason::InterpolatedEmpty,
&self.project,
cx,
);
return None;
}
let cursor_row = cursor_position.to_point(&snapshot).row;
let (closest_edit_ix, (closest_edit_range, _)) =
edits.iter().enumerate().min_by_key(|(_, (range, _))| {
@@ -392,5 +392,5 @@ pub fn interpolate_edits(
edits.extend(model_edits.cloned());
if edits.is_empty() { None } else { Some(edits) }
Some(edits)
}