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