increment.rs

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