mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 08:02:24 +00:00
merge: the commit graph
Two conflicts, both from the graph and the commit detail view landing in the same week: - scm/mod.rs: each had removed its own dead-code allow. Both are gone now; status_rank's stays, since the file tree is what will use it. - graph.rs built a CommitDetailView by hand while detail.rs had grown a constructor that takes the commit the caller already holds. It now goes through open_commit_detail with the row's own commit as the seed, which is what the detail view's author asked for: a click costs one git command (the file list) instead of two. The test that covered the old hand-built view asserted `loading`, which was an artefact of building it directly. It now asserts what its name says — that the seed arrives — and covers the unseeded path too.
This commit is contained in:
@@ -850,6 +850,14 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::ScmGraphFilterPlaceholder => "Filter commits…",
|
||||
L10nKey::ScmGraphAllBranches => "All Branches",
|
||||
L10nKey::ScmGraphEmpty => "No commits yet",
|
||||
L10nKey::ScmGraphCurrentBranch => "Current Branch",
|
||||
L10nKey::ScmGraphFoldLanes => "Hide Lanes",
|
||||
L10nKey::ScmGraphShowLanes => "Show Lanes",
|
||||
L10nKey::ScmCheckoutCommit => "Checkout Commit",
|
||||
L10nKey::ScmCreateBranchHere => "Create Branch Here…",
|
||||
L10nKey::ScmResetSoft => "Reset (Soft)",
|
||||
L10nKey::ScmResetMixed => "Reset (Mixed)",
|
||||
L10nKey::ScmResetHard => "Reset (Hard)",
|
||||
L10nKey::ScmCommitDetailTitle => "Commit",
|
||||
L10nKey::ScmCopyCommitSha => "Copy Commit SHA",
|
||||
L10nKey::ScmCherryPick => "Cherry Pick",
|
||||
|
||||
@@ -900,6 +900,14 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::ScmGraphFilterPlaceholder => "コミットを絞り込む…",
|
||||
L10nKey::ScmGraphAllBranches => "すべてのブランチ",
|
||||
L10nKey::ScmGraphEmpty => "まだコミットがありません",
|
||||
L10nKey::ScmGraphCurrentBranch => "現在のブランチ",
|
||||
L10nKey::ScmGraphFoldLanes => "レーンを隠す",
|
||||
L10nKey::ScmGraphShowLanes => "レーンを表示",
|
||||
L10nKey::ScmCheckoutCommit => "このコミットをチェックアウト",
|
||||
L10nKey::ScmCreateBranchHere => "ここにブランチを作成…",
|
||||
L10nKey::ScmResetSoft => "リセット(ソフト)",
|
||||
L10nKey::ScmResetMixed => "リセット(ミックス)",
|
||||
L10nKey::ScmResetHard => "リセット(ハード)",
|
||||
L10nKey::ScmCommitDetailTitle => "コミット",
|
||||
L10nKey::ScmCopyCommitSha => "コミット SHA をコピー",
|
||||
L10nKey::ScmCherryPick => "チェリーピック",
|
||||
|
||||
@@ -676,6 +676,14 @@ pub enum L10nKey {
|
||||
ScmGraphFilterPlaceholder,
|
||||
ScmGraphAllBranches,
|
||||
ScmGraphEmpty,
|
||||
ScmGraphCurrentBranch,
|
||||
ScmGraphFoldLanes,
|
||||
ScmGraphShowLanes,
|
||||
ScmCheckoutCommit,
|
||||
ScmCreateBranchHere,
|
||||
ScmResetSoft,
|
||||
ScmResetMixed,
|
||||
ScmResetHard,
|
||||
ScmCommitDetailTitle,
|
||||
ScmCopyCommitSha,
|
||||
ScmCherryPick,
|
||||
@@ -1141,6 +1149,14 @@ const SCM_KEYS_AWAITING_A_CALLER: &[L10nKey] = &[
|
||||
L10nKey::ScmGraphFilterPlaceholder,
|
||||
L10nKey::ScmGraphAllBranches,
|
||||
L10nKey::ScmGraphEmpty,
|
||||
L10nKey::ScmGraphCurrentBranch,
|
||||
L10nKey::ScmGraphFoldLanes,
|
||||
L10nKey::ScmGraphShowLanes,
|
||||
L10nKey::ScmCheckoutCommit,
|
||||
L10nKey::ScmCreateBranchHere,
|
||||
L10nKey::ScmResetSoft,
|
||||
L10nKey::ScmResetMixed,
|
||||
L10nKey::ScmResetHard,
|
||||
L10nKey::ScmCommitDetailTitle,
|
||||
L10nKey::ScmCherryPick,
|
||||
L10nKey::ScmRevertCommit,
|
||||
|
||||
@@ -823,6 +823,14 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::ScmGraphFilterPlaceholder => "筛选提交…",
|
||||
L10nKey::ScmGraphAllBranches => "全部分支",
|
||||
L10nKey::ScmGraphEmpty => "还没有提交",
|
||||
L10nKey::ScmGraphCurrentBranch => "当前分支",
|
||||
L10nKey::ScmGraphFoldLanes => "隐藏泳道",
|
||||
L10nKey::ScmGraphShowLanes => "显示泳道",
|
||||
L10nKey::ScmCheckoutCommit => "检出此提交",
|
||||
L10nKey::ScmCreateBranchHere => "在此创建分支…",
|
||||
L10nKey::ScmResetSoft => "重置(保留暂存)",
|
||||
L10nKey::ScmResetMixed => "重置(保留工作区)",
|
||||
L10nKey::ScmResetHard => "重置(丢弃更改)",
|
||||
L10nKey::ScmCommitDetailTitle => "提交",
|
||||
L10nKey::ScmCopyCommitSha => "复制提交 SHA",
|
||||
L10nKey::ScmCherryPick => "拣选提交",
|
||||
|
||||
+181
-4
@@ -123,6 +123,26 @@ pub struct ActiveAccent(pub u32);
|
||||
|
||||
impl Global for ActiveAccent {}
|
||||
|
||||
/// How many lanes of the commit graph get a colour of their own.
|
||||
///
|
||||
/// Six because that is how many hues of the ANSI set survive being pulled to a
|
||||
/// contrast floor while staying apart from each other — and because the graph
|
||||
/// caps its visible lanes at the same number, which is what guarantees no two
|
||||
/// columns on screen are ever the same colour.
|
||||
pub const LANE_SLOTS: usize = 6;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Lanes {
|
||||
pub ink: [u32; LANE_SLOTS],
|
||||
/// Everything past the last slot shares one column, so it gets a neutral:
|
||||
/// a hue there would claim a branch identity the column does not have.
|
||||
pub overflow: u32,
|
||||
}
|
||||
|
||||
pub struct ActiveLanes(pub Lanes);
|
||||
|
||||
impl Global for ActiveLanes {}
|
||||
|
||||
impl Theme {
|
||||
pub fn background_color(&self) -> u32 {
|
||||
self.background.color()
|
||||
@@ -177,13 +197,16 @@ impl Theme {
|
||||
}
|
||||
}
|
||||
|
||||
/// One entry of the palette as a packed `0xRRGGBB`.
|
||||
fn ansi(&self, i: usize) -> u32 {
|
||||
let (r, g, b) = self.ansi16[i];
|
||||
(r as u32) << 16 | (g as u32) << 8 | b as u32
|
||||
}
|
||||
|
||||
pub fn semantics(&self) -> Semantics {
|
||||
let bg = self.background_color();
|
||||
let fg = legible_foreground(bg, self.foreground);
|
||||
let ansi = |i: usize| -> u32 {
|
||||
let (r, g, b) = self.ansi16[i];
|
||||
(r as u32) << 16 | (g as u32) << 8 | b as u32
|
||||
};
|
||||
let ansi = |i: usize| self.ansi(i);
|
||||
// An error line lands on a popover or a sidebar row as often as on the
|
||||
// window, and both of those fills sit a step toward the foreground.
|
||||
// Clear the floor on every surface the ink can be painted on, not just
|
||||
@@ -211,6 +234,44 @@ impl Theme {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lane colours for the commit graph, derived the same way every other
|
||||
/// colour in this file is: seeded from the theme's own palette, then walked
|
||||
/// to a contrast floor on each surface it can be painted on.
|
||||
///
|
||||
/// Not a fixed table of hexes. A hard-coded palette would be the one thing
|
||||
/// here that does not follow the theme, and — worse — the contrast tests
|
||||
/// below cannot see it, so the four light builtins would ship a graph whose
|
||||
/// lanes sit at 2:1 against their own background.
|
||||
///
|
||||
/// The seed order is blue, yellow, magenta, green, cyan, red. Three
|
||||
/// constraints picked it: no two adjacent slots share a hue family; red and
|
||||
/// green are never neighbours, for the readers who cannot tell them apart;
|
||||
/// and red is last because a panel three or four lanes wide never reaches
|
||||
/// it, so the one colour that also means "danger" everywhere else in the UI
|
||||
/// stays out of the common case.
|
||||
pub fn lanes(&self) -> Lanes {
|
||||
const SEEDS: [usize; LANE_SLOTS] = [4, 3, 5, 2, 6, 1];
|
||||
let bg = self.background_color();
|
||||
let fg = legible_foreground(bg, self.foreground);
|
||||
// Same three surfaces as `semantics`: the graph draws on the window in
|
||||
// a floating panel, on the sidebar when the panel is docked, and on a
|
||||
// popover in the commit detail view.
|
||||
let surfaces = [bg, mix(bg, fg, 0.03), mix(bg, fg, 0.05)];
|
||||
let clear = |seed: u32| {
|
||||
surfaces.iter().fold(seed, |ink, surface| {
|
||||
legible_ink(*surface, ink, ACCENT_FLOOR)
|
||||
})
|
||||
};
|
||||
let mut ink = [0u32; LANE_SLOTS];
|
||||
for (slot, seed) in SEEDS.iter().enumerate() {
|
||||
ink[slot] = clear(self.ansi(*seed));
|
||||
}
|
||||
Lanes {
|
||||
ink,
|
||||
overflow: dim(fg, bg, state::TEXT_RESTING),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn surfaces(&self) -> Surfaces {
|
||||
let m = self.neutrals();
|
||||
let mut sidebar = self.surface(m.sidebar);
|
||||
@@ -1205,6 +1266,122 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// CIE L*a*b* for a packed sRGB colour, D65.
|
||||
///
|
||||
/// Contrast is a luminance ratio and says nothing about hue: two lanes can
|
||||
/// both clear 3:1 against the background and still be the same colour to
|
||||
/// look at. ΔE is the measure that catches that, and it needs Lab.
|
||||
fn lab(c: u32) -> (f32, f32, f32) {
|
||||
fn linear(v: u32) -> f32 {
|
||||
let s = v as f32 / 255.0;
|
||||
if s <= 0.04045 {
|
||||
s / 12.92
|
||||
} else {
|
||||
((s + 0.055) / 1.055).powf(2.4)
|
||||
}
|
||||
}
|
||||
let (r, g, b) = (
|
||||
linear(c >> 16 & 0xff),
|
||||
linear(c >> 8 & 0xff),
|
||||
linear(c & 0xff),
|
||||
);
|
||||
// sRGB → XYZ, then normalised by the D65 white point.
|
||||
let x = (0.4124 * r + 0.3576 * g + 0.1805 * b) / 0.95047;
|
||||
let y = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
let z = (0.0193 * r + 0.1192 * g + 0.9505 * b) / 1.08883;
|
||||
let f = |t: f32| {
|
||||
if t > 0.008856 {
|
||||
t.cbrt()
|
||||
} else {
|
||||
7.787 * t + 16.0 / 116.0
|
||||
}
|
||||
};
|
||||
let (fx, fy, fz) = (f(x), f(y), f(z));
|
||||
(116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz))
|
||||
}
|
||||
|
||||
fn delta_e76(a: u32, b: u32) -> f32 {
|
||||
let (l1, a1, b1) = lab(a);
|
||||
let (l2, a2, b2) = lab(b);
|
||||
((l1 - l2).powi(2) + (a1 - a2).powi(2) + (b1 - b2).powi(2)).sqrt()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lane_colours_clear_the_floor_on_every_surface() {
|
||||
for t in builtins() {
|
||||
let bg = t.background_color();
|
||||
let fg = legible_foreground(bg, t.foreground);
|
||||
let lanes = t.lanes();
|
||||
for (name, surface) in [
|
||||
("background", bg),
|
||||
("sidebar", mix(bg, fg, 0.03)),
|
||||
("popover", mix(bg, fg, 0.05)),
|
||||
] {
|
||||
for (slot, ink) in lanes.ink.iter().enumerate() {
|
||||
let ratio = contrast(*ink, surface);
|
||||
assert!(
|
||||
ratio >= ACCENT_FLOOR,
|
||||
"{}/{name}: lane {slot} is only {ratio:.2}:1",
|
||||
t.id
|
||||
);
|
||||
}
|
||||
let ratio = contrast(lanes.overflow, surface);
|
||||
assert!(
|
||||
ratio >= ACCENT_FLOOR,
|
||||
"{}/{name}: the overflow lane is only {ratio:.2}:1",
|
||||
t.id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjacent_lanes_are_never_the_same_colour() {
|
||||
// A just-noticeable difference is around 2.3. The floor is set far
|
||||
// above it because these are 1.5px lines a few pixels apart, not
|
||||
// patches side by side, and the eye is much worse at hairlines.
|
||||
const FLOOR: f32 = 12.0;
|
||||
for t in builtins() {
|
||||
let lanes = t.lanes();
|
||||
for slot in 0..LANE_SLOTS - 1 {
|
||||
let d = delta_e76(lanes.ink[slot], lanes.ink[slot + 1]);
|
||||
assert!(
|
||||
d >= FLOOR,
|
||||
"{}: lanes {slot} and {} are ΔE {d:.1} apart",
|
||||
t.id,
|
||||
slot + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lane_colours_are_deterministic() {
|
||||
for t in builtins() {
|
||||
assert_eq!(
|
||||
t.lanes().ink,
|
||||
t.lanes().ink,
|
||||
"{}: lane derivation is not a pure function",
|
||||
t.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The seeds were chosen so that no two neighbours share a hue family and
|
||||
/// red never sits beside green. Both are properties of the *order*, so a
|
||||
/// reshuffle has to fail here rather than only looking slightly worse.
|
||||
#[test]
|
||||
fn the_lane_seed_order_keeps_red_and_green_apart() {
|
||||
let seeds = [4usize, 3, 5, 2, 6, 1];
|
||||
let red = seeds.iter().position(|s| *s == 1).expect("red is a seed");
|
||||
let green = seeds.iter().position(|s| *s == 2).expect("green is a seed");
|
||||
assert!(
|
||||
red.abs_diff(green) > 1,
|
||||
"red and green ended up adjacent at slots {red} and {green}"
|
||||
);
|
||||
assert_eq!(red, LANE_SLOTS - 1, "red should be the last slot reached");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resting_labels_stay_readable() {
|
||||
for t in builtins() {
|
||||
|
||||
@@ -403,7 +403,7 @@ impl Tty7App {
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let files = detail.files.clone().unwrap_or_default();
|
||||
let mut list = v_flex().child(self.panel_subtitle(
|
||||
let list = v_flex().child(self.panel_subtitle(
|
||||
&t_plural(L10nKey::ScmFilesChanged, files.len(), &[]),
|
||||
true,
|
||||
None,
|
||||
|
||||
+1822
-11
File diff suppressed because it is too large
Load Diff
+2
-6
@@ -4,17 +4,13 @@
|
||||
//! `file_tree.rs` use. The directory only keeps the surface from piling into
|
||||
//! `right_panel.rs`.
|
||||
|
||||
// What is left unused is what the graph will call, plus `status_rank`, which
|
||||
// is the file tree's to use. Both allows come off with the step that wires
|
||||
// them up.
|
||||
// The graph and the commit detail view both have callers now, so their allows
|
||||
// are gone. What is left is `status_rank`, which is the file tree's to use.
|
||||
pub(crate) mod actions;
|
||||
pub(crate) mod detail;
|
||||
#[allow(dead_code)]
|
||||
pub(crate) mod graph;
|
||||
pub(crate) mod panel;
|
||||
#[allow(dead_code)]
|
||||
pub(crate) mod path;
|
||||
#[allow(dead_code)]
|
||||
pub(crate) mod state;
|
||||
#[allow(dead_code)]
|
||||
pub(crate) mod status;
|
||||
|
||||
+24
-4
@@ -16,7 +16,7 @@ use std::sync::Arc;
|
||||
|
||||
use gpui::Entity;
|
||||
use gpui_component::input::InputState;
|
||||
use tty7_core::core::git::log::{Commit, CommitFile};
|
||||
use tty7_core::core::git::log::{Commit, CommitFile, CommitPage, GraphScope};
|
||||
use tty7_core::core::git::status::HeadState;
|
||||
|
||||
use crate::ui::host_ops::HostId;
|
||||
@@ -158,10 +158,30 @@ pub(crate) struct GraphState {
|
||||
/// and shifts under you when a ref moves between pages.
|
||||
pub(crate) requested: usize,
|
||||
pub(crate) loading: bool,
|
||||
/// Filter box. Like `commit_input`, created on first render.
|
||||
/// The page the graph is currently drawn from, and which repository and
|
||||
/// query produced it. `Arc` because paint reads it while the next page is
|
||||
/// being laid out on a worker, and the key so a repository switch shows an
|
||||
/// empty graph rather than the previous repository's history.
|
||||
pub(crate) page: Option<Arc<CommitPage>>,
|
||||
pub(crate) page_key: Option<(RepoKey, u64, GraphScope)>,
|
||||
/// Fold the lane gutter down to a single column. Worth about six
|
||||
/// characters of the message, which at this width is the difference
|
||||
/// between reading a subject and reading its first word.
|
||||
pub(crate) lanes_collapsed: bool,
|
||||
/// Filter box. Like `commit_input`, created on first render — and with the
|
||||
/// subscription that turns typing into a repaint. An `InputState` is its
|
||||
/// own entity; without this the box would take text the list never sees.
|
||||
pub(crate) search: Option<Entity<InputState>>,
|
||||
/// `refs/heads/...` the graph is restricted to; empty means all refs.
|
||||
pub(crate) branch_filter: Option<String>,
|
||||
pub(crate) search_sub: Option<gpui::Subscription>,
|
||||
/// An open "name a branch at this commit" input, and the rev it starts
|
||||
/// from. The panel's own naming row cannot serve this: it always creates
|
||||
/// at HEAD, and the whole point here is the commit under the cursor.
|
||||
pub(crate) naming: Option<(Entity<InputState>, String)>,
|
||||
/// Which refs the graph walks from. Three states rather than an
|
||||
/// `Option<branch>`: "this branch and its upstream", "one named branch",
|
||||
/// and "everything" are all reachable from the header's dropdown, and only
|
||||
/// the enum the data layer already takes can express all three.
|
||||
pub(crate) scope: GraphScope,
|
||||
/// The selected row, by full sha.
|
||||
pub(crate) selected: Option<String>,
|
||||
pub(crate) scroll: gpui::ScrollHandle,
|
||||
|
||||
@@ -283,6 +283,10 @@ pub(crate) fn apply_theme(mut window: Option<&mut Window>, cx: &mut App) {
|
||||
});
|
||||
cx.set_global(surfaces.clone());
|
||||
cx.set_global(presets::ActiveAccent(m.accent));
|
||||
// Same treatment as `Surfaces`: derived once here rather than recomputed
|
||||
// in `render`, because the graph reads it once per visible row per frame
|
||||
// and each entry costs a contrast bisection on three surfaces.
|
||||
cx.set_global(presets::ActiveLanes(theme.lanes()));
|
||||
|
||||
let t = Theme::global_mut(cx);
|
||||
let mut base: Hsla = rgb(m.background).into();
|
||||
|
||||
Reference in New Issue
Block a user