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