action_log.rs

  1use anyhow::{anyhow, Result};
  2use buffer_diff::BufferDiff;
  3use collections::{BTreeMap, HashMap, HashSet};
  4use gpui::{App, AppContext, Context, Entity, Task};
  5use language::{Buffer, OffsetRangeExt, ToOffset};
  6use std::{future::Future, ops::Range};
  7
  8/// Tracks actions performed by tools in a thread
  9#[derive(Debug)]
 10pub struct ActionLog {
 11    /// Buffers that user manually added to the context, and whose content has
 12    /// changed since the model last saw them.
 13    stale_buffers_in_context: HashSet<Entity<Buffer>>,
 14    /// Buffers that we want to notify the model about when they change.
 15    tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
 16}
 17
 18#[derive(Debug, Clone)]
 19pub struct TrackedBuffer {
 20    buffer: Entity<Buffer>,
 21    unreviewed_edit_ids: Vec<clock::Lamport>,
 22    accepted_edit_ids: Vec<clock::Lamport>,
 23    version: clock::Global,
 24    diff: Entity<BufferDiff>,
 25    secondary_diff: Entity<BufferDiff>,
 26}
 27
 28impl TrackedBuffer {
 29    pub fn needs_review(&self) -> bool {
 30        !self.unreviewed_edit_ids.is_empty()
 31    }
 32
 33    pub fn diff(&self) -> &Entity<BufferDiff> {
 34        &self.diff
 35    }
 36
 37    fn update_diff(&mut self, cx: &mut App) -> impl 'static + Future<Output = ()> {
 38        let edits_to_undo = self
 39            .unreviewed_edit_ids
 40            .iter()
 41            .chain(&self.accepted_edit_ids)
 42            .map(|edit_id| (*edit_id, u32::MAX))
 43            .collect::<HashMap<_, _>>();
 44        let buffer_without_edits = self.buffer.update(cx, |buffer, cx| buffer.branch(cx));
 45        buffer_without_edits.update(cx, |buffer, cx| {
 46            buffer.undo_operations(edits_to_undo, cx);
 47        });
 48        let primary_diff_update = self.diff.update(cx, |diff, cx| {
 49            diff.set_base_text(
 50                buffer_without_edits,
 51                self.buffer.read(cx).text_snapshot(),
 52                cx,
 53            )
 54        });
 55
 56        let unreviewed_edits_to_undo = self
 57            .unreviewed_edit_ids
 58            .iter()
 59            .map(|edit_id| (*edit_id, u32::MAX))
 60            .collect::<HashMap<_, _>>();
 61        let buffer_without_unreviewed_edits =
 62            self.buffer.update(cx, |buffer, cx| buffer.branch(cx));
 63        buffer_without_unreviewed_edits.update(cx, |buffer, cx| {
 64            buffer.undo_operations(unreviewed_edits_to_undo, cx);
 65        });
 66        let secondary_diff_update = self.secondary_diff.update(cx, |diff, cx| {
 67            diff.set_base_text(
 68                buffer_without_unreviewed_edits.clone(),
 69                self.buffer.read(cx).text_snapshot(),
 70                cx,
 71            )
 72        });
 73
 74        async move {
 75            _ = primary_diff_update.await;
 76            _ = secondary_diff_update.await;
 77        }
 78    }
 79}
 80
 81impl ActionLog {
 82    /// Creates a new, empty action log.
 83    pub fn new() -> Self {
 84        Self {
 85            stale_buffers_in_context: HashSet::default(),
 86            tracked_buffers: BTreeMap::default(),
 87        }
 88    }
 89
 90    fn track_buffer(
 91        &mut self,
 92        buffer: Entity<Buffer>,
 93        cx: &mut Context<Self>,
 94    ) -> &mut TrackedBuffer {
 95        let tracked_buffer = self
 96            .tracked_buffers
 97            .entry(buffer.clone())
 98            .or_insert_with(|| {
 99                let text_snapshot = buffer.read(cx).text_snapshot();
100                let unreviewed_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
101                let diff = cx.new(|cx| {
102                    let mut diff = BufferDiff::new(&text_snapshot, cx);
103                    diff.set_secondary_diff(unreviewed_diff.clone());
104                    diff
105                });
106                TrackedBuffer {
107                    buffer: buffer.clone(),
108                    unreviewed_edit_ids: Vec::new(),
109                    accepted_edit_ids: Vec::new(),
110                    version: buffer.read(cx).version(),
111                    diff,
112                    secondary_diff: unreviewed_diff,
113                }
114            });
115        tracked_buffer.version = buffer.read(cx).version();
116        tracked_buffer
117    }
118
119    /// Track a buffer as read, so we can notify the model about user edits.
120    pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
121        self.track_buffer(buffer, cx);
122    }
123
124    /// Mark a buffer as edited, so we can refresh it in the context
125    pub fn buffer_edited(
126        &mut self,
127        buffer: Entity<Buffer>,
128        edit_ids: Vec<clock::Lamport>,
129        cx: &mut Context<Self>,
130    ) -> Task<Result<()>> {
131        self.stale_buffers_in_context.insert(buffer.clone());
132
133        let tracked_buffer = self.track_buffer(buffer.clone(), cx);
134        tracked_buffer
135            .unreviewed_edit_ids
136            .extend(edit_ids.iter().copied());
137        let update = tracked_buffer.update_diff(cx);
138        cx.spawn(async move |this, cx| {
139            update.await;
140            this.update(cx, |_this, cx| cx.notify())?;
141            Ok(())
142        })
143    }
144
145    /// Accepts edits in a given range within a buffer.
146    pub fn review_edits_in_range<T: ToOffset>(
147        &mut self,
148        buffer: Entity<Buffer>,
149        buffer_range: Range<T>,
150        accept: bool,
151        cx: &mut Context<Self>,
152    ) -> Task<Result<()>> {
153        let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
154            return Task::ready(Err(anyhow!("buffer not found")));
155        };
156
157        let buffer = buffer.read(cx);
158        let buffer_range = buffer_range.to_offset(buffer);
159
160        let source;
161        let destination;
162        if accept {
163            source = &mut tracked_buffer.unreviewed_edit_ids;
164            destination = &mut tracked_buffer.accepted_edit_ids;
165        } else {
166            source = &mut tracked_buffer.accepted_edit_ids;
167            destination = &mut tracked_buffer.unreviewed_edit_ids;
168        }
169
170        source.retain(|edit_id| {
171            for range in buffer.edited_ranges_for_edit_ids::<usize>([edit_id]) {
172                if buffer_range.end >= range.start && buffer_range.start <= range.end {
173                    destination.push(*edit_id);
174                    return false;
175                }
176            }
177            true
178        });
179
180        let update = tracked_buffer.update_diff(cx);
181        cx.spawn(async move |this, cx| {
182            update.await;
183            this.update(cx, |_this, cx| cx.notify())?;
184            Ok(())
185        })
186    }
187
188    /// Returns the set of buffers that contain changes that haven't been reviewed by the user.
189    pub fn unreviewed_buffers(&self) -> BTreeMap<Entity<Buffer>, TrackedBuffer> {
190        self.tracked_buffers
191            .iter()
192            .map(|(buffer, tracked)| (buffer.clone(), tracked.clone()))
193            .collect()
194    }
195
196    /// Iterate over buffers changed since last read or edited by the model
197    pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
198        self.tracked_buffers
199            .iter()
200            .filter(|(buffer, tracked)| tracked.version != buffer.read(cx).version)
201            .map(|(buffer, _)| buffer)
202    }
203
204    /// Takes and returns the set of buffers pending refresh, clearing internal state.
205    pub fn take_stale_buffers_in_context(&mut self) -> HashSet<Entity<Buffer>> {
206        std::mem::take(&mut self.stale_buffers_in_context)
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use buffer_diff::DiffHunkStatusKind;
214    use gpui::TestAppContext;
215    use language::Point;
216
217    #[gpui::test]
218    async fn test_edit_review(cx: &mut TestAppContext) {
219        let action_log = cx.new(|_| ActionLog::new());
220        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
221
222        let edit1 = buffer.update(cx, |buffer, cx| {
223            buffer
224                .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
225                .unwrap()
226        });
227        let edit2 = buffer.update(cx, |buffer, cx| {
228            buffer
229                .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
230                .unwrap()
231        });
232        assert_eq!(
233            buffer.read_with(cx, |buffer, _| buffer.text()),
234            "abc\ndEf\nghi\njkl\nmnO"
235        );
236
237        action_log
238            .update(cx, |log, cx| {
239                log.buffer_edited(buffer.clone(), vec![edit1, edit2], cx)
240            })
241            .await
242            .unwrap();
243        assert_eq!(
244            unreviewed_hunks(&action_log, cx),
245            vec![(
246                buffer.clone(),
247                vec![
248                    HunkStatus {
249                        range: Point::new(1, 0)..Point::new(2, 0),
250                        review_status: ReviewStatus::Unreviewed,
251                        diff_status: DiffHunkStatusKind::Modified,
252                    },
253                    HunkStatus {
254                        range: Point::new(4, 0)..Point::new(4, 3),
255                        review_status: ReviewStatus::Unreviewed,
256                        diff_status: DiffHunkStatusKind::Modified,
257                    }
258                ],
259            )]
260        );
261
262        action_log
263            .update(cx, |log, cx| {
264                log.review_edits_in_range(
265                    buffer.clone(),
266                    Point::new(3, 0)..Point::new(4, 3),
267                    true,
268                    cx,
269                )
270            })
271            .await
272            .unwrap();
273        assert_eq!(
274            unreviewed_hunks(&action_log, cx),
275            vec![(
276                buffer.clone(),
277                vec![
278                    HunkStatus {
279                        range: Point::new(1, 0)..Point::new(2, 0),
280                        review_status: ReviewStatus::Unreviewed,
281                        diff_status: DiffHunkStatusKind::Modified,
282                    },
283                    HunkStatus {
284                        range: Point::new(4, 0)..Point::new(4, 3),
285                        review_status: ReviewStatus::Reviewed,
286                        diff_status: DiffHunkStatusKind::Modified,
287                    }
288                ],
289            )]
290        );
291
292        action_log
293            .update(cx, |log, cx| {
294                log.review_edits_in_range(
295                    buffer.clone(),
296                    Point::new(3, 0)..Point::new(4, 3),
297                    false,
298                    cx,
299                )
300            })
301            .await
302            .unwrap();
303        assert_eq!(
304            unreviewed_hunks(&action_log, cx),
305            vec![(
306                buffer.clone(),
307                vec![
308                    HunkStatus {
309                        range: Point::new(1, 0)..Point::new(2, 0),
310                        review_status: ReviewStatus::Unreviewed,
311                        diff_status: DiffHunkStatusKind::Modified,
312                    },
313                    HunkStatus {
314                        range: Point::new(4, 0)..Point::new(4, 3),
315                        review_status: ReviewStatus::Unreviewed,
316                        diff_status: DiffHunkStatusKind::Modified,
317                    }
318                ],
319            )]
320        );
321
322        action_log
323            .update(cx, |log, cx| {
324                log.review_edits_in_range(
325                    buffer.clone(),
326                    Point::new(0, 0)..Point::new(4, 3),
327                    true,
328                    cx,
329                )
330            })
331            .await
332            .unwrap();
333        assert_eq!(
334            unreviewed_hunks(&action_log, cx),
335            vec![(
336                buffer.clone(),
337                vec![
338                    HunkStatus {
339                        range: Point::new(1, 0)..Point::new(2, 0),
340                        review_status: ReviewStatus::Reviewed,
341                        diff_status: DiffHunkStatusKind::Modified,
342                    },
343                    HunkStatus {
344                        range: Point::new(4, 0)..Point::new(4, 3),
345                        review_status: ReviewStatus::Reviewed,
346                        diff_status: DiffHunkStatusKind::Modified,
347                    }
348                ],
349            )]
350        );
351    }
352
353    #[derive(Debug, Clone, PartialEq, Eq)]
354    struct HunkStatus {
355        range: Range<Point>,
356        review_status: ReviewStatus,
357        diff_status: DiffHunkStatusKind,
358    }
359
360    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
361    enum ReviewStatus {
362        Unreviewed,
363        Reviewed,
364    }
365
366    fn unreviewed_hunks(
367        action_log: &Entity<ActionLog>,
368        cx: &TestAppContext,
369    ) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
370        cx.read(|cx| {
371            action_log
372                .read(cx)
373                .unreviewed_buffers()
374                .into_iter()
375                .map(|(buffer, tracked_buffer)| {
376                    let snapshot = buffer.read(cx).snapshot();
377                    (
378                        buffer,
379                        tracked_buffer
380                            .diff
381                            .read(cx)
382                            .hunks(&snapshot, cx)
383                            .map(|hunk| HunkStatus {
384                                review_status: if hunk.status().has_secondary_hunk() {
385                                    ReviewStatus::Unreviewed
386                                } else {
387                                    ReviewStatus::Reviewed
388                                },
389                                diff_status: hunk.status().kind,
390                                range: hunk.range,
391                            })
392                            .collect(),
393                    )
394                })
395                .collect()
396        })
397    }
398}