Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/commands.rs')
-rw-r--r--helix-term/src/commands.rs348
1 files changed, 236 insertions, 112 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 430d4430..c74c7429 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -530,6 +530,7 @@ impl MappableCommand {
select_prev_sibling, "Select previous sibling the in syntax tree",
select_all_siblings, "Select all siblings of the current node",
select_all_children, "Select all children of the current node",
+ expand_selection_around, "Expand selection to parent syntax node, but exclude the selection you started with",
jump_forward, "Jump forward on jumplist",
jump_backward, "Jump backward on jumplist",
save_selection, "Save current selection to jumplist",
@@ -3870,7 +3871,6 @@ fn goto_column_impl(cx: &mut Context, movement: Movement) {
let pos = graphemes::nth_next_grapheme_boundary(text, line_start, count - 1).min(line_end);
range.put_cursor(text, pos, movement == Movement::Extend)
});
- push_jump(view, doc);
doc.set_selection(view.id, selection);
}
@@ -3892,7 +3892,6 @@ fn goto_last_modification(cx: &mut Context) {
.selection(view.id)
.clone()
.transform(|range| range.put_cursor(text, pos, cx.editor.mode == Mode::Select));
- push_jump(view, doc);
doc.set_selection(view.id, selection);
}
}
@@ -3944,7 +3943,7 @@ fn goto_first_diag(cx: &mut Context) {
Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return,
};
- push_jump(view, doc);
+
doc.set_selection(view.id, selection);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
@@ -3956,7 +3955,7 @@ fn goto_last_diag(cx: &mut Context) {
Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return,
};
- push_jump(view, doc);
+
doc.set_selection(view.id, selection);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
@@ -3980,7 +3979,6 @@ fn goto_next_diag(cx: &mut Context) {
Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return,
};
- push_jump(view, doc);
doc.set_selection(view.id, selection);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
@@ -4010,11 +4008,11 @@ fn goto_prev_diag(cx: &mut Context) {
Some(diag) => Selection::single(diag.range.end, diag.range.start),
None => return,
};
- push_jump(view, doc);
doc.set_selection(view.id, selection);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
};
+
cx.editor.apply_motion(motion)
}
@@ -4041,7 +4039,6 @@ fn goto_first_change_impl(cx: &mut Context, reverse: bool) {
};
if hunk != Hunk::NONE {
let range = hunk_range(hunk, doc.text().slice(..));
- push_jump(view, doc);
doc.set_selection(view.id, Selection::single(range.anchor, range.head));
}
}
@@ -4097,7 +4094,6 @@ fn goto_next_change_impl(cx: &mut Context, direction: Direction) {
}
});
- push_jump(view, doc);
doc.set_selection(view.id, selection)
};
cx.editor.apply_motion(motion);
@@ -4135,16 +4131,6 @@ pub mod insert {
}
}
- // The default insert hook: simply insert the character
- #[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
- fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
- let cursors = selection.clone().cursors(doc.slice(..));
- let mut t = Tendril::new();
- t.push(ch);
- let transaction = Transaction::insert(doc, &cursors, t);
- Some(transaction)
- }
-
use helix_core::auto_pairs;
use helix_view::editor::SmartTabConfig;
@@ -4154,15 +4140,25 @@ pub mod insert {
let selection = doc.selection(view.id);
let auto_pairs = doc.auto_pairs(cx.editor);
- let transaction = auto_pairs
- .as_ref()
- .and_then(|ap| auto_pairs::hook(text, selection, c, ap))
- .or_else(|| insert(text, selection, c));
+ let insert_char = |range: Range, ch: char| {
+ let cursor = range.cursor(text.slice(..));
+ let t = Tendril::from_iter([ch]);
+ ((cursor, cursor, Some(t)), None)
+ };
- let (view, doc) = current!(cx.editor);
- if let Some(t) = transaction {
- doc.apply(&t, view.id);
- }
+ let transaction = Transaction::change_by_and_with_selection(text, selection, |range| {
+ auto_pairs
+ .as_ref()
+ .and_then(|ap| {
+ auto_pairs::hook_insert(text, range, c, ap)
+ .map(|(change, range)| (change, Some(range)))
+ .or_else(|| Some(insert_char(*range, c)))
+ })
+ .unwrap_or_else(|| insert_char(*range, c))
+ });
+
+ let doc = doc_mut!(cx.editor, &doc.id());
+ doc.apply(&transaction, view.id);
helix_event::dispatch(PostInsertChar { c, cx });
}
@@ -4406,82 +4402,96 @@ pub mod insert {
doc.apply(&transaction, view.id);
}
+ fn dedent(doc: &Document, range: &Range) -> Option<Deletion> {
+ let text = doc.text().slice(..);
+ let pos = range.cursor(text);
+ let line_start_pos = text.line_to_char(range.cursor_line(text));
+
+ // consider to delete by indent level if all characters before `pos` are indent units.
+ let fragment = Cow::from(text.slice(line_start_pos..pos));
+
+ if fragment.is_empty() || !fragment.chars().all(|ch| ch == ' ' || ch == '\t') {
+ return None;
+ }
+
+ if text.get_char(pos.saturating_sub(1)) == Some('\t') {
+ // fast path, delete one char
+ return Some((graphemes::nth_prev_grapheme_boundary(text, pos, 1), pos));
+ }
+
+ let tab_width = doc.tab_width();
+ let indent_width = doc.indent_width();
+
+ let width: usize = fragment
+ .chars()
+ .map(|ch| {
+ if ch == '\t' {
+ tab_width
+ } else {
+ // it can be none if it still meet control characters other than '\t'
+ // here just set the width to 1 (or some value better?).
+ ch.width().unwrap_or(1)
+ }
+ })
+ .sum();
+
+ // round down to nearest unit
+ let mut drop = width % indent_width;
+
+ // if it's already at a unit, consume a whole unit
+ if drop == 0 {
+ drop = indent_width
+ };
+
+ let mut chars = fragment.chars().rev();
+ let mut start = pos;
+
+ for _ in 0..drop {
+ // delete up to `drop` spaces
+ match chars.next() {
+ Some(' ') => start -= 1,
+ _ => break,
+ }
+ }
+
+ Some((start, pos)) // delete!
+ }
+
pub fn delete_char_backward(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current_ref!(cx.editor);
let text = doc.text().slice(..);
- let tab_width = doc.tab_width();
- let indent_width = doc.indent_width();
- let auto_pairs = doc.auto_pairs(cx.editor);
- let transaction =
- Transaction::delete_by_selection(doc.text(), doc.selection(view.id), |range| {
+ let transaction = Transaction::delete_by_and_with_selection(
+ doc.text(),
+ doc.selection(view.id),
+ |range| {
let pos = range.cursor(text);
+
+ log::debug!("cursor: {}, len: {}", pos, text.len_chars());
+
if pos == 0 {
- return (pos, pos);
- }
- let line_start_pos = text.line_to_char(range.cursor_line(text));
- // consider to delete by indent level if all characters before `pos` are indent units.
- let fragment = Cow::from(text.slice(line_start_pos..pos));
- if !fragment.is_empty() && fragment.chars().all(|ch| ch == ' ' || ch == '\t') {
- if text.get_char(pos.saturating_sub(1)) == Some('\t') {
- // fast path, delete one char
- (graphemes::nth_prev_grapheme_boundary(text, pos, 1), pos)
- } else {
- let width: usize = fragment
- .chars()
- .map(|ch| {
- if ch == '\t' {
- tab_width
- } else {
- // it can be none if it still meet control characters other than '\t'
- // here just set the width to 1 (or some value better?).
- ch.width().unwrap_or(1)
- }
- })
- .sum();
- let mut drop = width % indent_width; // round down to nearest unit
- if drop == 0 {
- drop = indent_width
- }; // if it's already at a unit, consume a whole unit
- let mut chars = fragment.chars().rev();
- let mut start = pos;
- for _ in 0..drop {
- // delete up to `drop` spaces
- match chars.next() {
- Some(' ') => start -= 1,
- _ => break,
- }
- }
- (start, pos) // delete!
- }
- } else {
- match (
- text.get_char(pos.saturating_sub(1)),
- text.get_char(pos),
- auto_pairs,
- ) {
- (Some(_x), Some(_y), Some(ap))
- if range.is_single_grapheme(text)
- && ap.get(_x).is_some()
- && ap.get(_x).unwrap().open == _x
- && ap.get(_x).unwrap().close == _y =>
- // delete both autopaired characters
- {
- (
- graphemes::nth_prev_grapheme_boundary(text, pos, count),
- graphemes::nth_next_grapheme_boundary(text, pos, count),
- )
- }
- _ =>
- // delete 1 char
- {
- (graphemes::nth_prev_grapheme_boundary(text, pos, count), pos)
- }
- }
+ return ((pos, pos), None);
}
- });
- let (view, doc) = current!(cx.editor);
+
+ dedent(doc, range)
+ .map(|dedent| (dedent, None))
+ .or_else(|| {
+ auto_pairs::hook_delete(doc.text(), range, doc.auto_pairs(cx.editor)?)
+ .map(|(delete, new_range)| (delete, Some(new_range)))
+ })
+ .unwrap_or_else(|| {
+ (
+ (graphemes::nth_prev_grapheme_boundary(text, pos, count), pos),
+ None,
+ )
+ })
+ },
+ );
+
+ log::debug!("delete_char_backward transaction: {:?}", transaction);
+
+ let doc = doc_mut!(cx.editor, &doc.id());
doc.apply(&transaction, view.id);
}
@@ -5452,6 +5462,10 @@ fn reverse_selection_contents(cx: &mut Context) {
// tree sitter node selection
+const EXPAND_KEY: &str = "expand";
+const EXPAND_AROUND_BASE_KEY: &str = "expand_around_base";
+const PARENTS_KEY: &str = "parents";
+
fn expand_selection(cx: &mut Context) {
let motion = |editor: &mut Editor| {
let (view, doc) = current!(editor);
@@ -5459,42 +5473,154 @@ fn expand_selection(cx: &mut Context) {
if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
- let current_selection = doc.selection(view.id);
+ let current_selection = doc.selection(view.id).clone();
let selection = object::expand_selection(syntax, text, current_selection.clone());
// check if selection is different from the last one
- if *current_selection != selection {
- // save current selection so it can be restored using shrink_selection
- view.object_selections.push(current_selection.clone());
+ if current_selection != selection {
+ let prev_selections = doc
+ .view_data_mut(view.id)
+ .object_selections
+ .entry(EXPAND_KEY)
+ .or_default();
- doc.set_selection(view.id, selection);
+ // save current selection so it can be restored using shrink_selection
+ prev_selections.push(current_selection);
+ doc.set_selection_clear(view.id, selection, false);
}
}
};
+
cx.editor.apply_motion(motion);
}
fn shrink_selection(cx: &mut Context) {
let motion = |editor: &mut Editor| {
let (view, doc) = current!(editor);
- let current_selection = doc.selection(view.id);
+ let current_selection = doc.selection(view.id).clone();
+ let prev_expansions = doc
+ .view_data_mut(view.id)
+ .object_selections
+ .entry(EXPAND_KEY)
+ .or_default();
+
// try to restore previous selection
- if let Some(prev_selection) = view.object_selections.pop() {
- if current_selection.contains(&prev_selection) {
- doc.set_selection(view.id, prev_selection);
- return;
- } else {
- // clear existing selection as they can't be shrunk to anyway
- view.object_selections.clear();
+ if let Some(prev_selection) = prev_expansions.pop() {
+ // allow shrinking the selection only if current selection contains the previous object selection
+ doc.set_selection_clear(view.id, prev_selection, false);
+
+ // Do a corresponding pop of the parents from `expand_selection_around`
+ doc.view_data_mut(view.id)
+ .object_selections
+ .entry(PARENTS_KEY)
+ .and_modify(|parents| {
+ parents.pop();
+ });
+
+ // need to do this again because borrowing
+ let prev_expansions = doc
+ .view_data_mut(view.id)
+ .object_selections
+ .entry(EXPAND_KEY)
+ .or_default();
+
+ // if we've emptied out the previous expansions, then clear out the
+ // base history as well so it doesn't get used again erroneously
+ if prev_expansions.is_empty() {
+ doc.view_data_mut(view.id)
+ .object_selections
+ .entry(EXPAND_AROUND_BASE_KEY)
+ .and_modify(|base| {
+ base.clear();
+ });
}
+
+ return;
}
+
// if not previous selection, shrink to first child
if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
- let selection = object::shrink_selection(syntax, text, current_selection.clone());
- doc.set_selection(view.id, selection);
+ let selection = object::shrink_selection(syntax, text, current_selection);
+ doc.set_selection_clear(view.id, selection, false);
}
};
+
+ cx.editor.apply_motion(motion);
+}
+
+fn expand_selection_around(cx: &mut Context) {
+ let motion = |editor: &mut Editor| {
+ let (view, doc) = current!(editor);
+
+ if doc.syntax().is_some() {
+ // [NOTE] we do this pop and push dance because if we don't take
+ // ownership of the objects, then we require multiple
+ // mutable references to the view's object selections
+ let mut parents_selection = doc
+ .view_data_mut(view.id)
+ .object_selections
+ .entry(PARENTS_KEY)
+ .or_default()
+ .pop();
+
+ let mut base_selection = doc
+ .view_data_mut(view.id)
+ .object_selections
+ .entry(EXPAND_AROUND_BASE_KEY)
+ .or_default()
+ .pop();
+
+ let current_selection = doc.selection(view.id).clone();
+
+ if parents_selection.is_none() || base_selection.is_none() {
+ parents_selection = Some(current_selection.clone());
+ base_selection = Some(current_selection.clone());
+ }
+
+ let text = doc.text().slice(..);
+ let syntax = doc.syntax().unwrap();
+
+ let outside_selection =
+ object::expand_selection(syntax, text, parents_selection.clone().unwrap());
+
+ let target_selection = match outside_selection
+ .clone()
+ .without(&base_selection.clone().unwrap())
+ {
+ Some(sel) => sel,
+ None => outside_selection.clone(),
+ };
+
+ // check if selection is different from the last one
+ if target_selection != current_selection {
+ // save current selection so it can be restored using shrink_selection
+ doc.view_data_mut(view.id)
+ .object_selections
+ .entry(EXPAND_KEY)
+ .or_default()
+ .push(current_selection);
+
+ doc.set_selection_clear(view.id, target_selection, false);
+ }
+
+ let parents = doc
+ .view_data_mut(view.id)
+ .object_selections
+ .entry(PARENTS_KEY)
+ .or_default();
+
+ parents.push(parents_selection.unwrap());
+ parents.push(outside_selection);
+
+ doc.view_data_mut(view.id)
+ .object_selections
+ .entry(EXPAND_AROUND_BASE_KEY)
+ .or_default()
+ .push(base_selection.unwrap());
+ }
+ };
+
cx.editor.apply_motion(motion);
}
@@ -5512,6 +5638,7 @@ where
doc.set_selection(view.id, selection);
}
};
+
cx.editor.apply_motion(motion);
}
@@ -5613,8 +5740,6 @@ fn match_brackets(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
-//
-
fn jump_forward(cx: &mut Context) {
let count = cx.count();
let config = cx.editor.config();
@@ -5920,7 +6045,6 @@ fn goto_ts_object_impl(cx: &mut Context, object: &'static str, direction: Direct
}
});
- push_jump(view, doc);
doc.set_selection(view.id, selection);
} else {
editor.set_status("Syntax-tree is not available in current buffer");