Skip to main content

markdown_editor/
command.rs

1use super::{
2    document::{BlockContent, EditorDocument},
3    editor::BlockEditorModel,
4    id::BlockId,
5    inline::InlineText,
6    kind::{BlockKind, BlockKindData},
7    marks::{MarkChange, MarkKind, MarkState},
8    position::{BlockPosition, PositionError},
9    selection::{BlockSelection, ResolvedSelection, SelectionError},
10    step::{DetachedBlock, DetachedContent, StepError},
11    transaction::{EditOrigin, TransactionBuilder},
12};
13use gpui::SharedString;
14
15use gpui_kit::base::text::editor_host::TextMark;
16
17#[non_exhaustive]
18#[derive(Clone, PartialEq, Debug)]
19pub enum EditorCommand {
20    ToggleMark(MarkKind),
21    ApplyMark(MarkChange),
22    SetBlockKind(BlockKind),
23    ToggleList {
24        ordered: bool,
25    },
26    ToggleTask,
27    /// Make the caret's list item a task item, checked or not (D14).
28    SetTaskChecked(bool),
29    Indent,
30    Outdent,
31    MoveBlocks {
32        down: bool,
33    },
34    InsertBlock(BlockKind),
35    /// Make the caret's paragraph a code block (its text becomes the code),
36    /// or set the language of the code block holding the caret.
37    SetCodeBlock {
38        lang: Option<String>,
39    },
40    /// Insert an empty table (`rows` includes the header row) after the
41    /// caret's block, replacing it when it is an empty paragraph.
42    InsertTable {
43        rows: usize,
44        cols: usize,
45    },
46    /// Add or remove a row or column at the caret's cell.
47    EditTable(super::table::TableEdit),
48    SelectAll,
49    DeleteSelection,
50}
51
52#[non_exhaustive]
53#[derive(Clone, PartialEq, Debug)]
54pub enum CommandError {
55    Position(PositionError),
56    Selection(SelectionError),
57    /// The caret is in a block the spike cannot edit in place (code block,
58    /// atom); Phase 4.
59    NotEditable(BlockId),
60    Unsupported(&'static str),
61    /// A step failed; the transaction was rolled back.
62    Internal(String),
63}
64
65impl From<StepError> for CommandError {
66    fn from(error: StepError) -> Self {
67        Self::Internal(format!("{error:?}"))
68    }
69}
70
71impl From<SelectionError> for CommandError {
72    fn from(error: SelectionError) -> Self {
73        Self::Selection(error)
74    }
75}
76
77impl From<PositionError> for CommandError {
78    fn from(error: PositionError) -> Self {
79        Self::Position(error)
80    }
81}
82
83/// A position plus its path from the root, which commands walk to find
84/// their target ("the nearest `ListItem`") at any nesting depth (R7).
85pub(crate) struct ResolvedPosition {
86    pub(crate) pos: BlockPosition,
87    pub(crate) path: Vec<BlockId>,
88}
89
90impl ResolvedPosition {
91    pub(crate) fn resolve(doc: &EditorDocument, pos: BlockPosition) -> Result<Self, PositionError> {
92        let pos = doc.validate(pos)?;
93        Ok(Self {
94            pos,
95            path: doc.path(pos.block),
96        })
97    }
98
99    /// Nearest ancestor-or-self matching `pred`, innermost first.
100    pub(crate) fn nearest(
101        &self,
102        doc: &EditorDocument,
103        pred: impl Fn(&BlockKindData) -> bool,
104    ) -> Option<BlockId> {
105        self.path
106            .iter()
107            .rev()
108            .copied()
109            .find(|id| doc.kind(*id).is_some_and(&pred))
110    }
111}
112
113fn is_list_item(kind: &BlockKindData) -> bool {
114    matches!(kind, BlockKindData::ListItem { .. })
115}
116
117fn caret(pos: BlockPosition) -> Option<BlockSelection> {
118    Some(BlockSelection::caret(pos))
119}
120
121/// Delete a resolved range and return where the caret goes. Atoms and code
122/// blocks between (or at fully covered ends of) the range are removed.
123fn delete_range(
124    tx: &mut TransactionBuilder<'_>,
125    range: ResolvedSelection,
126) -> Result<BlockPosition, CommandError> {
127    let (start, end) = (range.start, range.end);
128    let doc = &*tx.doc;
129    if start.block == end.block {
130        if doc.inline(start.block).is_some() {
131            tx.replace_text(start.block, start.offset..end.offset, InlineText::new())?;
132        } else if doc.plain(start.block).is_some() {
133            tx.replace_plain(start.block, start.offset..end.offset, "")?;
134        } else if start.offset == 0 && end.offset == 1 {
135            tx.remove_block(start.block)?;
136        }
137        return Ok(start);
138    }
139    // Select-all followed by an edit starts a fresh document (R2 repro):
140    // every leaf is fully covered, including the code block.
141    if start == doc.first_caret()
142        && end.block == doc.last_caret().block
143        && end.offset == doc.last_caret().offset
144    {
145        let root = doc.root;
146        let len = doc.children(root).len();
147        tx.remove(root, 0..len)?;
148        let paragraph = DetachedBlock::paragraph(InlineText::new());
149        let id = paragraph.id;
150        tx.insert(root, 0, vec![paragraph])?;
151        return Ok(BlockPosition::new(id, 0));
152    }
153    let leaves = doc.walk_leaves();
154    let (Some(si), Some(ei)) = (
155        leaves.iter().position(|id| *id == start.block),
156        leaves.iter().position(|id| *id == end.block),
157    ) else {
158        return Err(CommandError::Position(PositionError::UnknownBlock(
159            start.block,
160        )));
161    };
162    let is_cell =
163        |doc: &EditorDocument, id: BlockId| doc.kind(id) == Some(&BlockKindData::TableCell);
164
165    // Start leaf.
166    let start_code = doc.plain(start.block).map(str::len);
167    let start_kept = if let Some(inline) = doc.inline(start.block) {
168        let len = inline.len();
169        tx.replace_text(start.block, start.offset..len, InlineText::new())?;
170        true
171    } else if let Some(len) = start_code {
172        // Code keeps its head; it never joins with text.
173        tx.replace_plain(start.block, start.offset..len, "")?;
174        true
175    } else if start.offset == 0 {
176        tx.remove_block(start.block)?;
177        false
178    } else {
179        false
180    };
181    let start_inline = start_kept && start_code.is_none();
182    // Leaves in between.
183    for id in &leaves[si + 1..ei] {
184        if is_cell(tx.doc, *id) {
185            let len = tx.doc.leaf_len(*id).unwrap_or(0);
186            tx.replace_text(*id, 0..len, InlineText::new())?;
187        } else if tx.doc.get(*id).is_some() {
188            tx.remove_block(*id)?;
189        }
190    }
191    // End leaf.
192    if let Some(inline) = tx.doc.inline(end.block) {
193        let tail = inline.slice(end.offset..inline.len());
194        let joinable = start_inline && !is_cell(tx.doc, end.block) && !is_cell(tx.doc, start.block);
195        if joinable {
196            tx.remove_block(end.block)?;
197            tx.replace_text(start.block, start.offset..start.offset, tail)?;
198        } else {
199            tx.replace_text(end.block, 0..end.offset, InlineText::new())?;
200            if !start_kept {
201                return Ok(BlockPosition::new(end.block, 0));
202            }
203        }
204    } else if tx.doc.plain(end.block).is_some() {
205        tx.replace_plain(end.block, 0..end.offset, "")?;
206    } else if end.offset == 1 {
207        tx.remove_block(end.block)?;
208    }
209    if start_kept {
210        Ok(start)
211    } else {
212        Ok(tx.doc.first_caret())
213    }
214}
215
216impl BlockEditorModel {
217    /// Marks for text typed at `pos` (D6): bold/italic/strike/underline/
218    /// highlight extend at their trailing edge; link and code only apply
219    /// strictly inside.
220    pub(crate) fn inherited_marks(&self, pos: BlockPosition) -> TextMark {
221        let Some(inline) = self.doc.inline(pos.block) else {
222            return TextMark::default();
223        };
224        let before = inline.mark_before(pos.offset);
225        let after = inline.mark_after(pos.offset);
226        match (before, after) {
227            (Some(b), Some(a)) if b == a => b.clone(),
228            (Some(b), _) => {
229                let mut mark = b.clone();
230                for kind in [MarkKind::Link, MarkKind::Code] {
231                    if !kind.extends_at_boundary() {
232                        mark = MarkChange::new().clear(kind).apply(&mark);
233                    }
234                }
235                mark
236            }
237            _ => TextMark::default(),
238        }
239    }
240
241    /// Per-leaf `(block, start, end)` covered by a text range.
242    pub(crate) fn text_ranges(&self, range: ResolvedSelection) -> Vec<(BlockId, usize, usize)> {
243        // O(1) leaf lookups through the order index (Phase 5: painting a
244        // selection costs O(selected leaves), not O(document)).
245        let leaves = self.doc.leaves();
246        let (Some(si), Some(ei)) = (
247            self.doc.order.rank(range.start.block).map(|r| r as usize),
248            self.doc.order.rank(range.end.block).map(|r| r as usize),
249        ) else {
250            return Vec::new();
251        };
252        leaves[si..=ei]
253            .iter()
254            .map(|id| {
255                let len = self.doc.leaf_len(*id).unwrap_or(0);
256                let lo = if *id == range.start.block {
257                    range.start.offset
258                } else {
259                    0
260                };
261                let hi = if *id == range.end.block {
262                    range.end.offset
263                } else {
264                    len
265                };
266                (*id, lo, hi)
267            })
268            .collect()
269    }
270
271    fn resolved(&self) -> Result<ResolvedSelection, CommandError> {
272        Ok(self.doc.resolve_selection(&self.selection)?)
273    }
274
275    /// Insert plain text at the selection (replacing it); `\n` splits blocks.
276    pub fn insert_text(&mut self, text: &str, origin: EditOrigin) -> Result<(), CommandError> {
277        // Typing over a node or cell selection first replaces it; the
278        // replacement and the typing are one undo step.
279        if self.selected_cells().is_some() || self.selected_blocks().is_some() {
280            self.history.begin_transaction();
281            let result = (|| {
282                if self.selected_cells().is_some() {
283                    self.clear_cells(origin)?;
284                } else {
285                    self.replace_blocks_with_paragraph(origin)?;
286                }
287                self.insert_text_at_selection(text, origin)
288            })();
289            self.history.commit_transaction();
290            return result;
291        }
292        self.insert_text_at_selection(text, origin)
293    }
294
295    /// Replace a `BlockSelection::Blocks` by one empty paragraph holding
296    /// the caret (like Select All + typing).
297    fn replace_blocks_with_paragraph(&mut self, origin: EditOrigin) -> Result<bool, CommandError> {
298        let Some((parent, range)) = self.selected_blocks() else {
299            return Ok(false);
300        };
301        self.transact(origin, |tx| {
302            let start = range.start;
303            tx.remove(parent, range)?;
304            let paragraph = DetachedBlock::paragraph(InlineText::new());
305            let id = paragraph.id;
306            tx.insert(parent, start, vec![paragraph])?;
307            Ok(caret(BlockPosition::new(id, 0)))
308        })
309    }
310
311    fn insert_text_at_selection(
312        &mut self,
313        text: &str,
314        origin: EditOrigin,
315    ) -> Result<(), CommandError> {
316        let range = self.resolved()?;
317        let stored = self.stored_marks.clone();
318        let marks = match &stored {
319            Some(marks) => marks.clone(),
320            None => self.inherited_marks(range.start),
321        };
322        self.transact(origin, |tx| {
323            let mut pos = if range.is_collapsed() {
324                range.start
325            } else {
326                delete_range(tx, range)?
327            };
328            if tx.doc.plain(pos.block).is_some() {
329                // Code: raw text, newlines included; marks do not apply.
330                let text = text.replace('\r', "");
331                tx.replace_plain(pos.block, pos.offset..pos.offset, &text)?;
332                pos.offset += text.len();
333                return Ok(caret(pos));
334            }
335            for (ix, line) in text.split('\n').enumerate() {
336                if ix > 0 {
337                    pos = split_at(tx, pos)?;
338                }
339                let Some(inline) = tx.doc.inline(pos.block) else {
340                    return Err(CommandError::NotEditable(pos.block));
341                };
342                let _ = inline;
343                let insert = InlineText::plain(line, marks.clone());
344                let len = insert.len();
345                tx.replace_text(pos.block, pos.offset..pos.offset, insert)?;
346                pos.offset += len;
347            }
348            Ok(caret(pos))
349        })?;
350        Ok(())
351    }
352
353    /// Paste the editor's own inline Markdown copy (D17): each line is
354    /// parsed as inline Markdown (marks survive), lines become blocks as
355    /// typed. Edge whitespace stays literal. One undo step. In a code block
356    /// the marks are dropped (the text is inserted raw); `Ok(false)` when
357    /// the target cannot take it (a table cell with several lines).
358    pub fn paste_inline_markdown(&mut self, text: &str) -> Result<bool, CommandError> {
359        let lines: Vec<InlineText> = text
360            .split('\n')
361            .map(|l| self.inline_from_markdown(l))
362            .collect();
363        let cell = |m: &Self| m.current_cell().is_some();
364        if lines.len() > 1 && cell(self) {
365            return Ok(false);
366        }
367        self.history.begin_transaction();
368        let result = (|| {
369            if self.selected_cells().is_some() {
370                self.clear_cells(EditOrigin::Paste)?;
371            } else if self.selected_blocks().is_some() {
372                self.replace_blocks_with_paragraph(EditOrigin::Paste)?;
373            }
374            let range = self.resolved()?;
375            self.transact(EditOrigin::Paste, |tx| {
376                let mut pos = if range.is_collapsed() {
377                    range.start
378                } else {
379                    delete_range(tx, range)?
380                };
381                if tx.doc.plain(pos.block).is_some() {
382                    let raw = lines
383                        .iter()
384                        .map(InlineText::text)
385                        .collect::<Vec<_>>()
386                        .join("\n");
387                    tx.replace_plain(pos.block, pos.offset..pos.offset, &raw)?;
388                    pos.offset += raw.len();
389                    return Ok(caret(pos));
390                }
391                for (ix, line) in lines.iter().enumerate() {
392                    if ix > 0 {
393                        pos = split_at(tx, pos)?;
394                    }
395                    if tx.doc.inline(pos.block).is_none() {
396                        return Err(CommandError::NotEditable(pos.block));
397                    }
398                    let len = line.len();
399                    tx.replace_text(pos.block, pos.offset..pos.offset, line.clone())?;
400                    pos.offset += len;
401                }
402                Ok(caret(pos))
403            })
404        })();
405        self.history.commit_transaction();
406        result.map(|_| true)
407    }
408
409    /// One line of inline Markdown as marked text. Leading and trailing
410    /// whitespace stays literal (a Markdown paragraph would drop it); a line
411    /// that does not parse to exactly one paragraph is kept as plain text.
412    fn inline_from_markdown(&self, line: &str) -> InlineText {
413        let core = line.trim();
414        if core.is_empty() {
415            return InlineText::plain(line, TextMark::default());
416        }
417        let start = line.len() - line.trim_start().len();
418        let parsed =
419            gpui_kit::base::text::editor_host::parse_markdown(core, self.doc.extensions.clone())
420                .ok()
421                .and_then(|parsed| {
422                    let mut defs = super::import::Defs::new();
423                    super::import::collect_definitions(&parsed, &mut defs);
424                    let mut blocks = Vec::new();
425                    for node in parsed.iter() {
426                        super::import::import_block(node, &defs, &mut blocks);
427                    }
428                    match (blocks.len(), blocks.pop()) {
429                        (1, Some(block)) if block.kind == BlockKindData::Paragraph => {
430                            match block.content {
431                                DetachedContent::Leaf(BlockContent::Inline(inline)) => Some(inline),
432                                _ => None,
433                            }
434                        }
435                        _ => None,
436                    }
437                });
438        let Some(inline) = parsed else {
439            return InlineText::plain(line, TextMark::default());
440        };
441        let mut out = InlineText::plain(&line[..start], TextMark::default());
442        out.push(inline);
443        out.push(InlineText::plain(
444            &line[start + core.len()..],
445            TextMark::default(),
446        ));
447        out
448    }
449
450    /// Backspace (`forward == false`) or Delete: a grapheme, the selection, a join with the neighbouring block, or (next to a table) select the table.
451    pub fn delete(&mut self, forward: bool) -> Result<(), CommandError> {
452        self.delete_as(forward, false)
453    }
454
455    /// [`Self::delete`]; `word` records it as a word delete (D21), which
456    /// coalesces only with other word deletes.
457    pub(crate) fn delete_as(&mut self, forward: bool, word: bool) -> Result<(), CommandError> {
458        if self.selected_cells().is_some() {
459            return self
460                .clear_cells(EditOrigin::Command("clear_cells"))
461                .map(|_| ());
462        }
463        if self.selected_blocks().is_some() {
464            return self.delete_blocks().map(|_| ());
465        }
466        let range = self.resolved()?;
467        // Backspace at the start of a heading: back to a paragraph, keeping
468        // the text and marks, wherever the heading sits.
469        if !forward
470            && range.is_collapsed()
471            && range.start.offset == 0
472            && matches!(
473                self.doc.kind(range.start.block),
474                Some(BlockKindData::Heading { .. })
475            )
476        {
477            return self.heading_to_paragraph(range.start.block, "heading_backspace");
478        }
479        if range.is_collapsed() && self.select_adjacent_table(range.start, forward)? {
480            return Ok(());
481        }
482        // Backspace at the start of a quote's first paragraph, or of a list
483        // item's trailing empty paragraph: lift it (one undo step).
484        if !forward
485            && range.is_collapsed()
486            && range.start.offset == 0
487            && (self.lift_first_in_quote(range.start)?
488                || self.lift_item_trailing(range.start, false)?)
489        {
490            return Ok(());
491        }
492        // Backspace at the start (Delete at the end) of a non-empty code
493        // block: never merge text into or out of code; step the caret to the
494        // neighbouring leaf instead.
495        if range.is_collapsed()
496            && let Some(code) = self.doc.plain(range.start.block)
497            && !code.is_empty()
498            && ((!forward && range.start.offset == 0)
499                || (forward && range.start.offset == code.len()))
500        {
501            let leaves = self.doc.walk_leaves();
502            let ix = leaves.iter().position(|id| *id == range.start.block);
503            let target = ix.and_then(|ix| {
504                if forward {
505                    leaves.get(ix + 1).map(|id| (*id, 0))
506                } else {
507                    ix.checked_sub(1)
508                        .and_then(|i| leaves.get(i))
509                        .map(|id| (*id, self.doc.leaf_len(*id).unwrap_or(0)))
510                }
511            });
512            if let Some((block, offset)) = target {
513                self.set_selection(BlockSelection::caret(BlockPosition::new(block, offset)))?;
514            }
515            return Ok(());
516        }
517        let origin = match (forward, word) {
518            (true, true) => EditOrigin::WordDelete,
519            (false, true) => EditOrigin::WordBackspace,
520            (true, false) => EditOrigin::DeleteForward,
521            (false, false) => EditOrigin::Backspace,
522        };
523        self.transact(origin, |tx| {
524            if !range.is_collapsed() {
525                let pos = delete_range(tx, range)?;
526                drop_empty_heading(tx, pos.block)?;
527                return Ok(caret(pos));
528            }
529            let pos = range.start;
530            let doc = &*tx.doc;
531            if let Some(code) = doc.plain(pos.block) {
532                let prev = code[..pos.offset].chars().next_back().map(char::len_utf8);
533                let next = code[pos.offset..].chars().next().map(char::len_utf8);
534                match (forward, prev, next) {
535                    (false, Some(n), _) => {
536                        tx.replace_plain(pos.block, pos.offset - n..pos.offset, "")?;
537                        return Ok(caret(BlockPosition::new(pos.block, pos.offset - n)));
538                    }
539                    (true, _, Some(n)) => {
540                        tx.replace_plain(pos.block, pos.offset..pos.offset + n, "")?;
541                        return Ok(caret(pos));
542                    }
543                    // Backspace at the start of an empty code block: back to
544                    // a paragraph.
545                    (false, None, None) => {
546                        let (parent, index) = doc
547                            .index_in_parent(pos.block)
548                            .ok_or(PositionError::UnknownBlock(pos.block))?;
549                        tx.remove_block(pos.block)?;
550                        let paragraph = DetachedBlock::paragraph(InlineText::new());
551                        let id = paragraph.id;
552                        tx.insert(parent, index, vec![paragraph])?;
553                        return Ok(caret(BlockPosition::new(id, 0)));
554                    }
555                    _ => return Ok(None),
556                }
557            }
558            let Some(inline) = doc.inline(pos.block) else {
559                // Caret on an atom or code block: delete the block.
560                tx.remove_block(pos.block)?;
561                return Ok(None);
562            };
563            if !forward && pos.offset > 0 {
564                let prev = inline.prev_grapheme(pos.offset).unwrap_or(0);
565                tx.replace_text(pos.block, prev..pos.offset, InlineText::new())?;
566                drop_empty_heading(tx, pos.block)?;
567                return Ok(caret(BlockPosition::new(pos.block, prev)));
568            }
569            if forward && pos.offset < inline.len() {
570                let next = inline.next_grapheme(pos.offset).unwrap_or(inline.len());
571                tx.replace_text(pos.block, pos.offset..next, InlineText::new())?;
572                drop_empty_heading(tx, pos.block)?;
573                return Ok(caret(pos));
574            }
575            if doc.kind(pos.block) == Some(&BlockKindData::TableCell) {
576                return Ok(None);
577            }
578            // At a block edge: join with the neighbouring leaf.
579            let leaves = doc.walk_leaves();
580            let Some(ix) = leaves.iter().position(|id| *id == pos.block) else {
581                return Ok(None);
582            };
583            let (first, second) = if forward {
584                match leaves.get(ix + 1) {
585                    Some(next) => (pos.block, *next),
586                    None => return Ok(None),
587                }
588            } else {
589                match ix.checked_sub(1).and_then(|i| leaves.get(i)) {
590                    Some(prev) => (*prev, pos.block),
591                    None => return Ok(None),
592                }
593            };
594            if doc.kind(first) == Some(&BlockKindData::TableCell)
595                || doc.kind(second) == Some(&BlockKindData::TableCell)
596            {
597                return Ok(None);
598            }
599            match (doc.inline(first), doc.inline(second)) {
600                (Some(a), Some(b)) => {
601                    let at = a.len();
602                    let moved = b.clone();
603                    tx.remove_block(second)?;
604                    tx.replace_text(first, at..at, moved)?;
605                    Ok(caret(BlockPosition::new(first, at)))
606                }
607                // A code block next door: step into it, never delete it.
608                (None, Some(_)) if doc.plain(first).is_some() => {
609                    let len = doc.leaf_len(first).unwrap_or(0);
610                    Ok(caret(BlockPosition::new(first, len)))
611                }
612                (Some(_), None) if doc.plain(second).is_some() => {
613                    Ok(caret(BlockPosition::new(second, 0)))
614                }
615                // The neighbour is an atom: remove it.
616                _ => {
617                    let other = if forward { second } else { first };
618                    tx.remove_block(other)?;
619                    Ok(caret(pos))
620                }
621            }
622        })?;
623        Ok(())
624    }
625
626    /// Enter: split the text block (in a list, a new item; in an empty item, exit or outdent).
627    pub fn split_block(&mut self) -> Result<(), CommandError> {
628        let range = self.resolved()?;
629        // Enter in an empty heading: a paragraph, not another heading.
630        if range.is_collapsed()
631            && matches!(
632                self.doc.kind(range.start.block),
633                Some(BlockKindData::Heading { .. })
634            )
635            && self.doc.leaf_len(range.start.block) == Some(0)
636        {
637            return self.heading_to_paragraph(range.start.block, "heading_enter");
638        }
639        if range.is_collapsed() && self.exit_empty_item(range.start)? {
640            return Ok(());
641        }
642        if range.is_collapsed() && self.lift_empty_trailing(range.start)? {
643            return Ok(());
644        }
645        if self.doc.plain(range.start.block).is_some() {
646            return self.code_enter(range).map(|_| ());
647        }
648        self.transact(EditOrigin::Command("split_block"), |tx| {
649            let pos = if range.is_collapsed() {
650                range.start
651            } else {
652                delete_range(tx, range)?
653            };
654            Ok(caret(split_at(tx, pos)?))
655        })?;
656        Ok(())
657    }
658
659    /// A heading becomes a paragraph in one transaction; text, marks and
660    /// the caret stay.
661    fn heading_to_paragraph(
662        &mut self,
663        block: BlockId,
664        name: &'static str,
665    ) -> Result<(), CommandError> {
666        self.transact(EditOrigin::Command(name), |tx| {
667            tx.set_kind(block, BlockKindData::Paragraph)?;
668            Ok(None)
669        })
670        .map(|_| ())
671    }
672
673    /// Enter in a code block inserts a newline; Enter on a trailing empty
674    /// line (caret at the end, text ending in a newline) leaves the block:
675    /// the newline goes and an empty paragraph follows.
676    fn code_enter(&mut self, range: ResolvedSelection) -> Result<bool, CommandError> {
677        self.transact(EditOrigin::Command("code_enter"), |tx| {
678            let pos = if range.is_collapsed() {
679                range.start
680            } else {
681                delete_range(tx, range)?
682            };
683            let Some(code) = tx.doc.plain(pos.block) else {
684                return Err(CommandError::NotEditable(pos.block));
685            };
686            if pos.offset == code.len() && code.ends_with('\n') {
687                tx.replace_plain(pos.block, pos.offset - 1..pos.offset, "")?;
688                return Ok(caret(exit_below(tx, pos.block)?));
689            }
690            tx.replace_plain(pos.block, pos.offset..pos.offset, "\n")?;
691            Ok(caret(BlockPosition::new(pos.block, pos.offset + 1)))
692        })
693    }
694
695    /// Arrow down past the end of a code block that ends the document (or
696    /// its container): add an empty paragraph after it and move there.
697    pub(crate) fn exit_code_block(&mut self) -> Result<bool, CommandError> {
698        let Some(pos) = self.caret() else {
699            return Ok(false);
700        };
701        if self.doc.plain(pos.block).is_none() {
702            return Ok(false);
703        }
704        self.transact(EditOrigin::Command("exit_code_block"), |tx| {
705            Ok(caret(exit_below(tx, pos.block)?))
706        })
707    }
708
709    /// Turn the caret's paragraph into a code block holding its text (or
710    /// insert an empty code block when the caret is elsewhere).
711    fn set_code_block(&mut self, lang: Option<String>) -> Result<bool, CommandError> {
712        self.set_code_block_info(lang, None)
713    }
714    pub(crate) fn set_code_block_info(
715        &mut self,
716        lang: Option<String>,
717        meta: Option<String>,
718    ) -> Result<bool, CommandError> {
719        let pos = self
720            .caret()
721            .ok_or(CommandError::Unsupported("block selection"))?;
722        let lang = lang.filter(|l| !l.is_empty()).map(SharedString::from);
723        let meta = meta.filter(|m| !m.is_empty()).map(SharedString::from);
724        self.transact(EditOrigin::Command("code_block"), |tx| {
725            let doc = &*tx.doc;
726            let (parent, index) = doc
727                .index_in_parent(pos.block)
728                .ok_or(PositionError::UnknownBlock(pos.block))?;
729            let (text, at) = match doc.kind(pos.block) {
730                Some(BlockKindData::Paragraph | BlockKindData::Heading { .. }) => {
731                    let text = doc
732                        .inline(pos.block)
733                        .map(|i| i.text().replace(super::inline::ATOM_CHAR, ""))
734                        .unwrap_or_default();
735                    tx.remove_block(pos.block)?;
736                    (text, index)
737                }
738                Some(BlockKindData::CodeBlock { meta: old_meta, .. }) => {
739                    let meta = meta.or_else(|| old_meta.clone());
740                    tx.set_kind(pos.block, BlockKindData::CodeBlock { lang, meta })?;
741                    return Ok(caret(pos));
742                }
743                _ => (String::new(), index + 1),
744            };
745            let offset = pos.offset.min(text.len());
746            let code = DetachedBlock::leaf(
747                BlockKindData::CodeBlock { lang, meta },
748                BlockContent::Plain(text),
749            );
750            let id = code.id;
751            tx.insert(parent, at, vec![code])?;
752            Ok(caret(BlockPosition::new(id, offset)))
753        })
754    }
755
756    /// Tab / Shift+Tab in a code block: indent or outdent the caret's line by
757    /// four spaces.
758    fn code_indent(&mut self, pos: BlockPosition, outdent: bool) -> Result<bool, CommandError> {
759        let Some(code) = self.doc.plain(pos.block) else {
760            return Ok(false);
761        };
762        let line_start = code[..pos.offset].rfind('\n').map_or(0, |i| i + 1);
763        if !outdent {
764            return self.transact(EditOrigin::Command("indent"), |tx| {
765                tx.replace_plain(pos.block, pos.offset..pos.offset, "    ")?;
766                Ok(caret(BlockPosition::new(pos.block, pos.offset + 4)))
767            });
768        }
769        let spaces = code[line_start..]
770            .bytes()
771            .take(4)
772            .take_while(|b| *b == b' ')
773            .count();
774        if spaces == 0 {
775            return Ok(false);
776        }
777        self.transact(EditOrigin::Command("outdent"), |tx| {
778            tx.replace_plain(pos.block, line_start..line_start + spaces, "")?;
779            let offset = pos.offset.saturating_sub(spaces).max(line_start);
780            Ok(caret(BlockPosition::new(pos.block, offset)))
781        })
782    }
783
784    /// Enter in an empty list item whose only child is the caret's empty
785    /// paragraph: a nested item outdents one level, a top-level item becomes
786    /// a paragraph after the list (splitting it when mid-list). One
787    /// transaction, so one undo step. Returns `false` when it does not apply.
788    /// Enter on an empty paragraph that is a trailing child of a list item
789    /// or any child of a blockquote (D16): lift it out, like an empty item.
790    fn lift_empty_trailing(&mut self, pos: BlockPosition) -> Result<bool, CommandError> {
791        if self.lift_item_trailing(pos, true)? {
792            return Ok(true);
793        }
794        self.exit_quote(pos)
795    }
796
797    fn empty_paragraph_at(&self, pos: BlockPosition) -> bool {
798        self.doc.kind(pos.block) == Some(&BlockKindData::Paragraph)
799            && self.doc.inline(pos.block).is_some_and(|i| i.is_empty())
800    }
801
802    /// An (empty, when `empty`) paragraph that is the last, not the first,
803    /// child of a list item becomes an empty item of its own after that
804    /// item. From there Enter / Shift+Tab outdent and leave the list as
805    /// for any empty item.
806    fn lift_item_trailing(
807        &mut self,
808        pos: BlockPosition,
809        empty: bool,
810    ) -> Result<bool, CommandError> {
811        let doc = &self.doc;
812        if doc.kind(pos.block) != Some(&BlockKindData::Paragraph)
813            || (empty && !self.empty_paragraph_at(pos))
814        {
815            return Ok(false);
816        }
817        let Some((item, index)) = doc.index_in_parent(pos.block) else {
818            return Ok(false);
819        };
820        if index == 0
821            || !doc.kind(item).is_some_and(is_list_item)
822            || index + 1 != doc.children(item).len()
823            || !self.empty_paragraph_at(pos)
824        {
825            return Ok(false);
826        }
827        let Some((list, item_index)) = doc.index_in_parent(item) else {
828            return Ok(false);
829        };
830        let checked = match doc.kind(item) {
831            Some(BlockKindData::ListItem { checked, .. }) => checked.map(|_| false),
832            _ => None,
833        };
834        let leaf = pos.block;
835        self.transact(EditOrigin::Command("lift_paragraph"), |tx| {
836            let para = tx.remove_block(leaf)?;
837            // The item was loose only because of the trailing paragraph.
838            if tx.doc.children(item).len() == 1
839                && let Some(BlockKindData::ListItem { checked, .. }) = tx.doc.kind(item).cloned()
840            {
841                tx.set_kind(
842                    item,
843                    BlockKindData::ListItem {
844                        checked,
845                        spread: false,
846                    },
847                )?;
848            }
849            let new_item = DetachedBlock::container(
850                BlockKindData::ListItem {
851                    checked,
852                    spread: false,
853                },
854                vec![para],
855            );
856            tx.insert(list, item_index + 1, vec![new_item])?;
857            Ok(caret(BlockPosition::new(leaf, 0)))
858        })?;
859        Ok(true)
860    }
861
862    /// Enter on an empty paragraph in a blockquote: the last child moves
863    /// after the quote; one in the middle splits the quote around it.
864    fn exit_quote(&mut self, pos: BlockPosition) -> Result<bool, CommandError> {
865        if !self.empty_paragraph_at(pos) {
866            return Ok(false);
867        }
868        let doc = &self.doc;
869        let Some((quote, index)) = doc.index_in_parent(pos.block) else {
870            return Ok(false);
871        };
872        if doc.kind(quote) != Some(&BlockKindData::Blockquote) {
873            return Ok(false);
874        }
875        let Some((outer, quote_index)) = doc.index_in_parent(quote) else {
876            return Ok(false);
877        };
878        let count = doc.children(quote).len();
879        let leaf = pos.block;
880        self.transact(EditOrigin::Command("exit_quote"), |tx| {
881            let rest = tx.remove(quote, index + 1..count)?;
882            let para = tx.remove_block(leaf)?;
883            let mut at = quote_index + 1;
884            if index == 0 {
885                tx.remove(outer, quote_index..quote_index + 1)?;
886                at = quote_index;
887            }
888            tx.insert(outer, at, vec![para])?;
889            if !rest.is_empty() {
890                let tail = DetachedBlock::container(BlockKindData::Blockquote, rest);
891                tx.insert(outer, at + 1, vec![tail])?;
892            }
893            Ok(caret(BlockPosition::new(leaf, 0)))
894        })?;
895        Ok(true)
896    }
897
898    /// Backspace at the start of a blockquote's first paragraph: move it
899    /// out before the quote (the quote goes when it empties).
900    fn lift_first_in_quote(&mut self, pos: BlockPosition) -> Result<bool, CommandError> {
901        let doc = &self.doc;
902        if doc.kind(pos.block) != Some(&BlockKindData::Paragraph) {
903            return Ok(false);
904        }
905        let Some((quote, 0)) = doc.index_in_parent(pos.block) else {
906            return Ok(false);
907        };
908        if doc.kind(quote) != Some(&BlockKindData::Blockquote) {
909            return Ok(false);
910        }
911        let Some((outer, quote_index)) = doc.index_in_parent(quote) else {
912            return Ok(false);
913        };
914        let only = doc.children(quote).len() == 1;
915        let leaf = pos.block;
916        self.transact(EditOrigin::Command("lift_quote"), |tx| {
917            let para = tx.remove_block(leaf)?;
918            if only {
919                tx.remove(outer, quote_index..quote_index + 1)?;
920            }
921            tx.insert(outer, quote_index, vec![para])?;
922            Ok(caret(BlockPosition::new(leaf, 0)))
923        })?;
924        Ok(true)
925    }
926
927    /// Check or uncheck the task item whose first leaf is `leaf`, keeping
928    /// the selection. One undo step.
929    pub(crate) fn set_item_checked(
930        &mut self,
931        leaf: BlockId,
932        on: bool,
933    ) -> Result<bool, CommandError> {
934        let Some((item, _)) = self.doc.index_in_parent(leaf) else {
935            return Ok(false);
936        };
937        let Some(BlockKindData::ListItem { spread, .. }) = self.doc.kind(item).cloned() else {
938            return Ok(false);
939        };
940        self.transact(EditOrigin::Command("set_task_checked"), |tx| {
941            tx.set_kind(
942                item,
943                BlockKindData::ListItem {
944                    checked: Some(on),
945                    spread,
946                },
947            )?;
948            Ok(None)
949        })
950    }
951
952    fn exit_empty_item(&mut self, pos: BlockPosition) -> Result<bool, CommandError> {
953        let doc = &self.doc;
954        if doc.kind(pos.block) != Some(&BlockKindData::Paragraph)
955            || doc
956                .inline(pos.block)
957                .is_none_or(|inline| !inline.is_empty())
958        {
959            return Ok(false);
960        }
961        let Some((item, 0)) = doc.index_in_parent(pos.block) else {
962            return Ok(false);
963        };
964        if !doc.kind(item).is_some_and(is_list_item) || doc.children(item).len() != 1 {
965            return Ok(false);
966        }
967        let Some((list, index)) = doc.index_in_parent(item) else {
968            return Ok(false);
969        };
970        let Some(BlockKindData::List { ordered, .. }) = doc.kind(list).cloned() else {
971            return Ok(false);
972        };
973        let Some((outer, list_index)) = doc.index_in_parent(list) else {
974            return Ok(false);
975        };
976        if doc.kind(outer).is_some_and(is_list_item) {
977            self.outdent()?;
978            return Ok(true);
979        }
980        let leaf = pos.block;
981        self.transact(EditOrigin::Command("exit_list"), |tx| {
982            let count = tx.doc.children(list).len();
983            let mut removed = tx.remove(list, index..count)?;
984            let rest: Vec<_> = removed.drain(1..).collect();
985            let item = removed.pop().ok_or(StepError::OutOfRange)?;
986            let DetachedContent::Children(content) = item.content else {
987                return Err(StepError::KindMismatch.into());
988            };
989            let mut at = list_index + 1;
990            if index == 0 {
991                tx.remove(outer, list_index..list_index + 1)?;
992                at = list_index;
993            }
994            let inserted = content.len();
995            tx.insert(outer, at, content)?;
996            if !rest.is_empty() {
997                let tail = DetachedBlock::container(
998                    BlockKindData::List {
999                        ordered,
1000                        start: None,
1001                    },
1002                    rest,
1003                );
1004                tx.insert(outer, at + inserted, vec![tail])?;
1005            }
1006            Ok(caret(BlockPosition::new(leaf, 0)))
1007        })?;
1008        Ok(true)
1009    }
1010
1011    /// Paste Markdown as blocks, as one undo step. Text without a line
1012    /// break, or that parses to a single paragraph, is left to plain
1013    /// insertion (`insert_text`). Otherwise the text is parsed: a
1014    /// leading paragraph merges into the text before the caret, a trailing
1015    /// one into the text after it, and the blocks between are inserted
1016    /// between the two halves. Returns whether it applied (`false` for
1017    /// single-line text or when the caret is not in an editable text block;
1018    /// the caller then inserts plain text).
1019    pub fn paste_blocks(&mut self, text: &str) -> Result<bool, CommandError> {
1020        self.paste_block_markdown(text, false)
1021    }
1022
1023    /// Paste the editor's own block copy: always parsed as blocks, even a
1024    /// single line (`# Title`).
1025    pub(crate) fn paste_blocks_markdown(&mut self, text: &str) -> Result<bool, CommandError> {
1026        self.paste_block_markdown(text, true)
1027    }
1028
1029    fn paste_block_markdown(&mut self, text: &str, own: bool) -> Result<bool, CommandError> {
1030        // Single-line text is plain unless it holds an inline atom's
1031        // Markdown (a copied image), which pastes back as the atom, or an
1032        // inline code span (`x`), which pastes back as code.
1033        if !own
1034            && !text.trim_end_matches('\n').contains('\n')
1035            && !text.contains("![")
1036            && !text.contains('`')
1037        {
1038            return Ok(false);
1039        }
1040        let Ok(parsed) =
1041            gpui_kit::base::text::editor_host::parse_markdown(text, self.doc.extensions.clone())
1042        else {
1043            return Ok(false);
1044        };
1045        let mut defs = super::import::Defs::new();
1046        super::import::collect_definitions(&parsed, &mut defs);
1047        let mut blocks = Vec::new();
1048        for node in parsed.iter() {
1049            super::import::import_block(node, &defs, &mut blocks);
1050        }
1051        // Nothing structural (one paragraph, soft line breaks only): plain
1052        // text, where each line becomes a block as typed.
1053        let has_atoms = |block: &DetachedBlock| {
1054            matches!(
1055                &block.content,
1056                DetachedContent::Leaf(BlockContent::Inline(inline))
1057                    if inline.text().contains(super::inline::ATOM_CHAR)
1058                        || inline.pieces().any(|(_, mark, _)| mark.code)
1059            )
1060        };
1061        if blocks.is_empty()
1062            || (blocks.len() == 1
1063                && blocks[0].kind == BlockKindData::Paragraph
1064                && !has_atoms(&blocks[0]))
1065        {
1066            return Ok(false);
1067        }
1068        if self.selected_blocks().is_some() {
1069            self.delete_blocks()?;
1070        }
1071        let range = self.resolved()?;
1072        let editable = |doc: &EditorDocument, pos: BlockPosition| {
1073            doc.inline(pos.block).is_some()
1074                && doc.kind(pos.block) != Some(&BlockKindData::TableCell)
1075        };
1076        if !editable(&self.doc, range.start) || !editable(&self.doc, range.end) {
1077            return Ok(false);
1078        }
1079        let paragraph_inline = |block: &DetachedBlock| match (&block.kind, &block.content) {
1080            (BlockKindData::Paragraph, DetachedContent::Leaf(BlockContent::Inline(inline))) => {
1081                Some(inline.clone())
1082            }
1083            _ => None,
1084        };
1085        self.transact(EditOrigin::Paste, |tx| {
1086            let pos = if range.is_collapsed() {
1087                range.start
1088            } else {
1089                delete_range(tx, range)?
1090            };
1091            let mut blocks = blocks;
1092            let head = blocks.first().and_then(paragraph_inline);
1093            if head.is_some() {
1094                blocks.remove(0);
1095            }
1096            let tail = if blocks.is_empty() {
1097                None
1098            } else {
1099                blocks.last().and_then(paragraph_inline)
1100            };
1101            if tail.is_some() {
1102                blocks.pop();
1103            }
1104            // Only one paragraph: plain inline insertion with its marks.
1105            if blocks.is_empty() && tail.is_none() {
1106                let inline = head.unwrap_or_default();
1107                let len = inline.len();
1108                tx.replace_text(pos.block, pos.offset..pos.offset, inline)?;
1109                return Ok(caret(BlockPosition::new(pos.block, pos.offset + len)));
1110            }
1111            let left = pos.block;
1112            let right = split_at(tx, pos)?.block;
1113            let merged_head = head.is_some();
1114            if let Some(head) = head {
1115                let at = tx.doc.leaf_len(left).unwrap_or(0);
1116                tx.replace_text(left, at..at, head)?;
1117            }
1118            let mut caret_at = BlockPosition::new(right, 0);
1119            if let Some(tail) = tail.clone() {
1120                caret_at.offset = tail.len();
1121                tx.replace_text(right, 0..0, tail)?;
1122            }
1123            let (parent, index) = tx
1124                .doc
1125                .index_in_parent(left)
1126                .ok_or(PositionError::UnknownBlock(left))?;
1127            let last = blocks.last().map(|b| b.id);
1128            tx.insert(parent, index + 1, blocks)?;
1129            // Halves that stayed empty go away.
1130            if !merged_head && tx.doc.leaf_len(left) == Some(0) {
1131                tx.remove_block(left)?;
1132            }
1133            if tail.is_none()
1134                && tx.doc.leaf_len(right) == Some(0)
1135                && let Some(last) = last
1136            {
1137                tx.remove_block(right)?;
1138                caret_at = tx.doc.last_leaf_in(last);
1139            }
1140            Ok(caret(caret_at))
1141        })?;
1142        Ok(true)
1143    }
1144
1145    /// A `BlockSelection::Blocks` as its parent and child index range.
1146    pub(crate) fn selected_blocks(&self) -> Option<(BlockId, std::ops::Range<usize>)> {
1147        let BlockSelection::Blocks { anchor, head } = &self.selection else {
1148            return None;
1149        };
1150        let (parent, a) = self.doc.index_in_parent(*anchor)?;
1151        let (_, b) = self.doc.index_in_parent(*head)?;
1152        Some((parent, a.min(b)..a.max(b) + 1))
1153    }
1154
1155    /// Remove the selected blocks; the caret goes to the block that took
1156    /// their place (an empty paragraph when nothing is left).
1157    fn delete_blocks(&mut self) -> Result<bool, CommandError> {
1158        let Some((parent, range)) = self.selected_blocks() else {
1159            return Ok(false);
1160        };
1161        self.transact(EditOrigin::Command("delete_blocks"), |tx| {
1162            let start = range.start;
1163            tx.remove(parent, range)?;
1164            let children = tx.doc.children(parent).to_vec();
1165            let pos = if let Some(next) = children.get(start) {
1166                tx.doc.first_leaf_in(*next)
1167            } else if let Some(prev) = start.checked_sub(1).and_then(|i| children.get(i)) {
1168                tx.doc.last_leaf_in(*prev)
1169            } else {
1170                let paragraph = DetachedBlock::paragraph(InlineText::new());
1171                let id = paragraph.id;
1172                tx.insert(parent, start, vec![paragraph])?;
1173                BlockPosition::new(id, 0)
1174            };
1175            Ok(caret(pos))
1176        })
1177    }
1178
1179    /// Backspace at the start of a block after a table (or Delete at the end
1180    /// of one before it) selects the table as a node instead of editing its
1181    /// cells (no in-place cell editing yet).
1182    fn select_adjacent_table(
1183        &mut self,
1184        pos: BlockPosition,
1185        forward: bool,
1186    ) -> Result<bool, CommandError> {
1187        let Some(len) = self.doc.leaf_len(pos.block) else {
1188            return Ok(false);
1189        };
1190        if self.doc.kind(pos.block) == Some(&BlockKindData::TableCell)
1191            || (forward && pos.offset < len)
1192            || (!forward && pos.offset > 0)
1193        {
1194            return Ok(false);
1195        }
1196        let leaves = self.doc.walk_leaves();
1197        let Some(ix) = leaves.iter().position(|id| *id == pos.block) else {
1198            return Ok(false);
1199        };
1200        let neighbour = if forward {
1201            leaves.get(ix + 1)
1202        } else {
1203            ix.checked_sub(1).and_then(|i| leaves.get(i))
1204        };
1205        let Some(&cell) = neighbour else {
1206            return Ok(false);
1207        };
1208        let Some(table) = self.table_of(cell) else {
1209            return Ok(false);
1210        };
1211        self.set_selection(BlockSelection::Blocks {
1212            anchor: table,
1213            head: table,
1214        })?;
1215        Ok(true)
1216    }
1217
1218    /// The table containing a cell.
1219    pub(crate) fn table_of(&self, cell: BlockId) -> Option<BlockId> {
1220        if self.doc.kind(cell) != Some(&BlockKindData::TableCell) {
1221            return None;
1222        }
1223        self.doc
1224            .path(cell)
1225            .into_iter()
1226            .rev()
1227            .find(|id| matches!(self.doc.kind(*id), Some(BlockKindData::Table { .. })))
1228    }
1229
1230    /// Run an [`EditorCommand`]; returns whether the document or selection changed.
1231    pub fn run(&mut self, command: EditorCommand) -> Result<bool, CommandError> {
1232        match command {
1233            EditorCommand::SelectAll => {
1234                let selection = BlockSelection::Text {
1235                    anchor: self.doc.first_caret(),
1236                    head: self.doc.last_caret(),
1237                };
1238                self.set_selection(selection)?;
1239                Ok(true)
1240            }
1241            EditorCommand::DeleteSelection => {
1242                if self.selected_cells().is_some() {
1243                    return self.clear_cells(EditOrigin::Command("clear_cells"));
1244                }
1245                if self.selected_blocks().is_some() {
1246                    return self.delete_blocks();
1247                }
1248                let range = self.resolved()?;
1249                if range.is_collapsed() {
1250                    return Ok(false);
1251                }
1252                self.transact(EditOrigin::Command("delete_selection"), |tx| {
1253                    Ok(caret(delete_range(tx, range)?))
1254                })
1255            }
1256            EditorCommand::ToggleMark(kind) => {
1257                if kind == MarkKind::Link {
1258                    return Err(CommandError::Unsupported("toggle link: use ApplyMark"));
1259                }
1260                let change = if self.mark_state(kind) == MarkState::On {
1261                    MarkChange::new().clear(kind)
1262                } else {
1263                    MarkChange::new().set(kind)
1264                };
1265                self.apply_mark(change)
1266            }
1267            EditorCommand::ApplyMark(change) => self.apply_mark(change),
1268            EditorCommand::SetBlockKind(kind) => self.set_block_kind(kind),
1269            EditorCommand::ToggleList { ordered } => self.toggle_list(ordered),
1270            EditorCommand::ToggleTask => {
1271                // On a paragraph the Task button makes it a task item (a
1272                // bullet list item with an empty checkbox), like Bullets.
1273                let in_item = {
1274                    let resolved = self.caret_resolved()?;
1275                    resolved.nearest(&self.doc, is_list_item).is_some()
1276                };
1277                if !in_item && !self.toggle_list(false)? {
1278                    return Ok(false);
1279                }
1280                self.toggle_task_item()
1281            }
1282            EditorCommand::SetTaskChecked(on) => self.with_item("set_task_checked", |tx, item| {
1283                if let Some(BlockKindData::ListItem { spread, .. }) = tx.doc.kind(item).cloned() {
1284                    tx.set_kind(
1285                        item,
1286                        BlockKindData::ListItem {
1287                            checked: Some(on),
1288                            spread,
1289                        },
1290                    )?;
1291                }
1292                Ok(())
1293            }),
1294            EditorCommand::Indent | EditorCommand::Outdent
1295                if self
1296                    .caret()
1297                    .is_some_and(|c| self.doc.plain(c.block).is_some()) =>
1298            {
1299                let pos = self.caret().unwrap_or_else(|| self.doc.first_caret());
1300                self.code_indent(pos, command == EditorCommand::Outdent)
1301            }
1302            EditorCommand::Indent | EditorCommand::Outdent if self.current_cell().is_some() => {
1303                self.tab_cell(command == EditorCommand::Outdent)
1304            }
1305            EditorCommand::InsertTable { rows, cols } => self.insert_table(rows, cols),
1306            EditorCommand::EditTable(edit) => self.edit_table(edit),
1307            EditorCommand::Indent => self.indent(),
1308            EditorCommand::Outdent => self.outdent(),
1309            EditorCommand::SetCodeBlock { lang } => self.set_code_block(lang),
1310            EditorCommand::MoveBlocks { down } => self.move_blocks(down),
1311            EditorCommand::InsertBlock(BlockKind::HorizontalRule) => {
1312                let pos = self
1313                    .caret()
1314                    .ok_or(CommandError::Unsupported("block selection"))?;
1315                self.transact(EditOrigin::Command("insert_block"), |tx| {
1316                    let (parent, index) = tx
1317                        .doc
1318                        .index_in_parent(pos.block)
1319                        .ok_or(PositionError::UnknownBlock(pos.block))?;
1320                    let rule = DetachedBlock::leaf(
1321                        BlockKindData::HorizontalRule,
1322                        BlockContent::Atom(super::document::AtomData::None),
1323                    );
1324                    tx.insert(parent, index + 1, vec![rule])?;
1325                    Ok(None)
1326                })
1327            }
1328            EditorCommand::InsertBlock(_) => Err(CommandError::Unsupported("insert_block kind")),
1329        }
1330    }
1331
1332    fn apply_mark(&mut self, change: MarkChange) -> Result<bool, CommandError> {
1333        let range = self.resolved()?;
1334        if range.is_collapsed() {
1335            let current = self.active_marks().0;
1336            self.stored_marks = Some(change.apply(&current));
1337            return Ok(false);
1338        }
1339        let ranges = match self.selected_cells() {
1340            Some(cells) => cells
1341                .into_iter()
1342                .flatten()
1343                .map(|c| (c, 0, self.doc.leaf_len(c).unwrap_or(0)))
1344                .collect(),
1345            None => self.text_ranges(range),
1346        };
1347        let selection = self.selection.clone();
1348        self.transact(EditOrigin::Command("toggle_mark"), |tx| {
1349            for (block, lo, hi) in ranges {
1350                let Some(inline) = tx.doc.inline(block) else {
1351                    continue;
1352                };
1353                if lo >= hi {
1354                    continue;
1355                }
1356                let mut marked = inline.slice(lo..hi);
1357                let len = marked.len();
1358                marked.map_marks(0..len, |mark| change.apply(mark));
1359                if marked != inline.slice(lo..hi) {
1360                    tx.replace_text(block, lo..hi, marked)?;
1361                }
1362            }
1363            Ok(Some(selection))
1364        })
1365    }
1366
1367    fn set_block_kind(&mut self, kind: BlockKind) -> Result<bool, CommandError> {
1368        let data = match kind {
1369            BlockKind::Paragraph => BlockKindData::Paragraph,
1370            BlockKind::Heading { level } if (1..=6).contains(&level) => {
1371                BlockKindData::Heading { level }
1372            }
1373            _ => return Err(CommandError::Unsupported("set_block_kind target")),
1374        };
1375        let range = self.resolved()?;
1376        let blocks: Vec<BlockId> = self
1377            .text_ranges(range)
1378            .into_iter()
1379            .map(|(id, _, _)| id)
1380            .filter(|id| {
1381                matches!(
1382                    self.doc.kind(*id),
1383                    Some(BlockKindData::Paragraph | BlockKindData::Heading { .. })
1384                )
1385            })
1386            .collect();
1387        self.transact(EditOrigin::Command("set_block_kind"), |tx| {
1388            for id in blocks {
1389                tx.set_kind(id, data.clone())?;
1390            }
1391            Ok(None)
1392        })
1393    }
1394
1395    fn caret_resolved(&self) -> Result<ResolvedPosition, CommandError> {
1396        let pos = self
1397            .caret()
1398            .ok_or(CommandError::Unsupported("block selection"))?;
1399        Ok(ResolvedPosition::resolve(&self.doc, pos)?)
1400    }
1401
1402    fn with_item(
1403        &mut self,
1404        name: &'static str,
1405        f: impl FnOnce(&mut TransactionBuilder<'_>, BlockId) -> Result<(), CommandError>,
1406    ) -> Result<bool, CommandError> {
1407        let resolved = self.caret_resolved()?;
1408        let Some(item) = resolved.nearest(&self.doc, is_list_item) else {
1409            return Ok(false);
1410        };
1411        self.transact(EditOrigin::Command(name), |tx| {
1412            f(tx, item)?;
1413            Ok(None)
1414        })
1415    }
1416
1417    fn toggle_task_item(&mut self) -> Result<bool, CommandError> {
1418        self.with_item("toggle_task", |tx, item| {
1419            if let Some(BlockKindData::ListItem { checked, spread }) = tx.doc.kind(item).cloned() {
1420                let checked = if checked.is_some() { None } else { Some(false) };
1421                tx.set_kind(item, BlockKindData::ListItem { checked, spread })?;
1422            }
1423            Ok(())
1424        })
1425    }
1426
1427    fn toggle_list(&mut self, ordered: bool) -> Result<bool, CommandError> {
1428        let resolved = self.caret_resolved()?;
1429        let leaf = resolved.pos.block;
1430        let item = resolved.nearest(&self.doc, is_list_item);
1431        self.transact(EditOrigin::Command("toggle_list"), |tx| {
1432            match item {
1433                Some(item) => {
1434                    let (list, index) = tx
1435                        .doc
1436                        .index_in_parent(item)
1437                        .ok_or(StepError::UnknownBlock(item))?;
1438                    let Some(BlockKindData::List {
1439                        ordered: was,
1440                        start,
1441                    }) = tx.doc.kind(list).cloned()
1442                    else {
1443                        return Ok(None);
1444                    };
1445                    if was != ordered {
1446                        tx.set_kind(list, BlockKindData::List { ordered, start })?;
1447                        return Ok(None);
1448                    }
1449                    // Lift the item out of its list, splitting the list.
1450                    let (outer, list_index) = tx
1451                        .doc
1452                        .index_in_parent(list)
1453                        .ok_or(StepError::UnknownBlock(list))?;
1454                    let count = tx.doc.children(list).len();
1455                    let after = tx.remove(list, index + 1..count)?;
1456                    if !after.is_empty() {
1457                        let rest = DetachedBlock::container(
1458                            BlockKindData::List {
1459                                ordered,
1460                                start: None,
1461                            },
1462                            after,
1463                        );
1464                        tx.insert(outer, list_index + 1, vec![rest])?;
1465                    }
1466                    let content_len = tx.doc.children(item).len();
1467                    let content = tx.remove(item, 0..content_len)?;
1468                    tx.insert(outer, list_index + 1, content)?;
1469                }
1470                None => {
1471                    let (parent, index) = tx
1472                        .doc
1473                        .index_in_parent(leaf)
1474                        .ok_or(StepError::UnknownBlock(leaf))?;
1475                    let block = tx.remove_block(leaf)?;
1476                    let list = DetachedBlock::container(
1477                        BlockKindData::List {
1478                            ordered,
1479                            start: None,
1480                        },
1481                        vec![DetachedBlock::container(
1482                            BlockKindData::ListItem {
1483                                checked: None,
1484                                spread: false,
1485                            },
1486                            vec![block],
1487                        )],
1488                    );
1489                    tx.insert(parent, index, vec![list])?;
1490                }
1491            }
1492            Ok(None)
1493        })
1494    }
1495
1496    /// The `---` input rule: the caret's (empty) paragraph becomes a
1497    /// horizontal rule followed by an empty paragraph holding the caret.
1498    pub(crate) fn paragraph_to_rule(&mut self) -> Result<bool, CommandError> {
1499        let leaf = self.caret_resolved()?.pos.block;
1500        self.transact(EditOrigin::Command("horizontal_rule"), |tx| {
1501            let (parent, index) = tx
1502                .doc
1503                .index_in_parent(leaf)
1504                .ok_or(StepError::UnknownBlock(leaf))?;
1505            let rule = DetachedBlock::leaf(
1506                BlockKindData::HorizontalRule,
1507                BlockContent::Atom(super::document::AtomData::None),
1508            );
1509            let para = DetachedBlock::paragraph(InlineText::new());
1510            let id = para.id;
1511            tx.remove_block(leaf)?;
1512            tx.insert(parent, index, vec![rule, para])?;
1513            Ok(caret(BlockPosition::new(id, 0)))
1514        })
1515    }
1516
1517    /// Wrap the caret's leaf in a blockquote (the `> ` input rule).
1518    pub(crate) fn wrap_in_blockquote(&mut self) -> Result<bool, CommandError> {
1519        let leaf = self.caret_resolved()?.pos.block;
1520        self.transact(EditOrigin::Command("blockquote"), |tx| {
1521            let (parent, index) = tx
1522                .doc
1523                .index_in_parent(leaf)
1524                .ok_or(StepError::UnknownBlock(leaf))?;
1525            let block = tx.remove_block(leaf)?;
1526            let quote = DetachedBlock::container(BlockKindData::Blockquote, vec![block]);
1527            tx.insert(parent, index, vec![quote])?;
1528            Ok(None)
1529        })
1530    }
1531
1532    fn indent(&mut self) -> Result<bool, CommandError> {
1533        let resolved = self.caret_resolved()?;
1534        let Some(item) = resolved.nearest(&self.doc, is_list_item) else {
1535            return Ok(false);
1536        };
1537        let Some((list, index)) = self.doc.index_in_parent(item) else {
1538            return Ok(false);
1539        };
1540        if index == 0 {
1541            return Ok(false);
1542        }
1543        let prev = self.doc.children(list)[index - 1];
1544        let ordered = matches!(
1545            self.doc.kind(list),
1546            Some(BlockKindData::List { ordered: true, .. })
1547        );
1548        self.transact(EditOrigin::Command("indent"), |tx| {
1549            let nested = tx
1550                .doc
1551                .children(prev)
1552                .last()
1553                .copied()
1554                .filter(|id| matches!(tx.doc.kind(*id), Some(BlockKindData::List { .. })));
1555            let block = tx.remove_block(item)?;
1556            match nested {
1557                Some(nested) => {
1558                    let len = tx.doc.children(nested).len();
1559                    tx.insert(nested, len, vec![block])?;
1560                }
1561                None => {
1562                    let len = tx.doc.children(prev).len();
1563                    let list = DetachedBlock::container(
1564                        BlockKindData::List {
1565                            ordered,
1566                            start: None,
1567                        },
1568                        vec![block],
1569                    );
1570                    tx.insert(prev, len, vec![list])?;
1571                }
1572            }
1573            Ok(None)
1574        })
1575    }
1576
1577    fn outdent(&mut self) -> Result<bool, CommandError> {
1578        // Shift+Tab on a list item's trailing empty paragraph lifts it.
1579        if let Ok(range) = self.resolved()
1580            && range.is_collapsed()
1581            && self.lift_item_trailing(range.start, true)?
1582        {
1583            return Ok(true);
1584        }
1585        let resolved = self.caret_resolved()?;
1586        let Some(item) = resolved.nearest(&self.doc, is_list_item) else {
1587            return Ok(false);
1588        };
1589        let Some((list, index)) = self.doc.index_in_parent(item) else {
1590            return Ok(false);
1591        };
1592        let Some((outer_item, _)) = self.doc.index_in_parent(list) else {
1593            return Ok(false);
1594        };
1595        if !self.doc.kind(outer_item).is_some_and(is_list_item) {
1596            // Lifting a top-level item out of its list is toggle_list.
1597            return Ok(false);
1598        }
1599        let list_kind = self.doc.kind(list).cloned();
1600        self.transact(EditOrigin::Command("outdent"), |tx| {
1601            let (outer_list, outer_index) = tx
1602                .doc
1603                .index_in_parent(outer_item)
1604                .ok_or(StepError::UnknownBlock(outer_item))?;
1605            // Following siblings become children of the outdented item.
1606            let count = tx.doc.children(list).len();
1607            let following = tx.remove(list, index + 1..count)?;
1608            let mut block = tx.remove_block(item)?;
1609            if !following.is_empty()
1610                && let (DetachedContent::Children(children), Some(kind)) =
1611                    (&mut block.content, list_kind.clone())
1612            {
1613                children.push(DetachedBlock::container(kind, following));
1614            }
1615            tx.insert(outer_list, outer_index + 1, vec![block])?;
1616            Ok(None)
1617        })
1618    }
1619
1620    /// Move the top-level `block` to the gap `to` between the document's
1621    /// top-level blocks (0 = before the first, `len` = after the last),
1622    /// counted before the move (drag to reorder, D22). Nested blocks are
1623    /// refused with `Unsupported`; a move onto its own position is a no-op.
1624    pub(crate) fn move_block_to(
1625        &mut self,
1626        block: BlockId,
1627        to: usize,
1628    ) -> Result<bool, CommandError> {
1629        let root = self.doc.root;
1630        let Some((parent, index)) = self.doc.index_in_parent(block) else {
1631            return Err(CommandError::Position(PositionError::UnknownBlock(block)));
1632        };
1633        if parent != root {
1634            return Err(CommandError::Unsupported("move a nested block"));
1635        }
1636        let len = self.doc.children(root).len();
1637        if to > len {
1638            return Err(CommandError::Unsupported("gap out of range"));
1639        }
1640        let new_index = if to > index { to - 1 } else { to };
1641        if new_index == index {
1642            return Ok(false);
1643        }
1644        let selection = BlockSelection::Blocks {
1645            anchor: block,
1646            head: block,
1647        };
1648        self.transact(EditOrigin::Command("move_block_to"), |tx| {
1649            tx.move_block(block, root, new_index)?;
1650            Ok(Some(selection))
1651        })
1652    }
1653
1654    fn move_blocks(&mut self, down: bool) -> Result<bool, CommandError> {
1655        let target = match self.selected_blocks() {
1656            Some((parent, range)) if range.len() == 1 => self.doc.children(parent)[range.start],
1657            Some(_) => return Err(CommandError::Unsupported("move several blocks")),
1658            None => {
1659                let resolved = self.caret_resolved()?;
1660                resolved
1661                    .nearest(&self.doc, is_list_item)
1662                    .unwrap_or(resolved.pos.block)
1663            }
1664        };
1665        let Some((parent, index)) = self.doc.index_in_parent(target) else {
1666            return Ok(false);
1667        };
1668        let len = self.doc.children(parent).len();
1669        let new_index = if down {
1670            if index + 1 >= len {
1671                return Ok(false);
1672            }
1673            index + 1
1674        } else {
1675            if index == 0 {
1676                return Ok(false);
1677            }
1678            index - 1
1679        };
1680        let selection = self.selection.clone();
1681        self.transact(EditOrigin::Command("move_blocks"), |tx| {
1682            tx.move_block(target, parent, new_index)?;
1683            Ok(Some(selection))
1684        })
1685    }
1686}
1687
1688/// Split the text block at `pos`; returns the caret at the start of the new
1689/// block. In a list item's first paragraph this creates a new list item.
1690fn split_at(
1691    tx: &mut TransactionBuilder<'_>,
1692    pos: BlockPosition,
1693) -> Result<BlockPosition, CommandError> {
1694    let doc = &*tx.doc;
1695    let Some(inline) = doc.inline(pos.block) else {
1696        return Err(CommandError::NotEditable(pos.block));
1697    };
1698    let kind = doc.kind(pos.block).cloned();
1699    if kind == Some(BlockKindData::TableCell) {
1700        return Err(CommandError::NotEditable(pos.block));
1701    }
1702    let tail = inline.slice(pos.offset..inline.len());
1703    let len = inline.len();
1704    let (parent, index) = doc
1705        .index_in_parent(pos.block)
1706        .ok_or(PositionError::UnknownBlock(pos.block))?;
1707    let new_kind = match kind {
1708        Some(BlockKindData::Heading { level }) if pos.offset < len => {
1709            BlockKindData::Heading { level }
1710        }
1711        _ => BlockKindData::Paragraph,
1712    };
1713    let parent_kind = doc.kind(parent).cloned();
1714    tx.replace_text(pos.block, pos.offset..len, InlineText::new())?;
1715    let block = DetachedBlock::leaf(new_kind, BlockContent::Inline(tail));
1716    let id = block.id;
1717    match parent_kind {
1718        Some(BlockKindData::ListItem { checked, spread }) if index == 0 => {
1719            let item = DetachedBlock::container(
1720                BlockKindData::ListItem {
1721                    checked: checked.map(|_| false),
1722                    spread,
1723                },
1724                vec![block],
1725            );
1726            let (list, item_index) = tx
1727                .doc
1728                .index_in_parent(parent)
1729                .ok_or(StepError::UnknownBlock(parent))?;
1730            tx.insert(list, item_index + 1, vec![item])?;
1731        }
1732        _ => tx.insert(parent, index + 1, vec![block])?,
1733    }
1734    Ok(BlockPosition::new(id, 0))
1735}
1736
1737/// Insert an empty paragraph right after `block` and return its caret.
1738fn exit_below(
1739    tx: &mut TransactionBuilder<'_>,
1740    block: BlockId,
1741) -> Result<BlockPosition, CommandError> {
1742    let (parent, index) = tx
1743        .doc
1744        .index_in_parent(block)
1745        .ok_or(PositionError::UnknownBlock(block))?;
1746    let paragraph = DetachedBlock::paragraph(InlineText::new());
1747    let id = paragraph.id;
1748    tx.insert(parent, index + 1, vec![paragraph])?;
1749    Ok(BlockPosition::new(id, 0))
1750}
1751
1752/// A heading that a delete has emptied becomes a paragraph, in the same
1753/// transaction (one undo step restores both text and level).
1754fn drop_empty_heading(tx: &mut TransactionBuilder<'_>, block: BlockId) -> Result<(), CommandError> {
1755    if matches!(tx.doc.kind(block), Some(BlockKindData::Heading { .. }))
1756        && tx.doc.leaf_len(block) == Some(0)
1757    {
1758        tx.set_kind(block, BlockKindData::Paragraph)?;
1759    }
1760    Ok(())
1761}