package de.neitzel.fxdesigner.contracts; import java.util.*; /** One named undo transaction. All UTF-16 ranges address the same expected revision. */ public record EditBatch(long expectedRevision, String name, List replacements) { public record Replacement(int start, int end, String text) { public Replacement { if (start < 0 || end < start) throw new IllegalArgumentException("Invalid range"); Objects.requireNonNull(text); } } public EditBatch { if (expectedRevision < 0) throw new IllegalArgumentException("Negative revision"); if (name == null || name.isBlank()) throw new IllegalArgumentException("Missing command name"); replacements = List.copyOf(replacements); } /** Validates every range before computing a result; never partially edits a host draft. */ public String applyTo(DocumentSnapshot draft) { if (!draft.writable()) throw new IllegalStateException("Document is read-only"); if (draft.revision() != expectedRevision) throw new ConcurrentModificationException("Stale document revision"); var ordered = new ArrayList<>(replacements); ordered.sort(Comparator.comparingInt(Replacement::start)); int previousEnd = -1; for (var r : ordered) { if (r.end() > draft.text().length() || r.start() < previousEnd) throw new IllegalArgumentException("Overlapping/out-of-bounds edits"); previousEnd = r.end(); } var result = new StringBuilder(draft.text()); for (int i = ordered.size() - 1; i >= 0; i--) { var r = ordered.get(i); result.replace(r.start(), r.end(), r.text()); } return result.toString(); } }