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