Skip to content

Commit 2fbc7e7

Browse files
committed
fix(textprop): remove text properties in place so aliased snapshots update
`text-properties-at` returns the live interval plist, so a captured value aliases its cons cells (matching GNU). But `plist_value_remove` rebuilt the plist from scratch via `plist_value_from_pairs`, repointing `node.plist` to fresh cells and leaving the captured snapshot stale. GNU's `remove_properties` (src/textprop.c) mutates the existing chain in place: head matches advance the plist pointer without touching the old head cell, body matches are spliced out with setcdr. Rewrite `plist_value_remove` to do the same (mirrors the existing in-place `plist_value_put_replace`); allocates nothing. Fixes div_v7_text_property_remove_overlapping: a value captured before remove-text-properties / remove-list-of-text-properties now reflects the shared-structure mutation, as in GNU.
1 parent bd8d938 commit 2fbc7e7

1 file changed

Lines changed: 47 additions & 7 deletions

File tree

neovm-core/src/buffer/text_props.rs

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -286,15 +286,55 @@ fn plist_value_put_replace(plist: &mut Value, key: Value, value: Value) -> bool
286286
true
287287
}
288288

289+
/// Remove KEY from a property list IN PLACE, mirroring GNU `remove_properties`
290+
/// (src/textprop.c). `text-properties-at` returns the live interval plist, so a
291+
/// captured snapshot aliases these cons cells; rebuilding the plist (the old
292+
/// behavior) left that snapshot stale. Head matches advance the plist pointer
293+
/// without mutating the old head cell; body matches are spliced out via
294+
/// `set_cdr`. Allocates nothing.
289295
fn plist_value_remove(plist: &mut Value, key: Value) -> bool {
290-
let mut pairs = plist_pairs(*plist);
291-
let before = pairs.len();
292-
pairs.retain(|(name, _)| !eq_value(name, &key));
293-
if pairs.len() == before {
294-
return false;
296+
let mut changed = false;
297+
298+
// Phase 1: strip matching pairs at the head (GNU advances
299+
// current_plist = XCDR(XCDR(current_plist)) without mutating the old head).
300+
let mut current = *plist;
301+
while current.is_cons() {
302+
let rest = current.cons_cdr();
303+
if !rest.is_cons() {
304+
break;
305+
}
306+
if eq_value(&current.cons_car(), &key) {
307+
current = rest.cons_cdr();
308+
changed = true;
309+
} else {
310+
break;
311+
}
295312
}
296-
*plist = plist_value_from_pairs(&pairs);
297-
true
313+
314+
// Phase 2: splice out body matches in place
315+
// (GNU `Fsetcdr (XCDR (tail2), XCDR (XCDR (this)))`).
316+
let mut tail = current;
317+
while tail.is_cons() {
318+
let val_cell = tail.cons_cdr(); // XCDR(tail2)
319+
if !val_cell.is_cons() {
320+
break;
321+
}
322+
let this = val_cell.cons_cdr(); // next key cell
323+
if this.is_cons() && eq_value(&this.cons_car(), &key) {
324+
let this_val = this.cons_cdr();
325+
if this_val.is_cons() {
326+
val_cell.set_cdr(this_val.cons_cdr());
327+
changed = true;
328+
continue; // stay on the same tail; another match may follow
329+
}
330+
}
331+
tail = this;
332+
}
333+
334+
if changed {
335+
*plist = current;
336+
}
337+
changed
298338
}
299339

300340
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)