1use anyhow::{Context as _, Result};
2use buffer_diff::{BufferDiff, BufferDiffSnapshot};
3use editor::display_map::{BlockPlacement, BlockProperties, BlockStyle};
4use editor::{Editor, EditorEvent, ExcerptRange, MultiBuffer, multibuffer_context_lines};
5use git::repository::{CommitDetails, CommitDiff, RepoPath};
6use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url};
7use gpui::{
8 AnyElement, App, AppContext as _, AsyncApp, AsyncWindowContext, Context, Element, Entity,
9 EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
10 PromptLevel, Render, Styled, Task, WeakEntity, Window, actions,
11};
12use language::{
13 Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
14 Point, ReplicaId, Rope, TextBuffer,
15};
16use multi_buffer::PathKey;
17use project::{Project, WorktreeId, git_store::Repository};
18use std::{
19 any::{Any, TypeId},
20 path::PathBuf,
21 sync::Arc,
22};
23use theme::ActiveTheme;
24use ui::{DiffStat, Tooltip, prelude::*};
25use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
26use workspace::item::TabTooltipContent;
27use workspace::{
28 Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
29 Workspace,
30 item::{BreadcrumbText, ItemEvent, TabContentParams},
31 notifications::NotifyTaskExt,
32 pane::SaveIntent,
33 searchable::SearchableItemHandle,
34};
35
36use crate::commit_tooltip::CommitAvatar;
37use crate::git_panel::GitPanel;
38
39actions!(git, [ApplyCurrentStash, PopCurrentStash, DropCurrentStash,]);
40
41pub fn init(cx: &mut App) {
42 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
43 workspace.register_action(|workspace, _: &ApplyCurrentStash, window, cx| {
44 CommitView::apply_stash(workspace, window, cx);
45 });
46 workspace.register_action(|workspace, _: &DropCurrentStash, window, cx| {
47 CommitView::remove_stash(workspace, window, cx);
48 });
49 workspace.register_action(|workspace, _: &PopCurrentStash, window, cx| {
50 CommitView::pop_stash(workspace, window, cx);
51 });
52 })
53 .detach();
54}
55
56pub struct CommitView {
57 commit: CommitDetails,
58 editor: Entity<Editor>,
59 stash: Option<usize>,
60 multibuffer: Entity<MultiBuffer>,
61 repository: Entity<Repository>,
62 remote: Option<GitRemote>,
63}
64
65struct GitBlob {
66 path: RepoPath,
67 worktree_id: WorktreeId,
68 is_deleted: bool,
69 display_name: Arc<str>,
70}
71
72const COMMIT_MESSAGE_SORT_PREFIX: u64 = 0;
73const FILE_NAMESPACE_SORT_PREFIX: u64 = 1;
74
75impl CommitView {
76 pub fn open(
77 commit_sha: String,
78 repo: WeakEntity<Repository>,
79 workspace: WeakEntity<Workspace>,
80 stash: Option<usize>,
81 file_filter: Option<RepoPath>,
82 window: &mut Window,
83 cx: &mut App,
84 ) {
85 let commit_diff = repo
86 .update(cx, |repo, _| repo.load_commit_diff(commit_sha.clone()))
87 .ok();
88 let commit_details = repo
89 .update(cx, |repo, _| repo.show(commit_sha.clone()))
90 .ok();
91
92 window
93 .spawn(cx, async move |cx| {
94 let (commit_diff, commit_details) = futures::join!(commit_diff?, commit_details?);
95 let mut commit_diff = commit_diff.log_err()?.log_err()?;
96 let commit_details = commit_details.log_err()?.log_err()?;
97
98 // Filter to specific file if requested
99 if let Some(ref filter_path) = file_filter {
100 commit_diff.files.retain(|f| &f.path == filter_path);
101 }
102
103 let repo = repo.upgrade()?;
104
105 workspace
106 .update_in(cx, |workspace, window, cx| {
107 let project = workspace.project();
108 let commit_view = cx.new(|cx| {
109 CommitView::new(
110 commit_details,
111 commit_diff,
112 repo,
113 project.clone(),
114 stash,
115 window,
116 cx,
117 )
118 });
119
120 let pane = workspace.active_pane();
121 pane.update(cx, |pane, cx| {
122 let ix = pane.items().position(|item| {
123 let commit_view = item.downcast::<CommitView>();
124 commit_view
125 .is_some_and(|view| view.read(cx).commit.sha == commit_sha)
126 });
127 if let Some(ix) = ix {
128 pane.activate_item(ix, true, true, window, cx);
129 } else {
130 pane.add_item(Box::new(commit_view), true, true, None, window, cx);
131 }
132 })
133 })
134 .log_err()
135 })
136 .detach();
137 }
138
139 fn new(
140 commit: CommitDetails,
141 commit_diff: CommitDiff,
142 repository: Entity<Repository>,
143 project: Entity<Project>,
144 stash: Option<usize>,
145 window: &mut Window,
146 cx: &mut Context<Self>,
147 ) -> Self {
148 let language_registry = project.read(cx).languages().clone();
149 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadOnly));
150
151 let message_buffer = cx.new(|cx| {
152 let mut buffer = Buffer::local(commit.message.clone(), cx);
153 buffer.set_capability(Capability::ReadOnly, cx);
154 buffer
155 });
156
157 multibuffer.update(cx, |multibuffer, cx| {
158 let snapshot = message_buffer.read(cx).snapshot();
159 let full_range = Point::zero()..snapshot.max_point();
160 let range = ExcerptRange {
161 context: full_range.clone(),
162 primary: full_range,
163 };
164 multibuffer.set_excerpt_ranges_for_path(
165 PathKey::with_sort_prefix(
166 COMMIT_MESSAGE_SORT_PREFIX,
167 RelPath::unix("commit message").unwrap().into(),
168 ),
169 message_buffer.clone(),
170 &snapshot,
171 vec![range],
172 cx,
173 )
174 });
175
176 let editor = cx.new(|cx| {
177 let mut editor =
178 Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
179
180 editor.disable_inline_diagnostics();
181 editor.set_show_breakpoints(false, cx);
182 editor.set_expand_all_diff_hunks(cx);
183 editor.disable_header_for_buffer(message_buffer.read(cx).remote_id(), cx);
184 editor.disable_indent_guides_for_buffer(message_buffer.read(cx).remote_id(), cx);
185
186 editor.insert_blocks(
187 [BlockProperties {
188 placement: BlockPlacement::Above(editor::Anchor::min()),
189 height: Some(1),
190 style: BlockStyle::Sticky,
191 render: Arc::new(|_| gpui::Empty.into_any_element()),
192 priority: 0,
193 }]
194 .into_iter()
195 .chain(
196 editor
197 .buffer()
198 .read(cx)
199 .buffer_anchor_to_anchor(&message_buffer, Anchor::MAX, cx)
200 .map(|anchor| BlockProperties {
201 placement: BlockPlacement::Below(anchor),
202 height: Some(1),
203 style: BlockStyle::Sticky,
204 render: Arc::new(|_| gpui::Empty.into_any_element()),
205 priority: 0,
206 }),
207 ),
208 None,
209 cx,
210 );
211
212 editor
213 });
214
215 let commit_sha = Arc::<str>::from(commit.sha.as_ref());
216
217 let first_worktree_id = project
218 .read(cx)
219 .worktrees(cx)
220 .next()
221 .map(|worktree| worktree.read(cx).id());
222
223 let repository_clone = repository.clone();
224
225 cx.spawn(async move |this, cx| {
226 for file in commit_diff.files {
227 let is_deleted = file.new_text.is_none();
228 let new_text = file.new_text.unwrap_or_default();
229 let old_text = file.old_text;
230 let worktree_id = repository_clone
231 .update(cx, |repository, cx| {
232 repository
233 .repo_path_to_project_path(&file.path, cx)
234 .map(|path| path.worktree_id)
235 .or(first_worktree_id)
236 })?
237 .context("project has no worktrees")?;
238 let short_sha = commit_sha.get(0..7).unwrap_or(&commit_sha);
239 let file_name = file
240 .path
241 .file_name()
242 .map(|name| name.to_string())
243 .unwrap_or_else(|| file.path.display(PathStyle::Posix).to_string());
244 let display_name: Arc<str> =
245 Arc::from(format!("{short_sha} - {file_name}").into_boxed_str());
246
247 let file = Arc::new(GitBlob {
248 path: file.path.clone(),
249 is_deleted,
250 worktree_id,
251 display_name,
252 }) as Arc<dyn language::File>;
253
254 let buffer = build_buffer(new_text, file, &language_registry, cx).await?;
255 let buffer_diff =
256 build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
257
258 this.update(cx, |this, cx| {
259 this.multibuffer.update(cx, |multibuffer, cx| {
260 let snapshot = buffer.read(cx).snapshot();
261 let path = snapshot.file().unwrap().path().clone();
262 let excerpt_ranges = {
263 let mut hunks = buffer_diff.read(cx).hunks(&snapshot, cx).peekable();
264 if hunks.peek().is_none() {
265 vec![language::Point::zero()..snapshot.max_point()]
266 } else {
267 hunks
268 .map(|hunk| hunk.buffer_range.to_point(&snapshot))
269 .collect::<Vec<_>>()
270 }
271 };
272
273 let _is_newly_added = multibuffer.set_excerpts_for_path(
274 PathKey::with_sort_prefix(FILE_NAMESPACE_SORT_PREFIX, path),
275 buffer,
276 excerpt_ranges,
277 multibuffer_context_lines(cx),
278 cx,
279 );
280 multibuffer.add_diff(buffer_diff, cx);
281 });
282 })?;
283 }
284
285 anyhow::Ok(())
286 })
287 .detach();
288
289 let snapshot = repository.read(cx).snapshot();
290 let remote_url = snapshot
291 .remote_upstream_url
292 .as_ref()
293 .or(snapshot.remote_origin_url.as_ref());
294
295 let remote = remote_url.and_then(|url| {
296 let provider_registry = GitHostingProviderRegistry::default_global(cx);
297 parse_git_remote_url(provider_registry, url).map(|(host, parsed)| GitRemote {
298 host,
299 owner: parsed.owner.into(),
300 repo: parsed.repo.into(),
301 })
302 });
303
304 Self {
305 commit,
306 editor,
307 multibuffer,
308 stash,
309 repository,
310 remote,
311 }
312 }
313
314 fn render_commit_avatar(
315 &self,
316 sha: &SharedString,
317 size: impl Into<gpui::AbsoluteLength>,
318 window: &mut Window,
319 cx: &mut App,
320 ) -> AnyElement {
321 let size = size.into();
322 let avatar = CommitAvatar::new(sha, self.remote.as_ref());
323
324 v_flex()
325 .w(size)
326 .h(size)
327 .border_1()
328 .border_color(cx.theme().colors().border)
329 .rounded_full()
330 .justify_center()
331 .items_center()
332 .child(
333 avatar
334 .avatar(window, cx)
335 .map(|a| a.size(size).into_any_element())
336 .unwrap_or_else(|| {
337 Icon::new(IconName::Person)
338 .color(Color::Muted)
339 .size(IconSize::Medium)
340 .into_any_element()
341 }),
342 )
343 .into_any()
344 }
345
346 fn calculate_changed_lines(&self, cx: &App) -> (u32, u32) {
347 let snapshot = self.multibuffer.read(cx).snapshot(cx);
348 let mut total_additions = 0u32;
349 let mut total_deletions = 0u32;
350
351 let mut seen_buffers = std::collections::HashSet::new();
352 for (_, buffer, _) in snapshot.excerpts() {
353 let buffer_id = buffer.remote_id();
354 if !seen_buffers.insert(buffer_id) {
355 continue;
356 }
357
358 let Some(diff) = snapshot.diff_for_buffer_id(buffer_id) else {
359 continue;
360 };
361
362 let base_text = diff.base_text();
363
364 for hunk in diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer) {
365 let added_rows = hunk.range.end.row.saturating_sub(hunk.range.start.row);
366 total_additions += added_rows;
367
368 let base_start = base_text
369 .offset_to_point(hunk.diff_base_byte_range.start)
370 .row;
371 let base_end = base_text.offset_to_point(hunk.diff_base_byte_range.end).row;
372 let deleted_rows = base_end.saturating_sub(base_start);
373
374 total_deletions += deleted_rows;
375 }
376 }
377
378 (total_additions, total_deletions)
379 }
380
381 fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
382 let commit = &self.commit;
383 let author_name = commit.author_name.clone();
384 let commit_date = time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)
385 .unwrap_or_else(|_| time::OffsetDateTime::now_utc());
386 let local_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
387 let date_string = time_format::format_localized_timestamp(
388 commit_date,
389 time::OffsetDateTime::now_utc(),
390 local_offset,
391 time_format::TimestampFormat::MediumAbsolute,
392 );
393
394 let remote_info = self.remote.as_ref().map(|remote| {
395 let provider = remote.host.name();
396 let url = format!(
397 "{}/{}/{}/commit/{}",
398 remote.host.base_url(),
399 remote.owner,
400 remote.repo,
401 commit.sha
402 );
403 (provider, url)
404 });
405
406 let (additions, deletions) = self.calculate_changed_lines(cx);
407
408 let commit_diff_stat = if additions > 0 || deletions > 0 {
409 Some(DiffStat::new(
410 "commit-diff-stat",
411 additions as usize,
412 deletions as usize,
413 ))
414 } else {
415 None
416 };
417
418 let gutter_width = self.editor.update(cx, |editor, cx| {
419 let snapshot = editor.snapshot(window, cx);
420 let style = editor.style(cx);
421 let font_id = window.text_system().resolve_font(&style.text.font());
422 let font_size = style.text.font_size.to_pixels(window.rem_size());
423 snapshot
424 .gutter_dimensions(font_id, font_size, style, window, cx)
425 .full_width()
426 });
427
428 h_flex()
429 .border_b_1()
430 .border_color(cx.theme().colors().border_variant)
431 .w_full()
432 .child(
433 h_flex()
434 .w(gutter_width)
435 .justify_center()
436 .child(self.render_commit_avatar(&commit.sha, rems_from_px(48.), window, cx)),
437 )
438 .child(
439 h_flex()
440 .py_4()
441 .pl_1()
442 .pr_4()
443 .w_full()
444 .items_start()
445 .justify_between()
446 .flex_wrap()
447 .child(
448 v_flex()
449 .child(
450 h_flex()
451 .gap_1()
452 .child(Label::new(author_name).color(Color::Default))
453 .child(
454 Label::new(format!("Commit:{}", commit.sha))
455 .color(Color::Muted)
456 .size(LabelSize::Small)
457 .truncate()
458 .buffer_font(cx),
459 ),
460 )
461 .child(
462 h_flex()
463 .gap_1p5()
464 .child(
465 Label::new(date_string)
466 .color(Color::Muted)
467 .size(LabelSize::Small),
468 )
469 .child(
470 Label::new("•")
471 .color(Color::Ignored)
472 .size(LabelSize::Small),
473 )
474 .children(commit_diff_stat),
475 ),
476 )
477 .children(remote_info.map(|(provider_name, url)| {
478 let icon = match provider_name.as_str() {
479 "GitHub" => IconName::Github,
480 _ => IconName::Link,
481 };
482
483 Button::new("view_on_provider", format!("View on {}", provider_name))
484 .icon(icon)
485 .icon_color(Color::Muted)
486 .icon_size(IconSize::Small)
487 .icon_position(IconPosition::Start)
488 .on_click(move |_, _, cx| cx.open_url(&url))
489 })),
490 )
491 }
492
493 fn apply_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
494 Self::stash_action(
495 workspace,
496 "Apply",
497 window,
498 cx,
499 async move |repository, sha, stash, commit_view, workspace, cx| {
500 let result = repository.update(cx, |repo, cx| {
501 if !stash_matches_index(&sha, stash, repo) {
502 return Err(anyhow::anyhow!("Stash has changed, not applying"));
503 }
504 Ok(repo.stash_apply(Some(stash), cx))
505 })?;
506
507 match result {
508 Ok(task) => task.await?,
509 Err(err) => {
510 Self::close_commit_view(commit_view, workspace, cx).await?;
511 return Err(err);
512 }
513 };
514 Self::close_commit_view(commit_view, workspace, cx).await?;
515 anyhow::Ok(())
516 },
517 );
518 }
519
520 fn pop_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
521 Self::stash_action(
522 workspace,
523 "Pop",
524 window,
525 cx,
526 async move |repository, sha, stash, commit_view, workspace, cx| {
527 let result = repository.update(cx, |repo, cx| {
528 if !stash_matches_index(&sha, stash, repo) {
529 return Err(anyhow::anyhow!("Stash has changed, pop aborted"));
530 }
531 Ok(repo.stash_pop(Some(stash), cx))
532 })?;
533
534 match result {
535 Ok(task) => task.await?,
536 Err(err) => {
537 Self::close_commit_view(commit_view, workspace, cx).await?;
538 return Err(err);
539 }
540 };
541 Self::close_commit_view(commit_view, workspace, cx).await?;
542 anyhow::Ok(())
543 },
544 );
545 }
546
547 fn remove_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
548 Self::stash_action(
549 workspace,
550 "Drop",
551 window,
552 cx,
553 async move |repository, sha, stash, commit_view, workspace, cx| {
554 let result = repository.update(cx, |repo, cx| {
555 if !stash_matches_index(&sha, stash, repo) {
556 return Err(anyhow::anyhow!("Stash has changed, drop aborted"));
557 }
558 Ok(repo.stash_drop(Some(stash), cx))
559 })?;
560
561 match result {
562 Ok(task) => task.await??,
563 Err(err) => {
564 Self::close_commit_view(commit_view, workspace, cx).await?;
565 return Err(err);
566 }
567 };
568 Self::close_commit_view(commit_view, workspace, cx).await?;
569 anyhow::Ok(())
570 },
571 );
572 }
573
574 fn stash_action<AsyncFn>(
575 workspace: &mut Workspace,
576 str_action: &str,
577 window: &mut Window,
578 cx: &mut App,
579 callback: AsyncFn,
580 ) where
581 AsyncFn: AsyncFnOnce(
582 Entity<Repository>,
583 &SharedString,
584 usize,
585 Entity<CommitView>,
586 WeakEntity<Workspace>,
587 &mut AsyncWindowContext,
588 ) -> anyhow::Result<()>
589 + 'static,
590 {
591 let Some(commit_view) = workspace.active_item_as::<CommitView>(cx) else {
592 return;
593 };
594 let Some(stash) = commit_view.read(cx).stash else {
595 return;
596 };
597 let sha = commit_view.read(cx).commit.sha.clone();
598 let answer = window.prompt(
599 PromptLevel::Info,
600 &format!("{} stash@{{{}}}?", str_action, stash),
601 None,
602 &[str_action, "Cancel"],
603 cx,
604 );
605
606 let workspace_weak = workspace.weak_handle();
607 let commit_view_entity = commit_view;
608
609 window
610 .spawn(cx, async move |cx| {
611 if answer.await != Ok(0) {
612 return anyhow::Ok(());
613 }
614
615 let Some(workspace) = workspace_weak.upgrade() else {
616 return Ok(());
617 };
618
619 let repo = workspace.update(cx, |workspace, cx| {
620 workspace
621 .panel::<GitPanel>(cx)
622 .and_then(|p| p.read(cx).active_repository.clone())
623 })?;
624
625 let Some(repo) = repo else {
626 return Ok(());
627 };
628
629 callback(repo, &sha, stash, commit_view_entity, workspace_weak, cx).await?;
630 anyhow::Ok(())
631 })
632 .detach_and_notify_err(window, cx);
633 }
634
635 async fn close_commit_view(
636 commit_view: Entity<CommitView>,
637 workspace: WeakEntity<Workspace>,
638 cx: &mut AsyncWindowContext,
639 ) -> anyhow::Result<()> {
640 workspace
641 .update_in(cx, |workspace, window, cx| {
642 let active_pane = workspace.active_pane();
643 let commit_view_id = commit_view.entity_id();
644 active_pane.update(cx, |pane, cx| {
645 pane.close_item_by_id(commit_view_id, SaveIntent::Skip, window, cx)
646 })
647 })?
648 .await?;
649 anyhow::Ok(())
650 }
651}
652
653impl language::File for GitBlob {
654 fn as_local(&self) -> Option<&dyn language::LocalFile> {
655 None
656 }
657
658 fn disk_state(&self) -> DiskState {
659 if self.is_deleted {
660 DiskState::Deleted
661 } else {
662 DiskState::New
663 }
664 }
665
666 fn path_style(&self, _: &App) -> PathStyle {
667 PathStyle::Posix
668 }
669
670 fn path(&self) -> &Arc<RelPath> {
671 self.path.as_ref()
672 }
673
674 fn full_path(&self, _: &App) -> PathBuf {
675 self.path.as_std_path().to_path_buf()
676 }
677
678 fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
679 self.display_name.as_ref()
680 }
681
682 fn worktree_id(&self, _: &App) -> WorktreeId {
683 self.worktree_id
684 }
685
686 fn to_proto(&self, _cx: &App) -> language::proto::File {
687 unimplemented!()
688 }
689
690 fn is_private(&self) -> bool {
691 false
692 }
693}
694
695// No longer needed since metadata buffer is not created
696// impl language::File for CommitMetadataFile {
697// fn as_local(&self) -> Option<&dyn language::LocalFile> {
698// None
699// }
700//
701// fn disk_state(&self) -> DiskState {
702// DiskState::New
703// }
704//
705// fn path_style(&self, _: &App) -> PathStyle {
706// PathStyle::Posix
707// }
708//
709// fn path(&self) -> &Arc<RelPath> {
710// &self.title
711// }
712//
713// fn full_path(&self, _: &App) -> PathBuf {
714// self.title.as_std_path().to_path_buf()
715// }
716//
717// fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
718// self.title.file_name().unwrap_or("commit")
719// }
720//
721// fn worktree_id(&self, _: &App) -> WorktreeId {
722// self.worktree_id
723// }
724//
725// fn to_proto(&self, _cx: &App) -> language::proto::File {
726// unimplemented!()
727// }
728//
729// fn is_private(&self) -> bool {
730// false
731// }
732// }
733
734async fn build_buffer(
735 mut text: String,
736 blob: Arc<dyn File>,
737 language_registry: &Arc<language::LanguageRegistry>,
738 cx: &mut AsyncApp,
739) -> Result<Entity<Buffer>> {
740 let line_ending = LineEnding::detect(&text);
741 LineEnding::normalize(&mut text);
742 let text = Rope::from(text);
743 let language = cx.update(|cx| language_registry.language_for_file(&blob, Some(&text), cx))?;
744 let language = if let Some(language) = language {
745 language_registry
746 .load_language(&language)
747 .await
748 .ok()
749 .and_then(|e| e.log_err())
750 } else {
751 None
752 };
753 let buffer = cx.new(|cx| {
754 let buffer = TextBuffer::new_normalized(
755 ReplicaId::LOCAL,
756 cx.entity_id().as_non_zero_u64().into(),
757 line_ending,
758 text,
759 );
760 let mut buffer = Buffer::build(buffer, Some(blob), Capability::ReadWrite);
761 buffer.set_language_async(language, cx);
762 buffer
763 })?;
764 Ok(buffer)
765}
766
767async fn build_buffer_diff(
768 mut old_text: Option<String>,
769 buffer: &Entity<Buffer>,
770 language_registry: &Arc<LanguageRegistry>,
771 cx: &mut AsyncApp,
772) -> Result<Entity<BufferDiff>> {
773 if let Some(old_text) = &mut old_text {
774 LineEnding::normalize(old_text);
775 }
776
777 let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
778
779 let base_buffer = cx
780 .update(|cx| {
781 Buffer::build_snapshot(
782 old_text.as_deref().unwrap_or("").into(),
783 buffer.language().cloned(),
784 Some(language_registry.clone()),
785 cx,
786 )
787 })?
788 .await;
789
790 let diff_snapshot = cx
791 .update(|cx| {
792 BufferDiffSnapshot::new_with_base_buffer(
793 buffer.text.clone(),
794 old_text.map(Arc::new),
795 base_buffer,
796 cx,
797 )
798 })?
799 .await;
800
801 cx.new(|cx| {
802 let mut diff = BufferDiff::new(&buffer.text, cx);
803 diff.set_snapshot(diff_snapshot, &buffer.text, cx);
804 diff
805 })
806}
807
808impl EventEmitter<EditorEvent> for CommitView {}
809
810impl Focusable for CommitView {
811 fn focus_handle(&self, cx: &App) -> FocusHandle {
812 self.editor.focus_handle(cx)
813 }
814}
815
816impl Item for CommitView {
817 type Event = EditorEvent;
818
819 fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
820 Some(Icon::new(IconName::GitBranch).color(Color::Muted))
821 }
822
823 fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
824 Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx))
825 .color(if params.selected {
826 Color::Default
827 } else {
828 Color::Muted
829 })
830 .into_any_element()
831 }
832
833 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
834 let short_sha = self.commit.sha.get(0..7).unwrap_or(&*self.commit.sha);
835 let subject = truncate_and_trailoff(self.commit.message.split('\n').next().unwrap(), 20);
836 format!("{short_sha} — {subject}").into()
837 }
838
839 fn tab_tooltip_content(&self, _: &App) -> Option<TabTooltipContent> {
840 let short_sha = self.commit.sha.get(0..16).unwrap_or(&*self.commit.sha);
841 let subject = self.commit.message.split('\n').next().unwrap();
842
843 Some(TabTooltipContent::Custom(Box::new(Tooltip::element({
844 let subject = subject.to_string();
845 let short_sha = short_sha.to_string();
846
847 move |_, _| {
848 v_flex()
849 .child(Label::new(subject.clone()))
850 .child(
851 Label::new(short_sha.clone())
852 .color(Color::Muted)
853 .size(LabelSize::Small),
854 )
855 .into_any_element()
856 }
857 }))))
858 }
859
860 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
861 Editor::to_item_events(event, f)
862 }
863
864 fn telemetry_event_text(&self) -> Option<&'static str> {
865 Some("Commit View Opened")
866 }
867
868 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
869 self.editor
870 .update(cx, |editor, cx| editor.deactivated(window, cx));
871 }
872
873 fn act_as_type<'a>(
874 &'a self,
875 type_id: TypeId,
876 self_handle: &'a Entity<Self>,
877 _: &'a App,
878 ) -> Option<gpui::AnyEntity> {
879 if type_id == TypeId::of::<Self>() {
880 Some(self_handle.clone().into())
881 } else if type_id == TypeId::of::<Editor>() {
882 Some(self.editor.clone().into())
883 } else {
884 None
885 }
886 }
887
888 fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
889 Some(Box::new(self.editor.clone()))
890 }
891
892 fn for_each_project_item(
893 &self,
894 cx: &App,
895 f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
896 ) {
897 self.editor.for_each_project_item(cx, f)
898 }
899
900 fn set_nav_history(
901 &mut self,
902 nav_history: ItemNavHistory,
903 _: &mut Window,
904 cx: &mut Context<Self>,
905 ) {
906 self.editor.update(cx, |editor, _| {
907 editor.set_nav_history(Some(nav_history));
908 });
909 }
910
911 fn navigate(
912 &mut self,
913 data: Box<dyn Any>,
914 window: &mut Window,
915 cx: &mut Context<Self>,
916 ) -> bool {
917 self.editor
918 .update(cx, |editor, cx| editor.navigate(data, window, cx))
919 }
920
921 fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
922 ToolbarItemLocation::Hidden
923 }
924
925 fn breadcrumbs(&self, _theme: &theme::Theme, _cx: &App) -> Option<Vec<BreadcrumbText>> {
926 None
927 }
928
929 fn added_to_workspace(
930 &mut self,
931 workspace: &mut Workspace,
932 window: &mut Window,
933 cx: &mut Context<Self>,
934 ) {
935 self.editor.update(cx, |editor, cx| {
936 editor.added_to_workspace(workspace, window, cx)
937 });
938 }
939
940 fn can_split(&self) -> bool {
941 true
942 }
943
944 fn clone_on_split(
945 &self,
946 _workspace_id: Option<workspace::WorkspaceId>,
947 window: &mut Window,
948 cx: &mut Context<Self>,
949 ) -> Task<Option<Entity<Self>>>
950 where
951 Self: Sized,
952 {
953 Task::ready(Some(cx.new(|cx| {
954 let editor = cx.new(|cx| {
955 self.editor
956 .update(cx, |editor, cx| editor.clone(window, cx))
957 });
958 let multibuffer = editor.read(cx).buffer().clone();
959 Self {
960 editor,
961 multibuffer,
962 commit: self.commit.clone(),
963 stash: self.stash,
964 repository: self.repository.clone(),
965 remote: self.remote.clone(),
966 }
967 })))
968 }
969}
970
971impl Render for CommitView {
972 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
973 let is_stash = self.stash.is_some();
974
975 v_flex()
976 .key_context(if is_stash { "StashDiff" } else { "CommitDiff" })
977 .size_full()
978 .bg(cx.theme().colors().editor_background)
979 .child(self.render_header(window, cx))
980 .when(!self.editor.read(cx).is_empty(cx), |this| {
981 this.child(div().flex_grow().child(self.editor.clone()))
982 })
983 }
984}
985
986pub struct CommitViewToolbar {
987 commit_view: Option<WeakEntity<CommitView>>,
988}
989
990impl CommitViewToolbar {
991 pub fn new() -> Self {
992 Self { commit_view: None }
993 }
994}
995
996impl EventEmitter<ToolbarItemEvent> for CommitViewToolbar {}
997
998impl Render for CommitViewToolbar {
999 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1000 div().hidden()
1001 }
1002}
1003
1004impl ToolbarItemView for CommitViewToolbar {
1005 fn set_active_pane_item(
1006 &mut self,
1007 active_pane_item: Option<&dyn ItemHandle>,
1008 _: &mut Window,
1009 cx: &mut Context<Self>,
1010 ) -> ToolbarItemLocation {
1011 if let Some(entity) = active_pane_item.and_then(|i| i.act_as::<CommitView>(cx))
1012 && entity.read(cx).stash.is_some()
1013 {
1014 self.commit_view = Some(entity.downgrade());
1015 return ToolbarItemLocation::PrimaryRight;
1016 }
1017 ToolbarItemLocation::Hidden
1018 }
1019
1020 fn pane_focus_update(
1021 &mut self,
1022 _pane_focused: bool,
1023 _window: &mut Window,
1024 _cx: &mut Context<Self>,
1025 ) {
1026 }
1027}
1028
1029fn stash_matches_index(sha: &str, stash_index: usize, repo: &Repository) -> bool {
1030 repo.stash_entries
1031 .entries
1032 .get(stash_index)
1033 .map(|entry| entry.oid.to_string() == sha)
1034 .unwrap_or(false)
1035}