Add placeholder git panel (#21894)

Nate Butler created

Adds a simple git placeholder panel for us to iterate from. Also
includes a number of assets from the git prototyping branch that we will
use.

Note: This panel is staff flagged for now.

Release Notes:

- N/A

Change summary

Cargo.lock                                |  12 +
Cargo.toml                                |   2 
assets/icons/git_branch.svg               |   1 
assets/icons/panel_left.svg               |   1 
assets/icons/panel_right.svg              |   1 
assets/icons/square_dot.svg               |   1 
assets/icons/square_minus.svg             |   1 
assets/icons/square_plus.svg              |   1 
crates/feature_flags/src/feature_flags.rs |   5 
crates/git_ui/Cargo.toml                  |  25 +++
crates/git_ui/LICENSE-GPL                 |   1 
crates/git_ui/src/git_panel.rs            | 181 +++++++++++++++++++++++++
crates/git_ui/src/git_ui.rs               |   1 
crates/ui/src/components/button/button.rs |   2 
crates/ui/src/components/checkbox.rs      |   4 
crates/ui/src/components/content_group.rs |   2 
crates/ui/src/components/divider.rs       |  87 ++++++++++-
crates/ui/src/components/facepile.rs      |   2 
crates/ui/src/components/icon.rs          |  12 +
crates/ui/src/components/indicator.rs     |   2 
crates/ui/src/components/table.rs         |   2 
crates/ui/src/traits/component_preview.rs |   6 
crates/workspace/src/theme_preview.rs     | 145 -------------------
crates/zed/Cargo.toml                     |   1 
crates/zed/src/main.rs                    |   1 
crates/zed/src/zed.rs                     |  23 ++
26 files changed, 352 insertions(+), 170 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5169,6 +5169,17 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "git_ui"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+ "serde",
+ "ui",
+ "windows 0.58.0",
+ "workspace",
+]
+
 [[package]]
 name = "glob"
 version = "0.3.1"
