Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

surround as selection #368

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions helix-core/src/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ impl Range {
pub fn fragment<'a, 'b: 'a>(&'a self, text: RopeSlice<'b>) -> Cow<'b, str> {
Cow::from(text.slice(self.from()..self.to() + 1))
}

#[inline]
pub fn len(&self) -> usize {
self.from() + 1 - self.to()
}
}

/// A selection consists of one or more selection ranges.
Expand Down
68 changes: 67 additions & 1 deletion helix-core/src/surround.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,50 @@ pub fn get_pair(ch: char) -> (char, char) {
.unwrap_or((ch, ch))
}

/// Find the position of balanced surround pairs of `ch` which can be either a closing
/// or opening pair.
pub fn find_balanced_pairs_pos(text: RopeSlice, ch: char, pos: usize) -> Option<(usize, usize)> {
let (open, close) = get_pair(ch);

let starting_pos = pos;
let mut pos = pos;
let mut skip = 0;
let mut chars = text.chars_at(pos);
let open_pos = if text.char(pos) == open {
Some(pos)
} else {
loop {
if let Some(c) = chars.prev() {
pos = pos.saturating_sub(1);

if c == open {
if skip > 0 {
skip -= 1;
} else {
break Some(pos);
}
} else if c == close {
skip += 1;
}
} else {
break None;
}
}
}?;
let mut count = 1;
for (i, c) in text.slice(open_pos + 1..).chars().enumerate() {
if c == close {
count -= 1;
if count == 0 {
return Some((open_pos, open_pos + 1 + i));
}
} else if c == open {
count += 1;
}
}
None
}

/// 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)
Expand Down Expand Up @@ -59,7 +103,7 @@ pub fn get_surround_pos(
let mut change_pos = Vec::new();

for range in selection {
let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range.head, skip)?;
let (open_pos, close_pos) = find_balanced_pairs_pos(text, ch, range.head)?;
if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
return None;
}
Expand Down Expand Up @@ -90,6 +134,28 @@ mod test {
assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10)));
}

#[test]
fn test_find_balanced_pairs_pos() {
let doc = Rope::from("some ((text) here)");
let slice = doc.slice(..);

// cursor on [t]ext
assert_eq!(find_balanced_pairs_pos(slice, '(', 7), Some((6, 11)));
assert_eq!(find_balanced_pairs_pos(slice, ')', 7), Some((6, 11)));
// cursor on so[m]e
assert_eq!(find_balanced_pairs_pos(slice, '(', 2), None);
// cursor on bracket itself
assert_eq!(find_balanced_pairs_pos(slice, '(', 6), Some((6, 11)));
// cursor on outer parens
assert_eq!(find_balanced_pairs_pos(slice, '(', 5), Some((5, 17)));

let doc = Rope::from("some (text (here))");
let slice = doc.slice(..);

// cursor on outer parens
assert_eq!(find_balanced_pairs_pos(slice, '(', 17), Some((5, 17)));
}

#[test]
fn test_find_nth_pairs_pos_skip() {
let doc = Rope::from("(so (many (good) text) here)");
Expand Down
70 changes: 56 additions & 14 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,43 @@ fn replace(cx: &mut Context) {
};

if let Some(ch) = ch {
let transaction =
let ranges = doc.selection(view.id).ranges();

let text = doc.text();
let transaction = match ranges {
[open_range, close_range] => {
let open_char = text.char(open_range.from());
let close_char = text.char(close_range.from());
let ch = ch.chars().next().unwrap();
let (open, close) = get_pair(open_char);
if open_char == open
&& close_char == close
&& open_range.len() == 1
&& close_range.len() == 1
{
let (open_replace, close_replace) = get_pair(ch);
Some(Transaction::change(
doc.text(),
std::array::IntoIter::new([
(
open_range.from(),
open_range.to() + 1,
Some(open_replace.to_string().into()),
),
(
close_range.from(),
close_range.to() + 1,
Some(close_replace.to_string().into()),
),
]),
))
} else {
None
}
}
_ => None,
}
.unwrap_or_else(|| {
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
let max_to = doc.text().len_chars().saturating_sub(1);
let to = std::cmp::min(max_to, range.to() + 1);
Expand All @@ -628,7 +664,8 @@ fn replace(cx: &mut Context) {
.collect();

(range.from(), to, Some(text.into()))
});
})
});

doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);
Expand Down Expand Up @@ -3313,30 +3350,35 @@ fn right_bracket_mode(cx: &mut Context) {
}

fn match_mode(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 {
'm' => match_brackets(cx),
's' => surround_add(cx),
'r' => surround_replace(cx),
'd' => {
surround_delete(cx);
let (view, doc) = current!(cx.editor);
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
let pos = selection.cursor();

let change_pos = match surround::get_surround_pos(text, selection, ch, count) {
Some(c) => {
let ranges: SmallVec<_> = c
.chunks(2)
.flat_map(|r| [Range::new(r[0], r[0]), Range::new(r[1], r[1])])
.collect();
doc.set_selection(view.id, Selection::new(ranges, 0));
}
_ => (),
}
None => return,
};
}
})
}

use helix_core::surround;
use helix_core::surround::get_pair;
use std::iter::FromIterator;

fn surround_add(cx: &mut Context) {
cx.on_next_key(move |cx, event| {
Expand Down