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, ToPoint};
   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_ranges(
 367        &mut self,
 368        buffer: Entity<Buffer>,
 369        buffer_ranges: Vec<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 mut buffer_row_ranges = buffer_ranges
 407                        .into_iter()
 408                        .map(|range| {
 409                            range.start.to_point(buffer).row..range.end.to_point(buffer).row
 410                        })
 411                        .peekable();
 412
 413                    let mut edits_to_revert = Vec::new();
 414                    for edit in tracked_buffer.unreviewed_changes.edits() {
 415                        let new_range = tracked_buffer
 416                            .snapshot
 417                            .anchor_before(Point::new(edit.new.start, 0))
 418                            ..tracked_buffer.snapshot.anchor_after(cmp::min(
 419                                Point::new(edit.new.end, 0),
 420                                tracked_buffer.snapshot.max_point(),
 421                            ));
 422                        let new_row_range = new_range.start.to_point(buffer).row
 423                            ..new_range.end.to_point(buffer).row;
 424
 425                        let mut revert = false;
 426                        while let Some(buffer_row_range) = buffer_row_ranges.peek() {
 427                            if buffer_row_range.end < new_row_range.start {
 428                                buffer_row_ranges.next();
 429                            } else if buffer_row_range.start > new_row_range.end {
 430                                break;
 431                            } else {
 432                                revert = true;
 433                                break;
 434                            }
 435                        }
 436
 437                        if revert {
 438                            let old_range = tracked_buffer
 439                                .base_text
 440                                .point_to_offset(Point::new(edit.old.start, 0))
 441                                ..tracked_buffer.base_text.point_to_offset(cmp::min(
 442                                    Point::new(edit.old.end, 0),
 443                                    tracked_buffer.base_text.max_point(),
 444                                ));
 445                            let old_text = tracked_buffer
 446                                .base_text
 447                                .chunks_in_range(old_range)
 448                                .collect::<String>();
 449                            edits_to_revert.push((new_range, old_text));
 450                        }
 451                    }
 452
 453                    buffer.edit(edits_to_revert, None, cx);
 454                });
 455                self.project
 456                    .update(cx, |project, cx| project.save_buffer(buffer, cx))
 457            }
 458        }
 459    }
 460
 461    pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) {
 462        self.tracked_buffers
 463            .retain(|_buffer, tracked_buffer| match tracked_buffer.status {
 464                TrackedBufferStatus::Deleted => false,
 465                _ => {
 466                    tracked_buffer.unreviewed_changes.clear();
 467                    tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone();
 468                    tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
 469                    true
 470                }
 471            });
 472        cx.notify();
 473    }
 474
 475    /// Returns the set of buffers that contain changes that haven't been reviewed by the user.
 476    pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
 477        self.tracked_buffers
 478            .iter()
 479            .filter(|(_, tracked)| tracked.has_changes(cx))
 480            .map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone()))
 481            .collect()
 482    }
 483
 484    /// Iterate over buffers changed since last read or edited by the model
 485    pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
 486        self.tracked_buffers
 487            .iter()
 488            .filter(|(buffer, tracked)| {
 489                let buffer = buffer.read(cx);
 490
 491                tracked.version != buffer.version
 492                    && buffer
 493                        .file()
 494                        .map_or(false, |file| file.disk_state() != DiskState::Deleted)
 495            })
 496            .map(|(buffer, _)| buffer)
 497    }
 498}
 499
 500fn apply_non_conflicting_edits(
 501    patch: &Patch<u32>,
 502    edits: Vec<Edit<u32>>,
 503    old_text: &mut Rope,
 504    new_text: &Rope,
 505) {
 506    let mut old_edits = patch.edits().iter().cloned().peekable();
 507    let mut new_edits = edits.into_iter().peekable();
 508    let mut applied_delta = 0i32;
 509    let mut rebased_delta = 0i32;
 510
 511    while let Some(mut new_edit) = new_edits.next() {
 512        let mut conflict = false;
 513
 514        // Push all the old edits that are before this new edit or that intersect with it.
 515        while let Some(old_edit) = old_edits.peek() {
 516            if new_edit.old.end < old_edit.new.start
 517                || (!old_edit.new.is_empty() && new_edit.old.end == old_edit.new.start)
 518            {
 519                break;
 520            } else if new_edit.old.start > old_edit.new.end
 521                || (!old_edit.new.is_empty() && new_edit.old.start == old_edit.new.end)
 522            {
 523                let old_edit = old_edits.next().unwrap();
 524                rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
 525            } else {
 526                conflict = true;
 527                if new_edits
 528                    .peek()
 529                    .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new))
 530                {
 531                    new_edit = new_edits.next().unwrap();
 532                } else {
 533                    let old_edit = old_edits.next().unwrap();
 534                    rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
 535                }
 536            }
 537        }
 538
 539        if !conflict {
 540            // This edit doesn't intersect with any old edit, so we can apply it to the old text.
 541            new_edit.old.start = (new_edit.old.start as i32 + applied_delta - rebased_delta) as u32;
 542            new_edit.old.end = (new_edit.old.end as i32 + applied_delta - rebased_delta) as u32;
 543            let old_bytes = old_text.point_to_offset(Point::new(new_edit.old.start, 0))
 544                ..old_text.point_to_offset(cmp::min(
 545                    Point::new(new_edit.old.end, 0),
 546                    old_text.max_point(),
 547                ));
 548            let new_bytes = new_text.point_to_offset(Point::new(new_edit.new.start, 0))
 549                ..new_text.point_to_offset(cmp::min(
 550                    Point::new(new_edit.new.end, 0),
 551                    new_text.max_point(),
 552                ));
 553
 554            old_text.replace(
 555                old_bytes,
 556                &new_text.chunks_in_range(new_bytes).collect::<String>(),
 557            );
 558            applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32;
 559        }
 560    }
 561}
 562
 563fn diff_snapshots(
 564    old_snapshot: &text::BufferSnapshot,
 565    new_snapshot: &text::BufferSnapshot,
 566) -> Vec<Edit<u32>> {
 567    let mut edits = new_snapshot
 568        .edits_since::<Point>(&old_snapshot.version)
 569        .map(|edit| point_to_row_edit(edit, old_snapshot.as_rope(), new_snapshot.as_rope()))
 570        .peekable();
 571    let mut row_edits = Vec::new();
 572    while let Some(mut edit) = edits.next() {
 573        while let Some(next_edit) = edits.peek() {
 574            if edit.old.end >= next_edit.old.start {
 575                edit.old.end = next_edit.old.end;
 576                edit.new.end = next_edit.new.end;
 577                edits.next();
 578            } else {
 579                break;
 580            }
 581        }
 582        row_edits.push(edit);
 583    }
 584    row_edits
 585}
 586
 587fn point_to_row_edit(edit: Edit<Point>, old_text: &Rope, new_text: &Rope) -> Edit<u32> {
 588    if edit.old.start.column == old_text.line_len(edit.old.start.row)
 589        && new_text
 590            .chars_at(new_text.point_to_offset(edit.new.start))
 591            .next()
 592            == Some('\n')
 593        && edit.old.start != old_text.max_point()
 594    {
 595        Edit {
 596            old: edit.old.start.row + 1..edit.old.end.row + 1,
 597            new: edit.new.start.row + 1..edit.new.end.row + 1,
 598        }
 599    } else if edit.old.start.column == 0
 600        && edit.old.end.column == 0
 601        && edit.new.end.column == 0
 602        && edit.old.end != old_text.max_point()
 603    {
 604        Edit {
 605            old: edit.old.start.row..edit.old.end.row,
 606            new: edit.new.start.row..edit.new.end.row,
 607        }
 608    } else {
 609        Edit {
 610            old: edit.old.start.row..edit.old.end.row + 1,
 611            new: edit.new.start.row..edit.new.end.row + 1,
 612        }
 613    }
 614}
 615
 616#[derive(Copy, Clone, Debug)]
 617enum ChangeAuthor {
 618    User,
 619    Agent,
 620}
 621
 622#[derive(Copy, Clone, Eq, PartialEq)]
 623enum TrackedBufferStatus {
 624    Created,
 625    Modified,
 626    Deleted,
 627}
 628
 629struct TrackedBuffer {
 630    buffer: Entity<Buffer>,
 631    base_text: Rope,
 632    unreviewed_changes: Patch<u32>,
 633    status: TrackedBufferStatus,
 634    version: clock::Global,
 635    diff: Entity<BufferDiff>,
 636    snapshot: text::BufferSnapshot,
 637    diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
 638    _open_lsp_handle: OpenLspBufferHandle,
 639    _maintain_diff: Task<()>,
 640    _subscription: Subscription,
 641}
 642
 643impl TrackedBuffer {
 644    fn has_changes(&self, cx: &App) -> bool {
 645        self.diff
 646            .read(cx)
 647            .hunks(&self.buffer.read(cx), cx)
 648            .next()
 649            .is_some()
 650    }
 651
 652    fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) {
 653        self.diff_update
 654            .unbounded_send((author, self.buffer.read(cx).text_snapshot()))
 655            .ok();
 656    }
 657}
 658
 659pub struct ChangedBuffer {
 660    pub diff: Entity<BufferDiff>,
 661}
 662
 663#[cfg(test)]
 664mod tests {
 665    use std::env;
 666
 667    use super::*;
 668    use buffer_diff::DiffHunkStatusKind;
 669    use gpui::TestAppContext;
 670    use language::Point;
 671    use project::{FakeFs, Fs, Project, RemoveOptions};
 672    use rand::prelude::*;
 673    use serde_json::json;
 674    use settings::SettingsStore;
 675    use util::{RandomCharIter, path};
 676
 677    #[ctor::ctor]
 678    fn init_logger() {
 679        if std::env::var("RUST_LOG").is_ok() {
 680            env_logger::init();
 681        }
 682    }
 683
 684    fn init_test(cx: &mut TestAppContext) {
 685        cx.update(|cx| {
 686            let settings_store = SettingsStore::test(cx);
 687            cx.set_global(settings_store);
 688            language::init(cx);
 689            Project::init_settings(cx);
 690        });
 691    }
 692
 693    #[gpui::test(iterations = 10)]
 694    async fn test_keep_edits(cx: &mut TestAppContext) {
 695        init_test(cx);
 696
 697        let fs = FakeFs::new(cx.executor());
 698        let project = Project::test(fs.clone(), [], cx).await;
 699        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 700        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
 701
 702        cx.update(|cx| {
 703            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 704            buffer.update(cx, |buffer, cx| {
 705                buffer
 706                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
 707                    .unwrap()
 708            });
 709            buffer.update(cx, |buffer, cx| {
 710                buffer
 711                    .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
 712                    .unwrap()
 713            });
 714            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 715        });
 716        cx.run_until_parked();
 717        assert_eq!(
 718            buffer.read_with(cx, |buffer, _| buffer.text()),
 719            "abc\ndEf\nghi\njkl\nmnO"
 720        );
 721        assert_eq!(
 722            unreviewed_hunks(&action_log, cx),
 723            vec![(
 724                buffer.clone(),
 725                vec![
 726                    HunkStatus {
 727                        range: Point::new(1, 0)..Point::new(2, 0),
 728                        diff_status: DiffHunkStatusKind::Modified,
 729                        old_text: "def\n".into(),
 730                    },
 731                    HunkStatus {
 732                        range: Point::new(4, 0)..Point::new(4, 3),
 733                        diff_status: DiffHunkStatusKind::Modified,
 734                        old_text: "mno".into(),
 735                    }
 736                ],
 737            )]
 738        );
 739
 740        action_log.update(cx, |log, cx| {
 741            log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
 742        });
 743        cx.run_until_parked();
 744        assert_eq!(
 745            unreviewed_hunks(&action_log, cx),
 746            vec![(
 747                buffer.clone(),
 748                vec![HunkStatus {
 749                    range: Point::new(1, 0)..Point::new(2, 0),
 750                    diff_status: DiffHunkStatusKind::Modified,
 751                    old_text: "def\n".into(),
 752                }],
 753            )]
 754        );
 755
 756        action_log.update(cx, |log, cx| {
 757            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
 758        });
 759        cx.run_until_parked();
 760        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 761    }
 762
 763    #[gpui::test(iterations = 10)]
 764    async fn test_deletions(cx: &mut TestAppContext) {
 765        init_test(cx);
 766
 767        let fs = FakeFs::new(cx.executor());
 768        let project = Project::test(fs.clone(), [], cx).await;
 769        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 770        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno\npqr", cx));
 771
 772        cx.update(|cx| {
 773            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 774            buffer.update(cx, |buffer, cx| {
 775                buffer
 776                    .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx)
 777                    .unwrap();
 778                buffer.finalize_last_transaction();
 779            });
 780            buffer.update(cx, |buffer, cx| {
 781                buffer
 782                    .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx)
 783                    .unwrap();
 784                buffer.finalize_last_transaction();
 785            });
 786            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 787        });
 788        cx.run_until_parked();
 789        assert_eq!(
 790            buffer.read_with(cx, |buffer, _| buffer.text()),
 791            "abc\nghi\njkl\npqr"
 792        );
 793        assert_eq!(
 794            unreviewed_hunks(&action_log, cx),
 795            vec![(
 796                buffer.clone(),
 797                vec![
 798                    HunkStatus {
 799                        range: Point::new(1, 0)..Point::new(1, 0),
 800                        diff_status: DiffHunkStatusKind::Deleted,
 801                        old_text: "def\n".into(),
 802                    },
 803                    HunkStatus {
 804                        range: Point::new(3, 0)..Point::new(3, 0),
 805                        diff_status: DiffHunkStatusKind::Deleted,
 806                        old_text: "mno\n".into(),
 807                    }
 808                ],
 809            )]
 810        );
 811
 812        buffer.update(cx, |buffer, cx| buffer.undo(cx));
 813        cx.run_until_parked();
 814        assert_eq!(
 815            buffer.read_with(cx, |buffer, _| buffer.text()),
 816            "abc\nghi\njkl\nmno\npqr"
 817        );
 818        assert_eq!(
 819            unreviewed_hunks(&action_log, cx),
 820            vec![(
 821                buffer.clone(),
 822                vec![HunkStatus {
 823                    range: Point::new(1, 0)..Point::new(1, 0),
 824                    diff_status: DiffHunkStatusKind::Deleted,
 825                    old_text: "def\n".into(),
 826                }],
 827            )]
 828        );
 829
 830        action_log.update(cx, |log, cx| {
 831            log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx)
 832        });
 833        cx.run_until_parked();
 834        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 835    }
 836
 837    #[gpui::test(iterations = 10)]
 838    async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
 839        init_test(cx);
 840
 841        let fs = FakeFs::new(cx.executor());
 842        let project = Project::test(fs.clone(), [], cx).await;
 843        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 844        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
 845
 846        cx.update(|cx| {
 847            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 848            buffer.update(cx, |buffer, cx| {
 849                buffer
 850                    .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
 851                    .unwrap()
 852            });
 853            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 854        });
 855        cx.run_until_parked();
 856        assert_eq!(
 857            buffer.read_with(cx, |buffer, _| buffer.text()),
 858            "abc\ndeF\nGHI\njkl\nmno"
 859        );
 860        assert_eq!(
 861            unreviewed_hunks(&action_log, cx),
 862            vec![(
 863                buffer.clone(),
 864                vec![HunkStatus {
 865                    range: Point::new(1, 0)..Point::new(3, 0),
 866                    diff_status: DiffHunkStatusKind::Modified,
 867                    old_text: "def\nghi\n".into(),
 868                }],
 869            )]
 870        );
 871
 872        buffer.update(cx, |buffer, cx| {
 873            buffer.edit(
 874                [
 875                    (Point::new(0, 2)..Point::new(0, 2), "X"),
 876                    (Point::new(3, 0)..Point::new(3, 0), "Y"),
 877                ],
 878                None,
 879                cx,
 880            )
 881        });
 882        cx.run_until_parked();
 883        assert_eq!(
 884            buffer.read_with(cx, |buffer, _| buffer.text()),
 885            "abXc\ndeF\nGHI\nYjkl\nmno"
 886        );
 887        assert_eq!(
 888            unreviewed_hunks(&action_log, cx),
 889            vec![(
 890                buffer.clone(),
 891                vec![HunkStatus {
 892                    range: Point::new(1, 0)..Point::new(3, 0),
 893                    diff_status: DiffHunkStatusKind::Modified,
 894                    old_text: "def\nghi\n".into(),
 895                }],
 896            )]
 897        );
 898
 899        buffer.update(cx, |buffer, cx| {
 900            buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx)
 901        });
 902        cx.run_until_parked();
 903        assert_eq!(
 904            buffer.read_with(cx, |buffer, _| buffer.text()),
 905            "abXc\ndZeF\nGHI\nYjkl\nmno"
 906        );
 907        assert_eq!(
 908            unreviewed_hunks(&action_log, cx),
 909            vec![(
 910                buffer.clone(),
 911                vec![HunkStatus {
 912                    range: Point::new(1, 0)..Point::new(3, 0),
 913                    diff_status: DiffHunkStatusKind::Modified,
 914                    old_text: "def\nghi\n".into(),
 915                }],
 916            )]
 917        );
 918
 919        action_log.update(cx, |log, cx| {
 920            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
 921        });
 922        cx.run_until_parked();
 923        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 924    }
 925
 926    #[gpui::test(iterations = 10)]
 927    async fn test_creating_files(cx: &mut TestAppContext) {
 928        init_test(cx);
 929
 930        let fs = FakeFs::new(cx.executor());
 931        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 932        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 933
 934        let fs = FakeFs::new(cx.executor());
 935        fs.insert_tree(path!("/dir"), json!({})).await;
 936
 937        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 938        let file_path = project
 939            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
 940            .unwrap();
 941
 942        // Simulate file2 being recreated by a tool.
 943        let buffer = project
 944            .update(cx, |project, cx| project.open_buffer(file_path, cx))
 945            .await
 946            .unwrap();
 947        cx.update(|cx| {
 948            buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
 949            action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
 950        });
 951        project
 952            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 953            .await
 954            .unwrap();
 955        cx.run_until_parked();
 956        assert_eq!(
 957            unreviewed_hunks(&action_log, cx),
 958            vec![(
 959                buffer.clone(),
 960                vec![HunkStatus {
 961                    range: Point::new(0, 0)..Point::new(0, 5),
 962                    diff_status: DiffHunkStatusKind::Added,
 963                    old_text: "".into(),
 964                }],
 965            )]
 966        );
 967
 968        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx));
 969        cx.run_until_parked();
 970        assert_eq!(
 971            unreviewed_hunks(&action_log, cx),
 972            vec![(
 973                buffer.clone(),
 974                vec![HunkStatus {
 975                    range: Point::new(0, 0)..Point::new(0, 6),
 976                    diff_status: DiffHunkStatusKind::Added,
 977                    old_text: "".into(),
 978                }],
 979            )]
 980        );
 981
 982        action_log.update(cx, |log, cx| {
 983            log.keep_edits_in_range(buffer.clone(), 0..5, cx)
 984        });
 985        cx.run_until_parked();
 986        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 987    }
 988
 989    #[gpui::test(iterations = 10)]
 990    async fn test_deleting_files(cx: &mut TestAppContext) {
 991        init_test(cx);
 992
 993        let fs = FakeFs::new(cx.executor());
 994        fs.insert_tree(
 995            path!("/dir"),
 996            json!({"file1": "lorem\n", "file2": "ipsum\n"}),
 997        )
 998        .await;
 999
1000        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1001        let file1_path = project
1002            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
1003            .unwrap();
1004        let file2_path = project
1005            .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
1006            .unwrap();
1007
1008        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1009        let buffer1 = project
1010            .update(cx, |project, cx| {
1011                project.open_buffer(file1_path.clone(), cx)
1012            })
1013            .await
1014            .unwrap();
1015        let buffer2 = project
1016            .update(cx, |project, cx| {
1017                project.open_buffer(file2_path.clone(), cx)
1018            })
1019            .await
1020            .unwrap();
1021
1022        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
1023        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
1024        project
1025            .update(cx, |project, cx| {
1026                project.delete_file(file1_path.clone(), false, cx)
1027            })
1028            .unwrap()
1029            .await
1030            .unwrap();
1031        project
1032            .update(cx, |project, cx| {
1033                project.delete_file(file2_path.clone(), false, cx)
1034            })
1035            .unwrap()
1036            .await
1037            .unwrap();
1038        cx.run_until_parked();
1039        assert_eq!(
1040            unreviewed_hunks(&action_log, cx),
1041            vec![
1042                (
1043                    buffer1.clone(),
1044                    vec![HunkStatus {
1045                        range: Point::new(0, 0)..Point::new(0, 0),
1046                        diff_status: DiffHunkStatusKind::Deleted,
1047                        old_text: "lorem\n".into(),
1048                    }]
1049                ),
1050                (
1051                    buffer2.clone(),
1052                    vec![HunkStatus {
1053                        range: Point::new(0, 0)..Point::new(0, 0),
1054                        diff_status: DiffHunkStatusKind::Deleted,
1055                        old_text: "ipsum\n".into(),
1056                    }],
1057                )
1058            ]
1059        );
1060
1061        // Simulate file1 being recreated externally.
1062        fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
1063            .await;
1064
1065        // Simulate file2 being recreated by a tool.
1066        let buffer2 = project
1067            .update(cx, |project, cx| project.open_buffer(file2_path, cx))
1068            .await
1069            .unwrap();
1070        buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
1071        action_log.update(cx, |log, cx| log.will_create_buffer(buffer2.clone(), cx));
1072        project
1073            .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
1074            .await
1075            .unwrap();
1076
1077        cx.run_until_parked();
1078        assert_eq!(
1079            unreviewed_hunks(&action_log, cx),
1080            vec![(
1081                buffer2.clone(),
1082                vec![HunkStatus {
1083                    range: Point::new(0, 0)..Point::new(0, 5),
1084                    diff_status: DiffHunkStatusKind::Modified,
1085                    old_text: "ipsum\n".into(),
1086                }],
1087            )]
1088        );
1089
1090        // Simulate file2 being deleted externally.
1091        fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
1092            .await
1093            .unwrap();
1094        cx.run_until_parked();
1095        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1096    }
1097
1098    #[gpui::test(iterations = 10)]
1099    async fn test_reject_edits(cx: &mut TestAppContext) {
1100        init_test(cx);
1101
1102        let fs = FakeFs::new(cx.executor());
1103        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
1104            .await;
1105        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1106        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1107        let file_path = project
1108            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1109            .unwrap();
1110        let buffer = project
1111            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1112            .await
1113            .unwrap();
1114
1115        cx.update(|cx| {
1116            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1117            buffer.update(cx, |buffer, cx| {
1118                buffer
1119                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
1120                    .unwrap()
1121            });
1122            buffer.update(cx, |buffer, cx| {
1123                buffer
1124                    .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
1125                    .unwrap()
1126            });
1127            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1128        });
1129        cx.run_until_parked();
1130        assert_eq!(
1131            buffer.read_with(cx, |buffer, _| buffer.text()),
1132            "abc\ndE\nXYZf\nghi\njkl\nmnO"
1133        );
1134        assert_eq!(
1135            unreviewed_hunks(&action_log, cx),
1136            vec![(
1137                buffer.clone(),
1138                vec![
1139                    HunkStatus {
1140                        range: Point::new(1, 0)..Point::new(3, 0),
1141                        diff_status: DiffHunkStatusKind::Modified,
1142                        old_text: "def\n".into(),
1143                    },
1144                    HunkStatus {
1145                        range: Point::new(5, 0)..Point::new(5, 3),
1146                        diff_status: DiffHunkStatusKind::Modified,
1147                        old_text: "mno".into(),
1148                    }
1149                ],
1150            )]
1151        );
1152
1153        // If the rejected range doesn't overlap with any hunk, we ignore it.
1154        action_log
1155            .update(cx, |log, cx| {
1156                log.reject_edits_in_ranges(
1157                    buffer.clone(),
1158                    vec![Point::new(4, 0)..Point::new(4, 0)],
1159                    cx,
1160                )
1161            })
1162            .await
1163            .unwrap();
1164        cx.run_until_parked();
1165        assert_eq!(
1166            buffer.read_with(cx, |buffer, _| buffer.text()),
1167            "abc\ndE\nXYZf\nghi\njkl\nmnO"
1168        );
1169        assert_eq!(
1170            unreviewed_hunks(&action_log, cx),
1171            vec![(
1172                buffer.clone(),
1173                vec![
1174                    HunkStatus {
1175                        range: Point::new(1, 0)..Point::new(3, 0),
1176                        diff_status: DiffHunkStatusKind::Modified,
1177                        old_text: "def\n".into(),
1178                    },
1179                    HunkStatus {
1180                        range: Point::new(5, 0)..Point::new(5, 3),
1181                        diff_status: DiffHunkStatusKind::Modified,
1182                        old_text: "mno".into(),
1183                    }
1184                ],
1185            )]
1186        );
1187
1188        action_log
1189            .update(cx, |log, cx| {
1190                log.reject_edits_in_ranges(
1191                    buffer.clone(),
1192                    vec![Point::new(0, 0)..Point::new(1, 0)],
1193                    cx,
1194                )
1195            })
1196            .await
1197            .unwrap();
1198        cx.run_until_parked();
1199        assert_eq!(
1200            buffer.read_with(cx, |buffer, _| buffer.text()),
1201            "abc\ndef\nghi\njkl\nmnO"
1202        );
1203        assert_eq!(
1204            unreviewed_hunks(&action_log, cx),
1205            vec![(
1206                buffer.clone(),
1207                vec![HunkStatus {
1208                    range: Point::new(4, 0)..Point::new(4, 3),
1209                    diff_status: DiffHunkStatusKind::Modified,
1210                    old_text: "mno".into(),
1211                }],
1212            )]
1213        );
1214
1215        action_log
1216            .update(cx, |log, cx| {
1217                log.reject_edits_in_ranges(
1218                    buffer.clone(),
1219                    vec![Point::new(4, 0)..Point::new(4, 0)],
1220                    cx,
1221                )
1222            })
1223            .await
1224            .unwrap();
1225        cx.run_until_parked();
1226        assert_eq!(
1227            buffer.read_with(cx, |buffer, _| buffer.text()),
1228            "abc\ndef\nghi\njkl\nmno"
1229        );
1230        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1231    }
1232
1233    #[gpui::test(iterations = 10)]
1234    async fn test_reject_multiple_edits(cx: &mut TestAppContext) {
1235        init_test(cx);
1236
1237        let fs = FakeFs::new(cx.executor());
1238        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
1239            .await;
1240        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1241        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1242        let file_path = project
1243            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1244            .unwrap();
1245        let buffer = project
1246            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1247            .await
1248            .unwrap();
1249
1250        cx.update(|cx| {
1251            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1252            buffer.update(cx, |buffer, cx| {
1253                buffer
1254                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
1255                    .unwrap()
1256            });
1257            buffer.update(cx, |buffer, cx| {
1258                buffer
1259                    .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
1260                    .unwrap()
1261            });
1262            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1263        });
1264        cx.run_until_parked();
1265        assert_eq!(
1266            buffer.read_with(cx, |buffer, _| buffer.text()),
1267            "abc\ndE\nXYZf\nghi\njkl\nmnO"
1268        );
1269        assert_eq!(
1270            unreviewed_hunks(&action_log, cx),
1271            vec![(
1272                buffer.clone(),
1273                vec![
1274                    HunkStatus {
1275                        range: Point::new(1, 0)..Point::new(3, 0),
1276                        diff_status: DiffHunkStatusKind::Modified,
1277                        old_text: "def\n".into(),
1278                    },
1279                    HunkStatus {
1280                        range: Point::new(5, 0)..Point::new(5, 3),
1281                        diff_status: DiffHunkStatusKind::Modified,
1282                        old_text: "mno".into(),
1283                    }
1284                ],
1285            )]
1286        );
1287
1288        action_log.update(cx, |log, cx| {
1289            let range_1 = buffer.read(cx).anchor_before(Point::new(0, 0))
1290                ..buffer.read(cx).anchor_before(Point::new(1, 0));
1291            let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0))
1292                ..buffer.read(cx).anchor_before(Point::new(5, 3));
1293
1294            log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], cx)
1295                .detach();
1296            assert_eq!(
1297                buffer.read_with(cx, |buffer, _| buffer.text()),
1298                "abc\ndef\nghi\njkl\nmno"
1299            );
1300        });
1301        cx.run_until_parked();
1302        assert_eq!(
1303            buffer.read_with(cx, |buffer, _| buffer.text()),
1304            "abc\ndef\nghi\njkl\nmno"
1305        );
1306        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1307    }
1308
1309    #[gpui::test(iterations = 10)]
1310    async fn test_reject_deleted_file(cx: &mut TestAppContext) {
1311        init_test(cx);
1312
1313        let fs = FakeFs::new(cx.executor());
1314        fs.insert_tree(path!("/dir"), json!({"file": "content"}))
1315            .await;
1316        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1317        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1318        let file_path = project
1319            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1320            .unwrap();
1321        let buffer = project
1322            .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
1323            .await
1324            .unwrap();
1325
1326        cx.update(|cx| {
1327            action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx));
1328        });
1329        project
1330            .update(cx, |project, cx| {
1331                project.delete_file(file_path.clone(), false, cx)
1332            })
1333            .unwrap()
1334            .await
1335            .unwrap();
1336        cx.run_until_parked();
1337        assert!(!fs.is_file(path!("/dir/file").as_ref()).await);
1338        assert_eq!(
1339            unreviewed_hunks(&action_log, cx),
1340            vec![(
1341                buffer.clone(),
1342                vec![HunkStatus {
1343                    range: Point::new(0, 0)..Point::new(0, 0),
1344                    diff_status: DiffHunkStatusKind::Deleted,
1345                    old_text: "content".into(),
1346                }]
1347            )]
1348        );
1349
1350        action_log
1351            .update(cx, |log, cx| {
1352                log.reject_edits_in_ranges(
1353                    buffer.clone(),
1354                    vec![Point::new(0, 0)..Point::new(0, 0)],
1355                    cx,
1356                )
1357            })
1358            .await
1359            .unwrap();
1360        cx.run_until_parked();
1361        assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "content");
1362        assert!(fs.is_file(path!("/dir/file").as_ref()).await);
1363        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1364    }
1365
1366    #[gpui::test(iterations = 10)]
1367    async fn test_reject_created_file(cx: &mut TestAppContext) {
1368        init_test(cx);
1369
1370        let fs = FakeFs::new(cx.executor());
1371        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1372        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1373        let file_path = project
1374            .read_with(cx, |project, cx| {
1375                project.find_project_path("dir/new_file", cx)
1376            })
1377            .unwrap();
1378
1379        let buffer = project
1380            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1381            .await
1382            .unwrap();
1383        cx.update(|cx| {
1384            buffer.update(cx, |buffer, cx| buffer.set_text("content", cx));
1385            action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
1386        });
1387        project
1388            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1389            .await
1390            .unwrap();
1391        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
1392        cx.run_until_parked();
1393        assert_eq!(
1394            unreviewed_hunks(&action_log, cx),
1395            vec![(
1396                buffer.clone(),
1397                vec![HunkStatus {
1398                    range: Point::new(0, 0)..Point::new(0, 7),
1399                    diff_status: DiffHunkStatusKind::Added,
1400                    old_text: "".into(),
1401                }],
1402            )]
1403        );
1404
1405        action_log
1406            .update(cx, |log, cx| {
1407                log.reject_edits_in_ranges(
1408                    buffer.clone(),
1409                    vec![Point::new(0, 0)..Point::new(0, 11)],
1410                    cx,
1411                )
1412            })
1413            .await
1414            .unwrap();
1415        cx.run_until_parked();
1416        assert!(!fs.is_file(path!("/dir/new_file").as_ref()).await);
1417        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1418    }
1419
1420    #[gpui::test(iterations = 100)]
1421    async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
1422        init_test(cx);
1423
1424        let operations = env::var("OPERATIONS")
1425            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
1426            .unwrap_or(20);
1427
1428        let text = RandomCharIter::new(&mut rng).take(50).collect::<String>();
1429        let fs = FakeFs::new(cx.executor());
1430        fs.insert_tree(path!("/dir"), json!({"file": text})).await;
1431        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1432        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1433        let file_path = project
1434            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1435            .unwrap();
1436        let buffer = project
1437            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1438            .await
1439            .unwrap();
1440
1441        action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1442
1443        for _ in 0..operations {
1444            match rng.gen_range(0..100) {
1445                0..25 => {
1446                    action_log.update(cx, |log, cx| {
1447                        let range = buffer.read(cx).random_byte_range(0, &mut rng);
1448                        log::info!("keeping edits in range {:?}", range);
1449                        log.keep_edits_in_range(buffer.clone(), range, cx)
1450                    });
1451                }
1452                25..50 => {
1453                    action_log
1454                        .update(cx, |log, cx| {
1455                            let range = buffer.read(cx).random_byte_range(0, &mut rng);
1456                            log::info!("rejecting edits in range {:?}", range);
1457                            log.reject_edits_in_ranges(buffer.clone(), vec![range], cx)
1458                        })
1459                        .await
1460                        .unwrap();
1461                }
1462                _ => {
1463                    let is_agent_change = rng.gen_bool(0.5);
1464                    if is_agent_change {
1465                        log::info!("agent edit");
1466                    } else {
1467                        log::info!("user edit");
1468                    }
1469                    cx.update(|cx| {
1470                        buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
1471                        if is_agent_change {
1472                            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1473                        }
1474                    });
1475                }
1476            }
1477
1478            if rng.gen_bool(0.2) {
1479                quiesce(&action_log, &buffer, cx);
1480            }
1481        }
1482
1483        quiesce(&action_log, &buffer, cx);
1484
1485        fn quiesce(
1486            action_log: &Entity<ActionLog>,
1487            buffer: &Entity<Buffer>,
1488            cx: &mut TestAppContext,
1489        ) {
1490            log::info!("quiescing...");
1491            cx.run_until_parked();
1492            action_log.update(cx, |log, cx| {
1493                let tracked_buffer = log.track_buffer(buffer.clone(), false, cx);
1494                let mut old_text = tracked_buffer.base_text.clone();
1495                let new_text = buffer.read(cx).as_rope();
1496                for edit in tracked_buffer.unreviewed_changes.edits() {
1497                    let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
1498                    let old_end = old_text.point_to_offset(cmp::min(
1499                        Point::new(edit.new.start + edit.old_len(), 0),
1500                        old_text.max_point(),
1501                    ));
1502                    old_text.replace(
1503                        old_start..old_end,
1504                        &new_text.slice_rows(edit.new.clone()).to_string(),
1505                    );
1506                }
1507                pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
1508            })
1509        }
1510    }
1511
1512    #[derive(Debug, Clone, PartialEq, Eq)]
1513    struct HunkStatus {
1514        range: Range<Point>,
1515        diff_status: DiffHunkStatusKind,
1516        old_text: String,
1517    }
1518
1519    fn unreviewed_hunks(
1520        action_log: &Entity<ActionLog>,
1521        cx: &TestAppContext,
1522    ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
1523        cx.read(|cx| {
1524            action_log
1525                .read(cx)
1526                .changed_buffers(cx)
1527                .into_iter()
1528                .map(|(buffer, diff)| {
1529                    let snapshot = buffer.read(cx).snapshot();
1530                    (
1531                        buffer,
1532                        diff.read(cx)
1533                            .hunks(&snapshot, cx)
1534                            .map(|hunk| HunkStatus {
1535                                diff_status: hunk.status().kind,
1536                                range: hunk.range,
1537                                old_text: diff
1538                                    .read(cx)
1539                                    .base_text()
1540                                    .text_for_range(hunk.diff_base_byte_range)
1541                                    .collect(),
1542                            })
1543                            .collect(),
1544                    )
1545                })
1546                .collect()
1547        })
1548    }
1549}