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 i32, 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 i32), step, cx)
 38    });
 39}
 40
 41impl Vim {
 42    fn increment(&mut self, mut delta: i32, 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                        if let Ok(val) = i32::from_str_radix(&num, radix) {
 64                            let result = val + delta;
 65                            delta += step;
 66                            let replace = match radix {
 67                                10 => format!("{}", result),
 68                                16 => {
 69                                    if num.to_ascii_lowercase() == num {
 70                                        format!("{:x}", result)
 71                                    } else {
 72                                        format!("{:X}", result)
 73                                    }
 74                                }
 75                                2 => format!("{:b}", result),
 76                                _ => unreachable!(),
 77                            };
 78                            edits.push((range.clone(), replace));
 79                        }
 80                        if selection.is_empty() {
 81                            new_anchors.push((false, snapshot.anchor_after(range.end)))
 82                        }
 83                    } else if selection.is_empty() {
 84                        new_anchors.push((true, snapshot.anchor_after(start)))
 85                    }
 86                }
 87            }
 88            editor.transact(cx, |editor, cx| {
 89                editor.edit(edits, cx);
 90
 91                let snapshot = editor.buffer().read(cx).snapshot(cx);
 92                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 93                    let mut new_ranges = Vec::new();
 94                    for (visual, anchor) in new_anchors.iter() {
 95                        let mut point = anchor.to_point(&snapshot);
 96                        if !*visual && point.column > 0 {
 97                            point.column -= 1;
 98                            point = snapshot.clip_point(point, Bias::Left)
 99                        }
100                        new_ranges.push(point..point);
101                    }
102                    s.select_ranges(new_ranges)
103                })
104            });
105        });
106        self.switch_mode(Mode::Normal, true, cx)
107    }
108}
109
110fn find_number(
111    snapshot: &MultiBufferSnapshot,
112    start: Point,
113) -> Option<(Range<Point>, String, u32)> {
114    let mut offset = start.to_offset(snapshot);
115
116    let ch0 = snapshot.chars_at(offset).next();
117    if ch0.as_ref().is_some_and(char::is_ascii_digit) || matches!(ch0, Some('-' | 'b' | 'x')) {
118        // go backwards to the start of any number the selection is within
119        for ch in snapshot.reversed_chars_at(offset) {
120            if ch.is_ascii_digit() || ch == '-' || ch == 'b' || ch == 'x' {
121                offset -= ch.len_utf8();
122                continue;
123            }
124            break;
125        }
126    }
127
128    let mut begin = None;
129    let mut end = None;
130    let mut num = String::new();
131    let mut radix = 10;
132
133    let mut chars = snapshot.chars_at(offset).peekable();
134    // find the next number on the line (may start after the original cursor position)
135    while let Some(ch) = chars.next() {
136        if num == "0" && ch == 'b' && chars.peek().is_some() && chars.peek().unwrap().is_digit(2) {
137            radix = 2;
138            begin = None;
139            num = String::new();
140        }
141        if num == "0"
142            && ch == 'x'
143            && chars.peek().is_some()
144            && chars.peek().unwrap().is_ascii_hexdigit()
145        {
146            radix = 16;
147            begin = None;
148            num = String::new();
149        }
150
151        if ch.is_digit(radix)
152            || (begin.is_none()
153                && ch == '-'
154                && chars.peek().is_some()
155                && chars.peek().unwrap().is_digit(radix))
156        {
157            if begin.is_none() {
158                begin = Some(offset);
159            }
160            num.push(ch);
161        } else if begin.is_some() {
162            end = Some(offset);
163            break;
164        } else if ch == '\n' {
165            break;
166        }
167        offset += ch.len_utf8();
168    }
169    if let Some(begin) = begin {
170        let end = end.unwrap_or(offset);
171        Some((begin.to_point(snapshot)..end.to_point(snapshot), num, radix))
172    } else {
173        None
174    }
175}
176
177#[cfg(test)]
178mod test {
179    use indoc::indoc;
180
181    use crate::test::NeovimBackedTestContext;
182
183    #[gpui::test]
184    async fn test_increment(cx: &mut gpui::TestAppContext) {
185        let mut cx = NeovimBackedTestContext::new(cx).await;
186
187        cx.set_shared_state(indoc! {"
188            1ˇ2
189            "})
190            .await;
191
192        cx.simulate_shared_keystrokes("ctrl-a").await;
193        cx.shared_state().await.assert_eq(indoc! {"
194            1ˇ3
195            "});
196        cx.simulate_shared_keystrokes("ctrl-x").await;
197        cx.shared_state().await.assert_eq(indoc! {"
198            1ˇ2
199            "});
200
201        cx.simulate_shared_keystrokes("9 9 ctrl-a").await;
202        cx.shared_state().await.assert_eq(indoc! {"
203            11ˇ1
204            "});
205        cx.simulate_shared_keystrokes("1 1 1 ctrl-x").await;
206        cx.shared_state().await.assert_eq(indoc! {"
207            ˇ0
208            "});
209        cx.simulate_shared_keystrokes(".").await;
210        cx.shared_state().await.assert_eq(indoc! {"
211            -11ˇ1
212            "});
213    }
214
215    #[gpui::test]
216    async fn test_increment_with_dot(cx: &mut gpui::TestAppContext) {
217        let mut cx = NeovimBackedTestContext::new(cx).await;
218
219        cx.set_shared_state(indoc! {"
220            1ˇ.2
221            "})
222            .await;
223
224        cx.simulate_shared_keystrokes("ctrl-a").await;
225        cx.shared_state().await.assert_eq(indoc! {"
226            1.ˇ3
227            "});
228        cx.simulate_shared_keystrokes("ctrl-x").await;
229        cx.shared_state().await.assert_eq(indoc! {"
230            1.ˇ2
231            "});
232    }
233
234    #[gpui::test]
235    async fn test_increment_with_two_dots(cx: &mut gpui::TestAppContext) {
236        let mut cx = NeovimBackedTestContext::new(cx).await;
237
238        cx.set_shared_state(indoc! {"
239            111.ˇ.2
240            "})
241            .await;
242
243        cx.simulate_shared_keystrokes("ctrl-a").await;
244        cx.shared_state().await.assert_eq(indoc! {"
245            111..ˇ3
246            "});
247        cx.simulate_shared_keystrokes("ctrl-x").await;
248        cx.shared_state().await.assert_eq(indoc! {"
249            111..ˇ2
250            "});
251    }
252
253    #[gpui::test]
254    async fn test_increment_radix(cx: &mut gpui::TestAppContext) {
255        let mut cx = NeovimBackedTestContext::new(cx).await;
256
257        cx.simulate("ctrl-a", "ˇ total: 0xff")
258            .await
259            .assert_matches();
260        cx.simulate("ctrl-x", "ˇ total: 0xff")
261            .await
262            .assert_matches();
263        cx.simulate("ctrl-x", "ˇ total: 0xFF")
264            .await
265            .assert_matches();
266        cx.simulate("ctrl-a", "(ˇ0b10f)").await.assert_matches();
267        cx.simulate("ctrl-a", "ˇ-1").await.assert_matches();
268        cx.simulate("ctrl-a", "banˇana").await.assert_matches();
269    }
270
271    #[gpui::test]
272    async fn test_increment_steps(cx: &mut gpui::TestAppContext) {
273        let mut cx = NeovimBackedTestContext::new(cx).await;
274
275        cx.set_shared_state(indoc! {"
276            ˇ1
277            1
278            1  2
279            1
280            1"})
281            .await;
282
283        cx.simulate_shared_keystrokes("j v shift-g g ctrl-a").await;
284        cx.shared_state().await.assert_eq(indoc! {"
285            1
286            ˇ2
287            3  2
288            4
289            5"});
290
291        cx.simulate_shared_keystrokes("shift-g ctrl-v g g").await;
292        cx.shared_state().await.assert_eq(indoc! {"
293            «1ˇ»
294            «2ˇ»
295            «3ˇ»  2
296            «4ˇ»
297            «5ˇ»"});
298
299        cx.simulate_shared_keystrokes("g ctrl-x").await;
300        cx.shared_state().await.assert_eq(indoc! {"
301            ˇ0
302            0
303            0  2
304            0
305            0"});
306    }
307}