From 97f4c981acd3151b7cf13de9c03f36c46940f822 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 5 Feb 2026 12:26:32 -0500 Subject: [PATCH] very basic per-line staging visualization Co-authored-by: Smit Barmase --- crates/buffer_diff/src/buffer_diff.rs | 78 ++++++++++++++++++++++++- crates/editor/src/editor.rs | 2 + crates/editor/src/element.rs | 24 ++++++-- crates/language/src/language.rs | 4 +- crates/language/src/text_diff.rs | 36 +++++++----- crates/multi_buffer/src/multi_buffer.rs | 14 +++++ 6 files changed, 136 insertions(+), 22 deletions(-) diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 51e0a5be572e0ddc0e769dacde9cbb46a719b2a9..cdd3f3d2e39e04bc446f66d53c0c11226443299a 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -3,7 +3,7 @@ use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task}; use language::{ Capability, Diff, DiffOptions, File, Language, LanguageName, LanguageRegistry, - language_settings::language_settings, word_diff_ranges, + language_settings::language_settings, text_diff, word_diff_ranges, }; use rope::Rope; use std::{ @@ -108,12 +108,17 @@ pub struct DiffHunk { pub buffer_word_diffs: Vec>, // Offsets relative to the start of the deleted diff that represent word diff locations pub base_word_diffs: Vec>, + // These fields are nonempty only if the secondary status is OverlapsWithSecondaryHunk + pub buffer_staged_lines: Vec>, + pub base_staged_lines: Vec>, } /// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range. #[derive(Debug, Clone, PartialEq, Eq)] struct InternalDiffHunk { + // Range of text that has been added to the main buffer buffer_range: Range, + // Range of text that has been deleted from the diff base diff_base_byte_range: Range, base_word_diffs: Vec>, buffer_word_diffs: Vec>, @@ -820,6 +825,9 @@ impl BufferDiffInner { let base_word_diffs = hunk.base_word_diffs.clone(); let buffer_word_diffs = hunk.buffer_word_diffs.clone(); + let mut buffer_staged_lines = Vec::new(); + let mut base_staged_lines = Vec::new(); + if !start_anchor.is_valid(buffer) { continue; } @@ -879,6 +887,17 @@ impl BufferDiffInner { } else if secondary_range == (start_point..end_point) { secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk; } else if secondary_range.start <= end_point { + // FIXME this should be a background computation that only happens when either the diff or the secondary diff changes + let (buffer, base) = compute_staged_lines( + &hunk, + &secondary_hunk, + buffer, + &self.base_text, + &secondary.unwrap().base_text, + ); + buffer_staged_lines = buffer; + base_staged_lines = base; + secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk; } } @@ -891,6 +910,8 @@ impl BufferDiffInner { base_word_diffs, buffer_word_diffs, secondary_status, + buffer_staged_lines, + base_staged_lines, }); } }) @@ -917,11 +938,66 @@ impl BufferDiffInner { secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk, base_word_diffs: hunk.base_word_diffs.clone(), buffer_word_diffs: hunk.buffer_word_diffs.clone(), + base_staged_lines: Vec::new(), + buffer_staged_lines: Vec::new(), }) }) } } +fn compute_staged_lines( + hunk: &InternalDiffHunk, + secondary_hunk: &InternalDiffHunk, + buffer: &text::BufferSnapshot, + base_text: &language::BufferSnapshot, + secondary_base_text: &language::BufferSnapshot, +) -> (Vec>, Vec>) { + let primary_hunk_buffer_text = buffer + .text_for_range(hunk.buffer_range.clone()) + .collect::(); + let secondary_hunk_buffer_text = buffer + .text_for_range(secondary_hunk.buffer_range.clone()) + .collect::(); + let primary_hunk_base_text = base_text + .text_for_range(hunk.diff_base_byte_range.clone()) + .collect::(); + let secondary_hunk_base_text = secondary_base_text + .text_for_range(secondary_hunk.diff_base_byte_range.clone()) + .collect::(); + + let primary_hunk_start_in_buffer = hunk.buffer_range.start.to_offset(buffer); + // No word diffs + let buffer_staged_lines = language::text_diff_with_options_internal( + dbg!(&secondary_hunk_buffer_text), + dbg!(&primary_hunk_buffer_text), + DiffOptions { + language_scope: None, + max_word_diff_len: 0, + max_word_diff_line_count: 0, + }, + ) + .into_iter() + .map(|edit| { + buffer.anchor_before(edit.new.start + primary_hunk_start_in_buffer) + ..buffer.anchor_after(edit.new.end + primary_hunk_start_in_buffer) + }) + .collect::>(); + + // FIXME + if !buffer_staged_lines.is_empty() { + eprintln!( + "staged lines: {:?}", + buffer_staged_lines + .iter() + .map(|range| buffer.text_for_range(range.clone()).collect::()) + .collect::>() + ); + } + + // FIXME + (buffer_staged_lines, Vec::new()) +} + fn build_diff_options( file: Option<&Arc>, language: Option, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3f1717836bcb629719d9b31c162775d5a41a0402..d1e0f76f91ac8731822d294a43f60a9607f39feb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -20622,6 +20622,8 @@ impl Editor { ..hunk.diff_base_byte_range.end.0, secondary_status: hunk.status.secondary, range: Point::zero()..Point::zero(), // unused + buffer_staged_lines: Vec::new(), // unused + base_staged_lines: Vec::new(), // unused }) .collect::>(), &buffer_snapshot, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ed48b9ab5ec333c53a0413790bec2747c3859bae..489bae02f8520662284adaecc5319445df0658a3 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -9682,6 +9682,20 @@ impl Element for EditorElement { .row_infos(start_row) .take((start_row..end_row).len()) .collect::>(); + + // FIXME + let all_staged_lines = snapshot + .buffer_snapshot() + .diff_hunks_in_range(Anchor::min()..Anchor::max()) + .flat_map(|hunk| hunk.staged_rows) + .map(|row| { + snapshot + .display_snapshot + .point_to_display_point(Point::new(row.0, 0), Bias::Left) + .row() + }) + .collect::>(); + let is_row_soft_wrapped = |row: usize| { row_infos .get(row) @@ -9761,15 +9775,17 @@ impl Element for EditorElement { type_id: None, }; - let background = if Self::diff_hunk_hollow(diff_status, cx) { + let base_display_point = + DisplayPoint::new(start_row + DisplayRow(ix as u32), 0); + + let background = if Self::diff_hunk_hollow(diff_status, cx) + || all_staged_lines.contains(&base_display_point.row()) + { hollow_highlight } else { filled_highlight }; - let base_display_point = - DisplayPoint::new(start_row + DisplayRow(ix as u32), 0); - highlighted_rows .entry(base_display_point.row()) .or_insert(background); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 63fbc28f3cca111bfa1cfc1783b1a52e703caeb1..a3c3b16215b8ffdb4fd084da1d89bb77e3ba03bd 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -67,8 +67,8 @@ use task::RunnableTag; pub use task_context::{ContextLocation, ContextProvider, RunnableRange}; pub use text_diff::{ DiffOptions, apply_diff_patch, apply_reversed_diff_patch, line_diff, text_diff, - text_diff_with_options, unified_diff, unified_diff_with_context, unified_diff_with_offsets, - word_diff_ranges, + text_diff_with_options, text_diff_with_options_internal, unified_diff, + unified_diff_with_context, unified_diff_with_offsets, word_diff_ranges, }; use theme::SyntaxTheme; pub use toolchain::{ diff --git a/crates/language/src/text_diff.rs b/crates/language/src/text_diff.rs index c46796827242f0a483c0b31416b89f3e73fa14e8..e043c1a1dcba31ea667ea4553171e3f891e2e55f 100644 --- a/crates/language/src/text_diff.rs +++ b/crates/language/src/text_diff.rs @@ -237,12 +237,11 @@ impl Default for DiffOptions { /// Computes a diff between two strings, using a specific language scope's /// word characters for word-level diffing. -pub fn text_diff_with_options( +pub fn text_diff_with_options_internal( old_text: &str, new_text: &str, options: DiffOptions, -) -> Vec<(Range, Arc)> { - let empty: Arc = Arc::default(); +) -> Vec> { let mut edits = Vec::new(); let mut hunk_input = InternedInput::default(); let input = InternedInput::new( @@ -275,26 +274,33 @@ pub fn text_diff_with_options( old_offset + old_byte_range.start..old_offset + old_byte_range.end; let new_byte_range = new_offset + new_byte_range.start..new_offset + new_byte_range.end; - let replacement_text = if new_byte_range.is_empty() { - empty.clone() - } else { - new_text[new_byte_range].into() - }; - edits.push((old_byte_range, replacement_text)); + edits.push(text::Edit { + old: old_byte_range, + new: new_byte_range, + }); }); } else { - let replacement_text = if new_byte_range.is_empty() { - empty.clone() - } else { - new_text[new_byte_range].into() - }; - edits.push((old_byte_range, replacement_text)); + edits.push(text::Edit { + old: old_byte_range, + new: new_byte_range, + }); } }, ); edits } +pub fn text_diff_with_options( + old_text: &str, + new_text: &str, + options: DiffOptions, +) -> Vec<(Range, Arc)> { + text_diff_with_options_internal(old_text, new_text, options) + .into_iter() + .map(|edit| (edit.old, new_text[edit.new].into())) + .collect() +} + pub fn apply_diff_patch(base_text: &str, patch: &str) -> Result { let patch = diffy::Patch::from_str(patch).context("Failed to parse patch")?; let result = diffy::apply(base_text, &patch); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 9b66666e75ed577d4b988dc9498d36f172846d30..c8631177272d59c6d077773c8e3dc9a5e986f1db 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -150,6 +150,7 @@ pub struct MultiBufferDiffHunk { pub status: DiffHunkStatus, /// The word diffs for this hunk. pub word_diffs: Vec>, + pub staged_rows: Vec, } impl MultiBufferDiffHunk { @@ -3933,6 +3934,18 @@ impl MultiBufferSnapshot { }) .unwrap_or_default(); + let staged_rows = (!hunk.buffer_staged_lines.is_empty()) + .then(|| { + let mut rows = Vec::new(); + for range in &hunk.buffer_staged_lines { + let range = + Anchor::range_in_buffer(excerpt.id, range.clone()).to_point(self); + rows.extend((range.start.row..range.end.row).map(MultiBufferRow)); + } + rows + }) + .unwrap_or_default(); + let buffer_range = if is_inverted { excerpt.buffer.anchor_after(hunk.diff_base_byte_range.start) ..excerpt.buffer.anchor_before(hunk.diff_base_byte_range.end) @@ -3958,6 +3971,7 @@ impl MultiBufferSnapshot { kind: status_kind, secondary: hunk.secondary_status, }, + staged_rows, }) }) }