Skip to main content

tsu_editor/editor/
render.rs

1use std::io::Write;
2
3use crossterm::{
4    QueueableCommand as _,
5    cursor::{self},
6    style,
7};
8
9use crate::{
10    color::{Color, blend_color},
11    debug,
12    editor::{Mode, Point, determine_style_for_position, render_buffer::Change},
13    theme::Style,
14    unicode::char_display_width,
15};
16
17use super::{Editor, render_buffer::RenderBuffer};
18
19impl Editor {
20    /// The main render function for updating the terminal
21    ///
22    /// # Arguments
23    /// - `buffer`: The buffer to render
24    ///
25    /// # Errors
26    /// Can return an `IoError`
27    pub fn render(&mut self, buffer: &mut RenderBuffer) -> anyhow::Result<()> {
28        self.update_gutter_width();
29        let current_buffer = buffer.clone();
30
31        let window_count = self.window_manager.windows().len();
32        crate::log!("Starting render of {window_count} window(s)");
33        for window_id in 0..window_count {
34            self.render_window(buffer, window_id)?;
35        }
36
37        self.render_window_separators(buffer);
38
39        self.render_decorations(buffer);
40
41        self.update_and_render_overlays(buffer);
42
43        let diff = buffer.diff(&current_buffer);
44        self.render_diff(diff)?;
45
46        Ok(())
47    }
48
49    fn render_window(&mut self, buffer: &mut RenderBuffer, window_id: usize) -> anyhow::Result<()> {
50        let window_data = {
51            let windows = self.window_manager.windows();
52            let window_count = windows.len();
53
54            windows
55                .get(window_id)
56                .map(|window| ((*window).clone(), window_count))
57        };
58
59        if let Some((window, window_count)) = window_data {
60            self.render_gutter_in_window(buffer, &window);
61            self.render_main_content_in_window(buffer, &window)?;
62            if window_id < window_count - 1 {
63                self.render_window_separator(buffer, &window);
64            }
65        }
66
67        Ok(())
68    }
69
70    fn render_window_separator(
71        &mut self,
72        buffer: &mut RenderBuffer,
73        window: &crate::window::Window,
74    ) {
75        let separator_style = Style {
76            foreground: Some(Color::Rgb {
77                r: 100,
78                g: 100,
79                b: 100,
80            }),
81            background: None,
82            bold: false,
83            italic: false,
84        };
85
86        let x = window.position.x + window.size.0;
87        if x < self.size.0 as usize {
88            for y in 0..window.size.1 {
89                let terminal_y = self.window_to_terminal_y(window, y);
90                buffer.set_char(x, terminal_y, '|', &separator_style, &self.theme);
91            }
92        }
93    }
94
95    #[allow(clippy::too_many_lines)]
96    fn render_window_separators(&mut self, buffer: &mut RenderBuffer) {
97        let separator_style = Style {
98            foreground: Some(Color::Rgb {
99                r: 100,
100                g: 100,
101                b: 100,
102            }),
103            background: None,
104            bold: false,
105            italic: false,
106        };
107
108        let (term_width, term_height) = (self.size.0 as usize, self.size.1 as usize);
109
110        let windows = self.window_manager.windows();
111        if windows.len() <= 1 {
112            return;
113        }
114
115        let use_ascii = self.config.window_borders_ascii;
116
117        let mut vertical_separators: Vec<(usize, usize, usize)> = Vec::new();
118        let mut horizontal_separators: Vec<(usize, usize, usize)> = Vec::new();
119
120        let mut vertical_x_positions: std::collections::HashSet<usize> =
121            std::collections::HashSet::new();
122
123        for i in 0..windows.len() {
124            for j in 0..windows.len() {
125                if i == j {
126                    continue;
127                }
128
129                let first_window = windows[i];
130                let second_window = windows[j];
131
132                if first_window.position.x + first_window.size.0 + 1 == second_window.position.x {
133                    let x = first_window.position.x + first_window.size.0;
134                    vertical_x_positions.insert(x);
135                }
136            }
137        }
138
139        // Now for each vertical separator position, find the full extent
140        for x in vertical_x_positions {
141            let mut min_y = term_height;
142            let mut max_y = 0;
143
144            // Find all windows that have this separator on their right edge
145            for window in &windows {
146                if window.position.x + window.size.0 == x {
147                    min_y = min_y.min(window.position.y);
148                    max_y = max_y.max(window.position.y + window.size.1);
149                }
150            }
151
152            if min_y < max_y {
153                vertical_separators.push((x, min_y, max_y));
154            }
155        }
156
157        let mut horizontal_y_positions: std::collections::HashSet<usize> =
158            std::collections::HashSet::new();
159
160        for i in 0..windows.len() {
161            for j in 0..windows.len() {
162                if i == j {
163                    continue;
164                }
165                let first_window = windows[i];
166                let second_window = windows[j];
167
168                if first_window.position.y + first_window.size.1 + 1 == second_window.position.y {
169                    let y = first_window.position.y + first_window.size.1;
170                    horizontal_y_positions.insert(y);
171                }
172            }
173        }
174
175        for y in horizontal_y_positions {
176            let mut min_x = term_width;
177            let mut max_x = 0;
178
179            for window in &windows {
180                if window.position.y + window.size.1 == y {
181                    min_x = min_x.min(window.position.x);
182                    max_x = max_x.max(window.position.x + window.size.0);
183                }
184            }
185
186            if min_x < max_x {
187                horizontal_separators.push((y, min_x, max_x));
188            }
189        }
190
191        let mut temp_grid: std::collections::HashMap<(usize, usize), char> =
192            std::collections::HashMap::new();
193
194        for (x, y_start, y_end) in &vertical_separators {
195            for y in *y_start..*y_end {
196                temp_grid.insert((*x, y), if use_ascii { '|' } else { '│' });
197            }
198        }
199
200        for (y, x_start, x_end) in &horizontal_separators {
201            for x in *x_start..*x_end {
202                if let Some(existing) = temp_grid.get(&(x, *y)) {
203                    if *existing == '|' || *existing == '│' {
204                        temp_grid.insert((x, *y), if use_ascii { '+' } else { '┼' });
205                    }
206                } else {
207                    temp_grid.insert((x, *y), if use_ascii { '-' } else { '─' });
208                }
209            }
210        }
211
212        let mut intersections = Vec::new();
213        for ((x, y), ch) in &temp_grid {
214            if *ch == '┼' || *ch == '+' {
215                intersections.push((*x, *y, *ch));
216            }
217        }
218
219        let has_vertical_component = |c: char| -> bool {
220            matches!(
221                c,
222                '│' | '|' | '┼' | '+' | '├' | '┤' | '┬' | '┴' | '┌' | '┐' | '└' | '┘'
223            )
224        };
225
226        let has_horizontal_component = |c: char| -> bool {
227            matches!(
228                c,
229                '─' | '-' | '┼' | '+' | '┬' | '┴' | '├' | '┤' | '┌' | '┐' | '└' | '┘'
230            )
231        };
232
233        let mut final_grid: std::collections::HashMap<(usize, usize), char> =
234            std::collections::HashMap::new();
235
236        for (x, y) in temp_grid.keys() {
237            let connects_up = if *y > 0 {
238                temp_grid
239                    .get(&(*x, y.saturating_sub(1)))
240                    .is_some_and(|&c| has_vertical_component(c))
241            } else {
242                false
243            };
244
245            let connects_down = if *y < term_height - 1 {
246                temp_grid
247                    .get(&(*x, y + 1))
248                    .is_some_and(|&c| has_vertical_component(c))
249            } else {
250                false
251            };
252
253            let connects_left = if *x > 0 {
254                temp_grid
255                    .get(&(x.saturating_sub(1), *y))
256                    .is_some_and(|&c| has_horizontal_component(c))
257            } else {
258                false
259            };
260
261            let connects_right = if *x < term_width - 1 {
262                temp_grid
263                    .get(&(x + 1, *y))
264                    .is_some_and(|&c| has_horizontal_component(c))
265            } else {
266                false
267            };
268
269            let junction_char = if use_ascii {
270                if connects_up || connects_down || connects_left || connects_right {
271                    if (connects_up || connects_down) && (connects_left || connects_right) {
272                        '+'
273                    } else if connects_up || connects_down {
274                        '|'
275                    } else {
276                        '-'
277                    }
278                } else {
279                    '+'
280                }
281            } else {
282                match (connects_up, connects_down, connects_left, connects_right) {
283                    (true, true, true, true) => '┼',
284                    (true, true, true, false) => '┤',
285                    (true, true, false, true) => '├',
286                    (true, false, true, true) => '┴',
287                    (false, true, true, true) => '┬',
288                    (true, false, false, true) => '└',
289                    (true, false, true, false) => '┘',
290                    (false, true, false, true) => '┌',
291                    (false, true, true, false) => '┐',
292                    (true | false, true, false, false) | (true, false, false, false) => '│',
293                    (false, false, true | false, true) | (false, false, true, false) => '─',
294                    (false, false, false, false) => '·',
295                }
296            };
297
298            final_grid.insert((*x, *y), junction_char);
299        }
300
301        for ((x, y), char) in final_grid {
302            buffer.set_char(x, y, char, &separator_style, &self.theme);
303        }
304    }
305
306    fn render_decorations(&mut self, buffer: &mut RenderBuffer) {
307        self.draw_status_line(buffer);
308        self.draw_command_line(buffer);
309    }
310
311    /// Renders new changes to the terminal
312    ///
313    /// # Arguments
314    /// - `change_set`: A vector of changes to render
315    ///
316    /// # Errors
317    /// Can return an `IoError`
318    ///
319    /// # Panics
320    /// This function will panic if `draw_cursor` panics
321    pub fn render_diff(&mut self, change_set: Vec<Change<'_>>) -> anyhow::Result<()> {
322        self.stdout.queue(cursor::Hide)?;
323
324        let mut sorted_changes = change_set;
325
326        sorted_changes.sort_by_key(|change| (change.y, change.x));
327
328        let mut skip_next = false;
329        for change in sorted_changes {
330            if skip_next {
331                skip_next = false;
332                continue;
333            }
334
335            let x = change.x;
336            let y = change.y;
337            let cell = change.cell;
338
339            self.stdout.queue(cursor::MoveTo(x as u16, y as u16))?;
340
341            if let Some(background) = cell.style.background {
342                let background = blend_color(
343                    background,
344                    self.theme
345                        .style
346                        .background
347                        .unwrap_or(Color::Rgb { r: 0, g: 0, b: 0 }),
348                );
349                self.stdout
350                    .queue(style::SetBackgroundColor(background.into()))?;
351            } else {
352                self.stdout.queue(style::SetBackgroundColor(
353                    self.theme.style.background.unwrap().into(),
354                ))?;
355            }
356
357            if let Some(foreground) = cell.style.foreground {
358                let foreground = blend_color(
359                    foreground,
360                    self.theme
361                        .style
362                        .background
363                        .unwrap_or(Color::Rgb { r: 0, g: 0, b: 0 }),
364                );
365                self.stdout
366                    .queue(style::SetForegroundColor(foreground.into()))?;
367            } else {
368                self.stdout.queue(style::SetForegroundColor(
369                    self.theme.style.foreground.unwrap().into(),
370                ))?;
371            }
372            if cell.style.italic {
373                self.stdout
374                    .queue(style::SetAttribute(style::Attribute::Italic))?;
375            } else {
376                self.stdout
377                    .queue(style::SetAttribute(style::Attribute::NoItalic))?;
378            }
379            self.stdout.queue(style::Print(cell.c))?;
380        }
381
382        self.stdout.queue(cursor::Show)?;
383
384        self.set_cursor_style()?;
385        self.draw_cursor()?;
386        self.stdout.flush()?;
387
388        Ok(())
389    }
390
391    fn render_gutter_in_window(
392        &mut self,
393        buffer: &mut RenderBuffer,
394        window: &crate::window::Window,
395    ) {
396        let width = self.gutter_width();
397        let gutter_style = self
398            .theme
399            .gutter_style
400            .fallback_background(&self.theme.style);
401
402        let window_buffer = &self.buffers[window.buffer_index];
403
404        for y in 0..window.inner_height() {
405            let line_number = y + 1 + window.vtop;
406            let text = if line_number <= window_buffer.len() {
407                format!("{line_number:>width$} ")
408            } else {
409                " ".repeat(width + 1)
410            };
411
412            let terminal_x = window.position.x;
413            let terminal_y = window.position.y + y;
414            buffer.set_text(terminal_x, terminal_y, &text, &gutter_style);
415        }
416    }
417
418    fn update_and_render_overlays(&mut self, _buffer: &mut RenderBuffer) {
419        let _cursor_position = Some(Point::new(
420            self.cursor_x + self.gutter_width() + 1,
421            self.cursor_y,
422        ));
423    }
424
425    fn render_main_content_in_window(
426        &mut self,
427        buffer: &mut RenderBuffer,
428        window: &crate::window::Window,
429    ) -> anyhow::Result<()> {
430        use crate::log;
431        let window_buffer = &self.buffers[window.buffer_index];
432        let viewport_content = window_buffer.view(window.vtop, window.inner_height());
433
434        if viewport_content
435            .chars()
436            .any(|c| c as u32 >= 0x1F300 && c as u32 <= 0x1F9FF)
437        {
438            for (i, c) in viewport_content.chars().enumerate().take(50) {
439                if c as u32 >= 0x1F300 && c as u32 <= 0x1F9FF {
440                    log!("  Char {i}: '{c}' (U+{t:04x})", t = c as u32);
441                }
442            }
443        }
444
445        let style_info = self.highlight(&viewport_content)?;
446        let theme_style = self.theme.style.clone();
447
448        let gutter_width = self.gutter_width();
449        let mut x = gutter_width + 1;
450        let mut y = 0;
451
452        for (position, c) in viewport_content.chars().enumerate() {
453            if c == '\n' {
454                let terminal_x = self.window_to_terminal_x(window, x);
455                let terminal_y = self.window_to_terminal_y(window, y);
456
457                if x < window.inner_width() {
458                    self.fill_line_in_window(
459                        buffer,
460                        terminal_x,
461                        terminal_y,
462                        window.inner_width() - x,
463                        &theme_style,
464                    );
465                }
466
467                x = gutter_width + 1;
468                y += 1;
469                if y >= window.inner_height() {
470                    break;
471                }
472                continue;
473            }
474
475            let char_width = char_display_width(c);
476
477            if x + char_width > window.inner_width() {
478                continue;
479            }
480
481            let style = determine_style_for_position(&style_info, position)
482                .unwrap_or(self.theme.style.clone());
483
484            let terminal_x = self.window_to_terminal_x(window, x);
485            let terminal_y = self.window_to_terminal_y(window, y);
486
487            if char_width > 1 {
488                buffer.set_char(terminal_x, terminal_y, c, &style, &self.theme);
489
490                for i in 1..char_width {
491                    if x + i < window.inner_width() {
492                        buffer.set_char(terminal_x + i, terminal_y, ' ', &style, &self.theme);
493                    }
494                }
495                x += char_width;
496            } else if char_width == 0 {
497                // NOOP
498            } else {
499                buffer.set_char(terminal_x, terminal_y, c, &style, &self.theme);
500                x += 1;
501            }
502        }
503
504        while y < window.inner_height() {
505            let terminal_x = self.window_to_terminal_x(window, gutter_width + 1);
506            let terminal_y = self.window_to_terminal_y(window, y);
507            self.fill_line_in_window(
508                buffer,
509                terminal_x,
510                terminal_y,
511                window.inner_width() - gutter_width - 1,
512                &theme_style,
513            );
514            y += 1;
515        }
516
517        Ok(())
518    }
519
520    fn fill_line_in_window(
521        &mut self,
522        buffer: &mut RenderBuffer,
523        x: usize,
524        y: usize,
525        width: usize,
526        style: &Style,
527    ) {
528        for i in 0..width {
529            buffer.set_char(x + i, y, ' ', style, &self.theme);
530        }
531    }
532
533    /// Draws the status line at the bottom of the editor view
534    ///
535    /// # Arguments
536    /// - `buffer`: The buffer containing the status line
537    ///
538    /// # Panics
539    /// This function may panic if the lengths of the mode and position strings exceed `u16::MAX`,
540    /// which is currently impossible for the mode string but is not necessarily impossible for the
541    /// position string
542    pub fn draw_status_line(&mut self, buffer: &mut RenderBuffer) {
543        let mode = format!(" {:?} ", self.mode).to_uppercase();
544        debug!("Mode: {mode}");
545
546        let active_window = self.window_manager.active_window();
547        let (file, position, window_indicator) = if let Some(window) = active_window {
548            let window_buffer = &self.buffers[window.buffer_index];
549            let dirty = if window_buffer.is_dirty() {
550                " [+] "
551            } else {
552                ""
553            };
554            let file = format!(" {}{}", window_buffer.name(), dirty);
555            let position = format!(
556                " {}:{} ",
557                window.vtop + window.cursor_y + 1,
558                window.cursor_x + 1
559            );
560
561            let window_count = self.window_manager.windows().len();
562            let window_indicator = if window_count > 1 {
563                format!(
564                    " [{}/{}]",
565                    self.window_manager.active_window_id() + 1,
566                    window_count
567                )
568            } else {
569                String::new()
570            };
571
572            (file, position, window_indicator)
573        } else {
574            let dirty = if self.current_buffer().is_dirty() {
575                " [+] "
576            } else {
577                ""
578            };
579            let file = format!(" {}{}", self.current_buffer().name(), dirty);
580            let position = format!(" {}:{} ", self.vtop + self.cursor_y + 1, self.cursor_x + 1);
581            (file, position, String::new())
582        };
583
584        let file_width = self.size.0
585            - mode.len() as u16
586            - position.len() as u16
587            - window_indicator.len() as u16
588            - 2;
589        let y = self.size.1 as usize - 2;
590
591        let transition_style = Style {
592            foreground: self.theme.status_line_style.outer_style.background,
593            background: self.theme.status_line_style.inner_style.background,
594            ..Default::default()
595        };
596
597        buffer.set_text(0, y, &mode, &self.theme.status_line_style.outer_style);
598
599        buffer.set_text(
600            mode.len(),
601            y,
602            &self.theme.status_line_style.outer_chars[1].to_string(),
603            &transition_style,
604        );
605
606        buffer.set_text(
607            mode.len() + 1,
608            y,
609            &format!("{:<width$}", file, width = file_width as usize),
610            &self.theme.status_line_style.inner_style,
611        );
612
613        buffer.set_text(
614            mode.len() + 1 + file_width as usize,
615            y,
616            &self.theme.status_line_style.outer_chars[2].to_string(),
617            &transition_style,
618        );
619
620        buffer.set_text(
621            mode.len() + 2 + file_width as usize,
622            y,
623            &format!("{position}{window_indicator}"),
624            &self.theme.status_line_style.outer_style,
625        );
626    }
627
628    fn draw_command_line(&mut self, buffer: &mut RenderBuffer) {
629        let style = &self.theme.style;
630        let y = self.size.1 as usize - 1;
631
632        if !self.is_command() {
633            let wc = if let Some(ref waiting_command) = self.waiting_command {
634                waiting_command.clone()
635            } else {
636                " ".repeat(10)
637            };
638
639            if let Some(ref last_error) = self.last_error {
640                let error = format!("{:width$}", last_error, width = self.size.0 as usize);
641                buffer.set_text(0, self.size.1 as usize - 1, &error, style);
642            } else {
643                let clear_line = " ".repeat(self.size.0 as usize - 10);
644                buffer.set_text(0, y, &clear_line, style);
645            }
646
647            buffer.set_text(self.size.0 as usize - 10, y, &wc, style);
648
649            return;
650        }
651
652        let cmd_line = format!(
653            ":{:width$}",
654            self.command,
655            width = self.size.0 as usize - self.command.len() - 1
656        );
657
658        buffer.set_text(0, self.size.1 as usize - 1, &cmd_line, style);
659    }
660
661    /// Draws the cursor based on current style and position
662    ///
663    /// # Errors
664    /// Can return an error if `set_cursor_style` fails or if the stdout queue fails to move the
665    /// cursor
666    ///
667    /// # Panics
668    /// This function might panic if the numeric values of the cursor's target (x, y) position
669    /// exceed `u16::MAX`
670    pub fn draw_cursor(&mut self) -> anyhow::Result<()> {
671        self.fix_cursor_position();
672        self.set_cursor_style()?;
673        self.check_bounds();
674
675        let cursor_position = if let Some(window) = self.window_manager.active_window() {
676            let window_cursor_x = window.cursor_x;
677            let window_cursor_y = window.cursor_y;
678
679            let display_column = if let Some(line) = self.view_line(window_cursor_y) {
680                let line = line.trim_end_matches('\n');
681                crate::unicode::char_to_column(line, window_cursor_x)
682            } else {
683                window_cursor_x
684            };
685
686            let terminal_x = window.position.x + self.gutter_width() + 1 + display_column;
687            let terminal_y =
688                window.position.y + window_cursor_y.min(window.inner_height().saturating_sub(1));
689            Some((terminal_x, terminal_y))
690        } else {
691            let display_column = if let Some(line) = self.view_line(self.cursor_y) {
692                let line = line.trim_end_matches('\n');
693                crate::unicode::char_to_column(line, self.cursor_x)
694            } else {
695                self.cursor_x
696            };
697            Some(((self.vx + display_column), self.cursor_y))
698        };
699
700        if let Some((x, y)) = cursor_position {
701            self.stdout.queue(cursor::MoveTo(x as u16, y as u16))?;
702        } else {
703            self.stdout.queue(cursor::Hide)?;
704        }
705
706        Ok(())
707    }
708
709    fn update_gutter_width(&mut self) {
710        self.vx = self.gutter_width() + 1;
711    }
712
713    fn set_cursor_style(&mut self) -> anyhow::Result<()> {
714        self.stdout.queue(match self.waiting_key_action {
715            Some(_) => cursor::SetCursorStyle::SteadyUnderScore,
716            _ => match self.mode {
717                Mode::Insert => cursor::SetCursorStyle::SteadyBar,
718                _ => cursor::SetCursorStyle::DefaultUserShape,
719            },
720        })?;
721
722        Ok(())
723    }
724}