Compare commits

...

43 Commits

Author SHA1 Message Date
Nate Butler
0ae34d98ad Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
392f597966 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
c3e801c586 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
86c4faa12f Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
b545a02125 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
4762103c2a Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
a982ff848f Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
416e940bdd Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
6899dab525 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
7f28b16825 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
196941a310 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
1e82854b7e Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
cc81f91dfb Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
be633875a5 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
ff28aed411 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
0bc287b863 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
d325d60a21 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
9b8bc2a2cd Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
57978ece43 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
31de418b58 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
3376d63f57 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
87e49e4d91 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
2727be4df9 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
7498f508f1 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
0ebe6f78cf Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
c327d9d3f4 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
60f6fe3454 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
c47d805806 Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
14552268fd Checkpoint 2024-12-11 11:10:03 -05:00
Nate Butler
23eb000abe panel 2024-12-11 11:10:03 -05:00
Nate Butler
7abe14dba8 wip 2024-12-11 11:09:03 -05:00
Nate Butler
f0c4643bbd Revert "wip - broken"
This reverts commit c09cf6eee0039f0b43d6cc7d5087646c842f984e.
2024-12-11 11:09:03 -05:00
Nate Butler
6883907163 wip - broken 2024-12-11 11:09:03 -05:00
Nate Butler
49a62b6414 Update git_ui.rs 2024-12-11 11:09:02 -05:00
Nate Butler
160b65771c headers in list 2024-12-11 11:09:02 -05:00
Nate Butler
214e997b72 Organize 2024-12-11 11:09:02 -05:00
Nate Butler
825a353228 Update git_ui.rs 2024-12-11 11:09:02 -05:00
Nate Butler
310cc3e315 add kb movement 2024-12-11 11:09:02 -05:00
Nate Butler
75ff03e45b wip basic list 2024-12-11 11:09:02 -05:00
Nate Butler
eb95efb0f3 clean up unused 2024-12-11 11:09:02 -05:00
Nate Butler
059ebd88fe new icons 2024-12-11 11:09:02 -05:00
Nate Butler
e8435a8915 Update git_ui.rs 2024-12-11 11:07:12 -05:00
Nate Butler
e41e3f8463 wip 2024-12-11 11:07:12 -05:00
23 changed files with 2641 additions and 25 deletions

20
Cargo.lock generated
View File

