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