From 59501b48b5a5173fbc83617feca277e82f0fb432 Mon Sep 17 00:00:00 2001 From: longvo920 Date: Sun, 13 Sep 2026 20:00:42 +0700 Subject: [PATCH 1/2] feat: show a function name once per run of changes in the report The report printed the enclosing function above every change group, so a function with several changes repeated its name over each table. The name now appears only where the scope changes. The Reviewed badge can hide the group carrying the name, so the next visible group shows it in that state instead. Docs follow, dropping a stale mention of the removed Affected list. --- compare_tool/report.py | 30 +++++++++++++-- docs/architecture.md | 9 ++--- docs/usage.md | 2 + docs/vi/architecture.md | 8 ++-- docs/vi/usage.md | 4 ++ tests/test_report.py | 83 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 123 insertions(+), 13 deletions(-) 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('