Skip to repository content1857 lines · 62.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:31:14.992Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
surrounds.rs
1use crate::{
2 Vim,
3 motion::{self, Motion},
4 object::{Object, surrounding_markers},
5 state::Mode,
6};
7use editor::{Anchor, Bias, MultiBufferOffset, ToOffset, movement};
8use gpui::{Context, Window};
9use language::BracketPair;
10
11use std::sync::Arc;
12
13/// A char-based surround pair definition.
14/// Single source of truth for all supported surround pairs.
15#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct SurroundPair {
17 pub open: char,
18 pub close: char,
19}
20
21impl SurroundPair {
22 pub const fn new(open: char, close: char) -> Self {
23 Self { open, close }
24 }
25
26 pub fn to_bracket_pair(self) -> BracketPair {
27 BracketPair {
28 start: self.open.to_string(),
29 end: self.close.to_string(),
30 close: true,
31 surround: true,
32 newline: false,
33 }
34 }
35
36 pub fn to_object(self) -> Option<Object> {
37 match self.open {
38 '\'' => Some(Object::Quotes),
39 '`' => Some(Object::BackQuotes),
40 '"' => Some(Object::DoubleQuotes),
41 '|' => Some(Object::VerticalBars),
42 '(' => Some(Object::Parentheses),
43 '[' => Some(Object::SquareBrackets),
44 '{' => Some(Object::CurlyBrackets),
45 '<' => Some(Object::AngleBrackets),
46 _ => None,
47 }
48 }
49}
50
51/// All supported surround pairs - single source of truth.
52pub const SURROUND_PAIRS: &[SurroundPair] = &[
53 SurroundPair::new('(', ')'),
54 SurroundPair::new('[', ']'),
55 SurroundPair::new('{', '}'),
56 SurroundPair::new('<', '>'),
57 SurroundPair::new('"', '"'),
58 SurroundPair::new('\'', '\''),
59 SurroundPair::new('`', '`'),
60 SurroundPair::new('|', '|'),
61];
62
63/// Bracket-only pairs for AnyBrackets matching.
64pub const BRACKET_PAIRS: &[SurroundPair] = &[
65 SurroundPair::new('(', ')'),
66 SurroundPair::new('[', ']'),
67 SurroundPair::new('{', '}'),
68 SurroundPair::new('<', '>'),
69];
70
71/// Quote-only pairs for AnyQuotes matching.
72pub const QUOTE_PAIRS: &[SurroundPair] = &[
73 SurroundPair::new('"', '"'),
74 SurroundPair::new('\'', '\''),
75 SurroundPair::new('`', '`'),
76];
77
78#[derive(Clone, Debug, PartialEq, Eq)]
79pub enum SurroundsType {
80 Motion(Motion),
81 Object(Object, bool),
82 Selection,
83}
84
85impl Vim {
86 pub fn add_surrounds(
87 &mut self,
88 text: Arc<str>,
89 target: SurroundsType,
90 window: &mut Window,
91 cx: &mut Context<Self>,
92 ) {
93 self.stop_recording(cx);
94 let count = Vim::take_count(cx);
95 let forced_motion = Vim::take_forced_motion(cx);
96 let mode = self.mode;
97 self.update_editor(cx, |_, editor, cx| {
98 let text_layout_details = editor.text_layout_details(window, cx);
99 editor.transact(window, cx, |editor, window, cx| {
100 editor.set_clip_at_line_ends(false, cx);
101
102 let pair = bracket_pair_for_str_vim(&text);
103 let surround = pair.end != surround_alias((*text).as_ref());
104 let display_map = editor.display_snapshot(cx);
105 let display_selections = editor.selections.all_adjusted_display(&display_map);
106 let mut edits = Vec::new();
107 let mut anchors = Vec::new();
108
109 for selection in &display_selections {
110 let range = match &target {
111 SurroundsType::Object(object, around) => {
112 object.range(&display_map, selection.clone(), *around, None)
113 }
114 SurroundsType::Motion(motion) => {
115 motion
116 .range(
117 &display_map,
118 selection.clone(),
119 count,
120 &text_layout_details,
121 forced_motion,
122 )
123 .map(|(mut range, _)| {
124 // The Motion::CurrentLine operation will contain the newline of the current line and leading/trailing whitespace
125 if let Motion::CurrentLine = motion {
126 range.start = motion::first_non_whitespace(
127 &display_map,
128 false,
129 range.start,
130 );
131 range.end = movement::saturating_right(
132 &display_map,
133 motion::last_non_whitespace(&display_map, range.end, 1),
134 );
135 }
136 range
137 })
138 }
139 SurroundsType::Selection => Some(selection.range()),
140 };
141
142 if let Some(range) = range {
143 let start = range.start.to_offset(&display_map, Bias::Right);
144 let end = range.end.to_offset(&display_map, Bias::Left);
145 let (start_cursor_str, end_cursor_str) = if mode == Mode::VisualLine {
146 (format!("{}\n", pair.start), format!("\n{}", pair.end))
147 } else {
148 let maybe_space = if surround { " " } else { "" };
149 (
150 format!("{}{}", pair.start, maybe_space),
151 format!("{}{}", maybe_space, pair.end),
152 )
153 };
154 let start_anchor = display_map.buffer_snapshot().anchor_before(start);
155
156 edits.push((start..start, start_cursor_str));
157 edits.push((end..end, end_cursor_str));
158 anchors.push(start_anchor..start_anchor);
159 } else {
160 let start_anchor = display_map
161 .buffer_snapshot()
162 .anchor_before(selection.head().to_offset(&display_map, Bias::Left));
163 anchors.push(start_anchor..start_anchor);
164 }
165 }
166
167 editor.edit(edits, cx);
168 editor.set_clip_at_line_ends(true, cx);
169 editor.change_selections(Default::default(), window, cx, |s| {
170 if mode == Mode::VisualBlock {
171 s.select_anchor_ranges(anchors.into_iter().take(1))
172 } else {
173 s.select_anchor_ranges(anchors)
174 }
175 });
176 });
177 });
178 self.switch_mode(Mode::Normal, false, window, cx);
179 }
180
181 pub fn delete_surrounds(
182 &mut self,
183 text: Arc<str>,
184 window: &mut Window,
185 cx: &mut Context<Self>,
186 ) {
187 self.stop_recording(cx);
188
189 // only legitimate surrounds can be removed
190 let Some(first_char) = text.chars().next() else {
191 return;
192 };
193 let Some(surround_pair) = surround_pair_for_char_vim(first_char) else {
194 return;
195 };
196 let Some(pair_object) = surround_pair.to_object() else {
197 return;
198 };
199 let pair = surround_pair.to_bracket_pair();
200 let surround = pair.end != *text;
201
202 self.update_editor(cx, |_, editor, cx| {
203 editor.transact(window, cx, |editor, window, cx| {
204 editor.set_clip_at_line_ends(false, cx);
205
206 let display_map = editor.display_snapshot(cx);
207 let display_selections = editor.selections.all_display(&display_map);
208 let mut edits = Vec::new();
209 let mut anchors = Vec::new();
210
211 for selection in &display_selections {
212 let start = selection.start.to_offset(&display_map, Bias::Left);
213 if let Some(range) =
214 pair_object.range(&display_map, selection.clone(), true, None)
215 {
216 // If the current parenthesis object is single-line,
217 // then we need to filter whether it is the current line or not
218 if !pair_object.is_multiline() {
219 let is_same_row = selection.start.row() == range.start.row()
220 && selection.end.row() == range.end.row();
221 if !is_same_row {
222 anchors.push(start..start);
223 continue;
224 }
225 }
226 // This is a bit cumbersome, and it is written to deal with some special cases, as shown below
227 // hello«ˇ "hello in a word" »again.
228 // Sometimes the expand_selection will not be matched at both ends, and there will be extra spaces
229 // In order to be able to accurately match and replace in this case, some cumbersome methods are used
230 let mut chars_and_offset = display_map
231 .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
232 .peekable();
233 while let Some((ch, offset)) = chars_and_offset.next() {
234 if ch.to_string() == pair.start {
235 let start = offset;
236 let mut end = start + 1usize;
237 if surround
238 && let Some((next_ch, _)) = chars_and_offset.peek()
239 && next_ch.eq(&' ')
240 {
241 end += 1;
242 }
243 edits.push((start..end, ""));
244 anchors.push(start..start);
245 break;
246 }
247 }
248 let mut reverse_chars_and_offsets = display_map
249 .reverse_buffer_chars_at(range.end.to_offset(&display_map, Bias::Left))
250 .peekable();
251 while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
252 if ch.to_string() == pair.end {
253 let mut start = offset;
254 let end = start + 1usize;
255 if surround
256 && let Some((next_ch, _)) = reverse_chars_and_offsets.peek()
257 && next_ch.eq(&' ')
258 {
259 start -= 1;
260 }
261 edits.push((start..end, ""));
262 break;
263 }
264 }
265 } else {
266 anchors.push(start..start);
267 }
268 }
269
270 editor.change_selections(Default::default(), window, cx, |s| {
271 s.select_ranges(anchors);
272 });
273 edits.sort_by_key(|(range, _)| range.start);
274 editor.edit(edits, cx);
275 editor.set_clip_at_line_ends(true, cx);
276 });
277 });
278 }
279
280 pub fn change_surrounds(
281 &mut self,
282 text: Arc<str>,
283 target: Object,
284 opening: bool,
285 bracket_anchors: Vec<Option<(Anchor, Anchor)>>,
286 window: &mut Window,
287 cx: &mut Context<Self>,
288 ) {
289 if let Some(will_replace_pair) = self.object_to_bracket_pair(target, cx) {
290 self.stop_recording(cx);
291 self.update_editor(cx, |_, editor, cx| {
292 editor.transact(window, cx, |editor, window, cx| {
293 editor.set_clip_at_line_ends(false, cx);
294
295 let pair = bracket_pair_for_str_vim(&text);
296
297 // A single space should be added if the new surround is a
298 // bracket and not a quote (pair.start != pair.end) and if
299 // the bracket used is the opening bracket.
300 let add_space =
301 !(pair.start == pair.end) && (pair.end != surround_alias((*text).as_ref()));
302
303 // Space should be preserved if either the surrounding
304 // characters being updated are quotes
305 // (will_replace_pair.start == will_replace_pair.end) or if
306 // the bracket used in the command is not an opening
307 // bracket.
308 let preserve_space =
309 will_replace_pair.start == will_replace_pair.end || !opening;
310
311 let display_map = editor.display_snapshot(cx);
312 let mut edits = Vec::new();
313
314 // Collect (open_offset, close_offset) pairs to replace from the
315 // pre-computed anchors stored during check_and_move_to_valid_bracket_pair.
316 let mut pairs_to_replace: Vec<(MultiBufferOffset, MultiBufferOffset)> =
317 Vec::new();
318 let snapshot = display_map.buffer_snapshot();
319 for anchors in &bracket_anchors {
320 let Some((open_anchor, close_anchor)) = anchors else {
321 continue;
322 };
323 let pair = (
324 open_anchor.to_offset(&snapshot),
325 close_anchor.to_offset(&snapshot),
326 );
327 if !pairs_to_replace.contains(&pair) {
328 pairs_to_replace.push(pair);
329 }
330 }
331
332 for (open_offset, close_offset) in pairs_to_replace {
333 let mut open_str = pair.start.clone();
334 let mut chars_and_offset =
335 display_map.buffer_chars_at(open_offset).peekable();
336 chars_and_offset.next(); // skip the bracket itself
337 let mut open_range_end = open_offset + 1usize;
338 while let Some((next_ch, _)) = chars_and_offset.next()
339 && next_ch == ' '
340 {
341 open_range_end += 1;
342 if preserve_space {
343 open_str.push(next_ch);
344 }
345 }
346 if add_space {
347 open_str.push(' ');
348 }
349 let edit_len = open_range_end - open_offset;
350 edits.push((open_offset..open_range_end, open_str));
351
352 let mut close_str = String::new();
353 let close_end = close_offset + 1usize;
354 let mut close_start = close_offset;
355 for (next_ch, _) in display_map.reverse_buffer_chars_at(close_offset) {
356 if next_ch != ' '
357 || close_str.len() >= edit_len - 1
358 || close_start <= open_range_end
359 {
360 break;
361 }
362 close_start -= 1;
363 if preserve_space {
364 close_str.push(next_ch);
365 }
366 }
367 if add_space {
368 close_str.push(' ');
369 }
370 close_str.push_str(&pair.end);
371 edits.push((close_start..close_end, close_str));
372 }
373
374 let stable_anchors = editor
375 .selections
376 .disjoint_anchors_arc()
377 .iter()
378 .map(|selection| {
379 let start = selection.start.bias_left(&display_map.buffer_snapshot());
380 start..start
381 })
382 .collect::<Vec<_>>();
383 edits.sort_by_key(|(range, _)| range.start);
384 editor.edit(edits, cx);
385 editor.set_clip_at_line_ends(true, cx);
386 editor.change_selections(Default::default(), window, cx, |s| {
387 s.select_anchor_ranges(stable_anchors);
388 });
389 });
390 });
391 }
392 }
393
394 /// **Only intended for use by the `cs` (change surrounds) operator.**
395 ///
396 /// For each cursor, checks whether it is surrounded by a valid bracket pair for the given
397 /// object. Moves each cursor to the opening bracket of its found pair, and returns a
398 /// `Vec<Option<(Anchor, Anchor)>>` with one entry per selection containing the pre-computed
399 /// open and close bracket positions.
400 ///
401 /// Storing these anchors avoids re-running the bracket search from the moved cursor position,
402 /// which can misidentify the opening bracket for symmetric quote characters when the same
403 /// character appears earlier on the line (e.g. `I'm 'good'`).
404 ///
405 /// Returns an empty `Vec` if no valid pair was found for any cursor.
406 pub fn prepare_and_move_to_valid_bracket_pair(
407 &mut self,
408 object: Object,
409 window: &mut Window,
410 cx: &mut Context<Self>,
411 ) -> Vec<Option<(Anchor, Anchor)>> {
412 let mut matched_pair_anchors: Vec<Option<(Anchor, Anchor)>> = Vec::new();
413 if let Some(pair) = self.object_to_bracket_pair(object, cx) {
414 self.update_editor(cx, |_, editor, cx| {
415 editor.transact(window, cx, |editor, window, cx| {
416 editor.set_clip_at_line_ends(false, cx);
417 let display_map = editor.display_snapshot(cx);
418 let selections = editor.selections.all_adjusted_display(&display_map);
419 let mut updated_cursor_ranges = Vec::new();
420
421 for selection in &selections {
422 let start = selection.start.to_offset(&display_map, Bias::Left);
423 let in_range = object
424 .range(&display_map, selection.clone(), true, None)
425 .filter(|range| {
426 object.is_multiline()
427 || (selection.start.row() == range.start.row()
428 && selection.end.row() == range.end.row())
429 });
430 let Some(range) = in_range else {
431 updated_cursor_ranges.push(start..start);
432 matched_pair_anchors.push(None);
433 continue;
434 };
435
436 let range_start = range.start.to_offset(&display_map, Bias::Left);
437 let range_end = range.end.to_offset(&display_map, Bias::Left);
438 let open_offset = display_map
439 .buffer_chars_at(range_start)
440 .find(|(ch, _)| ch.to_string() == pair.start)
441 .map(|(_, offset)| offset);
442 let close_offset = display_map
443 .reverse_buffer_chars_at(range_end)
444 .find(|(ch, _)| ch.to_string() == pair.end)
445 .map(|(_, offset)| offset);
446
447 if let (Some(open), Some(close)) = (open_offset, close_offset) {
448 let snapshot = &display_map.buffer_snapshot();
449 updated_cursor_ranges.push(open..open);
450 matched_pair_anchors.push(Some((
451 snapshot.anchor_before(open),
452 snapshot.anchor_before(close),
453 )));
454 } else {
455 updated_cursor_ranges.push(start..start);
456 matched_pair_anchors.push(None);
457 }
458 }
459 editor.change_selections(Default::default(), window, cx, |s| {
460 s.select_ranges(updated_cursor_ranges);
461 });
462 editor.set_clip_at_line_ends(true, cx);
463
464 if !matched_pair_anchors.iter().any(|a| a.is_some()) {
465 matched_pair_anchors.clear();
466 }
467 });
468 });
469 }
470 matched_pair_anchors
471 }
472
473 fn object_to_bracket_pair(
474 &self,
475 object: Object,
476 cx: &mut Context<Self>,
477 ) -> Option<BracketPair> {
478 if let Some(pair) = object_to_surround_pair(object) {
479 return Some(pair.to_bracket_pair());
480 }
481
482 match object {
483 Object::AnyBrackets => self.any_pair(BRACKET_PAIRS, cx),
484 Object::AnyQuotes => self.any_pair(QUOTE_PAIRS, cx),
485 Object::MiniQuotes | Object::MiniBrackets => self.mini_pair(object, cx),
486 _ => None,
487 }
488 }
489
490 fn any_pair(
491 &self,
492 allowed_pairs: &[SurroundPair],
493 cx: &mut Context<Self>,
494 ) -> Option<BracketPair> {
495 // If we're dealing with `AnyBrackets`, which can map to multiple bracket
496 // pairs, we'll need to first determine which `BracketPair` to target.
497 // As such, we keep track of the smallest range size, so that in cases
498 // like `({ name: "John" })` if the cursor is inside the curly brackets,
499 // we target the curly brackets instead of the parentheses.
500 let mut best_pair = None;
501 let mut min_range_size = usize::MAX;
502
503 let _ = self.editor.update(cx, |editor, cx| {
504 let display_map = editor.display_snapshot(cx);
505 let selections = editor.selections.all_adjusted_display(&display_map);
506 // Even if there's multiple cursors, we'll simply rely on the first one
507 // to understand what bracket pair to map to. I believe we could, if
508 // worth it, go one step above and have a `BracketPair` per selection, so
509 // that `AnyBracket` could work in situations where the transformation
510 // below could be done.
511 //
512 // ```
513 // (< name:ˇ'Zed' >)
514 // <[ name:ˇ'DeltaDB' ]>
515 // ```
516 //
517 // After using `csb{`:
518 //
519 // ```
520 // (ˇ{ name:'Zed' })
521 // <ˇ{ name:'DeltaDB' }>
522 // ```
523 if let Some(selection) = selections.first() {
524 let relative_to = selection.head();
525 let cursor_offset = relative_to.to_offset(&display_map, Bias::Left);
526
527 for pair in allowed_pairs {
528 if let Some(range) = surrounding_markers(
529 &display_map,
530 relative_to,
531 true,
532 false,
533 pair.open,
534 pair.close,
535 ) {
536 let start_offset = range.start.to_offset(&display_map, Bias::Left);
537 let end_offset = range.end.to_offset(&display_map, Bias::Right);
538
539 if cursor_offset >= start_offset && cursor_offset <= end_offset {
540 let size = end_offset - start_offset;
541 if size < min_range_size {
542 min_range_size = size;
543 best_pair = Some(*pair);
544 }
545 }
546 }
547 }
548 }
549 });
550
551 best_pair.map(|p| p.to_bracket_pair())
552 }
553
554 fn mini_pair(&self, object: Object, cx: &mut Context<Self>) -> Option<BracketPair> {
555 self.editor
556 .update(cx, |editor, cx| {
557 let display_map = editor.display_snapshot(cx);
558 let selections = editor.selections.all_adjusted_display(&display_map);
559 // For now, only primary selection is used to select the bracket/quote pair. It might be weird
560 // if multi-select resulted in different quote kinds being replaced for different selections.
561 // any_pair uses the same logic, so this should be consistent across {Any,Mini}{Quotes,Brackets}
562 let selection = selections.first()?.clone();
563 let range = object.range(&display_map, selection, true, None)?;
564 let start_offset = range.start.to_offset(&display_map, Bias::Left);
565 let (pair_char, _) = display_map.buffer_chars_at(start_offset).next()?;
566 literal_surround_pair(pair_char)
567 })
568 .ok()
569 .flatten()
570 .map(|surround| surround.to_bracket_pair())
571 }
572}
573
574/// Convert an Object to its corresponding SurroundPair.
575fn object_to_surround_pair(object: Object) -> Option<SurroundPair> {
576 let open = match object {
577 Object::Quotes => '\'',
578 Object::BackQuotes => '`',
579 Object::DoubleQuotes => '"',
580 Object::VerticalBars => '|',
581 Object::Parentheses => '(',
582 Object::SquareBrackets => '[',
583 Object::CurlyBrackets { .. } => '{',
584 Object::AngleBrackets => '<',
585 _ => return None,
586 };
587 surround_pair_for_char_vim(open)
588}
589
590pub fn surround_alias(ch: &str) -> &str {
591 match ch {
592 "b" => ")",
593 "B" => "}",
594 "a" => ">",
595 "r" => "]",
596 _ => ch,
597 }
598}
599
600fn literal_surround_pair(ch: char) -> Option<SurroundPair> {
601 SURROUND_PAIRS
602 .iter()
603 .find(|p| p.open == ch || p.close == ch)
604 .copied()
605}
606
607/// Resolve a character (including Vim aliases) to its surround pair.
608/// Returns None for 'm' (match nearest) or unknown chars.
609pub fn surround_pair_for_char_vim(ch: char) -> Option<SurroundPair> {
610 let resolved = match ch {
611 'b' => ')',
612 'B' => '}',
613 'r' => ']',
614 'a' => '>',
615 'm' => return None,
616 _ => ch,
617 };
618 literal_surround_pair(resolved)
619}
620
621/// Get a BracketPair for the given string, with fallback for unknown chars.
622/// For vim surround operations that accept any character as a surround.
623pub fn bracket_pair_for_str_vim(text: &str) -> BracketPair {
624 text.chars()
625 .next()
626 .and_then(surround_pair_for_char_vim)
627 .map(|p| p.to_bracket_pair())
628 .unwrap_or_else(|| BracketPair {
629 start: text.to_string(),
630 end: text.to_string(),
631 close: true,
632 surround: true,
633 newline: false,
634 })
635}
636
637/// Resolve a character to its surround pair using Helix semantics (no Vim aliases).
638/// Returns None only for 'm' (match nearest). Unknown chars map to symmetric pairs.
639pub fn surround_pair_for_char_helix(ch: char) -> Option<SurroundPair> {
640 if ch == 'm' {
641 return None;
642 }
643 literal_surround_pair(ch).or_else(|| Some(SurroundPair::new(ch, ch)))
644}
645
646/// Get a BracketPair for the given string in Helix mode (literal, symmetric fallback).
647pub fn bracket_pair_for_str_helix(text: &str) -> BracketPair {
648 text.chars()
649 .next()
650 .and_then(surround_pair_for_char_helix)
651 .map(|p| p.to_bracket_pair())
652 .unwrap_or_else(|| BracketPair {
653 start: text.to_string(),
654 end: text.to_string(),
655 close: true,
656 surround: true,
657 newline: false,
658 })
659}
660
661#[cfg(test)]
662mod test {
663 use gpui::KeyBinding;
664 use indoc::indoc;
665
666 use crate::{
667 PushAddSurrounds,
668 object::{AnyBrackets, AnyQuotes, MiniBrackets, MiniQuotes},
669 state::Mode,
670 test::VimTestContext,
671 };
672
673 #[gpui::test]
674 async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
675 let mut cx = VimTestContext::new(cx, true).await;
676
677 // test add surrounds with around
678 cx.set_state(
679 indoc! {"
680 The quˇick brown
681 fox jumps over
682 the lazy dog."},
683 Mode::Normal,
684 );
685 cx.simulate_keystrokes("y s i w {");
686 cx.assert_state(
687 indoc! {"
688 The ˇ{ quick } brown
689 fox jumps over
690 the lazy dog."},
691 Mode::Normal,
692 );
693
694 // test add surrounds not with around
695 cx.set_state(
696 indoc! {"
697 The quˇick brown
698 fox jumps over
699 the lazy dog."},
700 Mode::Normal,
701 );
702 cx.simulate_keystrokes("y s i w }");
703 cx.assert_state(
704 indoc! {"
705 The ˇ{quick} brown
706 fox jumps over
707 the lazy dog."},
708 Mode::Normal,
709 );
710
711 // test add surrounds with motion
712 cx.set_state(
713 indoc! {"
714 The quˇick brown
715 fox jumps over
716 the lazy dog."},
717 Mode::Normal,
718 );
719 cx.simulate_keystrokes("y s $ }");
720 cx.assert_state(
721 indoc! {"
722 The quˇ{ick brown}
723 fox jumps over
724 the lazy dog."},
725 Mode::Normal,
726 );
727
728 // test add surrounds with multi cursor
729 cx.set_state(
730 indoc! {"
731 The quˇick brown
732 fox jumps over
733 the laˇzy dog."},
734 Mode::Normal,
735 );
736 cx.simulate_keystrokes("y s i w '");
737 cx.assert_state(
738 indoc! {"
739 The ˇ'quick' brown
740 fox jumps over
741 the ˇ'lazy' dog."},
742 Mode::Normal,
743 );
744
745 // test multi cursor add surrounds with motion
746 cx.set_state(
747 indoc! {"
748 The quˇick brown
749 fox jumps over
750 the laˇzy dog."},
751 Mode::Normal,
752 );
753 cx.simulate_keystrokes("y s $ '");
754 cx.assert_state(
755 indoc! {"
756 The quˇ'ick brown'
757 fox jumps over
758 the laˇ'zy dog.'"},
759 Mode::Normal,
760 );
761
762 // test multi cursor add surrounds with motion and custom string
763 cx.set_state(
764 indoc! {"
765 The quˇick brown
766 fox jumps over
767 the laˇzy dog."},
768 Mode::Normal,
769 );
770 cx.simulate_keystrokes("y s $ 1");
771 cx.assert_state(
772 indoc! {"
773 The quˇ1ick brown1
774 fox jumps over
775 the laˇ1zy dog.1"},
776 Mode::Normal,
777 );
778
779 // test add surrounds with motion current line
780 cx.set_state(
781 indoc! {"
782 The quˇick brown
783 fox jumps over
784 the lazy dog."},
785 Mode::Normal,
786 );
787 cx.simulate_keystrokes("y s s {");
788 cx.assert_state(
789 indoc! {"
790 ˇ{ The quick brown }
791 fox jumps over
792 the lazy dog."},
793 Mode::Normal,
794 );
795
796 cx.set_state(
797 indoc! {"
798 The quˇick brown•
799 fox jumps over
800 the lazy dog."},
801 Mode::Normal,
802 );
803 cx.simulate_keystrokes("y s s {");
804 cx.assert_state(
805 indoc! {"
806 ˇ{ The quick brown }•
807 fox jumps over
808 the lazy dog."},
809 Mode::Normal,
810 );
811 cx.simulate_keystrokes("2 y s s )");
812 cx.assert_state(
813 indoc! {"
814 ˇ({ The quick brown }•
815 fox jumps over)
816 the lazy dog."},
817 Mode::Normal,
818 );
819
820 // test add surrounds around object
821 cx.set_state(
822 indoc! {"
823 The [quˇick] brown
824 fox jumps over
825 the lazy dog."},
826 Mode::Normal,
827 );
828 cx.simulate_keystrokes("y s a ] )");
829 cx.assert_state(
830 indoc! {"
831 The ˇ([quick]) brown
832 fox jumps over
833 the lazy dog."},
834 Mode::Normal,
835 );
836
837 // test add surrounds inside object
838 cx.set_state(
839 indoc! {"
840 The [quˇick] brown
841 fox jumps over
842 the lazy dog."},
843 Mode::Normal,
844 );
845 cx.simulate_keystrokes("y s i ] )");
846 cx.assert_state(
847 indoc! {"
848 The [ˇ(quick)] brown
849 fox jumps over
850 the lazy dog."},
851 Mode::Normal,
852 );
853 }
854
855 #[gpui::test]
856 async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) {
857 let mut cx = VimTestContext::new(cx, true).await;
858
859 cx.update(|_, cx| {
860 cx.bind_keys([KeyBinding::new(
861 "shift-s",
862 PushAddSurrounds {},
863 Some("vim_mode == visual"),
864 )])
865 });
866
867 // test add surrounds with around
868 cx.set_state(
869 indoc! {"
870 The quˇick brown
871 fox jumps over
872 the lazy dog."},
873 Mode::Normal,
874 );
875 cx.simulate_keystrokes("v i w shift-s {");
876 cx.assert_state(
877 indoc! {"
878 The ˇ{ quick } brown
879 fox jumps over
880 the lazy dog."},
881 Mode::Normal,
882 );
883
884 // test add surrounds not with around
885 cx.set_state(
886 indoc! {"
887 The quˇick brown
888 fox jumps over
889 the lazy dog."},
890 Mode::Normal,
891 );
892 cx.simulate_keystrokes("v i w shift-s }");
893 cx.assert_state(
894 indoc! {"
895 The ˇ{quick} brown
896 fox jumps over
897 the lazy dog."},
898 Mode::Normal,
899 );
900
901 // test add surrounds with motion
902 cx.set_state(
903 indoc! {"
904 The quˇick brown
905 fox jumps over
906 the lazy dog."},
907 Mode::Normal,
908 );
909 cx.simulate_keystrokes("v e shift-s }");
910 cx.assert_state(
911 indoc! {"
912 The quˇ{ick} brown
913 fox jumps over
914 the lazy dog."},
915 Mode::Normal,
916 );
917
918 // test add surrounds with multi cursor
919 cx.set_state(
920 indoc! {"
921 The quˇick brown
922 fox jumps over
923 the laˇzy dog."},
924 Mode::Normal,
925 );
926 cx.simulate_keystrokes("v i w shift-s '");
927 cx.assert_state(
928 indoc! {"
929 The ˇ'quick' brown
930 fox jumps over
931 the ˇ'lazy' dog."},
932 Mode::Normal,
933 );
934
935 // test add surrounds with visual block
936 cx.set_state(
937 indoc! {"
938 The quˇick brown
939 fox jumps over
940 the lazy dog."},
941 Mode::Normal,
942 );
943 cx.simulate_keystrokes("ctrl-v i w j j shift-s '");
944 cx.assert_state(
945 indoc! {"
946 The ˇ'quick' brown
947 fox 'jumps' over
948 the 'lazy 'dog."},
949 Mode::Normal,
950 );
951
952 // test add surrounds with visual line
953 cx.set_state(
954 indoc! {"
955 The quˇick brown
956 fox jumps over
957 the lazy dog."},
958 Mode::Normal,
959 );
960 cx.simulate_keystrokes("j shift-v shift-s '");
961 cx.assert_state(
962 indoc! {"
963 The quick brown
964 ˇ'
965 fox jumps over
966 '
967 the lazy dog."},
968 Mode::Normal,
969 );
970 }
971
972 #[gpui::test]
973 async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
974 let mut cx = VimTestContext::new(cx, true).await;
975
976 // test delete surround
977 cx.set_state(
978 indoc! {"
979 The {quˇick} brown
980 fox jumps over
981 the lazy dog."},
982 Mode::Normal,
983 );
984 cx.simulate_keystrokes("d s {");
985 cx.assert_state(
986 indoc! {"
987 The ˇquick brown
988 fox jumps over
989 the lazy dog."},
990 Mode::Normal,
991 );
992
993 // test delete not exist surrounds
994 cx.set_state(
995 indoc! {"
996 The {quˇick} brown
997 fox jumps over
998 the lazy dog."},
999 Mode::Normal,
1000 );
1001 cx.simulate_keystrokes("d s [");
1002 cx.assert_state(
1003 indoc! {"
1004 The {quˇick} brown
1005 fox jumps over
1006 the lazy dog."},
1007 Mode::Normal,
1008 );
1009
1010 // test delete surround forward exist, in the surrounds plugin of other editors,
1011 // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
1012 cx.set_state(
1013 indoc! {"
1014 The {quick} brˇown
1015 fox jumps over
1016 the lazy dog."},
1017 Mode::Normal,
1018 );
1019 cx.simulate_keystrokes("d s {");
1020 cx.assert_state(
1021 indoc! {"
1022 The {quick} brˇown
1023 fox jumps over
1024 the lazy dog."},
1025 Mode::Normal,
1026 );
1027
1028 // test cursor delete inner surrounds
1029 cx.set_state(
1030 indoc! {"
1031 The { quick brown
1032 fox jumˇps over }
1033 the lazy dog."},
1034 Mode::Normal,
1035 );
1036 cx.simulate_keystrokes("d s {");
1037 cx.assert_state(
1038 indoc! {"
1039 The ˇquick brown
1040 fox jumps over
1041 the lazy dog."},
1042 Mode::Normal,
1043 );
1044
1045 // test multi cursor delete surrounds
1046 cx.set_state(
1047 indoc! {"
1048 The [quˇick] brown
1049 fox jumps over
1050 the [laˇzy] dog."},
1051 Mode::Normal,
1052 );
1053 cx.simulate_keystrokes("d s ]");
1054 cx.assert_state(
1055 indoc! {"
1056 The ˇquick brown
1057 fox jumps over
1058 the ˇlazy dog."},
1059 Mode::Normal,
1060 );
1061
1062 // test multi cursor delete surrounds with around
1063 cx.set_state(
1064 indoc! {"
1065 Tˇhe [ quick ] brown
1066 fox jumps over
1067 the [laˇzy] dog."},
1068 Mode::Normal,
1069 );
1070 cx.simulate_keystrokes("d s [");
1071 cx.assert_state(
1072 indoc! {"
1073 The ˇquick brown
1074 fox jumps over
1075 the ˇlazy dog."},
1076 Mode::Normal,
1077 );
1078
1079 cx.set_state(
1080 indoc! {"
1081 Tˇhe [ quick ] brown
1082 fox jumps over
1083 the [laˇzy ] dog."},
1084 Mode::Normal,
1085 );
1086 cx.simulate_keystrokes("d s [");
1087 cx.assert_state(
1088 indoc! {"
1089 The ˇquick brown
1090 fox jumps over
1091 the ˇlazy dog."},
1092 Mode::Normal,
1093 );
1094
1095 // test multi cursor delete different surrounds
1096 // the pair corresponding to the two cursors is the same,
1097 // so they are combined into one cursor
1098 cx.set_state(
1099 indoc! {"
1100 The [quˇick] brown
1101 fox jumps over
1102 the {laˇzy} dog."},
1103 Mode::Normal,
1104 );
1105 cx.simulate_keystrokes("d s {");
1106 cx.assert_state(
1107 indoc! {"
1108 The [quick] brown
1109 fox jumps over
1110 the ˇlazy dog."},
1111 Mode::Normal,
1112 );
1113
1114 // test delete surround with multi cursor and nest surrounds
1115 cx.set_state(
1116 indoc! {"
1117 fn test_surround() {
1118 ifˇ 2 > 1 {
1119 ˇprintln!(\"it is fine\");
1120 };
1121 }"},
1122 Mode::Normal,
1123 );
1124 cx.simulate_keystrokes("d s }");
1125 cx.assert_state(
1126 indoc! {"
1127 fn test_surround() ˇ
1128 if 2 > 1 ˇ
1129 println!(\"it is fine\");
1130 ;
1131 "},
1132 Mode::Normal,
1133 );
1134 }
1135
1136 #[gpui::test]
1137 async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
1138 let mut cx = VimTestContext::new(cx, true).await;
1139
1140 cx.set_state(
1141 indoc! {"
1142 The {quˇick} brown
1143 fox jumps over
1144 the lazy dog."},
1145 Mode::Normal,
1146 );
1147 cx.simulate_keystrokes("c s { [");
1148 cx.assert_state(
1149 indoc! {"
1150 The ˇ[ quick ] brown
1151 fox jumps over
1152 the lazy dog."},
1153 Mode::Normal,
1154 );
1155
1156 // test multi cursor change surrounds
1157 cx.set_state(
1158 indoc! {"
1159 The {quˇick} brown
1160 fox jumps over
1161 the {laˇzy} dog."},
1162 Mode::Normal,
1163 );
1164 cx.simulate_keystrokes("c s { [");
1165 cx.assert_state(
1166 indoc! {"
1167 The ˇ[ quick ] brown
1168 fox jumps over
1169 the ˇ[ lazy ] dog."},
1170 Mode::Normal,
1171 );
1172
1173 // test multi cursor delete different surrounds with after cursor
1174 cx.set_state(
1175 indoc! {"
1176 Thˇe {quick} brown
1177 fox jumps over
1178 the {laˇzy} dog."},
1179 Mode::Normal,
1180 );
1181 cx.simulate_keystrokes("c s { [");
1182 cx.assert_state(
1183 indoc! {"
1184 The ˇ[ quick ] brown
1185 fox jumps over
1186 the ˇ[ lazy ] dog."},
1187 Mode::Normal,
1188 );
1189
1190 // test multi cursor change surrount with not around
1191 cx.set_state(
1192 indoc! {"
1193 Thˇe { quick } brown
1194 fox jumps over
1195 the {laˇzy} dog."},
1196 Mode::Normal,
1197 );
1198 cx.simulate_keystrokes("c s { ]");
1199 cx.assert_state(
1200 indoc! {"
1201 The ˇ[quick] brown
1202 fox jumps over
1203 the ˇ[lazy] dog."},
1204 Mode::Normal,
1205 );
1206
1207 // test multi cursor change with not exist surround
1208 cx.set_state(
1209 indoc! {"
1210 The {quˇick} brown
1211 fox jumps over
1212 the [laˇzy] dog."},
1213 Mode::Normal,
1214 );
1215 cx.simulate_keystrokes("c s [ '");
1216 cx.assert_state(
1217 indoc! {"
1218 The {quick} brown
1219 fox jumps over
1220 the ˇ'lazy' dog."},
1221 Mode::Normal,
1222 );
1223
1224 // test change nesting surrounds
1225 cx.set_state(
1226 indoc! {"
1227 fn test_surround() {
1228 ifˇ 2 > 1 {
1229 ˇprintln!(\"it is fine\");
1230 }
1231 };"},
1232 Mode::Normal,
1233 );
1234 cx.simulate_keystrokes("c s } ]");
1235 cx.assert_state(
1236 indoc! {"
1237 fn test_surround() ˇ[
1238 if 2 > 1 ˇ[
1239 println!(\"it is fine\");
1240 ]
1241 ];"},
1242 Mode::Normal,
1243 );
1244
1245 // test spaces with quote change surrounds
1246 cx.set_state(
1247 indoc! {"
1248 fn test_surround() {
1249 \"ˇ \"
1250 };"},
1251 Mode::Normal,
1252 );
1253 cx.simulate_keystrokes("c s \" '");
1254 cx.assert_state(
1255 indoc! {"
1256 fn test_surround() {
1257 ˇ' '
1258 };"},
1259 Mode::Normal,
1260 );
1261
1262 // Currently, the same test case but using the closing bracket `]`
1263 // actually removes a whitespace before the closing bracket, something
1264 // that might need to be fixed?
1265 cx.set_state(
1266 indoc! {"
1267 fn test_surround() {
1268 ifˇ 2 > 1 {
1269 ˇprintln!(\"it is fine\");
1270 }
1271 };"},
1272 Mode::Normal,
1273 );
1274 cx.simulate_keystrokes("c s { ]");
1275 cx.assert_state(
1276 indoc! {"
1277 fn test_surround() ˇ[
1278 if 2 > 1 ˇ[
1279 println!(\"it is fine\");
1280 ]
1281 ];"},
1282 Mode::Normal,
1283 );
1284
1285 // test change quotes.
1286 cx.set_state(indoc! {"' ˇstr '"}, Mode::Normal);
1287 cx.simulate_keystrokes("c s ' \"");
1288 cx.assert_state(indoc! {"ˇ\" str \""}, Mode::Normal);
1289
1290 // test multi cursor change quotes
1291 cx.set_state(
1292 indoc! {"
1293 ' ˇstr '
1294 some example text here
1295 ˇ' str '
1296 "},
1297 Mode::Normal,
1298 );
1299 cx.simulate_keystrokes("c s ' \"");
1300 cx.assert_state(
1301 indoc! {"
1302 ˇ\" str \"
1303 some example text here
1304 ˇ\" str \"
1305 "},
1306 Mode::Normal,
1307 );
1308
1309 // test quote to bracket spacing.
1310 cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
1311 cx.simulate_keystrokes("c s ' {");
1312 cx.assert_state(indoc! {"ˇ{ foobar }"}, Mode::Normal);
1313
1314 cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
1315 cx.simulate_keystrokes("c s ' }");
1316 cx.assert_state(indoc! {"ˇ{foobar}"}, Mode::Normal);
1317
1318 cx.set_state(indoc! {"I'm 'goˇod'"}, Mode::Normal);
1319 cx.simulate_keystrokes("c s ' \"");
1320 cx.assert_state(indoc! {"I'm ˇ\"good\""}, Mode::Normal);
1321
1322 cx.set_state(indoc! {"I'm 'goˇod'"}, Mode::Normal);
1323 cx.simulate_keystrokes("c s ' {");
1324 cx.assert_state(indoc! {"I'm ˇ{ good }"}, Mode::Normal);
1325 }
1326
1327 #[gpui::test]
1328 async fn test_change_surrounds_any_brackets(cx: &mut gpui::TestAppContext) {
1329 let mut cx = VimTestContext::new(cx, true).await;
1330
1331 // Update keybindings so that using `csb` triggers Vim's `AnyBrackets`
1332 // action.
1333 cx.update(|_, cx| {
1334 cx.bind_keys([KeyBinding::new(
1335 "b",
1336 AnyBrackets,
1337 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
1338 )]);
1339 });
1340
1341 cx.set_state(indoc! {"{braˇcketed}"}, Mode::Normal);
1342 cx.simulate_keystrokes("c s b [");
1343 cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1344
1345 cx.set_state(indoc! {"[braˇcketed]"}, Mode::Normal);
1346 cx.simulate_keystrokes("c s b {");
1347 cx.assert_state(indoc! {"ˇ{ bracketed }"}, Mode::Normal);
1348
1349 cx.set_state(indoc! {"<braˇcketed>"}, Mode::Normal);
1350 cx.simulate_keystrokes("c s b [");
1351 cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1352
1353 cx.set_state(indoc! {"(braˇcketed)"}, Mode::Normal);
1354 cx.simulate_keystrokes("c s b [");
1355 cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1356
1357 cx.set_state(indoc! {"(< name: ˇ'Zed' >)"}, Mode::Normal);
1358 cx.simulate_keystrokes("c s b }");
1359 cx.assert_state(indoc! {"(ˇ{ name: 'Zed' })"}, Mode::Normal);
1360
1361 cx.set_state(
1362 indoc! {"
1363 (< name: ˇ'Zed' >)
1364 (< nˇame: 'DeltaDB' >)
1365 "},
1366 Mode::Normal,
1367 );
1368 cx.simulate_keystrokes("c s b {");
1369 cx.set_state(
1370 indoc! {"
1371 (ˇ{ name: 'Zed' })
1372 (ˇ{ name: 'DeltaDB' })
1373 "},
1374 Mode::Normal,
1375 );
1376 }
1377
1378 #[gpui::test]
1379 async fn test_change_surrounds_mini_brackets(cx: &mut gpui::TestAppContext) {
1380 let mut cx = VimTestContext::new(cx, true).await;
1381
1382 // Update keybindings so that using `csb` triggers Vim's `MiniBrackets` action.
1383 cx.update(|_, cx| {
1384 cx.bind_keys([KeyBinding::new(
1385 "b",
1386 MiniBrackets,
1387 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
1388 )]);
1389 });
1390
1391 cx.set_state(indoc! {"{braˇcketed}"}, Mode::Normal);
1392 cx.simulate_keystrokes("c s b [");
1393 cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1394
1395 cx.set_state(indoc! {"[braˇcketed]"}, Mode::Normal);
1396 cx.simulate_keystrokes("c s b {");
1397 cx.assert_state(indoc! {"ˇ{ bracketed }"}, Mode::Normal);
1398
1399 cx.set_state(indoc! {"<braˇcketed>"}, Mode::Normal);
1400 cx.simulate_keystrokes("c s b [");
1401 cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1402
1403 cx.set_state(indoc! {"(braˇcketed)"}, Mode::Normal);
1404 cx.simulate_keystrokes("c s b [");
1405 cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
1406
1407 cx.set_state(indoc! {"(<ˇZed>)"}, Mode::Normal);
1408 cx.simulate_keystrokes("c s b )");
1409 cx.assert_state(indoc! {"(ˇ(Zed))"}, Mode::Normal);
1410
1411 cx.set_state(
1412 indoc! {"
1413 (<ˇZed>)
1414 (<ˇDeltaDB>)
1415 "},
1416 Mode::Normal,
1417 );
1418 cx.simulate_keystrokes("c s b (");
1419 cx.assert_state(
1420 indoc! {"
1421 (ˇ( Zed ))
1422 (ˇ( DeltaDB ))
1423 "},
1424 Mode::Normal,
1425 );
1426 }
1427
1428 #[gpui::test]
1429 async fn test_change_surrounds_any_quotes(cx: &mut gpui::TestAppContext) {
1430 let mut cx = VimTestContext::new(cx, true).await;
1431
1432 // Update keybindings so that using `csq` triggers Vim's `AnyQuotes` action.
1433 cx.update(|_, cx| {
1434 cx.bind_keys([KeyBinding::new(
1435 "q",
1436 AnyQuotes,
1437 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
1438 )]);
1439 });
1440
1441 cx.set_state(indoc! {"' ˇstr '"}, Mode::Normal);
1442 cx.simulate_keystrokes("c s q \"");
1443 cx.assert_state(indoc! {"ˇ\" str \""}, Mode::Normal);
1444
1445 cx.set_state(indoc! {"` ˇstr `"}, Mode::Normal);
1446 cx.simulate_keystrokes("c s q '");
1447 cx.assert_state(indoc! {"ˇ' str '"}, Mode::Normal);
1448
1449 cx.set_state(indoc! {"\" ˇstr \""}, Mode::Normal);
1450 cx.simulate_keystrokes("c s q `");
1451 cx.assert_state(indoc! {"ˇ` str `"}, Mode::Normal);
1452 }
1453
1454 #[gpui::test]
1455 async fn test_change_surrounds_mini_quotes(cx: &mut gpui::TestAppContext) {
1456 // NOTE: needs TypeScript test cx to recognize single/backquotes
1457 let mut cx = VimTestContext::new_typescript(cx).await;
1458
1459 // Update keybindings so that using `csq` triggers Vim's `MiniQuotes` action.
1460 cx.update(|_, cx| {
1461 cx.bind_keys([KeyBinding::new(
1462 "q",
1463 MiniQuotes,
1464 Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
1465 )]);
1466 });
1467 cx.set_state(indoc! {"' ˇstr '"}, Mode::Normal);
1468 cx.simulate_keystrokes("c s q \"");
1469 cx.assert_state(indoc! {"ˇ\" str \""}, Mode::Normal);
1470
1471 cx.set_state(indoc! {"` ˇstr `"}, Mode::Normal);
1472 cx.simulate_keystrokes("c s q '");
1473 cx.assert_state(indoc! {"ˇ' str '"}, Mode::Normal);
1474
1475 cx.set_state(indoc! {"\" ˇstr \""}, Mode::Normal);
1476 cx.simulate_keystrokes("c s q `");
1477 cx.assert_state(indoc! {"ˇ` str `"}, Mode::Normal);
1478 }
1479
1480 // The following test cases all follow tpope/vim-surround's behaviour
1481 // and are more focused on how whitespace is handled.
1482 #[gpui::test]
1483 async fn test_change_surrounds_vim(cx: &mut gpui::TestAppContext) {
1484 let mut cx = VimTestContext::new(cx, true).await;
1485
1486 // Changing quote to quote should never change the surrounding
1487 // whitespace.
1488 cx.set_state(indoc! {"' ˇa '"}, Mode::Normal);
1489 cx.simulate_keystrokes("c s ' \"");
1490 cx.assert_state(indoc! {"ˇ\" a \""}, Mode::Normal);
1491
1492 cx.set_state(indoc! {"\" ˇa \""}, Mode::Normal);
1493 cx.simulate_keystrokes("c s \" '");
1494 cx.assert_state(indoc! {"ˇ' a '"}, Mode::Normal);
1495
1496 // Changing quote to bracket adds one more space when the opening
1497 // bracket is used, does not affect whitespace when the closing bracket
1498 // is used.
1499 cx.set_state(indoc! {"' ˇa '"}, Mode::Normal);
1500 cx.simulate_keystrokes("c s ' {");
1501 cx.assert_state(indoc! {"ˇ{ a }"}, Mode::Normal);
1502
1503 cx.set_state(indoc! {"' ˇa '"}, Mode::Normal);
1504 cx.simulate_keystrokes("c s ' }");
1505 cx.assert_state(indoc! {"ˇ{ a }"}, Mode::Normal);
1506
1507 // Changing bracket to quote should remove all space when the
1508 // opening bracket is used and preserve all space when the
1509 // closing one is used.
1510 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1511 cx.simulate_keystrokes("c s { '");
1512 cx.assert_state(indoc! {"ˇ'a'"}, Mode::Normal);
1513
1514 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1515 cx.simulate_keystrokes("c s } '");
1516 cx.assert_state(indoc! {"ˇ' a '"}, Mode::Normal);
1517
1518 // Changing bracket to bracket follows these rules:
1519 // * opening → opening – keeps only one space.
1520 // * opening → closing – removes all space.
1521 // * closing → opening – adds one space.
1522 // * closing → closing – does not change space.
1523 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1524 cx.simulate_keystrokes("c s { [");
1525 cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal);
1526
1527 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1528 cx.simulate_keystrokes("c s { ]");
1529 cx.assert_state(indoc! {"ˇ[a]"}, Mode::Normal);
1530
1531 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1532 cx.simulate_keystrokes("c s } [");
1533 cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal);
1534
1535 cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal);
1536 cx.simulate_keystrokes("c s } ]");
1537 cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal);
1538 }
1539
1540 #[gpui::test]
1541 async fn test_surrounds(cx: &mut gpui::TestAppContext) {
1542 let mut cx = VimTestContext::new(cx, true).await;
1543
1544 cx.set_state(
1545 indoc! {"
1546 The quˇick brown
1547 fox jumps over
1548 the lazy dog."},
1549 Mode::Normal,
1550 );
1551 cx.simulate_keystrokes("y s i w [");
1552 cx.assert_state(
1553 indoc! {"
1554 The ˇ[ quick ] brown
1555 fox jumps over
1556 the lazy dog."},
1557 Mode::Normal,
1558 );
1559
1560 cx.simulate_keystrokes("c s [ }");
1561 cx.assert_state(
1562 indoc! {"
1563 The ˇ{quick} brown
1564 fox jumps over
1565 the lazy dog."},
1566 Mode::Normal,
1567 );
1568
1569 cx.simulate_keystrokes("d s {");
1570 cx.assert_state(
1571 indoc! {"
1572 The ˇquick brown
1573 fox jumps over
1574 the lazy dog."},
1575 Mode::Normal,
1576 );
1577
1578 cx.simulate_keystrokes("u");
1579 cx.assert_state(
1580 indoc! {"
1581 The ˇ{quick} brown
1582 fox jumps over
1583 the lazy dog."},
1584 Mode::Normal,
1585 );
1586 }
1587
1588 #[gpui::test]
1589 async fn test_surround_aliases(cx: &mut gpui::TestAppContext) {
1590 let mut cx = VimTestContext::new(cx, true).await;
1591
1592 // add aliases
1593 cx.set_state(
1594 indoc! {"
1595 The quˇick brown
1596 fox jumps over
1597 the lazy dog."},
1598 Mode::Normal,
1599 );
1600 cx.simulate_keystrokes("y s i w b");
1601 cx.assert_state(
1602 indoc! {"
1603 The ˇ(quick) brown
1604 fox jumps over
1605 the lazy dog."},
1606 Mode::Normal,
1607 );
1608
1609 cx.set_state(
1610 indoc! {"
1611 The quˇick brown
1612 fox jumps over
1613 the lazy dog."},
1614 Mode::Normal,
1615 );
1616 cx.simulate_keystrokes("y s i w B");
1617 cx.assert_state(
1618 indoc! {"
1619 The ˇ{quick} brown
1620 fox jumps over
1621 the lazy dog."},
1622 Mode::Normal,
1623 );
1624
1625 cx.set_state(
1626 indoc! {"
1627 The quˇick brown
1628 fox jumps over
1629 the lazy dog."},
1630 Mode::Normal,
1631 );
1632 cx.simulate_keystrokes("y s i w a");
1633 cx.assert_state(
1634 indoc! {"
1635 The ˇ<quick> brown
1636 fox jumps over
1637 the lazy dog."},
1638 Mode::Normal,
1639 );
1640
1641 cx.set_state(
1642 indoc! {"
1643 The quˇick brown
1644 fox jumps over
1645 the lazy dog."},
1646 Mode::Normal,
1647 );
1648 cx.simulate_keystrokes("y s i w r");
1649 cx.assert_state(
1650 indoc! {"
1651 The ˇ[quick] brown
1652 fox jumps over
1653 the lazy dog."},
1654 Mode::Normal,
1655 );
1656
1657 // change aliases
1658 cx.set_state(
1659 indoc! {"
1660 The {quˇick} brown
1661 fox jumps over
1662 the lazy dog."},
1663 Mode::Normal,
1664 );
1665 cx.simulate_keystrokes("c s { b");
1666 cx.assert_state(
1667 indoc! {"
1668 The ˇ(quick) brown
1669 fox jumps over
1670 the lazy dog."},
1671 Mode::Normal,
1672 );
1673
1674 cx.set_state(
1675 indoc! {"
1676 The (quˇick) brown
1677 fox jumps over
1678 the lazy dog."},
1679 Mode::Normal,
1680 );
1681 cx.simulate_keystrokes("c s ( B");
1682 cx.assert_state(
1683 indoc! {"
1684 The ˇ{quick} brown
1685 fox jumps over
1686 the lazy dog."},
1687 Mode::Normal,
1688 );
1689
1690 cx.set_state(
1691 indoc! {"
1692 The (quˇick) brown
1693 fox jumps over
1694 the lazy dog."},
1695 Mode::Normal,
1696 );
1697 cx.simulate_keystrokes("c s ( a");
1698 cx.assert_state(
1699 indoc! {"
1700 The ˇ<quick> brown
1701 fox jumps over
1702 the lazy dog."},
1703 Mode::Normal,
1704 );
1705
1706 cx.set_state(
1707 indoc! {"
1708 The <quˇick> brown
1709 fox jumps over
1710 the lazy dog."},
1711 Mode::Normal,
1712 );
1713 cx.simulate_keystrokes("c s < b");
1714 cx.assert_state(
1715 indoc! {"
1716 The ˇ(quick) brown
1717 fox jumps over
1718 the lazy dog."},
1719 Mode::Normal,
1720 );
1721
1722 cx.set_state(
1723 indoc! {"
1724 The (quˇick) brown
1725 fox jumps over
1726 the lazy dog."},
1727 Mode::Normal,
1728 );
1729 cx.simulate_keystrokes("c s ( r");
1730 cx.assert_state(
1731 indoc! {"
1732 The ˇ[quick] brown
1733 fox jumps over
1734 the lazy dog."},
1735 Mode::Normal,
1736 );
1737
1738 cx.set_state(
1739 indoc! {"
1740 The [quˇick] brown
1741 fox jumps over
1742 the lazy dog."},
1743 Mode::Normal,
1744 );
1745 cx.simulate_keystrokes("c s [ b");
1746 cx.assert_state(
1747 indoc! {"
1748 The ˇ(quick) brown
1749 fox jumps over
1750 the lazy dog."},
1751 Mode::Normal,
1752 );
1753
1754 // delete alias
1755 cx.set_state(
1756 indoc! {"
1757 The {quˇick} brown
1758 fox jumps over
1759 the lazy dog."},
1760 Mode::Normal,
1761 );
1762 cx.simulate_keystrokes("d s B");
1763 cx.assert_state(
1764 indoc! {"
1765 The ˇquick brown
1766 fox jumps over
1767 the lazy dog."},
1768 Mode::Normal,
1769 );
1770
1771 cx.set_state(
1772 indoc! {"
1773 The (quˇick) brown
1774 fox jumps over
1775 the lazy dog."},
1776 Mode::Normal,
1777 );
1778 cx.simulate_keystrokes("d s b");
1779 cx.assert_state(
1780 indoc! {"
1781 The ˇquick brown
1782 fox jumps over
1783 the lazy dog."},
1784 Mode::Normal,
1785 );
1786
1787 cx.set_state(
1788 indoc! {"
1789 The [quˇick] brown
1790 fox jumps over
1791 the lazy dog."},
1792 Mode::Normal,
1793 );
1794 cx.simulate_keystrokes("d s r");
1795 cx.assert_state(
1796 indoc! {"
1797 The ˇquick brown
1798 fox jumps over
1799 the lazy dog."},
1800 Mode::Normal,
1801 );
1802
1803 cx.set_state(
1804 indoc! {"
1805 The <quˇick> brown
1806 fox jumps over
1807 the lazy dog."},
1808 Mode::Normal,
1809 );
1810 cx.simulate_keystrokes("d s a");
1811 cx.assert_state(
1812 indoc! {"
1813 The ˇquick brown
1814 fox jumps over
1815 the lazy dog."},
1816 Mode::Normal,
1817 );
1818 }
1819
1820 #[test]
1821 fn test_surround_pair_for_char() {
1822 use super::{SURROUND_PAIRS, surround_pair_for_char_helix, surround_pair_for_char_vim};
1823
1824 fn as_tuple(pair: Option<super::SurroundPair>) -> Option<(char, char)> {
1825 pair.map(|p| (p.open, p.close))
1826 }
1827
1828 assert_eq!(as_tuple(surround_pair_for_char_vim('b')), Some(('(', ')')));
1829 assert_eq!(as_tuple(surround_pair_for_char_vim('B')), Some(('{', '}')));
1830 assert_eq!(as_tuple(surround_pair_for_char_vim('r')), Some(('[', ']')));
1831 assert_eq!(as_tuple(surround_pair_for_char_vim('a')), Some(('<', '>')));
1832
1833 assert_eq!(surround_pair_for_char_vim('m'), None);
1834
1835 for pair in SURROUND_PAIRS {
1836 assert_eq!(
1837 as_tuple(surround_pair_for_char_vim(pair.open)),
1838 Some((pair.open, pair.close))
1839 );
1840 assert_eq!(
1841 as_tuple(surround_pair_for_char_vim(pair.close)),
1842 Some((pair.open, pair.close))
1843 );
1844 }
1845
1846 // Test unknown char returns None
1847 assert_eq!(surround_pair_for_char_vim('x'), None);
1848
1849 // Helix resolves literal chars and falls back to symmetric pairs.
1850 assert_eq!(
1851 as_tuple(surround_pair_for_char_helix('*')),
1852 Some(('*', '*'))
1853 );
1854 assert_eq!(surround_pair_for_char_helix('m'), None);
1855 }
1856}
1857