Compare commits
43 Commits
optional-c
...
gui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ae34d98ad | ||
|
|
392f597966 | ||
|
|
c3e801c586 | ||
|
|
86c4faa12f | ||
|
|
b545a02125 | ||
|
|
4762103c2a | ||
|
|
a982ff848f | ||
|
|
416e940bdd | ||
|
|
6899dab525 | ||
|
|
7f28b16825 | ||
|
|
196941a310 | ||
|
|
1e82854b7e | ||
|
|
cc81f91dfb | ||
|
|
be633875a5 | ||
|
|
ff28aed411 | ||
|
|
0bc287b863 | ||
|
|
d325d60a21 | ||
|
|
9b8bc2a2cd | ||
|
|
57978ece43 | ||
|
|
31de418b58 | ||
|
|
3376d63f57 | ||
|
|
87e49e4d91 | ||
|
|
2727be4df9 | ||
|
|
7498f508f1 | ||
|
|
0ebe6f78cf | ||
|
|
c327d9d3f4 | ||
|
|
60f6fe3454 | ||
|
|
c47d805806 | ||
|
|
14552268fd | ||
|
|
23eb000abe | ||
|
|
7abe14dba8 | ||
|
|
f0c4643bbd | ||
|
|
6883907163 | ||
|
|
49a62b6414 | ||
|
|
160b65771c | ||
|
|
214e997b72 | ||
|
|
825a353228 | ||
|
|
310cc3e315 | ||
|
|
75ff03e45b | ||
|
|
eb95efb0f3 | ||
|
|
059ebd88fe | ||
|
|
e8435a8915 | ||
|
|
e41e3f8463 |
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
1
assets/icons/git_branch.svg
Normal file
1
assets/icons/git_branch.svg
Normal 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 |
1
assets/icons/panel_left.svg
Normal file
1
assets/icons/panel_left.svg
Normal 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 |
1
assets/icons/panel_right.svg
Normal file
1
assets/icons/panel_right.svg
Normal 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 |
1
assets/icons/square_dot.svg
Normal file
1
assets/icons/square_dot.svg
Normal 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 |
1
assets/icons/square_minus.svg
Normal file
1
assets/icons/square_minus.svg
Normal 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 |
1
assets/icons/square_plus.svg
Normal file
1
assets/icons/square_plus.svg
Normal 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
34
crates/git_ui/Cargo.toml
Normal 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"]
|
||||
1512
crates/git_ui/src/git_panel.rs
Normal file
1512
crates/git_ui/src/git_panel.rs
Normal file
File diff suppressed because it is too large
Load Diff
949
crates/git_ui/src/git_ui.rs
Normal file
949
crates/git_ui/src/git_ui.rs
Normal 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,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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("::")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user