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 """ 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 logging
import sys import sys
@@ -8,7 +8,7 @@ import sys
import sublime import sublime
# sublime reloads only this file: drop the submodules so an edited one is re-imported below. # 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")]: for _name in [n for n in sys.modules if n.startswith(__package__ + ".vdiff")]:
del sys.modules[_name] 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): def scheme_override_sandbox(tmp_path, monkeypatch):
"""Keep the colour-scheme override palette.py writes inside the test's own tmp_path """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 Both caches are per session and every stub view shares one id, so they would otherwise leak and
leak between tests and make every assertion about the override depend on test order. make override assertions depend on test order.
""" """
import sublime import sublime

View File

@@ -114,9 +114,8 @@ class Window:
return self.input_view return self.input_view
# a bundled scheme's styles: the diff.* and region.* scopes defined, the plugin's vdiff.* ones not. # a bundled scheme's styles: diff.* and region.* defined, the plugin's vdiff.* ones not. Nothing
# Nothing here simulates sublime merging in a written override, so a test that wants the plugin's # simulates sublime merging an override, so a test wanting those to resolve adds them itself.
# scopes to resolve has to say so by adding them.
DEFAULT_FG = "#ffffff" DEFAULT_FG = "#ffffff"
DEFAULT_STYLES = { DEFAULT_STYLES = {
"diff.deleted": {"foreground": DEFAULT_FG, "background": "#4a2b2b"}, "diff.deleted": {"foreground": DEFAULT_FG, "background": "#4a2b2b"},
@@ -246,8 +245,8 @@ class View:
def extract_tokens_with_scopes(self, region): 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 """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 Enough for the colouring tests: real sublime returns the syntax's tokens, but the painter only
painter needs is that adjacent tokens differ and each maps to a scope with a colour. needs adjacent tokens to differ and each to map to a coloured scope.
""" """
tokens = [] tokens = []
text = self.text[region.a:region.b] 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): def loading(monkeypatch):
"""Queue run_async instead of running it inline, so a test can act mid-load like a user can """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 The stub runs set_timeout synchronously, so the real async window — the one n / p read stale
n / p used to read stale state through - is invisible to every other test in the suite. state through — is invisible to every other test.
""" """
queue = [] queue = []
monkeypatch.setattr(commands, "run_async", lambda work, done: queue.append((work, done))) 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): def press(monkeypatch, action, before, path, after):
"""Run `action` on `path` of `before`; git then reports `after`. Returns the paths shown """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 ACTIONS captured git.stage and friends at build time, so the stub goes one level down at git.run,
one level down, at git.run; that keeps the real wiring of the action under test. keeping the action's real wiring under test.
""" """
shown = [] shown = []
monkeypatch.setattr(commands.git, "run", lambda repo, args, ok_codes=(0,): "") 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): def with_loaded_override(view):
"""Model a view whose scheme has picked up the override palette.py writes """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, The stub no more simulates that merge than sublime does before a restart, so a test says so itself.
so a test wanting the generated colour scopes to resolve has to say so.
""" """
view.override_loaded = True view.override_loaded = True
return view return view
@@ -210,8 +209,8 @@ def test_keymap_matches_help_table():
def test_no_binding_yields_to_vintage_command_mode(): 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 """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 init_view opens in command mode; a binding still carrying the old `command_mode: false` context
`command_mode: false` context would go dead there, which is what broke n / p. would go dead there, which is what broke n / p.
""" """
guard = {"key": "setting.command_mode", "operand": False} guard = {"key": "setting.command_mode", "operand": False}
for binding in keymap_bindings(): 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(): def test_minihtml_stylesheets_reject_unsupported_selectors():
"""minihtml drops the rest of a stylesheet at a selector it cannot parse; these are the forms """minihtml drops the rest of a stylesheet at an unparsable selector; these forms broke HELP_CSS"""
that silently broke HELP_CSS, so no stylesheet constant may use them"""
# descendant/child/sibling combinators, pseudo-classes, attribute selectors, compound classes # descendant/child/sibling combinators, pseudo-classes, attribute selectors, compound classes
unsupported = re.compile(r"[\s:>~+\[]|[.#][^.#]*[.#]") unsupported = re.compile(r"[\s:>~+\[]|[.#][^.#]*[.#]")
sheets = [(name, css) for name, css in vars(render).items() if name.endswith("_CSS") and isinstance(css, str)] 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"] keyword = view.styles["keyword"]["foreground"]
punctuation = view.styles["punctuation.definition"]["foreground"] punctuation = view.styles["punctuation.definition"]["foreground"]
# " return 10" is an inserted line: its words carry the keyword colour, its spaces and digits # " return 10" is inserted: its words carry the keyword colour, its spaces and digits their
# are their own runs. Without this layer every one of them would be the tint's flat foreground. # own runs. Without this layer all of them would be the tint's flat foreground.
inserted = token_regions(view, "ins", keyword) inserted = token_regions(view, "ins", keyword)
assert sublime.Region(view.text_point(4, 4), view.text_point(4, 10)) in inserted 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 # 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.init_view(view)
render.render(view, make_state()) render.render(view, make_state())
# sublime has not loaded the override yet, so the changed lines keep the scheme's flat tint and # the override is not loaded yet, so the lines keep the flat tint and no token key is painted:
# not one token key is painted: an unknown scope would draw its text in sublime's marker green # 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_DEL][1] == "diff.deleted"
assert view.regions[render.KEY_INS][1] == "diff.inserted" assert view.regions[render.KEY_INS][1] == "diff.inserted"
assert not [key for key in view.regions if key.startswith("vdiff.tok.")] 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") 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 # the annotation is drawn hard right of the subject, so past this budget the subject is clipped.
# subject would run into it, so it is clipped. The floor keeps a readable stub on a narrow panel. # The floor keeps a readable stub on a narrow panel.
COMMIT_ROW_WIDTH = 120 COMMIT_ROW_WIDTH = 120
MIN_SUBJECT_WIDTH = 24 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: 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 """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 `pick(files, index)` may name another file instead; `index` is the shown file's slot, new or vacated.
sits in the new list, or the slot it left behind when it is gone.
""" """
if message: if message:
status(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], def _git_action(verb: str, done_verb: str, operation: Callable[[str, FileChange], None],
confirm: Optional[Callable[[FileChange], bool]] = None, confirm: Optional[Callable[[FileChange], bool]] = None,
pick: Optional[Callable[[List[FileChange], int], Optional[FileChange]]] = None) -> Action: pick: Optional[Callable[[List[FileChange], int], Optional[FileChange]]] = None) -> Action:
"""Action running `operation(repo, change)` on the shown working-tree file, then re-listing """Run `operation(repo, change)` on the shown working-tree file, then re-list; `pick` goes to refresh_listing"""
`pick` is handed to refresh_listing to choose which file to show afterwards.
"""
def act(view: sublime.View, window: sublime.Window, st: ViewState) -> None: def act(view: sublime.View, window: sublime.Window, st: ViewState) -> None:
if st.in_commit or st.change is 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: 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 """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 The one-line panel offers only the subject; the body rides along verbatim, and nothing staged is a reword.
verbatim. Nothing staged is fine - then the amend is a pure reword.
""" """
if st.in_commit: if st.in_commit:
status("amend works on uncommitted changes only") 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: 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 """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 Working tree vs HEAD by default (staged and unstaged merged), a commit vs its first parent with
first parent when `sha` is given; the whole file as insertions when untracked. `sha`, the whole file as insertions when untracked.
""" """
common = ["--no-color", "--no-ext-diff", FULL_CONTEXT] common = ["--no-color", "--no-ext-diff", FULL_CONTEXT]
if untracked: if untracked:
@@ -291,8 +291,7 @@ def head_message(repo: str) -> str:
def amend(repo: str, message: str, allow_empty: bool = False) -> None: def amend(repo: str, message: str, allow_empty: bool = False) -> None:
"""Rewrite HEAD with `message`, folding in whatever is staged """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, `allow_empty` for a pure reword: git refuses an amend carrying no change, failing on an empty commit.
which would fail on a commit that happens to be empty.
""" """
run(repo, ["commit", "--amend"] + (["--allow-empty"] if allow_empty else []) + ["-m", message]) 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 """ 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 view.add_regions repaints text with the region scope's foreground, so one scope cannot give both a
give both a red/green background and the file's own syntax colours. To get both, this module generates red/green background and the file's syntax colours. This module generates a scope per (background
a scope per (background kind, syntax colour) pair carrying both properties, and render paints each run kind, colour) pair carrying both, and render paints each run of same-coloured tokens with it.
of same-coloured tokens with the matching one.
Sublime only picks up a colour-scheme override it has not seen before on a restart, so the palette is Sublime picks up a new override only on a restart, so the palette is probed and written in one pass
probed and written in one pass rather than growing a colour at a time. rather than a colour at a time.
""" """
import json import json
import logging import logging
@@ -17,9 +16,8 @@ import sublime
LOG = logging.getLogger("vdiff") LOG = logging.getLogger("vdiff")
# the full-line tint under the token layer. The scheme's own, not ours: the token layer repaints the # the full-line tint under the token layer, the scheme's own: the token layer repaints the text on
# text on top, so the tint's foreground never shows, and a scope the scheme already defines needs no # top so the tint's foreground never shows, and a scope the scheme defines needs no restart.
# provisioning and so no restart before the changed lines have their colours.
SCOPE_DEL = "diff.deleted" SCOPE_DEL = "diff.deleted"
SCOPE_INS = "diff.inserted" SCOPE_INS = "diff.inserted"
SCOPE_DEL_CHAR = "diff.deleted.char" 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_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 # scope roots to probe for the scheme's palette: schemes colour by these standard names, so one pass
# one pass over them yields the working set in one write. There is no API that returns a scheme's own # yields the working set. No API returns a scheme's palette — style_for_scope takes one scope at a
# palette: style_for_scope resolves one scope at a time, and reading the scheme file would mean # time, and reading the file would mean re-implementing sublime's override and .tmTheme merging.
# re-implementing sublime's merging of overrides and .tmTheme inheritance.
PALETTE_PROBES: Tuple[str, ...] = ( PALETTE_PROBES: Tuple[str, ...] = (
"source", "source",
"comment", "comment",
@@ -89,8 +86,7 @@ def reset() -> None:
def claim_restart_notice(view: sublime.View) -> bool: def claim_restart_notice(view: sublime.View) -> bool:
"""Whether the reader still has to be told to restart, asked once per scheme """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 True only when a restart helps — provisioned but not yet loaded; it cannot fix one we never wrote.
loaded it yet. A scheme we failed to provision at all is not something restarting fixes.
""" """
if not (_active and _palettes.get(_active)) or tokens_resolved(view) or _active in _notified: if not (_active and _palettes.get(_active)) or tokens_resolved(view) or _active in _notified:
return False return False
@@ -120,8 +116,7 @@ def tokens_resolved(view: sublime.View) -> bool:
"""Whether the generated (tint + colour) scopes are live """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 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 sublime's unknown-scope green. A new override loads only on a restart, so the writing session sees false.
only on a restart, so this stays false for the session that first wrote one.
""" """
colours = (_active and _palettes.get(_active)) or {} colours = (_active and _palettes.get(_active)) or {}
if not colours: if not colours:
@@ -154,8 +149,7 @@ def _scope_name(kind: str, index: int) -> str:
def ensure(view: sublime.View) -> None: def ensure(view: sublime.View) -> None:
"""Provision the scopes for the active scheme, once per scheme per session """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 Whether they have taken effect is tokens_resolved()'s question: a new override loads only on a restart.
sublime loads an override it has not seen before only on a restart.
""" """
global _active global _active
name = override_name(view) name = override_name(view)
@@ -167,8 +161,8 @@ def ensure(view: sublime.View) -> None:
scheme: Dict[str, Any] = {} scheme: Dict[str, Any] = {}
if os.path.exists(path): if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f:
# a scheme the user has hand-edited may hold comments decode_value rejects; the raise # a hand-edited scheme may hold comments decode_value rejects: the raise leaves the
# leaves the file untouched, and render keeps the changed lines visible meanwhile # file untouched, and render still shows the changed lines
scheme = sublime.decode_value(f.read()) or {} scheme = sublime.decode_value(f.read()) or {}
rules = scheme.setdefault("rules", []) rules = scheme.setdefault("rules", [])
tints = _tints(view) tints = _tints(view)
@@ -198,8 +192,7 @@ def _probe_palette(view: sublime.View) -> List[str]:
def _tints(view: sublime.View) -> Dict[str, 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 """{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 All or nothing per pair: taking only the half a scheme defines would put our hue next to its own.
otherwise get its line tint next to our char tint, in a different hue.
""" """
tints: Dict[str, str] = {} tints: Dict[str, str] = {}
for line_scope, char_scope in ((SCOPE_DEL, SCOPE_DEL_CHAR), (SCOPE_INS, SCOPE_INS_CHAR)): 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: view: sublime.View) -> bool:
"""Bring the scheme's rules up to date, in place; True when anything changed """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 add_regions draws text in the region scope's own foreground, so each (kind, colour) pair needs a
the text it covers in the region scope's own foreground: a background alone would flatten the rule carrying both; a background alone would flatten the line. Plain tints are written only when
changed line to the scheme's default. The plain tints are only written for a scheme that has none the scheme has none, and never overwritten, so a hand-tuned one survives.
of its own, and never overwritten, so one tuned by hand survives.
""" """
present = {rule.get("scope"): rule for rule in rules} present = {rule.get("scope"): rule for rule in rules}
changed = False 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" PHANTOM_KEY = "vdiff.header"
STATUS_KEY = "vdiff" STATUS_KEY = "vdiff"
# visual bell: a whole-buffer tint, erased on a timer. Deliberately not in REGION_KEYS, so the # visual bell: a whole-buffer tint on a timer. Not in REGION_KEYS, so a wrap's re-render cannot wipe it early.
# re-render a wrap triggers does not wipe it before it has been seen.
BELL_KEY = "vdiff.bell" BELL_KEY = "vdiff.bell"
BELL_MS = 90 BELL_MS = 90
# above this much changed text, skip per-token colouring: a diff that large is scrolled, not read, # above this much changed text the colours cost more than they are worth: such a diff is scrolled, not read
# and tokenising every changed line would cost more than the colours are worth
TOKEN_COLOUR_MAX_CHARS = 200000 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. # a block phantom at point 0 draws below row 0, so the file starts at row 1; only index_point / caret_index know
# Only index_point / caret_index know about it.
_HEADER_ROWS = 1 _HEADER_ROWS = 1
# gutter dots use the scope's foreground # 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 # 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" 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 making it a link. short: status-bar
# actions: the ACTIONS names the row covers; a single one makes the row a link. short: status-bar # wording, "" keeps the row out. The group flag is False for groups needing a working-tree file.
# wording, "" keeps the row out of it. The group flag is False for groups needing a working-tree file.
class Key(NamedTuple): class Key(NamedTuple):
"""One row of the status-bar hints and the ? popup""" """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)); } .body { color: color(var(--foreground) alpha(0.6)); }
""" """
# minihtml stops applying a stylesheet at the first selector it cannot parse, and its documentation # minihtml drops the rest of a stylesheet at a selector it cannot parse, and documents properties
# covers properties only, not selectors: every rule here sticks to one bare element or one class, # only, not selectors: every rule sticks to one bare element or class, the forms HEADER_CSS proves.
# the forms HEADER_CSS proves work. The first group's margin stands in for body's top padding, and # The first group's margin stands in for body's top padding; .kdim takes its colour from .dim.
# .kdim takes its colour from the .dim row around it.
HELP_CSS = """ HELP_CSS = """
body { margin: 0; padding: 0 0.8rem 0.6rem 0.8rem; } body { margin: 0; padding: 0 0.8rem 0.6rem 0.8rem; }
.group { margin-top: 0.6rem; color: var(--bluish); font-weight: bold; } .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("show_definitions", False)
s.set("mini_diff", False) s.set("mini_diff", False)
s.set("spell_check", False) s.set("spell_check", False)
# a read-only viewer, so vim's insert mode is meaningless: open in command mode with the block # a read-only viewer, so vim's insert mode is meaningless: command mode, block caret. vdiff's
# caret. vdiff's own keys carry no command_mode context, so they fire in either mode regardless. # own keys carry no command_mode context, so they fire in either mode regardless.
s.set("command_mode", True) s.set("command_mode", True)
s.set("inverse_caret_state", True) s.set("inverse_caret_state", True)
view.set_scratch(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) _apply_syntax(view, path if lines and not st.binary else None)
if lines and not st.binary: if lines and not st.binary:
# here rather than in init_view: a session restore runs that while sublime is still settling, # here rather than init_view: during a session restore the scheme colours still read as absent
# and the scheme colours we borrow read as absent that early
palette.ensure(view) palette.ensure(view)
_paint_regions(view, st) _paint_regions(view, st)
if palette.claim_restart_notice(view): 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: char_regions: Sequence[sublime.Region]) -> None:
"""Repaint each run of same-coloured tokens with a scope carrying the tint *and* that colour """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 add_regions draws text in the region scope's own foreground, so the tints above have just
just flattened every changed line to one colour. Nothing gives back the file's own highlighting flattened every changed line; only handing each run its own colour gives the highlighting back.
except handing each run the colour it should have had.
""" """
if not palette.tokens_resolved(view): if not palette.tokens_resolved(view):
return return

View File

@@ -34,10 +34,7 @@ def find_change(files: Sequence["FileChange"], path: Optional[str]) -> int:
@dataclass(eq=False) @dataclass(eq=False)
class ViewState: class ViewState:
"""Everything a review view needs to re-render itself and act on its file """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.
"""
repo: str repo: str
sha: Optional[str] = None # None in working-tree mode, the reviewed commit's hash otherwise 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: def index(self) -> int:
"""Position in `files` of the file being shown or loaded, or -1 """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 `requested` leads: a diff arrives asynchronously, so counting from `change` would make n / p
n / p step from whatever was on screen before the pending file landed. step from what was on screen before the pending file landed.
""" """
change = self.requested or self.change change = self.requested or self.change
return find_change(self.files, change.path) if change else -1 return find_change(self.files, change.path) if change else -1