1use 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
39const MONO_FAMILY: &str = "Menlo";
41const PAD_X: f32 = 10.0;
43const PAD_Y: f32 = 8.0;
45const GUTTER_PAD: f32 = 10.0;
47
48#[derive(Debug, Clone)]
50pub enum EditorEvent {
51 Change(String),
53 Run(String),
55}
56
57struct EditorDrag(EntityId);
60
61#[derive(Clone, Copy, Default)]
64pub struct EditorStyle {
65 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
77pub 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 text_bounds: Bounds<Pixels>,
100 cell_w: f32,
103 line_h: f32,
105 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 pub fn value(mut self, text: &str) -> Self {
140 self.model.set_text(text);
141 self
142 }
143
144 pub fn language(mut self, language: Language) -> Self {
146 self.language = language;
147 self
148 }
149
150 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
152 self.placeholder = placeholder.into();
153 self
154 }
155
156 pub fn read_only(mut self, read_only: bool) -> Self {
158 self.read_only = read_only;
159 self
160 }
161
162 pub fn line_numbers(mut self, show: bool) -> Self {
164 self.line_numbers = show;
165 self
166 }
167
168 pub fn tab_size(mut self, n: usize) -> Self {
170 self.model.set_tab_size(n);
171 self
172 }
173
174 pub fn font_size(mut self, size: f32) -> Self {
176 self.font_size = size;
177 self
178 }
179
180 pub fn rows(mut self, rows: usize) -> Self {
182 self.rows = Some(rows);
183 self
184 }
185
186 pub fn token_colors(mut self, colors: [Hsla; 8]) -> Self {
190 self.token_palette = Some(colors);
191 self
192 }
193
194 pub fn style(mut self, style: EditorStyle) -> Self {
196 self.style = style;
197 self
198 }
199
200 pub fn set_style(&mut self, style: EditorStyle, cx: &mut Context<Self>) {
202 self.style = style;
203 cx.notify();
204 }
205
206 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 pub fn text(&self) -> String {
218 self.model.text()
219 }
220
221 pub fn set_text(&mut self, value: &str, cx: &mut Context<Self>) {
223 self.model.set_text(value);
224 cx.notify();
225 }
226
227 pub fn focus_handle(&self) -> FocusHandle {
229 self.focus.clone()
230 }
231
232 pub fn model(&self) -> &EditorModel {
236 &self.model
237 }
238
239 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 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 pub fn line_height(&self) -> f32 {
276 self.line_h
277 }
278
279 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 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 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 if m.platform || self.read_only {
428 return;
429 }
430 self.model.tab();
431 self.after_edit(window, cx);
432 }
433 "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 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 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 }
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 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 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 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 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 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 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 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 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
854fn 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
879fn 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
910fn hit_line(y: f32, line_h: f32) -> usize {
913 (y / line_h).floor().max(0.0) as usize
914}
915
916fn 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
924fn col_for_byte(line: &str, byte: usize) -> usize {
927 line.char_indices().take_while(|&(i, _)| i < byte).count()
928}
929
930fn 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 let tokens = vec![(0..6, TokenKind::Keyword), (4..8, TokenKind::Number)];
978 let s = spans(10, &tokens);
979 assert_eq!(total(&s), 10);
980 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 assert_eq!(line_selection(at(0, 3), at(2, 2), 0, 6), Some((3, 6, true)));
1008 assert_eq!(line_selection(at(0, 3), at(2, 2), 1, 4), Some((0, 4, true)));
1010 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); 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); }
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); 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); assert_eq!(col_for_byte("", 0), 0);
1057 }
1058
1059 #[test]
1060 fn scroll_adjust_reveals_the_target() {
1061 assert_eq!(scroll_adjust(-10.0, 100.0, 20.0, 40.0), -10.0);
1063 assert_eq!(scroll_adjust(-50.0, 100.0, 20.0, 40.0), -20.0);
1065 assert_eq!(scroll_adjust(0.0, 100.0, 150.0, 170.0), -70.0);
1067 assert_eq!(scroll_adjust(0.0, 50.0, 100.0, 200.0), -100.0);
1069 }
1070}