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 * -1, 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                    if vim.mode != Mode::VisualBlock || new_anchors.is_empty() {
 52                        new_anchors.push((true, snapshot.anchor_before(selection.start)))
 53                    }
 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 {
 84                        if selection.is_empty() {
 85                            new_anchors.push((true, snapshot.anchor_after(start)))
 86                        }
 87                    }
 88                }
 89            }
 90            editor.transact(cx, |editor, cx| {
 91                editor.edit(edits, cx);
 92
 93                let snapshot = editor.buffer().read(cx).snapshot(cx);
 94                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 95                    let mut new_ranges = Vec::new();
 96                    for (visual, anchor) in new_anchors.iter() {
 97                        let mut point = anchor.to_point(&snapshot);
 98                        if !*visual && point.column > 0 {
 99                            point.column -= 1;
100                            point = snapshot.clip_point(point, Bias::Left)
101                        }
102                        new_ranges.push(point..point);
103                    }
104                    s.select_ranges(new_ranges)
105                })
106            });
107        });
108        self.switch_mode(Mode::Normal, true, cx)
109    }
110}
111
112fn find_number(
113    snapshot: &MultiBufferSnapshot,
114    start: Point,
115) -> Option<(Range<Point>, String, u32)> {
116    let mut offset = start.to_offset(snapshot);
117
118    let ch0 = snapshot.chars_at(offset).next();
119    if ch0.as_ref().is_some_and(char::is_ascii_digit) || matches!(ch0, Some('-' | 'b' | 'x')) {
120        // go backwards to the start of any number the selection is within
121        for ch in snapshot.reversed_chars_at(offset) {
122            if ch.is_ascii_digit() || ch == '-' || ch == 'b' || ch == 'x' {
123                offset -= ch.len_utf8();
124                continue;
125            }
126            break;
127        }
128    }
129
130    let mut begin = None;
131    let mut end = None;
132    let mut num = String::new();
133    let mut radix = 10;
134
135    let mut chars = snapshot.chars_at(offset).peekable();
136    // find the next number on the line (may start after the original cursor position)
137    while let Some(ch) = chars.next() {
138        if num == "0" && ch == 'b' && chars.peek().is_some() && chars.peek().unwrap().is_digit(2) {
139            radix = 2;
140            begin = None;
141            num = String::new();
142        }
143        if num == "0" && ch == 'x' && chars.peek().is_some() && chars.peek().unwrap().is_digit(16) {
144            radix = 16;
145            begin = None;
146            num = String::new();
147        }
148
149        if ch.is_digit(radix)
150            || (begin.is_none()
151                && ch == '-'
152                && chars.peek().is_some()
153                && chars.peek().unwrap().is_digit(radix))
154        {
155            if begin.is_none() {
156                begin = Some(offset);
157            }
158            num.push(ch);
159        } else {
160            if begin.is_some() {
161                end = Some(offset);
162                break;
163            } else if ch == '\n' {
164                break;
165            }
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}