mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
markdown: Add ability to wrap/unwrap codeblock content (#57312)
This PR adds an icon button to Markdown codeblocks allowing to control whether or not the content should be wrapped. At the moment, this is not hard-persisted, meaning that 1) wrapping text in one codeblock instance does not affect others, and 2) the codeblock will be reset every time its view is recreated (i.e., closing and opening a Markdown Preview tab, an agent thread, etc.). I intentionally kept it simple just to see how it feels, but we can certainly consider a setting later on. | Unwrapping | Wrapping | |--------|--------| | <img width="782" height="658" alt="Screenshot 2026-05-20 at 5 09 2@2x" src="https://github.com/user-attachments/assets/e9151e91-32ba-40d4-9c65-535dec309291" /> | <img width="736" height="604" alt="Screenshot 2026-05-20 at 5 09@2x" src="https://github.com/user-attachments/assets/157db6fd-ec4c-4c96-b44a-119273cbd0f9" /> | Release Notes: - Added the ability to control codeblock content wrapping through the UI.
This commit is contained in:
parent
2b506483b2
commit
0042fb5850
16 changed files with 142 additions and 26 deletions
6
assets/icons/text_unwrap.svg
Normal file
6
assets/icons/text_unwrap.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.748 5.74794L14 7.99998L11.748 10.252" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 7.99997L3 7.99997" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 3L8 6" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 10L8 13" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 566 B |
5
assets/icons/text_wrap.svg
Normal file
5
assets/icons/text_wrap.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.19924 8.90081L2.9472 6.64876L5.19924 4.39673" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.9472 6.64877H7.67649C8.00178 6.64877 8.32391 6.71285 8.6245 6.83734C8.92501 6.96184 9.19813 7.14431 9.42817 7.37434C9.6582 7.60437 9.84067 7.87746 9.96512 8.17802C10.0897 8.47856 10.1537 8.8007 10.1537 9.12601C10.1537 9.45131 10.0897 9.77345 9.96512 10.074C9.84067 10.3745 9.6582 10.6477 9.42817 10.8777C9.19813 11.1077 8.92501 11.2902 8.6245 11.4147C8.32391 11.5392 8.00178 11.6033 7.67649 11.6033H6.10005" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.0528 3.33331V12.6666" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 871 B |
|
|
@ -508,6 +508,7 @@ impl AcpTools {
|
|||
} else {
|
||||
CopyButtonVisibility::Hidden
|
||||
},
|
||||
wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3133,6 +3133,11 @@ fn render_agent_markdown(
|
|||
})
|
||||
.unwrap_or_default();
|
||||
MarkdownElement::new(markdown, style)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button_visibility: markdown::CopyButtonVisibility::VisibleOnHover,
|
||||
wrap_button_visibility: markdown::WrapButtonVisibility::VisibleOnHover,
|
||||
border: false,
|
||||
})
|
||||
.image_resolver(move |dest_url| resolve_agent_image(dest_url, &worktree_roots))
|
||||
.on_url_click(move |text, window, cx| {
|
||||
thread_view::open_link(text, &workspace, window, cx);
|
||||
|
|
|
|||
|
|
@ -5991,6 +5991,7 @@ impl ThreadView {
|
|||
.render_markdown(command, style, cx)
|
||||
.code_block_renderer(CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||
wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
});
|
||||
let copy_button = CopyButton::new("copy-command", command_text)
|
||||
|
|
|
|||
|
|
@ -240,6 +240,7 @@ impl DiagnosticBlock {
|
|||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||
wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click({
|
||||
|
|
|
|||
|
|
@ -1260,6 +1260,7 @@ impl CompletionsMenu {
|
|||
MarkdownElement::new(markdown, hover_markdown_style(window, cx))
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||
wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(open_markdown_url),
|
||||
|
|
|
|||
|
|
@ -1043,6 +1043,7 @@ impl InfoPopover {
|
|||
MarkdownElement::new(markdown, hover_markdown_style(window, cx))
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||
wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(open_markdown_url)
|
||||
|
|
@ -1157,6 +1158,7 @@ impl DiagnosticPopover {
|
|||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||
wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(
|
||||
|
|
|
|||
|
|
@ -409,6 +409,8 @@ impl SignatureHelpPopover {
|
|||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||
wrap_button_visibility:
|
||||
markdown::WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(open_markdown_url),
|
||||
|
|
@ -421,6 +423,8 @@ impl SignatureHelpPopover {
|
|||
MarkdownElement::new(description, hover_markdown_style(window, cx))
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||
wrap_button_visibility:
|
||||
markdown::WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(open_markdown_url),
|
||||
|
|
|
|||
|
|
@ -239,6 +239,8 @@ pub enum IconName {
|
|||
Terminal,
|
||||
TerminalAlt,
|
||||
TextSnippet,
|
||||
TextWrap,
|
||||
TextUnwrap,
|
||||
ThinkingMode,
|
||||
ThinkingModeOff,
|
||||
Thread,
|
||||
|
|
|
|||
|
|
@ -561,6 +561,8 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
use crate::WrapButtonVisibility;
|
||||
|
||||
fn render_markdown_text(markdown: &str, cx: &mut TestAppContext) -> crate::RenderedText {
|
||||
struct TestWindow;
|
||||
|
||||
|
|
@ -582,6 +584,7 @@ mod tests {
|
|||
MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer(
|
||||
CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||
wrap_button_visibility: WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
},
|
||||
)
|
||||
|
|
@ -642,6 +645,7 @@ mod tests {
|
|||
MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer(
|
||||
CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||
wrap_button_visibility: WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -334,6 +334,7 @@ pub struct Markdown {
|
|||
mermaid_state: MermaidState,
|
||||
mermaid_showing_code: HashSet<usize>,
|
||||
copied_code_blocks: HashSet<ElementId>,
|
||||
wrapped_code_blocks: HashSet<usize>,
|
||||
code_block_scroll_handles: BTreeMap<usize, ScrollHandle>,
|
||||
context_menu_link: Option<SharedString>,
|
||||
context_menu_selected_text: Option<String>,
|
||||
|
|
@ -356,9 +357,17 @@ pub enum CopyButtonVisibility {
|
|||
VisibleOnHover,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WrapButtonVisibility {
|
||||
Hidden,
|
||||
AlwaysVisible,
|
||||
VisibleOnHover,
|
||||
}
|
||||
|
||||
pub enum CodeBlockRenderer {
|
||||
Default {
|
||||
copy_button_visibility: CopyButtonVisibility,
|
||||
wrap_button_visibility: WrapButtonVisibility,
|
||||
border: bool,
|
||||
},
|
||||
Custom {
|
||||
|
|
@ -505,6 +514,7 @@ impl Markdown {
|
|||
mermaid_state: MermaidState::default(),
|
||||
mermaid_showing_code: HashSet::default(),
|
||||
copied_code_blocks: HashSet::default(),
|
||||
wrapped_code_blocks: HashSet::default(),
|
||||
code_block_scroll_handles: BTreeMap::default(),
|
||||
context_menu_link: None,
|
||||
context_menu_selected_text: None,
|
||||
|
|
@ -528,6 +538,16 @@ impl Markdown {
|
|||
)
|
||||
}
|
||||
|
||||
fn is_code_block_wrapped(&self, id: usize) -> bool {
|
||||
self.wrapped_code_blocks.contains(&id)
|
||||
}
|
||||
|
||||
fn toggle_code_block_wrap(&mut self, id: usize) {
|
||||
if !self.wrapped_code_blocks.remove(&id) {
|
||||
self.wrapped_code_blocks.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
fn code_block_scroll_handle(&mut self, id: usize) -> ScrollHandle {
|
||||
self.code_block_scroll_handles
|
||||
.entry(id)
|
||||
|
|
@ -1073,6 +1093,7 @@ impl MarkdownElement {
|
|||
style,
|
||||
code_block_renderer: CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::VisibleOnHover,
|
||||
wrap_button_visibility: WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
},
|
||||
on_url_click: None,
|
||||
|
|
@ -1891,11 +1912,18 @@ impl Element for MarkdownElement {
|
|||
parent_container.style().refine(&self.style.code_block);
|
||||
builder.push_div(parent_container, range, markdown_end);
|
||||
|
||||
let is_wrapped =
|
||||
self.markdown.read(cx).is_code_block_wrapped(range.start);
|
||||
|
||||
let code_block = div()
|
||||
.id(("code-block", range.start))
|
||||
.rounded_lg()
|
||||
.map(|mut code_block| {
|
||||
if let Some(scroll_handle) = scroll_handle.as_ref() {
|
||||
if is_wrapped {
|
||||
code_block.w_full()
|
||||
} else if let Some(scroll_handle) =
|
||||
scroll_handle.as_ref()
|
||||
{
|
||||
code_block.style().restrict_scroll_to_axis =
|
||||
Some(true);
|
||||
code_block
|
||||
|
|
@ -2134,10 +2162,14 @@ impl Element for MarkdownElement {
|
|||
|
||||
if let CodeBlockRenderer::Default {
|
||||
copy_button_visibility,
|
||||
wrap_button_visibility,
|
||||
..
|
||||
} = &self.code_block_renderer
|
||||
&& *copy_button_visibility != CopyButtonVisibility::Hidden
|
||||
&& (*copy_button_visibility != CopyButtonVisibility::Hidden
|
||||
|| *wrap_button_visibility != WrapButtonVisibility::Hidden)
|
||||
{
|
||||
let copy_button_visibility = *copy_button_visibility;
|
||||
let wrap_button_visibility = *wrap_button_visibility;
|
||||
builder.modify_current_div(|el| {
|
||||
let content_range = parser::extract_code_block_content_range(
|
||||
&parsed_markdown.source()[range.clone()],
|
||||
|
|
@ -2146,28 +2178,48 @@ impl Element for MarkdownElement {
|
|||
..content_range.end + range.start;
|
||||
|
||||
let code = parsed_markdown.source()[content_range].to_string();
|
||||
let codeblock = render_copy_code_block_button(
|
||||
|
||||
let any_hover = copy_button_visibility
|
||||
== CopyButtonVisibility::VisibleOnHover
|
||||
|| wrap_button_visibility
|
||||
== WrapButtonVisibility::VisibleOnHover;
|
||||
let any_always = copy_button_visibility
|
||||
== CopyButtonVisibility::AlwaysVisible
|
||||
|| wrap_button_visibility
|
||||
== WrapButtonVisibility::AlwaysVisible;
|
||||
let use_hover = any_hover && !any_always;
|
||||
|
||||
let mut button_row = h_flex()
|
||||
.gap_0p5()
|
||||
.absolute()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.when_else(
|
||||
use_hover,
|
||||
|this| {
|
||||
this.top_1().right_1().visible_on_hover("code_block")
|
||||
},
|
||||
|this| this.top_1p5().right_1p5(),
|
||||
);
|
||||
|
||||
if wrap_button_visibility != WrapButtonVisibility::Hidden {
|
||||
let is_wrapped =
|
||||
self.markdown.read(cx).is_code_block_wrapped(range.start);
|
||||
button_row = button_row.child(render_wrap_code_block_button(
|
||||
range.start,
|
||||
is_wrapped,
|
||||
self.markdown.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
if copy_button_visibility != CopyButtonVisibility::Hidden {
|
||||
button_row = button_row.child(render_copy_code_block_button(
|
||||
range.end,
|
||||
code,
|
||||
self.markdown.clone(),
|
||||
);
|
||||
el.child(
|
||||
h_flex()
|
||||
.w_4()
|
||||
.absolute()
|
||||
.justify_end()
|
||||
.when_else(
|
||||
*copy_button_visibility
|
||||
== CopyButtonVisibility::VisibleOnHover,
|
||||
|this| {
|
||||
this.top_0()
|
||||
.right_0()
|
||||
.visible_on_hover("code_block")
|
||||
},
|
||||
|this| this.top_1p5().right_1p5(),
|
||||
)
|
||||
.child(codeblock),
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
el.child(button_row)
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -2418,6 +2470,29 @@ fn apply_heading_style(
|
|||
heading
|
||||
}
|
||||
|
||||
fn render_wrap_code_block_button(
|
||||
id: usize,
|
||||
is_wrapped: bool,
|
||||
markdown: Entity<Markdown>,
|
||||
) -> impl IntoElement {
|
||||
let (icon, tooltip) = if is_wrapped {
|
||||
(IconName::TextUnwrap, "Unwrap Content")
|
||||
} else {
|
||||
(IconName::TextWrap, "Wrap Content")
|
||||
};
|
||||
|
||||
IconButton::new(("wrap-code-block", id), icon)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text(tooltip))
|
||||
.on_click(move |_event, _window, cx| {
|
||||
markdown.update(cx, |markdown, cx| {
|
||||
markdown.toggle_code_block_wrap(id);
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn render_copy_code_block_button(
|
||||
id: usize,
|
||||
code: String,
|
||||
|
|
@ -3434,6 +3509,7 @@ mod tests {
|
|||
MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer(
|
||||
CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||
wrap_button_visibility: WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -585,7 +585,7 @@ mod tests {
|
|||
};
|
||||
use crate::{
|
||||
CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownOptions,
|
||||
MarkdownStyle,
|
||||
MarkdownStyle, WrapButtonVisibility,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use gpui::{Context, Hsla, IntoElement, Render, RenderImage, TestAppContext, Window, size};
|
||||
|
|
@ -644,6 +644,7 @@ mod tests {
|
|||
MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer(
|
||||
CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||
wrap_button_visibility: WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
},
|
||||
)
|
||||
|
|
@ -924,6 +925,7 @@ mod tests {
|
|||
MarkdownElement::new(markdown.clone(), MarkdownStyle::default())
|
||||
.code_block_renderer(CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||
wrap_button_visibility: WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
})
|
||||
},
|
||||
|
|
|
|||
|
|
@ -623,6 +623,7 @@ impl MarkdownPreviewView {
|
|||
let mut markdown_element = MarkdownElement::new(self.markdown.clone(), markdown_style)
|
||||
.code_block_renderer(CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::VisibleOnHover,
|
||||
wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
})
|
||||
.scroll_handle(self.scroll_handle.clone())
|
||||
|
|
|
|||
|
|
@ -402,6 +402,7 @@ impl Render for LanguageServerPrompt {
|
|||
.text_size(TextSize::Small.rems(cx))
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button_visibility: CopyButtonVisibility::Hidden,
|
||||
wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(|link, _, cx| cx.open_url(&link)),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ use gpui::{
|
|||
StyleRefinement, Task, TextStyleRefinement, Window, list, prelude::*,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use markdown::{CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle};
|
||||
use markdown::{
|
||||
CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle,
|
||||
WrapButtonVisibility,
|
||||
};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use telemetry_events::{Event, EventWrapper};
|
||||
|
|
@ -429,6 +432,7 @@ impl TelemetryLogView {
|
|||
} else {
|
||||
CopyButtonVisibility::Hidden
|
||||
},
|
||||
wrap_button_visibility: WrapButtonVisibility::Hidden,
|
||||
border: false,
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue