Skip to content

Commit

Permalink
Add support for moving lines and selections above and below
Browse files Browse the repository at this point in the history
  • Loading branch information
sireliah committed Oct 31, 2022
1 parent 2457111 commit d5b260e
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 4 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions helix-term/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ helix-loader = { version = "0.6", path = "../helix-loader" }

anyhow = "1"
once_cell = "1.15"
itertools = "0.10.5"

which = "4.2"

Expand Down
172 changes: 170 additions & 2 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ use helix_core::{
selection, shellwords, surround, textobject,
tree_sitter::Node,
unicode::width::UnicodeWidthChar,
visual_coords_at_pos, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection,
SmallVec, Tendril, Transaction,
visual_coords_at_pos, Change, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice,
Selection, SmallVec, Tendril, Transaction,
};
use helix_view::{
apply_transaction,
Expand Down Expand Up @@ -67,6 +67,8 @@ use serde::de::{self, Deserialize, Deserializer};
use grep_regex::RegexMatcherBuilder;
use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
use ignore::{DirEntry, WalkBuilder, WalkState};
use itertools::FoldWhile::{Continue, Done};
use itertools::Itertools;
use tokio_stream::wrappers::UnboundedReceiverStream;

pub struct Context<'a> {
Expand Down Expand Up @@ -284,6 +286,8 @@ impl MappableCommand {
goto_definition, "Goto definition",
add_newline_above, "Add newline above",
add_newline_below, "Add newline below",
move_selection_above, "Move current line or selection up",
move_selection_below, "Move current line or selection down",
goto_type_definition, "Goto type definition",
goto_implementation, "Goto implementation",
goto_file_start, "Goto line number <n> else file start",
Expand Down Expand Up @@ -4781,6 +4785,8 @@ fn add_newline_impl(cx: &mut Context, open: Open) {

let changes = selection.into_iter().map(|range| {
let (start, end) = range.line_range(slice);

log::info!("Selection: {}, {}", start, end);
let line = match open {
Open::Above => start,
Open::Below => end + 1,
Expand All @@ -4797,6 +4803,168 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
apply_transaction(&transaction, doc, view);
}

#[derive(Debug, PartialEq, Eq)]
pub enum MoveSelection {
Below,
Above,
}

/// Predict where selection cursor should be after moving the code block up or down.
/// This function makes it look like the selection didn't change relative
/// to the text that have been moved.
fn get_adjusted_selection_pos(
doc: &Document,
// text: &Rope,
range: Range,
pos: usize,
direction: &MoveSelection,
) -> usize {
let text = doc.text();
let slice = text.slice(..);
let (selection_start_line, selection_end_line) = range.line_range(slice);
let next_line = match direction {
MoveSelection::Above => selection_start_line.saturating_sub(1),
MoveSelection::Below => selection_end_line + 1,
};
if next_line == selection_start_line || next_line >= text.len_lines() {
pos
} else {
let next_line_len = {
// This omits the next line (above or below) when counting the future position of head/anchor
let line_start = text.line_to_char(next_line);
let line_end = line_end_char_index(&slice, next_line);
line_end.saturating_sub(line_start)
};

let cursor = coords_at_pos(slice, pos);
let pos_line = text.char_to_line(pos);
let start_line_pos = text.line_to_char(pos_line);
let ending_len = doc.line_ending.len_chars();
match direction {
MoveSelection::Above => start_line_pos + cursor.col - next_line_len - ending_len,
MoveSelection::Below => start_line_pos + cursor.col + next_line_len + ending_len,
}
}
}

/// Move line or block of text in specified direction.
/// The function respects single line, single selection, multiple lines using
/// several cursors and multiple selections.
fn move_selection(cx: &mut Context, direction: MoveSelection) {
let (view, doc) = current!(cx.editor);
let selection = doc.selection(view.id);
let text = doc.text();
let slice = text.slice(..);
let all_changes = selection.into_iter().map(|range| {
let (start, end) = range.line_range(slice);
let line_start = text.line_to_char(start);
let line_end = line_end_char_index(&slice, end);
let line = text.slice(line_start..line_end).to_string();

let next_line = match direction {
MoveSelection::Above => start.saturating_sub(1),
MoveSelection::Below => end + 1,
};

if next_line == start || next_line >= text.len_lines() {
vec![(line_start, line_end, Some(line.into()))]
} else {
let next_line_start = text.line_to_char(next_line);
let next_line_end = line_end_char_index(&slice, next_line);

let next_line_text = text.slice(next_line_start..next_line_end).to_string();

match direction {
MoveSelection::Above => vec![
(next_line_start, next_line_end, Some(line.into())),
(line_start, line_end, Some(next_line_text.into())),
],
MoveSelection::Below => vec![
(line_start, line_end, Some(next_line_text.into())),
(next_line_start, next_line_end, Some(line.into())),
],
}
}
});

// Conflicts might arise when two cursors are pointing to adjacent lines.
// The resulting change vector would contain two changes referring the same lines,
// which would make the transaction to panic.
// Conflicts are resolved by picking only the top change in such case.
fn remove_conflicts(changes: Vec<Change>) -> Vec<Change> {
if changes.len() > 2 {
changes
.into_iter()
.fold_while(vec![], |mut acc: Vec<Change>, change| {
if let Some(last_change) = acc.pop() {
if last_change.0 >= change.0 || last_change.1 >= change.1 {
acc.push(last_change);
Done(acc)
} else {
acc.push(last_change);
acc.push(change);
Continue(acc)
}
} else {
acc.push(change);
Continue(acc)
}
})
.into_inner()
} else {
changes
}
}
let flat: Vec<Change> = all_changes.into_iter().flatten().unique().collect();
let filtered = remove_conflicts(flat);

let new_selection = selection.clone().transform(|range| {
let anchor_pos = get_adjusted_selection_pos(&doc, range, range.anchor, &direction);
let head_pos = get_adjusted_selection_pos(&doc, range, range.head, &direction);

Range::new(anchor_pos, head_pos)
});
let transaction = Transaction::change(doc.text(), filtered.into_iter());

// Analogically to the conflicting line changes, selections can also panic
// in case the ranges would overlap.
// Only one selection is returned to prevent that.
let selections_collide = || -> bool {
let mut last: Option<Range> = None;
for range in new_selection.iter() {
let line = range.cursor_line(slice);
match last {
Some(last_r) => {
let last_line = last_r.cursor_line(slice);
if range.overlaps(&last_r) || last_line + 1 == line || last_line == line {
return true;
} else {
last = Some(*range);
};
}
None => last = Some(*range),
};
}
false
};
let cleaned_selection = if new_selection.len() > 1 && selections_collide() {
new_selection.into_single()
} else {
new_selection
};

apply_transaction(&transaction, doc, view);
doc.set_selection(view.id, cleaned_selection);
}

fn move_selection_below(cx: &mut Context) {
move_selection(cx, MoveSelection::Below)
}

fn move_selection_above(cx: &mut Context) {
move_selection(cx, MoveSelection::Above)
}

/// Increment object under cursor by count.
fn increment(cx: &mut Context) {
increment_impl(cx, cx.count() as i64);
Expand Down
6 changes: 4 additions & 2 deletions helix-term/src/keymap/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"S" => split_selection,
";" => collapse_selection,
"A-;" => flip_selections,
"A-o" | "A-up" => expand_selection,
"A-i" | "A-down" => shrink_selection,
"A-o" => expand_selection,
"A-i" => shrink_selection,
"A-p" | "A-left" => select_prev_sibling,
"A-n" | "A-right" => select_next_sibling,

Expand Down Expand Up @@ -311,6 +311,8 @@ pub fn default() -> HashMap<Mode, Keymap> {

"C-a" => increment,
"C-x" => decrement,
"C-up" => move_selection_above,
"C-down" => move_selection_below,
});
let mut select = normal.clone();
select.merge_nodes(keymap!({ "Select mode"
Expand Down

0 comments on commit d5b260e

Please # to comment.