Skip to main content

guise/editor/
editor.rs

1//! `Editor` — a multiline code editor (gpui entity).
2//!
3//! Renders an [`EditorModel`] with a line-number gutter, syntax highlighting
4//! (a built-in [`Language`] tokenizer, or a whole-document
5//! [`DocumentHighlighter`] such as the `treesitter` feature's adapter),
6//! selection, caret, and the full macOS-convention keyboard map. Emits
7//! [`EditorEvent::Change`] on every edit and [`EditorEvent::Run`] on
8//! Cmd+Enter, so a host can execute the buffer (a query console, a REPL).
9//!
10//! ```ignore
11//! let editor = cx.new(|cx| {
12//!     Editor::new(cx)
13//!         .language(Language::Rust)
14//!         .rows(8)
15//!         .value("fn main() {\n    println!(\"hi\");\n}")
16//! });
17//! cx.subscribe(&editor, |_this, _editor, event: &EditorEvent, _cx| {
18//!     if let EditorEvent::Run(source) = event {
19//!         // Cmd+Enter — run `source`
20//!     }
21//! })
22//! .detach();
23//! ```
24
25use std::ops::Range;
26
27use gpui::prelude::*;
28use gpui::{
29    canvas, div, point, px, App, Bounds, ClipboardItem, Context, Div, DragMoveEvent, Empty, Entity,
30    EntityId, EventEmitter, FocusHandle, Font, Hsla, IntoElement, KeyDownEvent, MouseButton,
31    MouseDownEvent, Pixels, Point, ScrollHandle, ShapedLine, SharedString, StyledText, TextRun,
32    Window,
33};
34
35use super::cache::HighlightCache;
36use super::diagnostic::{line_message, line_severity, Diagnostic};
37use super::highlight::{token_color, DocumentHighlighter, Language, TokenKind};
38use super::model::{EditorModel, Pos};
39use crate::reactive::Signal;
40use crate::theme::theme;
41
42use crate::devtools::Probed;
43/// The monospace family used for the buffer, gutter, and placeholder.
44use crate::style::MONO_FAMILY;
45/// Horizontal padding around the text content, in px.
46const PAD_X: f32 = 10.0;
47/// Vertical padding above and below the lines, in px.
48const PAD_Y: f32 = 8.0;
49/// Padding inside the gutter on each side of the line numbers, in px.
50const GUTTER_PAD: f32 = 10.0;
51
52/// Emitted as the user edits or asks to run the buffer.
53#[derive(Debug, Clone)]
54pub enum EditorEvent {
55    /// The document changed. Carries the full new text.
56    Change(String),
57    /// Cmd+Enter. Carries the current text, for hosts that execute it.
58    Run(String),
59}
60
61/// The drag payload for selection-by-mouse; tagged with the owning entity so
62/// two editors in one window never react to each other's drags.
63struct EditorDrag(EntityId);
64
65/// Per-editor visual overrides. Unset fields fall back to the theme-derived
66/// defaults, so an empty style changes nothing.
67#[derive(Clone, Copy, Default)]
68pub struct EditorStyle {
69    /// Paint no frame border and no corner radius (an embedded strip).
70    pub bare: bool,
71    pub bg: Option<Hsla>,
72    pub text: Option<Hsla>,
73    pub caret: Option<Hsla>,
74    pub selection: Option<Hsla>,
75    pub active_line: Option<Hsla>,
76    pub gutter_fg: Option<Hsla>,
77    pub gutter_fg_active: Option<Hsla>,
78    pub placeholder: Option<Hsla>,
79}
80
81/// A multiline code editor. Create with `cx.new(|cx| Editor::new(cx))`.
82///
83/// The text model is the unit-tested [`EditorModel`] (char-index cursor,
84/// anchor selection, coalesced undo). Read-only editors still support
85/// selection and copy — only mutations are blocked.
86pub struct Editor {
87    model: EditorModel,
88    language: Language,
89    /// Whole-document highlighter; overrides `language` while set.
90    doc_highlighter: Option<Box<dyn DocumentHighlighter>>,
91    /// Whether `doc_highlighter` must reparse before the next paint.
92    doc_dirty: bool,
93    /// Per-line tokens for the `language` path, revalidated each frame and
94    /// re-tokenized only for lines that changed.
95    hl_cache: HighlightCache,
96    placeholder: SharedString,
97    read_only: bool,
98    line_numbers: bool,
99    font_size: f32,
100    rows: Option<usize>,
101    token_palette: Option<[Hsla; 8]>,
102    style: EditorStyle,
103    highlights: Vec<(Pos, Pos, Hsla)>,
104    diagnostics: Vec<Diagnostic>,
105    focus: FocusHandle,
106    scroll: ScrollHandle,
107    hscroll: ScrollHandle,
108    /// Window-space bounds of the text content, captured at prepaint. Mouse
109    /// positions minus this origin give content coordinates directly (the
110    /// captured origin already moves with both scroll axes).
111    text_bounds: Bounds<Pixels>,
112    /// Measured monospace cell advance ('0'), refreshed every render. Only a
113    /// rough unit (gutter width, scroll margins) — never per-glyph math.
114    cell_w: f32,
115    /// Line height in px, refreshed every render.
116    line_h: f32,
117    /// The resolved mono font, refreshed every render so shaping for mouse
118    /// math uses exactly the font the glyphs are painted with.
119    mono_font: Font,
120}
121
122impl EventEmitter<EditorEvent> for Editor {}
123
124impl Editor {
125    pub fn new(cx: &mut Context<Self>) -> Self {
126        Editor {
127            model: EditorModel::new(""),
128            language: Language::None,
129            doc_highlighter: None,
130            doc_dirty: true,
131            hl_cache: HighlightCache::new(),
132            placeholder: SharedString::default(),
133            read_only: false,
134            line_numbers: true,
135            font_size: 13.0,
136            rows: None,
137            token_palette: None,
138            style: EditorStyle::default(),
139            highlights: Vec::new(),
140            diagnostics: Vec::new(),
141            focus: cx.focus_handle(),
142            scroll: ScrollHandle::new(),
143            hscroll: ScrollHandle::new(),
144            text_bounds: Bounds::default(),
145            cell_w: 13.0 * 0.6,
146            line_h: 20.0,
147            mono_font: gpui::font(MONO_FAMILY),
148        }
149    }
150
151    // ---- builders ----
152
153    /// Initial text (named like [`TextInput::value`](crate::input::TextInput::value);
154    /// `text()` is the getter).
155    pub fn value(mut self, text: &str) -> Self {
156        self.model.set_text(text);
157        self
158    }
159
160    /// Syntax highlighting language (default [`Language::None`]).
161    pub fn language(mut self, language: Language) -> Self {
162        self.language = language;
163        self.hl_cache.clear();
164        self
165    }
166
167    /// Highlight with a whole-document backend (a tree-sitter adapter)
168    /// instead of the line-based `language` tokenizer. Takes precedence over
169    /// [`language`](Self::language) while set.
170    pub fn highlighter(mut self, highlighter: impl DocumentHighlighter + 'static) -> Self {
171        self.doc_highlighter = Some(Box::new(highlighter));
172        self.doc_dirty = true;
173        self
174    }
175
176    /// Dimmed hint shown while the buffer is empty and unfocused.
177    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
178        self.placeholder = placeholder.into();
179        self
180    }
181
182    /// Block edits. Selection, copy, and Cmd+Enter still work.
183    pub fn read_only(mut self, read_only: bool) -> Self {
184        self.read_only = read_only;
185        self
186    }
187
188    /// Show the line-number gutter (default true).
189    pub fn line_numbers(mut self, show: bool) -> Self {
190        self.line_numbers = show;
191        self
192    }
193
194    /// Spaces per tab stop (default 4).
195    pub fn tab_size(mut self, n: usize) -> Self {
196        self.model.set_tab_size(n);
197        self
198    }
199
200    /// Buffer font size in px (default 13.0).
201    pub fn font_size(mut self, size: f32) -> Self {
202        self.font_size = size;
203        self
204    }
205
206    /// Minimum height, as a number of visible lines.
207    pub fn rows(mut self, rows: usize) -> Self {
208        self.rows = Some(rows);
209        self
210    }
211
212    /// Override the syntax palette — one color per [`TokenKind`], in
213    /// [`TokenKind::ALL`] order. Defaults to the theme mapping
214    /// ([`token_color`]).
215    pub fn token_colors(mut self, colors: [Hsla; 8]) -> Self {
216        self.token_palette = Some(colors);
217        self
218    }
219
220    /// Per-editor visual overrides (see [`EditorStyle`]).
221    pub fn style(mut self, style: EditorStyle) -> Self {
222        self.style = style;
223        self
224    }
225
226    /// Replace the style at runtime (theme switches).
227    pub fn set_style(&mut self, style: EditorStyle, cx: &mut Context<Self>) {
228        self.style = style;
229        cx.notify();
230    }
231
232    /// Switch the highlighting language at runtime (a file-type change).
233    /// Ignored while a document highlighter is set.
234    pub fn set_language(&mut self, language: Language, cx: &mut Context<Self>) {
235        self.language = language;
236        self.hl_cache.clear();
237        cx.notify();
238    }
239
240    /// Install or replace the document highlighter at runtime; `None` falls
241    /// back to the line-based `language` tokenizer.
242    pub fn set_highlighter(
243        &mut self,
244        highlighter: Option<Box<dyn DocumentHighlighter>>,
245        cx: &mut Context<Self>,
246    ) {
247        self.doc_highlighter = highlighter;
248        self.doc_dirty = true;
249        cx.notify();
250    }
251
252    /// Background rectangles painted under the text — search matches,
253    /// occurrence highlights. Document-position ranges; multi-line ranges
254    /// paint like selections.
255    pub fn set_highlights(&mut self, highlights: Vec<(Pos, Pos, Hsla)>, cx: &mut Context<Self>) {
256        self.highlights = highlights;
257        cx.notify();
258    }
259
260    /// Attach diagnostics (compiler/linter/LSP output). Affected lines get a
261    /// severity-colored gutter dot and range underline, and the active
262    /// line's message shows in a strip under the buffer.
263    pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>, cx: &mut Context<Self>) {
264        self.diagnostics = diagnostics;
265        cx.notify();
266    }
267
268    pub fn clear_diagnostics(&mut self, cx: &mut Context<Self>) {
269        if !self.diagnostics.is_empty() {
270            self.diagnostics.clear();
271            cx.notify();
272        }
273    }
274
275    pub fn diagnostics(&self) -> &[Diagnostic] {
276        &self.diagnostics
277    }
278
279    // ---- runtime API ----
280
281    /// The current document text.
282    pub fn text(&self) -> String {
283        self.model.text()
284    }
285
286    /// Replace the document, resetting cursor, selection, and history.
287    pub fn set_text(&mut self, value: &str, cx: &mut Context<Self>) {
288        self.model.set_text(value);
289        self.doc_dirty = true;
290        cx.notify();
291    }
292
293    /// The editor's focus handle, so a host can focus it on open.
294    pub fn focus_handle(&self) -> FocusHandle {
295        self.focus.clone()
296    }
297
298    /// Read access to the underlying [`EditorModel`] — cursor, selection,
299    /// lines — for hosts that build features over the buffer (completion,
300    /// modal keymaps, search).
301    pub fn model(&self) -> &EditorModel {
302        &self.model
303    }
304
305    /// Mutate the [`EditorModel`] directly. Emits [`EditorEvent::Change`]
306    /// when the text changed, keeps the caret visible, and repaints — the
307    /// same bookkeeping every built-in edit goes through.
308    pub fn edit<R>(
309        &mut self,
310        window: &mut Window,
311        cx: &mut Context<Self>,
312        f: impl FnOnce(&mut EditorModel) -> R,
313    ) -> R {
314        let before = self.model.text();
315        let result = f(&mut self.model);
316        let after = self.model.text();
317        if after != before {
318            self.doc_dirty = true;
319            cx.emit(EditorEvent::Change(after));
320        }
321        self.ensure_cursor_visible(window);
322        cx.notify();
323        result
324    }
325
326    /// Window-space origin of the caret's cell — where a completion popup or
327    /// search bar anchors. Tracks both scroll axes; meaningless before the
328    /// first paint (returns the content origin).
329    pub fn caret_origin(&self, window: &Window) -> Point<Pixels> {
330        let cursor = self.model.cursor();
331        let x = match self.model.line(cursor.line) {
332            Some(line) => self.caret_x(line, cursor.col, window),
333            None => 0.0,
334        };
335        point(
336            self.text_bounds.origin.x + px(x),
337            self.text_bounds.origin.y + px(cursor.line as f32 * self.line_h),
338        )
339    }
340
341    /// The pixel height of one buffer line, as painted last frame.
342    pub fn line_height(&self) -> f32 {
343        self.line_h
344    }
345
346    /// Two-way bind this editor's text to a `Signal<String>`. The signal is
347    /// the source of truth: the editor adopts its value now, edits write back
348    /// through [`Signal::set_if_changed`], and signal writes replace the text.
349    /// Equality guards on both directions prevent update loops.
350    pub fn bind(entity: &Entity<Editor>, signal: &Signal<String>, cx: &mut App) {
351        let initial = signal.get(cx);
352        entity.update(cx, |this, cx| {
353            if this.text() != initial {
354                this.set_text(&initial, cx);
355            }
356        });
357        let sink = signal.clone();
358        cx.subscribe(entity, move |_editor, event: &EditorEvent, cx| {
359            if let EditorEvent::Change(text) = event {
360                sink.set_if_changed(cx, text.clone());
361            }
362        })
363        .detach();
364        // Weak handle: a strong clone would form a retain cycle with the
365        // subscription above and leak both the editor and the signal.
366        let editor = entity.downgrade();
367        cx.observe(signal.entity(), move |observed, cx| {
368            let value = observed.read(cx).clone();
369            editor
370                .update(cx, |this, cx| {
371                    if this.text() != value {
372                        this.set_text(&value, cx);
373                    }
374                })
375                .ok();
376        })
377        .detach();
378    }
379
380    // ---- input handling ----
381
382    fn on_key(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
383        let ks = &event.keystroke;
384        let m = ks.modifiers;
385        let shift = m.shift;
386        match ks.key.as_str() {
387            "left" => {
388                if m.platform {
389                    self.model.home(shift);
390                } else if m.alt {
391                    self.model.word_left(shift);
392                } else {
393                    self.model.move_left(shift);
394                }
395                self.after_move(window, cx);
396            }
397            "right" => {
398                if m.platform {
399                    self.model.end(shift);
400                } else if m.alt {
401                    self.model.word_right(shift);
402                } else {
403                    self.model.move_right(shift);
404                }
405                self.after_move(window, cx);
406            }
407            "up" => {
408                if m.platform {
409                    self.model.doc_start(shift);
410                } else {
411                    self.model.move_up(shift);
412                }
413                self.after_move(window, cx);
414            }
415            "down" => {
416                if m.platform {
417                    self.model.doc_end(shift);
418                } else {
419                    self.model.move_down(shift);
420                }
421                self.after_move(window, cx);
422            }
423            "home" => {
424                if m.platform {
425                    self.model.doc_start(shift);
426                } else {
427                    self.model.home(shift);
428                }
429                self.after_move(window, cx);
430            }
431            "end" => {
432                if m.platform {
433                    self.model.doc_end(shift);
434                } else {
435                    self.model.end(shift);
436                }
437                self.after_move(window, cx);
438            }
439            "backspace" => {
440                if self.read_only {
441                    return;
442                }
443                let changed = if self.model.selection().is_some() {
444                    self.model.delete_selection()
445                } else if m.platform {
446                    self.model.home(true);
447                    self.model.delete_selection()
448                } else if m.alt {
449                    self.model.word_left(true);
450                    self.model.delete_selection()
451                } else {
452                    self.model.backspace()
453                };
454                if changed {
455                    self.after_edit(window, cx);
456                } else {
457                    cx.stop_propagation();
458                }
459            }
460            "delete" => {
461                if self.read_only {
462                    return;
463                }
464                let changed = if self.model.selection().is_some() {
465                    self.model.delete_selection()
466                } else if m.platform {
467                    self.model.end(true);
468                    self.model.delete_selection()
469                } else if m.alt {
470                    self.model.word_right(true);
471                    self.model.delete_selection()
472                } else {
473                    self.model.delete()
474                };
475                if changed {
476                    self.after_edit(window, cx);
477                } else {
478                    cx.stop_propagation();
479                }
480            }
481            "enter" if m.platform => {
482                cx.emit(EditorEvent::Run(self.model.text()));
483                cx.stop_propagation();
484            }
485            "enter" => {
486                if self.read_only {
487                    return;
488                }
489                self.model.newline();
490                self.after_edit(window, cx);
491            }
492            "tab" => {
493                // Cmd+Tab (and read-only Tab) bubbles so hosts keep focus moves.
494                if m.platform || self.read_only {
495                    return;
496                }
497                self.model.tab();
498                self.after_edit(window, cx);
499            }
500            // Escape bubbles (dialogs close on it) but still drops the selection.
501            "escape" => {
502                if self.model.selection().is_some() {
503                    self.model.clear_selection();
504                    cx.notify();
505                }
506            }
507            "a" if m.platform => {
508                self.model.select_all();
509                cx.notify();
510                cx.stop_propagation();
511            }
512            "c" if m.platform => {
513                if let Some(text) = self.model.copy() {
514                    cx.write_to_clipboard(ClipboardItem::new_string(text));
515                }
516                cx.stop_propagation();
517            }
518            "x" if m.platform => {
519                if self.read_only {
520                    // Selection stays; degrade cut to copy.
521                    if let Some(text) = self.model.copy() {
522                        cx.write_to_clipboard(ClipboardItem::new_string(text));
523                    }
524                } else if let Some(text) = self.model.cut() {
525                    cx.write_to_clipboard(ClipboardItem::new_string(text));
526                    self.after_edit(window, cx);
527                    return;
528                }
529                cx.stop_propagation();
530            }
531            "v" if m.platform => {
532                if !self.read_only {
533                    if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
534                        if !text.is_empty() {
535                            self.model.insert(&text);
536                            self.after_edit(window, cx);
537                            return;
538                        }
539                    }
540                }
541                cx.stop_propagation();
542            }
543            "z" if m.platform => {
544                if !self.read_only {
545                    let changed = if m.shift {
546                        self.model.redo()
547                    } else {
548                        self.model.undo()
549                    };
550                    if changed {
551                        self.after_edit(window, cx);
552                        return;
553                    }
554                }
555                cx.stop_propagation();
556            }
557            _ => {
558                // Printable input: never on Cmd/Ctrl chords; Option+key is
559                // allowed so composed glyphs land (matches `input::apply_key`).
560                if !self.read_only && !m.platform && !m.control {
561                    if let Some(text) = ks.key_char.as_deref().filter(|t| !t.is_empty()) {
562                        self.model.insert(text);
563                        self.after_edit(window, cx);
564                    }
565                }
566                // Everything else bubbles to the host.
567            }
568        }
569    }
570
571    fn on_mouse_down(&mut self, ev: &MouseDownEvent, window: &mut Window, cx: &mut Context<Self>) {
572        window.focus(&self.focus);
573        let (line, col) = self.hit(ev.position, window);
574        match ev.click_count {
575            2 => {
576                self.model.move_to(line, col, false);
577                self.model.select_word();
578            }
579            n if n > 2 => {
580                self.model.move_to(line, col, false);
581                self.model.select_line();
582            }
583            _ => self.model.move_to(line, col, ev.modifiers.shift),
584        }
585        cx.notify();
586    }
587
588    fn on_drag_move(
589        &mut self,
590        ev: &DragMoveEvent<EditorDrag>,
591        window: &mut Window,
592        cx: &mut Context<Self>,
593    ) {
594        if ev.drag(cx).0 != cx.entity_id() {
595            return;
596        }
597        let (line, col) = self.hit(ev.event.position, window);
598        self.model.move_to(line, col, true);
599        self.ensure_cursor_visible(window);
600        cx.notify();
601    }
602
603    /// Shape one line with the editor's mono font. The resulting layout maps
604    /// char boundaries to painted x positions (and back), so mouse math, the
605    /// caret, and selection agree with the glyphs `StyledText` actually paints
606    /// — including double-width CJK/emoji fallback glyphs and literal tabs.
607    fn shape(&self, line: &str, window: &Window) -> ShapedLine {
608        let text = SharedString::from(line.to_string());
609        let run = TextRun {
610            len: text.len(),
611            font: self.mono_font.clone(),
612            color: Hsla::default(),
613            background_color: None,
614            underline: None,
615            strikethrough: None,
616        };
617        window
618            .text_system()
619            .shape_line(text, px(self.font_size), &[run], None)
620    }
621
622    /// Window position -> (line, col): the line from the fixed row height,
623    /// the column from the shaped line's closest char boundary. The model
624    /// clamps out-of-range values.
625    fn hit(&self, position: Point<Pixels>, window: &Window) -> (usize, usize) {
626        let x = f32::from(position.x) - f32::from(self.text_bounds.origin.x);
627        let y = f32::from(position.y) - f32::from(self.text_bounds.origin.y);
628        let line = hit_line(y, self.line_h).min(self.model.line_count().saturating_sub(1));
629        let Some(text) = self.model.line(line) else {
630            return (line, 0);
631        };
632        let byte = self.shape(text, window).closest_index_for_x(px(x.max(0.0)));
633        (line, col_for_byte(text, byte))
634    }
635
636    /// Painted x of the caret at char column `col` on `line`.
637    fn caret_x(&self, line: &str, col: usize, window: &Window) -> f32 {
638        f32::from(
639            self.shape(line, window)
640                .x_for_index(byte_for_col(line, col)),
641        )
642    }
643
644    fn after_edit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
645        self.doc_dirty = true;
646        cx.emit(EditorEvent::Change(self.model.text()));
647        self.ensure_cursor_visible(window);
648        cx.notify();
649        cx.stop_propagation();
650    }
651
652    fn after_move(&mut self, window: &mut Window, cx: &mut Context<Self>) {
653        self.ensure_cursor_visible(window);
654        cx.notify();
655        cx.stop_propagation();
656    }
657
658    /// Nudge both scroll axes so the caret (plus a padding margin) is inside
659    /// the viewport. No-op before the first paint.
660    fn ensure_cursor_visible(&mut self, window: &Window) {
661        let cursor = self.model.cursor();
662        let view_h = f32::from(self.scroll.bounds().size.height);
663        if view_h > 0.0 {
664            let top = cursor.line as f32 * self.line_h;
665            let bottom = top + self.line_h + 2.0 * PAD_Y;
666            let offset = self.scroll.offset();
667            let y = scroll_adjust(f32::from(offset.y), view_h, top, bottom);
668            if y != f32::from(offset.y) {
669                self.scroll.set_offset(point(offset.x, px(y)));
670            }
671        }
672        let view_w = f32::from(self.hscroll.bounds().size.width);
673        if view_w > 0.0 {
674            let left = match self.model.line(cursor.line) {
675                Some(line) => self.caret_x(line, cursor.col, window),
676                None => cursor.col as f32 * self.cell_w,
677            };
678            let right = left + self.cell_w + 2.0 * PAD_X;
679            let offset = self.hscroll.offset();
680            let x = scroll_adjust(f32::from(offset.x), view_w, left, right);
681            if x != f32::from(offset.x) {
682                self.hscroll.set_offset(point(px(x), offset.y));
683            }
684        }
685    }
686}
687
688impl Render for Editor {
689    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
690        let focused = self.focus.is_focused(window);
691
692        let t = theme(cx);
693        let style = self.style;
694        let frame_border = if focused {
695            t.primary().hsla()
696        } else {
697            t.border().hsla()
698        };
699        let edge = t.border().hsla();
700        let bg = style.bg.unwrap_or_else(|| t.surface().hsla());
701        let text_color = style.text.unwrap_or_else(|| t.text().hsla());
702        let dimmed = style.placeholder.unwrap_or_else(|| t.dimmed().hsla());
703        let caret_color = style.caret.unwrap_or_else(|| t.primary().hsla());
704        let selection_bg = style.selection.unwrap_or_else(|| t.primary().alpha(0.25));
705        let active_bg = style
706            .active_line
707            .unwrap_or_else(|| t.surface_hover().alpha(0.55));
708        let gutter_fg = style.gutter_fg.unwrap_or_else(|| t.dimmed().alpha(0.7));
709        let gutter_fg_active = style.gutter_fg_active.unwrap_or(text_color);
710        let radius = t.radius(t.default_radius);
711        let token_colors: [Hsla; 8] = self
712            .token_palette
713            .unwrap_or_else(|| TokenKind::ALL.map(|kind| token_color(kind, t)));
714
715        // Resolve the mono font once per render and keep it on self: mouse
716        // math shapes lines with the same font the glyphs are painted with.
717        let font_size = self.font_size;
718        let line_h = (font_size * 1.5).round();
719        let font = Font {
720            family: MONO_FAMILY.into(),
721            ..window.text_style().font()
722        };
723        let cell_w = {
724            let ts = window.text_system();
725            let font_id = ts.resolve_font(&font);
726            ts.ch_advance(font_id, px(font_size))
727                .map(f32::from)
728                .unwrap_or(font_size * 0.6)
729        };
730        self.line_h = line_h;
731        self.cell_w = cell_w;
732        self.mono_font = font.clone();
733
734        let cursor = self.model.cursor();
735        let selection = self.model.selection();
736        let line_count = self.model.line_count();
737        let show_placeholder = self.model.is_empty() && !focused && !self.placeholder.is_empty();
738        let show_caret = focused && !self.read_only;
739
740        let digits = line_count.to_string().len().max(2);
741        let gutter_w = digits as f32 * cell_w + 2.0 * GUTTER_PAD;
742        let mut max_line_w: f32 = 0.0;
743
744        // Re-highlight only what changed, never per frame: the document
745        // highlighter reparses on its dirty flag; the line cache revalidates
746        // against the lines and re-tokenizes only the ones that moved.
747        match &mut self.doc_highlighter {
748            Some(doc) => {
749                if self.doc_dirty {
750                    let text = self.model.text();
751                    doc.update(&text);
752                    self.doc_dirty = false;
753                }
754            }
755            None => {
756                self.hl_cache.sync(&self.language, self.model.lines());
757            }
758        }
759
760        let mut gutter_rows: Vec<Div> = Vec::with_capacity(line_count);
761        let mut text_rows: Vec<Div> = Vec::with_capacity(line_count);
762        for (i, line) in self.model.lines().iter().enumerate() {
763            let is_active = i == cursor.line;
764
765            if self.line_numbers {
766                let mut gutter_row = div()
767                    .relative()
768                    .h(px(line_h))
769                    .flex()
770                    .items_center()
771                    .justify_end()
772                    .text_color(if is_active && focused {
773                        gutter_fg_active
774                    } else {
775                        gutter_fg
776                    })
777                    .child(SharedString::from((i + 1).to_string()));
778                if let Some(severity) = line_severity(&self.diagnostics, i) {
779                    gutter_row = gutter_row.child(
780                        div()
781                            .absolute()
782                            .left(px(1.0))
783                            .top(px((line_h - 6.0) / 2.0))
784                            .w(px(6.0))
785                            .h(px(6.0))
786                            .rounded_full()
787                            .bg(severity.color(t)),
788                    );
789                }
790                gutter_rows.push(gutter_row);
791            }
792
793            // Shaped once per line (cached across frames by gpui): painted
794            // width, selection rects, and the caret all read real glyph
795            // positions instead of assuming one uniform cell per char.
796            let shaped = self.shape(line, window);
797            max_line_w = max_line_w.max(f32::from(shaped.width));
798
799            let mut row = div().relative().h(px(line_h)).w_full();
800            if focused && is_active {
801                row = row.bg(active_bg);
802            }
803            for (start, end, color) in &self.highlights {
804                if let Some((s, e, newline)) = line_selection(*start, *end, i, line.chars().count())
805                {
806                    let sx = f32::from(shaped.x_for_index(byte_for_col(line, s)));
807                    let ex = f32::from(shaped.x_for_index(byte_for_col(line, e)));
808                    let w = (ex - sx) + if newline { cell_w } else { 0.0 };
809                    row = row.child(
810                        div()
811                            .absolute()
812                            .top_0()
813                            .bottom_0()
814                            .left(px(sx))
815                            .w(px(w))
816                            .bg(*color),
817                    );
818                }
819            }
820            if let Some((start, end)) = selection {
821                if let Some((s, e, newline)) = line_selection(start, end, i, line.chars().count()) {
822                    let sx = f32::from(shaped.x_for_index(byte_for_col(line, s)));
823                    let ex = f32::from(shaped.x_for_index(byte_for_col(line, e)));
824                    let w = (ex - sx) + if newline { cell_w } else { 0.0 };
825                    row = row.child(
826                        div()
827                            .absolute()
828                            .top_0()
829                            .bottom_0()
830                            .left(px(sx))
831                            .w(px(w))
832                            .bg(selection_bg),
833                    );
834                }
835            }
836            // Diagnostic underlines: a 2px severity-colored bar under the
837            // char range (empty range = the whole line).
838            for diag in self.diagnostics.iter().filter(|d| d.line == i) {
839                let chars = line.chars().count();
840                let (s, e) = if diag.cols.is_empty() || diag.cols.start >= chars {
841                    (0, chars)
842                } else {
843                    (diag.cols.start, diag.cols.end.min(chars))
844                };
845                let sx = f32::from(shaped.x_for_index(byte_for_col(line, s)));
846                let ex = f32::from(shaped.x_for_index(byte_for_col(line, e)));
847                row = row.child(
848                    div()
849                        .absolute()
850                        .bottom(px(1.0))
851                        .left(px(sx))
852                        .w(px((ex - sx).max(cell_w * 0.75)))
853                        .h(px(2.0))
854                        .rounded(px(1.0))
855                        .bg(diag.severity.color(t)),
856                );
857            }
858            let tokens = match &self.doc_highlighter {
859                Some(doc) => doc.tokens(i),
860                None => self.hl_cache.tokens(i),
861            };
862            if !line.is_empty() {
863                let runs: Vec<TextRun> = spans(line.len(), tokens)
864                    .into_iter()
865                    .map(|(len, kind)| TextRun {
866                        len,
867                        font: font.clone(),
868                        color: kind.map_or(text_color, |k| token_colors[k.index()]),
869                        background_color: None,
870                        underline: None,
871                        strikethrough: None,
872                    })
873                    .collect();
874                row = row.child(StyledText::new(SharedString::from(line.clone())).with_runs(runs));
875            } else if i == 0 && show_placeholder {
876                row = row.child(div().text_color(dimmed).child(self.placeholder.clone()));
877            }
878            if show_caret && is_active {
879                let x = f32::from(shaped.x_for_index(byte_for_col(line, cursor.col)));
880                row = row.child(
881                    div()
882                        .absolute()
883                        .top_0()
884                        .h(px(line_h))
885                        .left(px((x - 1.0).max(0.0)))
886                        .w(px(2.0))
887                        .bg(caret_color),
888                );
889            }
890            text_rows.push(row);
891        }
892        let content_w = max_line_w + cell_w;
893
894        // Invisible bounds probe: its painted origin (which moves with both
895        // scroll axes) is exactly where content cell (0, 0) is, so mouse
896        // hit-testing is a subtraction.
897        let entity = cx.entity();
898        let probe = canvas(
899            move |bounds, _window, cx| {
900                entity.update(cx, |this, _| this.text_bounds = bounds);
901            },
902            |_, _, _, _| {},
903        )
904        .absolute()
905        .size_full();
906
907        let lines_col = div()
908            .relative()
909            .flex()
910            .flex_col()
911            .flex_grow()
912            .min_w(px(content_w))
913            .whitespace_nowrap()
914            .child(probe)
915            .children(text_rows);
916
917        let text_area = div()
918            .id("guise-editor-text")
919            .flex_1()
920            .overflow_x_scroll()
921            .track_scroll(&self.hscroll)
922            .px(px(PAD_X))
923            .child(lines_col);
924
925        let mut content_row = div().flex().items_start().w_full().py(px(PAD_Y));
926        if self.line_numbers {
927            content_row = content_row.child(
928                div()
929                    .flex()
930                    .flex_col()
931                    .flex_none()
932                    .w(px(gutter_w))
933                    .pr(px(GUTTER_PAD))
934                    .border_r_1()
935                    .border_color(edge)
936                    .children(gutter_rows),
937            );
938        }
939        content_row = content_row.child(text_area);
940
941        let mut body = div()
942            .id("guise-editor-body")
943            .track_focus(&self.focus)
944            .on_key_down(cx.listener(Self::on_key))
945            .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
946            .on_drag(EditorDrag(cx.entity_id()), |_, _, _, cx| cx.new(|_| Empty))
947            .on_drag_move(cx.listener(Self::on_drag_move))
948            .overflow_y_scroll()
949            .track_scroll(&self.scroll)
950            .w_full()
951            .max_h_full()
952            .cursor_text()
953            .child(content_row);
954        if let Some(rows) = self.rows {
955            body = body.min_h(px(rows as f32 * line_h + 2.0 * PAD_Y));
956        }
957
958        let mut frame = div().flex().flex_col().w_full().h_full();
959        if !style.bare {
960            frame = frame
961                .rounded(px(radius))
962                .border_1()
963                .border_color(frame_border);
964        }
965        frame = frame
966            .bg(bg)
967            .overflow_hidden()
968            .font_family(MONO_FAMILY)
969            .text_size(px(font_size))
970            .line_height(px(line_h))
971            .text_color(text_color)
972            .child(body);
973
974        // The active line's diagnostic, in a strip under the buffer (no
975        // hover needed, no hit-test interference with text selection).
976        if let Some(diag) = line_message(&self.diagnostics, cursor.line) {
977            let accent = diag.severity.color(t);
978            frame = frame.child(
979                div()
980                    .flex()
981                    .items_center()
982                    .gap(px(6.0))
983                    .px(px(PAD_X))
984                    .py(px(3.0))
985                    .border_t_1()
986                    .border_color(edge)
987                    .bg(Hsla { a: 0.06, ..accent })
988                    .text_size(px(font_size - 2.0))
989                    .text_color(dimmed)
990                    .child(div().w(px(6.0)).h(px(6.0)).rounded_full().bg(accent))
991                    .child(diag.message.clone()),
992            );
993        }
994        frame.probe("Editor")
995    }
996}
997
998// ---- pure geometry helpers (unit-tested) -----------------------------------
999
1000/// Cover `len` bytes with contiguous span lengths: token ranges keep their
1001/// kind, gaps get `None`. Clamps overlapping or out-of-range tokens so the
1002/// lengths always sum to exactly `len` (a gpui `StyledText` requirement).
1003fn spans(len: usize, tokens: &[(Range<usize>, TokenKind)]) -> Vec<(usize, Option<TokenKind>)> {
1004    let mut out = Vec::new();
1005    let mut at = 0;
1006    for (range, kind) in tokens {
1007        let start = range.start.max(at).min(len);
1008        let end = range.end.max(start).min(len);
1009        if start > at {
1010            out.push((start - at, None));
1011        }
1012        if end > start {
1013            out.push((end - start, Some(*kind)));
1014        }
1015        at = end.max(at);
1016    }
1017    if at < len {
1018        out.push((len - at, None));
1019    }
1020    out
1021}
1022
1023/// The selected char-column range on `line` for a normalized selection
1024/// `(start, end)`, or `None` when the selection misses the line entirely.
1025/// The `bool` is whether the selection continues past this line's end (draw
1026/// the newline as one extra cell).
1027fn line_selection(
1028    start: Pos,
1029    end: Pos,
1030    line: usize,
1031    line_len: usize,
1032) -> Option<(usize, usize, bool)> {
1033    if line < start.line || line > end.line {
1034        return None;
1035    }
1036    let s = if line == start.line {
1037        start.col.min(line_len)
1038    } else {
1039        0
1040    };
1041    let e = if line == end.line {
1042        end.col.min(line_len)
1043    } else {
1044        line_len
1045    };
1046    let e = e.max(s);
1047    let newline = line < end.line;
1048    if e == s && !newline {
1049        return None;
1050    }
1051    Some((s, e, newline))
1052}
1053
1054/// Content-space y -> line row, clamping negatives to zero. Rows share one
1055/// fixed height, so this stays uniform math; columns go through shaping.
1056fn hit_line(y: f32, line_h: f32) -> usize {
1057    (y / line_h).floor().max(0.0) as usize
1058}
1059
1060/// Byte offset of char column `col` in `line`, clamped to the line end.
1061fn byte_for_col(line: &str, col: usize) -> usize {
1062    line.char_indices()
1063        .nth(col)
1064        .map(|(i, _)| i)
1065        .unwrap_or(line.len())
1066}
1067
1068/// Char column of byte offset `byte` in `line`. Boundary-safe: offsets inside
1069/// a multi-byte char count as the column of that char.
1070fn col_for_byte(line: &str, byte: usize) -> usize {
1071    line.char_indices().take_while(|&(i, _)| i < byte).count()
1072}
1073
1074/// Adjust a scroll offset (0 or negative, more negative = scrolled further)
1075/// so the content range `top..bottom` is inside a `view`-long viewport.
1076fn scroll_adjust(offset: f32, view: f32, top: f32, bottom: f32) -> f32 {
1077    let mut adjusted = offset;
1078    if bottom + adjusted > view {
1079        adjusted = view - bottom;
1080    }
1081    if top + adjusted < 0.0 {
1082        adjusted = -top;
1083    }
1084    adjusted
1085}
1086
1087#[cfg(test)]
1088mod tests {
1089    use super::*;
1090
1091    fn total(spans: &[(usize, Option<TokenKind>)]) -> usize {
1092        spans.iter().map(|(len, _)| len).sum()
1093    }
1094
1095    #[test]
1096    fn spans_cover_the_line_exactly() {
1097        let tokens = vec![(2..5, TokenKind::Keyword), (7..9, TokenKind::Number)];
1098        let s = spans(10, &tokens);
1099        assert_eq!(
1100            s,
1101            vec![
1102                (2, None),
1103                (3, Some(TokenKind::Keyword)),
1104                (2, None),
1105                (2, Some(TokenKind::Number)),
1106                (1, None),
1107            ]
1108        );
1109        assert_eq!(total(&s), 10);
1110    }
1111
1112    #[test]
1113    fn spans_with_no_tokens_is_one_gap() {
1114        assert_eq!(spans(4, &[]), vec![(4, None)]);
1115        assert!(spans(0, &[]).is_empty());
1116    }
1117
1118    #[test]
1119    fn spans_clamp_overlap_and_overflow() {
1120        // Overlapping ranges never double-cover bytes...
1121        let tokens = vec![(0..6, TokenKind::Keyword), (4..8, TokenKind::Number)];
1122        let s = spans(10, &tokens);
1123        assert_eq!(total(&s), 10);
1124        // ...and ranges past the end clamp to it.
1125        let tokens = vec![(8..20, TokenKind::Comment)];
1126        let s = spans(10, &tokens);
1127        assert_eq!(s, vec![(8, None), (2, Some(TokenKind::Comment))]);
1128    }
1129
1130    #[test]
1131    fn spans_token_flush_to_both_edges() {
1132        let tokens = vec![(0..10, TokenKind::Comment)];
1133        assert_eq!(spans(10, &tokens), vec![(10, Some(TokenKind::Comment))]);
1134    }
1135
1136    fn at(line: usize, col: usize) -> Pos {
1137        Pos::new(line, col)
1138    }
1139
1140    #[test]
1141    fn selection_on_a_single_line() {
1142        let sel = line_selection(at(1, 2), at(1, 5), 1, 8);
1143        assert_eq!(sel, Some((2, 5, false)));
1144        assert_eq!(line_selection(at(1, 2), at(1, 5), 0, 8), None);
1145        assert_eq!(line_selection(at(1, 2), at(1, 5), 2, 8), None);
1146    }
1147
1148    #[test]
1149    fn selection_across_lines() {
1150        // First line: from start.col to the end, plus the newline cell.
1151        assert_eq!(line_selection(at(0, 3), at(2, 2), 0, 6), Some((3, 6, true)));
1152        // Middle line: everything, plus the newline cell.
1153        assert_eq!(line_selection(at(0, 3), at(2, 2), 1, 4), Some((0, 4, true)));
1154        // Last line: from col 0 to end.col.
1155        assert_eq!(
1156            line_selection(at(0, 3), at(2, 2), 2, 6),
1157            Some((0, 2, false))
1158        );
1159    }
1160
1161    #[test]
1162    fn selection_on_an_empty_middle_line_shows_the_newline() {
1163        assert_eq!(line_selection(at(0, 0), at(2, 1), 1, 0), Some((0, 0, true)));
1164    }
1165
1166    #[test]
1167    fn selection_cols_clamp_to_line_len() {
1168        assert_eq!(line_selection(at(0, 10), at(0, 20), 0, 5), None); // both past end
1169        assert_eq!(
1170            line_selection(at(0, 2), at(0, 20), 0, 5),
1171            Some((2, 5, false))
1172        );
1173    }
1174
1175    #[test]
1176    fn hit_line_maps_and_clamps() {
1177        assert_eq!(hit_line(0.0, 20.0), 0);
1178        assert_eq!(hit_line(45.0, 20.0), 2);
1179        assert_eq!(hit_line(-10.0, 20.0), 0); // padding clicks above the text
1180    }
1181
1182    #[test]
1183    fn byte_for_col_handles_multibyte_chars() {
1184        assert_eq!(byte_for_col("abc", 0), 0);
1185        assert_eq!(byte_for_col("abc", 2), 2);
1186        assert_eq!(byte_for_col("abc", 9), 3); // clamps to the line end
1187                                               // "日本語abc": each CJK char is 3 bytes.
1188        assert_eq!(byte_for_col("日本語abc", 1), 3);
1189        assert_eq!(byte_for_col("日本語abc", 3), 9);
1190        assert_eq!(byte_for_col("日本語abc", 4), 10);
1191    }
1192
1193    #[test]
1194    fn col_for_byte_inverts_byte_for_col() {
1195        let line = "日本語abc";
1196        for col in 0..=6 {
1197            assert_eq!(col_for_byte(line, byte_for_col(line, col)), col);
1198        }
1199        assert_eq!(col_for_byte(line, 999), 6); // past the end
1200        assert_eq!(col_for_byte("", 0), 0);
1201    }
1202
1203    #[test]
1204    fn scroll_adjust_reveals_the_target() {
1205        // Already visible: unchanged.
1206        assert_eq!(scroll_adjust(-10.0, 100.0, 20.0, 40.0), -10.0);
1207        // Above the viewport: scroll up to the top edge.
1208        assert_eq!(scroll_adjust(-50.0, 100.0, 20.0, 40.0), -20.0);
1209        // Below the viewport: scroll down to the bottom edge.
1210        assert_eq!(scroll_adjust(0.0, 100.0, 150.0, 170.0), -70.0);
1211        // Taller than the viewport: the top wins.
1212        assert_eq!(scroll_adjust(0.0, 50.0, 100.0, 200.0), -100.0);
1213    }
1214}