buffer_diff: Ensure that base text has finished parsing before completing update (#46054)

Follow-up to #46001 

That initial fix partly addressed the issue for diffs managed by the
`GitStore`, but not for other diffs (e.g. those managed by the
`ActionLog` or `CommitView`). The underlying issue is that we switched
to using a `Buffer` to represent the diff base text, and when updating
the diff we were calling `set_text` on this buffer and not waiting for
reparsing to finish. When the base text was represented by a series of
independent `BufferSnapshot`s, this wasn't an issue because we would
parse the base text in the background as part of computing the diff
update. This PR fixes the issue by waiting on reparsing to finish after
each call to `set_text`.

Release Notes:

- N/A
This commit is contained in:
Cole Miller 2026-01-04 19:56:58 -05:00 committed by GitHub
parent 851aa9eb40
commit 88a0b310d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 103 additions and 74 deletions

View file

@ -212,12 +212,16 @@ impl PendingDiff {
)
})?
.await;
buffer_diff.update(cx, |diff, cx| {
diff.set_snapshot(update.clone(), &text_snapshot, cx);
diff.secondary_diff().unwrap().update(cx, |diff, cx| {
diff.set_snapshot(update, &text_snapshot, cx);
});
let (task1, task2) = buffer_diff.update(cx, |diff, cx| {
let task1 = diff.set_snapshot(update.clone(), &text_snapshot, cx);
let task2 = diff
.secondary_diff()
.unwrap()
.update(cx, |diff, cx| diff.set_snapshot(update, &text_snapshot, cx));
(task1, task2)
})?;
task1.await;
task2.await;
diff.update(cx, |diff, cx| {
if let Diff::Pending(diff) = diff {
diff.update_visible_ranges(cx);
@ -380,17 +384,20 @@ async fn build_buffer_diff(
})?
.await;
secondary_diff.update(cx, |secondary_diff, cx| {
secondary_diff.language_changed(language.clone(), language_registry.clone(), cx);
secondary_diff.set_snapshot(update.clone(), &buffer, cx);
})?;
secondary_diff
.update(cx, |secondary_diff, cx| {
secondary_diff.language_changed(language.clone(), language_registry.clone(), cx);
secondary_diff.set_snapshot(update.clone(), &buffer, cx)
})?
.await;
let diff = cx.new(|cx| BufferDiff::new(&buffer, cx))?;
diff.update(cx, |diff, cx| {
diff.language_changed(language, language_registry, cx);
diff.set_snapshot(update.clone(), &buffer, cx);
diff.set_secondary_diff(secondary_diff);
})?;
diff.set_snapshot(update.clone(), &buffer, cx)
})?
.await;
Ok(diff)
}

View file

@ -401,10 +401,11 @@ impl ActionLog {
if let Ok(update) = update {
let update = update.await;
let diff_snapshot = diff.update(cx, |diff, cx| {
diff.set_snapshot(update.clone(), &buffer_snapshot, cx);
diff.snapshot(cx)
})?;
diff.update(cx, |diff, cx| {
diff.set_snapshot(update.clone(), &buffer_snapshot, cx)
})?
.await;
let diff_snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx))?;
unreviewed_edits = cx
.background_spawn({

View file

@ -1148,7 +1148,7 @@ impl BufferDiff {
None,
cx,
));
this.set_snapshot(inner, &buffer, cx);
this.set_snapshot(inner, &buffer, cx).detach();
this
}
@ -1293,26 +1293,19 @@ impl BufferDiff {
language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut Context<Self>,
) {
let base_text = self.inner.base_text.downgrade();
let fut = self.inner.base_text.update(cx, |base_text, cx| {
if let Some(language_registry) = language_registry {
base_text.set_language_registry(language_registry);
}
base_text.set_language(language, cx);
base_text.parsing_idle()
});
cx.spawn(async move |this, cx| {
let fut = base_text
.update(cx, |base_text, cx| {
if let Some(language_registry) = language_registry {
base_text.set_language_registry(language_registry);
}
base_text.set_language(language, cx);
base_text.parsing_idle()
})
.ok()?;
fut.await;
this.update(cx, |_, cx| {
cx.emit(BufferDiffEvent::LanguageChanged);
})
.ok()?;
Some(())
.ok();
})
.detach();
}
@ -1322,7 +1315,7 @@ impl BufferDiff {
new_state: BufferDiffUpdate,
buffer: &text::BufferSnapshot,
cx: &mut Context<Self>,
) -> Option<Range<Anchor>> {
) -> Task<Option<Range<Anchor>>> {
self.set_snapshot_with_secondary(new_state, buffer, None, false, cx)
}
@ -1333,7 +1326,7 @@ impl BufferDiff {
secondary_diff_change: Option<Range<Anchor>>,
clear_pending_hunks: bool,
cx: &mut Context<Self>,
) -> Option<Range<Anchor>> {
) -> Task<Option<Range<Anchor>>> {
log::debug!("set snapshot with secondary {secondary_diff_change:?}");
let old_snapshot = self.snapshot(cx);
@ -1372,13 +1365,16 @@ impl BufferDiff {
let state = &mut self.inner;
state.base_text_exists = new_state.base_text_exists;
if update.base_text_changed {
let parsing_idle = if update.base_text_changed {
state.base_text.update(cx, |base_text, cx| {
base_text.set_capability(Capability::ReadWrite, cx);
base_text.set_text(new_state.base_text.clone(), cx);
base_text.set_capability(Capability::ReadOnly, cx);
Some(base_text.parsing_idle())
})
}
} else {
None
};
state.hunks = new_state.hunks;
if update.base_text_changed || clear_pending_hunks {
if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last())
@ -1402,11 +1398,19 @@ impl BufferDiff {
state.pending_hunks = SumTree::new(buffer);
}
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: changed_range.clone(),
base_text_changed_range,
});
changed_range
cx.spawn(async move |this, cx| {
if let Some(parsing_idle) = parsing_idle {
parsing_idle.await;
}
this.update(cx, |_, cx| {
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: changed_range.clone(),
base_text_changed_range,
});
})
.ok();
changed_range
})
}
pub fn base_text(&self, cx: &App) -> language::BufferSnapshot {
@ -1454,10 +1458,12 @@ impl BufferDiff {
return;
};
let state = state.await;
this.update(cx, |this, cx| {
this.set_snapshot(state, &buffer, cx);
})
.log_err();
if let Some(task) = this
.update(cx, |this, cx| this.set_snapshot(state, &buffer, cx))
.log_err()
{
task.await;
}
drop(complete_on_drop)
})
.detach();
@ -1475,8 +1481,9 @@ impl BufferDiff {
let language = self.base_text(cx).language().cloned();
let base_text = self.base_text_string(cx).map(|s| s.as_str().into());
let fut = self.update_diff(buffer.clone(), base_text, false, language, cx);
let snapshot = cx.background_executor().block(fut);
self.set_snapshot(snapshot, &buffer, cx);
let executor = cx.background_executor().clone();
let snapshot = executor.block(fut);
self.set_snapshot(snapshot, &buffer, cx).detach();
}
pub fn base_text_buffer(&self) -> Entity<language::Buffer> {
@ -2589,6 +2596,7 @@ mod tests {
let diff = cx.new(|cx| {
BufferDiff::new_with_base_text(&base_text, &buffer.read(cx).text_snapshot(), cx)
});
cx.run_until_parked();
let (tx, rx) = mpsc::channel();
let subscription =
cx.update(|cx| cx.subscribe(&diff, move |_, event, _| tx.send(event.clone()).unwrap()));
@ -2619,7 +2627,8 @@ mod tests {
)
})
.await;
diff.update(cx, |diff, cx| diff.set_snapshot(update, &snapshot, cx));
diff.update(cx, |diff, cx| diff.set_snapshot(update, &snapshot, cx))
.await;
cx.run_until_parked();
drop(subscription);
let events = rx.into_iter().collect::<Vec<_>>();

