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    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        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        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
 699            .await;
 700        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 701        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 702        let file_path = project
 703            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
 704            .unwrap();
 705        let buffer = project
 706            .update(cx, |project, cx| project.open_buffer(file_path, cx))
 707            .await
 708            .unwrap();
 709
 710        cx.update(|cx| {
 711            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 712            buffer.update(cx, |buffer, cx| {
 713                buffer
 714                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
 715                    .unwrap()
 716            });
 717            buffer.update(cx, |buffer, cx| {
 718                buffer
 719                    .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
 720                    .unwrap()
 721            });
 722            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 723        });
 724        cx.run_until_parked();
 725        assert_eq!(
 726            buffer.read_with(cx, |buffer, _| buffer.text()),
 727            "abc\ndEf\nghi\njkl\nmnO"
 728        );
 729        assert_eq!(
 730            unreviewed_hunks(&action_log, cx),
 731            vec![(
 732                buffer.clone(),
 733                vec![
 734                    HunkStatus {
 735                        range: Point::new(1, 0)..Point::new(2, 0),
 736                        diff_status: DiffHunkStatusKind::Modified,
 737                        old_text: "def\n".into(),
 738                    },
 739                    HunkStatus {
 740                        range: Point::new(4, 0)..Point::new(4, 3),
 741                        diff_status: DiffHunkStatusKind::Modified,
 742                        old_text: "mno".into(),
 743                    }
 744                ],
 745            )]
 746        );
 747
 748        action_log.update(cx, |log, cx| {
 749            log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
 750        });
 751        cx.run_until_parked();
 752        assert_eq!(
 753            unreviewed_hunks(&action_log, cx),
 754            vec![(
 755                buffer.clone(),
 756                vec![HunkStatus {
 757                    range: Point::new(1, 0)..Point::new(2, 0),
 758                    diff_status: DiffHunkStatusKind::Modified,
 759                    old_text: "def\n".into(),
 760                }],
 761            )]
 762        );
 763
 764        action_log.update(cx, |log, cx| {
 765            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
 766        });
 767        cx.run_until_parked();
 768        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 769    }
 770
 771    #[gpui::test(iterations = 10)]
 772    async fn test_deletions(cx: &mut TestAppContext) {
 773        init_test(cx);
 774
 775        let fs = FakeFs::new(cx.executor());
 776        fs.insert_tree(
 777            path!("/dir"),
 778            json!({"file": "abc\ndef\nghi\njkl\nmno\npqr"}),
 779        )
 780        .await;
 781        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 782        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 783        let file_path = project
 784            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
 785            .unwrap();
 786        let buffer = project
 787            .update(cx, |project, cx| project.open_buffer(file_path, cx))
 788            .await
 789            .unwrap();
 790
 791        cx.update(|cx| {
 792            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 793            buffer.update(cx, |buffer, cx| {
 794                buffer
 795                    .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx)
 796                    .unwrap();
 797                buffer.finalize_last_transaction();
 798            });
 799            buffer.update(cx, |buffer, cx| {
 800                buffer
 801                    .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx)
 802                    .unwrap();
 803                buffer.finalize_last_transaction();
 804            });
 805            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 806        });
 807        cx.run_until_parked();
 808        assert_eq!(
 809            buffer.read_with(cx, |buffer, _| buffer.text()),
 810            "abc\nghi\njkl\npqr"
 811        );
 812        assert_eq!(
 813            unreviewed_hunks(&action_log, cx),
 814            vec![(
 815                buffer.clone(),
 816                vec![
 817                    HunkStatus {
 818                        range: Point::new(1, 0)..Point::new(1, 0),
 819                        diff_status: DiffHunkStatusKind::Deleted,
 820                        old_text: "def\n".into(),
 821                    },
 822                    HunkStatus {
 823                        range: Point::new(3, 0)..Point::new(3, 0),
 824                        diff_status: DiffHunkStatusKind::Deleted,
 825                        old_text: "mno\n".into(),
 826                    }
 827                ],
 828            )]
 829        );
 830
 831        buffer.update(cx, |buffer, cx| buffer.undo(cx));
 832        cx.run_until_parked();
 833        assert_eq!(
 834            buffer.read_with(cx, |buffer, _| buffer.text()),
 835            "abc\nghi\njkl\nmno\npqr"
 836        );
 837        assert_eq!(
 838            unreviewed_hunks(&action_log, cx),
 839            vec![(
 840                buffer.clone(),
 841                vec![HunkStatus {
 842                    range: Point::new(1, 0)..Point::new(1, 0),
 843                    diff_status: DiffHunkStatusKind::Deleted,
 844                    old_text: "def\n".into(),
 845                }],
 846            )]
 847        );
 848
 849        action_log.update(cx, |log, cx| {
 850            log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx)
 851        });
 852        cx.run_until_parked();
 853        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 854    }
 855
 856    #[gpui::test(iterations = 10)]
 857    async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
 858        init_test(cx);
 859
 860        let fs = FakeFs::new(cx.executor());
 861        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
 862            .await;
 863        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 864        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 865        let file_path = project
 866            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
 867            .unwrap();
 868        let buffer = project
 869            .update(cx, |project, cx| project.open_buffer(file_path, cx))
 870            .await
 871            .unwrap();
 872
 873        cx.update(|cx| {
 874            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 875            buffer.update(cx, |buffer, cx| {
 876                buffer
 877                    .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
 878                    .unwrap()
 879            });
 880            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 881        });
 882        cx.run_until_parked();
 883        assert_eq!(
 884            buffer.read_with(cx, |buffer, _| buffer.text()),
 885            "abc\ndeF\nGHI\njkl\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(
 901                [
 902                    (Point::new(0, 2)..Point::new(0, 2), "X"),
 903                    (Point::new(3, 0)..Point::new(3, 0), "Y"),
 904                ],
 905                None,
 906                cx,
 907            )
 908        });
 909        cx.run_until_parked();
 910        assert_eq!(
 911            buffer.read_with(cx, |buffer, _| buffer.text()),
 912            "abXc\ndeF\nGHI\nYjkl\nmno"
 913        );
 914        assert_eq!(
 915            unreviewed_hunks(&action_log, cx),
 916            vec![(
 917                buffer.clone(),
 918                vec![HunkStatus {
 919                    range: Point::new(1, 0)..Point::new(3, 0),
 920                    diff_status: DiffHunkStatusKind::Modified,
 921                    old_text: "def\nghi\n".into(),
 922                }],
 923            )]
 924        );
 925
 926        buffer.update(cx, |buffer, cx| {
 927            buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx)
 928        });
 929        cx.run_until_parked();
 930        assert_eq!(
 931            buffer.read_with(cx, |buffer, _| buffer.text()),
 932            "abXc\ndZeF\nGHI\nYjkl\nmno"
 933        );
 934        assert_eq!(
 935            unreviewed_hunks(&action_log, cx),
 936            vec![(
 937                buffer.clone(),
 938                vec![HunkStatus {
 939                    range: Point::new(1, 0)..Point::new(3, 0),
 940                    diff_status: DiffHunkStatusKind::Modified,
 941                    old_text: "def\nghi\n".into(),
 942                }],
 943            )]
 944        );
 945
 946        action_log.update(cx, |log, cx| {
 947            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
 948        });
 949        cx.run_until_parked();
 950        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 951    }
 952
 953    #[gpui::test(iterations = 10)]
 954    async fn test_creating_files(cx: &mut TestAppContext) {
 955        init_test(cx);
 956
 957        let fs = FakeFs::new(cx.executor());
 958        fs.insert_tree(path!("/dir"), json!({})).await;
 959        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 960        let action_log = cx.new(|_| ActionLog::new(project.clone()));
 961        let file_path = project
 962            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
 963            .unwrap();
 964
 965        let buffer = project
 966            .update(cx, |project, cx| project.open_buffer(file_path, cx))
 967            .await
 968            .unwrap();
 969        cx.update(|cx| {
 970            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
 971            buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
 972            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 973        });
 974        project
 975            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 976            .await
 977            .unwrap();
 978        cx.run_until_parked();
 979        assert_eq!(
 980            unreviewed_hunks(&action_log, cx),
 981            vec![(
 982                buffer.clone(),
 983                vec![HunkStatus {
 984                    range: Point::new(0, 0)..Point::new(0, 5),
 985                    diff_status: DiffHunkStatusKind::Added,
 986                    old_text: "".into(),
 987                }],
 988            )]
 989        );
 990
 991        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx));
 992        cx.run_until_parked();
 993        assert_eq!(
 994            unreviewed_hunks(&action_log, cx),
 995            vec![(
 996                buffer.clone(),
 997                vec![HunkStatus {
 998                    range: Point::new(0, 0)..Point::new(0, 6),
 999                    diff_status: DiffHunkStatusKind::Added,
1000                    old_text: "".into(),
1001                }],
1002            )]
1003        );
1004
1005        action_log.update(cx, |log, cx| {
1006            log.keep_edits_in_range(buffer.clone(), 0..5, cx)
1007        });
1008        cx.run_until_parked();
1009        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1010    }
1011
1012    #[gpui::test(iterations = 10)]
1013    async fn test_deleting_files(cx: &mut TestAppContext) {
1014        init_test(cx);
1015
1016        let fs = FakeFs::new(cx.executor());
1017        fs.insert_tree(
1018            path!("/dir"),
1019            json!({"file1": "lorem\n", "file2": "ipsum\n"}),
1020        )
1021        .await;
1022
1023        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1024        let file1_path = project
1025            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
1026            .unwrap();
1027        let file2_path = project
1028            .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
1029            .unwrap();
1030
1031        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1032        let buffer1 = project
1033            .update(cx, |project, cx| {
1034                project.open_buffer(file1_path.clone(), cx)
1035            })
1036            .await
1037            .unwrap();
1038        let buffer2 = project
1039            .update(cx, |project, cx| {
1040                project.open_buffer(file2_path.clone(), cx)
1041            })
1042            .await
1043            .unwrap();
1044
1045        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
1046        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
1047        project
1048            .update(cx, |project, cx| {
1049                project.delete_file(file1_path.clone(), false, cx)
1050            })
1051            .unwrap()
1052            .await
1053            .unwrap();
1054        project
1055            .update(cx, |project, cx| {
1056                project.delete_file(file2_path.clone(), false, cx)
1057            })
1058            .unwrap()
1059            .await
1060            .unwrap();
1061        cx.run_until_parked();
1062        assert_eq!(
1063            unreviewed_hunks(&action_log, cx),
1064            vec![
1065                (
1066                    buffer1.clone(),
1067                    vec![HunkStatus {
1068                        range: Point::new(0, 0)..Point::new(0, 0),
1069                        diff_status: DiffHunkStatusKind::Deleted,
1070                        old_text: "lorem\n".into(),
1071                    }]
1072                ),
1073                (
1074                    buffer2.clone(),
1075                    vec![HunkStatus {
1076                        range: Point::new(0, 0)..Point::new(0, 0),
1077                        diff_status: DiffHunkStatusKind::Deleted,
1078                        old_text: "ipsum\n".into(),
1079                    }],
1080                )
1081            ]
1082        );
1083
1084        // Simulate file1 being recreated externally.
1085        fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
1086            .await;
1087
1088        // Simulate file2 being recreated by a tool.
1089        let buffer2 = project
1090            .update(cx, |project, cx| project.open_buffer(file2_path, cx))
1091            .await
1092            .unwrap();
1093        action_log.update(cx, |log, cx| log.buffer_read(buffer2.clone(), cx));
1094        buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
1095        action_log.update(cx, |log, cx| log.buffer_edited(buffer2.clone(), cx));
1096        project
1097            .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
1098            .await
1099            .unwrap();
1100
1101        cx.run_until_parked();
1102        assert_eq!(
1103            unreviewed_hunks(&action_log, cx),
1104            vec![(
1105                buffer2.clone(),
1106                vec![HunkStatus {
1107                    range: Point::new(0, 0)..Point::new(0, 5),
1108                    diff_status: DiffHunkStatusKind::Modified,
1109                    old_text: "ipsum\n".into(),
1110                }],
1111            )]
1112        );
1113
1114        // Simulate file2 being deleted externally.
1115        fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
1116            .await
1117            .unwrap();
1118        cx.run_until_parked();
1119        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1120    }
1121
1122    #[gpui::test(iterations = 10)]
1123    async fn test_reject_edits(cx: &mut TestAppContext) {
1124        init_test(cx);
1125
1126        let fs = FakeFs::new(cx.executor());
1127        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
1128            .await;
1129        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1130        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1131        let file_path = project
1132            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1133            .unwrap();
1134        let buffer = project
1135            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1136            .await
1137            .unwrap();
1138
1139        cx.update(|cx| {
1140            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1141            buffer.update(cx, |buffer, cx| {
1142                buffer
1143                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
1144                    .unwrap()
1145            });
1146            buffer.update(cx, |buffer, cx| {
1147                buffer
1148                    .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
1149                    .unwrap()
1150            });
1151            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1152        });
1153        cx.run_until_parked();
1154        assert_eq!(
1155            buffer.read_with(cx, |buffer, _| buffer.text()),
1156            "abc\ndE\nXYZf\nghi\njkl\nmnO"
1157        );
1158        assert_eq!(
1159            unreviewed_hunks(&action_log, cx),
1160            vec![(
1161                buffer.clone(),
1162                vec![
1163                    HunkStatus {
1164                        range: Point::new(1, 0)..Point::new(3, 0),
1165                        diff_status: DiffHunkStatusKind::Modified,
1166                        old_text: "def\n".into(),
1167                    },
1168                    HunkStatus {
1169                        range: Point::new(5, 0)..Point::new(5, 3),
1170                        diff_status: DiffHunkStatusKind::Modified,
1171                        old_text: "mno".into(),
1172                    }
1173                ],
1174            )]
1175        );
1176
1177        // If the rejected range doesn't overlap with any hunk, we ignore it.
1178        action_log
1179            .update(cx, |log, cx| {
1180                log.reject_edits_in_ranges(
1181                    buffer.clone(),
1182                    vec![Point::new(4, 0)..Point::new(4, 0)],
1183                    cx,
1184                )
1185            })
1186            .await
1187            .unwrap();
1188        cx.run_until_parked();
1189        assert_eq!(
1190            buffer.read_with(cx, |buffer, _| buffer.text()),
1191            "abc\ndE\nXYZf\nghi\njkl\nmnO"
1192        );
1193        assert_eq!(
1194            unreviewed_hunks(&action_log, cx),
1195            vec![(
1196                buffer.clone(),
1197                vec![
1198                    HunkStatus {
1199                        range: Point::new(1, 0)..Point::new(3, 0),
1200                        diff_status: DiffHunkStatusKind::Modified,
1201                        old_text: "def\n".into(),
1202                    },
1203                    HunkStatus {
1204                        range: Point::new(5, 0)..Point::new(5, 3),
1205                        diff_status: DiffHunkStatusKind::Modified,
1206                        old_text: "mno".into(),
1207                    }
1208                ],
1209            )]
1210        );
1211
1212        action_log
1213            .update(cx, |log, cx| {
1214                log.reject_edits_in_ranges(
1215                    buffer.clone(),
1216                    vec![Point::new(0, 0)..Point::new(1, 0)],
1217                    cx,
1218                )
1219            })
1220            .await
1221            .unwrap();
1222        cx.run_until_parked();
1223        assert_eq!(
1224            buffer.read_with(cx, |buffer, _| buffer.text()),
1225            "abc\ndef\nghi\njkl\nmnO"
1226        );
1227        assert_eq!(
1228            unreviewed_hunks(&action_log, cx),
1229            vec![(
1230                buffer.clone(),
1231                vec![HunkStatus {
1232                    range: Point::new(4, 0)..Point::new(4, 3),
1233                    diff_status: DiffHunkStatusKind::Modified,
1234                    old_text: "mno".into(),
1235                }],
1236            )]
1237        );
1238
1239        action_log
1240            .update(cx, |log, cx| {
1241                log.reject_edits_in_ranges(
1242                    buffer.clone(),
1243                    vec![Point::new(4, 0)..Point::new(4, 0)],
1244                    cx,
1245                )
1246            })
1247            .await
1248            .unwrap();
1249        cx.run_until_parked();
1250        assert_eq!(
1251            buffer.read_with(cx, |buffer, _| buffer.text()),
1252            "abc\ndef\nghi\njkl\nmno"
1253        );
1254        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1255    }
1256
1257    #[gpui::test(iterations = 10)]
1258    async fn test_reject_multiple_edits(cx: &mut TestAppContext) {
1259        init_test(cx);
1260
1261        let fs = FakeFs::new(cx.executor());
1262        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
1263            .await;
1264        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1265        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1266        let file_path = project
1267            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1268            .unwrap();
1269        let buffer = project
1270            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1271            .await
1272            .unwrap();
1273
1274        cx.update(|cx| {
1275            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1276            buffer.update(cx, |buffer, cx| {
1277                buffer
1278                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
1279                    .unwrap()
1280            });
1281            buffer.update(cx, |buffer, cx| {
1282                buffer
1283                    .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
1284                    .unwrap()
1285            });
1286            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1287        });
1288        cx.run_until_parked();
1289        assert_eq!(
1290            buffer.read_with(cx, |buffer, _| buffer.text()),
1291            "abc\ndE\nXYZf\nghi\njkl\nmnO"
1292        );
1293        assert_eq!(
1294            unreviewed_hunks(&action_log, cx),
1295            vec![(
1296                buffer.clone(),
1297                vec![
1298                    HunkStatus {
1299                        range: Point::new(1, 0)..Point::new(3, 0),
1300                        diff_status: DiffHunkStatusKind::Modified,
1301                        old_text: "def\n".into(),
1302                    },
1303                    HunkStatus {
1304                        range: Point::new(5, 0)..Point::new(5, 3),
1305                        diff_status: DiffHunkStatusKind::Modified,
1306                        old_text: "mno".into(),
1307                    }
1308                ],
1309            )]
1310        );
1311
1312        action_log.update(cx, |log, cx| {
1313            let range_1 = buffer.read(cx).anchor_before(Point::new(0, 0))
1314                ..buffer.read(cx).anchor_before(Point::new(1, 0));
1315            let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0))
1316                ..buffer.read(cx).anchor_before(Point::new(5, 3));
1317
1318            log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], cx)
1319                .detach();
1320            assert_eq!(
1321                buffer.read_with(cx, |buffer, _| buffer.text()),
1322                "abc\ndef\nghi\njkl\nmno"
1323            );
1324        });
1325        cx.run_until_parked();
1326        assert_eq!(
1327            buffer.read_with(cx, |buffer, _| buffer.text()),
1328            "abc\ndef\nghi\njkl\nmno"
1329        );
1330        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1331    }
1332
1333    #[gpui::test(iterations = 10)]
1334    async fn test_reject_deleted_file(cx: &mut TestAppContext) {
1335        init_test(cx);
1336
1337        let fs = FakeFs::new(cx.executor());
1338        fs.insert_tree(path!("/dir"), json!({"file": "content"}))
1339            .await;
1340        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1341        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1342        let file_path = project
1343            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1344            .unwrap();
1345        let buffer = project
1346            .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
1347            .await
1348            .unwrap();
1349
1350        cx.update(|cx| {
1351            action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx));
1352        });
1353        project
1354            .update(cx, |project, cx| {
1355                project.delete_file(file_path.clone(), false, cx)
1356            })
1357            .unwrap()
1358            .await
1359            .unwrap();
1360        cx.run_until_parked();
1361        assert!(!fs.is_file(path!("/dir/file").as_ref()).await);
1362        assert_eq!(
1363            unreviewed_hunks(&action_log, cx),
1364            vec![(
1365                buffer.clone(),
1366                vec![HunkStatus {
1367                    range: Point::new(0, 0)..Point::new(0, 0),
1368                    diff_status: DiffHunkStatusKind::Deleted,
1369                    old_text: "content".into(),
1370                }]
1371            )]
1372        );
1373
1374        action_log
1375            .update(cx, |log, cx| {
1376                log.reject_edits_in_ranges(
1377                    buffer.clone(),
1378                    vec![Point::new(0, 0)..Point::new(0, 0)],
1379                    cx,
1380                )
1381            })
1382            .await
1383            .unwrap();
1384        cx.run_until_parked();
1385        assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "content");
1386        assert!(fs.is_file(path!("/dir/file").as_ref()).await);
1387        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1388    }
1389
1390    #[gpui::test(iterations = 10)]
1391    async fn test_reject_created_file(cx: &mut TestAppContext) {
1392        init_test(cx);
1393
1394        let fs = FakeFs::new(cx.executor());
1395        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1396        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1397        let file_path = project
1398            .read_with(cx, |project, cx| {
1399                project.find_project_path("dir/new_file", cx)
1400            })
1401            .unwrap();
1402
1403        let buffer = project
1404            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1405            .await
1406            .unwrap();
1407        cx.update(|cx| {
1408            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
1409            buffer.update(cx, |buffer, cx| buffer.set_text("content", cx));
1410            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1411        });
1412        project
1413            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1414            .await
1415            .unwrap();
1416        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
1417        cx.run_until_parked();
1418        assert_eq!(
1419            unreviewed_hunks(&action_log, cx),
1420            vec![(
1421                buffer.clone(),
1422                vec![HunkStatus {
1423                    range: Point::new(0, 0)..Point::new(0, 7),
1424                    diff_status: DiffHunkStatusKind::Added,
1425                    old_text: "".into(),
1426                }],
1427            )]
1428        );
1429
1430        action_log
1431            .update(cx, |log, cx| {
1432                log.reject_edits_in_ranges(
1433                    buffer.clone(),
1434                    vec![Point::new(0, 0)..Point::new(0, 11)],
1435                    cx,
1436                )
1437            })
1438            .await
1439            .unwrap();
1440        cx.run_until_parked();
1441        assert!(!fs.is_file(path!("/dir/new_file").as_ref()).await);
1442        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1443    }
1444
1445    #[gpui::test(iterations = 100)]
1446    async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
1447        init_test(cx);
1448
1449        let operations = env::var("OPERATIONS")
1450            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
1451            .unwrap_or(20);
1452
1453        let text = RandomCharIter::new(&mut rng).take(50).collect::<String>();
1454        let fs = FakeFs::new(cx.executor());
1455        fs.insert_tree(path!("/dir"), json!({"file": text})).await;
1456        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1457        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1458        let file_path = project
1459            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1460            .unwrap();
1461        let buffer = project
1462            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1463            .await
1464            .unwrap();
1465
1466        action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1467
1468        for _ in 0..operations {
1469            match rng.gen_range(0..100) {
1470                0..25 => {
1471                    action_log.update(cx, |log, cx| {
1472                        let range = buffer.read(cx).random_byte_range(0, &mut rng);
1473                        log::info!("keeping edits in range {:?}", range);
1474                        log.keep_edits_in_range(buffer.clone(), range, cx)
1475                    });
1476                }
1477                25..50 => {
1478                    action_log
1479                        .update(cx, |log, cx| {
1480                            let range = buffer.read(cx).random_byte_range(0, &mut rng);
1481                            log::info!("rejecting edits in range {:?}", range);
1482                            log.reject_edits_in_ranges(buffer.clone(), vec![range], cx)
1483                        })
1484                        .await
1485                        .unwrap();
1486                }
1487                _ => {
1488                    let is_agent_change = rng.gen_bool(0.5);
1489                    if is_agent_change {
1490                        log::info!("agent edit");
1491                    } else {
1492                        log::info!("user edit");
1493                    }
1494                    cx.update(|cx| {
1495                        buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
1496                        if is_agent_change {
1497                            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1498                        }
1499                    });
1500                }
1501            }
1502
1503            if rng.gen_bool(0.2) {
1504                quiesce(&action_log, &buffer, cx);
1505            }
1506        }
1507
1508        quiesce(&action_log, &buffer, cx);
1509
1510        fn quiesce(
1511            action_log: &Entity<ActionLog>,
1512            buffer: &Entity<Buffer>,
1513            cx: &mut TestAppContext,
1514        ) {
1515            log::info!("quiescing...");
1516            cx.run_until_parked();
1517            action_log.update(cx, |log, cx| {
1518                let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap();
1519                let mut old_text = tracked_buffer.base_text.clone();
1520                let new_text = buffer.read(cx).as_rope();
1521                for edit in tracked_buffer.unreviewed_changes.edits() {
1522                    let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
1523                    let old_end = old_text.point_to_offset(cmp::min(
1524                        Point::new(edit.new.start + edit.old_len(), 0),
1525                        old_text.max_point(),
1526                    ));
1527                    old_text.replace(
1528                        old_start..old_end,
1529                        &new_text.slice_rows(edit.new.clone()).to_string(),
1530                    );
1531                }
1532                pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
1533            })
1534        }
1535    }
1536
1537    #[derive(Debug, Clone, PartialEq, Eq)]
1538    struct HunkStatus {
1539        range: Range<Point>,
1540        diff_status: DiffHunkStatusKind,
1541        old_text: String,
1542    }
1543
1544    fn unreviewed_hunks(
1545        action_log: &Entity<ActionLog>,
1546        cx: &TestAppContext,
1547    ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
1548        cx.read(|cx| {
1549            action_log
1550                .read(cx)
1551                .changed_buffers(cx)
1552                .into_iter()
1553                .map(|(buffer, diff)| {
1554                    let snapshot = buffer.read(cx).snapshot();
1555                    (
1556                        buffer,
1557                        diff.read(cx)
1558                            .hunks(&snapshot, cx)
1559                            .map(|hunk| HunkStatus {
1560                                diff_status: hunk.status().kind,
1561                                range: hunk.range,
1562                                old_text: diff
1563                                    .read(cx)
1564                                    .base_text()
1565                                    .text_for_range(hunk.diff_base_byte_range)
1566                                    .collect(),
1567                            })
1568                            .collect(),
1569                    )
1570                })
1571                .collect()
1572        })
1573    }
1574}