1pub mod parser;
2mod path_range;
3
4use base64::Engine as _;
5use futures::FutureExt as _;
6use language::LanguageName;
7use log::Level;
8pub use path_range::{LineCol, PathWithRange};
9
10use std::borrow::Cow;
11use std::collections::HashMap;
12use std::collections::HashSet;
13use std::iter;
14use std::mem;
15use std::ops::Range;
16use std::path::Path;
17use std::rc::Rc;
18use std::sync::Arc;
19use std::time::Duration;
20
21use gpui::{
22 AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity,
23 FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image,
24 ImageFormat, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent,
25 Point, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun,
26 TextStyle, TextStyleRefinement, actions, img, point, quad,
27};
28use language::{Language, LanguageRegistry, Rope};
29use parser::CodeBlockMetadata;
30use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown};
31use pulldown_cmark::Alignment;
32use sum_tree::TreeMap;
33use theme::SyntaxTheme;
34use ui::{Tooltip, prelude::*};
35use util::ResultExt;
36
37use crate::parser::CodeBlockKind;
38
39/// A callback function that can be used to customize the style of links based on the destination URL.
40/// If the callback returns `None`, the default link style will be used.
41type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>;
42
43/// Defines custom style refinements for each heading level (H1-H6)
44#[derive(Clone, Default)]
45pub struct HeadingLevelStyles {
46 pub h1: Option<TextStyleRefinement>,
47 pub h2: Option<TextStyleRefinement>,
48 pub h3: Option<TextStyleRefinement>,
49 pub h4: Option<TextStyleRefinement>,
50 pub h5: Option<TextStyleRefinement>,
51 pub h6: Option<TextStyleRefinement>,
52}
53
54#[derive(Clone)]
55pub struct MarkdownStyle {
56 pub base_text_style: TextStyle,
57 pub code_block: StyleRefinement,
58 pub code_block_overflow_x_scroll: bool,
59 pub inline_code: TextStyleRefinement,
60 pub block_quote: TextStyleRefinement,
61 pub link: TextStyleRefinement,
62 pub link_callback: Option<LinkStyleCallback>,
63 pub rule_color: Hsla,
64 pub block_quote_border_color: Hsla,
65 pub syntax: Arc<SyntaxTheme>,
66 pub selection_background_color: Hsla,
67 pub heading: StyleRefinement,
68 pub heading_level_styles: Option<HeadingLevelStyles>,
69 pub table_overflow_x_scroll: bool,
70 pub height_is_multiple_of_line_height: bool,
71}
72
73impl Default for MarkdownStyle {
74 fn default() -> Self {
75 Self {
76 base_text_style: Default::default(),
77 code_block: Default::default(),
78 code_block_overflow_x_scroll: false,
79 inline_code: Default::default(),
80 block_quote: Default::default(),
81 link: Default::default(),
82 link_callback: None,
83 rule_color: Default::default(),
84 block_quote_border_color: Default::default(),
85 syntax: Arc::new(SyntaxTheme::default()),
86 selection_background_color: Default::default(),
87 heading: Default::default(),
88 heading_level_styles: None,
89 table_overflow_x_scroll: false,
90 height_is_multiple_of_line_height: false,
91 }
92 }
93}
94
95pub struct Markdown {
96 source: SharedString,
97 selection: Selection,
98 pressed_link: Option<RenderedLink>,
99 autoscroll_request: Option<usize>,
100 parsed_markdown: ParsedMarkdown,
101 images_by_source_offset: HashMap<usize, Arc<Image>>,
102 should_reparse: bool,
103 pending_parse: Option<Task<()>>,
104 focus_handle: FocusHandle,
105 language_registry: Option<Arc<LanguageRegistry>>,
106 fallback_code_block_language: Option<LanguageName>,
107 options: Options,
108 copied_code_blocks: HashSet<ElementId>,
109}
110
111struct Options {
112 parse_links_only: bool,
113}
114
115pub enum CodeBlockRenderer {
116 Default {
117 copy_button: bool,
118 copy_button_on_hover: bool,
119 border: bool,
120 },
121 Custom {
122 render: CodeBlockRenderFn,
123 /// A function that can modify the parent container after the code block
124 /// content has been appended as a child element.
125 transform: Option<CodeBlockTransformFn>,
126 },
127}
128
129pub type CodeBlockRenderFn = Arc<
130 dyn Fn(
131 &CodeBlockKind,
132 &ParsedMarkdown,
133 Range<usize>,
134 CodeBlockMetadata,
135 &mut Window,
136 &App,
137 ) -> Div,
138>;
139
140pub type CodeBlockTransformFn =
141 Arc<dyn Fn(AnyDiv, Range<usize>, CodeBlockMetadata, &mut Window, &App) -> AnyDiv>;
142
143actions!(markdown, [Copy, CopyAsMarkdown]);
144
145impl Markdown {
146 pub fn new(
147 source: SharedString,
148 language_registry: Option<Arc<LanguageRegistry>>,
149 fallback_code_block_language: Option<LanguageName>,
150 cx: &mut Context<Self>,
151 ) -> Self {
152 let focus_handle = cx.focus_handle();
153 let mut this = Self {
154 source,
155 selection: Selection::default(),
156 pressed_link: None,
157 autoscroll_request: None,
158 should_reparse: false,
159 images_by_source_offset: Default::default(),
160 parsed_markdown: ParsedMarkdown::default(),
161 pending_parse: None,
162 focus_handle,
163 language_registry,
164 fallback_code_block_language,
165 options: Options {
166 parse_links_only: false,
167 },
168 copied_code_blocks: HashSet::new(),
169 };
170 this.parse(cx);
171 this
172 }
173
174 pub fn new_text(source: SharedString, cx: &mut Context<Self>) -> Self {
175 let focus_handle = cx.focus_handle();
176 let mut this = Self {
177 source,
178 selection: Selection::default(),
179 pressed_link: None,
180 autoscroll_request: None,
181 should_reparse: false,
182 parsed_markdown: ParsedMarkdown::default(),
183 images_by_source_offset: Default::default(),
184 pending_parse: None,
185 focus_handle,
186 language_registry: None,
187 fallback_code_block_language: None,
188 options: Options {
189 parse_links_only: true,
190 },
191 copied_code_blocks: HashSet::new(),
192 };
193 this.parse(cx);
194 this
195 }
196
197 pub fn is_parsing(&self) -> bool {
198 self.pending_parse.is_some()
199 }
200
201 pub fn source(&self) -> &str {
202 &self.source
203 }
204
205 pub fn append(&mut self, text: &str, cx: &mut Context<Self>) {
206 self.source = SharedString::new(self.source.to_string() + text);
207 self.parse(cx);
208 }
209
210 pub fn replace(&mut self, source: impl Into<SharedString>, cx: &mut Context<Self>) {
211 self.source = source.into();
212 self.parse(cx);
213 }
214
215 pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) {
216 if source == self.source() {
217 return;
218 }
219 self.source = source;
220 self.selection = Selection::default();
221 self.autoscroll_request = None;
222 self.pending_parse = None;
223 self.should_reparse = false;
224 self.parsed_markdown = ParsedMarkdown::default();
225 self.parse(cx);
226 }
227
228 #[cfg(feature = "test-support")]
229 pub fn parsed_markdown(&self) -> &ParsedMarkdown {
230 &self.parsed_markdown
231 }
232
233 pub fn escape(s: &str) -> Cow<str> {
234 // Valid to use bytes since multi-byte UTF-8 doesn't use ASCII chars.
235 let count = s
236 .bytes()
237 .filter(|c| *c == b'\n' || c.is_ascii_punctuation())
238 .count();
239 if count > 0 {
240 let mut output = String::with_capacity(s.len() + count);
241 let mut is_newline = false;
242 for c in s.chars() {
243 if is_newline && c == ' ' {
244 continue;
245 }
246 is_newline = c == '\n';
247 if c == '\n' {
248 output.push('\n')
249 } else if c.is_ascii_punctuation() {
250 output.push('\\')
251 }
252 output.push(c)
253 }
254 output.into()
255 } else {
256 s.into()
257 }
258 }
259
260 fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
261 if self.selection.end <= self.selection.start {
262 return;
263 }
264 let text = text.text_for_range(self.selection.start..self.selection.end);
265 cx.write_to_clipboard(ClipboardItem::new_string(text));
266 }
267
268 fn copy_as_markdown(&self, _: &mut Window, cx: &mut Context<Self>) {
269 if self.selection.end <= self.selection.start {
270 return;
271 }
272 let text = self.source[self.selection.start..self.selection.end].to_string();
273 cx.write_to_clipboard(ClipboardItem::new_string(text));
274 }
275
276 fn parse(&mut self, cx: &mut Context<Self>) {
277 if self.source.is_empty() {
278 return;
279 }
280
281 if self.pending_parse.is_some() {
282 self.should_reparse = true;
283 return;
284 }
285 self.should_reparse = false;
286 self.pending_parse = Some(self.start_background_parse(cx));
287 }
288
289 fn start_background_parse(&self, cx: &Context<Self>) -> Task<()> {
290 let source = self.source.clone();
291 let should_parse_links_only = self.options.parse_links_only;
292 let language_registry = self.language_registry.clone();
293 let fallback = self.fallback_code_block_language.clone();
294
295 let parsed = cx.background_spawn(async move {
296 if should_parse_links_only {
297 return (
298 ParsedMarkdown {
299 events: Arc::from(parse_links_only(source.as_ref())),
300 source,
301 languages_by_name: TreeMap::default(),
302 languages_by_path: TreeMap::default(),
303 },
304 Default::default(),
305 );
306 }
307
308 let (events, language_names, paths) = parse_markdown(&source);
309 let mut images_by_source_offset = HashMap::default();
310 let mut languages_by_name = TreeMap::default();
311 let mut languages_by_path = TreeMap::default();
312 if let Some(registry) = language_registry.as_ref() {
313 for name in language_names {
314 let language = if !name.is_empty() {
315 registry.language_for_name_or_extension(&name).left_future()
316 } else if let Some(fallback) = &fallback {
317 registry.language_for_name(fallback.as_ref()).right_future()
318 } else {
319 continue;
320 };
321 if let Ok(language) = language.await {
322 languages_by_name.insert(name, language);
323 }
324 }
325
326 for path in paths {
327 if let Ok(language) = registry.language_for_file_path(&path).await {
328 languages_by_path.insert(path, language);
329 }
330 }
331 }
332
333 for (range, event) in &events {
334 if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event {
335 if let Some(data_url) = dest_url.strip_prefix("data:") {
336 let Some((mime_info, data)) = data_url.split_once(',') else {
337 continue;
338 };
339 let Some((mime_type, encoding)) = mime_info.split_once(';') else {
340 continue;
341 };
342 let Some(format) = ImageFormat::from_mime_type(mime_type) else {
343 continue;
344 };
345 let is_base64 = encoding == "base64";
346 if is_base64 {
347 if let Some(bytes) = base64::prelude::BASE64_STANDARD
348 .decode(data)
349 .log_with_level(Level::Debug)
350 {
351 let image = Arc::new(Image::from_bytes(format, bytes));
352 images_by_source_offset.insert(range.start, image);
353 }
354 }
355 }
356 }
357 }
358
359 (
360 ParsedMarkdown {
361 source,
362 events: Arc::from(events),
363 languages_by_name,
364 languages_by_path,
365 },
366 images_by_source_offset,
367 )
368 });
369
370 cx.spawn(async move |this, cx| {
371 let (parsed, images_by_source_offset) = parsed.await;
372
373 this.update(cx, |this, cx| {
374 this.parsed_markdown = parsed;
375 this.images_by_source_offset = images_by_source_offset;
376 this.pending_parse.take();
377 if this.should_reparse {
378 this.parse(cx);
379 }
380 cx.refresh_windows();
381 })
382 .ok();
383 })
384 }
385}
386
387impl Focusable for Markdown {
388 fn focus_handle(&self, _cx: &App) -> FocusHandle {
389 self.focus_handle.clone()
390 }
391}
392
393#[derive(Copy, Clone, Default, Debug)]
394struct Selection {
395 start: usize,
396 end: usize,
397 reversed: bool,
398 pending: bool,
399}
400
401impl Selection {
402 fn set_head(&mut self, head: usize) {
403 if head < self.tail() {
404 if !self.reversed {
405 self.end = self.start;
406 self.reversed = true;
407 }
408 self.start = head;
409 } else {
410 if self.reversed {
411 self.start = self.end;
412 self.reversed = false;
413 }
414 self.end = head;
415 }
416 }
417
418 fn tail(&self) -> usize {
419 if self.reversed { self.end } else { self.start }
420 }
421}
422
423#[derive(Clone, Default)]
424pub struct ParsedMarkdown {
425 pub source: SharedString,
426 pub events: Arc<[(Range<usize>, MarkdownEvent)]>,
427 pub languages_by_name: TreeMap<SharedString, Arc<Language>>,
428 pub languages_by_path: TreeMap<Arc<Path>, Arc<Language>>,
429}
430
431impl ParsedMarkdown {
432 pub fn source(&self) -> &SharedString {
433 &self.source
434 }
435
436 pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
437 &self.events
438 }
439}
440
441pub struct MarkdownElement {
442 markdown: Entity<Markdown>,
443 style: MarkdownStyle,
444 code_block_renderer: CodeBlockRenderer,
445 on_url_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
446}
447
448impl MarkdownElement {
449 pub fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
450 Self {
451 markdown,
452 style,
453 code_block_renderer: CodeBlockRenderer::Default {
454 copy_button: true,
455 copy_button_on_hover: false,
456 border: false,
457 },
458 on_url_click: None,
459 }
460 }
461
462 #[cfg(any(test, feature = "test-support"))]
463 pub fn rendered_text(
464 markdown: Entity<Markdown>,
465 cx: &mut gpui::VisualTestContext,
466 style: impl FnOnce(&Window, &App) -> MarkdownStyle,
467 ) -> String {
468 use gpui::size;
469
470 let (text, _) = cx.draw(
471 Default::default(),
472 size(px(600.0), px(600.0)),
473 |window, cx| Self::new(markdown, style(window, cx)),
474 );
475 text.text
476 .lines
477 .iter()
478 .map(|line| line.layout.wrapped_text())
479 .collect::<Vec<_>>()
480 .join("\n")
481 }
482
483 pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self {
484 self.code_block_renderer = variant;
485 self
486 }
487
488 pub fn on_url_click(
489 mut self,
490 handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
491 ) -> Self {
492 self.on_url_click = Some(Box::new(handler));
493 self
494 }
495
496 fn paint_selection(
497 &self,
498 bounds: Bounds<Pixels>,
499 rendered_text: &RenderedText,
500 window: &mut Window,
501 cx: &mut App,
502 ) {
503 let selection = self.markdown.read(cx).selection;
504 let selection_start = rendered_text.position_for_source_index(selection.start);
505 let selection_end = rendered_text.position_for_source_index(selection.end);
506
507 if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
508 selection_start.zip(selection_end)
509 {
510 if start_position.y == end_position.y {
511 window.paint_quad(quad(
512 Bounds::from_corners(
513 start_position,
514 point(end_position.x, end_position.y + end_line_height),
515 ),
516 Pixels::ZERO,
517 self.style.selection_background_color,
518 Edges::default(),
519 Hsla::transparent_black(),
520 BorderStyle::default(),
521 ));
522 } else {
523 window.paint_quad(quad(
524 Bounds::from_corners(
525 start_position,
526 point(bounds.right(), start_position.y + start_line_height),
527 ),
528 Pixels::ZERO,
529 self.style.selection_background_color,
530 Edges::default(),
531 Hsla::transparent_black(),
532 BorderStyle::default(),
533 ));
534
535 if end_position.y > start_position.y + start_line_height {
536 window.paint_quad(quad(
537 Bounds::from_corners(
538 point(bounds.left(), start_position.y + start_line_height),
539 point(bounds.right(), end_position.y),
540 ),
541 Pixels::ZERO,
542 self.style.selection_background_color,
543 Edges::default(),
544 Hsla::transparent_black(),
545 BorderStyle::default(),
546 ));
547 }
548
549 window.paint_quad(quad(
550 Bounds::from_corners(
551 point(bounds.left(), end_position.y),
552 point(end_position.x, end_position.y + end_line_height),
553 ),
554 Pixels::ZERO,
555 self.style.selection_background_color,
556 Edges::default(),
557 Hsla::transparent_black(),
558 BorderStyle::default(),
559 ));
560 }
561 }
562 }
563
564 fn paint_mouse_listeners(
565 &mut self,
566 hitbox: &Hitbox,
567 rendered_text: &RenderedText,
568 window: &mut Window,
569 cx: &mut App,
570 ) {
571 let is_hovering_link = hitbox.is_hovered(window)
572 && !self.markdown.read(cx).selection.pending
573 && rendered_text
574 .link_for_position(window.mouse_position())
575 .is_some();
576
577 if is_hovering_link {
578 window.set_cursor_style(CursorStyle::PointingHand, Some(hitbox));
579 } else {
580 window.set_cursor_style(CursorStyle::IBeam, Some(hitbox));
581 }
582
583 let on_open_url = self.on_url_click.take();
584
585 self.on_mouse_event(window, cx, {
586 let rendered_text = rendered_text.clone();
587 let hitbox = hitbox.clone();
588 move |markdown, event: &MouseDownEvent, phase, window, cx| {
589 if hitbox.is_hovered(window) {
590 if phase.bubble() {
591 if let Some(link) = rendered_text.link_for_position(event.position) {
592 markdown.pressed_link = Some(link.clone());
593 } else {
594 let source_index =
595 match rendered_text.source_index_for_position(event.position) {
596 Ok(ix) | Err(ix) => ix,
597 };
598 let range = if event.click_count == 2 {
599 rendered_text.surrounding_word_range(source_index)
600 } else if event.click_count == 3 {
601 rendered_text.surrounding_line_range(source_index)
602 } else {
603 source_index..source_index
604 };
605 markdown.selection = Selection {
606 start: range.start,
607 end: range.end,
608 reversed: false,
609 pending: true,
610 };
611 window.focus(&markdown.focus_handle);
612 }
613
614 window.prevent_default();
615 cx.notify();
616 }
617 } else if phase.capture() {
618 markdown.selection = Selection::default();
619 markdown.pressed_link = None;
620 cx.notify();
621 }
622 }
623 });
624 self.on_mouse_event(window, cx, {
625 let rendered_text = rendered_text.clone();
626 let hitbox = hitbox.clone();
627 let was_hovering_link = is_hovering_link;
628 move |markdown, event: &MouseMoveEvent, phase, window, cx| {
629 if phase.capture() {
630 return;
631 }
632
633 if markdown.selection.pending {
634 let source_index = match rendered_text.source_index_for_position(event.position)
635 {
636 Ok(ix) | Err(ix) => ix,
637 };
638 markdown.selection.set_head(source_index);
639 markdown.autoscroll_request = Some(source_index);
640 cx.notify();
641 } else {
642 let is_hovering_link = hitbox.is_hovered(window)
643 && rendered_text.link_for_position(event.position).is_some();
644 if is_hovering_link != was_hovering_link {
645 cx.notify();
646 }
647 }
648 }
649 });
650 self.on_mouse_event(window, cx, {
651 let rendered_text = rendered_text.clone();
652 move |markdown, event: &MouseUpEvent, phase, window, cx| {
653 if phase.bubble() {
654 if let Some(pressed_link) = markdown.pressed_link.take() {
655 if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
656 if let Some(open_url) = on_open_url.as_ref() {
657 open_url(pressed_link.destination_url, window, cx);
658 } else {
659 cx.open_url(&pressed_link.destination_url);
660 }
661 }
662 }
663 } else if markdown.selection.pending {
664 markdown.selection.pending = false;
665 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
666 {
667 let text = rendered_text
668 .text_for_range(markdown.selection.start..markdown.selection.end);
669 cx.write_to_primary(ClipboardItem::new_string(text))
670 }
671 cx.notify();
672 }
673 }
674 });
675 }
676
677 fn autoscroll(
678 &self,
679 rendered_text: &RenderedText,
680 window: &mut Window,
681 cx: &mut App,
682 ) -> Option<()> {
683 let autoscroll_index = self
684 .markdown
685 .update(cx, |markdown, _| markdown.autoscroll_request.take())?;
686 let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
687
688 let text_style = self.style.base_text_style.clone();
689 let font_id = window.text_system().resolve_font(&text_style.font());
690 let font_size = text_style.font_size.to_pixels(window.rem_size());
691 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
692 window.request_autoscroll(Bounds::from_corners(
693 point(position.x - 3. * em_width, position.y - 3. * line_height),
694 point(position.x + 3. * em_width, position.y + 3. * line_height),
695 ));
696 Some(())
697 }
698
699 fn on_mouse_event<T: MouseEvent>(
700 &self,
701 window: &mut Window,
702 _cx: &mut App,
703 mut f: impl 'static
704 + FnMut(&mut Markdown, &T, DispatchPhase, &mut Window, &mut Context<Markdown>),
705 ) {
706 window.on_mouse_event({
707 let markdown = self.markdown.downgrade();
708 move |event, phase, window, cx| {
709 markdown
710 .update(cx, |markdown, cx| f(markdown, event, phase, window, cx))
711 .log_err();
712 }
713 });
714 }
715}
716
717impl Element for MarkdownElement {
718 type RequestLayoutState = RenderedMarkdown;
719 type PrepaintState = Hitbox;
720
721 fn id(&self) -> Option<ElementId> {
722 None
723 }
724
725 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
726 None
727 }
728
729 fn request_layout(
730 &mut self,
731 _id: Option<&GlobalElementId>,
732 _inspector_id: Option<&gpui::InspectorElementId>,
733 window: &mut Window,
734 cx: &mut App,
735 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
736 let mut builder = MarkdownElementBuilder::new(
737 self.style.base_text_style.clone(),
738 self.style.syntax.clone(),
739 );
740 let markdown = self.markdown.read(cx);
741 let parsed_markdown = &markdown.parsed_markdown;
742 let images = &markdown.images_by_source_offset;
743 let markdown_end = if let Some(last) = parsed_markdown.events.last() {
744 last.0.end
745 } else {
746 0
747 };
748
749 let mut current_code_block_metadata = None;
750 let mut current_img_block_range: Option<Range<usize>> = None;
751 for (range, event) in parsed_markdown.events.iter() {
752 // Skip alt text for images that rendered
753 if let Some(current_img_block_range) = ¤t_img_block_range {
754 if current_img_block_range.end > range.end {
755 continue;
756 }
757 }
758
759 match event {
760 MarkdownEvent::Start(tag) => {
761 match tag {
762 MarkdownTag::Image { .. } => {
763 if let Some(image) = images.get(&range.start) {
764 current_img_block_range = Some(range.clone());
765 builder.modify_current_div(|el| {
766 el.items_center()
767 .flex()
768 .flex_row()
769 .child(img(image.clone()))
770 });
771 }
772 }
773 MarkdownTag::Paragraph => {
774 builder.push_div(
775 div().when(!self.style.height_is_multiple_of_line_height, |el| {
776 el.mb_2().line_height(rems(1.3))
777 }),
778 range,
779 markdown_end,
780 );
781 }
782 MarkdownTag::Heading { level, .. } => {
783 let mut heading = div().mb_2();
784
785 heading = apply_heading_style(
786 heading,
787 *level,
788 self.style.heading_level_styles.as_ref(),
789 );
790
791 heading.style().refine(&self.style.heading);
792
793 let text_style =
794 self.style.heading.text_style().clone().unwrap_or_default();
795
796 builder.push_text_style(text_style);
797 builder.push_div(heading, range, markdown_end);
798 }
799 MarkdownTag::BlockQuote => {
800 builder.push_text_style(self.style.block_quote.clone());
801 builder.push_div(
802 div()
803 .pl_4()
804 .mb_2()
805 .border_l_4()
806 .border_color(self.style.block_quote_border_color),
807 range,
808 markdown_end,
809 );
810 }
811 MarkdownTag::CodeBlock { kind, metadata } => {
812 let language = match kind {
813 CodeBlockKind::Fenced => None,
814 CodeBlockKind::FencedLang(language) => {
815 parsed_markdown.languages_by_name.get(language).cloned()
816 }
817 CodeBlockKind::FencedSrc(path_range) => parsed_markdown
818 .languages_by_path
819 .get(&path_range.path)
820 .cloned(),
821 _ => None,
822 };
823
824 current_code_block_metadata = Some(metadata.clone());
825
826 let is_indented = matches!(kind, CodeBlockKind::Indented);
827
828 match (&self.code_block_renderer, is_indented) {
829 (CodeBlockRenderer::Default { .. }, _) | (_, true) => {
830 // This is a parent container that we can position the copy button inside.
831 builder.push_div(
832 div().group("code_block").relative().w_full(),
833 range,
834 markdown_end,
835 );
836
837 let mut code_block = div()
838 .id(("code-block", range.start))
839 .rounded_lg()
840 .map(|mut code_block| {
841 if self.style.code_block_overflow_x_scroll {
842 code_block.style().restrict_scroll_to_axis =
843 Some(true);
844 code_block.flex().overflow_x_scroll()
845 } else {
846 code_block.w_full()
847 }
848 });
849
850 if let CodeBlockRenderer::Default { border: true, .. } =
851 &self.code_block_renderer
852 {
853 code_block = code_block
854 .rounded_md()
855 .border_1()
856 .border_color(cx.theme().colors().border_variant);
857 }
858
859 code_block.style().refine(&self.style.code_block);
860 if let Some(code_block_text_style) = &self.style.code_block.text
861 {
862 builder.push_text_style(code_block_text_style.to_owned());
863 }
864 builder.push_code_block(language);
865 builder.push_div(code_block, range, markdown_end);
866 }
867 (CodeBlockRenderer::Custom { render, .. }, _) => {
868 let parent_container = render(
869 kind,
870 &parsed_markdown,
871 range.clone(),
872 metadata.clone(),
873 window,
874 cx,
875 );
876
877 builder.push_div(parent_container, range, markdown_end);
878
879 let mut code_block = div()
880 .id(("code-block", range.start))
881 .rounded_b_lg()
882 .map(|mut code_block| {
883 if self.style.code_block_overflow_x_scroll {
884 code_block.style().restrict_scroll_to_axis =
885 Some(true);
886 code_block
887 .flex()
888 .overflow_x_scroll()
889 .overflow_y_hidden()
890 } else {
891 code_block.w_full().overflow_hidden()
892 }
893 });
894
895 code_block.style().refine(&self.style.code_block);
896
897 if let Some(code_block_text_style) = &self.style.code_block.text
898 {
899 builder.push_text_style(code_block_text_style.to_owned());
900 }
901
902 builder.push_code_block(language);
903 builder.push_div(code_block, range, markdown_end);
904 }
905 }
906 }
907 MarkdownTag::HtmlBlock => builder.push_div(div(), range, markdown_end),
908 MarkdownTag::List(bullet_index) => {
909 builder.push_list(*bullet_index);
910 builder.push_div(div().pl_4(), range, markdown_end);
911 }
912 MarkdownTag::Item => {
913 let bullet = if let Some(bullet_index) = builder.next_bullet_index() {
914 format!("{}.", bullet_index)
915 } else {
916 "•".to_string()
917 };
918 builder.push_div(
919 div()
920 .when(!self.style.height_is_multiple_of_line_height, |el| {
921 el.mb_1().gap_1().line_height(rems(1.3))
922 })
923 .h_flex()
924 .items_start()
925 .child(bullet),
926 range,
927 markdown_end,
928 );
929 // Without `w_0`, text doesn't wrap to the width of the container.
930 builder.push_div(div().flex_1().w_0(), range, markdown_end);
931 }
932 MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
933 font_style: Some(FontStyle::Italic),
934 ..Default::default()
935 }),
936 MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
937 font_weight: Some(FontWeight::BOLD),
938 ..Default::default()
939 }),
940 MarkdownTag::Strikethrough => {
941 builder.push_text_style(TextStyleRefinement {
942 strikethrough: Some(StrikethroughStyle {
943 thickness: px(1.),
944 color: None,
945 }),
946 ..Default::default()
947 })
948 }
949 MarkdownTag::Link { dest_url, .. } => {
950 if builder.code_block_stack.is_empty() {
951 builder.push_link(dest_url.clone(), range.clone());
952 let style = self
953 .style
954 .link_callback
955 .as_ref()
956 .and_then(|callback| callback(dest_url, cx))
957 .unwrap_or_else(|| self.style.link.clone());
958 builder.push_text_style(style)
959 }
960 }
961 MarkdownTag::MetadataBlock(_) => {}
962 MarkdownTag::Table(alignments) => {
963 builder.table_alignments = alignments.clone();
964 builder.push_div(
965 div()
966 .id(("table", range.start))
967 .flex()
968 .border_1()
969 .border_color(cx.theme().colors().border)
970 .rounded_sm()
971 .when(self.style.table_overflow_x_scroll, |mut table| {
972 table.style().restrict_scroll_to_axis = Some(true);
973 table.overflow_x_scroll()
974 }),
975 range,
976 markdown_end,
977 );
978 // This inner `v_flex` is so the table rows will stack vertically without disrupting the `overflow_x_scroll`.
979 builder.push_div(div().v_flex().flex_grow(), range, markdown_end);
980 }
981 MarkdownTag::TableHead => {
982 builder.push_div(
983 div()
984 .flex()
985 .justify_between()
986 .border_b_1()
987 .border_color(cx.theme().colors().border),
988 range,
989 markdown_end,
990 );
991 builder.push_text_style(TextStyleRefinement {
992 font_weight: Some(FontWeight::BOLD),
993 ..Default::default()
994 });
995 }
996 MarkdownTag::TableRow => {
997 builder.push_div(
998 div().h_flex().justify_between().px_1().py_0p5(),
999 range,
1000 markdown_end,
1001 );
1002 }
1003 MarkdownTag::TableCell => {
1004 let column_count = builder.table_alignments.len();
1005
1006 builder.push_div(
1007 div()
1008 .flex()
1009 .px_1()
1010 .w(relative(1. / column_count as f32))
1011 .truncate(),
1012 range,
1013 markdown_end,
1014 );
1015 }
1016 _ => log::debug!("unsupported markdown tag {:?}", tag),
1017 }
1018 }
1019 MarkdownEvent::End(tag) => match tag {
1020 MarkdownTagEnd::Image => {
1021 current_img_block_range.take();
1022 }
1023 MarkdownTagEnd::Paragraph => {
1024 builder.pop_div();
1025 }
1026 MarkdownTagEnd::Heading(_) => {
1027 builder.pop_div();
1028 builder.pop_text_style()
1029 }
1030 MarkdownTagEnd::BlockQuote(_kind) => {
1031 builder.pop_text_style();
1032 builder.pop_div()
1033 }
1034 MarkdownTagEnd::CodeBlock => {
1035 builder.trim_trailing_newline();
1036
1037 builder.pop_div();
1038 builder.pop_code_block();
1039 if self.style.code_block.text.is_some() {
1040 builder.pop_text_style();
1041 }
1042
1043 let metadata = current_code_block_metadata.take();
1044
1045 if let CodeBlockRenderer::Custom {
1046 transform: Some(transform),
1047 ..
1048 } = &self.code_block_renderer
1049 {
1050 builder.modify_current_div(|el| {
1051 transform(
1052 el,
1053 range.clone(),
1054 metadata.clone().unwrap_or_default(),
1055 window,
1056 cx,
1057 )
1058 });
1059 }
1060
1061 if let CodeBlockRenderer::Default {
1062 copy_button: true, ..
1063 } = &self.code_block_renderer
1064 {
1065 builder.modify_current_div(|el| {
1066 let content_range = parser::extract_code_block_content_range(
1067 parsed_markdown.source()[range.clone()].trim(),
1068 );
1069 let content_range = content_range.start + range.start
1070 ..content_range.end + range.start;
1071
1072 let code = parsed_markdown.source()[content_range].to_string();
1073 let codeblock = render_copy_code_block_button(
1074 range.end,
1075 code,
1076 self.markdown.clone(),
1077 cx,
1078 );
1079 el.child(div().absolute().top_1().right_1().w_5().child(codeblock))
1080 });
1081 }
1082
1083 if let CodeBlockRenderer::Default {
1084 copy_button_on_hover: true,
1085 ..
1086 } = &self.code_block_renderer
1087 {
1088 builder.modify_current_div(|el| {
1089 let content_range = parser::extract_code_block_content_range(
1090 parsed_markdown.source()[range.clone()].trim(),
1091 );
1092 let content_range = content_range.start + range.start
1093 ..content_range.end + range.start;
1094
1095 let code = parsed_markdown.source()[content_range].to_string();
1096 let codeblock = render_copy_code_block_button(
1097 range.end,
1098 code,
1099 self.markdown.clone(),
1100 cx,
1101 );
1102 el.child(
1103 div()
1104 .absolute()
1105 .top_0()
1106 .right_0()
1107 .w_5()
1108 .visible_on_hover("code_block")
1109 .child(codeblock),
1110 )
1111 });
1112 }
1113
1114 // Pop the parent container.
1115 builder.pop_div();
1116 }
1117 MarkdownTagEnd::HtmlBlock => builder.pop_div(),
1118 MarkdownTagEnd::List(_) => {
1119 builder.pop_list();
1120 builder.pop_div();
1121 }
1122 MarkdownTagEnd::Item => {
1123 builder.pop_div();
1124 builder.pop_div();
1125 }
1126 MarkdownTagEnd::Emphasis => builder.pop_text_style(),
1127 MarkdownTagEnd::Strong => builder.pop_text_style(),
1128 MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
1129 MarkdownTagEnd::Link => {
1130 if builder.code_block_stack.is_empty() {
1131 builder.pop_text_style()
1132 }
1133 }
1134 MarkdownTagEnd::Table => {
1135 builder.pop_div();
1136 builder.pop_div();
1137 builder.table_alignments.clear();
1138 }
1139 MarkdownTagEnd::TableHead => {
1140 builder.pop_div();
1141 builder.pop_text_style();
1142 }
1143 MarkdownTagEnd::TableRow => {
1144 builder.pop_div();
1145 }
1146 MarkdownTagEnd::TableCell => {
1147 builder.pop_div();
1148 }
1149 _ => log::debug!("unsupported markdown tag end: {:?}", tag),
1150 },
1151 MarkdownEvent::Text => {
1152 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1153 }
1154 MarkdownEvent::SubstitutedText(text) => {
1155 builder.push_text(text, range.clone());
1156 }
1157 MarkdownEvent::Code => {
1158 builder.push_text_style(self.style.inline_code.clone());
1159 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1160 builder.pop_text_style();
1161 }
1162 MarkdownEvent::Html => {
1163 let html = &parsed_markdown.source[range.clone()];
1164 if html.starts_with("<!--") {
1165 builder.html_comment = true;
1166 }
1167 if html.trim_end().ends_with("-->") {
1168 builder.html_comment = false;
1169 continue;
1170 }
1171 if builder.html_comment {
1172 continue;
1173 }
1174 builder.push_text(html, range.clone());
1175 }
1176 MarkdownEvent::InlineHtml => {
1177 builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1178 }
1179 MarkdownEvent::Rule => {
1180 builder.push_div(
1181 div()
1182 .border_b_1()
1183 .my_2()
1184 .border_color(self.style.rule_color),
1185 range,
1186 markdown_end,
1187 );
1188 builder.pop_div()
1189 }
1190 MarkdownEvent::SoftBreak => builder.push_text(" ", range.clone()),
1191 MarkdownEvent::HardBreak => builder.push_text("\n", range.clone()),
1192 _ => log::error!("unsupported markdown event {:?}", event),
1193 }
1194 }
1195 let mut rendered_markdown = builder.build();
1196 let child_layout_id = rendered_markdown.element.request_layout(window, cx);
1197 let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);
1198 (layout_id, rendered_markdown)
1199 }
1200
1201 fn prepaint(
1202 &mut self,
1203 _id: Option<&GlobalElementId>,
1204 _inspector_id: Option<&gpui::InspectorElementId>,
1205 bounds: Bounds<Pixels>,
1206 rendered_markdown: &mut Self::RequestLayoutState,
1207 window: &mut Window,
1208 cx: &mut App,
1209 ) -> Self::PrepaintState {
1210 let focus_handle = self.markdown.read(cx).focus_handle.clone();
1211 window.set_focus_handle(&focus_handle, cx);
1212
1213 let hitbox = window.insert_hitbox(bounds, false);
1214 rendered_markdown.element.prepaint(window, cx);
1215 self.autoscroll(&rendered_markdown.text, window, cx);
1216 hitbox
1217 }
1218
1219 fn paint(
1220 &mut self,
1221 _id: Option<&GlobalElementId>,
1222 _inspector_id: Option<&gpui::InspectorElementId>,
1223 bounds: Bounds<Pixels>,
1224 rendered_markdown: &mut Self::RequestLayoutState,
1225 hitbox: &mut Self::PrepaintState,
1226 window: &mut Window,
1227 cx: &mut App,
1228 ) {
1229 let mut context = KeyContext::default();
1230 context.add("Markdown");
1231 window.set_key_context(context);
1232 window.on_action(std::any::TypeId::of::<crate::Copy>(), {
1233 let entity = self.markdown.clone();
1234 let text = rendered_markdown.text.clone();
1235 move |_, phase, window, cx| {
1236 let text = text.clone();
1237 if phase == DispatchPhase::Bubble {
1238 entity.update(cx, move |this, cx| this.copy(&text, window, cx))
1239 }
1240 }
1241 });
1242 window.on_action(std::any::TypeId::of::<crate::CopyAsMarkdown>(), {
1243 let entity = self.markdown.clone();
1244 move |_, phase, window, cx| {
1245 if phase == DispatchPhase::Bubble {
1246 entity.update(cx, move |this, cx| this.copy_as_markdown(window, cx))
1247 }
1248 }
1249 });
1250
1251 self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
1252 rendered_markdown.element.paint(window, cx);
1253 self.paint_selection(bounds, &rendered_markdown.text, window, cx);
1254 }
1255}
1256
1257fn apply_heading_style(
1258 mut heading: Div,
1259 level: pulldown_cmark::HeadingLevel,
1260 custom_styles: Option<&HeadingLevelStyles>,
1261) -> Div {
1262 heading = match level {
1263 pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(),
1264 pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(),
1265 pulldown_cmark::HeadingLevel::H3 => heading.text_xl(),
1266 pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
1267 pulldown_cmark::HeadingLevel::H5 => heading.text_base(),
1268 pulldown_cmark::HeadingLevel::H6 => heading.text_sm(),
1269 };
1270
1271 if let Some(styles) = custom_styles {
1272 let style_opt = match level {
1273 pulldown_cmark::HeadingLevel::H1 => &styles.h1,
1274 pulldown_cmark::HeadingLevel::H2 => &styles.h2,
1275 pulldown_cmark::HeadingLevel::H3 => &styles.h3,
1276 pulldown_cmark::HeadingLevel::H4 => &styles.h4,
1277 pulldown_cmark::HeadingLevel::H5 => &styles.h5,
1278 pulldown_cmark::HeadingLevel::H6 => &styles.h6,
1279 };
1280
1281 if let Some(style) = style_opt {
1282 heading.style().text = Some(style.clone());
1283 }
1284 }
1285
1286 heading
1287}
1288
1289fn render_copy_code_block_button(
1290 id: usize,
1291 code: String,
1292 markdown: Entity<Markdown>,
1293 cx: &App,
1294) -> impl IntoElement {
1295 let id = ElementId::named_usize("copy-markdown-code", id);
1296 let was_copied = markdown.read(cx).copied_code_blocks.contains(&id);
1297 IconButton::new(
1298 id.clone(),
1299 if was_copied {
1300 IconName::Check
1301 } else {
1302 IconName::Copy
1303 },
1304 )
1305 .icon_color(Color::Muted)
1306 .shape(ui::IconButtonShape::Square)
1307 .tooltip(Tooltip::text("Copy Code"))
1308 .on_click({
1309 let id = id.clone();
1310 let markdown = markdown.clone();
1311 move |_event, _window, cx| {
1312 let id = id.clone();
1313 markdown.update(cx, |this, cx| {
1314 this.copied_code_blocks.insert(id.clone());
1315
1316 cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
1317
1318 cx.spawn(async move |this, cx| {
1319 cx.background_executor().timer(Duration::from_secs(2)).await;
1320
1321 cx.update(|cx| {
1322 this.update(cx, |this, cx| {
1323 this.copied_code_blocks.remove(&id);
1324 cx.notify();
1325 })
1326 })
1327 .ok();
1328 })
1329 .detach();
1330 });
1331 }
1332 })
1333}
1334
1335impl IntoElement for MarkdownElement {
1336 type Element = Self;
1337
1338 fn into_element(self) -> Self::Element {
1339 self
1340 }
1341}
1342
1343pub enum AnyDiv {
1344 Div(Div),
1345 Stateful(Stateful<Div>),
1346}
1347
1348impl AnyDiv {
1349 fn into_any_element(self) -> AnyElement {
1350 match self {
1351 Self::Div(div) => div.into_any_element(),
1352 Self::Stateful(div) => div.into_any_element(),
1353 }
1354 }
1355}
1356
1357impl From<Div> for AnyDiv {
1358 fn from(value: Div) -> Self {
1359 Self::Div(value)
1360 }
1361}
1362
1363impl From<Stateful<Div>> for AnyDiv {
1364 fn from(value: Stateful<Div>) -> Self {
1365 Self::Stateful(value)
1366 }
1367}
1368
1369impl Styled for AnyDiv {
1370 fn style(&mut self) -> &mut StyleRefinement {
1371 match self {
1372 Self::Div(div) => div.style(),
1373 Self::Stateful(div) => div.style(),
1374 }
1375 }
1376}
1377
1378impl ParentElement for AnyDiv {
1379 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1380 match self {
1381 Self::Div(div) => div.extend(elements),
1382 Self::Stateful(div) => div.extend(elements),
1383 }
1384 }
1385}
1386
1387struct MarkdownElementBuilder {
1388 div_stack: Vec<AnyDiv>,
1389 rendered_lines: Vec<RenderedLine>,
1390 pending_line: PendingLine,
1391 rendered_links: Vec<RenderedLink>,
1392 current_source_index: usize,
1393 html_comment: bool,
1394 base_text_style: TextStyle,
1395 text_style_stack: Vec<TextStyleRefinement>,
1396 code_block_stack: Vec<Option<Arc<Language>>>,
1397 list_stack: Vec<ListStackEntry>,
1398 table_alignments: Vec<Alignment>,
1399 syntax_theme: Arc<SyntaxTheme>,
1400}
1401
1402#[derive(Default)]
1403struct PendingLine {
1404 text: String,
1405 runs: Vec<TextRun>,
1406 source_mappings: Vec<SourceMapping>,
1407}
1408
1409struct ListStackEntry {
1410 bullet_index: Option<u64>,
1411}
1412
1413impl MarkdownElementBuilder {
1414 fn new(base_text_style: TextStyle, syntax_theme: Arc<SyntaxTheme>) -> Self {
1415 Self {
1416 div_stack: vec![div().debug_selector(|| "inner".into()).into()],
1417 rendered_lines: Vec::new(),
1418 pending_line: PendingLine::default(),
1419 rendered_links: Vec::new(),
1420 current_source_index: 0,
1421 html_comment: false,
1422 base_text_style,
1423 text_style_stack: Vec::new(),
1424 code_block_stack: Vec::new(),
1425 list_stack: Vec::new(),
1426 table_alignments: Vec::new(),
1427 syntax_theme,
1428 }
1429 }
1430
1431 fn push_text_style(&mut self, style: TextStyleRefinement) {
1432 self.text_style_stack.push(style);
1433 }
1434
1435 fn text_style(&self) -> TextStyle {
1436 let mut style = self.base_text_style.clone();
1437 for refinement in &self.text_style_stack {
1438 style.refine(refinement);
1439 }
1440 style
1441 }
1442
1443 fn pop_text_style(&mut self) {
1444 self.text_style_stack.pop();
1445 }
1446
1447 fn push_div(&mut self, div: impl Into<AnyDiv>, range: &Range<usize>, markdown_end: usize) {
1448 let mut div = div.into();
1449 self.flush_text();
1450
1451 if range.start == 0 {
1452 // Remove the top margin on the first element.
1453 div.style().refine(&StyleRefinement {
1454 margin: gpui::EdgesRefinement {
1455 top: Some(Length::Definite(px(0.).into())),
1456 left: None,
1457 right: None,
1458 bottom: None,
1459 },
1460 ..Default::default()
1461 });
1462 }
1463
1464 if range.end == markdown_end {
1465 div.style().refine(&StyleRefinement {
1466 margin: gpui::EdgesRefinement {
1467 top: None,
1468 left: None,
1469 right: None,
1470 bottom: Some(Length::Definite(rems(0.).into())),
1471 },
1472 ..Default::default()
1473 });
1474 }
1475
1476 self.div_stack.push(div);
1477 }
1478
1479 fn modify_current_div(&mut self, f: impl FnOnce(AnyDiv) -> AnyDiv) {
1480 self.flush_text();
1481 if let Some(div) = self.div_stack.pop() {
1482 self.div_stack.push(f(div));
1483 }
1484 }
1485
1486 fn pop_div(&mut self) {
1487 self.flush_text();
1488 let div = self.div_stack.pop().unwrap().into_any_element();
1489 self.div_stack.last_mut().unwrap().extend(iter::once(div));
1490 }
1491
1492 fn push_list(&mut self, bullet_index: Option<u64>) {
1493 self.list_stack.push(ListStackEntry { bullet_index });
1494 }
1495
1496 fn next_bullet_index(&mut self) -> Option<u64> {
1497 self.list_stack.last_mut().and_then(|entry| {
1498 let item_index = entry.bullet_index.as_mut()?;
1499 *item_index += 1;
1500 Some(*item_index - 1)
1501 })
1502 }
1503
1504 fn pop_list(&mut self) {
1505 self.list_stack.pop();
1506 }
1507
1508 fn push_code_block(&mut self, language: Option<Arc<Language>>) {
1509 self.code_block_stack.push(language);
1510 }
1511
1512 fn pop_code_block(&mut self) {
1513 self.code_block_stack.pop();
1514 }
1515
1516 fn push_link(&mut self, destination_url: SharedString, source_range: Range<usize>) {
1517 self.rendered_links.push(RenderedLink {
1518 source_range,
1519 destination_url,
1520 });
1521 }
1522
1523 fn push_text(&mut self, text: &str, source_range: Range<usize>) {
1524 self.pending_line.source_mappings.push(SourceMapping {
1525 rendered_index: self.pending_line.text.len(),
1526 source_index: source_range.start,
1527 });
1528 self.pending_line.text.push_str(text);
1529 self.current_source_index = source_range.end;
1530
1531 if let Some(Some(language)) = self.code_block_stack.last() {
1532 let mut offset = 0;
1533 for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
1534 if range.start > offset {
1535 self.pending_line
1536 .runs
1537 .push(self.text_style().to_run(range.start - offset));
1538 }
1539
1540 let mut run_style = self.text_style();
1541 if let Some(highlight) = highlight_id.style(&self.syntax_theme) {
1542 run_style = run_style.highlight(highlight);
1543 }
1544 self.pending_line.runs.push(run_style.to_run(range.len()));
1545 offset = range.end;
1546 }
1547
1548 if offset < text.len() {
1549 self.pending_line
1550 .runs
1551 .push(self.text_style().to_run(text.len() - offset));
1552 }
1553 } else {
1554 self.pending_line
1555 .runs
1556 .push(self.text_style().to_run(text.len()));
1557 }
1558 }
1559
1560 fn trim_trailing_newline(&mut self) {
1561 if self.pending_line.text.ends_with('\n') {
1562 self.pending_line
1563 .text
1564 .truncate(self.pending_line.text.len() - 1);
1565 self.pending_line.runs.last_mut().unwrap().len -= 1;
1566 self.current_source_index -= 1;
1567 }
1568 }
1569
1570 fn flush_text(&mut self) {
1571 let line = mem::take(&mut self.pending_line);
1572 if line.text.is_empty() {
1573 return;
1574 }
1575
1576 let text = StyledText::new(line.text).with_runs(line.runs);
1577 self.rendered_lines.push(RenderedLine {
1578 layout: text.layout().clone(),
1579 source_mappings: line.source_mappings,
1580 source_end: self.current_source_index,
1581 });
1582 self.div_stack.last_mut().unwrap().extend([text.into_any()]);
1583 }
1584
1585 fn build(mut self) -> RenderedMarkdown {
1586 debug_assert_eq!(self.div_stack.len(), 1);
1587 self.flush_text();
1588 RenderedMarkdown {
1589 element: self.div_stack.pop().unwrap().into_any_element(),
1590 text: RenderedText {
1591 lines: self.rendered_lines.into(),
1592 links: self.rendered_links.into(),
1593 },
1594 }
1595 }
1596}
1597
1598struct RenderedLine {
1599 layout: TextLayout,
1600 source_mappings: Vec<SourceMapping>,
1601 source_end: usize,
1602}
1603
1604impl RenderedLine {
1605 fn rendered_index_for_source_index(&self, source_index: usize) -> usize {
1606 if source_index >= self.source_end {
1607 return self.layout.len();
1608 }
1609
1610 let mapping = match self
1611 .source_mappings
1612 .binary_search_by_key(&source_index, |probe| probe.source_index)
1613 {
1614 Ok(ix) => &self.source_mappings[ix],
1615 Err(ix) => &self.source_mappings[ix - 1],
1616 };
1617 mapping.rendered_index + (source_index - mapping.source_index)
1618 }
1619
1620 fn source_index_for_rendered_index(&self, rendered_index: usize) -> usize {
1621 if rendered_index >= self.layout.len() {
1622 return self.source_end;
1623 }
1624
1625 let mapping = match self
1626 .source_mappings
1627 .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
1628 {
1629 Ok(ix) => &self.source_mappings[ix],
1630 Err(ix) => &self.source_mappings[ix - 1],
1631 };
1632 mapping.source_index + (rendered_index - mapping.rendered_index)
1633 }
1634
1635 fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
1636 let line_rendered_index;
1637 let out_of_bounds;
1638 match self.layout.index_for_position(position) {
1639 Ok(ix) => {
1640 line_rendered_index = ix;
1641 out_of_bounds = false;
1642 }
1643 Err(ix) => {
1644 line_rendered_index = ix;
1645 out_of_bounds = true;
1646 }
1647 };
1648 let source_index = self.source_index_for_rendered_index(line_rendered_index);
1649 if out_of_bounds {
1650 Err(source_index)
1651 } else {
1652 Ok(source_index)
1653 }
1654 }
1655}
1656
1657#[derive(Copy, Clone, Debug, Default)]
1658struct SourceMapping {
1659 rendered_index: usize,
1660 source_index: usize,
1661}
1662
1663pub struct RenderedMarkdown {
1664 element: AnyElement,
1665 text: RenderedText,
1666}
1667
1668#[derive(Clone)]
1669struct RenderedText {
1670 lines: Rc<[RenderedLine]>,
1671 links: Rc<[RenderedLink]>,
1672}
1673
1674#[derive(Clone, Eq, PartialEq)]
1675struct RenderedLink {
1676 source_range: Range<usize>,
1677 destination_url: SharedString,
1678}
1679
1680impl RenderedText {
1681 fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
1682 let mut lines = self.lines.iter().peekable();
1683
1684 while let Some(line) = lines.next() {
1685 let line_bounds = line.layout.bounds();
1686 if position.y > line_bounds.bottom() {
1687 if let Some(next_line) = lines.peek() {
1688 if position.y < next_line.layout.bounds().top() {
1689 return Err(line.source_end);
1690 }
1691 }
1692
1693 continue;
1694 }
1695
1696 return line.source_index_for_position(position);
1697 }
1698
1699 Err(self.lines.last().map_or(0, |line| line.source_end))
1700 }
1701
1702 fn position_for_source_index(&self, source_index: usize) -> Option<(Point<Pixels>, Pixels)> {
1703 for line in self.lines.iter() {
1704 let line_source_start = line.source_mappings.first().unwrap().source_index;
1705 if source_index < line_source_start {
1706 break;
1707 } else if source_index > line.source_end {
1708 continue;
1709 } else {
1710 let line_height = line.layout.line_height();
1711 let rendered_index_within_line = line.rendered_index_for_source_index(source_index);
1712 let position = line.layout.position_for_index(rendered_index_within_line)?;
1713 return Some((position, line_height));
1714 }
1715 }
1716 None
1717 }
1718
1719 fn surrounding_word_range(&self, source_index: usize) -> Range<usize> {
1720 for line in self.lines.iter() {
1721 if source_index > line.source_end {
1722 continue;
1723 }
1724
1725 let line_rendered_start = line.source_mappings.first().unwrap().rendered_index;
1726 let rendered_index_in_line =
1727 line.rendered_index_for_source_index(source_index) - line_rendered_start;
1728 let text = line.layout.text();
1729 let previous_space = if let Some(idx) = text[0..rendered_index_in_line].rfind(' ') {
1730 idx + ' '.len_utf8()
1731 } else {
1732 0
1733 };
1734 let next_space = if let Some(idx) = text[rendered_index_in_line..].find(' ') {
1735 rendered_index_in_line + idx
1736 } else {
1737 text.len()
1738 };
1739
1740 return line.source_index_for_rendered_index(line_rendered_start + previous_space)
1741 ..line.source_index_for_rendered_index(line_rendered_start + next_space);
1742 }
1743
1744 source_index..source_index
1745 }
1746
1747 fn surrounding_line_range(&self, source_index: usize) -> Range<usize> {
1748 for line in self.lines.iter() {
1749 if source_index > line.source_end {
1750 continue;
1751 }
1752 let line_source_start = line.source_mappings.first().unwrap().source_index;
1753 return line_source_start..line.source_end;
1754 }
1755
1756 source_index..source_index
1757 }
1758
1759 fn text_for_range(&self, range: Range<usize>) -> String {
1760 let mut ret = vec![];
1761
1762 for line in self.lines.iter() {
1763 if range.start > line.source_end {
1764 continue;
1765 }
1766 let line_source_start = line.source_mappings.first().unwrap().source_index;
1767 if range.end < line_source_start {
1768 break;
1769 }
1770
1771 let text = line.layout.text();
1772
1773 let start = if range.start < line_source_start {
1774 0
1775 } else {
1776 line.rendered_index_for_source_index(range.start)
1777 };
1778 let end = if range.end > line.source_end {
1779 line.rendered_index_for_source_index(line.source_end)
1780 } else {
1781 line.rendered_index_for_source_index(range.end)
1782 }
1783 .min(text.len());
1784
1785 ret.push(text[start..end].to_string());
1786 }
1787 ret.join("\n")
1788 }
1789
1790 fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {
1791 let source_index = self.source_index_for_position(position).ok()?;
1792 self.links
1793 .iter()
1794 .find(|link| link.source_range.contains(&source_index))
1795 }
1796}
1797
1798#[cfg(test)]
1799mod tests {
1800 use super::*;
1801 use gpui::{TestAppContext, size};
1802
1803 #[gpui::test]
1804 fn test_mappings(cx: &mut TestAppContext) {
1805 // Formatting.
1806 assert_mappings(
1807 &render_markdown("He*l*lo", cx),
1808 vec![vec![(0, 0), (1, 1), (2, 3), (3, 5), (4, 6), (5, 7)]],
1809 );
1810
1811 // Multiple lines.
1812 assert_mappings(
1813 &render_markdown("Hello\n\nWorld", cx),
1814 vec![
1815 vec![(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)],
1816 vec![(0, 7), (1, 8), (2, 9), (3, 10), (4, 11), (5, 12)],
1817 ],
1818 );
1819
1820 // Multi-byte characters.
1821 assert_mappings(
1822 &render_markdown("αβγ\n\nδεζ", cx),
1823 vec![
1824 vec![(0, 0), (2, 2), (4, 4), (6, 6)],
1825 vec![(0, 8), (2, 10), (4, 12), (6, 14)],
1826 ],
1827 );
1828
1829 // Smart quotes.
1830 assert_mappings(&render_markdown("\"", cx), vec![vec![(0, 0), (3, 1)]]);
1831 assert_mappings(
1832 &render_markdown("\"hey\"", cx),
1833 vec![vec![(0, 0), (3, 1), (4, 2), (5, 3), (6, 4), (9, 5)]],
1834 );
1835
1836 // HTML Comments are ignored
1837 assert_mappings(
1838 &render_markdown(
1839 "<!--\nrdoc-file=string.c\n- str.intern -> symbol\n- str.to_sym -> symbol\n-->\nReturns",
1840 cx,
1841 ),
1842 vec![vec![
1843 (0, 78),
1844 (1, 79),
1845 (2, 80),
1846 (3, 81),
1847 (4, 82),
1848 (5, 83),
1849 (6, 84),
1850 ]],
1851 );
1852 }
1853
1854 fn render_markdown(markdown: &str, cx: &mut TestAppContext) -> RenderedText {
1855 struct TestWindow;
1856
1857 impl Render for TestWindow {
1858 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1859 div()
1860 }
1861 }
1862
1863 let (_, cx) = cx.add_window_view(|_, _| TestWindow);
1864 let markdown = cx.new(|cx| Markdown::new(markdown.to_string().into(), None, None, cx));
1865 cx.run_until_parked();
1866 let (rendered, _) = cx.draw(
1867 Default::default(),
1868 size(px(600.0), px(600.0)),
1869 |_window, _cx| MarkdownElement::new(markdown, MarkdownStyle::default()),
1870 );
1871 rendered.text
1872 }
1873
1874 #[test]
1875 fn test_escape() {
1876 assert_eq!(Markdown::escape("hello `world`"), "hello \\`world\\`");
1877 assert_eq!(
1878 Markdown::escape("hello\n cool world"),
1879 "hello\n\ncool world"
1880 );
1881 }
1882
1883 #[track_caller]
1884 fn assert_mappings(rendered: &RenderedText, expected: Vec<Vec<(usize, usize)>>) {
1885 assert_eq!(rendered.lines.len(), expected.len(), "line count mismatch");
1886 for (line_ix, line_mappings) in expected.into_iter().enumerate() {
1887 let line = &rendered.lines[line_ix];
1888
1889 assert!(
1890 line.source_mappings.windows(2).all(|mappings| {
1891 mappings[0].source_index < mappings[1].source_index
1892 && mappings[0].rendered_index < mappings[1].rendered_index
1893 }),
1894 "line {} has duplicate mappings: {:?}",
1895 line_ix,
1896 line.source_mappings
1897 );
1898
1899 for (rendered_ix, source_ix) in line_mappings {
1900 assert_eq!(
1901 line.source_index_for_rendered_index(rendered_ix),
1902 source_ix,
1903 "line {}, rendered_ix {}",
1904 line_ix,
1905 rendered_ix
1906 );
1907
1908 assert_eq!(
1909 line.rendered_index_for_source_index(source_ix),
1910 rendered_ix,
1911 "line {}, source_ix {}",
1912 line_ix,
1913 source_ix
1914 );
1915 }
1916 }
1917 }
1918}