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 let count = s
227 .bytes()
228 .filter(|c| *c == b'\n' || c.is_ascii_punctuation())
229 .count();
230 if count > 0 {
231 let mut output = String::with_capacity(s.len() + count);
232 let mut is_newline = false;
233 for c in s.chars() {
234 if is_newline && c == ' ' {
235 continue;
236 }
237 is_newline = c == '\n';
238 if c == '\n' {
239 output.push('\n')
240 } else if c.is_ascii_punctuation() {
241 output.push('\\')
242 }
243 output.push(c)
244 }
245 output.into()
246 } else {
247 s.into()
248 }
249 }
250
251 fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
252 if self.selection.end <= self.selection.start {
253 return;
254 }
255 let text = text.text_for_range(self.selection.start..self.selection.end);
256 cx.write_to_clipboard(ClipboardItem::new_string(text));
257 }
258
259 fn copy_as_markdown(&self, _: &mut Window, cx: &mut Context<Self>) {
260 if self.selection.end <= self.selection.start {
261 return;
262 }
263 let text = self.source[self.selection.start..self.selection.end].to_string();
264 cx.write_to_clipboard(ClipboardItem::new_string(text));
265 }
266
267 fn parse(&mut self, cx: &mut Context<Self>) {
268 if self.source.is_empty() {
269 return;
270 }
271
272 if self.pending_parse.is_some() {
273 self.should_reparse = true;
274 return;
275 }
276
277 let source = self.source.clone();
278 let should_parse_links_only = self.options.parse_links_only;
279 let language_registry = self.language_registry.clone();
280 let fallback = self.fallback_code_block_language.clone();
281 let parsed = cx.background_spawn(async move {
282 if should_parse_links_only {
283 return anyhow::Ok((
284 ParsedMarkdown {
285 events: Arc::from(parse_links_only(source.as_ref())),
286 source,
287 languages_by_name: TreeMap::default(),
288 languages_by_path: TreeMap::default(),
289 },
290 Default::default(),
291 ));
292 }
293 let (events, language_names, paths) = parse_markdown(&source);
294 let mut images_by_source_offset = HashMap::default();
295 let mut languages_by_name = TreeMap::default();
296 let mut languages_by_path = TreeMap::default();
297 if let Some(registry) = language_registry.as_ref() {
298 for name in language_names {
299 let language = if !name.is_empty() {
300 registry.language_for_name_or_extension(&name)
301 } else if let Some(fallback) = &fallback {
302 registry.language_for_name_or_extension(fallback)
303 } else {
304 continue;
305 };
306 if let Ok(language) = language.await {
307 languages_by_name.insert(name, language);
308 }
309 }
310
311 for path in paths {
312 if let Ok(language) = registry.language_for_file_path(&path).await {
313 languages_by_path.insert(path, language);
314 }
315 }
316 }
317
318 for (range, event) in &events {
319 if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event {
320 if let Some(data_url) = dest_url.strip_prefix("data:") {
321 let Some((mime_info, data)) = data_url.split_once(',') else {
322 continue;
323 };
324 let Some((mime_type, encoding)) = mime_info.split_once(';') else {
325 continue;
326 };
327 let Some(format) = ImageFormat::from_mime_type(mime_type) else {
328 continue;
329 };
330 let is_base64 = encoding == "base64";
331 if is_base64 {
332 if let Some(bytes) = base64::prelude::BASE64_STANDARD
333 .decode(data)
334 .log_with_level(Level::Debug)
335 {
336 let image = Arc::new(Image::from_bytes(format, bytes));
337 images_by_source_offset.insert(range.start, image);
338 }
339 }
340 }
341 }
342 }
343
344 anyhow::Ok((
345 ParsedMarkdown {
346 source,
347 events: Arc::from(events),
348 languages_by_name,
349 languages_by_path,
350 },
351 images_by_source_offset,
352 ))
353 });
354
355 self.should_reparse = false;
356 self.pending_parse = Some(cx.spawn(async move |this, cx| {
357 async move {
358 let (parsed, images_by_source_offset) = parsed.await?;
359
360 this.update(cx, |this, cx| {
361 this.parsed_markdown = parsed;
362 this.images_by_source_offset = images_by_source_offset;
363 this.pending_parse.take();
364 if this.should_reparse {
365 this.parse(cx);
366 }
367 cx.notify();
368 })
369 .ok();
370 anyhow::Ok(())
371 }
372 .log_err()
373 .await
374 }));
375 }
376}
377
378impl Focusable for Markdown {
379 fn focus_handle(&self, _cx: &App) -> FocusHandle {
380 self.focus_handle.clone()
381 }
382}
383
384#[derive(Copy, Clone, Default, Debug)]
385struct Selection {
386 start: usize,
387 end: usize,
388 reversed: bool,
389 pending: bool,
390}
391
392impl Selection {
393 fn set_head(&mut self, head: usize) {
394 if head < self.tail() {
395 if !self.reversed {
396 self.end = self.start;
397 self.reversed = true;
398 }
399 self.start = head;
400 } else {
401 if self.reversed {
402 self.start = self.end;
403 self.reversed = false;
404 }
405 self.end = head;
406 }
407 }
408
409 fn tail(&self) -> usize {
410 if self.reversed { self.end } else { self.start }
411 }
412}
413
414#[derive(Clone, Default)]
415pub struct ParsedMarkdown {
416 pub source: SharedString,
417 pub events: Arc<[(Range<usize>, MarkdownEvent)]>,
418 pub languages_by_name: TreeMap<SharedString, Arc<Language>>,
419 pub languages_by_path: TreeMap<Arc<Path>, Arc<Language>>,
420}
421
422impl ParsedMarkdown {
423 pub fn source(&self) -> &SharedString {
424 &self.source
425 }
426
427 pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
428 &self.events
429 }
430}
431
432pub struct MarkdownElement {
433 markdown: Entity<Markdown>,
434 style: MarkdownStyle,
435 code_block_renderer: CodeBlockRenderer,
436 on_url_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
437}
438
439impl MarkdownElement {
440 pub fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
441 Self {
442 markdown,
443 style,
444 code_block_renderer: CodeBlockRenderer::Default {
445 copy_button: true,
446 border: false,
447 },
448 on_url_click: None,
449 }
450 }
451
452 #[cfg(any(test, feature = "test-support"))]
453 pub fn rendered_text(
454 markdown: Entity<Markdown>,
455 cx: &mut gpui::VisualTestContext,
456 style: impl FnOnce(&Window, &App) -> MarkdownStyle,
457 ) -> String {
458 use gpui::size;
459
460 let (text, _) = cx.draw(
461 Default::default(),
462 size(px(600.0), px(600.0)),
463 |window, cx| Self::new(markdown, style(window, cx)),
464 );
465 text.text
466 .lines
467 .iter()
468 .map(|line| line.layout.wrapped_text())
469 .collect::<Vec<_>>()
470 .join("\n")
471 }
472
473 pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self {
474 self.code_block_renderer = variant;
475 self
476 }
477
478 pub fn on_url_click(
479 mut self,
480 handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
481 ) -> Self {
482 self.on_url_click = Some(Box::new(handler));
483 self
484 }
485
486 fn paint_selection(
487 &self,
488 bounds: Bounds<Pixels>,
489 rendered_text: &RenderedText,
490 window: &mut Window,
491 cx: &mut App,
492 ) {
493 let selection = self.markdown.read(cx).selection;
494 let selection_start = rendered_text.position_for_source_index(selection.start);
495 let selection_end = rendered_text.position_for_source_index(selection.end);
496
497 if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
498 selection_start.zip(selection_end)
499 {
500 if start_position.y == end_position.y {
501 window.paint_quad(quad(
502 Bounds::from_corners(
503 start_position,
504 point(end_position.x, end_position.y + end_line_height),
505 ),
506 Pixels::ZERO,
507 self.style.selection_background_color,
508 Edges::default(),
509 Hsla::transparent_black(),
510 BorderStyle::default(),
511 ));
512 } else {
513 window.paint_quad(quad(
514 Bounds::from_corners(
515 start_position,
516 point(bounds.right(), start_position.y + start_line_height),
517 ),
518 Pixels::ZERO,
519 self.style.selection_background_color,
520 Edges::default(),
521 Hsla::transparent_black(),
522 BorderStyle::default(),
523 ));
524
525 if end_position.y > start_position.y + start_line_height {
526 window.paint_quad(quad(
527 Bounds::from_corners(
528 point(bounds.left(), start_position.y + start_line_height),
529 point(bounds.right(), end_position.y),
530 ),
531 Pixels::ZERO,
532 self.style.selection_background_color,
533 Edges::default(),
534 Hsla::transparent_black(),
535 BorderStyle::default(),
536 ));
537 }
538
539 window.paint_quad(quad(
540 Bounds::from_corners(
541 point(bounds.left(), end_position.y),
542 point(end_position.x, end_position.y + end_line_height),
543 ),
544 Pixels::ZERO,
545 self.style.selection_background_color,
546 Edges::default(),
547 Hsla::transparent_black(),
548 BorderStyle::default(),
549 ));
550 }
551 }
552 }
553
554 fn paint_mouse_listeners(
555 &mut self,
556 hitbox: &Hitbox,
557 rendered_text: &RenderedText,
558 window: &mut Window,
559 cx: &mut App,
560 ) {
561 let is_hovering_link = hitbox.is_hovered(window)
562 && !self.markdown.read(cx).selection.pending
563 && rendered_text
564 .link_for_position(window.mouse_position())
565 .is_some();
566
567 if is_hovering_link {
568 window.set_cursor_style(CursorStyle::PointingHand, Some(hitbox));
569 } else {
570 window.set_cursor_style(CursorStyle::IBeam, Some(hitbox));
571 }
572
573 let on_open_url = self.on_url_click.take();
574
575 self.on_mouse_event(window, cx, {
576 let rendered_text = rendered_text.clone();
577 let hitbox = hitbox.clone();
578 move |markdown, event: &MouseDownEvent, phase, window, cx| {
579 if hitbox.is_hovered(window) {
580 if phase.bubble() {
581 if let Some(link) = rendered_text.link_for_position(event.position) {
582 markdown.pressed_link = Some(link.clone());
583 } else {
584 let source_index =
585 match rendered_text.source_index_for_position(event.position) {
586 Ok(ix) | Err(ix) => ix,
587 };
588 let range = if event.click_count == 2 {
589 rendered_text.surrounding_word_range(source_index)
590 } else if event.click_count == 3 {
591 rendered_text.surrounding_line_range(source_index)
592 } else {
593 source_index..source_index
594 };
595 markdown.selection = Selection {
596 start: range.start,
597 end: range.end,
598 reversed: false,
599 pending: true,
600 };
601 window.focus(&markdown.focus_handle);
602 }
603
604 window.prevent_default();
605 cx.notify();
606 }
607 } else if phase.capture() {
608 markdown.selection = Selection::default();
609 markdown.pressed_link = None;
610 cx.notify();
611 }
612 }
613 });
614 self.on_mouse_event(window, cx, {
615 let rendered_text = rendered_text.clone();
616 let hitbox = hitbox.clone();
617 let was_hovering_link = is_hovering_link;
618 move |markdown, event: &MouseMoveEvent, phase, window, cx| {
619 if phase.capture() {
620 return;
621 }
622
623 if markdown.selection.pending {
624 let source_index = match rendered_text.source_index_for_position(event.position)
625 {
626 Ok(ix) | Err(ix) => ix,
627 };
628 markdown.selection.set_head(source_index);
629 markdown.autoscroll_request = Some(source_index);
630 cx.notify();
631 } else {
632 let is_hovering_link = hitbox.is_hovered(window)
633 && rendered_text.link_for_position(event.position).is_some();
634 if is_hovering_link != was_hovering_link {
635 cx.notify();
636 }
637 }
638 }
639 });
640 self.on_mouse_event(window, cx, {
641 let rendered_text = rendered_text.clone();
642 move |markdown, event: &MouseUpEvent, phase, window, cx| {
643 if phase.bubble() {
644 if let Some(pressed_link) = markdown.pressed_link.take() {
645 if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
646 if let Some(open_url) = on_open_url.as_ref() {
647 open_url(pressed_link.destination_url, window, cx);
648 } else {
649 cx.open_url(&pressed_link.destination_url);
650 }
651 }
652 }
653 } else if markdown.selection.pending {
654 markdown.selection.pending = false;
655 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
656 {
657 let text = rendered_text
658 .text_for_range(markdown.selection.start..markdown.selection.end);
659 cx.write_to_primary(ClipboardItem::new_string(text))
660 }
661 cx.notify();
662 }
663 }
664 });
665 }
666
667 fn autoscroll(
668 &self,
669 rendered_text: &RenderedText,
670 window: &mut Window,
671 cx: &mut App,
672 ) -> Option<()> {
673 let autoscroll_index = self
674 .markdown
675 .update(cx, |markdown, _| markdown.autoscroll_request.take())?;
676 let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
677
678 let text_style = self.style.base_text_style.clone();
679 let font_id = window.text_system().resolve_font(&text_style.font());
680 let font_size = text_style.font_size.to_pixels(window.rem_size());
681 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
682 window.request_autoscroll(Bounds::from_corners(
683 point(position.x - 3. * em_width, position.y - 3. * line_height),
684 point(position.x + 3. * em_width, position.y + 3. * line_height),
685 ));
686 Some(())
687 }
688
689 fn on_mouse_event<T: MouseEvent>(
690 &self,
691 window: &mut Window,
692 _cx: &mut App,
693 mut f: impl 'static
694 + FnMut(&mut Markdown, &T, DispatchPhase, &mut Window, &mut Context<Markdown>),
695 ) {
696 window.on_mouse_event({
697 let markdown = self.markdown.downgrade();
698 move |event, phase, window, cx| {
699 markdown
700 .update(cx, |markdown, cx| f(markdown, event, phase, window, cx))
701 .log_err();
702 }
703 });
704 }
705}
706
707impl Element for MarkdownElement {
708 type RequestLayoutState = RenderedMarkdown;
709 type PrepaintState = Hitbox;
710
711 fn id(&self) -> Option<ElementId> {
712 None
713 }
714
715 fn request_layout(
716 &mut self,
717 _id: Option<&GlobalElementId>,
718 window: &mut Window,
719 cx: &mut App,
720 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
721 let mut builder = MarkdownElementBuilder::new(
722 self.style.base_text_style.clone(),
723 self.style.syntax.clone(),
724 );
725 let markdown = self.markdown.read(cx);
726 let parsed_markdown = &markdown.parsed_markdown;
727 let images = &markdown.images_by_source_offset;
728 let markdown_end = if let Some(last) = parsed_markdown.events.last() {
729 last.0.end
730 } else {
731 0
732 };
733
734 let mut current_code_block_metadata = None;
735 let mut current_img_block_range: Option<Range<usize>> = None;
736 for (range, event) in parsed_markdown.events.iter() {
737 // Skip alt text for images that rendered
738 if let Some(current_img_block_range) = ¤t_img_block_range {
739 if current_img_block_range.end > range.end {
740 continue;
741 }
742 }
743
744 match event {
745 MarkdownEvent::Start(tag) => {
746 match tag {
747 MarkdownTag::Image { .. } => {
748 if let Some(image) = images.get(&range.start) {
749 current_img_block_range = Some(range.clone());
750 builder.modify_current_div(|el| {
751 el.items_center()
752 .flex()
753 .flex_row()
754 .child(img(image.clone()))
755 });
756 }
757 }
758 MarkdownTag::Paragraph => {
759 builder.push_div(
760 div().when(!self.style.height_is_multiple_of_line_height, |el| {
761 el.mb_2().line_height(rems(1.3))
762 }),
763 range,
764 markdown_end,
765 );
766 }
767 MarkdownTag::Heading { level, .. } => {
768 let mut heading = div().mb_2();
769
770 heading = apply_heading_style(
771 heading,
772 *level,
773 self.style.heading_level_styles.as_ref(),
774 );
775
776 heading.style().refine(&self.style.heading);
777
778 let text_style =
779 self.style.heading.text_style().clone().unwrap_or_default();
780
781 builder.push_text_style(text_style);
782 builder.push_div(heading, range, markdown_end);
783 }
784 MarkdownTag::BlockQuote => {
785 builder.push_text_style(self.style.block_quote.clone());
786 builder.push_div(
787 div()
788 .pl_4()
789 .mb_2()
790 .border_l_4()
791 .border_color(self.style.block_quote_border_color),
792 range,
793 markdown_end,
794 );
795 }
796 MarkdownTag::CodeBlock { kind, metadata } => {
797 let language = match kind {
798 CodeBlockKind::Fenced => None,
799 CodeBlockKind::FencedLang(language) => {
800 parsed_markdown.languages_by_name.get(language).cloned()
801 }
802 CodeBlockKind::FencedSrc(path_range) => parsed_markdown
803 .languages_by_path
804 .get(&path_range.path)
805 .cloned(),
806 _ => None,
807 };
808
809 current_code_block_metadata = Some(metadata.clone());
810
811 let is_indented = matches!(kind, CodeBlockKind::Indented);
812
813 match (&self.code_block_renderer, is_indented) {
814 (CodeBlockRenderer::Default { .. }, _) | (_, true) => {
815 // This is a parent container that we can position the copy button inside.
816 builder.push_div(
817 div().relative().w_full(),
818 range,
819 markdown_end,
820 );
821
822 let mut code_block = div()
823 .id(("code-block", range.start))
824 .rounded_lg()
825 .map(|mut code_block| {
826 if self.style.code_block_overflow_x_scroll {
827 code_block.style().restrict_scroll_to_axis =
828 Some(true);
829 code_block.flex().overflow_x_scroll()
830 } else {
831 code_block.w_full()
832 }
833 });
834
835 if let CodeBlockRenderer::Default { border: true, .. } =
836 &self.code_block_renderer
837 {
838 code_block = code_block
839 .rounded_md()
840 .border_1()
841 .border_color(cx.theme().colors().border_variant);
842 }
843
844 code_block.style().refine(&self.style.code_block);
845 if let Some(code_block_text_style) = &self.style.code_block.text
846 {
847 builder.push_text_style(code_block_text_style.to_owned());
848 }
849 builder.push_code_block(language);
850 builder.push_div(code_block, range, markdown_end);
851 }
852 (CodeBlockRenderer::Custom { render, .. }, _) => {
853 let parent_container = render(
854 kind,
855 &parsed_markdown,
856 range.clone(),
857 metadata.clone(),
858 window,
859 cx,
860 );
861
862 builder.push_div(parent_container, range, markdown_end);
863
864 let mut code_block = div()
865 .id(("code-block", range.start))
866 .rounded_b_lg()
867 .map(|mut code_block| {
868 if self.style.code_block_overflow_x_scroll {
869 code_block.style().restrict_scroll_to_axis =
870 Some(true);
871 code_block
872 .flex()
873 .overflow_x_scroll()
874 .overflow_y_hidden()
875 } else {
876 code_block.w_full().overflow_hidden()
877 }
878 });
879
880 code_block.style().refine(&self.style.code_block);
881
882 if let Some(code_block_text_style) = &self.style.code_block.text
883 {
884 builder.push_text_style(code_block_text_style.to_owned());
885 }
886
887 builder.push_code_block(language);
888 builder.push_div(code_block, range, markdown_end);
889 }
890 }
891 }
892 MarkdownTag::HtmlBlock => builder.push_div(div(), range, markdown_end),
893 MarkdownTag::List(bullet_index) => {
894 builder.push_list(*bullet_index);
895 builder.push_div(div().pl_4(), range, markdown_end);
896 }
897 MarkdownTag::Item => {
898 let bullet = if let Some(bullet_index) = builder.next_bullet_index() {
899 format!("{}.", bullet_index)
900 } else {
901 "•".to_string()
902 };
903 builder.push_div(
904 div()
905 .when(!self.style.height_is_multiple_of_line_height, |el| {
906 el.mb_1().gap_1().line_height(rems(1.3))
907 })
908 .h_flex()
909 .items_start()
910 .child(bullet),
911 range,
912 markdown_end,
913 );
914 // Without `w_0`, text doesn't wrap to the width of the container.
915 builder.push_div(div().flex_1().w_0(), range, markdown_end);
916 }
917 MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
918 font_style: Some(FontStyle::Italic),
919 ..Default::default()
920 }),
921 MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
922 font_weight: Some(FontWeight::BOLD),
923 ..Default::default()
924 }),
925 MarkdownTag::Strikethrough => {
926 builder.push_text_style(TextStyleRefinement {
927 strikethrough: Some(StrikethroughStyle {
928 thickness: px(1.),
929 color: None,
930 }),
931 ..Default::default()
932 })
933 }
934 MarkdownTag::Link { dest_url, .. } => {
935 if builder.code_block_stack.is_empty() {
936 builder.push_link(dest_url.clone(), range.clone());
937 let style = self
938 .style
939 .link_callback
940 .as_ref()
941 .and_then(|callback| callback(dest_url, cx))
942 .unwrap_or_else(|| self.style.link.clone());
943 builder.push_text_style(style)
944 }
945 }
946 MarkdownTag::MetadataBlock(_) => {}
947 MarkdownTag::Table(alignments) => {
948 builder.table_alignments = alignments.clone();
949 builder.push_div(
950 div()
951 .id(("table", range.start))
952 .flex()
953 .border_1()
954 .border_color(cx.theme().colors().border)
955 .rounded_sm()
956 .when(self.style.table_overflow_x_scroll, |mut table| {
957 table.style().restrict_scroll_to_axis = Some(true);
958 table.overflow_x_scroll()
959 }),
960 range,
961 markdown_end,
962 );
963 // This inner `v_flex` is so the table rows will stack vertically without disrupting the `overflow_x_scroll`.
964 builder.push_div(div().v_flex().flex_grow(), range, markdown_end);
965 }
966 MarkdownTag::TableHead => {
967 builder.push_div(
968 div()
969 .flex()
970 .justify_between()
971 .border_b_1()
972 .border_color(cx.theme().colors().border),
973 range,
974 markdown_end,
975 );
976 builder.push_text_style(TextStyleRefinement {
977 font_weight: Some(FontWeight::BOLD),
978 ..Default::default()
979 });
980 }
981 MarkdownTag::TableRow => {
982 builder.push_div(
983 div().h_flex().justify_between().px_1().py_0p5(),
984 range,
985 markdown_end,
986 );
987 }
988 MarkdownTag::TableCell => {
989 let column_count = builder.table_alignments.len();
990
991 builder.push_div(
992 div()
993 .flex()
994 .px_1()
995 .w(relative(1. / column_count as f32))
996 .truncate(),
997 range,
998 markdown_end,
999 );
1000 }
1001 _ => log::debug!("unsupported markdown tag {:?}", tag),
1002 }
1003 }
1004 MarkdownEvent::End(tag) => match tag {
1005 MarkdownTagEnd::Image => {
1006 current_img_block_range.take();
1007 }
1008 MarkdownTagEnd::Paragraph => {
1009 builder.pop_div();
1010 }
1011 MarkdownTagEnd::Heading(_) => {
1012 builder.pop_div();
1013 builder.pop_text_style()
1014 }
1015 MarkdownTagEnd::BlockQuote(_kind) => {
1016 builder.pop_text_style();
1017 builder.pop_div()
1018 }
1019 MarkdownTagEnd::CodeBlock => {
1020 builder.trim_trailing_newline();
1021
1022 builder.pop_div();
1023 builder.pop_code_block();
1024 if self.style.code_block.text.is_some() {
1025 builder.pop_text_style();
1026 }
1027
1028 let metadata = current_code_block_metadata.take();
1029
1030 if let CodeBlockRenderer::Custom {
1031 transform: Some(transform),
1032 ..
1033 } = &self.code_block_renderer
1034 {
1035 builder.modify_current_div(|el| {
1036 transform(
1037 el,
1038 range.clone(),
1039 metadata.clone().unwrap_or_default(),
1040 window,
1041 cx,
1042 )
1043 });
1044 }
1045
1046 if let CodeBlockRenderer::Default {
1047 copy_button: true, ..
1048 } = &self.code_block_renderer
1049 {
1050 builder.flush_text();
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}