Git · CommitList, FileHistory, BlameView draw only rows in view, rows are stops with list semantics; CommitList loads more near its end; CommitInput words read i18n
This commit is contained in:
9 files changed
+472
-221
No files matched your search
@@ -37,7 +37,14 @@ pub fn blame(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
];
|
];
|
||||||
let history_rows: Vec<_> = history()
|
let history_rows: Vec<_> = history()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.zip([(12, 4), (3, 3), (9, 1), (18, 0), (2, 1), (40, 0)])
|
.zip([
|
||||||
|
Some((12, 4)),
|
||||||
|
Some((3, 3)),
|
||||||
|
Some((9, 1)),
|
||||||
|
Some((18, 0)),
|
||||||
|
None,
|
||||||
|
Some((40, 0)),
|
||||||
|
])
|
||||||
.collect();
|
.collect();
|
||||||
let (pick, select) = (said.clone(), said.clone());
|
let (pick, select) = (said.clone(), said.clone());
|
||||||
section(
|
section(
|
||||||
@@ -46,7 +53,7 @@ pub fn blame(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().w(px(840.)).child(
|
div().w(px(840.)).h(px(240.)).child(
|
||||||
BlameView::new("git-blame", CODE, [0, 1, 1, 1, 0, 0, 2, 2, 0, 0], blames)
|
BlameView::new("git-blame", CODE, [0, 1, 1, 1, 0, 0, 2, 2, 0, 0], blames)
|
||||||
.current(2)
|
.current(2)
|
||||||
.on_commit(move |commit, _, cx| set(&pick, Some(format!("Picked {}.", &commit[..7]).into()), cx)),
|
.on_commit(move |commit, _, cx| set(&pick, Some(format!("Picked {}.", &commit[..7]).into()), cx)),
|
||||||
@@ -54,8 +61,8 @@ pub fn blame(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
)
|
)
|
||||||
.children(told.map(Caption::new))
|
.children(told.map(Caption::new))
|
||||||
.child(
|
.child(
|
||||||
div().w(px(520.)).child(
|
div().w(px(520.)).h(px(360.)).child(
|
||||||
FileHistory::new("git-file-history", history_rows)
|
FileHistory::new("git-file-history", std::rc::Rc::new(history_rows))
|
||||||
.selected(history()[1].id.clone())
|
.selected(history()[1].id.clone())
|
||||||
.on_pick(move |commit, _, cx| set(&select, Some(format!("Opened {}.", &commit[..7]).into()), cx)),
|
.on_pick(move |commit, _, cx| set(&select, Some(format!("Opened {}.", &commit[..7]).into()), cx)),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ use ely_gpui_component::{
|
|||||||
forms::TextInput,
|
forms::TextInput,
|
||||||
git::{
|
git::{
|
||||||
Branch, BranchList, BranchSelector, ChangeAction, Changed, ChangesList, Commit,
|
Branch, BranchList, BranchSelector, ChangeAction, Changed, ChangesList, Commit,
|
||||||
CommitInput, CommitList, GitTag, Stash, StashAction, StashList, TagList,
|
CommitInput, CommitList, GitTag, GraphRow, Stash, StashAction, StashList, TagList, lanes,
|
||||||
},
|
},
|
||||||
lists::GitStatus,
|
lists::GitStatus,
|
||||||
typography::Caption,
|
typography::Caption,
|
||||||
};
|
};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -210,6 +212,21 @@ pub(super) fn history() -> Vec<Commit> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The sample history's lanes.
|
||||||
|
fn graph() -> Vec<GraphRow> {
|
||||||
|
let commits = history();
|
||||||
|
let parents: Vec<Vec<&str>> = commits
|
||||||
|
.iter()
|
||||||
|
.map(|commit| commit.parents.iter().map(|id| id.as_ref()).collect())
|
||||||
|
.collect();
|
||||||
|
lanes(
|
||||||
|
commits
|
||||||
|
.iter()
|
||||||
|
.zip(&parents)
|
||||||
|
.map(|(commit, parents)| (commit.id.as_ref(), parents.as_slice())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn commits(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
pub fn commits(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||||
let picked = keep("git-picked", || history()[0].id.clone(), window, cx);
|
let picked = keep("git-picked", || history()[0].id.clone(), window, cx);
|
||||||
let now = picked.read(cx).clone();
|
let now = picked.read(cx).clone();
|
||||||
@@ -220,9 +237,9 @@ pub fn commits(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().w(px(840.)).child(
|
div().w(px(840.)).h(px(320.)).child(
|
||||||
CommitList::new("git-graph", history())
|
CommitList::new("git-graph", Rc::new(history()))
|
||||||
.graph()
|
.graph(Rc::new(graph()))
|
||||||
.selected(now)
|
.selected(now)
|
||||||
.on_pick(move |id, _, cx| set(&pick, id.clone(), cx)),
|
.on_pick(move |id, _, cx| set(&pick, id.clone(), cx)),
|
||||||
),
|
),
|
||||||
|
|||||||
+133
-121
@@ -1,14 +1,15 @@
|
|||||||
use std::{ops::Range, rc::Rc};
|
use std::{ops::Range, rc::Rc};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, ElementId, FontWeight, InteractiveElement, IntoElement, ParentElement,
|
App, ElementId, FontWeight, InteractiveElement, IntoElement, ParentElement, RenderOnce, Role,
|
||||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, StyledText, Window, div,
|
SharedString, StatefulInteractiveElement, Styled, StyledText, Window, div, prelude::*,
|
||||||
prelude::*, relative,
|
relative, uniform_list,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data_display::Avatar,
|
data_display::Avatar,
|
||||||
editor::code_colors,
|
editor::code_colors,
|
||||||
|
primitives::FocusRing,
|
||||||
theme::{ActiveTheme, AvatarSize, TextSize},
|
theme::{ActiveTheme, AvatarSize, TextSize},
|
||||||
typography::{Ellipsis, LEADING},
|
typography::{Ellipsis, LEADING},
|
||||||
};
|
};
|
||||||
@@ -130,126 +131,137 @@ impl BlameView {
|
|||||||
impl RenderOnce for BlameView {
|
impl RenderOnce for BlameView {
|
||||||
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 colors = theme.colors.clone();
|
let frame = div()
|
||||||
let lines: Vec<&str> = self.code.lines().collect();
|
.size_full()
|
||||||
let digits = lines.len().to_string().len();
|
|
||||||
let blocks: Vec<AnyElement> = runs(&self.owners)
|
|
||||||
.into_iter()
|
|
||||||
.map(|(owner, span)| {
|
|
||||||
let blame = owner.map(|owner| self.blames[owner].clone());
|
|
||||||
let Some(found) = blame.clone() else {
|
|
||||||
let bare = div()
|
|
||||||
.flex_none()
|
|
||||||
.w_64()
|
|
||||||
.border_l_2()
|
|
||||||
.border_color(colors.border);
|
|
||||||
return (span, bare.into_any_element(), None);
|
|
||||||
};
|
|
||||||
let blame = found;
|
|
||||||
let fade = 1.0 - blame.age.clamp(0.0, 1.0) * 0.8;
|
|
||||||
let pick = self.on_commit.clone();
|
|
||||||
let commit = blame.commit.clone();
|
|
||||||
let gutter = div()
|
|
||||||
.id((self.id.clone(), format!("run-{}", span.start)))
|
|
||||||
.flex_none()
|
|
||||||
.w_64()
|
|
||||||
.flex()
|
|
||||||
.items_start()
|
|
||||||
.gap_2()
|
|
||||||
.px_2()
|
|
||||||
.border_l_2()
|
|
||||||
.border_color(colors.accent.opacity(fade))
|
|
||||||
.font_family(theme.font_family.clone())
|
|
||||||
.text_size(theme.text_size(TextSize::Xs))
|
|
||||||
.when_some(pick, |gutter, pick| {
|
|
||||||
gutter
|
|
||||||
.cursor_pointer()
|
|
||||||
.hover(|gutter| gutter.bg(colors.hover))
|
|
||||||
.on_click(move |_, window, cx| pick(&commit, window, cx))
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
Avatar::new(
|
|
||||||
(self.id.clone(), format!("author-{}", span.start)),
|
|
||||||
blame.author.clone(),
|
|
||||||
)
|
|
||||||
.size(AvatarSize::Xs),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.min_w_0()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.font_weight(FontWeight::MEDIUM)
|
|
||||||
.text_color(colors.fg)
|
|
||||||
.child(blame.author.clone()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(colors.fg_subtle)
|
|
||||||
.child(blame.when.clone()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(colors.fg_muted)
|
|
||||||
.child(Ellipsis::new(blame.subject.clone())),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
(span, gutter.into_any_element(), Some(blame))
|
|
||||||
})
|
|
||||||
.map(|(span, gutter, blame)| {
|
|
||||||
let code = div()
|
|
||||||
.flex_1()
|
|
||||||
.min_w_0()
|
|
||||||
.overflow_hidden()
|
|
||||||
.line_height(relative(LEADING))
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.children(span.clone().map(|line| {
|
|
||||||
let text = lines[line].to_string();
|
|
||||||
let styles = code_colors(&text, cx);
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.gap_3()
|
|
||||||
.whitespace_nowrap()
|
|
||||||
.when(self.current == Some(line), |row| row.bg(colors.hover))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex_none()
|
|
||||||
.w(theme.text_size(TextSize::Xs) * (digits as f32 * 0.62))
|
|
||||||
.text_right()
|
|
||||||
.text_color(colors.fg_subtle)
|
|
||||||
.child((line + 1).to_string()),
|
|
||||||
)
|
|
||||||
.child(StyledText::new(text).with_highlights(styles))
|
|
||||||
.when_some(
|
|
||||||
blame.clone().filter(|_| self.current == Some(line)),
|
|
||||||
|row, blame| row.child(GitBlameAnnotation::new(blame)),
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.border_t_1()
|
|
||||||
.border_color(colors.border)
|
|
||||||
.child(gutter)
|
|
||||||
.child(code)
|
|
||||||
.into_any_element()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.font_family(theme.mono_family.clone())
|
.font_family(theme.mono_family.clone())
|
||||||
.text_size(theme.text_size(TextSize::Xs))
|
.text_size(theme.text_size(TextSize::Xs))
|
||||||
.text_color(colors.fg)
|
.text_color(theme.colors.fg);
|
||||||
.children(blocks)
|
let Self {
|
||||||
|
id,
|
||||||
|
code,
|
||||||
|
owners,
|
||||||
|
blames,
|
||||||
|
current,
|
||||||
|
on_commit,
|
||||||
|
} = self;
|
||||||
|
let lines: Rc<Vec<SharedString>> =
|
||||||
|
Rc::new(code.lines().map(|line| line.to_string().into()).collect());
|
||||||
|
let digits = lines.len().to_string().len();
|
||||||
|
// Each line's owner, and whether it starts a run.
|
||||||
|
let heads: Rc<Vec<(Option<usize>, bool)>> = Rc::new(
|
||||||
|
runs(&owners)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|(owner, span)| span.clone().map(move |line| (owner, line == span.start)))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
let blames = Rc::new(blames);
|
||||||
|
let list = uniform_list((id.clone(), "lines"), lines.len(), move |range, _, cx| {
|
||||||
|
let theme = cx.theme();
|
||||||
|
let colors = theme.colors.clone();
|
||||||
|
range
|
||||||
|
.map(|line| {
|
||||||
|
let (owner, head) = heads[line];
|
||||||
|
let blame = owner.map(|owner| blames[owner].clone());
|
||||||
|
let fade = blame
|
||||||
|
.as_ref()
|
||||||
|
.map(|blame| 1.0 - blame.age.clamp(0.0, 1.0) * 0.8);
|
||||||
|
let gutter = div()
|
||||||
|
.id((id.clone(), format!("gutter-{line}")))
|
||||||
|
.flex_none()
|
||||||
|
.w_80()
|
||||||
|
.h_full()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.px_2()
|
||||||
|
.border_l_2()
|
||||||
|
.border_color(
|
||||||
|
fade.map_or(colors.border, |fade| colors.accent.opacity(fade)),
|
||||||
|
)
|
||||||
|
.font_family(theme.font_family.clone())
|
||||||
|
.text_size(theme.text_size(TextSize::Xs));
|
||||||
|
let gutter = match blame.clone().filter(|_| head) {
|
||||||
|
Some(blame) => {
|
||||||
|
let said: SharedString =
|
||||||
|
format!("{}, {} · {}", blame.author, blame.when, blame.subject)
|
||||||
|
.into();
|
||||||
|
let pick = on_commit.clone();
|
||||||
|
let commit = blame.commit.clone();
|
||||||
|
gutter
|
||||||
|
.when_some(pick, |gutter, pick| {
|
||||||
|
gutter
|
||||||
|
.role(Role::Button)
|
||||||
|
.aria_label(said)
|
||||||
|
.tab_index(0)
|
||||||
|
.focus_ring(cx)
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|gutter| gutter.bg(colors.hover))
|
||||||
|
.on_click(move |_, window, cx| pick(&commit, window, cx))
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
Avatar::new(
|
||||||
|
(id.clone(), format!("author-{line}")),
|
||||||
|
blame.author.clone(),
|
||||||
|
)
|
||||||
|
.size(AvatarSize::Xs),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_none()
|
||||||
|
.font_weight(FontWeight::MEDIUM)
|
||||||
|
.text_color(colors.fg)
|
||||||
|
.child(blame.author.clone()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_none()
|
||||||
|
.text_color(colors.fg_subtle)
|
||||||
|
.child(blame.when.clone()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.min_w_0()
|
||||||
|
.text_color(colors.fg_muted)
|
||||||
|
.child(Ellipsis::new(blame.subject.clone())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None => gutter,
|
||||||
|
};
|
||||||
|
let text = lines[line].clone();
|
||||||
|
let styles = code_colors(&text, cx);
|
||||||
|
div()
|
||||||
|
.w_full()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_3()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(if head && line > 0 {
|
||||||
|
colors.border
|
||||||
|
} else {
|
||||||
|
gpui::transparent_black()
|
||||||
|
})
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.line_height(relative(LEADING))
|
||||||
|
.when(current == Some(line), |row| row.bg(colors.hover))
|
||||||
|
.child(gutter)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_none()
|
||||||
|
.w(theme.text_size(TextSize::Xs) * (digits as f32 * 0.62))
|
||||||
|
.text_right()
|
||||||
|
.text_color(colors.fg_subtle)
|
||||||
|
.child((line + 1).to_string()),
|
||||||
|
)
|
||||||
|
.child(StyledText::new(text).with_highlights(styles))
|
||||||
|
.when_some(blame.filter(|_| current == Some(line)), |row, blame| {
|
||||||
|
row.child(GitBlameAnnotation::new(blame))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.size_full();
|
||||||
|
frame.child(list)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-4
@@ -398,12 +398,15 @@ impl RenderOnce for CommitInput {
|
|||||||
let commit = self.on_commit.filter(|_| ready);
|
let commit = self.on_commit.filter(|_| ready);
|
||||||
let submit = commit.clone();
|
let submit = commit.clone();
|
||||||
let label = if self.amend {
|
let label = if self.amend {
|
||||||
format!("Amend {}", self.branch)
|
i18n::text(cx, "git.commit.amend_to", &[("branch", &self.branch)])
|
||||||
} else {
|
} else {
|
||||||
format!("Commit to {}", self.branch)
|
i18n::text(cx, "git.commit.commit_to", &[("branch", &self.branch)])
|
||||||
};
|
};
|
||||||
let amend =
|
let amend = Checkbox::new((self.id.clone(), "amend"), self.amend).label(i18n::text(
|
||||||
Checkbox::new((self.id.clone(), "amend"), self.amend).label("Amend last commit");
|
cx,
|
||||||
|
"git.commit.amend",
|
||||||
|
&[],
|
||||||
|
));
|
||||||
let amend = match self.on_amend {
|
let amend = match self.on_amend {
|
||||||
Some(on_amend) => amend.on_change(move |on, window, cx| on_amend(on, window, cx)),
|
Some(on_amend) => amend.on_change(move |on, window, cx| on_amend(on, window, cx)),
|
||||||
None => amend,
|
None => amend,
|
||||||
|
|||||||
+89
-59
@@ -2,13 +2,14 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Bounds, ElementId, Hsla, InteractiveElement, IntoElement, ParentElement,
|
AnyElement, App, Bounds, ElementId, Hsla, InteractiveElement, IntoElement, ParentElement,
|
||||||
PathBuilder, Pixels, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
PathBuilder, Pixels, RenderOnce, Role, SharedString, StatefulInteractiveElement, Styled,
|
||||||
canvas, div, point, prelude::*, size,
|
Window, canvas, div, point, prelude::*, size, uniform_list,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::graph::{GraphRow, Half, lanes};
|
use super::graph::{GraphRow, Half};
|
||||||
use crate::{
|
use crate::{
|
||||||
data_display::{Avatar, Tag, Tone},
|
data_display::{Avatar, Tag, Tone},
|
||||||
|
primitives::FocusRing,
|
||||||
theme::{ActiveTheme, AvatarSize, ControlSize, IconSize, Radius, TextSize},
|
theme::{ActiveTheme, AvatarSize, ControlSize, IconSize, Radius, TextSize},
|
||||||
typography::Ellipsis,
|
typography::Ellipsis,
|
||||||
};
|
};
|
||||||
@@ -68,6 +69,11 @@ impl RenderOnce for CommitItem {
|
|||||||
let (pick, picked) = (self.on_pick, commit.id.clone());
|
let (pick, picked) = (self.on_pick, commit.id.clone());
|
||||||
div()
|
div()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
|
.role(Role::ListItem)
|
||||||
|
.aria_label(commit.subject.clone())
|
||||||
|
.aria_selected(self.selected)
|
||||||
|
.tab_index(0)
|
||||||
|
.focus_ring(cx)
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
.h(theme.control_height(ControlSize::Lg))
|
.h(theme.control_height(ControlSize::Lg))
|
||||||
@@ -126,24 +132,28 @@ impl RenderOnce for CommitItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Commits newest first, one to a row; a press picks one.
|
type OnEnd = Rc<dyn Fn(&mut App)>;
|
||||||
|
|
||||||
|
/// Commits newest first; draws rows in view, fills its box.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct CommitList {
|
pub struct CommitList {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
commits: Vec<Commit>,
|
commits: Rc<Vec<Commit>>,
|
||||||
|
graph: Option<Rc<Vec<GraphRow>>>,
|
||||||
selected: Option<SharedString>,
|
selected: Option<SharedString>,
|
||||||
on_pick: Option<OnPick>,
|
on_pick: Option<OnPick>,
|
||||||
graph: bool,
|
on_end: Option<OnEnd>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommitList {
|
impl CommitList {
|
||||||
pub fn new(id: impl Into<ElementId>, commits: impl IntoIterator<Item = Commit>) -> Self {
|
pub fn new(id: impl Into<ElementId>, commits: Rc<Vec<Commit>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
commits: commits.into_iter().collect(),
|
commits,
|
||||||
|
graph: None,
|
||||||
selected: None,
|
selected: None,
|
||||||
on_pick: None,
|
on_pick: None,
|
||||||
graph: false,
|
on_end: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,13 +170,22 @@ impl CommitList {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws the lanes beside the rows, as a commit graph.
|
/// Lanes beside the rows, from `lanes`.
|
||||||
pub fn graph(mut self) -> Self {
|
pub fn graph(mut self, rows: Rc<Vec<GraphRow>>) -> Self {
|
||||||
self.graph = true;
|
self.graph = Some(rows);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when rows near the end draw.
|
||||||
|
pub fn on_end(mut self, handler: impl Fn(&mut App) + 'static) -> Self {
|
||||||
|
self.on_end = Some(Rc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rows from the end that ask for more.
|
||||||
|
const NEAR_END: usize = 20;
|
||||||
|
|
||||||
/// Paints one row's lanes, `span` lanes wide so every row lines up: strokes into and out of its commit, lanes passing it, and the commit's dot.
|
/// Paints one row's lanes, `span` lanes wide so every row lines up: strokes into and out of its commit, lanes passing it, and the commit's dot.
|
||||||
fn lanes_cell(
|
fn lanes_cell(
|
||||||
row: GraphRow,
|
row: GraphRow,
|
||||||
@@ -219,56 +238,67 @@ fn lanes_cell(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for CommitList {
|
impl RenderOnce for CommitList {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||||
let theme = cx.theme();
|
if let Some(graph) = &self.graph
|
||||||
let rem = window.rem_size();
|
&& graph.len() != self.commits.len()
|
||||||
let height = theme.control_height(ControlSize::Lg).to_pixels(rem);
|
{
|
||||||
let lane = theme.icon_size(IconSize::Md).to_pixels(rem);
|
log::error!(
|
||||||
let stroke = theme.chart().hairline * 1.5;
|
"commit list: {} graph rows for {} commits; rows past them draw bare",
|
||||||
let palette = theme.colors.chart;
|
graph.len(),
|
||||||
let graph: Vec<Option<GraphRow>> = if self.graph {
|
self.commits.len()
|
||||||
let parents: Vec<Vec<&str>> = self
|
);
|
||||||
.commits
|
}
|
||||||
.iter()
|
let span = self
|
||||||
.map(|commit| commit.parents.iter().map(|id| id.as_ref()).collect())
|
.graph
|
||||||
.collect();
|
|
||||||
lanes(
|
|
||||||
self.commits
|
|
||||||
.iter()
|
|
||||||
.zip(&parents)
|
|
||||||
.map(|(commit, parents)| (commit.id.as_ref(), parents.as_slice())),
|
|
||||||
)
|
|
||||||
.into_iter()
|
|
||||||
.map(Some)
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
vec![None; self.commits.len()]
|
|
||||||
};
|
|
||||||
let span = graph
|
|
||||||
.iter()
|
.iter()
|
||||||
.flatten()
|
.flat_map(|rows| rows.iter())
|
||||||
.map(|row| row.width)
|
.map(|row| row.width)
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
div()
|
let Self {
|
||||||
.flex()
|
id,
|
||||||
.flex_col()
|
commits,
|
||||||
.children(self.commits.into_iter().zip(graph).map(|(commit, lanes)| {
|
graph,
|
||||||
let selected = self.selected.as_ref() == Some(&commit.id);
|
selected,
|
||||||
let item = CommitItem::new((self.id.clone(), commit.id.clone()), commit)
|
on_pick,
|
||||||
.selected(selected);
|
on_end,
|
||||||
let item = match &self.on_pick {
|
} = self;
|
||||||
Some(pick) => {
|
let count = commits.len();
|
||||||
let pick = pick.clone();
|
uniform_list((id.clone(), "rows"), count, move |range, window, cx| {
|
||||||
item.on_pick(move |id, window, cx| pick(id, window, cx))
|
if let Some(on_end) = on_end.clone().filter(|_| range.end + NEAR_END >= count) {
|
||||||
}
|
cx.defer(move |cx| on_end(cx));
|
||||||
None => item,
|
}
|
||||||
};
|
let theme = cx.theme();
|
||||||
div()
|
let rem = window.rem_size();
|
||||||
.flex()
|
let height = theme.control_height(ControlSize::Lg).to_pixels(rem);
|
||||||
.items_center()
|
let lane = theme.icon_size(IconSize::Md).to_pixels(rem);
|
||||||
.children(lanes.map(|row| lanes_cell(row, span, lane, height, stroke, palette)))
|
let stroke = theme.chart().hairline * 1.5;
|
||||||
.child(item)
|
let palette = theme.colors.chart;
|
||||||
}))
|
range
|
||||||
|
.map(|ix| {
|
||||||
|
let commit = commits[ix].clone();
|
||||||
|
let picked = selected.as_ref() == Some(&commit.id);
|
||||||
|
let item =
|
||||||
|
CommitItem::new((id.clone(), commit.id.clone()), commit).selected(picked);
|
||||||
|
let item = match &on_pick {
|
||||||
|
Some(pick) => {
|
||||||
|
let pick = pick.clone();
|
||||||
|
item.on_pick(move |id, window, cx| pick(id, window, cx))
|
||||||
|
}
|
||||||
|
None => item,
|
||||||
|
};
|
||||||
|
let lanes = graph.as_ref().and_then(|rows| rows.get(ix)).cloned();
|
||||||
|
div()
|
||||||
|
.w_full()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.children(
|
||||||
|
lanes.map(|row| lanes_cell(row, span, lane, height, stroke, palette)),
|
||||||
|
)
|
||||||
|
.child(item)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.size_full()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+43
-29
@@ -1,36 +1,37 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
|
App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, Role, SharedString,
|
||||||
StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
StatefulInteractiveElement, Styled, Window, div, prelude::*, uniform_list,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{badges::DiffStat, commits::Commit};
|
use super::{badges::DiffStat, commits::Commit};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
primitives::FocusRing,
|
||||||
theme::{ActiveTheme, Radius, TextSize},
|
theme::{ActiveTheme, Radius, TextSize},
|
||||||
typography::Ellipsis,
|
typography::Ellipsis,
|
||||||
};
|
};
|
||||||
|
|
||||||
type OnPick = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
|
type OnPick = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
|
||||||
|
|
||||||
/// A file's commits newest first on a line through them: subject, author and when, and what each changed; a press picks one.
|
/// A file's commits newest first; draws rows in view.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct FileHistory {
|
pub struct FileHistory {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
commits: Vec<(Commit, (usize, usize))>,
|
commits: Rc<Vec<(Commit, Option<(usize, usize)>)>>,
|
||||||
selected: Option<SharedString>,
|
selected: Option<SharedString>,
|
||||||
on_pick: Option<OnPick>,
|
on_pick: Option<OnPick>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileHistory {
|
impl FileHistory {
|
||||||
/// Each commit with the lines it added and removed in the file.
|
/// Each commit with lines added and removed, if counted.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
id: impl Into<ElementId>,
|
id: impl Into<ElementId>,
|
||||||
commits: impl IntoIterator<Item = (Commit, (usize, usize))>,
|
commits: Rc<Vec<(Commit, Option<(usize, usize)>)>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
commits: commits.into_iter().collect(),
|
commits,
|
||||||
selected: None,
|
selected: None,
|
||||||
on_pick: None,
|
on_pick: None,
|
||||||
}
|
}
|
||||||
@@ -51,30 +52,41 @@ impl FileHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for FileHistory {
|
impl RenderOnce for FileHistory {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||||
let theme = cx.theme();
|
let Self {
|
||||||
let colors = theme.colors.clone();
|
id,
|
||||||
let last = self.commits.len().saturating_sub(1);
|
commits,
|
||||||
div()
|
selected,
|
||||||
.flex()
|
on_pick,
|
||||||
.flex_col()
|
} = self;
|
||||||
.text_size(theme.text_size(TextSize::Sm))
|
let last = commits.len().saturating_sub(1);
|
||||||
.children(self.commits.into_iter().enumerate().map(
|
uniform_list((id.clone(), "rows"), commits.len(), move |range, _, cx| {
|
||||||
|(ix, (commit, (added, removed)))| {
|
let theme = cx.theme();
|
||||||
let selected = self.selected.as_ref() == Some(&commit.id);
|
let colors = theme.colors.clone();
|
||||||
let (pick, id) = (self.on_pick.clone(), commit.id.clone());
|
range
|
||||||
|
.map(|ix| {
|
||||||
|
let (commit, lines) = &commits[ix];
|
||||||
|
let picked = selected.as_ref() == Some(&commit.id);
|
||||||
|
let (pick, picked_id) = (on_pick.clone(), commit.id.clone());
|
||||||
let short: String = commit.id.chars().take(7).collect();
|
let short: String = commit.id.chars().take(7).collect();
|
||||||
div()
|
div()
|
||||||
.id((self.id.clone(), format!("commit-{ix}")))
|
.id((id.clone(), format!("commit-{ix}")))
|
||||||
|
.w_full()
|
||||||
|
.role(Role::ListItem)
|
||||||
|
.aria_label(commit.subject.clone())
|
||||||
|
.aria_selected(picked)
|
||||||
|
.tab_index(0)
|
||||||
|
.focus_ring(cx)
|
||||||
.flex()
|
.flex()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.px_2()
|
.px_2()
|
||||||
|
.text_size(theme.text_size(TextSize::Sm))
|
||||||
.rounded(theme.radius(Radius::Sm))
|
.rounded(theme.radius(Radius::Sm))
|
||||||
.when(selected, |row| row.bg(colors.active))
|
.when(picked, |row| row.bg(colors.active))
|
||||||
.when(!selected, |row| row.hover(|row| row.bg(colors.hover)))
|
.when(!picked, |row| row.hover(|row| row.bg(colors.hover)))
|
||||||
.when_some(pick, |row, pick| {
|
.when_some(pick, |row, pick| {
|
||||||
row.cursor_pointer()
|
row.cursor_pointer()
|
||||||
.on_click(move |_, window, cx| pick(&id, window, cx))
|
.on_click(move |_, window, cx| pick(&picked_id, window, cx))
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -90,7 +102,7 @@ impl RenderOnce for FileHistory {
|
|||||||
.size_2()
|
.size_2()
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(if selected {
|
.border_color(if picked {
|
||||||
colors.focus
|
colors.focus
|
||||||
} else {
|
} else {
|
||||||
colors.border_strong
|
colors.border_strong
|
||||||
@@ -130,13 +142,15 @@ impl RenderOnce for FileHistory {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.children(lines.map(|(added, removed)| {
|
||||||
div()
|
div()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.py_1p5()
|
.py_1p5()
|
||||||
.child(DiffStat::new(added, removed)),
|
.child(DiffStat::new(added, removed))
|
||||||
)
|
}))
|
||||||
},
|
})
|
||||||
))
|
.collect()
|
||||||
|
})
|
||||||
|
.size_full()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,8 @@ mod graph;
|
|||||||
mod history;
|
mod history;
|
||||||
mod merge;
|
mod merge;
|
||||||
mod merging;
|
mod merging;
|
||||||
|
#[cfg(all(test, feature = "test-support"))]
|
||||||
|
mod picking;
|
||||||
mod refs;
|
mod refs;
|
||||||
mod review;
|
mod review;
|
||||||
mod rows;
|
mod rows;
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
//! Long lists draw in view, load more, pick by key.
|
||||||
|
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
Context, IntoElement, KeyBinding, KeyUpEvent, Keystroke, ParentElement, Render, Styled,
|
||||||
|
TestAppContext, VisualTestContext, Window, div, px,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{Blame, BlameView, Commit, CommitList, FileHistory};
|
||||||
|
use crate::{forms, primitives::FocusNext, theme::Theme};
|
||||||
|
|
||||||
|
fn commit(n: usize) -> Commit {
|
||||||
|
Commit {
|
||||||
|
id: format!("{n:040}").into(),
|
||||||
|
parents: vec![format!("{:040}", n + 1).into()],
|
||||||
|
subject: format!("change {n}").into(),
|
||||||
|
author: "Ann".into(),
|
||||||
|
when: "today".into(),
|
||||||
|
refs: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a list asked for.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Heard {
|
||||||
|
picked: Vec<String>,
|
||||||
|
ends: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Lists {
|
||||||
|
count: usize,
|
||||||
|
heard: Rc<RefCell<Heard>>,
|
||||||
|
which: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Lists {
|
||||||
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let commits: Vec<Commit> = (0..self.count).map(commit).collect();
|
||||||
|
let (pick, end) = (self.heard.clone(), self.heard.clone());
|
||||||
|
let frame = div().w(px(600.0)).h(px(240.0));
|
||||||
|
match self.which {
|
||||||
|
"commits" => frame.child(
|
||||||
|
CommitList::new("commits", Rc::new(commits))
|
||||||
|
.on_pick(move |id, _, _| pick.borrow_mut().picked.push(id.to_string()))
|
||||||
|
.on_end(move |_| end.borrow_mut().ends += 1),
|
||||||
|
),
|
||||||
|
"history" => frame.child(
|
||||||
|
FileHistory::new(
|
||||||
|
"history",
|
||||||
|
Rc::new(commits.into_iter().map(|c| (c, None)).collect()),
|
||||||
|
)
|
||||||
|
.on_pick(move |id, _, _| pick.borrow_mut().picked.push(id.to_string())),
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
let blame = |commit: &str| Blame {
|
||||||
|
commit: commit.into(),
|
||||||
|
author: "Ann".into(),
|
||||||
|
when: "today".into(),
|
||||||
|
subject: "first".into(),
|
||||||
|
age: 0.0,
|
||||||
|
};
|
||||||
|
frame.child(
|
||||||
|
BlameView::new(
|
||||||
|
"blame",
|
||||||
|
"one\ntwo\nthree",
|
||||||
|
[0, 0, 1],
|
||||||
|
[blame("a1"), blame("b2")],
|
||||||
|
)
|
||||||
|
.on_commit(move |id, _, _| pick.borrow_mut().picked.push(id.to_string())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shown<'a>(
|
||||||
|
cx: &'a mut TestAppContext,
|
||||||
|
count: usize,
|
||||||
|
which: &'static str,
|
||||||
|
) -> (Rc<RefCell<Heard>>, &'a mut VisualTestContext) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
Theme::init(cx);
|
||||||
|
forms::bind_keys(cx);
|
||||||
|
cx.bind_keys([KeyBinding::new("tab", FocusNext, None)]);
|
||||||
|
});
|
||||||
|
let heard = Rc::new(RefCell::new(Heard::default()));
|
||||||
|
let held = heard.clone();
|
||||||
|
let (_, cx) = cx.add_window_view(move |_, _| Lists {
|
||||||
|
count,
|
||||||
|
heard: held,
|
||||||
|
which,
|
||||||
|
});
|
||||||
|
cx.update(|window, _| window.activate_window());
|
||||||
|
settle(cx);
|
||||||
|
(heard, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn settle(cx: &mut VisualTestContext) {
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx.update(|window, _| window.refresh());
|
||||||
|
cx.run_until_parked();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tab to the first stop and press Enter.
|
||||||
|
fn press_first(cx: &mut VisualTestContext) {
|
||||||
|
cx.update(|window, cx| window.focus_next(cx));
|
||||||
|
settle(cx);
|
||||||
|
cx.simulate_keystrokes("enter");
|
||||||
|
settle(cx);
|
||||||
|
cx.simulate_event(KeyUpEvent {
|
||||||
|
keystroke: Keystroke::parse("enter").expect("a key"),
|
||||||
|
});
|
||||||
|
settle(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_long_list_asks_for_more_only_near_its_end(cx: &mut TestAppContext) {
|
||||||
|
let (heard, _) = shown(cx, 500, "commits");
|
||||||
|
assert_eq!(heard.borrow().ends, 0, "500 rows, few in view");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_short_list_asks_for_more(cx: &mut TestAppContext) {
|
||||||
|
let (heard, _) = shown(cx, 12, "commits");
|
||||||
|
assert!(heard.borrow().ends > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_commit_picks_from_the_keyboard(cx: &mut TestAppContext) {
|
||||||
|
let (heard, cx) = shown(cx, 500, "commits");
|
||||||
|
press_first(cx);
|
||||||
|
assert_eq!(heard.borrow().picked, [format!("{:040}", 0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_file_history_row_picks_from_the_keyboard(cx: &mut TestAppContext) {
|
||||||
|
let (heard, cx) = shown(cx, 300, "history");
|
||||||
|
press_first(cx);
|
||||||
|
assert_eq!(heard.borrow().picked, [format!("{:040}", 0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_blame_run_picks_its_commit_from_the_keyboard(cx: &mut TestAppContext) {
|
||||||
|
let (heard, cx) = shown(cx, 0, "blame");
|
||||||
|
press_first(cx);
|
||||||
|
cx.update(|window, cx| window.focus_next(cx));
|
||||||
|
settle(cx);
|
||||||
|
cx.simulate_keystrokes("enter");
|
||||||
|
settle(cx);
|
||||||
|
cx.simulate_event(KeyUpEvent {
|
||||||
|
keystroke: Keystroke::parse("enter").expect("a key"),
|
||||||
|
});
|
||||||
|
settle(cx);
|
||||||
|
assert_eq!(
|
||||||
|
heard.borrow().picked,
|
||||||
|
["a1", "b2"],
|
||||||
|
"each run's head, no stop between"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -82,6 +82,9 @@ const EN: &[(&str, &str)] = &[
|
|||||||
("git.diff.split", "Split"),
|
("git.diff.split", "Split"),
|
||||||
("git.diff.unchanged.one", "{n} unchanged line"),
|
("git.diff.unchanged.one", "{n} unchanged line"),
|
||||||
("git.diff.unchanged.other", "{n} unchanged lines"),
|
("git.diff.unchanged.other", "{n} unchanged lines"),
|
||||||
|
("git.commit.commit_to", "Commit to {branch}"),
|
||||||
|
("git.commit.amend_to", "Amend {branch}"),
|
||||||
|
("git.commit.amend", "Amend last commit"),
|
||||||
];
|
];
|
||||||
|
|
||||||
const ZH_CN: &[(&str, &str)] = &[
|
const ZH_CN: &[(&str, &str)] = &[
|
||||||
@@ -162,6 +165,9 @@ const ZH_CN: &[(&str, &str)] = &[
|
|||||||
("git.diff.split", "并排"),
|
("git.diff.split", "并排"),
|
||||||
("git.diff.unchanged.one", "{n} 行未改动"),
|
("git.diff.unchanged.one", "{n} 行未改动"),
|
||||||
("git.diff.unchanged.other", "{n} 行未改动"),
|
("git.diff.unchanged.other", "{n} 行未改动"),
|
||||||
|
("git.commit.commit_to", "提交到 {branch}"),
|
||||||
|
("git.commit.amend_to", "修改 {branch} 的最近提交"),
|
||||||
|
("git.commit.amend", "修改最近一次提交"),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// 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