feat: impl md table rendering

This commit is contained in:
2026-06-06 23:19:15 +01:00
parent 5cd153ce58
commit 6f1e447747
13 changed files with 217 additions and 106 deletions

View File

@@ -2,7 +2,9 @@
use std::sync::{Arc, LazyLock};
use gpui::{AppContext, FontWeight, ParentElement, Styled, div, relative, rems};
use gpui::{
AppContext, FontWeight, ParentElement, Styled, div, prelude::FluentBuilder, relative, rems,
};
use crate::{
app,
@@ -108,6 +110,7 @@ enum ContentBlock {
Paragraph {
decoration: Option<String>,
content: RichTextContent,
has_padding: bool,
},
Empty,
Table {
@@ -159,6 +162,7 @@ impl MarkdownText {
cursor.goto_first_child();
let mut is_first_heading = true;
let mut last_node_kind_id: u16 = 0;
fn build_rich_text_for_node(
cursor: &mut tree_sitter::TreeCursor,
@@ -187,14 +191,10 @@ impl MarkdownText {
match node.kind_id() {
| MARKDOWN_KIND_ID_TEXT => {
println!(
"current node start byte {} parent node start byte {}",
node.start_byte(),
node_start_byte
);
if let Some(t) = node.utf8_text(content.as_ref()).ok() {
builder.push_text(t, style);
}
let start = node.start_byte() + byte_offset;
let end = node.end_byte();
let text = &content[start..end];
builder.push_text(text, style);
}
| MARKDOWN_KIND_ID_EMPHASIS => {
@@ -331,6 +331,7 @@ impl MarkdownText {
ContentBlock::Paragraph {
decoration: Some(list_marker_char.clone()),
content: builder.build(),
has_padding: false,
}
} else {
// empty block
@@ -397,7 +398,7 @@ impl MarkdownText {
| MARKDOWN_KIND_ID_ATX_H2_MARKER => ContentBlock::Heading {
font_size: rems(1.5),
font_weight: gpui::FontWeight::BOLD,
mt: rems(1.5),
mt: rems(4.),
mb: rems(1.),
content,
},
@@ -428,6 +429,12 @@ impl MarkdownText {
if is_first_heading {
is_first_heading = false;
match block {
| ContentBlock::Heading { ref mut mt, .. } => {
*mt = rems(0.);
}
| _ => {}
}
}
cursor.goto_parent();
@@ -443,6 +450,7 @@ impl MarkdownText {
self.blocks.push(ContentBlock::Paragraph {
decoration: None,
content: builder.build(),
has_padding: last_node_kind_id != MARKDOWN_KIND_ID_ATX_HEADING,
});
}
@@ -493,32 +501,111 @@ impl MarkdownText {
self.blocks.push(ContentBlock::Code { content });
}
// | MARKDOWN_KIND_ID_TABLE => {
// cursor.goto_first_child();
// debug_assert!(cursor.node().kind_id() == MARKDOWN_KIND_ID_TABLE_HEADER_ROW);
| MARKDOWN_KIND_ID_TABLE => {
cursor.goto_first_child();
debug_assert!(cursor.node().kind_id() == MARKDOWN_KIND_ID_TABLE_HEADER_ROW);
// let col_count = cursor.node().child_count();
// // markdown tables aren't usually that big
// // lets assume the average markdown table has 10 rows (inc header)
// // preallocate the vec with capacity row * col, should be big enough to avoid realloc
// let min_row_count = 10;
let col_count = cursor.node().child_count();
// markdown tables aren't usually that big
// lets assume the average markdown table has 10 rows (inc header)
// preallocate the vec with capacity row * col, should be big enough to avoid realloc
let min_row_count = 10;
// // cell text blocks are stored in row-major order
// let cell_blocks: Vec<ContentBlock> = Vec::with_capacity(col_count * min_row_count);
// cell text blocks are stored in row-major order
let mut cell_blocks: Vec<RichTextContent> =
Vec::with_capacity(col_count * min_row_count);
let mut builder = RichTextContentBuilder::new();
// cursor.goto_first_child();
// debug_assert!(cursor.node().kind_id() == MARKDOWN_KIND_ID_TABLE_CELL);
cursor.goto_first_child();
debug_assert!(cursor.node().kind_id() == MARKDOWN_KIND_ID_TABLE_CELL);
// loop {
// let cell_node = cursor.node();
// let cell_text_block = rich_text_for_node(&mut cursor, &self.content, 1, theme);
// cell_blocks.push(ContentBlock::Paragraph(cell_text_block));
// construct the header row first
loop {
build_rich_text_for_node(
&mut cursor,
&mut builder,
&self.content,
1,
theme,
Some(gpui::HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
..Default::default()
}),
);
cell_blocks.push(builder.build());
builder.clear();
if !cursor.goto_next_sibling() {
break;
}
}
cursor.goto_parent();
cursor.goto_next_sibling();
debug_assert!(cursor.node().kind_id() == MARKDOWN_KIND_ID_TABLE_DELIMITER_ROW);
let mut row_count = 1;
loop {
if !cursor.goto_next_sibling() {
break;
}
let row_node = cursor.node();
if row_node.kind_id() != MARKDOWN_KIND_ID_TABLE_DATA_ROW {
break;
}
row_count += 1;
if !cursor.goto_first_child() {
continue;
}
debug_assert!(cursor.node().kind_id() == MARKDOWN_KIND_ID_TABLE_CELL);
let mut current_col_count = 0;
loop {
build_rich_text_for_node(
&mut cursor,
&mut builder,
&self.content,
0,
theme,
None,
);
cell_blocks.push(builder.build());
current_col_count += 1;
if !cursor.goto_next_sibling() {
break;
}
builder.clear();
}
cursor.goto_parent();
// if there is fewer cells in this row than the header row
// fill in the gap
for _ in 0..(col_count - current_col_count) {
cell_blocks.push(RichTextContent::default());
}
}
debug_assert!(row_count * col_count == cell_blocks.len());
// the table consists of only the header row
self.blocks.push(ContentBlock::Table {
row_count,
col_count,
cells: cell_blocks,
});
cursor.goto_parent();
}
// if !cursor.goto_next_sibling() {
// break;
// }
// }
// }
| _ => {
println!(
"[WARN] formatting not implemenetd for node type {:?}",
@@ -531,10 +618,13 @@ impl MarkdownText {
self.blocks.push(ContentBlock::Paragraph {
decoration: None,
content: builder.build(),
has_padding: true,
});
}
}
last_node_kind_id = current_node.kind_id();
if !cursor.goto_next_sibling() {
break;
}
@@ -550,65 +640,81 @@ impl gpui::Render for MarkdownText {
) -> impl gpui::prelude::IntoElement {
let theme = app::current_theme(cx);
let children = self
.blocks
.iter()
.enumerate()
.map(|(i, block)| match block {
| ContentBlock::Heading {
font_size,
font_weight,
mt,
mb,
content,
} => div()
.min_w_0()
.mt(gpui::Length::from(*mt))
.mb(gpui::Length::from(*mb))
.text_size(gpui::AbsoluteLength::from(*font_size))
.font_weight(*font_weight)
.child(rich_text(content.clone())),
let children = self.blocks.iter().map(|block| match block {
| ContentBlock::Heading {
font_size,
font_weight,
mt,
mb,
content,
} => div()
.min_w_0()
.mt(gpui::Length::from(*mt))
.mb(gpui::Length::from(*mb))
.text_size(gpui::AbsoluteLength::from(*font_size))
.font_weight(*font_weight)
.child(rich_text(content.clone())),
| ContentBlock::Paragraph {
decoration,
content,
} => match decoration {
| None => div().min_w_0().child(rich_text(content.clone())),
| Some(decoration) => div()
| ContentBlock::Paragraph {
decoration,
content,
has_padding,
} => match decoration {
| None => div().min_w_0().child(rich_text(content.clone())),
| Some(decoration) => div()
.w_full()
.flex()
.flex_row()
.gap_2()
.items_start()
.text_color(theme.colors.text)
.child(decoration.clone())
.child(div().flex_1().min_w_0().child(rich_text(content.clone()))),
}
.when(*has_padding, |it| it.py_4()),
| ContentBlock::Code { content } => div()
.min_w_0()
.w_full()
.text_sm()
.text_color(theme.colors.text)
.line_height(relative(1.2))
.font_family("Menlo")
.px_3()
.py_2()
.rounded_sm()
.bg(theme.colors.code_bg)
.border_1()
.my_4()
.border_color(theme.colors.code_border)
.child(content.clone()),
| ContentBlock::Table {
row_count,
col_count,
cells,
} => div().flex().w_full().child(
div()
.w_full()
.flex()
.flex_row()
.gap_2()
.items_start()
.text_color(theme.colors.text)
.child(decoration.clone())
.child(div().flex_1().min_w_0().child(rich_text(content.clone()))),
},
.grid()
.grid_cols(*col_count as u16)
.grid_rows(*row_count as u16)
.h_40()
.border_l_1()
.border_t_1()
.border_color(theme.colors.border_muted)
.children(cells.iter().map(|cell_content| {
div()
.p_1()
.border_r_1()
.border_b_1()
.border_color(theme.colors.border_muted)
.child(rich_text(cell_content.clone()))
})),
),
| ContentBlock::Code { content } => div()
.min_w_0()
.w_full()
.text_sm()
.text_color(theme.colors.text)
.line_height(relative(1.2))
.font_family("Menlo")
.px_3()
.py_2()
.rounded_sm()
.bg(theme.colors.code_bg)
.border_1()
.my_4()
.border_color(theme.colors.code_border)
.child(content.clone()),
| ContentBlock::Table {
row_count,
col_count,
cells,
} => div(),
| ContentBlock::Empty => div(),
});
| ContentBlock::Empty => div(),
});
div().w_full().children(children)
}