1use crate::markdown_elements::{
2 HeadingLevel, Link, ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock,
3 ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownList, ParsedMarkdownListItemType,
4 ParsedMarkdownTable, ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, ParsedMarkdownText,
5};
6use gpui::{
7 div, px, rems, AbsoluteLength, AnyElement, DefiniteLength, Div, Element, ElementId,
8 HighlightStyle, Hsla, InteractiveText, IntoElement, ParentElement, SharedString, Styled,
9 StyledText, TextStyle, WeakView, WindowContext,
10};
11use std::{ops::Range, sync::Arc};
12use theme::{ActiveTheme, SyntaxTheme};
13use ui::{h_flex, v_flex, Label};
14use workspace::Workspace;
15
16pub struct RenderContext {
17 workspace: Option<WeakView<Workspace>>,
18 next_id: usize,
19 text_style: TextStyle,
20 border_color: Hsla,
21 text_color: Hsla,
22 text_muted_color: Hsla,
23 code_block_background_color: Hsla,
24 code_span_background_color: Hsla,
25 syntax_theme: Arc<SyntaxTheme>,
26 indent: usize,
27}
28
29impl RenderContext {
30 pub fn new(workspace: Option<WeakView<Workspace>>, cx: &WindowContext) -> RenderContext {
31 let theme = cx.theme().clone();
32
33 RenderContext {
34 workspace,
35 next_id: 0,
36 indent: 0,
37 text_style: cx.text_style(),
38 syntax_theme: theme.syntax().clone(),
39 border_color: theme.colors().border,
40 text_color: theme.colors().text,
41 text_muted_color: theme.colors().text_muted,
42 code_block_background_color: theme.colors().surface_background,
43 code_span_background_color: theme.colors().editor_document_highlight_read_background,
44 }
45 }
46
47 fn next_id(&mut self, span: &Range<usize>) -> ElementId {
48 let id = format!("markdown-{}-{}-{}", self.next_id, span.start, span.end);
49 self.next_id += 1;
50 ElementId::from(SharedString::from(id))
51 }
52
53 /// This ensures that children inside of block quotes
54 /// have padding between them.
55 ///
56 /// For example, for this markdown:
57 ///
58 /// ```markdown
59 /// > This is a block quote.
60 /// >
61 /// > And this is the next paragraph.
62 /// ```
63 ///
64 /// We give padding between "This is a block quote."
65 /// and "And this is the next paragraph."
66 fn with_common_p(&self, element: Div) -> Div {
67 if self.indent > 0 {
68 element.pb_3()
69 } else {
70 element
71 }
72 }
73}
74
75pub fn render_parsed_markdown(
76 parsed: &ParsedMarkdown,
77 workspace: Option<WeakView<Workspace>>,
78 cx: &WindowContext,
79) -> Vec<AnyElement> {
80 let mut cx = RenderContext::new(workspace, cx);
81 let mut elements = Vec::new();
82
83 for child in &parsed.children {
84 elements.push(render_markdown_block(child, &mut cx));
85 }
86
87 return elements;
88}
89
90pub fn render_markdown_block(block: &ParsedMarkdownElement, cx: &mut RenderContext) -> AnyElement {
91 use ParsedMarkdownElement::*;
92 match block {
93 Paragraph(text) => render_markdown_paragraph(text, cx),
94 Heading(heading) => render_markdown_heading(heading, cx),
95 List(list) => render_markdown_list(list, cx),
96 Table(table) => render_markdown_table(table, cx),
97 BlockQuote(block_quote) => render_markdown_block_quote(block_quote, cx),
98 CodeBlock(code_block) => render_markdown_code_block(code_block, cx),
99 HorizontalRule(_) => render_markdown_rule(cx),
100 }
101}
102
103fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContext) -> AnyElement {
104 let size = match parsed.level {
105 HeadingLevel::H1 => rems(2.),
106 HeadingLevel::H2 => rems(1.5),
107 HeadingLevel::H3 => rems(1.25),
108 HeadingLevel::H4 => rems(1.),
109 HeadingLevel::H5 => rems(0.875),
110 HeadingLevel::H6 => rems(0.85),
111 };
112
113 let color = match parsed.level {
114 HeadingLevel::H6 => cx.text_muted_color,
115 _ => cx.text_color,
116 };
117
118 let line_height = DefiniteLength::from(rems(1.25));
119
120 div()
121 .line_height(line_height)
122 .text_size(size)
123 .text_color(color)
124 .pt(rems(0.15))
125 .pb_1()
126 .child(render_markdown_text(&parsed.contents, cx))
127 .into_any()
128}
129
130fn render_markdown_list(parsed: &ParsedMarkdownList, cx: &mut RenderContext) -> AnyElement {
131 use ParsedMarkdownListItemType::*;
132
133 let mut items = vec![];
134 for item in &parsed.children {
135 let padding = rems((item.depth - 1) as f32 * 0.25);
136
137 let bullet = match item.item_type {
138 Ordered(order) => format!("{}.", order),
139 Unordered => "•".to_string(),
140 Task(checked) => if checked { "☑" } else { "☐" }.to_string(),
141 };
142 let bullet = div().mr_2().child(Label::new(bullet));
143
144 let contents: Vec<AnyElement> = item
145 .contents
146 .iter()
147 .map(|c| render_markdown_block(c.as_ref(), cx))
148 .collect();
149
150 let item = h_flex()
151 .pl(DefiniteLength::Absolute(AbsoluteLength::Rems(padding)))
152 .items_start()
153 .children(vec![bullet, div().children(contents).pr_2().w_full()]);
154
155 items.push(item);
156 }
157
158 cx.with_common_p(div()).children(items).into_any()
159}
160
161fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement {
162 let header = render_markdown_table_row(&parsed.header, &parsed.column_alignments, true, cx);
163
164 let body: Vec<AnyElement> = parsed
165 .body
166 .iter()
167 .map(|row| render_markdown_table_row(row, &parsed.column_alignments, false, cx))
168 .collect();
169
170 cx.with_common_p(v_flex())
171 .w_full()
172 .child(header)
173 .children(body)
174 .into_any()
175}
176
177fn render_markdown_table_row(
178 parsed: &ParsedMarkdownTableRow,
179 alignments: &Vec<ParsedMarkdownTableAlignment>,
180 is_header: bool,
181 cx: &mut RenderContext,
182) -> AnyElement {
183 let mut items = vec![];
184
185 for cell in &parsed.children {
186 let alignment = alignments
187 .get(items.len())
188 .copied()
189 .unwrap_or(ParsedMarkdownTableAlignment::None);
190
191 let contents = render_markdown_text(cell, cx);
192
193 let container = match alignment {
194 ParsedMarkdownTableAlignment::Left | ParsedMarkdownTableAlignment::None => div(),
195 ParsedMarkdownTableAlignment::Center => v_flex().items_center(),
196 ParsedMarkdownTableAlignment::Right => v_flex().items_end(),
197 };
198
199 let mut cell = container
200 .w_full()
201 .child(contents)
202 .px_2()
203 .py_1()
204 .border_color(cx.border_color);
205
206 if is_header {
207 cell = cell.border_2()
208 } else {
209 cell = cell.border_1()
210 }
211
212 items.push(cell);
213 }
214
215 h_flex().children(items).into_any_element()
216}
217
218fn render_markdown_block_quote(
219 parsed: &ParsedMarkdownBlockQuote,
220 cx: &mut RenderContext,
221) -> AnyElement {
222 cx.indent += 1;
223
224 let children: Vec<AnyElement> = parsed
225 .children
226 .iter()
227 .map(|child| render_markdown_block(child, cx))
228 .collect();
229
230 cx.indent -= 1;
231
232 cx.with_common_p(div())
233 .child(
234 div()
235 .border_l_4()
236 .border_color(cx.border_color)
237 .pl_3()
238 .children(children),
239 )
240 .into_any()
241}
242
243fn render_markdown_code_block(
244 parsed: &ParsedMarkdownCodeBlock,
245 cx: &mut RenderContext,
246) -> AnyElement {
247 cx.with_common_p(div())
248 .px_3()
249 .py_3()
250 .bg(cx.code_block_background_color)
251 .child(StyledText::new(parsed.contents.clone()))
252 .into_any()
253}
254
255fn render_markdown_paragraph(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> AnyElement {
256 cx.with_common_p(div())
257 .child(render_markdown_text(parsed, cx))
258 .into_any_element()
259}
260
261fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> AnyElement {
262 let element_id = cx.next_id(&parsed.source_range);
263
264 let highlights = gpui::combine_highlights(
265 parsed.highlights.iter().filter_map(|(range, highlight)| {
266 let highlight = highlight.to_highlight_style(&cx.syntax_theme)?;
267 Some((range.clone(), highlight))
268 }),
269 parsed
270 .regions
271 .iter()
272 .zip(&parsed.region_ranges)
273 .filter_map(|(region, range)| {
274 if region.code {
275 Some((
276 range.clone(),
277 HighlightStyle {
278 background_color: Some(cx.code_span_background_color),
279 ..Default::default()
280 },
281 ))
282 } else {
283 None
284 }
285 }),
286 );
287
288 let mut links = Vec::new();
289 let mut link_ranges = Vec::new();
290 for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) {
291 if let Some(link) = region.link.clone() {
292 links.push(link);
293 link_ranges.push(range.clone());
294 }
295 }
296
297 let workspace = cx.workspace.clone();
298
299 InteractiveText::new(
300 element_id,
301 StyledText::new(parsed.contents.clone()).with_highlights(&cx.text_style, highlights),
302 )
303 .on_click(
304 link_ranges,
305 move |clicked_range_ix, window_cx| match &links[clicked_range_ix] {
306 Link::Web { url } => window_cx.open_url(url),
307 Link::Path { path } => {
308 if let Some(workspace) = &workspace {
309 _ = workspace.update(window_cx, |workspace, cx| {
310 workspace.open_abs_path(path.clone(), false, cx).detach();
311 });
312 }
313 }
314 },
315 )
316 .into_any_element()
317}
318
319fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement {
320 let rule = div().w_full().h(px(2.)).bg(cx.border_color);
321 div().pt_3().pb_3().child(rule).into_any()
322}