1use editor::{Editor, MultiBufferSnapshot, ToOffset, ToPoint, scroll::Autoscroll};
2use gpui::{Context, Window, impl_actions};
3use language::{Bias, Point};
4use schemars::JsonSchema;
5use serde::Deserialize;
6use std::ops::Range;
7
8use crate::{Vim, state::Mode};
9
10const BOOLEAN_PAIRS: &[(&str, &str)] = &[("true", "false"), ("yes", "no"), ("on", "off")];
11
12#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
13#[serde(deny_unknown_fields)]
14struct Increment {
15 #[serde(default)]
16 step: bool,
17}
18
19#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
20#[serde(deny_unknown_fields)]
21struct Decrement {
22 #[serde(default)]
23 step: bool,
24}
25
26impl_actions!(vim, [Increment, Decrement]);
27
28pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
29 Vim::action(editor, cx, |vim, action: &Increment, window, cx| {
30 vim.record_current_action(cx);
31 let count = Vim::take_count(cx).unwrap_or(1);
32 let step = if action.step { count as i32 } else { 0 };
33 vim.increment(count as i64, step, window, cx)
34 });
35 Vim::action(editor, cx, |vim, action: &Decrement, window, cx| {
36 vim.record_current_action(cx);
37 let count = Vim::take_count(cx).unwrap_or(1);
38 let step = if action.step { -1 * (count as i32) } else { 0 };
39 vim.increment(-(count as i64), step, window, cx)
40 });
41}
42
43impl Vim {
44 fn increment(
45 &mut self,
46 mut delta: i64,
47 step: i32,
48 window: &mut Window,
49 cx: &mut Context<Self>,
50 ) {
51 self.store_visual_marks(window, cx);
52 self.update_editor(window, cx, |vim, editor, window, cx| {
53 let mut edits = Vec::new();
54 let mut new_anchors = Vec::new();
55
56 let snapshot = editor.buffer().read(cx).snapshot(cx);
57 for selection in editor.selections.all_adjusted(cx) {
58 if !selection.is_empty()
59 && (vim.mode != Mode::VisualBlock || new_anchors.is_empty())
60 {
61 new_anchors.push((true, snapshot.anchor_before(selection.start)))
62 }
63 for row in selection.start.row..=selection.end.row {
64 let start = if row == selection.start.row {
65 selection.start
66 } else {
67 Point::new(row, 0)
68 };
69
70 if let Some((range, num, radix)) = find_number(&snapshot, start) {
71 let replace = match radix {
72 10 => increment_decimal_string(&num, delta),
73 16 => increment_hex_string(&num, delta),
74 2 => increment_binary_string(&num, delta),
75 _ => unreachable!(),
76 };
77 delta += step as i64;
78 edits.push((range.clone(), replace));
79 if selection.is_empty() {
80 new_anchors.push((false, snapshot.anchor_after(range.end)))
81 }
82 } else if let Some((range, boolean)) = find_boolean(&snapshot, start) {
83 let replace = toggle_boolean(&boolean);
84 delta += step as i64;
85 edits.push((range.clone(), replace));
86 if selection.is_empty() {
87 new_anchors.push((false, snapshot.anchor_after(range.end)))
88 }
89 } else if selection.is_empty() {
90 new_anchors.push((true, snapshot.anchor_after(start)))
91 }
92 }
93 }
94 editor.transact(window, cx, |editor, window, cx| {
95 editor.edit(edits, cx);
96
97 let snapshot = editor.buffer().read(cx).snapshot(cx);
98 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
99 let mut new_ranges = Vec::new();
100 for (visual, anchor) in new_anchors.iter() {
101 let mut point = anchor.to_point(&snapshot);
102 if !*visual && point.column > 0 {
103 point.column -= 1;
104 point = snapshot.clip_point(point, Bias::Left)
105 }
106 new_ranges.push(point..point);
107 }
108 s.select_ranges(new_ranges)
109 })
110 });
111 });
112 self.switch_mode(Mode::Normal, true, window, cx)
113 }
114}
115
116fn increment_decimal_string(num: &str, delta: i64) -> String {
117 let (negative, delta, num_str) = match num.strip_prefix('-') {
118 Some(n) => (true, -delta, n),
119 None => (false, delta, num),
120 };
121 let num_length = num_str.len();
122 let leading_zero = num_str.starts_with('0');
123
124 let (result, new_negative) = match u64::from_str_radix(num_str, 10) {
125 Ok(value) => {
126 let wrapped = value.wrapping_add_signed(delta);
127 if delta < 0 && wrapped > value {
128 ((u64::MAX - wrapped).wrapping_add(1), !negative)
129 } else if delta > 0 && wrapped < value {
130 (u64::MAX - wrapped, !negative)
131 } else {
132 (wrapped, negative)
133 }
134 }
135 Err(_) => (u64::MAX, negative),
136 };
137
138 let formatted = format!("{}", result);
139 let new_significant_digits = formatted.len();
140 let padding = if leading_zero {
141 num_length.saturating_sub(new_significant_digits)
142 } else {
143 0
144 };
145
146 if new_negative && result != 0 {
147 format!("-{}{}", "0".repeat(padding), formatted)
148 } else {
149 format!("{}{}", "0".repeat(padding), formatted)
150 }
151}
152
153fn increment_hex_string(num: &str, delta: i64) -> String {
154 let result = if let Ok(val) = u64::from_str_radix(&num, 16) {
155 val.wrapping_add_signed(delta)
156 } else {
157 u64::MAX
158 };
159 if should_use_lowercase(num) {
160 format!("{:0width$x}", result, width = num.len())
161 } else {
162 format!("{:0width$X}", result, width = num.len())
163 }
164}
165
166fn should_use_lowercase(num: &str) -> bool {
167 let mut use_uppercase = false;
168 for ch in num.chars() {
169 if ch.is_ascii_lowercase() {
170 return true;
171 }
172 if ch.is_ascii_uppercase() {
173 use_uppercase = true;
174 }
175 }
176 !use_uppercase
177}
178
179fn increment_binary_string(num: &str, delta: i64) -> String {
180 let result = if let Ok(val) = u64::from_str_radix(&num, 2) {
181 val.wrapping_add_signed(delta)
182 } else {
183 u64::MAX
184 };
185 format!("{:0width$b}", result, width = num.len())
186}
187
188fn find_number(
189 snapshot: &MultiBufferSnapshot,
190 start: Point,
191) -> Option<(Range<Point>, String, u32)> {
192 let mut offset = start.to_offset(snapshot);
193
194 let ch0 = snapshot.chars_at(offset).next();
195 if ch0.as_ref().is_some_and(char::is_ascii_hexdigit) || matches!(ch0, Some('-' | 'b' | 'x')) {
196 // go backwards to the start of any number the selection is within
197 for ch in snapshot.reversed_chars_at(offset) {
198 if ch.is_ascii_hexdigit() || ch == '-' || ch == 'b' || ch == 'x' {
199 offset -= ch.len_utf8();
200 continue;
201 }
202 break;
203 }
204 }
205
206 let mut begin = None;
207 let mut end = None;
208 let mut num = String::new();
209 let mut radix = 10;
210
211 let mut chars = snapshot.chars_at(offset).peekable();
212 // find the next number on the line (may start after the original cursor position)
213 while let Some(ch) = chars.next() {
214 if num == "0" && ch == 'b' && chars.peek().is_some() && chars.peek().unwrap().is_digit(2) {
215 radix = 2;
216 begin = None;
217 num = String::new();
218 }
219 if num == "0"
220 && ch == 'x'
221 && chars.peek().is_some()
222 && chars.peek().unwrap().is_ascii_hexdigit()
223 {
224 radix = 16;
225 begin = None;
226 num = String::new();
227 }
228
229 if ch.is_digit(radix)
230 || (begin.is_none()
231 && ch == '-'
232 && chars.peek().is_some()
233 && chars.peek().unwrap().is_digit(radix))
234 {
235 if begin.is_none() {
236 begin = Some(offset);
237 }
238 num.push(ch);
239 } else if begin.is_some() {
240 end = Some(offset);
241 break;
242 } else if ch == '\n' {
243 break;
244 }
245 offset += ch.len_utf8();
246 }
247 if let Some(begin) = begin {
248 let end = end.unwrap_or(offset);
249 Some((begin.to_point(snapshot)..end.to_point(snapshot), num, radix))
250 } else {
251 None
252 }
253}
254
255fn find_boolean(snapshot: &MultiBufferSnapshot, start: Point) -> Option<(Range<Point>, String)> {
256 let mut offset = start.to_offset(snapshot);
257
258 let ch0 = snapshot.chars_at(offset).next();
259 if ch0.as_ref().is_some_and(|c| c.is_ascii_alphabetic()) {
260 for ch in snapshot.reversed_chars_at(offset) {
261 if ch.is_ascii_alphabetic() {
262 offset -= ch.len_utf8();
263 continue;
264 }
265 break;
266 }
267 }
268
269 let mut begin = None;
270 let mut end = None;
271 let mut word = String::new();
272
273 let mut chars = snapshot.chars_at(offset);
274
275 while let Some(ch) = chars.next() {
276 if ch.is_ascii_alphabetic() {
277 if begin.is_none() {
278 begin = Some(offset);
279 }
280 word.push(ch);
281 } else if begin.is_some() {
282 end = Some(offset);
283 let word_lower = word.to_lowercase();
284 if BOOLEAN_PAIRS
285 .iter()
286 .any(|(a, b)| word_lower == *a || word_lower == *b)
287 {
288 return Some((
289 begin.unwrap().to_point(snapshot)..end.unwrap().to_point(snapshot),
290 word,
291 ));
292 }
293 begin = None;
294 end = None;
295 word = String::new();
296 } else if ch == '\n' {
297 break;
298 }
299 offset += ch.len_utf8();
300 }
301 if let Some(begin) = begin {
302 let end = end.unwrap_or(offset);
303 let word_lower = word.to_lowercase();
304 if BOOLEAN_PAIRS
305 .iter()
306 .any(|(a, b)| word_lower == *a || word_lower == *b)
307 {
308 return Some((begin.to_point(snapshot)..end.to_point(snapshot), word));
309 }
310 }
311 None
312}
313
314fn toggle_boolean(boolean: &str) -> String {
315 let lower = boolean.to_lowercase();
316
317 let target = BOOLEAN_PAIRS
318 .iter()
319 .find_map(|(a, b)| {
320 if lower == *a {
321 Some(b)
322 } else if lower == *b {
323 Some(a)
324 } else {
325 None
326 }
327 })
328 .unwrap_or(&boolean);
329
330 if boolean.chars().all(|c| c.is_uppercase()) {
331 // Upper case
332 target.to_uppercase()
333 } else if boolean.chars().next().unwrap_or(' ').is_uppercase() {
334 // Title case
335 let mut chars = target.chars();
336 match chars.next() {
337 None => String::new(),
338 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
339 }
340 } else {
341 target.to_string()
342 }
343}
344
345#[cfg(test)]
346mod test {
347 use indoc::indoc;
348
349 use crate::{
350 state::Mode,
351 test::{NeovimBackedTestContext, VimTestContext},
352 };
353
354 #[gpui::test]
355 async fn test_increment(cx: &mut gpui::TestAppContext) {
356 let mut cx = NeovimBackedTestContext::new(cx).await;
357
358 cx.set_shared_state(indoc! {"
359 1ˇ2
360 "})
361 .await;
362
363 cx.simulate_shared_keystrokes("ctrl-a").await;
364 cx.shared_state().await.assert_eq(indoc! {"
365 1ˇ3
366 "});
367 cx.simulate_shared_keystrokes("ctrl-x").await;
368 cx.shared_state().await.assert_eq(indoc! {"
369 1ˇ2
370 "});
371
372 cx.simulate_shared_keystrokes("9 9 ctrl-a").await;
373 cx.shared_state().await.assert_eq(indoc! {"
374 11ˇ1
375 "});
376 cx.simulate_shared_keystrokes("1 1 1 ctrl-x").await;
377 cx.shared_state().await.assert_eq(indoc! {"
378 ˇ0
379 "});
380 cx.simulate_shared_keystrokes(".").await;
381 cx.shared_state().await.assert_eq(indoc! {"
382 -11ˇ1
383 "});
384 }
385
386 #[gpui::test]
387 async fn test_increment_with_dot(cx: &mut gpui::TestAppContext) {
388 let mut cx = NeovimBackedTestContext::new(cx).await;
389
390 cx.set_shared_state(indoc! {"
391 1ˇ.2
392 "})
393 .await;
394
395 cx.simulate_shared_keystrokes("ctrl-a").await;
396 cx.shared_state().await.assert_eq(indoc! {"
397 1.ˇ3
398 "});
399 cx.simulate_shared_keystrokes("ctrl-x").await;
400 cx.shared_state().await.assert_eq(indoc! {"
401 1.ˇ2
402 "});
403 }
404
405 #[gpui::test]
406 async fn test_increment_with_leading_zeros(cx: &mut gpui::TestAppContext) {
407 let mut cx = NeovimBackedTestContext::new(cx).await;
408
409 cx.set_shared_state(indoc! {"
410 000ˇ9
411 "})
412 .await;
413
414 cx.simulate_shared_keystrokes("ctrl-a").await;
415 cx.shared_state().await.assert_eq(indoc! {"
416 001ˇ0
417 "});
418 cx.simulate_shared_keystrokes("2 ctrl-x").await;
419 cx.shared_state().await.assert_eq(indoc! {"
420 000ˇ8
421 "});
422 }
423
424 #[gpui::test]
425 async fn test_increment_with_leading_zeros_and_zero(cx: &mut gpui::TestAppContext) {
426 let mut cx = NeovimBackedTestContext::new(cx).await;
427
428 cx.set_shared_state(indoc! {"
429 01ˇ1
430 "})
431 .await;
432
433 cx.simulate_shared_keystrokes("ctrl-a").await;
434 cx.shared_state().await.assert_eq(indoc! {"
435 01ˇ2
436 "});
437 cx.simulate_shared_keystrokes("1 2 ctrl-x").await;
438 cx.shared_state().await.assert_eq(indoc! {"
439 00ˇ0
440 "});
441 }
442
443 #[gpui::test]
444 async fn test_increment_with_changing_leading_zeros(cx: &mut gpui::TestAppContext) {
445 let mut cx = NeovimBackedTestContext::new(cx).await;
446
447 cx.set_shared_state(indoc! {"
448 099ˇ9
449 "})
450 .await;
451
452 cx.simulate_shared_keystrokes("ctrl-a").await;
453 cx.shared_state().await.assert_eq(indoc! {"
454 100ˇ0
455 "});
456 cx.simulate_shared_keystrokes("2 ctrl-x").await;
457 cx.shared_state().await.assert_eq(indoc! {"
458 99ˇ8
459 "});
460 }
461
462 #[gpui::test]
463 async fn test_increment_with_two_dots(cx: &mut gpui::TestAppContext) {
464 let mut cx = NeovimBackedTestContext::new(cx).await;
465
466 cx.set_shared_state(indoc! {"
467 111.ˇ.2
468 "})
469 .await;
470
471 cx.simulate_shared_keystrokes("ctrl-a").await;
472 cx.shared_state().await.assert_eq(indoc! {"
473 111..ˇ3
474 "});
475 cx.simulate_shared_keystrokes("ctrl-x").await;
476 cx.shared_state().await.assert_eq(indoc! {"
477 111..ˇ2
478 "});
479 }
480
481 #[gpui::test]
482 async fn test_increment_sign_change(cx: &mut gpui::TestAppContext) {
483 let mut cx = NeovimBackedTestContext::new(cx).await;
484 cx.set_shared_state(indoc! {"
485 ˇ0
486 "})
487 .await;
488 cx.simulate_shared_keystrokes("ctrl-x").await;
489 cx.shared_state().await.assert_eq(indoc! {"
490 -ˇ1
491 "});
492 cx.simulate_shared_keystrokes("2 ctrl-a").await;
493 cx.shared_state().await.assert_eq(indoc! {"
494 ˇ1
495 "});
496 }
497
498 #[gpui::test]
499 async fn test_increment_sign_change_with_leading_zeros(cx: &mut gpui::TestAppContext) {
500 let mut cx = NeovimBackedTestContext::new(cx).await;
501 cx.set_shared_state(indoc! {"
502 00ˇ1
503 "})
504 .await;
505 cx.simulate_shared_keystrokes("ctrl-x").await;
506 cx.shared_state().await.assert_eq(indoc! {"
507 00ˇ0
508 "});
509 cx.simulate_shared_keystrokes("ctrl-x").await;
510 cx.shared_state().await.assert_eq(indoc! {"
511 -00ˇ1
512 "});
513 cx.simulate_shared_keystrokes("2 ctrl-a").await;
514 cx.shared_state().await.assert_eq(indoc! {"
515 00ˇ1
516 "});
517 }
518
519 #[gpui::test]
520 async fn test_increment_bin_wrapping_and_padding(cx: &mut gpui::TestAppContext) {
521 let mut cx = NeovimBackedTestContext::new(cx).await;
522 cx.set_shared_state(indoc! {"
523 0b111111111111111111111111111111111111111111111111111111111111111111111ˇ1
524 "})
525 .await;
526
527 cx.simulate_shared_keystrokes("ctrl-a").await;
528 cx.shared_state().await.assert_eq(indoc! {"
529 0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1
530 "});
531 cx.simulate_shared_keystrokes("ctrl-a").await;
532 cx.shared_state().await.assert_eq(indoc! {"
533 0b000000000000000000000000000000000000000000000000000000000000000000000ˇ0
534 "});
535
536 cx.simulate_shared_keystrokes("ctrl-a").await;
537 cx.shared_state().await.assert_eq(indoc! {"
538 0b000000000000000000000000000000000000000000000000000000000000000000000ˇ1
539 "});
540 cx.simulate_shared_keystrokes("2 ctrl-x").await;
541 cx.shared_state().await.assert_eq(indoc! {"
542 0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1
543 "});
544 }
545
546 #[gpui::test]
547 async fn test_increment_hex_wrapping_and_padding(cx: &mut gpui::TestAppContext) {
548 let mut cx = NeovimBackedTestContext::new(cx).await;
549 cx.set_shared_state(indoc! {"
550 0xfffffffffffffffffffˇf
551 "})
552 .await;
553
554 cx.simulate_shared_keystrokes("ctrl-a").await;
555 cx.shared_state().await.assert_eq(indoc! {"
556 0x0000fffffffffffffffˇf
557 "});
558 cx.simulate_shared_keystrokes("ctrl-a").await;
559 cx.shared_state().await.assert_eq(indoc! {"
560 0x0000000000000000000ˇ0
561 "});
562 cx.simulate_shared_keystrokes("ctrl-a").await;
563 cx.shared_state().await.assert_eq(indoc! {"
564 0x0000000000000000000ˇ1
565 "});
566 cx.simulate_shared_keystrokes("2 ctrl-x").await;
567 cx.shared_state().await.assert_eq(indoc! {"
568 0x0000fffffffffffffffˇf
569 "});
570 }
571
572 #[gpui::test]
573 async fn test_increment_wrapping(cx: &mut gpui::TestAppContext) {
574 let mut cx = NeovimBackedTestContext::new(cx).await;
575 cx.set_shared_state(indoc! {"
576 1844674407370955161ˇ9
577 "})
578 .await;
579
580 cx.simulate_shared_keystrokes("ctrl-a").await;
581 cx.shared_state().await.assert_eq(indoc! {"
582 1844674407370955161ˇ5
583 "});
584 cx.simulate_shared_keystrokes("ctrl-a").await;
585 cx.shared_state().await.assert_eq(indoc! {"
586 -1844674407370955161ˇ5
587 "});
588 cx.simulate_shared_keystrokes("ctrl-a").await;
589 cx.shared_state().await.assert_eq(indoc! {"
590 -1844674407370955161ˇ4
591 "});
592 cx.simulate_shared_keystrokes("3 ctrl-x").await;
593 cx.shared_state().await.assert_eq(indoc! {"
594 1844674407370955161ˇ4
595 "});
596 cx.simulate_shared_keystrokes("2 ctrl-a").await;
597 cx.shared_state().await.assert_eq(indoc! {"
598 -1844674407370955161ˇ5
599 "});
600 }
601
602 #[gpui::test]
603 async fn test_increment_inline(cx: &mut gpui::TestAppContext) {
604 let mut cx = NeovimBackedTestContext::new(cx).await;
605 cx.set_shared_state(indoc! {"
606 inline0x3ˇ9u32
607 "})
608 .await;
609
610 cx.simulate_shared_keystrokes("ctrl-a").await;
611 cx.shared_state().await.assert_eq(indoc! {"
612 inline0x3ˇau32
613 "});
614 cx.simulate_shared_keystrokes("ctrl-a").await;
615 cx.shared_state().await.assert_eq(indoc! {"
616 inline0x3ˇbu32
617 "});
618 cx.simulate_shared_keystrokes("l l l ctrl-a").await;
619 cx.shared_state().await.assert_eq(indoc! {"
620 inline0x3bu3ˇ3
621 "});
622 }
623
624 #[gpui::test]
625 async fn test_increment_hex_casing(cx: &mut gpui::TestAppContext) {
626 let mut cx = NeovimBackedTestContext::new(cx).await;
627 cx.set_shared_state(indoc! {"
628 0xFˇa
629 "})
630 .await;
631
632 cx.simulate_shared_keystrokes("ctrl-a").await;
633 cx.shared_state().await.assert_eq(indoc! {"
634 0xfˇb
635 "});
636 cx.simulate_shared_keystrokes("ctrl-a").await;
637 cx.shared_state().await.assert_eq(indoc! {"
638 0xfˇc
639 "});
640 }
641
642 #[gpui::test]
643 async fn test_increment_radix(cx: &mut gpui::TestAppContext) {
644 let mut cx = NeovimBackedTestContext::new(cx).await;
645
646 cx.simulate("ctrl-a", "ˇ total: 0xff")
647 .await
648 .assert_matches();
649 cx.simulate("ctrl-x", "ˇ total: 0xff")
650 .await
651 .assert_matches();
652 cx.simulate("ctrl-x", "ˇ total: 0xFF")
653 .await
654 .assert_matches();
655 cx.simulate("ctrl-a", "(ˇ0b10f)").await.assert_matches();
656 cx.simulate("ctrl-a", "ˇ-1").await.assert_matches();
657 cx.simulate("ctrl-a", "banˇana").await.assert_matches();
658 }
659
660 #[gpui::test]
661 async fn test_increment_steps(cx: &mut gpui::TestAppContext) {
662 let mut cx = NeovimBackedTestContext::new(cx).await;
663
664 cx.set_shared_state(indoc! {"
665 ˇ1
666 1
667 1 2
668 1
669 1"})
670 .await;
671
672 cx.simulate_shared_keystrokes("j v shift-g g ctrl-a").await;
673 cx.shared_state().await.assert_eq(indoc! {"
674 1
675 ˇ2
676 3 2
677 4
678 5"});
679
680 cx.simulate_shared_keystrokes("shift-g ctrl-v g g").await;
681 cx.shared_state().await.assert_eq(indoc! {"
682 «1ˇ»
683 «2ˇ»
684 «3ˇ» 2
685 «4ˇ»
686 «5ˇ»"});
687
688 cx.simulate_shared_keystrokes("g ctrl-x").await;
689 cx.shared_state().await.assert_eq(indoc! {"
690 ˇ0
691 0
692 0 2
693 0
694 0"});
695 cx.simulate_shared_keystrokes("v shift-g g ctrl-a").await;
696 cx.simulate_shared_keystrokes("v shift-g 5 g ctrl-a").await;
697 cx.shared_state().await.assert_eq(indoc! {"
698 ˇ6
699 12
700 18 2
701 24
702 30"});
703 }
704
705 #[gpui::test]
706 async fn test_toggle_boolean(cx: &mut gpui::TestAppContext) {
707 let mut cx = VimTestContext::new(cx, true).await;
708
709 cx.set_state("let enabled = trˇue;", Mode::Normal);
710 cx.simulate_keystrokes("ctrl-a");
711 cx.assert_state("let enabled = falsˇe;", Mode::Normal);
712
713 cx.simulate_keystrokes("0 ctrl-a");
714 cx.assert_state("let enabled = truˇe;", Mode::Normal);
715
716 cx.set_state(
717 indoc! {"
718 ˇlet enabled = TRUE;
719 let enabled = TRUE;
720 let enabled = TRUE;
721 "},
722 Mode::Normal,
723 );
724 cx.simulate_keystrokes("shift-v j j ctrl-x");
725 cx.assert_state(
726 indoc! {"
727 ˇlet enabled = FALSE;
728 let enabled = FALSE;
729 let enabled = FALSE;
730 "},
731 Mode::Normal,
732 );
733
734 cx.set_state(
735 indoc! {"
736 let enabled = ˇYes;
737 let enabled = Yes;
738 let enabled = Yes;
739 "},
740 Mode::Normal,
741 );
742 cx.simulate_keystrokes("ctrl-v j j e ctrl-x");
743 cx.assert_state(
744 indoc! {"
745 let enabled = ˇNo;
746 let enabled = No;
747 let enabled = No;
748 "},
749 Mode::Normal,
750 );
751
752 cx.set_state("ˇlet enabled = True;", Mode::Normal);
753 cx.simulate_keystrokes("ctrl-a");
754 cx.assert_state("let enabled = Falsˇe;", Mode::Normal);
755
756 cx.simulate_keystrokes("ctrl-a");
757 cx.assert_state("let enabled = Truˇe;", Mode::Normal);
758
759 cx.set_state("let enabled = Onˇ;", Mode::Normal);
760 cx.simulate_keystrokes("v b ctrl-a");
761 cx.assert_state("let enabled = ˇOff;", Mode::Normal);
762 }
763}