1use anyhow::{Context as _, Result};
   2use buffer_diff::BufferDiff;
   3use clock;
   4use collections::BTreeMap;
   5use futures::{FutureExt, StreamExt, channel::mpsc};
   6use gpui::{
   7    App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Subscription, Task, WeakEntity,
   8};
   9use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
  10use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
  11use std::{cmp, ops::Range, sync::Arc};
  12use text::{Edit, Patch, Rope};
  13use util::{RangeExt, ResultExt as _};
  14
  15/// Tracks actions performed by tools in a thread
  16pub struct ActionLog {
  17    /// Buffers that we want to notify the model about when they change.
  18    tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
  19    /// The project this action log is associated with
  20    project: Entity<Project>,
  21}
  22
  23impl ActionLog {
  24    /// Creates a new, empty action log associated with the given project.
  25    pub fn new(project: Entity<Project>) -> Self {
  26        Self {
  27            tracked_buffers: BTreeMap::default(),
  28            project,
  29        }
  30    }
  31
  32    pub fn project(&self) -> &Entity<Project> {
  33        &self.project
  34    }
  35
  36    pub fn latest_snapshot(&self, buffer: &Entity<Buffer>) -> Option<text::BufferSnapshot> {
  37        Some(self.tracked_buffers.get(buffer)?.snapshot.clone())
  38    }
  39
  40    /// Return a unified diff patch with user edits made since last read or notification
  41    pub fn unnotified_user_edits(&self, cx: &Context<Self>) -> Option<String> {
  42        let diffs = self
  43            .tracked_buffers
  44            .values()
  45            .filter_map(|tracked| {
  46                if !tracked.may_have_unnotified_user_edits {
  47                    return None;
  48                }
  49
  50                let text_with_latest_user_edits = tracked.diff_base.to_string();
  51                let text_with_last_seen_user_edits = tracked.last_seen_base.to_string();
  52                if text_with_latest_user_edits == text_with_last_seen_user_edits {
  53                    return None;
  54                }
  55                let patch = language::unified_diff(
  56                    &text_with_last_seen_user_edits,
  57                    &text_with_latest_user_edits,
  58                );
  59
  60                let buffer = tracked.buffer.clone();
  61                let file_path = buffer
  62                    .read(cx)
  63                    .file()
  64                    .map(|file| {
  65                        let mut path = file.full_path(cx).to_string_lossy().into_owned();
  66                        if file.path_style(cx).is_windows() {
  67                            path = path.replace('\\', "/");
  68                        }
  69                        path
  70                    })
  71                    .unwrap_or_else(|| format!("buffer_{}", buffer.entity_id()));
  72
  73                let mut result = String::new();
  74                result.push_str(&format!("--- a/{}\n", file_path));
  75                result.push_str(&format!("+++ b/{}\n", file_path));
  76                result.push_str(&patch);
  77
  78                Some(result)
  79            })
  80            .collect::<Vec<_>>();
  81
  82        if diffs.is_empty() {
  83            return None;
  84        }
  85
  86        let unified_diff = diffs.join("\n\n");
  87        Some(unified_diff)
  88    }
  89
  90    /// Return a unified diff patch with user edits made since last read/notification
  91    /// and mark them as notified
  92    pub fn flush_unnotified_user_edits(&mut self, cx: &Context<Self>) -> Option<String> {
  93        let patch = self.unnotified_user_edits(cx);
  94        self.tracked_buffers.values_mut().for_each(|tracked| {
  95            tracked.may_have_unnotified_user_edits = false;
  96            tracked.last_seen_base = tracked.diff_base.clone();
  97        });
  98        patch
  99    }
 100
 101    fn track_buffer_internal(
 102        &mut self,
 103        buffer: Entity<Buffer>,
 104        is_created: bool,
 105        cx: &mut Context<Self>,
 106    ) -> &mut TrackedBuffer {
 107        let status = if is_created {
 108            if let Some(tracked) = self.tracked_buffers.remove(&buffer) {
 109                match tracked.status {
 110                    TrackedBufferStatus::Created {
 111                        existing_file_content,
 112                    } => TrackedBufferStatus::Created {
 113                        existing_file_content,
 114                    },
 115                    TrackedBufferStatus::Modified | TrackedBufferStatus::Deleted => {
 116                        TrackedBufferStatus::Created {
 117                            existing_file_content: Some(tracked.diff_base),
 118                        }
 119                    }
 120                }
 121            } else if buffer
 122                .read(cx)
 123                .file()
 124                .is_some_and(|file| file.disk_state().exists())
 125            {
 126                TrackedBufferStatus::Created {
 127                    existing_file_content: Some(buffer.read(cx).as_rope().clone()),
 128                }
 129            } else {
 130                TrackedBufferStatus::Created {
 131                    existing_file_content: None,
 132                }
 133            }
 134        } else {
 135            TrackedBufferStatus::Modified
 136        };
 137
 138        let tracked_buffer = self
 139            .tracked_buffers
 140            .entry(buffer.clone())
 141            .or_insert_with(|| {
 142                let open_lsp_handle = self.project.update(cx, |project, cx| {
 143                    project.register_buffer_with_language_servers(&buffer, cx)
 144                });
 145
 146                let text_snapshot = buffer.read(cx).text_snapshot();
 147                let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
 148                let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
 149                let diff_base;
 150                let last_seen_base;
 151                let unreviewed_edits;
 152                if is_created {
 153                    diff_base = Rope::default();
 154                    last_seen_base = Rope::default();
 155                    unreviewed_edits = Patch::new(vec![Edit {
 156                        old: 0..1,
 157                        new: 0..text_snapshot.max_point().row + 1,
 158                    }])
 159                } else {
 160                    diff_base = buffer.read(cx).as_rope().clone();
 161                    last_seen_base = diff_base.clone();
 162                    unreviewed_edits = Patch::default();
 163                }
 164                TrackedBuffer {
 165                    buffer: buffer.clone(),
 166                    diff_base,
 167                    last_seen_base,
 168                    unreviewed_edits,
 169                    snapshot: text_snapshot,
 170                    status,
 171                    version: buffer.read(cx).version(),
 172                    diff,
 173                    diff_update: diff_update_tx,
 174                    may_have_unnotified_user_edits: false,
 175                    _open_lsp_handle: open_lsp_handle,
 176                    _maintain_diff: cx.spawn({
 177                        let buffer = buffer.clone();
 178                        async move |this, cx| {
 179                            Self::maintain_diff(this, buffer, diff_update_rx, cx)
 180                                .await
 181                                .ok();
 182                        }
 183                    }),
 184                    _subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
 185                }
 186            });
 187        tracked_buffer.version = buffer.read(cx).version();
 188        tracked_buffer
 189    }
 190
 191    fn handle_buffer_event(
 192        &mut self,
 193        buffer: Entity<Buffer>,
 194        event: &BufferEvent,
 195        cx: &mut Context<Self>,
 196    ) {
 197        match event {
 198            BufferEvent::Edited => self.handle_buffer_edited(buffer, cx),
 199            BufferEvent::FileHandleChanged => {
 200                self.handle_buffer_file_changed(buffer, cx);
 201            }
 202            _ => {}
 203        };
 204    }
 205
 206    fn handle_buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 207        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 208            return;
 209        };
 210        tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
 211    }
 212
 213    fn handle_buffer_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 214        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 215            return;
 216        };
 217
 218        match tracked_buffer.status {
 219            TrackedBufferStatus::Created { .. } | TrackedBufferStatus::Modified => {
 220                if buffer
 221                    .read(cx)
 222                    .file()
 223                    .is_some_and(|file| file.disk_state() == DiskState::Deleted)
 224                {
 225                    // If the buffer had been edited by a tool, but it got
 226                    // deleted externally, we want to stop tracking it.
 227                    self.tracked_buffers.remove(&buffer);
 228                }
 229                cx.notify();
 230            }
 231            TrackedBufferStatus::Deleted => {
 232                if buffer
 233                    .read(cx)
 234                    .file()
 235                    .is_some_and(|file| file.disk_state() != DiskState::Deleted)
 236                {
 237                    // If the buffer had been deleted by a tool, but it got
 238                    // resurrected externally, we want to clear the edits we
 239                    // were tracking and reset the buffer's state.
 240                    self.tracked_buffers.remove(&buffer);
 241                    self.track_buffer_internal(buffer, false, cx);
 242                }
 243                cx.notify();
 244            }
 245        }
 246    }
 247
 248    async fn maintain_diff(
 249        this: WeakEntity<Self>,
 250        buffer: Entity<Buffer>,
 251        mut buffer_updates: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>,
 252        cx: &mut AsyncApp,
 253    ) -> Result<()> {
 254        let git_store = this.read_with(cx, |this, cx| this.project.read(cx).git_store().clone())?;
 255        let git_diff = this
 256            .update(cx, |this, cx| {
 257                this.project.update(cx, |project, cx| {
 258                    project.open_uncommitted_diff(buffer.clone(), cx)
 259                })
 260            })?
 261            .await
 262            .ok();
 263        let buffer_repo = git_store.read_with(cx, |git_store, cx| {
 264            git_store.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
 265        })?;
 266
 267        let (mut git_diff_updates_tx, mut git_diff_updates_rx) = watch::channel(());
 268        let _repo_subscription =
 269            if let Some((git_diff, (buffer_repo, _))) = git_diff.as_ref().zip(buffer_repo) {
 270                cx.update(|cx| {
 271                    let mut old_head = buffer_repo.read(cx).head_commit.clone();
 272                    Some(cx.subscribe(git_diff, move |_, event, cx| {
 273                        if let buffer_diff::BufferDiffEvent::DiffChanged { .. } = event {
 274                            let new_head = buffer_repo.read(cx).head_commit.clone();
 275                            if new_head != old_head {
 276                                old_head = new_head;
 277                                git_diff_updates_tx.send(()).ok();
 278                            }
 279                        }
 280                    }))
 281                })?
 282            } else {
 283                None
 284            };
 285
 286        loop {
 287            futures::select_biased! {
 288                buffer_update = buffer_updates.next() => {
 289                    if let Some((author, buffer_snapshot)) = buffer_update {
 290                        Self::track_edits(&this, &buffer, author, buffer_snapshot, cx).await?;
 291                    } else {
 292                        break;
 293                    }
 294                }
 295                _ = git_diff_updates_rx.changed().fuse() => {
 296                    if let Some(git_diff) = git_diff.as_ref() {
 297                        Self::keep_committed_edits(&this, &buffer, git_diff, cx).await?;
 298                    }
 299                }
 300            }
 301        }
 302
 303        Ok(())
 304    }
 305
 306    async fn track_edits(
 307        this: &WeakEntity<ActionLog>,
 308        buffer: &Entity<Buffer>,
 309        author: ChangeAuthor,
 310        buffer_snapshot: text::BufferSnapshot,
 311        cx: &mut AsyncApp,
 312    ) -> Result<()> {
 313        let rebase = this.update(cx, |this, cx| {
 314            let tracked_buffer = this
 315                .tracked_buffers
 316                .get_mut(buffer)
 317                .context("buffer not tracked")?;
 318
 319            let rebase = cx.background_spawn({
 320                let mut base_text = tracked_buffer.diff_base.clone();
 321                let old_snapshot = tracked_buffer.snapshot.clone();
 322                let new_snapshot = buffer_snapshot.clone();
 323                let unreviewed_edits = tracked_buffer.unreviewed_edits.clone();
 324                let edits = diff_snapshots(&old_snapshot, &new_snapshot);
 325                let mut has_user_changes = false;
 326                let executor = cx.background_executor().clone();
 327                async move {
 328                    if let ChangeAuthor::User = author {
 329                        has_user_changes = apply_non_conflicting_edits(
 330                            &unreviewed_edits,
 331                            edits,
 332                            &mut base_text,
 333                            new_snapshot.as_rope(),
 334                            &executor,
 335                        );
 336                    }
 337
 338                    (Arc::new(base_text.to_string()), base_text, has_user_changes)
 339                }
 340            });
 341
 342            anyhow::Ok(rebase)
 343        })??;
 344        let (new_base_text, new_diff_base, has_user_changes) = rebase.await;
 345
 346        this.update(cx, |this, _| {
 347            let tracked_buffer = this
 348                .tracked_buffers
 349                .get_mut(buffer)
 350                .context("buffer not tracked")
 351                .unwrap();
 352            tracked_buffer.may_have_unnotified_user_edits |= has_user_changes;
 353        })?;
 354
 355        Self::update_diff(
 356            this,
 357            buffer,
 358            buffer_snapshot,
 359            new_base_text,
 360            new_diff_base,
 361            cx,
 362        )
 363        .await
 364    }
 365
 366    async fn keep_committed_edits(
 367        this: &WeakEntity<ActionLog>,
 368        buffer: &Entity<Buffer>,
 369        git_diff: &Entity<BufferDiff>,
 370        cx: &mut AsyncApp,
 371    ) -> Result<()> {
 372        let buffer_snapshot = this.read_with(cx, |this, _cx| {
 373            let tracked_buffer = this
 374                .tracked_buffers
 375                .get(buffer)
 376                .context("buffer not tracked")?;
 377            anyhow::Ok(tracked_buffer.snapshot.clone())
 378        })??;
 379        let (new_base_text, new_diff_base) = this
 380            .read_with(cx, |this, cx| {
 381                let tracked_buffer = this
 382                    .tracked_buffers
 383                    .get(buffer)
 384                    .context("buffer not tracked")?;
 385                let old_unreviewed_edits = tracked_buffer.unreviewed_edits.clone();
 386                let agent_diff_base = tracked_buffer.diff_base.clone();
 387                let git_diff_base = git_diff.read(cx).base_text().as_rope().clone();
 388                let buffer_text = tracked_buffer.snapshot.as_rope().clone();
 389                let executor = cx.background_executor().clone();
 390                anyhow::Ok(cx.background_spawn(async move {
 391                    let mut old_unreviewed_edits = old_unreviewed_edits.into_iter().peekable();
 392                    let committed_edits = language::line_diff(
 393                        &agent_diff_base.to_string(),
 394                        &git_diff_base.to_string(),
 395                    )
 396                    .into_iter()
 397                    .map(|(old, new)| Edit { old, new });
 398
 399                    let mut new_agent_diff_base = agent_diff_base.clone();
 400                    let mut row_delta = 0i32;
 401                    for committed in committed_edits {
 402                        while let Some(unreviewed) = old_unreviewed_edits.peek() {
 403                            // If the committed edit matches the unreviewed
 404                            // edit, assume the user wants to keep it.
 405                            if committed.old == unreviewed.old {
 406                                let unreviewed_new =
 407                                    buffer_text.slice_rows(unreviewed.new.clone()).to_string();
 408                                let committed_new =
 409                                    git_diff_base.slice_rows(committed.new.clone()).to_string();
 410                                if unreviewed_new == committed_new {
 411                                    let old_byte_start =
 412                                        new_agent_diff_base.point_to_offset(Point::new(
 413                                            (unreviewed.old.start as i32 + row_delta) as u32,
 414                                            0,
 415                                        ));
 416                                    let old_byte_end =
 417                                        new_agent_diff_base.point_to_offset(cmp::min(
 418                                            Point::new(
 419                                                (unreviewed.old.end as i32 + row_delta) as u32,
 420                                                0,
 421                                            ),
 422                                            new_agent_diff_base.max_point(),
 423                                        ));
 424                                    new_agent_diff_base.replace(
 425                                        old_byte_start..old_byte_end,
 426                                        &unreviewed_new,
 427                                        &executor,
 428                                    );
 429                                    row_delta +=
 430                                        unreviewed.new_len() as i32 - unreviewed.old_len() as i32;
 431                                }
 432                            } else if unreviewed.old.start >= committed.old.end {
 433                                break;
 434                            }
 435
 436                            old_unreviewed_edits.next().unwrap();
 437                        }
 438                    }
 439
 440                    (
 441                        Arc::new(new_agent_diff_base.to_string()),
 442                        new_agent_diff_base,
 443                    )
 444                }))
 445            })??
 446            .await;
 447
 448        Self::update_diff(
 449            this,
 450            buffer,
 451            buffer_snapshot,
 452            new_base_text,
 453            new_diff_base,
 454            cx,
 455        )
 456        .await
 457    }
 458
 459    async fn update_diff(
 460        this: &WeakEntity<ActionLog>,
 461        buffer: &Entity<Buffer>,
 462        buffer_snapshot: text::BufferSnapshot,
 463        new_base_text: Arc<String>,
 464        new_diff_base: Rope,
 465        cx: &mut AsyncApp,
 466    ) -> Result<()> {
 467        let (diff, language, language_registry) = this.read_with(cx, |this, cx| {
 468            let tracked_buffer = this
 469                .tracked_buffers
 470                .get(buffer)
 471                .context("buffer not tracked")?;
 472            anyhow::Ok((
 473                tracked_buffer.diff.clone(),
 474                buffer.read(cx).language().cloned(),
 475                buffer.read(cx).language_registry(),
 476            ))
 477        })??;
 478        let diff_snapshot = BufferDiff::update_diff(
 479            diff.clone(),
 480            buffer_snapshot.clone(),
 481            Some(new_base_text),
 482            true,
 483            false,
 484            language,
 485            language_registry,
 486            cx,
 487        )
 488        .await;
 489        let mut unreviewed_edits = Patch::default();
 490        if let Ok(diff_snapshot) = diff_snapshot {
 491            unreviewed_edits = cx
 492                .background_spawn({
 493                    let diff_snapshot = diff_snapshot.clone();
 494                    let buffer_snapshot = buffer_snapshot.clone();
 495                    let new_diff_base = new_diff_base.clone();
 496                    async move {
 497                        let mut unreviewed_edits = Patch::default();
 498                        for hunk in diff_snapshot
 499                            .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer_snapshot)
 500                        {
 501                            let old_range = new_diff_base
 502                                .offset_to_point(hunk.diff_base_byte_range.start)
 503                                ..new_diff_base.offset_to_point(hunk.diff_base_byte_range.end);
 504                            let new_range = hunk.range.start..hunk.range.end;
 505                            unreviewed_edits.push(point_to_row_edit(
 506                                Edit {
 507                                    old: old_range,
 508                                    new: new_range,
 509                                },
 510                                &new_diff_base,
 511                                buffer_snapshot.as_rope(),
 512                            ));
 513                        }
 514                        unreviewed_edits
 515                    }
 516                })
 517                .await;
 518
 519            diff.update(cx, |diff, cx| {
 520                diff.set_snapshot(diff_snapshot, &buffer_snapshot, cx);
 521            })?;
 522        }
 523        this.update(cx, |this, cx| {
 524            let tracked_buffer = this
 525                .tracked_buffers
 526                .get_mut(buffer)
 527                .context("buffer not tracked")?;
 528            tracked_buffer.diff_base = new_diff_base;
 529            tracked_buffer.snapshot = buffer_snapshot;
 530            tracked_buffer.unreviewed_edits = unreviewed_edits;
 531            cx.notify();
 532            anyhow::Ok(())
 533        })?
 534    }
 535
 536    /// Track a buffer as read by agent, so we can notify the model about user edits.
 537    pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 538        self.track_buffer_internal(buffer, false, cx);
 539    }
 540
 541    /// Mark a buffer as created by agent, so we can refresh it in the context
 542    pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 543        self.track_buffer_internal(buffer, true, cx);
 544    }
 545
 546    /// Mark a buffer as edited by agent, so we can refresh it in the context
 547    pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 548        let tracked_buffer = self.track_buffer_internal(buffer, false, cx);
 549        if let TrackedBufferStatus::Deleted = tracked_buffer.status {
 550            tracked_buffer.status = TrackedBufferStatus::Modified;
 551        }
 552        tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
 553    }
 554
 555    pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 556        let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx);
 557        match tracked_buffer.status {
 558            TrackedBufferStatus::Created { .. } => {
 559                self.tracked_buffers.remove(&buffer);
 560                cx.notify();
 561            }
 562            TrackedBufferStatus::Modified => {
 563                buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
 564                tracked_buffer.status = TrackedBufferStatus::Deleted;
 565                tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
 566            }
 567            TrackedBufferStatus::Deleted => {}
 568        }
 569        cx.notify();
 570    }
 571
 572    pub fn keep_edits_in_range(
 573        &mut self,
 574        buffer: Entity<Buffer>,
 575        buffer_range: Range<impl language::ToPoint>,
 576        cx: &mut Context<Self>,
 577    ) {
 578        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 579            return;
 580        };
 581
 582        match tracked_buffer.status {
 583            TrackedBufferStatus::Deleted => {
 584                self.tracked_buffers.remove(&buffer);
 585                cx.notify();
 586            }
 587            _ => {
 588                let buffer = buffer.read(cx);
 589                let buffer_range =
 590                    buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
 591                let mut delta = 0i32;
 592
 593                tracked_buffer.unreviewed_edits.retain_mut(|edit| {
 594                    edit.old.start = (edit.old.start as i32 + delta) as u32;
 595                    edit.old.end = (edit.old.end as i32 + delta) as u32;
 596
 597                    if buffer_range.end.row < edit.new.start
 598                        || buffer_range.start.row > edit.new.end
 599                    {
 600                        true
 601                    } else {
 602                        let old_range = tracked_buffer
 603                            .diff_base
 604                            .point_to_offset(Point::new(edit.old.start, 0))
 605                            ..tracked_buffer.diff_base.point_to_offset(cmp::min(
 606                                Point::new(edit.old.end, 0),
 607                                tracked_buffer.diff_base.max_point(),
 608                            ));
 609                        let new_range = tracked_buffer
 610                            .snapshot
 611                            .point_to_offset(Point::new(edit.new.start, 0))
 612                            ..tracked_buffer.snapshot.point_to_offset(cmp::min(
 613                                Point::new(edit.new.end, 0),
 614                                tracked_buffer.snapshot.max_point(),
 615                            ));
 616                        tracked_buffer.diff_base.replace(
 617                            old_range,
 618                            &tracked_buffer
 619                                .snapshot
 620                                .text_for_range(new_range)
 621                                .collect::<String>(),
 622                            cx.background_executor(),
 623                        );
 624                        delta += edit.new_len() as i32 - edit.old_len() as i32;
 625                        false
 626                    }
 627                });
 628                if tracked_buffer.unreviewed_edits.is_empty()
 629                    && let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status
 630                {
 631                    tracked_buffer.status = TrackedBufferStatus::Modified;
 632                }
 633                tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
 634            }
 635        }
 636    }
 637
 638    pub fn reject_edits_in_ranges(
 639        &mut self,
 640        buffer: Entity<Buffer>,
 641        buffer_ranges: Vec<Range<impl language::ToPoint>>,
 642        cx: &mut Context<Self>,
 643    ) -> Task<Result<()>> {
 644        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 645            return Task::ready(Ok(()));
 646        };
 647
 648        match &tracked_buffer.status {
 649            TrackedBufferStatus::Created {
 650                existing_file_content,
 651            } => {
 652                let task = if let Some(existing_file_content) = existing_file_content {
 653                    buffer.update(cx, |buffer, cx| {
 654                        buffer.start_transaction();
 655                        buffer.set_text("", cx);
 656                        for chunk in existing_file_content.chunks() {
 657                            buffer.append(chunk, cx);
 658                        }
 659                        buffer.end_transaction(cx);
 660                    });
 661                    self.project
 662                        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 663                } else {
 664                    // For a file created by AI with no pre-existing content,
 665                    // only delete the file if we're certain it contains only AI content
 666                    // with no edits from the user.
 667
 668                    let initial_version = tracked_buffer.version.clone();
 669                    let current_version = buffer.read(cx).version();
 670
 671                    let current_content = buffer.read(cx).text();
 672                    let tracked_content = tracked_buffer.snapshot.text();
 673
 674                    let is_ai_only_content =
 675                        initial_version == current_version && current_content == tracked_content;
 676
 677                    if is_ai_only_content {
 678                        buffer
 679                            .read(cx)
 680                            .entry_id(cx)
 681                            .and_then(|entry_id| {
 682                                self.project.update(cx, |project, cx| {
 683                                    project.delete_entry(entry_id, false, cx)
 684                                })
 685                            })
 686                            .unwrap_or(Task::ready(Ok(())))
 687                    } else {
 688                        // Not sure how to disentangle edits made by the user
 689                        // from edits made by the AI at this point.
 690                        // For now, preserve both to avoid data loss.
 691                        //
 692                        // TODO: Better solution (disable "Reject" after user makes some
 693                        // edit or find a way to differentiate between AI and user edits)
 694                        Task::ready(Ok(()))
 695                    }
 696                };
 697
 698                self.tracked_buffers.remove(&buffer);
 699                cx.notify();
 700                task
 701            }
 702            TrackedBufferStatus::Deleted => {
 703                buffer.update(cx, |buffer, cx| {
 704                    buffer.set_text(tracked_buffer.diff_base.to_string(), cx)
 705                });
 706                let save = self
 707                    .project
 708                    .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
 709
 710                // Clear all tracked edits for this buffer and start over as if we just read it.
 711                self.tracked_buffers.remove(&buffer);
 712                self.buffer_read(buffer.clone(), cx);
 713                cx.notify();
 714                save
 715            }
 716            TrackedBufferStatus::Modified => {
 717                buffer.update(cx, |buffer, cx| {
 718                    let mut buffer_row_ranges = buffer_ranges
 719                        .into_iter()
 720                        .map(|range| {
 721                            range.start.to_point(buffer).row..range.end.to_point(buffer).row
 722                        })
 723                        .peekable();
 724
 725                    let mut edits_to_revert = Vec::new();
 726                    for edit in tracked_buffer.unreviewed_edits.edits() {
 727                        let new_range = tracked_buffer
 728                            .snapshot
 729                            .anchor_before(Point::new(edit.new.start, 0))
 730                            ..tracked_buffer.snapshot.anchor_after(cmp::min(
 731                                Point::new(edit.new.end, 0),
 732                                tracked_buffer.snapshot.max_point(),
 733                            ));
 734                        let new_row_range = new_range.start.to_point(buffer).row
 735                            ..new_range.end.to_point(buffer).row;
 736
 737                        let mut revert = false;
 738                        while let Some(buffer_row_range) = buffer_row_ranges.peek() {
 739                            if buffer_row_range.end < new_row_range.start {
 740                                buffer_row_ranges.next();
 741                            } else if buffer_row_range.start > new_row_range.end {
 742                                break;
 743                            } else {
 744                                revert = true;
 745                                break;
 746                            }
 747                        }
 748
 749                        if revert {
 750                            let old_range = tracked_buffer
 751                                .diff_base
 752                                .point_to_offset(Point::new(edit.old.start, 0))
 753                                ..tracked_buffer.diff_base.point_to_offset(cmp::min(
 754                                    Point::new(edit.old.end, 0),
 755                                    tracked_buffer.diff_base.max_point(),
 756                                ));
 757                            let old_text = tracked_buffer
 758                                .diff_base
 759                                .chunks_in_range(old_range)
 760                                .collect::<String>();
 761                            edits_to_revert.push((new_range, old_text));
 762                        }
 763                    }
 764
 765                    buffer.edit(edits_to_revert, None, cx);
 766                });
 767                self.project
 768                    .update(cx, |project, cx| project.save_buffer(buffer, cx))
 769            }
 770        }
 771    }
 772
 773    pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) {
 774        self.tracked_buffers
 775            .retain(|_buffer, tracked_buffer| match tracked_buffer.status {
 776                TrackedBufferStatus::Deleted => false,
 777                _ => {
 778                    if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status {
 779                        tracked_buffer.status = TrackedBufferStatus::Modified;
 780                    }
 781                    tracked_buffer.unreviewed_edits.clear();
 782                    tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone();
 783                    tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
 784                    true
 785                }
 786            });
 787        cx.notify();
 788    }
 789
 790    pub fn reject_all_edits(&mut self, cx: &mut Context<Self>) -> Task<()> {
 791        let futures = self.changed_buffers(cx).into_keys().map(|buffer| {
 792            let reject = self.reject_edits_in_ranges(buffer, vec![Anchor::MIN..Anchor::MAX], cx);
 793
 794            async move {
 795                reject.await.log_err();
 796            }
 797        });
 798
 799        let task = futures::future::join_all(futures);
 800
 801        cx.spawn(async move |_, _| {
 802            task.await;
 803        })
 804    }
 805
 806    /// Returns the set of buffers that contain edits that haven't been reviewed by the user.
 807    pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
 808        self.tracked_buffers
 809            .iter()
 810            .filter(|(_, tracked)| tracked.has_edits(cx))
 811            .map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone()))
 812            .collect()
 813    }
 814
 815    /// Iterate over buffers changed since last read or edited by the model
 816    pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
 817        self.tracked_buffers
 818            .iter()
 819            .filter(|(buffer, tracked)| {
 820                let buffer = buffer.read(cx);
 821
 822                tracked.version != buffer.version
 823                    && buffer
 824                        .file()
 825                        .is_some_and(|file| file.disk_state() != DiskState::Deleted)
 826            })
 827            .map(|(buffer, _)| buffer)
 828    }
 829}
 830
 831fn apply_non_conflicting_edits(
 832    patch: &Patch<u32>,
 833    edits: Vec<Edit<u32>>,
 834    old_text: &mut Rope,
 835    new_text: &Rope,
 836    executor: &BackgroundExecutor,
 837) -> bool {
 838    let mut old_edits = patch.edits().iter().cloned().peekable();
 839    let mut new_edits = edits.into_iter().peekable();
 840    let mut applied_delta = 0i32;
 841    let mut rebased_delta = 0i32;
 842    let mut has_made_changes = false;
 843
 844    while let Some(mut new_edit) = new_edits.next() {
 845        let mut conflict = false;
 846
 847        // Push all the old edits that are before this new edit or that intersect with it.
 848        while let Some(old_edit) = old_edits.peek() {
 849            if new_edit.old.end < old_edit.new.start
 850                || (!old_edit.new.is_empty() && new_edit.old.end == old_edit.new.start)
 851            {
 852                break;
 853            } else if new_edit.old.start > old_edit.new.end
 854                || (!old_edit.new.is_empty() && new_edit.old.start == old_edit.new.end)
 855            {
 856                let old_edit = old_edits.next().unwrap();
 857                rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
 858            } else {
 859                conflict = true;
 860                if new_edits
 861                    .peek()
 862                    .is_some_and(|next_edit| next_edit.old.overlaps(&old_edit.new))
 863                {
 864                    new_edit = new_edits.next().unwrap();
 865                } else {
 866                    let old_edit = old_edits.next().unwrap();
 867                    rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
 868                }
 869            }
 870        }
 871
 872        if !conflict {
 873            // This edit doesn't intersect with any old edit, so we can apply it to the old text.
 874            new_edit.old.start = (new_edit.old.start as i32 + applied_delta - rebased_delta) as u32;
 875            new_edit.old.end = (new_edit.old.end as i32 + applied_delta - rebased_delta) as u32;
 876            let old_bytes = old_text.point_to_offset(Point::new(new_edit.old.start, 0))
 877                ..old_text.point_to_offset(cmp::min(
 878                    Point::new(new_edit.old.end, 0),
 879                    old_text.max_point(),
 880                ));
 881            let new_bytes = new_text.point_to_offset(Point::new(new_edit.new.start, 0))
 882                ..new_text.point_to_offset(cmp::min(
 883                    Point::new(new_edit.new.end, 0),
 884                    new_text.max_point(),
 885                ));
 886
 887            old_text.replace(
 888                old_bytes,
 889                &new_text.chunks_in_range(new_bytes).collect::<String>(),
 890                executor,
 891            );
 892            applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32;
 893            has_made_changes = true;
 894        }
 895    }
 896    has_made_changes
 897}
 898
 899fn diff_snapshots(
 900    old_snapshot: &text::BufferSnapshot,
 901    new_snapshot: &text::BufferSnapshot,
 902) -> Vec<Edit<u32>> {
 903    let mut edits = new_snapshot
 904        .edits_since::<Point>(&old_snapshot.version)
 905        .map(|edit| point_to_row_edit(edit, old_snapshot.as_rope(), new_snapshot.as_rope()))
 906        .peekable();
 907    let mut row_edits = Vec::new();
 908    while let Some(mut edit) = edits.next() {
 909        while let Some(next_edit) = edits.peek() {
 910            if edit.old.end >= next_edit.old.start {
 911                edit.old.end = next_edit.old.end;
 912                edit.new.end = next_edit.new.end;
 913                edits.next();
 914            } else {
 915                break;
 916            }
 917        }
 918        row_edits.push(edit);
 919    }
 920    row_edits
 921}
 922
 923fn point_to_row_edit(edit: Edit<Point>, old_text: &Rope, new_text: &Rope) -> Edit<u32> {
 924    if edit.old.start.column == old_text.line_len(edit.old.start.row)
 925        && new_text
 926            .chars_at(new_text.point_to_offset(edit.new.start))
 927            .next()
 928            == Some('\n')
 929        && edit.old.start != old_text.max_point()
 930    {
 931        Edit {
 932            old: edit.old.start.row + 1..edit.old.end.row + 1,
 933            new: edit.new.start.row + 1..edit.new.end.row + 1,
 934        }
 935    } else if edit.old.start.column == 0 && edit.old.end.column == 0 && edit.new.end.column == 0 {
 936        Edit {
 937            old: edit.old.start.row..edit.old.end.row,
 938            new: edit.new.start.row..edit.new.end.row,
 939        }
 940    } else {
 941        Edit {
 942            old: edit.old.start.row..edit.old.end.row + 1,
 943            new: edit.new.start.row..edit.new.end.row + 1,
 944        }
 945    }
 946}
 947
 948#[derive(Copy, Clone, Debug)]
 949enum ChangeAuthor {
 950    User,
 951    Agent,
 952}
 953
 954enum TrackedBufferStatus {
 955    Created { existing_file_content: Option<Rope> },
 956    Modified,
 957    Deleted,
 958}
 959
 960struct TrackedBuffer {
 961    buffer: Entity<Buffer>,
 962    diff_base: Rope,
 963    last_seen_base: Rope,
 964    unreviewed_edits: Patch<u32>,
 965    status: TrackedBufferStatus,
 966    version: clock::Global,
 967    diff: Entity<BufferDiff>,
 968    snapshot: text::BufferSnapshot,
 969    diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
 970    may_have_unnotified_user_edits: bool,
 971    _open_lsp_handle: OpenLspBufferHandle,
 972    _maintain_diff: Task<()>,
 973    _subscription: Subscription,
 974}
 975
 976impl TrackedBuffer {
 977    fn has_edits(&self, cx: &App) -> bool {
 978        self.diff
 979            .read(cx)
 980            .hunks(self.buffer.read(cx), cx)
 981            .next()
 982            .is_some()
 983    }
 984
 985    fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) {
 986        self.diff_update
 987            .unbounded_send((author, self.buffer.read(cx).text_snapshot()))
 988            .ok();
 989    }
 990}
 991
 992pub struct ChangedBuffer {
 993    pub diff: Entity<BufferDiff>,
 994}
 995
 996#[cfg(test)]
 997mod tests {
 998    use super::*;
 999    use buffer_diff::DiffHunkStatusKind;
1000    use gpui::TestAppContext;
1001    use indoc::indoc;
1002    use language::Point;
1003    use project::{FakeFs, Fs, Project, RemoveOptions};
1004    use rand::prelude::*;
1005    use serde_json::json;
1006    use settings::SettingsStore;
1007    use std::env;
1008    use util::{RandomCharIter, path};
1009
1010    #[ctor::ctor]
1011    fn init_logger() {
1012        zlog::init_test();
1013    }
1014
1015    fn init_test(cx: &mut TestAppContext) {
1016        cx.update(|cx| {
1017            let settings_store = SettingsStore::test(cx);
1018            cx.set_global(settings_store);
1019            language::init(cx);
1020            Project::init_settings(cx);
1021        });
1022    }
1023
1024    #[gpui::test(iterations = 10)]
1025    async fn test_keep_edits(cx: &mut TestAppContext) {
1026        init_test(cx);
1027
1028        let fs = FakeFs::new(cx.executor());
1029        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
1030            .await;
1031        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1032        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1033        let file_path = project
1034            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1035            .unwrap();
1036        let buffer = project
1037            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1038            .await
1039            .unwrap();
1040
1041        cx.update(|cx| {
1042            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1043            buffer.update(cx, |buffer, cx| {
1044                buffer
1045                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
1046                    .unwrap()
1047            });
1048            buffer.update(cx, |buffer, cx| {
1049                buffer
1050                    .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
1051                    .unwrap()
1052            });
1053            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1054        });
1055        cx.run_until_parked();
1056        assert_eq!(
1057            buffer.read_with(cx, |buffer, _| buffer.text()),
1058            "abc\ndEf\nghi\njkl\nmnO"
1059        );
1060        assert_eq!(
1061            unreviewed_hunks(&action_log, cx),
1062            vec![(
1063                buffer.clone(),
1064                vec![
1065                    HunkStatus {
1066                        range: Point::new(1, 0)..Point::new(2, 0),
1067                        diff_status: DiffHunkStatusKind::Modified,
1068                        old_text: "def\n".into(),
1069                    },
1070                    HunkStatus {
1071                        range: Point::new(4, 0)..Point::new(4, 3),
1072                        diff_status: DiffHunkStatusKind::Modified,
1073                        old_text: "mno".into(),
1074                    }
1075                ],
1076            )]
1077        );
1078
1079        action_log.update(cx, |log, cx| {
1080            log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
1081        });
1082        cx.run_until_parked();
1083        assert_eq!(
1084            unreviewed_hunks(&action_log, cx),
1085            vec![(
1086                buffer.clone(),
1087                vec![HunkStatus {
1088                    range: Point::new(1, 0)..Point::new(2, 0),
1089                    diff_status: DiffHunkStatusKind::Modified,
1090                    old_text: "def\n".into(),
1091                }],
1092            )]
1093        );
1094
1095        action_log.update(cx, |log, cx| {
1096            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
1097        });
1098        cx.run_until_parked();
1099        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1100    }
1101
1102    #[gpui::test(iterations = 10)]
1103    async fn test_deletions(cx: &mut TestAppContext) {
1104        init_test(cx);
1105
1106        let fs = FakeFs::new(cx.executor());
1107        fs.insert_tree(
1108            path!("/dir"),
1109            json!({"file": "abc\ndef\nghi\njkl\nmno\npqr"}),
1110        )
1111        .await;
1112        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1113        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1114        let file_path = project
1115            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1116            .unwrap();
1117        let buffer = project
1118            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1119            .await
1120            .unwrap();
1121
1122        cx.update(|cx| {
1123            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1124            buffer.update(cx, |buffer, cx| {
1125                buffer
1126                    .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx)
1127                    .unwrap();
1128                buffer.finalize_last_transaction();
1129            });
1130            buffer.update(cx, |buffer, cx| {
1131                buffer
1132                    .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx)
1133                    .unwrap();
1134                buffer.finalize_last_transaction();
1135            });
1136            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1137        });
1138        cx.run_until_parked();
1139        assert_eq!(
1140            buffer.read_with(cx, |buffer, _| buffer.text()),
1141            "abc\nghi\njkl\npqr"
1142        );
1143        assert_eq!(
1144            unreviewed_hunks(&action_log, cx),
1145            vec![(
1146                buffer.clone(),
1147                vec![
1148                    HunkStatus {
1149                        range: Point::new(1, 0)..Point::new(1, 0),
1150                        diff_status: DiffHunkStatusKind::Deleted,
1151                        old_text: "def\n".into(),
1152                    },
1153                    HunkStatus {
1154                        range: Point::new(3, 0)..Point::new(3, 0),
1155                        diff_status: DiffHunkStatusKind::Deleted,
1156                        old_text: "mno\n".into(),
1157                    }
1158                ],
1159            )]
1160        );
1161
1162        buffer.update(cx, |buffer, cx| buffer.undo(cx));
1163        cx.run_until_parked();
1164        assert_eq!(
1165            buffer.read_with(cx, |buffer, _| buffer.text()),
1166            "abc\nghi\njkl\nmno\npqr"
1167        );
1168        assert_eq!(
1169            unreviewed_hunks(&action_log, cx),
1170            vec![(
1171                buffer.clone(),
1172                vec![HunkStatus {
1173                    range: Point::new(1, 0)..Point::new(1, 0),
1174                    diff_status: DiffHunkStatusKind::Deleted,
1175                    old_text: "def\n".into(),
1176                }],
1177            )]
1178        );
1179
1180        action_log.update(cx, |log, cx| {
1181            log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx)
1182        });
1183        cx.run_until_parked();
1184        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1185    }
1186
1187    #[gpui::test(iterations = 10)]
1188    async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
1189        init_test(cx);
1190
1191        let fs = FakeFs::new(cx.executor());
1192        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
1193            .await;
1194        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1195        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1196        let file_path = project
1197            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1198            .unwrap();
1199        let buffer = project
1200            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1201            .await
1202            .unwrap();
1203
1204        cx.update(|cx| {
1205            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1206            buffer.update(cx, |buffer, cx| {
1207                buffer
1208                    .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
1209                    .unwrap()
1210            });
1211            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1212        });
1213        cx.run_until_parked();
1214        assert_eq!(
1215            buffer.read_with(cx, |buffer, _| buffer.text()),
1216            "abc\ndeF\nGHI\njkl\nmno"
1217        );
1218        assert_eq!(
1219            unreviewed_hunks(&action_log, cx),
1220            vec![(
1221                buffer.clone(),
1222                vec![HunkStatus {
1223                    range: Point::new(1, 0)..Point::new(3, 0),
1224                    diff_status: DiffHunkStatusKind::Modified,
1225                    old_text: "def\nghi\n".into(),
1226                }],
1227            )]
1228        );
1229
1230        buffer.update(cx, |buffer, cx| {
1231            buffer.edit(
1232                [
1233                    (Point::new(0, 2)..Point::new(0, 2), "X"),
1234                    (Point::new(3, 0)..Point::new(3, 0), "Y"),
1235                ],
1236                None,
1237                cx,
1238            )
1239        });
1240        cx.run_until_parked();
1241        assert_eq!(
1242            buffer.read_with(cx, |buffer, _| buffer.text()),
1243            "abXc\ndeF\nGHI\nYjkl\nmno"
1244        );
1245        assert_eq!(
1246            unreviewed_hunks(&action_log, cx),
1247            vec![(
1248                buffer.clone(),
1249                vec![HunkStatus {
1250                    range: Point::new(1, 0)..Point::new(3, 0),
1251                    diff_status: DiffHunkStatusKind::Modified,
1252                    old_text: "def\nghi\n".into(),
1253                }],
1254            )]
1255        );
1256
1257        buffer.update(cx, |buffer, cx| {
1258            buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx)
1259        });
1260        cx.run_until_parked();
1261        assert_eq!(
1262            buffer.read_with(cx, |buffer, _| buffer.text()),
1263            "abXc\ndZeF\nGHI\nYjkl\nmno"
1264        );
1265        assert_eq!(
1266            unreviewed_hunks(&action_log, cx),
1267            vec![(
1268                buffer.clone(),
1269                vec![HunkStatus {
1270                    range: Point::new(1, 0)..Point::new(3, 0),
1271                    diff_status: DiffHunkStatusKind::Modified,
1272                    old_text: "def\nghi\n".into(),
1273                }],
1274            )]
1275        );
1276
1277        action_log.update(cx, |log, cx| {
1278            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
1279        });
1280        cx.run_until_parked();
1281        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1282    }
1283
1284    #[gpui::test(iterations = 10)]
1285    async fn test_user_edits_notifications(cx: &mut TestAppContext) {
1286        init_test(cx);
1287
1288        let fs = FakeFs::new(cx.executor());
1289        fs.insert_tree(
1290            path!("/dir"),
1291            json!({"file": indoc! {"
1292            abc
1293            def
1294            ghi
1295            jkl
1296            mno"}}),
1297        )
1298        .await;
1299        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1300        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1301        let file_path = project
1302            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1303            .unwrap();
1304        let buffer = project
1305            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1306            .await
1307            .unwrap();
1308
1309        // Agent edits
1310        cx.update(|cx| {
1311            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1312            buffer.update(cx, |buffer, cx| {
1313                buffer
1314                    .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
1315                    .unwrap()
1316            });
1317            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1318        });
1319        cx.run_until_parked();
1320        assert_eq!(
1321            buffer.read_with(cx, |buffer, _| buffer.text()),
1322            indoc! {"
1323                abc
1324                deF
1325                GHI
1326                jkl
1327                mno"}
1328        );
1329        assert_eq!(
1330            unreviewed_hunks(&action_log, cx),
1331            vec![(
1332                buffer.clone(),
1333                vec![HunkStatus {
1334                    range: Point::new(1, 0)..Point::new(3, 0),
1335                    diff_status: DiffHunkStatusKind::Modified,
1336                    old_text: "def\nghi\n".into(),
1337                }],
1338            )]
1339        );
1340
1341        // User edits
1342        buffer.update(cx, |buffer, cx| {
1343            buffer.edit(
1344                [
1345                    (Point::new(0, 2)..Point::new(0, 2), "X"),
1346                    (Point::new(3, 0)..Point::new(3, 0), "Y"),
1347                ],
1348                None,
1349                cx,
1350            )
1351        });
1352        cx.run_until_parked();
1353        assert_eq!(
1354            buffer.read_with(cx, |buffer, _| buffer.text()),
1355            indoc! {"
1356                abXc
1357                deF
1358                GHI
1359                Yjkl
1360                mno"}
1361        );
1362
1363        // User edits should be stored separately from agent's
1364        let user_edits = action_log.update(cx, |log, cx| log.unnotified_user_edits(cx));
1365        assert_eq!(
1366            user_edits.expect("should have some user edits"),
1367            indoc! {"
1368                --- a/dir/file
1369                +++ b/dir/file
1370                @@ -1,5 +1,5 @@
1371                -abc
1372                +abXc
1373                 def
1374                 ghi
1375                -jkl
1376                +Yjkl
1377                 mno
1378            "}
1379        );
1380
1381        action_log.update(cx, |log, cx| {
1382            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
1383        });
1384        cx.run_until_parked();
1385        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1386    }
1387
1388    #[gpui::test(iterations = 10)]
1389    async fn test_creating_files(cx: &mut TestAppContext) {
1390        init_test(cx);
1391
1392        let fs = FakeFs::new(cx.executor());
1393        fs.insert_tree(path!("/dir"), json!({})).await;
1394        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1395        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1396        let file_path = project
1397            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
1398            .unwrap();
1399
1400        let buffer = project
1401            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1402            .await
1403            .unwrap();
1404        cx.update(|cx| {
1405            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
1406            buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
1407            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1408        });
1409        project
1410            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1411            .await
1412            .unwrap();
1413        cx.run_until_parked();
1414        assert_eq!(
1415            unreviewed_hunks(&action_log, cx),
1416            vec![(
1417                buffer.clone(),
1418                vec![HunkStatus {
1419                    range: Point::new(0, 0)..Point::new(0, 5),
1420                    diff_status: DiffHunkStatusKind::Added,
1421                    old_text: "".into(),
1422                }],
1423            )]
1424        );
1425
1426        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx));
1427        cx.run_until_parked();
1428        assert_eq!(
1429            unreviewed_hunks(&action_log, cx),
1430            vec![(
1431                buffer.clone(),
1432                vec![HunkStatus {
1433                    range: Point::new(0, 0)..Point::new(0, 6),
1434                    diff_status: DiffHunkStatusKind::Added,
1435                    old_text: "".into(),
1436                }],
1437            )]
1438        );
1439
1440        action_log.update(cx, |log, cx| {
1441            log.keep_edits_in_range(buffer.clone(), 0..5, cx)
1442        });
1443        cx.run_until_parked();
1444        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1445    }
1446
1447    #[gpui::test(iterations = 10)]
1448    async fn test_overwriting_files(cx: &mut TestAppContext) {
1449        init_test(cx);
1450
1451        let fs = FakeFs::new(cx.executor());
1452        fs.insert_tree(
1453            path!("/dir"),
1454            json!({
1455                "file1": "Lorem ipsum dolor"
1456            }),
1457        )
1458        .await;
1459        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1460        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1461        let file_path = project
1462            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
1463            .unwrap();
1464
1465        let buffer = project
1466            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1467            .await
1468            .unwrap();
1469        cx.update(|cx| {
1470            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
1471            buffer.update(cx, |buffer, cx| buffer.set_text("sit amet consecteur", cx));
1472            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1473        });
1474        project
1475            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1476            .await
1477            .unwrap();
1478        cx.run_until_parked();
1479        assert_eq!(
1480            unreviewed_hunks(&action_log, cx),
1481            vec![(
1482                buffer.clone(),
1483                vec![HunkStatus {
1484                    range: Point::new(0, 0)..Point::new(0, 19),
1485                    diff_status: DiffHunkStatusKind::Added,
1486                    old_text: "".into(),
1487                }],
1488            )]
1489        );
1490
1491        action_log
1492            .update(cx, |log, cx| {
1493                log.reject_edits_in_ranges(buffer.clone(), vec![2..5], cx)
1494            })
1495            .await
1496            .unwrap();
1497        cx.run_until_parked();
1498        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1499        assert_eq!(
1500            buffer.read_with(cx, |buffer, _cx| buffer.text()),
1501            "Lorem ipsum dolor"
1502        );
1503    }
1504
1505    #[gpui::test(iterations = 10)]
1506    async fn test_overwriting_previously_edited_files(cx: &mut TestAppContext) {
1507        init_test(cx);
1508
1509        let fs = FakeFs::new(cx.executor());
1510        fs.insert_tree(
1511            path!("/dir"),
1512            json!({
1513                "file1": "Lorem ipsum dolor"
1514            }),
1515        )
1516        .await;
1517        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1518        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1519        let file_path = project
1520            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
1521            .unwrap();
1522
1523        let buffer = project
1524            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1525            .await
1526            .unwrap();
1527        cx.update(|cx| {
1528            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1529            buffer.update(cx, |buffer, cx| buffer.append(" sit amet consecteur", cx));
1530            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1531        });
1532        project
1533            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1534            .await
1535            .unwrap();
1536        cx.run_until_parked();
1537        assert_eq!(
1538            unreviewed_hunks(&action_log, cx),
1539            vec![(
1540                buffer.clone(),
1541                vec![HunkStatus {
1542                    range: Point::new(0, 0)..Point::new(0, 37),
1543                    diff_status: DiffHunkStatusKind::Modified,
1544                    old_text: "Lorem ipsum dolor".into(),
1545                }],
1546            )]
1547        );
1548
1549        cx.update(|cx| {
1550            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
1551            buffer.update(cx, |buffer, cx| buffer.set_text("rewritten", cx));
1552            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1553        });
1554        project
1555            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1556            .await
1557            .unwrap();
1558        cx.run_until_parked();
1559        assert_eq!(
1560            unreviewed_hunks(&action_log, cx),
1561            vec![(
1562                buffer.clone(),
1563                vec![HunkStatus {
1564                    range: Point::new(0, 0)..Point::new(0, 9),
1565                    diff_status: DiffHunkStatusKind::Added,
1566                    old_text: "".into(),
1567                }],
1568            )]
1569        );
1570
1571        action_log
1572            .update(cx, |log, cx| {
1573                log.reject_edits_in_ranges(buffer.clone(), vec![2..5], cx)
1574            })
1575            .await
1576            .unwrap();
1577        cx.run_until_parked();
1578        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1579        assert_eq!(
1580            buffer.read_with(cx, |buffer, _cx| buffer.text()),
1581            "Lorem ipsum dolor"
1582        );
1583    }
1584
1585    #[gpui::test(iterations = 10)]
1586    async fn test_deleting_files(cx: &mut TestAppContext) {
1587        init_test(cx);
1588
1589        let fs = FakeFs::new(cx.executor());
1590        fs.insert_tree(
1591            path!("/dir"),
1592            json!({"file1": "lorem\n", "file2": "ipsum\n"}),
1593        )
1594        .await;
1595
1596        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1597        let file1_path = project
1598            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
1599            .unwrap();
1600        let file2_path = project
1601            .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
1602            .unwrap();
1603
1604        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1605        let buffer1 = project
1606            .update(cx, |project, cx| {
1607                project.open_buffer(file1_path.clone(), cx)
1608            })
1609            .await
1610            .unwrap();
1611        let buffer2 = project
1612            .update(cx, |project, cx| {
1613                project.open_buffer(file2_path.clone(), cx)
1614            })
1615            .await
1616            .unwrap();
1617
1618        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
1619        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
1620        project
1621            .update(cx, |project, cx| {
1622                project.delete_file(file1_path.clone(), false, cx)
1623            })
1624            .unwrap()
1625            .await
1626            .unwrap();
1627        project
1628            .update(cx, |project, cx| {
1629                project.delete_file(file2_path.clone(), false, cx)
1630            })
1631            .unwrap()
1632            .await
1633            .unwrap();
1634        cx.run_until_parked();
1635        assert_eq!(
1636            unreviewed_hunks(&action_log, cx),
1637            vec![
1638                (
1639                    buffer1.clone(),
1640                    vec![HunkStatus {
1641                        range: Point::new(0, 0)..Point::new(0, 0),
1642                        diff_status: DiffHunkStatusKind::Deleted,
1643                        old_text: "lorem\n".into(),
1644                    }]
1645                ),
1646                (
1647                    buffer2.clone(),
1648                    vec![HunkStatus {
1649                        range: Point::new(0, 0)..Point::new(0, 0),
1650                        diff_status: DiffHunkStatusKind::Deleted,
1651                        old_text: "ipsum\n".into(),
1652                    }],
1653                )
1654            ]
1655        );
1656
1657        // Simulate file1 being recreated externally.
1658        fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
1659            .await;
1660
1661        // Simulate file2 being recreated by a tool.
1662        let buffer2 = project
1663            .update(cx, |project, cx| project.open_buffer(file2_path, cx))
1664            .await
1665            .unwrap();
1666        action_log.update(cx, |log, cx| log.buffer_created(buffer2.clone(), cx));
1667        buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
1668        action_log.update(cx, |log, cx| log.buffer_edited(buffer2.clone(), cx));
1669        project
1670            .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
1671            .await
1672            .unwrap();
1673
1674        cx.run_until_parked();
1675        assert_eq!(
1676            unreviewed_hunks(&action_log, cx),
1677            vec![(
1678                buffer2.clone(),
1679                vec![HunkStatus {
1680                    range: Point::new(0, 0)..Point::new(0, 5),
1681                    diff_status: DiffHunkStatusKind::Added,
1682                    old_text: "".into(),
1683                }],
1684            )]
1685        );
1686
1687        // Simulate file2 being deleted externally.
1688        fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
1689            .await
1690            .unwrap();
1691        cx.run_until_parked();
1692        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1693    }
1694
1695    #[gpui::test(iterations = 10)]
1696    async fn test_reject_edits(cx: &mut TestAppContext) {
1697        init_test(cx);
1698
1699        let fs = FakeFs::new(cx.executor());
1700        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
1701            .await;
1702        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1703        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1704        let file_path = project
1705            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1706            .unwrap();
1707        let buffer = project
1708            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1709            .await
1710            .unwrap();
1711
1712        cx.update(|cx| {
1713            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1714            buffer.update(cx, |buffer, cx| {
1715                buffer
1716                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
1717                    .unwrap()
1718            });
1719            buffer.update(cx, |buffer, cx| {
1720                buffer
1721                    .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
1722                    .unwrap()
1723            });
1724            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1725        });
1726        cx.run_until_parked();
1727        assert_eq!(
1728            buffer.read_with(cx, |buffer, _| buffer.text()),
1729            "abc\ndE\nXYZf\nghi\njkl\nmnO"
1730        );
1731        assert_eq!(
1732            unreviewed_hunks(&action_log, cx),
1733            vec![(
1734                buffer.clone(),
1735                vec![
1736                    HunkStatus {
1737                        range: Point::new(1, 0)..Point::new(3, 0),
1738                        diff_status: DiffHunkStatusKind::Modified,
1739                        old_text: "def\n".into(),
1740                    },
1741                    HunkStatus {
1742                        range: Point::new(5, 0)..Point::new(5, 3),
1743                        diff_status: DiffHunkStatusKind::Modified,
1744                        old_text: "mno".into(),
1745                    }
1746                ],
1747            )]
1748        );
1749
1750        // If the rejected range doesn't overlap with any hunk, we ignore it.
1751        action_log
1752            .update(cx, |log, cx| {
1753                log.reject_edits_in_ranges(
1754                    buffer.clone(),
1755                    vec![Point::new(4, 0)..Point::new(4, 0)],
1756                    cx,
1757                )
1758            })
1759            .await
1760            .unwrap();
1761        cx.run_until_parked();
1762        assert_eq!(
1763            buffer.read_with(cx, |buffer, _| buffer.text()),
1764            "abc\ndE\nXYZf\nghi\njkl\nmnO"
1765        );
1766        assert_eq!(
1767            unreviewed_hunks(&action_log, cx),
1768            vec![(
1769                buffer.clone(),
1770                vec![
1771                    HunkStatus {
1772                        range: Point::new(1, 0)..Point::new(3, 0),
1773                        diff_status: DiffHunkStatusKind::Modified,
1774                        old_text: "def\n".into(),
1775                    },
1776                    HunkStatus {
1777                        range: Point::new(5, 0)..Point::new(5, 3),
1778                        diff_status: DiffHunkStatusKind::Modified,
1779                        old_text: "mno".into(),
1780                    }
1781                ],
1782            )]
1783        );
1784
1785        action_log
1786            .update(cx, |log, cx| {
1787                log.reject_edits_in_ranges(
1788                    buffer.clone(),
1789                    vec![Point::new(0, 0)..Point::new(1, 0)],
1790                    cx,
1791                )
1792            })
1793            .await
1794            .unwrap();
1795        cx.run_until_parked();
1796        assert_eq!(
1797            buffer.read_with(cx, |buffer, _| buffer.text()),
1798            "abc\ndef\nghi\njkl\nmnO"
1799        );
1800        assert_eq!(
1801            unreviewed_hunks(&action_log, cx),
1802            vec![(
1803                buffer.clone(),
1804                vec![HunkStatus {
1805                    range: Point::new(4, 0)..Point::new(4, 3),
1806                    diff_status: DiffHunkStatusKind::Modified,
1807                    old_text: "mno".into(),
1808                }],
1809            )]
1810        );
1811
1812        action_log
1813            .update(cx, |log, cx| {
1814                log.reject_edits_in_ranges(
1815                    buffer.clone(),
1816                    vec![Point::new(4, 0)..Point::new(4, 0)],
1817                    cx,
1818                )
1819            })
1820            .await
1821            .unwrap();
1822        cx.run_until_parked();
1823        assert_eq!(
1824            buffer.read_with(cx, |buffer, _| buffer.text()),
1825            "abc\ndef\nghi\njkl\nmno"
1826        );
1827        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1828    }
1829
1830    #[gpui::test(iterations = 10)]
1831    async fn test_reject_multiple_edits(cx: &mut TestAppContext) {
1832        init_test(cx);
1833
1834        let fs = FakeFs::new(cx.executor());
1835        fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
1836            .await;
1837        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1838        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1839        let file_path = project
1840            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1841            .unwrap();
1842        let buffer = project
1843            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1844            .await
1845            .unwrap();
1846
1847        cx.update(|cx| {
1848            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1849            buffer.update(cx, |buffer, cx| {
1850                buffer
1851                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
1852                    .unwrap()
1853            });
1854            buffer.update(cx, |buffer, cx| {
1855                buffer
1856                    .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
1857                    .unwrap()
1858            });
1859            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1860        });
1861        cx.run_until_parked();
1862        assert_eq!(
1863            buffer.read_with(cx, |buffer, _| buffer.text()),
1864            "abc\ndE\nXYZf\nghi\njkl\nmnO"
1865        );
1866        assert_eq!(
1867            unreviewed_hunks(&action_log, cx),
1868            vec![(
1869                buffer.clone(),
1870                vec![
1871                    HunkStatus {
1872                        range: Point::new(1, 0)..Point::new(3, 0),
1873                        diff_status: DiffHunkStatusKind::Modified,
1874                        old_text: "def\n".into(),
1875                    },
1876                    HunkStatus {
1877                        range: Point::new(5, 0)..Point::new(5, 3),
1878                        diff_status: DiffHunkStatusKind::Modified,
1879                        old_text: "mno".into(),
1880                    }
1881                ],
1882            )]
1883        );
1884
1885        action_log.update(cx, |log, cx| {
1886            let range_1 = buffer.read(cx).anchor_before(Point::new(0, 0))
1887                ..buffer.read(cx).anchor_before(Point::new(1, 0));
1888            let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0))
1889                ..buffer.read(cx).anchor_before(Point::new(5, 3));
1890
1891            log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], cx)
1892                .detach();
1893            assert_eq!(
1894                buffer.read_with(cx, |buffer, _| buffer.text()),
1895                "abc\ndef\nghi\njkl\nmno"
1896            );
1897        });
1898        cx.run_until_parked();
1899        assert_eq!(
1900            buffer.read_with(cx, |buffer, _| buffer.text()),
1901            "abc\ndef\nghi\njkl\nmno"
1902        );
1903        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1904    }
1905
1906    #[gpui::test(iterations = 10)]
1907    async fn test_reject_deleted_file(cx: &mut TestAppContext) {
1908        init_test(cx);
1909
1910        let fs = FakeFs::new(cx.executor());
1911        fs.insert_tree(path!("/dir"), json!({"file": "content"}))
1912            .await;
1913        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1914        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1915        let file_path = project
1916            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
1917            .unwrap();
1918        let buffer = project
1919            .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
1920            .await
1921            .unwrap();
1922
1923        cx.update(|cx| {
1924            action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx));
1925        });
1926        project
1927            .update(cx, |project, cx| {
1928                project.delete_file(file_path.clone(), false, cx)
1929            })
1930            .unwrap()
1931            .await
1932            .unwrap();
1933        cx.run_until_parked();
1934        assert!(!fs.is_file(path!("/dir/file").as_ref()).await);
1935        assert_eq!(
1936            unreviewed_hunks(&action_log, cx),
1937            vec![(
1938                buffer.clone(),
1939                vec![HunkStatus {
1940                    range: Point::new(0, 0)..Point::new(0, 0),
1941                    diff_status: DiffHunkStatusKind::Deleted,
1942                    old_text: "content".into(),
1943                }]
1944            )]
1945        );
1946
1947        action_log
1948            .update(cx, |log, cx| {
1949                log.reject_edits_in_ranges(
1950                    buffer.clone(),
1951                    vec![Point::new(0, 0)..Point::new(0, 0)],
1952                    cx,
1953                )
1954            })
1955            .await
1956            .unwrap();
1957        cx.run_until_parked();
1958        assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "content");
1959        assert!(fs.is_file(path!("/dir/file").as_ref()).await);
1960        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
1961    }
1962
1963    #[gpui::test(iterations = 10)]
1964    async fn test_reject_created_file(cx: &mut TestAppContext) {
1965        init_test(cx);
1966
1967        let fs = FakeFs::new(cx.executor());
1968        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1969        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1970        let file_path = project
1971            .read_with(cx, |project, cx| {
1972                project.find_project_path("dir/new_file", cx)
1973            })
1974            .unwrap();
1975        let buffer = project
1976            .update(cx, |project, cx| project.open_buffer(file_path, cx))
1977            .await
1978            .unwrap();
1979        cx.update(|cx| {
1980            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
1981            buffer.update(cx, |buffer, cx| buffer.set_text("content", cx));
1982            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1983        });
1984        project
1985            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1986            .await
1987            .unwrap();
1988        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
1989        cx.run_until_parked();
1990        assert_eq!(
1991            unreviewed_hunks(&action_log, cx),
1992            vec![(
1993                buffer.clone(),
1994                vec![HunkStatus {
1995                    range: Point::new(0, 0)..Point::new(0, 7),
1996                    diff_status: DiffHunkStatusKind::Added,
1997                    old_text: "".into(),
1998                }],
1999            )]
2000        );
2001
2002        action_log
2003            .update(cx, |log, cx| {
2004                log.reject_edits_in_ranges(
2005                    buffer.clone(),
2006                    vec![Point::new(0, 0)..Point::new(0, 11)],
2007                    cx,
2008                )
2009            })
2010            .await
2011            .unwrap();
2012        cx.run_until_parked();
2013        assert!(!fs.is_file(path!("/dir/new_file").as_ref()).await);
2014        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2015    }
2016
2017    #[gpui::test]
2018    async fn test_reject_created_file_with_user_edits(cx: &mut TestAppContext) {
2019        init_test(cx);
2020
2021        let fs = FakeFs::new(cx.executor());
2022        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2023        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2024
2025        let file_path = project
2026            .read_with(cx, |project, cx| {
2027                project.find_project_path("dir/new_file", cx)
2028            })
2029            .unwrap();
2030        let buffer = project
2031            .update(cx, |project, cx| project.open_buffer(file_path, cx))
2032            .await
2033            .unwrap();
2034
2035        // AI creates file with initial content
2036        cx.update(|cx| {
2037            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
2038            buffer.update(cx, |buffer, cx| buffer.set_text("ai content", cx));
2039            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2040        });
2041
2042        project
2043            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2044            .await
2045            .unwrap();
2046
2047        cx.run_until_parked();
2048
2049        // User makes additional edits
2050        cx.update(|cx| {
2051            buffer.update(cx, |buffer, cx| {
2052                buffer.edit([(10..10, "\nuser added this line")], None, cx);
2053            });
2054        });
2055
2056        project
2057            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2058            .await
2059            .unwrap();
2060
2061        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
2062
2063        // Reject all
2064        action_log
2065            .update(cx, |log, cx| {
2066                log.reject_edits_in_ranges(
2067                    buffer.clone(),
2068                    vec![Point::new(0, 0)..Point::new(100, 0)],
2069                    cx,
2070                )
2071            })
2072            .await
2073            .unwrap();
2074        cx.run_until_parked();
2075
2076        // File should still contain all the content
2077        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
2078
2079        let content = buffer.read_with(cx, |buffer, _| buffer.text());
2080        assert_eq!(content, "ai content\nuser added this line");
2081    }
2082
2083    #[gpui::test]
2084    async fn test_reject_after_accepting_hunk_on_created_file(cx: &mut TestAppContext) {
2085        init_test(cx);
2086
2087        let fs = FakeFs::new(cx.executor());
2088        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2089        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2090
2091        let file_path = project
2092            .read_with(cx, |project, cx| {
2093                project.find_project_path("dir/new_file", cx)
2094            })
2095            .unwrap();
2096        let buffer = project
2097            .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
2098            .await
2099            .unwrap();
2100
2101        // AI creates file with initial content
2102        cx.update(|cx| {
2103            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
2104            buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx));
2105            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2106        });
2107        project
2108            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2109            .await
2110            .unwrap();
2111        cx.run_until_parked();
2112        assert_ne!(unreviewed_hunks(&action_log, cx), vec![]);
2113
2114        // User accepts the single hunk
2115        action_log.update(cx, |log, cx| {
2116            log.keep_edits_in_range(buffer.clone(), Anchor::MIN..Anchor::MAX, cx)
2117        });
2118        cx.run_until_parked();
2119        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2120        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
2121
2122        // AI modifies the file
2123        cx.update(|cx| {
2124            buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx));
2125            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2126        });
2127        project
2128            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2129            .await
2130            .unwrap();
2131        cx.run_until_parked();
2132        assert_ne!(unreviewed_hunks(&action_log, cx), vec![]);
2133
2134        // User rejects the hunk
2135        action_log
2136            .update(cx, |log, cx| {
2137                log.reject_edits_in_ranges(buffer.clone(), vec![Anchor::MIN..Anchor::MAX], cx)
2138            })
2139            .await
2140            .unwrap();
2141        cx.run_until_parked();
2142        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await,);
2143        assert_eq!(
2144            buffer.read_with(cx, |buffer, _| buffer.text()),
2145            "ai content v1"
2146        );
2147        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2148    }
2149
2150    #[gpui::test]
2151    async fn test_reject_edits_on_previously_accepted_created_file(cx: &mut TestAppContext) {
2152        init_test(cx);
2153
2154        let fs = FakeFs::new(cx.executor());
2155        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2156        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2157
2158        let file_path = project
2159            .read_with(cx, |project, cx| {
2160                project.find_project_path("dir/new_file", cx)
2161            })
2162            .unwrap();
2163        let buffer = project
2164            .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
2165            .await
2166            .unwrap();
2167
2168        // AI creates file with initial content
2169        cx.update(|cx| {
2170            action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
2171            buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx));
2172            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2173        });
2174        project
2175            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2176            .await
2177            .unwrap();
2178        cx.run_until_parked();
2179
2180        // User clicks "Accept All"
2181        action_log.update(cx, |log, cx| log.keep_all_edits(cx));
2182        cx.run_until_parked();
2183        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
2184        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); // Hunks are cleared
2185
2186        // AI modifies file again
2187        cx.update(|cx| {
2188            buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx));
2189            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2190        });
2191        project
2192            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2193            .await
2194            .unwrap();
2195        cx.run_until_parked();
2196        assert_ne!(unreviewed_hunks(&action_log, cx), vec![]);
2197
2198        // User clicks "Reject All"
2199        action_log
2200            .update(cx, |log, cx| log.reject_all_edits(cx))
2201            .await;
2202        cx.run_until_parked();
2203        assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
2204        assert_eq!(
2205            buffer.read_with(cx, |buffer, _| buffer.text()),
2206            "ai content v1"
2207        );
2208        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2209    }
2210
2211    #[gpui::test(iterations = 100)]
2212    async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
2213        init_test(cx);
2214
2215        let operations = env::var("OPERATIONS")
2216            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2217            .unwrap_or(20);
2218
2219        let text = RandomCharIter::new(&mut rng).take(50).collect::<String>();
2220        let fs = FakeFs::new(cx.executor());
2221        fs.insert_tree(path!("/dir"), json!({"file": text})).await;
2222        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2223        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2224        let file_path = project
2225            .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
2226            .unwrap();
2227        let buffer = project
2228            .update(cx, |project, cx| project.open_buffer(file_path, cx))
2229            .await
2230            .unwrap();
2231
2232        action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
2233
2234        for _ in 0..operations {
2235            match rng.random_range(0..100) {
2236                0..25 => {
2237                    action_log.update(cx, |log, cx| {
2238                        let range = buffer.read(cx).random_byte_range(0, &mut rng);
2239                        log::info!("keeping edits in range {:?}", range);
2240                        log.keep_edits_in_range(buffer.clone(), range, cx)
2241                    });
2242                }
2243                25..50 => {
2244                    action_log
2245                        .update(cx, |log, cx| {
2246                            let range = buffer.read(cx).random_byte_range(0, &mut rng);
2247                            log::info!("rejecting edits in range {:?}", range);
2248                            log.reject_edits_in_ranges(buffer.clone(), vec![range], cx)
2249                        })
2250                        .await
2251                        .unwrap();
2252                }
2253                _ => {
2254                    let is_agent_edit = rng.random_bool(0.5);
2255                    if is_agent_edit {
2256                        log::info!("agent edit");
2257                    } else {
2258                        log::info!("user edit");
2259                    }
2260                    cx.update(|cx| {
2261                        buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
2262                        if is_agent_edit {
2263                            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2264                        }
2265                    });
2266                }
2267            }
2268
2269            if rng.random_bool(0.2) {
2270                quiesce(&action_log, &buffer, cx);
2271            }
2272        }
2273
2274        quiesce(&action_log, &buffer, cx);
2275
2276        fn quiesce(
2277            action_log: &Entity<ActionLog>,
2278            buffer: &Entity<Buffer>,
2279            cx: &mut TestAppContext,
2280        ) {
2281            log::info!("quiescing...");
2282            cx.run_until_parked();
2283            action_log.update(cx, |log, cx| {
2284                let tracked_buffer = log.tracked_buffers.get(buffer).unwrap();
2285                let mut old_text = tracked_buffer.diff_base.clone();
2286                let new_text = buffer.read(cx).as_rope();
2287                for edit in tracked_buffer.unreviewed_edits.edits() {
2288                    let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
2289                    let old_end = old_text.point_to_offset(cmp::min(
2290                        Point::new(edit.new.start + edit.old_len(), 0),
2291                        old_text.max_point(),
2292                    ));
2293                    old_text.replace(
2294                        old_start..old_end,
2295                        &new_text.slice_rows(edit.new.clone()).to_string(),
2296                        cx.background_executor(),
2297                    );
2298                }
2299                pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
2300            })
2301        }
2302    }
2303
2304    #[gpui::test]
2305    async fn test_keep_edits_on_commit(cx: &mut gpui::TestAppContext) {
2306        init_test(cx);
2307
2308        let fs = FakeFs::new(cx.background_executor.clone());
2309        fs.insert_tree(
2310            path!("/project"),
2311            json!({
2312                ".git": {},
2313                "file.txt": "a\nb\nc\nd\ne\nf\ng\nh\ni\nj",
2314            }),
2315        )
2316        .await;
2317        fs.set_head_for_repo(
2318            path!("/project/.git").as_ref(),
2319            &[("file.txt", "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())],
2320            "0000000",
2321        );
2322        cx.run_until_parked();
2323
2324        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
2325        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2326
2327        let file_path = project
2328            .read_with(cx, |project, cx| {
2329                project.find_project_path(path!("/project/file.txt"), cx)
2330            })
2331            .unwrap();
2332        let buffer = project
2333            .update(cx, |project, cx| project.open_buffer(file_path, cx))
2334            .await
2335            .unwrap();
2336
2337        cx.update(|cx| {
2338            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
2339            buffer.update(cx, |buffer, cx| {
2340                buffer.edit(
2341                    [
2342                        // Edit at the very start: a -> A
2343                        (Point::new(0, 0)..Point::new(0, 1), "A"),
2344                        // Deletion in the middle: remove lines d and e
2345                        (Point::new(3, 0)..Point::new(5, 0), ""),
2346                        // Modification: g -> GGG
2347                        (Point::new(6, 0)..Point::new(6, 1), "GGG"),
2348                        // Addition: insert new line after h
2349                        (Point::new(7, 1)..Point::new(7, 1), "\nNEW"),
2350                        // Edit the very last character: j -> J
2351                        (Point::new(9, 0)..Point::new(9, 1), "J"),
2352                    ],
2353                    None,
2354                    cx,
2355                );
2356            });
2357            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
2358        });
2359        cx.run_until_parked();
2360        assert_eq!(
2361            unreviewed_hunks(&action_log, cx),
2362            vec![(
2363                buffer.clone(),
2364                vec![
2365                    HunkStatus {
2366                        range: Point::new(0, 0)..Point::new(1, 0),
2367                        diff_status: DiffHunkStatusKind::Modified,
2368                        old_text: "a\n".into()
2369                    },
2370                    HunkStatus {
2371                        range: Point::new(3, 0)..Point::new(3, 0),
2372                        diff_status: DiffHunkStatusKind::Deleted,
2373                        old_text: "d\ne\n".into()
2374                    },
2375                    HunkStatus {
2376                        range: Point::new(4, 0)..Point::new(5, 0),
2377                        diff_status: DiffHunkStatusKind::Modified,
2378                        old_text: "g\n".into()
2379                    },
2380                    HunkStatus {
2381                        range: Point::new(6, 0)..Point::new(7, 0),
2382                        diff_status: DiffHunkStatusKind::Added,
2383                        old_text: "".into()
2384                    },
2385                    HunkStatus {
2386                        range: Point::new(8, 0)..Point::new(8, 1),
2387                        diff_status: DiffHunkStatusKind::Modified,
2388                        old_text: "j".into()
2389                    }
2390                ]
2391            )]
2392        );
2393
2394        // Simulate a git commit that matches some edits but not others:
2395        // - Accepts the first edit (a -> A)
2396        // - Accepts the deletion (remove d and e)
2397        // - Makes a different change to g (g -> G instead of GGG)
2398        // - Ignores the NEW line addition
2399        // - Ignores the last line edit (j stays as j)
2400        fs.set_head_for_repo(
2401            path!("/project/.git").as_ref(),
2402            &[("file.txt", "A\nb\nc\nf\nG\nh\ni\nj".into())],
2403            "0000001",
2404        );
2405        cx.run_until_parked();
2406        assert_eq!(
2407            unreviewed_hunks(&action_log, cx),
2408            vec![(
2409                buffer.clone(),
2410                vec![
2411                    HunkStatus {
2412                        range: Point::new(4, 0)..Point::new(5, 0),
2413                        diff_status: DiffHunkStatusKind::Modified,
2414                        old_text: "g\n".into()
2415                    },
2416                    HunkStatus {
2417                        range: Point::new(6, 0)..Point::new(7, 0),
2418                        diff_status: DiffHunkStatusKind::Added,
2419                        old_text: "".into()
2420                    },
2421                    HunkStatus {
2422                        range: Point::new(8, 0)..Point::new(8, 1),
2423                        diff_status: DiffHunkStatusKind::Modified,
2424                        old_text: "j".into()
2425                    }
2426                ]
2427            )]
2428        );
2429
2430        // Make another commit that accepts the NEW line but with different content
2431        fs.set_head_for_repo(
2432            path!("/project/.git").as_ref(),
2433            &[("file.txt", "A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into())],
2434            "0000002",
2435        );
2436        cx.run_until_parked();
2437        assert_eq!(
2438            unreviewed_hunks(&action_log, cx),
2439            vec![(
2440                buffer,
2441                vec![
2442                    HunkStatus {
2443                        range: Point::new(6, 0)..Point::new(7, 0),
2444                        diff_status: DiffHunkStatusKind::Added,
2445                        old_text: "".into()
2446                    },
2447                    HunkStatus {
2448                        range: Point::new(8, 0)..Point::new(8, 1),
2449                        diff_status: DiffHunkStatusKind::Modified,
2450                        old_text: "j".into()
2451                    }
2452                ]
2453            )]
2454        );
2455
2456        // Final commit that accepts all remaining edits
2457        fs.set_head_for_repo(
2458            path!("/project/.git").as_ref(),
2459            &[("file.txt", "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())],
2460            "0000003",
2461        );
2462        cx.run_until_parked();
2463        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
2464    }
2465
2466    #[derive(Debug, Clone, PartialEq, Eq)]
2467    struct HunkStatus {
2468        range: Range<Point>,
2469        diff_status: DiffHunkStatusKind,
2470        old_text: String,
2471    }
2472
2473    fn unreviewed_hunks(
2474        action_log: &Entity<ActionLog>,
2475        cx: &TestAppContext,
2476    ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
2477        cx.read(|cx| {
2478            action_log
2479                .read(cx)
2480                .changed_buffers(cx)
2481                .into_iter()
2482                .map(|(buffer, diff)| {
2483                    let snapshot = buffer.read(cx).snapshot();
2484                    (
2485                        buffer,
2486                        diff.read(cx)
2487                            .hunks(&snapshot, cx)
2488                            .map(|hunk| HunkStatus {
2489                                diff_status: hunk.status().kind,
2490                                range: hunk.range,
2491                                old_text: diff
2492                                    .read(cx)
2493                                    .base_text()
2494                                    .text_for_range(hunk.diff_base_byte_range)
2495                                    .collect(),
2496                            })
2497                            .collect(),
2498                    )
2499                })
2500                .collect()
2501        })
2502    }
2503
2504    #[gpui::test]
2505    async fn test_format_patch(cx: &mut TestAppContext) {
2506        init_test(cx);
2507
2508        let fs = FakeFs::new(cx.executor());
2509        fs.insert_tree(
2510            path!("/dir"),
2511            json!({"test.txt": "line 1\nline 2\nline 3\n"}),
2512        )
2513        .await;
2514        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2515        let action_log = cx.new(|_| ActionLog::new(project.clone()));
2516
2517        let file_path = project
2518            .read_with(cx, |project, cx| {
2519                project.find_project_path("dir/test.txt", cx)
2520            })
2521            .unwrap();
2522        let buffer = project
2523            .update(cx, |project, cx| project.open_buffer(file_path, cx))
2524            .await
2525            .unwrap();
2526
2527        cx.update(|cx| {
2528            // Track the buffer and mark it as read first
2529            action_log.update(cx, |log, cx| {
2530                log.buffer_read(buffer.clone(), cx);
2531            });
2532
2533            // Make some edits to create a patch
2534            buffer.update(cx, |buffer, cx| {
2535                buffer
2536                    .edit([(Point::new(1, 0)..Point::new(1, 6), "CHANGED")], None, cx)
2537                    .unwrap(); // Replace "line2" with "CHANGED"
2538            });
2539        });
2540
2541        cx.run_until_parked();
2542
2543        // Get the patch
2544        let patch = action_log.update(cx, |log, cx| log.unnotified_user_edits(cx));
2545
2546        // Verify the patch format contains expected unified diff elements
2547        assert_eq!(
2548            patch.unwrap(),
2549            indoc! {"
2550            --- a/dir/test.txt
2551            +++ b/dir/test.txt
2552            @@ -1,3 +1,3 @@
2553             line 1
2554            -line 2
2555            +CHANGED
2556             line 3
2557            "}
2558        );
2559    }
2560}