ad_editor/ui/
tui.rs

1//! A terminal UI for ad
2use crate::{
3    ORIGINAL_TERMIOS,
4    buffer::{Buffer, Chars, GapBuffer},
5    config::{ColorScheme, Config},
6    config_handle, die,
7    dot::Range,
8    editor::{Click, MiniBufferState},
9    input::Event,
10    key::{Input, MouseButton, MouseEvent},
11    restore_terminal_state,
12    syntax::{LineIter, RangeToken},
13    term::{
14        CurShape, Cursor, RESET_STYLE, Style, Styles, clear_screen, enable_alternate_screen,
15        enable_bracketed_paste, enable_mouse_support, enable_raw_mode, get_termios, get_termsize,
16        register_signal_handler, win_size_changed,
17    },
18    ui::{
19        Layout, StateChange, UserInterface,
20        layout::{Column, Scratch, Window},
21    },
22    ziplist,
23};
24use std::{
25    char,
26    cmp::Ordering,
27    collections::HashMap,
28    fmt::Write as _,
29    io::{self, BufWriter, Read, StdoutLock, Write, stdin, stdout},
30    iter::{Peekable, repeat_n},
31    panic,
32    sync::{Arc, RwLock, mpsc::Sender},
33    thread::{JoinHandle, spawn},
34    time::Instant,
35};
36use tracing::debug;
37use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
38
39// If the screen dimensions drop below these values then we disable rendering
40const MIN_COLS: usize = 20;
41const MIN_ROWS: usize = 5;
42
43const H_STR: &str = "─";
44const V_STR: &str = "│";
45const TR_STR: &str = "├";
46const TL_STR: &str = "┤";
47const X_STR: &str = "┼";
48
49pub type Tui = GenericTui<StdoutLock<'static>>;
50
51#[derive(Debug)]
52pub struct GenericTui<W: Write> {
53    stdout: BufWriter<W>,
54    config: Arc<RwLock<Config>>,
55    status_message: String,
56    last_status: Instant,
57    mb_last_frame: bool,
58    frame: Frame,
59}
60
61impl Default for Tui {
62    fn default() -> Self {
63        Self::new(Default::default())
64    }
65}
66
67impl<W: Write> Drop for GenericTui<W> {
68    fn drop(&mut self) {
69        restore_terminal_state(&mut self.stdout);
70    }
71}
72
73impl Tui {
74    pub fn new(config: Arc<RwLock<Config>>) -> Self {
75        Self::new_with_stdout_handle(config, stdout().lock())
76    }
77}
78
79impl<W: Write> GenericTui<W> {
80    pub fn new_with_stdout_handle(config: Arc<RwLock<Config>>, stdout: W) -> Self {
81        Self {
82            stdout: BufWriter::new(stdout),
83            config,
84            status_message: String::new(),
85            last_status: Instant::now(),
86            mb_last_frame: false,
87            frame: Frame::new(),
88        }
89    }
90
91    pub fn set_size(&mut self, rows: usize, cols: usize) {
92        self.frame.screen_rows = rows;
93        self.frame.screen_cols = cols;
94    }
95
96    fn render(
97        &mut self,
98        mode_name: &str,
99        layout: &Layout,
100        n_running: usize,
101        pending_keys: &[Input],
102        held_click: Option<&Click>,
103        mb: Option<MiniBufferState<'_>>,
104    ) {
105        let conf = config_handle!(self);
106        let (cs, status_timeout, tabstop, max_mb_lines) = (
107            &conf.colorscheme,
108            conf.status_timeout,
109            conf.tabstop,
110            conf.minibuffer_lines,
111        );
112
113        // If we have a minibuffer open then that takes priority over an open scratch buffer
114        let w_minibuffer = mb.is_some();
115        let mb = mb.unwrap_or_default();
116        let active_buffer = layout.active_buffer();
117
118        let mb_has_lines = mb.b.map(|b| !b.is_empty()).unwrap_or_default();
119        let offset = if mb_has_lines {
120            mb.bottom - mb.top + 1
121        } else if !w_minibuffer && layout.scratch.is_visible {
122            max_mb_lines
123        } else {
124            0
125        };
126
127        // This is the screen size that we have to work with for the buffer content we currently want to
128        // display. If the minibuffer is active then it take priority over anything else and we always
129        // show the status bar as the final two lines of the UI.
130        let effective_screen_rows = self.frame.screen_rows.saturating_sub(offset);
131
132        let load_exec_range = match held_click {
133            Some(Click::Text { btn, selection, .. })
134                if *btn == MouseButton::Right || *btn == MouseButton::Middle =>
135            {
136                Some((*btn == MouseButton::Right, *selection))
137            }
138            _ => None,
139        };
140
141        let (load_exec_range, scratch_load_exec_range) = if layout.scratch.is_focused {
142            (None, load_exec_range)
143        } else {
144            (load_exec_range, None)
145        };
146
147        self.frame
148            .render_windows(layout, load_exec_range, effective_screen_rows, tabstop, cs);
149        self.frame
150            .render_status_bar(cs, mode_name, n_running, active_buffer);
151
152        self.frame.show_mb = w_minibuffer || layout.scratch.is_visible;
153        self.frame.show_msg_bar = !w_minibuffer;
154
155        if w_minibuffer {
156            self.frame.render_minibuffer_state(&mb, tabstop, cs);
157        } else if layout.scratch.is_visible {
158            self.frame
159                .render_scratch(&layout.scratch, scratch_load_exec_range, tabstop, cs);
160        };
161
162        if self.frame.show_msg_bar {
163            self.frame.render_message_bar(
164                cs,
165                pending_keys,
166                status_timeout,
167                self.status_message.clone(),
168                self.last_status,
169            );
170        };
171
172        let (cur_x, cur_y) = if w_minibuffer {
173            (mb.cx, self.frame.screen_rows + mb.n_visible_lines + 1)
174        } else {
175            layout.ui_xy()
176        };
177
178        self.frame.cur_x = cur_x;
179        self.frame.cur_y = cur_y;
180    }
181}
182
183impl<W: Write> UserInterface for GenericTui<W> {
184    fn init(&mut self, tx: Sender<Event>) -> (usize, usize) {
185        let original_termios = get_termios();
186        enable_raw_mode(original_termios);
187        _ = ORIGINAL_TERMIOS.set(original_termios);
188
189        panic::set_hook(Box::new(|panic_info| {
190            let mut stdout = stdout();
191            restore_terminal_state(&mut stdout);
192            _ = stdout.flush();
193
194            // Force capturing a backtrace for easier debugging
195            let bt = std::backtrace::Backtrace::force_capture();
196
197            // Restoring the terminal state to move us off of the alternate screen
198            // can race with our attempt to print the panic info so given that we
199            // are already in a fatal situation, sleeping briefly to ensure that
200            // the cause of the panic is visible before we exit isn't _too_ bad.
201            std::thread::sleep(std::time::Duration::from_millis(300));
202            eprintln!("Fatal error:\n{panic_info}\n{bt}");
203            _ = std::fs::write("/tmp/ad.panic", format!("{panic_info}\n{bt}"));
204        }));
205
206        enable_mouse_support(&mut self.stdout);
207        enable_alternate_screen(&mut self.stdout);
208        enable_bracketed_paste(&mut self.stdout);
209
210        // SAFETY: we only register our signal handler once
211        unsafe { register_signal_handler() };
212
213        let (screen_rows, screen_cols) = get_termsize();
214        self.frame.screen_rows = screen_rows;
215        self.frame.screen_cols = screen_cols;
216
217        spawn_input_thread(tx);
218
219        (screen_rows, screen_cols)
220    }
221
222    fn shutdown(&mut self) {
223        clear_screen(&mut self.stdout);
224    }
225
226    fn state_change(&mut self, change: StateChange) {
227        match change {
228            StateChange::ConfigUpdated => self.frame.style_cache.clear(),
229            StateChange::StatusMessage { msg } => {
230                self.status_message = msg;
231                self.last_status = Instant::now();
232            }
233        }
234    }
235
236    fn refresh(
237        &mut self,
238        mode_name: &str,
239        layout: &mut Layout,
240        n_running: usize,
241        pending_keys: &[Input],
242        held_click: Option<&Click>,
243        mb: Option<MiniBufferState<'_>>,
244    ) {
245        self.frame.screen_rows = layout.screen_rows;
246        self.frame.screen_cols = layout.screen_cols;
247        self.frame.show_msg_bar = mb.is_none();
248        let mb_this_frame = mb.is_some();
249
250        if self.frame.screen_cols < MIN_COLS || self.frame.screen_rows < MIN_ROWS {
251            return;
252        }
253
254        // If the UI changed or we have an active minibuffer then we need to rerender the UI.
255        // We also need to re-render on the frame after a minibuffer is closed in order to
256        // get rid of it, as none of the other buffers in the layout will be marked as changed
257        // since the last render.
258        let need_render = layout.changed_since_last_render()
259            || mb_this_frame
260            || self.mb_last_frame | held_click.is_some();
261
262        if need_render {
263            layout.update_visible_ts_state();
264            self.render(mode_name, layout, n_running, pending_keys, held_click, mb);
265            if let Err(e) = self.frame.write(&mut self.stdout) {
266                die!("Unable to refresh screen: {e}");
267            }
268        } else if self.frame.show_msg_bar {
269            // match self.render in not showing the message bar if the minibuffer is open
270            let conf = config_handle!(self);
271            let (cs, status_timeout) = (&conf.colorscheme, conf.status_timeout);
272            self.frame.render_message_bar(
273                cs,
274                pending_keys,
275                status_timeout,
276                self.status_message.clone(),
277                self.last_status,
278            );
279            if let Err(e) = self.frame.write_msg_bar(&mut self.stdout) {
280                die!("Unable to refresh screen: {e}");
281            }
282        }
283
284        if let Err(e) = self.stdout.flush() {
285            die!("Unable to refresh screen: {e}");
286        }
287
288        self.mb_last_frame = mb_this_frame;
289    }
290
291    fn set_cursor_shape(&mut self, cur_shape: CurShape) {
292        if let Err(e) = self.stdout.write_all(cur_shape.to_string().as_bytes()) {
293            // In this situation we're probably not going to be able to do all that much
294            // but we might as well try
295            die!("Unable to write to stdout: {e}");
296        };
297    }
298}
299
300#[derive(Debug, Default)]
301pub struct Frame {
302    win_lines: String,
303    status_bar: String,
304    mb_lines: String,
305    show_mb: bool,
306    msg_bar: String,
307    show_msg_bar: bool,
308    screen_rows: usize,
309    screen_cols: usize,
310    cur_x: usize,
311    cur_y: usize,
312    // Cache of the ANSI escape code strings required for each fully qualified tree-sitter
313    // highlighting tag. See render_line for details on how the cache is used.
314    style_cache: HashMap<String, String>,
315}
316
317impl Frame {
318    fn new() -> Self {
319        let win_lines_cap = 128 * 1024;
320        let bar_cap = 8 * 1024;
321
322        Self {
323            win_lines: String::with_capacity(win_lines_cap),
324            mb_lines: String::with_capacity(bar_cap),
325            status_bar: String::with_capacity(bar_cap),
326            msg_bar: String::with_capacity(bar_cap),
327            ..Default::default()
328        }
329    }
330
331    fn write(&self, w: &mut impl Write) -> io::Result<()> {
332        write!(w, "{}{}", Cursor::Hide, Cursor::ToStart)?;
333        w.write_all(self.win_lines.as_bytes())?;
334        w.write_all(self.status_bar.as_bytes())?;
335        if self.show_mb {
336            w.write_all(self.mb_lines.as_bytes())?;
337        }
338        if self.show_msg_bar {
339            w.write_all(self.msg_bar.as_bytes())?;
340        }
341
342        write!(
343            w,
344            "{}{}",
345            Cursor::To(self.cur_x + 1, self.cur_y + 1),
346            Cursor::Show
347        )
348    }
349
350    fn write_msg_bar(&self, w: &mut impl Write) -> io::Result<()> {
351        write!(w, "{}{}", Cursor::Hide, Cursor::To(1, self.screen_rows + 2))?;
352        w.write_all(self.msg_bar.as_bytes())?;
353        write!(
354            w,
355            "{}{}",
356            Cursor::To(self.cur_x + 1, self.cur_y + 1),
357            Cursor::Show
358        )
359    }
360
361    fn render_windows(
362        &mut self,
363        layout: &Layout,
364        load_exec_range: Option<(bool, Range)>,
365        screen_rows: usize,
366        tabstop: usize,
367        cs: &ColorScheme,
368    ) {
369        self.win_lines.clear();
370
371        let mut col_renderers: Vec<_> = layout
372            .cols
373            .iter()
374            .map(|(is_focus, col)| {
375                let rng = if is_focus { load_exec_range } else { None };
376                ColRenderer::new(col, layout, rng, screen_rows, tabstop, cs)
377            })
378            .collect();
379
380        let n_cols = col_renderers.len();
381        'outer: loop {
382            let mut remaining;
383            let mut prev_col = None;
384
385            for (i, cr) in col_renderers.iter_mut().enumerate() {
386                (remaining, prev_col) =
387                    cr.render_next_line(&mut self.win_lines, prev_col, &mut self.style_cache);
388                if i == n_cols - 1 && !remaining {
389                    _ = write!(&mut self.win_lines, "{}\r\n", Cursor::ClearRight);
390                    break 'outer;
391                }
392            }
393
394            _ = write!(&mut self.win_lines, "{}\r\n", Cursor::ClearRight);
395        }
396    }
397
398    fn render_status_bar(
399        &mut self,
400        cs: &ColorScheme,
401        mode_name: &str,
402        n_running: usize,
403        b: &Buffer,
404    ) {
405        self.status_bar.clear();
406
407        let lstatus = format!(
408            "{} {} - {} lines {}{}",
409            mode_name,
410            b.display_name(),
411            b.len_lines(),
412            if b.dirty { "[+]" } else { "" },
413            if !b.has_trailing_newline() {
414                "[noeol]"
415            } else {
416                ""
417            }
418        );
419        let rstatus = format!(
420            "{}{}",
421            if n_running == 0 {
422                String::new()
423            } else {
424                format!("[{n_running} running] ")
425            },
426            b.dot.addr(b)
427        );
428        let width = self
429            .screen_cols
430            .saturating_sub(UnicodeWidthStr::width(lstatus.as_str()));
431
432        _ = write!(
433            &mut self.status_bar,
434            "{}{}{lstatus}{rstatus:>width$}{}\r\n",
435            Style::Bg(cs.bar_bg),
436            Style::Fg(cs.fg),
437            Style::Reset
438        );
439    }
440
441    // current prompt and pending chars
442    fn render_message_bar(
443        &mut self,
444        cs: &ColorScheme,
445        pending_keys: &[Input],
446        status_timeout: u64,
447        mut msg: String,
448        last_status: Instant,
449    ) {
450        self.msg_bar.clear();
451        self.msg_bar.push_str(&Cursor::ClearRight.to_string());
452        msg.truncate(self.screen_cols.saturating_sub(10));
453
454        let pending = render_pending(pending_keys);
455        let delta = (Instant::now() - last_status).as_secs();
456
457        if !msg.is_empty() && delta < status_timeout {
458            let width = self
459                .screen_cols
460                .saturating_sub(msg.len())
461                .saturating_sub(10);
462            _ = write!(
463                &mut self.msg_bar,
464                "{}{}{msg}{pending:>width$}          ",
465                Style::Fg(cs.fg),
466                Style::Bg(cs.bg)
467            );
468        } else {
469            let width = self.screen_cols.saturating_sub(10);
470            _ = write!(
471                &mut self.msg_bar,
472                "{}{}{pending:>width$}          ",
473                Style::Fg(cs.fg),
474                Style::Bg(cs.bg)
475            );
476        }
477    }
478
479    fn render_minibuffer_state(
480        &mut self,
481        mb: &MiniBufferState<'_>,
482        tabstop: usize,
483        cs: &ColorScheme,
484    ) {
485        self.mb_lines.clear();
486
487        if let Some(b) = mb.b {
488            for i in mb.top..=mb.bottom {
489                let slice = b.line(i).unwrap();
490                let bg = if i == mb.selected_line_idx {
491                    cs.minibuffer_hl
492                } else {
493                    cs.bg
494                };
495
496                let mut cols = 0;
497                let mut chars = slice.chars().peekable();
498                _ = write!(
499                    &mut self.mb_lines,
500                    "{}",
501                    Styles {
502                        fg: Some(cs.fg),
503                        bg: Some(bg),
504                        ..Default::default()
505                    }
506                );
507
508                render_chars(
509                    &mut chars,
510                    None,
511                    self.screen_cols,
512                    tabstop,
513                    &mut cols,
514                    &mut self.mb_lines,
515                );
516
517                if cols < self.screen_cols {
518                    self.mb_lines.push_str(&Style::Bg(bg).to_string());
519                }
520
521                let width = self.screen_cols;
522                _ = write!(&mut self.mb_lines, "{:>width$}\r\n", Cursor::ClearRight);
523            }
524        }
525
526        _ = write!(
527            &mut self.mb_lines,
528            "{}{}{}{}{}",
529            Style::Fg(cs.fg),
530            Style::Bg(cs.bg),
531            mb.prompt,
532            mb.input,
533            Cursor::ClearRight
534        );
535    }
536
537    fn render_scratch(
538        &mut self,
539        scratch: &Scratch,
540        load_exec_range: Option<(bool, Range)>,
541        tabstop: usize,
542        cs: &ColorScheme,
543    ) {
544        self.mb_lines.clear();
545        let b = scratch.b.buffer();
546        let (w_lnum, _) = b.sign_col_dims();
547        let rng = if scratch.is_focused {
548            load_exec_range
549        } else {
550            None
551        };
552
553        let mut wr = WinRenderer {
554            y: 0,
555            w_lnum,
556            n_cols: self.screen_cols,
557            tabstop,
558            it: b.iter_tokenized_lines_from(scratch.w.view.row_off, rng),
559            gb: &b.txt,
560            w: &scratch.w,
561            cs,
562        };
563
564        while wr.render_next_line(&mut self.mb_lines, None, &mut self.style_cache) {}
565    }
566}
567
568#[derive(Clone, Copy)]
569enum PrevCol {
570    Buffer,
571    Hline,
572}
573
574struct ColRenderer<'a> {
575    inner: ziplist::Iter<'a, Window>,
576    current: Option<WinRenderer<'a>>,
577    layout: &'a Layout,
578    cs: &'a ColorScheme,
579    load_exec_range: Option<(bool, Range)>,
580    screen_rows: usize,
581    tabstop: usize,
582    n_cols: usize,
583    row: usize,
584}
585
586impl<'a> ColRenderer<'a> {
587    fn new(
588        col: &'a Column,
589        layout: &'a Layout,
590        load_exec_range: Option<(bool, Range)>,
591        screen_rows: usize,
592        tabstop: usize,
593        cs: &'a ColorScheme,
594    ) -> Self {
595        ColRenderer {
596            inner: col.wins.iter(),
597            current: None,
598            layout,
599            cs,
600            load_exec_range,
601            screen_rows,
602            tabstop,
603            n_cols: col.n_cols,
604            row: 0,
605        }
606    }
607
608    fn next_window(&mut self) -> Option<WinRenderer<'a>> {
609        let (is_focus, w) = self.inner.next()?;
610        let b = self
611            .layout
612            .buffer_with_id(w.view.bufid)
613            .expect("valid buffer id");
614
615        let (w_lnum, _) = b.sign_col_dims();
616        let rng = if is_focus { self.load_exec_range } else { None };
617        let it = b.iter_tokenized_lines_from(w.view.row_off, rng);
618
619        Some(WinRenderer {
620            y: 0,
621            w_lnum,
622            n_cols: self.n_cols,
623            tabstop: self.tabstop,
624            it,
625            gb: &b.txt,
626            w,
627            cs: self.cs,
628        })
629    }
630
631    /// Render the next available line into the provided buffer.
632    ///
633    /// Returns false if there are no more lines to render.
634    fn render_next_line(
635        &mut self,
636        buf: &mut String,
637        prev_col: Option<PrevCol>,
638        style_cache: &mut HashMap<String, String>,
639    ) -> (bool, Option<PrevCol>) {
640        if self.current.is_none() {
641            self.current = match self.next_window() {
642                Some(w) => Some(w),
643                None => return (false, None),
644            }
645        }
646
647        let lines_remaining =
648            self.current
649                .as_mut()
650                .unwrap()
651                .render_next_line(buf, prev_col, style_cache);
652        self.row += 1;
653
654        let this_col = if lines_remaining {
655            Some(PrevCol::Buffer)
656        } else {
657            self.current = None;
658            let left_edge = match prev_col {
659                Some(PrevCol::Buffer) => TR_STR,
660                Some(PrevCol::Hline) => X_STR,
661                None => "",
662            };
663
664            _ = write!(
665                buf,
666                "{}{}{left_edge}{}",
667                Style::Fg(self.cs.minibuffer_hl),
668                Style::Bg(self.cs.bg),
669                H_STR.repeat(self.n_cols)
670            );
671            Some(PrevCol::Hline)
672        };
673
674        (self.row < self.screen_rows, this_col)
675    }
676}
677
678struct WinRenderer<'a> {
679    y: usize,
680    w_lnum: usize,
681    n_cols: usize,
682    tabstop: usize,
683    it: LineIter<'a>,
684    gb: &'a GapBuffer,
685    w: &'a Window,
686    cs: &'a ColorScheme,
687}
688
689impl<'a> WinRenderer<'a> {
690    fn render_next_line(
691        &mut self,
692        buf: &mut String,
693        prev_col: Option<PrevCol>,
694        style_cache: &mut HashMap<String, String>,
695    ) -> bool {
696        if self.y >= self.w.n_rows {
697            return false;
698        }
699
700        let file_row = self.y + self.w.view.row_off;
701        self.y += 1;
702
703        if let Some(pc) = prev_col {
704            let left_edge = match pc {
705                PrevCol::Buffer => V_STR,
706                PrevCol::Hline => TL_STR,
707            };
708
709            _ = write!(
710                buf,
711                "{}{}{left_edge}",
712                Style::Fg(self.cs.minibuffer_hl),
713                Style::Bg(self.cs.bg)
714            );
715        }
716
717        match self.it.next() {
718            None => {
719                _ = write!(
720                    buf,
721                    "{}{}~ {V_STR:>width$}{}",
722                    Style::Fg(self.cs.signcol_fg),
723                    Style::Bg(self.cs.bg),
724                    Style::Fg(self.cs.fg),
725                    width = self.w_lnum
726                );
727                let padding = self.n_cols.saturating_sub(self.w_lnum).saturating_sub(2);
728                buf.push_str(&" ".repeat(padding));
729            }
730
731            Some(it) => {
732                // +2 for the leading space and vline chars
733                let padding = self.w_lnum + 2;
734
735                _ = write!(
736                    buf,
737                    "{}{} {:>width$}{V_STR}",
738                    Style::Fg(self.cs.signcol_fg),
739                    Style::Bg(self.cs.bg),
740                    file_row + 1,
741                    width = self.w_lnum
742                );
743
744                render_line(
745                    self.gb,
746                    it,
747                    self.w.view.col_off,
748                    self.n_cols.saturating_sub(padding),
749                    self.tabstop,
750                    self.cs,
751                    style_cache,
752                    buf,
753                );
754            }
755        };
756
757        true
758    }
759}
760
761fn render_pending(keys: &[Input]) -> String {
762    let mut s = String::new();
763    for k in keys {
764        match k {
765            Input::Char(c) if c.is_ascii_whitespace() => s.push_str(&format!("<{:x}>", *c as u8)),
766            Input::Char(c) => s.push(*c),
767            Input::Ctrl(c) => {
768                s.push('^');
769                s.push(*c);
770            }
771            Input::Alt(c) => {
772                s.push('^');
773                s.push('[');
774                s.push(*c);
775            }
776            Input::CtrlAlt(c) => {
777                s.push('^');
778                s.push('[');
779                s.push('^');
780                s.push(*c);
781            }
782
783            _ => (),
784        }
785    }
786
787    if s.len() > 10 {
788        s = s.split_off(s.len() - 10);
789    }
790
791    s
792}
793
794#[inline]
795fn skip_token_chars(
796    chars: &mut Peekable<Chars<'_>>,
797    tabstop: usize,
798    to_skip: &mut usize,
799) -> Option<usize> {
800    for ch in chars.by_ref() {
801        let w = if ch == '\t' {
802            tabstop
803        } else {
804            UnicodeWidthChar::width(ch).unwrap_or(1)
805        };
806
807        match (*to_skip).cmp(&w) {
808            Ordering::Less => {
809                let spaces = Some(w - *to_skip);
810                *to_skip = 0;
811                return spaces;
812            }
813
814            Ordering::Equal => {
815                *to_skip = 0;
816                break;
817            }
818
819            Ordering::Greater => *to_skip -= w,
820        }
821    }
822
823    None
824}
825
826fn render_chars(
827    chars: &mut Peekable<Chars<'_>>,
828    spaces: Option<usize>,
829    max_cols: usize,
830    tabstop: usize,
831    cols: &mut usize,
832    buf: &mut String,
833) {
834    if let Some(n) = spaces {
835        buf.extend(repeat_n(' ', n));
836        *cols = n;
837    }
838
839    for ch in chars {
840        if ch == '\n' {
841            break;
842        }
843
844        let (w, ch) = if ch == '\t' {
845            (tabstop, ch)
846        } else {
847            match UnicodeWidthChar::width(ch) {
848                Some(0) | None => (1, char::REPLACEMENT_CHARACTER),
849                Some(n) => (n, ch),
850            }
851        };
852
853        if *cols + w <= max_cols {
854            if ch == '\t' {
855                // Tab is just a control character that moves the cursor rather than
856                // replacing the previous buffer content so we need to explicitly
857                // insert spaces instead.
858                buf.extend(repeat_n(' ', tabstop));
859            } else {
860                buf.push(ch);
861            }
862            *cols += w;
863        } else {
864            break;
865        }
866    }
867
868    buf.push_str(RESET_STYLE);
869}
870
871#[allow(clippy::too_many_arguments)]
872fn render_line<'a>(
873    gb: &'a GapBuffer,
874    it: impl Iterator<Item = RangeToken<'a>>,
875    col_off: usize,
876    max_cols: usize,
877    tabstop: usize,
878    cs: &ColorScheme,
879    style_cache: &mut HashMap<String, String>,
880    buf: &mut String,
881) {
882    let mut to_skip = col_off;
883    let mut cols = 0;
884
885    for tk in it {
886        let slice = tk.as_slice(gb);
887        let mut chars = slice.chars().peekable();
888        let spaces = if to_skip > 0 {
889            let spaces = skip_token_chars(&mut chars, tabstop, &mut to_skip);
890            if to_skip > 0 || (chars.peek().is_none() && spaces.is_none()) {
891                continue;
892            }
893            spaces
894        } else {
895            None
896        };
897
898        // In the common case we have the styles for each tag already cached, so we take the hit on
899        // allocating the cache key and looking it up a second time when we insert into the cache.
900        // This allows us to avoid having to allocate the key on every lookup in order to make use
901        // of the entry API.
902        //
903        // The cache styles are also stored against the original tag rather so they can be looked
904        // up directly each time they are used, rather than need to to traverse the fallback path
905        // as done in ColorScheme::styles_for.
906        let style_str = match style_cache.get(tk.tag) {
907            Some(s) => s,
908            None => {
909                let s = cs.styles_for(tk.tag).to_string();
910                style_cache.insert(tk.tag.to_string(), s);
911                style_cache.get(tk.tag).unwrap()
912            }
913        };
914
915        buf.push_str(style_str);
916        render_chars(&mut chars, spaces, max_cols, tabstop, &mut cols, buf);
917
918        if cols == max_cols {
919            break;
920        }
921    }
922
923    if cols < max_cols {
924        buf.push_str(&Style::Bg(cs.bg).to_string());
925        buf.extend(repeat_n(' ', max_cols - cols));
926    }
927}
928
929#[derive(Debug)]
930enum RawInput {
931    Input(Input),
932    // Control sequences
933    // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode
934    // https://invisible-island.net/xterm/xterm-paste64.html
935    BPasteStart,
936}
937
938impl From<Input> for RawInput {
939    fn from(value: Input) -> Self {
940        Self::Input(value)
941    }
942}
943
944/// Spawn a thread to read from stdin and process user input to send Events to
945/// the main editor event loop.
946fn spawn_input_thread(tx: Sender<Event>) -> JoinHandle<()> {
947    spawn(move || {
948        let mut stdin = stdin().lock();
949
950        loop {
951            match try_read_input(&mut stdin) {
952                Some(RawInput::Input(i)) => {
953                    _ = tx.send(Event::Input(i));
954                    continue;
955                }
956
957                Some(RawInput::BPasteStart) => {
958                    let mut s = String::new();
959                    let mut buf = Vec::with_capacity(6);
960
961                    while let Some(c) = try_read_char(&mut stdin) {
962                        match (c, buf.as_slice()) {
963                            ('\x1b', [])
964                            | ('[', ['\x1b'])
965                            | ('2', ['\x1b', '['])
966                            | ('0', ['\x1b', '[', '2'])
967                            | ('1', ['\x1b', '[', '2', '0']) => buf.push(c),
968
969                            ('~', ['\x1b', '[', '2', '0', '1']) => {
970                                _ = tx.send(Event::BracketedPaste(s));
971                                break;
972                            }
973
974                            (c, _) => {
975                                s.extend(buf.drain(..));
976                                s.push(c);
977                            }
978                        }
979                    }
980                }
981
982                None => (),
983            }
984
985            // Always check for the window size changing after processing multiple inputs or if we
986            // failed to read anything.
987            if win_size_changed() {
988                let (rows, cols) = get_termsize();
989                _ = tx.send(Event::WinsizeChanged { rows, cols });
990            }
991        }
992    })
993}
994
995fn try_read_char(stdin: &mut impl Read) -> Option<char> {
996    let mut buf: [u8; 4] = [0; 4];
997
998    for i in 0..4 {
999        if stdin.read_exact(&mut buf[i..i + 1]).is_err() {
1000            return if i == 0 {
1001                None
1002            } else {
1003                Some(char::REPLACEMENT_CHARACTER)
1004            };
1005        }
1006
1007        match std::str::from_utf8(&buf[0..i + 1]) {
1008            Ok(s) => return s.chars().next(),
1009            Err(e) if e.error_len().is_some() => return Some(char::REPLACEMENT_CHARACTER),
1010            Err(_) => (),
1011        }
1012    }
1013
1014    // utf8 requires at most 4 bytes so at this point we have invalid data
1015    Some(char::REPLACEMENT_CHARACTER)
1016}
1017
1018fn try_read_input(stdin: &mut impl Read) -> Option<RawInput> {
1019    let c = try_read_char(stdin)?;
1020
1021    // Normal key press
1022    match Input::from_char(c) {
1023        Input::Esc => (),
1024        i => return Some(i.into()),
1025    }
1026
1027    let c2 = match try_read_char(stdin) {
1028        Some(c2) => c2,
1029        None => return Some(Input::Esc.into()),
1030    };
1031    let c3 = match try_read_char(stdin) {
1032        Some(c3) => c3,
1033        None => return Some(Input::try_from_seq2(c, c2).unwrap_or(Input::Esc).into()),
1034    };
1035
1036    if let Some(i) = Input::try_from_seq2(c2, c3) {
1037        return Some(i.into());
1038    }
1039
1040    // https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands
1041    if c2 == '[' && c3.is_ascii_digit() {
1042        let mut digits = vec![c3];
1043        loop {
1044            match try_read_char(stdin)? {
1045                c if c.is_ascii_digit() => digits.push(c),
1046                '~' => break,
1047                c => {
1048                    debug!("unknown CSIC sequence: ^[[{}{c}", String::from_iter(digits));
1049                    return None;
1050                }
1051            }
1052        }
1053
1054        // https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands
1055        return match digits.as_slice() {
1056            ['1' | '7'] => Some(Input::Home.into()),
1057            ['4' | '8'] => Some(Input::End.into()),
1058            ['3'] => Some(Input::Del.into()),
1059            ['5'] => Some(Input::PageUp.into()),
1060            ['6'] => Some(Input::PageDown.into()),
1061            ['2', '0', '0'] => Some(RawInput::BPasteStart),
1062            // 201 == bracketed paste end
1063            _ => None,
1064        };
1065    }
1066
1067    // xterm SGR (1006) mouse encoding: "^[< Cb;Cx;Cy(;) (M or m) "
1068    //   https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
1069    if c2 == '[' && c3 == '<' {
1070        let mut buf = Vec::new();
1071        let m;
1072
1073        loop {
1074            match try_read_char(stdin) {
1075                Some(c @ 'm' | c @ 'M') => {
1076                    m = c;
1077                    break;
1078                }
1079                Some(c) => buf.push(c as u8),
1080                None => return None,
1081            };
1082        }
1083        let s = String::from_utf8(buf).unwrap();
1084        let nums: Vec<usize> = s.split(';').map(|s| s.parse::<usize>().unwrap()).collect();
1085        let (b, x, y) = (nums[0], nums[1], nums[2]);
1086
1087        return MouseEvent::try_from_raw(b, x, y, m).map(|i| RawInput::Input(Input::Mouse(i)));
1088    }
1089
1090    Some(Input::Esc.into())
1091}
1092
1093#[cfg(test)]
1094mod tests {
1095    use super::*;
1096    use crate::syntax::{ByteRange, TK_DEFAULT};
1097    use simple_test_case::test_case;
1098    use std::{char::REPLACEMENT_CHARACTER, io};
1099
1100    #[test_case("a".as_bytes(), &['a']; "single ascii character")]
1101    #[test_case(&[240, 159, 146, 150], &['💖']; "single utf8 character")]
1102    #[test_case(&[165, 159, 146, 150], &[REPLACEMENT_CHARACTER; 4]; "invalid utf8 with non-ascii prefix")]
1103    #[test_case(&[65, 159, 146, 150], &['A', REPLACEMENT_CHARACTER, REPLACEMENT_CHARACTER, REPLACEMENT_CHARACTER]; "invalid utf8 with ascii prefix")]
1104    #[test]
1105    fn try_read_char_works(bytes: &[u8], expected: &[char]) {
1106        let mut r = io::Cursor::new(bytes);
1107        let mut chars = Vec::new();
1108
1109        while let Some(ch) = try_read_char(&mut r) {
1110            chars.push(ch);
1111        }
1112
1113        assert_eq!(&chars, expected);
1114    }
1115
1116    fn rt(tag: &'static str, from: usize, to: usize) -> RangeToken<'static> {
1117        RangeToken {
1118            tag,
1119            r: ByteRange { from, to },
1120        }
1121    }
1122
1123    // https://en.wikipedia.org/wiki/Bidirectional_text#Table_of_possible_BiDi_character_types
1124    // https://i18n-puzzles.com/puzzle/18/
1125    #[test]
1126    fn render_chars_correctly_handles_bidi_markers() {
1127        let line = GapBuffer::from("⁧foo⁦bar⁩baz⁩");
1128        let expected = format!("�foo�bar�baz�{RESET_STYLE}");
1129
1130        let max_cols = line.chars().count();
1131        let mut chars = line.chars().peekable();
1132        let mut buf = String::with_capacity(line.len());
1133        let mut cols = 0;
1134
1135        render_chars(&mut chars, None, max_cols, 4, &mut cols, &mut buf);
1136
1137        assert_eq!(buf, expected);
1138    }
1139
1140    // The !| characters here are the dummy style strings in the style_cache
1141    // The $# characters are replaced with RESET_STYLE and bg color respectively
1142    #[test_case(0, 14, "foo\tbar baz", "!foo$|  $!bar$| $!baz$#  "; "full line padded to max cols")]
1143    #[test_case(0, 12, "foo\tbar baz", "!foo$|  $!bar$| $!baz$"; "full line")]
1144    #[test_case(1, 11, "foo\tbar baz", "!oo$|  $!bar$| $!baz$"; "skipping first character")]
1145    #[test_case(3, 9, "foo\tbar baz", "|  $!bar$| $!baz$"; "skipping first token")]
1146    #[test_case(4, 8, "foo\tbar baz", "| $!bar$| $!baz$"; "skipping part way through a tab")]
1147    #[test_case(0, 10, "世\t界 foo", "!世$|  $!界$| $!foo$"; "unicode full line")]
1148    #[test_case(0, 12, "世\t界 foo", "!世$|  $!界$| $!foo$#  "; "unicode full line padded to max cols")]
1149    // In the case where we skip part way through a unicode character we still apply the tag
1150    // styling to the spaces we insert to pad to the correct offset rather than replacing it
1151    // with default styling instead
1152    #[test_case(1, 9, "世\t界 foo", "! $|  $!界$| $!foo$"; "unicode skipping first column of multibyte char")]
1153    #[test]
1154    fn render_line_correctly_skips_tokens(
1155        col_off: usize,
1156        max_cols: usize,
1157        s: &str,
1158        expected_template: &str,
1159    ) {
1160        let gb = GapBuffer::from(s);
1161        let range_tokens = vec![
1162            rt("a", 0, 3),
1163            rt(TK_DEFAULT, 3, 4),
1164            rt("a", 4, 7),
1165            rt(TK_DEFAULT, 7, 8),
1166            rt("a", 8, 11),
1167        ];
1168
1169        let cs = ColorScheme::default();
1170        let mut style_cache: HashMap<String, String> = [
1171            ("a".to_owned(), "!".to_owned()),
1172            (TK_DEFAULT.to_owned(), "|".to_owned()),
1173        ]
1174        .into_iter()
1175        .collect();
1176
1177        let mut s = String::new();
1178        render_line(
1179            &gb,
1180            range_tokens.into_iter(),
1181            col_off,
1182            max_cols,
1183            2,
1184            &cs,
1185            &mut style_cache,
1186            &mut s,
1187        );
1188
1189        let expected = expected_template
1190            .replace("$", RESET_STYLE)
1191            .replace("#", &Style::Bg(cs.bg).to_string());
1192
1193        assert_eq!(s, expected);
1194    }
1195
1196    // Reproduction and regression test for https://github.com/sminez/ad/issues/137
1197    #[test]
1198    fn minibuffer_lines_with_multibyte_chars_dont_panic() {
1199        let s = "  56 | Fastställa att under samtliga öppetdagar den här veckan så finns det alltid minst en";
1200        let b = Buffer::new_virtual(0, "test", s, Default::default());
1201
1202        let mb = MiniBufferState {
1203            cx: 0,
1204            n_visible_lines: 10,
1205            selected_line_idx: 0,
1206            prompt: "> ",
1207            input: Default::default(),
1208            b: Some(&b),
1209            top: 0,
1210            bottom: 0,
1211        };
1212
1213        let mut frame = Frame::new();
1214        frame.screen_cols = 91;
1215
1216        // In 137 this was panicking due to indexing into the rendered line using self.screen_cols
1217        // as a raw byte offset. The fix is simply not to truncate in that way as render_chars is
1218        // already ensuring that the buffer it is building up is staying within the available
1219        // screen space.
1220        frame.render_minibuffer_state(&mb, 4, &Default::default());
1221    }
1222}