buntrail-core 0.2.0

Revisioned documents and shared editor contracts.
Documentation
use buntrail_core::{
    Document,
    contracts::*,
    search::{find::*, replace},
};
fn options() -> FindOptions {
    FindOptions {
        expression: "(café)".into(),
        mode: SearchMode::Regex,
        case_sensitive: false,
        whole_word: true,
        scope: None,
    }
}
#[test]
fn test_replace_all_is_one_undo_group() {
    let mut d = Document::new("😀 CAFÉ café cafeteria\n".into());
    let tx = replace::transaction(
        &d.snapshot().text,
        BufferRevision(0),
        ViewID(1),
        &options(),
        "${1}🐇",
    )
    .unwrap();
    assert_eq!(tx.replacements.len(), 2);
    d.apply_transaction(tx).unwrap();
    assert_eq!(d.text(), "😀 CAFÉ🐇 café🐇 cafeteria\n");
    assert_eq!(d.undo_depth(), 1);
    d.undo(d.revision(), 1).unwrap();
    assert_eq!(d.text(), "😀 CAFÉ café cafeteria\n");
}
#[test]
fn selection_and_unicode_boundaries() {
    let d = Document::new("😀 café café".into());
    let mut o = options();
    o.scope = Some(BufferRange {
        revision: BufferRevision(0),
        start: 8,
        end: 12,
    });
    let m = find(&d.snapshot().text, BufferRevision(0), &o).unwrap();
    assert_eq!(m.len(), 1);
    assert_eq!(m[0].start, 8);
    o.scope.as_mut().unwrap().start = 1;
    assert!(find(&d.snapshot().text, BufferRevision(0), &o).is_err());
    o = options();
    o.expression = "[".into();
    assert!(find(&d.snapshot().text, BufferRevision(0), &o).is_err());
}

use std::{
    alloc::{GlobalAlloc, Layout, System},
    cell::Cell,
};
struct MeasuredAllocator;
thread_local! { static MEASURE: Cell<bool> = const {Cell::new(false)}; static PEAK: Cell<usize> = const {Cell::new(0)}; static TOTAL: Cell<usize> = const {Cell::new(0)}; }
fn measured(size: usize) {
    if MEASURE.try_with(Cell::get).unwrap_or(false) {
        let _ = PEAK.try_with(|v| v.set(v.get().max(size)));
        let _ = TOTAL.try_with(|v| v.set(v.get().saturating_add(size)));
    }
}
unsafe impl GlobalAlloc for MeasuredAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        measured(layout.size());
        unsafe { System.alloc(layout) }
    }
    unsafe fn dealloc(&self, p: *mut u8, layout: Layout) {
        unsafe { System.dealloc(p, layout) }
    }
    unsafe fn realloc(&self, p: *mut u8, layout: Layout, size: usize) -> *mut u8 {
        measured(size);
        unsafe { System.realloc(p, layout, size) }
    }
}
#[global_allocator]
static ALLOCATOR: MeasuredAllocator = MeasuredAllocator;
fn allocation_measure<T>(f: impl FnOnce() -> T) -> (T, usize, usize) {
    PEAK.set(0);
    TOTAL.set(0);
    MEASURE.set(true);
    let value = f();
    MEASURE.set(false);
    (value, PEAK.get(), TOTAL.get())
}
#[test]
fn capture_expansion_refuses_before_oversized_allocation() {
    let text = buntrail_core::Text::from("a".repeat(1024 * 1024));
    let mut options = options();
    options.expression = "^(a+)$".into();
    options.whole_word = false;
    let replacement = "$1".repeat(40);
    let (result, peak, _) =
        allocation_measure(|| plan(&text, BufferRevision(0), &options, &replacement));
    assert!(result.is_err());
    eprintln!("single_capture_peak_bytes={peak}");
    assert!(
        peak <= 4 * 1024 * 1024,
        "oversized capture allocated {peak} before refusal"
    );
}
#[test]
fn aggregate_capture_budget_refuses_before_expanding_later_match() {
    let text = buntrail_core::Text::from(format!(
        "{}\n{}\n",
        "a".repeat(1024 * 1024),
        "a".repeat(1024 * 1024)
    ));
    let mut options = options();
    options.expression = "^(a+)$".into();
    options.whole_word = false;
    let replacement = "$1".repeat(20);
    let (result, peak, total) =
        allocation_measure(|| plan(&text, BufferRevision(0), &options, &replacement));
    assert!(result.is_err());
    eprintln!("aggregate_capture_peak_bytes={peak} requested_total_bytes={total}");
    assert!(
        peak <= 24 * 1024 * 1024,
        "allocated {peak} for a 20 MiB first value or over-budget later value"
    );
    assert!(
        total < 32 * 1024 * 1024,
        "allocation requests {total} expanded a later match beyond the remaining budget"
    );
}
#[test]
fn bounded_replacement_keeps_regex_interpolation_grammar() {
    let text = buntrail_core::Text::from("ab");
    let mut options = options();
    options.expression = "(?<first>a)(b)?(?<missing>c)?".into();
    options.whole_word = false;
    let regex = pattern(&options).unwrap();
    let captures = regex.captures("ab").unwrap();
    for template in [
        "$0|$1|${2}|$first|${first}|$missing|$100|$1a",
        "$$|$$1|$$$1|${}|${1|$|${名前}|$99999999999999999999999999999999",
        "${first}🐇${2}",
    ] {
        let mut expected = String::new();
        captures.expand(template, &mut expected);
        assert_eq!(
            plan(&text, BufferRevision(0), &options, template)
                .unwrap()
                .replacements[0]
                .text,
            expected
        );
    }
}