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