markdown.rs

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