From 57476e78dca8bdb30bc6c1b4db2afb085c7c7b98 Mon Sep 17 00:00:00 2001 From: Vaclav Tvrdik Date: Thu, 24 Sep 2026 22:32:14 +0200 Subject: [PATCH] "[" and "]" cycle over changes (hunks) --- README.md | 2 +- tests/test_commands.py | 48 ++++++++++++++++++++++++++++++++++++++++++ vdiff/commands.py | 10 ++++++--- vdiff/render.py | 2 +- 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9936d16..e011206 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Keys inside the review view: | `s` / `u` / `d` | stage / unstage / discard the file (uncommitted mode) | | `C` or `m` | commit the staged files, typing the message in an input panel | | `o` or Enter | open the real file at this line | -| `]` / `[` | next / previous change | +| `]` / `[` | next / previous change (wraps at the ends) | | `n` / `p` | next / previous file of the current list (wraps at the ends) | | `j` / `k` | move down / up one line (vim keys) | | `r` | refresh | diff --git a/tests/test_commands.py b/tests/test_commands.py index 36b1b65..5fef742 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -403,3 +403,51 @@ def test_first_step_without_a_shown_file_picks_an_end(loading, rings): assert requested_path(queue) == "f4.py" # neither is a wrap: there was no position to run past assert rings == [] + + +def at_change(blocks, row): + """A rendered view with `blocks` as its changes and the caret on `row`""" + view = sublime.View() + st = state.ViewState("/repo") + st.blocks = blocks + view.text = "\n" + "\n".join("l{}".format(i) for i in range(20)) + view.sel().add(sublime.Region(commands.render.index_point(view, row))) + return view, st + + +def test_next_change_wraps_past_the_last(rings): + view, st = at_change([(2, 4), (7, 8)], 7) + + commands.ACTIONS["next_change"](view, None, st) + assert commands.render.caret_index(view) == 2 and len(rings) == 1 + + +def test_prev_change_wraps_before_the_first(rings): + view, st = at_change([(2, 4), (7, 8)], 2) + + commands.ACTIONS["prev_change"](view, None, st) + assert commands.render.caret_index(view) == 7 and len(rings) == 1 + + +def test_stepping_inside_the_changes_does_not_ring(rings): + view, st = at_change([(2, 4), (7, 8), (12, 13)], 7) + + commands.ACTIONS["next_change"](view, None, st) + commands.ACTIONS["prev_change"](view, None, st) + assert commands.render.caret_index(view) == 7 and rings == [] + + +def test_single_change_rings_and_stays_put(rings): + view, st = at_change([(2, 4)], 2) + + commands.ACTIONS["next_change"](view, None, st) + commands.ACTIONS["prev_change"](view, None, st) + assert commands.render.caret_index(view) == 2 and len(rings) == 2 + + +def test_a_file_without_changes_neither_moves_nor_rings(rings): + view, st = at_change([], 3) + + commands.ACTIONS["next_change"](view, None, st) + commands.ACTIONS["prev_change"](view, None, st) + assert commands.render.caret_index(view) == 3 and rings == [] diff --git a/vdiff/commands.py b/vdiff/commands.py index 9a640be..fb0c296 100644 --- a/vdiff/commands.py +++ b/vdiff/commands.py @@ -380,11 +380,15 @@ def _step_file(delta): def _step_change(delta): def act(view, window, st): starts = [start for start, _ in st.blocks] + if not starts: + status("no changes") + return row = render.caret_index(view) position = bisect.bisect_right(starts, row) if delta > 0 else bisect.bisect_left(starts, row) - 1 - if not 0 <= position < len(starts): - status("no more changes") - return + wrapped = not 0 <= position < len(starts) + position %= len(starts) # -1 is the last change, len(starts) the first + if wrapped: + render.flash(view) point = render.index_point(view, starts[position]) view.sel().clear() view.sel().add(sublime.Region(point)) diff --git a/vdiff/render.py b/vdiff/render.py index 6b0e574..abd8b48 100644 --- a/vdiff/render.py +++ b/vdiff/render.py @@ -61,7 +61,7 @@ Key = namedtuple("Key", "keys label short actions") KEY_GROUPS = [ ("Navigate", True, [ - Key("] [", "next / previous change", "change", ("next_change", "prev_change")), + Key("] [", "next / previous change (wraps)", "change", ("next_change", "prev_change")), Key("n p", "next / previous file (wraps)", "file", ("next_file", "prev_file")), Key("o \u23ce", "open the real file at this line", "open", ("open",)), Key("j k", "down / up one line (vim)", "", ("line_down", "line_up")),