diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index f697bc7fd..7d51f88cf 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 000000000..a629d2d3d --- /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> { + 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 8866b79bf..b530e30b7 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 46d495c3d..cce3d31f9 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 bd45db5a1..f0d7b55af 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;