bezel-editor 0.2.13

A Notion-style block editor for gpui — the editing surface over the markdown document model
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
//! The editing surface.
//!
//! The document is the single source of truth. There is no `TextField` per
//! block: a selection is a pair of `Cursor`s into one `Doc`, which is what
//! makes Enter split, Backspace merge and Tab indent into list operations
//! rather than negotiations between separate widgets each owning a string.
//!
//! Everything about *what* an edit does lives in `markdown` — `edit` and
//! `select` — and is tested there without a window. This crate owns only what
//! needs one: a focus handle, key bindings, the platform input handler, and
//! turning a click into a position.

use gpui::{
    App, Context, CursorStyle, ElementInputHandler, Entity, EventEmitter, FocusHandle, Focusable,
    KeyContext, MouseButton, Render, Styled as _, Task, Window, canvas, div, prelude::*,
};
use markdown::{
    Affinity, Annotation, AppExt as _, Block, BlockKind, BlockLayouts, Cursor, Doc, Form, Mark,
    Part, Selection, Splice, Text, edit, edit::shortcut,
};
use std::{ops::Range, time::Duration};
use theme::Theme;

use crate::{
    anchor::{Anchor, AnchorId, Delta},
    document::{Change, Document, DocumentEvent, Tracked},
    history::{EditKind, Run},
    link::{self, Choice},
    slash::{Slash, SlashAction, SlashAt},
    text_size,
};

mod anchors;
mod blocks;
mod caret;
mod clipboard;
pub(crate) mod image;
mod input;
pub mod keys;
pub(crate) mod menu;
pub(crate) mod mode;
mod pointer;
mod render;
mod sizing;
mod sync;
mod table;
mod typing;

pub use keys::init;
use keys::{
    Backspace, Copy, Cut, DecreaseTextSize, Delete, DeleteToHome, DeleteWordLeft, DeleteWordRight,
    Dismiss, DocumentEnd, DocumentStart, Down, DuplicateBlock, End, Home, IncreaseTextSize, Indent,
    InsertParagraph, KillLine, Left, MoveBlockDown, MoveBlockUp, Outdent, Paste, Redo, RemoveBlock,
    ResetTextSize, Right, SelectAll, SelectDocumentEnd, SelectDocumentStart, SelectDown, SelectEnd,
    SelectHome, SelectLeft, SelectRight, SelectUp, SelectWordLeft, SelectWordRight, SoftBreak,
    SplitBlock, ToggleBold, ToggleCode, ToggleHighlight, ToggleItalic, ToggleStrike, Undo, Up,
    WordLeft, WordRight,
};

pub const CONTEXT: &str = "BezelEditor";

/// The custom mark [`ToggleHighlight`] toggles. It does nothing until the app
/// registers a mark under this name with [`markdown::AppExt::set_marks`].
pub const HIGHLIGHT_MARK: &str = "highlight";

/// [`CONTEXT`], which every binding in [`keys`] is scoped to, plus the mark
/// that keeps `tab` for [`Editor::indent`].
fn key_context() -> KeyContext {
    let mut context = KeyContext::default();
    context.add(CONTEXT);
    context.add(ui::focus::CLAIMS_TAB);
    context
}

/// What the editor tells its host about.
///
/// An app holding comment threads has to hear that the document moved, or its
/// side of the pairing goes stale against anchors that did not. Split the way
/// [`ui::input::FieldEvent`] is, so a listener takes only the half it needs.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum EditorEvent {
    /// The document is different, and the anchors have been mapped through it.
    Changed,
    /// A click landed on an anchor's range.
    AnchorActivated(AnchorId),
    /// The pointer came over an anchor's painted text, or left it: `None`
    /// over none, and when the pointer leaves the editor.
    AnchorHovered(Option<AnchorId>),
    /// The editor switched between the document and its source, which a host
    /// lighting its own toggle has no other way to hear about — the switch can
    /// come from an undo as well as from the button.
    ModeChanged(Mode),
}

/// Which form the document is being edited in.
///
/// The trigger is the app's: a button, a menu row, a chord of its own. What is
/// here is the switch it calls, because the caret, the undo history and the
/// focus have to survive it, and only the editor owns all three.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Mode {
    /// The document as it reads.
    #[default]
    Blocks,
    /// The markdown a save would write, in one editable text.
    Source,
}

