bezel-markdown 0.1.22

A Notion-style block document model for gpui — markdown in, markdown out
Documentation
use gpui::{
    Context, Render, TestAppContext, VisualTestContext, Window, div, point, prelude::*, px, size,
};
use markdown::{BlockLayouts, Cursor, Editing, Part, render_source};

struct Page {
    source: String,
    layouts: BlockLayouts,
}

impl Render for Page {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        div().w(px(320.0)).child(render_source(
            &self.source,
            Editing {
                layouts: Some(&self.layouts),
                ..Default::default()
            },
            cx,
        ))
    }
}

fn open(source: &str, cx: &mut TestAppContext) -> (gpui::Entity<Page>, VisualTestContext) {
    cx.update(|cx| theme::Theme::install(theme::Appearance::Dark, cx));
    let window = cx.add_window(|_, _| Page {
        source: source.into(),
        layouts: BlockLayouts::default(),
    });
    let page = window.root(cx).unwrap();
    let visual = VisualTestContext::from_window(window.into(), cx);
    visual.simulate_resize(size(px(320.0), px(600.0)));
    visual.run_until_parked();
    (page, visual)
}

#[gpui::test]
fn wrapped_and_empty_lines_keep_their_caret_and_hit_positions(cx: &mut TestAppContext) {
    let line = "word ".repeat(30);
    let source = format!("{line}\n\nlast\n");
    let (page, mut cx) = open(&source, cx);
    cx.update(|_, cx| {
        let layouts = &page.read(cx).layouts;
        let at = |offset| {
            layouts
                .position(Cursor::new(0, Part::Code, offset))
                .unwrap()
        };
        let (start, _) = at(0);
        let (end, height) = at(line.len());
        let (blank, _) = at(line.len() + 1);
        let (last, _) = at(line.len() + 2);
        let (trailing, _) = at(source.len());
        assert!(start.x > px(20.0), "text leaves room for the gutter");
        assert!(end.y > start.y, "long source lines still wrap");
        assert!(blank.y > end.y);
        assert_eq!(last.y - blank.y, height);
        assert_eq!(trailing.y - last.y, height);
        assert_eq!(last.x, start.x);
        for offset in [0, line.len() + 1, line.len() + 2, source.len()] {
            let (position, height) = at(offset);
            assert_eq!(
                layouts.hit(point(position.x, position.y + height / 2.0)),
                Some(Cursor::new(0, Part::Code, offset)),
            );
        }
    });
}

#[gpui::test]
fn gutter_grows_when_line_numbers_gain_a_digit(cx: &mut TestAppContext) {
    let (page, mut cx) = open(&["x"; 9].join("\n"), cx);
    let before = cx.update(|_, cx| {
        page.read(cx)
            .layouts
            .position(Cursor::new(0, Part::Code, 0))
            .unwrap()
            .0
            .x
    });
    cx.update(|_, cx| {
        page.update(cx, |page, cx| {
            page.source.push_str("\nx");
            cx.notify();
        })
    });
    cx.run_until_parked();
    cx.update(|_, cx| {
        let layouts = &page.read(cx).layouts;
        let first = layouts.position(Cursor::new(0, Part::Code, 0)).unwrap().0;
        let last = layouts.position(Cursor::new(0, Part::Code, 18)).unwrap().0;
        assert!(first.x > before);
        assert_eq!(last.x, first.x);
    });
}

#[gpui::test]
fn host_styles_resize_and_hide_the_gutter(cx: &mut TestAppContext) {
    let (page, mut cx) = open("first\nlast", cx);
    let x = |page: &gpui::Entity<Page>, cx: &mut gpui::App| {
        page.read(cx)
            .layouts
            .position(Cursor::new(0, Part::Code, 0))
            .unwrap()
            .0
            .x
    };
    let before = cx.update(|_, cx| x(&page, cx));
    cx.update(|_, cx| {
        markdown::set_source_style(cx, |_| markdown::SourceStyle {
            gutter_min_digits: 4,
            gutter_gap: 2.0,
            ..Default::default()
        })
    });
    cx.run_until_parked();
    assert!(cx.update(|_, cx| x(&page, cx)) > before);
    cx.update(|_, cx| {
        markdown::set_source_style(cx, |_| markdown::SourceStyle {
            line_numbers: false,
            ..Default::default()
        })
    });
    cx.run_until_parked();
    assert!(cx.update(|_, cx| x(&page, cx)) < before);
}

#[gpui::test]
fn host_colors_follow_theme_changes(cx: &mut TestAppContext) {
    cx.update(|cx| {
        markdown::set_source_style(cx, |theme| markdown::SourceStyle {
            gutter_color: Some(theme.text_muted),
            ..Default::default()
        });
        for appearance in [theme::Appearance::Dark, theme::Appearance::Light] {
            theme::Theme::install(appearance, cx);
            assert_eq!(
                markdown::SourceStyle::of(cx).gutter_color,
                Some(theme::Theme::of(cx).text_muted),
            );
        }
    });
}