refactor: markdown rendering into rich_text component
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
// markdown treesitter playground: https://ikatyang.github.io/tree-sitter-markdown/
|
||||
|
||||
use std::{
|
||||
ops::Range,
|
||||
sync::{Arc, LazyLock},
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use gpui::{AppContext, FontWeight, ParentElement, Styled, div, relative, rems};
|
||||
|
||||
use crate::{
|
||||
app,
|
||||
component::rich_text::{RichText, RichTextContent, RichTextContentBuilder, rich_text},
|
||||
theme,
|
||||
};
|
||||
|
||||
use gpui::{AppContext, ParentElement, Refineable, Styled, div, px, relative, rems};
|
||||
|
||||
use crate::{app, theme};
|
||||
|
||||
const MARKDOWN_KIND_ID_SETEXT_H1_UNDERLINE: u16 = 8;
|
||||
const MARKDOWN_KIND_ID_SETEXT_H2_UNDERLINE: u16 = 9;
|
||||
const MARKDOWN_KIND_ID_ATX_H1_MARKER: u16 = 11;
|
||||
@@ -94,12 +95,25 @@ pub(crate) struct MarkdownText {
|
||||
}
|
||||
|
||||
enum ContentBlock {
|
||||
Text {
|
||||
decoration: Option<gpui::SharedString>,
|
||||
text: gpui::SharedString,
|
||||
highlights: Vec<(Range<usize>, gpui::HighlightStyle)>,
|
||||
links: Vec<(Range<usize>, gpui::SharedString)>,
|
||||
style: gpui::StyleRefinement,
|
||||
Heading {
|
||||
font_size: gpui::Rems,
|
||||
font_weight: gpui::FontWeight,
|
||||
mt: gpui::Rems,
|
||||
mb: gpui::Rems,
|
||||
content: RichTextContent,
|
||||
},
|
||||
Code {
|
||||
content: gpui::SharedString,
|
||||
},
|
||||
Paragraph {
|
||||
decoration: Option<String>,
|
||||
content: RichTextContent,
|
||||
},
|
||||
Empty,
|
||||
Table {
|
||||
row_count: usize,
|
||||
col_count: usize,
|
||||
cells: Vec<RichTextContent>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -112,14 +126,6 @@ pub(crate) fn new(content: Arc<str>, cx: &mut gpui::Context<MarkdownText>) -> Ma
|
||||
view
|
||||
}
|
||||
|
||||
impl Styled for ContentBlock {
|
||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||
match self {
|
||||
| ContentBlock::Text { style, .. } => style,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MarkdownText {
|
||||
fn on_create(&mut self, cx: &gpui::Context<Self>) {
|
||||
let content = Arc::clone(&self.content);
|
||||
@@ -154,17 +160,18 @@ impl MarkdownText {
|
||||
|
||||
let mut is_first_heading = true;
|
||||
|
||||
fn block_for_node(
|
||||
fn build_rich_text_for_node(
|
||||
cursor: &mut tree_sitter::TreeCursor,
|
||||
builder: &mut RichTextContentBuilder,
|
||||
content: &str,
|
||||
// byte_offset is the number of bytes to offset the content start byte by
|
||||
byte_offset: usize,
|
||||
theme: &theme::Theme,
|
||||
) -> ContentBlock {
|
||||
parent_style: Option<gpui::HighlightStyle>,
|
||||
) {
|
||||
let node_start_byte = cursor.node().start_byte();
|
||||
|
||||
let mut highlights: Vec<(Range<usize>, gpui::HighlightStyle)> = Vec::new();
|
||||
let mut links: Vec<(Range<usize>, gpui::SharedString)> = Vec::new();
|
||||
let style = parent_style.unwrap_or_default();
|
||||
|
||||
cursor.goto_first_child();
|
||||
|
||||
@@ -179,56 +186,76 @@ impl MarkdownText {
|
||||
}
|
||||
|
||||
match node.kind_id() {
|
||||
| MARKDOWN_KIND_ID_EMPHASIS => {
|
||||
highlights.push((
|
||||
node_range!(),
|
||||
gpui::HighlightStyle {
|
||||
font_style: Some(gpui::FontStyle::Italic),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
| 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);
|
||||
}
|
||||
| MARKDOWN_KIND_ID_STRONG_EMPHASIS => highlights.push((
|
||||
node_range!(),
|
||||
gpui::HighlightStyle {
|
||||
}
|
||||
|
||||
| MARKDOWN_KIND_ID_EMPHASIS => {
|
||||
build_rich_text_for_node(
|
||||
cursor,
|
||||
builder,
|
||||
content,
|
||||
byte_offset,
|
||||
theme,
|
||||
Some(gpui::HighlightStyle {
|
||||
font_style: Some(gpui::FontStyle::Italic),
|
||||
..style
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
| MARKDOWN_KIND_ID_STRONG_EMPHASIS => {
|
||||
build_rich_text_for_node(
|
||||
cursor,
|
||||
builder,
|
||||
content,
|
||||
byte_offset,
|
||||
theme,
|
||||
Some(gpui::HighlightStyle {
|
||||
font_weight: Some(gpui::FontWeight::BOLD),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
..style
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
| MARKDOWN_KIND_ID_LINK => {
|
||||
if cursor.goto_first_child() {
|
||||
highlights.push((
|
||||
node_range!(),
|
||||
gpui::HighlightStyle {
|
||||
color: Some(theme.colors.link.into()),
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
color: Some(theme.colors.link.into()),
|
||||
thickness: px(1.),
|
||||
wavy: false,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
| MARKDOWN_KIND_ID_LINK => {
|
||||
cursor.goto_first_child();
|
||||
|
||||
if cursor.goto_next_sibling()
|
||||
&& let Ok(src) = cursor.node().utf8_text(content.as_bytes())
|
||||
{
|
||||
links.push((
|
||||
node_range!(),
|
||||
gpui::SharedString::from(String::from(src)),
|
||||
));
|
||||
let (description, src) =
|
||||
if cursor.node().kind_id() == MARKDOWN_KIND_ID_LINK_DESTINATION {
|
||||
let node = cursor.node();
|
||||
let src = &content[node_range!()];
|
||||
(src, src)
|
||||
} else {
|
||||
let node = cursor.node();
|
||||
let description = &content[node_range!()];
|
||||
if cursor.goto_next_sibling() {
|
||||
debug_assert!(
|
||||
cursor.node().kind_id() == MARKDOWN_KIND_ID_LINK_DESTINATION
|
||||
);
|
||||
let node = cursor.node();
|
||||
(description, &content[node_range!()])
|
||||
} else {
|
||||
// the link src is invalid, use an empty string as a fallback
|
||||
// link on click handler will ignore empty string
|
||||
links.push((node_range!(), "".into()))
|
||||
// no src for this link node
|
||||
(description, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
| _ => {
|
||||
// extend here to support more markdown node stylings
|
||||
}
|
||||
builder.push_link(description, src.to_owned());
|
||||
|
||||
cursor.goto_parent();
|
||||
}
|
||||
|
||||
| _ => {
|
||||
// extend here to support more styles
|
||||
}
|
||||
};
|
||||
|
||||
if !cursor.goto_next_sibling() {
|
||||
@@ -237,35 +264,11 @@ impl MarkdownText {
|
||||
}
|
||||
|
||||
cursor.goto_parent();
|
||||
|
||||
ContentBlock::Text {
|
||||
decoration: None,
|
||||
text: gpui::SharedString::new(
|
||||
&content[(node_start_byte + byte_offset)..cursor.node().end_byte()],
|
||||
),
|
||||
highlights: highlights,
|
||||
links: links,
|
||||
style: gpui::StyleRefinement::default(),
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
let current_node = cursor.node();
|
||||
|
||||
fn render_fallback_content(
|
||||
cursor: &tree_sitter::TreeCursor,
|
||||
content: &str,
|
||||
blocks: &mut Vec<ContentBlock>,
|
||||
) {
|
||||
blocks.push(ContentBlock::Text {
|
||||
decoration: None,
|
||||
text: gpui::SharedString::new(&content[cursor.node().byte_range()]),
|
||||
highlights: Vec::new(),
|
||||
links: Vec::new(),
|
||||
style: gpui::StyleRefinement::default(),
|
||||
});
|
||||
}
|
||||
|
||||
fn render_list_node(
|
||||
cursor: &mut tree_sitter::TreeCursor,
|
||||
content: &str,
|
||||
@@ -281,10 +284,7 @@ impl MarkdownText {
|
||||
// tight_list <-- recursive point
|
||||
|
||||
// go to list_item node
|
||||
if !cursor.goto_first_child() {
|
||||
render_fallback_content(&cursor, content, blocks);
|
||||
return false;
|
||||
}
|
||||
cursor.goto_first_child();
|
||||
|
||||
let mut list_index: Option<usize> = None;
|
||||
|
||||
@@ -305,51 +305,37 @@ impl MarkdownText {
|
||||
let marker_content = &content[marker_node.byte_range()];
|
||||
|
||||
let list_marker_char = match marker_content {
|
||||
// unordered list item
|
||||
| "-" | "+" | "*" => Some("•".to_string()),
|
||||
// unordered list item
|
||||
| "-" | "+" | "*" => "•".to_string(),
|
||||
|
||||
| marker_content if ORDERED_LIST_MARKER_REGEX.is_match(marker_content) => {
|
||||
let i = list_index.get_or_insert_with(|| {
|
||||
marker_content
|
||||
.strip_suffix('.')
|
||||
.unwrap()
|
||||
.parse::<usize>()
|
||||
.unwrap()
|
||||
});
|
||||
let j = *i;
|
||||
*i = j + 1;
|
||||
Some(format!("{j}."))
|
||||
}
|
||||
| marker_content if ORDERED_LIST_MARKER_REGEX.is_match(marker_content) => {
|
||||
let i = list_index.get_or_insert_with(|| {
|
||||
marker_content
|
||||
.strip_suffix('.')
|
||||
.unwrap()
|
||||
.parse::<usize>()
|
||||
.unwrap()
|
||||
});
|
||||
let j = *i;
|
||||
*i = j + 1;
|
||||
format!("{j}.")
|
||||
}
|
||||
|
||||
| _ => None,
|
||||
};
|
||||
|
||||
let Some(list_marker_char) = list_marker_char else {
|
||||
render_fallback_content(&cursor, content, blocks);
|
||||
return false;
|
||||
| _ => "•".to_string(),
|
||||
};
|
||||
|
||||
// go to paragraph sibling node
|
||||
let block = if cursor.goto_next_sibling() {
|
||||
let mut b = block_for_node(cursor, content, 0, theme);
|
||||
match b {
|
||||
| ContentBlock::Text {
|
||||
ref mut decoration, ..
|
||||
} => *decoration = Some(list_marker_char.into()),
|
||||
let mut builder = RichTextContentBuilder::new();
|
||||
build_rich_text_for_node(cursor, &mut builder, content, 0, theme, None);
|
||||
ContentBlock::Paragraph {
|
||||
decoration: Some(list_marker_char.clone()),
|
||||
content: builder.build(),
|
||||
}
|
||||
b
|
||||
} else {
|
||||
ContentBlock::Text {
|
||||
decoration: Some(list_marker_char.into()),
|
||||
text: gpui::SharedString::default(),
|
||||
highlights: Vec::new(),
|
||||
links: Vec::new(),
|
||||
style: gpui::StyleRefinement::default(),
|
||||
}
|
||||
}
|
||||
.text_sm()
|
||||
.text_color(theme.colors.text)
|
||||
.p(rems(indentation as f32));
|
||||
// empty block
|
||||
ContentBlock::Empty
|
||||
};
|
||||
|
||||
blocks.push(block);
|
||||
|
||||
@@ -374,150 +360,179 @@ impl MarkdownText {
|
||||
}
|
||||
|
||||
match current_node.kind_id() {
|
||||
| MARKDOWN_KIND_ID_ATX_HEADING => {
|
||||
if !cursor.goto_first_child() {
|
||||
render_fallback_content(&cursor, &self.content, &mut self.blocks);
|
||||
continue;
|
||||
}
|
||||
| MARKDOWN_KIND_ID_ATX_HEADING => {
|
||||
cursor.goto_first_child();
|
||||
|
||||
let marker_node_kind = cursor.node().kind_id();
|
||||
let marker_node_kind = cursor.node().kind_id();
|
||||
|
||||
let block = if cursor.goto_next_sibling()
|
||||
&& cursor.node().kind_id() == MARKDOWN_KIND_ID_HEADING_CONTENT
|
||||
{
|
||||
// because HEADING_CONTENT node includes the space after the heading marker
|
||||
// offset by 1 to exclude the space
|
||||
block_for_node(&mut cursor, &self.content, 1, theme)
|
||||
} else {
|
||||
ContentBlock::Text {
|
||||
decoration: None,
|
||||
text: gpui::SharedString::new(&self.content[current_node.byte_range()]),
|
||||
highlights: Vec::new(),
|
||||
links: Vec::new(),
|
||||
style: gpui::StyleRefinement::default(),
|
||||
}
|
||||
};
|
||||
let Some(content) = (if cursor.goto_next_sibling()
|
||||
&& cursor.node().kind_id() == MARKDOWN_KIND_ID_HEADING_CONTENT
|
||||
{
|
||||
let mut builder = RichTextContentBuilder::new();
|
||||
// because HEADING_CONTENT node includes the space after the heading marker
|
||||
// offset by 1 to exclude the space
|
||||
build_rich_text_for_node(
|
||||
&mut cursor,
|
||||
&mut builder,
|
||||
&self.content,
|
||||
1,
|
||||
theme,
|
||||
None,
|
||||
);
|
||||
Some(builder.build())
|
||||
} else {
|
||||
None
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut block = match marker_node_kind {
|
||||
| MARKDOWN_KIND_ID_ATX_H1_MARKER => block
|
||||
.text_size(rems(2.25))
|
||||
.font_weight(gpui::FontWeight::EXTRA_BOLD)
|
||||
.mb_6(),
|
||||
| MARKDOWN_KIND_ID_ATX_H2_MARKER => block
|
||||
.text_2xl()
|
||||
.font_weight(gpui::FontWeight::BOLD)
|
||||
.mt_12()
|
||||
.mb_4(),
|
||||
| MARKDOWN_KIND_ID_ATX_H3_MARKER => block
|
||||
.text_xl()
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.mt_8()
|
||||
.mb_3(),
|
||||
| MARKDOWN_KIND_ID_ATX_H4_MARKER => block
|
||||
.text_base()
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.mt_6()
|
||||
.mb_2(),
|
||||
| _ => block,
|
||||
}
|
||||
.text_color(theme.colors.text);
|
||||
|
||||
if is_first_heading {
|
||||
is_first_heading = false;
|
||||
block = block.mt_0();
|
||||
}
|
||||
|
||||
cursor.goto_parent();
|
||||
|
||||
self.blocks.push(block);
|
||||
}
|
||||
|
||||
| MARKDOWN_KIND_ID_PARAGRAPH => {
|
||||
let block = block_for_node(&mut cursor, &self.content, 0, theme)
|
||||
.text_color(theme.colors.text)
|
||||
.text_sm();
|
||||
|
||||
self.blocks.push(block);
|
||||
}
|
||||
|
||||
| MARKDOWN_KIND_ID_TIGHT_LIST => {
|
||||
let is_rendered =
|
||||
render_list_node(&mut cursor, &self.content, &mut self.blocks, theme, 0);
|
||||
if !is_rendered {
|
||||
continue;
|
||||
let mut block = match marker_node_kind {
|
||||
| MARKDOWN_KIND_ID_ATX_H1_MARKER => ContentBlock::Heading {
|
||||
font_size: rems(2.25),
|
||||
font_weight: gpui::FontWeight::EXTRA_BOLD,
|
||||
mt: rems(0.),
|
||||
mb: rems(1.5),
|
||||
content,
|
||||
},
|
||||
| MARKDOWN_KIND_ID_ATX_H2_MARKER => ContentBlock::Heading {
|
||||
font_size: rems(1.5),
|
||||
font_weight: gpui::FontWeight::BOLD,
|
||||
mt: rems(1.5),
|
||||
mb: rems(1.),
|
||||
content,
|
||||
},
|
||||
| MARKDOWN_KIND_ID_ATX_H3_MARKER => ContentBlock::Heading {
|
||||
font_size: rems(1.25),
|
||||
font_weight: gpui::FontWeight::SEMIBOLD,
|
||||
mt: rems(2.),
|
||||
mb: rems(0.75),
|
||||
content,
|
||||
},
|
||||
| MARKDOWN_KIND_ID_ATX_H4_MARKER => ContentBlock::Heading {
|
||||
font_size: rems(1.),
|
||||
font_weight: FontWeight::SEMIBOLD,
|
||||
mt: rems(1.5),
|
||||
mb: rems(0.5),
|
||||
content,
|
||||
},
|
||||
| MARKDOWN_KIND_ID_ATX_H5_MARKER | MARKDOWN_KIND_ID_ATX_H6_MARKER | _ => {
|
||||
ContentBlock::Heading {
|
||||
font_size: rems(1.),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
mt: rems(1.5),
|
||||
mb: rems(0.5),
|
||||
content,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
| MARKDOWN_KIND_ID_FENCED_CODE_BLOCK => {
|
||||
// expected tree shape:
|
||||
// fenced_code_block
|
||||
// ├── info_string? (present if there is a language annotation)
|
||||
// └── code_fence_content? (present if there is some content between the backticks)
|
||||
if is_first_heading {
|
||||
is_first_heading = false;
|
||||
}
|
||||
|
||||
if !cursor.goto_first_child() {
|
||||
render_fallback_content(&cursor, &self.content, &mut self.blocks);
|
||||
continue;
|
||||
}
|
||||
cursor.goto_parent();
|
||||
|
||||
let content = if cursor.node().kind_id() == MARKDOWN_KIND_ID_INFO_STRING {
|
||||
// skipping info string (which annotates the code block)
|
||||
if cursor.goto_next_sibling() {
|
||||
// this is code_fence_content node
|
||||
gpui::SharedString::new(
|
||||
cursor
|
||||
.node()
|
||||
.utf8_text(self.content.as_bytes())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
} else {
|
||||
gpui::SharedString::default()
|
||||
}
|
||||
} else {
|
||||
// assuming the current node is already code_fence_content
|
||||
self.blocks.push(block);
|
||||
}
|
||||
|
||||
| MARKDOWN_KIND_ID_PARAGRAPH => {
|
||||
let mut builder = RichTextContentBuilder::new();
|
||||
// because HEADING_CONTENT node includes the space after the heading marker
|
||||
// offset by 1 to exclude the space
|
||||
build_rich_text_for_node(&mut cursor, &mut builder, &self.content, 0, theme, None);
|
||||
self.blocks.push(ContentBlock::Paragraph {
|
||||
decoration: None,
|
||||
content: builder.build(),
|
||||
});
|
||||
}
|
||||
|
||||
| MARKDOWN_KIND_ID_TIGHT_LIST => {
|
||||
let is_rendered =
|
||||
render_list_node(&mut cursor, &self.content, &mut self.blocks, theme, 0);
|
||||
if !is_rendered {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
| MARKDOWN_KIND_ID_FENCED_CODE_BLOCK => {
|
||||
// expected tree shape:
|
||||
// fenced_code_block
|
||||
// ├── info_string? (present if there is a language annotation)
|
||||
// └── code_fence_content? (present if there is some content between the backticks)
|
||||
|
||||
if !cursor.goto_first_child() {
|
||||
self.blocks.push(ContentBlock::Empty);
|
||||
continue;
|
||||
}
|
||||
|
||||
let content = if cursor.node().kind_id() == MARKDOWN_KIND_ID_INFO_STRING {
|
||||
// skipping info string (which annotates the code block)
|
||||
if cursor.goto_next_sibling() {
|
||||
// this is code_fence_content node
|
||||
gpui::SharedString::new(
|
||||
cursor
|
||||
.node()
|
||||
.utf8_text(self.content.as_bytes())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
};
|
||||
|
||||
cursor.goto_parent();
|
||||
|
||||
let block = ContentBlock::Text {
|
||||
decoration: None,
|
||||
text: content,
|
||||
highlights: Vec::new(),
|
||||
links: Vec::new(),
|
||||
style: gpui::StyleRefinement::default(),
|
||||
} else {
|
||||
gpui::SharedString::default()
|
||||
}
|
||||
.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);
|
||||
} else {
|
||||
// assuming the current node is already code_fence_content
|
||||
gpui::SharedString::new(
|
||||
cursor
|
||||
.node()
|
||||
.utf8_text(self.content.as_bytes())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
};
|
||||
|
||||
self.blocks.push(block);
|
||||
}
|
||||
cursor.goto_parent();
|
||||
|
||||
| _ => {
|
||||
println!(
|
||||
"[WARN] formatting not implemenetd for node type {:?}",
|
||||
current_node.kind()
|
||||
);
|
||||
self.blocks.push(ContentBlock::Code { content });
|
||||
}
|
||||
|
||||
let block = block_for_node(&mut cursor, &self.content, 0, theme)
|
||||
.text_color(theme.colors.text)
|
||||
.text_sm();
|
||||
// | MARKDOWN_KIND_ID_TABLE => {
|
||||
// cursor.goto_first_child();
|
||||
// debug_assert!(cursor.node().kind_id() == MARKDOWN_KIND_ID_TABLE_HEADER_ROW);
|
||||
|
||||
self.blocks.push(block);
|
||||
}
|
||||
// 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);
|
||||
|
||||
// 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));
|
||||
|
||||
// if !cursor.goto_next_sibling() {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
| _ => {
|
||||
println!(
|
||||
"[WARN] formatting not implemenetd for node type {:?}",
|
||||
current_node.kind()
|
||||
);
|
||||
|
||||
let mut builder = RichTextContentBuilder::new();
|
||||
build_rich_text_for_node(&mut cursor, &mut builder, &self.content, 0, theme, None);
|
||||
|
||||
self.blocks.push(ContentBlock::Paragraph {
|
||||
decoration: None,
|
||||
content: builder.build(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if !cursor.goto_next_sibling() {
|
||||
@@ -533,59 +548,67 @@ impl gpui::Render for MarkdownText {
|
||||
_window: &mut gpui::Window,
|
||||
cx: &mut gpui::prelude::Context<Self>,
|
||||
) -> impl gpui::prelude::IntoElement {
|
||||
let children = self.blocks.iter().enumerate().map(|(i, block)| {
|
||||
match block {
|
||||
| ContentBlock::Text {
|
||||
decoration,
|
||||
text,
|
||||
highlights,
|
||||
links,
|
||||
style,
|
||||
} => {
|
||||
let styled_text =
|
||||
gpui::StyledText::new(text.clone()).with_highlights(highlights.clone());
|
||||
let theme = app::current_theme(cx);
|
||||
|
||||
let content = if links.is_empty() {
|
||||
div().w_full().child(styled_text)
|
||||
} else {
|
||||
// if link in block, interactive text is needed
|
||||
// to handle link clicks
|
||||
let (link_ranges, srcs): (Vec<_>, Vec<_>) = links.iter().cloned().unzip();
|
||||
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 weak = cx.entity();
|
||||
let t = gpui::InteractiveText::new(i, styled_text).on_click(
|
||||
link_ranges,
|
||||
move |i, _, cx| {
|
||||
if let Some(src) = srcs.get(i) {
|
||||
weak.update(cx, |this, cx| {
|
||||
this.on_open_link(src, cx);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
},
|
||||
);
|
||||
| ContentBlock::Paragraph {
|
||||
decoration,
|
||||
content,
|
||||
} => 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()))),
|
||||
},
|
||||
|
||||
div().w_full().child(t)
|
||||
};
|
||||
| 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()),
|
||||
|
||||
let mut div = match decoration {
|
||||
| Some(d) => div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.gap_2()
|
||||
.items_start()
|
||||
.child(d.clone())
|
||||
.child(div().flex_1().min_w_0().child(content)),
|
||||
| None => div().w_full().child(content),
|
||||
};
|
||||
| ContentBlock::Table {
|
||||
row_count,
|
||||
col_count,
|
||||
cells,
|
||||
} => div(),
|
||||
|
||||
div.style().refine(&style);
|
||||
|
||||
div
|
||||
}
|
||||
}
|
||||
});
|
||||
| ContentBlock::Empty => div(),
|
||||
});
|
||||
|
||||
div().w_full().children(children)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user