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)| tracked.version != buffer.read(cx).version)
406 .map(|(buffer, _)| buffer)
407 }
408
409 /// Takes and returns the set of buffers pending refresh, clearing internal state.
410 pub fn take_stale_buffers_in_context(&mut self) -> HashSet<Entity<Buffer>> {
411 std::mem::take(&mut self.stale_buffers_in_context)
412 }
413}
414
415fn ranges_intersect(
416 ranges_a: impl IntoIterator<Item = Range<usize>>,
417 ranges_b: impl IntoIterator<Item = Range<usize>>,
418) -> bool {
419 let mut ranges_a_iter = ranges_a.into_iter().peekable();
420 let mut ranges_b_iter = ranges_b.into_iter().peekable();
421 while let (Some(range_a), Some(range_b)) = (ranges_a_iter.peek(), ranges_b_iter.peek()) {
422 if range_a.end < range_b.start {
423 ranges_a_iter.next();
424 } else if range_b.end < range_a.start {
425 ranges_b_iter.next();
426 } else {
427 return true;
428 }
429 }
430 false
431}
432
433struct TrackedBuffer {
434 buffer: Entity<Buffer>,
435 change: Change,
436 version: clock::Global,
437 diff: Entity<BufferDiff>,
438 secondary_diff: Entity<BufferDiff>,
439 diff_update: async_watch::Sender<()>,
440 _maintain_diff: Task<()>,
441 _subscription: Subscription,
442}
443
444enum Change {
445 Edited {
446 unreviewed_edit_ids: HashSet<clock::Lamport>,
447 accepted_edit_ids: HashSet<clock::Lamport>,
448 initial_content: Option<TextBufferSnapshot>,
449 },
450 Deleted {
451 reviewed: bool,
452 deleted_content: TextBufferSnapshot,
453 deletion_id: Option<clock::Lamport>,
454 },
455}
456
457impl TrackedBuffer {
458 fn has_changes(&self, cx: &App) -> bool {
459 self.diff
460 .read(cx)
461 .hunks(&self.buffer.read(cx), cx)
462 .next()
463 .is_some()
464 }
465
466 fn schedule_diff_update(&self) {
467 self.diff_update.send(()).ok();
468 }
469
470 fn update_diff(&mut self, cx: &mut App) -> Task<()> {
471 match &self.change {
472 Change::Edited {
473 unreviewed_edit_ids,
474 accepted_edit_ids,
475 ..
476 } => {
477 let edits_to_undo = unreviewed_edit_ids
478 .iter()
479 .chain(accepted_edit_ids)
480 .map(|edit_id| (*edit_id, u32::MAX))
481 .collect::<HashMap<_, _>>();
482 let buffer_without_edits = self.buffer.update(cx, |buffer, cx| buffer.branch(cx));
483 buffer_without_edits
484 .update(cx, |buffer, cx| buffer.undo_operations(edits_to_undo, cx));
485 let primary_diff_update = self.diff.update(cx, |diff, cx| {
486 diff.set_base_text(
487 buffer_without_edits,
488 self.buffer.read(cx).text_snapshot(),
489 cx,
490 )
491 });
492
493 let unreviewed_edits_to_undo = unreviewed_edit_ids
494 .iter()
495 .map(|edit_id| (*edit_id, u32::MAX))
496 .collect::<HashMap<_, _>>();
497 let buffer_without_unreviewed_edits =
498 self.buffer.update(cx, |buffer, cx| buffer.branch(cx));
499 buffer_without_unreviewed_edits.update(cx, |buffer, cx| {
500 buffer.undo_operations(unreviewed_edits_to_undo, cx)
501 });
502 let secondary_diff_update = self.secondary_diff.update(cx, |diff, cx| {
503 diff.set_base_text(
504 buffer_without_unreviewed_edits.clone(),
505 self.buffer.read(cx).text_snapshot(),
506 cx,
507 )
508 });
509
510 cx.background_spawn(async move {
511 _ = primary_diff_update.await;
512 _ = secondary_diff_update.await;
513 })
514 }
515 Change::Deleted {
516 reviewed,
517 deleted_content,
518 ..
519 } => {
520 let reviewed = *reviewed;
521 let deleted_content = deleted_content.clone();
522
523 let primary_diff = self.diff.clone();
524 let secondary_diff = self.secondary_diff.clone();
525 let buffer_snapshot = self.buffer.read(cx).text_snapshot();
526 let language = self.buffer.read(cx).language().cloned();
527 let language_registry = self.buffer.read(cx).language_registry().clone();
528
529 cx.spawn(async move |cx| {
530 let base_text = Arc::new(deleted_content.text());
531
532 let primary_diff_snapshot = BufferDiff::update_diff(
533 primary_diff.clone(),
534 buffer_snapshot.clone(),
535 Some(base_text.clone()),
536 true,
537 false,
538 language.clone(),
539 language_registry.clone(),
540 cx,
541 )
542 .await;
543 let secondary_diff_snapshot = BufferDiff::update_diff(
544 secondary_diff.clone(),
545 buffer_snapshot.clone(),
546 if reviewed {
547 None
548 } else {
549 Some(base_text.clone())
550 },
551 true,
552 false,
553 language.clone(),
554 language_registry.clone(),
555 cx,
556 )
557 .await;
558
559 if let Ok(primary_diff_snapshot) = primary_diff_snapshot {
560 primary_diff
561 .update(cx, |diff, cx| {
562 diff.set_snapshot(
563 &buffer_snapshot,
564 primary_diff_snapshot,
565 false,
566 None,
567 cx,
568 )
569 })
570 .ok();
571 }
572
573 if let Ok(secondary_diff_snapshot) = secondary_diff_snapshot {
574 secondary_diff
575 .update(cx, |diff, cx| {
576 diff.set_snapshot(
577 &buffer_snapshot,
578 secondary_diff_snapshot,
579 false,
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}