edit prediction: Respect enabled settings when refreshing from diagnostics (#44640)

Release Notes:

- N/A
This commit is contained in:
Agus Zubiaga
2025-12-11 17:39:57 +00:00
committed by GitHub
parent 5a6198cc39
commit 2098b67304
3 changed files with 162 additions and 102 deletions
+72 -13
View File
@@ -283,6 +283,18 @@ impl ProjectState {
})
.detach()
}
fn active_buffer(
&self,
project: &Entity<Project>,
cx: &App,
) -> Option<(Entity<Buffer>, Option<Anchor>)> {
let project = project.read(cx);
let active_path = project.path_for_entry(project.active_entry()?, cx)?;
let active_buffer = project.buffer_store().read(cx).get_by_path(&active_path)?;
let registered_buffer = self.registered_buffers.get(&active_buffer.entity_id())?;
Some((active_buffer, registered_buffer.last_position))
}
}
#[derive(Debug, Clone)]
@@ -373,6 +385,7 @@ impl std::ops::Deref for BufferEditPrediction<'_> {
struct RegisteredBuffer {
snapshot: BufferSnapshot,
last_position: Option<Anchor>,
_subscriptions: [gpui::Subscription; 2],
}
@@ -795,6 +808,7 @@ impl EditPredictionStore {
let project_entity_id = project.entity_id();
entry.insert(RegisteredBuffer {
snapshot,
last_position: None,
_subscriptions: [
cx.subscribe(buffer, {
let project = project.downgrade();
@@ -882,13 +896,21 @@ impl EditPredictionStore {
});
}
fn current_prediction_for_buffer(
&self,
fn prediction_at(
&mut self,
buffer: &Entity<Buffer>,
position: Option<language::Anchor>,
project: &Entity<Project>,
cx: &App,
) -> Option<BufferEditPrediction<'_>> {
let project_state = self.projects.get(&project.entity_id())?;
let project_state = self.projects.get_mut(&project.entity_id())?;
if let Some(position) = position
&& let Some(buffer) = project_state
.registered_buffers
.get_mut(&buffer.entity_id())
{
buffer.last_position = Some(position);
}
let CurrentEditPrediction {
requested_by,
@@ -1131,12 +1153,21 @@ impl EditPredictionStore {
};
self.queue_prediction_refresh(project.clone(), project.entity_id(), cx, move |this, cx| {
let Some(open_buffer_task) = project
.update(cx, |project, cx| {
project
.active_entry()
.and_then(|entry| project.path_for_entry(entry, cx))
.map(|path| project.open_buffer(path, cx))
let Some((active_buffer, snapshot, cursor_point)) = this
.read_with(cx, |this, cx| {
let project_state = this.projects.get(&project.entity_id())?;
let (buffer, position) = project_state.active_buffer(&project, cx)?;
let snapshot = buffer.read(cx).snapshot();
if !Self::predictions_enabled_at(&snapshot, position, cx) {
return None;
}
let cursor_point = position
.map(|pos| pos.to_point(&snapshot))
.unwrap_or_default();
Some((buffer, snapshot, cursor_point))
})
.log_err()
.flatten()
@@ -1145,14 +1176,11 @@ impl EditPredictionStore {
};
cx.spawn(async move |cx| {
let active_buffer = open_buffer_task.await?;
let snapshot = active_buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let Some((jump_buffer, jump_position)) = Self::next_diagnostic_location(
active_buffer,
&snapshot,
Default::default(),
Default::default(),
cursor_point,
&project,
cx,
)
@@ -1197,6 +1225,37 @@ impl EditPredictionStore {
});
}
fn predictions_enabled_at(
snapshot: &BufferSnapshot,
position: Option<language::Anchor>,
cx: &App,
) -> bool {
let file = snapshot.file();
let all_settings = all_language_settings(file, cx);
if !all_settings.show_edit_predictions(snapshot.language(), cx)
|| file.is_some_and(|file| !all_settings.edit_predictions_enabled_for_file(file, cx))
{
return false;
}
if let Some(last_position) = position {
let settings = snapshot.settings_at(last_position, cx);
if !settings.edit_predictions_disabled_in.is_empty()
&& let Some(scope) = snapshot.language_scope_at(last_position)
&& let Some(scope_name) = scope.override_name()
&& settings
.edit_predictions_disabled_in
.iter()
.any(|s| s == scope_name)
{
return false;
}
}
true
}
#[cfg(not(test))]
pub const THROTTLE_TIMEOUT: Duration = Duration::from_millis(300);
#[cfg(test)]
@@ -45,10 +45,6 @@ async fn test_current_state(cx: &mut TestAppContext) {
.await;
let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
ep_store.update(cx, |ep_store, cx| {
ep_store.register_project(&project, cx);
});
let buffer1 = project
.update(cx, |project, cx| {
let path = project.find_project_path(path!("/root/1.txt"), cx).unwrap();
@@ -60,6 +56,11 @@ async fn test_current_state(cx: &mut TestAppContext) {
let snapshot1 = buffer1.read_with(cx, |buffer, _cx| buffer.snapshot());
let position = snapshot1.anchor_before(language::Point::new(1, 3));
ep_store.update(cx, |ep_store, cx| {
ep_store.register_project(&project, cx);
ep_store.register_buffer(&buffer1, &project, cx);
});
// Prediction for current file
ep_store.update(cx, |ep_store, cx| {
@@ -84,9 +85,9 @@ async fn test_current_state(cx: &mut TestAppContext) {
cx.run_until_parked();
ep_store.read_with(cx, |ep_store, cx| {
ep_store.update(cx, |ep_store, cx| {
let prediction = ep_store
.current_prediction_for_buffer(&buffer1, &project, cx)
.prediction_at(&buffer1, None, &project, cx)
.unwrap();
assert_matches!(prediction, BufferEditPrediction::Local { .. });
});
@@ -140,9 +141,9 @@ async fn test_current_state(cx: &mut TestAppContext) {
.unwrap();
cx.run_until_parked();
ep_store.read_with(cx, |ep_store, cx| {
ep_store.update(cx, |ep_store, cx| {
let prediction = ep_store
.current_prediction_for_buffer(&buffer1, &project, cx)
.prediction_at(&buffer1, None, &project, cx)
.unwrap();
assert_matches!(
prediction,
@@ -158,9 +159,9 @@ async fn test_current_state(cx: &mut TestAppContext) {
.await
.unwrap();
ep_store.read_with(cx, |ep_store, cx| {
ep_store.update(cx, |ep_store, cx| {
let prediction = ep_store
.current_prediction_for_buffer(&buffer2, &project, cx)
.prediction_at(&buffer2, None, &project, cx)
.unwrap();
assert_matches!(prediction, BufferEditPrediction::Local { .. });
});
@@ -344,10 +345,10 @@ async fn test_empty_prediction(cx: &mut TestAppContext) {
cx.run_until_parked();
ep_store.read_with(cx, |ep_store, cx| {
ep_store.update(cx, |ep_store, cx| {
assert!(
ep_store
.current_prediction_for_buffer(&buffer, &project, cx)
.prediction_at(&buffer, None, &project, cx)
.is_none()
);
});
@@ -404,10 +405,10 @@ async fn test_interpolated_empty(cx: &mut TestAppContext) {
cx.run_until_parked();
ep_store.read_with(cx, |ep_store, cx| {
ep_store.update(cx, |ep_store, cx| {
assert!(
ep_store
.current_prediction_for_buffer(&buffer, &project, cx)
.prediction_at(&buffer, None, &project, cx)
.is_none()
);
});
@@ -469,10 +470,10 @@ async fn test_replace_current(cx: &mut TestAppContext) {
cx.run_until_parked();
ep_store.read_with(cx, |ep_store, cx| {
ep_store.update(cx, |ep_store, cx| {
assert_eq!(
ep_store
.current_prediction_for_buffer(&buffer, &project, cx)
.prediction_at(&buffer, None, &project, cx)
.unwrap()
.id
.0,
@@ -492,11 +493,11 @@ async fn test_replace_current(cx: &mut TestAppContext) {
cx.run_until_parked();
ep_store.read_with(cx, |ep_store, cx| {
ep_store.update(cx, |ep_store, cx| {
// second replaces first
assert_eq!(
ep_store
.current_prediction_for_buffer(&buffer, &project, cx)
.prediction_at(&buffer, None, &project, cx)
.unwrap()
.id
.0,
@@ -551,10 +552,10 @@ async fn test_current_preferred(cx: &mut TestAppContext) {
cx.run_until_parked();
ep_store.read_with(cx, |ep_store, cx| {
ep_store.update(cx, |ep_store, cx| {
assert_eq!(
ep_store
.current_prediction_for_buffer(&buffer, &project, cx)
.prediction_at(&buffer, None, &project, cx)
.unwrap()
.id
.0,
@@ -586,11 +587,11 @@ async fn test_current_preferred(cx: &mut TestAppContext) {
cx.run_until_parked();
ep_store.read_with(cx, |ep_store, cx| {
ep_store.update(cx, |ep_store, cx| {
// first is preferred over second
assert_eq!(
ep_store
.current_prediction_for_buffer(&buffer, &project, cx)
.prediction_at(&buffer, None, &project, cx)
.unwrap()
.id
.0,
@@ -657,11 +658,11 @@ async fn test_cancel_earlier_pending_requests(cx: &mut TestAppContext) {
cx.run_until_parked();
ep_store.read_with(cx, |ep_store, cx| {
ep_store.update(cx, |ep_store, cx| {
// current prediction is second
assert_eq!(
ep_store
.current_prediction_for_buffer(&buffer, &project, cx)
.prediction_at(&buffer, None, &project, cx)
.unwrap()
.id
.0,
@@ -675,11 +676,11 @@ async fn test_cancel_earlier_pending_requests(cx: &mut TestAppContext) {
cx.run_until_parked();
ep_store.read_with(cx, |ep_store, cx| {
ep_store.update(cx, |ep_store, cx| {
// current prediction is still second, since first was cancelled
assert_eq!(
ep_store
.current_prediction_for_buffer(&buffer, &project, cx)
.prediction_at(&buffer, None, &project, cx)
.unwrap()
.id
.0,
@@ -768,11 +769,11 @@ async fn test_cancel_second_on_third_request(cx: &mut TestAppContext) {
cx.run_until_parked();
ep_store.read_with(cx, |ep_store, cx| {
ep_store.update(cx, |ep_store, cx| {
// current prediction is first
assert_eq!(
ep_store
.current_prediction_for_buffer(&buffer, &project, cx)
.prediction_at(&buffer, None, &project, cx)
.unwrap()
.id
.0,
@@ -786,11 +787,11 @@ async fn test_cancel_second_on_third_request(cx: &mut TestAppContext) {
cx.run_until_parked();
ep_store.read_with(cx, |ep_store, cx| {
ep_store.update(cx, |ep_store, cx| {
// current prediction is still first, since second was cancelled
assert_eq!(
ep_store
.current_prediction_for_buffer(&buffer, &project, cx)
.prediction_at(&buffer, None, &project, cx)
.unwrap()
.id
.0,
@@ -804,11 +805,11 @@ async fn test_cancel_second_on_third_request(cx: &mut TestAppContext) {
cx.run_until_parked();
ep_store.read_with(cx, |ep_store, cx| {
ep_store.update(cx, |ep_store, cx| {
// third completes and replaces first
assert_eq!(
ep_store
.current_prediction_for_buffer(&buffer, &project, cx)
.prediction_at(&buffer, None, &project, cx)
.unwrap()
.id
.0,
@@ -125,14 +125,15 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
return;
}
if let Some(current) = store.current_prediction_for_buffer(&buffer, &self.project, cx)
&& let BufferEditPrediction::Local { prediction } = current
&& prediction.interpolate(buffer.read(cx)).is_some()
{
return;
}
self.store.update(cx, |store, cx| {
if let Some(current) =
store.prediction_at(&buffer, Some(cursor_position), &self.project, cx)
&& let BufferEditPrediction::Local { prediction } = current
&& prediction.interpolate(buffer.read(cx)).is_some()
{
return;
}
store.refresh_context(&self.project, &buffer, cursor_position, cx);
store.refresh_prediction_from_buffer(self.project.clone(), buffer, cursor_position, cx)
});
@@ -171,69 +172,68 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
cursor_position: language::Anchor,
cx: &mut Context<Self>,
) -> Option<edit_prediction_types::EditPrediction> {
let prediction =
self.store
.read(cx)
.current_prediction_for_buffer(buffer, &self.project, cx)?;
self.store.update(cx, |store, cx| {
let prediction =
store.prediction_at(buffer, Some(cursor_position), &self.project, cx)?;
let prediction = match prediction {
BufferEditPrediction::Local { prediction } => prediction,
BufferEditPrediction::Jump { prediction } => {
return Some(edit_prediction_types::EditPrediction::Jump {
id: Some(prediction.id.to_string().into()),
snapshot: prediction.snapshot.clone(),
target: prediction.edits.first().unwrap().0.start,
});
}
};
let prediction = match prediction {
BufferEditPrediction::Local { prediction } => prediction,
BufferEditPrediction::Jump { prediction } => {
return Some(edit_prediction_types::EditPrediction::Jump {
id: Some(prediction.id.to_string().into()),
snapshot: prediction.snapshot.clone(),
target: prediction.edits.first().unwrap().0.start,
});
}
};
let buffer = buffer.read(cx);
let snapshot = buffer.snapshot();
let buffer = buffer.read(cx);
let snapshot = buffer.snapshot();
let Some(edits) = prediction.interpolate(&snapshot) else {
self.store.update(cx, |store, _cx| {
let Some(edits) = prediction.interpolate(&snapshot) else {
store.reject_current_prediction(
EditPredictionRejectReason::InterpolatedEmpty,
&self.project,
);
});
return None;
};
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, _))| {
let distance_from_start = cursor_row.abs_diff(range.start.to_point(&snapshot).row);
let distance_from_end = cursor_row.abs_diff(range.end.to_point(&snapshot).row);
cmp::min(distance_from_start, distance_from_end)
})?;
let cursor_row = cursor_position.to_point(&snapshot).row;
let (closest_edit_ix, (closest_edit_range, _)) =
edits.iter().enumerate().min_by_key(|(_, (range, _))| {
let distance_from_start =
cursor_row.abs_diff(range.start.to_point(&snapshot).row);
let distance_from_end = cursor_row.abs_diff(range.end.to_point(&snapshot).row);
cmp::min(distance_from_start, distance_from_end)
})?;
let mut edit_start_ix = closest_edit_ix;
for (range, _) in edits[..edit_start_ix].iter().rev() {
let distance_from_closest_edit = closest_edit_range.start.to_point(&snapshot).row
- range.end.to_point(&snapshot).row;
if distance_from_closest_edit <= 1 {
edit_start_ix -= 1;
} else {
break;
let mut edit_start_ix = closest_edit_ix;
for (range, _) in edits[..edit_start_ix].iter().rev() {
let distance_from_closest_edit = closest_edit_range.start.to_point(&snapshot).row
- range.end.to_point(&snapshot).row;
if distance_from_closest_edit <= 1 {
edit_start_ix -= 1;
} else {
break;
}
}
}
let mut edit_end_ix = closest_edit_ix + 1;
for (range, _) in &edits[edit_end_ix..] {
let distance_from_closest_edit =
range.start.to_point(buffer).row - closest_edit_range.end.to_point(&snapshot).row;
if distance_from_closest_edit <= 1 {
edit_end_ix += 1;
} else {
break;
let mut edit_end_ix = closest_edit_ix + 1;
for (range, _) in &edits[edit_end_ix..] {
let distance_from_closest_edit = range.start.to_point(buffer).row
- closest_edit_range.end.to_point(&snapshot).row;
if distance_from_closest_edit <= 1 {
edit_end_ix += 1;
} else {
break;
}
}
}
Some(edit_prediction_types::EditPrediction::Local {
id: Some(prediction.id.to_string().into()),
edits: edits[edit_start_ix..edit_end_ix].to_vec(),
edit_preview: Some(prediction.edit_preview.clone()),
Some(edit_prediction_types::EditPrediction::Local {
id: Some(prediction.id.to_string().into()),
edits: edits[edit_start_ix..edit_end_ix].to_vec(),
edit_preview: Some(prediction.edit_preview.clone()),
})
})
}
}