Line data Source code
1 : //! Text measurement of the shell: extended grapheme clusters (`unicode-segmentation`) and terminal column widths
2 : //! (`unicode-width`).
3 : //!
4 : //! Cursors move by grapheme clusters, and a cluster occupies the columns of `unicode-width`. A cluster that starts
5 : //! with a control character (like `"\r\n"`) does not advance the cursor and has width 0. Texts that are not valid
6 : //! UTF-8 are measured byte by byte: every byte is a cluster of width 1.
7 :
8 : use unicode_segmentation::UnicodeSegmentation;
9 : use unicode_width::UnicodeWidthStr;
10 :
11 : /// Is the text valid UTF-8?
12 : #[inline]
13 86 : pub fn is_valid(text: &[u8]) -> bool {
14 86 : std::str::from_utf8(text).is_ok()
15 86 : }
16 :
17 : /// The terminal columns of one grapheme cluster
18 111644 : fn cluster_width(cluster: &str) -> usize {
19 111644 : if cluster.starts_with(char::is_control) {
20 1 : 0
21 : } else {
22 111643 : cluster.width()
23 : }
24 111644 : }
25 :
26 : /// The terminal columns of a text: the sum of its cluster widths, or its byte length if it is not valid UTF-8
27 61601 : pub fn display_width(text: &[u8]) -> usize {
28 61601 : match std::str::from_utf8(text) {
29 61600 : Ok(text) => text.graphemes(true).map(cluster_width).sum(),
30 1 : Err(_) => text.len(),
31 : }
32 61601 : }
33 :
34 : /// The grapheme clusters of a text as (begin, end) byte ranges, one byte per cluster if it is not valid UTF-8
35 202 : pub fn graphemes(text: &[u8]) -> Vec<(usize, usize)> {
36 202 : match std::str::from_utf8(text) {
37 1503 : Ok(text) => text.grapheme_indices(true).map(|(begin, cluster)| (begin, begin + cluster.len())).collect(),
38 2 : Err(_) => (0..text.len()).map(|begin| (begin, begin + 1)).collect(),
39 : }
40 202 : }
41 :
42 : /// The end of the grapheme cluster that starts at the byte offset `offset`, the text length at the end
43 50012 : pub fn next_grapheme(text: &str, offset: usize) -> usize {
44 50012 : offset + text[offset..].graphemes(true).next().map_or(0, str::len)
45 50012 : }
46 :
47 : /// The begin of the grapheme cluster that ends at the byte offset `offset`, 0 at the begin
48 64 : pub fn previous_grapheme(text: &str, offset: usize) -> usize {
49 64 : offset - text[..offset].graphemes(true).next_back().map_or(0, str::len)
50 64 : }
51 :
52 : /// The begin offsets of the grapheme clusters of a text, followed by the text length
53 2301 : pub fn grapheme_boundaries(text: &str) -> Vec<usize> {
54 2301 : text.grapheme_indices(true).map(|(begin, _)| begin).chain(std::iter::once(text.len())).collect()
55 2301 : }
|