Add helix match surround operations (#44317)
Partially addresses #38151 Release Notes: - Added Helix match surround operations
This commit is contained in:
1 parent
1d85101344
commit
e3dce59f5d
6 files changed
+785
-225
No files matched your search
@@ -643,6 +643,9 @@
|
||||
"context": "vim_operator == helix_m",
|
||||
"bindings": {
|
||||
"m": "vim::Matching",
|
||||
"s": "vim::PushHelixSurroundAdd",
|
||||
"r": "vim::PushHelixSurroundReplace",
|
||||
"d": "vim::PushHelixSurroundDelete",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
+29
-2
@@ -3,6 +3,7 @@ mod duplicate;
|
||||
mod object;
|
||||
mod paste;
|
||||
mod select;
|
||||
mod surround;
|
||||
|
||||
use editor::display_map::DisplaySnapshot;
|
||||
use editor::{
|
||||
@@ -19,9 +20,9 @@ use workspace::searchable::FilteredSearchRange;
|
||||
use workspace::searchable::{self, Direction};
|
||||
|
||||
use crate::motion::{self, MotionKind};
|
||||
use crate::state::SearchState;
|
||||
use crate::state::{Operator, SearchState};
|
||||
use crate::{
|
||||
Vim,
|
||||
PushHelixSurroundAdd, PushHelixSurroundDelete, PushHelixSurroundReplace, Vim,
|
||||
motion::{Motion, right},
|
||||
state::Mode,
|
||||
};
|
||||
@@ -80,6 +81,32 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
Vim::action(editor, cx, Vim::helix_substitute_no_yank);
|
||||
Vim::action(editor, cx, Vim::helix_select_next);
|
||||
Vim::action(editor, cx, Vim::helix_select_previous);
|
||||
Vim::action(editor, cx, |vim, _: &PushHelixSurroundAdd, window, cx| {
|
||||
vim.clear_operator(window, cx);
|
||||
vim.push_operator(Operator::HelixSurroundAdd, window, cx);
|
||||
});
|
||||
Vim::action(
|
||||
editor,
|
||||
cx,
|
||||
|vim, _: &PushHelixSurroundReplace, window, cx| {
|
||||
vim.clear_operator(window, cx);
|
||||
vim.push_operator(
|
||||
Operator::HelixSurroundReplace {
|
||||
replaced_char: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
},
|
||||
);
|
||||
Vim::action(
|
||||
editor,
|
||||
cx,
|
||||
|vim, _: &PushHelixSurroundDelete, window, cx| {
|
||||
vim.clear_operator(window, cx);
|
||||
vim.push_operator(Operator::HelixSurroundDelete, window, cx);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
use editor::display_map::DisplaySnapshot;
|
||||
use editor::{Bias, DisplayPoint, MultiBufferOffset};
|
||||
use gpui::{Context, Window};
|
||||
use multi_buffer::Anchor;
|
||||
use text::Selection;
|
||||
|
||||
use crate::Vim;
|
||||
use crate::object::surrounding_markers;
|
||||
use crate::surrounds::{SURROUND_PAIRS, bracket_pair_for_str_helix, surround_pair_for_char_helix};
|
||||
|
||||
/// Find the nearest surrounding bracket pair around the cursor.
|
||||
fn find_nearest_surrounding_pair(
|
||||
display_map: &DisplaySnapshot,
|
||||
cursor: DisplayPoint,
|
||||
) -> Option<(char, char)> {
|
||||
let cursor_offset = cursor.to_offset(display_map, Bias::Left);
|
||||
let mut best_pair: Option<(char, char)> = None;
|
||||
let mut min_range_size = usize::MAX;
|
||||
|
||||
for pair in SURROUND_PAIRS {
|
||||
if let Some(range) =
|
||||
surrounding_markers(display_map, cursor, true, true, pair.open, pair.close)
|
||||
{
|
||||
let start_offset = range.start.to_offset(display_map, Bias::Left);
|
||||
let end_offset = range.end.to_offset(display_map, Bias::Right);
|
||||
|
||||
if cursor_offset >= start_offset && cursor_offset <= end_offset {
|
||||
let size = end_offset - start_offset;
|
||||
if size < min_range_size {
|
||||
min_range_size = size;
|
||||
best_pair = Some((pair.open, pair.close));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
best_pair
|
||||
}
|
||||
|
||||
fn selection_cursor(map: &DisplaySnapshot, selection: &Selection<DisplayPoint>) -> DisplayPoint {
|
||||
if selection.reversed || selection.is_empty() {
|
||||
selection.head()
|
||||
} else {
|
||||
editor::movement::left(map, selection.head())
|
||||
}
|
||||
}
|
||||
|
||||
type SurroundEdits = Vec<(std::ops::Range<MultiBufferOffset>, String)>;
|
||||
type SurroundAnchors = Vec<std::ops::Range<Anchor>>;
|
||||
|
||||
fn apply_helix_surround_edits<F>(
|
||||
vim: &mut Vim,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Vim>,
|
||||
mut build: F,
|
||||
) where
|
||||
F: FnMut(&DisplaySnapshot, Vec<Selection<DisplayPoint>>) -> (SurroundEdits, SurroundAnchors),
|
||||
{
|
||||
vim.update_editor(cx, |_, editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
||||
let display_map = editor.display_snapshot(cx);
|
||||
let selections = editor.selections.all_display(&display_map);
|
||||
let (mut edits, anchors) = build(&display_map, selections);
|
||||
|
||||
edits.sort_by(|a, b| b.0.start.cmp(&a.0.start));
|
||||
editor.edit(edits, cx);
|
||||
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_anchor_ranges(anchors);
|
||||
});
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
/// ms - Add surrounding characters around selection.
|
||||
pub fn helix_surround_add(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.stop_recording(cx);
|
||||
|
||||
let pair = bracket_pair_for_str_helix(text);
|
||||
|
||||
apply_helix_surround_edits(self, window, cx, |display_map, selections| {
|
||||
let mut edits = Vec::new();
|
||||
let mut anchors = Vec::new();
|
||||
|
||||
for selection in selections {
|
||||
let range = selection.range();
|
||||
let start = range.start.to_offset(display_map, Bias::Right);
|
||||
let end = range.end.to_offset(display_map, Bias::Left);
|
||||
|
||||
let end_anchor = display_map.buffer_snapshot().anchor_before(end);
|
||||
edits.push((end..end, pair.end.clone()));
|
||||
edits.push((start..start, pair.start.clone()));
|
||||
anchors.push(end_anchor..end_anchor);
|
||||
}
|
||||
|
||||
(edits, anchors)
|
||||
});
|
||||
}
|
||||
|
||||
/// mr - Replace innermost surrounding pair containing the cursor.
|
||||
pub fn helix_surround_replace(
|
||||
&mut self,
|
||||
old_char: char,
|
||||
new_char: char,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
|
||||
let new_char_str = new_char.to_string();
|
||||
let new_pair = bracket_pair_for_str_helix(&new_char_str);
|
||||
|
||||
apply_helix_surround_edits(self, window, cx, |display_map, selections| {
|
||||
let mut edits: Vec<(std::ops::Range<MultiBufferOffset>, String)> = Vec::new();
|
||||
let mut anchors = Vec::new();
|
||||
|
||||
for selection in selections {
|
||||
let cursor = selection_cursor(display_map, &selection);
|
||||
|
||||
// For 'm', find the nearest surrounding pair
|
||||
let markers = match surround_pair_for_char_helix(old_char) {
|
||||
Some(pair) => Some((pair.open, pair.close)),
|
||||
None => find_nearest_surrounding_pair(display_map, cursor),
|
||||
};
|
||||
|
||||
let Some((open_marker, close_marker)) = markers else {
|
||||
let offset = selection.head().to_offset(display_map, Bias::Left);
|
||||
let anchor = display_map.buffer_snapshot().anchor_before(offset);
|
||||
anchors.push(anchor..anchor);
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(range) =
|
||||
surrounding_markers(display_map, cursor, true, true, open_marker, close_marker)
|
||||
{
|
||||
let open_start = range.start.to_offset(display_map, Bias::Left);
|
||||
let open_end = open_start + open_marker.len_utf8();
|
||||
let close_end = range.end.to_offset(display_map, Bias::Left);
|
||||
let close_start = close_end - close_marker.len_utf8();
|
||||
|
||||
edits.push((close_start..close_end, new_pair.end.clone()));
|
||||
edits.push((open_start..open_end, new_pair.start.clone()));
|
||||
|
||||
let cursor_offset = cursor.to_offset(display_map, Bias::Left);
|
||||
let anchor = display_map.buffer_snapshot().anchor_before(cursor_offset);
|
||||
anchors.push(anchor..anchor);
|
||||
} else {
|
||||
let offset = selection.head().to_offset(display_map, Bias::Left);
|
||||
let anchor = display_map.buffer_snapshot().anchor_before(offset);
|
||||
anchors.push(anchor..anchor);
|
||||
}
|
||||
}
|
||||
|
||||
(edits, anchors)
|
||||
});
|
||||
}
|
||||
|
||||
/// md - Delete innermost surrounding pair containing the cursor.
|
||||
pub fn helix_surround_delete(
|
||||
&mut self,
|
||||
target_char: char,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
|
||||
apply_helix_surround_edits(self, window, cx, |display_map, selections| {
|
||||
let mut edits: Vec<(std::ops::Range<MultiBufferOffset>, String)> = Vec::new();
|
||||
let mut anchors = Vec::new();
|
||||
|
||||
for selection in selections {
|
||||
let cursor = selection_cursor(display_map, &selection);
|
||||
|
||||
// For 'm', find the nearest surrounding pair
|
||||
let markers = match surround_pair_for_char_helix(target_char) {
|
||||
Some(pair) => Some((pair.open, pair.close)),
|
||||
None => find_nearest_surrounding_pair(display_map, cursor),
|
||||
};
|
||||
|
||||
let Some((open_marker, close_marker)) = markers else {
|
||||
let offset = selection.head().to_offset(display_map, Bias::Left);
|
||||
let anchor = display_map.buffer_snapshot().anchor_before(offset);
|
||||
anchors.push(anchor..anchor);
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(range) =
|
||||
surrounding_markers(display_map, cursor, true, true, open_marker, close_marker)
|
||||
{
|
||||
let open_start = range.start.to_offset(display_map, Bias::Left);
|
||||
let open_end = open_start + open_marker.len_utf8();
|
||||
let close_end = range.end.to_offset(display_map, Bias::Left);
|
||||
let close_start = close_end - close_marker.len_utf8();
|
||||
|
||||
edits.push((close_start..close_end, String::new()));
|
||||
edits.push((open_start..open_end, String::new()));
|
||||
|
||||
let cursor_offset = cursor.to_offset(display_map, Bias::Left);
|
||||
let anchor = display_map.buffer_snapshot().anchor_before(cursor_offset);
|
||||
anchors.push(anchor..anchor);
|
||||
} else {
|
||||
let offset = selection.head().to_offset(display_map, Bias::Left);
|
||||
let anchor = display_map.buffer_snapshot().anchor_before(offset);
|
||||
anchors.push(anchor..anchor);
|
||||
}
|
||||
}
|
||||
|
||||
(edits, anchors)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_add(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state("hello ˇworld", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m s (");
|
||||
cx.assert_state("hello (wˇ)orld", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello ˇworld", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m s )");
|
||||
cx.assert_state("hello (wˇ)orld", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m s [");
|
||||
cx.assert_state("hello [worlˇ]d", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m s \"");
|
||||
cx.assert_state("hello \"worlˇ\"d", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_delete(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d (");
|
||||
cx.assert_state("hello woˇrld test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello \"woˇrld\" test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d \"");
|
||||
cx.assert_state("hello woˇrld test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello woˇrld test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d (");
|
||||
cx.assert_state("hello woˇrld test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("((woˇrld))", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d (");
|
||||
cx.assert_state("(woˇrld)", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_replace(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r ( [");
|
||||
cx.assert_state("hello [woˇrld] test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r ( ]");
|
||||
cx.assert_state("hello [woˇrld] test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello \"woˇrld\" test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r \" {");
|
||||
cx.assert_state("hello {woˇrld} test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("((woˇrld))", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r ( [");
|
||||
cx.assert_state("([woˇrld])", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_multiline(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
function test() {
|
||||
return ˇvalue;
|
||||
}"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("m d {");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
function test()
|
||||
return ˇvalue;
|
||||
"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_select_mode(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state("hello «worldˇ» test", Mode::HelixSelect);
|
||||
cx.simulate_keystrokes("m s {");
|
||||
cx.assert_state("hello {worldˇ} test", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_multi_cursor(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
(heˇllo)
|
||||
(woˇrld)"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("m d (");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
heˇllo
|
||||
woˇrld"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_escape_cancels(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state("hello ˇworld", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m escape");
|
||||
cx.assert_state("hello ˇworld", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello (woˇrld)", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r ( escape");
|
||||
cx.assert_state("hello (woˇrld)", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_no_vim_aliases(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
// In Helix mode, 'b', 'B', 'r', 'a' are NOT aliases for brackets.
|
||||
// They are treated as literal characters, so 'mdb' looks for 'b...b' surrounds.
|
||||
|
||||
// 'b' is not an alias - it looks for literal 'b...b', finds none, does nothing
|
||||
cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d b");
|
||||
cx.assert_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
|
||||
// 'B' looks for literal 'B...B', not {}
|
||||
cx.set_state("hello {woˇrld} test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d B");
|
||||
cx.assert_state("hello {woˇrld} test", Mode::HelixNormal);
|
||||
|
||||
// 'r' looks for literal 'r...r', not []
|
||||
cx.set_state("hello [woˇrld] test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d r");
|
||||
cx.assert_state("hello [woˇrld] test", Mode::HelixNormal);
|
||||
|
||||
// 'a' looks for literal 'a...a', not <>
|
||||
cx.set_state("hello <woˇrld> test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d a");
|
||||
cx.assert_state("hello <woˇrld> test", Mode::HelixNormal);
|
||||
|
||||
// Arbitrary chars work as symmetric pairs (Helix feature)
|
||||
cx.set_state("hello *woˇrld* test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d *");
|
||||
cx.assert_state("hello woˇrld test", Mode::HelixNormal);
|
||||
|
||||
// ms (add) also doesn't use aliases - 'msb' adds literal 'b' surrounds
|
||||
cx.set_state("hello ˇworld", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m s b");
|
||||
cx.assert_state("hello bwˇborld", Mode::HelixNormal);
|
||||
|
||||
// mr (replace) also doesn't use aliases
|
||||
cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r ( b");
|
||||
cx.assert_state("hello bwoˇrldb test", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_match_nearest(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
// mdm - delete nearest surrounding pair
|
||||
cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d m");
|
||||
cx.assert_state("hello woˇrld test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello [woˇrld] test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d m");
|
||||
cx.assert_state("hello woˇrld test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello {woˇrld} test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d m");
|
||||
cx.assert_state("hello woˇrld test", Mode::HelixNormal);
|
||||
|
||||
// Nested - deletes innermost
|
||||
cx.set_state("([woˇrld])", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d m");
|
||||
cx.assert_state("(woˇrld)", Mode::HelixNormal);
|
||||
|
||||
// mrm - replace nearest surrounding pair
|
||||
cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r m [");
|
||||
cx.assert_state("hello [woˇrld] test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello {woˇrld} test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r m (");
|
||||
cx.assert_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
|
||||
// Nested - replaces innermost
|
||||
cx.set_state("([woˇrld])", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r m {");
|
||||
cx.assert_state("({woˇrld})", Mode::HelixNormal);
|
||||
}
|
||||
}
|
||||
+23
-1
@@ -142,6 +142,11 @@ pub enum Operator {
|
||||
HelixPrevious {
|
||||
around: bool,
|
||||
},
|
||||
HelixSurroundAdd,
|
||||
HelixSurroundReplace {
|
||||
replaced_char: Option<char>,
|
||||
},
|
||||
HelixSurroundDelete,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
@@ -1043,6 +1048,9 @@ impl Operator {
|
||||
Operator::HelixMatch => "helix_m",
|
||||
Operator::HelixNext { .. } => "helix_next",
|
||||
Operator::HelixPrevious { .. } => "helix_previous",
|
||||
Operator::HelixSurroundAdd => "helix_ms",
|
||||
Operator::HelixSurroundReplace { .. } => "helix_mr",
|
||||
Operator::HelixSurroundDelete => "helix_md",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1067,6 +1075,14 @@ impl Operator {
|
||||
Operator::HelixMatch => "m".to_string(),
|
||||
Operator::HelixNext { .. } => "]".to_string(),
|
||||
Operator::HelixPrevious { .. } => "[".to_string(),
|
||||
Operator::HelixSurroundAdd => "ms".to_string(),
|
||||
Operator::HelixSurroundReplace {
|
||||
replaced_char: None,
|
||||
} => "mr".to_string(),
|
||||
Operator::HelixSurroundReplace {
|
||||
replaced_char: Some(c),
|
||||
} => format!("mr{}", c),
|
||||
Operator::HelixSurroundDelete => "md".to_string(),
|
||||
_ => self.id().to_string(),
|
||||
}
|
||||
}
|
||||
@@ -1111,6 +1127,9 @@ impl Operator {
|
||||
| Operator::HelixMatch
|
||||
| Operator::HelixNext { .. }
|
||||
| Operator::HelixPrevious { .. } => false,
|
||||
Operator::HelixSurroundAdd
|
||||
| Operator::HelixSurroundReplace { .. }
|
||||
| Operator::HelixSurroundDelete => true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1136,7 +1155,10 @@ impl Operator {
|
||||
| Operator::DeleteSurrounds
|
||||
| Operator::Exchange
|
||||
| Operator::HelixNext { .. }
|
||||
| Operator::HelixPrevious { .. } => true,
|
||||
| Operator::HelixPrevious { .. }
|
||||
| Operator::HelixSurroundAdd
|
||||
| Operator::HelixSurroundReplace { .. }
|
||||
| Operator::HelixSurroundDelete => true,
|
||||
Operator::Yank
|
||||
| Operator::Object { .. }
|
||||
| Operator::FindForward { .. }
|
||||
|
||||
+234
-222
@@ -10,6 +10,64 @@ use language::BracketPair;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A char-based surround pair definition.
|
||||
/// Single source of truth for all supported surround pairs.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct SurroundPair {
|
||||
pub open: char,
|
||||
pub close: char,
|
||||
}
|
||||
|
||||
impl SurroundPair {
|
||||
pub const fn new(open: char, close: char) -> Self {
|
||||
Self { open, close }
|
||||
}
|
||||
|
||||
pub fn to_bracket_pair(self) -> BracketPair {
|
||||
BracketPair {
|
||||
start: self.open.to_string(),
|
||||
end: self.close.to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_object(self) -> Option<Object> {
|
||||
match self.open {
|
||||
'\'' => Some(Object::Quotes),
|
||||
'`' => Some(Object::BackQuotes),
|
||||
'"' => Some(Object::DoubleQuotes),
|
||||
'|' => Some(Object::VerticalBars),
|
||||
'(' => Some(Object::Parentheses),
|
||||
'[' => Some(Object::SquareBrackets),
|
||||
'{' => Some(Object::CurlyBrackets),
|
||||
'<' => Some(Object::AngleBrackets),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// All supported surround pairs - single source of truth.
|
||||
pub const SURROUND_PAIRS: &[SurroundPair] = &[
|
||||
SurroundPair::new('(', ')'),
|
||||
SurroundPair::new('[', ']'),
|
||||
SurroundPair::new('{', '}'),
|
||||
SurroundPair::new('<', '>'),
|
||||
SurroundPair::new('"', '"'),
|
||||
SurroundPair::new('\'', '\''),
|
||||
SurroundPair::new('`', '`'),
|
||||
SurroundPair::new('|', '|'),
|
||||
];
|
||||
|
||||
/// Bracket-only pairs for AnyBrackets matching.
|
||||
const BRACKET_PAIRS: &[SurroundPair] = &[
|
||||
SurroundPair::new('(', ')'),
|
||||
SurroundPair::new('[', ']'),
|
||||
SurroundPair::new('{', '}'),
|
||||
SurroundPair::new('<', '>'),
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum SurroundsType {
|
||||
Motion(Motion),
|
||||
@@ -34,16 +92,7 @@ impl Vim {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
||||
let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
|
||||
Some(pair) => pair.clone(),
|
||||
None => BracketPair {
|
||||
start: text.to_string(),
|
||||
end: text.to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
},
|
||||
};
|
||||
let pair = bracket_pair_for_str_vim(&text);
|
||||
let surround = pair.end != surround_alias((*text).as_ref());
|
||||
let display_map = editor.display_snapshot(cx);
|
||||
let display_selections = editor.selections.all_adjusted_display(&display_map);
|
||||
@@ -131,14 +180,16 @@ impl Vim {
|
||||
self.stop_recording(cx);
|
||||
|
||||
// only legitimate surrounds can be removed
|
||||
let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
|
||||
Some(pair) => pair.clone(),
|
||||
None => return,
|
||||
let Some(first_char) = text.chars().next() else {
|
||||
return;
|
||||
};
|
||||
let pair_object = match pair_to_object(&pair) {
|
||||
Some(pair_object) => pair_object,
|
||||
None => return,
|
||||
let Some(surround_pair) = surround_pair_for_char_vim(first_char) else {
|
||||
return;
|
||||
};
|
||||
let Some(pair_object) = surround_pair.to_object() else {
|
||||
return;
|
||||
};
|
||||
let pair = surround_pair.to_bracket_pair();
|
||||
let surround = pair.end != *text;
|
||||
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
@@ -233,16 +284,7 @@ impl Vim {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
||||
let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
|
||||
Some(pair) => pair.clone(),
|
||||
None => BracketPair {
|
||||
start: text.to_string(),
|
||||
end: text.to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
},
|
||||
};
|
||||
let pair = bracket_pair_for_str_vim(&text);
|
||||
|
||||
// A single space should be added if the new surround is a
|
||||
// bracket and not a quote (pair.start != pair.end) and if
|
||||
@@ -437,144 +479,91 @@ impl Vim {
|
||||
object: Object,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<BracketPair> {
|
||||
match object {
|
||||
Object::Quotes => Some(BracketPair {
|
||||
start: "'".to_string(),
|
||||
end: "'".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
}),
|
||||
Object::BackQuotes => Some(BracketPair {
|
||||
start: "`".to_string(),
|
||||
end: "`".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
}),
|
||||
Object::DoubleQuotes => Some(BracketPair {
|
||||
start: "\"".to_string(),
|
||||
end: "\"".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
}),
|
||||
Object::VerticalBars => Some(BracketPair {
|
||||
start: "|".to_string(),
|
||||
end: "|".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
}),
|
||||
Object::Parentheses => Some(BracketPair {
|
||||
start: "(".to_string(),
|
||||
end: ")".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
}),
|
||||
Object::SquareBrackets => Some(BracketPair {
|
||||
start: "[".to_string(),
|
||||
end: "]".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
}),
|
||||
Object::CurlyBrackets { .. } => Some(BracketPair {
|
||||
start: "{".to_string(),
|
||||
end: "}".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
}),
|
||||
Object::AngleBrackets => Some(BracketPair {
|
||||
start: "<".to_string(),
|
||||
end: ">".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
}),
|
||||
Object::AnyBrackets => {
|
||||
// If we're dealing with `AnyBrackets`, which can map to multiple
|
||||
// bracket pairs, we'll need to first determine which `BracketPair` to
|
||||
// target.
|
||||
// As such, we keep track of the smallest range size, so
|
||||
// that in cases like `({ name: "John" })` if the cursor is
|
||||
// inside the curly brackets, we target the curly brackets
|
||||
// instead of the parentheses.
|
||||
let mut bracket_pair = None;
|
||||
let mut min_range_size = usize::MAX;
|
||||
if let Some(pair) = object_to_surround_pair(object) {
|
||||
return Some(pair.to_bracket_pair());
|
||||
}
|
||||
|
||||
let _ = self.editor.update(cx, |editor, cx| {
|
||||
let display_map = editor.display_snapshot(cx);
|
||||
let selections = editor.selections.all_adjusted_display(&display_map);
|
||||
// Even if there's multiple cursors, we'll simply rely on
|
||||
// the first one to understand what bracket pair to map to.
|
||||
// I believe we could, if worth it, go one step above and
|
||||
// have a `BracketPair` per selection, so that `AnyBracket`
|
||||
// could work in situations where the transformation below
|
||||
// could be done.
|
||||
//
|
||||
// ```
|
||||
// (< name:ˇ'Zed' >)
|
||||
// <[ name:ˇ'DeltaDB' ]>
|
||||
// ```
|
||||
//
|
||||
// After using `csb{`:
|
||||
//
|
||||
// ```
|
||||
// (ˇ{ name:'Zed' })
|
||||
// <ˇ{ name:'DeltaDB' }>
|
||||
// ```
|
||||
if let Some(selection) = selections.first() {
|
||||
let relative_to = selection.head();
|
||||
let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
|
||||
let cursor_offset = relative_to.to_offset(&display_map, Bias::Left);
|
||||
if object != Object::AnyBrackets {
|
||||
return None;
|
||||
}
|
||||
|
||||
for &(open, close) in bracket_pairs.iter() {
|
||||
if let Some(range) = surrounding_markers(
|
||||
&display_map,
|
||||
relative_to,
|
||||
true,
|
||||
false,
|
||||
open,
|
||||
close,
|
||||
) {
|
||||
let start_offset = range.start.to_offset(&display_map, Bias::Left);
|
||||
let end_offset = range.end.to_offset(&display_map, Bias::Right);
|
||||
// If we're dealing with `AnyBrackets`, which can map to multiple bracket
|
||||
// pairs, we'll need to first determine which `BracketPair` to target.
|
||||
// As such, we keep track of the smallest range size, so that in cases
|
||||
// like `({ name: "John" })` if the cursor is inside the curly brackets,
|
||||
// we target the curly brackets instead of the parentheses.
|
||||
let mut best_pair = None;
|
||||
let mut min_range_size = usize::MAX;
|
||||
|
||||
if cursor_offset >= start_offset && cursor_offset <= end_offset {
|
||||
let size = end_offset - start_offset;
|
||||
if size < min_range_size {
|
||||
min_range_size = size;
|
||||
bracket_pair = Some(BracketPair {
|
||||
start: open.to_string(),
|
||||
end: close.to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
let _ = self.editor.update(cx, |editor, cx| {
|
||||
let display_map = editor.display_snapshot(cx);
|
||||
let selections = editor.selections.all_adjusted_display(&display_map);
|
||||
// Even if there's multiple cursors, we'll simply rely on the first one
|
||||
// to understand what bracket pair to map to. I believe we could, if
|
||||
// worth it, go one step above and have a `BracketPair` per selection, so
|
||||
// that `AnyBracket` could work in situations where the transformation
|
||||
// below could be done.
|
||||
//
|
||||
// ```
|
||||
// (< name:ˇ'Zed' >)
|
||||
// <[ name:ˇ'DeltaDB' ]>
|
||||
// ```
|
||||
//
|
||||
// After using `csb{`:
|
||||
//
|
||||
// ```
|
||||
// (ˇ{ name:'Zed' })
|
||||
// <ˇ{ name:'DeltaDB' }>
|
||||
// ```
|
||||
if let Some(selection) = selections.first() {
|
||||
let relative_to = selection.head();
|
||||
let cursor_offset = relative_to.to_offset(&display_map, Bias::Left);
|
||||
|
||||
for pair in BRACKET_PAIRS {
|
||||
if let Some(range) = surrounding_markers(
|
||||
&display_map,
|
||||
relative_to,
|
||||
true,
|
||||
false,
|
||||
pair.open,
|
||||
pair.close,
|
||||
) {
|
||||
let start_offset = range.start.to_offset(&display_map, Bias::Left);
|
||||
let end_offset = range.end.to_offset(&display_map, Bias::Right);
|
||||
|
||||
if cursor_offset >= start_offset && cursor_offset <= end_offset {
|
||||
let size = end_offset - start_offset;
|
||||
if size < min_range_size {
|
||||
min_range_size = size;
|
||||
best_pair = Some(*pair);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bracket_pair
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
best_pair.map(|p| p.to_bracket_pair())
|
||||
}
|
||||
}
|
||||
|
||||
fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
|
||||
pairs
|
||||
.iter()
|
||||
.find(|pair| pair.start == surround_alias(ch) || pair.end == surround_alias(ch))
|
||||
/// Convert an Object to its corresponding SurroundPair.
|
||||
fn object_to_surround_pair(object: Object) -> Option<SurroundPair> {
|
||||
let open = match object {
|
||||
Object::Quotes => '\'',
|
||||
Object::BackQuotes => '`',
|
||||
Object::DoubleQuotes => '"',
|
||||
Object::VerticalBars => '|',
|
||||
Object::Parentheses => '(',
|
||||
Object::SquareBrackets => '[',
|
||||
Object::CurlyBrackets { .. } => '{',
|
||||
Object::AngleBrackets => '<',
|
||||
_ => return None,
|
||||
};
|
||||
surround_pair_for_char_vim(open)
|
||||
}
|
||||
|
||||
fn surround_alias(ch: &str) -> &str {
|
||||
pub fn surround_alias(ch: &str) -> &str {
|
||||
match ch {
|
||||
"b" => ")",
|
||||
"B" => "}",
|
||||
@@ -584,79 +573,65 @@ fn surround_alias(ch: &str) -> &str {
|
||||
}
|
||||
}
|
||||
|
||||
fn all_support_surround_pair() -> Vec<BracketPair> {
|
||||
vec![
|
||||
BracketPair {
|
||||
start: "{".into(),
|
||||
end: "}".into(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
},
|
||||
BracketPair {
|
||||
start: "'".into(),
|
||||
end: "'".into(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
},
|
||||
BracketPair {
|
||||
start: "`".into(),
|
||||
end: "`".into(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
},
|
||||
BracketPair {
|
||||
start: "\"".into(),
|
||||
end: "\"".into(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
},
|
||||
BracketPair {
|
||||
start: "(".into(),
|
||||
end: ")".into(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
},
|
||||
BracketPair {
|
||||
start: "|".into(),
|
||||
end: "|".into(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
},
|
||||
BracketPair {
|
||||
start: "[".into(),
|
||||
end: "]".into(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
},
|
||||
BracketPair {
|
||||
start: "<".into(),
|
||||
end: ">".into(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
},
|
||||
]
|
||||
fn literal_surround_pair(ch: char) -> Option<SurroundPair> {
|
||||
SURROUND_PAIRS
|
||||
.iter()
|
||||
.find(|p| p.open == ch || p.close == ch)
|
||||
.copied()
|
||||
}
|
||||
|
||||
fn pair_to_object(pair: &BracketPair) -> Option<Object> {
|
||||
match pair.start.as_str() {
|
||||
"'" => Some(Object::Quotes),
|
||||
"`" => Some(Object::BackQuotes),
|
||||
"\"" => Some(Object::DoubleQuotes),
|
||||
"|" => Some(Object::VerticalBars),
|
||||
"(" => Some(Object::Parentheses),
|
||||
"[" => Some(Object::SquareBrackets),
|
||||
"{" => Some(Object::CurlyBrackets),
|
||||
"<" => Some(Object::AngleBrackets),
|
||||
_ => None,
|
||||
/// Resolve a character (including Vim aliases) to its surround pair.
|
||||
/// Returns None for 'm' (match nearest) or unknown chars.
|
||||
pub fn surround_pair_for_char_vim(ch: char) -> Option<SurroundPair> {
|
||||
let resolved = match ch {
|
||||
'b' => ')',
|
||||
'B' => '}',
|
||||
'r' => ']',
|
||||
'a' => '>',
|
||||
'm' => return None,
|
||||
_ => ch,
|
||||
};
|
||||
literal_surround_pair(resolved)
|
||||
}
|
||||
|
||||
/// Get a BracketPair for the given string, with fallback for unknown chars.
|
||||
/// For vim surround operations that accept any character as a surround.
|
||||
pub fn bracket_pair_for_str_vim(text: &str) -> BracketPair {
|
||||
text.chars()
|
||||
.next()
|
||||
.and_then(surround_pair_for_char_vim)
|
||||
.map(|p| p.to_bracket_pair())
|
||||
.unwrap_or_else(|| BracketPair {
|
||||
start: text.to_string(),
|
||||
end: text.to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve a character to its surround pair using Helix semantics (no Vim aliases).
|
||||
/// Returns None only for 'm' (match nearest). Unknown chars map to symmetric pairs.
|
||||
pub fn surround_pair_for_char_helix(ch: char) -> Option<SurroundPair> {
|
||||
if ch == 'm' {
|
||||
return None;
|
||||
}
|
||||
literal_surround_pair(ch).or_else(|| Some(SurroundPair::new(ch, ch)))
|
||||
}
|
||||
|
||||
/// Get a BracketPair for the given string in Helix mode (literal, symmetric fallback).
|
||||
pub fn bracket_pair_for_str_helix(text: &str) -> BracketPair {
|
||||
text.chars()
|
||||
.next()
|
||||
.and_then(surround_pair_for_char_helix)
|
||||
.map(|p| p.to_bracket_pair())
|
||||
.unwrap_or_else(|| BracketPair {
|
||||
start: text.to_string(),
|
||||
end: text.to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1702,4 +1677,41 @@ mod test {
|
||||
Mode::Normal,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_surround_pair_for_char() {
|
||||
use super::{SURROUND_PAIRS, surround_pair_for_char_helix, surround_pair_for_char_vim};
|
||||
|
||||
fn as_tuple(pair: Option<super::SurroundPair>) -> Option<(char, char)> {
|
||||
pair.map(|p| (p.open, p.close))
|
||||
}
|
||||
|
||||
assert_eq!(as_tuple(surround_pair_for_char_vim('b')), Some(('(', ')')));
|
||||
assert_eq!(as_tuple(surround_pair_for_char_vim('B')), Some(('{', '}')));
|
||||
assert_eq!(as_tuple(surround_pair_for_char_vim('r')), Some(('[', ']')));
|
||||
assert_eq!(as_tuple(surround_pair_for_char_vim('a')), Some(('<', '>')));
|
||||
|
||||
assert_eq!(surround_pair_for_char_vim('m'), None);
|
||||
|
||||
for pair in SURROUND_PAIRS {
|
||||
assert_eq!(
|
||||
as_tuple(surround_pair_for_char_vim(pair.open)),
|
||||
Some((pair.open, pair.close))
|
||||
);
|
||||
assert_eq!(
|
||||
as_tuple(surround_pair_for_char_vim(pair.close)),
|
||||
Some((pair.open, pair.close))
|
||||
);
|
||||
}
|
||||
|
||||
// Test unknown char returns None
|
||||
assert_eq!(surround_pair_for_char_vim('x'), None);
|
||||
|
||||
// Helix resolves literal chars and falls back to symmetric pairs.
|
||||
assert_eq!(
|
||||
as_tuple(surround_pair_for_char_helix('*')),
|
||||
Some(('*', '*'))
|
||||
);
|
||||
assert_eq!(surround_pair_for_char_helix('m'), None);
|
||||
}
|
||||
}
|
||||
@@ -252,6 +252,12 @@ actions!(
|
||||
ToggleProjectPanelFocus,
|
||||
/// Starts a match operation.
|
||||
PushHelixMatch,
|
||||
/// Adds surrounding characters in Helix mode.
|
||||
PushHelixSurroundAdd,
|
||||
/// Replaces surrounding characters in Helix mode.
|
||||
PushHelixSurroundReplace,
|
||||
/// Deletes surrounding characters in Helix mode.
|
||||
PushHelixSurroundDelete,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -1888,6 +1894,60 @@ impl Vim {
|
||||
}
|
||||
_ => self.clear_operator(window, cx),
|
||||
},
|
||||
Some(Operator::HelixSurroundAdd) => match self.mode {
|
||||
Mode::HelixNormal | Mode::HelixSelect => {
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() {
|
||||
selection.end = movement::right(map, selection.start);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
self.helix_surround_add(&text, window, cx);
|
||||
self.switch_mode(Mode::HelixNormal, false, window, cx);
|
||||
self.clear_operator(window, cx);
|
||||
}
|
||||
_ => self.clear_operator(window, cx),
|
||||
},
|
||||
Some(Operator::HelixSurroundReplace {
|
||||
replaced_char: Some(old),
|
||||
}) => match self.mode {
|
||||
Mode::HelixNormal | Mode::HelixSelect => {
|
||||
if let Some(new_char) = text.chars().next() {
|
||||
self.helix_surround_replace(old, new_char, window, cx);
|
||||
}
|
||||
self.clear_operator(window, cx);
|
||||
}
|
||||
_ => self.clear_operator(window, cx),
|
||||
},
|
||||
Some(Operator::HelixSurroundReplace {
|
||||
replaced_char: None,
|
||||
}) => match self.mode {
|
||||
Mode::HelixNormal | Mode::HelixSelect => {
|
||||
if let Some(ch) = text.chars().next() {
|
||||
self.pop_operator(window, cx);
|
||||
self.push_operator(
|
||||
Operator::HelixSurroundReplace {
|
||||
replaced_char: Some(ch),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => self.clear_operator(window, cx),
|
||||
},
|
||||
Some(Operator::HelixSurroundDelete) => match self.mode {
|
||||
Mode::HelixNormal | Mode::HelixSelect => {
|
||||
if let Some(ch) = text.chars().next() {
|
||||
self.helix_surround_delete(ch, window, cx);
|
||||
}
|
||||
self.clear_operator(window, cx);
|
||||
}
|
||||
_ => self.clear_operator(window, cx),
|
||||
},
|
||||
Some(Operator::Mark) => self.create_mark(text, window, cx),
|
||||
Some(Operator::RecordRegister) => {
|
||||
self.record_register(text.chars().next().unwrap(), window, cx)
|
||||
|
||||
Reference in new issue
Block a user