diff --git a/main.py b/main.py index 367b9da..204cdc8 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ """ vDiff plugin entry point: review uncommitted changes and recent commits inside Sublime Text -Sublime reloads a plugin only when this file changes; after editing a submodule, save this file to reload everything. +Sublime reloads a plugin only when this file changes, so save it after editing a submodule. """ import logging import sys @@ -8,7 +8,7 @@ import sys import sublime # sublime reloads only this file: drop the submodules so an edited one is re-imported below. -# Open review views lose their in-memory state and are rebuilt from their settings by _restore_all. +# Open views lose in-memory state and are rebuilt from their settings by _restore_all. for _name in [n for n in sys.modules if n.startswith(__package__ + ".vdiff")]: del sys.modules[_name] diff --git a/tests/conftest.py b/tests/conftest.py index 6158916..ce025dc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,8 +13,8 @@ sys.path.insert(0, os.path.join(HERE, "stubs")) def scheme_override_sandbox(tmp_path, monkeypatch): """Keep the colour-scheme override palette.py writes inside the test's own tmp_path - Both caches are per session and the stub hands every view the same id, so they would otherwise - leak between tests and make every assertion about the override depend on test order. + Both caches are per session and every stub view shares one id, so they would otherwise leak and + make override assertions depend on test order. """ import sublime diff --git a/tests/stubs/sublime.py b/tests/stubs/sublime.py index 7cd1cdd..cd5898b 100644 --- a/tests/stubs/sublime.py +++ b/tests/stubs/sublime.py @@ -114,9 +114,8 @@ class Window: return self.input_view -# a bundled scheme's styles: the diff.* and region.* scopes defined, the plugin's vdiff.* ones not. -# Nothing here simulates sublime merging in a written override, so a test that wants the plugin's -# scopes to resolve has to say so by adding them. +# a bundled scheme's styles: diff.* and region.* defined, the plugin's vdiff.* ones not. Nothing +# simulates sublime merging an override, so a test wanting those to resolve adds them itself. DEFAULT_FG = "#ffffff" DEFAULT_STYLES = { "diff.deleted": {"foreground": DEFAULT_FG, "background": "#4a2b2b"}, @@ -246,8 +245,8 @@ class View: def extract_tokens_with_scopes(self, region): """One token per run of word characters and one per run of anything else, scoped by its shape - Enough shape for the colouring tests: real sublime returns the syntax's tokens, and all the - painter needs is that adjacent tokens differ and each maps to a scope with a colour. + Enough for the colouring tests: real sublime returns the syntax's tokens, but the painter only + needs adjacent tokens to differ and each to map to a coloured scope. """ tokens = [] text = self.text[region.a:region.b] diff --git a/tests/test_commands.py b/tests/test_commands.py index 39d1519..c7c1008 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -329,8 +329,8 @@ def test_commit_items_fall_back_for_an_empty_subject(): 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. + The stub runs set_timeout synchronously, so the real async window — the one n / p read stale + state through — is invisible to every other test. """ queue = [] monkeypatch.setattr(commands, "run_async", lambda work, done: queue.append((work, done))) @@ -522,8 +522,8 @@ def test_a_file_without_changes_neither_moves_nor_rings(rings): def press(monkeypatch, action, before, path, after): """Run `action` on `path` of `before`; git then reports `after`. Returns the paths shown - ACTIONS captured git.stage and friends when the table was built, so the subprocess is stubbed - one level down, at git.run; that keeps the real wiring of the action under test. + ACTIONS captured git.stage and friends at build time, so the stub goes one level down at git.run, + keeping the action's real wiring under test. """ shown = [] monkeypatch.setattr(commands.git, "run", lambda repo, args, ok_codes=(0,): "") diff --git a/tests/test_render.py b/tests/test_render.py index 209d3e5..813f18d 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -37,8 +37,7 @@ def header(view): def with_loaded_override(view): """Model a view whose scheme has picked up the override palette.py writes - The stub deliberately does not simulate that merge, just as sublime does not until it restarts, - so a test wanting the generated colour scopes to resolve has to say so. + The stub no more simulates that merge than sublime does before a restart, so a test says so itself. """ view.override_loaded = True return view @@ -210,8 +209,8 @@ def test_keymap_matches_help_table(): 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 - 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. + init_view opens in command mode; a binding still carrying 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(): @@ -222,8 +221,7 @@ def test_no_binding_yields_to_vintage_command_mode(): def test_minihtml_stylesheets_reject_unsupported_selectors(): - """minihtml drops the rest of a stylesheet at a selector it cannot parse; these are the forms - that silently broke HELP_CSS, so no stylesheet constant may use them""" + """minihtml drops the rest of a stylesheet at an unparsable selector; these forms broke HELP_CSS""" # descendant/child/sibling combinators, pseudo-classes, attribute selectors, compound classes unsupported = re.compile(r"[\s:>~+\[]|[.#][^.#]*[.#]") sheets = [(name, css) for name, css in vars(render).items() if name.endswith("_CSS") and isinstance(css, str)] @@ -351,8 +349,8 @@ def test_changed_lines_are_repainted_in_the_files_own_colours(): keyword = view.styles["keyword"]["foreground"] punctuation = view.styles["punctuation.definition"]["foreground"] - # " return 10" is an inserted line: its words carry the keyword colour, its spaces and digits - # are their own runs. Without this layer every one of them would be the tint's flat foreground. + # " return 10" is inserted: its words carry the keyword colour, its spaces and digits their + # own runs. Without this layer all of them would be the tint's flat foreground. inserted = token_regions(view, "ins", keyword) assert sublime.Region(view.text_point(4, 4), view.text_point(4, 10)) in inserted # and a deleted line is painted from the deleted palette, not the inserted one @@ -381,8 +379,8 @@ def test_token_layer_waits_for_the_scopes_to_resolve(): render.init_view(view) render.render(view, make_state()) - # sublime has not loaded the override yet, so the changed lines keep the scheme's flat tint and - # not one token key is painted: an unknown scope would draw its text in sublime's marker green + # the override is not loaded yet, so the lines keep the flat tint and no token key is painted: + # an unknown scope would draw its text in sublime's marker green assert view.regions[render.KEY_DEL][1] == "diff.deleted" assert view.regions[render.KEY_INS][1] == "diff.inserted" assert not [key for key in view.regions if key.startswith("vdiff.tok.")] diff --git a/vdiff/commands.py b/vdiff/commands.py index d09107f..08c7bc5 100644 --- a/vdiff/commands.py +++ b/vdiff/commands.py @@ -27,8 +27,8 @@ STATUS_COLOURS: Dict[str, int] = { } COMMIT_KIND = (sublime.KIND_ID_COLOR_BLUISH, "c", "commit") -# a quick-panel row draws the annotation hard right on the subject's line; past this budget the -# subject would run into it, so it is clipped. The floor keeps a readable stub on a narrow panel. +# the annotation is drawn hard right of the subject, so past this budget the subject is clipped. +# The floor keeps a readable stub on a narrow panel. COMMIT_ROW_WIDTH = 120 MIN_SUBJECT_WIDTH = 24 @@ -193,8 +193,7 @@ def refresh_listing(view: sublime.View, st: ViewState, message: Optional[str] = pick: Optional[Callable[[List[FileChange], int], Optional[FileChange]]] = None) -> None: """Re-list the working tree changes and show the same file, or the next one when it is gone - `pick(files, index)` may name another file to show instead; `index` is where the shown file - sits in the new list, or the slot it left behind when it is gone. + `pick(files, index)` may name another file instead; `index` is the shown file's slot, new or vacated. """ if message: status(message) @@ -330,10 +329,7 @@ def _act_help(view: sublime.View, window: sublime.Window, st: ViewState) -> None def _git_action(verb: str, done_verb: str, operation: Callable[[str, FileChange], None], confirm: Optional[Callable[[FileChange], bool]] = None, pick: Optional[Callable[[List[FileChange], int], Optional[FileChange]]] = None) -> Action: - """Action running `operation(repo, change)` on the shown working-tree file, then re-listing - - `pick` is handed to refresh_listing to choose which file to show afterwards. - """ + """Run `operation(repo, change)` on the shown working-tree file, then re-list; `pick` goes to refresh_listing""" def act(view: sublime.View, window: sublime.Window, st: ViewState) -> None: if st.in_commit or st.change is None: @@ -468,8 +464,7 @@ def _act_commit(view: sublime.View, window: sublime.Window, st: ViewState) -> No def _act_amend(view: sublime.View, window: sublime.Window, st: ViewState) -> None: """Rewrite HEAD's message in an input panel, folding in whatever is staged; Esc there cancels - The panel is one line, so only the subject is offered; the rest of the message rides along - verbatim. Nothing staged is fine - then the amend is a pure reword. + The one-line panel offers only the subject; the body rides along verbatim, and nothing staged is a reword. """ if st.in_commit: status("amend works on uncommitted changes only") diff --git a/vdiff/git.py b/vdiff/git.py index a350930..80d649c 100644 --- a/vdiff/git.py +++ b/vdiff/git.py @@ -262,8 +262,8 @@ def _in_tree(repo: str, ref: str, path: str) -> bool: def full_diff(repo: str, path: str, sha: Optional[str] = None, untracked: bool = False) -> str: """Unified diff of one file with the whole file as context - Working tree vs HEAD (staged and unstaged merged) by default; a commit vs its - first parent when `sha` is given; the whole file as insertions when untracked. + Working tree vs HEAD by default (staged and unstaged merged), a commit vs its first parent with + `sha`, the whole file as insertions when untracked. """ common = ["--no-color", "--no-ext-diff", FULL_CONTEXT] if untracked: @@ -291,8 +291,7 @@ def head_message(repo: str) -> str: def amend(repo: str, message: str, allow_empty: bool = False) -> None: """Rewrite HEAD with `message`, folding in whatever is staged - `allow_empty` for a pure reword: git otherwise refuses an amend whose result carries no change, - which would fail on a commit that happens to be empty. + `allow_empty` for a pure reword: git refuses an amend carrying no change, failing on an empty commit. """ run(repo, ["commit", "--amend"] + (["--allow-empty"] if allow_empty else []) + ["-m", message]) diff --git a/vdiff/palette.py b/vdiff/palette.py index 1e35bc5..e6dff33 100644 --- a/vdiff/palette.py +++ b/vdiff/palette.py @@ -1,12 +1,11 @@ """ The colour-scheme scopes vDiff paints changed lines with, provisioned into a scheme override -view.add_regions repaints the text it covers with the region scope's foreground, so one scope cannot -give both a red/green background and the file's own syntax colours. To get both, this module generates -a scope per (background kind, syntax colour) pair carrying both properties, and render paints each run -of same-coloured tokens with the matching one. +view.add_regions repaints text with the region scope's foreground, so one scope cannot give both a +red/green background and the file's syntax colours. This module generates a scope per (background +kind, colour) pair carrying both, and render paints each run of same-coloured tokens with it. -Sublime only picks up a colour-scheme override it has not seen before on a restart, so the palette is -probed and written in one pass rather than growing a colour at a time. +Sublime picks up a new override only on a restart, so the palette is probed and written in one pass +rather than a colour at a time. """ import json import logging @@ -17,9 +16,8 @@ import sublime LOG = logging.getLogger("vdiff") -# the full-line tint under the token layer. The scheme's own, not ours: the token layer repaints the -# text on top, so the tint's foreground never shows, and a scope the scheme already defines needs no -# provisioning and so no restart before the changed lines have their colours. +# the full-line tint under the token layer, the scheme's own: the token layer repaints the text on +# top so the tint's foreground never shows, and a scope the scheme defines needs no restart. SCOPE_DEL = "diff.deleted" SCOPE_INS = "diff.inserted" SCOPE_DEL_CHAR = "diff.deleted.char" @@ -37,10 +35,9 @@ FALLBACK_TINTS: Dict[str, str] = { SCOPE_INS_CHAR: "hsla(114, 45%, 68%, 0.30)", } -# scope roots to resolve for the scheme's palette. Colour schemes colour by these standard names, so -# one pass over them yields the working set in one write. There is no API that returns a scheme's own -# palette: style_for_scope resolves one scope at a time, and reading the scheme file would mean -# re-implementing sublime's merging of overrides and .tmTheme inheritance. +# scope roots to probe for the scheme's palette: schemes colour by these standard names, so one pass +# yields the working set. No API returns a scheme's palette — style_for_scope takes one scope at a +# time, and reading the file would mean re-implementing sublime's override and .tmTheme merging. PALETTE_PROBES: Tuple[str, ...] = ( "source", "comment", @@ -89,8 +86,7 @@ def reset() -> None: def claim_restart_notice(view: sublime.View) -> bool: """Whether the reader still has to be told to restart, asked once per scheme - True only when a restart would actually help: the palette is provisioned but sublime has not - loaded it yet. A scheme we failed to provision at all is not something restarting fixes. + True only when a restart helps — provisioned but not yet loaded; it cannot fix one we never wrote. """ if not (_active and _palettes.get(_active)) or tokens_resolved(view) or _active in _notified: return False @@ -120,8 +116,7 @@ def tokens_resolved(view: sublime.View) -> bool: """Whether the generated (tint + colour) scopes are live The token layer must never paint with a scope sublime does not know: it would draw that text in - the bright green it uses for an unknown scope. Sublime loads an override it has not seen before - only on a restart, so this stays false for the session that first wrote one. + sublime's unknown-scope green. A new override loads only on a restart, so the writing session sees false. """ colours = (_active and _palettes.get(_active)) or {} if not colours: @@ -154,8 +149,7 @@ def _scope_name(kind: str, index: int) -> str: def ensure(view: sublime.View) -> None: """Provision the scopes for the active scheme, once per scheme per session - Whether the rules have taken effect is a separate question, asked by tokens_resolved(), because - sublime loads an override it has not seen before only on a restart. + Whether they have taken effect is tokens_resolved()'s question: a new override loads only on a restart. """ global _active name = override_name(view) @@ -167,8 +161,8 @@ def ensure(view: sublime.View) -> None: scheme: Dict[str, Any] = {} if os.path.exists(path): with open(path, "r", encoding="utf-8") as f: - # a scheme the user has hand-edited may hold comments decode_value rejects; the raise - # leaves the file untouched, and render keeps the changed lines visible meanwhile + # a hand-edited scheme may hold comments decode_value rejects: the raise leaves the + # file untouched, and render still shows the changed lines scheme = sublime.decode_value(f.read()) or {} rules = scheme.setdefault("rules", []) tints = _tints(view) @@ -198,8 +192,7 @@ def _probe_palette(view: sublime.View) -> List[str]: def _tints(view: sublime.View) -> Dict[str, str]: """{tint scope: background} for the scheme's diff colours, falling back to ours per colour pair - All or nothing per pair: a scheme that defines diff.deleted but not diff.deleted.char would - otherwise get its line tint next to our char tint, in a different hue. + All or nothing per pair: taking only the half a scheme defines would put our hue next to its own. """ tints: Dict[str, str] = {} for line_scope, char_scope in ((SCOPE_DEL, SCOPE_DEL_CHAR), (SCOPE_INS, SCOPE_INS_CHAR)): @@ -214,10 +207,9 @@ def _merge_rules(rules: List[Dict[str, str]], tints: Dict[str, str], colours: Di view: sublime.View) -> bool: """Bring the scheme's rules up to date, in place; True when anything changed - Each (background kind, palette colour) pair needs a rule carrying both, because add_regions draws - the text it covers in the region scope's own foreground: a background alone would flatten the - changed line to the scheme's default. The plain tints are only written for a scheme that has none - of its own, and never overwritten, so one tuned by hand survives. + add_regions draws text in the region scope's own foreground, so each (kind, colour) pair needs a + rule carrying both; a background alone would flatten the line. Plain tints are written only when + the scheme has none, and never overwritten, so a hand-tuned one survives. """ present = {rule.get("scope"): rule for rule in rules} changed = False diff --git a/vdiff/render.py b/vdiff/render.py index 11a0f53..306c365 100644 --- a/vdiff/render.py +++ b/vdiff/render.py @@ -27,17 +27,14 @@ REGION_KEYS: Tuple[str, ...] = (KEY_DEL, KEY_INS, KEY_DEL_GUTTER, KEY_INS_GUTTER PHANTOM_KEY = "vdiff.header" STATUS_KEY = "vdiff" -# visual bell: a whole-buffer tint, erased on a timer. Deliberately not in REGION_KEYS, so the -# re-render a wrap triggers does not wipe it before it has been seen. +# visual bell: a whole-buffer tint on a timer. Not in REGION_KEYS, so a wrap's re-render cannot wipe it early. BELL_KEY = "vdiff.bell" BELL_MS = 90 -# above this much changed text, skip per-token colouring: a diff that large is scrolled, not read, -# and tokenising every changed line would cost more than the colours are worth +# above this much changed text the colours cost more than they are worth: such a diff is scrolled, not read TOKEN_COLOUR_MAX_CHARS = 200000 -# a block phantom anchored at point 0 is drawn below row 0, so row 0 stays empty and the file starts at row 1. -# Only index_point / caret_index know about it. +# a block phantom at point 0 draws below row 0, so the file starts at row 1; only index_point / caret_index know _HEADER_ROWS = 1 # gutter dots use the scope's foreground @@ -49,9 +46,8 @@ SCOPE_BELL = "region.yellowish" # sublime only loads an override it has not seen before on a restart NEEDS_RESTART = "restart Sublime Text to finish enabling syntax highlighting on changed lines" -# one source for the status-bar hints and the ? popup. -# actions: the ACTIONS names the row covers; a single one makes the row a link. short: status-bar -# wording, "" keeps the row out of it. The group flag is False for groups needing a working-tree file. +# actions: the ACTIONS names the row covers, a single one making it a link. short: status-bar +# wording, "" keeps the row out. The group flag is False for groups needing a working-tree file. class Key(NamedTuple): """One row of the status-bar hints and the ? popup""" @@ -127,10 +123,9 @@ a { text-decoration: none; padding: 0.15rem 0.5rem; margin-left: 0.3rem; border- .body { color: color(var(--foreground) alpha(0.6)); } """ -# minihtml stops applying a stylesheet at the first selector it cannot parse, and its documentation -# covers properties only, not selectors: every rule here sticks to one bare element or one class, -# the forms HEADER_CSS proves work. The first group's margin stands in for body's top padding, and -# .kdim takes its colour from the .dim row around it. +# minihtml drops the rest of a stylesheet at a selector it cannot parse, and documents properties +# only, not selectors: every rule sticks to one bare element or class, the forms HEADER_CSS proves. +# The first group's margin stands in for body's top padding; .kdim takes its colour from .dim. HELP_CSS = """ body { margin: 0; padding: 0 0.8rem 0.6rem 0.8rem; } .group { margin-top: 0.6rem; color: var(--bluish); font-weight: bold; } @@ -165,8 +160,8 @@ def init_view(view: sublime.View) -> None: s.set("show_definitions", False) s.set("mini_diff", False) s.set("spell_check", False) - # a read-only viewer, so vim's insert mode is meaningless: open in command mode with the block - # caret. vdiff's own keys carry no command_mode context, so they fire in either mode regardless. + # a read-only viewer, so vim's insert mode is meaningless: command mode, block caret. vdiff's + # own keys carry no command_mode context, so they fire in either mode regardless. s.set("command_mode", True) s.set("inverse_caret_state", True) view.set_scratch(True) @@ -246,8 +241,7 @@ def render(view: sublime.View, st: ViewState) -> None: _apply_syntax(view, path if lines and not st.binary else None) if lines and not st.binary: - # here rather than in init_view: a session restore runs that while sublime is still settling, - # and the scheme colours we borrow read as absent that early + # here rather than init_view: during a session restore the scheme colours still read as absent palette.ensure(view) _paint_regions(view, st) if palette.claim_restart_notice(view): @@ -285,9 +279,8 @@ def _paint_tokens(view: sublime.View, lines: Sequence[Line], starts: Sequence[in char_regions: Sequence[sublime.Region]) -> None: """Repaint each run of same-coloured tokens with a scope carrying the tint *and* that colour - add_regions draws the text it covers in the region scope's own foreground, so the tints above have - just flattened every changed line to one colour. Nothing gives back the file's own highlighting - except handing each run the colour it should have had. + add_regions draws text in the region scope's own foreground, so the tints above have just + flattened every changed line; only handing each run its own colour gives the highlighting back. """ if not palette.tokens_resolved(view): return diff --git a/vdiff/state.py b/vdiff/state.py index 2ae613b..c6f3a39 100644 --- a/vdiff/state.py +++ b/vdiff/state.py @@ -34,10 +34,7 @@ def find_change(files: Sequence["FileChange"], path: Optional[str]) -> int: @dataclass(eq=False) class ViewState: - """Everything a review view needs to re-render itself and act on its file - - `sha` is None in working-tree mode and the reviewed commit's hash otherwise. - """ + """Everything a review view needs to re-render itself and act on its file""" repo: str sha: Optional[str] = None # None in working-tree mode, the reviewed commit's hash otherwise @@ -61,8 +58,8 @@ class ViewState: def index(self) -> int: """Position in `files` of the file being shown or loaded, or -1 - `requested` leads: a diff arrives asynchronously, so counting from `change` alone would make - n / p step from whatever was on screen before the pending file landed. + `requested` leads: a diff arrives asynchronously, so counting from `change` would make n / p + step from what was on screen before the pending file landed. """ change = self.requested or self.change return find_change(self.files, change.path) if change else -1