weavatrix-edit 0.1.7

Deterministic, Unicode-safe edit-plan validation and application for source refactoring
Documentation
use crate::error::{EditError, ErrorCode};

use super::PreparedEdit;

pub(super) fn sort_prepared(edits: &mut [PreparedEdit]) {
    if !edits
        .windows(2)
        .all(|pair| compare_prepared(&pair[0], &pair[1]).is_le())
    {
        edits.sort_unstable_by(compare_prepared);
    }
}

pub(super) fn compare_prepared(left: &PreparedEdit, right: &PreparedEdit) -> core::cmp::Ordering {
    left.start
        .cmp(&right.start)
        .then_with(|| empty_rank(left.start, left.end).cmp(&empty_rank(right.start, right.end)))
        .then_with(|| left.end.cmp(&right.end))
        .then_with(|| left.order.cmp(&right.order))
}

pub(super) const fn empty_rank(start: usize, end: usize) -> u8 {
    if start == end { 0 } else { 1 }
}

pub(super) fn verify_ranges(
    ranges: impl Iterator<Item = (usize, usize, usize)>,
) -> Result<(), EditError> {
    let mut active: Option<(usize, usize)> = None;
    for (start, end, order) in ranges {
        if let Some((active_end, active_order)) = active
            && start < active_end
        {
            return Err(EditError::new(
                ErrorCode::OverlappingEdits,
                format!("edits {active_order} and {order} overlap"),
            )
            .at_edit(active_order)
            .with_related_edit(order));
        }
        if end > start {
            active = Some((end, order));
        }
    }
    Ok(())
}

pub(super) fn prepared_output_size(
    source_size: usize,
    edits: &[PreparedEdit],
    maximum: usize,
) -> Result<usize, EditError> {
    output_size(
        source_size,
        edits
            .iter()
            .map(|edit| (edit.start, edit.end, edit.after.len())),
        maximum,
    )
}

pub(super) fn output_size(
    source_size: usize,
    edits: impl Iterator<Item = (usize, usize, usize)>,
    maximum: usize,
) -> Result<usize, EditError> {
    let mut removed = 0_usize;
    let mut added = 0_usize;
    for (start, end, after_size) in edits {
        removed = removed
            .checked_add(end - start)
            .ok_or_else(|| EditError::new(ErrorCode::OutputTooLarge, "output size overflow"))?;
        added = added
            .checked_add(after_size)
            .ok_or_else(|| EditError::new(ErrorCode::OutputTooLarge, "output size overflow"))?;
    }
    output_size_from_totals(source_size, removed, added, maximum)
}

pub(super) fn output_size_from_totals(
    source_size: usize,
    removed: usize,
    added: usize,
    maximum: usize,
) -> Result<usize, EditError> {
    let size = source_size
        .checked_sub(removed)
        .and_then(|value| value.checked_add(added))
        .ok_or_else(|| EditError::new(ErrorCode::OutputTooLarge, "output size overflow"))?;
    if size > maximum {
        return Err(EditError::new(
            ErrorCode::OutputTooLarge,
            format!("output exceeds the {maximum}-byte limit"),
        ));
    }
    Ok(size)
}