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 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    /// Takes and returns the set of buffers pending refresh, clearing internal state.
 396    pub fn take_stale_buffers_in_context(&mut self) -> HashSet<Entity<Buffer>> {
 397        std::mem::take(&mut self.stale_buffers_in_context)
 398    }
 399}
 400
 401fn apply_non_conflicting_edits(
 402    patch: &Patch<u32>,
 403    edits: Vec<Edit<u32>>,
 404    old_text: &mut Rope,
 405    new_text: &Rope,
 406) {
 407    let mut old_edits = patch.edits().iter().cloned().peekable();
 408    let mut new_edits = edits.into_iter().peekable();
 409    let mut applied_delta = 0i32;
 410    let mut rebased_delta = 0i32;
 411
 412    while let Some(mut new_edit) = new_edits.next() {
 413        let mut conflict = false;
 414
 415        // Push all the old edits that are before this new edit or that intersect with it.
 416        while let Some(old_edit) = old_edits.peek() {
 417            if new_edit.old.end < old_edit.new.start
 418                || (!old_edit.new.is_empty() && new_edit.old.end == old_edit.new.start)
 419            {
 420                break;
 421            } else if new_edit.old.start > old_edit.new.end
 422                || (!old_edit.new.is_empty() && new_edit.old.start == old_edit.new.end)
 423            {
 424                let old_edit = old_edits.next().unwrap();
 425                rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
 426            } else {
 427                conflict = true;
 428                if new_edits
 429                    .peek()
 430                    .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new))
 431                {
 432                    new_edit = new_edits.next().unwrap();
 433                } else {
 434                    let old_edit = old_edits.next().unwrap();
 435                    rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
 436                }
 437            }
 438        }
 439
 440        if !conflict {
 441            // This edit doesn't intersect with any old edit, so we can apply it to the old text.
 442            new_edit.old.start = (new_edit.old.start as i32 + applied_delta - rebased_delta) as u32;
 443            new_edit.old.end = (new_edit.old.end as i32 + applied_delta - rebased_delta) as u32;
 444            let old_bytes = old_text.point_to_offset(Point::new(new_edit.old.start, 0))
 445                ..old_text.point_to_offset(cmp::min(
 446                    Point::new(new_edit.old.end, 0),
 447                    old_text.max_point(),
 448                ));
 449            let new_bytes = new_text.point_to_offset(Point::new(new_edit.new.start, 0))
 450                ..new_text.point_to_offset(cmp::min(
 451                    Point::new(new_edit.new.end, 0),
 452                    new_text.max_point(),
 453                ));
 454
 455            old_text.replace(
 456                old_bytes,
 457                &new_text.chunks_in_range(new_bytes).collect::<String>(),
 458            );
 459            applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32;
 460        }
 461    }
 462}
 463
 464fn diff_snapshots(
 465    old_snapshot: &text::BufferSnapshot,
 466    new_snapshot: &text::BufferSnapshot,
 467) -> Vec<Edit<u32>> {
 468    let mut edits = new_snapshot
 469        .edits_since::<Point>(&old_snapshot.version)
 470        .map(|edit| point_to_row_edit(edit, old_snapshot.as_rope(), new_snapshot.as_rope()))
 471        .peekable();
 472    let mut row_edits = Vec::new();
 473    while let Some(mut edit) = edits.next() {
 474        while let Some(next_edit) = edits.peek() {
 475            if edit.old.end >= next_edit.old.start {
 476                edit.old.end = next_edit.old.end;
 477                edit.new.end = next_edit.new.end;
 478                edits.next();
 479            } else {
 480                break;
 481            }
 482        }
 483        row_edits.push(edit);
 484    }
 485    row_edits
 486}
 487
 488fn point_to_row_edit(edit: Edit<Point>, old_text: &Rope, new_text: &Rope) -> Edit<u32> {
 489    if edit.old.start.column == old_text.line_len(edit.old.start.row)
 490        && new_text
 491            .chars_at(new_text.point_to_offset(edit.new.start))
 492            .next()
 493            == Some('\n')
 494        && edit.old.start != old_text.max_point()
 495    {
 496        Edit {
 497            old: edit.old.start.row + 1..edit.old.end.row + 1,
 498            new: edit.new.start.row + 1..edit.new.end.row + 1,
 499        }
 500    } else if edit.old.start.column == 0
 501        && edit.old.end.column == 0
 502        && edit.new.end.column == 0
 503        && edit.old.end != old_text.max_point()
 504    {
 505        Edit {
 506            old: edit.old.start.row..edit.old.end.row,
 507            new: edit.new.start.row..edit.new.end.row,
 508        }
 509    } else {
 510        Edit {
 511            old: edit.old.start.row..edit.old.end.row + 1,
 512            new: edit.new.start.row..edit.new.end.row + 1,
 513        }
 514    }
 515}
 516
 517enum ChangeAuthor {
 518    User,
 519    Agent,
 520}
 521
 522#[derive(Copy, Clone, Eq, PartialEq)]
 523enum TrackedBufferStatus {
 524    Created,
 525    Modified,
 526    Deleted,
 527}
 528
 529struct TrackedBuffer {
 530    buffer: Entity<Buffer>,
 531    base_text: Rope,
 532    unreviewed_changes: Patch<u32>,
 533    status: TrackedBufferStatus,
 534    version: clock::Global,
 535    diff: Entity<BufferDiff>,
 536    snapshot: text::BufferSnapshot,
 537    diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
 538    _maintain_diff: Task<()>,
 539    _subscription: Subscription,
 540}
 541
 542impl TrackedBuffer {
 543    fn has_changes(&self, cx: &App) -> bool {
 544        self.diff
 545            .read(cx)
 546            .hunks(&self.buffer.read(cx), cx)
 547            .next()
 548            .is_some()
 549    }
 550
 551    fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) {
 552        self.diff_update
 553            .unbounded_send((author, self.buffer.read(cx).text_snapshot()))
 554            .ok();
 555    }
 556}
 557
 558pub struct ChangedBuffer {
 559    pub diff: Entity<BufferDiff>,
 560}
 561
 562#[cfg(test)]
 563mod tests {
 564    use std::env;
 565
 566    use super::*;
 567    use buffer_diff::DiffHunkStatusKind;
 568    use gpui::TestAppContext;
 569    use language::Point;
 570    use project::{FakeFs, Fs, Project, RemoveOptions};
 571    use rand::prelude::*;
 572    use serde_json::json;
 573    use settings::SettingsStore;
 574    use util::{RandomCharIter, path};
 575
 576    #[ctor::ctor]
 577    fn init_logger() {
 578        if std::env::var("RUST_LOG").is_ok() {
 579            env_logger::init();
 580        }
 581    }
 582
 583    #[gpui::test(iterations = 10)]
 584    async fn test_keep_edits(cx: &mut TestAppContext) {
 585        let action_log = cx.new(|_| ActionLog::new());
 586        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
 587
 588        cx.update(|cx| {
 589            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 590            buffer.update(cx, |buffer, cx| {
 591                buffer
 592                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
 593                    .unwrap()
 594            });
 595            buffer.update(cx, |buffer, cx| {
 596                buffer
 597                    .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
 598                    .unwrap()
 599            });
 600            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 601        });
 602        cx.run_until_parked();
 603        assert_eq!(
 604            buffer.read_with(cx, |buffer, _| buffer.text()),
 605            "abc\ndEf\nghi\njkl\nmnO"
 606        );
 607        assert_eq!(
 608            unreviewed_hunks(&action_log, cx),
 609            vec![(
 610                buffer.clone(),
 611                vec![
 612                    HunkStatus {
 613                        range: Point::new(1, 0)..Point::new(2, 0),
 614                        diff_status: DiffHunkStatusKind::Modified,
 615                        old_text: "def\n".into(),
 616                    },
 617                    HunkStatus {
 618                        range: Point::new(4, 0)..Point::new(4, 3),
 619                        diff_status: DiffHunkStatusKind::Modified,
 620                        old_text: "mno".into(),
 621                    }
 622                ],
 623            )]
 624        );
 625
 626        action_log.update(cx, |log, cx| {
 627            log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
 628        });
 629        cx.run_until_parked();
 630        assert_eq!(
 631            unreviewed_hunks(&action_log, cx),
 632            vec![(
 633                buffer.clone(),
 634                vec![HunkStatus {
 635                    range: Point::new(1, 0)..Point::new(2, 0),
 636                    diff_status: DiffHunkStatusKind::Modified,
 637                    old_text: "def\n".into(),
 638                }],
 639            )]
 640        );
 641
 642        action_log.update(cx, |log, cx| {
 643            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
 644        });
 645        cx.run_until_parked();
 646        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 647    }
 648
 649    #[gpui::test(iterations = 10)]
 650    async fn test_deletions(cx: &mut TestAppContext) {
 651        let action_log = cx.new(|_| ActionLog::new());
 652        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno\npqr", cx));
 653
 654        cx.update(|cx| {
 655            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 656            buffer.update(cx, |buffer, cx| {
 657                buffer
 658                    .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx)
 659                    .unwrap();
 660                buffer.finalize_last_transaction();
 661            });
 662            buffer.update(cx, |buffer, cx| {
 663                buffer
 664                    .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx)
 665                    .unwrap();
 666                buffer.finalize_last_transaction();
 667            });
 668            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 669        });
 670        cx.run_until_parked();
 671        assert_eq!(
 672            buffer.read_with(cx, |buffer, _| buffer.text()),
 673            "abc\nghi\njkl\npqr"
 674        );
 675        assert_eq!(
 676            unreviewed_hunks(&action_log, cx),
 677            vec![(
 678                buffer.clone(),
 679                vec![
 680                    HunkStatus {
 681                        range: Point::new(1, 0)..Point::new(1, 0),
 682                        diff_status: DiffHunkStatusKind::Deleted,
 683                        old_text: "def\n".into(),
 684                    },
 685                    HunkStatus {
 686                        range: Point::new(3, 0)..Point::new(3, 0),
 687                        diff_status: DiffHunkStatusKind::Deleted,
 688                        old_text: "mno\n".into(),
 689                    }
 690                ],
 691            )]
 692        );
 693
 694        buffer.update(cx, |buffer, cx| buffer.undo(cx));
 695        cx.run_until_parked();
 696        assert_eq!(
 697            buffer.read_with(cx, |buffer, _| buffer.text()),
 698            "abc\nghi\njkl\nmno\npqr"
 699        );
 700        assert_eq!(
 701            unreviewed_hunks(&action_log, cx),
 702            vec![(
 703                buffer.clone(),
 704                vec![HunkStatus {
 705                    range: Point::new(1, 0)..Point::new(1, 0),
 706                    diff_status: DiffHunkStatusKind::Deleted,
 707                    old_text: "def\n".into(),
 708                }],
 709            )]
 710        );
 711
 712        action_log.update(cx, |log, cx| {
 713            log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx)
 714        });
 715        cx.run_until_parked();
 716        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 717    }
 718
 719    #[gpui::test(iterations = 10)]
 720    async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
 721        let action_log = cx.new(|_| ActionLog::new());
 722        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
 723
 724        cx.update(|cx| {
 725            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 726            buffer.update(cx, |buffer, cx| {
 727                buffer
 728                    .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
 729                    .unwrap()
 730            });
 731            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 732        });
 733        cx.run_until_parked();
 734        assert_eq!(
 735            buffer.read_with(cx, |buffer, _| buffer.text()),
 736            "abc\ndeF\nGHI\njkl\nmno"
 737        );
 738        assert_eq!(
 739            unreviewed_hunks(&action_log, cx),
 740            vec![(
 741                buffer.clone(),
 742                vec![HunkStatus {
 743                    range: Point::new(1, 0)..Point::new(3, 0),
 744                    diff_status: DiffHunkStatusKind::Modified,
 745                    old_text: "def\nghi\n".into(),
 746                }],
 747            )]
 748        );
 749
 750        buffer.update(cx, |buffer, cx| {
 751            buffer.edit(
 752                [
 753                    (Point::new(0, 2)..Point::new(0, 2), "X"),
 754                    (Point::new(3, 0)..Point::new(3, 0), "Y"),
 755                ],
 756                None,
 757                cx,
 758            )
 759        });
 760        cx.run_until_parked();
 761        assert_eq!(
 762            buffer.read_with(cx, |buffer, _| buffer.text()),
 763            "abXc\ndeF\nGHI\nYjkl\nmno"
 764        );
 765        assert_eq!(
 766            unreviewed_hunks(&action_log, cx),
 767            vec![(
 768                buffer.clone(),
 769                vec![HunkStatus {
 770                    range: Point::new(1, 0)..Point::new(3, 0),
 771                    diff_status: DiffHunkStatusKind::Modified,
 772                    old_text: "def\nghi\n".into(),
 773                }],
 774            )]
 775        );
 776
 777        buffer.update(cx, |buffer, cx| {
 778            buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx)
 779        });
 780        cx.run_until_parked();
 781        assert_eq!(
 782            buffer.read_with(cx, |buffer, _| buffer.text()),
 783            "abXc\ndZeF\nGHI\nYjkl\nmno"
 784        );
 785        assert_eq!(
 786            unreviewed_hunks(&action_log, cx),
 787            vec![(
 788                buffer.clone(),
 789                vec![HunkStatus {
 790                    range: Point::new(1, 0)..Point::new(3, 0),
 791                    diff_status: DiffHunkStatusKind::Modified,
 792                    old_text: "def\nghi\n".into(),
 793                }],
 794            )]
 795        );
 796
 797        action_log.update(cx, |log, cx| {
 798            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
 799        });
 800        cx.run_until_parked();
 801        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 802    }
 803
 804    #[gpui::test(iterations = 10)]
 805    async fn test_creation(cx: &mut TestAppContext) {
 806        cx.update(|cx| {
 807            let settings_store = SettingsStore::test(cx);
 808            cx.set_global(settings_store);
 809            language::init(cx);
 810            Project::init_settings(cx);
 811        });
 812
 813        let action_log = cx.new(|_| ActionLog::new());
 814
 815        let fs = FakeFs::new(cx.executor());
 816        fs.insert_tree(path!("/dir"), json!({})).await;
 817
 818        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 819        let file_path = project
 820            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
 821            .unwrap();
 822
 823        // Simulate file2 being recreated by a tool.
 824        let buffer = project
 825            .update(cx, |project, cx| project.open_buffer(file_path, cx))
 826            .await
 827            .unwrap();
 828        cx.update(|cx| {
 829            buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
 830            action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
 831        });
 832        project
 833            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 834            .await
 835            .unwrap();
 836        cx.run_until_parked();
 837        assert_eq!(
 838            unreviewed_hunks(&action_log, cx),
 839            vec![(
 840                buffer.clone(),
 841                vec![HunkStatus {
 842                    range: Point::new(0, 0)..Point::new(0, 5),
 843                    diff_status: DiffHunkStatusKind::Added,
 844                    old_text: "".into(),
 845                }],
 846            )]
 847        );
 848
 849        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx));
 850        cx.run_until_parked();
 851        assert_eq!(
 852            unreviewed_hunks(&action_log, cx),
 853            vec![(
 854                buffer.clone(),
 855                vec![HunkStatus {
 856                    range: Point::new(0, 0)..Point::new(0, 6),
 857                    diff_status: DiffHunkStatusKind::Added,
 858                    old_text: "".into(),
 859                }],
 860            )]
 861        );
 862
 863        action_log.update(cx, |log, cx| {
 864            log.keep_edits_in_range(buffer.clone(), 0..5, cx)
 865        });
 866        cx.run_until_parked();
 867        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 868    }
 869
 870    #[gpui::test(iterations = 10)]
 871    async fn test_deleting_files(cx: &mut TestAppContext) {
 872        cx.update(|cx| {
 873            let settings_store = SettingsStore::test(cx);
 874            cx.set_global(settings_store);
 875            language::init(cx);
 876            Project::init_settings(cx);
 877        });
 878
 879        let fs = FakeFs::new(cx.executor());
 880        fs.insert_tree(
 881            path!("/dir"),
 882            json!({"file1": "lorem\n", "file2": "ipsum\n"}),
 883        )
 884        .await;
 885
 886        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 887        let file1_path = project
 888            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
 889            .unwrap();
 890        let file2_path = project
 891            .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
 892            .unwrap();
 893
 894        let action_log = cx.new(|_| ActionLog::new());
 895        let buffer1 = project
 896            .update(cx, |project, cx| {
 897                project.open_buffer(file1_path.clone(), cx)
 898            })
 899            .await
 900            .unwrap();
 901        let buffer2 = project
 902            .update(cx, |project, cx| {
 903                project.open_buffer(file2_path.clone(), cx)
 904            })
 905            .await
 906            .unwrap();
 907
 908        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
 909        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
 910        project
 911            .update(cx, |project, cx| {
 912                project.delete_file(file1_path.clone(), false, cx)
 913            })
 914            .unwrap()
 915            .await
 916            .unwrap();
 917        project
 918            .update(cx, |project, cx| {
 919                project.delete_file(file2_path.clone(), false, cx)
 920            })
 921            .unwrap()
 922            .await
 923            .unwrap();
 924        cx.run_until_parked();
 925        assert_eq!(
 926            unreviewed_hunks(&action_log, cx),
 927            vec![
 928                (
 929                    buffer1.clone(),
 930                    vec![HunkStatus {
 931                        range: Point::new(0, 0)..Point::new(0, 0),
 932                        diff_status: DiffHunkStatusKind::Deleted,
 933                        old_text: "lorem\n".into(),
 934                    }]
 935                ),
 936                (
 937                    buffer2.clone(),
 938                    vec![HunkStatus {
 939                        range: Point::new(0, 0)..Point::new(0, 0),
 940                        diff_status: DiffHunkStatusKind::Deleted,
 941                        old_text: "ipsum\n".into(),
 942                    }],
 943                )
 944            ]
 945        );
 946
 947        // Simulate file1 being recreated externally.
 948        fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
 949            .await;
 950
 951        // Simulate file2 being recreated by a tool.
 952        let buffer2 = project
 953            .update(cx, |project, cx| project.open_buffer(file2_path, cx))
 954            .await
 955            .unwrap();
 956        buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
 957        action_log.update(cx, |log, cx| log.will_create_buffer(buffer2.clone(), cx));
 958        project
 959            .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
 960            .await
 961            .unwrap();
 962
 963        cx.run_until_parked();
 964        assert_eq!(
 965            unreviewed_hunks(&action_log, cx),
 966            vec![(
 967                buffer2.clone(),
 968                vec![HunkStatus {
 969                    range: Point::new(0, 0)..Point::new(0, 5),
 970                    diff_status: DiffHunkStatusKind::Modified,
 971                    old_text: "ipsum\n".into(),
 972                }],
 973            )]
 974        );
 975
 976        // Simulate file2 being deleted externally.
 977        fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
 978            .await
 979            .unwrap();
 980        cx.run_until_parked();
 981        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 982    }
 983
 984    #[gpui::test(iterations = 100)]
 985    async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
 986        let operations = env::var("OPERATIONS")
 987            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
 988            .unwrap_or(20);
 989
 990        let action_log = cx.new(|_| ActionLog::new());
 991        let text = RandomCharIter::new(&mut rng).take(50).collect::<String>();
 992        let buffer = cx.new(|cx| Buffer::local(text, cx));
 993        action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 994
 995        for _ in 0..operations {
 996            match rng.gen_range(0..100) {
 997                0..25 => {
 998                    action_log.update(cx, |log, cx| {
 999                        let range = buffer.read(cx).random_byte_range(0, &mut rng);
1000                        log::info!("keeping all edits in range {:?}", range);
1001                        log.keep_edits_in_range(buffer.clone(), range, cx)
1002                    });
1003                }
1004                _ => {
1005                    let is_agent_change = rng.gen_bool(0.5);
1006                    if is_agent_change {
1007                        log::info!("agent edit");
1008                    } else {
1009                        log::info!("user edit");
1010                    }
1011                    cx.update(|cx| {
1012                        buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
1013                        if is_agent_change {
1014                            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1015                        }
1016                    });
1017                }
1018            }
1019
1020            if rng.gen_bool(0.2) {
1021                quiesce(&action_log, &buffer, cx);
1022            }
1023        }
1024
1025        quiesce(&action_log, &buffer, cx);
1026
1027        fn quiesce(
1028            action_log: &Entity<ActionLog>,
1029            buffer: &Entity<Buffer>,
1030            cx: &mut TestAppContext,
1031        ) {
1032            log::info!("quiescing...");
1033            cx.run_until_parked();
1034            action_log.update(cx, |log, cx| {
1035                let tracked_buffer = log.track_buffer(buffer.clone(), false, cx);
1036                let mut old_text = tracked_buffer.base_text.clone();
1037                let new_text = buffer.read(cx).as_rope();
1038                for edit in tracked_buffer.unreviewed_changes.edits() {
1039                    let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
1040                    let old_end = old_text.point_to_offset(cmp::min(
1041                        Point::new(edit.new.start + edit.old_len(), 0),
1042                        old_text.max_point(),
1043                    ));
1044                    old_text.replace(
1045                        old_start..old_end,
1046                        &new_text.slice_rows(edit.new.clone()).to_string(),
1047                    );
1048                }
1049                pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
1050            })
1051        }
1052    }
1053
1054    #[derive(Debug, Clone, PartialEq, Eq)]
1055    struct HunkStatus {
1056        range: Range<Point>,
1057        diff_status: DiffHunkStatusKind,
1058        old_text: String,
1059    }
1060
1061    fn unreviewed_hunks(
1062        action_log: &Entity<ActionLog>,
1063        cx: &TestAppContext,
1064    ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
1065        cx.read(|cx| {
1066            action_log
1067                .read(cx)
1068                .changed_buffers(cx)
1069                .into_iter()
1070                .map(|(buffer, diff)| {
1071                    let snapshot = buffer.read(cx).snapshot();
1072                    (
1073                        buffer,
1074                        diff.read(cx)
1075                            .hunks(&snapshot, cx)
1076                            .map(|hunk| HunkStatus {
1077                                diff_status: hunk.status().kind,
1078                                range: hunk.range,
1079                                old_text: diff
1080                                    .read(cx)
1081                                    .base_text()
1082                                    .text_for_range(hunk.diff_base_byte_range)
1083                                    .collect(),
1084                            })
1085                            .collect(),
1086                    )
1087                })
1088                .collect()
1089        })
1090    }
1091}