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 pub fn set_language(&mut self, ix: usize, language: Option<String>) {
321 if let Some(BlockKind::Code { language: tag, .. }) =
322 self.blocks.get_mut(ix).map(|block| &mut block.kind)
323 {
324 *tag = language;
325 }
326 }
327
328 /// Turn what a selection covers into one code block, leaving whatever it
329 /// did not cover as blocks of its own.
330 ///
331 /// The fence is what markdown has for code over more than one line. An
332 /// inline span is not: no CommonMark spelling puts a line break inside
333 /// backticks, so one written that way comes back as a space.
334 ///
335 /// Marks are dropped on the way in, the way [`Doc::set_kind`] drops them
336 /// when it turns a block into a fence — code is literal to its closing
337 /// fence, and nothing in it is markup.
338 pub fn fence(&mut self, selection: Selection) -> Cursor {
339 let lines: Vec<String> = self
340 .spans(selection)
341 .iter()
342 .filter(|(at, _)| at.part == Part::Body)
343 .filter_map(|(at, range)| {
344 let text = self.blocks[at.block].text_at(at.part)?;
345 text.text.get(range.clone()).map(str::to_string)
346 })
347 .collect();
348 if lines.is_empty() {
349 return selection.head.clamp(self);
350 }
351 let code = Text::plain(lines.join("\n"));
352
353 // Cutting the selection leaves the head and the tail it did not cover
354 // joined in one block, with the caret at the seam between them — which
355 // is where the fence goes.
356 let at = self.replace(selection, Text::default()).caret;
357 let tail = self.split(at.block, at.offset);
358 let indent = self.blocks[at.block].indent;
359 self.blocks.insert(
360 tail,
361 Block::at(
362 BlockKind::Code {
363 language: None,
364 code,
365 },
366 indent,
367 ),
368 );
369 // A selection that covered whole blocks leaves nothing on either side,
370 // and an empty paragraph is not what "turn this into code" asked for.
371 let empty = |block: &Block| {
372 block
373 .text_at(Part::Body)
374 .is_some_and(|text| text.text.is_empty())
375 };
376 if self.blocks.get(tail + 1).is_some_and(empty) {
377 self.blocks.remove(tail + 1);
378 }
379 let mut fence = tail;
380 if empty(&self.blocks[at.block]) {
381 self.blocks.remove(at.block);
382 fence -= 1;
383 }
384 self.repair();
385 Cursor::new(fence, Part::Code, 0).clamp(self)
386 }
387
388 /// The way back out of a fence: every line becomes a paragraph. `None` when
389 /// the selection is not all code, which is what makes this the other half
390 /// of a toggle rather than an operation of its own.
391 pub fn unfence(&mut self, selection: Selection) -> Option<Cursor> {
392 let (start, end) = selection.clamp(self).ordered();
393 let blocks = start.block..=end.block;
394 if !blocks
395 .clone()
396 .all(|ix| matches!(self.blocks[ix].kind, BlockKind::Code { .. }))
397 {
398 return None;
399 }
400 for ix in blocks.rev() {
401 let BlockKind::Code { code, .. } = &self.blocks[ix].kind else {
402 continue;
403 };
404 let indent = self.blocks[ix].indent;
405 let paragraphs: Vec<Block> = code
406 .text
407 .split('\n')
408 .map(|line| Block::at(BlockKind::Paragraph(Text::plain(line)), indent))
409 .collect();
410 self.blocks.splice(ix..=ix, paragraphs);
411 }
412 self.repair();
413 Some(Cursor::new(start.block, Part::Body, 0).clamp(self))
414 }
415
416 /// Every text a selection touches, with the slice of it covered.
417 ///
418 /// One selection can reach across paragraphs and table cells, and a mark
419 /// applies to each of them separately — marks live inside a [`Text`] and
420 /// have no way to span two.
421 pub fn spans(&self, selection: Selection) -> Vec<(Cursor, Range<usize>)> {
422 let (start, end) = selection.clamp(self).ordered();
423 let (first, last) = (
424 Cursor::new(start.block, start.part, 0),
425 Cursor::new(end.block, end.part, 0),
426 );
427 let mut out = Vec::new();
428 for block in start.block..=end.block.min(self.blocks.len().saturating_sub(1)) {
429 for part in self.blocks[block].parts() {
430 let here = Cursor::new(block, part, 0);
431 if here < first || here > last {
432 continue;
433 }
434 let len = here.len_in(self).unwrap_or(0);
435 let from = if here == first { start.offset } else { 0 };
436 let to = if here == last { end.offset } else { len };
437 if from < to.min(len) {
438 out.push((here, from..to.min(len)));
439 }
440 }
441 }
442 out
443 }
444
445 /// Add `mark` over a selection, or take it away if every part of the
446 /// selection already carries it.
447 ///
448 /// The decision is made across the whole selection before anything moves:
449 /// dragging over a bold word and a plain one and pressing cmd-B should bold
450 /// the rest rather than unbolding the half that was already there.
451 pub fn toggle_mark(&mut self, selection: Selection, mark: Mark) {
452 let spans = self.spans(selection);
453 let remove = self.covered_by(selection, &mark);
454
455 for (at, range) in spans {
456 // Code is literal to its closing fence and a caption has no room
457 // for markup between its brackets; nothing in either is markup.
458 if matches!(at.part, Part::Code | Part::Caption) {
459 continue;
460 }
461 if remove == self.carries(&at, &range, &mark) {
462 let mark = mark.clone();
463 self.edit_at(at, |text| text.toggle(range, mark));
464 }
465 }
466 }
467
468 /// The marks a selection carries throughout — what a toolbar paints as lit.
469 ///
470 /// Collapsed, it answers with the marks the next character typed here would
471 /// join, which is the **left-sticky** rule [`Text::insert`] already
472 /// follows: the run ending at the caret, never the one starting there.
473 pub fn marks(&self, selection: Selection) -> Vec<Mark> {
474 let mut marks: Vec<Mark> = Vec::new();
475 if selection.is_collapsed() {
476 let at = selection.head.clamp(self);
477 let Some(text) = self.blocks.get(at.block).and_then(|b| b.text_at(at.part)) else {
478 return marks;
479 };
480 for span in &text.marks {
481 if span.range.start < at.offset && at.offset <= span.range.end {
482 marks.push(span.mark.clone());
483 }
484 }
485 marks.dedup();
486 return marks;
487 }
488 for (at, range) in self.spans(selection) {
489 let Some(text) = self.blocks[at.block].text_at(at.part) else {
490 continue;
491 };
492 for span in &text.marks {
493 if span.range.start < range.end
494 && span.range.end > range.start
495 && !marks.contains(&span.mark)
496 {
497 marks.push(span.mark.clone());
498 }
499 }
500 }
501 marks.retain(|mark| self.covered_by(selection, mark));
502 marks
503 }
504
505 /// Whether every part of a selection already carries `mark` — what decides
506 /// between adding it and taking it away, and what a toolbar button reads to
507 /// know whether it is lit.
508 pub fn covered_by(&self, selection: Selection, mark: &Mark) -> bool {
509 let spans = self.spans(selection);
510 !spans.is_empty()
511 && spans.iter().all(|(at, range)| {
512 matches!(at.part, Part::Code | Part::Caption) || self.carries(at, range, mark)
513 })
514 }
515
516 fn carries(&self, at: &Cursor, range: &Range<usize>, mark: &Mark) -> bool {
517 self.blocks[at.block]
518 .text_at(at.part)
519 .is_some_and(|text| text.covered_by(range, mark))
520 }
521
522 /// The sub-document a selection covers — what a copy puts on the clipboard.
523 ///
524 /// A table is atomic here for the same reason it is in [`Doc::replace`]:
525 /// half a table has no shape worth keeping, so a selection reaching into
526 /// one takes it whole.
527 pub fn slice(&self, selection: Selection) -> Doc {
528 let (start, end) = selection.clamp(self).ordered();
529 let mut out = Doc {
530 blocks: self.blocks[start.block..=end.block].to_vec(),
531 };
532 let last = end.block - start.block;
533 // Tail first: trimming the head would move the offsets the tail is in.
534 if !matches!(end.part, Part::Cell { .. })
535 && let Some(text) = out.blocks[last].text_at_mut(end.part)
536 {
537 text.split_off(end.offset);
538 }
539 if !matches!(start.part, Part::Cell { .. })
540 && let Some(text) = out.blocks[0].text_at_mut(start.part)
541 {
542 *text = text.split_off(start.offset);
543 }
544 // The slice starts at the left margin whatever depth it was cut from.
545 out.repair();
546 out
547 }
548
549 /// Replace a selection with a whole document — the paste path.
550 ///
551 /// A lone paragraph goes in as inline text, marks and all: pasting a
552 /// sentence into a sentence must not make a new block. Anything else
553 /// arrives as blocks, and the remainder of the caret's block follows them.
554 pub fn splice(&mut self, selection: Selection, other: Doc) -> Cursor {
555 let blocks = other.blocks;
556 let inline = match blocks.as_slice() {
557 [] => Some(Text::default()),
558 [block] => match &block.kind {
559 BlockKind::Paragraph(text) => Some(text.clone()),
560 _ => None,
561 },
562 _ => None,
563 };
564 if let Some(text) = inline {
565 return self.replace(selection, text).caret;
566 }
567
568 let caret = self.replace(selection, Text::default()).caret;
569 let base = self.blocks[caret.block].indent;
570 // Split so what followed the caret follows the paste too. An empty
571 // remainder is the blank block a paste at the end would leave behind.
572 let tail = self.split(caret.block, caret.offset);
573 let empty_tail = self.blocks[tail]
574 .text_at(Part::Body)
575 .is_some_and(Text::is_empty);
576
577 let mut at = caret.block;
578 for block in blocks {
579 at += 1;
580 self.blocks
581 .insert(at, Block::at(block.kind, base.saturating_add(block.indent)));
582 }
583 if empty_tail {
584 self.blocks.remove(at + 1);
585 }
586 // And the block the caret opened in, if the paste displaced all of it.
587 let head_empty = self.blocks[caret.block]
588 .text_at(Part::Body)
589 .is_some_and(Text::is_empty);
590 if head_empty && matches!(self.blocks[caret.block].kind, BlockKind::Paragraph(_)) {
591 self.blocks.remove(caret.block);
592 at -= 1;
593 }
594 self.repair();
595 Cursor::new(at, Part::Body, 0).end(self).clamp(self)
596 }
597
598 /// Replace everything a selection covers with `text`, and say where the
599 /// caret lands.
600 ///
601 /// **The one mutation.** Typing, backspace, delete, cut and paste are all
602 /// this call with a different argument, which is why none of them needs to
603 /// know whether a selection was empty, spanned two paragraphs, or swallowed
604 /// a table on the way past.
605 pub fn replace(&mut self, selection: Selection, text: Text) -> Splice {
606 // An empty document has no block to put anything in; editing one opens
607 // the paragraph every other path then assumes exists.
608 if self.blocks.is_empty() {
609 self.blocks
610 .push(Block::new(BlockKind::Paragraph(Text::default())));
611 }
612 // Counted after that, so the block a nothing-document opens with is not
613 // a shift anything downstream has to hear about.
614 let before = self.blocks.len();
615 let (start, end) = selection.clamp(self).ordered();
616 let removed = Selection::new(start, end);
617
618 // Code is literal and a caption is written between brackets, so marks
619 // arriving from a paste have nowhere to go in either.
620 let text = if matches!(start.part, Part::Code | Part::Caption) {
621 Text::plain(text.text)
622 } else {
623 text
624 };
625
626 if start.block == end.block && start.part == end.part {
627 let at = start.offset + text.text.len();
628 self.edit_at(start, |body| {
629 body.remove(start.offset..end.offset);
630 body.insert(start.offset, &text.text);
631 for span in &text.marks {
632 body.marks.push(MarkSpan {
633 range: start.offset + span.range.start..start.offset + span.range.end,
634 mark: span.mark.clone(),
635 });
636 }
637 body.normalize_marks();
638 });
639 return Splice {
640 removed,
641 caret: Cursor {
642 offset: at,
643 ..start
644 }
645 .clamp(self),
646 blocks: 0,
647 };
648 }
649
650 // Across cells of one table the table itself survives: the covered
651 // cells are emptied and the shape stays, which is what a spreadsheet
652 // selection does and what keeps the columns from collapsing.
653 if start.block == end.block {
654 for part in self.blocks[start.block].parts() {
655 if part < start.part || part > end.part {
656 continue;
657 }
658 // `remove` clamps, so the open end needs no length.
659 let (from, to) = (
660 if part == start.part { start.offset } else { 0 },
661 if part == end.part {
662 end.offset
663 } else {
664 usize::MAX
665 },
666 );
667 self.edit_at(Cursor::new(start.block, part, 0), |body| {
668 body.remove(from..to)
669 });
670 }
671 // The recursion is the insert alone, so what it covered is not what
672 // this call covered — only the caret comes back out of it.
673 let caret = self.replace(Selection::at(start), text).caret;
674 return Splice {
675 removed,
676 caret,
677 blocks: self.blocks.len() as isize - before as isize,
678 };
679 }
680
681 // Across blocks the head keeps its kind and takes the tail's
682 // remainder, and everything between them goes.
683 //
684 // A **table is atomic** to a selection that leaves it. Half a table has
685 // no shape worth keeping, so an end landing in one takes the whole
686 // block rather than splicing a lone cell into a paragraph.
687 let head_keeps = !matches!(start.part, Part::Cell { .. })
688 && self.blocks[start.block].text_at(start.part).is_some();
689 let tail = match end.part {
690 Part::Cell { .. } => Text::default(),
691 part => self.blocks[end.block]
692 .text_at_mut(part)
693 .map(|body| body.split_off(end.offset))
694 .unwrap_or_default(),
695 };
696
697 let indent = self.blocks[start.block].indent;
698 let first = if head_keeps {
699 start.block + 1
700 } else {
701 start.block
702 };
703 self.blocks.drain(first..=end.block);
704
705 let caret = if head_keeps {
706 self.edit_at(start, |body| body.remove(start.offset..usize::MAX));
707 self.edit_at(start, |body| body.append(tail));
708 start
709 } else {
710 // Everything the selection touched is gone, so the tail arrives as
711 // a paragraph in its place.
712 self.blocks
713 .insert(start.block, Block::at(BlockKind::Paragraph(tail), indent));
714 Cursor::new(start.block, Part::Body, 0)
715 };
716 self.repair();
717 let caret = caret.clamp(self);
718 let caret = self.replace(Selection::at(caret), text).caret;
719 Splice {
720 removed,
721 caret,
722 blocks: self.blocks.len() as isize - before as isize,
723 }
724 }
725
726 /// Put the document into the form markdown can hold — the save step.
727 ///
728 /// Drops the whitespace markdown discards anyway (leading and trailing on
729 /// every line, blank lines at a block's edges), flattens the blocks whose
730 /// output is one line, and renumbers ordered runs. After this,
731 /// `parse(serialize(doc)) == doc`.
732 pub fn normalize(&mut self) {
733 self.normalize_with(&crate::Marks::default());
734 }
735
736 /// [`Doc::normalize`] with the app's own marks — see [`crate::Marks`].
737 pub fn normalize_with(&mut self, marks: &crate::Marks) {
738 for block in &mut self.blocks {
739 let one_line = matches!(block.kind, BlockKind::Heading { .. });
740 match &mut block.kind {
741 BlockKind::Paragraph(text)
742 | BlockKind::Heading { text, .. }
743 | BlockKind::Bullet(text)
744 | BlockKind::Ordered { text, .. }
745 | BlockKind::Task { text, .. }
746 | BlockKind::Quote { text, .. } => {
747 *text = crate::parse::normalize(&text.text, &text.marks);
748 text.normalize_marks();
749 if one_line {
750 crate::parse::collapse_to_one_line(text);
751 }
752 }
753 BlockKind::Table { header, rows, .. } => {
754 for cell in header.iter_mut().chain(rows.iter_mut().flatten()) {
755 *cell = crate::parse::normalize(&cell.text, &cell.marks);
756 cell.normalize_marks();
757 crate::parse::collapse_to_one_line(cell);
758 }
759 }
760 // A caption lives between brackets, where a line break has no
761 // spelling at all.
762 BlockKind::Image { alt, .. } => crate::parse::collapse_to_one_line(alt),
763 BlockKind::Code { .. } | BlockKind::Bookmark { .. } | BlockKind::Rule => {}
764 }
765 }
766 // A blank paragraph is the empty line an editor leaves behind, and
767 // markdown has no way to write one down — blank lines there separate
768 // blocks rather than being one. An empty heading or list item is
769 // different: `# ` and `- ` are both real, so those stay. So is an
770 // alert with no body: `> [!TIP]` writes down and reads back.
771 self.blocks.retain(|block| {
772 !matches!(
773 &block.kind,
774 BlockKind::Paragraph(text) | BlockKind::Quote { kind: None, text } if text.is_empty()
775 )
776 });
777 self.repair();
778
779 // The rules above keep every ordinary edit lossless. They cannot be
780 // complete, and no serializer fix would make them so: whether a mark
781 // boundary can be written depends on CommonMark's flanking rules, and
782 // some marks have no spelling at all. Bold ending on a `~` with a letter
783 // after it is one — a closing delimiter preceded by punctuation and
784 // followed by a letter is not right-flanking, so `Tit**l\~\~**e` does
785 // not close. That is a limit of the format, not a bug in the writer.
786 //
787 // So the last word goes to markdown: adopt the document it can hold.
788 //
789 // This is exact rather than approximate. Anything [`crate::parse`]
790 // returns is a fixed point of the round trip — that is the guarantee the
791 // parser is tested for — so writing this document out and reading it
792 // back yields one by construction. Marks with no spelling are dropped
793 // here, in front of the reader, rather than silently at save time.
794 //
795 // The cheaper rules above still earn their place: they are what keeps
796 // the ordinary edit lossless, so this step has nothing left to take.
797 *self = crate::parse_with(&crate::serialize_with(self, marks), marks);
798 }
799
800 /// Tab. A block can go one level deeper than the one above it, and its
801 /// children come with it.
802 pub fn indent(&mut self, ix: usize) -> bool {
803 let Some(block) = self.blocks.get(ix) else {
804 return false;
805 };
806 if block.indent >= self.ceiling(ix) {
807 return false;
808 }
809 self.shift_subtree(ix, 1);
810 // A run that has just been nested has nothing above it to carry on
811 // from, so it starts over. `renumber` cannot decide this on its own: a
812 // list written `5.` keeps its 5, and from the numbers alone the two
813 // cases look the same.
814 if self.begins_run(ix)
815 && let BlockKind::Ordered { number, .. } = &mut self.blocks[ix].kind
816 {
817 *number = 1;
818 }
819 self.repair();
820 true
821 }
822
823 /// Whether the block at `ix` starts a run of ordered items rather than
824 /// carrying one on: the nearest block at its own indent, before the list
825 /// it sits in ends, is not an ordered item.
826 fn begins_run(&self, ix: usize) -> bool {
827 let indent = self.blocks[ix].indent;
828 self.blocks[..ix]
829 .iter()
830 .rev()
831 .take_while(|block| block.indent >= indent)
832 .find(|block| block.indent == indent)
833 .is_none_or(|block| !matches!(block.kind, BlockKind::Ordered { .. }))
834 }
835
836 /// How deep block `ix` is allowed to sit.
837 ///
838 /// Markdown expresses nesting through list items and nothing else, so a
839 /// block may only go deeper than the one above it when that one is a
840 /// marker. Indenting a paragraph under a *heading* would serialize to four
841 /// leading spaces, which reads back as an indented code block.
842 pub fn ceiling(&self, ix: usize) -> u8 {
843 match ix.checked_sub(1).map(|previous| &self.blocks[previous]) {
844 None => 0,
845 Some(previous) if is_marker(&previous.kind) => previous.indent + 1,
846 Some(previous) => previous.indent,
847 }
848 }
849
850 /// Clamp every indent to what the document can actually express, then make
851 /// ordered runs consecutive. Cheap, total, and called after anything
852 /// structural — a local rule is not enough, because outdenting one block
853 /// can leave the block *after* it stranded a level too deep.
854 ///
855 /// Public because an editor that changes a block's *kind* has to restore
856 /// the invariant too, and only this knows what it is.
857 pub fn repair(&mut self) {
858 for ix in 0..self.blocks.len() {
859 let ceiling = self.ceiling(ix);
860 self.blocks[ix].indent = self.blocks[ix].indent.min(ceiling);
861 }
862 self.renumber();
863 }
864
865 /// Shift-Tab, children included.
866 pub fn outdent(&mut self, ix: usize) -> bool {
867 if self.blocks.get(ix).is_none_or(|block| block.indent == 0) {
868 return false;
869 }
870 self.shift_subtree(ix, -1);
871 self.repair();
872 true
873 }
874
875 /// Move a block and everything nested under it. Children have to travel
876 /// with the parent or the document invariant breaks the moment a level
877 /// disappears from under them.
878 fn shift_subtree(&mut self, ix: usize, by: i8) {
879 let span = self.subtree(ix);
880 for block in &mut self.blocks[span] {
881 block.indent = block.indent.saturating_add_signed(by);
882 }
883 }
884}
885
886fn is_marker(kind: &BlockKind) -> bool {
887 matches!(
888 kind,
889 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
890 )
891}