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