use std::sync::{Arc, Mutex};
use text_document::{
Color, FlowElement, FlowElementSnapshot, HighlightContext, HighlightFormat, HighlightMask,
PaintHighlightSpan, RangeHighlight, SyntaxHighlighter, TextDocument,
};
fn new_doc(text: &str) -> TextDocument {
let doc = TextDocument::new();
doc.set_plain_text(text).unwrap();
doc
}
fn masked_paint_spans(doc: &TextDocument, mask: &HighlightMask) -> Vec<PaintHighlightSpan> {
match &doc.snapshot_flow_masked(mask).elements[0] {
FlowElementSnapshot::Block(b) => b.paint_highlights.clone(),
_ => panic!("expected a block"),
}
}
fn first_block_id(doc: &TextDocument) -> usize {
match &doc.flow()[0] {
FlowElement::Block(b) => b.id(),
_ => panic!("expected a block"),
}
}
const RED: Color = Color {
red: 255,
green: 0,
blue: 0,
alpha: 255,
};
const BLUE: Color = Color {
red: 0,
green: 0,
blue: 255,
alpha: 255,
};
const GREEN: Color = Color {
red: 0,
green: 255,
blue: 0,
alpha: 255,
};
struct ColorAll(Color);
impl SyntaxHighlighter for ColorAll {
fn highlight_block(&self, text: &str, ctx: &mut HighlightContext) {
let len = text.chars().count();
if len > 0 {
ctx.set_format(
0,
len,
HighlightFormat {
foreground_color: Some(self.0),
..Default::default()
},
);
}
}
}
fn bg(color: Color) -> HighlightFormat {
HighlightFormat {
background_color: Some(color),
..Default::default()
}
}
#[test]
fn two_syntax_sessions_compose_by_field() {
let doc = new_doc("hello world");
doc.add_syntax_session(Arc::new(ColorAll(RED)));
struct BgAll(Color);
impl SyntaxHighlighter for BgAll {
fn highlight_block(&self, text: &str, ctx: &mut HighlightContext) {
let len = text.chars().count();
if len > 0 {
ctx.set_format(0, len, bg(self.0));
}
}
}
doc.add_syntax_session(Arc::new(BgAll(BLUE)));
let spans = masked_paint_spans(&doc, &HighlightMask::all());
assert!(!spans.is_empty(), "both sessions should paint");
assert!(spans.iter().all(|s| s.foreground_color == Some(RED)));
assert!(spans.iter().all(|s| s.background_color == Some(BLUE)));
}
#[test]
fn a_later_session_wins_the_same_field() {
let doc = new_doc("hello");
doc.add_syntax_session(Arc::new(ColorAll(RED)));
doc.add_syntax_session(Arc::new(ColorAll(BLUE)));
let spans = masked_paint_spans(&doc, &HighlightMask::all());
assert!(!spans.is_empty());
assert!(
spans.iter().all(|s| s.foreground_color == Some(BLUE)),
"the later session's foreground must win"
);
}
#[test]
fn per_session_state_cascades_do_not_interfere() {
struct StateTracker {
seen_prev: Mutex<Vec<i64>>,
}
impl SyntaxHighlighter for StateTracker {
fn highlight_block(&self, _text: &str, ctx: &mut HighlightContext) {
self.seen_prev
.lock()
.unwrap()
.push(ctx.previous_block_state());
ctx.set_current_block_state(ctx.previous_block_state() + 1);
}
}
let doc = new_doc("a\n\nb\n\nc");
let a = Arc::new(StateTracker {
seen_prev: Mutex::new(Vec::new()),
});
let b = Arc::new(StateTracker {
seen_prev: Mutex::new(Vec::new()),
});
doc.add_syntax_session(a.clone());
doc.add_syntax_session(b.clone());
a.seen_prev.lock().unwrap().clear();
b.seen_prev.lock().unwrap().clear();
doc.rehighlight();
let a_seen = a.seen_prev.lock().unwrap().clone();
let b_seen = b.seen_prev.lock().unwrap().clone();
assert_eq!(
a_seen, b_seen,
"the two cascades must be independent and identical"
);
assert_eq!(a_seen.first(), Some(&-1), "the cascade starts clean at −1");
assert!(
a_seen.windows(2).all(|w| w[1] == w[0] + 1),
"each block sees exactly the previous block's state — no other session's: {a_seen:?}"
);
}
#[test]
fn a_range_session_highlights_absolute_offsets() {
let doc = new_doc("hello world");
let find = doc.add_range_session();
assert!(doc.set_session_ranges(
find,
vec![RangeHighlight {
start: 6,
length: 5,
format: bg(GREEN),
}]
));
let spans = masked_paint_spans(&doc, &HighlightMask::all());
assert_eq!(spans.len(), 1, "one contiguous highlighted range");
assert_eq!(spans[0].start, 6);
assert_eq!(spans[0].length, 5);
assert_eq!(spans[0].background_color, Some(GREEN));
}
#[test]
fn a_range_session_slices_across_blocks() {
let doc = new_doc("hello\n\nworld\n\nagain");
let s = doc.add_range_session();
doc.set_session_ranges(
s,
vec![RangeHighlight {
start: 0,
length: 10_000,
format: bg(GREEN),
}],
);
let flow = doc.snapshot_flow_masked(&HighlightMask::all());
let painted: Vec<&PaintHighlightSpan> = flow
.elements
.iter()
.filter_map(|e| match e {
FlowElementSnapshot::Block(b) if !b.paint_highlights.is_empty() => {
Some(&b.paint_highlights[0])
}
_ => None,
})
.collect();
assert!(
painted.len() >= 2,
"the one absolute range must have sliced into several blocks, got {}",
painted.len()
);
for p in painted {
assert_eq!(
p.start, 0,
"each block's slice is BLOCK-relative — a block after the first must not carry an \
absolute offset"
);
assert!(p.length > 0);
assert_eq!(p.background_color, Some(GREEN));
}
}
#[test]
fn two_masks_over_one_document_differ() {
let doc = new_doc("hello world");
let syntax = doc.add_syntax_session(Arc::new(ColorAll(RED)));
let find_a = doc.add_range_session();
let find_b = doc.add_range_session();
doc.set_session_ranges(
find_a,
vec![RangeHighlight {
start: 0,
length: 5,
format: bg(GREEN),
}],
);
doc.set_session_ranges(
find_b,
vec![RangeHighlight {
start: 6,
length: 5,
format: bg(BLUE),
}],
);
let a = masked_paint_spans(&doc, &HighlightMask::only([syntax, find_a]));
assert!(a.iter().any(|s| s.background_color == Some(GREEN)));
assert!(
a.iter().all(|s| s.background_color != Some(BLUE)),
"pane A must not see pane B's find highlighting"
);
let b = masked_paint_spans(&doc, &HighlightMask::only([syntax, find_b]));
assert!(b.iter().any(|s| s.background_color == Some(BLUE)));
assert!(b.iter().all(|s| s.background_color != Some(GREEN)));
assert!(masked_paint_spans(&doc, &HighlightMask::none()).is_empty());
}
#[test]
fn a_mask_hides_the_sessions_it_omits() {
let doc = new_doc("hello world");
let syntax = doc.add_syntax_session(Arc::new(ColorAll(RED)));
let find = doc.add_range_session();
doc.set_session_ranges(
find,
vec![RangeHighlight {
start: 0,
length: 5,
format: bg(GREEN),
}],
);
let only_syntax = masked_paint_spans(&doc, &HighlightMask::only([syntax]));
assert!(
only_syntax
.iter()
.all(|s| s.background_color != Some(GREEN))
);
assert!(only_syntax.iter().any(|s| s.foreground_color == Some(RED)));
}
#[test]
fn set_syntax_highlighter_replaces_syntax_but_spares_range_sessions() {
let doc = new_doc("hello world");
let find = doc.add_range_session();
doc.set_session_ranges(
find,
vec![RangeHighlight {
start: 0,
length: 5,
format: bg(GREEN),
}],
);
doc.set_syntax_highlighter(Some(Arc::new(ColorAll(RED))));
doc.set_syntax_highlighter(Some(Arc::new(ColorAll(BLUE))));
let spans = masked_paint_spans(&doc, &HighlightMask::all());
assert!(
spans.iter().all(|s| s.foreground_color != Some(RED)),
"the replaced syntax highlighter must be gone"
);
assert!(spans.iter().any(|s| s.foreground_color == Some(BLUE)));
assert!(
spans.iter().any(|s| s.background_color == Some(GREEN)),
"the range session must survive set_syntax_highlighter"
);
doc.set_syntax_highlighter(None);
let spans = masked_paint_spans(&doc, &HighlightMask::all());
assert!(spans.iter().all(|s| s.foreground_color.is_none()));
assert!(spans.iter().any(|s| s.background_color == Some(GREEN)));
}
#[test]
fn set_syntax_highlighter_spares_a_separately_added_syntax_session() {
let doc = new_doc("hello world");
struct Underliner;
impl SyntaxHighlighter for Underliner {
fn highlight_block(&self, text: &str, ctx: &mut HighlightContext) {
let n = text.chars().count();
if n > 0 {
ctx.set_format(
0,
n,
HighlightFormat {
font_underline: Some(true),
..Default::default()
},
);
}
}
}
doc.add_syntax_session(Arc::new(Underliner));
doc.set_syntax_highlighter(Some(Arc::new(ColorAll(RED))));
doc.set_syntax_highlighter(Some(Arc::new(ColorAll(BLUE))));
let spans = masked_paint_spans(&doc, &HighlightMask::all());
assert!(spans.iter().any(|s| s.foreground_color == Some(BLUE)));
assert!(spans.iter().all(|s| s.foreground_color != Some(RED)));
assert!(
spans.iter().any(|s| s.font_underline == Some(true)),
"the independently-added syntax session must not be clobbered by the shim"
);
doc.set_syntax_highlighter(None);
let spans = masked_paint_spans(&doc, &HighlightMask::all());
assert!(spans.iter().all(|s| s.foreground_color.is_none()));
assert!(spans.iter().any(|s| s.font_underline == Some(true)));
}
#[test]
fn remove_session_retires_one_layer() {
let doc = new_doc("hello");
let a = doc.add_range_session();
let b = doc.add_range_session();
doc.set_session_ranges(
a,
vec![RangeHighlight {
start: 0,
length: 2,
format: bg(GREEN),
}],
);
doc.set_session_ranges(
b,
vec![RangeHighlight {
start: 3,
length: 2,
format: bg(BLUE),
}],
);
assert!(doc.remove_session(a));
assert!(!doc.remove_session(a), "already gone");
let spans = masked_paint_spans(&doc, &HighlightMask::all());
assert!(spans.iter().all(|s| s.background_color != Some(GREEN)));
assert!(spans.iter().any(|s| s.background_color == Some(BLUE)));
}
#[test]
fn set_session_ranges_rejects_a_syntax_session() {
let doc = new_doc("hello");
let syntax = doc.add_syntax_session(Arc::new(ColorAll(RED)));
assert!(
!doc.set_session_ranges(syntax, vec![]),
"a syntax session cannot take ranges"
);
assert!(!doc.set_session_ranges(text_document::SessionId(9999), vec![]));
let _ = first_block_id(&doc);
}
fn block_geometry(doc: &TextDocument) -> Vec<(usize, usize)> {
doc.snapshot_flow_masked(&HighlightMask::all())
.elements
.iter()
.filter_map(|e| match e {
FlowElementSnapshot::Block(b) => Some((b.position, b.text.chars().count())),
_ => None,
})
.collect()
}
fn all_block_spans(doc: &TextDocument) -> Vec<Vec<PaintHighlightSpan>> {
doc.snapshot_flow_masked(&HighlightMask::all())
.elements
.iter()
.filter_map(|e| match e {
FlowElementSnapshot::Block(b) => Some(b.paint_highlights.clone()),
_ => None,
})
.collect()
}
fn naive_painted_chars(geom: &[(usize, usize)], ranges: &[RangeHighlight]) -> Vec<Vec<usize>> {
geom.iter()
.map(|&(abs_start, len)| {
let block_end = abs_start + len;
let mut covered = std::collections::BTreeSet::new();
for r in ranges {
let lo = r.start.max(abs_start);
let hi = r.start.saturating_add(r.length).min(block_end);
for c in lo..hi {
covered.insert(c - abs_start);
}
}
covered.into_iter().collect()
})
.collect()
}
fn actual_painted_chars(doc: &TextDocument) -> Vec<Vec<usize>> {
all_block_spans(doc)
.iter()
.map(|spans| {
let mut covered = std::collections::BTreeSet::new();
for s in spans {
for c in s.start..s.start + s.length {
covered.insert(c);
}
}
covered.into_iter().collect()
})
.collect()
}
fn assert_matches_naive(doc: &TextDocument, ranges: &[RangeHighlight]) {
let geom = block_geometry(doc);
assert_eq!(
actual_painted_chars(doc),
naive_painted_chars(&geom, ranges),
"the indexed slicing must paint exactly the characters the naive full-scan does, \
ranges={ranges:?}"
);
}
#[test]
fn indexed_slicing_matches_the_naive_scan_over_many_shapes() {
let doc = new_doc("alpha\n\nbravo\n\ncharlie\n\ndelta\n\necho\n\nfoxtrot");
let s = doc.add_range_session();
let geom = block_geometry(&doc);
let doc_end = geom.last().map(|&(p, l)| p + l).unwrap();
for start in 0..=doc_end + 2 {
for &length in &[0usize, 1, 3, 7, doc_end + 100] {
let shape = vec![RangeHighlight {
start,
length,
format: bg(GREEN),
}];
assert!(doc.set_session_ranges(s, shape.clone()));
assert_matches_naive(&doc, &shape);
}
}
let multi: Vec<Vec<RangeHighlight>> = vec![
vec![
RangeHighlight {
start: 0,
length: 2,
format: bg(GREEN),
},
RangeHighlight {
start: 3,
length: 2,
format: bg(RED),
},
],
vec![
RangeHighlight {
start: geom[2].0,
length: geom[2].1,
format: bg(BLUE),
},
RangeHighlight {
start: 0,
length: doc_end + 50,
format: bg(RED),
},
],
];
for shape in multi {
assert!(doc.set_session_ranges(s, shape.clone()));
assert_matches_naive(&doc, &shape);
}
}
#[test]
fn a_downstream_edit_without_a_push_keeps_early_blocks_correct_and_the_new_block_empty() {
let doc = new_doc("alpha\n\nbravo");
let s = doc.add_range_session();
assert!(doc.set_session_ranges(
s,
vec![RangeHighlight {
start: 0,
length: 5,
format: bg(GREEN)
}]
));
let before = all_block_spans(&doc);
assert_eq!(before[0].len(), 1, "alpha flagged");
assert_eq!(before[0][0].length, 5);
assert!(before[1].is_empty(), "bravo not flagged");
let geom = block_geometry(&doc);
let end = geom.last().map(|&(p, l)| p + l).unwrap();
doc.cursor_at(end).insert_text("\n\ngamma").unwrap();
let after = all_block_spans(&doc);
assert_eq!(
after[0].len(),
1,
"alpha's squiggle survives an unrelated downstream edit"
);
assert_eq!(after[0][0].start, 0);
assert_eq!(after[0][0].length, 5);
assert!(
after.len() >= 3,
"the new paragraph exists as its own block"
);
assert!(
after.last().unwrap().is_empty(),
"the freshly-created block is absent from the index → it shows nothing (missing, not a \
stale span) until the next push"
);
}
#[test]
fn a_push_after_an_edit_buckets_against_the_new_layout() {
let doc = new_doc("alpha\n\nbravo");
let s = doc.add_range_session();
doc.cursor_at(0).insert_text("XX ").unwrap();
let geom = block_geometry(&doc);
let bravo = geom.last().unwrap().0;
assert!(doc.set_session_ranges(
s,
vec![RangeHighlight {
start: bravo,
length: 5,
format: bg(RED)
}]
));
let spans = all_block_spans(&doc);
assert!(spans[0].is_empty(), "block 0 (alpha) has no flag");
let last = spans.last().unwrap();
assert_eq!(last.len(), 1, "bravo flagged in its shifted block");
assert_eq!(last[0].start, 0, "at block-relative 0");
assert_eq!(last[0].length, 5);
}