increment.rs

  1use std::ops::Range;
  2
  3use editor::{scroll::Autoscroll, Editor, MultiBufferSnapshot, ToOffset, ToPoint};
  4use gpui::{impl_actions, ViewContext};
  5use language::{Bias, Point};
  6use serde::Deserialize;
  7
  8use crate::{state::Mode, Vim};
  9
 10#[derive(Clone, Deserialize, PartialEq)]
 11#[serde(rename_all = "camelCase")]
 12struct Increment {
 13    #[serde(default)]
 14    step: bool,
 15}
 16
 17#[derive(Clone, Deserialize, PartialEq)]
 18#[serde(rename_all = "camelCase")]
 19struct Decrement {
 20    #[serde(default)]
 21    step: bool,
 22}
 23
 24impl_actions!(vim, [Increment, Decrement]);
 25
 26pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
 27    Vim::action(editor, cx, |vim, action: &Increment, cx| {
 28        vim.record_current_action(cx);
 29        let count = vim.take_count(cx).unwrap_or(1);
 30        let step = if action.step { 1 } else { 0 };
 31        vim.increment(count as i64, step, cx)
 32    });
 33    Vim::action(editor, cx, |vim, action: &Decrement, cx| {
 34        vim.record_current_action(cx);
 35        let count = vim.take_count(cx).unwrap_or(1);
 36        let step = if action.step { -1 } else { 0 };
 37        vim.increment(-(count as i64), step, cx)
 38    });
 39}
 40
 41impl Vim {
 42    fn increment(&mut self, mut delta: i64, step: i32, cx: &mut ViewContext<Self>) {
 43        self.store_visual_marks(cx);
 44        self.update_editor(cx, |vim, editor, cx| {
 45            let mut edits = Vec::new();
 46            let mut new_anchors = Vec::new();
 47
 48            let snapshot = editor.buffer().read(cx).snapshot(cx);
 49            for selection in editor.selections.all_adjusted(cx) {
 50                if !selection.is_empty()
 51                    && (vim.mode != Mode::VisualBlock || new_anchors.is_empty())
 52                {
 53                    new_anchors.push((true, snapshot.anchor_before(selection.start)))
 54                }
 55                for row in selection.start.row..=selection.end.row {
 56                    let start = if row == selection.start.row {
 57                        selection.start
 58                    } else {
 59                        Point::new(row, 0)
 60                    };
 61
 62                    if let Some((range, num, radix)) = find_number(&snapshot, start) {
 63                        let replace = match radix {
 64                            10 => increment_decimal_string(&num, delta),
 65                            16 => increment_hex_string(&num, delta),
 66                            2 => increment_binary_string(&num, delta),
 67                            _ => unreachable!(),
 68                        };
 69                        delta += step as i64;
 70                        edits.push((range.clone(), replace));
 71                        if selection.is_empty() {
 72                            new_anchors.push((false, snapshot.anchor_after(range.end)))
 73                        }
 74                    } else if selection.is_empty() {
 75                        new_anchors.push((true, snapshot.anchor_after(start)))
 76                    }
 77                }
 78            }
 79            editor.transact(cx, |editor, cx| {
 80                editor.edit(edits, cx);
 81
 82                let snapshot = editor.buffer().read(cx).snapshot(cx);
 83                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 84                    let mut new_ranges = Vec::new();
 85                    for (visual, anchor) in new_anchors.iter() {
 86                        let mut point = anchor.to_point(&snapshot);
 87                        if !*visual && point.column > 0 {
 88                            point.column -= 1;
 89                            point = snapshot.clip_point(point, Bias::Left)
 90                        }
 91                        new_ranges.push(point..point);
 92                    }
 93                    s.select_ranges(new_ranges)
 94                })
 95            });
 96        });
 97        self.switch_mode(Mode::Normal, true, cx)
 98    }
 99}
