1use anyhow::{Context as _, Result};
2use buffer_diff::BufferDiff;
3use collections::BTreeMap;
4use futures::{StreamExt, channel::mpsc};
5use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
6use language::{Anchor, Buffer, BufferEvent, DiskState, Point};
7use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
8use std::{cmp, ops::Range, sync::Arc};
9use text::{Edit, Patch, Rope};
10use util::RangeExt;
11
12/// Tracks actions performed by tools in a thread
13pub struct ActionLog {
14 /// Buffers that we want to notify the model about when they change.
15 tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
16 /// Has the model edited a file since it last checked diagnostics?
17 edited_since_project_diagnostics_check: bool,
18 /// The project this action log is associated with
19 project: Entity<Project>,
20}
21
22impl ActionLog {
23 /// Creates a new, empty action log associated with the given project.
24 pub fn new(project: Entity<Project>) -> Self {
25 Self {
26 tracked_buffers: BTreeMap::default(),
27 edited_since_project_diagnostics_check: false,
28 project,
29 }
30 }
31
32 /// Notifies a diagnostics check
33 pub fn checked_project_diagnostics(&mut self) {
34 self.edited_since_project_diagnostics_check = false;
35 }
36
37 /// Returns true if any files have been edited since the last project diagnostics check
38 pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
39 self.edited_since_project_diagnostics_check
40 }
41
42 fn track_buffer(
43 &mut self,
44 buffer: Entity<Buffer>,
45 created: bool,
46 cx: &mut Context<Self>,
47 ) -> &mut TrackedBuffer {
48 let tracked_buffer = self
49 .tracked_buffers
50 .entry(buffer.clone())
51 .or_insert_with(|| {
52 let open_lsp_handle = self.project.update(cx, |project, cx| {
53 project.register_buffer_with_language_servers(&buffer, cx)
54 });
55
56 let text_snapshot = buffer.read(cx).text_snapshot();
57 let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
58 let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
59 let base_text;
60 let status;
61 let unreviewed_changes;
62 if created {
63 base_text = Rope::default();
64 status = TrackedBufferStatus::Created;
65 unreviewed_changes = Patch::new(vec![Edit {
66 old: 0..1,
67 new: 0..text_snapshot.max_point().row + 1,
68 }])
69 } else {
70 base_text = buffer.read(cx).as_rope().clone();
71 status = TrackedBufferStatus::Modified;
72 unreviewed_changes = Patch::default();
73 }
74 TrackedBuffer {
75 buffer: buffer.clone(),
76 base_text,
77 unreviewed_changes,
78 snapshot: text_snapshot.clone(),
79 status,
80 version: buffer.read(cx).version(),
81 diff,
82 diff_update: diff_update_tx,
83 _open_lsp_handle: open_lsp_handle,
84 _maintain_diff: cx.spawn({
85 let buffer = buffer.clone();
86 async move |this, cx| {
87 Self::maintain_diff(this, buffer, diff_update_rx, cx)
88 .await
89 .ok();
90 }
91 }),
92 _subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
93 }
94 });
95 tracked_buffer.version = buffer.read(cx).version();
96 tracked_buffer
97 }
98
99 fn handle_buffer_event(
100 &mut self,
101 buffer: Entity<Buffer>,
102 event: &BufferEvent,
103 cx: &mut Context<Self>,
104 ) {
105 match event {
106 BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx),
107 BufferEvent::FileHandleChanged => {
108 self.handle_buffer_file_changed(buffer, cx);
109 }
110 _ => {}
111 };
112 }
113
114 fn handle_buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
115 let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
116 return;
117 };
118 tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
119 }
120
121 fn handle_buffer_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
122 let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
123 return;
124 };
125
126 match tracked_buffer.status {
127 TrackedBufferStatus::Created | TrackedBufferStatus::Modified => {
128 if buffer
129 .read(cx)
130 .file()
131 .map_or(false, |file| file.disk_state() == DiskState::Deleted)
132 {
133 // If the buffer had been edited by a tool, but it got
134 // deleted externally, we want to stop tracking it.
135 self.tracked_buffers.remove(&buffer);
136 }
137 cx.notify();
138 }
139 TrackedBufferStatus::Deleted => {
140 if buffer
141 .read(cx)
142 .file()
143 .map_or(false, |file| file.disk_state() != DiskState::Deleted)
144 {
145 // If the buffer had been deleted by a tool, but it got
146 // resurrected externally, we want to clear the changes we
147 // were tracking and reset the buffer's state.
148 self.tracked_buffers.remove(&buffer);
149 self.track_buffer(buffer, false, cx);
150 }
151 cx.notify();
152 }
153 }
154 }
155
156 async fn maintain_diff(
157 this: WeakEntity<Self>,
158 buffer: Entity<Buffer>,
159 mut diff_update: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>,
160 cx: &mut AsyncApp,
161 ) -> Result<()> {
162 while let Some((author, buffer_snapshot)) = diff_update.next().await {
163 let (rebase, diff, language, language_registry) =
164 this.read_with(cx, |this, cx| {
165 let tracked_buffer = this
166 .tracked_buffers
167 .get(&buffer)
168 .context("buffer not tracked")?;
169
170 let rebase = cx.background_spawn({
171 let mut base_text = tracked_buffer.base_text.clone();
172 let old_snapshot = tracked_buffer.snapshot.clone();
173 let new_snapshot = buffer_snapshot.clone();
174 let unreviewed_changes = tracked_buffer.unreviewed_changes.clone();
175 async move {
176 let edits = diff_snapshots(&old_snapshot, &new_snapshot);
177 if let ChangeAuthor::User = author {
178 apply_non_conflicting_edits(
179 &unreviewed_changes,
180 edits,
181 &mut base_text,
182 new_snapshot.as_rope(),
183 );
184 }
185 (Arc::new(base_text.to_string()), base_text)
186 }
187 });
188
189 anyhow::Ok((
190 rebase,
191 tracked_buffer.diff.clone(),
192 tracked_buffer.buffer.read(cx).language().cloned(),
193 tracked_buffer.buffer.read(cx).language_registry(),
194 ))
195 })??;
196
197 let (new_base_text, new_base_text_rope) = rebase.await;
198 let diff_snapshot = BufferDiff::update_diff(
199 diff.clone(),
200 buffer_snapshot.clone(),
201 Some(new_base_text),
202 true,
203 false,
204 language,
205 language_registry,
206 cx,
207 )
208 .await;
209
210 let mut unreviewed_changes = Patch::default();
211 if let Ok(diff_snapshot) = diff_snapshot {
212 unreviewed_changes = cx
213 .background_spawn({
214 let diff_snapshot = diff_snapshot.clone();
215 let buffer_snapshot = buffer_snapshot.clone();
216 let new_base_text_rope = new_base_text_rope.clone();
217 async move {
218 let mut unreviewed_changes = Patch::default();
219 for hunk in diff_snapshot.hunks_intersecting_range(
220 Anchor::MIN..Anchor::MAX,
221 &buffer_snapshot,
222 ) {
223 let old_range = new_base_text_rope
224 .offset_to_point(hunk.diff_base_byte_range.start)
225 ..new_base_text_rope
226 .offset_to_point(hunk.diff_base_byte_range.end);
227 let new_range = hunk.range.start..hunk.range.end;
228 unreviewed_changes.push(point_to_row_edit(
229 Edit {
230 old: old_range,
231 new: new_range,
232 },
233 &new_base_text_rope,
234 &buffer_snapshot.as_rope(),
235 ));
236 }
237 unreviewed_changes
238 }
239 })
240 .await;
241
242 diff.update(cx, |diff, cx| {
243 diff.set_snapshot(diff_snapshot, &buffer_snapshot, cx)
244 })?;
245 }
246 this.update(cx, |this, cx| {
247 let tracked_buffer = this
248 .tracked_buffers
249 .get_mut(&buffer)
250 .context("buffer not tracked")?;
251 tracked_buffer.base_text = new_base_text_rope;
252 tracked_buffer.snapshot = buffer_snapshot;
253 tracked_buffer.unreviewed_changes = unreviewed_changes;
254 cx.notify();
255 anyhow::Ok(())
256 })??;
257 }
258
259 Ok(())
260 }
261
262 /// Track a buffer as read, so we can notify the model about user edits.
263 pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
264 self.track_buffer(buffer, false, cx);
265 }
266
267 /// Track a buffer that was added as context, so we can notify the model about user edits.
268 pub fn buffer_added_as_context(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
269 self.track_buffer(buffer, false, cx);
270 }
271
272 /// Track a buffer as read, so we can notify the model about user edits.
273 pub fn will_create_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
274 self.track_buffer(buffer.clone(), true, cx);
275 self.buffer_edited(buffer, cx)
276 }
277
278 /// Mark a buffer as edited, so we can refresh it in the context
279 pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
280 self.edited_since_project_diagnostics_check = true;
281
282 let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
283 if let TrackedBufferStatus::Deleted = tracked_buffer.status {
284 tracked_buffer.status = TrackedBufferStatus::Modified;
285 }
286 tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
287 }
288
289 pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
290 let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
291 match tracked_buffer.status {
292 TrackedBufferStatus::Created => {
293 self.tracked_buffers.remove(&buffer);
294 cx.notify();
295 }
296 TrackedBufferStatus::Modified => {
297 buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
298 tracked_buffer.status = TrackedBufferStatus::Deleted;
299 tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
300 }
301 TrackedBufferStatus::Deleted => {}
302 }
303 cx.notify();
304 }
305
306 pub fn keep_edits_in_range(
307 &mut self,
308 buffer: Entity<Buffer>,
309 buffer_range: Range<impl language::ToPoint>,
310 cx: &mut Context<Self>,
311 ) {
312 let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
313 return;
314 };
315
316 match tracked_buffer.status {
317 TrackedBufferStatus::Deleted => {
318 self.tracked_buffers.remove(&buffer);
319 cx.notify();
320 }
321 _ => {
322 let buffer = buffer.read(cx);
323 let buffer_range =
324 buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
325 let mut delta = 0i32;
326
327 tracked_buffer.unreviewed_changes.retain_mut(|edit| {
328 edit.old.start = (edit.old.start as i32 + delta) as u32;
329 edit.old.end = (edit.old.end as i32 + delta) as u32;
330
331 if buffer_range.end.row < edit.new.start
332 || buffer_range.start.row > edit.new.end
333 {
334 true
335 } else {
336 let old_range = tracked_buffer
337 .base_text
338 .point_to_offset(Point::new(edit.old.start, 0))
339 ..tracked_buffer.base_text.point_to_offset(cmp::min(
340 Point::new(edit.old.end, 0),
341 tracked_buffer.base_text.max_point(),
342 ));
343 let new_range = tracked_buffer
344 .snapshot
345 .point_to_offset(Point::new(edit.new.start, 0))
346 ..tracked_buffer.snapshot.point_to_offset(cmp::min(
347 Point::new(edit.new.end, 0),
348 tracked_buffer.snapshot.max_point(),
349 ));
350 tracked_buffer.base_text.replace(
351 old_range,
352 &tracked_buffer
353 .snapshot
354 .text_for_range(new_range)
355 .collect::<String>(),
356 );
357 delta += edit.new_len() as i32 - edit.old_len() as i32;
358 false
359 }
360 });
361 tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
362 }
363 }
364 }
365
366 pub fn reject_edits_in_range(
367 &mut self,
368 buffer: Entity<Buffer>,
369 buffer_range: Range<impl language::ToPoint>,
370 cx: &mut Context<Self>,
371 ) -> Task<Result<()>> {
372 let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
373 return Task::ready(Ok(()));
374 };
375
376 match tracked_buffer.status {
377 TrackedBufferStatus::Created => {
378 let delete = buffer
379 .read(cx)
380 .entry_id(cx)
381 .and_then(|entry_id| {
382 self.project
383 .update(cx, |project, cx| project.delete_entry(entry_id, false, cx))
384 })
385 .unwrap_or(Task::ready(Ok(())));
386 self.tracked_buffers.remove(&buffer);
387 cx.notify();
388 delete
389 }
390 TrackedBufferStatus::Deleted => {
391 buffer.update(cx, |buffer, cx| {
392 buffer.set_text(tracked_buffer.base_text.to_string(), cx)
393 });
394 let save = self
395 .project
396 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
397
398 // Clear all tracked changes for this buffer and start over as if we just read it.
399 self.tracked_buffers.remove(&buffer);
400 self.track_buffer(buffer.clone(), false, cx);
401 cx.notify();
402 save
403 }
404 TrackedBufferStatus::Modified => {
405 buffer.update(cx, |buffer, cx| {
406 let buffer_range =
407 buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
408
409 let mut edits_to_revert = Vec::new();
410 for edit in tracked_buffer.unreviewed_changes.edits() {
411 if buffer_range.end.row < edit.new.start {
412 break;
413 } else if buffer_range.start.row > edit.new.end {
414 continue;
415 }
416
417 let old_range = tracked_buffer
418 .base_text
419 .point_to_offset(Point::new(edit.old.start, 0))
420 ..tracked_buffer.base_text.point_to_offset(cmp::min(
421 Point::new(edit.old.end, 0),
422 tracked_buffer.base_text.max_point(),
423 ));
424 let old_text = tracked_buffer
425 .base_text
426 .chunks_in_range(old_range)
427 .collect::<String>();
428
429 let new_range = tracked_buffer
430 .snapshot
431 .anchor_before(Point::new(edit.new.start, 0))
432 ..tracked_buffer.snapshot.anchor_after(cmp::min(
433 Point::new(edit.new.end, 0),
434 tracked_buffer.snapshot.max_point(),
435 ));
436 edits_to_revert.push((new_range, old_text));
437 }
438
439 buffer.edit(edits_to_revert, None, cx);
440 });
441 self.project
442 .update(cx, |project, cx| project.save_buffer(buffer, cx))
443 }
444 }
445 }
446
447 pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) {
448 self.tracked_buffers
449 .retain(|_buffer, tracked_buffer| match tracked_buffer.status {
450 TrackedBufferStatus::Deleted => false,
451 _ => {
452 tracked_buffer.unreviewed_changes.clear();
453 tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone();
454 tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
455 true
456 }
457 });
458 cx.notify();
459 }
460
461 /// Returns the set of buffers that contain changes that haven't been reviewed by the user.
462 pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
463 self.tracked_buffers
464 .iter()
465 .filter(|(_, tracked)| tracked.has_changes(cx))
466 .map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone()))
467 .collect()
468 }
469
470 /// Iterate over buffers changed since last read or edited by the model
471 pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
472 self.tracked_buffers
473 .iter()
474 .filter(|(buffer, tracked)| {
475 let buffer = buffer.read(cx);
476
477 tracked.version != buffer.version
478 && buffer
479 .file()
480 .map_or(false, |file| file.disk_state() != DiskState::Deleted)
481 })
482 .map(|(buffer, _)| buffer)
483 }
484}
485
486fn apply_non_conflicting_edits(
487 patch: &Patch<u32>,
488 edits: Vec<Edit<u32>>,
489 old_text: &mut Rope,
490 new_text: &Rope,
491) {
492 let mut old_edits = patch.edits().iter().cloned().peekable();
493 let mut new_edits = edits.into_iter().peekable();
494 let mut applied_delta = 0i32;
495 let mut rebased_delta = 0i32;
496
497 while let Some(mut new_edit) = new_edits.next() {
498 let mut conflict = false;
499
500 // Push all the old edits that are before this new edit or that intersect with it.
501 while let Some(old_edit) = old_edits.peek() {
502 if new_edit.old.end < old_edit.new.start
503 || (!old_edit.new.is_empty() && new_edit.old.end == old_edit.new.start)
504 {
505 break;
506 } else if new_edit.old.start > old_edit.new.end
507 || (!old_edit.new.is_empty() && new_edit.old.start == old_edit.new.end)
508 {
509 let old_edit = old_edits.next().unwrap();
510 rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
511 } else {
512 conflict = true;
513 if new_edits
514 .peek()
515 .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new))
516 {
517 new_edit = new_edits.next().unwrap();
518 } else {
519 let old_edit = old_edits.next().unwrap();
520 rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
521 }
522 }
523 }
524
525 if !conflict {
526 // This edit doesn't intersect with any old edit, so we can apply it to the old text.
527 new_edit.old.start = (new_edit.old.start as i32 + applied_delta - rebased_delta) as u32;
528 new_edit.old.end = (new_edit.old.end as i32 + applied_delta - rebased_delta) as u32;
529 let old_bytes = old_text.point_to_offset(Point::new(new_edit.old.start, 0))
530 ..old_text.point_to_offset(cmp::min(
531 Point::new(new_edit.old.end, 0),
532 old_text.max_point(),
533 ));
534 let new_bytes = new_text.point_to_offset(Point::new(new_edit.new.start, 0))
535 ..new_text.point_to_offset(cmp::min(
536 Point::new(new_edit.new.end, 0),
537 new_text.max_point(),
538 ));
539
540 old_text.replace(
541 old_bytes,
542 &new_text.chunks_in_range(new_bytes).collect::<String>(),
543 );
544 applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32;
545 }
546 }
547}
548
549fn diff_snapshots(
550 old_snapshot: &text::BufferSnapshot,
551 new_snapshot: &text::BufferSnapshot,
552) -> Vec<Edit<u32>> {
553 let mut edits = new_snapshot
554 .edits_since::<Point>(&old_snapshot.version)
555 .map(|edit| point_to_row_edit(edit, old_snapshot.as_rope(), new_snapshot.as_rope()))
556 .peekable();
557 let mut row_edits = Vec::new();
558 while let Some(mut edit) = edits.next() {
559 while let Some(next_edit) = edits.peek() {
560 if edit.old.end >= next_edit.old.start {
561 edit.old.end = next_edit.old.end;
562 edit.new.end = next_edit.new.end;
563 edits.next();
564 } else {
565 break;
566 }
567 }
568 row_edits.push(edit);
569 }
570 row_edits
571}
572
573fn point_to_row_edit(edit: Edit<Point>, old_text: &Rope, new_text: &Rope) -> Edit<u32> {
574 if edit.old.start.column == old_text.line_len(edit.old.start.row)
575 && new_text
576 .chars_at(new_text.point_to_offset(edit.new.start))
577 .next()
578 == Some('\n')
579 && edit.old.start != old_text.max_point()
580 {
581 Edit {
582 old: edit.old.start.row + 1..edit.old.end.row + 1,
583 new: edit.new.start.row + 1..edit.new.end.row + 1,
584 }
585 } else if edit.old.start.column == 0
586 && edit.old.end.column == 0
587 && edit.new.end.column == 0
588 && edit.old.end != old_text.max_point()
589 {
590 Edit {
591 old: edit.old.start.row..edit.old.end.row,
592 new: edit.new.start.row..edit.new.end.row,
593 }
594 } else {
595 Edit {
596 old: edit.old.start.row..edit.old.end.row + 1,
597 new: edit.new.start.row..edit.new.end.row + 1,
598 }
599 }
600}
601
602enum ChangeAuthor {
603 User,
604 Agent,
605}
606
607#[derive(Copy, Clone, Eq, PartialEq)]
608enum TrackedBufferStatus {
609 Created,
610 Modified,
611 Deleted,
612}
613
614struct TrackedBuffer {
615 buffer: Entity<Buffer>,
616 base_text: Rope,
617 unreviewed_changes: Patch<u32>,
618 status: TrackedBufferStatus,
619 version: clock::Global,
620 diff: Entity<BufferDiff>,
621 snapshot: text::BufferSnapshot,
622 diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
623 _open_lsp_handle: OpenLspBufferHandle,
624 _maintain_diff: Task<()>,
625 _subscription: Subscription,
626}
627
628impl TrackedBuffer {
629 fn has_changes(&self, cx: &App) -> bool {
630 self.diff
631 .read(cx)
632 .hunks(&self.buffer.read(cx), cx)
633 .next()
634 .is_some()
635 }
636
637 fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) {
638 self.diff_update
639 .unbounded_send((author, self.buffer.read(cx).text_snapshot()))
640 .ok();
641 }
642}
643
644pub struct ChangedBuffer {
645 pub diff: Entity<BufferDiff>,
646}
647
648#[cfg(test)]
649mod tests {
650 use std::env;
651
652 use super::*;
653 use buffer_diff::DiffHunkStatusKind;
654 use gpui::TestAppContext;
655 use language::Point;
656 use project::{FakeFs, Fs, Project, RemoveOptions};
657 use rand::prelude::*;
658 use serde_json::json;
659 use settings::SettingsStore;
660 use util::{RandomCharIter, path};
661
662 #[ctor::ctor]
663 fn init_logger() {
664 if std::env::var("RUST_LOG").is_ok() {
665 env_logger::init();
666 }
667 }
668
669 fn init_test(cx: &mut TestAppContext) {
670 cx.update(|cx| {
671 let settings_store = SettingsStore::test(cx);
672 cx.set_global(settings_store);
673 language::init(cx);
674 Project::init_settings(cx);
675 });
676 }
677
678 #[gpui::test(iterations = 10)]
679 async fn test_keep_edits(cx: &mut TestAppContext) {
680 init_test(cx);
681
682 let fs = FakeFs::new(cx.executor());
683 let project = Project::test(fs.clone(), [], cx).await;
684 let action_log = cx.new(|_| ActionLog::new(project.clone()));
685 let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
686
687 cx.update(|cx| {
688 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
689 buffer.update(cx, |buffer, cx| {
690 buffer
691 .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
692 .unwrap()
693 });
694 buffer.update(cx, |buffer, cx| {
695 buffer
696 .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
697 .unwrap()
698 });
699 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
700 });
701 cx.run_until_parked();
702 assert_eq!(
703 buffer.read_with(cx, |buffer, _| buffer.text()),
704 "abc\ndEf\nghi\njkl\nmnO"
705 );
706 assert_eq!(
707 unreviewed_hunks(&action_log, cx),
708 vec![(
709 buffer.clone(),
710 vec![
711 HunkStatus {
712 range: Point::new(1, 0)..Point::new(2, 0),
713 diff_status: DiffHunkStatusKind::Modified,
714 old_text: "def\n".into(),
715 },
716 HunkStatus {
717 range: Point::new(4, 0)..Point::new(4, 3),
718 diff_status: DiffHunkStatusKind::Modified,
719 old_text: "mno".into(),
720 }
721 ],
722 )]
723 );
724
725 action_log.update(cx, |log, cx| {
726 log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
727 });
728 cx.run_until_parked();
729 assert_eq!(
730 unreviewed_hunks(&action_log, cx),
731 vec![(
732 buffer.clone(),
733 vec![HunkStatus {
734 range: Point::new(1, 0)..Point::new(2, 0),
735 diff_status: DiffHunkStatusKind::Modified,
736 old_text: "def\n".into(),
737 }],
738 )]
739 );
740
741 action_log.update(cx, |log, cx| {
742 log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
743 });
744 cx.run_until_parked();
745 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
746 }
747
748 #[gpui::test(iterations = 10)]
749 async fn test_deletions(cx: &mut TestAppContext) {
750 init_test(cx);
751
752 let fs = FakeFs::new(cx.executor());
753 let project = Project::test(fs.clone(), [], cx).await;
754 let action_log = cx.new(|_| ActionLog::new(project.clone()));
755 let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno\npqr", cx));
756
757 cx.update(|cx| {
758 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
759 buffer.update(cx, |buffer, cx| {
760 buffer
761 .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx)
762 .unwrap();
763 buffer.finalize_last_transaction();
764 });
765 buffer.update(cx, |buffer, cx| {
766 buffer
767 .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx)
768 .unwrap();
769 buffer.finalize_last_transaction();
770 });
771 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
772 });
773 cx.run_until_parked();
774 assert_eq!(
775 buffer.read_with(cx, |buffer, _| buffer.text()),
776 "abc\nghi\njkl\npqr"
777 );
778 assert_eq!(
779 unreviewed_hunks(&action_log, cx),
780 vec![(
781 buffer.clone(),
782 vec![
783 HunkStatus {
784 range: Point::new(1, 0)..Point::new(1, 0),
785 diff_status: DiffHunkStatusKind::Deleted,
786 old_text: "def\n".into(),
787 },
788 HunkStatus {
789 range: Point::new(3, 0)..Point::new(3, 0),
790 diff_status: DiffHunkStatusKind::Deleted,
791 old_text: "mno\n".into(),
792 }
793 ],
794 )]
795 );
796
797 buffer.update(cx, |buffer, cx| buffer.undo(cx));
798 cx.run_until_parked();
799 assert_eq!(
800 buffer.read_with(cx, |buffer, _| buffer.text()),
801 "abc\nghi\njkl\nmno\npqr"
802 );
803 assert_eq!(
804 unreviewed_hunks(&action_log, cx),
805 vec![(
806 buffer.clone(),
807 vec![HunkStatus {
808 range: Point::new(1, 0)..Point::new(1, 0),
809 diff_status: DiffHunkStatusKind::Deleted,
810 old_text: "def\n".into(),
811 }],
812 )]
813 );
814
815 action_log.update(cx, |log, cx| {
816 log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx)
817 });
818 cx.run_until_parked();
819 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
820 }
821
822 #[gpui::test(iterations = 10)]
823 async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
824 init_test(cx);
825
826 let fs = FakeFs::new(cx.executor());
827 let project = Project::test(fs.clone(), [], cx).await;
828 let action_log = cx.new(|_| ActionLog::new(project.clone()));
829 let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
830
831 cx.update(|cx| {
832 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
833 buffer.update(cx, |buffer, cx| {
834 buffer
835 .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
836 .unwrap()
837 });
838 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
839 });
840 cx.run_until_parked();
841 assert_eq!(
842 buffer.read_with(cx, |buffer, _| buffer.text()),
843 "abc\ndeF\nGHI\njkl\nmno"
844 );
845 assert_eq!(
846 unreviewed_hunks(&action_log, cx),
847 vec![(
848 buffer.clone(),
849 vec![HunkStatus {
850 range: Point::new(1, 0)..Point::new(3, 0),
851 diff_status: DiffHunkStatusKind::Modified,
852 old_text: "def\nghi\n".into(),
853 }],
854 )]
855 );
856
857 buffer.update(cx, |buffer, cx| {
858 buffer.edit(
859 [
860 (Point::new(0, 2)..Point::new(0, 2), "X"),
861 (Point::new(3, 0)..Point::new(3, 0), "Y"),
862 ],
863 None,
864 cx,
865 )
866 });
867 cx.run_until_parked();
868 assert_eq!(
869 buffer.read_with(cx, |buffer, _| buffer.text()),
870 "abXc\ndeF\nGHI\nYjkl\nmno"
871 );
872 assert_eq!(
873 unreviewed_hunks(&action_log, cx),
874 vec![(
875 buffer.clone(),
876 vec![HunkStatus {
877 range: Point::new(1, 0)..Point::new(3, 0),
878 diff_status: DiffHunkStatusKind::Modified,
879 old_text: "def\nghi\n".into(),
880 }],
881 )]
882 );
883
884 buffer.update(cx, |buffer, cx| {
885 buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx)
886 });
887 cx.run_until_parked();
888 assert_eq!(
889 buffer.read_with(cx, |buffer, _| buffer.text()),
890 "abXc\ndZeF\nGHI\nYjkl\nmno"
891 );
892 assert_eq!(
893 unreviewed_hunks(&action_log, cx),
894 vec![(
895 buffer.clone(),
896 vec![HunkStatus {
897 range: Point::new(1, 0)..Point::new(3, 0),
898 diff_status: DiffHunkStatusKind::Modified,
899 old_text: "def\nghi\n".into(),
900 }],
901 )]
902 );
903
904 action_log.update(cx, |log, cx| {
905 log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
906 });
907 cx.run_until_parked();
908 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
909 }
910
911 #[gpui::test(iterations = 10)]
912 async fn test_creating_files(cx: &mut TestAppContext) {
913 init_test(cx);
914
915 let fs = FakeFs::new(cx.executor());
916 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
917 let action_log = cx.new(|_| ActionLog::new(project.clone()));
918
919 let fs = FakeFs::new(cx.executor());
920 fs.insert_tree(path!("/dir"), json!({})).await;
921
922 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
923 let file_path = project
924 .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
925 .unwrap();
926
927 // Simulate file2 being recreated by a tool.
928 let buffer = project
929 .update(cx, |project, cx| project.open_buffer(file_path, cx))
930 .await
931 .unwrap();
932 cx.update(|cx| {
933 buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
934 action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
935 });
936 project
937 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
938 .await
939 .unwrap();
940 cx.run_until_parked();
941 assert_eq!(
942 unreviewed_hunks(&action_log, cx),
943 vec![(
944 buffer.clone(),
945 vec![HunkStatus {
946 range: Point::new(0, 0)..Point::new(0, 5),
947 diff_status: DiffHunkStatusKind::Added,
948 old_text: "".into(),
949 }],
950 )]
951 );
952
953 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx));
954 cx.run_until_parked();
955 assert_eq!(
956 unreviewed_hunks(&action_log, cx),
957 vec![(
958 buffer.clone(),
959 vec![HunkStatus {
960 range: Point::new(0, 0)..Point::new(0, 6),
961 diff_status: DiffHunkStatusKind::Added,
962 old_text: "".into(),
963 }],
964 )]
965 );
966
967 action_log.update(cx, |log, cx| {
968 log.keep_edits_in_range(buffer.clone(), 0..5, cx)
969 });
970 cx.run_until_parked();
971 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
972 }
973
974 #[gpui::test(iterations = 10)]
975 async fn test_deleting_files(cx: &mut TestAppContext) {
976 init_test(cx);
977
978 let fs = FakeFs::new(cx.executor());
979 fs.insert_tree(
980 path!("/dir"),
981 json!({"file1": "lorem\n", "file2": "ipsum\n"}),
982 )
983 .await;
984
985 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
986 let file1_path = project
987 .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
988 .unwrap();
989 let file2_path = project
990 .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
991 .unwrap();
992
993 let action_log = cx.new(|_| ActionLog::new(project.clone()));
994 let buffer1 = project
995 .update(cx, |project, cx| {
996 project.open_buffer(file1_path.clone(), cx)
997 })
998 .await
999 .unwrap();
1000 let buffer2 = project
1001 .update(cx, |project, cx| {
1002 project.open_buffer(file2_path.clone(), cx)
1003 })
1004 .await
1005 .unwrap();
1006
1007 action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
1008 action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
1009 project
1010 .update(cx, |project, cx| {
1011 project.delete_file(file1_path.clone(), false, cx)
1012 })
1013 .unwrap()
1014 .await
1015 .unwrap();
1016 project
1017 .update(cx, |project, cx| {
1018 project.delete_file(file2_path.clone(), false, cx)
1019 })
1020 .unwrap()
1021 .await
1022 .unwrap();
1023 cx.run_until_parked();
1024 assert_eq!(
1025 unreviewed_hunks(&action_log, cx),
1026 vec![
1027 (
1028 buffer1.clone(),
1029 vec![HunkStatus {
1030 range: Point::new(0, 0)..Point::new(0, 0),
1031 diff_status: DiffHunkStatusKind::Deleted,
1032 old_text: "lorem\n".into(),
1033 }]
1034 ),
1035 (
1036 buffer2.clone(),
1037 vec![HunkStatus {
1038 range: Point::new(0, 0)..Point::new(0, 0),
1039 diff_status: DiffHunkStatusKind::Deleted,
1040 old_text: "ipsum\n".into(),
1041 }],
1042 )
1043 ]
1044 );
1045
1046 // Simulate file1 being recreated externally.
1047 fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
1048 .await;
1049
1050 // Simulate file2 being recreated by a tool.
1051 let buffer2 = project
1052 .update(cx, |project, cx| project.open_buffer(file2_path, cx))
1053 .await
1054 .unwrap();
1055 buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
1056 action_log.update(cx, |log, cx| log.will_create_buffer(buffer2.clone(), cx));
1057 project
1058 .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
1059 .await
1060 .unwrap();
1061
1062 cx.run_until_parked();
1063 assert_eq!(
1064 unreviewed_hunks(&action_log, cx),
1065 vec![(
1066 buffer2.clone(),
1067 vec![HunkStatus {
1068 range: Point::new(0, 0)..Point::new(0, 5),
1069 diff_status: DiffHunkStatusKind::Modified,
1070 old_text: "ipsum\n".into(),
1071 }],
1072 )]
1073 );
1074
1075 // Simulate file2 being deleted externally.
1076 fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
1077 .await
1078 .unwrap();
1079 cx.run_until_parked();
1080 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1081 }
1082
1083 #[gpui::test(iterations = 10)]
1084 async fn test_reject_edits(cx: &mut TestAppContext) {
1085 init_test(cx);
1086
1087 let fs = FakeFs::new(cx.executor());
1088 fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
1089 .await;
1090 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1091 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1092 let file_path = project
1093 .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1094 .unwrap();
1095 let buffer = project
1096 .update(cx, |project, cx| project.open_buffer(file_path, cx))
1097 .await
1098 .unwrap();
1099
1100 cx.update(|cx| {
1101 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1102 buffer.update(cx, |buffer, cx| {
1103 buffer
1104 .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
1105 .unwrap()
1106 });
1107 buffer.update(cx, |buffer, cx| {
1108 buffer
1109 .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
1110 .unwrap()
1111 });
1112 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1113 });
1114 cx.run_until_parked();
1115 assert_eq!(
1116 buffer.read_with(cx, |buffer, _| buffer.text()),
1117 "abc\ndE\nXYZf\nghi\njkl\nmnO"
1118 );
1119 assert_eq!(
1120 unreviewed_hunks(&action_log, cx),
1121 vec![(
1122 buffer.clone(),
1123 vec![
1124 HunkStatus {
1125 range: Point::new(1, 0)..Point::new(3, 0),
1126 diff_status: DiffHunkStatusKind::Modified,
1127 old_text: "def\n".into(),
1128 },
1129 HunkStatus {
1130 range: Point::new(5, 0)..Point::new(5, 3),
1131 diff_status: DiffHunkStatusKind::Modified,
1132 old_text: "mno".into(),
1133 }
1134 ],
1135 )]
1136 );
1137
1138 action_log
1139 .update(cx, |log, cx| {
1140 log.reject_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
1141 })
1142 .await
1143 .unwrap();
1144 cx.run_until_parked();
1145 assert_eq!(
1146 buffer.read_with(cx, |buffer, _| buffer.text()),
1147 "abc\ndef\nghi\njkl\nmnO"
1148 );
1149 assert_eq!(
1150 unreviewed_hunks(&action_log, cx),
1151 vec![(
1152 buffer.clone(),
1153 vec![HunkStatus {
1154 range: Point::new(4, 0)..Point::new(4, 3),
1155 diff_status: DiffHunkStatusKind::Modified,
1156 old_text: "mno".into(),
1157 }],
1158 )]
1159 );
1160
1161 action_log
1162 .update(cx, |log, cx| {
1163 log.reject_edits_in_range(buffer.clone(), Point::new(4, 0)..Point::new(4, 0), cx)
1164 })
1165 .await
1166 .unwrap();
1167 cx.run_until_parked();
1168 assert_eq!(
1169 buffer.read_with(cx, |buffer, _| buffer.text()),
1170 "abc\ndef\nghi\njkl\nmno"
1171 );
1172 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1173 }
1174
1175 #[gpui::test(iterations = 10)]
1176 async fn test_reject_deleted_file(cx: &mut TestAppContext) {
1177 init_test(cx);
1178
1179 let fs = FakeFs::new(cx.executor());
1180 fs.insert_tree(path!("/dir"), json!({"file": "content"}))
1181 .await;
1182 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1183 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1184 let file_path = project
1185 .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1186 .unwrap();
1187 let buffer = project
1188 .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
1189 .await
1190 .unwrap();
1191
1192 cx.update(|cx| {
1193 action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx));
1194 });
1195 project
1196 .update(cx, |project, cx| {
1197 project.delete_file(file_path.clone(), false, cx)
1198 })
1199 .unwrap()
1200 .await
1201 .unwrap();
1202 cx.run_until_parked();
1203 assert!(!fs.is_file(path!("/dir/file").as_ref()).await);
1204 assert_eq!(
1205 unreviewed_hunks(&action_log, cx),
1206 vec![(
1207 buffer.clone(),
1208 vec![HunkStatus {
1209 range: Point::new(0, 0)..Point::new(0, 0),
1210 diff_status: DiffHunkStatusKind::Deleted,
1211 old_text: "content".into(),
1212 }]
1213 )]
1214 );
1215
1216 action_log
1217 .update(cx, |log, cx| {
1218 log.reject_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(0, 0), cx)
1219 })
1220 .await
1221 .unwrap();
1222 cx.run_until_parked();
1223 assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "content");
1224 assert!(fs.is_file(path!("/dir/file").as_ref()).await);
1225 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1226 }
1227
1228 #[gpui::test(iterations = 10)]
1229 async fn test_reject_created_file(cx: &mut TestAppContext) {
1230 init_test(cx);
1231
1232 let fs = FakeFs::new(cx.executor());
1233 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1234 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1235 let file_path = project
1236 .read_with(cx, |project, cx| {
1237 project.find_project_path("dir/new_file", cx)
1238 })
1239 .unwrap();
1240
1241 let buffer = project
1242 .update(cx, |project, cx| project.open_buffer(file_path, cx))
1243 .await
1244 .unwrap();
1245 cx.update(|cx| {
1246 buffer.update(cx, |buffer, cx| buffer.set_text("content", cx));
1247 action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
1248 });
1249 project
1250 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1251 .await
1252 .unwrap();
1253 assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
1254 cx.run_until_parked();
1255 assert_eq!(
1256 unreviewed_hunks(&action_log, cx),
1257 vec![(
1258 buffer.clone(),
1259 vec![HunkStatus {
1260 range: Point::new(0, 0)..Point::new(0, 7),
1261 diff_status: DiffHunkStatusKind::Added,
1262 old_text: "".into(),
1263 }],
1264 )]
1265 );
1266
1267 action_log
1268 .update(cx, |log, cx| {
1269 log.reject_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(0, 11), cx)
1270 })
1271 .await
1272 .unwrap();
1273 cx.run_until_parked();
1274 assert!(!fs.is_file(path!("/dir/new_file").as_ref()).await);
1275 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1276 }
1277
1278 #[gpui::test(iterations = 100)]
1279 async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
1280 init_test(cx);
1281
1282 let operations = env::var("OPERATIONS")
1283 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
1284 .unwrap_or(20);
1285
1286 let text = RandomCharIter::new(&mut rng).take(50).collect::<String>();
1287 let fs = FakeFs::new(cx.executor());
1288 fs.insert_tree(path!("/dir"), json!({"file": text})).await;
1289 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1290 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1291 let file_path = project
1292 .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1293 .unwrap();
1294 let buffer = project
1295 .update(cx, |project, cx| project.open_buffer(file_path, cx))
1296 .await
1297 .unwrap();
1298
1299 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1300
1301 for _ in 0..operations {
1302 match rng.gen_range(0..100) {
1303 0..25 => {
1304 action_log.update(cx, |log, cx| {
1305 let range = buffer.read(cx).random_byte_range(0, &mut rng);
1306 log::info!("keeping edits in range {:?}", range);
1307 log.keep_edits_in_range(buffer.clone(), range, cx)
1308 });
1309 }
1310 25..50 => {
1311 action_log
1312 .update(cx, |log, cx| {
1313 let range = buffer.read(cx).random_byte_range(0, &mut rng);
1314 log::info!("rejecting edits in range {:?}", range);
1315 log.reject_edits_in_range(buffer.clone(), range, cx)
1316 })
1317 .await
1318 .unwrap();
1319 }
1320 _ => {
1321 let is_agent_change = rng.gen_bool(0.5);
1322 if is_agent_change {
1323 log::info!("agent edit");
1324 } else {
1325 log::info!("user edit");
1326 }
1327 cx.update(|cx| {
1328 buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
1329 if is_agent_change {
1330 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1331 }
1332 });
1333 }
1334 }
1335
1336 if rng.gen_bool(0.2) {
1337 quiesce(&action_log, &buffer, cx);
1338 }
1339 }
1340
1341 quiesce(&action_log, &buffer, cx);
1342
1343 fn quiesce(
1344 action_log: &Entity<ActionLog>,
1345 buffer: &Entity<Buffer>,
1346 cx: &mut TestAppContext,
1347 ) {
1348 log::info!("quiescing...");
1349 cx.run_until_parked();
1350 action_log.update(cx, |log, cx| {
1351 let tracked_buffer = log.track_buffer(buffer.clone(), false, cx);
1352 let mut old_text = tracked_buffer.base_text.clone();
1353 let new_text = buffer.read(cx).as_rope();
1354 for edit in tracked_buffer.unreviewed_changes.edits() {
1355 let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
1356 let old_end = old_text.point_to_offset(cmp::min(
1357 Point::new(edit.new.start + edit.old_len(), 0),
1358 old_text.max_point(),
1359 ));
1360 old_text.replace(
1361 old_start..old_end,
1362 &new_text.slice_rows(edit.new.clone()).to_string(),
1363 );
1364 }
1365 pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
1366 })
1367 }
1368 }
1369
1370 #[derive(Debug, Clone, PartialEq, Eq)]
1371 struct HunkStatus {
1372 range: Range<Point>,
1373 diff_status: DiffHunkStatusKind,
1374 old_text: String,
1375 }
1376
1377 fn unreviewed_hunks(
1378 action_log: &Entity<ActionLog>,
1379 cx: &TestAppContext,
1380 ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
1381 cx.read(|cx| {
1382 action_log
1383 .read(cx)
1384 .changed_buffers(cx)
1385 .into_iter()
1386 .map(|(buffer, diff)| {
1387 let snapshot = buffer.read(cx).snapshot();
1388 (
1389 buffer,
1390 diff.read(cx)
1391 .hunks(&snapshot, cx)
1392 .map(|hunk| HunkStatus {
1393 diff_status: hunk.status().kind,
1394 range: hunk.range,
1395 old_text: diff
1396 .read(cx)
1397 .base_text()
1398 .text_for_range(hunk.diff_base_byte_range)
1399 .collect(),
1400 })
1401 .collect(),
1402 )
1403 })
1404 .collect()
1405 })
1406 }
1407}