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