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