@@ -5133,6 +5133,25 @@ dependencies = [
"util",
]
[[package]]
name = "git_ui"
version = "0.1.0"
dependencies = [
"editor",
"git",
"gpui",
"itertools 0.13.0",
"menu",
"project",
"serde",
"settings",
"smallvec",
"theme",
"ui",
"windows 0.58.0",
"workspace",
]
[[package]]
name = "glob"
version = "0.3.1"
@@ -15985,6 +16004,7 @@ dependencies = [
"futures 0.3.31",
"git",
"git_hosting_providers",
"git_ui",
"go_to_line",
"gpui",
"http_client",

View File

@@ -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" }

View File

@@ -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>

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -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>

After

Width:  |  Height:  |  Size: 289 B

View File

@@ -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>

After

Width:  |  Height:  |  Size: 291 B

View File

@@ -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>

After

Width:  |  Height:  |  Size: 301 B

View File

@@ -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>

After

Width:  |  Height:  |  Size: 291 B

View File

@@ -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>

After

Width:  |  Height:  |  Size: 309 B

34
crates/git_ui/Cargo.toml Normal file
View File

@@ -0,0 +1,34 @@
[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
itertools = { workspace = true, optional = true }
menu.workspace = true
serde.workspace = true
settings.workspace = true
theme.workspace = true
workspace.workspace = true
ui.workspace = true
project.workspace = true
smallvec.workspace = true
git.workspace = true
editor.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true
[features]
default = []
stories = ["dep:itertools"]

File diff suppressed because it is too large Load Diff

949
crates/git_ui/src/git_ui.rs Normal file
View File

@@ -0,0 +1,949 @@
use editor::Editor;
use git::repository::GitFileStatus;
use gpui::*;
use ui::{prelude::*, ElevationIndex, IconButtonShape};
use ui::{Disclosure, Divider};
use workspace::item::TabContentParams;
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
pub mod git_panel;
actions!(
vcs_status,
[
Deploy,
DiscardAll,
StageAll,
DiscardSelected,
StageSelected,
UnstageSelected,
UnstageAll,
FilesChanged
]
);
#[derive(Debug, Clone)]
pub struct ChangedFile {
pub staged: bool,
pub file_path: SharedString,
pub lines_added: usize,
pub lines_removed: usize,
pub status: GitFileStatus,
}
pub struct GitLines {
pub added: usize,
pub removed: usize,
}
#[derive(IntoElement)]
pub struct ChangedFileHeader {
id: ElementId,
file: ChangedFile,
is_selected: bool,
}
impl ChangedFileHeader {
fn new(id: impl Into<ElementId>, file: ChangedFile, is_selected: bool) -> Self {
Self {
id: id.into(),
file,
is_selected,
}
}
fn icon_for_status(&self) -> impl IntoElement {
let (icon_name, color) = match self.file.status {
GitFileStatus::Added => (IconName::SquarePlus, Color::Created),
GitFileStatus::Modified => (IconName::SquareDot, Color::Modified),
GitFileStatus::Conflict => (IconName::SquareMinus, Color::Conflict),
};
Icon::new(icon_name).size(IconSize::Small).color(color)
}
}
impl RenderOnce for ChangedFileHeader {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let disclosure_id = ElementId::Name(format!("{}-file-disclosure", self.id.clone()).into());
let file_path = self.file.file_path.clone();
h_flex()
.id(self.id.clone())
.justify_between()
.w_full()
.when(!self.is_selected, |this| {
this.hover(|this| this.bg(cx.theme().colors().ghost_element_hover))
})
.cursor(CursorStyle::PointingHand)
.when(self.is_selected, |this| {
this.bg(cx.theme().colors().ghost_element_active)
})
.group("")
.rounded_sm()
.px_2()
.py_1p5()
.child(
h_flex()
.gap_2()
.child(Disclosure::new(disclosure_id, false))
.child(self.icon_for_status())
.child(Label::new(file_path).size(LabelSize::Small))
.child(
h_flex()
.gap_1()
.when(self.file.lines_added > 0, |this| {
this.child(
Label::new(format!("+{}", self.file.lines_added))
.color(Color::Created)
.size(LabelSize::Small),
)
})
.when(self.file.lines_removed > 0, |this| {
this.child(
Label::new(format!("-{}", self.file.lines_removed))
.color(Color::Deleted)
.size(LabelSize::Small),
)
}),
),
)
.child(
h_flex()
.gap_2()
.child(
IconButton::new("more-menu", IconName::EllipsisVertical)
.shape(IconButtonShape::Square)
.size(ButtonSize::Compact)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted),
)
.child(
IconButton::new("remove-file", IconName::X)
.shape(IconButtonShape::Square)
.size(ButtonSize::Compact)
.icon_size(IconSize::XSmall)
.icon_color(Color::Error)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.on_click(move |_, cx| cx.dispatch_action(Box::new(DiscardSelected))),
)
.child(
IconButton::new("check-file", IconName::Check)
.shape(IconButtonShape::Square)
.size(ButtonSize::Compact)
.icon_size(IconSize::XSmall)
.icon_color(Color::Accent)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::Background)
.on_click(move |_, cx| {
if self.file.staged {
cx.dispatch_action(Box::new(UnstageSelected))
} else {
cx.dispatch_action(Box::new(StageSelected))
}
}),
),
)
}
}
#[derive(IntoElement)]
pub struct GitProjectOverview {
id: ElementId,
project_status: Model<GitProjectStatus>,
}
impl GitProjectOverview {
pub fn new(id: impl Into<ElementId>, project_status: Model<GitProjectStatus>) -> Self {
Self {
id: id.into(),
project_status,
}
}
pub fn toggle_file_list(&self, cx: &mut WindowContext) {
self.project_status.update(cx, |status, cx| {
status.show_list = !status.show_list;
cx.notify();
});
}
}
impl RenderOnce for GitProjectOverview {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let status = self.project_status.read(cx);
let changed_files: SharedString =
format!("{} Changed files", status.changed_file_count()).into();
let added_label: Option<SharedString> = (status.lines_changed.added > 0)
.then(|| format!("+{}", status.lines_changed.added).into());
let removed_label: Option<SharedString> = (status.lines_changed.removed > 0)
.then(|| format!("-{}", status.lines_changed.removed).into());
let total_label: SharedString = "total lines changed".into();
h_flex()
.id(self.id.clone())
.w_full()
.bg(cx.theme().colors().elevated_surface_background)
.px_2()
.py_2p5()
.gap_2()
.child(
IconButton::new("open-sidebar", IconName::PanelLeft)
.selected(self.project_status.read(cx).show_list)
.icon_color(Color::Muted)
.on_click(move |_, cx| self.toggle_file_list(cx)),
)
.child(
h_flex()
.gap_4()
.child(Label::new(changed_files).size(LabelSize::Small))
.child(
h_flex()
.gap_1()
.when(added_label.is_some(), |this| {
this.child(
Label::new(added_label.unwrap())
.color(Color::Created)
.size(LabelSize::Small),
)
})
.when(removed_label.is_some(), |this| {
this.child(
Label::new(removed_label.unwrap())
.color(Color::Deleted)
.size(LabelSize::Small),
)
})
.child(
Label::new(total_label)
.color(Color::Muted)
.size(LabelSize::Small),
),
),
)
}
}
#[derive(IntoElement)]
pub struct GitStagingControls {
id: ElementId,
project_status: Model<GitProjectStatus>,
is_staged: bool,
is_selected: bool,
}
impl GitStagingControls {
pub fn new(
id: impl Into<ElementId>,
project_status: Model<GitProjectStatus>,
is_staged: bool,
is_selected: bool,
) -> Self {
Self {
id: id.into(),
project_status,
is_staged,
is_selected,
}
}
}
impl RenderOnce for GitStagingControls {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let status = self.project_status.read(cx);
let (staging_type, count) = if self.is_staged {
("Staged", status.staged_count())
} else {
("Unstaged", status.unstaged_count())
};
let is_expanded = if self.is_staged {
status.staged_expanded
} else {
status.unstaged_expanded
};
let label: SharedString = format!("{} Changes: {}", staging_type, count).into();
h_flex()
.id(self.id.clone())
.hover(|this| this.bg(cx.theme().colors().ghost_element_hover))
.on_click(move |_, cx| {
self.project_status.update(cx, |status, cx| {
if self.is_staged {
status.staged_expanded = !status.staged_expanded;
} else {
status.unstaged_expanded = !status.unstaged_expanded;
}
cx.notify();
})
})
.justify_between()
.w_full()
.map(|this| {
if self.is_selected {
this.bg(cx.theme().colors().ghost_element_active)
} else {
this.bg(cx.theme().colors().elevated_surface_background)
}
})
.px_3()
.py_2()
.child(
h_flex()
.gap_2()
.child(Disclosure::new(self.id.clone(), is_expanded))
.child(Label::new(label).size(LabelSize::Small)),
)
.child(h_flex().gap_2().map(|this| {
if !self.is_staged {
this.child(
Button::new(
ElementId::Name(format!("{}-discard", self.id.clone()).into()),
"Discard All",
)
.style(ButtonStyle::Filled)
.layer(ui::ElevationIndex::ModalSurface)
.size(ButtonSize::Compact)
.label_size(LabelSize::Small)
.icon(IconName::X)
.icon_position(IconPosition::Start)
.icon_color(Color::Muted)
.disabled(status.changed_file_count() == 0)
.on_click(move |_, cx| cx.dispatch_action(Box::new(DiscardAll))),
)
.child(
Button::new(
ElementId::Name(format!("{}-stage", self.id.clone()).into()),
"Stage All",
)
.style(ButtonStyle::Filled)
.size(ButtonSize::Compact)
.label_size(LabelSize::Small)
.layer(ui::ElevationIndex::ModalSurface)
.icon(IconName::Check)
.icon_position(IconPosition::Start)
.icon_color(Color::Muted)
.disabled(status.no_unstaged())
.on_click(move |_, cx| cx.dispatch_action(Box::new(StageAll))),
)
} else {
this.child(
Button::new(
ElementId::Name(format!("{}-unstage", self.id.clone()).into()),
"Unstage All",
)
.layer(ui::ElevationIndex::ModalSurface)
.icon(IconName::Check)
.icon_position(IconPosition::Start)
.icon_color(Color::Muted)
.disabled(status.no_staged())
.on_click(move |_, cx| cx.dispatch_action(Box::new(UnstageAll))),
)
}
}))
}
}
pub struct GitProjectStatus {
unstaged_files: Vec<ChangedFile>,
staged_files: Vec<ChangedFile>,
lines_changed: GitLines,
staged_expanded: bool,
unstaged_expanded: bool,
show_list: bool,
selected_index: usize,
}
impl GitProjectStatus {
fn new(changed_files: Vec<ChangedFile>) -> Self {
let (unstaged_files, staged_files): (Vec<_>, Vec<_>) =
changed_files.into_iter().partition(|f| !f.staged);
let lines_changed = GitLines {
added: unstaged_files
.iter()
.chain(staged_files.iter())
.map(|f| f.lines_added)
.sum(),
removed: unstaged_files
.iter()
.chain(staged_files.iter())
.map(|f| f.lines_removed)
.sum(),
};
Self {
unstaged_files,
staged_files,
lines_changed,
staged_expanded: true,
unstaged_expanded: true,
show_list: false,
selected_index: 0,
}
}
fn changed_file_count(&self) -> usize {
self.unstaged_files.len() + self.staged_files.len()
}
fn unstaged_count(&self) -> usize {
self.unstaged_files.len()
}
fn staged_count(&self) -> usize {
self.staged_files.len()
}
fn total_item_count(&self) -> usize {
self.changed_file_count() + 2 // +2 for the two controls
}
fn no_unstaged(&self) -> bool {
self.unstaged_files.is_empty()
}
fn all_unstaged(&self) -> bool {
self.staged_files.is_empty()
}
fn no_staged(&self) -> bool {
self.staged_files.is_empty()
}
fn all_staged(&self) -> bool {
self.unstaged_files.is_empty()
}
fn update_lines_changed(&mut self) {
self.lines_changed = GitLines {
added: self
.unstaged_files
.iter()
.chain(self.staged_files.iter())
.map(|f| f.lines_added)
.sum(),
removed: self
.unstaged_files
.iter()
.chain(self.staged_files.iter())
.map(|f| f.lines_removed)
.sum(),
};
}
fn discard_all(&mut self) {
self.unstaged_files.clear();
self.staged_files.clear();
self.update_lines_changed();
}
fn stage_all(&mut self) {
self.staged_files.extend(self.unstaged_files.drain(..));
self.update_lines_changed();
}
fn unstage_all(&mut self) {
self.unstaged_files.extend(self.staged_files.drain(..));
self.update_lines_changed();
}
fn discard_selected(&mut self) {
let total_len = self.unstaged_files.len() + self.staged_files.len();
if self.selected_index > 0 && self.selected_index <= total_len {
if self.selected_index <= self.unstaged_files.len() {
self.unstaged_files.remove(self.selected_index - 1);
} else {
self.staged_files
.remove(self.selected_index - 1 - self.unstaged_files.len());
}
self.update_lines_changed();
}
}
fn stage_selected(&mut self) {
if self.selected_index > 0 && self.selected_index <= self.unstaged_files.len() {
let file = self.unstaged_files.remove(self.selected_index - 1);
self.staged_files.push(file);
self.update_lines_changed();
}
}
fn unstage_selected(&mut self) {
let unstaged_len = self.unstaged_files.len();
if self.selected_index > unstaged_len && self.selected_index <= self.total_item_count() - 2
{
let file = self
.staged_files
.remove(self.selected_index - 1 - unstaged_len);
self.unstaged_files.push(file);
self.update_lines_changed();
}
}
}
#[derive(Clone)]
pub struct ProjectStatusTab {
id: ElementId,
focus_handle: FocusHandle,
status: Model<GitProjectStatus>,
list_state: ListState,
}
impl ProjectStatusTab {
pub fn new(id: impl Into<ElementId>, cx: &mut ViewContext<Self>) -> Self {
let changed_files = static_changed_files();
let status = cx.new_model(|_| GitProjectStatus::new(changed_files));
let status_clone = status.clone();
let list_state = ListState::new(
status.read(cx).total_item_count(),
gpui::ListAlignment::Top,
px(10.),
move |ix, cx| {
let status = status_clone.read(cx);
let is_selected = status.selected_index == ix;
if ix == 0 {
GitStagingControls::new(
"unstaged-controls",
status_clone.clone(),
false,
is_selected,
)
.into_any_element()
} else if ix == status.total_item_count() - 1 {
GitStagingControls::new(
"staged-controls",
status_clone.clone(),
true,
is_selected,
)
.into_any_element()
} else {
let file_ix = ix - 1;
let file = if file_ix < status.unstaged_count() {
status.unstaged_files.get(file_ix)
} else {
status.staged_files.get(file_ix - status.unstaged_count())
};
file.map(|file| {
ChangedFileHeader::new(
ElementId::Name(format!("file-{}", file_ix).into()),
file.clone(),
is_selected,
)
.into_any_element()
})
.unwrap_or_else(|| div().into_any_element())
}
},
);
Self {
id: id.into(),
focus_handle: cx.focus_handle(),
status,
list_state,
}
}
fn recreate_list_state(&mut self, cx: &mut ViewContext<Self>) {
let status = self.status.read(cx);
let status_clone = self.status.clone();
self.list_state = ListState::new(
status.total_item_count(),
gpui::ListAlignment::Top,
px(10.),
move |ix, cx| {
let is_selected = status_clone.read(cx).selected_index == ix;
if ix == 0 {
GitStagingControls::new(
"unstaged-controls",
status_clone.clone(),
false,
is_selected,
)
.into_any_element()
} else if ix == status_clone.read(cx).total_item_count() - 1 {
GitStagingControls::new(
"staged-controls",
status_clone.clone(),
true,
is_selected,
)
.into_any_element()
} else {
let file_ix = ix - 1;
let status = status_clone.read(cx);
let file = if file_ix < status.unstaged_count() {
status.unstaged_files.get(file_ix)
} else {
status.staged_files.get(file_ix - status.unstaged_count())
};
file.map(|file| {
ChangedFileHeader::new(
ElementId::Name(format!("file-{}", file_ix).into()),
file.clone(),
is_selected,
)
.into_any_element()
})
.unwrap_or_else(|| div().into_any_element())
}
},
);
}
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
if let Some(existing) = workspace.item_of_type::<ProjectStatusTab>(cx) {
workspace.activate_item(&existing, true, true, cx);
} else {
let status_tab = cx.new_view(|cx| Self::new("project-status-tab", cx));
workspace.add_item_to_active_pane(Box::new(status_tab), None, true, cx);
}
}
fn discard_all(&mut self, _: &DiscardAll, cx: &mut ViewContext<Self>) {
self.status.update(cx, |status, _| {
status.discard_all();
});
self.recreate_list_state(cx);
cx.notify();
}
fn stage_all(&mut self, _: &StageAll, cx: &mut ViewContext<Self>) {
self.status.update(cx, |status, _| {
status.stage_all();
});
self.recreate_list_state(cx);
cx.notify();
}
fn unstage_all(&mut self, _: &UnstageAll, cx: &mut ViewContext<Self>) {
self.status.update(cx, |status, _| {
status.unstage_all();
});
self.recreate_list_state(cx);
cx.notify();
}
fn discard_selected(&mut self, _: &DiscardSelected, cx: &mut ViewContext<Self>) {
self.status.update(cx, |status, _| {
status.discard_selected();
});
self.recreate_list_state(cx);
cx.notify();
}
fn stage_selected(&mut self, _: &StageSelected, cx: &mut ViewContext<Self>) {
self.status.update(cx, |status, _| {
status.stage_selected();
});
self.recreate_list_state(cx);
cx.notify();
}
fn unstage_selected(&mut self, _: &UnstageSelected, cx: &mut ViewContext<Self>) {
self.status.update(cx, |status, _| {
status.unstage_selected();
});
self.recreate_list_state(cx);
cx.notify();
}
fn selected_index(&self, cx: &WindowContext) -> usize {
self.status.read(cx).selected_index
}
pub fn set_selected_index(
&mut self,
index: usize,
jump_to_index: bool,
cx: &mut ViewContext<Self>,
) {
self.status.update(cx, |status, _| {
status.selected_index = index.min(status.total_item_count() - 1);
});
if jump_to_index {
self.jump_to_cell(index, cx);
}
}
pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
let current_index = self.status.read(cx).selected_index;
let total_count = self.status.read(cx).total_item_count();
let new_index = (current_index + 1).min(total_count - 1);
self.set_selected_index(new_index, true, cx);
cx.notify();
}
pub fn select_previous(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
let current_index = self.status.read(cx).selected_index;
let new_index = current_index.saturating_sub(1);
self.set_selected_index(new_index, true, cx);
cx.notify();
}
pub fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
self.set_selected_index(0, true, cx);
cx.notify();
}
pub fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
let total_count = self.status.read(cx).total_item_count();
self.set_selected_index(total_count - 1, true, cx);
cx.notify();
}
fn jump_to_cell(&mut self, index: usize, _cx: &mut ViewContext<Self>) {
self.list_state.scroll_to_reveal_item(index);
}
}
impl Render for ProjectStatusTab {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let project_status = self.status.read(cx);
h_flex()
.id(self.id.clone())
.key_context("vcs_status")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::discard_all))
.on_action(cx.listener(Self::stage_all))
.on_action(cx.listener(Self::unstage_all))
.on_action(cx.listener(Self::discard_selected))
.on_action(cx.listener(Self::stage_selected))
.on_action(cx.listener(Self::unstage_selected))
.on_action(cx.listener(|this, &FilesChanged, cx| this.recreate_list_state(cx)))
.flex_1()
.size_full()
.overflow_hidden()
.when(project_status.show_list, |this| {
this.child(
v_flex()
.bg(ElevationIndex::Surface.bg(cx))
.border_r_1()
.border_color(cx.theme().colors().border)
.w(px(280.))
.flex_none()
.h_full()
.child("sidebar"),
)
})
.child(
v_flex()
.h_full()
.flex_1()
.overflow_hidden()
.bg(ElevationIndex::Surface.bg(cx))
.child(GitProjectOverview::new(
"project-overview",
self.status.clone(),
))
.child(Divider::horizontal_dashed())
.child(list(self.list_state.clone()).size_full()),
)
}
}
impl EventEmitter<()> for ProjectStatusTab {}
impl FocusableView for ProjectStatusTab {
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl workspace::Item for ProjectStatusTab {
type Event = ();
fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
fn tab_content(&self, _params: TabContentParams, _cx: &WindowContext) -> AnyElement {
Label::new("Project Status").into_any_element()
}
fn telemetry_event_text(&self) -> Option<&'static str> {
None
}
fn is_singleton(&self, _cx: &AppContext) -> bool {
true
}
}
pub struct GitStatusIndicator {
active_editor: Option<WeakView<Editor>>,
workspace: WeakView<Workspace>,
current_status: Option<GitProjectStatus>,
_observe_active_editor: Option<Subscription>,
}
impl Render for GitStatusIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
h_flex().h(rems(1.375)).gap_2().child(
IconButton::new("git-status-indicator", IconName::GitBranch).on_click(cx.listener(
|this, _, cx| {
if let Some(workspace) = this.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
ProjectStatusTab::deploy(workspace, &Default::default(), cx)
})
}
},
)),
)
}
}
impl GitStatusIndicator {
pub fn new(workspace: &Workspace, _: &mut ViewContext<Self>) -> Self {
Self {
active_editor: None,
workspace: workspace.weak_handle(),
current_status: None,
_observe_active_editor: None,
}
}
}
impl EventEmitter<ToolbarItemEvent> for GitStatusIndicator {}
impl StatusItemView for GitStatusIndicator {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
self.active_editor = Some(editor.downgrade());
} else {
self.active_editor = None;
self.current_status = None;
self._observe_active_editor = None;
}
cx.notify();
}
}
fn static_changed_files() -> Vec<ChangedFile> {
vec![
ChangedFile {
staged: false,
file_path: "path/to/changed_file1".into(),
lines_added: 10,
lines_removed: 5,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file2".into(),
lines_added: 8,
lines_removed: 0,
status: GitFileStatus::Added,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file3".into(),
lines_added: 15,
lines_removed: 20,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file4".into(),
lines_added: 5,
lines_removed: 0,
status: GitFileStatus::Added,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file5".into(),
lines_added: 12,
lines_removed: 7,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file6".into(),
lines_added: 0,
lines_removed: 12,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file7".into(),
lines_added: 7,
lines_removed: 3,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file8".into(),
lines_added: 2,
lines_removed: 0,
status: GitFileStatus::Added,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file9".into(),
lines_added: 18,
lines_removed: 15,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file10".into(),
lines_added: 22,
lines_removed: 0,
status: GitFileStatus::Added,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file11".into(),
lines_added: 5,
lines_removed: 5,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file12".into(),
lines_added: 7,
lines_removed: 0,
status: GitFileStatus::Added,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file13".into(),
lines_added: 3,
lines_removed: 11,
status: GitFileStatus::Modified,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file14".into(),
lines_added: 30,
lines_removed: 0,
status: GitFileStatus::Added,
},
ChangedFile {
staged: false,
file_path: "path/to/changed_file15".into(),
lines_added: 12,
lines_removed: 22,
status: GitFileStatus::Modified,
},
]
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,47 @@ 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))
}
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)),
)
}
}

