fix(sidebar): elide branch names from the end only

The sidebar branch (group header and row git line) was front-elided,
but its width budget was a few pixels short: the line paints with
tabular figures while it was measured with proportional ones, and the
separator was measured as " · " though it is drawn as a bare "·"
between two gaps. The front-elided branch then overflowed and CSS
truncation cut its tail too, leaving "…nd-error-classificati…".

- Measure the git line and header with the same tabular font they paint
  with, and measure the separator as drawn.
- Elide the branch from the end, the plain cut a reader expects; the
  CSS backstop now cuts in the same direction.
This commit is contained in:
l0ng-ai
2026-09-29 18:05:14 +08:00
parent 4e97498e2c
commit 5902e27c77
2 changed files with 73 additions and 24 deletions
+28 -24
View File
@@ -24,8 +24,8 @@ use crate::ui::i18n::{L10nKey, t, t_fmt};
use crate::ui::reorder::{self, Reorder, Surface};
use crate::ui::right_panel::RESIZE_HANDLE_WIDTH;
use crate::ui::tab_strip::{
DragTab, REORDER_SLIDE_MS, abbreviate_home, elide_label, elide_path_keep_tail,
elide_tail_clusters, measure_text,
DragTab, REORDER_SLIDE_MS, abbreviate_home, elide_end_clusters, elide_label,
elide_path_keep_tail, measure_text,
};
pub(crate) const MIN_SIDEBAR_WIDTH: f32 = 180.;
@@ -68,7 +68,6 @@ const HEAD_CONTROL_HEIGHT: f32 = 28.;
const HEAD_CONTROL_RADIUS: f32 = 7.;
/// What sits between a group's branch and its diff counts.
const META_SEP: &str = " · ";
const META_SEP_TRIMMED: &str = "·";
/// Tabular numerals, so a column of diff counts lines up digit for digit.
@@ -375,9 +374,17 @@ impl Tty7App {
let rem = window.rem_size().as_f32();
// Measure with the same interface scale used to paint the header.
let header_size = rem * META_REM;
// The git lines and the group header paint with `tabular()` figures,
// which set a `1` as wide as an `8`; measuring them proportionally
// came out a few pixels short on counts like `+1565 −124`, and the
// front-elided branch then lost its tail to CSS truncation as well.
let meta_font = gpui::Font {
features: tabular(),
..font.clone()
};
let header_font = gpui::Font {
weight: FontWeight::MEDIUM,
..font.clone()
..meta_font.clone()
};
// On the rail the diff counts are metadata like the branch beside
// them, so they take its caption ink; the hover card, which is where
@@ -678,13 +685,12 @@ impl Tty7App {
.text_size(px(meta_size))
.text_color(cx.theme().muted_foreground)
.font_features(tabular());
let counts_w = counts_width(&window.text_system(), &font, meta_size, &g);
// Branch: cut from the front, so `…session-auth` keeps
// the part that tells two branches off one prefix apart.
let counts_w = counts_width(&window.text_system(), &meta_font, meta_size, &g);
// Branch: cut from the end, the way any other label is.
let branch_avail = (label_avail - counts_w).max(0.);
let shown = elide_tail_clusters(
let shown = elide_end_clusters(
&window.text_system(),
&font,
&meta_font,
meta_size,
&g.branch,
branch_avail,
@@ -1236,24 +1242,23 @@ impl Tty7App {
}
let count_label = row_count.to_string();
if folded {
avail -=
measure_text(&ts, &font, header_size, &count_label) + row_metrics::META_GAP;
avail -= measure_text(&ts, &meta_font, header_size, &count_label)
+ row_metrics::META_GAP;
}
let avail = avail.max(HEADER_NAME_FLOOR);
// What the shared branch would take if nothing were in its
// way: the icon, the gap after it, the branch itself, the
// counts, and the two gaps the spacer between the name and
// the branch sits in.
// What the shared branch would take if nothing were in its
// way: the branch itself, the ` · ` before its counts, the
// way: the branch itself, the `·` before its counts, the
// counts, and the gaps the spacer between the name and the
// branch sits in.
let sep_w = measure_text(&ts, &font, header_size, META_SEP);
// Drawn as a bare `·` with a gap on either side; the gap before
// it is the one `counts_width` already reserves.
let sep_w = measure_text(&ts, &meta_font, header_size, META_SEP_TRIMMED)
+ row_metrics::META_GAP;
let git_want = shared_git.as_ref().map(|shared| {
let counts = counts_width(&ts, &font, header_size, &shared.status);
let counts = counts_width(&ts, &meta_font, header_size, &shared.status);
let sep = if counts > 0. { sep_w } else { 0. };
2. * row_metrics::META_GAP
+ measure_text(&ts, &font, header_size, &shared.status.branch)
+ measure_text(&ts, &meta_font, header_size, &shared.status.branch)
+ sep
+ counts
});
@@ -1398,15 +1403,14 @@ impl Tty7App {
click,
rows,
} = shared;
let counts_w = counts_width(&ts, &font, header_size, &status);
let counts_w = counts_width(&ts, &meta_font, header_size, &status);
let sep = if counts_w > 0. { sep_w } else { 0. };
let branch_avail =
(avail - name_w - 2. * row_metrics::META_GAP - sep - counts_w).max(0.);
// Cut from the front, like a row's: the tail is what
// tells two branches off the same prefix apart.
let branch = elide_tail_clusters(
// Cut from the end, like a row's.
let branch = elide_end_clusters(
&ts,
&font,
&meta_font,
header_size,
&status.branch,
branch_avail,
+45
View File
@@ -471,6 +471,38 @@ pub(crate) fn elide_label(
}
}
/// Keeps the longest head of `text` that fits before a trailing ellipsis —
/// the plain cut a reader expects, for a string read left to right like a
/// branch name.
pub(crate) fn elide_end_clusters(
text_system: &gpui::WindowTextSystem,
font: &gpui::Font,
size: f32,
text: &str,
max_width: f32,
) -> SharedString {
if measure_text(text_system, font, size, text) <= max_width {
return SharedString::from(text.to_string());
}
let budget = max_width - measure_text(text_system, font, size, "…");
if budget <= 0. {
return SharedString::from("…");
}
let cells = clusters(text);
let (mut lo, mut hi) = (0usize, cells.len());
while lo < hi {
let mid = (lo + hi + 1) / 2;
if measure_text(text_system, font, size, &cells[..mid].concat()) <= budget {
lo = mid;
} else {
hi = mid - 1;
}
}
let mut out = cells[..lo].concat();
out.push('…');
SharedString::from(out)
}
/// Keeps the longest tail of `text` that fits after a bare ellipsis. Shared
/// by the path and token elisions as their last resort.
pub(crate) fn elide_tail_clusters(
@@ -3223,6 +3255,19 @@ mod tests {
assert!(out.chars().count() < branch.chars().count());
}
#[gpui::test]
fn elide_end_keeps_the_head_of_a_branch(cx: &mut TestAppContext) {
let (ts, font, size) = elide_setup(cx);
let branch = "fix/rpc-proxy-and-error-classification";
let max = 140.;
let out = elide_end_clusters(&ts, &font, size, branch, max);
assert!(out.starts_with("fix/rpc-"), "head survives: {out}");
assert!(out.ends_with('…') && out.matches('…').count() == 1, "{out}");
assert!(measure_text(&ts, &font, size, &out) <= max);
let fits = measure_text(&ts, &font, size, branch);
assert_eq!(elide_end_clusters(&ts, &font, size, branch, fits), branch);
}
#[gpui::test]
fn elide_edges_leaves_short_branches_alone(cx: &mut TestAppContext) {
let (ts, font, size) = elide_setup(cx);