1use crate::{
3 buffer::{Buffer, Chars, GapBuffer},
4 config::ColorScheme,
5 config_handle, die,
6 dot::Range,
7 editor::{Click, MiniBufferState},
8 input::Event,
9 key::{Input, MouseButton, MouseEvent},
10 restore_terminal_state,
11 term::{
12 clear_screen, enable_alternate_screen, enable_mouse_support, enable_raw_mode, get_termios,
13 get_termsize, register_signal_handler, win_size_changed, CurShape, Cursor, Style, Styles,
14 RESET_STYLE,
15 },
16 ts::{LineIter, RangeToken},
17 ui::{
18 layout::{Column, Window},
19 Layout, StateChange, UserInterface,
20 },
21 ziplist, ORIGINAL_TERMIOS, VERSION,
22};
23use std::{
24 cell::RefCell,
25 char,
26 cmp::{min, Ordering},
27 collections::HashMap,
28 io::{stdin, stdout, Read, Stdout, Write},
29 iter::{repeat_n, Peekable},
30 panic,
31 rc::Rc,
32 sync::mpsc::Sender,
33 thread::{spawn, JoinHandle},
34 time::Instant,
35};
36use unicode_width::UnicodeWidthChar;
37
38const HLINE: &str = "-";
40const VLINE: &str = "β";
41const TSTR: &str = "β";
42const XSTR: &str = "βΌ";
43
44fn box_draw_str(s: &str, cs: &ColorScheme) -> String {
45 format!("{}{}{s}", Style::Fg(cs.minibuffer_hl), Style::Bg(cs.bg))
46}
47
48#[derive(Debug)]
49pub struct Tui {
50 stdout: Stdout,
51 screen_rows: usize,
52 screen_cols: usize,
53 status_message: String,
54 last_status: Instant,
55 vstr: String,
57 xstr: String,
58 tstr: String,
59 hvh: String,
60 vh: String,
61 style_cache: Rc<RefCell<HashMap<String, String>>>,
64}
65
66impl Default for Tui {
67 fn default() -> Self {
68 Self::new()
69 }
70}
71
72impl Drop for Tui {
73 fn drop(&mut self) {
74 restore_terminal_state(&mut self.stdout);
75 }
76}
77
78impl Tui {
79 pub fn new() -> Self {
80 let mut tui = Self {
81 stdout: stdout(),
82 screen_rows: 0,
83 screen_cols: 0,
84 status_message: String::new(),
85 last_status: Instant::now(),
86 vstr: String::new(),
87 tstr: String::new(),
88 xstr: String::new(),
89 hvh: String::new(),
90 vh: String::new(),
91 style_cache: Default::default(),
92 };
93 tui.update_cached_elements();
94
95 tui
96 }
97
98 fn update_cached_elements(&mut self) {
99 let cs = &config_handle!().colorscheme;
100 let vstr = box_draw_str(VLINE, cs);
101 let hstr = box_draw_str(HLINE, cs);
102 self.tstr = box_draw_str(TSTR, cs);
103 self.xstr = box_draw_str(XSTR, cs);
104 self.hvh = format!("{hstr}{vstr}{hstr}");
105 self.vh = format!("{vstr}{hstr}");
106 self.vstr = vstr;
107 self.style_cache.borrow_mut().clear();
108 }
109
110 fn render_banner(&self, screen_rows: usize, cs: &ColorScheme) -> Vec<String> {
111 let mut lines = Vec::with_capacity(screen_rows);
112 let (w_lnum, w_sgncol) = (1, 3);
113 let y_banner = self.screen_rows / 3;
114
115 let banner_line = |mut banner: String| {
116 let mut buf = String::new();
117 banner.truncate(self.screen_cols - w_sgncol);
118 let padding = (self.screen_cols - w_sgncol - banner.len()) / 2;
119 buf.push_str(&" ".repeat(padding));
120 buf.push_str(&banner);
121
122 buf
123 };
124
125 for y in 0..screen_rows {
126 let mut line = format!(
127 "{}{}~ {VLINE:>width$}{}",
128 Style::Fg(cs.signcol_fg),
129 Style::Bg(cs.bg),
130 Style::Fg(cs.fg),
131 width = w_lnum
132 );
133
134 if y == y_banner && y < screen_rows {
135 line.push_str(&banner_line(format!("ad editor :: version {VERSION}")));
136 } else if y == y_banner + 1 && y + 1 < screen_rows {
137 line.push_str(&banner_line("type :help to view help".to_string()));
138 }
139 line.push_str(&format!("{}\r\n", Cursor::ClearRight));
140 lines.push(line);
141 }
142
143 lines
144 }
145
146 fn render_status_bar(&self, cs: &ColorScheme, mode_name: &str, b: &Buffer) -> String {
147 let lstatus = format!(
148 "{} {} - {} lines {}",
149 mode_name,
150 b.display_name(),
151 b.len_lines(),
152 if b.dirty { "[+]" } else { "" }
153 );
154 let rstatus = b.dot.addr(b);
155 let width = self.screen_cols - lstatus.len();
156
157 format!(
158 "{}{}{lstatus}{rstatus:>width$}{}\r\n",
159 Style::Bg(cs.bar_bg),
160 Style::Fg(cs.fg),
161 Style::Reset
162 )
163 }
164
165 fn render_message_bar(
167 &self,
168 cs: &ColorScheme,
169 pending_keys: &[Input],
170 status_timeout: u64,
171 ) -> String {
172 let mut buf = String::new();
173 buf.push_str(&Cursor::ClearRight.to_string());
174
175 let mut msg = self.status_message.clone();
176 msg.truncate(self.screen_cols.saturating_sub(10));
177
178 let pending = render_pending(pending_keys);
179 let delta = (Instant::now() - self.last_status).as_secs();
180
181 if !msg.is_empty() && delta < status_timeout {
182 let width = self.screen_cols - msg.len() - 10;
183 buf.push_str(&format!(
184 "{}{}{msg}{pending:>width$} ",
185 Style::Fg(cs.fg),
186 Style::Bg(cs.bg)
187 ));
188 } else {
189 let width = self.screen_cols - 10;
190 buf.push_str(&format!(
191 "{}{}{pending:>width$} ",
192 Style::Fg(cs.fg),
193 Style::Bg(cs.bg)
194 ));
195 }
196
197 buf
198 }
199
200 fn render_minibuffer_state(
201 &self,
202 mb: &MiniBufferState<'_>,
203 tabstop: usize,
204 cs: &ColorScheme,
205 ) -> Vec<String> {
206 let mut lines = Vec::new();
207
208 if let Some(b) = mb.b {
209 for i in mb.top..=mb.bottom {
210 let slice = b.line(i).unwrap();
211 let bg = if i == mb.selected_line_idx {
212 cs.minibuffer_hl
213 } else {
214 cs.bg
215 };
216
217 let mut cols = 0;
218 let mut chars = slice.chars().peekable();
219 let mut rline = Styles {
220 fg: Some(cs.fg),
221 bg: Some(bg),
222 ..Default::default()
223 }
224 .to_string();
225
226 render_chars(
227 &mut chars,
228 None,
229 self.screen_cols,
230 tabstop,
231 &mut cols,
232 &mut rline,
233 );
234
235 if i == mb.selected_line_idx && cols < self.screen_cols {
236 rline.push_str(&Style::Bg(cs.minibuffer_hl).to_string());
237 }
238
239 let len = min(self.screen_cols, rline.len());
240 let width = self.screen_cols;
241 lines.push(format!(
242 "{:<width$}{}\r\n",
243 &rline[0..len],
244 Cursor::ClearRight
245 ));
246 }
247 }
248
249 lines.push(format!(
250 "{}{}{}{}{}",
251 Style::Fg(cs.fg),
252 Style::Bg(cs.bg),
253 mb.prompt,
254 mb.input,
255 Cursor::ClearRight
256 ));
257
258 lines
259 }
260}
261
262impl UserInterface for Tui {
263 fn init(&mut self, tx: Sender<Event>) -> (usize, usize) {
264 let original_termios = get_termios();
265 enable_raw_mode(original_termios);
266 _ = ORIGINAL_TERMIOS.set(original_termios);
267
268 panic::set_hook(Box::new(|panic_info| {
269 let mut stdout = stdout();
270 restore_terminal_state(&mut stdout);
271 _ = stdout.flush();
272
273 std::thread::sleep(std::time::Duration::from_millis(300));
278 eprintln!("Fatal error:\n{panic_info}");
279 _ = std::fs::write("/tmp/ad.panic", format!("{panic_info}"));
280 }));
281
282 enable_mouse_support(&mut self.stdout);
283 enable_alternate_screen(&mut self.stdout);
284
285 unsafe { register_signal_handler() };
287
288 let (screen_rows, screen_cols) = get_termsize();
289 self.screen_rows = screen_rows;
290 self.screen_cols = screen_cols;
291
292 spawn_input_thread(tx);
293
294 (screen_rows, screen_cols)
295 }
296
297 fn shutdown(&mut self) {
298 clear_screen(&mut self.stdout);
299 }
300
301 fn state_change(&mut self, change: StateChange) {
302 match change {
303 StateChange::ConfigUpdated => self.update_cached_elements(),
304 StateChange::StatusMessage { msg } => {
305 self.status_message = msg;
306 self.last_status = Instant::now();
307 }
308 }
309 }
310
311 fn refresh(
312 &mut self,
313 mode_name: &str,
314 layout: &Layout,
315 pending_keys: &[Input],
316 held_click: Option<&Click>,
317 mb: Option<MiniBufferState<'_>>,
318 ) {
319 self.screen_rows = layout.screen_rows;
320 self.screen_cols = layout.screen_cols;
321 let w_minibuffer = mb.is_some();
322 let mb = mb.unwrap_or_default();
323 let active_buffer = layout.active_buffer();
324 let mb_lines = mb.b.map(|b| b.len_lines()).unwrap_or_default();
325 let mb_offset = if mb_lines > 0 { 1 } else { 0 };
326
327 let effective_screen_rows = self.screen_rows - (mb.bottom - mb.top) - mb_offset;
331
332 let conf = config_handle!();
333 let (cs, status_timeout, tabstop) = (&conf.colorscheme, conf.status_timeout, conf.tabstop);
334
335 let load_exec_range = match held_click {
336 Some(click) if click.btn == MouseButton::Right || click.btn == MouseButton::Middle => {
337 Some((click.btn == MouseButton::Right, click.selection))
338 }
339 _ => None,
340 };
341
342 let mut lines = Vec::with_capacity(self.screen_rows + 2);
344 lines.push(format!("{}{}", Cursor::Hide, Cursor::ToStart));
345
346 if layout.is_empty_scratch() {
347 lines.append(&mut self.render_banner(effective_screen_rows, cs));
348 } else {
349 lines.extend(WinsIter::new(
350 layout,
351 load_exec_range,
352 effective_screen_rows,
353 tabstop,
354 self,
355 cs,
356 ));
357 }
358
359 lines.push(self.render_status_bar(cs, mode_name, active_buffer));
360
361 if w_minibuffer {
362 lines.append(&mut self.render_minibuffer_state(&mb, tabstop, cs));
363 } else {
364 lines.push(self.render_message_bar(cs, pending_keys, status_timeout));
365 }
366
367 let (x, y) = if w_minibuffer {
369 (mb.cx, self.screen_rows + mb.n_visible_lines + 1)
370 } else {
371 layout.ui_xy(active_buffer)
372 };
373 lines.push(format!("{}{}", Cursor::To(x + 1, y + 1), Cursor::Show));
374
375 if let Err(e) = self.stdout.write_all(lines.join("").as_bytes()) {
376 die!("Unable to refresh screen: {e}");
377 }
378
379 if let Err(e) = self.stdout.flush() {
380 die!("Unable to refresh screen: {e}");
381 }
382 }
383
384 fn set_cursor_shape(&mut self, cur_shape: CurShape) {
385 if let Err(e) = self.stdout.write_all(cur_shape.to_string().as_bytes()) {
386 die!("Unable to write to stdout: {e}");
389 };
390 }
391}
392
393struct WinsIter<'a> {
394 col_iters: Vec<ColIter<'a>>,
395 buf: Vec<String>,
396 vstr: &'a str,
397 xstr: &'a str,
398 tstr: &'a str,
399 hvh: &'a str,
400 vh: &'a str,
401}
402
403impl<'a> WinsIter<'a> {
404 fn new(
405 layout: &'a Layout,
406 load_exec_range: Option<(bool, Range)>,
407 screen_rows: usize,
408 tabstop: usize,
409 tui: &'a Tui,
410 cs: &'a ColorScheme,
411 ) -> Self {
412 let col_iters: Vec<_> = layout
413 .cols
414 .iter()
415 .map(|(is_focus, col)| {
416 let rng = if is_focus { load_exec_range } else { None };
417 ColIter::new(
418 col,
419 layout,
420 rng,
421 screen_rows,
422 tabstop,
423 cs,
424 tui.style_cache.clone(),
425 )
426 })
427 .collect();
428 let buf = Vec::with_capacity(col_iters.len());
429
430 Self {
431 col_iters,
432 buf,
433 vstr: &tui.vstr,
434 xstr: &tui.xstr,
435 tstr: &tui.tstr,
436 hvh: &tui.hvh,
437 vh: &tui.vh,
438 }
439 }
440}
441
442impl Iterator for WinsIter<'_> {
443 type Item = String;
444
445 fn next(&mut self) -> Option<Self::Item> {
446 self.buf.clear();
447
448 for it in self.col_iters.iter_mut() {
449 self.buf.push(it.next()?);
450 }
451
452 let mut buf = self.buf.join(self.vstr);
453 buf = buf.replace(self.hvh, self.xstr).replace(self.vh, self.tstr);
454 buf.push_str(&format!("{}\r\n", Cursor::ClearRight));
455
456 Some(buf)
457 }
458}
459
460struct ColIter<'a> {
461 inner: ziplist::Iter<'a, Window>,
462 current: Option<WinIter<'a>>,
463 layout: &'a Layout,
464 cs: &'a ColorScheme,
465 style_cache: Rc<RefCell<HashMap<String, String>>>,
466 load_exec_range: Option<(bool, Range)>,
467 screen_rows: usize,
468 tabstop: usize,
469 n_cols: usize,
470 yielded: usize,
471}
472
473impl<'a> ColIter<'a> {
474 fn new(
475 col: &'a Column,
476 layout: &'a Layout,
477 load_exec_range: Option<(bool, Range)>,
478 screen_rows: usize,
479 tabstop: usize,
480 cs: &'a ColorScheme,
481 style_cache: Rc<RefCell<HashMap<String, String>>>,
482 ) -> Self {
483 ColIter {
484 inner: col.wins.iter(),
485 current: None,
486 layout,
487 cs,
488 style_cache,
489 load_exec_range,
490 screen_rows,
491 tabstop,
492 n_cols: col.n_cols,
493 yielded: 0,
494 }
495 }
496}
497
498impl<'a> ColIter<'a> {
499 fn next_win_iter(&mut self) -> Option<WinIter<'a>> {
500 let (is_focus, w) = self.inner.next()?;
501 let b = self
502 .layout
503 .buffer_with_id(w.view.bufid)
504 .expect("valid buffer id");
505
506 let (w_lnum, _) = b.sign_col_dims();
507 let rng = if is_focus { self.load_exec_range } else { None };
508 let it = b.iter_tokenized_lines_from(w.view.row_off, rng);
509
510 Some(WinIter {
511 y: 0,
512 w_lnum,
513 n_cols: self.n_cols,
514 tabstop: self.tabstop,
515 it,
516 gb: &b.txt,
517 w,
518 cs: self.cs,
519 style_cache: self.style_cache.clone(),
520 })
521 }
522}
523
524impl Iterator for ColIter<'_> {
525 type Item = String;
526
527 fn next(&mut self) -> Option<Self::Item> {
528 if self.current.is_none() {
529 self.current = Some(self.next_win_iter()?);
530 }
531
532 let next_line = self.current.as_mut()?.next();
533 if self.yielded == self.screen_rows {
534 return None;
535 }
536 self.yielded += 1;
537
538 match next_line {
539 Some(line) => Some(line),
540 None => {
541 self.current = None;
542 Some(box_draw_str(&HLINE.repeat(self.n_cols), self.cs))
543 }
544 }
545 }
546}
547
548struct WinIter<'a> {
549 y: usize,
550 w_lnum: usize,
551 n_cols: usize,
552 tabstop: usize,
553 it: LineIter<'a>,
554 gb: &'a GapBuffer,
555 w: &'a Window,
556 cs: &'a ColorScheme,
557 style_cache: Rc<RefCell<HashMap<String, String>>>,
558}
559
560impl Iterator for WinIter<'_> {
561 type Item = String;
562
563 fn next(&mut self) -> Option<Self::Item> {
564 if self.y >= self.w.n_rows {
565 return None;
566 }
567 let file_row = self.y + self.w.view.row_off;
568 self.y += 1;
569
570 let next = self.it.next();
571
572 let line = match next {
573 None => {
574 let mut buf = format!(
575 "{}{}~ {VLINE:>width$}{}",
576 Style::Fg(self.cs.signcol_fg),
577 Style::Bg(self.cs.bg),
578 Style::Fg(self.cs.fg),
579 width = self.w_lnum
580 );
581 let padding = self.n_cols - self.w_lnum - 2;
582 buf.push_str(&" ".repeat(padding));
583
584 buf
585 }
586
587 Some(it) => {
588 let padding = self.w_lnum + 2;
590
591 format!(
592 "{}{} {:>width$}{VLINE}{}",
593 Style::Fg(self.cs.signcol_fg),
594 Style::Bg(self.cs.bg),
595 file_row + 1,
596 render_line(
597 self.gb,
598 it,
599 self.w.view.col_off,
600 self.n_cols - padding,
601 self.tabstop,
602 self.cs,
603 &mut self.style_cache
604 ),
605 width = self.w_lnum
606 )
607 }
608 };
609
610 Some(line)
611 }
612}
613
614fn render_pending(keys: &[Input]) -> String {
615 let mut s = String::new();
616 for k in keys {
617 match k {
618 Input::Char(c) if c.is_ascii_whitespace() => s.push_str(&format!("<{:x}>", *c as u8)),
619 Input::Char(c) => s.push(*c),
620 Input::Ctrl(c) => {
621 s.push('^');
622 s.push(*c);
623 }
624 Input::Alt(c) => {
625 s.push('^');
626 s.push('[');
627 s.push(*c);
628 }
629 Input::CtrlAlt(c) => {
630 s.push('^');
631 s.push('[');
632 s.push('^');
633 s.push(*c);
634 }
635
636 _ => (),
637 }
638 }
639
640 if s.len() > 10 {
641 s = s.split_off(s.len() - 10);
642 }
643
644 s
645}
646
647#[inline]
648fn skip_token_chars(
649 chars: &mut Peekable<Chars<'_>>,
650 tabstop: usize,
651 to_skip: &mut usize,
652) -> Option<usize> {
653 for ch in chars.by_ref() {
654 let w = if ch == '\t' {
655 tabstop
656 } else {
657 UnicodeWidthChar::width(ch).unwrap_or(1)
658 };
659
660 match (*to_skip).cmp(&w) {
661 Ordering::Less => {
662 let spaces = Some(w - *to_skip);
663 *to_skip = 0;
664 return spaces;
665 }
666
667 Ordering::Equal => {
668 *to_skip = 0;
669 break;
670 }
671
672 Ordering::Greater => *to_skip -= w,
673 }
674 }
675
676 None
677}
678
679fn render_chars(
680 chars: &mut Peekable<Chars<'_>>,
681 spaces: Option<usize>,
682 max_cols: usize,
683 tabstop: usize,
684 cols: &mut usize,
685 buf: &mut String,
686) {
687 if let Some(n) = spaces {
688 buf.extend(repeat_n(' ', n));
689 *cols = n;
690 }
691
692 for ch in chars {
693 if ch == '\n' {
694 break;
695 }
696 let w = if ch == '\t' {
697 tabstop
698 } else {
699 UnicodeWidthChar::width(ch).unwrap_or(1)
700 };
701
702 if *cols + w <= max_cols {
703 if ch == '\t' {
704 buf.extend(repeat_n(' ', tabstop));
708 } else {
709 buf.push(ch);
710 }
711 *cols += w;
712 } else {
713 break;
714 }
715 }
716
717 buf.push_str(RESET_STYLE);
718}
719
720fn render_line<'a>(
721 gb: &'a GapBuffer,
722 it: impl Iterator<Item = RangeToken<'a>>,
723 col_off: usize,
724 max_cols: usize,
725 tabstop: usize,
726 cs: &ColorScheme,
727 style_cache: &mut Rc<RefCell<HashMap<String, String>>>,
728) -> String {
729 let mut buf = String::new();
730 let mut to_skip = col_off;
731 let mut cols = 0;
732
733 for tk in it {
734 let slice = tk.as_slice(gb);
735 let mut chars = slice.chars().peekable();
736 let spaces = if to_skip > 0 {
737 let spaces = skip_token_chars(&mut chars, tabstop, &mut to_skip);
738 if to_skip > 0 || (chars.peek().is_none() && spaces.is_none()) {
739 continue;
740 }
741 spaces
742 } else {
743 None
744 };
745
746 let mut guard = style_cache.borrow_mut();
760 let style_str = match guard.get(tk.tag) {
761 Some(s) => s,
762 None => {
763 let s = cs.styles_for(tk.tag).to_string();
764 guard.insert(tk.tag.to_string(), s);
765 guard.get(tk.tag).unwrap()
766 }
767 };
768
769 buf.push_str(style_str);
770 render_chars(&mut chars, spaces, max_cols, tabstop, &mut cols, &mut buf);
771
772 if cols == max_cols {
773 break;
774 }
775 }
776
777 if cols < max_cols {
778 buf.push_str(&Style::Bg(cs.bg).to_string());
779 buf.extend(repeat_n(' ', max_cols - cols));
780 }
781
782 buf
783}
784
785fn spawn_input_thread(tx: Sender<Event>) -> JoinHandle<()> {
788 let mut stdin = stdin();
789
790 spawn(move || loop {
791 if let Some(key) = try_read_input(&mut stdin) {
792 _ = tx.send(Event::Input(key));
793 } else if win_size_changed() {
794 let (rows, cols) = get_termsize();
795 _ = tx.send(Event::WinsizeChanged { rows, cols });
796 }
797 })
798}
799
800fn try_read_char(stdin: &mut impl Read) -> Option<char> {
801 let mut buf: [u8; 4] = [0; 4];
802
803 for i in 0..4 {
804 if stdin.read_exact(&mut buf[i..i + 1]).is_err() {
805 return if i == 0 {
806 None
807 } else {
808 Some(char::REPLACEMENT_CHARACTER)
809 };
810 }
811
812 match std::str::from_utf8(&buf[0..i + 1]) {
813 Ok(s) => return s.chars().next(),
814 Err(e) if e.error_len().is_some() => return Some(char::REPLACEMENT_CHARACTER),
815 Err(_) => (),
816 }
817 }
818
819 Some(char::REPLACEMENT_CHARACTER)
821}
822
823fn try_read_input(stdin: &mut impl Read) -> Option<Input> {
824 let c = try_read_char(stdin)?;
825
826 match Input::from_char(c) {
828 Input::Esc => (),
829 key => return Some(key),
830 }
831
832 let c2 = match try_read_char(stdin) {
833 Some(c2) => c2,
834 None => return Some(Input::Esc),
835 };
836 let c3 = match try_read_char(stdin) {
837 Some(c3) => c3,
838 None => return Some(Input::try_from_seq2(c, c2).unwrap_or(Input::Esc)),
839 };
840
841 if let Some(key) = Input::try_from_seq2(c2, c3) {
842 return Some(key);
843 }
844
845 if c2 == '[' && c3.is_ascii_digit() {
846 if let Some('~') = try_read_char(stdin) {
847 if let Some(key) = Input::try_from_bracket_tilde(c3) {
848 return Some(key);
849 }
850 }
851 }
852
853 if c2 == '[' && c3 == '<' {
855 let mut buf = Vec::new();
856 let m;
857
858 loop {
859 match try_read_char(stdin) {
860 Some(c @ 'm' | c @ 'M') => {
861 m = c;
862 break;
863 }
864 Some(c) => buf.push(c as u8),
865 None => return None,
866 };
867 }
868 let s = String::from_utf8(buf).unwrap();
869 let nums: Vec<usize> = s.split(';').map(|s| s.parse::<usize>().unwrap()).collect();
870 let (b, x, y) = (nums[0], nums[1], nums[2]);
871
872 return MouseEvent::try_from_raw(b, x, y, m).map(Input::Mouse);
873 }
874
875 Some(Input::Esc)
876}
877
878#[cfg(test)]
879mod tests {
880 use super::*;
881 use crate::ts::{ByteRange, TK_DEFAULT};
882 use simple_test_case::test_case;
883 use std::{char::REPLACEMENT_CHARACTER, io};
884
885 #[test_case("a".as_bytes(), &['a']; "single ascii character")]
886 #[test_case(&[240, 159, 146, 150], &['π']; "single utf8 character")]
887 #[test_case(&[165, 159, 146, 150], &[REPLACEMENT_CHARACTER; 4]; "invalid utf8 with non-ascii prefix")]
888 #[test_case(&[65, 159, 146, 150], &['A', REPLACEMENT_CHARACTER, REPLACEMENT_CHARACTER, REPLACEMENT_CHARACTER]; "invalid utf8 with ascii prefix")]
889 #[test]
890 fn try_read_char_works(bytes: &[u8], expected: &[char]) {
891 let mut r = io::Cursor::new(bytes);
892 let mut chars = Vec::new();
893
894 while let Some(ch) = try_read_char(&mut r) {
895 chars.push(ch);
896 }
897
898 assert_eq!(&chars, expected);
899 }
900
901 fn rt(tag: &'static str, from: usize, to: usize) -> RangeToken<'static> {
902 RangeToken {
903 tag,
904 r: ByteRange { from, to },
905 }
906 }
907
908 #[test_case(0, 14, "foo\tbar baz", "!foo$| $!bar$| $!baz$# "; "full line padded to max cols")]
911 #[test_case(0, 12, "foo\tbar baz", "!foo$| $!bar$| $!baz$"; "full line")]
912 #[test_case(1, 11, "foo\tbar baz", "!oo$| $!bar$| $!baz$"; "skipping first character")]
913 #[test_case(3, 9, "foo\tbar baz", "| $!bar$| $!baz$"; "skipping first token")]
914 #[test_case(4, 8, "foo\tbar baz", "| $!bar$| $!baz$"; "skipping part way through a tab")]
915 #[test_case(0, 10, "δΈ\tη foo", "!δΈ$| $!η$| $!foo$"; "unicode full line")]
916 #[test_case(0, 12, "δΈ\tη foo", "!δΈ$| $!η$| $!foo$# "; "unicode full line padded to max cols")]
917 #[test_case(1, 9, "δΈ\tη foo", "! $| $!η$| $!foo$"; "unicode skipping first column of multibyte char")]
921 #[test]
922 fn render_line_correctly_skips_tokens(
923 col_off: usize,
924 max_cols: usize,
925 s: &str,
926 expected_template: &str,
927 ) {
928 let gb = GapBuffer::from(s);
929 let range_tokens = vec![
930 rt("a", 0, 3),
931 rt(TK_DEFAULT, 3, 4),
932 rt("a", 4, 7),
933 rt(TK_DEFAULT, 7, 8),
934 rt("a", 8, 11),
935 ];
936
937 let cs = ColorScheme::default();
938 let style_cache: HashMap<String, String> = [
939 ("a".to_owned(), "!".to_owned()),
940 (TK_DEFAULT.to_owned(), "|".to_owned()),
941 ]
942 .into_iter()
943 .collect();
944
945 let s = render_line(
946 &gb,
947 range_tokens.into_iter(),
948 col_off,
949 max_cols,
950 2,
951 &cs,
952 &mut Rc::new(RefCell::new(style_cache)),
953 );
954
955 let expected = expected_template
956 .replace("$", RESET_STYLE)
957 .replace("#", &Style::Bg(cs.bg).to_string());
958
959 assert_eq!(s, expected);
960 }
961}