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