Skip to content

Commit aff4c25

Browse files
authored
markdown: Restore horizontal scrollbars for codeblocks (#40736)
### Summary Restore the agent pane’s code-block horizontal scrollbar for easier scrolling without trackpad and preserve individual scroll state across multiple code blocks. ### Motivation Addresses #34224, where agent responses with wide code snippets couldn’t be scrolled horizontally in the panel. Previously there is no visual effect for scrollbar to let the user move the code snippet and it was not obviously to use trackpad or hold down `shift` while scrolling. This PR will ensure the user being able to only use their mouse to drag the horizontal scrollbar to show the complete line when the code overflow the width of code block. ### Changes - Support auto-hide horizontal scrollbar for rendering code block in agent panel by adding scrollbar support in markdown.rs - Add `code_block_scroll_handles` cache in _crates/markdown/src/markdown.rs_ to give each code block a persistent `ScrollHandle`. - Wrap rendered code blocks with custom horizontal scrollbars that match the vertical scrollbar styling and track hover visibility. - Retain or clear scroll handles based on whether horizontal overflow is enabled, preventing leaks when the markdown re-renders. ### How to Test 1. Open the agent panel, request code generation, and ensure wide snippets show a horizontal scrollbar on hover. 3. Scroll horizontally, navigate away (e.g., change tabs or trigger a re-render), and confirm the scroll position sticks when returning. 5. Toggle horizontal overflow styling off/on (if applicable) and verify scrollbars appear or disappear appropriately. ### Screenshots / Demos (if UI change) https://github.com/user-attachments/assets/e23f94d9-8fe3-42f5-8f77-81b1005a14c8 ### Notes for Reviewers - This is my first time contribution for `zed`, sorry for any code patten inconsistency. So please let me know if you have any comments and suggestions to make the code pattern consistent and easy to maintain. - For now, the horizontal scrollbar is not configurable from the setting and the style is fixed with the same design as the vertical one. I am happy to readjust this setting to fit the needs. - Please let me know if you think any behaviors or designs need to be changed for the scrollbar. - All changes live inside _crates/markdown/src/markdown.rs_; no API surface changes. Closes #34224 ### Release Notes: - AI: Show horizontal scroll-bars in wide markdown elements
1 parent 146e754 commit aff4c25

File tree

1 file changed

+112
-29
lines changed

1 file changed

+112
-29
lines changed

crates/markdown/src/markdown.rs

Lines changed: 112 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@ use gpui::{
2222
AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity,
2323
FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image,
2424
ImageFormat, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent,
25-
Point, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun,
26-
TextStyle, TextStyleRefinement, actions, img, point, quad,
25+
Point, ScrollHandle, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task,
26+
TextLayout, TextRun, TextStyle, TextStyleRefinement, actions, img, point, quad,
2727
};
2828
use language::{Language, LanguageRegistry, Rope};
2929
use parser::CodeBlockMetadata;
3030
use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown};
3131
use pulldown_cmark::Alignment;
3232
use sum_tree::TreeMap;
3333
use theme::SyntaxTheme;
34-
use ui::{Tooltip, prelude::*};
34+
use ui::{ScrollAxes, Scrollbars, Tooltip, WithScrollbar, prelude::*};
3535
use util::ResultExt;
3636

3737
use crate::parser::CodeBlockKind;
@@ -108,6 +108,7 @@ pub struct Markdown {
108108
fallback_code_block_language: Option<LanguageName>,
109109
options: Options,
110110
copied_code_blocks: HashSet<ElementId>,
111+
code_block_scroll_handles: HashMap<usize, ScrollHandle>,
111112
}
112113

113114
struct Options {
@@ -176,6 +177,7 @@ impl Markdown {
176177
parse_links_only: false,
177178
},
178179
copied_code_blocks: HashSet::default(),
180+
code_block_scroll_handles: HashMap::default(),
179181
};
180182
this.parse(cx);
181183
this
@@ -199,11 +201,28 @@ impl Markdown {
199201
parse_links_only: true,
200202
},
201203
copied_code_blocks: HashSet::default(),
204+
code_block_scroll_handles: HashMap::default(),
202205
};
203206
this.parse(cx);
204207
this
205208
}
206209

