1use std::borrow::Cow;
13
14use ropey::RopeSlice;
15use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete, UnicodeSegmentation};
16use unicode_width::UnicodeWidthStr;
17
18use crate::id::DisplayColumn;
19
20pub fn printable_grapheme(grapheme: &str) -> &str {
23 if grapheme.chars().any(char::is_control) {
24 "\u{fffd}"
25 } else {
26 grapheme
27 }
28}
29
30pub fn printable_text<'a>(text: impl Into<Cow<'a, str>>) -> Cow<'a, str> {
33 let text = text.into();
34 if !text.chars().any(char::is_control) {
35 return text;
36 }
37 let mut output = String::with_capacity(text.len());
38 for grapheme in text.graphemes(true) {
39 output.push_str(printable_grapheme(grapheme));
40 }
41 Cow::Owned(output)
42}
43
44fn grapheme_width(text: &str, cell: DisplayColumn, tab: usize) -> usize {
47 let tab = tab.max(1);
48 if text == "\t" {
49 tab - cell.get() % tab
50 } else {
51 UnicodeWidthStr::width(printable_grapheme(text))
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct GraphemeSpan {
58 pub byte: usize,
60 pub cell: DisplayColumn,
62 pub width: usize,
65}
66
67pub struct RopeGraphemes<'a> {
71 text: RopeSlice<'a>,
72 cursor: GraphemeCursor,
73 chunk: &'a str,
74 chunk_start: usize,
75 cell: DisplayColumn,
76 tab: usize,
77}
78
79impl<'a> RopeGraphemes<'a> {
80 pub fn new(text: RopeSlice<'a>, tab: usize) -> Self {
81 Self::new_at(text, tab, DisplayColumn::new(0))
82 }
83
84 pub fn new_at(text: RopeSlice<'a>, tab: usize, cell: DisplayColumn) -> Self {
87 let (chunk, chunk_start, _, _) = text.chunk_at_byte(0);
88 Self {
89 text,
90 cursor: GraphemeCursor::new(0, text.len_bytes(), true),
91 chunk,
92 chunk_start,
93 cell,
94 tab: tab.max(1),
95 }
96 }
97}
98
99impl<'a> Iterator for RopeGraphemes<'a> {
100 type Item = (GraphemeSpan, Cow<'a, str>);
101
102 fn next(&mut self) -> Option<Self::Item> {
103 let start = self.cursor.cur_cursor();
104 if start == self.text.len_bytes() {
105 return None;
106 }
107 let end = loop {
108 match self.cursor.next_boundary(self.chunk, self.chunk_start) {
109 Ok(Some(end)) => break end,
110 Ok(None) => return None,
111 Err(GraphemeIncomplete::NextChunk) => {
112 let next = self.chunk_start + self.chunk.len();
113 let (chunk, offset, _, _) = self.text.chunk_at_byte(next);
114 self.chunk = chunk;
115 self.chunk_start = offset;
116 }
117 Err(GraphemeIncomplete::PreContext(end)) => {
118 let (chunk, offset, _, _) = self.text.chunk_at_byte(end - 1);
119 self.cursor.provide_context(&chunk[..end - offset], offset);
120 }
121 Err(other) => unreachable!("forward grapheme traversal: {other:?}"),
122 }
123 };
124 let text = if start >= self.chunk_start && end <= self.chunk_start + self.chunk.len() {
125 Cow::Borrowed(&self.chunk[start - self.chunk_start..end - self.chunk_start])
127 } else {
128 let slice = self.text.byte_slice(start..end);
129 match slice.as_str() {
130 Some(text) => Cow::Borrowed(text),
131 None => Cow::Owned(slice.to_string()),
132 }
133 };
134 let width = grapheme_width(&text, self.cell, self.tab);
135 let span = GraphemeSpan {
136 byte: start,
137 cell: self.cell,
138 width,
139 };
140 self.cell += width;
141 Some((span, text))
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub struct CellClip {
148 pub x: usize,
150 pub width: usize,
152 pub complete: bool,
155}
156
157pub fn clip(span: GraphemeSpan, origin: DisplayColumn, width: usize) -> Option<CellClip> {
160 let left = span.cell.get().max(origin.get());
161 let end = span.cell.get() + span.width;
162 let right = end.min(origin.get().saturating_add(width));
163 (left < right).then(|| CellClip {
164 x: left - origin.get(),
165 width: right - left,
166 complete: left == span.cell.get() && right == end,
167 })
168}
169
170#[derive(Debug, Clone, Default)]
172pub struct LineLayout {
173 spans: Vec<GraphemeSpan>,
174 pub len_bytes: usize,
176 pub width: DisplayColumn,
178}
179
180impl LineLayout {
181 pub fn build(text: &str, tab: usize) -> Self {
184 let mut spans = Vec::new();
185 let mut cell = DisplayColumn::new(0);
186 for (byte, text) in text.grapheme_indices(true) {
187 let width = grapheme_width(text, cell, tab);
188 spans.push(GraphemeSpan { byte, cell, width });
189 cell += width;
190 }
191 Self {
192 spans,
193 len_bytes: text.len(),
194 width: cell,
195 }
196 }
197
198 pub fn spans(&self) -> &[GraphemeSpan] {
200 &self.spans
201 }
202
203 pub fn cell_at_byte(&self, byte: usize) -> DisplayColumn {
207 if byte >= self.len_bytes {
208 return self.width;
209 }
210 let next = self.spans.partition_point(|s| s.byte <= byte);
211 next.checked_sub(1)
212 .map_or(DisplayColumn::new(0), |i| self.spans[i].cell)
213 }
214
215 pub fn byte_at_cell(&self, cell: DisplayColumn) -> usize {
219 if cell >= self.width {
220 return self.len_bytes;
221 }
222 let next = self.spans.partition_point(|s| s.cell <= cell);
223 next.checked_sub(1).map_or(0, |i| self.spans[i].byte)
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn absolute_tabs_and_unicode_cells() {
233 let text = "ab\t界e\u{301}\x1bZ";
234 for (tab, cells, width) in [(3, [0, 1, 2, 3, 5, 6, 7], 8), (4, [0, 1, 2, 4, 6, 7, 8], 9)] {
235 let layout = LineLayout::build(text, tab);
236 assert_eq!(
237 layout
238 .spans()
239 .iter()
240 .map(|s| s.cell.get())
241 .collect::<Vec<_>>(),
242 cells
243 );
244 assert_eq!(layout.width.get(), width);
245 assert_eq!(layout.cell_at_byte(8).get(), cells[4]);
246 assert_eq!(layout.byte_at_cell(DisplayColumn::new(cells[3] + 1)), 3);
247 assert_eq!(layout.byte_at_cell(DisplayColumn::new(width)), text.len());
248 }
249 }
250
251 #[test]
252 fn columns_do_not_alias_at_terminal_limit() {
253 let text = format!("{}\t界Z", "x".repeat(70_001));
254 let layout = LineLayout::build(&text, 4);
255 assert_eq!(layout.cell_at_byte(70_002).get(), 70_004);
256 assert_eq!(layout.cell_at_byte(70_005).get(), 70_006);
257 assert_eq!(layout.byte_at_cell(DisplayColumn::new(70_005)), 70_002);
258 assert_eq!(LineLayout::build("x\tZ", 300).cell_at_byte(2).get(), 300);
259 }
260
261 #[test]
262 fn rope_chunk_boundaries_preserve_extended_clusters() {
263 let text = format!("{}e{}\t界🧑🚀Z", "a".repeat(997), "\u{301}".repeat(2000));
264 let rope = ropey::Rope::from_str(&text);
265 let got = RopeGraphemes::new(rope.slice(..), 3).collect::<Vec<_>>();
266 assert_eq!(
267 got[997].0,
268 GraphemeSpan {
269 byte: 997,
270 cell: DisplayColumn::new(997),
271 width: 1
272 }
273 );
274 assert_eq!(got[997].1, format!("e{}", "\u{301}".repeat(2000)));
275 let tail = got
276 .iter()
277 .skip(998)
278 .map(|(s, t)| (s.cell.get(), s.width, t.as_ref()))
279 .collect::<Vec<_>>();
280 assert_eq!(
281 tail,
282 [
283 (998, 1, "\t"),
284 (999, 2, "界"),
285 (1001, 2, "🧑🚀"),
286 (1003, 1, "Z")
287 ]
288 );
289 }
290
291 #[test]
292 fn clipping_preserves_cells_not_partial_glyphs() {
293 let span = GraphemeSpan {
294 byte: 0,
295 cell: DisplayColumn::new(4),
296 width: 2,
297 };
298 assert_eq!(
299 clip(span, DisplayColumn::new(5), 4),
300 Some(CellClip {
301 x: 0,
302 width: 1,
303 complete: false
304 })
305 );
306 assert_eq!(
307 clip(span, DisplayColumn::new(3), 2),
308 Some(CellClip {
309 x: 1,
310 width: 1,
311 complete: false
312 })
313 );
314 assert_eq!(
315 clip(span, DisplayColumn::new(3), 3),
316 Some(CellClip {
317 x: 1,
318 width: 2,
319 complete: true
320 })
321 );
322 }
323}