kimun_notes/ropetext/
width.rs1use unicode_width::UnicodeWidthStr;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct Metrics {
9 pub tab_width: usize,
11}
12
13impl Default for Metrics {
14 fn default() -> Self {
15 Self {
16 tab_width: Metrics::DEFAULT_TAB_WIDTH,
17 }
18 }
19}
20
21impl Metrics {
22 pub const DEFAULT_TAB_WIDTH: usize = 4;
29
30 pub fn width_at(&self, cluster: &str, column: usize) -> usize {
38 if cluster.len() == 1 && cluster.as_bytes()[0] == b'\t' {
42 let stop = self.tab_width.max(1);
43 stop - (column % stop)
44 } else {
45 cluster.width()
46 }
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53
54 #[test]
55 fn a_cluster_is_measured_whole() {
56 let m = Metrics::default();
57 assert_eq!(m.width_at("\u{1F1EA}\u{1F1F8}", 0), 2, "flag");
59 assert_eq!(m.width_at("\u{2764}\u{FE0F}", 0), 2, "heart with VS16");
60 assert_eq!(m.width_at("1\u{FE0F}\u{20E3}", 0), 2, "keycap");
61 assert_eq!(m.width_at("\u{3042}", 0), 2, "CJK");
62 assert_eq!(m.width_at("a", 0), 1);
63 assert_eq!(m.width_at("e\u{301}", 0), 1, "e plus combining acute");
64 }
65
66 #[test]
67 fn zero_width_clusters_measure_zero() {
68 let m = Metrics::default();
69 for zero in ["\u{200B}", "\u{00AD}", "\u{200C}", "\u{FEFF}", "\u{301}"] {
70 assert_eq!(m.width_at(zero, 0), 0, "{zero:?}");
71 }
72 }
73
74 #[test]
75 fn a_tab_advances_to_the_next_stop() {
76 let m = Metrics::default();
77 assert_eq!(m.width_at("\t", 0), 4);
78 assert_eq!(m.width_at("\t", 1), 3);
79 assert_eq!(m.width_at("\t", 3), 1);
80 assert_eq!(m.width_at("\t", 4), 4);
81 }
82
83 #[test]
84 fn a_tab_width_of_zero_still_advances() {
85 let m = Metrics { tab_width: 0 };
86 assert_eq!(m.width_at("\t", 0), 1, "forward progress is not optional");
87 }
88}