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