Git · ChangesList: conflicted, untracked and ignored files apart, each with its own actions; titles and tooltips read i18n (English and zh-CN built in)
This commit is contained in:
3 files changed
+166
-44
No files matched your search
+112
-44
@@ -9,6 +9,7 @@ use super::badges::GitStatusBadge;
|
|||||||
use crate::{
|
use crate::{
|
||||||
buttons::{Button, ButtonVariant, IconButton},
|
buttons::{Button, ButtonVariant, IconButton},
|
||||||
forms::{Checkbox, Input, Submit, TextInput},
|
forms::{Checkbox, Input, Submit, TextInput},
|
||||||
|
i18n,
|
||||||
lists::GitStatus,
|
lists::GitStatus,
|
||||||
primitives::IconName,
|
primitives::IconName,
|
||||||
theme::{ActiveTheme, ControlSize, Radius, TextSize},
|
theme::{ActiveTheme, ControlSize, Radius, TextSize},
|
||||||
@@ -36,12 +37,37 @@ pub enum ChangeAction {
|
|||||||
type OnAction = Rc<dyn Fn(&SharedString, ChangeAction, &mut Window, &mut App)>;
|
type OnAction = Rc<dyn Fn(&SharedString, ChangeAction, &mut Window, &mut App)>;
|
||||||
type OnAll = Rc<dyn Fn(bool, &mut Window, &mut App)>;
|
type OnAll = Rc<dyn Fn(bool, &mut Window, &mut App)>;
|
||||||
|
|
||||||
/// Files changed in the work tree, the staged apart: each with its status, its folder and what it changed; open, discard, stage or unstage it, or all at once.
|
/// Which part of the work tree a file is listed in.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum Section {
|
||||||
|
Conflicted,
|
||||||
|
Staged,
|
||||||
|
Unstaged,
|
||||||
|
Untracked,
|
||||||
|
Ignored,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Section {
|
||||||
|
fn key(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Conflicted => "git.changes.conflicted",
|
||||||
|
Self::Staged => "git.changes.staged",
|
||||||
|
Self::Unstaged => "git.changes.unstaged",
|
||||||
|
Self::Untracked => "git.changes.untracked",
|
||||||
|
Self::Ignored => "git.changes.ignored",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Files changed in the work tree, kept apart as conflicted, staged, changed, untracked and ignored: each with its status, its folder and what it changed; open, discard, stage or unstage it, or all at once.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct ChangesList {
|
pub struct ChangesList {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
|
conflicted: Vec<Changed>,
|
||||||
staged: Vec<Changed>,
|
staged: Vec<Changed>,
|
||||||
unstaged: Vec<Changed>,
|
unstaged: Vec<Changed>,
|
||||||
|
untracked: Vec<Changed>,
|
||||||
|
ignored: Vec<Changed>,
|
||||||
on_action: Option<OnAction>,
|
on_action: Option<OnAction>,
|
||||||
on_all: Option<OnAll>,
|
on_all: Option<OnAll>,
|
||||||
}
|
}
|
||||||
@@ -54,13 +80,34 @@ impl ChangesList {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
|
conflicted: Vec::new(),
|
||||||
staged: staged.into_iter().collect(),
|
staged: staged.into_iter().collect(),
|
||||||
unstaged: unstaged.into_iter().collect(),
|
unstaged: unstaged.into_iter().collect(),
|
||||||
|
untracked: Vec::new(),
|
||||||
|
ignored: Vec::new(),
|
||||||
on_action: None,
|
on_action: None,
|
||||||
on_all: None,
|
on_all: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Files both sides changed; listed first, opened only.
|
||||||
|
pub fn conflicted(mut self, files: impl IntoIterator<Item = Changed>) -> Self {
|
||||||
|
self.conflicted = files.into_iter().collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Files Git does not track yet; staged or discarded one by one.
|
||||||
|
pub fn untracked(mut self, files: impl IntoIterator<Item = Changed>) -> Self {
|
||||||
|
self.untracked = files.into_iter().collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Files Git ignores; listed last, opened only.
|
||||||
|
pub fn ignored(mut self, files: impl IntoIterator<Item = Changed>) -> Self {
|
||||||
|
self.ignored = files.into_iter().collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn on_action(
|
pub fn on_action(
|
||||||
mut self,
|
mut self,
|
||||||
handler: impl Fn(&SharedString, ChangeAction, &mut Window, &mut App) + 'static,
|
handler: impl Fn(&SharedString, ChangeAction, &mut Window, &mut App) + 'static,
|
||||||
@@ -77,18 +124,19 @@ impl ChangesList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ChangesList {
|
impl ChangesList {
|
||||||
fn row(&self, file: &Changed, staged: bool, cx: &App) -> AnyElement {
|
fn row(&self, file: &Changed, section: Section, cx: &App) -> AnyElement {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let colors = theme.colors.clone();
|
let colors = theme.colors.clone();
|
||||||
let (folder, name) = match file.path.rsplit_once('/') {
|
let (folder, name) = match file.path.rsplit_once('/') {
|
||||||
Some((folder, name)) => (folder.to_string(), name.to_string()),
|
Some((folder, name)) => (folder.to_string(), name.to_string()),
|
||||||
None => (String::new(), file.path.to_string()),
|
None => (String::new(), file.path.to_string()),
|
||||||
};
|
};
|
||||||
let group = SharedString::from(format!("change-{staged}-{}", file.path));
|
let group = SharedString::from(format!("change-{section:?}-{}", file.path));
|
||||||
let action = |key: &str, icon, words: &'static str, act: ChangeAction| {
|
let action = |key: &str, icon, words: &str, act: ChangeAction| {
|
||||||
|
let words = i18n::text(cx, words, &[]);
|
||||||
let (on_action, path) = (self.on_action.clone(), file.path.clone());
|
let (on_action, path) = (self.on_action.clone(), file.path.clone());
|
||||||
IconButton::new(
|
IconButton::new(
|
||||||
(self.id.clone(), format!("{key}-{staged}-{}", file.path)),
|
(self.id.clone(), format!("{key}-{section:?}-{}", file.path)),
|
||||||
icon,
|
icon,
|
||||||
)
|
)
|
||||||
.variant(ButtonVariant::Ghost)
|
.variant(ButtonVariant::Ghost)
|
||||||
@@ -103,7 +151,7 @@ impl ChangesList {
|
|||||||
};
|
};
|
||||||
let (open, path) = (self.on_action.clone(), file.path.clone());
|
let (open, path) = (self.on_action.clone(), file.path.clone());
|
||||||
div()
|
div()
|
||||||
.id((self.id.clone(), format!("row-{staged}-{}", file.path)))
|
.id((self.id.clone(), format!("row-{section:?}-{}", file.path)))
|
||||||
.group(group.clone())
|
.group(group.clone())
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -117,7 +165,7 @@ impl ChangesList {
|
|||||||
row.on_click(move |_, window, cx| open(&path, ChangeAction::Open, window, cx))
|
row.on_click(move |_, window, cx| open(&path, ChangeAction::Open, window, cx))
|
||||||
})
|
})
|
||||||
.child(GitStatusBadge::new(
|
.child(GitStatusBadge::new(
|
||||||
(self.id.clone(), format!("status-{staged}-{}", file.path)),
|
(self.id.clone(), format!("status-{section:?}-{}", file.path)),
|
||||||
file.status,
|
file.status,
|
||||||
))
|
))
|
||||||
.child(
|
.child(
|
||||||
@@ -152,22 +200,39 @@ impl ChangesList {
|
|||||||
.child(action(
|
.child(action(
|
||||||
"open",
|
"open",
|
||||||
IconName::FileText,
|
IconName::FileText,
|
||||||
"Open file",
|
"git.changes.open",
|
||||||
ChangeAction::Open,
|
ChangeAction::Open,
|
||||||
))
|
))
|
||||||
.when(!staged, |actions| {
|
.when(
|
||||||
|
matches!(section, Section::Unstaged | Section::Untracked),
|
||||||
|
|actions| {
|
||||||
|
actions.child(action(
|
||||||
|
"discard",
|
||||||
|
IconName::RotateCcw,
|
||||||
|
"git.changes.discard",
|
||||||
|
ChangeAction::Discard,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.when(section == Section::Staged, |actions| {
|
||||||
actions.child(action(
|
actions.child(action(
|
||||||
"discard",
|
"unstage",
|
||||||
IconName::RotateCcw,
|
IconName::Minus,
|
||||||
"Discard changes",
|
"git.changes.unstage",
|
||||||
ChangeAction::Discard,
|
ChangeAction::Unstage,
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.child(if staged {
|
.when(
|
||||||
action("unstage", IconName::Minus, "Unstage", ChangeAction::Unstage)
|
matches!(section, Section::Unstaged | Section::Untracked),
|
||||||
} else {
|
|actions| {
|
||||||
action("stage", IconName::Plus, "Stage", ChangeAction::Stage)
|
actions.child(action(
|
||||||
}),
|
"stage",
|
||||||
|
IconName::Plus,
|
||||||
|
"git.changes.stage",
|
||||||
|
ChangeAction::Stage,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
tabular(div().flex_none().text_color(colors.fg_subtle))
|
tabular(div().flex_none().text_color(colors.fg_subtle))
|
||||||
@@ -176,24 +241,28 @@ impl ChangesList {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn section(
|
fn section(&self, section: Section, files: &[Changed], cx: &App) -> Option<AnyElement> {
|
||||||
&self,
|
|
||||||
title: &'static str,
|
|
||||||
files: &[Changed],
|
|
||||||
staged: bool,
|
|
||||||
cx: &App,
|
|
||||||
) -> Option<AnyElement> {
|
|
||||||
if files.is_empty() {
|
if files.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let colors = theme.colors.clone();
|
let colors = theme.colors.clone();
|
||||||
let on_all = self.on_all.clone();
|
// Only staged and changed files move all at once.
|
||||||
let (icon, words) = if staged {
|
let all = match section {
|
||||||
(IconName::Minus, "Unstage all")
|
Section::Staged => Some((IconName::Minus, "git.changes.unstage_all", false)),
|
||||||
} else {
|
Section::Unstaged => Some((IconName::Plus, "git.changes.stage_all", true)),
|
||||||
(IconName::Plus, "Stage all")
|
_ => None,
|
||||||
};
|
};
|
||||||
|
let on_all = self.on_all.clone();
|
||||||
|
let button = all.map(|(icon, key, stage)| {
|
||||||
|
IconButton::new((self.id.clone(), format!("all-{section:?}")), icon)
|
||||||
|
.variant(ButtonVariant::Ghost)
|
||||||
|
.size(ControlSize::Sm)
|
||||||
|
.tooltip(i18n::text(cx, key, &[]))
|
||||||
|
.when_some(on_all, |button, on_all| {
|
||||||
|
button.on_click(move |_, window, cx| on_all(stage, window, cx))
|
||||||
|
})
|
||||||
|
});
|
||||||
Some(
|
Some(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -207,20 +276,16 @@ impl ChangesList {
|
|||||||
.py_1()
|
.py_1()
|
||||||
.text_size(theme.text_size(TextSize::Xs))
|
.text_size(theme.text_size(TextSize::Xs))
|
||||||
.text_color(colors.fg_muted)
|
.text_color(colors.fg_muted)
|
||||||
.child(div().flex_1().font_weight(FontWeight::MEDIUM).child(title))
|
|
||||||
.child(tabular(div()).child(files.len().to_string()))
|
|
||||||
.child(
|
.child(
|
||||||
IconButton::new((self.id.clone(), format!("all-{staged}")), icon)
|
div()
|
||||||
.variant(ButtonVariant::Ghost)
|
.flex_1()
|
||||||
.size(ControlSize::Sm)
|
.font_weight(FontWeight::MEDIUM)
|
||||||
.tooltip(words)
|
.child(i18n::text(cx, section.key(), &[])),
|
||||||
.when_some(on_all, |button, on_all| {
|
)
|
||||||
button
|
.child(tabular(div()).child(files.len().to_string()))
|
||||||
.on_click(move |_, window, cx| on_all(!staged, window, cx))
|
.children(button),
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.children(files.iter().map(|file| self.row(file, staged, cx)))
|
.children(files.iter().map(|file| self.row(file, section, cx)))
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -230,8 +295,11 @@ impl RenderOnce for ChangesList {
|
|||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let sections = [
|
let sections = [
|
||||||
self.section("Staged changes", &self.staged, true, cx),
|
self.section(Section::Conflicted, &self.conflicted, cx),
|
||||||
self.section("Changes", &self.unstaged, false, cx),
|
self.section(Section::Staged, &self.staged, cx),
|
||||||
|
self.section(Section::Unstaged, &self.unstaged, cx),
|
||||||
|
self.section(Section::Untracked, &self.untracked, cx),
|
||||||
|
self.section(Section::Ignored, &self.ignored, cx),
|
||||||
];
|
];
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
|
|||||||
@@ -75,3 +75,35 @@ fn a_blame_that_lags_its_code_draws_every_line(cx: &mut TestAppContext) {
|
|||||||
let (_, cx) = cx.add_window_view(|_, _| Behind);
|
let (_, cx) = cx.add_window_view(|_, _| Behind);
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every section of a changes list, each with one file.
|
||||||
|
struct Sorted;
|
||||||
|
|
||||||
|
impl Render for Sorted {
|
||||||
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
use crate::lists::GitStatus;
|
||||||
|
let file = |path: &str, status| super::Changed {
|
||||||
|
path: path.to_string().into(),
|
||||||
|
status,
|
||||||
|
added: 1,
|
||||||
|
removed: 0,
|
||||||
|
};
|
||||||
|
div().w(px(420.0)).child(
|
||||||
|
super::ChangesList::new(
|
||||||
|
"changes",
|
||||||
|
[file("a.rs", GitStatus::Modified)],
|
||||||
|
[file("b.rs", GitStatus::Modified)],
|
||||||
|
)
|
||||||
|
.conflicted([file("c.rs", GitStatus::Conflicted)])
|
||||||
|
.untracked([file("d.rs", GitStatus::Untracked)])
|
||||||
|
.ignored([file("target/", GitStatus::Untracked)]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn every_section_draws_with_its_own_words(cx: &mut TestAppContext) {
|
||||||
|
cx.update(Theme::init);
|
||||||
|
let (_, cx) = cx.add_window_view(|_, _| Sorted);
|
||||||
|
settle(cx);
|
||||||
|
}
|
||||||
@@ -67,6 +67,17 @@ const EN: &[(&str, &str)] = &[
|
|||||||
("pagination.jump", "Go to"),
|
("pagination.jump", "Go to"),
|
||||||
("signature.here", "Sign here"),
|
("signature.here", "Sign here"),
|
||||||
("signature.clear", "Clear"),
|
("signature.clear", "Clear"),
|
||||||
|
("git.changes.conflicted", "Conflicts"),
|
||||||
|
("git.changes.staged", "Staged changes"),
|
||||||
|
("git.changes.unstaged", "Changes"),
|
||||||
|
("git.changes.untracked", "Untracked"),
|
||||||
|
("git.changes.ignored", "Ignored"),
|
||||||
|
("git.changes.open", "Open file"),
|
||||||
|
("git.changes.discard", "Discard changes"),
|
||||||
|
("git.changes.stage", "Stage"),
|
||||||
|
("git.changes.unstage", "Unstage"),
|
||||||
|
("git.changes.stage_all", "Stage all"),
|
||||||
|
("git.changes.unstage_all", "Unstage all"),
|
||||||
];
|
];
|
||||||
|
|
||||||
const ZH_CN: &[(&str, &str)] = &[
|
const ZH_CN: &[(&str, &str)] = &[
|
||||||
@@ -132,6 +143,17 @@ const ZH_CN: &[(&str, &str)] = &[
|
|||||||
("pagination.jump", "跳至"),
|
("pagination.jump", "跳至"),
|
||||||
("signature.here", "在此签名"),
|
("signature.here", "在此签名"),
|
||||||
("signature.clear", "清除"),
|
("signature.clear", "清除"),
|
||||||
|
("git.changes.conflicted", "冲突"),
|
||||||
|
("git.changes.staged", "已暂存"),
|
||||||
|
("git.changes.unstaged", "改动"),
|
||||||
|
("git.changes.untracked", "未跟踪"),
|
||||||
|
("git.changes.ignored", "已忽略"),
|
||||||
|
("git.changes.open", "打开文件"),
|
||||||
|
("git.changes.discard", "丢弃改动"),
|
||||||
|
("git.changes.stage", "暂存"),
|
||||||
|
("git.changes.unstage", "取消暂存"),
|
||||||
|
("git.changes.stage_all", "全部暂存"),
|
||||||
|
("git.changes.unstage_all", "全部取消暂存"),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Ely's own message for `key` in `locale`, English for a locale Ely does not ship; fails on an unknown key.
|
/// Ely's own message for `key` in `locale`, English for a locale Ely does not ship; fails on an unknown key.
|
||||||
|
|||||||
Reference in new issue
Block a user