1use std::{ops::Range, sync::Arc};
2
3use futures::FutureExt;
4use gpui::{
5 color::Color,
6 elements::Text,
7 fonts::{HighlightStyle, TextStyle, Underline, Weight},
8 platform::{CursorStyle, MouseButton},
9 AnyElement, CursorRegion, Element, MouseRegion, ViewContext,
10};
11use language::{HighlightId, Language, LanguageRegistry};
12use theme::SyntaxTheme;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum Highlight {
16 Id(HighlightId),
17 Highlight(HighlightStyle),
18}
19
20#[derive(Debug, Clone)]
21pub struct RichText {
22 pub text: String,
23 pub highlights: Vec<(Range<usize>, Highlight)>,
24 pub region_ranges: Vec<Range<usize>>,
25 pub regions: Vec<RenderedRegion>,
26}
27
28#[derive(Debug, Clone)]
29pub struct RenderedRegion {
30 code: bool,
31 link_url: Option<String>,
32}
33
34impl RichText {
35 pub fn element<V: 'static>(
36 &self,
37 syntax: Arc<SyntaxTheme>,
38 style: TextStyle,
39 code_span_background_color: Color,
40 cx: &mut ViewContext<V>,
41 ) -> AnyElement<V> {
42 let mut region_id = 0;
43 let view_id = cx.view_id();
44
45 let regions = self.regions.clone();
46
47 enum Markdown {}
48 Text::new(self.text.clone(), style.clone())
49 .with_highlights(
50 self.highlights
51 .iter()
52 .filter_map(|(range, highlight)| {
53 let style = match highlight {
54 Highlight::Id(id) => id.style(&syntax)?,
55 Highlight::Highlight(style) => style.clone(),
56 };
57 Some((range.clone(), style))
58 })
59 .collect::<Vec<_>>(),
60 )
61 .with_custom_runs(self.region_ranges.clone(), move |ix, bounds, cx| {
62 region_id += 1;
63 let region = regions[ix].clone();
64 if let Some(url) = region.link_url {
65 cx.scene().push_cursor_region(CursorRegion {
66 bounds,
67 style: CursorStyle::PointingHand,
68 });
69 cx.scene().push_mouse_region(
70 MouseRegion::new::<Markdown>(view_id, region_id, bounds)
71 .on_click::<V, _>(MouseButton::Left, move |_, _, cx| {
72 cx.platform().open_url(&url)
73 }),
74 );
75 }
76 if region.code {
77 cx.scene().push_quad(gpui::Quad {
78 bounds,
79 background: Some(code_span_background_color),
80 border: Default::default(),
81 corner_radii: (2.0).into(),
82 });
83 }
84 })
85 .with_soft_wrap(true)
86 .into_any()
87 }
88}
89
90pub fn render_markdown_mut(
91 block: &str,
92 language_registry: &Arc<LanguageRegistry>,
93 language: Option<&Arc<Language>>,
94 data: &mut RichText,
95) {
96 use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
97
98 let mut bold_depth = 0;
99 let mut italic_depth = 0;
100 let mut link_url = None;
101 let mut current_language = None;
102 let mut list_stack = Vec::new();
103
104 for event in Parser::new_ext(&block, Options::all()) {
105 let prev_len = data.text.len();
106 match event {
107 Event::Text(t) => {
108 if let Some(language) = ¤t_language {
109 render_code(&mut data.text, &mut data.highlights, t.as_ref(), language);
110 } else {
111 data.text.push_str(t.as_ref());
112
113 let mut style = HighlightStyle::default();
114 if bold_depth > 0 {
115 style.weight = Some(Weight::BOLD);
116 }
117 if italic_depth > 0 {
118 style.italic = Some(true);
119 }
120 if let Some(link_url) = link_url.clone() {
121 data.region_ranges.push(prev_len..data.text.len());
122 data.regions.push(RenderedRegion {
123 link_url: Some(link_url),
124 code: false,
125 });
126 style.underline = Some(Underline {
127 thickness: 1.0.into(),
128 ..Default::default()
129 });
130 }
131
132 if style != HighlightStyle::default() {
133 let mut new_highlight = true;
134 if let Some((last_range, last_style)) = data.highlights.last_mut() {
135 if last_range.end == prev_len
136 && last_style == &Highlight::Highlight(style)
137 {
138 last_range.end = data.text.len();
139 new_highlight = false;
140 }
141 }
142 if new_highlight {
143 data.highlights
144 .push((prev_len..data.text.len(), Highlight::Highlight(style)));
145 }
146 }
147 }
148 }
149 Event::Code(t) => {
150 data.text.push_str(t.as_ref());
151 data.region_ranges.push(prev_len..data.text.len());
152 if link_url.is_some() {
153 data.highlights.push((
154 prev_len..data.text.len(),
155 Highlight::Highlight(HighlightStyle {
156 underline: Some(Underline {
157 thickness: 1.0.into(),
158 ..Default::default()
159 }),
160 ..Default::default()
161 }),
162 ));
163 }
164 data.regions.push(RenderedRegion {
165 code: true,
166 link_url: link_url.clone(),
167 });
168 }
169 Event::Start(tag) => match tag {
170 Tag::Paragraph => new_paragraph(&mut data.text, &mut list_stack),
171 Tag::Heading(_, _, _) => {
172 new_paragraph(&mut data.text, &mut list_stack);
173 bold_depth += 1;
174 }
175 Tag::CodeBlock(kind) => {
176 new_paragraph(&mut data.text, &mut list_stack);
177 current_language = if let CodeBlockKind::Fenced(language) = kind {
178 language_registry
179 .language_for_name(language.as_ref())
180 .now_or_never()
181 .and_then(Result::ok)
182 } else {
183 language.cloned()
184 }
185 }
186 Tag::Emphasis => italic_depth += 1,
187 Tag::Strong => bold_depth += 1,
188 Tag::Link(_, url, _) => link_url = Some(url.to_string()),
189 Tag::List(number) => {
190 list_stack.push((number, false));
191 }
192 Tag::Item => {
193 let len = list_stack.len();
194 if let Some((list_number, has_content)) = list_stack.last_mut() {
195 *has_content = false;
196 if !data.text.is_empty() && !data.text.ends_with('\n') {
197 data.text.push('\n');
198 }
199 for _ in 0..len - 1 {
200 data.text.push_str(" ");
201 }
202 if let Some(number) = list_number {
203 data.text.push_str(&format!("{}. ", number));
204 *number += 1;
205 *has_content = false;
206 } else {
207 data.text.push_str("- ");
208 }
209 }
210 }
211 _ => {}
212 },
213 Event::End(tag) => match tag {
214 Tag::Heading(_, _, _) => bold_depth -= 1,
215 Tag::CodeBlock(_) => current_language = None,
216 Tag::Emphasis => italic_depth -= 1,
217 Tag::Strong => bold_depth -= 1,
218 Tag::Link(_, _, _) => link_url = None,
219 Tag::List(_) => drop(list_stack.pop()),
220 _ => {}
221 },
222 Event::HardBreak => data.text.push('\n'),
223 Event::SoftBreak => data.text.push(' '),
224 _ => {}
225 }
226 }
227}
228
229pub fn render_markdown(
230 block: String,
231 language_registry: &Arc<LanguageRegistry>,
232 language: Option<&Arc<Language>>,
233) -> RichText {
234 let mut data = RichText {
235 text: Default::default(),
236 highlights: Default::default(),
237 region_ranges: Default::default(),
238 regions: Default::default(),
239 };
240
241 render_markdown_mut(&block, language_registry, language, &mut data);
242
243 data.text = data.text.trim().to_string();
244
245 data
246}
247
248pub fn render_code(
249 text: &mut String,
250 highlights: &mut Vec<(Range<usize>, Highlight)>,
251 content: &str,
252 language: &Arc<Language>,
253) {
254 let prev_len = text.len();
255 text.push_str(content);
256 for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
257 highlights.push((
258 prev_len + range.start..prev_len + range.end,
259 Highlight::Id(highlight_id),
260 ));
261 }
262}
263
264pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
265 let mut is_subsequent_paragraph_of_list = false;
266 if let Some((_, has_content)) = list_stack.last_mut() {
267 if *has_content {
268 is_subsequent_paragraph_of_list = true;
269 } else {
270 *has_content = true;
271 return;
272 }
273 }
274
275 if !text.is_empty() {
276 if !text.ends_with('\n') {
277 text.push('\n');
278 }
279 text.push('\n');
280 }
281 for _ in 0..list_stack.len().saturating_sub(1) {
282 text.push_str(" ");
283 }
284 if is_subsequent_paragraph_of_list {
285 text.push_str(" ");
286 }
287}