Skip to main content

editor/editor/
typing.rs

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