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 crate::{
14 anchor::{Anchor, AnchorId, Delta},
15 document::{Change, Document, DocumentEvent, Tracked},
16 history::{EditKind, Run},
17 link::{self, Choice},
18 slash::{Slash, SlashAction, SlashAt},
19 text_size,
20};
21use gpui::{
22 App, Context, CursorStyle, ElementInputHandler, Entity, EventEmitter, FocusHandle, Focusable,
23 Hsla, KeyContext, MouseButton, Render, Styled as _, Task, Window, canvas, div, prelude::*,
24};
25use markdown::{
26 Affinity, Annotation, AppExt as _, Block, BlockKind, BlockLayouts, Cursor, Doc, Form, Mark,
27 Part, Selection, Splice, Synced, Text, Typography, edit, edit::shortcut,
28};
29use std::{cell::RefCell, ops::Range, rc::Rc, time::Duration};
30use theme::Theme;
31
32mod anchors;
33mod blocks;
34mod caret;
35mod clipboard;
36pub(crate) mod image;
37mod input;
38pub mod keys;
39pub(crate) mod menu;
40pub(crate) mod mode;
41mod pointer;
42mod render;
43mod sizing;
44mod sync;
45mod table;
46mod typing;
47
48pub use keys::init;
49use keys::{
50 Backspace, Copy, Cut, DecreaseTextSize, Delete, DeleteToHome, DeleteWordLeft, DeleteWordRight,
51 Dismiss, DocumentEnd, DocumentStart, Down, DuplicateBlock, End, Home, IncreaseTextSize, Indent,
52 InsertParagraph, KillLine, Left, MoveBlockDown, MoveBlockUp, Outdent, Paste, Redo, RemoveBlock,
53 ResetTextSize, Right, SelectAll, SelectDocumentEnd, SelectDocumentStart, SelectDown, SelectEnd,
54 SelectHome, SelectLeft, SelectRight, SelectUp, SelectWordLeft, SelectWordRight, SoftBreak,
55 SplitBlock, ToggleBold, ToggleCode, ToggleHighlight, ToggleItalic, ToggleStrike, Undo, Up,
56 WordLeft, WordRight,
57};
58
59pub const CONTEXT: &str = "BezelEditor";
60
61/// The custom mark [`ToggleHighlight`] toggles. It does nothing until the app
62/// registers a mark under this name with [`markdown::AppExt::set_marks`].
63pub const HIGHLIGHT_MARK: &str = "highlight";
64
65/// [`CONTEXT`], which every binding in [`keys`] is scoped to, plus the mark
66/// that keeps `tab` for [`Editor::indent`].
67fn key_context() -> KeyContext {
68 let mut context = KeyContext::default();
69 context.add(CONTEXT);
70 context.add(ui::focus::CLAIMS_TAB);
71 context
72}
73
74/// What the editor tells its host about.
75///
76/// An app holding comment threads has to hear that the document moved, or its
77/// side of the pairing goes stale against anchors that did not. Split the way
78/// [`ui::input::FieldEvent`] is, so a listener takes only the half it needs.
79#[derive(Clone, Copy, Debug, PartialEq)]
80pub enum EditorEvent {
81 /// The document is different, and the anchors have been mapped through it.
82 Changed,
83 /// A click landed on an anchor's range.
84 AnchorActivated(AnchorId),
85 /// The pointer came over an anchor's painted text, or left it: `None`
86 /// over none, and when the pointer leaves the editor.
87 AnchorHovered(Option<AnchorId>),
88 /// The editor switched between the document and its source, which a host
89 /// lighting its own toggle has no other way to hear about — the switch can
90 /// come from an undo as well as from the button.
91 ModeChanged(Mode),
92}
93
94/// Which form the document is being edited in.
95///
96/// The trigger is the app's: a button, a menu row, a chord of its own. What is
97/// here is the switch it calls, because the caret, the undo history and the
98/// focus have to survive it, and only the editor owns all three.
99#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
100pub enum Mode {
101 /// The document as it reads.
102 #[default]
103 Blocks,
104 /// The markdown a save would write, in one editable text.
105 Source,
106}
107
108/// Which of the editor's own affordances are on.
109///
110/// All of them unless an app says otherwise: a document with nothing
111/// discoverable on it is the wrong default for a library. Turning one off is
112/// for an app that puts its own in the same place — a bar with its own block
113/// menu does not want the gutter handle's as well — rather than for trimming.
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub struct Chrome {
116 /// The gutter handle, the menu it opens, and dragging a block by it.
117 pub handle: bool,
118 /// The `/` menu at an empty block.
119 pub slash: bool,
120 /// A fence's language label, and the picker it opens.
121 pub language: bool,
122 /// The menu a pasted URL drops — leave it, or make a card, a chip or the
123 /// picture it points at.
124 pub paste: bool,
125 /// The mention menus over the app's [`crate::MentionSource`]s. Off by
126 /// default.
127 pub mention: bool,
128}
129
130impl Default for Chrome {
131 fn default() -> Self {
132 Self {
133 handle: true,
134 slash: true,
135 language: true,
136 paste: true,
137 mention: false,
138 }
139 }
140}
141
142/// What a toolbar reads to light itself, in one call.
143///
144/// Every field is a question a bar asks on every frame, and each was a separate
145/// reach into the document before: which marks are lit, what the block is
146/// called, whether cmd-E would fence, and whether any of it applies at all.
147/// Taken together so a bar cannot answer half of them from one frame and half
148/// from the next.
149#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct Formatting {
151 /// Which form the document is in. In [`Mode::Source`] the markup is already
152 /// spelled out, so [`Self::marks`] is empty and a bar has nothing to light.
153 pub mode: Mode,
154 /// The marks the selection carries throughout — and at a collapsed caret,
155 /// the ones the next character typed would carry, cmd-B before typing
156 /// included.
157 pub marks: Vec<Mark>,
158 /// What the caret's block is called in [`turns`], or `None` for a block the
159 /// menu does not offer. Matched on the kind and a heading's level alone: a
160 /// list numbered from 7 and a fence tagged `rs` report their row.
161 pub block: Option<gpui::SharedString>,
162 /// The quote the caret's block sits in — see [`quotes`].
163 pub quote: Option<markdown::Quoted>,
164 /// Whether [`Mark::Code`] here makes a fence out of the selection rather
165 /// than an inline span — the one chord whose meaning changes with what is
166 /// selected, and the one a bar cannot work out for itself.
167 pub fenceable: bool,
168}
169
170/// Every block a block can be turned into, and what each is called — the
171/// vocabulary the slash menu and the block menu both offer, for an app building
172/// a menu of its own. Pair with [`Editor::set_block`], which is what both of
173/// bezel's own menus call.
174pub fn turns() -> Vec<(gpui::SharedString, BlockKind)> {
175 crate::slash::items()
176}
177
178/// Every quote a block can be put in, and what each is called. Pair with
179/// [`Editor::toggle_quote`].
180pub fn quotes() -> Vec<(gpui::SharedString, Option<markdown::QuoteKind>)> {
181 crate::slash::quotes()
182}
183
184/// Shown on the focused block while it is empty — the only discoverable place
185/// to say that `/` does anything.
186const PLACEHOLDER: &str = "Type / for commands";
187
188/// What tab inserts in the source, where there are no blocks to indent. Two
189/// spaces, which is what markdown's own nesting is written in.
190const INDENT: &str = " ";
191
192/// Half the caret's blink period — `ui::TextField`'s, which is the 500ms on,
193/// 500ms off macOS itself uses.
194const BLINK: Duration = Duration::from_millis(500);
195
196/// The handle's box. Wide enough to be hit without crowding the margin; how
197/// far left of the text it sits is [`Layout::text_inset`](crate::Layout).
198const HANDLE_SIZE: f32 = 18.0;
199
200/// How far a drag on an image's edge handle can shrink it — matches
201/// `markdown::render`'s `TABLE_MIN_COLUMN_WIDTH`, the same floor for the
202/// other block content a drag can resize.
203const MIN_IMAGE_WIDTH: f32 = 96.0;
204
205/// Whether a selection is prose covering more than one line — two blocks, or
206/// one line break inside a single block.
207///
208/// What a chord means about a selection is the editor's to decide; a [`Doc`]
209/// has no opinion about it. A selection reaching into a table or a fence is not
210/// prose and keeps the inline behaviour, because half a table has no lines to
211/// make a fence out of.
212fn fenceable(doc: &Doc, selection: Selection) -> bool {
213 let spans = doc.spans(selection);
214 let covered = |at: &Cursor, range: &Range<usize>| {
215 doc.blocks[at.block]
216 .text_at(at.part)
217 .and_then(|text| text.text.get(range.clone()))
218 };
219 spans.iter().all(|(at, _)| at.part == Part::Body)
220 && (spans.len() > 1
221 || spans
222 .iter()
223 .any(|(at, range)| covered(at, range).is_some_and(|text| text.contains('\n'))))
224}
225
226fn line_home(at: Cursor, doc: &Doc) -> Cursor {
227 let Some(text) = doc
228 .blocks
229 .get(at.block)
230 .and_then(|block| block.text_at(at.part))
231 else {
232 return at.home();
233 };
234 Cursor {
235 offset: ui::input::line_start(&text.text, at.offset.min(text.text.len())),
236 ..at
237 }
238}
239
240fn line_end(at: Cursor, doc: &Doc) -> Cursor {
241 let Some(text) = doc
242 .blocks
243 .get(at.block)
244 .and_then(|block| block.text_at(at.part))
245 else {
246 return at.end(doc);
247 };
248 Cursor {
249 offset: ui::input::line_end(&text.text, at.offset.min(text.text.len())),
250 ..at
251 }
252}
253
254/// The document a source view is edited as: one fence holding the markdown.
255///
256/// A fence rather than a paragraph because a fence is the block whose caret
257/// already behaves like a plain text editor's — Enter is a newline, nothing
258/// typed into it is markup, and its lines are laid out one per source line.
259fn source_doc(source: &str) -> Doc {
260 Doc {
261 blocks: vec![Block::new(BlockKind::Code {
262 language: Some(markdown::source::LANGUAGES[0].to_string()),
263 code: Text::plain(source),
264 height: None,
265 })],
266 }
267}
268
269/// A floating menu a block drops. A `Popup` rather than an `Option` for the
270/// exit phase, and for the press note: the card's `on_mouse_down_out` fires on
271/// the *press*, so without one a trigger's click on the *release* reopens what
272/// it just shut.
273pub(crate) type MenuPopup<T> = ui::popover::Popup<Dropped<T>>;
274
275/// What a dropped menu belongs to, where it hangs, and its live row.
276pub(crate) struct Dropped<T> {
277 pub of: T,
278 pub at: gpui::Point<gpui::Pixels>,
279 pub cursor: ui::menu::Cursor,
280}
281
282impl<T> Dropped<T> {
283 pub fn new(of: T, at: gpui::Point<gpui::Pixels>) -> Self {
284 Self {
285 of,
286 at,
287 cursor: ui::menu::Cursor::default(),
288 }
289 }
290}
291
292/// The row, column, or cell whose table menu is open.
293#[derive(Clone, Copy, PartialEq, Eq)]
294pub(crate) enum TableTarget {
295 Row(usize),
296 Column(usize),
297 Cell { row: usize, column: usize },
298}
299
300/// A table's row or column, as [`markdown::Part::Cell`] numbers them.
301#[derive(Clone, Copy, Debug, PartialEq, Eq)]
302pub(crate) enum Line {
303 Row(usize),
304 Column(usize),
305}
306
307/// Where a reveal leaves the caret's row in the scroll box.
308#[derive(Clone, Copy, Debug, PartialEq, Eq)]
309pub(crate) enum Reveal {
310 /// The least scroll that brings the row inside the box.
311 Nearest,
312 /// The row at the box's top edge, or as near it as the document's end
313 /// allows.
314 Top,
315}
316
317/// One view of a [`Document`].
318///
319/// Every edit notifies the entity and emits [`EditorEvent::Changed`].
320///
321/// The slash menu, the gutter handle and its block drag, a fence's language
322/// picker, the pasted-URL menu, undo and the clipboard need no wiring;
323/// [`Chrome`] turns the menus off. Hovering a table cell shows handles for its
324/// row and column: a click opens insert, delete and move, a drag reorders, and
325/// `escape` or a drop outside the table cancels. A right press on a cell opens
326/// both sets. The strips along a table's bottom and right edges append a row
327/// and a column.
328pub struct Editor {
329 document: Entity<Document>,
330 /// This editor's snapshot of the document, its form, its marks and its
331 /// anchors: what it edits, handed back to [`Self::document`] after each
332 /// change, and replaced whenever another editor changes the document.
333 doc: Tracked,
334 /// The source view's text and the [`Tracked::revision`] it was built at.
335 /// Read only in [`Mode::Source`].
336 source_view: Option<(u64, markdown::SourceText)>,
337 /// Highlighting the source view's text after an edit. Dropping it cancels.
338 source_spans: Option<Task<()>>,
339 marks: markdown::Marks,
340 /// The document's markdown and blocks, shared with it — see
341 /// [`Document`]'s field of the name.
342 synced: Rc<RefCell<Synced>>,
343 /// Which of the editor's own affordances paint. The app's, so one document
344 /// can carry the lot and another none of it.
345 chrome: Chrome,
346 /// In [`Mode::Source`] `doc` is one fenced block holding the markdown, so
347 /// every operation below that is about *blocks* asks [`Editor::blocks`]
348 /// first.
349 mode: Mode,
350 /// Collapsed for an ordinary caret, so there is one position here rather
351 /// than a caret and a range that can disagree.
352 selection: Selection,
353 focus_handle: FocusHandle,
354 /// The IME composition range within the caret's text, underlined while it
355 /// is being composed.
356 marked: Option<Range<usize>>,
357 /// Where each text landed last frame, so a click can be turned into a
358 /// caret. Only paint knows this, so the renderer fills it.
359 layouts: BlockLayouts,
360 run: Run,
361 /// Which half of the blink the caret is in. Flipped by [`Self::start_blink`].
362 caret_on: bool,
363 /// The blink, alive only while the document holds focus.
364 blink: Option<Task<()>>,
365 anchors: Vec<Anchor>,
366 /// Ranges washed for one frame's worth of the app's say — find hits. Not
367 /// mapped through edits and not in the history: the app sets them again.
368 washes: Vec<(Selection, Annotation)>,
369 /// Marks the next typed character will carry — cmd-B at a collapsed caret,
370 /// which otherwise has no range to apply to and so would do nothing.
371 /// Cleared by any motion, because they belong to a spot and not to a mood.
372 stored: Vec<Mark>,
373 /// The open slash menu, if `/` started one.
374 slash: Option<Slash>,
375 mention: Option<crate::mention::MentionMenu>,
376 /// The open paste menu, if a URL landed in a block of its own.
377 pasted: Option<link::Paste>,
378 /// The open prompt, if an image is waiting to be told where to look.
379 url_prompt: Option<image::Prompt>,
380 /// The block a file being dragged over the document would land after.
381 dropping: Option<usize>,
382 /// The block the pointer is over, which is the only one showing a handle.
383 hovered: Option<usize>,
384 /// The table cell the pointer is over, whose row and column show handles.
385 hovered_cell: Option<(usize, Part)>,
386 /// A table row's or column's menu: the table, the line and where it hangs.
387 table_drag: Option<table::TableDrag>,
388 table_dragged: bool,
389 table_menu: MenuPopup<(usize, TableTarget)>,
390 /// The right-click menu on text, with its rows as they stood when it
391 /// opened.
392 text_menu: MenuPopup<Vec<ui::menu::Item>>,
393 /// The right-click menu on a picture.
394 image_menu: MenuPopup<markdown::PictureMenu>,
395 /// A block being dragged by its handle, and where it would land.
396 lifted: Option<(usize, usize)>,
397 /// An image being dragged wider or narrower by its edge handle, and the
398 /// width it holds now — `None` being the natural one, exactly as the
399 /// document spells it. The document only learns the final width on
400 /// release, the same reason `lifted` waits for the drop; carrying the
401 /// document's own value here is what makes a press that never moved
402 /// read back as no change at all.
403 resizing: Option<(usize, Option<u32>)>,
404 /// A painted block being dragged taller or shorter by its bottom handle.
405 sizing: Option<sizing::Sizing>,
406 /// The block menu the handle opened, over the block and its rows.
407 block_menu: MenuPopup<(usize, crate::slash::Rows)>,
408 /// The language menu a fence's header opened, over its block.
409 language_menu: MenuPopup<usize>,
410 /// Set by a floating layer's press — the gutter handle, the URL prompt —
411 /// so the editor's own press does not undo what that press just did.
412 press_claimed: bool,
413 /// Where the editor's own box starts, so a position recorded in window
414 /// coordinates can be placed inside it, and how far it reaches — which is
415 /// how wide a resized image is allowed to be. Its own box rather than the
416 /// picture's: a picture already narrowed would otherwise be its own
417 /// ceiling, and no drag could ever widen it again.
418 origin: gpui::Point<gpui::Pixels>,
419 width: gpui::Pixels,
420 /// The press being dragged out: what it selects by, and what it selected.
421 dragging: Option<(ui::input::Granularity, Range<Cursor>)>,
422 /// Whether the pointer is over painted text, which is the only place the
423 /// editor claims an I-beam.
424 over_text: bool,
425 /// The anchor whose text the pointer is over — see
426 /// [`EditorEvent::AnchorHovered`].
427 over_anchor: Option<AnchorId>,
428 /// The host's scroll box, when it gave one, and the reveal the caret still
429 /// owes it.
430 scroll: Option<gpui::ScrollHandle>,
431 reveal: Option<Reveal>,
432 /// Where a held drag last was, in window coordinates.
433 drag_at: Option<gpui::Point<gpui::Pixels>>,
434 /// Scrolls [`Self::scroll`] while a drag is held past its top or bottom.
435 edge_scroll: Option<Task<()>>,
436 /// Where the gutter handle was placed this frame, so the frame after can
437 /// tell whether the block moved out from under it.
438 handle_at: Option<gpui::Point<gpui::Pixels>>,
439 /// The column held across consecutive vertical moves.
440 goal: Option<gpui::Pixels>,
441 /// The size the app set this document in, in points, or `None` to follow
442 /// the app's own text size. Absolute rather than a factor over the ladder,
443 /// so moving the interface size leaves a document set to 16pt at 16pt.
444 ///
445 /// What the chords move is the shared adjustment on top of this; the base
446 /// itself is the app's alone.
447 text_size: Option<f32>,
448 /// Overrides document text-size settings for an embedded editing surface.
449 pub typography: Option<Typography>,
450 pub text_color: Option<Hsla>,
451 /// The directory relative image paths resolve against.
452 base: Option<std::path::PathBuf>,
453 image_overlay: Option<markdown::ImageOverlay>,
454 image_overlay_corner: markdown::ImageOverlayCorner,
455 _document: gpui::Subscription,
456}
457
458impl Editor {
459 /// An editor on a document of its own.
460 pub fn new(source: &str, cx: &mut Context<Self>) -> Self {
461 let document = cx.new(|cx| Document::new(source, cx));
462 Self::for_document(document, cx)
463 }
464
465 /// An editor on `document`, beside any others already on it.
466 pub fn for_document(document: Entity<Document>, cx: &mut Context<Self>) -> Self {
467 let (doc, mode, marks, synced, anchors) = {
468 let document = document.read(cx);
469 (
470 document.doc.clone(),
471 document.mode,
472 document.marks.clone(),
473 document.synced.clone(),
474 document.anchors.clone(),
475 )
476 };
477 let subscription = cx.subscribe(&document, |this, document, event: &DocumentEvent, cx| {
478 if event.by != Some(cx.entity_id()) {
479 this.take_document(&document, &event.change, cx);
480 }
481 });
482 Self {
483 document,
484 marks,
485 // Clamped, not defaulted: a document opening on a fence or a table
486 // has no body at block zero, and a caret claiming one resolves
487 // against nothing until something moves it.
488 selection: Selection::at(Cursor::default().clamp(&doc)),
489 doc,
490 mode,
491 synced,
492 anchors,
493 source_view: None,
494 source_spans: None,
495 chrome: Chrome::default(),
496 focus_handle: cx.focus_handle(),
497 marked: None,
498 layouts: BlockLayouts::default(),
499 run: Run::default(),
500 caret_on: true,
501 blink: None,
502 washes: Vec::new(),
503 stored: Vec::new(),
504 slash: None,
505 mention: None,
506 pasted: None,
507 url_prompt: None,
508 dropping: None,
509 hovered: None,
510 hovered_cell: None,
511 table_menu: Default::default(),
512 text_menu: Default::default(),
513 image_menu: Default::default(),
514 table_drag: None,
515 table_dragged: false,
516 lifted: None,
517 resizing: None,
518 sizing: None,
519 block_menu: MenuPopup::default(),
520 language_menu: MenuPopup::default(),
521 press_claimed: false,
522 origin: gpui::Point::default(),
523 width: gpui::Pixels::ZERO,
524 dragging: None,
525 over_text: false,
526 over_anchor: None,
527 scroll: None,
528 reveal: None,
529 drag_at: None,
530 edge_scroll: None,
531 goal: None,
532 handle_at: None,
533 text_size: None,
534 typography: None,
535 text_color: None,
536 base: None,
537 image_overlay: None,
538 image_overlay_corner: markdown::ImageOverlayCorner::default(),
539 _document: subscription,
540 }
541 }
542
543 /// The document this editor is a view of.
544 pub fn document(&self) -> &Entity<Document> {
545 &self.document
546 }
547
548 /// Which of the editor's own affordances to paint. See [`Chrome`].
549 pub fn with_chrome(mut self, chrome: Chrome) -> Self {
550 self.chrome = chrome;
551 self
552 }
553
554 /// Adds an app-provided hover control to pictures in rich mode.
555 pub fn with_image_overlay(mut self, overlay: markdown::ImageOverlay) -> Self {
556 self.image_overlay = Some(overlay);
557 self
558 }
559
560 /// Replaces or removes the picture hover control.
561 pub fn set_image_overlay(
562 &mut self,
563 overlay: Option<markdown::ImageOverlay>,
564 cx: &mut Context<Self>,
565 ) {
566 self.image_overlay = overlay;
567 cx.notify();
568 }
569
570 /// Positions picture hover controls; bottom-right by default.
571 pub fn with_image_overlay_corner(mut self, corner: markdown::ImageOverlayCorner) -> Self {
572 self.image_overlay_corner = corner;
573 self
574 }
575
576 /// Repositions the picture hover control.
577 pub fn set_image_overlay_corner(
578 &mut self,
579 corner: markdown::ImageOverlayCorner,
580 cx: &mut Context<Self>,
581 ) {
582 self.image_overlay_corner = corner;
583 cx.notify();
584 }
585
586 /// What is painting now, for an app whose own bar mirrors it.
587 pub fn chrome(&self) -> Chrome {
588 self.chrome
589 }
590
591 /// Open the document at a size of its own, in points — the app's settings
592 /// field for prose. Unset, it is set at the app's own text size.
593 ///
594 /// The base, not the current size: `cmd-+` moves a shared adjustment over
595 /// this, and `cmd-0` clears that adjustment to come back here.
596 pub fn with_text_size(mut self, points: f32) -> Self {
597 self.text_size = Some(points);
598 self
599 }
600
601 /// Update the configured base size without changing the temporary zoom.
602 pub fn set_text_size(&mut self, points: f32, cx: &mut Context<Self>) {
603 if self.text_size != Some(points) {
604 self.text_size = Some(points);
605 cx.notify();
606 }
607 }
608
609 /// The base the app set, if any. Add
610 /// [`crate::AppExt::editor_text_size_adjustment`](crate::AppExt::editor_text_size_adjustment) for what is on
611 /// screen.
612 pub fn text_size(&self) -> Option<f32> {
613 self.text_size
614 }
615
616 pub fn resolved_typography(&self, cx: &App) -> Typography {
617 self.typography.unwrap_or_else(|| {
618 cx.typography()
619 .scaled(text_size::resolve(self.text_size, cx) / theme::base_text_size())
620 })
621 }
622
623 /// Resolve relative image paths against `dir` — the document's own folder,
624 /// for a document that keeps its pictures beside it. The stored URL stays
625 /// as written.
626 pub fn with_base(mut self, dir: impl Into<std::path::PathBuf>) -> Self {
627 self.base = Some(dir.into());
628 self
629 }
630
631 /// Change or clear the directory relative image paths resolve against.
632 pub fn set_base(&mut self, dir: Option<std::path::PathBuf>, cx: &mut Context<Self>) {
633 if self.base != dir {
634 self.base = dir;
635 cx.notify();
636 }
637 }
638
639 /// The directory relative image paths resolve against, if one is set.
640 pub fn base(&self) -> Option<&std::path::Path> {
641 self.base.as_deref()
642 }
643
644 /// The box the document scrolls in, so typing off the bottom follows the
645 /// caret down and a drag held past its top or bottom scrolls it.
646 ///
647 /// The host's rather than the editor's: a document goes in whatever pane
648 /// the app gives it, and the gutter handle, the drop indicator and the
649 /// menus are all placed absolutely against this editor's own origin — put
650 /// the scroll box here and every one of them would be offset twice.
651 pub fn with_scroll(mut self, handle: gpui::ScrollHandle) -> Self {
652 self.scroll = Some(handle);
653 self
654 }
655
656 pub fn doc(&self) -> &Doc {
657 &self.doc
658 }
659
660 pub fn selection(&self) -> Selection {
661 self.selection
662 }
663
664 /// Put the selection somewhere — what a thread in a sidebar does when it is
665 /// clicked, and what a caret restored with a document needs.
666 ///
667 /// Clamped, because the caller's range came from somewhere the document may
668 /// have moved on from.
669 pub fn select(&mut self, selection: Selection, cx: &mut Context<Self>) {
670 self.select_revealing(selection, Reveal::Nearest, cx);
671 }
672
673 /// [`Self::select`], scrolling the caret's row to the top of the scroll
674 /// box — what a jump to a heading wants, from either side of it.
675 pub fn select_to_top(&mut self, selection: Selection, cx: &mut Context<Self>) {
676 self.select_revealing(selection, Reveal::Top, cx);
677 }
678
679 fn select_revealing(&mut self, selection: Selection, reveal: Reveal, cx: &mut Context<Self>) {
680 self.selection = selection.clamp(&self.doc);
681 self.run.interrupt();
682 self.reveal = Some(reveal);
683 self.caret_moved();
684 cx.notify();
685 }
686
687 /// Where everything landed last frame — blocks, pictures, a fence's
688 /// language label, and the row rects of any range.
689 ///
690 /// Handed out whole rather than a method per question: an app placing
691 /// chrome of its own asks the geometry, and which question it needs is not
692 /// this crate's to guess. See [`markdown::BlockLayouts`].
693 pub fn layouts(&self) -> &BlockLayouts {
694 &self.layouts
695 }
696
697 /// Where the selection sits on screen, in window coordinates, so a host can
698 /// float a toolbar at it.
699 ///
700 /// The head's row only — a selection spanning ten blocks wants its bubble
701 /// where the pointer left off, not centred over the whole span. `None` when
702 /// nothing is selected or the caret has not painted yet.
703 pub fn selection_bounds(&self) -> Option<gpui::Bounds<gpui::Pixels>> {
704 // The head alone. A bar centred over the whole selection wants
705 // `layouts().rects(selection)`, which is every painted row of it.
706 if self.selection.is_collapsed() {
707 return None;
708 }
709 let (point, line_height) = self
710 .layouts
711 .position(self.selection.head, self.selection.affinity)?;
712 Some(gpui::Bounds::new(
713 point,
714 gpui::size(gpui::px(0.0), line_height),
715 ))
716 }
717}
718
719impl EventEmitter<EditorEvent> for Editor {}
720
721impl Focusable for Editor {
722 fn focus_handle(&self, _: &App) -> FocusHandle {
723 self.focus_handle.clone()
724 }
725}