pub mod data;
pub mod diff;
pub mod highlight;
mod truncate;
mod wrap;
pub use data::{DataKind, JsonLexer, YamlLexer};
pub use diff::{DiffKind, DiffLexer};
pub use highlight::{CLikeLexer, Highlighter, TokenKind};
pub use truncate::truncate_ellipsis;
pub use wrap::wrap;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
const VS16: char = '\u{FE0F}';
pub(crate) fn is_control_cluster(cluster: &str) -> bool {
cluster.chars().next().is_some_and(char::is_control)
}
pub fn cluster_width(cluster: &str) -> i32 {
if cluster.is_empty() || is_control_cluster(cluster) {
return 0;
}
let base = UnicodeWidthStr::width(cluster) as i32;
if base <= 0 {
return 0;
}
if cluster.contains(VS16) {
return 2;
}
base.min(2)
}
pub fn width(s: &str) -> i32 {
s.graphemes(true).map(cluster_width).sum()
}
const ZWJ: char = '\u{200D}';
pub(crate) fn is_risky_cluster(cluster: &str) -> bool {
if cluster.is_ascii() {
return false;
}
cluster
.chars()
.any(|c| c == VS16 || c == ZWJ || is_risky_ambiguous(c))
}
fn is_risky_ambiguous(c: char) -> bool {
use unicode_width::UnicodeWidthChar;
if ('\u{2500}'..='\u{25FF}').contains(&c) {
return false;
}
c.width() != c.width_cjk()
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Segment<'a> {
pub cluster: &'a str,
pub offset: usize,
pub width: i32,
}
pub fn segments(s: &str) -> impl Iterator<Item = Segment<'_>> {
s.grapheme_indices(true).map(|(offset, cluster)| Segment {
cluster,
offset,
width: cluster_width(cluster),
})
}
pub fn next_boundary(s: &str, byte_idx: usize) -> usize {
for seg in segments(s) {
let end = seg.offset + seg.cluster.len();
if end > byte_idx {
return end;
}
}
s.len()
}
pub fn prev_boundary(s: &str, byte_idx: usize) -> usize {
let mut prev = 0;
for seg in segments(s) {
if seg.offset >= byte_idx {
return prev;
}
prev = seg.offset;
}
prev
}
const UNBOUNDED_WIDTH: i32 = 1 << 24;
pub fn measure(s: &str, avail: crate::base::Size) -> crate::base::Size {
#[cfg(test)]
count_measure_call();
let window = if avail.w <= 0 {
UNBOUNDED_WIDTH
} else {
avail.w
};
let lines = wrap(s, window);
let w = lines.iter().map(|l| width(l)).max().unwrap_or(0);
crate::base::Size::new(w, lines.len() as i32)
}
#[cfg(test)]
thread_local! {
static MEASURE_CALLS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
fn count_measure_call() {
MEASURE_CALLS.with(|c| c.set(c.get() + 1));
}
#[cfg(test)]
pub(crate) fn measure_calls() -> u64 {
MEASURE_CALLS.with(|c| c.get())
}
#[cfg(test)]
pub(crate) fn reset_measure_calls() {
MEASURE_CALLS.with(|c| c.set(0));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore = "perf profile: report-only, run explicitly with --nocapture"]
fn profile_segments_and_measure_per_keystroke_cost() {
use std::time::Instant;
let line = "let value = compute(width, \"héllo 世界 👍🏽 done\"); // trailing comment...";
let para = "The quick brown fox 跳过 the lazy dog 🐕 while counting 一二三 clusters.";
const N: u32 = 20_000;
let s = Instant::now();
let mut sink = 0usize;
for _ in 0..N {
sink += segments(line).map(|seg| seg.width as usize).sum::<usize>();
}
let seg_us = s.elapsed().as_secs_f64() * 1e6 / N as f64;
let s = Instant::now();
for _ in 0..N {
sink += measure(para, crate::base::Size::new(40, 0)).h as usize;
}
let meas_us = s.elapsed().as_secs_f64() * 1e6 / N as f64;
eprintln!("segments(120-col line): {seg_us:.2} us/call; measure(paragraph @40): {meas_us:.2} us/call; sink {sink}");
}
#[test]
fn ascii_and_cjk() {
assert_eq!(width("hello"), 5);
assert_eq!(width("世界"), 4);
assert_eq!(width("aé"), 2); assert_eq!(width(""), 0);
}
#[test]
fn emoji_presentation_is_wide() {
assert_eq!(cluster_width("❤\u{FE0F}"), 2); assert_eq!(cluster_width("1\u{FE0F}\u{20E3}"), 2); assert_eq!(cluster_width("👍"), 2);
assert_eq!(cluster_width("👨\u{200D}👩\u{200D}👧\u{200D}👦"), 2);
assert_eq!(cluster_width("👍🏽"), 2);
}
#[test]
fn controls_and_zero_width_measure_zero() {
assert_eq!(cluster_width("\t"), 0);
assert_eq!(cluster_width("\r\n"), 0);
assert_eq!(cluster_width("\u{200B}"), 0); assert_eq!(cluster_width("\u{FE0F}"), 0); assert_eq!(width("a\nb"), 2);
}
#[test]
fn zwj_between_letters_does_not_inflate() {
assert_eq!(width("a\u{200D}b"), 2);
}
#[test]
fn risky_cluster_classification() {
assert!(!is_risky_cluster("a"));
assert!(!is_risky_cluster(" "));
assert!(!is_risky_cluster("世"), "plain CJK is unambiguous wide");
assert!(!is_risky_cluster("é"), "combining accent is settled narrow");
assert!(is_risky_cluster("❤\u{FE0F}"), "VS16 presentation");
assert!(is_risky_cluster("👨\u{200D}👩\u{200D}👧"), "ZWJ sequence");
assert!(is_risky_cluster("§"));
assert!(is_risky_cluster("°"));
assert!(is_risky_cluster("☆"));
assert!(!is_risky_cluster("─"), "box drawing excluded by design");
assert!(!is_risky_cluster("│"));
assert!(!is_risky_cluster("█"), "block elements excluded");
assert!(!is_risky_cluster("▲"), "geometric shapes excluded");
}
#[test]
fn segments_cover_every_byte_with_widths() {
let s = "a世\t👍🏽é";
let segs: Vec<_> = segments(s).collect();
let mut expected_offset = 0;
for seg in &segs {
assert_eq!(seg.offset, expected_offset);
expected_offset += seg.cluster.len();
}
assert_eq!(expected_offset, s.len());
let widths: Vec<i32> = segs.iter().map(|s| s.width).collect();
assert_eq!(widths, vec![1, 2, 0, 2, 1], "control kept at width 0");
assert_eq!(widths.iter().sum::<i32>(), width(s));
assert_eq!(segments("").count(), 0);
}
#[test]
fn boundaries_step_whole_clusters() {
let s = "a👍🏽b"; assert_eq!(next_boundary(s, 0), 1);
assert_eq!(
next_boundary(s, 1),
9,
"steps over the whole ZWJ-ish cluster"
);
assert_eq!(next_boundary(s, 9), 10);
assert_eq!(next_boundary(s, 10), 10, "clamped at the end");
assert_eq!(prev_boundary(s, 10), 9);
assert_eq!(
prev_boundary(s, 9),
1,
"backspace target is the cluster start"
);
assert_eq!(prev_boundary(s, 1), 0);
assert_eq!(prev_boundary(s, 0), 0, "floored at 0");
}
#[test]
fn boundaries_snap_mid_cluster_inputs() {
let s = "x👍🏽y";
assert_eq!(next_boundary(s, 3), 9, "mid-cluster snaps to cluster end");
assert_eq!(prev_boundary(s, 3), 1, "mid-cluster snaps to cluster start");
assert_eq!(prev_boundary(s, 400), 9);
assert_eq!(next_boundary(s, 400), s.len());
assert_eq!(next_boundary("", 0), 0);
assert_eq!(prev_boundary("", 0), 0);
let caret = s.len();
let cut = prev_boundary(s, caret);
let mut owned = s.to_string();
owned.replace_range(cut..caret, "");
assert_eq!(owned, "x👍🏽");
}
#[test]
fn measure_wraps_and_measures() {
use crate::base::Size;
assert_eq!(measure("hello", Size::new(10, 5)), Size::new(5, 1));
assert_eq!(
measure("the quick brown fox", Size::new(10, 5)),
Size::new(9, 2)
);
assert_eq!(measure("ab\ncdef", Size::new(0, 0)), Size::new(4, 2));
assert_eq!(measure("世界人", Size::new(4, 9)), Size::new(4, 2));
assert_eq!(measure("", Size::new(10, 5)), Size::new(0, 1));
assert_eq!(measure("界", Size::new(1, 1)), Size::new(2, 1));
}
}