1use std::ops::Range;
26use std::rc::Rc;
27use std::sync::Arc;
28
29use gpui::{
30 AnyElement, App, BorderStyle, Bounds, CursorStyle, DefiniteLength, DispatchPhase, Div, Edges,
31 ElementId, Entity, FontStyle, FontWeight, GlobalElementId, Hitbox, HitboxBehavior, Hsla,
32 ImageSource, InteractiveElement, IntoElement, KeyContext, MouseButton, MouseDownEvent,
33 MouseEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Refineable as _, ScrollHandle,
34 SharedString, Stateful, StatefulInteractiveElement as _, StrikethroughStyle, StyleRefinement,
35 Styled, StyledImage as _, TextAlign, TextStyleRefinement, Window, div, img, point,
36 prelude::FluentBuilder as _, px, quad, rems,
37};
38use icons::IconName;
39use pulldown_cmark::{Alignment, BlockQuoteKind, HeadingLevel};
40use theme::ActiveTheme as _;
41use util::ResultExt as _;
42
43use crate::builder::{MarkdownElementBuilder, MetadataCellStyle, alignment_to_text_align, h_flex};
44use crate::controls::{Checkbox, CopyButton, icon_svg, simple_tooltip};
45use crate::entity::{
46 AutoscrollBehavior, CodeBlockRenderer, Markdown, SelectMode, Selection,
47 resolve_code_block_language,
48};
49use crate::parser::{
50 CodeBlockKind, MarkdownEvent, MarkdownTag, MarkdownTagEnd, ParsedMetadataBlock,
51};
52use crate::rendered::{RenderedMarkdown, RenderedText};
53use crate::style::{CopyButtonVisibility, HeadingLevelStyles, MarkdownStyle, WrapButtonVisibility};
54
55pub type CodeSpanLinkCallback = Arc<dyn Fn(&str, &App) -> Option<SharedString> + 'static>;
57type SourceClickCallback = Box<dyn Fn(usize, usize, &mut Window, &mut App) -> bool>;
58pub(crate) type CheckboxToggleCallback = Rc<dyn Fn(Range<usize>, bool, &mut Window, &mut App)>;
59
60pub struct MarkdownElement {
61 pub(crate) markdown: Entity<Markdown>,
62 pub(crate) style: MarkdownStyle,
63 code_block_renderer: CodeBlockRenderer,
64 on_url_click: Option<Rc<dyn Fn(SharedString, &mut Window, &mut App)>>,
65 code_span_link: Option<CodeSpanLinkCallback>,
66 on_source_click: Option<SourceClickCallback>,
67 on_checkbox_toggle: Option<CheckboxToggleCallback>,
68 pub(crate) image_resolver: Option<Box<dyn Fn(&str) -> Option<ImageSource>>>,
69 show_root_block_markers: bool,
70 autoscroll: AutoscrollBehavior,
71}
72
73impl MarkdownElement {
74 pub fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
75 Self {
76 markdown,
77 style,
78 code_block_renderer: CodeBlockRenderer::Default {
79 copy_button_visibility: CopyButtonVisibility::VisibleOnHover,
80 wrap_button_visibility: WrapButtonVisibility::Hidden,
81 border: false,
82 },
83 on_url_click: None,
84 code_span_link: None,
85 on_source_click: None,
86 on_checkbox_toggle: None,
87 image_resolver: None,
88 show_root_block_markers: false,
89 autoscroll: AutoscrollBehavior::Propagate,
90 }
91 }
92
93 #[cfg(any(test, feature = "test-support"))]
94 pub fn rendered_text(
95 markdown: Entity<Markdown>,
96 cx: &mut gpui::VisualTestContext,
97 style: impl FnOnce(&Window, &App) -> MarkdownStyle,
98 ) -> String {
99 use gpui::size;
100
101 let (text, _) = cx.draw(
102 Default::default(),
103 size(px(600.0), px(600.0)),
104 |window, cx| Self::new(markdown, style(window, cx)),
105 );
106 text.text
107 .lines
108 .iter()
109 .map(|line| line.layout.wrapped_text())
110 .collect::<Vec<_>>()
111 .join("\n")
112 }
113
114 pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self {
115 self.code_block_renderer = variant;
116 self
117 }
118
119 pub fn on_url_click(
120 mut self,
121 handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
122 ) -> Self {
123 self.on_url_click = Some(Rc::new(handler));
124 self
125 }
126
127 pub fn on_code_span_link(
128 mut self,
129 callback: impl Fn(&str, &App) -> Option<SharedString> + 'static,
130 ) -> Self {
131 self.code_span_link = Some(Arc::new(callback));
132 self
133 }
134
135 pub fn on_source_click(
136 mut self,
137 handler: impl Fn(usize, usize, &mut Window, &mut App) -> bool + 'static,
138 ) -> Self {
139 self.on_source_click = Some(Box::new(handler));
140 self
141 }
142
143 pub fn on_checkbox_toggle(
144 mut self,
145 handler: impl Fn(Range<usize>, bool, &mut Window, &mut App) + 'static,
146 ) -> Self {
147 self.on_checkbox_toggle = Some(Rc::new(handler));
148 self
149 }
150
151 pub fn image_resolver(
152 mut self,
153 resolver: impl Fn(&str) -> Option<ImageSource> + 'static,
154 ) -> Self {
155 self.image_resolver = Some(Box::new(resolver));
156 self
157 }
158
159 pub fn show_root_block_markers(mut self) -> Self {
160 self.show_root_block_markers = true;
161 self
162 }
163
164 pub fn scroll_handle(mut self, scroll_handle: ScrollHandle) -> Self {
165 self.autoscroll = AutoscrollBehavior::Controlled(scroll_handle);
166 self
167 }
168
169 fn push_markdown_code_span(
170 &self,
171 builder: &mut MarkdownElementBuilder,
172 text: &str,
173 range: Range<usize>,
174 cx: &App,
175 ) {
176 let link_url = if builder.code_block_stack.is_empty()
177 && builder.link_depth == 0
178 && !self.style.prevent_mouse_interaction
179 {
180 self.code_span_link
181 .as_ref()
182 .and_then(|callback| callback(text, cx))
183 } else {
184 None
185 };
186
187 if let Some(url) = link_url {
188 builder.push_link(url.clone(), range.clone());
189 let link_style = self
190 .style
191 .link_callback
192 .as_ref()
193 .and_then(|callback| callback(url.as_ref(), cx))
194 .unwrap_or_else(|| self.style.link.clone());
195 builder.push_text_style(self.style.inline_code.clone());
196 builder.push_text_style(link_style);
197 builder.push_text(text, range);
198 builder.pop_text_style();
199 builder.pop_text_style();
200 } else {
201 let mut code_style = self.style.inline_code.clone();
202 if builder.link_depth > 0 {
203 code_style.color = self.style.link.color.or(code_style.color);
204 }
205 builder.push_text_style(code_style);
206 builder.push_text(text, range);
207 builder.pop_text_style();
208 }
209 }
210
211 pub(crate) fn push_markdown_image(
212 &self,
213 builder: &mut MarkdownElementBuilder,
214 range: &Range<usize>,
215 source: ImageSource,
216 dest_url: SharedString,
217 alt_text: Option<SharedString>,
218 width: Option<DefiniteLength>,
219 height: Option<DefiniteLength>,
220 ) {
221 let enclosing_link_url = (builder.link_depth > 0)
222 .then(|| builder.rendered_links.last())
223 .flatten()
224 .map(|link| link.destination_url.clone());
225 let fallback_opens_image_url = enclosing_link_url.is_none();
226
227 let image_element = {
228 let wrapper = div().id(("markdown-image-link", range.start)).min_w_0();
229 let wrapper = if !self.style.prevent_mouse_interaction
230 && let Some(url) = enclosing_link_url
231 {
232 let click_url = url.clone();
233 let markdown = self.markdown.clone();
234 let url_click = self.on_url_click.clone();
235 wrapper
236 .cursor_pointer()
237 .on_click(move |_, window, cx| {
238 if let Some(ref on_url_click) = url_click {
239 on_url_click(click_url.clone(), window, cx);
240 } else {
241 cx.open_url(&click_url);
242 }
243 })
244 .capture_any_mouse_down(move |event, _window, cx| {
245 if event.button == MouseButton::Right {
246 markdown.update(cx, |md, _| {
247 md.capture_for_context_menu(Some(url.clone()), None)
248 });
249 }
250 })
251 } else {
252 wrapper
253 };
254 wrapper.child(
255 img(source)
256 .id(("markdown-image", range.start))
257 .min_w_0()
258 .max_w_full()
259 .rounded_md()
260 .mr_1()
261 .mb_1()
262 .when_some(height, |this, height| this.h(height))
263 .when_some(width, |this, width| this.w(width))
264 .with_fallback(move || {
265 image_fallback_element(
266 dest_url.clone(),
267 alt_text.clone(),
268 fallback_opens_image_url,
269 )
270 }),
271 )
272 };
273
274 builder.push_image_child(image_element);
275 }
276
277 pub(crate) fn push_markdown_paragraph(
278 &self,
279 builder: &mut MarkdownElementBuilder,
280 range: &Range<usize>,
281 markdown_end: usize,
282 text_align_override: Option<TextAlign>,
283 ) {
284 let align = text_align_override.unwrap_or(self.style.base_text_style.text_align);
285 let mut paragraph = div().when(!self.style.height_is_multiple_of_line_height, |el| {
286 el.mb_2().line_height(rems(1.3))
287 });
288
289 paragraph = match align {
290 TextAlign::Center => paragraph.text_center(),
291 TextAlign::Left => paragraph.text_left(),
292 TextAlign::Right => paragraph.text_right(),
293 };
294
295 builder.push_text_style(TextStyleRefinement {
296 text_align: Some(align),
297 ..Default::default()
298 });
299 builder.push_div(paragraph, range, markdown_end);
300 }
301
302 pub(crate) fn pop_markdown_paragraph(&self, builder: &mut MarkdownElementBuilder) {
303 builder.pop_div();
304 builder.pop_text_style();
305 }
306
307 pub(crate) fn push_markdown_heading(
308 &self,
309 builder: &mut MarkdownElementBuilder,
310 level: HeadingLevel,
311 range: &Range<usize>,
312 markdown_end: usize,
313 text_align_override: Option<TextAlign>,
314 ) {
315 let align = text_align_override.unwrap_or(self.style.base_text_style.text_align);
316 let mut heading = div().mt_4().mb_2();
317 heading = apply_heading_style(
318 heading,
319 level,
320 self.style.heading_level_styles.as_ref(),
321 self.style.heading_border_color,
322 );
323
324 heading = match align {
325 TextAlign::Center => heading.text_center(),
326 TextAlign::Left => heading.text_left(),
327 TextAlign::Right => heading.text_right(),
328 };
329
330 let heading_style = self.style.heading.clone();
331 let heading_text_style = heading_style.text.clone();
332 heading.style().refine(&heading_style);
333
334 builder.push_text_style(TextStyleRefinement {
335 text_align: Some(align),
336 ..heading_text_style
337 });
338 builder.push_div(heading, range, markdown_end);
339 }
340
341 pub(crate) fn pop_markdown_heading(&self, builder: &mut MarkdownElementBuilder) {
342 builder.pop_div();
343 builder.pop_text_style();
344 }
345
346 pub(crate) fn push_markdown_block_quote(
347 &self,
348 builder: &mut MarkdownElementBuilder,
349 kind: Option<BlockQuoteKind>,
350 range: &Range<usize>,
351 markdown_end: usize,
352 ) {
353 let border_color = self
354 .style
355 .block_quote_kind_colors
356 .for_kind(kind, self.style.block_quote_border_color);
357
358 let header = kind.map(|kind| {
359 let (icon_name, label) = match kind {
360 BlockQuoteKind::Note => (IconName::Info, "Note"),
361 BlockQuoteKind::Tip => (IconName::Sparkle, "Tip"),
362 BlockQuoteKind::Important => (IconName::Chat, "Important"),
363 BlockQuoteKind::Warning => (IconName::Warning, "Warning"),
364 BlockQuoteKind::Caution => (IconName::Stop, "Caution"),
365 };
366 h_flex()
367 .gap_1()
368 .mb_1()
369 .child(icon_svg(icon_name, px(14.), border_color))
370 .child(
371 div()
372 .text_color(border_color)
373 .font_weight(FontWeight::BOLD)
374 .child(label),
375 )
376 .into_any_element()
377 });
378
379 let block_div = div().pl_4().mb_2().border_l_4().border_color(border_color);
380 let block_div = match header {
381 Some(header) => block_div.child(header),
382 None => block_div,
383 };
384
385 builder.push_text_style(self.style.block_quote.clone());
386 builder.push_div(block_div, range, markdown_end);
387 }
388
389 pub(crate) fn pop_markdown_block_quote(&self, builder: &mut MarkdownElementBuilder) {
390 builder.pop_div();
391 builder.pop_text_style();
392 }
393
394 fn push_metadata_block(
395 &self,
396 builder: &mut MarkdownElementBuilder,
397 source: &str,
398 metadata_block: &ParsedMetadataBlock,
399 markdown_end: usize,
400 cx: &App,
401 ) {
402 let content_range = &metadata_block.content_range;
403 if let Some(rows) = metadata_block.rows.as_deref() {
404 builder.push_div(
405 div()
406 .grid()
407 .grid_cols(2)
408 .w_full()
409 .mb_2()
410 .border_1()
411 .border_color(cx.theme().colors().border)
412 .rounded_sm()
413 .overflow_hidden(),
414 content_range,
415 markdown_end,
416 );
417
418 for (row_index, row) in rows.iter().enumerate() {
419 self.push_metadata_cell(
420 builder,
421 source,
422 row.key.clone(),
423 content_range,
424 markdown_end,
425 MetadataCellStyle {
426 row_index,
427 is_key: true,
428 },
429 cx,
430 );
431 self.push_metadata_cell(
432 builder,
433 source,
434 row.value.clone(),
435 content_range,
436 markdown_end,
437 MetadataCellStyle {
438 row_index,
439 is_key: false,
440 },
441 cx,
442 );
443 }
444
445 builder.pop_div();
446 } else {
447 let mut metadata_block = div().w_full().rounded_md();
448 metadata_block.style().refine(&self.style.code_block);
449 builder.push_text_style(self.style.code_block.text.to_owned());
450 builder.push_code_block(None);
451 builder.push_div(metadata_block, content_range, markdown_end);
452 builder.push_text(&source[content_range.clone()], content_range.clone());
453 builder.trim_trailing_newline();
454 builder.pop_div();
455 builder.pop_code_block();
456 builder.pop_text_style();
457 }
458 }
459
460 fn push_metadata_cell(
461 &self,
462 builder: &mut MarkdownElementBuilder,
463 source: &str,
464 text_range: Range<usize>,
465 block_range: &Range<usize>,
466 markdown_end: usize,
467 cell_style: MetadataCellStyle,
468 cx: &App,
469 ) {
470 builder.push_div(
471 div()
472 .flex()
473 .flex_col()
474 .min_w_0()
475 .px_2()
476 .py_1()
477 .border_color(cx.theme().colors().border)
478 .when(cell_style.row_index > 0, |this| this.border_t_1())
479 .when(!cell_style.is_key, |this| this.border_l_1())
480 .when(cell_style.is_key, |this| {
481 this.bg(cx.theme().colors().panel_background)
482 }),
483 block_range,
484 markdown_end,
485 );
486
487 let text_style = if cell_style.is_key {
488 TextStyleRefinement {
489 color: Some(cx.theme().colors().text_muted),
490 font_weight: Some(FontWeight::SEMIBOLD),
491 ..Default::default()
492 }
493 } else {
494 TextStyleRefinement::default()
495 };
496 builder.push_text_style(text_style);
497 builder.push_text(&source[text_range.clone()], text_range);
498 builder.pop_text_style();
499 builder.pop_div();
500 }
501
502 pub(crate) fn push_markdown_list_item(
503 &self,
504 builder: &mut MarkdownElementBuilder,
505 bullet: AnyElement,
506 range: &Range<usize>,
507 markdown_end: usize,
508 ) {
509 builder.push_div(
510 div()
511 .when(!self.style.height_is_multiple_of_line_height, |el| {
512 el.mb_1().gap_1().line_height(rems(1.3))
513 })
514 .flex()
515 .flex_row()
516 .items_start()
517 .child(bullet),
518 range,
519 markdown_end,
520 );
521 builder.push_div(div().flex_1().w_0(), range, markdown_end);
523 }
524
525 pub(crate) fn pop_markdown_list_item(&self, builder: &mut MarkdownElementBuilder) {
526 builder.pop_div();
527 builder.pop_div();
528 }
529
530 fn paint_highlight_range(
531 start: usize,
532 end: usize,
533 color: Hsla,
534 rendered_text: &RenderedText,
535 window: &mut Window,
536 ) {
537 for bounds in rendered_text.bounds_for_source_range(start..end) {
538 window.paint_quad(quad(
539 bounds,
540 Pixels::ZERO,
541 color,
542 Edges::default(),
543 Hsla::transparent_black(),
544 BorderStyle::default(),
545 ));
546 }
547 }
548
549 fn paint_selection(&self, rendered_text: &RenderedText, window: &mut Window, cx: &mut App) {
550 let selection = self.markdown.read(cx).selection.clone();
551 Self::paint_highlight_range(
552 selection.start,
553 selection.end,
554 self.style.selection_background_color,
555 rendered_text,
556 window,
557 );
558 }
559
560 fn paint_search_highlights(
561 &self,
562 rendered_text: &RenderedText,
563 window: &mut Window,
564 cx: &mut App,
565 ) {
566 let markdown = self.markdown.read(cx);
567 let active_index = markdown.active_search_highlight;
568 let colors = cx.theme().colors();
569
570 let highlight_bounds = rendered_text.bounds_for_sorted_source_ranges(
571 markdown
572 .search_highlights
573 .iter()
574 .enumerate()
575 .map(|(ix, range)| (ix, range.clone())),
576 );
577 for (highlight_ix, bounds) in highlight_bounds {
578 let color = if Some(highlight_ix) == active_index {
579 colors.search_active_match_background
580 } else {
581 colors.search_match_background
582 };
583 window.paint_quad(quad(
584 bounds,
585 Pixels::ZERO,
586 color,
587 Edges::default(),
588 Hsla::transparent_black(),
589 BorderStyle::default(),
590 ));
591 }
592 }
593
594 fn paint_mouse_listeners(
595 &mut self,
596 hitbox: &Hitbox,
597 rendered_text: &RenderedText,
598 window: &mut Window,
599 cx: &mut App,
600 ) {
601 if self.style.prevent_mouse_interaction {
602 return;
603 }
604
605 let is_hovering_clickable = hitbox.is_hovered(window)
606 && !self.markdown.read(cx).selection.pending
607 && rendered_text
608 .source_index_for_position(window.mouse_position())
609 .ok()
610 .is_some_and(|source_index| {
611 rendered_text.link_for_source_index(source_index).is_some()
612 || rendered_text
613 .footnote_ref_for_source_index(source_index)
614 .is_some()
615 });
616
617 if is_hovering_clickable {
618 window.set_cursor_style(CursorStyle::PointingHand, hitbox);
619 } else {
620 window.set_cursor_style(CursorStyle::IBeam, hitbox);
621 }
622
623 let on_open_url = self.on_url_click.take();
624 let on_source_click = self.on_source_click.take();
625
626 self.on_mouse_event(window, cx, {
627 let hitbox = hitbox.clone();
628 let rendered_text = rendered_text.clone();
629 move |markdown, event: &MouseDownEvent, phase, window, _cx| {
630 if phase.capture()
631 && event.button == MouseButton::Right
632 && hitbox.is_hovered(window)
633 {
634 let link = rendered_text
635 .source_index_for_position(event.position)
636 .ok()
637 .and_then(|ix| rendered_text.link_for_source_index(ix))
638 .map(|link| link.destination_url.clone());
639 markdown.capture_for_context_menu(link, Some(&rendered_text));
640 }
641 }
642 });
643
644 self.on_mouse_event(window, cx, {
645 let rendered_text = rendered_text.clone();
646 let hitbox = hitbox.clone();
647 move |markdown, event: &MouseDownEvent, phase, window, cx| {
648 if hitbox.is_hovered(window) {
649 if phase.bubble() && event.button != MouseButton::Right {
650 let position_result =
651 rendered_text.source_index_for_position(event.position);
652
653 if let Ok(source_index) = position_result {
654 if let Some(footnote_ref) =
655 rendered_text.footnote_ref_for_source_index(source_index)
656 {
657 markdown.pressed_footnote_ref = Some(footnote_ref.clone());
658 } else if let Some(link) =
659 rendered_text.link_for_source_index(source_index)
660 {
661 markdown.pressed_link = Some(link.clone());
662 }
663 }
664
665 if markdown.pressed_footnote_ref.is_none()
666 && markdown.pressed_link.is_none()
667 {
668 let source_index = match position_result {
669 Ok(ix) | Err(ix) => ix,
670 };
671 if let Some(handler) = on_source_click.as_ref() {
672 let blocked = handler(source_index, event.click_count, window, cx);
673 if blocked {
674 markdown.selection = Selection::default();
675 markdown.pressed_link = None;
676 window.prevent_default();
677 cx.notify();
678 return;
679 }
680 }
681 let (range, mode, reversed) = match event.click_count {
682 1 if event.modifiers.shift => {
683 let tail = markdown.selection.tail();
684 let reversed = source_index < tail;
685 let range = if reversed {
686 source_index..tail
687 } else {
688 tail..source_index
689 };
690 (range, SelectMode::Character, reversed)
691 }
692 1 => {
693 let range = source_index..source_index;
694 (range, SelectMode::Character, false)
695 }
696 2 => {
697 let range = rendered_text.surrounding_word_range(source_index);
698 (range.clone(), SelectMode::Word(range), false)
699 }
700 3 => {
701 let range = rendered_text.surrounding_line_range(source_index);
702 (range.clone(), SelectMode::Line(range), false)
703 }
704 _ => {
705 let range = 0..rendered_text
706 .lines
707 .last()
708 .map(|line| line.source_end)
709 .unwrap_or(0);
710 (range, SelectMode::All, false)
711 }
712 };
713 markdown.selection = Selection {
714 start: range.start,
715 end: range.end,
716 reversed,
717 pending: true,
718 mode,
719 };
720 window.focus(&markdown.focus_handle, cx);
721 }
722
723 window.prevent_default();
724 cx.notify();
725 }
726 } else if phase.capture() && event.button == MouseButton::Left {
727 markdown.selection = Selection::default();
728 markdown.pressed_link = None;
729 cx.notify();
730 }
731 }
732 });
733 self.on_mouse_event(window, cx, {
734 let rendered_text = rendered_text.clone();
735 let hitbox = hitbox.clone();
736 let was_hovering_clickable = is_hovering_clickable;
737 move |markdown, event: &MouseMoveEvent, phase, window, cx| {
738 if phase.capture() {
739 return;
740 }
741
742 if markdown.selection.pending {
743 let source_index = match rendered_text.source_index_for_position(event.position)
744 {
745 Ok(ix) | Err(ix) => ix,
746 };
747 markdown.selection.set_head(source_index, &rendered_text);
748 markdown.autoscroll_code_block(source_index, event.position);
749 markdown.autoscroll_request = Some(source_index);
750 cx.notify();
751 } else {
752 let is_hovering_clickable = hitbox.is_hovered(window)
753 && rendered_text
754 .source_index_for_position(event.position)
755 .ok()
756 .is_some_and(|source_index| {
757 rendered_text.link_for_source_index(source_index).is_some()
758 || rendered_text
759 .footnote_ref_for_source_index(source_index)
760 .is_some()
761 });
762 if is_hovering_clickable != was_hovering_clickable {
763 cx.notify();
764 }
765 }
766 }
767 });
768 self.on_mouse_event(window, cx, {
769 let rendered_text = rendered_text.clone();
770 move |markdown, event: &MouseUpEvent, phase, window, cx| {
771 if phase.bubble() {
772 let source_index = rendered_text.source_index_for_position(event.position).ok();
773 if let Some(pressed_footnote_ref) = markdown.pressed_footnote_ref.take()
774 && source_index
775 .and_then(|ix| rendered_text.footnote_ref_for_source_index(ix))
776 == Some(&pressed_footnote_ref)
777 {
778 if let Some(source_index) =
779 markdown.footnote_definition_content_start(&pressed_footnote_ref.label)
780 {
781 markdown.autoscroll_request = Some(source_index);
782 cx.notify();
783 }
784 } else if let Some(pressed_link) = markdown.pressed_link.take()
785 && source_index.and_then(|ix| rendered_text.link_for_source_index(ix))
786 == Some(&pressed_link)
787 {
788 if let Some(open_url) = on_open_url.as_ref() {
789 open_url(pressed_link.destination_url, window, cx);
790 } else {
791 cx.open_url(&pressed_link.destination_url);
792 }
793 }
794 } else if markdown.selection.pending {
795 markdown.selection.pending = false;
796 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
797 {
798 let text = rendered_text
799 .text_for_range(markdown.selection.start..markdown.selection.end);
800 cx.write_to_primary(gpui::ClipboardItem::new_string(text))
801 }
802 cx.notify();
803 }
804 }
805 });
806 }
807
808 fn autoscroll(
809 &self,
810 rendered_text: &RenderedText,
811 window: &mut Window,
812 cx: &mut App,
813 ) -> Option<()> {
814 let autoscroll_index = self
815 .markdown
816 .update(cx, |markdown, _| markdown.autoscroll_request.take())?;
817 let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
818
819 match &self.autoscroll {
820 AutoscrollBehavior::Controlled(scroll_handle) => {
821 let viewport = scroll_handle.bounds();
822 let margin = line_height * 3.;
823 let top_goal = viewport.top() + margin;
824 let bottom_goal = viewport.bottom() - margin;
825 let current_offset = scroll_handle.offset();
826
827 let new_offset_y = if position.y < top_goal {
828 current_offset.y + (top_goal - position.y)
829 } else if position.y + line_height > bottom_goal {
830 current_offset.y + (bottom_goal - (position.y + line_height))
831 } else {
832 current_offset.y
833 };
834
835 scroll_handle.set_offset(point(
836 current_offset.x,
837 new_offset_y.clamp(-scroll_handle.max_offset().y, Pixels::ZERO),
838 ));
839 }
840 AutoscrollBehavior::Propagate => {
841 let text_style = self.style.base_text_style.clone();
842 let font_id = window.text_system().resolve_font(&text_style.font());
843 let font_size = text_style.font_size.to_pixels(window.rem_size());
844 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
845 window.request_autoscroll(Bounds::from_corners(
846 point(position.x - 3. * em_width, position.y - 3. * line_height),
847 point(position.x + 3. * em_width, position.y + 3. * line_height),
848 ));
849 }
850 }
851 Some(())
852 }
853
854 fn on_mouse_event<T: MouseEvent>(
855 &self,
856 window: &mut Window,
857 _cx: &mut App,
858 mut f: impl 'static
859 + FnMut(&mut Markdown, &T, DispatchPhase, &mut Window, &mut gpui::Context<Markdown>),
860 ) {
861 window.on_mouse_event({
862 let markdown = self.markdown.downgrade();
863 move |event, phase, window, cx| {
864 markdown
865 .update(cx, |markdown, cx| f(markdown, event, phase, window, cx))
866 .log_err();
867 }
868 });
869 }
870}
871
872impl Styled for MarkdownElement {
873 fn style(&mut self) -> &mut StyleRefinement {
874 &mut self.style.container_style
875 }
876}
877
878impl gpui::Element for MarkdownElement {
879 type RequestLayoutState = RenderedMarkdown;
880 type PrepaintState = Hitbox;
881
882 fn id(&self) -> Option<ElementId> {
883 None
884 }
885
886 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
887 None
888 }
889
890 fn request_layout(
891 &mut self,
892 _id: Option<&GlobalElementId>,
893 _inspector_id: Option<&gpui::InspectorElementId>,
894 window: &mut Window,
895 cx: &mut App,
896 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
897 let mut builder = MarkdownElementBuilder::new(
898 &self.style.container_style,
899 self.style.base_text_style.clone(),
900 self.style.syntax.clone(),
901 );
902 let (parsed_markdown, images, active_root_block) = {
903 let markdown = self.markdown.read(cx);
904 (
905 markdown.parsed_markdown.clone(),
906 markdown.images_by_source_offset.clone(),
907 markdown.active_root_block(),
908 )
909 };
910 let markdown_end = if let Some(last) = parsed_markdown.events.last() {
911 last.0.end
912 } else {
913 0
914 };
915 let mut code_block_ids = collections::HashSet::default();
916
917 let mut current_img_block_range: Option<Range<usize>> = None;
918 let mut handled_html_block = false;
919 let mut rendered_metadata_block = false;
920 for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
921 if let Some(current_img_block_range) = ¤t_img_block_range
923 && current_img_block_range.end > range.end
924 {
925 continue;
926 }
927
928 if handled_html_block {
929 if let MarkdownEvent::End(MarkdownTagEnd::HtmlBlock) = event {
930 handled_html_block = false;
931 } else {
932 continue;
933 }
934 }
935
936 if rendered_metadata_block {
937 if matches!(event, MarkdownEvent::End(MarkdownTagEnd::MetadataBlock(_))) {
938 rendered_metadata_block = false;
939 }
940 continue;
941 }
942
943 match event {
944 MarkdownEvent::RootStart => {
945 if self.show_root_block_markers {
946 builder.push_root_block(range, markdown_end);
947 }
948 }
949 MarkdownEvent::RootEnd(root_block_index) => {
950 if self.show_root_block_markers {
951 builder.pop_root_block(
952 active_root_block == Some(*root_block_index),
953 cx.theme().colors().border,
954 cx.theme().colors().border_variant,
955 );
956 }
957 }
958 MarkdownEvent::Start(tag) => {
959 match tag {
960 MarkdownTag::Image { dest_url, .. } => {
961 let alt_text = collect_image_alt_text(
962 &parsed_markdown.events[index..],
963 &parsed_markdown.source,
964 );
965 if let Some(image) = images.get(&range.start) {
966 current_img_block_range = Some(range.clone());
967 self.push_markdown_image(
968 &mut builder,
969 range,
970 image.clone().into(),
971 dest_url.clone(),
972 alt_text,
973 None,
974 None,
975 );
976 } else if let Some(source) = self
977 .image_resolver
978 .as_ref()
979 .and_then(|resolve| resolve(dest_url.as_ref()))
980 {
981 current_img_block_range = Some(range.clone());
982 self.push_markdown_image(
983 &mut builder,
984 range,
985 source,
986 dest_url.clone(),
987 alt_text,
988 None,
989 None,
990 );
991 }
992 }
993 MarkdownTag::Paragraph => {
994 let text_align_override = builder
995 .table
996 .current_cell_alignment()
997 .and_then(alignment_to_text_align);
998 self.push_markdown_paragraph(
999 &mut builder,
1000 range,
1001 markdown_end,
1002 text_align_override,
1003 );
1004 }
1005 MarkdownTag::Heading { level, .. } => {
1006 let text_align_override = builder
1007 .table
1008 .current_cell_alignment()
1009 .and_then(alignment_to_text_align);
1010 self.push_markdown_heading(
1011 &mut builder,
1012 *level,
1013 range,
1014 markdown_end,
1015 text_align_override,
1016 );
1017 }
1018 MarkdownTag::BlockQuote(kind) => {
1019 self.push_markdown_block_quote(
1020 &mut builder,
1021 *kind,
1022 range,
1023 markdown_end,
1024 );
1025 }
1026 MarkdownTag::CodeBlock { kind, .. } => {
1027 let language = resolve_code_block_language(kind);
1028
1029 let is_indented = matches!(kind, CodeBlockKind::Indented);
1030 let scroll_handle = if self.style.code_block_overflow_x_scroll {
1031 self.markdown.update(cx, |markdown, _| {
1032 markdown.code_block_scroll_handle(range.start)
1033 })
1034 } else {
1035 None
1036 };
1037 if scroll_handle.is_some() {
1038 code_block_ids.insert(range.start);
1039 }
1040
1041 match (&self.code_block_renderer, is_indented) {
1042 (CodeBlockRenderer::Default { .. }, _) | (_, true) => {
1043 let mut parent_container: AnyDiv =
1045 div().group("code_block").relative().w_full().into();
1046
1047 if let CodeBlockRenderer::Default { border: true, .. } =
1048 &self.code_block_renderer
1049 {
1050 parent_container = parent_container
1051 .rounded_md()
1052 .border_1()
1053 .border_color(cx.theme().colors().border_variant);
1054 }
1055
1056 parent_container.style().refine(&self.style.code_block);
1057 builder.push_div(parent_container, range, markdown_end);
1058
1059 let code_block = div()
1060 .id(("code-block", range.start))
1061 .rounded_lg()
1062 .map(|mut code_block| {
1063 if let Some(scroll_handle) = scroll_handle.as_ref() {
1064 code_block.style().restrict_scroll_to_axis =
1065 Some(true);
1066 code_block
1067 .flex()
1068 .overflow_x_scroll()
1069 .track_scroll(scroll_handle)
1070 } else {
1071 code_block.w_full()
1072 }
1073 });
1074
1075 builder.push_text_style(self.style.code_block.text.to_owned());
1076 builder.push_code_block(language);
1077 builder.push_div(code_block, range, markdown_end);
1078 }
1079 (CodeBlockRenderer::Custom { .. }, _) => {}
1080 }
1081 }
1082 MarkdownTag::HtmlBlock => {
1083 builder.push_div(div(), range, markdown_end);
1084 if let Some(block) = parsed_markdown.html_blocks.get(&range.start) {
1085 self.render_html_block(block, &mut builder, markdown_end, cx);
1086 handled_html_block = true;
1087 }
1088 }
1089 MarkdownTag::List(bullet_index) => {
1090 builder.push_list(*bullet_index);
1091 builder.push_div(div().pl_2p5(), range, markdown_end);
1092 }
1093 MarkdownTag::Item => {
1094 let bullet =
1095 if let Some((task_range, MarkdownEvent::TaskListMarker(checked))) =
1096 parsed_markdown.events.get(index.saturating_add(1))
1097 {
1098 let source = &parsed_markdown.source()[range.clone()];
1099 let checked = *checked;
1100
1101 let checkbox = Checkbox::new(
1102 ElementId::Name(source.to_string().into()),
1103 checked,
1104 );
1105
1106 if let Some(on_toggle) = self.on_checkbox_toggle.clone() {
1107 let task_source_range = task_range.clone();
1108 checkbox
1109 .on_click(move |_state, window, cx| {
1110 on_toggle(
1111 task_source_range.clone(),
1112 !checked,
1113 window,
1114 cx,
1115 );
1116 })
1117 .into_any_element()
1118 } else {
1119 checkbox.into_any_element()
1120 }
1121 } else if let Some(bullet_index) = builder.next_bullet_index() {
1122 div().child(format!("{}.", bullet_index)).into_any_element()
1123 } else {
1124 div().child("•").into_any_element()
1125 };
1126 self.push_markdown_list_item(&mut builder, bullet, range, markdown_end);
1127 }
1128 MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
1129 font_style: Some(FontStyle::Italic),
1130 ..Default::default()
1131 }),
1132 MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
1133 font_weight: Some(FontWeight::BOLD),
1134 color: Some(cx.theme().colors().text),
1135 ..Default::default()
1136 }),
1137 MarkdownTag::Strikethrough => {
1138 builder.push_text_style(TextStyleRefinement {
1139 strikethrough: Some(StrikethroughStyle {
1140 thickness: px(1.),
1141 color: None,
1142 }),
1143 ..Default::default()
1144 })
1145 }
1146 MarkdownTag::Link { dest_url, .. } => {
1147 if builder.code_block_stack.is_empty() {
1148 builder.link_depth += 1;
1149 builder.push_link(dest_url.clone(), range.clone());
1150 let style = self
1151 .style
1152 .link_callback
1153 .as_ref()
1154 .and_then(|callback| callback(dest_url, cx))
1155 .unwrap_or_else(|| self.style.link.clone());
1156 builder.push_text_style(style)
1157 }
1158 }
1159 MarkdownTag::FootnoteDefinition(label) => {
1160 if !builder.rendered_footnote_separator {
1161 builder.rendered_footnote_separator = true;
1162 builder.push_div(
1163 div()
1164 .border_t_1()
1165 .mt_2()
1166 .border_color(self.style.rule_color),
1167 range,
1168 markdown_end,
1169 );
1170 builder.pop_div();
1171 }
1172 builder.push_div(
1173 div()
1174 .pt_1()
1175 .mb_1()
1176 .line_height(rems(1.3))
1177 .text_size(rems(0.85))
1178 .flex()
1179 .flex_row()
1180 .items_start()
1181 .gap_2()
1182 .child(
1183 div().text_size(rems(0.85)).child(format!("{}.", label)),
1184 ),
1185 range,
1186 markdown_end,
1187 );
1188 builder.push_div(div().flex_1().w_0(), range, markdown_end);
1189 }
1190 MarkdownTag::MetadataBlock(_) => {
1191 if let Some(metadata_block) =
1192 parsed_markdown.metadata_blocks.get(&range.start)
1193 {
1194 self.push_metadata_block(
1195 &mut builder,
1196 &parsed_markdown.source,
1197 metadata_block,
1198 markdown_end,
1199 cx,
1200 );
1201 rendered_metadata_block = true;
1202 }
1203 }
1204 MarkdownTag::Table(alignments) => {
1205 builder.table.start(alignments.clone());
1206
1207 let column_count = alignments.len();
1208 builder.push_div(
1209 div()
1210 .id(("table", range.start))
1211 .grid()
1212 .grid_cols(column_count as u16)
1213 .when(self.style.table_columns_min_size, |this| {
1214 this.grid_cols_min_content(column_count as u16)
1215 })
1216 .when(!self.style.table_columns_min_size, |this| {
1217 this.grid_cols(column_count as u16)
1218 })
1219 .w_full()
1220 .mb_2()
1221 .border(px(1.5))
1222 .border_color(cx.theme().colors().border)
1223 .rounded_sm()
1224 .overflow_hidden(),
1225 range,
1226 markdown_end,
1227 );
1228 }
1229 MarkdownTag::TableHead => {
1230 builder.table.start_head();
1231 builder.push_text_style(TextStyleRefinement {
1232 font_weight: Some(FontWeight::SEMIBOLD),
1233 ..Default::default()
1234 });
1235 }
1236 MarkdownTag::TableRow => {
1237 builder.table.start_row();
1238 }
1239 MarkdownTag::TableCell => {
1240 let is_header = builder.table.in_head;
1241 let row_index = builder.table.row_index;
1242 let col_index = builder.table.col_index;
1243 let alignment = builder.table.current_cell_alignment();
1244 let text_align = alignment
1245 .and_then(alignment_to_text_align)
1246 .unwrap_or(self.style.base_text_style.text_align);
1247
1248 let mut cell_div = div()
1249 .flex()
1250 .flex_col()
1251 .h_full()
1252 .when(col_index > 0, |this| this.border_l_1())
1253 .when(row_index > 0, |this| this.border_t_1())
1254 .border_color(cx.theme().colors().border)
1255 .px_1()
1256 .py_0p5()
1257 .when(is_header, |this| {
1258 this.bg(cx.theme().colors().title_bar_background)
1259 })
1260 .when(!is_header && row_index % 2 == 1, |this| {
1261 this.bg(cx.theme().colors().panel_background)
1262 });
1263
1264 cell_div = match alignment {
1265 Some(Alignment::Center) => cell_div.items_center(),
1266 Some(Alignment::Right) => cell_div.items_end(),
1267 _ => cell_div,
1268 };
1269
1270 builder.push_text_style(TextStyleRefinement {
1271 text_align: Some(text_align),
1272 ..Default::default()
1273 });
1274 builder.push_div(cell_div, range, markdown_end);
1275 builder.push_div(
1276 div()
1277 .flex()
1278 .flex_col()
1279 .flex_1()
1280 .w_full()
1281 .justify_center()
1282 .text_align(text_align),
1283 range,
1284 markdown_end,
1285 );
1286 }
1287 _ => log::debug!("unsupported markdown tag {:?}", tag),
1288 }
1289 }
1290 MarkdownEvent::End(tag) => match tag {
1291 MarkdownTagEnd::Image => {
1292 current_img_block_range.take();
1293 }
1294 MarkdownTagEnd::Paragraph => {
1295 self.pop_markdown_paragraph(&mut builder);
1296 }
1297 MarkdownTagEnd::Heading(_) => {
1298 self.pop_markdown_heading(&mut builder);
1299 }
1300 MarkdownTagEnd::BlockQuote(_kind) => {
1301 self.pop_markdown_block_quote(&mut builder);
1302 }
1303 MarkdownTagEnd::CodeBlock => {
1304 builder.trim_trailing_newline();
1305
1306 builder.pop_div();
1307 builder.pop_code_block();
1308 builder.pop_text_style();
1309
1310 if let CodeBlockRenderer::Default {
1311 copy_button_visibility,
1312 wrap_button_visibility,
1313 ..
1314 } = &self.code_block_renderer
1315 && (*copy_button_visibility != CopyButtonVisibility::Hidden
1316 || *wrap_button_visibility != WrapButtonVisibility::Hidden)
1317 {
1318 let copy_button_visibility = *copy_button_visibility;
1319 let wrap_button_visibility = *wrap_button_visibility;
1320 builder.modify_current_div(|el| {
1321 let content_range = crate::parser::extract_code_block_content_range(
1322 &parsed_markdown.source()[range.clone()],
1323 );
1324 let content_range = content_range.start + range.start
1325 ..content_range.end + range.start;
1326
1327 let code = parsed_markdown.source()[content_range].to_string();
1328
1329 let any_hover = copy_button_visibility
1330 == CopyButtonVisibility::VisibleOnHover
1331 || wrap_button_visibility
1332 == WrapButtonVisibility::VisibleOnHover;
1333 let any_always = copy_button_visibility
1334 == CopyButtonVisibility::AlwaysVisible
1335 || wrap_button_visibility
1336 == WrapButtonVisibility::AlwaysVisible;
1337 let use_hover = any_hover && !any_always;
1338
1339 let button_row = h_flex()
1340 .gap_0p5()
1341 .absolute()
1342 .bg(cx.theme().colors().editor_background)
1343 .when_else(
1344 use_hover,
1345 |this| {
1346 this.top_1()
1347 .right_1()
1348 .opacity(0.)
1349 .group_hover("code_block", |el| el.opacity(1.))
1350 },
1351 |this| this.top_1p5().right_1p5(),
1352 )
1353 .when(
1354 wrap_button_visibility != WrapButtonVisibility::Hidden,
1355 |this| {
1356 let is_wrapped = self
1357 .markdown
1358 .read(cx)
1359 .is_code_block_wrapped(range.start);
1360
1361 this.child(render_wrap_code_block_button(
1362 range.start,
1363 is_wrapped,
1364 self.markdown.clone(),
1365 ))
1366 },
1367 )
1368 .when(
1369 copy_button_visibility != CopyButtonVisibility::Hidden,
1370 |this| {
1371 this.child(render_copy_code_block_button(
1372 range.end,
1373 code,
1374 self.markdown.clone(),
1375 ))
1376 },
1377 );
1378
1379 el.child(button_row)
1380 });
1381 }
1382
1383 builder.pop_div();
1385 }
1386 MarkdownTagEnd::HtmlBlock => builder.pop_div(),
1387 MarkdownTagEnd::List(_) => {
1388 builder.pop_list();
1389 builder.pop_div();
1390 }
1391 MarkdownTagEnd::Item => {
1392 self.pop_markdown_list_item(&mut builder);
1393 }
1394 MarkdownTagEnd::Emphasis => builder.pop_text_style(),
1395 MarkdownTagEnd::Strong => builder.pop_text_style(),
1396 MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
1397 MarkdownTagEnd::Link => {
1398 if builder.code_block_stack.is_empty() {
1399 builder.link_depth = builder.link_depth.saturating_sub(1);
1400 builder.pop_text_style()
1401 }
1402 }
1403 MarkdownTagEnd::Table => {
1404 builder.pop_div();
1405 builder.table.end();
1406 }
1407 MarkdownTagEnd::TableHead => {
1408 builder.pop_text_style();
1409 builder.table.end_head();
1410 }
1411 MarkdownTagEnd::TableRow => {
1412 builder.table.end_row();
1413 }
1414 MarkdownTagEnd::TableCell => {
1415 builder.replace_pending_checkbox(self.on_checkbox_toggle.clone());
1416 builder.pop_div();
1417 builder.pop_div();
1418 builder.pop_text_style();
1419 builder.table.end_cell();
1420 }
1421 MarkdownTagEnd::FootnoteDefinition => {
1422 builder.pop_div();
1423 builder.pop_div();
1424 }
1425 MarkdownTagEnd::MetadataBlock(_) => {}
1426 _ => log::debug!("unsupported markdown tag end: {:?}", tag),
1427 },
1428 MarkdownEvent::Text => {
1429 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1430 }
1431 MarkdownEvent::SubstitutedText(text) => {
1432 builder.push_text(text, range.clone());
1433 }
1434 MarkdownEvent::Code => {
1435 self.push_markdown_code_span(
1436 &mut builder,
1437 &parsed_markdown.source[range.clone()],
1438 range.clone(),
1439 cx,
1440 );
1441 }
1442 MarkdownEvent::SubstitutedCode(text) => {
1443 self.push_markdown_code_span(&mut builder, text, range.clone(), cx);
1444 }
1445 MarkdownEvent::Html => {
1446 let html = &parsed_markdown.source[range.clone()];
1447 if html.starts_with("<!--") {
1448 builder.html_comment = true;
1449 }
1450 if html.trim_end().ends_with("-->") {
1451 builder.html_comment = false;
1452 continue;
1453 }
1454 if builder.html_comment {
1455 continue;
1456 }
1457 builder.push_text(html, range.clone());
1458 }
1459 MarkdownEvent::InlineHtml => {
1460 let html = &parsed_markdown.source[range.clone()];
1461 if let Some(code) = html
1462 .strip_prefix("<code>")
1463 .and_then(|html| html.strip_suffix("</code>"))
1464 {
1465 let code_start = range.start + "<code>".len();
1466 self.push_markdown_code_span(
1467 &mut builder,
1468 code,
1469 code_start..code_start + code.len(),
1470 cx,
1471 );
1472 continue;
1473 }
1474 if html.starts_with("<code>") {
1475 builder.push_text_style(self.style.inline_code.clone());
1476 continue;
1477 }
1478 if html.trim_end().starts_with("</code>") {
1479 builder.pop_text_style();
1480 continue;
1481 }
1482 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1483 }
1484 MarkdownEvent::Rule => {
1485 builder.push_div(
1486 div()
1487 .border_b_1()
1488 .my_2()
1489 .border_color(self.style.rule_color),
1490 range,
1491 markdown_end,
1492 );
1493 builder.pop_div()
1494 }
1495 MarkdownEvent::SoftBreak if !self.style.soft_break_as_hard_break => {
1496 builder.push_soft_break(range.clone());
1497 }
1498 MarkdownEvent::SoftBreak | MarkdownEvent::HardBreak => {
1499 builder.push_line_break(range.clone());
1500 }
1501 MarkdownEvent::TaskListMarker(_) => {
1502 }
1504 MarkdownEvent::FootnoteReference(label) => {
1505 builder.push_footnote_ref(label.clone(), range.clone());
1506 builder.push_text_style(self.style.link.clone());
1507 builder.push_text(&format!("[{label}]"), range.clone());
1508 builder.pop_text_style();
1509 }
1510 }
1511 }
1512 if self.style.code_block_overflow_x_scroll {
1513 let code_block_ids = code_block_ids;
1514 self.markdown.update(cx, move |markdown, _| {
1515 markdown.retain_code_block_scroll_handles(&code_block_ids);
1516 });
1517 } else {
1518 self.markdown
1519 .update(cx, |markdown, _| markdown.clear_code_block_scroll_handles());
1520 }
1521 let mut rendered_markdown = builder.build();
1522 let child_layout_id = rendered_markdown.element.request_layout(window, cx);
1523 let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);
1524 (layout_id, rendered_markdown)
1525 }
1526
1527 fn prepaint(
1528 &mut self,
1529 _id: Option<&GlobalElementId>,
1530 _inspector_id: Option<&gpui::InspectorElementId>,
1531 bounds: Bounds<Pixels>,
1532 rendered_markdown: &mut Self::RequestLayoutState,
1533 window: &mut Window,
1534 cx: &mut App,
1535 ) -> Self::PrepaintState {
1536 let focus_handle = self.markdown.read(cx).focus_handle.clone();
1537 window.set_focus_handle(&focus_handle, cx);
1538 window.set_view_id(self.markdown.entity_id());
1539
1540 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1541 rendered_markdown.element.prepaint(window, cx);
1542 self.autoscroll(&rendered_markdown.text, window, cx);
1543 hitbox
1544 }
1545
1546 fn paint(
1547 &mut self,
1548 _id: Option<&GlobalElementId>,
1549 _inspector_id: Option<&gpui::InspectorElementId>,
1550 _bounds: Bounds<Pixels>,
1551 rendered_markdown: &mut Self::RequestLayoutState,
1552 hitbox: &mut Self::PrepaintState,
1553 window: &mut Window,
1554 cx: &mut App,
1555 ) {
1556 let mut context = KeyContext::default();
1557 context.add("Markdown");
1558 window.set_key_context(context);
1559 window.on_action(std::any::TypeId::of::<crate::entity::Copy>(), {
1560 let entity = self.markdown.clone();
1561 let text = rendered_markdown.text.clone();
1562 move |_, phase, window, cx| {
1563 let text = text.clone();
1564 if phase == DispatchPhase::Bubble {
1565 entity.update(cx, move |this, cx| this.copy(&text, window, cx))
1566 }
1567 }
1568 });
1569 window.on_action(std::any::TypeId::of::<crate::entity::CopyAsMarkdown>(), {
1570 let entity = self.markdown.clone();
1571 move |_, phase, window, cx| {
1572 if phase == DispatchPhase::Bubble {
1573 entity.update(cx, move |this, cx| this.copy_as_markdown(window, cx))
1574 }
1575 }
1576 });
1577
1578 self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
1579 rendered_markdown.element.paint(window, cx);
1580 self.paint_search_highlights(&rendered_markdown.text, window, cx);
1581 self.paint_selection(&rendered_markdown.text, window, cx);
1582 }
1583}
1584
1585fn collect_image_alt_text(
1586 events_from_image_start: &[(Range<usize>, MarkdownEvent)],
1587 source: &str,
1588) -> Option<SharedString> {
1589 let mut alt_text = String::new();
1590 for (range, event) in events_from_image_start.iter().skip(1) {
1591 match event {
1592 MarkdownEvent::End(MarkdownTagEnd::Image) => break,
1593 MarkdownEvent::Text => alt_text.push_str(&source[range.clone()]),
1594 _ => {}
1595 }
1596 }
1597 if alt_text.is_empty() {
1598 None
1599 } else {
1600 Some(alt_text.into())
1601 }
1602}
1603
1604fn image_fallback_element(
1605 dest_url: SharedString,
1606 alt_text: Option<SharedString>,
1607 open_image_url_on_click: bool,
1608) -> AnyElement {
1609 let link_label = alt_text
1610 .filter(|alt| !alt.is_empty())
1611 .unwrap_or_else(|| dest_url.clone());
1612
1613 let label = format!("Failed to Load: {link_label}");
1614
1615 div()
1616 .id("image-fallback")
1617 .min_w_0()
1618 .text_color(gpui::red())
1619 .child(label)
1620 .tooltip(simple_tooltip(
1621 "Image failed to load. Check the logs for more details.",
1622 ))
1623 .when(open_image_url_on_click, |this| {
1624 this.cursor_pointer()
1625 .on_click(move |_, _, cx| cx.open_url(&dest_url))
1626 })
1627 .into_any_element()
1628}
1629
1630fn apply_heading_style(
1631 mut heading: Div,
1632 level: HeadingLevel,
1633 custom_styles: Option<&HeadingLevelStyles>,
1634 border_color: Option<Hsla>,
1635) -> Div {
1636 heading = match level {
1637 HeadingLevel::H1 => heading.text_3xl(),
1638 HeadingLevel::H2 => heading.text_2xl(),
1639 HeadingLevel::H3 => heading.text_xl(),
1640 HeadingLevel::H4 => heading.text_lg(),
1641 HeadingLevel::H5 => heading.text_base(),
1642 HeadingLevel::H6 => heading.text_sm(),
1643 };
1644
1645 heading = match level {
1646 HeadingLevel::H1 => heading,
1647 _ => heading.mt_6(),
1648 };
1649
1650 if let Some(border_color) = border_color
1651 && matches!(
1652 level,
1653 HeadingLevel::H1 | HeadingLevel::H2 | HeadingLevel::H3
1654 )
1655 {
1656 heading = heading.pb_1().border_b_1().border_color(border_color);
1657 }
1658
1659 if let Some(styles) = custom_styles {
1660 let style_opt = match level {
1661 HeadingLevel::H1 => &styles.h1,
1662 HeadingLevel::H2 => &styles.h2,
1663 HeadingLevel::H3 => &styles.h3,
1664 HeadingLevel::H4 => &styles.h4,
1665 HeadingLevel::H5 => &styles.h5,
1666 HeadingLevel::H6 => &styles.h6,
1667 };
1668
1669 if let Some(style) = style_opt {
1670 heading.style().text = style.clone();
1671 }
1672 }
1673
1674 heading
1675}
1676
1677fn render_wrap_code_block_button(
1678 id: usize,
1679 is_wrapped: bool,
1680 markdown: Entity<Markdown>,
1681) -> impl IntoElement {
1682 let tooltip = if is_wrapped {
1683 "Unwrap Content"
1684 } else {
1685 "Wrap Content"
1686 };
1687 let button_id = ElementId::NamedChild(
1688 Arc::new(ElementId::from(("wrap-code-block", markdown.entity_id()))),
1689 id.to_string().into(),
1690 );
1691
1692 div()
1693 .id(button_id)
1694 .flex()
1695 .items_center()
1696 .justify_center()
1697 .size(px(20.))
1698 .rounded_xs()
1699 .cursor_pointer()
1700 .child(icon_svg(
1701 IconName::ArrowRightLeft,
1702 px(14.),
1703 gpui::rgb(0xA0AEC0),
1704 ))
1705 .tooltip(simple_tooltip(tooltip))
1706 .on_click(move |_event, _window, cx| {
1707 markdown.update(cx, |markdown, cx| {
1708 markdown.toggle_code_block_wrap(id);
1709 cx.notify();
1710 });
1711 })
1712}
1713
1714fn render_copy_code_block_button(
1715 id: usize,
1716 code: String,
1717 markdown: Entity<Markdown>,
1718) -> impl IntoElement {
1719 let id = ElementId::NamedChild(
1720 Arc::new(ElementId::from((
1721 "copy-markdown-code",
1722 markdown.entity_id(),
1723 ))),
1724 id.to_string().into(),
1725 );
1726
1727 CopyButton::new(id, code)
1728}
1729
1730impl IntoElement for MarkdownElement {
1731 type Element = Self;
1732
1733 fn into_element(self) -> Self::Element {
1734 self
1735 }
1736}
1737
1738pub enum AnyDiv {
1739 Div(Div),
1740 Stateful(Stateful<Div>),
1741}
1742
1743impl AnyDiv {
1744 pub(crate) fn into_any_element(self) -> AnyElement {
1745 match self {
1746 Self::Div(div) => div.into_any_element(),
1747 Self::Stateful(div) => div.into_any_element(),
1748 }
1749 }
1750}
1751
1752impl From<Div> for AnyDiv {
1753 fn from(value: Div) -> Self {
1754 Self::Div(value)
1755 }
1756}
1757
1758impl From<Stateful<Div>> for AnyDiv {
1759 fn from(value: Stateful<Div>) -> Self {
1760 Self::Stateful(value)
1761 }
1762}
1763
1764impl Styled for AnyDiv {
1765 fn style(&mut self) -> &mut StyleRefinement {
1766 match self {
1767 Self::Div(div) => div.style(),
1768 Self::Stateful(div) => div.style(),
1769 }
1770 }
1771}
1772
1773impl ParentElement for AnyDiv {
1774 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1775 match self {
1776 Self::Div(div) => div.extend(elements),
1777 Self::Stateful(div) => div.extend(elements),
1778 }
1779 }
1780}