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::{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                            let unreviewed_changes = match author {
 173                                ChangeAuthor::User => rebase_patch(
 174                                    &unreviewed_changes,
 175                                    edits,
 176                                    &mut base_text,
 177                                    new_snapshot.as_rope(),
 178                                ),
 179                                ChangeAuthor::Agent => unreviewed_changes.compose(edits),
 180                            };
 181                            (
 182                                Arc::new(base_text.to_string()),
 183                                base_text,
 184                                unreviewed_changes,
 185                            )
 186                        }
 187                    });
 188
 189                    anyhow::Ok((
 190                        rebase,
 191                        tracked_buffer.diff.clone(),
 192                        tracked_buffer.buffer.read(cx).language().cloned(),
 193                        tracked_buffer.buffer.read(cx).language_registry(),
 194                    ))
 195                })??;
 196
 197            let (new_base_text, new_base_text_rope, unreviewed_changes) = rebase.await;
 198            let diff_snapshot = BufferDiff::update_diff(
 199                diff.clone(),
 200                buffer_snapshot.clone(),
 201                Some(new_base_text),
 202                true,
 203                false,
 204                language,
 205                language_registry,
 206                cx,
 207            )
 208            .await;
 209            if let Ok(diff_snapshot) = diff_snapshot {
 210                diff.update(cx, |diff, cx| {
 211                    diff.set_snapshot(diff_snapshot, &buffer_snapshot, None, cx)
 212                })?;
 213            }
 214            this.update(cx, |this, cx| {
 215                let tracked_buffer = this
 216                    .tracked_buffers
 217                    .get_mut(&buffer)
 218                    .context("buffer not tracked")?;
 219                tracked_buffer.base_text = new_base_text_rope;
 220                tracked_buffer.snapshot = buffer_snapshot;
 221                tracked_buffer.unreviewed_changes = unreviewed_changes;
 222                cx.notify();
 223                anyhow::Ok(())
 224            })??;
 225        }
 226
 227        Ok(())
 228    }
 229
 230    /// Track a buffer as read, so we can notify the model about user edits.
 231    pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 232        self.track_buffer(buffer, false, cx);
 233    }
 234
 235    /// Track a buffer as read, so we can notify the model about user edits.
 236    pub fn will_create_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 237        self.track_buffer(buffer.clone(), true, cx);
 238        self.buffer_edited(buffer, cx)
 239    }
 240
 241    /// Mark a buffer as edited, so we can refresh it in the context
 242    pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 243        self.edited_since_project_diagnostics_check = true;
 244        self.stale_buffers_in_context.insert(buffer.clone());
 245
 246        let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
 247        if let TrackedBufferStatus::Deleted = tracked_buffer.status {
 248            tracked_buffer.status = TrackedBufferStatus::Modified;
 249        }
 250        tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
 251    }
 252
 253    pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
 254        let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
 255        match tracked_buffer.status {
 256            TrackedBufferStatus::Created => {
 257                self.tracked_buffers.remove(&buffer);
 258                cx.notify();
 259            }
 260            TrackedBufferStatus::Modified => {
 261                buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
 262                tracked_buffer.status = TrackedBufferStatus::Deleted;
 263                tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
 264            }
 265            TrackedBufferStatus::Deleted => {}
 266        }
 267        cx.notify();
 268    }
 269
 270    pub fn keep_edits_in_range<T>(
 271        &mut self,
 272        buffer: Entity<Buffer>,
 273        buffer_range: Range<T>,
 274        cx: &mut Context<Self>,
 275    ) where
 276        T: 'static + language::ToPoint, // + Clone
 277                                        // + Copy
 278                                        // + Ord
 279                                        // + Sub<T, Output = T>
 280                                        // + Add<T, Output = T>
 281                                        // + AddAssign
 282                                        // + Default
 283                                        // + PartialEq,
 284    {
 285        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
 286            return;
 287        };
 288
 289        match tracked_buffer.status {
 290            TrackedBufferStatus::Deleted => {
 291                self.tracked_buffers.remove(&buffer);
 292                cx.notify();
 293            }
 294            _ => {
 295                let buffer = buffer.read(cx);
 296                let buffer_range =
 297                    buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
 298                let buffer_row_range = buffer_range.start.row..buffer_range.end.row + 1;
 299                let mut delta = 0i32;
 300                tracked_buffer.unreviewed_changes.retain_mut(|edit| {
 301                    edit.old.start = (edit.old.start as i32 + delta) as u32;
 302                    edit.old.end = (edit.old.end as i32 + delta) as u32;
 303                    if edit.new.overlaps(&buffer_row_range) {
 304                        let old_bytes = tracked_buffer
 305                            .base_text
 306                            .point_to_offset(Point::new(edit.old.start, 0))
 307                            ..tracked_buffer.base_text.point_to_offset(cmp::min(
 308                                Point::new(edit.old.end, 0),
 309                                tracked_buffer.base_text.max_point(),
 310                            ));
 311                        let new_bytes = tracked_buffer
 312                            .snapshot
 313                            .point_to_offset(Point::new(edit.new.start, 0))
 314                            ..tracked_buffer.snapshot.point_to_offset(cmp::min(
 315                                Point::new(edit.new.end, 0),
 316                                tracked_buffer.snapshot.max_point(),
 317                            ));
 318                        tracked_buffer.base_text.replace(
 319                            old_bytes,
 320                            &tracked_buffer
 321                                .snapshot
 322                                .text_for_range(new_bytes)
 323                                .collect::<String>(),
 324                        );
 325                        delta += edit.new_len() as i32 - edit.old_len() as i32;
 326                        false
 327                    } else {
 328                        true
 329                    }
 330                });
 331                tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
 332            }
 333        }
 334    }
 335
 336    pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) {
 337        self.tracked_buffers
 338            .retain(|_buffer, tracked_buffer| match tracked_buffer.status {
 339                TrackedBufferStatus::Deleted => false,
 340                _ => {
 341                    tracked_buffer.unreviewed_changes.clear();
 342                    tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone();
 343                    tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
 344                    true
 345                }
 346            });
 347        cx.notify();
 348    }
 349
 350    /// Returns the set of buffers that contain changes that haven't been reviewed by the user.
 351    pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
 352        self.tracked_buffers
 353            .iter()
 354            .filter(|(_, tracked)| tracked.has_changes(cx))
 355            .map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone()))
 356            .collect()
 357    }
 358
 359    /// Iterate over buffers changed since last read or edited by the model
 360    pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
 361        self.tracked_buffers
 362            .iter()
 363            .filter(|(buffer, tracked)| {
 364                let buffer = buffer.read(cx);
 365
 366                tracked.version != buffer.version
 367                    && buffer
 368                        .file()
 369                        .map_or(false, |file| file.disk_state() != DiskState::Deleted)
 370            })
 371            .map(|(buffer, _)| buffer)
 372    }
 373
 374    /// Takes and returns the set of buffers pending refresh, clearing internal state.
 375    pub fn take_stale_buffers_in_context(&mut self) -> HashSet<Entity<Buffer>> {
 376        std::mem::take(&mut self.stale_buffers_in_context)
 377    }
 378}
 379
 380fn rebase_patch(
 381    patch: &Patch<u32>,
 382    edits: Vec<Edit<u32>>,
 383    old_text: &mut Rope,
 384    new_text: &Rope,
 385) -> Patch<u32> {
 386    let mut translated_unreviewed_edits = Patch::default();
 387    let mut conflicting_edits = Vec::new();
 388
 389    let mut old_edits = patch.edits().iter().cloned().peekable();
 390    let mut new_edits = edits.into_iter().peekable();
 391    let mut applied_delta = 0i32;
 392    let mut rebased_delta = 0i32;
 393
 394    while let Some(mut new_edit) = new_edits.next() {
 395        let mut conflict = false;
 396
 397        // Push all the old edits that are before this new edit or that intersect with it.
 398        while let Some(old_edit) = old_edits.peek() {
 399            if new_edit.old.end <= old_edit.new.start {
 400                break;
 401            } else if new_edit.old.start >= old_edit.new.end {
 402                let mut old_edit = old_edits.next().unwrap();
 403                old_edit.old.start = (old_edit.old.start as i32 + applied_delta) as u32;
 404                old_edit.old.end = (old_edit.old.end as i32 + applied_delta) as u32;
 405                old_edit.new.start = (old_edit.new.start as i32 + applied_delta) as u32;
 406                old_edit.new.end = (old_edit.new.end as i32 + applied_delta) as u32;
 407                rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
 408                translated_unreviewed_edits.push(old_edit);
 409            } else {
 410                conflict = true;
 411                if new_edits
 412                    .peek()
 413                    .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new))
 414                {
 415                    new_edit.old.start = (new_edit.old.start as i32 + applied_delta) as u32;
 416                    new_edit.old.end = (new_edit.old.end as i32 + applied_delta) as u32;
 417                    conflicting_edits.push(new_edit);
 418                    new_edit = new_edits.next().unwrap();
 419                } else {
 420                    let mut old_edit = old_edits.next().unwrap();
 421                    old_edit.old.start = (old_edit.old.start as i32 + applied_delta) as u32;
 422                    old_edit.old.end = (old_edit.old.end as i32 + applied_delta) as u32;
 423                    old_edit.new.start = (old_edit.new.start as i32 + applied_delta) as u32;
 424                    old_edit.new.end = (old_edit.new.end as i32 + applied_delta) as u32;
 425                    rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32;
 426                    translated_unreviewed_edits.push(old_edit);
 427                }
 428            }
 429        }
 430
 431        if conflict {
 432            new_edit.old.start = (new_edit.old.start as i32 + applied_delta) as u32;
 433            new_edit.old.end = (new_edit.old.end as i32 + applied_delta) as u32;
 434            conflicting_edits.push(new_edit);
 435        } else {
 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    // Push all the outstanding old edits.
 459    for mut old_edit in old_edits {
 460        old_edit.old.start = (old_edit.old.start as i32 + applied_delta) as u32;
 461        old_edit.old.end = (old_edit.old.end as i32 + applied_delta) as u32;
 462        old_edit.new.start = (old_edit.new.start as i32 + applied_delta) as u32;
 463        old_edit.new.end = (old_edit.new.end as i32 + applied_delta) as u32;
 464        translated_unreviewed_edits.push(old_edit);
 465    }
 466
 467    translated_unreviewed_edits.compose(conflicting_edits)
 468}
 469
 470fn diff_snapshots(
 471    old_snapshot: &text::BufferSnapshot,
 472    new_snapshot: &text::BufferSnapshot,
 473) -> Vec<Edit<u32>> {
 474    let mut edits = new_snapshot
 475        .edits_since::<Point>(&old_snapshot.version)
 476        .map(|edit| {
 477            if edit.old.start.column == old_snapshot.line_len(edit.old.start.row)
 478                && new_snapshot.chars_at(edit.new.start).next() == Some('\n')
 479                && edit.old.start != old_snapshot.max_point()
 480            {
 481                Edit {
 482                    old: edit.old.start.row + 1..edit.old.end.row + 1,
 483                    new: edit.new.start.row + 1..edit.new.end.row + 1,
 484                }
 485            } else if edit.old.start.column == 0
 486                && edit.old.end.column == 0
 487                && edit.new.end.column == 0
 488                && edit.old.end != old_snapshot.max_point()
 489            {
 490                Edit {
 491                    old: edit.old.start.row..edit.old.end.row,
 492                    new: edit.new.start.row..edit.new.end.row,
 493                }
 494            } else {
 495                Edit {
 496                    old: edit.old.start.row..edit.old.end.row + 1,
 497                    new: edit.new.start.row..edit.new.end.row + 1,
 498                }
 499            }
 500        })
 501        .peekable();
 502    let mut row_edits = Vec::new();
 503    while let Some(mut edit) = edits.next() {
 504        while let Some(next_edit) = edits.peek() {
 505            if edit.old.end >= next_edit.old.start {
 506                edit.old.end = next_edit.old.end;
 507                edit.new.end = next_edit.new.end;
 508                edits.next();
 509            } else {
 510                break;
 511            }
 512        }
 513        row_edits.push(edit);
 514    }
 515    row_edits
 516}
 517
 518enum ChangeAuthor {
 519    User,
 520    Agent,
 521}
 522
 523#[derive(Copy, Clone, Eq, PartialEq)]
 524enum TrackedBufferStatus {
 525    Created,
 526    Modified,
 527    Deleted,
 528}
 529
 530struct TrackedBuffer {
 531    buffer: Entity<Buffer>,
 532    base_text: Rope,
 533    unreviewed_changes: Patch<u32>,
 534    status: TrackedBufferStatus,
 535    version: clock::Global,
 536    diff: Entity<BufferDiff>,
 537    snapshot: text::BufferSnapshot,
 538    diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
 539    _maintain_diff: Task<()>,
 540    _subscription: Subscription,
 541}
 542
 543impl TrackedBuffer {
 544    fn has_changes(&self, cx: &App) -> bool {
 545        self.diff
 546            .read(cx)
 547            .hunks(&self.buffer.read(cx), cx)
 548            .next()
 549            .is_some()
 550    }
 551
 552    fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) {
 553        self.diff_update
 554            .unbounded_send((author, self.buffer.read(cx).text_snapshot()))
 555            .ok();
 556    }
 557}
 558
 559pub struct ChangedBuffer {
 560    pub diff: Entity<BufferDiff>,
 561}
 562
 563#[cfg(test)]
 564mod tests {
 565    use std::env;
 566
 567    use super::*;
 568    use buffer_diff::DiffHunkStatusKind;
 569    use gpui::TestAppContext;
 570    use language::Point;
 571    use project::{FakeFs, Fs, Project, RemoveOptions};
 572    use rand::prelude::*;
 573    use serde_json::json;
 574    use settings::SettingsStore;
 575    use util::{RandomCharIter, path, post_inc};
 576
 577    #[ctor::ctor]
 578    fn init_logger() {
 579        if std::env::var("RUST_LOG").is_ok() {
 580            env_logger::init();
 581        }
 582    }
 583
 584    #[gpui::test(iterations = 10)]
 585    async fn test_edit_review(cx: &mut TestAppContext) {
 586        let action_log = cx.new(|_| ActionLog::new());
 587        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
 588
 589        cx.update(|cx| {
 590            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 591            buffer.update(cx, |buffer, cx| {
 592                buffer
 593                    .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
 594                    .unwrap()
 595            });
 596            buffer.update(cx, |buffer, cx| {
 597                buffer
 598                    .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
 599                    .unwrap()
 600            });
 601            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 602        });
 603        cx.run_until_parked();
 604        assert_eq!(
 605            buffer.read_with(cx, |buffer, _| buffer.text()),
 606            "abc\ndEf\nghi\njkl\nmnO"
 607        );
 608        assert_eq!(
 609            unreviewed_hunks(&action_log, cx),
 610            vec![(
 611                buffer.clone(),
 612                vec![
 613                    HunkStatus {
 614                        range: Point::new(1, 0)..Point::new(2, 0),
 615                        diff_status: DiffHunkStatusKind::Modified,
 616                        old_text: "def\n".into(),
 617                    },
 618                    HunkStatus {
 619                        range: Point::new(4, 0)..Point::new(4, 3),
 620                        diff_status: DiffHunkStatusKind::Modified,
 621                        old_text: "mno".into(),
 622                    }
 623                ],
 624            )]
 625        );
 626
 627        action_log.update(cx, |log, cx| {
 628            log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
 629        });
 630        cx.run_until_parked();
 631        assert_eq!(
 632            unreviewed_hunks(&action_log, cx),
 633            vec![(
 634                buffer.clone(),
 635                vec![HunkStatus {
 636                    range: Point::new(1, 0)..Point::new(2, 0),
 637                    diff_status: DiffHunkStatusKind::Modified,
 638                    old_text: "def\n".into(),
 639                }],
 640            )]
 641        );
 642
 643        action_log.update(cx, |log, cx| {
 644            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
 645        });
 646        cx.run_until_parked();
 647        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 648    }
 649
 650    #[gpui::test(iterations = 10)]
 651    async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
 652        let action_log = cx.new(|_| ActionLog::new());
 653        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
 654
 655        cx.update(|cx| {
 656            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 657            buffer.update(cx, |buffer, cx| {
 658                buffer
 659                    .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
 660                    .unwrap()
 661            });
 662            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 663        });
 664        cx.run_until_parked();
 665        assert_eq!(
 666            buffer.read_with(cx, |buffer, _| buffer.text()),
 667            "abc\ndeF\nGHI\njkl\nmno"
 668        );
 669        assert_eq!(
 670            unreviewed_hunks(&action_log, cx),
 671            vec![(
 672                buffer.clone(),
 673                vec![HunkStatus {
 674                    range: Point::new(1, 0)..Point::new(3, 0),
 675                    diff_status: DiffHunkStatusKind::Modified,
 676                    old_text: "def\nghi\n".into(),
 677                }],
 678            )]
 679        );
 680
 681        buffer.update(cx, |buffer, cx| {
 682            buffer.edit(
 683                [
 684                    (Point::new(0, 2)..Point::new(0, 2), "X"),
 685                    (Point::new(3, 0)..Point::new(3, 0), "Y"),
 686                ],
 687                None,
 688                cx,
 689            )
 690        });
 691        cx.run_until_parked();
 692        assert_eq!(
 693            buffer.read_with(cx, |buffer, _| buffer.text()),
 694            "abXc\ndeF\nGHI\nYjkl\nmno"
 695        );
 696        assert_eq!(
 697            unreviewed_hunks(&action_log, cx),
 698            vec![(
 699                buffer.clone(),
 700                vec![HunkStatus {
 701                    range: Point::new(1, 0)..Point::new(3, 0),
 702                    diff_status: DiffHunkStatusKind::Modified,
 703                    old_text: "def\nghi\n".into(),
 704                }],
 705            )]
 706        );
 707
 708        buffer.update(cx, |buffer, cx| {
 709            buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx)
 710        });
 711        cx.run_until_parked();
 712        assert_eq!(
 713            buffer.read_with(cx, |buffer, _| buffer.text()),
 714            "abXc\ndZeF\nGHI\nYjkl\nmno"
 715        );
 716        assert_eq!(
 717            unreviewed_hunks(&action_log, cx),
 718            vec![(
 719                buffer.clone(),
 720                vec![HunkStatus {
 721                    range: Point::new(1, 0)..Point::new(3, 0),
 722                    diff_status: DiffHunkStatusKind::Modified,
 723                    old_text: "def\nghi\n".into(),
 724                }],
 725            )]
 726        );
 727
 728        action_log.update(cx, |log, cx| {
 729            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
 730        });
 731        cx.run_until_parked();
 732        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 733    }
 734
 735    #[gpui::test(iterations = 10)]
 736    async fn test_creation(cx: &mut TestAppContext) {
 737        cx.update(|cx| {
 738            let settings_store = SettingsStore::test(cx);
 739            cx.set_global(settings_store);
 740            language::init(cx);
 741            Project::init_settings(cx);
 742        });
 743
 744        let action_log = cx.new(|_| ActionLog::new());
 745
 746        let fs = FakeFs::new(cx.executor());
 747        fs.insert_tree(path!("/dir"), json!({})).await;
 748
 749        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 750        let file_path = project
 751            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
 752            .unwrap();
 753
 754        // Simulate file2 being recreated by a tool.
 755        let buffer = project
 756            .update(cx, |project, cx| project.open_buffer(file_path, cx))
 757            .await
 758            .unwrap();
 759        cx.update(|cx| {
 760            buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
 761            action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
 762        });
 763        project
 764            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 765            .await
 766            .unwrap();
 767        cx.run_until_parked();
 768        assert_eq!(
 769            unreviewed_hunks(&action_log, cx),
 770            vec![(
 771                buffer.clone(),
 772                vec![HunkStatus {
 773                    range: Point::new(0, 0)..Point::new(0, 5),
 774                    diff_status: DiffHunkStatusKind::Added,
 775                    old_text: "".into(),
 776                }],
 777            )]
 778        );
 779
 780        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx));
 781        cx.run_until_parked();
 782        assert_eq!(
 783            unreviewed_hunks(&action_log, cx),
 784            vec![(
 785                buffer.clone(),
 786                vec![HunkStatus {
 787                    range: Point::new(0, 0)..Point::new(0, 6),
 788                    diff_status: DiffHunkStatusKind::Added,
 789                    old_text: "".into(),
 790                }],
 791            )]
 792        );
 793
 794        action_log.update(cx, |log, cx| {
 795            log.keep_edits_in_range(buffer.clone(), 0..5, cx)
 796        });
 797        cx.run_until_parked();
 798        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 799    }
 800
 801    #[gpui::test(iterations = 10)]
 802    async fn test_deletion(cx: &mut TestAppContext) {
 803        cx.update(|cx| {
 804            let settings_store = SettingsStore::test(cx);
 805            cx.set_global(settings_store);
 806            language::init(cx);
 807            Project::init_settings(cx);
 808        });
 809
 810        let fs = FakeFs::new(cx.executor());
 811        fs.insert_tree(
 812            path!("/dir"),
 813            json!({"file1": "lorem\n", "file2": "ipsum\n"}),
 814        )
 815        .await;
 816
 817        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 818        let file1_path = project
 819            .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
 820            .unwrap();
 821        let file2_path = project
 822            .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
 823            .unwrap();
 824
 825        let action_log = cx.new(|_| ActionLog::new());
 826        let buffer1 = project
 827            .update(cx, |project, cx| {
 828                project.open_buffer(file1_path.clone(), cx)
 829            })
 830            .await
 831            .unwrap();
 832        let buffer2 = project
 833            .update(cx, |project, cx| {
 834                project.open_buffer(file2_path.clone(), cx)
 835            })
 836            .await
 837            .unwrap();
 838
 839        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
 840        action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
 841        project
 842            .update(cx, |project, cx| {
 843                project.delete_file(file1_path.clone(), false, cx)
 844            })
 845            .unwrap()
 846            .await
 847            .unwrap();
 848        project
 849            .update(cx, |project, cx| {
 850                project.delete_file(file2_path.clone(), false, cx)
 851            })
 852            .unwrap()
 853            .await
 854            .unwrap();
 855        cx.run_until_parked();
 856        assert_eq!(
 857            unreviewed_hunks(&action_log, cx),
 858            vec![
 859                (
 860                    buffer1.clone(),
 861                    vec![HunkStatus {
 862                        range: Point::new(0, 0)..Point::new(0, 0),
 863                        diff_status: DiffHunkStatusKind::Deleted,
 864                        old_text: "lorem\n".into(),
 865                    }]
 866                ),
 867                (
 868                    buffer2.clone(),
 869                    vec![HunkStatus {
 870                        range: Point::new(0, 0)..Point::new(0, 0),
 871                        diff_status: DiffHunkStatusKind::Deleted,
 872                        old_text: "ipsum\n".into(),
 873                    }],
 874                )
 875            ]
 876        );
 877
 878        // Simulate file1 being recreated externally.
 879        fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
 880            .await;
 881
 882        // Simulate file2 being recreated by a tool.
 883        let buffer2 = project
 884            .update(cx, |project, cx| project.open_buffer(file2_path, cx))
 885            .await
 886            .unwrap();
 887        buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
 888        action_log.update(cx, |log, cx| log.will_create_buffer(buffer2.clone(), cx));
 889        project
 890            .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
 891            .await
 892            .unwrap();
 893
 894        cx.run_until_parked();
 895        assert_eq!(
 896            unreviewed_hunks(&action_log, cx),
 897            vec![(
 898                buffer2.clone(),
 899                vec![HunkStatus {
 900                    range: Point::new(0, 0)..Point::new(0, 5),
 901                    diff_status: DiffHunkStatusKind::Modified,
 902                    old_text: "ipsum\n".into(),
 903                }],
 904            )]
 905        );
 906
 907        // Simulate file2 being deleted externally.
 908        fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
 909            .await
 910            .unwrap();
 911        cx.run_until_parked();
 912        assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
 913    }
 914
 915    #[gpui::test(iterations = 100)]
 916    async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
 917        let operations = env::var("OPERATIONS")
 918            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
 919            .unwrap_or(20);
 920
 921        let action_log = cx.new(|_| ActionLog::new());
 922        let text = RandomCharIter::new(&mut rng).take(50).collect::<String>();
 923        let buffer = cx.new(|cx| Buffer::local(text, cx));
 924        action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 925
 926        for _ in 0..operations {
 927            match rng.gen_range(0..100) {
 928                0..25 => {
 929                    action_log.update(cx, |log, cx| {
 930                        let range = buffer.read(cx).random_byte_range(0, &mut rng);
 931                        log::info!("keeping all edits in range {:?}", range);
 932                        log.keep_edits_in_range(buffer.clone(), range, cx)
 933                    });
 934                }
 935                _ => {
 936                    let is_agent_change = rng.gen_bool(0.5);
 937                    if is_agent_change {
 938                        log::info!("agent edit");
 939                    } else {
 940                        log::info!("user edit");
 941                    }
 942                    cx.update(|cx| {
 943                        buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
 944                        if is_agent_change {
 945                            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 946                        }
 947                    });
 948                }
 949            }
 950
 951            if rng.gen_bool(0.2) {
 952                quiesce(&action_log, &buffer, cx);
 953            }
 954        }
 955
 956        quiesce(&action_log, &buffer, cx);
 957
 958        fn quiesce(
 959            action_log: &Entity<ActionLog>,
 960            buffer: &Entity<Buffer>,
 961            cx: &mut TestAppContext,
 962        ) {
 963            log::info!("quiescing...");
 964            cx.run_until_parked();
 965            action_log.update(cx, |log, cx| {
 966                let tracked_buffer = log.track_buffer(buffer.clone(), false, cx);
 967                let mut old_text = tracked_buffer.base_text.clone();
 968                let new_text = buffer.read(cx).as_rope();
 969                for edit in tracked_buffer.unreviewed_changes.edits() {
 970                    let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
 971                    let old_end = old_text.point_to_offset(cmp::min(
 972                        Point::new(edit.new.start + edit.old_len(), 0),
 973                        old_text.max_point(),
 974                    ));
 975                    old_text.replace(
 976                        old_start..old_end,
 977                        &new_text.slice_rows(edit.new.clone()).to_string(),
 978                    );
 979                }
 980                pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
 981            })
 982        }
 983    }
 984
 985    #[gpui::test(iterations = 100)]
 986    fn test_rebase_random(mut rng: StdRng) {
 987        let operations = env::var("OPERATIONS")
 988            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
 989            .unwrap_or(20);
 990
 991        let mut next_line_id = 0;
 992        let base_lines = (0..rng.gen_range(1..=20))
 993            .map(|_| post_inc(&mut next_line_id).to_string())
 994            .collect::<Vec<_>>();
 995        log::info!("base lines: {:?}", base_lines);
 996
 997        let (new_lines, patch_1) =
 998            build_edits(&base_lines, operations, &mut rng, &mut next_line_id);
 999        log::info!("agent edits: {:#?}", patch_1);
1000        let (new_lines, patch_2) = build_edits(&new_lines, operations, &mut rng, &mut next_line_id);
1001        log::info!("user edits: {:#?}", patch_2);
1002
1003        let mut old_text = Rope::from(base_lines.join("\n"));
1004        let new_text = Rope::from(new_lines.join("\n"));
1005        let patch = rebase_patch(&patch_1, patch_2.into_inner(), &mut old_text, &new_text);
1006        log::info!("rebased edits: {:#?}", patch.edits());
1007
1008        for edit in patch.edits() {
1009            let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
1010            let old_end = old_text.point_to_offset(cmp::min(
1011                Point::new(edit.new.start + edit.old_len(), 0),
1012                old_text.max_point(),
1013            ));
1014            old_text.replace(
1015                old_start..old_end,
1016                &new_text.slice_rows(edit.new.clone()).to_string(),
1017            );
1018        }
1019        pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());
1020    }
1021
1022    fn build_edits(
1023        lines: &Vec<String>,
1024        count: usize,
1025        rng: &mut StdRng,
1026        next_line_id: &mut usize,
1027    ) -> (Vec<String>, Patch<u32>) {
1028        let mut delta = 0i32;
1029        let mut last_edit_end = 0;
1030        let mut edits = Patch::default();
1031        let mut edited_lines = lines.clone();
1032        for _ in 0..count {
1033            if last_edit_end >= lines.len() {
1034                break;
1035            }
1036
1037            let end = rng.gen_range(last_edit_end..lines.len());
1038            let start = rng.gen_range(last_edit_end..=end);
1039            let old_len = end - start;
1040
1041            let mut new_len: usize = rng.gen_range(0..=3);
1042            if start == end && new_len == 0 {
1043                new_len += 1;
1044            }
1045
1046            last_edit_end = end + 1;
1047
1048            let new_lines = (0..new_len)
1049                .map(|_| post_inc(next_line_id).to_string())
1050                .collect::<Vec<_>>();
1051            log::info!("  editing {:?}: {:?}", start..end, new_lines);
1052            let old = start as u32..end as u32;
1053            let new = (start as i32 + delta) as u32..(start as i32 + delta + new_len as i32) as u32;
1054            edited_lines.splice(
1055                new.start as usize..new.start as usize + old.len(),
1056                new_lines,
1057            );
1058            edits.push(Edit { old, new });
1059            delta += new_len as i32 - old_len as i32;
1060        }
1061        (edited_lines, edits)
1062    }
1063
1064    #[derive(Debug, Clone, PartialEq, Eq)]
1065    struct HunkStatus {
1066        range: Range<Point>,
1067        diff_status: DiffHunkStatusKind,
1068        old_text: String,
1069    }
1070
1071    fn unreviewed_hunks(
1072        action_log: &Entity<ActionLog>,
1073        cx: &TestAppContext,
1074    ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
1075        cx.read(|cx| {
1076            action_log
1077                .read(cx)
1078                .changed_buffers(cx)
1079                .into_iter()
1080                .map(|(buffer, diff)| {
1081                    let snapshot = buffer.read(cx).snapshot();
1082                    (
1083                        buffer,
1084                        diff.read(cx)
1085                            .hunks(&snapshot, cx)
1086                            .map(|hunk| HunkStatus {
1087                                diff_status: hunk.status().kind,
1088                                range: hunk.range,
1089                                old_text: diff
1090                                    .read(cx)
1091                                    .base_text()
1092                                    .text_for_range(hunk.diff_base_byte_range)
1093                                    .collect(),
1094                            })
1095                            .collect(),
1096                    )
1097                })
1098                .collect()
1099        })
1100    }
1101}