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