diff --git a/CHANGELOG.md b/CHANGELOG.md index cd63f11..b1ab4f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project are documented here. Versions follow ## [Unreleased] +## [1.14.1] — 2026-09-13 + +### Changed + +- Improve report readability. + ## [1.14.0] — 2026-09-11 ### Added diff --git a/compare_tool/__init__.py b/compare_tool/__init__.py index f720ac1..eb55963 100644 --- a/compare_tool/__init__.py +++ b/compare_tool/__init__.py @@ -1,3 +1,3 @@ """CodeGen Compare Tool - AUTOSAR MATLAB codegen diff with noise filtering.""" -__version__ = "1.14.0" +__version__ = "1.14.1" diff --git a/compare_tool/report.py b/compare_tool/report.py index 2536500..28e964d 100644 --- a/compare_tool/report.py +++ b/compare_tool/report.py @@ -169,6 +169,11 @@ caption on the diff, not as a diff row. */ .fnhdr { font-family: Consolas, monospace; font-size: 11px; color: var(--fg-muted); padding: 3px 8px 1px; } +/* groups inside one scope share the first group's caption (see _groups_html). + Which caption comes first depends on the Reviewed badge folding signed-off + groups away: .dup repeats the one above while every group shows, .dup-unrev + while reviewed groups are hidden. */ +body:not(.hide-rev) .fnhdr.dup, body.hide-rev .fnhdr.dup-unrev { display: none; } tr.mvnote td { text-align: center; color: var(--mv-fg); background: var(--panel-2); font-size: 11px; } /* Unimportant rows hide per ROW, not per group: a group used to be wrapped @@ -661,6 +666,9 @@ def _groups_html(old_lines, new_lines, hunks, notes=None, language=None, funcname.enclosing(new_lines, language)) out = [] rows_so_far = 0 + # the scope named by the nearest code group above: once with every group + # on screen, once with signed-off groups folded away by the Reviewed badge + above = above_unrev = None runs = _focus_runs(hunks) for idx, (i, j, lean) in enumerate(runs): # --max-diff-lines caps the diff embedded per file: a regenerate that @@ -682,10 +690,24 @@ def _groups_html(old_lines, new_lines, hunks, notes=None, language=None, cls = ' grp-rev' if done else '' block = ['
'.format(' lean' if lean else '', cls)] # a lean group is a collapsed noise placeholder with no code window, so - # a caption over it would point at rows that are not shown - label = None if lean else funcname.hunk_label(old_labels, new_labels, g[0]) - if label: - block.append('
ƒ {}
'.format(_esc(label))) + # a caption over it would point at rows that are not shown; naming no + # scope, it does not break a run of groups inside one either + if not lean: + label = funcname.hunk_label(old_labels, new_labels, g[0]) + # a caption only where the scope changes, not over every table of + # one function. The Reviewed badge can fold the group carrying it + # away, so a caption that repeats in only one badge state is still + # emitted, marked for the CSS to hide in that state. A signed-off + # group is itself hidden once reviewed groups are, so only the + # every-group-shown state decides its own caption. + dup = label == above + dup_unrev = dup if done else label == above_unrev + if label and not (dup and dup_unrev): + mark = ' dup' if dup else ' dup-unrev' if dup_unrev else '' + block.append('
ƒ {}
'.format(mark, _esc(label))) + above = label + if not done: + above_unrev = label block.append(notes_html) budget = (max_rows - rows_so_far) if max_rows else 0 block.append(_group_table(old_lines, new_lines, g, language, old_states, diff --git a/docs/architecture.md b/docs/architecture.md index 02192f2..5b83a01 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -360,11 +360,10 @@ until someone adds a new kind to one of them. detach them on the next scan. - **`funcname.enclosing`** — the scope name for each line (C/C++ function, Python class/method, AUTOSAR SHORT-NAME, A2L block), one list the report and - the viewer both read. The - report captions each hunk group and lists a file's `Affected` functions from - it; the viewer tracks a "current function" as the pane scrolls. It never - decides a verdict — a wrong name costs a caption, so the heuristics say - `None` rather than guess. + the viewer both read. The report shows it above each change group (once, + when consecutive groups share it); the viewer tracks a "current function" as + the pane scrolls. It never decides a verdict — a wrong name costs a caption, + so the heuristics say `None` rather than guess. - **`langspec.SPECS`** — one table of each language's comment/string grammar (what opens a comment, how a string escapes). Two very different surfaces read it: `syntax.py` colours a comment, and the diff shadow (`langspec.shadow` for diff --git a/docs/usage.md b/docs/usage.md index 2384f2b..0507f97 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -287,6 +287,8 @@ Each real change shows three lines of context on either side of it — not the w The lines themselves are always in the file — only the screen stays quiet about them. A file whose differences are purely comments doesn't even get a detail section; it just keeps its `≈` mark and `Comment` count in the tree. (If you're curious why the window is kept this tight rather than wider, that's covered in [architecture.md](architecture.md#decisions-worth-knowing-before-you-change-something).) +Each change is labelled with the function it sits in (for ARXML the SHORT-NAME, for A2L the block). When several changes sit in the same function, the name appears once, above the first. + `Focus on changes`, next to the folder tree, narrows the tree down to files that actually changed — identical, comment-only and Unimportant rows drop out, and any folder left holding none of them goes with them. Like the viewer's `Hide identical`, this is purely a view: verdicts and counts underneath are untouched. A `☀ Light` / `☾ Dark` button sits in the top right; both palettes are embedded in the file itself, so switching between them fetches nothing and works fine on a machine with no internet connection at all. ## CI integration diff --git a/docs/vi/architecture.md b/docs/vi/architecture.md index 5255f51..27bb2a6 100644 --- a/docs/vi/architecture.md +++ b/docs/vi/architecture.md @@ -287,10 +287,10 @@ khớp nhau hoàn hảo cho tới lúc ai đó thêm một kind mới vào một làm chúng rớt ra ở lần scan sau. - **`funcname.enclosing`** — tên scope của mỗi dòng (hàm C/C++, class/method Python, SHORT-NAME AUTOSAR, block A2L), một list mà cả report và viewer cùng - đọc. Report chú thích mỗi nhóm - hunk và liệt kê danh sách `Affected` của file từ đó; viewer bám theo "hàm hiện - tại" khi pane cuộn. Nó không bao giờ quyết định verdict — tên sai chỉ tốn một - caption — nên heuristic thà trả `None` còn hơn đoán. + đọc. Report hiện tên này phía trên mỗi nhóm thay đổi (chỉ một lần nếu các nhóm + liền nhau cùng scope); viewer bám theo "hàm hiện tại" khi pane cuộn. Nó không + bao giờ quyết định verdict — tên sai chỉ tốn một caption — nên heuristic thà + trả `None` còn hơn đoán. - **`langspec.SPECS`** — một bảng duy nhất mô tả ngữ pháp comment/string của mỗi ngôn ngữ (cái gì mở một comment, string escape ra sao). Hai surface rất khác nhau cùng đọc nó: `syntax.py` tô màu comment, còn shadow của diff diff --git a/docs/vi/usage.md b/docs/vi/usage.md index 1c047f5..d7eb6cf 100644 --- a/docs/vi/usage.md +++ b/docs/vi/usage.md @@ -451,6 +451,10 @@ khác biệt *chỉ* là comment thì không có mục chi tiết riêng; nó ch không rộng hơn: [architecture.md](architecture.md#những-quyết-định-nên-biết-trước-khi-sửa).) +Mỗi change được ghi tên hàm chứa nó (với ARXML là SHORT-NAME, với A2L là +block). Nhiều change cùng nằm trong một hàm thì tên hàm chỉ hiện một lần, ở trên +change đầu tiên. + `Focus on changes`, cạnh cây thư mục, thu cây lại còn đúng các file thật sự có thay đổi — dòng identical, comment-only và Unimportant biến mất, thư mục nào chỉ còn lại toàn những loại đó thì biến theo luôn. Giống `Hide identical` bên diff --git a/tests/test_report.py b/tests/test_report.py index f32d0ea..bee12a4 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -131,6 +131,89 @@ def test_report_shows_minor_hunks_in_modified_files(self): self.assertIn('delm', page) # NoiseDemo.c has comment hunks too +_CAPTION_RE = re.compile(r'
ƒ ([^<]*)
') + + +def _scoped_c(scopes): + """C source with one function per ``(name, x, y)``, the ``x`` and ``y`` + statements padded far enough apart -- and from the next function -- that + a change to either renders as its own group.""" + lines = [] + for name, x, y in scopes: + lines += ['void {}(void)'.format(name), '{'] + for stmt in ('x = {};'.format(x), 'y = {};'.format(y), None): + # unique filler, so the line matcher has nothing to mis-anchor on + lines += [' pad{} = 0;'.format(len(lines) + k) for k in range(8)] + if stmt: + lines.append(' ' + stmt) + lines += ['}', ''] + return '\n'.join(lines) + + +class TestFunctionCaption(unittest.TestCase): + """The ``ƒ scope`` caption is printed where the scope changes, not over + every group: consecutive groups inside one function share the first one's. + The Reviewed badge can fold that first group away, so a repeat is marked + per badge state instead of being dropped.""" + + OLD = _scoped_c([('step_a', 1, 2), ('step_b', 3, 4)]) + + @staticmethod + def _render(old, new, rel, language, signed_off=()): + r = compare_pair(old, new, rel) + real = [h for h in r['hunks'] if h['kind'] == 'real'] + # signed_off: indices into the real hunks + notes = {tuple(real[k]['old_range']) + tuple(real[k]['new_range']): + ('ok', True, '') for k in signed_off} + out = _groups_html(old.split('\n'), new.split('\n'), r['hunks'], notes, + language) + return real, out, _CAPTION_RE.findall(out) + + def test_groups_inside_one_function_name_it_once(self): + new = _scoped_c([('step_a', 10, 20), ('step_b', 3, 4)]) + real, out, caps = self._render(self.OLD, new, 'f.c', 'c') + self.assertEqual(len(real), 2) + self.assertEqual(out.count('