rune/
textprops.rs

1use crate::{
2    core::{
3        cons::Cons,
4        env::Env,
5        error::{Type, TypeError},
6        gc::{Context, Rt, Slot},
7        object::{Gc, ListType, NIL, Object, ObjectType, WithLifetime},
8    },
9    fns::eq,
10    intervals::textget,
11};
12use anyhow::{Result, anyhow, bail};
13use rune_core::macros::list;
14use rune_macros::defun;
15
16use crate::core::object::BufferData;
17
18#[allow(dead_code)]
19#[derive(Clone, Copy, PartialEq, Eq, Debug)]
20pub enum PropertySetType {
21    Replace,
22    Prepend,
23    Append,
24}
25
26/// Add the properties of PLIST to the interval I, or set
27/// the value of I's property to the value of the property on PLIST
28/// if they are different.
29///
30/// OBJECT should be the string or buffer the interval is in.
31///
32/// If DESTRUCTIVE, the function is allowed to reuse list values in the
33/// properties.
34///
35/// Return true if this changes I (i.e., if any members of PLIST
36/// are actually added to I's plist)
37pub fn add_properties<'ob>(
38    plist: Object<'ob>,
39    mut obj_i: Object<'ob>,
40    _set_type: PropertySetType,
41    _destructive: bool,
42    cx: &'ob Context,
43) -> Result<Object<'ob>> {
44    // TODO return type
45    let Ok(plist) = Gc::<ListType>::try_from(plist) else { return Ok(obj_i) };
46    let Ok(plist_i) = Gc::<ListType>::try_from(obj_i) else { return Ok(obj_i) };
47    let mut iter = plist.elements();
48    // iterate through plist, finding key1 and val1
49    while let Some(key1) = iter.next() {
50        let key1 = key1?;
51        let Some(val1) = iter.next() else { return Ok(obj_i) };
52        let mut found = false;
53
54        let mut iter_i = plist_i.conses();
55        // iterate through i's plist, finding (key2, val2) and set val2 if key2 == key1;
56        while let Some(key2_cons) = iter_i.next() {
57            let Some(val2_cons) = iter_i.next() else { return Ok(obj_i) };
58            if eq(key1, key2_cons?.car()) {
59                // TODO this should depend on set_type
60                val2_cons?.set_car(val1?)?;
61                found = true;
62                break;
63            }
64        }
65        // if no key1 found, append them
66        if !found {
67            let pl = plist_i.untag();
68            let new_cons = Cons::new(key1, Cons::new(val1?, pl, cx), cx);
69            obj_i = new_cons.into();
70        }
71    }
72    Ok(obj_i)
73}
74
75/// Helper function to safely modify buffer data.
76///
77/// Takes an object (buffer or nil) and environment, and applies the given function
78/// to the buffer's data. Handles both current buffer and other buffers safely.
79///
80/// # Arguments
81/// * `object` - The buffer object to modify (or nil for current buffer)
82/// * `env` - The environment containing buffer state
83/// * `func` - Function to apply to the buffer's data
84///
85/// # Returns
86/// Result containing the return value from func or an error
87fn modify_buffer_data<'ob, T>(
88    object: Object<'ob>,
89    env: &'ob mut Rt<Env>,
90    func: impl FnOnce(&mut BufferData) -> Result<T>,
91) -> Result<T> {
92    if object.is_nil() {
93        let data = env.current_buffer.get_mut();
94        func(data)
95    } else {
96        let current_buf = env.current_buffer.buf_ref;
97        if let Some(b) = object.buffer() {
98            if b == current_buf {
99                let data = env.current_buffer.get_mut();
100                func(data)
101            } else {
102                let mut open_buf = b.lock()?;
103                func(open_buf.get_mut())
104            }
105        } else {
106            Err(anyhow!(TypeError::new(Type::BufferOrString, object.untag())))
107        }
108    }
109}
110
111/// Return the list of properties of the character at POSITION in OBJECT.
112/// If the optional second argument OBJECT is a buffer (or nil, which means
113/// the current buffer), POSITION is a buffer position (integer or marker).
114///
115/// If OBJECT is a string, POSITION is a 0-based index into it.
116///
117/// If POSITION is at the end of OBJECT, the value is nil, but note that
118/// buffer narrowing does not affect the value.  That is, if OBJECT is a
119/// buffer or nil, and the buffer is narrowed and POSITION is at the end
120/// of the narrowed buffer, the result may be non-nil.
121///
122/// If you want to display the text properties at point in a human-readable
123/// form, use the `describe-text-properties' command.
124#[defun]
125pub fn text_properties_at<'ob>(
126    position: usize,
127    object: Object<'ob>,
128    env: &'ob mut Rt<Env>,
129) -> Result<Object<'ob>> {
130    let tree = if eq(object, NIL) {
131        &env.current_buffer.get().textprops
132    } else {
133        let obj = object.untag();
134        match obj {
135            ObjectType::Buffer(buf) => &buf.lock().unwrap().textprops,
136            ObjectType::String(_str) => {
137                todo!()
138            }
139            _ => {
140                bail!(TypeError::new(Type::BufferOrString, obj))
141            }
142        }
143    };
144    let a = tree.find(position).map(|a| *a.val).unwrap_or(NIL);
145    Ok(unsafe { a.with_lifetime() })
146}
147
148/// Return the value of POSITION's property PROP, in OBJECT.
149/// OBJECT should be a buffer or a string; if omitted or nil, it defaults
150/// to the current buffer.
151///
152/// If POSITION is at the end of OBJECT, the value is nil, but note that
153/// buffer narrowing does not affect the value.  That is, if the buffer is
154/// narrowed and POSITION is at the end of the narrowed buffer, the result
155/// may be non-nil.
156#[defun]
157pub fn get_text_property<'ob>(
158    position: usize,
159    prop: Object<'ob>,
160    object: Object<'ob>,
161    env: &'ob mut Rt<Env>,
162) -> Result<Object<'ob>> {
163    let props = text_properties_at(position, object, env)?;
164    // TODO see lookup_char_property, should also lookup
165    // 1. category
166    // 2. char_property_alias_alist
167    // 3.  default_text_properties
168    textget(props, prop)
169}
170
171#[defun]
172#[allow(unused)]
173pub fn get_char_property_and_overlay<'ob>(
174    position: usize,
175    prop: Object<'ob>,
176    object: Object<'ob>,
177    env: &'ob mut Rt<Env>,
178) -> Result<Object<'ob>> {
179    todo!()
180}
181
182#[defun]
183#[allow(unused)]
184pub fn get_char_property<'ob>(
185    position: usize,
186    prop: Object<'ob>,
187    object: Object<'ob>,
188    env: &'ob mut Rt<Env>,
189) -> Result<Object<'ob>> {
190    todo!()
191}
192
193// TODO also missing `next-char-property-change` and 3 other similar functions.
194
195/// Set one property of the text from START to END.
196/// The third and fourth arguments PROPERTY and VALUE
197/// specify the property to add.
198/// If the optional fifth argument OBJECT is a buffer (or nil, which means
199/// the current buffer), START and END are buffer positions (integers or
200/// markers).  If OBJECT is a string, START and END are 0-based indices into it.
201#[defun]
202pub fn put_text_property<'ob>(
203    start: usize,
204    end: usize,
205    property: Object<'ob>,
206    value: Object<'ob>,
207    object: Object<'ob>,
208    env: &mut Rt<Env>,
209    cx: &'ob Context,
210) -> Result<()> {
211    let prop = list!(property, value; cx);
212    let prop = Slot::new(prop);
213    modify_buffer_data(object, env, |data| {
214        let tree = &mut data.textprops_with_lifetime();
215        tree.insert(start, end, prop, cx);
216        Ok(())
217    })
218}
219/// Return the position of next property change.
220/// Scans characters forward from POSITION in OBJECT till it finds
221/// a change in some text property, then returns the position of the change.
222/// If the optional second argument OBJECT is a buffer (or nil, which means
223/// the current buffer), POSITION is a buffer position (integer or marker).
224/// If OBJECT is a string, POSITION is a 0-based index into it.
225/// Return nil if LIMIT is nil or omitted, and the property is constant all
226/// the way to the end of OBJECT; if the value is non-nil, it is a position
227/// greater than POSITION, never equal.
228///
229/// If the optional third argument LIMIT is non-nil, don't search
230/// past position LIMIT; return LIMIT if nothing is found before LIMIT.
231#[defun]
232pub fn next_property_change<'ob>(
233    position: usize,
234    object: Object<'ob>,
235    limit: Option<usize>,
236    env: &'ob mut Rt<Env>,
237    cx: &'ob Context,
238) -> Result<Object<'ob>> {
239    modify_buffer_data(object, env, |data| -> Result<Object<'ob>> {
240        let point_max = data.text.len_chars() + 1;
241        let end = limit.unwrap_or(point_max);
242        let tree = data.textprops_with_lifetime();
243        // NOTE this can be optimized
244        tree.clean();
245        let prop = tree.tree.find_intersect_min(position..end);
246
247        match prop {
248            Some(p) => {
249                let range = p.key;
250                if range.start <= position {
251                    // should have range.end > position
252                    if range.end < end { Ok(cx.add(range.end)) } else { Ok(cx.add(limit)) }
253                } else {
254                    // empty property before interval, so prop changed just at range start
255                    Ok(cx.add(range.start))
256                }
257
258                // range is fully inside region, prop changes at end
259            }
260            None => {
261                let result = match limit {
262                    Some(n) => cx.add(n),
263                    None => NIL,
264                };
265                Ok(result)
266            }
267        }
268    })
269}
270
271/// Return the position of next property change for a specific property.
272/// Scans characters forward from POSITION till it finds
273/// a change in the PROP property, then returns the position of the change.
274/// If the optional third argument OBJECT is a buffer (or nil, which means
275/// the current buffer), POSITION is a buffer position (integer or marker).
276/// If OBJECT is a string, POSITION is a 0-based index into it.
277/// The property values are compared with `eq'.
278/// Return nil if LIMIT is nil or omitted, and the property is constant all
279/// the way to the end of OBJECT; if the value is non-nil, it is a position
280/// greater than POSITION, never equal.
281///
282/// If the optional fourth argument LIMIT is non-nil, don't search
283/// past position LIMIT; return LIMIT if nothing is found before LIMIT.
284#[defun]
285pub fn next_single_property_change<'ob>(
286    position: usize,
287    prop: Object<'ob>,
288    object: Object<'ob>,
289    limit: Option<usize>,
290    env: &'ob mut Rt<Env>,
291    cx: &'ob Context,
292) -> Result<Object<'ob>> {
293    modify_buffer_data(object, env, |data| -> Result<Object<'ob>> {
294        let point_max = data.text.len_chars() + 1;
295        let end = limit.unwrap_or(point_max);
296        let tree = data.textprops_with_lifetime();
297        // NOTE this can be optimized
298        tree.clean();
299        let iter = tree.iter(position, end);
300
301        let mut val = None;
302        for (interval, props) in iter {
303            let text_prop = textget(props, prop)?;
304            match val {
305                Some(v) => {
306                    if eq(v, text_prop) {
307                        continue;
308                    } else {
309                        return Ok(cx.add(interval.start));
310                    }
311                }
312                None => val = Some(text_prop),
313            }
314        }
315        Ok(cx.add(limit))
316    })
317}
318
319/// Return the position of previous property change.
320/// Scans characters backwards from POSITION in OBJECT till it finds
321/// a change in some text property, then returns the position of the change.
322/// If the optional second argument OBJECT is a buffer (or nil, which means
323/// the current buffer), POSITION is a buffer position (integer or marker).
324/// If OBJECT is a string, POSITION is a 0-based index into it.
325/// Return nil if LIMIT is nil or omitted, and the property is constant all
326/// the way to the start of OBJECT; if the value is non-nil, it is a position
327/// less than POSITION, never equal.
328///
329/// If the optional third argument LIMIT is non-nil, don't search
330/// back past position LIMIT; return LIMIT if nothing is found until LIMIT.
331#[defun]
332pub fn previous_property_change<'ob>(
333    position: usize,
334    object: Object<'ob>,
335    limit: Option<usize>,
336    env: &'ob mut Rt<Env>,
337    cx: &'ob Context,
338) -> Result<Object<'ob>> {
339    modify_buffer_data(object, env, |data| -> Result<Object<'ob>> {
340        let point_min = 1;
341        let start = limit.unwrap_or(point_min);
342        let end = position;
343        let tree = data.textprops_with_lifetime();
344        // NOTE this can be optimized
345        tree.clean();
346        let prop = tree.tree.find_intersect_max(start..end);
347
348        match prop {
349            Some(p) => {
350                let interval = p.key;
351                if interval.end >= position {
352                    // should have range.start < position
353                    if interval.start > start {
354                        Ok(cx.add(interval.start))
355                    } else {
356                        Ok(cx.add(limit))
357                    }
358                } else {
359                    // empty property after interval, so prop changed just at its end
360                    Ok(cx.add(interval.end))
361                }
362
363                // range is fully inside region, prop changes at end
364            }
365            None => {
366                let result = match limit {
367                    Some(n) => cx.add(n),
368                    None => NIL,
369                };
370                Ok(result)
371            }
372        }
373    })
374}
375
376/// Return the position of next property change for a specific property.
377/// Scans characters forward from POSITION till it finds
378/// a change in the PROP property, then returns the position of the change.
379/// If the optional third argument OBJECT is a buffer (or nil, which means
380/// the current buffer), POSITION is a buffer position (integer or marker).
381/// If OBJECT is a string, POSITION is a 0-based index into it.
382/// The property values are compared with `eq'.
383/// Return nil if LIMIT is nil or omitted, and the property is constant all
384/// the way to the end of OBJECT; if the value is non-nil, it is a position
385/// greater than POSITION, never equal.
386///
387/// If the optional fourth argument LIMIT is non-nil, don't search
388/// past position LIMIT; return LIMIT if nothing is found before LIMIT.
389#[defun]
390pub fn previous_single_property_change<'ob>(
391    position: usize,
392    prop: Object<'ob>,
393    object: Object<'ob>,
394    limit: Option<usize>,
395    env: &'ob mut Rt<Env>,
396    cx: &'ob Context,
397) -> Result<Object<'ob>> {
398    modify_buffer_data(object, env, |data| -> Result<Object<'ob>> {
399        let point_min = 1;
400        let start = limit.unwrap_or(point_min);
401        let tree = data.textprops_with_lifetime();
402        // NOTE this can be optimized
403        tree.clean();
404        let iter = tree.iter_reverse(start, position);
405
406        let mut val = None;
407        for (interval, props) in iter {
408            let text_prop = textget(props, prop)?;
409            match val {
410                Some(v) => {
411                    if eq(v, text_prop) {
412                        continue;
413                    } else {
414                        return Ok(cx.add(interval.start));
415                    }
416                }
417                None => val = Some(text_prop),
418            }
419        }
420        Ok(cx.add(limit))
421    })
422}
423
424/// Completely replace properties of text from START to END.
425/// The third argument PROPERTIES is the new property list.
426/// If the optional fourth argument OBJECT is a buffer (or nil, which means
427/// the current buffer), START and END are buffer positions (integers or
428/// markers).  If OBJECT is a string, START and END are 0-based indices into it.
429/// If PROPERTIES is nil, the effect is to remove all properties from
430/// the designated part of OBJECT.
431#[defun]
432pub fn set_text_properties<'ob>(
433    start: usize,
434    end: usize,
435    properties: Object<'ob>,
436    object: Object<'ob>,
437    env: &mut Rt<Env>,
438) -> Result<()> {
439    modify_buffer_data(object, env, |data| -> Result<()> {
440        let tree = data.textprops_with_lifetime();
441        tree.set_properties(start, end, properties);
442        Ok(())
443    })
444}
445
446/// Remove some properties from text from START to END.
447/// The third argument PROPERTIES is a property list
448/// whose property names specify the properties to remove.
449/// \(The values stored in PROPERTIES are ignored.)
450/// If the optional fourth argument OBJECT is a buffer (or nil, which means
451/// the current buffer), START and END are buffer positions (integers or
452/// markers).  If OBJECT is a string, START and END are 0-based indices into it.
453/// Return t if any property was actually removed, nil otherwise.
454///
455/// Use `set-text-properties' if you want to remove all text properties.
456#[defun]
457pub fn remove_text_properties<'ob>(
458    start: usize,
459    end: usize,
460    properties: Object<'ob>,
461    object: Object<'ob>,
462    env: &mut Rt<Env>,
463    cx: &'ob Context,
464) -> Result<()> {
465    modify_buffer_data(object, env, |data| -> Result<()> {
466        let tree = data.textprops_with_lifetime();
467        tree.delete(start, end, list![properties; cx])
468    })
469}
470
471/// /* Remove some properties from text from START to END.
472/// The third argument LIST-OF-PROPERTIES is a list of property names to remove.
473/// If the optional fourth argument OBJECT is a buffer (or nil, which means
474/// the current buffer), START and END are buffer positions (integers or
475/// markers).  If OBJECT is a string, START and END are 0-based indices into it.
476/// Return t if any property was actually removed, nil otherwise.
477#[defun]
478pub fn remove_list_of_text_properties<'ob>(
479    start: usize,
480    end: usize,
481    list_of_properties: Object<'ob>,
482    object: Object<'ob>,
483    env: &mut Rt<Env>,
484) -> Result<()> {
485    modify_buffer_data(object, env, |data| -> Result<()> {
486        let tree = data.textprops_with_lifetime();
487        tree.delete(start, end, list_of_properties)
488    })
489}
490
491/// Check text from START to END for property PROPERTY equaling VALUE.
492/// If so, return the position of the first character whose property PROPERTY
493/// is `eq' to VALUE.  Otherwise return nil.
494/// If the optional fifth argument OBJECT is a buffer (or nil, which means
495/// the current buffer), START and END are buffer positions (integers or
496/// markers).  If OBJECT is a string, START and END are 0-based indices into it.
497#[defun]
498pub fn text_properties_any<'ob>(
499    start: usize,
500    end: usize,
501    property: Object<'ob>,
502    value: Object<'ob>,
503    object: Object<'ob>,
504    env: &mut Rt<Env>,
505    cx: &'ob Context,
506) -> Result<Object<'ob>> {
507    modify_buffer_data(object, env, |data| -> Result<Object<'ob>> {
508        let tree = data.textprops_with_lifetime();
509        let iter = tree.iter(start, end);
510        for (interval, props) in iter {
511            let val = textget(props, property)?;
512            if !eq(val, value) {
513                return Ok(cx.add(interval.start));
514            }
515        }
516        Ok(NIL)
517    })
518}
519
520/// Check text from START to END for property PROPERTY not equaling VALUE.
521/// If so, return the position of the first character whose property PROPERTY
522/// is not `eq' to VALUE.  Otherwise, return nil.
523/// If the optional fifth argument OBJECT is a buffer (or nil, which means
524/// the current buffer), START and END are buffer positions (integers or
525/// markers).  If OBJECT is a string, START and END are 0-based indices into it.
526#[defun]
527pub fn text_properties_not_all<'ob>(
528    start: usize,
529    end: usize,
530    property: Object<'ob>,
531    value: Object<'ob>,
532    object: Object<'ob>,
533    env: &mut Rt<Env>,
534    cx: &'ob Context,
535) -> Result<Object<'ob>> {
536    modify_buffer_data(object, env, |data| -> Result<Object<'ob>> {
537        let tree = data.textprops_with_lifetime();
538        let iter = tree.iter(start, end);
539        for (interval, props) in iter {
540            let val = textget(props, property)?;
541            if eq(val, value) {
542                return Ok(cx.add(interval.start));
543            }
544        }
545        Ok(NIL)
546    })
547}
548
549#[cfg(test)]
550mod tests {
551    use crate::{
552        buffer::{BUFFERS, get_buffer_create},
553        core::{
554            env::intern,
555            gc::{Context, RootSet},
556        },
557        fns::plist_get,
558    };
559    use rune_core::macros::{list, root};
560
561    use super::*;
562
563    #[test]
564    fn test_add_properties() {
565        let roots = &RootSet::default();
566        let mut context = Context::new(roots);
567        let cx = &mut context;
568        let plist_1 = list![intern(":a", cx), 1, intern(":b", cx), 2; cx];
569        let plist_2 = list![intern(":a", cx), 4, intern(":c", cx), 5; cx];
570        let plist_1 =
571            add_properties(plist_2, plist_1, PropertySetType::Replace, false, cx).unwrap();
572        let plist_1 = dbg!(plist_1);
573        let a = plist_get(plist_1, intern(":a", cx).into()).unwrap();
574        let b = plist_get(plist_1, intern(":b", cx).into()).unwrap();
575        let c = plist_get(plist_1, intern(":c", cx).into()).unwrap();
576        assert_eq!(a, 4);
577        assert_eq!(b, 2);
578        assert_eq!(c, 5);
579    }
580
581    #[test]
582    fn test_next_property_change() -> Result<()> {
583        let roots = &RootSet::default();
584        let mut context = Context::new(roots);
585        let cx = &mut context;
586        root!(env, new(Env), cx);
587
588        let buf = get_buffer_create(cx.add("test_next_property_change"), None, cx)?;
589        if let Some(b) = buf.buffer() {
590            b.lock()
591                .unwrap()
592                .get_mut()
593                .text
594                .insert("lorem ipsum quia dolor sit amet, consectetur, adipisci velit.");
595        }
596
597        let a = intern(":a", cx);
598        let a = cx.add(a);
599
600        // Add properties at different ranges
601        put_text_property(0, 5, a, cx.add(1), buf, env, cx)?;
602        put_text_property(5, 10, a, cx.add(2), buf, env, cx)?;
603        put_text_property(15, 20, a, cx.add(3), buf, env, cx)?;
604
605        // Test property changes
606        let change = next_property_change(0, buf, None, env, cx)?;
607        assert_eq!(change, cx.add(5)); // Change at end of first range
608
609        let change = next_property_change(5, buf, None, env, cx)?;
610        assert_eq!(change, cx.add(10)); // Change at end of second range
611
612        let change = next_property_change(10, buf, None, env, cx)?;
613        assert_eq!(change, cx.add(15)); // Change at start of third range
614
615        let change = next_property_change(15, buf, None, env, cx)?;
616        assert_eq!(change, cx.add(20)); // Change at end of third range
617
618        // Test with limit
619        let change = next_property_change(0, buf, Some(8), env, cx)?;
620        assert_eq!(change, cx.add(5)); // Change within limit
621
622        let change = next_property_change(5, buf, Some(8), env, cx)?;
623        assert_eq!(change, cx.add(8)); // Limit reached
624
625        // Test no change case
626        let change = next_property_change(20, buf, None, env, cx)?;
627        assert!(change.is_nil()); // No changes after last property
628
629        BUFFERS.lock().unwrap().clear();
630        Ok(())
631    }
632
633    #[test]
634    fn test_next_single_property_change() -> Result<()> {
635        let roots = &RootSet::default();
636        let mut context = Context::new(roots);
637        let cx = &mut context;
638        root!(env, new(Env), cx);
639
640        let buf = get_buffer_create(cx.add("test_next_single_property_change"), None, cx)?;
641        if let ObjectType::Buffer(b) = buf.untag() {
642            b.lock()
643                .unwrap()
644                .get_mut()
645                .text
646                .insert("lorem ipsum quia dolor sit amet, consectetur, adipisci velit.");
647        }
648
649        let a = intern(":a", cx);
650        let a = cx.add(a);
651        let b = intern(":b", cx);
652        let b = cx.add(b);
653
654        // Add properties at different ranges
655        put_text_property(1, 5, a, cx.add(1), buf, env, cx)?;
656        put_text_property(5, 8, a, cx.add(2), buf, env, cx)?;
657        put_text_property(10, 15, b, cx.add(4), buf, env, cx)?;
658        put_text_property(15, 20, a, cx.add(3), buf, env, cx)?;
659
660        // Test property changes for :a
661        let change = next_single_property_change(1, a, buf, None, env, cx)?;
662        assert_eq!(change, cx.add(5)); // Change at end of first range
663
664        let change = next_single_property_change(5, a, buf, None, env, cx)?;
665        assert_eq!(change, cx.add(8)); // Change at end of second range
666
667        let change = next_single_property_change(10, a, buf, None, env, cx)?;
668        assert_eq!(change, cx.add(15)); // Change at start of third range
669
670        let change = next_single_property_change(15, a, buf, None, env, cx)?;
671        assert_eq!(change, cx.add(20)); // Change at end of third range
672
673        // Test with limit
674        let change = next_single_property_change(1, a, buf, Some(8), env, cx)?;
675        assert_eq!(change, cx.add(5)); // Change within limit
676
677        let change = next_single_property_change(5, a, buf, Some(8), env, cx)?;
678        assert_eq!(change, cx.add(8)); // Limit reached
679
680        // Test no change case
681        let change = next_single_property_change(20, a, buf, None, env, cx)?;
682        assert!(change.is_nil()); // No changes after last property
683
684        // Test property changes for :b
685        let change = next_single_property_change(1, b, buf, None, env, cx)?;
686        assert_eq!(change, cx.add(10)); // Change at start of b range
687
688        let change = next_single_property_change(10, b, buf, None, env, cx)?;
689        assert_eq!(change, cx.add(15)); // Change at end of b range
690
691        BUFFERS.lock().unwrap().clear();
692        Ok(())
693    }
694
695    #[test]
696    fn test_remove_text_properties() -> Result<()> {
697        let roots = &RootSet::default();
698        let mut context = Context::new(roots);
699        let cx = &mut context;
700        root!(env, new(Env), cx);
701
702        let buf = get_buffer_create(cx.add("test_remove_text_properties"), None, cx)?;
703        if let ObjectType::Buffer(b) = buf.untag() {
704            b.lock().unwrap().get_mut().text.insert("test text");
705        }
706
707        let a = intern(":a", cx);
708        let a = cx.add(a);
709        let b = intern(":b", cx);
710        let b = cx.add(b);
711
712        // Add properties
713        put_text_property(1, 5, a, cx.add(1), buf, env, cx)?;
714        put_text_property(5, 10, b, cx.add(2), buf, env, cx)?;
715
716        // Remove property :a
717        remove_text_properties(1, 10, a, buf, env, cx)?;
718
719        // Verify :a was removed
720        let props = text_properties_at(3, buf, env)?;
721        assert!(props.is_nil());
722
723        // Verify :b remains
724        let props = text_properties_at(5, buf, env)?;
725        let val = plist_get(props, b)?;
726        assert_eq!(val, cx.add(2));
727
728        // Try removing non-existent property
729        remove_text_properties(0, 10, a, buf, env, cx)?;
730
731        BUFFERS.lock().unwrap().clear();
732        Ok(())
733    }
734
735    #[test]
736    fn test_text_properties_at() -> Result<()> {
737        let roots = &RootSet::default();
738        let mut context = Context::new(roots);
739        let cx = &mut context;
740        root!(env, new(Env), cx);
741
742        let buf = get_buffer_create(cx.add("test_text_properties_at"), None, cx)?;
743        let n = text_properties_at(0, buf, env)?;
744        assert!(n.is_nil());
745
746        let a = intern(":a", cx);
747        let a = cx.add(a);
748        put_text_property(0, 1, a, cx.add(3), buf, env, cx)?;
749        let n = text_properties_at(0, buf, env)?;
750        let val = plist_get(n, a)?;
751        assert!(eq(val, cx.add(3)));
752
753        BUFFERS.lock().unwrap().clear();
754        Ok(())
755    }
756}