1use futures::FutureExt;
2use gpui::{
3 AnyElement, ElementId, FontStyle, FontWeight, HighlightStyle, InteractiveText, IntoElement,
4 SharedString, StyledText, UnderlineStyle, WindowContext,
5};
6use language::{HighlightId, Language, LanguageRegistry};
7use std::{ops::Range, sync::Arc};
8use theme::ActiveTheme;
9use ui::LinkPreview;
10use util::RangeExt;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum Highlight {
14 Code,
15 Id(HighlightId),
16 Highlight(HighlightStyle),
17 Mention,
18 SelfMention,
19}
20
21impl From<HighlightStyle> for Highlight {
22 fn from(style: HighlightStyle) -> Self {
23 Self::Highlight(style)
24 }
25}
26
27impl From<HighlightId> for Highlight {
28 fn from(style: HighlightId) -> Self {
29 Self::Id(style)
30 }
31}
32
33#[derive(Debug, Clone)]
34pub struct RichText {
35 pub text: SharedString,
36 pub highlights: Vec<(Range<usize>, Highlight)>,
37 pub link_ranges: Vec<Range<usize>>,
38 pub link_urls: Arc<[String]>,
39}
40
41/// Allows one to specify extra links to the rendered markdown, which can be used
42/// for e.g. mentions.
43#[derive(Debug)]
44pub struct Mention {
45 pub range: Range<usize>,
46 pub is_self_mention: bool,
47}
48
49impl RichText {
50 pub fn element(&self, id: ElementId, cx: &mut WindowContext) -> AnyElement {
51 let theme = cx.theme();
52 let code_background = theme.colors().surface_background;
53
54 InteractiveText::new(
55 id,
56 StyledText::new(self.text.clone()).with_highlights(
57 &cx.text_style(),
58 self.highlights.iter().map(|(range, highlight)| {
59 (
60 range.clone(),
61 match highlight {
62 Highlight::Code => HighlightStyle {
63 background_color: Some(code_background),
64 ..Default::default()
65 },
66 Highlight::Id(id) => HighlightStyle {
67 background_color: Some(code_background),
68 ..id.style(theme.syntax()).unwrap_or_default()
69 },
70 Highlight::Highlight(highlight) => *highlight,
71 Highlight::Mention => HighlightStyle {
72 font_weight: Some(FontWeight::BOLD),
73 ..Default::default()
74 },
75 Highlight::SelfMention => HighlightStyle {
76 font_weight: Some(FontWeight::BOLD),
77 ..Default::default()
78 },
79 },
80 )
81 }),
82 ),
83 )
84 .on_click(self.link_ranges.clone(), {
85 let link_urls = self.link_urls.clone();
86 move |ix, cx| cx.open_url(&link_urls[ix])
87 })
88 .tooltip({
89 let link_ranges = self.link_ranges.clone();
90 let link_urls = self.link_urls.clone();
91 move |idx, cx| {
92 for (ix, range) in link_ranges.iter().enumerate() {
93 if range.contains(&idx) {
94 return Some(LinkPreview::new(&link_urls[ix], cx));
95 }
96 }
97 None
98 }
99 })
100 .into_any_element()
101 }
102}
103
104pub fn render_markdown_mut(
105 block: &str,
106 mut mentions: &[Mention],
107 language_registry: &Arc<LanguageRegistry>,
108 language: Option<&Arc<Language>>,
109 text: &mut String,
110 highlights: &mut Vec<(Range<usize>, Highlight)>,
111 link_ranges: &mut Vec<Range<usize>>,
112 link_urls: &mut Vec<String>,
113) {
114 use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
115
116 let mut bold_depth = 0;
117 let mut italic_depth = 0;
118 let mut link_url = None;
119 let mut current_language = None;
120 let mut list_stack = Vec::new();
121
122 let options = Options::all();
123 for (event, source_range) in Parser::new_ext(block, options).into_offset_iter() {
124 let prev_len = text.len();
125 match event {
126 Event::Text(t) => {
127 if let Some(language) = ¤t_language {
128 render_code(text, highlights, t.as_ref(), language);
129 } else {
130 while let Some(mention) = mentions.first() {
131 if !source_range.contains_inclusive(&mention.range) {
132 break;
133 }
134 mentions = &mentions[1..];
135 let range = (prev_len + mention.range.start - source_range.start)
136 ..(prev_len + mention.range.end - source_range.start);
137 highlights.push((
138 range.clone(),
139 if mention.is_self_mention {
140 Highlight::SelfMention
141 } else {
142 Highlight::Mention
143 },
144 ));
145 }
146
147 text.push_str(t.as_ref());
148 let mut style = HighlightStyle::default();
149 if bold_depth > 0 {
150 style.font_weight = Some(FontWeight::BOLD);
151 }
152 if italic_depth > 0 {
153 style.font_style = Some(FontStyle::Italic);
154 }
155 if let Some(link_url) = link_url.clone() {
156 link_ranges.push(prev_len..text.len());
157 link_urls.push(link_url);
158 style.underline = Some(UnderlineStyle {
159 thickness: 1.0.into(),
160 ..Default::default()
161 });
162 }
163
164 if style != HighlightStyle::default() {
165 let mut new_highlight = true;
166 if let Some((last_range, last_style)) = highlights.last_mut() {
167 if last_range.end == prev_len
168 && last_style == &Highlight::Highlight(style)
169 {
170 last_range.end = text.len();
171 new_highlight = false;
172 }
173 }
174 if new_highlight {
175 highlights.push((prev_len..text.len(), Highlight::Highlight(style)));
176 }
177 }
178 }
179 }
180 Event::Code(t) => {
181 text.push_str(t.as_ref());
182 if link_url.is_some() {
183 highlights.push((
184 prev_len..text.len(),
185 Highlight::Highlight(HighlightStyle {
186 underline: Some(UnderlineStyle {
187 thickness: 1.0.into(),
188 ..Default::default()
189 }),
190 ..Default::default()
191 }),
192 ));
193 }
194 if let Some(link_url) = link_url.clone() {
195 link_ranges.push(prev_len..text.len());
196 link_urls.push(link_url);
197 }
198 }
199 Event::Start(tag) => match tag {
200 Tag::Paragraph => new_paragraph(text, &mut list_stack),
201 Tag::Heading(_, _, _) => {
202 new_paragraph(text, &mut list_stack);
203 bold_depth += 1;
204 }
205 Tag::CodeBlock(kind) => {
206 new_paragraph(text, &mut list_stack);
207 current_language = if let CodeBlockKind::Fenced(language) = kind {
208 language_registry
209 .language_for_name(language.as_ref())
210 .now_or_never()
211 .and_then(Result::ok)
212 } else {
213 language.cloned()
214 }
215 }
216 Tag::Emphasis => italic_depth += 1,
217 Tag::Strong => bold_depth += 1,
218 Tag::Link(_, url, _) => link_url = Some(url.to_string()),
219 Tag::List(number) => {
220 list_stack.push((number, false));
221 }
222 Tag::Item => {
223 let len = list_stack.len();
224 if let Some((list_number, has_content)) = list_stack.last_mut() {
225 *has_content = false;
226 if !text.is_empty() && !text.ends_with('\n') {
227 text.push('\n');
228 }
229 for _ in 0..len - 1 {
230 text.push_str(" ");
231 }
232 if let Some(number) = list_number {
233 text.push_str(&format!("{}. ", number));
234 *number += 1;
235 *has_content = false;
236 } else {
237 text.push_str("- ");
238 }
239 }
240 }
241 _ => {}
242 },
243 Event::End(tag) => match tag {
244 Tag::Heading(_, _, _) => bold_depth -= 1,
245 Tag::CodeBlock(_) => current_language = None,
246 Tag::Emphasis => italic_depth -= 1,
247 Tag::Strong => bold_depth -= 1,
248 Tag::Link(_, _, _) => link_url = None,
249 Tag::List(_) => drop(list_stack.pop()),
250 _ => {}
251 },
252 Event::HardBreak => text.push('\n'),
253 Event::SoftBreak => text.push('\n'),
254 _ => {}
255 }
256 }
257}
258
259pub fn render_markdown(
260 block: String,
261 mentions: &[Mention],
262 language_registry: &Arc<LanguageRegistry>,
263 language: Option<&Arc<Language>>,
264) -> RichText {
265 let mut text = String::new();
266 let mut highlights = Vec::new();
267 let mut link_ranges = Vec::new();
268 let mut link_urls = Vec::new();
269 render_markdown_mut(
270 &block,
271 mentions,
272 language_registry,
273 language,
274 &mut text,
275 &mut highlights,
276 &mut link_ranges,
277 &mut link_urls,
278 );
279 text.truncate(text.trim_end().len());
280
281 RichText {
282 text: SharedString::from(text),
283 link_urls: link_urls.into(),
284 link_ranges,
285 highlights,
286 }
287}
288
289pub fn render_code(
290 text: &mut String,
291 highlights: &mut Vec<(Range<usize>, Highlight)>,
292 content: &str,
293 language: &Arc<Language>,
294) {
295 let prev_len = text.len();
296 text.push_str(content);
297 let mut offset = 0;
298 for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
299 if range.start > offset {
300 highlights.push((prev_len + offset..prev_len + range.start, Highlight::Code));
301 }
302 highlights.push((
303 prev_len + range.start..prev_len + range.end,
304 Highlight::Id(highlight_id),
305 ));
306 offset = range.end;
307 }
308 if offset < content.len() {
309 highlights.push((prev_len + offset..prev_len + content.len(), Highlight::Code));
310 }
311}
312
313pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
314 let mut is_subsequent_paragraph_of_list = false;
315 if let Some((_, has_content)) = list_stack.last_mut() {
316 if *has_content {
317 is_subsequent_paragraph_of_list = true;
318 } else {
319 *has_content = true;
320 return;
321 }
322 }
323
324 if !text.is_empty() {
325 if !text.ends_with('\n') {
326 text.push('\n');
327 }
328 text.push('\n');
329 }
330 for _ in 0..list_stack.len().saturating_sub(1) {
331 text.push_str(" ");
332 }
333 if is_subsequent_paragraph_of_list {
334 text.push_str(" ");
335 }
336}