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    /// Returns the set of buffers that contain changes that haven't been reviewed by the user.
357    pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, ChangedBuffer> {
358        self.tracked_buffers
359            .iter()
360            .filter(|(_, tracked)| tracked.has_changes(cx))
361            .map(|(buffer, tracked)| {
362                (
363                    buffer.clone(),
364                    ChangedBuffer {
365                        diff: tracked.diff.clone(),
366                        needs_review: match &tracked.change {
367                            Change::Edited {
368                                unreviewed_edit_ids,
369                                ..
370                            } => !unreviewed_edit_ids.is_empty(),
371                            Change::Deleted { reviewed, .. } => !reviewed,
372                        },
373                    },
374                )
375            })
376            .collect()
377    }
378
379    /// Iterate over buffers changed since last read or edited by the model
380    pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
381        self.tracked_buffers
382            .iter()
383            .filter(|(buffer, tracked)| tracked.version != buffer.read(cx).version)
384            .map(|(buffer, _)| buffer)
385    }
386
387    /// Takes and returns the set of buffers pending refresh, clearing internal state.
388    pub fn take_stale_buffers_in_context(&mut self) -> HashSet<Entity<Buffer>> {
389        std::mem::take(&mut self.stale_buffers_in_context)
390    }
391}
392
393fn ranges_intersect(
394    ranges_a: impl IntoIterator<Item = Range<usize>>,
395    ranges_b: impl IntoIterator<Item = Range<usize>>,
396) -> bool {
397    let mut ranges_a_iter = ranges_a.into_iter().peekable();
398    let mut ranges_b_iter = ranges_b.into_iter().peekable();
399    while let (Some(range_a), Some(range_b)) = (ranges_a_iter.peek(), ranges_b_iter.peek()) {
400        if range_a.end < range_b.start {
401            ranges_a_iter.next();
402        } else if range_b.end < range_a.start {
403            ranges_b_iter.next();
404        } else {
405            return true;
406        }
407    }
408    false
409}
410
411struct TrackedBuffer {
412    buffer: Entity<Buffer>,
413    change: Change,
414    version: clock::Global,
415    diff: Entity<BufferDiff>,
416    secondary_diff: Entity<BufferDiff>,
417    diff_update: async_watch::Sender<()>,
418    _maintain_diff: Task<()>,
419    _subscription: Subscription,
420}
421
422enum Change {
423    Edited {
424        unreviewed_edit_ids: HashSet<clock::Lamport>,
425        accepted_edit_ids: HashSet<clock::Lamport>,
426        initial_content: Option<TextBufferSnapshot>,
427    },
428    Deleted {
429        reviewed: bool,
430        deleted_content: TextBufferSnapshot,
431        deletion_id: Option<clock::Lamport>,
432    },
433}
434
435impl TrackedBuffer {
436    fn has_changes(&self, cx: &App) -> bool {
437        self.diff
438            .read(cx)
439            .hunks(&self.buffer.read(cx), cx)
440            .next()
441            .is_some()
442    }
443
444    fn schedule_diff_update(&self) {
445        self.diff_update.send(()).ok();
446    }
447
448    fn update_diff(&mut self, cx: &mut App) -> Task<()> {
449        match &self.change {
450            Change::Edited {
451                unreviewed_edit_ids,
452                accepted_edit_ids,
453                ..
454            } => {
455                let edits_to_undo = unreviewed_edit_ids
456                    .iter()
457                    .chain(accepted_edit_ids)
458                    .map(|edit_id| (*edit_id, u32::MAX))
459                    .collect::<HashMap<_, _>>();
460                let buffer_without_edits = self.buffer.update(cx, |buffer, cx| buffer.branch(cx));
461                buffer_without_edits
462                    .update(cx, |buffer, cx| buffer.undo_operations(edits_to_undo, cx));
463                let primary_diff_update = self.diff.update(cx, |diff, cx| {
464                    diff.set_base_text(
465                        buffer_without_edits,
466                        self.buffer.read(cx).text_snapshot(),
467                        cx,
468                    )
469                });
470
471                let unreviewed_edits_to_undo = unreviewed_edit_ids
472                    .iter()
473                    .map(|edit_id| (*edit_id, u32::MAX))
474                    .collect::<HashMap<_, _>>();
475                let buffer_without_unreviewed_edits =
476                    self.buffer.update(cx, |buffer, cx| buffer.branch(cx));
477                buffer_without_unreviewed_edits.update(cx, |buffer, cx| {
478                    buffer.undo_operations(unreviewed_edits_to_undo, cx)
479                });
480                let secondary_diff_update = self.secondary_diff.update(cx, |diff, cx| {
481                    diff.set_base_text(
482                        buffer_without_unreviewed_edits.clone(),
483                        self.buffer.read(cx).text_snapshot(),
484                        cx,
485                    )
486                });
487
488                cx.background_spawn(async move {
489                    _ = primary_diff_update.await;
490                    _ = secondary_diff_update.await;
491                })
492            }
493            Change::Deleted {
494                reviewed,
495                deleted_content,
496                ..
497            } => {
498                let reviewed = *reviewed;
499                let deleted_content = deleted_content.clone();
500
501                let primary_diff = self.diff.clone();
502                let secondary_diff = self.secondary_diff.clone();
503                let buffer_snapshot = self.buffer.read(cx).text_snapshot();
504                let language = self.buffer.read(cx).language().cloned();
505                let language_registry = self.buffer.read(cx).language_registry().clone();
506
507                cx.spawn(async move |cx| {
508                    let base_text = Arc::new(deleted_content.text());
509
510                    let primary_diff_snapshot = BufferDiff::update_diff(
511                        primary_diff.clone(),
512                        buffer_snapshot.clone(),
513                        Some(base_text.clone()),
514                        true,
515                        false,
516                        language.clone(),
517                        language_registry.clone(),
518                        cx,
519                    )
520                    .await;
521                    let secondary_diff_snapshot = BufferDiff::update_diff(
522                        secondary_diff.clone(),
523                        buffer_snapshot.clone(),
524                        if reviewed {
525                            None
526                        } else {
527                            Some(base_text.clone())
528                        },
529                        true,
530                        false,
531                        language.clone(),
532                        language_registry.clone(),
533                        cx,
534                    )
535                    .await;
536
537                    if let Ok(primary_diff_snapshot) = primary_diff_snapshot {
538                        primary_diff
539                            .update(cx, |diff, cx| {
540                                diff.set_snapshot(
541                                    &buffer_snapshot,
542                                    primary_diff_snapshot,
543                                    false,
544                                    None,
545                                    cx,
546                                )
547                            })
548                            .ok();
549                    }
550
551                    if let Ok(secondary_diff_snapshot) = secondary_diff_snapshot {
552                        secondary_diff
553                            .update(cx, |diff, cx| {
554                                diff.set_snapshot(
555                                    &buffer_snapshot,
556                                    secondary_diff_snapshot,
557                                    false,
558                                    None,
559                                    cx,
560                                )
561                            })
562                            .ok();
563                    }
564                })
565            }
566        }
567    }
568}
569
570pub struct ChangedBuffer {
571    pub diff: Entity<BufferDiff>,
572    pub needs_review: bool,
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578    use buffer_diff::DiffHunkStatusKind;
579    use gpui::TestAppContext;
580    use language::Point;
581    use project::{FakeFs, Fs, Project, RemoveOptions};
582    use serde_json::json;
583    use settings::SettingsStore;
584    use util::path;
585
586    #[gpui::test(iterations = 10)]
587    async fn test_edit_review(cx: &mut TestAppContext) {
588        let action_log = cx.new(|_| ActionLog::new());
589        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
590
591        let edit1 = buffer.update(cx, |buffer, cx| {
592            buffer
593                .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
594                .unwrap()
595        });
596        let edit2 = buffer.update(cx, |buffer, cx| {
597            buffer
598                .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
599                .unwrap()
600        });
601        assert_eq!(
602            buffer.read_with(cx, |buffer, _| buffer.text()),
603            "abc\ndEf\nghi\njkl\nmnO"
604        );
605
606        action_log.update(cx, |log, cx| {
607            log.buffer_edited(buffer.clone(), vec![edit1, edit2], cx)
608        });
609        cx.run_until_parked();
610        assert_eq!(
611            unreviewed_hunks(&action_log, cx),
612            vec![(
613                buffer.clone(),
614                vec![
615                    HunkStatus {
616                        range: Point::new(1, 0)..Point::new(2, 0),
617                        review_status: ReviewStatus::Unreviewed,
618                        diff_status: DiffHunkStatusKind::Modified,
619                        old_text: "def\n".into(),
620                    },
621                    HunkStatus {
622                        range: Point::new(4, 0)..Point::new(4, 3),
623                        review_status: ReviewStatus::Unreviewed,
624                        diff_status: DiffHunkStatusKind::Modified,
625                        old_text: "mno".into(),
626                    }
627                ],
628            )]
629        );
630
631        action_log.update(cx, |log, cx| {
632            log.review_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), true, cx)
633        });
634        cx.run_until_parked();
635        assert_eq!(
636            unreviewed_hunks(&action_log, cx),
637            vec![(
638                buffer.clone(),
639                vec![
640                    HunkStatus {
641                        range: Point::new(1, 0)..Point::new(2, 0),
642                        review_status: ReviewStatus::Unreviewed,
643                        diff_status: DiffHunkStatusKind::Modified,
644                        old_text: "def\n".into(),
645                    },
646                    HunkStatus {
647                        range: Point::new(4, 0)..Point::new(4, 3),
648                        review_status: ReviewStatus::Reviewed,
649                        diff_status: DiffHunkStatusKind::Modified,
650                        old_text: "mno".into(),
651                    }
652                ],
653            )]
654        );
655
656        action_log.update(cx, |log, cx| {
657            log.review_edits_in_range(
658                buffer.clone(),
659                Point::new(3, 0)..Point::new(4, 3),
660                false,
661                cx,
662            )
663        });
664        cx.run_until_parked();
665        assert_eq!(
666            unreviewed_hunks(&action_log, cx),
667            vec![(
668                buffer.clone(),
669                vec![
670                    HunkStatus {
671                        range: Point::new(1, 0)..Point::new(2, 0),
672                        review_status: ReviewStatus::Unreviewed,
673                        diff_status: DiffHunkStatusKind::Modified,
674                        old_text: "def\n".into(),
675                    },
676                    HunkStatus {
677                        range: Point::new(4, 0)..Point::new(4, 3),
678                        review_status: ReviewStatus::Unreviewed,
679                        diff_status: DiffHunkStatusKind::Modified,
680                        old_text: "mno".into(),
681                    }
682                ],
683            )]
684        );
685
686        action_log.update(cx, |log, cx| {
687            log.review_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), true, cx)
688        });
689        cx.run_until_parked();
690        assert_eq!(
691            unreviewed_hunks(&action_log, cx),
692            vec![(
693                buffer.clone(),
694                vec![
695                    HunkStatus {
696                        range: Point::new(1, 0)..Point::new(2, 0),
697                        review_status: ReviewStatus::Reviewed,
698                        diff_status: DiffHunkStatusKind::Modified,
699                        old_text: "def\n".into(),
700                    },
701                    HunkStatus {
702                        range: Point::new(4, 0)..Point::new(4, 3),
703                        review_status: ReviewStatus::Reviewed,
704                        diff_status: DiffHunkStatusKind::Modified,
705                        old_text: "mno".into(),
706                    }
707                ],
708            )]
709        );
710    }
711
712    #[gpui::test(iterations = 10)]
713    async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
714        let action_log = cx.new(|_| ActionLog::new());
715        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
716
717        let tool_edit = buffer.update(cx, |buffer, cx| {
718            buffer
719                .edit(
720                    [(Point::new(0, 2)..Point::new(2, 3), "C\nDEF\nGHI")],
721                    None,
722                    cx,
723                )
724                .unwrap()
725        });
726        assert_eq!(
727            buffer.read_with(cx, |buffer, _| buffer.text()),
728            "abC\nDEF\nGHI\njkl\nmno"
729        );
730
731        action_log.update(cx, |log, cx| {
732            log.buffer_edited(buffer.clone(), vec![tool_edit], cx)
733        });
734        cx.run_until_parked();
735        assert_eq!(
736            unreviewed_hunks(&action_log, cx),
737            vec![(
738                buffer.clone(),
739                vec![HunkStatus {
740                    range: Point::new(0, 0)..Point::new(3, 0),
741                    review_status: ReviewStatus::Unreviewed,
742                    diff_status: DiffHunkStatusKind::Modified,
743                    old_text: "abc\ndef\nghi\n".into(),
744                }],
745            )]
746        );
747
748        action_log.update(cx, |log, cx| {
749            log.review_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), true, cx)
750        });
751        cx.run_until_parked();
752        assert_eq!(
753            unreviewed_hunks(&action_log, cx),
754            vec![(
755                buffer.clone(),
756                vec![HunkStatus {
757                    range: Point::new(0, 0)..Point::new(3, 0),
758                    review_status: ReviewStatus::Reviewed,
759                    diff_status: DiffHunkStatusKind::Modified,
760                    old_text: "abc\ndef\nghi\n".into(),
761                }],
762            )]
763        );
764
765        buffer.update(cx, |buffer, cx| {
766            buffer.edit([(Point::new(0, 2)..Point::new(0, 2), "X")], None, cx)
767        });
768        cx.run_until_parked();
769        assert_eq!(
770            unreviewed_hunks(&action_log, cx),
771            vec![(
772                buffer.clone(),
773                vec![HunkStatus {
774                    range: Point::new(0, 0)..Point::new(3, 0),
775                    review_status: ReviewStatus::Unreviewed,
776                    diff_status: DiffHunkStatusKind::Modified,
777                    old_text: "abc\ndef\nghi\n".into(),
778                }],
779            )]
780        );
781
782        action_log.update(cx, |log, cx| log.clear_reviewed_changes(cx));
783        cx.run_until_parked();
784        assert_eq!(
785            unreviewed_hunks(&action_log, cx),
786            vec![(
787                buffer.clone(),
788                vec![HunkStatus {
789                    range: Point::new(0, 0)..Point::new(3, 0),
790                    review_status: ReviewStatus::Unreviewed,
791                    diff_status: DiffHunkStatusKind::Modified,
792                    old_text: "abc\ndef\nghi\n".into(),
793                }],
794            )]
795        );
796    }
797
798    #[gpui::test(iterations = 10)]
799    async fn test_deletion(cx: &mut TestAppContext) {
800        cx.update(|cx| {
801            let settings_store = SettingsStore::test(cx);
802            cx.set_global(settings_store);
803            language::init(cx);
804            Project::init_settings(cx);
805        });
806
807        let fs = FakeFs::new(cx.executor());
808        fs.insert_tree(
809            path!("/dir"),
810            json!({"file1": "lorem\n", "file2": "ipsum\n"}),
811        )
812        .await;
813
814        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
815        let file1_path = project
816            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
817            .unwrap();
818        let file2_path = project
819            .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
820            .unwrap();
821
822        let action_log = cx.new(|_| ActionLog::new());
823        let buffer1 = project
824            .update(cx, |project, cx| {
825                project.open_buffer(file1_path.clone(), cx)
826            })
827            .await
828            .unwrap();
829        let buffer2 = project
830            .update(cx, |project, cx| {
831                project.open_buffer(file2_path.clone(), cx)
832            })
833            .await
834            .unwrap();
835
836        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
837        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
838        project
839            .update(cx, |project, cx| {
840                project.delete_file(file1_path.clone(), false, cx)
841            })
842            .unwrap()
843            .await
844            .unwrap();
845        project
846            .update(cx, |project, cx| {
847                project.delete_file(file2_path.clone(), false, cx)
848            })
849            .unwrap()
850            .await
851            .unwrap();
852        cx.run_until_parked();
853        assert_eq!(
854            unreviewed_hunks(&action_log, cx),
855            vec![
856                (
857                    buffer1.clone(),
858                    vec![HunkStatus {
859                        range: Point::new(0, 0)..Point::new(0, 0),
860                        review_status: ReviewStatus::Unreviewed,
861                        diff_status: DiffHunkStatusKind::Deleted,
862                        old_text: "lorem\n".into(),
863                    }]
864                ),
865                (
866                    buffer2.clone(),
867                    vec![HunkStatus {
868                        range: Point::new(0, 0)..Point::new(0, 0),
869                        review_status: ReviewStatus::Unreviewed,
870                        diff_status: DiffHunkStatusKind::Deleted,
871                        old_text: "ipsum\n".into(),
872                    }],
873                )
874            ]
875        );
876
877        // Simulate file1 being recreated externally.
878        fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
879            .await;
880        let buffer2 = project
881            .update(cx, |project, cx| project.open_buffer(file2_path, cx))
882            .await
883            .unwrap();
884        cx.run_until_parked();
885        // Simulate file2 being recreated by a tool.
886        let edit_id = buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
887        action_log.update(cx, |log, cx| {
888            log.will_create_buffer(buffer2.clone(), edit_id, cx)
889        });
890        project
891            .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
892            .await
893            .unwrap();
894        cx.run_until_parked();
895        assert_eq!(
896            unreviewed_hunks(&action_log, cx),
897            vec![(
898                buffer2.clone(),
899                vec![HunkStatus {
900                    range: Point::new(0, 0)..Point::new(0, 5),
901                    review_status: ReviewStatus::Unreviewed,
902                    diff_status: DiffHunkStatusKind::Modified,
903                    old_text: "ipsum\n".into(),
904                }],
905            )]
906        );
907
908        // Simulate file2 being deleted externally.
909        fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
910            .await
911            .unwrap();
912        cx.run_until_parked();
913        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
914    }
915
916    #[derive(Debug, Clone, PartialEq, Eq)]
917    struct HunkStatus {
918        range: Range<Point>,
919        review_status: ReviewStatus,
920        diff_status: DiffHunkStatusKind,
921        old_text: String,
922    }
923
924    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
925    enum ReviewStatus {
926        Unreviewed,
927        Reviewed,
928    }
929
930    fn unreviewed_hunks(
931        action_log: &Entity<ActionLog>,
932        cx: &TestAppContext,
933    ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
934        cx.read(|cx| {
935            action_log
936                .read(cx)
937                .changed_buffers(cx)
938                .into_iter()
939                .map(|(buffer, tracked_buffer)| {
940                    let snapshot = buffer.read(cx).snapshot();
941                    (
942                        buffer,
943                        tracked_buffer
944                            .diff
945                            .read(cx)
946                            .hunks(&snapshot, cx)
947                            .map(|hunk| HunkStatus {
948                                review_status: if hunk.status().has_secondary_hunk() {
949                                    ReviewStatus::Unreviewed
950                                } else {
951                                    ReviewStatus::Reviewed
952                                },
953                                diff_status: hunk.status().kind,
954                                range: hunk.range,
955                                old_text: tracked_buffer
956                                    .diff
957                                    .read(cx)
958                                    .base_text()
959                                    .text_for_range(hunk.diff_base_byte_range)
960                                    .collect(),
961                            })
962                            .collect(),
963                    )
964                })
965                .collect()
966        })
967    }
968}