From 2b64a64d7ea43e22ad82f97f2c118891b74c3199 Mon Sep 17 00:00:00 2001
From: Pascal Kuthe <pascal.kuthe@semimod.de>
Date: Thu, 9 Mar 2023 22:19:14 +0100
Subject: [PATCH] Add API to create a Transaction from potentially overlapping
 changes

This commit adds new functions to `Transaction` that allow creating
edits that might potentially overlap. Any change that overlaps
previous changes is ignored. Furthermore, a utility method is added
that also drops selections associated with dropped changes (for
transactions that are created from a selection).

This is needed to avoid crashes when applying multicursor
autocompletions, as the edit from a previous cursor may overlap
with the next cursor/edit.
---
 helix-core/src/transaction.rs | 67 +++++++++++++++++++++++++++++++++++
 1 file changed, 67 insertions(+)

diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs
index d2f4de07..d8e581aa 100644
--- a/helix-core/src/transaction.rs
+++ b/helix-core/src/transaction.rs
@@ -1,3 +1,5 @@
+use smallvec::SmallVec;
+
 use crate::{Range, Rope, Selection, Tendril};
 use std::borrow::Cow;
 
@@ -466,6 +468,33 @@ impl Transaction {
         self
     }
 
+    /// Generate a transaction from a set of potentially overlapping changes. The `change_ranges`
+    /// iterator yield the range (of removed text) in the old document for each edit. If any change
+    /// overlaps with a range overlaps with a previous range then that range is ignored.
+    ///
+    /// The `process_change` callback is called for each edit that is not ignored (in the order
+    /// yielded by `changes`) and should return the new text that the associated range will be
+    /// replaced with.
+    ///
+    /// To make this function more flexible the iterator can yield additional data for each change
+    /// that is passed to `process_change`
+    pub fn change_ignore_overlapping<T>(
+        doc: &Rope,
+        change_ranges: impl Iterator<Item = (usize, usize, T)>,
+        mut process_change: impl FnMut(usize, usize, T) -> Option<Tendril>,
+    ) -> Self {
+        let mut last = 0;
+        let changes = change_ranges.filter_map(|(from, to, data)| {
+            if from < last {
+                return None;
+            }
+            let tendril = process_change(from, to, data);
+            last = to;
+            Some((from, to, tendril))
+        });
+        Self::change(doc, changes)
+    }
+
     /// Generate a transaction from a set of changes.
     pub fn change<I>(doc: &Rope, changes: I) -> Self
     where
@@ -513,6 +542,44 @@ impl Transaction {
         Self::change(doc, selection.iter().map(f))
     }
 
+    pub fn change_by_selection_ignore_overlapping(
+        doc: &Rope,
+        selection: &Selection,
+        mut change_range: impl FnMut(&Range) -> (usize, usize),
+        mut create_tendril: impl FnMut(usize, usize) -> Option<Tendril>,
+    ) -> (Transaction, Selection) {
+        let mut last_selection_idx = None;
+        let mut new_primary_idx = None;
+        let mut ranges: SmallVec<[Range; 1]> = SmallVec::new();
+        let process_change = |change_start, change_end, (idx, range): (usize, &Range)| {
+            // update the primary idx
+            if idx == selection.primary_index() {
+                new_primary_idx = Some(idx);
+            } else if new_primary_idx.is_none() {
+                if idx > selection.primary_index() {
+                    new_primary_idx = last_selection_idx;
+                } else {
+                    last_selection_idx = Some(idx);
+                }
+            }
+            ranges.push(*range);
+            create_tendril(change_start, change_end)
+        };
+        let transaction = Self::change_ignore_overlapping(
+            doc,
+            selection.iter().enumerate().map(|range| {
+                let (change_start, change_end) = change_range(range.1);
+                (change_start, change_end, range)
+            }),
+            process_change,
+        );
+
+        (
+            transaction,
+            Selection::new(ranges, new_primary_idx.unwrap_or(0)),
+        )
+    }
+
     /// Insert text at each selection head.
     pub fn insert(doc: &Rope, selection: &Selection, text: Tendril) -> Self {
         Self::change_by_selection(doc, selection, |range| {