bezel-editor 0.2.22

A Notion-style block editor for gpui — the editing surface over the markdown document model
Documentation
//! The caret: where it is, how it moves, its blink, and the edits that move it.

use super::*;

impl Editor {
    /// Bring the caret back into view.
    ///
    /// Read *after* paint, because a block that has only just appeared — the
    /// one Enter made — has no position recorded until it has painted once,
    /// which is exactly the case worth scrolling for.
    /// Ask for another frame when the block the handle sits on has moved.
    ///
    /// The handle is built from the records of the frame *before* this one —
    /// the document fills them as it paints, which is after the editor has
    /// finished building its tree — so a block that has just been indented, or
    /// grown a line, leaves the handle behind. Reading the records back here,
    /// once the document has painted, is what turns that into one late frame
    /// instead of a handle stranded until the caret blink happens to draw
    /// again.
    pub(super) fn settle_handle(&mut self, window: &Window, cx: &mut Context<Self>) {
        let focused = self.focus_handle.is_focused(window);
        let now = self
            .handle_block(focused)
            .and_then(|ix| self.handle_origin(ix, cx));
        if now != self.handle_at {
            // Not `notify`: this runs *during* the draw, and the dirty flag it
            // sets is cleared when that draw finishes. Asking for the next
            // frame is what survives it.
            window.request_animation_frame();
        }
    }

    pub(super) fn reveal_caret(&mut self, window: &mut Window) {
        let Some(reveal) = self.reveal else {
            return;
        };
        let Some(scroll) = self.scroll.clone() else {
            self.reveal = None;
            return;
        };
        // Left set when the caret has not painted: a block with no text at all
        // never answers, and the next move is what gets it back.
        let Some((at, line)) = self
            .layouts
            .position(self.selection.head, self.selection.affinity)
        else {
            return;
        };

        let view = scroll.bounds();
        let offset = scroll.offset();
        let mut y = offset.y;
        match reveal {
            // Within half a pixel is there: painted rows snap to device
            // pixels, and an exact target can be missed every frame.
            Reveal::Top if (at.y - view.top()).abs() >= gpui::px(0.5) => {
                y += view.top() - at.y;
            }
            Reveal::Top => {}
            Reveal::Nearest if at.y < view.top() => y += view.top() - at.y,
            Reveal::Nearest if at.y + line > view.bottom() => {
                y -= at.y + line - view.bottom();
            }
            Reveal::Nearest => {}
        }
        // `set_offset` clamps nothing, and past the ends the document would
        // scroll away from the caret it was asked to show.
        let y = y.clamp(-scroll.max_offset().y, gpui::px(0.0));
        // Left set after a scroll: a caret far off was placed at a guess, and
        // the frame that builds what lies between can move it again.
        if y != offset.y {
            scroll.set_offset(gpui::point(offset.x, y));
            window.request_animation_frame();
        } else {
            self.reveal = None;
        }
    }

    /// A new caret position restarts its blink and ends any vertical run.
    /// Vertical motion records its next goal after moving the caret.
    pub(super) fn caret_moved(&mut self) {
        self.blink = None;
        self.goal = None;
    }

    /// Blink the caret for as long as the document holds focus.
    pub(super) fn start_blink(&mut self, cx: &mut Context<Self>) {
        self.caret_on = true;
        self.blink = Some(cx.spawn(async move |editor, cx| {
            loop {
                cx.background_executor().timer(BLINK).await;
                let flipped = editor.update(cx, |editor, cx| {
                    editor.caret_on = !editor.caret_on;
                    cx.notify();
                });
                if flipped.is_err() {
                    break;
                }
            }
        }));
    }

    /// Where typing would land — the moving end of the selection.
    pub(super) fn cursor(&self) -> Cursor {
        self.selection.head
    }

    /// Put the caret somewhere, collapsed.
    pub(super) fn place(&mut self, cursor: Cursor) {
        self.selection = Selection::at(cursor.clamp(&self.doc));
    }

    /// Move the head, extending the selection or collapsing it — the one path
    /// every motion key takes, so shift is a flag rather than a second handler.
    pub(super) fn moved(
        &mut self,
        extend: bool,
        to: impl FnOnce(Cursor, &Doc) -> Cursor,
        cx: &mut Context<Self>,
    ) {
        let head = to(self.selection.head, &self.doc).clamp(&self.doc);
        self.head_to(head, extend);
        cx.notify();
    }

