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