multi_diff_view.rs

  1use anyhow::Result;
  2use buffer_diff::BufferDiff;
  3use editor::{Editor, EditorEvent, MultiBuffer, multibuffer_context_lines};
  4use gpui::{
  5    AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
  6    Focusable, IntoElement, Render, SharedString, Task, Window,
  7};
  8use language::{Buffer, Capability, OffsetRangeExt};
  9use multi_buffer::PathKey;
 10use project::Project;
 11use std::{
 12    any::{Any, TypeId},
 13    path::{Path, PathBuf},
 14    sync::Arc,
 15};
 16use theme;
 17use ui::{Color, Icon, IconName, Label, LabelCommon as _};
 18use util::paths::PathStyle;
 19use util::rel_path::RelPath;
 20use workspace::{
 21    Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
 22    item::{BreadcrumbText, ItemEvent, SaveOptions, TabContentParams},
 23    searchable::SearchableItemHandle,
 24};
 25
 26pub struct MultiDiffView {
 27    editor: Entity<Editor>,
 28    file_count: usize,
 29}
 30
 31struct Entry {
 32    index: usize,
 33    new_path: PathBuf,
 34    new_buffer: Entity<Buffer>,
 35    diff: Entity<BufferDiff>,
 36}
 37
 38async fn load_entries(
 39    diff_pairs: Vec<[String; 2]>,
 40    project: &Entity<Project>,
 41    cx: &mut AsyncApp,
 42) -> Result<(Vec<Entry>, Option<PathBuf>)> {
 43    let mut entries = Vec::with_capacity(diff_pairs.len());
 44    let mut all_paths = Vec::with_capacity(diff_pairs.len());
 45
 46    for (ix, pair) in diff_pairs.into_iter().enumerate() {
 47        let old_path = PathBuf::from(&pair[0]);
 48        let new_path = PathBuf::from(&pair[1]);
 49
 50        let old_buffer = project
 51            .update(cx, |project, cx| project.open_local_buffer(&old_path, cx))
 52            .await?;
 53        let new_buffer = project
 54            .update(cx, |project, cx| project.open_local_buffer(&new_path, cx))
 55            .await?;
 56
 57        let diff = build_buffer_diff(&old_buffer, &new_buffer, cx).await?;
 58
 59        all_paths.push(new_path.clone());
 60        entries.push(Entry {
 61            index: ix,
 62            new_path,
 63            new_buffer: new_buffer.clone(),
 64            diff,
 65        });
 66    }
 67
 68    let common_root = common_prefix(&all_paths);
 69    Ok((entries, common_root))
 70}
 71
 72fn register_entry(
 73    multibuffer: &Entity<MultiBuffer>,
 74    entry: Entry,
 75    common_root: &Option<PathBuf>,
 76    context_lines: u32,
 77    cx: &mut Context<Workspace>,
 78) {
 79    let snapshot = entry.new_buffer.read(cx).snapshot();
 80    let diff_snapshot = entry.diff.read(cx).snapshot(cx);
 81
 82    let ranges: Vec<std::ops::Range<language::Point>> = diff_snapshot
 83        .hunks(&snapshot)
 84        .map(|hunk| hunk.buffer_range.to_point(&snapshot))
 85        .collect();
 86
 87    let display_rel = common_root
 88        .as_ref()
 89        .and_then(|root| entry.new_path.strip_prefix(root).ok())
 90        .map(|rel| {
 91            RelPath::new(rel, PathStyle::local())
 92                .map(|r| r.into_owned().into())
 93                .unwrap_or_else(|_| {
 94                    RelPath::new(Path::new("untitled"), PathStyle::Posix)
 95                        .unwrap()
 96                        .into_owned()
 97                        .into()
 98                })
 99        })
100        .unwrap_or_else(|| {
101            entry
102                .new_path
103                .file_name()
104                .and_then(|n| n.to_str())
105                .and_then(|s| RelPath::new(Path::new(s), PathStyle::Posix).ok())
106                .map(|r| r.into_owned().into())
107                .unwrap_or_else(|| {
108                    RelPath::new(Path::new("untitled"), PathStyle::Posix)
109                        .unwrap()
110                        .into_owned()
111                        .into()
112                })
113        });
114
115    let path_key = PathKey::with_sort_prefix(entry.index as u64, display_rel);
116
117    multibuffer.update(cx, |multibuffer, cx| {
118        multibuffer.set_excerpts_for_path(
119            path_key,
120            entry.new_buffer.clone(),
121            ranges,
122            context_lines,
123            cx,
124        );
125        multibuffer.add_diff(entry.diff.clone(), cx);
126    });
127}
128
129fn common_prefix(paths: &[PathBuf]) -> Option<PathBuf> {
130    let mut iter = paths.iter();
131    let mut prefix = iter.next()?.clone();
132
133    for path in iter {
134        while !path.starts_with(&prefix) {
135            if !prefix.pop() {
136                return Some(PathBuf::new());
137            }
138        }
139    }
140
141    Some(prefix)
142}
143
144async fn build_buffer_diff(
145    old_buffer: &Entity<Buffer>,
146    new_buffer: &Entity<Buffer>,
147    cx: &mut AsyncApp,
148) -> Result<Entity<BufferDiff>> {
149    let old_buffer_snapshot = old_buffer.read_with(cx, |buffer, _| buffer.snapshot());
150    let new_buffer_snapshot = new_buffer.read_with(cx, |buffer, _| buffer.snapshot());
151
152    let diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot.text, cx));
153
154    let update = diff
155        .update(cx, |diff, cx| {
156            diff.update_diff(
157                new_buffer_snapshot.text.clone(),
158                Some(old_buffer_snapshot.text().into()),
159                Some(true),
160                new_buffer_snapshot.language().cloned(),
161                cx,
162            )
163        })
164        .await;
165
166    diff.update(cx, |diff, cx| {
167        diff.set_snapshot(update, &new_buffer_snapshot.text, cx)
168    })
169    .await;
170
171    Ok(diff)
172}
173
174impl MultiDiffView {
175    pub fn open(
176        diff_pairs: Vec<[String; 2]>,
177        workspace: &Workspace,
178        window: &mut Window,
179        cx: &mut App,
180    ) -> Task<Result<Entity<Self>>> {
181        let project = workspace.project().clone();
182        let workspace = workspace.weak_handle();
183        let context_lines = multibuffer_context_lines(cx);
184
185        window.spawn(cx, async move |cx| {
186            let (entries, common_root) = load_entries(diff_pairs, &project, cx).await?;
187
188            workspace.update_in(cx, |workspace, window, cx| {
189                let multibuffer = cx.new(|cx| {
190                    let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
191                    multibuffer.set_all_diff_hunks_expanded(cx);
192                    multibuffer
193                });
194
195                let file_count = entries.len();
196                for entry in entries {
197                    register_entry(&multibuffer, entry, &common_root, context_lines, cx);
198                }
199
200                let diff_view = cx.new(|cx| {
201                    Self::new(multibuffer.clone(), project.clone(), file_count, window, cx)
202                });
203
204                let pane = workspace.active_pane();
205                pane.update(cx, |pane, cx| {
206                    pane.add_item(Box::new(diff_view.clone()), true, true, None, window, cx);
207                });
208
209                // Hide the left dock (file explorer) for a cleaner diff view
210                workspace.left_dock().update(cx, |dock, cx| {
211                    dock.set_open(false, window, cx);
212                });
213
214                diff_view
215            })
216        })
217    }
218
219    fn new(
220        multibuffer: Entity<MultiBuffer>,
221        project: Entity<Project>,
222        file_count: usize,
223        window: &mut Window,
224        cx: &mut Context<Self>,
225    ) -> Self {
226        let editor = cx.new(|cx| {
227            let mut editor =
228                Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx);
229            editor.start_temporary_diff_override();
230            editor.disable_diagnostics(cx);
231            editor.set_expand_all_diff_hunks(cx);
232            editor.set_render_diff_hunk_controls(
233                Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),
234                cx,
235            );
236            editor
237        });
238
239        Self { editor, file_count }
240    }
241
242    fn title(&self) -> SharedString {
243        let suffix = if self.file_count == 1 {
244            "1 file".to_string()
245        } else {
246            format!("{} files", self.file_count)
247        };
248        format!("Diff ({suffix})").into()
249    }
250}
251
252impl EventEmitter<EditorEvent> for MultiDiffView {}
253
254impl Focusable for MultiDiffView {
255    fn focus_handle(&self, cx: &App) -> FocusHandle {
256        self.editor.focus_handle(cx)
257    }
258}
259
260impl Item for MultiDiffView {
261    type Event = EditorEvent;
262
263    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
264        Some(Icon::new(IconName::Diff).color(Color::Muted))
265    }
266
267    fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement {
268        Label::new(self.title())
269            .color(if params.selected {
270                Color::Default
271            } else {
272                Color::Muted
273            })
274            .into_any_element()
275    }
276
277    fn tab_tooltip_text(&self, _cx: &App) -> Option<ui::SharedString> {
278        Some(self.title())
279    }
280
281    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
282        self.title()
283    }
284
285    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
286        Editor::to_item_events(event, f)
287    }
288
289    fn telemetry_event_text(&self) -> Option<&'static str> {
290        Some("Diff View Opened")
291    }
292
293    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
294        self.editor
295            .update(cx, |editor, cx| editor.deactivated(window, cx));
296    }
297
298    fn act_as_type<'a>(
299        &'a self,
300        type_id: TypeId,
301        self_handle: &'a Entity<Self>,
302        _: &'a App,
303    ) -> Option<gpui::AnyEntity> {
304        if type_id == TypeId::of::<Self>() {
305            Some(self_handle.clone().into())
306        } else if type_id == TypeId::of::<Editor>() {
307            Some(self.editor.clone().into())
308        } else {
309            None
310        }
311    }
312
313    fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
314        Some(Box::new(self.editor.clone()))
315    }
316
317    fn set_nav_history(
318        &mut self,
319        nav_history: ItemNavHistory,
320        _: &mut Window,
321        cx: &mut Context<Self>,
322    ) {
323        self.editor.update(cx, |editor, _| {
324            editor.set_nav_history(Some(nav_history));
325        });
326    }
327
328    fn navigate(
329        &mut self,
330        data: Arc<dyn Any + Send>,
331        window: &mut Window,
332        cx: &mut Context<Self>,
333    ) -> bool {
334        self.editor
335            .update(cx, |editor, cx| editor.navigate(data, window, cx))
336    }
337
338    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
339        ToolbarItemLocation::PrimaryLeft
340    }
341
342    fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
343        self.editor.breadcrumbs(theme, cx)
344    }
345
346    fn added_to_workspace(
347        &mut self,
348        workspace: &mut Workspace,
349        window: &mut Window,
350        cx: &mut Context<Self>,
351    ) {
352        self.editor.update(cx, |editor, cx| {
353            editor.added_to_workspace(workspace, window, cx)
354        });
355    }
356
357    fn can_save(&self, cx: &App) -> bool {
358        self.editor.read(cx).can_save(cx)
359    }
360
361    fn save(
362        &mut self,
363        options: SaveOptions,
364        project: Entity<Project>,
365        window: &mut Window,
366        cx: &mut Context<Self>,
367    ) -> gpui::Task<Result<()>> {
368        self.editor
369            .update(cx, |editor, cx| editor.save(options, project, window, cx))
370    }
371}
372
373impl Render for MultiDiffView {
374    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
375        self.editor.clone()
376    }
377}