Git partially staged changes (#46541)

This PR explores the addition of a new feature and UI to improve
visibility into partially staged commits.

Currently, the Git panel shows tracked and untracked changes, but it
does not clearly distinguish between staged and unstaged changes.

As a result, it’s difficult to quickly see which changes are not staged
in the current UI. Both staged and unstaged changes are combined into
the `Uncommitted Changes` multibuffer. This developer experience differs
from other editors, most notably VS Code; which presents separate Staged
Changes and Changes lists.

### Staged and unstaged diffs in multibuffers

This PR introduces an alternative UI for unstaged changes that aligns
with the overall Zed experience. Instead of showing changes on a
per-file basis, staged and unstaged diffs are each displayed in their
own multibuffers, similar to how `Uncommitted Changes` currently works.

For example the following screenshot shows the current `Uncommitted
Changes` on the left, the `Staged Changes` in the middle and the
`Unstaged Changes` buffer on the right for comparison

<img width="1408" height="859"
src="https://github.com/user-attachments/assets/aa709f7a-041d-4cb1-95d6-84c0f5fff688"
/>

### Indicators/interactions

The new multibuffers can be opened in two ways:

1. Via a new `U` chip, which appears when a file has unstaged changes
2. Via new menu options

(See screenshots below for both interaction paths.)


<table>
  <tr>
    <td style="text-align: center; vertical-align: top;">
      <p>via the chip</p>
      <img
        height="400"

src="https://github.com/user-attachments/assets/3ef69f02-b787-499c-959a-25f50b3728e8"
        alt="Via the chip"
      />
    </td>
    <td style="text-align: center; vertical-align: top;">
      <p>via the menu</p>
      <img
        height="400"

src="https://github.com/user-attachments/assets/f5be8b6d-ccdc-4420-bd29-75570b558016"
        alt="Via the menu"
      />
    </td>
  </tr>
</table>

### Design goals
- minimally intrusive UI changes (small new badge and menu items)
- adhere by Zed'ism (use multibuffer where possible)
- avoid disabling any current interactions (Uncommitted Changes ui is
unchanged)
- avoid introducing an app level view mode (no new settings needed)

### Experience goals
- make it easy to see what changes are not staged
- make it easy to see that a file has unstaged changes (avoid developers
accidently leaving out changes in a commit; a personal issue that I have
when using Zed)
- elegantly handle large file's unstaged changes (follows the same
collapse and expanding seen in `Uncommitted Changes`)

### How to try

- Clone the repo and run `cargo run`
- Make a change to a file and stage it
- Make another change to the file (the `U` indicator will appear)
- Click the `U` to see the unstaged view

### Open questions/rough edges
- [ ] determine if this user experience is useful for others
- [ ] ensure all interactions work as expected (response to all update
cases)

In general I'm really interested in hearing the community's feedback
about this interface, more than happy to make any changes or explore a
different solution!

### Related issue:
- https://github.com/zed-industries/zed/pull/36646
- https://github.com/zed-industries/zed/issues/26560

Release Notes:

- Support partially staged commit multibuffers via a staged and unstaged
changes view.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
drbh 2026-07-07 02:55:29 -04:00 committed by GitHub
parent 872ca8fef5
commit c31b2b0dc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 7599 additions and 2833 deletions

View file

