Files
oak-gpui/crates/git_ui/src/commit_view.rs
T
Agus Zubiagaandcameron 8a12ecf849 commit view: Display message within editor (#44024)
#42441 moved the commit message out of the multi-buffer editor into its
own header element which looks nicer, but unfortunately can make the
view become unusable when the commit message is too long since it
doesn't scroll with the diff.

This PR maintains the metadata in its own element, but moves the commit
message back to the editor so the user can scroll past it. This does
mean that we lose markdown rendering for now, but we think this is a
good solution for the moment.


https://github.com/user-attachments/assets/d67cf22e-1a79-451a-932a-cdc8a65e43de

Release Notes:

- N/A

---------

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
2025-12-02 22:30:43 +00:00

1040 lines
33 KiB
Rust

use anyhow::{Context as _, Result};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::display_map::{BlockPlacement, BlockProperties, BlockStyle};
use editor::{Addon, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer};
use git::repository::{CommitDetails, CommitDiff, RepoPath};
use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url};
use gpui::{
AnyElement, App, AppContext as _, Asset, AsyncApp, AsyncWindowContext, Context, Element,
Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
PromptLevel, Render, Styled, Task, WeakEntity, Window, actions,
};
use language::{
Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, ReplicaId, Rope,
TextBuffer, ToPoint,
};
use multi_buffer::ExcerptInfo;
use multi_buffer::PathKey;
use project::{Project, WorktreeId, git_store::Repository};
use std::{
any::{Any, TypeId},
path::PathBuf,
sync::Arc,
};
use theme::ActiveTheme;
use ui::{
Avatar, Button, ButtonCommon, Clickable, Color, Icon, IconName, IconSize, Label,
LabelCommon as _, LabelSize, SharedString, div, h_flex, v_flex,
};
use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
use workspace::{
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
Workspace,
item::{BreadcrumbText, ItemEvent, TabContentParams},
notifications::NotifyTaskExt,
pane::SaveIntent,
searchable::SearchableItemHandle,
};
use crate::git_panel::GitPanel;
actions!(git, [ApplyCurrentStash, PopCurrentStash, DropCurrentStash,]);
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
workspace.register_action(|workspace, _: &ApplyCurrentStash, window, cx| {
CommitView::apply_stash(workspace, window, cx);
});
workspace.register_action(|workspace, _: &DropCurrentStash, window, cx| {
CommitView::remove_stash(workspace, window, cx);
});
workspace.register_action(|workspace, _: &PopCurrentStash, window, cx| {
CommitView::pop_stash(workspace, window, cx);
});
})
.detach();
}
pub struct CommitView {
commit: CommitDetails,
editor: Entity<Editor>,
stash: Option<usize>,
multibuffer: Entity<MultiBuffer>,
repository: Entity<Repository>,
remote: Option<GitRemote>,
}
struct GitBlob {
path: RepoPath,
worktree_id: WorktreeId,
is_deleted: bool,
}
const FILE_NAMESPACE_SORT_PREFIX: u64 = 1;
impl CommitView {
pub fn open(
commit_sha: String,
repo: WeakEntity<Repository>,
workspace: WeakEntity<Workspace>,
stash: Option<usize>,
file_filter: Option<RepoPath>,
window: &mut Window,
cx: &mut App,
) {
let commit_diff = repo
.update(cx, |repo, _| repo.load_commit_diff(commit_sha.clone()))
.ok();
let commit_details = repo
.update(cx, |repo, _| repo.show(commit_sha.clone()))
.ok();
window
.spawn(cx, async move |cx| {
let (commit_diff, commit_details) = futures::join!(commit_diff?, commit_details?);
let mut commit_diff = commit_diff.log_err()?.log_err()?;
let commit_details = commit_details.log_err()?.log_err()?;
// Filter to specific file if requested
if let Some(ref filter_path) = file_filter {
commit_diff.files.retain(|f| &f.path == filter_path);
}
let repo = repo.upgrade()?;
workspace
.update_in(cx, |workspace, window, cx| {
let project = workspace.project();
let commit_view = cx.new(|cx| {
CommitView::new(
commit_details,
commit_diff,
repo,
project.clone(),
stash,
window,
cx,
)
});
let pane = workspace.active_pane();
pane.update(cx, |pane, cx| {
let ix = pane.items().position(|item| {
let commit_view = item.downcast::<CommitView>();
commit_view
.is_some_and(|view| view.read(cx).commit.sha == commit_sha)
});
if let Some(ix) = ix {
pane.activate_item(ix, true, true, window, cx);
} else {
pane.add_item(Box::new(commit_view), true, true, None, window, cx);
}
})
})
.log_err()
})
.detach();
}
fn new(
commit: CommitDetails,
commit_diff: CommitDiff,
repository: Entity<Repository>,
project: Entity<Project>,
stash: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let language_registry = project.read(cx).languages().clone();
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadOnly));
let editor = cx.new(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
editor.disable_inline_diagnostics();
editor.set_expand_all_diff_hunks(cx);
editor.register_addon(CommitViewAddon {
multibuffer: multibuffer.downgrade(),
});
editor
});
let first_worktree_id = project
.read(cx)
.worktrees(cx)
.next()
.map(|worktree| worktree.read(cx).id());
let repository_clone = repository.clone();
let commit_message = commit.message.clone();
cx.spawn(async move |this, cx| {
for file in commit_diff.files {
let is_deleted = file.new_text.is_none();
let new_text = file.new_text.unwrap_or_default();
let old_text = file.old_text;
let worktree_id = repository_clone
.update(cx, |repository, cx| {
repository
.repo_path_to_project_path(&file.path, cx)
.map(|path| path.worktree_id)
.or(first_worktree_id)
})?
.context("project has no worktrees")?;
let file = Arc::new(GitBlob {
path: file.path.clone(),
is_deleted,
worktree_id,
}) as Arc<dyn language::File>;
let buffer = build_buffer(new_text, file, &language_registry, cx).await?;
let buffer_diff =
build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
this.update(cx, |this, cx| {
this.multibuffer.update(cx, |multibuffer, cx| {
let snapshot = buffer.read(cx).snapshot();
let path = snapshot.file().unwrap().path().clone();
let hunks: Vec<_> = buffer_diff.read(cx).hunks(&snapshot, cx).collect();
let excerpt_ranges = if hunks.is_empty() {
vec![language::Point::zero()..snapshot.max_point()]
} else {
hunks
.into_iter()
.map(|hunk| {
let start = hunk.range.start.max(language::Point::new(
hunk.range.start.row.saturating_sub(3),
0,
));
let end_row =
(hunk.range.end.row + 3).min(snapshot.max_point().row);
let end =
language::Point::new(end_row, snapshot.line_len(end_row));
start..end
})
.collect()
};
let _is_newly_added = multibuffer.set_excerpts_for_path(
PathKey::with_sort_prefix(FILE_NAMESPACE_SORT_PREFIX, path),
buffer,
excerpt_ranges,
0,
cx,
);
multibuffer.add_diff(buffer_diff, cx);
});
})?;
}
let message_buffer = cx.new(|cx| {
let mut buffer = Buffer::local(commit_message, cx);
buffer.set_capability(Capability::ReadOnly, cx);
buffer
})?;
this.update(cx, |this, cx| {
this.multibuffer.update(cx, |multibuffer, cx| {
let range = ExcerptRange {
context: Anchor::MIN..Anchor::MAX,
primary: Anchor::MIN..Anchor::MAX,
};
multibuffer.insert_excerpts_after(
ExcerptId::min(),
message_buffer.clone(),
[range],
cx,
)
});
this.editor.update(cx, |editor, cx| {
editor.disable_header_for_buffer(message_buffer.read(cx).remote_id(), cx);
editor.insert_blocks(
[BlockProperties {
placement: BlockPlacement::Above(editor::Anchor::min()),
height: Some(1),
style: BlockStyle::Sticky,
render: Arc::new(|_| gpui::Empty.into_any_element()),
priority: 0,
}]
.into_iter()
.chain(
editor
.buffer()
.read(cx)
.buffer_anchor_to_anchor(&message_buffer, Anchor::MAX, cx)
.map(|anchor| BlockProperties {
placement: BlockPlacement::Below(anchor),
height: Some(1),
style: BlockStyle::Sticky,
render: Arc::new(|_| gpui::Empty.into_any_element()),
priority: 0,
}),
),
None,
cx,
)
});
})?;
anyhow::Ok(())
})
.detach();
let snapshot = repository.read(cx).snapshot();
let remote_url = snapshot
.remote_upstream_url
.as_ref()
.or(snapshot.remote_origin_url.as_ref());
let remote = remote_url.and_then(|url| {
let provider_registry = GitHostingProviderRegistry::default_global(cx);
parse_git_remote_url(provider_registry, url).map(|(host, parsed)| GitRemote {
host,
owner: parsed.owner.into(),
repo: parsed.repo.into(),
})
});
Self {
commit,
editor,
multibuffer,
stash,
repository,
remote,
}
}
fn render_commit_avatar(
&self,
sha: &SharedString,
size: impl Into<gpui::AbsoluteLength>,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let size = size.into();
let remote = self.remote.as_ref().filter(|r| r.host_supports_avatars());
if let Some(remote) = remote {
let avatar_asset = CommitAvatarAsset::new(remote.clone(), sha.clone());
if let Some(Some(url)) = window.use_asset::<CommitAvatarAsset>(&avatar_asset, cx) {
return Avatar::new(url.to_string())
.size(size)
.into_element()
.into_any();
}
}
v_flex()
.w(size)
.h(size)
.border_1()
.border_color(cx.theme().colors().border)
.rounded_full()
.justify_center()
.items_center()
.child(
Icon::new(IconName::Person)
.color(Color::Muted)
.size(IconSize::Medium)
.into_element(),
)
.into_any()
}
fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let commit = &self.commit;
let author_name = commit.author_name.clone();
let commit_date = time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)
.unwrap_or_else(|_| time::OffsetDateTime::now_utc());
let local_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
let date_string = time_format::format_localized_timestamp(
commit_date,
time::OffsetDateTime::now_utc(),
local_offset,
time_format::TimestampFormat::MediumAbsolute,
);
let github_url = self.remote.as_ref().map(|remote| {
format!(
"{}/{}/{}/commit/{}",
remote.host.base_url(),
remote.owner,
remote.repo,
commit.sha
)
});
v_flex()
.p_4()
.pl_0()
.gap_4()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.items_start()
.child(
h_flex()
.w(self.editor.read(cx).last_gutter_dimensions().full_width())
.justify_center()
.child(self.render_commit_avatar(
&commit.sha,
gpui::rems(3.0),
window,
cx,
)),
)
.child(
v_flex()
.gap_1()
.child(
h_flex()
.gap_3()
.items_baseline()
.child(Label::new(author_name).color(Color::Default))
.child(
Label::new(format!("commit {}", commit.sha))
.color(Color::Muted),
),
)
.child(Label::new(date_string).color(Color::Muted)),
)
.child(div().flex_grow())
.children(github_url.map(|url| {
Button::new("view_on_github", "View on GitHub")
.icon(IconName::Github)
.style(ui::ButtonStyle::Subtle)
.on_click(move |_, _, cx| cx.open_url(&url))
})),
)
}
fn apply_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
Self::stash_action(
workspace,
"Apply",
window,
cx,
async move |repository, sha, stash, commit_view, workspace, cx| {
let result = repository.update(cx, |repo, cx| {
if !stash_matches_index(&sha, stash, repo) {
return Err(anyhow::anyhow!("Stash has changed, not applying"));
}
Ok(repo.stash_apply(Some(stash), cx))
})?;
match result {
Ok(task) => task.await?,
Err(err) => {
Self::close_commit_view(commit_view, workspace, cx).await?;
return Err(err);
}
};
Self::close_commit_view(commit_view, workspace, cx).await?;
anyhow::Ok(())
},
);
}
fn pop_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
Self::stash_action(
workspace,
"Pop",
window,
cx,
async move |repository, sha, stash, commit_view, workspace, cx| {
let result = repository.update(cx, |repo, cx| {
if !stash_matches_index(&sha, stash, repo) {
return Err(anyhow::anyhow!("Stash has changed, pop aborted"));
}
Ok(repo.stash_pop(Some(stash), cx))
})?;
match result {
Ok(task) => task.await?,
Err(err) => {
Self::close_commit_view(commit_view, workspace, cx).await?;
return Err(err);
}
};
Self::close_commit_view(commit_view, workspace, cx).await?;
anyhow::Ok(())
},
);
}
fn remove_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
Self::stash_action(
workspace,
"Drop",
window,
cx,
async move |repository, sha, stash, commit_view, workspace, cx| {
let result = repository.update(cx, |repo, cx| {
if !stash_matches_index(&sha, stash, repo) {
return Err(anyhow::anyhow!("Stash has changed, drop aborted"));
}
Ok(repo.stash_drop(Some(stash), cx))
})?;
match result {
Ok(task) => task.await??,
Err(err) => {
Self::close_commit_view(commit_view, workspace, cx).await?;
return Err(err);
}
};
Self::close_commit_view(commit_view, workspace, cx).await?;
anyhow::Ok(())
},
);
}
fn stash_action<AsyncFn>(
workspace: &mut Workspace,
str_action: &str,
window: &mut Window,
cx: &mut App,
callback: AsyncFn,
) where
AsyncFn: AsyncFnOnce(
Entity<Repository>,
&SharedString,
usize,
Entity<CommitView>,
WeakEntity<Workspace>,
&mut AsyncWindowContext,
) -> anyhow::Result<()>
+ 'static,
{
let Some(commit_view) = workspace.active_item_as::<CommitView>(cx) else {
return;
};
let Some(stash) = commit_view.read(cx).stash else {
return;
};
let sha = commit_view.read(cx).commit.sha.clone();
let answer = window.prompt(
PromptLevel::Info,
&format!("{} stash@{{{}}}?", str_action, stash),
None,
&[str_action, "Cancel"],
cx,
);
let workspace_weak = workspace.weak_handle();
let commit_view_entity = commit_view;
window
.spawn(cx, async move |cx| {
if answer.await != Ok(0) {
return anyhow::Ok(());
}
let Some(workspace) = workspace_weak.upgrade() else {
return Ok(());
};
let repo = workspace.update(cx, |workspace, cx| {
workspace
.panel::<GitPanel>(cx)
.and_then(|p| p.read(cx).active_repository.clone())
})?;
let Some(repo) = repo else {
return Ok(());
};
callback(repo, &sha, stash, commit_view_entity, workspace_weak, cx).await?;
anyhow::Ok(())
})
.detach_and_notify_err(window, cx);
}
async fn close_commit_view(
commit_view: Entity<CommitView>,
workspace: WeakEntity<Workspace>,
cx: &mut AsyncWindowContext,
) -> anyhow::Result<()> {
workspace
.update_in(cx, |workspace, window, cx| {
let active_pane = workspace.active_pane();
let commit_view_id = commit_view.entity_id();
active_pane.update(cx, |pane, cx| {
pane.close_item_by_id(commit_view_id, SaveIntent::Skip, window, cx)
})
})?
.await?;
anyhow::Ok(())
}
}
#[derive(Clone, Debug)]
struct CommitAvatarAsset {
sha: SharedString,
remote: GitRemote,
}
impl std::hash::Hash for CommitAvatarAsset {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.sha.hash(state);
self.remote.host.name().hash(state);
}
}
impl CommitAvatarAsset {
fn new(remote: GitRemote, sha: SharedString) -> Self {
Self { remote, sha }
}
}
impl Asset for CommitAvatarAsset {
type Source = Self;
type Output = Option<SharedString>;
fn load(
source: Self::Source,
cx: &mut App,
) -> impl Future<Output = Self::Output> + Send + 'static {
let client = cx.http_client();
async move {
match source
.remote
.host
.commit_author_avatar_url(
&source.remote.owner,
&source.remote.repo,
source.sha.clone(),
client,
)
.await
{
Ok(Some(url)) => Some(SharedString::from(url.to_string())),
Ok(None) => None,
Err(_) => None,
}
}
}
}
impl language::File for GitBlob {
fn as_local(&self) -> Option<&dyn language::LocalFile> {
None
}
fn disk_state(&self) -> DiskState {
if self.is_deleted {
DiskState::Deleted
} else {
DiskState::New
}
}
fn path_style(&self, _: &App) -> PathStyle {
PathStyle::Posix
}
fn path(&self) -> &Arc<RelPath> {
self.path.as_ref()
}
fn full_path(&self, _: &App) -> PathBuf {
self.path.as_std_path().to_path_buf()
}
fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
self.path.file_name().unwrap()
}
fn worktree_id(&self, _: &App) -> WorktreeId {
self.worktree_id
}
fn to_proto(&self, _cx: &App) -> language::proto::File {
unimplemented!()
}
fn is_private(&self) -> bool {
false
}
}
// No longer needed since metadata buffer is not created
// impl language::File for CommitMetadataFile {
// fn as_local(&self) -> Option<&dyn language::LocalFile> {
// None
// }
//
// fn disk_state(&self) -> DiskState {
// DiskState::New
// }
//
// fn path_style(&self, _: &App) -> PathStyle {
// PathStyle::Posix
// }
//
// fn path(&self) -> &Arc<RelPath> {
// &self.title
// }
//
// fn full_path(&self, _: &App) -> PathBuf {
// self.title.as_std_path().to_path_buf()
// }
//
// fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
// self.title.file_name().unwrap_or("commit")
// }
//
// fn worktree_id(&self, _: &App) -> WorktreeId {
// self.worktree_id
// }
//
// fn to_proto(&self, _cx: &App) -> language::proto::File {
// unimplemented!()
// }
//
// fn is_private(&self) -> bool {
// false
// }
// }
struct CommitViewAddon {
multibuffer: WeakEntity<MultiBuffer>,
}
impl Addon for CommitViewAddon {
fn render_buffer_header_controls(
&self,
excerpt: &ExcerptInfo,
_window: &Window,
cx: &App,
) -> Option<AnyElement> {
let multibuffer = self.multibuffer.upgrade()?;
let snapshot = multibuffer.read(cx).snapshot(cx);
let excerpts = snapshot.excerpts().collect::<Vec<_>>();
let current_idx = excerpts.iter().position(|(id, _, _)| *id == excerpt.id)?;
let (_, _, current_range) = &excerpts[current_idx];
let start_row = current_range.context.start.to_point(&excerpt.buffer).row;
let prev_end_row = if current_idx > 0 {
let (_, prev_buffer, prev_range) = &excerpts[current_idx - 1];
if prev_buffer.remote_id() == excerpt.buffer_id {
prev_range.context.end.to_point(&excerpt.buffer).row
} else {
0
}
} else {
0
};
let skipped_lines = start_row.saturating_sub(prev_end_row);
if skipped_lines > 0 {
Some(
Label::new(format!("{} unchanged lines", skipped_lines))
.color(Color::Muted)
.size(LabelSize::Small)
.into_any_element(),
)
} else {
None
}
}
fn to_any(&self) -> &dyn Any {
self
}
}
async fn build_buffer(
mut text: String,
blob: Arc<dyn File>,
language_registry: &Arc<language::LanguageRegistry>,
cx: &mut AsyncApp,
) -> Result<Entity<Buffer>> {
let line_ending = LineEnding::detect(&text);
LineEnding::normalize(&mut text);
let text = Rope::from(text);
let language = cx.update(|cx| language_registry.language_for_file(&blob, Some(&text), cx))?;
let language = if let Some(language) = language {
language_registry
.load_language(&language)
.await
.ok()
.and_then(|e| e.log_err())
} else {
None
};
let buffer = cx.new(|cx| {
let buffer = TextBuffer::new_normalized(
ReplicaId::LOCAL,
cx.entity_id().as_non_zero_u64().into(),
line_ending,
text,
);
let mut buffer = Buffer::build(buffer, Some(blob), Capability::ReadWrite);
buffer.set_language_async(language, cx);
buffer
})?;
Ok(buffer)
}
async fn build_buffer_diff(
mut old_text: Option<String>,
buffer: &Entity<Buffer>,
language_registry: &Arc<LanguageRegistry>,
cx: &mut AsyncApp,
) -> Result<Entity<BufferDiff>> {
if let Some(old_text) = &mut old_text {
LineEnding::normalize(old_text);
}
let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
let base_buffer = cx
.update(|cx| {
Buffer::build_snapshot(
old_text.as_deref().unwrap_or("").into(),
buffer.language().cloned(),
Some(language_registry.clone()),
cx,
)
})?
.await;
let diff_snapshot = cx
.update(|cx| {
BufferDiffSnapshot::new_with_base_buffer(
buffer.text.clone(),
old_text.map(Arc::new),
base_buffer,
cx,
)
})?
.await;
cx.new(|cx| {
let mut diff = BufferDiff::new(&buffer.text, cx);
diff.set_snapshot(diff_snapshot, &buffer.text, cx);
diff
})
}
impl EventEmitter<EditorEvent> for CommitView {}
impl Focusable for CommitView {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.editor.focus_handle(cx)
}
}
impl Item for CommitView {
type Event = EditorEvent;
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
Some(Icon::new(IconName::GitBranch).color(Color::Muted))
}
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx))
.color(if params.selected {
Color::Default
} else {
Color::Muted
})
.into_any_element()
}
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
let short_sha = self.commit.sha.get(0..7).unwrap_or(&*self.commit.sha);
let subject = truncate_and_trailoff(self.commit.message.split('\n').next().unwrap(), 20);
format!("{short_sha} - {subject}").into()
}
fn tab_tooltip_text(&self, _: &App) -> Option<ui::SharedString> {
let short_sha = self.commit.sha.get(0..16).unwrap_or(&*self.commit.sha);
let subject = self.commit.message.split('\n').next().unwrap();
Some(format!("{short_sha} - {subject}").into())
}
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
Editor::to_item_events(event, f)
}
fn telemetry_event_text(&self) -> Option<&'static str> {
Some("Commit View Opened")
}
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.editor
.update(cx, |editor, cx| editor.deactivated(window, cx));
}
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.clone().into())
} else {
None
}
}
fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
fn for_each_project_item(
&self,
cx: &App,
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
) {
self.editor.for_each_project_item(cx, f)
}
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
});
}
fn navigate(
&mut self,
data: Box<dyn Any>,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
self.editor
.update(cx, |editor, cx| editor.navigate(data, window, cx))
}
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
ToolbarItemLocation::Hidden
}
fn breadcrumbs(&self, _theme: &theme::Theme, _cx: &App) -> Option<Vec<BreadcrumbText>> {
None
}
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.added_to_workspace(workspace, window, cx)
});
}
fn can_split(&self) -> bool {
true
}
fn clone_on_split(
&self,
_workspace_id: Option<workspace::WorkspaceId>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Option<Entity<Self>>>
where
Self: Sized,
{
Task::ready(Some(cx.new(|cx| {
let editor = cx.new(|cx| {
self.editor
.update(cx, |editor, cx| editor.clone(window, cx))
});
let multibuffer = editor.read(cx).buffer().clone();
Self {
editor,
multibuffer,
commit: self.commit.clone(),
stash: self.stash,
repository: self.repository.clone(),
remote: self.remote.clone(),
}
})))
}
}
impl Render for CommitView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_stash = self.stash.is_some();
div()
.key_context(if is_stash { "StashDiff" } else { "CommitDiff" })
.bg(cx.theme().colors().editor_background)
.flex()
.flex_col()
.size_full()
.child(self.render_header(window, cx))
.child(div().flex_grow().child(self.editor.clone()))
}
}
pub struct CommitViewToolbar {
commit_view: Option<WeakEntity<CommitView>>,
}
impl CommitViewToolbar {
pub fn new() -> Self {
Self { commit_view: None }
}
}
impl EventEmitter<ToolbarItemEvent> for CommitViewToolbar {}
impl Render for CommitViewToolbar {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
}
}
impl ToolbarItemView for CommitViewToolbar {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_: &mut Window,
cx: &mut Context<Self>,
) -> ToolbarItemLocation {
if let Some(entity) = active_pane_item.and_then(|i| i.act_as::<CommitView>(cx))
&& entity.read(cx).stash.is_some()
{
self.commit_view = Some(entity.downgrade());
return ToolbarItemLocation::PrimaryRight;
}
ToolbarItemLocation::Hidden
}
fn pane_focus_update(
&mut self,
_pane_focused: bool,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
}
}
fn stash_matches_index(sha: &str, stash_index: usize, repo: &Repository) -> bool {
repo.stash_entries
.entries
.get(stash_index)
.map(|entry| entry.oid.to_string() == sha)
.unwrap_or(false)
}