use std::rc::Rc; use gpui::{AppContext as _, Context, MouseButton, Window}; use markdown::mdast; use super::{DocumentEvent, MarkdownDocument, edit::Landing, edit::moved, parse::Block}; use crate::documents::{ markdown::{file_tree, span, walk}, render::{OnPoint, Place, Pointed}, tree::{Node, alert, is_toc, lines_at}, }; /// A top-level block's kind. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TargetBlock { Paragraph, Heading(u8), Quote, Alert, List, Code, Math, Table, Rule, Html, Footnote, Definition, FrontMatter, Toc, } impl TargetBlock { /// `node`'s kind, read from `source`. pub(super) fn of(node: &mdast::Node, source: &str) -> Self { match node { mdast::Node::Paragraph(_) if is_toc(node, source) => Self::Toc, mdast::Node::Paragraph(_) => Self::Paragraph, mdast::Node::Heading(heading) => Self::Heading(heading.depth), mdast::Node::Blockquote(quote) if alert(quote).is_some() => Self::Alert, mdast::Node::Blockquote(_) => Self::Quote, mdast::Node::List(_) => Self::List, mdast::Node::Code(_) => Self::Code, mdast::Node::Math(_) => Self::Math, mdast::Node::Table(_) => Self::Table, mdast::Node::ThematicBreak(_) => Self::Rule, mdast::Node::Html(_) => Self::Html, mdast::Node::FootnoteDefinition(_) => Self::Footnote, mdast::Node::Definition(_) => Self::Definition, mdast::Node::Yaml(_) | mdast::Node::Toml(_) => Self::FrontMatter, other => unreachable!("{other:?} is no block outside MDX"), } } } /// What a secondary press landed on. #[derive(Clone, Debug, PartialEq, Eq)] pub struct DocumentTarget { /// The caret's source byte; an open block's as unwritten. pub at: usize, /// None past the last block. pub block: Option, pub link: Option, pub image: Option, pub in_table: bool, /// The code block's text, pressed in one. pub code: Option, } impl MarkdownDocument { /// Presses on block `ix`'s drawing, read as it was drawn. pub(super) fn on_point(&self, ix: usize, cx: &mut Context) -> OnPoint { let (document, parsed) = (cx.entity().downgrade(), self.parsed.clone()); let start = parsed.blocks[ix].range.start; Rc::new(move |pointed, button, window, cx| { // The drawing counts from its block's start. let at = match &pointed.place { Place::Text(inline, drawn) => inline.source_at(&parsed.text[start..], *drawn), // Unreadable lines land at their start. Place::Lines { text, from, at } => { Some(lines_at(&parsed.text[start..], *from, text, *at).unwrap_or(*from)) } Place::At(at) => Some(*at), Place::Block => None, } .map(|at| start + at); let ix = Some(ix); document .update(cx, |document, cx| match button { MouseButton::Left => { document.open(ix, at.map_or(Landing::End, Landing::At), window, cx) } MouseButton::Right => document.point(ix, at, pointed, window, cx), _ => {} }) .expect("a block's drawing lives in its document"); }) } /// A block's kind, text and what it draws. fn drawn(&self, block: &Block) -> (TargetBlock, String, Option>) { let text = self.parsed.text[block.range.clone()].to_string(); (block.kind, text, block.nodes.clone()) } /// Caret to `at`, else block `ix`; the owner hears. pub(super) fn point( &mut self, ix: Option, at: Option, pointed: Pointed, window: &mut Window, cx: &mut Context, ) { let block = match ix.map(|ix| self.parsed.blocks.get(ix)) { Some(None) => return log::warn!("markdown document: a press on a block read away"), block => block.flatten(), }; let kind = block.map(|block| block.kind); let pressed = block.map(|block| self.drawn(block)); let code = (pointed.code.as_deref().map(str::to_string)).or_else(|| { match block.and_then(|block| block.nodes.as_deref()) { Some([Node::Code { text, .. }]) => Some(text.clone()), _ => None, } }); let at = at.or(block.map(|block| block.range.start)); let written = self.commit(cx); if written.is_some() { self.reparse(true, cx); } if !self.read_now(cx) { return log::warn!("markdown document: pressed while the text is read again"); } let at = match at { None => self.editor.read(cx).text().len(), Some(at) => match moved(at, &written) { Some(at) => at, None => return log::warn!("markdown document: the pressed text was written over"), }, }; // A write-back may change the pressed block. let now = (self.parsed.blocks.iter()) .find(|block| block.range.start <= at && at <= block.range.end) .map(|block| self.drawn(block)); if written.is_some() && pressed.is_some() && now != pressed { return log::warn!("markdown document: the write-back changed the pressed block"); } self.editor .update(cx, |editor, cx| editor.select(std::iter::once(at..at), cx)); // Its focus opens no empty block. self.pointing = !self.focus.is_focused(window); window.focus(&self.focus, cx); log::info!("markdown document: a secondary press puts the caret at byte {at}"); cx.emit(DocumentEvent::Pointed(DocumentTarget { at, block: kind, link: pointed.link, image: pointed.image, in_table: pointed.table || kind == Some(TargetBlock::Table), code, })); cx.notify(); } /// A right press in the open block; it stays open. pub(super) fn point_open(&mut self, cx: &mut Context) { if self.open.is_none() { return log::warn!("markdown document: a press on a block closed since"); } let taken = self.snapshot(cx); let (full, _, selection) = taken.clone(); cx.spawn(async move |document, cx| { let target = cx .background_spawn(async move { target_at(&full, selection.start) }) .await; document .update(cx, |document, cx| { if document.open.is_none() || document.snapshot(cx) != taken { return log::info!("markdown document: the pressed block changed first"); } log::info!("markdown document: a secondary press in the open block"); cx.emit(DocumentEvent::Pointed(target)); }) .ok(); }) .detach(); } } /// What byte `at` of `text` holds. fn target_at(text: &str, at: usize) -> DocumentTarget { let root = file_tree(text); let mut nodes = Vec::new(); walk(&root, &mut nodes); let block = (root .children() .map(Vec::as_slice) .unwrap_or_default() .iter()) .find(|node| span(node).start <= at && at <= span(node).end) .map(|node| TargetBlock::of(node, text)); let around: Vec<&mdast::Node> = (nodes.iter().copied()) .filter(|node| span(node).contains(&at)) .collect(); let defined = |identifier: &str| { nodes.iter().find_map(|node| match node { mdast::Node::Definition(definition) if definition.identifier == identifier => { Some(definition.url.clone()) } _ => None, }) }; let link = around.iter().rev().find_map(|node| match node { mdast::Node::Link(link) => Some(link.url.clone()), mdast::Node::LinkReference(link) => defined(&link.identifier), _ => None, }); let image = around.iter().rev().find_map(|node| match node { mdast::Node::Image(picture) => Some(picture.url.clone()), mdast::Node::ImageReference(picture) => defined(&picture.identifier), _ => None, }); let code = around.iter().find_map(|node| match node { mdast::Node::Code(code) => Some(code.value.clone()), _ => None, }); DocumentTarget { at, block, link, image, in_table: around .iter() .any(|node| matches!(node, mdast::Node::Table(_))), code, } }