markdown_preview: Add font size settings and actions (#55489)

Add support for scaling the Markdown preview's body text and headings
with `cmd-=` / `cmd--` / `cmd-0`. Previously these shortcuts only
resized code blocks because the preview's body used `ui_font_size` and
headings used a rem-based scale anchored to it, while the keybindings
only mutated `BufferFontSize`.

The preview's scroll subtree is now wrapped in
`WithRemSize(buffer_font_size)`, so `1rem` resolves to the buffer font
size inside the preview. Body text, headings
(`text_3xl`/`text_2xl`/...), and rem-based spacing all scale together.
The outer focus root and scrollbar remain at the UI font size.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #55374

Release Notes:

- Added `markdown_preview_font_size` setting and actions to scale
Markdown preview font size separately from the editor.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
David AlecrimandSmit Barmase authored and GitHub committed 2026-06-26 05:12:59 +00:00
1 parent fea22b3934
commit 4306847359
9 files changed
+349 -47

No files matched your search

+6
View File
@@ -73,6 +73,12 @@
"agent_buffer_font_size": 12,
// The default font size for the commit editor in the git panel and commit modal.
"git_commit_buffer_font_size": 12,
// The default font size for the markdown preview. Falls back to the editor font size if unset.
"markdown_preview_font_size": null,
// The font family for the markdown preview. Falls back to the UI font family if unset.
"markdown_preview_font_family": null,
// The font family for code blocks in the markdown preview. Falls back to the editor font family if unset.
"markdown_preview_code_font_family": null,
// How much to fade out unused code.
"unnecessary_code_fade": 0.3,
// Active pane styling settings.
+102 -6
View File
@@ -175,10 +175,14 @@ impl MarkdownStyle {
theme_settings.agent_buffer_font_size(cx),
theme_settings.agent_ui_font_size(cx),
),
MarkdownFont::Editor | MarkdownFont::Preview => (
MarkdownFont::Editor => (
theme_settings.buffer_font_size(cx),
theme_settings.ui_font_size(cx),
),
MarkdownFont::Preview => (
theme_settings.markdown_preview_font_size(cx),
theme_settings.ui_font_size(cx),
),
};
let body_font_family = if is_preview {
@@ -199,7 +203,11 @@ impl MarkdownStyle {
font_family: Some(body_font_family),
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
font_features: Some(theme_settings.ui_font.features.clone()),
font_size: Some(ui_font_size.into()),
font_size: Some(if is_preview {
rems(1.0).into()
} else {
ui_font_size.into()
}),
line_height: Some(line_height.into()),
color: Some(colors.text),
..Default::default()
@@ -306,14 +314,14 @@ impl MarkdownStyle {
};
if is_preview {
style.with_preview_overrides(ui_font_size, colors)
style.with_preview_overrides(colors)
} else {
style
}
}
fn with_preview_overrides(mut self, ui_font_size: Pixels, colors: &theme::ThemeColors) -> Self {
let body_font_size = ui_font_size * 0.92;
fn with_preview_overrides(mut self, colors: &theme::ThemeColors) -> Self {
let body_font_size = rems(0.92);
self.base_text_style.font_size = body_font_size.into();
self.container_style.text.font_size = Some(body_font_size.into());
@@ -3966,7 +3974,7 @@ impl RenderedText {
#[cfg(test)]
mod tests {
use super::*;
use gpui::{RenderImage, TestAppContext, size};
use gpui::{RenderImage, TestAppContext, UpdateGlobal, size};
use language::{Language, LanguageConfig, LanguageMatcher};
use std::sync::{
Arc,
@@ -5168,6 +5176,28 @@ mod tests {
});
}
#[gpui::test]
fn test_preview_body_font_size_is_rem_based(cx: &mut TestAppContext) {
ensure_theme_initialized(cx);
let (_, cx) = cx.add_window_view(|_, _| TestWindow);
cx.update(|window, cx| {
let style = MarkdownStyle::themed(MarkdownFont::Preview, window, cx);
assert!(
matches!(style.base_text_style.font_size, AbsoluteLength::Rems(_)),
"preview body font size must be rem-based, got {:?}",
style.base_text_style.font_size
);
assert!(
matches!(
style.container_style.text.font_size,
Some(AbsoluteLength::Rems(_))
),
"preview container font size must be rem-based, got {:?}",
style.container_style.text.font_size
);
});
}
#[track_caller]
fn assert_mappings(rendered: &RenderedText, expected: Vec<Vec<(usize, usize)>>) {
assert_eq!(rendered.lines.len(), expected.len(), "line count mismatch");
@@ -5276,4 +5306,70 @@ mod tests {
"H3 line height ({h3_line_height:?}) should be greater than body text ({body_line_height:?})"
);
}
#[gpui::test]
fn test_editor_zoom_does_not_affect_markdown_preview(cx: &mut TestAppContext) {
ensure_theme_initialized(cx);
cx.update(|cx| {
settings::SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |settings| {
settings.theme.buffer_font_size = Some(16.0.into());
settings.theme.markdown_preview_font_size = None;
});
});
});
cx.run_until_parked();
cx.update(|cx| {
let before = ThemeSettings::get_global(cx).markdown_preview_font_size(cx);
assert_eq!(before, px(16.0));
theme_settings::increase_buffer_font_size(cx);
theme_settings::increase_buffer_font_size(cx);
theme_settings::increase_buffer_font_size(cx);
assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(19.0));
assert_eq!(
ThemeSettings::get_global(cx).markdown_preview_font_size(cx),
before
);
});
}
#[gpui::test]
fn test_markdown_preview_follows_buffer_font_size_setting_when_unset(cx: &mut TestAppContext) {
ensure_theme_initialized(cx);
cx.update(|cx| {
settings::SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |settings| {
settings.theme.buffer_font_size = Some(20.0.into());
settings.theme.markdown_preview_font_size = None;
});
});
});
cx.run_until_parked();
cx.update(|cx| {
assert_eq!(
ThemeSettings::get_global(cx).markdown_preview_font_size(cx),
px(20.0)
);
});
cx.update(|cx| {
settings::SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |settings| {
settings.theme.buffer_font_size = Some(24.0.into());
});
});
});
cx.run_until_parked();
cx.update(|cx| {
assert_eq!(
ThemeSettings::get_global(cx).markdown_preview_font_size(cx),
px(24.0)
);
});
}
}
@@ -12,7 +12,7 @@ use editor::{Editor, EditorEvent, MultiBufferOffset, SelectionEffects};
use gpui::{
App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageSource,
InteractiveElement, IntoElement, IsZero, Pixels, Render, Resource, RetainAllImageCache,
ScrollHandle, SharedString, SharedUri, Subscription, Task, WeakEntity, Window, point,
ScrollHandle, SharedString, SharedUri, Subscription, Task, WeakEntity, Window, point, px,
};
use language::LanguageRegistry;
use markdown::{
@@ -21,9 +21,10 @@ use markdown::{
};
use project::search::SearchQuery;
use project::{Project, ProjectPath};
use settings::{SeedQuerySetting, Settings};
use settings::{SeedQuerySetting, Settings, update_settings_file};
use theme::{SystemAppearance, Theme, ThemeRegistry};
use theme_settings::ThemeSettings;
use ui::utils::WithRemSize;
use ui::{ContextMenu, WithScrollbar, prelude::*, right_click_menu};
use util::markdown::split_local_url_fragment;
use workspace::item::{Item, ItemBufferKind, ItemHandle, SaveOptions, SerializableItem};
@@ -31,6 +32,7 @@ use workspace::searchable::{
Direction, SearchEvent, SearchOptions, SearchToken, SearchableItem, SearchableItemHandle,
};
use workspace::{ItemId, Pane, Workspace, WorkspaceId, delete_unloaded_items};
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
use crate::markdown_preview_settings::MarkdownPreviewSettings;
use crate::{
@@ -589,7 +591,64 @@ impl MarkdownPreviewView {
fn line_scroll_amount(&self, cx: &App) -> Pixels {
let settings = ThemeSettings::get_global(cx);
settings.buffer_font_size(cx) * settings.buffer_line_height.value()
settings.markdown_preview_font_size(cx) * settings.buffer_line_height.value()
}
fn increase_font_size(
&mut self,
action: &IncreaseBufferFontSize,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.adjust_font_size(action.persist, px(1.0), cx);
}
fn decrease_font_size(
&mut self,
action: &DecreaseBufferFontSize,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.adjust_font_size(action.persist, px(-1.0), cx);
}
fn adjust_font_size(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
if persist {
let Ok(fs) = self
.workspace
.read_with(cx, |workspace, _| workspace.app_state().fs.clone())
else {
return;
};
update_settings_file(fs, cx, move |settings, cx| {
let size = ThemeSettings::get_global(cx).markdown_preview_font_size(cx) + delta;
settings.theme.markdown_preview_font_size =
Some(f32::from(theme_settings::clamp_font_size(size)).into());
});
} else {
theme_settings::adjust_markdown_preview_font_size(cx, |size| size + delta);
}
}
fn reset_font_size(
&mut self,
action: &ResetBufferFontSize,
_: &mut Window,
cx: &mut Context<Self>,
) {
if action.persist {
let Ok(fs) = self
.workspace
.read_with(cx, |workspace, _| workspace.app_state().fs.clone())
else {
return;
};
update_settings_file(fs, cx, move |settings, _| {
settings.theme.markdown_preview_font_size = None;
});
} else {
theme_settings::reset_markdown_preview_font_size(cx);
}
}
fn scroll_by_amount(&self, distance: Pixels) {
@@ -1127,6 +1186,7 @@ impl Render for MarkdownPreviewView {
.as_ref()
.map(|theme| theme.colors().editor_background)
.unwrap_or_else(|| cx.theme().colors().editor_background);
let preview_font_size = ThemeSettings::get_global(cx).markdown_preview_font_size(cx);
div()
.image_cache(self.image_cache.clone())
.id("MarkdownPreview")
@@ -1140,44 +1200,51 @@ impl Render for MarkdownPreviewView {
.on_action(cx.listener(MarkdownPreviewView::scroll_down_by_item))
.on_action(cx.listener(MarkdownPreviewView::scroll_to_top))
.on_action(cx.listener(MarkdownPreviewView::scroll_to_bottom))
.on_action(cx.listener(MarkdownPreviewView::increase_font_size))
.on_action(cx.listener(MarkdownPreviewView::decrease_font_size))
.on_action(cx.listener(MarkdownPreviewView::reset_font_size))
.w_full()
.flex_1()
.min_h_0()
.bg(bg_color)
.child(
div()
.id("markdown-preview-scroll-container")
.size_full()
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
.p_4()
.child({
let markdown_element =
self.render_markdown_element(&preview_theme, window, cx);
let markdown = self.markdown.clone();
let max_width = MarkdownPreviewSettings::get_global(cx).max_width;
let content = right_click_menu("markdown-preview-context-menu")
.trigger(move |_, _, _| markdown_element)
.menu(move |window, cx| {
let focus = window.focused(cx);
let context_menu_link =
markdown.read(cx).context_menu_link().cloned();
ContextMenu::build(window, cx, move |menu, _, _cx| {
menu.when_some(focus, |menu, focus| menu.context(focus))
.when_some(context_menu_link, |menu, url| {
menu.entry("Copy Link", None, move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(
url.to_string(),
));
WithRemSize::new(preview_font_size).size_full().child(
div()
.id("markdown-preview-scroll-container")
.size_full()
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
.p_4()
.child({
let markdown_element =
self.render_markdown_element(&preview_theme, window, cx);
let markdown = self.markdown.clone();
let max_width = MarkdownPreviewSettings::get_global(cx).max_width;
let content = right_click_menu("markdown-preview-context-menu")
.trigger(move |_, _, _| markdown_element)
.menu(move |window, cx| {
let focus = window.focused(cx);
let context_menu_link =
markdown.read(cx).context_menu_link().cloned();
ContextMenu::build(window, cx, move |menu, _, _cx| {
menu.when_some(focus, |menu, focus| menu.context(focus))
.when_some(context_menu_link, |menu, url| {
menu.entry("Copy Link", None, move |_, cx| {
cx.write_to_clipboard(
ClipboardItem::new_string(url.to_string()),
);
})
})
})
})
});
div()
.w_full()
.when_some(max_width, |this, max_width| {
this.max_w(max_width).mx_auto()
})
});
div()
.w_full()
.when_some(max_width, |this, max_width| this.max_w(max_width).mx_auto())
.child(content)
}),
.child(content)
}),
),
)
.vertical_scrollbar_for(&self.scroll_handle, window, cx)
}
+1
View File
@@ -982,6 +982,7 @@ impl VsCodeSettings {
git_commit_buffer_font_size: None,
markdown_preview_font_family: None,
markdown_preview_code_font_family: None,
markdown_preview_font_size: None,
markdown_preview_theme: None,
theme: None,
icon_theme: None,
+3
View File
@@ -156,6 +156,9 @@ pub struct ThemeSettingsContent {
/// The name of a font to use for code (code blocks and inline code) in the
/// markdown preview. Falls back to the buffer font if unset.
pub markdown_preview_code_font_family: Option<FontFamilyName>,
/// The font size to use for rendering in the markdown preview.
/// Falls back to the buffer font size if unset.
pub markdown_preview_font_size: Option<FontSize>,
/// The theme to use for the markdown preview.
/// Falls back to the main editor theme if unset.
pub markdown_preview_theme: Option<ThemeSelection>,
+62
View File
@@ -1161,6 +1161,67 @@ fn appearance_page() -> SettingsPage {
]
}
fn markdown_preview_font_section() -> [SettingsPageItem; 4] {
[
SettingsPageItem::SectionHeader("Markdown Preview Font"),
SettingsPageItem::SettingItem(SettingItem {
title: "Font Family",
description: "Font family for the markdown preview. Falls back to the UI font family.",
field: Box::new(SettingField {
organization_override: None,
json_path: Some("markdown_preview_font_family"),
pick: |settings_content| {
settings_content.theme.markdown_preview_font_family.as_ref()
},
write: |settings_content, value, _| {
settings_content.theme.markdown_preview_font_family = value;
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Code Font Family",
description: "Font family for code blocks in the markdown preview. Falls back to the editor font family.",
field: Box::new(SettingField {
organization_override: None,
json_path: Some("markdown_preview_code_font_family"),
pick: |settings_content| {
settings_content
.theme
.markdown_preview_code_font_family
.as_ref()
},
write: |settings_content, value, _| {
settings_content.theme.markdown_preview_code_font_family = value;
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Font Size",
description: "Font size for the markdown preview. Falls back to the editor font size.",
field: Box::new(SettingField {
organization_override: None,
json_path: Some("markdown_preview_font_size"),
pick: |settings_content| {
settings_content
.theme
.markdown_preview_font_size
.as_ref()
.or(settings_content.theme.buffer_font_size.as_ref())
},
write: |settings_content, value, _| {
settings_content.theme.markdown_preview_font_size = value;
},
}),
metadata: None,
files: USER,
}),
]
}
fn text_rendering_section() -> [SettingsPageItem; 2] {
[
SettingsPageItem::SectionHeader("Text Rendering"),
@@ -1389,6 +1450,7 @@ fn appearance_page() -> SettingsPage {
buffer_font_section(),
ui_font_section(),
agent_panel_font_section(),
markdown_preview_font_section(),
text_rendering_section(),
cursor_section(),
highlighting_section(),
+48
View File
@@ -63,6 +63,9 @@ pub struct ThemeSettings {
/// The font family to use for code in the markdown preview.
/// Falls back to the buffer font family if unset.
markdown_preview_code_font_family: Option<SharedString>,
/// The font size to use for rendering in the markdown preview.
/// Falls back to the buffer font size if unset.
markdown_preview_font_size: Option<Pixels>,
/// The theme to use for the markdown preview.
/// Falls back to the main editor theme if unset.
pub markdown_preview_theme: Option<ThemeSelection>,
@@ -124,6 +127,12 @@ pub struct GitCommitBufferFontSize(Pixels);
impl Global for GitCommitBufferFontSize {}
/// In-memory override for the markdown preview font size.
#[derive(Default)]
pub struct MarkdownPreviewFontSize(Pixels);
impl Global for MarkdownPreviewFontSize {}
/// Represents the selection of a theme, which can be either static or dynamic.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(untagged)]
@@ -440,6 +449,18 @@ impl ThemeSettings {
.unwrap_or(&self.buffer_font.family)
}
/// Returns the markdown preview font size.
///
/// Note: the fallback deliberately uses `self.buffer_font_size` instead of `buffer_font_size(cx)`,
/// so that temporary editor zoom does not also resize the markdown preview.
pub fn markdown_preview_font_size(&self, cx: &App) -> Pixels {
cx.try_global::<MarkdownPreviewFontSize>()
.map(|size| size.0)
.or(self.markdown_preview_font_size)
.map(clamp_font_size)
.unwrap_or_else(|| clamp_font_size(self.buffer_font_size))
}
/// Returns the buffer font size, read from the settings.
///
/// The real buffer font size is stored in-memory, to support temporary font size changes.
@@ -476,6 +497,14 @@ impl ThemeSettings {
self.git_commit_buffer_font_size
}
/// Returns the markdown preview font size, read from the settings.
///
/// The real markdown preview font size is stored in-memory, to support temporary
/// font size changes. Use [`Self::markdown_preview_font_size`] to get the real font size.
pub fn markdown_preview_font_size_settings(&self) -> Option<Pixels> {
self.markdown_preview_font_size
}
/// Returns the buffer's line height.
pub fn line_height(&self) -> f32 {
f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
@@ -643,6 +672,24 @@ pub fn reset_git_commit_buffer_font_size(cx: &mut App) {
}
}
/// Sets the adjusted font size of the markdown preview.
pub fn adjust_markdown_preview_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) {
let markdown_preview_font_size = ThemeSettings::get_global(cx).markdown_preview_font_size(cx);
let adjusted_size = cx
.try_global::<MarkdownPreviewFontSize>()
.map_or(markdown_preview_font_size, |adjusted_size| adjusted_size.0);
cx.set_global(MarkdownPreviewFontSize(clamp_font_size(f(adjusted_size))));
cx.refresh_windows();
}
/// Resets the markdown preview font size to the default value.
pub fn reset_markdown_preview_font_size(cx: &mut App) {
if cx.has_global::<MarkdownPreviewFontSize>() {
cx.remove_global::<MarkdownPreviewFontSize>();
cx.refresh_windows();
}
}
/// Ensures font size is within the valid range.
pub fn clamp_font_size(size: Pixels) -> Pixels {
size.clamp(MIN_FONT_SIZE, MAX_FONT_SIZE)
@@ -701,6 +748,7 @@ impl settings::Settings for ThemeSettings {
.markdown_preview_code_font_family
.as_ref()
.map(|f| f.0.clone().into()),
markdown_preview_font_size: content.markdown_preview_font_size.map(|s| s.into_gpui()),
markdown_preview_theme: content
.markdown_preview_theme
.clone()
+15 -6
View File
@@ -29,13 +29,14 @@ pub use crate::schema::{
use crate::settings::adjust_buffer_font_size;
pub use crate::settings::{
AgentBufferFontSize, AgentUiFontSize, BufferLineHeight, FontFamilyName,
GitCommitBufferFontSize, IconThemeName, IconThemeSelection, ThemeAppearanceMode, ThemeName,
ThemeSelection, ThemeSettings, adjust_agent_buffer_font_size, adjust_agent_ui_font_size,
adjust_git_commit_buffer_font_size, adjust_ui_font_size, adjusted_font_size,
appearance_to_mode, clamp_font_size, default_theme, observe_buffer_font_size_adjustment,
GitCommitBufferFontSize, IconThemeName, IconThemeSelection, MarkdownPreviewFontSize,
ThemeAppearanceMode, ThemeName, ThemeSelection, ThemeSettings, adjust_agent_buffer_font_size,
adjust_agent_ui_font_size, adjust_git_commit_buffer_font_size,
adjust_markdown_preview_font_size, adjust_ui_font_size, adjusted_font_size, appearance_to_mode,
clamp_font_size, default_theme, observe_buffer_font_size_adjustment,
reset_agent_buffer_font_size, reset_agent_ui_font_size, reset_buffer_font_size,
reset_git_commit_buffer_font_size, reset_ui_font_size, set_icon_theme, set_mode, set_theme,
setup_ui_font,
reset_git_commit_buffer_font_size, reset_markdown_preview_font_size, reset_ui_font_size,
set_icon_theme, set_mode, set_theme, setup_ui_font,
};
pub use theme::UiDensity;
@@ -91,6 +92,8 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut App) {
let mut prev_agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings();
let mut prev_git_commit_buffer_font_size_settings =
settings.git_commit_buffer_font_size_settings();
let mut prev_markdown_preview_font_size_settings =
settings.markdown_preview_font_size_settings();
let mut prev_theme_name = settings.theme.name(SystemAppearance::global(cx).0);
let mut prev_icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0);
let mut prev_theme_overrides = (
@@ -106,6 +109,7 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut App) {
let agent_ui_font_size_settings = settings.agent_ui_font_size_settings();
let agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings();
let git_commit_buffer_font_size_settings = settings.git_commit_buffer_font_size_settings();
let markdown_preview_font_size_settings = settings.markdown_preview_font_size_settings();
let theme_name = settings.theme.name(SystemAppearance::global(cx).0);
let icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0);
let theme_overrides = (
@@ -138,6 +142,11 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut App) {
reset_git_commit_buffer_font_size(cx);
}
if markdown_preview_font_size_settings != prev_markdown_preview_font_size_settings {
prev_markdown_preview_font_size_settings = markdown_preview_font_size_settings;
reset_markdown_preview_font_size(cx);
}
if theme_name != prev_theme_name || theme_overrides != prev_theme_overrides {
prev_theme_name = theme_name;
prev_theme_overrides = theme_overrides;
+11 -1
View File
@@ -71,7 +71,17 @@ If you would like to use distinct themes for light mode/dark mode that can be se
"agent_ui_font_size": 15,
// Controls the font size for the agent panel's message editor, user message,
// and any other snippet of code.
"agent_buffer_font_size": 12
"agent_buffer_font_size": 12,
// Controls the font size for the markdown preview.
// If not specified, it falls back to the editor font size.
"markdown_preview_font_size": null,
// Controls the font family for the markdown preview.
// If not specified, it falls back to the UI font family.
"markdown_preview_font_family": null,
// Controls the font family for code blocks in the markdown preview.
// If not specified, it falls back to the editor font family.
"markdown_preview_code_font_family": null
```
### Font ligatures