action_log.rs

   1use anyhow::{Context as _, Result};
   2use buffer_diff::BufferDiff;
   3use collections::BTreeMap;
   4use futures::{StreamExt, channel::mpsc};
   5use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
   6use language::{Anchor, Buffer, BufferEvent, DiskState, Point};
   7use project::{Project, ProjectItem};
   8use std::{cmp, ops::Range, sync::Arc};
   9use text::{Edit, Patch, Rope};
  10use util::RangeExt;
  11
  12/// Tracks actions performed by tools in a thread
  13pub struct ActionLog {
  14    /// Buffers that we want to notify the model about when they change.
  15    tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
  16    /// Has the model edited a file since it last checked diagnostics?
  17    edited_since_project_diagnostics_check: bool,
  18    /// The project this action log is associated with
  19    project: Entity<Project>,
  20}
  21
  22impl ActionLog {
  23    /// Creates a new, empty action log associated with the given project.
  24    pub fn new(project: Entity<Project>) -> Self {
  25        Self {
  26            tracked_buffers: BTreeMap::default(),
  27            edited_since_project_diagnostics_check: false,
  28            project,
  29        }
  30    }
  31
  32    /// 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 that was added as context, so we can notify the model about user edits.
 263    pub fn buffer_added_as_context(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 264        self.track_buffer(buffer, false, cx);
 265    }
 266
 267    /// Track a buffer as read, so we can notify the model about user edits.
 268    pub fn will_create_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 269        self.track_buffer(buffer.clone(), true, cx);
 270        self.buffer_edited(buffer, cx)
 271    }
 272
 273    /// Mark a buffer as edited, so we can refresh it in the context
 274    pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 275        self.edited_since_project_diagnostics_check = true;
 276
 277        let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
 278        if let TrackedBufferStatus::Deleted = tracked_buffer.status {
 279            tracked_buffer.status = TrackedBufferStatus::Modified;
 280        }
 281        tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
 282    }
 283
 284    pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 285        let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
 286        match tracked_buffer.status {
 287            TrackedBufferStatus::Created => {
 288                self.tracked_buffers.remove(&buffer);
 289                cx.notify();
 290            }
 291            TrackedBufferStatus::Modified => {
 292                buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
 293                tracked_buffer.status = TrackedBufferStatus::Deleted;
 294                tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
 295            }
 296            TrackedBufferStatus::Deleted => {}
 297        }
 298        cx.notify();
 299    }
 300
 301    pub fn keep_edits_in_range(
 302        &mut self,
 303        buffer: Entity<Buffer>,
 304        buffer_range: Range<impl language::ToPoint>,
 305        cx: &mut Context<Self>,
 306    ) {
 307        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 308            return;
 309        };
 310
 311        match tracked_buffer.status {
 312            TrackedBufferStatus::Deleted => {
 313                self.tracked_buffers.remove(&buffer);
 314                cx.notify();
 315            }
 316            _ => {
 317                let buffer = buffer.read(cx);
 318                let buffer_range =
 319                    buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
 320                let mut delta = 0i32;
 321
 322                tracked_buffer.unreviewed_changes.retain_mut(|edit| {
 323                    edit.old.start = (edit.old.start as i32 + delta) as u32;
 324                    edit.old.end = (edit.old.end as i32 + delta) as u32;
 325
 326                    if buffer_range.end.row < edit.new.start
 327                        || buffer_range.start.row > edit.new.end
 328                    {
 329                        true
 330                    } else {
 331                        let old_range = tracked_buffer
 332                            .base_text
 333                            .point_to_offset(Point::new(edit.old.start, 0))
 334                            ..tracked_buffer.base_text.point_to_offset(cmp::min(
 335                                Point::new(edit.old.end, 0),
 336                                tracked_buffer.base_text.max_point(),
 337                            ));
 338                        let new_range = tracked_buffer
 339                            .snapshot
 340                            .point_to_offset(Point::new(edit.new.start, 0))
 341                            ..tracked_buffer.snapshot.point_to_offset(cmp::min(
 342                                Point::new(edit.new.end, 0),
 343                                tracked_buffer.snapshot.max_point(),
 344                            ));
 345                        tracked_buffer.base_text.replace(
 346                            old_range,
 347                            &tracked_buffer
 348                                .snapshot
 349                                .text_for_range(new_range)
 350                                .collect::<String>(),
 351                        );
 352                        delta += edit.new_len() as i32 - edit.old_len() as i32;
 353                        false
 354                    }
 355                });
 356                tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
 357            }
 358        }
 359    }
 360
 361    pub fn reject_edits_in_range(
 362        &mut self,
 363        buffer: Entity<Buffer>,
 364        buffer_range: Range<impl language::ToPoint>,
 365        cx: &mut Context<Self>,
 366    ) -> Task<Result<()>> {
 367        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 368            return Task::ready(Ok(()));
 369        };
 370
 371        match tracked_buffer.status {
 372            TrackedBufferStatus::Created => {
 373                let delete = buffer
 374                    .read(cx)
 375                    .entry_id(cx)
 376                    .and_then(|entry_id| {
 377                        self.project
 378                            .update(cx, |project, cx| project.delete_entry(entry_id, false, cx))
 379                    })
 380                    .unwrap_or(Task::ready(Ok(())));
 381                self.tracked_buffers.remove(&buffer);
 382                cx.notify();
 383                delete
 384            }
 385            TrackedBufferStatus::Deleted => {
 386                buffer.update(cx, |buffer, cx| {
 387                    buffer.set_text(tracked_buffer.base_text.to_string(), cx)
 388                });
 389                let save = self
 390                    .project
 391                    .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
 392
 393                // Clear all tracked changes for this buffer and start over as if we just read it.
 394                self.tracked_buffers.remove(&buffer);
 395                self.track_buffer(buffer.clone(), false, cx);
 396                cx.notify();
 397                save
 398            }
 399            TrackedBufferStatus::Modified => {
 400                buffer.update(cx, |buffer, cx| {
 401                    let buffer_range =
 402                        buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
 403
 404                    let mut edits_to_revert = Vec::new();
 405                    for edit in tracked_buffer.unreviewed_changes.edits() {
 406                        if buffer_range.end.row < edit.new.start {
 407                            break;
 408                        } else if buffer_range.start.row > edit.new.end {
 409                            continue;
 410                        }
 411
 412                        let old_range = tracked_buffer
 413                            .base_text
 414                            .point_to_offset(Point::new(edit.old.start, 0))
 415                            ..tracked_buffer.base_text.point_to_offset(cmp::min(
 416                                Point::new(edit.old.end, 0),
 417                                tracked_buffer.base_text.max_point(),
 418                            ));
 419                        let old_text = tracked_buffer
 420                            .base_text
 421                            .chunks_in_range(old_range)
 422                            .collect::<String>();
 423
 424                        let new_range = tracked_buffer
 425                            .snapshot
 426                            .anchor_before(Point::new(edit.new.start, 0))
 427                            ..tracked_buffer.snapshot.anchor_after(cmp::min(
 428                                Point::new(edit.new.end, 0),
 429                                tracked_buffer.snapshot.max_point(),
 430                            ));
 431                        edits_to_revert.push((new_range, old_text));
 432                    }
 433
 434                    buffer.edit(edits_to_revert, None, cx);
 435                });
 436                self.project
 437                    .update(cx, |project, cx| project.save_buffer(buffer, cx))
 438            }
 439        }
 440    }
 441
 442    pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) {
 443        self.tracked_buffers
 444            .retain(|_buffer, tracked_buffer| match tracked_buffer.status {
 445                TrackedBufferStatus::Deleted => false,
 446                _ => {
 447                    tracked_buffer.unreviewed_changes.clear();
 448                    tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone();
 449                    tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
 450                    true
 451                }
 452            });
 453        cx.notify();
 454    }
 455
 456    /// Returns the set of buffers that contain changes that haven't been reviewed by the user.
 457    pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
 458        self.tracked_buffers
 459            .iter()
 460            .filter(|(_, tracked)| tracked.has_changes(cx))
 461            .map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone()))
 462            .collect()
 463    }
 464
 465    /// Iterate over buffers changed since last read or edited by the model
 466    pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
 467        self.tracked_buffers
 468            .iter()
 469            .filter(|(buffer, tracked)| {
 470                let buffer = buffer.read(cx);
 471
 472                tracked.version != buffer.version
 473                    && buffer
 474                        .file()
 475                        .map_or(false, |file| file.disk_state() != DiskState::Deleted)
 476            })
 477            .map(|(buffer, _)| buffer)
 478    }
 479}
 480
 481fn apply_non_conflicting_edits(
 482    patch: &Patch<u32>,
 483    edits: Vec<Edit<u32>>,
 484    old_text: &mut Rope,
 485    new_text: &Rope,
 486) {
 487    let mut old_edits = patch.edits().iter().cloned().peekable();
 488    let mut new_edits = edits.into_iter().peekable();
 489    let mut applied_delta = 0i32;
 490    let mut rebased_delta = 0i32;
 491
 492    while let Some(mut new_edit) = new_edits.next() {
 493        let mut conflict = false;
 494
 495        // Push all the old edits that are before this new edit or that intersect with it.
 496        while let Some(old_edit) = old_edits.peek() {
 497            if new_edit.old.end < old_edit.new.start
 498                || (!old_edit.new.is_empty() && new_edit.old.end == old_edit.new.start)
 499            {
 500                break;
 501            } else if new_edit.old.start > old_edit.new.end
 502                || (!old_edit.new.is_empty() && new_edit.old.start == old_edit.new.end)
 503            {
 504                let old_edit = old_edits.next().unwrap();
 505                rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
 506            } else {
 507                conflict = true;
 508                if new_edits
 509                    .peek()
 510                    .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new))
 511                {
 512                    new_edit = new_edits.next().unwrap();
 513                } else {
 514                    let old_edit = old_edits.next().unwrap();
 515                    rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
 516                }
 517            }
 518        }
 519
 520        if !conflict {
 521            // This edit doesn't intersect with any old edit, so we can apply it to the old text.
 522            new_edit.old.start = (new_edit.old.start as i32 + applied_delta - rebased_delta) as u32;
 523            new_edit.old.end = (new_edit.old.end as i32 + applied_delta - rebased_delta) as u32;
 524            let old_bytes = old_text.point_to_offset(Point::new(new_edit.old.start, 0))
 525                ..old_text.point_to_offset(cmp::min(
 526                    Point::new(new_edit.old.end, 0),
 527                    old_text.max_point(),
 528                ));
 529            let new_bytes = new_text.point_to_offset(Point::new(new_edit.new.start, 0))
 530                ..new_text.point_to_offset(cmp::min(
 531                    Point::new(new_edit.new.end, 0),
 532                    new_text.max_point(),
 533                ));
 534
 535            old_text.replace(
 536                old_bytes,
 537                &new_text.chunks_in_range(new_bytes).collect::<String>(),
 538            );
 539            applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32;
 540        }
 541    }
 542}
 543
 544fn diff_snapshots(
 545    old_snapshot: &text::BufferSnapshot,
 546    new_snapshot: &text::BufferSnapshot,
 547) -> Vec<Edit<u32>> {
 548    let mut edits = new_snapshot
 549        .edits_since::<Point>(&old_snapshot.version)
 550        .map(|edit| point_to_row_edit(edit, old_snapshot.as_rope(), new_snapshot.as_rope()))
 551        .peekable();
 552    let mut row_edits = Vec::new();
 553    while let Some(mut edit) = edits.next() {
 554        while let Some(next_edit) = edits.peek() {
 555            if edit.old.end >= next_edit.old.start {
 556                edit.old.end = next_edit.old.end;
 557                edit.new.end = next_edit.new.end;
 558                edits.next();
 559            } else {
 560                break;
 561            }
 562        }
 563        row_edits.push(edit);
 564    }
 565    row_edits
 566}
 567
 568fn point_to_row_edit(edit: Edit<Point>, old_text: &Rope, new_text: &Rope) -> Edit<u32> {
 569    if edit.old.start.column == old_text.line_len(edit.old.start.row)
 570        && new_text
 571            .chars_at(new_text.point_to_offset(edit.new.start))
 572            .next()
 573            == Some('\n')
 574        && edit.old.start != old_text.max_point()
 575    {
 576        Edit {
 577            old: edit.old.start.row + 1..edit.old.end.row + 1,
 578            new: edit.new.start.row + 1..edit.new.end.row + 1,
 579        }
 580    } else if edit.old.start.column == 0
 581        && edit.old.end.column == 0
 582        && edit.new.end.column == 0
 583        && edit.old.end != old_text.max_point()
 584    {
 585        Edit {
 586            old: edit.old.start.row..edit.old.end.row,
 587            new: edit.new.start.row..edit.new.end.row,
 588        }
 589    } else {
 590        Edit {
 591            old: edit.old.start.row..edit.old.end.row + 1,
 592            new: edit.new.start.row..edit.new.end.row + 1,
 593        }
 594    }
 595}
 596
 597enum ChangeAuthor {
 598    User,
 599    Agent,
 600}
 601
 602#[derive(Copy, Clone, Eq, PartialEq)]
 603enum TrackedBufferStatus {
 604    Created,
 605    Modified,
 606    Deleted,
 607}
 608
 609struct TrackedBuffer {
 610    buffer: Entity<Buffer>,
 611    base_text: Rope,
 612    unreviewed_changes: Patch<u32>,
 613    status: TrackedBufferStatus,
 614    version: clock::Global,
 615    diff: Entity<BufferDiff>,
 616    snapshot: text::BufferSnapshot,
 617    diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
 618    _maintain_diff: Task<()>,
 619    _subscription: Subscription,
 620}
 621
 622impl TrackedBuffer {
 623    fn has_changes(&self, cx: &App) -> bool {
 624        self.diff
 625            .read(cx)
 626            .hunks(&self.buffer.read(cx), cx)
 627            .next()
 628            .is_some()
 629    }
 630
 631    fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) {
 632        self.diff_update
 633            .unbounded_send((author, self.buffer.read(cx).text_snapshot()))
 634            .ok();
 635    }
 636}
 637
 638pub struct ChangedBuffer {
 639    pub diff: Entity<BufferDiff>,
 640}
 641
 642#[cfg(test)]
 643mod tests {
 644    use std::env;
 645
 646    use super::*;
 647    use buffer_diff::DiffHunkStatusKind;
 648    use gpui::TestAppContext;
 649    use language::Point;
 650    use project::{FakeFs, Fs, Project, RemoveOptions};
 651    use rand::prelude::*;
 652    use serde_json::json;
 653    use settings::SettingsStore;
 654    use util::{RandomCharIter, path};
 655
 656    #[ctor::ctor]
 657    fn init_logger() {
 658        if std::env::var("RUST_LOG").is_ok() {
 659            env_logger::init();
 660        }
 661    }
 662
 663    fn init_test(cx: &mut TestAppContext) {
 664        cx.update(|cx| {
 665            let settings_store = SettingsStore::test(cx);
 666            cx.set_global(settings_store);
 667            language::init(cx);
 668            Project::init_settings(cx);
 669        });
 670    }
 671
 672    #[gpui::test(iterations = 10)]
 673    async fn test_keep_edits(cx: &mut TestAppContext) {
 674        init_test(cx);
 675
 676        let fs = FakeFs::new(cx.executor());
 677        let project = Project::test(fs.clone(), [], cx).await;
 678        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 679        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
 680
 681        cx.update(|cx| {
 682            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 683            buffer.update(cx, |buffer, cx| {
 684                buffer
 685                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
 686                    .unwrap()
 687            });
 688            buffer.update(cx, |buffer, cx| {
 689                buffer
 690                    .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
 691                    .unwrap()
 692            });
 693            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 694        });
 695        cx.run_until_parked();
 696        assert_eq!(
 697            buffer.read_with(cx, |buffer, _| buffer.text()),
 698            "abc\ndEf\nghi\njkl\nmnO"
 699        );
 700        assert_eq!(
 701            unreviewed_hunks(&action_log, cx),
 702            vec![(
 703                buffer.clone(),
 704                vec![
 705                    HunkStatus {
 706                        range: Point::new(1, 0)..Point::new(2, 0),
 707                        diff_status: DiffHunkStatusKind::Modified,
 708                        old_text: "def\n".into(),
 709                    },
 710                    HunkStatus {
 711                        range: Point::new(4, 0)..Point::new(4, 3),
 712                        diff_status: DiffHunkStatusKind::Modified,
 713                        old_text: "mno".into(),
 714                    }
 715                ],
 716            )]
 717        );
 718
 719        action_log.update(cx, |log, cx| {
 720            log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
 721        });
 722        cx.run_until_parked();
 723        assert_eq!(
 724            unreviewed_hunks(&action_log, cx),
 725            vec![(
 726                buffer.clone(),
 727                vec![HunkStatus {
 728                    range: Point::new(1, 0)..Point::new(2, 0),
 729                    diff_status: DiffHunkStatusKind::Modified,
 730                    old_text: "def\n".into(),
 731                }],
 732            )]
 733        );
 734
 735        action_log.update(cx, |log, cx| {
 736            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
 737        });
 738        cx.run_until_parked();
 739        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 740    }
 741
 742    #[gpui::test(iterations = 10)]
 743    async fn test_deletions(cx: &mut TestAppContext) {
 744        init_test(cx);
 745
 746        let fs = FakeFs::new(cx.executor());
 747        let project = Project::test(fs.clone(), [], cx).await;
 748        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 749        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno\npqr", cx));
 750
 751        cx.update(|cx| {
 752            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 753            buffer.update(cx, |buffer, cx| {
 754                buffer
 755                    .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx)
 756                    .unwrap();
 757                buffer.finalize_last_transaction();
 758            });
 759            buffer.update(cx, |buffer, cx| {
 760                buffer
 761                    .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx)
 762                    .unwrap();
 763                buffer.finalize_last_transaction();
 764            });
 765            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 766        });
 767        cx.run_until_parked();
 768        assert_eq!(
 769            buffer.read_with(cx, |buffer, _| buffer.text()),
 770            "abc\nghi\njkl\npqr"
 771        );
 772        assert_eq!(
 773            unreviewed_hunks(&action_log, cx),
 774            vec![(
 775                buffer.clone(),
 776                vec![
 777                    HunkStatus {
 778                        range: Point::new(1, 0)..Point::new(1, 0),
 779                        diff_status: DiffHunkStatusKind::Deleted,
 780                        old_text: "def\n".into(),
 781                    },
 782                    HunkStatus {
 783                        range: Point::new(3, 0)..Point::new(3, 0),
 784                        diff_status: DiffHunkStatusKind::Deleted,
 785                        old_text: "mno\n".into(),
 786                    }
 787                ],
 788            )]
 789        );
 790
 791        buffer.update(cx, |buffer, cx| buffer.undo(cx));
 792        cx.run_until_parked();
 793        assert_eq!(
 794            buffer.read_with(cx, |buffer, _| buffer.text()),
 795            "abc\nghi\njkl\nmno\npqr"
 796        );
 797        assert_eq!(
 798            unreviewed_hunks(&action_log, cx),
 799            vec![(
 800                buffer.clone(),
 801                vec![HunkStatus {
 802                    range: Point::new(1, 0)..Point::new(1, 0),
 803                    diff_status: DiffHunkStatusKind::Deleted,
 804                    old_text: "def\n".into(),
 805                }],
 806            )]
 807        );
 808
 809        action_log.update(cx, |log, cx| {
 810            log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx)
 811        });
 812        cx.run_until_parked();
 813        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 814    }
 815
 816    #[gpui::test(iterations = 10)]
 817    async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
 818        init_test(cx);
 819
 820        let fs = FakeFs::new(cx.executor());
 821        let project = Project::test(fs.clone(), [], cx).await;
 822        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 823        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
 824
 825        cx.update(|cx| {
 826            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 827            buffer.update(cx, |buffer, cx| {
 828                buffer
 829                    .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
 830                    .unwrap()
 831            });
 832            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 833        });
 834        cx.run_until_parked();
 835        assert_eq!(
 836            buffer.read_with(cx, |buffer, _| buffer.text()),
 837            "abc\ndeF\nGHI\njkl\nmno"
 838        );
 839        assert_eq!(
 840            unreviewed_hunks(&action_log, cx),
 841            vec![(
 842                buffer.clone(),
 843                vec![HunkStatus {
 844                    range: Point::new(1, 0)..Point::new(3, 0),
 845                    diff_status: DiffHunkStatusKind::Modified,
 846                    old_text: "def\nghi\n".into(),
 847                }],
 848            )]
 849        );
 850
 851        buffer.update(cx, |buffer, cx| {
 852            buffer.edit(
 853                [
 854                    (Point::new(0, 2)..Point::new(0, 2), "X"),
 855                    (Point::new(3, 0)..Point::new(3, 0), "Y"),
 856                ],
 857                None,
 858                cx,
 859            )
 860        });
 861        cx.run_until_parked();
 862        assert_eq!(
 863            buffer.read_with(cx, |buffer, _| buffer.text()),
 864            "abXc\ndeF\nGHI\nYjkl\nmno"
 865        );
 866        assert_eq!(
 867            unreviewed_hunks(&action_log, cx),
 868            vec![(
 869                buffer.clone(),
 870                vec![HunkStatus {
 871                    range: Point::new(1, 0)..Point::new(3, 0),
 872                    diff_status: DiffHunkStatusKind::Modified,
 873                    old_text: "def\nghi\n".into(),
 874                }],
 875            )]
 876        );
 877
 878        buffer.update(cx, |buffer, cx| {
 879            buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx)
 880        });
 881        cx.run_until_parked();
 882        assert_eq!(
 883            buffer.read_with(cx, |buffer, _| buffer.text()),
 884            "abXc\ndZeF\nGHI\nYjkl\nmno"
 885        );
 886        assert_eq!(
 887            unreviewed_hunks(&action_log, cx),
 888            vec![(
 889                buffer.clone(),
 890                vec![HunkStatus {
 891                    range: Point::new(1, 0)..Point::new(3, 0),
 892                    diff_status: DiffHunkStatusKind::Modified,
 893                    old_text: "def\nghi\n".into(),
 894                }],
 895            )]
 896        );
 897
 898        action_log.update(cx, |log, cx| {
 899            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
 900        });
 901        cx.run_until_parked();
 902        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 903    }
 904
 905    #[gpui::test(iterations = 10)]
 906    async fn test_creating_files(cx: &mut TestAppContext) {
 907        init_test(cx);
 908
 909        let fs = FakeFs::new(cx.executor());
 910        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 911        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 912
 913        let fs = FakeFs::new(cx.executor());
 914        fs.insert_tree(path!("/dir"), json!({})).await;
 915
 916        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 917        let file_path = project
 918            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
 919            .unwrap();
 920
 921        // Simulate file2 being recreated by a tool.
 922        let buffer = project
 923            .update(cx, |project, cx| project.open_buffer(file_path, cx))
 924            .await
 925            .unwrap();
 926        cx.update(|cx| {
 927            buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
 928            action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
 929        });
 930        project
 931            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 932            .await
 933            .unwrap();
 934        cx.run_until_parked();
 935        assert_eq!(
 936            unreviewed_hunks(&action_log, cx),
 937            vec![(
 938                buffer.clone(),
 939                vec![HunkStatus {
 940                    range: Point::new(0, 0)..Point::new(0, 5),
 941                    diff_status: DiffHunkStatusKind::Added,
 942                    old_text: "".into(),
 943                }],
 944            )]
 945        );
 946
 947        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx));
 948        cx.run_until_parked();
 949        assert_eq!(
 950            unreviewed_hunks(&action_log, cx),
 951            vec![(
 952                buffer.clone(),
 953                vec![HunkStatus {
 954                    range: Point::new(0, 0)..Point::new(0, 6),
 955                    diff_status: DiffHunkStatusKind::Added,
 956                    old_text: "".into(),
 957                }],
 958            )]
 959        );
 960
 961        action_log.update(cx, |log, cx| {
 962            log.keep_edits_in_range(buffer.clone(), 0..5, cx)
 963        });
 964        cx.run_until_parked();
 965        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 966    }
 967
 968    #[gpui::test(iterations = 10)]
 969    async fn test_deleting_files(cx: &mut TestAppContext) {
 970        init_test(cx);
 971
 972        let fs = FakeFs::new(cx.executor());
 973        fs.insert_tree(
 974            path!("/dir"),
 975            json!({"file1": "lorem\n", "file2": "ipsum\n"}),
 976        )
 977        .await;
 978
 979        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 980        let file1_path = project
 981            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
 982            .unwrap();
 983        let file2_path = project
 984            .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
 985            .unwrap();
 986
 987        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 988        let buffer1 = project
 989            .update(cx, |project, cx| {
 990                project.open_buffer(file1_path.clone(), cx)
 991            })
 992            .await
 993            .unwrap();
 994        let buffer2 = project
 995            .update(cx, |project, cx| {
 996                project.open_buffer(file2_path.clone(), cx)
 997            })
 998            .await
 999            .unwrap();
1000
1001        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
1002        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
1003        project
1004            .update(cx, |project, cx| {
1005                project.delete_file(file1_path.clone(), false, cx)
1006            })
1007            .unwrap()
1008            .await
1009            .unwrap();
1010        project
1011            .update(cx, |project, cx| {
1012                project.delete_file(file2_path.clone(), false, cx)
1013            })
1014            .unwrap()
1015            .await
1016            .unwrap();
1017        cx.run_until_parked();
1018        assert_eq!(
1019            unreviewed_hunks(&action_log, cx),
1020            vec![
1021                (
1022                    buffer1.clone(),
1023                    vec![HunkStatus {
1024                        range: Point::new(0, 0)..Point::new(0, 0),
1025                        diff_status: DiffHunkStatusKind::Deleted,
1026                        old_text: "lorem\n".into(),
1027                    }]
1028                ),
1029                (
1030                    buffer2.clone(),
1031                    vec![HunkStatus {
1032                        range: Point::new(0, 0)..Point::new(0, 0),
1033                        diff_status: DiffHunkStatusKind::Deleted,
1034                        old_text: "ipsum\n".into(),
1035                    }],
1036                )
1037            ]
1038        );
1039
1040        // Simulate file1 being recreated externally.
1041        fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
1042            .await;
1043
1044        // Simulate file2 being recreated by a tool.
1045        let buffer2 = project
1046            .update(cx, |project, cx| project.open_buffer(file2_path, cx))
1047            .await
1048            .unwrap();
1049        buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
1050        action_log.update(cx, |log, cx| log.will_create_buffer(buffer2.clone(), cx));
1051        project
1052            .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
1053            .await
1054            .unwrap();
1055
1056        cx.run_until_parked();
1057        assert_eq!(
1058            unreviewed_hunks(&action_log, cx),
1059            vec![(
1060                buffer2.clone(),
1061                vec![HunkStatus {
1062                    range: Point::new(0, 0)..Point::new(0, 5),
1063                    diff_status: DiffHunkStatusKind::Modified,
1064                    old_text: "ipsum\n".into(),
1065                }],
1066            )]
1067        );
1068
1069        // Simulate file2 being deleted externally.
1070        fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
1071            .await
1072            .unwrap();
1073        cx.run_until_parked();
1074        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1075    }
1076
1077    #[gpui::test(iterations = 10)]
1078    async fn test_reject_edits(cx: &mut TestAppContext) {
1079        init_test(cx);
1080
1081        let fs = FakeFs::new(cx.executor());
1082        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
1083            .await;
1084        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1085        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1086        let file_path = project
1087            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1088            .unwrap();
1089        let buffer = project
1090            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1091            .await
1092            .unwrap();
1093
1094        cx.update(|cx| {
1095            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1096            buffer.update(cx, |buffer, cx| {
1097                buffer
1098                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
1099                    .unwrap()
1100            });
1101            buffer.update(cx, |buffer, cx| {
1102                buffer
1103                    .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
1104                    .unwrap()
1105            });
1106            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1107        });
1108        cx.run_until_parked();
1109        assert_eq!(
1110            buffer.read_with(cx, |buffer, _| buffer.text()),
1111            "abc\ndE\nXYZf\nghi\njkl\nmnO"
1112        );
1113        assert_eq!(
1114            unreviewed_hunks(&action_log, cx),
1115            vec![(
1116                buffer.clone(),
1117                vec![
1118                    HunkStatus {
1119                        range: Point::new(1, 0)..Point::new(3, 0),
1120                        diff_status: DiffHunkStatusKind::Modified,
1121                        old_text: "def\n".into(),
1122                    },
1123                    HunkStatus {
1124                        range: Point::new(5, 0)..Point::new(5, 3),
1125                        diff_status: DiffHunkStatusKind::Modified,
1126                        old_text: "mno".into(),
1127                    }
1128                ],
1129            )]
1130        );
1131
1132        action_log
1133            .update(cx, |log, cx| {
1134                log.reject_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
1135            })
1136            .await
1137            .unwrap();
1138        cx.run_until_parked();
1139        assert_eq!(
1140            buffer.read_with(cx, |buffer, _| buffer.text()),
1141            "abc\ndef\nghi\njkl\nmnO"
1142        );
1143        assert_eq!(
1144            unreviewed_hunks(&action_log, cx),
1145            vec![(
1146                buffer.clone(),
1147                vec![HunkStatus {
1148                    range: Point::new(4, 0)..Point::new(4, 3),
1149                    diff_status: DiffHunkStatusKind::Modified,
1150                    old_text: "mno".into(),
1151                }],
1152            )]
1153        );
1154
1155        action_log
1156            .update(cx, |log, cx| {
1157                log.reject_edits_in_range(buffer.clone(), Point::new(4, 0)..Point::new(4, 0), cx)
1158            })
1159            .await
1160            .unwrap();
1161        cx.run_until_parked();
1162        assert_eq!(
1163            buffer.read_with(cx, |buffer, _| buffer.text()),
1164            "abc\ndef\nghi\njkl\nmno"
1165        );
1166        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1167    }
1168
1169    #[gpui::test(iterations = 10)]
1170    async fn test_reject_deleted_file(cx: &mut TestAppContext) {
1171        init_test(cx);
1172
1173        let fs = FakeFs::new(cx.executor());
1174        fs.insert_tree(path!("/dir"), json!({"file": "content"}))
1175            .await;
1176        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1177        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1178        let file_path = project
1179            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1180            .unwrap();
1181        let buffer = project
1182            .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
1183            .await
1184            .unwrap();
1185
1186        cx.update(|cx| {
1187            action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx));
1188        });
1189        project
1190            .update(cx, |project, cx| {
1191                project.delete_file(file_path.clone(), false, cx)
1192            })
1193            .unwrap()
1194            .await
1195            .unwrap();
1196        cx.run_until_parked();
1197        assert!(!fs.is_file(path!("/dir/file").as_ref()).await);
1198        assert_eq!(
1199            unreviewed_hunks(&action_log, cx),
1200            vec![(
1201                buffer.clone(),
1202                vec![HunkStatus {
1203                    range: Point::new(0, 0)..Point::new(0, 0),
1204                    diff_status: DiffHunkStatusKind::Deleted,
1205                    old_text: "content".into(),
1206                }]
1207            )]
1208        );
1209
1210        action_log
1211            .update(cx, |log, cx| {
1212                log.reject_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(0, 0), cx)
1213            })
1214            .await
1215            .unwrap();
1216        cx.run_until_parked();
1217        assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "content");
1218        assert!(fs.is_file(path!("/dir/file").as_ref()).await);
1219        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1220    }
1221
1222    #[gpui::test(iterations = 10)]
1223    async fn test_reject_created_file(cx: &mut TestAppContext) {
1224        init_test(cx);
1225
1226        let fs = FakeFs::new(cx.executor());
1227        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1228        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1229        let file_path = project
1230            .read_with(cx, |project, cx| {
1231                project.find_project_path("dir/new_file", cx)
1232            })
1233            .unwrap();
1234
1235        let buffer = project
1236            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1237            .await
1238            .unwrap();
1239        cx.update(|cx| {
1240            buffer.update(cx, |buffer, cx| buffer.set_text("content", cx));
1241            action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
1242        });
1243        project
1244            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1245            .await
1246            .unwrap();
1247        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
1248        cx.run_until_parked();
1249        assert_eq!(
1250            unreviewed_hunks(&action_log, cx),
1251            vec![(
1252                buffer.clone(),
1253                vec![HunkStatus {
1254                    range: Point::new(0, 0)..Point::new(0, 7),
1255                    diff_status: DiffHunkStatusKind::Added,
1256                    old_text: "".into(),
1257                }],
1258            )]
1259        );
1260
1261        action_log
1262            .update(cx, |log, cx| {
1263                log.reject_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(0, 11), cx)
1264            })
1265            .await
1266            .unwrap();
1267        cx.run_until_parked();
1268        assert!(!fs.is_file(path!("/dir/new_file").as_ref()).await);
1269        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1270    }
1271
1272    #[gpui::test(iterations = 100)]
1273    async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
1274        init_test(cx);
1275
1276        let operations = env::var("OPERATIONS")
1277            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
1278            .unwrap_or(20);
1279
1280        let text = RandomCharIter::new(&mut rng).take(50).collect::<String>();
1281        let fs = FakeFs::new(cx.executor());
1282        fs.insert_tree(path!("/dir"), json!({"file": text})).await;
1283        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1284        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1285        let file_path = project
1286            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1287            .unwrap();
1288        let buffer = project
1289            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1290            .await
1291            .unwrap();
1292
1293        action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1294
1295        for _ in 0..operations {
1296            match rng.gen_range(0..100) {
1297                0..25 => {
1298                    action_log.update(cx, |log, cx| {
1299                        let range = buffer.read(cx).random_byte_range(0, &mut rng);
1300                        log::info!("keeping edits in range {:?}", range);
1301                        log.keep_edits_in_range(buffer.clone(), range, cx)
1302                    });
1303                }
1304                25..50 => {
1305                    action_log
1306                        .update(cx, |log, cx| {
1307                            let range = buffer.read(cx).random_byte_range(0, &mut rng);
1308                            log::info!("rejecting edits in range {:?}", range);
1309                            log.reject_edits_in_range(buffer.clone(), range, cx)
1310                        })
1311                        .await
1312                        .unwrap();
1313                }
1314                _ => {
1315                    let is_agent_change = rng.gen_bool(0.5);
1316                    if is_agent_change {
1317                        log::info!("agent edit");
1318                    } else {
1319                        log::info!("user edit");
1320                    }
1321                    cx.update(|cx| {
1322                        buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
1323                        if is_agent_change {
1324                            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1325                        }
1326                    });
1327                }
1328            }
1329
1330            if rng.gen_bool(0.2) {
1331                quiesce(&action_log, &buffer, cx);
1332            }
1333        }
1334
1335        quiesce(&action_log, &buffer, cx);
1336
1337        fn quiesce(
1338            action_log: &Entity<ActionLog>,
1339            buffer: &Entity<Buffer>,
1340            cx: &mut TestAppContext,
1341        ) {
1342            log::info!("quiescing...");
1343            cx.run_until_parked();
1344            action_log.update(cx, |log, cx| {
1345                let tracked_buffer = log.track_buffer(buffer.clone(), false, cx);
1346                let mut old_text = tracked_buffer.base_text.clone();
1347                let new_text = buffer.read(cx).as_rope();
1348                for edit in tracked_buffer.unreviewed_changes.edits() {
1349                    let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
1350                    let old_end = old_text.point_to_offset(cmp::min(
1351                        Point::new(edit.new.start + edit.old_len(), 0),
1352                        old_text.max_point(),
1353                    ));
1354                    old_text.replace(
1355                        old_start..old_end,
1356                        &new_text.slice_rows(edit.new.clone()).to_string(),
1357                    );
1358                }
1359                pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
1360            })
1361        }
1362    }
1363
1364    #[derive(Debug, Clone, PartialEq, Eq)]
1365    struct HunkStatus {
1366        range: Range<Point>,
1367        diff_status: DiffHunkStatusKind,
1368        old_text: String,
1369    }
1370
1371    fn unreviewed_hunks(
1372        action_log: &Entity<ActionLog>,
1373        cx: &TestAppContext,
1374    ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
1375        cx.read(|cx| {
1376            action_log
1377                .read(cx)
1378                .changed_buffers(cx)
1379                .into_iter()
1380                .map(|(buffer, diff)| {
1381                    let snapshot = buffer.read(cx).snapshot();
1382                    (
1383                        buffer,
1384                        diff.read(cx)
1385                            .hunks(&snapshot, cx)
1386                            .map(|hunk| HunkStatus {
1387                                diff_status: hunk.status().kind,
1388                                range: hunk.range,
1389                                old_text: diff
1390                                    .read(cx)
1391                                    .base_text()
1392                                    .text_for_range(hunk.diff_base_byte_range)
1393                                    .collect(),
1394                            })
1395                            .collect(),
1396                    )
1397                })
1398                .collect()
1399        })
1400    }
1401}