feat: impl md table rendering
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ pub(crate) struct RichTextContentBuilder {
|
||||
annotations: Vec<Annotation>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct RichTextContent {
|
||||
elements: Rc<[RichTextElement]>,
|
||||
links: Rc<[gpui::SharedString]>,
|
||||
@@ -88,7 +88,7 @@ impl RichTextContentBuilder {
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn build(self) -> RichTextContent {
|
||||
pub(crate) fn build(&self) -> RichTextContent {
|
||||
let mut text_start = 0;
|
||||
let mut text_end = 0;
|
||||
|
||||
@@ -98,10 +98,10 @@ impl RichTextContentBuilder {
|
||||
let mut elements: Vec<RichTextElement> = Vec::new();
|
||||
let mut link_i_offset = 0;
|
||||
|
||||
for annotation in self.annotations {
|
||||
for annotation in &self.annotations {
|
||||
match annotation {
|
||||
| Annotation::Text { style, range } => {
|
||||
highlights.push(((range.start - text_start)..(range.end - text_start), style));
|
||||
highlights.push(((range.start - text_start)..(range.end - text_start), *style));
|
||||
text_end = range.end;
|
||||
}
|
||||
| Annotation::Link { src, range } => {
|
||||
@@ -115,7 +115,7 @@ impl RichTextContentBuilder {
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
links.push(src);
|
||||
links.push(src.clone());
|
||||
link_ranges.push((range.start - text_start)..(range.end - text_start));
|
||||
|
||||
text_end = range.end;
|
||||
@@ -151,6 +151,11 @@ impl RichTextContentBuilder {
|
||||
links: Rc::from(links),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.annotations.clear();
|
||||
self.raw_content.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::RenderOnce for RichText {
|
||||
|
||||
Reference in New Issue
Block a user