diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 4f714b9b451a54a4a01099922763ece5df2f6b8a..2efae05285f77b6c7ec62a6aabbab979558b0ab3 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -22,8 +22,8 @@ use gpui::{ AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity, FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image, ImageFormat, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, - Point, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun, - TextStyle, TextStyleRefinement, actions, img, point, quad, + Point, ScrollHandle, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, + TextLayout, TextRun, TextStyle, TextStyleRefinement, actions, img, point, quad, }; use language::{Language, LanguageRegistry, Rope}; use parser::CodeBlockMetadata; @@ -31,7 +31,7 @@ use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse use pulldown_cmark::Alignment; use sum_tree::TreeMap; use theme::SyntaxTheme; -use ui::{Tooltip, prelude::*}; +use ui::{ScrollAxes, Scrollbars, Tooltip, WithScrollbar, prelude::*}; use util::ResultExt; use crate::parser::CodeBlockKind; @@ -108,6 +108,7 @@ pub struct Markdown { fallback_code_block_language: Option, options: Options, copied_code_blocks: HashSet, + code_block_scroll_handles: HashMap, } struct Options { @@ -176,6 +177,7 @@ impl Markdown { parse_links_only: false, }, copied_code_blocks: HashSet::default(), + code_block_scroll_handles: HashMap::default(), }; this.parse(cx); this @@ -199,11 +201,28 @@ impl Markdown { parse_links_only: true, }, copied_code_blocks: HashSet::default(), + code_block_scroll_handles: HashMap::default(), }; this.parse(cx); this } + fn code_block_scroll_handle(&mut self, id: usize) -> ScrollHandle { + self.code_block_scroll_handles + .entry(id) + .or_insert_with(ScrollHandle::new) + .clone() + } + + fn retain_code_block_scroll_handles(&mut self, ids: &HashSet) { + self.code_block_scroll_handles + .retain(|id, _| ids.contains(id)); + } + + fn clear_code_block_scroll_handles(&mut self) { + self.code_block_scroll_handles.clear(); + } + pub fn is_parsing(&self) -> bool { self.pending_parse.is_some() } @@ -754,14 +773,19 @@ impl Element for MarkdownElement { self.style.base_text_style.clone(), self.style.syntax.clone(), ); - let markdown = self.markdown.read(cx); - let parsed_markdown = &markdown.parsed_markdown; - let images = &markdown.images_by_source_offset; + let (parsed_markdown, images) = { + let markdown = self.markdown.read(cx); + ( + markdown.parsed_markdown.clone(), + markdown.images_by_source_offset.clone(), + ) + }; let markdown_end = if let Some(last) = parsed_markdown.events.last() { last.0.end } else { 0 }; + let mut code_block_ids = HashSet::default(); let mut current_code_block_metadata = None; let mut current_img_block_range: Option> = None; @@ -841,39 +865,69 @@ impl Element for MarkdownElement { current_code_block_metadata = Some(metadata.clone()); let is_indented = matches!(kind, CodeBlockKind::Indented); + let scroll_handle = if self.style.code_block_overflow_x_scroll { + code_block_ids.insert(range.start); + Some(self.markdown.update(cx, |markdown, _| { + markdown.code_block_scroll_handle(range.start) + })) + } else { + None + }; match (&self.code_block_renderer, is_indented) { (CodeBlockRenderer::Default { .. }, _) | (_, true) => { // This is a parent container that we can position the copy button inside. - builder.push_div( - div().group("code_block").relative().w_full(), - range, - markdown_end, - ); + let parent_container = + div().group("code_block").relative().w_full(); + + let mut parent_container: AnyDiv = if let Some(scroll_handle) = + scroll_handle.as_ref() + { + let scrollbars = Scrollbars::new(ScrollAxes::Horizontal) + .id(("markdown-code-block-scrollbar", range.start)) + .tracked_scroll_handle(scroll_handle.clone()) + .with_track_along( + ScrollAxes::Horizontal, + cx.theme().colors().editor_background, + ) + .notify_content(); + + parent_container + .rounded_lg() + .custom_scrollbars(scrollbars, window, cx) + .into() + } else { + parent_container.into() + }; + + if let CodeBlockRenderer::Default { border: true, .. } = + &self.code_block_renderer + { + parent_container = parent_container + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border_variant); + } - let mut code_block = div() + parent_container.style().refine(&self.style.code_block); + builder.push_div(parent_container, range, markdown_end); + + let code_block = div() .id(("code-block", range.start)) .rounded_lg() .map(|mut code_block| { - if self.style.code_block_overflow_x_scroll { + if let Some(scroll_handle) = scroll_handle.as_ref() { code_block.style().restrict_scroll_to_axis = Some(true); - code_block.flex().overflow_x_scroll() + code_block + .flex() + .overflow_x_scroll() + .track_scroll(scroll_handle) } else { code_block.w_full() } }); - if let CodeBlockRenderer::Default { border: true, .. } = - &self.code_block_renderer - { - code_block = code_block - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border_variant); - } - - code_block.style().refine(&self.style.code_block); if let Some(code_block_text_style) = &self.style.code_block.text { builder.push_text_style(code_block_text_style.to_owned()); @@ -884,33 +938,53 @@ impl Element for MarkdownElement { (CodeBlockRenderer::Custom { render, .. }, _) => { let parent_container = render( kind, - parsed_markdown, + &parsed_markdown, range.clone(), metadata.clone(), window, cx, ); + let mut parent_container: AnyDiv = if let Some(scroll_handle) = + scroll_handle.as_ref() + { + let scrollbars = Scrollbars::new(ScrollAxes::Horizontal) + .id(("markdown-code-block-scrollbar", range.start)) + .tracked_scroll_handle(scroll_handle.clone()) + .with_track_along( + ScrollAxes::Horizontal, + cx.theme().colors().editor_background, + ) + .notify_content(); + + parent_container + .rounded_b_lg() + .custom_scrollbars(scrollbars, window, cx) + .into() + } else { + parent_container.into() + }; + + parent_container.style().refine(&self.style.code_block); builder.push_div(parent_container, range, markdown_end); - let mut code_block = div() + let code_block = div() .id(("code-block", range.start)) .rounded_b_lg() .map(|mut code_block| { - if self.style.code_block_overflow_x_scroll { + if let Some(scroll_handle) = scroll_handle.as_ref() { code_block.style().restrict_scroll_to_axis = Some(true); code_block .flex() .overflow_x_scroll() .overflow_y_hidden() + .track_scroll(scroll_handle) } else { code_block.w_full().overflow_hidden() } }); - code_block.style().refine(&self.style.code_block); - if let Some(code_block_text_style) = &self.style.code_block.text { builder.push_text_style(code_block_text_style.to_owned()); @@ -1218,6 +1292,15 @@ impl Element for MarkdownElement { _ => log::debug!("unsupported markdown event {:?}", event), } } + if self.style.code_block_overflow_x_scroll { + let code_block_ids = code_block_ids; + self.markdown.update(cx, move |markdown, _| { + markdown.retain_code_block_scroll_handles(&code_block_ids); + }); + } else { + self.markdown + .update(cx, |markdown, _| markdown.clear_code_block_scroll_handles()); + } let mut rendered_markdown = builder.build(); let child_layout_id = rendered_markdown.element.request_layout(window, cx); let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);