Skip to content

Commit 7cf597f

Browse files
committed
Implement line change evaluation
1 parent b19db53 commit 7cf597f

File tree

1 file changed

+152
-108
lines changed

1 file changed

+152
-108
lines changed

helix-term/src/commands.rs

Lines changed: 152 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,10 @@ use helix_core::{
2828
textobject,
2929
tree_sitter::Node,
3030
unicode::width::UnicodeWidthChar,
31-
visual_offset_from_block, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes,
31+
visual_offset_from_block, Change, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes,
3232
RopeSlice, Selection, SmallVec, Tendril, Transaction,
3333
};
3434
use helix_view::{
35-
apply_transaction,
3635
clipboard::ClipboardType,
3736
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
3837
editor::{Action, CompleteAction, Motion},
@@ -77,8 +76,6 @@ use serde::de::{self, Deserialize, Deserializer};
7776
use grep_regex::RegexMatcherBuilder;
7877
use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
7978
use ignore::{DirEntry, WalkBuilder, WalkState};
80-
use itertools::FoldWhile::{Continue, Done};
81-
use itertools::Itertools;
8279
use tokio_stream::wrappers::UnboundedReceiverStream;
8380

8481
pub type OnKeyCallback = Box<dyn FnOnce(&mut Context, KeyEvent)>;
@@ -5470,43 +5467,7 @@ pub enum MoveSelection {
54705467
Above,
54715468
}
54725469

5473-
/// Predict where selection cursor should be after moving the code block up or down.
5474-
/// This function makes it look like the selection didn't change relative
5475-
/// to the text that have been moved.
5476-
fn get_adjusted_selection_pos(
5477-
doc: &Document,
5478-
// text: &Rope,
5479-
range: Range,
5480-
pos: usize,
5481-
direction: &MoveSelection,
5482-
) -> usize {
5483-
let text = doc.text();
5484-
let slice = text.slice(..);
5485-
let (selection_start_line, selection_end_line) = range.line_range(slice);
5486-
let next_line = match direction {
5487-
MoveSelection::Above => selection_start_line.saturating_sub(1),
5488-
MoveSelection::Below => selection_end_line + 1,
5489-
};
5490-
if next_line == selection_start_line || next_line >= text.len_lines() {
5491-
pos
5492-
} else {
5493-
let next_line_len = {
5494-
// This omits the next line (above or below) when counting the future position of head/anchor
5495-
let line_start = text.line_to_char(next_line);
5496-
let line_end = line_end_char_index(&slice, next_line);
5497-
line_end.saturating_sub(line_start)
5498-
};
5499-
5500-
let cursor = coords_at_pos(slice, pos);
5501-
let pos_line = text.char_to_line(pos);
5502-
let start_line_pos = text.line_to_char(pos_line);
5503-
let ending_len = doc.line_ending.len_chars();
5504-
match direction {
5505-
MoveSelection::Above => start_line_pos + cursor.col - next_line_len - ending_len,
5506-
MoveSelection::Below => start_line_pos + cursor.col + next_line_len + ending_len,
5507-
}
5508-
}
5509-
}
5470+
type ExtendedChange = (usize, usize, Option<Tendril>, Option<(usize, usize)>);
55105471

55115472
/// Move line or block of text in specified direction.
55125473
/// The function respects single line, single selection, multiple lines using
@@ -5516,6 +5477,8 @@ fn move_selection(cx: &mut Context, direction: MoveSelection) {
55165477
let selection = doc.selection(view.id);
55175478
let text = doc.text();
55185479
let slice = text.slice(..);
5480+
let mut last_step_changes: Vec<ExtendedChange> = vec![];
5481+
let mut at_doc_edge = false;
55195482
let all_changes = selection.into_iter().map(|range| {
55205483
let (start, end) = range.line_range(slice);
55215484
let line_start = text.line_to_char(start);
@@ -5527,95 +5490,176 @@ fn move_selection(cx: &mut Context, direction: MoveSelection) {
55275490
MoveSelection::Below => end + 1,
55285491
};
55295492

5530-
if next_line == start || next_line >= text.len_lines() {
5531-
vec![(line_start, line_end, Some(line.into()))]
5493+
let rel_pos_anchor = range.anchor - line_start;
5494+
let rel_pos_head = range.head - line_start;
5495+
5496+
if next_line == start || next_line >= text.len_lines() || at_doc_edge {
5497+
at_doc_edge = true;
5498+
let cursor_rel_pos = (rel_pos_anchor, rel_pos_head);
5499+
let changes = vec![(
5500+
line_start,
5501+
line_end,
5502+
Some(line.into()),
5503+
Some(cursor_rel_pos),
5504+
)];
5505+
last_step_changes = changes.clone();
5506+
changes
55325507
} else {
55335508
let next_line_start = text.line_to_char(next_line);
55345509
let next_line_end = line_end_char_index(&slice, next_line);
5535-
55365510
let next_line_text = text.slice(next_line_start..next_line_end).to_string();
55375511

5538-
match direction {
5512+
let cursor_rel_pos = (rel_pos_anchor, rel_pos_head);
5513+
let changes = match direction {
55395514
MoveSelection::Above => vec![
5540-
(next_line_start, next_line_end, Some(line.into())),
5541-
(line_start, line_end, Some(next_line_text.into())),
5515+
(
5516+
next_line_start,
5517+
next_line_end,
5518+
Some(line.into()),
5519+
Some(cursor_rel_pos),
5520+
),
5521+
(line_start, line_end, Some(next_line_text.into()), None),
55425522
],
55435523
MoveSelection::Below => vec![
5544-
(line_start, line_end, Some(next_line_text.into())),
5545-
(next_line_start, next_line_end, Some(line.into())),
5524+
(line_start, line_end, Some(next_line_text.into()), None),
5525+
(
5526+
next_line_start,
5527+
next_line_end,
5528+
Some(line.into()),
5529+
Some(cursor_rel_pos),
5530+
),
55465531
],
5547-
}
5532+
};
5533+
5534+
let changes = if last_step_changes.len() > 1 {
5535+
evaluate_changes(last_step_changes.clone(), changes.clone(), &direction)
5536+
} else {
5537+
changes
5538+
};
5539+
last_step_changes = changes.clone();
5540+
changes
55485541
}
55495542
});
55505543

5551-
// Conflicts might arise when two cursors are pointing to adjacent lines.
5552-
// The resulting change vector would contain two changes referring the same lines,
5553-
// which would make the transaction to panic.
5554-
// Conflicts are resolved by picking only the top change in such case.
5555-
fn remove_conflicts(changes: Vec<Change>) -> Vec<Change> {
5556-
if changes.len() > 2 {
5557-
changes
5558-
.into_iter()
5559-
.fold_while(vec![], |mut acc: Vec<Change>, change| {
5560-
if let Some(last_change) = acc.pop() {
5561-
if last_change.0 >= change.0 || last_change.1 >= change.1 {
5562-
acc.push(last_change);
5563-
Done(acc)
5564-
} else {
5565-
acc.push(last_change);
5566-
acc.push(change);
5567-
Continue(acc)
5544+
/// Merge changes from subsequent cursors
5545+
fn evaluate_changes(
5546+
mut last_changes: Vec<ExtendedChange>,
5547+
current_changes: Vec<ExtendedChange>,
5548+
direction: &MoveSelection,
5549+
) -> Vec<ExtendedChange> {
5550+
let mut current_it = current_changes.into_iter();
5551+
5552+
if let (Some(mut last), Some(mut current_first), Some(current_last)) =
5553+
(last_changes.pop(), current_it.next(), current_it.next())
5554+
{
5555+
if last.0 == current_first.0 {
5556+
match direction {
5557+
MoveSelection::Above => {
5558+
last.0 = current_last.0;
5559+
last.1 = current_last.1;
5560+
if let Some(first) = last_changes.pop() {
5561+
last_changes.push(first)
55685562
}
5569-
} else {
5570-
acc.push(change);
5571-
Continue(acc)
5563+
last_changes.extend(vec![current_first, last.to_owned()]);
5564+
last_changes
55725565
}
5573-
})
5574-
.into_inner()
5566+
MoveSelection::Below => {
5567+
current_first.0 = last_changes[0].0;
5568+
current_first.1 = last_changes[0].1;
5569+
last_changes[0] = current_first;
5570+
last_changes.extend(vec![last.to_owned(), current_last]);
5571+
last_changes
5572+
}
5573+
}
5574+
} else {
5575+
if let Some(first) = last_changes.pop() {
5576+
last_changes.push(first)
5577+
}
5578+
last_changes.extend(vec![last.to_owned(), current_first, current_last]);
5579+
last_changes
5580+
}
55755581
} else {
5576-
changes
5582+
last_changes
55775583
}
55785584
}
5579-
let flat: Vec<Change> = all_changes.into_iter().flatten().unique().collect();
5580-
let filtered = remove_conflicts(flat);
55815585

5582-
let new_selection = selection.clone().transform(|range| {
5583-
let anchor_pos = get_adjusted_selection_pos(doc, range, range.anchor, &direction);
5584-
let head_pos = get_adjusted_selection_pos(doc, range, range.head, &direction);
5586+
let mut flattened: Vec<Vec<ExtendedChange>> = all_changes.into_iter().collect();
5587+
let last_changes = flattened.pop().unwrap_or(vec![]);
55855588

5586-
Range::new(anchor_pos, head_pos)
5587-
});
5588-
let transaction = Transaction::change(doc.text(), filtered.into_iter());
5589-
5590-
// Analogically to the conflicting line changes, selections can also panic
5591-
// in case the ranges would overlap.
5592-
// Only one selection is returned to prevent that.
5593-
let selections_collide = || -> bool {
5594-
let mut last: Option<Range> = None;
5595-
for range in new_selection.iter() {
5596-
let line = range.cursor_line(slice);
5597-
match last {
5598-
Some(last_r) => {
5599-
let last_line = last_r.cursor_line(slice);
5600-
if range.overlaps(&last_r) || last_line + 1 == line || last_line == line {
5601-
return true;
5602-
} else {
5603-
last = Some(*range);
5604-
};
5589+
let acc_cursors = get_adjusted_selection(&doc, &last_changes, direction, at_doc_edge);
5590+
5591+
let changes: Vec<Change> = last_changes
5592+
.into_iter()
5593+
.map(|change| (change.0, change.1, change.2.to_owned()))
5594+
.collect();
5595+
5596+
let new_sel = Selection::new(acc_cursors.into(), 0);
5597+
let transaction = Transaction::change(doc.text(), changes.into_iter());
5598+
5599+
doc.apply(&transaction, view.id);
5600+
doc.set_selection(view.id, new_sel);
5601+
}
5602+
5603+
/// Returns selection range that is valid for the updated document
5604+
/// This logic is necessary because it's not possible to apply changes
5605+
/// to the document first and then set selection.
5606+
fn get_adjusted_selection(
5607+
doc: &Document,
5608+
last_changes: &Vec<ExtendedChange>,
5609+
direction: MoveSelection,
5610+
at_doc_edge: bool,
5611+
) -> Vec<Range> {
5612+
let mut first_change_len = 0;
5613+
let mut next_start = 0;
5614+
let mut acc_cursors: Vec<Range> = vec![];
5615+
5616+
for change in last_changes.iter() {
5617+
let change_len = change.2.as_ref().map_or(0, |x| x.len());
5618+
5619+
if let Some((rel_anchor, rel_head)) = change.3 {
5620+
let (anchor, head) = if at_doc_edge {
5621+
let anchor = change.0 + rel_anchor;
5622+
let head = change.0 + rel_head;
5623+
(anchor, head)
5624+
} else {
5625+
match direction {
5626+
MoveSelection::Above => {
5627+
if next_start == 0 {
5628+
next_start = change.0;
5629+
}
5630+
let anchor = next_start + rel_anchor;
5631+
let head = next_start + rel_head;
5632+
5633+
// If there is next cursor below, selection position should be adjusted
5634+
// according to the length of the current line.
5635+
next_start += change_len + doc.line_ending.len_chars();
5636+
(anchor, head)
5637+
}
5638+
MoveSelection::Below => {
5639+
let anchor = change.0 + first_change_len + rel_anchor - change_len;
5640+
let head = change.0 + first_change_len + rel_head - change_len;
5641+
(anchor, head)
5642+
}
56055643
}
5606-
None => last = Some(*range),
56075644
};
5608-
}
5609-
false
5610-
};
5611-
let cleaned_selection = if new_selection.len() > 1 && selections_collide() {
5612-
new_selection.into_single()
5613-
} else {
5614-
new_selection
5615-
};
56165645

5617-
apply_transaction(&transaction, doc, view);
5618-
doc.set_selection(view.id, cleaned_selection);
5646+
let cursor = Range::new(anchor, head);
5647+
if let Some(last) = acc_cursors.pop() {
5648+
if cursor.overlaps(&last) {
5649+
acc_cursors.push(last);
5650+
} else {
5651+
acc_cursors.push(last);
5652+
acc_cursors.push(cursor);
5653+
};
5654+
} else {
5655+
acc_cursors.push(cursor);
5656+
};
5657+
} else {
5658+
first_change_len = change.2.as_ref().map_or(0, |x| x.len());
5659+
next_start = 0;
5660+
};
5661+
}
5662+
acc_cursors
56195663
}
56205664

56215665
fn move_selection_below(cx: &mut Context) {

0 commit comments

Comments
 (0)