markdown.rs

  1use std::ops::Range;
  2use std::sync::Arc;
  3
  4use futures::FutureExt;
  5use gpui::{
  6    elements::Text,
  7    fonts::{HighlightStyle, Underline, Weight},
  8    platform::{CursorStyle, MouseButton},
  9    CursorRegion, MouseRegion, ViewContext,
 10};
 11use language::{Language, LanguageRegistry};
 12use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
 13
 14use crate::{Editor, EditorStyle};
 15
 16#[derive(Debug, Clone)]
 17struct RenderedRegion {
 18    code: bool,
 19    link_url: Option<String>,
 20}
 21
 22pub fn render_markdown(
 23    markdown: &str,
 24    language_registry: &Arc<LanguageRegistry>,
 25    language: &Option<Arc<Language>>,
 26    style: &EditorStyle,
 27    cx: &mut ViewContext<Editor>,
 28) -> Text {
 29    let mut text = String::new();
 30    let mut highlights = Vec::new();
 31    let mut region_ranges = Vec::new();
 32    let mut regions = Vec::new();
 33
 34    let mut bold_depth = 0;
 35    let mut italic_depth = 0;
 36    let mut link_url = None;
 37    let mut current_language = None;
 38    let mut list_stack = Vec::new();
 39
 40    for event in Parser::new_ext(&markdown, Options::all()) {
 41        let prev_len = text.len();
 42        match event {
 43            Event::Text(t) => {
 44                if let Some(language) = &current_language {
 45                    render_code(&mut text, &mut highlights, t.as_ref(), language, style);
 46                } else {
 47                    text.push_str(t.as_ref());
 48
 49                    let mut style = HighlightStyle::default();
 50                    if bold_depth > 0 {
 51                        style.weight = Some(Weight::BOLD);
 52                    }
 53                    if italic_depth > 0 {
 54                        style.italic = Some(true);
 55                    }
 56                    if let Some(link_url) = link_url.clone() {
 57                        region_ranges.push(prev_len..text.len());
 58                        regions.push(RenderedRegion {
 59                            link_url: Some(link_url),
 60                            code: false,
 61                        });
 62                        style.underline = Some(Underline {
 63                            thickness: 1.0.into(),
 64                            ..Default::default()
 65                        });
 66                    }
 67
 68                    if style != HighlightStyle::default() {
 69                        let mut new_highlight = true;
 70                        if let Some((last_range, last_style)) = highlights.last_mut() {
 71                            if last_range.end == prev_len && last_style == &style {
 72                                last_range.end = text.len();
 73                                new_highlight = false;
 74                            }
 75                        }
 76                        if new_highlight {
 77                            highlights.push((prev_len..text.len(), style));
 78                        }
 79                    }
 80                }
 81            }
 82
 83            Event::Code(t) => {
 84                text.push_str(t.as_ref());
 85                region_ranges.push(prev_len..text.len());
 86                if link_url.is_some() {
 87                    highlights.push((
 88                        prev_len..text.len(),
 89                        HighlightStyle {
 90                            underline: Some(Underline {
 91                                thickness: 1.0.into(),
 92                                ..Default::default()
 93                            }),
 94                            ..Default::default()
 95                        },
 96                    ));
 97                }
 98                regions.push(RenderedRegion {
 99                    code: true,
100                    link_url: link_url.clone(),
101                });
102            }
103
104            Event::Start(tag) => match tag {
105                Tag::Paragraph => new_paragraph(&mut text, &mut list_stack),
106
107                Tag::Heading(_, _, _) => {
108                    new_paragraph(&mut text, &mut list_stack);
109                    bold_depth += 1;
110                }
111
112                Tag::CodeBlock(kind) => {
113                    new_paragraph(&mut text, &mut list_stack);
114                    current_language = if let CodeBlockKind::Fenced(language) = kind {
115                        language_registry
116                            .language_for_name(language.as_ref())
117                            .now_or_never()
118                            .and_then(Result::ok)
119                    } else {
120                        language.clone()
121                    }
122                }
123
124                Tag::Emphasis => italic_depth += 1,
125
126                Tag::Strong => bold_depth += 1,
127
128                Tag::Link(_, url, _) => link_url = Some(url.to_string()),
129
130                Tag::List(number) => {
131                    list_stack.push((number, false));
132                }
133
134                Tag::Item => {
135                    let len = list_stack.len();
136                    if let Some((list_number, has_content)) = list_stack.last_mut() {
137                        *has_content = false;
138                        if !text.is_empty() && !text.ends_with('\n') {
139                            text.push('\n');
140                        }
141                        for _ in 0..len - 1 {
142                            text.push_str("  ");
143                        }
144                        if let Some(number) = list_number {
145                            text.push_str(&format!("{}. ", number));
146                            *number += 1;
147                            *has_content = false;
148                        } else {
149                            text.push_str("- ");
150                        }
151                    }
152                }
153
154                _ => {}
155            },
156
157            Event::End(tag) => match tag {
158                Tag::Heading(_, _, _) => bold_depth -= 1,
159                Tag::CodeBlock(_) => current_language = None,
160                Tag::Emphasis => italic_depth -= 1,
161                Tag::Strong => bold_depth -= 1,
162                Tag::Link(_, _, _) => link_url = None,
163                Tag::List(_) => drop(list_stack.pop()),
164                _ => {}
165            },
166
167            Event::HardBreak => text.push('\n'),
168
169            Event::SoftBreak => text.push(' '),
170
171            _ => {}
172        }
173    }
174
175    let code_span_background_color = style.document_highlight_read_background;
176    let view_id = cx.view_id();
177    let mut region_id = 0;
178    Text::new(text, style.text.clone())
179        .with_highlights(highlights)
180        .with_custom_runs(region_ranges, move |ix, bounds, scene, _| {
181            region_id += 1;
182            let region = regions[ix].clone();
183            if let Some(url) = region.link_url {
184                scene.push_cursor_region(CursorRegion {
185                    bounds,
186                    style: CursorStyle::PointingHand,
187                });
188                scene.push_mouse_region(
189                    MouseRegion::new::<Editor>(view_id, region_id, bounds)
190                        .on_click::<Editor, _>(MouseButton::Left, move |_, _, cx| {
191                            cx.platform().open_url(&url)
192                        }),
193                );
194            }
195            if region.code {
196                scene.push_quad(gpui::Quad {
197                    bounds,
198                    background: Some(code_span_background_color),
199                    border: Default::default(),
200                    corner_radii: (2.0).into(),
201                });
202            }
203        })
204        .with_soft_wrap(true)
205}
206
207fn render_code(
208    text: &mut String,
209    highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
210    content: &str,
211    language: &Arc<Language>,
212    style: &EditorStyle,
213) {
214    let prev_len = text.len();
215    text.push_str(content);
216    for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
217        if let Some(style) = highlight_id.style(&style.syntax) {
218            highlights.push((prev_len + range.start..prev_len + range.end, style));
219        }
220    }
221}
222
223fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
224    let mut is_subsequent_paragraph_of_list = false;
225    if let Some((_, has_content)) = list_stack.last_mut() {
226        if *has_content {
227            is_subsequent_paragraph_of_list = true;
228        } else {
229            *has_content = true;
230            return;
231        }
232    }
233
234    if !text.is_empty() {
235        if !text.ends_with('\n') {
236            text.push('\n');
237        }
238        text.push('\n');
239    }
240    for _ in 0..list_stack.len().saturating_sub(1) {
241        text.push_str("  ");
242    }
243    if is_subsequent_paragraph_of_list {
244        text.push_str("  ");
245    }
246}