tighten the multi-line comments

This commit is contained in:
Vaclav Tvrdik
2026-09-29 15:10:26 +02:00
parent d6f0ed3009
commit 413731df47
10 changed files with 63 additions and 90 deletions

View File

@@ -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]

View File

@@ -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

View File

@@ -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]

View File

@@ -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,): "")

View File

@@ -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.")]

View File

@@ -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")

View File

@@ -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])

View File

@@ -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

View File

@@ -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

View File

@@ -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