/// Which of the editor's own affordances are on.
///
/// All of them unless an app says otherwise: a document with nothing
/// discoverable on it is the wrong default for a library. Turning one off is
/// for an app that puts its own in the same place — a bar with its own block
/// menu does not want the gutter handle's as well — rather than for trimming.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Chrome {
    /// The gutter handle, the menu it opens, and dragging a block by it.
    pub handle: bool,
    /// The `/` menu at an empty block.
    pub slash: bool,
    /// A fence's language label, and the picker it opens.
    pub language: bool,
    /// The menu a pasted URL drops — leave it, or make a card, a chip or the
    /// picture it points at.
    pub paste: bool,
    /// The mention menus over the app's [`crate::MentionSource`]s. Off by
    /// default.
    pub mention: bool,
}

impl Default for Chrome {
    fn default() -> Self {
        Self {
            handle: true,
            slash: true,
            language: true,
            paste: true,
            mention: false,
        }
    }
}

/// What a toolbar reads to light itself, in one call.
///
/// Every field is a question a bar asks on every frame, and each was a separate
/// reach into the document before: which marks are lit, what the block is
/// called, whether cmd-E would fence, and whether any of it applies at all.
/// Taken together so a bar cannot answer half of them from one frame and half
/// from the next.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Formatting {
    /// Which form the document is in. In [`Mode::Source`] the markup is already
    /// spelled out, so [`Self::marks`] is empty and a bar has nothing to light.
    pub mode: Mode,
    /// The marks the selection carries throughout — and at a collapsed caret,
    /// the ones the next character typed would carry, cmd-B before typing
    /// included.
    pub marks: Vec<Mark>,
    /// What the caret's block is called in [`turns`], or `None` for a block the
    /// menu does not offer.
    pub block: Option<gpui::SharedString>,
    /// The quote the caret's block sits in — see [`quotes`].
    pub quote: Option<markdown::Quoted>,
    /// Whether [`Mark::Code`] here makes a fence out of the selection rather
    /// than an inline span — the one chord whose meaning changes with what is
    /// selected, and the one a bar cannot work out for itself.
    pub fenceable: bool,
}

/// Every block a block can be turned into, and what each is called — the
/// vocabulary the slash menu and the block menu both offer, for an app building
/// a menu of its own. Pair with [`Editor::set_block`], which is what both of
/// bezel's own menus call.
pub fn turns() -> Vec<(gpui::SharedString, BlockKind)> {
    crate::slash::items()
}

/// Every quote a block can be put in, and what each is called. Pair with
/// [`Editor::toggle_quote`].
pub fn quotes() -> Vec<(gpui::SharedString, Option<markdown::QuoteKind>)> {
    crate::slash::quotes()
}

/// Shown on the focused block while it is empty — the only discoverable place
/// to say that `/` does anything.
const PLACEHOLDER: &str = "Type / for commands";

/// What tab inserts in the source, where there are no blocks to indent. Two
/// spaces, which is what markdown's own nesting is written in.
const INDENT: &str = "  ";

/// Half the caret's blink period — `ui::TextField`'s, which is the 500ms on,
/// 500ms off macOS itself uses.
const BLINK: Duration = Duration::from_millis(500);

/// The handle's box. Wide enough to be hit without crowding the margin; how
/// far left of the text it sits is [`Layout::text_inset`](crate::Layout).
const HANDLE_SIZE: f32 = 18.0;

/// How far a drag on an image's edge handle can shrink it — matches
/// `markdown::render`'s `TABLE_MIN_COLUMN_WIDTH`, the same floor for the
/// other block content a drag can resize.
const MIN_IMAGE_WIDTH: f32 = 96.0;

/// Whether a selection is prose covering more than one line — two blocks, or
/// one line break inside a single block.
///
/// What a chord means about a selection is the editor's to decide; a [`Doc`]
/// has no opinion about it. A selection reaching into a table or a fence is not
/// prose and keeps the inline behaviour, because half a table has no lines to
/// make a fence out of.
fn fenceable(doc: &Doc, selection: Selection) -> bool {
    let spans = doc.spans(selection);
    let covered = |at: &Cursor, range: &Range<usize>| {
        doc.blocks[at.block]
            .text_at(at.part)
            .and_then(|text| text.text.get(range.clone()))
    };
    spans.iter().all(|(at, _)| at.part == Part::Body)
        && (spans.len() > 1
            || spans
                .iter()
                .any(|(at, range)| covered(at, range).is_some_and(|text| text.contains('\n'))))
}

