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