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()
1123 .min_w(px(16.))
1124 .text_color(cx.theme().colors().text)
1125 .child(format!("{}.", bullet_index))
1126 .into_any_element()
1127 } else {
1128 div()
1129 .min_w(px(16.))
1130 .text_color(cx.theme().colors().text)
1131 .child("•")
1132 .into_any_element()
1133 };
1134 self.push_markdown_list_item(&mut builder, bullet, range, markdown_end);
1135 }
1136 MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
1137 font_style: Some(FontStyle::Italic),
1138 ..Default::default()
1139 }),
1140 MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
1141 font_weight: Some(FontWeight::BOLD),
1142 color: Some(cx.theme().colors().text),
1143 ..Default::default()
1144 }),
1145 MarkdownTag::Strikethrough => {
1146 builder.push_text_style(TextStyleRefinement {
1147 strikethrough: Some(StrikethroughStyle {
1148 thickness: px(1.),
1149 color: None,
1150 }),
1151 ..Default::default()
1152 })
1153 }
1154 MarkdownTag::Link { dest_url, .. } => {
1155 if builder.code_block_stack.is_empty() {
1156 builder.link_depth += 1;
1157 builder.push_link(dest_url.clone(), range.clone());
1158 let style = self
1159 .style
1160 .link_callback
1161 .as_ref()
1162 .and_then(|callback| callback(dest_url, cx))
1163 .unwrap_or_else(|| self.style.link.clone());
1164 builder.push_text_style(style)
1165 }
1166 }
1167 MarkdownTag::FootnoteDefinition(label) => {
1168 if !builder.rendered_footnote_separator {
1169 builder.rendered_footnote_separator = true;
1170 builder.push_div(
1171 div()
1172 .border_t_1()
1173 .mt_2()
1174 .border_color(self.style.rule_color),
1175 range,
1176 markdown_end,
1177 );
1178 builder.pop_div();
1179 }
1180 builder.push_div(
1181 div()
1182 .pt_1()
1183 .mb_1()
1184 .line_height(rems(1.3))
1185 .text_size(rems(0.85))
1186 .flex()
1187 .flex_row()
1188 .items_start()
1189 .gap_2()
1190 .child(
1191 div().text_size(rems(0.85)).child(format!("{}.", label)),
1192 ),
1193 range,
1194 markdown_end,
1195 );
1196 builder.push_div(div().flex_1().w_0(), range, markdown_end);
1197 }
1198 MarkdownTag::MetadataBlock(_) => {
1199 if let Some(metadata_block) =
1200 parsed_markdown.metadata_blocks.get(&range.start)
1201 {
1202 self.push_metadata_block(
1203 &mut builder,
1204 &parsed_markdown.source,
1205 metadata_block,
1206 markdown_end,
1207 cx,
1208 );
1209 rendered_metadata_block = true;
1210 }
1211 }
1212 MarkdownTag::Table(alignments) => {
1213 builder.table.start(alignments.clone());
1214
1215 let column_count = alignments.len();
1216 builder.push_div(
1217 div()
1218 .id(("table", range.start))
1219 .grid()
1220 .grid_cols(column_count as u16)
1221 .when(self.style.table_columns_min_size, |this| {
1222 this.grid_cols_min_content(column_count as u16)
1223 })
1224 .when(!self.style.table_columns_min_size, |this| {
1225 this.grid_cols(column_count as u16)
1226 })
1227 .w_full()
1228 .mb_2()
1229 .border(px(1.5))
1230 .border_color(cx.theme().colors().border)
1231 .rounded_sm()
1232 .overflow_hidden(),
1233 range,
1234 markdown_end,
1235 );
1236 }
1237 MarkdownTag::TableHead => {
1238 builder.table.start_head();
1239 builder.push_text_style(TextStyleRefinement {
1240 font_weight: Some(FontWeight::SEMIBOLD),
1241 ..Default::default()
1242 });
1243 }
1244 MarkdownTag::TableRow => {
1245 builder.table.start_row();
1246 }
1247 MarkdownTag::TableCell => {
1248 let is_header = builder.table.in_head;
1249 let row_index = builder.table.row_index;
1250 let col_index = builder.table.col_index;
1251 let alignment = builder.table.current_cell_alignment();
1252 let text_align = alignment
1253 .and_then(alignment_to_text_align)
1254 .unwrap_or(self.style.base_text_style.text_align);
1255
1256 let mut cell_div = div()
1257 .flex()
1258 .flex_col()
1259 .h_full()
1260 .when(col_index > 0, |this| this.border_l_1())
1261 .when(row_index > 0, |this| this.border_t_1())
1262 .border_color(cx.theme().colors().border)
1263 .px_1()
1264 .py_0p5()
1265 .when(is_header, |this| {
1266 this.bg(cx.theme().colors().title_bar_background)
1267 })
1268 .when(!is_header && row_index % 2 == 1, |this| {
1269 this.bg(cx.theme().colors().panel_background)
1270 });
1271
1272 cell_div = match alignment {
1273 Some(Alignment::Center) => cell_div.items_center(),
1274 Some(Alignment::Right) => cell_div.items_end(),
1275 _ => cell_div,
1276 };
1277
1278 builder.push_text_style(TextStyleRefinement {
1279 text_align: Some(text_align),
1280 ..Default::default()
1281 });
1282 builder.push_div(cell_div, range, markdown_end);
1283 builder.push_div(
1284 div()
1285 .flex()
1286 .flex_col()
1287 .flex_1()
1288 .w_full()
1289 .justify_center()
1290 .text_align(text_align),
1291 range,
1292 markdown_end,
1293 );
1294 }
1295 _ => log::debug!("unsupported markdown tag {:?}", tag),
1296 }
1297 }
1298 MarkdownEvent::End(tag) => match tag {
1299 MarkdownTagEnd::Image => {
1300 current_img_block_range.take();
1301 }
1302 MarkdownTagEnd::Paragraph => {
1303 self.pop_markdown_paragraph(&mut builder);
1304 }
1305 MarkdownTagEnd::Heading(_) => {
1306 self.pop_markdown_heading(&mut builder);
1307 }
1308 MarkdownTagEnd::BlockQuote(_kind) => {
1309 self.pop_markdown_block_quote(&mut builder);
1310 }
1311 MarkdownTagEnd::CodeBlock => {
1312 builder.trim_trailing_newline();
1313
1314 builder.pop_div();
1315 builder.pop_code_block();
1316 builder.pop_text_style();
1317
1318 if let CodeBlockRenderer::Default {
1319 copy_button_visibility,
1320 wrap_button_visibility,
1321 ..
1322 } = &self.code_block_renderer
1323 && (*copy_button_visibility != CopyButtonVisibility::Hidden
1324 || *wrap_button_visibility != WrapButtonVisibility::Hidden)
1325 {
1326 let copy_button_visibility = *copy_button_visibility;
1327 let wrap_button_visibility = *wrap_button_visibility;
1328 builder.modify_current_div(|el| {
1329 let content_range = crate::parser::extract_code_block_content_range(
1330 &parsed_markdown.source()[range.clone()],
1331 );
1332 let content_range = content_range.start + range.start
1333 ..content_range.end + range.start;
1334
1335 let code = parsed_markdown.source()[content_range].to_string();
1336
1337 let any_hover = copy_button_visibility
1338 == CopyButtonVisibility::VisibleOnHover
1339 || wrap_button_visibility
1340 == WrapButtonVisibility::VisibleOnHover;
1341 let any_always = copy_button_visibility
1342 == CopyButtonVisibility::AlwaysVisible
1343 || wrap_button_visibility
1344 == WrapButtonVisibility::AlwaysVisible;
1345 let use_hover = any_hover && !any_always;
1346
1347 let button_row = h_flex()
1348 .gap_0p5()
1349 .absolute()
1350 .bg(cx.theme().colors().editor_background)
1351 .when_else(
1352 use_hover,
1353 |this| {
1354 this.top_1()
1355 .right_1()
1356 .opacity(0.)
1357 .group_hover("code_block", |el| el.opacity(1.))
1358 },
1359 |this| this.top_1p5().right_1p5(),
1360 )
1361 .when(
1362 wrap_button_visibility != WrapButtonVisibility::Hidden,
1363 |this| {
1364 let is_wrapped = self
1365 .markdown
1366 .read(cx)
1367 .is_code_block_wrapped(range.start);
1368
1369 this.child(render_wrap_code_block_button(
1370 range.start,
1371 is_wrapped,
1372 self.markdown.clone(),
1373 ))
1374 },
1375 )
1376 .when(
1377 copy_button_visibility != CopyButtonVisibility::Hidden,
1378 |this| {
1379 this.child(render_copy_code_block_button(
1380 range.end,
1381 code,
1382 self.markdown.clone(),
1383 ))
1384 },
1385 );
1386
1387 el.child(button_row)
1388 });
1389 }
1390
1391 builder.pop_div();
1393 }
1394 MarkdownTagEnd::HtmlBlock => builder.pop_div(),
1395 MarkdownTagEnd::List(_) => {
1396 builder.pop_list();
1397 builder.pop_div();
1398 }
1399 MarkdownTagEnd::Item => {
1400 self.pop_markdown_list_item(&mut builder);
1401 }
1402 MarkdownTagEnd::Emphasis => builder.pop_text_style(),
1403 MarkdownTagEnd::Strong => builder.pop_text_style(),
1404 MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
1405 MarkdownTagEnd::Link => {
1406 if builder.code_block_stack.is_empty() {
1407 builder.link_depth = builder.link_depth.saturating_sub(1);
1408 builder.pop_text_style()
1409 }
1410 }
1411 MarkdownTagEnd::Table => {
1412 builder.pop_div();
1413 builder.table.end();
1414 }
1415 MarkdownTagEnd::TableHead => {
1416 builder.pop_text_style();
1417 builder.table.end_head();
1418 }
1419 MarkdownTagEnd::TableRow => {
1420 builder.table.end_row();
1421 }
1422 MarkdownTagEnd::TableCell => {
1423 builder.replace_pending_checkbox(self.on_checkbox_toggle.clone());
1424 builder.pop_div();
1425 builder.pop_div();
1426 builder.pop_text_style();
1427 builder.table.end_cell();
1428 }
1429 MarkdownTagEnd::FootnoteDefinition => {
1430 builder.pop_div();
1431 builder.pop_div();
1432 }
1433 MarkdownTagEnd::MetadataBlock(_) => {}
1434 _ => log::debug!("unsupported markdown tag end: {:?}", tag),
1435 },
1436 MarkdownEvent::Text => {
1437 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1438 }
1439 MarkdownEvent::SubstitutedText(text) => {
1440 builder.push_text(text, range.clone());
1441 }
1442 MarkdownEvent::Code => {
1443 self.push_markdown_code_span(
1444 &mut builder,
1445 &parsed_markdown.source[range.clone()],
1446 range.clone(),
1447 cx,
1448 );
1449 }
1450 MarkdownEvent::SubstitutedCode(text) => {
1451 self.push_markdown_code_span(&mut builder, text, range.clone(), cx);
1452 }
1453 MarkdownEvent::Html => {
1454 let html = &parsed_markdown.source[range.clone()];
1455 if html.starts_with("<!--") {
1456 builder.html_comment = true;
1457 }
1458 if html.trim_end().ends_with("-->") {
1459 builder.html_comment = false;
1460 continue;
1461 }
1462 if builder.html_comment {
1463 continue;
1464 }
1465 builder.push_text(html, range.clone());
1466 }
1467 MarkdownEvent::InlineHtml => {
1468 let html = &parsed_markdown.source[range.clone()];
1469 if let Some(code) = html
1470 .strip_prefix("<code>")
1471 .and_then(|html| html.strip_suffix("</code>"))
1472 {
1473 let code_start = range.start + "<code>".len();
1474 self.push_markdown_code_span(
1475 &mut builder,
1476 code,
1477 code_start..code_start + code.len(),
1478 cx,
1479 );
1480 continue;
1481 }
1482 if html.starts_with("<code>") {
1483 builder.push_text_style(self.style.inline_code.clone());
1484 continue;
1485 }
1486 if html.trim_end().starts_with("</code>") {
1487 builder.pop_text_style();
1488 continue;
1489 }
1490 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1491 }
1492 MarkdownEvent::Rule => {
1493 builder.push_div(
1494 div()
1495 .border_b_1()
1496 .my_2()
1497 .border_color(self.style.rule_color),
1498 range,
1499 markdown_end,
1500 );
1501 builder.pop_div()
1502 }
1503 MarkdownEvent::SoftBreak if !self.style.soft_break_as_hard_break => {
1504 builder.push_soft_break(range.clone());
1505 }
1506 MarkdownEvent::SoftBreak | MarkdownEvent::HardBreak => {
1507 builder.push_line_break(range.clone());
1508 }
1509 MarkdownEvent::TaskListMarker(_) => {
1510 }
1512 MarkdownEvent::FootnoteReference(label) => {
1513 builder.push_footnote_ref(label.clone(), range.clone());
1514 builder.push_text_style(self.style.link.clone());
1515 builder.push_text(&format!("[{label}]"), range.clone());
1516 builder.pop_text_style();
1517 }
1518 }
1519 }
1520 if self.style.code_block_overflow_x_scroll {
1521 let code_block_ids = code_block_ids;
1522 self.markdown.update(cx, move |markdown, _| {
1523 markdown.retain_code_block_scroll_handles(&code_block_ids);
1524 });
1525 } else {
1526 self.markdown
1527 .update(cx, |markdown, _| markdown.clear_code_block_scroll_handles());
1528 }
1529 let mut rendered_markdown = builder.build();
1530 let child_layout_id = rendered_markdown.element.request_layout(window, cx);
1531 let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);
1532 (layout_id, rendered_markdown)
1533 }
1534
1535 fn prepaint(
1536 &mut self,
1537 _id: Option<&GlobalElementId>,
1538 _inspector_id: Option<&gpui::InspectorElementId>,
1539 bounds: Bounds<Pixels>,
1540 rendered_markdown: &mut Self::RequestLayoutState,
1541 window: &mut Window,
1542 cx: &mut App,
1543 ) -> Self::PrepaintState {
1544 let focus_handle = self.markdown.read(cx).focus_handle.clone();
1545 window.set_focus_handle(&focus_handle, cx);
1546 window.set_view_id(self.markdown.entity_id());
1547
1548 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1549 rendered_markdown.element.prepaint(window, cx);
1550 self.autoscroll(&rendered_markdown.text, window, cx);
1551 hitbox
1552 }
1553
1554 fn paint(
1555 &mut self,
1556 _id: Option<&GlobalElementId>,
1557 _inspector_id: Option<&gpui::InspectorElementId>,
1558 _bounds: Bounds<Pixels>,
1559 rendered_markdown: &mut Self::RequestLayoutState,
1560 hitbox: &mut Self::PrepaintState,
1561 window: &mut Window,
1562 cx: &mut App,
1563 ) {
1564 let mut context = KeyContext::default();
1565 context.add("Markdown");
1566 window.set_key_context(context);
1567 window.on_action(std::any::TypeId::of::<crate::entity::Copy>(), {
1568 let entity = self.markdown.clone();
1569 let text = rendered_markdown.text.clone();
1570 move |_, phase, window, cx| {
1571 let text = text.clone();
1572 if phase == DispatchPhase::Bubble {
1573 entity.update(cx, move |this, cx| this.copy(&text, window, cx))
1574 }
1575 }
1576 });
1577 window.on_action(std::any::TypeId::of::<crate::entity::CopyAsMarkdown>(), {
1578 let entity = self.markdown.clone();
1579 move |_, phase, window, cx| {
1580 if phase == DispatchPhase::Bubble {
1581 entity.update(cx, move |this, cx| this.copy_as_markdown(window, cx))
1582 }
1583 }
1584 });
1585
1586 self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
1587 rendered_markdown.element.paint(window, cx);
1588 self.paint_search_highlights(&rendered_markdown.text, window, cx);
1589 self.paint_selection(&rendered_markdown.text, window, cx);
1590 }
1591}
1592
1593fn collect_image_alt_text(
1594 events_from_image_start: &[(Range<usize>, MarkdownEvent)],
1595 source: &str,
1596) -> Option<SharedString> {
1597 let mut alt_text = String::new();
1598 for (range, event) in events_from_image_start.iter().skip(1) {
1599 match event {
1600 MarkdownEvent::End(MarkdownTagEnd::Image) => break,
1601 MarkdownEvent::Text => alt_text.push_str(&source[range.clone()]),
1602 _ => {}
1603 }
1604 }
1605 if alt_text.is_empty() {
1606 None
1607 } else {
1608 Some(alt_text.into())
1609 }
1610}
1611
1612fn image_fallback_element(
1613 dest_url: SharedString,
1614 alt_text: Option<SharedString>,
1615 open_image_url_on_click: bool,
1616) -> AnyElement {
1617 let link_label = alt_text
1618 .filter(|alt| !alt.is_empty())
1619 .unwrap_or_else(|| dest_url.clone());
1620
1621 let label = format!("Failed to Load: {link_label}");
1622
1623 div()
1624 .id("image-fallback")
1625 .min_w_0()
1626 .text_color(gpui::red())
1627 .child(label)
1628 .tooltip(simple_tooltip(
1629 "Image failed to load. Check the logs for more details.",
1630 ))
1631 .when(open_image_url_on_click, |this| {
1632 this.cursor_pointer()
1633 .on_click(move |_, _, cx| cx.open_url(&dest_url))
1634 })
1635 .into_any_element()
1636}
1637
1638fn apply_heading_style(
1639 mut heading: Div,
1640 level: HeadingLevel,
1641 custom_styles: Option<&HeadingLevelStyles>,
1642 border_color: Option<Hsla>,
1643) -> Div {
1644 heading = match level {
1645 HeadingLevel::H1 => heading.text_3xl(),
1646 HeadingLevel::H2 => heading.text_2xl(),
1647 HeadingLevel::H3 => heading.text_xl(),
1648 HeadingLevel::H4 => heading.text_lg(),
1649 HeadingLevel::H5 => heading.text_base(),
1650 HeadingLevel::H6 => heading.text_sm(),
1651 };
1652
1653 heading = match level {
1654 HeadingLevel::H1 => heading,
1655 _ => heading.mt_6(),
1656 };
1657
1658 if let Some(border_color) = border_color
1659 && matches!(
1660 level,
1661 HeadingLevel::H1 | HeadingLevel::H2 | HeadingLevel::H3
1662 )
1663 {
1664 heading = heading.pb_1().border_b_1().border_color(border_color);
1665 }
1666
1667 if let Some(styles) = custom_styles {
1668 let style_opt = match level {
1669 HeadingLevel::H1 => &styles.h1,
1670 HeadingLevel::H2 => &styles.h2,
1671 HeadingLevel::H3 => &styles.h3,
1672 HeadingLevel::H4 => &styles.h4,
1673 HeadingLevel::H5 => &styles.h5,
1674 HeadingLevel::H6 => &styles.h6,
1675 };
1676
1677 if let Some(style) = style_opt {
1678 heading.style().text = style.clone();
1679 }
1680 }
1681
1682 heading
1683}
1684
1685fn render_wrap_code_block_button(
1686 id: usize,
1687 is_wrapped: bool,
1688 markdown: Entity<Markdown>,
1689) -> impl IntoElement {
1690 let tooltip = if is_wrapped {
1691 "Unwrap Content"
1692 } else {
1693 "Wrap Content"
1694 };
1695 let button_id = ElementId::NamedChild(
1696 Arc::new(ElementId::from(("wrap-code-block", markdown.entity_id()))),
1697 id.to_string().into(),
1698 );
1699
1700 div()
1701 .id(button_id)
1702 .flex()
1703 .items_center()
1704 .justify_center()
1705 .size(px(20.))
1706 .rounded_xs()
1707 .cursor_pointer()
1708 .child(icon_svg(
1709 IconName::ArrowRightLeft,
1710 px(14.),
1711 gpui::rgb(0xA0AEC0),
1712 ))
1713 .tooltip(simple_tooltip(tooltip))
1714 .on_click(move |_event, _window, cx| {
1715 markdown.update(cx, |markdown, cx| {
1716 markdown.toggle_code_block_wrap(id);
1717 cx.notify();
1718 });
1719 })
1720}
1721
1722fn render_copy_code_block_button(
1723 id: usize,
1724 code: String,
1725 markdown: Entity<Markdown>,
1726) -> impl IntoElement {
1727 let id = ElementId::NamedChild(
1728 Arc::new(ElementId::from((
1729 "copy-markdown-code",
1730 markdown.entity_id(),
1731 ))),
1732 id.to_string().into(),
1733 );
1734
1735 CopyButton::new(id, code)
1736}
1737
1738impl IntoElement for MarkdownElement {
1739 type Element = Self;
1740
1741 fn into_element(self) -> Self::Element {
1742 self
1743 }
1744}
1745
1746pub enum AnyDiv {
1747 Div(Div),
1748 Stateful(Stateful<Div>),
1749}
1750
1751impl AnyDiv {
1752 pub(crate) fn into_any_element(self) -> AnyElement {
1753 match self {
1754 Self::Div(div) => div.into_any_element(),
1755 Self::Stateful(div) => div.into_any_element(),
1756 }
1757 }
1758}
1759
1760impl From<Div> for AnyDiv {
1761 fn from(value: Div) -> Self {
1762 Self::Div(value)
1763 }
1764}
1765
1766impl From<Stateful<Div>> for AnyDiv {
1767 fn from(value: Stateful<Div>) -> Self {
1768 Self::Stateful(value)
1769 }
1770}
1771
1772impl Styled for AnyDiv {
1773 fn style(&mut self) -> &mut StyleRefinement {
1774 match self {
1775 Self::Div(div) => div.style(),
1776 Self::Stateful(div) => div.style(),
1777 }
1778 }
1779}
1780
1781impl ParentElement for AnyDiv {
1782 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1783 match self {
1784 Self::Div(div) => div.extend(elements),
1785 Self::Stateful(div) => div.extend(elements),
1786 }
1787 }
1788}