/// Give an empty document a block to hold a caret, and say whether one was
/// needed.
///
/// A [`Doc`] with no blocks is a legitimate document — it is what `parse("")`
/// returns — but it is not something that can be edited: nothing paints, so
/// there is no caret and no placeholder, and neither a click nor a hit test
/// has a target to find. An empty file would sit inert until something typed
/// a block into existence, which is the one thing you cannot do with no caret.
pub(crate) fn ensure_block(doc: &mut Doc) -> bool {
    if !doc.blocks.is_empty() {
        return false;
    }
    doc.blocks
        .push(Block::new(BlockKind::Paragraph(Text::default())));
    true
}

fn line_home(at: Cursor, doc: &Doc) -> Cursor {
    let Some(text) = doc
        .blocks
        .get(at.block)
        .and_then(|block| block.text_at(at.part))
    else {
        return at.home();
    };
    Cursor {
        offset: ui::input::line_start(&text.text, at.offset.min(text.text.len())),
        ..at
    }
}

fn line_end(at: Cursor, doc: &Doc) -> Cursor {
    let Some(text) = doc
        .blocks
        .get(at.block)
        .and_then(|block| block.text_at(at.part))
    else {
        return at.end(doc);
    };
    Cursor {
        offset: ui::input::line_end(&text.text, at.offset.min(text.text.len())),
        ..at
    }
}

/// The document a source view is edited as: one fence holding the markdown.
///
/// A fence rather than a paragraph because a fence is the block whose caret
/// already behaves like a plain text editor's — Enter is a newline, nothing
/// typed into it is markup, and its lines are laid out one per source line.
fn source_doc(source: &str) -> Doc {
    Doc {
        blocks: vec![Block::new(BlockKind::Code {
            language: Some(markdown::source::LANGUAGES[0].to_string()),
            code: Text::plain(source),
            height: None,
        })],
    }
}

/// A floating menu a block drops. A `Popup` rather than an `Option` for the
/// exit phase, and for the press note: the card's `on_mouse_down_out` fires on
/// the *press*, so without one a trigger's click on the *release* reopens what
/// it just shut.
pub(crate) type MenuPopup<T> = ui::popover::Popup<Dropped<T>>;

/// What a dropped menu belongs to, where it hangs, and its live row.
pub(crate) struct Dropped<T> {
    pub of: T,
    pub at: gpui::Point<gpui::Pixels>,
    pub cursor: ui::menu::Cursor,
}

impl<T> Dropped<T> {
    pub fn new(of: T, at: gpui::Point<gpui::Pixels>) -> Self {
        Self {
            of,
            at,
            cursor: ui::menu::Cursor::default(),
        }
    }
}

/// The row, column, or cell whose table menu is open.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum TableTarget {
    Row(usize),
    Column(usize),
    Cell { row: usize, column: usize },
}

/// A table's row or column, as [`markdown::Part::Cell`] numbers them.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Line {
    Row(usize),
    Column(usize),
}

/// Where a reveal leaves the caret's row in the scroll box.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Reveal {
    /// The least scroll that brings the row inside the box.
    Nearest,
    /// The row at the box's top edge, or as near it as the document's end
    /// allows.
    Top,
}

