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