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};
   7use std::{cmp, ops::Range, sync::Arc};
   8use text::{Edit, Patch, Rope};
   9use util::RangeExt;
  10
  11/// Tracks actions performed by tools in a thread
  12pub struct ActionLog {
  13    /// Buffers that we want to notify the model about when they change.
  14    tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
  15    /// Has the model edited a file since it last checked diagnostics?
  16    edited_since_project_diagnostics_check: bool,
  17}
  18
  19impl ActionLog {
  20    /// Creates a new, empty action log.
  21    pub fn new() -> Self {
  22        Self {
  23            tracked_buffers: BTreeMap::default(),
  24            edited_since_project_diagnostics_check: false,
  25        }
  26    }
  27
  28    /// Notifies a diagnostics check
  29    pub fn checked_project_diagnostics(&mut self) {
  30        self.edited_since_project_diagnostics_check = false;
  31    }
  32
  33    /// Returns true if any files have been edited since the last project diagnostics check
  34    pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
  35        self.edited_since_project_diagnostics_check
  36    }
  37
  38    fn track_buffer(
  39        &mut self,
  40        buffer: Entity<Buffer>,
  41        created: bool,
  42        cx: &mut Context<Self>,
  43    ) -> &mut TrackedBuffer {
  44        let tracked_buffer = self
  45            .tracked_buffers
  46            .entry(buffer.clone())
  47            .or_insert_with(|| {
  48                let text_snapshot = buffer.read(cx).text_snapshot();
  49                let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
  50                let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
  51                let base_text;
  52                let status;
  53                let unreviewed_changes;
  54                if created {
  55                    base_text = Rope::default();
  56                    status = TrackedBufferStatus::Created;
  57                    unreviewed_changes = Patch::new(vec![Edit {
  58                        old: 0..1,
  59                        new: 0..text_snapshot.max_point().row + 1,
  60                    }])
  61                } else {
  62                    base_text = buffer.read(cx).as_rope().clone();
  63                    status = TrackedBufferStatus::Modified;
  64                    unreviewed_changes = Patch::default();
  65                }
  66                TrackedBuffer {
  67                    buffer: buffer.clone(),
  68                    base_text,
  69                    unreviewed_changes,
  70                    snapshot: text_snapshot.clone(),
  71                    status,
  72                    version: buffer.read(cx).version(),
  73                    diff,
  74                    diff_update: diff_update_tx,
  75                    _maintain_diff: cx.spawn({
  76                        let buffer = buffer.clone();
  77                        async move |this, cx| {
  78                            Self::maintain_diff(this, buffer, diff_update_rx, cx)
  79                                .await
  80                                .ok();
  81                        }
  82                    }),
  83                    _subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
  84                }
  85            });
  86        tracked_buffer.version = buffer.read(cx).version();
  87        tracked_buffer
  88    }
  89
  90    fn handle_buffer_event(
  91        &mut self,
  92        buffer: Entity<Buffer>,
  93        event: &BufferEvent,
  94        cx: &mut Context<Self>,
  95    ) {
  96        match event {
  97            BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx),
  98            BufferEvent::FileHandleChanged => {
  99                self.handle_buffer_file_changed(buffer, cx);
 100            }
 101            _ => {}
 102        };
 103    }
 104
 105    fn handle_buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 106        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 107            return;
 108        };
 109        tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
 110    }
 111
 112    fn handle_buffer_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 113        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 114            return;
 115        };
 116
 117        match tracked_buffer.status {
 118            TrackedBufferStatus::Created | TrackedBufferStatus::Modified => {
 119                if buffer
 120                    .read(cx)
 121                    .file()
 122                    .map_or(false, |file| file.disk_state() == DiskState::Deleted)
 123                {
 124                    // If the buffer had been edited by a tool, but it got
 125                    // deleted externally, we want to stop tracking it.
 126                    self.tracked_buffers.remove(&buffer);
 127                }
 128                cx.notify();
 129            }
 130            TrackedBufferStatus::Deleted => {
 131                if buffer
 132                    .read(cx)
 133                    .file()
 134                    .map_or(false, |file| file.disk_state() != DiskState::Deleted)
 135                {
 136                    // If the buffer had been deleted by a tool, but it got
 137                    // resurrected externally, we want to clear the changes we
 138                    // were tracking and reset the buffer's state.
 139                    self.tracked_buffers.remove(&buffer);
 140                    self.track_buffer(buffer, false, cx);
 141                }
 142                cx.notify();
 143            }
 144        }
 145    }
 146
 147    async fn maintain_diff(
 148        this: WeakEntity<Self>,
 149        buffer: Entity<Buffer>,
 150        mut diff_update: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>,
 151        cx: &mut AsyncApp,
 152    ) -> Result<()> {
 153        while let Some((author, buffer_snapshot)) = diff_update.next().await {
 154            let (rebase, diff, language, language_registry) =
 155                this.read_with(cx, |this, cx| {
 156                    let tracked_buffer = this
 157                        .tracked_buffers
 158                        .get(&buffer)
 159                        .context("buffer not tracked")?;
 160
 161                    let rebase = cx.background_spawn({
 162                        let mut base_text = tracked_buffer.base_text.clone();
 163                        let old_snapshot = tracked_buffer.snapshot.clone();
 164                        let new_snapshot = buffer_snapshot.clone();
 165                        let unreviewed_changes = tracked_buffer.unreviewed_changes.clone();
 166                        async move {
 167                            let edits = diff_snapshots(&old_snapshot, &new_snapshot);
 168                            if let ChangeAuthor::User = author {
 169                                apply_non_conflicting_edits(
 170                                    &unreviewed_changes,
 171                                    edits,
 172                                    &mut base_text,
 173                                    new_snapshot.as_rope(),
 174                                );
 175                            }
 176                            (Arc::new(base_text.to_string()), base_text)
 177                        }
 178                    });
 179
 180                    anyhow::Ok((
 181                        rebase,
 182                        tracked_buffer.diff.clone(),
 183                        tracked_buffer.buffer.read(cx).language().cloned(),
 184                        tracked_buffer.buffer.read(cx).language_registry(),
 185                    ))
 186                })??;
 187
 188            let (new_base_text, new_base_text_rope) = rebase.await;
 189            let diff_snapshot = BufferDiff::update_diff(
 190                diff.clone(),
 191                buffer_snapshot.clone(),
 192                Some(new_base_text),
 193                true,
 194                false,
 195                language,
 196                language_registry,
 197                cx,
 198            )
 199            .await;
 200
 201            let mut unreviewed_changes = Patch::default();
 202            if let Ok(diff_snapshot) = diff_snapshot {
 203                unreviewed_changes = cx
 204                    .background_spawn({
 205                        let diff_snapshot = diff_snapshot.clone();
 206                        let buffer_snapshot = buffer_snapshot.clone();
 207                        let new_base_text_rope = new_base_text_rope.clone();
 208                        async move {
 209                            let mut unreviewed_changes = Patch::default();
 210                            for hunk in diff_snapshot.hunks_intersecting_range(
 211                                Anchor::MIN..Anchor::MAX,
 212                                &buffer_snapshot,
 213                            ) {
 214                                let old_range = new_base_text_rope
 215                                    .offset_to_point(hunk.diff_base_byte_range.start)
 216                                    ..new_base_text_rope
 217                                        .offset_to_point(hunk.diff_base_byte_range.end);
 218                                let new_range = hunk.range.start..hunk.range.end;
 219                                unreviewed_changes.push(point_to_row_edit(
 220                                    Edit {
 221                                        old: old_range,
 222                                        new: new_range,
 223                                    },
 224                                    &new_base_text_rope,
 225                                    &buffer_snapshot.as_rope(),
 226                                ));
 227                            }
 228                            unreviewed_changes
 229                        }
 230                    })
 231                    .await;
 232
 233                diff.update(cx, |diff, cx| {
 234                    diff.set_snapshot(diff_snapshot, &buffer_snapshot, None, cx)
 235                })?;
 236            }
 237            this.update(cx, |this, cx| {
 238                let tracked_buffer = this
 239                    .tracked_buffers
 240                    .get_mut(&buffer)
 241                    .context("buffer not tracked")?;
 242                tracked_buffer.base_text = new_base_text_rope;
 243                tracked_buffer.snapshot = buffer_snapshot;
 244                tracked_buffer.unreviewed_changes = unreviewed_changes;
 245                cx.notify();
 246                anyhow::Ok(())
 247            })??;
 248        }
 249
 250        Ok(())
 251    }
 252
 253    /// Track a buffer as read, so we can notify the model about user edits.
 254    pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 255        self.track_buffer(buffer, false, cx);
 256    }
 257
 258    /// Track a buffer that was added as context, so we can notify the model about user edits.
 259    pub fn buffer_added_as_context(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 260        self.track_buffer(buffer, false, cx);
 261    }
 262
 263    /// Track a buffer as read, so we can notify the model about user edits.
 264    pub fn will_create_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 265        self.track_buffer(buffer.clone(), true, cx);
 266        self.buffer_edited(buffer, cx)
 267    }
 268
 269    /// Mark a buffer as edited, so we can refresh it in the context
 270    pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 271        self.edited_since_project_diagnostics_check = true;
 272
 273        let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
 274        if let TrackedBufferStatus::Deleted = tracked_buffer.status {
 275            tracked_buffer.status = TrackedBufferStatus::Modified;
 276        }
 277        tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
 278    }
 279
 280    pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 281        let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
 282        match tracked_buffer.status {
 283            TrackedBufferStatus::Created => {
 284                self.tracked_buffers.remove(&buffer);
 285                cx.notify();
 286            }
 287            TrackedBufferStatus::Modified => {
 288                buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
 289                tracked_buffer.status = TrackedBufferStatus::Deleted;
 290                tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
 291            }
 292            TrackedBufferStatus::Deleted => {}
 293        }
 294        cx.notify();
 295    }
 296
 297    pub fn keep_edits_in_range(
 298        &mut self,
 299        buffer: Entity<Buffer>,
 300        buffer_range: Range<impl language::ToPoint>,
 301        cx: &mut Context<Self>,
 302    ) {
 303        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 304            return;
 305        };
 306
 307        match tracked_buffer.status {
 308            TrackedBufferStatus::Deleted => {
 309                self.tracked_buffers.remove(&buffer);
 310                cx.notify();
 311            }
 312            _ => {
 313                let buffer = buffer.read(cx);
 314                let buffer_range =
 315                    buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
 316                let mut delta = 0i32;
 317
 318                tracked_buffer.unreviewed_changes.retain_mut(|edit| {
 319                    edit.old.start = (edit.old.start as i32 + delta) as u32;
 320                    edit.old.end = (edit.old.end as i32 + delta) as u32;
 321
 322                    if buffer_range.end.row < edit.new.start
 323                        || buffer_range.start.row > edit.new.end
 324                    {
 325                        true
 326                    } else {
 327                        let old_bytes = tracked_buffer
 328                            .base_text
 329                            .point_to_offset(Point::new(edit.old.start, 0))
 330                            ..tracked_buffer.base_text.point_to_offset(cmp::min(
 331                                Point::new(edit.old.end, 0),
 332                                tracked_buffer.base_text.max_point(),
 333                            ));
 334                        let new_bytes = tracked_buffer
 335                            .snapshot
 336                            .point_to_offset(Point::new(edit.new.start, 0))
 337                            ..tracked_buffer.snapshot.point_to_offset(cmp::min(
 338                                Point::new(edit.new.end, 0),
 339                                tracked_buffer.snapshot.max_point(),
 340                            ));
 341                        tracked_buffer.base_text.replace(
 342                            old_bytes,
 343                            &tracked_buffer
 344                                .snapshot
 345                                .text_for_range(new_bytes)
 346                                .collect::<String>(),
 347                        );
 348                        delta += edit.new_len() as i32 - edit.old_len() as i32;
 349                        false
 350                    }
 351                });
 352                tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
 353            }
 354        }
 355    }
 356
 357    pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) {
 358        self.tracked_buffers
 359            .retain(|_buffer, tracked_buffer| match tracked_buffer.status {
 360                TrackedBufferStatus::Deleted => false,
 361                _ => {
 362                    tracked_buffer.unreviewed_changes.clear();
 363                    tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone();
 364                    tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
 365                    true
 366                }
 367            });
 368        cx.notify();
 369    }
 370
 371    /// Returns the set of buffers that contain changes that haven't been reviewed by the user.
 372    pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
 373        self.tracked_buffers
 374            .iter()
 375            .filter(|(_, tracked)| tracked.has_changes(cx))
 376            .map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone()))
 377            .collect()
 378    }
 379
 380    /// Iterate over buffers changed since last read or edited by the model
 381    pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
 382        self.tracked_buffers
 383            .iter()
 384            .filter(|(buffer, tracked)| {
 385                let buffer = buffer.read(cx);
 386
 387                tracked.version != buffer.version
 388                    && buffer
 389                        .file()
 390                        .map_or(false, |file| file.disk_state() != DiskState::Deleted)
 391            })
 392            .map(|(buffer, _)| buffer)
 393    }
 394}
 395
 396fn apply_non_conflicting_edits(
 397    patch: &Patch<u32>,
 398    edits: Vec<Edit<u32>>,
 399    old_text: &mut Rope,
 400    new_text: &Rope,
 401) {
 402    let mut old_edits = patch.edits().iter().cloned().peekable();
 403    let mut new_edits = edits.into_iter().peekable();
 404    let mut applied_delta = 0i32;
 405    let mut rebased_delta = 0i32;
 406
 407    while let Some(mut new_edit) = new_edits.next() {
 408        let mut conflict = false;
 409
 410        // Push all the old edits that are before this new edit or that intersect with it.
 411        while let Some(old_edit) = old_edits.peek() {
 412            if new_edit.old.end < old_edit.new.start
 413                || (!old_edit.new.is_empty() && new_edit.old.end == old_edit.new.start)
 414            {
 415                break;
 416            } else if new_edit.old.start > old_edit.new.end
 417                || (!old_edit.new.is_empty() && new_edit.old.start == old_edit.new.end)
 418            {
 419                let old_edit = old_edits.next().unwrap();
 420                rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
 421            } else {
 422                conflict = true;
 423                if new_edits
 424                    .peek()
 425                    .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new))
 426                {
 427                    new_edit = new_edits.next().unwrap();
 428                } else {
 429                    let old_edit = old_edits.next().unwrap();
 430                    rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
 431                }
 432            }
 433        }
 434
 435        if !conflict {
 436            // This edit doesn't intersect with any old edit, so we can apply it to the old text.
 437            new_edit.old.start = (new_edit.old.start as i32 + applied_delta - rebased_delta) as u32;
 438            new_edit.old.end = (new_edit.old.end as i32 + applied_delta - rebased_delta) as u32;
 439            let old_bytes = old_text.point_to_offset(Point::new(new_edit.old.start, 0))
 440                ..old_text.point_to_offset(cmp::min(
 441                    Point::new(new_edit.old.end, 0),
 442                    old_text.max_point(),
 443                ));
 444            let new_bytes = new_text.point_to_offset(Point::new(new_edit.new.start, 0))
 445                ..new_text.point_to_offset(cmp::min(
 446                    Point::new(new_edit.new.end, 0),
 447                    new_text.max_point(),
 448                ));
 449
 450            old_text.replace(
 451                old_bytes,
 452                &new_text.chunks_in_range(new_bytes).collect::<String>(),
 453            );
 454            applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32;
 455        }
 456    }
 457}
 458
 459fn diff_snapshots(
 460    old_snapshot: &text::BufferSnapshot,
 461    new_snapshot: &text::BufferSnapshot,
 462) -> Vec<Edit<u32>> {
 463    let mut edits = new_snapshot
 464        .edits_since::<Point>(&old_snapshot.version)
 465        .map(|edit| point_to_row_edit(edit, old_snapshot.as_rope(), new_snapshot.as_rope()))
 466        .peekable();
 467    let mut row_edits = Vec::new();
 468    while let Some(mut edit) = edits.next() {
 469        while let Some(next_edit) = edits.peek() {
 470            if edit.old.end >= next_edit.old.start {
 471                edit.old.end = next_edit.old.end;
 472                edit.new.end = next_edit.new.end;
 473                edits.next();
 474            } else {
 475                break;
 476            }
 477        }
 478        row_edits.push(edit);
 479    }
 480    row_edits
 481}
 482
 483fn point_to_row_edit(edit: Edit<Point>, old_text: &Rope, new_text: &Rope) -> Edit<u32> {
 484    if edit.old.start.column == old_text.line_len(edit.old.start.row)
 485        && new_text
 486            .chars_at(new_text.point_to_offset(edit.new.start))
 487            .next()
 488            == Some('\n')
 489        && edit.old.start != old_text.max_point()
 490    {
 491        Edit {
 492            old: edit.old.start.row + 1..edit.old.end.row + 1,
 493            new: edit.new.start.row + 1..edit.new.end.row + 1,
 494        }
 495    } else if edit.old.start.column == 0
 496        && edit.old.end.column == 0
 497        && edit.new.end.column == 0
 498        && edit.old.end != old_text.max_point()
 499    {
 500        Edit {
 501            old: edit.old.start.row..edit.old.end.row,
 502            new: edit.new.start.row..edit.new.end.row,
 503        }
 504    } else {
 505        Edit {
 506            old: edit.old.start.row..edit.old.end.row + 1,
 507            new: edit.new.start.row..edit.new.end.row + 1,
 508        }
 509    }
 510}
 511
 512enum ChangeAuthor {
 513    User,
 514    Agent,
 515}
 516
 517#[derive(Copy, Clone, Eq, PartialEq)]
 518enum TrackedBufferStatus {
 519    Created,
 520    Modified,
 521    Deleted,
 522}
 523
 524struct TrackedBuffer {
 525    buffer: Entity<Buffer>,
 526    base_text: Rope,
 527    unreviewed_changes: Patch<u32>,
 528    status: TrackedBufferStatus,
 529    version: clock::Global,
 530    diff: Entity<BufferDiff>,
 531    snapshot: text::BufferSnapshot,
 532    diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
 533    _maintain_diff: Task<()>,
 534    _subscription: Subscription,
 535}
 536
 537impl TrackedBuffer {
 538    fn has_changes(&self, cx: &App) -> bool {
 539        self.diff
 540            .read(cx)
 541            .hunks(&self.buffer.read(cx), cx)
 542            .next()
 543            .is_some()
 544    }
 545
 546    fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) {
 547        self.diff_update
 548            .unbounded_send((author, self.buffer.read(cx).text_snapshot()))
 549            .ok();
 550    }
 551}
 552
 553pub struct ChangedBuffer {
 554    pub diff: Entity<BufferDiff>,
 555}
 556
 557#[cfg(test)]
 558mod tests {
 559    use std::env;
 560
 561    use super::*;
 562    use buffer_diff::DiffHunkStatusKind;
 563    use gpui::TestAppContext;
 564    use language::Point;
 565    use project::{FakeFs, Fs, Project, RemoveOptions};
 566    use rand::prelude::*;
 567    use serde_json::json;
 568    use settings::SettingsStore;
 569    use util::{RandomCharIter, path};
 570
 571    #[ctor::ctor]
 572    fn init_logger() {
 573        if std::env::var("RUST_LOG").is_ok() {
 574            env_logger::init();
 575        }
 576    }
 577
 578    #[gpui::test(iterations = 10)]
 579    async fn test_keep_edits(cx: &mut TestAppContext) {
 580        let action_log = cx.new(|_| ActionLog::new());
 581        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
 582
 583        cx.update(|cx| {
 584            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 585            buffer.update(cx, |buffer, cx| {
 586                buffer
 587                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
 588                    .unwrap()
 589            });
 590            buffer.update(cx, |buffer, cx| {
 591                buffer
 592                    .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
 593                    .unwrap()
 594            });
 595            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 596        });
 597        cx.run_until_parked();
 598        assert_eq!(
 599            buffer.read_with(cx, |buffer, _| buffer.text()),
 600            "abc\ndEf\nghi\njkl\nmnO"
 601        );
 602        assert_eq!(
 603            unreviewed_hunks(&action_log, cx),
 604            vec![(
 605                buffer.clone(),
 606                vec![
 607                    HunkStatus {
 608                        range: Point::new(1, 0)..Point::new(2, 0),
 609                        diff_status: DiffHunkStatusKind::Modified,
 610                        old_text: "def\n".into(),
 611                    },
 612                    HunkStatus {
 613                        range: Point::new(4, 0)..Point::new(4, 3),
 614                        diff_status: DiffHunkStatusKind::Modified,
 615                        old_text: "mno".into(),
 616                    }
 617                ],
 618            )]
 619        );
 620
 621        action_log.update(cx, |log, cx| {
 622            log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
 623        });
 624        cx.run_until_parked();
 625        assert_eq!(
 626            unreviewed_hunks(&action_log, cx),
 627            vec![(
 628                buffer.clone(),
 629                vec![HunkStatus {
 630                    range: Point::new(1, 0)..Point::new(2, 0),
 631                    diff_status: DiffHunkStatusKind::Modified,
 632                    old_text: "def\n".into(),
 633                }],
 634            )]
 635        );
 636
 637        action_log.update(cx, |log, cx| {
 638            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
 639        });
 640        cx.run_until_parked();
 641        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 642    }
 643
 644    #[gpui::test(iterations = 10)]
 645    async fn test_deletions(cx: &mut TestAppContext) {
 646        let action_log = cx.new(|_| ActionLog::new());
 647        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno\npqr", cx));
 648
 649        cx.update(|cx| {
 650            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 651            buffer.update(cx, |buffer, cx| {
 652                buffer
 653                    .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx)
 654                    .unwrap();
 655                buffer.finalize_last_transaction();
 656            });
 657            buffer.update(cx, |buffer, cx| {
 658                buffer
 659                    .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx)
 660                    .unwrap();
 661                buffer.finalize_last_transaction();
 662            });
 663            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 664        });
 665        cx.run_until_parked();
 666        assert_eq!(
 667            buffer.read_with(cx, |buffer, _| buffer.text()),
 668            "abc\nghi\njkl\npqr"
 669        );
 670        assert_eq!(
 671            unreviewed_hunks(&action_log, cx),
 672            vec![(
 673                buffer.clone(),
 674                vec![
 675                    HunkStatus {
 676                        range: Point::new(1, 0)..Point::new(1, 0),
 677                        diff_status: DiffHunkStatusKind::Deleted,
 678                        old_text: "def\n".into(),
 679                    },
 680                    HunkStatus {
 681                        range: Point::new(3, 0)..Point::new(3, 0),
 682                        diff_status: DiffHunkStatusKind::Deleted,
 683                        old_text: "mno\n".into(),
 684                    }
 685                ],
 686            )]
 687        );
 688
 689        buffer.update(cx, |buffer, cx| buffer.undo(cx));
 690        cx.run_until_parked();
 691        assert_eq!(
 692            buffer.read_with(cx, |buffer, _| buffer.text()),
 693            "abc\nghi\njkl\nmno\npqr"
 694        );
 695        assert_eq!(
 696            unreviewed_hunks(&action_log, cx),
 697            vec![(
 698                buffer.clone(),
 699                vec![HunkStatus {
 700                    range: Point::new(1, 0)..Point::new(1, 0),
 701                    diff_status: DiffHunkStatusKind::Deleted,
 702                    old_text: "def\n".into(),
 703                }],
 704            )]
 705        );
 706
 707        action_log.update(cx, |log, cx| {
 708            log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx)
 709        });
 710        cx.run_until_parked();
 711        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 712    }
 713
 714    #[gpui::test(iterations = 10)]
 715    async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
 716        let action_log = cx.new(|_| ActionLog::new());
 717        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
 718
 719        cx.update(|cx| {
 720            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 721            buffer.update(cx, |buffer, cx| {
 722                buffer
 723                    .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
 724                    .unwrap()
 725            });
 726            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 727        });
 728        cx.run_until_parked();
 729        assert_eq!(
 730            buffer.read_with(cx, |buffer, _| buffer.text()),
 731            "abc\ndeF\nGHI\njkl\nmno"
 732        );
 733        assert_eq!(
 734            unreviewed_hunks(&action_log, cx),
 735            vec![(
 736                buffer.clone(),
 737                vec![HunkStatus {
 738                    range: Point::new(1, 0)..Point::new(3, 0),
 739                    diff_status: DiffHunkStatusKind::Modified,
 740                    old_text: "def\nghi\n".into(),
 741                }],
 742            )]
 743        );
 744
 745        buffer.update(cx, |buffer, cx| {
 746            buffer.edit(
 747                [
 748                    (Point::new(0, 2)..Point::new(0, 2), "X"),
 749                    (Point::new(3, 0)..Point::new(3, 0), "Y"),
 750                ],
 751                None,
 752                cx,
 753            )
 754        });
 755        cx.run_until_parked();
 756        assert_eq!(
 757            buffer.read_with(cx, |buffer, _| buffer.text()),
 758            "abXc\ndeF\nGHI\nYjkl\nmno"
 759        );
 760        assert_eq!(
 761            unreviewed_hunks(&action_log, cx),
 762            vec![(
 763                buffer.clone(),
 764                vec![HunkStatus {
 765                    range: Point::new(1, 0)..Point::new(3, 0),
 766                    diff_status: DiffHunkStatusKind::Modified,
 767                    old_text: "def\nghi\n".into(),
 768                }],
 769            )]
 770        );
 771
 772        buffer.update(cx, |buffer, cx| {
 773            buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx)
 774        });
 775        cx.run_until_parked();
 776        assert_eq!(
 777            buffer.read_with(cx, |buffer, _| buffer.text()),
 778            "abXc\ndZeF\nGHI\nYjkl\nmno"
 779        );
 780        assert_eq!(
 781            unreviewed_hunks(&action_log, cx),
 782            vec![(
 783                buffer.clone(),
 784                vec![HunkStatus {
 785                    range: Point::new(1, 0)..Point::new(3, 0),
 786                    diff_status: DiffHunkStatusKind::Modified,
 787                    old_text: "def\nghi\n".into(),
 788                }],
 789            )]
 790        );
 791
 792        action_log.update(cx, |log, cx| {
 793            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
 794        });
 795        cx.run_until_parked();
 796        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 797    }
 798
 799    #[gpui::test(iterations = 10)]
 800    async fn test_creation(cx: &mut TestAppContext) {
 801        cx.update(|cx| {
 802            let settings_store = SettingsStore::test(cx);
 803            cx.set_global(settings_store);
 804            language::init(cx);
 805            Project::init_settings(cx);
 806        });
 807
 808        let action_log = cx.new(|_| ActionLog::new());
 809
 810        let fs = FakeFs::new(cx.executor());
 811        fs.insert_tree(path!("/dir"), json!({})).await;
 812
 813        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 814        let file_path = project
 815            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
 816            .unwrap();
 817
 818        // Simulate file2 being recreated by a tool.
 819        let buffer = project
 820            .update(cx, |project, cx| project.open_buffer(file_path, cx))
 821            .await
 822            .unwrap();
 823        cx.update(|cx| {
 824            buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
 825            action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
 826        });
 827        project
 828            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 829            .await
 830            .unwrap();
 831        cx.run_until_parked();
 832        assert_eq!(
 833            unreviewed_hunks(&action_log, cx),
 834            vec![(
 835                buffer.clone(),
 836                vec![HunkStatus {
 837                    range: Point::new(0, 0)..Point::new(0, 5),
 838                    diff_status: DiffHunkStatusKind::Added,
 839                    old_text: "".into(),
 840                }],
 841            )]
 842        );
 843
 844        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx));
 845        cx.run_until_parked();
 846        assert_eq!(
 847            unreviewed_hunks(&action_log, cx),
 848            vec![(
 849                buffer.clone(),
 850                vec![HunkStatus {
 851                    range: Point::new(0, 0)..Point::new(0, 6),
 852                    diff_status: DiffHunkStatusKind::Added,
 853                    old_text: "".into(),
 854                }],
 855            )]
 856        );
 857
 858        action_log.update(cx, |log, cx| {
 859            log.keep_edits_in_range(buffer.clone(), 0..5, cx)
 860        });
 861        cx.run_until_parked();
 862        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 863    }
 864
 865    #[gpui::test(iterations = 10)]
 866    async fn test_deleting_files(cx: &mut TestAppContext) {
 867        cx.update(|cx| {
 868            let settings_store = SettingsStore::test(cx);
 869            cx.set_global(settings_store);
 870            language::init(cx);
 871            Project::init_settings(cx);
 872        });
 873
 874        let fs = FakeFs::new(cx.executor());
 875        fs.insert_tree(
 876            path!("/dir"),
 877            json!({"file1": "lorem\n", "file2": "ipsum\n"}),
 878        )
 879        .await;
 880
 881        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 882        let file1_path = project
 883            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
 884            .unwrap();
 885        let file2_path = project
 886            .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
 887            .unwrap();
 888
 889        let action_log = cx.new(|_| ActionLog::new());
 890        let buffer1 = project
 891            .update(cx, |project, cx| {
 892                project.open_buffer(file1_path.clone(), cx)
 893            })
 894            .await
 895            .unwrap();
 896        let buffer2 = project
 897            .update(cx, |project, cx| {
 898                project.open_buffer(file2_path.clone(), cx)
 899            })
 900            .await
 901            .unwrap();
 902
 903        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
 904        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
 905        project
 906            .update(cx, |project, cx| {
 907                project.delete_file(file1_path.clone(), false, cx)
 908            })
 909            .unwrap()
 910            .await
 911            .unwrap();
 912        project
 913            .update(cx, |project, cx| {
 914                project.delete_file(file2_path.clone(), false, cx)
 915            })
 916            .unwrap()
 917            .await
 918            .unwrap();
 919        cx.run_until_parked();
 920        assert_eq!(
 921            unreviewed_hunks(&action_log, cx),
 922            vec![
 923                (
 924                    buffer1.clone(),
 925                    vec![HunkStatus {
 926                        range: Point::new(0, 0)..Point::new(0, 0),
 927                        diff_status: DiffHunkStatusKind::Deleted,
 928                        old_text: "lorem\n".into(),
 929                    }]
 930                ),
 931                (
 932                    buffer2.clone(),
 933                    vec![HunkStatus {
 934                        range: Point::new(0, 0)..Point::new(0, 0),
 935                        diff_status: DiffHunkStatusKind::Deleted,
 936                        old_text: "ipsum\n".into(),
 937                    }],
 938                )
 939            ]
 940        );
 941
 942        // Simulate file1 being recreated externally.
 943        fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
 944            .await;
 945
 946        // Simulate file2 being recreated by a tool.
 947        let buffer2 = project
 948            .update(cx, |project, cx| project.open_buffer(file2_path, cx))
 949            .await
 950            .unwrap();
 951        buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
 952        action_log.update(cx, |log, cx| log.will_create_buffer(buffer2.clone(), cx));
 953        project
 954            .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
 955            .await
 956            .unwrap();
 957
 958        cx.run_until_parked();
 959        assert_eq!(
 960            unreviewed_hunks(&action_log, cx),
 961            vec![(
 962                buffer2.clone(),
 963                vec![HunkStatus {
 964                    range: Point::new(0, 0)..Point::new(0, 5),
 965                    diff_status: DiffHunkStatusKind::Modified,
 966                    old_text: "ipsum\n".into(),
 967                }],
 968            )]
 969        );
 970
 971        // Simulate file2 being deleted externally.
 972        fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
 973            .await
 974            .unwrap();
 975        cx.run_until_parked();
 976        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 977    }
 978
 979    #[gpui::test(iterations = 100)]
 980    async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
 981        let operations = env::var("OPERATIONS")
 982            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
 983            .unwrap_or(20);
 984
 985        let action_log = cx.new(|_| ActionLog::new());
 986        let text = RandomCharIter::new(&mut rng).take(50).collect::<String>();
 987        let buffer = cx.new(|cx| Buffer::local(text, cx));
 988        action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 989
 990        for _ in 0..operations {
 991            match rng.gen_range(0..100) {
 992                0..25 => {
 993                    action_log.update(cx, |log, cx| {
 994                        let range = buffer.read(cx).random_byte_range(0, &mut rng);
 995                        log::info!("keeping all edits in range {:?}", range);
 996                        log.keep_edits_in_range(buffer.clone(), range, cx)
 997                    });
 998                }
 999                _ => {
1000                    let is_agent_change = rng.gen_bool(0.5);
1001                    if is_agent_change {
1002                        log::info!("agent edit");
1003                    } else {
1004                        log::info!("user edit");
1005                    }
1006                    cx.update(|cx| {
1007                        buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
1008                        if is_agent_change {
1009                            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1010                        }
1011                    });
1012                }
1013            }
1014
1015            if rng.gen_bool(0.2) {
1016                quiesce(&action_log, &buffer, cx);
1017            }
1018        }
1019
1020        quiesce(&action_log, &buffer, cx);
1021
1022        fn quiesce(
1023            action_log: &Entity<ActionLog>,
1024            buffer: &Entity<Buffer>,
1025            cx: &mut TestAppContext,
1026        ) {
1027            log::info!("quiescing...");
1028            cx.run_until_parked();
1029            action_log.update(cx, |log, cx| {
1030                let tracked_buffer = log.track_buffer(buffer.clone(), false, cx);
1031                let mut old_text = tracked_buffer.base_text.clone();
1032                let new_text = buffer.read(cx).as_rope();
1033                for edit in tracked_buffer.unreviewed_changes.edits() {
1034                    let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
1035                    let old_end = old_text.point_to_offset(cmp::min(
1036                        Point::new(edit.new.start + edit.old_len(), 0),
1037                        old_text.max_point(),
1038                    ));
1039                    old_text.replace(
1040                        old_start..old_end,
1041                        &new_text.slice_rows(edit.new.clone()).to_string(),
1042                    );
1043                }
1044                pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
1045            })
1046        }
1047    }
1048
1049    #[derive(Debug, Clone, PartialEq, Eq)]
1050    struct HunkStatus {
1051        range: Range<Point>,
1052        diff_status: DiffHunkStatusKind,
1053        old_text: String,
1054    }
1055
1056    fn unreviewed_hunks(
1057        action_log: &Entity<ActionLog>,
1058        cx: &TestAppContext,
1059    ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
1060        cx.read(|cx| {
1061            action_log
1062                .read(cx)
1063                .changed_buffers(cx)
1064                .into_iter()
1065                .map(|(buffer, diff)| {
1066                    let snapshot = buffer.read(cx).snapshot();
1067                    (
1068                        buffer,
1069                        diff.read(cx)
1070                            .hunks(&snapshot, cx)
1071                            .map(|hunk| HunkStatus {
1072                                diff_status: hunk.status().kind,
1073                                range: hunk.range,
1074                                old_text: diff
1075                                    .read(cx)
1076                                    .base_text()
1077                                    .text_for_range(hunk.diff_base_byte_range)
1078                                    .collect(),
1079                            })
1080                            .collect(),
1081                    )
1082                })
1083                .collect()
1084        })
1085    }
1086}