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