Compare commits
2 Commits
904b941be7
...
f18bad79cd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f18bad79cd | ||
|
|
2d9db745b0 |
@@ -66,7 +66,7 @@ Use one method or the other, not both: an unpacked `Packages/vDiff` directory ov
|
|||||||
| `?` | show this key list in a popup |
|
| `?` | show this key list in a popup |
|
||||||
| Esc | close the view |
|
| Esc | close the view |
|
||||||
|
|
||||||
The header links do the same with the mouse. The all-files view shows five context lines around each change by default; configure this with `all_files_context` in `vDiff.sublime-settings`.
|
The header links do the same with the mouse. The all-files view shows five context lines around each change by default; configure this with `all_files_context` in `vDiff.sublime-settings`. The same file sets the background of deleted and inserted lines (`deleted_line_background`, `inserted_line_background`) and the colours of the all-files line-number column (`line_numbers_foreground`, `line_numbers_background`); left empty, they follow the colour scheme. The all-files file heading takes its colours from `file_header_background`, `file_header_deleted_foreground` and `file_header_inserted_foreground`.
|
||||||
|
|
||||||
## The channel
|
## The channel
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import sublime # the stub, via conftest
|
import sublime # the stub, via conftest
|
||||||
@@ -92,6 +94,64 @@ def test_enter_skips_refetch_when_preview_is_current(monkeypatch):
|
|||||||
assert shown == ["b.py", "a.py"]
|
assert shown == ["b.py", "a.py"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mode, all_mode", [("file_by_file", False), ("all_in_one", True), (None, False)])
|
||||||
|
def test_a_new_review_view_starts_in_the_default_view_mode(monkeypatch, mode, all_mode):
|
||||||
|
option(monkeypatch, "default_view_mode", mode)
|
||||||
|
monkeypatch.setattr(commands, "resolve_repo", lambda window: "/repo")
|
||||||
|
monkeypatch.setattr(commands, "find_review_view", lambda window: sublime.View())
|
||||||
|
_, st = commands.open_review(sublime.Window())
|
||||||
|
assert st.all_mode is all_mode
|
||||||
|
|
||||||
|
|
||||||
|
def test_panel_in_all_in_one_mode_scrolls_instead_of_loading_files(monkeypatch):
|
||||||
|
loads, focused = [], []
|
||||||
|
|
||||||
|
def fake_show_all(view, st, focus=None):
|
||||||
|
# load at once: what show_all's done does once the diffs land
|
||||||
|
st.token += 1
|
||||||
|
st.rendered_token = st.token
|
||||||
|
loads.append(st.change.path)
|
||||||
|
|
||||||
|
monkeypatch.setattr(commands, "show_all", fake_show_all)
|
||||||
|
monkeypatch.setattr(commands, "show_file", lambda view, st, c: pytest.fail("loaded a single file"))
|
||||||
|
monkeypatch.setattr(commands.render, "render_all", lambda view, st, focus=None: focused.append(focus))
|
||||||
|
st = state.ViewState("/repo", all_mode=True)
|
||||||
|
st.change = change("kept.py")
|
||||||
|
st.files = [st.change]
|
||||||
|
files = [change("a.py"), change("b.py")]
|
||||||
|
st.collapsed = {"b.py"}
|
||||||
|
window = sublime.Window()
|
||||||
|
commands.show_files_panel(window, sublime.View(), st, files, st.kept())
|
||||||
|
_, on_select, _, on_highlight, _ = window.panel
|
||||||
|
assert loads == ["a.py"]
|
||||||
|
|
||||||
|
# a highlight unfolds and scrolls to its file; Enter on it needs no redraw
|
||||||
|
on_highlight(1)
|
||||||
|
assert focused == ["b.py"] and st.change.path == "b.py" and "b.py" not in st.collapsed
|
||||||
|
on_select(1)
|
||||||
|
assert focused == ["b.py"] and loads == ["a.py"]
|
||||||
|
|
||||||
|
# Esc goes back to the kept file, still in the all-in-one view
|
||||||
|
on_select(-1)
|
||||||
|
assert st.all_mode and st.change.path == "kept.py" and loads == ["a.py", "kept.py"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_esc_goes_back_to_the_kept_view_mode(monkeypatch):
|
||||||
|
calls = []
|
||||||
|
monkeypatch.setattr(commands, "show_file", lambda view, st, c: calls.append(("file", c.path)))
|
||||||
|
monkeypatch.setattr(commands, "show_all", lambda view, st, focus=None: calls.append(("all", st.change.path)))
|
||||||
|
st = state.ViewState("/repo")
|
||||||
|
st.change = change("kept.py")
|
||||||
|
st.files = [st.change]
|
||||||
|
kept = st.kept()
|
||||||
|
# the view switched to all-in-one after the snapshot; Esc still restores file-by-file
|
||||||
|
st.all_mode = True
|
||||||
|
window = sublime.Window()
|
||||||
|
commands.show_files_panel(window, sublime.View(), st, [change("a.py")], kept)
|
||||||
|
window.panel[1](-1)
|
||||||
|
assert calls[-1] == ("file", "kept.py") and not st.all_mode
|
||||||
|
|
||||||
|
|
||||||
def test_on_cancel_goes_back_to_commit_list(monkeypatch):
|
def test_on_cancel_goes_back_to_commit_list(monkeypatch):
|
||||||
monkeypatch.setattr(commands, "show_file", lambda view, st, c: None)
|
monkeypatch.setattr(commands, "show_file", lambda view, st, c: None)
|
||||||
st = state.ViewState("/repo")
|
st = state.ViewState("/repo")
|
||||||
@@ -599,6 +659,45 @@ def test_stage_all_and_unstage_all_need_something_to_do(monkeypatch):
|
|||||||
assert messages[-1] == "stage all works on uncommitted changes only"
|
assert messages[-1] == "stage all works on uncommitted changes only"
|
||||||
|
|
||||||
|
|
||||||
|
def watched(monkeypatch, tmp_path, all_mode):
|
||||||
|
"""A working-tree view of a.py and b.py on disk with its fingerprint taken; returns (st, refreshes)"""
|
||||||
|
for name in ("a.py", "b.py", "index"):
|
||||||
|
(tmp_path / name).write_text(name)
|
||||||
|
monkeypatch.setattr(commands.git, "index_path", lambda repo: str(tmp_path / "index"))
|
||||||
|
refreshes = []
|
||||||
|
monkeypatch.setattr(commands, "refresh_listing", lambda view, st: refreshes.append(st))
|
||||||
|
st = state.ViewState(str(tmp_path))
|
||||||
|
st.files = [change("a.py"), change("b.py")]
|
||||||
|
st.change, st.all_mode = st.files[0], all_mode
|
||||||
|
st.fingerprint = commands.render.fingerprint(st)
|
||||||
|
return st, refreshes
|
||||||
|
|
||||||
|
|
||||||
|
def touch(path):
|
||||||
|
"""Push the file's mtime forward, as an edit would"""
|
||||||
|
stat = path.stat()
|
||||||
|
os.utime(path, (stat.st_atime, stat.st_mtime + 5))
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_if_changed_watches_every_file_of_the_all_files_view(monkeypatch, tmp_path):
|
||||||
|
st, refreshes = watched(monkeypatch, tmp_path, all_mode=True)
|
||||||
|
commands.refresh_if_changed(sublime.View(), st)
|
||||||
|
assert refreshes == []
|
||||||
|
touch(tmp_path / "b.py") # not the file under the caret
|
||||||
|
commands.refresh_if_changed(sublime.View(), st)
|
||||||
|
assert refreshes == [st]
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_if_changed_watches_the_shown_file_and_the_index(monkeypatch, tmp_path):
|
||||||
|
st, refreshes = watched(monkeypatch, tmp_path, all_mode=False)
|
||||||
|
touch(tmp_path / "b.py") # another file: the single-file view does not show it
|
||||||
|
commands.refresh_if_changed(sublime.View(), st)
|
||||||
|
assert refreshes == []
|
||||||
|
touch(tmp_path / "index")
|
||||||
|
commands.refresh_if_changed(sublime.View(), st)
|
||||||
|
assert refreshes == [st]
|
||||||
|
|
||||||
|
|
||||||
def test_staging_the_only_file_leaves_an_empty_listing(monkeypatch):
|
def test_staging_the_only_file_leaves_an_empty_listing(monkeypatch):
|
||||||
messages = []
|
messages = []
|
||||||
monkeypatch.setattr(commands.render, "render_message", lambda view, st, text: messages.append(text))
|
monkeypatch.setattr(commands.render, "render_message", lambda view, st, text: messages.append(text))
|
||||||
|
|||||||
@@ -97,6 +97,16 @@ def test_stage_all_unstage_all(repo):
|
|||||||
assert changes["sub/n.py"].status == "?"
|
assert changes["sub/n.py"].status == "?"
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_path_finds_a_worktrees_index(repo, tmp_path_factory):
|
||||||
|
assert git.index_path(repo) == os.path.join(repo, ".git", "index")
|
||||||
|
# a worktree's .git is a file pointing into the main repository
|
||||||
|
tree = str(tmp_path_factory.mktemp("tree") / "wt")
|
||||||
|
sh(repo, "worktree", "add", "-q", tree)
|
||||||
|
index = git.index_path(tree)
|
||||||
|
assert os.path.isfile(os.path.join(tree, ".git")) and os.path.isfile(index)
|
||||||
|
assert os.path.realpath(index).startswith(os.path.realpath(os.path.join(repo, ".git", "worktrees")))
|
||||||
|
|
||||||
|
|
||||||
def test_commits(repo):
|
def test_commits(repo):
|
||||||
write(repo, "a.py", "def f():\n return 10\n\n\ndef g():\n return 2\n")
|
write(repo, "a.py", "def f():\n return 10\n\n\ndef g():\n return 2\n")
|
||||||
write(repo, "c.py", "c\n")
|
write(repo, "c.py", "c\n")
|
||||||
|
|||||||
@@ -84,3 +84,66 @@ def test_a_hand_edited_override_is_never_clobbered(tmp_path):
|
|||||||
assert "// mine" in path.read_text()
|
assert "// mine" in path.read_text()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def colour_option(monkeypatch, name, value):
|
||||||
|
"""Set one of the colour settings for this test"""
|
||||||
|
from vdiff import utils
|
||||||
|
monkeypatch.setitem(sublime.load_settings(utils.SETTINGS_FILE), name, value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_line_colours_get_rules_and_tint_the_token_layer(tmp_path, monkeypatch):
|
||||||
|
colour_option(monkeypatch, "deleted_line_background", "#ff000040")
|
||||||
|
colour_option(monkeypatch, "line_numbers_foreground", "#888888")
|
||||||
|
view = sublime.View()
|
||||||
|
palette.ensure(view)
|
||||||
|
|
||||||
|
rules = by_scope(tmp_path)
|
||||||
|
assert rules[palette.SCOPE_LINE_DEL] == {"scope": palette.SCOPE_LINE_DEL, "background": "#ff000040"}
|
||||||
|
assert palette.SCOPE_LINE_INS not in rules
|
||||||
|
# the token layer repaints deleted text, so it carries the same background
|
||||||
|
colour = view.styles["comment"]["foreground"]
|
||||||
|
assert rules[palette.scope_for("del", colour)]["background"] == "#ff000040"
|
||||||
|
# a foreground alone still gets a background, or sublime would fill the region with the foreground
|
||||||
|
numbers = rules[palette.SCOPE_LINE_NUMBERS]
|
||||||
|
assert numbers["foreground"] == "#888888" and numbers["background"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_changing_a_colour_setting_rewrites_the_override(tmp_path, monkeypatch):
|
||||||
|
view = sublime.View()
|
||||||
|
palette.ensure(view)
|
||||||
|
assert palette.SCOPE_LINE_INS not in by_scope(tmp_path)
|
||||||
|
colour_option(monkeypatch, "inserted_line_background", "#00ff0040")
|
||||||
|
palette.ensure(view)
|
||||||
|
assert by_scope(tmp_path)[palette.SCOPE_LINE_INS]["background"] == "#00ff0040"
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_uses_the_user_scopes_only_once_set_and_live(monkeypatch):
|
||||||
|
view = sublime.View()
|
||||||
|
# unset: the scheme's diff tint and the plain-text line numbers, as before
|
||||||
|
assert palette.line_scope(view, "del") == palette.SCOPE_DEL
|
||||||
|
assert palette.line_numbers_scope(view) == "text.plain"
|
||||||
|
colour_option(monkeypatch, "deleted_line_background", "#ff000040")
|
||||||
|
colour_option(monkeypatch, "line_numbers_background", "#222222")
|
||||||
|
# set, but the override is not loaded yet: an unknown scope would paint nothing
|
||||||
|
assert palette.line_scope(view, "del") == palette.SCOPE_DEL
|
||||||
|
view.override_loaded = True
|
||||||
|
assert palette.line_scope(view, "del") == palette.SCOPE_LINE_DEL
|
||||||
|
assert palette.line_scope(view, "ins") == palette.SCOPE_INS
|
||||||
|
assert palette.line_numbers_scope(view) == palette.SCOPE_LINE_NUMBERS
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_header_colours_come_from_settings(tmp_path, monkeypatch):
|
||||||
|
palette.ensure(sublime.View())
|
||||||
|
rules = by_scope(tmp_path)
|
||||||
|
# unset: the built-in defaults
|
||||||
|
assert rules[palette.SCOPE_FILE_HEADER]["background"] == palette.FILE_HEADER_BACKGROUND
|
||||||
|
colour_option(monkeypatch, "file_header_background", "#202040")
|
||||||
|
colour_option(monkeypatch, "file_header_inserted_foreground", "#00ff00")
|
||||||
|
palette.ensure(sublime.View())
|
||||||
|
rules = by_scope(tmp_path)
|
||||||
|
assert rules[palette.SCOPE_FILE_HEADER]["background"] == "#202040"
|
||||||
|
# the counts sit on the heading, so they share its background
|
||||||
|
assert rules[palette.SCOPE_FILE_INS_COUNT]["background"] == "#202040"
|
||||||
|
assert rules[palette.SCOPE_FILE_INS_COUNT]["foreground"] == "#00ff00"
|
||||||
|
assert rules[palette.SCOPE_FILE_DEL_COUNT]["foreground"] == palette.FILE_DEL_FOREGROUND
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
// how a review view first shows its files: "file_by_file" or "all_in_one"; v switches between them
|
||||||
|
"default_view_mode": "file_by_file",
|
||||||
|
|
||||||
// number of commits listed by "vDiff: Recent Commits"
|
// number of commits listed by "vDiff: Recent Commits"
|
||||||
"commits_count": 30,
|
"commits_count": 30,
|
||||||
|
|
||||||
@@ -14,6 +17,19 @@
|
|||||||
// number of unchanged lines shown around each change in the all-files view
|
// number of unchanged lines shown around each change in the all-files view
|
||||||
"all_files_context": 5,
|
"all_files_context": 5,
|
||||||
|
|
||||||
|
// background of deleted and inserted lines, e.g. "#d6464633"; empty uses the colour scheme's diff colours
|
||||||
|
"deleted_line_background": "",
|
||||||
|
"inserted_line_background": "",
|
||||||
|
|
||||||
|
// line-number column of the all-files view, e.g. "#888888" on "#1e1e1e"; empty keeps the plain-text look
|
||||||
|
"line_numbers_foreground": "",
|
||||||
|
"line_numbers_background": "",
|
||||||
|
|
||||||
|
// file heading of the all-files view: its background and the colours of its -/+ counts
|
||||||
|
"file_header_background": "#0000EE",
|
||||||
|
"file_header_deleted_foreground": "#d64646",
|
||||||
|
"file_header_inserted_foreground": "#46d646",
|
||||||
|
|
||||||
// git executable; sublime's PATH may not include a user-installed git
|
// git executable; sublime's PATH may not include a user-installed git
|
||||||
"git_binary": "git",
|
"git_binary": "git",
|
||||||
|
|
||||||
|
|||||||
@@ -86,13 +86,17 @@ def open_review(window: sublime.Window) -> Optional[Tuple[sublime.View, ViewStat
|
|||||||
st = state.get(view)
|
st = state.get(view)
|
||||||
if st is None or st.repo != repo:
|
if st is None or st.repo != repo:
|
||||||
# a different repository: browsing state no longer applies
|
# a different repository: browsing state no longer applies
|
||||||
st = state.put(view, state.ViewState(repo))
|
all_mode = settings().get("default_view_mode") == "all_in_one"
|
||||||
|
st = state.put(view, state.ViewState(repo, all_mode=all_mode))
|
||||||
window.focus_view(view)
|
window.focus_view(view)
|
||||||
return view, st
|
return view, st
|
||||||
|
|
||||||
|
|
||||||
def show_all(view: sublime.View, st: ViewState, focus: Optional[str] = None) -> None:
|
def show_all(view: sublime.View, st: ViewState, focus: Optional[str] = None) -> None:
|
||||||
"""Load all file diffs off the UI thread and switch to the aggregate view, on `focus`'s heading if given"""
|
"""Load all file diffs off the UI thread and switch to the aggregate view, on `focus`'s heading
|
||||||
|
|
||||||
|
Without `focus` the caret goes to the current file's heading, as it is when the diffs land
|
||||||
|
"""
|
||||||
st.all_mode = True
|
st.all_mode = True
|
||||||
# bump the token so earlier loads drop their results
|
# bump the token so earlier loads drop their results
|
||||||
st.token += 1
|
st.token += 1
|
||||||
@@ -110,7 +114,8 @@ def show_all(view: sublime.View, st: ViewState, focus: Optional[str] = None) ->
|
|||||||
st.set_all_diffs(result)
|
st.set_all_diffs(result)
|
||||||
if st.change is None and files:
|
if st.change is None and files:
|
||||||
st.change = files[0]
|
st.change = files[0]
|
||||||
render.render_all(view, st, focus=focus)
|
render.render_all(view, st, focus=focus or (st.change.path if st.change else None))
|
||||||
|
st.rendered_token = token
|
||||||
run_async(work, done)
|
run_async(work, done)
|
||||||
|
|
||||||
|
|
||||||
@@ -183,12 +188,26 @@ def commit_items(commits: Sequence[Commit]) -> List[sublime.QuickPanelItem]:
|
|||||||
def show_files_panel(window: sublime.Window, view: sublime.View, st: ViewState, files: List[FileChange],
|
def show_files_panel(window: sublime.Window, view: sublime.View, st: ViewState, files: List[FileChange],
|
||||||
kept: Kept, on_cancel: Optional[Callable[[], None]] = None,
|
kept: Kept, on_cancel: Optional[Callable[[], None]] = None,
|
||||||
placeholder: str = "Changed files") -> None:
|
placeholder: str = "Changed files") -> None:
|
||||||
"""Quick panel over `files`: highlight previews, Enter shows, Esc restores `kept` (then runs on_cancel)"""
|
"""Quick panel over `files`: highlight previews, Enter shows, Esc restores `kept` (then runs on_cancel)
|
||||||
|
|
||||||
|
In the aggregate view every diff is loaded once as the panel opens, and a highlight only scrolls to its file
|
||||||
|
"""
|
||||||
# the panel opens on the shown file, the view focused behind it
|
# the panel opens on the shown file, the view focused behind it
|
||||||
st.files = files
|
st.files = files
|
||||||
selected = max(0, st.index())
|
selected = max(0, st.index())
|
||||||
window.focus_view(view)
|
window.focus_view(view)
|
||||||
|
|
||||||
|
def show(change: FileChange) -> None:
|
||||||
|
"""Show `change`: its own diff, or its heading in the aggregate view"""
|
||||||
|
if not st.all_mode:
|
||||||
|
show_file(view, st, change)
|
||||||
|
return
|
||||||
|
st.change = change
|
||||||
|
st.collapsed.discard(change.path)
|
||||||
|
# still loading: show_all lands on st.change when the diffs arrive
|
||||||
|
if st.rendered_token == st.token:
|
||||||
|
render.render_all(view, st, focus=change.path)
|
||||||
|
|
||||||
def on_select(index: int) -> None:
|
def on_select(index: int) -> None:
|
||||||
"""Enter shows the chosen file; Esc restores `kept`, then runs on_cancel if given"""
|
"""Enter shows the chosen file; Esc restores `kept`, then runs on_cancel if given"""
|
||||||
st.highlight += 1 # cancel a pending preview
|
st.highlight += 1 # cancel a pending preview
|
||||||
@@ -197,15 +216,18 @@ def show_files_panel(window: sublime.Window, view: sublime.View, st: ViewState,
|
|||||||
st.restore(kept)
|
st.restore(kept)
|
||||||
on_cancel()
|
on_cancel()
|
||||||
elif kept.change is not None:
|
elif kept.change is not None:
|
||||||
# reshow the kept file
|
# reshow the kept file in the kept mode
|
||||||
st.restore(kept)
|
st.restore(kept)
|
||||||
show_file(view, st, kept.change)
|
if st.all_mode:
|
||||||
|
show_all(view, st)
|
||||||
|
else:
|
||||||
|
show_file(view, st, kept.change)
|
||||||
# nothing was kept before: the last previewed file stays
|
# nothing was kept before: the last previewed file stays
|
||||||
return
|
return
|
||||||
change = files[index]
|
change = files[index]
|
||||||
if st.change is change and st.rendered_token == st.token:
|
if st.change is change and st.rendered_token == st.token:
|
||||||
return # the preview already shows it
|
return # the preview already shows it
|
||||||
show_file(view, st, change)
|
show(change)
|
||||||
|
|
||||||
def on_highlight(index: int) -> None:
|
def on_highlight(index: int) -> None:
|
||||||
"""Schedule a debounced preview of the highlighted file"""
|
"""Schedule a debounced preview of the highlighted file"""
|
||||||
@@ -217,10 +239,13 @@ def show_files_panel(window: sublime.Window, view: sublime.View, st: ViewState,
|
|||||||
def preview() -> None:
|
def preview() -> None:
|
||||||
"""Show the file unless another highlight came since"""
|
"""Show the file unless another highlight came since"""
|
||||||
if ticket == st.highlight:
|
if ticket == st.highlight:
|
||||||
show_file(view, st, files[index])
|
show(files[index])
|
||||||
|
|
||||||
sublime.set_timeout(preview, PREVIEW_DELAY_MS)
|
sublime.set_timeout(preview, PREVIEW_DELAY_MS)
|
||||||
|
|
||||||
|
if st.all_mode and files:
|
||||||
|
st.change = files[selected]
|
||||||
|
show_all(view, st)
|
||||||
window.show_quick_panel(file_items(files), on_select, 0, selected, on_highlight, placeholder)
|
window.show_quick_panel(file_items(files), on_select, 0, selected, on_highlight, placeholder)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
13
vdiff/git.py
13
vdiff/git.py
@@ -1,4 +1,5 @@
|
|||||||
"""Git access via subprocess; results are plain tuples, no sublime imports (testable)"""
|
"""Git access via subprocess; results are plain tuples, no sublime imports (testable)"""
|
||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -115,6 +116,18 @@ def has_head(repo: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=None)
|
||||||
|
def index_path(repo: str) -> Optional[str]:
|
||||||
|
"""Absolute path of the repository's index file, None outside a repository
|
||||||
|
|
||||||
|
A worktree or submodule keeps it outside <repo>/.git, which there is a file, not a directory
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return os.path.join(repo, run(repo, ["rev-parse", "--git-path", "index"]).strip())
|
||||||
|
except (GitError, OSError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def base_ref(repo: str) -> str:
|
def base_ref(repo: str) -> str:
|
||||||
"""HEAD, or the empty tree when there is no commit yet"""
|
"""HEAD, or the empty tree when there is no commit yet"""
|
||||||
return "HEAD" if has_head(repo) else EMPTY_TREE
|
return "HEAD" if has_head(repo) else EMPTY_TREE
|
||||||
|
|||||||
123
vdiff/palette.py
123
vdiff/palette.py
@@ -14,6 +14,8 @@ from typing import Any, Dict, List, Optional, Set, Tuple
|
|||||||
|
|
||||||
import sublime
|
import sublime
|
||||||
|
|
||||||
|
from .utils import settings
|
||||||
|
|
||||||
LOG = logging.getLogger("vdiff")
|
LOG = logging.getLogger("vdiff")
|
||||||
|
|
||||||
# tints under the token layer: the scheme's own diff scopes need no restart, and the token layer repaints their text
|
# tints under the token layer: the scheme's own diff scopes need no restart, and the token layer repaints their text
|
||||||
@@ -30,7 +32,7 @@ KINDS: Tuple[str, ...] = TINT_KINDS + ("ctx",)
|
|||||||
KIND_TINT: Dict[str, str] = {"del": SCOPE_DEL, "ins": SCOPE_INS, "del.char": SCOPE_DEL_CHAR, "ins.char": SCOPE_INS_CHAR,
|
KIND_TINT: Dict[str, str] = {"del": SCOPE_DEL, "ins": SCOPE_INS, "del.char": SCOPE_DEL_CHAR, "ins.char": SCOPE_INS_CHAR,
|
||||||
"ctx": SCOPE_CTX}
|
"ctx": SCOPE_CTX}
|
||||||
|
|
||||||
# all-files file heading and its -/+ counts; the colours are user-requested
|
# all-files file heading and its -/+ counts; their colours come from settings, these when one is empty
|
||||||
SCOPE_FILE_HEADER = "vdiff.file.header"
|
SCOPE_FILE_HEADER = "vdiff.file.header"
|
||||||
SCOPE_FILE_DEL_COUNT = "vdiff.file.del.count"
|
SCOPE_FILE_DEL_COUNT = "vdiff.file.del.count"
|
||||||
SCOPE_FILE_INS_COUNT = "vdiff.file.ins.count"
|
SCOPE_FILE_INS_COUNT = "vdiff.file.ins.count"
|
||||||
@@ -39,6 +41,16 @@ FILE_HEADER_BACKGROUND = "#0000FF"
|
|||||||
FILE_DEL_FOREGROUND = "#d64646"
|
FILE_DEL_FOREGROUND = "#d64646"
|
||||||
FILE_INS_FOREGROUND = "#46d646"
|
FILE_INS_FOREGROUND = "#46d646"
|
||||||
|
|
||||||
|
# colours the user may set; an empty line colour keeps the colour scheme's tint or the plain-text look
|
||||||
|
COLOUR_SETTINGS: Tuple[str, ...] = ("deleted_line_background", "inserted_line_background",
|
||||||
|
"line_numbers_foreground", "line_numbers_background",
|
||||||
|
"file_header_background", "file_header_deleted_foreground",
|
||||||
|
"file_header_inserted_foreground")
|
||||||
|
# the scopes those colours go into: the changed-line tints and the all-files line-number column
|
||||||
|
SCOPE_LINE_DEL = "vdiff.line.del"
|
||||||
|
SCOPE_LINE_INS = "vdiff.line.ins"
|
||||||
|
SCOPE_LINE_NUMBERS = "vdiff.line.numbers"
|
||||||
|
|
||||||
# tints to provision when the active scheme defines none of its own (some third-party schemes)
|
# tints to provision when the active scheme defines none of its own (some third-party schemes)
|
||||||
FALLBACK_TINTS: Dict[str, str] = {
|
FALLBACK_TINTS: Dict[str, str] = {
|
||||||
SCOPE_DEL: "hsla(357, 45%, 60%, 0.15)",
|
SCOPE_DEL: "hsla(357, 45%, 60%, 0.15)",
|
||||||
@@ -84,6 +96,8 @@ _palettes: Dict[str, Dict[str, int]] = {}
|
|||||||
_active: Optional[str] = None
|
_active: Optional[str] = None
|
||||||
# schemes the user has already been told need a restart
|
# schemes the user has already been told need a restart
|
||||||
_notified: Set[Optional[str]] = set()
|
_notified: Set[Optional[str]] = set()
|
||||||
|
# {override file name: the user colours it was provisioned with}, so changing one re-provisions
|
||||||
|
_provisioned_with: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
|
|
||||||
def reset() -> None:
|
def reset() -> None:
|
||||||
@@ -91,6 +105,7 @@ def reset() -> None:
|
|||||||
global _active
|
global _active
|
||||||
_palettes.clear()
|
_palettes.clear()
|
||||||
_notified.clear()
|
_notified.clear()
|
||||||
|
_provisioned_with.clear()
|
||||||
_active = None
|
_active = None
|
||||||
|
|
||||||
|
|
||||||
@@ -161,6 +176,26 @@ def token_keys() -> List[str]:
|
|||||||
return [token_key(_scope_name(kind, i)) for kind in KINDS for i in sorted(colours.values())]
|
return [token_key(_scope_name(kind, i)) for kind in KINDS for i in sorted(colours.values())]
|
||||||
|
|
||||||
|
|
||||||
|
def user_colours() -> Dict[str, str]:
|
||||||
|
"""{setting: colour} for COLOUR_SETTINGS, "" where unset"""
|
||||||
|
return {name: settings().get(name) or "" for name in COLOUR_SETTINGS}
|
||||||
|
|
||||||
|
|
||||||
|
def line_scope(view: sublime.View, kind: str) -> str:
|
||||||
|
"""Tint scope for a changed line, "del" or "ins": the user's colour once its rule is live, else the scheme's"""
|
||||||
|
own, setting, scheme = ((SCOPE_LINE_DEL, "deleted_line_background", SCOPE_DEL) if kind == "del" else
|
||||||
|
(SCOPE_LINE_INS, "inserted_line_background", SCOPE_INS))
|
||||||
|
return own if settings().get(setting) and scope_background(view, own) else scheme
|
||||||
|
|
||||||
|
|
||||||
|
def line_numbers_scope(view: sublime.View) -> str:
|
||||||
|
"""Scope for the all-files line-number column: the user's colours once their rule is live, else plain text"""
|
||||||
|
if (settings().get("line_numbers_foreground") or settings().get("line_numbers_background")) \
|
||||||
|
and scope_background(view, SCOPE_LINE_NUMBERS):
|
||||||
|
return SCOPE_LINE_NUMBERS
|
||||||
|
return "text.plain"
|
||||||
|
|
||||||
|
|
||||||
def _scope_name(kind: str, index: int) -> str:
|
def _scope_name(kind: str, index: int) -> str:
|
||||||
"""Generated scope name for a background kind and palette index"""
|
"""Generated scope name for a background kind and palette index"""
|
||||||
return f"vdiff.{kind}.c{index}"
|
return f"vdiff.{kind}.c{index}"
|
||||||
@@ -174,8 +209,10 @@ def ensure(view: sublime.View) -> None:
|
|||||||
global _active
|
global _active
|
||||||
name = override_name(view)
|
name = override_name(view)
|
||||||
_active = name
|
_active = name
|
||||||
if not name or name in _palettes:
|
user = user_colours()
|
||||||
|
if not name or (name in _palettes and _provisioned_with.get(name) == user):
|
||||||
return
|
return
|
||||||
|
_provisioned_with[name] = user
|
||||||
# merge into the existing override, if any
|
# merge into the existing override, if any
|
||||||
path = os.path.join(sublime.packages_path(), "User", name)
|
path = os.path.join(sublime.packages_path(), "User", name)
|
||||||
try:
|
try:
|
||||||
@@ -186,10 +223,10 @@ def ensure(view: sublime.View) -> None:
|
|||||||
scheme = sublime.decode_value(f.read()) or {}
|
scheme = sublime.decode_value(f.read()) or {}
|
||||||
rules = scheme.setdefault("rules", [])
|
rules = scheme.setdefault("rules", [])
|
||||||
# probe the scheme, record the palette, and write only when a rule changed
|
# probe the scheme, record the palette, and write only when a rule changed
|
||||||
tints = _tints(view)
|
tints = _tints(view, user)
|
||||||
colours = {colour: i for i, colour in enumerate(_probe_palette(view))}
|
colours = {colour: i for i, colour in enumerate(_probe_palette(view))}
|
||||||
_palettes[name] = colours
|
_palettes[name] = colours
|
||||||
if not _merge_rules(rules, tints, colours, view):
|
if not _merge_rules(rules, tints, colours, view, user):
|
||||||
return
|
return
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
@@ -211,10 +248,11 @@ def _probe_palette(view: sublime.View) -> List[str]:
|
|||||||
return seen
|
return seen
|
||||||
|
|
||||||
|
|
||||||
def _tints(view: sublime.View) -> Dict[str, str]:
|
def _tints(view: sublime.View, user: Dict[str, str]) -> 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: taking only the half a scheme defines would put our hue next to its own
|
All or nothing per pair: taking only the half a scheme defines would put our hue next to its own.
|
||||||
|
A line colour the user set wins over both
|
||||||
"""
|
"""
|
||||||
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)):
|
||||||
@@ -222,6 +260,9 @@ def _tints(view: sublime.View) -> Dict[str, str]:
|
|||||||
char = scope_background(view, char_scope)
|
char = scope_background(view, char_scope)
|
||||||
tints[line_scope] = line if line and char else FALLBACK_TINTS[line_scope]
|
tints[line_scope] = line if line and char else FALLBACK_TINTS[line_scope]
|
||||||
tints[char_scope] = char if line and char else FALLBACK_TINTS[char_scope]
|
tints[char_scope] = char if line and char else FALLBACK_TINTS[char_scope]
|
||||||
|
for scope, setting in ((SCOPE_DEL, "deleted_line_background"), (SCOPE_INS, "inserted_line_background")):
|
||||||
|
if user[setting]:
|
||||||
|
tints[scope] = user[setting]
|
||||||
# a region whose background is missing, transparent or equal to the editor's is filled with its
|
# a region whose background is missing, transparent or equal to the editor's is filled with its
|
||||||
# foreground, so context colours carry the editor background nudged by an invisible step
|
# foreground, so context colours carry the editor background nudged by an invisible step
|
||||||
tints[SCOPE_CTX] = _nudge(_global_background(view))
|
tints[SCOPE_CTX] = _nudge(_global_background(view))
|
||||||
@@ -246,57 +287,55 @@ def _nudge(colour: str) -> str:
|
|||||||
return f"{colour[:5]}{blue + 1 if blue < 128 else blue - 1:02x}{colour[7:]}"
|
return f"{colour[:5]}{blue + 1 if blue < 128 else blue - 1:02x}{colour[7:]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _put_rule(rules: List[Dict[str, str]], present: Dict[Optional[str], Dict[str, str]], wanted: Dict[str, str]) -> bool:
|
||||||
|
"""Add `wanted`, or make the rule with its scope equal to it; True when anything changed"""
|
||||||
|
rule = present.get(wanted["scope"])
|
||||||
|
if rule is None:
|
||||||
|
rules.append(wanted)
|
||||||
|
elif rule != wanted:
|
||||||
|
rule.clear()
|
||||||
|
rule.update(wanted)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _merge_rules(rules: List[Dict[str, str]], tints: Dict[str, str], colours: Dict[str, int],
|
def _merge_rules(rules: List[Dict[str, str]], tints: Dict[str, str], colours: Dict[str, int],
|
||||||
view: sublime.View) -> bool:
|
view: sublime.View, user: Dict[str, str]) -> 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 (kind, colour) rule carries a background and a foreground, as a background alone would flatten
|
Each (kind, colour) rule carries a background and a foreground, as a background alone would flatten
|
||||||
the line's colours; plain tints are added only when missing and never overwritten, so a hand-tuned one survives
|
the line's colours; plain tints are added only when missing and never overwritten, so a hand-tuned one survives
|
||||||
"""
|
"""
|
||||||
present = {rule.get("scope"): rule for rule in rules}
|
present = {rule.get("scope"): rule for rule in rules}
|
||||||
|
# the all-files file heading and its deletion and insertion counts
|
||||||
|
header = user["file_header_background"] or FILE_HEADER_BACKGROUND
|
||||||
|
wanted = [{"scope": SCOPE_FILE_HEADER, "background": header}]
|
||||||
|
for scope, colour in ((SCOPE_FILE_DEL_COUNT, user["file_header_deleted_foreground"] or FILE_DEL_FOREGROUND),
|
||||||
|
(SCOPE_FILE_INS_COUNT, user["file_header_inserted_foreground"] or FILE_INS_FOREGROUND)):
|
||||||
|
wanted.append({"scope": scope, "background": header, "foreground": colour, "font_style": "bold"})
|
||||||
|
# the user's line colours; an unset one keeps any old rule, which render then ignores
|
||||||
|
for scope, setting in ((SCOPE_LINE_DEL, "deleted_line_background"), (SCOPE_LINE_INS, "inserted_line_background")):
|
||||||
|
if user[setting]:
|
||||||
|
wanted.append({"scope": scope, "background": user[setting]})
|
||||||
|
if user["line_numbers_foreground"] or user["line_numbers_background"]:
|
||||||
|
# both halves, as a region without a background is filled with its foreground
|
||||||
|
wanted.append({"scope": SCOPE_LINE_NUMBERS,
|
||||||
|
"foreground": user["line_numbers_foreground"] or foreground(view, "text.plain") or "#ffffff",
|
||||||
|
"background": user["line_numbers_background"] or _nudge(_global_background(view))})
|
||||||
changed = False
|
changed = False
|
||||||
# the all-files file heading
|
for rule in wanted:
|
||||||
header = present.get(SCOPE_FILE_HEADER)
|
changed = _put_rule(rules, present, rule) or changed
|
||||||
wanted_header = {"scope": SCOPE_FILE_HEADER, "background": FILE_HEADER_BACKGROUND}
|
|
||||||
if header is None:
|
|
||||||
rules.append(wanted_header)
|
|
||||||
changed = True
|
|
||||||
elif header != wanted_header:
|
|
||||||
header.clear()
|
|
||||||
header.update(wanted_header)
|
|
||||||
changed = True
|
|
||||||
# its deletion and insertion counts
|
|
||||||
for scope, foreground in ((SCOPE_FILE_DEL_COUNT, FILE_DEL_FOREGROUND),
|
|
||||||
(SCOPE_FILE_INS_COUNT, FILE_INS_FOREGROUND)):
|
|
||||||
count_rule = present.get(scope)
|
|
||||||
wanted_count = {"scope": scope, "background": FILE_HEADER_BACKGROUND,
|
|
||||||
"foreground": foreground, "font_style": "bold"}
|
|
||||||
if count_rule is None:
|
|
||||||
rules.append(wanted_count)
|
|
||||||
changed = True
|
|
||||||
elif count_rule != wanted_count:
|
|
||||||
count_rule.clear()
|
|
||||||
count_rule.update(wanted_count)
|
|
||||||
changed = True
|
|
||||||
# plain tints the scheme lacks; the context tint gets no rule of its own
|
# plain tints the scheme lacks; the context tint gets no rule of its own
|
||||||
for scope, background in tints.items():
|
for scope, background in tints.items():
|
||||||
if scope != SCOPE_CTX and scope not in present and not scope_background(view, scope):
|
if scope != SCOPE_CTX and scope not in present and not scope_background(view, scope):
|
||||||
rules.append({"scope": scope, "background": background})
|
rules.append({"scope": scope, "background": background})
|
||||||
changed = True
|
changed = True
|
||||||
# one rule per (kind, colour) pair
|
# one rule per (kind, colour) pair; a palette shifted under an earlier session's rule is ours to correct
|
||||||
for colour, index in colours.items():
|
for colour, index in colours.items():
|
||||||
for kind in KINDS:
|
for kind in KINDS:
|
||||||
wanted = {"scope": _scope_name(kind, index), "background": tints[KIND_TINT[kind]],
|
pair = {"scope": _scope_name(kind, index), "background": tints[KIND_TINT[kind]], "foreground": colour}
|
||||||
"foreground": colour}
|
changed = _put_rule(rules, present, pair) or changed
|
||||||
rule = present.get(wanted["scope"])
|
|
||||||
if rule is None:
|
|
||||||
rules.append(wanted)
|
|
||||||
changed = True
|
|
||||||
elif rule != wanted:
|
|
||||||
# the palette shifted under a rule an earlier session wrote; ours to keep correct
|
|
||||||
rule.clear()
|
|
||||||
rule.update(wanted)
|
|
||||||
changed = True
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import Dict, List, NamedTuple, Optional, Sequence, Tuple, Union
|
|||||||
import sublime
|
import sublime
|
||||||
import sublime_plugin
|
import sublime_plugin
|
||||||
|
|
||||||
from . import palette, parse
|
from . import git, palette, parse
|
||||||
from .git import FileChange, STATUS_NAMES
|
from .git import FileChange, STATUS_NAMES
|
||||||
from .parse import Block, Line
|
from .parse import Block, Line
|
||||||
from .state import VIEW_FLAG, ViewState
|
from .state import VIEW_FLAG, ViewState
|
||||||
@@ -215,11 +215,13 @@ def caret_index(view: sublime.View) -> int:
|
|||||||
return view.rowcol(view.sel()[0].begin())[0] - _HEADER_ROWS
|
return view.rowcol(view.sel()[0].begin())[0] - _HEADER_ROWS
|
||||||
|
|
||||||
|
|
||||||
def fingerprint(st: ViewState) -> Optional[Tuple[Optional[float], Optional[float]]]:
|
def fingerprint(st: ViewState) -> Optional[Tuple[Optional[float], ...]]:
|
||||||
"""Cheap "did anything change" key for working-tree mode: mtimes of the file and of the git index"""
|
"""Cheap "did anything change" key for working-tree mode: mtimes of the shown files and of the git index"""
|
||||||
if st.in_commit or st.change is None:
|
if st.in_commit or st.change is None:
|
||||||
return None
|
return None
|
||||||
return (_mtime(os.path.join(st.repo, st.change.path)), _mtime(os.path.join(st.repo, ".git", "index")))
|
files = st.files if st.all_mode else [st.change]
|
||||||
|
index = git.index_path(st.repo)
|
||||||
|
return tuple(_mtime(os.path.join(st.repo, c.path)) for c in files) + (_mtime(index) if index else None,)
|
||||||
|
|
||||||
|
|
||||||
def _mtime(path: str) -> Optional[float]:
|
def _mtime(path: str) -> Optional[float]:
|
||||||
@@ -386,12 +388,12 @@ def render_all(view: sublime.View, st: ViewState, focus: Union[None, str, Tuple[
|
|||||||
elif line.kind == "+":
|
elif line.kind == "+":
|
||||||
ins_regions.append(region)
|
ins_regions.append(region)
|
||||||
# paint everything
|
# paint everything
|
||||||
view.add_regions(KEY_DEL, del_regions, palette.SCOPE_DEL, flags=sublime.DRAW_NO_OUTLINE)
|
view.add_regions(KEY_DEL, del_regions, palette.line_scope(view, "del"), flags=sublime.DRAW_NO_OUTLINE)
|
||||||
view.add_regions(KEY_INS, ins_regions, palette.SCOPE_INS, flags=sublime.DRAW_NO_OUTLINE)
|
view.add_regions(KEY_INS, ins_regions, palette.line_scope(view, "ins"), flags=sublime.DRAW_NO_OUTLINE)
|
||||||
view.add_regions(KEY_FILE_HEADER, file_regions, _file_header_scope(view), flags=sublime.DRAW_NO_OUTLINE)
|
view.add_regions(KEY_FILE_HEADER, file_regions, _file_header_scope(view), flags=sublime.DRAW_NO_OUTLINE)
|
||||||
view.add_regions(KEY_FILE_DEL_COUNT, file_del_counts, KEY_FILE_DEL_COUNT, flags=sublime.DRAW_NO_OUTLINE)
|
view.add_regions(KEY_FILE_DEL_COUNT, file_del_counts, KEY_FILE_DEL_COUNT, flags=sublime.DRAW_NO_OUTLINE)
|
||||||
view.add_regions(KEY_FILE_INS_COUNT, file_ins_counts, KEY_FILE_INS_COUNT, flags=sublime.DRAW_NO_OUTLINE)
|
view.add_regions(KEY_FILE_INS_COUNT, file_ins_counts, KEY_FILE_INS_COUNT, flags=sublime.DRAW_NO_OUTLINE)
|
||||||
view.add_regions(KEY_FILE_PREFIX, prefix_regions, "text.plain", flags=sublime.DRAW_NO_OUTLINE)
|
view.add_regions(KEY_FILE_PREFIX, prefix_regions, palette.line_numbers_scope(view), flags=sublime.DRAW_NO_OUTLINE)
|
||||||
for scope, regions in token_groups.items():
|
for scope, regions in token_groups.items():
|
||||||
view.add_regions(palette.token_key(scope), regions, scope, flags=sublime.DRAW_NO_OUTLINE)
|
view.add_regions(palette.token_key(scope), regions, scope, flags=sublime.DRAW_NO_OUTLINE)
|
||||||
insertions, deletions = _totals(st.files)
|
insertions, deletions = _totals(st.files)
|
||||||
@@ -513,8 +515,8 @@ def _paint_regions(view: sublime.View, st: ViewState) -> None:
|
|||||||
# full-line tint
|
# full-line tint
|
||||||
starts = _row_starts(lines)
|
starts = _row_starts(lines)
|
||||||
deleted, inserted = _line_regions(lines, starts)
|
deleted, inserted = _line_regions(lines, starts)
|
||||||
view.add_regions(KEY_DEL, deleted, palette.SCOPE_DEL, flags=sublime.DRAW_NO_OUTLINE)
|
view.add_regions(KEY_DEL, deleted, palette.line_scope(view, "del"), flags=sublime.DRAW_NO_OUTLINE)
|
||||||
view.add_regions(KEY_INS, inserted, palette.SCOPE_INS, flags=sublime.DRAW_NO_OUTLINE)
|
view.add_regions(KEY_INS, inserted, palette.line_scope(view, "ins"), flags=sublime.DRAW_NO_OUTLINE)
|
||||||
# gutter dots
|
# gutter dots
|
||||||
mark_flags = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE
|
mark_flags = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE
|
||||||
del_marks = [sublime.Region(starts[i]) for i, line in enumerate(lines) if line.kind == "-"]
|
del_marks = [sublime.Region(starts[i]) for i, line in enumerate(lines) if line.kind == "-"]
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class Kept(NamedTuple):
|
|||||||
commit: Optional["CommitInfo"]
|
commit: Optional["CommitInfo"]
|
||||||
change: Optional["FileChange"]
|
change: Optional["FileChange"]
|
||||||
files: List["FileChange"]
|
files: List["FileChange"]
|
||||||
|
all_mode: bool
|
||||||
|
|
||||||
|
|
||||||
def find_change(files: Sequence["FileChange"], path: Optional[str]) -> int:
|
def find_change(files: Sequence["FileChange"], path: Optional[str]) -> int:
|
||||||
@@ -48,12 +49,9 @@ class ViewState:
|
|||||||
token: int = 0 # bumped per show request; a stale async result is dropped
|
token: int = 0 # bumped per show request; a stale async result is dropped
|
||||||
rendered_token: int = -1 # token of the request that produced the current render
|
rendered_token: int = -1 # token of the request that produced the current render
|
||||||
highlight: int = 0 # bumped per quick-panel highlight, to debounce previews
|
highlight: int = 0 # bumped per quick-panel highlight, to debounce previews
|
||||||
# (file mtime, index mtime) at render time, working-tree mode; either mtime is None when unreadable
|
fingerprint: Optional[Tuple[Optional[float], ...]] = None # all-files view: diffs cached so collapse/expand is instant
|
||||||
fingerprint: Optional[Tuple[Optional[float], Optional[float]]] = None
|
|
||||||
# all-files view: diffs cached so collapse/expand is instant
|
|
||||||
all_mode: bool = False
|
all_mode: bool = False
|
||||||
all_diffs: Dict[str, Tuple[List["Line"], List["Block"], bool]] = field(default_factory=dict)
|
all_diffs: Dict[str, Tuple[List["Line"], List["Block"], bool]] = field(default_factory=dict)
|
||||||
# path -> {(old_no, new_no): [(start, end, colour)]}: syntax colours per row, offsets within the line
|
|
||||||
all_colours: Dict[str, Dict[Tuple[Optional[int], Optional[int]], List[Tuple[int, int, str]]]] = field(default_factory=dict)
|
all_colours: Dict[str, Dict[Tuple[Optional[int], Optional[int]], List[Tuple[int, int, str]]]] = field(default_factory=dict)
|
||||||
collapsed: set = field(default_factory=set)
|
collapsed: set = field(default_factory=set)
|
||||||
all_rows: List[Tuple[Optional["FileChange"], Optional["Line"], bool]] = field(default_factory=list)
|
all_rows: List[Tuple[Optional["FileChange"], Optional["Line"], bool]] = field(default_factory=list)
|
||||||
@@ -77,7 +75,7 @@ class ViewState:
|
|||||||
|
|
||||||
def kept(self) -> Kept:
|
def kept(self) -> Kept:
|
||||||
"""Snapshot of what to go back to on Esc"""
|
"""Snapshot of what to go back to on Esc"""
|
||||||
return Kept(self.sha, self.commit, self.change, list(self.files))
|
return Kept(self.sha, self.commit, self.change, list(self.files), self.all_mode)
|
||||||
|
|
||||||
def enter(self, sha: Optional[str] = None, commit: Optional["CommitInfo"] = None) -> Kept:
|
def enter(self, sha: Optional[str] = None, commit: Optional["CommitInfo"] = None) -> Kept:
|
||||||
"""Switch to browsing a commit (or the working tree); returns what to restore on Esc"""
|
"""Switch to browsing a commit (or the working tree); returns what to restore on Esc"""
|
||||||
@@ -89,13 +87,16 @@ class ViewState:
|
|||||||
|
|
||||||
def restore(self, kept: Kept) -> None:
|
def restore(self, kept: Kept) -> None:
|
||||||
"""Go back to a snapshot; its file becomes the requested one"""
|
"""Go back to a snapshot; its file becomes the requested one"""
|
||||||
self.sha, self.commit, self.change, self.files = kept
|
self.sha, self.commit, self.change, self.files, self.all_mode = kept
|
||||||
self.requested = kept.change
|
self.requested = kept.change
|
||||||
|
|
||||||
def set_all_diffs(self, diffs: Dict[str, Tuple[List["Line"], List["Block"], bool]]) -> None:
|
def set_all_diffs(self, diffs: Dict[str, Tuple[List["Line"], List["Block"], bool]]) -> None:
|
||||||
"""Replace the all-files diffs; a file's cached colours survive only if its diff is unchanged"""
|
"""Replace the all-files diffs; a file's cached colours survive only if its diff is unchanged"""
|
||||||
self.all_colours = {path: colours for path, colours in self.all_colours.items()
|
self.all_colours = {
|
||||||
if path in diffs and path in self.all_diffs and diffs[path][0] == self.all_diffs[path][0]}
|
path: colours
|
||||||
|
for path, colours in self.all_colours.items()
|
||||||
|
if path in diffs and path in self.all_diffs and diffs[path][0] == self.all_diffs[path][0]
|
||||||
|
}
|
||||||
self.all_diffs = diffs
|
self.all_diffs = diffs
|
||||||
|
|
||||||
def persist(self, view: "sublime.View") -> None:
|
def persist(self, view: "sublime.View") -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user