#!/usr/bin/env python3 """Rewrap over-long Zig comment lines at a column limit (default 100). Rules: - Only comment-only lines are touched; trailing comments after code are left alone. - Consecutive comment lines with the same indentation and marker (`//`, `///`, `//!`) form a block. Blank comment lines separate paragraphs within a block. - A line whose text starts with `- ` begins a bullet paragraph; its continuation lines are the ones indented to align under the bullet's text (bullet lead + 2). - Plain paragraphs join consecutive lines with the same text indentation; wrapped lines align where the first line's text begins. - A paragraph is rewrapped only if at least one of its lines exceeds the limit, so deliberate short line breaks elsewhere are preserved. """ import argparse import difflib import re import subprocess import sys import textwrap LIMIT = 100 COMMENT_RE = re.compile(r"^(\s*)(//[/!]?)(?:\s(.*))?$") BULLET_RE = re.compile(r"^(\s*)- (.*)$") def split_paragraphs(texts): """texts: list of comment text (None for a bare marker line). Returns paragraphs: dicts with lead/hang/bullet/texts/lines(indices).""" paragraphs = [] current = None for index, text in enumerate(texts): if text is None or text.strip() == "": paragraphs.append({"literal": True, "lines": [index]}) current = None continue lead = len(text) - len(text.lstrip(" ")) bullet = BULLET_RE.match(text) if bullet: current = { "lead": len(bullet.group(1)), "hang": len(bullet.group(1)) + 2, "bullet": True, "texts": [bullet.group(2)], "lines": [index], } paragraphs.append(current) elif current is not None and lead == current["hang"]: current["texts"].append(text) current["lines"].append(index) else: current = { "lead": lead, "hang": lead, "bullet": False, "texts": [text], "lines": [index], } paragraphs.append(current) return paragraphs def wrap_paragraph(paragraph, prefix): joined = re.sub(r"\s+", " ", " ".join(t.strip() for t in paragraph["texts"])) if paragraph["bullet"]: initial = prefix + " " * paragraph["lead"] + "- " else: initial = prefix + " " * paragraph["lead"] subsequent = prefix + " " * paragraph["hang"] return textwrap.wrap( joined, width=LIMIT, initial_indent=initial, subsequent_indent=subsequent, break_long_words=False, break_on_hyphens=False, ) def rewrap_block(original_lines, indent, marker, texts): prefix = indent + marker + " " output = [] for paragraph in split_paragraphs(texts): block_originals = [original_lines[i] for i in paragraph["lines"]] if paragraph.get("literal") or all(len(l) <= LIMIT for l in block_originals): output.extend(block_originals) else: output.extend(wrap_paragraph(paragraph, prefix)) return output def process(source): lines = source.split("\n") result = [] i = 0 while i < len(lines): match = COMMENT_RE.match(lines[i]) if not match: result.append(lines[i]) i += 1 continue indent, marker = match.group(1), match.group(2) block_lines, texts = [], [] while i < len(lines): m = COMMENT_RE.match(lines[i]) if not m or m.group(1) != indent or m.group(2) != marker: break block_lines.append(lines[i]) texts.append(m.group(3)) i += 1 result.extend(rewrap_block(block_lines, indent, marker, texts)) return "\n".join(result) def main(): parser = argparse.ArgumentParser() parser.add_argument("--write", action="store_true", help="apply changes (default: diff only)") parser.add_argument("files", nargs="*", help="files to process (default: git ls-files '*.zig')") args = parser.parse_args() files = args.files or subprocess.run( ["git", "ls-files", "*.zig"], capture_output=True, text=True, check=True ).stdout.split() changed = 0 for path in files: with open(path, encoding="utf-8") as f: source = f.read() rewrapped = process(source) if rewrapped == source: continue changed += 1 if args.write: with open(path, "w", encoding="utf-8") as f: f.write(rewrapped) print(f"rewrapped {path}") else: sys.stdout.writelines( difflib.unified_diff( source.splitlines(keepends=True), rewrapped.splitlines(keepends=True), fromfile=path, tofile=path, ) ) if not changed: print("no comments over the limit") if __name__ == "__main__": main()