View file

@ -335,9 +335,14 @@ impl RatePredictionsModal {
);
cx.spawn(async move |diff, cx| {
let update = update.await;
diff.update(cx, |diff, cx| {
diff.set_snapshot(update, &new_buffer_snapshot.text, cx);
})
if let Some(task) = diff
.update(cx, |diff, cx| {
diff.set_snapshot(update, &new_buffer_snapshot.text, cx)
})
.ok()
{
task.await;
}
})
.detach();
});

View file

@ -806,8 +806,8 @@ async fn build_buffer_diff(
diff.update(cx, |diff, cx| {
diff.language_changed(language, Some(language_registry.clone()), cx);
diff.set_snapshot(update, &buffer.text, cx)
})
.ok();
})?
.await;
Ok(diff)
}

View file

@ -191,8 +191,9 @@ async fn build_buffer_diff(
Some(language_registry),
cx,
);
diff.set_snapshot(update, &new_buffer_snapshot.text, cx);
})?;
diff.set_snapshot(update, &new_buffer_snapshot.text, cx)
})?
.await;
Ok(diff)
}

View file

@ -275,8 +275,9 @@ async fn update_diff_buffer(
.await;
diff.update(cx, |diff, cx| {
diff.set_snapshot(update, &source_buffer_snapshot.text, cx);
})?;
diff.set_snapshot(update, &source_buffer_snapshot.text, cx)
})?
.await;
Ok(())
}

View file

@ -4019,8 +4019,9 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
})
.await;
diff.update(cx, |diff, cx| {
diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx);
});
diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx)
})
.await;
cx.run_until_parked();
assert_new_snapshot(
@ -4056,8 +4057,9 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
})
.await;
diff.update(cx, |diff, cx| {
diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx);
});
diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx)
})
.await;
cx.run_until_parked();
assert_new_snapshot(

View file

@ -3159,12 +3159,13 @@ impl BufferGitState {
let unstaged_changed_range = if let Some((unstaged_diff, new_unstaged_diff)) =
unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
{
unstaged_diff.update(cx, |diff, cx| {
let task = unstaged_diff.update(cx, |diff, cx| {
if language_changed {
diff.language_changed(language.clone(), language_registry.clone(), cx);
}
diff.set_snapshot(new_unstaged_diff, &buffer, cx)
})?
})?;
Some(task.await)
} else {
None
};
@ -3174,18 +3175,20 @@ impl BufferGitState {
if let Some((uncommitted_diff, new_uncommitted_diff)) =
uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone())
{
uncommitted_diff.update(cx, |diff, cx| {
if language_changed {
diff.language_changed(language, language_registry, cx);
}
diff.set_snapshot_with_secondary(
new_uncommitted_diff,
&buffer,
unstaged_changed_range,
true,
cx,
);
})?;
uncommitted_diff
.update(cx, |diff, cx| {
if language_changed {
diff.language_changed(language, language_registry, cx);
}
diff.set_snapshot_with_secondary(
new_uncommitted_diff,
&buffer,
unstaged_changed_range.flatten(),
true,
cx,
)
})?
.await;
}
log::debug!(