use std::collections::{BTreeMap, HashMap};
use crate::emacs_core::value::{Value, equal_value};
use crate::gc_trace::GcTrace;
#[derive(Clone, Debug)]
pub struct PropertyInterval {
pub start: usize,
pub end: usize,
pub properties: HashMap<String, Value>,
pub(crate) key_order: Vec<String>,
}
impl PropertyInterval {
fn new(start: usize, end: usize) -> Self {
Self {
start,
end,
properties: HashMap::new(),
key_order: Vec::new(),
}
}
pub fn with_properties(start: usize, end: usize, properties: HashMap<String, Value>) -> Self {
let key_order: Vec<String> = properties.keys().cloned().collect();
Self {
start,
end,
properties,
key_order,
}
}
fn insert_property(&mut self, name: &str, value: Value) -> bool {
let already_equal = self
.properties
.get(name)
.map_or(false, |existing| equal_value(existing, &value, 0));
if already_equal {
return false;
}
let is_new = !self.properties.contains_key(name);
self.properties.insert(name.to_string(), value);
if is_new {
self.key_order.insert(0, name.to_string());
}
true
}
fn remove_property(&mut self, name: &str) -> Option<Value> {
let result = self.properties.remove(name);
if result.is_some() {
self.key_order.retain(|k| k != name);
}
result
}
fn is_empty_props(&self) -> bool {
self.properties.is_empty()
}
pub fn ordered_properties(&self) -> impl Iterator<Item = (&str, &Value)> {
self.key_order
.iter()
.filter_map(move |k| self.properties.get(k).map(|v| (k.as_str(), v)))
}
}
fn props_equal(a: &HashMap<String, Value>, b: &HashMap<String, Value>) -> bool {
if a.len() != b.len() {
return false;
}
for (key, val_a) in a {
match b.get(key) {
Some(val_b) => {
if !equal_value(val_a, val_b, 0) {
return false;
}
}
None => return false,
}
}
true
}
#[derive(Clone)]
pub struct TextPropertyTable {
intervals: BTreeMap<usize, PropertyInterval>,
}
impl TextPropertyTable {
pub fn new() -> Self {
Self {
intervals: BTreeMap::new(),
}
}
pub fn put_property(&mut self, start: usize, end: usize, name: &str, value: Value) -> bool {
if start >= end {
return false;
}
self.split_at(start);
self.split_at(end);
self.ensure_coverage(start, end);
let mut changed = false;
let keys: Vec<usize> = self
.intervals
.range(start..end)
.map(|(&key, _)| key)
.collect();
for key in keys {
if let Some(interval) = self.intervals.get_mut(&key)
&& interval.insert_property(name, value)
{
changed = true;
}
}
self.merge_adjacent();
changed
}
pub fn get_property(&self, pos: usize, name: &str) -> Option<&Value> {
self.interval_containing(pos)
.and_then(|interval| interval.properties.get(name))
}
pub fn get_properties(&self, pos: usize) -> HashMap<String, Value> {
self.interval_containing(pos)
.map(|interval| interval.properties.clone())
.unwrap_or_default()
}
pub fn get_properties_ordered(&self, pos: usize) -> Vec<(String, Value)> {
self.interval_containing(pos)
.map(|interval| {
interval
.ordered_properties()
.map(|(k, v)| (k.to_string(), *v))
.collect()
})
.unwrap_or_default()
}
pub fn remove_property(&mut self, start: usize, end: usize, name: &str) -> bool {
if start >= end {
return false;
}
self.split_at(start);
self.split_at(end);
let mut removed = false;
let keys: Vec<usize> = self
.intervals
.range(start..end)
.map(|(&key, _)| key)
.collect();
for key in keys {
if let Some(interval) = self.intervals.get_mut(&key)
&& interval.remove_property(name).is_some()
{
removed = true;
}
}
self.cleanup();
self.merge_adjacent();
removed
}
pub fn remove_all_properties(&mut self, start: usize, end: usize) {
if start >= end {
return;
}
self.split_at(start);
self.split_at(end);
let keys: Vec<usize> = self
.intervals
.range(start..end)
.map(|(&key, _)| key)
.collect();
for key in keys {
if let Some(interval) = self.intervals.get_mut(&key) {
interval.properties.clear();
interval.key_order.clear();
}
}
self.cleanup();
self.merge_adjacent();
}
pub fn next_property_change(&self, pos: usize) -> Option<usize> {
if let Some(interval) = self.interval_containing(pos) {
return Some(interval.end);
}
self.intervals
.range((std::ops::Bound::Excluded(pos), std::ops::Bound::Unbounded))
.next()
.map(|(_, interval)| interval.start)
}
pub fn previous_property_change(&self, pos: usize) -> Option<usize> {
if let Some(interval) = self.interval_containing(pos.saturating_sub(1))
&& pos <= interval.end
&& interval.start < pos
{
return Some(interval.start);
}
self.intervals
.range(..pos)
.next_back()
.map(|(_, interval)| interval.end)
}
pub fn adjust_for_insert(&mut self, pos: usize, len: usize) {
if len == 0 {
return;
}
let mut shifted = BTreeMap::new();
for interval in self.intervals_snapshot() {
if interval.start == pos {
let mut shifted_interval = interval;
shifted_interval.start += len;
shifted_interval.end += len;
shifted.insert(shifted_interval.start, shifted_interval);
} else if interval.start > pos {
let mut shifted_interval = interval;
shifted_interval.start += len;
shifted_interval.end += len;
shifted.insert(shifted_interval.start, shifted_interval);
} else if interval.end > pos {
let mut left = interval.clone();
left.end = pos;
if left.start < left.end {
shifted.insert(left.start, left);
}
let mut right = interval;
right.start = pos + len;
right.end += len;
if right.start < right.end {
shifted.insert(right.start, right);
}
} else {
shifted.insert(interval.start, interval);
}
}
self.intervals = shifted;
}
pub fn adjust_for_delete(&mut self, start: usize, end: usize) {
if start >= end {
return;
}
let len = end - start;
let mut shifted = BTreeMap::new();
for mut interval in self.intervals_snapshot() {
if interval.start >= end {
interval.start -= len;
interval.end -= len;
} else if interval.end <= start {
} else if interval.start >= start && interval.end <= end {
continue;
} else if interval.start < start && interval.end > end {
interval.end -= len;
} else if interval.start < start {
interval.end = start;
} else {
interval.start = start;
interval.end -= len;
}
shifted.insert(interval.start, interval);
}
self.intervals = shifted;
self.merge_adjacent();
}
fn split_at(&mut self, pos: usize) {
let Some((&start, interval)) = self.intervals.range(..pos).next_back() else {
return;
};
if !(interval.start < pos && pos < interval.end) {
return;
}
let second = PropertyInterval {
start: pos,
end: interval.end,
properties: interval.properties.clone(),
key_order: interval.key_order.clone(),
};
if let Some(first) = self.intervals.get_mut(&start) {
first.end = pos;
}
self.intervals.insert(pos, second);
}
fn ensure_coverage(&mut self, start: usize, end: usize) {
let mut gaps = Vec::new();
let mut cursor = start;
for interval in self.intervals.values() {
if interval.start >= end {
break;
}
if interval.end <= cursor {
continue;
}
if interval.start > cursor {
gaps.push((cursor, interval.start));
}
if interval.end > cursor {
cursor = interval.end;
}
}
if cursor < end {
gaps.push((cursor, end));
}
for (gap_start, gap_end) in gaps {
self.intervals
.insert(gap_start, PropertyInterval::new(gap_start, gap_end));
}
}
fn cleanup(&mut self) {
self.intervals.retain(|_, iv| !iv.is_empty_props());
}
fn merge_adjacent(&mut self) {
if self.intervals.len() < 2 {
return;
}
let mut merged = BTreeMap::new();
let mut current: Option<PropertyInterval> = None;
for interval in self.intervals.values().cloned() {
match current.take() {
None => current = Some(interval),
Some(mut active) => {
if active.end == interval.start
&& props_equal(&active.properties, &interval.properties)
{
active.end = interval.end;
current = Some(active);
} else {
merged.insert(active.start, active);
current = Some(interval);
}
}
}
}
if let Some(interval) = current {
merged.insert(interval.start, interval);
}
self.intervals = merged;
}
fn interval_containing(&self, pos: usize) -> Option<&PropertyInterval> {
let (_, interval) = self.intervals.range(..=pos).next_back()?;
(interval.start <= pos && pos < interval.end).then_some(interval)
}
pub fn intervals_snapshot(&self) -> Vec<PropertyInterval> {
self.intervals.values().cloned().collect()
}
pub fn is_empty(&self) -> bool {
self.intervals.is_empty()
}
pub fn slice(&self, start: usize, end: usize) -> TextPropertyTable {
if start >= end {
return TextPropertyTable::new();
}
let mut result = BTreeMap::new();
for iv in self.intervals.values() {
if iv.end <= start || iv.start >= end {
continue;
}
let new_start = iv.start.max(start) - start;
let new_end = iv.end.min(end) - start;
if new_start < new_end && !iv.properties.is_empty() {
result.insert(
new_start,
PropertyInterval {
start: new_start,
end: new_end,
properties: iv.properties.clone(),
key_order: iv.key_order.clone(),
},
);
}
}
TextPropertyTable { intervals: result }
}
pub fn append_shifted(&mut self, other: &TextPropertyTable, byte_offset: usize) {
for iv in other.intervals.values() {
if iv.properties.is_empty() {
continue;
}
let start = iv.start + byte_offset;
self.intervals.insert(
start,
PropertyInterval {
start,
end: iv.end + byte_offset,
properties: iv.properties.clone(),
key_order: iv.key_order.clone(),
},
);
}
self.merge_adjacent();
}
pub(crate) fn dump_intervals(&self) -> Vec<PropertyInterval> {
self.intervals_snapshot()
}
pub(crate) fn from_dump(intervals: Vec<PropertyInterval>) -> Self {
Self {
intervals: intervals
.into_iter()
.map(|interval| (interval.start, interval))
.collect(),
}
}
}
impl Default for TextPropertyTable {
fn default() -> Self {
Self::new()
}
}
impl GcTrace for TextPropertyTable {
fn trace_roots(&self, roots: &mut Vec<Value>) {
for interval in self.intervals.values() {
for value in interval.properties.values() {
roots.push(*value);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn put_and_get_basic() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 5, "face", Value::symbol("bold"));
assert!(table.get_property(0, "face").is_some());
assert!(table.get_property(2, "face").is_some());
assert!(table.get_property(4, "face").is_some());
assert!(table.get_property(5, "face").is_none()); }
#[test]
fn get_property_returns_correct_value() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 10, "face", Value::symbol("bold"));
let val = table.get_property(5, "face").unwrap();
assert!(
val.as_symbol_id()
.map_or(false, |id| crate::emacs_core::intern::resolve_sym(id)
== "bold")
);
}
#[test]
fn get_property_nonexistent_name() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 10, "face", Value::symbol("bold"));
assert!(table.get_property(5, "syntax-table").is_none());
}
#[test]
fn get_properties_returns_all() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 10, "face", Value::symbol("bold"));
table.put_property(0, 10, "help-echo", Value::string("tooltip"));
let props = table.get_properties(5);
assert_eq!(props.len(), 2);
assert!(props.contains_key("face"));
assert!(props.contains_key("help-echo"));
}
#[test]
fn get_property_outside_any_interval() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(5, 10, "face", Value::symbol("bold"));
assert!(table.get_property(0, "face").is_none());
assert!(table.get_property(3, "face").is_none());
assert!(table.get_property(10, "face").is_none());
assert!(table.get_property(15, "face").is_none());
}
#[test]
fn overlapping_put_splits_intervals() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 10, "face", Value::symbol("bold"));
table.put_property(5, 15, "face", Value::symbol("italic"));
let val = table.get_property(3, "face").unwrap();
assert!(
val.as_symbol_id()
.map_or(false, |id| crate::emacs_core::intern::resolve_sym(id)
== "bold")
);
let val = table.get_property(7, "face").unwrap();
assert!(
val.as_symbol_id()
.map_or(false, |id| crate::emacs_core::intern::resolve_sym(id)
== "italic")
);
let val = table.get_property(12, "face").unwrap();
assert!(
val.as_symbol_id()
.map_or(false, |id| crate::emacs_core::intern::resolve_sym(id)
== "italic")
);
}
#[test]
fn multiple_properties_on_same_range() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 10, "face", Value::symbol("bold"));
table.put_property(0, 10, "mouse-face", Value::symbol("highlight"));
let props = table.get_properties(5);
assert_eq!(props.len(), 2);
}
#[test]
fn put_property_inner_range() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 20, "face", Value::symbol("default"));
table.put_property(5, 15, "face", Value::symbol("bold"));
let val = table.get_property(3, "face").unwrap();
assert!(
val.as_symbol_id()
.map_or(false, |id| crate::emacs_core::intern::resolve_sym(id)
== "default")
);
let val = table.get_property(10, "face").unwrap();
assert!(
val.as_symbol_id()
.map_or(false, |id| crate::emacs_core::intern::resolve_sym(id)
== "bold")
);
let val = table.get_property(17, "face").unwrap();
assert!(
val.as_symbol_id()
.map_or(false, |id| crate::emacs_core::intern::resolve_sym(id)
== "default")
);
}
#[test]
fn put_different_properties_on_overlapping_ranges() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 10, "face", Value::symbol("bold"));
table.put_property(5, 15, "syntax-table", Value::fixnum(42));
let props = table.get_properties(3);
assert_eq!(props.len(), 1);
assert!(props.contains_key("face"));
let props = table.get_properties(7);
assert_eq!(props.len(), 2);
let props = table.get_properties(12);
assert_eq!(props.len(), 1);
assert!(props.contains_key("syntax-table"));
}
#[test]
fn remove_property_basic() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 10, "face", Value::symbol("bold"));
table.put_property(0, 10, "help-echo", Value::string("help"));
table.remove_property(0, 10, "face");
assert!(table.get_property(5, "face").is_none());
assert!(table.get_property(5, "help-echo").is_some());
}
#[test]
fn remove_property_partial_range() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 10, "face", Value::symbol("bold"));
table.remove_property(3, 7, "face");
assert!(table.get_property(2, "face").is_some());
assert!(table.get_property(5, "face").is_none());
assert!(table.get_property(8, "face").is_some());
}
#[test]
fn remove_all_properties_basic() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 10, "face", Value::symbol("bold"));
table.put_property(0, 10, "help-echo", Value::string("help"));
table.remove_all_properties(0, 10);
assert!(table.get_property(5, "face").is_none());
assert!(table.get_property(5, "help-echo").is_none());
}
#[test]
fn remove_all_properties_partial() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 10, "face", Value::symbol("bold"));
table.remove_all_properties(3, 7);
assert!(table.get_property(2, "face").is_some());
assert!(table.get_property(5, "face").is_none());
assert!(table.get_property(8, "face").is_some());
}
#[test]
fn next_property_change_basic() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(5, 10, "face", Value::symbol("bold"));
table.put_property(15, 20, "face", Value::symbol("italic"));
assert_eq!(table.next_property_change(0), Some(5));
assert_eq!(table.next_property_change(7), Some(10));
assert_eq!(table.next_property_change(12), Some(15));
assert_eq!(table.next_property_change(17), Some(20));
assert_eq!(table.next_property_change(25), None);
}
#[test]
fn next_property_change_at_boundary() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(5, 10, "face", Value::symbol("bold"));
assert_eq!(table.next_property_change(5), Some(10));
}
#[test]
fn previous_property_change_basic() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(5, 10, "face", Value::symbol("bold"));
table.put_property(15, 20, "face", Value::symbol("italic"));
assert_eq!(table.previous_property_change(25), Some(20));
assert_eq!(table.previous_property_change(17), Some(15));
assert_eq!(table.previous_property_change(12), Some(10));
assert_eq!(table.previous_property_change(7), Some(5));
assert_eq!(table.previous_property_change(3), None);
assert_eq!(table.previous_property_change(0), None);
}
#[test]
fn previous_property_change_at_end() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(5, 10, "face", Value::symbol("bold"));
assert_eq!(table.previous_property_change(10), Some(10));
}
#[test]
fn next_previous_empty_table() {
crate::test_utils::init_test_tracing();
let table = TextPropertyTable::new();
assert_eq!(table.next_property_change(0), None);
assert_eq!(table.previous_property_change(10), None);
}
#[test]
fn adjust_insert_shifts_intervals_after() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(10, 20, "face", Value::symbol("bold"));
table.adjust_for_insert(5, 3);
assert!(table.get_property(12, "face").is_none());
assert!(table.get_property(13, "face").is_some());
assert!(table.get_property(22, "face").is_some());
assert!(table.get_property(23, "face").is_none());
}
#[test]
fn adjust_insert_splits_spanning_interval_around_plain_inserted_text() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(5, 15, "face", Value::symbol("bold"));
table.adjust_for_insert(10, 5);
assert!(table.get_property(5, "face").is_some());
assert!(table.get_property(9, "face").is_some());
assert!(table.get_property(10, "face").is_none());
assert!(table.get_property(12, "face").is_none());
assert!(table.get_property(14, "face").is_none());
assert!(table.get_property(15, "face").is_some());
assert!(table.get_property(20, "face").is_none());
}
#[test]
fn adjust_insert_at_interval_start() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(5, 10, "face", Value::symbol("bold"));
table.adjust_for_insert(5, 3);
assert!(table.get_property(7, "face").is_none());
assert!(table.get_property(8, "face").is_some());
assert!(table.get_property(12, "face").is_some());
assert!(table.get_property(13, "face").is_none());
}
#[test]
fn adjust_insert_before_all() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(5, 10, "face", Value::symbol("bold"));
table.adjust_for_insert(0, 2);
assert!(table.get_property(7, "face").is_some());
assert!(table.get_property(6, "face").is_none());
}
#[test]
fn adjust_insert_zero_length() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(5, 10, "face", Value::symbol("bold"));
table.adjust_for_insert(7, 0);
assert!(table.get_property(5, "face").is_some());
assert!(table.get_property(9, "face").is_some());
assert!(table.get_property(10, "face").is_none());
}
#[test]
fn adjust_delete_shifts_intervals_after() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(10, 20, "face", Value::symbol("bold"));
table.adjust_for_delete(2, 5);
assert!(table.get_property(6, "face").is_none());
assert!(table.get_property(7, "face").is_some());
assert!(table.get_property(16, "face").is_some());
assert!(table.get_property(17, "face").is_none());
}
#[test]
fn adjust_delete_removes_contained_interval() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(5, 10, "face", Value::symbol("bold"));
table.adjust_for_delete(3, 12);
assert!(table.get_property(5, "face").is_none());
assert!(table.get_property(3, "face").is_none());
}
#[test]
fn adjust_delete_truncates_start() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(5, 15, "face", Value::symbol("bold"));
table.adjust_for_delete(10, 20);
assert!(table.get_property(5, "face").is_some());
assert!(table.get_property(9, "face").is_some());
assert!(table.get_property(10, "face").is_none());
}
#[test]
fn adjust_delete_shrinks_spanning_interval() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(5, 20, "face", Value::symbol("bold"));
table.adjust_for_delete(10, 15);
assert!(table.get_property(5, "face").is_some());
assert!(table.get_property(14, "face").is_some());
assert!(table.get_property(15, "face").is_none());
}
#[test]
fn adjust_delete_overlaps_interval_start() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(5, 15, "face", Value::symbol("bold"));
table.adjust_for_delete(2, 10);
assert!(table.get_property(2, "face").is_some());
assert!(table.get_property(6, "face").is_some());
assert!(table.get_property(7, "face").is_none());
}
#[test]
fn adjust_delete_empty_range() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(5, 10, "face", Value::symbol("bold"));
table.adjust_for_delete(7, 7);
assert!(table.get_property(5, "face").is_some());
assert!(table.get_property(9, "face").is_some());
}
#[test]
fn merge_adjacent_same_properties() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 5, "face", Value::symbol("bold"));
table.put_property(5, 10, "face", Value::symbol("bold"));
assert!(table.get_property(0, "face").is_some());
assert!(table.get_property(7, "face").is_some());
assert_eq!(table.next_property_change(0), Some(10));
}
#[test]
fn no_merge_different_properties() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 5, "face", Value::symbol("bold"));
table.put_property(5, 10, "face", Value::symbol("italic"));
assert_eq!(table.next_property_change(0), Some(5));
assert_eq!(table.next_property_change(5), Some(10));
}
#[test]
fn put_property_empty_range() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(5, 5, "face", Value::symbol("bold"));
assert!(table.get_property(5, "face").is_none());
}
#[test]
fn put_property_overwrites_same_name() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 10, "face", Value::symbol("bold"));
table.put_property(0, 10, "face", Value::symbol("italic"));
let val = table.get_property(5, "face").unwrap();
assert!(
val.as_symbol_id()
.map_or(false, |id| crate::emacs_core::intern::resolve_sym(id)
== "italic")
);
}
#[test]
fn multiple_non_contiguous_intervals() {
crate::test_utils::init_test_tracing();
let mut table = TextPropertyTable::new();
table.put_property(0, 5, "face", Value::symbol("bold"));
table.put_property(10, 15, "face", Value::symbol("italic"));
table.put_property(20, 25, "face", Value::symbol("underline"));
assert!(table.get_property(3, "face").is_some());
assert!(table.get_property(7, "face").is_none());
assert!(table.get_property(12, "face").is_some());
assert!(table.get_property(17, "face").is_none());
assert!(table.get_property(22, "face").is_some());
}
}