tighten the multi-line comments
This commit is contained in:
4
main.py
4
main.py
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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,): "")
|
||||||
|
|||||||
@@ -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.")]
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user