on master help sign in
Add surround keybinds
13648d2Gokul Soumya
helix-core/src/lib.rs+1
@@ -16,6 +16,7 @@ pub mod register;
16pub mod search;
16pub mod search;
17pub mod selection;
17pub mod selection;
18mod state;
18mod state;
19+pub mod surround;
19pub mod syntax;
20pub mod syntax;
20mod transaction;
21mod transaction;
21
22
helix-core/src/surround.rsnew file+58
@@ -0,0 +1,58 @@
1+use crate::{search, Selection};
2+use ropey::RopeSlice;
3+
4+pub const PAIRS: &[(char, char)] = &[('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
5+
6+/// Given any char in [PAIRS], return the open and closing chars. If not found in
7+/// [PAIRS] return (ch, ch).
8+pub fn get_pair(ch: char) -> (char, char) {
9+ PAIRS
10+ .iter()
11+ .find(|(open, close)| *open == ch || *close == ch)
12+ .copied()
13+ .unwrap_or((ch, ch))
14+}
15+
16+/// Find the position of surround pairs of `ch` which can be either a closing
17+/// or opening pair. `n` will skip n - 1 pairs (eg. n=2 will discard (only)
18+/// the first pair found and keep looking)
19+pub fn find_nth_pairs_pos(
20+ text: RopeSlice,
21+ ch: char,
22+ pos: usize,
23+ n: usize,
24+) -> Option<(usize, usize)> {
25+ let (open, close) = get_pair(ch);
26+ let open_pos = search::find_nth_prev(text, open, pos, n, true)?;
27+ let close_pos = search::find_nth_next(text, close, pos, n, true)?;
28+
29+ Some((open_pos, close_pos))
30+}
31+
32+/// Find position of surround characters around every cursor. Returns None
33+/// if any positions overlap. Note that the positions are in a flat Vec.
34+/// Use get_surround_pos().chunks(2) to get matching pairs of surround positions.
35+/// `ch` can be either closing or opening pair.
36+pub fn get_surround_pos(
37+ text: RopeSlice,
38+ selection: &Selection,
39+ ch: char,
40+ skip: usize,
41+) -> Option<Vec<usize>> {
42+ let mut change_pos = Vec::new();
43+
44+ for range in selection {
45+ let head = range.head;
46+
47+ match find_nth_pairs_pos(text, ch, head, skip) {
48+ Some((open_pos, close_pos)) => {
49+ if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
50+ return None;
51+ }
52+ change_pos.extend_from_slice(&[open_pos, close_pos]);
53+ }
54+ None => return None,
55+ }
56+ }
57+ Some(change_pos)
58+}
helix-term/src/commands.rs+119−1
@@ -255,7 +255,8 @@ impl Command {
255 space_mode,
255 space_mode,
256 view_mode,
256 view_mode,
257 left_bracket_mode,
257 left_bracket_mode,
258− right_bracket_mode
258+ right_bracket_mode,
259+ surround
259 );
260 );
260}
261}
261
262
@@ -1906,6 +1907,7 @@ fn goto_mode(cx: &mut Context) {
1906 match (doc.mode, ch) {
1907 match (doc.mode, ch) {
1907 (_, 'g') => move_file_start(cx),
1908 (_, 'g') => move_file_start(cx),
1908 (_, 'e') => move_file_end(cx),
1909 (_, 'e') => move_file_end(cx),
1910+ (_, 'm') => match_brackets(cx),
1909 (_, 'a') => switch_to_last_accessed_file(cx),
1911 (_, 'a') => switch_to_last_accessed_file(cx),
1910 (Mode::Normal, 'h') => move_line_start(cx),
1912 (Mode::Normal, 'h') => move_line_start(cx),
1911 (Mode::Normal, 'l') => move_line_end(cx),
1913 (Mode::Normal, 'l') => move_line_end(cx),
@@ -3311,6 +3313,122 @@ fn right_bracket_mode(cx: &mut Context) {
3311 })
3313 })
3312}
3314}
3313
3315
3316+fn surround(cx: &mut Context) {
3317+ let count = cx.count;
3318+ cx.on_next_key(move |cx, event| {
3319+ if let KeyEvent {
3320+ code: KeyCode::Char(ch),
3321+ ..
3322+ } = event
3323+ {
3324+ // FIXME: count gets reset because of cx.on_next_key()
3325+ cx.count = count;
3326+ match ch {
3327+ 'a' => surround_add(cx),
3328+ 'r' => surround_replace(cx),
3329+ 'd' => {
3330+ surround_delete(cx);
3331+ let (view, doc) = current!(cx.editor);
3332+ }
3333+ _ => (),
3334+ }
3335+ }
3336+ })
3337+}
3338+
3339+use helix_core::surround;
3340+
3341+fn surround_add(cx: &mut Context) {
3342+ cx.on_next_key(move |cx, event| {
3343+ if let KeyEvent {
3344+ code: KeyCode::Char(ch),
3345+ ..
3346+ } = event
3347+ {
3348+ let (view, doc) = current!(cx.editor);
3349+ let text = doc.text().slice(..);
3350+ let selection = doc.selection(view.id);
3351+ let (open, close) = surround::get_pair(ch);
3352+
3353+ let mut changes = Vec::new();
3354+ for (i, range) in selection.iter().enumerate() {
3355+ let (from, to) = (range.from(), range.to() + 1);
3356+ changes.push((from, from, Some(Tendril::from_char(open))));
3357+ changes.push((to, to, Some(Tendril::from_char(close))));
3358+ }
3359+
3360+ let transaction = Transaction::change(doc.text(), changes.into_iter());
3361+ doc.apply(&transaction, view.id);
3362+ doc.append_changes_to_history(view.id);
3363+ }
3364+ })
3365+}
3366+
3367+fn surround_replace(cx: &mut Context) {
3368+ let count = cx.count();
3369+ cx.on_next_key(move |cx, event| {
3370+ if let KeyEvent {
3371+ code: KeyCode::Char(from),
3372+ ..
3373+ } = event
3374+ {
3375+ cx.on_next_key(move |cx, event| {
3376+ if let KeyEvent {
3377+ code: KeyCode::Char(to),
3378+ ..
3379+ } = event
3380+ {
3381+ let (view, doc) = current!(cx.editor);
3382+ let text = doc.text().slice(..);
3383+ let selection = doc.selection(view.id);
3384+
3385+ let change_pos = match surround::get_surround_pos(text, selection, from, count)
3386+ {
3387+ Some(c) => c,
3388+ None => return,
3389+ };
3390+
3391+ let (open, close) = surround::get_pair(to);
3392+ let transaction = Transaction::change(
3393+ doc.text(),
3394+ change_pos.iter().enumerate().map(|(i, &pos)| {
3395+ let ch = if i % 2 == 0 { open } else { close };
3396+ (pos, pos + 1, Some(Tendril::from_char(ch)))
3397+ }),
3398+ );
3399+ doc.apply(&transaction, view.id);
3400+ doc.append_changes_to_history(view.id);
3401+ }
3402+ });
3403+ }
3404+ })
3405+}
3406+
3407+fn surround_delete(cx: &mut Context) {
3408+ let count = cx.count();
3409+ cx.on_next_key(move |cx, event| {
3410+ if let KeyEvent {
3411+ code: KeyCode::Char(ch),
3412+ ..
3413+ } = event
3414+ {
3415+ let (view, doc) = current!(cx.editor);
3416+ let text = doc.text().slice(..);
3417+ let selection = doc.selection(view.id);
3418+
3419+ let change_pos = match surround::get_surround_pos(text, selection, ch, count) {
3420+ Some(c) => c,
3421+ None => return,
3422+ };
3423+
3424+ let transaction =
3425+ Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None)));
3426+ doc.apply(&transaction, view.id);
3427+ doc.append_changes_to_history(view.id);
3428+ }
3429+ })
3430+}
3431+
3314impl fmt::Display for Command {
3432impl fmt::Display for Command {
3315 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3433 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3316 let Command(name, _) = self;
3434 let Command(name, _) = self;
helix-term/src/keymap.rs+1−1
@@ -200,7 +200,7 @@ impl Default for Keymaps {
200 // extend_to_whole_line, crop_to_whole_line
200 // extend_to_whole_line, crop_to_whole_line
201
201
202
202
203− key!('m') => Command::match_brackets,
203+ key!('m') => Command::surround,
204 // TODO: refactor into
204 // TODO: refactor into
205 // key!('m') => commands::select_to_matching,
205 // key!('m') => commands::select_to_matching,
206 // key!('M') => commands::back_select_to_matching,
206 // key!('M') => commands::back_select_to_matching,
helix-view/src/document.rs+1
@@ -649,6 +649,7 @@ impl Document {
649 }
649 }
650 }
650 }
651
651
652+ /// Commit pending changes to history
652 pub fn append_changes_to_history(&mut self, view_id: ViewId) {
653 pub fn append_changes_to_history(&mut self, view_id: ViewId) {
653 if self.changes.is_empty() {
654 if self.changes.is_empty() {
654 return;
655 return;