fix "n" and "p" keys not working all the time

This commit is contained in:
Vaclav Tvrdik
2026-09-24 16:49:52 +02:00
parent 120676e952
commit 419b1a95fb
6 changed files with 133 additions and 27 deletions

View File

@@ -1,3 +1,5 @@
import pytest
import sublime # the stub, via conftest
from vdiff import commands, git, state
@@ -236,3 +238,94 @@ def test_commit_items_keep_a_readable_subject_when_the_annotation_is_huge():
def test_commit_items_fall_back_for_an_empty_subject():
assert commands.commit_items([commit("")])[0].trigger == "(no subject)"
# --- navigating while a diff is still loading -------------------------------
@pytest.fixture
def loading(monkeypatch):
"""Queue run_async instead of running it inline, so a test can act mid-load like a user can
The stub runs set_timeout synchronously, which is why the real plugin's async window - the one
n / p used to read stale state through - is invisible to every other test in the suite.
"""
queue = []
monkeypatch.setattr(commands, "run_async", lambda work, done: queue.append((work, done)))
monkeypatch.setattr(commands.render, "render", lambda view, st: None)
monkeypatch.setattr(commands.git, "full_diff", lambda *a, **k: "--- a/x\n+++ b/x\n@@ -1 +1 @@\n-a\n+b\n")
def flush():
while queue:
work, done = queue.pop(0)
done(work())
return queue, flush
def commit_panel(files):
"""A review view showing a commit, its file panel open, nothing loaded yet"""
view, window = sublime.View(), sublime.Window()
st = state.ViewState("/repo")
st.enter("abc", git.CommitInfo("abc", "abc1234", "A", "d", "s", ""))
commands.show_files_panel(window, view, st, files, st.kept(), placeholder="c")
return view, window, st
def numbered(count=5):
return [git.FileChange("f{}.py".format(i), "M", False, 1, 1, None) for i in range(count)]
def requested_path(queue):
"""Path of the file the newest queued load will fetch"""
return queue[-1][0].__closure__[0].cell_contents.path
def test_next_file_steps_from_the_file_being_loaded(loading):
queue, _ = loading
files = numbered()
view, window, st = commit_panel(files)
window.panel[1](2) # Enter on the third file; its diff has not arrived yet
assert st.change is None
commands.ACTIONS["next_file"](view, window, st)
assert requested_path(queue) == "f3.py"
def test_prev_file_works_before_the_diff_arrives(loading, monkeypatch):
queue, _ = loading
messages = []
monkeypatch.setattr(commands.sublime, "status_message", lambda m: messages.append(m))
files = numbered()
view, window, st = commit_panel(files)
window.panel[1](2)
commands.ACTIONS["prev_file"](view, window, st)
assert requested_path(queue) == "f1.py" and messages == []
def test_rapid_next_file_presses_each_advance(loading):
queue, flush = loading
files = numbered()
view, window, st = commit_panel(files)
window.panel[1](1)
for _ in range(3):
commands.ACTIONS["next_file"](view, window, st)
assert requested_path(queue) == "f4.py"
flush()
assert st.change.path == "f4.py"
def test_esc_rewinds_the_file_navigation_counts_from(loading):
queue, flush = loading
files = numbered()
view, window, st = commit_panel(files)
window.panel[1](3)
flush()
kept = st.kept()
window.panel[1](0) # preview another file, then back out
st.restore(kept)
commands.ACTIONS["next_file"](view, window, st)
assert requested_path(queue) == "f4.py"

View File

@@ -197,18 +197,18 @@ 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_no_binding_yields_to_vintage_command_mode():
"""The review view is read-only, so its letters are vdiff commands in either vim mode
def test_command_mode_guard_is_deliberate():
"""Every binding yields to Vintage except the ones that documented a reason not to"""
init_view opens the view in command mode; a binding that still carried the old
`command_mode: false` context would go dead there, which is what broke n / p.
"""
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"]
assert guard not in binding["context"], binding["keys"]
view = sublime.View()
render.init_view(view)
assert view.settings().get("command_mode") is True
def test_minihtml_stylesheets_reject_unsupported_selectors():