Skip to main content

editor/editor/
render.rs

1use super::*;
2use crate::AppExt as _;
3use std::rc::Rc;
4use ui::AppExt as _;
5
6impl Render for Editor {
7    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
8        if let Some(mention) = &mut self.mention {
9            mention.refresh(cx);
10        }
11        let theme = Theme::of(cx).clone();
12        let layout = cx.editor_layout();
13        let focused = self.focus_handle.is_focused(window);
14        // The only place the blink starts: `caret_moved` drops the task, so the
15        // next render brings it back in phase, lit beat first.
16        if focused && cx.caret_blink() {
17            if self.blink.is_none() {
18                self.start_blink(cx);
19            }
20        } else {
21            self.blink = None;
22            self.caret_on = true;
23        }
24        let selection = focused.then_some(self.selection);
25        // The source line a reveal owed in source mode lands on.
26        let reveal_line = match self.mode {
27            Mode::Source => self
28                .source_text()
29                .get(..self.selection.head.offset)
30                .map_or(0, |before| before.matches('\n').count()),
31            Mode::Blocks => 0,
32        };
33        // gpui ends an outside file drag — left the window or released
34        // elsewhere — without a drop here, so the indicator goes with it.
35        if !cx.has_active_drag() {
36            self.dropping = None;
37        }
38
39        // Typed text and IME reach an entity only through an input handler
40        // registered during *paint*, against the bounds it should be anchored
41        // to. There is no custom element here to do that from, so a zero-cost
42        // canvas over the document supplies the paint phase. Without this the
43        // key bindings still fire and nothing types.
44        let handle = self.focus_handle.clone();
45        let entity = cx.entity();
46        let in_drag = self.in_drag();
47        let input = canvas(
48            |_, _, _| (),
49            move |bounds, _, window, cx| {
50                // `on_mouse_move` hears the pointer only over this box, and a
51                // drag goes on past it. Registering it is paint's alone.
52                if in_drag {
53                    let entity = entity.clone();
54                    window.on_mouse_event(move |event: &gpui::MouseMoveEvent, phase, _, cx| {
55                        if phase == gpui::DispatchPhase::Bubble && event.dragging() {
56                            entity.update(cx, |this, cx| this.drag_to(event.position, cx));
57                        }
58                    });
59                }
60                // The gutter handle is placed from positions recorded in window
61                // coordinates, so the box they have to be measured against is
62                // taken here — the one place that knows it.
63                entity.update(cx, |this, _| {
64                    this.origin = bounds.origin;
65                    this.width = bounds.size.width;
66                });
67                window.handle_input(
68                    &handle,
69                    ElementInputHandler::new(bounds, entity.clone()),
70                    cx,
71                );
72            },
73        )
74        .absolute()
75        .size_full();
76
77        // A tab stop, so the editor is reachable the same way every other
78        // control in the library is.
79        let handle = self.focus_handle.clone().tab_stop(true);
80
81        div()
82            // Stateful only so the pointer leaving can be heard: `on_hover` is
83            // what tells the gutter handle to stop pointing at a block the
84            // pointer left behind.
85            .id("bezel-editor")
86            // The mark is what keeps `tab`: without it traversal answers the
87            // key first and the caret never sees it.
88            .key_context(key_context())
89            .track_focus(&handle)
90            // Tracking focus does not take it. Without this, clicking into the
91            // document blurs the editor instead of putting a caret in it, and
92            // the caret vanishes on the first click.
93            .on_mouse_down(
94                MouseButton::Left,
95                cx.listener(|this, event: &gpui::MouseDownEvent, window, cx| {
96                    // The handle's own listener runs first and claims the
97                    // press; without the flag this would close the menu it
98                    // just opened. `ui::popover::Popup` solves it the same way.
99                    if std::mem::take(&mut this.press_claimed) {
100                        this.focus_handle.clone().focus(window, cx);
101                    } else {
102                        this.pressed(
103                            event.position,
104                            event.click_count,
105                            event.modifiers,
106                            window,
107                            cx,
108                        );
109                    }
110                    // What `press` reads to skip a press this box already
111                    // took. It also stops gpui's own focus transfer, which runs
112                    // after this listener, so both arms focus by hand.
113                    window.prevent_default();
114                }),
115            )
116            .on_mouse_down(
117                MouseButton::Right,
118                cx.listener(|this, event: &gpui::MouseDownEvent, window, cx| {
119                    if this.blocks()
120                        && let Some((block, Part::Cell { row, column })) =
121                            this.layouts.cell_at(event.position)
122                    {
123                        this.table_menu.open(Dropped::new(
124                            (block, TableTarget::Cell { row, column }),
125                            event.position,
126                        ));
127                        this.focus_handle.focus(window, cx);
128                    } else {
129                        this.right_pressed(event.position, window, cx);
130                    }
131                    window.prevent_default();
132                    cx.stop_propagation();
133                    cx.notify();
134                }),
135            )
136            // The drag has to be tracked from the container rather than from a
137            // payload: a text selection has nothing to carry, and gpui's drag
138            // payload is for things being dropped somewhere.
139            .on_hover(cx.listener(|this, hovered: &bool, _, cx| {
140                if !*hovered && (this.hovered.take().is_some() | this.hovered_cell.take().is_some())
141                {
142                    cx.notify();
143                }
144            }))
145            .on_mouse_move(cx.listener(|this, event: &gpui::MouseMoveEvent, _, cx| {
146                // Ahead of every drag branch below, because the pointer's shape
147                // is about where it *is* rather than about what it is doing.
148                let over_text = this.layouts.over_text(event.position);
149                if over_text != this.over_text {
150                    this.over_text = over_text;
151                    cx.notify();
152                }
153                // A drag in flight is followed by the window-wide listener
154                // `render` registers; otherwise the pointer only decides which
155                // block wears the handle.
156                if this.in_drag() {
157                    return;
158                }
159                let hovered = this.layouts.block_at(event.position);
160                let cell = this.layouts.cell_at(event.position).or_else(|| {
161                    // Keep the target while crossing its control lanes.
162                    this.hovered_cell.filter(|(block, _)| {
163                        this.layouts
164                            .block_bounds(*block)
165                            .is_some_and(|bounds| bounds.contains(&event.position))
166                    })
167                });
168                if hovered != this.hovered || cell != this.hovered_cell {
169                    this.hovered = hovered;
170                    this.hovered_cell = cell;
171                    cx.notify();
172                }
173            }))
174            // Both, because a release can land anywhere on screen and only the
175            // first fires over the editor. A resize left running would leave
176            // its stand-in picture painted over the document for good.
177            .on_mouse_up_out(
178                MouseButton::Left,
179                cx.listener(|this, event: &gpui::MouseUpEvent, window, cx| {
180                    this.drag_table_to(event.position, cx);
181                    this.dragging = None;
182                    this.drop_resize(window, cx);
183                    this.drop_height(cx);
184                    this.drop_table_drag(cx);
185                }),
186            )
187            .on_mouse_up(
188                MouseButton::Left,
189                cx.listener(|this, event: &gpui::MouseUpEvent, window, cx| {
190                    this.drag_table_to(event.position, cx);
191                    this.dragging = None;
192                    if this.drop_table_drag(cx) {
193                        return;
194                    }
195                    if this.drop_resize(window, cx) {
196                        return;
197                    }
198                    if this.drop_height(cx) {
199                        return;
200                    }
201                    let Some((from, to)) = this.lifted.take() else {
202                        return;
203                    };
204                    if from == to {
205                        // A press that never moved is a click, and a click on
206                        // the handle is what opens the menu — unless that same
207                        // press is what dismissed it, which the note taken on
208                        // the way down is the only way to tell.
209                        if !this.block_menu.take_press_was_open() {
210                            let rows = crate::slash::Rows::new(crate::block_menu::installed(cx));
211                            this.block_menu
212                                .open(Dropped::new((from, rows), event.position));
213                        }
214                        return cx.notify();
215                    }
216                    this.edit(EditKind::Structure, cx, |this| {
217                        // `move_block` steps one sibling at a time, so a drop
218                        // several blocks away is that many steps. Bounded by
219                        // the block count, which no drag can exceed.
220                        let delta = if to > from { 1 } else { -1 };
221                        let mut at = from;
222                        // Each step is its own move, so each is its own delta —
223                        // folding them into one would have to compose the
224                        // hops, and they are already in order.
225                        let mut deltas = Vec::new();
226                        for _ in 0..this.doc.blocks.len() {
227                            let span = this.doc.subtree(at);
228                            let Some(next) = this.doc.move_block(at, delta) else {
229                                break;
230                            };
231                            deltas.push(Delta::Moved {
232                                at: span,
233                                to: Some(next),
234                            });
235                            at = next;
236                            if (delta > 0 && at >= to) || (delta < 0 && at <= to) {
237                                break;
238                            }
239                        }
240                        this.selection =
241                            Selection::at(Cursor::new(at, Part::Body, 0).clamp(&this.doc));
242                        deltas
243                    });
244                }),
245            )
246            .on_action(cx.listener(Self::backspace))
247            .on_action(cx.listener(Self::delete))
248            .on_action(cx.listener(|this, _: &KillLine, _, cx| this.delete_to(true, line_end, cx)))
249            .on_action(cx.listener(|this, _: &DeleteWordLeft, _, cx| {
250                this.delete_to(false, Cursor::word_left, cx)
251            }))
252            .on_action(cx.listener(|this, _: &DeleteWordRight, _, cx| {
253                this.delete_to(true, Cursor::word_right, cx)
254            }))
255            .on_action(cx.listener(|this, _: &DeleteToHome, _, cx| {
256                this.delete_to(false, |at, _| at.home(), cx)
257            }))
258            .on_action(cx.listener(Self::split_block))
259            .on_action(cx.listener(Self::soft_break))
260            .on_action(cx.listener(Self::insert_paragraph))
261            .on_action(cx.listener(Self::indent))
262            .on_action(cx.listener(Self::increase_text_size))
263            .on_action(cx.listener(Self::decrease_text_size))
264            .on_action(cx.listener(Self::reset_text_size))
265            .on_action(cx.listener(Self::outdent))
266            .on_action(cx.listener(Self::dismiss))
267            .on_action(cx.listener(Self::select_all))
268            .on_action(cx.listener(Self::copy))
269            .on_action(cx.listener(Self::cut))
270            .on_action(cx.listener(Self::paste))
271            .on_action(cx.listener(Self::undo))
272            .on_action(cx.listener(Self::redo))
273            .on_action(cx.listener(Self::confirm_url))
274            .on_action(cx.listener(Self::cancel_url))
275            // A file crossing the document lights the same indicator a lifted
276            // block does, so a drop from outside lands where it looks like it
277            // will.
278            .on_drag_move(cx.listener(
279                |this, event: &gpui::DragMoveEvent<gpui::ExternalPaths>, _, cx| {
280                    let at = event.event.position;
281                    let over = event
282                        .bounds
283                        .contains(&at)
284                        .then(|| this.layouts.block_at(at))
285                        .flatten();
286                    if over != this.dropping {
287                        this.dropping = over;
288                        cx.notify();
289                    }
290                },
291            ))
292            .on_drop(
293                cx.listener(|this, paths: &gpui::ExternalPaths, window, cx| {
294                    this.focus_handle.clone().focus(window, cx);
295                    this.drop_paths(paths, cx);
296                }),
297            )
298            .on_action(cx.listener(|this, _: &ToggleBold, _, cx| this.toggle_mark(Mark::Bold, cx)))
299            .on_action(
300                cx.listener(|this, _: &ToggleItalic, _, cx| this.toggle_mark(Mark::Italic, cx)),
301            )
302            .on_action(
303                cx.listener(|this, _: &ToggleStrike, _, cx| this.toggle_mark(Mark::Strike, cx)),
304            )
305            .on_action(cx.listener(|this, _: &ToggleCode, _, cx| this.toggle_mark(Mark::Code, cx)))
306            .on_action(cx.listener(|this, _: &ToggleHighlight, _, cx| {
307                if this.marks.delimiter(HIGHLIGHT_MARK).is_some() {
308                    this.toggle_mark(Mark::Custom(HIGHLIGHT_MARK.into()), cx);
309                }
310            }))
311            .on_action(cx.listener(|this, _: &MoveBlockUp, _, cx| {
312                this.move_block(this.cursor().block, -1, cx)
313            }))
314            .on_action(cx.listener(|this, _: &MoveBlockDown, _, cx| {
315                this.move_block(this.cursor().block, 1, cx)
316            }))
317            .on_action(cx.listener(|this, _: &DuplicateBlock, _, cx| {
318                this.duplicate_block(this.cursor().block, cx)
319            }))
320            .on_action(cx.listener(|this, _: &RemoveBlock, _, cx| {
321                this.remove_block(this.cursor().block, cx)
322            }))
323            // Motion is one method with a `Cursor` function and an "extend"
324            // flag, so a shift variant cannot drift from the key it shadows.
325            .on_action(cx.listener(|this, _: &Left, _, cx| {
326                if !this.menu_side(false, cx) {
327                    this.moved(false, Cursor::left, cx)
328                }
329            }))
330            .on_action(cx.listener(|this, _: &Right, _, cx| {
331                if !this.menu_side(true, cx) {
332                    this.moved(false, Cursor::right, cx)
333                }
334            }))
335            .on_action(cx.listener(|this, _: &Up, _, cx| this.vertical(false, false, cx)))
336            .on_action(cx.listener(|this, _: &Down, _, cx| this.vertical(true, false, cx)))
337            .on_action(cx.listener(|this, _: &Home, _, cx| this.moved(false, line_home, cx)))
338            .on_action(cx.listener(|this, _: &End, _, cx| this.moved(false, line_end, cx)))
339            .on_action(cx.listener(|this, _: &DocumentStart, _, cx| {
340                this.moved(false, |_, doc| Selection::all(doc).anchor, cx)
341            }))
342            .on_action(cx.listener(|this, _: &DocumentEnd, _, cx| {
343                this.moved(false, |_, doc| Selection::all(doc).head, cx)
344            }))
345            .on_action(
346                cx.listener(|this, _: &WordLeft, _, cx| this.moved(false, Cursor::word_left, cx)),
347            )
348            .on_action(
349                cx.listener(|this, _: &WordRight, _, cx| this.moved(false, Cursor::word_right, cx)),
350            )
351            .on_action(
352                cx.listener(|this, _: &SelectLeft, _, cx| this.moved(true, Cursor::left, cx)),
353            )
354            .on_action(
355                cx.listener(|this, _: &SelectRight, _, cx| this.moved(true, Cursor::right, cx)),
356            )
357            .on_action(cx.listener(|this, _: &SelectUp, _, cx| this.vertical(false, true, cx)))
358            .on_action(cx.listener(|this, _: &SelectDown, _, cx| this.vertical(true, true, cx)))
359            .on_action(cx.listener(|this, _: &SelectHome, _, cx| this.moved(true, line_home, cx)))
360            .on_action(cx.listener(|this, _: &SelectEnd, _, cx| this.moved(true, line_end, cx)))
361            .on_action(cx.listener(|this, _: &SelectDocumentStart, _, cx| {
362                this.moved(true, |_, doc| Selection::all(doc).anchor, cx)
363            }))
364            .on_action(cx.listener(|this, _: &SelectDocumentEnd, _, cx| {
365                this.moved(true, |_, doc| Selection::all(doc).head, cx)
366            }))
367            .on_action(cx.listener(|this, _: &SelectWordLeft, _, cx| {
368                this.moved(true, Cursor::word_left, cx)
369            }))
370            .on_action(cx.listener(|this, _: &SelectWordRight, _, cx| {
371                this.moved(true, Cursor::word_right, cx)
372            }))
373            .w_full()
374            // Text under the pointer, so the pointer says so — and only there,
375            // or while a drag is still sweeping one out. The editor's box
376            // reaches over its gutter, the margin beside a short line, a rule,
377            // an image and a card, none of which a caret can be put into.
378            // Where it has nothing to say it stays quiet rather than
379            // overriding the page with an arrow of its own.
380            .when(self.over_text || self.dragging.is_some(), |el| {
381                el.cursor(CursorStyle::IBeam)
382            })
383            // No focus ring. A ring says *widget*, and a document is not one —
384            // the caret already paints only while focused, so a box around the
385            // whole page is a second, louder signal for the same fact.
386            .relative()
387            .child(input)
388            // A height drag holds the resize cursor wherever the pointer
389            // runs, over the editor's text and the card's own cursors alike.
390            .when(self.sizing.is_some(), |el| {
391                el.child(
392                    canvas(
393                        |_, _, _| (),
394                        |_, _, window, _| window.set_window_cursor_style(CursorStyle::ResizeUpDown),
395                    )
396                    .absolute()
397                    .size_0(),
398                )
399            })
400            // The document is inset by the gutter so the handle has somewhere
401            // to sit *inside* the editor. Outside it the handle is clipped by
402            // any scrolling ancestor, and a pointer over it never reaches
403            // `on_mouse_move`, which fires only while this element is the one
404            // under the pointer.
405            .child(
406                div()
407                    .w_full()
408                    .pl(gpui::px(layout.text_inset))
409                    .child(match self.mode {
410                        // The source is one text, so it paints as one text —
411                        // the same caret, the same clicks, no block chrome to
412                        // suppress a piece at a time.
413                        Mode::Source => markdown::render_source(
414                            self.source_text(),
415                            markdown::Editing {
416                                selection,
417                                caret_on: self.caret_on,
418                                layouts: Some(&self.layouts),
419                                annotations: &self.annotations(),
420                                keep: if self.reveal {
421                                    std::slice::from_ref(&reveal_line)
422                                } else {
423                                    &[]
424                                },
425                                scroll: self.scroll.as_ref(),
426                                typography: Some(cx.typography().scaled(
427                                    text_size::resolve(self.text_size, cx)
428                                        / theme::base_text_size(),
429                                )),
430                                ..Default::default()
431                            },
432                            cx,
433                        ),
434                        Mode::Blocks => markdown::render_with(
435                            &self.doc,
436                            markdown::Editing {
437                                selection,
438                                caret_on: self.caret_on,
439                                layouts: Some(&self.layouts),
440                                annotations: &self.annotations(),
441                                placeholder: focused.then(|| PLACEHOLDER.into()),
442                                // A caret goes into the caption here, so it is always
443                                // painted — an editor that could hide it would be
444                                // hiding a place you can already be typing.
445                                caption: markdown::Caption::Shown,
446                                // The size is absolute, so the factor the ladder
447                                // is already scaled by comes back out of it —
448                                // otherwise the app's size and this one multiply.
449                                typography: Some(cx.typography().scaled(
450                                    text_size::resolve(self.text_size, cx)
451                                        / theme::base_text_size(),
452                                )),
453                                // The editor's own press hit-tests
454                                // `checkbox_bounds`, which is what keeps a
455                                // toggle in the undo history.
456                                toggle: Some(markdown::Toggle::HitTested),
457                                table_controls: true,
458                                image_overlay: self.image_overlay.clone(),
459                                image_overlay_corner: self.image_overlay_corner,
460                                sizing: self.held_height(),
461                                fence: Some(markdown::FenceHost {
462                                    rewrite: {
463                                        let editor = cx.entity().downgrade();
464                                        Rc::new(move |ix, code, _, cx| {
465                                            editor
466                                                .update(cx, |this, cx| this.set_code(ix, code, cx))
467                                                .ok();
468                                        })
469                                    },
470                                    resize: Some({
471                                        let editor = cx.entity().downgrade();
472                                        Rc::new(move |ix, y, _, cx| {
473                                            editor
474                                                .update(cx, |this, cx| this.start_height(ix, y, cx))
475                                                .ok();
476                                        })
477                                    }),
478                                    leave: {
479                                        let editor = cx.entity().downgrade();
480                                        Rc::new(move |ix, window, cx| {
481                                            editor
482                                                .update(cx, |this, cx| {
483                                                    this.leave_block(ix, window, cx)
484                                                })
485                                                .ok();
486                                        })
487                                    },
488                                }),
489                                base: self.base.as_deref(),
490                                // A reveal owed to a caret nobody is focused on
491                                // still needs its block built to find it.
492                                keep: if self.reveal {
493                                    std::slice::from_ref(&self.selection.head.block)
494                                } else {
495                                    &[]
496                                },
497                                scroll: self.scroll.as_ref(),
498                                ..Default::default()
499                            },
500                            window,
501                            cx,
502                        ),
503                    }),
504            )
505            // Last, so the layouts it reads are this frame's rather than the
506            // one before — children paint in order.
507            .child(
508                canvas(|_, _, _| (), {
509                    let entity = cx.entity();
510                    move |_, _, window, cx| {
511                        entity.update(cx, |this, cx| {
512                            this.reveal_caret(window);
513                            this.settle_handle(window, cx);
514                        });
515                    }
516                })
517                .absolute()
518                .size(gpui::px(0.0)),
519            )
520            .children(self.slash_menu(&theme, window, cx))
521            .children(self.mention_menu(&theme, window, cx))
522            .children(self.paste_menu(&theme, window, cx))
523            .children(self.url_prompt(&theme, cx))
524            .children(self.image_target(cx))
525            .children(self.resize_preview())
526            .children(self.handle(focused, &theme, cx))
527            .children(self.resize_handle(&theme, cx))
528            .children(self.drop_indicator(&theme))
529            .children(self.table_strips(&theme, cx))
530            .children(self.table_handles(&theme, cx))
531            .children(self.table_drop_indicator(&theme))
532            .children(self.dropdown(menu::Dropdown::Table, &theme, window, cx))
533            .children(self.language_chip(&theme, cx))
534            .children(self.dropdown(menu::Dropdown::Block, &theme, window, cx))
535            .children(self.dropdown(menu::Dropdown::Language, &theme, window, cx))
536            .children(self.dropdown(menu::Dropdown::Text, &theme, window, cx))
537            .children(self.dropdown(menu::Dropdown::Image, &theme, window, cx))
538    }
539}