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