action_log.rs

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