action_log.rs

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