mod parser; use crate::parser::CodeBlockKind;use futures::FutureExt;use gpui::{ actions, point, quad, AnyElement, AppContext, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, FocusHandle, FocusableView, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, KeyContext, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, Render, StrikethroughStyle, Style, StyledText, Task, TextLayout, TextRun, TextStyle, TextStyleRefinement, View,};use language::{Language, LanguageRegistry, Rope};use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};use std::{iter, mem, ops::Range, rc::Rc, sync::Arc};use theme::SyntaxTheme;use ui::prelude::*;use util::{ResultExt, TryFutureExt}; #[derive(Clone)]pub struct MarkdownStyle { pub code_block: TextStyleRefinement, pub inline_code: TextStyleRefinement, pub block_quote: TextStyleRefinement, pub link: TextStyleRefinement, pub rule_color: Hsla, pub block_quote_border_color: Hsla, pub syntax: Arc<SyntaxTheme>, pub selection_background_color: Hsla,} pub struct Markdown { source: String, selection: Selection, pressed_link: Option<RenderedLink>, autoscroll_request: Option<usize>, style: MarkdownStyle, parsed_markdown: ParsedMarkdown, should_reparse: bool, pending_parse: Option<Task<Option<()>>>, focus_handle: FocusHandle, language_registry: Option<Arc<LanguageRegistry>>,} actions!(markdown, [Copy]); impl Markdown { pub fn new( source: String, style: MarkdownStyle, language_registry: Option<Arc<LanguageRegistry>>, cx: &mut ViewContext<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let mut this = Self { source, selection: Selection::default(), pressed_link: None, autoscroll_request: None, style, should_reparse: false, parsed_markdown: ParsedMarkdown::default(), pending_parse: None, focus_handle, language_registry, }; this.parse(cx); this } pub fn append(&mut self, text: &str, cx: &mut ViewContext<Self>) { self.source.push_str(text); self.parse(cx); } pub fn reset(&mut self, source: String, cx: &mut ViewContext<Self>) { self.source = source; self.selection = Selection::default(); self.autoscroll_request = None; self.pending_parse = None; self.should_reparse = false; self.parsed_markdown = ParsedMarkdown::default(); self.parse(cx); } pub fn source(&self) -> &str { &self.source } fn copy(&self, text: &RenderedText, cx: &mut ViewContext<Self>) { let text = text.text_for_range(self.selection.start..self.selection.end); cx.write_to_clipboard(ClipboardItem::new(text)); } fn parse(&mut self, cx: &mut ViewContext<Self>) { if self.source.is_empty() { return; } if self.pending_parse.is_some() { self.should_reparse = true; return; } let text = self.source.clone(); let parsed = cx.background_executor().spawn(async move { let text = SharedString::from(text); let events = Arc::from(parse_markdown(text.as_ref())); anyhow::Ok(ParsedMarkdown { source: text, events, }) }); self.should_reparse = false; self.pending_parse = Some(cx.spawn(|this, mut cx| { async move { let parsed = parsed.await?; this.update(&mut cx, |this, cx| { this.parsed_markdown = parsed; this.pending_parse.take(); if this.should_reparse { this.parse(cx); } cx.notify(); }) .ok(); anyhow::Ok(()) } .log_err() })); }} impl Render for Markdown { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { MarkdownElement::new( cx.view().clone(), self.style.clone(), self.language_registry.clone(), ) }} impl FocusableView for Markdown { fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { self.focus_handle.clone() }} #[derive(Copy, Clone, Default, Debug)]struct Selection { start: usize, end: usize, reversed: bool, pending: bool,} impl Selection { fn set_head(&mut self, head: usize) { if head < self.tail() { if !self.reversed { self.end = self.start; self.reversed = true; } self.start = head; } else { if self.reversed { self.start = self.end; self.reversed = false; } self.end = head; } } fn tail(&self) -> usize { if self.reversed { self.end } else { self.start } }} #[derive(Clone)]struct ParsedMarkdown { source: SharedString, events: Arc<[(Range<usize>, MarkdownEvent)]>,} impl Default for ParsedMarkdown { fn default() -> Self { Self { source: SharedString::default(), events: Arc::from([]), } }} pub struct MarkdownElement { markdown: View<Markdown>, style: MarkdownStyle, language_registry: Option<Arc<LanguageRegistry>>,} impl MarkdownElement { fn new( markdown: View<Markdown>, style: MarkdownStyle, language_registry: Option<Arc<LanguageRegistry>>, ) -> Self { Self { markdown, style, language_registry, } } fn load_language(&self, name: &str, cx: &mut WindowContext) -> Option<Arc<Language>> { let language = self .language_registry .as_ref()? .language_for_name(name) .map(|language| language.ok()) .shared(); match language.clone().now_or_never() { Some(language) => language, None => { let markdown = self.markdown.downgrade(); cx.spawn(|mut cx| async move { language.await; markdown.update(&mut cx, |_, cx| cx.notify()) }) .detach_and_log_err(cx); None } } } fn paint_selection( &mut self, bounds: Bounds<Pixels>, rendered_text: &RenderedText, cx: &mut WindowContext, ) { let selection = self.markdown.read(cx).selection; let selection_start = rendered_text.position_for_source_index(selection.start); let selection_end = rendered_text.position_for_source_index(selection.end); if let Some(((start_position, start_line_height), (end_position, end_line_height))) = selection_start.zip(selection_end) { if start_position.y == end_position.y { cx.paint_quad(quad( Bounds::from_corners( start_position, point(end_position.x, end_position.y + end_line_height), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), )); } else { cx.paint_quad(quad( Bounds::from_corners( start_position, point(bounds.right(), start_position.y + start_line_height), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), )); if end_position.y > start_position.y + start_line_height { cx.paint_quad(quad( Bounds::from_corners( point(bounds.left(), start_position.y + start_line_height), point(bounds.right(), end_position.y), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), )); } cx.paint_quad(quad( Bounds::from_corners( point(bounds.left(), end_position.y), point(end_position.x, end_position.y + end_line_height), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), )); } } } fn paint_mouse_listeners( &mut self, hitbox: &Hitbox, rendered_text: &RenderedText, cx: &mut WindowContext, ) { let is_hovering_link = hitbox.is_hovered(cx) && !self.markdown.read(cx).selection.pending && rendered_text .link_for_position(cx.mouse_position()) .is_some(); if is_hovering_link { cx.set_cursor_style(CursorStyle::PointingHand, hitbox); } else { cx.set_cursor_style(CursorStyle::IBeam, hitbox); } self.on_mouse_event(cx, { let rendered_text = rendered_text.clone(); let hitbox = hitbox.clone(); move |markdown, event: &MouseDownEvent, phase, cx| { if hitbox.is_hovered(cx) { if phase.bubble() { if let Some(link) = rendered_text.link_for_position(event.position) { markdown.pressed_link = Some(link.clone()); } else { let source_index = match rendered_text.source_index_for_position(event.position) { Ok(ix) | Err(ix) => ix, }; let range = if event.click_count == 2 { rendered_text.surrounding_word_range(source_index) } else if event.click_count == 3 { rendered_text.surrounding_line_range(source_index) } else { source_index..source_index }; markdown.selection = Selection { start: range.start, end: range.end, reversed: false, pending: true, }; cx.focus(&markdown.focus_handle); cx.prevent_default() } cx.notify(); } } else if phase.capture() { markdown.selection = Selection::default(); markdown.pressed_link = None; cx.notify(); } } }); self.on_mouse_event(cx, { let rendered_text = rendered_text.clone(); let hitbox = hitbox.clone(); let was_hovering_link = is_hovering_link; move |markdown, event: &MouseMoveEvent, phase, cx| { if phase.capture() { return; } if markdown.selection.pending { let source_index = match rendered_text.source_index_for_position(event.position) { Ok(ix) | Err(ix) => ix, }; markdown.selection.set_head(source_index); markdown.autoscroll_request = Some(source_index); cx.notify(); } else { let is_hovering_link = hitbox.is_hovered(cx) && rendered_text.link_for_position(event.position).is_some(); if is_hovering_link != was_hovering_link { cx.notify(); } } } }); self.on_mouse_event(cx, { let rendered_text = rendered_text.clone(); move |markdown, event: &MouseUpEvent, phase, cx| { if phase.bubble() { if let Some(pressed_link) = markdown.pressed_link.take() { if Some(&pressed_link) == rendered_text.link_for_position(event.position) { cx.open_url(&pressed_link.destination_url); } } } else { if markdown.selection.pending { markdown.selection.pending = false; #[cfg(target_os = "linux")] { let text = rendered_text .text_for_range(markdown.selection.start..markdown.selection.end); cx.write_to_primary(ClipboardItem::new(text)) } cx.notify(); } } } }); } fn autoscroll(&mut self, rendered_text: &RenderedText, cx: &mut WindowContext) -> Option<()> { let autoscroll_index = self .markdown .update(cx, |markdown, _| markdown.autoscroll_request.take())?; let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?; let text_style = cx.text_style(); let font_id = cx.text_system().resolve_font(&text_style.font()); let font_size = text_style.font_size.to_pixels(cx.rem_size()); let em_width = cx .text_system() .typographic_bounds(font_id, font_size, 'm') .unwrap() .size .width; cx.request_autoscroll(Bounds::from_corners( point(position.x - 3. * em_width, position.y - 3. * line_height), point(position.x + 3. * em_width, position.y + 3. * line_height), )); Some(()) } fn on_mouse_event<T: MouseEvent>( &self, cx: &mut WindowContext, mut f: impl 'static + FnMut(&mut Markdown, &T, DispatchPhase, &mut ViewContext<Markdown>), ) { cx.on_mouse_event({ let markdown = self.markdown.downgrade(); move |event, phase, cx| { markdown .update(cx, |markdown, cx| f(markdown, event, phase, cx)) .log_err(); } }); }} impl Element for MarkdownElement { type RequestLayoutState = RenderedMarkdown; type PrepaintState = Hitbox; fn id(&self) -> Option<ElementId> { None } fn request_layout( &mut self, _id: Option<&GlobalElementId>, cx: &mut WindowContext, ) -> (gpui::LayoutId, Self::RequestLayoutState) { let mut builder = MarkdownElementBuilder::new(cx.text_style(), self.style.syntax.clone()); let parsed_markdown = self.markdown.read(cx).parsed_markdown.clone(); for (range, event) in parsed_markdown.events.iter() { match event { MarkdownEvent::Start(tag) => { match tag { MarkdownTag::Paragraph => { builder.push_div(div().mb_2().line_height(rems(1.3))); } MarkdownTag::Heading { level, .. } => { let mut heading = div().mb_2(); heading = match level { pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(), pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(), pulldown_cmark::HeadingLevel::H3 => heading.text_xl(), pulldown_cmark::HeadingLevel::H4 => heading.text_lg(), _ => heading, }; builder.push_div(heading); } MarkdownTag::BlockQuote => { builder.push_text_style(self.style.block_quote.clone()); builder.push_div( div() .pl_4() .mb_2() .border_l_4() .border_color(self.style.block_quote_border_color), ); } MarkdownTag::CodeBlock(kind) => { let language = if let CodeBlockKind::Fenced(language) = kind { self.load_language(language.as_ref(), cx) } else { None }; builder.push_code_block(language); builder.push_text_style(self.style.code_block.clone());Lines 1–500 of 1033. Load more
mod parser; use crate::parser::CodeBlockKind; use futures::FutureExt; use gpui::{ actions, point, quad, AnyElement, AppContext, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, FocusHandle, FocusableView, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, KeyContext, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, Render, StrikethroughStyle, Style, StyledText, Task, TextLayout, TextRun, TextStyle, TextStyleRefinement, View, }; use language::{Language, LanguageRegistry, Rope}; use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd}; use std::{iter, mem, ops::Range, rc::Rc, sync::Arc}; use theme::SyntaxTheme; use ui::prelude::*; use util::{ResultExt, TryFutureExt}; #[derive(Clone)] pub struct MarkdownStyle { pub code_block: TextStyleRefinement, pub inline_code: TextStyleRefinement, pub block_quote: TextStyleRefinement, pub link: TextStyleRefinement, pub rule_color: Hsla, pub block_quote_border_color: Hsla, pub syntax: Arc<SyntaxTheme>, pub selection_background_color: Hsla, } pub struct Markdown { source: String, selection: Selection, pressed_link: Option<RenderedLink>, autoscroll_request: Option<usize>, style: MarkdownStyle, parsed_markdown: ParsedMarkdown, should_reparse: bool, pending_parse: Option<Task<Option<()>>>, focus_handle: FocusHandle, language_registry: Option<Arc<LanguageRegistry>>, } actions!(markdown, [Copy]); impl Markdown { pub fn new( source: String, style: MarkdownStyle, language_registry: Option<Arc<LanguageRegistry>>, cx: &mut ViewContext<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let mut this = Self { source, selection: Selection::default(), pressed_link: None, autoscroll_request: None, style, should_reparse: false, parsed_markdown: ParsedMarkdown::default(), pending_parse: None, focus_handle, language_registry, }; this.parse(cx); this } pub fn append(&mut self, text: &str, cx: &mut ViewContext<Self>) { self.source.push_str(text); self.parse(cx); } pub fn reset(&mut self, source: String, cx: &mut ViewContext<Self>) { self.source = source; self.selection = Selection::default(); self.autoscroll_request = None; self.pending_parse = None; self.should_reparse = false; self.parsed_markdown = ParsedMarkdown::default(); self.parse(cx); } pub fn source(&self) -> &str { &self.source } fn copy(&self, text: &RenderedText, cx: &mut ViewContext<Self>) { let text = text.text_for_range(self.selection.start..self.selection.end); cx.write_to_clipboard(ClipboardItem::new(text)); } fn parse(&mut self, cx: &mut ViewContext<Self>) { if self.source.is_empty() { return; } if self.pending_parse.is_some() { self.should_reparse = true; return; } let text = self.source.clone(); let parsed = cx.background_executor().spawn(async move { let text = SharedString::from(text); let events = Arc::from(parse_markdown(text.as_ref())); anyhow::Ok(ParsedMarkdown { source: text, events, }) }); self.should_reparse = false; self.pending_parse = Some(cx.spawn(|this, mut cx| { async move { let parsed = parsed.await?; this.update(&mut cx, |this, cx| { this.parsed_markdown = parsed; this.pending_parse.take(); if this.should_reparse { this.parse(cx); } cx.notify(); }) .ok(); anyhow::Ok(()) } .log_err() })); } } impl Render for Markdown { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { MarkdownElement::new( cx.view().clone(), self.style.clone(), self.language_registry.clone(), ) } } impl FocusableView for Markdown { fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { self.focus_handle.clone() } } #[derive(Copy, Clone, Default, Debug)] struct Selection { start: usize, end: usize, reversed: bool, pending: bool, } impl Selection { fn set_head(&mut self, head: usize) { if head < self.tail() { if !self.reversed { self.end = self.start; self.reversed = true; } self.start = head; } else { if self.reversed { self.start = self.end; self.reversed = false; } self.end = head; } } fn tail(&self) -> usize { if self.reversed { self.end } else { self.start } } } #[derive(Clone)] struct ParsedMarkdown { source: SharedString, events: Arc<[(Range<usize>, MarkdownEvent)]>, } impl Default for ParsedMarkdown { fn default() -> Self { Self { source: SharedString::default(), events: Arc::from([]), } } } pub struct MarkdownElement { markdown: View<Markdown>, style: MarkdownStyle, language_registry: Option<Arc<LanguageRegistry>>, } impl MarkdownElement { fn new( markdown: View<Markdown>, style: MarkdownStyle, language_registry: Option<Arc<LanguageRegistry>>, ) -> Self { Self { markdown, style, language_registry, } } fn load_language(&self, name: &str, cx: &mut WindowContext) -> Option<Arc<Language>> { let language = self .language_registry .as_ref()? .language_for_name(name) .map(|language| language.ok()) .shared(); match language.clone().now_or_never() { Some(language) => language, None => { let markdown = self.markdown.downgrade(); cx.spawn(|mut cx| async move { language.await; markdown.update(&mut cx, |_, cx| cx.notify()) }) .detach_and_log_err(cx); None } } } fn paint_selection( &mut self, bounds: Bounds<Pixels>, rendered_text: &RenderedText, cx: &mut WindowContext, ) { let selection = self.markdown.read(cx).selection; let selection_start = rendered_text.position_for_source_index(selection.start); let selection_end = rendered_text.position_for_source_index(selection.end); if let Some(((start_position, start_line_height), (end_position, end_line_height))) = selection_start.zip(selection_end) { if start_position.y == end_position.y { cx.paint_quad(quad( Bounds::from_corners( start_position, point(end_position.x, end_position.y + end_line_height), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), )); } else { cx.paint_quad(quad( Bounds::from_corners( start_position, point(bounds.right(), start_position.y + start_line_height), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), )); if end_position.y > start_position.y + start_line_height { cx.paint_quad(quad( Bounds::from_corners( point(bounds.left(), start_position.y + start_line_height), point(bounds.right(), end_position.y), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), )); } cx.paint_quad(quad( Bounds::from_corners( point(bounds.left(), end_position.y), point(end_position.x, end_position.y + end_line_height), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), )); } } } fn paint_mouse_listeners( &mut self, hitbox: &Hitbox, rendered_text: &RenderedText, cx: &mut WindowContext, ) { let is_hovering_link = hitbox.is_hovered(cx) && !self.markdown.read(cx).selection.pending && rendered_text .link_for_position(cx.mouse_position()) .is_some(); if is_hovering_link { cx.set_cursor_style(CursorStyle::PointingHand, hitbox); } else { cx.set_cursor_style(CursorStyle::IBeam, hitbox); } self.on_mouse_event(cx, { let rendered_text = rendered_text.clone(); let hitbox = hitbox.clone(); move |markdown, event: &MouseDownEvent, phase, cx| { if hitbox.is_hovered(cx) { if phase.bubble() { if let Some(link) = rendered_text.link_for_position(event.position) { markdown.pressed_link = Some(link.clone()); } else { let source_index = match rendered_text.source_index_for_position(event.position) { Ok(ix) | Err(ix) => ix, }; let range = if event.click_count == 2 { rendered_text.surrounding_word_range(source_index) } else if event.click_count == 3 { rendered_text.surrounding_line_range(source_index) } else { source_index..source_index }; markdown.selection = Selection { start: range.start, end: range.end, reversed: false, pending: true, }; cx.focus(&markdown.focus_handle); cx.prevent_default() } cx.notify(); } } else if phase.capture() { markdown.selection = Selection::default(); markdown.pressed_link = None; cx.notify(); } } }); self.on_mouse_event(cx, { let rendered_text = rendered_text.clone(); let hitbox = hitbox.clone(); let was_hovering_link = is_hovering_link; move |markdown, event: &MouseMoveEvent, phase, cx| { if phase.capture() { return; } if markdown.selection.pending { let source_index = match rendered_text.source_index_for_position(event.position) { Ok(ix) | Err(ix) => ix, }; markdown.selection.set_head(source_index); markdown.autoscroll_request = Some(source_index); cx.notify(); } else { let is_hovering_link = hitbox.is_hovered(cx) && rendered_text.link_for_position(event.position).is_some(); if is_hovering_link != was_hovering_link { cx.notify(); } } } }); self.on_mouse_event(cx, { let rendered_text = rendered_text.clone(); move |markdown, event: &MouseUpEvent, phase, cx| { if phase.bubble() { if let Some(pressed_link) = markdown.pressed_link.take() { if Some(&pressed_link) == rendered_text.link_for_position(event.position) { cx.open_url(&pressed_link.destination_url); } } } else { if markdown.selection.pending { markdown.selection.pending = false; #[cfg(target_os = "linux")] { let text = rendered_text .text_for_range(markdown.selection.start..markdown.selection.end); cx.write_to_primary(ClipboardItem::new(text)) } cx.notify(); } } } }); } fn autoscroll(&mut self, rendered_text: &RenderedText, cx: &mut WindowContext) -> Option<()> { let autoscroll_index = self .markdown .update(cx, |markdown, _| markdown.autoscroll_request.take())?; let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?; let text_style = cx.text_style(); let font_id = cx.text_system().resolve_font(&text_style.font()); let font_size = text_style.font_size.to_pixels(cx.rem_size()); let em_width = cx .text_system() .typographic_bounds(font_id, font_size, 'm') .unwrap() .size .width; cx.request_autoscroll(Bounds::from_corners( point(position.x - 3. * em_width, position.y - 3. * line_height), point(position.x + 3. * em_width, position.y + 3. * line_height), )); Some(()) } fn on_mouse_event<T: MouseEvent>( &self, cx: &mut WindowContext, mut f: impl 'static + FnMut(&mut Markdown, &T, DispatchPhase, &mut ViewContext<Markdown>), ) { cx.on_mouse_event({ let markdown = self.markdown.downgrade(); move |event, phase, cx| { markdown .update(cx, |markdown, cx| f(markdown, event, phase, cx)) .log_err(); } }); } } impl Element for MarkdownElement { type RequestLayoutState = RenderedMarkdown; type PrepaintState = Hitbox; fn id(&self) -> Option<ElementId> { None } fn request_layout( &mut self, _id: Option<&GlobalElementId>, cx: &mut WindowContext, ) -> (gpui::LayoutId, Self::RequestLayoutState) { let mut builder = MarkdownElementBuilder::new(cx.text_style(), self.style.syntax.clone()); let parsed_markdown = self.markdown.read(cx).parsed_markdown.clone(); for (range, event) in parsed_markdown.events.iter() { match event { MarkdownEvent::Start(tag) => { match tag { MarkdownTag::Paragraph => { builder.push_div(div().mb_2().line_height(rems(1.3))); } MarkdownTag::Heading { level, .. } => { let mut heading = div().mb_2(); heading = match level { pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(), pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(), pulldown_cmark::HeadingLevel::H3 => heading.text_xl(), pulldown_cmark::HeadingLevel::H4 => heading.text_lg(), _ => heading, }; builder.push_div(heading); } MarkdownTag::BlockQuote => { builder.push_text_style(self.style.block_quote.clone()); builder.push_div( div() .pl_4() .mb_2() .border_l_4() .border_color(self.style.block_quote_border_color), ); } MarkdownTag::CodeBlock(kind) => { let language = if let CodeBlockKind::Fenced(language) = kind { self.load_language(language.as_ref(), cx) } else { None }; builder.push_code_block(language); builder.push_text_style(self.style.code_block.clone());