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