refactor: prefer Arc<str> to String
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
// markdown treesitter playground: https://ikatyang.github.io/tree-sitter-markdown/
|
||||
|
||||
use std::{ops::Range, sync::LazyLock};
|
||||
use std::{
|
||||
ops::Range,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
|
||||
use gpui::{AppContext, ParentElement, Refineable, Styled, div, px, relative, rems};
|
||||
|
||||
@@ -86,7 +89,7 @@ const MARKDOWN_KIND_ID_TABLE_CELL: u16 = 235;
|
||||
const MARKDOWN_KIND_ID_TASK_LIST_ITEM_MARKER: u16 = 236;
|
||||
|
||||
pub(crate) struct MarkdownText {
|
||||
content: gpui::SharedString,
|
||||
content: Arc<str>,
|
||||
blocks: Vec<ContentBlock>,
|
||||
}
|
||||
|
||||
@@ -100,10 +103,7 @@ enum ContentBlock {
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) fn new(
|
||||
content: gpui::SharedString,
|
||||
cx: &mut gpui::Context<MarkdownText>,
|
||||
) -> MarkdownText {
|
||||
pub(crate) fn new(content: Arc<str>, cx: &mut gpui::Context<MarkdownText>) -> MarkdownText {
|
||||
let mut view = MarkdownText {
|
||||
content,
|
||||
blocks: Vec::new(),
|
||||
@@ -115,20 +115,20 @@ pub(crate) fn new(
|
||||
impl Styled for ContentBlock {
|
||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||
match self {
|
||||
| ContentBlock::Text { style, .. } => style,
|
||||
| ContentBlock::Text { style, .. } => style,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MarkdownText {
|
||||
fn on_create(&mut self, cx: &gpui::Context<Self>) {
|
||||
let content = self.content.clone();
|
||||
let content = Arc::clone(&self.content);
|
||||
let t = cx.background_spawn(async move {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
parser
|
||||
.set_language(tree_sitter_markdown::language())
|
||||
.expect("tree-sitter-markdown language should load");
|
||||
parser.parse(content.as_str(), None)
|
||||
parser.parse(content.as_bytes(), None)
|
||||
});
|
||||
|
||||
cx.spawn(async |weak, cx| {
|
||||
@@ -179,56 +179,54 @@ impl MarkdownText {
|
||||
}
|
||||
|
||||
match node.kind_id() {
|
||||
| MARKDOWN_KIND_ID_EMPHASIS => {
|
||||
| MARKDOWN_KIND_ID_EMPHASIS => {
|
||||
highlights.push((
|
||||
node_range!(),
|
||||
gpui::HighlightStyle {
|
||||
font_style: Some(gpui::FontStyle::Italic),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
| MARKDOWN_KIND_ID_STRONG_EMPHASIS => highlights.push((
|
||||
node_range!(),
|
||||
gpui::HighlightStyle {
|
||||
font_weight: Some(gpui::FontWeight::BOLD),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
|
||||
| MARKDOWN_KIND_ID_LINK => {
|
||||
if cursor.goto_first_child() {
|
||||
highlights.push((
|
||||
node_range!(),
|
||||
gpui::HighlightStyle {
|
||||
font_style: Some(gpui::FontStyle::Italic),
|
||||
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_STRONG_EMPHASIS => highlights.push((
|
||||
node_range!(),
|
||||
gpui::HighlightStyle {
|
||||
font_weight: Some(gpui::FontWeight::BOLD),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
|
||||
| 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()
|
||||
},
|
||||
));
|
||||
|
||||
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)),
|
||||
));
|
||||
} 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()))
|
||||
}
|
||||
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))));
|
||||
} 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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
| _ => {
|
||||
// extend here to support more markdown node stylings
|
||||
}
|
||||
| _ => {
|
||||
// extend here to support more markdown node stylings
|
||||
}
|
||||
};
|
||||
|
||||
if !cursor.goto_next_sibling() {
|
||||
@@ -305,23 +303,23 @@ 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
|
||||
| "-" | "+" | "*" => Some("•".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;
|
||||
Some(format!("{j}."))
|
||||
}
|
||||
|
||||
| _ => None,
|
||||
| _ => None,
|
||||
};
|
||||
|
||||
let Some(list_marker_char) = list_marker_char else {
|
||||
@@ -333,9 +331,9 @@ impl MarkdownText {
|
||||
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()),
|
||||
| ContentBlock::Text {
|
||||
ref mut decoration, ..
|
||||
} => *decoration = Some(list_marker_char.into()),
|
||||
}
|
||||
b
|
||||
} else {
|
||||
@@ -374,150 +372,150 @@ 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;
|
||||
}
|
||||
|
||||
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 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_ATX_HEADING => {
|
||||
if !cursor.goto_first_child() {
|
||||
render_fallback_content(&cursor, &self.content, &mut self.blocks);
|
||||
continue;
|
||||
}
|
||||
|
||||
| MARKDOWN_KIND_ID_PARAGRAPH => {
|
||||
let block = block_for_node(&mut cursor, &self.content, 0, theme)
|
||||
.text_color(theme.colors.text)
|
||||
.text_sm();
|
||||
let marker_node_kind = cursor.node().kind_id();
|
||||
|
||||
self.blocks.push(block);
|
||||
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 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();
|
||||
}
|
||||
|
||||
| MARKDOWN_KIND_ID_TIGHT_LIST => {
|
||||
let is_rendered =
|
||||
render_list_node(&mut cursor, &self.content, &mut self.blocks, theme, 0);
|
||||
if !is_rendered {
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
| 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() {
|
||||
render_fallback_content(&cursor, &self.content, &mut self.blocks);
|
||||
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() {
|
||||
render_fallback_content(&cursor, &self.content, &mut self.blocks);
|
||||
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(),
|
||||
)
|
||||
} else {
|
||||
gpui::SharedString::default()
|
||||
}
|
||||
} else {
|
||||
// assuming the current node is already code_fence_content
|
||||
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()
|
||||
} else {
|
||||
// assuming the current node is already code_fence_content
|
||||
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(),
|
||||
}
|
||||
.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);
|
||||
|
||||
self.blocks.push(block);
|
||||
}
|
||||
|
||||
| _ => {
|
||||
println!(
|
||||
"[WARN] formatting not implemenetd for node type {:?}",
|
||||
current_node.kind()
|
||||
);
|
||||
|
||||
let block = block_for_node(&mut cursor, &self.content, 0, theme)
|
||||
.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);
|
||||
.text_sm();
|
||||
|
||||
self.blocks.push(block);
|
||||
}
|
||||
|
||||
| _ => {
|
||||
println!(
|
||||
"[WARN] formatting not implemenetd for node type {:?}",
|
||||
current_node.kind()
|
||||
);
|
||||
|
||||
let block = block_for_node(&mut cursor, &self.content, 0, theme)
|
||||
.text_color(theme.colors.text)
|
||||
.text_sm();
|
||||
|
||||
self.blocks.push(block);
|
||||
}
|
||||
self.blocks.push(block);
|
||||
}
|
||||
}
|
||||
|
||||
if !cursor.goto_next_sibling() {
|
||||
@@ -535,55 +533,55 @@ impl gpui::Render for MarkdownText {
|
||||
) -> 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());
|
||||
| ContentBlock::Text {
|
||||
decoration,
|
||||
text,
|
||||
highlights,
|
||||
links,
|
||||
style,
|
||||
} => {
|
||||
let styled_text =
|
||||
gpui::StyledText::new(text.clone()).with_highlights(highlights.clone());
|
||||
|
||||
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 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 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();
|
||||
})
|
||||
}
|
||||
},
|
||||
);
|
||||
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();
|
||||
})
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
div().w_full().child(t)
|
||||
};
|
||||
div().w_full().child(t)
|
||||
};
|
||||
|
||||
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),
|
||||
};
|
||||
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),
|
||||
};
|
||||
|
||||
div.style().refine(&style);
|
||||
div.style().refine(&style);
|
||||
|
||||
div
|
||||
}
|
||||
div
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user