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