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