Skip to main content

editor/editor/
blocks.rs

1//! Whole-block operations and undo.
2
3use super::*;
4
5impl Editor {
6    /// Give a block over to the link it holds — a card, or the picture it
7    /// points at.
8    ///
9    /// One step, not two: turning the block and giving the caret somewhere to
10    /// go are one gesture, and undo has to agree.
11    pub(super) fn turn_into(&mut self, ix: usize, kind: BlockKind, cx: &mut Context<Self>) {
12        self.edit(EditKind::Structure, cx, |this| {
13            // The URL is the block's whole text and the new block shows it
14            // already, so [`Doc::set_kind`] is given nothing to carry across —
15            // otherwise a picture prints its own URL as its caption.
16            let held = this.doc.blocks[ix]
17                .text_at(Part::Body)
18                .map_or(0, |text| text.text.len());
19            this.doc
20                .edit()
21                .edit_at(Cursor::new(ix, Part::Body, 0), |text| {
22                    *text = Text::default()
23                });
24            this.doc.edit().set_kind(ix, kind);
25            // A caret goes where the block admits one, and otherwise carries on
26            // in the block after it — a fresh one when it ends the document.
27            let part = this.doc.blocks[ix].parts().first().copied();
28            let at = match part {
29                Some(part) => Cursor::new(ix, part, 0),
30                None => {
31                    if this.doc.blocks.len() <= ix + 1 {
32                        this.doc
33                            .edit()
34                            .blocks
35                            .push(markdown::Block::new(BlockKind::Paragraph(Text::default())));
36                    }
37                    Cursor::new(ix + 1, Part::Body, 0)
38                }
39            };
40            this.selection = Selection::at(at.clamp(&this.doc));
41            vec![Delta::Spliced(Splice {
42                removed: Selection::new(
43                    Cursor::new(ix, Part::Body, 0),
44                    Cursor::new(ix, Part::Body, held),
45                ),
46                caret: Cursor::new(ix, Part::Body, 0),
47                blocks: 0,
48            })]
49        });
50    }
51
52    pub(super) fn undo(&mut self, _: &Undo, _: &mut Window, cx: &mut Context<Self>) {
53        let (mode, selection) = (self.mode, self.selection);
54        let step = self.document.update(cx, |document, _| {
55            document
56                .history
57                .undo(mode, &self.doc, selection, &self.anchors)
58        });
59        if let Some(step) = step {
60            self.restore(step, cx);
61        }
62    }
63
64    pub(super) fn redo(&mut self, _: &Redo, _: &mut Window, cx: &mut Context<Self>) {
65        let (mode, selection) = (self.mode, self.selection);
66        let step = self.document.update(cx, |document, _| {
67            document
68                .history
69                .redo(mode, &self.doc, selection, &self.anchors)
70        });
71        if let Some(step) = step {
72            self.restore(step, cx);
73        }
74    }
75
76    /// Put a whole moment back — document, caret and anchors together.
77    ///
78    /// The anchors come from the snapshot rather than from mapping, because a
79    /// step back is not an edit: there is no delta between here and a document
80    /// two hundred keystrokes ago.
81    pub(super) fn restore(&mut self, step: crate::history::Step, cx: &mut Context<Self>) {
82        self.doc.set(step.doc);
83        self.selection = step.selection.clamp(&self.doc);
84        self.run.interrupt();
85        self.caret_moved();
86        self.anchors = step.anchors;
87        let switched = step.mode != self.mode;
88        self.mode = step.mode;
89        self.publish(Change::Replaced, cx);
90        if switched {
91            self.dismiss_menus();
92            cx.emit(EditorEvent::ModeChanged(step.mode));
93        }
94        cx.emit(EditorEvent::Changed);
95        cx.notify();
96    }
97
98    /// Move the caret's block, children and all, and follow it.
99    ///
100    /// Public because a gutter handle and a menu row reach the same operation
101    /// as the key does — one vocabulary, not three paths into [`Doc`].
102    pub fn move_block(&mut self, ix: usize, delta: isize, cx: &mut Context<Self>) {
103        if !self.blocks() {
104            return;
105        }
106        self.edit(EditKind::Structure, cx, |this| {
107            let caret = this.cursor();
108            let at = this.doc.subtree(ix);
109            let Some(to) = this.doc.edit().move_block(ix, delta) else {
110                return vec![];
111            };
112            // The caret rides along, keeping its depth within the subtree
113            // that moved and its offset within its own text.
114            let block = to + caret.block.saturating_sub(ix);
115            this.selection = Selection::at(Cursor { block, ..caret }.clamp(&this.doc));
116            vec![Delta::Moved { at, to: Some(to) }]
117        });
118    }
119
120    pub fn duplicate_block(&mut self, ix: usize, cx: &mut Context<Self>) {
121        if !self.blocks() {
122            return;
123        }
124        self.edit(EditKind::Structure, cx, |this| {
125            let span = this.doc.subtree(ix);
126            let Some(copy) = this.doc.edit().duplicate(ix) else {
127                return vec![];
128            };
129            this.selection = Selection::at(Cursor::new(copy, Part::Body, 0).clamp(&this.doc));
130            vec![Delta::Opened {
131                at: copy,
132                count: span.len(),
133            }]
134        });
135    }
136
137    pub fn remove_block(&mut self, ix: usize, cx: &mut Context<Self>) {
138        if !self.blocks() {
139            return;
140        }
141        self.edit(EditKind::Structure, cx, |this| {
142            let at = this.doc.subtree(ix);
143            this.doc.edit().remove_block(ix);
144            this.selection =
145                Selection::at(Cursor::new(ix.saturating_sub(1), Part::Body, 0).clamp(&this.doc));
146            vec![Delta::Moved { at, to: None }]
147        });
148    }
149
150    /// Tag a fenced block with the language it holds, or `None` for plain.
151    pub fn set_language(&mut self, ix: usize, language: Option<String>, cx: &mut Context<Self>) {
152        if !self.blocks() {
153            return;
154        }
155        self.edit(EditKind::Structure, cx, |this| {
156            this.doc.edit().set_language(ix, language);
157            vec![]
158        });
159    }
160
161    /// Turn the caret's block into `kind` — what the slash menu and the block
162    /// menu both do.
163    pub fn set_block(&mut self, ix: usize, kind: BlockKind, cx: &mut Context<Self>) {
164        if !self.blocks() {
165            return;
166        }
167        self.edit(EditKind::Structure, cx, |this| {
168            this.doc.edit().set_kind(ix, kind);
169            this.selection = this.selection.clamp(&this.doc);
170            vec![]
171        });
172    }
173
174    /// Put the block at `ix` in a quote with `alert`, or take it out of the one
175    /// it is in — see [`markdown::Doc::toggle_quote`].
176    pub fn toggle_quote(
177        &mut self,
178        ix: usize,
179        alert: Option<markdown::QuoteKind>,
180        cx: &mut Context<Self>,
181    ) {
182        if !self.blocks() {
183            return;
184        }
185        self.edit(EditKind::Structure, cx, |this| {
186            this.doc.edit().toggle_quote(ix, alert);
187            this.selection = this.selection.clamp(&this.doc);
188            vec![]
189        });
190    }
191
192    /// Replace a fenced block's code, as one undo step — what a painted fence
193    /// writes through [`markdown::Fence::rewrite`].
194    pub fn set_code(&mut self, ix: usize, code: String, cx: &mut Context<Self>) {
195        if !self.blocks() {
196            return;
197        }
198        self.edit(EditKind::Structure, cx, |this| {
199            this.doc.edit().set_code(ix, code);
200            this.selection = this.selection.clamp(&this.doc);
201            vec![]
202        });
203    }
204
205    /// Turn the block at `ix` into `kind` and put the caret after it — what a
206    /// [`crate::SlashAction::Run`] row does with a block the caret cannot sit
207    /// in.
208    /// The block is replaced as given, not converted: nothing of what it held
209    /// carries over.
210    pub fn place_block(&mut self, ix: usize, kind: BlockKind, cx: &mut Context<Self>) {
211        if !self.blocks() || ix >= self.doc.blocks.len() {
212            return;
213        }
214        self.edit(EditKind::Structure, cx, |this| {
215            this.doc.edit().blocks[ix].kind = kind;
216            this.selection = this.selection.clamp(&this.doc);
217            vec![]
218        });
219        self.step_past(ix, cx);
220    }
221
222    /// Put the caret at the start of the block after `ix`, making an empty
223    /// paragraph when `ix` ends the document.
224    pub(super) fn step_past(&mut self, ix: usize, cx: &mut Context<Self>) {
225        if self.doc.blocks.len() <= ix + 1 {
226            self.edit(EditKind::Structure, cx, |this| {
227                this.doc
228                    .edit()
229                    .blocks
230                    .push(markdown::Block::new(BlockKind::Paragraph(Text::default())));
231                vec![]
232            });
233        }
234        let at = Cursor::new(ix + 1, Part::Body, 0).clamp(&self.doc);
235        self.select(Selection::at(at), cx);
236    }
237
238    /// Focus back from inside the painted fence at `ix`, the caret after it.
239    pub(super) fn leave_block(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
240        self.focus_handle.focus(window, cx);
241        self.step_past(ix, cx);
242    }
243
244    /// Point every picture at `from` to `to`: an image block's URL, or in
245    /// source mode each `](from)` and `](<from>)` in the text. One undo step,
246    /// and the caret keeps its place in the text around it. Nothing pointing
247    /// at `from` is no edit.
248    pub fn relink(&mut self, from: &str, to: &str, cx: &mut Context<Self>) {
249        if self.blocks() {
250            let found: Vec<(usize, BlockKind)> = self
251                .doc
252                .blocks
253                .iter()
254                .enumerate()
255                .filter_map(|(ix, block)| match &block.kind {
256                    BlockKind::Image { url, alt, width } if url == from => Some((
257                        ix,
258                        BlockKind::Image {
259                            url: to.to_owned(),
260                            alt: alt.clone(),
261                            width: *width,
262                        },
263                    )),
264                    _ => None,
265                })
266                .collect();
267            if found.is_empty() {
268                return;
269            }
270            self.edit(EditKind::Structure, cx, |this| {
271                for (ix, kind) in found {
272                    this.doc.edit().set_kind(ix, kind);
273                }
274                this.selection = this.selection.clamp(&this.doc);
275                vec![]
276            });
277            return;
278        }
279        let source = self.source_text();
280        let mut hits: Vec<(Range<usize>, String)> = [
281            (format!("]({from})"), format!("]({to})")),
282            (format!("](<{from}>)"), format!("](<{to}>)")),
283        ]
284        .into_iter()
285        .flat_map(|(old, new)| {
286            source
287                .match_indices(&old)
288                .map(|(at, _)| (at..at + old.len(), new.clone()))
289                .collect::<Vec<_>>()
290        })
291        .collect();
292        if hits.is_empty() {
293            return;
294        }
295        // Last first, so each splice leaves the offsets before it standing.
296        hits.sort_by_key(|(range, _)| std::cmp::Reverse(range.start));
297        let moved = |cursor: Cursor| {
298            let offset = hits
299                .iter()
300                .fold(cursor.offset as isize, |at, (range, new)| {
301                    if cursor.offset >= range.end {
302                        at + new.len() as isize - range.len() as isize
303                    } else if cursor.offset > range.start {
304                        at - (cursor.offset - range.start) as isize
305                    } else {
306                        at
307                    }
308                });
309            Cursor {
310                offset: offset.max(0) as usize,
311                ..cursor
312            }
313        };
314        let selection = Selection::new(moved(self.selection.anchor), moved(self.selection.head));
315        self.edit(EditKind::Structure, cx, |this| {
316            let at = |offset| Cursor::new(0, Part::Code, offset);
317            let splices = hits
318                .iter()
319                .map(|(range, new)| {
320                    Delta::Spliced(this.doc.edit().replace(
321                        Selection::new(at(range.start), at(range.end)),
322                        Text::plain(new.as_str()),
323                    ))
324                })
325                .collect();
326            this.selection = selection.clamp(&this.doc);
327            splices
328        });
329    }
330
331    /// Check or uncheck the task block at `ix`. Does nothing to a block that
332    /// is not one.
333    ///
334    /// An edit like any other: one undo step, and the caret stays where it
335    /// was, so toggling a box across the document does not move whatever is
336    /// being typed.
337    pub fn toggle_task(&mut self, ix: usize, cx: &mut Context<Self>) {
338        if !self.blocks() {
339            return;
340        }
341        let checked = match self.doc.blocks.get(ix).map(|block| &block.kind) {
342            Some(BlockKind::Task { checked, .. }) => !checked,
343            _ => return,
344        };
345        self.edit(EditKind::Structure, cx, |this| {
346            if let Some(BlockKind::Task { checked: at, .. }) = this
347                .doc
348                .edit()
349                .blocks
350                .get_mut(ix)
351                .map(|block| &mut block.kind)
352            {
353                *at = checked;
354            }
355            vec![]
356        });
357        // Every other edit is made at the caret and owes it a reveal. This one
358        // is made where a press landed, and the caret can be pages away: the
359        // reveal `edit` asked for would scroll the box being checked off the
360        // screen.
361        self.reveal = None;
362    }
363
364    /// Put an empty row in table `ix` before `row` ([`Part::Cell`]
365    /// numbering) and the caret in its first cell.
366    pub fn add_row(&mut self, ix: usize, row: usize, cx: &mut Context<Self>) {
367        if !self.blocks() || !matches!(self.kind_at(ix), Some(BlockKind::Table { .. })) {
368            return;
369        }
370        self.edit(EditKind::Structure, cx, |this| {
371            this.doc.edit().insert_row(ix, row);
372            let row = row.max(1);
373            this.selection = Selection::at(Cursor::new(ix, Part::Cell { row, column: 0 }, 0));
374            vec![Delta::Cells {
375                block: ix,
376                row: Some(row),
377                column: None,
378            }]
379        });
380    }
381
382    /// Put an empty column in table `ix` before `column` and the caret in it,
383    /// on the caret's row when the caret is in this table.
384    pub fn add_column(&mut self, ix: usize, column: usize, cx: &mut Context<Self>) {
385        if !self.blocks() || !matches!(self.kind_at(ix), Some(BlockKind::Table { .. })) {
386            return;
387        }
388        let row = match self.cursor() {
389            Cursor {
390                block,
391                part: Part::Cell { row, .. },
392                ..
393            } if block == ix => row,
394            _ => self.doc.blocks[ix]
395                .parts()
396                .first()
397                .map_or(0, |part| match part {
398                    Part::Cell { row, .. } => *row,
399                    _ => 0,
400                }),
401        };
402        self.edit(EditKind::Structure, cx, |this| {
403            this.doc.edit().insert_column(ix, column);
404            this.selection = Selection::at(Cursor::new(ix, Part::Cell { row, column }, 0));
405            vec![Delta::Cells {
406                block: ix,
407                row: None,
408                column: Some(column),
409            }]
410        });
411    }
412
413    /// Tab and Shift-Tab in a table: the next or previous cell, with Tab in
414    /// the last one adding a row. `false` when the caret is not in a cell.
415    pub(super) fn step_cell(&mut self, forward: bool, cx: &mut Context<Self>) -> bool {
416        let at = self.cursor();
417        let Part::Cell { row, .. } = at.part else {
418            return false;
419        };
420        let parts = self.doc.blocks[at.block].parts();
421        let Some(here) = parts.iter().position(|part| *part == at.part) else {
422            return false;
423        };
424        let next = match forward {
425            true => parts.get(here + 1),
426            false => here.checked_sub(1).and_then(|ix| parts.get(ix)),
427        };
428        match next {
429            Some(part) => {
430                let end = Cursor::new(at.block, *part, usize::MAX).clamp(&self.doc);
431                self.head_to(end, false);
432                cx.notify();
433            }
434            None if forward => self.add_row(at.block, row + 1, cx),
435            None => {}
436        }
437        true
438    }
439
440    /// Enter in a table: the same column one row down, adding a row below the
441    /// last. `false` when the caret is not in a cell.
442    pub(super) fn cell_down(&mut self, cx: &mut Context<Self>) -> bool {
443        let at = self.cursor();
444        let Part::Cell { row, column } = at.part else {
445            return false;
446        };
447        let below = Part::Cell {
448            row: row + 1,
449            column,
450        };
451        if self.doc.blocks[at.block].text_at(below).is_none() {
452            self.add_row(at.block, row + 1, cx);
453            if column > 0 {
454                self.head_to(Cursor::new(at.block, below, 0).clamp(&self.doc), false);
455            }
456            return true;
457        }
458        let end = Cursor::new(at.block, below, usize::MAX).clamp(&self.doc);
459        self.head_to(end, false);
460        cx.notify();
461        true
462    }
463
464    /// Take body row `row` out of table `ix`. Does nothing to the header row.
465    pub fn remove_row(&mut self, ix: usize, row: usize, cx: &mut Context<Self>) {
466        self.remove_line(ix, Line::Row(row), cx);
467    }
468
469    /// Take `column` out of table `ix`. Does nothing to the last column.
470    pub fn remove_column(&mut self, ix: usize, column: usize, cx: &mut Context<Self>) {
471        self.remove_line(ix, Line::Column(column), cx);
472    }
473
474    fn remove_line(&mut self, ix: usize, line: Line, cx: &mut Context<Self>) {
475        if !self.blocks() {
476            return;
477        }
478        let Some(BlockKind::Table { header, rows, .. }) = self.kind_at(ix) else {
479            return;
480        };
481        let width = rows
482            .iter()
483            .map(Vec::len)
484            .chain([header.len()])
485            .max()
486            .unwrap_or(0);
487        let removable = match line {
488            Line::Row(row) => (1..=rows.len()).contains(&row),
489            Line::Column(column) => width > 1 && column < width,
490        };
491        if !removable {
492            return;
493        }
494        self.edit(EditKind::Structure, cx, |this| {
495            match line {
496                Line::Row(row) => this.doc.edit().remove_row(ix, row),
497                Line::Column(column) => this.doc.edit().remove_column(ix, column),
498            };
499            this.selection = this.selection.clamp(&this.doc);
500            let (row, column) = match line {
501                Line::Row(row) => (Some(row), None),
502                Line::Column(column) => (None, Some(column)),
503            };
504            vec![Delta::CellsRemoved {
505                block: ix,
506                row,
507                column,
508            }]
509        });
510    }
511
512    fn kind_at(&self, ix: usize) -> Option<&BlockKind> {
513        self.doc.blocks.get(ix).map(|block| &block.kind)
514    }
515}