aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGokul Soumya2021-06-19 17:25:50 +0000
committerBlaž Hrastnik2021-06-22 05:27:51 +0000
commit13648d28b902ed9d0db2afe3824786fa844f50e7 (patch)
tree537a06e4651cfe7cd62d16488dca4ba095661acc
parent2f321b9335fe566a8e0af4f792d4c3b62d585ba5 (diff)
Add surround keybinds
-rw-r--r--helix-core/src/lib.rs1
-rw-r--r--helix-core/src/surround.rs58
-rw-r--r--helix-term/src/commands.rs120
-rw-r--r--helix-term/src/keymap.rs2
-rw-r--r--helix-view/src/document.rs1
5 files changed, 180 insertions, 2 deletions
diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs
index f697bc7f..7d51f88c 100644
--- a/helix-core/src/lib.rs
+++ b/helix-core/src/lib.rs
@@ -16,6 +16,7 @@ pub mod register;
pub mod search;
pub mod selection;
mod state;
+pub mod surround;
pub mod syntax;
mod transaction;
diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs
new file mode 100644
index 00000000..a629d2d3
--- /dev/null
+++ b/helix-core/src/surround.rs
@@ -0,0 +1,58 @@
+use crate::{search, Selection};
+use ropey::RopeSlice;
+
+pub const PAIRS: &[(char, char)] = &[('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
+
+/// Given any char in [PAIRS], return the open and closing chars. If not found in
+/// [PAIRS] return (ch, ch).
+pub fn get_pair(ch: char) -> (char, char) {
+ PAIRS
+ .iter()
+ .find(|(open, close)| *open == ch || *close == ch)
+ .copied()
+ .unwrap_or((ch, ch))
+}
+
+/// Find the position of surround pairs of `ch` which can be either a closing
+/// or opening pair. `n` will skip n - 1 pairs (eg. n=2 will discard (only)
+/// the first pair found and keep looking)
+pub fn find_nth_pairs_pos(
+ text: RopeSlice,
+ ch: char,
+ pos: usize,
+ n: usize,
+) -> Option<(usize, usize)> {
+ let (open, close) = get_pair(ch);
+ let open_pos = search::find_nth_prev(text, open, pos, n, true)?;
+ let close_pos = search::find_nth_next(text, close, pos, n, true)?;
+
+ Some((open_pos, close_pos))
+}
+
+/// Find position of surround characters around every cursor. Returns None
+/// if any positions overlap. Note that the positions are in a flat Vec.
+/// Use get_surround_pos().chunks(2) to get matching pairs of surround positions.
+/// `ch` can be either closing or opening pair.
+pub fn get_surround_pos(
+ text: RopeSlice,
+ selection: &Selection,
+ ch: char,
+ skip: usize,
+) -> Option<Vec<usize>> {
+ let mut change_pos = Vec::new();
+
+ for range in selection {
+ let head = range.head;
+
+ match find_nth_pairs_pos(text, ch, head, skip) {
+ Some((open_pos, close_pos)) => {
+ if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
+ return None;
+ }
+ change_pos.extend_from_slice(&[open_pos, close_pos]);
+ }
+ None => return None,
+ }
+ }
+ Some(change_pos)
+}
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 8866b79b..b530e30b 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -255,7 +255,8 @@ impl Command {
space_mode,
view_mode,
left_bracket_mode,
- right_bracket_mode
+ right_bracket_mode,
+ surround
);
}
@@ -1906,6 +1907,7 @@ fn goto_mode(cx: &mut Context) {
match (doc.mode, ch) {
(_, 'g') => move_file_start(cx),
(_, 'e') => move_file_end(cx),
+ (_, 'm') => match_brackets(cx),
(_, 'a') => switch_to_last_accessed_file(cx),
(Mode::Normal, 'h') => move_line_start(cx),
(Mode::Normal, 'l') => move_line_end(cx),
@@ -3311,6 +3313,122 @@ fn right_bracket_mode(cx: &mut Context) {
})
}
+fn surround(cx: &mut Context) {
+ let count = cx.count;
+ cx.on_next_key(move |cx, event| {
+ if let KeyEvent {
+ code: KeyCode::Char(ch),
+ ..
+ } = event
+ {
+ // FIXME: count gets reset because of cx.on_next_key()
+ cx.count = count;
+ match ch {
+ 'a' => surround_add(cx),
+ 'r' => surround_replace(cx),
+ 'd' => {
+ surround_delete(cx);
+ let (view, doc) = current!(cx.editor);
+ }
+ _ => (),
+ }
+ }
+ })
+}
+
+use helix_core::surround;
+
+fn surround_add(cx: &mut Context) {
+ cx.on_next_key(move |cx, event| {
+ if let KeyEvent {
+ code: KeyCode::Char(ch),
+ ..
+ } = event
+ {
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+ let selection = doc.selection(view.id);
+ let (open, close) = surround::get_pair(ch);
+
+ let mut changes = Vec::new();
+ for (i, range) in selection.iter().enumerate() {
+ let (from, to) = (range.from(), range.to() + 1);
+ changes.push((from, from, Some(Tendril::from_char(open))));
+ changes.push((to, to, Some(Tendril::from_char(close))));
+ }
+
+ let transaction = Transaction::change(doc.text(), changes.into_iter());
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+ }
+ })
+}
+
+fn surround_replace(cx: &mut Context) {
+ let count = cx.count();
+ cx.on_next_key(move |cx, event| {
+ if let KeyEvent {
+ code: KeyCode::Char(from),
+ ..
+ } = event
+ {
+ cx.on_next_key(move |cx, event| {
+ if let KeyEvent {
+ code: KeyCode::Char(to),
+ ..
+ } = event
+ {
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+ let selection = doc.selection(view.id);
+
+ let change_pos = match surround::get_surround_pos(text, selection, from, count)
+ {
+ Some(c) => c,
+ None => return,
+ };
+
+ let (open, close) = surround::get_pair(to);
+ let transaction = Transaction::change(
+ doc.text(),
+ change_pos.iter().enumerate().map(|(i, &pos)| {
+ let ch = if i % 2 == 0 { open } else { close };
+ (pos, pos + 1, Some(Tendril::from_char(ch)))
+ }),
+ );
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+ }
+ });
+ }
+ })
+}
+
+fn surround_delete(cx: &mut Context) {
+ let count = cx.count();
+ cx.on_next_key(move |cx, event| {
+ if let KeyEvent {
+ code: KeyCode::Char(ch),
+ ..
+ } = event
+ {
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+ let selection = doc.selection(view.id);
+
+ let change_pos = match surround::get_surround_pos(text, selection, ch, count) {
+ Some(c) => c,
+ None => return,
+ };
+
+ let transaction =
+ Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None)));
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+ }
+ })
+}
+
impl fmt::Display for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Command(name, _) = self;
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 46d495c3..cce3d31f 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -200,7 +200,7 @@ impl Default for Keymaps {
// extend_to_whole_line, crop_to_whole_line
- key!('m') => Command::match_brackets,
+ key!('m') => Command::surround,
// TODO: refactor into
// key!('m') => commands::select_to_matching,
// key!('M') => commands::back_select_to_matching,
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index bd45db5a..f0d7b55a 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -649,6 +649,7 @@ impl Document {
}
}
+ /// Commit pending changes to history
pub fn append_changes_to_history(&mut self, view_id: ViewId) {
if self.changes.is_empty() {
return;