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}