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}