Skip to content

Commit 4f30a36

Browse files
committed
Refine Mermaid layout settings and default alignment
1 parent 5c8a1a2 commit 4f30a36

9 files changed

Lines changed: 635 additions & 72 deletions

File tree

‎assets/settings/default.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@
129129
"max_width": 800,
130130
// Whether to constrain top-level Mermaid blocks to `mermaid_max_width`.
131131
// When disabled, no Mermaid-specific maximum width is applied.
132-
// Zed's default layout is preserved when both Mermaid width options
133-
// are disabled and mermaid_alignment is "left".
132+
// To restore Zed's native layout, disable both Mermaid width options
133+
// and set mermaid_alignment to "left".
134134
"limit_mermaid_width": false,
135135
// The maximum width, in pixels, of top-level Mermaid blocks when
136136
// `limit_mermaid_width` is enabled and `mermaid_width_follows_diagram`
@@ -145,7 +145,7 @@
145145
// "center", or "right". In the default or limited-width layout this
146146
// aligns the diagram within the Mermaid block; when
147147
// `mermaid_width_follows_diagram` is enabled, it aligns the block itself.
148-
"mermaid_alignment": "left",
148+
"mermaid_alignment": "center",
149149
},
150150
// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier.
151151
//

‎crates/markdown/src/markdown.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,9 +1731,9 @@ pub enum MermaidAlignment {
17311731

17321732
/// Layout overrides for top-level Mermaid diagrams.
17331733
///
1734-
/// Default values preserve Zed's native Mermaid layout. Diagrams nested in
1735-
/// lists, block quotes, or other constrained parents continue to use their
1736-
/// parent layout.
1734+
/// These defaults describe Zed's native renderer, independently of the
1735+
/// Markdown Preview preference defaults. Nested diagrams use this baseline
1736+
/// to retain their parent layout.
17371737
#[derive(Clone, Copy, Debug, Default, PartialEq)]
17381738
pub struct MermaidLayout {
17391739
/// Optional Mermaid-specific maximum width for the block.

‎crates/markdown_preview/src/markdown_preview_settings.rs‎

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use markdown::{MermaidAlignment, MermaidLayout};
33
use settings::{IntoGpui, RegisterSetting, Settings};
44

55
/// The settings for the markdown preview.
6-
#[derive(Clone, Copy, Debug, Default, RegisterSetting)]
6+
#[derive(Clone, Copy, Debug, RegisterSetting)]
77
pub struct MarkdownPreviewSettings {
88
/// Whether to automatically open Markdown files in the preview.
99
pub open_markdown_files_in_preview: bool,
@@ -14,6 +14,12 @@ pub struct MarkdownPreviewSettings {
1414
pub mermaid_layout: MermaidLayout,
1515
}
1616

17+
impl Default for MarkdownPreviewSettings {
18+
fn default() -> Self {
19+
Self::from_settings(&settings::SettingsContent::default())
20+
}
21+
}
22+
1723
impl Settings for MarkdownPreviewSettings {
1824
fn from_settings(content: &settings::SettingsContent) -> Self {
1925
let content = content.markdown_preview.clone().unwrap_or_default();
@@ -64,11 +70,51 @@ mod tests {
6470
.expect("valid preview settings");
6571
let settings = MarkdownPreviewSettings::from_settings(&content);
6672
assert_eq!(settings.max_width, limit_content_width.then_some(px(900.)));
67-
assert_eq!(settings.mermaid_layout, MermaidLayout::default());
73+
assert_eq!(
74+
settings.mermaid_layout,
75+
MermaidLayout {
76+
alignment: MermaidAlignment::Center,
77+
..MermaidLayout::default()
78+
}
79+
);
80+
assert!(!settings.mermaid_layout.has_width_override());
6881
}
6982
}
7083
}
7184

85+
#[test]
86+
fn test_mermaid_alignment_defaults_to_center_and_preserves_explicit_choices() {
87+
assert_eq!(
88+
MarkdownPreviewSettings::default().mermaid_layout.alignment,
89+
MermaidAlignment::Center
90+
);
91+
for (content, expected) in [
92+
(json!({}), MermaidAlignment::Center),
93+
(json!({"markdown_preview": {}}), MermaidAlignment::Center),
94+
(
95+
json!({"markdown_preview": {"mermaid_alignment": null}}),
96+
MermaidAlignment::Center,
97+
),
98+
(
99+
json!({"markdown_preview": {"mermaid_alignment": "left"}}),
100+
MermaidAlignment::Left,
101+
),
102+
(
103+
json!({"markdown_preview": {"mermaid_alignment": "center"}}),
104+
MermaidAlignment::Center,
105+
),
106+
(
107+
json!({"markdown_preview": {"mermaid_alignment": "right"}}),
108+
MermaidAlignment::Right,
109+
),
110+
] {
111+
let content = serde_json::from_value(content).expect("valid preview settings");
112+
let settings = MarkdownPreviewSettings::from_settings(&content);
113+
assert_eq!(settings.mermaid_layout.alignment, expected);
114+
assert!(!settings.mermaid_layout.has_width_override());
115+
}
116+
}
117+
72118
#[test]
73119
fn test_mermaid_settings_keep_width_flags_and_alignment_independent() {
74120
for limit_mermaid_width in [false, true] {

‎crates/settings_content/src/settings_content.rs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,9 +1304,9 @@ pub enum LineIndicatorFormat {
13041304
#[serde(rename_all = "snake_case")]
13051305
pub enum MermaidAlignment {
13061306
/// Align the diagram to the left.
1307-
#[default]
13081307
Left,
13091308
/// Center the diagram.
1309+
#[default]
13101310
Center,
13111311
/// Align the diagram to the right.
13121312
Right,
@@ -1345,8 +1345,8 @@ pub struct MarkdownPreviewSettingsContent {
13451345
pub max_width: Option<PixelSetting>,
13461346
/// Whether to constrain top-level Mermaid blocks to `mermaid_max_width`.
13471347
/// When disabled, no Mermaid-specific maximum width is applied.
1348-
/// Zed's default layout is preserved when both Mermaid width options
1349-
/// are disabled and `mermaid_alignment` is `left`.
1348+
/// To restore Zed's native layout, disable both Mermaid width options
1349+
/// and set `mermaid_alignment` to `left`.
13501350
///
13511351
/// Default: false
13521352
pub limit_mermaid_width: Option<bool>,
@@ -1362,7 +1362,7 @@ pub struct MarkdownPreviewSettingsContent {
13621362
/// the Mermaid block. When `mermaid_width_follows_diagram` is enabled,
13631363
/// it aligns the block itself.
13641364
///
1365-
/// Default: left
1365+
/// Default: center
13661366
pub mermaid_alignment: Option<MermaidAlignment>,
13671367
/// Whether top-level Mermaid blocks follow the rendered diagram's 100%
13681368
/// natural width, with enough space for controls, instead of using Zed's

‎crates/settings_ui/src/components/number_field.rs‎

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ pub struct NumberField<T: NumberFieldType = usize> {
277277
tab_index: Option<isize>,
278278
aria_label: Option<SharedString>,
279279
aria_description: Option<SharedString>,
280+
disabled: bool,
280281
}
281282

282283
impl<T: NumberFieldType> NumberField<T> {
@@ -319,6 +320,7 @@ impl<T: NumberFieldType> NumberField<T> {
319320
tab_index: None,
320321
aria_label: None,
321322
aria_description: None,
323+
disabled: false,
322324
}
323325
}
324326

@@ -342,6 +344,11 @@ impl<T: NumberFieldType> NumberField<T> {
342344
self
343345
}
344346

347+
pub fn disabled(mut self, disabled: bool) -> Self {
348+
self.disabled = disabled;
349+
self
350+
}
351+
345352
pub fn on_change(mut self, on_change: impl Fn(&T, &mut Window, &mut App) + 'static) -> Self {
346353
self.on_change = Rc::new(on_change);
347354
self
@@ -390,6 +397,66 @@ fn a11y_value_to_field_value<T: NumberFieldType>(value: f64) -> Option<T> {
390397

391398
impl<T: NumberFieldType> RenderOnce for NumberField<T> {
392399
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
400+
if self.disabled {
401+
// Invalidate the retained editor's blur callback before removing it
402+
// from the element tree, so disabling cannot commit an old draft.
403+
self.on_change_state.write(cx, None);
404+
if let Some(editor) = self
405+
.edit_editor
406+
.read(cx)
407+
.as_ref()
408+
.and_then(|editor| editor.upgrade())
409+
{
410+
editor.update(cx, |editor, cx| {
411+
editor.set_read_only(true);
412+
editor.set_text(format!("{}", self.value), window, cx);
413+
});
414+
}
415+
self.last_synced_value.write(cx, Some(self.value));
416+
417+
let border_color = cx.theme().colors().border_variant;
418+
let background = cx.theme().colors().surface_background;
419+
let button = |icon| {
420+
h_flex()
421+
.p_1p5()
422+
.border_1()
423+
.border_color(border_color)
424+
.bg(background)
425+
.child(Icon::new(icon).size(IconSize::Small).color(Color::Disabled))
426+
};
427+
return h_flex()
428+
.id(self.id.clone())
429+
.items_stretch()
430+
.role(Role::SpinButton)
431+
.when_some(self.aria_label, |this, label| this.aria_label(label))
432+
.when_some(self.aria_description, |this, description| {
433+
this.aria_description(description)
434+
})
435+
.when_some(a11y_numeric_value(&self.value), |this, value| {
436+
this.aria_numeric_value(value)
437+
})
438+
.child(button(IconName::Dash).rounded_tl_sm().rounded_bl_sm())
439+
.child(
440+
h_flex()
441+
.min_w_16()
442+
.px_1()
443+
.border_y_1()
444+
.border_color(border_color)
445+
.bg(background)
446+
.justify_center()
447+
.child(Label::new((self.format)(&self.value)).color(Color::Disabled)),
448+
)
449+
.child(button(IconName::Plus).rounded_tr_sm().rounded_br_sm())
450+
.into_any_element();
451+
}
452+
if let Some(editor) = self
453+
.edit_editor
454+
.read(cx)
455+
.as_ref()
456+
.and_then(|editor| editor.upgrade())
457+
{
458+
editor.update(cx, |editor, _| editor.set_read_only(false));
459+
}
393460
// Sync the on_change callback to Entity state so focus_out handlers can access it
394461
self.sync_on_change_state(cx);
395462

@@ -834,6 +901,7 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
834901
)
835902
})
836903
})
904+
.into_any_element()
837905
}
838906
}
839907

@@ -884,3 +952,110 @@ impl Component for NumberField<usize> {
884952
.into_any_element()
885953
}
886954
}
955+
956+
#[cfg(test)]
957+
mod tests {
958+
use super::*;
959+
960+
struct NumberFieldTestView {
961+
value: usize,
962+
disabled: bool,
963+
changes: usize,
964+
editor_state: Option<Entity<Option<WeakEntity<Editor>>>>,
965+
callback_state: Option<Entity<Option<OnChangeCallback<usize>>>>,
966+
}
967+
968+
impl Render for NumberFieldTestView {
969+
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
970+
let field = NumberField::new("number-field", self.value, window, cx)
971+
.mode(NumberFieldMode::Edit, cx)
972+
.disabled(self.disabled)
973+
.on_change(cx.listener(|this, value, _, cx| {
974+
this.value = *value;
975+
this.changes += 1;
976+
cx.notify();
977+
}));
978+
self.editor_state = Some(field.edit_editor.clone());
979+
self.callback_state = Some(field.on_change_state.clone());
980+
div().child(field)
981+
}
982+
}
983+
984+
#[gpui::test]
985+
fn disabled_number_field_discards_pending_edit_and_reenables(cx: &mut gpui::TestAppContext) {
986+
cx.update(crate::test::register_settings);
987+
let (view, cx) = cx.add_window_view(|_, _| NumberFieldTestView {
988+
value: 1200,
989+
disabled: false,
990+
changes: 0,
991+
editor_state: None,
992+
callback_state: None,
993+
});
994+
cx.run_until_parked();
995+
996+
let editor = view.read_with(cx, |view, cx| {
997+
view.editor_state
998+
.as_ref()
999+
.expect("editor state")
1000+
.read(cx)
1001+
.as_ref()
1002+
.and_then(|editor| editor.upgrade())
1003+
.expect("number editor")
1004+
});
1005+
cx.update(|window, cx| {
1006+
editor.update(cx, |editor, cx| {
1007+
window.focus(&editor.focus_handle(cx), cx);
1008+
editor.set_text("9999", window, cx);
1009+
});
1010+
});
1011+
view.update(cx, |view, cx| {
1012+
view.disabled = true;
1013+
cx.notify();
1014+
});
1015+
cx.run_until_parked();
1016+
assert!(editor.read_with(cx, |editor, cx| editor.read_only(cx)));
1017+
assert_eq!(editor.read_with(cx, |editor, cx| editor.text(cx)), "1200");
1018+
assert!(view.read_with(cx, |view, cx| {
1019+
view.callback_state
1020+
.as_ref()
1021+
.expect("callback state")
1022+
.read(cx)
1023+
.is_none()
1024+
}));
1025+
cx.update(|window, cx| window.blur(cx));
1026+
cx.run_until_parked();
1027+
assert_eq!(
1028+
view.read_with(cx, |view, _| (view.value, view.changes)),
1029+
(1200, 0)
1030+
);
1031+
1032+
view.update(cx, |view, cx| {
1033+
view.disabled = false;
1034+
cx.notify();
1035+
});
1036+
cx.run_until_parked();
1037+
let (editor, on_change) = view.read_with(cx, |view, cx| {
1038+
(
1039+
view.editor_state
1040+
.as_ref()
1041+
.expect("editor state")
1042+
.read(cx)
1043+
.as_ref()
1044+
.and_then(|editor| editor.upgrade())
1045+
.expect("number editor"),
1046+
view.callback_state
1047+
.as_ref()
1048+
.expect("callback state")
1049+
.read(cx)
1050+
.clone()
1051+
.expect("enabled callback"),
1052+
)
1053+
});
1054+
assert!(!editor.read_with(cx, |editor, cx| editor.read_only(cx)));
1055+
cx.update(|window, cx| on_change(&1300, window, cx));
1056+
assert_eq!(
1057+
view.read_with(cx, |view, _| (view.value, view.changes)),
1058+
(1300, 1)
1059+
);
1060+
}
1061+
}

0 commit comments

Comments
 (0)