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        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}