action_log.rs

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