1use anyhow::Result;
2use buffer_diff::BufferDiff;
3use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task};
4use itertools::Itertools;
5use language::{
6 Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, TextBuffer,
7};
8use multi_buffer::{MultiBuffer, PathKey, excerpt_context_lines};
9use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
10use util::ResultExt;
11
12pub enum Diff {
13 Pending(PendingDiff),
14 Finalized(FinalizedDiff),
15}
16
17impl Diff {
18 pub fn finalized(
19 path: String,
20 old_text: Option<String>,
21 new_text: String,
22 language_registry: Arc<LanguageRegistry>,
23 cx: &mut Context<Self>,
24 ) -> Self {
25 let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly));
26 let new_buffer = cx.new(|cx| Buffer::local(new_text, cx));
27 let base_text = old_text.clone().unwrap_or(String::new()).into();
28 let task = cx.spawn({
29 let multibuffer = multibuffer.clone();
30 let path = path.clone();
31 let buffer = new_buffer.clone();
32 async move |_, cx| {
33 let language = language_registry
34 .load_language_for_file_path(Path::new(&path))
35 .await
36 .log_err();
37
38 buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx));
39 buffer.update(cx, |buffer, _| buffer.parsing_idle()).await;
40
41 let diff = build_buffer_diff(
42 old_text.unwrap_or("".into()).into(),
43 &buffer,
44 Some(language_registry.clone()),
45 cx,
46 )
47 .await?;
48
49 multibuffer.update(cx, |multibuffer, cx| {
50 let hunk_ranges = {
51 let buffer = buffer.read(cx);
52 diff.read(cx)
53 .snapshot(cx)
54 .hunks_intersecting_range(
55 Anchor::min_for_buffer(buffer.remote_id())
56 ..Anchor::max_for_buffer(buffer.remote_id()),
57 buffer,
58 )
59 .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
60 .collect::<Vec<_>>()
61 };
62
63 multibuffer.set_excerpts_for_path(
64 PathKey::for_buffer(&buffer, cx),
65 buffer.clone(),
66 hunk_ranges,
67 excerpt_context_lines(cx),
68 cx,
69 );
70 multibuffer.add_diff(diff, cx);
71 });
72
73 anyhow::Ok(())
74 }
75 });
76
77 Self::Finalized(FinalizedDiff {
78 multibuffer,
79 path,
80 base_text,
81 new_buffer,
82 _update_diff: task,
83 })
84 }
85
86 pub fn new(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self {
87 let buffer_text_snapshot = buffer.read(cx).text_snapshot();
88 let language = buffer.read(cx).language().cloned();
89 let language_registry = buffer.read(cx).language_registry();
90 let buffer_diff = cx.new(|cx| {
91 let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, cx);
92 diff.language_changed(language.clone(), language_registry.clone(), cx);
93 let secondary_diff = cx.new(|cx| {
94 // For the secondary diff buffer we skip assigning the language as we do not really need to perform any syntax highlighting on
95 // it. As a result, by skipping it we are potentially shaving off a lot of RSS plus we get a snappier feel for large diff
96 // view multibuffers.
97 BufferDiff::new_unchanged(&buffer_text_snapshot, cx)
98 });
99 diff.set_secondary_diff(secondary_diff);
100 diff
101 });
102
103 let multibuffer = cx.new(|cx| {
104 let mut multibuffer = MultiBuffer::without_headers(Capability::ReadOnly);
105 multibuffer.add_diff(buffer_diff.clone(), cx);
106 multibuffer
107 });
108
109 Self::Pending(PendingDiff {
110 multibuffer,
111 base_text: Arc::from(buffer_text_snapshot.text().as_str()),
112 _subscription: cx.observe(&buffer, |this, _, cx| {
113 if let Diff::Pending(diff) = this {
114 diff.update(cx);
115 }
116 }),
117 new_buffer: buffer,
118 diff: buffer_diff,
119 revealed_ranges: Vec::new(),
120 update_diff: Task::ready(Ok(())),
121 })
122 }
123
124 pub fn reveal_range(&mut self, range: Range<Anchor>, cx: &mut Context<Self>) {
125 if let Self::Pending(diff) = self {
126 diff.reveal_range(range, cx);
127 }
128 }
129
130 pub fn finalize(&mut self, cx: &mut Context<Self>) {
131 if let Self::Pending(diff) = self {
132 *self = Self::Finalized(diff.finalize(cx));
133 }
134 }
135
136 /// Returns the original text before any edits were applied.
137 pub fn base_text(&self) -> &Arc<str> {
138 match self {
139 Self::Pending(PendingDiff { base_text, .. }) => base_text,
140 Self::Finalized(FinalizedDiff { base_text, .. }) => base_text,
141 }
142 }
143
144 /// Returns the buffer being edited (for pending diffs) or the snapshot buffer (for finalized diffs).
145 pub fn buffer(&self) -> &Entity<Buffer> {
146 match self {
147 Self::Pending(PendingDiff { new_buffer, .. }) => new_buffer,
148 Self::Finalized(FinalizedDiff { new_buffer, .. }) => new_buffer,
149 }
150 }
151
152 pub fn file_path(&self, cx: &App) -> Option<String> {
153 match self {
154 Self::Pending(PendingDiff { new_buffer, .. }) => new_buffer
155 .read(cx)
156 .file()
157 .map(|file| file.full_path(cx).to_string_lossy().into_owned()),
158 Self::Finalized(FinalizedDiff { path, .. }) => Some(path.clone()),
159 }
160 }
161
162 pub fn multibuffer(&self) -> &Entity<MultiBuffer> {
163 match self {
164 Self::Pending(PendingDiff { multibuffer, .. }) => multibuffer,
165 Self::Finalized(FinalizedDiff { multibuffer, .. }) => multibuffer,
166 }
167 }
168
169 pub fn to_markdown(&self, cx: &App) -> String {
170 let buffer_text = self
171 .multibuffer()
172 .read(cx)
173 .all_buffers()
174 .iter()
175 .map(|buffer| buffer.read(cx).text())
176 .join("\n");
177 let path = match self {
178 Diff::Pending(PendingDiff {
179 new_buffer: buffer, ..
180 }) => buffer
181 .read(cx)
182 .file()
183 .map(|file| file.path().display(file.path_style(cx))),
184 Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_str().into()),
185 };
186 format!(
187 "Diff: {}\n```\n{}\n```\n",
188 path.unwrap_or("untitled".into()),
189 buffer_text
190 )
191 }
192
193 pub fn has_revealed_range(&self, cx: &App) -> bool {
194 self.multibuffer().read(cx).paths().next().is_some()
195 }
196
197 pub fn needs_update(&self, old_text: &str, new_text: &str, cx: &App) -> bool {
198 match self {
199 Diff::Pending(PendingDiff {
200 base_text,
201 new_buffer,
202 ..
203 }) => {
204 base_text.as_ref() != old_text
205 || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
206 }
207 Diff::Finalized(FinalizedDiff {
208 base_text,
209 new_buffer,
210 ..
211 }) => {
212 base_text.as_ref() != old_text
213 || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
214 }
215 }
216 }
217}
218
219pub struct PendingDiff {
220 multibuffer: Entity<MultiBuffer>,
221 base_text: Arc<str>,
222 new_buffer: Entity<Buffer>,
223 diff: Entity<BufferDiff>,
224 revealed_ranges: Vec<Range<Anchor>>,
225 _subscription: Subscription,
226 update_diff: Task<Result<()>>,
227}
228
229impl PendingDiff {
230 pub fn update(&mut self, cx: &mut Context<Diff>) {
231 let buffer = self.new_buffer.clone();
232 let buffer_diff = self.diff.clone();
233 let base_text = self.base_text.clone();
234 self.update_diff = cx.spawn(async move |diff, cx| {
235 let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
236 let language = buffer.read_with(cx, |buffer, _| buffer.language().cloned());
237 let update = buffer_diff
238 .update(cx, |diff, cx| {
239 diff.update_diff(
240 text_snapshot.clone(),
241 Some(base_text.clone()),
242 None,
243 language,
244 cx,
245 )
246 })
247 .await;
248 let (task1, task2) = buffer_diff.update(cx, |diff, cx| {
249 let task1 = diff.set_snapshot(update.clone(), &text_snapshot, cx);
250 let task2 = diff
251 .secondary_diff()
252 .unwrap()
253 .update(cx, |diff, cx| diff.set_snapshot(update, &text_snapshot, cx));
254 (task1, task2)
255 });
256 task1.await;
257 task2.await;
258 diff.update(cx, |diff, cx| {
259 if let Diff::Pending(diff) = diff {
260 diff.update_visible_ranges(cx);
261 }
262 })
263 });
264 }
265
266 pub fn reveal_range(&mut self, range: Range<Anchor>, cx: &mut Context<Diff>) {
267 self.revealed_ranges.push(range);
268 self.update_visible_ranges(cx);
269 }
270
271 fn finalize(&self, cx: &mut Context<Diff>) -> FinalizedDiff {
272 let ranges = self.excerpt_ranges(cx);
273 let base_text = self.base_text.clone();
274 let new_buffer = self.new_buffer.read(cx);
275 let language_registry = new_buffer.language_registry();
276
277 let path = new_buffer
278 .file()
279 .map(|file| file.path().display(file.path_style(cx)))
280 .unwrap_or("untitled".into())
281 .into();
282 let replica_id = new_buffer.replica_id();
283
284 // Replace the buffer in the multibuffer with the snapshot
285 let buffer = cx.new(|cx| {
286 let language = self.new_buffer.read(cx).language().cloned();
287 let buffer = TextBuffer::new_normalized(
288 replica_id,
289 cx.entity_id().as_non_zero_u64().into(),
290 self.new_buffer.read(cx).line_ending(),
291 self.new_buffer.read(cx).as_rope().clone(),
292 );
293 let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
294 buffer.set_language(language, cx);
295 buffer
296 });
297
298 let buffer_diff = cx.spawn({
299 let buffer = buffer.clone();
300 async move |_this, cx| {
301 buffer.update(cx, |buffer, _| buffer.parsing_idle()).await;
302 build_buffer_diff(base_text, &buffer, language_registry, cx).await
303 }
304 });
305
306 let update_diff = cx.spawn(async move |this, cx| {
307 let buffer_diff = buffer_diff.await?;
308 this.update(cx, |this, cx| {
309 this.multibuffer().update(cx, |multibuffer, cx| {
310 let path_key = PathKey::for_buffer(&buffer, cx);
311 multibuffer.clear(cx);
312 multibuffer.set_excerpts_for_path(
313 path_key,
314 buffer,
315 ranges,
316 excerpt_context_lines(cx),
317 cx,
318 );
319 multibuffer.add_diff(buffer_diff.clone(), cx);
320 });
321
322 cx.notify();
323 })
324 });
325
326 FinalizedDiff {
327 path,
328 base_text: self.base_text.clone(),
329 multibuffer: self.multibuffer.clone(),
330 new_buffer: self.new_buffer.clone(),
331 _update_diff: update_diff,
332 }
333 }
334
335 fn update_visible_ranges(&mut self, cx: &mut Context<Diff>) {
336 let ranges = self.excerpt_ranges(cx);
337 self.multibuffer.update(cx, |multibuffer, cx| {
338 multibuffer.set_excerpts_for_path(
339 PathKey::for_buffer(&self.new_buffer, cx),
340 self.new_buffer.clone(),
341 ranges,
342 excerpt_context_lines(cx),
343 cx,
344 );
345 let end = multibuffer.len(cx);
346 Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1)
347 });
348 cx.notify();
349 }
350
351 fn excerpt_ranges(&self, cx: &App) -> Vec<Range<Point>> {
352 let buffer = self.new_buffer.read(cx);
353 let mut ranges = self
354 .diff
355 .read(cx)
356 .snapshot(cx)
357 .hunks_intersecting_range(
358 Anchor::min_for_buffer(buffer.remote_id())
359 ..Anchor::max_for_buffer(buffer.remote_id()),
360 buffer,
361 )
362 .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
363 .collect::<Vec<_>>();
364 ranges.extend(
365 self.revealed_ranges
366 .iter()
367 .map(|range| range.to_point(buffer)),
368 );
369 ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end)));
370
371 // Merge adjacent ranges
372 let mut ranges = ranges.into_iter().peekable();
373 let mut merged_ranges = Vec::new();
374 while let Some(mut range) = ranges.next() {
375 while let Some(next_range) = ranges.peek() {
376 if range.end >= next_range.start {
377 range.end = range.end.max(next_range.end);
378 ranges.next();
379 } else {
380 break;
381 }
382 }
383
384 merged_ranges.push(range);
385 }
386 merged_ranges
387 }
388}
389
390pub struct FinalizedDiff {
391 path: String,
392 base_text: Arc<str>,
393 new_buffer: Entity<Buffer>,
394 multibuffer: Entity<MultiBuffer>,
395 _update_diff: Task<Result<()>>,
396}
397
398async fn build_buffer_diff(
399 old_text: Arc<str>,
400 buffer: &Entity<Buffer>,
401 language_registry: Option<Arc<LanguageRegistry>>,
402 cx: &mut AsyncApp,
403) -> Result<Entity<BufferDiff>> {
404 let language = cx.update(|cx| buffer.read(cx).language().cloned());
405 let text_snapshot = cx.update(|cx| buffer.read(cx).text_snapshot());
406 let buffer = cx.update(|cx| buffer.read(cx).snapshot());
407
408 let secondary_diff = cx.new(|cx| BufferDiff::new(&buffer, cx));
409
410 let update = secondary_diff
411 .update(cx, |secondary_diff, cx| {
412 secondary_diff.update_diff(
413 text_snapshot.clone(),
414 Some(old_text),
415 Some(false),
416 language.clone(),
417 cx,
418 )
419 })
420 .await;
421
422 secondary_diff
423 .update(cx, |secondary_diff, cx| {
424 secondary_diff.set_snapshot(update.clone(), &buffer, cx)
425 })
426 .await;
427
428 let diff = cx.new(|cx| BufferDiff::new(&buffer, cx));
429 diff.update(cx, |diff, cx| {
430 diff.language_changed(language, language_registry, cx);
431 diff.set_secondary_diff(secondary_diff);
432 diff.set_snapshot(update.clone(), &buffer, cx)
433 })
434 .await;
435 Ok(diff)
436}
437
438#[cfg(test)]
439mod tests {
440 use gpui::{AppContext as _, TestAppContext};
441 use language::Buffer;
442
443 use crate::Diff;
444
445 #[gpui::test]
446 async fn test_pending_diff(cx: &mut TestAppContext) {
447 let buffer = cx.new(|cx| Buffer::local("hello!", cx));
448 let _diff = cx.new(|cx| Diff::new(buffer.clone(), cx));
449 buffer.update(cx, |buffer, cx| {
450 buffer.set_text("HELLO!", cx);
451 });
452 cx.run_until_parked();
453 }
454}