diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index 2fd373ded..467d55d92 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -110,17 +110,22 @@ impl Default for AutoPairs { // middle of triple quotes, and more exotic pairs like Jinja's {% %} #[must_use] -pub fn hook(doc: &Rope, range: &Range, ch: char, pairs: &AutoPairs) -> Option<(Change, Range)> { +pub fn hook_insert( + doc: &Rope, + range: &Range, + ch: char, + pairs: &AutoPairs, +) -> Option<(Change, Range)> { log::trace!("autopairs hook range: {:#?}", range); if let Some(pair) = pairs.get(ch) { if pair.same() { - return handle_same(doc, range, pair); + return handle_insert_same(doc, range, pair); } else if pair.open == ch { - return handle_open(doc, range, pair); + return handle_insert_open(doc, range, pair); } else if pair.close == ch { // && char_at pos == close - return handle_close(doc, range, pair); + return handle_insert_close(doc, range, pair); } } @@ -243,7 +248,7 @@ fn get_next_range(doc: &Rope, start_range: &Range, len_inserted: usize) -> Range Range::new(end_anchor, end_head) } -fn handle_open(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range)> { +fn handle_insert_open(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range)> { let cursor = range.cursor(doc.slice(..)); let next_char = doc.get_char(cursor); let len_inserted; @@ -271,7 +276,7 @@ fn handle_open(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range) Some(result) } -fn handle_close(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range)> { +fn handle_insert_close(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range)> { let cursor = range.cursor(doc.slice(..)); let next_char = doc.get_char(cursor); @@ -291,7 +296,7 @@ fn handle_close(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range } /// handle cases where open and close is the same, or in triples ("""docstring""") -fn handle_same(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range)> { +fn handle_insert_same(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range)> { let cursor = range.cursor(doc.slice(..)); let mut len_inserted = 0; let next_char = doc.get_char(cursor); diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index 0a88090f3..e0d6abc39 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -738,16 +738,6 @@ impl Transaction { ) } - /// Generate a transaction with a deletion per selection range. - /// Compared to using `change_by_selection` directly these ranges may overlap. - /// In that case they are merged - pub fn delete_by_selection(doc: &Rope, selection: &Selection, f: F) -> Self - where - F: FnMut(&Range) -> Deletion, - { - Self::delete(doc, selection.iter().map(f)) - } - /// Generate a transaction with a change per selection range, which /// generates a new selection as well. Each range is operated upon by /// the given function and can optionally produce a new range. If none @@ -801,6 +791,60 @@ impl Transaction { transaction.with_selection(Selection::new(end_ranges, selection.primary_index())) } + /// Generate a transaction with a deletion per selection range. + /// Compared to using `change_by_selection` directly these ranges may overlap. + /// In that case they are merged. + pub fn delete_by_selection(doc: &Rope, selection: &Selection, f: F) -> Self + where + F: FnMut(&Range) -> Deletion, + { + Self::delete(doc, selection.iter().map(f)) + } + + /// Generate a transaction with a delete per selection range, which + /// generates a new selection as well. Each range is operated upon by + /// the given function and can optionally produce a new range. If none + /// is returned by the function, that range is mapped through the change + /// as usual. + /// + /// Compared to using `change_by_and_with_selection` directly these ranges + /// may overlap. In that case they are merged. + pub fn delete_by_and_with_selection(doc: &Rope, selection: &Selection, mut f: F) -> Self + where + F: FnMut(&Range) -> (Deletion, Option), + { + let mut end_ranges = SmallVec::with_capacity(selection.len()); + let mut offset = 0; + + let transaction = Transaction::delete_by_selection(doc, selection, |start_range| { + let ((from, to), end_range) = f(start_range); + let change_size = to - from; + + if let Some(end_range) = end_range { + let offset_range = Range::new( + end_range.anchor.saturating_sub(offset), + end_range.head.saturating_sub(offset), + ); + + log::trace!("end range {:?} offset to: {:?}", end_range, offset_range); + + end_ranges.push(offset_range); + } else { + let changeset = ChangeSet::from_change(doc, (from, to, None)); + let end_range = start_range.map(&changeset); + end_ranges.push(end_range); + } + + offset += change_size; + + log::trace!("delete from: {}, to: {}, offset: {}", from, to, offset); + + (from, to) + }); + + transaction.with_selection(Selection::new(end_ranges, selection.primary_index())) + } + /// Insert text at each selection head. pub fn insert(doc: &Rope, selection: &Selection, text: Tendril) -> Self { Self::change_by_selection(doc, selection, |range| { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 50e394756..5c755c8c1 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3868,7 +3868,7 @@ pub mod insert { auto_pairs .as_ref() .and_then(|ap| { - auto_pairs::hook(text, range, c, ap) + auto_pairs::hook_insert(text, range, c, ap) .map(|(change, range)| (change, Some(range))) .or(Some(insert_char(*range, c))) }) @@ -4055,11 +4055,13 @@ pub mod insert { let indent_width = doc.indent_width(); let auto_pairs = doc.auto_pairs(cx.editor); - let transaction = - Transaction::delete_by_selection(doc.text(), doc.selection(view.id), |range| { + let transaction = Transaction::delete_by_and_with_selection( + doc.text(), + doc.selection(view.id), + |range| { let pos = range.cursor(text); if pos == 0 { - return (pos, pos); + return ((pos, pos), None); } let line_start_pos = text.line_to_char(range.cursor_line(text)); // consider to delete by indent level if all characters before `pos` are indent units. @@ -4067,7 +4069,10 @@ pub mod insert { if !fragment.is_empty() && fragment.chars().all(|ch| ch == ' ' || ch == '\t') { if text.get_char(pos.saturating_sub(1)) == Some('\t') { // fast path, delete one char - (graphemes::nth_prev_grapheme_boundary(text, pos, 1), pos) + ( + (graphemes::nth_prev_grapheme_boundary(text, pos, 1), pos), + None, + ) } else { let width: usize = fragment .chars() @@ -4094,7 +4099,7 @@ pub mod insert { _ => break, } } - (start, pos) // delete! + ((start, pos), None) // delete! } } else { match ( @@ -4110,18 +4115,26 @@ pub mod insert { // delete both autopaired characters { ( - graphemes::nth_prev_grapheme_boundary(text, pos, count), - graphemes::nth_next_grapheme_boundary(text, pos, count), + ( + graphemes::nth_prev_grapheme_boundary(text, pos, count), + graphemes::nth_next_grapheme_boundary(text, pos, count), + ), + None, ) } _ => // delete 1 char { - (graphemes::nth_prev_grapheme_boundary(text, pos, count), pos) + ( + (graphemes::nth_prev_grapheme_boundary(text, pos, count), pos), + None, + ) } } } - }); + }, + ); + let (view, doc) = current!(cx.editor); doc.apply(&transaction, view.id); }