fix(gpu): stop the minimap dividing by zero on an all-blank slab
`dominant_line_shape` averages only the lines in a bucket that have
content, then guarded the result with `bool::then_some`. `then_some`
takes its argument by value, so the `MinimapLineShape` literal --- and
with it `indent_sum / count` --- is evaluated before the `count > 0`
guard is ever consulted. When a bucket holds no contentful lines the
division panics and takes the GPU frontend down.
This is reachable in ordinary use, not at an edge: the bucketing branch
runs whenever a file has more lines than the minimap has pixel rows, and
it is exactly then that a run of blank lines can fill a whole downsampled
row. A whitespace-only line counts as blank too --- `minimap_line_shape`
subtracts the indent from the total, so `content_cols` is zero.
Switch to `bool::then`, which defers the body into a closure so the zero
case short-circuits to `None`. The call site already treats `None` as
"draw no stroke for this row", so no other change is needed.
Three tests, two of which fail against the previous line:
* a 10,000-line all-blank file driven through `minimap_rects`, which
reproduces the original panic through the real downsampling path;
* `dominant_line_shape` on an empty bucket;
* a mixed bucket, asserting the average still ignores blank lines ---
a companion guard so the fix cannot regress into counting the whole
slice.
A comment records why this must not be "simplified" back: clippy's
`unnecessary_lazy_evaluations` pushes in precisely the wrong direction
here, and does not fire on a body that can panic.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e74506879f
commit
e547a90e37
|
|
@ -8109,7 +8109,17 @@ fn dominant_line_shape(
|
|||
indent_sum += shape.indent_cols;
|
||||
content_sum += shape.content_cols;
|
||||
}
|
||||
(count > 0).then_some(MinimapLineShape {
|
||||
// `then`, NOT `then_some`: `bool::then_some` takes its argument by
|
||||
// value, so the struct literal --- and with it `indent_sum / count`
|
||||
// --- is evaluated before the guard is ever consulted. A slab of
|
||||
// all-blank source lines makes `count` zero and panics the frontend
|
||||
// on the division. `bool::then` defers the body into a closure, so
|
||||
// the zero case short-circuits to `None`.
|
||||
//
|
||||
// Clippy's `unnecessary_lazy_evaluations` lint pushes in exactly the
|
||||
// wrong direction here; it does not fire on a body that can panic,
|
||||
// but do not "simplify" this back.
|
||||
(count > 0).then(|| MinimapLineShape {
|
||||
indent_cols: indent_sum / count,
|
||||
content_cols: content_sum.div_ceil(count),
|
||||
})
|
||||
|
|
@ -10678,6 +10688,66 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimap_downsampling_survives_a_slab_of_blank_lines() {
|
||||
// Regression: `dominant_line_shape` counted only lines with
|
||||
// content, then built its average with `then_some` --- which
|
||||
// evaluates its argument eagerly, so `indent_sum / count`
|
||||
// divided by zero whenever a downsampled pixel row covered
|
||||
// nothing but blank lines. Reachable on any long file with a
|
||||
// run of blank lines, which is precisely when the bucketing
|
||||
// branch runs at all.
|
||||
let red = style_with_fg(CellColor::Rgb(255, 0, 0));
|
||||
let lines = vec![red; 10_000];
|
||||
// Every line blank: `minimap_line_shape("")` yields
|
||||
// `content_cols == 0`, so `has_content()` is false throughout
|
||||
// and every bucket counts zero contentful lines.
|
||||
let shapes = vec![
|
||||
MinimapLineShape {
|
||||
indent_cols: 0,
|
||||
content_cols: 0,
|
||||
};
|
||||
lines.len()
|
||||
];
|
||||
|
||||
let rects = minimap_rects(&lines, &shapes, 240, 120, 0, 30, FontMetrics::default());
|
||||
|
||||
// The strokes are all suppressed (no content to draw), but the
|
||||
// thumb still paints --- the point is that this returns at all.
|
||||
assert!(
|
||||
rects.len() <= 8,
|
||||
"blank slabs must emit no line strokes, got {}",
|
||||
rects.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimap_downsampling_averages_only_contentful_lines() {
|
||||
// Guards the other half: a bucket that mixes blank and
|
||||
// contentful lines must average over the contentful ones only,
|
||||
// so the fix cannot regress into `count = slice.len()`.
|
||||
let blank = MinimapLineShape {
|
||||
indent_cols: 0,
|
||||
content_cols: 0,
|
||||
};
|
||||
let solid = MinimapLineShape {
|
||||
indent_cols: 4,
|
||||
content_cols: 20,
|
||||
};
|
||||
let shapes = [blank, solid, solid, blank];
|
||||
|
||||
let shape = dominant_line_shape(&shapes, 0, 4).expect("bucket has contentful lines");
|
||||
|
||||
assert_eq!(shape.indent_cols, 4, "blank lines must not dilute indent");
|
||||
assert_eq!(shape.content_cols, 20, "blank lines must not dilute length");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimap_dominant_line_shape_is_none_for_an_empty_bucket() {
|
||||
let shape = dominant_line_shape(&[], 0, 0);
|
||||
assert!(shape.is_none(), "an empty bucket has no shape");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimap_hidden_when_surface_is_too_narrow() {
|
||||
let lines = [style_with_fg(CellColor::Rgb(255, 0, 0))];
|
||||
|
|
|
|||
Loading…
Reference in New Issue