mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
git_graph: Improve commit detail panel UI (#49876)
- Use git panel icons to show a changed file's state - Centered avatar at top and move close button to top right - Made changed file list scrollable - clicking on a file open's it's historic commit view - Note: The commit view doesn't fully populate the multibuffer, will fix this in a different PR because it involves updating the commit view interface to add more functionality ## Before <img width="602" height="1704" alt="image" src="https://github.com/user-attachments/assets/75a12fff-8a6a-4d0f-90dd-544adb0c2814" /> ## After <img width="227" height="856" alt="Screenshot 2026-02-23 at 1 23 45 PM" src="https://github.com/user-attachments/assets/244cc9f3-e94d-4cc6-ac46-80fe70a619ff" /> Before you mark this PR as ready for review, make sure that you have: - [ ] Added a solid test coverage and/or screenshots from doing manual testing - [x] Done a self-review taking into account security and performance aspects - [ ] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Release Notes: - N/A
This commit is contained in:
parent
1240fd1f36
commit
15885647e1
2 changed files with 219 additions and 77 deletions
|
|
@ -451,6 +451,13 @@ pub struct CommitDiff {
|
|||
pub files: Vec<CommitFile>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum CommitFileStatus {
|
||||
Added,
|
||||
Modified,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommitFile {
|
||||
pub path: RepoPath,
|
||||
|
|
@ -459,6 +466,16 @@ pub struct CommitFile {
|
|||
pub is_binary: bool,
|
||||
}
|
||||
|
||||
impl CommitFile {
|
||||
pub fn status(&self) -> CommitFileStatus {
|
||||
match (&self.old_text, &self.new_text) {
|
||||
(None, Some(_)) => CommitFileStatus::Added,
|
||||
(Some(_), None) => CommitFileStatus::Deleted,
|
||||
_ => CommitFileStatus::Modified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommitDetails {
|
||||
pub fn short_sha(&self) -> SharedString {
|
||||
self.sha[..SHORT_SHA_LENGTH].to_string().into()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
|
|||
use git::{
|
||||
BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, ParsedGitRemote,
|
||||
parse_git_remote_url,
|
||||
repository::{CommitDiff, InitialGraphCommitData, LogOrder, LogSource},
|
||||
repository::{
|
||||
CommitDiff, CommitFile, CommitFileStatus, InitialGraphCommitData, LogOrder, LogSource,
|
||||
RepoPath,
|
||||
},
|
||||
};
|
||||
use git_ui::{commit_tooltip::CommitAvatar, commit_view::CommitView};
|
||||
use gpui::{
|
||||
|
|
@ -11,7 +14,7 @@ use gpui::{
|
|||
DragMoveEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight,
|
||||
Hsla, InteractiveElement, ParentElement, PathBuilder, Pixels, Point, Render, ScrollStrategy,
|
||||
ScrollWheelEvent, SharedString, Styled, Subscription, Task, WeakEntity, Window, actions,
|
||||
anchored, deferred, point, px,
|
||||
anchored, deferred, point, px, uniform_list,
|
||||
};
|
||||
use menu::{Cancel, SelectNext, SelectPrevious};
|
||||
use project::{
|
||||
|
|
@ -41,6 +44,117 @@ const RESIZE_HANDLE_WIDTH: f32 = 8.0;
|
|||
|
||||
struct DraggedSplitHandle;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ChangedFileEntry {
|
||||
icon_name: IconName,
|
||||
icon_color: Hsla,
|
||||
file_name: SharedString,
|
||||
dir_path: SharedString,
|
||||
repo_path: RepoPath,
|
||||
}
|
||||
|
||||
impl ChangedFileEntry {
|
||||
fn from_commit_file(file: &CommitFile, cx: &App) -> Self {
|
||||
let file_name: SharedString = file
|
||||
.path
|
||||
.file_name()
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or_default()
|
||||
.into();
|
||||
let dir_path: SharedString = file
|
||||
.path
|
||||
.parent()
|
||||
.map(|p| p.as_unix_str().to_string())
|
||||
.unwrap_or_default()
|
||||
.into();
|
||||
let colors = cx.theme().colors();
|
||||
let (icon_name, icon_color) = match file.status() {
|
||||
CommitFileStatus::Added => (IconName::SquarePlus, colors.version_control_added),
|
||||
CommitFileStatus::Modified => (IconName::SquareDot, colors.version_control_modified),
|
||||
CommitFileStatus::Deleted => (IconName::SquareMinus, colors.version_control_deleted),
|
||||
};
|
||||
Self {
|
||||
icon_name,
|
||||
icon_color,
|
||||
file_name,
|
||||
dir_path,
|
||||
repo_path: file.path.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_in_commit_view(
|
||||
&self,
|
||||
commit_sha: &SharedString,
|
||||
repository: &WeakEntity<Repository>,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
CommitView::open(
|
||||
commit_sha.to_string(),
|
||||
repository.clone(),
|
||||
workspace.clone(),
|
||||
None,
|
||||
Some(self.repo_path.clone()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn render(
|
||||
&self,
|
||||
ix: usize,
|
||||
commit_sha: SharedString,
|
||||
repository: WeakEntity<Repository>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &App,
|
||||
) -> AnyElement {
|
||||
h_flex()
|
||||
.id(("changed-file", ix))
|
||||
.px_3()
|
||||
.py_px()
|
||||
.gap_1()
|
||||
.min_w_0()
|
||||
.overflow_hidden()
|
||||
.cursor_pointer()
|
||||
.rounded_md()
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.child(
|
||||
div().flex_none().child(
|
||||
Icon::new(self.icon_name)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Custom(self.icon_color)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().flex_none().child(
|
||||
Label::new(self.file_name.clone())
|
||||
.size(LabelSize::Small)
|
||||
.single_line(),
|
||||
),
|
||||
)
|
||||
.when(!self.dir_path.is_empty(), |this| {
|
||||
this.child(
|
||||
div().min_w_0().overflow_hidden().child(
|
||||
Label::new(self.dir_path.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate()
|
||||
.single_line(),
|
||||
),
|
||||
)
|
||||
})
|
||||
.on_click({
|
||||
let entry = self.clone();
|
||||
move |_, window, cx| {
|
||||
entry.open_in_commit_view(&commit_sha, &repository, &workspace, window, cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SplitState {
|
||||
left_ratio: f32,
|
||||
visible_left_ratio: f32,
|
||||
|
|
@ -1154,7 +1268,22 @@ impl GitGraph {
|
|||
.map(|diff| diff.files.len())
|
||||
.unwrap_or(0);
|
||||
|
||||
let sorted_file_entries: Rc<Vec<ChangedFileEntry>> = Rc::new(
|
||||
self.selected_commit_diff
|
||||
.as_ref()
|
||||
.map(|diff| {
|
||||
let mut files: Vec<_> = diff.files.iter().collect();
|
||||
files.sort_by_key(|file| file.status());
|
||||
files
|
||||
.into_iter()
|
||||
.map(|file| ChangedFileEntry::from_commit_file(file, cx))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.relative()
|
||||
.w(px(300.))
|
||||
.h_full()
|
||||
.border_l_1()
|
||||
|
|
@ -1163,25 +1292,32 @@ impl GitGraph {
|
|||
.flex_basis(DefiniteLength::Fraction(
|
||||
self.commit_details_split_state.read(cx).right_ratio(),
|
||||
))
|
||||
.child(
|
||||
div().absolute().top_2().right_2().child(
|
||||
IconButton::new("close-detail", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.selected_entry_idx = None;
|
||||
this.selected_commit_diff = None;
|
||||
this._commit_diff_task = None;
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_3()
|
||||
.w_full()
|
||||
.min_w_0()
|
||||
.flex_none()
|
||||
.pt_3()
|
||||
.gap_3()
|
||||
.child(
|
||||
h_flex().justify_between().child(avatar).child(
|
||||
IconButton::new("close-detail", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.selected_entry_idx = None;
|
||||
this.selected_commit_diff = None;
|
||||
this._commit_diff_task = None;
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.px_3()
|
||||
.items_center()
|
||||
.gap_0p5()
|
||||
.child(avatar)
|
||||
.child(Label::new(author_name.clone()).weight(FontWeight::SEMIBOLD))
|
||||
.child(
|
||||
Label::new(date_string)
|
||||
|
|
@ -1190,14 +1326,20 @@ impl GitGraph {
|
|||
),
|
||||
)
|
||||
.children((!ref_names.is_empty()).then(|| {
|
||||
h_flex().gap_1().flex_wrap().children(
|
||||
ref_names
|
||||
.iter()
|
||||
.map(|name| self.render_badge(name, accent_color)),
|
||||
)
|
||||
h_flex()
|
||||
.px_3()
|
||||
.justify_center()
|
||||
.gap_1()
|
||||
.flex_wrap()
|
||||
.children(
|
||||
ref_names
|
||||
.iter()
|
||||
.map(|name| self.render_badge(name, accent_color)),
|
||||
)
|
||||
}))
|
||||
.child(
|
||||
v_flex()
|
||||
.px_3()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
h_flex()
|
||||
|
|
@ -1224,7 +1366,7 @@ impl GitGraph {
|
|||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Hash)
|
||||
Icon::new(IconName::FileGit)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
|
|
@ -1286,72 +1428,55 @@ impl GitGraph {
|
|||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.p_3()
|
||||
.min_w_0()
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
div()
|
||||
.w_full()
|
||||
.min_w_0()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.p_3()
|
||||
.child(Label::new(subject).weight(FontWeight::MEDIUM)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.overflow_hidden()
|
||||
.min_h_0()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.p_3()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(format!("{} Changed Files", changed_files_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(self.selected_commit_diff.as_ref().map(|diff| {
|
||||
v_flex().gap_1().children(diff.files.iter().map(|file| {
|
||||
let file_name: String = file
|
||||
.path
|
||||
.file_name()
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or_default();
|
||||
let dir_path: String = file
|
||||
.path
|
||||
.parent()
|
||||
.map(|p| p.as_unix_str().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
Icon::new(IconName::File)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent),
|
||||
div().px_3().pt_3().pb_1().child(
|
||||
Label::new(format!("{} Changed Files", changed_files_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child({
|
||||
let entries = sorted_file_entries;
|
||||
let entry_count = entries.len();
|
||||
let commit_sha = full_sha.clone();
|
||||
let repository = repository.downgrade();
|
||||
let workspace = self.workspace.clone();
|
||||
uniform_list(
|
||||
"changed-files-list",
|
||||
entry_count,
|
||||
move |range, _window, cx| {
|
||||
range
|
||||
.map(|ix| {
|
||||
entries[ix].render(
|
||||
ix,
|
||||
commit_sha.clone(),
|
||||
repository.clone(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
Label::new(file_name)
|
||||
.size(LabelSize::Small)
|
||||
.single_line(),
|
||||
)
|
||||
.when(!dir_path.is_empty(), |this| {
|
||||
this.child(
|
||||
Label::new(dir_path)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.single_line(),
|
||||
)
|
||||
})
|
||||
}))
|
||||
})),
|
||||
),
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.flex_1()
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue