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