Right press: one alignment pass, latest press wins
drawn_at aligns each source piece once instead of re-reading the span for every drawn byte, so a right press near a long paragraph's start stays linear. A newer press drops an older pending open-block lookup. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a4bf6965f9
commit
2b8bd9fcf3
5 files changed
+99
-19
No files matched your search
@@ -116,6 +116,8 @@ pub struct MarkdownDocument {
|
||||
focus: FocusHandle,
|
||||
/// A secondary press took focus.
|
||||
pointing: bool,
|
||||
/// The latest press; older lookups lapse.
|
||||
pressed: u64,
|
||||
on_link: Option<OnLink>,
|
||||
on_heading: Option<OnHeading>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
@@ -182,6 +184,7 @@ impl MarkdownDocument {
|
||||
room: Pixels::ZERO,
|
||||
focus,
|
||||
pointing: false,
|
||||
pressed: 0,
|
||||
on_link: None,
|
||||
on_heading: Some(on_heading),
|
||||
_subscriptions: subscriptions,
|
||||
|
||||
@@ -111,6 +111,7 @@ impl MarkdownDocument {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.pressed += 1;
|
||||
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(),
|
||||
@@ -170,12 +171,17 @@ impl MarkdownDocument {
|
||||
let pressed = open.input.read(cx).offset_for_point(position);
|
||||
let taken = self.snapshot(cx);
|
||||
let (full, offset, _) = taken.clone();
|
||||
self.pressed += 1;
|
||||
let press = self.pressed;
|
||||
cx.spawn(async move |document, cx| {
|
||||
let target = cx
|
||||
.background_spawn(async move { target_at(&full, offset + pressed) })
|
||||
.await;
|
||||
document
|
||||
.update(cx, |document, cx| {
|
||||
if document.pressed != press {
|
||||
return log::info!("markdown document: a newer press came first");
|
||||
}
|
||||
if document.open.is_none() || document.snapshot(cx) != taken {
|
||||
return log::info!("markdown document: the pressed block changed first");
|
||||
}
|
||||
@@ -291,14 +297,7 @@ fn drawn_link(nodes: &[&Node], text: &str, at: usize) -> Option<Option<String>>
|
||||
}
|
||||
_ => 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 drawn = inline.drawn_at(text, at)?;
|
||||
let link = (inline.links.iter()).find(|(range, _)| range.contains(&drawn));
|
||||
Some(link.map(|(_, url)| url.clone()))
|
||||
}
|
||||
@@ -162,6 +162,30 @@ fn a_press_in_a_selection_reads_the_pressed_link(cx: &mut TestAppContext) {
|
||||
assert_eq!(link.as_deref(), Some("https://b.test"));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn only_the_latest_press_in_an_open_block_is_heard(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);
|
||||
let spots = document.read_with(cx, |document, cx| {
|
||||
let input = document.open.as_ref().expect("open").input.read(cx);
|
||||
["one", "two"].map(|word| {
|
||||
let at = TWO.find(word).expect("word") + 1;
|
||||
input.bounds_for_range(at..at + 1)[0].center()
|
||||
})
|
||||
});
|
||||
document.update(cx, |document, cx| {
|
||||
document.point_open(spots[0], cx);
|
||||
document.point_open(spots[1], cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let links: Vec<_> = (heard.borrow().iter())
|
||||
.map(|target| target.link.clone())
|
||||
.collect();
|
||||
assert_eq!(links, [Some("https://b.test".to_string())]);
|
||||
}
|
||||
|
||||
const PICTURES: &str = "x  \n";
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -196,6 +196,25 @@ impl Inline {
|
||||
// Unreadable text lands at its start.
|
||||
Some(span.start + align(drawn, written, *escapes, at - start).unwrap_or(0))
|
||||
}
|
||||
|
||||
/// The last drawn byte written by source byte `at`.
|
||||
pub(crate) fn drawn_at(&self, source: &str, at: usize) -> Option<usize> {
|
||||
(0..self.sources.len()).rev().find_map(|piece| {
|
||||
let (start, span, escapes) = &self.sources[piece];
|
||||
if span.start > at {
|
||||
return None;
|
||||
}
|
||||
let end = (self.sources.get(piece + 1)).map_or(self.text.len(), |(next, ..)| *next);
|
||||
let drawn = self.text.get(*start..end.max(*start))?;
|
||||
let written = source.get(span.clone())?;
|
||||
// Unreadable text lands at its start.
|
||||
let mut places = aligned(drawn, written, *escapes).into_iter();
|
||||
(drawn.char_indices().rev())
|
||||
.map(|(byte, _)| (byte, places.next().map_or(0, |(_, place)| place)))
|
||||
.find(|(_, place)| span.start + place <= at)
|
||||
.map(|(byte, _)| start + byte)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A source char as read, and where it was written.
|
||||
@@ -285,11 +304,21 @@ fn align(drawn: &str, written: &str, escapes: Escapes, at: usize) -> Option<usiz
|
||||
if at >= drawn.len() {
|
||||
return Some(written.len());
|
||||
}
|
||||
(aligned(drawn, written, escapes).into_iter())
|
||||
.find(|(byte, _)| *byte == at)
|
||||
.map(|(_, place)| place)
|
||||
}
|
||||
|
||||
/// Drawn chars from the end and where each was written.
|
||||
fn aligned(drawn: &str, written: &str, escapes: Escapes) -> Vec<(usize, usize)> {
|
||||
let source = read(written, escapes);
|
||||
let mut j = source.len();
|
||||
let mut out = Vec::new();
|
||||
for (byte, d) in drawn.char_indices().rev() {
|
||||
let found = loop {
|
||||
let s = source[..j].last()?;
|
||||
let Some(s) = source[..j].last() else {
|
||||
return out;
|
||||
};
|
||||
if d == s.ch {
|
||||
j -= 1;
|
||||
break s.at;
|
||||
@@ -307,29 +336,54 @@ fn align(drawn: &str, written: &str, escapes: Escapes, at: usize) -> Option<usiz
|
||||
j -= 1;
|
||||
continue;
|
||||
}
|
||||
return None;
|
||||
return out;
|
||||
};
|
||||
if byte == at {
|
||||
return Some(found);
|
||||
}
|
||||
out.push((byte, found));
|
||||
}
|
||||
None
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::{Reader, tree};
|
||||
use super::super::{Inline, Reader, tree};
|
||||
|
||||
/// The source byte under drawn text in the first paragraph.
|
||||
fn under(source: &str, drawn: &str) -> usize {
|
||||
/// The first paragraph, as drawn.
|
||||
fn first(source: &str) -> Inline {
|
||||
let root = tree(source);
|
||||
let reader = Reader::new(source, &root);
|
||||
let paragraph = &root.children().expect("blocks")[0];
|
||||
let inline = reader.inline(paragraph.children().expect("inlines"));
|
||||
reader.inline(paragraph.children().expect("inlines"))
|
||||
}
|
||||
|
||||
/// The source byte under drawn text in the first paragraph.
|
||||
fn under(source: &str, drawn: &str) -> usize {
|
||||
let inline = first(source);
|
||||
let at = inline.text.find(drawn).expect("drawn");
|
||||
inline.source_at(source, at).expect("a piece")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_source_byte_reads_back_to_its_drawn_byte() {
|
||||
let source = "Say **bold** and \\*x";
|
||||
let inline = first(source);
|
||||
let bold = source.find("bold").unwrap();
|
||||
assert_eq!(inline.drawn_at(source, bold), inline.text.find("bold"));
|
||||
let mark = source.find("**").unwrap();
|
||||
assert_eq!(inline.drawn_at(source, mark), inline.text.find(' '));
|
||||
let star = source.find("*x").unwrap();
|
||||
assert_eq!(inline.drawn_at(source, star), inline.text.find('*'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_paragraph_reads_back_in_one_pass() {
|
||||
let source = "x".repeat(40_000);
|
||||
let inline = first(&source);
|
||||
let started = std::time::Instant::now();
|
||||
assert_eq!(inline.drawn_at(&source, 5), Some(5));
|
||||
let took = started.elapsed();
|
||||
assert!(took.as_millis() < 500, "took {took:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_word_lands_where_it_was_written() {
|
||||
let source = "Say **bold** and `code` here\nnext line";
|
||||
|
||||
Reference in new issue
Block a user