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