Skip to content

Commit 2efb508

Browse files
committed
map positions through changes in O(N)
1 parent b0cdc2d commit 2efb508

File tree

4 files changed

+98
-76
lines changed

4 files changed

+98
-76
lines changed

helix-core/src/selection.rs

Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -161,36 +161,6 @@ impl Range {
161161
self.from() <= pos && pos < self.to()
162162
}
163163

164-
/// Map a range through a set of changes. Returns a new range representing the same position
165-
/// after the changes are applied.
166-
pub fn map(self, changes: &ChangeSet) -> Self {
167-
use std::cmp::Ordering;
168-
let (anchor, head) = match self.anchor.cmp(&self.head) {
169-
Ordering::Equal => (
170-
changes.map_pos(self.anchor, Assoc::After),
171-
changes.map_pos(self.head, Assoc::After),
172-
),
173-
Ordering::Less => (
174-
changes.map_pos(self.anchor, Assoc::After),
175-
changes.map_pos(self.head, Assoc::Before),
176-
),
177-
Ordering::Greater => (
178-
changes.map_pos(self.anchor, Assoc::Before),
179-
changes.map_pos(self.head, Assoc::After),
180-
),
181-
};
182-
183-
// We want to return a new `Range` with `horiz == None` every time,
184-
// even if the anchor and head haven't changed, because we don't
185-
// know if the *visual* position hasn't changed due to
186-
// character-width or grapheme changes earlier in the text.
187-
Self {
188-
anchor,
189-
head,
190-
old_visual_position: None,
191-
}
192-
}
193-
194164
/// Extend the range to cover at least `from` `to`.
195165
#[must_use]
196166
pub fn extend(&self, from: usize, to: usize) -> Self {
@@ -450,18 +420,44 @@ impl Selection {
450420

451421
/// Map selections over a set of changes. Useful for adjusting the selection position after
452422
/// applying changes to a document.
453-
pub fn map(self, changes: &ChangeSet) -> Self {
423+
pub fn map(mut self, changes: &ChangeSet) -> Self {
454424
if changes.is_empty() {
455425
return self;
456426
}
427+
self = self.map_no_normalize(changes);
428+
if self.ranges.len() > 1 {
429+
// TODO: only normalize if needed (any ranges out of order)
430+
self = self.normalize();
431+
}
432+
self
433+
}
457434

458-
Self::new(
459-
self.ranges
460-
.into_iter()
461-
.map(|range| range.map(changes))
462-
.collect(),
463-
self.primary_index,
464-
)
435+
/// Map selections over a set of changes. Useful for adjusting the selection position after
436+
/// applying changes to a document. Doesn't normalize the selection
437+
pub fn map_no_normalize(mut self, changes: &ChangeSet) -> Self {
438+
if changes.is_empty() {
439+
return self;
440+
}
441+
442+
let positions_to_map = self.ranges.iter_mut().flat_map(|range| {
443+
use std::cmp::Ordering;
444+
match range.anchor.cmp(&range.head) {
445+
Ordering::Equal => [
446+
(&mut range.anchor, Assoc::After),
447+
(&mut range.head, Assoc::After),
448+
],
449+
Ordering::Less => [
450+
(&mut range.anchor, Assoc::After),
451+
(&mut range.head, Assoc::Before),
452+
],
453+
Ordering::Greater => [
454+
(&mut range.head, Assoc::After),
455+
(&mut range.anchor, Assoc::Before),
456+
],
457+
}
458+
});
459+
changes.update_positions(positions_to_map);
460+
self
465461
}
466462

467463
pub fn ranges(&self) -> &[Range] {

helix-core/src/transaction.rs

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use smallvec::SmallVec;
22

33
use crate::{Range, Rope, Selection, Tendril};
4-
use std::borrow::Cow;
4+
use std::{borrow::Cow, iter::once};
55

66
/// (from, to, replacement)
77
pub type Change = (usize, usize, Option<Tendril>);
@@ -326,17 +326,33 @@ impl ChangeSet {
326326
self.changes.is_empty() || self.changes == [Operation::Retain(self.len)]
327327
}
328328

329-
/// Map a position through the changes.
329+
/// Map a *sorted* list of position through the changes.
330330
///
331-
/// `assoc` indicates which size to associate the position with. `Before` will keep the
332-
/// position close to the character before, and will place it before insertions over that
333-
/// range, or at that point. `After` will move it forward, placing it at the end of such
334-
/// insertions.
335-
pub fn map_pos(&self, pos: usize, assoc: Assoc) -> usize {
331+
/// This is equivalent to updating each position with `map_pos`:
332+
///
333+
/// ``` no-compile
334+
/// for (assoc, pos) in positions {
335+
/// *pos = changes.map_pos(*pos, assoc);
336+
/// }
337+
/// ```
338+
/// However this function is significantly faster running in `O(N+M)` instead of `O(NM)`
339+
pub fn update_positions<'a>(&self, positions: impl Iterator<Item = (&'a mut usize, Assoc)>) {
336340
use Operation::*;
341+
342+
let mut positions = positions.peekable();
343+
macro_rules! map {
344+
($map: expr) => {
345+
loop {
346+
let Some((pos, assoc)) = positions.peek_mut() else { return; };
347+
let Some(new_pos) = $map(*assoc, **pos) else { break; };
348+
**pos = new_pos;
349+
positions.next();
350+
}
351+
};
352+
}
353+
337354
let mut old_pos = 0;
338355
let mut new_pos = 0;
339-
340356
let mut iter = self.changes.iter().peekable();
341357

342358
while let Some(change) = iter.next() {
@@ -348,16 +364,12 @@ impl ChangeSet {
348364

349365
match change {
350366
Retain(_) => {
351-
if old_end > pos {
352-
return new_pos + (pos - old_pos);
353-
}
367+
map!(|_, pos| (old_end > pos).then_some(new_pos + (pos - old_pos)));
354368
new_pos += len;
355369
}
356370
Delete(_) => {
357371
// in range
358-
if old_end > pos {
359-
return new_pos;
360-
}
372+
map!(|_, pos| (old_end > pos).then_some(new_pos));
361373
}
362374
Insert(s) => {
363375
let ins = s.chars().count();
@@ -368,41 +380,48 @@ impl ChangeSet {
368380

369381
old_end = old_pos + len;
370382
// in range of replaced text
371-
if old_end > pos {
383+
map!(|assoc, pos| (old_end > pos).then(|| {
372384
// at point or tracking before
373385
if pos == old_pos || assoc == Assoc::Before {
374-
return new_pos;
386+
new_pos
375387
} else {
376388
// place to end of insert
377-
return new_pos + ins;
389+
new_pos + ins
378390
}
379-
}
391+
}));
380392
} else {
381393
// at insert point
382-
if old_pos == pos {
394+
map!(|assoc, pos| (old_pos == pos).then(|| {
383395
// return position before inserted text
384396
if assoc == Assoc::Before {
385-
return new_pos;
397+
new_pos
386398
} else {
387399
// after text
388-
return new_pos + ins;
400+
new_pos + ins
389401
}
390-
}
402+
}));
391403
}
392404

393405
new_pos += ins;
394406
}
395407
}
396408
old_pos = old_end;
397409
}
410+
map!(|_, pos| (old_pos == pos).then_some(new_pos));
411+
let out_of_bounds: Vec<_> = positions.collect();
398412

399-
if pos > old_pos {
400-
panic!(
401-
"Position {} is out of range for changeset len {}!",
402-
pos, old_pos
403-
)
404-
}
405-
new_pos
413+
panic!("Positions {out_of_bounds:?} are out of range for changeset len {old_pos}!",)
414+
}
415+
416+
/// Map a position through the changes.
417+
///
418+
/// `assoc` indicates which size to associate the position with. `Before` will keep the
419+
/// position close to the character before, and will place it before insertions over that
420+
/// range, or at that point. `After` will move it forward, placing it at the end of such
421+
/// insertions.
422+
pub fn map_pos(&self, mut pos: usize, assoc: Assoc) -> usize {
423+
self.update_positions(once((&mut pos, assoc)));
424+
pos
406425
}
407426

408427
pub fn changes_iter(&self) -> ChangeIterator {

helix-lsp/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ pub mod util {
377377
.expect("transaction must be valid for primary selection");
378378
let removed_text = text.slice(removed_start..removed_end);
379379

380-
let (transaction, selection) = Transaction::change_by_selection_ignore_overlapping(
380+
let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping(
381381
doc,
382382
selection,
383383
|range| {
@@ -420,6 +420,7 @@ pub mod util {
420420
return transaction;
421421
}
422422

423+
selection = selection.map_no_normalize(changes);
423424
let mut mapped_selection = SmallVec::with_capacity(selection.len());
424425
let mut mapped_primary_idx = 0;
425426
let primary_range = selection.primary();
@@ -428,7 +429,6 @@ pub mod util {
428429
mapped_primary_idx = mapped_selection.len()
429430
}
430431

431-
let range = range.map(changes);
432432
let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty());
433433
let Some(tabstops) = tabstops else{
434434
// no tabstop normal mapping

helix-view/src/document.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,21 +1121,28 @@ impl Document {
11211121

11221122
let changes = transaction.changes();
11231123

1124+
changes.update_positions(
1125+
self.diagnostics
1126+
.iter_mut()
1127+
.map(|diagnostic| (&mut diagnostic.range.start, Assoc::After)),
1128+
);
1129+
changes.update_positions(
1130+
self.diagnostics
1131+
.iter_mut()
1132+
.map(|diagnostic| (&mut diagnostic.range.end, Assoc::After)),
1133+
);
11241134
// map state.diagnostics over changes::map_pos too
11251135
for diagnostic in &mut self.diagnostics {
1126-
diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After);
1127-
diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After);
11281136
diagnostic.line = self.text.char_to_line(diagnostic.range.start);
11291137
}
1130-
self.diagnostics
1131-
.sort_unstable_by_key(|diagnostic| diagnostic.range);
11321138

11331139
// Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place
11341140
let apply_inlay_hint_changes = |annotations: &mut Rc<[InlineAnnotation]>| {
11351141
if let Some(data) = Rc::get_mut(annotations) {
1136-
for inline in data.iter_mut() {
1137-
inline.char_idx = changes.map_pos(inline.char_idx, Assoc::After);
1138-
}
1142+
changes.update_positions(
1143+
data.iter_mut()
1144+
.map(|diagnostic| (&mut diagnostic.char_idx, Assoc::After)),
1145+
);
11391146
}
11401147
};
11411148

0 commit comments

Comments
 (0)