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