use crate::model::marker::{MarkerId, MarkerList};
use fresh_core::overlay::OverlayNamespace;
use std::collections::HashMap;
use std::ops::Range;
#[derive(Debug, Clone)]
pub struct ConcealRange {
pub namespace: OverlayNamespace,
pub start_marker: MarkerId,
pub end_marker: MarkerId,
pub replacement: Option<String>,
}
impl ConcealRange {
pub fn range(&self, marker_list: &MarkerList) -> Range<usize> {
let start = marker_list.get_position(self.start_marker).unwrap_or(0);
let end = marker_list.get_position(self.end_marker).unwrap_or(0);
start..end
}
pub fn overlaps(&self, range: &Range<usize>, marker_list: &MarkerList) -> bool {
let self_range = self.range(marker_list);
self_range.start < range.end && range.start < self_range.end
}
}
#[derive(Debug, Clone)]
pub struct ConcealManager {
ranges: Vec<ConcealRange>,
marker_to_idx: HashMap<MarkerId, usize>,
version: u32,
}
impl ConcealManager {
pub fn new() -> Self {
Self {
ranges: Vec::new(),
marker_to_idx: HashMap::new(),
version: 0,
}
}
pub fn version(&self) -> u32 {
self.version
}
pub fn add(
&mut self,
marker_list: &mut MarkerList,
namespace: OverlayNamespace,
range: Range<usize>,
replacement: Option<String>,
) {
let start_marker = marker_list.create(range.start, true); let end_marker = marker_list.create(range.end, false);
let idx = self.ranges.len();
self.marker_to_idx.insert(start_marker, idx);
self.marker_to_idx.insert(end_marker, idx);
self.ranges.push(ConcealRange {
namespace,
start_marker,
end_marker,
replacement,
});
self.version = self.version.wrapping_add(1);
}
pub fn clear_namespace(&mut self, namespace: &OverlayNamespace, marker_list: &mut MarkerList) {
let mut indices: Vec<usize> = self
.ranges
.iter()
.enumerate()
.filter_map(|(i, r)| (&r.namespace == namespace).then_some(i))
.collect();
if indices.is_empty() {
return;
}
indices.sort_unstable_by(|a, b| b.cmp(a));
for idx in indices {
self.swap_remove_at(idx, marker_list);
}
self.version = self.version.wrapping_add(1);
}
pub fn remove_in_range(&mut self, range: &Range<usize>, marker_list: &mut MarkerList) {
if range.start >= range.end {
return;
}
let hits = marker_list.query_range(range.start, range.end);
if hits.is_empty() {
return;
}
let mut candidates: Vec<usize> = hits
.iter()
.filter_map(|(mid, _, _)| self.marker_to_idx.get(mid).copied())
.collect();
candidates.sort_unstable();
candidates.dedup();
let mut to_remove: Vec<usize> = candidates
.into_iter()
.filter(|&idx| {
let r = &self.ranges[idx];
let start = marker_list.get_position(r.start_marker).unwrap_or(0);
let end = marker_list.get_position(r.end_marker).unwrap_or(0);
start < range.end && range.start < end
})
.collect();
if to_remove.is_empty() {
return;
}
to_remove.sort_unstable_by(|a, b| b.cmp(a));
for idx in to_remove {
self.swap_remove_at(idx, marker_list);
}
self.version = self.version.wrapping_add(1);
}
pub fn remove_in_range_for_namespace(
&mut self,
namespace: &OverlayNamespace,
range: &Range<usize>,
marker_list: &mut MarkerList,
) {
if range.start >= range.end {
return;
}
let hits = marker_list.query_range(range.start, range.end);
if hits.is_empty() {
return;
}
let mut candidates: Vec<usize> = hits
.iter()
.filter_map(|(mid, _, _)| self.marker_to_idx.get(mid).copied())
.collect();
candidates.sort_unstable();
candidates.dedup();
let mut to_remove: Vec<usize> = candidates
.into_iter()
.filter(|&idx| {
let r = &self.ranges[idx];
if &r.namespace != namespace {
return false;
}
let start = marker_list.get_position(r.start_marker).unwrap_or(0);
let end = marker_list.get_position(r.end_marker).unwrap_or(0);
start < range.end && range.start < end
})
.collect();
if to_remove.is_empty() {
return;
}
to_remove.sort_unstable_by(|a, b| b.cmp(a));
for idx in to_remove {
self.swap_remove_at(idx, marker_list);
}
self.version = self.version.wrapping_add(1);
}
pub fn clear(&mut self, marker_list: &mut MarkerList) {
let had_any = !self.ranges.is_empty();
for range in &self.ranges {
marker_list.delete(range.start_marker);
marker_list.delete(range.end_marker);
}
self.ranges.clear();
self.marker_to_idx.clear();
if had_any {
self.version = self.version.wrapping_add(1);
}
}
fn swap_remove_at(&mut self, idx: usize, marker_list: &mut MarkerList) {
let removed = self.ranges.swap_remove(idx);
self.marker_to_idx.remove(&removed.start_marker);
self.marker_to_idx.remove(&removed.end_marker);
marker_list.delete(removed.start_marker);
marker_list.delete(removed.end_marker);
if let Some(moved) = self.ranges.get(idx) {
self.marker_to_idx.insert(moved.start_marker, idx);
self.marker_to_idx.insert(moved.end_marker, idx);
}
}
pub fn query_viewport(
&self,
start: usize,
end: usize,
marker_list: &MarkerList,
) -> Vec<(Range<usize>, Option<&str>)> {
let mut results: Vec<(Range<usize>, Option<&str>)> = self
.ranges
.iter()
.filter_map(|r| {
let range = r.range(marker_list);
if range.start < end && start < range.end {
Some((range, r.replacement.as_deref()))
} else {
None
}
})
.collect();
results.sort_by_key(|(range, _)| range.start);
if !results.is_empty() {
let summary: Vec<String> = results
.iter()
.map(|(r, repl)| format!("{}..{}={}", r.start, r.end, repl.unwrap_or("hide")))
.collect();
tracing::trace!(
"[conceal] query_viewport({start}..{end}): {} ranges: {}",
results.len(),
summary.join(", ")
);
}
results
}
pub fn is_concealed(
&self,
position: usize,
marker_list: &MarkerList,
) -> Option<(Range<usize>, Option<&str>)> {
for r in &self.ranges {
let range = r.range(marker_list);
if range.contains(&position) {
return Some((range, r.replacement.as_deref()));
}
}
None
}
pub fn is_empty(&self) -> bool {
self.ranges.is_empty()
}
#[cfg(test)]
fn check_invariants(&self) {
assert_eq!(
self.marker_to_idx.len(),
self.ranges.len() * 2,
"marker_to_idx size != 2 * ranges.len()"
);
for (i, r) in self.ranges.iter().enumerate() {
assert_eq!(
self.marker_to_idx.get(&r.start_marker).copied(),
Some(i),
"start_marker {:?} of range {} mismapped",
r.start_marker,
i,
);
assert_eq!(
self.marker_to_idx.get(&r.end_marker).copied(),
Some(i),
"end_marker {:?} of range {} mismapped",
r.end_marker,
i,
);
}
}
}
impl Default for ConcealManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ns() -> OverlayNamespace {
OverlayNamespace::from_string("test".to_string())
}
#[test]
fn test_conceal_remove_in_range_keeps_only_disjoint() {
let mut marker_list = MarkerList::new();
marker_list.set_buffer_size(200);
let mut manager = ConcealManager::new();
manager.add(&mut marker_list, ns(), 0..5, None);
manager.add(&mut marker_list, ns(), 10..20, None);
manager.add(&mut marker_list, ns(), 30..40, None);
manager.add(&mut marker_list, ns(), 50..60, None);
manager.remove_in_range(&(15..35), &mut marker_list);
let kept: Vec<_> = manager
.query_viewport(0, 1000, &marker_list)
.into_iter()
.map(|(r, _)| r)
.collect();
assert_eq!(kept, vec![0..5, 50..60]);
}
#[test]
fn test_conceal_remove_in_range_deletes_markers_and_bumps_version() {
let mut marker_list = MarkerList::new();
marker_list.set_buffer_size(100);
let mut manager = ConcealManager::new();
manager.add(&mut marker_list, ns(), 10..20, None);
let v0 = manager.version();
manager.remove_in_range(&(0..50), &mut marker_list);
assert!(manager.is_empty());
assert_ne!(manager.version(), v0);
}
#[test]
fn test_conceal_remove_in_range_no_match_keeps_version() {
let mut marker_list = MarkerList::new();
marker_list.set_buffer_size(100);
let mut manager = ConcealManager::new();
manager.add(&mut marker_list, ns(), 10..20, None);
let v0 = manager.version();
manager.remove_in_range(&(50..60), &mut marker_list);
assert!(!manager.is_empty());
assert_eq!(manager.version(), v0);
}
#[test]
fn test_conceal_remove_in_range_endpoint_semantics() {
let mut marker_list = MarkerList::new();
marker_list.set_buffer_size(100);
let mut manager = ConcealManager::new();
manager.add(&mut marker_list, ns(), 10..20, None);
manager.remove_in_range(&(20..30), &mut marker_list);
assert!(!manager.is_empty());
manager.remove_in_range(&(0..10), &mut marker_list);
assert!(!manager.is_empty());
manager.remove_in_range(&(19..21), &mut marker_list);
assert!(manager.is_empty());
}
fn ns_named(name: &str) -> OverlayNamespace {
OverlayNamespace::from_string(name.to_string())
}
#[test]
fn test_remove_in_range_for_namespace_only_touches_that_namespace() {
let mut marker_list = MarkerList::new();
marker_list.set_buffer_size(200);
let mut manager = ConcealManager::new();
manager.add(&mut marker_list, ns_named("md"), 10..20, None);
manager.add(&mut marker_list, ns_named("other"), 12..18, None);
manager.add(&mut marker_list, ns_named("md"), 100..110, None);
manager.remove_in_range_for_namespace(&ns_named("md"), &(0..50), &mut marker_list);
let kept: Vec<_> = manager
.query_viewport(0, 1000, &marker_list)
.into_iter()
.map(|(r, _)| r)
.collect();
assert_eq!(kept, vec![12..18, 100..110]);
}
#[test]
fn test_remove_in_range_for_namespace_version_and_endpoints() {
let mut marker_list = MarkerList::new();
marker_list.set_buffer_size(100);
let mut manager = ConcealManager::new();
manager.add(&mut marker_list, ns_named("md"), 10..20, None);
let v0 = manager.version();
manager.remove_in_range_for_namespace(&ns_named("md"), &(20..30), &mut marker_list);
assert_eq!(manager.version(), v0);
assert!(!manager.is_empty());
manager.remove_in_range_for_namespace(&ns_named("other"), &(0..50), &mut marker_list);
assert_eq!(manager.version(), v0);
assert!(!manager.is_empty());
manager.remove_in_range_for_namespace(&ns_named("md"), &(15..25), &mut marker_list);
assert!(manager.is_empty());
assert_ne!(manager.version(), v0);
}
#[test]
fn perf_full_buffer_rebuild_pass() {
const LINES: usize = 500;
const LINE_BYTES: usize = 50;
const CONCEALS_PER_LINE: usize = 5;
let mut marker_list = MarkerList::new();
marker_list.set_buffer_size(LINES * LINE_BYTES);
let mut manager = ConcealManager::new();
let conceal_byte = |line: usize, k: usize| -> usize {
line * LINE_BYTES + k * (LINE_BYTES / CONCEALS_PER_LINE)
};
for line in 0..LINES {
for k in 0..CONCEALS_PER_LINE {
let s = conceal_byte(line, k);
manager.add(&mut marker_list, ns(), s..(s + 2), None);
}
}
let initial = LINES * CONCEALS_PER_LINE;
let start = std::time::Instant::now();
for line in 0..LINES {
let line_range = (line * LINE_BYTES)..((line + 1) * LINE_BYTES);
manager.remove_in_range(&line_range, &mut marker_list);
for k in 0..CONCEALS_PER_LINE {
let s = conceal_byte(line, k);
manager.add(&mut marker_list, ns(), s..(s + 2), None);
}
}
let elapsed = start.elapsed();
eprintln!(
"[perf] conceal full-buffer rebuild ({LINES} lines, {} entries steady): \
{:?} total, {:?}/line",
initial,
elapsed,
elapsed / LINES as u32,
);
let still_present = manager
.query_viewport(0, LINES * LINE_BYTES, &marker_list)
.len();
assert_eq!(still_present, initial);
}
mod proptests {
use super::*;
use proptest::prelude::*;
#[derive(Debug, Clone)]
enum Op {
Add {
start: usize,
len: usize,
ns_idx: u8,
},
RemoveInRange {
start: usize,
end: usize,
},
ClearNamespace {
ns_idx: u8,
},
}
const BUFFER_SIZE: usize = 200;
const MAX_CONCEAL_LEN: usize = 4;
const MIN_QUERY_LEN: usize = MAX_CONCEAL_LEN + 1;
fn arb_op() -> impl Strategy<Value = Op> {
prop_oneof![
3 => (0..(BUFFER_SIZE - MAX_CONCEAL_LEN), 1..=MAX_CONCEAL_LEN, 0u8..3u8)
.prop_map(|(start, len, ns_idx)| Op::Add { start, len, ns_idx }),
2 => (0..BUFFER_SIZE, MIN_QUERY_LEN..=BUFFER_SIZE)
.prop_map(|(start, qlen)| {
let s = start.min(BUFFER_SIZE - 1);
let e = (s + qlen).min(BUFFER_SIZE);
Op::RemoveInRange { start: s, end: e }
}),
1 => (0u8..3u8).prop_map(|ns_idx| Op::ClearNamespace { ns_idx }),
]
}
fn nsf(idx: u8) -> OverlayNamespace {
OverlayNamespace::from_string(format!("ns{idx}"))
}
proptest! {
#[test]
fn prop_marker_index_consistent(ops in prop::collection::vec(arb_op(), 0..40)) {
let mut marker_list = MarkerList::new();
marker_list.set_buffer_size(BUFFER_SIZE);
let mut manager = ConcealManager::new();
for op in ops {
match op {
Op::Add { start, len, ns_idx } => {
manager.add(&mut marker_list, nsf(ns_idx), start..(start + len), None);
}
Op::RemoveInRange { start, end } => {
manager.remove_in_range(&(start..end), &mut marker_list);
for (rng, _) in manager.query_viewport(0, BUFFER_SIZE, &marker_list) {
let overlaps = rng.start < end && start < rng.end;
prop_assert!(
!overlaps,
"conceal {:?} survived remove_in_range({start}..{end})",
rng,
);
}
}
Op::ClearNamespace { ns_idx } => {
manager.clear_namespace(&nsf(ns_idx), &mut marker_list);
}
}
manager.check_invariants();
}
}
}
}
}