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