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}