1mod parser;
2
3use crate::parser::CodeBlockKind;
4use futures::FutureExt;
5use gpui::{
6 actions, point, quad, AnyElement, AppContext, Bounds, ClipboardItem, CursorStyle,
7 DispatchPhase, Edges, FocusHandle, FocusableView, FontStyle, FontWeight, GlobalElementId,
8 Hitbox, Hsla, KeyContext, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point,
9 Render, StrikethroughStyle, Style, StyledText, Task, TextLayout, TextRun, TextStyle,
10 TextStyleRefinement, View,
11};
12use language::{Language, LanguageRegistry, Rope};
13use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
14use std::{iter, mem, ops::Range, rc::Rc, sync::Arc};
15use theme::SyntaxTheme;
16use ui::prelude::*;
17use util::{ResultExt, TryFutureExt};
18
19#[derive(Clone)]
20pub struct MarkdownStyle {
21 pub code_block: TextStyleRefinement,
22 pub inline_code: TextStyleRefinement,
23 pub block_quote: TextStyleRefinement,
24 pub link: TextStyleRefinement,
25 pub rule_color: Hsla,
26 pub block_quote_border_color: Hsla,
27 pub syntax: Arc<SyntaxTheme>,
28 pub selection_background_color: Hsla,
29}
30
31pub struct Markdown {
32 source: String,
33 selection: Selection,
34 pressed_link: Option<RenderedLink>,
35 autoscroll_request: Option<usize>,
36 style: MarkdownStyle,
37 parsed_markdown: ParsedMarkdown,
38 should_reparse: bool,
39 pending_parse: Option<Task<Option<()>>>,
40 focus_handle: FocusHandle,
41 language_registry: Option<Arc<LanguageRegistry>>,
42}
43
44actions!(markdown, [Copy]);
45
46impl Markdown {
47 pub fn new(
48 source: String,
49 style: MarkdownStyle,
50 language_registry: Option<Arc<LanguageRegistry>>,
51 cx: &mut ViewContext<Self>,
52 ) -> Self {
53 let focus_handle = cx.focus_handle();
54 let mut this = Self {
55 source,
56 selection: Selection::default(),
57 pressed_link: None,
58 autoscroll_request: None,
59 style,
60 should_reparse: false,
61 parsed_markdown: ParsedMarkdown::default(),
62 pending_parse: None,
63 focus_handle,
64 language_registry,
65 };
66 this.parse(cx);
67 this
68 }
69
70 pub fn append(&mut self, text: &str, cx: &mut ViewContext<Self>) {
71 self.source.push_str(text);
72 self.parse(cx);
73 }
74
75 pub fn reset(&mut self, source: String, cx: &mut ViewContext<Self>) {
76 self.source = source;
77 self.selection = Selection::default();
78 self.autoscroll_request = None;
79 self.pending_parse = None;
80 self.should_reparse = false;
81 self.parsed_markdown = ParsedMarkdown::default();
82 self.parse(cx);
83 }
84
85 pub fn source(&self) -> &str {
86 &self.source
87 }
88
89 fn copy(&self, text: &RenderedText, cx: &mut ViewContext<Self>) {
90 let text = text.text_for_range(self.selection.start..self.selection.end);
91 cx.write_to_clipboard(ClipboardItem::new(text));
92 }
93
94 fn parse(&mut self, cx: &mut ViewContext<Self>) {
95 if self.source.is_empty() {
96 return;
97 }
98
99 if self.pending_parse.is_some() {
100 self.should_reparse = true;
101 return;
102 }
103
104 let text = self.source.clone();
105 let parsed = cx.background_executor().spawn(async move {
106 let text = SharedString::from(text);
107 let events = Arc::from(parse_markdown(text.as_ref()));
108 anyhow::Ok(ParsedMarkdown {
109 source: text,
110 events,
111 })
112 });
113
114 self.should_reparse = false;
115 self.pending_parse = Some(cx.spawn(|this, mut cx| {
116 async move {
117 let parsed = parsed.await?;
118 this.update(&mut cx, |this, cx| {
119 this.parsed_markdown = parsed;
120 this.pending_parse.take();
121 if this.should_reparse {
122 this.parse(cx);
123 }
124 cx.notify();
125 })
126 .ok();
127 anyhow::Ok(())
128 }
129 .log_err()
130 }));
131 }
132}
133
134impl Render for Markdown {
135 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
136 MarkdownElement::new(
137 cx.view().clone(),
138 self.style.clone(),
139 self.language_registry.clone(),
140 )
141 }
142}
143
144impl FocusableView for Markdown {
145 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
146 self.focus_handle.clone()
147 }
148}
149
150#[derive(Copy, Clone, Default, Debug)]
151struct Selection {
152 start: usize,
153 end: usize,
154 reversed: bool,
155 pending: bool,
156}
157
158impl Selection {
159 fn set_head(&mut self, head: usize) {
160 if head < self.tail() {
161 if !self.reversed {
162 self.end = self.start;
163 self.reversed = true;
164 }
165 self.start = head;
166 } else {
167 if self.reversed {
168 self.start = self.end;
169 self.reversed = false;
170 }
171 self.end = head;
172 }
173 }
174
175 fn tail(&self) -> usize {
176 if self.reversed {
177 self.end
178 } else {
179 self.start
180 }
181 }
182}
183
184#[derive(Clone)]
185struct ParsedMarkdown {
186 source: SharedString,
187 events: Arc<[(Range<usize>, MarkdownEvent)]>,
188}
189
190impl Default for ParsedMarkdown {
191 fn default() -> Self {
192 Self {
193 source: SharedString::default(),
194 events: Arc::from([]),
195 }
196 }
197}
198
199pub struct MarkdownElement {
200 markdown: View<Markdown>,
201 style: MarkdownStyle,
202 language_registry: Option<Arc<LanguageRegistry>>,
203}
204
205impl MarkdownElement {
206 fn new(
207 markdown: View<Markdown>,
208 style: MarkdownStyle,
209 language_registry: Option<Arc<LanguageRegistry>>,
210 ) -> Self {
211 Self {
212 markdown,
213 style,
214 language_registry,
215 }
216 }
217
218 fn load_language(&self, name: &str, cx: &mut WindowContext) -> Option<Arc<Language>> {
219 let language = self
220 .language_registry
221 .as_ref()?
222 .language_for_name(name)
223 .map(|language| language.ok())
224 .shared();
225
226 match language.clone().now_or_never() {
227 Some(language) => language,
228 None => {
229 let markdown = self.markdown.downgrade();
230 cx.spawn(|mut cx| async move {
231 language.await;
232 markdown.update(&mut cx, |_, cx| cx.notify())
233 })
234 .detach_and_log_err(cx);
235 None
236 }
237 }
238 }
239
240 fn paint_selection(
241 &mut self,
242 bounds: Bounds<Pixels>,
243 rendered_text: &RenderedText,
244 cx: &mut WindowContext,
245 ) {
246 let selection = self.markdown.read(cx).selection;
247 let selection_start = rendered_text.position_for_source_index(selection.start);
248 let selection_end = rendered_text.position_for_source_index(selection.end);
249
250 if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
251 selection_start.zip(selection_end)
252 {
253 if start_position.y == end_position.y {
254 cx.paint_quad(quad(
255 Bounds::from_corners(
256 start_position,
257 point(end_position.x, end_position.y + end_line_height),
258 ),
259 Pixels::ZERO,
260 self.style.selection_background_color,
261 Edges::default(),
262 Hsla::transparent_black(),
263 ));
264 } else {
265 cx.paint_quad(quad(
266 Bounds::from_corners(
267 start_position,
268 point(bounds.right(), start_position.y + start_line_height),
269 ),
270 Pixels::ZERO,
271 self.style.selection_background_color,
272 Edges::default(),
273 Hsla::transparent_black(),
274 ));
275
276 if end_position.y > start_position.y + start_line_height {
277 cx.paint_quad(quad(
278 Bounds::from_corners(
279 point(bounds.left(), start_position.y + start_line_height),
280 point(bounds.right(), end_position.y),
281 ),
282 Pixels::ZERO,
283 self.style.selection_background_color,
284 Edges::default(),
285 Hsla::transparent_black(),
286 ));
287 }
288
289 cx.paint_quad(quad(
290 Bounds::from_corners(
291 point(bounds.left(), end_position.y),
292 point(end_position.x, end_position.y + end_line_height),
293 ),
294 Pixels::ZERO,
295 self.style.selection_background_color,
296 Edges::default(),
297 Hsla::transparent_black(),
298 ));
299 }
300 }
301 }
302
303 fn paint_mouse_listeners(
304 &mut self,
305 hitbox: &Hitbox,
306 rendered_text: &RenderedText,
307 cx: &mut WindowContext,
308 ) {
309 let is_hovering_link = hitbox.is_hovered(cx)
310 && !self.markdown.read(cx).selection.pending
311 && rendered_text
312 .link_for_position(cx.mouse_position())
313 .is_some();
314
315 if is_hovering_link {
316 cx.set_cursor_style(CursorStyle::PointingHand, hitbox);
317 } else {
318 cx.set_cursor_style(CursorStyle::IBeam, hitbox);
319 }
320
321 self.on_mouse_event(cx, {
322 let rendered_text = rendered_text.clone();
323 let hitbox = hitbox.clone();
324 move |markdown, event: &MouseDownEvent, phase, cx| {
325 if hitbox.is_hovered(cx) {
326 if phase.bubble() {
327 if let Some(link) = rendered_text.link_for_position(event.position) {
328 markdown.pressed_link = Some(link.clone());
329 } else {
330 let source_index =
331 match rendered_text.source_index_for_position(event.position) {
332 Ok(ix) | Err(ix) => ix,
333 };
334 let range = if event.click_count == 2 {
335 rendered_text.surrounding_word_range(source_index)
336 } else if event.click_count == 3 {
337 rendered_text.surrounding_line_range(source_index)
338 } else {
339 source_index..source_index
340 };
341 markdown.selection = Selection {
342 start: range.start,
343 end: range.end,
344 reversed: false,
345 pending: true,
346 };
347 cx.focus(&markdown.focus_handle);
348 cx.prevent_default()
349 }
350
351 cx.notify();
352 }
353 } else if phase.capture() {
354 markdown.selection = Selection::default();
355 markdown.pressed_link = None;
356 cx.notify();
357 }
358 }
359 });
360 self.on_mouse_event(cx, {
361 let rendered_text = rendered_text.clone();
362 let hitbox = hitbox.clone();
363 let was_hovering_link = is_hovering_link;
364 move |markdown, event: &MouseMoveEvent, phase, cx| {
365 if phase.capture() {
366 return;
367 }
368
369 if markdown.selection.pending {
370 let source_index = match rendered_text.source_index_for_position(event.position)
371 {
372 Ok(ix) | Err(ix) => ix,
373 };
374 markdown.selection.set_head(source_index);
375 markdown.autoscroll_request = Some(source_index);
376 cx.notify();
377 } else {
378 let is_hovering_link = hitbox.is_hovered(cx)
379 && rendered_text.link_for_position(event.position).is_some();
380 if is_hovering_link != was_hovering_link {
381 cx.notify();
382 }
383 }
384 }
385 });
386 self.on_mouse_event(cx, {
387 let rendered_text = rendered_text.clone();
388 move |markdown, event: &MouseUpEvent, phase, cx| {
389 if phase.bubble() {
390 if let Some(pressed_link) = markdown.pressed_link.take() {
391 if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
392 cx.open_url(&pressed_link.destination_url);
393 }
394 }
395 } else {
396 if markdown.selection.pending {
397 markdown.selection.pending = false;
398 #[cfg(target_os = "linux")]
399 {
400 let text = rendered_text
401 .text_for_range(markdown.selection.start..markdown.selection.end);
402 cx.write_to_primary(ClipboardItem::new(text))
403 }
404 cx.notify();
405 }
406 }
407 }
408 });
409 }
410
411 fn autoscroll(&mut self, rendered_text: &RenderedText, cx: &mut WindowContext) -> Option<()> {
412 let autoscroll_index = self
413 .markdown
414 .update(cx, |markdown, _| markdown.autoscroll_request.take())?;
415 let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
416
417 let text_style = cx.text_style();
418 let font_id = cx.text_system().resolve_font(&text_style.font());
419 let font_size = text_style.font_size.to_pixels(cx.rem_size());
420 let em_width = cx
421 .text_system()
422 .typographic_bounds(font_id, font_size, 'm')
423 .unwrap()
424 .size
425 .width;
426 cx.request_autoscroll(Bounds::from_corners(
427 point(position.x - 3. * em_width, position.y - 3. * line_height),
428 point(position.x + 3. * em_width, position.y + 3. * line_height),
429 ));
430 Some(())
431 }
432
433 fn on_mouse_event<T: MouseEvent>(
434 &self,
435 cx: &mut WindowContext,
436 mut f: impl 'static + FnMut(&mut Markdown, &T, DispatchPhase, &mut ViewContext<Markdown>),
437 ) {
438 cx.on_mouse_event({
439 let markdown = self.markdown.downgrade();
440 move |event, phase, cx| {
441 markdown
442 .update(cx, |markdown, cx| f(markdown, event, phase, cx))
443 .log_err();
444 }
445 });
446 }
447}
448
449impl Element for MarkdownElement {
450 type RequestLayoutState = RenderedMarkdown;
451 type PrepaintState = Hitbox;
452
453 fn id(&self) -> Option<ElementId> {
454 None
455 }
456
457 fn request_layout(
458 &mut self,
459 _id: Option<&GlobalElementId>,
460 cx: &mut WindowContext,
461 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
462 let mut builder = MarkdownElementBuilder::new(cx.text_style(), self.style.syntax.clone());
463 let parsed_markdown = self.markdown.read(cx).parsed_markdown.clone();
464 for (range, event) in parsed_markdown.events.iter() {
465 match event {
466 MarkdownEvent::Start(tag) => {
467 match tag {
468 MarkdownTag::Paragraph => {
469 builder.push_div(div().mb_2().line_height(rems(1.3)));
470 }
471 MarkdownTag::Heading { level, .. } => {
472 let mut heading = div().mb_2();
473 heading = match level {
474 pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(),
475 pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(),
476 pulldown_cmark::HeadingLevel::H3 => heading.text_xl(),
477 pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
478 _ => heading,
479 };
480 builder.push_div(heading);
481 }
482 MarkdownTag::BlockQuote => {
483 builder.push_text_style(self.style.block_quote.clone());
484 builder.push_div(
485 div()
486 .pl_4()
487 .mb_2()
488 .border_l_4()
489 .border_color(self.style.block_quote_border_color),
490 );
491 }
492 MarkdownTag::CodeBlock(kind) => {
493 let language = if let CodeBlockKind::Fenced(language) = kind {
494 self.load_language(language.as_ref(), cx)
495 } else {
496 None
497 };
498
499 builder.push_code_block(language);
500 builder.push_text_style(self.style.code_block.clone());
501 builder.push_div(div().rounded_lg().p_4().mb_2().w_full().when_some(
502 self.style.code_block.background_color,
503 |div, color| div.bg(color),
504 ));
505 }
506 MarkdownTag::HtmlBlock => builder.push_div(div()),
507 MarkdownTag::List(bullet_index) => {
508 builder.push_list(*bullet_index);
509 builder.push_div(div().pl_4());
510 }
511 MarkdownTag::Item => {
512 let bullet = if let Some(bullet_index) = builder.next_bullet_index() {
513 format!("{}.", bullet_index)
514 } else {
515 "•".to_string()
516 };
517 builder.push_div(
518 div()
519 .h_flex()
520 .mb_2()
521 .line_height(rems(1.3))
522 .items_start()
523 .gap_1()
524 .child(bullet),
525 );
526 // Without `w_0`, text doesn't wrap to the width of the container.
527 builder.push_div(div().flex_1().w_0());
528 }
529 MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
530 font_style: Some(FontStyle::Italic),
531 ..Default::default()
532 }),
533 MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
534 font_weight: Some(FontWeight::BOLD),
535 ..Default::default()
536 }),
537 MarkdownTag::Strikethrough => {
538 builder.push_text_style(TextStyleRefinement {
539 strikethrough: Some(StrikethroughStyle {
540 thickness: px(1.),
541 color: None,
542 }),
543 ..Default::default()
544 })
545 }
546 MarkdownTag::Link { dest_url, .. } => {
547 builder.push_link(dest_url.clone(), range.clone());
548 builder.push_text_style(self.style.link.clone())
549 }
550 _ => log::error!("unsupported markdown tag {:?}", tag),
551 }
552 }
553 MarkdownEvent::End(tag) => match tag {
554 MarkdownTagEnd::Paragraph => {
555 builder.pop_div();
556 }
557 MarkdownTagEnd::Heading(_) => builder.pop_div(),
558 MarkdownTagEnd::BlockQuote => {
559 builder.pop_text_style();
560 builder.pop_div()
561 }
562 MarkdownTagEnd::CodeBlock => {
563 builder.trim_trailing_newline();
564 builder.pop_div();
565 builder.pop_text_style();
566 builder.pop_code_block();
567 }
568 MarkdownTagEnd::HtmlBlock => builder.pop_div(),
569 MarkdownTagEnd::List(_) => {
570 builder.pop_list();
571 builder.pop_div();
572 }
573 MarkdownTagEnd::Item => {
574 builder.pop_div();
575 builder.pop_div();
576 }
577 MarkdownTagEnd::Emphasis => builder.pop_text_style(),
578 MarkdownTagEnd::Strong => builder.pop_text_style(),
579 MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
580 MarkdownTagEnd::Link => builder.pop_text_style(),
581 _ => log::error!("unsupported markdown tag end: {:?}", tag),
582 },
583 MarkdownEvent::Text => {
584 builder.push_text(&parsed_markdown.source[range.clone()], range.start);
585 }
586 MarkdownEvent::Code => {
587 builder.push_text_style(self.style.inline_code.clone());
588 builder.push_text(&parsed_markdown.source[range.clone()], range.start);
589 builder.pop_text_style();
590 }
591 MarkdownEvent::Html => {
592 builder.push_text(&parsed_markdown.source[range.clone()], range.start);
593 }
594 MarkdownEvent::InlineHtml => {
595 builder.push_text(&parsed_markdown.source[range.clone()], range.start);
596 }
597 MarkdownEvent::Rule => {
598 builder.push_div(
599 div()
600 .border_b_1()
601 .my_2()
602 .border_color(self.style.rule_color),
603 );
604 builder.pop_div()
605 }
606 MarkdownEvent::SoftBreak => builder.push_text("\n", range.start),
607 MarkdownEvent::HardBreak => builder.push_text("\n", range.start),
608 _ => log::error!("unsupported markdown event {:?}", event),
609 }
610 }
611
612 let mut rendered_markdown = builder.build();
613 let child_layout_id = rendered_markdown.element.request_layout(cx);
614 let layout_id = cx.request_layout(Style::default(), [child_layout_id]);
615 (layout_id, rendered_markdown)
616 }
617
618 fn prepaint(
619 &mut self,
620 _id: Option<&GlobalElementId>,
621 bounds: Bounds<Pixels>,
622 rendered_markdown: &mut Self::RequestLayoutState,
623 cx: &mut WindowContext,
624 ) -> Self::PrepaintState {
625 let hitbox = cx.insert_hitbox(bounds, false);
626 rendered_markdown.element.prepaint(cx);
627 self.autoscroll(&rendered_markdown.text, cx);
628 hitbox
629 }
630
631 fn paint(
632 &mut self,
633 _id: Option<&GlobalElementId>,
634 bounds: Bounds<Pixels>,
635 rendered_markdown: &mut Self::RequestLayoutState,
636 hitbox: &mut Self::PrepaintState,
637 cx: &mut WindowContext,
638 ) {
639 let focus_handle = self.markdown.read(cx).focus_handle.clone();
640 cx.set_focus_handle(&focus_handle);
641
642 let mut context = KeyContext::default();
643 context.add("Markdown");
644 cx.set_key_context(context);
645 let view = self.markdown.clone();
646 cx.on_action(std::any::TypeId::of::<crate::Copy>(), {
647 let text = rendered_markdown.text.clone();
648 move |_, phase, cx| {
649 let text = text.clone();
650 if phase == DispatchPhase::Bubble {
651 view.update(cx, move |this, cx| this.copy(&text, cx))
652 }
653 }
654 });
655
656 self.paint_mouse_listeners(hitbox, &rendered_markdown.text, cx);
657 rendered_markdown.element.paint(cx);
658 self.paint_selection(bounds, &rendered_markdown.text, cx);
659 }
660}
661
662impl IntoElement for MarkdownElement {
663 type Element = Self;
664
665 fn into_element(self) -> Self::Element {
666 self
667 }
668}
669
670struct MarkdownElementBuilder {
671 div_stack: Vec<Div>,
672 rendered_lines: Vec<RenderedLine>,
673 pending_line: PendingLine,
674 rendered_links: Vec<RenderedLink>,
675 current_source_index: usize,
676 base_text_style: TextStyle,
677 text_style_stack: Vec<TextStyleRefinement>,
678 code_block_stack: Vec<Option<Arc<Language>>>,
679 list_stack: Vec<ListStackEntry>,
680 syntax_theme: Arc<SyntaxTheme>,
681}
682
683#[derive(Default)]
684struct PendingLine {
685 text: String,
686 runs: Vec<TextRun>,
687 source_mappings: Vec<SourceMapping>,
688}
689
690struct ListStackEntry {
691 bullet_index: Option<u64>,
692}
693
694impl MarkdownElementBuilder {
695 fn new(base_text_style: TextStyle, syntax_theme: Arc<SyntaxTheme>) -> Self {
696 Self {
697 div_stack: vec![div().debug_selector(|| "inner".into())],
698 rendered_lines: Vec::new(),
699 pending_line: PendingLine::default(),
700 rendered_links: Vec::new(),
701 current_source_index: 0,
702 base_text_style,
703 text_style_stack: Vec::new(),
704 code_block_stack: Vec::new(),
705 list_stack: Vec::new(),
706 syntax_theme,
707 }
708 }
709
710 fn push_text_style(&mut self, style: TextStyleRefinement) {
711 self.text_style_stack.push(style);
712 }
713
714 fn text_style(&self) -> TextStyle {
715 let mut style = self.base_text_style.clone();
716 for refinement in &self.text_style_stack {
717 style.refine(refinement);
718 }
719 style
720 }
721
722 fn pop_text_style(&mut self) {
723 self.text_style_stack.pop();
724 }
725
726 fn push_div(&mut self, div: Div) {
727 self.flush_text();
728 self.div_stack.push(div);
729 }
730
731 fn pop_div(&mut self) {
732 self.flush_text();
733 let div = self.div_stack.pop().unwrap().into_any();
734 self.div_stack.last_mut().unwrap().extend(iter::once(div));
735 }
736
737 fn push_list(&mut self, bullet_index: Option<u64>) {
738 self.list_stack.push(ListStackEntry { bullet_index });
739 }
740
741 fn next_bullet_index(&mut self) -> Option<u64> {
742 self.list_stack.last_mut().and_then(|entry| {
743 let item_index = entry.bullet_index.as_mut()?;
744 *item_index += 1;
745 Some(*item_index - 1)
746 })
747 }
748
749 fn pop_list(&mut self) {
750 self.list_stack.pop();
751 }
752
753 fn push_code_block(&mut self, language: Option<Arc<Language>>) {
754 self.code_block_stack.push(language);
755 }
756
757 fn pop_code_block(&mut self) {
758 self.code_block_stack.pop();
759 }
760
761 fn push_link(&mut self, destination_url: SharedString, source_range: Range<usize>) {
762 self.rendered_links.push(RenderedLink {
763 source_range,
764 destination_url,
765 });
766 }
767
768 fn push_text(&mut self, text: &str, source_index: usize) {
769 self.pending_line.source_mappings.push(SourceMapping {
770 rendered_index: self.pending_line.text.len(),
771 source_index,
772 });
773 self.pending_line.text.push_str(text);
774 self.current_source_index = source_index + text.len();
775
776 if let Some(Some(language)) = self.code_block_stack.last() {
777 let mut offset = 0;
778 for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
779 if range.start > offset {
780 self.pending_line
781 .runs
782 .push(self.text_style().to_run(range.start - offset));
783 }
784
785 let mut run_style = self.text_style();
786 if let Some(highlight) = highlight_id.style(&self.syntax_theme) {
787 run_style = run_style.highlight(highlight);
788 }
789 self.pending_line.runs.push(run_style.to_run(range.len()));
790 offset = range.end;
791 }
792
793 if offset < text.len() {
794 self.pending_line
795 .runs
796 .push(self.text_style().to_run(text.len() - offset));
797 }
798 } else {
799 self.pending_line
800 .runs
801 .push(self.text_style().to_run(text.len()));
802 }
803 }
804
805 fn trim_trailing_newline(&mut self) {
806 if self.pending_line.text.ends_with('\n') {
807 self.pending_line
808 .text
809 .truncate(self.pending_line.text.len() - 1);
810 self.pending_line.runs.last_mut().unwrap().len -= 1;
811 self.current_source_index -= 1;
812 }
813 }
814
815 fn flush_text(&mut self) {
816 let line = mem::take(&mut self.pending_line);
817 if line.text.is_empty() {
818 return;
819 }
820
821 let text = StyledText::new(line.text).with_runs(line.runs);
822 self.rendered_lines.push(RenderedLine {
823 layout: text.layout().clone(),
824 source_mappings: line.source_mappings,
825 source_end: self.current_source_index,
826 });
827 self.div_stack.last_mut().unwrap().extend([text.into_any()]);
828 }
829
830 fn build(mut self) -> RenderedMarkdown {
831 debug_assert_eq!(self.div_stack.len(), 1);
832 self.flush_text();
833 RenderedMarkdown {
834 element: self.div_stack.pop().unwrap().into_any(),
835 text: RenderedText {
836 lines: self.rendered_lines.into(),
837 links: self.rendered_links.into(),
838 },
839 }
840 }
841}
842
843struct RenderedLine {
844 layout: TextLayout,
845 source_mappings: Vec<SourceMapping>,
846 source_end: usize,
847}
848
849impl RenderedLine {
850 fn rendered_index_for_source_index(&self, source_index: usize) -> usize {
851 let mapping = match self
852 .source_mappings
853 .binary_search_by_key(&source_index, |probe| probe.source_index)
854 {
855 Ok(ix) => &self.source_mappings[ix],
856 Err(ix) => &self.source_mappings[ix - 1],
857 };
858 mapping.rendered_index + (source_index - mapping.source_index)
859 }
860
861 fn source_index_for_rendered_index(&self, rendered_index: usize) -> usize {
862 let mapping = match self
863 .source_mappings
864 .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
865 {
866 Ok(ix) => &self.source_mappings[ix],
867 Err(ix) => &self.source_mappings[ix - 1],
868 };
869 mapping.source_index + (rendered_index - mapping.rendered_index)
870 }
871
872 fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
873 let line_rendered_index;
874 let out_of_bounds;
875 match self.layout.index_for_position(position) {
876 Ok(ix) => {
877 line_rendered_index = ix;
878 out_of_bounds = false;
879 }
880 Err(ix) => {
881 line_rendered_index = ix;
882 out_of_bounds = true;
883 }
884 };
885 let source_index = self.source_index_for_rendered_index(line_rendered_index);
886 if out_of_bounds {
887 Err(source_index)
888 } else {
889 Ok(source_index)
890 }
891 }
892}
893
894#[derive(Copy, Clone, Debug, Default)]
895struct SourceMapping {
896 rendered_index: usize,
897 source_index: usize,
898}
899
900pub struct RenderedMarkdown {
901 element: AnyElement,
902 text: RenderedText,
903}
904
905#[derive(Clone)]
906struct RenderedText {
907 lines: Rc<[RenderedLine]>,
908 links: Rc<[RenderedLink]>,
909}
910
911#[derive(Clone, Eq, PartialEq)]
912struct RenderedLink {
913 source_range: Range<usize>,
914 destination_url: SharedString,
915}
916
917impl RenderedText {
918 fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
919 let mut lines = self.lines.iter().peekable();
920
921 while let Some(line) = lines.next() {
922 let line_bounds = line.layout.bounds();
923 if position.y > line_bounds.bottom() {
924 if let Some(next_line) = lines.peek() {
925 if position.y < next_line.layout.bounds().top() {
926 return Err(line.source_end);
927 }
928 }
929
930 continue;
931 }
932
933 return line.source_index_for_position(position);
934 }
935
936 Err(self.lines.last().map_or(0, |line| line.source_end))
937 }
938
939 fn position_for_source_index(&self, source_index: usize) -> Option<(Point<Pixels>, Pixels)> {
940 for line in self.lines.iter() {
941 let line_source_start = line.source_mappings.first().unwrap().source_index;
942 if source_index < line_source_start {
943 break;
944 } else if source_index > line.source_end {
945 continue;
946 } else {
947 let line_height = line.layout.line_height();
948 let rendered_index_within_line = line.rendered_index_for_source_index(source_index);
949 let position = line.layout.position_for_index(rendered_index_within_line)?;
950 return Some((position, line_height));
951 }
952 }
953 None
954 }
955
956 fn surrounding_word_range(&self, source_index: usize) -> Range<usize> {
957 for line in self.lines.iter() {
958 if source_index > line.source_end {
959 continue;
960 }
961
962 let line_rendered_start = line.source_mappings.first().unwrap().rendered_index;
963 let rendered_index_in_line =
964 line.rendered_index_for_source_index(source_index) - line_rendered_start;
965 let text = line.layout.text();
966 let previous_space = if let Some(idx) = text[0..rendered_index_in_line].rfind(' ') {
967 idx + ' '.len_utf8()
968 } else {
969 0
970 };
971 let next_space = if let Some(idx) = text[rendered_index_in_line..].find(' ') {
972 rendered_index_in_line + idx
973 } else {
974 text.len()
975 };
976
977 return line.source_index_for_rendered_index(line_rendered_start + previous_space)
978 ..line.source_index_for_rendered_index(line_rendered_start + next_space);
979 }
980
981 source_index..source_index
982 }
983
984 fn surrounding_line_range(&self, source_index: usize) -> Range<usize> {
985 for line in self.lines.iter() {
986 if source_index > line.source_end {
987 continue;
988 }
989 let line_source_start = line.source_mappings.first().unwrap().source_index;
990 return line_source_start..line.source_end;
991 }
992
993 source_index..source_index
994 }
995
996 fn text_for_range(&self, range: Range<usize>) -> String {
997 let mut ret = vec![];
998
999 for line in self.lines.iter() {
1000 if range.start > line.source_end {
1001 continue;
1002 }
1003 let line_source_start = line.source_mappings.first().unwrap().source_index;
1004 if range.end < line_source_start {
1005 break;
1006 }
1007
1008 let text = line.layout.text();
1009
1010 let start = if range.start < line_source_start {
1011 0
1012 } else {
1013 line.rendered_index_for_source_index(range.start)
1014 };
1015 let end = if range.end > line.source_end {
1016 line.rendered_index_for_source_index(line.source_end)
1017 } else {
1018 line.rendered_index_for_source_index(range.end)
1019 }
1020 .min(text.len());
1021
1022 ret.push(text[start..end].to_string());
1023 }
1024 ret.join("\n")
1025 }
1026
1027 fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {
1028 let source_index = self.source_index_for_position(position).ok()?;
1029 self.links
1030 .iter()
1031 .find(|link| link.source_range.contains(&source_index))
1032 }
1033}