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
28use crate::{
29 doc::{Block, BlockKind, Doc, Mark, MarkSpan, Part, Text},
30 select::{Cursor, Selection},
31};
32
33/// What a [`Doc::replace`] did, for anything holding a position it moved.
34///
35/// The caret is [`Doc::replace`]'s own answer and every caller wants it. The
36/// other two are for a caller keeping a position of its own — a comment anchor,
37/// a bookmark into the document — which has no other way to learn that the text
38/// under it shifted.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct Splice {
41 /// What the call covered, clamped and in document order.
42 pub removed: Selection,
43 /// Where the caret landed: the end of what went in.
44 pub caret: Cursor,
45 /// The change in block count, which every block after [`Self::removed`]
46 /// moves by.
47 pub blocks: isize,
48}
49
50impl Text {
51 /// Insert at a byte offset, moving the marks with it.
52 pub fn insert(&mut self, at: usize, s: &str) {
53 let at = at.min(self.text.len());
54 if s.is_empty() {
55 return;
56 }
57 let n = s.len();
58 self.text.insert_str(at, s);
59 for span in &mut self.marks {
60 if at <= span.range.start {
61 span.range.start += n;
62 span.range.end += n;
63 } else if at <= span.range.end {
64 // Inside, or exactly at the end — the left-sticky rule.
65 span.range.end += n;
66 }
67 }
68 self.normalize_marks();
69 }
70
71 /// Remove a byte range, collapsing any mark that covered it.
72 pub fn remove(&mut self, range: Range<usize>) {
73 let range = self.clamp(range);
74 if range.is_empty() {
75 return;
76 }
77 self.text.replace_range(range.clone(), "");
78 let shift = |offset: usize| {
79 if offset <= range.start {
80 offset
81 } else if offset >= range.end {
82 offset - range.len()
83 } else {
84 range.start
85 }
86 };
87 for span in &mut self.marks {
88 span.range = shift(span.range.start)..shift(span.range.end);
89 }
90 self.normalize_marks();
91 }
92
93 /// Add `mark` over `range`, or take it away if the whole range has it.
94 pub fn toggle(&mut self, range: Range<usize>, mark: Mark) {
95 let range = self.clamp(range);
96 if range.is_empty() {
97 return;
98 }
99 if self.covered_by(&range, &mark) {
100 self.marks = std::mem::take(&mut self.marks)
101 .into_iter()
102 .flat_map(|span| subtract(span, &range, &mark))
103 .collect();
104 } else {
105 self.marks.push(MarkSpan { range, mark });
106 }
107 self.normalize_marks();
108 }
109
110 /// Whether every byte of `range` already carries `mark`.
111 pub fn covered_by(&self, range: &Range<usize>, mark: &Mark) -> bool {
112 !range.is_empty()
113 && self.marks.iter().any(|span| {
114 span.mark == *mark && span.range.start <= range.start && span.range.end >= range.end
115 })
116 }
117
118 /// Cut at `at`, returning the tail. The head keeps this [`Text`].
119 pub fn split_off(&mut self, at: usize) -> Text {
120 let at = at.min(self.text.len());
121 let mut tail = Text {
122 text: self.text.split_off(at),
123 marks: Vec::new(),
124 };
125 let mut head = Vec::new();
126 for span in std::mem::take(&mut self.marks) {
127 if span.range.start < at {
128 head.push(MarkSpan {
129 range: span.range.start..span.range.end.min(at),
130 mark: span.mark.clone(),
131 });
132 }
133 if span.range.end > at {
134 tail.marks.push(MarkSpan {
135 range: span.range.start.saturating_sub(at)..span.range.end - at,
136 mark: span.mark,
137 });
138 }
139 }
140 self.marks = head;
141 self.normalize_marks();
142 tail.normalize_marks();
143 tail
144 }
145
146 /// Append `other`, shifting its marks onto the end of this text.
147 pub fn append(&mut self, other: Text) {
148 let offset = self.text.len();
149 self.text.push_str(&other.text);
150 self.marks
151 .extend(other.marks.into_iter().map(|span| MarkSpan {
152 range: span.range.start + offset..span.range.end + offset,
153 mark: span.mark,
154 }));
155 self.normalize_marks();
156 }
157
158 fn clamp(&self, range: Range<usize>) -> Range<usize> {
159 let start = range.start.min(self.text.len());
160 let end = range.end.clamp(start, self.text.len());
161 start..end
162 }
163
164 /// Drop marks that cover nothing and merge ones that touch.
165 ///
166 /// Both matter to the round trip rather than to tidiness: an empty bold
167 /// span serializes to `****`, which is literal text, and two abutting bold
168 /// spans serialize to `**a****b**`, which is not one bold run.
169 pub(crate) fn normalize_marks(&mut self) {
170 // Emphasis cannot open or close against whitespace — `* t*` is two
171 // literal asterisks, not italic — so a mark reaching over a space has
172 // no spelling that survives a round trip. Shrinking it to the text it
173 // can actually cover is also what a user means when a drag-selection
174 // catches the trailing space.
175 for ix in 0..self.marks.len() {
176 if !matches!(
177 self.marks[ix].mark,
178 Mark::Bold | Mark::Italic | Mark::Strike | Mark::Code
179 ) {
180 continue;
181 }
182 let range = self.marks[ix].range.clone();
183 if range.end > self.text.len() {
184 continue;
185 }
186 let slice = &self.text[range.clone()];
187 let start = range.start + (slice.len() - slice.trim_start().len());
188 let end = (range.end - (slice.len() - slice.trim_end().len())).max(start);
189 self.marks[ix].range = start..end;
190 }
191
192 let len = self.text.len();
193 self.marks.retain(|span| {
194 span.range.end <= len && (!span.range.is_empty() || matches!(span.mark, Mark::Image(_)))
195 });
196
197 // A code span is atomic: nothing can start or stop inside one. A mark
198 // that only half covers it has no spelling, so it grows to take the
199 // whole span — which is also what the markdown for it reads back as.
200 let code: Vec<Range<usize>> = self
201 .marks
202 .iter()
203 .filter(|span| span.mark == Mark::Code)
204 .map(|span| span.range.clone())
205 .collect();
206 for span in &mut self.marks {
207 if span.mark == Mark::Code {
208 continue;
209 }
210 for range in &code {
211 let crosses = span.range.start > range.start && span.range.start < range.end
212 || span.range.end > range.start && span.range.end < range.end;
213 if crosses {
214 span.range.start = span.range.start.min(range.start);
215 span.range.end = span.range.end.max(range.end);
216 }
217 }
218 }
219
220 // Emphasis nests or it is disjoint; it cannot cross. `**a*b**c*` is
221 // not bold-then-italic overlapping, it is a parse error waiting to
222 // happen — so when two spans cross, the one that opened first grows to
223 // contain the other. Growing rather than clipping keeps every mark the
224 // user applied; only its reach changes, and only where markdown left
225 // no alternative.
226 for _ in 0..self.marks.len().max(1) {
227 let mut crossed = false;
228 for a in 0..self.marks.len() {
229 for b in 0..self.marks.len() {
230 let (first, second) = (&self.marks[a].range, &self.marks[b].range);
231 if second.start > first.start
232 && second.start < first.end
233 && second.end > first.end
234 {
235 let end = second.end;
236 self.marks[a].range.end = end;
237 crossed = true;
238 }
239 }
240 }
241 if !crossed {
242 break;
243 }
244 }
245
246 // Two marks that end at the same offset close as two delimiter runs
247 // back to back — `**b**~~`. CommonMark will not let the outer one close
248 // there if a letter follows: a run preceded by punctuation has to be
249 // followed by whitespace or punctuation to be right-flanking, so
250 // `~~a **b**~~c` cannot be written at all. Nudging the outer end past
251 // the word separates the two runs and it can.
252 for _ in 0..self.marks.len().max(1) {
253 let mut nudged = false;
254 for a in 0..self.marks.len() {
255 let end = self.marks[a].range.end;
256 let followed_by_word = self.text[end..]
257 .chars()
258 .next()
259 .is_some_and(char::is_alphanumeric);
260 let shared = self.marks.iter().enumerate().any(|(b, other)| {
261 b != a
262 && other.range.end == end
263 && other.range.start > self.marks[a].range.start
264 });
265 if followed_by_word && shared {
266 let extra = self.text[end..]
267 .find(|c: char| !c.is_alphanumeric())
268 .unwrap_or(self.text.len() - end);
269 self.marks[a].range.end = end + extra;
270 nudged = true;
271 }
272 }
273 if !nudged {
274 break;
275 }
276 }
277
278 let mut ix = 0;
279 while ix < self.marks.len() {
280 let mut merged = None;
281 for other in ix + 1..self.marks.len() {
282 let (a, b) = (&self.marks[ix], &self.marks[other]);
283 if a.mark == b.mark
284 && a.range.start <= b.range.end
285 && b.range.start <= a.range.end
286 && !matches!(a.mark, Mark::Image(_) | Mark::Mention { .. })
287 {
288 merged = Some((
289 other,
290 a.range.start.min(b.range.start),
291 a.range.end.max(b.range.end),
292 ));
293 break;
294 }
295 }
296 match merged {
297 Some((other, start, end)) => {
298 self.marks[ix].range = start..end;
299 self.marks.remove(other);
300 }
301 None => ix += 1,
302 }
303 }
304
305 // Document order, outermost first — the order a parse produces, so an
306 // edited document compares equal to the same document read from disk.
307 // The sort is stable, which is what keeps `**_x_**` and `_**x**_`
308 // apart: their spans are identical and only their order differs.
309 self.marks.sort_by(|a, b| {
310 a.range
311 .start
312 .cmp(&b.range.start)
313 .then(b.range.end.cmp(&a.range.end))
314 });
315 }
316}
317
318/// `span` minus `range`, when they share a mark — zero, one or two pieces.
319fn subtract(span: MarkSpan, range: &Range<usize>, mark: &Mark) -> Vec<MarkSpan> {
320 if span.mark != *mark || span.range.end <= range.start || span.range.start >= range.end {
321 return vec![span];
322 }
323 let mut out = Vec::new();
324 if span.range.start < range.start {
325 out.push(MarkSpan {
326 range: span.range.start..range.start,
327 mark: span.mark.clone(),
328 });
329 }
330 if span.range.end > range.end {
331 out.push(MarkSpan {
332 range: range.end..span.range.end,
333 mark: span.mark,
334 });
335 }
336 out
337}
338
339impl Doc {
340 /// Split block `ix` at byte offset `at`, returning the new block's index.
341 ///
342 /// The tail keeps the block's kind so Enter in a list makes another item —
343 /// except for a heading, where the body that follows a title is body text.
344 pub fn split(&mut self, ix: usize, at: usize) -> usize {
345 if ix >= self.blocks.len() {
346 return ix;
347 }
348 let indent = self.blocks[ix].indent;
349 // Nothing to cut for a block with no body — Enter after an atomic
350 // block opens a paragraph.
351 let tail = self.blocks[ix]
352 .text_at_mut(Part::Body)
353 .map(|text| text.split_off(at))
354 .unwrap_or_default();
355 let kind = match &self.blocks[ix].kind {
356 BlockKind::Bullet(_) => BlockKind::Bullet(tail),
357 BlockKind::Ordered { .. } => BlockKind::Ordered {
358 number: 1,
359 text: tail,
360 },
361 BlockKind::Task { .. } => BlockKind::Task {
362 checked: false,
363 text: tail,
364 },
365 BlockKind::Quote { kind, .. } => BlockKind::Quote {
366 kind: *kind,
367 text: tail,
368 },
369 // A heading titles what follows it; what follows is body text.
370 _ => BlockKind::Paragraph(tail),
371 };
372 self.blocks.insert(ix + 1, Block::at(kind, indent));
373 self.repair();
374 ix + 1
375 }
376
377 /// Backspace at the start of a block.
378 ///
379 /// Notion's chain, in order: an indented block outdents, an image with
380 /// nothing written under it goes, a block wearing syntax around its text
381 /// gives the syntax up, and only a plain block at the left margin merges
382 /// into the one above it. When that one holds no body there is nothing to
383 /// merge into, so the caret steps into a fence or a table, and a rule —
384 /// which no caret can enter, and so no other key can remove — goes.
385 /// Returns where the caret landed, and `None` when nothing moved.
386 ///
387 /// A table cell is not a position that can swallow its neighbour, so
388 /// backspace at the start of one does nothing rather than eating the table.
389 pub fn merge_back(&mut self, at: Cursor) -> Option<Cursor> {
390 if matches!(at.part, Part::Cell { .. }) {
391 return None;
392 }
393 let block = self.blocks.get(at.block)?;
394 if block.indent > 0 {
395 self.outdent(at.block);
396 return Some(Cursor::new(at.block, at.part, 0));
397 }
398 // A caption is the only handle a caret has on an image, so with the
399 // caption empty there is nothing left to take but the picture.
400 if at.part == Part::Caption && block.text_at(Part::Caption)?.is_empty() {
401 let previous = at.block.checked_sub(1);
402 self.blocks.remove(at.block);
403 self.repair();
404 let Some(previous) = previous else {
405 return Some(Cursor::default().clamp(self));
406 };
407 let part = self.blocks[previous]
408 .parts()
409 .last()
410 .copied()
411 .unwrap_or_default();
412 return Some(Cursor::new(previous, part, 0).end(self));
413 }
414 // Every prefix [`shortcut`] reads is chrome around text; the first
415 // backspace takes the chrome and leaves the text where it was, so what
416 // can be typed in can be typed out.
417 let unwrapped = match &block.kind {
418 kind if is_marker(kind) => block.text_at(Part::Body).cloned(),
419 BlockKind::Heading { text, .. } | BlockKind::Quote { text, .. } => Some(text.clone()),
420 BlockKind::Code { code, .. } => Some(code.clone()),
421 _ => None,
422 };
423 if let Some(text) = unwrapped {
424 self.blocks[at.block].kind = BlockKind::Paragraph(text);
425 self.repair();
426 return Some(Cursor::new(at.block, Part::Body, 0));
427 }
428 if at.block == 0 {
429 return None;
430 }
431 let tail = self.blocks[at.block].text_at(Part::Body)?.clone();
432 let previous = at.block - 1;
433 match self.blocks[previous].parts().last().copied() {
434 // Only two blocks that both hold a body can become one.
435 Some(Part::Body) => {
436 let head = self.blocks[previous].text_at_mut(Part::Body)?;
437 let caret = head.text.len();
438 head.append(tail);
439 self.blocks.remove(at.block);
440 self.repair();
441 Some(Cursor::new(previous, Part::Body, caret))
442 }
443 Some(part) => {
444 let end = self.blocks[previous]
445 .text_at(part)
446 .map_or(0, |text| text.text.len());
447 // Stepping into a fence, a caption or a cell leaves this block
448 // where it is, which is right while it still holds something
449 // and a trap once it does not: nothing above it merges, so a
450 // block left empty here is one backspace can never reach again.
451 if tail.is_empty() {
452 self.blocks.remove(at.block);
453 self.repair();
454 }
455 Some(Cursor::new(previous, part, end))
456 }
457 None => {
458 self.blocks.remove(previous);
459 self.repair();
460 Some(Cursor::new(previous, at.part, at.offset))
461 }
462 }
463 }
464
465 /// Apply an edit to the text at `at`, then put the block back in order.
466 ///
467 /// The editor should reach text through here rather than mutating a block
468 /// directly: a heading or a table cell that acquires a newline has no
469 /// spelling, and nothing else is positioned to notice.
470 pub fn edit_at(&mut self, at: Cursor, edit: impl FnOnce(&mut Text)) {
471 let Some(block) = self.blocks.get_mut(at.block) else {
472 return;
473 };
474 let one_line = matches!(block.kind, BlockKind::Heading { .. })
475 || matches!(at.part, Part::Cell { .. } | Part::Caption);
476 let Some(text) = block.text_at_mut(at.part) else {
477 return;
478 };
479 edit(text);
480 if one_line {
481 crate::parse::collapse_to_one_line(text);
482 }
483 }
484
485 /// The blocks nested under `ix`, `ix` included — what a move, a duplicate
486 /// or a drag carries with it.
487 ///
488 /// A flat list makes this a scan for the next block that is not deeper,
489 /// which is the whole argument for the flat list.
490 pub fn subtree(&self, ix: usize) -> Range<usize> {
491 let Some(base) = self.blocks.get(ix).map(|block| block.indent) else {
492 return ix..ix;
493 };
494 let mut end = ix + 1;
495 while self
496 .blocks
497 .get(end)
498 .is_some_and(|block| block.indent > base)
499 {
500 end += 1;
501 }
502 ix..end
503 }
504
505 /// Move a block and its children to sit before or after their neighbour.
506 ///
507 /// `delta` counts *siblings*, not rows: moving down past a bullet with
508 /// three children clears all four, or a block would land inside the run it
509 /// was trying to step over.
510 pub fn move_block(&mut self, ix: usize, delta: isize) -> Option<usize> {
511 let span = self.subtree(ix);
512 if span.is_empty() {
513 return None;
514 }
515 let to = match delta {
516 ..0 => {
517 // The start of whichever subtree ends where this one begins.
518 (0..span.start)
519 .rev()
520 .find(|&above| self.subtree(above).end == span.start)?
521 }
522 0.. => {
523 let next = self.subtree(span.end);
524 if next.is_empty() {
525 return None;
526 }
527 // Landing after the neighbour means landing where it ends,
528 // less the hole this subtree leaves behind.
529 next.end - span.len()
530 }
531 };
532 let moved: Vec<Block> = self.blocks.drain(span.clone()).collect();
533 self.blocks.splice(to..to, moved);
534 self.repair();
535 Some(to)
536 }
537
538 /// Copy a block and its children in below themselves.
539 pub fn duplicate(&mut self, ix: usize) -> Option<usize> {
540 let span = self.subtree(ix);
541 if span.is_empty() {
542 return None;
543 }
544 let copy: Vec<Block> = self.blocks[span.clone()].to_vec();
545 self.blocks.splice(span.end..span.end, copy);
546 self.repair();
547 Some(span.end)
548 }
549
550 /// Delete a block and its children.
551 pub fn remove_block(&mut self, ix: usize) {
552 let span = self.subtree(ix);
553 if span.is_empty() {
554 return;
555 }
556 self.blocks.drain(span);
557 self.repair();
558 }
559
560 /// Turn block `ix` into `kind`, carrying its text across and keeping its
561 /// indent.
562 ///
563 /// The one operation a typed prefix, the slash menu and the block menu all
564 /// perform, so none of them reaches into a block's kind on its own.
565 pub fn set_kind(&mut self, ix: usize, kind: BlockKind) {
566 let Some(block) = self.blocks.get_mut(ix) else {
567 return;
568 };
569 let text = match &block.kind {
570 // A bookmark's text is the link it shows, so turning one back into
571 // prose hands the URL over instead of an empty block.
572 BlockKind::Bookmark { url, .. } => Text::link(url),
573 BlockKind::Image { alt, .. } => alt.clone(),
574 _ => block.text_at(Part::Body).cloned().unwrap_or_default(),
575 };
576 block.kind = kind;
577 match block.text_at_mut(Part::Body) {
578 Some(body) => *body = text,
579 // The two kinds whose text is not a body. Code is also the one the
580 // marks cannot come with.
581 None => match &mut block.kind {
582 BlockKind::Code { code, .. } => *code = Text::plain(text.text),
583 BlockKind::Image { alt, .. } => *alt = text,
584 _ => {}
585 },
586 }
587 self.repair();
588 }
589
590 /// The tag on a fenced block — what the label shows, what the highlighter
591 /// reads, and what the info string carries. Not [`Doc::set_kind`]'s job:
592 /// that carries a *body* across, and a fence has none to give back.
593 pub fn set_language(&mut self, ix: usize, language: Option<String>) {
594 if let Some(BlockKind::Code { language: tag, .. }) =
595 self.blocks.get_mut(ix).map(|block| &mut block.kind)
596 {
597 *tag = language;
598 }
599 }
600
601 /// Turn what a selection covers into one code block, leaving whatever it
602 /// did not cover as blocks of its own.
603 ///
604 /// The fence is what markdown has for code over more than one line. An
605 /// inline span is not: no CommonMark spelling puts a line break inside
606 /// backticks, so one written that way comes back as a space.
607 ///
608 /// Marks are dropped on the way in, the way [`Doc::set_kind`] drops them
609 /// when it turns a block into a fence — code is literal to its closing
610 /// fence, and nothing in it is markup.
611 pub fn fence(&mut self, selection: Selection) -> Cursor {
612 let lines: Vec<String> = self
613 .spans(selection)
614 .iter()
615 .filter(|(at, _)| at.part == Part::Body)
616 .filter_map(|(at, range)| {
617 let text = self.blocks[at.block].text_at(at.part)?;
618 text.text.get(range.clone()).map(str::to_string)
619 })
620 .collect();
621 if lines.is_empty() {
622 return selection.head.clamp(self);
623 }
624 let code = Text::plain(lines.join("\n"));
625
626 // Cutting the selection leaves the head and the tail it did not cover
627 // joined in one block, with the caret at the seam between them — which
628 // is where the fence goes.
629 let at = self.replace(selection, Text::default()).caret;
630 let tail = self.split(at.block, at.offset);
631 let indent = self.blocks[at.block].indent;
632 self.blocks.insert(
633 tail,
634 Block::at(
635 BlockKind::Code {
636 language: None,
637 code,
638 },
639 indent,
640 ),
641 );
642 // A selection that covered whole blocks leaves nothing on either side,
643 // and an empty paragraph is not what "turn this into code" asked for.
644 let empty = |block: &Block| {
645 block
646 .text_at(Part::Body)
647 .is_some_and(|text| text.text.is_empty())
648 };
649 if self.blocks.get(tail + 1).is_some_and(empty) {
650 self.blocks.remove(tail + 1);
651 }
652 let mut fence = tail;
653 if empty(&self.blocks[at.block]) {
654 self.blocks.remove(at.block);
655 fence -= 1;
656 }
657 self.repair();
658 Cursor::new(fence, Part::Code, 0).clamp(self)
659 }
660
661 /// The way back out of a fence: every line becomes a paragraph. `None` when
662 /// the selection is not all code, which is what makes this the other half
663 /// of a toggle rather than an operation of its own.
664 pub fn unfence(&mut self, selection: Selection) -> Option<Cursor> {
665 let (start, end) = selection.clamp(self).ordered();
666 let blocks = start.block..=end.block;
667 if !blocks
668 .clone()
669 .all(|ix| matches!(self.blocks[ix].kind, BlockKind::Code { .. }))
670 {
671 return None;
672 }
673 for ix in blocks.rev() {
674 let BlockKind::Code { code, .. } = &self.blocks[ix].kind else {
675 continue;
676 };
677 let indent = self.blocks[ix].indent;
678 let paragraphs: Vec<Block> = code
679 .text
680 .split('\n')
681 .map(|line| Block::at(BlockKind::Paragraph(Text::plain(line)), indent))
682 .collect();
683 self.blocks.splice(ix..=ix, paragraphs);
684 }
685 self.repair();
686 Some(Cursor::new(start.block, Part::Body, 0).clamp(self))
687 }
688
689 /// Every text a selection touches, with the slice of it covered.
690 ///
691 /// One selection can reach across paragraphs and table cells, and a mark
692 /// applies to each of them separately — marks live inside a [`Text`] and
693 /// have no way to span two.
694 pub fn spans(&self, selection: Selection) -> Vec<(Cursor, Range<usize>)> {
695 let (start, end) = selection.clamp(self).ordered();
696 let (first, last) = (
697 Cursor::new(start.block, start.part, 0),
698 Cursor::new(end.block, end.part, 0),
699 );
700 let mut out = Vec::new();
701 for block in start.block..=end.block.min(self.blocks.len().saturating_sub(1)) {
702 for part in self.blocks[block].parts() {
703 let here = Cursor::new(block, part, 0);
704 if here < first || here > last {
705 continue;
706 }
707 let len = here.len_in(self).unwrap_or(0);
708 let from = if here == first { start.offset } else { 0 };
709 let to = if here == last { end.offset } else { len };
710 if from < to.min(len) {
711 out.push((here, from..to.min(len)));
712 }
713 }
714 }
715 out
716 }
717
718 /// Add `mark` over a selection, or take it away if every part of the
719 /// selection already carries it.
720 ///
721 /// The decision is made across the whole selection before anything moves:
722 /// dragging over a bold word and a plain one and pressing cmd-B should bold
723 /// the rest rather than unbolding the half that was already there.
724 pub fn toggle_mark(&mut self, selection: Selection, mark: Mark) {
725 let spans = self.spans(selection);
726 let remove = self.covered_by(selection, &mark);
727
728 for (at, range) in spans {
729 // Code is literal to its closing fence and a caption has no room
730 // for markup between its brackets; nothing in either is markup.
731 if matches!(at.part, Part::Code | Part::Caption) {
732 continue;
733 }
734 if remove == self.carries(&at, &range, &mark) {
735 let mark = mark.clone();
736 self.edit_at(at, |text| text.toggle(range, mark));
737 }
738 }
739 }
740
741 /// The marks a selection carries throughout — what a toolbar paints as lit.
742 ///
743 /// Collapsed, it answers with the marks the next character typed here would
744 /// join, which is the **left-sticky** rule [`Text::insert`] already
745 /// follows: the run ending at the caret, never the one starting there.
746 pub fn marks(&self, selection: Selection) -> Vec<Mark> {
747 let mut marks: Vec<Mark> = Vec::new();
748 if selection.is_collapsed() {
749 let at = selection.head.clamp(self);
750 let Some(text) = self.blocks.get(at.block).and_then(|b| b.text_at(at.part)) else {
751 return marks;
752 };
753 for span in &text.marks {
754 if span.range.start < at.offset && at.offset <= span.range.end {
755 marks.push(span.mark.clone());
756 }
757 }
758 marks.dedup();
759 return marks;
760 }
761 for (at, range) in self.spans(selection) {
762 let Some(text) = self.blocks[at.block].text_at(at.part) else {
763 continue;
764 };
765 for span in &text.marks {
766 if span.range.start < range.end
767 && span.range.end > range.start
768 && !marks.contains(&span.mark)
769 {
770 marks.push(span.mark.clone());
771 }
772 }
773 }
774 marks.retain(|mark| self.covered_by(selection, mark));
775 marks
776 }
777
778 /// Whether every part of a selection already carries `mark` — what decides
779 /// between adding it and taking it away, and what a toolbar button reads to
780 /// know whether it is lit.
781 pub fn covered_by(&self, selection: Selection, mark: &Mark) -> bool {
782 let spans = self.spans(selection);
783 !spans.is_empty()
784 && spans.iter().all(|(at, range)| {
785 matches!(at.part, Part::Code | Part::Caption) || self.carries(at, range, mark)
786 })
787 }
788
789 fn carries(&self, at: &Cursor, range: &Range<usize>, mark: &Mark) -> bool {
790 self.blocks[at.block]
791 .text_at(at.part)
792 .is_some_and(|text| text.covered_by(range, mark))
793 }
794
795 /// The sub-document a selection covers — what a copy puts on the clipboard.
796 ///
797 /// A table is atomic here for the same reason it is in [`Doc::replace`]:
798 /// half a table has no shape worth keeping, so a selection reaching into
799 /// one takes it whole.
800 pub fn slice(&self, selection: Selection) -> Doc {
801 let (start, end) = selection.clamp(self).ordered();
802 let mut out = Doc {
803 blocks: self.blocks[start.block..=end.block].to_vec(),
804 };
805 let last = end.block - start.block;
806 // Tail first: trimming the head would move the offsets the tail is in.
807 if !matches!(end.part, Part::Cell { .. })
808 && let Some(text) = out.blocks[last].text_at_mut(end.part)
809 {
810 text.split_off(end.offset);
811 }
812 if !matches!(start.part, Part::Cell { .. })
813 && let Some(text) = out.blocks[0].text_at_mut(start.part)
814 {
815 *text = text.split_off(start.offset);
816 }
817 // The slice starts at the left margin whatever depth it was cut from.
818 out.repair();
819 out
820 }
821
822 /// Replace a selection with a whole document — the paste path.
823 ///
824 /// A lone paragraph goes in as inline text, marks and all: pasting a
825 /// sentence into a sentence must not make a new block. Anything else
826 /// arrives as blocks, and the remainder of the caret's block follows them.
827 pub fn splice(&mut self, selection: Selection, other: Doc) -> Cursor {
828 let blocks = other.blocks;
829 let inline = match blocks.as_slice() {
830 [] => Some(Text::default()),
831 [block] => match &block.kind {
832 BlockKind::Paragraph(text) => Some(text.clone()),
833 _ => None,
834 },
835 _ => None,
836 };
837 if let Some(text) = inline {
838 return self.replace(selection, text).caret;
839 }
840
841 let caret = self.replace(selection, Text::default()).caret;
842 let base = self.blocks[caret.block].indent;
843 // Split so what followed the caret follows the paste too. An empty
844 // remainder is the blank block a paste at the end would leave behind.
845 let tail = self.split(caret.block, caret.offset);
846 let empty_tail = self.blocks[tail]
847 .text_at(Part::Body)
848 .is_some_and(Text::is_empty);
849
850 let mut at = caret.block;
851 for block in blocks {
852 at += 1;
853 self.blocks
854 .insert(at, Block::at(block.kind, base.saturating_add(block.indent)));
855 }
856 if empty_tail {
857 self.blocks.remove(at + 1);
858 }
859 // And the block the caret opened in, if the paste displaced all of it.
860 let head_empty = self.blocks[caret.block]
861 .text_at(Part::Body)
862 .is_some_and(Text::is_empty);
863 if head_empty && matches!(self.blocks[caret.block].kind, BlockKind::Paragraph(_)) {
864 self.blocks.remove(caret.block);
865 at -= 1;
866 }
867 self.repair();
868 Cursor::new(at, Part::Body, 0).end(self).clamp(self)
869 }
870
871 /// Replace everything a selection covers with `text`, and say where the
872 /// caret lands.
873 ///
874 /// **The one mutation.** Typing, backspace, delete, cut and paste are all
875 /// this call with a different argument, which is why none of them needs to
876 /// know whether a selection was empty, spanned two paragraphs, or swallowed
877 /// a table on the way past.
878 pub fn replace(&mut self, selection: Selection, text: Text) -> Splice {
879 // An empty document has no block to put anything in; editing one opens
880 // the paragraph every other path then assumes exists.
881 if self.blocks.is_empty() {
882 self.blocks
883 .push(Block::new(BlockKind::Paragraph(Text::default())));
884 }
885 // Counted after that, so the block a nothing-document opens with is not
886 // a shift anything downstream has to hear about.
887 let before = self.blocks.len();
888 let (start, end) = selection.clamp(self).ordered();
889 let removed = Selection::new(start, end);
890
891 // Code is literal and a caption is written between brackets, so marks
892 // arriving from a paste have nowhere to go in either.
893 let text = if matches!(start.part, Part::Code | Part::Caption) {
894 Text::plain(text.text)
895 } else {
896 text
897 };
898
899 if start.block == end.block && start.part == end.part {
900 let at = start.offset + text.text.len();
901 self.edit_at(start, |body| {
902 body.remove(start.offset..end.offset);
903 body.insert(start.offset, &text.text);
904 for span in &text.marks {
905 body.marks.push(MarkSpan {
906 range: start.offset + span.range.start..start.offset + span.range.end,
907 mark: span.mark.clone(),
908 });
909 }
910 body.normalize_marks();
911 });
912 return Splice {
913 removed,
914 caret: Cursor {
915 offset: at,
916 ..start
917 }
918 .clamp(self),
919 blocks: 0,
920 };
921 }
922
923 // Across cells of one table the table itself survives: the covered
924 // cells are emptied and the shape stays, which is what a spreadsheet
925 // selection does and what keeps the columns from collapsing.
926 if start.block == end.block {
927 for part in self.blocks[start.block].parts() {
928 if part < start.part || part > end.part {
929 continue;
930 }
931 // `remove` clamps, so the open end needs no length.
932 let (from, to) = (
933 if part == start.part { start.offset } else { 0 },
934 if part == end.part {
935 end.offset
936 } else {
937 usize::MAX
938 },
939 );
940 self.edit_at(Cursor::new(start.block, part, 0), |body| {
941 body.remove(from..to)
942 });
943 }
944 // The recursion is the insert alone, so what it covered is not what
945 // this call covered — only the caret comes back out of it.
946 let caret = self.replace(Selection::at(start), text).caret;
947 return Splice {
948 removed,
949 caret,
950 blocks: self.blocks.len() as isize - before as isize,
951 };
952 }
953
954 // Across blocks the head keeps its kind and takes the tail's
955 // remainder, and everything between them goes.
956 //
957 // A **table is atomic** to a selection that leaves it. Half a table has
958 // no shape worth keeping, so an end landing in one takes the whole
959 // block rather than splicing a lone cell into a paragraph.
960 let head_keeps = !matches!(start.part, Part::Cell { .. })
961 && self.blocks[start.block].text_at(start.part).is_some();
962 let tail = match end.part {
963 Part::Cell { .. } => Text::default(),
964 part => self.blocks[end.block]
965 .text_at_mut(part)
966 .map(|body| body.split_off(end.offset))
967 .unwrap_or_default(),
968 };
969
970 let indent = self.blocks[start.block].indent;
971 let first = if head_keeps {
972 start.block + 1
973 } else {
974 start.block
975 };
976 self.blocks.drain(first..=end.block);
977
978 let caret = if head_keeps {
979 self.edit_at(start, |body| body.remove(start.offset..usize::MAX));
980 self.edit_at(start, |body| body.append(tail));
981 start
982 } else {
983 // Everything the selection touched is gone, so the tail arrives as
984 // a paragraph in its place.
985 self.blocks
986 .insert(start.block, Block::at(BlockKind::Paragraph(tail), indent));
987 Cursor::new(start.block, Part::Body, 0)
988 };
989 self.repair();
990 let caret = caret.clamp(self);
991 let caret = self.replace(Selection::at(caret), text).caret;
992 Splice {
993 removed,
994 caret,
995 blocks: self.blocks.len() as isize - before as isize,
996 }
997 }
998
999 /// Put the document into the form markdown can hold — the save step.
1000 ///
1001 /// Drops the whitespace markdown discards anyway (leading and trailing on
1002 /// every line, blank lines at a block's edges), flattens the blocks whose
1003 /// output is one line, and renumbers ordered runs. After this,
1004 /// `parse(serialize(doc)) == doc`.
1005 pub fn normalize(&mut self) {
1006 self.normalize_with(&crate::Marks::default());
1007 }
1008
1009 /// [`Doc::normalize`] with the app's own marks — see [`crate::Marks`].
1010 pub fn normalize_with(&mut self, marks: &crate::Marks) {
1011 for block in &mut self.blocks {
1012 let one_line = matches!(block.kind, BlockKind::Heading { .. });
1013 match &mut block.kind {
1014 BlockKind::Paragraph(text)
1015 | BlockKind::Heading { text, .. }
1016 | BlockKind::Bullet(text)
1017 | BlockKind::Ordered { text, .. }
1018 | BlockKind::Task { text, .. }
1019 | BlockKind::Quote { text, .. } => {
1020 *text = crate::parse::normalize(&text.text, &text.marks);
1021 text.normalize_marks();
1022 if one_line {
1023 crate::parse::collapse_to_one_line(text);
1024 }
1025 }
1026 BlockKind::Table { header, rows, .. } => {
1027 for cell in header.iter_mut().chain(rows.iter_mut().flatten()) {
1028 *cell = crate::parse::normalize(&cell.text, &cell.marks);
1029 cell.normalize_marks();
1030 crate::parse::collapse_to_one_line(cell);
1031 }
1032 }
1033 // A caption lives between brackets, where a line break has no
1034 // spelling at all.
1035 BlockKind::Image { alt, .. } => crate::parse::collapse_to_one_line(alt),
1036 BlockKind::Code { .. } | BlockKind::Bookmark { .. } | BlockKind::Rule => {}
1037 }
1038 }
1039 // A blank paragraph is the empty line an editor leaves behind, and
1040 // markdown has no way to write one down — blank lines there separate
1041 // blocks rather than being one. An empty heading or list item is
1042 // different: `# ` and `- ` are both real, so those stay. So is an
1043 // alert with no body: `> [!TIP]` writes down and reads back.
1044 self.blocks.retain(|block| {
1045 !matches!(
1046 &block.kind,
1047 BlockKind::Paragraph(text) | BlockKind::Quote { kind: None, text } if text.is_empty()
1048 )
1049 });
1050 self.repair();
1051
1052 // The rules above keep every ordinary edit lossless. They cannot be
1053 // complete, and no serializer fix would make them so: whether a mark
1054 // boundary can be written depends on CommonMark's flanking rules, and
1055 // some marks have no spelling at all. Bold ending on a `~` with a letter
1056 // after it is one — a closing delimiter preceded by punctuation and
1057 // followed by a letter is not right-flanking, so `Tit**l\~\~**e` does
1058 // not close. That is a limit of the format, not a bug in the writer.
1059 //
1060 // So the last word goes to markdown: adopt the document it can hold.
1061 //
1062 // This is exact rather than approximate. Anything [`crate::parse`]
1063 // returns is a fixed point of the round trip — that is the guarantee the
1064 // parser is tested for — so writing this document out and reading it
1065 // back yields one by construction. Marks with no spelling are dropped
1066 // here, in front of the reader, rather than silently at save time.
1067 //
1068 // The cheaper rules above still earn their place: they are what keeps
1069 // the ordinary edit lossless, so this step has nothing left to take.
1070 *self = crate::parse_with(&crate::serialize_with(self, marks), marks);
1071 }
1072
1073 /// Tab. A block can go one level deeper than the one above it, and its
1074 /// children come with it.
1075 pub fn indent(&mut self, ix: usize) -> bool {
1076 let Some(block) = self.blocks.get(ix) else {
1077 return false;
1078 };
1079 if block.indent >= self.ceiling(ix) {
1080 return false;
1081 }
1082 self.shift_subtree(ix, 1);
1083 // A run that has just been nested has nothing above it to carry on
1084 // from, so it starts over. `renumber` cannot decide this on its own: a
1085 // list written `5.` keeps its 5, and from the numbers alone the two
1086 // cases look the same.
1087 if self.begins_run(ix)
1088 && let BlockKind::Ordered { number, .. } = &mut self.blocks[ix].kind
1089 {
1090 *number = 1;
1091 }
1092 self.repair();
1093 true
1094 }
1095
1096 /// Whether the block at `ix` starts a run of ordered items rather than
1097 /// carrying one on: the nearest block at its own indent, before the list
1098 /// it sits in ends, is not an ordered item.
1099 fn begins_run(&self, ix: usize) -> bool {
1100 let indent = self.blocks[ix].indent;
1101 self.blocks[..ix]
1102 .iter()
1103 .rev()
1104 .take_while(|block| block.indent >= indent)
1105 .find(|block| block.indent == indent)
1106 .is_none_or(|block| !matches!(block.kind, BlockKind::Ordered { .. }))
1107 }
1108
1109 /// How deep block `ix` is allowed to sit.
1110 ///
1111 /// Markdown expresses nesting through list items and nothing else, so a
1112 /// block may only go deeper than the one above it when that one is a
1113 /// marker. Indenting a paragraph under a *heading* would serialize to four
1114 /// leading spaces, which reads back as an indented code block.
1115 pub fn ceiling(&self, ix: usize) -> u8 {
1116 match ix.checked_sub(1).map(|previous| &self.blocks[previous]) {
1117 None => 0,
1118 Some(previous) if is_marker(&previous.kind) => previous.indent + 1,
1119 Some(previous) => previous.indent,
1120 }
1121 }
1122
1123 /// Clamp every indent to what the document can actually express, then make
1124 /// ordered runs consecutive. Cheap, total, and called after anything
1125 /// structural — a local rule is not enough, because outdenting one block
1126 /// can leave the block *after* it stranded a level too deep.
1127 ///
1128 /// Public because an editor that changes a block's *kind* has to restore
1129 /// the invariant too, and only this knows what it is.
1130 pub fn repair(&mut self) {
1131 for ix in 0..self.blocks.len() {
1132 let ceiling = self.ceiling(ix);
1133 self.blocks[ix].indent = self.blocks[ix].indent.min(ceiling);
1134 }
1135 self.renumber();
1136 }
1137
1138 /// Shift-Tab, children included.
1139 pub fn outdent(&mut self, ix: usize) -> bool {
1140 if self.blocks.get(ix).is_none_or(|block| block.indent == 0) {
1141 return false;
1142 }
1143 self.shift_subtree(ix, -1);
1144 self.repair();
1145 true
1146 }
1147
1148 /// Move a block and everything nested under it. Children have to travel
1149 /// with the parent or the document invariant breaks the moment a level
1150 /// disappears from under them.
1151 fn shift_subtree(&mut self, ix: usize, by: i8) {
1152 let span = self.subtree(ix);
1153 for block in &mut self.blocks[span] {
1154 block.indent = block.indent.saturating_add_signed(by);
1155 }
1156 }
1157}
1158
1159fn is_marker(kind: &BlockKind) -> bool {
1160 matches!(
1161 kind,
1162 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
1163 )
1164}
1165
1166/// A markdown prefix typed at the start of a block, and what it turns it into.
1167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1168pub enum Shortcut {
1169 Heading(u8),
1170 Bullet,
1171 Ordered,
1172 Task(bool),
1173 Quote,
1174 Code,
1175 Rule,
1176}
1177
1178impl Shortcut {
1179 /// The block this shortcut makes, carrying whatever text was left over.
1180 pub fn apply(self, text: Text) -> BlockKind {
1181 match self {
1182 Self::Heading(level) => BlockKind::Heading { level, text },
1183 Self::Bullet => BlockKind::Bullet(text),
1184 Self::Ordered => BlockKind::Ordered { number: 1, text },
1185 Self::Task(checked) => BlockKind::Task { checked, text },
1186 Self::Quote => BlockKind::Quote { kind: None, text },
1187 // Code is literal, so whatever marks the text carried have no
1188 // meaning inside the fence.
1189 Self::Code => BlockKind::Code {
1190 language: None,
1191 code: Text::plain(text.text),
1192 },
1193 Self::Rule => BlockKind::Rule,
1194 }
1195 }
1196}
1197
1198/// Match a markdown prefix at the start of a block, returning it and how many
1199/// bytes it occupied.
1200///
1201/// This is the input side of the same vocabulary [`crate::parse`] reads: typing
1202/// `## ` makes a heading because pasting `## ` would have. Order matters — a
1203/// task marker is a bullet with more on the end.
1204pub fn shortcut(text: &str) -> Option<(Shortcut, usize)> {
1205 const PREFIXES: &[(&str, Shortcut)] = &[
1206 ("- [ ] ", Shortcut::Task(false)),
1207 ("- [x] ", Shortcut::Task(true)),
1208 ("###### ", Shortcut::Heading(6)),
1209 ("##### ", Shortcut::Heading(5)),
1210 ("#### ", Shortcut::Heading(4)),
1211 ("### ", Shortcut::Heading(3)),
1212 ("## ", Shortcut::Heading(2)),
1213 ("# ", Shortcut::Heading(1)),
1214 ("- ", Shortcut::Bullet),
1215 ("* ", Shortcut::Bullet),
1216 ("+ ", Shortcut::Bullet),
1217 ("1. ", Shortcut::Ordered),
1218 ("> ", Shortcut::Quote),
1219 ("```", Shortcut::Code),
1220 ("---", Shortcut::Rule),
1221 ];
1222 PREFIXES
1223 .iter()
1224 .find(|(prefix, _)| text.starts_with(prefix))
1225 .map(|(prefix, shortcut)| (*shortcut, prefix.len()))
1226}
1227
1228/// A closing inline delimiter just typed, and the run it closes.
1229///
1230/// The inline half of the same vocabulary [`shortcut`] covers: typing the last
1231/// `*` of `**bold**` makes it bold because pasting `**bold**` would have.
1232/// Returns the opening delimiter's range and the text between it and the caret;
1233/// the closing delimiter is `inner.end..caret`.
1234pub fn inline_rule(text: &str, caret: usize) -> Option<(Range<usize>, Range<usize>, Mark)> {
1235 let head = text.get(..caret)?;
1236 // Longest first — `**` is bold, and only what is left of it is italic.
1237 for (delimiter, mark) in [
1238 ("**", Mark::Bold),
1239 ("__", Mark::Bold),
1240 ("~~", Mark::Strike),
1241 ("`", Mark::Code),
1242 ("_", Mark::Italic),
1243 ("*", Mark::Italic),
1244 ] {
1245 let Some(closes) = head.strip_suffix(delimiter) else {
1246 continue;
1247 };
1248 let Some(open) = closes.rfind(delimiter) else {
1249 continue;
1250 };
1251 // `**bold*` is one keystroke from closing. Reading its second opening
1252 // star as italic's spends it, and the star still to come then finds no
1253 // `**` to close. The same holds for `__bold_`.
1254 if matches!(delimiter, "*" | "_") && text[..open].ends_with(delimiter) {
1255 continue;
1256 }
1257 let inner = open + delimiter.len()..closes.len();
1258 let Some(body) = text.get(inner.clone()).filter(|body| !body.is_empty()) else {
1259 continue;
1260 };
1261 // Emphasis cannot open or close against whitespace, so a mark reaching
1262 // over one has no spelling and [`Text::normalize_marks`] would shrink
1263 // it straight back off. A rule that fires and vanishes is worse than
1264 // one that does not fire.
1265 if body.starts_with(char::is_whitespace) || body.ends_with(char::is_whitespace) {
1266 continue;
1267 }
1268 // An underscore inside a word is not emphasis in CommonMark, which is
1269 // the only reason `snake_case_names` survive being typed.
1270 if delimiter.starts_with('_')
1271 && text[..open]
1272 .chars()
1273 .next_back()
1274 .is_some_and(char::is_alphanumeric)
1275 {
1276 continue;
1277 }
1278 return Some((open..open + delimiter.len(), inner, mark));
1279 }
1280 None
1281}