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:
ZacharyZhang-NY committed 2026-10-07 01:16:54 -04:00
1 parent 7b807a9478
commit 51d2aea02e
9 files changed
+472 -221

No files matched your search

+11 -4
View File
@@ -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)),
),
+21 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
}
}
+2
View File
@@ -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;
+160
View File
@@ -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"
);
}
+6
View File
@@ -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.