//! Block commands as Typora's Paragraph menu (R4.7). use gpui::{Modifiers, TestAppContext}; use markdown::mdast::Node; use super::{open, opened, press, text}; use crate::documents::{ BlockCommand, EditRefusal, MarkdownMode, document::block::run, markdown::file_tree, }; /// `marked` after `command`: `‸` is the caret, `⟨…⟩` the selection. fn ran(marked: &str, command: BlockCommand) -> Result { let caret = marked.find('‸'); let text: String = marked.chars().filter(|ch| !"‸⟨⟩".contains(*ch)).collect(); let selection = match caret { Some(at) => at..at, None => { let start = marked.find('⟨').expect("a selection"); start..marked.find('⟩').expect("its end") - '⟨'.len_utf8() } }; let (edits, selected) = run(&text, selection, command)?; let mut out = text.clone(); for (range, with) in edits.into_iter().rev() { out.replace_range(range, &with); } Ok(match selected.is_empty() { true => format!("{}‸{}", &out[..selected.start], &out[selected.start..]), false => format!( "{}⟨{}⟩{}", &out[..selected.start], &out[selected.clone()], &out[selected.end..] ), }) } fn ok(marked: &str, command: BlockCommand) -> String { ran(marked, command).expect("ran") } /// The top-level blocks `text` reads as. fn kinds(text: &str) -> Vec<&'static str> { let text: String = text.chars().filter(|ch| !"‸⟨⟩".contains(*ch)).collect(); let root = file_tree(&text); let kids = root.children().map(Vec::as_slice).unwrap_or_default(); (kids.iter()) .map(|kid| match kid { Node::Paragraph(_) => "p", Node::Heading(_) => "h", Node::Blockquote(_) => "quote", Node::List(_) => "list", Node::Code(_) => "code", Node::Math(_) => "math", Node::ThematicBreak(_) => "rule", Node::Yaml(_) => "yaml", Node::Definition(_) => "definition", Node::FootnoteDefinition(_) => "note", _ => "other", }) .collect() } #[test] fn headings_set_and_toggle_as_typora() { use BlockCommand::*; assert_eq!(ok("a‸b", Heading(2)), "## a‸b"); assert_eq!(ok("## a‸b", Heading(2)), "a‸b"); assert_eq!(ok("## a‸b", Heading(3)), "### a‸b"); assert_eq!(ok("a‸", Promote), "###### a‸"); assert_eq!(ok("# a‸", Promote), "# a‸"); assert_eq!(ok("## a‸", Promote), "# a‸"); assert_eq!(ok("###### a‸", Demote), "a‸"); assert_eq!(ok("a‸", Demote), "a‸"); assert_eq!(ok("a\n\n‸\n\nb", Heading(2)), "a\n\n## ‸\n\nb"); } #[test] fn a_paragraph_line_becomes_the_heading_alone() { let out = ok("a\nb‸\nc", BlockCommand::Heading(1)); assert_eq!(out, "a\n# b‸\nc"); assert_eq!(kinds(&out), ["p", "h", "p"]); assert_eq!(ok("> a\nb‸", BlockCommand::Heading(1)), "> a\n> # b‸"); assert_eq!(ok("> a‸\nb", BlockCommand::Heading(1)), "> # a‸\n> b"); } #[test] fn setext_headings_relevel_and_go_atx_past_two() { use BlockCommand::*; assert_eq!(ok("a‸\n===", Heading(2)), "a‸\n---"); assert_eq!(ok("a‸\n===", Heading(3)), "### a‸"); assert_eq!(ok("a\nb‸\n---", Heading(4)), "#### a b‸"); assert_eq!(ok("a‸\n===", Paragraph), "a‸"); } #[test] fn a_heading_made_text_keeps_apart_from_its_neighbors() { use BlockCommand::Paragraph; assert_eq!(ok("a\n# h‸", Paragraph), "a\n\nh‸"); assert_eq!(ok("# h‸\nb", Paragraph), "h‸\n\nb"); let out = ok("# h‸\n---", Paragraph); assert_eq!(kinds(&out), ["p", "rule"]); assert_eq!(ok("- a\n- # h‸", Paragraph), "- a\n- h‸"); let out = ok("> # h‸\nb", Paragraph); assert_eq!(kinds(&out), ["quote", "p"]); } #[test] fn heading_commands_refuse_or_add_a_paragraph_as_typora() { use BlockCommand::*; assert_eq!(ran("```\na‸\n```", Heading(1)), Err(EditRefusal::Code)); assert_eq!(ran("$$\na‸\n$$", Promote), Err(EditRefusal::Math)); assert_eq!(ok("$$\na‸\n$$", Heading(1)), "$$\na\n$$\n\n‸"); assert_eq!( ran("---\nt: a‸\n---\n", Heading(1)), Err(EditRefusal::FrontMatter) ); assert_eq!(ran("- [ ] a‸", Heading(1)), Err(EditRefusal::Outside)); } #[test] fn quotes_wrap_and_unwrap() { use BlockCommand::Quote; assert_eq!(ok("a‸", Quote), "> a‸"); assert_eq!(ok("> a‸", Quote), "a‸"); assert_eq!(ok("> a\n> b‸", Quote), "a\nb‸"); assert_eq!(ok("> a\nb‸", Quote), "a\nb‸"); assert_eq!(ok("⟨a\n\nb⟩", Quote), "> ⟨a\n>\n> b⟩"); assert_eq!(ok("x\n> a‸", Quote), "x\n\na‸"); assert_eq!(ok("> [!NOTE]\n> a‸", Quote), "a‸"); assert_eq!(ok("- a‸", Quote), "- > a‸"); assert_eq!(ok("> a\n>\n> ⟨b⟩", Quote), "> a\n>\n> > ⟨b⟩"); } #[test] fn lists_wrap_each_line() { use BlockCommand::*; assert_eq!(ok("a‸", BulletList), "- a‸"); assert_eq!(ok("a‸", OrderedList), "1. a‸"); assert_eq!(ok("a‸", TaskList), "- [ ] a‸"); assert_eq!(ok("⟨a\nb⟩", OrderedList), "1. ⟨a\n2. b⟩"); assert_eq!(ok("> a‸", BulletList), "> - a‸"); assert_eq!(ok("- a\n\n b‸", BulletList), "- a\n\n - b‸"); } #[test] fn a_list_of_the_kind_unwraps_and_another_retypes() { use BlockCommand::*; let out = ok("- a\n- b‸", BulletList); assert_eq!(out, "a\n\nb‸"); assert_eq!(kinds(&out), ["p", "p"]); assert_eq!(ok("- a\n- b‸", OrderedList), "1. a\n2. b‸"); assert_eq!(ok("- a\n b‸", OrderedList), "1. a\n b‸"); assert_eq!(ok("- [ ] a‸", BulletList), "- a‸"); assert_eq!(ok("* a‸", TaskList), "* [ ] a‸"); assert_eq!(ok("3. a\n4. b‸", BulletList), "- a\n- b‸"); } #[test] fn inserts_split_the_paragraph_at_the_caret() { use BlockCommand::*; assert_eq!(ok("ab‸cd", Rule), "ab\n\n---\n\n‸cd"); assert_eq!(ok("ab‸", CodeBlock), "ab\n\n```\n‸\n```"); assert_eq!(ok("a ⟨b⟩", CodeBlock), "a\n\n```\n‸b\n```"); assert_eq!(ok("‸ab", Toc), "[toc]‸\n\nab"); assert_eq!(ok("# ab‸cd", Rule), "# ab\n\n---\n\n# ‸cd"); assert_eq!(ok("- ab‸", Rule), "- ab\n\n ---\n\n ‸"); assert_eq!(ok("```\na‸\n```", Rule), "```\na\n```\n\n---\n\n‸"); assert_eq!(ok("a\n\n‸\n\nb", Rule), "a\n\n---\n\n‸\n\nb"); assert_eq!(ran("⟨a\n\nb⟩", Rule), Err(EditRefusal::Across)); } #[test] fn notes_and_definitions_take_free_names() { use BlockCommand::*; let out = ok("x[^1]\n\n[^1]: y\n\nab‸", Footnote); assert_eq!(out, "x[^1]\n\n[^1]: y\n\nab\n\n[^2]: ‸"); assert_eq!(ok("‸", LinkDefinition), "[‸]: "); assert_eq!(ok("⟨see⟩", LinkDefinition), "[see]: ‸"); assert_eq!(ok("⟨a/b.md⟩", LinkDefinition), "[‸]: a/b.md"); let out = ok("ab‸", MathBlock); assert_eq!(kinds(&out), ["p", "math"]); } #[test] fn empty_paragraphs_come_before_and_after() { use BlockCommand::*; assert_eq!(ok("a‸", ParagraphAfter), "a\n\n‸"); assert_eq!(ok("# a‸\nb", ParagraphAfter), "# a\n\n‸\n\nb"); assert_eq!(ok("a\n\nb‸", ParagraphBefore), "a\n\n‸\n\nb"); assert_eq!(ok("a\n# h‸", ParagraphBefore), "a\n\n‸\n\n# h"); assert_eq!(ok("- a‸", ParagraphBefore), "- ‸\n\n a"); } #[test] fn front_matter_goes_on_top_once() { let out = ok("a‸", BlockCommand::FrontMatter); assert_eq!(out, "---\n‸\n---\n\na"); assert_eq!(kinds(&out), ["yaml", "p"]); assert_eq!( ran("---\nt: 1\n---\n\na‸", BlockCommand::FrontMatter), Err(EditRefusal::Present) ); } #[test] fn delete_takes_the_block_or_its_lone_holder() { use BlockCommand::Delete; assert_eq!(ok("a\n\nb‸\n\nc", Delete), "a\n\n‸c"); assert_eq!(ok("x\n\n> b‸\n\nc", Delete), "x\n\n‸c"); assert_eq!(ok("- a\n- b‸\n- c", Delete), "- a\n‸- c"); assert_eq!(ok("- a‸\n\n b", Delete), "- ‸\n\n b"); } #[test] fn line_endings_follow_the_text() { assert_eq!( ok("ab‸\r\n", BlockCommand::ParagraphAfter), "ab\r\n\r\n‸\r\n" ); } #[gpui::test] fn a_source_command_is_one_undo_step(cx: &mut TestAppContext) { let (document, cx) = open("alpha\n", MarkdownMode::Source, cx); document.update(cx, |document, cx| { document .editor .update(cx, |editor, cx| editor.select(std::iter::once(2..2), cx)); document.block(BlockCommand::Heading(2), cx).expect("taken"); }); cx.run_until_parked(); assert_eq!(text(&document, cx), "## alpha\n"); document.update(cx, |document, cx| { document.editor.update(cx, |editor, cx| editor.undo(cx)); }); assert_eq!(text(&document, cx), "alpha\n"); } #[gpui::test] fn an_open_block_takes_a_command_and_stays_open(cx: &mut TestAppContext) { let (document, cx) = open("alpha\n", MarkdownMode::Visual, cx); press(0, Modifiers::none(), cx); document.update(cx, |document, cx| { document.block(BlockCommand::Quote, cx).expect("taken"); }); cx.run_until_parked(); assert_eq!(opened(&document, cx), "> alpha", "still open to type in"); } #[gpui::test] fn a_command_past_the_open_block_writes_it_back_first(cx: &mut TestAppContext) { let (document, cx) = open("first\n\nalpha\n", MarkdownMode::Visual, cx); press(1, Modifiers::none(), cx); document.update(cx, |document, cx| { let open = document.open.as_ref().expect("opened"); open.input.update(cx, |input, cx| input.insert("x", cx)); document .block(BlockCommand::FrontMatter, cx) .expect("taken"); }); cx.run_until_parked(); let written = text(&document, cx); assert!(written.starts_with("---\n\n---\n"), "{written:?}"); assert!( written.contains("alpha") && written.contains('x'), "{written:?}" ); document.read_with(cx, |document, _| assert!(document.open.is_none())); }