ultron_core/
text_buffer.rs

1use nalgebra::Point2;
2use std::iter::FromIterator;
3use std::iter::IntoIterator;
4use unicode_width::UnicodeWidthChar;
5
6pub const BLANK_CH: char = ' ';
7
8/// A text buffer where characters are manipulated visually with
9/// consideration on the unicode width of characters.
10/// Characters can span more than 1 cell, therefore
11/// visually manipulating text in a 2-dimensional way should consider using the unicode width.
12#[derive(Clone)]
13pub struct TextBuffer {
14    chars: Vec<Vec<Ch>>,
15    cursor: Point2<usize>,
16}
17
18impl Default for TextBuffer {
19    fn default() -> Self {
20        Self {
21            chars: vec![],
22            cursor: Point2::new(0, 0),
23        }
24    }
25}
26
27#[derive(Clone, Copy, Debug)]
28pub struct Ch {
29    pub ch: char,
30    pub width: usize,
31}
32
33impl Ch {
34    pub fn new(ch: char) -> Self {
35        Self {
36            width: ch.width().unwrap_or(0),
37            ch,
38        }
39    }
40}
41
42impl TextBuffer {
43    pub fn new_from_str(content: &str) -> Self {
44        Self {
45            chars: content
46                .lines()
47                .map(|line| line.chars().map(Ch::new).collect())
48                .collect(),
49            cursor: Point2::new(0, 0),
50        }
51    }
52
53    pub fn from_ch(chars: &[&[Ch]]) -> Self {
54        Self {
55            chars: chars.iter().map(|inner| inner.to_vec()).collect(),
56            cursor: Point2::new(0, 0),
57        }
58    }
59
60    pub fn is_empty(&self) -> bool {
61        self.chars.is_empty()
62    }
63
64    pub fn chars(&self) -> &[Vec<Ch>] {
65        &self.chars
66    }
67
68    /// return the total number of characters
69    /// excluding new lines
70    pub fn total_chars(&self) -> usize {
71        self.chars.iter().map(|line| line.len()).sum()
72    }
73
74    pub fn split_line_at_point(&self, loc: Point2<usize>) -> (String, String) {
75        let loc = self.point_to_index(loc);
76        let first = &self.chars[loc.y][0..loc.x];
77        let first_str = String::from_iter(first.iter().map(|ch| ch.ch));
78        let second = &self.chars[loc.y][loc.x..];
79        let second_str = String::from_iter(second.iter().map(|ch| ch.ch));
80        (first_str, second_str)
81    }
82
83    pub fn split_line_at_2_points(
84        &self,
85        loc: Point2<usize>,
86        loc2: Point2<usize>,
87    ) -> (String, String, String) {
88        let loc = self.point_to_index(loc);
89        let loc2 = self.point_to_index(loc2);
90
91        let first = &self.chars[loc.y][0..loc.x];
92        let first_str = String::from_iter(first.iter().map(|ch| ch.ch));
93        let second = &self.chars[loc.y][loc.x..loc2.x];
94        let second_str = String::from_iter(second.iter().map(|ch| ch.ch));
95        let third = &self.chars[loc.y][loc2.x..];
96        let third_str = String::from_iter(third.iter().map(|ch| ch.ch));
97
98        (first_str, second_str, third_str)
99    }
100
101    /// Remove the text within the start and end position then return the deleted text
102    pub fn cut_text_in_linear_mode(&mut self, start: Point2<usize>, end: Point2<usize>) -> String {
103        let start = self.point_to_index(start);
104        let end = self.point_to_index(end);
105        let is_one_line = start.y == end.y;
106        if is_one_line {
107            let selection: Vec<Ch> = self.chars[start.y].drain(start.x..=end.x).collect();
108            String::from_iter(selection.iter().map(|ch| ch.ch))
109        } else {
110            let end_text: Vec<Ch> = self.chars[end.y].drain(0..=end.x).collect();
111
112            let mid_text_range = start.y + 1..end.y;
113            let mid_text: Option<Vec<Vec<Ch>>> = if !mid_text_range.is_empty() {
114                Some(self.chars.drain(mid_text_range).collect())
115            } else {
116                None
117            };
118            let start_text: Vec<Ch> = self.chars[start.y].drain(start.x..).collect();
119
120            let start_text_str: String = String::from_iter(start_text.iter().map(|ch| ch.ch));
121
122            let end_text_str: String = String::from_iter(end_text.iter().map(|ch| ch.ch));
123
124            if let Some(mid_text) = mid_text {
125                let mid_text_str: String = mid_text
126                    .iter()
127                    .map(|line| String::from_iter(line.iter().map(|ch| ch.ch)))
128                    .collect::<Vec<_>>()
129                    .join("\n");
130
131                [start_text_str, mid_text_str, end_text_str].join("\n")
132            } else {
133                [start_text_str, end_text_str].join("\n")
134            }
135        }
136    }
137
138    /// get the text in between start and end if selected in linear mode
139    pub fn get_text_in_linear_mode(&self, start: Point2<usize>, end: Point2<usize>) -> String {
140        let start = self.point_to_index(start);
141        let end = self.point_to_index(end);
142        let is_one_line = start.y == end.y;
143        if is_one_line {
144            let selection: &[Ch] = &self.chars[start.y][start.x..=end.x];
145            String::from_iter(selection.iter().map(|ch| ch.ch))
146        } else {
147            let start_text: &[Ch] = &self.chars[start.y][start.x..];
148
149            let mid_text_range = start.y + 1..end.y;
150            let mid_text: Option<&[Vec<Ch>]> = if !mid_text_range.is_empty() {
151                Some(&self.chars[mid_text_range])
152            } else {
153                None
154            };
155
156            let end_text: &[Ch] = &self.chars[end.y][0..=end.x];
157            let start_text_str: String = String::from_iter(start_text.iter().map(|ch| ch.ch));
158
159            let end_text_str: String = String::from_iter(end_text.iter().map(|ch| ch.ch));
160
161            if let Some(mid_text) = mid_text {
162                let mid_text_str: String = mid_text
163                    .iter()
164                    .map(|line| String::from_iter(line.iter().map(|ch| ch.ch)))
165                    .collect::<Vec<_>>()
166                    .join("\n");
167
168                [start_text_str, mid_text_str, end_text_str].join("\n")
169            } else {
170                [start_text_str, end_text_str].join("\n")
171            }
172        }
173    }
174
175    /// get the text in between start and end if selected in block mode
176    pub fn get_text_in_block_mode(&self, start: Point2<usize>, end: Point2<usize>) -> String {
177        let start = self.point_to_index(start);
178        let end = self.point_to_index(end);
179        log::info!("here: {} {}", start, end);
180        (start.y..=end.y)
181            .map(|y| {
182                if let Some(chars) = &self.chars.get(y) {
183                    let text =
184                        (start.x..=end.x).map(|x| chars.get(x).map(|ch| ch.ch).unwrap_or(BLANK_CH));
185                    String::from_iter(text)
186                } else {
187                    String::new()
188                }
189            })
190            .collect::<Vec<_>>()
191            .join("\n")
192    }
193
194    pub fn cut_text_in_block_mode(&mut self, start: Point2<usize>, end: Point2<usize>) -> String {
195        let start = self.point_to_index(start);
196        let end = self.point_to_index(end);
197        (start.y..=end.y)
198            .map(|y| {
199                let text = self.chars[y].drain(start.x..=end.x);
200                String::from_iter(text.map(|ch| ch.ch))
201            })
202            .collect::<Vec<_>>()
203            .join("\n")
204    }
205
206    /// paste the text block in the cursor location
207    pub fn paste_text_in_block_mode(&mut self, text_block: String) {
208        for (line_index, line) in text_block.lines().enumerate() {
209            let mut width = 0;
210            let y = self.cursor.y + line_index;
211            for ch in line.chars() {
212                let x = self.cursor.x + width;
213                self.replace_char(Point2::new(x, y), ch);
214                width += ch.width().unwrap_or(0);
215            }
216        }
217    }
218}
219
220/// text manipulation
221/// This are purely manipulating text into the text buffer.
222/// The cursor shouldn't be move here, since it is done by the commands functions
223impl TextBuffer {
224    /// the total number of lines of this text canvas
225    pub fn total_lines(&self) -> usize {
226        self.chars.len()
227    }
228
229    /// return the number of characters to represent the line number of the last line of
230    /// this text buffer
231    pub fn numberline_wide(&self) -> usize {
232        self.total_lines().to_string().len()
233    }
234
235    pub fn lines(&self) -> Vec<String> {
236        self.chars
237            .iter()
238            .map(|line| String::from_iter(line.iter().map(|ch| ch.ch)))
239            .collect()
240    }
241
242    /// return the first non blank line
243    pub fn first_non_blank_line(&self) -> Option<usize> {
244        self.chars
245            .iter()
246            .enumerate()
247            .find_map(|(line_index, line)| {
248                if line.iter().any(|ch| ch.ch != BLANK_CH) {
249                    Some(line_index)
250                } else {
251                    None
252                }
253            })
254    }
255
256    /// return the last non blank line
257    pub fn last_non_blank_line(&self) -> Option<usize> {
258        self.chars
259            .iter()
260            .enumerate()
261            .rev()
262            .find_map(|(line_index, line)| {
263                if line.iter().any(|ch| ch.ch != BLANK_CH) {
264                    Some(line_index)
265                } else {
266                    None
267                }
268            })
269    }
270
271    /// the width of the line at line `n`
272    pub fn line_width(&self, n: usize) -> usize {
273        self.chars
274            .get(n)
275            .map(|line| line.iter().map(|ch| ch.width).sum())
276            .unwrap_or(0)
277    }
278
279    /// get the length of the widest line
280    pub fn max_column_width(&self) -> usize {
281        self.chars
282            .iter()
283            .map(|line| line.iter().map(|ch| ch.width).sum())
284            .max()
285            .unwrap_or(0)
286    }
287
288    /// return rectangular position starting from 0,0 to contain all
289    /// the text
290    pub fn max_position(&self) -> Point2<usize> {
291        let last_line = self.total_lines().saturating_sub(1);
292        let max_column = self.max_column_width();
293        Point2::new(max_column, last_line)
294    }
295
296    pub fn last_char_position(&self) -> Point2<usize> {
297        let last_line = self.total_lines().saturating_sub(1);
298        let bottom_last_x = self.line_width(last_line).saturating_sub(1);
299        Point2::new(bottom_last_x, last_line)
300    }
301
302    /// break at line y and put the characters after x on the next line
303    pub fn break_line(&mut self, loc: Point2<usize>) {
304        self.ensure_before_cell_exist(loc);
305        let line = &self.chars[loc.y];
306        if let Some(break_point) = self.column_index(loc) {
307            let (break1, break2): (Vec<_>, Vec<_>) = line
308                .iter()
309                .enumerate()
310                .partition(|(i, _ch)| *i < break_point);
311
312            let break1: Vec<Ch> = break1.into_iter().map(|(_, ch)| *ch).collect();
313            let break2: Vec<Ch> = break2.into_iter().map(|(_, ch)| *ch).collect();
314            self.chars.remove(loc.y);
315            self.chars.insert(loc.y, break2);
316            self.chars.insert(loc.y, break1);
317        } else {
318            self.chars.insert(loc.y + 1, vec![]);
319        }
320    }
321
322    pub fn join_line(&mut self, loc: Point2<usize>) {
323        let next_line_index = loc.y.saturating_add(1);
324        let mut next_line = self.chars.remove(next_line_index);
325        self.chars[loc.y].append(&mut next_line);
326    }
327
328    /// ensure line at index y exist
329    pub fn ensure_line_exist(&mut self, y: usize) {
330        let total_lines = self.total_lines();
331        let diff = y.saturating_add(1).saturating_sub(total_lines);
332        for _ in 0..diff {
333            self.chars.push(vec![]);
334        }
335    }
336
337    pub fn ensure_before_line_exist(&mut self, y: usize) {
338        if y > 0 {
339            self.ensure_line_exist(y.saturating_sub(1));
340        }
341    }
342
343    /// ensure line in index y exist and the cell at index x
344    pub fn ensure_cell_exist(&mut self, loc: Point2<usize>) {
345        self.ensure_line_exist(loc.y);
346        let line_width = self.line_width(loc.y);
347        let diff = loc.x.saturating_add(1).saturating_sub(line_width);
348        for _ in 0..diff {
349            self.chars[loc.y].push(Ch::new(BLANK_CH));
350        }
351    }
352
353    pub fn ensure_before_cell_exist(&mut self, loc: Point2<usize>) {
354        self.ensure_line_exist(loc.y);
355        if loc.x > 0 {
356            self.ensure_cell_exist(Point2::new(loc.x.saturating_sub(1), loc.y));
357        }
358    }
359
360    /// calculate the column index base on position of x and y
361    /// and considering the unicode width of the characters
362    fn column_index(&self, loc: Point2<usize>) -> Option<usize> {
363        if let Some(line) = self.chars.get(loc.y) {
364            let mut width_sum = 0;
365            for (i, ch) in line.iter().enumerate() {
366                if width_sum == loc.x {
367                    return Some(i);
368                }
369                width_sum += ch.width;
370            }
371            None
372        } else {
373            None
374        }
375    }
376
377    /// translate this point into the correct index position
378    /// considering the character widths
379    fn point_to_index(&self, point: Point2<usize>) -> Point2<usize> {
380        let column_x = self.column_index(point).unwrap_or(point.x);
381        Point2::new(column_x, point.y)
382    }
383
384    /// insert a character at this x and y and move cells after it to the right
385    pub fn insert_char(&mut self, loc: Point2<usize>, ch: char) {
386        self.ensure_before_cell_exist(loc);
387        let new_ch = Ch::new(ch);
388        if let Some(column_index) = self.column_index(loc) {
389            let insert_index = column_index;
390            self.chars[loc.y].insert(insert_index, new_ch);
391        } else {
392            self.chars[loc.y].push(new_ch);
393        }
394    }
395
396    /// insert a text, must not contain a \n
397    fn insert_line_text(&mut self, loc: Point2<usize>, text: &str) {
398        let mut width_inc = 0;
399        for ch in text.chars() {
400            let new_ch = Ch::new(ch);
401            self.insert_char(Point2::new(loc.x + width_inc, loc.y), new_ch.ch);
402            width_inc += new_ch.width;
403        }
404    }
405
406    pub fn insert_text(&mut self, loc: Point2<usize>, text: &str) {
407        let mut start = loc.x;
408        for (i, line) in text.lines().enumerate() {
409            if i > 0 {
410                self.chars.insert(loc.y + 1, vec![]);
411            }
412            self.insert_line_text(Point2::new(start, loc.y + i), line);
413            start = 0;
414        }
415    }
416
417    /// replace the character at this location
418    pub fn replace_char(&mut self, loc: Point2<usize>, ch: char) -> Option<char> {
419        self.ensure_cell_exist(loc);
420        let column_index = self.column_index(loc).expect("must have a column index");
421        let ex_ch = self.chars[loc.y].remove(column_index);
422        self.chars[loc.y].insert(column_index, Ch::new(ch));
423        Some(ex_ch.ch)
424    }
425
426    /// get the character at this cursor position
427    pub fn get_char(&self, loc: Point2<usize>) -> Option<char> {
428        if let Some(line) = self.chars.get(loc.y) {
429            let column_index = self.column_index(loc);
430            column_index.and_then(|col| line.get(col).map(|ch| ch.ch))
431        } else {
432            None
433        }
434    }
435
436    /// delete character at this position
437    pub fn delete_char(&mut self, loc: Point2<usize>) -> Option<char> {
438        if let Some(column_index) = self.column_index(loc) {
439            let ex_ch = self.chars[loc.y].remove(column_index);
440            Some(ex_ch.ch)
441        } else {
442            None
443        }
444    }
445
446    /// return the position of the cursor
447    pub fn get_position(&self) -> Point2<usize> {
448        self.cursor
449    }
450}
451
452/// Command implementation here
453///
454/// functions that are preceeded with command also moves the
455/// cursor and highlight the texts
456///
457/// Note: methods that are preceeded with `command` such as `command_insert_char` are high level methods
458/// which has consequences in the text buffer such as moving the cursor.
459/// While there corresponding more primitive counter parts such as `insert_char` are low level
460/// commands, which doesn't move the cursor location
461impl TextBuffer {
462    pub fn command_insert_char(&mut self, ch: char) {
463        self.insert_char(self.cursor, ch);
464        let width = ch.width().expect("must have a unicode width");
465        self.move_x(width);
466    }
467
468    pub fn command_replace_char(&mut self, ch: char) -> Option<char> {
469        self.replace_char(self.cursor, ch)
470    }
471
472    pub fn command_insert_text(&mut self, text: &str) {
473        self.insert_text(self.cursor, text);
474    }
475
476    pub fn move_left(&mut self) {
477        self.cursor.x = self.cursor.x.saturating_sub(1);
478    }
479
480    pub fn move_left_start(&mut self) {
481        self.cursor.x = 0;
482    }
483
484    pub fn move_right(&mut self) {
485        self.cursor.x = self.cursor.x.saturating_add(1);
486    }
487
488    fn line_max_column(&self, line: usize) -> usize {
489        self.chars.get(line).map(|line| line.len()).unwrap_or(0)
490    }
491
492    fn current_line_max_column(&self) -> usize {
493        self.line_max_column(self.cursor.y)
494    }
495
496    pub fn move_right_clamped(&mut self) {
497        if self.cursor.x < self.current_line_max_column() {
498            self.move_right();
499        }
500    }
501
502    pub fn move_right_end(&mut self) {
503        self.cursor.x = self.current_line_max_column();
504    }
505
506    pub fn move_x(&mut self, x: usize) {
507        self.cursor.x = self.cursor.x.saturating_add(x);
508    }
509
510    pub fn move_y(&mut self, y: usize) {
511        self.cursor.y = self.cursor.y.saturating_add(y);
512    }
513
514    pub fn move_up(&mut self) {
515        self.cursor.y = self.cursor.y.saturating_sub(1);
516    }
517
518    pub fn move_down(&mut self) {
519        self.cursor.y = self.cursor.y.saturating_add(1);
520    }
521
522    pub fn move_up_clamped(&mut self) {
523        let target_line = self.cursor.y.saturating_sub(1);
524        let target_line_max_column = self.line_max_column(target_line);
525        if target_line < self.total_lines() {
526            if self.cursor.x > target_line_max_column {
527                self.cursor.x = target_line_max_column;
528            }
529            self.move_up()
530        }
531    }
532
533    pub fn clamp_position(&self, loc: Point2<usize>) -> Point2<usize> {
534        let line = loc.y;
535        let line_max_column = self.line_max_column(line);
536        let loc_x = if loc.x > line_max_column {
537            line_max_column
538        } else {
539            loc.x
540        };
541        Point2::new(loc_x, loc.y)
542    }
543
544    pub fn move_down_clamped(&mut self) {
545        let target_line = self.cursor.y.saturating_add(1);
546        let target_line_max_column = self.line_max_column(target_line);
547        if target_line < self.total_lines() {
548            if self.cursor.x > target_line_max_column {
549                self.cursor.x = target_line_max_column;
550            }
551            self.move_down()
552        }
553    }
554
555    pub fn set_position(&mut self, pos: Point2<usize>) {
556        self.cursor = pos;
557    }
558
559    /// set the position to the max_column of the line if it is out of
560    /// bounds
561    pub fn set_position_clamped(&mut self, pos: Point2<usize>) {
562        let total_lines = self.total_lines();
563        let mut y = pos.y;
564        if y > total_lines {
565            y = total_lines.saturating_sub(1);
566        }
567        let line_width = self.line_width(y);
568        let mut x = pos.x;
569        if x > line_width {
570            x = line_width.saturating_sub(1);
571        }
572        self.set_position(Point2::new(x, y))
573    }
574
575    pub fn command_break_line(&mut self, loc: Point2<usize>) {
576        self.break_line(loc);
577        self.move_left_start();
578        self.move_down();
579    }
580
581    pub fn command_join_line(&mut self, loc: Point2<usize>) {
582        self.join_line(loc);
583        self.set_position(loc);
584    }
585
586    pub fn command_delete_back(&mut self) -> Option<char> {
587        if self.cursor.x > 0 {
588            let c = self.delete_char(Point2::new(self.cursor.x.saturating_sub(1), self.cursor.y));
589            self.move_left();
590            c
591        } else {
592            None
593        }
594    }
595
596    pub fn command_delete_forward(&mut self) -> Option<char> {
597        self.delete_char(self.cursor)
598    }
599
600    /// move the cursor to position
601    pub fn move_to(&mut self, pos: Point2<usize>) {
602        self.set_position(pos);
603    }
604
605    /// clear the contents of this text buffer
606    pub fn clear(&mut self) {
607        self.chars.clear();
608    }
609}
610
611impl ToString for TextBuffer {
612    fn to_string(&self) -> String {
613        self.chars
614            .iter()
615            .map(|line| String::from_iter(line.iter().map(|ch| ch.ch)))
616            .collect::<Vec<_>>()
617            .join("\n")
618    }
619}