mirror of https://github.com/helix-editor/helix
Add Transaction::change_by_and_with_selection
Adds `Transaction::change_by_and_with_selection` which centralizes logic for producing change sets with a potentially new selection that is applied incrementally, rather than all at once at the end with `with_selection`. It also centralizes the offset tracking logic so that the caller can construct a new selection with ranges as if they were operating on the text as-is.pull/7269/head
parent
2fbe7fc5b5
commit
d29ca3d4a5
|
@ -4,8 +4,6 @@
|
|||
use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
||||
// Heavily based on https://github.com/codemirror/closebrackets/
|
||||
pub const DEFAULT_PAIRS: &[(char, char)] = &[
|
||||
('(', ')'),
|
||||
|
@ -146,7 +144,7 @@ fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
|
|||
}
|
||||
|
||||
/// calculate what the resulting range should be for an auto pair insertion
|
||||
fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted: usize) -> Range {
|
||||
fn get_next_range(doc: &Rope, start_range: &Range, len_inserted: usize) -> Range {
|
||||
// When the character under the cursor changes due to complete pair
|
||||
// insertion, we must look backward a grapheme and then add the length
|
||||
// of the insertion to put the resulting cursor in the right place, e.g.
|
||||
|
@ -165,10 +163,7 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
|
|||
|
||||
// inserting at the very end of the document after the last newline
|
||||
if start_range.head == doc.len_chars() && start_range.anchor == doc.len_chars() {
|
||||
return Range::new(
|
||||
start_range.anchor + offset + 1,
|
||||
start_range.head + offset + 1,
|
||||
);
|
||||
return Range::new(start_range.anchor + 1, start_range.head + 1);
|
||||
}
|
||||
|
||||
let doc_slice = doc.slice(..);
|
||||
|
@ -177,7 +172,7 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
|
|||
// just skip over graphemes
|
||||
if len_inserted == 0 {
|
||||
let end_anchor = if single_grapheme {
|
||||
graphemes::next_grapheme_boundary(doc_slice, start_range.anchor) + offset
|
||||
graphemes::next_grapheme_boundary(doc_slice, start_range.anchor)
|
||||
|
||||
// even for backward inserts with multiple grapheme selections,
|
||||
// we want the anchor to stay where it is so that the relative
|
||||
|
@ -185,42 +180,38 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
|
|||
//
|
||||
// foo([) wor]d -> insert ) -> foo()[ wor]d
|
||||
} else {
|
||||
start_range.anchor + offset
|
||||
start_range.anchor
|
||||
};
|
||||
|
||||
return Range::new(
|
||||
end_anchor,
|
||||
graphemes::next_grapheme_boundary(doc_slice, start_range.head) + offset,
|
||||
graphemes::next_grapheme_boundary(doc_slice, start_range.head),
|
||||
);
|
||||
}
|
||||
|
||||
// trivial case: only inserted a single-char opener, just move the selection
|
||||
if len_inserted == 1 {
|
||||
let end_anchor = if single_grapheme || start_range.direction() == Direction::Backward {
|
||||
start_range.anchor + offset + 1
|
||||
start_range.anchor + 1
|
||||
} else {
|
||||
start_range.anchor + offset
|
||||
start_range.anchor
|
||||
};
|
||||
|
||||
return Range::new(end_anchor, start_range.head + offset + 1);
|
||||
return Range::new(end_anchor, start_range.head + 1);
|
||||
}
|
||||
|
||||
// If the head = 0, then we must be in insert mode with a backward
|
||||
// cursor, which implies the head will just move
|
||||
let end_head = if start_range.head == 0 || start_range.direction() == Direction::Backward {
|
||||
start_range.head + offset + 1
|
||||
start_range.head + 1
|
||||
} else {
|
||||
// We must have a forward cursor, which means we must move to the
|
||||
// other end of the grapheme to get to where the new characters
|
||||
// are inserted, then move the head to where it should be
|
||||
let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
|
||||
log::trace!(
|
||||
"prev_bound: {}, offset: {}, len_inserted: {}",
|
||||
prev_bound,
|
||||
offset,
|
||||
len_inserted
|
||||
);
|
||||
prev_bound + offset + len_inserted
|
||||
log::trace!("prev_bound: {}, len_inserted: {}", prev_bound, len_inserted);
|
||||
|
||||
prev_bound + len_inserted
|
||||
};
|
||||
|
||||
let end_anchor = match (start_range.len(), start_range.direction()) {
|
||||
|
@ -239,7 +230,7 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
|
|||
// if we are appending, the anchor stays where it is; only offset
|
||||
// for multiple range insertions
|
||||
} else {
|
||||
start_range.anchor + offset
|
||||
start_range.anchor
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,13 +239,11 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
|
|||
// if we're backward, then the head is at the first char
|
||||
// of the typed char, so we need to add the length of
|
||||
// the closing char
|
||||
graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor)
|
||||
+ len_inserted
|
||||
+ offset
|
||||
graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor) + len_inserted
|
||||
} else {
|
||||
// when we are inserting in front of a selection, we need to move
|
||||
// the anchor over by however many characters were inserted overall
|
||||
start_range.anchor + offset + len_inserted
|
||||
start_range.anchor + len_inserted
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -263,10 +252,7 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
|
|||
}
|
||||
|
||||
fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
||||
let mut end_ranges = SmallVec::with_capacity(selection.len());
|
||||
let mut offs = 0;
|
||||
|
||||
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
|
||||
let transaction = Transaction::change_by_and_with_selection(doc, selection, |start_range| {
|
||||
let cursor = start_range.cursor(doc.slice(..));
|
||||
let next_char = doc.get_char(cursor);
|
||||
let len_inserted;
|
||||
|
@ -289,23 +275,17 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
|||
}
|
||||
};
|
||||
|
||||
let next_range = get_next_range(doc, start_range, offs, len_inserted);
|
||||
end_ranges.push(next_range);
|
||||
offs += len_inserted;
|
||||
let next_range = get_next_range(doc, start_range, len_inserted);
|
||||
|
||||
change
|
||||
(change, Some(next_range))
|
||||
});
|
||||
|
||||
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
|
||||
log::debug!("auto pair transaction: {:#?}", t);
|
||||
t
|
||||
log::debug!("auto pair transaction: {:#?}", transaction);
|
||||
transaction
|
||||
}
|
||||
|
||||
fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
||||
let mut end_ranges = SmallVec::with_capacity(selection.len());
|
||||
let mut offs = 0;
|
||||
|
||||
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
|
||||
let transaction = Transaction::change_by_and_with_selection(doc, selection, |start_range| {
|
||||
let cursor = start_range.cursor(doc.slice(..));
|
||||
let next_char = doc.get_char(cursor);
|
||||
let mut len_inserted = 0;
|
||||
|
@ -320,25 +300,18 @@ fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
|||
(cursor, cursor, Some(tendril))
|
||||
};
|
||||
|
||||
let next_range = get_next_range(doc, start_range, offs, len_inserted);
|
||||
end_ranges.push(next_range);
|
||||
offs += len_inserted;
|
||||
let next_range = get_next_range(doc, start_range, len_inserted);
|
||||
|
||||
change
|
||||
(change, Some(next_range))
|
||||
});
|
||||
|
||||
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
|
||||
log::debug!("auto pair transaction: {:#?}", t);
|
||||
t
|
||||
log::debug!("auto pair transaction: {:#?}", transaction);
|
||||
transaction
|
||||
}
|
||||
|
||||
/// handle cases where open and close is the same, or in triples ("""docstring""")
|
||||
fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
||||
let mut end_ranges = SmallVec::with_capacity(selection.len());
|
||||
|
||||
let mut offs = 0;
|
||||
|
||||
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
|
||||
let transaction = Transaction::change_by_and_with_selection(doc, selection, |start_range| {
|
||||
let cursor = start_range.cursor(doc.slice(..));
|
||||
let mut len_inserted = 0;
|
||||
let next_char = doc.get_char(cursor);
|
||||
|
@ -360,14 +333,12 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
|||
(cursor, cursor, Some(pair_str))
|
||||
};
|
||||
|
||||
let next_range = get_next_range(doc, start_range, offs, len_inserted);
|
||||
end_ranges.push(next_range);
|
||||
offs += len_inserted;
|
||||
let next_range = get_next_range(doc, start_range, len_inserted);
|
||||
|
||||
change
|
||||
(change, Some(next_range))
|
||||
});
|
||||
|
||||
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
|
||||
log::debug!("auto pair transaction: {:#?}", t);
|
||||
t
|
||||
log::debug!("auto pair transaction: {:#?}", transaction);
|
||||
|
||||
transaction
|
||||
}
|
||||
|
|
|
@ -736,6 +736,55 @@ impl Transaction {
|
|||
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
|
||||
/// is returned by the function, that range is dropped from the resulting
|
||||
/// selection.
|
||||
pub fn change_by_and_with_selection<F>(doc: &Rope, selection: &Selection, mut f: F) -> Self
|
||||
where
|
||||
F: FnMut(&Range) -> (Change, Option<Range>),
|
||||
{
|
||||
let mut end_ranges = SmallVec::with_capacity(selection.len());
|
||||
let mut offset = 0;
|
||||
|
||||
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
|
||||
let ((from, to, replacement), end_range) = f(start_range);
|
||||
let mut change_size = to as isize - from as isize;
|
||||
|
||||
if let Some(ref text) = replacement {
|
||||
change_size = text.chars().count() as isize - change_size;
|
||||
} else {
|
||||
change_size = -change_size;
|
||||
}
|
||||
|
||||
if let Some(end_range) = end_range {
|
||||
let offset_range = Range::new(
|
||||
(end_range.anchor as isize + offset) as usize,
|
||||
(end_range.head as isize + offset) as usize,
|
||||
);
|
||||
|
||||
log::trace!("end range {:?} offset to: {:?}", end_range, offset_range);
|
||||
|
||||
end_ranges.push(offset_range);
|
||||
}
|
||||
|
||||
offset += change_size;
|
||||
|
||||
log::trace!(
|
||||
"from: {}, to: {}, replacement: {:?}, offset: {}",
|
||||
from,
|
||||
to,
|
||||
replacement,
|
||||
offset
|
||||
);
|
||||
|
||||
(from, to, replacement)
|
||||
});
|
||||
|
||||
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| {
|
||||
|
|
Loading…
Reference in New Issue