pub struct Editor {
    document: Entity<Document>,
    /// This editor's snapshot of the document, its form, its marks and its
    /// anchors: what it edits, handed back to [`Self::document`] after each
    /// change, and replaced whenever another editor changes the document.
    doc: Tracked,
    /// The source view's text and the [`Tracked::revision`] it was built at.
    /// Read only in [`Mode::Source`].
    source_view: Option<(u64, markdown::SourceText)>,
    /// Highlighting the source view's text after an edit. Dropping it cancels.
    source_spans: Option<Task<()>>,
    marks: markdown::Marks,
    /// Which of the editor's own affordances paint. The app's, so one document
    /// can carry the lot and another none of it.
    chrome: Chrome,
    /// In [`Mode::Source`] `doc` is one fenced block holding the markdown, so
    /// every operation below that is about *blocks* asks [`Editor::blocks`]
    /// first.
    mode: Mode,
    /// Collapsed for an ordinary caret, so there is one position here rather
    /// than a caret and a range that can disagree.
    selection: Selection,
    focus_handle: FocusHandle,
    /// The IME composition range within the caret's text, underlined while it
    /// is being composed.
    marked: Option<Range<usize>>,
    /// Where each text landed last frame, so a click can be turned into a
    /// caret. Only paint knows this, so the renderer fills it.
    layouts: BlockLayouts,
    run: Run,
    /// Which half of the blink the caret is in. Flipped by [`Self::start_blink`].
    caret_on: bool,
    /// The blink, alive only while the document holds focus.
    blink: Option<Task<()>>,
    anchors: Vec<Anchor>,
    /// Ranges washed for one frame's worth of the app's say — find hits. Not
    /// mapped through edits and not in the history: the app sets them again.
    washes: Vec<(Selection, Annotation)>,
    /// Marks the next typed character will carry — cmd-B at a collapsed caret,
    /// which otherwise has no range to apply to and so would do nothing.
    /// Cleared by any motion, because they belong to a spot and not to a mood.
    stored: Vec<Mark>,
    /// The open slash menu, if `/` started one.
    slash: Option<Slash>,
    mention: Option<crate::mention::MentionMenu>,
    /// The open paste menu, if a URL landed in a block of its own.
    pasted: Option<link::Paste>,
    /// The open prompt, if an image is waiting to be told where to look.
    url_prompt: Option<image::Prompt>,
    /// The block a file being dragged over the document would land after.
    dropping: Option<usize>,
    /// The block the pointer is over, which is the only one showing a handle.
    hovered: Option<usize>,
    /// The table cell the pointer is over, whose row and column show handles.
    hovered_cell: Option<(usize, Part)>,
    /// A table row's or column's menu: the table, the line and where it hangs.
    table_drag: Option<table::TableDrag>,
    table_dragged: bool,
    table_menu: MenuPopup<(usize, TableTarget)>,
    /// The right-click menu on text, with its rows as they stood when it
    /// opened.
    text_menu: MenuPopup<Vec<ui::menu::Item>>,
    /// The right-click menu on a picture.
    image_menu: MenuPopup<image::ImageTarget>,
    /// A block being dragged by its handle, and where it would land.
    lifted: Option<(usize, usize)>,
    /// An image being dragged wider or narrower by its edge handle, and the
    /// width it holds now — `None` being the natural one, exactly as the
    /// document spells it. The document only learns the final width on
    /// release, the same reason `lifted` waits for the drop; carrying the
    /// document's own value here is what makes a press that never moved
    /// read back as no change at all.
    resizing: Option<(usize, Option<u32>)>,
    /// A painted block being dragged taller or shorter by its bottom handle.
    sizing: Option<sizing::Sizing>,
    /// The block menu the handle opened, over the block and its rows.
    block_menu: MenuPopup<(usize, crate::slash::Rows)>,
    /// The language menu a fence's header opened, over its block.
    language_menu: MenuPopup<usize>,
    /// Set by a floating layer's press — the gutter handle, the URL prompt —
    /// so the editor's own press does not undo what that press just did.
    press_claimed: bool,
    /// Where the editor's own box starts, so a position recorded in window
    /// coordinates can be placed inside it, and how far it reaches — which is
    /// how wide a resized image is allowed to be. Its own box rather than the
    /// picture's: a picture already narrowed would otherwise be its own
    /// ceiling, and no drag could ever widen it again.
    origin: gpui::Point<gpui::Pixels>,
    width: gpui::Pixels,
    /// The press being dragged out: what it selects by, and what it selected.
    dragging: Option<(ui::input::Granularity, Range<Cursor>)>,
    /// Whether the pointer is over painted text, which is the only place the
    /// editor claims an I-beam.
    over_text: bool,
    /// The anchor whose text the pointer is over — see
    /// [`EditorEvent::AnchorHovered`].
    over_anchor: Option<AnchorId>,
    /// The host's scroll box, when it gave one, and the reveal the caret still
    /// owes it.
    scroll: Option<gpui::ScrollHandle>,
    reveal: Option<Reveal>,
    /// Where a held drag last was, in window coordinates.
    drag_at: Option<gpui::Point<gpui::Pixels>>,
    /// Scrolls [`Self::scroll`] while a drag is held past its top or bottom.
    edge_scroll: Option<Task<()>>,
    /// Where the gutter handle was placed this frame, so the frame after can
    /// tell whether the block moved out from under it.
    handle_at: Option<gpui::Point<gpui::Pixels>>,
    /// The column held across consecutive vertical moves.
    goal: Option<gpui::Pixels>,
    /// The size the app set this document in, in points, or `None` to follow
    /// the app's own text size. Absolute rather than a factor over the ladder,
    /// so moving the interface size leaves a document set to 16pt at 16pt.
    ///
    /// What the chords move is the shared adjustment on top of this; the base
    /// itself is the app's alone.
    text_size: Option<f32>,
    /// The directory relative image paths resolve against.
    base: Option<std::path::PathBuf>,
    image_overlay: Option<markdown::ImageOverlay>,
    image_overlay_corner: markdown::ImageOverlayCorner,
    _document: gpui::Subscription,
}

