Skip to content

Commit f7e7363

Browse files
committed
Add support for moving lines and selections above and below
1 parent 2457111 commit f7e7363

File tree

4 files changed

+183
-2
lines changed

4 files changed

+183
-2
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

helix-term/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ helix-loader = { version = "0.6", path = "../helix-loader" }
3333

3434
anyhow = "1"
3535
once_cell = "1.15"
36+
itertools = "0.10.5"
3637

3738
which = "4.2"
3839

helix-term/src/commands.rs

Lines changed: 170 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ use helix_core::{
2323
selection, shellwords, surround, textobject,
2424
tree_sitter::Node,
2525
unicode::width::UnicodeWidthChar,
26-
visual_coords_at_pos, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection,
27-
SmallVec, Tendril, Transaction,
26+
visual_coords_at_pos, Change, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice,
27+
Selection, SmallVec, Tendril, Transaction,
2828
};
2929
use helix_view::{
3030
apply_transaction,
@@ -67,6 +67,8 @@ use serde::de::{self, Deserialize, Deserializer};
6767
use grep_regex::RegexMatcherBuilder;
6868
use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
6969
use ignore::{DirEntry, WalkBuilder, WalkState};
70+
use itertools::FoldWhile::{Continue, Done};
71+
use itertools::Itertools;
7072
use tokio_stream::wrappers::UnboundedReceiverStream;
7173

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

47824786
let changes = selection.into_iter().map(|range| {
47834787
let (start, end) = range.line_range(slice);
4788+
4789+
log::info!("Selection: {}, {}", start, end);
47844790
let line = match open {
47854791
Open::Above => start,
47864792
Open::Below => end + 1,
@@ -4797,6 +4803,168 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
47974803
apply_transaction(&transaction, doc, view);
47984804
}
47994805

4806+
#[derive(Debug, PartialEq, Eq)]
4807+
pub enum MoveSelection {
4808+
Below,
4809+
Above,
4810+
}
4811+
4812+
/// Predict where selection cursor should be after moving the code block up or down.
4813+
/// This function makes it look like the selection didn't change relative
4814+
/// to the text that have been moved.
4815+
fn get_adjusted_selection_pos(
4816+
doc: &Document,
4817+
// text: &Rope,
4818+
range: Range,
4819+
pos: usize,
4820+
direction: &MoveSelection,
4821+
) -> usize {
4822+
let text = doc.text();
4823+
let slice = text.slice(..);
4824+
let (selection_start_line, selection_end_line) = range.line_range(slice);
4825+
let next_line = match direction {
4826+
MoveSelection::Above => selection_start_line.saturating_sub(1),
4827+
MoveSelection::Below => selection_end_line + 1,
4828+
};
4829+
if next_line == selection_start_line || next_line >= text.len_lines() {
4830+
pos
4831+
} else {
4832+
let next_line_len = {
4833+
// This omits the next line (above or below) when counting the future position of head/anchor
4834+
let line_start = text.line_to_char(next_line);
4835+
let line_end = line_end_char_index(&slice, next_line);
4836+
line_end.saturating_sub(line_start)
4837+
};
4838+
4839+
let cursor = coords_at_pos(slice, pos);
4840+
let pos_line = text.char_to_line(pos);
4841+
let start_line_pos = text.line_to_char(pos_line);
4842+
let ending_len = doc.line_ending.len_chars();
4843+
match direction {
4844+
MoveSelection::Above => start_line_pos + cursor.col - next_line_len - ending_len,
4845+
MoveSelection::Below => start_line_pos + cursor.col + next_line_len + ending_len,
4846+
}
4847+
}
4848+
}
4849+
4850+
/// Move line or block of text in specified direction.
4851+
/// The function respects single line, single selection, multiple lines using
4852+
/// several cursors and multiple selections.
4853+
fn move_selection(cx: &mut Context, direction: MoveSelection) {
4854+
let (view, doc) = current!(cx.editor);
4855+
let selection = doc.selection(view.id);
4856+
let text = doc.text();
4857+
let slice = text.slice(..);
4858+
let all_changes = selection.into_iter().map(|range| {
4859+
let (start, end) = range.line_range(slice);
4860+
let line_start = text.line_to_char(start);
4861+
let line_end = line_end_char_index(&slice, end);
4862+
let line = text.slice(line_start..line_end).to_string();
4863+
4864+
let next_line = match direction {
4865+
MoveSelection::Above => start.saturating_sub(1),
4866+
MoveSelection::Below => end + 1,
4867+
};
4868+
4869+
if next_line == start || next_line >= text.len_lines() {
4870+
vec![(line_start, line_end, Some(line.into()))]
4871+
} else {
4872+
let next_line_start = text.line_to_char(next_line);
4873+
let next_line_end = line_end_char_index(&slice, next_line);
4874+
4875+
let next_line_text = text.slice(next_line_start..next_line_end).to_string();
4876+
4877+
match direction {
4878+
MoveSelection::Above => vec![
4879+
(next_line_start, next_line_end, Some(line.into())),
4880+
(line_start, line_end, Some(next_line_text.into())),
4881+
],
4882+
MoveSelection::Below => vec![
4883+
(line_start, line_end, Some(next_line_text.into())),
4884+
(next_line_start, next_line_end, Some(line.into())),
4885+
],
4886+
}
4887+
}
4888+
});
4889+
4890+
// Conflicts might arise when two cursors are pointing to adjacent lines.
4891+
// The resulting change vector would contain two changes referring the same lines,
4892+
// which would make the transaction to panic.
4893+
// Conflicts are resolved by picking only the top change in such case.
4894+
fn remove_conflicts(changes: Vec<Change>) -> Vec<Change> {
4895+
if changes.len() > 2 {
4896+
changes
4897+
.into_iter()
4898+
.fold_while(vec![], |mut acc: Vec<Change>, change| {
4899+
if let Some(last_change) = acc.pop() {
4900+
if last_change.0 >= change.0 || last_change.1 >= change.1 {
4901+
acc.push(last_change);
4902+
Done(acc)
4903+
} else {
4904+
acc.push(last_change);
4905+
acc.push(change);
4906+
Continue(acc)
4907+
}
4908+
} else {
4909+
acc.push(change);
4910+
Continue(acc)
4911+
}
4912+
})
4913+
.into_inner()
4914+
} else {
4915+
changes
4916+
}
4917+
}
4918+
let flat: Vec<Change> = all_changes.into_iter().flatten().unique().collect();
4919+
let filtered = remove_conflicts(flat);
4920+
4921+
let new_selection = selection.clone().transform(|range| {
4922+
let anchor_pos = get_adjusted_selection_pos(doc, range, range.anchor, &direction);
4923+
let head_pos = get_adjusted_selection_pos(doc, range, range.head, &direction);
4924+
4925+
Range::new(anchor_pos, head_pos)
4926+
});
4927+
let transaction = Transaction::change(doc.text(), filtered.into_iter());
4928+
4929+
// Analogically to the conflicting line changes, selections can also panic
4930+
// in case the ranges would overlap.
4931+
// Only one selection is returned to prevent that.
4932+
let selections_collide = || -> bool {
4933+
let mut last: Option<Range> = None;
4934+
for range in new_selection.iter() {
4935+
let line = range.cursor_line(slice);
4936+
match last {
4937+
Some(last_r) => {
4938+
let last_line = last_r.cursor_line(slice);
4939+
if range.overlaps(&last_r) || last_line + 1 == line || last_line == line {
4940+
return true;
4941+
} else {
4942+
last = Some(*range);
4943+
};
4944+
}
4945+
None => last = Some(*range),
4946+
};
4947+
}
4948+
false
4949+
};
4950+
let cleaned_selection = if new_selection.len() > 1 && selections_collide() {
4951+
new_selection.into_single()
4952+
} else {
4953+
new_selection
4954+
};
4955+
4956+
apply_transaction(&transaction, doc, view);
4957+
doc.set_selection(view.id, cleaned_selection);
4958+
}
4959+
4960+
fn move_selection_below(cx: &mut Context) {
4961+
move_selection(cx, MoveSelection::Below)
4962+
}
4963+
4964+
fn move_selection_above(cx: &mut Context) {
4965+
move_selection(cx, MoveSelection::Above)
4966+
}
4967+
48004968
/// Increment object under cursor by count.
48014969
fn increment(cx: &mut Context) {
48024970
increment_impl(cx, cx.count() as i64);

helix-term/src/keymap/default.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
311311

312312
"C-a" => increment,
313313
"C-x" => decrement,
314+
"C-up" => move_selection_above,
315+
"C-down" => move_selection_below,
314316
});
315317
let mut select = normal.clone();
316318
select.merge_nodes(keymap!({ "Select mode"

0 commit comments

Comments
 (0)