Skip to main content

editor/editor/
typing.rs

1//! Typing, deletion, block splits, indent and text size.
2
3use super::*;
4
5impl Editor {
6    /// Replace whatever is selected with `text`, applying a markdown prefix if
7    /// one completes.
8    ///
9    /// Typing, backspace, delete and IME all land here, so none of them has to
10    /// ask whether a selection was empty.
11    pub(super) fn insert(&mut self, text: &str, cx: &mut Context<Self>) {
12        let painter = Painter::of(cx);
13        self.edit(EditKind::Insert, cx, |this| {
14            let mut typed = Text::plain(text);
15            // A stored mark applies to what is typed next and to nothing else,
16            // so it is spent here.
17            for mark in this.stored.drain(..) {
18                typed.marks.push(markdown::MarkSpan {
19                    range: 0..typed.text.len(),
20                    mark,
21                });
22            }
23            let splice = this.doc.replace(this.selection, typed);
24            this.selection = Selection::at(splice.caret.clamp(&this.doc));
25            let shortcut = this.apply_shortcut();
26            let promoted = this.promote_quote_marker();
27            let inline = this.apply_inline_rule();
28            this.track_slash(text, painter);
29            std::iter::once(Delta::Spliced(splice))
30                .chain(shortcut)
31                .chain(promoted)
32                .chain(inline)
33                .collect()
34        });
35    }
36
37    /// Open the menu on a typed `/`, and keep its query in step afterwards.
38    ///
39    /// The query is the text between the `/` and the caret, so there is no
40    /// second field and no focus to hand over — typing filters because typing
41    /// is what it already was.
42    pub(super) fn track_slash(&mut self, typed: &str, painter: Painter) {
43        if !self.chrome.slash {
44            return;
45        }
46        let at = self.cursor();
47        let text = self
48            .doc
49            .blocks
50            .get(at.block)
51            .and_then(|block| block.text_at(at.part))
52            .map(|text| text.text.clone())
53            .unwrap_or_default();
54
55        if self.slash.is_none() {
56            // Only a `/` that starts a word — a URL's slashes are not commands.
57            let opened = at.offset.checked_sub(1).filter(|_| typed == "/");
58            let starts_word = opened.is_none_or(|slash| {
59                text[..slash]
60                    .chars()
61                    .next_back()
62                    .is_none_or(char::is_whitespace)
63            });
64            // Only in a body: a fence holds its slash literally, and a caption
65            // belongs to a block that is already what it is.
66            if let Some(slash) = opened.filter(|_| starts_word && at.part == Part::Body) {
67                self.slash = Some(Slash::open(
68                    Cursor {
69                        offset: slash,
70                        ..at
71                    },
72                    painter,
73                ));
74            }
75            return;
76        }
77
78        // Anything that leaves the run — a space, a click away, backspacing
79        // onto the slash — closes it.
80        let Some(query) = self.slash.as_ref().and_then(|slash| slash.query(at, &text)) else {
81            self.slash = None;
82            return;
83        };
84        if let Some(slash) = &mut self.slash {
85            slash.refilter(&query);
86        }
87    }
88
89    /// Take the highlighted block, replacing the `/query` that summoned it.
90    /// Take `kind`, or the highlighted row when the caller names none — Enter
91    /// and a click are the same operation with a different source.
92    pub(super) fn confirm_slash(
93        &mut self,
94        kind: Option<BlockKind>,
95        cx: &mut Context<Self>,
96    ) -> bool {
97        let Some(slash) = &self.slash else {
98            return false;
99        };
100        let (at, kind) = (slash.at, kind.or_else(|| slash.choice()));
101        self.slash = None;
102        let Some(kind) = kind else {
103            return false;
104        };
105        let caret = self.cursor();
106        self.edit(EditKind::Structure, cx, |this| {
107            this.doc
108                .edit_at(at, |text| text.remove(at.offset..caret.offset));
109            this.doc.set_kind(at.block, kind);
110            this.selection =
111                Selection::at(Cursor::new(at.block, Part::Body, at.offset).clamp(&this.doc));
112            vec![Delta::Spliced(Splice {
113                removed: Selection::new(at, caret),
114                caret: at,
115                blocks: 0,
116            })]
117        });
118        true
119    }
120
121    /// Collapse `**bold**` into a mark when its closing delimiter is typed.
122    ///
123    /// Runs after the insertion, on the text as it now stands, so a paste and a
124    /// keystroke reach it the same way.
125    pub(super) fn apply_inline_rule(&mut self) -> Vec<Delta> {
126        let at = self.cursor();
127        // Code is literal to its closing fence, and a caption holds no mark a
128        // `![...]` could spell.
129        if matches!(at.part, Part::Code | Part::Caption) {
130            return Vec::new();
131        }
132        let Some(text) = self
133            .doc
134            .blocks
135            .get(at.block)
136            .and_then(|block| block.text_at(at.part))
137        else {
138            return Vec::new();
139        };
140        let Some((open, inner, mark)) = edit::inline_rule(&text.text, at.offset) else {
141            return Vec::new();
142        };
143        let width = open.len();
144        let (opening, closing) = (open.clone(), inner.end..at.offset);
145        self.doc.edit_at(at, |text| {
146            // The closing delimiter first — taking the opening one would move
147            // every offset after it.
148            text.remove(inner.end..at.offset);
149            text.remove(open);
150            text.toggle(inner.start - width..inner.end - width, mark);
151        });
152        self.selection =
153            Selection::at(Cursor::new(at.block, at.part, at.offset - 2 * width).clamp(&self.doc));
154        // In the order the two removals went. The opening delimiter is ahead of
155        // the closing one, so taking that one first left its offsets standing.
156        vec![Self::taken(at, closing), Self::taken(at, opening)]
157    }
158
159    /// Add `mark` over the selection, or take it away if the whole selection
160    /// already carries it. Public because a toolbar reaches the same operation
161    /// the key does.
162    pub fn toggle_mark(&mut self, mark: Mark, cx: &mut Context<Self>) {
163        // Nothing in the source is a mark: the markup is already spelled out,
164        // and cmd-B over `**bold**` would fence what it reads.
165        if !self.blocks() {
166            return;
167        }
168        // A caret inside a fence is enough to leave one, so this is the mark
169        // that does not wait for a range: nothing typed into code is markup,
170        // which leaves a stored mark there nothing to mean.
171        let leaving_code = matches!(mark, Mark::Code) && self.cursor().part == Part::Code;
172        // With nothing selected there is no range to mark, so the mark waits
173        // for the next character — ProseMirror's stored marks, and the only way
174        // cmd-B before typing can mean anything.
175        if self.selection.is_collapsed() && !leaving_code {
176            match self.stored.iter().position(|stored| *stored == mark) {
177                Some(ix) => drop(self.stored.remove(ix)),
178                None => self.stored.push(mark),
179            }
180            return cx.notify();
181        }
182        let selection = self.selection;
183        self.edit(EditKind::Structure, cx, |this| {
184            // Code over more than one line is a fence, which is the only shape
185            // markdown has for it, and the same key is the way back out.
186            let before = this.doc.blocks.len();
187            // Fencing and unfencing rebuild the blocks the selection covered,
188            // so what sat inside it has no position left to keep.
189            let refenced = |doc: &Doc, head: Cursor| {
190                vec![Delta::Spliced(Splice {
191                    removed: selection,
192                    caret: head,
193                    blocks: doc.blocks.len() as isize - before as isize,
194                })]
195            };
196            if matches!(mark, Mark::Code) {
197                if let Some(head) = this.doc.unfence(selection) {
198                    this.selection = Selection::at(head.clamp(&this.doc));
199                    return refenced(&this.doc, head);
200                }
201                if fenceable(&this.doc, selection) {
202                    let head = this.doc.fence(selection);
203                    this.selection = Selection::at(head.clamp(&this.doc));
204                    return refenced(&this.doc, head);
205                }
206            }
207            // A mark is paint over text that does not move.
208            this.doc.toggle_mark(selection, mark);
209            vec![]
210        });
211    }
212
213    /// Turn a typed prefix into the block it spells — `## ` into a heading.
214    ///
215    /// Runs after every insertion rather than only on space, because the
216    /// vocabulary includes prefixes that end in one (`- [ ] `) and prefixes
217    /// that do not (```` ``` ````).
218    pub(super) fn apply_shortcut(&mut self) -> Option<Delta> {
219        let at = self.cursor();
220        // A prefix is block syntax; inside a code fence or a table cell it is
221        // the literal text the author typed.
222        if at.part != Part::Body {
223            return None;
224        }
225        let text = self.doc.blocks.get(at.block)?.text_at(Part::Body)?;
226        // Only from the very start of a block, and only up to the caret: a
227        // `- ` typed in the middle of a sentence is a hyphen.
228        let (hit, len) = shortcut(&text.text)?;
229        if at.offset < len {
230            return None;
231        }
232        // Strip the prefix, then turn the block — the same two steps the slash
233        // menu takes, so a `## ` and a menu pick land in one place.
234        self.doc.edit_at(at, |text| text.remove(0..len));
235        self.doc.set_kind(at.block, hit.apply(Text::default()));
236        self.selection =
237            Selection::at(Cursor::new(at.block, Part::Body, at.offset - len).clamp(&self.doc));
238        // The transformation is its own step: undo after typing `## Title`
239        // should give back the heading, not the paragraph before the hashes.
240        self.history.interrupt();
241        Some(Self::taken(at, 0..len))
242    }
243
244    /// Promote a quote whose first line is a GFM alert marker into the alert
245    /// block kind the parser would have produced from the same markdown.
246    ///
247    /// The marker line goes the way a block shortcut's prefix does, so what was
248    /// written under it stays as the alert's body.
249    pub(super) fn promote_quote_marker(&mut self) -> Option<Delta> {
250        let at = self.cursor();
251        if at.part != Part::Body {
252            return None;
253        }
254        let block = self.doc.blocks.get(at.block)?;
255        if !matches!(block.kind, BlockKind::Quote { kind: None, .. }) {
256            return None;
257        }
258        let text = &block.text_at(Part::Body)?.text;
259        let first = text.split('\n').next().unwrap_or_default();
260        let kind = Self::quote_marker(first)?;
261        // The marker line, and the break after it where a body follows.
262        let len = first.len() + usize::from(text.len() > first.len());
263        self.doc.edit_at(at, |text| text.remove(0..len));
264        self.doc.set_kind(
265            at.block,
266            BlockKind::Quote {
267                kind: Some(kind),
268                text: Text::default(),
269            },
270        );
271        self.selection = Selection::at(
272            Cursor::new(at.block, Part::Body, at.offset.saturating_sub(len)).clamp(&self.doc),
273        );
274        self.history.interrupt();
275        Some(Self::taken(at, 0..len))
276    }
277
278    /// The delta for `range` taken out of the text `at` is in — what an anchor
279    /// sitting in that text has to move through.
280    pub(super) fn taken(at: Cursor, range: Range<usize>) -> Delta {
281        let start = Cursor::new(at.block, at.part, range.start);
282        Delta::Spliced(Splice {
283            removed: Selection {
284                anchor: start,
285                head: Cursor::new(at.block, at.part, range.end),
286            },
287            caret: start,
288            blocks: 0,
289        })
290    }
291
292    pub(super) fn backspace(&mut self, _: &Backspace, _: &mut Window, cx: &mut Context<Self>) {
293        self.delete_back(cx);
294    }
295
296    pub(super) fn quote_marker(text: &str) -> Option<markdown::QuoteKind> {
297        [
298            markdown::QuoteKind::Note,
299            markdown::QuoteKind::Tip,
300            markdown::QuoteKind::Important,
301            markdown::QuoteKind::Warning,
302            markdown::QuoteKind::Caution,
303        ]
304        .into_iter()
305        .find(|kind| text.eq_ignore_ascii_case(kind.marker()))
306    }
307
308    /// Delete backwards: the selection if there is one, otherwise the character
309    /// before the caret, otherwise whatever the start of a block means.
310    ///
311    /// The kill chords land here too when they have nothing left to take within
312    /// the block, so reaching out of one is decided in a single place.
313    pub(super) fn delete_back(&mut self, cx: &mut Context<Self>) {
314        let at = self.cursor();
315        // Reaching out of a block is structural; taking a character is not.
316        let kind = if self.selection.is_collapsed() && at.offset == 0 {
317            EditKind::Structure
318        } else {
319            EditKind::Delete
320        };
321        let painter = Painter::of(cx);
322        self.edit(kind, cx, |this| {
323            let before = this.doc.blocks.len();
324            let splice = if !this.selection.is_collapsed() {
325                this.doc.replace(this.selection, Text::default())
326            } else if at.offset > 0 {
327                this.doc
328                    .replace(Selection::new(at.left(&this.doc), at), Text::default())
329            } else {
330                // `merge_back` outdents, unmarkers, unfences or merges —
331                // whichever the block's state calls for — and says where the
332                // caret landed.
333                match this.doc.merge_back(at) {
334                    // The seam between where the caret was and where it landed
335                    // is exactly what the merge closed up.
336                    Some(head) => Splice {
337                        removed: Selection::new(head, at),
338                        caret: head,
339                        blocks: this.doc.blocks.len() as isize - before as isize,
340                    },
341                    None => return vec![],
342                }
343            };
344            let head = splice.caret;
345            this.selection = Selection::at(head.clamp(&this.doc));
346            // Deleting narrows the query too, and backspacing onto the slash
347            // itself is what closes the menu.
348            this.track_slash("", painter);
349            vec![Delta::Spliced(splice)]
350        });
351    }
352
353    pub(super) fn delete(&mut self, _: &Delete, _: &mut Window, cx: &mut Context<Self>) {
354        self.delete_forward(cx);
355    }
356
357    /// Delete forwards, joining the next block when the caret is at the end of
358    /// this one — which is what a kill to the end of a line does there too.
359    pub(super) fn delete_forward(&mut self, cx: &mut Context<Self>) {
360        self.edit(EditKind::Delete, cx, |this| {
361            let at = this.cursor();
362            let range = if this.selection.is_collapsed() {
363                Selection::new(at, at.right(&this.doc))
364            } else {
365                this.selection
366            };
367            let splice = this.doc.replace(range, Text::default());
368            this.selection = Selection::at(splice.caret.clamp(&this.doc));
369            vec![Delta::Spliced(splice)]
370        });
371    }
372
373    /// Whether an open menu answered Enter itself.
374    ///
375    /// Every Enter chord asks first, or picking a block would also edit the one
376    /// it is turning — and a chord the menu never sees leaves it open over a
377    /// query the caret has walked away from.
378    pub(super) fn menu_took_enter(&mut self, cx: &mut Context<Self>) -> bool {
379        if let Some(choice) = self.pasted.as_ref().map(link::Paste::choice) {
380            self.confirm_paste(choice, cx);
381            return true;
382        }
383        self.confirm_slash(None, cx)
384    }
385
386    /// Enter. In a body it splits the block; in a code fence it is a newline,
387    /// which is the whole reason a fence is worth typing into.
388    pub(super) fn split_block(
389        &mut self,
390        _: &SplitBlock,
391        window: &mut Window,
392        cx: &mut Context<Self>,
393    ) {
394        if self.menu_took_enter(cx) {
395            return;
396        }
397        let at = self.cursor();
398        match at.part {
399            Part::Code => return self.insert("\n", cx),
400            // A cell is one line by definition; Enter has nowhere to put a
401            // break, so it does nothing rather than something surprising.
402            Part::Cell { .. } => return,
403            // An image with nothing to show yet is missing one thing, so Enter
404            // asks for it rather than carrying on past a blank.
405            Part::Caption
406                if matches!(
407                    self.doc.blocks.get(at.block).map(|block| &block.kind),
408                    Some(BlockKind::Image { url, .. }) if url.is_empty()
409                ) =>
410            {
411                return self.prompt_for_url(at.block, window, cx);
412            }
413            // A caption is one line too, but the block it belongs to is the
414            // end of something — so Enter carries on underneath the picture,
415            // which is [`Doc::split`]'s answer for a block with no body.
416            Part::Caption | Part::Body => {}
417        }
418        self.edit(EditKind::Structure, cx, |this| {
419            let mut deltas = Vec::new();
420            if !this.selection.is_collapsed() {
421                let splice = this.doc.replace(this.selection, Text::default());
422                this.selection = Selection::at(splice.caret.clamp(&this.doc));
423                deltas.push(Delta::Spliced(splice));
424            }
425            let at = this.cursor();
426            let new = this.doc.split(at.block, at.offset);
427            this.selection = Selection::at(Cursor::new(new, Part::Body, 0).clamp(&this.doc));
428            // What followed the caret moved into a block of its own, which
429            // everything below it now sits under.
430            deltas.push(Delta::Spliced(Splice {
431                removed: Selection::at(at),
432                caret: Cursor::new(new, Part::Body, 0),
433                blocks: 1,
434            }));
435            deltas
436        });
437    }
438
439    /// Shift+Enter. In prose it keeps the caret in the block and inserts a
440    /// literal newline; the places markdown itself keeps to one line still do.
441    pub(super) fn soft_break(&mut self, _: &SoftBreak, _: &mut Window, cx: &mut Context<Self>) {
442        if self.menu_took_enter(cx) {
443            return;
444        }
445        match self.cursor().part {
446            Part::Body | Part::Code => self.insert("\n", cx),
447            Part::Caption | Part::Cell { .. } => {}
448        }
449    }
450
451    /// Ctrl+Enter. Open a plain paragraph after the current block's whole
452    /// subtree and leave the current block's text untouched.
453    pub(super) fn insert_paragraph(
454        &mut self,
455        _: &InsertParagraph,
456        _: &mut Window,
457        cx: &mut Context<Self>,
458    ) {
459        if self.menu_took_enter(cx) {
460            return;
461        }
462        if !self.blocks() {
463            return;
464        }
465        self.edit(EditKind::Structure, cx, |this| {
466            let at = this.cursor().block;
467            let insert_at = this.doc.subtree(at).end;
468            let indent = this.doc.blocks.get(at).map_or(0, |block| block.indent);
469            this.doc.blocks.insert(
470                insert_at,
471                Block::at(BlockKind::Paragraph(Text::default()), indent),
472            );
473            this.doc.repair();
474            this.selection = Selection::at(Cursor::new(insert_at, Part::Body, 0).clamp(&this.doc));
475            vec![Delta::Opened {
476                at: insert_at,
477                count: 1,
478            }]
479        });
480    }
481
482    pub(super) fn indent(&mut self, _: &Indent, _: &mut Window, cx: &mut Context<Self>) {
483        // In the source there is one block to indent and indenting it would be
484        // invisible, so tab is what it is in any text editor: two spaces.
485        if !self.blocks() {
486            return self.insert(INDENT, cx);
487        }
488        self.edit(EditKind::Structure, cx, |this| {
489            this.doc.indent(this.cursor().block);
490            vec![]
491        });
492    }
493
494    pub(super) fn outdent(&mut self, _: &Outdent, _: &mut Window, cx: &mut Context<Self>) {
495        if !self.blocks() {
496            return self.unindent(cx);
497        }
498        self.edit(EditKind::Structure, cx, |this| {
499            this.doc.outdent(this.cursor().block);
500            vec![]
501        });
502    }
503
504    pub(super) fn increase_text_size(
505        &mut self,
506        _: &IncreaseTextSize,
507        _: &mut Window,
508        cx: &mut Context<Self>,
509    ) {
510        self.step_text_size(TextSize::of(cx).step, cx);
511    }
512
513    pub(super) fn decrease_text_size(
514        &mut self,
515        _: &DecreaseTextSize,
516        _: &mut Window,
517        cx: &mut Context<Self>,
518    ) {
519        self.step_text_size(-TextSize::of(cx).step, cx);
520    }
521
522    pub(super) fn reset_text_size(
523        &mut self,
524        _: &ResetTextSize,
525        _: &mut Window,
526        cx: &mut Context<Self>,
527    ) {
528        text_size::reset_text_size(cx);
529    }
530
531    /// Sizing is not an edit: it changes nothing about the document, so it
532    /// leaves no undo step and no anchor moves.
533    ///
534    /// The step is taken against *this* document's size and stored back as the
535    /// shared adjustment, so a press at the end of the range banks up nothing
536    /// to work back through on the way down.
537    pub(super) fn step_text_size(&mut self, by: f32, cx: &mut Context<Self>) {
538        let base = self.text_size.unwrap_or_else(theme::base_text_size);
539        let next = TextSize::of(cx).clamp(text_size::resolve(self.text_size, cx) + by);
540        text_size::set_adjustment(next - base, cx);
541    }
542
543    /// Escape closes an open menu, and otherwise collapses a selection — the
544    /// things there are to back out of, innermost first.
545    pub(super) fn dismiss(&mut self, _: &Dismiss, _: &mut Window, cx: &mut Context<Self>) {
546        if self.pasted.take().is_none() && self.slash.take().is_none() {
547            self.selection = Selection::at(self.selection.head);
548        }
549        cx.notify();
550    }
551}