1use serde::{Deserialize, Serialize};
7use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete, UnicodeSegmentation};
8use unicode_width::UnicodeWidthStr;
9
10use crate::text::Document;
11
12#[derive(
14 Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
15)]
16pub struct ByteOffset(pub usize);
17
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub struct CharIdx(pub usize);
21
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub struct LineCol {
25 pub line: usize,
26 pub col: ByteOffset,
27}
28
29#[derive(
31 Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
32)]
33pub struct VisualCol(pub usize);
34
35pub fn byte_to_line_col(doc: &Document, pos: ByteOffset) -> LineCol {
36 let line = doc.line_of(pos);
37 LineCol {
38 line,
39 col: ByteOffset(pos.0 - doc.line_start(line).0),
40 }
41}
42
43pub fn line_col_to_byte(doc: &Document, lc: LineCol) -> ByteOffset {
46 if lc.line >= doc.line_count() {
47 return doc.len();
48 }
49 let start = doc.line_start(lc.line);
50 ByteOffset((start.0 + lc.col.0).min(doc.line_end(lc.line).0))
51}
52
53pub fn next_grapheme_boundary(doc: &Document, pos: ByteOffset) -> ByteOffset {
55 let len = doc.len();
56 if pos >= len {
57 return len;
58 }
59 let (mut chunk, mut chunk_start) = doc.chunk_at(pos);
60 let mut gc = GraphemeCursor::new(pos.0, len.0, true);
61 loop {
62 match gc.next_boundary(chunk, chunk_start.0) {
63 Ok(Some(n)) => return ByteOffset(n),
64 Ok(None) => return len,
65 Err(GraphemeIncomplete::NextChunk) => {
66 chunk_start = ByteOffset(chunk_start.0 + chunk.len());
67 chunk = doc.chunk_at(chunk_start).0;
68 }
69 Err(GraphemeIncomplete::PreContext(n)) => provide_context(doc, &mut gc, n),
70 Err(_) => return step_char_forward(doc, pos),
71 }
72 }
73}
74
75pub fn prev_grapheme_boundary(doc: &Document, pos: ByteOffset) -> ByteOffset {
77 if pos.0 == 0 {
78 return pos;
79 }
80 let (mut chunk, mut chunk_start) = doc.chunk_at(pos);
81 let mut gc = GraphemeCursor::new(pos.0, doc.len().0, true);
82 loop {
83 match gc.prev_boundary(chunk, chunk_start.0) {
84 Ok(Some(n)) => return ByteOffset(n),
85 Ok(None) => return ByteOffset(0),
86 Err(GraphemeIncomplete::PrevChunk) => {
87 (chunk, chunk_start) = doc.chunk_at(ByteOffset(chunk_start.0 - 1));
88 }
89 Err(GraphemeIncomplete::PreContext(n)) => provide_context(doc, &mut gc, n),
90 Err(_) => return step_char_backward(doc, pos),
91 }
92 }
93}
94
95fn provide_context(doc: &Document, gc: &mut GraphemeCursor, n: usize) {
96 let (ctx, ctx_start) = doc.chunk_at(ByteOffset(n - 1));
97 gc.provide_context(&ctx[..n - ctx_start.0], ctx_start.0);
98}
99
100fn step_char_forward(doc: &Document, pos: ByteOffset) -> ByteOffset {
101 ByteOffset(pos.0 + doc.char_at(pos).map_or(0, char::len_utf8))
102}
103
104fn step_char_backward(doc: &Document, pos: ByteOffset) -> ByteOffset {
105 let mut p = pos.0.saturating_sub(1);
106 while p > 0 && !doc.is_char_boundary(ByteOffset(p)) {
107 p -= 1;
108 }
109 ByteOffset(p)
110}
111
112pub fn grapheme_width(g: &str, col: usize, tab_width: usize) -> usize {
116 if g == "\t" {
117 let tw = tab_width.max(1);
118 tw - col % tw
119 } else {
120 g.width().max(1)
121 }
122}
123
124fn advance(s: &str, start: usize, tab_width: usize) -> usize {
126 s.graphemes(true)
127 .fold(start, |col, g| col + grapheme_width(g, col, tab_width))
128}
129
130pub fn byte_to_visual_col(doc: &Document, pos: ByteOffset, tab_width: usize) -> VisualCol {
131 let line_start = doc.line_start(doc.line_of(pos));
132 VisualCol(advance(&doc.slice(line_start..pos), 0, tab_width))
133}
134
135pub fn visual_col_to_byte(
138 doc: &Document,
139 line: usize,
140 col: VisualCol,
141 tab_width: usize,
142) -> ByteOffset {
143 let start = doc.line_start(line);
144 let text = doc.line_text(line);
145 let mut c = 0;
146 for (i, g) in text.grapheme_indices(true) {
147 let w = grapheme_width(g, c, tab_width);
148 if c + w > col.0 {
149 return ByteOffset(start.0 + i);
150 }
151 c += w;
152 }
153 ByteOffset(start.0 + text.len())
154}
155
156pub fn visual_col_to_byte_ceil(
160 doc: &Document,
161 line: usize,
162 col: VisualCol,
163 tab_width: usize,
164) -> ByteOffset {
165 let start = doc.line_start(line);
166 let text = doc.line_text(line);
167 let mut c = 0;
168 for (i, g) in text.grapheme_indices(true) {
169 if c >= col.0 {
170 return ByteOffset(start.0 + i);
171 }
172 c += grapheme_width(g, c, tab_width);
173 }
174 ByteOffset(start.0 + text.len())
175}
176
177pub fn line_visual_width(doc: &Document, line: usize, tab_width: usize) -> VisualCol {
179 VisualCol(advance(&doc.line_text(line), 0, tab_width))
180}
181
182pub fn cells(text: &str, tab_width: usize) -> Vec<(usize, usize, usize)> {
185 let mut col = 0;
186 let mut out = Vec::with_capacity(text.len());
187 if text.is_ascii() {
188 let tw = tab_width.max(1);
190 for (i, b) in text.bytes().enumerate() {
191 let w = if b == b'\t' { tw - col % tw } else { 1 };
192 out.push((i, col, w));
193 col += w;
194 }
195 } else {
196 for (i, g) in text.grapheme_indices(true) {
197 let w = grapheme_width(g, col, tab_width);
198 out.push((i, col, w));
199 col += w;
200 }
201 }
202 out
203}
204
205pub const MIN_WRAP_WIDTH: usize = 10;
207
208#[derive(Clone, Debug, PartialEq, Eq)]
210pub struct Wrapped {
211 pub starts: Vec<usize>,
213 pub start_cols: Vec<usize>,
215 pub indent: usize,
217}
218
219pub fn wrap_line(text: &str, width: usize, tab_width: usize) -> Wrapped {
227 let width = width.max(MIN_WRAP_WIDTH);
228 if text.len() <= width && text.bytes().all(|b| b.is_ascii() && b != b'\t') {
230 let lead = text.len() - text.trim_start_matches(' ').len();
231 return Wrapped {
232 starts: vec![0],
233 start_cols: vec![0],
234 indent: lead.min(width / 2),
235 };
236 }
237 let lead = text.len() - text.trim_start_matches([' ', '\t']).len();
238 let indent = advance(&text[..lead], 0, tab_width).min(width / 2);
239 let mut out = Wrapped {
240 starts: vec![0],
241 start_cols: vec![0],
242 indent,
243 };
244 let mut col = 0;
246 let gs: Vec<(usize, usize, usize, bool)> = if text.is_ascii() {
247 text.bytes()
250 .enumerate()
251 .map(|(i, b)| {
252 let w = if b == b'\t' {
253 let tw = tab_width.max(1);
254 tw - col % tw
255 } else {
256 1
257 };
258 col += w;
259 (i, col - w, w, b == b' ' || b == b'\t')
260 })
261 .collect()
262 } else {
263 text.grapheme_indices(true)
264 .map(|(i, g)| {
265 let w = grapheme_width(g, col, tab_width);
266 col += w;
267 (i, col - w, w, g == " " || g == "\t")
268 })
269 .collect()
270 };
271 let lead_graphemes = gs.iter().take_while(|g| g.3).count();
273 let (mut row_start, mut cap) = (0, width);
274 let mut candidate = None;
276 let mut j = 0;
277 while j < gs.len() {
278 let (_, col, w, space) = gs[j];
279 if j > row_start.max(lead_graphemes) && gs[j - 1].3 && !space {
280 candidate = Some(j);
281 }
282 let row_col = gs[row_start].1;
283 if col + w - row_col > cap && j > row_start {
284 let at = candidate.unwrap_or(j);
285 out.starts.push(gs[at].0);
286 out.start_cols.push(gs[at].1);
287 row_start = at;
288 cap = width - indent;
289 candidate = None;
290 j = at;
292 continue;
293 }
294 j += 1;
295 }
296 out
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 fn rows(text: &str, width: usize) -> Vec<&str> {
305 let w = wrap_line(text, width, 4);
306 let mut ends = w.starts[1..].to_vec();
307 ends.push(text.len());
308 w.starts
309 .iter()
310 .zip(ends)
311 .map(|(&a, b)| &text[a..b])
312 .collect()
313 }
314
315 #[test]
316 fn wrapping_breaks_after_whitespace_or_inside_long_words() {
317 assert_eq!(rows("short", 10), vec!["short"]);
318 assert_eq!(rows("aaaa bbbb cccc", 10), vec!["aaaa bbbb ", "cccc"]);
319 assert_eq!(
320 rows("abcdefghijklmnopqrstuvwxy", 10),
321 vec!["abcdefghij", "klmnopqrst", "uvwxy"]
322 );
323 assert_eq!(
326 rows(" xxxxxxxxxxxxxxxx", 10),
327 vec![" xxxxxx", "xxxxxx", "xxxx"]
328 );
329 assert_eq!(wrap_line(" x x", 10, 4).indent, 4);
330 assert_eq!(rows("漢字漢字漢字", 10), vec!["漢字漢字漢", "字"]);
332 assert_eq!(rows("ab\tcdefghijk", 10), vec!["ab\t", "cdefghijk"]);
333 assert_eq!(wrap_line("ab\tcdefghijk", 10, 4).start_cols, vec![0, 4]);
334 let marks = "e\u{301}e\u{301}e\u{301}👨\u{200d}👩\u{200d}👧xxxxxxx";
336 assert_eq!(
337 rows(marks, 10),
338 vec!["e\u{301}e\u{301}e\u{301}👨\u{200d}👩\u{200d}👧xxxxx", "xx"]
339 );
340 assert_eq!(rows("aaaaaaaaaaaa", 3), vec!["aaaaaaaaaa", "aa"]);
342 }
343
344 fn boundaries(text: &str) -> Vec<usize> {
345 let doc = Document::new(text);
346 let mut out = vec![0];
347 let mut p = ByteOffset(0);
348 while p < doc.len() {
349 p = next_grapheme_boundary(&doc, p);
350 out.push(p.0);
351 }
352 let mut back = vec![doc.len().0];
353 let mut p = doc.len();
354 while p.0 > 0 {
355 p = prev_grapheme_boundary(&doc, p);
356 back.push(p.0);
357 }
358 back.reverse();
359 assert_eq!(out, back, "forward and backward stepping disagree");
360 out
361 }
362
363 #[test]
364 fn grapheme_stepping() {
365 assert_eq!(boundaries("e\u{301}x"), vec![0, 3, 4]);
367 assert_eq!(boundaries("👨👩👧!"), vec![0, 18, 19]);
369 assert_eq!(boundaries("👍🏽"), vec![0, 8]);
371 assert_eq!(boundaries("漢字"), vec![0, 3, 6]);
372 assert_eq!(boundaries("a\nb"), vec![0, 1, 2, 3]);
373 assert_eq!(boundaries(""), vec![0]);
374 }
375
376 #[test]
377 fn grapheme_stepping_across_rope_chunks() {
378 let unit = "ab e\u{301}👨👩👧漢\t";
381 let text = unit.repeat(500);
382 let b = boundaries(&text);
383 let expected: Vec<usize> = std::iter::once(0)
384 .chain(text.grapheme_indices(true).map(|(i, g)| i + g.len()))
385 .collect();
386 assert_eq!(b, expected);
387 }
388
389 #[test]
390 fn visual_columns() {
391 let doc = Document::new("\ta\t漢e\u{301}👨👩👧x\nnext");
392 let cols: Vec<usize> = [0, 1, 2, 3, 6, 9, 27, 28]
393 .iter()
394 .map(|&b| byte_to_visual_col(&doc, ByteOffset(b), 4).0)
395 .collect();
396 assert_eq!(cols, vec![0, 4, 5, 8, 10, 11, 13, 14]);
398 assert_eq!(line_visual_width(&doc, 0, 4), VisualCol(14));
399 assert_eq!(byte_to_visual_col(&doc, ByteOffset(29), 4), VisualCol(0));
400 }
401
402 #[test]
403 fn visual_col_to_byte_snaps_left_inside_wide_glyphs_and_tabs() {
404 let doc = Document::new("\ta漢b");
405 let at = |c| visual_col_to_byte(&doc, 0, VisualCol(c), 4).0;
406 assert_eq!(at(0), 0);
407 assert_eq!(at(2), 0); assert_eq!(at(4), 1);
409 assert_eq!(at(5), 2);
410 assert_eq!(at(6), 2); assert_eq!(at(7), 5);
412 assert_eq!(at(99), 6); }
414
415 #[test]
416 fn line_col_round_trip() {
417 let doc = Document::new("ab\ncdé\n");
418 for b in [0, 1, 2, 3, 4, 5, 7, 8] {
419 let lc = byte_to_line_col(&doc, ByteOffset(b));
420 assert_eq!(line_col_to_byte(&doc, lc), ByteOffset(b));
421 }
422 assert_eq!(
423 byte_to_line_col(&doc, ByteOffset(5)),
424 LineCol {
425 line: 1,
426 col: ByteOffset(2)
427 }
428 );
429 }
430}