1use 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;
43use crate::style::MONO_FAMILY;
45const PAD_X: f32 = 10.0;
47const PAD_Y: f32 = 8.0;
49const GUTTER_PAD: f32 = 10.0;
51
52#[derive(Debug, Clone)]
54pub enum EditorEvent {
55 Change(String),
57 Run(String),
59}
60
61struct EditorDrag(EntityId);
64
65#[derive(Clone, Copy, Default)]
68pub struct EditorStyle {
69 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
81pub struct Editor {
87 model: EditorModel,
88 language: Language,
89 doc_highlighter: Option<Box<dyn DocumentHighlighter>>,
91 doc_dirty: bool,
93 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 text_bounds: Bounds<Pixels>,
112 cell_w: f32,
115 line_h: f32,
117 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 pub fn value(mut self, text: &str) -> Self {
156 self.model.set_text(text);
157 self
158 }
159
160 pub fn language(mut self, language: Language) -> Self {
162 self.language = language;
163 self.hl_cache.clear();
164 self
165 }
166
167 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 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
178 self.placeholder = placeholder.into();
179 self
180 }
181
182 pub fn read_only(mut self, read_only: bool) -> Self {
184 self.read_only = read_only;
185 self
186 }
187
188 pub fn line_numbers(mut self, show: bool) -> Self {
190 self.line_numbers = show;
191 self
192 }
193
194 pub fn tab_size(mut self, n: usize) -> Self {
196 self.model.set_tab_size(n);
197 self
198 }
199
200 pub fn font_size(mut self, size: f32) -> Self {
202 self.font_size = size;
203 self
204 }
205
206 pub fn rows(mut self, rows: usize) -> Self {
208 self.rows = Some(rows);
209 self
210 }
211
212 pub fn token_colors(mut self, colors: [Hsla; 8]) -> Self {
216 self.token_palette = Some(colors);
217 self
218 }
219
220 pub fn style(mut self, style: EditorStyle) -> Self {
222 self.style = style;
223 self
224 }
225
226 pub fn set_style(&mut self, style: EditorStyle, cx: &mut Context<Self>) {
228 self.style = style;
229 cx.notify();
230 }
231
232 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 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 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 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 pub fn text(&self) -> String {
283 self.model.text()
284 }
285
286 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 pub fn focus_handle(&self) -> FocusHandle {
295 self.focus.clone()
296 }
297
298 pub fn model(&self) -> &EditorModel {
302 &self.model
303 }
304
305 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 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 pub fn line_height(&self) -> f32 {
343 self.line_h
344 }
345
346 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 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 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 if m.platform || self.read_only {
495 return;
496 }
497 self.model.tab();
498 self.after_edit(window, cx);
499 }
500 "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 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 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 }
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 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 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 fn caret_x(&self, line: &str, col: usize, window: &Window) -> f32 {
638 f32::from(
639 self
640 .shape(line, window)
641 .x_for_index(byte_for_col(line, col)),
642 )
643 }
644
645 fn after_edit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
646 self.doc_dirty = true;
647 cx.emit(EditorEvent::Change(self.model.text()));
648 self.ensure_cursor_visible(window);
649 cx.notify();
650 cx.stop_propagation();
651 }
652
653 fn after_move(&mut self, window: &mut Window, cx: &mut Context<Self>) {
654 self.ensure_cursor_visible(window);
655 cx.notify();
656 cx.stop_propagation();
657 }
658
659 fn ensure_cursor_visible(&mut self, window: &Window) {
662 let cursor = self.model.cursor();
663 let view_h = f32::from(self.scroll.bounds().size.height);
664 if view_h > 0.0 {
665 let top = cursor.line as f32 * self.line_h;
666 let bottom = top + self.line_h + 2.0 * PAD_Y;
667 let offset = self.scroll.offset();
668 let y = scroll_adjust(f32::from(offset.y), view_h, top, bottom);
669 if y != f32::from(offset.y) {
670 self.scroll.set_offset(point(offset.x, px(y)));
671 }
672 }
673 let view_w = f32::from(self.hscroll.bounds().size.width);
674 if view_w > 0.0 {
675 let left = match self.model.line(cursor.line) {
676 Some(line) => self.caret_x(line, cursor.col, window),
677 None => cursor.col as f32 * self.cell_w,
678 };
679 let right = left + self.cell_w + 2.0 * PAD_X;
680 let offset = self.hscroll.offset();
681 let x = scroll_adjust(f32::from(offset.x), view_w, left, right);
682 if x != f32::from(offset.x) {
683 self.hscroll.set_offset(point(px(x), offset.y));
684 }
685 }
686 }
687}
688
689impl Render for Editor {
690 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
691 let focused = self.focus.is_focused(window);
692
693 let t = theme(cx);
694 let style = self.style;
695 let frame_border = if focused {
696 t.primary().hsla()
697 } else {
698 t.border().hsla()
699 };
700 let edge = t.border().hsla();
701 let bg = style.bg.unwrap_or_else(|| t.surface().hsla());
702 let text_color = style.text.unwrap_or_else(|| t.text().hsla());
703 let dimmed = style.placeholder.unwrap_or_else(|| t.dimmed().hsla());
704 let caret_color = style.caret.unwrap_or_else(|| t.primary().hsla());
705 let selection_bg = style.selection.unwrap_or_else(|| t.primary().alpha(0.25));
706 let active_bg = style
707 .active_line
708 .unwrap_or_else(|| t.surface_hover().alpha(0.55));
709 let gutter_fg = style.gutter_fg.unwrap_or_else(|| t.dimmed().alpha(0.7));
710 let gutter_fg_active = style.gutter_fg_active.unwrap_or(text_color);
711 let radius = t.radius(t.default_radius);
712 let token_colors: [Hsla; 8] = self
713 .token_palette
714 .unwrap_or_else(|| TokenKind::ALL.map(|kind| token_color(kind, t)));
715
716 let font_size = self.font_size;
719 let line_h = (font_size * 1.5).round();
720 let font = Font {
721 family: MONO_FAMILY.into(),
722 ..window.text_style().font()
723 };
724 let cell_w = {
725 let ts = window.text_system();
726 let font_id = ts.resolve_font(&font);
727 ts.ch_advance(font_id, px(font_size))
728 .map(f32::from)
729 .unwrap_or(font_size * 0.6)
730 };
731 self.line_h = line_h;
732 self.cell_w = cell_w;
733 self.mono_font = font.clone();
734
735 let cursor = self.model.cursor();
736 let selection = self.model.selection();
737 let line_count = self.model.line_count();
738 let show_placeholder = self.model.is_empty() && !focused && !self.placeholder.is_empty();
739 let show_caret = focused && !self.read_only;
740
741 let digits = line_count.to_string().len().max(2);
742 let gutter_w = digits as f32 * cell_w + 2.0 * GUTTER_PAD;
743 let mut max_line_w: f32 = 0.0;
744
745 match &mut self.doc_highlighter {
749 Some(doc) => {
750 if self.doc_dirty {
751 let text = self.model.text();
752 doc.update(&text);
753 self.doc_dirty = false;
754 }
755 }
756 None => {
757 self.hl_cache.sync(&self.language, self.model.lines());
758 }
759 }
760
761 let mut gutter_rows: Vec<Div> = Vec::with_capacity(line_count);
762 let mut text_rows: Vec<Div> = Vec::with_capacity(line_count);
763 for (i, line) in self.model.lines().iter().enumerate() {
764 let is_active = i == cursor.line;
765
766 if self.line_numbers {
767 let mut gutter_row = div()
768 .relative()
769 .h(px(line_h))
770 .flex()
771 .items_center()
772 .justify_end()
773 .text_color(if is_active && focused {
774 gutter_fg_active
775 } else {
776 gutter_fg
777 })
778 .child(SharedString::from((i + 1).to_string()));
779 if let Some(severity) = line_severity(&self.diagnostics, i) {
780 gutter_row = gutter_row.child(
781 div()
782 .absolute()
783 .left(px(1.0))
784 .top(px((line_h - 6.0) / 2.0))
785 .w(px(6.0))
786 .h(px(6.0))
787 .rounded_full()
788 .bg(severity.color(t)),
789 );
790 }
791 gutter_rows.push(gutter_row);
792 }
793
794 let shaped = self.shape(line, window);
798 max_line_w = max_line_w.max(f32::from(shaped.width));
799
800 let mut row = div().relative().h(px(line_h)).w_full();
801 if focused && is_active {
802 row = row.bg(active_bg);
803 }
804 for (start, end, color) in &self.highlights {
805 if let Some((s, e, newline)) = line_selection(*start, *end, i, line.chars().count()) {
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 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 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 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
998fn 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
1023fn 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
1054fn hit_line(y: f32, line_h: f32) -> usize {
1057 (y / line_h).floor().max(0.0) as usize
1058}
1059
1060fn byte_for_col(line: &str, col: usize) -> usize {
1062 line
1063 .char_indices()
1064 .nth(col)
1065 .map(|(i, _)| i)
1066 .unwrap_or(line.len())
1067}
1068
1069fn col_for_byte(line: &str, byte: usize) -> usize {
1072 line.char_indices().take_while(|&(i, _)| i < byte).count()
1073}
1074
1075fn scroll_adjust(offset: f32, view: f32, top: f32, bottom: f32) -> f32 {
1078 let mut adjusted = offset;
1079 if bottom + adjusted > view {
1080 adjusted = view - bottom;
1081 }
1082 if top + adjusted < 0.0 {
1083 adjusted = -top;
1084 }
1085 adjusted
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090 use super::*;
1091
1092 fn total(spans: &[(usize, Option<TokenKind>)]) -> usize {
1093 spans.iter().map(|(len, _)| len).sum()
1094 }
1095
1096 #[test]
1097 fn spans_cover_the_line_exactly() {
1098 let tokens = vec![(2..5, TokenKind::Keyword), (7..9, TokenKind::Number)];
1099 let s = spans(10, &tokens);
1100 assert_eq!(
1101 s,
1102 vec![
1103 (2, None),
1104 (3, Some(TokenKind::Keyword)),
1105 (2, None),
1106 (2, Some(TokenKind::Number)),
1107 (1, None),
1108 ]
1109 );
1110 assert_eq!(total(&s), 10);
1111 }
1112
1113 #[test]
1114 fn spans_with_no_tokens_is_one_gap() {
1115 assert_eq!(spans(4, &[]), vec![(4, None)]);
1116 assert!(spans(0, &[]).is_empty());
1117 }
1118
1119 #[test]
1120 fn spans_clamp_overlap_and_overflow() {
1121 let tokens = vec![(0..6, TokenKind::Keyword), (4..8, TokenKind::Number)];
1123 let s = spans(10, &tokens);
1124 assert_eq!(total(&s), 10);
1125 let tokens = vec![(8..20, TokenKind::Comment)];
1127 let s = spans(10, &tokens);
1128 assert_eq!(s, vec![(8, None), (2, Some(TokenKind::Comment))]);
1129 }
1130
1131 #[test]
1132 fn spans_token_flush_to_both_edges() {
1133 let tokens = vec![(0..10, TokenKind::Comment)];
1134 assert_eq!(spans(10, &tokens), vec![(10, Some(TokenKind::Comment))]);
1135 }
1136
1137 fn at(line: usize, col: usize) -> Pos {
1138 Pos::new(line, col)
1139 }
1140
1141 #[test]
1142 fn selection_on_a_single_line() {
1143 let sel = line_selection(at(1, 2), at(1, 5), 1, 8);
1144 assert_eq!(sel, Some((2, 5, false)));
1145 assert_eq!(line_selection(at(1, 2), at(1, 5), 0, 8), None);
1146 assert_eq!(line_selection(at(1, 2), at(1, 5), 2, 8), None);
1147 }
1148
1149 #[test]
1150 fn selection_across_lines() {
1151 assert_eq!(line_selection(at(0, 3), at(2, 2), 0, 6), Some((3, 6, true)));
1153 assert_eq!(line_selection(at(0, 3), at(2, 2), 1, 4), Some((0, 4, true)));
1155 assert_eq!(
1157 line_selection(at(0, 3), at(2, 2), 2, 6),
1158 Some((0, 2, false))
1159 );
1160 }
1161
1162 #[test]
1163 fn selection_on_an_empty_middle_line_shows_the_newline() {
1164 assert_eq!(line_selection(at(0, 0), at(2, 1), 1, 0), Some((0, 0, true)));
1165 }
1166
1167 #[test]
1168 fn selection_cols_clamp_to_line_len() {
1169 assert_eq!(line_selection(at(0, 10), at(0, 20), 0, 5), None); assert_eq!(
1171 line_selection(at(0, 2), at(0, 20), 0, 5),
1172 Some((2, 5, false))
1173 );
1174 }
1175
1176 #[test]
1177 fn hit_line_maps_and_clamps() {
1178 assert_eq!(hit_line(0.0, 20.0), 0);
1179 assert_eq!(hit_line(45.0, 20.0), 2);
1180 assert_eq!(hit_line(-10.0, 20.0), 0); }
1182
1183 #[test]
1184 fn byte_for_col_handles_multibyte_chars() {
1185 assert_eq!(byte_for_col("abc", 0), 0);
1186 assert_eq!(byte_for_col("abc", 2), 2);
1187 assert_eq!(byte_for_col("abc", 9), 3); assert_eq!(byte_for_col("日本語abc", 1), 3);
1190 assert_eq!(byte_for_col("日本語abc", 3), 9);
1191 assert_eq!(byte_for_col("日本語abc", 4), 10);
1192 }
1193
1194 #[test]
1195 fn col_for_byte_inverts_byte_for_col() {
1196 let line = "日本語abc";
1197 for col in 0..=6 {
1198 assert_eq!(col_for_byte(line, byte_for_col(line, col)), col);
1199 }
1200 assert_eq!(col_for_byte(line, 999), 6); assert_eq!(col_for_byte("", 0), 0);
1202 }
1203
1204 #[test]
1205 fn scroll_adjust_reveals_the_target() {
1206 assert_eq!(scroll_adjust(-10.0, 100.0, 20.0, 40.0), -10.0);
1208 assert_eq!(scroll_adjust(-50.0, 100.0, 20.0, 40.0), -20.0);
1210 assert_eq!(scroll_adjust(0.0, 100.0, 150.0, 170.0), -70.0);
1212 assert_eq!(scroll_adjust(0.0, 50.0, 100.0, 200.0), -100.0);
1214 }
1215}