action_log.rs

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