Refactor BufferDiff to allow multiple diffs to share the same base text buffer (#58266)
This PR changes how base texts are managed by the `buffer_diff` crate, to enable keeping two diff entities alive that share the same base text buffer entity. Previously, each diff owned its own base text buffer and edited it when calling `BufferDiff::set_snapshot`, so the only way to reuse the same base text between two diffs was to have two independent buffers for it, which is pretty inefficient. After this PR, each diff still has a base text buffer, but `set_snapshot` doesn't edit it. Instead, that responsibility moves into the caller. For updating the base text buffer, this PR also introduces a new pair of APIs, `Buffer::snapshot_with_edits` and `Buffer::fast_forward`, which allow us to move the parsing of the new base text into the background and then install the new syntax tree synchronously on the foreground. The git store uses the low-level APIs `set_snapshot` and `fast_forward` directly, and manages the head text and index text buffers itself (garbage-collecting them when they're no longer needed); this enables adding an `open_staged_diff` API which returns a diff between the managed index buffer and the managed head buffer (the latter is also used for the uncommitted diff's base text). Other downstreams don't need to reuse a base text buffer, and those have been migrated to use the high-level `set_base_text` API, which now calls `set_snapshot` and `fast_forward` internally, with a guard to prevent concurrent updates. Another change worthy of note is that we now always diff the old base text with the new base text to create `snapshot_with_edits`. There are also some incidental bug fixes: - Fixed an issue where a dangling weak unstaged diff could stick around in the git store forever - Restored the `IndexMatchesHead` optimization that had become inoperative in the remote case - Fixed a crash in the multibuffer due to the handling of `BufferDiffEvent::LanguageChanged`, which could cause the multibuffer to have transforms that were inconsistent with the diff base text. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes ZED-81P Release Notes: - Fixed a rare crash that could occur while using the uncommitted diff. --------- Co-authored-by: Ben Kunkle <ben@zed.dev> Co-authored-by: Anthony Eid <hello@anthonyeid.me>
This commit is contained in:
1 parent
d0b81bc65b
commit
7a469725d3
26 files changed
+1948
-1124
No files matched your search
Generated
-1
@@ -2440,7 +2440,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"clock",
|
||||
"ctor",
|
||||
"futures 0.3.32",
|
||||
"gpui",
|
||||
"imara-diff",
|
||||
"language",
|
||||
|
||||
@@ -24,6 +24,7 @@ impl Diff {
|
||||
) -> Self {
|
||||
let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly));
|
||||
let new_buffer = cx.new(|cx| Buffer::local(new_text, cx));
|
||||
let base_text_exists = old_text.is_some();
|
||||
let base_text = old_text.clone().unwrap_or(String::new()).into();
|
||||
let task = cx.spawn({
|
||||
let multibuffer = multibuffer.clone();
|
||||
@@ -40,8 +41,8 @@ impl Diff {
|
||||
|
||||
let diff = build_buffer_diff(
|
||||
old_text.unwrap_or("".into()).into(),
|
||||
base_text_exists,
|
||||
&buffer,
|
||||
Some(language_registry.clone()),
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
@@ -88,16 +89,7 @@ impl Diff {
|
||||
let language = buffer.read(cx).language().cloned();
|
||||
let language_registry = buffer.read(cx).language_registry();
|
||||
let buffer_diff = cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, cx);
|
||||
diff.language_changed(language.clone(), language_registry.clone(), cx);
|
||||
let secondary_diff = cx.new(|cx| {
|
||||
// For the secondary diff buffer we skip assigning the language as we do not really need to perform any syntax highlighting on
|
||||
// it. As a result, by skipping it we are potentially shaving off a lot of RSS plus we get a snappier feel for large diff
|
||||
// view multibuffers.
|
||||
BufferDiff::new_unchanged(&buffer_text_snapshot, cx)
|
||||
});
|
||||
diff.set_secondary_diff(secondary_diff);
|
||||
diff
|
||||
BufferDiff::new_unchanged(&buffer_text_snapshot, language, language_registry, cx)
|
||||
});
|
||||
|
||||
let multibuffer = cx.new(|cx| {
|
||||
@@ -233,28 +225,20 @@ impl PendingDiff {
|
||||
let base_text = self.base_text.clone();
|
||||
self.update_diff = cx.spawn(async move |diff, cx| {
|
||||
let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
||||
let language = buffer.read_with(cx, |buffer, _| buffer.language().cloned());
|
||||
let base_text_snapshot = buffer_diff.read_with(cx, |diff, cx| diff.base_text(cx));
|
||||
let update = buffer_diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.update_diff(
|
||||
text_snapshot.clone(),
|
||||
&base_text_snapshot,
|
||||
Some(base_text.clone()),
|
||||
None,
|
||||
language,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
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)
|
||||
buffer_diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(update.clone(), cx);
|
||||
});
|
||||
task1.await;
|
||||
task2.await;
|
||||
diff.update(cx, |diff, cx| {
|
||||
if let Diff::Pending(diff) = diff {
|
||||
diff.update_visible_ranges(cx);
|
||||
@@ -272,7 +256,6 @@ impl PendingDiff {
|
||||
let ranges = self.excerpt_ranges(cx);
|
||||
let base_text = self.base_text.clone();
|
||||
let new_buffer = self.new_buffer.read(cx);
|
||||
let language_registry = new_buffer.language_registry();
|
||||
|
||||
let path = new_buffer
|
||||
.file()
|
||||
@@ -299,7 +282,7 @@ impl PendingDiff {
|
||||
let buffer = buffer.clone();
|
||||
async move |_this, cx| {
|
||||
buffer.update(cx, |buffer, _| buffer.parsing_idle()).await;
|
||||
build_buffer_diff(base_text, &buffer, language_registry, cx).await
|
||||
build_buffer_diff(base_text, true, &buffer, cx).await
|
||||
}
|
||||
});
|
||||
|
||||
@@ -397,39 +380,18 @@ pub struct FinalizedDiff {
|
||||
|
||||
async fn build_buffer_diff(
|
||||
old_text: Arc<str>,
|
||||
base_text_exists: bool,
|
||||
buffer: &Entity<Buffer>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Entity<BufferDiff>> {
|
||||
let language = cx.update(|cx| buffer.read(cx).language().cloned());
|
||||
let text_snapshot = cx.update(|cx| buffer.read(cx).text_snapshot());
|
||||
let language_registry = cx.update(|cx| buffer.read(cx).language_registry());
|
||||
let buffer = cx.update(|cx| buffer.read(cx).snapshot());
|
||||
let base_text = base_text_exists.then(|| old_text);
|
||||
|
||||
let secondary_diff = cx.new(|cx| BufferDiff::new(&buffer, cx));
|
||||
|
||||
let update = secondary_diff
|
||||
.update(cx, |secondary_diff, cx| {
|
||||
secondary_diff.update_diff(
|
||||
text_snapshot.clone(),
|
||||
Some(old_text),
|
||||
Some(false),
|
||||
language.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
secondary_diff
|
||||
.update(cx, |secondary_diff, cx| {
|
||||
secondary_diff.set_snapshot(update.clone(), &buffer, cx)
|
||||
})
|
||||
.await;
|
||||
|
||||
let diff = cx.new(|cx| BufferDiff::new(&buffer, cx));
|
||||
let diff = cx.new(|cx| BufferDiff::new(&buffer, language, language_registry, cx));
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.language_changed(language, language_registry, cx);
|
||||
diff.set_secondary_diff(secondary_diff);
|
||||
diff.set_snapshot(update.clone(), &buffer, cx)
|
||||
diff.set_base_text(base_text, buffer.text, cx)
|
||||
})
|
||||
.await;
|
||||
Ok(diff)
|
||||
|
||||
@@ -159,11 +159,8 @@ impl ActionLog {
|
||||
let text_snapshot = buffer.read(cx).text_snapshot();
|
||||
let language = buffer.read(cx).language().cloned();
|
||||
let language_registry = buffer.read(cx).language_registry();
|
||||
let diff = cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&text_snapshot, cx);
|
||||
diff.language_changed(language, language_registry, cx);
|
||||
diff
|
||||
});
|
||||
let diff =
|
||||
cx.new(|cx| BufferDiff::new(&text_snapshot, language, language_registry, cx));
|
||||
let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
|
||||
let diff_base;
|
||||
let unreviewed_edits;
|
||||
@@ -465,29 +462,15 @@ impl ActionLog {
|
||||
new_diff_base: Rope,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
let (diff, language) = this.read_with(cx, |this, cx| {
|
||||
let diff = this.read_with(cx, |this, _cx| {
|
||||
let tracked_buffer = this
|
||||
.tracked_buffers
|
||||
.get(buffer)
|
||||
.context("buffer not tracked")?;
|
||||
anyhow::Ok((
|
||||
tracked_buffer.diff.clone(),
|
||||
buffer.read(cx).language().cloned(),
|
||||
))
|
||||
anyhow::Ok(tracked_buffer.diff.clone())
|
||||
})??;
|
||||
let update = diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.update_diff(
|
||||
buffer_snapshot.clone(),
|
||||
Some(new_base_text),
|
||||
Some(true),
|
||||
language,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(update.clone(), &buffer_snapshot, cx)
|
||||
diff.set_base_text(Some(new_base_text), buffer_snapshot.clone(), cx)
|
||||
})
|
||||
.await;
|
||||
let diff_snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx));
|
||||
|
||||
@@ -102,6 +102,7 @@ impl AgentDiffPane {
|
||||
);
|
||||
diff_display_editor
|
||||
.set_render_diff_hunk_controls(diff_hunk_controls(&thread, workspace.clone()), cx);
|
||||
diff_display_editor.set_render_diff_hunks_as_unstaged(cx);
|
||||
diff_display_editor.update_editors(cx, |editor, _cx| {
|
||||
editor.register_addon(AgentDiffAddon);
|
||||
});
|
||||
@@ -1572,6 +1573,7 @@ impl AgentDiff {
|
||||
diff_hunk_controls(&thread, workspace.clone()),
|
||||
cx,
|
||||
);
|
||||
editor.set_render_diff_hunks_as_unstaged(true, cx);
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor.register_addon(EditorAgentDiffAddon);
|
||||
});
|
||||
|
||||
@@ -470,6 +470,7 @@ fn create_editor_diff(
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor.set_render_diff_hunks_as_unstaged(true, cx);
|
||||
editor.set_text_style_refinement(diff_editor_text_style_refinement(cx));
|
||||
editor
|
||||
})
|
||||
|
||||
@@ -16,7 +16,6 @@ test-support = ["settings"]
|
||||
|
||||
[dependencies]
|
||||
clock.workspace = true
|
||||
futures.workspace = true
|
||||
imara-diff.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -2886,6 +2886,107 @@ async fn test_git_diff_base_change(
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_git_diff_index_matches_head(
|
||||
executor: BackgroundExecutor,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
let committed_text = "
|
||||
one
|
||||
two
|
||||
three
|
||||
"
|
||||
.unindent();
|
||||
let file_contents = "
|
||||
one
|
||||
TWO
|
||||
three
|
||||
"
|
||||
.unindent();
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".git": {},
|
||||
"a.txt": file_contents,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
client_a
|
||||
.fs()
|
||||
.set_head_and_index_for_repo(Path::new("/dir/.git"), &[("a.txt", committed_text.clone())]);
|
||||
|
||||
let (project_local, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.share_project(project_local.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let project_remote = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the uncommitted diff on the guest, without opening it on the host
|
||||
// first, so that the host loads the diff bases in response to the guest's
|
||||
// request.
|
||||
let remote_buffer = project_remote
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer((worktree_id, rel_path("a.txt")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let remote_uncommitted_diff = project_remote
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_uncommitted_diff(remote_buffer.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
// The guest's index and head texts share one allocation, which is only
|
||||
// possible if the host detected that the index matches the head and sent
|
||||
// `Mode::IndexMatchesHead`.
|
||||
let buffer_id = remote_buffer.read_with(cx_b, |buffer, _| buffer.remote_id());
|
||||
project_remote.read_with(cx_b, |project, cx| {
|
||||
assert!(
|
||||
project
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.index_matches_head_for_buffer(buffer_id, cx),
|
||||
"the host should send IndexMatchesHead when the index is clean"
|
||||
);
|
||||
});
|
||||
|
||||
remote_uncommitted_diff.read_with(cx_b, |diff, cx| {
|
||||
let buffer = remote_buffer.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(committed_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.snapshot(cx).hunks_in_row_range(0..3, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string(cx).unwrap(),
|
||||
&[(
|
||||
1..2,
|
||||
"two\n",
|
||||
"TWO\n",
|
||||
DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
|
||||
)],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_git_branch_name(
|
||||
executor: BackgroundExecutor,
|
||||
|
||||
@@ -5,7 +5,7 @@ use editor::{Editor, Inlay, MultiBuffer};
|
||||
use feature_flags::{FeatureFlag, PresenceFlag, register_feature_flag};
|
||||
use gpui::{
|
||||
App, BorderStyle, DismissEvent, EdgesRefinement, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Length, StyleRefinement, TextStyleRefinement, Window, actions, prelude::*,
|
||||
Length, StyleRefinement, Task, TextStyleRefinement, Window, actions, prelude::*,
|
||||
};
|
||||
use language::{
|
||||
Bias, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, Point, ToOffset, ToPoint,
|
||||
@@ -71,6 +71,8 @@ struct ActivePrediction {
|
||||
expected_editor: Entity<Editor>,
|
||||
_expected_buffer_subscription: gpui::Subscription,
|
||||
formatted_inputs: Entity<Markdown>,
|
||||
_predicted_diff_task: Task<()>,
|
||||
expected_diff_task: Task<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
||||
@@ -308,29 +310,14 @@ impl RatePredictionsModal {
|
||||
new_buffer_snapshot: BufferSnapshot,
|
||||
old_buffer_snapshot: BufferSnapshot,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let language = new_buffer_snapshot.language().cloned();
|
||||
) -> Task<()> {
|
||||
diff.update(cx, |diff, cx| {
|
||||
let update = diff.update_diff(
|
||||
new_buffer_snapshot.text.clone(),
|
||||
diff.set_base_text(
|
||||
Some(old_buffer_snapshot.text().into()),
|
||||
Some(true),
|
||||
language,
|
||||
new_buffer_snapshot.text,
|
||||
cx,
|
||||
);
|
||||
cx.spawn(async move |diff, cx| {
|
||||
let update = update.await;
|
||||
if let Some(task) = diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(update, &new_buffer_snapshot.text, cx)
|
||||
})
|
||||
.ok()
|
||||
{
|
||||
task.await;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_editable_region_markers(
|
||||
@@ -469,10 +456,17 @@ impl RatePredictionsModal {
|
||||
Point::new(visible_range.start.row.saturating_sub(5), 0)
|
||||
..Point::new(visible_range.end.row.saturating_add(5), 0)
|
||||
.min(predicted_buffer_snapshot.max_point());
|
||||
self.diff_editor.update(cx, |editor, cx| {
|
||||
let predicted_diff_task = self.diff_editor.update(cx, |editor, cx| {
|
||||
let predicted_buffer_id = predicted_buffer_snapshot.remote_id();
|
||||
let diff = cx.new(|cx| BufferDiff::new(&predicted_buffer_snapshot.text, cx));
|
||||
Self::update_buffer_diff(
|
||||
let diff = cx.new(|cx| {
|
||||
BufferDiff::new(
|
||||
&predicted_buffer_snapshot.text,
|
||||
predicted_buffer_snapshot.language().cloned(),
|
||||
predicted_buffer.read(cx).language_registry(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let predicted_diff_task = Self::update_buffer_diff(
|
||||
&diff,
|
||||
predicted_buffer_snapshot.clone(),
|
||||
prediction.snapshot.clone(),
|
||||
@@ -490,6 +484,7 @@ impl RatePredictionsModal {
|
||||
);
|
||||
multibuffer.add_diff(diff, cx);
|
||||
});
|
||||
predicted_diff_task
|
||||
});
|
||||
|
||||
if let Some(editable_range) = editable_range.as_ref() {
|
||||
@@ -625,8 +620,15 @@ impl RatePredictionsModal {
|
||||
..range.end.to_point(&expected_buffer_snapshot)
|
||||
})
|
||||
.unwrap_or(visible_range);
|
||||
let expected_diff = cx.new(|cx| BufferDiff::new(&expected_buffer_snapshot.text, cx));
|
||||
Self::update_buffer_diff(
|
||||
let expected_diff = cx.new(|cx| {
|
||||
BufferDiff::new(
|
||||
&expected_buffer_snapshot.text,
|
||||
expected_buffer_snapshot.language().cloned(),
|
||||
expected_buffer.read(cx).language_registry(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let expected_diff_task = Self::update_buffer_diff(
|
||||
&expected_diff,
|
||||
expected_buffer_snapshot.clone(),
|
||||
prediction.snapshot.clone(),
|
||||
@@ -676,16 +678,19 @@ impl RatePredictionsModal {
|
||||
let expected_buffer_subscription = cx.subscribe(&expected_buffer, {
|
||||
let expected_diff = expected_diff.clone();
|
||||
let original_snapshot = prediction.snapshot.clone();
|
||||
move |_this, buffer, event, cx| match event {
|
||||
move |this, buffer, event, cx| match event {
|
||||
language::BufferEvent::Edited { .. }
|
||||
| language::BufferEvent::LanguageChanged(_)
|
||||
| language::BufferEvent::Reparsed => {
|
||||
Self::update_buffer_diff(
|
||||
let task = Self::update_buffer_diff(
|
||||
&expected_diff,
|
||||
buffer.read(cx).snapshot(),
|
||||
original_snapshot.clone(),
|
||||
cx,
|
||||
);
|
||||
if let Some(active_prediction) = this.active_prediction.as_mut() {
|
||||
active_prediction.expected_diff_task = task;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -716,6 +721,8 @@ impl RatePredictionsModal {
|
||||
expected_buffer,
|
||||
expected_editor,
|
||||
_expected_buffer_subscription: expected_buffer_subscription,
|
||||
_predicted_diff_task: predicted_diff_task,
|
||||
expected_diff_task,
|
||||
formatted_inputs: cx.new(|cx| {
|
||||
Markdown::new(
|
||||
formatted_inputs.into(),
|
||||
|
||||
@@ -1122,6 +1122,10 @@ pub struct Editor {
|
||||
load_diff_task: Option<Shared<Task<()>>>,
|
||||
/// Whether we are temporarily displaying a diff other than git's
|
||||
temporary_diff_override: bool,
|
||||
/// Whether to render all diff hunks with the "unstaged" appearance,
|
||||
/// regardless of whether they have a secondary hunk. Used by views whose
|
||||
/// diffs aren't related to the git index (e.g. agent diffs).
|
||||
render_diff_hunks_as_unstaged: bool,
|
||||
selection_mark_mode: bool,
|
||||
toggle_fold_multiple_buffers: Task<()>,
|
||||
_scroll_cursor_center_top_bottom_task: Task<()>,
|
||||
@@ -2348,6 +2352,7 @@ impl Editor {
|
||||
text_style_refinement: None,
|
||||
load_diff_task: load_uncommitted_diff,
|
||||
temporary_diff_override: false,
|
||||
render_diff_hunks_as_unstaged: false,
|
||||
minimap: None,
|
||||
change_list: ChangeList::new(),
|
||||
mode,
|
||||
|
||||
@@ -37636,95 +37636,6 @@ async fn test_restore_and_next(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_restore_hunk_with_stale_base_text(cx: &mut TestAppContext) {
|
||||
// Regression test: prepare_restore_change must read base_text from the same
|
||||
// snapshot the hunk came from, not from the live BufferDiff entity. The live
|
||||
// entity's base_text may have already been updated asynchronously (e.g.
|
||||
// because git HEAD changed) while the MultiBufferSnapshot still holds the
|
||||
// old hunk byte ranges — using both together causes Rope::slice to panic
|
||||
// when the old range exceeds the new base text length.
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let long_base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n";
|
||||
cx.set_state("ˇONE\ntwo\nTHREE\nfour\nFIVE\nsix\nseven\neight\nnine\nten\n");
|
||||
cx.set_head_text(long_base_text);
|
||||
|
||||
let buffer_id = cx.update_buffer(|buffer, _| buffer.remote_id());
|
||||
|
||||
// Verify we have hunks from the initial diff.
|
||||
let has_hunks = cx.update_editor(|editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let hunks = snapshot
|
||||
.buffer_snapshot()
|
||||
.diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len());
|
||||
hunks.count() > 0
|
||||
});
|
||||
assert!(has_hunks, "should have diff hunks before restoring");
|
||||
|
||||
// Now trigger a git HEAD change to a much shorter base text.
|
||||
// After this, the live BufferDiff entity's base_text buffer will be
|
||||
// updated synchronously (inside set_snapshot_with_secondary_inner),
|
||||
// but DiffChanged is deferred until parsing_idle completes.
|
||||
// We step the executor tick-by-tick to find the window where the
|
||||
// live base_text is already short but the MultiBuffer snapshot is
|
||||
// still stale (old hunks + old base_text).
|
||||
let short_base_text = "short\n";
|
||||
let fs = cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
|
||||
let path = cx.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
|
||||
fs.set_head_for_repo(
|
||||
&Path::new(path!("/root")).join(".git"),
|
||||
&[(path.as_unix_str(), short_base_text.to_string())],
|
||||
"newcommit",
|
||||
);
|
||||
|
||||
// Step the executor tick-by-tick. At each step, check whether the
|
||||
// race condition exists: live BufferDiff has short base text but
|
||||
// the MultiBuffer snapshot still has old (long) hunks.
|
||||
let mut found_race = false;
|
||||
for _ in 0..200 {
|
||||
cx.executor().tick();
|
||||
|
||||
let race_exists = cx.update_editor(|editor, _window, cx| {
|
||||
let multi_buffer = editor.buffer().read(cx);
|
||||
let diff_entity = match multi_buffer.diff_for(buffer_id) {
|
||||
Some(d) => d,
|
||||
None => return false,
|
||||
};
|
||||
let live_base_len = diff_entity.read(cx).base_text(cx).len();
|
||||
let snapshot = multi_buffer.snapshot(cx);
|
||||
let snapshot_base_len = snapshot
|
||||
.diff_for_buffer_id(buffer_id)
|
||||
.map(|d| d.base_text().len());
|
||||
// Race: live base text is shorter than what the snapshot knows.
|
||||
live_base_len < long_base_text.len() && snapshot_base_len == Some(long_base_text.len())
|
||||
});
|
||||
|
||||
if race_exists {
|
||||
found_race = true;
|
||||
// The race window is open: the live entity has new (short) base
|
||||
// text but the MultiBuffer snapshot still has old hunks with byte
|
||||
// ranges computed against the old long base text. Attempt restore.
|
||||
// Without the fix, this panics with "cannot summarize past end of
|
||||
// rope". With the fix, it reads base_text from the stale snapshot
|
||||
// (consistent with the stale hunks) and succeeds.
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.select_all(&SelectAll, window, cx);
|
||||
editor.git_restore(&Default::default(), window, cx);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
found_race,
|
||||
"failed to observe the race condition between \
|
||||
live BufferDiff base_text and stale MultiBuffer snapshot; \
|
||||
the test may need adjustment if the async diff pipeline changed"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_align_selections(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -5135,6 +5135,7 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
fn paint_gutter_diff_hunks(
|
||||
&self,
|
||||
layout: &mut EditorLayout,
|
||||
split_side: Option<SplitSide>,
|
||||
window: &mut Window,
|
||||
@@ -5210,7 +5211,7 @@ impl EditorElement {
|
||||
.editor_background
|
||||
.blend(background_color);
|
||||
|
||||
if !Self::diff_hunk_hollow(status, cx) {
|
||||
if !self.diff_hunk_hollow(status, cx) {
|
||||
window.paint_quad(quad(
|
||||
hunk_bounds,
|
||||
corner_radii,
|
||||
@@ -5388,7 +5389,7 @@ impl EditorElement {
|
||||
)
|
||||
});
|
||||
if show_git_gutter {
|
||||
Self::paint_gutter_diff_hunks(layout, self.split_side, window, cx)
|
||||
self.paint_gutter_diff_hunks(layout, self.split_side, window, cx)
|
||||
}
|
||||
|
||||
let highlight_width = 0.275 * layout.position_map.line_height;
|
||||
@@ -6501,8 +6502,9 @@ impl EditorElement {
|
||||
)
|
||||
}
|
||||
|
||||
fn diff_hunk_hollow(status: DiffHunkStatus, cx: &mut App) -> bool {
|
||||
let unstaged = status.has_secondary_hunk();
|
||||
fn diff_hunk_hollow(&self, status: DiffHunkStatus, cx: &mut App) -> bool {
|
||||
let unstaged =
|
||||
self.editor.read(cx).render_diff_hunks_as_unstaged || status.has_secondary_hunk();
|
||||
let unstaged_hollow = matches!(
|
||||
ProjectSettings::get_global(cx).git.hunk_style,
|
||||
GitHunkStyleSetting::UnstagedHollow
|
||||
@@ -8194,7 +8196,7 @@ impl Element for EditorElement {
|
||||
type_id: None,
|
||||
};
|
||||
|
||||
let background = if Self::diff_hunk_hollow(diff_status, cx) {
|
||||
let background = if self.diff_hunk_hollow(diff_status, cx) {
|
||||
hollow_highlight
|
||||
} else {
|
||||
filled_highlight
|
||||
|
||||
@@ -175,6 +175,18 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Make all diff hunks render with the "unstaged" appearance, regardless
|
||||
/// of whether they have a secondary hunk. Intended for views whose diffs
|
||||
/// aren't related to the git index (e.g. agent diffs).
|
||||
pub fn set_render_diff_hunks_as_unstaged(
|
||||
&mut self,
|
||||
render_as_unstaged: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.render_diff_hunks_as_unstaged = render_as_unstaged;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn git_blame_inline_enabled(&self) -> bool {
|
||||
self.git_blame_inline_enabled
|
||||
}
|
||||
@@ -230,6 +242,7 @@ impl Editor {
|
||||
|
||||
pub fn end_temporary_diff_override(&mut self, cx: &mut Context<Self>) {
|
||||
self.temporary_diff_override = false;
|
||||
self.render_diff_hunks_as_unstaged = false;
|
||||
self.set_render_diff_hunk_controls(Arc::new(render_diff_hunk_controls), cx);
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_all_diff_hunks_collapsed(cx);
|
||||
|
||||
+45
-39
@@ -471,6 +471,12 @@ impl SplittableEditor {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_render_diff_hunks_as_unstaged(&self, cx: &mut Context<Self>) {
|
||||
self.update_editors(cx, |editor, cx| {
|
||||
editor.set_render_diff_hunks_as_unstaged(true, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn focused_side(&self) -> SplitSide {
|
||||
if let Some(lhs) = &self.lhs
|
||||
&& lhs.was_last_focused
|
||||
@@ -607,9 +613,11 @@ impl SplittableEditor {
|
||||
});
|
||||
|
||||
let render_diff_hunk_controls = self.rhs_editor.read(cx).render_diff_hunk_controls.clone();
|
||||
let render_diff_hunks_as_unstaged = self.rhs_editor.read(cx).render_diff_hunks_as_unstaged;
|
||||
let lhs_editor = cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(lhs_multibuffer.clone(), Some(project.clone()), window, cx);
|
||||
editor.set_render_diff_hunks_as_unstaged(render_diff_hunks_as_unstaged, cx);
|
||||
editor.set_number_deleted_lines(true, cx);
|
||||
editor.set_delegate_expand_excerpts(true);
|
||||
editor.set_delegate_stage_and_restore(true);
|
||||
@@ -2612,7 +2620,7 @@ mod tests {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(path, buffer.clone(), ranges, 0, diff.clone(), cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
@@ -2669,7 +2677,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -2798,7 +2806,7 @@ mod tests {
|
||||
let (buffer2, diff2) = buffer_with_diff(&base_text2, &base_text2, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path1 = PathKey::for_buffer(&buffer1, cx);
|
||||
let path1 = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path1,
|
||||
buffer1.clone(),
|
||||
@@ -2807,7 +2815,7 @@ mod tests {
|
||||
diff1.clone(),
|
||||
cx,
|
||||
);
|
||||
let path2 = PathKey::for_buffer(&buffer2, cx);
|
||||
let path2 = PathKey::sorted(1);
|
||||
editor.update_excerpts_for_path(
|
||||
path2,
|
||||
buffer2.clone(),
|
||||
@@ -2956,7 +2964,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -3083,7 +3091,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -3202,7 +3210,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -3332,7 +3340,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -3429,7 +3437,7 @@ mod tests {
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let end = Point::new(0, text.len() as u32);
|
||||
let path1 = PathKey::for_buffer(&buffer1, cx);
|
||||
let path1 = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path1,
|
||||
buffer1.clone(),
|
||||
@@ -3438,7 +3446,7 @@ mod tests {
|
||||
diff1.clone(),
|
||||
cx,
|
||||
);
|
||||
let path2 = PathKey::for_buffer(&buffer2, cx);
|
||||
let path2 = PathKey::sorted(1);
|
||||
editor.update_excerpts_for_path(
|
||||
path2,
|
||||
buffer2.clone(),
|
||||
@@ -3506,7 +3514,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -3569,7 +3577,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -3630,7 +3638,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&text, &text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -3743,10 +3751,10 @@ mod tests {
|
||||
.unindent();
|
||||
|
||||
let buffer2 = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
|
||||
let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), cx));
|
||||
let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), None, None, cx));
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path1 = PathKey::for_buffer(&buffer1, cx);
|
||||
let path1 = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path1,
|
||||
buffer1.clone(),
|
||||
@@ -3756,7 +3764,7 @@ mod tests {
|
||||
cx,
|
||||
);
|
||||
|
||||
let path2 = PathKey::for_buffer(&buffer2, cx);
|
||||
let path2 = PathKey::sorted(1);
|
||||
editor.update_excerpts_for_path(
|
||||
path2,
|
||||
buffer2.clone(),
|
||||
@@ -3854,7 +3862,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -3930,7 +3938,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -4017,7 +4025,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -4131,7 +4139,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -4215,7 +4223,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -4299,7 +4307,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&content, &content, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -4391,7 +4399,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -4519,7 +4527,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -4666,7 +4674,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -4888,7 +4896,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -5227,18 +5235,16 @@ mod tests {
|
||||
let buffer2_id = buffer2.read_with(cx, |buffer, _| buffer.remote_id());
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path1 = PathKey::for_buffer(&buffer1, cx);
|
||||
editor.update_excerpts_for_path(
|
||||
path1,
|
||||
PathKey::sorted(0),
|
||||
buffer1.clone(),
|
||||
vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
|
||||
0,
|
||||
diff1.clone(),
|
||||
cx,
|
||||
);
|
||||
let path2 = PathKey::for_buffer(&buffer2, cx);
|
||||
editor.update_excerpts_for_path(
|
||||
path2,
|
||||
PathKey::sorted(1),
|
||||
buffer2.clone(),
|
||||
vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
|
||||
1,
|
||||
@@ -5392,7 +5398,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -5553,7 +5559,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -5712,7 +5718,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -5843,7 +5849,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -5904,7 +5910,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -5987,7 +5993,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
@@ -6096,8 +6102,8 @@ mod tests {
|
||||
let (buffer_a, diff_a) = buffer_with_diff(&base_text_a, ¤t_text_a, &mut cx);
|
||||
let (buffer_b, diff_b) = buffer_with_diff(&base_text_b, ¤t_text_b, &mut cx);
|
||||
|
||||
let path_a = cx.read(|cx| PathKey::for_buffer(&buffer_a, cx));
|
||||
let path_b = cx.read(|cx| PathKey::for_buffer(&buffer_b, cx));
|
||||
let path_a = PathKey::sorted(0);
|
||||
let path_b = PathKey::sorted(1);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.update_excerpts_for_path(
|
||||
@@ -6238,7 +6244,7 @@ mod tests {
|
||||
let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
let path = PathKey::sorted(0);
|
||||
editor.update_excerpts_for_path(
|
||||
path,
|
||||
buffer.clone(),
|
||||
|
||||
@@ -368,7 +368,14 @@ impl CommitView {
|
||||
let buffer_diff = if is_binary {
|
||||
cx.update(|_, cx| {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
cx.new(|cx| BufferDiff::new_unchanged(&snapshot, cx))
|
||||
cx.new(|cx| {
|
||||
BufferDiff::new_unchanged(
|
||||
&snapshot,
|
||||
snapshot.language().cloned(),
|
||||
Some(language_registry.clone()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
} else {
|
||||
build_buffer_diff(old_text, &buffer, &language_registry, cx).await?
|
||||
@@ -996,23 +1003,15 @@ async fn build_buffer_diff(
|
||||
let language = cx.update(|_, cx| buffer.read(cx).language().cloned())?;
|
||||
let buffer = cx.update(|_, cx| buffer.read(cx).snapshot())?;
|
||||
|
||||
let diff = cx.new(|cx| BufferDiff::new(&buffer.text, cx));
|
||||
|
||||
let update = diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.update_diff(
|
||||
buffer.text.clone(),
|
||||
old_text.map(|old_text| Arc::from(old_text.as_str())),
|
||||
Some(true),
|
||||
language.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
let diff =
|
||||
cx.new(|cx| BufferDiff::new(&buffer.text, language, Some(language_registry.clone()), cx));
|
||||
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.language_changed(language, Some(language_registry.clone()), cx);
|
||||
diff.set_snapshot(update, &buffer.text, cx)
|
||||
diff.set_base_text(
|
||||
old_text.map(|old_text| Arc::from(old_text.as_str())),
|
||||
buffer.text.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use gpui::{
|
||||
AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, Font, IntoElement, Render, Task, WeakEntity, Window,
|
||||
};
|
||||
use language::{Buffer, HighlightedText, LanguageRegistry};
|
||||
use language::{Buffer, HighlightedText};
|
||||
use project::{Project, ProjectPath};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
@@ -53,9 +53,8 @@ impl FileDiffView {
|
||||
let new_buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer(&new_path, cx))
|
||||
.await?;
|
||||
let languages = project.update(cx, |project, _| project.languages().clone());
|
||||
|
||||
let buffer_diff = build_buffer_diff(&old_buffer, &new_buffer, languages, cx).await?;
|
||||
let buffer_diff = build_buffer_diff(&old_buffer, &new_buffer, cx).await?;
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
let workspace_entity = cx.entity();
|
||||
@@ -108,6 +107,7 @@ impl FileDiffView {
|
||||
editor.start_temporary_diff_override();
|
||||
});
|
||||
splittable.disable_diff_hunk_controls(cx);
|
||||
splittable.set_render_diff_hunks_as_unstaged(cx);
|
||||
splittable
|
||||
});
|
||||
|
||||
@@ -154,13 +154,11 @@ impl FileDiffView {
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.set_base_text(
|
||||
Some(old_snapshot.text().as_str().into()),
|
||||
old_snapshot.language().cloned(),
|
||||
new_snapshot.text.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.ok();
|
||||
.await;
|
||||
log::trace!("finish recalculating");
|
||||
}
|
||||
Ok(())
|
||||
@@ -170,36 +168,30 @@ impl FileDiffView {
|
||||
}
|
||||
|
||||
#[ztracing::instrument(skip_all)]
|
||||
async fn build_buffer_diff(
|
||||
pub(crate) async fn build_buffer_diff(
|
||||
old_buffer: &Entity<Buffer>,
|
||||
new_buffer: &Entity<Buffer>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Entity<BufferDiff>> {
|
||||
let old_buffer_snapshot = old_buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
let new_buffer_snapshot = new_buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
let language_registry = new_buffer.read_with(cx, |buffer, _| buffer.language_registry());
|
||||
|
||||
let diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot.text, cx));
|
||||
|
||||
let update = diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.update_diff(
|
||||
new_buffer_snapshot.text.clone(),
|
||||
Some(old_buffer_snapshot.text().into()),
|
||||
Some(true),
|
||||
new_buffer_snapshot.language().cloned(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
let diff = cx.new(|cx| {
|
||||
BufferDiff::new(
|
||||
&new_buffer_snapshot.text,
|
||||
new_buffer_snapshot.language().cloned(),
|
||||
language_registry,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.language_changed(
|
||||
new_buffer_snapshot.language().cloned(),
|
||||
Some(language_registry),
|
||||
diff.set_base_text(
|
||||
Some(old_buffer_snapshot.text().into()),
|
||||
new_buffer_snapshot.text.clone(),
|
||||
cx,
|
||||
);
|
||||
diff.set_snapshot(update, &new_buffer_snapshot.text, cx)
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::file_diff_view::build_buffer_diff;
|
||||
use anyhow::Result;
|
||||
use buffer_diff::BufferDiff;
|
||||
use editor::{Editor, EditorEvent, MultiBuffer, multibuffer_context_lines};
|
||||
@@ -140,36 +141,6 @@ fn common_prefix(paths: &[PathBuf]) -> Option<PathBuf> {
|
||||
Some(prefix)
|
||||
}
|
||||
|
||||
async fn build_buffer_diff(
|
||||
old_buffer: &Entity<Buffer>,
|
||||
new_buffer: &Entity<Buffer>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Entity<BufferDiff>> {
|
||||
let old_buffer_snapshot = old_buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
let new_buffer_snapshot = new_buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
let diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot.text, cx));
|
||||
|
||||
let update = diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.update_diff(
|
||||
new_buffer_snapshot.text.clone(),
|
||||
Some(old_buffer_snapshot.text().into()),
|
||||
Some(true),
|
||||
new_buffer_snapshot.language().cloned(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(update, &new_buffer_snapshot.text, cx)
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
impl MultiDiffView {
|
||||
pub fn open(
|
||||
diff_pairs: Vec<[String; 2]>,
|
||||
@@ -228,6 +199,7 @@ impl MultiDiffView {
|
||||
editor.start_temporary_diff_override();
|
||||
editor.disable_diagnostics(cx);
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor.set_render_diff_hunks_as_unstaged(true, cx);
|
||||
editor.set_render_diff_hunk_controls(
|
||||
Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),
|
||||
cx,
|
||||
|
||||
@@ -836,7 +836,6 @@ impl ProjectDiff {
|
||||
);
|
||||
}
|
||||
buffer_diff::BufferDiffEvent::BaseTextChanged
|
||||
| buffer_diff::BufferDiffEvent::LanguageChanged
|
||||
| buffer_diff::BufferDiffEvent::HunksStagedOrUnstaged(_) => {}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ use gpui::{
|
||||
AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, IntoElement, Render, Task, Window,
|
||||
};
|
||||
use language::{self, Buffer, OffsetRangeExt, Point};
|
||||
use language::{self, Buffer, Capability, OffsetRangeExt, Point};
|
||||
use project::{Project, ProjectPath};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
@@ -110,16 +110,22 @@ impl TextDiffView {
|
||||
}
|
||||
|
||||
let workspace = workspace.weak_handle();
|
||||
let diff_buffer = cx.new(|cx| BufferDiff::new(&source_buffer_snapshot.text, cx));
|
||||
let clipboard_buffer = build_clipboard_buffer(
|
||||
clipboard_text,
|
||||
&source_buffer,
|
||||
expanded_selection_range.clone(),
|
||||
cx,
|
||||
);
|
||||
let diff_buffer = cx.new(|cx| {
|
||||
BufferDiff::new_with_base_text_buffer(
|
||||
&source_buffer_snapshot.text,
|
||||
clipboard_buffer.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let task = window.spawn(cx, async move |cx| {
|
||||
update_diff_buffer(&diff_buffer, &source_buffer, &clipboard_buffer, cx).await?;
|
||||
update_diff_buffer(&diff_buffer, &source_buffer, &clipboard_buffer, cx).await;
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
let project = workspace.project().clone();
|
||||
@@ -179,6 +185,7 @@ impl TextDiffView {
|
||||
cx,
|
||||
);
|
||||
splittable.disable_diff_hunk_controls(cx);
|
||||
splittable.set_render_diff_hunks_as_unstaged(cx);
|
||||
splittable.rhs_editor().update(cx, |editor, _cx| {
|
||||
editor.start_temporary_diff_override();
|
||||
});
|
||||
@@ -240,7 +247,7 @@ impl TextDiffView {
|
||||
}
|
||||
|
||||
log::trace!("start recalculating");
|
||||
update_diff_buffer(&diff_buffer, &source_buffer, &clipboard_buffer, cx).await?;
|
||||
update_diff_buffer(&diff_buffer, &source_buffer, &clipboard_buffer, cx).await;
|
||||
log::trace!("finish recalculating");
|
||||
}
|
||||
Ok(())
|
||||
@@ -259,12 +266,17 @@ fn build_clipboard_buffer(
|
||||
cx.new(|cx| {
|
||||
let mut buffer = language::Buffer::local(source_buffer_snapshot.text(), cx);
|
||||
let language = source_buffer.read(cx).language().cloned();
|
||||
if let Some(language_registry) = source_buffer.read(cx).language_registry() {
|
||||
buffer.set_language_registry(language_registry);
|
||||
}
|
||||
buffer.set_language(language, cx);
|
||||
|
||||
let range_start = source_buffer_snapshot.point_to_offset(replacement_range.start);
|
||||
let range_end = source_buffer_snapshot.point_to_offset(replacement_range.end);
|
||||
buffer.edit([(range_start..range_end, text)], None, cx);
|
||||
|
||||
buffer.set_capability(Capability::ReadOnly, cx);
|
||||
|
||||
buffer
|
||||
})
|
||||
}
|
||||
@@ -274,32 +286,23 @@ async fn update_diff_buffer(
|
||||
source_buffer: &Entity<Buffer>,
|
||||
clipboard_buffer: &Entity<Buffer>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
) {
|
||||
let source_buffer_snapshot = source_buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
let language = source_buffer_snapshot.language().cloned();
|
||||
let language_registry = source_buffer.read_with(cx, |buffer, _| buffer.language_registry());
|
||||
|
||||
let base_buffer_snapshot = clipboard_buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
let base_text = base_buffer_snapshot.text();
|
||||
let base_text = Arc::<str>::from(base_buffer_snapshot.text());
|
||||
|
||||
let update = diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.update_diff(
|
||||
source_buffer_snapshot.text.clone(),
|
||||
Some(Arc::from(base_text.as_str())),
|
||||
Some(true),
|
||||
language.clone(),
|
||||
&base_buffer_snapshot,
|
||||
Some(base_text.clone()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.language_changed(language, language_registry, cx);
|
||||
diff.set_snapshot(update, &source_buffer_snapshot.text, cx)
|
||||
})
|
||||
.await;
|
||||
Ok(())
|
||||
diff.update(cx, |diff, cx| diff.set_snapshot(update, cx));
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for TextDiffView {}
|
||||
|
||||
@@ -3330,6 +3330,76 @@ impl Buffer {
|
||||
pub fn set_group_interval(&mut self, group_interval: Duration) {
|
||||
self.text.set_group_interval(group_interval);
|
||||
}
|
||||
|
||||
// TODO: see if ep can use this instead of Buffer::branch
|
||||
pub fn snapshot_with_edits<I, S, T>(
|
||||
&mut self,
|
||||
edits: I,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<EditedBufferSnapshot>
|
||||
where
|
||||
I: IntoIterator<Item = (Range<S>, T)>,
|
||||
S: ToOffset,
|
||||
T: Into<Arc<str>>,
|
||||
{
|
||||
let mut snapshot = self.snapshot();
|
||||
let text = snapshot.text.clone();
|
||||
let mut syntax = snapshot.syntax.clone();
|
||||
let language = self.language().cloned();
|
||||
let registry = self.language_registry();
|
||||
let new_text = self.text.snapshot_with_edits(edits);
|
||||
cx.background_spawn(async move {
|
||||
if let Some(language) = language.clone() {
|
||||
syntax.reparse(&text, registry.clone(), language);
|
||||
}
|
||||
|
||||
syntax.interpolate(&new_text.snapshot);
|
||||
|
||||
if let Some(language) = language {
|
||||
syntax.reparse(&new_text.snapshot, registry, language);
|
||||
}
|
||||
|
||||
snapshot.text = new_text.snapshot.clone();
|
||||
snapshot.syntax = syntax;
|
||||
|
||||
EditedBufferSnapshot {
|
||||
text: new_text,
|
||||
snapshot,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fast_forward(&mut self, edited: EditedBufferSnapshot, cx: &mut Context<Self>) {
|
||||
let base_version = edited.text.base_version.clone();
|
||||
let did_edit = edited.text.did_edit;
|
||||
self.text.fast_forward(edited.text);
|
||||
if edited.snapshot.language == self.language {
|
||||
self.reparse = None;
|
||||
self.did_finish_parsing(edited.snapshot.syntax, None, cx);
|
||||
if did_edit {
|
||||
cx.emit(BufferEvent::Edited {
|
||||
source: BufferEditSource::User,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
self.did_edit(&base_version, false, BufferEditSource::User, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditedBufferSnapshot {
|
||||
text: text::EditedBufferSnapshot,
|
||||
snapshot: BufferSnapshot,
|
||||
}
|
||||
|
||||
impl EditedBufferSnapshot {
|
||||
pub fn snapshot(&self) -> &BufferSnapshot {
|
||||
&self.snapshot
|
||||
}
|
||||
|
||||
pub fn base_version(&self) -> &clock::Global {
|
||||
&self.text.base_version
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
||||
@@ -615,6 +615,7 @@ impl DiffState {
|
||||
changed_range,
|
||||
base_text_changed_range: _,
|
||||
extended_range,
|
||||
base_text_changed: _,
|
||||
}) => {
|
||||
let use_extended = this.snapshot.borrow().use_extended_diff_range;
|
||||
let range = if use_extended {
|
||||
@@ -622,13 +623,10 @@ impl DiffState {
|
||||
} else {
|
||||
changed_range.clone()
|
||||
};
|
||||
if let Some(range) = range {
|
||||
this.buffer_diff_changed(diff, range, cx)
|
||||
}
|
||||
this.buffer_diff_changed(diff, range, cx);
|
||||
cx.emit(Event::BufferDiffChanged);
|
||||
}
|
||||
BufferDiffEvent::LanguageChanged => this.buffer_diff_language_changed(diff, cx),
|
||||
_ => {}
|
||||
BufferDiffEvent::BaseTextChanged | BufferDiffEvent::HunksStagedOrUnstaged(_) => {}
|
||||
}),
|
||||
diff,
|
||||
main_buffer: None,
|
||||
@@ -652,6 +650,7 @@ impl DiffState {
|
||||
changed_range: _,
|
||||
base_text_changed_range,
|
||||
extended_range: _,
|
||||
base_text_changed: _,
|
||||
}) => {
|
||||
this.inverted_buffer_diff_changed(
|
||||
diff,
|
||||
@@ -661,10 +660,8 @@ impl DiffState {
|
||||
);
|
||||
cx.emit(Event::BufferDiffChanged);
|
||||
}
|
||||
BufferDiffEvent::LanguageChanged => {
|
||||
this.inverted_buffer_diff_language_changed(diff, main_buffer, cx)
|
||||
}
|
||||
_ => {}
|
||||
BufferDiffEvent::BaseTextChanged
|
||||
| BufferDiffEvent::HunksStagedOrUnstaged(_) => {}
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -1973,38 +1970,10 @@ impl MultiBuffer {
|
||||
});
|
||||
}
|
||||
|
||||
fn buffer_diff_language_changed(&mut self, diff: Entity<BufferDiff>, cx: &mut Context<Self>) {
|
||||
let diff = diff.read(cx);
|
||||
let buffer_id = diff.buffer_id;
|
||||
let diff = DiffStateSnapshot {
|
||||
buffer_id,
|
||||
diff: diff.snapshot(cx),
|
||||
main_buffer: None,
|
||||
};
|
||||
self.snapshot.get_mut().diffs.insert_or_replace(diff, ());
|
||||
}
|
||||
|
||||
fn inverted_buffer_diff_language_changed(
|
||||
&mut self,
|
||||
diff: Entity<BufferDiff>,
|
||||
main_buffer: Entity<language::Buffer>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let base_text_buffer_id = diff.read(cx).base_text_buffer().read(cx).remote_id();
|
||||
let main_buffer_snapshot = main_buffer.read(cx).snapshot();
|
||||
let diff = diff.read(cx);
|
||||
let diff = DiffStateSnapshot {
|
||||
buffer_id: base_text_buffer_id,
|
||||
diff: diff.snapshot(cx),
|
||||
main_buffer: Some(main_buffer_snapshot),
|
||||
};
|
||||
self.snapshot.get_mut().diffs.insert_or_replace(diff, ());
|
||||
}
|
||||
|
||||
fn buffer_diff_changed(
|
||||
&mut self,
|
||||
diff: Entity<BufferDiff>,
|
||||
range: Range<text::Anchor>,
|
||||
range: Option<Range<text::Anchor>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(buffer) = self.buffer(diff.read(cx).buffer_id) else {
|
||||
@@ -2029,6 +1998,9 @@ impl MultiBuffer {
|
||||
snapshot.diffs.insert_or_replace(new_diff, ());
|
||||
|
||||
let buffer = buffer.read(cx);
|
||||
let Some(range) = range else {
|
||||
return;
|
||||
};
|
||||
let diff_change_range = range.to_offset(buffer);
|
||||
|
||||
let excerpt_edits = snapshot.excerpt_edits_for_diff_change(&path, diff_change_range);
|
||||
@@ -2239,7 +2211,7 @@ impl MultiBuffer {
|
||||
|
||||
self.buffer_diff_changed(
|
||||
diff.clone(),
|
||||
text::Anchor::min_max_range_for_buffer(buffer_id),
|
||||
Some(text::Anchor::min_max_range_for_buffer(buffer_id)),
|
||||
cx,
|
||||
);
|
||||
self.diffs.insert(buffer_id, DiffState::new(diff, cx));
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use super::*;
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
use gpui::{App, TestAppContext};
|
||||
use gpui::{App, Entity, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language::{Buffer, Rope};
|
||||
use parking_lot::RwLock;
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use util::RandomCharIter;
|
||||
use util::rel_path::rel_path;
|
||||
@@ -4783,21 +4784,19 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
|
||||
);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let base_text_snapshot = diff.read_with(cx, |diff, cx| diff.base_text(cx));
|
||||
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
||||
let update = diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.update_diff(
|
||||
buffer.read(cx).text_snapshot(),
|
||||
Some(base_text.into()),
|
||||
None,
|
||||
None,
|
||||
buffer_snapshot,
|
||||
&base_text_snapshot,
|
||||
Some(Arc::from(base_text)),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx)
|
||||
})
|
||||
.await;
|
||||
diff.update(cx, |diff, cx| diff.set_snapshot(update, cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_new_snapshot(
|
||||
@@ -4821,21 +4820,19 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
|
||||
buffer.set_text("ZERO\nONE\nTWO\n", cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let base_text_snapshot = diff.read_with(cx, |diff, cx| diff.base_text(cx));
|
||||
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
||||
let update = diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.update_diff(
|
||||
buffer.read(cx).text_snapshot(),
|
||||
Some(base_text.into()),
|
||||
None,
|
||||
None,
|
||||
buffer_snapshot,
|
||||
&base_text_snapshot,
|
||||
Some(Arc::from(base_text)),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx)
|
||||
})
|
||||
.await;
|
||||
diff.update(cx, |diff, cx| diff.set_snapshot(update, cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_new_snapshot(
|
||||
@@ -4858,13 +4855,11 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.set_base_text(
|
||||
Some("new base\n".into()),
|
||||
None,
|
||||
buffer.read(cx).text_snapshot(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_new_snapshot(
|
||||
@@ -4915,21 +4910,10 @@ async fn test_inverted_diff_base_text_change(cx: &mut TestAppContext) {
|
||||
),
|
||||
);
|
||||
|
||||
let update = diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.update_diff(
|
||||
buffer.read(cx).text_snapshot(),
|
||||
Some("ddd\n".into()),
|
||||
Some(true),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx)
|
||||
diff.set_base_text(Some("ddd\n".into()), buffer.read(cx).text_snapshot(), cx)
|
||||
})
|
||||
.detach();
|
||||
.await;
|
||||
|
||||
let _hunks: Vec<_> = multibuffer
|
||||
.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx))
|
||||
@@ -4961,22 +4945,19 @@ async fn test_inverted_diff_secondary_version_mismatch(cx: &mut TestAppContext)
|
||||
buffer.edit([(0..0, "ZERO\n")], None, cx);
|
||||
});
|
||||
|
||||
let base_text_snapshot = unstaged_diff.read_with(cx, |diff, cx| diff.base_text(cx));
|
||||
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
||||
let update = unstaged_diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.update_diff(
|
||||
buffer.read(cx).text_snapshot(),
|
||||
Some(index_text.into()),
|
||||
None,
|
||||
None,
|
||||
buffer_snapshot,
|
||||
&base_text_snapshot,
|
||||
Some(Arc::from(index_text)),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
unstaged_diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx)
|
||||
})
|
||||
.await;
|
||||
unstaged_diff.update(cx, |diff, cx| diff.set_snapshot(update, cx));
|
||||
|
||||
let base_text_buffer =
|
||||
uncommitted_diff.read_with(cx, |diff, _| diff.base_text_buffer().clone());
|
||||
|
||||
+449
-124
@@ -51,7 +51,7 @@ use gpui::{
|
||||
Subscription, Task, TaskExt, WeakEntity,
|
||||
};
|
||||
use language::{
|
||||
Buffer, BufferEvent, Language, LanguageRegistry,
|
||||
Buffer, BufferEvent, Capability, Language, LanguageRegistry,
|
||||
proto::{deserialize_version, serialize_version},
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
@@ -118,6 +118,7 @@ struct SharedDiffs {
|
||||
|
||||
struct BufferGitState {
|
||||
unstaged_diff: Option<WeakEntity<BufferDiff>>,
|
||||
staged_diff: Option<(WeakEntity<BufferDiff>, Entity<Buffer>)>,
|
||||
uncommitted_diff: Option<WeakEntity<BufferDiff>>,
|
||||
oid_diffs: HashMap<Option<git::Oid>, WeakEntity<BufferDiff>>,
|
||||
conflict_set: Option<WeakEntity<ConflictSet>>,
|
||||
@@ -142,6 +143,9 @@ struct BufferGitState {
|
||||
head_text: Option<Arc<str>>,
|
||||
index_text: Option<Arc<str>>,
|
||||
oid_texts: HashMap<git::Oid, Arc<str>>,
|
||||
head_text_buffer: WeakEntity<Buffer>,
|
||||
index_text_buffer: WeakEntity<Buffer>,
|
||||
index_text_buffer_language_enabled: bool,
|
||||
head_changed: bool,
|
||||
index_changed: bool,
|
||||
language_changed: bool,
|
||||
@@ -161,6 +165,7 @@ enum DiffBasesChange {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
enum DiffKind {
|
||||
Unstaged,
|
||||
Staged,
|
||||
Uncommitted,
|
||||
SinceOid(Option<git::Oid>),
|
||||
}
|
||||
@@ -931,6 +936,53 @@ impl GitStore {
|
||||
cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
|
||||
}
|
||||
|
||||
pub fn open_staged_diff(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<BufferDiff>>> {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
|
||||
if let Some(diff_state) = self.diffs.get(&buffer_id)
|
||||
&& let Some(staged_diff) = diff_state.read(cx).staged_diff()
|
||||
{
|
||||
if let Some(task) =
|
||||
diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
|
||||
{
|
||||
return cx.background_executor().spawn(async move {
|
||||
task.await;
|
||||
Ok(staged_diff)
|
||||
});
|
||||
}
|
||||
return Task::ready(Ok(staged_diff));
|
||||
}
|
||||
|
||||
let Some((repo, repo_path)) =
|
||||
self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("failed to find git repository for buffer")));
|
||||
};
|
||||
|
||||
let task = self
|
||||
.loading_diffs
|
||||
.entry((buffer_id, DiffKind::Staged))
|
||||
.or_insert_with(|| {
|
||||
let changes = repo.update(cx, |repo, cx| {
|
||||
repo.load_committed_text(buffer_id, repo_path, cx)
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
Self::open_diff_internal(this, DiffKind::Staged, changes.await, buffer, cx)
|
||||
.await
|
||||
.map_err(Arc::new)
|
||||
})
|
||||
.shared()
|
||||
})
|
||||
.clone();
|
||||
|
||||
cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
|
||||
}
|
||||
|
||||
pub fn open_diff_since(
|
||||
&mut self,
|
||||
oid: Option<git::Oid>,
|
||||
@@ -968,29 +1020,28 @@ impl GitStore {
|
||||
buffer.update(cx, |buffer, _| buffer.language_registry());
|
||||
let content: Option<Arc<str>> = match oid {
|
||||
None => None,
|
||||
Some(oid) => Some(
|
||||
repo.update(cx, |repo, cx| repo.load_blob_content(oid, cx))
|
||||
.await?
|
||||
.into(),
|
||||
),
|
||||
Some(oid) => Some({
|
||||
let mut content = repo
|
||||
.update(cx, |repo, cx| repo.load_blob_content(oid, cx))
|
||||
.await?;
|
||||
text::LineEnding::normalize(&mut content);
|
||||
content.into()
|
||||
}),
|
||||
};
|
||||
let buffer_diff = cx.new(|cx| BufferDiff::new(&buffer_snapshot, cx));
|
||||
let buffer_diff = cx.new(|cx| {
|
||||
BufferDiff::new(
|
||||
&buffer_snapshot,
|
||||
buffer_snapshot.language().cloned(),
|
||||
language_registry,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
buffer_diff
|
||||
.update(cx, |buffer_diff, cx| {
|
||||
buffer_diff.language_changed(
|
||||
buffer_snapshot.language().cloned(),
|
||||
language_registry,
|
||||
cx,
|
||||
);
|
||||
buffer_diff.set_base_text(
|
||||
content.clone(),
|
||||
buffer_snapshot.language().cloned(),
|
||||
buffer_snapshot.text,
|
||||
cx,
|
||||
)
|
||||
buffer_diff.set_base_text(content.clone(), buffer_snapshot.text, cx)
|
||||
})
|
||||
.await?;
|
||||
.await;
|
||||
let unstaged_diff = this
|
||||
.update(cx, |this, cx| this.open_unstaged_diff(buffer.clone(), cx))?
|
||||
.await?;
|
||||
@@ -1008,7 +1059,7 @@ impl GitStore {
|
||||
let diff_state = this
|
||||
.diffs
|
||||
.entry(buffer_id)
|
||||
.or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
|
||||
.or_insert_with(|| cx.new(|cx| BufferGitState::new(git_store, cx)));
|
||||
|
||||
diff_state.update(cx, |state, _| {
|
||||
if let Some(oid) = oid {
|
||||
@@ -1122,25 +1173,99 @@ impl GitStore {
|
||||
let diff_state = this
|
||||
.diffs
|
||||
.entry(buffer_id)
|
||||
.or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
|
||||
.or_insert_with(|| cx.new(|cx| BufferGitState::new(git_store, cx)));
|
||||
|
||||
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
|
||||
let existing_unstaged_diff = diff_state.read(cx).unstaged_diff();
|
||||
|
||||
cx.subscribe(&diff, Self::on_buffer_diff_event).detach();
|
||||
let mut staged_index_text_buffer = None;
|
||||
let diff = if kind == DiffKind::Unstaged
|
||||
&& let Some(existing_unstaged_diff) = existing_unstaged_diff.clone()
|
||||
{
|
||||
existing_unstaged_diff
|
||||
} else {
|
||||
let diff = match kind {
|
||||
DiffKind::Unstaged => {
|
||||
let base_text_buffer = diff_state.update(cx, |diff_state, cx| {
|
||||
diff_state.get_or_create_index_text_buffer(cx)
|
||||
});
|
||||
cx.new(|cx| {
|
||||
BufferDiff::new_with_base_text_buffer(
|
||||
&text_snapshot,
|
||||
base_text_buffer,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
DiffKind::Staged => {
|
||||
let (index_text_buffer, base_text_buffer) =
|
||||
diff_state.update(cx, |diff_state, cx| {
|
||||
(
|
||||
diff_state.get_or_create_index_text_buffer(cx),
|
||||
diff_state.get_or_create_head_text_buffer(cx),
|
||||
)
|
||||
});
|
||||
index_text_buffer.update(cx, |index_text_buffer, cx| {
|
||||
if let Some(language_registry) = language_registry.clone() {
|
||||
index_text_buffer.set_language_registry(language_registry);
|
||||
}
|
||||
index_text_buffer.set_language_async(language.clone(), cx);
|
||||
});
|
||||
let index_text_snapshot = index_text_buffer.read(cx).text_snapshot();
|
||||
staged_index_text_buffer = Some(index_text_buffer);
|
||||
cx.new(|cx| {
|
||||
BufferDiff::new_with_base_text_buffer(
|
||||
&index_text_snapshot,
|
||||
base_text_buffer,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
DiffKind::Uncommitted => {
|
||||
let base_text_buffer = diff_state.update(cx, |diff_state, cx| {
|
||||
diff_state.get_or_create_head_text_buffer(cx)
|
||||
});
|
||||
cx.new(|cx| {
|
||||
BufferDiff::new_with_base_text_buffer(
|
||||
&text_snapshot,
|
||||
base_text_buffer,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
DiffKind::SinceOid(_) => {
|
||||
unreachable!("open_diff_internal is not used for OID diffs")
|
||||
}
|
||||
};
|
||||
cx.subscribe(&diff, Self::on_buffer_diff_event).detach();
|
||||
diff
|
||||
};
|
||||
diff_state.update(cx, |diff_state, cx| {
|
||||
diff_state.language_changed = true;
|
||||
diff_state.language = language;
|
||||
diff_state.language_registry = language_registry;
|
||||
|
||||
match kind {
|
||||
DiffKind::Unstaged => {
|
||||
diff_state.unstaged_diff.get_or_insert(diff.downgrade());
|
||||
diff_state.unstaged_diff = Some(diff.downgrade());
|
||||
}
|
||||
DiffKind::Staged => {
|
||||
diff_state.index_text_buffer_language_enabled = true;
|
||||
let index_text_buffer = staged_index_text_buffer
|
||||
.take()
|
||||
.context("index text buffer was not created for staged diff")?;
|
||||
diff_state.staged_diff = Some((diff.downgrade(), index_text_buffer));
|
||||
}
|
||||
DiffKind::Uncommitted => {
|
||||
let unstaged_diff = if let Some(diff) = diff_state.unstaged_diff() {
|
||||
let unstaged_diff = if let Some(diff) = existing_unstaged_diff {
|
||||
diff
|
||||
} else {
|
||||
let unstaged_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
|
||||
let base_text_buffer = diff_state.get_or_create_index_text_buffer(cx);
|
||||
let unstaged_diff = cx.new(|cx| {
|
||||
BufferDiff::new_with_base_text_buffer(
|
||||
&text_snapshot,
|
||||
base_text_buffer,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
diff_state.unstaged_diff = Some(unstaged_diff.downgrade());
|
||||
unstaged_diff
|
||||
};
|
||||
@@ -1172,6 +1297,11 @@ impl GitStore {
|
||||
diff_state.read(cx).unstaged_diff.as_ref()?.upgrade()
|
||||
}
|
||||
|
||||
pub fn get_staged_diff(&self, buffer_id: BufferId, cx: &App) -> Option<Entity<BufferDiff>> {
|
||||
let diff_state = self.diffs.get(&buffer_id)?;
|
||||
diff_state.read(cx).staged_diff()
|
||||
}
|
||||
|
||||
pub fn get_uncommitted_diff(
|
||||
&self,
|
||||
buffer_id: BufferId,
|
||||
@@ -1191,6 +1321,17 @@ impl GitStore {
|
||||
diff_state.read(cx).oid_diff(oid)
|
||||
}
|
||||
|
||||
/// Whether this buffer's index text is known to match its committed text
|
||||
/// without comparing contents, i.e. whether the texts share one allocation.
|
||||
/// In a downstream project, this can only be true if the upstream sent
|
||||
/// `Mode::IndexMatchesHead`.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn index_matches_head_for_buffer(&self, buffer_id: BufferId, cx: &App) -> bool {
|
||||
self.diffs
|
||||
.get(&buffer_id)
|
||||
.is_some_and(|diff_state| diff_state.read(cx).index_matches_head())
|
||||
}
|
||||
|
||||
pub fn open_conflict_set(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
@@ -1225,7 +1366,7 @@ impl GitStore {
|
||||
let buffer_git_state = self
|
||||
.diffs
|
||||
.entry(buffer_id)
|
||||
.or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
|
||||
.or_insert_with(|| cx.new(|cx| BufferGitState::new(git_store, cx)));
|
||||
let conflict_set = cx.new(|cx| ConflictSet::new(buffer_id, is_unmerged, cx));
|
||||
|
||||
self._subscriptions
|
||||
@@ -3561,45 +3702,30 @@ impl GitStore {
|
||||
.or_default();
|
||||
shared_diffs.entry(buffer_id).or_default().uncommitted = Some(diff.clone());
|
||||
});
|
||||
Ok(diff.read_with(&cx, |diff, cx| {
|
||||
this.read_with(&cx, |this, cx| {
|
||||
use proto::open_uncommitted_diff_response::Mode;
|
||||
|
||||
let unstaged_diff = diff.secondary_diff();
|
||||
let index_snapshot = unstaged_diff.and_then(|diff| {
|
||||
let diff = diff.read(cx);
|
||||
diff.base_text_exists().then(|| diff.base_text(cx))
|
||||
});
|
||||
let diff_state = this.diffs.get(&buffer_id).context("missing diff state")?;
|
||||
let diff_state = diff_state.read(cx);
|
||||
let index_matches_head = diff_state.index_matches_head();
|
||||
let index_text = diff_state.index_text.clone();
|
||||
let head_text = diff_state.head_text.clone();
|
||||
|
||||
let mode;
|
||||
let staged_text;
|
||||
let committed_text;
|
||||
if diff.base_text_exists() {
|
||||
let committed_snapshot = diff.base_text(cx);
|
||||
committed_text = Some(committed_snapshot.text());
|
||||
if let Some(index_text) = index_snapshot {
|
||||
if index_text.remote_id() == committed_snapshot.remote_id() {
|
||||
mode = Mode::IndexMatchesHead;
|
||||
staged_text = None;
|
||||
} else {
|
||||
mode = Mode::IndexAndHead;
|
||||
staged_text = Some(index_text.text());
|
||||
}
|
||||
} else {
|
||||
mode = Mode::IndexAndHead;
|
||||
staged_text = None;
|
||||
let response = if index_matches_head {
|
||||
proto::OpenUncommittedDiffResponse {
|
||||
committed_text: head_text.map(|head| head.to_string()),
|
||||
staged_text: None,
|
||||
mode: Mode::IndexMatchesHead.into(),
|
||||
}
|
||||
} else {
|
||||
mode = Mode::IndexAndHead;
|
||||
committed_text = None;
|
||||
staged_text = index_snapshot.as_ref().map(|buffer| buffer.text());
|
||||
}
|
||||
|
||||
proto::OpenUncommittedDiffResponse {
|
||||
committed_text,
|
||||
staged_text,
|
||||
mode: mode.into(),
|
||||
}
|
||||
}))
|
||||
proto::OpenUncommittedDiffResponse {
|
||||
committed_text: head_text.map(|head| head.to_string()),
|
||||
staged_text: index_text.map(|index| index.to_string()),
|
||||
mode: Mode::IndexAndHead.into(),
|
||||
}
|
||||
};
|
||||
anyhow::Ok(response)
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_update_diff_bases(
|
||||
@@ -3794,9 +3920,10 @@ impl GitStore {
|
||||
}
|
||||
|
||||
impl BufferGitState {
|
||||
fn new(_git_store: WeakEntity<GitStore>) -> Self {
|
||||
fn new(_git_store: WeakEntity<GitStore>, _cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
unstaged_diff: Default::default(),
|
||||
staged_diff: Default::default(),
|
||||
uncommitted_diff: Default::default(),
|
||||
oid_diffs: Default::default(),
|
||||
recalculate_diff_task: Default::default(),
|
||||
@@ -3808,6 +3935,9 @@ impl BufferGitState {
|
||||
head_text: Default::default(),
|
||||
index_text: Default::default(),
|
||||
oid_texts: Default::default(),
|
||||
head_text_buffer: WeakEntity::new_invalid(),
|
||||
index_text_buffer: WeakEntity::new_invalid(),
|
||||
index_text_buffer_language_enabled: Default::default(),
|
||||
head_changed: Default::default(),
|
||||
index_changed: Default::default(),
|
||||
language_changed: Default::default(),
|
||||
@@ -3817,6 +3947,34 @@ impl BufferGitState {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_or_create_head_text_buffer(&mut self, cx: &mut Context<Self>) -> Entity<Buffer> {
|
||||
if let Some(buffer) = self.head_text_buffer.upgrade() {
|
||||
return buffer;
|
||||
}
|
||||
let head_text = self.head_text.clone();
|
||||
let buffer = cx.new(|cx| {
|
||||
let mut buffer = Buffer::local(head_text.as_deref().unwrap_or(""), cx);
|
||||
buffer.set_capability(Capability::ReadOnly, cx);
|
||||
buffer
|
||||
});
|
||||
self.head_text_buffer = buffer.downgrade();
|
||||
buffer
|
||||
}
|
||||
|
||||
fn get_or_create_index_text_buffer(&mut self, cx: &mut Context<Self>) -> Entity<Buffer> {
|
||||
if let Some(buffer) = self.index_text_buffer.upgrade() {
|
||||
return buffer;
|
||||
}
|
||||
let index_text = self.index_text.clone();
|
||||
let buffer = cx.new(|cx| {
|
||||
let mut buffer = Buffer::local(index_text.as_deref().unwrap_or(""), cx);
|
||||
buffer.set_capability(Capability::ReadOnly, cx);
|
||||
buffer
|
||||
});
|
||||
self.index_text_buffer = buffer.downgrade();
|
||||
buffer
|
||||
}
|
||||
|
||||
#[ztracing::instrument(skip_all)]
|
||||
fn buffer_language_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.language = buffer.read(cx).language().cloned();
|
||||
@@ -3876,6 +4034,10 @@ impl BufferGitState {
|
||||
self.unstaged_diff.as_ref().and_then(|set| set.upgrade())
|
||||
}
|
||||
|
||||
fn staged_diff(&self) -> Option<Entity<BufferDiff>> {
|
||||
self.staged_diff.as_ref().and_then(|(set, _)| set.upgrade())
|
||||
}
|
||||
|
||||
fn uncommitted_diff(&self) -> Option<Entity<BufferDiff>> {
|
||||
self.uncommitted_diff.as_ref().and_then(|set| set.upgrade())
|
||||
}
|
||||
@@ -3884,6 +4046,19 @@ impl BufferGitState {
|
||||
self.oid_diffs.get(&oid).and_then(|weak| weak.upgrade())
|
||||
}
|
||||
|
||||
/// Whether the index text is known to match the committed text, without
|
||||
/// comparing their contents. Always true when both texts were set by a
|
||||
/// single `DiffBasesChange::SetBoth`, which shares one allocation between
|
||||
/// them. May be false even when the contents are equal, if the texts were
|
||||
/// loaded separately.
|
||||
fn index_matches_head(&self) -> bool {
|
||||
match (self.index_text.as_ref(), self.head_text.as_ref()) {
|
||||
(Some(index), Some(head)) => Arc::ptr_eq(index, head),
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_base_texts_updated(
|
||||
&mut self,
|
||||
buffer: text::BufferSnapshot,
|
||||
@@ -3981,25 +4156,35 @@ impl BufferGitState {
|
||||
let language = self.language.clone();
|
||||
let language_registry = self.language_registry.clone();
|
||||
let unstaged_diff = self.unstaged_diff();
|
||||
let staged_diff = self.staged_diff();
|
||||
let uncommitted_diff = self.uncommitted_diff();
|
||||
let head = self.head_text.clone();
|
||||
let index = self.index_text.clone();
|
||||
let head_text_buffer = self.head_text_buffer.upgrade();
|
||||
let index_text_buffer = self.index_text_buffer.upgrade();
|
||||
let index_text_buffer_language_enabled = self.index_text_buffer_language_enabled;
|
||||
let index_changed = self.index_changed;
|
||||
let head_changed = self.head_changed;
|
||||
let language_changed = self.language_changed;
|
||||
let prev_hunk_staging_operation_count = self.hunk_staging_operation_count_as_of_write;
|
||||
let index_matches_head = match (self.index_text.as_ref(), self.head_text.as_ref()) {
|
||||
(Some(index), Some(head)) => Arc::ptr_eq(index, head),
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
};
|
||||
let index_matches_head = self.index_matches_head();
|
||||
|
||||
let oid_diffs: Vec<(Option<git::Oid>, Entity<BufferDiff>, Option<Arc<str>>)> = self
|
||||
let oid_diffs: Vec<(
|
||||
Option<git::Oid>,
|
||||
Entity<BufferDiff>,
|
||||
Entity<Buffer>,
|
||||
Option<Arc<str>>,
|
||||
)> = self
|
||||
.oid_diffs
|
||||
.iter()
|
||||
.filter_map(|(oid, weak)| {
|
||||
let base_text = oid.and_then(|oid| self.oid_texts.get(&oid).cloned());
|
||||
weak.upgrade().map(|diff| (*oid, diff, base_text))
|
||||
let diff = weak.upgrade()?;
|
||||
let base_text_buffer = diff.read(cx).base_text_buffer().clone();
|
||||
let base_text = match oid {
|
||||
Some(oid) => Some(self.oid_texts.get(oid)?.clone()),
|
||||
None => None,
|
||||
};
|
||||
Some((*oid, diff, base_text_buffer, base_text))
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -4012,21 +4197,86 @@ impl BufferGitState {
|
||||
}
|
||||
alive
|
||||
});
|
||||
if self
|
||||
.staged_diff
|
||||
.as_ref()
|
||||
.is_some_and(|(weak, _)| !weak.is_upgradable())
|
||||
{
|
||||
self.staged_diff = None;
|
||||
}
|
||||
self.recalculate_diff_task = Some(cx.spawn(async move |this, cx| {
|
||||
log::debug!(
|
||||
"start recalculating diffs for buffer {}",
|
||||
buffer.remote_id()
|
||||
);
|
||||
|
||||
if index_text_buffer_language_enabled
|
||||
&& let Some(index_text_buffer) = &index_text_buffer
|
||||
{
|
||||
index_text_buffer.update(cx, |index_text_buffer, cx| {
|
||||
if let Some(language_registry) = language_registry.clone() {
|
||||
index_text_buffer.set_language_registry(language_registry);
|
||||
}
|
||||
index_text_buffer.set_language_async(language.clone(), cx);
|
||||
});
|
||||
}
|
||||
if let Some(head_text_buffer) = &head_text_buffer {
|
||||
head_text_buffer.update(cx, |head_text_buffer, cx| {
|
||||
if let Some(language_registry) = language_registry.clone() {
|
||||
head_text_buffer.set_language_registry(language_registry);
|
||||
}
|
||||
head_text_buffer.set_language_async(language.clone(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
for (_, _, base_text_buffer, _) in &oid_diffs {
|
||||
base_text_buffer.update(cx, |base_text_buffer, cx| {
|
||||
if let Some(language_registry) = language_registry.clone() {
|
||||
base_text_buffer.set_language_registry(language_registry);
|
||||
}
|
||||
base_text_buffer.set_language_async(language.clone(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
let mut edited_index_text = None;
|
||||
|
||||
let index_text_snapshot = if let Some(index_text_buffer) = &index_text_buffer
|
||||
&& (unstaged_diff.is_some() || staged_diff.is_some())
|
||||
{
|
||||
let index_text_snapshot = if index_changed || language_changed {
|
||||
let new_index_text = index.clone().unwrap_or_default();
|
||||
let index_text_diff = index_text_buffer
|
||||
.update(cx, |index_text_buffer, cx| {
|
||||
index_text_buffer.diff(new_index_text.clone(), cx)
|
||||
})
|
||||
.await;
|
||||
let edited = index_text_buffer
|
||||
.update(cx, |index_text_buffer, cx| {
|
||||
index_text_buffer.snapshot_with_edits(index_text_diff.edits, cx)
|
||||
})
|
||||
.await;
|
||||
let snapshot = edited.snapshot().clone();
|
||||
edited_index_text = Some(edited);
|
||||
snapshot
|
||||
} else {
|
||||
index_text_buffer.read_with(cx, |buffer, _| buffer.snapshot())
|
||||
};
|
||||
Some(index_text_snapshot)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut new_unstaged_diff = None;
|
||||
if let Some(unstaged_diff) = &unstaged_diff {
|
||||
|
||||
if let (Some(unstaged_diff), Some(index_text_snapshot)) =
|
||||
(unstaged_diff.as_ref(), index_text_snapshot.as_ref())
|
||||
{
|
||||
new_unstaged_diff = Some(
|
||||
cx.update(|cx| {
|
||||
unstaged_diff.read(cx).update_diff(
|
||||
buffer.clone(),
|
||||
index,
|
||||
index_changed.then_some(false),
|
||||
language.clone(),
|
||||
index_text_snapshot,
|
||||
index.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -4038,23 +4288,72 @@ impl BufferGitState {
|
||||
// for a bit
|
||||
yield_now().await;
|
||||
|
||||
let mut edited_head_text = None;
|
||||
let mut new_staged_diff = None;
|
||||
let mut new_uncommitted_diff = None;
|
||||
if let Some(uncommitted_diff) = &uncommitted_diff {
|
||||
new_uncommitted_diff = if index_matches_head {
|
||||
new_unstaged_diff.clone()
|
||||
if let Some(head_text_buffer) = &head_text_buffer
|
||||
&& (staged_diff.is_some() || uncommitted_diff.is_some())
|
||||
{
|
||||
let head_base_text_exists = head.is_some();
|
||||
let head_text_snapshot = if head_changed || language_changed {
|
||||
let new_head_text = head.clone().unwrap_or_default();
|
||||
let head_text_diff = head_text_buffer
|
||||
.update(cx, |head_text_buffer, cx| {
|
||||
head_text_buffer.diff(new_head_text.clone(), cx)
|
||||
})
|
||||
.await;
|
||||
let edited = head_text_buffer
|
||||
.update(cx, |base_text_buffer, cx| {
|
||||
base_text_buffer.snapshot_with_edits(head_text_diff.edits, cx)
|
||||
})
|
||||
.await;
|
||||
let snapshot = edited.snapshot().clone();
|
||||
edited_head_text = Some(edited);
|
||||
snapshot
|
||||
} else {
|
||||
Some(
|
||||
head_text_buffer.read_with(cx, |buffer, _| buffer.snapshot())
|
||||
};
|
||||
if let (Some(staged_diff), Some(index_base_text_snapshot)) =
|
||||
(staged_diff.as_ref(), index_text_snapshot.as_ref())
|
||||
{
|
||||
new_staged_diff = Some(
|
||||
cx.update(|cx| {
|
||||
uncommitted_diff.read(cx).update_diff(
|
||||
buffer.clone(),
|
||||
head,
|
||||
head_changed.then_some(true),
|
||||
language.clone(),
|
||||
staged_diff.read(cx).update_diff(
|
||||
index_base_text_snapshot.text.clone(),
|
||||
&head_text_snapshot,
|
||||
head.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(uncommitted_diff) = &uncommitted_diff {
|
||||
new_uncommitted_diff = if index_matches_head {
|
||||
new_unstaged_diff.clone().map(|mut update| {
|
||||
update.set_base_text_snapshot(
|
||||
head_text_snapshot.clone(),
|
||||
head_base_text_exists,
|
||||
);
|
||||
update
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if new_uncommitted_diff.is_none() {
|
||||
new_uncommitted_diff = Some(
|
||||
cx.update(|cx| {
|
||||
uncommitted_diff.read(cx).update_diff(
|
||||
buffer.clone(),
|
||||
&head_text_snapshot,
|
||||
head.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4087,64 +4386,84 @@ impl BufferGitState {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let unstaged_changed_range = if let Some((unstaged_diff, new_unstaged_diff)) =
|
||||
unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
|
||||
{
|
||||
let task = unstaged_diff.update(cx, |diff, cx| {
|
||||
// For git index buffer we skip assigning the language as we do not really need to perform any syntax highlighting on
|
||||
// it. As a result, by skipping it we are potentially shaving off a lot of RSS plus we get a snappier feel for large diff
|
||||
// view multibuffers.
|
||||
diff.set_snapshot(new_unstaged_diff, &buffer, cx)
|
||||
});
|
||||
Some(task.await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
this.update(cx, |_, cx| {
|
||||
if let (Some(staged_diff), Some(new_staged_diff)) =
|
||||
(staged_diff.as_ref(), new_staged_diff.clone())
|
||||
{
|
||||
staged_diff.update(cx, |diff, cx| {
|
||||
if let Some(edited_base_text) = edited_index_text.take()
|
||||
&& let Some(index_text_buffer) = &index_text_buffer
|
||||
{
|
||||
index_text_buffer.update(cx, |index_text_buffer, cx| {
|
||||
index_text_buffer.fast_forward(edited_base_text, cx)
|
||||
});
|
||||
}
|
||||
if let Some(edited_head_text) = edited_head_text.take()
|
||||
&& let Some(head_text_buffer) = &head_text_buffer
|
||||
{
|
||||
head_text_buffer.update(cx, |head_text_buffer, cx| {
|
||||
head_text_buffer.fast_forward(edited_head_text, cx)
|
||||
});
|
||||
}
|
||||
diff.set_snapshot(new_staged_diff, cx)
|
||||
});
|
||||
}
|
||||
|
||||
yield_now().await;
|
||||
let unstaged_changed_range = if let (Some(unstaged_diff), Some(new_unstaged_diff)) =
|
||||
(unstaged_diff.as_ref(), new_unstaged_diff.clone())
|
||||
{
|
||||
Some(unstaged_diff.update(cx, |diff, cx| {
|
||||
if let Some(edited_index_text) = edited_index_text.take()
|
||||
&& let Some(index_text_buffer) = &index_text_buffer
|
||||
{
|
||||
index_text_buffer.update(cx, |index_text_buffer, cx| {
|
||||
index_text_buffer.fast_forward(edited_index_text, cx)
|
||||
});
|
||||
}
|
||||
diff.set_snapshot(new_unstaged_diff, cx)
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
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.clone(), language_registry.clone(), cx);
|
||||
if let (Some(uncommitted_diff), Some(new_uncommitted_diff)) =
|
||||
(uncommitted_diff.as_ref(), new_uncommitted_diff.clone())
|
||||
{
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
if let Some(edited_base_text) = edited_head_text.take()
|
||||
&& let Some(head_text_buffer) = &head_text_buffer
|
||||
{
|
||||
head_text_buffer.update(cx, |head_text_buffer, cx| {
|
||||
head_text_buffer.fast_forward(edited_base_text, cx)
|
||||
});
|
||||
}
|
||||
diff.set_snapshot_with_secondary(
|
||||
new_uncommitted_diff,
|
||||
&buffer,
|
||||
unstaged_changed_range.flatten(),
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
})?;
|
||||
|
||||
yield_now().await;
|
||||
|
||||
for (oid, oid_diff, base_text) in oid_diffs {
|
||||
for (oid, oid_diff, base_text_buffer, base_text) in oid_diffs {
|
||||
let base_text_snapshot =
|
||||
base_text_buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
let new_oid_diff = cx
|
||||
.update(|cx| {
|
||||
oid_diff.read(cx).update_diff(
|
||||
buffer.clone(),
|
||||
base_text,
|
||||
None,
|
||||
language.clone(),
|
||||
&base_text_snapshot,
|
||||
base_text.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
oid_diff
|
||||
.update(cx, |diff, cx| {
|
||||
if language_changed {
|
||||
diff.language_changed(language.clone(), language_registry.clone(), cx);
|
||||
}
|
||||
diff.set_snapshot(new_oid_diff, &buffer, cx)
|
||||
})
|
||||
.await;
|
||||
oid_diff.update(cx, |diff, cx| diff.set_snapshot(new_oid_diff, cx));
|
||||
|
||||
log::debug!(
|
||||
"finished recalculating oid diff for buffer {} oid {:?}",
|
||||
@@ -4805,6 +5124,10 @@ impl Repository {
|
||||
.unstaged_diff
|
||||
.as_ref()
|
||||
.is_some_and(|diff| diff.is_upgradable());
|
||||
let has_staged_diff = diff_state
|
||||
.staged_diff
|
||||
.as_ref()
|
||||
.is_some_and(|(diff, _)| diff.is_upgradable());
|
||||
let has_uncommitted_diff = diff_state
|
||||
.uncommitted_diff
|
||||
.as_ref()
|
||||
@@ -4814,8 +5137,10 @@ impl Repository {
|
||||
buffer,
|
||||
repo_path,
|
||||
is_symlink,
|
||||
has_unstaged_diff.then(|| diff_state.index_text.clone()),
|
||||
has_uncommitted_diff.then(|| diff_state.head_text.clone()),
|
||||
(has_unstaged_diff || has_staged_diff)
|
||||
.then(|| diff_state.index_text.clone()),
|
||||
(has_staged_diff || has_uncommitted_diff)
|
||||
.then(|| diff_state.head_text.clone()),
|
||||
))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3219,6 +3219,19 @@ impl Project {
|
||||
.update(cx, |git_store, cx| git_store.open_unstaged_diff(buffer, cx))
|
||||
}
|
||||
|
||||
#[ztracing::instrument(skip_all)]
|
||||
pub fn open_staged_diff(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<BufferDiff>>> {
|
||||
if self.is_disconnected(cx) {
|
||||
return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
|
||||
}
|
||||
self.git_store
|
||||
.update(cx, |git_store, cx| git_store.open_staged_diff(buffer, cx))
|
||||
}
|
||||
|
||||
#[ztracing::instrument(skip_all)]
|
||||
pub fn open_uncommitted_diff(
|
||||
&mut self,
|
||||
|
||||
@@ -9198,6 +9198,412 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_reopening_unstaged_diff_after_drop(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let staged_contents = r#"
|
||||
fn main() {
|
||||
println!("hello world");
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
let file_contents = r#"
|
||||
// print goodbye
|
||||
fn main() {
|
||||
println!("goodbye world");
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".git": {},
|
||||
"src": {
|
||||
"main.rs": file_contents,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.set_index_for_repo(Path::new("/dir/.git"), &[("src/main.rs", staged_contents)]);
|
||||
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/src/main.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
|
||||
|
||||
let unstaged_diff = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_unstaged_diff(buffer.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Drop the diff while the buffer (and its git state) stays alive.
|
||||
drop(unstaged_diff);
|
||||
cx.run_until_parked();
|
||||
project.read_with(cx, |project, cx| {
|
||||
assert!(
|
||||
project
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.get_unstaged_diff(buffer_id, cx)
|
||||
.is_none(),
|
||||
"unstaged diff should have been released"
|
||||
);
|
||||
});
|
||||
|
||||
// Reopen the diff. The new entity must be registered in the git store,
|
||||
// and its hunks must be recalculated.
|
||||
let unstaged_diff = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_unstaged_diff(buffer.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
project.read_with(cx, |project, cx| {
|
||||
let registered = project
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.get_unstaged_diff(buffer_id, cx);
|
||||
assert_eq!(
|
||||
registered.as_ref(),
|
||||
Some(&unstaged_diff),
|
||||
"reopened unstaged diff should be registered in the git store"
|
||||
);
|
||||
});
|
||||
|
||||
unstaged_diff.update(cx, |unstaged_diff, cx| {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_hunks(
|
||||
unstaged_diff.snapshot(cx).hunks(&snapshot),
|
||||
&snapshot,
|
||||
&unstaged_diff.base_text_string(cx).unwrap(),
|
||||
&[
|
||||
(0..1, "", "// print goodbye\n", DiffHunkStatus::added_none()),
|
||||
(
|
||||
2..3,
|
||||
" println!(\"hello world\");\n",
|
||||
" println!(\"goodbye world\");\n",
|
||||
DiffHunkStatus::modified_none(),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_staged_diff_for_buffer(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let committed_contents = r#"
|
||||
fn main() {
|
||||
println!("hello world");
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
let staged_contents = r#"
|
||||
// print goodbye
|
||||
fn main() {
|
||||
println!("goodbye world");
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
let file_contents = r#"
|
||||
// print goodbye
|
||||
fn main() {
|
||||
println!("working copy only");
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".git": {},
|
||||
"src": {
|
||||
"main.rs": file_contents,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.set_head_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[("src/main.rs", committed_contents)],
|
||||
"deadbeef",
|
||||
);
|
||||
fs.set_index_for_repo(Path::new("/dir/.git"), &[("src/main.rs", staged_contents)]);
|
||||
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
let language = rust_lang();
|
||||
language_registry.add(language.clone());
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/src/main.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let unstaged_diff = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_unstaged_diff(buffer.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
unstaged_diff.read_with(cx, |diff, cx| {
|
||||
assert_eq!(diff.base_text(cx).language().cloned(), None);
|
||||
});
|
||||
|
||||
let staged_diff = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_staged_diff(buffer.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
unstaged_diff.read_with(cx, |diff, cx| {
|
||||
assert_eq!(
|
||||
diff.base_text(cx).language().cloned(),
|
||||
Some(language.clone())
|
||||
);
|
||||
});
|
||||
staged_diff.update(cx, |staged_diff, cx| {
|
||||
let snapshot = staged_diff.snapshot(cx);
|
||||
let buffer_snapshot = snapshot.buffer_snapshot();
|
||||
assert_hunks(
|
||||
snapshot.hunks(buffer_snapshot),
|
||||
buffer_snapshot,
|
||||
&staged_diff.base_text_string(cx).unwrap(),
|
||||
&[
|
||||
(0..1, "", "// print goodbye\n", DiffHunkStatus::added_none()),
|
||||
(
|
||||
2..3,
|
||||
" println!(\"hello world\");\n",
|
||||
" println!(\"goodbye world\");\n",
|
||||
DiffHunkStatus::modified_none(),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
let staged_contents = r#"
|
||||
// print goodbye
|
||||
fn main() {
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
fs.set_index_for_repo(Path::new("/dir/.git"), &[("src/main.rs", staged_contents)]);
|
||||
|
||||
cx.run_until_parked();
|
||||
staged_diff.update(cx, |staged_diff, cx| {
|
||||
let snapshot = staged_diff.snapshot(cx);
|
||||
let buffer_snapshot = snapshot.buffer_snapshot();
|
||||
assert_hunks(
|
||||
snapshot.hunks(buffer_snapshot),
|
||||
buffer_snapshot,
|
||||
&staged_diff.base_text_string(cx).unwrap(),
|
||||
&[
|
||||
(0..1, "", "// print goodbye\n", DiffHunkStatus::added_none()),
|
||||
(
|
||||
2..2,
|
||||
" println!(\"hello world\");\n",
|
||||
"",
|
||||
DiffHunkStatus::deleted_none(),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_base_text_buffers_released_when_diffs_dropped(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let committed_contents = "one\ntwo\nthree\n";
|
||||
let staged_contents = "one\nTWO\nthree\n";
|
||||
let file_contents = "one\nTWO\nTHREE\n";
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".git": {},
|
||||
"src": {
|
||||
"main.rs": file_contents,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.set_head_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[("src/main.rs", committed_contents.to_owned())],
|
||||
"deadbeef",
|
||||
);
|
||||
fs.set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[("src/main.rs", staged_contents.to_owned())],
|
||||
);
|
||||
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/src/main.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let uncommitted_diff = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_uncommitted_diff(buffer.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let unstaged_diff = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_unstaged_diff(buffer.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let weak_head_text_buffer =
|
||||
uncommitted_diff.read_with(cx, |diff, _| diff.base_text_buffer().downgrade());
|
||||
let weak_index_text_buffer =
|
||||
unstaged_diff.read_with(cx, |diff, _| diff.base_text_buffer().downgrade());
|
||||
|
||||
drop(uncommitted_diff);
|
||||
cx.run_until_parked();
|
||||
cx.update(|_| {});
|
||||
weak_head_text_buffer.assert_released();
|
||||
assert!(
|
||||
weak_index_text_buffer.upgrade().is_some(),
|
||||
"index text buffer should stay alive while the unstaged diff is open"
|
||||
);
|
||||
|
||||
drop(unstaged_diff);
|
||||
cx.run_until_parked();
|
||||
cx.update(|_| {});
|
||||
weak_index_text_buffer.assert_released();
|
||||
|
||||
let uncommitted_diff = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_uncommitted_diff(buffer.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
uncommitted_diff.update(cx, |uncommitted_diff, cx| {
|
||||
assert_eq!(
|
||||
uncommitted_diff.base_text_string(cx).as_deref(),
|
||||
Some(committed_contents),
|
||||
);
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_hunks(
|
||||
uncommitted_diff.snapshot(cx).hunks(&snapshot),
|
||||
&snapshot,
|
||||
&uncommitted_diff.base_text_string(cx).unwrap(),
|
||||
&[(
|
||||
1..3,
|
||||
"two\nthree\n",
|
||||
"TWO\nTHREE\n",
|
||||
DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
|
||||
)],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_staged_diff_without_unstaged_diff(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let committed_contents = "one\ntwo\nthree\n";
|
||||
let staged_contents = "one\nTWO\nthree\n";
|
||||
let file_contents = "one\nTWO\nthree\n";
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".git": {},
|
||||
"src": {
|
||||
"main.rs": file_contents,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.set_head_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[("src/main.rs", committed_contents.to_owned())],
|
||||
"deadbeef",
|
||||
);
|
||||
fs.set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[("src/main.rs", staged_contents.to_owned())],
|
||||
);
|
||||
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/src/main.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let staged_diff = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_staged_diff(buffer.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
staged_diff.update(cx, |staged_diff, cx| {
|
||||
let snapshot = staged_diff.snapshot(cx);
|
||||
let buffer_snapshot = snapshot.buffer_snapshot();
|
||||
assert_hunks(
|
||||
snapshot.hunks(buffer_snapshot),
|
||||
buffer_snapshot,
|
||||
&staged_diff.base_text_string(cx).unwrap(),
|
||||
&[(1..2, "two\n", "TWO\n", DiffHunkStatus::modified_none())],
|
||||
);
|
||||
});
|
||||
|
||||
fs.set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[("src/main.rs", "one\nTWO\nTHREE\n".to_owned())],
|
||||
);
|
||||
cx.run_until_parked();
|
||||
|
||||
staged_diff.update(cx, |staged_diff, cx| {
|
||||
let snapshot = staged_diff.snapshot(cx);
|
||||
let buffer_snapshot = snapshot.buffer_snapshot();
|
||||
assert_hunks(
|
||||
snapshot.hunks(buffer_snapshot),
|
||||
buffer_snapshot,
|
||||
&staged_diff.base_text_string(cx).unwrap(),
|
||||
&[(
|
||||
1..3,
|
||||
"two\nthree\n",
|
||||
"TWO\nTHREE\n",
|
||||
DiffHunkStatus::modified_none(),
|
||||
)],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -9521,6 +9927,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
||||
changed_range: Some(changed_range),
|
||||
base_text_changed_range: _,
|
||||
extended_range: _,
|
||||
base_text_changed: _,
|
||||
}) = event
|
||||
{
|
||||
let changed_range = changed_range.to_point(&snapshot);
|
||||
@@ -9565,10 +9972,11 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
||||
changed_range: Some(changed_range),
|
||||
base_text_changed_range: _,
|
||||
extended_range: _,
|
||||
base_text_changed: _,
|
||||
}) = event
|
||||
{
|
||||
let changed_range = changed_range.to_point(&snapshot);
|
||||
assert_eq!(changed_range, Point::new(0, 0)..Point::new(4, 0));
|
||||
assert_eq!(changed_range, Point::new(1, 0)..Point::new(2, 0));
|
||||
} else {
|
||||
panic!("Unexpected event {event:?}");
|
||||
}
|
||||
@@ -9624,6 +10032,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
||||
changed_range: Some(changed_range),
|
||||
base_text_changed_range: _,
|
||||
extended_range: _,
|
||||
base_text_changed: _,
|
||||
}) = event
|
||||
{
|
||||
let changed_range = changed_range.to_point(&snapshot);
|
||||
@@ -9667,6 +10076,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
||||
changed_range: Some(changed_range),
|
||||
base_text_changed_range: _,
|
||||
extended_range: _,
|
||||
base_text_changed: _,
|
||||
}) = event
|
||||
{
|
||||
let changed_range = changed_range.to_point(&snapshot);
|
||||
@@ -9776,13 +10186,32 @@ async fn test_uncommitted_diff_opened_before_unstaged_diff(cx: &mut gpui::TestAp
|
||||
let unstaged_diff_task = project.update(cx, |project, cx| {
|
||||
project.open_unstaged_diff(buffer.clone(), cx)
|
||||
});
|
||||
let (uncommitted_diff, _unstaged_diff) =
|
||||
let (uncommitted_diff, unstaged_diff) =
|
||||
futures::future::join(uncommitted_diff_task, unstaged_diff_task).await;
|
||||
let uncommitted_diff = uncommitted_diff.unwrap();
|
||||
let _unstaged_diff = _unstaged_diff.unwrap();
|
||||
let unstaged_diff = unstaged_diff.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
uncommitted_diff.read_with(cx, |diff, _| {
|
||||
assert_eq!(
|
||||
diff.secondary_diff(),
|
||||
Some(unstaged_diff.clone()),
|
||||
"the unstaged diff returned to callers should be the uncommitted diff's secondary"
|
||||
);
|
||||
});
|
||||
project.read_with(cx, |project, cx| {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
assert_eq!(
|
||||
project
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.get_unstaged_diff(buffer_id, cx),
|
||||
Some(unstaged_diff.clone()),
|
||||
"the unstaged diff returned to callers should be the registered one"
|
||||
);
|
||||
});
|
||||
|
||||
uncommitted_diff.read_with(cx, |diff, cx| {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_hunks(
|
||||
|
||||
+249
-200
@@ -894,163 +894,11 @@ impl Buffer {
|
||||
edits: impl ExactSizeIterator<Item = (Range<S>, T)>,
|
||||
timestamp: clock::Lamport,
|
||||
) -> EditOperation {
|
||||
let mut edits_patch = Patch::default();
|
||||
let mut edit_op = EditOperation {
|
||||
timestamp,
|
||||
version: self.version(),
|
||||
ranges: Vec::with_capacity(edits.len()),
|
||||
new_text: Vec::with_capacity(edits.len()),
|
||||
};
|
||||
let mut new_insertions = Vec::new();
|
||||
let mut insertion_offset: u32 = 0;
|
||||
let mut insertion_slices = Vec::new();
|
||||
|
||||
let mut edits = edits
|
||||
.map(|(range, new_text)| (range.to_offset(&*self), new_text))
|
||||
.peekable();
|
||||
|
||||
let mut new_ropes =
|
||||
RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0));
|
||||
let mut old_fragments = self.fragments.cursor::<FragmentTextSummary>(&None);
|
||||
let mut new_fragments =
|
||||
FragmentBuilder::new(old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right));
|
||||
new_ropes.append(new_fragments.summary().text);
|
||||
|
||||
let mut fragment_start = old_fragments.start().visible;
|
||||
for (range, new_text) in edits {
|
||||
let new_text = LineEnding::normalize_arc(new_text.into());
|
||||
let fragment_end = old_fragments.end().visible;
|
||||
|
||||
// If the current fragment ends before this range, then jump ahead to the first fragment
|
||||
// that extends past the start of this range, reusing any intervening fragments.
|
||||
if fragment_end < range.start {
|
||||
// If the current fragment has been partially consumed, then consume the rest of it
|
||||
// and advance to the next fragment before slicing.
|
||||
if fragment_start > old_fragments.start().visible {
|
||||
if fragment_end > fragment_start {
|
||||
let mut suffix = old_fragments.item().unwrap().clone();
|
||||
suffix.len = (fragment_end - fragment_start) as u32;
|
||||
suffix.insertion_offset +=
|
||||
(fragment_start - old_fragments.start().visible) as u32;
|
||||
new_insertions.push(InsertionFragment::insert_new(&suffix));
|
||||
new_ropes.push_fragment(&suffix, suffix.visible);
|
||||
new_fragments.push(suffix, &None);
|
||||
}
|
||||
old_fragments.next();
|
||||
}
|
||||
|
||||
let slice = old_fragments.slice(&range.start, Bias::Right);
|
||||
new_ropes.append(slice.summary().text);
|
||||
new_fragments.append(slice, &None);
|
||||
fragment_start = old_fragments.start().visible;
|
||||
}
|
||||
|
||||
let full_range_start = FullOffset(range.start + old_fragments.start().deleted);
|
||||
|
||||
// Preserve any portion of the current fragment that precedes this range.
|
||||
if fragment_start < range.start {
|
||||
let mut prefix = old_fragments.item().unwrap().clone();
|
||||
prefix.len = (range.start - fragment_start) as u32;
|
||||
prefix.insertion_offset += (fragment_start - old_fragments.start().visible) as u32;
|
||||
prefix.id = Locator::between(&new_fragments.summary().max_id, &prefix.id);
|
||||
new_insertions.push(InsertionFragment::insert_new(&prefix));
|
||||
new_ropes.push_fragment(&prefix, prefix.visible);
|
||||
new_fragments.push(prefix, &None);
|
||||
fragment_start = range.start;
|
||||
}
|
||||
|
||||
// Insert the new text before any existing fragments within the range.
|
||||
if !new_text.is_empty() {
|
||||
let new_start = new_fragments.summary().text.visible;
|
||||
|
||||
let next_fragment_id = old_fragments
|
||||
.item()
|
||||
.map_or(Locator::max_ref(), |old_fragment| &old_fragment.id);
|
||||
Self::push_fragments_for_insertion(
|
||||
new_text.as_ref(),
|
||||
timestamp,
|
||||
&mut insertion_offset,
|
||||
&mut new_fragments,
|
||||
&mut new_insertions,
|
||||
&mut insertion_slices,
|
||||
&mut new_ropes,
|
||||
next_fragment_id,
|
||||
timestamp,
|
||||
);
|
||||
edits_patch.push(Edit {
|
||||
old: fragment_start..fragment_start,
|
||||
new: new_start..new_start + new_text.len(),
|
||||
});
|
||||
}
|
||||
|
||||
// Advance through every fragment that intersects this range, marking the intersecting
|
||||
// portions as deleted.
|
||||
while fragment_start < range.end {
|
||||
let fragment = old_fragments.item().unwrap();
|
||||
let fragment_end = old_fragments.end().visible;
|
||||
let mut intersection = fragment.clone();
|
||||
let intersection_end = cmp::min(range.end, fragment_end);
|
||||
if fragment.visible {
|
||||
intersection.len = (intersection_end - fragment_start) as u32;
|
||||
intersection.insertion_offset +=
|
||||
(fragment_start - old_fragments.start().visible) as u32;
|
||||
intersection.id =
|
||||
Locator::between(&new_fragments.summary().max_id, &intersection.id);
|
||||
intersection.deletions.push(timestamp);
|
||||
intersection.visible = false;
|
||||
}
|
||||
if intersection.len > 0 {
|
||||
if fragment.visible && !intersection.visible {
|
||||
let new_start = new_fragments.summary().text.visible;
|
||||
edits_patch.push(Edit {
|
||||
old: fragment_start..intersection_end,
|
||||
new: new_start..new_start,
|
||||
});
|
||||
insertion_slices
|
||||
.push(InsertionSlice::from_fragment(timestamp, &intersection));
|
||||
}
|
||||
new_insertions.push(InsertionFragment::insert_new(&intersection));
|
||||
new_ropes.push_fragment(&intersection, fragment.visible);
|
||||
new_fragments.push(intersection, &None);
|
||||
fragment_start = intersection_end;
|
||||
}
|
||||
if fragment_end <= range.end {
|
||||
old_fragments.next();
|
||||
}
|
||||
}
|
||||
|
||||
let full_range_end = FullOffset(range.end + old_fragments.start().deleted);
|
||||
edit_op.ranges.push(full_range_start..full_range_end);
|
||||
edit_op.new_text.push(new_text);
|
||||
}
|
||||
|
||||
// If the current fragment has been partially consumed, then consume the rest of it
|
||||
// and advance to the next fragment before slicing.
|
||||
if fragment_start > old_fragments.start().visible {
|
||||
let fragment_end = old_fragments.end().visible;
|
||||
if fragment_end > fragment_start {
|
||||
let mut suffix = old_fragments.item().unwrap().clone();
|
||||
suffix.len = (fragment_end - fragment_start) as u32;
|
||||
suffix.insertion_offset += (fragment_start - old_fragments.start().visible) as u32;
|
||||
new_insertions.push(InsertionFragment::insert_new(&suffix));
|
||||
new_ropes.push_fragment(&suffix, suffix.visible);
|
||||
new_fragments.push(suffix, &None);
|
||||
}
|
||||
old_fragments.next();
|
||||
}
|
||||
|
||||
let suffix = old_fragments.suffix();
|
||||
new_ropes.append(suffix.summary().text);
|
||||
new_fragments.append(suffix, &None);
|
||||
let (visible_text, deleted_text) = new_ropes.finish();
|
||||
drop(old_fragments);
|
||||
|
||||
self.snapshot.fragments = new_fragments.to_sum_tree(&None);
|
||||
self.snapshot.insertions.edit(new_insertions, ());
|
||||
self.snapshot.visible_text = visible_text;
|
||||
self.snapshot.deleted_text = deleted_text;
|
||||
let edits: Vec<_> = edits
|
||||
.map(|(range, new_text)| (range.to_offset(&*self), new_text.into()))
|
||||
.collect();
|
||||
let (edit_op, edits_patch) = self.snapshot.apply_edit_internal(edits, timestamp);
|
||||
self.subscriptions.publish_mut(&edits_patch);
|
||||
self.snapshot.insertion_slices.extend(insertion_slices);
|
||||
edit_op
|
||||
}
|
||||
|
||||
@@ -1213,7 +1061,7 @@ impl Buffer {
|
||||
let next_fragment_id = old_fragments
|
||||
.item()
|
||||
.map_or(Locator::max_ref(), |old_fragment| &old_fragment.id);
|
||||
Self::push_fragments_for_insertion(
|
||||
push_fragments_for_insertion(
|
||||
new_text,
|
||||
timestamp,
|
||||
&mut insertion_offset,
|
||||
@@ -1301,49 +1149,6 @@ impl Buffer {
|
||||
self.subscriptions.publish_mut(&edits_patch)
|
||||
}
|
||||
|
||||
fn push_fragments_for_insertion(
|
||||
new_text: &str,
|
||||
timestamp: clock::Lamport,
|
||||
insertion_offset: &mut u32,
|
||||
new_fragments: &mut FragmentBuilder,
|
||||
new_insertions: &mut Vec<sum_tree::Edit<InsertionFragment>>,
|
||||
insertion_slices: &mut Vec<InsertionSlice>,
|
||||
new_ropes: &mut RopeBuilder,
|
||||
next_fragment_id: &Locator,
|
||||
edit_timestamp: clock::Lamport,
|
||||
) {
|
||||
let mut text_offset = 0;
|
||||
while text_offset < new_text.len() {
|
||||
let target_end = new_text.len().min(text_offset + MAX_INSERTION_LEN);
|
||||
let chunk_end = if target_end == new_text.len() {
|
||||
target_end
|
||||
} else {
|
||||
new_text.floor_char_boundary(target_end)
|
||||
};
|
||||
if chunk_end == text_offset {
|
||||
break;
|
||||
}
|
||||
let chunk_len = chunk_end - text_offset;
|
||||
|
||||
let fragment = Fragment {
|
||||
id: Locator::between(&new_fragments.summary().max_id, next_fragment_id),
|
||||
timestamp,
|
||||
insertion_offset: *insertion_offset,
|
||||
len: chunk_len as u32,
|
||||
deletions: Default::default(),
|
||||
max_undos: Default::default(),
|
||||
visible: true,
|
||||
};
|
||||
insertion_slices.push(InsertionSlice::from_fragment(edit_timestamp, &fragment));
|
||||
new_insertions.push(InsertionFragment::insert_new(&fragment));
|
||||
new_fragments.push(fragment, &None);
|
||||
|
||||
*insertion_offset += chunk_len as u32;
|
||||
text_offset = chunk_end;
|
||||
}
|
||||
new_ropes.push_str(new_text);
|
||||
}
|
||||
|
||||
fn fragment_ids_for_edits<'a>(
|
||||
&'a self,
|
||||
edit_ids: impl Iterator<Item = &'a clock::Lamport>,
|
||||
@@ -1832,6 +1637,51 @@ impl Buffer {
|
||||
pub fn set_group_interval(&mut self, group_interval: Duration) {
|
||||
self.history.group_interval = group_interval;
|
||||
}
|
||||
|
||||
pub fn snapshot_with_edits<I, S, T>(&mut self, edits: I) -> EditedBufferSnapshot
|
||||
where
|
||||
I: IntoIterator<Item = (Range<S>, T)>,
|
||||
S: ToOffset,
|
||||
T: Into<Arc<str>>,
|
||||
{
|
||||
let mut snapshot = self.snapshot.clone();
|
||||
let base_version = self.version();
|
||||
let edits: Vec<_> = edits
|
||||
.into_iter()
|
||||
.map(|(range, new_text)| (range.to_offset(&snapshot), new_text.into()))
|
||||
.collect();
|
||||
if edits.is_empty() {
|
||||
return EditedBufferSnapshot {
|
||||
base_version,
|
||||
snapshot,
|
||||
did_edit: false,
|
||||
};
|
||||
}
|
||||
let timestamp = self.lamport_clock.tick();
|
||||
snapshot.apply_edit_internal(edits, timestamp);
|
||||
snapshot.version.observe(timestamp);
|
||||
EditedBufferSnapshot {
|
||||
base_version,
|
||||
snapshot,
|
||||
did_edit: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fast_forward(&mut self, edited: EditedBufferSnapshot) {
|
||||
if self.version.changed_since(&edited.base_version) {
|
||||
panic!("buffer cannot be fast-forwarded")
|
||||
}
|
||||
self.snapshot = edited.snapshot.clone();
|
||||
for timestamp in edited.snapshot.version.iter() {
|
||||
self.lamport_clock.observe(timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditedBufferSnapshot {
|
||||
pub base_version: clock::Global,
|
||||
pub snapshot: BufferSnapshot,
|
||||
pub did_edit: bool,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -2011,6 +1861,49 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
fn push_fragments_for_insertion(
|
||||
new_text: &str,
|
||||
timestamp: clock::Lamport,
|
||||
insertion_offset: &mut u32,
|
||||
new_fragments: &mut FragmentBuilder,
|
||||
new_insertions: &mut Vec<sum_tree::Edit<InsertionFragment>>,
|
||||
insertion_slices: &mut Vec<InsertionSlice>,
|
||||
new_ropes: &mut RopeBuilder,
|
||||
next_fragment_id: &Locator,
|
||||
edit_timestamp: clock::Lamport,
|
||||
) {
|
||||
let mut text_offset = 0;
|
||||
while text_offset < new_text.len() {
|
||||
let target_end = new_text.len().min(text_offset + MAX_INSERTION_LEN);
|
||||
let chunk_end = if target_end == new_text.len() {
|
||||
target_end
|
||||
} else {
|
||||
new_text.floor_char_boundary(target_end)
|
||||
};
|
||||
if chunk_end == text_offset {
|
||||
break;
|
||||
}
|
||||
let chunk_len = chunk_end - text_offset;
|
||||
|
||||
let fragment = Fragment {
|
||||
id: Locator::between(&new_fragments.summary().max_id, next_fragment_id),
|
||||
timestamp,
|
||||
insertion_offset: *insertion_offset,
|
||||
len: chunk_len as u32,
|
||||
deletions: Default::default(),
|
||||
max_undos: Default::default(),
|
||||
visible: true,
|
||||
};
|
||||
insertion_slices.push(InsertionSlice::from_fragment(edit_timestamp, &fragment));
|
||||
new_insertions.push(InsertionFragment::insert_new(&fragment));
|
||||
new_fragments.push(fragment, &None);
|
||||
|
||||
*insertion_offset += chunk_len as u32;
|
||||
text_offset = chunk_end;
|
||||
}
|
||||
new_ropes.push_str(new_text);
|
||||
}
|
||||
|
||||
impl Deref for Buffer {
|
||||
type Target = BufferSnapshot;
|
||||
|
||||
@@ -2020,6 +1913,162 @@ impl Deref for Buffer {
|
||||
}
|
||||
|
||||
impl BufferSnapshot {
|
||||
fn apply_edit_internal(
|
||||
&mut self,
|
||||
edits: Vec<(Range<usize>, Arc<str>)>,
|
||||
timestamp: clock::Lamport,
|
||||
) -> (EditOperation, Patch<usize>) {
|
||||
let mut edits_patch = Patch::default();
|
||||
let mut edit_op = EditOperation {
|
||||
timestamp,
|
||||
version: self.version.clone(),
|
||||
ranges: Vec::with_capacity(edits.len()),
|
||||
new_text: Vec::with_capacity(edits.len()),
|
||||
};
|
||||
let mut new_insertions = Vec::new();
|
||||
let mut insertion_offset: u32 = 0;
|
||||
let mut insertion_slices = Vec::new();
|
||||
|
||||
let mut edits = edits.into_iter().peekable();
|
||||
|
||||
if edits.peek().is_none() {
|
||||
return (edit_op, edits_patch);
|
||||
}
|
||||
|
||||
let mut new_ropes =
|
||||
RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0));
|
||||
let mut old_fragments = self.fragments.cursor::<FragmentTextSummary>(&None);
|
||||
let mut new_fragments =
|
||||
FragmentBuilder::new(old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right));
|
||||
new_ropes.append(new_fragments.summary().text);
|
||||
|
||||
let mut fragment_start = old_fragments.start().visible;
|
||||
for (range, new_text) in edits {
|
||||
let new_text: Arc<str> = LineEnding::normalize_arc(new_text);
|
||||
let fragment_end = old_fragments.end().visible;
|
||||
|
||||
if fragment_end < range.start {
|
||||
if fragment_start > old_fragments.start().visible {
|
||||
if fragment_end > fragment_start {
|
||||
let mut suffix = old_fragments.item().unwrap().clone();
|
||||
suffix.len = (fragment_end - fragment_start) as u32;
|
||||
suffix.insertion_offset +=
|
||||
(fragment_start - old_fragments.start().visible) as u32;
|
||||
new_insertions.push(InsertionFragment::insert_new(&suffix));
|
||||
new_ropes.push_fragment(&suffix, suffix.visible);
|
||||
new_fragments.push(suffix, &None);
|
||||
}
|
||||
old_fragments.next();
|
||||
}
|
||||
|
||||
let slice = old_fragments.slice(&range.start, Bias::Right);
|
||||
new_ropes.append(slice.summary().text);
|
||||
new_fragments.append(slice, &None);
|
||||
fragment_start = old_fragments.start().visible;
|
||||
}
|
||||
|
||||
let full_range_start = FullOffset(range.start + old_fragments.start().deleted);
|
||||
|
||||
if fragment_start < range.start {
|
||||
let mut prefix = old_fragments.item().unwrap().clone();
|
||||
prefix.len = (range.start - fragment_start) as u32;
|
||||
prefix.insertion_offset += (fragment_start - old_fragments.start().visible) as u32;
|
||||
prefix.id = Locator::between(&new_fragments.summary().max_id, &prefix.id);
|
||||
new_insertions.push(InsertionFragment::insert_new(&prefix));
|
||||
new_ropes.push_fragment(&prefix, prefix.visible);
|
||||
new_fragments.push(prefix, &None);
|
||||
fragment_start = range.start;
|
||||
}
|
||||
|
||||
if !new_text.is_empty() {
|
||||
let new_start = new_fragments.summary().text.visible;
|
||||
|
||||
let next_fragment_id = old_fragments
|
||||
.item()
|
||||
.map_or(Locator::max_ref(), |old_fragment| &old_fragment.id);
|
||||
push_fragments_for_insertion(
|
||||
new_text.as_ref(),
|
||||
timestamp,
|
||||
&mut insertion_offset,
|
||||
&mut new_fragments,
|
||||
&mut new_insertions,
|
||||
&mut insertion_slices,
|
||||
&mut new_ropes,
|
||||
next_fragment_id,
|
||||
timestamp,
|
||||
);
|
||||
edits_patch.push(Edit {
|
||||
old: fragment_start..fragment_start,
|
||||
new: new_start..new_start + new_text.len(),
|
||||
});
|
||||
}
|
||||
|
||||
while fragment_start < range.end {
|
||||
let fragment = old_fragments.item().unwrap();
|
||||
let fragment_end = old_fragments.end().visible;
|
||||
let mut intersection = fragment.clone();
|
||||
let intersection_end = cmp::min(range.end, fragment_end);
|
||||
if fragment.visible {
|
||||
intersection.len = (intersection_end - fragment_start) as u32;
|
||||
intersection.insertion_offset +=
|
||||
(fragment_start - old_fragments.start().visible) as u32;
|
||||
intersection.id =
|
||||
Locator::between(&new_fragments.summary().max_id, &intersection.id);
|
||||
intersection.deletions.push(timestamp);
|
||||
intersection.visible = false;
|
||||
}
|
||||
if intersection.len > 0 {
|
||||
if fragment.visible && !intersection.visible {
|
||||
let new_start = new_fragments.summary().text.visible;
|
||||
edits_patch.push(Edit {
|
||||
old: fragment_start..intersection_end,
|
||||
new: new_start..new_start,
|
||||
});
|
||||
insertion_slices
|
||||
.push(InsertionSlice::from_fragment(timestamp, &intersection));
|
||||
}
|
||||
new_insertions.push(InsertionFragment::insert_new(&intersection));
|
||||
new_ropes.push_fragment(&intersection, fragment.visible);
|
||||
new_fragments.push(intersection, &None);
|
||||
fragment_start = intersection_end;
|
||||
}
|
||||
if fragment_end <= range.end {
|
||||
old_fragments.next();
|
||||
}
|
||||
}
|
||||
|
||||
let full_range_end = FullOffset(range.end + old_fragments.start().deleted);
|
||||
edit_op.ranges.push(full_range_start..full_range_end);
|
||||
edit_op.new_text.push(new_text);
|
||||
}
|
||||
|
||||
if fragment_start > old_fragments.start().visible {
|
||||
let fragment_end = old_fragments.end().visible;
|
||||
if fragment_end > fragment_start {
|
||||
let mut suffix = old_fragments.item().unwrap().clone();
|
||||
suffix.len = (fragment_end - fragment_start) as u32;
|
||||
suffix.insertion_offset += (fragment_start - old_fragments.start().visible) as u32;
|
||||
new_insertions.push(InsertionFragment::insert_new(&suffix));
|
||||
new_ropes.push_fragment(&suffix, suffix.visible);
|
||||
new_fragments.push(suffix, &None);
|
||||
}
|
||||
old_fragments.next();
|
||||
}
|
||||
|
||||
let suffix = old_fragments.suffix();
|
||||
new_ropes.append(suffix.summary().text);
|
||||
new_fragments.append(suffix, &None);
|
||||
let (visible_text, deleted_text) = new_ropes.finish();
|
||||
drop(old_fragments);
|
||||
|
||||
self.fragments = new_fragments.to_sum_tree(&None);
|
||||
self.insertions.edit(new_insertions, ());
|
||||
self.visible_text = visible_text;
|
||||
self.deleted_text = deleted_text;
|
||||
self.insertion_slices.extend(insertion_slices);
|
||||
(edit_op, edits_patch)
|
||||
}
|
||||
|
||||
pub fn as_rope(&self) -> &Rope {
|
||||
&self.visible_text
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user