Skip to repository content944 lines · 31.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:31:10.394Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
increment.rs
1use editor::{Editor, MultiBufferSnapshot, ToOffset, ToPoint};
2use gpui::{Action, Context, Window};
3use language::{Bias, Point};
4use schemars::JsonSchema;
5use serde::Deserialize;
6use std::ops::Range;
7
8use crate::{Vim, state::Mode};
9
10const BOOLEAN_PAIRS: &[(&str, &str)] = &[("true", "false"), ("yes", "no"), ("on", "off")];
11
12/// Increments the number under the cursor or toggles boolean values.
13#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
14#[action(namespace = vim)]
15#[serde(deny_unknown_fields)]
16struct Increment {
17 #[serde(default)]
18 step: bool,
19}
20
21/// Decrements the number under the cursor or toggles boolean values.
22#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
23#[action(namespace = vim)]
24#[serde(deny_unknown_fields)]
25struct Decrement {
26 #[serde(default)]
27 step: bool,
28}
29
30pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
31 Vim::action(editor, cx, |vim, action: &Increment, window, cx| {
32 vim.record_current_action(cx);
33 let count = Vim::take_count(cx).unwrap_or(1);
34 Vim::take_forced_motion(cx);
35 let step = if action.step { count as i32 } else { 0 };
36 vim.increment(count as i64, step, window, cx)
37 });
38 Vim::action(editor, cx, |vim, action: &Decrement, window, cx| {
39 vim.record_current_action(cx);
40 let count = Vim::take_count(cx).unwrap_or(1);
41 Vim::take_forced_motion(cx);
42 let step = if action.step { -1 * (count as i32) } else { 0 };
43 vim.increment(-(count as i64), step, window, cx)
44 });
45}
46
47impl Vim {
48 fn increment(
49 &mut self,
50 mut delta: i64,
51 step: i32,
52 window: &mut Window,
53 cx: &mut Context<Self>,
54 ) {
55 self.store_visual_marks(window, cx);
56 self.update_editor(cx, |vim, editor, cx| {
57 let mut edits = Vec::new();
58 let mut new_anchors = Vec::new();
59
60 let snapshot = editor.buffer().read(cx).snapshot(cx);
61 for selection in editor.selections.all_adjusted(&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 first_char_is_num = snapshot
207 .chars_at(start_offset)
208 .next()
209 .map_or(false, |ch| ch.is_ascii_hexdigit());
210 let mut pre_char = String::new();
211
212 let next_offset = start_offset
213 + snapshot
214 .chars_at(start_offset)
215 .next()
216 .map_or(0, |ch| ch.len_utf8());
217 // Backward scan to find the start of the number, but stop at start_offset.
218 // We track `offset` as the start position of the current character. Initialize
219 // to `next_offset` and decrement at the start of each iteration so that `offset`
220 // always lands on a valid character boundary (not in the middle of a multibyte char).
221 let mut offset = next_offset;
222 for ch in snapshot.reversed_chars_at(next_offset) {
223 offset -= ch.len_utf8();
224
225 // Search boundaries
226 if offset.0 == 0 || ch.is_whitespace() || (need_range && offset <= start_offset) {
227 break;
228 }
229
230 // vim's ctrl-a/ctrl-x operate on the number at or after the cursor and
231 // do not require it to be whitespace-separated. Stop the backward scan
232 // at a '-' so we keep the number the cursor is on (e.g. `05` in
233 // `2025-05-10`) instead of scanning past the '-' to an earlier number on
234 // the line. vim folds a leading '-' into the number, making it negative.
235 if ch == '-' {
236 break;
237 }
238
239 // Avoid the influence of hexadecimal letters
240 if first_char_is_num
241 && !ch.is_ascii_hexdigit()
242 && (ch != 'b' && ch != 'B')
243 && (ch != 'x' && ch != 'X')
244 && ch != '-'
245 {
246 // Used to determine if the initial character is a number.
247 if is_numeric_string(&pre_char) {
248 break;
249 } else {
250 first_char_is_num = false;
251 }
252 }
253
254 pre_char.insert(0, ch);
255 }
256
257 // The backward scan breaks on whitespace, including newlines. Without this
258 // skip, the forward scan would start on the newline and immediately break
259 // (since it also breaks on newlines), finding nothing on the current line.
260 if let Some(ch) = snapshot.chars_at(offset).next() {
261 if ch == '\n' {
262 offset += ch.len_utf8();
263 }
264 }
265
266 let mut begin = None;
267 let mut end = None;
268 let mut target = String::new();
269 let mut radix = 10;
270 let mut is_num = false;
271
272 let mut chars = snapshot.chars_at(offset).peekable();
273
274 while let Some(ch) = chars.next() {
275 if need_range && offset >= end_offset {
276 break; // stop at end of selection
277 }
278
279 if target == "0"
280 && (ch == 'b' || ch == 'B')
281 && chars.peek().is_some()
282 && chars.peek().unwrap().is_digit(2)
283 {
284 radix = 2;
285 begin = None;
286 target = String::new();
287 } else if target == "0"
288 && (ch == 'x' || ch == 'X')
289 && chars.peek().is_some()
290 && chars.peek().unwrap().is_ascii_hexdigit()
291 {
292 radix = 16;
293 begin = None;
294 target = String::new();
295 } else if ch == '.' {
296 // vim treats '.' as a separator, not a decimal point: ctrl-a/ctrl-x
297 // act on the whole digit run, not a float. So when the cursor is on
298 // the current number, terminate the match at the dot regardless of
299 // what follows it, so `ˇ1. item` and version strings like `0.8ˇ1.46`
300 // (-> `0.82.46`) both increment the number under the cursor. When the
301 // cursor is past the number (`111.ˇ.2`), `on_number` is false and we
302 // still reset so the forward scan finds the number after the dots.
303 let on_number =
304 is_num && begin.is_some_and(|begin| begin >= start_offset || start_offset < offset);
305
306 if on_number {
307 end = Some(offset);
308 break;
309 }
310
311 is_num = false;
312 begin = None;
313 target = String::new();
314 } else if ch.is_digit(radix)
315 || ((begin.is_none() || !is_num)
316 && ch == '-'
317 && chars.peek().is_some()
318 && chars.peek().unwrap().is_digit(radix))
319 {
320 if !is_num {
321 is_num = true;
322 begin = Some(offset);
323 target = String::new();
324 } else if begin.is_none() {
325 begin = Some(offset);
326 }
327 target.push(ch);
328 } else if ch.is_ascii_alphabetic() && !is_num {
329 if begin.is_none() {
330 begin = Some(offset);
331 }
332 target.push(ch);
333 } else if begin.is_some() && (is_num || !is_num && is_toggle_word(&target)) {
334 // End of matching
335 end = Some(offset);
336 break;
337 } else if ch == '\n' {
338 break;
339 } else {
340 // To match the next word
341 is_num = false;
342 begin = None;
343 target = String::new();
344 }
345
346 offset += ch.len_utf8();
347 }
348
349 if let Some(begin) = begin
350 && (is_num || !is_num && is_toggle_word(&target))
351 {
352 if !is_num {
353 radix = 0;
354 }
355
356 let end = end.unwrap_or(offset);
357 Some((
358 begin.to_point(snapshot)..end.to_point(snapshot),
359 target,
360 radix,
361 ))
362 } else {
363 None
364 }
365}
366
367fn is_numeric_string(s: &str) -> bool {
368 if s.is_empty() {
369 return false;
370 }
371
372 let (_, rest) = if let Some(r) = s.strip_prefix('-') {
373 (true, r)
374 } else {
375 (false, s)
376 };
377
378 if rest.is_empty() {
379 return false;
380 }
381
382 if let Some(digits) = rest.strip_prefix("0b").or_else(|| rest.strip_prefix("0B")) {
383 digits.is_empty() || digits.chars().all(|c| c == '0' || c == '1')
384 } else if let Some(digits) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) {
385 digits.is_empty() || digits.chars().all(|c| c.is_ascii_hexdigit())
386 } else {
387 !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit())
388 }
389}
390
391fn is_toggle_word(word: &str) -> bool {
392 let lower = word.to_lowercase();
393 BOOLEAN_PAIRS
394 .iter()
395 .any(|(a, b)| lower == *a || lower == *b)
396}
397
398fn increment_toggle_string(boolean: &str) -> String {
399 let lower = boolean.to_lowercase();
400
401 let target = BOOLEAN_PAIRS
402 .iter()
403 .find_map(|(a, b)| {
404 if lower == *a {
405 Some(b)
406 } else if lower == *b {
407 Some(a)
408 } else {
409 None
410 }
411 })
412 .unwrap_or(&boolean);
413
414 if boolean.chars().all(|c| c.is_uppercase()) {
415 // Upper case
416 target.to_uppercase()
417 } else if boolean.chars().next().unwrap_or(' ').is_uppercase() {
418 // Title case
419 let mut chars = target.chars();
420 match chars.next() {
421 None => String::new(),
422 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
423 }
424 } else {
425 target.to_string()
426 }
427}
428
429#[cfg(test)]
430mod test {
431 use indoc::indoc;
432
433 use crate::{
434 state::Mode,
435 test::{NeovimBackedTestContext, VimTestContext},
436 };
437
438 #[gpui::test]
439 async fn test_increment(cx: &mut gpui::TestAppContext) {
440 let mut cx = NeovimBackedTestContext::new(cx).await;
441
442 cx.set_shared_state(indoc! {"
443 1ˇ2
444 "})
445 .await;
446
447 cx.simulate_shared_keystrokes("ctrl-a").await;
448 cx.shared_state().await.assert_eq(indoc! {"
449 1ˇ3
450 "});
451 cx.simulate_shared_keystrokes("ctrl-x").await;
452 cx.shared_state().await.assert_eq(indoc! {"
453 1ˇ2
454 "});
455
456 cx.simulate_shared_keystrokes("9 9 ctrl-a").await;
457 cx.shared_state().await.assert_eq(indoc! {"
458 11ˇ1
459 "});
460 cx.simulate_shared_keystrokes("1 1 1 ctrl-x").await;
461 cx.shared_state().await.assert_eq(indoc! {"
462 ˇ0
463 "});
464 cx.simulate_shared_keystrokes(".").await;
465 cx.shared_state().await.assert_eq(indoc! {"
466 -11ˇ1
467 "});
468 }
469
470 #[gpui::test]
471 async fn test_increment_with_dot(cx: &mut gpui::TestAppContext) {
472 let mut cx = NeovimBackedTestContext::new(cx).await;
473
474 cx.set_shared_state(indoc! {"
475 1ˇ.2
476 "})
477 .await;
478
479 cx.simulate_shared_keystrokes("ctrl-a").await;
480 cx.shared_state().await.assert_eq(indoc! {"
481 1.ˇ3
482 "});
483 cx.simulate_shared_keystrokes("ctrl-x").await;
484 cx.shared_state().await.assert_eq(indoc! {"
485 1.ˇ2
486 "});
487
488 // '.' is a separator, not a decimal point, so the number the cursor is
489 // on is incremented even without surrounding whitespace.
490 cx.simulate("ctrl-a", "0.8ˇ1.46").await.assert_matches();
491 cx.simulate("ctrl-x", "0.8ˇ1.46").await.assert_matches();
492 }
493
494 #[gpui::test]
495 async fn test_increment_with_leading_zeros(cx: &mut gpui::TestAppContext) {
496 let mut cx = NeovimBackedTestContext::new(cx).await;
497
498 cx.set_shared_state(indoc! {"
499 000ˇ9
500 "})
501 .await;
502
503 cx.simulate_shared_keystrokes("ctrl-a").await;
504 cx.shared_state().await.assert_eq(indoc! {"
505 001ˇ0
506 "});
507 cx.simulate_shared_keystrokes("2 ctrl-x").await;
508 cx.shared_state().await.assert_eq(indoc! {"
509 000ˇ8
510 "});
511 }
512
513 #[gpui::test]
514 async fn test_increment_with_leading_zeros_and_zero(cx: &mut gpui::TestAppContext) {
515 let mut cx = NeovimBackedTestContext::new(cx).await;
516
517 cx.set_shared_state(indoc! {"
518 01ˇ1
519 "})
520 .await;
521
522 cx.simulate_shared_keystrokes("ctrl-a").await;
523 cx.shared_state().await.assert_eq(indoc! {"
524 01ˇ2
525 "});
526 cx.simulate_shared_keystrokes("1 2 ctrl-x").await;
527 cx.shared_state().await.assert_eq(indoc! {"
528 00ˇ0
529 "});
530 }
531
532 #[gpui::test]
533 async fn test_increment_with_changing_leading_zeros(cx: &mut gpui::TestAppContext) {
534 let mut cx = NeovimBackedTestContext::new(cx).await;
535
536 cx.set_shared_state(indoc! {"
537 099ˇ9
538 "})
539 .await;
540
541 cx.simulate_shared_keystrokes("ctrl-a").await;
542 cx.shared_state().await.assert_eq(indoc! {"
543 100ˇ0
544 "});
545 cx.simulate_shared_keystrokes("2 ctrl-x").await;
546 cx.shared_state().await.assert_eq(indoc! {"
547 99ˇ8
548 "});
549 }
550
551 #[gpui::test]
552 async fn test_increment_with_two_dots(cx: &mut gpui::TestAppContext) {
553 let mut cx = NeovimBackedTestContext::new(cx).await;
554
555 cx.set_shared_state(indoc! {"
556 111.ˇ.2
557 "})
558 .await;
559
560 cx.simulate_shared_keystrokes("ctrl-a").await;
561 cx.shared_state().await.assert_eq(indoc! {"
562 111..ˇ3
563 "});
564 cx.simulate_shared_keystrokes("ctrl-x").await;
565 cx.shared_state().await.assert_eq(indoc! {"
566 111..ˇ2
567 "});
568 }
569
570 #[gpui::test]
571 async fn test_increment_sign_change(cx: &mut gpui::TestAppContext) {
572 let mut cx = NeovimBackedTestContext::new(cx).await;
573 cx.set_shared_state(indoc! {"
574 ˇ0
575 "})
576 .await;
577 cx.simulate_shared_keystrokes("ctrl-x").await;
578 cx.shared_state().await.assert_eq(indoc! {"
579 -ˇ1
580 "});
581 cx.simulate_shared_keystrokes("2 ctrl-a").await;
582 cx.shared_state().await.assert_eq(indoc! {"
583 ˇ1
584 "});
585 }
586
587 #[gpui::test]
588 async fn test_increment_sign_change_with_leading_zeros(cx: &mut gpui::TestAppContext) {
589 let mut cx = NeovimBackedTestContext::new(cx).await;
590 cx.set_shared_state(indoc! {"
591 00ˇ1
592 "})
593 .await;
594 cx.simulate_shared_keystrokes("ctrl-x").await;
595 cx.shared_state().await.assert_eq(indoc! {"
596 00ˇ0
597 "});
598 cx.simulate_shared_keystrokes("ctrl-x").await;
599 cx.shared_state().await.assert_eq(indoc! {"
600 -00ˇ1
601 "});
602 cx.simulate_shared_keystrokes("2 ctrl-a").await;
603 cx.shared_state().await.assert_eq(indoc! {"
604 00ˇ1
605 "});
606 }
607
608 #[gpui::test]
609 async fn test_increment_bin_wrapping_and_padding(cx: &mut gpui::TestAppContext) {
610 let mut cx = NeovimBackedTestContext::new(cx).await;
611 cx.set_shared_state(indoc! {"
612 0b111111111111111111111111111111111111111111111111111111111111111111111ˇ1
613 "})
614 .await;
615
616 cx.simulate_shared_keystrokes("ctrl-a").await;
617 cx.shared_state().await.assert_eq(indoc! {"
618 0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1
619 "});
620 cx.simulate_shared_keystrokes("ctrl-a").await;
621 cx.shared_state().await.assert_eq(indoc! {"
622 0b000000000000000000000000000000000000000000000000000000000000000000000ˇ0
623 "});
624
625 cx.simulate_shared_keystrokes("ctrl-a").await;
626 cx.shared_state().await.assert_eq(indoc! {"
627 0b000000000000000000000000000000000000000000000000000000000000000000000ˇ1
628 "});
629 cx.simulate_shared_keystrokes("2 ctrl-x").await;
630 cx.shared_state().await.assert_eq(indoc! {"
631 0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1
632 "});
633 }
634
635 #[gpui::test]
636 async fn test_increment_hex_wrapping_and_padding(cx: &mut gpui::TestAppContext) {
637 let mut cx = NeovimBackedTestContext::new(cx).await;
638 cx.set_shared_state(indoc! {"
639 0xfffffffffffffffffffˇf
640 "})
641 .await;
642
643 cx.simulate_shared_keystrokes("ctrl-a").await;
644 cx.shared_state().await.assert_eq(indoc! {"
645 0x0000fffffffffffffffˇf
646 "});
647 cx.simulate_shared_keystrokes("ctrl-a").await;
648 cx.shared_state().await.assert_eq(indoc! {"
649 0x0000000000000000000ˇ0
650 "});
651 cx.simulate_shared_keystrokes("ctrl-a").await;
652 cx.shared_state().await.assert_eq(indoc! {"
653 0x0000000000000000000ˇ1
654 "});
655 cx.simulate_shared_keystrokes("2 ctrl-x").await;
656 cx.shared_state().await.assert_eq(indoc! {"
657 0x0000fffffffffffffffˇf
658 "});
659 }
660
661 #[gpui::test]
662 async fn test_increment_wrapping(cx: &mut gpui::TestAppContext) {
663 let mut cx = NeovimBackedTestContext::new(cx).await;
664 cx.set_shared_state(indoc! {"
665 1844674407370955161ˇ9
666 "})
667 .await;
668
669 cx.simulate_shared_keystrokes("ctrl-a").await;
670 cx.shared_state().await.assert_eq(indoc! {"
671 1844674407370955161ˇ5
672 "});
673 cx.simulate_shared_keystrokes("ctrl-a").await;
674 cx.shared_state().await.assert_eq(indoc! {"
675 -1844674407370955161ˇ5
676 "});
677 cx.simulate_shared_keystrokes("ctrl-a").await;
678 cx.shared_state().await.assert_eq(indoc! {"
679 -1844674407370955161ˇ4
680 "});
681 cx.simulate_shared_keystrokes("3 ctrl-x").await;
682 cx.shared_state().await.assert_eq(indoc! {"
683 1844674407370955161ˇ4
684 "});
685 cx.simulate_shared_keystrokes("2 ctrl-a").await;
686 cx.shared_state().await.assert_eq(indoc! {"
687 -1844674407370955161ˇ5
688 "});
689 }
690
691 #[gpui::test]
692 async fn test_increment_inline(cx: &mut gpui::TestAppContext) {
693 let mut cx = NeovimBackedTestContext::new(cx).await;
694 cx.set_shared_state(indoc! {"
695 inline0x3ˇ9u32
696 "})
697 .await;
698
699 cx.simulate_shared_keystrokes("ctrl-a").await;
700 cx.shared_state().await.assert_eq(indoc! {"
701 inline0x3ˇau32
702 "});
703 cx.simulate_shared_keystrokes("ctrl-a").await;
704 cx.shared_state().await.assert_eq(indoc! {"
705 inline0x3ˇbu32
706 "});
707 cx.simulate_shared_keystrokes("l l l ctrl-a").await;
708 cx.shared_state().await.assert_eq(indoc! {"
709 inline0x3bu3ˇ3
710 "});
711 }
712
713 #[gpui::test]
714 async fn test_increment_hex_casing(cx: &mut gpui::TestAppContext) {
715 let mut cx = NeovimBackedTestContext::new(cx).await;
716 cx.set_shared_state(indoc! {"
717 0xFˇa
718 "})
719 .await;
720
721 cx.simulate_shared_keystrokes("ctrl-a").await;
722 cx.shared_state().await.assert_eq(indoc! {"
723 0xfˇb
724 "});
725 cx.simulate_shared_keystrokes("ctrl-a").await;
726 cx.shared_state().await.assert_eq(indoc! {"
727 0xfˇc
728 "});
729 }
730
731 #[gpui::test]
732 async fn test_increment_radix(cx: &mut gpui::TestAppContext) {
733 let mut cx = NeovimBackedTestContext::new(cx).await;
734
735 cx.simulate("ctrl-a", "ˇ total: 0xff")
736 .await
737 .assert_matches();
738 cx.simulate("ctrl-x", "ˇ total: 0xff")
739 .await
740 .assert_matches();
741 cx.simulate("ctrl-x", "ˇ total: 0xFF")
742 .await
743 .assert_matches();
744 cx.simulate("ctrl-a", "(ˇ0b10f)").await.assert_matches();
745 cx.simulate("ctrl-a", "ˇ-1").await.assert_matches();
746 cx.simulate("ctrl-a", "-ˇ1").await.assert_matches();
747 cx.simulate("ctrl-a", "banˇana").await.assert_matches();
748 }
749
750 #[gpui::test]
751 async fn test_increment_steps(cx: &mut gpui::TestAppContext) {
752 let mut cx = NeovimBackedTestContext::new(cx).await;
753
754 cx.set_shared_state(indoc! {"
755 ˇ1
756 1
757 1 2
758 1
759 1"})
760 .await;
761
762 cx.simulate_shared_keystrokes("j v shift-g g ctrl-a").await;
763 cx.shared_state().await.assert_eq(indoc! {"
764 1
765 ˇ2
766 3 2
767 4
768 5"});
769
770 cx.simulate_shared_keystrokes("shift-g ctrl-v g g").await;
771 cx.shared_state().await.assert_eq(indoc! {"
772 «1ˇ»
773 «2ˇ»
774 «3ˇ» 2
775 «4ˇ»
776 «5ˇ»"});
777
778 cx.simulate_shared_keystrokes("g ctrl-x").await;
779 cx.shared_state().await.assert_eq(indoc! {"
780 ˇ0
781 0
782 0 2
783 0
784 0"});
785 cx.simulate_shared_keystrokes("v shift-g g ctrl-a").await;
786 cx.simulate_shared_keystrokes("v shift-g 5 g ctrl-a").await;
787 cx.shared_state().await.assert_eq(indoc! {"
788 ˇ6
789 12
790 18 2
791 24
792 30"});
793 }
794
795 #[gpui::test]
796 async fn test_increment_negative_numbers(cx: &mut gpui::TestAppContext) {
797 let mut cx = NeovimBackedTestContext::new(cx).await;
798
799 // vim folds a leading '-' into the number, so ctrl-a on the `05` here
800 // operates on `-05` and decrements the visible digits to `04`.
801 cx.simulate("ctrl-a", "2025-0ˇ5-10").await.assert_matches();
802
803 // Cursor on or just before a trailing '-' (with or without a following
804 // number) must not scan past the '-' into the earlier number.
805 cx.simulate("ctrl-a", "2025-05ˇ-").await.assert_matches();
806 cx.simulate("ctrl-a", "2025-05ˇ- 345")
807 .await
808 .assert_matches();
809 }
810
811 #[gpui::test]
812 async fn test_increment_toggle(cx: &mut gpui::TestAppContext) {
813 let mut cx = VimTestContext::new(cx, true).await;
814
815 cx.set_state("let enabled = trˇue;", Mode::Normal);
816 cx.simulate_keystrokes("ctrl-a");
817 cx.assert_state("let enabled = falsˇe;", Mode::Normal);
818
819 cx.simulate_keystrokes("0 ctrl-a");
820 cx.assert_state("let enabled = truˇe;", Mode::Normal);
821
822 cx.set_state(
823 indoc! {"
824 ˇlet enabled = TRUE;
825 let enabled = TRUE;
826 let enabled = TRUE;
827 "},
828 Mode::Normal,
829 );
830 cx.simulate_keystrokes("shift-v j j ctrl-x");
831 cx.assert_state(
832 indoc! {"
833 ˇlet enabled = FALSE;
834 let enabled = FALSE;
835 let enabled = FALSE;
836 "},
837 Mode::Normal,
838 );
839
840 cx.set_state(
841 indoc! {"
842 let enabled = ˇYes;
843 let enabled = Yes;
844 let enabled = Yes;
845 "},
846 Mode::Normal,
847 );
848 cx.simulate_keystrokes("ctrl-v j j e ctrl-x");
849 cx.assert_state(
850 indoc! {"
851 let enabled = ˇNo;
852 let enabled = No;
853 let enabled = No;
854 "},
855 Mode::Normal,
856 );
857
858 cx.set_state("ˇlet enabled = True;", Mode::Normal);
859 cx.simulate_keystrokes("ctrl-a");
860 cx.assert_state("let enabled = Falsˇe;", Mode::Normal);
861
862 cx.simulate_keystrokes("ctrl-a");
863 cx.assert_state("let enabled = Truˇe;", Mode::Normal);
864
865 cx.set_state("let enabled = Onˇ;", Mode::Normal);
866 cx.simulate_keystrokes("v b ctrl-a");
867 cx.assert_state("let enabled = ˇOff;", Mode::Normal);
868 }
869
870 #[gpui::test]
871 async fn test_increment_order(cx: &mut gpui::TestAppContext) {
872 let mut cx = VimTestContext::new(cx, true).await;
873
874 cx.set_state("aaˇa false 1 2 3", Mode::Normal);
875 cx.simulate_keystrokes("ctrl-a");
876 cx.assert_state("aaa truˇe 1 2 3", Mode::Normal);
877
878 cx.set_state("aaˇa 1 false 2 3", Mode::Normal);
879 cx.simulate_keystrokes("ctrl-a");
880 cx.assert_state("aaa ˇ2 false 2 3", Mode::Normal);
881
882 cx.set_state("trueˇ 1 2 3", Mode::Normal);
883 cx.simulate_keystrokes("ctrl-a");
884 cx.assert_state("true ˇ2 2 3", Mode::Normal);
885
886 cx.set_state("falseˇ", Mode::Normal);
887 cx.simulate_keystrokes("ctrl-a");
888 cx.assert_state("truˇe", Mode::Normal);
889
890 cx.set_state("⚡️ˇ⚡️", Mode::Normal);
891 cx.simulate_keystrokes("ctrl-a");
892 cx.assert_state("⚡️ˇ⚡️", Mode::Normal);
893 }
894
895 #[gpui::test]
896 async fn test_increment_visual_partial_number(cx: &mut gpui::TestAppContext) {
897 let mut cx = NeovimBackedTestContext::new(cx).await;
898
899 cx.set_shared_state("ˇ123").await;
900 cx.simulate_shared_keystrokes("v l ctrl-a").await;
901 cx.shared_state().await.assert_eq(indoc! {"ˇ133"});
902 cx.simulate_shared_keystrokes("l v l ctrl-a").await;
903 cx.shared_state().await.assert_eq(indoc! {"1ˇ34"});
904 cx.simulate_shared_keystrokes("shift-v y p p ctrl-v k k l ctrl-a")
905 .await;
906 cx.shared_state().await.assert_eq(indoc! {"ˇ144\n144\n144"});
907 }
908
909 #[gpui::test]
910 async fn test_increment_markdown_list_markers_multiline(cx: &mut gpui::TestAppContext) {
911 let mut cx = NeovimBackedTestContext::new(cx).await;
912
913 cx.set_shared_state("# Title\nˇ1. item\n2. item\n3. item")
914 .await;
915 cx.simulate_shared_keystrokes("ctrl-a").await;
916 cx.shared_state()
917 .await
918 .assert_eq("# Title\nˇ2. item\n2. item\n3. item");
919 cx.simulate_shared_keystrokes("j").await;
920 cx.shared_state()
921 .await
922 .assert_eq("# Title\n2. item\nˇ2. item\n3. item");
923 cx.simulate_shared_keystrokes("ctrl-a").await;
924 cx.shared_state()
925 .await
926 .assert_eq("# Title\n2. item\nˇ3. item\n3. item");
927 cx.simulate_shared_keystrokes("ctrl-x").await;
928 cx.shared_state()
929 .await
930 .assert_eq("# Title\n2. item\nˇ2. item\n3. item");
931 }
932
933 #[gpui::test]
934 async fn test_increment_with_multibyte_characters(cx: &mut gpui::TestAppContext) {
935 let mut cx = VimTestContext::new(cx, true).await;
936
937 // Test cursor after a multibyte character - this would panic before the fix
938 // because the backward scan would land in the middle of the Korean character
939 cx.set_state("지ˇ1", Mode::Normal);
940 cx.simulate_keystrokes("ctrl-a");
941 cx.assert_state("지ˇ2", Mode::Normal);
942 }
943}
944