before.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    pub fn project(&self) -> &Entity<Project> {
  33        &self.project
  34    }
  35
  36    /// Notifies a diagnostics check
  37    pub fn checked_project_diagnostics(&mut self) {
  38        self.edited_since_project_diagnostics_check = false;
  39    }
  40
  41    /// Returns true if any files have been edited since the last project diagnostics check
  42    pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
  43        self.edited_since_project_diagnostics_check
  44    }
  45
  46    fn track_buffer_internal(
  47        &mut self,
  48        buffer: Entity<Buffer>,
  49        is_created: bool,
  50        cx: &mut Context<Self>,
  51    ) -> &mut TrackedBuffer {
  52        let tracked_buffer = self
  53            .tracked_buffers
  54            .entry(buffer.clone())
  55            .or_insert_with(|| {
  56                let open_lsp_handle = self.project.update(cx, |project, cx| {
  57                    project.register_buffer_with_language_servers(&buffer, cx)
  58                });
  59
  60                let text_snapshot = buffer.read(cx).text_snapshot();
  61                let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
  62                let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
  63                let base_text;
  64                let status;
  65                let unreviewed_changes;
  66                if is_created {
  67                    base_text = Rope::default();
  68                    status = TrackedBufferStatus::Created;
  69                    unreviewed_changes = Patch::new(vec![Edit {
  70                        old: 0..1,
  71                        new: 0..text_snapshot.max_point().row + 1,
  72                    }])
  73                } else {
  74                    base_text = buffer.read(cx).as_rope().clone();
  75                    status = TrackedBufferStatus::Modified;
  76                    unreviewed_changes = Patch::default();
  77                }
  78                TrackedBuffer {
  79                    buffer: buffer.clone(),
  80                    base_text,
  81                    unreviewed_changes,
  82                    snapshot: text_snapshot.clone(),
  83                    status,
  84                    version: buffer.read(cx).version(),
  85                    diff,
  86                    diff_update: diff_update_tx,
  87                    _open_lsp_handle: open_lsp_handle,
  88                    _maintain_diff: cx.spawn({
  89                        let buffer = buffer.clone();
  90                        async move |this, cx| {
  91                            Self::maintain_diff(this, buffer, diff_update_rx, cx)
  92                                .await
  93                                .ok();
  94                        }
  95                    }),
  96                    _subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
  97                }
  98            });
  99        tracked_buffer.version = buffer.read(cx).version();
 100        tracked_buffer
 101    }
 102
 103    fn handle_buffer_event(
 104        &mut self,
 105        buffer: Entity<Buffer>,
 106        event: &BufferEvent,
 107        cx: &mut Context<Self>,
 108    ) {
 109        match event {
 110            BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx),
 111            BufferEvent::FileHandleChanged => {
 112                self.handle_buffer_file_changed(buffer, cx);
 113            }
 114            _ => {}
 115        };
 116    }
 117
 118    fn handle_buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 119        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 120            return;
 121        };
 122        tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
 123    }
 124
 125    fn handle_buffer_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 126        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 127            return;
 128        };
 129
 130        match tracked_buffer.status {
 131            TrackedBufferStatus::Created | TrackedBufferStatus::Modified => {
 132                if buffer
 133                    .read(cx)
 134                    .file()
 135                    .map_or(false, |file| file.disk_state() == DiskState::Deleted)
 136                {
 137                    // If the buffer had been edited by a tool, but it got
 138                    // deleted externally, we want to stop tracking it.
 139                    self.tracked_buffers.remove(&buffer);
 140                }
 141                cx.notify();
 142            }
 143            TrackedBufferStatus::Deleted => {
 144                if buffer
 145                    .read(cx)
 146                    .file()
 147                    .map_or(false, |file| file.disk_state() != DiskState::Deleted)
 148                {
 149                    // If the buffer had been deleted by a tool, but it got
 150                    // resurrected externally, we want to clear the changes we
 151                    // were tracking and reset the buffer's state.
 152                    self.tracked_buffers.remove(&buffer);
 153                    self.track_buffer_internal(buffer, false, cx);
 154                }
 155                cx.notify();
 156            }
 157        }
 158    }
 159
 160    async fn maintain_diff(
 161        this: WeakEntity<Self>,
 162        buffer: Entity<Buffer>,
 163        mut diff_update: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>,
 164        cx: &mut AsyncApp,
 165    ) -> Result<()> {
 166        while let Some((author, buffer_snapshot)) = diff_update.next().await {
 167            let (rebase, diff, language, language_registry) =
 168                this.read_with(cx, |this, cx| {
 169                    let tracked_buffer = this
 170                        .tracked_buffers
 171                        .get(&buffer)
 172                        .context("buffer not tracked")?;
 173
 174                    let rebase = cx.background_spawn({
 175                        let mut base_text = tracked_buffer.base_text.clone();
 176                        let old_snapshot = tracked_buffer.snapshot.clone();
 177                        let new_snapshot = buffer_snapshot.clone();
 178                        let unreviewed_changes = tracked_buffer.unreviewed_changes.clone();
 179                        async move {
 180                            let edits = diff_snapshots(&old_snapshot, &new_snapshot);
 181                            if let ChangeAuthor::User = author {
 182                                apply_non_conflicting_edits(
 183                                    &unreviewed_changes,
 184                                    edits,
 185                                    &mut base_text,
 186                                    new_snapshot.as_rope(),
 187                                );
 188                            }
 189                            (Arc::new(base_text.to_string()), base_text)
 190                        }
 191                    });
 192
 193                    anyhow::Ok((
 194                        rebase,
 195                        tracked_buffer.diff.clone(),
 196                        tracked_buffer.buffer.read(cx).language().cloned(),
 197                        tracked_buffer.buffer.read(cx).language_registry(),
 198                    ))
 199                })??;
 200
 201            let (new_base_text, new_base_text_rope) = rebase.await;
 202            let diff_snapshot = BufferDiff::update_diff(
 203                diff.clone(),
 204                buffer_snapshot.clone(),
 205                Some(new_base_text),
 206                true,
 207                false,
 208                language,
 209                language_registry,
 210                cx,
 211            )
 212            .await;
 213
 214            let mut unreviewed_changes = Patch::default();
 215            if let Ok(diff_snapshot) = diff_snapshot {
 216                unreviewed_changes = cx
 217                    .background_spawn({
 218                        let diff_snapshot = diff_snapshot.clone();
 219                        let buffer_snapshot = buffer_snapshot.clone();
 220                        let new_base_text_rope = new_base_text_rope.clone();
 221                        async move {
 222                            let mut unreviewed_changes = Patch::default();
 223                            for hunk in diff_snapshot.hunks_intersecting_range(
 224                                Anchor::MIN..Anchor::MAX,
 225                                &buffer_snapshot,
 226                            ) {
 227                                let old_range = new_base_text_rope
 228                                    .offset_to_point(hunk.diff_base_byte_range.start)
 229                                    ..new_base_text_rope
 230                                        .offset_to_point(hunk.diff_base_byte_range.end);
 231                                let new_range = hunk.range.start..hunk.range.end;
 232                                unreviewed_changes.push(point_to_row_edit(
 233                                    Edit {
 234                                        old: old_range,
 235                                        new: new_range,
 236                                    },
 237                                    &new_base_text_rope,
 238                                    &buffer_snapshot.as_rope(),
 239                                ));
 240                            }
 241                            unreviewed_changes
 242                        }
 243                    })
 244                    .await;
 245
 246                diff.update(cx, |diff, cx| {
 247                    diff.set_snapshot(diff_snapshot, &buffer_snapshot, cx)
 248                })?;
 249            }
 250            this.update(cx, |this, cx| {
 251                let tracked_buffer = this
 252                    .tracked_buffers
 253                    .get_mut(&buffer)
 254                    .context("buffer not tracked")?;
 255                tracked_buffer.base_text = new_base_text_rope;
 256                tracked_buffer.snapshot = buffer_snapshot;
 257                tracked_buffer.unreviewed_changes = unreviewed_changes;
 258                cx.notify();
 259                anyhow::Ok(())
 260            })??;
 261        }
 262
 263        Ok(())
 264    }
 265
 266    /// Track a buffer as read, so we can notify the model about user edits.
 267    pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 268        self.track_buffer_internal(buffer, false, cx);
 269    }
 270
 271    /// Mark a buffer as edited, so we can refresh it in the context
 272    pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 273        self.edited_since_project_diagnostics_check = true;
 274        self.tracked_buffers.remove(&buffer);
 275        self.track_buffer_internal(buffer.clone(), true, 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_internal(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_internal(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.buffer_read(buffer.clone(), 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        zlog::init_test();
 680    }
 681
 682    fn init_test(cx: &mut TestAppContext) {
 683        cx.update(|cx| {
 684            let settings_store = SettingsStore::test(cx);
 685            cx.set_global(settings_store);
 686            language::init(cx);
 687            Project::init_settings(cx);
 688        });
 689    }
 690
 691    #[gpui::test(iterations = 10)]
 692    async fn test_keep_edits(cx: &mut TestAppContext) {
 693        init_test(cx);
 694
 695        let fs = FakeFs::new(cx.executor());
 696        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
 697            .await;
 698        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 699        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 700        let file_path = project
 701            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
 702            .unwrap();
 703        let buffer = project
 704            .update(cx, |project, cx| project.open_buffer(file_path, cx))
 705            .await
 706            .unwrap();
 707
 708        cx.update(|cx| {
 709            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 710            buffer.update(cx, |buffer, cx| {
 711                buffer
 712                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
 713                    .unwrap()
 714            });
 715            buffer.update(cx, |buffer, cx| {
 716                buffer
 717                    .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
 718                    .unwrap()
 719            });
 720            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 721        });
 722        cx.run_until_parked();
 723        assert_eq!(
 724            buffer.read_with(cx, |buffer, _| buffer.text()),
 725            "abc\ndEf\nghi\njkl\nmnO"
 726        );
 727        assert_eq!(
 728            unreviewed_hunks(&action_log, cx),
 729            vec![(
 730                buffer.clone(),
 731                vec![
 732                    HunkStatus {
 733                        range: Point::new(1, 0)..Point::new(2, 0),
 734                        diff_status: DiffHunkStatusKind::Modified,
 735                        old_text: "def\n".into(),
 736                    },
 737                    HunkStatus {
 738                        range: Point::new(4, 0)..Point::new(4, 3),
 739                        diff_status: DiffHunkStatusKind::Modified,
 740                        old_text: "mno".into(),
 741                    }
 742                ],
 743            )]
 744        );
 745
 746        action_log.update(cx, |log, cx| {
 747            log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
 748        });
 749        cx.run_until_parked();
 750        assert_eq!(
 751            unreviewed_hunks(&action_log, cx),
 752            vec![(
 753                buffer.clone(),
 754                vec![HunkStatus {
 755                    range: Point::new(1, 0)..Point::new(2, 0),
 756                    diff_status: DiffHunkStatusKind::Modified,
 757                    old_text: "def\n".into(),
 758                }],
 759            )]
 760        );
 761
 762        action_log.update(cx, |log, cx| {
 763            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
 764        });
 765        cx.run_until_parked();
 766        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 767    }
 768
 769    #[gpui::test(iterations = 10)]
 770    async fn test_deletions(cx: &mut TestAppContext) {
 771        init_test(cx);
 772
 773        let fs = FakeFs::new(cx.executor());
 774        fs.insert_tree(
 775            path!("/dir"),
 776            json!({"file": "abc\ndef\nghi\njkl\nmno\npqr"}),
 777        )
 778        .await;
 779        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 780        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 781        let file_path = project
 782            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
 783            .unwrap();
 784        let buffer = project
 785            .update(cx, |project, cx| project.open_buffer(file_path, cx))
 786            .await
 787            .unwrap();
 788
 789        cx.update(|cx| {
 790            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 791            buffer.update(cx, |buffer, cx| {
 792                buffer
 793                    .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx)
 794                    .unwrap();
 795                buffer.finalize_last_transaction();
 796            });
 797            buffer.update(cx, |buffer, cx| {
 798                buffer
 799                    .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx)
 800                    .unwrap();
 801                buffer.finalize_last_transaction();
 802            });
 803            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 804        });
 805        cx.run_until_parked();
 806        assert_eq!(
 807            buffer.read_with(cx, |buffer, _| buffer.text()),
 808            "abc\nghi\njkl\npqr"
 809        );
 810        assert_eq!(
 811            unreviewed_hunks(&action_log, cx),
 812            vec![(
 813                buffer.clone(),
 814                vec![
 815                    HunkStatus {
 816                        range: Point::new(1, 0)..Point::new(1, 0),
 817                        diff_status: DiffHunkStatusKind::Deleted,
 818                        old_text: "def\n".into(),
 819                    },
 820                    HunkStatus {
 821                        range: Point::new(3, 0)..Point::new(3, 0),
 822                        diff_status: DiffHunkStatusKind::Deleted,
 823                        old_text: "mno\n".into(),
 824                    }
 825                ],
 826            )]
 827        );
 828
 829        buffer.update(cx, |buffer, cx| buffer.undo(cx));
 830        cx.run_until_parked();
 831        assert_eq!(
 832            buffer.read_with(cx, |buffer, _| buffer.text()),
 833            "abc\nghi\njkl\nmno\npqr"
 834        );
 835        assert_eq!(
 836            unreviewed_hunks(&action_log, cx),
 837            vec![(
 838                buffer.clone(),
 839                vec![HunkStatus {
 840                    range: Point::new(1, 0)..Point::new(1, 0),
 841                    diff_status: DiffHunkStatusKind::Deleted,
 842                    old_text: "def\n".into(),
 843                }],
 844            )]
 845        );
 846
 847        action_log.update(cx, |log, cx| {
 848            log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx)
 849        });
 850        cx.run_until_parked();
 851        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 852    }
 853
 854    #[gpui::test(iterations = 10)]
 855    async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
 856        init_test(cx);
 857
 858        let fs = FakeFs::new(cx.executor());
 859        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
 860            .await;
 861        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 862        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 863        let file_path = project
 864            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
 865            .unwrap();
 866        let buffer = project
 867            .update(cx, |project, cx| project.open_buffer(file_path, cx))
 868            .await
 869            .unwrap();
 870
 871        cx.update(|cx| {
 872            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 873            buffer.update(cx, |buffer, cx| {
 874                buffer
 875                    .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
 876                    .unwrap()
 877            });
 878            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 879        });
 880        cx.run_until_parked();
 881        assert_eq!(
 882            buffer.read_with(cx, |buffer, _| buffer.text()),
 883            "abc\ndeF\nGHI\njkl\nmno"
 884        );
 885        assert_eq!(
 886            unreviewed_hunks(&action_log, cx),
 887            vec![(
 888                buffer.clone(),
 889                vec![HunkStatus {
 890                    range: Point::new(1, 0)..Point::new(3, 0),
 891                    diff_status: DiffHunkStatusKind::Modified,
 892                    old_text: "def\nghi\n".into(),
 893                }],
 894            )]
 895        );
 896
 897        buffer.update(cx, |buffer, cx| {
 898            buffer.edit(
 899                [
 900                    (Point::new(0, 2)..Point::new(0, 2), "X"),
 901                    (Point::new(3, 0)..Point::new(3, 0), "Y"),
 902                ],
 903                None,
 904                cx,
 905            )
 906        });
 907        cx.run_until_parked();
 908        assert_eq!(
 909            buffer.read_with(cx, |buffer, _| buffer.text()),
 910            "abXc\ndeF\nGHI\nYjkl\nmno"
 911        );
 912        assert_eq!(
 913            unreviewed_hunks(&action_log, cx),
 914            vec![(
 915                buffer.clone(),
 916                vec![HunkStatus {
 917                    range: Point::new(1, 0)..Point::new(3, 0),
 918                    diff_status: DiffHunkStatusKind::Modified,
 919                    old_text: "def\nghi\n".into(),
 920                }],
 921            )]
 922        );
 923
 924        buffer.update(cx, |buffer, cx| {
 925            buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx)
 926        });
 927        cx.run_until_parked();
 928        assert_eq!(
 929            buffer.read_with(cx, |buffer, _| buffer.text()),
 930            "abXc\ndZeF\nGHI\nYjkl\nmno"
 931        );
 932        assert_eq!(
 933            unreviewed_hunks(&action_log, cx),
 934            vec![(
 935                buffer.clone(),
 936                vec![HunkStatus {
 937                    range: Point::new(1, 0)..Point::new(3, 0),
 938                    diff_status: DiffHunkStatusKind::Modified,
 939                    old_text: "def\nghi\n".into(),
 940                }],
 941            )]
 942        );
 943
 944        action_log.update(cx, |log, cx| {
 945            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
 946        });
 947        cx.run_until_parked();
 948        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 949    }
 950
 951    #[gpui::test(iterations = 10)]
 952    async fn test_creating_files(cx: &mut TestAppContext) {
 953        init_test(cx);
 954
 955        let fs = FakeFs::new(cx.executor());
 956        fs.insert_tree(path!("/dir"), json!({})).await;
 957        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 958        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 959        let file_path = project
 960            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
 961            .unwrap();
 962
 963        let buffer = project
 964            .update(cx, |project, cx| project.open_buffer(file_path, cx))
 965            .await
 966            .unwrap();
 967        cx.update(|cx| {
 968            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
 969            buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
 970            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 971        });
 972        project
 973            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 974            .await
 975            .unwrap();
 976        cx.run_until_parked();
 977        assert_eq!(
 978            unreviewed_hunks(&action_log, cx),
 979            vec![(
 980                buffer.clone(),
 981                vec![HunkStatus {
 982                    range: Point::new(0, 0)..Point::new(0, 5),
 983                    diff_status: DiffHunkStatusKind::Added,
 984                    old_text: "".into(),
 985                }],
 986            )]
 987        );
 988
 989        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx));
 990        cx.run_until_parked();
 991        assert_eq!(
 992            unreviewed_hunks(&action_log, cx),
 993            vec![(
 994                buffer.clone(),
 995                vec![HunkStatus {
 996                    range: Point::new(0, 0)..Point::new(0, 6),
 997                    diff_status: DiffHunkStatusKind::Added,
 998                    old_text: "".into(),
 999                }],
1000            )]
1001        );
1002
1003        action_log.update(cx, |log, cx| {
1004            log.keep_edits_in_range(buffer.clone(), 0..5, cx)
1005        });
1006        cx.run_until_parked();
1007        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1008    }
1009
1010    #[gpui::test(iterations = 10)]
1011    async fn test_deleting_files(cx: &mut TestAppContext) {
1012        init_test(cx);
1013
1014        let fs = FakeFs::new(cx.executor());
1015        fs.insert_tree(
1016            path!("/dir"),
1017            json!({"file1": "lorem\n", "file2": "ipsum\n"}),
1018        )
1019        .await;
1020
1021        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1022        let file1_path = project
1023            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
1024            .unwrap();
1025        let file2_path = project
1026            .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
1027            .unwrap();
1028
1029        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1030        let buffer1 = project
1031            .update(cx, |project, cx| {
1032                project.open_buffer(file1_path.clone(), cx)
1033            })
1034            .await
1035            .unwrap();
1036        let buffer2 = project
1037            .update(cx, |project, cx| {
1038                project.open_buffer(file2_path.clone(), cx)
1039            })
1040            .await
1041            .unwrap();
1042
1043        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
1044        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
1045        project
1046            .update(cx, |project, cx| {
1047                project.delete_file(file1_path.clone(), false, cx)
1048            })
1049            .unwrap()
1050            .await
1051            .unwrap();
1052        project
1053            .update(cx, |project, cx| {
1054                project.delete_file(file2_path.clone(), false, cx)
1055            })
1056            .unwrap()
1057            .await
1058            .unwrap();
1059        cx.run_until_parked();
1060        assert_eq!(
1061            unreviewed_hunks(&action_log, cx),
1062            vec![
1063                (
1064                    buffer1.clone(),
1065                    vec![HunkStatus {
1066                        range: Point::new(0, 0)..Point::new(0, 0),
1067                        diff_status: DiffHunkStatusKind::Deleted,
1068                        old_text: "lorem\n".into(),
1069                    }]
1070                ),
1071                (
1072                    buffer2.clone(),
1073                    vec![HunkStatus {
1074                        range: Point::new(0, 0)..Point::new(0, 0),
1075                        diff_status: DiffHunkStatusKind::Deleted,
1076                        old_text: "ipsum\n".into(),
1077                    }],
1078                )
1079            ]
1080        );
1081
1082        // Simulate file1 being recreated externally.
1083        fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
1084            .await;
1085
1086        // Simulate file2 being recreated by a tool.
1087        let buffer2 = project
1088            .update(cx, |project, cx| project.open_buffer(file2_path, cx))
1089            .await
1090            .unwrap();
1091        action_log.update(cx, |log, cx| log.buffer_read(buffer2.clone(), cx));
1092        buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
1093        action_log.update(cx, |log, cx| log.buffer_edited(buffer2.clone(), cx));
1094        project
1095            .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
1096            .await
1097            .unwrap();
1098
1099        cx.run_until_parked();
1100        assert_eq!(
1101            unreviewed_hunks(&action_log, cx),
1102            vec![(
1103                buffer2.clone(),
1104                vec![HunkStatus {
1105                    range: Point::new(0, 0)..Point::new(0, 5),
1106                    diff_status: DiffHunkStatusKind::Modified,
1107                    old_text: "ipsum\n".into(),
1108                }],
1109            )]
1110        );
1111
1112        // Simulate file2 being deleted externally.
1113        fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
1114            .await
1115            .unwrap();
1116        cx.run_until_parked();
1117        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1118    }
1119
1120    #[gpui::test(iterations = 10)]
1121    async fn test_reject_edits(cx: &mut TestAppContext) {
1122        init_test(cx);
1123
1124        let fs = FakeFs::new(cx.executor());
1125        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
1126            .await;
1127        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1128        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1129        let file_path = project
1130            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1131            .unwrap();
1132        let buffer = project
1133            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1134            .await
1135            .unwrap();
1136
1137        cx.update(|cx| {
1138            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1139            buffer.update(cx, |buffer, cx| {
1140                buffer
1141                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
1142                    .unwrap()
1143            });
1144            buffer.update(cx, |buffer, cx| {
1145                buffer
1146                    .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
1147                    .unwrap()
1148            });
1149            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1150        });
1151        cx.run_until_parked();
1152        assert_eq!(
1153            buffer.read_with(cx, |buffer, _| buffer.text()),
1154            "abc\ndE\nXYZf\nghi\njkl\nmnO"
1155        );
1156        assert_eq!(
1157            unreviewed_hunks(&action_log, cx),
1158            vec![(
1159                buffer.clone(),
1160                vec![
1161                    HunkStatus {
1162                        range: Point::new(1, 0)..Point::new(3, 0),
1163                        diff_status: DiffHunkStatusKind::Modified,
1164                        old_text: "def\n".into(),
1165                    },
1166                    HunkStatus {
1167                        range: Point::new(5, 0)..Point::new(5, 3),
1168                        diff_status: DiffHunkStatusKind::Modified,
1169                        old_text: "mno".into(),
1170                    }
1171                ],
1172            )]
1173        );
1174
1175        // If the rejected range doesn't overlap with any hunk, we ignore it.
1176        action_log
1177            .update(cx, |log, cx| {
1178                log.reject_edits_in_ranges(
1179                    buffer.clone(),
1180                    vec![Point::new(4, 0)..Point::new(4, 0)],
1181                    cx,
1182                )
1183            })
1184            .await
1185            .unwrap();
1186        cx.run_until_parked();
1187        assert_eq!(
1188            buffer.read_with(cx, |buffer, _| buffer.text()),
1189            "abc\ndE\nXYZf\nghi\njkl\nmnO"
1190        );
1191        assert_eq!(
1192            unreviewed_hunks(&action_log, cx),
1193            vec![(
1194                buffer.clone(),
1195                vec![
1196                    HunkStatus {
1197                        range: Point::new(1, 0)..Point::new(3, 0),
1198                        diff_status: DiffHunkStatusKind::Modified,
1199                        old_text: "def\n".into(),
1200                    },
1201                    HunkStatus {
1202                        range: Point::new(5, 0)..Point::new(5, 3),
1203                        diff_status: DiffHunkStatusKind::Modified,
1204                        old_text: "mno".into(),
1205                    }
1206                ],
1207            )]
1208        );
1209
1210        action_log
1211            .update(cx, |log, cx| {
1212                log.reject_edits_in_ranges(
1213                    buffer.clone(),
1214                    vec![Point::new(0, 0)..Point::new(1, 0)],
1215                    cx,
1216                )
1217            })
1218            .await
1219            .unwrap();
1220        cx.run_until_parked();
1221        assert_eq!(
1222            buffer.read_with(cx, |buffer, _| buffer.text()),
1223            "abc\ndef\nghi\njkl\nmnO"
1224        );
1225        assert_eq!(
1226            unreviewed_hunks(&action_log, cx),
1227            vec![(
1228                buffer.clone(),
1229                vec![HunkStatus {
1230                    range: Point::new(4, 0)..Point::new(4, 3),
1231                    diff_status: DiffHunkStatusKind::Modified,
1232                    old_text: "mno".into(),
1233                }],
1234            )]
1235        );
1236
1237        action_log
1238            .update(cx, |log, cx| {
1239                log.reject_edits_in_ranges(
1240                    buffer.clone(),
1241                    vec![Point::new(4, 0)..Point::new(4, 0)],
1242                    cx,
1243                )
1244            })
1245            .await
1246            .unwrap();
1247        cx.run_until_parked();
1248        assert_eq!(
1249            buffer.read_with(cx, |buffer, _| buffer.text()),
1250            "abc\ndef\nghi\njkl\nmno"
1251        );
1252        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1253    }
1254
1255    #[gpui::test(iterations = 10)]
1256    async fn test_reject_multiple_edits(cx: &mut TestAppContext) {
1257        init_test(cx);
1258
1259        let fs = FakeFs::new(cx.executor());
1260        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
1261            .await;
1262        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1263        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1264        let file_path = project
1265            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1266            .unwrap();
1267        let buffer = project
1268            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1269            .await
1270            .unwrap();
1271
1272        cx.update(|cx| {
1273            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1274            buffer.update(cx, |buffer, cx| {
1275                buffer
1276                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
1277                    .unwrap()
1278            });
1279            buffer.update(cx, |buffer, cx| {
1280                buffer
1281                    .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
1282                    .unwrap()
1283            });
1284            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1285        });
1286        cx.run_until_parked();
1287        assert_eq!(
1288            buffer.read_with(cx, |buffer, _| buffer.text()),
1289            "abc\ndE\nXYZf\nghi\njkl\nmnO"
1290        );
1291        assert_eq!(
1292            unreviewed_hunks(&action_log, cx),
1293            vec![(
1294                buffer.clone(),
1295                vec![
1296                    HunkStatus {
1297                        range: Point::new(1, 0)..Point::new(3, 0),
1298                        diff_status: DiffHunkStatusKind::Modified,
1299                        old_text: "def\n".into(),
1300                    },
1301                    HunkStatus {
1302                        range: Point::new(5, 0)..Point::new(5, 3),
1303                        diff_status: DiffHunkStatusKind::Modified,
1304                        old_text: "mno".into(),
1305                    }
1306                ],
1307            )]
1308        );
1309
1310        action_log.update(cx, |log, cx| {
1311            let range_1 = buffer.read(cx).anchor_before(Point::new(0, 0))
1312                ..buffer.read(cx).anchor_before(Point::new(1, 0));
1313            let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0))
1314                ..buffer.read(cx).anchor_before(Point::new(5, 3));
1315
1316            log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], cx)
1317                .detach();
1318            assert_eq!(
1319                buffer.read_with(cx, |buffer, _| buffer.text()),
1320                "abc\ndef\nghi\njkl\nmno"
1321            );
1322        });
1323        cx.run_until_parked();
1324        assert_eq!(
1325            buffer.read_with(cx, |buffer, _| buffer.text()),
1326            "abc\ndef\nghi\njkl\nmno"
1327        );
1328        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1329    }
1330
1331    #[gpui::test(iterations = 10)]
1332    async fn test_reject_deleted_file(cx: &mut TestAppContext) {
1333        init_test(cx);
1334
1335        let fs = FakeFs::new(cx.executor());
1336        fs.insert_tree(path!("/dir"), json!({"file": "content"}))
1337            .await;
1338        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1339        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1340        let file_path = project
1341            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1342            .unwrap();
1343        let buffer = project
1344            .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
1345            .await
1346            .unwrap();
1347
1348        cx.update(|cx| {
1349            action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx));
1350        });
1351        project
1352            .update(cx, |project, cx| {
1353                project.delete_file(file_path.clone(), false, cx)
1354            })
1355            .unwrap()
1356            .await
1357            .unwrap();
1358        cx.run_until_parked();
1359        assert!(!fs.is_file(path!("/dir/file").as_ref()).await);
1360        assert_eq!(
1361            unreviewed_hunks(&action_log, cx),
1362            vec![(
1363                buffer.clone(),
1364                vec![HunkStatus {
1365                    range: Point::new(0, 0)..Point::new(0, 0),
1366                    diff_status: DiffHunkStatusKind::Deleted,
1367                    old_text: "content".into(),
1368                }]
1369            )]
1370        );
1371
1372        action_log
1373            .update(cx, |log, cx| {
1374                log.reject_edits_in_ranges(
1375                    buffer.clone(),
1376                    vec![Point::new(0, 0)..Point::new(0, 0)],
1377                    cx,
1378                )
1379            })
1380            .await
1381            .unwrap();
1382        cx.run_until_parked();
1383        assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "content");
1384        assert!(fs.is_file(path!("/dir/file").as_ref()).await);
1385        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1386    }
1387
1388    #[gpui::test(iterations = 10)]
1389    async fn test_reject_created_file(cx: &mut TestAppContext) {
1390        init_test(cx);
1391
1392        let fs = FakeFs::new(cx.executor());
1393        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1394        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1395        let file_path = project
1396            .read_with(cx, |project, cx| {
1397                project.find_project_path("dir/new_file", cx)
1398            })
1399            .unwrap();
1400
1401        let buffer = project
1402            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1403            .await
1404            .unwrap();
1405        cx.update(|cx| {
1406            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
1407            buffer.update(cx, |buffer, cx| buffer.set_text("content", cx));
1408            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1409        });
1410        project
1411            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1412            .await
1413            .unwrap();
1414        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
1415        cx.run_until_parked();
1416        assert_eq!(
1417            unreviewed_hunks(&action_log, cx),
1418            vec![(
1419                buffer.clone(),
1420                vec![HunkStatus {
1421                    range: Point::new(0, 0)..Point::new(0, 7),
1422                    diff_status: DiffHunkStatusKind::Added,
1423                    old_text: "".into(),
1424                }],
1425            )]
1426        );
1427
1428        action_log
1429            .update(cx, |log, cx| {
1430                log.reject_edits_in_ranges(
1431                    buffer.clone(),
1432                    vec![Point::new(0, 0)..Point::new(0, 11)],
1433                    cx,
1434                )
1435            })
1436            .await
1437            .unwrap();
1438        cx.run_until_parked();
1439        assert!(!fs.is_file(path!("/dir/new_file").as_ref()).await);
1440        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1441    }
1442
1443    #[gpui::test(iterations = 100)]
1444    async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
1445        init_test(cx);
1446
1447        let operations = env::var("OPERATIONS")
1448            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
1449            .unwrap_or(20);
1450
1451        let text = RandomCharIter::new(&mut rng).take(50).collect::<String>();
1452        let fs = FakeFs::new(cx.executor());
1453        fs.insert_tree(path!("/dir"), json!({"file": text})).await;
1454        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1455        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1456        let file_path = project
1457            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1458            .unwrap();
1459        let buffer = project
1460            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1461            .await
1462            .unwrap();
1463
1464        action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1465
1466        for _ in 0..operations {
1467            match rng.gen_range(0..100) {
1468                0..25 => {
1469                    action_log.update(cx, |log, cx| {
1470                        let range = buffer.read(cx).random_byte_range(0, &mut rng);
1471                        log::info!("keeping edits in range {:?}", range);
1472                        log.keep_edits_in_range(buffer.clone(), range, cx)
1473                    });
1474                }
1475                25..50 => {
1476                    action_log
1477                        .update(cx, |log, cx| {
1478                            let range = buffer.read(cx).random_byte_range(0, &mut rng);
1479                            log::info!("rejecting edits in range {:?}", range);
1480                            log.reject_edits_in_ranges(buffer.clone(), vec![range], cx)
1481                        })
1482                        .await
1483                        .unwrap();
1484                }
1485                _ => {
1486                    let is_agent_change = rng.gen_bool(0.5);
1487                    if is_agent_change {
1488                        log::info!("agent edit");
1489                    } else {
1490                        log::info!("user edit");
1491                    }
1492                    cx.update(|cx| {
1493                        buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
1494                        if is_agent_change {
1495                            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1496                        }
1497                    });
1498                }
1499            }
1500
1501            if rng.gen_bool(0.2) {
1502                quiesce(&action_log, &buffer, cx);
1503            }
1504        }
1505
1506        quiesce(&action_log, &buffer, cx);
1507
1508        fn quiesce(
1509            action_log: &Entity<ActionLog>,
1510            buffer: &Entity<Buffer>,
1511            cx: &mut TestAppContext,
1512        ) {
1513            log::info!("quiescing...");
1514            cx.run_until_parked();
1515            action_log.update(cx, |log, cx| {
1516                let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap();
1517                let mut old_text = tracked_buffer.base_text.clone();
1518                let new_text = buffer.read(cx).as_rope();
1519                for edit in tracked_buffer.unreviewed_changes.edits() {
1520                    let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
1521                    let old_end = old_text.point_to_offset(cmp::min(
1522                        Point::new(edit.new.start + edit.old_len(), 0),
1523                        old_text.max_point(),
1524                    ));
1525                    old_text.replace(
1526                        old_start..old_end,
1527                        &new_text.slice_rows(edit.new.clone()).to_string(),
1528                    );
1529                }
1530                pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
1531            })
1532        }
1533    }
1534
1535    #[derive(Debug, Clone, PartialEq, Eq)]
1536    struct HunkStatus {
1537        range: Range<Point>,
1538        diff_status: DiffHunkStatusKind,
1539        old_text: String,
1540    }
1541
1542    fn unreviewed_hunks(
1543        action_log: &Entity<ActionLog>,
1544        cx: &TestAppContext,
1545    ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
1546        cx.read(|cx| {
1547            action_log
1548                .read(cx)
1549                .changed_buffers(cx)
1550                .into_iter()
1551                .map(|(buffer, diff)| {
1552                    let snapshot = buffer.read(cx).snapshot();
1553                    (
1554                        buffer,
1555                        diff.read(cx)
1556                            .hunks(&snapshot, cx)
1557                            .map(|hunk| HunkStatus {
1558                                diff_status: hunk.status().kind,
1559                                range: hunk.range,
1560                                old_text: diff
1561                                    .read(cx)
1562                                    .base_text()
1563                                    .text_for_range(hunk.diff_base_byte_range)
1564                                    .collect(),
1565                            })
1566                            .collect(),
1567                    )
1568                })
1569                .collect()
1570        })
1571    }
1572}