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