@@ -15994,6 +16005,7 @@ dependencies = [
  "futures 0.3.31",
  "git",
  "git_hosting_providers",
+ "git_ui",
  "go_to_line",
  "gpui",
  "http_client",

Cargo.toml 🔗

@@ -142,6 +142,7 @@ members = [
     "crates/zed",
     "crates/zed_actions",
     "crates/zeta",
+    "crates/git_ui",
 
     #
     # Extensions
@@ -227,6 +228,7 @@ fs = { path = "crates/fs" }
 fsevent = { path = "crates/fsevent" }
 fuzzy = { path = "crates/fuzzy" }
 git = { path = "crates/git" }
+git_ui = { path = "crates/git_ui" }
 git_hosting_providers = { path = "crates/git_hosting_providers" }
 go_to_line = { path = "crates/go_to_line" }
 google_ai = { path = "crates/google_ai" }

assets/icons/git_branch.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-branch"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>

assets/icons/panel_left.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-left"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>

assets/icons/panel_right.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-right"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/></svg>

assets/icons/square_dot.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-dot"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="12" cy="12" r="1"/></svg>

assets/icons/square_minus.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-minus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/></svg>

assets/icons/square_plus.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-plus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>

crates/feature_flags/src/feature_flags.rs 🔗

@@ -64,6 +64,11 @@ impl FeatureFlag for ZetaFeatureFlag {
     const NAME: &'static str = "zeta";
 }
 
+pub struct GitUiFeatureFlag;
+impl FeatureFlag for GitUiFeatureFlag {
+    const NAME: &'static str = "git-ui";
+}
+
 pub struct Remoting {}
 impl FeatureFlag for Remoting {
     const NAME: &'static str = "remoting";

crates/git_ui/Cargo.toml 🔗

@@ -0,0 +1,25 @@
+[package]
+name = "git_ui"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+name = "git_ui"
+path = "src/git_ui.rs"
+
+[dependencies]
+gpui.workspace = true
+serde.workspace = true
+workspace.workspace = true
+ui.workspace = true
+
+[target.'cfg(windows)'.dependencies]
+windows.workspace = true
+
+[features]
+default = []

crates/git_ui/src/git_panel.rs 🔗

@@ -0,0 +1,181 @@
+use gpui::*;
+use ui::{prelude::*, Checkbox, Divider, DividerColor, ElevationIndex};
+use workspace::dock::{DockPosition, Panel, PanelEvent};
+use workspace::Workspace;
+
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(
+        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
+            workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+                workspace.toggle_panel_focus::<GitPanel>(cx);
+            });
+        },
+    )
+    .detach();
+}
+
+actions!(git_panel, [Deploy, ToggleFocus]);
+
+#[derive(Clone)]
+pub struct GitPanel {
+    _workspace: WeakView<Workspace>,
+    focus_handle: FocusHandle,
+    width: Option<Pixels>,
+}
+
+impl GitPanel {
+    pub fn load(
+        workspace: WeakView<Workspace>,
+        cx: AsyncWindowContext,
+    ) -> Task<Result<View<Self>>> {
+        cx.spawn(|mut cx| async move {
+            workspace.update(&mut cx, |workspace, cx| {
+                let workspace_handle = workspace.weak_handle();
+
+                cx.new_view(|cx| Self::new(workspace_handle, cx))
+            })
+        })
+    }
+
+    pub fn new(workspace: WeakView<Workspace>, cx: &mut ViewContext<Self>) -> Self {
+        Self {
+            _workspace: workspace,
+            focus_handle: cx.focus_handle(),
+            width: Some(px(360.)),
+        }
+    }
+
+    pub fn render_panel_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        h_flex()
+            .h(px(32.))
+            .items_center()
+            .px_3()
+            .bg(ElevationIndex::Surface.bg(cx))
+            .child(
+                h_flex()
+                    .gap_1()
+                    .child(Checkbox::new("all-changes", true.into()).disabled(true))
+                    .child(div().text_buffer(cx).text_ui_sm(cx).child("0 changes")),
+            )
+            .child(div().flex_grow())
+            .child(
+                h_flex()
+                    .gap_1()
+                    .child(
+                        IconButton::new("discard-changes", IconName::Undo)
+                            .icon_size(IconSize::Small)
+                            .disabled(true),
+                    )
+                    .child(
+                        Button::new("stage-all", "Stage All")
+                            .label_size(LabelSize::Small)
+                            .layer(ElevationIndex::ElevatedSurface)
+                            .size(ButtonSize::Compact)
+                            .style(ButtonStyle::Filled)
+                            .disabled(true),
+                    ),
+            )
+    }
+
+    pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
+        div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
+            v_flex()
+                .h_full()
+                .py_2p5()
+                .px_3()
+                .bg(cx.theme().colors().editor_background)
+                .font_buffer(cx)
+                .text_ui_sm(cx)
+                .text_color(cx.theme().colors().text_muted)
+                .child("Add a message")
+                .gap_1()
+                .child(div().flex_grow())
+                .child(
+                    h_flex().child(div().gap_1().flex_grow()).child(
+                        Button::new("commit", "Commit")
+                            .label_size(LabelSize::Small)
+                            .layer(ElevationIndex::ElevatedSurface)
+                            .size(ButtonSize::Compact)
+                            .style(ButtonStyle::Filled)
+                            .disabled(true),
+                    ),
+                )
+                .cursor(CursorStyle::OperationNotAllowed)
+                .opacity(0.5),
+        )
+    }
+}
+
+impl Render for GitPanel {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        v_flex()
+            .key_context("GitPanel")
+            .font_buffer(cx)
+            .py_1()
+            .id("git_panel")
+            .track_focus(&self.focus_handle)
+            .size_full()
+            .overflow_hidden()
+            .bg(ElevationIndex::Surface.bg(cx))
+            .child(self.render_panel_header(cx))
+            .child(
+                h_flex()
+                    .items_center()
+                    .h(px(8.))
+                    .child(Divider::horizontal_dashed().color(DividerColor::Border)),
+            )
+            .child(div().flex_1())
+            .child(
+                h_flex()
+                    .items_center()
+                    .h(px(8.))
+                    .child(Divider::horizontal_dashed().color(DividerColor::Border)),
+            )
+            .child(self.render_commit_editor(cx))
+    }
+}
+
+impl FocusableView for GitPanel {
+    fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl EventEmitter<PanelEvent> for GitPanel {}
+
+impl Panel for GitPanel {
+    fn persistent_name() -> &'static str {
+        "GitPanel"
+    }
+
+    fn position(&self, _cx: &gpui::WindowContext) -> DockPosition {
+        DockPosition::Left
+    }
+
+    fn position_is_valid(&self, position: DockPosition) -> bool {
+        matches!(position, DockPosition::Left | DockPosition::Right)
+    }
+
+    fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
+
+    fn size(&self, _cx: &gpui::WindowContext) -> Pixels {
+        self.width.unwrap_or(px(360.))
+    }
+
+    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
+        self.width = size;
+        cx.notify();
+    }
+
+    fn icon(&self, _cx: &gpui::WindowContext) -> Option<ui::IconName> {
+        Some(ui::IconName::GitBranch)
+    }
+
+    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
+        Some("Git Panel")
+    }
+
+    fn toggle_action(&self) -> Box<dyn Action> {
+        Box::new(ToggleFocus)
+    }
+}

