Skip to main content

ratatui_code_editor/
render.rs

1use ratatui::{prelude::*, widgets::Widget};
2use crate::editor::Editor;
3use crate::code::{
4    RopeGraphemes, grapheme_width_and_chars_len, grapheme_width_and_bytes_len
5};
6
7/// Draws the main editor view in the provided area using the ratatui rendering buffer.
8///
9/// Renders the main editor view in four distinct layers:
10/// 1. Line numbers and text content are drawn in the visible viewport.
11/// 2. Syntax highlighting is overlaid on top of the text.
12/// 3. The selection highlight is rendered above the syntax layer.
13/// 4. User marks are rendered as the final uppermost overlay.
14///
15/// # Arguments
16///
17/// * `self` - The `Editor` instance (as reference) to render.
18/// * `area` - The rectangular area on the terminal to draw within.
19/// * `buf` - The ratatui `Buffer` that represents the screen cells to draw to.
20/// 
21impl Widget for &Editor {
22    fn render(self, area: Rect, buf: &mut Buffer) {
23        let code = self.code_ref();
24        let total_lines = code.len_lines();
25        let total_chars = code.len_chars();
26        let max_line_number = total_lines.max(1);
27        let line_number_digits = max_line_number.to_string().len().max(5);
28        let line_number_width = line_number_digits + 2;
29
30        let mut draw_y = area.top();
31        
32        let line_number_style = Style::default().fg(Color::DarkGray);
33        let default_text_style = Style::default().fg(Color::White);
34
35        // draw line numbers and text
36        for line_idx in self.offset_y..total_lines {
37            if draw_y >= area.bottom() { break }
38        
39            let line_number = format!("{:^width$}", line_idx + 1, width = line_number_digits);
40            buf.set_string(area.left(), draw_y, &line_number, line_number_style);
41        
42            let line_len = code.line_len(line_idx);
43            let max_x = (area.width as usize).saturating_sub(line_number_width);
44        
45            let start_col = self.offset_x.min(line_len);
46            let end_col = (start_col + max_x).min(line_len);
47        
48            let line_start_char = code.line_to_char(line_idx);
49            let char_start = line_start_char + start_col;
50            let char_end = line_start_char + end_col;
51        
52            let visible_chars = code.char_slice(char_start, char_end);
53
54            let displayed_line = visible_chars.to_string().replace("\t", &" ");
55        
56            let text_x = area.left() + line_number_width as u16;
57            if text_x < area.left() + area.width && draw_y < area.top() + area.height {
58                buf.set_string(text_x, draw_y, &displayed_line, default_text_style);
59            }
60        
61            draw_y += 1;
62        }
63
64        // draw syntax highlighting
65        if code.is_highlight() {
66            
67            // Render syntax highlighting for the visible portion of the text buffer.
68            // For each visible line within the viewport, limit the highlighting to the
69            // visible columns to avoid expensive processing of long lines outside the view.
70            // This improves performance by only querying Tree-sitter for the visible slice,
71            // then applying styles per character based on byte ranges returned by the syntax query.
72
73            for screen_y in 0..(area.height as usize) {
74                let line_idx = self.offset_y + screen_y;
75                if line_idx >= total_lines { break }
76            
77                let line_len = code.line_len(line_idx);
78                let max_x = (area.width as usize).saturating_sub(line_number_width);
79            
80                let line_start_char = code.line_to_char(line_idx);
81                let start_char = line_start_char + self.offset_x;
82                let visible_len = line_len.saturating_sub(self.offset_x);
83                let end = max_x.min(visible_len);
84                let end_char = start_char + end;
85
86                if start_char > total_chars || end_char > total_chars {
87                    continue; // last line offset case 
88                }
89
90                let chars = code.char_slice(start_char, end_char);
91
92                let start_byte = code.char_to_byte(start_char);
93                let end_byte = code.char_to_byte(end_char);
94            
95                let highlights = self.highlight_interval(
96                    start_byte, end_byte, &self.theme
97                );
98            
99                let mut x = 0;
100                let mut byte_idx_in_rope = start_byte;
101            
102                for g in RopeGraphemes::new(&chars) {
103                    let (g_width, g_bytes) = grapheme_width_and_bytes_len(g);
104                
105                    if x >= max_x { break; }
106                
107                    let start_x = area.left() + line_number_width as u16 + x as u16;
108                    let draw_y = area.top() + screen_y as u16;
109                
110                    for dx in 0..g_width {
111                        if x + dx >= max_x { break; }
112                        let draw_x = start_x + dx as u16;
113                        for &(start, end, s) in &highlights {
114                            if start <= byte_idx_in_rope && byte_idx_in_rope < end {
115                                buf[(draw_x, draw_y)].set_style(s);
116                                break;
117                            }
118                        }
119                    }
120                
121                    x = x.saturating_add(g_width);
122                    byte_idx_in_rope += g_bytes;
123                }
124            }
125        }
126
127        // draw selection
128        if let Some(selection) = self.selection && !selection.is_empty() {
129            let start = selection.start.min(selection.end);
130            let end = selection.start.max(selection.end);
131        
132            let start_line = code.char_to_line(start);
133            let end_line = code.char_to_line(end);
134        
135            for line_idx in start_line..=end_line {
136                if line_idx < self.offset_y { continue }
137                if line_idx >= self.offset_y + area.height as usize { break }
138        
139                let line_start_char = code.line_to_char(line_idx);
140                let line_len = code.line_len(line_idx);
141                let line_end_char = line_start_char + line_len;
142        
143                let sel_start = start.max(line_start_char);
144                let sel_end = end.min(line_end_char);
145        
146                let rel_start = sel_start - line_start_char;
147                let rel_end = sel_end - line_start_char;
148        
149                let start_col = self.offset_x.min(line_len);
150                let max_text_width = (area.width as usize).saturating_sub(line_number_width);
151                let end_col = (start_col + max_text_width).min(line_len);
152        
153                let char_slice_start = line_start_char + start_col;
154                let char_slice_end = line_start_char + end_col;
155        
156                let visible_chars = code.char_slice(char_slice_start, char_slice_end);
157
158                let draw_y = area.top() + (line_idx - self.offset_y) as u16;
159                let mut visual_x: u16 = 0;
160                let mut char_col = start_col;
161
162                for g in RopeGraphemes::new(&visible_chars) {
163                    let (g_width, g_chars) = grapheme_width_and_chars_len(g);
164                
165                    if char_col < rel_end && char_col + g_chars > rel_start {
166                        let start_x = area.left() + line_number_width as u16 + visual_x;
167                        for dx in 0..g_width as u16 {
168                            let draw_x = start_x + dx;
169                            if draw_x < area.right() && draw_y < area.bottom() {
170                                buf[(draw_x, draw_y)].set_style(Style::default().bg(Color::DarkGray));
171                            }
172                        }
173                    }
174                
175                    visual_x = visual_x.saturating_add(g_width as u16);
176                    char_col += g_chars;
177                }
178            }
179        }
180
181        // draw marks
182        if let Some(ref marks) = self.marks {
183            for &(start, end, color) in marks {
184                if start >= end || end > total_chars { continue }
185
186                let start_line = code.char_to_line(start);
187                let end_line = code.char_to_line(end);
188
189                for line_idx in start_line..=end_line {
190                    if line_idx < self.offset_y || line_idx >= self.offset_y + area.height as usize {
191                        continue;
192                    }
193
194                    let line_start_char = code.line_to_char(line_idx);
195                    let line_len = code.line_len(line_idx);
196                    let line_end_char = line_start_char + line_len;
197
198                    let highlight_start = start.max(line_start_char);
199                    let highlight_end = end.min(line_end_char);
200
201                    let rel_start = highlight_start - line_start_char;
202                    let rel_end = highlight_end - line_start_char;
203
204                    let start_col = self.offset_x.min(line_len);
205                    let max_text_width = (area.width as usize).saturating_sub(line_number_width);
206                    let end_col = (start_col + max_text_width).min(line_len);
207
208                    let char_slice_start = line_start_char + start_col;
209                    let char_slice_end = line_start_char + end_col;
210
211                    let visible_chars = code.char_slice(char_slice_start, char_slice_end);
212
213                    let draw_y = area.top() + (line_idx - self.offset_y) as u16;
214                    let mut visual_x: u16 = 0;
215                    let mut char_col = start_col;
216
217                    for g in RopeGraphemes::new(&visible_chars) {
218                        let (g_width, g_chars) = grapheme_width_and_chars_len(g);
219                    
220                        if char_col < rel_end && char_col + g_chars > rel_start {
221                            let start_x = area.left() + line_number_width as u16 + visual_x;
222                            for dx in 0..g_width as u16 {
223                                let draw_x = start_x + dx;
224                                if draw_x < area.right() && draw_y < area.bottom() {
225                                    buf[(draw_x, draw_y)].set_bg(color);
226                                }
227                            }
228                        }
229                    
230                        visual_x = visual_x.saturating_add(g_width as u16);
231                        char_col += g_chars;
232                    }
233                }
234            }
235        }
236    }
237}