added j / k for vim-style line movement

j and k move the caret one line, delegated to sublime's own `move` command
so they behave exactly like the arrow keys. They dispatch through the
existing ACTIONS registry, so the keymap, the status-bar hints and the ?
popup all stay in sync through the single KEY_GROUPS table.

Unlike the other letter keys, j / k keep no command_mode context: the view
is read-only, so leaving them live while Vintage is in command mode can
only ever move the caret. That asymmetry was previously documented only in
a comment, which was wrong (it claimed j / k were the only unguarded
bindings, but escape has always carried its own context instead). It is now
pinned by test_command_mode_guard_is_deliberate, so normalising the keymap
fails the suite with the reason attached.

Also tidies two long lines in commands.py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vaclav Tvrdik
2026-09-24 15:03:32 +02:00
parent 07f0503cef
commit d45a250e62
6 changed files with 51 additions and 7 deletions

View File

@@ -134,7 +134,7 @@ def test_help_popup_lists_every_key():
assert view.is_popup_visible()
for group in ("Navigate", "Panels", "This file", "View"):
assert ">{}<".format(group) in html
for keys in ("] [", "n p", "o ⏎", "f", "c", "s", "u", "d", "r", "?", "esc"):
for keys in ("] [", "n p", "o ⏎", "j k", "f", "c", "s", "u", "d", "r", "?", "esc"):
assert ">{}<".format(keys) in html
assert "next / previous change" in html and "open the real file at this line" in html
# working-tree mode: nothing dimmed, every single-action row is a link
@@ -176,11 +176,15 @@ def test_help_popup_toggles_and_navigates():
DISPLAY_KEYS = {"enter": "⏎", "escape": "esc"}
def test_keymap_matches_help_table():
"""The keymap, ACTIONS and the popup agree on both the actions and the keys that reach them"""
def keymap_bindings():
path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "Default.sublime-keymap")
with open(path, encoding="utf-8") as f:
bindings = json.load(f)
return json.load(f)
def test_keymap_matches_help_table():
"""The keymap, ACTIONS and the popup agree on both the actions and the keys that reach them"""
bindings = keymap_bindings()
assert all(b["command"] == "vdiff_action" for b in bindings)
bound = {b["args"]["action"] for b in bindings}
@@ -192,6 +196,20 @@ def test_keymap_matches_help_table():
assert bound_keys == {token for k in render.KEYS for token in k.keys.split()}
# j / k stay live in Vintage command mode on purpose: the view is read-only, so the worst they can do
# is move the caret, and a vim user should not have to leave command mode to scroll a diff.
# escape carries its own context (panel / overlay / popup / selection guards) instead.
UNGUARDED_ACTIONS = {"line_down", "line_up", "close"}
def test_command_mode_guard_is_deliberate():
"""Every binding yields to Vintage except the ones that documented a reason not to"""
guard = {"key": "setting.command_mode", "operand": False}
for binding in keymap_bindings():
unguarded = binding["args"]["action"] in UNGUARDED_ACTIONS
assert (guard in binding["context"]) != unguarded, binding["keys"]
def test_minihtml_stylesheets_reject_unsupported_selectors():
"""minihtml drops the rest of a stylesheet at a selector it cannot parse; these are the forms
that silently broke HELP_CSS, so no stylesheet constant may use them"""