@ -6,8 +6,8 @@ use anyhow::Result;
use buffer_diff::DiffHunkStatus;
use collections::{HashMap, HashSet};
use editor::{
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
SelectionEffects, SplittableEditor, ToPoint,
DiffHunkDelegate, Direction, Editor, EditorEvent, EditorSettings, MultiBuffer,
MultiBufferSnapshot, ResolvedDiffHunks, SelectionEffects, SplittableEditor, ToPoint,
actions::{GoToHunk, GoToPreviousHunk},
multibuffer_context_lines,
scroll::Autoscroll,
@ -101,8 +101,7 @@ impl AgentDiffPane {
cx,
);
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);
.set_diff_hunk_delegate(Some(agent_diff_delegate(&thread, workspace.clone())), cx);
diff_display_editor.update_editors(cx, |editor, _cx| {
editor.register_addon(AgentDiffAddon);
});
@ -722,29 +721,68 @@ impl Render for AgentDiffPane {
}
}
fn diff_hunk_controls(
struct AgentDiffDelegate {
thread: Entity<AcpThread>,
workspace: WeakEntity<Workspace>,
}
fn agent_diff_delegate(
thread: &Entity<AcpThread>,
workspace: WeakEntity<Workspace>,
) -> editor::RenderDiffHunkControlsFn {
let thread = thread.clone();
) -> Arc<dyn DiffHunkDelegate> {
Arc::new(AgentDiffDelegate {
thread: thread.clone(),
workspace,
})
}
Arc::new(
move |row, status, hunk_range, is_created_file, line_height, editor, _, cx| {
{
render_diff_hunk_controls(
row,
status,
hunk_range,
is_created_file,
line_height,
&thread,
editor,
workspace.clone(),
cx,
)
}
},
)
impl DiffHunkDelegate for AgentDiffDelegate {
fn toggle(
&self,
_hunks: Vec<ResolvedDiffHunks>,
_editor: &mut Editor,
_window: &mut Window,
_cx: &mut Context<Editor>,
) {
}
fn stage_or_unstage(
&self,
_stage: bool,
_hunks: Vec<ResolvedDiffHunks>,
_editor: &mut Editor,
_window: &mut Window,
_cx: &mut Context<Editor>,
) {
}
fn render_hunk_controls(
&self,
row: u32,
status: &DiffHunkStatus,
hunk_range: Range<editor::Anchor>,
is_created_file: bool,
line_height: Pixels,
editor: &Entity<Editor>,
_window: &mut Window,
cx: &mut App,
) -> AnyElement {
render_diff_hunk_controls(
row,
status,
hunk_range,
is_created_file,
line_height,
&self.thread,
editor,
self.workspace.clone(),
cx,
)
}
fn render_hunk_as_staged(&self, _status: &DiffHunkStatus, _cx: &App) -> bool {
false
}
}
fn render_diff_hunk_controls(
@ -1528,7 +1566,7 @@ impl AgentDiff {
for (editor, _) in self.reviewing_editors.drain() {
editor
.update(cx, |editor, cx| {
editor.end_temporary_diff_override(cx);
editor.set_diff_hunk_delegate(None, cx);
editor.unregister_addon::<EditorAgentDiffAddon>();
})
.ok();
@ -1577,12 +1615,10 @@ impl AgentDiff {
if previous_state.is_none() {
editor.update(cx, |editor, cx| {
editor.start_temporary_diff_override();
editor.set_render_diff_hunk_controls(
diff_hunk_controls(&thread, workspace.clone()),
editor.set_diff_hunk_delegate(
Some(agent_diff_delegate(&thread, workspace.clone())),
cx,
);
editor.set_render_diff_hunks_as_unstaged(true, cx);
editor.set_expand_all_diff_hunks(cx);
editor.register_addon(EditorAgentDiffAddon);
});
@ -1629,7 +1665,7 @@ impl AgentDiff {
if in_workspace {
editor
.update(cx, |editor, cx| {
editor.end_temporary_diff_override(cx);
editor.set_diff_hunk_delegate(None, cx);
editor.unregister_addon::<EditorAgentDiffAddon>();
})
.ok();

View file

@ -1,11 +1,14 @@
use std::ops::Range;
use std::{ops::Range, sync::Arc};
use acp_thread::{AcpThread, AgentThreadEntry, AssistantMessageChunk};
use agent::ThreadStore;
use agent_client_protocol::schema::v1 as acp;
use agent_settings::AgentSettings;
use collections::{HashMap, HashSet};
use editor::{Editor, EditorEvent, EditorMode, MinimapVisibility, SizingBehavior};
use editor::{
Editor, EditorEvent, EditorMode, MinimapVisibility, RestoreOnlyUnstagedDiffHunkDelegate,
SizingBehavior,
};
use gpui::{
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
ScrollHandle, TextStyleRefinement, WeakEntity, Window,
@ -682,7 +685,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_diff_hunk_delegate(Some(Arc::new(RestoreOnlyUnstagedDiffHunkDelegate)), cx);
editor.set_text_style_refinement(diff_editor_text_style_refinement(cx));
editor
})

View file

@ -122,11 +122,37 @@ struct InternalDiffHunk {
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct PendingHunk {
pub struct PendingHunk {
buffer_range: Range<Anchor>,
diff_base_byte_range: Range<usize>,
buffer_version: clock::Global,
new_status: DiffHunkSecondaryStatus,
sense: PendingSense,
}
impl PendingHunk {
pub fn new(
buffer_range: Range<Anchor>,
diff_base_byte_range: Range<usize>,
buffer_version: clock::Global,
sense: PendingSense,
) -> Self {
Self {
buffer_range,
diff_base_byte_range,
buffer_version,
sense,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PendingSense {
/// Override the secondary status of the matched hunk (used by the
/// uncommitted diff to show a hunk as staging/unstaging in place).
SetSecondaryStatus { stage: bool },
/// Suppress the matched hunk entirely (used by the unstaged/staged diffs so
/// that a hunk disappears the moment it is staged/unstaged).
Suppress,
}
#[derive(Debug, Clone)]
@ -294,6 +320,56 @@ impl BufferDiffSnapshot {
self.hunks_intersecting_range_impl(filter, buffer, unstaged_counterpart)
}
/// Like [`hunks_intersecting_range`], but ignores optimistic pending hunks
/// (both secondary-status overrides and suppressions) and does not compute a
/// secondary status.
pub fn raw_hunks_intersecting_range<'a>(
&'a self,
range: Range<Anchor>,
buffer: &'a text::BufferSnapshot,
) -> impl 'a + Iterator<Item = DiffHunk> {
let range = range.to_offset(buffer);
let filter = move |summary: &DiffHunkSummary| {
let summary_range = summary.buffer_range.to_offset(buffer);
!(summary_range.end < range.start) && !(summary_range.start > range.end)
};
self.hunks
.filter::<_, DiffHunkSummary>(buffer, filter)
.map(move |hunk| {
let buffer_range = hunk.buffer_range.clone();
DiffHunk {
range: buffer_range.to_point(buffer),
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
buffer_range,
secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
base_word_diffs: hunk.base_word_diffs.clone(),
buffer_word_diffs: hunk.buffer_word_diffs.clone(),
}
})
}
/// Maps a range in this diff's main buffer to the range it covers in the
/// base text, expanding to whole hunks wherever the range endpoints fall
/// inside or touch a hunk (`edit_for_old_position` is inclusive on both
/// boundaries, matching `raw_hunks_intersecting_range`). Used by the
/// index-write path to compute the index-coordinate footprint of a staging
/// operation; like the raw hunks, the mapping ignores optimistic pending
/// hunks.
pub fn base_text_range_for_buffer_range(
&self,
range: Range<Anchor>,
buffer: &text::BufferSnapshot,
) -> Range<usize> {
let point_range = range.to_point(buffer);
let patch = self.patch_for_buffer_range(point_range.start..=point_range.end, buffer);
let start_point = patch.edit_for_old_position(point_range.start).new.start;
let end_point = patch.edit_for_old_position(point_range.end).new.end;
let base_text = self.base_text();
let start = base_text.point_to_offset(start_point.min(base_text.max_point()));
let end = base_text.point_to_offset(end_point.min(base_text.max_point()));
start.min(end)..end
}
pub fn hunks_intersecting_range_rev<'a>(
&'a self,
range: Range<Anchor>,
@ -705,114 +781,80 @@ impl BufferDiffSnapshot {
}
impl BufferDiffSnapshot {
fn stage_or_unstage_hunks_impl(
&mut self,
// Compute the edits to apply to the index, and the resulting pending hunks,
// for a stage or unstage operation on the uncommitted diff.
pub fn compute_uncommitted_index_edits(
&self,
unstaged_diff: &Self,
stage: bool,
hunks: &[DiffHunk],
buffer: &text::BufferSnapshot,
file_exists: bool,
) -> Option<Rope> {
) -> (Option<Vec<(Range<usize>, Arc<str>)>>, Vec<PendingHunk>) {
let head_text = self
.base_text_exists
.then(|| self.base_text.as_rope().clone());
let index_text = unstaged_diff
.base_text_exists
.then(|| unstaged_diff.base_text.as_rope().clone());
let sense = PendingSense::SetSecondaryStatus { stage };
let version = buffer.version().clone();
// If the file doesn't exist in either HEAD or the index, then the
// entire file must be either created or deleted in the index.
let (index_text, head_text) = match (index_text, head_text) {
(Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
(index_text, head_text) => {
let (new_index_text, new_status) = if stage {
let index_len = index_text.as_ref().map_or(0, |rope| rope.len());
let new_index_text: Option<Rope> = if stage {
log::debug!("stage all");
(
file_exists.then(|| buffer.as_rope().clone()),
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
)
file_exists.then(|| buffer.as_rope().clone())
} else {
log::debug!("unstage all");
(
head_text,
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
)
head_text
};
let hunk = PendingHunk {
buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()),
diff_base_byte_range: 0..index_text.map_or(0, |rope| rope.len()),
buffer_version: buffer.version().clone(),
new_status,
};
self.pending_hunks = SumTree::from_item(hunk, buffer);
return new_index_text;
let pending = vec![PendingHunk::new(
Anchor::min_max_range_for_buffer(buffer.remote_id()),
0..index_len,
version,
sense,
)];
let edits =
new_index_text.map(|rope| vec![(0..index_len, Arc::from(rope.to_string()))]);
return (edits, pending);
}
};
let mut pending_hunks = SumTree::new(buffer);
let mut old_pending_hunks = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
// first, merge new hunks into pending_hunks
for DiffHunk {
buffer_range,
diff_base_byte_range,
secondary_status,
..
} in hunks.iter().cloned()
{
let preceding_pending_hunks = old_pending_hunks.slice(&buffer_range.start, Bias::Left);
pending_hunks.append(preceding_pending_hunks, buffer);
// Skip all overlapping or adjacent old pending hunks
while old_pending_hunks.item().is_some_and(|old_hunk| {
old_hunk
.buffer_range
.start
.cmp(&buffer_range.end, buffer)
.is_le()
}) {
old_pending_hunks.next();
}
if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
|| (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
{
continue;
}
pending_hunks.push(
PendingHunk {
buffer_range,
diff_base_byte_range,
buffer_version: buffer.version().clone(),
new_status: if stage {
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
} else {
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
},
},
buffer,
);
}
// append the remainder
pending_hunks.append(old_pending_hunks.suffix(), buffer);
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
unstaged_hunk_cursor.next();
// then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits
let mut prev_unstaged_hunk_buffer_end = 0;
let mut prev_unstaged_hunk_base_text_end = 0;
let mut edits = Vec::<(Range<usize>, String)>::new();
let mut pending_hunks_iter = pending_hunks.iter().cloned().peekable();
while let Some(PendingHunk {
buffer_range,
diff_base_byte_range,
new_status,
..
}) = pending_hunks_iter.next()
{
let mut edits = Vec::<(Range<usize>, Arc<str>)>::new();
let mut pending = Vec::<PendingHunk>::new();
// Process only the hunks the user acted on, skipping any already in the
// desired state.
let mut hunks_iter = hunks
.iter()
.filter(|hunk| {
!((stage && hunk.secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
|| (!stage
&& hunk.secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk))
})
.peekable();
while let Some(hunk) = hunks_iter.next() {
let buffer_range = hunk.buffer_range.clone();
let diff_base_byte_range = hunk.diff_base_byte_range.clone();
pending.push(PendingHunk::new(
buffer_range.clone(),
diff_base_byte_range.clone(),
version.clone(),
sense,
));
// Advance unstaged_hunk_cursor to skip unstaged hunks before current hunk
let skipped_unstaged = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left);
@ -846,16 +888,20 @@ impl BufferDiffSnapshot {
}
}
// If any unstaged hunks were merged, then subsequent pending hunks may
// now overlap this hunk. Merge them.
if let Some(next_pending_hunk) = pending_hunks_iter.peek() {
let next_pending_hunk_offset_range =
next_pending_hunk.buffer_range.to_offset(buffer);
if next_pending_hunk_offset_range.start <= buffer_offset_range.end {
buffer_offset_range.end = buffer_offset_range
.end
.max(next_pending_hunk_offset_range.end);
pending_hunks_iter.next();
// If any unstaged hunks were merged, then subsequent acted-on hunks
// may now overlap this hunk. Merge them.
if let Some(next_hunk) = hunks_iter.peek() {
let next_hunk_offset_range = next_hunk.buffer_range.to_offset(buffer);
if next_hunk_offset_range.start <= buffer_offset_range.end {
buffer_offset_range.end =
buffer_offset_range.end.max(next_hunk_offset_range.end);
let merged_hunk = hunks_iter.next().expect("peeked hunk exists");
pending.push(PendingHunk::new(
merged_hunk.buffer_range.clone(),
merged_hunk.diff_base_byte_range.clone(),
version.clone(),
sense,
));
continue;
}
}
@ -879,49 +925,59 @@ impl BufferDiffSnapshot {
let index_start = index_start.min(index_end);
let index_byte_range = index_start..index_end;
let replacement_text = match new_status {
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
log::debug!("staging hunk {:?}", buffer_offset_range);
let replacement_text: Arc<str> = if stage {
log::debug!("staging hunk {:?}", buffer_offset_range);
Arc::from(
buffer
.text_for_range(buffer_offset_range)
.collect::<String>()
}
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending => {
log::debug!("unstaging hunk {:?}", buffer_offset_range);
.collect::<String>(),
)
} else {
log::debug!("unstaging hunk {:?}", buffer_offset_range);
Arc::from(
head_text
.chunks_in_range(diff_base_byte_range.clone())
.collect::<String>()
}
_ => {
debug_assert!(false);
continue;
}
.collect::<String>(),
)
};
edits.push((index_byte_range, replacement_text));
// Distinct worktree hunks can project to touching index ranges
// (e.g. a staged deletion ending exactly where the next hunk's
// index position starts). Merge them so the edit list stays
// strictly disjoint, which the pending-edit eviction logic relies
// on to not evict one of these edits when the other is inserted.
if let Some((last_range, last_text)) = edits.last_mut()
&& index_byte_range.start <= last_range.end
{
debug_assert!(index_byte_range.start == last_range.end);
debug_assert!(index_byte_range.end >= last_range.end);
last_range.end = index_byte_range.end;
let mut merged_text =
String::with_capacity(last_text.len() + replacement_text.len());
merged_text.push_str(last_text);
merged_text.push_str(&replacement_text);
*last_text = Arc::from(merged_text);
} else {
edits.push((index_byte_range, replacement_text));
}
}
drop(pending_hunks_iter);
drop(old_pending_hunks);
self.pending_hunks = pending_hunks;
#[cfg(debug_assertions)] // invariants: non-overlapping and sorted
{
for window in edits.windows(2) {
let (range_a, range_b) = (&window[0].0, &window[1].0);
debug_assert!(range_a.end < range_b.start);
debug_assert!(
range_a.end < range_b.start,
"index edits out of order or overlapping: {:?}",
edits
.iter()
.map(|(range, text)| (range.clone(), text.len()))
.collect::<Vec<_>>()
);
}
}
let mut new_index_text = Rope::new();
let mut index_cursor = index_text.cursor(0);
for (old_range, replacement_text) in edits {
new_index_text.append(index_cursor.slice(old_range.start));
index_cursor.seek_forward(old_range.end);
new_index_text.push(&replacement_text);
}
new_index_text.append(index_cursor.suffix());
Some(new_index_text)
(Some(edits), pending)
}
}
@ -1005,8 +1061,17 @@ impl BufferDiffSnapshot {
start_anchor..end_anchor,
)
{
has_pending = true;
secondary_status = pending_hunk.new_status;
match pending_hunk.sense {
PendingSense::SetSecondaryStatus { stage } => {
has_pending = true;
secondary_status = if stage {
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
} else {
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
};
}
PendingSense::Suppress => continue,
}
}
}
@ -1474,7 +1539,6 @@ pub struct DiffChanged {
pub enum BufferDiffEvent {
BaseTextChanged,
DiffChanged(DiffChanged),
HunksStagedOrUnstaged(Option<Rope>),
}
impl EventEmitter<BufferDiffEvent> for BufferDiff {}
@ -1594,24 +1658,195 @@ impl BufferDiff {
let Some(diff_snapshot) = &mut self.diff_snapshot else {
return;
};
if self.secondary_diff.is_some() {
diff_snapshot.pending_hunks = SumTree::from_summary(DiffHunkSummary {
buffer_range: Anchor::min_min_range_for_buffer(self.buffer_id),
diff_base_byte_range: 0..0,
added_rows: 0,
removed_rows: 0,
});
let changed_range = Some(Anchor::min_max_range_for_buffer(self.buffer_id));
let base_text_range = Some(0..self.base_text(cx).len());
let Some((first, last)) = diff_snapshot
.pending_hunks
.first()
.zip(diff_snapshot.pending_hunks.last())
else {
return;
};
let changed_range = first.buffer_range.start..last.buffer_range.end;
let base_text_changed_range =
first.diff_base_byte_range.start..last.diff_base_byte_range.end;
let buffer = diff_snapshot.buffer_snapshot.clone();
diff_snapshot.pending_hunks = SumTree::new(&buffer);
cx.emit(BufferDiffEvent::DiffChanged(DiffChanged {
changed_range: Some(changed_range.clone()),
base_text_changed_range: Some(base_text_changed_range),
extended_range: Some(changed_range),
base_text_changed: false,
}));
}
/// Installs optimistic pending hunks in this diff, merging them with any
/// existing pending hunks (newest wins on overlap) and emitting a
/// `DiffChanged` covering both the new hunks and any existing pending hunks
/// they replace. `hunks` must be sorted by `buffer_range.start` and
/// non-overlapping.
///
/// `buffer` must be a current snapshot of this diff's main buffer: the
/// incoming hunks carry anchors minted from the current buffer, which this
/// diff's internal snapshot (from the last settled recalculation) may not
/// have observed yet.
pub fn set_pending_hunks(
&mut self,
hunks: &[PendingHunk],
buffer: &text::BufferSnapshot,
cx: &mut Context<Self>,
) {
if hunks.is_empty() {
return;
}
let Some(diff_snapshot) = self.diff_snapshot.as_mut() else {
return;
};
let mut new_pending = SumTree::new(buffer);
let mut old = diff_snapshot
.pending_hunks
.cursor::<DiffHunkSummary>(buffer);
let mut changed_start: Option<Anchor> = None;
let mut changed_end: Option<Anchor> = None;
let mut base_start = usize::MAX;
let mut base_end = 0usize;
let mut extend_changed_range = |buffer_range: &Range<Anchor>, base_range: &Range<usize>| {
changed_start = Some(changed_start.map_or(buffer_range.start, |start| {
*start.min(&buffer_range.start, buffer)
}));
changed_end = Some(
changed_end.map_or(buffer_range.end, |end| *end.max(&buffer_range.end, buffer)),
);
base_start = base_start.min(base_range.start);
base_end = base_end.max(base_range.end);
};
for hunk in hunks {
let preceding = old.slice(&hunk.buffer_range.start, Bias::Left);
new_pending.append(preceding, buffer);
// Drop any overlapping or adjacent existing pending hunks, folding
// them into the changed range so that views repaint their full
// extent (a replaced hunk can be wider than its replacement).
while let Some(old_hunk) = old.item() {
if old_hunk
.buffer_range
.start
.cmp(&hunk.buffer_range.end, buffer)
.is_gt()
{
break;
}
extend_changed_range(&old_hunk.buffer_range, &old_hunk.diff_base_byte_range);
old.next();
}
extend_changed_range(&hunk.buffer_range, &hunk.diff_base_byte_range);
new_pending.push(hunk.clone(), buffer);
}
new_pending.append(old.suffix(), buffer);
drop(old);
diff_snapshot.pending_hunks = new_pending;
if let (Some(start), Some(end)) = (changed_start, changed_end) {
let changed_range = Some(start..end);
cx.emit(BufferDiffEvent::DiffChanged(DiffChanged {
changed_range: changed_range.clone(),
base_text_changed_range: base_text_range,
base_text_changed_range: Some(base_start..base_end),
extended_range: changed_range,
base_text_changed: false,
}));
}
}
/// Optimistically marks every stageable (resp. unstageable) hunk in this diff
/// as staging (resp. unstaging). Used by whole-file staging from the git
/// panel, where the actual index change is performed by `git add`/`reset`
/// rather than the optimistic index patch.
pub fn mark_all_hunks_pending(
&mut self,
stage: bool,
buffer: &text::BufferSnapshot,
cx: &mut Context<Self>,
) {
let sense = PendingSense::SetSecondaryStatus { stage };
let version = buffer.version().clone();
let hunks = self
.snapshot(cx)
.hunks_intersecting_range(Anchor::min_max_range_for_buffer(buffer.remote_id()), buffer)
.filter(|hunk| {
!((stage && hunk.secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
|| (!stage
&& hunk.secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk))
})
.map(|hunk| {
PendingHunk::new(
hunk.buffer_range,
hunk.diff_base_byte_range,
version.clone(),
sense,
)
})
.collect::<Vec<_>>();
self.set_pending_hunks(&hunks, buffer, cx);
}
pub fn suppress_all_hunks_pending(
&mut self,
buffer: &text::BufferSnapshot,
cx: &mut Context<Self>,
) {
let version = buffer.version().clone();
let hunks = self
.snapshot(cx)
.raw_hunks_intersecting_range(
Anchor::min_max_range_for_buffer(buffer.remote_id()),
buffer,
)
.map(|hunk| {
PendingHunk::new(
hunk.buffer_range,
hunk.diff_base_byte_range,
version.clone(),
PendingSense::Suppress,
)
})
.collect::<Vec<_>>();
self.set_pending_hunks(&hunks, buffer, cx);
}
/// Computes the index-text edits for unstaging the given staged (HEAD-vs-index)
/// hunks. `index_buffer` is this diff's main buffer (the index text). The
/// returned edits are in index coordinates.
pub fn unstage_staged_hunks(
&self,
hunks: &[DiffHunk],
index_buffer: &text::BufferSnapshot,
) -> Option<Vec<(Range<usize>, Arc<str>)>> {
let Some(diff_snapshot) = self.diff_snapshot.as_ref() else {
return Some(Vec::new());
};
// With no HEAD, the whole file is one staged addition; unstaging it
// removes the file from the index entirely.
if !diff_snapshot.base_text_exists {
return None;
}
let head_text = diff_snapshot.base_text.as_rope();
let mut edits = hunks
.iter()
.map(|hunk| {
let index_range = hunk.buffer_range.to_offset(index_buffer);
let replacement_text: Arc<str> = Arc::from(
head_text
.chunks_in_range(hunk.diff_base_byte_range.clone())
.collect::<String>(),
);
(index_range, replacement_text)
})
.collect::<Vec<_>>();
edits.sort_by_key(|(range, _)| range.start);
Some(edits)
}
#[cfg(any(test, feature = "test-support"))]
pub fn stage_or_unstage_hunks(
&mut self,
stage: bool,
@ -1621,74 +1856,40 @@ impl BufferDiff {
cx: &mut Context<Self>,
) -> Option<Rope> {
let secondary_diff = self.secondary_diff.clone()?;
let diff_snapshot = self.diff_snapshot.as_mut()?;
let unstaged_diff_snapshot = secondary_diff.read_with(cx, |secondary_diff, _cx| {
secondary_diff.diff_snapshot.clone()
})?;
let new_index_text = diff_snapshot.stage_or_unstage_hunks_impl(
let diff_snapshot = self.diff_snapshot.clone()?;
let (edits, pending) = diff_snapshot.compute_uncommitted_index_edits(
&unstaged_diff_snapshot,
stage,
hunks,
buffer,
file_exists,
);
cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
new_index_text.clone(),
));
if let Some((first, last)) = hunks.first().zip(hunks.last()) {
let changed_range = Some(first.buffer_range.start..last.buffer_range.end);
let base_text_changed_range =
Some(first.diff_base_byte_range.start..last.diff_base_byte_range.end);
cx.emit(BufferDiffEvent::DiffChanged(DiffChanged {
changed_range: changed_range.clone(),
base_text_changed_range,
extended_range: changed_range,
base_text_changed: false,
}));
}
new_index_text
self.set_pending_hunks(&pending, buffer, cx);
edits.map(|edits| {
let mut index_text = unstaged_diff_snapshot.base_text.as_rope().clone();
for (old_range, replacement_text) in edits.iter().rev() {
index_text.replace(old_range.clone(), replacement_text);
}
index_text
})
}
#[cfg(any(test, feature = "test-support"))]
pub fn stage_or_unstage_all_hunks(
&mut self,
stage: bool,
buffer: &text::BufferSnapshot,
file_exists: bool,
cx: &mut Context<Self>,
) {
) -> Option<Rope> {
let hunks = self
.snapshot(cx)
.hunks_intersecting_range(Anchor::min_max_range_for_buffer(buffer.remote_id()), buffer)
.collect::<Vec<_>>();
let Some(diff_snapshot) = &mut self.diff_snapshot else {
return;
};
let Some(secondary) = self.secondary_diff.clone() else {
return;
};
let secondary = secondary.read(cx);
let Some(secondary_snapshot) = &secondary.diff_snapshot else {
return;
};
diff_snapshot.stage_or_unstage_hunks_impl(
&secondary_snapshot,
stage,
&hunks,
buffer,
file_exists,
);
if let Some((first, last)) = hunks.first().zip(hunks.last()) {
let changed_range = Some(first.buffer_range.start..last.buffer_range.end);
let base_text_changed_range =
Some(first.diff_base_byte_range.start..last.diff_base_byte_range.end);
cx.emit(BufferDiffEvent::DiffChanged(DiffChanged {
changed_range: changed_range.clone(),
base_text_changed_range,
extended_range: changed_range,
base_text_changed: false,
}));
}
self.stage_or_unstage_hunks(stage, &hunks, buffer, file_exists, cx)
}
pub fn update_diff(
@ -2837,6 +3038,81 @@ mod tests {
});
}
#[gpui::test]
async fn test_set_pending_hunks_change_covers_replaced_hunks(cx: &mut TestAppContext) {
let base_text = "
zero
one
two
three
four
five
"
.unindent();
let buffer_text = "
ZERO
one
two
THREE
four
FIVE
"
.unindent();
let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
let diff = cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer, cx));
// Install a whole-file pending hunk, as the no-HEAD staging paths do.
let version = buffer.version();
diff.update(cx, |diff, cx| {
diff.set_pending_hunks(
&[PendingHunk::new(
Anchor::min_max_range_for_buffer(buffer.remote_id()),
0..base_text.len(),
version.clone(),
PendingSense::SetSecondaryStatus { stage: true },
)],
&buffer,
cx,
)
});
let (tx, rx) = mpsc::channel();
let subscription =
cx.update(|cx| cx.subscribe(&diff, move |_, event, _| tx.send(event.clone()).unwrap()));
// Replace it with a narrower hunk; the emitted change must still cover
// the whole extent of the replaced hunk.
diff.update(cx, |diff, cx| {
diff.set_pending_hunks(
&[PendingHunk::new(
buffer.anchor_before(Point::new(3, 0))..buffer.anchor_before(Point::new(4, 0)),
base_text.find("three").unwrap()..base_text.find("four").unwrap(),
version,
PendingSense::Suppress,
)],
&buffer,
cx,
)
});
drop(subscription);
let events = rx.into_iter().collect::<Vec<_>>();
match events.as_slice() {
[
BufferDiffEvent::DiffChanged(DiffChanged {
changed_range: Some(changed_range),
..
}),
] => {
assert_eq!(
changed_range.to_point(&buffer),
Point::zero()..buffer.max_point(),
);
}
_ => panic!("unexpected events: {:?}", events),
}
}
#[gpui::test]
async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
let base_text = "

View file

@ -360,10 +360,6 @@ impl Editor {
self.delegate_expand_excerpts = delegate;
}
pub(super) fn set_delegate_stage_and_restore(&mut self, delegate: bool) {
self.delegate_stage_and_restore = delegate;
}
pub(super) fn set_on_local_selections_changed(
&mut self,
callback: Option<Box<dyn Fn(Point, &mut Window, &mut Context<Self>) + 'static>>,

View file

@ -106,13 +106,16 @@ pub use element::{
file_status_label_color, render_breadcrumb_text,
};
pub use git::blame::BlameRenderer;
pub use git::{
DiffHunkDelegate, ResolvedDiffHunk, ResolvedDiffHunks, RestoreOnlyDiffHunkDelegate,
RestoreOnlyUnstagedDiffHunkDelegate, UncommittedDiffHunkDelegate, render_diff_hunk_controls,
set_blame_renderer,
};
pub(crate) use git::{DiffHunkKey, StoredReviewComment};
use git::{
DiffReviewDragState, DiffReviewOverlay, InlineBlamePopover, render_diff_hunk_controls,
update_uncommitted_diff_for_buffer,
DiffReviewDragState, DiffReviewOverlay, InlineBlamePopover, update_uncommitted_diff_for_buffer,
};
pub(crate) use git::{DisplayDiffHunk, PhantomDiffReviewIndicator};
pub use git::{RenderDiffHunkControlsFn, set_blame_renderer};
pub use hover_popover::hover_markdown_style;
pub use inlays::Inlay;
pub use items::MAX_TAB_TITLE_LEN;
@ -973,7 +976,6 @@ pub struct Editor {
offset_content: bool,
disable_expand_excerpt_buttons: bool,
delegate_expand_excerpts: bool,
delegate_stage_and_restore: bool,
delegate_open_excerpts: bool,
enable_lsp_data: bool,
needs_initial_data_update: bool,
@ -1074,7 +1076,6 @@ pub struct Editor {
show_git_blame_inline: bool,
show_git_blame_inline_delay_task: Option<Task<()>>,
git_blame_inline_enabled: bool,
render_diff_hunk_controls: RenderDiffHunkControlsFn,
buffer_serialization: Option<BufferSerialization>,
show_selection_menu: Option<bool>,
blame: Option<Entity<GitBlame>>,
@ -1129,12 +1130,7 @@ pub struct Editor {
addons: TypeIdHashMap<Box<dyn Addon>>,
registered_buffers: HashMap<BufferId, OpenLspBufferHandle>,
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,
diff_hunk_delegate: Option<Arc<dyn DiffHunkDelegate>>,
selection_mark_mode: bool,
toggle_fold_multiple_buffers: Task<()>,
_scroll_cursor_center_top_bottom_task: Task<()>,
@ -2285,7 +2281,6 @@ impl Editor {
use_relative_line_numbers: None,
disable_expand_excerpt_buttons: !full_mode,
delegate_expand_excerpts: false,
delegate_stage_and_restore: false,
delegate_open_excerpts: false,
enable_lsp_data: full_mode,
needs_initial_data_update: full_mode,
@ -2390,7 +2385,6 @@ impl Editor {
show_git_blame_inline_delay_task: None,
git_blame_inline_enabled: full_mode
&& ProjectSettings::get_global(cx).git.inline_blame.enabled,
render_diff_hunk_controls: Arc::new(render_diff_hunk_controls),
buffer_serialization: is_minimap.not().then(|| {
BufferSerialization::new(
ProjectSettings::get_global(cx)
@ -2460,8 +2454,7 @@ impl Editor {
serialize_folds: Task::ready(()),
text_style_refinement: None,
load_diff_task: load_uncommitted_diff,
temporary_diff_override: false,
render_diff_hunks_as_unstaged: false,
diff_hunk_delegate: None,
minimap: None,
change_list: ChangeList::new(),
mode,
@ -11782,17 +11775,10 @@ pub enum EditorEvent {
lines: u32,
direction: ExpandExcerptDirection,
},
StageOrUnstageRequested {
stage: bool,
hunks: Vec<MultiBufferDiffHunk>,
},
OpenExcerptsRequested {
selections_by_buffer: HashMap<BufferId, (Vec<Range<BufferOffset>>, Option<u32>)>,
split: bool,
},
RestoreRequested {
hunks: Vec<MultiBufferDiffHunk>,
},
/// Emitted when an underlying buffer changes, including edits made through another editor.
BufferEdited,
/// Emitted when this editor creates, undoes, or redoes an edit transaction.

View file

@ -4624,7 +4624,7 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> (Vec<AnyElement>, Vec<(DisplayRow, Bounds<Pixels>)>) {
let render_diff_hunk_controls = editor.read(cx).render_diff_hunk_controls.clone();
let diff_hunk_delegate = editor.read(cx).diff_hunk_delegate();
let hovered_diff_hunk_row = editor.read(cx).hovered_diff_hunk_row;
let sticky_top = text_hitbox.bounds.top() + sticky_header_height;
@ -4696,7 +4696,7 @@ impl EditorElement {
sticky_top.min(max_y)
};
let mut element = render_diff_hunk_controls(
let mut element = diff_hunk_delegate.render_hunk_controls(
display_row_range.start.0,
status,
multi_buffer_range.clone(),
@ -6548,8 +6548,11 @@ impl EditorElement {
}
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 = !self
.editor
.read(cx)
.diff_hunk_delegate()
.render_hunk_as_staged(&status, cx);
let unstaged_hollow = matches!(
ProjectSettings::get_global(cx).git.hunk_style,
GitHunkStyleSetting::UnstagedHollow
@ -9296,7 +9299,7 @@ impl Element for EditorElement {
};
let (diff_hunk_controls, diff_hunk_control_bounds) =
if is_read_only && !self.editor.read(cx).delegate_stage_and_restore {
if is_read_only && self.editor.read(cx).diff_hunk_delegate.is_none() {
(vec![], vec![])
} else {
self.layout_diff_hunk_controls(

View file

@ -2,20 +2,242 @@ pub(super) mod blame;
use super::*;
use ::git::{Restore, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
use buffer_diff::DiffHunkStatus;
use buffer_diff::{BufferDiff, DiffHunkStatus, DiffHunkStatusKind};
pub type RenderDiffHunkControlsFn = Arc<
dyn Fn(
u32,
&DiffHunkStatus,
Range<Anchor>,
bool,
Pixels,
&Entity<Editor>,
&mut Window,
&mut App,
) -> AnyElement,
>;
#[derive(Clone)]
pub struct ResolvedDiffHunk {
pub buffer_range: Range<text::Anchor>,
pub diff_base_byte_range: Range<usize>,
pub status: DiffHunkStatus,
}
#[derive(Clone)]
pub struct ResolvedDiffHunks {
pub diff: Entity<BufferDiff>,
pub buffer_id: BufferId,
pub buffer: Option<Entity<Buffer>>,
pub hunks: Vec<ResolvedDiffHunk>,
}
pub trait DiffHunkDelegate {
fn toggle(
&self,
hunks: Vec<ResolvedDiffHunks>,
editor: &mut Editor,
window: &mut Window,
cx: &mut Context<Editor>,
);
fn stage_or_unstage(
&self,
stage: bool,
hunks: Vec<ResolvedDiffHunks>,
editor: &mut Editor,
window: &mut Window,
cx: &mut Context<Editor>,
);
fn restore(
&self,
hunks: Vec<ResolvedDiffHunks>,
editor: &mut Editor,
window: &mut Window,
cx: &mut Context<Editor>,
) {
if hunks.is_empty() || editor.read_only(cx) {
return;
}
self.stage_or_unstage(false, hunks.clone(), editor, window, cx);
editor.transact(window, cx, |editor, window, cx| {
editor.restore_diff_hunks(hunks, cx);
let selections = editor
.selections
.all::<MultiBufferOffset>(&editor.display_snapshot(cx));
editor.change_selections(
SelectionEffects::no_scroll(),
window,
cx,
|selections_state| {
selections_state.select(selections);
},
);
});
}
fn render_hunk_controls(
&self,
row: u32,
status: &DiffHunkStatus,
hunk_range: Range<Anchor>,
is_created_file: bool,
line_height: Pixels,
editor: &Entity<Editor>,
window: &mut Window,
cx: &mut App,
) -> AnyElement;
fn render_hunk_as_staged(&self, status: &DiffHunkStatus, _cx: &App) -> bool {
!status.has_secondary_hunk()
}
}
pub struct UncommittedDiffHunkDelegate;
impl DiffHunkDelegate for UncommittedDiffHunkDelegate {
fn toggle(
&self,
hunks: Vec<ResolvedDiffHunks>,
editor: &mut Editor,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let stage = hunks
.iter()
.flat_map(|hunks| hunks.hunks.iter())
.any(|hunk| hunk.status.has_secondary_hunk());
self.stage_or_unstage(stage, hunks, editor, window, cx);
}
fn stage_or_unstage(
&self,
stage: bool,
hunks: Vec<ResolvedDiffHunks>,
editor: &mut Editor,
_window: &mut Window,
cx: &mut Context<Editor>,
) {
let Some(project) = editor.project() else {
return;
};
for hunks in hunks {
let Some(buffer) = hunks.buffer else {
continue;
};
let ranges = hunks
.hunks
.into_iter()
.map(|hunk| hunk.buffer_range)
.collect::<Vec<_>>();
if ranges.is_empty() {
continue;
}
let secondary_diff = hunks.diff.read(cx).secondary_diff();
project
.update(cx, |project, cx| {
if stage {
let Some(secondary_diff) = secondary_diff else {
return Err(anyhow::anyhow!("diff has no unstaged secondary"));
};
project.stage_hunks(buffer, secondary_diff, ranges, cx)
} else {
project.unstage_uncommitted_hunks(buffer, hunks.diff, ranges, cx)
}
})
.log_err();
}
}
fn render_hunk_controls(
&self,
row: u32,
status: &DiffHunkStatus,
hunk_range: Range<Anchor>,
is_created_file: bool,
line_height: Pixels,
editor: &Entity<Editor>,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
render_diff_hunk_controls(
row,
status,
hunk_range,
is_created_file,
line_height,
editor,
window,
cx,
)
}
}
pub struct RestoreOnlyDiffHunkDelegate;
impl DiffHunkDelegate for RestoreOnlyDiffHunkDelegate {
fn toggle(
&self,
_hunks: Vec<ResolvedDiffHunks>,
_editor: &mut Editor,
_window: &mut Window,
_cx: &mut Context<Editor>,
) {
}
fn stage_or_unstage(
&self,
_stage: bool,
_hunks: Vec<ResolvedDiffHunks>,
_editor: &mut Editor,
_window: &mut Window,
_cx: &mut Context<Editor>,
) {
}
fn render_hunk_controls(
&self,
_row: u32,
_status: &DiffHunkStatus,
_hunk_range: Range<Anchor>,
_is_created_file: bool,
_line_height: Pixels,
_editor: &Entity<Editor>,
_window: &mut Window,
_cx: &mut App,
) -> AnyElement {
gpui::Empty.into_any_element()
}
}
pub struct RestoreOnlyUnstagedDiffHunkDelegate;
impl DiffHunkDelegate for RestoreOnlyUnstagedDiffHunkDelegate {
fn toggle(
&self,
_hunks: Vec<ResolvedDiffHunks>,
_editor: &mut Editor,
_window: &mut Window,
_cx: &mut Context<Editor>,
) {
}
fn stage_or_unstage(
&self,
_stage: bool,
_hunks: Vec<ResolvedDiffHunks>,
_editor: &mut Editor,
_window: &mut Window,
_cx: &mut Context<Editor>,
) {
}
fn render_hunk_controls(
&self,
_row: u32,
_status: &DiffHunkStatus,
_hunk_range: Range<Anchor>,
_is_created_file: bool,
_line_height: Pixels,
_editor: &Entity<Editor>,
_window: &mut Window,
_cx: &mut App,
) -> AnyElement {
gpui::Empty.into_any_element()
}
fn render_hunk_as_staged(&self, _status: &DiffHunkStatus, _cx: &App) -> bool {
false
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum DisplayDiffHunk {
@ -166,24 +388,115 @@ impl Editor {
})
}
pub fn set_render_diff_hunk_controls(
&mut self,
render_diff_hunk_controls: RenderDiffHunkControlsFn,
cx: &mut Context<Self>,
) {
self.render_diff_hunk_controls = render_diff_hunk_controls;
cx.notify();
fn resolve_diff_hunks(
&self,
hunks: Vec<MultiBufferDiffHunk>,
cx: &App,
) -> Vec<ResolvedDiffHunks> {
let multibuffer = self.buffer().read(cx);
let chunk_by = hunks.into_iter().chunk_by(|hunk| hunk.buffer_id);
let mut resolved = Vec::new();
for (source_buffer_id, hunks) in &chunk_by {
let Some(diff) = multibuffer.diff_for(source_buffer_id) else {
continue;
};
let diff_snapshot = diff.read(cx).snapshot(cx);
let main_buffer_id = diff_snapshot.buffer_id();
let buffer = multibuffer.buffer(main_buffer_id).or_else(|| {
self.project
.as_ref()
.and_then(|project| project.read(cx).buffer_for_id(main_buffer_id, cx))
});
let mut resolved_hunks = Vec::new();
for hunk in hunks {
if hunk.buffer_id == main_buffer_id {
resolved_hunks.push(ResolvedDiffHunk {
buffer_range: hunk.buffer_range,
diff_base_byte_range: hunk.diff_base_byte_range.start.0
..hunk.diff_base_byte_range.end.0,
status: hunk.status,
});
} else {
let diff_base_byte_range =
hunk.diff_base_byte_range.start.0..hunk.diff_base_byte_range.end.0;
let Some(hunk) = diff_snapshot
.hunks_intersecting_base_text_range(
diff_base_byte_range.clone(),
diff_snapshot.buffer_snapshot(),
)
.find(|hunk| hunk.diff_base_byte_range == diff_base_byte_range)
else {
continue;
};
let kind = if hunk.buffer_range.start == hunk.buffer_range.end {
DiffHunkStatusKind::Deleted
} else if hunk.diff_base_byte_range.is_empty() {
DiffHunkStatusKind::Added
} else {
DiffHunkStatusKind::Modified
};
resolved_hunks.push(ResolvedDiffHunk {
buffer_range: hunk.buffer_range,
diff_base_byte_range: hunk.diff_base_byte_range,
status: DiffHunkStatus {
kind,
secondary: hunk.secondary_status,
},
});
}
}
if !resolved_hunks.is_empty() {
resolved.push(ResolvedDiffHunks {
diff,
buffer_id: main_buffer_id,
buffer,
hunks: resolved_hunks,
});
}
}
resolved
}
/// 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(
pub fn diff_hunk_delegate(&self) -> Arc<dyn DiffHunkDelegate> {
self.diff_hunk_delegate
.clone()
.unwrap_or_else(|| Arc::new(UncommittedDiffHunkDelegate))
}
pub fn set_diff_hunk_delegate(
&mut self,
render_as_unstaged: bool,
delegate: Option<Arc<dyn DiffHunkDelegate>>,
cx: &mut Context<Self>,
) {
self.render_diff_hunks_as_unstaged = render_as_unstaged;
let had_delegate = self.diff_hunk_delegate.is_some();
let has_delegate = delegate.is_some();
self.diff_hunk_delegate = delegate;
if !had_delegate && has_delegate {
self.load_diff_task.take();
} else if had_delegate && !has_delegate {
self.buffer.update(cx, |buffer, cx| {
buffer.set_all_diff_hunks_collapsed(cx);
});
if let Some(project) = self.project.clone() {
self.load_diff_task = Some(
update_uncommitted_diff_for_buffer(
cx.entity(),
&project,
self.buffer.read(cx).all_buffers(),
self.buffer.clone(),
cx,
)
.shared(),
);
}
}
cx.notify();
}
@ -265,33 +578,6 @@ impl Editor {
cx.notify();
}
pub fn start_temporary_diff_override(&mut self) {
self.load_diff_task.take();
self.temporary_diff_override = true;
}
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);
});
if let Some(project) = self.project.clone() {
self.load_diff_task = Some(
update_uncommitted_diff_for_buffer(
cx.entity(),
&project,
self.buffer.read(cx).all_buffers(),
self.buffer.clone(),
cx,
)
.shared(),
);
}
}
/// Hides the inline blame popover element, in case it's already visible, or
/// interrupts the task meant to show it, in case the task is running.
///
@ -764,31 +1050,48 @@ impl Editor {
);
}
pub(super) fn restore_diff_hunks(&self, hunks: Vec<MultiBufferDiffHunk>, cx: &mut App) {
let mut revert_changes = HashMap::default();
let chunk_by = hunks.into_iter().chunk_by(|hunk| hunk.buffer_id);
for (buffer_id, hunks) in &chunk_by {
let hunks = hunks.collect::<Vec<_>>();
for hunk in &hunks {
self.prepare_restore_change(&mut revert_changes, hunk, cx);
}
self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), cx);
}
if !revert_changes.is_empty() {
self.buffer().update(cx, |multi_buffer, cx| {
for (buffer_id, changes) in revert_changes {
if let Some(buffer) = multi_buffer.buffer(buffer_id) {
buffer.update(cx, |buffer, cx| {
buffer.edit(
changes
.into_iter()
.map(|(range, text)| (range, text.to_string())),
None,
cx,
);
});
pub(super) fn restore_diff_hunks(
&mut self,
hunks: Vec<ResolvedDiffHunks>,
cx: &mut Context<Self>,
) {
let mut revert_changes = Vec::new();
for hunks in hunks {
let Some(buffer) = hunks.buffer else {
continue;
};
let diff_snapshot = hunks.diff.read(cx).snapshot(cx);
let changes = hunks
.hunks
.into_iter()
.filter_map(|hunk| {
if hunk.diff_base_byte_range == (0..0)
&& hunk.buffer_range.start.is_min()
&& hunk.buffer_range.end.is_max()
{
return None;
}
}
let original_text = diff_snapshot
.base_text()
.as_rope()
.slice(hunk.diff_base_byte_range.start..hunk.diff_base_byte_range.end);
Some((hunk.buffer_range, original_text))
})
.collect::<Vec<_>>();
if !changes.is_empty() {
revert_changes.push((buffer, changes));
}
}
for (buffer, changes) in revert_changes {
buffer.update(cx, |buffer, cx| {
buffer.edit(
changes
.into_iter()
.map(|(range, text)| (range, text.to_string())),
None,
cx,
);
});
}
}
@ -1421,18 +1724,25 @@ impl Editor {
pub(super) fn toggle_staged_selected_diff_hunks(
&mut self,
_: &::git::ToggleStaged,
_: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let ranges: Vec<_> = self
.selections
.disjoint_anchors()
.iter()
.map(|s| s.range())
.collect();
let stage = self.has_stageable_diff_hunks_in_ranges(&ranges, &snapshot);
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
let task = self.save_buffers_for_ranges_if_needed(&ranges, cx);
cx.spawn_in(window, async move |this, cx| {
task.await?;
this.update_in(cx, |this, window, cx| {
let snapshot = this.buffer.read(cx).snapshot(cx);
let hunks = this.diff_hunks_in_ranges(&ranges, &snapshot).collect();
this.apply_toggle(hunks, window, cx);
})
})
.detach_and_log_err(cx);
}
pub(super) fn stage_and_next(
@ -1453,42 +1763,47 @@ impl Editor {
self.do_stage_or_unstage_and_next(false, window, cx);
}
pub(super) fn do_stage_or_unstage(
&self,
pub fn apply_toggle(
&mut self,
hunks: Vec<MultiBufferDiffHunk>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let hunks = self.resolve_diff_hunks(hunks, cx);
if hunks.is_empty() {
return;
}
let delegate = self.diff_hunk_delegate();
delegate.toggle(hunks, self, window, cx);
}
pub fn apply_stage_or_unstage(
&mut self,
stage: bool,
buffer_id: BufferId,
hunks: impl Iterator<Item = MultiBufferDiffHunk>,
cx: &mut App,
) -> Option<()> {
let project = self.project()?;
let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?;
let diff = self.buffer.read(cx).diff_for(buffer_id)?;
let buffer_snapshot = buffer.read(cx).snapshot();
let file_exists = buffer_snapshot
.file()
.is_some_and(|file| file.disk_state().exists());
diff.update(cx, |diff, cx| {
diff.stage_or_unstage_hunks(
stage,
&hunks
.map(|hunk| buffer_diff::DiffHunk {
buffer_range: hunk.buffer_range,
// We don't need to pass in word diffs here because they're only used for rendering and
// this function changes internal state
base_word_diffs: Vec::default(),
buffer_word_diffs: Vec::default(),
diff_base_byte_range: hunk.diff_base_byte_range.start.0
..hunk.diff_base_byte_range.end.0,
secondary_status: hunk.status.secondary,
range: Point::zero()..Point::zero(), // unused
})
.collect::<Vec<_>>(),
&buffer_snapshot,
file_exists,
cx,
)
});
None
hunks: Vec<MultiBufferDiffHunk>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let hunks = self.resolve_diff_hunks(hunks, cx);
if hunks.is_empty() {
return;
}
let delegate = self.diff_hunk_delegate();
delegate.stage_or_unstage(stage, hunks, self, window, cx);
}
pub fn apply_restore(
&mut self,
hunks: Vec<MultiBufferDiffHunk>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let hunks = self.resolve_diff_hunks(hunks, cx);
if hunks.is_empty() {
return;
}
let delegate = self.diff_hunk_delegate();
delegate.restore(hunks, self, window, cx);
}
pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut Context<Self>) -> bool {
@ -1776,31 +2091,20 @@ impl Editor {
}
}
fn stage_or_unstage_diff_hunks(
pub fn stage_or_unstage_diff_hunks(
&mut self,
stage: bool,
ranges: Vec<Range<Anchor>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.delegate_stage_and_restore {
let snapshot = self.buffer.read(cx).snapshot(cx);
let hunks: Vec<_> = self.diff_hunks_in_ranges(&ranges, &snapshot).collect();
if !hunks.is_empty() {
cx.emit(EditorEvent::StageOrUnstageRequested { stage, hunks });
}
return;
}
let task = self.save_buffers_for_ranges_if_needed(&ranges, cx);
cx.spawn(async move |this, cx| {
cx.spawn_in(window, async move |this, cx| {
task.await?;
this.update(cx, |this, cx| {
this.update_in(cx, |this, window, cx| {
let snapshot = this.buffer.read(cx).snapshot(cx);
let chunk_by = this
.diff_hunks_in_ranges(&ranges, &snapshot)
.chunk_by(|hunk| hunk.buffer_id);
for (buffer_id, hunks) in &chunk_by {
this.do_stage_or_unstage(stage, buffer_id, hunks, cx);
}
let hunks = this.diff_hunks_in_ranges(&ranges, &snapshot).collect();
this.apply_stage_or_unstage(stage, hunks, window, cx);
})
})
.detach_and_log_err(cx);
@ -1847,66 +2151,8 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Editor>,
) {
if self.delegate_stage_and_restore {
let hunks = self.snapshot(window, cx).hunks_for_ranges(ranges);
if !hunks.is_empty() {
cx.emit(EditorEvent::RestoreRequested { hunks });
}
return;
}
let hunks = self.snapshot(window, cx).hunks_for_ranges(ranges);
self.transact(window, cx, |editor, window, cx| {
editor.restore_diff_hunks(hunks, cx);
let selections = editor
.selections
.all::<MultiBufferOffset>(&editor.display_snapshot(cx));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select(selections);
});
});
}
fn has_stageable_diff_hunks_in_ranges(
&self,
ranges: &[Range<Anchor>],
snapshot: &MultiBufferSnapshot,
) -> bool {
let mut hunks = self.diff_hunks_in_ranges(ranges, snapshot);
hunks.any(|hunk| hunk.status().has_secondary_hunk())
}
fn prepare_restore_change(
&self,
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
hunk: &MultiBufferDiffHunk,
cx: &mut App,
) -> Option<()> {
if hunk.is_created_file() {
return None;
}
let multi_buffer = self.buffer.read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let diff_snapshot = multi_buffer_snapshot.diff_for_buffer_id(hunk.buffer_id)?;
let original_text = diff_snapshot
.base_text()
.as_rope()
.slice(hunk.diff_base_byte_range.start.0..hunk.diff_base_byte_range.end.0);
let buffer = multi_buffer.buffer(hunk.buffer_id)?;
let buffer = buffer.read(cx);
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| {
probe
.0
.start
.cmp(&hunk.buffer_range.start, &buffer_snapshot)
.then(probe.0.end.cmp(&hunk.buffer_range.end, &buffer_snapshot))
}) {
buffer_revert_changes.insert(i, (hunk.buffer_range.clone(), original_text));
Some(())
} else {
None
}
self.apply_restore(hunks, window, cx);
}
fn save_buffers_for_ranges_if_needed(
@ -1949,11 +2195,11 @@ impl Editor {
let ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
if ranges.iter().any(|range| range.start != range.end) {
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
self.stage_or_unstage_diff_hunks(stage, ranges, window, cx);
return;
}
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
self.stage_or_unstage_diff_hunks(stage, ranges, window, cx);
let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded();
let wrap_around = !all_diff_hunks_expanded;
@ -2678,7 +2924,7 @@ pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App)
cx.set_global(GlobalBlameRenderer(Arc::new(renderer)));
}
pub(super) fn render_diff_hunk_controls(
pub fn render_diff_hunk_controls(
row: u32,
status: &DiffHunkStatus,
hunk_range: Range<Anchor>,
@ -2723,11 +2969,12 @@ pub(super) fn render_diff_hunk_controls(
})
.on_click({
let editor = editor.clone();
move |_event, _window, cx| {
move |_event, window, cx| {
editor.update(cx, |editor, cx| {
editor.stage_or_unstage_diff_hunks(
true,
vec![hunk_range.start..hunk_range.start],
window,
cx,
);
});
@ -2749,11 +2996,12 @@ pub(super) fn render_diff_hunk_controls(
})
.on_click({
let editor = editor.clone();
move |_event, _window, cx| {
move |_event, window, cx| {
editor.update(cx, |editor, cx| {
editor.stage_or_unstage_diff_hunks(
false,
vec![hunk_range.start..hunk_range.start],
window,
cx,
);
});
@ -2880,7 +3128,7 @@ pub(super) fn update_uncommitted_diff_for_buffer(
});
cx.spawn(async move |cx| {
let diffs = future::join_all(tasks).await;
if editor.read_with(cx, |editor, _cx| editor.temporary_diff_override) {
if editor.read_with(cx, |editor, _cx| editor.diff_hunk_delegate.is_some()) {
return;
}

View file

@ -3,19 +3,19 @@ use std::{
sync::Arc,
};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use buffer_diff::{BufferDiff, BufferDiffSnapshot, DiffHunkStatus};
use collections::HashMap;
use fs::Fs;
use gpui::{
Action, Entity, EventEmitter, Focusable, Font, Pixels, Subscription, WeakEntity, canvas,
prelude::*,
Action, AnyElement, Entity, EventEmitter, Focusable, Font, Pixels, Subscription, WeakEntity,
canvas, prelude::*,
};
use itertools::Itertools;
use language::{Buffer, Capability, HighlightedText};
use multi_buffer::{
Anchor, AnchorRangeExt as _, BufferOffset, ExcerptRange, ExpandExcerptDirection, MultiBuffer,
MultiBufferDiffHunk, MultiBufferPoint, MultiBufferSnapshot, PathKey,
MultiBufferPoint, MultiBufferSnapshot, PathKey,
};
use project::Project;
use rope::Point;
@ -23,6 +23,7 @@ use settings::{DiffViewStyle, SeedQuerySetting, Settings, SettingsStore, update_
use text::{Bias, BufferId, OffsetRangeExt as _, Patch, ToPoint as _};
use ui::{Toggleable as _, Tooltip, prelude::*, render_modifiers};
use util::ResultExt as _;
use crate::{
display_map::CompanionExcerptPatch,
@ -36,7 +37,8 @@ use workspace::{
};
use crate::{
Autoscroll, Editor, EditorEvent, EditorSettings, RenderDiffHunkControlsFn, ToggleSoftWrap,
Autoscroll, DiffHunkDelegate, Editor, EditorEvent, EditorSettings, ResolvedDiffHunks,
ToggleSoftWrap, UncommittedDiffHunkDelegate,
actions::{DisableBreakpoint, EditLogBreakpoint, EnableBreakpoint, ToggleBreakpoint},
display_map::Companion,
};
@ -149,32 +151,97 @@ fn translate_lhs_selections_to_rhs(
translated
}
fn translate_lhs_hunks_to_rhs(
lhs_hunks: &[MultiBufferDiffHunk],
splittable: &SplittableEditor,
cx: &App,
) -> Vec<MultiBufferDiffHunk> {
let Some(lhs) = &splittable.lhs else {
return vec![];
};
let lhs_snapshot = lhs.multibuffer.read(cx).snapshot(cx);
let rhs_snapshot = splittable.rhs_multibuffer.read(cx).snapshot(cx);
let rhs_hunks: Vec<MultiBufferDiffHunk> = rhs_snapshot.diff_hunks().collect();
struct SplitLhsDiffHunkDelegate {
splittable: WeakEntity<SplittableEditor>,
}
let mut translated = Vec::new();
for lhs_hunk in lhs_hunks {
let Some(diff) = lhs_snapshot.diff_for_buffer_id(lhs_hunk.buffer_id) else {
continue;
};
let rhs_buffer_id = diff.buffer_id();
if let Some(rhs_hunk) = rhs_hunks.iter().find(|rhs_hunk| {
rhs_hunk.buffer_id == rhs_buffer_id
&& rhs_hunk.diff_base_byte_range == lhs_hunk.diff_base_byte_range
}) {
translated.push(rhs_hunk.clone());
}
impl DiffHunkDelegate for SplitLhsDiffHunkDelegate {
fn toggle(
&self,
hunks: Vec<ResolvedDiffHunks>,
_editor: &mut Editor,
window: &mut Window,
cx: &mut Context<Editor>,
) {
self.splittable
.update(cx, |splittable, cx| {
splittable.rhs_editor.update(cx, |editor, cx| {
let delegate = editor.diff_hunk_delegate();
delegate.toggle(hunks, editor, window, cx);
});
})
.log_err();
}
fn stage_or_unstage(
&self,
stage: bool,
hunks: Vec<ResolvedDiffHunks>,
_editor: &mut Editor,
window: &mut Window,
cx: &mut Context<Editor>,
) {
self.splittable
.update(cx, |splittable, cx| {
splittable.rhs_editor.update(cx, |editor, cx| {
let delegate = editor.diff_hunk_delegate();
delegate.stage_or_unstage(stage, hunks, editor, window, cx);
});
})
.log_err();
}
fn restore(
&self,
hunks: Vec<ResolvedDiffHunks>,
_editor: &mut Editor,
window: &mut Window,
cx: &mut Context<Editor>,
) {
self.splittable
.update(cx, |splittable, cx| {
splittable.rhs_editor.update(cx, |editor, cx| {
let delegate = editor.diff_hunk_delegate();
delegate.restore(hunks, editor, window, cx);
});
})
.log_err();
}
fn render_hunk_controls(
&self,
row: u32,
status: &DiffHunkStatus,
hunk_range: Range<Anchor>,
is_created_file: bool,
line_height: Pixels,
editor: &Entity<Editor>,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let Some(splittable) = self.splittable.upgrade() else {
return gpui::Empty.into_any_element();
};
let delegate = splittable.read(cx).rhs_editor.read(cx).diff_hunk_delegate();
delegate.render_hunk_controls(
row,
status,
hunk_range,
is_created_file,
line_height,
editor,
window,
cx,
)
}
fn render_hunk_as_staged(&self, status: &DiffHunkStatus, cx: &App) -> bool {
let Some(splittable) = self.splittable.upgrade() else {
return false;
};
let delegate = splittable.read(cx).rhs_editor.read(cx).diff_hunk_delegate();
delegate.render_hunk_as_staged(status, cx)
}
translated
}
fn patches_for_range<F>(
@ -569,28 +636,13 @@ impl SplittableEditor {
self.lhs.is_some()
}
pub fn set_render_diff_hunk_controls(
pub fn set_diff_hunk_delegate(
&self,
render_diff_hunk_controls: RenderDiffHunkControlsFn,
delegate: Option<Arc<dyn DiffHunkDelegate>>,
cx: &mut Context<Self>,
) {
self.update_editors(cx, |editor, cx| {
editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx);
});
}
pub fn disable_diff_hunk_controls(&self, cx: &mut Context<Self>) {
let empty_controls = Arc::new(|_, _: &_, _, _, _, _: &_, _: &mut _, _: &mut _| {
gpui::Empty.into_any_element()
});
self.update_editors(cx, |editor, cx| {
editor.set_render_diff_hunk_controls(empty_controls.clone(), cx);
});
}
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);
self.rhs_editor.update(cx, |editor, cx| {
editor.set_diff_hunk_delegate(delegate, cx);
});
}
@ -631,7 +683,7 @@ impl SplittableEditor {
editor.disable_inline_diagnostics();
editor.disable_mouse_wheel_zoom();
editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx);
editor.start_temporary_diff_override();
editor.set_diff_hunk_delegate(Some(Arc::new(UncommittedDiffHunkDelegate)), cx);
editor
});
// TODO(split-diff) we might want to tag editor events with whether they came from rhs/lhs
@ -729,15 +781,16 @@ impl SplittableEditor {
multibuffer
});
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 splittable = cx.weak_entity();
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);
editor.set_diff_hunk_delegate(
Some(Arc::new(SplitLhsDiffHunkDelegate { splittable })),
cx,
);
editor.set_delegate_open_excerpts(true);
editor.set_show_vertical_scrollbar(false, cx);
editor.disable_lsp_data();
@ -748,10 +801,6 @@ impl SplittableEditor {
editor
});
lhs_editor.update(cx, |editor, cx| {
editor.set_render_diff_hunk_controls(render_diff_hunk_controls, cx);
});
let mut subscriptions = vec![cx.subscribe_in(
&lhs_editor,
window,
@ -782,30 +831,7 @@ impl SplittableEditor {
this.expand_excerpts(rhs_anchors.into_iter(), *lines, *direction, cx);
}
}
EditorEvent::StageOrUnstageRequested { stage, hunks } => {
if this.lhs.is_some() {
let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
if !translated.is_empty() {
let stage = *stage;
this.rhs_editor.update(cx, |editor, cx| {
let chunk_by = translated.into_iter().chunk_by(|h| h.buffer_id);
for (buffer_id, hunks) in &chunk_by {
editor.do_stage_or_unstage(stage, buffer_id, hunks, cx);
}
});
}
}
}
EditorEvent::RestoreRequested { hunks } => {
if this.lhs.is_some() {
let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
if !translated.is_empty() {
this.rhs_editor.update(cx, |editor, cx| {
editor.restore_diff_hunks(translated, cx);
});
}
}
}
EditorEvent::OpenExcerptsRequested {
selections_by_buffer,
split,

File diff suppressed because it is too large Load diff

View file

@ -2,8 +2,8 @@ use anyhow::{Context as _, Result};
use buffer_diff::BufferDiff;
use collections::HashMap;
use editor::{
Addon, Editor, EditorEvent, EditorSettings, MultiBuffer, SplittableEditor,
hover_markdown_style, multibuffer_context_lines,
Addon, Editor, EditorEvent, EditorSettings, MultiBuffer, RestoreOnlyDiffHunkDelegate,
SplittableEditor, hover_markdown_style, multibuffer_context_lines,
};
use futures_lite::future::yield_now;
use git::repository::{CommitDetails, CommitDiff, RepoPath, is_binary_content};
@ -275,7 +275,7 @@ impl CommitView {
window,
cx,
);
editor.disable_diff_hunk_controls(cx);
editor.set_diff_hunk_delegate(Some(Arc::new(RestoreOnlyDiffHunkDelegate)), cx);
editor.rhs_editor().update(cx, |editor, cx| {
editor.set_show_bookmarks(false, cx);
@ -1193,7 +1193,7 @@ impl Item for CommitView {
window,
cx,
);
editor.disable_diff_hunk_controls(cx);
editor.set_diff_hunk_delegate(Some(Arc::new(RestoreOnlyDiffHunkDelegate)), cx);
editor.rhs_editor().update(cx, |editor, cx| {
editor.set_show_bookmarks(false, cx);
editor.set_show_breakpoints(false, cx);

View file

@ -17,7 +17,7 @@ use project::{
use settings::Settings;
use std::{ops::Range, sync::Arc};
use ui::{ButtonLike, Divider, Tooltip, prelude::*};
use util::{debug_panic, maybe};
use util::debug_panic;
use workspace::{HideStatusItem, StatusItemView, Workspace, item::ItemHandle};
use zed_actions::agent::{
ConflictContent, ResolveConflictedFilesWithAgent, ResolveConflictsWithAgent,
@ -115,19 +115,17 @@ pub(crate) fn buffer_ranges_updated(
return;
}
let buffer_conflicts = editor
.addon_mut::<ConflictAddon>()
.unwrap()
.buffers
.entry(buffer_id)
.or_insert_with(|| {
let subscription = cx.subscribe(&conflict_set, conflicts_updated);
BufferConflicts {
block_ids: Vec::new(),
conflict_set: conflict_set.clone(),
_subscription: subscription,
}
});
let Some(conflict_addon) = editor.addon_mut::<ConflictAddon>() else {
return;
};
let buffer_conflicts = conflict_addon.buffers.entry(buffer_id).or_insert_with(|| {
let subscription = cx.subscribe(&conflict_set, conflicts_updated);
BufferConflicts {
block_ids: Vec::new(),
conflict_set: conflict_set.clone(),
_subscription: subscription,
}
});
let conflict_set = buffer_conflicts.conflict_set.clone();
let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
@ -150,18 +148,17 @@ pub(crate) fn buffers_removed(
cx: &mut Context<Editor>,
) {
let mut removed_block_ids = HashSet::default();
editor
.addon_mut::<ConflictAddon>()
.unwrap()
.buffers
.retain(|buffer_id, buffer| {
if removed_buffer_ids.contains(buffer_id) {
removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id));
false
} else {
true
}
});
let Some(conflict_addon) = editor.addon_mut::<ConflictAddon>() else {
return;
};
conflict_addon.buffers.retain(|buffer_id, buffer| {
if removed_buffer_ids.contains(buffer_id) {
removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id));
false
} else {
true
}
});
editor.remove_blocks(removed_block_ids, None, cx);
}
@ -176,9 +173,13 @@ fn conflicts_updated(
let conflict_set = conflict_set.read(cx).snapshot();
let multibuffer = editor.buffer().read(cx);
let snapshot = multibuffer.snapshot(cx);
let old_range = maybe!({
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
let buffer_conflicts = conflict_addon.buffers.get(&buffer_id)?;
let old_range = {
let Some(conflict_addon) = editor.addon_mut::<ConflictAddon>() else {
return;
};
let Some(buffer_conflicts) = conflict_addon.buffers.get(&buffer_id) else {
return;
};
match buffer_conflicts.block_ids.get(event.old_range.clone()) {
Some(_) => Some(event.old_range.clone()),
None => {
@ -197,10 +198,12 @@ fn conflicts_updated(
}
}
}
});
};
// Remove obsolete highlights and blocks
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
let Some(conflict_addon) = editor.addon_mut::<ConflictAddon>() else {
return;
};
if let Some((buffer_conflicts, old_range)) = conflict_addon
.buffers
.get_mut(&buffer_id)
@ -256,7 +259,9 @@ fn conflicts_updated(
}
let new_block_ids = editor.insert_blocks(blocks, None, cx);
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
let Some(conflict_addon) = editor.addon_mut::<ConflictAddon>() else {
return;
};
if let Some((buffer_conflicts, old_range)) =
conflict_addon.buffers.get_mut(&buffer_id).zip(old_range)
{
@ -481,7 +486,7 @@ pub(crate) fn resolve_conflict(
let buffer_id = resolved_conflict.ours.end.buffer_id;
let buffer = multibuffer.read(cx).buffer(buffer_id)?;
resolved_conflict.resolve(buffer.clone(), &ranges, cx);
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
let conflict_addon = editor.addon_mut::<ConflictAddon>()?;
let snapshot = multibuffer.read(cx).snapshot(cx);
let buffer_snapshot = buffer.read(cx).snapshot();
let state = conflict_addon

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,10 @@
use anyhow::Result;
use buffer_diff::BufferDiff;
use editor::{Editor, EditorEvent, EditorSettings, MultiBuffer, SplittableEditor};
use editor::{
Editor, EditorEvent, EditorSettings, MultiBuffer, RestoreOnlyUnstagedDiffHunkDelegate,
SplittableEditor,
};
use futures::{FutureExt, select_biased};
use gpui::{
AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
@ -103,11 +106,8 @@ impl FileDiffView {
window,
cx,
);
splittable.rhs_editor().update(cx, |editor, _| {
editor.start_temporary_diff_override();
});
splittable.disable_diff_hunk_controls(cx);
splittable.set_render_diff_hunks_as_unstaged(cx);
splittable
.set_diff_hunk_delegate(Some(Arc::new(RestoreOnlyUnstagedDiffHunkDelegate)), cx);
splittable
});

View file

@ -3,9 +3,11 @@ use crate::commit_modal::CommitModal;
use crate::commit_tooltip::{CommitAvatar, CommitTooltip};
use crate::commit_view::CommitView;
use crate::git_panel_settings::GitPanelScrollbarAccessor;
use crate::project_diff::{BranchDiff, Diff, ProjectDiff};
use crate::project_diff::{DeployBranchDiff, Diff, ProjectDiff};
use crate::remote_output::{self, RemoteAction, SuccessMessage};
use crate::solo_diff_view::SoloDiffView;
use crate::staged_diff::StagedDiff;
use crate::unstaged_diff::UnstagedDiff;
use crate::{branch_picker, picker_prompt, render_remote_button};
use crate::{
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
@ -37,10 +39,11 @@ use git::{
ViewFile, parse_git_remote_url,
};
use gpui::{
AbsoluteLength, Action, Anchor, AsyncApp, AsyncWindowContext, Bounds, ClickEvent, DismissEvent,
Empty, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, MouseButton, MouseDownEvent,
Point, PromptLevel, ScrollStrategy, Subscription, Task, TaskExt, TextStyle,
UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point, size, uniform_list,
AbsoluteLength, Action, Anchor, AnyElement, AsyncApp, AsyncWindowContext, Bounds, ClickEvent,
DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, MouseButton,
MouseDownEvent, Pixels, Point, PromptLevel, ScrollStrategy, Subscription, Task, TaskExt,
TextStyle, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point, size,
uniform_list,
};
use itertools::Itertools;
use language::{Buffer, BufferEvent, File};
@ -137,6 +140,10 @@ actions!(
ExpandSelectedEntry,
/// Collapses the selected entry to hide its children.
CollapseSelectedEntry,
/// View unstaged changes
ViewUnstagedChanges,
/// View staged changes
ViewStagedChanges,
/// Activates the Changes tab.
ActivateChangesTab,
/// Activates the History tab.
@ -3883,6 +3890,40 @@ impl GitPanel {
}
}
fn view_staged_changes(
&mut self,
_: &ViewStagedChanges,
window: &mut Window,
cx: &mut Context<Self>,
) {
let entry = self
.get_selected_entry()
.and_then(|entry| entry.status_entry())
.cloned();
if let Some(workspace) = self.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
StagedDiff::deploy_at(workspace, entry, window, cx);
});
}
}
fn view_unstaged_changes(
&mut self,
_: &ViewUnstagedChanges,
window: &mut Window,
cx: &mut Context<Self>,
) {
let entry = self
.get_selected_entry()
.and_then(|entry| entry.status_entry())
.cloned();
if let Some(workspace) = self.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
UnstagedDiff::deploy_at(workspace, entry, window, cx);
});
}
}
fn toggle_tree_view(&mut self, _: &ToggleTreeView, _: &mut Window, cx: &mut Context<Self>) {
let current_setting = GitPanelSettings::get_global(cx).tree_view;
if let Some(workspace) = self.workspace.upgrade() {
@ -6082,7 +6123,7 @@ impl GitPanel {
.style(ButtonStyle::Outlined)
.on_click(move |_, _, cx| {
cx.defer(move |cx| {
cx.dispatch_action(&BranchDiff);
cx.dispatch_action(&DeployBranchDiff);
})
}),
)
@ -6510,6 +6551,9 @@ impl GitPanel {
.action(stage_title, ToggleStaged.boxed_clone())
.action(restore_title, git::RestoreFile::default().boxed_clone())
.separator()
.action("Unstaged Changes", ViewUnstagedChanges.boxed_clone())
.action("Staged Changes", ViewStagedChanges.boxed_clone())
.separator()
.action_disabled_when(
!is_created,
"Add to .gitignore",
@ -7294,6 +7338,8 @@ impl Render for GitPanel {
.on_action(cx.listener(Self::open_diff))
.on_action(cx.listener(Self::open_solo_diff))
.on_action(cx.listener(Self::view_file))
.on_action(cx.listener(Self::view_unstaged_changes))
.on_action(cx.listener(Self::view_staged_changes))
.on_action(cx.listener(Self::focus_changes_list))
.on_action(cx.listener(Self::focus_editor))
.on_action(cx.listener(Self::expand_commit_editor))

View file

@ -34,12 +34,14 @@ use crate::{
};
mod askpass_modal;
pub mod branch_diff;
pub mod branch_picker;
mod commit_modal;
pub mod commit_tooltip;
pub mod commit_view;
mod conflict_view;
pub mod created_worktrees;
mod diff_multibuffer;
pub mod file_diff_view;
pub mod git_graph;
pub mod git_panel;
@ -52,8 +54,10 @@ pub mod project_diff;
pub(crate) mod remote_output;
pub mod repository_selector;
pub mod solo_diff_view;
pub mod staged_diff;
pub mod stash_picker;
pub mod text_diff_view;
pub mod unstaged_diff;
pub mod worktree_names;
pub mod worktree_picker;
pub mod worktree_service;
@ -87,6 +91,9 @@ pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _, cx| {
ProjectDiff::register(workspace, cx);
staged_diff::StagedDiff::register(workspace, cx);
unstaged_diff::UnstagedDiff::register(workspace, cx);
branch_diff::BranchDiff::register(workspace, cx);
CommitModal::register(workspace);
git_panel::register(workspace);
repository_selector::register(workspace);

View file

@ -1,7 +1,10 @@
use crate::file_diff_view::build_buffer_diff;
use anyhow::Result;
use buffer_diff::BufferDiff;
use editor::{Editor, EditorEvent, MultiBuffer, multibuffer_context_lines};
use editor::{
Editor, EditorEvent, MultiBuffer, RestoreOnlyUnstagedDiffHunkDelegate,
multibuffer_context_lines,
};
use gpui::{
AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
Focusable, Font, IntoElement, Render, SharedString, Task, Window,
@ -196,14 +199,9 @@ impl MultiDiffView {
let editor = cx.new(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx);
editor.start_temporary_diff_override();
editor.set_diff_hunk_delegate(Some(Arc::new(RestoreOnlyUnstagedDiffHunkDelegate)), cx);
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,
);
editor
});

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -3,8 +3,8 @@
use anyhow::Result;
use buffer_diff::BufferDiff;
use editor::{
Editor, EditorEvent, EditorSettings, MultiBuffer, SplittableEditor, ToPoint,
actions::DiffClipboardWithSelectionData,
Editor, EditorEvent, EditorSettings, MultiBuffer, RestoreOnlyUnstagedDiffHunkDelegate,
SplittableEditor, ToPoint, actions::DiffClipboardWithSelectionData,
};
use futures::{FutureExt, select_biased};
use gpui::{
@ -184,11 +184,8 @@ impl TextDiffView {
window,
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();
});
splittable
.set_diff_hunk_delegate(Some(Arc::new(RestoreOnlyUnstagedDiffHunkDelegate)), cx);
splittable
});

View file

@ -0,0 +1,722 @@
use crate::{
diff_multibuffer::DiffMultibuffer,
git_panel::{GitPanel, GitPanelAddon, GitStatusEntry},
};
use anyhow::{Context as _, Result};
use buffer_diff::DiffHunkStatus;
use editor::{
DiffHunkDelegate, Editor, EditorEvent, ResolvedDiffHunks, SplittableEditor,
actions::{GoToHunk, GoToPreviousHunk},
};
use git::{StageAll, StageAndNext};
use gpui::{
Action, AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render,
SharedString, Subscription, Task, WeakEntity,
};
use language::Capability;
use project::{
Project, ProjectPath,
git_store::diff_buffer_list::{DiffBase, DiffBufferList},
project_settings::ProjectSettings,
};
use settings::Settings;
use std::{
any::{Any, TypeId},
ops::Range,
sync::Arc,
};
use ui::{DiffStat, Divider, Icon, Tooltip, Window, prelude::*};
use util::ResultExt as _;
use workspace::{
ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
Workspace,
item::{Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams},
searchable::SearchableItemHandle,
};
pub(crate) struct UnstagedDiffDelegate;
impl DiffHunkDelegate for UnstagedDiffDelegate {
fn toggle(
&self,
hunks: Vec<ResolvedDiffHunks>,
editor: &mut Editor,
window: &mut Window,
cx: &mut Context<Editor>,
) {
self.stage_or_unstage(true, hunks, editor, window, cx);
}
fn stage_or_unstage(
&self,
stage: bool,
hunks: Vec<ResolvedDiffHunks>,
editor: &mut Editor,
_window: &mut Window,
cx: &mut Context<Editor>,
) {
if !stage {
return;
}
let Some(project) = editor.project().cloned() else {
return;
};
for hunks in hunks {
let Some(buffer) = hunks.buffer else {
continue;
};
let worktree_ranges = hunks
.hunks
.into_iter()
.map(|hunk| hunk.buffer_range)
.collect::<Vec<_>>();
if worktree_ranges.is_empty() {
continue;
}
project
.update(cx, |project, cx| {
project.stage_hunks(buffer, hunks.diff, worktree_ranges, cx)
})
.log_err();
}
}
fn render_hunk_controls(
&self,
row: u32,
status: &DiffHunkStatus,
hunk_range: Range<editor::Anchor>,
_is_created_file: bool,
line_height: Pixels,
editor: &Entity<Editor>,
_window: &mut Window,
cx: &mut App,
) -> AnyElement {
if !ProjectSettings::get_global(cx)
.git
.show_stage_restore_buttons
{
return gpui::Empty.into_any_element();
}
let hunk_range = hunk_range.start..hunk_range.start;
h_flex()
.h(line_height)
.mr_1()
.gap_1()
.px_0p5()
.pb_1()
.border_x_1()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.rounded_b_lg()
.bg(cx.theme().colors().editor_background)
.block_mouse_except_scroll()
.shadow_md()
.child(
Button::new(("stage", row as u64), "Stage")
.alpha(if status.is_pending() { 0.66 } else { 1.0 })
.tooltip(Tooltip::text("Stage Hunk"))
.on_click({
let editor = editor.clone();
move |_event, window, cx| {
editor.update(cx, |editor, cx| {
editor.stage_or_unstage_diff_hunks(
true,
vec![hunk_range.clone()],
window,
cx,
);
});
}
}),
)
.into_any_element()
}
fn render_hunk_as_staged(&self, _status: &DiffHunkStatus, _cx: &App) -> bool {
false
}
}
pub struct UnstagedDiff {
diff: Entity<DiffMultibuffer>,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
_diff_event_subscription: Subscription,
}
impl UnstagedDiff {
pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) {
let _ = workspace;
workspace::register_serializable_item::<Self>(cx);
}
pub fn deploy_at(
workspace: &mut Workspace,
entry: Option<GitStatusEntry>,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
telemetry::event!(
"Git Unstaged Diff Opened",
source = if entry.is_some() {
"Git Panel"
} else {
"Action"
}
);
let intended_repo = workspace.project().read(cx).active_repository(cx);
let existing = workspace.items_of_type::<Self>(cx).next();
let unstaged_diff = if let Some(existing) = existing {
workspace.activate_item(&existing, true, true, window, cx);
existing
} else {
let workspace_handle = cx.entity();
let unstaged_diff =
cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx));
workspace.add_item_to_active_pane(
Box::new(unstaged_diff.clone()),
None,
true,
window,
cx,
);
unstaged_diff
};
if let Some(intended) = &intended_repo {
let needs_switch = unstaged_diff
.read(cx)
.diff
.read(cx)
.repo(cx)
.map_or(true, |current| current.entity_id() != intended.entity_id());
if needs_switch {
unstaged_diff.update(cx, |unstaged_diff, cx| {
unstaged_diff.diff.update(cx, |diff, cx| {
diff.set_repo(Some(intended.clone()), cx);
});
});
}
}
if let Some(entry) = entry {
unstaged_diff.update(cx, |unstaged_diff, cx| {
unstaged_diff
.diff
.update(cx, |diff, cx| diff.move_to_entry(entry, window, cx));
});
}
}
pub(crate) fn new(
project: Entity<Project>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let branch_diff =
cx.new(|cx| DiffBufferList::new(DiffBase::Index, project.clone(), window, cx));
let workspace_handle = workspace.downgrade();
let diff = cx.new(|cx| {
DiffMultibuffer::new(
branch_diff,
Capability::ReadWrite,
"No unstaged changes",
move |editor, cx| {
editor.set_diff_hunk_delegate(Some(Arc::new(UnstagedDiffDelegate)), cx);
editor.rhs_editor().update(cx, |rhs_editor, _cx| {
rhs_editor.set_read_only(false);
rhs_editor.register_addon(GitPanelAddon {
workspace: workspace_handle,
});
});
},
project.clone(),
workspace.clone(),
window,
cx,
)
});
Self::from_diff(diff, project, workspace, cx)
}
pub(crate) fn from_diff(
diff: Entity<DiffMultibuffer>,
project: Entity<Project>,
workspace: Entity<Workspace>,
cx: &mut Context<Self>,
) -> Self {
let diff_event_subscription = cx.subscribe(&diff, |_, _, event: &EditorEvent, cx| {
cx.emit(event.clone())
});
Self {
diff,
project,
workspace: workspace.downgrade(),
_diff_event_subscription: diff_event_subscription,
}
}
fn button_states(&self, cx: &App) -> ButtonStates {
let diff = self.diff.read(cx);
let editor = diff.editor().read(cx).rhs_editor().clone();
let editor = editor.read(cx);
let snapshot = diff.multibuffer().read(cx).snapshot(cx);
let prev_next = snapshot.diff_hunks().nth(1).is_some();
let (selection, ranges) = diff.selected_ranges(cx);
let stage = editor
.diff_hunks_in_ranges(&ranges, &snapshot)
.next()
.is_some();
let mut stage_all = false;
self.workspace
.read_with(cx, |workspace, cx| {
if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
stage_all = git_panel.read(cx).can_stage_all();
}
})
.ok();
ButtonStates {
stage,
prev_next,
selection,
stage_all,
}
}
fn stage_selected_unstaged_hunks(
&mut self,
move_to_next: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.diff.update(cx, |diff, cx| {
diff.stage_or_unstage_selected_hunks(true, move_to_next, window, cx)
});
}
}
struct ButtonStates {
stage: bool,
prev_next: bool,
selection: bool,
stage_all: bool,
}
impl EventEmitter<EditorEvent> for UnstagedDiff {}
impl Focusable for UnstagedDiff {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.diff.read(cx).focus_handle(cx)
}
}
impl Item for UnstagedDiff {
type Event = EditorEvent;
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
Some(Icon::new(IconName::GitBranch).color(Color::Muted))
}
fn to_item_events(event: &EditorEvent, f: &mut dyn FnMut(ItemEvent)) {
Editor::to_item_events(event, f)
}
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.diff
.update(cx, |diff, cx| diff.deactivated(window, cx));
}
fn navigate(
&mut self,
data: Arc<dyn Any + Send>,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
self.diff
.update(cx, |diff, cx| diff.navigate(data, window, cx))
}
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
Some("Unstaged Changes".into())
}
fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement {
Label::new(self.tab_content_text(0, _cx))
.color(if params.selected {
Color::Default
} else {
Color::Muted
})
.into_any_element()
}
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Unstaged Changes".into()
}
fn telemetry_event_text(&self) -> Option<&'static str> {
Some("Git Unstaged Diff Opened")
}
fn as_searchable(&self, _: &Entity<Self>, cx: &App) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.diff.read(cx).editor().clone()))
}
fn for_each_project_item(
&self,
cx: &App,
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
) {
self.diff.read(cx).for_each_project_item(cx, f);
}
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.diff
.update(cx, |diff, cx| diff.set_nav_history(nav_history, cx));
}
fn can_split(&self) -> bool {
true
}
fn clone_on_split(
&self,
_workspace_id: Option<workspace::WorkspaceId>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Option<Entity<Self>>>
where
Self: Sized,
{
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(None);
};
let project = self.project.clone();
Task::ready(Some(cx.new(|cx| Self::new(project, workspace, window, cx))))
}
fn is_dirty(&self, cx: &App) -> bool {
self.diff.read(cx).is_dirty(cx)
}
fn has_conflict(&self, cx: &App) -> bool {
self.diff.read(cx).has_conflict(cx)
}
fn can_save(&self, _cx: &App) -> bool {
true
}
fn save(
&mut self,
options: SaveOptions,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.diff
.update(cx, |diff, cx| diff.save(options, project, window, cx))
}
fn save_as(
&mut self,
_: Entity<Project>,
_: ProjectPath,
_: &mut Window,
_: &mut Context<Self>,
) -> Task<Result<()>> {
unreachable!()
}
fn reload(
&mut self,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.diff
.update(cx, |diff, cx| diff.reload(project, window, cx))
}
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a Entity<Self>,
cx: &'a App,
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<DiffMultibuffer>() {
Some(self.diff.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(
self.diff
.read(cx)
.editor()
.read(cx)
.rhs_editor()
.clone()
.into(),
)
} else if type_id == TypeId::of::<SplittableEditor>() {
Some(self.diff.read(cx).editor().clone().into())
} else if type_id == TypeId::of::<DiffBufferList>() {
Some(self.diff.read(cx).branch_diff().clone().into())
} else {
None
}
}
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.diff.update(cx, |diff, cx| {
diff.added_to_workspace(workspace, window, cx)
});
}
}
impl SerializableItem for UnstagedDiff {
fn serialized_item_kind() -> &'static str {
"UnstagedDiff"
}
fn cleanup(
_: workspace::WorkspaceId,
_: Vec<workspace::ItemId>,
_: &mut Window,
_: &mut App,
) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn deserialize(
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
_: workspace::WorkspaceId,
_: workspace::ItemId,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Entity<Self>>> {
window.spawn(cx, async move |cx| {
let workspace = workspace.upgrade().context("workspace gone")?;
cx.update(|window, cx| Ok(cx.new(|cx| Self::new(project, workspace, window, cx))))?
})
}
fn serialize(
&mut self,
_: &mut Workspace,
_: workspace::ItemId,
_: bool,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<Task<Result<()>>> {
Some(Task::ready(Ok(())))
}
fn should_serialize(&self, _: &Self::Event) -> bool {
false
}
}
impl Render for UnstagedDiff {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.diff.clone()
}
}
pub struct UnstagedDiffToolbar {
unstaged_diff: Option<WeakEntity<UnstagedDiff>>,
workspace: WeakEntity<Workspace>,
}
impl UnstagedDiffToolbar {
pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
Self {
unstaged_diff: None,
workspace: workspace.weak_handle(),
}
}
fn unstaged_diff(&self, _: &App) -> Option<Entity<UnstagedDiff>> {
self.unstaged_diff.as_ref()?.upgrade()
}
fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
if let Some(unstaged_diff) = self.unstaged_diff(cx) {
unstaged_diff.focus_handle(cx).focus(window, cx);
}
let action = action.boxed_clone();
cx.defer(move |cx| {
cx.dispatch_action(action.as_ref());
})
}
fn stage_selected_unstaged_hunks(
&mut self,
move_to_next: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(unstaged_diff) = self.unstaged_diff(cx) else {
return;
};
unstaged_diff.update(cx, |unstaged_diff, cx| {
unstaged_diff.stage_selected_unstaged_hunks(move_to_next, window, cx);
});
}
fn stage_all(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.workspace
.update(cx, |workspace, cx| {
let Some(panel) = workspace.panel::<GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.stage_all(&Default::default(), window, cx);
});
})
.ok();
}
}
impl EventEmitter<ToolbarItemEvent> for UnstagedDiffToolbar {}
impl ToolbarItemView for UnstagedDiffToolbar {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_: &mut Window,
cx: &mut Context<Self>,
) -> ToolbarItemLocation {
self.unstaged_diff = active_pane_item
.and_then(|item| item.act_as::<UnstagedDiff>(cx))
.map(|entity| entity.downgrade());
if self.unstaged_diff.is_some() {
ToolbarItemLocation::PrimaryRight
} else {
ToolbarItemLocation::Hidden
}
}
fn pane_focus_update(
&mut self,
_pane_focused: bool,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
}
}
impl Render for UnstagedDiffToolbar {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let Some(unstaged_diff) = self.unstaged_diff(cx) else {
return div();
};
let focus_handle = unstaged_diff.focus_handle(cx);
let button_states = unstaged_diff.read(cx).button_states(cx);
let diff = unstaged_diff.read(cx).diff.read(cx);
let (additions, deletions) = diff.calculate_changed_lines(cx);
let is_multibuffer_empty = diff.multibuffer().read(cx).is_empty();
h_flex()
.my_neg_1()
.py_1()
.gap_1p5()
.flex_wrap()
.justify_between()
.when(!is_multibuffer_empty, |this| {
this.child(DiffStat::new(
"unstaged-diff-stat",
additions as usize,
deletions as usize,
))
.child(Divider::vertical().ml_1())
})
// n.b. the only reason these arrows are here is because we don't
// support "undo" for staging so we need a way to go back.
.child(
h_group_sm()
.child(
IconButton::new("up", IconName::ArrowUp)
.icon_size(IconSize::Small)
.disabled(!button_states.prev_next)
.tooltip(Tooltip::for_action_title_in(
"Go to Previous Hunk",
&GoToPreviousHunk,
&focus_handle,
))
.on_click(cx.listener(|this, _, window, cx| {
this.dispatch_action(&GoToPreviousHunk, window, cx)
})),
)
.child(
IconButton::new("down", IconName::ArrowDown)
.icon_size(IconSize::Small)
.disabled(!button_states.prev_next)
.tooltip(Tooltip::for_action_title_in(
"Go to Next Hunk",
&GoToHunk,
&focus_handle,
))
.on_click(cx.listener(|this, _, window, cx| {
this.dispatch_action(&GoToHunk, window, cx)
})),
),
)
.child(Divider::vertical())
.child(
h_group_sm()
.when(button_states.selection, |this| {
this.child(
Button::new("stage", "Stage")
.disabled(!button_states.stage)
.tooltip(Tooltip::text("Stage Selected Hunks"))
.on_click(cx.listener(|this, _, window, cx| {
this.stage_selected_unstaged_hunks(false, window, cx)
})),
)
})
.when(!button_states.selection, |this| {
this.child(
Button::new("stage", "Stage")
.disabled(!button_states.stage)
.tooltip(Tooltip::for_action_title_in(
"Stage and Go to Next Hunk",
&StageAndNext,
&focus_handle,
))
.on_click(cx.listener(|this, _, window, cx| {
this.stage_selected_unstaged_hunks(true, window, cx)
})),
)
}),
)
.child(Divider::vertical())
.child(
Button::new("stage-all", "Stage All")
.width(rems_from_px(80.))
.disabled(!button_states.stage_all)
.tooltip(Tooltip::for_action_title_in(
"Stage All Changes",
&StageAll,
&focus_handle,
))
.on_click(cx.listener(|this, _, window, cx| this.stage_all(window, cx))),
)
}
}

View file

@ -626,7 +626,7 @@ impl DiffState {
this.buffer_diff_changed(diff, range, cx);
cx.emit(Event::BufferDiffChanged);
}
BufferDiffEvent::BaseTextChanged | BufferDiffEvent::HunksStagedOrUnstaged(_) => {}
BufferDiffEvent::BaseTextChanged => {}
}),
diff,
main_buffer: None,
@ -660,8 +660,7 @@ impl DiffState {
);
cx.emit(Event::BufferDiffChanged);
}
BufferDiffEvent::BaseTextChanged
| BufferDiffEvent::HunksStagedOrUnstaged(_) => {}
BufferDiffEvent::BaseTextChanged => {}
}
}
}),

View file

@ -1,5 +1,5 @@
pub mod branch_diff;
mod conflict_set;
pub mod diff_buffer_list;
pub mod git_traversal;
pub mod job_debug_queue;
pub mod pending_op;
@ -15,7 +15,7 @@ use crate::{
};
use anyhow::{Context as _, Result, anyhow, bail};
use askpass::{AskPassDelegate, EncryptedPassword, IKnowWhatIAmDoingAndIHaveReadTheDocs};
use buffer_diff::{BufferDiff, BufferDiffEvent};
use buffer_diff::{BufferDiff, DiffHunk, DiffHunkSecondaryStatus, PendingHunk, PendingSense};
use client::ProjectId;
use collections::HashMap;
pub use conflict_set::{ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate};
@ -51,7 +51,7 @@ use gpui::{
Subscription, Task, TaskExt, WeakEntity,
};
use language::{
Buffer, BufferEvent, Capability, Language, LanguageRegistry,
Anchor, Buffer, BufferEvent, Capability, Language, LanguageRegistry,
proto::{deserialize_version, serialize_version},
};
use parking_lot::Mutex;
@ -82,7 +82,7 @@ use std::{
};
use sum_tree::{Edit, SumTree, TreeMap};
use task::Shell;
use text::{Bias, BufferId};
use text::{Bias, BufferId, OffsetRangeExt, Rope, ToOffset};
use util::{
ResultExt, debug_panic,
paths::{PathStyle, SanitizedPath},
@ -106,6 +106,7 @@ pub struct GitStore {
loading_diffs:
HashMap<(BufferId, DiffKind), Shared<Task<Result<Entity<BufferDiff>, Arc<anyhow::Error>>>>>,
diffs: HashMap<BufferId, Entity<BufferGitState>>,
buffer_ids_by_index_text_buffer_id: HashMap<BufferId, BufferId>,
shared_diffs: HashMap<proto::PeerId, HashMap<BufferId, SharedDiffs>>,
_subscriptions: Vec<Subscription>,
}
@ -142,6 +143,16 @@ struct BufferGitState {
head_text: Option<Arc<str>>,
index_text: Option<Arc<str>>,
/// The optimistic, in-flight index state: the sole input to the index write,
/// expressed relative to the currently-loaded index text (`I0`). Never shown
/// to any view (views use per-diff pending hunks). Cleared when a
/// recalculation settles.
///
/// `I0` is immutable for the lifetime of any pending edit (the index text
/// buffer is only fast-forwarded when a recalculation settles, which clears
/// this state in the same update), so byte offsets stay valid for the whole
/// window.
pending_index_edits: Option<Vec<(Range<usize>, Arc<str>)>>,
oid_texts: HashMap<git::Oid, Arc<str>>,
head_text_buffer: WeakEntity<Buffer>,
index_text_buffer: WeakEntity<Buffer>,
@ -151,6 +162,24 @@ struct BufferGitState {
language_changed: bool,
}
fn pending_hunks(
hunks: &[DiffHunk],
version: &clock::Global,
sense: PendingSense,
) -> Vec<PendingHunk> {
hunks
.iter()
.map(|hunk| {
PendingHunk::new(
hunk.buffer_range.clone(),
hunk.diff_base_byte_range.clone(),
version.clone(),
sense,
)
})
.collect()
}
#[derive(Clone, Debug)]
enum DiffBasesChange {
SetIndex(Option<String>),
@ -170,6 +199,73 @@ enum DiffKind {
SinceOid(Option<git::Oid>),
}
struct IndexTextFile {
path: Arc<RelPath>,
full_path: PathBuf,
path_style: PathStyle,
file_name: String,
worktree_id: WorktreeId,
is_private: bool,
}
impl IndexTextFile {
fn new(file: &dyn language::File, cx: &App) -> Self {
Self {
path: file.path().clone(),
full_path: file.full_path(cx),
path_style: file.path_style(cx),
file_name: file.file_name(cx).to_string(),
worktree_id: file.worktree_id(cx),
is_private: file.is_private(),
}
}
}
impl language::File for IndexTextFile {
fn as_local(&self) -> Option<&dyn language::LocalFile> {
None
}
fn disk_state(&self) -> language::DiskState {
language::DiskState::Historic { was_deleted: false }
}
fn path(&self) -> &Arc<RelPath> {
&self.path
}
fn full_path(&self, _: &App) -> PathBuf {
self.full_path.clone()
}
fn path_style(&self, _: &App) -> PathStyle {
self.path_style
}
fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
&self.file_name
}
fn worktree_id(&self, _: &App) -> WorktreeId {
self.worktree_id
}
fn to_proto(&self, _: &App) -> rpc::proto::File {
rpc::proto::File {
worktree_id: self.worktree_id.to_proto(),
entry_id: None,
path: self.path.as_ref().to_proto(),
mtime: None,
is_deleted: false,
is_historic: true,
}
}
fn is_private(&self) -> bool {
self.is_private
}
}
#[derive(Debug, Clone, Copy)]
pub enum GitAccess {
/// Either:
@ -648,6 +744,7 @@ impl GitStore {
loading_diffs: HashMap::default(),
shared_diffs: HashMap::default(),
diffs: HashMap::default(),
buffer_ids_by_index_text_buffer_id: HashMap::default(),
}
}
@ -897,6 +994,16 @@ impl GitStore {
.as_ref()
.and_then(|weak| weak.upgrade())
{
// If this unstaged diff was first opened as the uncommitted diff's
// secondary, its index text wasn't highlighted. Enable it now and
// recalc so the language gets applied to the deleted (base) side.
diff_state.update(cx, |diff_state, cx| {
if !diff_state.index_text_buffer_language_enabled {
diff_state.index_text_buffer_language_enabled = true;
let buffer_snapshot = buffer.read(cx).text_snapshot();
diff_state.recalculate_diffs(buffer_snapshot, cx);
}
});
if let Some(task) =
diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
{
@ -944,15 +1051,17 @@ impl GitStore {
cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
}
/// Opens the staged (HEAD-vs-index) diff for the given buffer, along with
/// the index text buffer that is the diff's main buffer.
pub fn open_staged_diff(
&mut self,
buffer: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<Entity<BufferDiff>>> {
) -> Task<Result<(Entity<BufferDiff>, Entity<Buffer>)>> {
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()
&& let Some(staged_diff) = diff_state.read(cx).staged_diff_and_index_text_buffer()
{
if let Some(task) =
diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
@ -988,7 +1097,270 @@ impl GitStore {
})
.clone();
cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
cx.spawn(async move |this, cx| {
let diff = task.await.map_err(|e| anyhow!("{e}"))?;
this.update(cx, |this, cx| {
let index_text_buffer = this
.diffs
.get(&buffer_id)
.and_then(|diff_state| {
let (_, index_text_buffer) = diff_state.read(cx).staged_diff.as_ref()?;
Some(index_text_buffer.clone())
})
.context("index text buffer missing after opening staged diff")?;
Ok((diff, index_text_buffer))
})?
})
}
/// Stages the worktree changes covered by `worktree_ranges`, acting on the
/// given unstaged (index-vs-worktree) diff. Used by both the unstaged-changes
/// view and the uncommitted (gutter) controls: "stage" means the same index
/// change regardless of which view it was invoked from, so callers holding an
/// uncommitted diff pass its unstaged secondary.
///
/// Decomposes the worktree region into the unstaged hunks it covers, so no
/// worktree->index projection is needed. Optimistically suppresses the staged
/// hunks from the unstaged diff and, if the uncommitted diff happens to be
/// open, marks the corresponding uncommitted hunks as staging.
pub fn stage_hunks(
&mut self,
buffer: Entity<Buffer>,
unstaged_diff: Entity<BufferDiff>,
worktree_ranges: Vec<Range<Anchor>>,
cx: &mut Context<Self>,
) -> Result<()> {
if worktree_ranges.is_empty() {
return Ok(());
}
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer_id = buffer_snapshot.remote_id();
let file_exists = buffer_snapshot
.file()
.is_some_and(|file| file.disk_state().exists());
let unstaged_snapshot = unstaged_diff.read(cx).snapshot(cx);
// Decompose: the unstaged hunks the worktree region covers carry the
// index range directly. Sorting by buffer offset also sorts by index
// offset, since the hunks are non-overlapping. We read the raw hunks
// (ignoring optimistic suppression) so that re-staging a hunk that was
// optimistically staged and then unstaged still finds it. The footprints
// let the patch drop earlier edits the region covers even where the raw
// diff is empty (re-staging a hunk that is staged on disk but
// optimistically unstaged).
let mut unstaged_hunks = Vec::new();
let mut index_footprints = Vec::new();
for range in &worktree_ranges {
unstaged_hunks.extend(
unstaged_snapshot.raw_hunks_intersecting_range(range.clone(), &buffer_snapshot),
);
index_footprints.push(
unstaged_snapshot.base_text_range_for_buffer_range(range.clone(), &buffer_snapshot),
);
}
unstaged_hunks.sort_by_key(|hunk| hunk.buffer_range.start.to_offset(&buffer_snapshot));
unstaged_hunks.dedup_by(|a, b| a.buffer_range.start == b.buffer_range.start);
// The uncommitted hunks the region covers get the optimistic
// "staging" secondary status (free cross-view update).
let uncommitted_diff = self.get_uncommitted_diff(buffer_id, cx);
let mut uncommitted_hunks = Vec::new();
if let Some(uncommitted_diff) = &uncommitted_diff {
let uncommitted_snapshot = uncommitted_diff.read(cx).snapshot(cx);
for range in &worktree_ranges {
uncommitted_hunks.extend(
uncommitted_snapshot
.hunks_intersecting_range(range.clone(), &buffer_snapshot)
.filter(|hunk| {
hunk.secondary_status != DiffHunkSecondaryStatus::NoSecondaryHunk
}),
);
}
uncommitted_hunks
.sort_by_key(|hunk| hunk.buffer_range.start.to_offset(&buffer_snapshot));
uncommitted_hunks.dedup_by(|a, b| a.buffer_range.start == b.buffer_range.start);
}
let index_edits = if !file_exists {
// The worktree file is gone: staging removes it from the index.
None
} else {
Some(
unstaged_hunks
.iter()
.map(|hunk| {
let worktree_range = hunk.buffer_range.to_offset(&buffer_snapshot);
let replacement: Arc<str> = Arc::from(
buffer_snapshot
.text_for_range(worktree_range)
.collect::<String>(),
);
(hunk.diff_base_byte_range.clone(), replacement)
})
.collect::<Vec<_>>(),
)
};
let version = buffer_snapshot.version().clone();
let unstaged_pending = pending_hunks(&unstaged_hunks, &version, PendingSense::Suppress);
let uncommitted_pending = pending_hunks(
&uncommitted_hunks,
&version,
PendingSense::SetSecondaryStatus { stage: true },
);
drop(unstaged_snapshot);
let diff_state = self
.diffs
.get(&buffer_id)
.cloned()
.context("failed to find git state for buffer")?;
diff_state.update(cx, |diff_state, _| {
diff_state.remove_overlapping_pending_index_edits(&index_footprints);
diff_state.insert_pending_index_edits(index_edits);
});
if let Some(uncommitted_diff) = uncommitted_diff {
uncommitted_diff.update(cx, |diff, cx| {
diff.set_pending_hunks(&uncommitted_pending, &buffer_snapshot, cx);
});
}
unstaged_diff.update(cx, |diff, cx| {
diff.set_pending_hunks(&unstaged_pending, &buffer_snapshot, cx);
});
self.write_optimistic_index(buffer_id, cx);
Ok(())
}
/// Unstages the worktree changes covered by `worktree_ranges`, acting on the
/// given uncommitted (HEAD-vs-worktree) diff, invoked from the uncommitted
/// (gutter) controls. Uses the worktree->index projection (the hard part)
/// because the acted-on hunks are HEAD-vs-worktree.
pub fn unstage_uncommitted_hunks(
&mut self,
buffer: Entity<Buffer>,
uncommitted_diff: Entity<BufferDiff>,
worktree_ranges: Vec<Range<Anchor>>,
cx: &mut Context<Self>,
) -> Result<()> {
if worktree_ranges.is_empty() {
return Ok(());
}
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer_id = buffer_snapshot.remote_id();
let file_exists = buffer_snapshot
.file()
.is_some_and(|file| file.disk_state().exists());
let uncommitted_snapshot = uncommitted_diff.read(cx).snapshot(cx);
let unstaged_snapshot = uncommitted_snapshot
.secondary_diff()
.context("diff has no unstaged secondary")?;
let mut hunks = Vec::new();
for range in &worktree_ranges {
hunks.extend(
uncommitted_snapshot.hunks_intersecting_range(range.clone(), &buffer_snapshot),
);
}
hunks.sort_by_key(|hunk| hunk.buffer_range.start.to_offset(&buffer_snapshot));
hunks.dedup_by(|a, b| a.buffer_range.start == b.buffer_range.start);
let (index_edits, pending) = uncommitted_snapshot.compute_uncommitted_index_edits(
unstaged_snapshot,
false,
&hunks,
&buffer_snapshot,
file_exists,
);
drop(uncommitted_snapshot);
let diff_state = self
.diffs
.get(&buffer_id)
.cloned()
.context("failed to find git state for buffer")?;
diff_state.update(cx, |diff_state, _| {
diff_state.insert_pending_index_edits(index_edits)
});
uncommitted_diff.update(cx, |diff, cx| {
diff.set_pending_hunks(&pending, &buffer_snapshot, cx);
});
self.write_optimistic_index(buffer_id, cx);
Ok(())
}
/// Unstages staged (HEAD-vs-index) hunks covered by `index_ranges` (in the
/// index text buffer's coordinates), acting on the given staged diff,
/// invoked from the staged-changes view. The acted-on hunks already carry
/// an index range, so no projection is needed; optimistically suppresses
/// them from the staged diff.
pub fn unstage_staged_hunks(
&mut self,
staged_diff: Entity<BufferDiff>,
index_ranges: Vec<Range<Anchor>>,
cx: &mut Context<Self>,
) -> Result<()> {
if index_ranges.is_empty() {
return Ok(());
}
let index_buffer_id = staged_diff.read(cx).buffer_id;
let buffer_id = *self
.buffer_ids_by_index_text_buffer_id
.get(&index_buffer_id)
.context("failed to find git state for index text buffer")?;
let diff_state = self
.diffs
.get(&buffer_id)
.cloned()
.context("failed to find git state for buffer")?;
let index_buffer = diff_state
.read(cx)
.index_text_buffer()
.context("index text is not loaded")?;
let index_snapshot = index_buffer.read(cx).text_snapshot();
let staged_snapshot = staged_diff.read(cx).snapshot(cx);
let mut hunks = Vec::new();
for range in &index_ranges {
hunks.extend(staged_snapshot.hunks_intersecting_range(range.clone(), &index_snapshot));
}
hunks.sort_by_key(|hunk| hunk.buffer_range.start.to_offset(&index_snapshot));
hunks.dedup_by(|a, b| a.buffer_range.start == b.buffer_range.start);
let index_edits = staged_diff
.read(cx)
.unstage_staged_hunks(&hunks, &index_snapshot);
let version = index_snapshot.version().clone();
let pending = pending_hunks(&hunks, &version, PendingSense::Suppress);
drop(staged_snapshot);
diff_state.update(cx, |diff_state, _| {
diff_state.insert_pending_index_edits(index_edits)
});
staged_diff.update(cx, |diff, cx| {
diff.set_pending_hunks(&pending, &index_snapshot, cx);
});
self.write_optimistic_index(buffer_id, cx);
Ok(())
}
/// Derives the desired index text from the buffer's optimistic patch and
/// schedules the write.
fn write_optimistic_index(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) {
let Some(diff_state) = self.diffs.get(&buffer_id) else {
return;
};
let new_index_text = diff_state
.read(cx)
.pending_index_text(cx)
.map(|rope| rope.to_string());
self.write_index_text_for_buffer_id(buffer_id, new_index_text, cx);
}
pub fn open_diff_since(
@ -1058,9 +1430,6 @@ impl GitStore {
});
this.update(cx, |this, cx| {
cx.subscribe(&buffer_diff, Self::on_buffer_diff_event)
.detach();
this.loading_diffs.remove(&(buffer_id, diff_kind));
let git_store = cx.weak_entity();
@ -1174,6 +1543,9 @@ impl GitStore {
let buffer_id = buffer.remote_id();
let language = buffer.language().cloned();
let language_registry = buffer.language_registry();
let index_text_file = buffer.file().map(|file| {
Arc::new(IndexTextFile::new(file.as_ref(), cx)) as Arc<dyn language::File>
});
let text_snapshot = buffer.text_snapshot();
this.loading_diffs.remove(&(buffer_id, kind));
@ -1194,7 +1566,7 @@ impl GitStore {
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)
diff_state.get_or_create_index_text_buffer(index_text_file.clone(), cx)
});
cx.new(|cx| {
BufferDiff::new_with_base_text_buffer(
@ -1208,7 +1580,10 @@ impl GitStore {
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_index_text_buffer(
index_text_file.clone(),
cx,
),
diff_state.get_or_create_head_text_buffer(cx),
)
});
@ -1244,16 +1619,19 @@ impl GitStore {
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| {
let rx = diff_state.update(cx, |diff_state, cx| {
diff_state.language = language;
diff_state.language_registry = language_registry;
match kind {
DiffKind::Unstaged => {
diff_state.unstaged_diff = Some(diff.downgrade());
// The deleted (base) side of a standalone unstaged diff
// is the index, so highlight it. The recalc kicked off by
// `diff_bases_changed` below applies the language.
diff_state.index_text_buffer_language_enabled = true;
}
DiffKind::Staged => {
diff_state.index_text_buffer_language_enabled = true;
@ -1266,7 +1644,8 @@ impl GitStore {
let unstaged_diff = if let Some(diff) = existing_unstaged_diff {
diff
} else {
let base_text_buffer = diff_state.get_or_create_index_text_buffer(cx);
let base_text_buffer =
diff_state.get_or_create_index_text_buffer(index_text_file, cx);
let unstaged_diff = cx.new(|cx| {
BufferDiff::new_with_base_text_buffer(
&text_snapshot,
@ -1295,7 +1674,15 @@ impl GitStore {
}
Ok(diff)
})
})
});
let diff_state = this.diffs.get(&buffer_id).cloned();
if let Some(index_text_buffer) =
diff_state.and_then(|diff_state| diff_state.read(cx).index_text_buffer())
{
this.buffer_ids_by_index_text_buffer_id
.insert(index_text_buffer.read(cx).remote_id(), buffer_id);
}
rx
})??
.await
}
@ -1997,6 +2384,8 @@ impl GitStore {
}
BufferStoreEvent::BufferDropped(buffer_id) => {
self.diffs.remove(buffer_id);
self.buffer_ids_by_index_text_buffer_id
.retain(|_, main_buffer_id| main_buffer_id != buffer_id);
for diffs in self.shared_diffs.values_mut() {
diffs.remove(buffer_id);
}
@ -2073,48 +2462,45 @@ impl GitStore {
}
}
fn on_buffer_diff_event(
fn write_index_text_for_buffer_id(
&mut self,
diff: Entity<buffer_diff::BufferDiff>,
event: &BufferDiffEvent,
buffer_id: BufferId,
new_index_text: Option<String>,
cx: &mut Context<Self>,
) {
if let BufferDiffEvent::HunksStagedOrUnstaged(new_index_text) = event {
let buffer_id = diff.read(cx).buffer_id;
if let Some(diff_state) = self.diffs.get(&buffer_id) {
let new_index_text = new_index_text.as_ref().map(|rope| rope.to_string());
if new_index_text.as_deref() == diff_state.read(cx).index_text.as_deref() {
return;
}
let hunk_staging_operation_count = diff_state.update(cx, |diff_state, _| {
diff_state.hunk_staging_operation_count += 1;
diff_state.hunk_staging_operation_count
});
if let Some((repo, path)) = self.repository_and_path_for_buffer_id(buffer_id, cx) {
let recv = repo.update(cx, |repo, cx| {
log::debug!("hunks changed for {}", path.as_unix_str());
repo.spawn_set_index_text_job(
path,
new_index_text,
Some(hunk_staging_operation_count),
cx,
)
});
let diff = diff.downgrade();
cx.spawn(async move |this, cx| {
if let Ok(Err(error)) = cx.background_spawn(recv).await {
diff.update(cx, |diff, cx| {
diff.clear_pending_hunks(cx);
})
.ok();
this.update(cx, |_, cx| cx.emit(GitStoreEvent::IndexWriteError(error)))
.ok();
}
})
.detach();
}
let Some(diff_state) = self.diffs.get(&buffer_id) else {
return;
};
let hunk_staging_operation_count = diff_state.update(cx, |diff_state, _| {
diff_state.hunk_staging_operation_count += 1;
diff_state.hunk_staging_operation_count
});
let Some((repo, path)) = self.repository_and_path_for_buffer_id(buffer_id, cx) else {
return;
};
let recv = repo.update(cx, |repo, cx| {
log::debug!("hunks changed for {}", path.as_unix_str());
repo.spawn_set_index_text_job(
path,
new_index_text,
Some(hunk_staging_operation_count),
cx,
)
});
cx.spawn(async move |this, cx| {
if let Ok(Err(error)) = cx.background_spawn(recv).await {
this.update(cx, |this, cx| {
if let Some(diff_state) = this.diffs.get(&buffer_id).cloned() {
diff_state.update(cx, |diff_state, cx| {
diff_state.clear_pending_index_edits_and_hunks(cx);
});
}
cx.emit(GitStoreEvent::IndexWriteError(error));
})
.ok();
}
}
})
.detach();
}
fn local_worktree_git_repos_changed(
@ -3951,6 +4337,7 @@ impl BufferGitState {
hunk_staging_operation_count_as_of_write: 0,
head_text: Default::default(),
index_text: Default::default(),
pending_index_edits: Some(Vec::new()),
oid_texts: Default::default(),
head_text_buffer: WeakEntity::new_invalid(),
index_text_buffer: WeakEntity::new_invalid(),
@ -3978,13 +4365,27 @@ impl BufferGitState {
buffer
}
fn get_or_create_index_text_buffer(&mut self, cx: &mut Context<Self>) -> Entity<Buffer> {
fn index_text_buffer(&self) -> Option<Entity<Buffer>> {
self.index_text_buffer.upgrade()
}
fn get_or_create_index_text_buffer(
&mut self,
file: Option<Arc<dyn language::File>>,
cx: &mut Context<Self>,
) -> Entity<Buffer> {
if let Some(buffer) = self.index_text_buffer.upgrade() {
if let Some(file) = file {
buffer.update(cx, |buffer, cx| buffer.file_updated(file, cx));
}
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);
if let Some(file) = file {
buffer.file_updated(file, cx);
}
buffer.set_capability(Capability::ReadOnly, cx);
buffer
});
@ -4055,6 +4456,11 @@ impl BufferGitState {
self.staged_diff.as_ref().and_then(|(set, _)| set.upgrade())
}
fn staged_diff_and_index_text_buffer(&self) -> Option<(Entity<BufferDiff>, Entity<Buffer>)> {
let (diff, index_text_buffer) = self.staged_diff.as_ref()?;
Some((diff.upgrade()?, index_text_buffer.clone()))
}
fn uncommitted_diff(&self) -> Option<Entity<BufferDiff>> {
self.uncommitted_diff.as_ref().and_then(|set| set.upgrade())
}
@ -4076,6 +4482,100 @@ impl BufferGitState {
}
}
fn remove_overlapping_pending_index_edits(&mut self, ranges: &[Range<usize>]) {
if let Some(edits) = &mut self.pending_index_edits {
edits.retain(|(existing, _)| {
ranges.iter().all(|footprint| {
existing.end < footprint.start || footprint.end < existing.start
})
});
}
}
fn insert_pending_index_edits(&mut self, edits: Option<Vec<(Range<usize>, Arc<str>)>>) {
match edits {
None => {
self.pending_index_edits = None;
}
Some(new_edits) => {
let mut edits = self.pending_index_edits.take().unwrap_or_default();
for (range, replacement) in new_edits {
edits.retain(|(existing, _)| {
existing.end < range.start || range.end < existing.start
});
let position =
edits.partition_point(|(existing, _)| existing.start < range.start);
edits.insert(position, (range, replacement));
}
self.pending_index_edits = Some(edits);
}
}
}
fn pending_index_text(&self, cx: &App) -> Option<Rope> {
let index_text_buffer = self.index_text_buffer.upgrade()?;
let edits = self.pending_index_edits.as_ref()?;
#[cfg(debug_assertions)]
for window in edits.windows(2) {
debug_assert!(window[0].0.end <= window[1].0.start);
}
let mut index_text = index_text_buffer.read(cx).text_snapshot().as_rope().clone();
for (old_range, replacement_text) in edits.iter().rev() {
index_text.replace(old_range.clone(), replacement_text);
}
Some(index_text)
}
fn clear_pending_index_edits(&mut self) {
self.pending_index_edits = Some(Vec::new());
}
fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
for diff in [
self.uncommitted_diff(),
self.unstaged_diff(),
self.staged_diff(),
]
.into_iter()
.flatten()
{
diff.update(cx, |diff, cx| diff.clear_pending_hunks(cx));
}
}
fn mark_whole_file_stage_or_unstage_pending(
&mut self,
stage: bool,
buffer_snapshot: &text::BufferSnapshot,
cx: &mut Context<Self>,
) {
if let Some(uncommitted_diff) = self.uncommitted_diff() {
uncommitted_diff.update(cx, |uncommitted_diff, cx| {
uncommitted_diff.mark_all_hunks_pending(stage, buffer_snapshot, cx);
});
}
if stage {
if let Some(unstaged_diff) = self.unstaged_diff() {
unstaged_diff.update(cx, |unstaged_diff, cx| {
unstaged_diff.suppress_all_hunks_pending(buffer_snapshot, cx);
});
}
} else if let Some(staged_diff) = self.staged_diff()
&& let Some(index_text_buffer) = self.index_text_buffer()
{
let index_snapshot = index_text_buffer.read(cx).text_snapshot();
staged_diff.update(cx, |staged_diff, cx| {
staged_diff.suppress_all_hunks_pending(&index_snapshot, cx);
});
}
}
fn clear_pending_index_edits_and_hunks(&mut self, cx: &mut Context<Self>) {
self.clear_pending_index_edits();
self.clear_pending_hunks(cx);
}
fn handle_base_texts_updated(
&mut self,
buffer: text::BufferSnapshot,
@ -4089,16 +4589,17 @@ impl BufferGitState {
};
let diff_bases_change = match mode {
Mode::HeadOnly => DiffBasesChange::SetHead(message.committed_text),
Mode::IndexOnly => DiffBasesChange::SetIndex(message.staged_text),
Mode::IndexMatchesHead => DiffBasesChange::SetBoth(message.committed_text),
Mode::IndexAndHead => DiffBasesChange::SetEach {
Mode::HeadOnly => Some(DiffBasesChange::SetHead(message.committed_text)),
Mode::IndexOnly => Some(DiffBasesChange::SetIndex(message.staged_text)),
Mode::IndexMatchesHead => Some(DiffBasesChange::SetBoth(message.committed_text)),
Mode::IndexAndHead => Some(DiffBasesChange::SetEach {
index: message.staged_text,
head: message.committed_text,
},
}),
Mode::Unchanged => None,
};
self.diff_bases_changed(buffer, Some(diff_bases_change), cx);
self.diff_bases_changed(buffer, diff_bases_change, cx);
}
pub fn wait_for_recalculation(&mut self) -> Option<impl Future<Output = ()> + use<>> {
@ -4403,7 +4904,9 @@ impl BufferGitState {
return Ok(());
}
this.update(cx, |_, cx| {
this.update(cx, |this, cx| {
this.clear_pending_index_edits();
if let (Some(staged_diff), Some(new_staged_diff)) =
(staged_diff.as_ref(), new_staged_diff.clone())
{
@ -4422,7 +4925,7 @@ impl BufferGitState {
head_text_buffer.fast_forward(edited_head_text, cx)
});
}
diff.set_snapshot(new_staged_diff, cx)
diff.set_snapshot_with_secondary(new_staged_diff, None, true, cx)
});
}
@ -4437,7 +4940,7 @@ impl BufferGitState {
index_text_buffer.fast_forward(edited_index_text, cx)
});
}
diff.set_snapshot(new_unstaged_diff, cx)
diff.set_snapshot_with_secondary(new_unstaged_diff, None, true, cx)
}))
} else {
None
@ -5244,21 +5747,23 @@ impl Repository {
diff_state.update(cx, |diff_state, cx| {
use proto::update_diff_bases::Mode;
if let Some((diff_bases_change, (client, project_id))) =
diff_bases_change.clone().zip(downstream_client)
{
let (staged_text, committed_text, mode) = match diff_bases_change {
DiffBasesChange::SetIndex(index) => {
(index, None, Mode::IndexOnly)
}
DiffBasesChange::SetHead(head) => (None, head, Mode::HeadOnly),
DiffBasesChange::SetEach { index, head } => {
(index, head, Mode::IndexAndHead)
}
DiffBasesChange::SetBoth(text) => {
(None, text, Mode::IndexMatchesHead)
}
};
if let Some((client, project_id)) = downstream_client {
let (staged_text, committed_text, mode) =
match diff_bases_change.clone() {
Some(DiffBasesChange::SetIndex(index)) => {
(index, None, Mode::IndexOnly)
}
Some(DiffBasesChange::SetHead(head)) => {
(None, head, Mode::HeadOnly)
}
Some(DiffBasesChange::SetEach { index, head }) => {
(index, head, Mode::IndexAndHead)
}
Some(DiffBasesChange::SetBoth(text)) => {
(None, text, Mode::IndexMatchesHead)
}
None => (None, None, Mode::Unchanged),
};
client
.send(proto::UpdateDiffBases {
project_id: project_id.to_proto(),
@ -6454,33 +6959,15 @@ impl Repository {
else {
continue;
};
let Some(uncommitted_diff) =
diff_state.read(cx).uncommitted_diff.as_ref().and_then(
|uncommitted_diff| uncommitted_diff.upgrade(),
)
else {
continue;
};
let buffer_snapshot = buffer.read(cx).text_snapshot();
let file_exists = buffer
.read(cx)
.file()
.is_some_and(|file| file.disk_state().exists());
let hunk_staging_operation_count =
diff_state.update(cx, |diff_state, cx| {
uncommitted_diff.update(
cx,
|uncommitted_diff, cx| {
uncommitted_diff
.stage_or_unstage_all_hunks(
stage,
&buffer_snapshot,
file_exists,
cx,
);
},
);
diff_state
.mark_whole_file_stage_or_unstage_pending(
stage,
&buffer_snapshot,
cx,
);
diff_state.hunk_staging_operation_count += 1;
diff_state.hunk_staging_operation_count
});
@ -6547,14 +7034,8 @@ impl Repository {
if result.is_ok() {
diff_state.hunk_staging_operation_count_as_of_write =
hunk_staging_operation_count;
} else if let Some(uncommitted_diff) =
&diff_state.uncommitted_diff
{
uncommitted_diff
.update(cx, |uncommitted_diff, cx| {
uncommitted_diff.clear_pending_hunks(cx);
})
.ok();
} else {
diff_state.clear_pending_hunks(cx);
}
})
.ok();

View file

@ -24,6 +24,8 @@ use crate::{
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum DiffBase {
Head,
Index,
Staged,
Merge { base_ref: SharedString },
}
@ -33,7 +35,7 @@ impl DiffBase {
}
}
pub struct BranchDiff {
pub struct DiffBufferList {
diff_base: DiffBase,
repo: Option<Entity<Repository>>,
project: Entity<Project>,
@ -52,9 +54,9 @@ pub enum BranchDiffEvent {
DiffBaseChanged,
}
impl EventEmitter<BranchDiffEvent> for BranchDiff {}
impl EventEmitter<BranchDiffEvent> for DiffBufferList {}
impl BranchDiff {
impl DiffBufferList {
pub fn new(
source: DiffBase,
project: Entity<Project>,
@ -350,8 +352,13 @@ impl BranchDiff {
.as_ref()
.and_then(|t| t.entries.get(&item.repo_path))
.cloned();
let Some(status) = self.merge_statuses(Some(item.status), branch_diff.as_ref())
else {
let Some(status) = (match self.diff_base {
DiffBase::Head | DiffBase::Merge { .. } => {
self.merge_statuses(Some(item.status), branch_diff.as_ref())
}
DiffBase::Index => item.status.staging().has_unstaged().then_some(item.status),
DiffBase::Staged => item.status.staging().has_staged().then_some(item.status),
}) else {
continue;
};
if !status.has_changes() {
@ -363,7 +370,13 @@ impl BranchDiff {
else {
continue;
};
let task = Self::load_buffer(branch_diff, project_path, repo.clone(), cx);
let task = Self::load_buffer(
self.diff_base.clone(),
branch_diff,
project_path,
repo.clone(),
cx,
);
output.push(DiffBuffer {
repo_path: item.repo_path.clone(),
@ -383,8 +396,13 @@ impl BranchDiff {
let Some(project_path) = repo.read(cx).repo_path_to_project_path(&path, cx) else {
continue;
};
let task =
Self::load_buffer(Some(branch_diff.clone()), project_path, repo.clone(), cx);
let task = Self::load_buffer(
self.diff_base.clone(),
Some(branch_diff.clone()),
project_path,
repo.clone(),
cx,
);
let file_status = diff_status_to_file_status(branch_diff);
@ -400,44 +418,87 @@ impl BranchDiff {
#[instrument(skip_all)]
fn load_buffer(
diff_base: DiffBase,
branch_diff: Option<git::status::TreeDiffStatus>,
project_path: crate::ProjectPath,
repo: Entity<Repository>,
cx: &Context<'_, Project>,
) -> Task<Result<(Entity<Buffer>, Entity<BufferDiff>, Entity<ConflictSet>)>> {
) -> Task<Result<LoadedDiffBuffer>> {
let task = cx.spawn(async move |project, cx| {
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
.await?;
let changes = if let Some(entry) = branch_diff {
let oid = match entry {
git::status::TreeDiffStatus::Added { .. } => None,
git::status::TreeDiffStatus::Modified { old, .. }
| git::status::TreeDiffStatus::Deleted { old } => Some(old),
};
project
.update(cx, |project, cx| {
project.git_store().update(cx, |git_store, cx| {
git_store.open_diff_since(oid, buffer.clone(), repo, cx)
})
})?
.await?
} else {
project
.update(cx, |project, cx| {
project.open_uncommitted_diff(buffer.clone(), cx)
})?
.await?
let main_buffer = buffer.clone();
let load_conflict_set = diff_base != DiffBase::Staged;
let (display_buffer, changes) = match diff_base {
DiffBase::Head => {
let diff = project
.update(cx, |project, cx| {
project.open_uncommitted_diff(buffer.clone(), cx)
})?
.await?;
(buffer, diff)
}
DiffBase::Index => {
let diff = project
.update(cx, |project, cx| {
project.open_unstaged_diff(buffer.clone(), cx)
})?
.await?;
(buffer, diff)
}
DiffBase::Staged => {
let (diff, index_buffer) = project
.update(cx, |project, cx| {
project.open_staged_diff(buffer.clone(), cx)
})?
.await?;
(index_buffer, diff)
}
DiffBase::Merge { .. } => {
let diff = if let Some(entry) = branch_diff {
let oid = match entry {
git::status::TreeDiffStatus::Added { .. } => None,
git::status::TreeDiffStatus::Modified { old, .. }
| git::status::TreeDiffStatus::Deleted { old } => Some(old),
};
project
.update(cx, |project, cx| {
project.git_store().update(cx, |git_store, cx| {
git_store.open_diff_since(oid, buffer.clone(), repo, cx)
})
})?
.await?
} else {
project
.update(cx, |project, cx| {
project.open_uncommitted_diff(buffer.clone(), cx)
})?
.await?
};
(buffer, diff)
}
};
let conflict_set = project
.update(cx, |project, cx| {
project.git_store().update(cx, |git_store, cx| {
git_store.open_conflict_set(buffer.clone(), cx)
})
})?
.await;
Ok((buffer, changes, conflict_set))
let conflict_set = if load_conflict_set {
Some(
project
.update(cx, |project, cx| {
project.git_store().update(cx, |git_store, cx| {
git_store.open_conflict_set(main_buffer.clone(), cx)
})
})?
.await,
)
} else {
None
};
Ok(LoadedDiffBuffer {
display_buffer,
main_buffer,
diff: changes,
conflict_set,
})
});
task
}
@ -461,9 +522,17 @@ fn diff_status_to_file_status(branch_diff: &git::status::TreeDiffStatus) -> File
file_status
}
#[derive(Debug)]
pub struct LoadedDiffBuffer {
pub display_buffer: Entity<Buffer>,
pub main_buffer: Entity<Buffer>,
pub diff: Entity<BufferDiff>,
pub conflict_set: Option<Entity<ConflictSet>>,
}
#[derive(Debug)]
pub struct DiffBuffer {
pub repo_path: RepoPath,
pub file_status: FileStatus,
pub load: Task<Result<(Entity<Buffer>, Entity<BufferDiff>, Entity<ConflictSet>)>>,
pub load: Task<Result<LoadedDiffBuffer>>,
}

View file

@ -3245,12 +3245,14 @@ impl Project {
.update(cx, |git_store, cx| git_store.open_unstaged_diff(buffer, cx))
}
/// Opens the staged (HEAD-vs-index) diff for the given buffer, along with
/// the index text buffer that is the diff's main buffer.
#[ztracing::instrument(skip_all)]
pub fn open_staged_diff(
&mut self,
buffer: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<Entity<BufferDiff>>> {
) -> Task<Result<(Entity<BufferDiff>, Entity<Buffer>)>> {
if self.is_disconnected(cx) {
return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
}
@ -3272,6 +3274,59 @@ impl Project {
})
}
/// Stages the worktree changes covered by `worktree_ranges` (in the worktree
/// buffer's coordinates), acting on the given unstaged diff. Used by both the
/// unstaged-changes view and the uncommitted (gutter) controls.
pub fn stage_hunks(
&mut self,
buffer: Entity<Buffer>,
unstaged_diff: Entity<BufferDiff>,
worktree_ranges: Vec<Range<Anchor>>,
cx: &mut Context<Self>,
) -> Result<()> {
if self.is_disconnected(cx) {
return Err(anyhow!(ErrorCode::Disconnected));
}
self.git_store.update(cx, |git_store, cx| {
git_store.stage_hunks(buffer, unstaged_diff, worktree_ranges, cx)
})
}
/// Unstages the worktree changes covered by `worktree_ranges` (in the worktree
/// buffer's coordinates), acting on the given uncommitted diff. Used by the
/// uncommitted (gutter) controls.
pub fn unstage_uncommitted_hunks(
&mut self,
buffer: Entity<Buffer>,
uncommitted_diff: Entity<BufferDiff>,
worktree_ranges: Vec<Range<Anchor>>,
cx: &mut Context<Self>,
) -> Result<()> {
if self.is_disconnected(cx) {
return Err(anyhow!(ErrorCode::Disconnected));
}
self.git_store.update(cx, |git_store, cx| {
git_store.unstage_uncommitted_hunks(buffer, uncommitted_diff, worktree_ranges, cx)
})
}
/// Unstages the staged changes covered by `index_ranges` (in the index
/// buffer's coordinates), acting on the given staged diff. Used by the
/// staged-changes view.
pub fn unstage_staged_hunks(
&mut self,
staged_diff: Entity<BufferDiff>,
index_ranges: Vec<Range<Anchor>>,
cx: &mut Context<Self>,
) -> Result<()> {
if self.is_disconnected(cx) {
return Err(anyhow!(ErrorCode::Disconnected));
}
self.git_store.update(cx, |git_store, cx| {
git_store.unstage_staged_hunks(staged_diff, index_ranges, cx)
})
}
pub fn open_buffer_by_id(
&mut self,
id: BufferId,

File diff suppressed because it is too large Load diff

View file

@ -25,6 +25,11 @@ message UpdateDiffBases {
// and the contents of the index and HEAD differ for this path,
// where None means the path doesn't exist in that state of the repo.
INDEX_AND_HEAD = 3;
// The contents of the index and HEAD are unchanged. Sent so that
// remote clients still recalculate their diffs in response to git
// events, just as the host does, even when an index write ends up
// changing nothing (e.g. writing content identical to what's there).
UNCHANGED = 4;
}
optional string staged_text = 3;

View file

@ -101,7 +101,6 @@ impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
impl Render for BufferSearchBar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
let has_splittable_editor = self.splittable_editor.is_some();
let split_buttons = self
.splittable_editor
@ -539,7 +538,9 @@ impl ToolbarItemView for BufferSearchBar {
.and_then(|entity| entity.downcast::<SplittableEditor>().ok())
{
self._splittable_editor_subscription =
Some(cx.observe(&splittable_editor, |_, _, cx| cx.notify()));
Some(cx.observe(&splittable_editor, |_, _, cx| {
cx.notify();
}));
self.splittable_editor = Some(splittable_editor.downgrade());
}

View file

@ -31,10 +31,13 @@ use feature_flags::{FeatureFlagAppExt as _, PanicFeatureFlag};
use fs::Fs;
use futures::FutureExt as _;
use futures::{StreamExt, channel::mpsc, select_biased};
use git_ui::branch_diff::BranchDiffToolbar;
use git_ui::commit_view::CommitViewToolbar;
use git_ui::git_panel::GitPanel;
use git_ui::project_diff::{BranchDiffToolbar, ProjectDiffToolbar};
use git_ui::project_diff::ProjectDiffToolbar;
use git_ui::solo_diff_view::{SoloDiffGitToolbar, SoloDiffStyleToolbar};
use git_ui::staged_diff::StagedDiffToolbar;
use git_ui::unstaged_diff::UnstagedDiffToolbar;
use gpui::{
Action, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, DismissEvent,
Element, Entity, FocusHandle, Focusable, Image, ImageFormat, KeyBinding, ParentElement,
@ -1413,6 +1416,10 @@ fn initialize_pane(
toolbar.add_item(highlights_tree_item, window, cx);
let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));
toolbar.add_item(project_diff_toolbar, window, cx);
let staged_diff_toolbar = cx.new(|cx| StagedDiffToolbar::new(workspace, cx));
toolbar.add_item(staged_diff_toolbar, window, cx);
let unstaged_diff_toolbar = cx.new(|cx| UnstagedDiffToolbar::new(workspace, cx));
toolbar.add_item(unstaged_diff_toolbar, window, cx);
let branch_diff_toolbar = cx.new(BranchDiffToolbar::new);
toolbar.add_item(branch_diff_toolbar, window, cx);
let solo_diff_git_toolbar = cx.new(SoloDiffGitToolbar::new);

View file

@ -352,6 +352,12 @@ pub mod git {
/// Opens the git branch selector.
#[action(deprecated_aliases = ["branches::OpenRecent"])]
Branch,
/// Shows uncommitted changes across the project.
ViewUncommittedChanges,
/// Shows unstaged changes across the project.
ViewUnstagedChanges,
/// Shows staged changes across the project.
ViewStagedChanges,
/// Opens the git stash selector.
ViewStash,
/// Opens the git worktree selector.