increment.rs

  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}