mirror of https://github.com/helix-editor/helix
map positions through changes in O(N)
parent
6c6923c39e
commit
d491e234f4
|
@ -161,34 +161,35 @@ impl Range {
|
||||||
self.from() <= pos && pos < self.to()
|
self.from() <= pos && pos < self.to()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Map a range through a set of changes. Returns a new range representing the same position
|
/// Map a range through a set of changes. Returns a new range representing
|
||||||
/// after the changes are applied.
|
/// the same position after the changes are applied. Note that this
|
||||||
pub fn map(self, changes: &ChangeSet) -> Self {
|
/// function runs in O(N) (N is number of changes) and can therefore
|
||||||
|
/// cause performance problems if run for a large number of ranges as the
|
||||||
|
/// complexity is then O(MN) (for multicuror M=N usually). Instead use
|
||||||
|
/// [Selection::map] or [ChangeSet::update_positions] instead
|
||||||
|
pub fn map(mut self, changes: &ChangeSet) -> Self {
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
let (anchor, head) = match self.anchor.cmp(&self.head) {
|
if changes.is_empty() {
|
||||||
Ordering::Equal => (
|
return self;
|
||||||
changes.map_pos(self.anchor, Assoc::After),
|
|
||||||
changes.map_pos(self.head, Assoc::After),
|
|
||||||
),
|
|
||||||
Ordering::Less => (
|
|
||||||
changes.map_pos(self.anchor, Assoc::After),
|
|
||||||
changes.map_pos(self.head, Assoc::Before),
|
|
||||||
),
|
|
||||||
Ordering::Greater => (
|
|
||||||
changes.map_pos(self.anchor, Assoc::Before),
|
|
||||||
changes.map_pos(self.head, Assoc::After),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
// We want to return a new `Range` with `horiz == None` every time,
|
|
||||||
// even if the anchor and head haven't changed, because we don't
|
|
||||||
// know if the *visual* position hasn't changed due to
|
|
||||||
// character-width or grapheme changes earlier in the text.
|
|
||||||
Self {
|
|
||||||
anchor,
|
|
||||||
head,
|
|
||||||
old_visual_position: None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let positions_to_map = match self.anchor.cmp(&self.head) {
|
||||||
|
Ordering::Equal => [
|
||||||
|
(&mut self.anchor, Assoc::After),
|
||||||
|
(&mut self.head, Assoc::After),
|
||||||
|
],
|
||||||
|
Ordering::Less => [
|
||||||
|
(&mut self.anchor, Assoc::After),
|
||||||
|
(&mut self.head, Assoc::Before),
|
||||||
|
],
|
||||||
|
Ordering::Greater => [
|
||||||
|
(&mut self.head, Assoc::After),
|
||||||
|
(&mut self.anchor, Assoc::Before),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
changes.update_positions(positions_to_map.into_iter());
|
||||||
|
self.old_visual_position = None;
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extend the range to cover at least `from` `to`.
|
/// Extend the range to cover at least `from` `to`.
|
||||||
|
@ -450,18 +451,44 @@ impl Selection {
|
||||||
|
|
||||||
/// Map selections over a set of changes. Useful for adjusting the selection position after
|
/// Map selections over a set of changes. Useful for adjusting the selection position after
|
||||||
/// applying changes to a document.
|
/// applying changes to a document.
|
||||||
pub fn map(self, changes: &ChangeSet) -> Self {
|
pub fn map(mut self, changes: &ChangeSet) -> Self {
|
||||||
|
if changes.is_empty() {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
self = self.map_no_normalize(changes);
|
||||||
|
// TODO: only normalize if needed (any ranges out of order)
|
||||||
|
self = self.normalize();
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map selections over a set of changes. Useful for adjusting the selection position after
|
||||||
|
/// applying changes to a document. Doesn't normalize the selection
|
||||||
|
pub fn map_no_normalize(mut self, changes: &ChangeSet) -> Self {
|
||||||
if changes.is_empty() {
|
if changes.is_empty() {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::new(
|
let positions_to_map = self.ranges.iter_mut().flat_map(|range| {
|
||||||
self.ranges
|
use std::cmp::Ordering;
|
||||||
.into_iter()
|
range.old_visual_position = None;
|
||||||
.map(|range| range.map(changes))
|
match range.anchor.cmp(&range.head) {
|
||||||
.collect(),
|
Ordering::Equal => [
|
||||||
self.primary_index,
|
(&mut range.anchor, Assoc::After),
|
||||||
)
|
(&mut range.head, Assoc::After),
|
||||||
|
],
|
||||||
|
Ordering::Less => [
|
||||||
|
(&mut range.anchor, Assoc::After),
|
||||||
|
(&mut range.head, Assoc::Before),
|
||||||
|
],
|
||||||
|
Ordering::Greater => [
|
||||||
|
(&mut range.head, Assoc::After),
|
||||||
|
(&mut range.anchor, Assoc::Before),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
changes.update_positions(positions_to_map);
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ranges(&self) -> &[Range] {
|
pub fn ranges(&self) -> &[Range] {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::{Range, Rope, Selection, Tendril};
|
use crate::{Range, Rope, Selection, Tendril};
|
||||||
use std::borrow::Cow;
|
use std::{borrow::Cow, iter::once};
|
||||||
|
|
||||||
/// (from, to, replacement)
|
/// (from, to, replacement)
|
||||||
pub type Change = (usize, usize, Option<Tendril>);
|
pub type Change = (usize, usize, Option<Tendril>);
|
||||||
|
@ -326,17 +326,33 @@ impl ChangeSet {
|
||||||
self.changes.is_empty() || self.changes == [Operation::Retain(self.len)]
|
self.changes.is_empty() || self.changes == [Operation::Retain(self.len)]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Map a position through the changes.
|
/// Map a *sorted* list of positions through the changes.
|
||||||
///
|
///
|
||||||
/// `assoc` indicates which size to associate the position with. `Before` will keep the
|
/// This is equivalent to updating each position with `map_pos`:
|
||||||
/// position close to the character before, and will place it before insertions over that
|
///
|
||||||
/// range, or at that point. `After` will move it forward, placing it at the end of such
|
/// ``` no-compile
|
||||||
/// insertions.
|
/// for (pos, assoc) in positions {
|
||||||
pub fn map_pos(&self, pos: usize, assoc: Assoc) -> usize {
|
/// *pos = changes.map_pos(*pos, assoc);
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// However this function is significantly faster running in `O(N+M)` instead of `O(NM)`
|
||||||
|
pub fn update_positions<'a>(&self, positions: impl Iterator<Item = (&'a mut usize, Assoc)>) {
|
||||||
use Operation::*;
|
use Operation::*;
|
||||||
|
|
||||||
|
let mut positions = positions.peekable();
|
||||||
|
macro_rules! map {
|
||||||
|
($map: expr) => {
|
||||||
|
loop {
|
||||||
|
let Some((pos, assoc)) = positions.peek_mut() else { return; };
|
||||||
|
let Some(new_pos) = $map(**pos, *assoc) else { break; };
|
||||||
|
**pos = new_pos;
|
||||||
|
positions.next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let mut old_pos = 0;
|
let mut old_pos = 0;
|
||||||
let mut new_pos = 0;
|
let mut new_pos = 0;
|
||||||
|
|
||||||
let mut iter = self.changes.iter().peekable();
|
let mut iter = self.changes.iter().peekable();
|
||||||
|
|
||||||
while let Some(change) = iter.next() {
|
while let Some(change) = iter.next() {
|
||||||
|
@ -348,16 +364,12 @@ impl ChangeSet {
|
||||||
|
|
||||||
match change {
|
match change {
|
||||||
Retain(_) => {
|
Retain(_) => {
|
||||||
if old_end > pos {
|
map!(|pos, _| (old_end > pos).then_some(new_pos + (pos - old_pos)));
|
||||||
return new_pos + (pos - old_pos);
|
|
||||||
}
|
|
||||||
new_pos += len;
|
new_pos += len;
|
||||||
}
|
}
|
||||||
Delete(_) => {
|
Delete(_) => {
|
||||||
// in range
|
// in range
|
||||||
if old_end > pos {
|
map!(|pos, _| (old_end > pos).then_some(new_pos));
|
||||||
return new_pos;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Insert(s) => {
|
Insert(s) => {
|
||||||
let ins = s.chars().count();
|
let ins = s.chars().count();
|
||||||
|
@ -368,26 +380,26 @@ impl ChangeSet {
|
||||||
|
|
||||||
old_end = old_pos + len;
|
old_end = old_pos + len;
|
||||||
// in range of replaced text
|
// in range of replaced text
|
||||||
if old_end > pos {
|
map!(|pos, assoc| (old_end > pos).then(|| {
|
||||||
// at point or tracking before
|
// at point or tracking before
|
||||||
if pos == old_pos || assoc == Assoc::Before {
|
if pos == old_pos || assoc == Assoc::Before {
|
||||||
return new_pos;
|
new_pos
|
||||||
} else {
|
} else {
|
||||||
// place to end of insert
|
// place to end of insert
|
||||||
return new_pos + ins;
|
new_pos + ins
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
// at insert point
|
// at insert point
|
||||||
if old_pos == pos {
|
map!(|pos, assoc| (old_pos == pos).then(|| {
|
||||||
// return position before inserted text
|
// return position before inserted text
|
||||||
if assoc == Assoc::Before {
|
if assoc == Assoc::Before {
|
||||||
return new_pos;
|
new_pos
|
||||||
} else {
|
} else {
|
||||||
// after text
|
// after text
|
||||||
return new_pos + ins;
|
new_pos + ins
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
new_pos += ins;
|
new_pos += ins;
|
||||||
|
@ -395,14 +407,21 @@ impl ChangeSet {
|
||||||
}
|
}
|
||||||
old_pos = old_end;
|
old_pos = old_end;
|
||||||
}
|
}
|
||||||
|
map!(|pos, _| (old_pos == pos).then_some(new_pos));
|
||||||
|
let out_of_bounds: Vec<_> = positions.collect();
|
||||||
|
|
||||||
if pos > old_pos {
|
panic!("Positions {out_of_bounds:?} are out of range for changeset len {old_pos}!",)
|
||||||
panic!(
|
|
||||||
"Position {} is out of range for changeset len {}!",
|
|
||||||
pos, old_pos
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
new_pos
|
|
||||||
|
/// Map a position through the changes.
|
||||||
|
///
|
||||||
|
/// `assoc` indicates which side to associate the position with. `Before` will keep the
|
||||||
|
/// position close to the character before, and will place it before insertions over that
|
||||||
|
/// range, or at that point. `After` will move it forward, placing it at the end of such
|
||||||
|
/// insertions.
|
||||||
|
pub fn map_pos(&self, mut pos: usize, assoc: Assoc) -> usize {
|
||||||
|
self.update_positions(once((&mut pos, assoc)));
|
||||||
|
pos
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn changes_iter(&self) -> ChangeIterator {
|
pub fn changes_iter(&self) -> ChangeIterator {
|
||||||
|
|
|
@ -377,7 +377,7 @@ pub mod util {
|
||||||
.expect("transaction must be valid for primary selection");
|
.expect("transaction must be valid for primary selection");
|
||||||
let removed_text = text.slice(removed_start..removed_end);
|
let removed_text = text.slice(removed_start..removed_end);
|
||||||
|
|
||||||
let (transaction, selection) = Transaction::change_by_selection_ignore_overlapping(
|
let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping(
|
||||||
doc,
|
doc,
|
||||||
selection,
|
selection,
|
||||||
|range| {
|
|range| {
|
||||||
|
@ -420,6 +420,11 @@ pub mod util {
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't normalize to avoid merging/reording selections which would
|
||||||
|
// break the association between tabstops and selections. Most ranges
|
||||||
|
// will be replaced by tabstops anyways and the final selection will be
|
||||||
|
// normalized anyways
|
||||||
|
selection = selection.map_no_normalize(changes);
|
||||||
let mut mapped_selection = SmallVec::with_capacity(selection.len());
|
let mut mapped_selection = SmallVec::with_capacity(selection.len());
|
||||||
let mut mapped_primary_idx = 0;
|
let mut mapped_primary_idx = 0;
|
||||||
let primary_range = selection.primary();
|
let primary_range = selection.primary();
|
||||||
|
@ -428,7 +433,6 @@ pub mod util {
|
||||||
mapped_primary_idx = mapped_selection.len()
|
mapped_primary_idx = mapped_selection.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
let range = range.map(changes);
|
|
||||||
let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty());
|
let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty());
|
||||||
let Some(tabstops) = tabstops else{
|
let Some(tabstops) = tabstops else{
|
||||||
// no tabstop normal mapping
|
// no tabstop normal mapping
|
||||||
|
|
|
@ -1121,21 +1121,28 @@ impl Document {
|
||||||
|
|
||||||
let changes = transaction.changes();
|
let changes = transaction.changes();
|
||||||
|
|
||||||
|
changes.update_positions(
|
||||||
|
self.diagnostics
|
||||||
|
.iter_mut()
|
||||||
|
.map(|diagnostic| (&mut diagnostic.range.start, Assoc::After)),
|
||||||
|
);
|
||||||
|
changes.update_positions(
|
||||||
|
self.diagnostics
|
||||||
|
.iter_mut()
|
||||||
|
.map(|diagnostic| (&mut diagnostic.range.end, Assoc::After)),
|
||||||
|
);
|
||||||
// map state.diagnostics over changes::map_pos too
|
// map state.diagnostics over changes::map_pos too
|
||||||
for diagnostic in &mut self.diagnostics {
|
for diagnostic in &mut self.diagnostics {
|
||||||
diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After);
|
|
||||||
diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After);
|
|
||||||
diagnostic.line = self.text.char_to_line(diagnostic.range.start);
|
diagnostic.line = self.text.char_to_line(diagnostic.range.start);
|
||||||
}
|
}
|
||||||
self.diagnostics
|
|
||||||
.sort_unstable_by_key(|diagnostic| diagnostic.range);
|
|
||||||
|
|
||||||
// Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place
|
// Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place
|
||||||
let apply_inlay_hint_changes = |annotations: &mut Rc<[InlineAnnotation]>| {
|
let apply_inlay_hint_changes = |annotations: &mut Rc<[InlineAnnotation]>| {
|
||||||
if let Some(data) = Rc::get_mut(annotations) {
|
if let Some(data) = Rc::get_mut(annotations) {
|
||||||
for inline in data.iter_mut() {
|
changes.update_positions(
|
||||||
inline.char_idx = changes.map_pos(inline.char_idx, Assoc::After);
|
data.iter_mut()
|
||||||
}
|
.map(|diagnostic| (&mut diagnostic.char_idx, Assoc::After)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue