use std::borrow::Cow;
use ropey::RopeSlice;
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete, UnicodeSegmentation};
use unicode_width::UnicodeWidthStr;
use crate::id::DisplayColumn;
pub fn printable_grapheme(grapheme: &str) -> &str {
if grapheme.chars().any(char::is_control) {
"\u{fffd}"
} else {
grapheme
}
}
fn grapheme_width(text: &str, cell: DisplayColumn, tab: usize) -> usize {
let tab = tab.max(1);
if text == "\t" {
tab - cell.get() % tab
} else {
UnicodeWidthStr::width(printable_grapheme(text))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GraphemeSpan {
pub byte: usize,
pub cell: DisplayColumn,
pub width: usize,
}
pub struct RopeGraphemes<'a> {
text: RopeSlice<'a>,
cursor: GraphemeCursor,
chunk: &'a str,
chunk_start: usize,
cell: DisplayColumn,
tab: usize,
}
impl<'a> RopeGraphemes<'a> {
pub fn new(text: RopeSlice<'a>, tab: usize) -> Self {
Self::new_at(text, tab, DisplayColumn::new(0))
}
pub fn new_at(text: RopeSlice<'a>, tab: usize, cell: DisplayColumn) -> Self {
let (chunk, chunk_start, _, _) = text.chunk_at_byte(0);
Self {
text,
cursor: GraphemeCursor::new(0, text.len_bytes(), true),
chunk,
chunk_start,
cell,
tab: tab.max(1),
}
}
}
impl<'a> Iterator for RopeGraphemes<'a> {
type Item = (GraphemeSpan, Cow<'a, str>);
fn next(&mut self) -> Option<Self::Item> {
let start = self.cursor.cur_cursor();
if start == self.text.len_bytes() {
return None;
}
let end = loop {
match self.cursor.next_boundary(self.chunk, self.chunk_start) {
Ok(Some(end)) => break end,
Ok(None) => return None,
Err(GraphemeIncomplete::NextChunk) => {
let next = self.chunk_start + self.chunk.len();
let (chunk, offset, _, _) = self.text.chunk_at_byte(next);
self.chunk = chunk;
self.chunk_start = offset;
}
Err(GraphemeIncomplete::PreContext(end)) => {
let (chunk, offset, _, _) = self.text.chunk_at_byte(end - 1);
self.cursor.provide_context(&chunk[..end - offset], offset);
}
Err(other) => unreachable!("forward grapheme traversal: {other:?}"),
}
};
let text = if start >= self.chunk_start && end <= self.chunk_start + self.chunk.len() {
Cow::Borrowed(&self.chunk[start - self.chunk_start..end - self.chunk_start])
} else {
let slice = self.text.byte_slice(start..end);
match slice.as_str() {
Some(text) => Cow::Borrowed(text),
None => Cow::Owned(slice.to_string()),
}
};
let width = grapheme_width(&text, self.cell, self.tab);
let span = GraphemeSpan {
byte: start,
cell: self.cell,
width,
};
self.cell += width;
Some((span, text))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CellClip {
pub x: usize,
pub width: usize,
pub complete: bool,
}
pub fn clip(span: GraphemeSpan, origin: DisplayColumn, width: usize) -> Option<CellClip> {
let left = span.cell.get().max(origin.get());
let end = span.cell.get() + span.width;
let right = end.min(origin.get().saturating_add(width));
(left < right).then(|| CellClip {
x: left - origin.get(),
width: right - left,
complete: left == span.cell.get() && right == end,
})
}
#[derive(Debug, Clone, Default)]
pub struct LineLayout {
spans: Vec<GraphemeSpan>,
pub len_bytes: usize,
pub width: DisplayColumn,
}
impl LineLayout {
pub fn build(text: &str, tab: usize) -> Self {
let mut spans = Vec::new();
let mut cell = DisplayColumn::new(0);
for (byte, text) in text.grapheme_indices(true) {
let width = grapheme_width(text, cell, tab);
spans.push(GraphemeSpan { byte, cell, width });
cell += width;
}
Self {
spans,
len_bytes: text.len(),
width: cell,
}
}
pub fn spans(&self) -> &[GraphemeSpan] {
&self.spans
}
pub fn cell_at_byte(&self, byte: usize) -> DisplayColumn {
if byte >= self.len_bytes {
return self.width;
}
let next = self.spans.partition_point(|s| s.byte <= byte);
next.checked_sub(1)
.map_or(DisplayColumn::new(0), |i| self.spans[i].cell)
}
pub fn byte_at_cell(&self, cell: DisplayColumn) -> usize {
if cell >= self.width {
return self.len_bytes;
}
let next = self.spans.partition_point(|s| s.cell <= cell);
next.checked_sub(1).map_or(0, |i| self.spans[i].byte)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn absolute_tabs_and_unicode_cells() {
let text = "ab\t界e\u{301}\x1bZ";
for (tab, cells, width) in [(3, [0, 1, 2, 3, 5, 6, 7], 8), (4, [0, 1, 2, 4, 6, 7, 8], 9)] {
let layout = LineLayout::build(text, tab);
assert_eq!(
layout
.spans()
.iter()
.map(|s| s.cell.get())
.collect::<Vec<_>>(),
cells
);
assert_eq!(layout.width.get(), width);
assert_eq!(layout.cell_at_byte(8).get(), cells[4]);
assert_eq!(layout.byte_at_cell(DisplayColumn::new(cells[3] + 1)), 3);
assert_eq!(layout.byte_at_cell(DisplayColumn::new(width)), text.len());
}
}
#[test]
fn columns_do_not_alias_at_terminal_limit() {
let text = format!("{}\t界Z", "x".repeat(70_001));
let layout = LineLayout::build(&text, 4);
assert_eq!(layout.cell_at_byte(70_002).get(), 70_004);
assert_eq!(layout.cell_at_byte(70_005).get(), 70_006);
assert_eq!(layout.byte_at_cell(DisplayColumn::new(70_005)), 70_002);
assert_eq!(LineLayout::build("x\tZ", 300).cell_at_byte(2).get(), 300);
}
#[test]
fn rope_chunk_boundaries_preserve_extended_clusters() {
let text = format!("{}e{}\t界🧑🚀Z", "a".repeat(997), "\u{301}".repeat(2000));
let rope = ropey::Rope::from_str(&text);
let got = RopeGraphemes::new(rope.slice(..), 3).collect::<Vec<_>>();
assert_eq!(
got[997].0,
GraphemeSpan {
byte: 997,
cell: DisplayColumn::new(997),
width: 1
}
);
assert_eq!(got[997].1, format!("e{}", "\u{301}".repeat(2000)));
let tail = got
.iter()
.skip(998)
.map(|(s, t)| (s.cell.get(), s.width, t.as_ref()))
.collect::<Vec<_>>();
assert_eq!(
tail,
[
(998, 1, "\t"),
(999, 2, "界"),
(1001, 2, "🧑🚀"),
(1003, 1, "Z")
]
);
}
#[test]
fn clipping_preserves_cells_not_partial_glyphs() {
let span = GraphemeSpan {
byte: 0,
cell: DisplayColumn::new(4),
width: 2,
};
assert_eq!(
clip(span, DisplayColumn::new(5), 4),
Some(CellClip {
x: 0,
width: 1,
complete: false
})
);
assert_eq!(
clip(span, DisplayColumn::new(3), 2),
Some(CellClip {
x: 1,
width: 1,
complete: false
})
);
assert_eq!(
clip(span, DisplayColumn::new(3), 3),
Some(CellClip {
x: 1,
width: 2,
complete: true
})
);
}
}