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:
1 parent
9ff403df31
commit
2eec25140d
15 files changed
+810
-118
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -370,6 +370,7 @@ mod history;
|
||||
mod links;
|
||||
mod look;
|
||||
mod place;
|
||||
mod toc;
|
||||
mod tools;
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
@@ -101,6 +101,7 @@ impl MarkdownDocument {
|
||||
nodes,
|
||||
&self.base,
|
||||
&self.on_link,
|
||||
&self.on_heading,
|
||||
self.look.scale,
|
||||
cx,
|
||||
)),
|
||||
|
||||
Reference in new issue
Block a user