From 394629ab73442ed07af210fbe2dddecd60b83300 Mon Sep 17 00:00:00 2001
From: Gokul Soumya <gokulps15@gmail.com>
Date: Thu, 24 Jun 2021 00:20:19 +0530
Subject: [PATCH] Skip enclosed pairs in surround

Surround operations previously ignored other pairs that are
enclosed within which should be skipped. For example if the
cursor is on the `,` in `{{a},{b}}`, doing `md{` previously
would delete the `{` on the left of `a` and `}` on the right
of `b` instead of the outermost braces. This commit corrects
this behavior.
---
 helix-core/src/surround.rs | 115 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 112 insertions(+), 3 deletions(-)

diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs
index d7314609..61981d6e 100644
--- a/helix-core/src/surround.rs
+++ b/helix-core/src/surround.rs
@@ -39,13 +39,101 @@ pub fn find_nth_pairs_pos(
     n: usize,
 ) -> Option<(usize, usize)> {
     let (open, close) = get_pair(ch);
-    // find_nth* do not consider current character; +1/-1 to include them
-    let open_pos = search::find_nth_prev(text, open, pos + 1, n, true)?;
-    let close_pos = search::find_nth_next(text, close, pos - 1, n, true)?;
+
+    let (open_pos, close_pos) = if open == close {
+        // find_nth* do not consider current character; +1/-1 to include them
+        (
+            search::find_nth_prev(text, open, pos + 1, n, true)?,
+            search::find_nth_next(text, close, pos - 1, n, true)?,
+        )
+    } else {
+        (
+            find_nth_open_pair(text, open, close, pos, n)?,
+            find_nth_close_pair(text, open, close, pos, n)?,
+        )
+    };
 
     Some((open_pos, close_pos))
 }
 
+fn find_nth_open_pair(
+    text: RopeSlice,
+    open: char,
+    close: char,
+    mut pos: usize,
+    n: usize,
+) -> Option<usize> {
+    let mut chars = text.chars_at(pos + 1);
+
+    // Adjusts pos for the first iteration, and handles the case of the
+    // cursor being *on* the close character which will get falsely stepped over
+    // if not skipped here
+    if chars.prev()? == open {
+        return Some(pos);
+    }
+
+    for _ in 0..n {
+        let mut step_over: usize = 0;
+
+        loop {
+            let c = chars.prev()?;
+            pos = pos.saturating_sub(1);
+
+            // ignore other surround pairs that are enclosed *within* our search scope
+            if c == close {
+                step_over += 1;
+            } else if c == open {
+                if step_over == 0 {
+                    break;
+                }
+
+                step_over = step_over.saturating_sub(1);
+            }
+        }
+    }
+
+    Some(pos)
+}
+
+fn find_nth_close_pair(
+    text: RopeSlice,
+    open: char,
+    close: char,
+    mut pos: usize,
+    n: usize,
+) -> Option<usize> {
+    if pos >= text.len_chars() {
+        return None;
+    }
+
+    let mut chars = text.chars_at(pos);
+
+    if chars.next()? == close {
+        return Some(pos);
+    }
+
+    for _ in 0..n {
+        let mut step_over: usize = 0;
+
+        loop {
+            let c = chars.next()?;
+            pos += 1;
+
+            if c == open {
+                step_over += 1;
+            } else if c == close {
+                if step_over == 0 {
+                    break;
+                }
+
+                step_over = step_over.saturating_sub(1);
+            }
+        }
+    }
+
+    Some(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.
@@ -101,6 +189,27 @@ mod test {
         assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27)));
     }
 
+    #[test]
+    fn test_find_nth_pairs_pos_same() {
+        let doc = Rope::from("'so 'many 'good' text' here'");
+        let slice = doc.slice(..);
+
+        // cursor on go[o]d
+        assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
+        assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
+        assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
+    }
+
+    #[test]
+    fn test_find_nth_pairs_pos_step() {
+        let doc = Rope::from("((so)((many) good (text))(here))");
+        let slice = doc.slice(..);
+
+        // cursor on go[o]d
+        assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24)));
+        assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31)));
+    }
+
     #[test]
     fn test_find_nth_pairs_pos_mixed() {
         let doc = Rope::from("(so [many {good} text] here)");