diff.rs

  1use anyhow::Result;
  2use buffer_diff::{BufferDiff, InternalDiffHunk};
  3use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task};
  4use itertools::Itertools;
  5use language::{
  6    Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, TextBuffer,
  7};
  8use multi_buffer::{MultiBuffer, PathKey, excerpt_context_lines};
  9use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
 10use streaming_diff::LineOperation;
 11use sum_tree::SumTree;
 12use util::ResultExt;
 13
 14pub enum Diff {
 15    Pending(PendingDiff),
 16    Finalized(FinalizedDiff),
 17}
 18
 19impl Diff {
 20    pub fn finalized(
 21        path: String,
 22        old_text: Option<String>,
 23        new_text: String,
 24        language_registry: Arc<LanguageRegistry>,
 25        cx: &mut Context<Self>,
 26    ) -> Self {
 27        let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly));
 28        let new_buffer = cx.new(|cx| Buffer::local(new_text, cx));
 29        let base_text = old_text.clone().unwrap_or(String::new()).into();
 30        let task = cx.spawn({
 31            let multibuffer = multibuffer.clone();
 32            let path = path.clone();
 33            let buffer = new_buffer.clone();
 34            async move |_, cx| {
 35                let language = language_registry
 36                    .load_language_for_file_path(Path::new(&path))
 37                    .await
 38                    .log_err();
 39
 40                buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx));
 41                buffer.update(cx, |buffer, _| buffer.parsing_idle()).await;
 42
 43                let diff = build_buffer_diff(
 44                    old_text.unwrap_or("".into()).into(),
 45                    &buffer,
 46                    Some(language_registry.clone()),
 47                    cx,
 48                )
 49                .await?;
 50
 51                multibuffer.update(cx, |multibuffer, cx| {
 52                    let hunk_ranges = {
 53                        let buffer = buffer.read(cx);
 54                        diff.read(cx)
 55                            .snapshot(cx)
 56                            .hunks_intersecting_range(
 57                                Anchor::min_for_buffer(buffer.remote_id())
 58                                    ..Anchor::max_for_buffer(buffer.remote_id()),
 59                                buffer,
 60                            )
 61                            .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
 62                            .collect::<Vec<_>>()
 63                    };
 64
 65                    multibuffer.set_excerpts_for_path(
 66                        PathKey::for_buffer(&buffer, cx),
 67                        buffer.clone(),
 68                        hunk_ranges,
 69                        excerpt_context_lines(cx),
 70                        cx,
 71                    );
 72                    multibuffer.add_diff(diff, cx);
 73                });
 74
 75                anyhow::Ok(())
 76            }
 77        });
 78
 79        Self::Finalized(FinalizedDiff {
 80            multibuffer,
 81            path,
 82            base_text,
 83            new_buffer,
 84            _update_diff: task,
 85        })
 86    }
 87
 88    pub fn new(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self {
 89        let buffer_text_snapshot = buffer.read(cx).text_snapshot();
 90        let language = buffer.read(cx).language().cloned();
 91        let language_registry = buffer.read(cx).language_registry();
 92        let buffer_diff = cx.new(|cx| {
 93            let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, cx);
 94            diff.language_changed(language.clone(), language_registry.clone(), cx);
 95            let secondary_diff = cx.new(|cx| {
 96                // For the secondary diff buffer we skip assigning the language as we do not really need to perform any syntax highlighting on
 97                // it. As a result, by skipping it we are potentially shaving off a lot of RSS plus we get a snappier feel for large diff
 98                // view multibuffers.
 99                BufferDiff::new_unchanged(&buffer_text_snapshot, cx)
100            });
101            diff.set_secondary_diff(secondary_diff);
102            diff
103        });
104
105        let multibuffer = cx.new(|cx| {
106            let mut multibuffer = MultiBuffer::without_headers(Capability::ReadOnly);
107            multibuffer.set_all_diff_hunks_expanded(cx);
108            multibuffer.add_diff(buffer_diff.clone(), cx);
109            multibuffer
110        });
111
112        Self::Pending(PendingDiff {
113            multibuffer,
114            base_text: Arc::from(buffer_text_snapshot.text().as_str()),
115            _subscription: cx.observe(&buffer, |this, _, cx| {
116                if let Diff::Pending(diff) = this {
117                    diff.update(cx);
118                }
119            }),
120            new_buffer: buffer,
121            diff: buffer_diff,
122            revealed_ranges: Vec::new(),
123            update_diff: Task::ready(Ok(())),
124            pending_update: None,
125            is_updating: false,
126            auto_update: false,
127        })
128    }
129
130    pub fn disable_auto_update(&mut self) {
131        if let Self::Pending(diff) = self {
132            diff.auto_update = false;
133        }
134    }
135
136    pub fn reveal_range(&mut self, range: Range<Anchor>, cx: &mut Context<Self>) {
137        if let Self::Pending(diff) = self {
138            diff.reveal_range(range, cx);
139        }
140    }
141
142    pub fn finalize(&mut self, cx: &mut Context<Self>) {
143        if let Self::Pending(diff) = self {
144            *self = Self::Finalized(diff.finalize(cx));
145        }
146    }
147
148    /// Returns the original text before any edits were applied.
149    pub fn base_text(&self) -> &Arc<str> {
150        match self {
151            Self::Pending(PendingDiff { base_text, .. }) => base_text,
152            Self::Finalized(FinalizedDiff { base_text, .. }) => base_text,
153        }
154    }
155
156    /// Returns the buffer being edited (for pending diffs) or the snapshot buffer (for finalized diffs).
157    pub fn buffer(&self) -> &Entity<Buffer> {
158        match self {
159            Self::Pending(PendingDiff { new_buffer, .. }) => new_buffer,
160            Self::Finalized(FinalizedDiff { new_buffer, .. }) => new_buffer,
161        }
162    }
163
164    pub fn file_path(&self, cx: &App) -> Option<String> {
165        match self {
166            Self::Pending(PendingDiff { new_buffer, .. }) => new_buffer
167                .read(cx)
168                .file()
169                .map(|file| file.full_path(cx).to_string_lossy().into_owned()),
170            Self::Finalized(FinalizedDiff { path, .. }) => Some(path.clone()),
171        }
172    }
173
174    pub fn multibuffer(&self) -> &Entity<MultiBuffer> {
175        match self {
176            Self::Pending(PendingDiff { multibuffer, .. }) => multibuffer,
177            Self::Finalized(FinalizedDiff { multibuffer, .. }) => multibuffer,
178        }
179    }
180
181    pub fn to_markdown(&self, cx: &App) -> String {
182        let buffer_text = self
183            .multibuffer()
184            .read(cx)
185            .all_buffers()
186            .iter()
187            .map(|buffer| buffer.read(cx).text())
188            .join("\n");
189        let path = match self {
190            Diff::Pending(PendingDiff {
191                new_buffer: buffer, ..
192            }) => buffer
193                .read(cx)
194                .file()
195                .map(|file| file.path().display(file.path_style(cx))),
196            Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_str().into()),
197        };
198        format!(
199            "Diff: {}\n```\n{}\n```\n",
200            path.unwrap_or("untitled".into()),
201            buffer_text
202        )
203    }
204
205    pub fn has_revealed_range(&self, cx: &App) -> bool {
206        self.multibuffer().read(cx).paths().next().is_some()
207    }
208
209    pub fn needs_update(&self, old_text: &str, new_text: &str, cx: &App) -> bool {
210        match self {
211            Diff::Pending(PendingDiff {
212                base_text,
213                new_buffer,
214                ..
215            }) => {
216                base_text.as_ref() != old_text
217                    || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
218            }
219            Diff::Finalized(FinalizedDiff {
220                base_text,
221                new_buffer,
222                ..
223            }) => {
224                base_text.as_ref() != old_text
225                    || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
226            }
227        }
228    }
229
230    pub fn update_pending(
231        &mut self,
232        operations: Vec<LineOperation>,
233        snapshot: text::BufferSnapshot,
234        cx: &mut Context<Diff>,
235    ) {
236        match self {
237            Diff::Pending(diff) => diff.update_manually(operations, snapshot, cx),
238            Diff::Finalized(_) => {}
239        }
240    }
241}
242
243pub struct PendingDiff {
244    multibuffer: Entity<MultiBuffer>,
245    base_text: Arc<str>,
246    new_buffer: Entity<Buffer>,
247    diff: Entity<BufferDiff>,
248    revealed_ranges: Vec<Range<Anchor>>,
249    _subscription: Subscription,
250    auto_update: bool,
251    update_diff: Task<Result<()>>,
252    // The latest update waiting to be processed. Storing only the latest means
253    // intermediate chunks are coalesced when the worker task can't keep up.
254    pending_update: Option<PendingUpdate>,
255    is_updating: bool,
256}
257
258struct PendingUpdate {
259    operations: Vec<LineOperation>,
260    base_snapshot: text::BufferSnapshot,
261    text_snapshot: text::BufferSnapshot,
262}
263
264fn compute_hunks(
265    diff_base: &text::BufferSnapshot,
266    buffer: &text::BufferSnapshot,
267    line_operations: Vec<LineOperation>,
268) -> SumTree<buffer_diff::InternalDiffHunk> {
269    let mut tree = SumTree::new(buffer);
270
271    let mut old_row = 0u32;
272    let mut new_row = 0u32;
273
274    // Merge adjacent Delete+Insert into a single Modified hunk
275    let mut pending_delete_lines: Option<u32> = None;
276
277    let flush_delete = |pending_delete_lines: &mut Option<u32>,
278                        old_row: &mut u32,
279                        new_row: u32,
280                        tree: &mut SumTree<InternalDiffHunk>,
281                        diff_base: &text::BufferSnapshot,
282                        buffer: &text::BufferSnapshot| {
283        if let Some(del_lines) = pending_delete_lines.take() {
284            let old_start =
285                diff_base.point_to_offset(Point::new(*old_row, 0).min(diff_base.max_point()));
286            let old_end = diff_base
287                .point_to_offset(Point::new(*old_row + del_lines, 0).min(diff_base.max_point()));
288            let new_pos = buffer.anchor_before(Point::new(new_row, 0).min(buffer.max_point()));
289            tree.push(
290                InternalDiffHunk {
291                    buffer_range: new_pos..new_pos,
292                    diff_base_byte_range: old_start..old_end,
293                    base_word_diffs: Vec::new(),
294                    buffer_word_diffs: Vec::new(),
295                },
296                buffer,
297            );
298            *old_row += del_lines;
299        }
300    };
301
302    for operation in line_operations {
303        match operation {
304            LineOperation::Delete { lines } => {
305                // Accumulate deletions — they might be followed by an Insert (= modification)
306                *pending_delete_lines.get_or_insert(0) += lines;
307            }
308            LineOperation::Insert { lines } => {
309                let old_start =
310                    diff_base.point_to_offset(Point::new(old_row, 0).min(diff_base.max_point()));
311                let (old_end, del_lines) = if let Some(del_lines) = pending_delete_lines.take() {
312                    // Delete followed by Insert = Modified hunk
313                    let old_end = diff_base.point_to_offset(
314                        Point::new(old_row + del_lines, 0).min(diff_base.max_point()),
315                    );
316                    (old_end, del_lines)
317                } else {
318                    // Pure insertion
319                    (old_start, 0)
320                };
321                let new_start =
322                    buffer.anchor_before(Point::new(new_row, 0).min(buffer.max_point()));
323                let new_end =
324                    buffer.anchor_before(Point::new(new_row + lines, 0).min(buffer.max_point()));
325                tree.push(
326                    InternalDiffHunk {
327                        buffer_range: new_start..new_end,
328                        diff_base_byte_range: old_start..old_end,
329                        base_word_diffs: Vec::new(),
330                        buffer_word_diffs: Vec::new(),
331                    },
332                    buffer,
333                );
334                old_row += del_lines;
335                new_row += lines;
336            }
337            LineOperation::Keep { lines } => {
338                // Flush any pending deletion before a Keep
339                flush_delete(
340                    &mut pending_delete_lines,
341                    &mut old_row,
342                    new_row,
343                    &mut tree,
344                    diff_base,
345                    buffer,
346                );
347                // Keep = unchanged, no hunk to push
348                old_row += lines;
349                new_row += lines;
350            }
351        }
352    }
353
354    // Flush any trailing deletion
355    flush_delete(
356        &mut pending_delete_lines,
357        &mut old_row,
358        new_row,
359        &mut tree,
360        diff_base,
361        buffer,
362    );
363
364    tree
365}
366
367impl PendingDiff {
368    pub fn update_manually(
369        &mut self,
370        operations: Vec<LineOperation>,
371        base_snapshot: text::BufferSnapshot,
372        cx: &mut Context<Diff>,
373    ) {
374        let text_snapshot = self.new_buffer.read(cx).text_snapshot();
375        self.pending_update = Some(PendingUpdate {
376            operations,
377            base_snapshot,
378            text_snapshot,
379        });
380        if !self.is_updating {
381            self.flush_pending_update(cx);
382        }
383    }
384
385    pub fn update(&mut self, cx: &mut Context<Diff>) {
386        if !self.auto_update {
387            return;
388        }
389
390        let buffer = self.new_buffer.clone();
391        let buffer_diff = self.diff.clone();
392        let base_text = self.base_text.clone();
393        self.update_diff = cx.spawn(async move |diff, cx| {
394            let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
395            let language = buffer.read_with(cx, |buffer, _| buffer.language().cloned());
396            let update = buffer_diff
397                .update(cx, |diff, cx| {
398                    diff.update_diff(
399                        text_snapshot.clone(),
400                        Some(base_text.clone()),
401                        None,
402                        language,
403                        cx,
404                    )
405                })
406                .await;
407            let (task1, task2) = buffer_diff.update(cx, |diff, cx| {
408                let task1 = diff.set_snapshot(update.clone(), &text_snapshot, cx);
409                let task2 = diff
410                    .secondary_diff()
411                    .unwrap()
412                    .update(cx, |diff, cx| diff.set_snapshot(update, &text_snapshot, cx));
413                (task1, task2)
414            });
415            task1.await;
416            task2.await;
417            diff.update(cx, |diff, cx| {
418                if let Diff::Pending(diff) = diff {
419                    diff.update_visible_ranges(cx);
420                }
421            })
422        });
423    }
424
425    fn flush_pending_update(&mut self, cx: &mut Context<Diff>) {
426        let Some(PendingUpdate {
427            operations,
428            base_snapshot,
429            text_snapshot,
430        }) = self.pending_update.take()
431        else {
432            self.is_updating = false;
433            return;
434        };
435        self.is_updating = true;
436
437        let buffer_diff = self.diff.clone();
438        let base_text = self.base_text.clone();
439        let language = self.new_buffer.read(cx).language().cloned();
440        self.update_diff = cx.spawn(async move |diff, cx| {
441            let snapshot = text_snapshot.clone();
442            let update = buffer_diff
443                .update(cx, |diff, cx| {
444                    diff.update_diff_impl(
445                        text_snapshot.clone(),
446                        Some(base_text.clone()),
447                        None,
448                        language,
449                        move |_d, _b, _o| compute_hunks(&base_snapshot, &text_snapshot, operations),
450                        cx,
451                    )
452                })
453                .await;
454            let (task1, task2) = buffer_diff.update(cx, |diff, cx| {
455                let task1 = diff.set_snapshot(update.clone(), &snapshot, cx);
456                let task2 = diff
457                    .secondary_diff()
458                    .unwrap()
459                    .update(cx, |diff, cx| diff.set_snapshot(update, &snapshot, cx));
460                (task1, task2)
461            });
462            task1.await;
463            task2.await;
464            diff.update(cx, |diff, cx| {
465                if let Diff::Pending(diff) = diff {
466                    diff.update_visible_ranges(cx);
467                    // Pick up any update that arrived while this task was running.
468                    diff.flush_pending_update(cx);
469                }
470            })
471        });
472    }
473
474    pub fn reveal_range(&mut self, range: Range<Anchor>, cx: &mut Context<Diff>) {
475        self.revealed_ranges.push(range);
476        self.update_visible_ranges(cx);
477    }
478
479    fn finalize(&self, cx: &mut Context<Diff>) -> FinalizedDiff {
480        let ranges = self.excerpt_ranges(cx);
481        let base_text = self.base_text.clone();
482        let new_buffer = self.new_buffer.read(cx);
483        let language_registry = new_buffer.language_registry();
484
485        let path = new_buffer
486            .file()
487            .map(|file| file.path().display(file.path_style(cx)))
488            .unwrap_or("untitled".into())
489            .into();
490        let replica_id = new_buffer.replica_id();
491
492        // Replace the buffer in the multibuffer with the snapshot
493        let buffer = cx.new(|cx| {
494            let language = self.new_buffer.read(cx).language().cloned();
495            let buffer = TextBuffer::new_normalized(
496                replica_id,
497                cx.entity_id().as_non_zero_u64().into(),
498                self.new_buffer.read(cx).line_ending(),
499                self.new_buffer.read(cx).as_rope().clone(),
500            );
501            let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
502            buffer.set_language(language, cx);
503            buffer
504        });
505
506        let buffer_diff = cx.spawn({
507            let buffer = buffer.clone();
508            async move |_this, cx| {
509                buffer.update(cx, |buffer, _| buffer.parsing_idle()).await;
510                build_buffer_diff(base_text, &buffer, language_registry, cx).await
511            }
512        });
513
514        let update_diff = cx.spawn(async move |this, cx| {
515            let buffer_diff = buffer_diff.await?;
516            this.update(cx, |this, cx| {
517                this.multibuffer().update(cx, |multibuffer, cx| {
518                    let path_key = PathKey::for_buffer(&buffer, cx);
519                    multibuffer.clear(cx);
520                    multibuffer.set_excerpts_for_path(
521                        path_key,
522                        buffer,
523                        ranges,
524                        excerpt_context_lines(cx),
525                        cx,
526                    );
527                    multibuffer.add_diff(buffer_diff.clone(), cx);
528                });
529
530                cx.notify();
531            })
532        });
533
534        FinalizedDiff {
535            path,
536            base_text: self.base_text.clone(),
537            multibuffer: self.multibuffer.clone(),
538            new_buffer: self.new_buffer.clone(),
539            _update_diff: update_diff,
540        }
541    }
542
543    fn update_visible_ranges(&mut self, cx: &mut Context<Diff>) {
544        let ranges = self.excerpt_ranges(cx);
545        self.multibuffer.update(cx, |multibuffer, cx| {
546            multibuffer.set_excerpts_for_path(
547                PathKey::for_buffer(&self.new_buffer, cx),
548                self.new_buffer.clone(),
549                ranges,
550                excerpt_context_lines(cx),
551                cx,
552            );
553            let end = multibuffer.len(cx);
554            Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1)
555        });
556        cx.notify();
557    }
558
559    fn excerpt_ranges(&self, cx: &App) -> Vec<Range<Point>> {
560        let buffer = self.new_buffer.read(cx);
561        let mut ranges = self
562            .diff
563            .read(cx)
564            .snapshot(cx)
565            .hunks_intersecting_range(
566                Anchor::min_for_buffer(buffer.remote_id())
567                    ..Anchor::max_for_buffer(buffer.remote_id()),
568                buffer,
569            )
570            .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
571            .collect::<Vec<_>>();
572        ranges.extend(
573            self.revealed_ranges
574                .iter()
575                .map(|range| range.to_point(buffer)),
576        );
577        ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end)));
578
579        // Merge adjacent ranges
580        let mut ranges = ranges.into_iter().peekable();
581        let mut merged_ranges = Vec::new();
582        while let Some(mut range) = ranges.next() {
583            while let Some(next_range) = ranges.peek() {
584                if range.end >= next_range.start {
585                    range.end = range.end.max(next_range.end);
586                    ranges.next();
587                } else {
588                    break;
589                }
590            }
591
592            merged_ranges.push(range);
593        }
594        merged_ranges
595    }
596}
597
598pub struct FinalizedDiff {
599    path: String,
600    base_text: Arc<str>,
601    new_buffer: Entity<Buffer>,
602    multibuffer: Entity<MultiBuffer>,
603    _update_diff: Task<Result<()>>,
604}
605
606async fn build_buffer_diff(
607    old_text: Arc<str>,
608    buffer: &Entity<Buffer>,
609    language_registry: Option<Arc<LanguageRegistry>>,
610    cx: &mut AsyncApp,
611) -> Result<Entity<BufferDiff>> {
612    let language = cx.update(|cx| buffer.read(cx).language().cloned());
613    let text_snapshot = cx.update(|cx| buffer.read(cx).text_snapshot());
614    let buffer = cx.update(|cx| buffer.read(cx).snapshot());
615
616    let secondary_diff = cx.new(|cx| BufferDiff::new(&buffer, cx));
617
618    let update = secondary_diff
619        .update(cx, |secondary_diff, cx| {
620            secondary_diff.update_diff(
621                text_snapshot.clone(),
622                Some(old_text),
623                Some(false),
624                language.clone(),
625                cx,
626            )
627        })
628        .await;
629
630    secondary_diff
631        .update(cx, |secondary_diff, cx| {
632            secondary_diff.set_snapshot(update.clone(), &buffer, cx)
633        })
634        .await;
635
636    let diff = cx.new(|cx| BufferDiff::new(&buffer, cx));
637    diff.update(cx, |diff, cx| {
638        diff.language_changed(language, language_registry, cx);
639        diff.set_secondary_diff(secondary_diff);
640        diff.set_snapshot(update.clone(), &buffer, cx)
641    })
642    .await;
643    Ok(diff)
644}
645
646#[cfg(test)]
647mod tests {
648    use gpui::{AppContext as _, TestAppContext};
649    use language::Buffer;
650
651    use crate::Diff;
652
653    #[gpui::test]
654    async fn test_pending_diff(cx: &mut TestAppContext) {
655        let buffer = cx.new(|cx| Buffer::local("hello!", cx));
656        let _diff = cx.new(|cx| Diff::new(buffer.clone(), cx));
657        buffer.update(cx, |buffer, cx| {
658            buffer.set_text("HELLO!", cx);
659        });
660        cx.run_until_parked();
661    }
662}