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