use escriba_shirube::Severity;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GutterRole {
Number,
Mark(Severity),
NoMark,
Separator,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GutterCell {
pub text: String,
pub role: GutterRole,
}
pub const MIN_GUTTER_WIDTH: usize = 8;
const MIN_NUMBER_WIDTH: usize = 4;
const FURNITURE_WIDTH: usize = 4;
#[must_use]
pub fn gutter_width(line_count: u32) -> usize {
number_width(line_count) + FURNITURE_WIDTH
}
#[must_use]
pub fn number_width(line_count: u32) -> usize {
let digits = line_count.max(1).to_string().len();
digits.max(MIN_NUMBER_WIDTH)
}
#[must_use]
pub fn gutter_cells(line: u32, mark: Option<Severity>, line_count: u32) -> Vec<GutterCell> {
let field = number_width(line_count);
let mut number = (line + 1).to_string();
if number.len() < field {
number = " ".repeat(field - number.len()) + &number;
}
vec![
GutterCell {
text: number,
role: GutterRole::Number,
},
GutterCell {
text: " ".to_string(),
role: GutterRole::Number,
},
match mark {
Some(s) => GutterCell {
text: mark_glyph(s).to_string(),
role: GutterRole::Mark(s),
},
None => GutterCell {
text: " ".to_string(),
role: GutterRole::NoMark,
},
},
GutterCell {
text: "│ ".to_string(),
role: GutterRole::Separator,
},
]
}
#[must_use]
pub const fn mark_glyph(severity: Severity) -> &'static str {
match severity {
Severity::Error => "\u{2716}", Severity::Warning => "\u{25b2}", Severity::Info => "\u{2022}", Severity::Hint => "\u{203a}", }
}
#[must_use]
pub fn gutter_text(line: u32, mark: Option<Severity>, line_count: u32) -> String {
gutter_cells(line, mark, line_count)
.into_iter()
.map(|c| c.text)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_line_of_one_buffer_gets_the_same_width() {
for line_count in [1u32, 9, 10, 999, 1_000, 9_999, 10_000, 123_456] {
let want = gutter_width(line_count);
assert!(want >= MIN_GUTTER_WIDTH, "{line_count}: {want} below floor");
for line in [0, line_count / 2, line_count.saturating_sub(1)] {
for mark in [None, Some(Severity::Error), Some(Severity::Hint)] {
let w = gutter_text(line, mark, line_count).chars().count();
assert_eq!(
w, want,
"buffer of {line_count} lines: line {line} mark \
{mark:?} rendered {w} columns, not {want}",
);
}
}
}
}
#[test]
fn the_last_line_of_a_buffer_always_fits_its_field() {
for line_count in [9u32, 10, 99, 100, 9_999, 10_000, 100_000] {
let label = line_count.to_string();
assert!(
label.len() <= number_width(line_count),
"a {line_count}-line buffer must fit the label {label:?}",
);
assert_eq!(
gutter_text(line_count - 1, None, line_count).chars().count(),
gutter_width(line_count),
"the LAST line must not be wider than every other one",
);
}
}
#[test]
fn a_small_buffer_still_gets_the_floor() {
assert_eq!(gutter_width(3), MIN_GUTTER_WIDTH);
assert_eq!(gutter_width(9_999), MIN_GUTTER_WIDTH);
assert_eq!(gutter_width(10_000), MIN_GUTTER_WIDTH + 1);
}
#[test]
fn every_severity_has_a_distinct_glyph() {
let all = [
Severity::Error,
Severity::Warning,
Severity::Info,
Severity::Hint,
];
let mut seen = std::collections::BTreeSet::new();
for s in all {
assert!(seen.insert(mark_glyph(s)), "{s:?} duplicates another mark");
}
}
#[test]
fn no_mark_collides_with_a_fleet_signal() {
let fleet = ['\u{25c6}', '\u{25b8}', '\u{25ae}', '\u{25cf}'];
for s in [
Severity::Error,
Severity::Warning,
Severity::Info,
Severity::Hint,
] {
let g = mark_glyph(s).chars().next().expect("one glyph");
assert!(!fleet.contains(&g), "{s:?} reuses a fleet signal glyph");
}
}
#[test]
fn the_mark_sits_between_the_number_and_the_rule() {
let cells = gutter_cells(0, Some(Severity::Error), 40);
let roles: Vec<GutterRole> = cells.iter().map(|c| c.role).collect();
assert_eq!(roles[0], GutterRole::Number);
assert_eq!(roles[2], GutterRole::Mark(Severity::Error));
assert_eq!(roles[3], GutterRole::Separator);
}
}