documents: a right press in an open block targets the pressed byte
The open block's target was read at the selection's start and from the syntax tree only, so a press on the second of two selected links named the first, and an inline anchor read as its outer link. point_open now reads the byte under the press and resolves links as drawing does; delete_image takes that byte. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6df9ab0aa6
commit
79e94f7e24
6 files changed
+165
-36
No files matched your search
@@ -13,7 +13,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
||||
- Web views: `wry` 0.57 (Apache-2.0 OR MIT, default features off, macOS only) lays a child WKWebView over the window.
|
||||
- Terminal: `alacritty_terminal` 0.26 (Apache-2.0, default features off, native only) for the grid, its parser and the pseudo-terminal; `futures` carries its events. `vte` 0.15 (Apache-2.0 OR MIT), the parser it re-exports, reads `AnsiText`'s codes alone, so colored output builds for wasm32.
|
||||
- Diffs: `similar` 3.2 (Apache-2.0) for line and word diffs and three-way merges; its `unicode` feature splits words at punctuation.
|
||||
- Markdown: `markdown` 1.0.0 (markdown-rs, MIT) reads GitHub's flavor and math into a syntax tree with byte positions for `MarkdownRenderer`, `MarkdownEditor`, the highlights, block ranges and outline. `MarkdownRenderer` draws display math through `typography::Latex`, a ```` ```mermaid ```` block through `documents::Diagram`, and pictures from the owner's folder (`base`) or `data:` addresses (`base64` 0.22, MIT OR Apache-2.0); web pictures stay unloaded. Inline math shows as TeX: gpui text carries no inline pictures. Inline it reads as Typora's defaults: `:shortcode:` turns to its emoji as GitHub reads it (the `emojis` crate's names, GitHub's today, so `:cricket:` is the insect; read after escapes and references, so `\:smile:` turns too; inside an autolink or `<code>` it stays text; within one DOM text node, which filtered tags do not split, the first `:name:`-shaped run decides for all: blank text or a space before it turns every shortcode, else none, so `x:smile: :smile:` stays), inline HTML's `u`, `b`/`strong`, `i`/`em`, `s`/`del`/`strike`, `code`/`kbd`, `mark`, `br` and `a href` draw as what they mean GFM's filtered tags (`script`, `style`, `title`, `textarea`, `iframe`, `xmp`, `noembed`, `noframes`, `plaintext`) show as text, and other tags, hyphenated names too, vanish, leaving their text; a tag's name runs to white space or `/`, as HTML5 reads it; attribute values split on ASCII whitespace alone and decode as HTML decodes them; each Markdown container is the element GitHub writes (`em`, `strong`, `del`, `a`), on one stack with the tags and repaired as HTML5 repairs: an end tag closes the innermost element of its name and all above it, of which only formatting elements (`a`, `b`, `code`, `em`, `i`, `s`, `strike`, `strong`, `u`) reopen, a new `a` ends the open one and an `a` without `href` links nothing; other tags are not tracked, and `</br>` breaks as `<br>` does; and a footnote reference is its number in superscript, numbered by first reference, as its definition is; one `~` still strikes. At the root, blocks read as GitHub shows them: a quote whose first line is plain text reading `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` in any case, after escapes and references, spaces and tabs around it, with more after it, is an alert under its title (Important in the palette's violet, `syntax.keyword`, read at 4.5:1); a paragraph that is `[toc]` alone, as Typora reads it, lists every heading by depth, and a press on one reveals it as a link press does (`MarkdownDocument`'s `reveal`), the heading found where `CodeEditor` anchors carried it through each edit since the read, gone if an edit or an undo took it, and a secondary press acts on press, since a block writing back moves the list; front matter shows in a box without its fences; an HTML block shows its text with the inline tags above, white space collapsed and owed as a browser lays it, block elements breaking lines and closing what opened inside them, HTML5's start tags ending an open `p`, `fieldset` and `menu` too, and an `li` start an `li`, a `dd` or `dt` start either, as HTML5 ends them, comments dropped, the filtered tags shown as text, and an unclosed tag dropping the rest; a rule is a 2px `border_strong` line; bullets go •, ◦, ▪ by list depth, ordered lists counted. Exports read a tree as the drawing does through `MarkdownReader` (`block` gives its `MarkdownBlock`s, `inline` its text, `note` and `label` its footnotes, `headings` the `[toc]` list), one fresh stack per paragraph, heading or cell. `MarkdownDocument` holds a whole file in one `CodeEditor` (text and undo, colored by `CodeEditor::highlighter` with `markdown_highlights`): visual draws the file's top-level blocks in a gpui `list`, a pressed block opening as its source in a `TextInput` whose typing is the document's text at once and whose write-back is one undo step; source is the editor; preview draws without presses. A written-back block, a mode switch or a new text is read at once up to 128 KiB, longer ones on the background executor; typing in source or split reads 300 ms after the last key, off the main thread (markdown-rs grows with the square of its nodes, not its bytes: in release, 128 KiB of one-letter paragraphs takes 1.7 s, 16 KiB 42 ms, while 128 KiB of prose takes 20 ms; a block-heavy text under the cutoff can still hold the UI thread past 100 ms, a known limit). Texts past `LARGEST_READ` (2 MiB) are never read: no blocks, no outline. A new read keeps the list's blocks alike at both ends. One caret serves every mode: the editor's selection, which a written-back block moves; `set_caret` and `set_top_line` set caret and view apart; a top line asked (numbered, so a late frame callback finishes only its own ask; new text places it again, clamped to the text as it then is) waits for the read, lands on its block, then a frame later at its share of the block's height; `reveal` is such an ask with the caret at its line; a switch between source and the listLine truncated
|
||||
- Markdown: `markdown` 1.0.0 (markdown-rs, MIT) reads GitHub's flavor and math into a syntax tree with byte positions for `MarkdownRenderer`, `MarkdownEditor`, the highlights, block ranges and outline. `MarkdownRenderer` draws display math through `typography::Latex`, a ```` ```mermaid ```` block through `documents::Diagram`, and pictures from the owner's folder (`base`) or `data:` addresses (`base64` 0.22, MIT OR Apache-2.0); web pictures stay unloaded. Inline math shows as TeX: gpui text carries no inline pictures. Inline it reads as Typora's defaults: `:shortcode:` turns to its emoji as GitHub reads it (the `emojis` crate's names, GitHub's today, so `:cricket:` is the insect; read after escapes and references, so `\:smile:` turns too; inside an autolink or `<code>` it stays text; within one DOM text node, which filtered tags do not split, the first `:name:`-shaped run decides for all: blank text or a space before it turns every shortcode, else none, so `x:smile: :smile:` stays), inline HTML's `u`, `b`/`strong`, `i`/`em`, `s`/`del`/`strike`, `code`/`kbd`, `mark`, `br` and `a href` draw as what they mean GFM's filtered tags (`script`, `style`, `title`, `textarea`, `iframe`, `xmp`, `noembed`, `noframes`, `plaintext`) show as text, and other tags, hyphenated names too, vanish, leaving their text; a tag's name runs to white space or `/`, as HTML5 reads it; attribute values split on ASCII whitespace alone and decode as HTML decodes them; each Markdown container is the element GitHub writes (`em`, `strong`, `del`, `a`), on one stack with the tags and repaired as HTML5 repairs: an end tag closes the innermost element of its name and all above it, of which only formatting elements (`a`, `b`, `code`, `em`, `i`, `s`, `strike`, `strong`, `u`) reopen, a new `a` ends the open one and an `a` without `href` links nothing; other tags are not tracked, and `</br>` breaks as `<br>` does; and a footnote reference is its number in superscript, numbered by first reference, as its definition is; one `~` still strikes. At the root, blocks read as GitHub shows them: a quote whose first line is plain text reading `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` in any case, after escapes and references, spaces and tabs around it, with more after it, is an alert under its title (Important in the palette's violet, `syntax.keyword`, read at 4.5:1); a paragraph that is `[toc]` alone, as Typora reads it, lists every heading by depth, and a press on one reveals it as a link press does (`MarkdownDocument`'s `reveal`), the heading found where `CodeEditor` anchors carried it through each edit since the read, gone if an edit or an undo took it, and a secondary press acts on press, since a block writing back moves the list; front matter shows in a box without its fences; an HTML block shows its text with the inline tags above, white space collapsed and owed as a browser lays it, block elements breaking lines and closing what opened inside them, HTML5's start tags ending an open `p`, `fieldset` and `menu` too, and an `li` start an `li`, a `dd` or `dt` start either, as HTML5 ends them, comments dropped, the filtered tags shown as text, and an unclosed tag dropping the rest; a rule is a 2px `border_strong` line; bullets go •, ◦, ▪ by list depth, ordered lists counted. Exports read a tree as the drawing does through `MarkdownReader` (`block` gives its `MarkdownBlock`s, `inline` its text, `note` and `label` its footnotes, `headings` the `[toc]` list), one fresh stack per paragraph, heading or cell. `MarkdownDocument` holds a whole file in one `CodeEditor` (text and undo, colored by `CodeEditor::highlighter` with `markdown_highlights`): visual draws the file's top-level blocks in a gpui `list`, a pressed block opening as its source in a `TextInput` whose typing is the document's text at once and whose write-back is one undo step; source is the editor; preview draws without presses. A written-back block, a mode switch or a new text is read at once up to 128 KiB, longer ones on the background executor; typing in source or split reads 300 ms after the last key, off the main thread (markdown-rs grows with the square of its nodes, not its bytes: in release, 128 KiB of one-letter paragraphs takes 1.7 s, 16 KiB 42 ms, while 128 KiB of prose takes 20 ms; a block-heavy text under the cutoff can still hold the UI thread past 100 ms, a known limit). Texts past `LARGEST_READ` (2 MiB) are never read: no blocks, no outline. A new read keeps the list's blocks alike at both ends. One caret serves every mode: the editor's selection, which a written-back block moves; `set_caret` and `set_top_line` set caret and view apart; a top line asked (numbered, so a late frame callback finishes only its own ask; new text places it again, clamped to the text as it then is) waits for the read, lands on its block, then a frame later at its share of the block's height; `reveal` is such an ask with the caret at its line; a switch between source and the listLine truncated
|
||||
- Pictures: `image` 0.25 (MIT OR Apache-2.0), the crate gpui decodes with, default features off; it turns decoded frames and rims map tiles.
|
||||
- Math and diagrams: RaTeX 0.1.14 (`ratex-parser`, `ratex-layout`, `ratex-svg` with `embed-fonts`, `ratex-types`; MIT, KaTeX's fonts OFL) typesets `Latex` to SVG with outlined glyphs; `mermaid-rs-renderer` 0.3.1 (MIT, default features off) draws `Diagram`. Both go through gpui's `SvgRenderer` at the window's scale, loaded as gpui assets off the main thread; `usvg` 0.46, gpui's own, reads a diagram's size. A RaTeX or Mermaid panic on parsed input becomes the element's error. Each element holds its picture through `primitives::held`: a new source, or the element leaving the window, removes the old asset from gpui's cache and drops its image from the atlas after the frame.
|
||||
- Maps: Natural Earth's 110m countries (public domain). `assets/maps/countries.geojson`, trimmed by `scripts/countries.py` to an ISO code, a name and positions to 0.01°, backs `maps::WorldMap`; the gallery's tiles are cut from the same file, z0 to z3, by `scripts/tiles.py` with Pillow.
|
||||
|
||||
@@ -29,9 +29,10 @@ impl MarkdownDocument {
|
||||
)
|
||||
}
|
||||
|
||||
/// Removes the picture at the caret, read apart.
|
||||
pub fn delete_image(&mut self, cx: &mut Context<Self>) -> Result<(), EditRefusal> {
|
||||
self.apart("delete image".to_string(), drop_image, cx)
|
||||
/// Removes the picture at source byte `at`, read apart.
|
||||
pub fn delete_image(&mut self, at: usize, cx: &mut Context<Self>) -> Result<(), EditRefusal> {
|
||||
let drop = move |full: &str, _| drop_image(full, at);
|
||||
self.apart("delete image".to_string(), drop, cx)
|
||||
}
|
||||
|
||||
/// `command` at the caret, read apart; refusals by event.
|
||||
|
||||
@@ -62,11 +62,10 @@ pub enum EditRefusal {
|
||||
pub(super) type Change = (Vec<(Range<usize>, String)>, Range<usize>);
|
||||
|
||||
/// Removes the picture at the caret, as Typora's Delete Image.
|
||||
pub(super) fn drop_image(text: &str, selection: Range<usize>) -> Result<Change, EditRefusal> {
|
||||
pub(super) fn drop_image(text: &str, at: usize) -> Result<Change, EditRefusal> {
|
||||
let root = file_tree(text);
|
||||
let mut nodes = Vec::new();
|
||||
walk(&root, &mut nodes);
|
||||
let at = selection.start;
|
||||
let picture = (nodes.iter())
|
||||
.filter(|node| matches!(node, Node::Image(_) | Node::ImageReference(_)))
|
||||
.map(|node| span(node))
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{AppContext as _, Context, MouseButton, Window};
|
||||
use gpui::{AppContext as _, Context, MouseButton, Pixels, Point, 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},
|
||||
tree::{Node, Reader, alert, is_toc, lines_at},
|
||||
};
|
||||
|
||||
/// A top-level block's kind.
|
||||
@@ -55,7 +55,7 @@ impl TargetBlock {
|
||||
/// 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.
|
||||
/// The pressed source byte; an open block's as unwritten.
|
||||
pub at: usize,
|
||||
/// None past the last block.
|
||||
pub block: Option<TargetBlock>,
|
||||
@@ -162,16 +162,17 @@ impl MarkdownDocument {
|
||||
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() {
|
||||
/// A right press at `position`; the open block stays open.
|
||||
pub(super) fn point_open(&mut self, position: Point<Pixels>, cx: &mut Context<Self>) {
|
||||
let Some(open) = &self.open else {
|
||||
return log::warn!("markdown document: a press on a block closed since");
|
||||
}
|
||||
};
|
||||
let pressed = open.input.read(cx).offset_for_point(position);
|
||||
let taken = self.snapshot(cx);
|
||||
let (full, _, selection) = taken.clone();
|
||||
let (full, offset, _) = taken.clone();
|
||||
cx.spawn(async move |document, cx| {
|
||||
let target = cx
|
||||
.background_spawn(async move { target_at(&full, selection.start) })
|
||||
.background_spawn(async move { target_at(&full, offset + pressed) })
|
||||
.await;
|
||||
document
|
||||
.update(cx, |document, cx| {
|
||||
@@ -187,18 +188,24 @@ impl MarkdownDocument {
|
||||
}
|
||||
}
|
||||
|
||||
/// What byte `at` of `text` holds.
|
||||
/// What byte `at` of `text` holds, read as drawn.
|
||||
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
|
||||
let top = (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));
|
||||
.find(|node| span(node).start <= at && at <= span(node).end);
|
||||
let block = top.map(|node| TargetBlock::of(node, text));
|
||||
let mut drawn = Vec::new();
|
||||
if let Some(node) = top.filter(|node| !matches!(node, mdast::Node::Definition(_))) {
|
||||
Reader::new(text, &root).top(node, &mut drawn);
|
||||
}
|
||||
let mut flat = Vec::new();
|
||||
flatten(&drawn, &mut flat);
|
||||
let around: Vec<&mdast::Node> = (nodes.iter().copied())
|
||||
.filter(|node| span(node).contains(&at))
|
||||
.collect();
|
||||
@@ -210,16 +217,37 @@ fn target_at(text: &str, at: usize) -> DocumentTarget {
|
||||
_ => 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 written = || {
|
||||
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 picture = around
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|node| matches!(node, mdast::Node::Image(_) | mdast::Node::ImageReference(_)));
|
||||
let shown = picture.and_then(|node| {
|
||||
flat.iter().find_map(|kid| match kid {
|
||||
Node::Image { at, url, link, .. } if *at == span(node).start => {
|
||||
Some((url.clone(), link.clone()))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
let image = match &shown {
|
||||
Some((url, _)) => Some(url.clone()),
|
||||
None => picture.and_then(|node| match node {
|
||||
mdast::Node::Image(picture) => Some(picture.url.clone()),
|
||||
mdast::Node::ImageReference(picture) => defined(&picture.identifier),
|
||||
_ => None,
|
||||
}),
|
||||
};
|
||||
let link = match shown {
|
||||
Some((_, link)) => link,
|
||||
None => drawn_link(&flat, text, at).unwrap_or_else(written),
|
||||
};
|
||||
let code = around.iter().find_map(|node| match node {
|
||||
mdast::Node::Code(code) => Some(code.value.clone()),
|
||||
_ => None,
|
||||
@@ -235,3 +263,42 @@ fn target_at(text: &str, at: usize) -> DocumentTarget {
|
||||
code,
|
||||
}
|
||||
}
|
||||
|
||||
/// `nodes` and all they hold, depth first.
|
||||
fn flatten<'a>(nodes: &'a [Node], out: &mut Vec<&'a Node>) {
|
||||
for node in nodes {
|
||||
out.push(node);
|
||||
match node {
|
||||
Node::Quote(body) | Node::Footnote { body, .. } | Node::Alert { body, .. } => {
|
||||
flatten(body, out)
|
||||
}
|
||||
Node::List { items, .. } => items.iter().for_each(|(_, body)| flatten(body, out)),
|
||||
Node::Table { head, rows, .. } => {
|
||||
(head.iter().chain(rows.iter().flatten())).for_each(|cell| flatten(cell, out))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The link drawn at source byte `at`; none if undrawn.
|
||||
fn drawn_link(nodes: &[&Node], text: &str, at: usize) -> Option<Option<String>> {
|
||||
let inline = nodes.iter().find_map(|node| match node {
|
||||
Node::Paragraph(inline) | Node::Heading(_, inline)
|
||||
if (inline.sources.iter()).any(|(_, span, _)| span.contains(&at)) =>
|
||||
{
|
||||
Some(inline)
|
||||
}
|
||||
_ => None,
|
||||
})?;
|
||||
let drawn = (0..inline.text.len())
|
||||
.filter(|ix| inline.text.is_char_boundary(*ix))
|
||||
.rev()
|
||||
.find(|ix| {
|
||||
inline
|
||||
.source_at(text, *ix)
|
||||
.is_some_and(|source| source <= at)
|
||||
})?;
|
||||
let link = (inline.links.iter()).find(|(range, _)| range.contains(&drawn));
|
||||
Some(link.map(|(_, url)| url.clone()))
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use gpui::{Entity, Modifiers, MouseButton, TestAppContext, VisualTestContext, po
|
||||
use super::{MarkdownMode, open, opened, point::heard, point::press_on, text};
|
||||
use crate::documents::{DocumentTarget, EditRefusal, MarkdownDocument, TargetBlock};
|
||||
|
||||
/// A right press near the left of the open block's text.
|
||||
/// A right press near the open block's left edge.
|
||||
fn press_open(cx: &mut VisualTestContext) {
|
||||
let field = cx.debug_bounds("markdown-open").expect("a block is open");
|
||||
let at = point(field.left() + px(20.0), field.top() + px(14.0));
|
||||
@@ -48,9 +48,11 @@ fn a_right_press_in_a_selection_keeps_the_block_and_the_selection(cx: &mut TestA
|
||||
Some(0..22),
|
||||
"kept open and selected"
|
||||
);
|
||||
let at = heard.borrow().first().expect("heard").at;
|
||||
assert!(at < 6, "the pressed byte, not the selection's: {at}");
|
||||
let target = DocumentTarget {
|
||||
link: Some("https://x.test".into()),
|
||||
..super::point::target(0, TargetBlock::Paragraph)
|
||||
..super::point::target(at, TargetBlock::Paragraph)
|
||||
};
|
||||
assert_eq!(*heard.borrow(), [target]);
|
||||
assert_eq!(text(&document, cx), LINKED, "nothing written");
|
||||
@@ -109,14 +111,12 @@ fn delete_image_takes_the_picture_away_in_one_step(cx: &mut TestAppContext) {
|
||||
let (document, cx) = open(source, MarkdownMode::Source, cx);
|
||||
let heard = super::format::refusals(&document, cx);
|
||||
document.update(cx, |document, cx| {
|
||||
document.set_caret(0, 0, cx);
|
||||
document.delete_image(cx).expect("taken");
|
||||
document.delete_image(0, cx).expect("taken");
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(*heard.borrow(), [EditRefusal::NoImage]);
|
||||
document.update(cx, |document, cx| {
|
||||
document.set_caret(0, 4, cx);
|
||||
document.delete_image(cx).expect("taken");
|
||||
document.delete_image(4, cx).expect("taken");
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(text(&document, cx), "A B\n");
|
||||
@@ -133,3 +133,65 @@ fn text_pastes_at_the_caret_with_no_block_open(cx: &mut TestAppContext) {
|
||||
cx.simulate_keystrokes("secondary-v");
|
||||
assert_eq!(text(&document, cx), "New First.\n");
|
||||
}
|
||||
|
||||
/// A right press on byte `at` of the open block.
|
||||
fn press_open_at(document: &Entity<MarkdownDocument>, at: usize, cx: &mut VisualTestContext) {
|
||||
let spot = document.read_with(cx, |document, cx| {
|
||||
let input = document.open.as_ref().expect("open").input.read(cx);
|
||||
input.bounds_for_range(at..at + 1)[0].center()
|
||||
});
|
||||
cx.simulate_mouse_down(spot, MouseButton::Right, Modifiers::none());
|
||||
cx.simulate_mouse_up(spot, MouseButton::Right, Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
const TWO: &str = "[one](https://a.test) [two](https://b.test)\n";
|
||||
|
||||
#[gpui::test]
|
||||
fn a_press_in_a_selection_reads_the_pressed_link(cx: &mut TestAppContext) {
|
||||
let (document, cx) = open(TWO, MarkdownMode::Visual, cx);
|
||||
let heard = heard(&document, cx);
|
||||
press_on(0, 1, MouseButton::Left, cx);
|
||||
select_open(&document, 0..43, cx);
|
||||
press_open_at(&document, TWO.find("two").expect("two") + 1, cx);
|
||||
assert_eq!(open_selection(&document, cx), Some(0..43), "kept");
|
||||
let link = heard
|
||||
.borrow()
|
||||
.first()
|
||||
.and_then(|target| target.link.clone());
|
||||
assert_eq!(link.as_deref(), Some("https://b.test"));
|
||||
}
|
||||
|
||||
const PICTURES: &str = "x  \n";
|
||||
|
||||
#[gpui::test]
|
||||
fn delete_image_takes_the_pressed_picture(cx: &mut TestAppContext) {
|
||||
let (document, cx) = open(PICTURES, MarkdownMode::Visual, cx);
|
||||
let heard = heard(&document, cx);
|
||||
press_on(0, 0, MouseButton::Left, cx);
|
||||
select_open(&document, 0..25, cx);
|
||||
press_open_at(&document, PICTURES.find("b.png").expect("b"), cx);
|
||||
let target = heard.borrow().first().cloned().expect("heard");
|
||||
assert_eq!(target.image.as_deref(), Some("b.png"));
|
||||
document.update(cx, |document, cx| {
|
||||
document.delete_image(target.at, cx).expect("taken");
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(opened(&document, cx), "x  ");
|
||||
}
|
||||
|
||||
const ANCHORED: &str = "[<a href=\"https://inner.test\">inside</a>](https://outer.test)\n";
|
||||
|
||||
#[gpui::test]
|
||||
fn an_open_block_reads_an_anchor_as_drawn(cx: &mut TestAppContext) {
|
||||
let (document, cx) = open(ANCHORED, MarkdownMode::Visual, cx);
|
||||
let heard = heard(&document, cx);
|
||||
press_on(0, 1, MouseButton::Left, cx);
|
||||
select_open(&document, 0..0, cx);
|
||||
press_open_at(&document, ANCHORED.find("inside").expect("word") + 2, cx);
|
||||
let link = heard
|
||||
.borrow()
|
||||
.first()
|
||||
.and_then(|target| target.link.clone());
|
||||
assert_eq!(link.as_deref(), Some("https://inner.test"));
|
||||
}
|
||||
@@ -72,10 +72,10 @@ impl MarkdownDocument {
|
||||
// Focus fades all but the reader's block.
|
||||
let faded = self.look.focus && editing_mode(self.mode) && ix != self.current_block(cx);
|
||||
let open = self.open.as_ref();
|
||||
let point = cx.listener(|document, _: &MouseDownEvent, window, cx| {
|
||||
let point = cx.listener(|document, event: &MouseDownEvent, window, cx| {
|
||||
window.prevent_default();
|
||||
cx.stop_propagation();
|
||||
document.point_open(cx);
|
||||
document.point_open(event.position, cx);
|
||||
});
|
||||
let field = |open: &super::Open| {
|
||||
div()
|
||||
|
||||
Reference in new issue
Block a user