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 SetTaskChecked(bool),
29 Indent,
30 Outdent,
31 MoveBlocks {
32 down: bool,
33 },
34 InsertBlock(BlockKind),
35 SetCodeBlock {
38 lang: Option<String>,
39 },
40 InsertTable {
43 rows: usize,
44 cols: usize,
45 },
46 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 NotEditable(BlockId),
60 Unsupported(&'static str),
61 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
83pub(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 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
121fn 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 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 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 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 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 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 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 pub(crate) fn text_ranges(&self, range: ResolvedSelection) -> Vec<(BlockId, usize, usize)> {
243 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 pub fn insert_text(&mut self, text: &str, origin: EditOrigin) -> Result<(), CommandError> {
277 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 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 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 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 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 pub fn delete(&mut self, forward: bool) -> Result<(), CommandError> {
452 self.delete_as(forward, false)
453 }
454
455 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 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 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 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 (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 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 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 (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 _ => {
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 pub fn split_block(&mut self) -> Result<(), CommandError> {
628 let range = self.resolved()?;
629 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 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 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 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 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 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 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 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 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 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 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 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 pub fn paste_blocks(&mut self, text: &str) -> Result<bool, CommandError> {
1020 self.paste_block_markdown(text, false)
1021 }
1022
1023 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 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 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 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 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 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 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 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 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 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 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(¤t));
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 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 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 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 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 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 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 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
1688fn 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
1737fn 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
1752fn 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}