action_log.rs

   1use anyhow::{Context as _, Result};
   2use buffer_diff::BufferDiff;
   3use clock;
   4use collections::{BTreeMap, HashMap};
   5use fs::MTime;
   6use futures::{FutureExt, StreamExt, channel::mpsc};
   7use gpui::{
   8    App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
   9};
  10use language::{Anchor, Buffer, BufferEvent, Point, ToOffset, ToPoint};
  11use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
  12use std::{
  13    cmp,
  14    ops::Range,
  15    path::{Path, PathBuf},
  16    sync::Arc,
  17};
  18use text::{Edit, Patch, Rope};
  19use util::{RangeExt, ResultExt as _};
  20
  21/// Stores undo information for a single buffer's rejected edits
  22#[derive(Clone)]
  23pub struct PerBufferUndo {
  24    pub buffer: WeakEntity<Buffer>,
  25    pub edits_to_restore: Vec<(Range<Anchor>, String)>,
  26    pub status: UndoBufferStatus,
  27}
  28
  29/// Tracks the buffer status for undo purposes
  30#[derive(Clone, Debug)]
  31pub enum UndoBufferStatus {
  32    Modified,
  33    /// Buffer was created by the agent.
  34    /// - `had_existing_content: true` - Agent overwrote an existing file. On reject, the
  35    ///   original content was restored. Undo is supported: we restore the agent's content.
  36    /// - `had_existing_content: false` - Agent created a new file that didn't exist before.
  37    ///   On reject, the file was deleted. Undo is NOT currently supported (would require
  38    ///   recreating the file). Future TODO.
  39    Created {
  40        had_existing_content: bool,
  41    },
  42}
  43
  44/// Stores undo information for the most recent reject operation
  45#[derive(Clone)]
  46pub struct LastRejectUndo {
  47    /// Per-buffer undo information
  48    pub buffers: Vec<PerBufferUndo>,
  49}
  50
  51/// Tracks actions performed by tools in a thread
  52pub struct ActionLog {
  53    /// Buffers that we want to notify the model about when they change.
  54    tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
  55    /// The project this action log is associated with
  56    project: Entity<Project>,
  57    /// An action log to forward all public methods to
  58    /// Useful in cases like subagents, where we want to track individual diffs for this subagent,
  59    /// but also want to associate the reads/writes with a parent review experience
  60    linked_action_log: Option<Entity<ActionLog>>,
  61    /// Stores undo information for the most recent reject operation
  62    last_reject_undo: Option<LastRejectUndo>,
  63    /// Tracks the last time files were read by the agent, to detect external modifications
  64    file_read_times: HashMap<PathBuf, MTime>,
  65}
  66
  67impl ActionLog {
  68    /// Creates a new, empty action log associated with the given project.
  69    pub fn new(project: Entity<Project>) -> Self {
  70        Self {
  71            tracked_buffers: BTreeMap::default(),
  72            project,
  73            linked_action_log: None,
  74            last_reject_undo: None,
  75            file_read_times: HashMap::default(),
  76        }
  77    }
  78
  79    pub fn with_linked_action_log(mut self, linked_action_log: Entity<ActionLog>) -> Self {
  80        self.linked_action_log = Some(linked_action_log);
  81        self
  82    }
  83
  84    pub fn project(&self) -> &Entity<Project> {
  85        &self.project
  86    }
  87
  88    pub fn file_read_time(&self, path: &Path) -> Option<MTime> {
  89        self.file_read_times.get(path).copied()
  90    }
  91
  92    fn update_file_read_time(&mut self, buffer: &Entity<Buffer>, cx: &App) {
  93        let buffer = buffer.read(cx);
  94        if let Some(file) = buffer.file() {
  95            if let Some(local_file) = file.as_local() {
  96                if let Some(mtime) = file.disk_state().mtime() {
  97                    let abs_path = local_file.abs_path(cx);
  98                    self.file_read_times.insert(abs_path, mtime);
  99                }
 100            }
 101        }
 102    }
 103
 104    fn remove_file_read_time(&mut self, buffer: &Entity<Buffer>, cx: &App) {
 105        let buffer = buffer.read(cx);
 106        if let Some(file) = buffer.file() {
 107            if let Some(local_file) = file.as_local() {
 108                let abs_path = local_file.abs_path(cx);
 109                self.file_read_times.remove(&abs_path);
 110            }
 111        }
 112    }
 113
 114    fn track_buffer_internal(
 115        &mut self,
 116        buffer: Entity<Buffer>,
 117        is_created: bool,
 118        cx: &mut Context<Self>,
 119    ) -> &mut TrackedBuffer {
 120        let status = if is_created {
 121            if let Some(tracked) = self.tracked_buffers.remove(&buffer) {
 122                match tracked.status {
 123                    TrackedBufferStatus::Created {
 124                        existing_file_content,
 125                    } => TrackedBufferStatus::Created {
 126                        existing_file_content,
 127                    },
 128                    TrackedBufferStatus::Modified | TrackedBufferStatus::Deleted => {
 129                        TrackedBufferStatus::Created {
 130                            existing_file_content: Some(tracked.diff_base),
 131                        }
 132                    }
 133                }
 134            } else if buffer
 135                .read(cx)
 136                .file()
 137                .is_some_and(|file| file.disk_state().exists())
 138            {
 139                TrackedBufferStatus::Created {
 140                    existing_file_content: Some(buffer.read(cx).as_rope().clone()),
 141                }
 142            } else {
 143                TrackedBufferStatus::Created {
 144                    existing_file_content: None,
 145                }
 146            }
 147        } else {
 148            TrackedBufferStatus::Modified
 149        };
 150
 151        let tracked_buffer = self
 152            .tracked_buffers
 153            .entry(buffer.clone())
 154            .or_insert_with(|| {
 155                let open_lsp_handle = self.project.update(cx, |project, cx| {
 156                    project.register_buffer_with_language_servers(&buffer, cx)
 157                });
 158
 159                let text_snapshot = buffer.read(cx).text_snapshot();
 160                let language = buffer.read(cx).language().cloned();
 161                let language_registry = buffer.read(cx).language_registry();
 162                let diff = cx.new(|cx| {
 163                    let mut diff = BufferDiff::new(&text_snapshot, cx);
 164                    diff.language_changed(language, language_registry, cx);
 165                    diff
 166                });
 167                let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
 168                let diff_base;
 169                let unreviewed_edits;
 170                if is_created {
 171                    diff_base = Rope::default();
 172                    unreviewed_edits = Patch::new(vec![Edit {
 173                        old: 0..1,
 174                        new: 0..text_snapshot.max_point().row + 1,
 175                    }])
 176                } else {
 177                    diff_base = buffer.read(cx).as_rope().clone();
 178                    unreviewed_edits = Patch::default();
 179                }
 180                TrackedBuffer {
 181                    buffer: buffer.clone(),
 182                    diff_base,
 183                    unreviewed_edits,
 184                    snapshot: text_snapshot,
 185                    status,
 186                    mode: TrackedBufferMode::Normal,
 187                    expected_external_edit: None,
 188                    version: buffer.read(cx).version(),
 189                    diff,
 190                    diff_update: diff_update_tx,
 191                    _open_lsp_handle: open_lsp_handle,
 192                    _maintain_diff: cx.spawn({
 193                        let buffer = buffer.clone();
 194                        async move |this, cx| {
 195                            Self::maintain_diff(this, buffer, diff_update_rx, cx)
 196                                .await
 197                                .ok();
 198                        }
 199                    }),
 200                    _subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
 201                }
 202            });
 203        tracked_buffer.version = buffer.read(cx).version();
 204        tracked_buffer
 205    }
 206
 207    fn handle_buffer_event(
 208        &mut self,
 209        buffer: Entity<Buffer>,
 210        event: &BufferEvent,
 211        cx: &mut Context<Self>,
 212    ) {
 213        if self.handle_expected_external_edit_event(buffer.clone(), event, cx) {
 214            return;
 215        }
 216
 217        match event {
 218            BufferEvent::Edited { .. } => {
 219                let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 220                    return;
 221                };
 222                let buffer_version = buffer.read(cx).version();
 223                if !buffer_version.changed_since(&tracked_buffer.version) {
 224                    return;
 225                }
 226                self.handle_buffer_edited(buffer, cx);
 227            }
 228            BufferEvent::FileHandleChanged => {
 229                self.handle_buffer_file_changed(buffer, cx);
 230            }
 231            _ => {}
 232        };
 233    }
 234
 235    fn handle_buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 236        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 237            return;
 238        };
 239        tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
 240    }
 241
 242    fn handle_buffer_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 243        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 244            return;
 245        };
 246
 247        match tracked_buffer.status {
 248            TrackedBufferStatus::Created { .. } | TrackedBufferStatus::Modified => {
 249                if buffer
 250                    .read(cx)
 251                    .file()
 252                    .is_some_and(|file| file.disk_state().is_deleted())
 253                {
 254                    // If the buffer had been edited by a tool, but it got
 255                    // deleted externally, we want to stop tracking it.
 256                    self.tracked_buffers.remove(&buffer);
 257                }
 258                cx.notify();
 259            }
 260            TrackedBufferStatus::Deleted => {
 261                if buffer
 262                    .read(cx)
 263                    .file()
 264                    .is_some_and(|file| !file.disk_state().is_deleted())
 265                {
 266                    // If the buffer had been deleted by a tool, but it got
 267                    // resurrected externally, we want to clear the edits we
 268                    // were tracking and reset the buffer's state.
 269                    self.tracked_buffers.remove(&buffer);
 270                    self.track_buffer_internal(buffer, false, cx);
 271                }
 272                cx.notify();
 273            }
 274        }
 275    }
 276
 277    fn handle_expected_external_edit_event(
 278        &mut self,
 279        buffer: Entity<Buffer>,
 280        event: &BufferEvent,
 281        cx: &mut Context<Self>,
 282    ) -> bool {
 283        let Some(expected_external_edit) = self
 284            .tracked_buffers
 285            .get(&buffer)
 286            .and_then(|tracked_buffer| tracked_buffer.expected_external_edit.clone())
 287        else {
 288            return false;
 289        };
 290
 291        if expected_external_edit.is_disqualified {
 292            return false;
 293        }
 294
 295        match event {
 296            BufferEvent::Saved
 297                if (expected_external_edit.observed_external_file_change
 298                    || expected_external_edit.armed_explicit_reload)
 299                    && !expected_external_edit.has_attributed_change =>
 300            {
 301                self.mark_expected_external_edit_disqualified(&buffer);
 302                true
 303            }
 304            BufferEvent::Edited { is_local: true } => {
 305                if expected_external_edit.pending_delete {
 306                    let (is_deleted, is_empty) = buffer.read_with(cx, |buffer, _| {
 307                        (
 308                            buffer
 309                                .file()
 310                                .is_some_and(|file| file.disk_state().is_deleted()),
 311                            buffer.text().is_empty(),
 312                        )
 313                    });
 314
 315                    if is_deleted && is_empty {
 316                        self.apply_expected_external_delete_local(buffer, cx);
 317                        return true;
 318                    }
 319                }
 320
 321                // Reload applies its text changes through ordinary local edit events before
 322                // emitting `Reloaded`, so an explicitly armed reload must suppress those edits
 323                // to preserve the pre-reload baseline for attribution.
 324                expected_external_edit.observed_external_file_change
 325                    || expected_external_edit.armed_explicit_reload
 326            }
 327            BufferEvent::FileHandleChanged => {
 328                let (is_deleted, is_empty, is_dirty) = buffer.read_with(cx, |buffer, _| {
 329                    (
 330                        buffer
 331                            .file()
 332                            .is_some_and(|file| file.disk_state().is_deleted()),
 333                        buffer.text().is_empty(),
 334                        buffer.is_dirty(),
 335                    )
 336                });
 337
 338                if let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) {
 339                    if let Some(expected_external_edit) =
 340                        tracked_buffer.expected_external_edit.as_mut()
 341                    {
 342                        if !is_dirty || is_deleted {
 343                            expected_external_edit.observed_external_file_change = true;
 344                            expected_external_edit.armed_explicit_reload = false;
 345                        }
 346                        // Non-delete external changes against dirty buffers stay unsupported for now.
 347                        // We do not mark them as observed here, so they are not automatically
 348                        // remembered for attribution once the buffer becomes clean. Later
 349                        // attribution only happens after a subsequent clean file change or an
 350                        // explicitly armed reload, which keeps conflicted reloads and local-save
 351                        // noise from becoming agent edits.
 352                        expected_external_edit.pending_delete = is_deleted;
 353                    }
 354                }
 355
 356                if is_deleted {
 357                    if is_empty {
 358                        self.apply_expected_external_delete_local(buffer, cx);
 359                    } else if self.linked_action_log.is_none() {
 360                        let buffer = buffer.clone();
 361                        cx.defer(move |cx| {
 362                            buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
 363                        });
 364                    }
 365                }
 366
 367                true
 368            }
 369            BufferEvent::Reloaded
 370                if (expected_external_edit.observed_external_file_change
 371                    || expected_external_edit.armed_explicit_reload)
 372                    && !expected_external_edit.pending_delete =>
 373            {
 374                if self.expected_external_edit_has_meaningful_change(&buffer, cx) {
 375                    self.apply_expected_external_reload_local(buffer, cx);
 376                } else if let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) {
 377                    if let Some(expected_external_edit) =
 378                        tracked_buffer.expected_external_edit.as_mut()
 379                    {
 380                        expected_external_edit.observed_external_file_change = false;
 381                        expected_external_edit.armed_explicit_reload = false;
 382                        expected_external_edit.pending_delete = false;
 383                    }
 384                }
 385
 386                true
 387            }
 388            _ => false,
 389        }
 390    }
 391
 392    fn mark_expected_external_edit_disqualified(&mut self, buffer: &Entity<Buffer>) {
 393        let Some(tracked_buffer) = self.tracked_buffers.get_mut(buffer) else {
 394            return;
 395        };
 396        let Some(expected_external_edit) = tracked_buffer.expected_external_edit.as_mut() else {
 397            return;
 398        };
 399
 400        expected_external_edit.is_disqualified = true;
 401        expected_external_edit.observed_external_file_change = false;
 402        expected_external_edit.armed_explicit_reload = false;
 403        expected_external_edit.pending_delete = false;
 404    }
 405
 406    fn expected_external_edit_has_meaningful_change(
 407        &self,
 408        buffer: &Entity<Buffer>,
 409        cx: &App,
 410    ) -> bool {
 411        let Some(tracked_buffer) = self.tracked_buffers.get(buffer) else {
 412            return false;
 413        };
 414        let Some(expected_external_edit) = tracked_buffer.expected_external_edit.as_ref() else {
 415            return false;
 416        };
 417
 418        let (current_snapshot, current_exists) = buffer.read_with(cx, |buffer, _| {
 419            (
 420                buffer.text_snapshot(),
 421                buffer.file().is_some_and(|file| file.disk_state().exists()),
 422            )
 423        });
 424
 425        if !expected_external_edit.initial_exists_on_disk {
 426            current_exists || current_snapshot.text() != tracked_buffer.snapshot.text()
 427        } else {
 428            !current_exists || current_snapshot.text() != tracked_buffer.snapshot.text()
 429        }
 430    }
 431
 432    fn apply_expected_external_reload_local(
 433        &mut self,
 434        buffer: Entity<Buffer>,
 435        cx: &mut Context<Self>,
 436    ) {
 437        let current_version = buffer.read(cx).version();
 438        let (record_file_read_time, initial_exists_on_disk) = self
 439            .tracked_buffers
 440            .get(&buffer)
 441            .and_then(|tracked_buffer| {
 442                tracked_buffer
 443                    .expected_external_edit
 444                    .as_ref()
 445                    .map(|expected_external_edit| {
 446                        (
 447                            expected_external_edit.record_file_read_time_source_count > 0,
 448                            expected_external_edit.initial_exists_on_disk,
 449                        )
 450                    })
 451            })
 452            .unwrap_or((false, true));
 453
 454        if record_file_read_time {
 455            self.update_file_read_time(&buffer, cx);
 456        }
 457
 458        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 459            return;
 460        };
 461        let Some(expected_external_edit) = tracked_buffer.expected_external_edit.as_mut() else {
 462            return;
 463        };
 464
 465        expected_external_edit.has_attributed_change = true;
 466        expected_external_edit.observed_external_file_change = false;
 467        expected_external_edit.armed_explicit_reload = false;
 468        expected_external_edit.pending_delete = false;
 469        tracked_buffer.mode = TrackedBufferMode::Normal;
 470
 471        if !initial_exists_on_disk {
 472            let existing_file_content = if tracked_buffer.diff_base.len() == 0 {
 473                None
 474            } else {
 475                Some(tracked_buffer.diff_base.clone())
 476            };
 477            tracked_buffer.status = TrackedBufferStatus::Created {
 478                existing_file_content,
 479            };
 480        } else {
 481            tracked_buffer.status = TrackedBufferStatus::Modified;
 482        }
 483
 484        tracked_buffer.version = current_version;
 485        tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
 486    }
 487
 488    fn apply_expected_external_delete_local(
 489        &mut self,
 490        buffer: Entity<Buffer>,
 491        cx: &mut Context<Self>,
 492    ) {
 493        let current_version = buffer.read(cx).version();
 494        let remove_file_read_time = self
 495            .tracked_buffers
 496            .get(&buffer)
 497            .and_then(|tracked_buffer| {
 498                tracked_buffer
 499                    .expected_external_edit
 500                    .as_ref()
 501                    .map(|expected_external_edit| {
 502                        expected_external_edit.record_file_read_time_source_count > 0
 503                    })
 504            })
 505            .unwrap_or(false);
 506
 507        if remove_file_read_time {
 508            self.remove_file_read_time(&buffer, cx);
 509        }
 510
 511        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 512            return;
 513        };
 514        let Some(expected_external_edit) = tracked_buffer.expected_external_edit.as_mut() else {
 515            return;
 516        };
 517
 518        expected_external_edit.has_attributed_change = true;
 519        expected_external_edit.observed_external_file_change = false;
 520        expected_external_edit.armed_explicit_reload = false;
 521        expected_external_edit.pending_delete = false;
 522        tracked_buffer.mode = TrackedBufferMode::Normal;
 523        tracked_buffer.status = TrackedBufferStatus::Deleted;
 524        tracked_buffer.version = current_version;
 525        tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
 526    }
 527
 528    async fn maintain_diff(
 529        this: WeakEntity<Self>,
 530        buffer: Entity<Buffer>,
 531        mut buffer_updates: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>,
 532        cx: &mut AsyncApp,
 533    ) -> Result<()> {
 534        let git_store = this.read_with(cx, |this, cx| this.project.read(cx).git_store().clone())?;
 535        let git_diff = this
 536            .update(cx, |this, cx| {
 537                this.project.update(cx, |project, cx| {
 538                    project.open_uncommitted_diff(buffer.clone(), cx)
 539                })
 540            })?
 541            .await
 542            .ok();
 543        let buffer_repo = git_store.read_with(cx, |git_store, cx| {
 544            git_store.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
 545        });
 546
 547        let (mut git_diff_updates_tx, mut git_diff_updates_rx) = watch::channel(());
 548        let _repo_subscription =
 549            if let Some((git_diff, (buffer_repo, _))) = git_diff.as_ref().zip(buffer_repo) {
 550                cx.update(|cx| {
 551                    let mut old_head = buffer_repo.read(cx).head_commit.clone();
 552                    Some(cx.subscribe(git_diff, move |_, event, cx| {
 553                        if let buffer_diff::BufferDiffEvent::DiffChanged { .. } = event {
 554                            let new_head = buffer_repo.read(cx).head_commit.clone();
 555                            if new_head != old_head {
 556                                old_head = new_head;
 557                                git_diff_updates_tx.send(()).ok();
 558                            }
 559                        }
 560                    }))
 561                })
 562            } else {
 563                None
 564            };
 565
 566        loop {
 567            futures::select_biased! {
 568                buffer_update = buffer_updates.next() => {
 569                    if let Some((author, buffer_snapshot)) = buffer_update {
 570                        Self::track_edits(&this, &buffer, author, buffer_snapshot, cx).await?;
 571                    } else {
 572                        break;
 573                    }
 574                }
 575                _ = git_diff_updates_rx.changed().fuse() => {
 576                    if let Some(git_diff) = git_diff.as_ref() {
 577                        Self::keep_committed_edits(&this, &buffer, git_diff, cx).await?;
 578                    }
 579                }
 580            }
 581        }
 582
 583        Ok(())
 584    }
 585
 586    async fn track_edits(
 587        this: &WeakEntity<ActionLog>,
 588        buffer: &Entity<Buffer>,
 589        author: ChangeAuthor,
 590        buffer_snapshot: text::BufferSnapshot,
 591        cx: &mut AsyncApp,
 592    ) -> Result<()> {
 593        let rebase = this.update(cx, |this, cx| {
 594            let tracked_buffer = this
 595                .tracked_buffers
 596                .get_mut(buffer)
 597                .context("buffer not tracked")?;
 598
 599            let rebase = cx.background_spawn({
 600                let mut base_text = tracked_buffer.diff_base.clone();
 601                let old_snapshot = tracked_buffer.snapshot.clone();
 602                let new_snapshot = buffer_snapshot.clone();
 603                let unreviewed_edits = tracked_buffer.unreviewed_edits.clone();
 604                let edits = diff_snapshots(&old_snapshot, &new_snapshot);
 605                async move {
 606                    if let ChangeAuthor::User = author {
 607                        apply_non_conflicting_edits(
 608                            &unreviewed_edits,
 609                            edits,
 610                            &mut base_text,
 611                            new_snapshot.as_rope(),
 612                        );
 613                    }
 614
 615                    (Arc::from(base_text.to_string().as_str()), base_text)
 616                }
 617            });
 618
 619            anyhow::Ok(rebase)
 620        })??;
 621        let (new_base_text, new_diff_base) = rebase.await;
 622
 623        Self::update_diff(
 624            this,
 625            buffer,
 626            buffer_snapshot,
 627            new_base_text,
 628            new_diff_base,
 629            cx,
 630        )
 631        .await
 632    }
 633
 634    async fn keep_committed_edits(
 635        this: &WeakEntity<ActionLog>,
 636        buffer: &Entity<Buffer>,
 637        git_diff: &Entity<BufferDiff>,
 638        cx: &mut AsyncApp,
 639    ) -> Result<()> {
 640        let buffer_snapshot = this.read_with(cx, |this, _cx| {
 641            let tracked_buffer = this
 642                .tracked_buffers
 643                .get(buffer)
 644                .context("buffer not tracked")?;
 645            anyhow::Ok(tracked_buffer.snapshot.clone())
 646        })??;
 647        let (new_base_text, new_diff_base) = this
 648            .read_with(cx, |this, cx| {
 649                let tracked_buffer = this
 650                    .tracked_buffers
 651                    .get(buffer)
 652                    .context("buffer not tracked")?;
 653                let old_unreviewed_edits = tracked_buffer.unreviewed_edits.clone();
 654                let agent_diff_base = tracked_buffer.diff_base.clone();
 655                let git_diff_base = git_diff.read(cx).base_text(cx).as_rope().clone();
 656                let buffer_text = tracked_buffer.snapshot.as_rope().clone();
 657                anyhow::Ok(cx.background_spawn(async move {
 658                    let mut old_unreviewed_edits = old_unreviewed_edits.into_iter().peekable();
 659                    let committed_edits = language::line_diff(
 660                        &agent_diff_base.to_string(),
 661                        &git_diff_base.to_string(),
 662                    )
 663                    .into_iter()
 664                    .map(|(old, new)| Edit { old, new });
 665
 666                    let mut new_agent_diff_base = agent_diff_base.clone();
 667                    let mut row_delta = 0i32;
 668                    for committed in committed_edits {
 669                        while let Some(unreviewed) = old_unreviewed_edits.peek() {
 670                            // If the committed edit matches the unreviewed
 671                            // edit, assume the user wants to keep it.
 672                            if committed.old == unreviewed.old {
 673                                let unreviewed_new =
 674                                    buffer_text.slice_rows(unreviewed.new.clone()).to_string();
 675                                let committed_new =
 676                                    git_diff_base.slice_rows(committed.new.clone()).to_string();
 677                                if unreviewed_new == committed_new {
 678                                    let old_byte_start =
 679                                        new_agent_diff_base.point_to_offset(Point::new(
 680                                            (unreviewed.old.start as i32 + row_delta) as u32,
 681                                            0,
 682                                        ));
 683                                    let old_byte_end =
 684                                        new_agent_diff_base.point_to_offset(cmp::min(
 685                                            Point::new(
 686                                                (unreviewed.old.end as i32 + row_delta) as u32,
 687                                                0,
 688                                            ),
 689                                            new_agent_diff_base.max_point(),
 690                                        ));
 691                                    new_agent_diff_base
 692                                        .replace(old_byte_start..old_byte_end, &unreviewed_new);
 693                                    row_delta +=
 694                                        unreviewed.new_len() as i32 - unreviewed.old_len() as i32;
 695                                }
 696                            } else if unreviewed.old.start >= committed.old.end {
 697                                break;
 698                            }
 699
 700                            old_unreviewed_edits.next().unwrap();
 701                        }
 702                    }
 703
 704                    (
 705                        Arc::from(new_agent_diff_base.to_string().as_str()),
 706                        new_agent_diff_base,
 707                    )
 708                }))
 709            })??
 710            .await;
 711
 712        Self::update_diff(
 713            this,
 714            buffer,
 715            buffer_snapshot,
 716            new_base_text,
 717            new_diff_base,
 718            cx,
 719        )
 720        .await
 721    }
 722
 723    async fn update_diff(
 724        this: &WeakEntity<ActionLog>,
 725        buffer: &Entity<Buffer>,
 726        buffer_snapshot: text::BufferSnapshot,
 727        new_base_text: Arc<str>,
 728        new_diff_base: Rope,
 729        cx: &mut AsyncApp,
 730    ) -> Result<()> {
 731        let (diff, language) = this.read_with(cx, |this, cx| {
 732            let tracked_buffer = this
 733                .tracked_buffers
 734                .get(buffer)
 735                .context("buffer not tracked")?;
 736            anyhow::Ok((
 737                tracked_buffer.diff.clone(),
 738                buffer.read(cx).language().cloned(),
 739            ))
 740        })??;
 741        let update = diff
 742            .update(cx, |diff, cx| {
 743                diff.update_diff(
 744                    buffer_snapshot.clone(),
 745                    Some(new_base_text),
 746                    Some(true),
 747                    language,
 748                    cx,
 749                )
 750            })
 751            .await;
 752        diff.update(cx, |diff, cx| {
 753            diff.set_snapshot(update.clone(), &buffer_snapshot, cx)
 754        })
 755        .await;
 756        let diff_snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx));
 757
 758        let unreviewed_edits = cx
 759            .background_spawn({
 760                let buffer_snapshot = buffer_snapshot.clone();
 761                let new_diff_base = new_diff_base.clone();
 762                async move {
 763                    let mut unreviewed_edits = Patch::default();
 764                    for hunk in diff_snapshot.hunks_intersecting_range(
 765                        Anchor::min_for_buffer(buffer_snapshot.remote_id())
 766                            ..Anchor::max_for_buffer(buffer_snapshot.remote_id()),
 767                        &buffer_snapshot,
 768                    ) {
 769                        let old_range = new_diff_base
 770                            .offset_to_point(hunk.diff_base_byte_range.start)
 771                            ..new_diff_base.offset_to_point(hunk.diff_base_byte_range.end);
 772                        let new_range = hunk.range.start..hunk.range.end;
 773                        unreviewed_edits.push(point_to_row_edit(
 774                            Edit {
 775                                old: old_range,
 776                                new: new_range,
 777                            },
 778                            &new_diff_base,
 779                            buffer_snapshot.as_rope(),
 780                        ));
 781                    }
 782                    unreviewed_edits
 783                }
 784            })
 785            .await;
 786        this.update(cx, |this, cx| {
 787            let tracked_buffer = this
 788                .tracked_buffers
 789                .get_mut(buffer)
 790                .context("buffer not tracked")?;
 791            tracked_buffer.diff_base = new_diff_base;
 792            tracked_buffer.snapshot = buffer_snapshot;
 793            tracked_buffer.unreviewed_edits = unreviewed_edits;
 794            cx.notify();
 795            anyhow::Ok(())
 796        })?
 797    }
 798
 799    /// Track a buffer as read by agent, so we can notify the model about user edits.
 800    pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 801        self.buffer_read_impl(buffer, true, cx);
 802    }
 803
 804    fn buffer_read_impl(
 805        &mut self,
 806        buffer: Entity<Buffer>,
 807        record_file_read_time: bool,
 808        cx: &mut Context<Self>,
 809    ) {
 810        if let Some(linked_action_log) = &self.linked_action_log {
 811            // We don't want to share read times since the other agent hasn't read it necessarily
 812            linked_action_log.update(cx, |log, cx| {
 813                log.buffer_read_impl(buffer.clone(), false, cx);
 814            });
 815        }
 816        if record_file_read_time {
 817            self.update_file_read_time(&buffer, cx);
 818        }
 819        self.track_buffer_internal(buffer, false, cx);
 820    }
 821
 822    /// Mark a buffer as created by agent, so we can refresh it in the context
 823    pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 824        self.buffer_created_impl(buffer, true, cx);
 825    }
 826
 827    fn buffer_created_impl(
 828        &mut self,
 829        buffer: Entity<Buffer>,
 830        record_file_read_time: bool,
 831        cx: &mut Context<Self>,
 832    ) {
 833        if let Some(linked_action_log) = &self.linked_action_log {
 834            // We don't want to share read times since the other agent hasn't read it necessarily
 835            linked_action_log.update(cx, |log, cx| {
 836                log.buffer_created_impl(buffer.clone(), false, cx);
 837            });
 838        }
 839        if record_file_read_time {
 840            self.update_file_read_time(&buffer, cx);
 841        }
 842        self.track_buffer_internal(buffer, true, cx);
 843    }
 844
 845    /// Mark a buffer as edited by agent, so we can refresh it in the context
 846    pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 847        self.buffer_edited_impl(buffer, true, cx);
 848    }
 849
 850    fn buffer_edited_impl(
 851        &mut self,
 852        buffer: Entity<Buffer>,
 853        record_file_read_time: bool,
 854        cx: &mut Context<Self>,
 855    ) {
 856        if let Some(linked_action_log) = &self.linked_action_log {
 857            // We don't want to share read times since the other agent hasn't read it necessarily
 858            linked_action_log.update(cx, |log, cx| {
 859                log.buffer_edited_impl(buffer.clone(), false, cx);
 860            });
 861        }
 862        if record_file_read_time {
 863            self.update_file_read_time(&buffer, cx);
 864        }
 865        let new_version = buffer.read(cx).version();
 866        let tracked_buffer = self.track_buffer_internal(buffer, false, cx);
 867        if let TrackedBufferStatus::Deleted = tracked_buffer.status {
 868            tracked_buffer.status = TrackedBufferStatus::Modified;
 869        }
 870
 871        tracked_buffer.version = new_version;
 872        tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
 873    }
 874
 875    fn prime_tracked_buffer_from_snapshot(
 876        &mut self,
 877        buffer: Entity<Buffer>,
 878        baseline_snapshot: text::BufferSnapshot,
 879        status: TrackedBufferStatus,
 880        cx: &mut Context<Self>,
 881    ) {
 882        let version = buffer.read(cx).version();
 883        let diff_base = match &status {
 884            TrackedBufferStatus::Created {
 885                existing_file_content: Some(existing_file_content),
 886            } => existing_file_content.clone(),
 887            TrackedBufferStatus::Created {
 888                existing_file_content: None,
 889            } => Rope::default(),
 890            TrackedBufferStatus::Modified | TrackedBufferStatus::Deleted => {
 891                baseline_snapshot.as_rope().clone()
 892            }
 893        };
 894
 895        let tracked_buffer = self.track_buffer_internal(buffer, false, cx);
 896        tracked_buffer.diff_base = diff_base;
 897        tracked_buffer.snapshot = baseline_snapshot;
 898        tracked_buffer.unreviewed_edits.clear();
 899        tracked_buffer.status = status;
 900        tracked_buffer.version = version;
 901    }
 902
 903    pub fn has_changed_buffer(&self, buffer: &Entity<Buffer>, cx: &App) -> bool {
 904        self.tracked_buffers
 905            .get(buffer)
 906            .is_some_and(|tracked_buffer| tracked_buffer.has_edits(cx))
 907    }
 908
 909    pub fn begin_expected_external_edit(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 910        self.begin_expected_external_edit_impl(buffer, true, cx);
 911    }
 912
 913    fn begin_expected_external_edit_impl(
 914        &mut self,
 915        buffer: Entity<Buffer>,
 916        record_file_read_time: bool,
 917        cx: &mut Context<Self>,
 918    ) {
 919        if let Some(linked_action_log) = &self.linked_action_log {
 920            linked_action_log.update(cx, |log, cx| {
 921                log.begin_expected_external_edit_impl(buffer.clone(), false, cx);
 922            });
 923        }
 924
 925        let initial_exists_on_disk = buffer
 926            .read(cx)
 927            .file()
 928            .is_some_and(|file| file.disk_state().exists());
 929        let had_tracked_buffer = self.tracked_buffers.contains_key(&buffer);
 930
 931        if !had_tracked_buffer {
 932            let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx);
 933            tracked_buffer.mode = TrackedBufferMode::ExpectationOnly;
 934            tracked_buffer.status = TrackedBufferStatus::Modified;
 935            tracked_buffer.diff_base = buffer.read(cx).as_rope().clone();
 936            tracked_buffer.snapshot = buffer.read(cx).text_snapshot();
 937            tracked_buffer.unreviewed_edits.clear();
 938        }
 939
 940        // Reusing an existing tracked buffer must preserve its prior version so stale-buffer
 941        // detection continues to reflect any user edits that predate the expectation.
 942        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 943            return;
 944        };
 945
 946        let expected_external_edit =
 947            tracked_buffer
 948                .expected_external_edit
 949                .get_or_insert_with(|| ExpectedExternalEdit {
 950                    active_source_count: 0,
 951                    record_file_read_time_source_count: 0,
 952                    initial_exists_on_disk,
 953                    observed_external_file_change: false,
 954                    armed_explicit_reload: false,
 955                    has_attributed_change: false,
 956                    pending_delete: false,
 957                    is_disqualified: false,
 958                });
 959        expected_external_edit.active_source_count += 1;
 960        if record_file_read_time {
 961            expected_external_edit.record_file_read_time_source_count += 1;
 962        }
 963    }
 964
 965    pub fn arm_expected_external_reload(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 966        self.arm_expected_external_reload_impl(buffer, true, cx);
 967    }
 968
 969    fn arm_expected_external_reload_impl(
 970        &mut self,
 971        buffer: Entity<Buffer>,
 972        forward_to_linked_action_log: bool,
 973        cx: &mut Context<Self>,
 974    ) {
 975        if forward_to_linked_action_log {
 976            if let Some(linked_action_log) = &self.linked_action_log {
 977                linked_action_log.update(cx, |log, cx| {
 978                    log.arm_expected_external_reload_impl(buffer.clone(), false, cx);
 979                });
 980            }
 981        }
 982
 983        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 984            return;
 985        };
 986        let Some(expected_external_edit) = tracked_buffer.expected_external_edit.as_mut() else {
 987            return;
 988        };
 989        if expected_external_edit.is_disqualified || expected_external_edit.pending_delete {
 990            return;
 991        }
 992
 993        // Explicit reloads can observe on-disk contents before the worktree has delivered
 994        // `FileHandleChanged`, so we arm the next reload for attribution ahead of time.
 995        expected_external_edit.armed_explicit_reload = true;
 996    }
 997
 998    pub fn end_expected_external_edit(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 999        self.end_expected_external_edit_impl(buffer, true, cx);
1000    }
1001
1002    fn end_expected_external_edit_impl(
1003        &mut self,
1004        buffer: Entity<Buffer>,
1005        record_file_read_time: bool,
1006        cx: &mut Context<Self>,
1007    ) {
1008        if let Some(linked_action_log) = &self.linked_action_log {
1009            linked_action_log.update(cx, |log, cx| {
1010                log.end_expected_external_edit_impl(buffer.clone(), false, cx);
1011            });
1012        }
1013
1014        let remove_tracked_buffer = if let Some(tracked_buffer) =
1015            self.tracked_buffers.get_mut(&buffer)
1016        {
1017            let Some(expected_external_edit) = tracked_buffer.expected_external_edit.as_mut()
1018            else {
1019                return;
1020            };
1021
1022            expected_external_edit.active_source_count =
1023                expected_external_edit.active_source_count.saturating_sub(1);
1024            if record_file_read_time {
1025                expected_external_edit.record_file_read_time_source_count = expected_external_edit
1026                    .record_file_read_time_source_count
1027                    .saturating_sub(1);
1028            }
1029
1030            if expected_external_edit.active_source_count > 0 {
1031                false
1032            } else {
1033                let remove_tracked_buffer = tracked_buffer.mode
1034                    == TrackedBufferMode::ExpectationOnly
1035                    && !expected_external_edit.has_attributed_change;
1036                tracked_buffer.expected_external_edit = None;
1037                tracked_buffer.mode = TrackedBufferMode::Normal;
1038                remove_tracked_buffer
1039            }
1040        } else {
1041            false
1042        };
1043
1044        if remove_tracked_buffer {
1045            self.tracked_buffers.remove(&buffer);
1046            cx.notify();
1047        }
1048    }
1049
1050    pub fn infer_buffer_created(
1051        &mut self,
1052        buffer: Entity<Buffer>,
1053        baseline_snapshot: text::BufferSnapshot,
1054        cx: &mut Context<Self>,
1055    ) {
1056        self.infer_buffer_from_snapshot_impl(
1057            buffer,
1058            baseline_snapshot,
1059            InferredSnapshotKind::Created,
1060            true,
1061            cx,
1062        );
1063    }
1064
1065    pub fn infer_buffer_edited_from_snapshot(
1066        &mut self,
1067        buffer: Entity<Buffer>,
1068        baseline_snapshot: text::BufferSnapshot,
1069        cx: &mut Context<Self>,
1070    ) {
1071        self.infer_buffer_from_snapshot_impl(
1072            buffer,
1073            baseline_snapshot,
1074            InferredSnapshotKind::Edited,
1075            true,
1076            cx,
1077        );
1078    }
1079
1080    pub fn infer_buffer_deleted_from_snapshot(
1081        &mut self,
1082        buffer: Entity<Buffer>,
1083        baseline_snapshot: text::BufferSnapshot,
1084        cx: &mut Context<Self>,
1085    ) {
1086        self.infer_buffer_from_snapshot_impl(
1087            buffer,
1088            baseline_snapshot,
1089            InferredSnapshotKind::Deleted,
1090            true,
1091            cx,
1092        );
1093    }
1094
1095    fn forward_inferred_snapshot_to_linked_action_log(
1096        &mut self,
1097        buffer: &Entity<Buffer>,
1098        baseline_snapshot: &text::BufferSnapshot,
1099        kind: InferredSnapshotKind,
1100        cx: &mut Context<Self>,
1101    ) {
1102        if let Some(linked_action_log) = &self.linked_action_log {
1103            let linked_baseline_snapshot = baseline_snapshot.clone();
1104            // Later inferred snapshots must keep refreshing linked logs for the same buffer so
1105            // parent and child review state do not diverge after the first forwarded hunk.
1106            linked_action_log.update(cx, |log, cx| {
1107                log.infer_buffer_from_snapshot_impl(
1108                    buffer.clone(),
1109                    linked_baseline_snapshot,
1110                    kind,
1111                    false,
1112                    cx,
1113                );
1114            });
1115        }
1116    }
1117
1118    fn infer_buffer_from_snapshot_impl(
1119        &mut self,
1120        buffer: Entity<Buffer>,
1121        baseline_snapshot: text::BufferSnapshot,
1122        kind: InferredSnapshotKind,
1123        record_file_read_time: bool,
1124        cx: &mut Context<Self>,
1125    ) {
1126        self.forward_inferred_snapshot_to_linked_action_log(&buffer, &baseline_snapshot, kind, cx);
1127
1128        if record_file_read_time {
1129            match kind {
1130                InferredSnapshotKind::Created | InferredSnapshotKind::Edited => {
1131                    self.update_file_read_time(&buffer, cx);
1132                }
1133                InferredSnapshotKind::Deleted => {
1134                    self.remove_file_read_time(&buffer, cx);
1135                }
1136            }
1137        }
1138
1139        let tracked_buffer_status = kind.tracked_buffer_status(&baseline_snapshot);
1140        self.prime_tracked_buffer_from_snapshot(
1141            buffer.clone(),
1142            baseline_snapshot,
1143            tracked_buffer_status,
1144            cx,
1145        );
1146
1147        if kind == InferredSnapshotKind::Deleted && self.linked_action_log.is_none() {
1148            buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
1149        }
1150
1151        if let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) {
1152            if kind == InferredSnapshotKind::Deleted {
1153                tracked_buffer.version = buffer.read(cx).version();
1154            }
1155            tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
1156        }
1157    }
1158
1159    pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
1160        // Ok to propagate file read time removal to linked action log
1161        self.remove_file_read_time(&buffer, cx);
1162        let has_linked_action_log = self.linked_action_log.is_some();
1163        let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx);
1164        match tracked_buffer.status {
1165            TrackedBufferStatus::Created { .. } => {
1166                self.tracked_buffers.remove(&buffer);
1167                cx.notify();
1168            }
1169            TrackedBufferStatus::Modified => {
1170                tracked_buffer.status = TrackedBufferStatus::Deleted;
1171                if !has_linked_action_log {
1172                    buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
1173                    tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
1174                }
1175            }
1176
1177            TrackedBufferStatus::Deleted => {}
1178        }
1179
1180        if let Some(linked_action_log) = &mut self.linked_action_log {
1181            linked_action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx));
1182        }
1183
1184        if let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) {
1185            tracked_buffer.version = buffer.read(cx).version();
1186            if has_linked_action_log {
1187                tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
1188            }
1189        }
1190
1191        cx.notify();
1192    }
1193
1194    pub fn keep_edits_in_range(
1195        &mut self,
1196        buffer: Entity<Buffer>,
1197        buffer_range: Range<impl language::ToPoint>,
1198        telemetry: Option<ActionLogTelemetry>,
1199        cx: &mut Context<Self>,
1200    ) {
1201        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
1202            return;
1203        };
1204
1205        let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx));
1206        match tracked_buffer.status {
1207            TrackedBufferStatus::Deleted => {
1208                metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
1209                self.tracked_buffers.remove(&buffer);
1210                cx.notify();
1211            }
1212            _ => {
1213                let buffer = buffer.read(cx);
1214                let buffer_range =
1215                    buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
1216                let mut delta = 0i32;
1217                tracked_buffer.unreviewed_edits.retain_mut(|edit| {
1218                    edit.old.start = (edit.old.start as i32 + delta) as u32;
1219                    edit.old.end = (edit.old.end as i32 + delta) as u32;
1220
1221                    if buffer_range.end.row < edit.new.start
1222                        || buffer_range.start.row > edit.new.end
1223                    {
1224                        true
1225                    } else {
1226                        let old_range = tracked_buffer
1227                            .diff_base
1228                            .point_to_offset(Point::new(edit.old.start, 0))
1229                            ..tracked_buffer.diff_base.point_to_offset(cmp::min(
1230                                Point::new(edit.old.end, 0),
1231                                tracked_buffer.diff_base.max_point(),
1232                            ));
1233                        let new_range = tracked_buffer
1234                            .snapshot
1235                            .point_to_offset(Point::new(edit.new.start, 0))
1236                            ..tracked_buffer.snapshot.point_to_offset(cmp::min(
1237                                Point::new(edit.new.end, 0),
1238                                tracked_buffer.snapshot.max_point(),
1239                            ));
1240                        tracked_buffer.diff_base.replace(
1241                            old_range,
1242                            &tracked_buffer
1243                                .snapshot
1244                                .text_for_range(new_range)
1245                                .collect::<String>(),
1246                        );
1247                        delta += edit.new_len() as i32 - edit.old_len() as i32;
1248                        metrics.add_edit(edit);
1249                        false
1250                    }
1251                });
1252                if tracked_buffer.unreviewed_edits.is_empty()
1253                    && let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status
1254                {
1255                    tracked_buffer.status = TrackedBufferStatus::Modified;
1256                }
1257                tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
1258            }
1259        }
1260        if let Some(telemetry) = telemetry {
1261            telemetry_report_accepted_edits(&telemetry, metrics);
1262        }
1263    }
1264
1265    pub fn reject_edits_in_ranges(
1266        &mut self,
1267        buffer: Entity<Buffer>,
1268        buffer_ranges: Vec<Range<impl language::ToPoint>>,
1269        telemetry: Option<ActionLogTelemetry>,
1270        cx: &mut Context<Self>,
1271    ) -> (Task<Result<()>>, Option<PerBufferUndo>) {
1272        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
1273            return (Task::ready(Ok(())), None);
1274        };
1275
1276        let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx));
1277        let mut undo_info: Option<PerBufferUndo> = None;
1278        let task = match &tracked_buffer.status {
1279            TrackedBufferStatus::Created {
1280                existing_file_content,
1281            } => {
1282                let task = if let Some(existing_file_content) = existing_file_content {
1283                    // Capture the agent's content before restoring existing file content
1284                    let agent_content = buffer.read(cx).text();
1285
1286                    buffer.update(cx, |buffer, cx| {
1287                        buffer.start_transaction();
1288                        buffer.set_text("", cx);
1289                        for chunk in existing_file_content.chunks() {
1290                            buffer.append(chunk, cx);
1291                        }
1292                        buffer.end_transaction(cx);
1293                    });
1294
1295                    undo_info = Some(PerBufferUndo {
1296                        buffer: buffer.downgrade(),
1297                        edits_to_restore: vec![(Anchor::MIN..Anchor::MAX, agent_content)],
1298                        status: UndoBufferStatus::Created {
1299                            had_existing_content: true,
1300                        },
1301                    });
1302
1303                    self.project
1304                        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1305                } else {
1306                    // For a file created by AI with no pre-existing content,
1307                    // only delete the file if we're certain it contains only AI content
1308                    // with no edits from the user.
1309
1310                    let initial_version = tracked_buffer.version.clone();
1311                    let current_version = buffer.read(cx).version();
1312
1313                    let current_content = buffer.read(cx).text();
1314                    let tracked_content = tracked_buffer.snapshot.text();
1315
1316                    let is_ai_only_content =
1317                        initial_version == current_version && current_content == tracked_content;
1318
1319                    if is_ai_only_content {
1320                        buffer
1321                            .read(cx)
1322                            .entry_id(cx)
1323                            .and_then(|entry_id| {
1324                                self.project.update(cx, |project, cx| {
1325                                    project.delete_entry(entry_id, false, cx)
1326                                })
1327                            })
1328                            .unwrap_or(Task::ready(Ok(())))
1329                    } else {
1330                        // Not sure how to disentangle edits made by the user
1331                        // from edits made by the AI at this point.
1332                        // For now, preserve both to avoid data loss.
1333                        //
1334                        // TODO: Better solution (disable "Reject" after user makes some
1335                        // edit or find a way to differentiate between AI and user edits)
1336                        Task::ready(Ok(()))
1337                    }
1338                };
1339
1340                metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
1341                self.tracked_buffers.remove(&buffer);
1342                cx.notify();
1343                task
1344            }
1345            TrackedBufferStatus::Deleted => {
1346                let current_version = buffer.read(cx).version();
1347                if current_version != tracked_buffer.version {
1348                    metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
1349                    self.tracked_buffers.remove(&buffer);
1350                    cx.notify();
1351                    Task::ready(Ok(()))
1352                } else {
1353                    buffer.update(cx, |buffer, cx| {
1354                        buffer.set_text(tracked_buffer.diff_base.to_string(), cx)
1355                    });
1356                    let save = self
1357                        .project
1358                        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
1359
1360                    // Clear all tracked edits for this buffer and start over as if we just read it.
1361                    metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
1362                    self.tracked_buffers.remove(&buffer);
1363                    self.buffer_read(buffer.clone(), cx);
1364                    cx.notify();
1365                    save
1366                }
1367            }
1368            TrackedBufferStatus::Modified => {
1369                let edits_to_restore = buffer.update(cx, |buffer, cx| {
1370                    let mut buffer_row_ranges = buffer_ranges
1371                        .into_iter()
1372                        .map(|range| {
1373                            range.start.to_point(buffer).row..range.end.to_point(buffer).row
1374                        })
1375                        .peekable();
1376
1377                    let mut edits_to_revert = Vec::new();
1378                    let mut edits_for_undo = Vec::new();
1379                    for edit in tracked_buffer.unreviewed_edits.edits() {
1380                        let new_range = tracked_buffer
1381                            .snapshot
1382                            .anchor_before(Point::new(edit.new.start, 0))
1383                            ..tracked_buffer.snapshot.anchor_after(cmp::min(
1384                                Point::new(edit.new.end, 0),
1385                                tracked_buffer.snapshot.max_point(),
1386                            ));
1387                        let new_row_range = new_range.start.to_point(buffer).row
1388                            ..new_range.end.to_point(buffer).row;
1389
1390                        let mut revert = false;
1391                        while let Some(buffer_row_range) = buffer_row_ranges.peek() {
1392                            if buffer_row_range.end < new_row_range.start {
1393                                buffer_row_ranges.next();
1394                            } else if buffer_row_range.start > new_row_range.end {
1395                                break;
1396                            } else {
1397                                revert = true;
1398                                break;
1399                            }
1400                        }
1401
1402                        if revert {
1403                            metrics.add_edit(edit);
1404                            let old_range = tracked_buffer
1405                                .diff_base
1406                                .point_to_offset(Point::new(edit.old.start, 0))
1407                                ..tracked_buffer.diff_base.point_to_offset(cmp::min(
1408                                    Point::new(edit.old.end, 0),
1409                                    tracked_buffer.diff_base.max_point(),
1410                                ));
1411                            let old_text = tracked_buffer
1412                                .diff_base
1413                                .chunks_in_range(old_range)
1414                                .collect::<String>();
1415
1416                            // Capture the agent's text before we revert it (for undo)
1417                            let new_range_offset =
1418                                new_range.start.to_offset(buffer)..new_range.end.to_offset(buffer);
1419                            let agent_text =
1420                                buffer.text_for_range(new_range_offset).collect::<String>();
1421                            edits_for_undo.push((new_range.clone(), agent_text));
1422
1423                            edits_to_revert.push((new_range, old_text));
1424                        }
1425                    }
1426
1427                    buffer.edit(edits_to_revert, None, cx);
1428                    edits_for_undo
1429                });
1430
1431                if !edits_to_restore.is_empty() {
1432                    undo_info = Some(PerBufferUndo {
1433                        buffer: buffer.downgrade(),
1434                        edits_to_restore,
1435                        status: UndoBufferStatus::Modified,
1436                    });
1437                }
1438
1439                self.project
1440                    .update(cx, |project, cx| project.save_buffer(buffer, cx))
1441            }
1442        };
1443        if let Some(telemetry) = telemetry {
1444            telemetry_report_rejected_edits(&telemetry, metrics);
1445        }
1446        (task, undo_info)
1447    }
1448
1449    pub fn keep_all_edits(
1450        &mut self,
1451        telemetry: Option<ActionLogTelemetry>,
1452        cx: &mut Context<Self>,
1453    ) {
1454        self.tracked_buffers.retain(|buffer, tracked_buffer| {
1455            let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx));
1456            metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
1457            if let Some(telemetry) = telemetry.as_ref() {
1458                telemetry_report_accepted_edits(telemetry, metrics);
1459            }
1460            match tracked_buffer.status {
1461                TrackedBufferStatus::Deleted => false,
1462                _ => {
1463                    if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status {
1464                        tracked_buffer.status = TrackedBufferStatus::Modified;
1465                    }
1466                    tracked_buffer.unreviewed_edits.clear();
1467                    tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone();
1468                    tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
1469                    true
1470                }
1471            }
1472        });
1473
1474        cx.notify();
1475    }
1476
1477    pub fn reject_all_edits(
1478        &mut self,
1479        telemetry: Option<ActionLogTelemetry>,
1480        cx: &mut Context<Self>,
1481    ) -> Task<()> {
1482        // Clear any previous undo state before starting a new reject operation
1483        self.last_reject_undo = None;
1484
1485        let mut undo_buffers = Vec::new();
1486        let mut futures = Vec::new();
1487
1488        for buffer in self.changed_buffers(cx).into_keys() {
1489            let buffer_ranges = vec![Anchor::min_max_range_for_buffer(
1490                buffer.read(cx).remote_id(),
1491            )];
1492            let (reject_task, undo_info) =
1493                self.reject_edits_in_ranges(buffer, buffer_ranges, telemetry.clone(), cx);
1494
1495            if let Some(undo) = undo_info {
1496                undo_buffers.push(undo);
1497            }
1498
1499            futures.push(async move {
1500                reject_task.await.log_err();
1501            });
1502        }
1503
1504        // Store the undo information if we have any
1505        if !undo_buffers.is_empty() {
1506            self.last_reject_undo = Some(LastRejectUndo {
1507                buffers: undo_buffers,
1508            });
1509        }
1510
1511        let task = futures::future::join_all(futures);
1512        cx.background_spawn(async move {
1513            task.await;
1514        })
1515    }
1516
1517    pub fn has_pending_undo(&self) -> bool {
1518        self.last_reject_undo.is_some()
1519    }
1520
1521    pub fn set_last_reject_undo(&mut self, undo: LastRejectUndo) {
1522        self.last_reject_undo = Some(undo);
1523    }
1524
1525    /// Undoes the most recent reject operation, restoring the rejected agent changes.
1526    /// This is a best-effort operation: if buffers have been closed or modified externally,
1527    /// those buffers will be skipped.
1528    pub fn undo_last_reject(&mut self, cx: &mut Context<Self>) -> Task<()> {
1529        let Some(undo) = self.last_reject_undo.take() else {
1530            return Task::ready(());
1531        };
1532
1533        let mut save_tasks = Vec::with_capacity(undo.buffers.len());
1534
1535        for per_buffer_undo in undo.buffers {
1536            // Skip if the buffer entity has been deallocated
1537            let Some(buffer) = per_buffer_undo.buffer.upgrade() else {
1538                continue;
1539            };
1540
1541            buffer.update(cx, |buffer, cx| {
1542                let mut valid_edits = Vec::new();
1543
1544                for (anchor_range, text_to_restore) in per_buffer_undo.edits_to_restore {
1545                    if anchor_range.start.buffer_id == Some(buffer.remote_id())
1546                        && anchor_range.end.buffer_id == Some(buffer.remote_id())
1547                    {
1548                        valid_edits.push((anchor_range, text_to_restore));
1549                    }
1550                }
1551
1552                if !valid_edits.is_empty() {
1553                    buffer.edit(valid_edits, None, cx);
1554                }
1555            });
1556
1557            if !self.tracked_buffers.contains_key(&buffer) {
1558                self.buffer_edited(buffer.clone(), cx);
1559            }
1560
1561            let save = self
1562                .project
1563                .update(cx, |project, cx| project.save_buffer(buffer, cx));
1564            save_tasks.push(save);
1565        }
1566
1567        cx.notify();
1568
1569        cx.background_spawn(async move {
1570            futures::future::join_all(save_tasks).await;
1571        })
1572    }
1573
1574    /// Returns the set of buffers that contain edits that haven't been reviewed by the user.
1575    pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
1576        self.tracked_buffers
1577            .iter()
1578            .filter(|(_, tracked)| tracked.has_edits(cx))
1579            .map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone()))
1580            .collect()
1581    }
1582
1583    /// Returns the total number of lines added and removed across all unreviewed buffers.
1584    pub fn diff_stats(&self, cx: &App) -> DiffStats {
1585        DiffStats::all_files(&self.changed_buffers(cx), cx)
1586    }
1587
1588    /// Iterate over buffers changed since last read or edited by the model
1589    pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
1590        self.tracked_buffers
1591            .iter()
1592            .filter(|(buffer, tracked)| {
1593                let buffer = buffer.read(cx);
1594
1595                tracked.mode == TrackedBufferMode::Normal
1596                    && tracked.version != buffer.version
1597                    && buffer
1598                        .file()
1599                        .is_some_and(|file| !file.disk_state().is_deleted())
1600            })
1601            .map(|(buffer, _)| buffer)
1602    }
1603}
1604
1605#[derive(Default, Debug, Clone, Copy)]
1606pub struct DiffStats {
1607    pub lines_added: u32,
1608    pub lines_removed: u32,
1609}
1610
1611impl DiffStats {
1612    pub fn single_file(buffer: &Buffer, diff: &BufferDiff, cx: &App) -> Self {
1613        let mut stats = DiffStats::default();
1614        let diff_snapshot = diff.snapshot(cx);
1615        let buffer_snapshot = buffer.snapshot();
1616        let base_text = diff_snapshot.base_text();
1617
1618        for hunk in diff_snapshot.hunks(&buffer_snapshot) {
1619            let added_rows = hunk.range.end.row.saturating_sub(hunk.range.start.row);
1620            stats.lines_added += added_rows;
1621
1622            let base_start = hunk.diff_base_byte_range.start.to_point(base_text).row;
1623            let base_end = hunk.diff_base_byte_range.end.to_point(base_text).row;
1624            let removed_rows = base_end.saturating_sub(base_start);
1625            stats.lines_removed += removed_rows;
1626        }
1627
1628        stats
1629    }
1630
1631    pub fn all_files(
1632        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
1633        cx: &App,
1634    ) -> Self {
1635        let mut total = DiffStats::default();
1636        for (buffer, diff) in changed_buffers {
1637            let stats = DiffStats::single_file(buffer.read(cx), diff.read(cx), cx);
1638            total.lines_added += stats.lines_added;
1639            total.lines_removed += stats.lines_removed;
1640        }
1641        total
1642    }
1643}
1644
1645#[derive(Clone)]
1646pub struct ActionLogTelemetry {
1647    pub agent_telemetry_id: SharedString,
1648    pub session_id: Arc<str>,
1649}
1650
1651struct ActionLogMetrics {
1652    lines_removed: u32,
1653    lines_added: u32,
1654    language: Option<SharedString>,
1655}
1656
1657impl ActionLogMetrics {
1658    fn for_buffer(buffer: &Buffer) -> Self {
1659        Self {
1660            language: buffer.language().map(|l| l.name().0),
1661            lines_removed: 0,
1662            lines_added: 0,
1663        }
1664    }
1665
1666    fn add_edits(&mut self, edits: &[Edit<u32>]) {
1667        for edit in edits {
1668            self.add_edit(edit);
1669        }
1670    }
1671
1672    fn add_edit(&mut self, edit: &Edit<u32>) {
1673        self.lines_added += edit.new_len();
1674        self.lines_removed += edit.old_len();
1675    }
1676}
1677
1678fn telemetry_report_accepted_edits(telemetry: &ActionLogTelemetry, metrics: ActionLogMetrics) {
1679    telemetry::event!(
1680        "Agent Edits Accepted",
1681        agent = telemetry.agent_telemetry_id,
1682        session = telemetry.session_id,
1683        language = metrics.language,
1684        lines_added = metrics.lines_added,
1685        lines_removed = metrics.lines_removed
1686    );
1687}
1688
1689fn telemetry_report_rejected_edits(telemetry: &ActionLogTelemetry, metrics: ActionLogMetrics) {
1690    telemetry::event!(
1691        "Agent Edits Rejected",
1692        agent = telemetry.agent_telemetry_id,
1693        session = telemetry.session_id,
1694        language = metrics.language,
1695        lines_added = metrics.lines_added,
1696        lines_removed = metrics.lines_removed
1697    );
1698}
1699
1700fn apply_non_conflicting_edits(
1701    patch: &Patch<u32>,
1702    edits: Vec<Edit<u32>>,
1703    old_text: &mut Rope,
1704    new_text: &Rope,
1705) -> bool {
1706    let mut old_edits = patch.edits().iter().cloned().peekable();
1707    let mut new_edits = edits.into_iter().peekable();
1708    let mut applied_delta = 0i32;
1709    let mut rebased_delta = 0i32;
1710    let mut has_made_changes = false;
1711
1712    while let Some(mut new_edit) = new_edits.next() {
1713        let mut conflict = false;
1714
1715        // Push all the old edits that are before this new edit or that intersect with it.
1716        while let Some(old_edit) = old_edits.peek() {
1717            if new_edit.old.end < old_edit.new.start
1718                || (!old_edit.new.is_empty() && new_edit.old.end == old_edit.new.start)
1719            {
1720                break;
1721            } else if new_edit.old.start > old_edit.new.end
1722                || (!old_edit.new.is_empty() && new_edit.old.start == old_edit.new.end)
1723            {
1724                let old_edit = old_edits.next().unwrap();
1725                rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
1726            } else {
1727                conflict = true;
1728                if new_edits
1729                    .peek()
1730                    .is_some_and(|next_edit| next_edit.old.overlaps(&old_edit.new))
1731                {
1732                    new_edit = new_edits.next().unwrap();
1733                } else {
1734                    let old_edit = old_edits.next().unwrap();
1735                    rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
1736                }
1737            }
1738        }
1739
1740        if !conflict {
1741            // This edit doesn't intersect with any old edit, so we can apply it to the old text.
1742            new_edit.old.start = (new_edit.old.start as i32 + applied_delta - rebased_delta) as u32;
1743            new_edit.old.end = (new_edit.old.end as i32 + applied_delta - rebased_delta) as u32;
1744            let old_bytes = old_text.point_to_offset(Point::new(new_edit.old.start, 0))
1745                ..old_text.point_to_offset(cmp::min(
1746                    Point::new(new_edit.old.end, 0),
1747                    old_text.max_point(),
1748                ));
1749            let new_bytes = new_text.point_to_offset(Point::new(new_edit.new.start, 0))
1750                ..new_text.point_to_offset(cmp::min(
1751                    Point::new(new_edit.new.end, 0),
1752                    new_text.max_point(),
1753                ));
1754
1755            old_text.replace(
1756                old_bytes,
1757                &new_text.chunks_in_range(new_bytes).collect::<String>(),
1758            );
1759            applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32;
1760            has_made_changes = true;
1761        }
1762    }
1763    has_made_changes
1764}
1765
1766fn diff_snapshots(
1767    old_snapshot: &text::BufferSnapshot,
1768    new_snapshot: &text::BufferSnapshot,
1769) -> Vec<Edit<u32>> {
1770    let mut edits = new_snapshot
1771        .edits_since::<Point>(&old_snapshot.version)
1772        .map(|edit| point_to_row_edit(edit, old_snapshot.as_rope(), new_snapshot.as_rope()))
1773        .peekable();
1774    let mut row_edits = Vec::new();
1775    while let Some(mut edit) = edits.next() {
1776        while let Some(next_edit) = edits.peek() {
1777            if edit.old.end >= next_edit.old.start {
1778                edit.old.end = next_edit.old.end;
1779                edit.new.end = next_edit.new.end;
1780                edits.next();
1781            } else {
1782                break;
1783            }
1784        }
1785        row_edits.push(edit);
1786    }
1787    row_edits
1788}
1789
1790fn point_to_row_edit(edit: Edit<Point>, old_text: &Rope, new_text: &Rope) -> Edit<u32> {
1791    if edit.old.start.column == old_text.line_len(edit.old.start.row)
1792        && new_text
1793            .chars_at(new_text.point_to_offset(edit.new.start))
1794            .next()
1795            == Some('\n')
1796        && edit.old.start != old_text.max_point()
1797    {
1798        Edit {
1799            old: edit.old.start.row + 1..edit.old.end.row + 1,
1800            new: edit.new.start.row + 1..edit.new.end.row + 1,
1801        }
1802    } else if edit.old.start.column == 0 && edit.old.end.column == 0 && edit.new.end.column == 0 {
1803        Edit {
1804            old: edit.old.start.row..edit.old.end.row,
1805            new: edit.new.start.row..edit.new.end.row,
1806        }
1807    } else {
1808        Edit {
1809            old: edit.old.start.row..edit.old.end.row + 1,
1810            new: edit.new.start.row..edit.new.end.row + 1,
1811        }
1812    }
1813}
1814
1815#[derive(Copy, Clone, Debug)]
1816enum ChangeAuthor {
1817    User,
1818    Agent,
1819}
1820
1821#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1822enum InferredSnapshotKind {
1823    Created,
1824    Edited,
1825    Deleted,
1826}
1827
1828impl InferredSnapshotKind {
1829    fn tracked_buffer_status(
1830        self,
1831        baseline_snapshot: &text::BufferSnapshot,
1832    ) -> TrackedBufferStatus {
1833        match self {
1834            Self::Created => TrackedBufferStatus::Created {
1835                existing_file_content: if baseline_snapshot.text().is_empty() {
1836                    None
1837                } else {
1838                    Some(baseline_snapshot.as_rope().clone())
1839                },
1840            },
1841            Self::Edited => TrackedBufferStatus::Modified,
1842            Self::Deleted => TrackedBufferStatus::Deleted,
1843        }
1844    }
1845}
1846
1847#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1848enum TrackedBufferMode {
1849    Normal,
1850    ExpectationOnly,
1851}
1852
1853#[derive(Clone, Debug)]
1854struct ExpectedExternalEdit {
1855    active_source_count: usize,
1856    record_file_read_time_source_count: usize,
1857    initial_exists_on_disk: bool,
1858    observed_external_file_change: bool,
1859    armed_explicit_reload: bool,
1860    has_attributed_change: bool,
1861    pending_delete: bool,
1862    is_disqualified: bool,
1863}
1864
1865#[derive(Debug)]
1866enum TrackedBufferStatus {
1867    Created { existing_file_content: Option<Rope> },
1868    Modified,
1869    Deleted,
1870}
1871
1872pub struct TrackedBuffer {
1873    buffer: Entity<Buffer>,
1874    diff_base: Rope,
1875    unreviewed_edits: Patch<u32>,
1876    status: TrackedBufferStatus,
1877    mode: TrackedBufferMode,
1878    expected_external_edit: Option<ExpectedExternalEdit>,
1879    version: clock::Global,
1880    diff: Entity<BufferDiff>,
1881    snapshot: text::BufferSnapshot,
1882    diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
1883    _open_lsp_handle: OpenLspBufferHandle,
1884    _maintain_diff: Task<()>,
1885    _subscription: Subscription,
1886}
1887
1888impl TrackedBuffer {
1889    #[cfg(any(test, feature = "test-support"))]
1890    pub fn diff(&self) -> &Entity<BufferDiff> {
1891        &self.diff
1892    }
1893
1894    #[cfg(any(test, feature = "test-support"))]
1895    pub fn diff_base_len(&self) -> usize {
1896        self.diff_base.len()
1897    }
1898
1899    fn has_edits(&self, cx: &App) -> bool {
1900        self.diff
1901            .read(cx)
1902            .snapshot(cx)
1903            .hunks(self.buffer.read(cx))
1904            .next()
1905            .is_some()
1906    }
1907
1908    fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) {
1909        self.diff_update
1910            .unbounded_send((author, self.buffer.read(cx).text_snapshot()))
1911            .ok();
1912    }
1913}
1914
1915pub struct ChangedBuffer {
1916    pub diff: Entity<BufferDiff>,
1917}
1918
1919#[cfg(test)]
1920mod tests {
1921    use super::*;
1922    use buffer_diff::DiffHunkStatusKind;
1923    use gpui::TestAppContext;
1924    use language::Point;
1925    use project::{FakeFs, Fs, Project, RemoveOptions};
1926    use rand::prelude::*;
1927    use serde_json::json;
1928    use settings::SettingsStore;
1929    use std::env;
1930    use util::{RandomCharIter, path};
1931
1932    #[ctor::ctor]
1933    fn init_logger() {
1934        zlog::init_test();
1935    }
1936
1937    fn init_test(cx: &mut TestAppContext) {
1938        cx.update(|cx| {
1939            let settings_store = SettingsStore::test(cx);
1940            cx.set_global(settings_store);
1941        });
1942    }
1943
1944    #[gpui::test(iterations = 10)]
1945    async fn test_keep_edits(cx: &mut TestAppContext) {
1946        init_test(cx);
1947
1948        let fs = FakeFs::new(cx.executor());
1949        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
1950            .await;
1951        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1952        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1953        let file_path = project
1954            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1955            .unwrap();
1956        let buffer = project
1957            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1958            .await
1959            .unwrap();
1960
1961        cx.update(|cx| {
1962            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1963            buffer.update(cx, |buffer, cx| {
1964                buffer
1965                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
1966                    .unwrap()
1967            });
1968            buffer.update(cx, |buffer, cx| {
1969                buffer
1970                    .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
1971                    .unwrap()
1972            });
1973            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1974        });
1975        cx.run_until_parked();
1976        assert_eq!(
1977            buffer.read_with(cx, |buffer, _| buffer.text()),
1978            "abc\ndEf\nghi\njkl\nmnO"
1979        );
1980        assert_eq!(
1981            unreviewed_hunks(&action_log, cx),
1982            vec![(
1983                buffer.clone(),
1984                vec![
1985                    HunkStatus {
1986                        range: Point::new(1, 0)..Point::new(2, 0),
1987                        diff_status: DiffHunkStatusKind::Modified,
1988                        old_text: "def\n".into(),
1989                    },
1990                    HunkStatus {
1991                        range: Point::new(4, 0)..Point::new(4, 3),
1992                        diff_status: DiffHunkStatusKind::Modified,
1993                        old_text: "mno".into(),
1994                    }
1995                ],
1996            )]
1997        );
1998
1999        action_log.update(cx, |log, cx| {
2000            log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), None, cx)
2001        });
2002        cx.run_until_parked();
2003        assert_eq!(
2004            unreviewed_hunks(&action_log, cx),
2005            vec![(
2006                buffer.clone(),
2007                vec![HunkStatus {
2008                    range: Point::new(1, 0)..Point::new(2, 0),
2009                    diff_status: DiffHunkStatusKind::Modified,
2010                    old_text: "def\n".into(),
2011                }],
2012            )]
2013        );
2014
2015        action_log.update(cx, |log, cx| {
2016            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), None, cx)
2017        });
2018        cx.run_until_parked();
2019        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2020    }
2021
2022    #[gpui::test(iterations = 10)]
2023    async fn test_deletions(cx: &mut TestAppContext) {
2024        init_test(cx);
2025
2026        let fs = FakeFs::new(cx.executor());
2027        fs.insert_tree(
2028            path!("/dir"),
2029            json!({"file": "abc\ndef\nghi\njkl\nmno\npqr"}),
2030        )
2031        .await;
2032        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2033        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2034        let file_path = project
2035            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
2036            .unwrap();
2037        let buffer = project
2038            .update(cx, |project, cx| project.open_buffer(file_path, cx))
2039            .await
2040            .unwrap();
2041
2042        cx.update(|cx| {
2043            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
2044            buffer.update(cx, |buffer, cx| {
2045                buffer
2046                    .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx)
2047                    .unwrap();
2048                buffer.finalize_last_transaction();
2049            });
2050            buffer.update(cx, |buffer, cx| {
2051                buffer
2052                    .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx)
2053                    .unwrap();
2054                buffer.finalize_last_transaction();
2055            });
2056            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2057        });
2058        cx.run_until_parked();
2059        assert_eq!(
2060            buffer.read_with(cx, |buffer, _| buffer.text()),
2061            "abc\nghi\njkl\npqr"
2062        );
2063        assert_eq!(
2064            unreviewed_hunks(&action_log, cx),
2065            vec![(
2066                buffer.clone(),
2067                vec![
2068                    HunkStatus {
2069                        range: Point::new(1, 0)..Point::new(1, 0),
2070                        diff_status: DiffHunkStatusKind::Deleted,
2071                        old_text: "def\n".into(),
2072                    },
2073                    HunkStatus {
2074                        range: Point::new(3, 0)..Point::new(3, 0),
2075                        diff_status: DiffHunkStatusKind::Deleted,
2076                        old_text: "mno\n".into(),
2077                    }
2078                ],
2079            )]
2080        );
2081
2082        buffer.update(cx, |buffer, cx| buffer.undo(cx));
2083        cx.run_until_parked();
2084        assert_eq!(
2085            buffer.read_with(cx, |buffer, _| buffer.text()),
2086            "abc\nghi\njkl\nmno\npqr"
2087        );
2088        assert_eq!(
2089            unreviewed_hunks(&action_log, cx),
2090            vec![(
2091                buffer.clone(),
2092                vec![HunkStatus {
2093                    range: Point::new(1, 0)..Point::new(1, 0),
2094                    diff_status: DiffHunkStatusKind::Deleted,
2095                    old_text: "def\n".into(),
2096                }],
2097            )]
2098        );
2099
2100        action_log.update(cx, |log, cx| {
2101            log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), None, cx)
2102        });
2103        cx.run_until_parked();
2104        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2105    }
2106
2107    #[gpui::test(iterations = 10)]
2108    async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
2109        init_test(cx);
2110
2111        let fs = FakeFs::new(cx.executor());
2112        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
2113            .await;
2114        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2115        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2116        let file_path = project
2117            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
2118            .unwrap();
2119        let buffer = project
2120            .update(cx, |project, cx| project.open_buffer(file_path, cx))
2121            .await
2122            .unwrap();
2123
2124        cx.update(|cx| {
2125            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
2126            buffer.update(cx, |buffer, cx| {
2127                buffer
2128                    .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
2129                    .unwrap()
2130            });
2131            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2132        });
2133        cx.run_until_parked();
2134        assert_eq!(
2135            buffer.read_with(cx, |buffer, _| buffer.text()),
2136            "abc\ndeF\nGHI\njkl\nmno"
2137        );
2138        assert_eq!(
2139            unreviewed_hunks(&action_log, cx),
2140            vec![(
2141                buffer.clone(),
2142                vec![HunkStatus {
2143                    range: Point::new(1, 0)..Point::new(3, 0),
2144                    diff_status: DiffHunkStatusKind::Modified,
2145                    old_text: "def\nghi\n".into(),
2146                }],
2147            )]
2148        );
2149
2150        buffer.update(cx, |buffer, cx| {
2151            buffer.edit(
2152                [
2153                    (Point::new(0, 2)..Point::new(0, 2), "X"),
2154                    (Point::new(3, 0)..Point::new(3, 0), "Y"),
2155                ],
2156                None,
2157                cx,
2158            )
2159        });
2160        cx.run_until_parked();
2161        assert_eq!(
2162            buffer.read_with(cx, |buffer, _| buffer.text()),
2163            "abXc\ndeF\nGHI\nYjkl\nmno"
2164        );
2165        assert_eq!(
2166            unreviewed_hunks(&action_log, cx),
2167            vec![(
2168                buffer.clone(),
2169                vec![HunkStatus {
2170                    range: Point::new(1, 0)..Point::new(3, 0),
2171                    diff_status: DiffHunkStatusKind::Modified,
2172                    old_text: "def\nghi\n".into(),
2173                }],
2174            )]
2175        );
2176
2177        buffer.update(cx, |buffer, cx| {
2178            buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx)
2179        });
2180        cx.run_until_parked();
2181        assert_eq!(
2182            buffer.read_with(cx, |buffer, _| buffer.text()),
2183            "abXc\ndZeF\nGHI\nYjkl\nmno"
2184        );
2185        assert_eq!(
2186            unreviewed_hunks(&action_log, cx),
2187            vec![(
2188                buffer.clone(),
2189                vec![HunkStatus {
2190                    range: Point::new(1, 0)..Point::new(3, 0),
2191                    diff_status: DiffHunkStatusKind::Modified,
2192                    old_text: "def\nghi\n".into(),
2193                }],
2194            )]
2195        );
2196
2197        action_log.update(cx, |log, cx| {
2198            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), None, cx)
2199        });
2200        cx.run_until_parked();
2201        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2202    }
2203
2204    #[gpui::test(iterations = 10)]
2205    async fn test_creating_files(cx: &mut TestAppContext) {
2206        init_test(cx);
2207
2208        let fs = FakeFs::new(cx.executor());
2209        fs.insert_tree(path!("/dir"), json!({})).await;
2210        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2211        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2212        let file_path = project
2213            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
2214            .unwrap();
2215
2216        let buffer = project
2217            .update(cx, |project, cx| project.open_buffer(file_path, cx))
2218            .await
2219            .unwrap();
2220        cx.update(|cx| {
2221            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
2222            buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
2223            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2224        });
2225        project
2226            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2227            .await
2228            .unwrap();
2229        cx.run_until_parked();
2230        assert_eq!(
2231            unreviewed_hunks(&action_log, cx),
2232            vec![(
2233                buffer.clone(),
2234                vec![HunkStatus {
2235                    range: Point::new(0, 0)..Point::new(0, 5),
2236                    diff_status: DiffHunkStatusKind::Added,
2237                    old_text: "".into(),
2238                }],
2239            )]
2240        );
2241
2242        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx));
2243        cx.run_until_parked();
2244        assert_eq!(
2245            unreviewed_hunks(&action_log, cx),
2246            vec![(
2247                buffer.clone(),
2248                vec![HunkStatus {
2249                    range: Point::new(0, 0)..Point::new(0, 6),
2250                    diff_status: DiffHunkStatusKind::Added,
2251                    old_text: "".into(),
2252                }],
2253            )]
2254        );
2255
2256        action_log.update(cx, |log, cx| {
2257            log.keep_edits_in_range(buffer.clone(), 0..5, None, cx)
2258        });
2259        cx.run_until_parked();
2260        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2261    }
2262
2263    #[gpui::test(iterations = 10)]
2264    async fn test_overwriting_files(cx: &mut TestAppContext) {
2265        init_test(cx);
2266
2267        let fs = FakeFs::new(cx.executor());
2268        fs.insert_tree(
2269            path!("/dir"),
2270            json!({
2271                "file1": "Lorem ipsum dolor"
2272            }),
2273        )
2274        .await;
2275        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2276        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2277        let file_path = project
2278            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
2279            .unwrap();
2280
2281        let buffer = project
2282            .update(cx, |project, cx| project.open_buffer(file_path, cx))
2283            .await
2284            .unwrap();
2285        cx.update(|cx| {
2286            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
2287            buffer.update(cx, |buffer, cx| buffer.set_text("sit amet consecteur", cx));
2288            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2289        });
2290        project
2291            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2292            .await
2293            .unwrap();
2294        cx.run_until_parked();
2295        assert_eq!(
2296            unreviewed_hunks(&action_log, cx),
2297            vec![(
2298                buffer.clone(),
2299                vec![HunkStatus {
2300                    range: Point::new(0, 0)..Point::new(0, 19),
2301                    diff_status: DiffHunkStatusKind::Added,
2302                    old_text: "".into(),
2303                }],
2304            )]
2305        );
2306
2307        action_log
2308            .update(cx, |log, cx| {
2309                let (task, _) = log.reject_edits_in_ranges(buffer.clone(), vec![2..5], None, cx);
2310                task
2311            })
2312            .await
2313            .unwrap();
2314        cx.run_until_parked();
2315        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2316        assert_eq!(
2317            buffer.read_with(cx, |buffer, _cx| buffer.text()),
2318            "Lorem ipsum dolor"
2319        );
2320    }
2321
2322    #[gpui::test(iterations = 10)]
2323    async fn test_overwriting_previously_edited_files(cx: &mut TestAppContext) {
2324        init_test(cx);
2325
2326        let fs = FakeFs::new(cx.executor());
2327        fs.insert_tree(
2328            path!("/dir"),
2329            json!({
2330                "file1": "Lorem ipsum dolor"
2331            }),
2332        )
2333        .await;
2334        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2335        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2336        let file_path = project
2337            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
2338            .unwrap();
2339
2340        let buffer = project
2341            .update(cx, |project, cx| project.open_buffer(file_path, cx))
2342            .await
2343            .unwrap();
2344        cx.update(|cx| {
2345            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
2346            buffer.update(cx, |buffer, cx| buffer.append(" sit amet consecteur", cx));
2347            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2348        });
2349        project
2350            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2351            .await
2352            .unwrap();
2353        cx.run_until_parked();
2354        assert_eq!(
2355            unreviewed_hunks(&action_log, cx),
2356            vec![(
2357                buffer.clone(),
2358                vec![HunkStatus {
2359                    range: Point::new(0, 0)..Point::new(0, 37),
2360                    diff_status: DiffHunkStatusKind::Modified,
2361                    old_text: "Lorem ipsum dolor".into(),
2362                }],
2363            )]
2364        );
2365
2366        cx.update(|cx| {
2367            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
2368            buffer.update(cx, |buffer, cx| buffer.set_text("rewritten", cx));
2369            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2370        });
2371        project
2372            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2373            .await
2374            .unwrap();
2375        cx.run_until_parked();
2376        assert_eq!(
2377            unreviewed_hunks(&action_log, cx),
2378            vec![(
2379                buffer.clone(),
2380                vec![HunkStatus {
2381                    range: Point::new(0, 0)..Point::new(0, 9),
2382                    diff_status: DiffHunkStatusKind::Added,
2383                    old_text: "".into(),
2384                }],
2385            )]
2386        );
2387
2388        action_log
2389            .update(cx, |log, cx| {
2390                let (task, _) = log.reject_edits_in_ranges(buffer.clone(), vec![2..5], None, cx);
2391                task
2392            })
2393            .await
2394            .unwrap();
2395        cx.run_until_parked();
2396        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2397        assert_eq!(
2398            buffer.read_with(cx, |buffer, _cx| buffer.text()),
2399            "Lorem ipsum dolor"
2400        );
2401    }
2402
2403    #[gpui::test(iterations = 10)]
2404    async fn test_deleting_files(cx: &mut TestAppContext) {
2405        init_test(cx);
2406
2407        let fs = FakeFs::new(cx.executor());
2408        fs.insert_tree(
2409            path!("/dir"),
2410            json!({"file1": "lorem\n", "file2": "ipsum\n"}),
2411        )
2412        .await;
2413
2414        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2415        let file1_path = project
2416            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
2417            .unwrap();
2418        let file2_path = project
2419            .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
2420            .unwrap();
2421
2422        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2423        let buffer1 = project
2424            .update(cx, |project, cx| {
2425                project.open_buffer(file1_path.clone(), cx)
2426            })
2427            .await
2428            .unwrap();
2429        let buffer2 = project
2430            .update(cx, |project, cx| {
2431                project.open_buffer(file2_path.clone(), cx)
2432            })
2433            .await
2434            .unwrap();
2435
2436        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
2437        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
2438        project
2439            .update(cx, |project, cx| {
2440                project.delete_file(file1_path.clone(), false, cx)
2441            })
2442            .unwrap()
2443            .await
2444            .unwrap();
2445        project
2446            .update(cx, |project, cx| {
2447                project.delete_file(file2_path.clone(), false, cx)
2448            })
2449            .unwrap()
2450            .await
2451            .unwrap();
2452        cx.run_until_parked();
2453        assert_eq!(
2454            unreviewed_hunks(&action_log, cx),
2455            vec![
2456                (
2457                    buffer1.clone(),
2458                    vec![HunkStatus {
2459                        range: Point::new(0, 0)..Point::new(0, 0),
2460                        diff_status: DiffHunkStatusKind::Deleted,
2461                        old_text: "lorem\n".into(),
2462                    }]
2463                ),
2464                (
2465                    buffer2.clone(),
2466                    vec![HunkStatus {
2467                        range: Point::new(0, 0)..Point::new(0, 0),
2468                        diff_status: DiffHunkStatusKind::Deleted,
2469                        old_text: "ipsum\n".into(),
2470                    }],
2471                )
2472            ]
2473        );
2474
2475        // Simulate file1 being recreated externally.
2476        fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
2477            .await;
2478
2479        // Simulate file2 being recreated by a tool.
2480        let buffer2 = project
2481            .update(cx, |project, cx| project.open_buffer(file2_path, cx))
2482            .await
2483            .unwrap();
2484        action_log.update(cx, |log, cx| log.buffer_created(buffer2.clone(), cx));
2485        buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
2486        action_log.update(cx, |log, cx| log.buffer_edited(buffer2.clone(), cx));
2487        project
2488            .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
2489            .await
2490            .unwrap();
2491
2492        cx.run_until_parked();
2493        assert_eq!(
2494            unreviewed_hunks(&action_log, cx),
2495            vec![(
2496                buffer2.clone(),
2497                vec![HunkStatus {
2498                    range: Point::new(0, 0)..Point::new(0, 5),
2499                    diff_status: DiffHunkStatusKind::Added,
2500                    old_text: "".into(),
2501                }],
2502            )]
2503        );
2504
2505        // Simulate file2 being deleted externally.
2506        fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
2507            .await
2508            .unwrap();
2509        cx.run_until_parked();
2510        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2511    }
2512
2513    #[gpui::test(iterations = 10)]
2514    async fn test_reject_edits(cx: &mut TestAppContext) {
2515        init_test(cx);
2516
2517        let fs = FakeFs::new(cx.executor());
2518        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
2519            .await;
2520        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2521        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2522        let file_path = project
2523            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
2524            .unwrap();
2525        let buffer = project
2526            .update(cx, |project, cx| project.open_buffer(file_path, cx))
2527            .await
2528            .unwrap();
2529
2530        cx.update(|cx| {
2531            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
2532            buffer.update(cx, |buffer, cx| {
2533                buffer
2534                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
2535                    .unwrap()
2536            });
2537            buffer.update(cx, |buffer, cx| {
2538                buffer
2539                    .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
2540                    .unwrap()
2541            });
2542            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2543        });
2544        cx.run_until_parked();
2545        assert_eq!(
2546            buffer.read_with(cx, |buffer, _| buffer.text()),
2547            "abc\ndE\nXYZf\nghi\njkl\nmnO"
2548        );
2549        assert_eq!(
2550            unreviewed_hunks(&action_log, cx),
2551            vec![(
2552                buffer.clone(),
2553                vec![
2554                    HunkStatus {
2555                        range: Point::new(1, 0)..Point::new(3, 0),
2556                        diff_status: DiffHunkStatusKind::Modified,
2557                        old_text: "def\n".into(),
2558                    },
2559                    HunkStatus {
2560                        range: Point::new(5, 0)..Point::new(5, 3),
2561                        diff_status: DiffHunkStatusKind::Modified,
2562                        old_text: "mno".into(),
2563                    }
2564                ],
2565            )]
2566        );
2567
2568        // If the rejected range doesn't overlap with any hunk, we ignore it.
2569        action_log
2570            .update(cx, |log, cx| {
2571                let (task, _) = log.reject_edits_in_ranges(
2572                    buffer.clone(),
2573                    vec![Point::new(4, 0)..Point::new(4, 0)],
2574                    None,
2575                    cx,
2576                );
2577                task
2578            })
2579            .await
2580            .unwrap();
2581        cx.run_until_parked();
2582        assert_eq!(
2583            buffer.read_with(cx, |buffer, _| buffer.text()),
2584            "abc\ndE\nXYZf\nghi\njkl\nmnO"
2585        );
2586        assert_eq!(
2587            unreviewed_hunks(&action_log, cx),
2588            vec![(
2589                buffer.clone(),
2590                vec![
2591                    HunkStatus {
2592                        range: Point::new(1, 0)..Point::new(3, 0),
2593                        diff_status: DiffHunkStatusKind::Modified,
2594                        old_text: "def\n".into(),
2595                    },
2596                    HunkStatus {
2597                        range: Point::new(5, 0)..Point::new(5, 3),
2598                        diff_status: DiffHunkStatusKind::Modified,
2599                        old_text: "mno".into(),
2600                    }
2601                ],
2602            )]
2603        );
2604
2605        action_log
2606            .update(cx, |log, cx| {
2607                let (task, _) = log.reject_edits_in_ranges(
2608                    buffer.clone(),
2609                    vec![Point::new(0, 0)..Point::new(1, 0)],
2610                    None,
2611                    cx,
2612                );
2613                task
2614            })
2615            .await
2616            .unwrap();
2617        cx.run_until_parked();
2618        assert_eq!(
2619            buffer.read_with(cx, |buffer, _| buffer.text()),
2620            "abc\ndef\nghi\njkl\nmnO"
2621        );
2622        assert_eq!(
2623            unreviewed_hunks(&action_log, cx),
2624            vec![(
2625                buffer.clone(),
2626                vec![HunkStatus {
2627                    range: Point::new(4, 0)..Point::new(4, 3),
2628                    diff_status: DiffHunkStatusKind::Modified,
2629                    old_text: "mno".into(),
2630                }],
2631            )]
2632        );
2633
2634        action_log
2635            .update(cx, |log, cx| {
2636                let (task, _) = log.reject_edits_in_ranges(
2637                    buffer.clone(),
2638                    vec![Point::new(4, 0)..Point::new(4, 0)],
2639                    None,
2640                    cx,
2641                );
2642                task
2643            })
2644            .await
2645            .unwrap();
2646        cx.run_until_parked();
2647        assert_eq!(
2648            buffer.read_with(cx, |buffer, _| buffer.text()),
2649            "abc\ndef\nghi\njkl\nmno"
2650        );
2651        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2652    }
2653
2654    #[gpui::test(iterations = 10)]
2655    async fn test_reject_multiple_edits(cx: &mut TestAppContext) {
2656        init_test(cx);
2657
2658        let fs = FakeFs::new(cx.executor());
2659        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
2660            .await;
2661        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2662        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2663        let file_path = project
2664            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
2665            .unwrap();
2666        let buffer = project
2667            .update(cx, |project, cx| project.open_buffer(file_path, cx))
2668            .await
2669            .unwrap();
2670
2671        cx.update(|cx| {
2672            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
2673            buffer.update(cx, |buffer, cx| {
2674                buffer
2675                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
2676                    .unwrap()
2677            });
2678            buffer.update(cx, |buffer, cx| {
2679                buffer
2680                    .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
2681                    .unwrap()
2682            });
2683            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2684        });
2685        cx.run_until_parked();
2686        assert_eq!(
2687            buffer.read_with(cx, |buffer, _| buffer.text()),
2688            "abc\ndE\nXYZf\nghi\njkl\nmnO"
2689        );
2690        assert_eq!(
2691            unreviewed_hunks(&action_log, cx),
2692            vec![(
2693                buffer.clone(),
2694                vec![
2695                    HunkStatus {
2696                        range: Point::new(1, 0)..Point::new(3, 0),
2697                        diff_status: DiffHunkStatusKind::Modified,
2698                        old_text: "def\n".into(),
2699                    },
2700                    HunkStatus {
2701                        range: Point::new(5, 0)..Point::new(5, 3),
2702                        diff_status: DiffHunkStatusKind::Modified,
2703                        old_text: "mno".into(),
2704                    }
2705                ],
2706            )]
2707        );
2708
2709        action_log.update(cx, |log, cx| {
2710            let range_1 = buffer.read(cx).anchor_before(Point::new(0, 0))
2711                ..buffer.read(cx).anchor_before(Point::new(1, 0));
2712            let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0))
2713                ..buffer.read(cx).anchor_before(Point::new(5, 3));
2714
2715            let (task, _) =
2716                log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], None, cx);
2717            task.detach();
2718            assert_eq!(
2719                buffer.read_with(cx, |buffer, _| buffer.text()),
2720                "abc\ndef\nghi\njkl\nmno"
2721            );
2722        });
2723        cx.run_until_parked();
2724        assert_eq!(
2725            buffer.read_with(cx, |buffer, _| buffer.text()),
2726            "abc\ndef\nghi\njkl\nmno"
2727        );
2728        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2729    }
2730
2731    #[gpui::test(iterations = 10)]
2732    async fn test_reject_deleted_file(cx: &mut TestAppContext) {
2733        init_test(cx);
2734
2735        let fs = FakeFs::new(cx.executor());
2736        fs.insert_tree(path!("/dir"), json!({"file": "content"}))
2737            .await;
2738        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2739        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2740        let file_path = project
2741            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
2742            .unwrap();
2743        let buffer = project
2744            .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
2745            .await
2746            .unwrap();
2747
2748        cx.update(|cx| {
2749            action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx));
2750        });
2751        project
2752            .update(cx, |project, cx| {
2753                project.delete_file(file_path.clone(), false, cx)
2754            })
2755            .unwrap()
2756            .await
2757            .unwrap();
2758        cx.run_until_parked();
2759        assert!(!fs.is_file(path!("/dir/file").as_ref()).await);
2760        assert_eq!(
2761            unreviewed_hunks(&action_log, cx),
2762            vec![(
2763                buffer.clone(),
2764                vec![HunkStatus {
2765                    range: Point::new(0, 0)..Point::new(0, 0),
2766                    diff_status: DiffHunkStatusKind::Deleted,
2767                    old_text: "content".into(),
2768                }]
2769            )]
2770        );
2771
2772        action_log
2773            .update(cx, |log, cx| {
2774                let (task, _) = log.reject_edits_in_ranges(
2775                    buffer.clone(),
2776                    vec![Point::new(0, 0)..Point::new(0, 0)],
2777                    None,
2778                    cx,
2779                );
2780                task
2781            })
2782            .await
2783            .unwrap();
2784        cx.run_until_parked();
2785        assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "content");
2786        assert!(fs.is_file(path!("/dir/file").as_ref()).await);
2787        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2788    }
2789
2790    #[gpui::test(iterations = 10)]
2791    async fn test_reject_created_file(cx: &mut TestAppContext) {
2792        init_test(cx);
2793
2794        let fs = FakeFs::new(cx.executor());
2795        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2796        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2797        let file_path = project
2798            .read_with(cx, |project, cx| {
2799                project.find_project_path("dir/new_file", cx)
2800            })
2801            .unwrap();
2802        let buffer = project
2803            .update(cx, |project, cx| project.open_buffer(file_path, cx))
2804            .await
2805            .unwrap();
2806        cx.update(|cx| {
2807            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
2808            buffer.update(cx, |buffer, cx| buffer.set_text("content", cx));
2809            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2810        });
2811        project
2812            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2813            .await
2814            .unwrap();
2815        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
2816        cx.run_until_parked();
2817        assert_eq!(
2818            unreviewed_hunks(&action_log, cx),
2819            vec![(
2820                buffer.clone(),
2821                vec![HunkStatus {
2822                    range: Point::new(0, 0)..Point::new(0, 7),
2823                    diff_status: DiffHunkStatusKind::Added,
2824                    old_text: "".into(),
2825                }],
2826            )]
2827        );
2828
2829        action_log
2830            .update(cx, |log, cx| {
2831                let (task, _) = log.reject_edits_in_ranges(
2832                    buffer.clone(),
2833                    vec![Point::new(0, 0)..Point::new(0, 11)],
2834                    None,
2835                    cx,
2836                );
2837                task
2838            })
2839            .await
2840            .unwrap();
2841        cx.run_until_parked();
2842        assert!(!fs.is_file(path!("/dir/new_file").as_ref()).await);
2843        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2844    }
2845
2846    #[gpui::test]
2847    async fn test_reject_created_file_with_user_edits(cx: &mut TestAppContext) {
2848        init_test(cx);
2849
2850        let fs = FakeFs::new(cx.executor());
2851        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2852        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2853
2854        let file_path = project
2855            .read_with(cx, |project, cx| {
2856                project.find_project_path("dir/new_file", cx)
2857            })
2858            .unwrap();
2859        let buffer = project
2860            .update(cx, |project, cx| project.open_buffer(file_path, cx))
2861            .await
2862            .unwrap();
2863
2864        // AI creates file with initial content
2865        cx.update(|cx| {
2866            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
2867            buffer.update(cx, |buffer, cx| buffer.set_text("ai content", cx));
2868            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2869        });
2870
2871        project
2872            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2873            .await
2874            .unwrap();
2875
2876        cx.run_until_parked();
2877
2878        // User makes additional edits
2879        cx.update(|cx| {
2880            buffer.update(cx, |buffer, cx| {
2881                buffer.edit([(10..10, "\nuser added this line")], None, cx);
2882            });
2883        });
2884
2885        project
2886            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2887            .await
2888            .unwrap();
2889
2890        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
2891
2892        // Reject all
2893        action_log
2894            .update(cx, |log, cx| {
2895                let (task, _) = log.reject_edits_in_ranges(
2896                    buffer.clone(),
2897                    vec![Point::new(0, 0)..Point::new(100, 0)],
2898                    None,
2899                    cx,
2900                );
2901                task
2902            })
2903            .await
2904            .unwrap();
2905        cx.run_until_parked();
2906
2907        // File should still contain all the content
2908        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
2909
2910        let content = buffer.read_with(cx, |buffer, _| buffer.text());
2911        assert_eq!(content, "ai content\nuser added this line");
2912    }
2913
2914    #[gpui::test]
2915    async fn test_reject_after_accepting_hunk_on_created_file(cx: &mut TestAppContext) {
2916        init_test(cx);
2917
2918        let fs = FakeFs::new(cx.executor());
2919        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2920        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2921
2922        let file_path = project
2923            .read_with(cx, |project, cx| {
2924                project.find_project_path("dir/new_file", cx)
2925            })
2926            .unwrap();
2927        let buffer = project
2928            .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
2929            .await
2930            .unwrap();
2931
2932        // AI creates file with initial content
2933        cx.update(|cx| {
2934            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
2935            buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx));
2936            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2937        });
2938        project
2939            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2940            .await
2941            .unwrap();
2942        cx.run_until_parked();
2943        assert_ne!(unreviewed_hunks(&action_log, cx), vec![]);
2944
2945        // User accepts the single hunk
2946        action_log.update(cx, |log, cx| {
2947            let buffer_range = Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id());
2948            log.keep_edits_in_range(buffer.clone(), buffer_range, None, cx)
2949        });
2950        cx.run_until_parked();
2951        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2952        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
2953
2954        // AI modifies the file
2955        cx.update(|cx| {
2956            buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx));
2957            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2958        });
2959        project
2960            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2961            .await
2962            .unwrap();
2963        cx.run_until_parked();
2964        assert_ne!(unreviewed_hunks(&action_log, cx), vec![]);
2965
2966        // User rejects the hunk
2967        action_log
2968            .update(cx, |log, cx| {
2969                let (task, _) = log.reject_edits_in_ranges(
2970                    buffer.clone(),
2971                    vec![Anchor::min_max_range_for_buffer(
2972                        buffer.read(cx).remote_id(),
2973                    )],
2974                    None,
2975                    cx,
2976                );
2977                task
2978            })
2979            .await
2980            .unwrap();
2981        cx.run_until_parked();
2982        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await,);
2983        assert_eq!(
2984            buffer.read_with(cx, |buffer, _| buffer.text()),
2985            "ai content v1"
2986        );
2987        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2988    }
2989
2990    #[gpui::test]
2991    async fn test_reject_edits_on_previously_accepted_created_file(cx: &mut TestAppContext) {
2992        init_test(cx);
2993
2994        let fs = FakeFs::new(cx.executor());
2995        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2996        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2997
2998        let file_path = project
2999            .read_with(cx, |project, cx| {
3000                project.find_project_path("dir/new_file", cx)
3001            })
3002            .unwrap();
3003        let buffer = project
3004            .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
3005            .await
3006            .unwrap();
3007
3008        // AI creates file with initial content
3009        cx.update(|cx| {
3010            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
3011            buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx));
3012            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
3013        });
3014        project
3015            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
3016            .await
3017            .unwrap();
3018        cx.run_until_parked();
3019
3020        // User clicks "Accept All"
3021        action_log.update(cx, |log, cx| log.keep_all_edits(None, cx));
3022        cx.run_until_parked();
3023        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
3024        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); // Hunks are cleared
3025
3026        // AI modifies file again
3027        cx.update(|cx| {
3028            buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx));
3029            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
3030        });
3031        project
3032            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
3033            .await
3034            .unwrap();
3035        cx.run_until_parked();
3036        assert_ne!(unreviewed_hunks(&action_log, cx), vec![]);
3037
3038        // User clicks "Reject All"
3039        action_log
3040            .update(cx, |log, cx| log.reject_all_edits(None, cx))
3041            .await;
3042        cx.run_until_parked();
3043        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
3044        assert_eq!(
3045            buffer.read_with(cx, |buffer, _| buffer.text()),
3046            "ai content v1"
3047        );
3048        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
3049    }
3050
3051    #[gpui::test(iterations = 100)]
3052    async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
3053        init_test(cx);
3054
3055        let operations = env::var("OPERATIONS")
3056            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
3057            .unwrap_or(20);
3058
3059        let text = RandomCharIter::new(&mut rng).take(50).collect::<String>();
3060        let fs = FakeFs::new(cx.executor());
3061        fs.insert_tree(path!("/dir"), json!({"file": text})).await;
3062        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3063        let action_log = cx.new(|_| ActionLog::new(project.clone()));
3064        let file_path = project
3065            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3066            .unwrap();
3067        let buffer = project
3068            .update(cx, |project, cx| project.open_buffer(file_path, cx))
3069            .await
3070            .unwrap();
3071
3072        action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
3073
3074        for _ in 0..operations {
3075            match rng.random_range(0..100) {
3076                0..25 => {
3077                    action_log.update(cx, |log, cx| {
3078                        let range = buffer.read(cx).random_byte_range(0, &mut rng);
3079                        log::info!("keeping edits in range {:?}", range);
3080                        log.keep_edits_in_range(buffer.clone(), range, None, cx)
3081                    });
3082                }
3083                25..50 => {
3084                    action_log
3085                        .update(cx, |log, cx| {
3086                            let range = buffer.read(cx).random_byte_range(0, &mut rng);
3087                            log::info!("rejecting edits in range {:?}", range);
3088                            let (task, _) =
3089                                log.reject_edits_in_ranges(buffer.clone(), vec![range], None, cx);
3090                            task
3091                        })
3092                        .await
3093                        .unwrap();
3094                }
3095                _ => {
3096                    let is_agent_edit = rng.random_bool(0.5);
3097                    if is_agent_edit {
3098                        log::info!("agent edit");
3099                    } else {
3100                        log::info!("user edit");
3101                    }
3102                    cx.update(|cx| {
3103                        buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
3104                        if is_agent_edit {
3105                            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
3106                        }
3107                    });
3108                }
3109            }
3110
3111            if rng.random_bool(0.2) {
3112                quiesce(&action_log, &buffer, cx);
3113            }
3114        }
3115
3116        quiesce(&action_log, &buffer, cx);
3117
3118        fn quiesce(
3119            action_log: &Entity<ActionLog>,
3120            buffer: &Entity<Buffer>,
3121            cx: &mut TestAppContext,
3122        ) {
3123            log::info!("quiescing...");
3124            cx.run_until_parked();
3125            action_log.update(cx, |log, cx| {
3126                let tracked_buffer = log.tracked_buffers.get(buffer).unwrap();
3127                let mut old_text = tracked_buffer.diff_base.clone();
3128                let new_text = buffer.read(cx).as_rope();
3129                for edit in tracked_buffer.unreviewed_edits.edits() {
3130                    let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
3131                    let old_end = old_text.point_to_offset(cmp::min(
3132                        Point::new(edit.new.start + edit.old_len(), 0),
3133                        old_text.max_point(),
3134                    ));
3135                    old_text.replace(
3136                        old_start..old_end,
3137                        &new_text.slice_rows(edit.new.clone()).to_string(),
3138                    );
3139                }
3140                pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
3141            })
3142        }
3143    }
3144
3145    #[gpui::test]
3146    async fn test_keep_edits_on_commit(cx: &mut gpui::TestAppContext) {
3147        init_test(cx);
3148
3149        let fs = FakeFs::new(cx.background_executor.clone());
3150        fs.insert_tree(
3151            path!("/project"),
3152            json!({
3153                ".git": {},
3154                "file.txt": "a\nb\nc\nd\ne\nf\ng\nh\ni\nj",
3155            }),
3156        )
3157        .await;
3158        fs.set_head_for_repo(
3159            path!("/project/.git").as_ref(),
3160            &[("file.txt", "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())],
3161            "0000000",
3162        );
3163        cx.run_until_parked();
3164
3165        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
3166        let action_log = cx.new(|_| ActionLog::new(project.clone()));
3167
3168        let file_path = project
3169            .read_with(cx, |project, cx| {
3170                project.find_project_path(path!("/project/file.txt"), cx)
3171            })
3172            .unwrap();
3173        let buffer = project
3174            .update(cx, |project, cx| project.open_buffer(file_path, cx))
3175            .await
3176            .unwrap();
3177
3178        cx.update(|cx| {
3179            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
3180            buffer.update(cx, |buffer, cx| {
3181                buffer.edit(
3182                    [
3183                        // Edit at the very start: a -> A
3184                        (Point::new(0, 0)..Point::new(0, 1), "A"),
3185                        // Deletion in the middle: remove lines d and e
3186                        (Point::new(3, 0)..Point::new(5, 0), ""),
3187                        // Modification: g -> GGG
3188                        (Point::new(6, 0)..Point::new(6, 1), "GGG"),
3189                        // Addition: insert new line after h
3190                        (Point::new(7, 1)..Point::new(7, 1), "\nNEW"),
3191                        // Edit the very last character: j -> J
3192                        (Point::new(9, 0)..Point::new(9, 1), "J"),
3193                    ],
3194                    None,
3195                    cx,
3196                );
3197            });
3198            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
3199        });
3200        cx.run_until_parked();
3201        assert_eq!(
3202            unreviewed_hunks(&action_log, cx),
3203            vec![(
3204                buffer.clone(),
3205                vec![
3206                    HunkStatus {
3207                        range: Point::new(0, 0)..Point::new(1, 0),
3208                        diff_status: DiffHunkStatusKind::Modified,
3209                        old_text: "a\n".into()
3210                    },
3211                    HunkStatus {
3212                        range: Point::new(3, 0)..Point::new(3, 0),
3213                        diff_status: DiffHunkStatusKind::Deleted,
3214                        old_text: "d\ne\n".into()
3215                    },
3216                    HunkStatus {
3217                        range: Point::new(4, 0)..Point::new(5, 0),
3218                        diff_status: DiffHunkStatusKind::Modified,
3219                        old_text: "g\n".into()
3220                    },
3221                    HunkStatus {
3222                        range: Point::new(6, 0)..Point::new(7, 0),
3223                        diff_status: DiffHunkStatusKind::Added,
3224                        old_text: "".into()
3225                    },
3226                    HunkStatus {
3227                        range: Point::new(8, 0)..Point::new(8, 1),
3228                        diff_status: DiffHunkStatusKind::Modified,
3229                        old_text: "j".into()
3230                    }
3231                ]
3232            )]
3233        );
3234
3235        // Simulate a git commit that matches some edits but not others:
3236        // - Accepts the first edit (a -> A)
3237        // - Accepts the deletion (remove d and e)
3238        // - Makes a different change to g (g -> G instead of GGG)
3239        // - Ignores the NEW line addition
3240        // - Ignores the last line edit (j stays as j)
3241        fs.set_head_for_repo(
3242            path!("/project/.git").as_ref(),
3243            &[("file.txt", "A\nb\nc\nf\nG\nh\ni\nj".into())],
3244            "0000001",
3245        );
3246        cx.run_until_parked();
3247        assert_eq!(
3248            unreviewed_hunks(&action_log, cx),
3249            vec![(
3250                buffer.clone(),
3251                vec![
3252                    HunkStatus {
3253                        range: Point::new(4, 0)..Point::new(5, 0),
3254                        diff_status: DiffHunkStatusKind::Modified,
3255                        old_text: "g\n".into()
3256                    },
3257                    HunkStatus {
3258                        range: Point::new(6, 0)..Point::new(7, 0),
3259                        diff_status: DiffHunkStatusKind::Added,
3260                        old_text: "".into()
3261                    },
3262                    HunkStatus {
3263                        range: Point::new(8, 0)..Point::new(8, 1),
3264                        diff_status: DiffHunkStatusKind::Modified,
3265                        old_text: "j".into()
3266                    }
3267                ]
3268            )]
3269        );
3270
3271        // Make another commit that accepts the NEW line but with different content
3272        fs.set_head_for_repo(
3273            path!("/project/.git").as_ref(),
3274            &[("file.txt", "A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into())],
3275            "0000002",
3276        );
3277        cx.run_until_parked();
3278        assert_eq!(
3279            unreviewed_hunks(&action_log, cx),
3280            vec![(
3281                buffer,
3282                vec![
3283                    HunkStatus {
3284                        range: Point::new(6, 0)..Point::new(7, 0),
3285                        diff_status: DiffHunkStatusKind::Added,
3286                        old_text: "".into()
3287                    },
3288                    HunkStatus {
3289                        range: Point::new(8, 0)..Point::new(8, 1),
3290                        diff_status: DiffHunkStatusKind::Modified,
3291                        old_text: "j".into()
3292                    }
3293                ]
3294            )]
3295        );
3296
3297        // Final commit that accepts all remaining edits
3298        fs.set_head_for_repo(
3299            path!("/project/.git").as_ref(),
3300            &[("file.txt", "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())],
3301            "0000003",
3302        );
3303        cx.run_until_parked();
3304        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
3305    }
3306
3307    #[gpui::test]
3308    async fn test_undo_last_reject(cx: &mut TestAppContext) {
3309        init_test(cx);
3310
3311        let fs = FakeFs::new(cx.executor());
3312        fs.insert_tree(
3313            path!("/dir"),
3314            json!({
3315                "file1": "abc\ndef\nghi"
3316            }),
3317        )
3318        .await;
3319        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3320        let action_log = cx.new(|_| ActionLog::new(project.clone()));
3321        let file_path = project
3322            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
3323            .unwrap();
3324
3325        let buffer = project
3326            .update(cx, |project, cx| project.open_buffer(file_path, cx))
3327            .await
3328            .unwrap();
3329
3330        // Track the buffer and make an agent edit
3331        cx.update(|cx| {
3332            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
3333            buffer.update(cx, |buffer, cx| {
3334                buffer
3335                    .edit(
3336                        [(Point::new(1, 0)..Point::new(1, 3), "AGENT_EDIT")],
3337                        None,
3338                        cx,
3339                    )
3340                    .unwrap()
3341            });
3342            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
3343        });
3344        cx.run_until_parked();
3345
3346        // Verify the agent edit is there
3347        assert_eq!(
3348            buffer.read_with(cx, |buffer, _| buffer.text()),
3349            "abc\nAGENT_EDIT\nghi"
3350        );
3351        assert!(!unreviewed_hunks(&action_log, cx).is_empty());
3352
3353        // Reject all edits
3354        action_log
3355            .update(cx, |log, cx| log.reject_all_edits(None, cx))
3356            .await;
3357        cx.run_until_parked();
3358
3359        // Verify the buffer is back to original
3360        assert_eq!(
3361            buffer.read_with(cx, |buffer, _| buffer.text()),
3362            "abc\ndef\nghi"
3363        );
3364        assert!(unreviewed_hunks(&action_log, cx).is_empty());
3365
3366        // Verify undo state is available
3367        assert!(action_log.read_with(cx, |log, _| log.has_pending_undo()));
3368
3369        // Undo the reject
3370        action_log
3371            .update(cx, |log, cx| log.undo_last_reject(cx))
3372            .await;
3373
3374        cx.run_until_parked();
3375
3376        // Verify the agent edit is restored
3377        assert_eq!(
3378            buffer.read_with(cx, |buffer, _| buffer.text()),
3379            "abc\nAGENT_EDIT\nghi"
3380        );
3381
3382        // Verify undo state is cleared
3383        assert!(!action_log.read_with(cx, |log, _| log.has_pending_undo()));
3384    }
3385
3386    #[gpui::test]
3387    async fn test_linked_action_log_buffer_read(cx: &mut TestAppContext) {
3388        init_test(cx);
3389
3390        let fs = FakeFs::new(cx.executor());
3391        fs.insert_tree(path!("/dir"), json!({"file": "hello world"}))
3392            .await;
3393        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3394        let parent_log = cx.new(|_| ActionLog::new(project.clone()));
3395        let child_log =
3396            cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
3397
3398        let file_path = project
3399            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3400            .unwrap();
3401        let buffer = project
3402            .update(cx, |project, cx| project.open_buffer(file_path, cx))
3403            .await
3404            .unwrap();
3405
3406        cx.update(|cx| {
3407            child_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
3408        });
3409
3410        // Neither log considers the buffer stale immediately after reading it.
3411        let child_stale = cx.read(|cx| {
3412            child_log
3413                .read(cx)
3414                .stale_buffers(cx)
3415                .cloned()
3416                .collect::<Vec<_>>()
3417        });
3418        let parent_stale = cx.read(|cx| {
3419            parent_log
3420                .read(cx)
3421                .stale_buffers(cx)
3422                .cloned()
3423                .collect::<Vec<_>>()
3424        });
3425        assert!(child_stale.is_empty());
3426        assert!(parent_stale.is_empty());
3427
3428        // Simulate a user edit after the agent read the file.
3429        cx.update(|cx| {
3430            buffer.update(cx, |buffer, cx| {
3431                buffer.edit([(0..5, "goodbye")], None, cx).unwrap();
3432            });
3433        });
3434        cx.run_until_parked();
3435
3436        // Both child and parent should see the buffer as stale because both tracked
3437        // it at the pre-edit version via buffer_read forwarding.
3438        let child_stale = cx.read(|cx| {
3439            child_log
3440                .read(cx)
3441                .stale_buffers(cx)
3442                .cloned()
3443                .collect::<Vec<_>>()
3444        });
3445        let parent_stale = cx.read(|cx| {
3446            parent_log
3447                .read(cx)
3448                .stale_buffers(cx)
3449                .cloned()
3450                .collect::<Vec<_>>()
3451        });
3452        assert_eq!(child_stale, vec![buffer.clone()]);
3453        assert_eq!(parent_stale, vec![buffer]);
3454    }
3455
3456    #[gpui::test]
3457    async fn test_linked_action_log_buffer_edited(cx: &mut TestAppContext) {
3458        init_test(cx);
3459
3460        let fs = FakeFs::new(cx.executor());
3461        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi"}))
3462            .await;
3463        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3464        let parent_log = cx.new(|_| ActionLog::new(project.clone()));
3465        let child_log =
3466            cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
3467
3468        let file_path = project
3469            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3470            .unwrap();
3471        let buffer = project
3472            .update(cx, |project, cx| project.open_buffer(file_path, cx))
3473            .await
3474            .unwrap();
3475
3476        cx.update(|cx| {
3477            child_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
3478            buffer.update(cx, |buffer, cx| {
3479                buffer
3480                    .edit([(Point::new(1, 0)..Point::new(1, 3), "DEF")], None, cx)
3481                    .unwrap();
3482            });
3483            child_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
3484        });
3485        cx.run_until_parked();
3486
3487        let expected_hunks = vec![(
3488            buffer,
3489            vec![HunkStatus {
3490                range: Point::new(1, 0)..Point::new(2, 0),
3491                diff_status: DiffHunkStatusKind::Modified,
3492                old_text: "def\n".into(),
3493            }],
3494        )];
3495        assert_eq!(
3496            unreviewed_hunks(&child_log, cx),
3497            expected_hunks,
3498            "child should track the agent edit"
3499        );
3500        assert_eq!(
3501            unreviewed_hunks(&parent_log, cx),
3502            expected_hunks,
3503            "parent should also track the agent edit via linked log forwarding"
3504        );
3505    }
3506
3507    #[gpui::test]
3508    async fn test_linked_action_log_buffer_created(cx: &mut TestAppContext) {
3509        init_test(cx);
3510
3511        let fs = FakeFs::new(cx.executor());
3512        fs.insert_tree(path!("/dir"), json!({})).await;
3513        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3514        let parent_log = cx.new(|_| ActionLog::new(project.clone()));
3515        let child_log =
3516            cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
3517
3518        let file_path = project
3519            .read_with(cx, |project, cx| {
3520                project.find_project_path("dir/new_file", cx)
3521            })
3522            .unwrap();
3523        let buffer = project
3524            .update(cx, |project, cx| project.open_buffer(file_path, cx))
3525            .await
3526            .unwrap();
3527
3528        cx.update(|cx| {
3529            child_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
3530            buffer.update(cx, |buffer, cx| buffer.set_text("hello", cx));
3531            child_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
3532        });
3533        project
3534            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
3535            .await
3536            .unwrap();
3537        cx.run_until_parked();
3538
3539        let expected_hunks = vec![(
3540            buffer.clone(),
3541            vec![HunkStatus {
3542                range: Point::new(0, 0)..Point::new(0, 5),
3543                diff_status: DiffHunkStatusKind::Added,
3544                old_text: "".into(),
3545            }],
3546        )];
3547        assert_eq!(
3548            unreviewed_hunks(&child_log, cx),
3549            expected_hunks,
3550            "child should track the created file"
3551        );
3552        assert_eq!(
3553            unreviewed_hunks(&parent_log, cx),
3554            expected_hunks,
3555            "parent should also track the created file via linked log forwarding"
3556        );
3557    }
3558
3559    #[gpui::test]
3560    async fn test_linked_action_log_will_delete_buffer(cx: &mut TestAppContext) {
3561        init_test(cx);
3562
3563        let fs = FakeFs::new(cx.executor());
3564        fs.insert_tree(path!("/dir"), json!({"file": "hello\n"}))
3565            .await;
3566        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3567        let parent_log = cx.new(|_| ActionLog::new(project.clone()));
3568        let child_log =
3569            cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
3570
3571        let file_path = project
3572            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3573            .unwrap();
3574        let buffer = project
3575            .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
3576            .await
3577            .unwrap();
3578
3579        cx.update(|cx| {
3580            child_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx));
3581        });
3582        project
3583            .update(cx, |project, cx| project.delete_file(file_path, false, cx))
3584            .unwrap()
3585            .await
3586            .unwrap();
3587        cx.run_until_parked();
3588
3589        let expected_hunks = vec![(
3590            buffer.clone(),
3591            vec![HunkStatus {
3592                range: Point::new(0, 0)..Point::new(0, 0),
3593                diff_status: DiffHunkStatusKind::Deleted,
3594                old_text: "hello\n".into(),
3595            }],
3596        )];
3597        assert_eq!(
3598            unreviewed_hunks(&child_log, cx),
3599            expected_hunks,
3600            "child should track the deleted file"
3601        );
3602        assert_eq!(
3603            unreviewed_hunks(&parent_log, cx),
3604            expected_hunks,
3605            "parent should also track the deleted file via linked log forwarding"
3606        );
3607    }
3608
3609    /// Simulates the subagent scenario: two child logs linked to the same parent, each
3610    /// editing a different file. The parent accumulates all edits while each child
3611    /// only sees its own.
3612    #[gpui::test]
3613    async fn test_linked_action_log_independent_tracking(cx: &mut TestAppContext) {
3614        init_test(cx);
3615
3616        let fs = FakeFs::new(cx.executor());
3617        fs.insert_tree(
3618            path!("/dir"),
3619            json!({
3620                "file_a": "content of a",
3621                "file_b": "content of b",
3622            }),
3623        )
3624        .await;
3625        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3626        let parent_log = cx.new(|_| ActionLog::new(project.clone()));
3627        let child_log_1 =
3628            cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
3629        let child_log_2 =
3630            cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
3631
3632        let file_a_path = project
3633            .read_with(cx, |project, cx| {
3634                project.find_project_path("dir/file_a", cx)
3635            })
3636            .unwrap();
3637        let file_b_path = project
3638            .read_with(cx, |project, cx| {
3639                project.find_project_path("dir/file_b", cx)
3640            })
3641            .unwrap();
3642        let buffer_a = project
3643            .update(cx, |project, cx| project.open_buffer(file_a_path, cx))
3644            .await
3645            .unwrap();
3646        let buffer_b = project
3647            .update(cx, |project, cx| project.open_buffer(file_b_path, cx))
3648            .await
3649            .unwrap();
3650
3651        cx.update(|cx| {
3652            child_log_1.update(cx, |log, cx| log.buffer_read(buffer_a.clone(), cx));
3653            buffer_a.update(cx, |buffer, cx| {
3654                buffer.edit([(0..0, "MODIFIED: ")], None, cx).unwrap();
3655            });
3656            child_log_1.update(cx, |log, cx| log.buffer_edited(buffer_a.clone(), cx));
3657
3658            child_log_2.update(cx, |log, cx| log.buffer_read(buffer_b.clone(), cx));
3659            buffer_b.update(cx, |buffer, cx| {
3660                buffer.edit([(0..0, "MODIFIED: ")], None, cx).unwrap();
3661            });
3662            child_log_2.update(cx, |log, cx| log.buffer_edited(buffer_b.clone(), cx));
3663        });
3664        cx.run_until_parked();
3665
3666        let child_1_changed: Vec<_> = cx.read(|cx| {
3667            child_log_1
3668                .read(cx)
3669                .changed_buffers(cx)
3670                .into_keys()
3671                .collect()
3672        });
3673        let child_2_changed: Vec<_> = cx.read(|cx| {
3674            child_log_2
3675                .read(cx)
3676                .changed_buffers(cx)
3677                .into_keys()
3678                .collect()
3679        });
3680        let parent_changed: Vec<_> = cx.read(|cx| {
3681            parent_log
3682                .read(cx)
3683                .changed_buffers(cx)
3684                .into_keys()
3685                .collect()
3686        });
3687
3688        assert_eq!(
3689            child_1_changed,
3690            vec![buffer_a.clone()],
3691            "child 1 should only track file_a"
3692        );
3693        assert_eq!(
3694            child_2_changed,
3695            vec![buffer_b.clone()],
3696            "child 2 should only track file_b"
3697        );
3698        assert_eq!(parent_changed.len(), 2, "parent should track both files");
3699        assert!(
3700            parent_changed.contains(&buffer_a) && parent_changed.contains(&buffer_b),
3701            "parent should contain both buffer_a and buffer_b"
3702        );
3703    }
3704
3705    #[gpui::test]
3706    async fn test_file_read_time_recorded_on_buffer_read(cx: &mut TestAppContext) {
3707        init_test(cx);
3708
3709        let fs = FakeFs::new(cx.executor());
3710        fs.insert_tree(path!("/dir"), json!({"file": "hello world"}))
3711            .await;
3712        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3713        let action_log = cx.new(|_| ActionLog::new(project.clone()));
3714
3715        let file_path = project
3716            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3717            .unwrap();
3718        let buffer = project
3719            .update(cx, |project, cx| project.open_buffer(file_path, cx))
3720            .await
3721            .unwrap();
3722
3723        let abs_path = PathBuf::from(path!("/dir/file"));
3724        assert!(
3725            action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
3726            "file_read_time should be None before buffer_read"
3727        );
3728
3729        cx.update(|cx| {
3730            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
3731        });
3732
3733        assert!(
3734            action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()),
3735            "file_read_time should be recorded after buffer_read"
3736        );
3737    }
3738
3739    #[gpui::test]
3740    async fn test_file_read_time_recorded_on_buffer_edited(cx: &mut TestAppContext) {
3741        init_test(cx);
3742
3743        let fs = FakeFs::new(cx.executor());
3744        fs.insert_tree(path!("/dir"), json!({"file": "hello world"}))
3745            .await;
3746        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3747        let action_log = cx.new(|_| ActionLog::new(project.clone()));
3748
3749        let file_path = project
3750            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3751            .unwrap();
3752        let buffer = project
3753            .update(cx, |project, cx| project.open_buffer(file_path, cx))
3754            .await
3755            .unwrap();
3756
3757        let abs_path = PathBuf::from(path!("/dir/file"));
3758        assert!(
3759            action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
3760            "file_read_time should be None before buffer_edited"
3761        );
3762
3763        cx.update(|cx| {
3764            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
3765        });
3766
3767        assert!(
3768            action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()),
3769            "file_read_time should be recorded after buffer_edited"
3770        );
3771    }
3772
3773    #[gpui::test]
3774    async fn test_file_read_time_recorded_on_buffer_created(cx: &mut TestAppContext) {
3775        init_test(cx);
3776
3777        let fs = FakeFs::new(cx.executor());
3778        fs.insert_tree(path!("/dir"), json!({"file": "existing content"}))
3779            .await;
3780        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3781        let action_log = cx.new(|_| ActionLog::new(project.clone()));
3782
3783        let file_path = project
3784            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3785            .unwrap();
3786        let buffer = project
3787            .update(cx, |project, cx| project.open_buffer(file_path, cx))
3788            .await
3789            .unwrap();
3790
3791        let abs_path = PathBuf::from(path!("/dir/file"));
3792        assert!(
3793            action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
3794            "file_read_time should be None before buffer_created"
3795        );
3796
3797        cx.update(|cx| {
3798            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
3799        });
3800
3801        assert!(
3802            action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()),
3803            "file_read_time should be recorded after buffer_created"
3804        );
3805    }
3806
3807    #[gpui::test]
3808    async fn test_file_read_time_removed_on_delete(cx: &mut TestAppContext) {
3809        init_test(cx);
3810
3811        let fs = FakeFs::new(cx.executor());
3812        fs.insert_tree(path!("/dir"), json!({"file": "hello world"}))
3813            .await;
3814        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3815        let action_log = cx.new(|_| ActionLog::new(project.clone()));
3816
3817        let file_path = project
3818            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3819            .unwrap();
3820        let buffer = project
3821            .update(cx, |project, cx| project.open_buffer(file_path, cx))
3822            .await
3823            .unwrap();
3824
3825        let abs_path = PathBuf::from(path!("/dir/file"));
3826
3827        cx.update(|cx| {
3828            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
3829        });
3830        assert!(
3831            action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()),
3832            "file_read_time should exist after buffer_read"
3833        );
3834
3835        cx.update(|cx| {
3836            action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx));
3837        });
3838        assert!(
3839            action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
3840            "file_read_time should be removed after will_delete_buffer"
3841        );
3842    }
3843
3844    #[gpui::test]
3845    async fn test_file_read_time_not_forwarded_to_linked_action_log(cx: &mut TestAppContext) {
3846        init_test(cx);
3847
3848        let fs = FakeFs::new(cx.executor());
3849        fs.insert_tree(path!("/dir"), json!({"file": "hello world"}))
3850            .await;
3851        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3852        let parent_log = cx.new(|_| ActionLog::new(project.clone()));
3853        let child_log =
3854            cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
3855
3856        let file_path = project
3857            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3858            .unwrap();
3859        let buffer = project
3860            .update(cx, |project, cx| project.open_buffer(file_path, cx))
3861            .await
3862            .unwrap();
3863
3864        let abs_path = PathBuf::from(path!("/dir/file"));
3865
3866        cx.update(|cx| {
3867            child_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
3868        });
3869        assert!(
3870            child_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()),
3871            "child should record file_read_time on buffer_read"
3872        );
3873        assert!(
3874            parent_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
3875            "parent should NOT get file_read_time from child's buffer_read"
3876        );
3877
3878        cx.update(|cx| {
3879            child_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
3880        });
3881        assert!(
3882            parent_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
3883            "parent should NOT get file_read_time from child's buffer_edited"
3884        );
3885
3886        cx.update(|cx| {
3887            child_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
3888        });
3889        assert!(
3890            parent_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
3891            "parent should NOT get file_read_time from child's buffer_created"
3892        );
3893    }
3894
3895    #[gpui::test]
3896    async fn test_file_read_time_not_forwarded_to_linked_action_log_for_inferred_edits(
3897        cx: &mut TestAppContext,
3898    ) {
3899        init_test(cx);
3900
3901        let fs = FakeFs::new(cx.executor());
3902        fs.insert_tree(
3903            path!("/dir"),
3904            json!({
3905                "edit": "hello world\n",
3906                "delete": "goodbye world\n",
3907            }),
3908        )
3909        .await;
3910        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3911        let parent_log = cx.new(|_| ActionLog::new(project.clone()));
3912        let child_log =
3913            cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
3914
3915        let edit_file_path = project
3916            .read_with(cx, |project, cx| project.find_project_path("dir/edit", cx))
3917            .unwrap();
3918        let edit_buffer = project
3919            .update(cx, |project, cx| project.open_buffer(edit_file_path, cx))
3920            .await
3921            .unwrap();
3922        let edit_abs_path = PathBuf::from(path!("/dir/edit"));
3923        let edit_baseline_snapshot = edit_buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3924
3925        edit_buffer.update(cx, |buffer, cx| buffer.set_text("hello world!\n", cx));
3926        project
3927            .update(cx, |project, cx| {
3928                project.save_buffer(edit_buffer.clone(), cx)
3929            })
3930            .await
3931            .unwrap();
3932
3933        cx.update(|cx| {
3934            child_log.update(cx, |log, cx| {
3935                log.infer_buffer_edited_from_snapshot(
3936                    edit_buffer.clone(),
3937                    edit_baseline_snapshot.clone(),
3938                    cx,
3939                );
3940            });
3941        });
3942
3943        assert!(
3944            child_log.read_with(cx, |log, _| log.file_read_time(&edit_abs_path).is_some()),
3945            "child should record file_read_time on inferred edit"
3946        );
3947        assert!(
3948            parent_log.read_with(cx, |log, _| log.file_read_time(&edit_abs_path).is_none()),
3949            "parent should NOT get file_read_time from child's inferred edit"
3950        );
3951
3952        let create_file_path = project
3953            .read_with(cx, |project, cx| {
3954                project.find_project_path("dir/new_file", cx)
3955            })
3956            .unwrap();
3957        let create_buffer = project
3958            .update(cx, |project, cx| project.open_buffer(create_file_path, cx))
3959            .await
3960            .unwrap();
3961        let create_abs_path = PathBuf::from(path!("/dir/new_file"));
3962        let create_baseline_snapshot =
3963            create_buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3964
3965        create_buffer.update(cx, |buffer, cx| buffer.set_text("new file\n", cx));
3966        project
3967            .update(cx, |project, cx| {
3968                project.save_buffer(create_buffer.clone(), cx)
3969            })
3970            .await
3971            .unwrap();
3972
3973        cx.update(|cx| {
3974            child_log.update(cx, |log, cx| {
3975                log.infer_buffer_created(
3976                    create_buffer.clone(),
3977                    create_baseline_snapshot.clone(),
3978                    cx,
3979                );
3980            });
3981        });
3982
3983        assert!(
3984            child_log.read_with(cx, |log, _| log.file_read_time(&create_abs_path).is_some()),
3985            "child should record file_read_time on inferred create"
3986        );
3987        assert!(
3988            parent_log.read_with(cx, |log, _| log.file_read_time(&create_abs_path).is_none()),
3989            "parent should NOT get file_read_time from child's inferred create"
3990        );
3991
3992        let delete_file_path = project
3993            .read_with(cx, |project, cx| {
3994                project.find_project_path("dir/delete", cx)
3995            })
3996            .unwrap();
3997        let delete_buffer = project
3998            .update(cx, |project, cx| project.open_buffer(delete_file_path, cx))
3999            .await
4000            .unwrap();
4001        let delete_abs_path = PathBuf::from(path!("/dir/delete"));
4002        let delete_baseline_snapshot =
4003            delete_buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4004
4005        cx.update(|cx| {
4006            parent_log.update(cx, |log, cx| log.buffer_read(delete_buffer.clone(), cx));
4007            child_log.update(cx, |log, cx| log.buffer_read(delete_buffer.clone(), cx));
4008        });
4009
4010        assert!(
4011            parent_log.read_with(cx, |log, _| log.file_read_time(&delete_abs_path).is_some()),
4012            "parent should record its own file_read_time before inferred delete"
4013        );
4014        assert!(
4015            child_log.read_with(cx, |log, _| log.file_read_time(&delete_abs_path).is_some()),
4016            "child should record its own file_read_time before inferred delete"
4017        );
4018
4019        fs.remove_file(path!("/dir/delete").as_ref(), RemoveOptions::default())
4020            .await
4021            .unwrap();
4022        cx.run_until_parked();
4023
4024        cx.update(|cx| {
4025            child_log.update(cx, |log, cx| {
4026                log.infer_buffer_deleted_from_snapshot(
4027                    delete_buffer.clone(),
4028                    delete_baseline_snapshot.clone(),
4029                    cx,
4030                );
4031            });
4032        });
4033
4034        assert!(
4035            child_log.read_with(cx, |log, _| log.file_read_time(&delete_abs_path).is_none()),
4036            "child should remove file_read_time on inferred delete"
4037        );
4038        assert!(
4039            parent_log.read_with(cx, |log, _| log.file_read_time(&delete_abs_path).is_some()),
4040            "parent should keep its own file_read_time on linked inferred delete"
4041        );
4042    }
4043
4044    #[gpui::test]
4045    async fn test_linked_action_log_infer_buffer_edited_from_snapshot(cx: &mut TestAppContext) {
4046        init_test(cx);
4047
4048        let fs = FakeFs::new(cx.executor());
4049        fs.insert_tree(path!("/dir"), json!({"file": "one\ntwo\n"}))
4050            .await;
4051        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4052        let parent_log = cx.new(|_| ActionLog::new(project.clone()));
4053        let child_log =
4054            cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
4055
4056        let file_path = project
4057            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
4058            .unwrap();
4059        let buffer = project
4060            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4061            .await
4062            .unwrap();
4063
4064        let baseline_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4065
4066        buffer.update(cx, |buffer, cx| buffer.set_text("one\ntwo\nthree\n", cx));
4067        project
4068            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
4069            .await
4070            .unwrap();
4071
4072        cx.update(|cx| {
4073            child_log.update(cx, |log, cx| {
4074                log.infer_buffer_edited_from_snapshot(
4075                    buffer.clone(),
4076                    baseline_snapshot.clone(),
4077                    cx,
4078                );
4079            });
4080        });
4081        cx.run_until_parked();
4082
4083        let child_hunks = unreviewed_hunks(&child_log, cx);
4084        assert!(
4085            !child_hunks.is_empty(),
4086            "child should track the inferred edit"
4087        );
4088        assert_eq!(
4089            unreviewed_hunks(&parent_log, cx),
4090            child_hunks,
4091            "parent should also track the inferred edit via linked log forwarding"
4092        );
4093    }
4094
4095    #[gpui::test]
4096    async fn test_linked_action_log_forwards_sequential_inferred_snapshots(
4097        cx: &mut TestAppContext,
4098    ) {
4099        init_test(cx);
4100
4101        let fs = FakeFs::new(cx.executor());
4102        fs.insert_tree(path!("/dir"), json!({"file": "one\ntwo\n"}))
4103            .await;
4104        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4105        let parent_log = cx.new(|_| ActionLog::new(project.clone()));
4106        let child_log =
4107            cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
4108
4109        let file_path = project
4110            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
4111            .expect("test file should exist");
4112        let buffer = project
4113            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4114            .await
4115            .expect("test buffer should open");
4116
4117        let first_baseline_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4118        buffer.update(cx, |buffer, cx| buffer.set_text("one\ntwo\nthree\n", cx));
4119        let second_baseline_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4120        buffer.update(cx, |buffer, cx| {
4121            buffer.set_text("one\ntwo\nthree\nfour\n", cx)
4122        });
4123        project
4124            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
4125            .await
4126            .expect("final inferred buffer contents should save");
4127
4128        cx.update(|cx| {
4129            child_log.update(cx, |log, cx| {
4130                log.infer_buffer_edited_from_snapshot(
4131                    buffer.clone(),
4132                    first_baseline_snapshot.clone(),
4133                    cx,
4134                );
4135            });
4136        });
4137        cx.run_until_parked();
4138
4139        let first_child_hunks = unreviewed_hunks(&child_log, cx);
4140        assert!(
4141            !first_child_hunks.is_empty(),
4142            "the first inferred snapshot should produce review hunks"
4143        );
4144        assert_eq!(
4145            unreviewed_hunks(&parent_log, cx),
4146            first_child_hunks,
4147            "parent should match the first forwarded inferred snapshot"
4148        );
4149
4150        cx.update(|cx| {
4151            child_log.update(cx, |log, cx| {
4152                log.infer_buffer_edited_from_snapshot(
4153                    buffer.clone(),
4154                    second_baseline_snapshot.clone(),
4155                    cx,
4156                );
4157            });
4158        });
4159        cx.run_until_parked();
4160
4161        let second_child_hunks = unreviewed_hunks(&child_log, cx);
4162        assert!(
4163            !second_child_hunks.is_empty(),
4164            "the second inferred snapshot should still produce review hunks"
4165        );
4166        assert_ne!(
4167            second_child_hunks, first_child_hunks,
4168            "the second inferred snapshot should refresh the tracked diff"
4169        );
4170        assert_eq!(
4171            unreviewed_hunks(&parent_log, cx),
4172            second_child_hunks,
4173            "parent should stay in sync after sequential inferred snapshots on one buffer"
4174        );
4175    }
4176
4177    #[gpui::test]
4178    async fn test_linked_action_log_infer_buffer_created(cx: &mut TestAppContext) {
4179        init_test(cx);
4180
4181        let fs = FakeFs::new(cx.executor());
4182        fs.insert_tree(path!("/dir"), json!({})).await;
4183        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4184        let parent_log = cx.new(|_| ActionLog::new(project.clone()));
4185        let child_log =
4186            cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
4187
4188        let file_path = project
4189            .read_with(cx, |project, cx| {
4190                project.find_project_path("dir/new_file", cx)
4191            })
4192            .unwrap();
4193        let buffer = project
4194            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4195            .await
4196            .unwrap();
4197
4198        let baseline_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4199
4200        buffer.update(cx, |buffer, cx| buffer.set_text("hello\n", cx));
4201        project
4202            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
4203            .await
4204            .unwrap();
4205
4206        cx.update(|cx| {
4207            child_log.update(cx, |log, cx| {
4208                log.infer_buffer_created(buffer.clone(), baseline_snapshot.clone(), cx);
4209            });
4210        });
4211        cx.run_until_parked();
4212
4213        let child_hunks = unreviewed_hunks(&child_log, cx);
4214        assert!(
4215            !child_hunks.is_empty(),
4216            "child should track the inferred creation"
4217        );
4218        assert_eq!(
4219            unreviewed_hunks(&parent_log, cx),
4220            child_hunks,
4221            "parent should also track the inferred creation via linked log forwarding"
4222        );
4223    }
4224
4225    #[gpui::test]
4226    async fn test_linked_action_log_infer_buffer_deleted_from_snapshot(cx: &mut TestAppContext) {
4227        init_test(cx);
4228
4229        let fs = FakeFs::new(cx.executor());
4230        fs.insert_tree(path!("/dir"), json!({"file": "hello\n"}))
4231            .await;
4232        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4233        let parent_log = cx.new(|_| ActionLog::new(project.clone()));
4234        let child_log =
4235            cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
4236
4237        let file_path = project
4238            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
4239            .unwrap();
4240        let buffer = project
4241            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4242            .await
4243            .unwrap();
4244
4245        let baseline_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4246
4247        fs.remove_file(path!("/dir/file").as_ref(), RemoveOptions::default())
4248            .await
4249            .unwrap();
4250        cx.run_until_parked();
4251
4252        cx.update(|cx| {
4253            child_log.update(cx, |log, cx| {
4254                log.infer_buffer_deleted_from_snapshot(
4255                    buffer.clone(),
4256                    baseline_snapshot.clone(),
4257                    cx,
4258                );
4259            });
4260        });
4261        cx.run_until_parked();
4262
4263        let child_hunks = unreviewed_hunks(&child_log, cx);
4264        assert!(
4265            !child_hunks.is_empty(),
4266            "child should track the inferred deletion"
4267        );
4268        assert_eq!(
4269            unreviewed_hunks(&parent_log, cx),
4270            child_hunks,
4271            "parent should also track the inferred deletion via linked log forwarding"
4272        );
4273    }
4274
4275    #[gpui::test]
4276    async fn test_expected_external_edit_does_not_mark_read_time_or_stale_before_first_agent_change(
4277        cx: &mut TestAppContext,
4278    ) {
4279        init_test(cx);
4280
4281        let fs = FakeFs::new(cx.executor());
4282        fs.insert_tree(path!("/dir"), json!({"file": "one\ntwo\n"}))
4283            .await;
4284        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4285        let action_log = cx.new(|_| ActionLog::new(project.clone()));
4286
4287        let file_path = project
4288            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
4289            .unwrap();
4290        let buffer = project
4291            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4292            .await
4293            .unwrap();
4294        let abs_path = PathBuf::from(path!("/dir/file"));
4295
4296        cx.update(|cx| {
4297            action_log.update(cx, |log, cx| {
4298                log.begin_expected_external_edit(buffer.clone(), cx);
4299            });
4300        });
4301
4302        assert!(
4303            action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
4304            "expected external edit should not record file_read_time before an agent change"
4305        );
4306        assert!(
4307            action_log.read_with(cx, |log, cx| log.stale_buffers(cx).next().is_none()),
4308            "expected external edit should not mark a synthetic tracker as stale"
4309        );
4310
4311        buffer.update(cx, |buffer, cx| {
4312            buffer.edit([(0..0, "zero\n")], None, cx).unwrap();
4313        });
4314        cx.run_until_parked();
4315
4316        assert!(
4317            action_log.read_with(cx, |log, cx| log.changed_buffers(cx).is_empty()),
4318            "local edits before the first external change should not become review hunks"
4319        );
4320        assert!(
4321            action_log.read_with(cx, |log, cx| log.stale_buffers(cx).next().is_none()),
4322            "expectation-only tracking should stay out of stale_buffers"
4323        );
4324        assert!(
4325            action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
4326            "local edits before the first external change should not record file_read_time"
4327        );
4328
4329        cx.update(|cx| {
4330            action_log.update(cx, |log, cx| {
4331                log.end_expected_external_edit(buffer.clone(), cx);
4332            });
4333        });
4334        cx.run_until_parked();
4335
4336        assert!(
4337            action_log.read_with(cx, |log, cx| log.changed_buffers(cx).is_empty()),
4338            "ending an expectation without an agent change should remove synthetic tracking"
4339        );
4340    }
4341
4342    #[gpui::test]
4343    async fn test_expected_external_edit_preserves_local_edits_before_first_agent_change(
4344        cx: &mut TestAppContext,
4345    ) {
4346        init_test(cx);
4347
4348        let fs = FakeFs::new(cx.executor());
4349        fs.insert_tree(path!("/dir"), json!({"file": "one\ntwo\n"}))
4350            .await;
4351        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4352        let action_log = cx.new(|_| ActionLog::new(project.clone()));
4353
4354        let file_path = project
4355            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
4356            .unwrap();
4357        let buffer = project
4358            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4359            .await
4360            .unwrap();
4361
4362        cx.update(|cx| {
4363            action_log.update(cx, |log, cx| {
4364                log.begin_expected_external_edit(buffer.clone(), cx);
4365            });
4366        });
4367
4368        buffer.update(cx, |buffer, cx| {
4369            buffer.edit([(0..0, "zero\n")], None, cx).unwrap();
4370        });
4371        project
4372            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
4373            .await
4374            .unwrap();
4375        cx.run_until_parked();
4376
4377        fs.save(
4378            path!("/dir/file").as_ref(),
4379            &"zero\none\ntwo\nthree\n".into(),
4380            Default::default(),
4381        )
4382        .await
4383        .unwrap();
4384        cx.run_until_parked();
4385
4386        assert_eq!(
4387            action_log.read_with(cx, |log, cx| log.changed_buffers(cx).len()),
4388            1,
4389            "the first external change should be attributed relative to the local user baseline"
4390        );
4391
4392        cx.update(|cx| {
4393            action_log.update(cx, |log, cx| {
4394                log.end_expected_external_edit(buffer.clone(), cx);
4395            });
4396        });
4397        cx.run_until_parked();
4398
4399        action_log
4400            .update(cx, |log, cx| log.reject_all_edits(None, cx))
4401            .await;
4402        cx.run_until_parked();
4403
4404        assert_eq!(
4405            buffer.read_with(cx, |buffer, _| buffer.text()),
4406            "zero\none\ntwo\n"
4407        );
4408        assert_eq!(
4409            String::from_utf8(fs.read_file_sync(path!("/dir/file")).unwrap()).unwrap(),
4410            "zero\none\ntwo\n"
4411        );
4412    }
4413
4414    #[gpui::test]
4415    async fn test_expected_external_edit_explicit_reload_arm_attributes_forced_reload(
4416        cx: &mut TestAppContext,
4417    ) {
4418        init_test(cx);
4419
4420        let fs = FakeFs::new(cx.executor());
4421        fs.insert_tree(path!("/dir"), json!({"file": "one\ntwo\n"}))
4422            .await;
4423        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4424        let action_log = cx.new(|_| ActionLog::new(project.clone()));
4425
4426        let file_path = project
4427            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
4428            .unwrap();
4429        let buffer = project
4430            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4431            .await
4432            .unwrap();
4433
4434        cx.update(|cx| {
4435            action_log.update(cx, |log, cx| {
4436                log.begin_expected_external_edit(buffer.clone(), cx);
4437            });
4438        });
4439
4440        fs.save(
4441            path!("/dir/file").as_ref(),
4442            &"one\ntwo\nthree\n".into(),
4443            Default::default(),
4444        )
4445        .await
4446        .unwrap();
4447
4448        cx.update(|cx| {
4449            action_log.update(cx, |log, cx| {
4450                log.arm_expected_external_reload(buffer.clone(), cx);
4451            });
4452        });
4453
4454        let reload = project.update(cx, |project, cx| {
4455            let mut buffers = collections::HashSet::default();
4456            buffers.insert(buffer.clone());
4457            project.reload_buffers(buffers, false, cx)
4458        });
4459        reload.await.unwrap();
4460        cx.run_until_parked();
4461
4462        assert_eq!(
4463            action_log.read_with(cx, |log, cx| log.changed_buffers(cx).len()),
4464            1,
4465            "arming an expected reload should attribute an explicit reload before file-handle updates arrive"
4466        );
4467    }
4468
4469    #[gpui::test]
4470    async fn test_expected_external_edit_does_not_attribute_dirty_non_delete_external_changes(
4471        cx: &mut TestAppContext,
4472    ) {
4473        init_test(cx);
4474
4475        let fs = FakeFs::new(cx.executor());
4476        fs.insert_tree(path!("/dir"), json!({"file": "one\ntwo\n"}))
4477            .await;
4478        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4479        let action_log = cx.new(|_| ActionLog::new(project.clone()));
4480        let abs_path = PathBuf::from(path!("/dir/file"));
4481
4482        let file_path = project
4483            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
4484            .unwrap();
4485        let buffer = project
4486            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4487            .await
4488            .unwrap();
4489
4490        cx.update(|cx| {
4491            action_log.update(cx, |log, cx| {
4492                log.begin_expected_external_edit(buffer.clone(), cx);
4493            });
4494        });
4495
4496        buffer.update(cx, |buffer, cx| {
4497            buffer.edit([(0..0, "zero\n")], None, cx).unwrap();
4498        });
4499        cx.run_until_parked();
4500
4501        fs.save(
4502            path!("/dir/file").as_ref(),
4503            &"one\ntwo\nthree\n".into(),
4504            Default::default(),
4505        )
4506        .await
4507        .unwrap();
4508        cx.run_until_parked();
4509
4510        assert!(
4511            action_log.read_with(cx, |log, cx| log.changed_buffers(cx).is_empty()),
4512            "dirty non-delete external changes should stay out of review until the behavior is explicitly supported"
4513        );
4514        assert!(
4515            action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
4516            "unsupported dirty external changes should not record file_read_time"
4517        );
4518        assert_eq!(
4519            buffer.read_with(cx, |buffer, _| buffer.text()),
4520            "zero\none\ntwo\n",
4521            "unsupported dirty external changes should preserve local buffer contents"
4522        );
4523    }
4524
4525    #[gpui::test]
4526    async fn test_linked_expected_external_edit_tracks_review_without_parent_file_read_time(
4527        cx: &mut TestAppContext,
4528    ) {
4529        init_test(cx);
4530
4531        let fs = FakeFs::new(cx.executor());
4532        fs.insert_tree(path!("/dir"), json!({"file": "one\ntwo\n"}))
4533            .await;
4534        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4535        let parent_log = cx.new(|_| ActionLog::new(project.clone()));
4536        let child_log =
4537            cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
4538
4539        let file_path = project
4540            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
4541            .unwrap();
4542        let buffer = project
4543            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4544            .await
4545            .unwrap();
4546        let abs_path = PathBuf::from(path!("/dir/file"));
4547
4548        cx.update(|cx| {
4549            child_log.update(cx, |log, cx| {
4550                log.begin_expected_external_edit(buffer.clone(), cx);
4551            });
4552        });
4553
4554        assert!(
4555            child_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
4556            "child should not record file_read_time until the first external agent change"
4557        );
4558        assert!(
4559            parent_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
4560            "parent should not inherit file_read_time from the child's pending expectation"
4561        );
4562
4563        fs.save(
4564            path!("/dir/file").as_ref(),
4565            &"one\ntwo\nthree\n".into(),
4566            Default::default(),
4567        )
4568        .await
4569        .unwrap();
4570        cx.run_until_parked();
4571
4572        cx.update(|cx| {
4573            child_log.update(cx, |log, cx| {
4574                log.end_expected_external_edit(buffer.clone(), cx);
4575            });
4576        });
4577        cx.run_until_parked();
4578
4579        let child_hunks = unreviewed_hunks(&child_log, cx);
4580        assert!(
4581            !child_hunks.is_empty(),
4582            "child should track the expected external edit"
4583        );
4584        assert_eq!(
4585            unreviewed_hunks(&parent_log, cx),
4586            child_hunks,
4587            "parent should also track the expected external edit"
4588        );
4589        assert!(
4590            child_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()),
4591            "child should record file_read_time once the expected external edit is attributed"
4592        );
4593        assert!(
4594            parent_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
4595            "parent should still not inherit file_read_time from the child's expected external edit"
4596        );
4597    }
4598
4599    #[gpui::test]
4600    async fn test_expected_external_edit_starts_unattributed_even_with_existing_hunks(
4601        cx: &mut TestAppContext,
4602    ) {
4603        init_test(cx);
4604
4605        let fs = FakeFs::new(cx.executor());
4606        fs.insert_tree(path!("/dir"), json!({"file": "one\ntwo\n"}))
4607            .await;
4608        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
4609        let action_log = cx.new(|_| ActionLog::new(project.clone()));
4610        let file_path = project
4611            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
4612            .unwrap();
4613        let buffer = project
4614            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4615            .await
4616            .unwrap();
4617
4618        cx.update(|cx| {
4619            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
4620            buffer.update(cx, |buffer, cx| buffer.set_text("one\ntwo\nthree\n", cx));
4621            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
4622        });
4623        cx.run_until_parked();
4624
4625        assert!(
4626            !unreviewed_hunks(&action_log, cx).is_empty(),
4627            "buffer should already have tracked hunks before the expectation starts"
4628        );
4629
4630        cx.update(|cx| {
4631            action_log.update(cx, |log, cx| {
4632                log.begin_expected_external_edit(buffer.clone(), cx);
4633            });
4634        });
4635
4636        assert!(
4637            action_log.read_with(cx, |log, _| {
4638                log.tracked_buffers
4639                    .get(&buffer)
4640                    .and_then(|tracked_buffer| tracked_buffer.expected_external_edit.as_ref())
4641                    .is_some_and(|expected_external_edit| {
4642                        !expected_external_edit.has_attributed_change
4643                    })
4644            }),
4645            "a new expected external edit should start as unattributed even when the buffer already has hunks"
4646        );
4647    }
4648
4649    #[gpui::test]
4650    async fn test_expected_external_edit_preserves_stale_tracking_for_existing_tracked_buffer(
4651        cx: &mut TestAppContext,
4652    ) {
4653        init_test(cx);
4654
4655        let fs = FakeFs::new(cx.executor());
4656        fs.insert_tree(path!("/dir"), json!({"file": "one\ntwo\n"}))
4657            .await;
4658        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
4659        let action_log = cx.new(|_| ActionLog::new(project.clone()));
4660        let file_path = project
4661            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
4662            .expect("test file should exist");
4663        let buffer = project
4664            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4665            .await
4666            .expect("test buffer should open");
4667
4668        cx.update(|cx| {
4669            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
4670        });
4671
4672        cx.update(|cx| {
4673            buffer.update(cx, |buffer, cx| {
4674                assert!(buffer.edit([(0..0, "zero\n")], None, cx).is_some());
4675            });
4676        });
4677        cx.run_until_parked();
4678
4679        assert_eq!(
4680            action_log.read_with(cx, |log, cx| {
4681                log.stale_buffers(cx).cloned().collect::<Vec<_>>()
4682            }),
4683            vec![buffer.clone()],
4684            "user edits after a read should mark the tracked buffer as stale"
4685        );
4686
4687        cx.update(|cx| {
4688            action_log.update(cx, |log, cx| {
4689                log.begin_expected_external_edit(buffer.clone(), cx);
4690            });
4691        });
4692
4693        assert_eq!(
4694            action_log.read_with(cx, |log, cx| {
4695                log.stale_buffers(cx).cloned().collect::<Vec<_>>()
4696            }),
4697            vec![buffer.clone()],
4698            "starting an expected external edit should not clear existing stale tracking"
4699        );
4700
4701        cx.update(|cx| {
4702            action_log.update(cx, |log, cx| {
4703                log.end_expected_external_edit(buffer.clone(), cx);
4704            });
4705        });
4706
4707        assert_eq!(
4708            action_log.read_with(cx, |log, cx| {
4709                log.stale_buffers(cx).cloned().collect::<Vec<_>>()
4710            }),
4711            vec![buffer],
4712            "ending an unattributed expected external edit should preserve existing stale tracking"
4713        );
4714    }
4715
4716    #[gpui::test]
4717    async fn test_infer_buffer_created_preserves_non_empty_baseline_on_reject(
4718        cx: &mut TestAppContext,
4719    ) {
4720        init_test(cx);
4721
4722        let fs = FakeFs::new(cx.executor());
4723        fs.insert_tree(path!("/dir"), json!({})).await;
4724        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4725        let action_log = cx.new(|_| ActionLog::new(project.clone()));
4726        let file_path = project
4727            .read_with(cx, |project, cx| {
4728                project.find_project_path("dir/new_file", cx)
4729            })
4730            .unwrap();
4731        let buffer = project
4732            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4733            .await
4734            .unwrap();
4735
4736        buffer.update(cx, |buffer, cx| buffer.set_text("draft\n", cx));
4737        let baseline_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4738
4739        buffer.update(cx, |buffer, cx| buffer.set_text("draft\nagent\n", cx));
4740        project
4741            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
4742            .await
4743            .unwrap();
4744
4745        cx.update(|cx| {
4746            action_log.update(cx, |log, cx| {
4747                log.infer_buffer_created(buffer.clone(), baseline_snapshot.clone(), cx);
4748            });
4749        });
4750        cx.run_until_parked();
4751
4752        action_log
4753            .update(cx, |log, cx| log.reject_all_edits(None, cx))
4754            .await;
4755        cx.run_until_parked();
4756
4757        assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "draft\n");
4758        assert_eq!(
4759            String::from_utf8(fs.read_file_sync(path!("/dir/new_file")).unwrap()).unwrap(),
4760            "draft\n"
4761        );
4762    }
4763
4764    #[gpui::test]
4765    async fn test_infer_buffer_edited_from_snapshot(cx: &mut TestAppContext) {
4766        init_test(cx);
4767
4768        let fs = FakeFs::new(cx.executor());
4769        fs.insert_tree(path!("/dir"), json!({"file": "one\ntwo\n"}))
4770            .await;
4771        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4772        let action_log = cx.new(|_| ActionLog::new(project.clone()));
4773        let file_path = project
4774            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
4775            .unwrap();
4776        let buffer = project
4777            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4778            .await
4779            .unwrap();
4780
4781        let baseline_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4782
4783        buffer.update(cx, |buffer, cx| buffer.set_text("one\ntwo\nthree\n", cx));
4784        project
4785            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
4786            .await
4787            .unwrap();
4788
4789        cx.update(|cx| {
4790            action_log.update(cx, |log, cx| {
4791                log.infer_buffer_edited_from_snapshot(
4792                    buffer.clone(),
4793                    baseline_snapshot.clone(),
4794                    cx,
4795                );
4796            });
4797        });
4798        cx.run_until_parked();
4799
4800        assert!(
4801            !unreviewed_hunks(&action_log, cx).is_empty(),
4802            "inferred edit should produce reviewable hunks"
4803        );
4804
4805        action_log
4806            .update(cx, |log, cx| log.reject_all_edits(None, cx))
4807            .await;
4808        cx.run_until_parked();
4809
4810        assert_eq!(
4811            buffer.read_with(cx, |buffer, _| buffer.text()),
4812            "one\ntwo\n"
4813        );
4814    }
4815
4816    #[gpui::test]
4817    async fn test_infer_buffer_created(cx: &mut TestAppContext) {
4818        init_test(cx);
4819
4820        let fs = FakeFs::new(cx.executor());
4821        fs.insert_tree(path!("/dir"), json!({})).await;
4822        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4823        let action_log = cx.new(|_| ActionLog::new(project.clone()));
4824        let file_path = project
4825            .read_with(cx, |project, cx| {
4826                project.find_project_path("dir/new_file", cx)
4827            })
4828            .unwrap();
4829        let buffer = project
4830            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4831            .await
4832            .unwrap();
4833
4834        let baseline_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4835
4836        buffer.update(cx, |buffer, cx| buffer.set_text("hello\n", cx));
4837        project
4838            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
4839            .await
4840            .unwrap();
4841
4842        cx.update(|cx| {
4843            action_log.update(cx, |log, cx| {
4844                log.infer_buffer_created(buffer.clone(), baseline_snapshot.clone(), cx);
4845            });
4846        });
4847        cx.run_until_parked();
4848
4849        assert!(
4850            !unreviewed_hunks(&action_log, cx).is_empty(),
4851            "inferred creation should produce reviewable hunks"
4852        );
4853
4854        action_log
4855            .update(cx, |log, cx| log.reject_all_edits(None, cx))
4856            .await;
4857        cx.run_until_parked();
4858
4859        assert!(fs.read_file_sync(path!("/dir/new_file")).is_err());
4860    }
4861
4862    #[gpui::test]
4863    async fn test_infer_buffer_deleted_from_snapshot(cx: &mut TestAppContext) {
4864        init_test(cx);
4865
4866        let fs = FakeFs::new(cx.executor());
4867        fs.insert_tree(path!("/dir"), json!({"file": "hello\n"}))
4868            .await;
4869        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4870        let action_log = cx.new(|_| ActionLog::new(project.clone()));
4871        let file_path = project
4872            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
4873            .unwrap();
4874        let buffer = project
4875            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4876            .await
4877            .unwrap();
4878
4879        let baseline_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4880
4881        fs.remove_file(path!("/dir/file").as_ref(), RemoveOptions::default())
4882            .await
4883            .unwrap();
4884        cx.run_until_parked();
4885
4886        cx.update(|cx| {
4887            action_log.update(cx, |log, cx| {
4888                log.infer_buffer_deleted_from_snapshot(
4889                    buffer.clone(),
4890                    baseline_snapshot.clone(),
4891                    cx,
4892                );
4893            });
4894        });
4895        cx.run_until_parked();
4896
4897        assert!(
4898            !unreviewed_hunks(&action_log, cx).is_empty(),
4899            "inferred deletion should produce reviewable hunks"
4900        );
4901
4902        action_log
4903            .update(cx, |log, cx| log.reject_all_edits(None, cx))
4904            .await;
4905        cx.run_until_parked();
4906
4907        assert_eq!(
4908            String::from_utf8(fs.read_file_sync(path!("/dir/file")).unwrap()).unwrap(),
4909            "hello\n"
4910        );
4911    }
4912
4913    #[gpui::test]
4914    async fn test_infer_buffer_deleted_from_snapshot_preserves_later_user_edits_on_reject(
4915        cx: &mut TestAppContext,
4916    ) {
4917        init_test(cx);
4918
4919        let fs = FakeFs::new(cx.executor());
4920        fs.insert_tree(path!("/dir"), json!({"file": "hello\n"}))
4921            .await;
4922        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4923        let action_log = cx.new(|_| ActionLog::new(project.clone()));
4924        let file_path = project
4925            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
4926            .unwrap();
4927        let buffer = project
4928            .update(cx, |project, cx| project.open_buffer(file_path, cx))
4929            .await
4930            .unwrap();
4931
4932        let baseline_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4933
4934        fs.remove_file(path!("/dir/file").as_ref(), RemoveOptions::default())
4935            .await
4936            .unwrap();
4937        cx.run_until_parked();
4938
4939        cx.update(|cx| {
4940            action_log.update(cx, |log, cx| {
4941                log.infer_buffer_deleted_from_snapshot(
4942                    buffer.clone(),
4943                    baseline_snapshot.clone(),
4944                    cx,
4945                );
4946            });
4947        });
4948        cx.run_until_parked();
4949
4950        buffer.update(cx, |buffer, cx| buffer.append("world\n", cx));
4951        project
4952            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
4953            .await
4954            .unwrap();
4955        cx.run_until_parked();
4956
4957        action_log
4958            .update(cx, |log, cx| log.reject_all_edits(None, cx))
4959            .await;
4960        cx.run_until_parked();
4961
4962        assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "world\n");
4963        assert_eq!(
4964            String::from_utf8(fs.read_file_sync(path!("/dir/file")).unwrap()).unwrap(),
4965            "world\n"
4966        );
4967    }
4968
4969    #[gpui::test]
4970    async fn test_will_delete_buffer_preserves_later_user_edits_on_reject(cx: &mut TestAppContext) {
4971        init_test(cx);
4972
4973        let fs = FakeFs::new(cx.executor());
4974        fs.insert_tree(path!("/dir"), json!({"file": "hello\n"}))
4975            .await;
4976        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4977        let action_log = cx.new(|_| ActionLog::new(project.clone()));
4978        let file_path = project
4979            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
4980            .unwrap();
4981        let buffer = project
4982            .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
4983            .await
4984            .unwrap();
4985
4986        cx.update(|cx| {
4987            action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx));
4988        });
4989        project
4990            .update(cx, |project, cx| project.delete_file(file_path, false, cx))
4991            .unwrap()
4992            .await
4993            .unwrap();
4994        cx.run_until_parked();
4995
4996        buffer.update(cx, |buffer, cx| buffer.append("world\n", cx));
4997        project
4998            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
4999            .await
5000            .unwrap();
5001        cx.run_until_parked();
5002
5003        action_log
5004            .update(cx, |log, cx| log.reject_all_edits(None, cx))
5005            .await;
5006        cx.run_until_parked();
5007
5008        assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "world\n");
5009        assert_eq!(
5010            String::from_utf8(fs.read_file_sync(path!("/dir/file")).unwrap()).unwrap(),
5011            "world\n"
5012        );
5013    }
5014
5015    #[gpui::test]
5016    async fn test_infer_buffer_edited_from_snapshot_preserves_later_user_edits(
5017        cx: &mut TestAppContext,
5018    ) {
5019        init_test(cx);
5020
5021        let fs = FakeFs::new(cx.executor());
5022        fs.insert_tree(path!("/dir"), json!({"file": "one\ntwo\n"}))
5023            .await;
5024        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5025        let action_log = cx.new(|_| ActionLog::new(project.clone()));
5026        let file_path = project
5027            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
5028            .unwrap();
5029        let buffer = project
5030            .update(cx, |project, cx| project.open_buffer(file_path, cx))
5031            .await
5032            .unwrap();
5033
5034        let baseline_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
5035
5036        buffer.update(cx, |buffer, cx| buffer.set_text("one\ntwo\nthree\n", cx));
5037        project
5038            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
5039            .await
5040            .unwrap();
5041
5042        cx.update(|cx| {
5043            action_log.update(cx, |log, cx| {
5044                log.infer_buffer_edited_from_snapshot(
5045                    buffer.clone(),
5046                    baseline_snapshot.clone(),
5047                    cx,
5048                );
5049            });
5050        });
5051        cx.run_until_parked();
5052
5053        buffer.update(cx, |buffer, cx| {
5054            buffer.edit([(0..0, "zero\n")], None, cx);
5055        });
5056        cx.run_until_parked();
5057
5058        action_log
5059            .update(cx, |log, cx| log.reject_all_edits(None, cx))
5060            .await;
5061        cx.run_until_parked();
5062
5063        assert_eq!(
5064            buffer.read_with(cx, |buffer, _| buffer.text()),
5065            "zero\none\ntwo\n"
5066        );
5067        assert_eq!(
5068            String::from_utf8(fs.read_file_sync(path!("/dir/file")).unwrap()).unwrap(),
5069            "zero\none\ntwo\n"
5070        );
5071    }
5072
5073    #[gpui::test]
5074    async fn test_infer_buffer_created_preserves_later_user_edits_on_reject(
5075        cx: &mut TestAppContext,
5076    ) {
5077        init_test(cx);
5078
5079        let fs = FakeFs::new(cx.executor());
5080        fs.insert_tree(path!("/dir"), json!({})).await;
5081        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5082        let action_log = cx.new(|_| ActionLog::new(project.clone()));
5083        let file_path = project
5084            .read_with(cx, |project, cx| {
5085                project.find_project_path("dir/new_file", cx)
5086            })
5087            .unwrap();
5088        let buffer = project
5089            .update(cx, |project, cx| project.open_buffer(file_path, cx))
5090            .await
5091            .unwrap();
5092
5093        let baseline_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
5094
5095        buffer.update(cx, |buffer, cx| buffer.set_text("hello\n", cx));
5096        project
5097            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
5098            .await
5099            .unwrap();
5100
5101        cx.update(|cx| {
5102            action_log.update(cx, |log, cx| {
5103                log.infer_buffer_created(buffer.clone(), baseline_snapshot.clone(), cx);
5104            });
5105        });
5106        cx.run_until_parked();
5107
5108        buffer.update(cx, |buffer, cx| buffer.append("world\n", cx));
5109        cx.run_until_parked();
5110
5111        action_log
5112            .update(cx, |log, cx| log.reject_all_edits(None, cx))
5113            .await;
5114        cx.run_until_parked();
5115
5116        assert_eq!(
5117            buffer.read_with(cx, |buffer, _| buffer.text()),
5118            "hello\nworld\n"
5119        );
5120        assert!(fs.read_file_sync(path!("/dir/new_file")).is_ok());
5121        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
5122    }
5123
5124    #[derive(Debug, PartialEq)]
5125    struct HunkStatus {
5126        range: Range<Point>,
5127        diff_status: DiffHunkStatusKind,
5128        old_text: String,
5129    }
5130
5131    fn unreviewed_hunks(
5132        action_log: &Entity<ActionLog>,
5133        cx: &TestAppContext,
5134    ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
5135        cx.read(|cx| {
5136            action_log
5137                .read(cx)
5138                .changed_buffers(cx)
5139                .into_iter()
5140                .map(|(buffer, diff)| {
5141                    let snapshot = buffer.read(cx).snapshot();
5142                    (
5143                        buffer,
5144                        diff.read(cx)
5145                            .snapshot(cx)
5146                            .hunks(&snapshot)
5147                            .map(|hunk| HunkStatus {
5148                                diff_status: hunk.status().kind,
5149                                range: hunk.range,
5150                                old_text: diff
5151                                    .read(cx)
5152                                    .base_text(cx)
5153                                    .text_for_range(hunk.diff_base_byte_range)
5154                                    .collect(),
5155                            })
5156                            .collect(),
5157                    )
5158                })
5159                .collect()
5160        })
5161    }
5162}