1pub mod parser;
2mod path_range;
3
4use base64::Engine as _;
5use log::Level;
6pub use path_range::{LineCol, PathWithRange};
7
8use std::borrow::Cow;
9use std::collections::HashMap;
10use std::collections::HashSet;
11use std::iter;
12use std::mem;
13use std::ops::Range;
14use std::path::Path;
15use std::rc::Rc;
16use std::sync::Arc;
17use std::time::Duration;
18
19use gpui::{
20 AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity,
21 FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image,
22 ImageFormat, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent,
23 Point, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun,
24 TextStyle, TextStyleRefinement, actions, img, point, quad,
25};
26use language::{Language, LanguageRegistry, Rope};
27use parser::CodeBlockMetadata;
28use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown};
29use pulldown_cmark::Alignment;
30use sum_tree::TreeMap;
31use theme::SyntaxTheme;
32use ui::{Tooltip, prelude::*};
33use util::{ResultExt, TryFutureExt};
34
35use crate::parser::CodeBlockKind;
36
37/// A callback function that can be used to customize the style of links based on the destination URL.
38/// If the callback returns `None`, the default link style will be used.
39type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>;
40
41/// Defines custom style refinements for each heading level (H1-H6)
42#[derive(Clone, Default)]
43pub struct HeadingLevelStyles {
44 pub h1: Option<TextStyleRefinement>,
45 pub h2: Option<TextStyleRefinement>,
46 pub h3: Option<TextStyleRefinement>,
47 pub h4: Option<TextStyleRefinement>,
48 pub h5: Option<TextStyleRefinement>,
49 pub h6: Option<TextStyleRefinement>,
50}
51
52#[derive(Clone)]
53pub struct MarkdownStyle {
54 pub base_text_style: TextStyle,
55 pub code_block: StyleRefinement,
56 pub code_block_overflow_x_scroll: bool,
57 pub inline_code: TextStyleRefinement,
58 pub block_quote: TextStyleRefinement,
59 pub link: TextStyleRefinement,
60 pub link_callback: Option<LinkStyleCallback>,
61 pub rule_color: Hsla,
62 pub block_quote_border_color: Hsla,
63 pub syntax: Arc<SyntaxTheme>,
64 pub selection_background_color: Hsla,
65 pub heading: StyleRefinement,
66 pub heading_level_styles: Option<HeadingLevelStyles>,
67 pub table_overflow_x_scroll: bool,
68 pub height_is_multiple_of_line_height: bool,
69}
70
71impl Default for MarkdownStyle {
72 fn default() -> Self {
73 Self {
74 base_text_style: Default::default(),
75 code_block: Default::default(),
76 code_block_overflow_x_scroll: false,
77 inline_code: Default::default(),
78 block_quote: Default::default(),
79 link: Default::default(),
80 link_callback: None,
81 rule_color: Default::default(),
82 block_quote_border_color: Default::default(),
83 syntax: Arc::new(SyntaxTheme::default()),
84 selection_background_color: Default::default(),
85 heading: Default::default(),
86 heading_level_styles: None,
87 table_overflow_x_scroll: false,
88 height_is_multiple_of_line_height: false,
89 }
90 }
91}
92
93pub struct Markdown {
94 source: SharedString,
95 selection: Selection,
96 pressed_link: Option<RenderedLink>,
97 autoscroll_request: Option<usize>,
98 parsed_markdown: ParsedMarkdown,
99 images_by_source_offset: HashMap<usize, Arc<Image>>,
100 should_reparse: bool,
101 pending_parse: Option<Task<Option<()>>>,
102 focus_handle: FocusHandle,
103 language_registry: Option<Arc<LanguageRegistry>>,
104 fallback_code_block_language: Option<String>,
105 options: Options,
106 copied_code_blocks: HashSet<ElementId>,
107}
108
109struct Options {
110 parse_links_only: bool,
111}
112
113pub enum CodeBlockRenderer {
114 Default {
115 copy_button: bool,
116 border: bool,
117 },
118 Custom {
119 render: CodeBlockRenderFn,
120 /// A function that can modify the parent container after the code block
121 /// content has been appended as a child element.
122 transform: Option<CodeBlockTransformFn>,
123 },
124}
125
126pub type CodeBlockRenderFn = Arc<
127 dyn Fn(
128 &CodeBlockKind,
129 &ParsedMarkdown,
130 Range<usize>,
131 CodeBlockMetadata,
132 &mut Window,
133 &App,
134 ) -> Div,
135>;
136
137pub type CodeBlockTransformFn =
138 Arc<dyn Fn(AnyDiv, Range<usize>, CodeBlockMetadata, &mut Window, &App) -> AnyDiv>;
139
140actions!(markdown, [Copy, CopyAsMarkdown]);
141
142impl Markdown {
143 pub fn new(
144 source: SharedString,
145 language_registry: Option<Arc<LanguageRegistry>>,
146 fallback_code_block_language: Option<String>,
147 cx: &mut Context<Self>,
148 ) -> Self {
149 let focus_handle = cx.focus_handle();
150 let mut this = Self {
151 source,
152 selection: Selection::default(),
153 pressed_link: None,
154 autoscroll_request: None,
155 should_reparse: false,
156 images_by_source_offset: Default::default(),
157 parsed_markdown: ParsedMarkdown::default(),
158 pending_parse: None,
159 focus_handle,
160 language_registry,
161 fallback_code_block_language,
162 options: Options {
163 parse_links_only: false,
164 },
165 copied_code_blocks: HashSet::new(),
166 };
167 this.parse(cx);
168 this
169 }
170
171 pub fn new_text(source: SharedString, cx: &mut Context<Self>) -> Self {
172 let focus_handle = cx.focus_handle();
173 let mut this = Self {
174 source,
175 selection: Selection::default(),
176 pressed_link: None,
177 autoscroll_request: None,
178 should_reparse: false,
179 parsed_markdown: ParsedMarkdown::default(),
180 images_by_source_offset: Default::default(),
181 pending_parse: None,
182 focus_handle,
183 language_registry: None,
184 fallback_code_block_language: None,
185 options: Options {
186 parse_links_only: true,
187 },
188 copied_code_blocks: HashSet::new(),
189 };
190 this.parse(cx);
191 this
192 }
193
194 pub fn source(&self) -> &str {
195 &self.source
196 }
197
198 pub fn append(&mut self, text: &str, cx: &mut Context<Self>) {
199 self.source = SharedString::new(self.source.to_string() + text);
200 self.parse(cx);
201 }
202
203 pub fn replace(&mut self, source: impl Into<SharedString>, cx: &mut Context<Self>) {
204 self.source = source.into();
205 self.parse(cx);
206 }
207
208 pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) {
209 if source == self.source() {
210 return;
211 }
212 self.source = source;
213 self.selection = Selection::default();
214 self.autoscroll_request = None;
215 self.pending_parse = None;
216 self.should_reparse = false;
217 self.parsed_markdown = ParsedMarkdown::default();
218 self.parse(cx);
219 }
220
221 pub fn parsed_markdown(&self) -> &ParsedMarkdown {
222 &self.parsed_markdown
223 }
224
225 pub fn escape(s: &str) -> Cow<str> {
226 // Valid to use bytes since multi-byte UTF-8 doesn't use ASCII chars.
227 let count = s
228 .bytes()
229 .filter(|c| *c == b'\n' || c.is_ascii_punctuation())
230 .count();
231 if count > 0 {
232 let mut output = String::with_capacity(s.len() + count);
233 let mut is_newline = false;
234 for c in s.chars() {
235 if is_newline && c == ' ' {
236 continue;
237 }
238 is_newline = c == '\n';
239 if c == '\n' {
240 output.push('\n')
241 } else if c.is_ascii_punctuation() {
242 output.push('\\')
243 }
244 output.push(c)
245 }
246 output.into()
247 } else {
248 s.into()
249 }
250 }
251
252 fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
253 if self.selection.end <= self.selection.start {
254 return;
255 }
256 let text = text.text_for_range(self.selection.start..self.selection.end);
257 cx.write_to_clipboard(ClipboardItem::new_string(text));
258 }
259
260 fn copy_as_markdown(&self, _: &mut Window, cx: &mut Context<Self>) {
261 if self.selection.end <= self.selection.start {
262 return;
263 }
264 let text = self.source[self.selection.start..self.selection.end].to_string();
265 cx.write_to_clipboard(ClipboardItem::new_string(text));
266 }
267
268 fn parse(&mut self, cx: &mut Context<Self>) {
269 if self.source.is_empty() {
270 return;
271 }
272
273 if self.pending_parse.is_some() {
274 self.should_reparse = true;
275 return;
276 }
277
278 let source = self.source.clone();
279 let should_parse_links_only = self.options.parse_links_only;
280 let language_registry = self.language_registry.clone();
281 let fallback = self.fallback_code_block_language.clone();
282 let parsed = cx.background_spawn(async move {
283 if should_parse_links_only {
284 return anyhow::Ok((
285 ParsedMarkdown {
286 events: Arc::from(parse_links_only(source.as_ref())),
287 source,
288 languages_by_name: TreeMap::default(),
289 languages_by_path: TreeMap::default(),
290 },
291 Default::default(),
292 ));
293 }
294 let (events, language_names, paths) = parse_markdown(&source);
295 let mut images_by_source_offset = HashMap::default();
296 let mut languages_by_name = TreeMap::default();
297 let mut languages_by_path = TreeMap::default();
298 if let Some(registry) = language_registry.as_ref() {
299 for name in language_names {
300 let language = if !name.is_empty() {
301 registry.language_for_name_or_extension(&name)
302 } else if let Some(fallback) = &fallback {
303 registry.language_for_name_or_extension(fallback)
304 } else {
305 continue;
306 };
307 if let Ok(language) = language.await {
308 languages_by_name.insert(name, language);
309 }
310 }
311
312 for path in paths {
313 if let Ok(language) = registry.language_for_file_path(&path).await {
314 languages_by_path.insert(path, language);
315 }
316 }
317 }
318
319 for (range, event) in &events {
320 if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event {
321 if let Some(data_url) = dest_url.strip_prefix("data:") {
322 let Some((mime_info, data)) = data_url.split_once(',') else {
323 continue;
324 };
325 let Some((mime_type, encoding)) = mime_info.split_once(';') else {
326 continue;
327 };
328 let Some(format) = ImageFormat::from_mime_type(mime_type) else {
329 continue;
330 };
331 let is_base64 = encoding == "base64";
332 if is_base64 {
333 if let Some(bytes) = base64::prelude::BASE64_STANDARD
334 .decode(data)
335 .log_with_level(Level::Debug)
336 {
337 let image = Arc::new(Image::from_bytes(format, bytes));
338 images_by_source_offset.insert(range.start, image);
339 }
340 }
341 }
342 }
343 }
344
345 anyhow::Ok((
346 ParsedMarkdown {
347 source,
348 events: Arc::from(events),
349 languages_by_name,
350 languages_by_path,
351 },
352 images_by_source_offset,
353 ))
354 });
355
356 self.should_reparse = false;
357 self.pending_parse = Some(cx.spawn(async move |this, cx| {
358 async move {
359 let (parsed, images_by_source_offset) = parsed.await?;
360
361 this.update(cx, |this, cx| {
362 this.parsed_markdown = parsed;
363 this.images_by_source_offset = images_by_source_offset;
364 this.pending_parse.take();
365 if this.should_reparse {
366 this.parse(cx);
367 }
368 cx.notify();
369 })
370 .ok();
371 anyhow::Ok(())
372 }
373 .log_err()
374 .await
375 }));
376 }
377}
378
379impl Focusable for Markdown {
380 fn focus_handle(&self, _cx: &App) -> FocusHandle {
381 self.focus_handle.clone()
382 }
383}
384
385#[derive(Copy, Clone, Default, Debug)]
386struct Selection {
387 start: usize,
388 end: usize,
389 reversed: bool,
390 pending: bool,
391}
392
393impl Selection {
394 fn set_head(&mut self, head: usize) {
395 if head < self.tail() {
396 if !self.reversed {
397 self.end = self.start;
398 self.reversed = true;
399 }
400 self.start = head;
401 } else {
402 if self.reversed {
403 self.start = self.end;
404 self.reversed = false;
405 }
406 self.end = head;
407 }
408 }
409
410 fn tail(&self) -> usize {
411 if self.reversed { self.end } else { self.start }
412 }
413}
414
415#[derive(Clone, Default)]
416pub struct ParsedMarkdown {
417 pub source: SharedString,
418 pub events: Arc<[(Range<usize>, MarkdownEvent)]>,
419 pub languages_by_name: TreeMap<SharedString, Arc<Language>>,
420 pub languages_by_path: TreeMap<Arc<Path>, Arc<Language>>,
421}
422
423impl ParsedMarkdown {
424 pub fn source(&self) -> &SharedString {
425 &self.source
426 }
427
428 pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
429 &self.events
430 }
431}
432
433pub struct MarkdownElement {
434 markdown: Entity<Markdown>,
435 style: MarkdownStyle,
436 code_block_renderer: CodeBlockRenderer,
437 on_url_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
438}
439
440impl MarkdownElement {
441 pub fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
442 Self {
443 markdown,
444 style,
445 code_block_renderer: CodeBlockRenderer::Default {
446 copy_button: true,
447 border: false,
448 },
449 on_url_click: None,
450 }
451 }
452
453 #[cfg(any(test, feature = "test-support"))]
454 pub fn rendered_text(
455 markdown: Entity<Markdown>,
456 cx: &mut gpui::VisualTestContext,
457 style: impl FnOnce(&Window, &App) -> MarkdownStyle,
458 ) -> String {
459 use gpui::size;
460
461 let (text, _) = cx.draw(
462 Default::default(),
463 size(px(600.0), px(600.0)),
464 |window, cx| Self::new(markdown, style(window, cx)),
465 );
466 text.text
467 .lines
468 .iter()
469 .map(|line| line.layout.wrapped_text())
470 .collect::<Vec<_>>()
471 .join("\n")
472 }
473
474 pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self {
475 self.code_block_renderer = variant;
476 self
477 }
478
479 pub fn on_url_click(
480 mut self,
481 handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
482 ) -> Self {
483 self.on_url_click = Some(Box::new(handler));
484 self
485 }
486
487 fn paint_selection(
488 &self,
489 bounds: Bounds<Pixels>,
490 rendered_text: &RenderedText,
491 window: &mut Window,
492 cx: &mut App,
493 ) {
494 let selection = self.markdown.read(cx).selection;
495 let selection_start = rendered_text.position_for_source_index(selection.start);
496 let selection_end = rendered_text.position_for_source_index(selection.end);
497
498 if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
499 selection_start.zip(selection_end)
500 {
501 if start_position.y == end_position.y {
502 window.paint_quad(quad(
503 Bounds::from_corners(
504 start_position,
505 point(end_position.x, end_position.y + end_line_height),
506 ),
507 Pixels::ZERO,
508 self.style.selection_background_color,
509 Edges::default(),
510 Hsla::transparent_black(),
511 BorderStyle::default(),
512 ));
513 } else {
514 window.paint_quad(quad(
515 Bounds::from_corners(
516 start_position,
517 point(bounds.right(), start_position.y + start_line_height),
518 ),
519 Pixels::ZERO,
520 self.style.selection_background_color,
521 Edges::default(),
522 Hsla::transparent_black(),
523 BorderStyle::default(),
524 ));
525
526 if end_position.y > start_position.y + start_line_height {
527 window.paint_quad(quad(
528 Bounds::from_corners(
529 point(bounds.left(), start_position.y + start_line_height),
530 point(bounds.right(), end_position.y),
531 ),
532 Pixels::ZERO,
533 self.style.selection_background_color,
534 Edges::default(),
535 Hsla::transparent_black(),
536 BorderStyle::default(),
537 ));
538 }
539
540 window.paint_quad(quad(
541 Bounds::from_corners(
542 point(bounds.left(), end_position.y),
543 point(end_position.x, end_position.y + end_line_height),
544 ),
545 Pixels::ZERO,
546 self.style.selection_background_color,
547 Edges::default(),
548 Hsla::transparent_black(),
549 BorderStyle::default(),
550 ));
551 }
552 }
553 }
554
555 fn paint_mouse_listeners(
556 &mut self,
557 hitbox: &Hitbox,
558 rendered_text: &RenderedText,
559 window: &mut Window,
560 cx: &mut App,
561 ) {
562 let is_hovering_link = hitbox.is_hovered(window)
563 && !self.markdown.read(cx).selection.pending
564 && rendered_text
565 .link_for_position(window.mouse_position())
566 .is_some();
567
568 if is_hovering_link {
569 window.set_cursor_style(CursorStyle::PointingHand, Some(hitbox));
570 } else {
571 window.set_cursor_style(CursorStyle::IBeam, Some(hitbox));
572 }
573
574 let on_open_url = self.on_url_click.take();
575
576 self.on_mouse_event(window, cx, {
577 let rendered_text = rendered_text.clone();
578 let hitbox = hitbox.clone();
579 move |markdown, event: &MouseDownEvent, phase, window, cx| {
580 if hitbox.is_hovered(window) {
581 if phase.bubble() {
582 if let Some(link) = rendered_text.link_for_position(event.position) {
583 markdown.pressed_link = Some(link.clone());
584 } else {
585 let source_index =
586 match rendered_text.source_index_for_position(event.position) {
587 Ok(ix) | Err(ix) => ix,
588 };
589 let range = if event.click_count == 2 {
590 rendered_text.surrounding_word_range(source_index)
591 } else if event.click_count == 3 {
592 rendered_text.surrounding_line_range(source_index)
593 } else {
594 source_index..source_index
595 };
596 markdown.selection = Selection {
597 start: range.start,
598 end: range.end,
599 reversed: false,
600 pending: true,
601 };
602 window.focus(&markdown.focus_handle);
603 }
604
605 window.prevent_default();
606 cx.notify();
607 }
608 } else if phase.capture() {
609 markdown.selection = Selection::default();
610 markdown.pressed_link = None;
611 cx.notify();
612 }
613 }
614 });
615 self.on_mouse_event(window, cx, {
616 let rendered_text = rendered_text.clone();
617 let hitbox = hitbox.clone();
618 let was_hovering_link = is_hovering_link;
619 move |markdown, event: &MouseMoveEvent, phase, window, cx| {
620 if phase.capture() {
621 return;
622 }
623
624 if markdown.selection.pending {
625 let source_index = match rendered_text.source_index_for_position(event.position)
626 {
627 Ok(ix) | Err(ix) => ix,
628 };
629 markdown.selection.set_head(source_index);
630 markdown.autoscroll_request = Some(source_index);
631 cx.notify();
632 } else {
633 let is_hovering_link = hitbox.is_hovered(window)
634 && rendered_text.link_for_position(event.position).is_some();
635 if is_hovering_link != was_hovering_link {
636 cx.notify();
637 }
638 }
639 }
640 });
641 self.on_mouse_event(window, cx, {
642 let rendered_text = rendered_text.clone();
643 move |markdown, event: &MouseUpEvent, phase, window, cx| {
644 if phase.bubble() {
645 if let Some(pressed_link) = markdown.pressed_link.take() {
646 if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
647 if let Some(open_url) = on_open_url.as_ref() {
648 open_url(pressed_link.destination_url, window, cx);
649 } else {
650 cx.open_url(&pressed_link.destination_url);
651 }
652 }
653 }
654 } else if markdown.selection.pending {
655 markdown.selection.pending = false;
656 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
657 {
658 let text = rendered_text
659 .text_for_range(markdown.selection.start..markdown.selection.end);
660 cx.write_to_primary(ClipboardItem::new_string(text))
661 }
662 cx.notify();
663 }
664 }
665 });
666 }
667
668 fn autoscroll(
669 &self,
670 rendered_text: &RenderedText,
671 window: &mut Window,
672 cx: &mut App,
673 ) -> Option<()> {
674 let autoscroll_index = self
675 .markdown
676 .update(cx, |markdown, _| markdown.autoscroll_request.take())?;
677 let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
678
679 let text_style = self.style.base_text_style.clone();
680 let font_id = window.text_system().resolve_font(&text_style.font());
681 let font_size = text_style.font_size.to_pixels(window.rem_size());
682 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
683 window.request_autoscroll(Bounds::from_corners(
684 point(position.x - 3. * em_width, position.y - 3. * line_height),
685 point(position.x + 3. * em_width, position.y + 3. * line_height),
686 ));
687 Some(())
688 }
689
690 fn on_mouse_event<T: MouseEvent>(
691 &self,
692 window: &mut Window,
693 _cx: &mut App,
694 mut f: impl 'static
695 + FnMut(&mut Markdown, &T, DispatchPhase, &mut Window, &mut Context<Markdown>),
696 ) {
697 window.on_mouse_event({
698 let markdown = self.markdown.downgrade();
699 move |event, phase, window, cx| {
700 markdown
701 .update(cx, |markdown, cx| f(markdown, event, phase, window, cx))
702 .log_err();
703 }
704 });
705 }
706}
707
708impl Element for MarkdownElement {
709 type RequestLayoutState = RenderedMarkdown;
710 type PrepaintState = Hitbox;
711
712 fn id(&self) -> Option<ElementId> {
713 None
714 }
715
716 fn request_layout(
717 &mut self,
718 _id: Option<&GlobalElementId>,
719 window: &mut Window,
720 cx: &mut App,
721 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
722 let mut builder = MarkdownElementBuilder::new(
723 self.style.base_text_style.clone(),
724 self.style.syntax.clone(),
725 );
726 let markdown = self.markdown.read(cx);
727 let parsed_markdown = &markdown.parsed_markdown;
728 let images = &markdown.images_by_source_offset;
729 let markdown_end = if let Some(last) = parsed_markdown.events.last() {
730 last.0.end
731 } else {
732 0
733 };
734
735 let mut current_code_block_metadata = None;
736 let mut current_img_block_range: Option<Range<usize>> = None;
737 for (range, event) in parsed_markdown.events.iter() {
738 // Skip alt text for images that rendered
739 if let Some(current_img_block_range) = ¤t_img_block_range {
740 if current_img_block_range.end > range.end {
741 continue;
742 }
743 }
744
745 match event {
746 MarkdownEvent::Start(tag) => {
747 match tag {
748 MarkdownTag::Image { .. } => {
749 if let Some(image) = images.get(&range.start) {
750 current_img_block_range = Some(range.clone());
751 builder.modify_current_div(|el| {
752 el.items_center()
753 .flex()
754 .flex_row()
755 .child(img(image.clone()))
756 });
757 }
758 }
759 MarkdownTag::Paragraph => {
760 builder.push_div(
761 div().when(!self.style.height_is_multiple_of_line_height, |el| {
762 el.mb_2().line_height(rems(1.3))
763 }),
764 range,
765 markdown_end,
766 );
767 }
768 MarkdownTag::Heading { level, .. } => {
769 let mut heading = div().mb_2();
770
771 heading = apply_heading_style(
772 heading,
773 *level,
774 self.style.heading_level_styles.as_ref(),
775 );
776
777 heading.style().refine(&self.style.heading);
778
779 let text_style =
780 self.style.heading.text_style().clone().unwrap_or_default();
781
782 builder.push_text_style(text_style);
783 builder.push_div(heading, range, markdown_end);
784 }
785 MarkdownTag::BlockQuote => {
786 builder.push_text_style(self.style.block_quote.clone());
787 builder.push_div(
788 div()
789 .pl_4()
790 .mb_2()
791 .border_l_4()
792 .border_color(self.style.block_quote_border_color),
793 range,
794 markdown_end,
795 );
796 }
797 MarkdownTag::CodeBlock { kind, metadata } => {
798 let language = match kind {
799 CodeBlockKind::Fenced => None,
800 CodeBlockKind::FencedLang(language) => {
801 parsed_markdown.languages_by_name.get(language).cloned()
802 }
803 CodeBlockKind::FencedSrc(path_range) => parsed_markdown
804 .languages_by_path
805 .get(&path_range.path)
806 .cloned(),
807 _ => None,
808 };
809
810 current_code_block_metadata = Some(metadata.clone());
811
812 let is_indented = matches!(kind, CodeBlockKind::Indented);
813
814 match (&self.code_block_renderer, is_indented) {
815 (CodeBlockRenderer::Default { .. }, _) | (_, true) => {
816 // This is a parent container that we can position the copy button inside.
817 builder.push_div(
818 div().relative().w_full(),
819 range,
820 markdown_end,
821 );
822
823 let mut code_block = div()
824 .id(("code-block", range.start))
825 .rounded_lg()
826 .map(|mut code_block| {
827 if self.style.code_block_overflow_x_scroll {
828 code_block.style().restrict_scroll_to_axis =
829 Some(true);
830 code_block.flex().overflow_x_scroll()
831 } else {
832 code_block.w_full()
833 }
834 });
835
836 if let CodeBlockRenderer::Default { border: true, .. } =
837 &self.code_block_renderer
838 {
839 code_block = code_block
840 .rounded_md()
841 .border_1()
842 .border_color(cx.theme().colors().border_variant);
843 }
844
845 code_block.style().refine(&self.style.code_block);
846 if let Some(code_block_text_style) = &self.style.code_block.text
847 {
848 builder.push_text_style(code_block_text_style.to_owned());
849 }
850 builder.push_code_block(language);
851 builder.push_div(code_block, range, markdown_end);
852 }
853 (CodeBlockRenderer::Custom { render, .. }, _) => {
854 let parent_container = render(
855 kind,
856 &parsed_markdown,
857 range.clone(),
858 metadata.clone(),
859 window,
860 cx,
861 );
862
863 builder.push_div(parent_container, range, markdown_end);
864
865 let mut code_block = div()
866 .id(("code-block", range.start))
867 .rounded_b_lg()
868 .map(|mut code_block| {
869 if self.style.code_block_overflow_x_scroll {
870 code_block.style().restrict_scroll_to_axis =
871 Some(true);
872 code_block
873 .flex()
874 .overflow_x_scroll()
875 .overflow_y_hidden()
876 } else {
877 code_block.w_full().overflow_hidden()
878 }
879 });
880
881 code_block.style().refine(&self.style.code_block);
882
883 if let Some(code_block_text_style) = &self.style.code_block.text
884 {
885 builder.push_text_style(code_block_text_style.to_owned());
886 }
887
888 builder.push_code_block(language);
889 builder.push_div(code_block, range, markdown_end);
890 }
891 }
892 }
893 MarkdownTag::HtmlBlock => builder.push_div(div(), range, markdown_end),
894 MarkdownTag::List(bullet_index) => {
895 builder.push_list(*bullet_index);
896 builder.push_div(div().pl_4(), range, markdown_end);
897 }
898 MarkdownTag::Item => {
899 let bullet = if let Some(bullet_index) = builder.next_bullet_index() {
900 format!("{}.", bullet_index)
901 } else {
902 "•".to_string()
903 };
904 builder.push_div(
905 div()
906 .when(!self.style.height_is_multiple_of_line_height, |el| {
907 el.mb_1().gap_1().line_height(rems(1.3))
908 })
909 .h_flex()
910 .items_start()
911 .child(bullet),
912 range,
913 markdown_end,
914 );
915 // Without `w_0`, text doesn't wrap to the width of the container.
916 builder.push_div(div().flex_1().w_0(), range, markdown_end);
917 }
918 MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
919 font_style: Some(FontStyle::Italic),
920 ..Default::default()
921 }),
922 MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
923 font_weight: Some(FontWeight::BOLD),
924 ..Default::default()
925 }),
926 MarkdownTag::Strikethrough => {
927 builder.push_text_style(TextStyleRefinement {
928 strikethrough: Some(StrikethroughStyle {
929 thickness: px(1.),
930 color: None,
931 }),
932 ..Default::default()
933 })
934 }
935 MarkdownTag::Link { dest_url, .. } => {
936 if builder.code_block_stack.is_empty() {
937 builder.push_link(dest_url.clone(), range.clone());
938 let style = self
939 .style
940 .link_callback
941 .as_ref()
942 .and_then(|callback| callback(dest_url, cx))
943 .unwrap_or_else(|| self.style.link.clone());
944 builder.push_text_style(style)
945 }
946 }
947 MarkdownTag::MetadataBlock(_) => {}
948 MarkdownTag::Table(alignments) => {
949 builder.table_alignments = alignments.clone();
950 builder.push_div(
951 div()
952 .id(("table", range.start))
953 .flex()
954 .border_1()
955 .border_color(cx.theme().colors().border)
956 .rounded_sm()
957 .when(self.style.table_overflow_x_scroll, |mut table| {
958 table.style().restrict_scroll_to_axis = Some(true);
959 table.overflow_x_scroll()
960 }),
961 range,
962 markdown_end,
963 );
964 // This inner `v_flex` is so the table rows will stack vertically without disrupting the `overflow_x_scroll`.
965 builder.push_div(div().v_flex().flex_grow(), range, markdown_end);
966 }
967 MarkdownTag::TableHead => {
968 builder.push_div(
969 div()
970 .flex()
971 .justify_between()
972 .border_b_1()
973 .border_color(cx.theme().colors().border),
974 range,
975 markdown_end,
976 );
977 builder.push_text_style(TextStyleRefinement {
978 font_weight: Some(FontWeight::BOLD),
979 ..Default::default()
980 });
981 }
982 MarkdownTag::TableRow => {
983 builder.push_div(
984 div().h_flex().justify_between().px_1().py_0p5(),
985 range,
986 markdown_end,
987 );
988 }
989 MarkdownTag::TableCell => {
990 let column_count = builder.table_alignments.len();
991
992 builder.push_div(
993 div()
994 .flex()
995 .px_1()
996 .w(relative(1. / column_count as f32))
997 .truncate(),
998 range,
999 markdown_end,
1000 );
1001 }
1002 _ => log::debug!("unsupported markdown tag {:?}", tag),
1003 }
1004 }
1005 MarkdownEvent::End(tag) => match tag {
1006 MarkdownTagEnd::Image => {
1007 current_img_block_range.take();
1008 }
1009 MarkdownTagEnd::Paragraph => {
1010 builder.pop_div();
1011 }
1012 MarkdownTagEnd::Heading(_) => {
1013 builder.pop_div();
1014 builder.pop_text_style()
1015 }
1016 MarkdownTagEnd::BlockQuote(_kind) => {
1017 builder.pop_text_style();
1018 builder.pop_div()
1019 }
1020 MarkdownTagEnd::CodeBlock => {
1021 builder.trim_trailing_newline();
1022
1023 builder.pop_div();
1024 builder.pop_code_block();
1025 if self.style.code_block.text.is_some() {
1026 builder.pop_text_style();
1027 }
1028
1029 let metadata = current_code_block_metadata.take();
1030
1031 if let CodeBlockRenderer::Custom {
1032 transform: Some(transform),
1033 ..
1034 } = &self.code_block_renderer
1035 {
1036 builder.modify_current_div(|el| {
1037 transform(
1038 el,
1039 range.clone(),
1040 metadata.clone().unwrap_or_default(),
1041 window,
1042 cx,
1043 )
1044 });
1045 }
1046
1047 if let CodeBlockRenderer::Default {
1048 copy_button: true, ..
1049 } = &self.code_block_renderer
1050 {
1051 builder.modify_current_div(|el| {
1052 let content_range = parser::extract_code_block_content_range(
1053 parsed_markdown.source()[range.clone()].trim(),
1054 );
1055 let content_range = content_range.start + range.start
1056 ..content_range.end + range.start;
1057
1058 let code = parsed_markdown.source()[content_range].to_string();
1059 let codeblock = render_copy_code_block_button(
1060 range.end,
1061 code,
1062 self.markdown.clone(),
1063 cx,
1064 );
1065 el.child(div().absolute().top_1().right_1().w_5().child(codeblock))
1066 });
1067 }
1068
1069 // Pop the parent container.
1070 builder.pop_div();
1071 }
1072 MarkdownTagEnd::HtmlBlock => builder.pop_div(),
1073 MarkdownTagEnd::List(_) => {
1074 builder.pop_list();
1075 builder.pop_div();
1076 }
1077 MarkdownTagEnd::Item => {
1078 builder.pop_div();
1079 builder.pop_div();
1080 }
1081 MarkdownTagEnd::Emphasis => builder.pop_text_style(),
1082 MarkdownTagEnd::Strong => builder.pop_text_style(),
1083 MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
1084 MarkdownTagEnd::Link => {
1085 if builder.code_block_stack.is_empty() {
1086 builder.pop_text_style()
1087 }
1088 }
1089 MarkdownTagEnd::Table => {
1090 builder.pop_div();
1091 builder.pop_div();
1092 builder.table_alignments.clear();
1093 }
1094 MarkdownTagEnd::TableHead => {
1095 builder.pop_div();
1096 builder.pop_text_style();
1097 }
1098 MarkdownTagEnd::TableRow => {
1099 builder.pop_div();
1100 }
1101 MarkdownTagEnd::TableCell => {
1102 builder.pop_div();
1103 }
1104 _ => log::debug!("unsupported markdown tag end: {:?}", tag),
1105 },
1106 MarkdownEvent::Text => {
1107 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1108 }
1109 MarkdownEvent::SubstitutedText(text) => {
1110 builder.push_text(text, range.clone());
1111 }
1112 MarkdownEvent::Code => {
1113 builder.push_text_style(self.style.inline_code.clone());
1114 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1115 builder.pop_text_style();
1116 }
1117 MarkdownEvent::Html => {
1118 let html = &parsed_markdown.source[range.clone()];
1119 if html.starts_with("<!--") {
1120 builder.html_comment = true;
1121 }
1122 if html.trim_end().ends_with("-->") {
1123 builder.html_comment = false;
1124 continue;
1125 }
1126 if builder.html_comment {
1127 continue;
1128 }
1129 builder.push_text(html, range.clone());
1130 }
1131 MarkdownEvent::InlineHtml => {
1132 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1133 }
1134 MarkdownEvent::Rule => {
1135 builder.push_div(
1136 div()
1137 .border_b_1()
1138 .my_2()
1139 .border_color(self.style.rule_color),
1140 range,
1141 markdown_end,
1142 );
1143 builder.pop_div()
1144 }
1145 MarkdownEvent::SoftBreak => builder.push_text(" ", range.clone()),
1146 MarkdownEvent::HardBreak => builder.push_text("\n", range.clone()),
1147 _ => log::error!("unsupported markdown event {:?}", event),
1148 }
1149 }
1150 let mut rendered_markdown = builder.build();
1151 let child_layout_id = rendered_markdown.element.request_layout(window, cx);
1152 let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);
1153 (layout_id, rendered_markdown)
1154 }
1155
1156 fn prepaint(
1157 &mut self,
1158 _id: Option<&GlobalElementId>,
1159 bounds: Bounds<Pixels>,
1160 rendered_markdown: &mut Self::RequestLayoutState,
1161 window: &mut Window,
1162 cx: &mut App,
1163 ) -> Self::PrepaintState {
1164 let focus_handle = self.markdown.read(cx).focus_handle.clone();
1165 window.set_focus_handle(&focus_handle, cx);
1166
1167 let hitbox = window.insert_hitbox(bounds, false);
1168 rendered_markdown.element.prepaint(window, cx);
1169 self.autoscroll(&rendered_markdown.text, window, cx);
1170 hitbox
1171 }
1172
1173 fn paint(
1174 &mut self,
1175 _id: Option<&GlobalElementId>,
1176 bounds: Bounds<Pixels>,
1177 rendered_markdown: &mut Self::RequestLayoutState,
1178 hitbox: &mut Self::PrepaintState,
1179 window: &mut Window,
1180 cx: &mut App,
1181 ) {
1182 let mut context = KeyContext::default();
1183 context.add("Markdown");
1184 window.set_key_context(context);
1185 window.on_action(std::any::TypeId::of::<crate::Copy>(), {
1186 let entity = self.markdown.clone();
1187 let text = rendered_markdown.text.clone();
1188 move |_, phase, window, cx| {
1189 let text = text.clone();
1190 if phase == DispatchPhase::Bubble {
1191 entity.update(cx, move |this, cx| this.copy(&text, window, cx))
1192 }
1193 }
1194 });
1195 window.on_action(std::any::TypeId::of::<crate::CopyAsMarkdown>(), {
1196 let entity = self.markdown.clone();
1197 move |_, phase, window, cx| {
1198 if phase == DispatchPhase::Bubble {
1199 entity.update(cx, move |this, cx| this.copy_as_markdown(window, cx))
1200 }
1201 }
1202 });
1203
1204 self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
1205 rendered_markdown.element.paint(window, cx);
1206 self.paint_selection(bounds, &rendered_markdown.text, window, cx);
1207 }
1208}
1209
1210fn apply_heading_style(
1211 mut heading: Div,
1212 level: pulldown_cmark::HeadingLevel,
1213 custom_styles: Option<&HeadingLevelStyles>,
1214) -> Div {
1215 heading = match level {
1216 pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(),
1217 pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(),
1218 pulldown_cmark::HeadingLevel::H3 => heading.text_xl(),
1219 pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
1220 pulldown_cmark::HeadingLevel::H5 => heading.text_base(),
1221 pulldown_cmark::HeadingLevel::H6 => heading.text_sm(),
1222 };
1223
1224 if let Some(styles) = custom_styles {
1225 let style_opt = match level {
1226 pulldown_cmark::HeadingLevel::H1 => &styles.h1,
1227 pulldown_cmark::HeadingLevel::H2 => &styles.h2,
1228 pulldown_cmark::HeadingLevel::H3 => &styles.h3,
1229 pulldown_cmark::HeadingLevel::H4 => &styles.h4,
1230 pulldown_cmark::HeadingLevel::H5 => &styles.h5,
1231 pulldown_cmark::HeadingLevel::H6 => &styles.h6,
1232 };
1233
1234 if let Some(style) = style_opt {
1235 heading.style().text = Some(style.clone());
1236 }
1237 }
1238
1239 heading
1240}
1241
1242fn render_copy_code_block_button(
1243 id: usize,
1244 code: String,
1245 markdown: Entity<Markdown>,
1246 cx: &App,
1247) -> impl IntoElement {
1248 let id = ElementId::named_usize("copy-markdown-code", id);
1249 let was_copied = markdown.read(cx).copied_code_blocks.contains(&id);
1250 IconButton::new(
1251 id.clone(),
1252 if was_copied {
1253 IconName::Check
1254 } else {
1255 IconName::Copy
1256 },
1257 )
1258 .icon_color(Color::Muted)
1259 .shape(ui::IconButtonShape::Square)
1260 .tooltip(Tooltip::text("Copy Code"))
1261 .on_click({
1262 let id = id.clone();
1263 let markdown = markdown.clone();
1264 move |_event, _window, cx| {
1265 let id = id.clone();
1266 markdown.update(cx, |this, cx| {
1267 this.copied_code_blocks.insert(id.clone());
1268
1269 cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
1270
1271 cx.spawn(async move |this, cx| {
1272 cx.background_executor().timer(Duration::from_secs(2)).await;
1273
1274 cx.update(|cx| {
1275 this.update(cx, |this, cx| {
1276 this.copied_code_blocks.remove(&id);
1277 cx.notify();
1278 })
1279 })
1280 .ok();
1281 })
1282 .detach();
1283 });
1284 }
1285 })
1286}
1287
1288impl IntoElement for MarkdownElement {
1289 type Element = Self;
1290
1291 fn into_element(self) -> Self::Element {
1292 self
1293 }
1294}
1295
1296pub enum AnyDiv {
1297 Div(Div),
1298 Stateful(Stateful<Div>),
1299}
1300
1301impl AnyDiv {
1302 fn into_any_element(self) -> AnyElement {
1303 match self {
1304 Self::Div(div) => div.into_any_element(),
1305 Self::Stateful(div) => div.into_any_element(),
1306 }
1307 }
1308}
1309
1310impl From<Div> for AnyDiv {
1311 fn from(value: Div) -> Self {
1312 Self::Div(value)
1313 }
1314}
1315
1316impl From<Stateful<Div>> for AnyDiv {
1317 fn from(value: Stateful<Div>) -> Self {
1318 Self::Stateful(value)
1319 }
1320}
1321
1322impl Styled for AnyDiv {
1323 fn style(&mut self) -> &mut StyleRefinement {
1324 match self {
1325 Self::Div(div) => div.style(),
1326 Self::Stateful(div) => div.style(),
1327 }
1328 }
1329}
1330
1331impl ParentElement for AnyDiv {
1332 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1333 match self {
1334 Self::Div(div) => div.extend(elements),
1335 Self::Stateful(div) => div.extend(elements),
1336 }
1337 }
1338}
1339
1340struct MarkdownElementBuilder {
1341 div_stack: Vec<AnyDiv>,
1342 rendered_lines: Vec<RenderedLine>,
1343 pending_line: PendingLine,
1344 rendered_links: Vec<RenderedLink>,
1345 current_source_index: usize,
1346 html_comment: bool,
1347 base_text_style: TextStyle,
1348 text_style_stack: Vec<TextStyleRefinement>,
1349 code_block_stack: Vec<Option<Arc<Language>>>,
1350 list_stack: Vec<ListStackEntry>,
1351 table_alignments: Vec<Alignment>,
1352 syntax_theme: Arc<SyntaxTheme>,
1353}
1354
1355#[derive(Default)]
1356struct PendingLine {
1357 text: String,
1358 runs: Vec<TextRun>,
1359 source_mappings: Vec<SourceMapping>,
1360}
1361
1362struct ListStackEntry {
1363 bullet_index: Option<u64>,
1364}
1365
1366impl MarkdownElementBuilder {
1367 fn new(base_text_style: TextStyle, syntax_theme: Arc<SyntaxTheme>) -> Self {
1368 Self {
1369 div_stack: vec![div().debug_selector(|| "inner".into()).into()],
1370 rendered_lines: Vec::new(),
1371 pending_line: PendingLine::default(),
1372 rendered_links: Vec::new(),
1373 current_source_index: 0,
1374 html_comment: false,
1375 base_text_style,
1376 text_style_stack: Vec::new(),
1377 code_block_stack: Vec::new(),
1378 list_stack: Vec::new(),
1379 table_alignments: Vec::new(),
1380 syntax_theme,
1381 }
1382 }
1383
1384 fn push_text_style(&mut self, style: TextStyleRefinement) {
1385 self.text_style_stack.push(style);
1386 }
1387
1388 fn text_style(&self) -> TextStyle {
1389 let mut style = self.base_text_style.clone();
1390 for refinement in &self.text_style_stack {
1391 style.refine(refinement);
1392 }
1393 style
1394 }
1395
1396 fn pop_text_style(&mut self) {
1397 self.text_style_stack.pop();
1398 }
1399
1400 fn push_div(&mut self, div: impl Into<AnyDiv>, range: &Range<usize>, markdown_end: usize) {
1401 let mut div = div.into();
1402 self.flush_text();
1403
1404 if range.start == 0 {
1405 // Remove the top margin on the first element.
1406 div.style().refine(&StyleRefinement {
1407 margin: gpui::EdgesRefinement {
1408 top: Some(Length::Definite(px(0.).into())),
1409 left: None,
1410 right: None,
1411 bottom: None,
1412 },
1413 ..Default::default()
1414 });
1415 }
1416
1417 if range.end == markdown_end {
1418 div.style().refine(&StyleRefinement {
1419 margin: gpui::EdgesRefinement {
1420 top: None,
1421 left: None,
1422 right: None,
1423 bottom: Some(Length::Definite(rems(0.).into())),
1424 },
1425 ..Default::default()
1426 });
1427 }
1428
1429 self.div_stack.push(div);
1430 }
1431
1432 fn modify_current_div(&mut self, f: impl FnOnce(AnyDiv) -> AnyDiv) {
1433 self.flush_text();
1434 if let Some(div) = self.div_stack.pop() {
1435 self.div_stack.push(f(div));
1436 }
1437 }
1438
1439 fn pop_div(&mut self) {
1440 self.flush_text();
1441 let div = self.div_stack.pop().unwrap().into_any_element();
1442 self.div_stack.last_mut().unwrap().extend(iter::once(div));
1443 }
1444
1445 fn push_list(&mut self, bullet_index: Option<u64>) {
1446 self.list_stack.push(ListStackEntry { bullet_index });
1447 }
1448
1449 fn next_bullet_index(&mut self) -> Option<u64> {
1450 self.list_stack.last_mut().and_then(|entry| {
1451 let item_index = entry.bullet_index.as_mut()?;
1452 *item_index += 1;
1453 Some(*item_index - 1)
1454 })
1455 }
1456
1457 fn pop_list(&mut self) {
1458 self.list_stack.pop();
1459 }
1460
1461 fn push_code_block(&mut self, language: Option<Arc<Language>>) {
1462 self.code_block_stack.push(language);
1463 }
1464
1465 fn pop_code_block(&mut self) {
1466 self.code_block_stack.pop();
1467 }
1468
1469 fn push_link(&mut self, destination_url: SharedString, source_range: Range<usize>) {
1470 self.rendered_links.push(RenderedLink {
1471 source_range,
1472 destination_url,
1473 });
1474 }
1475
1476 fn push_text(&mut self, text: &str, source_range: Range<usize>) {
1477 self.pending_line.source_mappings.push(SourceMapping {
1478 rendered_index: self.pending_line.text.len(),
1479 source_index: source_range.start,
1480 });
1481 self.pending_line.text.push_str(text);
1482 self.current_source_index = source_range.end;
1483
1484 if let Some(Some(language)) = self.code_block_stack.last() {
1485 let mut offset = 0;
1486 for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
1487 if range.start > offset {
1488 self.pending_line
1489 .runs
1490 .push(self.text_style().to_run(range.start - offset));
1491 }
1492
1493 let mut run_style = self.text_style();
1494 if let Some(highlight) = highlight_id.style(&self.syntax_theme) {
1495 run_style = run_style.highlight(highlight);
1496 }
1497 self.pending_line.runs.push(run_style.to_run(range.len()));
1498 offset = range.end;
1499 }
1500
1501 if offset < text.len() {
1502 self.pending_line
1503 .runs
1504 .push(self.text_style().to_run(text.len() - offset));
1505 }
1506 } else {
1507 self.pending_line
1508 .runs
1509 .push(self.text_style().to_run(text.len()));
1510 }
1511 }
1512
1513 fn trim_trailing_newline(&mut self) {
1514 if self.pending_line.text.ends_with('\n') {
1515 self.pending_line
1516 .text
1517 .truncate(self.pending_line.text.len() - 1);
1518 self.pending_line.runs.last_mut().unwrap().len -= 1;
1519 self.current_source_index -= 1;
1520 }
1521 }
1522
1523 fn flush_text(&mut self) {
1524 let line = mem::take(&mut self.pending_line);
1525 if line.text.is_empty() {
1526 return;
1527 }
1528
1529 let text = StyledText::new(line.text).with_runs(line.runs);
1530 self.rendered_lines.push(RenderedLine {
1531 layout: text.layout().clone(),
1532 source_mappings: line.source_mappings,
1533 source_end: self.current_source_index,
1534 });
1535 self.div_stack.last_mut().unwrap().extend([text.into_any()]);
1536 }
1537
1538 fn build(mut self) -> RenderedMarkdown {
1539 debug_assert_eq!(self.div_stack.len(), 1);
1540 self.flush_text();
1541 RenderedMarkdown {
1542 element: self.div_stack.pop().unwrap().into_any_element(),
1543 text: RenderedText {
1544 lines: self.rendered_lines.into(),
1545 links: self.rendered_links.into(),
1546 },
1547 }
1548 }
1549}
1550
1551struct RenderedLine {
1552 layout: TextLayout,
1553 source_mappings: Vec<SourceMapping>,
1554 source_end: usize,
1555}
1556
1557impl RenderedLine {
1558 fn rendered_index_for_source_index(&self, source_index: usize) -> usize {
1559 if source_index >= self.source_end {
1560 return self.layout.len();
1561 }
1562
1563 let mapping = match self
1564 .source_mappings
1565 .binary_search_by_key(&source_index, |probe| probe.source_index)
1566 {
1567 Ok(ix) => &self.source_mappings[ix],
1568 Err(ix) => &self.source_mappings[ix - 1],
1569 };
1570 mapping.rendered_index + (source_index - mapping.source_index)
1571 }
1572
1573 fn source_index_for_rendered_index(&self, rendered_index: usize) -> usize {
1574 if rendered_index >= self.layout.len() {
1575 return self.source_end;
1576 }
1577
1578 let mapping = match self
1579 .source_mappings
1580 .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
1581 {
1582 Ok(ix) => &self.source_mappings[ix],
1583 Err(ix) => &self.source_mappings[ix - 1],
1584 };
1585 mapping.source_index + (rendered_index - mapping.rendered_index)
1586 }
1587
1588 fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
1589 let line_rendered_index;
1590 let out_of_bounds;
1591 match self.layout.index_for_position(position) {
1592 Ok(ix) => {
1593 line_rendered_index = ix;
1594 out_of_bounds = false;
1595 }
1596 Err(ix) => {
1597 line_rendered_index = ix;
1598 out_of_bounds = true;
1599 }
1600 };
1601 let source_index = self.source_index_for_rendered_index(line_rendered_index);
1602 if out_of_bounds {
1603 Err(source_index)
1604 } else {
1605 Ok(source_index)
1606 }
1607 }
1608}
1609
1610#[derive(Copy, Clone, Debug, Default)]
1611struct SourceMapping {
1612 rendered_index: usize,
1613 source_index: usize,
1614}
1615
1616pub struct RenderedMarkdown {
1617 element: AnyElement,
1618 text: RenderedText,
1619}
1620
1621#[derive(Clone)]
1622struct RenderedText {
1623 lines: Rc<[RenderedLine]>,
1624 links: Rc<[RenderedLink]>,
1625}
1626
1627#[derive(Clone, Eq, PartialEq)]
1628struct RenderedLink {
1629 source_range: Range<usize>,
1630 destination_url: SharedString,
1631}
1632
1633impl RenderedText {
1634 fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
1635 let mut lines = self.lines.iter().peekable();
1636
1637 while let Some(line) = lines.next() {
1638 let line_bounds = line.layout.bounds();
1639 if position.y > line_bounds.bottom() {
1640 if let Some(next_line) = lines.peek() {
1641 if position.y < next_line.layout.bounds().top() {
1642 return Err(line.source_end);
1643 }
1644 }
1645
1646 continue;
1647 }
1648
1649 return line.source_index_for_position(position);
1650 }
1651
1652 Err(self.lines.last().map_or(0, |line| line.source_end))
1653 }
1654
1655 fn position_for_source_index(&self, source_index: usize) -> Option<(Point<Pixels>, Pixels)> {
1656 for line in self.lines.iter() {
1657 let line_source_start = line.source_mappings.first().unwrap().source_index;
1658 if source_index < line_source_start {
1659 break;
1660 } else if source_index > line.source_end {
1661 continue;
1662 } else {
1663 let line_height = line.layout.line_height();
1664 let rendered_index_within_line = line.rendered_index_for_source_index(source_index);
1665 let position = line.layout.position_for_index(rendered_index_within_line)?;
1666 return Some((position, line_height));
1667 }
1668 }
1669 None
1670 }
1671
1672 fn surrounding_word_range(&self, source_index: usize) -> Range<usize> {
1673 for line in self.lines.iter() {
1674 if source_index > line.source_end {
1675 continue;
1676 }
1677
1678 let line_rendered_start = line.source_mappings.first().unwrap().rendered_index;
1679 let rendered_index_in_line =
1680 line.rendered_index_for_source_index(source_index) - line_rendered_start;
1681 let text = line.layout.text();
1682 let previous_space = if let Some(idx) = text[0..rendered_index_in_line].rfind(' ') {
1683 idx + ' '.len_utf8()
1684 } else {
1685 0
1686 };
1687 let next_space = if let Some(idx) = text[rendered_index_in_line..].find(' ') {
1688 rendered_index_in_line + idx
1689 } else {
1690 text.len()
1691 };
1692
1693 return line.source_index_for_rendered_index(line_rendered_start + previous_space)
1694 ..line.source_index_for_rendered_index(line_rendered_start + next_space);
1695 }
1696
1697 source_index..source_index
1698 }
1699
1700 fn surrounding_line_range(&self, source_index: usize) -> Range<usize> {
1701 for line in self.lines.iter() {
1702 if source_index > line.source_end {
1703 continue;
1704 }
1705 let line_source_start = line.source_mappings.first().unwrap().source_index;
1706 return line_source_start..line.source_end;
1707 }
1708
1709 source_index..source_index
1710 }
1711
1712 fn text_for_range(&self, range: Range<usize>) -> String {
1713 let mut ret = vec![];
1714
1715 for line in self.lines.iter() {
1716 if range.start > line.source_end {
1717 continue;
1718 }
1719 let line_source_start = line.source_mappings.first().unwrap().source_index;
1720 if range.end < line_source_start {
1721 break;
1722 }
1723
1724 let text = line.layout.text();
1725
1726 let start = if range.start < line_source_start {
1727 0
1728 } else {
1729 line.rendered_index_for_source_index(range.start)
1730 };
1731 let end = if range.end > line.source_end {
1732 line.rendered_index_for_source_index(line.source_end)
1733 } else {
1734 line.rendered_index_for_source_index(range.end)
1735 }
1736 .min(text.len());
1737
1738 ret.push(text[start..end].to_string());
1739 }
1740 ret.join("\n")
1741 }
1742
1743 fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {
1744 let source_index = self.source_index_for_position(position).ok()?;
1745 self.links
1746 .iter()
1747 .find(|link| link.source_range.contains(&source_index))
1748 }
1749}
1750
1751#[cfg(test)]
1752mod tests {
1753 use super::*;
1754 use gpui::{TestAppContext, size};
1755
1756 #[gpui::test]
1757 fn test_mappings(cx: &mut TestAppContext) {
1758 // Formatting.
1759 assert_mappings(
1760 &render_markdown("He*l*lo", cx),
1761 vec![vec![(0, 0), (1, 1), (2, 3), (3, 5), (4, 6), (5, 7)]],
1762 );
1763
1764 // Multiple lines.
1765 assert_mappings(
1766 &render_markdown("Hello\n\nWorld", cx),
1767 vec![
1768 vec![(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)],
1769 vec![(0, 7), (1, 8), (2, 9), (3, 10), (4, 11), (5, 12)],
1770 ],
1771 );
1772
1773 // Multi-byte characters.
1774 assert_mappings(
1775 &render_markdown("αβγ\n\nδεζ", cx),
1776 vec![
1777 vec![(0, 0), (2, 2), (4, 4), (6, 6)],
1778 vec![(0, 8), (2, 10), (4, 12), (6, 14)],
1779 ],
1780 );
1781
1782 // Smart quotes.
1783 assert_mappings(&render_markdown("\"", cx), vec![vec![(0, 0), (3, 1)]]);
1784 assert_mappings(
1785 &render_markdown("\"hey\"", cx),
1786 vec![vec![(0, 0), (3, 1), (4, 2), (5, 3), (6, 4), (9, 5)]],
1787 );
1788
1789 // HTML Comments are ignored
1790 assert_mappings(
1791 &render_markdown(
1792 "<!--\nrdoc-file=string.c\n- str.intern -> symbol\n- str.to_sym -> symbol\n-->\nReturns",
1793 cx,
1794 ),
1795 vec![vec![
1796 (0, 78),
1797 (1, 79),
1798 (2, 80),
1799 (3, 81),
1800 (4, 82),
1801 (5, 83),
1802 (6, 84),
1803 ]],
1804 );
1805 }
1806
1807 fn render_markdown(markdown: &str, cx: &mut TestAppContext) -> RenderedText {
1808 struct TestWindow;
1809
1810 impl Render for TestWindow {
1811 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1812 div()
1813 }
1814 }
1815
1816 let (_, cx) = cx.add_window_view(|_, _| TestWindow);
1817 let markdown = cx.new(|cx| Markdown::new(markdown.to_string().into(), None, None, cx));
1818 cx.run_until_parked();
1819 let (rendered, _) = cx.draw(
1820 Default::default(),
1821 size(px(600.0), px(600.0)),
1822 |_window, _cx| MarkdownElement::new(markdown, MarkdownStyle::default()),
1823 );
1824 rendered.text
1825 }
1826
1827 #[test]
1828 fn test_escape() {
1829 assert_eq!(Markdown::escape("hello `world`"), "hello \\`world\\`");
1830 assert_eq!(
1831 Markdown::escape("hello\n cool world"),
1832 "hello\n\ncool world"
1833 );
1834 }
1835
1836 #[track_caller]
1837 fn assert_mappings(rendered: &RenderedText, expected: Vec<Vec<(usize, usize)>>) {
1838 assert_eq!(rendered.lines.len(), expected.len(), "line count mismatch");
1839 for (line_ix, line_mappings) in expected.into_iter().enumerate() {
1840 let line = &rendered.lines[line_ix];
1841
1842 assert!(
1843 line.source_mappings.windows(2).all(|mappings| {
1844 mappings[0].source_index < mappings[1].source_index
1845 && mappings[0].rendered_index < mappings[1].rendered_index
1846 }),
1847 "line {} has duplicate mappings: {:?}",
1848 line_ix,
1849 line.source_mappings
1850 );
1851
1852 for (rendered_ix, source_ix) in line_mappings {
1853 assert_eq!(
1854 line.source_index_for_rendered_index(rendered_ix),
1855 source_ix,
1856 "line {}, rendered_ix {}",
1857 line_ix,
1858 rendered_ix
1859 );
1860
1861 assert_eq!(
1862 line.rendered_index_for_source_index(source_ix),
1863 rendered_ix,
1864 "line {}, source_ix {}",
1865 line_ix,
1866 source_ix
1867 );
1868 }
1869 }
1870 }
1871}