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