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