Files
Ely-GPUI-Components/src/documents/document/point.rs
T
ZacharyZhang-NYandClaude Opus 5.5 013f14216f
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 21s
CI / crate (push) Failing after 12m8s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
documents: right presses keep a selection; Delete Image
A right press in a text field or the code editor keeps a selection it
lands in, else moves the caret. In visual mode a right press in the open
block keeps it open and emits Pointed, read apart from the whole text.
MarkdownDocument::delete_image removes the picture at the caret.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 18:15:38 -04:00

238 lines
8.7 KiB
Rust

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<TargetBlock>,
pub link: Option<String>,
pub image: Option<String>,
pub in_table: bool,
/// The code block's text, pressed in one.
pub code: Option<String>,
}
impl MarkdownDocument {
/// Presses on block `ix`'s drawing, read as it was drawn.
pub(super) fn on_point(&self, ix: usize, cx: &mut Context<Self>) -> 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<Vec<Node>>) {
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<usize>,
at: Option<usize>,
pointed: Pointed,
window: &mut Window,
cx: &mut Context<Self>,
) {
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<Self>) {
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,
}
}