action_log.rs

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