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