impl Editor {
    /// An editor on a document of its own.
    pub fn new(source: &str, cx: &mut Context<Self>) -> Self {
        let document = cx.new(|cx| Document::new(source, cx));
        Self::for_document(document, cx)
    }

    /// An editor on `document`, beside any others already on it.
    pub fn for_document(document: Entity<Document>, cx: &mut Context<Self>) -> Self {
        let (doc, mode, marks, anchors) = {
            let document = document.read(cx);
            (
                document.doc.clone(),
                document.mode,
                document.marks.clone(),
                document.anchors.clone(),
            )
        };
        let subscription = cx.subscribe(&document, |this, document, event: &DocumentEvent, cx| {
            if event.by != Some(cx.entity_id()) {
                this.take_document(&document, &event.change, cx);
            }
        });
        Self {
            document,
            marks,
            // Clamped, not defaulted: a document opening on a fence or a table
            // has no body at block zero, and a caret claiming one resolves
            // against nothing until something moves it.
            selection: Selection::at(Cursor::default().clamp(&doc)),
            doc,
            mode,
            anchors,
            source_view: None,
            source_spans: None,
            chrome: Chrome::default(),
            focus_handle: cx.focus_handle(),
            marked: None,
            layouts: BlockLayouts::default(),
            run: Run::default(),
            caret_on: true,
            blink: None,
            washes: Vec::new(),
            stored: Vec::new(),
            slash: None,
            mention: None,
            pasted: None,
            url_prompt: None,
            dropping: None,
            hovered: None,
            hovered_cell: None,
            table_menu: Default::default(),
            text_menu: Default::default(),
            image_menu: Default::default(),
            table_drag: None,
            table_dragged: false,
            lifted: None,
            resizing: None,
            sizing: None,
            block_menu: MenuPopup::default(),
            language_menu: MenuPopup::default(),
            press_claimed: false,
            origin: gpui::Point::default(),
            width: gpui::Pixels::ZERO,
            dragging: None,
            over_text: false,
            over_anchor: None,
            scroll: None,
            reveal: None,
            drag_at: None,
            edge_scroll: None,
            goal: None,
            handle_at: None,
            text_size: None,
            base: None,
            image_overlay: None,
            image_overlay_corner: markdown::ImageOverlayCorner::default(),
            _document: subscription,
        }
    }

    /// The document this editor is a view of.
    pub fn document(&self) -> &Entity<Document> {
        &self.document
    }

    /// Which of the editor's own affordances to paint. See [`Chrome`].
    pub fn with_chrome(mut self, chrome: Chrome) -> Self {
        self.chrome = chrome;
        self
    }

    /// Adds an app-provided hover control to pictures in rich mode.
    pub fn with_image_overlay(mut self, overlay: markdown::ImageOverlay) -> Self {
        self.image_overlay = Some(overlay);
        self
    }

    /// Replaces or removes the picture hover control.
    pub fn set_image_overlay(
        &mut self,
        overlay: Option<markdown::ImageOverlay>,
        cx: &mut Context<Self>,
    ) {
        self.image_overlay = overlay;
        cx.notify();
    }

    /// Positions picture hover controls; bottom-right by default.
    pub fn with_image_overlay_corner(mut self, corner: markdown::ImageOverlayCorner) -> Self {
        self.image_overlay_corner = corner;
        self
    }

    /// Repositions the picture hover control.
    pub fn set_image_overlay_corner(
        &mut self,
        corner: markdown::ImageOverlayCorner,
        cx: &mut Context<Self>,
    ) {
        self.image_overlay_corner = corner;
        cx.notify();
    }

    /// What is painting now, for an app whose own bar mirrors it.
    pub fn chrome(&self) -> Chrome {
        self.chrome
    }

    /// Open the document at a size of its own, in points — the app's settings
    /// field for prose. Unset, it is set at the app's own text size.
    ///
    /// The base, not the current size: `cmd-+` moves a shared adjustment over
    /// this, and `cmd-0` clears that adjustment to come back here.
    pub fn with_text_size(mut self, points: f32) -> Self {
        self.text_size = Some(points);
        self
    }

