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