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.edit_at(Cursor::new(ix, Part::Body, 0), |text| {
20                *text = Text::default()
21            });
22            this.doc.set_kind(ix, kind);
23            // A caret goes where the block admits one, and otherwise carries on
24            // in the block after it — a fresh one when it ends the document.
25            let part = this.doc.blocks[ix].parts().first().copied();
26            let at = match part {
27                Some(part) => Cursor::new(ix, part, 0),
28                None => {
29                    if this.doc.blocks.len() <= ix + 1 {
30                        this.doc
31                            .blocks
32                            .push(markdown::Block::new(BlockKind::Paragraph(Text::default())));
33                    }
34                    Cursor::new(ix + 1, Part::Body, 0)
35                }
36            };
37            this.selection = Selection::at(at.clamp(&this.doc));
38            vec![Delta::Spliced(Splice {
39                removed: Selection::new(
40                    Cursor::new(ix, Part::Body, 0),
41                    Cursor::new(ix, Part::Body, held),
42                ),
43                caret: Cursor::new(ix, Part::Body, 0),
44                blocks: 0,
45            })]
46        });
47    }
48
49    pub(super) fn undo(&mut self, _: &Undo, _: &mut Window, cx: &mut Context<Self>) {
50        if let Some(step) = self
51            .history
52            .undo(self.mode, &self.doc, self.selection, &self.anchors)
53        {
54            self.restore(step, cx);
55        }
56    }
57
58    pub(super) fn redo(&mut self, _: &Redo, _: &mut Window, cx: &mut Context<Self>) {
59        if let Some(step) = self
60            .history
61            .redo(self.mode, &self.doc, self.selection, &self.anchors)
62        {
63            self.restore(step, cx);
64        }
65    }
66
67    /// Put a whole moment back — document, caret and anchors together.
68    ///
69    /// The anchors come from the snapshot rather than from mapping, because a
70    /// step back is not an edit: there is no delta between here and a document
71    /// two hundred keystrokes ago.
72    pub(super) fn restore(&mut self, step: crate::history::Step, cx: &mut Context<Self>) {
73        self.doc = step.doc;
74        self.selection = step.selection.clamp(&self.doc);
75        self.caret_moved();
76        self.anchors = step.anchors;
77        if step.mode != self.mode {
78            self.mode = step.mode;
79            self.dismiss_menus();
80            cx.emit(EditorEvent::ModeChanged(step.mode));
81        }
82        cx.emit(EditorEvent::Changed);
83        cx.notify();
84    }
85
86    /// Move the caret's block, children and all, and follow it.
87    ///
88    /// Public because a gutter handle and a menu row reach the same operation
89    /// as the key does — one vocabulary, not three paths into [`Doc`].
90    pub fn move_block(&mut self, ix: usize, delta: isize, cx: &mut Context<Self>) {
91        if !self.blocks() {
92            return;
93        }
94        self.edit(EditKind::Structure, cx, |this| {
95            let caret = this.cursor();
96            let at = this.doc.subtree(ix);
97            let Some(to) = this.doc.move_block(ix, delta) else {
98                return vec![];
99            };
100            // The caret rides along, keeping its depth within the subtree
101            // that moved and its offset within its own text.
102            let block = to + caret.block.saturating_sub(ix);
103            this.selection = Selection::at(Cursor { block, ..caret }.clamp(&this.doc));
104            vec![Delta::Moved { at, to: Some(to) }]
105        });
106    }
107
108    pub fn duplicate_block(&mut self, ix: usize, cx: &mut Context<Self>) {
109        if !self.blocks() {
110            return;
111        }
112        self.edit(EditKind::Structure, cx, |this| {
113            let span = this.doc.subtree(ix);
114            let Some(copy) = this.doc.duplicate(ix) else {
115                return vec![];
116            };
117            this.selection = Selection::at(Cursor::new(copy, Part::Body, 0).clamp(&this.doc));
118            vec![Delta::Opened {
119                at: copy,
120                count: span.len(),
121            }]
122        });
123    }
124
125    pub fn remove_block(&mut self, ix: usize, cx: &mut Context<Self>) {
126        if !self.blocks() {
127            return;
128        }
129        self.edit(EditKind::Structure, cx, |this| {
130            let at = this.doc.subtree(ix);
131            this.doc.remove_block(ix);
132            this.selection =
133                Selection::at(Cursor::new(ix.saturating_sub(1), Part::Body, 0).clamp(&this.doc));
134            vec![Delta::Moved { at, to: None }]
135        });
136    }
137
138    /// Tag a fenced block with the language it holds, or `None` for plain.
139    pub fn set_language(&mut self, ix: usize, language: Option<String>, cx: &mut Context<Self>) {
140        if !self.blocks() {
141            return;
142        }
143        self.edit(EditKind::Structure, cx, |this| {
144            this.doc.set_language(ix, language);
145            vec![]
146        });
147    }
148
149    /// Turn the caret's block into `kind` — what the slash menu and the block
150    /// menu both do.
151    pub fn set_block(&mut self, ix: usize, kind: BlockKind, cx: &mut Context<Self>) {
152        if !self.blocks() {
153            return;
154        }
155        self.edit(EditKind::Structure, cx, |this| {
156            this.doc.set_kind(ix, kind);
157            this.selection = this.selection.clamp(&this.doc);
158            vec![]
159        });
160    }
161
162    /// Check or uncheck the task block at `ix`. Does nothing to a block that
163    /// is not one.
164    ///
165    /// An edit like any other: one undo step, and the caret stays where it
166    /// was, so toggling a box across the document does not move whatever is
167    /// being typed.
168    pub fn toggle_task(&mut self, ix: usize, cx: &mut Context<Self>) {
169        if !self.blocks() {
170            return;
171        }
172        let checked = match self.doc.blocks.get(ix).map(|block| &block.kind) {
173            Some(BlockKind::Task { checked, .. }) => !checked,
174            _ => return,
175        };
176        self.edit(EditKind::Structure, cx, |this| {
177            if let Some(BlockKind::Task { checked: at, .. }) =
178                this.doc.blocks.get_mut(ix).map(|block| &mut block.kind)
179            {
180                *at = checked;
181            }
182            vec![]
183        });
184        // Every other edit is made at the caret and owes it a reveal. This one
185        // is made where a press landed, and the caret can be pages away: the
186        // reveal `edit` asked for would scroll the box being checked off the
187        // screen.
188        self.reveal = false;
189    }
190
191    /// Put an empty row in table `ix` before `row` ([`Part::Cell`]
192    /// numbering) and the caret in its first cell.
193    pub fn add_row(&mut self, ix: usize, row: usize, cx: &mut Context<Self>) {
194        if !self.blocks() || !matches!(self.kind_at(ix), Some(BlockKind::Table { .. })) {
195            return;
196        }
197        self.edit(EditKind::Structure, cx, |this| {
198            this.doc.insert_row(ix, row);
199            let row = row.max(1);
200            this.selection = Selection::at(Cursor::new(ix, Part::Cell { row, column: 0 }, 0));
201            vec![Delta::Cells {
202                block: ix,
203                row: Some(row),
204                column: None,
205            }]
206        });
207    }
208
209    /// Put an empty column in table `ix` before `column` and the caret in it,
210    /// on the caret's row when the caret is in this table.
211    pub fn add_column(&mut self, ix: usize, column: usize, cx: &mut Context<Self>) {
212        if !self.blocks() || !matches!(self.kind_at(ix), Some(BlockKind::Table { .. })) {
213            return;
214        }
215        let row = match self.cursor() {
216            Cursor {
217                block,
218                part: Part::Cell { row, .. },
219                ..
220            } if block == ix => row,
221            _ => self.doc.blocks[ix]
222                .parts()
223                .first()
224                .map_or(0, |part| match part {
225                    Part::Cell { row, .. } => *row,
226                    _ => 0,
227                }),
228        };
229        self.edit(EditKind::Structure, cx, |this| {
230            this.doc.insert_column(ix, column);
231            this.selection = Selection::at(Cursor::new(ix, Part::Cell { row, column }, 0));
232            vec![Delta::Cells {
233                block: ix,
234                row: None,
235                column: Some(column),
236            }]
237        });
238    }
239
240    /// Tab and Shift-Tab in a table: the next or previous cell, with Tab in
241    /// the last one adding a row. `false` when the caret is not in a cell.
242    pub(super) fn step_cell(&mut self, forward: bool, cx: &mut Context<Self>) -> bool {
243        let at = self.cursor();
244        let Part::Cell { row, .. } = at.part else {
245            return false;
246        };
247        let parts = self.doc.blocks[at.block].parts();
248        let Some(here) = parts.iter().position(|part| *part == at.part) else {
249            return false;
250        };
251        let next = match forward {
252            true => parts.get(here + 1),
253            false => here.checked_sub(1).and_then(|ix| parts.get(ix)),
254        };
255        match next {
256            Some(part) => {
257                let end = Cursor::new(at.block, *part, usize::MAX).clamp(&self.doc);
258                self.head_to(end, false);
259                cx.notify();
260            }
261            None if forward => self.add_row(at.block, row + 1, cx),
262            None => {}
263        }
264        true
265    }
266
267    /// Enter in a table: the same column one row down, adding a row below the
268    /// last. `false` when the caret is not in a cell.
269    pub(super) fn cell_down(&mut self, cx: &mut Context<Self>) -> bool {
270        let at = self.cursor();
271        let Part::Cell { row, column } = at.part else {
272            return false;
273        };
274        let below = Part::Cell {
275            row: row + 1,
276            column,
277        };
278        if self.doc.blocks[at.block].text_at(below).is_none() {
279            self.add_row(at.block, row + 1, cx);
280            if column > 0 {
281                self.head_to(Cursor::new(at.block, below, 0).clamp(&self.doc), false);
282            }
283            return true;
284        }
285        let end = Cursor::new(at.block, below, usize::MAX).clamp(&self.doc);
286        self.head_to(end, false);
287        cx.notify();
288        true
289    }
290
291    /// Take body row `row` out of table `ix`. Does nothing to the header row.
292    pub fn remove_row(&mut self, ix: usize, row: usize, cx: &mut Context<Self>) {
293        self.remove_line(ix, Line::Row(row), cx);
294    }
295
296    /// Take `column` out of table `ix`. Does nothing to the last column.
297    pub fn remove_column(&mut self, ix: usize, column: usize, cx: &mut Context<Self>) {
298        self.remove_line(ix, Line::Column(column), cx);
299    }
300
301    fn remove_line(&mut self, ix: usize, line: Line, cx: &mut Context<Self>) {
302        if !self.blocks() {
303            return;
304        }
305        let Some(BlockKind::Table { header, rows, .. }) = self.kind_at(ix) else {
306            return;
307        };
308        let width = rows
309            .iter()
310            .map(Vec::len)
311            .chain([header.len()])
312            .max()
313            .unwrap_or(0);
314        let removable = match line {
315            Line::Row(row) => (1..=rows.len()).contains(&row),
316            Line::Column(column) => width > 1 && column < width,
317        };
318        if !removable {
319            return;
320        }
321        self.edit(EditKind::Structure, cx, |this| {
322            match line {
323                Line::Row(row) => this.doc.remove_row(ix, row),
324                Line::Column(column) => this.doc.remove_column(ix, column),
325            };
326            this.selection = this.selection.clamp(&this.doc);
327            let (row, column) = match line {
328                Line::Row(row) => (Some(row), None),
329                Line::Column(column) => (None, Some(column)),
330            };
331            vec![Delta::CellsRemoved {
332                block: ix,
333                row,
334                column,
335            }]
336        });
337    }
338
339    fn kind_at(&self, ix: usize) -> Option<&BlockKind> {
340        self.doc.blocks.get(ix).map(|block| &block.kind)
341    }
342}