Skip to main content

editor/
editor.rs

1//! The editing surface.
2//!
3//! The document is the single source of truth. There is no `TextField` per
4//! block: a selection is a pair of `Cursor`s into one `Doc`, which is what
5//! makes Enter split, Backspace merge and Tab indent into list operations
6//! rather than negotiations between separate widgets each owning a string.
7//!
8//! Everything about *what* an edit does lives in `markdown` — `edit` and
9//! `select` — and is tested there without a window. This crate owns only what
10//! needs one: a focus handle, key bindings, the platform input handler, and
11//! turning a click into a position.
12
13use gpui::{
14    App, Context, CursorStyle, ElementInputHandler, EventEmitter, FocusHandle, Focusable,
15    KeyContext, MouseButton, Render, Styled as _, Task, Window, canvas, div, prelude::*,
16};
17use markdown::{
18    Annotation, Block, BlockKind, BlockLayouts, Cursor, Doc, Form, Mark, Part, Selection, Splice,
19    Text, edit, edit::shortcut,
20};
21use std::{ops::Range, time::Duration};
22use theme::Theme;
23
24use crate::{
25    anchor::{Anchor, AnchorId, Delta},
26    history::{EditKind, History},
27    layout::Layout,
28    link::{self, Choice},
29    slash::Slash,
30    text_size::{self, TextSize},
31};
32
33mod anchors;
34mod blocks;
35mod caret;
36mod clipboard;
37pub(crate) mod image;
38mod input;
39pub mod keys;
40pub(crate) mod menu;
41mod mode;
42mod pointer;
43mod render;
44mod typing;
45
46pub use keys::init;
47use keys::{
48    Backspace, Copy, Cut, DecreaseTextSize, Delete, DeleteToHome, DeleteWordLeft, DeleteWordRight,
49    Dismiss, DocumentEnd, DocumentStart, Down, DuplicateBlock, End, Home, IncreaseTextSize, Indent,
50    InsertParagraph, KillLine, Left, MoveBlockDown, MoveBlockUp, Outdent, Paste, Redo, RemoveBlock,
51    ResetTextSize, Right, SelectAll, SelectDocumentEnd, SelectDocumentStart, SelectDown, SelectEnd,
52    SelectHome, SelectLeft, SelectRight, SelectUp, SelectWordLeft, SelectWordRight, SoftBreak,
53    SplitBlock, ToggleBold, ToggleCode, ToggleHighlight, ToggleItalic, ToggleStrike, Undo, Up,
54    WordLeft, WordRight,
55};
56
57pub const CONTEXT: &str = "BezelEditor";
58
59/// The custom mark [`ToggleHighlight`] toggles. It does nothing until the app
60/// registers a mark under this name with [`markdown::set_marks`].
61pub const HIGHLIGHT_MARK: &str = "highlight";
62
63/// [`CONTEXT`], which every binding in [`keys`] is scoped to, plus the mark
64/// that keeps `tab` for [`Editor::indent`].
65fn key_context() -> KeyContext {
66    let mut context = KeyContext::default();
67    context.add(CONTEXT);
68    context.add(ui::focus::CLAIMS_TAB);
69    context
70}
71
72/// What the editor tells its host about.
73///
74/// An app holding comment threads has to hear that the document moved, or its
75/// side of the pairing goes stale against anchors that did not. Split the way
76/// [`ui::input::FieldEvent`] is, so a listener takes only the half it needs.
77#[derive(Clone, Copy, Debug, PartialEq)]
78pub enum EditorEvent {
79    /// The document is different, and the anchors have been mapped through it.
80    Changed,
81    /// A click landed on an anchor's range.
82    AnchorActivated(AnchorId),
83    /// The editor switched between the document and its source, which a host
84    /// lighting its own toggle has no other way to hear about — the switch can
85    /// come from an undo as well as from the button.
86    ModeChanged(Mode),
87}
88
89/// Which form the document is being edited in.
90///
91/// The trigger is the app's: a button, a menu row, a chord of its own. What is
92/// here is the switch it calls, because the caret, the undo history and the
93/// focus have to survive it, and only the editor owns all three.
94#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
95pub enum Mode {
96    /// The document as it reads.
97    #[default]
98    Blocks,
99    /// The markdown a save would write, in one editable text.
100    Source,
101}
102
103/// Which of the editor's own affordances are on.
104///
105/// All of them unless an app says otherwise: a document with nothing
106/// discoverable on it is the wrong default for a library. Turning one off is
107/// for an app that puts its own in the same place — a bar with its own block
108/// menu does not want the gutter handle's as well — rather than for trimming.
109#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110pub struct Chrome {
111    /// The gutter handle, the menu it opens, and dragging a block by it.
112    pub handle: bool,
113    /// The `/` menu at an empty block.
114    pub slash: bool,
115    /// A fence's language label, and the picker it opens.
116    pub language: bool,
117    /// The menu a pasted URL drops — leave it, or make a card, a chip or the
118    /// picture it points at.
119    pub paste: bool,
120}
121
122impl Default for Chrome {
123    fn default() -> Self {
124        Self {
125            handle: true,
126            slash: true,
127            language: true,
128            paste: true,
129        }
130    }
131}
132
133/// What a toolbar reads to light itself, in one call.
134///
135/// Every field is a question a bar asks on every frame, and each was a separate
136/// reach into the document before: which marks are lit, what the block is
137/// called, whether cmd-E would fence, and whether any of it applies at all.
138/// Taken together so a bar cannot answer half of them from one frame and half
139/// from the next.
140#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct Formatting {
142    /// Which form the document is in. In [`Mode::Source`] the markup is already
143    /// spelled out, so [`Self::marks`] is empty and a bar has nothing to light.
144    pub mode: Mode,
145    /// The marks the selection carries throughout — and at a collapsed caret,
146    /// the ones the next character typed would carry, cmd-B before typing
147    /// included.
148    pub marks: Vec<Mark>,
149    /// What the caret's block is called in [`turns`], or `None` for a block the
150    /// menu does not offer.
151    pub block: Option<gpui::SharedString>,
152    /// Whether [`Mark::Code`] here makes a fence out of the selection rather
153    /// than an inline span — the one chord whose meaning changes with what is
154    /// selected, and the one a bar cannot work out for itself.
155    pub fenceable: bool,
156}
157
158/// Every block a block can be turned into, and what each is called — the
159/// vocabulary the slash menu and the block menu both offer, for an app building
160/// a menu of its own. Pair with [`Editor::set_block`], which is what both of
161/// bezel's own menus call.
162pub fn turns() -> Vec<(gpui::SharedString, BlockKind)> {
163    crate::slash::items()
164}
165
166/// Shown on the focused block while it is empty — the only discoverable place
167/// to say that `/` does anything.
168const PLACEHOLDER: &str = "Type / for commands";
169
170/// What tab inserts in the source, where there are no blocks to indent. Two
171/// spaces, which is what markdown's own nesting is written in.
172const INDENT: &str = "  ";
173
174/// Half the caret's blink period — `ui::TextField`'s, which is the 500ms on,
175/// 500ms off macOS itself uses.
176const BLINK: Duration = Duration::from_millis(500);
177
178/// The handle's box. Wide enough to be hit without crowding the margin; how
179/// far left of the text it sits is [`Layout::text_inset`](crate::Layout).
180const HANDLE_SIZE: f32 = 18.0;
181
182/// How far a drag on an image's edge handle can shrink it — matches
183/// `markdown::render`'s `TABLE_MIN_COLUMN_WIDTH`, the same floor for the
184/// other block content a drag can resize.
185const MIN_IMAGE_WIDTH: f32 = 96.0;
186
187/// Whether a selection is prose covering more than one line — two blocks, or
188/// one line break inside a single block.
189///
190/// What a chord means about a selection is the editor's to decide; a [`Doc`]
191/// has no opinion about it. A selection reaching into a table or a fence is not
192/// prose and keeps the inline behaviour, because half a table has no lines to
193/// make a fence out of.
194fn fenceable(doc: &Doc, selection: Selection) -> bool {
195    let spans = doc.spans(selection);
196    let covered = |at: &Cursor, range: &Range<usize>| {
197        doc.blocks[at.block]
198            .text_at(at.part)
199            .and_then(|text| text.text.get(range.clone()))
200    };
201    spans.iter().all(|(at, _)| at.part == Part::Body)
202        && (spans.len() > 1
203            || spans
204                .iter()
205                .any(|(at, range)| covered(at, range).is_some_and(|text| text.contains('\n'))))
206}
207
208/// Give an empty document a block to hold a caret, and say whether one was
209/// needed.
210///
211/// A [`Doc`] with no blocks is a legitimate document — it is what `parse("")`
212/// returns — but it is not something that can be edited: nothing paints, so
213/// there is no caret and no placeholder, and neither a click nor a hit test
214/// has a target to find. An empty file would sit inert until something typed
215/// a block into existence, which is the one thing you cannot do with no caret.
216fn ensure_block(doc: &mut Doc) -> bool {
217    if !doc.blocks.is_empty() {
218        return false;
219    }
220    doc.blocks
221        .push(Block::new(BlockKind::Paragraph(Text::default())));
222    true
223}
224
225fn line_home(at: Cursor, doc: &Doc) -> Cursor {
226    let Some(text) = doc
227        .blocks
228        .get(at.block)
229        .and_then(|block| block.text_at(at.part))
230    else {
231        return at.home();
232    };
233    Cursor {
234        offset: ui::input::line_start(&text.text, at.offset.min(text.text.len())),
235        ..at
236    }
237}
238
239fn line_end(at: Cursor, doc: &Doc) -> Cursor {
240    let Some(text) = doc
241        .blocks
242        .get(at.block)
243        .and_then(|block| block.text_at(at.part))
244    else {
245        return at.end(doc);
246    };
247    Cursor {
248        offset: ui::input::line_end(&text.text, at.offset.min(text.text.len())),
249        ..at
250    }
251}
252
253/// The document a source view is edited as: one fence holding the markdown.
254///
255/// A fence rather than a paragraph because a fence is the block whose caret
256/// already behaves like a plain text editor's — Enter is a newline, nothing
257/// typed into it is markup, and its lines are laid out one per source line.
258fn source_doc(source: &str) -> Doc {
259    Doc {
260        blocks: vec![Block::new(BlockKind::Code {
261            language: Some(markdown::source::LANGUAGES[0].to_string()),
262            code: Text::plain(source),
263        })],
264    }
265}
266
267/// One of the two floating menus a block drops — the block it belongs to and
268/// where it hangs. A `Popup` rather than an `Option` for the exit phase, and
269/// for the press note: the card's `on_mouse_down_out` fires on the *press*, so
270/// without one a trigger's click on the *release* reopens what it just shut.
271pub(crate) type MenuPopup = ui::popover::Popup<(usize, gpui::Point<gpui::Pixels>)>;
272
273/// A table's row or column, as [`markdown::Part::Cell`] numbers them.
274#[derive(Clone, Copy, Debug, PartialEq, Eq)]
275pub(crate) enum Line {
276    Row(usize),
277    Column(usize),
278}
279
280pub struct Editor {
281    doc: Doc,
282    /// The dialect this document is read and written in — the app's own marks,
283    /// taken once at construction. One editor, one spelling: a document that
284    /// changed dialect between a read and a write would rewrite itself.
285    marks: markdown::Marks,
286    /// Which of the editor's own affordances paint. The app's, so one document
287    /// can carry the lot and another none of it.
288    chrome: Chrome,
289    /// Which form the document is in. In [`Mode::Source`] `doc` is one fenced
290    /// block holding the markdown, so every operation below that is about
291    /// *blocks* asks [`Editor::blocks`] first.
292    mode: Mode,
293    /// Collapsed for an ordinary caret, so there is one position here rather
294    /// than a caret and a range that can disagree.
295    selection: Selection,
296    focus_handle: FocusHandle,
297    /// The IME composition range within the caret's text, underlined while it
298    /// is being composed.
299    marked: Option<Range<usize>>,
300    /// Where each text landed last frame, so a click can be turned into a
301    /// caret. Only paint knows this, so the renderer fills it.
302    layouts: BlockLayouts,
303    history: History,
304    /// Which half of the blink the caret is in. Flipped by [`Self::start_blink`].
305    caret_on: bool,
306    /// The blink, alive only while the document holds focus.
307    blink: Option<Task<()>>,
308    /// Comment ranges, mapped through every edit and snapshotted with the
309    /// document. Here rather than in the app because an undo restores a whole
310    /// document and leaves no delta an app could map its own copy through.
311    anchors: Vec<Anchor>,
312    /// Marks the next typed character will carry — cmd-B at a collapsed caret,
313    /// which otherwise has no range to apply to and so would do nothing.
314    /// Cleared by any motion, because they belong to a spot and not to a mood.
315    stored: Vec<Mark>,
316    /// The open slash menu, if `/` started one.
317    slash: Option<Slash>,
318    /// The open paste menu, if a URL landed in a block of its own.
319    pasted: Option<link::Paste>,
320    /// The open prompt, if an image is waiting to be told where to look.
321    url_prompt: Option<image::Prompt>,
322    /// The block a file being dragged over the document would land after.
323    dropping: Option<usize>,
324    /// The block the pointer is over, which is the only one showing a handle.
325    hovered: Option<usize>,
326    /// The table cell the pointer is over, whose row and column show handles.
327    hovered_cell: Option<(usize, Part)>,
328    /// A table row's or column's menu: the table, the line and where it hangs.
329    table_menu: ui::popover::Popup<(usize, Line, gpui::Point<gpui::Pixels>)>,
330    /// A block being dragged by its handle, and where it would land.
331    lifted: Option<(usize, usize)>,
332    /// An image being dragged wider or narrower by its edge handle, and the
333    /// width it holds now — `None` being the natural one, exactly as the
334    /// document spells it. The document only learns the final width on
335    /// release, the same reason `lifted` waits for the drop; carrying the
336    /// document's own value here is what makes a press that never moved
337    /// read back as no change at all.
338    resizing: Option<(usize, Option<u32>)>,
339    /// The block menu the handle opened, and where to anchor it.
340    block_menu: MenuPopup,
341    /// The language menu a fence's header opened, and the block it belongs to.
342    language_menu: MenuPopup,
343    /// Set by a floating layer's press — the gutter handle, the URL prompt —
344    /// so the editor's own press does not undo what that press just did.
345    press_claimed: bool,
346    /// Where the editor's own box starts, so a position recorded in window
347    /// coordinates can be placed inside it, and how far it reaches — which is
348    /// how wide a resized image is allowed to be. Its own box rather than the
349    /// picture's: a picture already narrowed would otherwise be its own
350    /// ceiling, and no drag could ever widen it again.
351    origin: gpui::Point<gpui::Pixels>,
352    width: gpui::Pixels,
353    /// Whether the pointer is dragging out a selection.
354    dragging: bool,
355    /// Whether the pointer is over painted text, which is the only place the
356    /// editor claims an I-beam.
357    over_text: bool,
358    /// The host's scroll box, when it gave one, and whether the caret still
359    /// owes it a reveal.
360    scroll: Option<gpui::ScrollHandle>,
361    reveal: bool,
362    /// Where the gutter handle was placed this frame, so the frame after can
363    /// tell whether the block moved out from under it.
364    handle_at: Option<gpui::Point<gpui::Pixels>>,
365    /// The column and row held across consecutive vertical moves.
366    goal: Option<VerticalGoal>,
367    /// The size the app set this document in, in points, or `None` to follow
368    /// the app's own text size. Absolute rather than a factor over the ladder,
369    /// so moving the interface size leaves a document set to 16pt at 16pt.
370    ///
371    /// What the chords move is the shared adjustment on top of this; the base
372    /// itself is the app's alone.
373    text_size: Option<f32>,
374    /// The directory relative image paths resolve against.
375    base: Option<std::path::PathBuf>,
376}
377
378#[derive(Clone, Copy)]
379struct VerticalGoal {
380    x: gpui::Pixels,
381    /// Relative to the caret's painted position so scrolling cannot change the row.
382    row_from_caret: gpui::Pixels,
383}
384
385impl Editor {
386    pub fn new(source: &str, cx: &mut Context<Self>) -> Self {
387        let marks = markdown::Marks::of(cx);
388        let mut doc = markdown::parse_with(source, &marks);
389        ensure_block(&mut doc);
390        Self {
391            marks,
392            // Clamped, not defaulted: a document opening on a fence or a table
393            // has no body at block zero, and a caret claiming one resolves
394            // against nothing until something moves it.
395            selection: Selection::at(Cursor::default().clamp(&doc)),
396            doc,
397            chrome: Chrome::default(),
398            mode: Mode::default(),
399            focus_handle: cx.focus_handle(),
400            marked: None,
401            layouts: BlockLayouts::default(),
402            history: History::default(),
403            caret_on: true,
404            blink: None,
405            anchors: Vec::new(),
406            stored: Vec::new(),
407            slash: None,
408            pasted: None,
409            url_prompt: None,
410            dropping: None,
411            hovered: None,
412            hovered_cell: None,
413            table_menu: Default::default(),
414            lifted: None,
415            resizing: None,
416            block_menu: MenuPopup::default(),
417            language_menu: MenuPopup::default(),
418            press_claimed: false,
419            origin: gpui::Point::default(),
420            width: gpui::Pixels::ZERO,
421            dragging: false,
422            over_text: false,
423            scroll: None,
424            reveal: false,
425            goal: None,
426            handle_at: None,
427            text_size: None,
428            base: None,
429        }
430    }
431
432    /// How many undo steps to keep. App-wide configuration would be a gpui
433    /// global alongside [`init`], not a `Theme` field — the theme is rebuilt on
434    /// every light/dark switch, which would quietly reset anything behavioural
435    /// parked in it.
436    pub fn with_undo_limit(mut self, limit: usize) -> Self {
437        self.history = History::with_limit(limit);
438        self
439    }
440
441    /// Read and write this document with marks of its own, rather than the ones
442    /// [`markdown::set_marks`] installed. For an app whose editors do not all
443    /// speak the same dialect.
444    pub fn with_marks(mut self, marks: markdown::Marks) -> Self {
445        let source = self.source();
446        self.marks = marks;
447        self.doc = markdown::parse_with(&source, &self.marks);
448        ensure_block(&mut self.doc);
449        self.selection = self.selection.clamp(&self.doc);
450        self
451    }
452
453    /// Which of the editor's own affordances to paint. See [`Chrome`].
454    pub fn with_chrome(mut self, chrome: Chrome) -> Self {
455        self.chrome = chrome;
456        self
457    }
458
459    /// What is painting now, for an app whose own bar mirrors it.
460    pub fn chrome(&self) -> Chrome {
461        self.chrome
462    }
463
464    /// Open in [`Mode::Source`] rather than on the document — an app whose
465    /// editor is a markdown file first. Nothing is recorded: this is where the
466    /// document starts, not a switch to step back over.
467    pub fn with_mode(mut self, mode: Mode) -> Self {
468        if mode != self.mode {
469            self.switch(mode);
470        }
471        self
472    }
473
474    /// Open the document at a size of its own, in points — the app's settings
475    /// field for prose. Unset, it is set at the app's own text size.
476    ///
477    /// The base, not the current size: `cmd-+` moves a shared adjustment over
478    /// this, and `cmd-0` clears that adjustment to come back here.
479    pub fn with_text_size(mut self, points: f32) -> Self {
480        self.text_size = Some(points);
481        self
482    }
483
484    /// Update the configured base size without changing the temporary zoom.
485    pub fn set_text_size(&mut self, points: f32, cx: &mut Context<Self>) {
486        if self.text_size != Some(points) {
487            self.text_size = Some(points);
488            cx.notify();
489        }
490    }
491
492    /// The base the app set, if any. Add
493    /// [`text_size_adjustment`](crate::text_size_adjustment) for what is on
494    /// screen.
495    pub fn text_size(&self) -> Option<f32> {
496        self.text_size
497    }
498
499    /// Resolve relative image paths against `dir` — the document's own folder,
500    /// for a document that keeps its pictures beside it. The stored URL stays
501    /// as written.
502    pub fn with_base(mut self, dir: impl Into<std::path::PathBuf>) -> Self {
503        self.base = Some(dir.into());
504        self
505    }
506
507    /// Change or clear the directory relative image paths resolve against.
508    pub fn set_base(&mut self, dir: Option<std::path::PathBuf>, cx: &mut Context<Self>) {
509        if self.base != dir {
510            self.base = dir;
511            cx.notify();
512        }
513    }
514
515    /// The directory relative image paths resolve against, if one is set.
516    pub fn base(&self) -> Option<&std::path::Path> {
517        self.base.as_deref()
518    }
519
520    /// The box the document scrolls in, so typing off the bottom follows the
521    /// caret down.
522    ///
523    /// The host's rather than the editor's: a document goes in whatever pane
524    /// the app gives it, and the gutter handle, the drop indicator and the
525    /// menus are all placed absolutely against this editor's own origin — put
526    /// the scroll box here and every one of them would be offset twice.
527    pub fn with_scroll(mut self, handle: gpui::ScrollHandle) -> Self {
528        self.scroll = Some(handle);
529        self
530    }
531
532    pub fn doc(&self) -> &Doc {
533        &self.doc
534    }
535
536    pub fn selection(&self) -> Selection {
537        self.selection
538    }
539
540    /// Put the selection somewhere — what a thread in a sidebar does when it is
541    /// clicked, and what a caret restored with a document needs.
542    ///
543    /// Clamped, because the caller's range came from somewhere the document may
544    /// have moved on from.
545    pub fn select(&mut self, selection: Selection, cx: &mut Context<Self>) {
546        self.selection = selection.clamp(&self.doc);
547        self.history.interrupt();
548        self.reveal = true;
549        self.caret_moved();
550        cx.notify();
551    }
552
553    /// Where the selection sits on screen, in window coordinates, so a host can
554    /// float a toolbar at it.
555    ///
556    /// The head's row only — a selection spanning ten blocks wants its bubble
557    /// where the pointer left off, not centred over the whole span. `None` when
558    /// nothing is selected or the caret has not painted yet.
559    /// Where everything landed last frame — blocks, pictures, a fence's
560    /// language label, and the row rects of any range.
561    ///
562    /// Handed out whole rather than a method per question: an app placing
563    /// chrome of its own asks the geometry, and which question it needs is not
564    /// this crate's to guess. See [`markdown::BlockLayouts`].
565    pub fn layouts(&self) -> &BlockLayouts {
566        &self.layouts
567    }
568
569    pub fn selection_bounds(&self) -> Option<gpui::Bounds<gpui::Pixels>> {
570        // The head alone. A bar centred over the whole selection wants
571        // `layouts().rects(selection)`, which is every painted row of it.
572        if self.selection.is_collapsed() {
573            return None;
574        }
575        let (point, line_height) = self.layouts.position(self.selection.head)?;
576        Some(gpui::Bounds::new(
577            point,
578            gpui::size(gpui::px(0.0), line_height),
579        ))
580    }
581}
582
583impl EventEmitter<EditorEvent> for Editor {}
584
585impl Focusable for Editor {
586    fn focus_handle(&self, _: &App) -> FocusHandle {
587        self.focus_handle.clone()
588    }
589}