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