1use futures::FutureExt;
  2use gpui::{
  3    AnyElement, AnyView, App, BackgroundExecutor, ElementId, FontStyle, FontWeight, HighlightStyle,
  4    InteractiveText, IntoElement, SharedString, StrikethroughStyle, StyledText, UnderlineStyle,
  5    Window,
  6};
  7use language::{HighlightId, Language, LanguageRegistry, Rope};
  8use std::{ops::Range, sync::Arc};
  9use theme::ActiveTheme;
 10use ui::LinkPreview;
 11use util::RangeExt;
 12
 13#[derive(Debug, Clone, PartialEq, Eq)]
 14pub enum Highlight {
 15    Code,
 16    Id(HighlightId),
 17    InlineCode(bool),
 18    Highlight(HighlightStyle),
 19    Mention,
 20    SelfMention,
 21}
 22
 23impl From<HighlightStyle> for Highlight {
 24    fn from(style: HighlightStyle) -> Self {
 25        Self::Highlight(style)
 26    }
 27}
 28
 29impl From<HighlightId> for Highlight {
 30    fn from(style: HighlightId) -> Self {
 31        Self::Id(style)
 32    }
 33}
 34
 35#[derive(Clone, Default)]
 36pub struct RichText {
 37    pub text: SharedString,
 38    pub highlights: Vec<(Range<usize>, Highlight)>,
 39    pub link_ranges: Vec<Range<usize>>,
 40    pub link_urls: Arc<[String]>,
 41
 42    pub custom_ranges: Vec<Range<usize>>,
 43    custom_ranges_tooltip_fn:
 44        Option<Arc<dyn Fn(usize, Range<usize>, &mut Window, &mut App) -> Option<AnyView>>>,
 45}
 46
 47/// Allows one to specify extra links to the rendered markdown, which can be used
 48/// for e.g. mentions.
 49#[derive(Debug)]
 50pub struct Mention {
 51    pub range: Range<usize>,
 52    pub is_self_mention: bool,
 53}
 54
 55impl RichText {
 56    pub fn new(
 57        block: String,
 58        mentions: &[Mention],
 59        language_registry: &Arc<LanguageRegistry>,
 60        executor: &BackgroundExecutor,
 61    ) -> Self {
 62        let mut text = String::new();
 63        let mut highlights = Vec::new();
 64        let mut link_ranges = Vec::new();
 65        let mut link_urls = Vec::new();
 66        render_markdown_mut(
 67            &block,
 68            mentions,
 69            language_registry,
 70            None,
 71            &mut text,
 72            &mut highlights,
 73            &mut link_ranges,
 74            &mut link_urls,
 75            executor,
 76        );
 77        text.truncate(text.trim_end().len());
 78
 79        RichText {
 80            text: SharedString::from(text),
 81            link_urls: link_urls.into(),
 82            link_ranges,
 83            highlights,
 84            custom_ranges: Vec::new(),
 85            custom_ranges_tooltip_fn: None,
 86        }
 87    }
 88
 89    pub fn set_tooltip_builder_for_custom_ranges(
 90        &mut self,
 91        f: impl Fn(usize, Range<usize>, &mut Window, &mut App) -> Option<AnyView> + 'static,
 92    ) {
 93        self.custom_ranges_tooltip_fn = Some(Arc::new(f));
 94    }
 95
 96    pub fn element(&self, id: ElementId, window: &mut Window, cx: &mut App) -> AnyElement {
 97        let theme = cx.theme();
 98        let code_background = theme.colors().surface_background;
 99
100        InteractiveText::new(
101            id,
102            StyledText::new(self.text.clone()).with_default_highlights(
103                &window.text_style(),
104                self.highlights.iter().map(|(range, highlight)| {
105                    (
106                        range.clone(),
107                        match highlight {
108                            Highlight::Code => HighlightStyle {
109                                background_color: Some(code_background),
110                                ..Default::default()
111                            },
112                            Highlight::Id(id) => HighlightStyle {
113                                background_color: Some(code_background),
114                                ..id.style(theme.syntax()).unwrap_or_default()
115                            },
116                            Highlight::InlineCode(link) => {
117                                if *link {
118                                    HighlightStyle {
119                                        background_color: Some(code_background),
120                                        underline: Some(UnderlineStyle {
121                                            thickness: 1.0.into(),
122                                            ..Default::default()
123                                        }),
124                                        ..Default::default()
125                                    }
126                                } else {
127                                    HighlightStyle {
128                                        background_color: Some(code_background),
129                                        ..Default::default()
130                                    }
131                                }
132                            }
133                            Highlight::Highlight(highlight) => *highlight,
134                            Highlight::Mention => HighlightStyle {
135                                font_weight: Some(FontWeight::BOLD),
136                                ..Default::default()
137                            },
138                            Highlight::SelfMention => HighlightStyle {
139                                font_weight: Some(FontWeight::BOLD),
140                                ..Default::default()
141                            },
142                        },
143                    )
144                }),
145            ),
146        )
147        .on_click(self.link_ranges.clone(), {
148            let link_urls = self.link_urls.clone();
149            move |ix, _, cx| {
150                let url = &link_urls[ix];
151                if url.starts_with("http") {
152                    cx.open_url(url);
153                }
154            }
155        })
156        .tooltip({
157            let link_ranges = self.link_ranges.clone();
158            let link_urls = self.link_urls.clone();
159            let custom_tooltip_ranges = self.custom_ranges.clone();
160            let custom_tooltip_fn = self.custom_ranges_tooltip_fn.clone();
161            move |idx, window, cx| {
162                for (ix, range) in link_ranges.iter().enumerate() {
163                    if range.contains(&idx) {
164                        return Some(LinkPreview::new(&link_urls[ix], cx));
165                    }
166                }
167                for range in &custom_tooltip_ranges {
168                    if range.contains(&idx)
169                        && let Some(f) = &custom_tooltip_fn
170                    {
171                        return f(idx, range.clone(), window, cx);
172                    }
173                }
174                None
175            }
176        })
177        .into_any_element()
178    }
179}
180
181pub fn render_markdown_mut(
182    block: &str,
183    mut mentions: &[Mention],
184    language_registry: &Arc<LanguageRegistry>,
185    language: Option<&Arc<Language>>,
186    text: &mut String,
187    highlights: &mut Vec<(Range<usize>, Highlight)>,
188    link_ranges: &mut Vec<Range<usize>>,
189    link_urls: &mut Vec<String>,
190    executor: &BackgroundExecutor,
191) {
192    use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
193
194    let mut bold_depth = 0;
195    let mut italic_depth = 0;
196    let mut strikethrough_depth = 0;
197    let mut link_url = None;
198    let mut current_language = None;
199    let mut list_stack = Vec::new();
200
201    let mut options = Options::all();
202    options.remove(pulldown_cmark::Options::ENABLE_DEFINITION_LIST);
203
204    for (event, source_range) in Parser::new_ext(block, options).into_offset_iter() {
205        let prev_len = text.len();
206        match event {
207            Event::Text(t) => {
208                if let Some(language) = ¤t_language {
209                    render_code(text, highlights, t.as_ref(), language, executor);
210                } else {
211                    while let Some(mention) = mentions.first() {
212                        if !source_range.contains_inclusive(&mention.range) {
213                            break;
214                        }
215                        mentions = &mentions[1..];
216                        let range = (prev_len + mention.range.start - source_range.start)
217                            ..(prev_len + mention.range.end - source_range.start);
218                        highlights.push((
219                            range.clone(),
220                            if mention.is_self_mention {
221                                Highlight::SelfMention
222                            } else {
223                                Highlight::Mention
224                            },
225                        ));
226                    }
227
228                    text.push_str(t.as_ref());
229                    let mut style = HighlightStyle::default();
230                    if bold_depth > 0 {
231                        style.font_weight = Some(FontWeight::BOLD);
232                    }
233                    if italic_depth > 0 {
234                        style.font_style = Some(FontStyle::Italic);
235                    }
236                    if strikethrough_depth > 0 {
237                        style.strikethrough = Some(StrikethroughStyle {
238                            thickness: 1.0.into(),
239                            ..Default::default()
240                        });
241                    }
242                    let last_run_len = if let Some(link_url) = link_url.clone() {
243                        link_ranges.push(prev_len..text.len());
244                        link_urls.push(link_url);
245                        style.underline = Some(UnderlineStyle {
246                            thickness: 1.0.into(),
247                            ..Default::default()
248                        });
249                        prev_len
250                    } else {
251                        // Manually scan for links
252                        let mut finder = linkify::LinkFinder::new();
253                        finder.kinds(&[linkify::LinkKind::Url]);
254                        let mut last_link_len = prev_len;
255                        for link in finder.links(&t) {
256                            let start = link.start();
257                            let end = link.end();
258                            let range = (prev_len + start)..(prev_len + end);
259                            link_ranges.push(range.clone());
260                            link_urls.push(link.as_str().to_string());
261
262                            // If there is a style before we match a link, we have to add this to the highlighted ranges
263                            if style != HighlightStyle::default() && last_link_len < link.start() {
264                                highlights.push((
265                                    last_link_len..link.start(),
266                                    Highlight::Highlight(style),
267                                ));
268                            }
269
270                            highlights.push((
271                                range,
272                                Highlight::Highlight(HighlightStyle {
273                                    underline: Some(UnderlineStyle {
274                                        thickness: 1.0.into(),
275                                        ..Default::default()
276                                    }),
277                                    ..style
278                                }),
279                            ));
280
281                            last_link_len = end;
282                        }
283                        last_link_len
284                    };
285
286                    if style != HighlightStyle::default() && last_run_len < text.len() {
287                        let mut new_highlight = true;
288                        if let Some((last_range, last_style)) = highlights.last_mut()
289                            && last_range.end == last_run_len
290                            && last_style == &Highlight::Highlight(style)
291                        {
292                            last_range.end = text.len();
293                            new_highlight = false;
294                        }
295                        if new_highlight {
296                            highlights
297                                .push((last_run_len..text.len(), Highlight::Highlight(style)));
298                        }
299                    }
300                }
301            }
302            Event::Code(t) => {
303                text.push_str(t.as_ref());
304                let is_link = link_url.is_some();
305
306                if let Some(link_url) = link_url.clone() {
307                    link_ranges.push(prev_len..text.len());
308                    link_urls.push(link_url);
309                }
310
311                highlights.push((prev_len..text.len(), Highlight::InlineCode(is_link)))
312            }
313            Event::Start(tag) => match tag {
314                Tag::Paragraph => new_paragraph(text, &mut list_stack),
315                Tag::Heading { .. } => {
316                    new_paragraph(text, &mut list_stack);
317                    bold_depth += 1;
318                }
319                Tag::CodeBlock(kind) => {
320                    new_paragraph(text, &mut list_stack);
321                    current_language = if let CodeBlockKind::Fenced(language) = kind {
322                        language_registry
323                            .language_for_name(language.as_ref())
324                            .now_or_never()
325                            .and_then(Result::ok)
326                    } else {
327                        language.cloned()
328                    }
329                }
330                Tag::Emphasis => italic_depth += 1,
331                Tag::Strong => bold_depth += 1,
332                Tag::Strikethrough => strikethrough_depth += 1,
333                Tag::Link { dest_url, .. } => link_url = Some(dest_url.to_string()),
334                Tag::List(number) => {
335                    list_stack.push((number, false));
336                }
337                Tag::Item => {
338                    let len = list_stack.len();
339                    if let Some((list_number, has_content)) = list_stack.last_mut() {
340                        *has_content = false;
341                        if !text.is_empty() && !text.ends_with('\n') {
342                            text.push('\n');
343                        }
344                        for _ in 0..len - 1 {
345                            text.push_str("  ");
346                        }
347                        if let Some(number) = list_number {
348                            text.push_str(&format!("{}. ", number));
349                            *number += 1;
350                            *has_content = false;
351                        } else {
352                            text.push_str("- ");
353                        }
354                    }
355                }
356                _ => {}
357            },
358            Event::End(tag) => match tag {
359                TagEnd::Heading(_) => bold_depth -= 1,
360                TagEnd::CodeBlock => current_language = None,
361                TagEnd::Emphasis => italic_depth -= 1,
362                TagEnd::Strong => bold_depth -= 1,
363                TagEnd::Strikethrough => strikethrough_depth -= 1,
364                TagEnd::Link => link_url = None,
365                TagEnd::List(_) => drop(list_stack.pop()),
366                _ => {}
367            },
368            Event::HardBreak => text.push('\n'),
369            Event::SoftBreak => text.push('\n'),
370            _ => {}
371        }
372    }
373}
374
375pub fn render_code(
376    text: &mut String,
377    highlights: &mut Vec<(Range<usize>, Highlight)>,
378    content: &str,
379    language: &Arc<Language>,
380    executor: &BackgroundExecutor,
381) {
382    let prev_len = text.len();
383    text.push_str(content);
384    let mut offset = 0;
385    for (range, highlight_id) in
386        language.highlight_text(&Rope::from_str(content, executor), 0..content.len())
387    {
388        if range.start > offset {
389            highlights.push((prev_len + offset..prev_len + range.start, Highlight::Code));
390        }
391        highlights.push((
392            prev_len + range.start..prev_len + range.end,
393            Highlight::Id(highlight_id),
394        ));
395        offset = range.end;
396    }
397    if offset < content.len() {
398        highlights.push((prev_len + offset..prev_len + content.len(), Highlight::Code));
399    }
400}
401
402pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
403    let mut is_subsequent_paragraph_of_list = false;
404    if let Some((_, has_content)) = list_stack.last_mut() {
405        if *has_content {
406            is_subsequent_paragraph_of_list = true;
407        } else {
408            *has_content = true;
409            return;
410        }
411    }
412
413    if !text.is_empty() {
414        if !text.ends_with('\n') {
415            text.push('\n');
416        }
417        text.push('\n');
418    }
419    for _ in 0..list_stack.len().saturating_sub(1) {
420        text.push_str("  ");
421    }
422    if is_subsequent_paragraph_of_list {
423        text.push_str("  ");
424    }
425}