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