210+
fn code_block_scroll_handle(&mut self, id: usize) -> ScrollHandle {
211+
self.code_block_scroll_handles
212+
.entry(id)
213+
.or_insert_with(ScrollHandle::new)
214+
.clone()
215+
}
216+
217+
fn retain_code_block_scroll_handles(&mut self, ids: &HashSet<usize>) {
218+
self.code_block_scroll_handles
219+
.retain(|id, _| ids.contains(id));
220+
}
221+
222+
fn clear_code_block_scroll_handles(&mut self) {
223+
self.code_block_scroll_handles.clear();
224+
}
225+
207226
pub fn is_parsing(&self) -> bool {
208227
self.pending_parse.is_some()
209228
}
@@ -754,14 +773,19 @@ impl Element for MarkdownElement {
754773
self.style.base_text_style.clone(),
755774
self.style.syntax.clone(),
756775
);
757-
let markdown = self.markdown.read(cx);
758-
let parsed_markdown = &markdown.parsed_markdown;
759-
let images = &markdown.images_by_source_offset;
776+
let (parsed_markdown, images) = {
777+
let markdown = self.markdown.read(cx);
778+
(
779+
markdown.parsed_markdown.clone(),
780+
markdown.images_by_source_offset.clone(),
781+
)
782+
};
760783
let markdown_end = if let Some(last) = parsed_markdown.events.last() {
761784
last.0.end
762785
} else {
763786
0
764787
};
788+
let mut code_block_ids = HashSet::default();
765789