crates/ui/src/components/button/button.rs 🔗

@@ -445,7 +445,7 @@ impl ComponentPreview for Button {
         "A button allows users to take actions, and make choices, with a single tap."
     }
 
-    fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
+    fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
         vec![
             example_group_with_title(
                 "Styles",

crates/ui/src/components/checkbox.rs 🔗

@@ -118,7 +118,7 @@ impl ComponentPreview for Checkbox {
         "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
     }
 
-    fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
+    fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
         vec![
             example_group_with_title(
                 "Default",
@@ -214,7 +214,7 @@ impl ComponentPreview for CheckboxWithLabel {
         "A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
     }
 
-    fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
+    fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
         vec![example_group(vec![
             single_example(
                 "Unselected",

crates/ui/src/components/content_group.rs 🔗

@@ -95,7 +95,7 @@ impl ComponentPreview for ContentGroup {
         ExampleLabelSide::Bottom
     }
 
-    fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
+    fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
         vec![example_group(vec![
             single_example(
                 "Default",

crates/ui/src/components/divider.rs 🔗

@@ -3,6 +3,13 @@ use gpui::{Hsla, IntoElement};
 
 use crate::prelude::*;
 
+#[derive(Clone, Copy, PartialEq)]
+enum DividerStyle {
+    Solid,
+    Dashed,
+}
+
+#[derive(Clone, Copy, PartialEq)]
 enum DividerDirection {
     Horizontal,
     Vertical,
@@ -27,6 +34,7 @@ impl DividerColor {
 
 #[derive(IntoElement)]
 pub struct Divider {
+    style: DividerStyle,
     direction: DividerDirection,
     color: DividerColor,
     inset: bool,
@@ -34,22 +42,17 @@ pub struct Divider {
 
 impl RenderOnce for Divider {
     fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        div()
-            .map(|this| match self.direction {
-                DividerDirection::Horizontal => {
-                    this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
-                }
-                DividerDirection::Vertical => {
-                    this.w_px().h_full().when(self.inset, |this| this.my_1p5())
-                }
-            })
-            .bg(self.color.hsla(cx))
+        match self.style {
+            DividerStyle::Solid => self.render_solid(cx).into_any_element(),
+            DividerStyle::Dashed => self.render_dashed(cx).into_any_element(),
+        }
     }
 }
 
 impl Divider {
     pub fn horizontal() -> Self {
         Self {
+            style: DividerStyle::Solid,
             direction: DividerDirection::Horizontal,
             color: DividerColor::default(),
             inset: false,
@@ -58,6 +61,25 @@ impl Divider {
 
     pub fn vertical() -> Self {
         Self {
+            style: DividerStyle::Solid,
+            direction: DividerDirection::Vertical,
+            color: DividerColor::default(),
+            inset: false,
+        }
+    }
+
+    pub fn horizontal_dashed() -> Self {
+        Self {
+            style: DividerStyle::Dashed,
+            direction: DividerDirection::Horizontal,
+            color: DividerColor::default(),
+            inset: false,
+        }
+    }
+
+    pub fn vertical_dashed() -> Self {
+        Self {
+            style: DividerStyle::Dashed,
             direction: DividerDirection::Vertical,
             color: DividerColor::default(),
             inset: false,
@@ -73,4 +95,49 @@ impl Divider {
         self.color = color;
         self
     }
+
+    pub fn render_solid(self, cx: &WindowContext) -> impl IntoElement {
+        div()
+            .map(|this| match self.direction {
+                DividerDirection::Horizontal => {
+                    this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
+                }
+                DividerDirection::Vertical => {
+                    this.w_px().h_full().when(self.inset, |this| this.my_1p5())
+                }
+            })
+            .bg(self.color.hsla(cx))
+    }
+
+    // TODO: Use canvas or a shader here
+    // This obviously is a short term approach
+    pub fn render_dashed(self, cx: &WindowContext) -> impl IntoElement {
+        let segment_count = 128;
+        let segment_count_f = segment_count as f32;
+        let segment_min_w = 6.;
+        let base = match self.direction {
+            DividerDirection::Horizontal => h_flex(),
+            DividerDirection::Vertical => v_flex(),
+        };
+        let (w, h) = match self.direction {
+            DividerDirection::Horizontal => (px(segment_min_w), px(1.)),
+            DividerDirection::Vertical => (px(1.), px(segment_min_w)),
+        };
+        let color = self.color.hsla(cx);
+        let total_min_w = segment_min_w * segment_count_f * 2.; // * 2 because of the gap
+
+        base.min_w(px(total_min_w))
+            .map(|this| {
+                if self.direction == DividerDirection::Horizontal {
+                    this.w_full().h_px()
+                } else {
+                    this.w_px().h_full()
+                }
+            })
+            .gap(px(segment_min_w))
+            .overflow_hidden()
+            .children(
+                (0..segment_count).map(|_| div().flex_grow().flex_shrink_0().w(w).h(h).bg(color)),
+            )
+    }
 }

crates/ui/src/components/facepile.rs 🔗

@@ -67,7 +67,7 @@ impl ComponentPreview for Facepile {
         \n\nFacepiles are used to display a group of people or things,\
         such as a list of participants in a collaboration session."
     }
-    fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
+    fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
         let few_faces: [&'static str; 3] = [
             "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
             "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",

crates/ui/src/components/icon.rs 🔗

@@ -200,6 +200,7 @@ pub enum IconName {
     GenericRestore,
     Github,
     Globe,
+    GitBranch,
     Hash,
     HistoryRerun,
     Indicator,
@@ -224,6 +225,8 @@ pub enum IconName {
     Option,
     PageDown,
     PageUp,
+    PanelLeft,
+    PanelRight,
     Pencil,
     Person,
     PhoneIncoming,
@@ -267,6 +270,9 @@ pub enum IconName {
     SparkleFilled,
     Spinner,
     Split,
+    SquareDot,
+    SquareMinus,
+    SquarePlus,
     Star,
     StarFilled,
     Stop,
@@ -497,7 +503,7 @@ impl RenderOnce for IconDecoration {
 }
 
 impl ComponentPreview for IconDecoration {
-    fn examples(cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
+    fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
         let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
 
         let examples = all_kinds
@@ -539,7 +545,7 @@ impl RenderOnce for DecoratedIcon {
 }
 
 impl ComponentPreview for DecoratedIcon {
-    fn examples(cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
+    fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
         let icon_1 = Icon::new(IconName::FileDoc);
         let icon_2 = Icon::new(IconName::FileDoc);
         let icon_3 = Icon::new(IconName::FileDoc);
@@ -658,7 +664,7 @@ impl RenderOnce for IconWithIndicator {
 }
 
 impl ComponentPreview for Icon {
-    fn examples(_cx: &WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
+    fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
         let arrow_icons = vec![
             IconName::ArrowDown,
             IconName::ArrowLeft,

crates/ui/src/components/indicator.rs 🔗

@@ -89,7 +89,7 @@ impl ComponentPreview for Indicator {
         "An indicator visually represents a status or state."
     }
 
-    fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
+    fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
         vec![
             example_group_with_title(
                 "Types",

crates/ui/src/components/table.rs 🔗

@@ -160,7 +160,7 @@ impl ComponentPreview for Table {
         ExampleLabelSide::Top
     }
 
-    fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
+    fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
         vec![
             example_group(vec![
                 single_example(

crates/ui/src/traits/component_preview.rs 🔗

@@ -30,20 +30,20 @@ pub trait ComponentPreview: IntoElement {
         ExampleLabelSide::default()
     }
 
-    fn examples(_cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>>;
+    fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>>;
 
     fn custom_example(_cx: &WindowContext) -> impl Into<Option<AnyElement>> {
         None::<AnyElement>
     }
 
-    fn component_previews(cx: &WindowContext) -> Vec<AnyElement> {
+    fn component_previews(cx: &mut WindowContext) -> Vec<AnyElement> {
         Self::examples(cx)
             .into_iter()
             .map(|example| Self::render_example_group(example))
             .collect()
     }
 
-    fn render_component_previews(cx: &WindowContext) -> AnyElement {
+    fn render_component_previews(cx: &mut WindowContext) -> AnyElement {
         let title = Self::title();
         let (source, title) = title
             .rsplit_once("::")

crates/workspace/src/theme_preview.rs 🔗

@@ -108,147 +108,6 @@ impl ThemePreview {
         cx.theme().colors().editor_background
     }
 
-    fn render_avatars(&self, cx: &ViewContext<Self>) -> impl IntoElement {
-        v_flex()
-            .gap_1()
-            .child(
-                Headline::new("Avatars")
-                    .size(HeadlineSize::Small)
-                    .color(Color::Muted),
-            )
-            .child(
-                h_flex()
-                    .items_start()
-                    .gap_4()
-                    .child(Avatar::new(AVATAR_URL).size(px(24.)))
-                    .child(Avatar::new(AVATAR_URL).size(px(24.)).grayscale(true))
-                    .child(
-                        Avatar::new(AVATAR_URL)
-                            .size(px(24.))
-                            .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)),
-                    )
-                    .child(
-                        Avatar::new(AVATAR_URL)
-                            .size(px(24.))
-                            .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)),
-                    )
-                    .child(
-                        Avatar::new(AVATAR_URL)
-                            .size(px(24.))
-                            .indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
-                    )
-                    .child(
-                        Avatar::new(AVATAR_URL)
-                            .size(px(24.))
-                            .indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
-                    )
-                    .child(
-                        Facepile::empty()
-                            .child(
-                                Avatar::new(AVATAR_URL)
-                                    .border_color(Self::preview_bg(cx))
-                                    .size(px(22.))
-                                    .into_any_element(),
-                            )
-                            .child(
-                                Avatar::new(AVATAR_URL)
-                                    .border_color(Self::preview_bg(cx))
-                                    .size(px(22.))
-                                    .into_any_element(),
-                            )
-                            .child(
-                                Avatar::new(AVATAR_URL)
-                                    .border_color(Self::preview_bg(cx))
-                                    .size(px(22.))
-                                    .into_any_element(),
-                            )
-                            .child(
-                                Avatar::new(AVATAR_URL)
-                                    .border_color(Self::preview_bg(cx))
-                                    .size(px(22.))
-                                    .into_any_element(),
-                            ),
-                    ),
-            )
-    }
-
-    fn render_buttons(&self, layer: ElevationIndex, cx: &ViewContext<Self>) -> impl IntoElement {
-        v_flex()
-            .gap_1()
-            .child(
-                Headline::new("Buttons")
-                    .size(HeadlineSize::Small)
-                    .color(Color::Muted),
-            )
-            .child(
-                h_flex()
-                    .items_start()
-                    .gap_px()
-                    .child(
-                        IconButton::new("icon_button_transparent", IconName::Check)
-                            .style(ButtonStyle::Transparent),
-                    )
-                    .child(
-                        IconButton::new("icon_button_subtle", IconName::Check)
-                            .style(ButtonStyle::Subtle),
-                    )
-                    .child(
-                        IconButton::new("icon_button_filled", IconName::Check)
-                            .style(ButtonStyle::Filled),
-                    )
-                    .child(
-                        IconButton::new("icon_button_selected_accent", IconName::Check)
-                            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                            .selected(true),
-                    )
-                    .child(IconButton::new("icon_button_selected", IconName::Check).selected(true))
-                    .child(
-                        IconButton::new("icon_button_positive", IconName::Check)
-                            .style(ButtonStyle::Tinted(TintColor::Positive)),
-                    )
-                    .child(
-                        IconButton::new("icon_button_warning", IconName::Check)
-                            .style(ButtonStyle::Tinted(TintColor::Warning)),
-                    )
-                    .child(
-                        IconButton::new("icon_button_negative", IconName::Check)
-                            .style(ButtonStyle::Tinted(TintColor::Negative)),
-                    ),
-            )
-            .child(
-                h_flex()
-                    .gap_px()
-                    .child(
-                        Button::new("button_transparent", "Transparent")
-                            .style(ButtonStyle::Transparent),
-                    )
-                    .child(Button::new("button_subtle", "Subtle").style(ButtonStyle::Subtle))
-                    .child(Button::new("button_filled", "Filled").style(ButtonStyle::Filled))
-                    .child(
-                        Button::new("button_selected", "Selected")
-                            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                            .selected(true),
-                    )
-                    .child(
-                        Button::new("button_selected_tinted", "Selected (Tinted)")
-                            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                            .selected(true),
-                    )
-                    .child(
-                        Button::new("button_positive", "Tint::Positive")
-                            .style(ButtonStyle::Tinted(TintColor::Positive)),
-                    )
-                    .child(
-                        Button::new("button_warning", "Tint::Warning")
-                            .style(ButtonStyle::Tinted(TintColor::Warning)),
-                    )
-                    .child(
-                        Button::new("button_negative", "Tint::Negative")
-                            .style(ButtonStyle::Tinted(TintColor::Negative)),
-                    ),
-            )
-    }
-
     fn render_text(&self, layer: ElevationIndex, cx: &ViewContext<Self>) -> impl IntoElement {
         let bg = layer.bg(cx);
 
@@ -502,7 +361,7 @@ impl ThemePreview {
             )
     }
 
-    fn render_components_page(&self, cx: &ViewContext<Self>) -> impl IntoElement {
+    fn render_components_page(&self, cx: &mut WindowContext) -> impl IntoElement {
         let layer = ElevationIndex::Surface;
 
         v_flex()
@@ -520,8 +379,6 @@ impl ThemePreview {
             .child(Indicator::render_component_previews(cx))
             .child(Icon::render_component_previews(cx))
             .child(Table::render_component_previews(cx))
-            .child(self.render_avatars(cx))
-            .child(self.render_buttons(layer, cx))
     }
 
     fn render_page_nav(&self, cx: &ViewContext<Self>) -> impl IntoElement {

crates/zed/Cargo.toml 🔗

@@ -52,6 +52,7 @@ file_icons.workspace = true
 fs.workspace = true
 futures.workspace = true
 git.workspace = true
+git_ui.workspace = true
 git_hosting_providers.workspace = true
 go_to_line.workspace = true
 gpui = { workspace = true, features = ["wayland", "x11", "font-kit"] }

crates/zed/src/main.rs 🔗

@@ -447,6 +447,7 @@ fn main() {
         outline::init(cx);
         project_symbols::init(cx);
         project_panel::init(Assets, cx);
+        git_ui::git_panel::init(cx);
         outline_panel::init(Assets, cx);
         tasks_ui::init(cx);
         snippets_ui::init(cx);

crates/zed/src/zed.rs 🔗

@@ -216,7 +216,7 @@ pub fn initialize_workspace(
             status_bar.add_left_item(activity_indicator, cx);
             status_bar.add_right_item(inline_completion_button, cx);
             status_bar.add_right_item(active_buffer_language, cx);
-                        status_bar.add_right_item(active_toolchain_language, cx);
+            status_bar.add_right_item(active_toolchain_language, cx);
             status_bar.add_right_item(vim_mode_indicator, cx);
             status_bar.add_right_item(cursor_position, cx);
         });
@@ -237,8 +237,11 @@ pub fn initialize_workspace(
 
         let release_channel = ReleaseChannel::global(cx);
         let assistant2_feature_flag = cx.wait_for_flag::<feature_flags::Assistant2FeatureFlag>();
+        let git_ui_feature_flag = cx.wait_for_flag::<feature_flags::GitUiFeatureFlag>();
 
         let prompt_builder = prompt_builder.clone();
+        let is_staff = cx.is_staff();
+
         cx.spawn(|workspace_handle, mut cx| async move {
             let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
             let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
@@ -276,13 +279,26 @@ pub fn initialize_workspace(
                 workspace.add_panel(chat_panel, cx);
                 workspace.add_panel(notification_panel, cx);
             })?;
+            let git_ui_enabled = git_ui_feature_flag.await || is_staff;
+
+            let git_panel = if git_ui_enabled {
+                Some(git_ui::git_panel::GitPanel::load(workspace_handle.clone(), cx.clone()).await?)
+            } else {
+                None
+            };
+
+            workspace_handle.update(&mut cx, |workspace, cx| {
+                if let Some(git_panel) = git_panel {
+                    workspace.add_panel(git_panel, cx);
+                }
+            })?;
+
             let is_assistant2_enabled =
                 if cfg!(test) || release_channel != ReleaseChannel::Dev {
                     false
                 } else {
                     assistant2_feature_flag.await
-                }
-            ;
+                };
 
             let (assistant_panel, assistant2_panel) = if is_assistant2_enabled {
                 let assistant2_panel =
@@ -303,6 +319,7 @@ pub fn initialize_workspace(
                 if let Some(assistant2_panel) = assistant2_panel {
                     workspace.add_panel(assistant2_panel, cx);
                 }
+
             })
         })
         .detach();