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