    /// Left and right. At a soft wrap a collapsed caret first crosses to the
    /// other row, keeping its offset.
    pub(super) fn horizontal(&mut self, right: bool, extend: bool, cx: &mut Context<Self>) {
        let selection = self.selection;
        let crossed = match (right, selection.affinity) {
            (false, Affinity::Downstream) => Affinity::Upstream,
            (true, Affinity::Upstream) => Affinity::Downstream,
            _ => selection.affinity,
        };
        if !extend
            && selection.is_collapsed()
            && crossed != selection.affinity
            && self.layouts.wraps_at(selection.head)
        {
            self.head_to(selection.head, false);
            self.selection.affinity = crossed;
            return cx.notify();
        }
        match right {
            true => self.moved(extend, Cursor::right, cx),
            false => self.moved(extend, Cursor::left, cx),
        }
    }

    /// Home and End: the edge of the painted row holding the caret, or of its
    /// line before anything has painted.
    pub(super) fn row_edge(&mut self, end: bool, extend: bool, cx: &mut Context<Self>) {
        let (head, affinity) = (self.selection.head, self.selection.affinity);
        let painted = match end {
            true => self.layouts.row_end(head, affinity),
            false => self
                .layouts
                .row_start(head, affinity)
                .map(|at| (at, Affinity::Downstream)),
        };
        let (to, affinity) = painted.unwrap_or_else(|| {
            let line = match end {
                true => line_end(head, &self.doc),
                false => line_home(head, &self.doc),
            };
            (line, Affinity::Downstream)
        });
        self.head_to(to.clamp(&self.doc), extend);
        self.selection.affinity = affinity;
        cx.notify();
    }

    /// Delete from the caret to wherever `to` lands — every kill chord, sharing
    /// the cursor functions the motion chords use so the two cannot disagree.
    ///
    /// Nothing left to take within the block — the target crossed out of it, or
    /// landed on the caret — is the block edge, and `forward` is which edge:
    /// [`Self::delete_forward`] joins the next block, [`Self::delete_back`]
    /// outdents or strips block syntax before it merges anything. The direction
    /// has to be the chord's own, because a target that lands on the caret is
    /// the same cursor whichever way it was reaching.
    pub(super) fn delete_to(
        &mut self,
        forward: bool,
        to: impl FnOnce(Cursor, &Doc) -> Cursor,
        cx: &mut Context<Self>,
    ) {
        if !self.selection.is_collapsed() {
            return self.delete_back(cx);
        }
        let at = self.cursor();
        let target = to(at, &self.doc).clamp(&self.doc);
        if target.block != at.block || target.part != at.part || target.offset == at.offset {
            return if forward {
                self.delete_forward(cx)
            } else {
                self.delete_back(cx)
            };
        }
        self.edit(EditKind::Delete, cx, |this| {
            let splice = this
                .doc
                .edit()
                .replace(Selection::new(target, at), Text::default());
            this.selection = Selection::at(splice.caret.clamp(&this.doc));
            this.track_slash("", Vec::new());
            this.track_mention(None);
            vec![Delta::Spliced(splice)]
        });
    }

    pub(super) fn head_to(&mut self, head: Cursor, extend: bool) {
        self.selection = if extend {
            self.selection.extend_to(head)
        } else {
            Selection::at(head)
        };
        // A motion ends the undo group: typing a word, moving away and typing
        // again must not undo as one step across two places. It also spends any
        // stored mark and any open paste menu, both of which belonged to the
        // spot the caret just left.
        self.run.interrupt();
        self.stored.clear();
        self.pasted = None;
        self.reveal = Some(Reveal::Nearest);
        self.caret_moved();
    }

    /// Every mutation goes through here, so none of them can forget to record
    /// a step and none of them has to know how steps coalesce.
    pub(super) fn edit(
        &mut self,
        kind: EditKind,
        cx: &mut Context<Self>,
        edit: impl FnOnce(&mut Self) -> Vec<Delta>,
    ) {
        // Any edit answers the paste menu by ignoring it — whatever it offered
        // was about a block that no longer holds only the link.
        self.pasted = None;
        let joins =
            self.run.joins(kind, self.selection) && self.document.read(cx).history.can_undo();
        if !joins {
            self.run.interrupt();
        }
        let (mode, selection) = (self.mode, self.selection);
        self.document.update(cx, |document, _| {
            document
                .history
                .record(joins, mode, &self.doc, selection, &self.anchors)
        });
        // A list rather than one: Enter clears a selection *and* splits, and an
        // anchor mapped through only half of that lands in the wrong place.
        let deltas = edit(self);
        for delta in &deltas {
            for anchor in &mut self.anchors {
                anchor.map(delta);
            }
        }
        // Deleting the last block is the other way to an empty document, and
        // the caret belongs at the start of whatever replaces it.
        let mut replaced = false;
        if self.doc.ensure_block() {
            self.selection = Selection::at(Cursor::default());
            replaced = true;
        }
        if !self.blocks() {
            replaced |= self.ensure_source();
        }
        self.run.landed(kind, self.selection);
        self.publish(
            match replaced {
                true => Change::Replaced,
                false => Change::Edited(deltas.into()),
            },
            cx,
        );
        // Typing moves the caret as surely as an arrow key does, and a split
        // moves it onto a block that does not exist until this frame paints.
        self.reveal = Some(Reveal::Nearest);
        self.caret_moved();
        cx.emit(EditorEvent::Changed);
        cx.notify();
    }

