Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T08:03:21.845Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

before.rs

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