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 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 /// Takes and returns the set of buffers pending refresh, clearing internal state.
396 pub fn take_stale_buffers_in_context(&mut self) -> HashSet<Entity<Buffer>> {
397 std::mem::take(&mut self.stale_buffers_in_context)
398 }
399}
400
401fn apply_non_conflicting_edits(
402 patch: &Patch<u32>,
403 edits: Vec<Edit<u32>>,
404 old_text: &mut Rope,
405 new_text: &Rope,
406) {
407 let mut old_edits = patch.edits().iter().cloned().peekable();
408 let mut new_edits = edits.into_iter().peekable();
409 let mut applied_delta = 0i32;
410 let mut rebased_delta = 0i32;
411
412 while let Some(mut new_edit) = new_edits.next() {
413 let mut conflict = false;
414
415 // Push all the old edits that are before this new edit or that intersect with it.
416 while let Some(old_edit) = old_edits.peek() {
417 if new_edit.old.end < old_edit.new.start
418 || (!old_edit.new.is_empty() && new_edit.old.end == old_edit.new.start)
419 {
420 break;
421 } else if new_edit.old.start > old_edit.new.end
422 || (!old_edit.new.is_empty() && new_edit.old.start == old_edit.new.end)
423 {
424 let old_edit = old_edits.next().unwrap();
425 rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
426 } else {
427 conflict = true;
428 if new_edits
429 .peek()
430 .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new))
431 {
432 new_edit = new_edits.next().unwrap();
433 } else {
434 let old_edit = old_edits.next().unwrap();
435 rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
436 }
437 }
438 }
439
440 if !conflict {
441 // This edit doesn't intersect with any old edit, so we can apply it to the old text.
442 new_edit.old.start = (new_edit.old.start as i32 + applied_delta - rebased_delta) as u32;
443 new_edit.old.end = (new_edit.old.end as i32 + applied_delta - rebased_delta) as u32;
444 let old_bytes = old_text.point_to_offset(Point::new(new_edit.old.start, 0))
445 ..old_text.point_to_offset(cmp::min(
446 Point::new(new_edit.old.end, 0),
447 old_text.max_point(),
448 ));
449 let new_bytes = new_text.point_to_offset(Point::new(new_edit.new.start, 0))
450 ..new_text.point_to_offset(cmp::min(
451 Point::new(new_edit.new.end, 0),
452 new_text.max_point(),
453 ));
454
455 old_text.replace(
456 old_bytes,
457 &new_text.chunks_in_range(new_bytes).collect::<String>(),
458 );
459 applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32;
460 }
461 }
462}
463
464fn diff_snapshots(
465 old_snapshot: &text::BufferSnapshot,
466 new_snapshot: &text::BufferSnapshot,
467) -> Vec<Edit<u32>> {
468 let mut edits = new_snapshot
469 .edits_since::<Point>(&old_snapshot.version)
470 .map(|edit| point_to_row_edit(edit, old_snapshot.as_rope(), new_snapshot.as_rope()))
471 .peekable();
472 let mut row_edits = Vec::new();
473 while let Some(mut edit) = edits.next() {
474 while let Some(next_edit) = edits.peek() {
475 if edit.old.end >= next_edit.old.start {
476 edit.old.end = next_edit.old.end;
477 edit.new.end = next_edit.new.end;
478 edits.next();
479 } else {
480 break;
481 }
482 }
483 row_edits.push(edit);
484 }
485 row_edits
486}
487
488fn point_to_row_edit(edit: Edit<Point>, old_text: &Rope, new_text: &Rope) -> Edit<u32> {
489 if edit.old.start.column == old_text.line_len(edit.old.start.row)
490 && new_text
491 .chars_at(new_text.point_to_offset(edit.new.start))
492 .next()
493 == Some('\n')
494 && edit.old.start != old_text.max_point()
495 {
496 Edit {
497 old: edit.old.start.row + 1..edit.old.end.row + 1,
498 new: edit.new.start.row + 1..edit.new.end.row + 1,
499 }
500 } else if edit.old.start.column == 0
501 && edit.old.end.column == 0
502 && edit.new.end.column == 0
503 && edit.old.end != old_text.max_point()
504 {
505 Edit {
506 old: edit.old.start.row..edit.old.end.row,
507 new: edit.new.start.row..edit.new.end.row,
508 }
509 } else {
510 Edit {
511 old: edit.old.start.row..edit.old.end.row + 1,
512 new: edit.new.start.row..edit.new.end.row + 1,
513 }
514 }
515}
516
517enum ChangeAuthor {
518 User,
519 Agent,
520}
521
522#[derive(Copy, Clone, Eq, PartialEq)]
523enum TrackedBufferStatus {
524 Created,
525 Modified,
526 Deleted,
527}
528
529struct TrackedBuffer {
530 buffer: Entity<Buffer>,
531 base_text: Rope,
532 unreviewed_changes: Patch<u32>,
533 status: TrackedBufferStatus,
534 version: clock::Global,
535 diff: Entity<BufferDiff>,
536 snapshot: text::BufferSnapshot,
537 diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
538 _maintain_diff: Task<()>,
539 _subscription: Subscription,
540}
541
542impl TrackedBuffer {
543 fn has_changes(&self, cx: &App) -> bool {
544 self.diff
545 .read(cx)
546 .hunks(&self.buffer.read(cx), cx)
547 .next()
548 .is_some()
549 }
550
551 fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) {
552 self.diff_update
553 .unbounded_send((author, self.buffer.read(cx).text_snapshot()))
554 .ok();
555 }
556}
557
558pub struct ChangedBuffer {
559 pub diff: Entity<BufferDiff>,
560}
561
562#[cfg(test)]
563mod tests {
564 use std::env;
565
566 use super::*;
567 use buffer_diff::DiffHunkStatusKind;
568 use gpui::TestAppContext;
569 use language::Point;
570 use project::{FakeFs, Fs, Project, RemoveOptions};
571 use rand::prelude::*;
572 use serde_json::json;
573 use settings::SettingsStore;
574 use util::{RandomCharIter, path};
575
576 #[ctor::ctor]
577 fn init_logger() {
578 if std::env::var("RUST_LOG").is_ok() {
579 env_logger::init();
580 }
581 }
582
583 #[gpui::test(iterations = 10)]
584 async fn test_keep_edits(cx: &mut TestAppContext) {
585 let action_log = cx.new(|_| ActionLog::new());
586 let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
587
588 cx.update(|cx| {
589 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
590 buffer.update(cx, |buffer, cx| {
591 buffer
592 .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
593 .unwrap()
594 });
595 buffer.update(cx, |buffer, cx| {
596 buffer
597 .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
598 .unwrap()
599 });
600 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
601 });
602 cx.run_until_parked();
603 assert_eq!(
604 buffer.read_with(cx, |buffer, _| buffer.text()),
605 "abc\ndEf\nghi\njkl\nmnO"
606 );
607 assert_eq!(
608 unreviewed_hunks(&action_log, cx),
609 vec![(
610 buffer.clone(),
611 vec![
612 HunkStatus {
613 range: Point::new(1, 0)..Point::new(2, 0),
614 diff_status: DiffHunkStatusKind::Modified,
615 old_text: "def\n".into(),
616 },
617 HunkStatus {
618 range: Point::new(4, 0)..Point::new(4, 3),
619 diff_status: DiffHunkStatusKind::Modified,
620 old_text: "mno".into(),
621 }
622 ],
623 )]
624 );
625
626 action_log.update(cx, |log, cx| {
627 log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
628 });
629 cx.run_until_parked();
630 assert_eq!(
631 unreviewed_hunks(&action_log, cx),
632 vec![(
633 buffer.clone(),
634 vec![HunkStatus {
635 range: Point::new(1, 0)..Point::new(2, 0),
636 diff_status: DiffHunkStatusKind::Modified,
637 old_text: "def\n".into(),
638 }],
639 )]
640 );
641
642 action_log.update(cx, |log, cx| {
643 log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
644 });
645 cx.run_until_parked();
646 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
647 }
648
649 #[gpui::test(iterations = 10)]
650 async fn test_deletions(cx: &mut TestAppContext) {
651 let action_log = cx.new(|_| ActionLog::new());
652 let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno\npqr", cx));
653
654 cx.update(|cx| {
655 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
656 buffer.update(cx, |buffer, cx| {
657 buffer
658 .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx)
659 .unwrap();
660 buffer.finalize_last_transaction();
661 });
662 buffer.update(cx, |buffer, cx| {
663 buffer
664 .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx)
665 .unwrap();
666 buffer.finalize_last_transaction();
667 });
668 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
669 });
670 cx.run_until_parked();
671 assert_eq!(
672 buffer.read_with(cx, |buffer, _| buffer.text()),
673 "abc\nghi\njkl\npqr"
674 );
675 assert_eq!(
676 unreviewed_hunks(&action_log, cx),
677 vec![(
678 buffer.clone(),
679 vec![
680 HunkStatus {
681 range: Point::new(1, 0)..Point::new(1, 0),
682 diff_status: DiffHunkStatusKind::Deleted,
683 old_text: "def\n".into(),
684 },
685 HunkStatus {
686 range: Point::new(3, 0)..Point::new(3, 0),
687 diff_status: DiffHunkStatusKind::Deleted,
688 old_text: "mno\n".into(),
689 }
690 ],
691 )]
692 );
693
694 buffer.update(cx, |buffer, cx| buffer.undo(cx));
695 cx.run_until_parked();
696 assert_eq!(
697 buffer.read_with(cx, |buffer, _| buffer.text()),
698 "abc\nghi\njkl\nmno\npqr"
699 );
700 assert_eq!(
701 unreviewed_hunks(&action_log, cx),
702 vec![(
703 buffer.clone(),
704 vec![HunkStatus {
705 range: Point::new(1, 0)..Point::new(1, 0),
706 diff_status: DiffHunkStatusKind::Deleted,
707 old_text: "def\n".into(),
708 }],
709 )]
710 );
711
712 action_log.update(cx, |log, cx| {
713 log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx)
714 });
715 cx.run_until_parked();
716 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
717 }
718
719 #[gpui::test(iterations = 10)]
720 async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
721 let action_log = cx.new(|_| ActionLog::new());
722 let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
723
724 cx.update(|cx| {
725 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
726 buffer.update(cx, |buffer, cx| {
727 buffer
728 .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
729 .unwrap()
730 });
731 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
732 });
733 cx.run_until_parked();
734 assert_eq!(
735 buffer.read_with(cx, |buffer, _| buffer.text()),
736 "abc\ndeF\nGHI\njkl\nmno"
737 );
738 assert_eq!(
739 unreviewed_hunks(&action_log, cx),
740 vec![(
741 buffer.clone(),
742 vec![HunkStatus {
743 range: Point::new(1, 0)..Point::new(3, 0),
744 diff_status: DiffHunkStatusKind::Modified,
745 old_text: "def\nghi\n".into(),
746 }],
747 )]
748 );
749
750 buffer.update(cx, |buffer, cx| {
751 buffer.edit(
752 [
753 (Point::new(0, 2)..Point::new(0, 2), "X"),
754 (Point::new(3, 0)..Point::new(3, 0), "Y"),
755 ],
756 None,
757 cx,
758 )
759 });
760 cx.run_until_parked();
761 assert_eq!(
762 buffer.read_with(cx, |buffer, _| buffer.text()),
763 "abXc\ndeF\nGHI\nYjkl\nmno"
764 );
765 assert_eq!(
766 unreviewed_hunks(&action_log, cx),
767 vec![(
768 buffer.clone(),
769 vec![HunkStatus {
770 range: Point::new(1, 0)..Point::new(3, 0),
771 diff_status: DiffHunkStatusKind::Modified,
772 old_text: "def\nghi\n".into(),
773 }],
774 )]
775 );
776
777 buffer.update(cx, |buffer, cx| {
778 buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx)
779 });
780 cx.run_until_parked();
781 assert_eq!(
782 buffer.read_with(cx, |buffer, _| buffer.text()),
783 "abXc\ndZeF\nGHI\nYjkl\nmno"
784 );
785 assert_eq!(
786 unreviewed_hunks(&action_log, cx),
787 vec![(
788 buffer.clone(),
789 vec![HunkStatus {
790 range: Point::new(1, 0)..Point::new(3, 0),
791 diff_status: DiffHunkStatusKind::Modified,
792 old_text: "def\nghi\n".into(),
793 }],
794 )]
795 );
796
797 action_log.update(cx, |log, cx| {
798 log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
799 });
800 cx.run_until_parked();
801 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
802 }
803
804 #[gpui::test(iterations = 10)]
805 async fn test_creation(cx: &mut TestAppContext) {
806 cx.update(|cx| {
807 let settings_store = SettingsStore::test(cx);
808 cx.set_global(settings_store);
809 language::init(cx);
810 Project::init_settings(cx);
811 });
812
813 let action_log = cx.new(|_| ActionLog::new());
814
815 let fs = FakeFs::new(cx.executor());
816 fs.insert_tree(path!("/dir"), json!({})).await;
817
818 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
819 let file_path = project
820 .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
821 .unwrap();
822
823 // Simulate file2 being recreated by a tool.
824 let buffer = project
825 .update(cx, |project, cx| project.open_buffer(file_path, cx))
826 .await
827 .unwrap();
828 cx.update(|cx| {
829 buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
830 action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
831 });
832 project
833 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
834 .await
835 .unwrap();
836 cx.run_until_parked();
837 assert_eq!(
838 unreviewed_hunks(&action_log, cx),
839 vec![(
840 buffer.clone(),
841 vec![HunkStatus {
842 range: Point::new(0, 0)..Point::new(0, 5),
843 diff_status: DiffHunkStatusKind::Added,
844 old_text: "".into(),
845 }],
846 )]
847 );
848
849 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx));
850 cx.run_until_parked();
851 assert_eq!(
852 unreviewed_hunks(&action_log, cx),
853 vec![(
854 buffer.clone(),
855 vec![HunkStatus {
856 range: Point::new(0, 0)..Point::new(0, 6),
857 diff_status: DiffHunkStatusKind::Added,
858 old_text: "".into(),
859 }],
860 )]
861 );
862
863 action_log.update(cx, |log, cx| {
864 log.keep_edits_in_range(buffer.clone(), 0..5, cx)
865 });
866 cx.run_until_parked();
867 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
868 }
869
870 #[gpui::test(iterations = 10)]
871 async fn test_deleting_files(cx: &mut TestAppContext) {
872 cx.update(|cx| {
873 let settings_store = SettingsStore::test(cx);
874 cx.set_global(settings_store);
875 language::init(cx);
876 Project::init_settings(cx);
877 });
878
879 let fs = FakeFs::new(cx.executor());
880 fs.insert_tree(
881 path!("/dir"),
882 json!({"file1": "lorem\n", "file2": "ipsum\n"}),
883 )
884 .await;
885
886 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
887 let file1_path = project
888 .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
889 .unwrap();
890 let file2_path = project
891 .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
892 .unwrap();
893
894 let action_log = cx.new(|_| ActionLog::new());
895 let buffer1 = project
896 .update(cx, |project, cx| {
897 project.open_buffer(file1_path.clone(), cx)
898 })
899 .await
900 .unwrap();
901 let buffer2 = project
902 .update(cx, |project, cx| {
903 project.open_buffer(file2_path.clone(), cx)
904 })
905 .await
906 .unwrap();
907
908 action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
909 action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
910 project
911 .update(cx, |project, cx| {
912 project.delete_file(file1_path.clone(), false, cx)
913 })
914 .unwrap()
915 .await
916 .unwrap();
917 project
918 .update(cx, |project, cx| {
919 project.delete_file(file2_path.clone(), false, cx)
920 })
921 .unwrap()
922 .await
923 .unwrap();
924 cx.run_until_parked();
925 assert_eq!(
926 unreviewed_hunks(&action_log, cx),
927 vec![
928 (
929 buffer1.clone(),
930 vec![HunkStatus {
931 range: Point::new(0, 0)..Point::new(0, 0),
932 diff_status: DiffHunkStatusKind::Deleted,
933 old_text: "lorem\n".into(),
934 }]
935 ),
936 (
937 buffer2.clone(),
938 vec![HunkStatus {
939 range: Point::new(0, 0)..Point::new(0, 0),
940 diff_status: DiffHunkStatusKind::Deleted,
941 old_text: "ipsum\n".into(),
942 }],
943 )
944 ]
945 );
946
947 // Simulate file1 being recreated externally.
948 fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
949 .await;
950
951 // Simulate file2 being recreated by a tool.
952 let buffer2 = project
953 .update(cx, |project, cx| project.open_buffer(file2_path, cx))
954 .await
955 .unwrap();
956 buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
957 action_log.update(cx, |log, cx| log.will_create_buffer(buffer2.clone(), cx));
958 project
959 .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
960 .await
961 .unwrap();
962
963 cx.run_until_parked();
964 assert_eq!(
965 unreviewed_hunks(&action_log, cx),
966 vec![(
967 buffer2.clone(),
968 vec![HunkStatus {
969 range: Point::new(0, 0)..Point::new(0, 5),
970 diff_status: DiffHunkStatusKind::Modified,
971 old_text: "ipsum\n".into(),
972 }],
973 )]
974 );
975
976 // Simulate file2 being deleted externally.
977 fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
978 .await
979 .unwrap();
980 cx.run_until_parked();
981 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
982 }
983
984 #[gpui::test(iterations = 100)]
985 async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
986 let operations = env::var("OPERATIONS")
987 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
988 .unwrap_or(20);
989
990 let action_log = cx.new(|_| ActionLog::new());
991 let text = RandomCharIter::new(&mut rng).take(50).collect::<String>();
992 let buffer = cx.new(|cx| Buffer::local(text, cx));
993 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
994
995 for _ in 0..operations {
996 match rng.gen_range(0..100) {
997 0..25 => {
998 action_log.update(cx, |log, cx| {
999 let range = buffer.read(cx).random_byte_range(0, &mut rng);
1000 log::info!("keeping all edits in range {:?}", range);
1001 log.keep_edits_in_range(buffer.clone(), range, cx)
1002 });
1003 }
1004 _ => {
1005 let is_agent_change = rng.gen_bool(0.5);
1006 if is_agent_change {
1007 log::info!("agent edit");
1008 } else {
1009 log::info!("user edit");
1010 }
1011 cx.update(|cx| {
1012 buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
1013 if is_agent_change {
1014 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1015 }
1016 });
1017 }
1018 }
1019
1020 if rng.gen_bool(0.2) {
1021 quiesce(&action_log, &buffer, cx);
1022 }
1023 }
1024
1025 quiesce(&action_log, &buffer, cx);
1026
1027 fn quiesce(
1028 action_log: &Entity<ActionLog>,
1029 buffer: &Entity<Buffer>,
1030 cx: &mut TestAppContext,
1031 ) {
1032 log::info!("quiescing...");
1033 cx.run_until_parked();
1034 action_log.update(cx, |log, cx| {
1035 let tracked_buffer = log.track_buffer(buffer.clone(), false, cx);
1036 let mut old_text = tracked_buffer.base_text.clone();
1037 let new_text = buffer.read(cx).as_rope();
1038 for edit in tracked_buffer.unreviewed_changes.edits() {
1039 let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
1040 let old_end = old_text.point_to_offset(cmp::min(
1041 Point::new(edit.new.start + edit.old_len(), 0),
1042 old_text.max_point(),
1043 ));
1044 old_text.replace(
1045 old_start..old_end,
1046 &new_text.slice_rows(edit.new.clone()).to_string(),
1047 );
1048 }
1049 pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
1050 })
1051 }
1052 }
1053
1054 #[derive(Debug, Clone, PartialEq, Eq)]
1055 struct HunkStatus {
1056 range: Range<Point>,
1057 diff_status: DiffHunkStatusKind,
1058 old_text: String,
1059 }
1060
1061 fn unreviewed_hunks(
1062 action_log: &Entity<ActionLog>,
1063 cx: &TestAppContext,
1064 ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
1065 cx.read(|cx| {
1066 action_log
1067 .read(cx)
1068 .changed_buffers(cx)
1069 .into_iter()
1070 .map(|(buffer, diff)| {
1071 let snapshot = buffer.read(cx).snapshot();
1072 (
1073 buffer,
1074 diff.read(cx)
1075 .hunks(&snapshot, cx)
1076 .map(|hunk| HunkStatus {
1077 diff_status: hunk.status().kind,
1078 range: hunk.range,
1079 old_text: diff
1080 .read(cx)
1081 .base_text()
1082 .text_for_range(hunk.diff_base_byte_range)
1083 .collect(),
1084 })
1085 .collect(),
1086 )
1087 })
1088 .collect()
1089 })
1090 }
1091}