documents: GitHub alerts, a [toc] list of headings, front matter in a box, HTML blocks as text, a visible rule and bullets by depth

At the root a quote marked [!NOTE] and the rest, as GitHub's API reads it,
is an alert; a [toc] paragraph lists the headings and a press reveals one;
an HTML block shows its text with the inline tags, white space as a browser
lays it, scripts and styles dropped. The rule is 2px; bullets turn by depth.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-13 23:08:51 -04:00
1 parent 9ff403df31
commit 2eec25140d
15 files changed
+810 -118

No files matched your search

+14 -2
View File
@@ -19,13 +19,18 @@ use gpui::{
ListState, Pixels, Subscription, Task, Window,
};
pub use crate::documents::tree::DocumentHeading;
pub use attach::Attachment;
pub use look::DocumentLook;
pub use parse::DocumentHeading;
use parse::{Parsed, alike, parsed};
pub use tables::{TableEdit, TableRefusal};
use super::{MarkdownMode, markdown::markdown_highlights, modes::joint, render::OnLink};
use super::{
MarkdownMode,
markdown::markdown_highlights,
modes::joint,
render::{OnHeading, OnLink},
};
use crate::{
editor::{CodeEditor, EditorEvent, LineNumbers},
forms::TextInput,
@@ -94,6 +99,7 @@ pub struct MarkdownDocument {
centered: Option<(usize, u64, u64, [u64; 2])>,
focus: FocusHandle,
on_link: Option<OnLink>,
on_heading: Option<OnHeading>,
_subscriptions: Vec<Subscription>,
}
@@ -132,6 +138,11 @@ impl MarkdownDocument {
cx.on_focus(&focus, window, Self::focused),
];
let overdraw = window.rem_size() * 40.0;
let this = cx.entity().downgrade();
let on_heading: OnHeading = Rc::new(move |at, window, cx| {
this.update(cx, |document, cx| document.reveal_at(at, window, cx))
.expect("a heading list lives in its document");
});
let mut document = Self {
editor,
mode,
@@ -151,6 +162,7 @@ impl MarkdownDocument {
room: Pixels::ZERO,
focus,
on_link: None,
on_heading: Some(on_heading),
_subscriptions: subscriptions,
};
document.reparse(true, cx);
+12 -37
View File
@@ -3,27 +3,18 @@ use std::ops::Range;
use markdown::mdast;
use crate::documents::{
markdown::{file_tree, span, walk},
tree::{Node, Reader},
markdown::{file_tree, span},
tree::{DocumentHeading, Node, Reader},
};
/// A top-level block: its source, and what it draws.
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Block {
pub range: Range<usize>,
/// None shows the source as written: definitions, HTML, front matter.
/// None shows a link definition as written.
pub nodes: Option<Vec<Node>>,
}
/// A heading anywhere in the file.
#[derive(Clone, Debug, PartialEq)]
pub struct DocumentHeading {
pub depth: u8,
pub title: String,
/// Where its source starts, in bytes.
pub at: usize,
}
/// A file read as blocks and headings.
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct Parsed {
@@ -44,16 +35,10 @@ pub(crate) fn parsed(text: &str) -> Parsed {
.map(|node| {
let range = span(node);
let kept = text[range.clone()].trim_end_matches(['\n', '\r']).len();
let raw = matches!(
node,
mdast::Node::Definition(_)
| mdast::Node::Html(_)
| mdast::Node::Yaml(_)
| mdast::Node::Toml(_)
);
let raw = matches!(node, mdast::Node::Definition(_));
let nodes = (!raw).then(|| {
let mut out = Vec::new();
reader.block(node, &mut out);
reader.top(node, &mut out);
out
});
Block {
@@ -62,23 +47,10 @@ pub(crate) fn parsed(text: &str) -> Parsed {
}
})
.collect();
let mut nodes = Vec::new();
walk(&root, &mut nodes);
let headings = nodes
.into_iter()
.filter_map(|node| match node {
mdast::Node::Heading(heading) => Some(DocumentHeading {
depth: heading.depth,
title: reader.inline(&heading.children).text,
at: span(node).start,
}),
_ => None,
})
.collect();
Parsed {
text: text.to_string(),
blocks,
headings,
headings: reader.headings,
}
}
@@ -111,7 +83,7 @@ mod tests {
use crate::documents::tree::Style;
#[test]
fn every_block_is_shown_raw_ones_as_written() {
fn every_block_is_shown_definitions_as_written() {
let text = "---\ntitle: a\n---\n\n# Lift\n\n[site]: https://ely.dev\n\n<div>x</div>\n\n> ## Quoted\n";
let found = parsed(text);
let shown: Vec<(&str, bool)> = found
@@ -122,13 +94,16 @@ mod tests {
assert_eq!(
shown,
[
("---\ntitle: a\n---", false),
("---\ntitle: a\n---", true),
("# Lift", true),
("[site]: https://ely.dev", false),
("<div>x</div>", false),
("<div>x</div>", true),
("> ## Quoted", true),
]
);
let drawn = |ix: usize| found.blocks[ix].nodes.clone().expect("drawn");
assert_eq!(drawn(0), [Node::FrontMatter("title: a".into())]);
assert!(matches!(&drawn(3)[..], [Node::Paragraph(line)] if line.text == "x"));
let headings: Vec<(u8, &str, usize)> = found
.headings
.iter()
+10
View File
@@ -189,6 +189,16 @@ impl MarkdownDocument {
.saturating_sub(1)
}
/// Reveals the line holding byte `at` of the text.
pub(super) fn reveal_at(&mut self, at: usize, window: &mut Window, cx: &mut Context<Self>) {
let editor = self.editor.read(cx);
if at > editor.text().len() {
return log::error!("markdown document: a heading past the text");
}
let line = editor.buffer.line_of(at);
self.reveal(line, window, cx);
}
/// Line `line` at top and focused, caret at start.
pub fn reveal(&mut self, line: usize, window: &mut Window, cx: &mut Context<Self>) {
self.set_caret(line, 0, cx);
+1
View File
@@ -370,6 +370,7 @@ mod history;
mod links;
mod look;
mod place;
mod toc;
mod tools;
#[gpui::test]
+36
View File
@@ -0,0 +1,36 @@
use gpui::{Modifiers, MouseButton, TestAppContext, VisualTestContext, point, px};
use super::{MarkdownMode, open};
const LISTED: &str = "[toc]\n\n# A\n\ntext\n\n## B\n";
/// Presses entry `ix` of the drawn heading list.
fn press_entry(ix: usize, modifiers: Modifiers, cx: &mut VisualTestContext) {
let bounds = cx
.debug_bounds(Box::leak(format!("markdown-toc-{ix}").into_boxed_str()))
.expect("the entry is drawn");
let at = point(bounds.left() + px(2.0), bounds.top() + px(2.0));
cx.simulate_mouse_down(at, MouseButton::Left, modifiers);
cx.simulate_mouse_up(at, MouseButton::Left, modifiers);
cx.run_until_parked();
}
#[gpui::test]
fn a_press_in_preview_reveals_the_heading(cx: &mut TestAppContext) {
let (document, cx) = open(LISTED, MarkdownMode::Preview, cx);
press_entry(1, Modifiers::none(), cx);
let caret = document.read_with(cx, |document, cx| document.caret(cx));
assert_eq!(caret, (6, 0));
}
#[gpui::test]
fn visual_reveals_on_a_secondary_press_and_edits_on_a_plain_one(cx: &mut TestAppContext) {
let (document, cx) = open(LISTED, MarkdownMode::Visual, cx);
press_entry(1, Modifiers::secondary_key(), cx);
let (caret, open) = document.read_with(cx, |document, cx| {
(document.caret(cx), document.open.is_some())
});
assert_eq!((caret, open), ((6, 0), false));
press_entry(0, Modifiers::none(), cx);
assert!(document.read_with(cx, |document, _| document.open.is_some()));
}
+1
View File
@@ -101,6 +101,7 @@ impl MarkdownDocument {
nodes,
&self.base,
&self.on_link,
&self.on_heading,
self.look.scale,
cx,
)),