summaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/commands.rs225
-rw-r--r--helix-term/src/keymap/default.rs2
2 files changed, 227 insertions, 0 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 7e15965c..cac86adb 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -327,6 +327,8 @@ impl MappableCommand {
goto_declaration, "Goto declaration",
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",
@@ -5651,6 +5653,229 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
doc.apply(&transaction, view.id);
}
+#[derive(Debug, PartialEq, Eq)]
+pub enum MoveSelection {
+ Below,
+ Above,
+}
+
+#[derive(Clone)]
+struct ExtendedChange {
+ line_start: usize,
+ line_end: usize,
+ line_text: Option<Tendril>,
+ line_selection: Option<(usize, usize)>,
+}
+
+/// 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 mut last_step_changes: Vec<ExtendedChange> = vec![];
+ let mut at_doc_edge = false;
+ 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 = text.slice(line_start..line_end).to_string();
+
+ let next_line = match direction {
+ MoveSelection::Above => start.saturating_sub(1),
+ MoveSelection::Below => end + 1,
+ };
+
+ let rel_pos_anchor = range.anchor - line_start;
+ let rel_pos_head = range.head - line_start;
+ let cursor_rel_pos = (rel_pos_anchor, rel_pos_head);
+
+ if next_line == start || next_line >= text.len_lines() || at_doc_edge {
+ at_doc_edge = true;
+ let changes = vec![ExtendedChange {
+ line_start,
+ line_end,
+ line_text: Some(line_text.into()),
+ line_selection: Some(cursor_rel_pos),
+ }];
+ last_step_changes = changes.clone();
+ changes
+ } 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();
+
+ let changes = match direction {
+ MoveSelection::Above => vec![
+ ExtendedChange {
+ line_start: next_line_start,
+ line_end: next_line_end,
+ line_text: Some(line_text.into()),
+ line_selection: Some(cursor_rel_pos),
+ },
+ ExtendedChange {
+ line_start,
+ line_end,
+ line_text: Some(next_line_text.into()),
+ line_selection: None,
+ },
+ ],
+ MoveSelection::Below => vec![
+ ExtendedChange {
+ line_start,
+ line_end,
+ line_text: Some(next_line_text.into()),
+ line_selection: None,
+ },
+ ExtendedChange {
+ line_start: next_line_start,
+ line_end: next_line_end,
+ line_text: Some(line_text.into()),
+ line_selection: Some(cursor_rel_pos),
+ },
+ ],
+ };
+
+ let changes = if last_step_changes.len() > 1 {
+ evaluate_changes(last_step_changes.clone(), changes, &direction)
+ } else {
+ changes
+ };
+ last_step_changes = changes.clone();
+ changes
+ }
+ });
+
+ /// Merge changes from subsequent cursors
+ fn evaluate_changes(
+ mut last_changes: Vec<ExtendedChange>,
+ current_changes: Vec<ExtendedChange>,
+ direction: &MoveSelection,
+ ) -> Vec<ExtendedChange> {
+ let mut current_it = current_changes.into_iter();
+
+ if let (Some(mut last), Some(mut current_first), Some(current_last)) =
+ (last_changes.pop(), current_it.next(), current_it.next())
+ {
+ if last.line_start == current_first.line_start {
+ match direction {
+ MoveSelection::Above => {
+ last.line_start = current_last.line_start;
+ last.line_end = current_last.line_end;
+ if let Some(first) = last_changes.pop() {
+ last_changes.push(first)
+ }
+ last_changes.extend(vec![current_first, last]);
+ last_changes
+ }
+ MoveSelection::Below => {
+ current_first.line_start = last_changes[0].line_start;
+ current_first.line_end = last_changes[0].line_end;
+ last_changes[0] = current_first;
+ last_changes.extend(vec![last, current_last]);
+ last_changes
+ }
+ }
+ } else {
+ if let Some(first) = last_changes.pop() {
+ last_changes.push(first)
+ }
+ last_changes.extend(vec![last, current_first, current_last]);
+ last_changes
+ }
+ } else {
+ last_changes
+ }
+ }
+
+ let mut flattened: Vec<Vec<ExtendedChange>> = all_changes.into_iter().collect();
+ let last_changes = flattened.pop().unwrap_or_default();
+
+ let acc_cursors = get_adjusted_selection(doc, &last_changes, direction, at_doc_edge);
+
+ let changes = last_changes
+ .into_iter()
+ .map(|change| (change.line_start, change.line_end, change.line_text));
+
+ let new_sel = Selection::new(acc_cursors.into(), 0);
+ let transaction = Transaction::change(doc.text(), changes);
+
+ doc.apply(&transaction, view.id);
+ doc.set_selection(view.id, new_sel);
+}
+
+/// Returns selection range that is valid for the updated document
+/// This logic is necessary because it's not possible to apply changes
+/// to the document first and then set selection.
+fn get_adjusted_selection(
+ doc: &Document,
+ last_changes: &[ExtendedChange],
+ direction: MoveSelection,
+ at_doc_edge: bool,
+) -> Vec<Range> {
+ let mut first_change_len = 0;
+ let mut next_start = 0;
+ let mut acc_cursors: Vec<Range> = vec![];
+
+ for change in last_changes.iter() {
+ let change_len = change.line_text.as_ref().map_or(0, |x| x.chars().count());
+
+ if let Some((rel_anchor, rel_head)) = change.line_selection {
+ let (anchor, head) = if at_doc_edge {
+ let anchor = change.line_start + rel_anchor;
+ let head = change.line_start + rel_head;
+ (anchor, head)
+ } else {
+ match direction {
+ MoveSelection::Above => {
+ if next_start == 0 {
+ next_start = change.line_start;
+ }
+ let anchor = next_start + rel_anchor;
+ let head = next_start + rel_head;
+
+ // If there is next cursor below, selection position should be adjusted
+ // according to the length of the current line.
+ next_start += change_len + doc.line_ending.len_chars();
+ (anchor, head)
+ }
+ MoveSelection::Below => {
+ let anchor = change.line_start + first_change_len + rel_anchor - change_len;
+ let head = change.line_start + first_change_len + rel_head - change_len;
+ (anchor, head)
+ }
+ }
+ };
+
+ let cursor = Range::new(anchor, head);
+ if let Some(last) = acc_cursors.pop() {
+ if cursor.overlaps(&last) {
+ acc_cursors.push(last);
+ } else {
+ acc_cursors.push(last);
+ acc_cursors.push(cursor);
+ };
+ } else {
+ acc_cursors.push(cursor);
+ };
+ } else {
+ first_change_len = change.line_text.as_ref().map_or(0, |x| x.chars().count());
+ next_start = 0;
+ };
+ }
+ acc_cursors
+}
+
+fn move_selection_below(cx: &mut Context) {
+ move_selection(cx, MoveSelection::Below)
+}
+
+fn move_selection_above(cx: &mut Context) {
+ move_selection(cx, MoveSelection::Above)
+}
+
enum IncrementDirection {
Increase,
Decrease,
diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs
index 58e8fdad..9b93c573 100644
--- a/helix-term/src/keymap/default.rs
+++ b/helix-term/src/keymap/default.rs
@@ -324,6 +324,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"C-a" => increment,
"C-x" => decrement,
+ "C-k" => move_selection_above,
+ "C-j" => move_selection_below,
});
let mut select = normal.clone();
select.merge_nodes(keymap!({ "Select mode"