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