Skip to main content

mq_edit/ui/
editor.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::Rect,
4    style::{Color, Style},
5    text::{Line, Span},
6    widgets::{Block, Borders, Paragraph, Widget},
7};
8
9use crate::document::{DocumentBuffer, LineAnalyzer, LineType, TableAlignment};
10use crate::renderer::{CodeRenderer, ImageManager, MarkdownRenderer, Renderer};
11use markdown_lsp::DiagnosticsManager;
12
13/// Table rendering context
14struct TableContext {
15    /// Column widths (calculated from all rows)
16    column_widths: Vec<usize>,
17    /// Column alignments from separator row
18    alignments: Vec<TableAlignment>,
19    /// Start line of current table
20    start_line: usize,
21    /// Line indices that belong to current table
22    table_lines: Vec<usize>,
23}
24
25/// Editor widget for rendering the document
26pub struct EditorWidget<'a> {
27    buffer: &'a DocumentBuffer,
28    scroll_offset: usize,
29    markdown_renderer: MarkdownRenderer,
30    code_renderer: Option<&'a CodeRenderer>,
31    image_manager: Option<&'a ImageManager>,
32    diagnostics: Option<&'a DiagnosticsManager>,
33    show_line_numbers: bool,
34    show_current_line_highlight: bool,
35}
36
37impl<'a> EditorWidget<'a> {
38    pub fn new(buffer: &'a DocumentBuffer) -> Self {
39        Self {
40            buffer,
41            scroll_offset: 0,
42            markdown_renderer: MarkdownRenderer::new(),
43            code_renderer: None,
44            image_manager: None,
45            diagnostics: None,
46            show_line_numbers: true,
47            show_current_line_highlight: true,
48        }
49    }
50
51    pub fn with_line_numbers(mut self, show: bool) -> Self {
52        self.show_line_numbers = show;
53        self
54    }
55
56    pub fn with_current_line_highlight(mut self, show: bool) -> Self {
57        self.show_current_line_highlight = show;
58        self
59    }
60
61    pub fn with_scroll(mut self, offset: usize) -> Self {
62        self.scroll_offset = offset;
63        self
64    }
65
66    pub fn with_code_renderer(mut self, renderer: &'a CodeRenderer) -> Self {
67        self.code_renderer = Some(renderer);
68        self
69    }
70
71    pub fn with_image_manager(mut self, image_manager: &'a ImageManager) -> Self {
72        self.image_manager = Some(image_manager);
73        self
74    }
75
76    pub fn with_diagnostics(mut self, diagnostics: &'a DiagnosticsManager) -> Self {
77        self.diagnostics = Some(diagnostics);
78        self
79    }
80
81    /// Calculate visible line range based on viewport
82    fn visible_range(&self, height: usize) -> (usize, usize) {
83        let start = self.scroll_offset;
84        let end = (start + height).min(self.buffer.line_count());
85        (start, end)
86    }
87
88    /// Add diagnostic marker to a line if there are diagnostics
89    fn add_diagnostic_marker<'b>(
90        &self,
91        mut spans: Vec<Span<'b>>,
92        line_idx: usize,
93    ) -> Vec<Span<'b>> {
94        if let Some(diagnostics) = self.diagnostics
95            && let Some(diagnostic) = diagnostics.most_severe_for_line(line_idx)
96        {
97            let marker = match diagnostic.severity {
98                Some(lsp_types::DiagnosticSeverity::ERROR) => {
99                    Span::styled(" ❌".to_string(), Style::default().fg(Color::Red))
100                }
101                Some(lsp_types::DiagnosticSeverity::WARNING) => {
102                    Span::styled(" ⚠️ ".to_string(), Style::default().fg(Color::Yellow))
103                }
104                Some(lsp_types::DiagnosticSeverity::INFORMATION) => {
105                    Span::styled(" ℹ️ ".to_string(), Style::default().fg(Color::Blue))
106                }
107                Some(lsp_types::DiagnosticSeverity::HINT) => {
108                    Span::styled(" 💡".to_string(), Style::default().fg(Color::Cyan))
109                }
110                _ => Span::styled(" ⚠️ ".to_string(), Style::default().fg(Color::Yellow)),
111            };
112            spans.push(marker);
113        }
114        spans
115    }
116
117    /// Create a line number span
118    fn make_line_number_span(
119        &self,
120        line_idx: usize,
121        width: usize,
122        is_current: bool,
123    ) -> Span<'static> {
124        let line_num = line_idx + 1; // 1-indexed display
125        let formatted = format!("{:>width$} │ ", line_num, width = width);
126        let style = if is_current {
127            Style::default().fg(Color::Yellow)
128        } else {
129            Style::default().fg(Color::DarkGray)
130        };
131        Span::styled(formatted, style)
132    }
133
134    /// Calculate the width needed for line numbers
135    fn line_number_width(&self) -> usize {
136        let total_lines = self.buffer.line_count();
137        // Calculate digits needed for the largest line number
138        if total_lines == 0 {
139            1
140        } else {
141            ((total_lines as f64).log10().floor() as usize) + 1
142        }
143        .max(3) // Minimum width of 3
144    }
145
146    /// Pre-scan lines to identify tables and calculate column widths
147    fn scan_tables(&self, start: usize, end: usize) -> Vec<TableContext> {
148        let mut tables = Vec::new();
149        let mut current_table: Option<TableContext> = None;
150
151        for line_idx in start..end {
152            let content = self.buffer.line(line_idx).unwrap_or("");
153
154            if LineAnalyzer::is_table_row(content) {
155                if current_table.is_none() {
156                    // Start new potential table
157                    let ctx = TableContext {
158                        column_widths: Vec::new(),
159                        alignments: Vec::new(),
160                        start_line: line_idx,
161                        table_lines: Vec::new(),
162                    };
163                    current_table = Some(ctx);
164                }
165
166                if let Some(ref mut ctx) = current_table {
167                    ctx.table_lines.push(line_idx);
168
169                    if LineAnalyzer::is_table_separator(content) {
170                        ctx.alignments = LineAnalyzer::parse_table_alignment(content);
171                    } else {
172                        // Update column widths
173                        let cells = LineAnalyzer::parse_table_cells(content);
174                        while ctx.column_widths.len() < cells.len() {
175                            ctx.column_widths.push(0);
176                        }
177                        for (i, cell) in cells.iter().enumerate() {
178                            ctx.column_widths[i] = ctx.column_widths[i].max(cell.chars().count());
179                        }
180                    }
181                }
182            } else {
183                // End of table
184                if let Some(ctx) = current_table.take() {
185                    // Validate: table must have header + separator (at least 2 lines)
186                    if ctx.table_lines.len() >= 2 && !ctx.alignments.is_empty() {
187                        tables.push(ctx);
188                    }
189                }
190            }
191        }
192
193        // Don't forget table at end of range
194        if let Some(ctx) = current_table
195            && ctx.table_lines.len() >= 2
196            && !ctx.alignments.is_empty()
197        {
198            tables.push(ctx);
199        }
200
201        tables
202    }
203
204    /// Get table context for a specific line
205    fn get_table_context(line_idx: usize, tables: &[TableContext]) -> Option<&TableContext> {
206        tables.iter().find(|t| t.table_lines.contains(&line_idx))
207    }
208}
209
210impl Widget for EditorWidget<'_> {
211    fn render(self, area: Rect, buf: &mut Buffer) {
212        let (start, end) = self.visible_range(area.height as usize);
213        let cursor_line = self.buffer.cursor().line;
214        let line_num_width = self.line_number_width();
215
216        // Determine which renderer to use based on file type
217        let use_code_renderer = matches!(
218            self.buffer.document_type(),
219            crate::document::DocumentType::Code { .. }
220        ) && self.code_renderer.is_some();
221
222        let mut lines = Vec::new();
223
224        if use_code_renderer {
225            // Use CodeRenderer for code files
226            let code_renderer = self.code_renderer.unwrap();
227
228            for line_idx in start..end {
229                let is_current = line_idx == cursor_line;
230
231                let mut spans = Vec::new();
232
233                // Add line number if enabled
234                if self.show_line_numbers {
235                    spans.push(self.make_line_number_span(line_idx, line_num_width, is_current));
236                }
237
238                spans.extend(code_renderer.render_line(self.buffer, line_idx, is_current));
239
240                // Add diagnostic marker
241                spans = self.add_diagnostic_marker(spans, line_idx);
242
243                if is_current && self.show_current_line_highlight {
244                    lines.push(Line::from(spans).style(Style::default().bg(Color::DarkGray)));
245                } else {
246                    lines.push(Line::from(spans));
247                }
248            }
249        } else {
250            // Use MarkdownRenderer for markdown files
251            let mut in_code_block = false;
252
253            // Pre-scan for tables
254            let tables = self.scan_tables(start, end);
255
256            for line_idx in start..end {
257                let content = self.buffer.line(line_idx).unwrap_or("");
258                let is_current = line_idx == cursor_line;
259                let trimmed = content.trim();
260
261                // Check if this line is a code fence
262                let is_code_fence = trimmed.starts_with("```");
263
264                // Create base spans with optional line number
265                let mut base_spans = Vec::new();
266                if self.show_line_numbers {
267                    base_spans.push(self.make_line_number_span(
268                        line_idx,
269                        line_num_width,
270                        is_current,
271                    ));
272                }
273
274                if is_code_fence {
275                    if !in_code_block {
276                        // Opening fence
277                        let code_block_lang = trimmed
278                            .strip_prefix("```")
279                            .map(|s| s.trim())
280                            .filter(|s| !s.is_empty())
281                            .map(|s| s.to_string());
282
283                        let content_spans = if is_current {
284                            self.markdown_renderer.render_source(content)
285                        } else {
286                            self.markdown_renderer
287                                .render_code_fence_start(code_block_lang.as_deref())
288                        };
289                        base_spans.extend(content_spans);
290
291                        // Add diagnostic marker
292                        base_spans = self.add_diagnostic_marker(base_spans, line_idx);
293
294                        lines.push(if is_current && self.show_current_line_highlight {
295                            Line::from(base_spans).style(Style::default().bg(Color::DarkGray))
296                        } else {
297                            Line::from(base_spans)
298                        });
299
300                        in_code_block = true;
301                    } else {
302                        // Closing fence
303                        let content_spans = if is_current {
304                            self.markdown_renderer.render_source(content)
305                        } else {
306                            self.markdown_renderer.render_code_fence_end()
307                        };
308                        base_spans.extend(content_spans);
309
310                        // Add diagnostic marker
311                        base_spans = self.add_diagnostic_marker(base_spans, line_idx);
312
313                        lines.push(if is_current && self.show_current_line_highlight {
314                            Line::from(base_spans).style(Style::default().bg(Color::DarkGray))
315                        } else {
316                            Line::from(base_spans)
317                        });
318
319                        in_code_block = false;
320                    }
321                } else if in_code_block && !is_current {
322                    // Inside code block (not cursor line)
323                    let content_spans = self.markdown_renderer.render_code_content(content);
324                    base_spans.extend(content_spans);
325                    // Add diagnostic marker
326                    base_spans = self.add_diagnostic_marker(base_spans, line_idx);
327                    lines.push(Line::from(base_spans));
328                } else if !in_code_block && Self::get_table_context(line_idx, &tables).is_some() {
329                    // Table line handling
330                    let table_ctx = Self::get_table_context(line_idx, &tables).unwrap();
331
332                    let content_spans = if is_current {
333                        // Current line: show raw source for editing
334                        self.markdown_renderer.render_source(content)
335                    } else if LineAnalyzer::is_table_separator(content) {
336                        // Separator row
337                        self.markdown_renderer
338                            .render_table_separator(&table_ctx.column_widths, &table_ctx.alignments)
339                    } else {
340                        // Header or data row
341                        let cells = LineAnalyzer::parse_table_cells(content);
342                        let is_header = line_idx == table_ctx.start_line;
343
344                        if is_header {
345                            self.markdown_renderer.render_table_header(
346                                &cells,
347                                &table_ctx.column_widths,
348                                &table_ctx.alignments,
349                            )
350                        } else {
351                            self.markdown_renderer.render_table_row(
352                                &cells,
353                                &table_ctx.column_widths,
354                                &table_ctx.alignments,
355                            )
356                        }
357                    };
358                    base_spans.extend(content_spans);
359
360                    // Add diagnostic marker
361                    base_spans = self.add_diagnostic_marker(base_spans, line_idx);
362
363                    if is_current && self.show_current_line_highlight {
364                        lines.push(
365                            Line::from(base_spans).style(Style::default().bg(Color::DarkGray)),
366                        );
367                    } else {
368                        lines.push(Line::from(base_spans));
369                    }
370                } else {
371                    // Regular line or cursor line
372                    let line_type = if in_code_block {
373                        LineType::InCode
374                    } else {
375                        LineAnalyzer::analyze_line(content)
376                    };
377
378                    let content_spans = if !is_current {
379                        // For non-current lines, check if it's a heading to apply full-width background
380                        if let LineType::Heading(level) = line_type {
381                            // Calculate available width for content (terminal width - line number width)
382                            let line_num_offset = if self.show_line_numbers {
383                                line_num_width + 3 // width + " │ "
384                            } else {
385                                0
386                            };
387                            let content_width =
388                                area.width.saturating_sub(line_num_offset as u16) as usize;
389                            self.markdown_renderer.render_heading_line_with_width(
390                                content,
391                                level,
392                                Some(content_width),
393                            )
394                        } else if let LineType::Image(ref alt_text, ref path) = line_type {
395                            // Try to get image dimensions and terminal support info
396                            if let Some(img_mgr) = self.image_manager.as_ref() {
397                                let dimensions = img_mgr.get_dimensions(path).ok();
398                                self.markdown_renderer
399                                    .render_image_with_info(alt_text, path, dimensions)
400                            } else {
401                                self.markdown_renderer.render_line(
402                                    self.buffer,
403                                    line_idx,
404                                    is_current,
405                                )
406                            }
407                        } else {
408                            self.markdown_renderer
409                                .render_line(self.buffer, line_idx, is_current)
410                        }
411                    } else {
412                        self.markdown_renderer
413                            .render_line(self.buffer, line_idx, is_current)
414                    };
415                    base_spans.extend(content_spans);
416
417                    // Add diagnostic marker
418                    base_spans = self.add_diagnostic_marker(base_spans, line_idx);
419
420                    if is_current && self.show_current_line_highlight {
421                        lines.push(
422                            Line::from(base_spans).style(Style::default().bg(Color::DarkGray)),
423                        );
424                    } else {
425                        lines.push(Line::from(base_spans));
426                    }
427                }
428            }
429        }
430
431        let paragraph = Paragraph::new(lines)
432            .block(Block::default().borders(Borders::NONE))
433            .style(Style::default().fg(Color::White).bg(Color::Black));
434
435        paragraph.render(area, buf);
436    }
437}