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