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()
|
||||
.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();
|
||||
let (pick, select) = (said.clone(), said.clone());
|
||||
section(
|
||||
@@ -46,7 +53,7 @@ pub fn blame(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
cx,
|
||||
)
|
||||
.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)
|
||||
.current(2)
|
||||
.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))
|
||||
.child(
|
||||
div().w(px(520.)).child(
|
||||
FileHistory::new("git-file-history", history_rows)
|
||||
div().w(px(520.)).h(px(360.)).child(
|
||||
FileHistory::new("git-file-history", std::rc::Rc::new(history_rows))
|
||||
.selected(history()[1].id.clone())
|
||||
.on_pick(move |commit, _, cx| set(&select, Some(format!("Opened {}.", &commit[..7]).into()), cx)),
|
||||
),
|
||||
|
||||
@@ -3,11 +3,13 @@ use ely_gpui_component::{
|
||||
forms::TextInput,
|
||||
git::{
|
||||
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,
|
||||
typography::Caption,
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
|
||||
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<> {
|
||||
let picked = keep("git-picked", || history()[0].id.clone(), window, cx);
|
||||
let now = picked.read(cx).clone();
|
||||
@@ -220,9 +237,9 @@ pub fn commits(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div().w(px(840.)).child(
|
||||
CommitList::new("git-graph", history())
|
||||
.graph()
|
||||
div().w(px(840.)).h(px(320.)).child(
|
||||
CommitList::new("git-graph", Rc::new(history()))
|
||||
.graph(Rc::new(graph()))
|
||||
.selected(now)
|
||||
.on_pick(move |id, _, cx| set(&pick, id.clone(), cx)),
|
||||
),
|
||||
|
||||
+133
-121
@@ -1,14 +1,15 @@
|
||||
use std::{ops::Range, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, FontWeight, InteractiveElement, IntoElement, ParentElement,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, StyledText, Window, div,
|
||||
prelude::*, relative,
|
||||
App, ElementId, FontWeight, InteractiveElement, IntoElement, ParentElement, RenderOnce, Role,
|
||||
SharedString, StatefulInteractiveElement, Styled, StyledText, Window, div, prelude::*,
|
||||
relative, uniform_list,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
data_display::Avatar,
|
||||
editor::code_colors,
|
||||
primitives::FocusRing,
|
||||
theme::{ActiveTheme, AvatarSize, TextSize},
|
||||
typography::{Ellipsis, LEADING},
|
||||
};
|
||||
@@ -130,126 +131,137 @@ impl BlameView {
|
||||
impl RenderOnce for BlameView {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let lines: Vec<&str> = self.code.lines().collect();
|
||||
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()
|
||||
let frame = div()
|
||||
.size_full()
|
||||
.font_family(theme.mono_family.clone())
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(colors.fg)
|
||||
.children(blocks)
|
||||
.text_color(theme.colors.fg);
|
||||
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 submit = commit.clone();
|
||||
let label = if self.amend {
|
||||
format!("Amend {}", self.branch)
|
||||
i18n::text(cx, "git.commit.amend_to", &[("branch", &self.branch)])
|
||||
} else {
|
||||
format!("Commit to {}", self.branch)
|
||||
i18n::text(cx, "git.commit.commit_to", &[("branch", &self.branch)])
|
||||
};
|
||||
let amend =
|
||||
Checkbox::new((self.id.clone(), "amend"), self.amend).label("Amend last commit");
|
||||
let amend = Checkbox::new((self.id.clone(), "amend"), self.amend).label(i18n::text(
|
||||
cx,
|
||||
"git.commit.amend",
|
||||
&[],
|
||||
));
|
||||
let amend = match self.on_amend {
|
||||
Some(on_amend) => amend.on_change(move |on, window, cx| on_amend(on, window, cx)),
|
||||
None => amend,
|
||||
|
||||
+89
-59
@@ -2,13 +2,14 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, Bounds, ElementId, Hsla, InteractiveElement, IntoElement, ParentElement,
|
||||
PathBuilder, Pixels, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
canvas, div, point, prelude::*, size,
|
||||
PathBuilder, Pixels, RenderOnce, Role, SharedString, StatefulInteractiveElement, Styled,
|
||||
Window, canvas, div, point, prelude::*, size, uniform_list,
|
||||
};
|
||||
|
||||
use super::graph::{GraphRow, Half, lanes};
|
||||
use super::graph::{GraphRow, Half};
|
||||
use crate::{
|
||||
data_display::{Avatar, Tag, Tone},
|
||||
primitives::FocusRing,
|
||||
theme::{ActiveTheme, AvatarSize, ControlSize, IconSize, Radius, TextSize},
|
||||
typography::Ellipsis,
|
||||
};
|
||||
@@ -68,6 +69,11 @@ impl RenderOnce for CommitItem {
|
||||
let (pick, picked) = (self.on_pick, commit.id.clone());
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.role(Role::ListItem)
|
||||
.aria_label(commit.subject.clone())
|
||||
.aria_selected(self.selected)
|
||||
.tab_index(0)
|
||||
.focus_ring(cx)
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.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)]
|
||||
pub struct CommitList {
|
||||
id: ElementId,
|
||||
commits: Vec<Commit>,
|
||||
commits: Rc<Vec<Commit>>,
|
||||
graph: Option<Rc<Vec<GraphRow>>>,
|
||||
selected: Option<SharedString>,
|
||||
on_pick: Option<OnPick>,
|
||||
graph: bool,
|
||||
on_end: Option<OnEnd>,
|
||||
}
|
||||
|
||||
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 {
|
||||
id: id.into(),
|
||||
commits: commits.into_iter().collect(),
|
||||
commits,
|
||||
graph: None,
|
||||
selected: None,
|
||||
on_pick: None,
|
||||
graph: false,
|
||||
on_end: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,13 +170,22 @@ impl CommitList {
|
||||
self
|
||||
}
|
||||
|
||||
/// Draws the lanes beside the rows, as a commit graph.
|
||||
pub fn graph(mut self) -> Self {
|
||||
self.graph = true;
|
||||
/// Lanes beside the rows, from `lanes`.
|
||||
pub fn graph(mut self, rows: Rc<Vec<GraphRow>>) -> Self {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn lanes_cell(
|
||||
row: GraphRow,
|
||||
@@ -219,56 +238,67 @@ fn lanes_cell(
|
||||
}
|
||||
|
||||
impl RenderOnce for CommitList {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let rem = window.rem_size();
|
||||
let height = theme.control_height(ControlSize::Lg).to_pixels(rem);
|
||||
let lane = theme.icon_size(IconSize::Md).to_pixels(rem);
|
||||
let stroke = theme.chart().hairline * 1.5;
|
||||
let palette = theme.colors.chart;
|
||||
let graph: Vec<Option<GraphRow>> = if self.graph {
|
||||
let parents: Vec<Vec<&str>> = self
|
||||
.commits
|
||||
.iter()
|
||||
.map(|commit| commit.parents.iter().map(|id| id.as_ref()).collect())
|
||||
.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
|
||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
if let Some(graph) = &self.graph
|
||||
&& graph.len() != self.commits.len()
|
||||
{
|
||||
log::error!(
|
||||
"commit list: {} graph rows for {} commits; rows past them draw bare",
|
||||
graph.len(),
|
||||
self.commits.len()
|
||||
);
|
||||
}
|
||||
let span = self
|
||||
.graph
|
||||
.iter()
|
||||
.flatten()
|
||||
.flat_map(|rows| rows.iter())
|
||||
.map(|row| row.width)
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.children(self.commits.into_iter().zip(graph).map(|(commit, lanes)| {
|
||||
let selected = self.selected.as_ref() == Some(&commit.id);
|
||||
let item = CommitItem::new((self.id.clone(), commit.id.clone()), commit)
|
||||
.selected(selected);
|
||||
let item = match &self.on_pick {
|
||||
Some(pick) => {
|
||||
let pick = pick.clone();
|
||||
item.on_pick(move |id, window, cx| pick(id, window, cx))
|
||||
}
|
||||
None => item,
|
||||
};
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.children(lanes.map(|row| lanes_cell(row, span, lane, height, stroke, palette)))
|
||||
.child(item)
|
||||
}))
|
||||
let Self {
|
||||
id,
|
||||
commits,
|
||||
graph,
|
||||
selected,
|
||||
on_pick,
|
||||
on_end,
|
||||
} = self;
|
||||
let count = commits.len();
|
||||
uniform_list((id.clone(), "rows"), count, move |range, window, cx| {
|
||||
if let Some(on_end) = on_end.clone().filter(|_| range.end + NEAR_END >= count) {
|
||||
cx.defer(move |cx| on_end(cx));
|
||||
}
|
||||
let theme = cx.theme();
|
||||
let rem = window.rem_size();
|
||||
let height = theme.control_height(ControlSize::Lg).to_pixels(rem);
|
||||
let lane = theme.icon_size(IconSize::Md).to_pixels(rem);
|
||||
let stroke = theme.chart().hairline * 1.5;
|
||||
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 gpui::{
|
||||
App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, Role, SharedString,
|
||||
StatefulInteractiveElement, Styled, Window, div, prelude::*, uniform_list,
|
||||
};
|
||||
|
||||
use super::{badges::DiffStat, commits::Commit};
|
||||
use crate::{
|
||||
primitives::FocusRing,
|
||||
theme::{ActiveTheme, Radius, TextSize},
|
||||
typography::Ellipsis,
|
||||
};
|
||||
|
||||
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)]
|
||||
pub struct FileHistory {
|
||||
id: ElementId,
|
||||
commits: Vec<(Commit, (usize, usize))>,
|
||||
commits: Rc<Vec<(Commit, Option<(usize, usize)>)>>,
|
||||
selected: Option<SharedString>,
|
||||
on_pick: Option<OnPick>,
|
||||
}
|
||||
|
||||
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(
|
||||
id: impl Into<ElementId>,
|
||||
commits: impl IntoIterator<Item = (Commit, (usize, usize))>,
|
||||
commits: Rc<Vec<(Commit, Option<(usize, usize)>)>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
commits: commits.into_iter().collect(),
|
||||
commits,
|
||||
selected: None,
|
||||
on_pick: None,
|
||||
}
|
||||
@@ -51,30 +52,41 @@ impl FileHistory {
|
||||
}
|
||||
|
||||
impl RenderOnce for FileHistory {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let last = self.commits.len().saturating_sub(1);
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.children(self.commits.into_iter().enumerate().map(
|
||||
|(ix, (commit, (added, removed)))| {
|
||||
let selected = self.selected.as_ref() == Some(&commit.id);
|
||||
let (pick, id) = (self.on_pick.clone(), commit.id.clone());
|
||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
let Self {
|
||||
id,
|
||||
commits,
|
||||
selected,
|
||||
on_pick,
|
||||
} = self;
|
||||
let last = commits.len().saturating_sub(1);
|
||||
uniform_list((id.clone(), "rows"), commits.len(), move |range, _, cx| {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.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();
|
||||
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()
|
||||
.gap_3()
|
||||
.px_2()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.when(selected, |row| row.bg(colors.active))
|
||||
.when(!selected, |row| row.hover(|row| row.bg(colors.hover)))
|
||||
.when(picked, |row| row.bg(colors.active))
|
||||
.when(!picked, |row| row.hover(|row| row.bg(colors.hover)))
|
||||
.when_some(pick, |row, pick| {
|
||||
row.cursor_pointer()
|
||||
.on_click(move |_, window, cx| pick(&id, window, cx))
|
||||
.on_click(move |_, window, cx| pick(&picked_id, window, cx))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
@@ -90,7 +102,7 @@ impl RenderOnce for FileHistory {
|
||||
.size_2()
|
||||
.rounded_full()
|
||||
.border_1()
|
||||
.border_color(if selected {
|
||||
.border_color(if picked {
|
||||
colors.focus
|
||||
} else {
|
||||
colors.border_strong
|
||||
@@ -130,13 +142,15 @@ impl RenderOnce for FileHistory {
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
.children(lines.map(|(added, removed)| {
|
||||
div()
|
||||
.flex_none()
|
||||
.py_1p5()
|
||||
.child(DiffStat::new(added, removed)),
|
||||
)
|
||||
},
|
||||
))
|
||||
.child(DiffStat::new(added, removed))
|
||||
}))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.size_full()
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ mod graph;
|
||||
mod history;
|
||||
mod merge;
|
||||
mod merging;
|
||||
#[cfg(all(test, feature = "test-support"))]
|
||||
mod picking;
|
||||
mod refs;
|
||||
mod review;
|
||||
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.unchanged.one", "{n} unchanged line"),
|
||||
("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)] = &[
|
||||
@@ -162,6 +165,9 @@ const ZH_CN: &[(&str, &str)] = &[
|
||||
("git.diff.split", "并排"),
|
||||
("git.diff.unchanged.one", "{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.
|
||||
|
||||
Reference in new issue
Block a user