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: &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| {
87 let url = &link_urls[ix];
88 if url.starts_with("http") {
89 cx.open_url(url);
90 }
91 }
92 })
93 .tooltip({
94 let link_ranges = self.link_ranges.clone();
95 let link_urls = self.link_urls.clone();
96 move |idx, cx| {
97 for (ix, range) in link_ranges.iter().enumerate() {
98 if range.contains(&idx) {
99 return Some(LinkPreview::new(&link_urls[ix], cx));
100 }
101 }
102 None
103 }
104 })
105 .into_any_element()
106 }
107}
108
109pub fn render_markdown_mut(
110 block: &str,
111 mut mentions: &[Mention],
112 language_registry: &Arc<LanguageRegistry>,
113 language: Option<&Arc<Language>>,
114 text: &mut String,
115 highlights: &mut Vec<(Range<usize>, Highlight)>,
116 link_ranges: &mut Vec<Range<usize>>,
117 link_urls: &mut Vec<String>,
118) {
119 use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
120
121 let mut bold_depth = 0;
122 let mut italic_depth = 0;
123 let mut link_url = None;
124 let mut current_language = None;
125 let mut list_stack = Vec::new();
126
127 let options = Options::all();
128 for (event, source_range) in Parser::new_ext(block, options).into_offset_iter() {
129 let prev_len = text.len();
130 match event {
131 Event::Text(t) => {
132 if let Some(language) = ¤t_language {
133 render_code(text, highlights, t.as_ref(), language);
134 } else {
135 while let Some(mention) = mentions.first() {
136 if !source_range.contains_inclusive(&mention.range) {
137 break;
138 }
139 mentions = &mentions[1..];
140 let range = (prev_len + mention.range.start - source_range.start)
141 ..(prev_len + mention.range.end - source_range.start);
142 highlights.push((
143 range.clone(),
144 if mention.is_self_mention {
145 Highlight::SelfMention
146 } else {
147 Highlight::Mention
148 },
149 ));
150 }
151
152 text.push_str(t.as_ref());
153 let mut style = HighlightStyle::default();
154 if bold_depth > 0 {
155 style.font_weight = Some(FontWeight::BOLD);
156 }
157 if italic_depth > 0 {
158 style.font_style = Some(FontStyle::Italic);
159 }
160 if let Some(link_url) = link_url.clone() {
161 link_ranges.push(prev_len..text.len());
162 link_urls.push(link_url);
163 style.underline = Some(UnderlineStyle {
164 thickness: 1.0.into(),
165 ..Default::default()
166 });
167 }
168
169 if style != HighlightStyle::default() {
170 let mut new_highlight = true;
171 if let Some((last_range, last_style)) = highlights.last_mut() {
172 if last_range.end == prev_len
173 && last_style == &Highlight::Highlight(style)
174 {
175 last_range.end = text.len();
176 new_highlight = false;
177 }
178 }
179 if new_highlight {
180 highlights.push((prev_len..text.len(), Highlight::Highlight(style)));
181 }
182 }
183 }
184 }
185 Event::Code(t) => {
186 text.push_str(t.as_ref());
187 if link_url.is_some() {
188 highlights.push((
189 prev_len..text.len(),
190 Highlight::Highlight(HighlightStyle {
191 underline: Some(UnderlineStyle {
192 thickness: 1.0.into(),
193 ..Default::default()
194 }),
195 ..Default::default()
196 }),
197 ));
198 }
199 if let Some(link_url) = link_url.clone() {
200 link_ranges.push(prev_len..text.len());
201 link_urls.push(link_url);
202 }
203 }
204 Event::Start(tag) => match tag {
205 Tag::Paragraph => new_paragraph(text, &mut list_stack),
206 Tag::Heading(_, _, _) => {
207 new_paragraph(text, &mut list_stack);
208 bold_depth += 1;
209 }
210 Tag::CodeBlock(kind) => {
211 new_paragraph(text, &mut list_stack);
212 current_language = if let CodeBlockKind::Fenced(language) = kind {
213 language_registry
214 .language_for_name(language.as_ref())
215 .now_or_never()
216 .and_then(Result::ok)
217 } else {
218 language.cloned()
219 }
220 }
221 Tag::Emphasis => italic_depth += 1,
222 Tag::Strong => bold_depth += 1,
223 Tag::Link(_, url, _) => link_url = Some(url.to_string()),
224 Tag::List(number) => {
225 list_stack.push((number, false));
226 }
227 Tag::Item => {
228 let len = list_stack.len();
229 if let Some((list_number, has_content)) = list_stack.last_mut() {
230 *has_content = false;
231 if !text.is_empty() && !text.ends_with('\n') {
232 text.push('\n');
233 }
234 for _ in 0..len - 1 {
235 text.push_str(" ");
236 }
237 if let Some(number) = list_number {
238 text.push_str(&format!("{}. ", number));
239 *number += 1;
240 *has_content = false;
241 } else {
242 text.push_str("- ");
243 }
244 }
245 }
246 _ => {}
247 },
248 Event::End(tag) => match tag {
249 Tag::Heading(_, _, _) => bold_depth -= 1,
250 Tag::CodeBlock(_) => current_language = None,
251 Tag::Emphasis => italic_depth -= 1,
252 Tag::Strong => bold_depth -= 1,
253 Tag::Link(_, _, _) => link_url = None,
254 Tag::List(_) => drop(list_stack.pop()),
255 _ => {}
256 },
257 Event::HardBreak => text.push('\n'),
258 Event::SoftBreak => text.push('\n'),
259 _ => {}
260 }
261 }
262}
263
264pub fn render_rich_text(
265 block: String,
266 mentions: &[Mention],
267 language_registry: &Arc<LanguageRegistry>,
268 language: Option<&Arc<Language>>,
269) -> RichText {
270 let mut text = String::new();
271 let mut highlights = Vec::new();
272 let mut link_ranges = Vec::new();
273 let mut link_urls = Vec::new();
274 render_markdown_mut(
275 &block,
276 mentions,
277 language_registry,
278 language,
279 &mut text,
280 &mut highlights,
281 &mut link_ranges,
282 &mut link_urls,
283 );
284 text.truncate(text.trim_end().len());
285
286 RichText {
287 text: SharedString::from(text),
288 link_urls: link_urls.into(),
289 link_ranges,
290 highlights,
291 }
292}
293
294pub fn render_code(
295 text: &mut String,
296 highlights: &mut Vec<(Range<usize>, Highlight)>,
297 content: &str,
298 language: &Arc<Language>,
299) {
300 let prev_len = text.len();
301 text.push_str(content);
302 let mut offset = 0;
303 for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
304 if range.start > offset {
305 highlights.push((prev_len + offset..prev_len + range.start, Highlight::Code));
306 }
307 highlights.push((
308 prev_len + range.start..prev_len + range.end,
309 Highlight::Id(highlight_id),
310 ));
311 offset = range.end;
312 }
313 if offset < content.len() {
314 highlights.push((prev_len + offset..prev_len + content.len(), Highlight::Code));
315 }
316}
317
318pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
319 let mut is_subsequent_paragraph_of_list = false;
320 if let Some((_, has_content)) = list_stack.last_mut() {
321 if *has_content {
322 is_subsequent_paragraph_of_list = true;
323 } else {
324 *has_content = true;
325 return;
326 }
327 }
328
329 if !text.is_empty() {
330 if !text.ends_with('\n') {
331 text.push('\n');
332 }
333 text.push('\n');
334 }
335 for _ in 0..list_stack.len().saturating_sub(1) {
336 text.push_str(" ");
337 }
338 if is_subsequent_paragraph_of_list {
339 text.push_str(" ");
340 }
341}