    /// Update the configured base size without changing the temporary zoom.
    pub fn set_text_size(&mut self, points: f32, cx: &mut Context<Self>) {
        if self.text_size != Some(points) {
            self.text_size = Some(points);
            cx.notify();
        }
    }

    /// The base the app set, if any. Add
    /// [`crate::AppExt::editor_text_size_adjustment`](crate::AppExt::editor_text_size_adjustment) for what is on
    /// screen.
    pub fn text_size(&self) -> Option<f32> {
        self.text_size
    }

    /// Resolve relative image paths against `dir` — the document's own folder,
    /// for a document that keeps its pictures beside it. The stored URL stays
    /// as written.
    pub fn with_base(mut self, dir: impl Into<std::path::PathBuf>) -> Self {
        self.base = Some(dir.into());
        self
    }

    /// Change or clear the directory relative image paths resolve against.
    pub fn set_base(&mut self, dir: Option<std::path::PathBuf>, cx: &mut Context<Self>) {
        if self.base != dir {
            self.base = dir;
            cx.notify();
        }
    }

    /// The directory relative image paths resolve against, if one is set.
    pub fn base(&self) -> Option<&std::path::Path> {
        self.base.as_deref()
    }

    /// The box the document scrolls in, so typing off the bottom follows the
    /// caret down and a drag held past its top or bottom scrolls it.
    ///
    /// The host's rather than the editor's: a document goes in whatever pane
    /// the app gives it, and the gutter handle, the drop indicator and the
    /// menus are all placed absolutely against this editor's own origin — put
    /// the scroll box here and every one of them would be offset twice.
    pub fn with_scroll(mut self, handle: gpui::ScrollHandle) -> Self {
        self.scroll = Some(handle);
        self
    }

    pub fn doc(&self) -> &Doc {
        &self.doc
    }

    pub fn selection(&self) -> Selection {
        self.selection
    }

    /// Put the selection somewhere — what a thread in a sidebar does when it is
    /// clicked, and what a caret restored with a document needs.
    ///
    /// Clamped, because the caller's range came from somewhere the document may
    /// have moved on from.
    pub fn select(&mut self, selection: Selection, cx: &mut Context<Self>) {
        self.select_revealing(selection, Reveal::Nearest, cx);
    }

    /// [`Self::select`], scrolling the caret's row to the top of the scroll
    /// box — what a jump to a heading wants, from either side of it.
    pub fn select_to_top(&mut self, selection: Selection, cx: &mut Context<Self>) {
        self.select_revealing(selection, Reveal::Top, cx);
    }

    fn select_revealing(&mut self, selection: Selection, reveal: Reveal, cx: &mut Context<Self>) {
        self.selection = selection.clamp(&self.doc);
        self.run.interrupt();
        self.reveal = Some(reveal);
        self.caret_moved();
        cx.notify();
    }

    /// Where the selection sits on screen, in window coordinates, so a host can
    /// float a toolbar at it.
    ///
    /// The head's row only — a selection spanning ten blocks wants its bubble
    /// where the pointer left off, not centred over the whole span. `None` when
    /// nothing is selected or the caret has not painted yet.
    /// Where everything landed last frame — blocks, pictures, a fence's
    /// language label, and the row rects of any range.
    ///
    /// Handed out whole rather than a method per question: an app placing
    /// chrome of its own asks the geometry, and which question it needs is not
    /// this crate's to guess. See [`markdown::BlockLayouts`].
    pub fn layouts(&self) -> &BlockLayouts {
        &self.layouts
    }

    pub fn selection_bounds(&self) -> Option<gpui::Bounds<gpui::Pixels>> {
        // The head alone. A bar centred over the whole selection wants
        // `layouts().rects(selection)`, which is every painted row of it.
        if self.selection.is_collapsed() {
            return None;
        }
        let (point, line_height) = self
            .layouts
            .position(self.selection.head, self.selection.affinity)?;
        Some(gpui::Bounds::new(
            point,
            gpui::size(gpui::px(0.0), line_height),
        ))
    }
}

impl EventEmitter<EditorEvent> for Editor {}

impl Focusable for Editor {
    fn focus_handle(&self, _: &App) -> FocusHandle {
        self.focus_handle.clone()
    }
}