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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
//! Rendered-row → source-line mapping for the line-number gutter. Rendered rows diverge
//! from buffer lines (tables, image reserves), while every other line-numbered surface
//! (`{count}G`, the Raw gutter) counts buffer lines, so the gutter translates.
//!
//! Invariants:
//! - The map is a walk over the rows' recorded origins (`ParsedDoc::row_origins`, through
//! [`row_map::lines_of_row`]), never a derivation of its own that could drift from the
//! renderer.
//! - **A row is numbered with the first line it shows, and only the first row to reach a line
//! numbers it.** A marker on a row of its own and the block below it, a table row's wrap
//! chunks and separators, an image's reserves: the later rows stay blank. A line with no row
//! of its own (a link reference definition inside a container) is omitted, never reassigned.
//! Numbers therefore ascend and never repeat.
//! - The walk reads [`ParsedDoc::source`], never the live `Buffer`: it resolves parse-time
//! byte ranges, and a deferred in-line edit leaves the buffer ahead of the parse. The
//! table is memoized per parse, so a mislabel would persist until the next re-parse.
use crate::document::row_map;
use crate::document::ParsedDoc;
use crate::editor::effective_rows::EffectiveRows;
use crate::editor::EditorState;
#[cfg(test)]
thread_local! {
/// Rebuild counter for the cache-key tests; thread-local because tests run in parallel.
static BUILD_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
impl EditorState {
/// Buffer line to label visual row `visual_row` with, or `None` for an unnumbered row.
/// Preview/Rendered counterpart of `raw_line_at_visual_row`. Builds an [`EffectiveRows`] per
/// call; the gutter, which asks per visible row, calls [`Self::source_line_at_visual_row_with`]
/// with one built for the whole frame instead.
pub fn source_line_at_visual_row(&self, visual_row: usize, width: usize) -> Option<usize> {
self.source_line_at_visual_row_with(&self.effective_rows(width), visual_row)
}
/// [`Self::source_line_at_visual_row`] against a prebuilt [`EffectiveRows`], so a per-row
/// caller doesn't reconstruct it (which allocates the revealed block's source) every row.
pub(crate) fn source_line_at_visual_row_with(
&self,
effective: &EffectiveRows,
visual_row: usize,
) -> Option<usize> {
use crate::editor::effective_rows::RowHit;
// Route through `EffectiveRows` so a revealed reflowed paragraph's raw expansion is
// counted: its rows are raw source lines, and rows below it shift. Identity everywhere
// else, so this equals the base `line_at_visual_row` outside that reveal.
match effective.line_at_visual_row(visual_row) {
RowHit::Raw { raw_line, sub } => {
if sub != 0 {
return None; // a wrap continuation of a raw line carries no number
}
// Each revealed raw line is a source line: `raw_line` is block-relative, so the
// block's first source line + it.
let cursor_byte = self.buffer.rope().char_to_byte(self.cursor.offset);
let block_start = self
.parsed
.source_map
.original_range_for_byte(cursor_byte)?
.start;
Some(self.buffer.rope().byte_to_line(block_start) + raw_line)
}
RowHit::Rendered {
line: rendered_idx,
sub,
} => {
if sub != 0 {
return None;
}
// Cached on the `ParsedDoc`, not keyed on buffer version: an in-line edit bumps the
// version without moving a line, and the walk is full-document.
let parsed = &self.parsed;
parsed
.source_lines_or_init(|| build_source_line_map(parsed))
.get(rendered_idx)
.copied()
.flatten()
}
}
}
}
/// Walk every block's rows, numbering each with the first source line its origin shows.
/// Reads [`ParsedDoc::source`] only (see the module doc).
fn build_source_line_map(parsed: &ParsedDoc) -> Vec<Option<usize>> {
#[cfg(test)]
BUILD_COUNT.with(|c| c.set(c.get() + 1));
let mut map: Vec<Option<usize>> = vec![None; parsed.lines.len()];
if map.is_empty() {
return map;
}
let contents = parsed.source();
// Running newline count instead of `byte_to_line` per block, which would be quadratic.
let mut scanned = 0usize;
let mut block_line = 0usize;
// The highest line reached so far: a row reaching no further shows nothing new.
let mut numbered: Option<usize> = None;
for block_idx in 0..parsed.source_map.block_count() {
let Some(range) = parsed.source_map.original_range_for_block(block_idx) else {
continue;
};
let start = range.start.min(contents.len());
// Before any `continue`: the counter is only honest while it sees every byte.
if start > scanned {
block_line += contents.as_bytes()[scanned..start]
.iter()
.filter(|&&b| b == b'\n')
.count();
scanned = start;
}
// Row-less blocks (collapsed blank, hidden HTML comment) must be skipped by their
// *own* row count: `rendered_lines_for_block` hands such a block its neighbor's
// range as a fallback, which would label another block's row with this one's line.
let own = parsed.block_own_line_count(block_idx);
let first_row = parsed.source_map.rendered_lines_for_block(block_idx).start;
for row in 0..own {
let Some(lines) = row_map::lines_of_row(parsed, block_idx, row) else {
continue;
};
let line = block_line + lines.start as usize;
if numbered.is_some_and(|n| line <= n) {
continue;
}
if let Some(slot) = map.get_mut(first_row + row) {
*slot = Some(line);
// A row showing several lines (a flow) reaches its last one too.
numbered = Some(
block_line
+ (lines.end as usize)
.saturating_sub(1)
.max(lines.start as usize),
);
}
}
}
map
}
#[cfg(test)]
mod tests {
use crate::config::Theme;
use crate::document::Buffer;
use crate::editor::EditorState;
fn theme() -> &'static Theme {
Box::leak(Box::new(Theme::default()))
}
fn state_for(source: &str, width: usize) -> EditorState {
let mut state = EditorState::new(Buffer::from_str(source), theme());
state.mode = crate::editor::Mode::Rendered;
state.set_viewport_width(width);
state.refresh_parsed();
state
}
/// How many times `f` rebuilt the memoised table.
fn builds(f: impl FnOnce()) -> usize {
let before = super::BUILD_COUNT.with(|c| c.get());
f();
super::BUILD_COUNT.with(|c| c.get()) - before
}
/// Type `text` the way `edit_ops::insert_text` does, so the deferred-parse path applies.
fn type_text(state: &mut EditorState, text: &str) {
state.apply_delta(crate::document::EditDelta {
offset: state.cursor.offset,
removed: String::new(),
inserted: text.to_owned(),
});
}
/// Every visual row's label, in order, for the whole document.
fn labels(state: &EditorState, width: usize) -> Vec<Option<usize>> {
(0..state.parsed.total_visual_rows(width))
.map(|row| state.source_line_at_visual_row(row, width))
.collect()
}
#[test]
fn plain_paragraphs_number_one_to_one() {
let state = state_for("alpha\n\nbravo\n", 80);
assert_eq!(labels(&state, 80), vec![Some(0), Some(1), Some(2), Some(3)]);
}
#[test]
fn table_rows_map_back_to_their_source_lines() {
let source = "| a | b |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |\nafter\n";
let state = state_for(source, 80);
let labels = labels(&state, 80);
let numbered: Vec<usize> = labels.iter().flatten().copied().collect();
assert_eq!(numbered, vec![0, 1, 2, 3, 4, 5]);
assert!(labels.len() > numbered.len());
}
/// The rule row `ParsedDoc::build` appends is the underline's own line, not an artifact.
#[test]
fn setext_heading_numbers_both_of_its_source_lines() {
let state = state_for("Title\n-----\n\nbody\n", 80);
let labels = labels(&state, 80);
assert_eq!(labels, vec![Some(0), Some(1), Some(2), Some(3), Some(4)]);
}
/// Guards the row-less-block skip in `build_source_line_map`.
#[test]
fn hidden_html_comment_claims_no_row() {
let state = state_for("Alpha.\n\n<!-- hidden -->\n\nBeta.\n", 80);
let labels = labels(&state, 80);
let numbered: Vec<usize> = labels.iter().flatten().copied().collect();
assert!(
!numbered.contains(&2),
"the comment's line must not be numbered: {labels:?}"
);
assert!(
numbered.contains(&4),
"`Beta.` must keep its own number: {labels:?}"
);
assert!(
numbered.windows(2).all(|w| w[0] < w[1]),
"numbers must stay ascending and unique: {labels:?}"
);
}
/// Same trap as the hidden comment, for collapsed blank runs.
#[test]
fn suppressed_blank_lines_claim_no_row() {
let theme = theme();
let mut state = EditorState::new_with_config(
Buffer::from_str("alpha\n\n\n\nbravo\n"),
theme,
false,
true,
24,
);
state.mode = crate::editor::Mode::Rendered;
state.set_viewport_width(80);
state.refresh_parsed();
let labels = labels(&state, 80);
let numbered: Vec<usize> = labels.iter().flatten().copied().collect();
assert!(
numbered.contains(&4),
"`bravo` must keep its own number: {labels:?}"
);
assert!(
numbered.windows(2).all(|w| w[0] < w[1]),
"numbers must stay ascending and unique: {labels:?}"
);
}
#[test]
fn image_reserve_rows_are_blank_below_the_first() {
let mut state = state_for("\n\nafter\n", 80);
state.image_max_height = 6;
state.refresh_parsed();
let labels = labels(&state, 80);
assert_eq!(labels.first().copied().flatten(), Some(0));
let numbered: Vec<usize> = labels.iter().flatten().copied().collect();
assert_eq!(numbered, vec![0, 1, 2, 3]);
}
#[test]
fn wrapped_continuation_rows_are_blank() {
let source = "aaaa bbbb cccc dddd eeee ffff\n";
let state = state_for(source, 12);
let labels = labels(&state, 12);
assert!(labels.len() > 2, "expected the line to wrap");
assert_eq!(labels[0], Some(0));
assert!(labels[1..labels.len() - 1].iter().all(|l| l.is_none()));
}
/// Regression for last-writer-wins: first-writer numbered the `continuation` row with
/// the swallowed blank's line.
#[test]
fn a_swallowed_blank_does_not_steal_the_next_line_number() {
let state = state_for("Intro.\n\n- item\n\n continuation\n\nafter\n", 80);
let labels = labels(&state, 80);
let rendered: Vec<String> = state
.parsed
.lines
.iter()
.map(|l| l.spans.iter().map(|s| s.content.as_ref()).collect())
.collect();
let row = rendered
.iter()
.position(|t| t.contains("continuation"))
.expect("the continuation must render");
assert_eq!(
labels[row],
Some(4),
"row {row} shows `continuation` (line index 4): {labels:?}"
);
// The blank inside the item has its own row, numbered with its own line.
assert_eq!(labels[row - 1], Some(3), "{labels:?}");
assert_eq!(
labels.iter().flatten().filter(|&&l| l == 3).count(),
1,
"{labels:?}"
);
}
/// A blank inside a code block nested in a list item renders a code row, and the blank
/// between the item's text and the block a blank row: every line keeps its number.
#[test]
fn a_blank_inside_a_nested_code_block_keeps_its_number() {
let source = "8. Tag it.\n\n ```bash\n gh run watch\n\n indented\n ```\n";
let state = state_for(source, 80);
let labels = labels(&state, 80);
// Rows: item, blank, ` bash ` label, body, blank body, body, closing fence.
assert_eq!(
labels[..7],
[
Some(0),
Some(1),
Some(2),
Some(3),
Some(4),
Some(5),
Some(6)
],
"{labels:?}"
);
}
/// Emphasis wrapped across a line break is cut in two at it, so each line keeps a row and
/// a number of its own. (When it rendered both lines on one row, taking that row for one
/// line numbered every row below it with the line above its own.)
#[test]
fn emphasis_across_a_break_keeps_the_numbers_below_it() {
// With reflow off, so the item keeps a row per source line.
let mut state = state_for("- *a\n b* c\n d\n", 80);
state.set_reflow(false);
// Rows: `• a`, `b c`, `d`.
assert_eq!(labels(&state, 80)[..3], [Some(0), Some(1), Some(2)]);
}
/// An item whose first block starts on its marker line renders the marker on a row of its
/// own, so that line spans two rows; counting it as one put every number below it a row
/// high. The extra row (the fence label, the nested item, the quote) stays unnumbered,
/// and the cursor on each line lands on its numbered row — except where the line's chars
/// show only on the row below its marker (`- - a`), where the cursor goes with them.
#[test]
fn a_marker_line_opening_a_block_keeps_the_numbers_below_it() {
for (source, expected) in [
(
"- ```bash\n code\n ```\n- next item\n\n- third\n",
// Rows: `•`, ` bash ` label, body, closing fence, item, separator, item.
&[Some(0), None, Some(1), Some(2), Some(3), Some(4), Some(5)][..],
),
(
"- - a\n - b\n- c\n",
// Rows: `•`, nested `• a`, nested `• b`, `• c`.
&[Some(0), None, Some(1), Some(2)][..],
),
(
"1. a\n - ```\n x\n ```\n2. b\n",
// Rows: `1. a`, nested `•`, empty label, body, closing fence, `2. b`.
&[Some(0), Some(1), None, Some(2), Some(3), Some(4)][..],
),
(
// An unclosed fence, as while typing it, still renders a closing row.
"- a\n ```\n x\n- b\n",
// Rows: `• a`, empty label, body, closing placeholder, `• b`.
&[Some(0), Some(1), Some(2), None, Some(3)][..],
),
(
"- a\n soft\n- b\n",
// Rows: `• a`, ` soft`, `• b`.
&[Some(0), Some(1), Some(2)][..],
),
] {
// With reflow off, so an item's soft break keeps its row.
let mut state = state_for(source, 80);
state.set_reflow(false);
let labels = labels(&state, 80);
assert_eq!(labels[..expected.len()], *expected, "{source:?}");
for (row, line) in expected
.iter()
.enumerate()
.filter_map(|(row, l)| l.map(|line| (row, line)))
{
// `- - a`: line 0's `•` row shows none of its chars; ` • a` below does.
let row = if source.starts_with("- - a") && line == 0 {
1
} else {
row
};
state.cursor.offset = state.buffer.line_to_char(line);
state.update_cursor_block();
assert_eq!(
state.cursor_visual_row(80),
row,
"{source:?}: the cursor on line {line} must land on row {row}"
);
}
}
}
/// A bare marker shares its paragraph's row (`• text`), numbered for the text's line. With
/// the cursor on the marker's line, which has no row of its own, the revealed row stacks both
/// lines, so the marker shows and each line keeps its number.
#[test]
fn a_bare_markers_line_reveals_with_the_row_it_shares() {
let source = "-\n text\n- b\n";
let mut state = state_for(source, 80);
state.set_reflow(false);
for (line, expected, row) in [
(2, &[Some(1), Some(2)][..], 1),
(1, &[Some(1), Some(2)][..], 0),
(0, &[Some(0), Some(1), Some(2)][..], 0),
] {
state.cursor.offset = state.buffer.line_to_char(line);
state.update_cursor_block();
// Past the reveal delay.
state.cursor_block_entered_at = None;
let labels = labels(&state, 80);
assert_eq!(
labels[..expected.len()],
*expected,
"the cursor on line {line}"
);
assert_eq!(
state.cursor_visual_row(80),
row,
"the cursor on line {line}"
);
}
}
/// A loose list inside a blockquote keeps its spacing, so every bare `>` line has a row of
/// its own and each number and cursor row matches its source line.
#[test]
fn a_loose_list_in_a_blockquote_keeps_the_numbers_below_it() {
let source = "> - a\n>\n> - b\n>\n> tail\n";
let mut state = state_for(source, 80);
// Rows: `• a`, the quoted blank, `• b`, the quoted blank, `tail`.
let expected = [Some(0), Some(1), Some(2), Some(3), Some(4)];
let labels = labels(&state, 80);
assert_eq!(labels[..expected.len()], expected, "{labels:?}");
for (row, line) in [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)] {
state.cursor.offset = state.buffer.line_to_char(line);
state.update_cursor_block();
assert_eq!(
state.cursor_visual_row(80),
row,
"the cursor on line {line} must land on row {row}"
);
}
}
/// Sweeps the gutter invariants (unique, ascending, agrees with where `{count}G` parks
/// the cursor) over every block kind in the sample fixture at a wrapping and a
/// non-wrapping width. Compares against `cursor_visual_row`, not
/// `cursor_rendered_line_idx`, which only coincides with visual rows when nothing wraps.
#[test]
fn fixture_labels_are_unique_ascending_and_agree_with_the_cursor() {
let source = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/general.md"
))
.expect("the sample fixture must be readable");
for width in [100, 60] {
let mut state = state_for(&source, width);
let labels = labels(&state, width);
let numbered: Vec<usize> = labels.iter().flatten().copied().collect();
assert!(
numbered.iter().all(|&l| l < state.buffer.line_count()),
"width {width}: a label named a line past the end of the buffer"
);
assert!(
numbered.windows(2).all(|w| w[0] < w[1]),
"width {width}: labels must ascend and be unique, got {numbered:?}"
);
for (row, line) in labels
.iter()
.enumerate()
.filter_map(|(row, l)| l.map(|line| (row, line)))
{
state.cursor.offset = state.buffer.line_to_char(line);
state.update_cursor_block();
assert_eq!(
state.cursor_visual_row(width),
row,
"width {width}: the gutter labels row {row} with line {line}, \
but the cursor on that line lands elsewhere"
);
}
}
}
/// A reflowed paragraph (Preview mode) collapses its source lines into one flow, so the
/// gutter numbers only its first rendered row — with the flow's *first* source line — and
/// leaves the rest folded. Without the first-writer branch the row would show the paragraph's
/// last source line.
#[test]
fn reflowed_paragraph_labels_its_first_source_line() {
let mut state = EditorState::new(Buffer::from_str("one\ntwo\nthree\n\nafter\n"), theme());
// Preview reflows by default; reconcile the parse the way the App does each frame.
state.set_viewport_width(80);
state.sync_reflow_for_mode();
let labels = labels(&state, 80);
assert_eq!(
labels.first().copied().flatten(),
Some(0),
"the flow's row must be numbered with its first source line: {labels:?}"
);
let numbered: Vec<usize> = labels.iter().flatten().copied().collect();
assert!(
!numbered.contains(&1) && !numbered.contains(&2),
"the folded soft-break lines must not be numbered: {labels:?}"
);
assert!(
numbered.contains(&4),
"`after` (line 4) must keep its own number: {labels:?}"
);
}
/// A nested reflowed paragraph (an item's) numbers its flow row with its first source line
/// only, its folded lines unnumbered, while it stays rendered; revealed, its stacked raw lines
/// each carry their own number, and every row below keeps its own.
#[test]
fn a_nested_reflowed_paragraph_numbers_its_lines() {
let source = "- one\n- alpha\n bravo\n charlie\n- four\n";
let mut state = state_for(source, 80);
// Entered on its first line, so the reveal delay runs: the item's flow is one row.
state.cursor.offset = source.find("alpha").unwrap();
state.update_cursor_block();
assert_eq!(labels(&state, 80)[..3], [Some(0), Some(1), Some(4)]);
state.cursor_block_entered_at = None;
let effective = state.effective_rows(80);
assert!(effective.has_reveal());
let revealed: Vec<_> = (0..effective.total_visual_rows())
.map(|row| state.source_line_at_visual_row_with(&effective, row))
.collect();
assert_eq!(revealed[..5], [Some(0), Some(1), Some(2), Some(3), Some(4)]);
}
/// The same for a quote's paragraph, one of two in its block: collapsed, its flow row takes
/// its first line's number; stacked, each line its own, and the paragraph below keeps its.
#[test]
fn a_quoted_reflowed_paragraph_numbers_its_lines() {
let source = "> alpha\n> bravo\n> charlie\n>\n> last\n";
let mut state = state_for(source, 80);
state.cursor.offset = source.find("alpha").unwrap();
state.update_cursor_block();
assert_eq!(labels(&state, 80)[..3], [Some(0), Some(3), Some(4)]);
state.cursor_block_entered_at = None;
let effective = state.effective_rows(80);
assert!(effective.has_reveal());
let revealed: Vec<_> = (0..effective.total_visual_rows())
.map(|row| state.source_line_at_visual_row_with(&effective, row))
.collect();
assert_eq!(revealed[..5], [Some(0), Some(1), Some(2), Some(3), Some(4)]);
}
/// A version-keyed cache would rebuild the full-document walk on every keystroke.
#[test]
fn typing_within_a_line_does_not_rebuild_the_table() {
let mut state = state_for("alpha\n\nbravo\n", 80);
// This exercises the memoized rendered→source table, which is orthogonal to reflow; with
// reflow on, row 0 is a revealed raw line that never consults the table, so disable it to
// keep the caching assertions about the table itself.
state.set_reflow(false);
let before = builds(|| {
let _ = state.source_line_at_visual_row(0, 80);
});
assert_eq!(before, 1, "first query builds");
state.cursor.offset = state.buffer.line_to_char(2);
type_text(&mut state, "xyz");
let rebuilds = builds(|| {
let _ = state.source_line_at_visual_row(0, 80);
});
assert_eq!(rebuilds, 0, "an in-line edit must reuse the table");
assert_eq!(labels(&state, 80), vec![Some(0), Some(1), Some(2), Some(3)]);
}
#[test]
fn inserting_a_newline_rebuilds_the_table() {
let mut state = state_for("alpha\n\nbravo\n", 80);
let _ = state.source_line_at_visual_row(0, 80);
state.cursor.offset = state.buffer.line_to_char(2);
type_text(&mut state, "\n");
let rebuilds = builds(|| {
let _ = state.source_line_at_visual_row(0, 80);
});
assert_eq!(rebuilds, 1, "a re-parse must drop the table");
assert_eq!(
labels(&state, 80),
vec![Some(0), Some(1), Some(2), Some(3), Some(4)]
);
}
/// Regression: the walk once read block text out of the live `Buffer` with parse-time
/// byte ranges, so a deferred edit ahead of the parse (a newline and a character typed
/// within one 16 ms frame) produced `[0, 1, 1, 3, 3, 5, 5]`.
#[test]
fn a_deferred_edit_does_not_shift_the_labels() {
let mut state = state_for("alpha\n\nbravo\n\ncharlie\n", 80);
state.cursor.offset = 0;
type_text(&mut state, "\n");
state.cursor.offset = 0;
type_text(&mut state, "zzz");
assert_eq!(state.buffer.contents(), "zzz\nalpha\n\nbravo\n\ncharlie\n");
assert_eq!(
labels(&state, 80),
vec![
Some(0),
Some(1),
Some(2),
Some(3),
Some(4),
Some(5),
Some(6)
],
);
}
#[test]
fn rows_past_the_end_have_no_label() {
let state = state_for("alpha\n", 80);
let total = state.parsed.total_visual_rows(80);
assert_eq!(state.source_line_at_visual_row(total, 80), None);
assert_eq!(state.source_line_at_visual_row(total + 50, 80), None);
}
}