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(
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_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.track_buffer(buffer.clone(), false, 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 if std::env::var("RUST_LOG").is_ok() {
680 env_logger::init();
681 }
682 }
683
684 fn init_test(cx: &mut TestAppContext) {
685 cx.update(|cx| {
686 let settings_store = SettingsStore::test(cx);
687 cx.set_global(settings_store);
688 language::init(cx);
689 Project::init_settings(cx);
690 });
691 }
692
693 #[gpui::test(iterations = 10)]
694 async fn test_keep_edits(cx: &mut TestAppContext) {
695 init_test(cx);
696
697 let fs = FakeFs::new(cx.executor());
698 let project = Project::test(fs.clone(), [], cx).await;
699 let action_log = cx.new(|_| ActionLog::new(project.clone()));
700 let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
701
702 cx.update(|cx| {
703 action_log.update(cx, |log, cx| log.buffer_read(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 let project = Project::test(fs.clone(), [], cx).await;
769 let action_log = cx.new(|_| ActionLog::new(project.clone()));
770 let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno\npqr", cx));
771
772 cx.update(|cx| {
773 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
774 buffer.update(cx, |buffer, cx| {
775 buffer
776 .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx)
777 .unwrap();
778 buffer.finalize_last_transaction();
779 });
780 buffer.update(cx, |buffer, cx| {
781 buffer
782 .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx)
783 .unwrap();
784 buffer.finalize_last_transaction();
785 });
786 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
787 });
788 cx.run_until_parked();
789 assert_eq!(
790 buffer.read_with(cx, |buffer, _| buffer.text()),
791 "abc\nghi\njkl\npqr"
792 );
793 assert_eq!(
794 unreviewed_hunks(&action_log, cx),
795 vec![(
796 buffer.clone(),
797 vec![
798 HunkStatus {
799 range: Point::new(1, 0)..Point::new(1, 0),
800 diff_status: DiffHunkStatusKind::Deleted,
801 old_text: "def\n".into(),
802 },
803 HunkStatus {
804 range: Point::new(3, 0)..Point::new(3, 0),
805 diff_status: DiffHunkStatusKind::Deleted,
806 old_text: "mno\n".into(),
807 }
808 ],
809 )]
810 );
811
812 buffer.update(cx, |buffer, cx| buffer.undo(cx));
813 cx.run_until_parked();
814 assert_eq!(
815 buffer.read_with(cx, |buffer, _| buffer.text()),
816 "abc\nghi\njkl\nmno\npqr"
817 );
818 assert_eq!(
819 unreviewed_hunks(&action_log, cx),
820 vec![(
821 buffer.clone(),
822 vec![HunkStatus {
823 range: Point::new(1, 0)..Point::new(1, 0),
824 diff_status: DiffHunkStatusKind::Deleted,
825 old_text: "def\n".into(),
826 }],
827 )]
828 );
829
830 action_log.update(cx, |log, cx| {
831 log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx)
832 });
833 cx.run_until_parked();
834 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
835 }
836
837 #[gpui::test(iterations = 10)]
838 async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
839 init_test(cx);
840
841 let fs = FakeFs::new(cx.executor());
842 let project = Project::test(fs.clone(), [], cx).await;
843 let action_log = cx.new(|_| ActionLog::new(project.clone()));
844 let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
845
846 cx.update(|cx| {
847 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
848 buffer.update(cx, |buffer, cx| {
849 buffer
850 .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
851 .unwrap()
852 });
853 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
854 });
855 cx.run_until_parked();
856 assert_eq!(
857 buffer.read_with(cx, |buffer, _| buffer.text()),
858 "abc\ndeF\nGHI\njkl\nmno"
859 );
860 assert_eq!(
861 unreviewed_hunks(&action_log, cx),
862 vec![(
863 buffer.clone(),
864 vec![HunkStatus {
865 range: Point::new(1, 0)..Point::new(3, 0),
866 diff_status: DiffHunkStatusKind::Modified,
867 old_text: "def\nghi\n".into(),
868 }],
869 )]
870 );
871
872 buffer.update(cx, |buffer, cx| {
873 buffer.edit(
874 [
875 (Point::new(0, 2)..Point::new(0, 2), "X"),
876 (Point::new(3, 0)..Point::new(3, 0), "Y"),
877 ],
878 None,
879 cx,
880 )
881 });
882 cx.run_until_parked();
883 assert_eq!(
884 buffer.read_with(cx, |buffer, _| buffer.text()),
885 "abXc\ndeF\nGHI\nYjkl\nmno"
886 );
887 assert_eq!(
888 unreviewed_hunks(&action_log, cx),
889 vec![(
890 buffer.clone(),
891 vec![HunkStatus {
892 range: Point::new(1, 0)..Point::new(3, 0),
893 diff_status: DiffHunkStatusKind::Modified,
894 old_text: "def\nghi\n".into(),
895 }],
896 )]
897 );
898
899 buffer.update(cx, |buffer, cx| {
900 buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx)
901 });
902 cx.run_until_parked();
903 assert_eq!(
904 buffer.read_with(cx, |buffer, _| buffer.text()),
905 "abXc\ndZeF\nGHI\nYjkl\nmno"
906 );
907 assert_eq!(
908 unreviewed_hunks(&action_log, cx),
909 vec![(
910 buffer.clone(),
911 vec![HunkStatus {
912 range: Point::new(1, 0)..Point::new(3, 0),
913 diff_status: DiffHunkStatusKind::Modified,
914 old_text: "def\nghi\n".into(),
915 }],
916 )]
917 );
918
919 action_log.update(cx, |log, cx| {
920 log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
921 });
922 cx.run_until_parked();
923 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
924 }
925
926 #[gpui::test(iterations = 10)]
927 async fn test_creating_files(cx: &mut TestAppContext) {
928 init_test(cx);
929
930 let fs = FakeFs::new(cx.executor());
931 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
932 let action_log = cx.new(|_| ActionLog::new(project.clone()));
933
934 let fs = FakeFs::new(cx.executor());
935 fs.insert_tree(path!("/dir"), json!({})).await;
936
937 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
938 let file_path = project
939 .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
940 .unwrap();
941
942 // Simulate file2 being recreated by a tool.
943 let buffer = project
944 .update(cx, |project, cx| project.open_buffer(file_path, cx))
945 .await
946 .unwrap();
947 cx.update(|cx| {
948 buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
949 action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
950 });
951 project
952 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
953 .await
954 .unwrap();
955 cx.run_until_parked();
956 assert_eq!(
957 unreviewed_hunks(&action_log, cx),
958 vec![(
959 buffer.clone(),
960 vec![HunkStatus {
961 range: Point::new(0, 0)..Point::new(0, 5),
962 diff_status: DiffHunkStatusKind::Added,
963 old_text: "".into(),
964 }],
965 )]
966 );
967
968 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx));
969 cx.run_until_parked();
970 assert_eq!(
971 unreviewed_hunks(&action_log, cx),
972 vec![(
973 buffer.clone(),
974 vec![HunkStatus {
975 range: Point::new(0, 0)..Point::new(0, 6),
976 diff_status: DiffHunkStatusKind::Added,
977 old_text: "".into(),
978 }],
979 )]
980 );
981
982 action_log.update(cx, |log, cx| {
983 log.keep_edits_in_range(buffer.clone(), 0..5, cx)
984 });
985 cx.run_until_parked();
986 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
987 }
988
989 #[gpui::test(iterations = 10)]
990 async fn test_deleting_files(cx: &mut TestAppContext) {
991 init_test(cx);
992
993 let fs = FakeFs::new(cx.executor());
994 fs.insert_tree(
995 path!("/dir"),
996 json!({"file1": "lorem\n", "file2": "ipsum\n"}),
997 )
998 .await;
999
1000 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1001 let file1_path = project
1002 .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
1003 .unwrap();
1004 let file2_path = project
1005 .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
1006 .unwrap();
1007
1008 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1009 let buffer1 = project
1010 .update(cx, |project, cx| {
1011 project.open_buffer(file1_path.clone(), cx)
1012 })
1013 .await
1014 .unwrap();
1015 let buffer2 = project
1016 .update(cx, |project, cx| {
1017 project.open_buffer(file2_path.clone(), cx)
1018 })
1019 .await
1020 .unwrap();
1021
1022 action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
1023 action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
1024 project
1025 .update(cx, |project, cx| {
1026 project.delete_file(file1_path.clone(), false, cx)
1027 })
1028 .unwrap()
1029 .await
1030 .unwrap();
1031 project
1032 .update(cx, |project, cx| {
1033 project.delete_file(file2_path.clone(), false, cx)
1034 })
1035 .unwrap()
1036 .await
1037 .unwrap();
1038 cx.run_until_parked();
1039 assert_eq!(
1040 unreviewed_hunks(&action_log, cx),
1041 vec![
1042 (
1043 buffer1.clone(),
1044 vec![HunkStatus {
1045 range: Point::new(0, 0)..Point::new(0, 0),
1046 diff_status: DiffHunkStatusKind::Deleted,
1047 old_text: "lorem\n".into(),
1048 }]
1049 ),
1050 (
1051 buffer2.clone(),
1052 vec![HunkStatus {
1053 range: Point::new(0, 0)..Point::new(0, 0),
1054 diff_status: DiffHunkStatusKind::Deleted,
1055 old_text: "ipsum\n".into(),
1056 }],
1057 )
1058 ]
1059 );
1060
1061 // Simulate file1 being recreated externally.
1062 fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
1063 .await;
1064
1065 // Simulate file2 being recreated by a tool.
1066 let buffer2 = project
1067 .update(cx, |project, cx| project.open_buffer(file2_path, cx))
1068 .await
1069 .unwrap();
1070 buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
1071 action_log.update(cx, |log, cx| log.will_create_buffer(buffer2.clone(), cx));
1072 project
1073 .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
1074 .await
1075 .unwrap();
1076
1077 cx.run_until_parked();
1078 assert_eq!(
1079 unreviewed_hunks(&action_log, cx),
1080 vec![(
1081 buffer2.clone(),
1082 vec![HunkStatus {
1083 range: Point::new(0, 0)..Point::new(0, 5),
1084 diff_status: DiffHunkStatusKind::Modified,
1085 old_text: "ipsum\n".into(),
1086 }],
1087 )]
1088 );
1089
1090 // Simulate file2 being deleted externally.
1091 fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
1092 .await
1093 .unwrap();
1094 cx.run_until_parked();
1095 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1096 }
1097
1098 #[gpui::test(iterations = 10)]
1099 async fn test_reject_edits(cx: &mut TestAppContext) {
1100 init_test(cx);
1101
1102 let fs = FakeFs::new(cx.executor());
1103 fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
1104 .await;
1105 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1106 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1107 let file_path = project
1108 .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1109 .unwrap();
1110 let buffer = project
1111 .update(cx, |project, cx| project.open_buffer(file_path, cx))
1112 .await
1113 .unwrap();
1114
1115 cx.update(|cx| {
1116 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1117 buffer.update(cx, |buffer, cx| {
1118 buffer
1119 .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
1120 .unwrap()
1121 });
1122 buffer.update(cx, |buffer, cx| {
1123 buffer
1124 .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
1125 .unwrap()
1126 });
1127 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1128 });
1129 cx.run_until_parked();
1130 assert_eq!(
1131 buffer.read_with(cx, |buffer, _| buffer.text()),
1132 "abc\ndE\nXYZf\nghi\njkl\nmnO"
1133 );
1134 assert_eq!(
1135 unreviewed_hunks(&action_log, cx),
1136 vec![(
1137 buffer.clone(),
1138 vec![
1139 HunkStatus {
1140 range: Point::new(1, 0)..Point::new(3, 0),
1141 diff_status: DiffHunkStatusKind::Modified,
1142 old_text: "def\n".into(),
1143 },
1144 HunkStatus {
1145 range: Point::new(5, 0)..Point::new(5, 3),
1146 diff_status: DiffHunkStatusKind::Modified,
1147 old_text: "mno".into(),
1148 }
1149 ],
1150 )]
1151 );
1152
1153 // If the rejected range doesn't overlap with any hunk, we ignore it.
1154 action_log
1155 .update(cx, |log, cx| {
1156 log.reject_edits_in_ranges(
1157 buffer.clone(),
1158 vec![Point::new(4, 0)..Point::new(4, 0)],
1159 cx,
1160 )
1161 })
1162 .await
1163 .unwrap();
1164 cx.run_until_parked();
1165 assert_eq!(
1166 buffer.read_with(cx, |buffer, _| buffer.text()),
1167 "abc\ndE\nXYZf\nghi\njkl\nmnO"
1168 );
1169 assert_eq!(
1170 unreviewed_hunks(&action_log, cx),
1171 vec![(
1172 buffer.clone(),
1173 vec![
1174 HunkStatus {
1175 range: Point::new(1, 0)..Point::new(3, 0),
1176 diff_status: DiffHunkStatusKind::Modified,
1177 old_text: "def\n".into(),
1178 },
1179 HunkStatus {
1180 range: Point::new(5, 0)..Point::new(5, 3),
1181 diff_status: DiffHunkStatusKind::Modified,
1182 old_text: "mno".into(),
1183 }
1184 ],
1185 )]
1186 );
1187
1188 action_log
1189 .update(cx, |log, cx| {
1190 log.reject_edits_in_ranges(
1191 buffer.clone(),
1192 vec![Point::new(0, 0)..Point::new(1, 0)],
1193 cx,
1194 )
1195 })
1196 .await
1197 .unwrap();
1198 cx.run_until_parked();
1199 assert_eq!(
1200 buffer.read_with(cx, |buffer, _| buffer.text()),
1201 "abc\ndef\nghi\njkl\nmnO"
1202 );
1203 assert_eq!(
1204 unreviewed_hunks(&action_log, cx),
1205 vec![(
1206 buffer.clone(),
1207 vec![HunkStatus {
1208 range: Point::new(4, 0)..Point::new(4, 3),
1209 diff_status: DiffHunkStatusKind::Modified,
1210 old_text: "mno".into(),
1211 }],
1212 )]
1213 );
1214
1215 action_log
1216 .update(cx, |log, cx| {
1217 log.reject_edits_in_ranges(
1218 buffer.clone(),
1219 vec![Point::new(4, 0)..Point::new(4, 0)],
1220 cx,
1221 )
1222 })
1223 .await
1224 .unwrap();
1225 cx.run_until_parked();
1226 assert_eq!(
1227 buffer.read_with(cx, |buffer, _| buffer.text()),
1228 "abc\ndef\nghi\njkl\nmno"
1229 );
1230 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1231 }
1232
1233 #[gpui::test(iterations = 10)]
1234 async fn test_reject_multiple_edits(cx: &mut TestAppContext) {
1235 init_test(cx);
1236
1237 let fs = FakeFs::new(cx.executor());
1238 fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
1239 .await;
1240 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1241 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1242 let file_path = project
1243 .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1244 .unwrap();
1245 let buffer = project
1246 .update(cx, |project, cx| project.open_buffer(file_path, cx))
1247 .await
1248 .unwrap();
1249
1250 cx.update(|cx| {
1251 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1252 buffer.update(cx, |buffer, cx| {
1253 buffer
1254 .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
1255 .unwrap()
1256 });
1257 buffer.update(cx, |buffer, cx| {
1258 buffer
1259 .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
1260 .unwrap()
1261 });
1262 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1263 });
1264 cx.run_until_parked();
1265 assert_eq!(
1266 buffer.read_with(cx, |buffer, _| buffer.text()),
1267 "abc\ndE\nXYZf\nghi\njkl\nmnO"
1268 );
1269 assert_eq!(
1270 unreviewed_hunks(&action_log, cx),
1271 vec![(
1272 buffer.clone(),
1273 vec![
1274 HunkStatus {
1275 range: Point::new(1, 0)..Point::new(3, 0),
1276 diff_status: DiffHunkStatusKind::Modified,
1277 old_text: "def\n".into(),
1278 },
1279 HunkStatus {
1280 range: Point::new(5, 0)..Point::new(5, 3),
1281 diff_status: DiffHunkStatusKind::Modified,
1282 old_text: "mno".into(),
1283 }
1284 ],
1285 )]
1286 );
1287
1288 action_log.update(cx, |log, cx| {
1289 let range_1 = buffer.read(cx).anchor_before(Point::new(0, 0))
1290 ..buffer.read(cx).anchor_before(Point::new(1, 0));
1291 let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0))
1292 ..buffer.read(cx).anchor_before(Point::new(5, 3));
1293
1294 log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], cx)
1295 .detach();
1296 assert_eq!(
1297 buffer.read_with(cx, |buffer, _| buffer.text()),
1298 "abc\ndef\nghi\njkl\nmno"
1299 );
1300 });
1301 cx.run_until_parked();
1302 assert_eq!(
1303 buffer.read_with(cx, |buffer, _| buffer.text()),
1304 "abc\ndef\nghi\njkl\nmno"
1305 );
1306 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1307 }
1308
1309 #[gpui::test(iterations = 10)]
1310 async fn test_reject_deleted_file(cx: &mut TestAppContext) {
1311 init_test(cx);
1312
1313 let fs = FakeFs::new(cx.executor());
1314 fs.insert_tree(path!("/dir"), json!({"file": "content"}))
1315 .await;
1316 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1317 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1318 let file_path = project
1319 .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1320 .unwrap();
1321 let buffer = project
1322 .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
1323 .await
1324 .unwrap();
1325
1326 cx.update(|cx| {
1327 action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx));
1328 });
1329 project
1330 .update(cx, |project, cx| {
1331 project.delete_file(file_path.clone(), false, cx)
1332 })
1333 .unwrap()
1334 .await
1335 .unwrap();
1336 cx.run_until_parked();
1337 assert!(!fs.is_file(path!("/dir/file").as_ref()).await);
1338 assert_eq!(
1339 unreviewed_hunks(&action_log, cx),
1340 vec![(
1341 buffer.clone(),
1342 vec![HunkStatus {
1343 range: Point::new(0, 0)..Point::new(0, 0),
1344 diff_status: DiffHunkStatusKind::Deleted,
1345 old_text: "content".into(),
1346 }]
1347 )]
1348 );
1349
1350 action_log
1351 .update(cx, |log, cx| {
1352 log.reject_edits_in_ranges(
1353 buffer.clone(),
1354 vec![Point::new(0, 0)..Point::new(0, 0)],
1355 cx,
1356 )
1357 })
1358 .await
1359 .unwrap();
1360 cx.run_until_parked();
1361 assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "content");
1362 assert!(fs.is_file(path!("/dir/file").as_ref()).await);
1363 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1364 }
1365
1366 #[gpui::test(iterations = 10)]
1367 async fn test_reject_created_file(cx: &mut TestAppContext) {
1368 init_test(cx);
1369
1370 let fs = FakeFs::new(cx.executor());
1371 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1372 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1373 let file_path = project
1374 .read_with(cx, |project, cx| {
1375 project.find_project_path("dir/new_file", cx)
1376 })
1377 .unwrap();
1378
1379 let buffer = project
1380 .update(cx, |project, cx| project.open_buffer(file_path, cx))
1381 .await
1382 .unwrap();
1383 cx.update(|cx| {
1384 buffer.update(cx, |buffer, cx| buffer.set_text("content", cx));
1385 action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
1386 });
1387 project
1388 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1389 .await
1390 .unwrap();
1391 assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
1392 cx.run_until_parked();
1393 assert_eq!(
1394 unreviewed_hunks(&action_log, cx),
1395 vec![(
1396 buffer.clone(),
1397 vec![HunkStatus {
1398 range: Point::new(0, 0)..Point::new(0, 7),
1399 diff_status: DiffHunkStatusKind::Added,
1400 old_text: "".into(),
1401 }],
1402 )]
1403 );
1404
1405 action_log
1406 .update(cx, |log, cx| {
1407 log.reject_edits_in_ranges(
1408 buffer.clone(),
1409 vec![Point::new(0, 0)..Point::new(0, 11)],
1410 cx,
1411 )
1412 })
1413 .await
1414 .unwrap();
1415 cx.run_until_parked();
1416 assert!(!fs.is_file(path!("/dir/new_file").as_ref()).await);
1417 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1418 }
1419
1420 #[gpui::test(iterations = 100)]
1421 async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
1422 init_test(cx);
1423
1424 let operations = env::var("OPERATIONS")
1425 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
1426 .unwrap_or(20);
1427
1428 let text = RandomCharIter::new(&mut rng).take(50).collect::<String>();
1429 let fs = FakeFs::new(cx.executor());
1430 fs.insert_tree(path!("/dir"), json!({"file": text})).await;
1431 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1432 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1433 let file_path = project
1434 .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1435 .unwrap();
1436 let buffer = project
1437 .update(cx, |project, cx| project.open_buffer(file_path, cx))
1438 .await
1439 .unwrap();
1440
1441 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1442
1443 for _ in 0..operations {
1444 match rng.gen_range(0..100) {
1445 0..25 => {
1446 action_log.update(cx, |log, cx| {
1447 let range = buffer.read(cx).random_byte_range(0, &mut rng);
1448 log::info!("keeping edits in range {:?}", range);
1449 log.keep_edits_in_range(buffer.clone(), range, cx)
1450 });
1451 }
1452 25..50 => {
1453 action_log
1454 .update(cx, |log, cx| {
1455 let range = buffer.read(cx).random_byte_range(0, &mut rng);
1456 log::info!("rejecting edits in range {:?}", range);
1457 log.reject_edits_in_ranges(buffer.clone(), vec![range], cx)
1458 })
1459 .await
1460 .unwrap();
1461 }
1462 _ => {
1463 let is_agent_change = rng.gen_bool(0.5);
1464 if is_agent_change {
1465 log::info!("agent edit");
1466 } else {
1467 log::info!("user edit");
1468 }
1469 cx.update(|cx| {
1470 buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
1471 if is_agent_change {
1472 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1473 }
1474 });
1475 }
1476 }
1477
1478 if rng.gen_bool(0.2) {
1479 quiesce(&action_log, &buffer, cx);
1480 }
1481 }
1482
1483 quiesce(&action_log, &buffer, cx);
1484
1485 fn quiesce(
1486 action_log: &Entity<ActionLog>,
1487 buffer: &Entity<Buffer>,
1488 cx: &mut TestAppContext,
1489 ) {
1490 log::info!("quiescing...");
1491 cx.run_until_parked();
1492 action_log.update(cx, |log, cx| {
1493 let tracked_buffer = log.track_buffer(buffer.clone(), false, cx);
1494 let mut old_text = tracked_buffer.base_text.clone();
1495 let new_text = buffer.read(cx).as_rope();
1496 for edit in tracked_buffer.unreviewed_changes.edits() {
1497 let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
1498 let old_end = old_text.point_to_offset(cmp::min(
1499 Point::new(edit.new.start + edit.old_len(), 0),
1500 old_text.max_point(),
1501 ));
1502 old_text.replace(
1503 old_start..old_end,
1504 &new_text.slice_rows(edit.new.clone()).to_string(),
1505 );
1506 }
1507 pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
1508 })
1509 }
1510 }
1511
1512 #[derive(Debug, Clone, PartialEq, Eq)]
1513 struct HunkStatus {
1514 range: Range<Point>,
1515 diff_status: DiffHunkStatusKind,
1516 old_text: String,
1517 }
1518
1519 fn unreviewed_hunks(
1520 action_log: &Entity<ActionLog>,
1521 cx: &TestAppContext,
1522 ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
1523 cx.read(|cx| {
1524 action_log
1525 .read(cx)
1526 .changed_buffers(cx)
1527 .into_iter()
1528 .map(|(buffer, diff)| {
1529 let snapshot = buffer.read(cx).snapshot();
1530 (
1531 buffer,
1532 diff.read(cx)
1533 .hunks(&snapshot, cx)
1534 .map(|hunk| HunkStatus {
1535 diff_status: hunk.status().kind,
1536 range: hunk.range,
1537 old_text: diff
1538 .read(cx)
1539 .base_text()
1540 .text_for_range(hunk.diff_base_byte_range)
1541 .collect(),
1542 })
1543 .collect(),
1544 )
1545 })
1546 .collect()
1547 })
1548 }
1549}