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