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) <noreply@anthropic.com>
This commit is contained in:
1 parent
bff7ebcd64
commit
1ff993dcb1
5 files changed
+182
-44
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
@@ -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<Self>,
|
||||
) -> 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)
|
||||
|
||||
@@ -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<usize>, Mark)> {
|
||||
/// Markdown styled as typed; its syntax recedes.
|
||||
pub fn markdown_highlights(text: &str, cx: &App) -> Vec<(Range<usize>, 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<Range<usize>> = marks
|
||||
.iter()
|
||||
.filter(|(_, mark)| *mark == Mark::Heading(6))
|
||||
.map(|(range, _)| range.clone())
|
||||
.collect();
|
||||
marks
|
||||
.into_iter()
|
||||
.map(|(range, mark)| {
|
||||
let within =
|
||||
|outer: &Range<usize>| 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<usize>, Highlight
|
||||
};
|
||||
let highlight = match mark {
|
||||
Mark::Strong | Mark::Heading(_) => Highlight {
|
||||
weight: Some(FontWeight::SEMIBOLD),
|
||||
weight: Some(HEAVY),
|
||||
..plain
|
||||
},
|
||||
Mark::Emphasis => Highlight {
|
||||
|
||||
Reference in new issue
Block a user