use std::ops::Range;
use std::sync::Arc;
use crate::coords::Bias;
use crate::patch::Patch;
use crate::sum_tree::{Dimension, Item, Summary, SumTree};
#[cfg(any(test, debug_assertions))]
thread_local! {
pub(crate) static DECORATION_SORTS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
#[cfg(any(test, debug_assertions))]
thread_local! {
pub(crate) static DECORATION_VISITS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
#[inline]
fn count_visit() {
#[cfg(any(test, debug_assertions))]
DECORATION_VISITS.with(|c| c.set(c.get() + 1));
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum Severity {
Hint = 0,
Info = 1,
Warning = 2,
Error = 3,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Stickiness {
AlwaysGrows,
NeverGrows,
GrowsOnlyBefore,
GrowsOnlyAfter,
}
impl Stickiness {
#[must_use]
pub fn biases(self) -> (Bias, Bias) {
match self {
Self::AlwaysGrows => (Bias::Left, Bias::Right),
Self::NeverGrows => (Bias::Right, Bias::Left),
Self::GrowsOnlyBefore => (Bias::Left, Bias::Left),
Self::GrowsOnlyAfter => (Bias::Right, Bias::Right),
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub enum DecorationKind {
Diagnostic {
severity: Severity,
message: Arc<str>,
code: Option<Arc<str>>,
},
FindMatch,
SnippetStop {
index: u8,
},
AutoClosePair,
}
impl DecorationKind {
#[must_use]
pub fn empty_policy(&self) -> EmptyPolicy {
match self {
Self::FindMatch => EmptyPolicy::Drop,
_ => EmptyPolicy::Keep,
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum EmptyPolicy {
Keep,
Drop,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct DecorationId(u64);
#[derive(Clone, Debug)]
pub struct TrackedRange {
pub id: DecorationId,
pub range: Range<u32>,
pub kind: DecorationKind,
pub stickiness: Stickiness,
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct Diagnostic {
pub span: Range<u32>,
pub severity: Severity,
pub message: String,
pub code: Option<String>,
}
impl Diagnostic {
#[must_use]
pub fn new(span: Range<u32>, severity: Severity, message: impl Into<String>) -> Self {
Self {
span,
severity,
message: message.into(),
code: None,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum DiagnosticsOutcome {
Applied {
count: usize,
},
Stale {
current: u64,
},
}
#[derive(Clone, Debug)]
struct DecoItem {
gap: u32,
len: u32,
id: DecorationId,
kind: DecorationKind,
stickiness: Stickiness,
}
#[derive(Clone, Copy, Debug, Default)]
struct DecoSummary {
span: u32,
count: u32,
max_end: u32,
find_count: u32,
sev_max: u8,
}
impl Summary for DecoSummary {
fn add_summary(&mut self, o: &Self) {
self.max_end = self.max_end.max(self.span + o.max_end);
self.span += o.span;
self.count += o.count;
self.find_count += o.find_count; self.sev_max = self.sev_max.max(o.sev_max); }
}
impl Item for DecoItem {
type Summary = DecoSummary;
fn summary(&self) -> DecoSummary {
DecoSummary {
span: self.gap,
count: 1,
max_end: self.gap + self.len,
find_count: u32::from(matches!(self.kind, DecorationKind::FindMatch)),
sev_max: match self.kind {
DecorationKind::Diagnostic { severity, .. } => severity as u8 + 1, _ => 0,
},
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct StartDim(u32);
impl Dimension<DecoSummary> for StartDim {
fn add_summary(&mut self, s: &DecoSummary) {
self.0 += s.span;
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct CountDim(u32);
impl Dimension<DecoSummary> for CountDim {
fn add_summary(&mut self, s: &DecoSummary) {
self.0 += s.count;
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct FindCountDim(u32);
impl Dimension<DecoSummary> for FindCountDim {
fn add_summary(&mut self, s: &DecoSummary) {
self.0 += s.find_count;
}
}
fn ranges_to_decoitems(v: Vec<TrackedRange>, base: u32) -> impl Iterator<Item = DecoItem> {
let mut prev = base;
v.into_iter().map(move |r| {
debug_assert!(r.range.start >= prev, "ranges_to_decoitems needs ascending starts");
let gap = r.range.start - prev;
prev = r.range.start;
DecoItem {
gap,
len: r.range.end - r.range.start,
id: r.id,
kind: r.kind,
stickiness: r.stickiness,
}
})
}
fn decoitems_to_ranges(tree: &SumTree<DecoItem>, base: u32) -> Vec<TrackedRange> {
let mut out = Vec::with_capacity(tree.summary().count as usize);
let mut start = base;
for it in tree.items() {
count_visit(); start += it.gap;
out.push(TrackedRange {
id: it.id,
range: start..start + it.len,
kind: it.kind,
stickiness: it.stickiness,
});
}
out
}
fn remap_ranges(patch: &Patch, v: &mut Vec<TrackedRange>) {
let mut queries: Vec<(u32, Bias)> = Vec::with_capacity(v.len() * 2);
for r in v.iter() {
let (bs, be) = r.stickiness.biases();
queries.push((r.range.start, bs));
queries.push((r.range.end, be));
}
let mut mapped: Vec<u32> = Vec::new();
patch.map_many(&queries, &mut mapped);
for (i, r) in v.iter_mut().enumerate() {
let (ms, me) = (mapped[2 * i], mapped[2 * i + 1]);
r.range = ms.min(me)..me;
}
v.retain(|r| {
let collapsed = r.range.start == r.range.end;
!(collapsed && matches!(r.kind.empty_policy(), EmptyPolicy::Drop))
});
}
fn reanchor(
zone_c: &SumTree<DecoItem>,
k_hi: u32,
orig: &SumTree<DecoItem>,
delta: i64,
prev_start: u32,
) -> SumTree<DecoItem> {
if zone_c.is_empty() {
return zone_c.clone();
}
let (item, _c, StartDim(s)) = orig
.seek::<CountDim, StartDim>(&CountDim(k_hi))
.expect("zone_c non-empty ⇒ a k_hi-th item exists");
let first_old_start = s + item.gap;
let new_gap = (i64::from(first_old_start) + delta - i64::from(prev_start)) as u32;
let fixed = DecoItem { gap: new_gap, ..item.clone() };
zone_c.replace(CountDim(0)..CountDim(1), std::iter::once(fixed))
}
#[derive(Clone, Debug)]
pub struct DecorationStore {
tree: SumTree<DecoItem>,
next_id: u64,
}
impl Default for DecorationStore {
fn default() -> Self {
Self::new()
}
}
impl DecorationStore {
#[must_use]
pub fn new() -> Self {
Self { tree: SumTree::new(), next_id: 1 }
}
#[must_use]
pub fn len(&self) -> usize {
self.tree.summary().count as usize
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.tree.is_empty()
}
pub fn add_decoration(
&mut self,
range: Range<u32>,
kind: DecorationKind,
stickiness: Stickiness,
) -> DecorationId {
let id = self.mint();
let mut v = self.to_vec();
v.push(TrackedRange { id, range, kind, stickiness });
self.set_sorted(v);
id
}
pub fn take_decoration(&mut self, id: DecorationId) -> Option<TrackedRange> {
let mut v = self.to_vec();
let i = v.iter().position(|r| r.id == id)?;
let taken = v.remove(i);
self.rebuild(v); Some(taken)
}
#[must_use]
pub fn decoration_range(&self, id: DecorationId) -> Option<Range<u32>> {
let mut found = None;
self.tree.filter_visit::<StartDim, _, _>(
&|_, _| true,
&mut |it: &DecoItem, before: &StartDim| {
crate::perf::charge(1);
count_visit();
if it.id == id {
let start = before.0 + it.gap;
found = Some(start..start + it.len);
}
},
);
found
}
pub fn set_decoration_stickiness(&mut self, id: DecorationId, s: Stickiness) -> bool {
let mut v = self.to_vec();
match v.iter_mut().find(|r| r.id == id) {
Some(r) => {
r.stickiness = s;
self.rebuild(v); true
}
None => false,
}
}
pub fn decorations_in(&self, range: Range<u32>) -> impl Iterator<Item = TrackedRange> {
let (qs, qe) = (range.start, range.end);
let mut out: Vec<TrackedRange> = Vec::new();
self.tree.filter_visit::<StartDim, _, _>(
&|before: &StartDim, sum: &DecoSummary| {
before.0 <= qe && before.0 + sum.max_end >= qs
},
&mut |it: &DecoItem, before: &StartDim| {
let start = before.0 + it.gap;
let end = start + it.len;
if start <= qe && end >= qs {
crate::perf::charge(1);
count_visit();
out.push(TrackedRange {
id: it.id,
range: start..end,
kind: it.kind.clone(),
stickiness: it.stickiness,
});
}
},
);
out.into_iter()
}
#[must_use]
pub fn find_count(&self) -> usize {
self.tree.summary().find_count as usize
}
#[must_use]
pub fn find_rank_before(&self, off: u32) -> usize {
self.tree.summary_before(&StartDim(off)).find_count as usize
}
#[must_use]
pub fn nth_find(&self, r: usize) -> Option<(DecorationId, Range<u32>)> {
if r >= self.find_count() {
return None;
}
let (it, _fc, StartDim(base)) =
self.tree.seek::<FindCountDim, StartDim>(&FindCountDim(r as u32))?;
debug_assert!(
matches!(it.kind, DecorationKind::FindMatch),
"FindCountDim seek must land on the r-th FindMatch (only finds carry the dimension)"
);
let start = base + it.gap;
Some((it.id, start..start + it.len))
}
pub fn splice_sorted_batch(
&mut self,
spans: &[Range<u32>],
kind: DecorationKind,
stickiness: Stickiness,
) -> Vec<DecorationId> {
if spans.is_empty() {
return Vec::new();
}
let mut batch: Vec<TrackedRange> = Vec::with_capacity(spans.len());
let mut ids = Vec::with_capacity(spans.len());
for span in spans {
let id = self.mint();
ids.push(id);
batch.push(TrackedRange { id, range: span.clone(), kind: kind.clone(), stickiness });
}
let lo = spans.first().expect("non-empty").start;
let hi = spans.last().expect("non-empty").start;
let k_lo = self.tree.summary_before(&StartDim(lo)).count; let k_hi = self.tree.summary_before(&StartDim(hi.saturating_add(1))).count;
let (left, rest) = self.tree.split_at(&CountDim(k_lo));
let (middle, zone_c) = rest.split_at(&CountDim(k_hi - k_lo));
let left_span = left.extent::<StartDim>().0;
let mut merged = decoitems_to_ranges(&middle, left_span);
merged.extend(batch);
merged.sort_by(|a, b| a.range.start.cmp(&b.range.start).then(a.id.cmp(&b.id)));
let middle_new = SumTree::from_items(ranges_to_decoitems(merged, left_span));
let head = left.append(&middle_new);
let prev_start = head.extent::<StartDim>().0;
let zone_c_new = reanchor(&zone_c, k_hi, &self.tree, 0, prev_start);
self.tree = head.append(&zone_c_new);
ids
}
pub fn diagnostic_overview(&self, bounds: &[u32], out: &mut Vec<(u8, u32)>) {
out.clear();
let start_bounds: Vec<StartDim> = bounds.iter().map(|&b| StartDim(b)).collect();
let sev = self.tree.bucketed_reduce(&start_bounds, 0u8, |acc: &mut u8, s: &DecoSummary| {
*acc = (*acc).max(s.sev_max);
});
for (b, &max_sev) in sev.iter().enumerate() {
if max_sev == 0 {
out.push((0, 0));
} else {
let off = self
.first_start_with_severity(bounds[b], bounds[b + 1], max_sev)
.unwrap_or(bounds[b]);
out.push((max_sev, off));
}
}
}
pub fn find_overview(&self, bounds: &[u32], out: &mut Vec<Option<u32>>) {
out.clear();
for w in bounds.windows(2) {
let (lo, hi) = (w[0], w[1]);
let r = self.find_rank_before(lo); let first = self.nth_find(r).map(|(_, rng)| rng.start).filter(|&s| s < hi);
out.push(first);
}
}
fn first_start_with_severity(&self, lo: u32, hi: u32, target: u8) -> Option<u32> {
let mut found: Option<u32> = None;
self.tree.filter_visit::<StartDim, _, _>(
&|before: &StartDim, sum: &DecoSummary| {
before.0 < hi && before.0 + sum.span >= lo && sum.sev_max >= target
},
&mut |it: &DecoItem, before: &StartDim| {
if found.is_some() {
return; }
let start = before.0 + it.gap;
if start >= lo && start < hi {
let sev = match it.kind {
DecorationKind::Diagnostic { severity, .. } => severity as u8 + 1,
_ => 0,
};
if sev == target {
found = Some(start);
}
}
},
);
found
}
pub fn add_sorted_batch(
&mut self,
spans: impl IntoIterator<Item = Range<u32>>,
kind: DecorationKind,
stickiness: Stickiness,
) -> Vec<DecorationId> {
let mut v = self.to_vec();
let mut ids = Vec::new();
for range in spans {
let id = self.mint();
v.push(TrackedRange { id, range, kind: kind.clone(), stickiness });
ids.push(id);
}
if !ids.is_empty() {
self.set_sorted(v); }
ids
}
pub fn take_matching_in(
&mut self,
window: Range<u32>,
mut pred: impl FnMut(&TrackedRange) -> bool,
) -> Vec<TrackedRange> {
let (qs, qe) = (window.start, window.end);
let Some(wl) = self.decorations_in(qs..qe).map(|r| r.range.start).min() else {
return Vec::new(); };
let k_hi = self.tree.summary_before(&StartDim(qe.saturating_add(1))).count; let k_lo = self.tree.summary_before(&StartDim(wl)).count;
let (left, rest) = self.tree.split_at(&CountDim(k_lo));
let (middle, zone_c) = rest.split_at(&CountDim(k_hi - k_lo));
let left_span = left.extent::<StartDim>().0;
let mut removed = Vec::new();
let mut survivors = Vec::new();
for r in decoitems_to_ranges(&middle, left_span) {
let touches = r.range.start <= qe && r.range.end >= qs;
if touches && pred(&r) {
removed.push(r);
} else {
survivors.push(r); }
}
if removed.is_empty() {
return removed; }
let middle_new = SumTree::from_items(ranges_to_decoitems(survivors, left_span));
let head = left.append(&middle_new);
let prev_start = head.extent::<StartDim>().0;
let zone_c_new = reanchor(&zone_c, k_hi, &self.tree, 0, prev_start);
self.tree = head.append(&zone_c_new);
removed
}
pub fn iter(&self) -> impl Iterator<Item = TrackedRange> {
self.to_vec().into_iter()
}
pub fn apply_patch(&mut self, patch: &Patch) {
if patch.is_empty() {
return;
}
match patch.edits() {
[edit] => self.apply_single_edit(patch, edit),
_ => self.apply_patch_naive(patch),
}
}
fn apply_patch_naive(&mut self, patch: &Patch) {
let mut v = self.to_vec();
remap_ranges(patch, &mut v);
if v.windows(2).any(|w| (w[0].range.start, w[0].id) > (w[1].range.start, w[1].id)) {
self.set_sorted(v);
} else {
self.rebuild(v);
}
}
fn apply_single_edit(&mut self, patch: &Patch, edit: &crate::patch::Edit) {
let (os, oe) = (edit.old.start, edit.old.end);
let delta = (i64::from(edit.new.end) - i64::from(edit.new.start))
- (i64::from(oe) - i64::from(os));
let wl = self.decorations_in(os..oe).map(|r| r.range.start).min();
let k_hi = self.tree.summary_before(&StartDim(oe.saturating_add(1))).count; let k_lo = match wl {
Some(wl) => self.tree.summary_before(&StartDim(wl)).count, None => k_hi, };
let (left, rest) = self.tree.split_at(&CountDim(k_lo));
let (middle, zone_c) = rest.split_at(&CountDim(k_hi - k_lo));
let left_span = left.extent::<StartDim>().0;
let mut mv = decoitems_to_ranges(&middle, left_span);
remap_ranges(patch, &mut mv);
mv.sort_by(|a, b| a.range.start.cmp(&b.range.start).then(a.id.cmp(&b.id)));
let middle_new = SumTree::from_items(ranges_to_decoitems(mv, left_span));
let head = left.append(&middle_new);
let prev_start = head.extent::<StartDim>().0;
let zone_c_new = reanchor(&zone_c, k_hi, &self.tree, delta, prev_start);
self.tree = head.append(&zone_c_new);
}
pub fn set_diagnostics(
&mut self,
revision: u64,
current_revision: u64,
diags: Vec<Diagnostic>,
) -> DiagnosticsOutcome {
if revision != current_revision {
return DiagnosticsOutcome::Stale {
current: current_revision,
};
}
let mut v = self.to_vec();
v.retain(|r| !matches!(r.kind, DecorationKind::Diagnostic { .. }));
let count = diags.len();
for d in diags {
let id = self.mint();
v.push(TrackedRange {
id,
range: d.span,
kind: DecorationKind::Diagnostic {
severity: d.severity,
message: Arc::from(d.message),
code: d.code.map(Arc::from),
},
stickiness: Stickiness::NeverGrows,
});
}
self.set_sorted(v);
DiagnosticsOutcome::Applied { count }
}
fn mint(&mut self) -> DecorationId {
let id = DecorationId(self.next_id);
self.next_id += 1;
id
}
fn to_vec(&self) -> Vec<TrackedRange> {
decoitems_to_ranges(&self.tree, 0)
}
fn set_sorted(&mut self, mut v: Vec<TrackedRange>) {
#[cfg(any(test, debug_assertions))]
DECORATION_SORTS.with(|c| c.set(c.get() + 1));
crate::perf::charge(v.len() as u64); v.sort_by(|a, b| a.range.start.cmp(&b.range.start).then(a.id.cmp(&b.id)));
self.tree = SumTree::from_items(ranges_to_decoitems(v, 0));
}
fn rebuild(&mut self, v: Vec<TrackedRange>) {
crate::perf::charge(v.len() as u64); self.tree = SumTree::from_items(ranges_to_decoitems(v, 0));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coords::Bias::{Left, Right};
use crate::patch::Edit;
fn store() -> DecorationStore {
DecorationStore::new()
}
#[test]
fn windowed_query_equals_the_linear_oracle() {
let linear = |s: &DecorationStore, qs: u32, qe: u32| -> Vec<u64> {
s.iter()
.filter(|r| r.range.start <= qe && r.range.end >= qs)
.map(|r| r.id.0)
.collect()
};
let mut rng = 0x9E3779B97F4A7C15u64;
let mut next = move || {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
rng
};
let mut s = store();
for _ in 0..300 {
let a = (next() % 10_000) as u32;
let len = (next() % 50) as u32 * if next() % 4 == 0 { 40 } else { 1 };
s.add_decoration(a..a + len, DecorationKind::FindMatch, Stickiness::NeverGrows);
}
for _ in 0..200 {
let qs = (next() % 11_000) as u32;
let qe = qs + (next() % 400) as u32;
let fast: Vec<u64> = s.decorations_in(qs..qe).map(|r| r.id.0).collect();
assert_eq!(fast, linear(&s, qs, qe), "window {qs}..{qe} diverged from the oracle");
}
for _ in 0..50 {
let victim = DecorationId((next() % 300) + 1);
let _ = s.take_decoration(victim);
}
for _ in 0..100 {
let qs = (next() % 11_000) as u32;
let qe = qs + (next() % 400) as u32;
let fast: Vec<u64> = s.decorations_in(qs..qe).map(|r| r.id.0).collect();
assert_eq!(fast, linear(&s, qs, qe));
}
}
#[test]
fn batch_add_and_windowed_take() {
let mut s = store();
s.add_decoration(5..8, DecorationKind::SnippetStop { index: 0 }, Stickiness::AlwaysGrows);
let ids =
s.add_sorted_batch([0..2, 10..12, 20..22], DecorationKind::FindMatch, Stickiness::NeverGrows);
assert_eq!(ids.len(), 3);
let starts: Vec<u32> = s.iter().map(|r| r.range.start).collect();
assert_eq!(starts, vec![0, 5, 10, 20]);
let removed = s.take_matching_in(4..15, |r| matches!(r.kind, DecorationKind::FindMatch));
assert_eq!(removed.len(), 1);
assert_eq!(removed[0].range, 10..12);
let left: Vec<u32> = s.iter().map(|r| r.range.start).collect();
assert_eq!(left, vec![0, 5, 20]);
assert_eq!(s.decorations_in(0..u32::MAX).count(), 3);
}
fn ins(at: u32, len: u32) -> Patch {
Patch::single(Edit { old: at..at, new: at..at + len })
}
fn del(start: u32, end: u32) -> Patch {
Patch::single(Edit { old: start..end, new: start..start })
}
fn moved(stickiness: Stickiness, patch: &Patch) -> Range<u32> {
let mut s = store();
let id = s.add_decoration(4..8, DecorationKind::AutoClosePair, stickiness);
s.apply_patch(patch);
s.decoration_range(id).unwrap()
}
#[test]
fn biases_are_the_four_named_pairs() {
assert_eq!(Stickiness::AlwaysGrows.biases(), (Left, Right));
assert_eq!(Stickiness::NeverGrows.biases(), (Right, Left));
assert_eq!(Stickiness::GrowsOnlyBefore.biases(), (Left, Left));
assert_eq!(Stickiness::GrowsOnlyAfter.biases(), (Right, Right));
}
#[test]
fn severity_orders_hint_to_error() {
assert!(Severity::Hint < Severity::Info);
assert!(Severity::Info < Severity::Warning);
assert!(Severity::Warning < Severity::Error);
}
#[test]
fn only_find_match_drops_on_collapse() {
assert_eq!(DecorationKind::FindMatch.empty_policy(), EmptyPolicy::Drop);
assert_eq!(DecorationKind::AutoClosePair.empty_policy(), EmptyPolicy::Keep);
assert_eq!(
DecorationKind::SnippetStop { index: 0 }.empty_policy(),
EmptyPolicy::Keep
);
assert_eq!(
DecorationKind::Diagnostic {
severity: Severity::Error,
message: Arc::from("x"),
code: None,
}
.empty_policy(),
EmptyPolicy::Keep
);
}
#[test]
fn ids_are_monotonic_and_never_reused() {
let mut s = store();
let a = s.add_decoration(0..1, DecorationKind::AutoClosePair, Stickiness::NeverGrows);
let b = s.add_decoration(2..3, DecorationKind::AutoClosePair, Stickiness::NeverGrows);
assert!(a < b);
s.take_decoration(a);
let c = s.add_decoration(0..1, DecorationKind::AutoClosePair, Stickiness::NeverGrows);
assert!(c > b);
}
#[test]
fn take_returns_the_range_then_forgets_it() {
let mut s = store();
let id = s.add_decoration(3..5, DecorationKind::AutoClosePair, Stickiness::NeverGrows);
let taken = s.take_decoration(id).unwrap();
assert_eq!(taken.range, 3..5);
assert_eq!(s.decoration_range(id), None);
assert!(s.take_decoration(id).is_none()); }
#[test]
fn set_stickiness_swaps_or_reports_gone() {
let mut s = store();
let id = s.add_decoration(0..1, DecorationKind::SnippetStop { index: 0 }, Stickiness::NeverGrows);
assert!(s.set_decoration_stickiness(id, Stickiness::AlwaysGrows));
s.apply_patch(&ins(1, 2));
assert_eq!(s.decoration_range(id), Some(0..3));
s.take_decoration(id);
assert!(!s.set_decoration_stickiness(id, Stickiness::NeverGrows));
}
#[test]
fn decorations_in_counts_touching() {
let mut s = store();
let a = s.add_decoration(0..2, DecorationKind::AutoClosePair, Stickiness::NeverGrows);
let b = s.add_decoration(5..5, DecorationKind::AutoClosePair, Stickiness::NeverGrows); let c = s.add_decoration(8..10, DecorationKind::AutoClosePair, Stickiness::NeverGrows);
let got: Vec<_> = s.decorations_in(2..8).map(|r| r.id).collect();
assert_eq!(got, vec![a, b, c]);
assert_eq!(s.decorations_in(20..21).count(), 0);
}
#[test]
fn decorations_in_is_ascending_by_start_then_id() {
let mut s = store();
s.add_decoration(9..9, DecorationKind::AutoClosePair, Stickiness::NeverGrows);
s.add_decoration(1..2, DecorationKind::AutoClosePair, Stickiness::NeverGrows);
s.add_decoration(1..1, DecorationKind::AutoClosePair, Stickiness::NeverGrows);
let starts: Vec<_> = s.decorations_in(0..100).map(|r| r.range.start).collect();
assert_eq!(starts, vec![1, 1, 9]);
let ids: Vec<_> = s.decorations_in(0..2).map(|r| r.id).collect();
assert!(ids[0] < ids[1]);
}
#[test]
fn table_insert_at_start() {
let p = ins(4, 2); assert_eq!(moved(Stickiness::AlwaysGrows, &p), 4..10); assert_eq!(moved(Stickiness::NeverGrows, &p), 6..10); assert_eq!(moved(Stickiness::GrowsOnlyBefore, &p), 4..10);
assert_eq!(moved(Stickiness::GrowsOnlyAfter, &p), 6..10);
}
#[test]
fn table_insert_at_end() {
let p = ins(8, 2); assert_eq!(moved(Stickiness::AlwaysGrows, &p), 4..10); assert_eq!(moved(Stickiness::NeverGrows, &p), 4..8); assert_eq!(moved(Stickiness::GrowsOnlyBefore, &p), 4..8);
assert_eq!(moved(Stickiness::GrowsOnlyAfter, &p), 4..10);
}
#[test]
fn table_insert_inside_always_grows() {
let p = ins(6, 2); for m in [
Stickiness::AlwaysGrows,
Stickiness::NeverGrows,
Stickiness::GrowsOnlyBefore,
Stickiness::GrowsOnlyAfter,
] {
assert_eq!(moved(m, &p), 4..10, "{m:?} must grow on interior insert");
}
}
#[test]
fn table_delete_around_collapses_without_inverting() {
let p = del(2, 10); for m in [
Stickiness::AlwaysGrows,
Stickiness::NeverGrows,
Stickiness::GrowsOnlyBefore,
Stickiness::GrowsOnlyAfter,
] {
let r = moved(m, &p);
assert!(r.start <= r.end, "{m:?} inverted: {r:?}");
assert_eq!(r, 2..2, "{m:?} collapses to the mapped end");
}
}
#[test]
fn table_delete_prefix_pins_start_and_shifts_end() {
let p = del(4, 6); for m in [
Stickiness::AlwaysGrows,
Stickiness::NeverGrows,
Stickiness::GrowsOnlyBefore,
Stickiness::GrowsOnlyAfter,
] {
assert_eq!(moved(m, &p), 4..6, "{m:?}");
}
}
#[test]
fn edit_before_a_range_shifts_it_rigidly() {
let p = ins(0, 3); assert_eq!(moved(Stickiness::NeverGrows, &p), 7..11);
}
#[test]
fn apply_patch_drops_collapsed_find_match_keeps_others() {
let mut s = store();
let fm = s.add_decoration(4..8, DecorationKind::FindMatch, Stickiness::NeverGrows);
let diag = s.add_decoration(
4..8,
DecorationKind::Diagnostic {
severity: Severity::Error,
message: Arc::from("boom"),
code: None,
},
Stickiness::NeverGrows,
);
s.apply_patch(&del(2, 10)); assert_eq!(s.decoration_range(fm), None, "find match dropped on collapse");
assert_eq!(
s.decoration_range(diag),
Some(2..2),
"diagnostic kept (owner lifetime), rendered ≥1 cell later"
);
}
#[test]
fn empty_patch_leaves_the_store_untouched() {
let mut s = store();
let id = s.add_decoration(4..8, DecorationKind::FindMatch, Stickiness::NeverGrows);
s.apply_patch(&Patch::new());
assert_eq!(s.decoration_range(id), Some(4..8));
}
#[test]
fn apply_patch_reestablishes_sort_order() {
let mut s = store();
let a = s.add_decoration(0..1, DecorationKind::AutoClosePair, Stickiness::NeverGrows);
let b = s.add_decoration(5..6, DecorationKind::AutoClosePair, Stickiness::NeverGrows);
s.apply_patch(&ins(2, 10)); let order: Vec<_> = s.iter().map(|r| r.id).collect();
assert_eq!(order, vec![a, b]);
assert_eq!(s.decoration_range(a), Some(0..1));
assert_eq!(s.decoration_range(b), Some(15..16));
}
#[test]
fn set_diagnostics_replaces_wholesale() {
let mut s = store();
let outcome = s.set_diagnostics(
0,
0,
vec![
Diagnostic::new(0..3, Severity::Warning, "one"),
Diagnostic::new(5..8, Severity::Error, "two"),
],
);
assert_eq!(outcome, DiagnosticsOutcome::Applied { count: 2 });
assert_eq!(s.len(), 2);
let outcome = s.set_diagnostics(0, 0, vec![Diagnostic::new(1..2, Severity::Hint, "solo")]);
assert_eq!(outcome, DiagnosticsOutcome::Applied { count: 1 });
assert_eq!(s.len(), 1);
let only = s.iter().next().unwrap();
assert_eq!(only.range, 1..2);
assert_eq!(only.stickiness, Stickiness::NeverGrows);
}
#[test]
fn set_diagnostics_stale_drops_and_keeps_previous() {
let mut s = store();
s.set_diagnostics(0, 0, vec![Diagnostic::new(0..3, Severity::Error, "kept")]);
let outcome = s.set_diagnostics(0, 2, vec![Diagnostic::new(0..3, Severity::Info, "stale")]);
assert_eq!(outcome, DiagnosticsOutcome::Stale { current: 2 });
assert_eq!(s.len(), 1);
let kept = s.iter().next().unwrap().clone();
match &kept.kind {
DecorationKind::Diagnostic { message, severity, .. } => {
assert_eq!(&**message, "kept");
assert_eq!(*severity, Severity::Error);
}
other => panic!("expected a diagnostic, got {other:?}"),
}
}
#[test]
fn set_diagnostics_leaves_other_kinds_alone() {
let mut s = store();
let stop = s.add_decoration(9..9, DecorationKind::SnippetStop { index: 0 }, Stickiness::AlwaysGrows);
s.set_diagnostics(0, 0, vec![Diagnostic::new(0..3, Severity::Error, "e")]);
s.set_diagnostics(0, 0, vec![Diagnostic::new(1..2, Severity::Warning, "w")]);
assert_eq!(s.decoration_range(stop), Some(9..9));
let diag_count = s
.iter()
.filter(|r| matches!(r.kind, DecorationKind::Diagnostic { .. }))
.count();
assert_eq!(diag_count, 1);
}
#[test]
fn diagnostic_content_rides_the_kind() {
let mut s = store();
s.set_diagnostics(
3,
3,
vec![Diagnostic {
span: 0..4,
severity: Severity::Warning,
message: "unused".to_string(),
code: Some("W0612".to_string()),
}],
);
let got = s.iter().next().unwrap().clone();
match &got.kind {
DecorationKind::Diagnostic { severity, message, code } => {
assert_eq!(*severity, Severity::Warning);
assert_eq!(&**message, "unused");
assert_eq!(code.as_deref(), Some("W0612"));
}
other => panic!("expected a diagnostic, got {other:?}"),
}
}
#[test]
fn diagnostics_ride_edits_between_publishes() {
let mut s = store();
s.set_diagnostics(0, 0, vec![Diagnostic::new(10..14, Severity::Error, "here")]);
let id = s.iter().next().unwrap().id;
s.apply_patch(&ins(0, 5)); assert_eq!(s.decoration_range(id), Some(15..19));
}
#[test]
fn windowed_apply_patch_equals_naive_under_random_single_edits() {
let mut rng = 0xD1CE_5EED_u64;
let mut next = move || {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
rng
};
let kind_of = |t: u64| -> (DecorationKind, Stickiness) {
match t % 4 {
0 => (DecorationKind::FindMatch, Stickiness::NeverGrows), 1 => (DecorationKind::AutoClosePair, Stickiness::AlwaysGrows),
2 => (DecorationKind::SnippetStop { index: 0 }, Stickiness::GrowsOnlyBefore),
_ => (
DecorationKind::Diagnostic {
severity: Severity::Error,
message: Arc::from("x"),
code: None,
},
Stickiness::GrowsOnlyAfter,
),
}
};
let proj = |s: &DecorationStore| -> Vec<(u64, u32, u32)> {
s.iter().map(|r| (r.id.0, r.range.start, r.range.end)).collect()
};
for trial in 0..4000u32 {
let mut windowed = store();
let mut naive = store();
let n = next() % 40;
for _ in 0..n {
let a = (next() % 120) as u32;
let (kind, stick) = kind_of(next());
let mut len = (next() % 25) as u32;
if matches!(kind, DecorationKind::FindMatch) && len == 0 {
len = 1;
}
windowed.add_decoration(a..a + len, kind.clone(), stick);
naive.add_decoration(a..a + len, kind, stick);
}
let os = (next() % 120) as u32;
let oe = os + (next() % 15) as u32;
let ins = (next() % 15) as u32;
let patch = Patch::single(Edit { old: os..oe, new: os..os + ins });
windowed.apply_patch(&patch); naive.apply_patch_naive(&patch); assert_eq!(
proj(&windowed),
proj(&naive),
"trial {trial}: edit {os}..{oe} → +{ins}"
);
}
}
#[test]
fn windowed_apply_patch_is_sublinear_in_store_size() {
use crate::sum_tree::NODE_ALLOCS;
let build = |n: u32| -> DecorationStore {
let mut s = store();
let spans: Vec<Range<u32>> = (0..n).map(|i| (i * 10)..(i * 10 + 3)).collect();
s.add_sorted_batch(spans, DecorationKind::FindMatch, Stickiness::NeverGrows);
s
};
let allocs_for = |n: u32| -> f64 {
let mut s = build(n);
let patch = Patch::single(Edit { old: 5..5, new: 5..6 });
NODE_ALLOCS.with(|c| c.set(0));
s.apply_patch(&patch);
NODE_ALLOCS.with(std::cell::Cell::get) as f64
};
let (small, big) = (allocs_for(1000), allocs_for(4000));
eprintln!("[decorations] apply_patch node allocs {small} -> {big} ({:.2}x)", big / small);
assert!(
big <= small * 2.0,
"apply_patch allocates superlinearly ({small} -> {big} nodes): the mover \
reverted to a whole-store rebuild instead of shifting the suffix at one seam"
);
}
#[test]
fn windowed_take_matching_in_equals_naive() {
let mut rng = 0x7A6E_1234_u64;
let mut next = move || {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
rng
};
for trial in 0..3000u32 {
let mut s = store();
let n = next() % 40;
for _ in 0..n {
let a = (next() % 120) as u32;
let len = (next() % 25) as u32;
let (kind, stick) = match next() % 3 {
0 => (DecorationKind::FindMatch, Stickiness::NeverGrows),
1 => (DecorationKind::AutoClosePair, Stickiness::AlwaysGrows),
_ => (DecorationKind::SnippetStop { index: 0 }, Stickiness::NeverGrows),
};
s.add_decoration(a..a + len, kind, stick);
}
let before: Vec<(u64, u32, u32, bool)> = s
.iter()
.map(|r| {
(r.id.0, r.range.start, r.range.end, matches!(r.kind, DecorationKind::FindMatch))
})
.collect();
let qs = (next() % 120) as u32;
let qe = qs + (next() % 30) as u32;
let removed: Vec<u64> = s
.take_matching_in(qs..qe, |r| matches!(r.kind, DecorationKind::FindMatch))
.iter()
.map(|r| r.id.0)
.collect();
let touched = |st: u32, en: u32| st <= qe && en >= qs;
let exp_removed: Vec<u64> = before
.iter()
.filter(|&&(_, st, en, f)| f && touched(st, en))
.map(|&(id, ..)| id)
.collect();
let exp_surv: Vec<(u64, u32, u32)> = before
.iter()
.filter(|&&(_, st, en, f)| !(f && touched(st, en)))
.map(|&(id, st, en, _)| (id, st, en))
.collect();
let got_surv: Vec<(u64, u32, u32)> =
s.iter().map(|r| (r.id.0, r.range.start, r.range.end)).collect();
assert_eq!(removed, exp_removed, "trial {trial}: removed for window {qs}..{qe}");
assert_eq!(got_surv, exp_surv, "trial {trial}: survivors for window {qs}..{qe}");
}
}
#[test]
fn windowed_take_matching_in_is_sublinear_in_store_size() {
use crate::sum_tree::NODE_ALLOCS;
let build = |n: u32| -> DecorationStore {
let mut s = store();
let spans: Vec<Range<u32>> = (0..n).map(|i| (i * 10)..(i * 10 + 3)).collect();
s.add_sorted_batch(spans, DecorationKind::FindMatch, Stickiness::NeverGrows);
s
};
let allocs_for = |n: u32| -> f64 {
let mut s = build(n);
NODE_ALLOCS.with(|c| c.set(0));
s.take_matching_in(30..45, |r| matches!(r.kind, DecorationKind::FindMatch));
NODE_ALLOCS.with(std::cell::Cell::get) as f64
};
let (small, big) = (allocs_for(1000), allocs_for(4000));
eprintln!("[decorations] take_matching_in node allocs {small} -> {big} ({:.2}x)", big / small);
assert!(
big <= small * 2.0,
"take_matching_in allocates superlinearly ({small} -> {big} nodes): it scanned \
and rebuilt the whole store instead of splicing the window band"
);
}
fn xorshift(seed: u64) -> impl FnMut() -> u64 {
let mut rng = seed;
move || {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
rng
}
}
fn diag(sev: Severity) -> DecorationKind {
DecorationKind::Diagnostic { severity: sev, message: Arc::from("x"), code: None }
}
#[test]
fn splice_batch_equals_naive() {
let mut next = xorshift(0xBEEF_1234);
let kind_of = |t: u64| -> (DecorationKind, Stickiness) {
match t % 4 {
0 => (DecorationKind::FindMatch, Stickiness::NeverGrows),
1 => (DecorationKind::AutoClosePair, Stickiness::AlwaysGrows),
2 => (DecorationKind::SnippetStop { index: 0 }, Stickiness::GrowsOnlyBefore),
_ => (diag(Severity::Warning), Stickiness::NeverGrows),
}
};
let proj = |s: &DecorationStore| -> Vec<(u64, u32, u32)> {
s.iter().map(|r| (r.id.0, r.range.start, r.range.end)).collect()
};
for trial in 0..4000u32 {
let mut a = store();
let n = next() % 40;
for _ in 0..n {
let start = (next() % 200) as u32;
let len = (next() % 20) as u32;
let (kind, stick) = kind_of(next());
a.add_decoration(start..start + len, kind, stick);
}
let mut b = a.clone();
let base = (next() % 180) as u32;
let width = 1 + (next() % 30) as u32;
let count = next() % 8;
let mut starts: Vec<u32> =
(0..count).map(|_| base + (next() % u64::from(width)) as u32).collect();
starts.sort_unstable();
let spans: Vec<Range<u32>> =
starts.iter().map(|&s| s..s + (next() % 5) as u32).collect();
let ids_a =
a.splice_sorted_batch(&spans, DecorationKind::FindMatch, Stickiness::NeverGrows);
let mut v = b.to_vec();
let mut ids_b = Vec::new();
for span in &spans {
let id = b.mint();
ids_b.push(id);
v.push(TrackedRange {
id,
range: span.clone(),
kind: DecorationKind::FindMatch,
stickiness: Stickiness::NeverGrows,
});
}
if !spans.is_empty() {
b.set_sorted(v);
}
assert_eq!(ids_a, ids_b, "trial {trial}: minted ids diverged");
assert_eq!(proj(&a), proj(&b), "trial {trial}: batch {spans:?}");
}
}
#[test]
fn find_count_rank_nth_agree_with_linear() {
let mut next = xorshift(0xF19D_5EED);
for trial in 0..3000u32 {
let mut s = store();
let n = next() % 60;
for _ in 0..n {
let start = (next() % 300) as u32;
let len = (next() % 10) as u32;
let (kind, stick) = match next() % 3 {
0 => (DecorationKind::FindMatch, Stickiness::NeverGrows),
1 => (diag(Severity::Error), Stickiness::NeverGrows),
_ => (DecorationKind::SnippetStop { index: 0 }, Stickiness::AlwaysGrows),
};
s.add_decoration(start..start + len, kind, stick);
}
let finds: Vec<(u64, u32, u32)> = s
.iter()
.filter(|r| matches!(r.kind, DecorationKind::FindMatch))
.map(|r| (r.id.0, r.range.start, r.range.end))
.collect();
assert_eq!(s.find_count(), finds.len(), "trial {trial}: find_count");
for off in [0u32, 1, 50, 100, 150, 200, 299, 300, u32::MAX] {
let want = finds.iter().filter(|(_, st, _)| *st < off).count();
assert_eq!(s.find_rank_before(off), want, "trial {trial}: rank before {off}");
}
for (r, want) in finds.iter().enumerate() {
let (id, rng) = s.nth_find(r).expect("r < find_count");
assert_eq!((id.0, rng.start, rng.end), *want, "trial {trial}: nth_find({r})");
}
assert!(s.nth_find(finds.len()).is_none(), "trial {trial}: nth_find past the end");
}
}
#[test]
fn overview_reduce_equals_linear_scan() {
let mut next = xorshift(0x0FF3_1234);
for trial in 0..2500u32 {
let mut s = store();
let n = next() % 60;
for _ in 0..n {
let start = (next() % 400) as u32;
let len = (next() % 8) as u32;
let (kind, stick) = match next() % 5 {
0 | 1 => {
let sev = match next() % 4 {
0 => Severity::Hint,
1 => Severity::Info,
2 => Severity::Warning,
_ => Severity::Error,
};
(diag(sev), Stickiness::NeverGrows)
}
2 | 3 => (DecorationKind::FindMatch, Stickiness::NeverGrows),
_ => (DecorationKind::SnippetStop { index: 0 }, Stickiness::AlwaysGrows),
};
s.add_decoration(start..start + len, kind, stick);
}
let m = 2 + (next() % 8) as usize;
let mut bounds: Vec<u32> = (0..m).map(|_| (next() % 450) as u32).collect();
bounds.sort_unstable();
let items: Vec<TrackedRange> = s.iter().collect();
let mut diag_out = Vec::new();
s.diagnostic_overview(&bounds, &mut diag_out);
let want_diag: Vec<(u8, u32)> = bounds
.windows(2)
.map(|w| {
let (lo, hi) = (w[0], w[1]);
let mut max_sev = 0u8;
let mut off = 0u32;
for r in &items {
if r.range.start >= lo && r.range.start < hi {
if let DecorationKind::Diagnostic { severity, .. } = r.kind {
let sev = severity as u8 + 1;
if sev > max_sev {
max_sev = sev;
off = r.range.start;
}
}
}
}
(max_sev, off)
})
.collect();
assert_eq!(diag_out, want_diag, "trial {trial}: diag, bounds {bounds:?}");
let mut find_out = Vec::new();
s.find_overview(&bounds, &mut find_out);
let want_find: Vec<Option<u32>> = bounds
.windows(2)
.map(|w| {
let (lo, hi) = (w[0], w[1]);
items
.iter()
.find(|r| {
matches!(r.kind, DecorationKind::FindMatch)
&& r.range.start >= lo
&& r.range.start < hi
})
.map(|r| r.range.start)
})
.collect();
assert_eq!(find_out, want_find, "trial {trial}: find, bounds {bounds:?}");
}
}
#[test]
fn splice_sorted_batch_is_sublinear_in_store_size() {
use crate::sum_tree::NODE_ALLOCS;
let build = |n: u32| -> DecorationStore {
let mut s = store();
let spans: Vec<Range<u32>> = (0..n).map(|i| (i * 10)..(i * 10 + 3)).collect();
s.add_sorted_batch(spans, DecorationKind::FindMatch, Stickiness::NeverGrows);
s
};
let allocs_for = |n: u32| -> f64 {
let mut s = build(n);
let spans = [35..37, 36..39, 44..46]; NODE_ALLOCS.with(|c| c.set(0));
s.splice_sorted_batch(&spans, DecorationKind::FindMatch, Stickiness::NeverGrows);
NODE_ALLOCS.with(std::cell::Cell::get) as f64
};
let (small, big) = (allocs_for(1000), allocs_for(4000));
eprintln!("[decorations] splice_sorted_batch node allocs {small} -> {big} ({:.2}x)", big / small);
assert!(
big <= small * 2.0,
"splice_sorted_batch allocates superlinearly ({small} -> {big} nodes): it rebuilt \
the whole store instead of splicing the window band"
);
}
}