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