View File

@@ -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",

View File

@@ -200,6 +200,8 @@ pub enum IconName {
GenericRestore,
Github,
Globe,
GitBranch,
Github,
Hash,
HistoryRerun,
Indicator,
@@ -224,6 +226,8 @@ pub enum IconName {
Option,
PageDown,
PageUp,
PanelLeft,
PanelRight,
Pencil,
Person,
PhoneIncoming,
@@ -233,6 +237,9 @@ pub enum IconName {
PocketKnife,
Public,
PullRequest,
PhoneIncoming,
PanelLeft,
PanelRight,
Quote,
RefreshTitle,
Regex,
@@ -267,6 +274,9 @@ pub enum IconName {
SparkleFilled,
Spinner,
Split,
SquareDot,
SquareMinus,
SquarePlus,
Star,
StarFilled,
Stop,
@@ -497,7 +507,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 +549,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 +668,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,

View File

@@ -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",

View File

@@ -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(

View File

@@ -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("::")

View File

@@ -502,7 +502,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 +520,8 @@ 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))
// .child(self.render_avatars(cx))
// .child(self.render_buttons(layer, cx))
}
fn render_page_nav(&self, cx: &ViewContext<Self>) -> impl IntoElement {

View File

@@ -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"] }

View File

@@ -442,6 +442,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);

View File

@@ -21,6 +21,9 @@ use editor::ProposedChangesEditorToolbar;
use editor::{scroll::Autoscroll, Editor, MultiBuffer};
use feature_flags::FeatureFlagAppExt;
use futures::{channel::mpsc, select_biased, StreamExt};
use git_ui::git_panel::GitPanel;
use git_ui::GitStatusIndicator;
use git_ui::{git_panel, GitStatusIndicator};
use gpui::{
actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, MenuItem,
PathPromptOptions, PromptLevel, ReadGlobal, Task, TitlebarOptions, View, ViewContext,
@@ -202,6 +205,8 @@ pub fn initialize_workspace(
let diagnostic_summary =
cx.new_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
let git_indicator =
cx.new_view(|cx| GitStatusIndicator::new(workspace, cx));
let activity_indicator =
activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
let active_buffer_language =
@@ -212,6 +217,7 @@ pub fn initialize_workspace(
let cursor_position =
cx.new_view(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
workspace.status_bar().update(cx, |status_bar, cx| {
status_bar.add_left_item(git_indicator, cx);
status_bar.add_left_item(diagnostic_summary, cx);
status_bar.add_left_item(activity_indicator, cx);
status_bar.add_right_item(inline_completion_button, cx);
@@ -239,7 +245,14 @@ pub fn initialize_workspace(
let assistant2_feature_flag = cx.wait_for_flag::<feature_flags::Assistant2FeatureFlag>();
let prompt_builder = prompt_builder.clone();
let git_panel = cx.new_view(|cx| GitPanel::new("git-panel", cx));
cx.spawn(|workspace_handle, mut cx| async move {
let assistant_panel =
assistant::AssistantPanel::load(workspace_handle.clone(), prompt_builder, cx.clone());
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
@@ -269,6 +282,9 @@ pub fn initialize_workspace(
)?;
workspace_handle.update(&mut cx, |workspace, cx| {
workspace.add_panel(git_panel, cx);
workspace.add_panel(assistant_panel, cx);
workspace.add_panel(project_panel, cx);
workspace.add_panel(outline_panel, cx);
workspace.add_panel(terminal_panel, cx);