From 1ff993dcb17a5babdb710885bf96dcb829357f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 16 Oct 2026 14:27:26 -0400 Subject: [PATCH] Documents: an open heading keeps its weight and color; front matter keeps its panel The field's highlighter gave headings and bold semibold over the drawn bold, and its plain color hid a sixth-level heading's muted color. It now uses the drawn weight, and runs inside a sixth-level heading stay muted. Front matter opens in its mono, muted panel. The open text box is built by one function, so tests read its background directly. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/documents/document/look.rs | 2 +- src/documents/document/tests/mod.rs | 6 +- src/documents/document/tests/open_look.rs | 121 +++++++++++++++++++++- src/documents/document/visual.rs | 75 ++++++++------ src/documents/markdown.rs | 22 +++- 5 files changed, 182 insertions(+), 44 deletions(-) diff --git a/src/documents/document/look.rs b/src/documents/document/look.rs index 6aafbd5..72972e1 100644 --- a/src/documents/document/look.rs +++ b/src/documents/document/look.rs @@ -90,7 +90,7 @@ impl MarkdownDocument { block.size.height -= self.room; } let view = self.list.viewport_bounds(); - // A new layout, a resize or the ends' room moves it. + // New layout, resize or end room moves it. let heights = [block.size.height, view.size.height, self.room] .map(|height| f32::from(height).round() as u64); let moved = (ix, self.edits, self.typed, heights); diff --git a/src/documents/document/tests/mod.rs b/src/documents/document/tests/mod.rs index fdd7b63..3f9fee3 100644 --- a/src/documents/document/tests/mod.rs +++ b/src/documents/document/tests/mod.rs @@ -210,8 +210,10 @@ fn the_source_colors_markdown(cx: &mut TestAppContext) { document.editor().read(cx).line_colors("a **b**", cx) }); assert!( - colors.iter().any(|(range, style)| *range == (2..7) - && style.font_weight == Some(gpui::FontWeight::SEMIBOLD)), + colors + .iter() + .any(|(range, style)| *range == (2..7) + && style.font_weight == Some(gpui::FontWeight::BOLD)), "{colors:?}" ); } diff --git a/src/documents/document/tests/open_look.rs b/src/documents/document/tests/open_look.rs index 2b71f2c..f283746 100644 --- a/src/documents/document/tests/open_look.rs +++ b/src/documents/document/tests/open_look.rs @@ -1,9 +1,17 @@ //! An open block keeps its place and size (R7.5). -use gpui::{Bounds, Modifiers, Pixels, TestAppContext, VisualTestContext, rems}; +use gpui::{ + Bounds, Fill, Modifiers, Pixels, StyleRefinement, Styled, TestAppContext, VisualTestContext, + rems, +}; use super::{open, press}; -use crate::documents::MarkdownMode; +use crate::documents::{TargetBlock, document::visual::open_text}; +use crate::{ + documents::{MarkdownMode, markdown_highlights, render::HEAVY}, + forms::Highlight, + theme::ActiveTheme, +}; /// Block 0's bounds closed, then open. fn closed_then_open<'a>( @@ -50,7 +58,17 @@ fn an_open_heading_keeps_its_size(cx: &mut TestAppContext) { #[gpui::test] fn an_open_code_block_keeps_its_panel(cx: &mut TestAppContext) { - let (closed, open, cx) = closed_then_open("```\nlet x = 1;\n```\n", cx); + keeps_its_panel("```\nlet x = 1;\n```\n", cx); +} + +#[gpui::test] +fn open_front_matter_keeps_its_panel(cx: &mut TestAppContext) { + keeps_its_panel("---\nkey: value\n---\n\nText.\n", cx); +} + +/// Block 0 opens in place, its panel at the closed inset. +fn keeps_its_panel(source: &str, cx: &mut TestAppContext) { + let (closed, open, cx) = closed_then_open(source, cx); assert!( near(closed.top(), open.top()) && near(closed.left(), open.left()), "{closed:?} {open:?}" @@ -71,3 +89,100 @@ fn an_open_code_block_keeps_its_panel(cx: &mut TestAppContext) { "{panel:?} {closed:?}" ); } + +/// The highlight on the run that holds `word`. +fn run(source: &str, word: &str, cx: &mut TestAppContext) -> Highlight { + cx.update(crate::init_for_tests); + let at = source.find(word).expect("the word"); + cx.update(|cx| { + markdown_highlights(source, cx) + .into_iter() + .rfind(|(range, _)| range.start <= at && at < range.end) + .expect("a run") + .1 + }) +} + +#[gpui::test] +fn an_open_heading_keeps_its_weight(cx: &mut TestAppContext) { + for source in [ + "# Heavy words +", + "a **Heavy** b +", + ] { + assert_eq!(run(source, "Heavy", cx).weight, Some(HEAVY), "{source}"); + } +} + +#[gpui::test] +fn an_open_sixth_heading_stays_muted(cx: &mut TestAppContext) { + let muted = cx.update(|cx| { + crate::init_for_tests(cx); + cx.theme().colors.fg_muted + }); + for word in ["Quiet", "Loud"] { + let source = "###### Quiet **Loud** +"; + assert_eq!(run(source, word, cx).color, muted, "{word}"); + } + let fg = cx.update(|cx| cx.theme().colors.fg); + assert_eq!( + run( + "##### Plain +", + "Plain", + cx + ) + .color, + fg + ); +} + +/// The open text box's style for `kind`. +fn styled(kind: TargetBlock, cx: &mut TestAppContext) -> StyleRefinement { + cx.update(crate::init_for_tests); + cx.update(|cx| { + open_text(Some((kind, true)), 1.0, cx.theme()) + .style() + .clone() + }) +} + +#[gpui::test] +fn open_prose_has_no_panel(cx: &mut TestAppContext) { + for kind in [ + TargetBlock::Paragraph, + TargetBlock::Heading(2), + TargetBlock::List, + TargetBlock::Quote, + ] { + assert_eq!(styled(kind, cx).background, None, "{kind:?}"); + } +} + +#[gpui::test] +fn open_code_math_and_front_matter_keep_their_panel(cx: &mut TestAppContext) { + let (sunken, muted, mono) = cx.update(|cx| { + crate::init_for_tests(cx); + let theme = cx.theme(); + ( + theme.colors.sunken, + theme.colors.fg_muted, + theme.mono_family.clone(), + ) + }); + for kind in [ + TargetBlock::Code, + TargetBlock::Math, + TargetBlock::FrontMatter, + ] { + assert_eq!( + styled(kind, cx).background, + Some(Fill::from(sunken)), + "{kind:?}" + ); + } + let front = styled(TargetBlock::FrontMatter, cx).text; + assert_eq!((front.color, front.font_family), (Some(muted), Some(mono))); +} diff --git a/src/documents/document/visual.rs b/src/documents/document/visual.rs index 4ac8f0b..fba80cf 100644 --- a/src/documents/document/visual.rs +++ b/src/documents/document/visual.rs @@ -1,5 +1,5 @@ use gpui::{ - AnyElement, App, ClickEvent, Context, ExternalPaths, FontWeight, InteractiveElement, + AnyElement, App, ClickEvent, Context, Div, ExternalPaths, FontWeight, InteractiveElement, IntoElement, KeyBinding, MouseButton, MouseDownEvent, ParentElement, Render, StatefulInteractiveElement, Styled, StyledText, Window, actions, div, list, prelude::FluentBuilder as _, relative, @@ -13,7 +13,7 @@ use crate::{ }, forms::{self, Down, Up}, primitives::FocusRing as _, - theme::{ActiveTheme, Radius, TextSize}, + theme::{ActiveTheme, Radius, TextSize, Theme}, }; actions!(markdown_document, [Undo, Redo, Indent, Outdent, Paste]); @@ -23,6 +23,44 @@ const CONTEXT: &str = "MarkdownDocument"; /// How far a faded block recedes. const FADED: f32 = 0.35; +/// The open block's text box: its closed font and panel. +pub(super) fn open_text(kind: Option<(TargetBlock, bool)>, scale: f32, theme: &Theme) -> Div { + let colors = &theme.colors; + let body = theme.text_size(TextSize::Base) * scale; + let mono = theme.text_size(TextSize::Sm) * scale; + let text = div() + .debug_selector(|| "markdown-open-text".to_string()) + .text_size(body) + .line_height(body * 1.6) + .text_color(colors.fg); + match kind { + Some((TargetBlock::Heading(level), true)) => text + .pt_2() + .text_size(body * heading_step(level)) + .line_height(relative(1.3)) + .font_weight(HEAVY) + .when(level == 6, |text| text.text_color(colors.fg_muted)), + // Code, math and front matter keep their panel. + Some((kind @ (TargetBlock::Code | TargetBlock::Math | TargetBlock::FrontMatter), true)) => { + text.p_3() + .rounded(theme.radius(Radius::Md)) + .bg(colors.sunken) + .when(kind != TargetBlock::Math, |text| { + text.font_family(theme.mono_family.clone()).text_size(mono) + }) + .when(kind == TargetBlock::FrontMatter, |text| { + text.text_color(colors.fg_muted) + }) + } + // Written as is: mono and muted. + Some((_, false)) => text + .font_family(theme.mono_family.clone()) + .line_height(gpui::phi()) + .text_color(colors.fg_muted), + _ => text, + } +} + /// Modes with a reader's block to keep. fn editing_mode(mode: MarkdownMode) -> bool { mode == MarkdownMode::Visual @@ -163,37 +201,8 @@ impl MarkdownDocument { cx: &mut Context, ) -> AnyElement { let point = Self::point_in_open(cx); - let theme = cx.theme(); - let colors = theme.colors.clone(); - let body = theme.text_size(TextSize::Base) * self.look.scale; - let text = div() - .debug_selector(|| "markdown-open-text".to_string()) - .text_size(body) - .line_height(body * 1.6) - .text_color(colors.fg); - let text = match block.map(|block| (block.kind, block.nodes.is_some())) { - Some((TargetBlock::Heading(level), true)) => text - .pt_2() - .text_size(body * heading_step(level)) - .line_height(relative(1.3)) - .font_weight(HEAVY) - .when(level == 6, |text| text.text_color(colors.fg_muted)), - // Code and math keep their panel. - Some((kind @ (TargetBlock::Code | TargetBlock::Math), true)) => text - .p_3() - .rounded(theme.radius(Radius::Md)) - .bg(colors.sunken) - .when(kind == TargetBlock::Code, |text| { - text.font_family(theme.mono_family.clone()) - .text_size(theme.text_size(TextSize::Sm) * self.look.scale) - }), - // Written as is: mono and muted. - Some((_, false)) => text - .font_family(theme.mono_family.clone()) - .line_height(gpui::phi()) - .text_color(colors.fg_muted), - _ => text, - }; + let kind = block.map(|block| (block.kind, block.nodes.is_some())); + let text = open_text(kind, self.look.scale, cx.theme()); div() .debug_selector(|| "markdown-open".to_string()) .on_mouse_down(MouseButton::Right, point) diff --git a/src/documents/markdown.rs b/src/documents/markdown.rs index c0199fc..1440e38 100644 --- a/src/documents/markdown.rs +++ b/src/documents/markdown.rs @@ -1,10 +1,10 @@ use std::ops::Range; -use gpui::{App, FontWeight}; +use gpui::App; use markdown::{Constructs, ParseOptions, mdast}; use unicode_segmentation::UnicodeSegmentation; -use crate::{forms::Highlight, theme::ActiveTheme}; +use crate::{documents::render::HEAVY, forms::Highlight, theme::ActiveTheme}; /// GitHub's flavor, and math between dollars. pub(crate) fn options() -> ParseOptions { @@ -141,10 +141,22 @@ pub fn marks(text: &str) -> Vec<(Range, Mark)> { /// Markdown styled as typed; its syntax recedes. pub fn markdown_highlights(text: &str, cx: &App) -> Vec<(Range, Highlight)> { let colors = &cx.theme().colors; - let plain = Highlight::new(colors.fg); - marks(text) + let marks = marks(text); + // A sixth-level heading reads muted, as drawn. + let muted: Vec> = marks + .iter() + .filter(|(_, mark)| *mark == Mark::Heading(6)) + .map(|(range, _)| range.clone()) + .collect(); + marks .into_iter() .map(|(range, mark)| { + let within = + |outer: &Range| outer.start <= range.start && range.end <= outer.end; + let plain = Highlight::new(match muted.iter().any(within) { + true => colors.fg_muted, + false => colors.fg, + }); let range = match mark { Mark::Strike => { let span = &text[range.clone()]; @@ -155,7 +167,7 @@ pub fn markdown_highlights(text: &str, cx: &App) -> Vec<(Range, Highlight }; let highlight = match mark { Mark::Strong | Mark::Heading(_) => Highlight { - weight: Some(FontWeight::SEMIBOLD), + weight: Some(HEAVY), ..plain }, Mark::Emphasis => Highlight {