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
13struct TableContext {
15 column_widths: Vec<usize>,
17 alignments: Vec<TableAlignment>,
19 start_line: usize,
21 table_lines: Vec<usize>,
23}
24
25pub 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 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 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 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; 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 fn line_number_width(&self) -> usize {
136 let total_lines = self.buffer.line_count();
137 if total_lines == 0 {
139 1
140 } else {
141 ((total_lines as f64).log10().floor() as usize) + 1
142 }
143 .max(3) }
145
146 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 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 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 if let Some(ctx) = current_table.take() {
185 if ctx.table_lines.len() >= 2 && !ctx.alignments.is_empty() {
187 tables.push(ctx);
188 }
189 }
190 }
191 }
192
193 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 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 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 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 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 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 let mut in_code_block = false;
252
253 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 let is_code_fence = trimmed.starts_with("```");
263
264 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 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 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 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 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 let content_spans = self.markdown_renderer.render_code_content(content);
324 base_spans.extend(content_spans);
325 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 let table_ctx = Self::get_table_context(line_idx, &tables).unwrap();
331
332 let content_spans = if is_current {
333 self.markdown_renderer.render_source(content)
335 } else if LineAnalyzer::is_table_separator(content) {
336 self.markdown_renderer
338 .render_table_separator(&table_ctx.column_widths, &table_ctx.alignments)
339 } else {
340 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 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 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 if let LineType::Heading(level) = line_type {
381 let line_num_offset = if self.show_line_numbers {
383 line_num_width + 3 } 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 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 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}