mirror of
https://github.com/encounter/zed.git
synced 2026-07-11 06:19:04 -07:00
Restructure git diff state management to allow viewing buffers with different diff bases (#21258)
This is a pure refactor of our Git diff state management. Buffers are no longer are associated with one single diff (the unstaged changes). Instead, there is an explicit project API for retrieving a buffer's unstaged changes, and the `Editor` view layer is responsible for choosing what diff to associate with a buffer. The reason for this change is that we'll soon want to add multiple "git diff views" to Zed, one of which will show the *uncommitted* changes for a buffer. But that view will need to co-exist with other views of the same buffer, which may want to show the unstaged changes. ### Todo * [x] Get git gutter and git hunks working with new structure * [x] Update editor tests to use new APIs * [x] Update buffer tests * [x] Restructure remoting/collab protocol * [x] Update assertions about staged text in `random_project_collaboration_tests` * [x] Move buffer tests for git diff management to a new spot, using the new APIs Release Notes: - N/A --------- Co-authored-by: Richard <richard@zed.dev> Co-authored-by: Cole <cole@zed.dev> Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
co-authored by
Richard
Cole
Conrad
parent
31796171de
commit
a2115e7242
Generated
-2
@@ -4995,7 +4995,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"clock",
|
||||
"collections",
|
||||
"derive_more",
|
||||
"git2",
|
||||
@@ -6534,7 +6533,6 @@ dependencies = [
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"globset",
|
||||
"gpui",
|
||||
"http_client",
|
||||
|
||||
@@ -673,6 +673,7 @@ new_ret_no_self = { level = "allow" }
|
||||
# We have a few `next` functions that differ in lifetimes
|
||||
# compared to Iterator::next. Yet, clippy complains about those.
|
||||
should_implement_trait = { level = "allow" }
|
||||
let_underscore_future = "allow"
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]
|
||||
|
||||
@@ -309,6 +309,7 @@ impl Server {
|
||||
.add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetStagedText>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::UpdateGitBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
|
||||
.add_request_handler(
|
||||
|
||||
@@ -2561,19 +2561,23 @@ async fn test_git_diff_base_change(
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let change_set_local_a = project_local
|
||||
.update(cx_a, |p, cx| {
|
||||
p.open_unstaged_changes(buffer_local_a.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait for it to catch up to the new diff
|
||||
executor.run_until_parked();
|
||||
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_local_a.read_with(cx_a, |buffer, _| {
|
||||
change_set_local_a.read_with(cx_a, |change_set, cx| {
|
||||
let buffer = buffer_local_a.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&[(1..2, "", "two\n")],
|
||||
@@ -2585,25 +2589,30 @@ async fn test_git_diff_base_change(
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let change_set_remote_a = project_remote
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_unstaged_changes(buffer_remote_a.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait remote buffer to catch up to the new diff
|
||||
executor.run_until_parked();
|
||||
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_remote_a.read_with(cx_b, |buffer, _| {
|
||||
change_set_remote_a.read_with(cx_b, |change_set, cx| {
|
||||
let buffer = buffer_remote_a.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&[(1..2, "", "two\n")],
|
||||
);
|
||||
});
|
||||
|
||||
// Update the staged text of the open buffer
|
||||
client_a.fs().set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), new_diff_base.clone())],
|
||||
@@ -2611,40 +2620,35 @@ async fn test_git_diff_base_change(
|
||||
|
||||
// Wait for buffer_local_a to receive it
|
||||
executor.run_until_parked();
|
||||
|
||||
// Smoke test new diffing
|
||||
|
||||
buffer_local_a.read_with(cx_a, |buffer, _| {
|
||||
change_set_local_a.read_with(cx_a, |change_set, cx| {
|
||||
let buffer = buffer_local_a.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&new_diff_base,
|
||||
&[(2..3, "", "three\n")],
|
||||
);
|
||||
});
|
||||
|
||||
// Smoke test B
|
||||
|
||||
buffer_remote_a.read_with(cx_b, |buffer, _| {
|
||||
change_set_remote_a.read_with(cx_b, |change_set, cx| {
|
||||
let buffer = buffer_remote_a.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&new_diff_base,
|
||||
&[(2..3, "", "three\n")],
|
||||
);
|
||||
});
|
||||
|
||||
//Nested git dir
|
||||
|
||||
// Nested git dir
|
||||
let diff_base = "
|
||||
one
|
||||
three
|
||||
@@ -2667,19 +2671,23 @@ async fn test_git_diff_base_change(
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let change_set_local_b = project_local
|
||||
.update(cx_a, |p, cx| {
|
||||
p.open_unstaged_changes(buffer_local_b.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait for it to catch up to the new diff
|
||||
executor.run_until_parked();
|
||||
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_local_b.read_with(cx_a, |buffer, _| {
|
||||
change_set_local_b.read_with(cx_a, |change_set, cx| {
|
||||
let buffer = buffer_local_b.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&[(1..2, "", "two\n")],
|
||||
@@ -2691,25 +2699,29 @@ async fn test_git_diff_base_change(
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let change_set_remote_b = project_remote
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_unstaged_changes(buffer_remote_b.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait remote buffer to catch up to the new diff
|
||||
executor.run_until_parked();
|
||||
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_remote_b.read_with(cx_b, |buffer, _| {
|
||||
change_set_remote_b.read_with(cx_b, |change_set, cx| {
|
||||
let buffer = buffer_remote_b.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&[(1..2, "", "two\n")],
|
||||
);
|
||||
});
|
||||
|
||||
// Update the staged text
|
||||
client_a.fs().set_index_for_repo(
|
||||
Path::new("/dir/sub/.git"),
|
||||
&[(Path::new("b.txt"), new_diff_base.clone())],
|
||||
@@ -2717,43 +2729,30 @@ async fn test_git_diff_base_change(
|
||||
|
||||
// Wait for buffer_local_b to receive it
|
||||
executor.run_until_parked();
|
||||
|
||||
// Smoke test new diffing
|
||||
|
||||
buffer_local_b.read_with(cx_a, |buffer, _| {
|
||||
change_set_local_b.read_with(cx_a, |change_set, cx| {
|
||||
let buffer = buffer_local_b.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
println!("{:?}", buffer.as_rope().to_string());
|
||||
println!("{:?}", buffer.diff_base());
|
||||
println!(
|
||||
"{:?}",
|
||||
buffer
|
||||
.snapshot()
|
||||
.git_diff_hunks_in_row_range(0..4)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&new_diff_base,
|
||||
&[(2..3, "", "three\n")],
|
||||
);
|
||||
});
|
||||
|
||||
// Smoke test B
|
||||
|
||||
buffer_remote_b.read_with(cx_b, |buffer, _| {
|
||||
change_set_remote_b.read_with(cx_b, |change_set, cx| {
|
||||
let buffer = buffer_remote_b.read(cx);
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
change_set.base_text_string(cx).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff_base,
|
||||
&new_diff_base,
|
||||
&[(2..3, "", "three\n")],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1336,10 +1336,24 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
(_, None) => panic!("guest's file is None, hosts's isn't"),
|
||||
}
|
||||
|
||||
let host_diff_base = host_buffer
|
||||
.read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string));
|
||||
let guest_diff_base = guest_buffer
|
||||
.read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
|
||||
let host_diff_base = host_project.read_with(host_cx, |project, cx| {
|
||||
project
|
||||
.buffer_store()
|
||||
.read(cx)
|
||||
.get_unstaged_changes(host_buffer.read(cx).remote_id())
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.base_text_string(cx)
|
||||
});
|
||||
let guest_diff_base = guest_project.read_with(client_cx, |project, cx| {
|
||||
project
|
||||
.buffer_store()
|
||||
.read(cx)
|
||||
.get_unstaged_changes(guest_buffer.read(cx).remote_id())
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.base_text_string(cx)
|
||||
});
|
||||
assert_eq!(
|
||||
guest_diff_base, host_diff_base,
|
||||
"guest {} diff base does not match host's for path {path:?} in project {project_id}",
|
||||
|
||||
@@ -585,7 +585,7 @@ impl Deref for TestClient {
|
||||
}
|
||||
|
||||
impl TestClient {
|
||||
pub fn fs(&self) -> &FakeFs {
|
||||
pub fn fs(&self) -> Arc<FakeFs> {
|
||||
self.app_state.fs.as_fake()
|
||||
}
|
||||
|
||||
|
||||
+127
-115
@@ -83,7 +83,7 @@ use gpui::{
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
pub(crate) use hunk_diff::HoveredHunk;
|
||||
use hunk_diff::{diff_hunk_to_display, ExpandedHunks};
|
||||
use hunk_diff::{diff_hunk_to_display, DiffMap, DiffMapSnapshot};
|
||||
use indent_guides::ActiveIndentGuidesState;
|
||||
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||
pub use inline_completion::Direction;
|
||||
@@ -625,7 +625,7 @@ pub struct Editor {
|
||||
enable_inline_completions: bool,
|
||||
show_inline_completions_override: Option<bool>,
|
||||
inlay_hint_cache: InlayHintCache,
|
||||
expanded_hunks: ExpandedHunks,
|
||||
diff_map: DiffMap,
|
||||
next_inlay_id: usize,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>,
|
||||
@@ -692,6 +692,7 @@ pub struct EditorSnapshot {
|
||||
git_blame_gutter_max_author_length: Option<usize>,
|
||||
pub display_snapshot: DisplaySnapshot,
|
||||
pub placeholder_text: Option<Arc<str>>,
|
||||
diff_map: DiffMapSnapshot,
|
||||
is_focused: bool,
|
||||
scroll_anchor: ScrollAnchor,
|
||||
ongoing_scroll: OngoingScroll,
|
||||
@@ -2002,11 +2003,10 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
let inlay_hint_settings = inlay_hint_settings(
|
||||
selections.newest_anchor().head(),
|
||||
&buffer.read(cx).snapshot(cx),
|
||||
cx,
|
||||
);
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
|
||||
let inlay_hint_settings =
|
||||
inlay_hint_settings(selections.newest_anchor().head(), &buffer_snapshot, cx);
|
||||
let focus_handle = cx.focus_handle();
|
||||
cx.on_focus(&focus_handle, Self::handle_focus).detach();
|
||||
cx.on_focus_in(&focus_handle, Self::handle_focus_in)
|
||||
@@ -2023,6 +2023,28 @@ impl Editor {
|
||||
|
||||
let mut code_action_providers = Vec::new();
|
||||
if let Some(project) = project.clone() {
|
||||
let mut tasks = Vec::new();
|
||||
buffer.update(cx, |multibuffer, cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
multibuffer.for_each_buffer(|buffer| {
|
||||
tasks.push(project.open_unstaged_changes(buffer.clone(), cx))
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let change_sets = futures::future::join_all(tasks).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
for change_set in change_sets {
|
||||
if let Some(change_set) = change_set.log_err() {
|
||||
this.diff_map.add_change_set(change_set, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
|
||||
code_action_providers.push(Arc::new(project) as Arc<_>);
|
||||
}
|
||||
|
||||
@@ -2105,7 +2127,7 @@ impl Editor {
|
||||
inline_completion_provider: None,
|
||||
active_inline_completion: None,
|
||||
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
|
||||
expanded_hunks: ExpandedHunks::default(),
|
||||
diff_map: DiffMap::default(),
|
||||
gutter_hovered: false,
|
||||
pixel_position_of_newest_cursor: None,
|
||||
last_bounds: None,
|
||||
@@ -2365,6 +2387,7 @@ impl Editor {
|
||||
scroll_anchor: self.scroll_manager.anchor(),
|
||||
ongoing_scroll: self.scroll_manager.ongoing_scroll(),
|
||||
placeholder_text: self.placeholder_text.clone(),
|
||||
diff_map: self.diff_map.snapshot(),
|
||||
is_focused: self.focus_handle.is_focused(cx),
|
||||
current_line_highlight: self
|
||||
.current_line_highlight
|
||||
@@ -6503,12 +6526,12 @@ impl Editor {
|
||||
|
||||
pub fn revert_file(&mut self, _: &RevertFile, cx: &mut ViewContext<Self>) {
|
||||
let mut revert_changes = HashMap::default();
|
||||
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
for hunk in hunks_for_rows(
|
||||
Some(MultiBufferRow(0)..multi_buffer_snapshot.max_row()).into_iter(),
|
||||
&multi_buffer_snapshot,
|
||||
let snapshot = self.snapshot(cx);
|
||||
for hunk in hunks_for_ranges(
|
||||
Some(Point::zero()..snapshot.buffer_snapshot.max_point()).into_iter(),
|
||||
&snapshot,
|
||||
) {
|
||||
Self::prepare_revert_change(&mut revert_changes, self.buffer(), &hunk, cx);
|
||||
self.prepare_revert_change(&mut revert_changes, &hunk, cx);
|
||||
}
|
||||
if !revert_changes.is_empty() {
|
||||
self.transact(cx, |editor, cx| {
|
||||
@@ -6525,7 +6548,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn revert_selected_hunks(&mut self, _: &RevertSelectedHunks, cx: &mut ViewContext<Self>) {
|
||||
let revert_changes = self.gather_revert_changes(&self.selections.disjoint_anchors(), cx);
|
||||
let revert_changes = self.gather_revert_changes(&self.selections.all(cx), cx);
|
||||
if !revert_changes.is_empty() {
|
||||
self.transact(cx, |editor, cx| {
|
||||
editor.revert(revert_changes, cx);
|
||||
@@ -6533,6 +6556,18 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn revert_hunk(&mut self, hunk: HoveredHunk, cx: &mut ViewContext<Editor>) {
|
||||
let snapshot = self.buffer.read(cx).read(cx);
|
||||
if let Some(hunk) = crate::hunk_diff::to_diff_hunk(&hunk, &snapshot) {
|
||||
drop(snapshot);
|
||||
let mut revert_changes = HashMap::default();
|
||||
self.prepare_revert_change(&mut revert_changes, &hunk, cx);
|
||||
if !revert_changes.is_empty() {
|
||||
self.revert(revert_changes, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_active_item_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
|
||||
if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| {
|
||||
let project_path = buffer.read(cx).project_path(cx)?;
|
||||
@@ -6552,26 +6587,33 @@ impl Editor {
|
||||
|
||||
fn gather_revert_changes(
|
||||
&mut self,
|
||||
selections: &[Selection<Anchor>],
|
||||
selections: &[Selection<Point>],
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>> {
|
||||
let mut revert_changes = HashMap::default();
|
||||
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
for hunk in hunks_for_selections(&multi_buffer_snapshot, selections) {
|
||||
Self::prepare_revert_change(&mut revert_changes, self.buffer(), &hunk, cx);
|
||||
let snapshot = self.snapshot(cx);
|
||||
for hunk in hunks_for_selections(&snapshot, selections) {
|
||||
self.prepare_revert_change(&mut revert_changes, &hunk, cx);
|
||||
}
|
||||
revert_changes
|
||||
}
|
||||
|
||||
pub fn prepare_revert_change(
|
||||
&mut self,
|
||||
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
|
||||
multi_buffer: &Model<MultiBuffer>,
|
||||
hunk: &MultiBufferDiffHunk,
|
||||
cx: &AppContext,
|
||||
) -> Option<()> {
|
||||
let buffer = multi_buffer.read(cx).buffer(hunk.buffer_id)?;
|
||||
let buffer = self.buffer.read(cx).buffer(hunk.buffer_id)?;
|
||||
let buffer = buffer.read(cx);
|
||||
let original_text = buffer.diff_base()?.slice(hunk.diff_base_byte_range.clone());
|
||||
let change_set = &self.diff_map.diff_bases.get(&hunk.buffer_id)?.change_set;
|
||||
let original_text = change_set
|
||||
.read(cx)
|
||||
.base_text
|
||||
.as_ref()?
|
||||
.read(cx)
|
||||
.as_rope()
|
||||
.slice(hunk.diff_base_byte_range.clone());
|
||||
let buffer_snapshot = buffer.snapshot();
|
||||
let buffer_revert_changes = revert_changes.entry(buffer.remote_id()).or_default();
|
||||
if let Err(i) = buffer_revert_changes.binary_search_by(|probe| {
|
||||
@@ -9752,80 +9794,63 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn go_to_next_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext<Self>) {
|
||||
let snapshot = self
|
||||
.display_map
|
||||
.update(cx, |display_map, cx| display_map.snapshot(cx));
|
||||
let snapshot = self.snapshot(cx);
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
self.go_to_hunk_after_position(&snapshot, selection.head(), cx);
|
||||
}
|
||||
|
||||
fn go_to_hunk_after_position(
|
||||
&mut self,
|
||||
snapshot: &DisplaySnapshot,
|
||||
snapshot: &EditorSnapshot,
|
||||
position: Point,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> Option<MultiBufferDiffHunk> {
|
||||
if let Some(hunk) = self.go_to_next_hunk_in_direction(
|
||||
snapshot,
|
||||
position,
|
||||
false,
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range(MultiBufferRow(position.row + 1)..MultiBufferRow::MAX),
|
||||
cx,
|
||||
) {
|
||||
return Some(hunk);
|
||||
for (ix, position) in [position, Point::zero()].into_iter().enumerate() {
|
||||
if let Some(hunk) = self.go_to_next_hunk_in_direction(
|
||||
snapshot,
|
||||
position,
|
||||
ix > 0,
|
||||
snapshot.diff_map.diff_hunks_in_range(
|
||||
position + Point::new(1, 0)..snapshot.buffer_snapshot.max_point(),
|
||||
&snapshot.buffer_snapshot,
|
||||
),
|
||||
cx,
|
||||
) {
|
||||
return Some(hunk);
|
||||
}
|
||||
}
|
||||
|
||||
let wrapped_point = Point::zero();
|
||||
self.go_to_next_hunk_in_direction(
|
||||
snapshot,
|
||||
wrapped_point,
|
||||
true,
|
||||
snapshot.buffer_snapshot.git_diff_hunks_in_range(
|
||||
MultiBufferRow(wrapped_point.row + 1)..MultiBufferRow::MAX,
|
||||
),
|
||||
cx,
|
||||
)
|
||||
None
|
||||
}
|
||||
|
||||
fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext<Self>) {
|
||||
let snapshot = self
|
||||
.display_map
|
||||
.update(cx, |display_map, cx| display_map.snapshot(cx));
|
||||
let snapshot = self.snapshot(cx);
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
|
||||
self.go_to_hunk_before_position(&snapshot, selection.head(), cx);
|
||||
}
|
||||
|
||||
fn go_to_hunk_before_position(
|
||||
&mut self,
|
||||
snapshot: &DisplaySnapshot,
|
||||
snapshot: &EditorSnapshot,
|
||||
position: Point,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> Option<MultiBufferDiffHunk> {
|
||||
if let Some(hunk) = self.go_to_next_hunk_in_direction(
|
||||
snapshot,
|
||||
position,
|
||||
false,
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(position.row)),
|
||||
cx,
|
||||
) {
|
||||
return Some(hunk);
|
||||
for (ix, position) in [position, snapshot.buffer_snapshot.max_point()]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
if let Some(hunk) = self.go_to_next_hunk_in_direction(
|
||||
snapshot,
|
||||
position,
|
||||
ix > 0,
|
||||
snapshot
|
||||
.diff_map
|
||||
.diff_hunks_in_range_rev(Point::zero()..position, &snapshot.buffer_snapshot),
|
||||
cx,
|
||||
) {
|
||||
return Some(hunk);
|
||||
}
|
||||
}
|
||||
|
||||
let wrapped_point = snapshot.buffer_snapshot.max_point();
|
||||
self.go_to_next_hunk_in_direction(
|
||||
snapshot,
|
||||
wrapped_point,
|
||||
true,
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(wrapped_point.row)),
|
||||
cx,
|
||||
)
|
||||
None
|
||||
}
|
||||
|
||||
fn go_to_next_hunk_in_direction(
|
||||
@@ -11270,13 +11295,13 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut buffers_affected = HashMap::default();
|
||||
let mut buffers_affected = HashSet::default();
|
||||
let multi_buffer = self.buffer().read(cx);
|
||||
for crease in &creases {
|
||||
if let Some((_, buffer, _)) =
|
||||
multi_buffer.excerpt_containing(crease.range().start.clone(), cx)
|
||||
{
|
||||
buffers_affected.insert(buffer.read(cx).remote_id(), buffer);
|
||||
buffers_affected.insert(buffer.read(cx).remote_id());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11286,8 +11311,8 @@ impl Editor {
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
}
|
||||
|
||||
for buffer in buffers_affected.into_values() {
|
||||
self.sync_expanded_diff_hunks(buffer, cx);
|
||||
for buffer_id in buffers_affected {
|
||||
Self::sync_expanded_diff_hunks(&mut self.diff_map, buffer_id, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
@@ -11344,11 +11369,11 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut buffers_affected = HashMap::default();
|
||||
let mut buffers_affected = HashSet::default();
|
||||
let multi_buffer = self.buffer().read(cx);
|
||||
for range in ranges {
|
||||
if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(range.start.clone(), cx) {
|
||||
buffers_affected.insert(buffer.read(cx).remote_id(), buffer);
|
||||
buffers_affected.insert(buffer.read(cx).remote_id());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11358,8 +11383,8 @@ impl Editor {
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
}
|
||||
|
||||
for buffer in buffers_affected.into_values() {
|
||||
self.sync_expanded_diff_hunks(buffer, cx);
|
||||
for buffer_id in buffers_affected {
|
||||
Self::sync_expanded_diff_hunks(&mut self.diff_map, buffer_id, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
@@ -12653,15 +12678,11 @@ impl Editor {
|
||||
multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => {
|
||||
cx.emit(EditorEvent::TitleChanged)
|
||||
}
|
||||
multi_buffer::Event::DiffBaseChanged => {
|
||||
self.scrollbar_marker_state.dirty = true;
|
||||
cx.emit(EditorEvent::DiffBaseChanged);
|
||||
cx.notify();
|
||||
}
|
||||
multi_buffer::Event::DiffUpdated { buffer } => {
|
||||
self.sync_expanded_diff_hunks(buffer.clone(), cx);
|
||||
cx.notify();
|
||||
}
|
||||
// multi_buffer::Event::DiffBaseChanged => {
|
||||
// self.scrollbar_marker_state.dirty = true;
|
||||
// cx.emit(EditorEvent::DiffBaseChanged);
|
||||
// cx.notify();
|
||||
// }
|
||||
multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed),
|
||||
multi_buffer::Event::DiagnosticsUpdated => {
|
||||
self.refresh_active_diagnostics(cx);
|
||||
@@ -12829,7 +12850,7 @@ impl Editor {
|
||||
// When editing branch buffers, jump to the corresponding location
|
||||
// in their base buffer.
|
||||
let buffer = buffer_handle.read(cx);
|
||||
if let Some(base_buffer) = buffer.diff_base_buffer() {
|
||||
if let Some(base_buffer) = buffer.base_buffer() {
|
||||
range = buffer.range_to_version(range, &base_buffer.read(cx).version());
|
||||
buffer_handle = base_buffer;
|
||||
}
|
||||
@@ -13606,35 +13627,29 @@ fn test_wrap_with_prefix() {
|
||||
}
|
||||
|
||||
fn hunks_for_selections(
|
||||
multi_buffer_snapshot: &MultiBufferSnapshot,
|
||||
selections: &[Selection<Anchor>],
|
||||
snapshot: &EditorSnapshot,
|
||||
selections: &[Selection<Point>],
|
||||
) -> Vec<MultiBufferDiffHunk> {
|
||||
let buffer_rows_for_selections = selections.iter().map(|selection| {
|
||||
let head = selection.head();
|
||||
let tail = selection.tail();
|
||||
let start = MultiBufferRow(tail.to_point(multi_buffer_snapshot).row);
|
||||
let end = MultiBufferRow(head.to_point(multi_buffer_snapshot).row);
|
||||
if start > end {
|
||||
end..start
|
||||
} else {
|
||||
start..end
|
||||
}
|
||||
});
|
||||
|
||||
hunks_for_rows(buffer_rows_for_selections, multi_buffer_snapshot)
|
||||
hunks_for_ranges(
|
||||
selections.iter().map(|selection| selection.range()),
|
||||
snapshot,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn hunks_for_rows(
|
||||
rows: impl Iterator<Item = Range<MultiBufferRow>>,
|
||||
multi_buffer_snapshot: &MultiBufferSnapshot,
|
||||
pub fn hunks_for_ranges(
|
||||
ranges: impl Iterator<Item = Range<Point>>,
|
||||
snapshot: &EditorSnapshot,
|
||||
) -> Vec<MultiBufferDiffHunk> {
|
||||
let mut hunks = Vec::new();
|
||||
let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
|
||||
HashMap::default();
|
||||
for selected_multi_buffer_rows in rows {
|
||||
for query_range in ranges {
|
||||
let query_rows =
|
||||
selected_multi_buffer_rows.start..selected_multi_buffer_rows.end.next_row();
|
||||
for hunk in multi_buffer_snapshot.git_diff_hunks_in_range(query_rows.clone()) {
|
||||
MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1);
|
||||
for hunk in snapshot.diff_map.diff_hunks_in_range(
|
||||
Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0),
|
||||
&snapshot.buffer_snapshot,
|
||||
) {
|
||||
// Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it
|
||||
// when the caret is just above or just below the deleted hunk.
|
||||
let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed;
|
||||
@@ -13643,10 +13658,7 @@ pub fn hunks_for_rows(
|
||||
|| hunk.row_range.start == query_rows.end
|
||||
|| hunk.row_range.end == query_rows.start
|
||||
} else {
|
||||
// `selected_multi_buffer_rows` are inclusive (e.g. [2..2] means 2nd row is selected)
|
||||
// `hunk.row_range` is exclusive (e.g. [2..3] means 2nd row is selected)
|
||||
hunk.row_range.overlaps(&selected_multi_buffer_rows)
|
||||
|| selected_multi_buffer_rows.end == hunk.row_range.start
|
||||
hunk.row_range.overlaps(&query_rows)
|
||||
};
|
||||
if related_to_selection {
|
||||
if !processed_buffer_rows
|
||||
|
||||
+181
-263
File diff suppressed because it is too large
Load Diff
@@ -1169,7 +1169,7 @@ impl EditorElement {
|
||||
let editor = self.editor.read(cx);
|
||||
let is_singleton = editor.is_singleton(cx);
|
||||
// Git
|
||||
(is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
|
||||
(is_singleton && scrollbar_settings.git_diff && !snapshot.diff_map.is_empty())
|
||||
||
|
||||
// Buffer Search Results
|
||||
(is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::<BufferSearchHighlights>())
|
||||
@@ -1320,17 +1320,8 @@ impl EditorElement {
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<(DisplayDiffHunk, Option<Hitbox>)> {
|
||||
let buffer_snapshot = &snapshot.buffer_snapshot;
|
||||
|
||||
let buffer_start_row = MultiBufferRow(
|
||||
DisplayPoint::new(display_rows.start, 0)
|
||||
.to_point(snapshot)
|
||||
.row,
|
||||
);
|
||||
let buffer_end_row = MultiBufferRow(
|
||||
DisplayPoint::new(display_rows.end, 0)
|
||||
.to_point(snapshot)
|
||||
.row,
|
||||
);
|
||||
let buffer_start = DisplayPoint::new(display_rows.start, 0).to_point(snapshot);
|
||||
let buffer_end = DisplayPoint::new(display_rows.end, 0).to_point(snapshot);
|
||||
|
||||
let git_gutter_setting = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
@@ -1338,7 +1329,7 @@ impl EditorElement {
|
||||
.unwrap_or_default();
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let expanded_hunks = &editor.expanded_hunks.hunks;
|
||||
let expanded_hunks = &editor.diff_map.hunks;
|
||||
let expanded_hunks_start_ix = expanded_hunks
|
||||
.binary_search_by(|hunk| {
|
||||
hunk.hunk_range
|
||||
@@ -1349,8 +1340,10 @@ impl EditorElement {
|
||||
.unwrap_err();
|
||||
let mut expanded_hunks = expanded_hunks[expanded_hunks_start_ix..].iter().peekable();
|
||||
|
||||
let display_hunks = buffer_snapshot
|
||||
.git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
|
||||
let mut display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)> = editor
|
||||
.diff_map
|
||||
.snapshot
|
||||
.diff_hunks_in_range(buffer_start..buffer_end, &buffer_snapshot)
|
||||
.filter_map(|hunk| {
|
||||
let display_hunk = diff_hunk_to_display(&hunk, snapshot);
|
||||
|
||||
@@ -1393,25 +1386,23 @@ impl EditorElement {
|
||||
Some(display_hunk)
|
||||
})
|
||||
.dedup()
|
||||
.map(|hunk| match git_gutter_setting {
|
||||
GitGutterSetting::TrackedFiles => {
|
||||
let hitbox = match hunk {
|
||||
DisplayDiffHunk::Unfolded { .. } => {
|
||||
let hunk_bounds = Self::diff_hunk_bounds(
|
||||
snapshot,
|
||||
line_height,
|
||||
gutter_hitbox.bounds,
|
||||
&hunk,
|
||||
);
|
||||
Some(cx.insert_hitbox(hunk_bounds, true))
|
||||
}
|
||||
DisplayDiffHunk::Folded { .. } => None,
|
||||
};
|
||||
(hunk, hitbox)
|
||||
}
|
||||
GitGutterSetting::Hide => (hunk, None),
|
||||
})
|
||||
.map(|hunk| (hunk, None))
|
||||
.collect();
|
||||
|
||||
if let GitGutterSetting::TrackedFiles = git_gutter_setting {
|
||||
for (hunk, hitbox) in &mut display_hunks {
|
||||
if let DisplayDiffHunk::Unfolded { .. } = hunk {
|
||||
let hunk_bounds = Self::diff_hunk_bounds(
|
||||
snapshot,
|
||||
line_height,
|
||||
gutter_hitbox.bounds,
|
||||
&hunk,
|
||||
);
|
||||
*hitbox = Some(cx.insert_hitbox(hunk_bounds, true));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
display_hunks
|
||||
})
|
||||
}
|
||||
@@ -3755,10 +3746,8 @@ impl EditorElement {
|
||||
let mut marker_quads = Vec::new();
|
||||
if scrollbar_settings.git_diff {
|
||||
let marker_row_ranges = snapshot
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range(
|
||||
MultiBufferRow::MIN..MultiBufferRow::MAX,
|
||||
)
|
||||
.diff_map
|
||||
.diff_hunks(&snapshot.buffer_snapshot)
|
||||
.map(|hunk| {
|
||||
let start_display_row =
|
||||
MultiBufferPoint::new(hunk.row_range.start.0, 0)
|
||||
@@ -5440,7 +5429,7 @@ impl Element for EditorElement {
|
||||
|
||||
let expanded_add_hunks_by_rows = self.editor.update(cx, |editor, _| {
|
||||
editor
|
||||
.expanded_hunks
|
||||
.diff_map
|
||||
.hunks(false)
|
||||
.filter(|hunk| hunk.status == DiffHunkStatus::Added)
|
||||
.map(|expanded_hunk| {
|
||||
|
||||
@@ -9,13 +9,15 @@ use std::{
|
||||
use anyhow::Context as _;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use git::{diff::DiffHunk, repository::GitFileStatus};
|
||||
use git::{
|
||||
diff::{BufferDiff, DiffHunk},
|
||||
repository::GitFileStatus,
|
||||
};
|
||||
use gpui::{
|
||||
actions, AnyElement, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView,
|
||||
InteractiveElement, Model, Render, Subscription, Task, View, WeakView,
|
||||
};
|
||||
use language::{Buffer, BufferRow, BufferSnapshot};
|
||||
use language::{Buffer, BufferRow};
|
||||
use multi_buffer::{ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer};
|
||||
use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
|
||||
use text::{OffsetRangeExt, ToPoint};
|
||||
@@ -215,54 +217,56 @@ impl ProjectDiffEditor {
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
let buffers_with_git_diff = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let mut open_tasks = open_tasks
|
||||
.into_iter()
|
||||
.map(|(status, entry_id, entry_path, open_task)| async move {
|
||||
let (_, opened_model) = open_task.await.with_context(|| {
|
||||
format!(
|
||||
"loading buffer {} for git diff",
|
||||
entry_path.path.display()
|
||||
)
|
||||
})?;
|
||||
let buffer = match opened_model.downcast::<Buffer>() {
|
||||
Ok(buffer) => buffer,
|
||||
Err(_model) => anyhow::bail!(
|
||||
"Could not load {} as a buffer for git diff",
|
||||
entry_path.path.display()
|
||||
),
|
||||
};
|
||||
anyhow::Ok((status, entry_id, entry_path, buffer))
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
let mut buffers_with_git_diff = Vec::new();
|
||||
while let Some(opened_buffer) = open_tasks.next().await {
|
||||
if let Some(opened_buffer) = opened_buffer.log_err() {
|
||||
buffers_with_git_diff.push(opened_buffer);
|
||||
}
|
||||
}
|
||||
buffers_with_git_diff
|
||||
})
|
||||
.await;
|
||||
|
||||
let Some((buffers, mut new_entries)) = cx
|
||||
.update(|cx| {
|
||||
let Some((buffers, mut new_entries, change_sets)) = cx
|
||||
.spawn(|mut cx| async move {
|
||||
let mut new_entries = Vec::new();
|
||||
let mut buffers = HashMap::<
|
||||
ProjectEntryId,
|
||||
(GitFileStatus, Model<Buffer>, BufferSnapshot),
|
||||
(
|
||||
GitFileStatus,
|
||||
text::BufferSnapshot,
|
||||
Model<Buffer>,
|
||||
BufferDiff,
|
||||
),
|
||||
>::default();
|
||||
let mut new_entries = Vec::new();
|
||||
for (status, entry_id, entry_path, buffer) in buffers_with_git_diff {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
buffers.insert(entry_id, (status, buffer, buffer_snapshot));
|
||||
let mut change_sets = Vec::new();
|
||||
for (status, entry_id, entry_path, open_task) in open_tasks {
|
||||
let (_, opened_model) = open_task.await.with_context(|| {
|
||||
format!("loading buffer {} for git diff", entry_path.path.display())
|
||||
})?;
|
||||
let buffer = match opened_model.downcast::<Buffer>() {
|
||||
Ok(buffer) => buffer,
|
||||
Err(_model) => anyhow::bail!(
|
||||
"Could not load {} as a buffer for git diff",
|
||||
entry_path.path.display()
|
||||
),
|
||||
};
|
||||
let change_set = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.open_unstaged_changes(buffer.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
cx.update(|cx| {
|
||||
buffers.insert(
|
||||
entry_id,
|
||||
(
|
||||
status,
|
||||
buffer.read(cx).text_snapshot(),
|
||||
buffer,
|
||||
change_set.read(cx).diff_to_buffer.clone(),
|
||||
),
|
||||
);
|
||||
})?;
|
||||
change_sets.push(change_set);
|
||||
new_entries.push((entry_path, entry_id));
|
||||
}
|
||||
(buffers, new_entries)
|
||||
|
||||
Ok((buffers, new_entries, change_sets))
|
||||
})
|
||||
.ok()
|
||||
.await
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -271,14 +275,14 @@ impl ProjectDiffEditor {
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let mut new_changes = HashMap::<ProjectEntryId, Changes>::default();
|
||||
for (entry_id, (status, buffer, buffer_snapshot)) in buffers {
|
||||
for (entry_id, (status, buffer_snapshot, buffer, buffer_diff)) in buffers {
|
||||
new_changes.insert(
|
||||
entry_id,
|
||||
Changes {
|
||||
_status: status,
|
||||
buffer,
|
||||
hunks: buffer_snapshot
|
||||
.git_diff_hunks_in_row_range(0..BufferRow::MAX)
|
||||
hunks: buffer_diff
|
||||
.hunks_in_row_range(0..BufferRow::MAX, &buffer_snapshot)
|
||||
.collect::<Vec<_>>(),
|
||||
},
|
||||
);
|
||||
@@ -294,33 +298,16 @@ impl ProjectDiffEditor {
|
||||
})
|
||||
.await;
|
||||
|
||||
let mut diff_recalculations = FuturesUnordered::new();
|
||||
project_diff_editor
|
||||
.update(&mut cx, |project_diff_editor, cx| {
|
||||
project_diff_editor.update_excerpts(id, new_changes, new_entry_order, cx);
|
||||
for buffer in project_diff_editor
|
||||
.editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.all_buffers()
|
||||
{
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
if let Some(diff_recalculation) = buffer.recalculate_diff(cx) {
|
||||
diff_recalculations.push(diff_recalculation);
|
||||
}
|
||||
for change_set in change_sets {
|
||||
project_diff_editor.editor.update(cx, |editor, cx| {
|
||||
editor.diff_map.add_change_set(change_set, cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
while let Some(()) = diff_recalculations.next().await {
|
||||
// another diff is calculated
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1100,13 +1087,13 @@ impl Render for ProjectDiffEditor {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{ops::Deref as _, path::Path, sync::Arc};
|
||||
// use std::{ops::Deref as _, path::Path, sync::Arc};
|
||||
|
||||
use fs::RealFs;
|
||||
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
|
||||
use settings::SettingsStore;
|
||||
// use fs::RealFs;
|
||||
// use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
|
||||
// use settings::SettingsStore;
|
||||
|
||||
use super::*;
|
||||
// use super::*;
|
||||
|
||||
// TODO finish
|
||||
// #[gpui::test]
|
||||
@@ -1122,114 +1109,114 @@ mod tests {
|
||||
// // Apply randomized changes to the project: select a random file, random change and apply to buffers
|
||||
// }
|
||||
|
||||
#[gpui::test]
|
||||
async fn simple_edit_test(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
init_test(cx);
|
||||
// #[gpui::test]
|
||||
// async fn simple_edit_test(cx: &mut TestAppContext) {
|
||||
// cx.executor().allow_parking();
|
||||
// init_test(cx);
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let dst = dir.path();
|
||||
// let dir = tempfile::tempdir().unwrap();
|
||||
// let dst = dir.path();
|
||||
|
||||
std::fs::write(dst.join("file_a"), "This is file_a").unwrap();
|
||||
std::fs::write(dst.join("file_b"), "This is file_b").unwrap();
|
||||
// std::fs::write(dst.join("file_a"), "This is file_a").unwrap();
|
||||
// std::fs::write(dst.join("file_b"), "This is file_b").unwrap();
|
||||
|
||||
run_git(dst, &["init"]);
|
||||
run_git(dst, &["add", "*"]);
|
||||
run_git(dst, &["commit", "-m", "Initial commit"]);
|
||||
// run_git(dst, &["init"]);
|
||||
// run_git(dst, &["add", "*"]);
|
||||
// run_git(dst, &["commit", "-m", "Initial commit"]);
|
||||
|
||||
let project = Project::test(Arc::new(RealFs::default()), [dst], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||
// let project = Project::test(Arc::new(RealFs::default()), [dst], cx).await;
|
||||
// let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
// let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||
|
||||
let file_a_editor = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let file_a_editor = workspace.open_abs_path(dst.join("file_a"), true, cx);
|
||||
ProjectDiffEditor::deploy(workspace, &Deploy, cx);
|
||||
file_a_editor
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.expect("did not open an item at all")
|
||||
.downcast::<Editor>()
|
||||
.expect("did not open an editor for file_a");
|
||||
// let file_a_editor = workspace
|
||||
// .update(cx, |workspace, cx| {
|
||||
// let file_a_editor = workspace.open_abs_path(dst.join("file_a"), true, cx);
|
||||
// ProjectDiffEditor::deploy(workspace, &Deploy, cx);
|
||||
// file_a_editor
|
||||
// })
|
||||
// .unwrap()
|
||||
// .await
|
||||
// .expect("did not open an item at all")
|
||||
// .downcast::<Editor>()
|
||||
// .expect("did not open an editor for file_a");
|
||||
|
||||
let project_diff_editor = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.items()
|
||||
.find_map(|item| item.downcast::<ProjectDiffEditor>())
|
||||
})
|
||||
.unwrap()
|
||||
.expect("did not find a ProjectDiffEditor");
|
||||
project_diff_editor.update(cx, |project_diff_editor, cx| {
|
||||
assert!(
|
||||
project_diff_editor.editor.read(cx).text(cx).is_empty(),
|
||||
"Should have no changes after opening the diff on no git changes"
|
||||
);
|
||||
});
|
||||
// let project_diff_editor = workspace
|
||||
// .update(cx, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_pane()
|
||||
// .read(cx)
|
||||
// .items()
|
||||
// .find_map(|item| item.downcast::<ProjectDiffEditor>())
|
||||
// })
|
||||
// .unwrap()
|
||||
// .expect("did not find a ProjectDiffEditor");
|
||||
// project_diff_editor.update(cx, |project_diff_editor, cx| {
|
||||
// assert!(
|
||||
// project_diff_editor.editor.read(cx).text(cx).is_empty(),
|
||||
// "Should have no changes after opening the diff on no git changes"
|
||||
// );
|
||||
// });
|
||||
|
||||
let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx));
|
||||
let change = "an edit after git add";
|
||||
file_a_editor
|
||||
.update(cx, |file_a_editor, cx| {
|
||||
file_a_editor.insert(change, cx);
|
||||
file_a_editor.save(false, project.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.expect("failed to save a file");
|
||||
cx.executor().advance_clock(Duration::from_secs(1));
|
||||
cx.run_until_parked();
|
||||
// let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx));
|
||||
// let change = "an edit after git add";
|
||||
// file_a_editor
|
||||
// .update(cx, |file_a_editor, cx| {
|
||||
// file_a_editor.insert(change, cx);
|
||||
// file_a_editor.save(false, project.clone(), cx)
|
||||
// })
|
||||
// .await
|
||||
// .expect("failed to save a file");
|
||||
// cx.executor().advance_clock(Duration::from_secs(1));
|
||||
// cx.run_until_parked();
|
||||
|
||||
// TODO does not work on Linux for some reason, returning a blank line
|
||||
// hence disable the last check for now, and do some fiddling to avoid the warnings.
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if true {
|
||||
return;
|
||||
}
|
||||
}
|
||||
project_diff_editor.update(cx, |project_diff_editor, cx| {
|
||||
// TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added)
|
||||
assert_eq!(
|
||||
project_diff_editor.editor.read(cx).text(cx),
|
||||
format!("{change}{old_text}"),
|
||||
"Should have a new change shown in the beginning, and the old text shown as deleted text afterwards"
|
||||
);
|
||||
});
|
||||
}
|
||||
// // TODO does not work on Linux for some reason, returning a blank line
|
||||
// // hence disable the last check for now, and do some fiddling to avoid the warnings.
|
||||
// #[cfg(target_os = "linux")]
|
||||
// {
|
||||
// if true {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// project_diff_editor.update(cx, |project_diff_editor, cx| {
|
||||
// // TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added)
|
||||
// assert_eq!(
|
||||
// project_diff_editor.editor.read(cx).text(cx),
|
||||
// format!("{change}{old_text}"),
|
||||
// "Should have a new change shown in the beginning, and the old text shown as deleted text afterwards"
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
fn run_git(path: &Path, args: &[&str]) -> String {
|
||||
let output = std::process::Command::new("git")
|
||||
.args(args)
|
||||
.current_dir(path)
|
||||
.output()
|
||||
.expect("git commit failed");
|
||||
// fn run_git(path: &Path, args: &[&str]) -> String {
|
||||
// let output = std::process::Command::new("git")
|
||||
// .args(args)
|
||||
// .current_dir(path)
|
||||
// .output()
|
||||
// .expect("git commit failed");
|
||||
|
||||
format!(
|
||||
"Stdout: {}; stderr: {}",
|
||||
String::from_utf8(output.stdout).unwrap(),
|
||||
String::from_utf8(output.stderr).unwrap()
|
||||
)
|
||||
}
|
||||
// format!(
|
||||
// "Stdout: {}; stderr: {}",
|
||||
// String::from_utf8(output.stdout).unwrap(),
|
||||
// String::from_utf8(output.stderr).unwrap()
|
||||
// )
|
||||
// }
|
||||
|
||||
fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::try_init().ok();
|
||||
}
|
||||
// fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
// if std::env::var("RUST_LOG").is_ok() {
|
||||
// env_logger::try_init().ok();
|
||||
// }
|
||||
|
||||
cx.update(|cx| {
|
||||
assets::Assets.load_test_fonts(cx);
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
client::init_settings(cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
workspace::init_settings(cx);
|
||||
crate::init(cx);
|
||||
});
|
||||
}
|
||||
// cx.update(|cx| {
|
||||
// assets::Assets.load_test_fonts(cx);
|
||||
// let settings_store = SettingsStore::test(cx);
|
||||
// cx.set_global(settings_store);
|
||||
// theme::init(theme::LoadThemes::JustBase, cx);
|
||||
// release_channel::init(SemanticVersion::default(), cx);
|
||||
// client::init_settings(cx);
|
||||
// language::init(cx);
|
||||
// Project::init_settings(cx);
|
||||
// workspace::init_settings(cx);
|
||||
// crate::init(cx);
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
+301
-263
File diff suppressed because it is too large
Load Diff
@@ -737,7 +737,7 @@ impl Item for Editor {
|
||||
let buffers = self.buffer().clone().read(cx).all_buffers();
|
||||
let buffers = buffers
|
||||
.into_iter()
|
||||
.map(|handle| handle.read(cx).diff_base_buffer().unwrap_or(handle.clone()))
|
||||
.map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone()))
|
||||
.collect::<HashSet<_>>();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if format {
|
||||
|
||||
@@ -4,7 +4,7 @@ use futures::{channel::mpsc, future::join_all};
|
||||
use gpui::{AppContext, EventEmitter, FocusableView, Model, Render, Subscription, Task, View};
|
||||
use language::{Buffer, BufferEvent, Capability};
|
||||
use multi_buffer::{ExcerptRange, MultiBuffer};
|
||||
use project::Project;
|
||||
use project::{buffer_store::BufferChangeSet, Project};
|
||||
use smol::stream::StreamExt;
|
||||
use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
|
||||
use text::ToOffset;
|
||||
@@ -75,7 +75,7 @@ impl ProposedChangesEditor {
|
||||
title: title.into(),
|
||||
buffer_entries: Vec::new(),
|
||||
recalculate_diffs_tx,
|
||||
_recalculate_diffs_task: cx.spawn(|_, mut cx| async move {
|
||||
_recalculate_diffs_task: cx.spawn(|this, mut cx| async move {
|
||||
let mut buffers_to_diff = HashSet::default();
|
||||
while let Some(mut recalculate_diff) = recalculate_diffs_rx.next().await {
|
||||
buffers_to_diff.insert(recalculate_diff.buffer);
|
||||
@@ -96,12 +96,37 @@ impl ProposedChangesEditor {
|
||||
}
|
||||
}
|
||||
|
||||
join_all(buffers_to_diff.drain().filter_map(|buffer| {
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| buffer.recalculate_diff(cx))
|
||||
.ok()?
|
||||
}))
|
||||
.await;
|
||||
let recalculate_diff_futures = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
buffers_to_diff
|
||||
.drain()
|
||||
.filter_map(|buffer| {
|
||||
let buffer = buffer.read(cx);
|
||||
let base_buffer = buffer.base_buffer()?;
|
||||
let buffer = buffer.text_snapshot();
|
||||
let change_set = this.editor.update(cx, |editor, _| {
|
||||
Some(
|
||||
editor
|
||||
.diff_map
|
||||
.diff_bases
|
||||
.get(&buffer.remote_id())?
|
||||
.change_set
|
||||
.clone(),
|
||||
)
|
||||
})?;
|
||||
Some(change_set.update(cx, |change_set, cx| {
|
||||
change_set.set_base_text(
|
||||
base_buffer.read(cx).text(),
|
||||
buffer,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
join_all(recalculate_diff_futures).await;
|
||||
}
|
||||
None
|
||||
}),
|
||||
@@ -154,6 +179,7 @@ impl ProposedChangesEditor {
|
||||
});
|
||||
|
||||
let mut buffer_entries = Vec::new();
|
||||
let mut new_change_sets = Vec::new();
|
||||
for location in locations {
|
||||
let branch_buffer;
|
||||
if let Some(ix) = self
|
||||
@@ -166,6 +192,15 @@ impl ProposedChangesEditor {
|
||||
buffer_entries.push(entry);
|
||||
} else {
|
||||
branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
||||
new_change_sets.push(cx.new_model(|cx| {
|
||||
let mut change_set = BufferChangeSet::new(branch_buffer.read(cx));
|
||||
let _ = change_set.set_base_text(
|
||||
location.buffer.read(cx).text(),
|
||||
branch_buffer.read(cx).text_snapshot(),
|
||||
cx,
|
||||
);
|
||||
change_set
|
||||
}));
|
||||
buffer_entries.push(BufferEntry {
|
||||
branch: branch_buffer.clone(),
|
||||
base: location.buffer.clone(),
|
||||
@@ -187,7 +222,10 @@ impl ProposedChangesEditor {
|
||||
|
||||
self.buffer_entries = buffer_entries;
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |selections| selections.refresh())
|
||||
editor.change_selections(None, cx, |selections| selections.refresh());
|
||||
for change_set in new_change_sets {
|
||||
editor.diff_map.add_change_set(change_set, cx)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -217,14 +255,14 @@ impl ProposedChangesEditor {
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
BufferEvent::DiffBaseChanged => {
|
||||
self.recalculate_diffs_tx
|
||||
.unbounded_send(RecalculateDiff {
|
||||
buffer,
|
||||
debounce: false,
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
// BufferEvent::DiffBaseChanged => {
|
||||
// self.recalculate_diffs_tx
|
||||
// .unbounded_send(RecalculateDiff {
|
||||
// buffer,
|
||||
// debounce: false,
|
||||
// })
|
||||
// .ok();
|
||||
// }
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -373,7 +411,7 @@ impl BranchBufferSemanticsProvider {
|
||||
positions: &[text::Anchor],
|
||||
cx: &AppContext,
|
||||
) -> Option<Model<Buffer>> {
|
||||
let base_buffer = buffer.read(cx).diff_base_buffer()?;
|
||||
let base_buffer = buffer.read(cx).base_buffer()?;
|
||||
let version = base_buffer.read(cx).version();
|
||||
if positions
|
||||
.iter()
|
||||
|
||||
@@ -113,7 +113,15 @@ impl EditorLspTestContext {
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(root, json!({ "dir": { file_name.clone(): "" }}))
|
||||
.insert_tree(
|
||||
root,
|
||||
json!({
|
||||
".git": {},
|
||||
"dir": {
|
||||
file_name.clone(): ""
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
@@ -42,16 +42,16 @@ pub struct EditorTestContext {
|
||||
impl EditorTestContext {
|
||||
pub async fn new(cx: &mut gpui::TestAppContext) -> EditorTestContext {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
// fs.insert_file("/file", "".to_owned()).await;
|
||||
let root = Self::root_path();
|
||||
fs.insert_tree(
|
||||
root,
|
||||
serde_json::json!({
|
||||
".git": {},
|
||||
"file": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, [root], cx).await;
|
||||
let project = Project::test(fs.clone(), [root], cx).await;
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(root.join("file"), cx)
|
||||
@@ -65,6 +65,8 @@ impl EditorTestContext {
|
||||
editor
|
||||
});
|
||||
let editor_view = editor.root_view(cx).unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
Self {
|
||||
cx: VisualTestContext::from_window(*editor.deref(), cx),
|
||||
window: editor.into(),
|
||||
@@ -276,8 +278,16 @@ impl EditorTestContext {
|
||||
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
|
||||
}
|
||||
|
||||
pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
|
||||
self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base.map(ToOwned::to_owned), cx));
|
||||
pub fn set_diff_base(&mut self, diff_base: &str) {
|
||||
self.cx.run_until_parked();
|
||||
let fs = self
|
||||
.update_editor(|editor, cx| editor.project.as_ref().unwrap().read(cx).fs().as_fake());
|
||||
let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
|
||||
fs.set_index_for_repo(
|
||||
&Self::root_path().join(".git"),
|
||||
&[(path.as_ref(), diff_base.to_string())],
|
||||
);
|
||||
self.cx.run_until_parked();
|
||||
}
|
||||
|
||||
/// Change the editor's text and selections using a string containing
|
||||
@@ -319,10 +329,12 @@ impl EditorTestContext {
|
||||
state_context
|
||||
}
|
||||
|
||||
/// Assert about the text of the editor, the selections, and the expanded
|
||||
/// diff hunks.
|
||||
///
|
||||
/// Diff hunks are indicated by lines starting with `+` and `-`.
|
||||
#[track_caller]
|
||||
pub fn assert_diff_hunks(&mut self, expected_diff: String) {
|
||||
// Normalize the expected diff. If it has no diff markers, then insert blank markers
|
||||
// before each line. Strip any whitespace-only lines.
|
||||
pub fn assert_state_with_diff(&mut self, expected_diff: String) {
|
||||
let has_diff_markers = expected_diff
|
||||
.lines()
|
||||
.any(|line| line.starts_with("+") || line.starts_with("-"));
|
||||
@@ -340,11 +352,14 @@ impl EditorTestContext {
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
let actual_selections = self.editor_selections();
|
||||
let actual_marked_text =
|
||||
generate_marked_text(&self.buffer_text(), &actual_selections, true);
|
||||
|
||||
// Read the actual diff from the editor's row highlights and block
|
||||
// decorations.
|
||||
let actual_diff = self.editor.update(&mut self.cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let text = editor.text(cx);
|
||||
let insertions = editor
|
||||
.highlighted_rows::<DiffRowHighlight>()
|
||||
.map(|(range, _)| {
|
||||
@@ -354,7 +369,7 @@ impl EditorTestContext {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let deletions = editor
|
||||
.expanded_hunks
|
||||
.diff_map
|
||||
.hunks
|
||||
.iter()
|
||||
.filter_map(|hunk| {
|
||||
@@ -371,10 +386,20 @@ impl EditorTestContext {
|
||||
.read(cx)
|
||||
.excerpt_containing(hunk.hunk_range.start, cx)
|
||||
.expect("no excerpt for expanded buffer's hunk start");
|
||||
let deleted_text = buffer
|
||||
.read(cx)
|
||||
.diff_base()
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let change_set = &editor
|
||||
.diff_map
|
||||
.diff_bases
|
||||
.get(&buffer_id)
|
||||
.expect("should have a diff base for expanded hunk")
|
||||
.change_set;
|
||||
let deleted_text = change_set
|
||||
.read(cx)
|
||||
.base_text
|
||||
.as_ref()
|
||||
.expect("no base text for expanded hunk")
|
||||
.read(cx)
|
||||
.as_rope()
|
||||
.slice(hunk.diff_base_byte_range.clone())
|
||||
.to_string();
|
||||
if let DiffHunkStatus::Modified | DiffHunkStatus::Removed = hunk.status {
|
||||
@@ -384,7 +409,7 @@ impl EditorTestContext {
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
format_diff(text, deletions, insertions)
|
||||
format_diff(actual_marked_text, deletions, insertions)
|
||||
});
|
||||
|
||||
pretty_assertions::assert_eq!(actual_diff, expected_diff_text, "unexpected diff state");
|
||||
|
||||
+8
-5
@@ -132,7 +132,7 @@ pub trait Fs: Send + Sync {
|
||||
async fn is_case_sensitive(&self) -> Result<bool>;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn as_fake(&self) -> &FakeFs {
|
||||
fn as_fake(&self) -> Arc<FakeFs> {
|
||||
panic!("called as_fake on a real fs");
|
||||
}
|
||||
}
|
||||
@@ -840,6 +840,7 @@ impl Watcher for RealWatcher {
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeFs {
|
||||
this: std::sync::Weak<Self>,
|
||||
// Use an unfair lock to ensure tests are deterministic.
|
||||
state: Mutex<FakeFsState>,
|
||||
executor: gpui::BackgroundExecutor,
|
||||
@@ -1022,7 +1023,8 @@ impl FakeFs {
|
||||
pub fn new(executor: gpui::BackgroundExecutor) -> Arc<Self> {
|
||||
let (tx, mut rx) = smol::channel::bounded::<PathBuf>(10);
|
||||
|
||||
let this = Arc::new(Self {
|
||||
let this = Arc::new_cyclic(|this| Self {
|
||||
this: this.clone(),
|
||||
executor: executor.clone(),
|
||||
state: Mutex::new(FakeFsState {
|
||||
root: Arc::new(Mutex::new(FakeFsEntry::Dir {
|
||||
@@ -1474,7 +1476,8 @@ struct FakeHandle {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FileHandle for FakeHandle {
|
||||
fn current_path(&self, fs: &Arc<dyn Fs>) -> Result<PathBuf> {
|
||||
let state = fs.as_fake().state.lock();
|
||||
let fs = fs.as_fake();
|
||||
let state = fs.state.lock();
|
||||
let Some(target) = state.moves.get(&self.inode) else {
|
||||
anyhow::bail!("fake fd not moved")
|
||||
};
|
||||
@@ -1970,8 +1973,8 @@ impl Fs for FakeFs {
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn as_fake(&self) -> &FakeFs {
|
||||
self
|
||||
fn as_fake(&self) -> Arc<FakeFs> {
|
||||
self.this.upgrade().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ path = "src/git.rs"
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
git2.workspace = true
|
||||
|
||||
+18
-19
@@ -64,18 +64,33 @@ impl sum_tree::Summary for DiffHunkSummary {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BufferDiff {
|
||||
last_buffer_version: Option<clock::Global>,
|
||||
tree: SumTree<InternalDiffHunk>,
|
||||
}
|
||||
|
||||
impl BufferDiff {
|
||||
pub fn new(buffer: &BufferSnapshot) -> BufferDiff {
|
||||
BufferDiff {
|
||||
last_buffer_version: None,
|
||||
tree: SumTree::new(buffer),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build(diff_base: &str, buffer: &text::BufferSnapshot) -> Self {
|
||||
let mut tree = SumTree::new(buffer);
|
||||
|
||||
let buffer_text = buffer.as_rope().to_string();
|
||||
let patch = Self::diff(diff_base, &buffer_text);
|
||||
|
||||
if let Some(patch) = patch {
|
||||
let mut divergence = 0;
|
||||
for hunk_index in 0..patch.num_hunks() {
|
||||
let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence);
|
||||
tree.push(hunk, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
Self { tree }
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tree.is_empty()
|
||||
}
|
||||
@@ -168,27 +183,11 @@ impl BufferDiff {
|
||||
|
||||
#[cfg(test)]
|
||||
fn clear(&mut self, buffer: &text::BufferSnapshot) {
|
||||
self.last_buffer_version = Some(buffer.version().clone());
|
||||
self.tree = SumTree::new(buffer);
|
||||
}
|
||||
|
||||
pub async fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) {
|
||||
let mut tree = SumTree::new(buffer);
|
||||
|
||||
let diff_base_text = diff_base.to_string();
|
||||
let buffer_text = buffer.as_rope().to_string();
|
||||
let patch = Self::diff(&diff_base_text, &buffer_text);
|
||||
|
||||
if let Some(patch) = patch {
|
||||
let mut divergence = 0;
|
||||
for hunk_index in 0..patch.num_hunks() {
|
||||
let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence);
|
||||
tree.push(hunk, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
self.tree = tree;
|
||||
self.last_buffer_version = Some(buffer.version().clone());
|
||||
*self = Self::build(&diff_base.to_string(), buffer).await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -34,7 +34,6 @@ ec4rs.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
git.workspace = true
|
||||
globset.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
|
||||
+20
-154
@@ -90,22 +90,11 @@ pub enum Capability {
|
||||
|
||||
pub type BufferRow = u32;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum BufferDiffBase {
|
||||
Git(Rope),
|
||||
PastBufferVersion {
|
||||
buffer: Model<Buffer>,
|
||||
rope: Rope,
|
||||
merged_operations: Vec<Lamport>,
|
||||
},
|
||||
}
|
||||
|
||||
/// An in-memory representation of a source code file, including its text,
|
||||
/// syntax trees, git status, and diagnostics.
|
||||
pub struct Buffer {
|
||||
text: TextBuffer,
|
||||
diff_base: Option<BufferDiffBase>,
|
||||
git_diff: git::diff::BufferDiff,
|
||||
branch_state: Option<BufferBranchState>,
|
||||
/// Filesystem state, `None` when there is no path.
|
||||
file: Option<Arc<dyn File>>,
|
||||
/// The mtime of the file when this buffer was last loaded from
|
||||
@@ -135,7 +124,6 @@ pub struct Buffer {
|
||||
deferred_ops: OperationQueue<Operation>,
|
||||
capability: Capability,
|
||||
has_conflict: bool,
|
||||
diff_base_version: usize,
|
||||
/// Memoize calls to has_changes_since(saved_version).
|
||||
/// The contents of a cell are (self.version, has_changes) at the time of a last call.
|
||||
has_unsaved_edits: Cell<(clock::Global, bool)>,
|
||||
@@ -148,11 +136,15 @@ pub enum ParseStatus {
|
||||
Parsing,
|
||||
}
|
||||
|
||||
struct BufferBranchState {
|
||||
base_buffer: Model<Buffer>,
|
||||
merged_operations: Vec<Lamport>,
|
||||
}
|
||||
|
||||
/// An immutable, cheaply cloneable representation of a fixed
|
||||
/// state of a buffer.
|
||||
pub struct BufferSnapshot {
|
||||
text: text::BufferSnapshot,
|
||||
git_diff: git::diff::BufferDiff,
|
||||
pub(crate) syntax: SyntaxSnapshot,
|
||||
file: Option<Arc<dyn File>>,
|
||||
diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>,
|
||||
@@ -345,10 +337,6 @@ pub enum BufferEvent {
|
||||
Reloaded,
|
||||
/// The buffer is in need of a reload
|
||||
ReloadNeeded,
|
||||
/// The buffer's diff_base changed.
|
||||
DiffBaseChanged,
|
||||
/// Buffer's excerpts for a certain diff base were recalculated.
|
||||
DiffUpdated,
|
||||
/// The buffer's language was changed.
|
||||
LanguageChanged,
|
||||
/// The buffer's syntax trees were updated.
|
||||
@@ -626,7 +614,6 @@ impl Buffer {
|
||||
Self::build(
|
||||
TextBuffer::new(0, cx.entity_id().as_non_zero_u64().into(), base_text.into()),
|
||||
None,
|
||||
None,
|
||||
Capability::ReadWrite,
|
||||
)
|
||||
}
|
||||
@@ -645,7 +632,6 @@ impl Buffer {
|
||||
base_text_normalized,
|
||||
),
|
||||
None,
|
||||
None,
|
||||
Capability::ReadWrite,
|
||||
)
|
||||
}
|
||||
@@ -660,7 +646,6 @@ impl Buffer {
|
||||
Self::build(
|
||||
TextBuffer::new(replica_id, remote_id, base_text.into()),
|
||||
None,
|
||||
None,
|
||||
capability,
|
||||
)
|
||||
}
|
||||
@@ -676,7 +661,7 @@ impl Buffer {
|
||||
let buffer_id = BufferId::new(message.id)
|
||||
.with_context(|| anyhow!("Could not deserialize buffer_id"))?;
|
||||
let buffer = TextBuffer::new(replica_id, buffer_id, message.base_text);
|
||||
let mut this = Self::build(buffer, message.diff_base, file, capability);
|
||||
let mut this = Self::build(buffer, file, capability);
|
||||
this.text.set_line_ending(proto::deserialize_line_ending(
|
||||
rpc::proto::LineEnding::from_i32(message.line_ending)
|
||||
.ok_or_else(|| anyhow!("missing line_ending"))?,
|
||||
@@ -692,7 +677,6 @@ impl Buffer {
|
||||
id: self.remote_id().into(),
|
||||
file: self.file.as_ref().map(|f| f.to_proto(cx)),
|
||||
base_text: self.base_text().to_string(),
|
||||
diff_base: self.diff_base().as_ref().map(|h| h.to_string()),
|
||||
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
|
||||
saved_version: proto::serialize_version(&self.saved_version),
|
||||
saved_mtime: self.saved_mtime.map(|time| time.into()),
|
||||
@@ -766,15 +750,9 @@ impl Buffer {
|
||||
}
|
||||
|
||||
/// Builds a [`Buffer`] with the given underlying [`TextBuffer`], diff base, [`File`] and [`Capability`].
|
||||
pub fn build(
|
||||
buffer: TextBuffer,
|
||||
diff_base: Option<String>,
|
||||
file: Option<Arc<dyn File>>,
|
||||
capability: Capability,
|
||||
) -> Self {
|
||||
pub fn build(buffer: TextBuffer, file: Option<Arc<dyn File>>, capability: Capability) -> Self {
|
||||
let saved_mtime = file.as_ref().and_then(|file| file.disk_state().mtime());
|
||||
let snapshot = buffer.snapshot();
|
||||
let git_diff = git::diff::BufferDiff::new(&snapshot);
|
||||
let syntax_map = Mutex::new(SyntaxMap::new(&snapshot));
|
||||
Self {
|
||||
saved_mtime,
|
||||
@@ -785,12 +763,7 @@ impl Buffer {
|
||||
was_dirty_before_starting_transaction: None,
|
||||
has_unsaved_edits: Cell::new((buffer.version(), false)),
|
||||
text: buffer,
|
||||
diff_base: diff_base.map(|mut raw_diff_base| {
|
||||
LineEnding::normalize(&mut raw_diff_base);
|
||||
BufferDiffBase::Git(Rope::from(raw_diff_base))
|
||||
}),
|
||||
diff_base_version: 0,
|
||||
git_diff,
|
||||
branch_state: None,
|
||||
file,
|
||||
capability,
|
||||
syntax_map,
|
||||
@@ -824,7 +797,6 @@ impl Buffer {
|
||||
BufferSnapshot {
|
||||
text,
|
||||
syntax,
|
||||
git_diff: self.git_diff.clone(),
|
||||
file: self.file.clone(),
|
||||
remote_selections: self.remote_selections.clone(),
|
||||
diagnostics: self.diagnostics.clone(),
|
||||
@@ -837,21 +809,15 @@ impl Buffer {
|
||||
let this = cx.handle();
|
||||
cx.new_model(|cx| {
|
||||
let mut branch = Self {
|
||||
diff_base: Some(BufferDiffBase::PastBufferVersion {
|
||||
buffer: this.clone(),
|
||||
rope: self.as_rope().clone(),
|
||||
branch_state: Some(BufferBranchState {
|
||||
base_buffer: this.clone(),
|
||||
merged_operations: Default::default(),
|
||||
}),
|
||||
language: self.language.clone(),
|
||||
has_conflict: self.has_conflict,
|
||||
has_unsaved_edits: Cell::new(self.has_unsaved_edits.get_mut().clone()),
|
||||
_subscriptions: vec![cx.subscribe(&this, Self::on_base_buffer_event)],
|
||||
..Self::build(
|
||||
self.text.branch(),
|
||||
None,
|
||||
self.file.clone(),
|
||||
self.capability(),
|
||||
)
|
||||
..Self::build(self.text.branch(), self.file.clone(), self.capability())
|
||||
};
|
||||
if let Some(language_registry) = self.language_registry() {
|
||||
branch.set_language_registry(language_registry);
|
||||
@@ -870,7 +836,7 @@ impl Buffer {
|
||||
/// If `ranges` is empty, then all changes will be applied. This buffer must
|
||||
/// be a branch buffer to call this method.
|
||||
pub fn merge_into_base(&mut self, ranges: Vec<Range<usize>>, cx: &mut ModelContext<Self>) {
|
||||
let Some(base_buffer) = self.diff_base_buffer() else {
|
||||
let Some(base_buffer) = self.base_buffer() else {
|
||||
debug_panic!("not a branch buffer");
|
||||
return;
|
||||
};
|
||||
@@ -906,14 +872,14 @@ impl Buffer {
|
||||
}
|
||||
|
||||
let operation = base_buffer.update(cx, |base_buffer, cx| {
|
||||
cx.emit(BufferEvent::DiffBaseChanged);
|
||||
// cx.emit(BufferEvent::DiffBaseChanged);
|
||||
base_buffer.edit(edits, None, cx)
|
||||
});
|
||||
|
||||
if let Some(operation) = operation {
|
||||
if let Some(BufferDiffBase::PastBufferVersion {
|
||||
if let Some(BufferBranchState {
|
||||
merged_operations, ..
|
||||
}) = &mut self.diff_base
|
||||
}) = &mut self.branch_state
|
||||
{
|
||||
merged_operations.push(operation);
|
||||
}
|
||||
@@ -929,9 +895,9 @@ impl Buffer {
|
||||
let BufferEvent::Operation { operation, .. } = event else {
|
||||
return;
|
||||
};
|
||||
let Some(BufferDiffBase::PastBufferVersion {
|
||||
let Some(BufferBranchState {
|
||||
merged_operations, ..
|
||||
}) = &mut self.diff_base
|
||||
}) = &mut self.branch_state
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -950,8 +916,6 @@ impl Buffer {
|
||||
let counts = [(timestamp, u32::MAX)].into_iter().collect();
|
||||
self.undo_operations(counts, cx);
|
||||
}
|
||||
|
||||
self.diff_base_version += 1;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1123,74 +1087,8 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current diff base, see [`Buffer::set_diff_base`].
|
||||
pub fn diff_base(&self) -> Option<&Rope> {
|
||||
match self.diff_base.as_ref()? {
|
||||
BufferDiffBase::Git(rope) | BufferDiffBase::PastBufferVersion { rope, .. } => {
|
||||
Some(rope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the text that will be used to compute a Git diff
|
||||
/// against the buffer text.
|
||||
pub fn set_diff_base(&mut self, diff_base: Option<String>, cx: &ModelContext<Self>) {
|
||||
self.diff_base = diff_base.map(|mut raw_diff_base| {
|
||||
LineEnding::normalize(&mut raw_diff_base);
|
||||
BufferDiffBase::Git(Rope::from(raw_diff_base))
|
||||
});
|
||||
self.diff_base_version += 1;
|
||||
if let Some(recalc_task) = self.recalculate_diff(cx) {
|
||||
cx.spawn(|buffer, mut cx| async move {
|
||||
recalc_task.await;
|
||||
buffer
|
||||
.update(&mut cx, |_, cx| {
|
||||
cx.emit(BufferEvent::DiffBaseChanged);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a number, unique per diff base set to the buffer.
|
||||
pub fn diff_base_version(&self) -> usize {
|
||||
self.diff_base_version
|
||||
}
|
||||
|
||||
pub fn diff_base_buffer(&self) -> Option<Model<Self>> {
|
||||
match self.diff_base.as_ref()? {
|
||||
BufferDiffBase::Git(_) => None,
|
||||
BufferDiffBase::PastBufferVersion { buffer, .. } => Some(buffer.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Recomputes the diff.
|
||||
pub fn recalculate_diff(&self, cx: &ModelContext<Self>) -> Option<Task<()>> {
|
||||
let diff_base_rope = match self.diff_base.as_ref()? {
|
||||
BufferDiffBase::Git(rope) => rope.clone(),
|
||||
BufferDiffBase::PastBufferVersion { buffer, .. } => buffer.read(cx).as_rope().clone(),
|
||||
};
|
||||
|
||||
let snapshot = self.snapshot();
|
||||
let mut diff = self.git_diff.clone();
|
||||
let diff = cx.background_executor().spawn(async move {
|
||||
diff.update(&diff_base_rope, &snapshot).await;
|
||||
(diff, diff_base_rope)
|
||||
});
|
||||
|
||||
Some(cx.spawn(|this, mut cx| async move {
|
||||
let (buffer_diff, diff_base_rope) = diff.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.git_diff = buffer_diff;
|
||||
this.non_text_state_update_count += 1;
|
||||
if let Some(BufferDiffBase::PastBufferVersion { rope, .. }) = &mut this.diff_base {
|
||||
*rope = diff_base_rope;
|
||||
}
|
||||
cx.emit(BufferEvent::DiffUpdated);
|
||||
})
|
||||
.ok();
|
||||
}))
|
||||
pub fn base_buffer(&self) -> Option<Model<Self>> {
|
||||
Some(self.branch_state.as_ref()?.base_buffer.clone())
|
||||
}
|
||||
|
||||
/// Returns the primary [`Language`] assigned to this [`Buffer`].
|
||||
@@ -3992,37 +3890,6 @@ impl BufferSnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the buffer contains any Git changes.
|
||||
pub fn has_git_diff(&self) -> bool {
|
||||
!self.git_diff.is_empty()
|
||||
}
|
||||
|
||||
/// Returns all the Git diff hunks intersecting the given row range.
|
||||
pub fn git_diff_hunks_in_row_range(
|
||||
&self,
|
||||
range: Range<BufferRow>,
|
||||
) -> impl '_ + Iterator<Item = git::diff::DiffHunk> {
|
||||
self.git_diff.hunks_in_row_range(range, self)
|
||||
}
|
||||
|
||||
/// Returns all the Git diff hunks intersecting the given
|
||||
/// range.
|
||||
pub fn git_diff_hunks_intersecting_range(
|
||||
&self,
|
||||
range: Range<Anchor>,
|
||||
) -> impl '_ + Iterator<Item = git::diff::DiffHunk> {
|
||||
self.git_diff.hunks_intersecting_range(range, self)
|
||||
}
|
||||
|
||||
/// Returns all the Git diff hunks intersecting the given
|
||||
/// range, in reverse order.
|
||||
pub fn git_diff_hunks_intersecting_range_rev(
|
||||
&self,
|
||||
range: Range<Anchor>,
|
||||
) -> impl '_ + Iterator<Item = git::diff::DiffHunk> {
|
||||
self.git_diff.hunks_intersecting_range_rev(range, self)
|
||||
}
|
||||
|
||||
/// Returns if the buffer contains any diagnostics.
|
||||
pub fn has_diagnostics(&self) -> bool {
|
||||
!self.diagnostics.is_empty()
|
||||
@@ -4167,7 +4034,6 @@ impl Clone for BufferSnapshot {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
text: self.text.clone(),
|
||||
git_diff: self.git_diff.clone(),
|
||||
syntax: self.syntax.clone(),
|
||||
file: self.file.clone(),
|
||||
remote_selections: self.remote_selections.clone(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user