Let --context expand any selection past its range, drawing from the whole file

This commit is contained in:
LeoVasanko committed 2026-09-28 04:46:08 +00:00
1 parent c2821dedb5
commit 788d285e38
3 files changed
+90 -11

No files matched your search

+1 -1
View File
@@ -82,7 +82,7 @@ A revision number identifies a *state position between diffs*, not a diff:
Like repeated `--grep` patterns (which must all match), repeated ranges are restrictive: `-r r1:r5 -r 0:2` prints only the first two records of those in the revision window, regardless of flag order. Plain ranges truncate Python-style and print nothing when the intersection is empty; a single plain index must exist and fails with exit code 2 otherwise.
`--context N` (default 0) adds GNU grep-style context as the final step: each printed item is expanded by N surrounding records drawn from the records in the processed range, free of the `--grep` and plain-number filters, with overlapping expansions merged so nearby matches do not duplicate context records.
`--context N` (default 0) adds GNU grep-style context as the final step: each printed item is expanded by N surrounding records drawn from the whole file — expanding whatever would be shown past any range restriction, so `-r s3 --context 2` shows that snapshot plus the two records before and after it. Overlapping expansions merge so nearby matches do not duplicate context records.
The JSON dump (`-o`) and migrations operate on the state after the **last printed record**, whatever filtering produced the printout — so a plain `-r 2:5` dump ends at the 5th filtered record, and `--grep` or `--context` shift the dump to their last printed record as well. When nothing is printed (an empty filter result or `-q`), the dump falls back to the state at the end of the unit-spec window (or the end of the file).
+27 -8
View File
@@ -289,9 +289,10 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
help=(
"Print N surrounding records around each printed item, GNU grep"
" style. Applied last, after --grep filtering and plain-number"
" -r slicing: context records are drawn from the records in the"
" processed range, free of those filters, and overlapping"
" expansions merge. Without --grep this has no visible effect."
" -r slicing: context records are drawn from the whole file,"
" expanding whatever would be shown past any range restriction,"
" and overlapping expansions merge. Without any other filter"
" this has no visible effect."
),
)
args = parser.parse_args(argv)
@@ -547,7 +548,7 @@ async def _run(args: argparse.Namespace) -> int:
grep_patterns = [GrepPattern.parse(p) for p in args.grep or ()]
if selection.snapshot is not None:
if selection.snapshot is not None and not args.context:
snap_event = selection.snapshot
state = snap_event.snap.state
version = snap_event.snap.v
@@ -560,16 +561,31 @@ async def _run(args: argparse.Namespace) -> int:
)
_print()
else:
# Replay up to the range end, collecting the in-window item pool
# and tracking which items survive --grep.
# Replay, collecting the item pool and tracking which items
# survive the range window and --grep. With --context the pool
# spans the whole file, so context can expand whatever would be
# shown past any range restriction; otherwise the pool is
# limited to the window.
state = {}
version = 0
window_state: dict[str, Any] = {}
window_version = 0
pool_end = end_of_file(events) if args.context else selection.end_line
pool: list[tuple[Event, dict[str, Any] | None, dict[str, Any], Any]] = []
matched: list[int] = []
for event, previous, current in replay_events(events, selection.end_line):
for event, previous, current in replay_events(events, pool_end):
if event.line_number < selection.end_line:
window_state = current
window_version = event.version
state = current
version = event.version
if event.line_number < selection.start_line or args.quiet:
if (
args.quiet
or event.line_number < selection.start_line
or event.line_number >= selection.end_line
):
if not args.quiet and args.context:
pool.append((event, previous, current, None))
continue
highlight = None
if isinstance(event, SnapshotEvent):
@@ -629,6 +645,9 @@ async def _run(args: argparse.Namespace) -> int:
last_event, _, last_current, _ = items[-1]
state = last_current
version = last_event.version
else:
state = window_state
version = window_version
for event, previous, current, highlight in items:
label = record_label(event.line_number, event.record_index)
if isinstance(event, SnapshotEvent):
+62 -2
View File
@@ -2,13 +2,17 @@
import asyncio
import json
from datetime import UTC, datetime
from kanta.serialization import JsonSerializer
from kanta.structs import META_FORMAT, ChangeRecord, Snapshot
from tests.support import Data, make_kanta
# The CLI reads files with its own JSON Kanta instance unless -k is given.
JSON = ("json", JsonSerializer)
T0 = datetime(2026, 1, 1, tzinfo=UTC)
def _run_cli(path, *args):
from kanta.__main__ import main
@@ -148,7 +152,7 @@ def test_unit_and_plain_combine(tmp_path, capsys):
def test_context_expands_after_grep(tmp_path, capsys):
"""Context records come from the in-window pool, free of --grep."""
"""Context records come from the whole file, free of --grep."""
path = tmp_path / "data.kantadb"
_build(path, increments=5)
capsys.readouterr()
@@ -168,7 +172,7 @@ def test_context_expands_after_grep(tmp_path, capsys):
assert "inc1" not in err
assert "inc2" in err
# Context is clamped at the start of the range.
# Context is clamped at the start of the file.
assert _run_cli(path, "--grep", "bootstrap", "--context", "2") == 0
err = capsys.readouterr().err
assert "bootstrap" in err
@@ -177,6 +181,62 @@ def test_context_expands_after_grep(tmp_path, capsys):
assert "inc2" not in err
def test_context_expands_past_unit_window(tmp_path, capsys):
"""Context expands whatever a unit selection would show, past the window."""
path = tmp_path / "data.kantadb"
_build(path)
capsys.readouterr()
# -r r2 alone shows only inc0 (the record producing state 2); context 1
# expands past the window to bootstrap and inc1, and the dump follows
# the last printed record.
assert _run_cli(path, "-r", "r2", "--context", "1", "-o", "-") == 0
out = capsys.readouterr()
assert "bootstrap" in out.err
assert "inc0" in out.err
assert "inc1" in out.err
assert "inc2" not in out.err
assert json.loads(out.out) == {"users": {}, "counter": 2}
def test_context_expands_snapshot_selection(tmp_path, capsys):
"""-r sN --context N shows the snapshot plus its neighbors."""
path = tmp_path / "data.kantadb"
serializer = JsonSerializer()
framer = serializer.framer_cls()
out_bytes = bytearray()
out_bytes += framer.frame_change(
serializer.encode(ChangeRecord(ts=T0, a="inc0", v=0, diff={"counter": 1})),
record_offset=len(out_bytes),
)
snap = Snapshot(ts=T0, v=0, state={"counter": 1}, r=1, k=META_FORMAT)
out_bytes += framer.frame_snapshot(
serializer.encode(snap), record_offset=len(out_bytes)
)
out_bytes += framer.frame_change(
serializer.encode(ChangeRecord(ts=T0, a="inc1", v=0, diff={"counter": 2})),
record_offset=len(out_bytes),
)
path.write_bytes(bytes(out_bytes))
capsys.readouterr()
# Without context, s1 (the file starts with a change, so s0 is l0)
# shows just the snapshot and its state.
assert _run_cli(path, "-r", "s1", "-o", "-") == 0
out = capsys.readouterr()
assert "snapshot s1" in out.err
assert "inc0" not in out.err
assert json.loads(out.out) == {"counter": 1}
# With context, the selection expands past the snapshot line.
assert _run_cli(path, "-r", "s1", "--context", "1", "-o", "-") == 0
out = capsys.readouterr()
assert "inc0" in out.err
assert "snapshot s1" in out.err
assert "inc1" in out.err
assert json.loads(out.out) == {"counter": 2}
def test_context_merges_overlaps(tmp_path, capsys):
"""Nearby matches share context instead of duplicating records."""
path = tmp_path / "data.kantadb"