use std::{cell::RefCell, rc::Rc};
use gpui::{EntityInputHandler, TestAppContext, TextRun, font};
use theme::{Appearance, HighlightKind, SyntaxPalette, Theme};
use ui::input::{Edit, FieldEvent, TextField, coloured, underlined};
fn base() -> TextRun {
TextRun {
len: 0,
font: font("Helvetica"),
color: gpui::black(),
background_color: None,
underline: None,
strikethrough: None,
}
}
fn palette(cx: &mut TestAppContext) -> SyntaxPalette {
cx.update(|cx| {
Theme::install(Appearance::Dark, cx);
Theme::of(cx).syntax.clone()
})
}
fn covers(text: &str, runs: &[TextRun]) {
assert_eq!(
runs.iter().map(|run| run.len).sum::<usize>(),
text.len(),
"runs must cover the text exactly"
);
}
#[gpui::test]
fn spans_colour_themselves_and_leave_the_gaps_alone(cx: &mut TestAppContext) {
let palette = palette(cx);
let text = "let x = 1;";
let runs = coloured(
text,
&[
(0..3, HighlightKind::Keyword),
(8..9, HighlightKind::Number),
],
&base(),
&palette,
);
covers(text, &runs);
assert_eq!(
runs.iter().map(|run| run.len).collect::<Vec<_>>(),
vec![3, 5, 1, 1],
"keyword, the gap, the number, the tail"
);
assert_eq!(runs[0].color, palette.color(HighlightKind::Keyword));
assert_eq!(runs[1].color, base().color, "the gap is the field's own");
assert_eq!(runs[2].color, palette.color(HighlightKind::Number));
}
#[gpui::test]
fn text_with_no_spans_is_one_run(cx: &mut TestAppContext) {
let palette = palette(cx);
let runs = coloured("plain", &[], &base(), &palette);
assert_eq!(runs.len(), 1);
covers("plain", &runs);
}
#[gpui::test]
fn spans_the_text_has_outgrown_are_dropped(cx: &mut TestAppContext) {
let palette = palette(cx);
let text = "let x = 1;";
for spans in [
vec![
(0..3, HighlightKind::Keyword),
(400..999, HighlightKind::Number),
],
vec![
(0..6, HighlightKind::Keyword),
(2..4, HighlightKind::Number),
],
vec![(5..5, HighlightKind::Number)],
vec![(0..text.len() + 1, HighlightKind::String)],
] {
let runs = coloured(text, &spans, &base(), &palette);
covers(text, &runs);
}
}
#[gpui::test]
fn spans_never_cut_a_character_in_half(cx: &mut TestAppContext) {
let palette = palette(cx);
let text = "let δΈζ = 1;";
let runs = coloured(text, &[(4..5, HighlightKind::String)], &base(), &palette);
covers(text, &runs);
assert_eq!(runs.len(), 1, "the span lands mid-character and is dropped");
}
#[gpui::test]
fn the_composition_range_underlines_whatever_colour_is_there(cx: &mut TestAppContext) {
let palette = palette(cx);
let text = "let x = 1;";
let runs = coloured(text, &[(0..3, HighlightKind::Keyword)], &base(), &palette);
let runs = underlined(runs, &(1..5));
covers(text, &runs);
let marked: Vec<_> = runs
.iter()
.scan(0, |at, run| {
let start = *at;
*at += run.len;
Some((start..*at, run))
})
.filter(|(_, run)| run.underline.is_some())
.map(|(range, run)| (range, run.color))
.collect();
assert_eq!(
marked
.iter()
.map(|(range, _)| range.clone())
.collect::<Vec<_>>(),
vec![1..3, 3..5],
"the marked range is underlined across the colour boundary"
);
assert_eq!(
marked[0].1,
palette.color(HighlightKind::Keyword),
"and keeps the colour it had"
);
}
#[gpui::test]
fn replacing_the_content_drops_the_colours(cx: &mut TestAppContext) {
cx.update(|cx| Theme::install(Appearance::Dark, cx));
let window = cx.add_window(|_, cx| TextField::new(cx));
window
.update(cx, |field, _, cx| {
field.set_content("let x = 1;", cx);
field.set_spans(vec![(0..3, HighlightKind::Keyword)], cx);
assert_eq!(field.spans().len(), 1);
field.set_content("let y = 2;", cx);
assert!(field.spans().is_empty());
})
.unwrap();
}
#[test]
fn an_edit_between_two_texts_leaves_out_what_they_share() {
assert_eq!(
Edit::between("let x = 1;", "let mut x = 1;"),
Edit {
start: 4,
old_end: 4,
new_end: 8
}
);
assert_eq!(
Edit::between("a δΈζ b", "a b"),
Edit {
start: 2,
old_end: 9,
new_end: 2
}
);
assert_eq!(
Edit::between("same", "same"),
Edit {
start: 4,
old_end: 4,
new_end: 4
}
);
}
#[test]
fn a_range_moves_with_the_edit_around_it() {
let insert = Edit {
start: 4,
old_end: 4,
new_end: 8,
};
assert_eq!(insert.map(0..3), Some(0..3), "before it stays");
assert_eq!(insert.map(0..4), Some(0..4), "ending where it starts stays");
assert_eq!(
insert.map(4..6),
Some(8..10),
"starting where it starts moves"
);
assert_eq!(insert.map(2..6), Some(2..10), "around it grows");
let delete = Edit {
start: 2,
old_end: 6,
new_end: 2,
};
assert_eq!(
delete.map(0..4),
Some(0..2),
"cut at its end keeps the start"
);
assert_eq!(
delete.map(4..9),
Some(2..5),
"cut at its start keeps the end"
);
assert_eq!(delete.map(3..5), None, "taken whole is gone");
assert_eq!(delete.map(7..9), Some(3..5), "after it moves back");
}
#[gpui::test]
fn spans_follow_the_text_through_an_edit(cx: &mut TestAppContext) {
cx.update(|cx| Theme::install(Appearance::Dark, cx));
let window = cx.add_window(|_, cx| TextField::new(cx));
let field = window.root(cx).unwrap();
let events: Rc<RefCell<Vec<FieldEvent>>> = Rc::default();
cx.update({
let events = events.clone();
|cx| {
cx.subscribe(&field, move |_, event: &FieldEvent, _| {
events.borrow_mut().push(*event)
})
.detach()
}
});
window
.update(cx, |field, window, cx| {
field.set_content("let s = \"ab\";", cx);
field.set_spans(
vec![
(0..3, HighlightKind::Keyword),
(8..12, HighlightKind::String),
],
cx,
);
field.select(4..4, cx);
field.replace_text_in_range(None, "mut ", window, cx);
assert_eq!(
field.spans(),
&[
(0..3, HighlightKind::Keyword),
(12..16, HighlightKind::String)
]
);
field.select(14..14, cx);
field.replace_text_in_range(None, "c", window, cx);
assert_eq!(field.spans()[1], (12..17, HighlightKind::String));
})
.unwrap();
assert_eq!(
events.borrow()[1],
FieldEvent::Changed(Edit {
start: 4,
old_end: 4,
new_end: 8
})
);
}