action_log.rs

  1use anyhow::{Context as _, Result};
  2use buffer_diff::BufferDiff;
  3use collections::{BTreeMap, HashMap, HashSet};
  4use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
  5use language::{
  6    Buffer, BufferEvent, DiskState, OffsetRangeExt, Operation, TextBufferSnapshot, ToOffset,
  7};
  8use std::{ops::Range, sync::Arc};
  9
 10/// Tracks actions performed by tools in a thread
 11pub struct ActionLog {
 12    /// Buffers that user manually added to the context, and whose content has
 13    /// changed since the model last saw them.
 14    stale_buffers_in_context: HashSet<Entity<Buffer>>,
 15    /// Buffers that we want to notify the model about when they change.
 16    tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
 17    /// Has the model edited a file since it last checked diagnostics?
 18    edited_since_project_diagnostics_check: bool,
 19}
 20
 21impl ActionLog {
 22    /// Creates a new, empty action log.
 23    pub fn new() -> Self {
 24        Self {
 25            stale_buffers_in_context: HashSet::default(),
 26            tracked_buffers: BTreeMap::default(),
 27            edited_since_project_diagnostics_check: false,
 28        }
 29    }
 30
 31    pub fn clear_reviewed_changes(&mut self, cx: &mut Context<Self>) {
 32        self.tracked_buffers
 33            .retain(|_buffer, tracked_buffer| match &mut tracked_buffer.change {
 34                Change::Edited {
 35                    accepted_edit_ids, ..
 36                } => {
 37                    accepted_edit_ids.clear();
 38                    tracked_buffer.schedule_diff_update();
 39                    true
 40                }
 41                Change::Deleted { reviewed, .. } => !*reviewed,
 42            });
 43        cx.notify();
 44    }
 45
 46    /// Notifies a diagnostics check
 47    pub fn checked_project_diagnostics(&mut self) {
 48        self.edited_since_project_diagnostics_check = false;
 49    }
 50
 51    /// Returns true if any files have been edited since the last project diagnostics check
 52    pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
 53        self.edited_since_project_diagnostics_check
 54    }
 55
 56    fn track_buffer(
 57        &mut self,
 58        buffer: Entity<Buffer>,
 59        created: bool,
 60        cx: &mut Context<Self>,
 61    ) -> &mut TrackedBuffer {
 62        let tracked_buffer = self
 63            .tracked_buffers
 64            .entry(buffer.clone())
 65            .or_insert_with(|| {
 66                let text_snapshot = buffer.read(cx).text_snapshot();
 67                let unreviewed_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
 68                let diff = cx.new(|cx| {
 69                    let mut diff = BufferDiff::new(&text_snapshot, cx);
 70                    diff.set_secondary_diff(unreviewed_diff.clone());
 71                    diff
 72                });
 73                let (diff_update_tx, diff_update_rx) = async_watch::channel(());
 74                TrackedBuffer {
 75                    buffer: buffer.clone(),
 76                    change: Change::Edited {
 77                        unreviewed_edit_ids: HashSet::default(),
 78                        accepted_edit_ids: HashSet::default(),
 79                        initial_content: if created {
 80                            None
 81                        } else {
 82                            Some(text_snapshot.clone())
 83                        },
 84                    },
 85                    version: buffer.read(cx).version(),
 86                    diff,
 87                    secondary_diff: unreviewed_diff,
 88                    diff_update: diff_update_tx,
 89                    _maintain_diff: cx.spawn({
 90                        let buffer = buffer.clone();
 91                        async move |this, cx| {
 92                            Self::maintain_diff(this, buffer, diff_update_rx, cx)
 93                                .await
 94                                .ok();
 95                        }
 96                    }),
 97                    _subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
 98                }
 99            });
100        tracked_buffer.version = buffer.read(cx).version();
101        tracked_buffer
102    }
103
104    fn handle_buffer_event(
105        &mut self,
106        buffer: Entity<Buffer>,
107        event: &BufferEvent,
108        cx: &mut Context<Self>,
109    ) {
110        match event {
111            BufferEvent::Operation { operation, .. } => {
112                self.handle_buffer_operation(buffer, operation, cx)
113            }
114            BufferEvent::FileHandleChanged => {
115                self.handle_buffer_file_changed(buffer, cx);
116            }
117            _ => {}
118        };
119    }
120
121    fn handle_buffer_operation(
122        &mut self,
123        buffer: Entity<Buffer>,
124        operation: &Operation,
125        cx: &mut Context<Self>,
126    ) {
127        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
128            return;
129        };
130        let Operation::Buffer(text::Operation::Edit(operation)) = operation else {
131            return;
132        };
133        let Change::Edited {
134            unreviewed_edit_ids,
135            accepted_edit_ids,
136            ..
137        } = &mut tracked_buffer.change
138        else {
139            return;
140        };
141
142        if unreviewed_edit_ids.contains(&operation.timestamp)
143            || accepted_edit_ids.contains(&operation.timestamp)
144        {
145            return;
146        }
147
148        let buffer = buffer.read(cx);
149        let operation_edit_ranges = buffer
150            .edited_ranges_for_edit_ids::<usize>([&operation.timestamp])
151            .collect::<Vec<_>>();
152        let intersects_unreviewed_edits = ranges_intersect(
153            operation_edit_ranges.iter().cloned(),
154            buffer.edited_ranges_for_edit_ids::<usize>(unreviewed_edit_ids.iter()),
155        );
156        let mut intersected_accepted_edits = HashSet::default();
157        for accepted_edit_id in accepted_edit_ids.iter() {
158            let intersects_accepted_edit = ranges_intersect(
159                operation_edit_ranges.iter().cloned(),
160                buffer.edited_ranges_for_edit_ids::<usize>([accepted_edit_id]),
161            );
162            if intersects_accepted_edit {
163                intersected_accepted_edits.insert(*accepted_edit_id);
164            }
165        }
166
167        // If the buffer operation overlaps with any tracked edits, mark it as unreviewed.
168        // If it intersects an already-accepted id, mark that edit as unreviewed again.
169        if intersects_unreviewed_edits || !intersected_accepted_edits.is_empty() {
170            unreviewed_edit_ids.insert(operation.timestamp);
171            for accepted_edit_id in intersected_accepted_edits {
172                unreviewed_edit_ids.insert(accepted_edit_id);
173                accepted_edit_ids.remove(&accepted_edit_id);
174            }
175            tracked_buffer.schedule_diff_update();
176        }
177    }
178
179    fn handle_buffer_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
180        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
181            return;
182        };
183
184        match tracked_buffer.change {
185            Change::Deleted { .. } => {
186                if buffer
187                    .read(cx)
188                    .file()
189                    .map_or(false, |file| file.disk_state() != DiskState::Deleted)
190                {
191                    // If the buffer had been deleted by a tool, but it got
192                    // resurrected externally, we want to clear the changes we
193                    // were tracking and reset the buffer's state.
194                    tracked_buffer.change = Change::Edited {
195                        unreviewed_edit_ids: HashSet::default(),
196                        accepted_edit_ids: HashSet::default(),
197                        initial_content: Some(buffer.read(cx).text_snapshot()),
198                    };
199                }
200                tracked_buffer.schedule_diff_update();
201            }
202            Change::Edited { .. } => {
203                if buffer
204                    .read(cx)
205                    .file()
206                    .map_or(false, |file| file.disk_state() == DiskState::Deleted)
207                {
208                    // If the buffer had been edited by a tool, but it got
209                    // deleted externally, we want to stop tracking it.
210                    self.tracked_buffers.remove(&buffer);
211                } else {
212                    tracked_buffer.schedule_diff_update();
213                }
214            }
215        }
216    }
217
218    async fn maintain_diff(
219        this: WeakEntity<Self>,
220        buffer: Entity<Buffer>,
221        mut diff_update: async_watch::Receiver<()>,
222        cx: &mut AsyncApp,
223    ) -> Result<()> {
224        while let Some(_) = diff_update.recv().await.ok() {
225            let update = this.update(cx, |this, cx| {
226                let tracked_buffer = this
227                    .tracked_buffers
228                    .get_mut(&buffer)
229                    .context("buffer not tracked")?;
230                anyhow::Ok(tracked_buffer.update_diff(cx))
231            })??;
232            update.await;
233            this.update(cx, |_this, cx| cx.notify())?;
234        }
235
236        Ok(())
237    }
238
239    /// Track a buffer as read, so we can notify the model about user edits.
240    pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
241        self.track_buffer(buffer, false, cx);
242    }
243
244    /// Track a buffer as read, so we can notify the model about user edits.
245    pub fn will_create_buffer(
246        &mut self,
247        buffer: Entity<Buffer>,
248        edit_id: Option<clock::Lamport>,
249        cx: &mut Context<Self>,
250    ) {
251        self.track_buffer(buffer.clone(), true, cx);
252        self.buffer_edited(buffer, edit_id.into_iter().collect(), cx)
253    }
254
255    /// Mark a buffer as edited, so we can refresh it in the context
256    pub fn buffer_edited(
257        &mut self,
258        buffer: Entity<Buffer>,
259        mut edit_ids: Vec<clock::Lamport>,
260        cx: &mut Context<Self>,
261    ) {
262        self.edited_since_project_diagnostics_check = true;
263        self.stale_buffers_in_context.insert(buffer.clone());
264
265        let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
266
267        match &mut tracked_buffer.change {
268            Change::Edited {
269                unreviewed_edit_ids,
270                ..
271            } => {
272                unreviewed_edit_ids.extend(edit_ids.iter().copied());
273            }
274            Change::Deleted {
275                deleted_content,
276                deletion_id,
277                ..
278            } => {
279                edit_ids.extend(*deletion_id);
280                tracked_buffer.change = Change::Edited {
281                    unreviewed_edit_ids: edit_ids.into_iter().collect(),
282                    accepted_edit_ids: HashSet::default(),
283                    initial_content: Some(deleted_content.clone()),
284                };
285            }
286        }
287
288        tracked_buffer.schedule_diff_update();
289    }
290
291    pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
292        let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
293        if let Change::Edited {
294            initial_content, ..
295        } = &tracked_buffer.change
296        {
297            if let Some(initial_content) = initial_content {
298                let deletion_id = buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
299                tracked_buffer.change = Change::Deleted {
300                    reviewed: false,
301                    deleted_content: initial_content.clone(),
302                    deletion_id,
303                };
304                tracked_buffer.schedule_diff_update();
305            } else {
306                self.tracked_buffers.remove(&buffer);
307                cx.notify();
308            }
309        }
310    }
311
312    /// Accepts edits in a given range within a buffer.
313    pub fn review_edits_in_range<T: ToOffset>(
314        &mut self,
315        buffer: Entity<Buffer>,
316        buffer_range: Range<T>,
317        accept: bool,
318        cx: &mut Context<Self>,
319    ) {
320        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
321            return;
322        };
323
324        let buffer = buffer.read(cx);
325        let buffer_range = buffer_range.to_offset(buffer);
326
327        match &mut tracked_buffer.change {
328            Change::Deleted { reviewed, .. } => {
329                *reviewed = accept;
330            }
331            Change::Edited {
332                unreviewed_edit_ids,
333                accepted_edit_ids,
334                ..
335            } => {
336                let (source, destination) = if accept {
337                    (unreviewed_edit_ids, accepted_edit_ids)
338                } else {
339                    (accepted_edit_ids, unreviewed_edit_ids)
340                };
341                source.retain(|edit_id| {
342                    for range in buffer.edited_ranges_for_edit_ids::<usize>([edit_id]) {
343                        if buffer_range.end >= range.start && buffer_range.start <= range.end {
344                            destination.insert(*edit_id);
345                            return false;
346                        }
347                    }
348                    true
349                });
350            }
351        }
352
353        tracked_buffer.schedule_diff_update();
354    }
355
356    /// Keep all edits across all buffers.
357    /// This is a more performant alternative to calling review_edits_in_range for each buffer.
358    pub fn keep_all_edits(&mut self) {
359        // Process all tracked buffers
360        for (_, tracked_buffer) in self.tracked_buffers.iter_mut() {
361            match &mut tracked_buffer.change {
362                Change::Deleted { reviewed, .. } => {
363                    *reviewed = true;
364                }
365                Change::Edited {
366                    unreviewed_edit_ids,
367                    accepted_edit_ids,
368                    ..
369                } => {
370                    accepted_edit_ids.extend(unreviewed_edit_ids.drain());
371                }
372            }
373
374            tracked_buffer.schedule_diff_update();
375        }
376    }
377
378    /// Returns the set of buffers that contain changes that haven't been reviewed by the user.
379    pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, ChangedBuffer> {
380        self.tracked_buffers
381            .iter()
382            .filter(|(_, tracked)| tracked.has_changes(cx))
383            .map(|(buffer, tracked)| {
384                (
385                    buffer.clone(),
386                    ChangedBuffer {
387                        diff: tracked.diff.clone(),
388                        needs_review: match &tracked.change {
389                            Change::Edited {
390                                unreviewed_edit_ids,
391                                ..
392                            } => !unreviewed_edit_ids.is_empty(),
393                            Change::Deleted { reviewed, .. } => !reviewed,
394                        },
395                    },
396                )
397            })
398            .collect()
399    }
400
401    /// Iterate over buffers changed since last read or edited by the model
402    pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
403        self.tracked_buffers
404            .iter()
405            .filter(|(buffer, tracked)| {
406                let buffer = buffer.read(cx);
407
408                tracked.version != buffer.version
409                    && buffer
410                        .file()
411                        .map_or(false, |file| file.disk_state() != DiskState::Deleted)
412            })
413            .map(|(buffer, _)| buffer)
414    }
415
416    /// Takes and returns the set of buffers pending refresh, clearing internal state.
417    pub fn take_stale_buffers_in_context(&mut self) -> HashSet<Entity<Buffer>> {
418        std::mem::take(&mut self.stale_buffers_in_context)
419    }
420}
421
422fn ranges_intersect(
423    ranges_a: impl IntoIterator<Item = Range<usize>>,
424    ranges_b: impl IntoIterator<Item = Range<usize>>,
425) -> bool {
426    let mut ranges_a_iter = ranges_a.into_iter().peekable();
427    let mut ranges_b_iter = ranges_b.into_iter().peekable();
428    while let (Some(range_a), Some(range_b)) = (ranges_a_iter.peek(), ranges_b_iter.peek()) {
429        if range_a.end < range_b.start {
430            ranges_a_iter.next();
431        } else if range_b.end < range_a.start {
432            ranges_b_iter.next();
433        } else {
434            return true;
435        }
436    }
437    false
438}
439
440struct TrackedBuffer {
441    buffer: Entity<Buffer>,
442    change: Change,
443    version: clock::Global,
444    diff: Entity<BufferDiff>,
445    secondary_diff: Entity<BufferDiff>,
446    diff_update: async_watch::Sender<()>,
447    _maintain_diff: Task<()>,
448    _subscription: Subscription,
449}
450
451enum Change {
452    Edited {
453        unreviewed_edit_ids: HashSet<clock::Lamport>,
454        accepted_edit_ids: HashSet<clock::Lamport>,
455        initial_content: Option<TextBufferSnapshot>,
456    },
457    Deleted {
458        reviewed: bool,
459        deleted_content: TextBufferSnapshot,
460        deletion_id: Option<clock::Lamport>,
461    },
462}
463
464impl TrackedBuffer {
465    fn has_changes(&self, cx: &App) -> bool {
466        self.diff
467            .read(cx)
468            .hunks(&self.buffer.read(cx), cx)
469            .next()
470            .is_some()
471    }
472
473    fn schedule_diff_update(&self) {
474        self.diff_update.send(()).ok();
475    }
476
477    fn update_diff(&mut self, cx: &mut App) -> Task<()> {
478        match &self.change {
479            Change::Edited {
480                unreviewed_edit_ids,
481                accepted_edit_ids,
482                ..
483            } => {
484                let edits_to_undo = unreviewed_edit_ids
485                    .iter()
486                    .chain(accepted_edit_ids)
487                    .map(|edit_id| (*edit_id, u32::MAX))
488                    .collect::<HashMap<_, _>>();
489                let buffer_without_edits = self.buffer.update(cx, |buffer, cx| buffer.branch(cx));
490                buffer_without_edits
491                    .update(cx, |buffer, cx| buffer.undo_operations(edits_to_undo, cx));
492                let primary_diff_update = self.diff.update(cx, |diff, cx| {
493                    diff.set_base_text_buffer(
494                        buffer_without_edits,
495                        self.buffer.read(cx).text_snapshot(),
496                        cx,
497                    )
498                });
499
500                let unreviewed_edits_to_undo = unreviewed_edit_ids
501                    .iter()
502                    .map(|edit_id| (*edit_id, u32::MAX))
503                    .collect::<HashMap<_, _>>();
504                let buffer_without_unreviewed_edits =
505                    self.buffer.update(cx, |buffer, cx| buffer.branch(cx));
506                buffer_without_unreviewed_edits.update(cx, |buffer, cx| {
507                    buffer.undo_operations(unreviewed_edits_to_undo, cx)
508                });
509                let secondary_diff_update = self.secondary_diff.update(cx, |diff, cx| {
510                    diff.set_base_text_buffer(
511                        buffer_without_unreviewed_edits.clone(),
512                        self.buffer.read(cx).text_snapshot(),
513                        cx,
514                    )
515                });
516
517                cx.background_spawn(async move {
518                    _ = primary_diff_update.await;
519                    _ = secondary_diff_update.await;
520                })
521            }
522            Change::Deleted {
523                reviewed,
524                deleted_content,
525                ..
526            } => {
527                let reviewed = *reviewed;
528                let deleted_content = deleted_content.clone();
529
530                let primary_diff = self.diff.clone();
531                let secondary_diff = self.secondary_diff.clone();
532                let buffer_snapshot = self.buffer.read(cx).text_snapshot();
533                let language = self.buffer.read(cx).language().cloned();
534                let language_registry = self.buffer.read(cx).language_registry().clone();
535
536                cx.spawn(async move |cx| {
537                    let base_text = Arc::new(deleted_content.text());
538
539                    let primary_diff_snapshot = BufferDiff::update_diff(
540                        primary_diff.clone(),
541                        buffer_snapshot.clone(),
542                        Some(base_text.clone()),
543                        true,
544                        false,
545                        language.clone(),
546                        language_registry.clone(),
547                        cx,
548                    )
549                    .await;
550                    let secondary_diff_snapshot = BufferDiff::update_diff(
551                        secondary_diff.clone(),
552                        buffer_snapshot.clone(),
553                        if reviewed {
554                            None
555                        } else {
556                            Some(base_text.clone())
557                        },
558                        true,
559                        false,
560                        language.clone(),
561                        language_registry.clone(),
562                        cx,
563                    )
564                    .await;
565
566                    if let Ok(primary_diff_snapshot) = primary_diff_snapshot {
567                        primary_diff
568                            .update(cx, |diff, cx| {
569                                diff.set_snapshot(primary_diff_snapshot, &buffer_snapshot, None, cx)
570                            })
571                            .ok();
572                    }
573
574                    if let Ok(secondary_diff_snapshot) = secondary_diff_snapshot {
575                        secondary_diff
576                            .update(cx, |diff, cx| {
577                                diff.set_snapshot(
578                                    secondary_diff_snapshot,
579                                    &buffer_snapshot,
580                                    None,
581                                    cx,
582                                )
583                            })
584                            .ok();
585                    }
586                })
587            }
588        }
589    }
590}
591
592pub struct ChangedBuffer {
593    pub diff: Entity<BufferDiff>,
594    pub needs_review: bool,
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600    use buffer_diff::DiffHunkStatusKind;
601    use gpui::TestAppContext;
602    use language::Point;
603    use project::{FakeFs, Fs, Project, RemoveOptions};
604    use serde_json::json;
605    use settings::SettingsStore;
606    use util::path;
607
608    #[gpui::test(iterations = 10)]
609    async fn test_edit_review(cx: &mut TestAppContext) {
610        let action_log = cx.new(|_| ActionLog::new());
611        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
612
613        let edit1 = buffer.update(cx, |buffer, cx| {
614            buffer
615                .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
616                .unwrap()
617        });
618        let edit2 = buffer.update(cx, |buffer, cx| {
619            buffer
620                .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
621                .unwrap()
622        });
623        assert_eq!(
624            buffer.read_with(cx, |buffer, _| buffer.text()),
625            "abc\ndEf\nghi\njkl\nmnO"
626        );
627
628        action_log.update(cx, |log, cx| {
629            log.buffer_edited(buffer.clone(), vec![edit1, edit2], cx)
630        });
631        cx.run_until_parked();
632        assert_eq!(
633            unreviewed_hunks(&action_log, cx),
634            vec![(
635                buffer.clone(),
636                vec![
637                    HunkStatus {
638                        range: Point::new(1, 0)..Point::new(2, 0),
639                        review_status: ReviewStatus::Unreviewed,
640                        diff_status: DiffHunkStatusKind::Modified,
641                        old_text: "def\n".into(),
642                    },
643                    HunkStatus {
644                        range: Point::new(4, 0)..Point::new(4, 3),
645                        review_status: ReviewStatus::Unreviewed,
646                        diff_status: DiffHunkStatusKind::Modified,
647                        old_text: "mno".into(),
648                    }
649                ],
650            )]
651        );
652
653        action_log.update(cx, |log, cx| {
654            log.review_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), true, cx)
655        });
656        cx.run_until_parked();
657        assert_eq!(
658            unreviewed_hunks(&action_log, cx),
659            vec![(
660                buffer.clone(),
661                vec![
662                    HunkStatus {
663                        range: Point::new(1, 0)..Point::new(2, 0),
664                        review_status: ReviewStatus::Unreviewed,
665                        diff_status: DiffHunkStatusKind::Modified,
666                        old_text: "def\n".into(),
667                    },
668                    HunkStatus {
669                        range: Point::new(4, 0)..Point::new(4, 3),
670                        review_status: ReviewStatus::Reviewed,
671                        diff_status: DiffHunkStatusKind::Modified,
672                        old_text: "mno".into(),
673                    }
674                ],
675            )]
676        );
677
678        action_log.update(cx, |log, cx| {
679            log.review_edits_in_range(
680                buffer.clone(),
681                Point::new(3, 0)..Point::new(4, 3),
682                false,
683                cx,
684            )
685        });
686        cx.run_until_parked();
687        assert_eq!(
688            unreviewed_hunks(&action_log, cx),
689            vec![(
690                buffer.clone(),
691                vec![
692                    HunkStatus {
693                        range: Point::new(1, 0)..Point::new(2, 0),
694                        review_status: ReviewStatus::Unreviewed,
695                        diff_status: DiffHunkStatusKind::Modified,
696                        old_text: "def\n".into(),
697                    },
698                    HunkStatus {
699                        range: Point::new(4, 0)..Point::new(4, 3),
700                        review_status: ReviewStatus::Unreviewed,
701                        diff_status: DiffHunkStatusKind::Modified,
702                        old_text: "mno".into(),
703                    }
704                ],
705            )]
706        );
707
708        action_log.update(cx, |log, cx| {
709            log.review_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), true, cx)
710        });
711        cx.run_until_parked();
712        assert_eq!(
713            unreviewed_hunks(&action_log, cx),
714            vec![(
715                buffer.clone(),
716                vec![
717                    HunkStatus {
718                        range: Point::new(1, 0)..Point::new(2, 0),
719                        review_status: ReviewStatus::Reviewed,
720                        diff_status: DiffHunkStatusKind::Modified,
721                        old_text: "def\n".into(),
722                    },
723                    HunkStatus {
724                        range: Point::new(4, 0)..Point::new(4, 3),
725                        review_status: ReviewStatus::Reviewed,
726                        diff_status: DiffHunkStatusKind::Modified,
727                        old_text: "mno".into(),
728                    }
729                ],
730            )]
731        );
732    }
733
734    #[gpui::test(iterations = 10)]
735    async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
736        let action_log = cx.new(|_| ActionLog::new());
737        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
738
739        let tool_edit = buffer.update(cx, |buffer, cx| {
740            buffer
741                .edit(
742                    [(Point::new(0, 2)..Point::new(2, 3), "C\nDEF\nGHI")],
743                    None,
744                    cx,
745                )
746                .unwrap()
747        });
748        assert_eq!(
749            buffer.read_with(cx, |buffer, _| buffer.text()),
750            "abC\nDEF\nGHI\njkl\nmno"
751        );
752
753        action_log.update(cx, |log, cx| {
754            log.buffer_edited(buffer.clone(), vec![tool_edit], cx)
755        });
756        cx.run_until_parked();
757        assert_eq!(
758            unreviewed_hunks(&action_log, cx),
759            vec![(
760                buffer.clone(),
761                vec![HunkStatus {
762                    range: Point::new(0, 0)..Point::new(3, 0),
763                    review_status: ReviewStatus::Unreviewed,
764                    diff_status: DiffHunkStatusKind::Modified,
765                    old_text: "abc\ndef\nghi\n".into(),
766                }],
767            )]
768        );
769
770        action_log.update(cx, |log, cx| {
771            log.review_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), true, cx)
772        });
773        cx.run_until_parked();
774        assert_eq!(
775            unreviewed_hunks(&action_log, cx),
776            vec![(
777                buffer.clone(),
778                vec![HunkStatus {
779                    range: Point::new(0, 0)..Point::new(3, 0),
780                    review_status: ReviewStatus::Reviewed,
781                    diff_status: DiffHunkStatusKind::Modified,
782                    old_text: "abc\ndef\nghi\n".into(),
783                }],
784            )]
785        );
786
787        buffer.update(cx, |buffer, cx| {
788            buffer.edit([(Point::new(0, 2)..Point::new(0, 2), "X")], None, cx)
789        });
790        cx.run_until_parked();
791        assert_eq!(
792            unreviewed_hunks(&action_log, cx),
793            vec![(
794                buffer.clone(),
795                vec![HunkStatus {
796                    range: Point::new(0, 0)..Point::new(3, 0),
797                    review_status: ReviewStatus::Unreviewed,
798                    diff_status: DiffHunkStatusKind::Modified,
799                    old_text: "abc\ndef\nghi\n".into(),
800                }],
801            )]
802        );
803
804        action_log.update(cx, |log, cx| log.clear_reviewed_changes(cx));
805        cx.run_until_parked();
806        assert_eq!(
807            unreviewed_hunks(&action_log, cx),
808            vec![(
809                buffer.clone(),
810                vec![HunkStatus {
811                    range: Point::new(0, 0)..Point::new(3, 0),
812                    review_status: ReviewStatus::Unreviewed,
813                    diff_status: DiffHunkStatusKind::Modified,
814                    old_text: "abc\ndef\nghi\n".into(),
815                }],
816            )]
817        );
818    }
819
820    #[gpui::test(iterations = 10)]
821    async fn test_deletion(cx: &mut TestAppContext) {
822        cx.update(|cx| {
823            let settings_store = SettingsStore::test(cx);
824            cx.set_global(settings_store);
825            language::init(cx);
826            Project::init_settings(cx);
827        });
828
829        let fs = FakeFs::new(cx.executor());
830        fs.insert_tree(
831            path!("/dir"),
832            json!({"file1": "lorem\n", "file2": "ipsum\n"}),
833        )
834        .await;
835
836        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
837        let file1_path = project
838            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
839            .unwrap();
840        let file2_path = project
841            .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
842            .unwrap();
843
844        let action_log = cx.new(|_| ActionLog::new());
845        let buffer1 = project
846            .update(cx, |project, cx| {
847                project.open_buffer(file1_path.clone(), cx)
848            })
849            .await
850            .unwrap();
851        let buffer2 = project
852            .update(cx, |project, cx| {
853                project.open_buffer(file2_path.clone(), cx)
854            })
855            .await
856            .unwrap();
857
858        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
859        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
860        project
861            .update(cx, |project, cx| {
862                project.delete_file(file1_path.clone(), false, cx)
863            })
864            .unwrap()
865            .await
866            .unwrap();
867        project
868            .update(cx, |project, cx| {
869                project.delete_file(file2_path.clone(), false, cx)
870            })
871            .unwrap()
872            .await
873            .unwrap();
874        cx.run_until_parked();
875        assert_eq!(
876            unreviewed_hunks(&action_log, cx),
877            vec![
878                (
879                    buffer1.clone(),
880                    vec![HunkStatus {
881                        range: Point::new(0, 0)..Point::new(0, 0),
882                        review_status: ReviewStatus::Unreviewed,
883                        diff_status: DiffHunkStatusKind::Deleted,
884                        old_text: "lorem\n".into(),
885                    }]
886                ),
887                (
888                    buffer2.clone(),
889                    vec![HunkStatus {
890                        range: Point::new(0, 0)..Point::new(0, 0),
891                        review_status: ReviewStatus::Unreviewed,
892                        diff_status: DiffHunkStatusKind::Deleted,
893                        old_text: "ipsum\n".into(),
894                    }],
895                )
896            ]
897        );
898
899        // Simulate file1 being recreated externally.
900        fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
901            .await;
902        let buffer2 = project
903            .update(cx, |project, cx| project.open_buffer(file2_path, cx))
904            .await
905            .unwrap();
906        cx.run_until_parked();
907        // Simulate file2 being recreated by a tool.
908        let edit_id = buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
909        action_log.update(cx, |log, cx| {
910            log.will_create_buffer(buffer2.clone(), edit_id, cx)
911        });
912        project
913            .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
914            .await
915            .unwrap();
916        cx.run_until_parked();
917        assert_eq!(
918            unreviewed_hunks(&action_log, cx),
919            vec![(
920                buffer2.clone(),
921                vec![HunkStatus {
922                    range: Point::new(0, 0)..Point::new(0, 5),
923                    review_status: ReviewStatus::Unreviewed,
924                    diff_status: DiffHunkStatusKind::Modified,
925                    old_text: "ipsum\n".into(),
926                }],
927            )]
928        );
929
930        // Simulate file2 being deleted externally.
931        fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
932            .await
933            .unwrap();
934        cx.run_until_parked();
935        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
936    }
937
938    #[derive(Debug, Clone, PartialEq, Eq)]
939    struct HunkStatus {
940        range: Range<Point>,
941        review_status: ReviewStatus,
942        diff_status: DiffHunkStatusKind,
943        old_text: String,
944    }
945
946    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
947    enum ReviewStatus {
948        Unreviewed,
949        Reviewed,
950    }
951
952    fn unreviewed_hunks(
953        action_log: &Entity<ActionLog>,
954        cx: &TestAppContext,
955    ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
956        cx.read(|cx| {
957            action_log
958                .read(cx)
959                .changed_buffers(cx)
960                .into_iter()
961                .map(|(buffer, tracked_buffer)| {
962                    let snapshot = buffer.read(cx).snapshot();
963                    (
964                        buffer,
965                        tracked_buffer
966                            .diff
967                            .read(cx)
968                            .hunks(&snapshot, cx)
969                            .map(|hunk| HunkStatus {
970                                review_status: if hunk.status().has_secondary_hunk() {
971                                    ReviewStatus::Unreviewed
972                                } else {
973                                    ReviewStatus::Reviewed
974                                },
975                                diff_status: hunk.status().kind,
976                                range: hunk.range,
977                                old_text: tracked_buffer
978                                    .diff
979                                    .read(cx)
980                                    .base_text()
981                                    .text_for_range(hunk.diff_base_byte_range)
982                                    .collect(),
983                            })
984                            .collect(),
985                    )
986                })
987                .collect()
988        })
989    }
990}