100
101fn increment_decimal_string(mut num: &str, mut delta: i64) -> String {
102    let mut negative = false;
103    if num.chars().next() == Some('-') {
104        negative = true;
105        delta = 0 - delta;
106        num = &num[1..];
107    }
108    let result = if let Ok(value) = u64::from_str_radix(num, 10) {
109        let wrapped = value.wrapping_add_signed(delta);
110        if delta < 0 && wrapped > value {
111            negative = !negative;
112            (u64::MAX - wrapped).wrapping_add(1)
113        } else if delta > 0 && wrapped < value {
114            negative = !negative;
115            u64::MAX - wrapped
116        } else {
117            wrapped
118        }
119    } else {
120        u64::MAX
121    };
122
123    if result == 0 || !negative {
124        format!("{}", result)
125    } else {
126        format!("-{}", result)
127    }
128}
129
130fn increment_hex_string(num: &str, delta: i64) -> String {
131    let result = if let Ok(val) = u64::from_str_radix(&num, 16) {
132        val.wrapping_add_signed(delta)
133    } else {
134        u64::MAX
135    };
136    if should_use_lowercase(num) {
137        format!("{:0width$x}", result, width = num.len())
138    } else {
139        format!("{:0width$X}", result, width = num.len())
140    }
141}
142
143fn should_use_lowercase(num: &str) -> bool {
144    let mut use_uppercase = false;
145    for ch in num.chars() {
146        if ch.is_ascii_lowercase() {
147            return true;
148        }
149        if ch.is_ascii_uppercase() {
150            use_uppercase = true;
151        }
152    }
153    !use_uppercase
154}
155
156fn increment_binary_string(num: &str, delta: i64) -> String {
157    let result = if let Ok(val) = u64::from_str_radix(&num, 2) {
158        val.wrapping_add_signed(delta)
159    } else {
160        u64::MAX
161    };
162    format!("{:0width$b}", result, width = num.len())
163}
164
165fn find_number(
166    snapshot: &MultiBufferSnapshot,
167    start: Point,
168) -> Option<(Range<Point>, String, u32)> {
169    let mut offset = start.to_offset(snapshot);
170
171    let ch0 = snapshot.chars_at(offset).next();
172    if ch0.as_ref().is_some_and(char::is_ascii_hexdigit) || matches!(ch0, Some('-' | 'b' | 'x')) {
173        // go backwards to the start of any number the selection is within
174        for ch in snapshot.reversed_chars_at(offset) {
175            if ch.is_ascii_hexdigit() || ch == '-' || ch == 'b' || ch == 'x' {
176                offset -= ch.len_utf8();
177                continue;
178            }
179            break;
180        }
181    }
182
183    let mut begin = None;
184    let mut end = None;
185    let mut num = String::new();
186    let mut radix = 10;
187
188    let mut chars = snapshot.chars_at(offset).peekable();
189    // find the next number on the line (may start after the original cursor position)
190    while let Some(ch) = chars.next() {
191        if num == "0" && ch == 'b' && chars.peek().is_some() && chars.peek().unwrap().is_digit(2) {
192            radix = 2;
193            begin = None;
194            num = String::new();
195        }
196        if num == "0"
197            && ch == 'x'
198            && chars.peek().is_some()
199            && chars.peek().unwrap().is_ascii_hexdigit()
200        {
201            radix = 16;
202            begin = None;
203            num = String::new();
204        }
205
206        if ch.is_digit(radix)
207            || (begin.is_none()
208                && ch == '-'
209                && chars.peek().is_some()
210                && chars.peek().unwrap().is_digit(radix))
211        {
212            if begin.is_none() {
213                begin = Some(offset);
214            }
215            num.push(ch);
216            println!("pushing {}", ch);
217            println!();
218        } else if begin.is_some() {
219            end = Some(offset);
220            break;
221        } else if ch == '\n' {
222            break;
223        }
224        offset += ch.len_utf8();
225    }
226    if let Some(begin) = begin {
227        let end = end.unwrap_or(offset);
228        Some((begin.to_point(snapshot)..end.to_point(snapshot), num, radix))
229    } else {
230        None
231    }
232}
233
234#[cfg(test)]
235mod test {
236    use indoc::indoc;
237
238    use crate::test::NeovimBackedTestContext;
239
240    #[gpui::test]
241    async fn test_increment(cx: &mut gpui::TestAppContext) {
242        let mut cx = NeovimBackedTestContext::new(cx).await;
243
244        cx.set_shared_state(indoc! {"
245            1ˇ2
246            "})
247            .await;
248
249        cx.simulate_shared_keystrokes("ctrl-a").await;
250        cx.shared_state().await.assert_eq(indoc! {"
251            1ˇ3
252            "});
253        cx.simulate_shared_keystrokes("ctrl-x").await;
254        cx.shared_state().await.assert_eq(indoc! {"
255            1ˇ2
256            "});
257
258        cx.simulate_shared_keystrokes("9 9 ctrl-a").await;
259        cx.shared_state().await.assert_eq(indoc! {"
260            11ˇ1
261            "});
262        cx.simulate_shared_keystrokes("1 1 1 ctrl-x").await;
263        cx.shared_state().await.assert_eq(indoc! {"
264            ˇ0
265            "});
266        cx.simulate_shared_keystrokes(".").await;
267        cx.shared_state().await.assert_eq(indoc! {"
268            -11ˇ1
269            "});
270    }
271
272    #[gpui::test]
273    async fn test_increment_with_dot(cx: &mut gpui::TestAppContext) {
274        let mut cx = NeovimBackedTestContext::new(cx).await;
275
276        cx.set_shared_state(indoc! {"
277            1ˇ.2
278            "})
279            .await;
280
281        cx.simulate_shared_keystrokes("ctrl-a").await;
282        cx.shared_state().await.assert_eq(indoc! {"
283            1.ˇ3
284            "});
285        cx.simulate_shared_keystrokes("ctrl-x").await;
286        cx.shared_state().await.assert_eq(indoc! {"
287            1.ˇ2
288            "});
289    }
290
291    #[gpui::test]
292    async fn test_increment_with_two_dots(cx: &mut gpui::TestAppContext) {
293        let mut cx = NeovimBackedTestContext::new(cx).await;
294
295        cx.set_shared_state(indoc! {"
296            111.ˇ.2
297            "})
298            .await;
299
300        cx.simulate_shared_keystrokes("ctrl-a").await;
301        cx.shared_state().await.assert_eq(indoc! {"
302            111..ˇ3
303            "});
304        cx.simulate_shared_keystrokes("ctrl-x").await;
305        cx.shared_state().await.assert_eq(indoc! {"
306            111..ˇ2
307            "});
308    }
309
310    #[gpui::test]
311    async fn test_increment_sign_change(cx: &mut gpui::TestAppContext) {
312        let mut cx = NeovimBackedTestContext::new(cx).await;
313        cx.set_shared_state(indoc! {"
314                ˇ0
315                "})
316            .await;
317        cx.simulate_shared_keystrokes("ctrl-x").await;
318        cx.shared_state().await.assert_eq(indoc! {"
319                -ˇ1
320                "});
321        cx.simulate_shared_keystrokes("2 ctrl-a").await;
322        cx.shared_state().await.assert_eq(indoc! {"
323                ˇ1
324                "});
325    }
326
327    #[gpui::test]
328    async fn test_increment_bin_wrapping_and_padding(cx: &mut gpui::TestAppContext) {
329        let mut cx = NeovimBackedTestContext::new(cx).await;
330        cx.set_shared_state(indoc! {"
331                    0b111111111111111111111111111111111111111111111111111111111111111111111ˇ1
332                    "})
333            .await;
334
335        cx.simulate_shared_keystrokes("ctrl-a").await;
336        cx.shared_state().await.assert_eq(indoc! {"
337                    0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1
338                    "});
339        cx.simulate_shared_keystrokes("ctrl-a").await;
340        cx.shared_state().await.assert_eq(indoc! {"
341                    0b000000000000000000000000000000000000000000000000000000000000000000000ˇ0
342                    "});
343
344        cx.simulate_shared_keystrokes("ctrl-a").await;
345        cx.shared_state().await.assert_eq(indoc! {"
346                    0b000000000000000000000000000000000000000000000000000000000000000000000ˇ1
347                    "});
348        cx.simulate_shared_keystrokes("2 ctrl-x").await;
349        cx.shared_state().await.assert_eq(indoc! {"
350                    0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1
351                    "});
352    }
353
354    #[gpui::test]
355    async fn test_increment_hex_wrapping_and_padding(cx: &mut gpui::TestAppContext) {
356        let mut cx = NeovimBackedTestContext::new(cx).await;
357        cx.set_shared_state(indoc! {"
358                    0xfffffffffffffffffffˇf
359                    "})
360            .await;
361
362        cx.simulate_shared_keystrokes("ctrl-a").await;
363        cx.shared_state().await.assert_eq(indoc! {"
364                    0x0000fffffffffffffffˇf
365                    "});
366        cx.simulate_shared_keystrokes("ctrl-a").await;
367        cx.shared_state().await.assert_eq(indoc! {"
368                    0x0000000000000000000ˇ0
369                    "});
370        cx.simulate_shared_keystrokes("ctrl-a").await;
371        cx.shared_state().await.assert_eq(indoc! {"
372                    0x0000000000000000000ˇ1
373                    "});
374        cx.simulate_shared_keystrokes("2 ctrl-x").await;
375        cx.shared_state().await.assert_eq(indoc! {"
376                    0x0000fffffffffffffffˇf
377                    "});
378    }
379
380    #[gpui::test]
381    async fn test_increment_wrapping(cx: &mut gpui::TestAppContext) {
382        let mut cx = NeovimBackedTestContext::new(cx).await;
383        cx.set_shared_state(indoc! {"
384                    1844674407370955161ˇ9
385                    "})
386            .await;
387
388        cx.simulate_shared_keystrokes("ctrl-a").await;
389        cx.shared_state().await.assert_eq(indoc! {"
390                    1844674407370955161ˇ5
391                    "});
392        cx.simulate_shared_keystrokes("ctrl-a").await;
393        cx.shared_state().await.assert_eq(indoc! {"
394                    -1844674407370955161ˇ5
395                    "});
396        cx.simulate_shared_keystrokes("ctrl-a").await;
397        cx.shared_state().await.assert_eq(indoc! {"
398                    -1844674407370955161ˇ4
399                    "});
400        cx.simulate_shared_keystrokes("3 ctrl-x").await;
401        cx.shared_state().await.assert_eq(indoc! {"
402                    1844674407370955161ˇ4
403                    "});
404        cx.simulate_shared_keystrokes("2 ctrl-a").await;
405        cx.shared_state().await.assert_eq(indoc! {"
406                    -1844674407370955161ˇ5
407                    "});
408    }
409
410    #[gpui::test]
411    async fn test_increment_inline(cx: &mut gpui::TestAppContext) {
412        let mut cx = NeovimBackedTestContext::new(cx).await;
413        cx.set_shared_state(indoc! {"
414                    inline0x3ˇ9u32
415                    "})
416            .await;
417
418        cx.simulate_shared_keystrokes("ctrl-a").await;
419        cx.shared_state().await.assert_eq(indoc! {"
420                    inline0x3ˇau32
421                    "});
422        cx.simulate_shared_keystrokes("ctrl-a").await;
423        cx.shared_state().await.assert_eq(indoc! {"
424                    inline0x3ˇbu32
425                    "});
426        cx.simulate_shared_keystrokes("l l l ctrl-a").await;
427        cx.shared_state().await.assert_eq(indoc! {"
428                    inline0x3bu3ˇ3
429                    "});
430    }
431
432    #[gpui::test]
433    async fn test_increment_hex_casing(cx: &mut gpui::TestAppContext) {
434        let mut cx = NeovimBackedTestContext::new(cx).await;
435        cx.set_shared_state(indoc! {"
436                        0xFˇa
437                    "})
438            .await;
439
440        cx.simulate_shared_keystrokes("ctrl-a").await;
441        cx.shared_state().await.assert_eq(indoc! {"
442                    0xfˇb
443                    "});
444        cx.simulate_shared_keystrokes("ctrl-a").await;
445        cx.shared_state().await.assert_eq(indoc! {"
446                    0xfˇc
447                    "});
448    }
449
450    #[gpui::test]
451    async fn test_increment_radix(cx: &mut gpui::TestAppContext) {
452        let mut cx = NeovimBackedTestContext::new(cx).await;
453
454        cx.simulate("ctrl-a", "ˇ total: 0xff")
455            .await
456            .assert_matches();
457        cx.simulate("ctrl-x", "ˇ total: 0xff")
458            .await
459            .assert_matches();
460        cx.simulate("ctrl-x", "ˇ total: 0xFF")
461            .await
462            .assert_matches();
463        cx.simulate("ctrl-a", "(ˇ0b10f)").await.assert_matches();
464        cx.simulate("ctrl-a", "ˇ-1").await.assert_matches();
465        cx.simulate("ctrl-a", "banˇana").await.assert_matches();
466    }
467
468    #[gpui::test]
469    async fn test_increment_steps(cx: &mut gpui::TestAppContext) {
470        let mut cx = NeovimBackedTestContext::new(cx).await;
471
472        cx.set_shared_state(indoc! {"
473            ˇ1
474            1
475            1  2
476            1
477            1"})
478            .await;
479
480        cx.simulate_shared_keystrokes("j v shift-g g ctrl-a").await;
481        cx.shared_state().await.assert_eq(indoc! {"
482            1
483            ˇ2
484            3  2
485            4
486            5"});
487
488        cx.simulate_shared_keystrokes("shift-g ctrl-v g g").await;
489        cx.shared_state().await.assert_eq(indoc! {"
490            «1ˇ»
491            «2ˇ»
492            «3ˇ»  2
493            «4ˇ»
494            «5ˇ»"});
495
496        cx.simulate_shared_keystrokes("g ctrl-x").await;
497        cx.shared_state().await.assert_eq(indoc! {"
498            ˇ0
499            0
500            0  2
501            0
502            0"});
503    }
504}