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.flush_text();
1052 builder.modify_current_div(|el| {
1053 let content_range = parser::extract_code_block_content_range(
1054 parsed_markdown.source()[range.clone()].trim(),
1055 );
1056 let content_range = content_range.start + range.start
1057 ..content_range.end + range.start;
1058
1059 let code = parsed_markdown.source()[content_range].to_string();
1060 let codeblock = render_copy_code_block_button(
1061 range.end,
1062 code,
1063 self.markdown.clone(),
1064 cx,
1065 );
1066 el.child(div().absolute().top_1().right_1().w_5().child(codeblock))
1067 });
1068 }
1069
1070 // Pop the parent container.
1071 builder.pop_div();
1072 }
1073 MarkdownTagEnd::HtmlBlock => builder.pop_div(),
1074 MarkdownTagEnd::List(_) => {
1075 builder.pop_list();
1076 builder.pop_div();
1077 }
1078 MarkdownTagEnd::Item => {
1079 builder.pop_div();
1080 builder.pop_div();
1081 }
1082 MarkdownTagEnd::Emphasis => builder.pop_text_style(),
1083 MarkdownTagEnd::Strong => builder.pop_text_style(),
1084 MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
1085 MarkdownTagEnd::Link => {
1086 if builder.code_block_stack.is_empty() {
1087 builder.pop_text_style()
1088 }
1089 }
1090 MarkdownTagEnd::Table => {
1091 builder.pop_div();
1092 builder.pop_div();
1093 builder.table_alignments.clear();
1094 }
1095 MarkdownTagEnd::TableHead => {
1096 builder.pop_div();
1097 builder.pop_text_style();
1098 }
1099 MarkdownTagEnd::TableRow => {
1100 builder.pop_div();
1101 }
1102 MarkdownTagEnd::TableCell => {
1103 builder.pop_div();
1104 }
1105 _ => log::debug!("unsupported markdown tag end: {:?}", tag),
1106 },
1107 MarkdownEvent::Text => {
1108 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1109 }
1110 MarkdownEvent::SubstitutedText(text) => {
1111 builder.push_text(text, range.clone());
1112 }
1113 MarkdownEvent::Code => {
1114 builder.push_text_style(self.style.inline_code.clone());
1115 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1116 builder.pop_text_style();
1117 }
1118 MarkdownEvent::Html => {
1119 let html = &parsed_markdown.source[range.clone()];
1120 if html.starts_with("<!--") {
1121 builder.html_comment = true;
1122 }
1123 if html.trim_end().ends_with("-->") {
1124 builder.html_comment = false;
1125 continue;
1126 }
1127 if builder.html_comment {
1128 continue;
1129 }
1130 builder.push_text(html, range.clone());
1131 }
1132 MarkdownEvent::InlineHtml => {
1133 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1134 }
1135 MarkdownEvent::Rule => {
1136 builder.push_div(
1137 div()
1138 .border_b_1()
1139 .my_2()
1140 .border_color(self.style.rule_color),
1141 range,
1142 markdown_end,
1143 );
1144 builder.pop_div()
1145 }
1146 MarkdownEvent::SoftBreak => builder.push_text(" ", range.clone()),
1147 MarkdownEvent::HardBreak => builder.push_text("\n", range.clone()),
1148 _ => log::error!("unsupported markdown event {:?}", event),
1149 }
1150 }
1151 let mut rendered_markdown = builder.build();
1152 let child_layout_id = rendered_markdown.element.request_layout(window, cx);
1153 let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);
1154 (layout_id, rendered_markdown)
1155 }
1156
1157 fn prepaint(
1158 &mut self,
1159 _id: Option<&GlobalElementId>,
1160 bounds: Bounds<Pixels>,
1161 rendered_markdown: &mut Self::RequestLayoutState,
1162 window: &mut Window,
1163 cx: &mut App,
1164 ) -> Self::PrepaintState {
1165 let focus_handle = self.markdown.read(cx).focus_handle.clone();
1166 window.set_focus_handle(&focus_handle, cx);
1167
1168 let hitbox = window.insert_hitbox(bounds, false);
1169 rendered_markdown.element.prepaint(window, cx);
1170 self.autoscroll(&rendered_markdown.text, window, cx);
1171 hitbox
1172 }
1173
1174 fn paint(
1175 &mut self,
1176 _id: Option<&GlobalElementId>,
1177 bounds: Bounds<Pixels>,
1178 rendered_markdown: &mut Self::RequestLayoutState,
1179 hitbox: &mut Self::PrepaintState,
1180 window: &mut Window,
1181 cx: &mut App,
1182 ) {
1183 let mut context = KeyContext::default();
1184 context.add("Markdown");
1185 window.set_key_context(context);
1186 window.on_action(std::any::TypeId::of::<crate::Copy>(), {
1187 let entity = self.markdown.clone();
1188 let text = rendered_markdown.text.clone();
1189 move |_, phase, window, cx| {
1190 let text = text.clone();
1191 if phase == DispatchPhase::Bubble {
1192 entity.update(cx, move |this, cx| this.copy(&text, window, cx))
1193 }
1194 }
1195 });
1196 window.on_action(std::any::TypeId::of::<crate::CopyAsMarkdown>(), {
1197 let entity = self.markdown.clone();
1198 move |_, phase, window, cx| {
1199 if phase == DispatchPhase::Bubble {
1200 entity.update(cx, move |this, cx| this.copy_as_markdown(window, cx))
1201 }
1202 }
1203 });
1204
1205 self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
1206 rendered_markdown.element.paint(window, cx);
1207 self.paint_selection(bounds, &rendered_markdown.text, window, cx);
1208 }
1209}
1210
1211fn apply_heading_style(
1212 mut heading: Div,
1213 level: pulldown_cmark::HeadingLevel,
1214 custom_styles: Option<&HeadingLevelStyles>,
1215) -> Div {
1216 heading = match level {
1217 pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(),
1218 pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(),
1219 pulldown_cmark::HeadingLevel::H3 => heading.text_xl(),
1220 pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
1221 pulldown_cmark::HeadingLevel::H5 => heading.text_base(),
1222 pulldown_cmark::HeadingLevel::H6 => heading.text_sm(),
1223 };
1224
1225 if let Some(styles) = custom_styles {
1226 let style_opt = match level {
1227 pulldown_cmark::HeadingLevel::H1 => &styles.h1,
1228 pulldown_cmark::HeadingLevel::H2 => &styles.h2,
1229 pulldown_cmark::HeadingLevel::H3 => &styles.h3,
1230 pulldown_cmark::HeadingLevel::H4 => &styles.h4,
1231 pulldown_cmark::HeadingLevel::H5 => &styles.h5,
1232 pulldown_cmark::HeadingLevel::H6 => &styles.h6,
1233 };
1234
1235 if let Some(style) = style_opt {
1236 heading.style().text = Some(style.clone());
1237 }
1238 }
1239
1240 heading
1241}
1242
1243fn render_copy_code_block_button(
1244 id: usize,
1245 code: String,
1246 markdown: Entity<Markdown>,
1247 cx: &App,
1248) -> impl IntoElement {
1249 let id = ElementId::named_usize("copy-markdown-code", id);
1250 let was_copied = markdown.read(cx).copied_code_blocks.contains(&id);
1251 IconButton::new(
1252 id.clone(),
1253 if was_copied {
1254 IconName::Check
1255 } else {
1256 IconName::Copy
1257 },
1258 )
1259 .icon_color(Color::Muted)
1260 .shape(ui::IconButtonShape::Square)
1261 .tooltip(Tooltip::text("Copy Code"))
1262 .on_click({
1263 let id = id.clone();
1264 let markdown = markdown.clone();
1265 move |_event, _window, cx| {
1266 let id = id.clone();
1267 markdown.update(cx, |this, cx| {
1268 this.copied_code_blocks.insert(id.clone());
1269
1270 cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
1271
1272 cx.spawn(async move |this, cx| {
1273 cx.background_executor().timer(Duration::from_secs(2)).await;
1274
1275 cx.update(|cx| {
1276 this.update(cx, |this, cx| {
1277 this.copied_code_blocks.remove(&id);
1278 cx.notify();
1279 })
1280 })
1281 .ok();
1282 })
1283 .detach();
1284 });
1285 }
1286 })
1287}
1288
1289impl IntoElement for MarkdownElement {
1290 type Element = Self;
1291
1292 fn into_element(self) -> Self::Element {
1293 self
1294 }
1295}
1296
1297pub enum AnyDiv {
1298 Div(Div),
1299 Stateful(Stateful<Div>),
1300}
1301
1302impl AnyDiv {
1303 fn into_any_element(self) -> AnyElement {
1304 match self {
1305 Self::Div(div) => div.into_any_element(),
1306 Self::Stateful(div) => div.into_any_element(),
1307 }
1308 }
1309}
1310
1311impl From<Div> for AnyDiv {
1312 fn from(value: Div) -> Self {
1313 Self::Div(value)
1314 }
1315}
1316
1317impl From<Stateful<Div>> for AnyDiv {
1318 fn from(value: Stateful<Div>) -> Self {
1319 Self::Stateful(value)
1320 }
1321}
1322
1323impl Styled for AnyDiv {
1324 fn style(&mut self) -> &mut StyleRefinement {
1325 match self {
1326 Self::Div(div) => div.style(),
1327 Self::Stateful(div) => div.style(),
1328 }
1329 }
1330}
1331
1332impl ParentElement for AnyDiv {
1333 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1334 match self {
1335 Self::Div(div) => div.extend(elements),
1336 Self::Stateful(div) => div.extend(elements),
1337 }
1338 }
1339}
1340
1341struct MarkdownElementBuilder {
1342 div_stack: Vec<AnyDiv>,
1343 rendered_lines: Vec<RenderedLine>,
1344 pending_line: PendingLine,
1345 rendered_links: Vec<RenderedLink>,
1346 current_source_index: usize,
1347 html_comment: bool,
1348 base_text_style: TextStyle,
1349 text_style_stack: Vec<TextStyleRefinement>,
1350 code_block_stack: Vec<Option<Arc<Language>>>,
1351 list_stack: Vec<ListStackEntry>,
1352 table_alignments: Vec<Alignment>,
1353 syntax_theme: Arc<SyntaxTheme>,
1354}
1355
1356#[derive(Default)]
1357struct PendingLine {
1358 text: String,
1359 runs: Vec<TextRun>,
1360 source_mappings: Vec<SourceMapping>,
1361}
1362
1363struct ListStackEntry {
1364 bullet_index: Option<u64>,
1365}
1366
1367impl MarkdownElementBuilder {
1368 fn new(base_text_style: TextStyle, syntax_theme: Arc<SyntaxTheme>) -> Self {
1369 Self {
1370 div_stack: vec![div().debug_selector(|| "inner".into()).into()],
1371 rendered_lines: Vec::new(),
1372 pending_line: PendingLine::default(),
1373 rendered_links: Vec::new(),
1374 current_source_index: 0,
1375 html_comment: false,
1376 base_text_style,
1377 text_style_stack: Vec::new(),
1378 code_block_stack: Vec::new(),
1379 list_stack: Vec::new(),
1380 table_alignments: Vec::new(),
1381 syntax_theme,
1382 }
1383 }
1384
1385 fn push_text_style(&mut self, style: TextStyleRefinement) {
1386 self.text_style_stack.push(style);
1387 }
1388
1389 fn text_style(&self) -> TextStyle {
1390 let mut style = self.base_text_style.clone();
1391 for refinement in &self.text_style_stack {
1392 style.refine(refinement);
1393 }
1394 style
1395 }
1396
1397 fn pop_text_style(&mut self) {
1398 self.text_style_stack.pop();
1399 }
1400
1401 fn push_div(&mut self, div: impl Into<AnyDiv>, range: &Range<usize>, markdown_end: usize) {
1402 let mut div = div.into();
1403 self.flush_text();
1404
1405 if range.start == 0 {
1406 // Remove the top margin on the first element.
1407 div.style().refine(&StyleRefinement {
1408 margin: gpui::EdgesRefinement {
1409 top: Some(Length::Definite(px(0.).into())),
1410 left: None,
1411 right: None,
1412 bottom: None,
1413 },
1414 ..Default::default()
1415 });
1416 }
1417
1418 if range.end == markdown_end {
1419 div.style().refine(&StyleRefinement {
1420 margin: gpui::EdgesRefinement {
1421 top: None,
1422 left: None,
1423 right: None,
1424 bottom: Some(Length::Definite(rems(0.).into())),
1425 },
1426 ..Default::default()
1427 });
1428 }
1429
1430 self.div_stack.push(div);
1431 }
1432
1433 fn modify_current_div(&mut self, f: impl FnOnce(AnyDiv) -> AnyDiv) {
1434 self.flush_text();
1435 if let Some(div) = self.div_stack.pop() {
1436 self.div_stack.push(f(div));
1437 }
1438 }
1439
1440 fn pop_div(&mut self) {
1441 self.flush_text();
1442 let div = self.div_stack.pop().unwrap().into_any_element();
1443 self.div_stack.last_mut().unwrap().extend(iter::once(div));
1444 }
1445
1446 fn push_list(&mut self, bullet_index: Option<u64>) {
1447 self.list_stack.push(ListStackEntry { bullet_index });
1448 }
1449
1450 fn next_bullet_index(&mut self) -> Option<u64> {
1451 self.list_stack.last_mut().and_then(|entry| {
1452 let item_index = entry.bullet_index.as_mut()?;
1453 *item_index += 1;
1454 Some(*item_index - 1)
1455 })
1456 }
1457
1458 fn pop_list(&mut self) {
1459 self.list_stack.pop();
1460 }
1461
1462 fn push_code_block(&mut self, language: Option<Arc<Language>>) {
1463 self.code_block_stack.push(language);
1464 }
1465
1466 fn pop_code_block(&mut self) {
1467 self.code_block_stack.pop();
1468 }
1469
1470 fn push_link(&mut self, destination_url: SharedString, source_range: Range<usize>) {
1471 self.rendered_links.push(RenderedLink {
1472 source_range,
1473 destination_url,
1474 });
1475 }
1476
1477 fn push_text(&mut self, text: &str, source_range: Range<usize>) {
1478 self.pending_line.source_mappings.push(SourceMapping {
1479 rendered_index: self.pending_line.text.len(),
1480 source_index: source_range.start,
1481 });
1482 self.pending_line.text.push_str(text);
1483 self.current_source_index = source_range.end;
1484
1485 if let Some(Some(language)) = self.code_block_stack.last() {
1486 let mut offset = 0;
1487 for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
1488 if range.start > offset {
1489 self.pending_line
1490 .runs
1491 .push(self.text_style().to_run(range.start - offset));
1492 }
1493
1494 let mut run_style = self.text_style();
1495 if let Some(highlight) = highlight_id.style(&self.syntax_theme) {
1496 run_style = run_style.highlight(highlight);
1497 }
1498 self.pending_line.runs.push(run_style.to_run(range.len()));
1499 offset = range.end;
1500 }
1501
1502 if offset < text.len() {
1503 self.pending_line
1504 .runs
1505 .push(self.text_style().to_run(text.len() - offset));
1506 }
1507 } else {
1508 self.pending_line
1509 .runs
1510 .push(self.text_style().to_run(text.len()));
1511 }
1512 }
1513
1514 fn trim_trailing_newline(&mut self) {
1515 if self.pending_line.text.ends_with('\n') {
1516 self.pending_line
1517 .text
1518 .truncate(self.pending_line.text.len() - 1);
1519 self.pending_line.runs.last_mut().unwrap().len -= 1;
1520 self.current_source_index -= 1;
1521 }
1522 }
1523
1524 fn flush_text(&mut self) {
1525 let line = mem::take(&mut self.pending_line);
1526 if line.text.is_empty() {
1527 return;
1528 }
1529
1530 let text = StyledText::new(line.text).with_runs(line.runs);
1531 self.rendered_lines.push(RenderedLine {
1532 layout: text.layout().clone(),
1533 source_mappings: line.source_mappings,
1534 source_end: self.current_source_index,
1535 });
1536 self.div_stack.last_mut().unwrap().extend([text.into_any()]);
1537 }
1538
1539 fn build(mut self) -> RenderedMarkdown {
1540 debug_assert_eq!(self.div_stack.len(), 1);
1541 self.flush_text();
1542 RenderedMarkdown {
1543 element: self.div_stack.pop().unwrap().into_any_element(),
1544 text: RenderedText {
1545 lines: self.rendered_lines.into(),
1546 links: self.rendered_links.into(),
1547 },
1548 }
1549 }
1550}
1551
1552struct RenderedLine {
1553 layout: TextLayout,
1554 source_mappings: Vec<SourceMapping>,
1555 source_end: usize,
1556}
1557
1558impl RenderedLine {
1559 fn rendered_index_for_source_index(&self, source_index: usize) -> usize {
1560 if source_index >= self.source_end {
1561 return self.layout.len();
1562 }
1563
1564 let mapping = match self
1565 .source_mappings
1566 .binary_search_by_key(&source_index, |probe| probe.source_index)
1567 {
1568 Ok(ix) => &self.source_mappings[ix],
1569 Err(ix) => &self.source_mappings[ix - 1],
1570 };
1571 mapping.rendered_index + (source_index - mapping.source_index)
1572 }
1573
1574 fn source_index_for_rendered_index(&self, rendered_index: usize) -> usize {
1575 if rendered_index >= self.layout.len() {
1576 return self.source_end;
1577 }
1578
1579 let mapping = match self
1580 .source_mappings
1581 .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
1582 {
1583 Ok(ix) => &self.source_mappings[ix],
1584 Err(ix) => &self.source_mappings[ix - 1],
1585 };
1586 mapping.source_index + (rendered_index - mapping.rendered_index)
1587 }
1588
1589 fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
1590 let line_rendered_index;
1591 let out_of_bounds;
1592 match self.layout.index_for_position(position) {
1593 Ok(ix) => {
1594 line_rendered_index = ix;
1595 out_of_bounds = false;
1596 }
1597 Err(ix) => {
1598 line_rendered_index = ix;
1599 out_of_bounds = true;
1600 }
1601 };
1602 let source_index = self.source_index_for_rendered_index(line_rendered_index);
1603 if out_of_bounds {
1604 Err(source_index)
1605 } else {
1606 Ok(source_index)
1607 }
1608 }
1609}
1610
1611#[derive(Copy, Clone, Debug, Default)]
1612struct SourceMapping {
1613 rendered_index: usize,
1614 source_index: usize,
1615}
1616
1617pub struct RenderedMarkdown {
1618 element: AnyElement,
1619 text: RenderedText,
1620}
1621
1622#[derive(Clone)]
1623struct RenderedText {
1624 lines: Rc<[RenderedLine]>,
1625 links: Rc<[RenderedLink]>,
1626}
1627
1628#[derive(Clone, Eq, PartialEq)]
1629struct RenderedLink {
1630 source_range: Range<usize>,
1631 destination_url: SharedString,
1632}
1633
1634impl RenderedText {
1635 fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
1636 let mut lines = self.lines.iter().peekable();
1637
1638 while let Some(line) = lines.next() {
1639 let line_bounds = line.layout.bounds();
1640 if position.y > line_bounds.bottom() {
1641 if let Some(next_line) = lines.peek() {
1642 if position.y < next_line.layout.bounds().top() {
1643 return Err(line.source_end);
1644 }
1645 }
1646
1647 continue;
1648 }
1649
1650 return line.source_index_for_position(position);
1651 }
1652
1653 Err(self.lines.last().map_or(0, |line| line.source_end))
1654 }
1655
1656 fn position_for_source_index(&self, source_index: usize) -> Option<(Point<Pixels>, Pixels)> {
1657 for line in self.lines.iter() {
1658 let line_source_start = line.source_mappings.first().unwrap().source_index;
1659 if source_index < line_source_start {
1660 break;
1661 } else if source_index > line.source_end {
1662 continue;
1663 } else {
1664 let line_height = line.layout.line_height();
1665 let rendered_index_within_line = line.rendered_index_for_source_index(source_index);
1666 let position = line.layout.position_for_index(rendered_index_within_line)?;
1667 return Some((position, line_height));
1668 }
1669 }
1670 None
1671 }
1672
1673 fn surrounding_word_range(&self, source_index: usize) -> Range<usize> {
1674 for line in self.lines.iter() {
1675 if source_index > line.source_end {
1676 continue;
1677 }
1678
1679 let line_rendered_start = line.source_mappings.first().unwrap().rendered_index;
1680 let rendered_index_in_line =
1681 line.rendered_index_for_source_index(source_index) - line_rendered_start;
1682 let text = line.layout.text();
1683 let previous_space = if let Some(idx) = text[0..rendered_index_in_line].rfind(' ') {
1684 idx + ' '.len_utf8()
1685 } else {
1686 0
1687 };
1688 let next_space = if let Some(idx) = text[rendered_index_in_line..].find(' ') {
1689 rendered_index_in_line + idx
1690 } else {
1691 text.len()
1692 };
1693
1694 return line.source_index_for_rendered_index(line_rendered_start + previous_space)
1695 ..line.source_index_for_rendered_index(line_rendered_start + next_space);
1696 }
1697
1698 source_index..source_index
1699 }
1700
1701 fn surrounding_line_range(&self, source_index: usize) -> Range<usize> {
1702 for line in self.lines.iter() {
1703 if source_index > line.source_end {
1704 continue;
1705 }
1706 let line_source_start = line.source_mappings.first().unwrap().source_index;
1707 return line_source_start..line.source_end;
1708 }
1709
1710 source_index..source_index
1711 }
1712
1713 fn text_for_range(&self, range: Range<usize>) -> String {
1714 let mut ret = vec![];
1715
1716 for line in self.lines.iter() {
1717 if range.start > line.source_end {
1718 continue;
1719 }
1720 let line_source_start = line.source_mappings.first().unwrap().source_index;
1721 if range.end < line_source_start {
1722 break;
1723 }
1724
1725 let text = line.layout.text();
1726
1727 let start = if range.start < line_source_start {
1728 0
1729 } else {
1730 line.rendered_index_for_source_index(range.start)
1731 };
1732 let end = if range.end > line.source_end {
1733 line.rendered_index_for_source_index(line.source_end)
1734 } else {
1735 line.rendered_index_for_source_index(range.end)
1736 }
1737 .min(text.len());
1738
1739 ret.push(text[start..end].to_string());
1740 }
1741 ret.join("\n")
1742 }
1743
1744 fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {
1745 let source_index = self.source_index_for_position(position).ok()?;
1746 self.links
1747 .iter()
1748 .find(|link| link.source_range.contains(&source_index))
1749 }
1750}
1751
1752#[cfg(test)]
1753mod tests {
1754 use super::*;
1755 use gpui::{TestAppContext, size};
1756
1757 #[gpui::test]
1758 fn test_mappings(cx: &mut TestAppContext) {
1759 // Formatting.
1760 assert_mappings(
1761 &render_markdown("He*l*lo", cx),
1762 vec![vec![(0, 0), (1, 1), (2, 3), (3, 5), (4, 6), (5, 7)]],
1763 );
1764
1765 // Multiple lines.
1766 assert_mappings(
1767 &render_markdown("Hello\n\nWorld", cx),
1768 vec![
1769 vec![(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)],
1770 vec![(0, 7), (1, 8), (2, 9), (3, 10), (4, 11), (5, 12)],
1771 ],
1772 );
1773
1774 // Multi-byte characters.
1775 assert_mappings(
1776 &render_markdown("αβγ\n\nδεζ", cx),
1777 vec![
1778 vec![(0, 0), (2, 2), (4, 4), (6, 6)],
1779 vec![(0, 8), (2, 10), (4, 12), (6, 14)],
1780 ],
1781 );
1782
1783 // Smart quotes.
1784 assert_mappings(&render_markdown("\"", cx), vec![vec![(0, 0), (3, 1)]]);
1785 assert_mappings(
1786 &render_markdown("\"hey\"", cx),
1787 vec![vec![(0, 0), (3, 1), (4, 2), (5, 3), (6, 4), (9, 5)]],
1788 );
1789
1790 // HTML Comments are ignored
1791 assert_mappings(
1792 &render_markdown(
1793 "<!--\nrdoc-file=string.c\n- str.intern -> symbol\n- str.to_sym -> symbol\n-->\nReturns",
1794 cx,
1795 ),
1796 vec![vec![
1797 (0, 78),
1798 (1, 79),
1799 (2, 80),
1800 (3, 81),
1801 (4, 82),
1802 (5, 83),
1803 (6, 84),
1804 ]],
1805 );
1806 }
1807
1808 fn render_markdown(markdown: &str, cx: &mut TestAppContext) -> RenderedText {
1809 struct TestWindow;
1810
1811 impl Render for TestWindow {
1812 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1813 div()
1814 }
1815 }
1816
1817 let (_, cx) = cx.add_window_view(|_, _| TestWindow);
1818 let markdown = cx.new(|cx| Markdown::new(markdown.to_string().into(), None, None, cx));
1819 cx.run_until_parked();
1820 let (rendered, _) = cx.draw(
1821 Default::default(),
1822 size(px(600.0), px(600.0)),
1823 |_window, _cx| MarkdownElement::new(markdown, MarkdownStyle::default()),
1824 );
1825 rendered.text
1826 }
1827
1828 #[test]
1829 fn test_escape() {
1830 assert_eq!(Markdown::escape("hello `world`"), "hello \\`world\\`");
1831 assert_eq!(
1832 Markdown::escape("hello\n cool world"),
1833 "hello\n\ncool world"
1834 );
1835 }
1836
1837 #[track_caller]
1838 fn assert_mappings(rendered: &RenderedText, expected: Vec<Vec<(usize, usize)>>) {
1839 assert_eq!(rendered.lines.len(), expected.len(), "line count mismatch");
1840 for (line_ix, line_mappings) in expected.into_iter().enumerate() {
1841 let line = &rendered.lines[line_ix];
1842
1843 assert!(
1844 line.source_mappings.windows(2).all(|mappings| {
1845 mappings[0].source_index < mappings[1].source_index
1846 && mappings[0].rendered_index < mappings[1].rendered_index
1847 }),
1848 "line {} has duplicate mappings: {:?}",
1849 line_ix,
1850 line.source_mappings
1851 );
1852
1853 for (rendered_ix, source_ix) in line_mappings {
1854 assert_eq!(
1855 line.source_index_for_rendered_index(rendered_ix),
1856 source_ix,
1857 "line {}, rendered_ix {}",
1858 line_ix,
1859 rendered_ix
1860 );
1861
1862 assert_eq!(
1863 line.rendered_index_for_source_index(source_ix),
1864 rendered_ix,
1865 "line {}, source_ix {}",
1866 line_ix,
1867 source_ix
1868 );
1869 }
1870 }
1871 }
1872}