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::{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 let unreviewed_changes = match author {
173 ChangeAuthor::User => rebase_patch(
174 &unreviewed_changes,
175 edits,
176 &mut base_text,
177 new_snapshot.as_rope(),
178 ),
179 ChangeAuthor::Agent => unreviewed_changes.compose(edits),
180 };
181 (
182 Arc::new(base_text.to_string()),
183 base_text,
184 unreviewed_changes,
185 )
186 }
187 });
188
189 anyhow::Ok((
190 rebase,
191 tracked_buffer.diff.clone(),
192 tracked_buffer.buffer.read(cx).language().cloned(),
193 tracked_buffer.buffer.read(cx).language_registry(),
194 ))
195 })??;
196
197 let (new_base_text, new_base_text_rope, unreviewed_changes) = rebase.await;
198 let diff_snapshot = BufferDiff::update_diff(
199 diff.clone(),
200 buffer_snapshot.clone(),
201 Some(new_base_text),
202 true,
203 false,
204 language,
205 language_registry,
206 cx,
207 )
208 .await;
209 if let Ok(diff_snapshot) = diff_snapshot {
210 diff.update(cx, |diff, cx| {
211 diff.set_snapshot(diff_snapshot, &buffer_snapshot, None, cx)
212 })?;
213 }
214 this.update(cx, |this, cx| {
215 let tracked_buffer = this
216 .tracked_buffers
217 .get_mut(&buffer)
218 .context("buffer not tracked")?;
219 tracked_buffer.base_text = new_base_text_rope;
220 tracked_buffer.snapshot = buffer_snapshot;
221 tracked_buffer.unreviewed_changes = unreviewed_changes;
222 cx.notify();
223 anyhow::Ok(())
224 })??;
225 }
226
227 Ok(())
228 }
229
230 /// Track a buffer as read, so we can notify the model about user edits.
231 pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
232 self.track_buffer(buffer, false, cx);
233 }
234
235 /// Track a buffer as read, so we can notify the model about user edits.
236 pub fn will_create_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
237 self.track_buffer(buffer.clone(), true, cx);
238 self.buffer_edited(buffer, cx)
239 }
240
241 /// Mark a buffer as edited, so we can refresh it in the context
242 pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
243 self.edited_since_project_diagnostics_check = true;
244 self.stale_buffers_in_context.insert(buffer.clone());
245
246 let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
247 if let TrackedBufferStatus::Deleted = tracked_buffer.status {
248 tracked_buffer.status = TrackedBufferStatus::Modified;
249 }
250 tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
251 }
252
253 pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
254 let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
255 match tracked_buffer.status {
256 TrackedBufferStatus::Created => {
257 self.tracked_buffers.remove(&buffer);
258 cx.notify();
259 }
260 TrackedBufferStatus::Modified => {
261 buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
262 tracked_buffer.status = TrackedBufferStatus::Deleted;
263 tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
264 }
265 TrackedBufferStatus::Deleted => {}
266 }
267 cx.notify();
268 }
269
270 pub fn keep_edits_in_range<T>(
271 &mut self,
272 buffer: Entity<Buffer>,
273 buffer_range: Range<T>,
274 cx: &mut Context<Self>,
275 ) where
276 T: 'static + language::ToPoint, // + Clone
277 // + Copy
278 // + Ord
279 // + Sub<T, Output = T>
280 // + Add<T, Output = T>
281 // + AddAssign
282 // + Default
283 // + PartialEq,
284 {
285 let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
286 return;
287 };
288
289 match tracked_buffer.status {
290 TrackedBufferStatus::Deleted => {
291 self.tracked_buffers.remove(&buffer);
292 cx.notify();
293 }
294 _ => {
295 let buffer = buffer.read(cx);
296 let buffer_range =
297 buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
298 let buffer_row_range = buffer_range.start.row..buffer_range.end.row + 1;
299 let mut delta = 0i32;
300 tracked_buffer.unreviewed_changes.retain_mut(|edit| {
301 edit.old.start = (edit.old.start as i32 + delta) as u32;
302 edit.old.end = (edit.old.end as i32 + delta) as u32;
303 if edit.new.overlaps(&buffer_row_range) {
304 let old_bytes = tracked_buffer
305 .base_text
306 .point_to_offset(Point::new(edit.old.start, 0))
307 ..tracked_buffer.base_text.point_to_offset(cmp::min(
308 Point::new(edit.old.end, 0),
309 tracked_buffer.base_text.max_point(),
310 ));
311 let new_bytes = tracked_buffer
312 .snapshot
313 .point_to_offset(Point::new(edit.new.start, 0))
314 ..tracked_buffer.snapshot.point_to_offset(cmp::min(
315 Point::new(edit.new.end, 0),
316 tracked_buffer.snapshot.max_point(),
317 ));
318 tracked_buffer.base_text.replace(
319 old_bytes,
320 &tracked_buffer
321 .snapshot
322 .text_for_range(new_bytes)
323 .collect::<String>(),
324 );
325 delta += edit.new_len() as i32 - edit.old_len() as i32;
326 false
327 } else {
328 true
329 }
330 });
331 tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
332 }
333 }
334 }
335
336 pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) {
337 self.tracked_buffers
338 .retain(|_buffer, tracked_buffer| match tracked_buffer.status {
339 TrackedBufferStatus::Deleted => false,
340 _ => {
341 tracked_buffer.unreviewed_changes.clear();
342 tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone();
343 tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
344 true
345 }
346 });
347 cx.notify();
348 }
349
350 /// Returns the set of buffers that contain changes that haven't been reviewed by the user.
351 pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
352 self.tracked_buffers
353 .iter()
354 .filter(|(_, tracked)| tracked.has_changes(cx))
355 .map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone()))
356 .collect()
357 }
358
359 /// Iterate over buffers changed since last read or edited by the model
360 pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
361 self.tracked_buffers
362 .iter()
363 .filter(|(buffer, tracked)| {
364 let buffer = buffer.read(cx);
365
366 tracked.version != buffer.version
367 && buffer
368 .file()
369 .map_or(false, |file| file.disk_state() != DiskState::Deleted)
370 })
371 .map(|(buffer, _)| buffer)
372 }
373
374 /// Takes and returns the set of buffers pending refresh, clearing internal state.
375 pub fn take_stale_buffers_in_context(&mut self) -> HashSet<Entity<Buffer>> {
376 std::mem::take(&mut self.stale_buffers_in_context)
377 }
378}
379
380fn rebase_patch(
381 patch: &Patch<u32>,
382 edits: Vec<Edit<u32>>,
383 old_text: &mut Rope,
384 new_text: &Rope,
385) -> Patch<u32> {
386 let mut translated_unreviewed_edits = Patch::default();
387 let mut conflicting_edits = Vec::new();
388
389 let mut old_edits = patch.edits().iter().cloned().peekable();
390 let mut new_edits = edits.into_iter().peekable();
391 let mut applied_delta = 0i32;
392 let mut rebased_delta = 0i32;
393
394 while let Some(mut new_edit) = new_edits.next() {
395 let mut conflict = false;
396
397 // Push all the old edits that are before this new edit or that intersect with it.
398 while let Some(old_edit) = old_edits.peek() {
399 if new_edit.old.end <= old_edit.new.start {
400 break;
401 } else if new_edit.old.start >= old_edit.new.end {
402 let mut old_edit = old_edits.next().unwrap();
403 old_edit.old.start = (old_edit.old.start as i32 + applied_delta) as u32;
404 old_edit.old.end = (old_edit.old.end as i32 + applied_delta) as u32;
405 old_edit.new.start = (old_edit.new.start as i32 + applied_delta) as u32;
406 old_edit.new.end = (old_edit.new.end as i32 + applied_delta) as u32;
407 rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
408 translated_unreviewed_edits.push(old_edit);
409 } else {
410 conflict = true;
411 if new_edits
412 .peek()
413 .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new))
414 {
415 new_edit.old.start = (new_edit.old.start as i32 + applied_delta) as u32;
416 new_edit.old.end = (new_edit.old.end as i32 + applied_delta) as u32;
417 conflicting_edits.push(new_edit);
418 new_edit = new_edits.next().unwrap();
419 } else {
420 let mut old_edit = old_edits.next().unwrap();
421 old_edit.old.start = (old_edit.old.start as i32 + applied_delta) as u32;
422 old_edit.old.end = (old_edit.old.end as i32 + applied_delta) as u32;
423 old_edit.new.start = (old_edit.new.start as i32 + applied_delta) as u32;
424 old_edit.new.end = (old_edit.new.end as i32 + applied_delta) as u32;
425 rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
426 translated_unreviewed_edits.push(old_edit);
427 }
428 }
429 }
430
431 if conflict {
432 new_edit.old.start = (new_edit.old.start as i32 + applied_delta) as u32;
433 new_edit.old.end = (new_edit.old.end as i32 + applied_delta) as u32;
434 conflicting_edits.push(new_edit);
435 } else {
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 // Push all the outstanding old edits.
459 for mut old_edit in old_edits {
460 old_edit.old.start = (old_edit.old.start as i32 + applied_delta) as u32;
461 old_edit.old.end = (old_edit.old.end as i32 + applied_delta) as u32;
462 old_edit.new.start = (old_edit.new.start as i32 + applied_delta) as u32;
463 old_edit.new.end = (old_edit.new.end as i32 + applied_delta) as u32;
464 translated_unreviewed_edits.push(old_edit);
465 }
466
467 translated_unreviewed_edits.compose(conflicting_edits)
468}
469
470fn diff_snapshots(
471 old_snapshot: &text::BufferSnapshot,
472 new_snapshot: &text::BufferSnapshot,
473) -> Vec<Edit<u32>> {
474 let mut edits = new_snapshot
475 .edits_since::<Point>(&old_snapshot.version)
476 .map(|edit| {
477 if edit.old.start.column == old_snapshot.line_len(edit.old.start.row)
478 && new_snapshot.chars_at(edit.new.start).next() == Some('\n')
479 && edit.old.start != old_snapshot.max_point()
480 {
481 Edit {
482 old: edit.old.start.row + 1..edit.old.end.row + 1,
483 new: edit.new.start.row + 1..edit.new.end.row + 1,
484 }
485 } else if edit.old.start.column == 0
486 && edit.old.end.column == 0
487 && edit.new.end.column == 0
488 && edit.old.end != old_snapshot.max_point()
489 {
490 Edit {
491 old: edit.old.start.row..edit.old.end.row,
492 new: edit.new.start.row..edit.new.end.row,
493 }
494 } else {
495 Edit {
496 old: edit.old.start.row..edit.old.end.row + 1,
497 new: edit.new.start.row..edit.new.end.row + 1,
498 }
499 }
500 })
501 .peekable();
502 let mut row_edits = Vec::new();
503 while let Some(mut edit) = edits.next() {
504 while let Some(next_edit) = edits.peek() {
505 if edit.old.end >= next_edit.old.start {
506 edit.old.end = next_edit.old.end;
507 edit.new.end = next_edit.new.end;
508 edits.next();
509 } else {
510 break;
511 }
512 }
513 row_edits.push(edit);
514 }
515 row_edits
516}
517
518enum ChangeAuthor {
519 User,
520 Agent,
521}
522
523#[derive(Copy, Clone, Eq, PartialEq)]
524enum TrackedBufferStatus {
525 Created,
526 Modified,
527 Deleted,
528}
529
530struct TrackedBuffer {
531 buffer: Entity<Buffer>,
532 base_text: Rope,
533 unreviewed_changes: Patch<u32>,
534 status: TrackedBufferStatus,
535 version: clock::Global,
536 diff: Entity<BufferDiff>,
537 snapshot: text::BufferSnapshot,
538 diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
539 _maintain_diff: Task<()>,
540 _subscription: Subscription,
541}
542
543impl TrackedBuffer {
544 fn has_changes(&self, cx: &App) -> bool {
545 self.diff
546 .read(cx)
547 .hunks(&self.buffer.read(cx), cx)
548 .next()
549 .is_some()
550 }
551
552 fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) {
553 self.diff_update
554 .unbounded_send((author, self.buffer.read(cx).text_snapshot()))
555 .ok();
556 }
557}
558
559pub struct ChangedBuffer {
560 pub diff: Entity<BufferDiff>,
561}
562
563#[cfg(test)]
564mod tests {
565 use std::env;
566
567 use super::*;
568 use buffer_diff::DiffHunkStatusKind;
569 use gpui::TestAppContext;
570 use language::Point;
571 use project::{FakeFs, Fs, Project, RemoveOptions};
572 use rand::prelude::*;
573 use serde_json::json;
574 use settings::SettingsStore;
575 use util::{RandomCharIter, path, post_inc};
576
577 #[ctor::ctor]
578 fn init_logger() {
579 if std::env::var("RUST_LOG").is_ok() {
580 env_logger::init();
581 }
582 }
583
584 #[gpui::test(iterations = 10)]
585 async fn test_edit_review(cx: &mut TestAppContext) {
586 let action_log = cx.new(|_| ActionLog::new());
587 let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
588
589 cx.update(|cx| {
590 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
591 buffer.update(cx, |buffer, cx| {
592 buffer
593 .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
594 .unwrap()
595 });
596 buffer.update(cx, |buffer, cx| {
597 buffer
598 .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
599 .unwrap()
600 });
601 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
602 });
603 cx.run_until_parked();
604 assert_eq!(
605 buffer.read_with(cx, |buffer, _| buffer.text()),
606 "abc\ndEf\nghi\njkl\nmnO"
607 );
608 assert_eq!(
609 unreviewed_hunks(&action_log, cx),
610 vec![(
611 buffer.clone(),
612 vec![
613 HunkStatus {
614 range: Point::new(1, 0)..Point::new(2, 0),
615 diff_status: DiffHunkStatusKind::Modified,
616 old_text: "def\n".into(),
617 },
618 HunkStatus {
619 range: Point::new(4, 0)..Point::new(4, 3),
620 diff_status: DiffHunkStatusKind::Modified,
621 old_text: "mno".into(),
622 }
623 ],
624 )]
625 );
626
627 action_log.update(cx, |log, cx| {
628 log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
629 });
630 cx.run_until_parked();
631 assert_eq!(
632 unreviewed_hunks(&action_log, cx),
633 vec![(
634 buffer.clone(),
635 vec![HunkStatus {
636 range: Point::new(1, 0)..Point::new(2, 0),
637 diff_status: DiffHunkStatusKind::Modified,
638 old_text: "def\n".into(),
639 }],
640 )]
641 );
642
643 action_log.update(cx, |log, cx| {
644 log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
645 });
646 cx.run_until_parked();
647 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
648 }
649
650 #[gpui::test(iterations = 10)]
651 async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
652 let action_log = cx.new(|_| ActionLog::new());
653 let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
654
655 cx.update(|cx| {
656 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
657 buffer.update(cx, |buffer, cx| {
658 buffer
659 .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
660 .unwrap()
661 });
662 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
663 });
664 cx.run_until_parked();
665 assert_eq!(
666 buffer.read_with(cx, |buffer, _| buffer.text()),
667 "abc\ndeF\nGHI\njkl\nmno"
668 );
669 assert_eq!(
670 unreviewed_hunks(&action_log, cx),
671 vec![(
672 buffer.clone(),
673 vec![HunkStatus {
674 range: Point::new(1, 0)..Point::new(3, 0),
675 diff_status: DiffHunkStatusKind::Modified,
676 old_text: "def\nghi\n".into(),
677 }],
678 )]
679 );
680
681 buffer.update(cx, |buffer, cx| {
682 buffer.edit(
683 [
684 (Point::new(0, 2)..Point::new(0, 2), "X"),
685 (Point::new(3, 0)..Point::new(3, 0), "Y"),
686 ],
687 None,
688 cx,
689 )
690 });
691 cx.run_until_parked();
692 assert_eq!(
693 buffer.read_with(cx, |buffer, _| buffer.text()),
694 "abXc\ndeF\nGHI\nYjkl\nmno"
695 );
696 assert_eq!(
697 unreviewed_hunks(&action_log, cx),
698 vec![(
699 buffer.clone(),
700 vec![HunkStatus {
701 range: Point::new(1, 0)..Point::new(3, 0),
702 diff_status: DiffHunkStatusKind::Modified,
703 old_text: "def\nghi\n".into(),
704 }],
705 )]
706 );
707
708 buffer.update(cx, |buffer, cx| {
709 buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx)
710 });
711 cx.run_until_parked();
712 assert_eq!(
713 buffer.read_with(cx, |buffer, _| buffer.text()),
714 "abXc\ndZeF\nGHI\nYjkl\nmno"
715 );
716 assert_eq!(
717 unreviewed_hunks(&action_log, cx),
718 vec![(
719 buffer.clone(),
720 vec![HunkStatus {
721 range: Point::new(1, 0)..Point::new(3, 0),
722 diff_status: DiffHunkStatusKind::Modified,
723 old_text: "def\nghi\n".into(),
724 }],
725 )]
726 );
727
728 action_log.update(cx, |log, cx| {
729 log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
730 });
731 cx.run_until_parked();
732 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
733 }
734
735 #[gpui::test(iterations = 10)]
736 async fn test_creation(cx: &mut TestAppContext) {
737 cx.update(|cx| {
738 let settings_store = SettingsStore::test(cx);
739 cx.set_global(settings_store);
740 language::init(cx);
741 Project::init_settings(cx);
742 });
743
744 let action_log = cx.new(|_| ActionLog::new());
745
746 let fs = FakeFs::new(cx.executor());
747 fs.insert_tree(path!("/dir"), json!({})).await;
748
749 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
750 let file_path = project
751 .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
752 .unwrap();
753
754 // Simulate file2 being recreated by a tool.
755 let buffer = project
756 .update(cx, |project, cx| project.open_buffer(file_path, cx))
757 .await
758 .unwrap();
759 cx.update(|cx| {
760 buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
761 action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
762 });
763 project
764 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
765 .await
766 .unwrap();
767 cx.run_until_parked();
768 assert_eq!(
769 unreviewed_hunks(&action_log, cx),
770 vec![(
771 buffer.clone(),
772 vec![HunkStatus {
773 range: Point::new(0, 0)..Point::new(0, 5),
774 diff_status: DiffHunkStatusKind::Added,
775 old_text: "".into(),
776 }],
777 )]
778 );
779
780 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx));
781 cx.run_until_parked();
782 assert_eq!(
783 unreviewed_hunks(&action_log, cx),
784 vec![(
785 buffer.clone(),
786 vec![HunkStatus {
787 range: Point::new(0, 0)..Point::new(0, 6),
788 diff_status: DiffHunkStatusKind::Added,
789 old_text: "".into(),
790 }],
791 )]
792 );
793
794 action_log.update(cx, |log, cx| {
795 log.keep_edits_in_range(buffer.clone(), 0..5, cx)
796 });
797 cx.run_until_parked();
798 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
799 }
800
801 #[gpui::test(iterations = 10)]
802 async fn test_deletion(cx: &mut TestAppContext) {
803 cx.update(|cx| {
804 let settings_store = SettingsStore::test(cx);
805 cx.set_global(settings_store);
806 language::init(cx);
807 Project::init_settings(cx);
808 });
809
810 let fs = FakeFs::new(cx.executor());
811 fs.insert_tree(
812 path!("/dir"),
813 json!({"file1": "lorem\n", "file2": "ipsum\n"}),
814 )
815 .await;
816
817 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
818 let file1_path = project
819 .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
820 .unwrap();
821 let file2_path = project
822 .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
823 .unwrap();
824
825 let action_log = cx.new(|_| ActionLog::new());
826 let buffer1 = project
827 .update(cx, |project, cx| {
828 project.open_buffer(file1_path.clone(), cx)
829 })
830 .await
831 .unwrap();
832 let buffer2 = project
833 .update(cx, |project, cx| {
834 project.open_buffer(file2_path.clone(), cx)
835 })
836 .await
837 .unwrap();
838
839 action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
840 action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
841 project
842 .update(cx, |project, cx| {
843 project.delete_file(file1_path.clone(), false, cx)
844 })
845 .unwrap()
846 .await
847 .unwrap();
848 project
849 .update(cx, |project, cx| {
850 project.delete_file(file2_path.clone(), false, cx)
851 })
852 .unwrap()
853 .await
854 .unwrap();
855 cx.run_until_parked();
856 assert_eq!(
857 unreviewed_hunks(&action_log, cx),
858 vec![
859 (
860 buffer1.clone(),
861 vec![HunkStatus {
862 range: Point::new(0, 0)..Point::new(0, 0),
863 diff_status: DiffHunkStatusKind::Deleted,
864 old_text: "lorem\n".into(),
865 }]
866 ),
867 (
868 buffer2.clone(),
869 vec![HunkStatus {
870 range: Point::new(0, 0)..Point::new(0, 0),
871 diff_status: DiffHunkStatusKind::Deleted,
872 old_text: "ipsum\n".into(),
873 }],
874 )
875 ]
876 );
877
878 // Simulate file1 being recreated externally.
879 fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
880 .await;
881
882 // Simulate file2 being recreated by a tool.
883 let buffer2 = project
884 .update(cx, |project, cx| project.open_buffer(file2_path, cx))
885 .await
886 .unwrap();
887 buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
888 action_log.update(cx, |log, cx| log.will_create_buffer(buffer2.clone(), cx));
889 project
890 .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
891 .await
892 .unwrap();
893
894 cx.run_until_parked();
895 assert_eq!(
896 unreviewed_hunks(&action_log, cx),
897 vec![(
898 buffer2.clone(),
899 vec![HunkStatus {
900 range: Point::new(0, 0)..Point::new(0, 5),
901 diff_status: DiffHunkStatusKind::Modified,
902 old_text: "ipsum\n".into(),
903 }],
904 )]
905 );
906
907 // Simulate file2 being deleted externally.
908 fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
909 .await
910 .unwrap();
911 cx.run_until_parked();
912 assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
913 }
914
915 #[gpui::test(iterations = 100)]
916 async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
917 let operations = env::var("OPERATIONS")
918 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
919 .unwrap_or(20);
920
921 let action_log = cx.new(|_| ActionLog::new());
922 let text = RandomCharIter::new(&mut rng).take(50).collect::<String>();
923 let buffer = cx.new(|cx| Buffer::local(text, cx));
924 action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
925
926 for _ in 0..operations {
927 match rng.gen_range(0..100) {
928 0..25 => {
929 action_log.update(cx, |log, cx| {
930 let range = buffer.read(cx).random_byte_range(0, &mut rng);
931 log::info!("keeping all edits in range {:?}", range);
932 log.keep_edits_in_range(buffer.clone(), range, cx)
933 });
934 }
935 _ => {
936 let is_agent_change = rng.gen_bool(0.5);
937 if is_agent_change {
938 log::info!("agent edit");
939 } else {
940 log::info!("user edit");
941 }
942 cx.update(|cx| {
943 buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
944 if is_agent_change {
945 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
946 }
947 });
948 }
949 }
950
951 if rng.gen_bool(0.2) {
952 quiesce(&action_log, &buffer, cx);
953 }
954 }
955
956 quiesce(&action_log, &buffer, cx);
957
958 fn quiesce(
959 action_log: &Entity<ActionLog>,
960 buffer: &Entity<Buffer>,
961 cx: &mut TestAppContext,
962 ) {
963 log::info!("quiescing...");
964 cx.run_until_parked();
965 action_log.update(cx, |log, cx| {
966 let tracked_buffer = log.track_buffer(buffer.clone(), false, cx);
967 let mut old_text = tracked_buffer.base_text.clone();
968 let new_text = buffer.read(cx).as_rope();
969 for edit in tracked_buffer.unreviewed_changes.edits() {
970 let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
971 let old_end = old_text.point_to_offset(cmp::min(
972 Point::new(edit.new.start + edit.old_len(), 0),
973 old_text.max_point(),
974 ));
975 old_text.replace(
976 old_start..old_end,
977 &new_text.slice_rows(edit.new.clone()).to_string(),
978 );
979 }
980 pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
981 })
982 }
983 }
984
985 #[gpui::test(iterations = 100)]
986 fn test_rebase_random(mut rng: StdRng) {
987 let operations = env::var("OPERATIONS")
988 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
989 .unwrap_or(20);
990
991 let mut next_line_id = 0;
992 let base_lines = (0..rng.gen_range(1..=20))
993 .map(|_| post_inc(&mut next_line_id).to_string())
994 .collect::<Vec<_>>();
995 log::info!("base lines: {:?}", base_lines);
996
997 let (new_lines, patch_1) =
998 build_edits(&base_lines, operations, &mut rng, &mut next_line_id);
999 log::info!("agent edits: {:#?}", patch_1);
1000 let (new_lines, patch_2) = build_edits(&new_lines, operations, &mut rng, &mut next_line_id);
1001 log::info!("user edits: {:#?}", patch_2);
1002
1003 let mut old_text = Rope::from(base_lines.join("\n"));
1004 let new_text = Rope::from(new_lines.join("\n"));
1005 let patch = rebase_patch(&patch_1, patch_2.into_inner(), &mut old_text, &new_text);
1006 log::info!("rebased edits: {:#?}", patch.edits());
1007
1008 for edit in patch.edits() {
1009 let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
1010 let old_end = old_text.point_to_offset(cmp::min(
1011 Point::new(edit.new.start + edit.old_len(), 0),
1012 old_text.max_point(),
1013 ));
1014 old_text.replace(
1015 old_start..old_end,
1016 &new_text.slice_rows(edit.new.clone()).to_string(),
1017 );
1018 }
1019 pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
1020 }
1021
1022 fn build_edits(
1023 lines: &Vec<String>,
1024 count: usize,
1025 rng: &mut StdRng,
1026 next_line_id: &mut usize,
1027 ) -> (Vec<String>, Patch<u32>) {
1028 let mut delta = 0i32;
1029 let mut last_edit_end = 0;
1030 let mut edits = Patch::default();
1031 let mut edited_lines = lines.clone();
1032 for _ in 0..count {
1033 if last_edit_end >= lines.len() {
1034 break;
1035 }
1036
1037 let end = rng.gen_range(last_edit_end..lines.len());
1038 let start = rng.gen_range(last_edit_end..=end);
1039 let old_len = end - start;
1040
1041 let mut new_len: usize = rng.gen_range(0..=3);
1042 if start == end && new_len == 0 {
1043 new_len += 1;
1044 }
1045
1046 last_edit_end = end + 1;
1047
1048 let new_lines = (0..new_len)
1049 .map(|_| post_inc(next_line_id).to_string())
1050 .collect::<Vec<_>>();
1051 log::info!(" editing {:?}: {:?}", start..end, new_lines);
1052 let old = start as u32..end as u32;
1053 let new = (start as i32 + delta) as u32..(start as i32 + delta + new_len as i32) as u32;
1054 edited_lines.splice(
1055 new.start as usize..new.start as usize + old.len(),
1056 new_lines,
1057 );
1058 edits.push(Edit { old, new });
1059 delta += new_len as i32 - old_len as i32;
1060 }
1061 (edited_lines, edits)
1062 }
1063
1064 #[derive(Debug, Clone, PartialEq, Eq)]
1065 struct HunkStatus {
1066 range: Range<Point>,
1067 diff_status: DiffHunkStatusKind,
1068 old_text: String,
1069 }
1070
1071 fn unreviewed_hunks(
1072 action_log: &Entity<ActionLog>,
1073 cx: &TestAppContext,
1074 ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
1075 cx.read(|cx| {
1076 action_log
1077 .read(cx)
1078 .changed_buffers(cx)
1079 .into_iter()
1080 .map(|(buffer, diff)| {
1081 let snapshot = buffer.read(cx).snapshot();
1082 (
1083 buffer,
1084 diff.read(cx)
1085 .hunks(&snapshot, cx)
1086 .map(|hunk| HunkStatus {
1087 diff_status: hunk.status().kind,
1088 range: hunk.range,
1089 old_text: diff
1090 .read(cx)
1091 .base_text()
1092 .text_for_range(hunk.diff_base_byte_range)
1093 .collect(),
1094 })
1095 .collect(),
1096 )
1097 })
1098 .collect()
1099 })
1100 }
1101}