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