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};
7use std::{cmp, ops::Range, sync::Arc};
8use text::{Edit, Patch, Rope};
9use util::RangeExt;
10
11/// Tracks actions performed by tools in a thread
12pub struct ActionLog {
13 /// Buffers that we want to notify the model about when they change.
14 tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
15 /// Has the model edited a file since it last checked diagnostics?
16 edited_since_project_diagnostics_check: bool,
17}
18
19impl ActionLog {
20 /// Creates a new, empty action log.
21 pub fn new() -> Self {
22 Self {
23 tracked_buffers: BTreeMap::default(),
24 edited_since_project_diagnostics_check: false,
25 }
26 }
27
28 /// Notifies a diagnostics check
29 pub fn checked_project_diagnostics(&mut self) {
30 self.edited_since_project_diagnostics_check = false;
31 }
32
33 /// Returns true if any files have been edited since the last project diagnostics check
34 pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
35 self.edited_since_project_diagnostics_check
36 }
37
38 fn track_buffer(
39 &mut self,
40 buffer: Entity<Buffer>,
41 created: bool,
42 cx: &mut Context<Self>,
43 ) -> &mut TrackedBuffer {
44 let tracked_buffer = self
45 .tracked_buffers
46 .entry(buffer.clone())
47 .or_insert_with(|| {
48 let text_snapshot = buffer.read(cx).text_snapshot();
49 let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
50 let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
51 let base_text;
52 let status;
53 let unreviewed_changes;
54 if created {
55 base_text = Rope::default();
56 status = TrackedBufferStatus::Created;
57 unreviewed_changes = Patch::new(vec![Edit {
58 old: 0..1,
59 new: 0..text_snapshot.max_point().row + 1,
60 }])
61 } else {
62 base_text = buffer.read(cx).as_rope().clone();
63 status = TrackedBufferStatus::Modified;
64 unreviewed_changes = Patch::default();
65 }
66 TrackedBuffer {
67 buffer: buffer.clone(),
68 base_text,
69 unreviewed_changes,
70 snapshot: text_snapshot.clone(),
71 status,
72 version: buffer.read(cx).version(),
73 diff,
74 diff_update: diff_update_tx,
75 _maintain_diff: cx.spawn({
76 let buffer = buffer.clone();
77 async move |this, cx| {
78 Self::maintain_diff(this, buffer, diff_update_rx, cx)
79 .await
80 .ok();
81 }
82 }),
83 _subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
84 }
85 });
86 tracked_buffer.version = buffer.read(cx).version();
87 tracked_buffer
88 }
89
90 fn handle_buffer_event(
91 &mut self,
92 buffer: Entity<Buffer>,
93 event: &BufferEvent,
94 cx: &mut Context<Self>,
95 ) {
96 match event {
97 BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx),
98 BufferEvent::FileHandleChanged => {
99 self.handle_buffer_file_changed(buffer, cx);
100 }
101 _ => {}
102 };
103 }
104
105 fn handle_buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
106 let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
107 return;
108 };
109 tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
110 }
111
112 fn handle_buffer_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
113 let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
114 return;
115 };
116
117 match tracked_buffer.status {
118 TrackedBufferStatus::Created | TrackedBufferStatus::Modified => {
119 if buffer
120 .read(cx)
121 .file()
122 .map_or(false, |file| file.disk_state() == DiskState::Deleted)
123 {
124 // If the buffer had been edited by a tool, but it got
125 // deleted externally, we want to stop tracking it.
126 self.tracked_buffers.remove(&buffer);
127 }
128 cx.notify();
129 }
130 TrackedBufferStatus::Deleted => {
131 if buffer
132 .read(cx)
133 .file()
134 .map_or(false, |file| file.disk_state() != DiskState::Deleted)
135 {
136 // If the buffer had been deleted by a tool, but it got
137 // resurrected externally, we want to clear the changes we
138 // were tracking and reset the buffer's state.
139 self.tracked_buffers.remove(&buffer);
140 self.track_buffer(buffer, false, cx);
141 }
142 cx.notify();
143 }
144 }
145 }
146
147 async fn maintain_diff(
148 this: WeakEntity<Self>,
149 buffer: Entity<Buffer>,
150 mut diff_update: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>,
151 cx: &mut AsyncApp,
152 ) -> Result<()> {
153 while let Some((author, buffer_snapshot)) = diff_update.next().await {
154 let (rebase, diff, language, language_registry) =
155 this.read_with(cx, |this, cx| {
156 let tracked_buffer = this
157 .tracked_buffers
158 .get(&buffer)
159 .context("buffer not tracked")?;
160
161 let rebase = cx.background_spawn({
162 let mut base_text = tracked_buffer.base_text.clone();
163 let old_snapshot = tracked_buffer.snapshot.clone();
164 let new_snapshot = buffer_snapshot.clone();
165 let unreviewed_changes = tracked_buffer.unreviewed_changes.clone();
166 async move {
167 let edits = diff_snapshots(&old_snapshot, &new_snapshot);
168 if let ChangeAuthor::User = author {
169 apply_non_conflicting_edits(
170 &unreviewed_changes,
171 edits,
172 &mut base_text,
173 new_snapshot.as_rope(),
174 );
175 }
176 (Arc::new(base_text.to_string()), base_text)
177 }
178 });
179
180 anyhow::Ok((
181 rebase,
182 tracked_buffer.diff.clone(),
183 tracked_buffer.buffer.read(cx).language().cloned(),
184 tracked_buffer.buffer.read(cx).language_registry(),
185 ))
186 })??;
187
188 let (new_base_text, new_base_text_rope) = rebase.await;
189 let diff_snapshot = BufferDiff::update_diff(
190 diff.clone(),
191 buffer_snapshot.clone(),
192 Some(new_base_text),
193 true,
194 false,
195 language,
196 language_registry,
197 cx,
198 )
199 .await;
200
201 let mut unreviewed_changes = Patch::default();
202 if let Ok(diff_snapshot) = diff_snapshot {
203 unreviewed_changes = cx
204 .background_spawn({
205 let diff_snapshot = diff_snapshot.clone();
206 let buffer_snapshot = buffer_snapshot.clone();
207 let new_base_text_rope = new_base_text_rope.clone();
208 async move {
209 let mut unreviewed_changes = Patch::default();
210 for hunk in diff_snapshot.hunks_intersecting_range(
211 Anchor::MIN..Anchor::MAX,
212 &buffer_snapshot,
213 ) {
214 let old_range = new_base_text_rope
215 .offset_to_point(hunk.diff_base_byte_range.start)
216 ..new_base_text_rope
217 .offset_to_point(hunk.diff_base_byte_range.end);
218 let new_range = hunk.range.start..hunk.range.end;
219 unreviewed_changes.push(point_to_row_edit(
220 Edit {
221 old: old_range,
222 new: new_range,
223 },
224 &new_base_text_rope,
225 &buffer_snapshot.as_rope(),
226 ));
227 }
228 unreviewed_changes
229 }
230 })
231 .await;
232
233 diff.update(cx, |diff, cx| {
234 diff.set_snapshot(diff_snapshot, &buffer_snapshot, None, cx)
235 })?;
236 }
237 this.update(cx, |this, cx| {
238 let tracked_buffer = this
239 .tracked_buffers
240 .get_mut(&buffer)
241 .context("buffer not tracked")?;
242 tracked_buffer.base_text = new_base_text_rope;
243 tracked_buffer.snapshot = buffer_snapshot;
244 tracked_buffer.unreviewed_changes = unreviewed_changes;
245 cx.notify();
246 anyhow::Ok(())
247 })??;
248 }
249
250 Ok(())
251 }
252
253 /// Track a buffer as read, so we can notify the model about user edits.
254 pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
255 self.track_buffer(buffer, false, cx);
256 }
257
258 /// Track a buffer that was added as context, so we can notify the model about user edits.
259 pub fn buffer_added_as_context(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
260 self.track_buffer(buffer, false, cx);
261 }
262
263 /// Track a buffer as read, so we can notify the model about user edits.
264 pub fn will_create_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
265 self.track_buffer(buffer.clone(), true, cx);
266 self.buffer_edited(buffer, cx)
267 }
268
269 /// Mark a buffer as edited, so we can refresh it in the context
270 pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
271 self.edited_since_project_diagnostics_check = true;
272
273 let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
274 if let TrackedBufferStatus::Deleted = tracked_buffer.status {
275 tracked_buffer.status = TrackedBufferStatus::Modified;
276 }
277 tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
278 }
279
280 pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
281 let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
282 match tracked_buffer.status {
283 TrackedBufferStatus::Created => {
284 self.tracked_buffers.remove(&buffer);
285 cx.notify();
286 }
287 TrackedBufferStatus::Modified => {
288 buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
289 tracked_buffer.status = TrackedBufferStatus::Deleted;
290 tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
291 }
292 TrackedBufferStatus::Deleted => {}
293 }
294 cx.notify();
295 }
296
297 pub fn keep_edits_in_range(
298 &mut self,
299 buffer: Entity<Buffer>,
300 buffer_range: Range<impl language::ToPoint>,
301 cx: &mut Context<Self>,
302 ) {
303 let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
304 return;
305 };
306
307 match tracked_buffer.status {
308 TrackedBufferStatus::Deleted => {
309 self.tracked_buffers.remove(&buffer);
310 cx.notify();
311 }
312 _ => {
313 let buffer = buffer.read(cx);
314 let buffer_range =
315 buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
316 let mut delta = 0i32;
317
318 tracked_buffer.unreviewed_changes.retain_mut(|edit| {
319 edit.old.start = (edit.old.start as i32 + delta) as u32;
320 edit.old.end = (edit.old.end as i32 + delta) as u32;
321
322 if buffer_range.end.row < edit.new.start
323 || buffer_range.start.row > edit.new.end
324 {
325 true
326 } else {
327 let old_bytes = tracked_buffer
328 .base_text
329 .point_to_offset(Point::new(edit.old.start, 0))
330 ..tracked_buffer.base_text.point_to_offset(cmp::min(
331 Point::new(edit.old.end, 0),
332 tracked_buffer.base_text.max_point(),
333 ));
334 let new_bytes = tracked_buffer
335 .snapshot
336 .point_to_offset(Point::new(edit.new.start, 0))
337 ..tracked_buffer.snapshot.point_to_offset(cmp::min(
338 Point::new(edit.new.end, 0),
339 tracked_buffer.snapshot.max_point(),
340 ));
341 tracked_buffer.base_text.replace(
342 old_bytes,
343 &tracked_buffer
344 .snapshot
345 .text_for_range(new_bytes)
346 .collect::<String>(),
347 );
348 delta += edit.new_len() as i32 - edit.old_len() as i32;
349 false
350 }
351 });
352 tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
353 }
354 }
355 }
356
357 pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) {
358 self.tracked_buffers
359 .retain(|_buffer, tracked_buffer| match tracked_buffer.status {
360 TrackedBufferStatus::Deleted => false,
361 _ => {
362 tracked_buffer.unreviewed_changes.clear();
363 tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone();
364 tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
365 true
366 }
367 });
368 cx.notify();
369 }
370
371 /// Returns the set of buffers that contain changes that haven't been reviewed by the user.
372 pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
373 self.tracked_buffers
374 .iter()
375 .filter(|(_, tracked)| tracked.has_changes(cx))
376 .map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone()))
377 .collect()
378 }
379
380 /// Iterate over buffers changed since last read or edited by the model
381 pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
382 self.tracked_buffers
383 .iter()
384 .filter(|(buffer, tracked)| {
385 let buffer = buffer.read(cx);
386
387 tracked.version != buffer.version
388 && buffer
389 .file()
390 .map_or(false, |file| file.disk_state() != DiskState::Deleted)
391 })
392 .map(|(buffer, _)| buffer)
393 }
394}
395
396fn apply_non_conflicting_edits(
397 patch: &Patch<u32>,
398 edits: Vec<Edit<u32>>,
399 old_text: &mut Rope,
400 new_text: &Rope,
401) {
402 let mut old_edits = patch.edits().iter().cloned().peekable();
403 let mut new_edits = edits.into_iter().peekable();
404 let mut applied_delta = 0i32;
405 let mut rebased_delta = 0i32;
406
407 while let Some(mut new_edit) = new_edits.next() {
408 let mut conflict = false;
409
410 // Push all the old edits that are before this new edit or that intersect with it.
411 while let Some(old_edit) = old_edits.peek() {
412 if new_edit.old.end < old_edit.new.start
413 || (!old_edit.new.is_empty() && new_edit.old.end == old_edit.new.start)
414 {
415 break;
416 } else if new_edit.old.start > old_edit.new.end
417 || (!old_edit.new.is_empty() && new_edit.old.start == old_edit.new.end)
418 {
419 let old_edit = old_edits.next().unwrap();
420 rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
421 } else {
422 conflict = true;
423 if new_edits
424 .peek()
425 .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new))
426 {
427 new_edit = new_edits.next().unwrap();
428 } else {
429 let old_edit = old_edits.next().unwrap();
430 rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
431 }
432 }
433 }
434
435 if !conflict {
436 // This edit doesn't intersect with any old edit, so we can apply it to the old text.
437 new_edit.old.start = (new_edit.old.start as i32 + applied_delta - rebased_delta) as u32;
438 new_edit.old.end = (new_edit.old.end as i32 + applied_delta - rebased_delta) as u32;
439 let old_bytes = old_text.point_to_offset(Point::new(new_edit.old.start, 0))
440 ..old_text.point_to_offset(cmp::min(
441 Point::new(new_edit.old.end, 0),
442 old_text.max_point(),
443 ));
444 let new_bytes = new_text.point_to_offset(Point::new(new_edit.new.start, 0))
445 ..new_text.point_to_offset(cmp::min(
446 Point::new(new_edit.new.end, 0),
447 new_text.max_point(),
448 ));
449
450 old_text.replace(
451 old_bytes,
452 &new_text.chunks_in_range(new_bytes).collect::<String>(),
453 );
454 applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32;
455 }
456 }
457}
458
459fn diff_snapshots(
460 old_snapshot: &text::BufferSnapshot,
461 new_snapshot: &text::BufferSnapshot,
462) -> Vec<Edit<u32>> {
463 let mut edits = new_snapshot
464 .edits_since::<Point>(&old_snapshot.version)
465 .map(|edit| point_to_row_edit(edit, old_snapshot.as_rope(), new_snapshot.as_rope()))
466 .peekable();
467 let mut row_edits = Vec::new();
468 while let Some(mut edit) = edits.next() {
469 while let Some(next_edit) = edits.peek() {
470 if edit.old.end >= next_edit.old.start {
471 edit.old.end = next_edit.old.end;
472 edit.new.end = next_edit.new.end;
473 edits.next();
474 } else {
475 break;
476 }
477 }
478 row_edits.push(edit);
479 }
480 row_edits
481}
482
483fn point_to_row_edit(edit: Edit<Point>, old_text: &Rope, new_text: &Rope) -> Edit<u32> {
484 if edit.old.start.column == old_text.line_len(edit.old.start.row)
485 && new_text
486 .chars_at(new_text.point_to_offset(edit.new.start))
487 .next()
488 == Some('\n')
489 && edit.old.start != old_text.max_point()
490 {
491 Edit {
492 old: edit.old.start.row + 1..edit.old.end.row + 1,
493 new: edit.new.start.row + 1..edit.new.end.row + 1,
494 }
495 } else if edit.old.start.column == 0
496 && edit.old.end.column == 0
497 && edit.new.end.column == 0
498 && edit.old.end != old_text.max_point()
499 {
500 Edit {
501 old: edit.old.start.row..edit.old.end.row,
502 new: edit.new.start.row..edit.new.end.row,
503 }
504 } else {
505 Edit {
506 old: edit.old.start.row..edit.old.end.row + 1,
507 new: edit.new.start.row..edit.new.end.row + 1,
508 }
509 }
510}
511
512enum ChangeAuthor {
513 User,
514 Agent,
515}
516
517#[derive(Copy, Clone, Eq, PartialEq)]
518enum TrackedBufferStatus {
519 Created,
520 Modified,
521 Deleted,
522}
523
524struct TrackedBuffer {
525 buffer: Entity<Buffer>,
526 base_text: Rope,
527 unreviewed_changes: Patch<u32>,
528 status: TrackedBufferStatus,
529 version: clock::Global,
530 diff: Entity<BufferDiff>,
531 snapshot: text::BufferSnapshot,
532 diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
533 _maintain_diff: Task<()>,
534 _subscription: Subscription,
535}
536
537impl TrackedBuffer {
538 fn has_changes(&self, cx: &App) -> bool {
539 self.diff
540 .read(cx)
541 .hunks(&self.buffer.read(cx), cx)
542 .next()
543 .is_some()
544 }
545
546 fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) {
547 self.diff_update
548 .unbounded_send((author, self.buffer.read(cx).text_snapshot()))
549 .ok();
550 }
551}
552
553pub struct ChangedBuffer {
554 pub diff: Entity<BufferDiff>,
555}
556
557#[cfg(test)]
558mod tests {
559 use std::env;
560
561 use super::*;
562 use buffer_diff::DiffHunkStatusKind;
563 use gpui::TestAppContext;
564 use language::Point;
565 use project::{FakeFs, Fs, Project, RemoveOptions};
566 use rand::prelude::*;
567 use serde_json::json;
568 use settings::SettingsStore;
569 use util::{RandomCharIter, path};
570
571 #[ctor::ctor]
572 fn init_logger() {
573 if std::env::var("RUST_LOG").is_ok() {
574 env_logger::init();
575 }
576 }
577
578 #[gpui::test(iterations = 10)]
579 async fn test_keep_edits(cx: &mut TestAppContext) {
580 let action_log = cx.new(|_| ActionLog::new());
581 let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
582
583 cx.update(|cx| {
584 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
585 buffer.update(cx, |buffer, cx| {
586 buffer
587 .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
588 .unwrap()
589 });
590 buffer.update(cx, |buffer, cx| {
591 buffer
592 .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
593 .unwrap()
594 });
595 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
596 });
597 cx.run_until_parked();
598 assert_eq!(
599 buffer.read_with(cx, |buffer, _| buffer.text()),
600 "abc\ndEf\nghi\njkl\nmnO"
601 );
602 assert_eq!(
603 unreviewed_hunks(&action_log, cx),
604 vec![(
605 buffer.clone(),
606 vec![
607 HunkStatus {
608 range: Point::new(1, 0)..Point::new(2, 0),
609 diff_status: DiffHunkStatusKind::Modified,
610 old_text: "def\n".into(),
611 },
612 HunkStatus {
613 range: Point::new(4, 0)..Point::new(4, 3),
614 diff_status: DiffHunkStatusKind::Modified,
615 old_text: "mno".into(),
616 }
617 ],
618 )]
619 );
620
621 action_log.update(cx, |log, cx| {
622 log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
623 });
624 cx.run_until_parked();
625 assert_eq!(
626 unreviewed_hunks(&action_log, cx),
627 vec![(
628 buffer.clone(),
629 vec![HunkStatus {
630 range: Point::new(1, 0)..Point::new(2, 0),
631 diff_status: DiffHunkStatusKind::Modified,
632 old_text: "def\n".into(),
633 }],
634 )]
635 );
636
637 action_log.update(cx, |log, cx| {
638 log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
639 });
640 cx.run_until_parked();
641 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
642 }
643
644 #[gpui::test(iterations = 10)]
645 async fn test_deletions(cx: &mut TestAppContext) {
646 let action_log = cx.new(|_| ActionLog::new());
647 let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno\npqr", cx));
648
649 cx.update(|cx| {
650 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
651 buffer.update(cx, |buffer, cx| {
652 buffer
653 .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx)
654 .unwrap();
655 buffer.finalize_last_transaction();
656 });
657 buffer.update(cx, |buffer, cx| {
658 buffer
659 .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx)
660 .unwrap();
661 buffer.finalize_last_transaction();
662 });
663 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
664 });
665 cx.run_until_parked();
666 assert_eq!(
667 buffer.read_with(cx, |buffer, _| buffer.text()),
668 "abc\nghi\njkl\npqr"
669 );
670 assert_eq!(
671 unreviewed_hunks(&action_log, cx),
672 vec![(
673 buffer.clone(),
674 vec![
675 HunkStatus {
676 range: Point::new(1, 0)..Point::new(1, 0),
677 diff_status: DiffHunkStatusKind::Deleted,
678 old_text: "def\n".into(),
679 },
680 HunkStatus {
681 range: Point::new(3, 0)..Point::new(3, 0),
682 diff_status: DiffHunkStatusKind::Deleted,
683 old_text: "mno\n".into(),
684 }
685 ],
686 )]
687 );
688
689 buffer.update(cx, |buffer, cx| buffer.undo(cx));
690 cx.run_until_parked();
691 assert_eq!(
692 buffer.read_with(cx, |buffer, _| buffer.text()),
693 "abc\nghi\njkl\nmno\npqr"
694 );
695 assert_eq!(
696 unreviewed_hunks(&action_log, cx),
697 vec![(
698 buffer.clone(),
699 vec![HunkStatus {
700 range: Point::new(1, 0)..Point::new(1, 0),
701 diff_status: DiffHunkStatusKind::Deleted,
702 old_text: "def\n".into(),
703 }],
704 )]
705 );
706
707 action_log.update(cx, |log, cx| {
708 log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx)
709 });
710 cx.run_until_parked();
711 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
712 }
713
714 #[gpui::test(iterations = 10)]
715 async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
716 let action_log = cx.new(|_| ActionLog::new());
717 let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
718
719 cx.update(|cx| {
720 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
721 buffer.update(cx, |buffer, cx| {
722 buffer
723 .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
724 .unwrap()
725 });
726 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
727 });
728 cx.run_until_parked();
729 assert_eq!(
730 buffer.read_with(cx, |buffer, _| buffer.text()),
731 "abc\ndeF\nGHI\njkl\nmno"
732 );
733 assert_eq!(
734 unreviewed_hunks(&action_log, cx),
735 vec![(
736 buffer.clone(),
737 vec![HunkStatus {
738 range: Point::new(1, 0)..Point::new(3, 0),
739 diff_status: DiffHunkStatusKind::Modified,
740 old_text: "def\nghi\n".into(),
741 }],
742 )]
743 );
744
745 buffer.update(cx, |buffer, cx| {
746 buffer.edit(
747 [
748 (Point::new(0, 2)..Point::new(0, 2), "X"),
749 (Point::new(3, 0)..Point::new(3, 0), "Y"),
750 ],
751 None,
752 cx,
753 )
754 });
755 cx.run_until_parked();
756 assert_eq!(
757 buffer.read_with(cx, |buffer, _| buffer.text()),
758 "abXc\ndeF\nGHI\nYjkl\nmno"
759 );
760 assert_eq!(
761 unreviewed_hunks(&action_log, cx),
762 vec![(
763 buffer.clone(),
764 vec![HunkStatus {
765 range: Point::new(1, 0)..Point::new(3, 0),
766 diff_status: DiffHunkStatusKind::Modified,
767 old_text: "def\nghi\n".into(),
768 }],
769 )]
770 );
771
772 buffer.update(cx, |buffer, cx| {
773 buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx)
774 });
775 cx.run_until_parked();
776 assert_eq!(
777 buffer.read_with(cx, |buffer, _| buffer.text()),
778 "abXc\ndZeF\nGHI\nYjkl\nmno"
779 );
780 assert_eq!(
781 unreviewed_hunks(&action_log, cx),
782 vec![(
783 buffer.clone(),
784 vec![HunkStatus {
785 range: Point::new(1, 0)..Point::new(3, 0),
786 diff_status: DiffHunkStatusKind::Modified,
787 old_text: "def\nghi\n".into(),
788 }],
789 )]
790 );
791
792 action_log.update(cx, |log, cx| {
793 log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
794 });
795 cx.run_until_parked();
796 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
797 }
798
799 #[gpui::test(iterations = 10)]
800 async fn test_creation(cx: &mut TestAppContext) {
801 cx.update(|cx| {
802 let settings_store = SettingsStore::test(cx);
803 cx.set_global(settings_store);
804 language::init(cx);
805 Project::init_settings(cx);
806 });
807
808 let action_log = cx.new(|_| ActionLog::new());
809
810 let fs = FakeFs::new(cx.executor());
811 fs.insert_tree(path!("/dir"), json!({})).await;
812
813 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
814 let file_path = project
815 .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
816 .unwrap();
817
818 // Simulate file2 being recreated by a tool.
819 let buffer = project
820 .update(cx, |project, cx| project.open_buffer(file_path, cx))
821 .await
822 .unwrap();
823 cx.update(|cx| {
824 buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
825 action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
826 });
827 project
828 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
829 .await
830 .unwrap();
831 cx.run_until_parked();
832 assert_eq!(
833 unreviewed_hunks(&action_log, cx),
834 vec![(
835 buffer.clone(),
836 vec![HunkStatus {
837 range: Point::new(0, 0)..Point::new(0, 5),
838 diff_status: DiffHunkStatusKind::Added,
839 old_text: "".into(),
840 }],
841 )]
842 );
843
844 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx));
845 cx.run_until_parked();
846 assert_eq!(
847 unreviewed_hunks(&action_log, cx),
848 vec![(
849 buffer.clone(),
850 vec![HunkStatus {
851 range: Point::new(0, 0)..Point::new(0, 6),
852 diff_status: DiffHunkStatusKind::Added,
853 old_text: "".into(),
854 }],
855 )]
856 );
857
858 action_log.update(cx, |log, cx| {
859 log.keep_edits_in_range(buffer.clone(), 0..5, cx)
860 });
861 cx.run_until_parked();
862 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
863 }
864
865 #[gpui::test(iterations = 10)]
866 async fn test_deleting_files(cx: &mut TestAppContext) {
867 cx.update(|cx| {
868 let settings_store = SettingsStore::test(cx);
869 cx.set_global(settings_store);
870 language::init(cx);
871 Project::init_settings(cx);
872 });
873
874 let fs = FakeFs::new(cx.executor());
875 fs.insert_tree(
876 path!("/dir"),
877 json!({"file1": "lorem\n", "file2": "ipsum\n"}),
878 )
879 .await;
880
881 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
882 let file1_path = project
883 .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
884 .unwrap();
885 let file2_path = project
886 .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
887 .unwrap();
888
889 let action_log = cx.new(|_| ActionLog::new());
890 let buffer1 = project
891 .update(cx, |project, cx| {
892 project.open_buffer(file1_path.clone(), cx)
893 })
894 .await
895 .unwrap();
896 let buffer2 = project
897 .update(cx, |project, cx| {
898 project.open_buffer(file2_path.clone(), cx)
899 })
900 .await
901 .unwrap();
902
903 action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
904 action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
905 project
906 .update(cx, |project, cx| {
907 project.delete_file(file1_path.clone(), false, cx)
908 })
909 .unwrap()
910 .await
911 .unwrap();
912 project
913 .update(cx, |project, cx| {
914 project.delete_file(file2_path.clone(), false, cx)
915 })
916 .unwrap()
917 .await
918 .unwrap();
919 cx.run_until_parked();
920 assert_eq!(
921 unreviewed_hunks(&action_log, cx),
922 vec![
923 (
924 buffer1.clone(),
925 vec![HunkStatus {
926 range: Point::new(0, 0)..Point::new(0, 0),
927 diff_status: DiffHunkStatusKind::Deleted,
928 old_text: "lorem\n".into(),
929 }]
930 ),
931 (
932 buffer2.clone(),
933 vec![HunkStatus {
934 range: Point::new(0, 0)..Point::new(0, 0),
935 diff_status: DiffHunkStatusKind::Deleted,
936 old_text: "ipsum\n".into(),
937 }],
938 )
939 ]
940 );
941
942 // Simulate file1 being recreated externally.
943 fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
944 .await;
945
946 // Simulate file2 being recreated by a tool.
947 let buffer2 = project
948 .update(cx, |project, cx| project.open_buffer(file2_path, cx))
949 .await
950 .unwrap();
951 buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
952 action_log.update(cx, |log, cx| log.will_create_buffer(buffer2.clone(), cx));
953 project
954 .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
955 .await
956 .unwrap();
957
958 cx.run_until_parked();
959 assert_eq!(
960 unreviewed_hunks(&action_log, cx),
961 vec![(
962 buffer2.clone(),
963 vec![HunkStatus {
964 range: Point::new(0, 0)..Point::new(0, 5),
965 diff_status: DiffHunkStatusKind::Modified,
966 old_text: "ipsum\n".into(),
967 }],
968 )]
969 );
970
971 // Simulate file2 being deleted externally.
972 fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
973 .await
974 .unwrap();
975 cx.run_until_parked();
976 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
977 }
978
979 #[gpui::test(iterations = 100)]
980 async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
981 let operations = env::var("OPERATIONS")
982 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
983 .unwrap_or(20);
984
985 let action_log = cx.new(|_| ActionLog::new());
986 let text = RandomCharIter::new(&mut rng).take(50).collect::<String>();
987 let buffer = cx.new(|cx| Buffer::local(text, cx));
988 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
989
990 for _ in 0..operations {
991 match rng.gen_range(0..100) {
992 0..25 => {
993 action_log.update(cx, |log, cx| {
994 let range = buffer.read(cx).random_byte_range(0, &mut rng);
995 log::info!("keeping all edits in range {:?}", range);
996 log.keep_edits_in_range(buffer.clone(), range, cx)
997 });
998 }
999 _ => {
1000 let is_agent_change = rng.gen_bool(0.5);
1001 if is_agent_change {
1002 log::info!("agent edit");
1003 } else {
1004 log::info!("user edit");
1005 }
1006 cx.update(|cx| {
1007 buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
1008 if is_agent_change {
1009 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1010 }
1011 });
1012 }
1013 }
1014
1015 if rng.gen_bool(0.2) {
1016 quiesce(&action_log, &buffer, cx);
1017 }
1018 }
1019
1020 quiesce(&action_log, &buffer, cx);
1021
1022 fn quiesce(
1023 action_log: &Entity<ActionLog>,
1024 buffer: &Entity<Buffer>,
1025 cx: &mut TestAppContext,
1026 ) {
1027 log::info!("quiescing...");
1028 cx.run_until_parked();
1029 action_log.update(cx, |log, cx| {
1030 let tracked_buffer = log.track_buffer(buffer.clone(), false, cx);
1031 let mut old_text = tracked_buffer.base_text.clone();
1032 let new_text = buffer.read(cx).as_rope();
1033 for edit in tracked_buffer.unreviewed_changes.edits() {
1034 let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
1035 let old_end = old_text.point_to_offset(cmp::min(
1036 Point::new(edit.new.start + edit.old_len(), 0),
1037 old_text.max_point(),
1038 ));
1039 old_text.replace(
1040 old_start..old_end,
1041 &new_text.slice_rows(edit.new.clone()).to_string(),
1042 );
1043 }
1044 pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
1045 })
1046 }
1047 }
1048
1049 #[derive(Debug, Clone, PartialEq, Eq)]
1050 struct HunkStatus {
1051 range: Range<Point>,
1052 diff_status: DiffHunkStatusKind,
1053 old_text: String,
1054 }
1055
1056 fn unreviewed_hunks(
1057 action_log: &Entity<ActionLog>,
1058 cx: &TestAppContext,
1059 ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
1060 cx.read(|cx| {
1061 action_log
1062 .read(cx)
1063 .changed_buffers(cx)
1064 .into_iter()
1065 .map(|(buffer, diff)| {
1066 let snapshot = buffer.read(cx).snapshot();
1067 (
1068 buffer,
1069 diff.read(cx)
1070 .hunks(&snapshot, cx)
1071 .map(|hunk| HunkStatus {
1072 diff_status: hunk.status().kind,
1073 range: hunk.range,
1074 old_text: diff
1075 .read(cx)
1076 .base_text()
1077 .text_for_range(hunk.diff_base_byte_range)
1078 .collect(),
1079 })
1080 .collect(),
1081 )
1082 })
1083 .collect()
1084 })
1085 }
1086}