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