766790
let mut current_code_block_metadata = None;
767791
let mut current_img_block_range: Option<Range<usize>> = None;
@@ -841,39 +865,69 @@ impl Element for MarkdownElement {
841865
current_code_block_metadata = Some(metadata.clone());
842866

843867
let is_indented = matches!(kind, CodeBlockKind::Indented);
868+
let scroll_handle = if self.style.code_block_overflow_x_scroll {
869+
code_block_ids.insert(range.start);
870+
Some(self.markdown.update(cx, |markdown, _| {
871+
markdown.code_block_scroll_handle(range.start)
872+
}))
873+
} else {
874+
None
875+
};
844876

845877
match (&self.code_block_renderer, is_indented) {
846878
(CodeBlockRenderer::Default { .. }, _) | (_, true) => {
847879
// This is a parent container that we can position the copy button inside.
848-
builder.push_div(
849-
div().group("code_block").relative().w_full(),
850-
range,
851-
markdown_end,
852-
);
880+
let parent_container =
881+
div().group("code_block").relative().w_full();
882+
883+
let mut parent_container: AnyDiv = if let Some(scroll_handle) =
884+
scroll_handle.as_ref()
885+
{
886+
let scrollbars = Scrollbars::new(ScrollAxes::Horizontal)
887+
.id(("markdown-code-block-scrollbar", range.start))
888+
.tracked_scroll_handle(scroll_handle.clone())
889+
.with_track_along(
890+
ScrollAxes::Horizontal,
891+
cx.theme().colors().editor_background,
892+
)
893+
.notify_content();
894+
895+
parent_container
896+
.rounded_lg()
897+
.custom_scrollbars(scrollbars, window, cx)
898+
.into()
899+
} else {
900+
parent_container.into()
901+
};
902+
903+
if let CodeBlockRenderer::Default { border: true, .. } =
904+
&self.code_block_renderer
905+
{
906+
parent_container = parent_container
907+
.rounded_md()
908+
.border_1()
909+
.border_color(cx.theme().colors().border_variant);
910+
}
853911

854-
let mut code_block = div()
912+
parent_container.style().refine(&self.style.code_block);
913+
builder.push_div(parent_container, range, markdown_end);
914+
915+
let code_block = div()
855916
.id(("code-block", range.start))
856917
.rounded_lg()
857918
.map(|mut code_block| {
858-
if self.style.code_block_overflow_x_scroll {
919+
if let Some(scroll_handle) = scroll_handle.as_ref() {
859920
code_block.style().restrict_scroll_to_axis =
860921
Some(true);
861-
code_block.flex().overflow_x_scroll()
922+
code_block
923+
.flex()
924+
.overflow_x_scroll()
925+
.track_scroll(scroll_handle)
862926
} else {
863927
code_block.w_full()
864928
}
865929
});
866930

867-
if let CodeBlockRenderer::Default { border: true, .. } =
868-
&self.code_block_renderer
869-
{
870-
code_block = code_block
871-
.rounded_md()
872-
.border_1()
873-
.border_color(cx.theme().colors().border_variant);
874-
}
875-
876-
code_block.style().refine(&self.style.code_block);
877931
if let Some(code_block_text_style) = &self.style.code_block.text
878932
{
879933
builder.push_text_style(code_block_text_style.to_owned());
@@ -884,33 +938,53 @@ impl Element for MarkdownElement {
884938
(CodeBlockRenderer::Custom { render, .. }, _) => {
885939
let parent_container = render(
886940
kind,
887-
parsed_markdown,
941+
&parsed_markdown,
888942
range.clone(),
889943
metadata.clone(),
890944
window,
891945
cx,
892946
);
893947

948+
let mut parent_container: AnyDiv = if let Some(scroll_handle) =
949+
scroll_handle.as_ref()
950+
{
951+
let scrollbars = Scrollbars::new(ScrollAxes::Horizontal)
952+
.id(("markdown-code-block-scrollbar", range.start))
953+
.tracked_scroll_handle(scroll_handle.clone())
954+
.with_track_along(
955+
ScrollAxes::Horizontal,
956+
cx.theme().colors().editor_background,
957+
)
958+
.notify_content();
959+
960+
parent_container
961+
.rounded_b_lg()
962+
.custom_scrollbars(scrollbars, window, cx)
963+
.into()
964+
} else {
965+
parent_container.into()
966+
};
967+
968+
parent_container.style().refine(&self.style.code_block);
894969
builder.push_div(parent_container, range, markdown_end);
895970

896-
let mut code_block = div()
971+
let code_block = div()
897972
.id(("code-block", range.start))
898973
.rounded_b_lg()
899974
.map(|mut code_block| {
900-
if self.style.code_block_overflow_x_scroll {
975+
if let Some(scroll_handle) = scroll_handle.as_ref() {
901976
code_block.style().restrict_scroll_to_axis =
902977
Some(true);
903978
code_block
904979
.flex()
905980
.overflow_x_scroll()
906981
.overflow_y_hidden()
982+
.track_scroll(scroll_handle)
907983
} else {
908984
code_block.w_full().overflow_hidden()
909985
}
910986
});
911987

912-
code_block.style().refine(&self.style.code_block);
913-
914988
if let Some(code_block_text_style) = &self.style.code_block.text
915989
{
916990
builder.push_text_style(code_block_text_style.to_owned());
@@ -1218,6 +1292,15 @@ impl Element for MarkdownElement {
12181292
_ => log::debug!("unsupported markdown event {:?}", event),
12191293
}
12201294
}
1295+
if self.style.code_block_overflow_x_scroll {
1296+
let code_block_ids = code_block_ids;
1297+
self.markdown.update(cx, move |markdown, _| {
1298+
markdown.retain_code_block_scroll_handles(&code_block_ids);
1299+
});
1300+
} else {
1301+
self.markdown
1302+
.update(cx, |markdown, _| markdown.clear_code_block_scroll_handles());
1303+
}
12211304
let mut rendered_markdown = builder.build();
12221305
let child_layout_id = rendered_markdown.element.request_layout(window, cx);
12231306
let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);

0 commit comments

Comments
 (0)