1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
//! Cursor-block tracking and the jitter-suppression reveal timer.
//!
//! Methods extracted from `EditorState`'s big `impl` block. Lives on the
//! same struct via Rust's ability to have multiple `impl` blocks across
//! files in the same crate.
use std::time::Instant;
use crate::editor::state::ImageReveal;
use crate::editor::{EditorState, Mode, RAW_REVEAL_DELAY};
impl EditorState {
/// Call after any cursor movement in Rendered mode. Tracks which block the
/// cursor is in and which buffer line it is on. `RenderedView` uses
/// `cursor_block_entered_at` to delay revealing the raw cursor-block view.
/// The timer resets whenever the cursor moves to a **different buffer line**
/// (not just a different block), so that the delay is consistent regardless
/// of whether the block is a single-line paragraph or a fifty-line table.
pub fn update_cursor_block(&mut self) {
let cursor_byte = self.buffer.rope().char_to_byte(self.cursor.offset);
let previous_block_idx = self.cursor_block_idx;
// Always keep cursor_block_idx up-to-date (used by rendered_view for
// extracting the raw source of the current block).
self.cursor_block_idx = self.parsed.source_map.block_for_byte(cursor_byte);
// Cache the cursor block's buffer line range. Used by rendered_view
// to extract the raw block source during a typing burst without
// consulting the (then-stale) source_map. In-line edits keep line
// indices stable — no newlines added or removed — so this range
// stays correct until the cursor moves or a cross-line edit fires
// refresh_parsed.
self.cursor_block_line_range = self.cursor_block_idx.and_then(|idx| {
let byte_range = self.parsed.source_map.original_range_for_block(idx)?;
let rope = self.buffer.rope();
let total_bytes = rope.len_bytes();
let start_byte = byte_range.start.min(total_bytes);
let end_byte = byte_range.end.min(total_bytes);
let start_char = rope.byte_to_char(start_byte);
// Use `end_byte.saturating_sub(1)` so a range that ends on a `\n`
// doesn't claim the next line.
let end_char = rope.byte_to_char(end_byte.saturating_sub(1).max(start_byte));
let start_line = rope.char_to_line(start_char);
let end_line = rope.char_to_line(end_char).max(start_line);
Some(start_line..end_line + 1)
});
// Reset the reveal timer only when the cursor moves to a different
// logical buffer line — this makes scrolling through a large table feel
// uniform: each row gets the same delay, not the whole table at once.
//
// Exception: a mermaid diagram block reveals as a single unit (every
// rendered row swaps to raw source), so re-arming the timer on every
// intra-block line move would flash the image placeholder back in
// between line moves. Keep the existing reveal time once the cursor
// is inside a mermaid block until it leaves.
let (current_line, _) = self.cursor.line_col(&self.buffer);
if Some(current_line) != self.cursor_line_idx {
let staying_in_mermaid = previous_block_idx == self.cursor_block_idx
&& self
.cursor_block_idx
.is_some_and(|idx| self.parsed.is_mermaid_block(idx));
self.cursor_line_idx = Some(current_line);
if !staying_in_mermaid {
self.cursor_block_entered_at = Some(Instant::now());
}
}
self.cursor_blink.reset();
}
/// Whether the cursor should be painted this frame. Combines the
/// blink state with the current mode — Preview never shows a cursor.
pub fn cursor_visible(&self) -> bool {
self.terminal_focused
&& self.mode != Mode::Preview
&& (self.modal_open || self.cursor_blink.is_visible())
}
/// Returns true when the raw view for the cursor block should be shown.
/// False during the `RAW_REVEAL_DELAY` window after the cursor entered a
/// new block (so rapidly-traversed blocks stay rendered), and false
/// while a mouse drag is in progress (so the user's visible click
/// anchor doesn't shift under the drag).
pub fn cursor_block_revealed(&self) -> bool {
if self.drag_in_progress {
return false;
}
// An active search flow keeps the document fully rendered:
// tabbing through matches must not flip blocks between rendered
// and raw under the highlights. This holds even for a
// non-capturing navigate flow, where editing is allowed — the
// highlights stay stable until the user dismisses the search.
if self.search.is_some() {
return false;
}
// Likewise during a live `:s` preview: the preview parks the
// cursor on the first affected line while the user types, and
// the reveal delay would elapse mid-typing, flipping that block
// to raw source under the preview highlights.
if self.substitute_preview.is_some() {
return false;
}
match self.cursor_block_entered_at {
None => true,
Some(t) => t.elapsed() >= RAW_REVEAL_DELAY,
}
}
/// Bring [`EditorState::image_reveal`] in line with where the cursor
/// is right now, re-parsing when it changed. Returns `true` when the
/// document was re-laid-out, so the caller can force a redraw.
///
/// Called once per frame from `App::prepare_viewport` because the
/// reveal is time-driven (the `RAW_REVEAL_DELAY` window elapses without
/// any event of its own), so there is no single action site that could
/// own the transition. It is a no-op on every frame where the target
/// hasn't moved — the re-parse only fires as the cursor enters or
/// leaves an image block.
pub fn sync_image_reveal(&mut self) -> bool {
// The target borrows out of `parsed`, so nothing is allocated on
// the overwhelmingly common no-op path — this runs on every pass of
// the event loop (every keystroke and every idle tick), not just on
// the frames that draw. Only a real transition pays for the
// `String`.
let target = self.image_reveal_target();
let unchanged = match (target, self.image_reveal.as_ref()) {
(Some((ordinal, url, rows)), Some(cur)) => {
ordinal == cur.ordinal && url == cur.url.as_str() && rows == cur.rows
}
(None, None) => true,
_ => false,
};
if unchanged {
return false;
}
self.image_reveal = target.map(|(ordinal, url, rows)| ImageReveal {
ordinal,
url: url.to_owned(),
rows,
});
// Only the block's *rendered* row count changed — the source is
// untouched, so every byte range (and with it the cached cursor
// block and its line range) survives the re-parse unchanged.
self.refresh_parsed();
true
}
/// The reservation the image reveal wants for the current cursor
/// position: `(image-block ordinal, image URL, one row per raw source
/// line)`, or `None` when the cursor isn't resting inside a revealed
/// image block. The ordinal is the block's index into
/// `ParsedDoc::image_blocks`, which is the index space the renderer's
/// row override counts in — see [`ImageReveal`] for why the URL alone
/// is not enough to name a block.
///
/// The URL is borrowed out of `self.parsed` rather than cloned: the
/// caller runs this per event-loop pass purely to compare against the
/// stashed reservation, and owns the result only when they differ.
fn image_reveal_target(&self) -> Option<(usize, &str, usize)> {
// Preview is browse-only and Raw already shows the source, so the
// reveal — and its reflow — belongs to Rendered mode alone.
if self.mode != Mode::Rendered {
return None;
}
// Mid-typing the parse is stale, and with it the block's URL (a
// diagram's hashes its source; an ordinary image's *is* source
// text). An in-line edit can't change the block's line count, so
// hold the current reservation rather than recomputing one against
// a URL that no longer exists.
if self.parsed_dirty {
return self
.image_reveal
.as_ref()
.map(|r| (r.ordinal, r.url.as_str(), r.rows));
}
if !self.cursor_block_revealed() {
return None;
}
let cursor_byte = self.buffer.rope().char_to_byte(self.cursor.offset);
let block_idx = self.parsed.source_map.block_for_byte(cursor_byte)?;
// Every `Block::ImageBlock` qualifies, diagram or not: the row
// reservation tracks the rendered image in both cases, and the raw
// source it hides is a mermaid fence in one and a single
// `` line in the other.
if !self.parsed.is_image_block(block_idx) {
return None;
}
// `position` rather than `find`: the index *is* the ordinal the
// renderer's row override counts to, so the same walk answers both
// halves of the block's identity.
let ordinal = self
.parsed
.image_blocks
.iter()
.position(|info| info.block_idx == block_idx)?;
let url = self.parsed.image_blocks[ordinal].url.as_str();
let range = self.parsed.source_map.original_range_for_block(block_idx)?;
// Read the document out of `ParsedDoc`, not the live `Buffer`: the
// range is a *parse-time* byte range, and this runs on every frame
// the cursor rests in an image block — `Buffer::contents()` would
// allocate the whole document as a `String` each time.
let contents = self.parsed.source();
let source = contents.get(range.start..range.end.min(contents.len()))?;
// Counts the lines `RenderedView` reveals through — minus a
// trailing blank the extended block range absorbed, which owns a
// rendered row of its own as a virtual block. Derived from the
// same split the painter uses, so the reserved rows and the raw
// lines painted onto them can't disagree, and without allocating
// the `Vec` of slices just to read its length.
let rows = crate::ui::rendered_view::revealed_source_line_count(source);
Some((ordinal, url, rows))
}
}