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