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