markdown/edit.rs
1//! Editing a [`Doc`].
2//!
3//! This is the half of a block editor that has nothing to do with gpui: text
4//! goes in and out of a [`Text`], marks move with it, and blocks split, merge
5//! and indent. Keeping it pure is what makes it testable — the guarantee below
6//! is checked over generated edit sequences, not over the handful of cases
7//! anyone thinks to write down.
8//!
9//! **The guarantee is [`crate::serialize`]'s, preserved.** Call
10//! [`Doc::normalize`] and the document round-trips: serialize, parse, and
11//! nothing moves. An editor that can reach a state its own serializer cannot
12//! express is an editor that corrupts the file on save, and no amount of UI
13//! polish recovers from that.
14//!
15//! Normalizing is a *save* step rather than a keystroke step, and deliberately.
16//! Markdown cannot hold a space at the end of a line, but stripping one the
17//! moment it is typed takes it away mid-word — so the model carries it and
18//! sheds it on the way out, which is what every editor that writes markdown
19//! does.
20//!
21//! Marks are **left-sticky**: text typed at the end of a bold run is bold, text
22//! typed at its start is not. The caret inherits formatting from the character
23//! before it, which is what every editor does and what nobody notices until it
24//! is wrong.
25
26use std::ops::Range;
27
28mod shortcut;
29mod text;
30
31pub use shortcut::*;
32
33use crate::{
34 doc::{Block, BlockKind, Doc, Mark, MarkSpan, Part, Text},
35 select::{Cursor, Selection},
36};
37
38/// What a [`Doc::replace`] did, for anything holding a position it moved.
39///
40/// The caret is [`Doc::replace`]'s own answer and every caller wants it. The
41/// other two are for a caller keeping a position of its own — a comment anchor,
42/// a bookmark into the document — which has no other way to learn that the text
43/// under it shifted.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct Splice {
46 /// What the call covered, clamped and in document order.
47 pub removed: Selection,
48 /// Where the caret landed: the end of what went in.
49 pub caret: Cursor,
50 /// The change in block count, which every block after [`Self::removed`]
51 /// moves by.
52 pub blocks: isize,
53}
54
55impl Doc {
56 /// Split block `ix` at byte offset `at`, returning the new block's index.
57 ///
58 /// The tail keeps the block's kind where a continued Enter means "another of
59 /// these" — a list item, for example. A heading titles what follows. A
60 /// quote keeps its kind on whichever side still holds text: split at the
61 /// end of one and what opens below is plain body text, split at its start
62 /// and what opens above is.
63 pub fn split(&mut self, ix: usize, at: usize) -> usize {
64 if ix >= self.blocks.len() {
65 return ix;
66 }
67 let indent = self.blocks[ix].indent;
68 // Nothing to cut for a block with no body — Enter after an atomic
69 // block opens a paragraph.
70 let tail = self.blocks[ix]
71 .text_at_mut(Part::Body)
72 .map(|text| text.split_off(at))
73 .unwrap_or_default();
74 let kind = match &self.blocks[ix].kind {
75 BlockKind::Bullet(_) => BlockKind::Bullet(tail),
76 BlockKind::Ordered { .. } => BlockKind::Ordered {
77 number: 1,
78 text: tail,
79 },
80 BlockKind::Task { .. } => BlockKind::Task {
81 checked: false,
82 text: tail,
83 },
84 BlockKind::Quote { kind, .. } if !tail.text.is_empty() => BlockKind::Quote {
85 kind: *kind,
86 text: tail,
87 },
88 _ => BlockKind::Paragraph(tail),
89 };
90 // A quote that pushed all its text down keeps none of its own: an
91 // alert's marker written twice is two alerts, the first one empty.
92 if matches!(kind, BlockKind::Quote { .. })
93 && self.blocks[ix]
94 .text_at(Part::Body)
95 .is_none_or(|text| text.text.is_empty())
96 {
97 self.blocks[ix].kind = BlockKind::Paragraph(Text::default());
98 }
99 self.blocks.insert(ix + 1, Block::at(kind, indent));
100 self.repair();
101 ix + 1
102 }
103
104 /// Backspace at the start of a block.
105 ///
106 /// Notion's chain, in order: an indented block outdents, an image with
107 /// nothing written under it goes, a block wearing syntax around its text
108 /// gives the syntax up, and only a plain block at the left margin merges
109 /// into the one above it. When that one holds no body there is nothing to
110 /// merge into, so the caret steps into a fence or a table, and a rule —
111 /// which no caret can enter, and so no other key can remove — goes.
112 /// Returns where the caret landed, and `None` when nothing moved.
113 ///
114 /// A table cell is not a position that can swallow its neighbour, so
115 /// backspace at the start of one does nothing rather than eating the table.
116 pub fn merge_back(&mut self, at: Cursor) -> Option<Cursor> {
117 if matches!(at.part, Part::Cell { .. }) {
118 return None;
119 }
120 let block = self.blocks.get(at.block)?;
121 if block.indent > 0 {
122 self.outdent(at.block);
123 return Some(Cursor::new(at.block, at.part, 0));
124 }
125 // A caption is the only handle a caret has on an image, so with the
126 // caption empty there is nothing left to take but the picture.
127 if at.part == Part::Caption && block.text_at(Part::Caption)?.is_empty() {
128 let previous = at.block.checked_sub(1);
129 self.blocks.remove(at.block);
130 self.repair();
131 let Some(previous) = previous else {
132 return Some(Cursor::default().clamp(self));
133 };
134 let part = self.blocks[previous]
135 .parts()
136 .last()
137 .copied()
138 .unwrap_or_default();
139 return Some(Cursor::new(previous, part, 0).end(self));
140 }
141 // Every prefix [`shortcut`] reads is chrome around text; the first
142 // backspace takes the chrome and leaves the text where it was, so what
143 // can be typed in can be typed out.
144 let unwrapped = match &block.kind {
145 kind if is_marker(kind) => block.text_at(Part::Body).cloned(),
146 BlockKind::Heading { text, .. } | BlockKind::Quote { text, .. } => Some(text.clone()),
147 BlockKind::Code { code, .. } => Some(code.clone()),
148 _ => None,
149 };
150 if let Some(text) = unwrapped {
151 self.blocks[at.block].kind = BlockKind::Paragraph(text);
152 self.repair();
153 return Some(Cursor::new(at.block, Part::Body, 0));
154 }
155 if at.block == 0 {
156 return None;
157 }
158 let tail = self.blocks[at.block].text_at(Part::Body)?.clone();
159 let previous = at.block - 1;
160 match self.blocks[previous].parts().last().copied() {
161 // Only two blocks that both hold a body can become one.
162 Some(Part::Body) => {
163 let head = self.blocks[previous].text_at_mut(Part::Body)?;
164 let caret = head.text.len();
165 head.append(tail);
166 self.blocks.remove(at.block);
167 self.repair();
168 Some(Cursor::new(previous, Part::Body, caret))
169 }
170 Some(part) => {
171 let end = self.blocks[previous]
172 .text_at(part)
173 .map_or(0, |text| text.text.len());
174 // Stepping into a fence, a caption or a cell leaves this block
175 // where it is, which is right while it still holds something
176 // and a trap once it does not: nothing above it merges, so a
177 // block left empty here is one backspace can never reach again.
178 if tail.is_empty() {
179 self.blocks.remove(at.block);
180 self.repair();
181 }
182 Some(Cursor::new(previous, part, end))
183 }
184 None => {
185 self.blocks.remove(previous);
186 self.repair();
187 Some(Cursor::new(previous, at.part, at.offset))
188 }
189 }
190 }
191
192 /// Apply an edit to the text at `at`, then put the block back in order.
193 ///
194 /// The editor should reach text through here rather than mutating a block
195 /// directly: a heading or a table cell that acquires a newline has no
196 /// spelling, and nothing else is positioned to notice.
197 pub fn edit_at(&mut self, at: Cursor, edit: impl FnOnce(&mut Text)) {
198 let Some(block) = self.blocks.get_mut(at.block) else {
199 return;
200 };
201 let one_line = matches!(block.kind, BlockKind::Heading { .. })
202 || matches!(at.part, Part::Cell { .. } | Part::Caption);
203 let Some(text) = block.text_at_mut(at.part) else {
204 return;
205 };
206 edit(text);
207 if one_line {
208 crate::parse::collapse_to_one_line(text);
209 }
210 }
211
212 /// The blocks nested under `ix`, `ix` included — what a move, a duplicate
213 /// or a drag carries with it.
214 ///
215 /// A flat list makes this a scan for the next block that is not deeper,
216 /// which is the whole argument for the flat list.
217 pub fn subtree(&self, ix: usize) -> Range<usize> {
218 let Some(base) = self.blocks.get(ix).map(|block| block.indent) else {
219 return ix..ix;
220 };
221 let mut end = ix + 1;
222 while self
223 .blocks
224 .get(end)
225 .is_some_and(|block| block.indent > base)
226 {
227 end += 1;
228 }
229 ix..end
230 }
231
232 /// Move a block and its children to sit before or after their neighbour.
233 ///
234 /// `delta` counts *siblings*, not rows: moving down past a bullet with
235 /// three children clears all four, or a block would land inside the run it
236 /// was trying to step over.
237 pub fn move_block(&mut self, ix: usize, delta: isize) -> Option<usize> {
238 let span = self.subtree(ix);
239 if span.is_empty() {
240 return None;
241 }
242 let to = match delta {
243 ..0 => {
244 // The start of whichever subtree ends where this one begins.
245 (0..span.start)
246 .rev()
247 .find(|&above| self.subtree(above).end == span.start)?
248 }
249 0.. => {
250 let next = self.subtree(span.end);
251 if next.is_empty() {
252 return None;
253 }
254 // Landing after the neighbour means landing where it ends,
255 // less the hole this subtree leaves behind.
256 next.end - span.len()
257 }
258 };
259 let moved: Vec<Block> = self.blocks.drain(span.clone()).collect();
260 self.blocks.splice(to..to, moved);
261 self.repair();
262 Some(to)
263 }
264
265 /// Copy a block and its children in below themselves.
266 pub fn duplicate(&mut self, ix: usize) -> Option<usize> {
267 let span = self.subtree(ix);
268 if span.is_empty() {
269 return None;
270 }
271 let copy: Vec<Block> = self.blocks[span.clone()].to_vec();
272 self.blocks.splice(span.end..span.end, copy);
273 self.repair();
274 Some(span.end)
275 }
276
277 /// Delete a block and its children.
278 pub fn remove_block(&mut self, ix: usize) {
279 let span = self.subtree(ix);
280 if span.is_empty() {
281 return;
282 }
283 self.blocks.drain(span);
284 self.repair();
285 }
286
287 /// Turn block `ix` into `kind`, carrying its text across and keeping its
288 /// indent.
289 ///
290 /// The one operation a typed prefix, the slash menu and the block menu all
291 /// perform, so none of them reaches into a block's kind on its own.
292 pub fn set_kind(&mut self, ix: usize, kind: BlockKind) {
293 let Some(block) = self.blocks.get_mut(ix) else {
294 return;
295 };
296 let text = match &block.kind {
297 // A bookmark's text is the link it shows, so turning one back into
298 // prose hands the URL over instead of an empty block.
299 BlockKind::Bookmark { url, .. } => Text::link(url),
300 BlockKind::Image { alt, .. } => alt.clone(),
301 _ => block.text_at(Part::Body).cloned().unwrap_or_default(),
302 };
303 block.kind = kind;
304 match block.text_at_mut(Part::Body) {
305 Some(body) => *body = text,
306 // The two kinds whose text is not a body. Code is also the one the
307 // marks cannot come with.
308 None => match &mut block.kind {
309 BlockKind::Code { code, .. } => *code = Text::plain(text.text),
310 BlockKind::Image { alt, .. } => *alt = text,
311 _ => {}
312 },
313 }
314 self.repair();
315 }
316
317 /// The tag on a fenced block — what the label shows, what the highlighter
318 /// reads, and what the info string carries. Not [`Doc::set_kind`]'s job:
319 /// that carries a *body* across, and a fence has none to give back.
320 /// Put an empty row in table `ix` before `row`, numbered as
321 /// [`Part::Cell`] numbers rows (1 is the first body row), as wide as the
322 /// widest row. `false` when `ix` is not a table.
323 pub fn insert_row(&mut self, ix: usize, row: usize) -> bool {
324 let Some(BlockKind::Table { header, rows, .. }) =
325 self.blocks.get_mut(ix).map(|block| &mut block.kind)
326 else {
327 return false;
328 };
329 let width = rows
330 .iter()
331 .map(Vec::len)
332 .chain([header.len(), 1])
333 .max()
334 .unwrap_or(1);
335 let at = row.saturating_sub(1).min(rows.len());
336 rows.insert(at, vec![Text::default(); width]);
337 true
338 }
339
340 /// Put an empty, left-aligned column in table `ix` before `column`, in the
341 /// header and every row. `false` when `ix` is not a table.
342 pub fn insert_column(&mut self, ix: usize, column: usize) -> bool {
343 let Some(BlockKind::Table {
344 align,
345 header,
346 rows,
347 }) = self.blocks.get_mut(ix).map(|block| &mut block.kind)
348 else {
349 return false;
350 };
351 for cells in std::iter::once(&mut *header)
352 .filter(|header| !header.is_empty())
353 .chain(rows.iter_mut())
354 {
355 cells.insert(column.min(cells.len()), Text::default());
356 }
357 align.insert(column.min(align.len()), crate::Align::Left);
358 true
359 }
360
361 /// Take body row `row` ([`Part::Cell`] numbering) out of table `ix`.
362 /// `false` for the header row, a row that is not there, or a block that
363 /// is not a table.
364 pub fn remove_row(&mut self, ix: usize, row: usize) -> bool {
365 let Some(BlockKind::Table { rows, .. }) =
366 self.blocks.get_mut(ix).map(|block| &mut block.kind)
367 else {
368 return false;
369 };
370 if row == 0 || row > rows.len() {
371 return false;
372 }
373 rows.remove(row - 1);
374 true
375 }
376
377 /// Take `column` out of table `ix`, from the header and every row.
378 /// `false` for the last column left, or a block that is not a table.
379 pub fn remove_column(&mut self, ix: usize, column: usize) -> bool {
380 let Some(BlockKind::Table {
381 align,
382 header,
383 rows,
384 }) = self.blocks.get_mut(ix).map(|block| &mut block.kind)
385 else {
386 return false;
387 };
388 let width = rows
389 .iter()
390 .map(Vec::len)
391 .chain([header.len()])
392 .max()
393 .unwrap_or(0);
394 if width < 2 || column >= width {
395 return false;
396 }
397 for cells in std::iter::once(&mut *header).chain(rows.iter_mut()) {
398 if column < cells.len() {
399 cells.remove(column);
400 }
401 }
402 if column < align.len() {
403 align.remove(column);
404 }
405 true
406 }
407
408 pub fn set_language(&mut self, ix: usize, language: Option<String>) {
409 if let Some(BlockKind::Code { language: tag, .. }) =
410 self.blocks.get_mut(ix).map(|block| &mut block.kind)
411 {
412 *tag = language;
413 }
414 }
415
416 /// Turn what a selection covers into one code block, leaving whatever it
417 /// did not cover as blocks of its own.
418 ///
419 /// The fence is what markdown has for code over more than one line. An
420 /// inline span is not: no CommonMark spelling puts a line break inside
421 /// backticks, so one written that way comes back as a space.
422 ///
423 /// Marks are dropped on the way in, the way [`Doc::set_kind`] drops them
424 /// when it turns a block into a fence — code is literal to its closing
425 /// fence, and nothing in it is markup.
426 pub fn fence(&mut self, selection: Selection) -> Cursor {
427 let lines: Vec<String> = self
428 .spans(selection)
429 .iter()
430 .filter(|(at, _)| at.part == Part::Body)
431 .filter_map(|(at, range)| {
432 let text = self.blocks[at.block].text_at(at.part)?;
433 text.text.get(range.clone()).map(str::to_string)
434 })
435 .collect();
436 if lines.is_empty() {
437 return selection.head.clamp(self);
438 }
439 let code = Text::plain(lines.join("\n"));
440
441 // Cutting the selection leaves the head and the tail it did not cover
442 // joined in one block, with the caret at the seam between them — which
443 // is where the fence goes.
444 let at = self.replace(selection, Text::default()).caret;
445 let tail = self.split(at.block, at.offset);
446 let indent = self.blocks[at.block].indent;
447 self.blocks.insert(
448 tail,
449 Block::at(
450 BlockKind::Code {
451 language: None,
452 code,
453 },
454 indent,
455 ),
456 );
457 // A selection that covered whole blocks leaves nothing on either side,
458 // and an empty paragraph is not what "turn this into code" asked for.
459 let empty = |block: &Block| {
460 block
461 .text_at(Part::Body)
462 .is_some_and(|text| text.text.is_empty())
463 };
464 if self.blocks.get(tail + 1).is_some_and(empty) {
465 self.blocks.remove(tail + 1);
466 }
467 let mut fence = tail;
468 if empty(&self.blocks[at.block]) {
469 self.blocks.remove(at.block);
470 fence -= 1;
471 }
472 self.repair();
473 Cursor::new(fence, Part::Code, 0).clamp(self)
474 }
475
476 /// The way back out of a fence: every line becomes a paragraph. `None` when
477 /// the selection is not all code, which is what makes this the other half
478 /// of a toggle rather than an operation of its own.
479 pub fn unfence(&mut self, selection: Selection) -> Option<Cursor> {
480 let (start, end) = selection.clamp(self).ordered();
481 let blocks = start.block..=end.block;
482 if !blocks
483 .clone()
484 .all(|ix| matches!(self.blocks[ix].kind, BlockKind::Code { .. }))
485 {
486 return None;
487 }
488 for ix in blocks.rev() {
489 let BlockKind::Code { code, .. } = &self.blocks[ix].kind else {
490 continue;
491 };
492 let indent = self.blocks[ix].indent;
493 let paragraphs: Vec<Block> = code
494 .text
495 .split('\n')
496 .map(|line| Block::at(BlockKind::Paragraph(Text::plain(line)), indent))
497 .collect();
498 self.blocks.splice(ix..=ix, paragraphs);
499 }
500 self.repair();
501 Some(Cursor::new(start.block, Part::Body, 0).clamp(self))
502 }
503
504 /// Every text a selection touches, with the slice of it covered.
505 ///
506 /// One selection can reach across paragraphs and table cells, and a mark
507 /// applies to each of them separately — marks live inside a [`Text`] and
508 /// have no way to span two.
509 pub fn spans(&self, selection: Selection) -> Vec<(Cursor, Range<usize>)> {
510 let (start, end) = selection.clamp(self).ordered();
511 let (first, last) = (
512 Cursor::new(start.block, start.part, 0),
513 Cursor::new(end.block, end.part, 0),
514 );
515 let mut out = Vec::new();
516 for block in start.block..=end.block.min(self.blocks.len().saturating_sub(1)) {
517 for part in self.blocks[block].parts() {
518 let here = Cursor::new(block, part, 0);
519 if here < first || here > last {
520 continue;
521 }
522 let len = here.len_in(self).unwrap_or(0);
523 let from = if here == first { start.offset } else { 0 };
524 let to = if here == last { end.offset } else { len };
525 if from < to.min(len) {
526 out.push((here, from..to.min(len)));
527 }
528 }
529 }
530 out
531 }
532
533 /// Add `mark` over a selection, or take it away if every part of the
534 /// selection already carries it.
535 ///
536 /// The decision is made across the whole selection before anything moves:
537 /// dragging over a bold word and a plain one and pressing cmd-B should bold
538 /// the rest rather than unbolding the half that was already there.
539 pub fn toggle_mark(&mut self, selection: Selection, mark: Mark) {
540 let spans = self.spans(selection);
541 let remove = self.covered_by(selection, &mark);
542
543 for (at, range) in spans {
544 // Code is literal to its closing fence and a caption has no room
545 // for markup between its brackets; nothing in either is markup.
546 if matches!(at.part, Part::Code | Part::Caption) {
547 continue;
548 }
549 if remove == self.carries(&at, &range, &mark) {
550 let mark = mark.clone();
551 self.edit_at(at, |text| text.toggle(range, mark));
552 }
553 }
554 }
555
556 /// The marks a selection carries throughout — what a toolbar paints as lit.
557 ///
558 /// Collapsed, it answers with the marks the next character typed here would
559 /// join, which is the **left-sticky** rule [`Text::insert`] already
560 /// follows: the run ending at the caret, never the one starting there.
561 pub fn marks(&self, selection: Selection) -> Vec<Mark> {
562 let mut marks: Vec<Mark> = Vec::new();
563 if selection.is_collapsed() {
564 let at = selection.head.clamp(self);
565 let Some(text) = self.blocks.get(at.block).and_then(|b| b.text_at(at.part)) else {
566 return marks;
567 };
568 for span in &text.marks {
569 if span.range.start < at.offset && at.offset <= span.range.end {
570 marks.push(span.mark.clone());
571 }
572 }
573 marks.dedup();
574 return marks;
575 }
576 for (at, range) in self.spans(selection) {
577 let Some(text) = self.blocks[at.block].text_at(at.part) else {
578 continue;
579 };
580 for span in &text.marks {
581 if span.range.start < range.end
582 && span.range.end > range.start
583 && !marks.contains(&span.mark)
584 {
585 marks.push(span.mark.clone());
586 }
587 }
588 }
589 marks.retain(|mark| self.covered_by(selection, mark));
590 marks
591 }
592
593 /// Whether every part of a selection already carries `mark` — what decides
594 /// between adding it and taking it away, and what a toolbar button reads to
595 /// know whether it is lit.
596 pub fn covered_by(&self, selection: Selection, mark: &Mark) -> bool {
597 let spans = self.spans(selection);
598 !spans.is_empty()
599 && spans.iter().all(|(at, range)| {
600 matches!(at.part, Part::Code | Part::Caption) || self.carries(at, range, mark)
601 })
602 }
603
604 fn carries(&self, at: &Cursor, range: &Range<usize>, mark: &Mark) -> bool {
605 self.blocks[at.block]
606 .text_at(at.part)
607 .is_some_and(|text| text.covered_by(range, mark))
608 }
609
610 /// The sub-document a selection covers — what a copy puts on the clipboard.
611 ///
612 /// A table is atomic here for the same reason it is in [`Doc::replace`]:
613 /// half a table has no shape worth keeping, so a selection reaching into
614 /// one takes it whole.
615 pub fn slice(&self, selection: Selection) -> Doc {
616 let (start, end) = selection.clamp(self).ordered();
617 let mut out = Doc {
618 blocks: self.blocks[start.block..=end.block].to_vec(),
619 };
620 let last = end.block - start.block;
621 // Tail first: trimming the head would move the offsets the tail is in.
622 if !matches!(end.part, Part::Cell { .. })
623 && let Some(text) = out.blocks[last].text_at_mut(end.part)
624 {
625 text.split_off(end.offset);
626 }
627 if !matches!(start.part, Part::Cell { .. })
628 && let Some(text) = out.blocks[0].text_at_mut(start.part)
629 {
630 *text = text.split_off(start.offset);
631 }
632 // The slice starts at the left margin whatever depth it was cut from.
633 out.repair();
634 out
635 }
636
637 /// Replace a selection with a whole document — the paste path.
638 ///
639 /// A lone paragraph goes in as inline text, marks and all: pasting a
640 /// sentence into a sentence must not make a new block. Anything else
641 /// arrives as blocks, and the remainder of the caret's block follows them.
642 pub fn splice(&mut self, selection: Selection, other: Doc) -> Cursor {
643 let blocks = other.blocks;
644 let inline = match blocks.as_slice() {
645 [] => Some(Text::default()),
646 [block] => match &block.kind {
647 BlockKind::Paragraph(text) => Some(text.clone()),
648 _ => None,
649 },
650 _ => None,
651 };
652 if let Some(text) = inline {
653 return self.replace(selection, text).caret;
654 }
655
656 let caret = self.replace(selection, Text::default()).caret;
657 let base = self.blocks[caret.block].indent;
658 // Split so what followed the caret follows the paste too. An empty
659 // remainder is the blank block a paste at the end would leave behind.
660 let tail = self.split(caret.block, caret.offset);
661 let empty_tail = self.blocks[tail]
662 .text_at(Part::Body)
663 .is_some_and(Text::is_empty);
664
665 let mut at = caret.block;
666 for block in blocks {
667 at += 1;
668 self.blocks
669 .insert(at, Block::at(block.kind, base.saturating_add(block.indent)));
670 }
671 if empty_tail {
672 self.blocks.remove(at + 1);
673 }
674 // And the block the caret opened in, if the paste displaced all of it.
675 let head_empty = self.blocks[caret.block]
676 .text_at(Part::Body)
677 .is_some_and(Text::is_empty);
678 if head_empty && matches!(self.blocks[caret.block].kind, BlockKind::Paragraph(_)) {
679 self.blocks.remove(caret.block);
680 at -= 1;
681 }
682 self.repair();
683 Cursor::new(at, Part::Body, 0).end(self).clamp(self)
684 }
685
686 /// Replace everything a selection covers with `text`, and say where the
687 /// caret lands.
688 ///
689 /// **The one mutation.** Typing, backspace, delete, cut and paste are all
690 /// this call with a different argument, which is why none of them needs to
691 /// know whether a selection was empty, spanned two paragraphs, or swallowed
692 /// a table on the way past.
693 pub fn replace(&mut self, selection: Selection, text: Text) -> Splice {
694 // An empty document has no block to put anything in; editing one opens
695 // the paragraph every other path then assumes exists.
696 if self.blocks.is_empty() {
697 self.blocks
698 .push(Block::new(BlockKind::Paragraph(Text::default())));
699 }
700 // Counted after that, so the block a nothing-document opens with is not
701 // a shift anything downstream has to hear about.
702 let before = self.blocks.len();
703 let (start, end) = selection.clamp(self).ordered();
704 let removed = Selection::new(start, end);
705
706 // Code is literal and a caption is written between brackets, so marks
707 // arriving from a paste have nowhere to go in either.
708 let text = if matches!(start.part, Part::Code | Part::Caption) {
709 Text::plain(text.text)
710 } else {
711 text
712 };
713
714 if start.block == end.block && start.part == end.part {
715 let at = start.offset + text.text.len();
716 self.edit_at(start, |body| {
717 body.remove(start.offset..end.offset);
718 body.insert(start.offset, &text.text);
719 for span in &text.marks {
720 body.marks.push(MarkSpan {
721 range: start.offset + span.range.start..start.offset + span.range.end,
722 mark: span.mark.clone(),
723 });
724 }
725 body.normalize_marks();
726 });
727 return Splice {
728 removed,
729 caret: Cursor {
730 offset: at,
731 ..start
732 }
733 .clamp(self),
734 blocks: 0,
735 };
736 }
737
738 // Across cells of one table the table itself survives: the covered
739 // cells are emptied and the shape stays, which is what a spreadsheet
740 // selection does and what keeps the columns from collapsing.
741 if start.block == end.block {
742 for part in self.blocks[start.block].parts() {
743 if part < start.part || part > end.part {
744 continue;
745 }
746 // `remove` clamps, so the open end needs no length.
747 let (from, to) = (
748 if part == start.part { start.offset } else { 0 },
749 if part == end.part {
750 end.offset
751 } else {
752 usize::MAX
753 },
754 );
755 self.edit_at(Cursor::new(start.block, part, 0), |body| {
756 body.remove(from..to)
757 });
758 }
759 // The recursion is the insert alone, so what it covered is not what
760 // this call covered — only the caret comes back out of it.
761 let caret = self.replace(Selection::at(start), text).caret;
762 return Splice {
763 removed,
764 caret,
765 blocks: self.blocks.len() as isize - before as isize,
766 };
767 }
768
769 // Across blocks the head keeps its kind and takes the tail's
770 // remainder, and everything between them goes.
771 //
772 // A **table is atomic** to a selection that leaves it. Half a table has
773 // no shape worth keeping, so an end landing in one takes the whole
774 // block rather than splicing a lone cell into a paragraph.
775 let head_keeps = !matches!(start.part, Part::Cell { .. })
776 && self.blocks[start.block].text_at(start.part).is_some();
777 let tail = match end.part {
778 Part::Cell { .. } => Text::default(),
779 part => self.blocks[end.block]
780 .text_at_mut(part)
781 .map(|body| body.split_off(end.offset))
782 .unwrap_or_default(),
783 };
784
785 let indent = self.blocks[start.block].indent;
786 let first = if head_keeps {
787 start.block + 1
788 } else {
789 start.block
790 };
791 self.blocks.drain(first..=end.block);
792
793 let caret = if head_keeps {
794 self.edit_at(start, |body| body.remove(start.offset..usize::MAX));
795 self.edit_at(start, |body| body.append(tail));
796 start
797 } else {
798 // Everything the selection touched is gone, so the tail arrives as
799 // a paragraph in its place.
800 self.blocks
801 .insert(start.block, Block::at(BlockKind::Paragraph(tail), indent));
802 Cursor::new(start.block, Part::Body, 0)
803 };
804 self.repair();
805 let caret = caret.clamp(self);
806 let caret = self.replace(Selection::at(caret), text).caret;
807 Splice {
808 removed,
809 caret,
810 blocks: self.blocks.len() as isize - before as isize,
811 }
812 }
813
814 /// Put the document into the form markdown can hold — the save step.
815 ///
816 /// Drops the whitespace markdown discards anyway (leading and trailing on
817 /// every line, blank lines at a block's edges), flattens the blocks whose
818 /// output is one line, and renumbers ordered runs. After this,
819 /// `parse(serialize(doc)) == doc`.
820 pub fn normalize(&mut self) {
821 self.normalize_with(&crate::Marks::default());
822 }
823
824 /// [`Doc::normalize`] with the app's own marks — see [`crate::Marks`].
825 pub fn normalize_with(&mut self, marks: &crate::Marks) {
826 for block in &mut self.blocks {
827 let one_line = matches!(block.kind, BlockKind::Heading { .. });
828 match &mut block.kind {
829 BlockKind::Paragraph(text)
830 | BlockKind::Heading { text, .. }
831 | BlockKind::Bullet(text)
832 | BlockKind::Ordered { text, .. }
833 | BlockKind::Task { text, .. }
834 | BlockKind::Quote { text, .. } => {
835 *text = crate::parse::normalize(&text.text, &text.marks);
836 text.normalize_marks();
837 if one_line {
838 crate::parse::collapse_to_one_line(text);
839 }
840 }
841 BlockKind::Table { header, rows, .. } => {
842 for cell in header.iter_mut().chain(rows.iter_mut().flatten()) {
843 *cell = crate::parse::normalize(&cell.text, &cell.marks);
844 cell.normalize_marks();
845 crate::parse::collapse_to_one_line(cell);
846 }
847 }
848 // A caption lives between brackets, where a line break has no
849 // spelling at all.
850 BlockKind::Image { alt, .. } => crate::parse::collapse_to_one_line(alt),
851 BlockKind::Code { .. } | BlockKind::Bookmark { .. } | BlockKind::Rule => {}
852 }
853 }
854 // A blank paragraph is the empty line an editor leaves behind, and
855 // markdown has no way to write one down — blank lines there separate
856 // blocks rather than being one. An empty heading or list item is
857 // different: `# ` and `- ` are both real, so those stay. So is an
858 // alert with no body: `> [!TIP]` writes down and reads back.
859 self.blocks.retain(|block| {
860 !matches!(
861 &block.kind,
862 BlockKind::Paragraph(text) | BlockKind::Quote { kind: None, text } if text.is_empty()
863 )
864 });
865 self.repair();
866
867 // The rules above keep every ordinary edit lossless. They cannot be
868 // complete, and no serializer fix would make them so: whether a mark
869 // boundary can be written depends on CommonMark's flanking rules, and
870 // some marks have no spelling at all. Bold ending on a `~` with a letter
871 // after it is one — a closing delimiter preceded by punctuation and
872 // followed by a letter is not right-flanking, so `Tit**l\~\~**e` does
873 // not close. That is a limit of the format, not a bug in the writer.
874 //
875 // So the last word goes to markdown: adopt the document it can hold.
876 //
877 // This is exact rather than approximate. Anything [`crate::parse`]
878 // returns is a fixed point of the round trip — that is the guarantee the
879 // parser is tested for — so writing this document out and reading it
880 // back yields one by construction. Marks with no spelling are dropped
881 // here, in front of the reader, rather than silently at save time.
882 //
883 // The cheaper rules above still earn their place: they are what keeps
884 // the ordinary edit lossless, so this step has nothing left to take.
885 *self = crate::parse_with(&crate::serialize_with(self, marks), marks);
886 }
887
888 /// Tab. A block can go one level deeper than the one above it, and its
889 /// children come with it.
890 pub fn indent(&mut self, ix: usize) -> bool {
891 let Some(block) = self.blocks.get(ix) else {
892 return false;
893 };
894 if block.indent >= self.ceiling(ix) {
895 return false;
896 }
897 self.shift_subtree(ix, 1);
898 // A run that has just been nested has nothing above it to carry on
899 // from, so it starts over. `renumber` cannot decide this on its own: a
900 // list written `5.` keeps its 5, and from the numbers alone the two
901 // cases look the same.
902 if self.begins_run(ix)
903 && let BlockKind::Ordered { number, .. } = &mut self.blocks[ix].kind
904 {
905 *number = 1;
906 }
907 self.repair();
908 true
909 }
910
911 /// Whether the block at `ix` starts a run of ordered items rather than
912 /// carrying one on: the nearest block at its own indent, before the list
913 /// it sits in ends, is not an ordered item.
914 fn begins_run(&self, ix: usize) -> bool {
915 let indent = self.blocks[ix].indent;
916 self.blocks[..ix]
917 .iter()
918 .rev()
919 .take_while(|block| block.indent >= indent)
920 .find(|block| block.indent == indent)
921 .is_none_or(|block| !matches!(block.kind, BlockKind::Ordered { .. }))
922 }
923
924 /// How deep block `ix` is allowed to sit.
925 ///
926 /// Markdown expresses nesting through list items and nothing else, so a
927 /// block may only go deeper than the one above it when that one is a
928 /// marker. Indenting a paragraph under a *heading* would serialize to four
929 /// leading spaces, which reads back as an indented code block.
930 pub fn ceiling(&self, ix: usize) -> u8 {
931 match ix.checked_sub(1).map(|previous| &self.blocks[previous]) {
932 None => 0,
933 Some(previous) if is_marker(&previous.kind) => previous.indent + 1,
934 Some(previous) => previous.indent,
935 }
936 }
937
938 /// Clamp every indent to what the document can actually express, then make
939 /// ordered runs consecutive. Cheap, total, and called after anything
940 /// structural — a local rule is not enough, because outdenting one block
941 /// can leave the block *after* it stranded a level too deep.
942 ///
943 /// Public because an editor that changes a block's *kind* has to restore
944 /// the invariant too, and only this knows what it is.
945 pub fn repair(&mut self) {
946 for ix in 0..self.blocks.len() {
947 let ceiling = self.ceiling(ix);
948 self.blocks[ix].indent = self.blocks[ix].indent.min(ceiling);
949 }
950 self.renumber();
951 }
952
953 /// Shift-Tab, children included.
954 pub fn outdent(&mut self, ix: usize) -> bool {
955 if self.blocks.get(ix).is_none_or(|block| block.indent == 0) {
956 return false;
957 }
958 self.shift_subtree(ix, -1);
959 self.repair();
960 true
961 }
962
963 /// Move a block and everything nested under it. Children have to travel
964 /// with the parent or the document invariant breaks the moment a level
965 /// disappears from under them.
966 fn shift_subtree(&mut self, ix: usize, by: i8) {
967 let span = self.subtree(ix);
968 for block in &mut self.blocks[span] {
969 block.indent = block.indent.saturating_add_signed(by);
970 }
971 }
972}
973
974fn is_marker(kind: &BlockKind) -> bool {
975 matches!(
976 kind,
977 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
978 )
979}