increment.rs

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