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