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 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(¤t_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 for x in vertical_x_positions {
141 let mut min_y = term_height;
142 let mut max_y = 0;
143
144 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 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 } 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 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 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}