    /// Left and right into and out of an open menu's submenu, when it has one
    /// to open or close. `false` leaves the key to the caret.
    pub(super) fn menu_side(&mut self, right: bool, cx: &mut Context<Self>) -> bool {
        let Some(slash) = &mut self.slash else {
            return self.dropdown_side(right, cx);
        };
        let moved = match right {
            true => slash.descend(),
            false => slash.cursor.ascend(),
        };
        if moved {
            cx.notify();
        }
        moved
    }

    /// Up and down, by one painted row.
    ///
    /// Geometry rather than arithmetic on line numbers, so a wrapped paragraph,
    /// a code block's lines and a table's rows are all the same case and none
    /// needs counting — but geometry walked in document order rather than
    /// hit-tested, which is [`markdown::BlockLayouts::step_row`]'s whole point.
    /// Falls back to the block-wise motion off either end of the document, and
    /// on the first frame, when nothing has painted to walk.
    pub(super) fn vertical(&mut self, down: bool, extend: bool, cx: &mut Context<Self>) {
        // Up and down walk a menu while it is open, not the document.
        let delta = if down { 1 } else { -1 };
        if let Some(pasted) = &mut self.pasted {
            pasted.step(delta);
            return cx.notify();
        }
        if let Some(slash) = &mut self.slash {
            slash.step(delta);
            return cx.notify();
        }
        if let Some(mention) = &mut self.mention {
            mention.step(delta);
            return cx.notify();
        }
        if self.dropdown_step(delta, cx) {
            return;
        }
        let (head, affinity) = (self.selection.head, self.selection.affinity);
        let Some((at, _)) = self.layouts.position(head, affinity) else {
            return self.moved(
                extend,
                |at, doc| if down { at.down(doc) } else { at.up(doc) },
                cx,
            );
        };
        let x = self.goal.unwrap_or(at.x);
        match self.layouts.step_row(head, affinity, x, down) {
            Some((to, affinity)) => {
                self.head_to(to.clamp(&self.doc), extend);
                self.selection.affinity = affinity;
                self.goal = Some(x);
            }
            // Off the top is the start of the document and off the bottom is
            // its end, which is what every native field does.
            None => {
                // Except where the end is a block a caret cannot carry on from,
                // and going down means the paragraph after it — the one a click
                // below the document asks for by the same rule.
                if down
                    && !extend
                    && self.cursor().block + 1 == self.doc.blocks.len()
                    && self.append_tail(cx)
                {
                    return;
                }
                let to = if down {
                    head.down(&self.doc)
                } else {
                    head.up(&self.doc)
                };
                self.head_to(to.clamp(&self.doc), extend);
                // The column outlives the trip to either end, so coming back
                // retraces the path.
                self.goal = Some(x);
            }
        }
        cx.notify();
    }

    /// Shift-tab in the source: take back up to one [`INDENT`] of the spaces
    /// before the caret, and nothing else — a line that is not indented has
    /// nothing to give.
    pub(super) fn unindent(&mut self, cx: &mut Context<Self>) {
        let at = self.cursor();
        let Some(text) = self.caret_text() else {
            return;
        };
        let before = &text.text[..at.offset];
        let width = before.len() - before.trim_end_matches(' ').len();
        let width = width.min(INDENT.len());
        if width == 0 {
            return;
        }
        self.edit(EditKind::Delete, cx, |this| {
            let from = Cursor::new(at.block, at.part, at.offset - width);
            let splice = this
                .doc
                .edit()
                .replace(Selection::new(from, at), Text::default());
            this.selection = Selection::at(splice.caret.clamp(&this.doc));
            vec![Delta::Spliced(splice)]
        });
    }

    /// The caret's text, for the input handler's offset arithmetic.
    pub(super) fn caret_text(&self) -> Option<&Text> {
        let at = self.cursor();
        self.doc.blocks.get(at.block)?.text_at(at.part)
    }
}