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