initial commit

This commit is contained in:
Vaclav Tvrdik
2026-09-23 21:10:59 +02:00
commit 8c174bc131
23 changed files with 2192 additions and 0 deletions

7
tests/conftest.py Normal file
View File

@@ -0,0 +1,7 @@
import os
import sys
# tests import the package directly; sublime/sublime_plugin come from tests/stubs
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, os.path.join(HERE, "stubs"))

223
tests/stubs/sublime.py Normal file
View File

@@ -0,0 +1,223 @@
""" Minimal sublime API stub so the plugin modules import and header/regions logic runs offline """
OP_EQUAL = 0
OP_NOT_EQUAL = 1
DRAW_NO_OUTLINE = 1
DRAW_NO_FILL = 2
LAYOUT_BLOCK = 2
ENCODED_POSITION = 1
KIND_ID_COLOR_REDISH = 1
KIND_ID_COLOR_ORANGISH = 2
KIND_ID_COLOR_YELLOWISH = 3
KIND_ID_COLOR_GREENISH = 4
KIND_ID_COLOR_CYANISH = 5
KIND_ID_COLOR_BLUISH = 6
KIND_ID_COLOR_PURPLISH = 7
KIND_ID_COLOR_PINKISH = 8
KIND_AMBIGUOUS = (0, "", "")
class Region:
def __init__(self, a, b=None):
self.a = a
self.b = a if b is None else b
def begin(self):
return min(self.a, self.b)
def end(self):
return max(self.a, self.b)
def __eq__(self, other):
return (self.a, self.b) == (other.a, other.b)
def __repr__(self):
return "Region({}, {})".format(self.a, self.b)
class Settings(dict):
def get(self, key, default=None):
return dict.get(self, key, default)
def set(self, key, value):
self[key] = value
def add_on_change(self, tag, callback):
pass
_settings = {}
def load_settings(name):
return _settings.setdefault(name, Settings())
def set_timeout(fn, delay=0):
fn()
def set_timeout_async(fn, delay=0):
fn()
def status_message(text):
pass
def packages_path():
return "/tmp/Packages"
def windows():
return []
class QuickPanelItem:
def __init__(self, trigger, details="", annotation="", kind=KIND_AMBIGUOUS):
self.trigger, self.details, self.annotation, self.kind = trigger, details, annotation, kind
class Phantom:
def __init__(self, region, content, layout, on_navigate=None):
self.region, self.content, self.layout, self.on_navigate = region, content, layout, on_navigate
class Window:
"""Captures the quick panel so tests can drive its callbacks"""
def __init__(self):
self.panel = None
self.commands = []
def focus_view(self, view):
pass
def run_command(self, name, args=None):
self.commands.append((name, args))
def show_quick_panel(self, items, on_select, flags=0, selected=-1, on_highlight=None, placeholder=None):
self.panel = (items, on_select, selected, on_highlight, placeholder)
class View:
"""Text buffer with just the calls render.py makes"""
def __init__(self, text=""):
self.text = text
self.phantoms = {}
self.settings_ = Settings()
self.regions = {}
self.syntax = None
self.read_only = True
self.name_ = ""
self.sel_ = []
self.status = {}
def id(self):
return 1
def settings(self):
return self.settings_
def size(self):
return len(self.text)
def replace(self, edit, region, text):
self.text = self.text[: region.begin()] + text + self.text[region.end() :]
def run_command(self, name, args=None):
if name == "vdiff_replace":
self.replace(None, Region(0, self.size()), args["text"])
def set_read_only(self, flag):
self.read_only = flag
def is_read_only(self):
return self.read_only
def assign_syntax(self, path):
self.syntax = path
def add_regions(self, key, regions, scope="", icon="", flags=0):
self.regions[key] = (list(regions), scope, icon, flags)
def erase_regions(self, key):
self.regions.pop(key, None)
def text_point(self, row, col):
lines = self.text.split("\n")
return sum(len(l) + 1 for l in lines[:row]) + col
def rowcol(self, point):
before = self.text[:point]
row = before.count("\n")
return row, len(before) - (before.rfind("\n") + 1)
def full_line(self, point):
start = self.text.rfind("\n", 0, point) + 1
end = self.text.find("\n", point)
end = len(self.text) if end == -1 else end + 1
return Region(start, end)
def erase_phantoms(self, key):
self.phantoms.pop(key, None)
def add_phantom(self, key, region, content, layout, on_navigate=None):
self.phantoms.setdefault(key, []).append(Phantom(region, content, layout, on_navigate))
return len(self.phantoms[key])
def set_name(self, name):
self.name_ = name
def name(self):
return self.name_
def set_scratch(self, flag):
pass
def set_status(self, key, value):
self.status[key] = value
def set_viewport_position(self, xy, animate=True):
pass
def show_at_center(self, point):
pass
def style_for_scope(self, scope):
return {"foreground": "#ffffff", "background": "#333333"}
def is_valid(self):
return True
class _Sel(list):
def clear(self):
del self[:]
def add(self, region):
self.append(region)
def sel(self):
if not isinstance(self.sel_, View._Sel):
self.sel_ = View._Sel(self.sel_)
return self.sel_
def find_syntax_for_file(path, first_line=""):
class Syntax:
pass
syntax = Syntax()
syntax.path = "Packages/Python/Python.sublime-syntax" if path.endswith(".py") else "Packages/Text/Plain text.tmLanguage"
return syntax
def ui_info():
return {"system": {"style": "dark"}, "color_scheme": {"value": "Mariana.sublime-color-scheme", "resolved_value": "Packages/Color Scheme - Default/Mariana.sublime-color-scheme"}}
def decode_value(text):
import json
return json.loads(text)

View File

@@ -0,0 +1,12 @@
class TextCommand:
def __init__(self, view):
self.view = view
class WindowCommand:
def __init__(self, window):
self.window = window
class EventListener:
pass

126
tests/test_commands.py Normal file
View File

@@ -0,0 +1,126 @@
import sublime # the stub, via conftest
from vdiff import commands, git, state
def change(path, status="M"):
return git.FileChange(path, status, False, 1, 1, None)
def flush_previews():
"""The stub runs set_timeout synchronously, so previews are already shown"""
def test_esc_restores_previous_file_and_mode(monkeypatch):
shown = []
monkeypatch.setattr(commands, "show_file", lambda view, st, c: shown.append((st.sha, c.path)))
view = sublime.View()
st = state.ViewState("/repo")
st.change = change("render.py")
st.files = [st.change]
# switch to a commit, as open_commit does; the panel gets the pre-switch snapshot
kept = st.enter("abc", git.CommitInfo("abc", "abc1234", "A", "d", "s", ""))
assert st.change is None and kept.change.path == "render.py"
files = [change("notes.txt")]
window = sublime.Window()
commands.show_files_panel(window, view, st, files, kept, placeholder="x")
items, on_select, selected, on_highlight, _ = window.panel
assert [i.trigger for i in items] == ["notes.txt"] and selected == 0
on_highlight(0)
assert shown[-1] == ("abc", "notes.txt")
on_select(-1)
assert st.sha is None and st.commit is None and st.change.path == "render.py" and st.files == [st.change]
assert shown[-1] == (None, "render.py")
def test_esc_with_nothing_kept_keeps_preview(monkeypatch):
shown = []
monkeypatch.setattr(commands, "show_file", lambda view, st, c: shown.append(c.path))
st = state.ViewState("/repo")
window = sublime.Window()
commands.show_files_panel(window, sublime.View(), st, [change("a.py"), change("b.py")], st.kept())
_, on_select, _, on_highlight, _ = window.panel
on_highlight(1)
on_select(-1)
assert shown == ["b.py"]
def test_enter_skips_refetch_when_preview_is_current(monkeypatch):
shown = []
def fake_show(view, st, c):
st.token += 1
st.change = c
st.rendered_token = st.token
shown.append(c.path)
monkeypatch.setattr(commands, "show_file", fake_show)
st = state.ViewState("/repo")
window = sublime.Window()
files = [change("a.py"), change("b.py")]
commands.show_files_panel(window, sublime.View(), st, files, st.kept())
_, on_select, _, on_highlight, _ = window.panel
on_highlight(1)
on_select(1)
assert shown == ["b.py"] and st.files[1].path == "b.py"
on_select(0)
assert shown == ["b.py", "a.py"]
def test_on_cancel_goes_back_to_commit_list(monkeypatch):
monkeypatch.setattr(commands, "show_file", lambda view, st, c: None)
st = state.ViewState("/repo")
kept = st.enter("abc", None)
window = sublime.Window()
back = []
commands.show_files_panel(window, sublime.View(), st, [change("a.py")], kept, on_cancel=lambda: back.append(1))
window.panel[1](-1)
assert back == [1] and st.sha is None
def test_step_change_uses_blocks():
view = sublime.View()
st = state.ViewState("/repo")
st.blocks = [(2, 4), (7, 8)]
view.text = "\n" + "\n".join("l{}".format(i) for i in range(10))
view.sel().add(sublime.Region(commands.render.index_point(view, 0)))
commands.ACTIONS["next_change"](view, None, st)
assert commands.render.caret_index(view) == 2
commands.ACTIONS["next_change"](view, None, st)
assert commands.render.caret_index(view) == 7
commands.ACTIONS["prev_change"](view, None, st)
assert commands.render.caret_index(view) == 2
def test_file_items_annotations():
items = commands.file_items([git.FileChange("x.py", "?", False, 3, 0, None), git.FileChange("i.png", "M", True, None, None, None)])
assert items[0].annotation == "-0 +3" and items[0].details == "untracked" and items[0].kind[1] == "?"
assert items[1].annotation == "binary" and items[1].details == "modified, staged"
def test_git_actions_refuse_commit_mode(monkeypatch):
calls = []
monkeypatch.setattr(commands, "run_async", lambda work, done: calls.append(work))
st = state.ViewState("/repo")
st.enter("abc", None)
st.change = change("a.py")
commands.ACTIONS["stage"](sublime.View(), None, st)
assert calls == []
st.enter()
st.change = change("a.py")
commands.ACTIONS["stage"](sublime.View(), None, st)
assert len(calls) == 1
def test_run_async_reports_git_error(monkeypatch):
messages = []
monkeypatch.setattr(commands.sublime, "status_message", lambda m: messages.append(m))
def boom():
raise git.GitError("fatal: bad revision 'nope'\nmore", 128)
commands.run_async(boom, lambda r: messages.append("done"))
assert messages == ["vDiff: fatal: bad revision 'nope'"]

148
tests/test_git.py Normal file
View File

@@ -0,0 +1,148 @@
import os
import subprocess
import pytest
from vdiff import git, parse
def sh(repo, *args):
subprocess.run(["git"] + list(args), cwd=repo, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def write(repo, name, text):
path = os.path.join(repo, name)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
f.write(text)
@pytest.fixture
def repo(tmp_path):
repo = str(tmp_path)
sh(repo, "init", "-q", "-b", "main")
sh(repo, "config", "user.email", "t@example.com")
sh(repo, "config", "user.name", "Test")
write(repo, "a.py", "def f():\n return 1\n\n\ndef g():\n return 2\n")
write(repo, "b.txt", "keep\n")
write(repo, "gone.py", "bye\n")
sh(repo, "add", ".")
sh(repo, "commit", "-q", "-m", "initial")
return repo
def test_repo_root(repo):
assert git.repo_root(os.path.join(repo, "a.py")) == os.path.realpath(repo)
assert git.repo_root(repo) == os.path.realpath(repo)
assert git.repo_root("/") is None
def test_changed_files_all_kinds(repo):
write(repo, "a.py", "def f():\n return 10\n\n\ndef g():\n return 2\n") # unstaged M
write(repo, "b.txt", "keep\nmore\n")
sh(repo, "add", "b.txt") # staged M
os.remove(os.path.join(repo, "gone.py")) # unstaged D
write(repo, "sub/new.py", "x = 1\ny = 2") # untracked, no trailing newline
changes = {c.path: c for c in git.changed_files(repo)}
assert set(changes) == {"a.py", "b.txt", "gone.py", "sub/new.py"}
assert changes["a.py"] == git.FileChange("a.py", "M", False, 1, 1, None)
assert changes["b.txt"] == git.FileChange("b.txt", "M", True, 1, 0, None)
assert changes["gone.py"].status == "D" and changes["gone.py"].deletions == 1
assert changes["sub/new.py"] == git.FileChange("sub/new.py", "?", False, 2, 0, None)
def test_full_diff_working_tree(repo):
write(repo, "a.py", "def f():\n return 10\n\n\ndef g():\n return 2\n")
diff = parse.parse_unified(git.full_diff(repo, "a.py"))
assert [l.kind for l in diff.lines] == [" ", "-", "+", " ", " ", " ", " "]
assert diff.lines[2].text == " return 10"
write(repo, "n.py", "one\ntwo\n")
new = parse.parse_unified(git.full_diff(repo, "n.py", untracked=True))
assert [l.text for l in new.lines] == ["one", "two"] and all(l.kind == "+" for l in new.lines)
def test_stage_unstage_discard(repo):
write(repo, "a.py", "changed\n")
write(repo, "n.py", "new\n")
changes = {c.path: c for c in git.changed_files(repo)}
git.stage(repo, changes["a.py"])
changes = {c.path: c for c in git.changed_files(repo)}
assert changes["a.py"].staged
git.unstage(repo, changes["a.py"])
changes = {c.path: c for c in git.changed_files(repo)}
assert not changes["a.py"].staged
git.discard(repo, changes["a.py"])
git.discard(repo, git.FileChange("n.py", "?", False, 1, 0, None))
assert git.changed_files(repo) == []
assert not os.path.exists(os.path.join(repo, "n.py"))
# staged new file: discard removes it from index and disk
write(repo, "s.py", "s\n")
git.stage(repo, git.FileChange("s.py", "?", False, 1, 0, None))
change = git.changed_files(repo)[0]
assert change.status == "A"
git.discard(repo, change)
assert git.changed_files(repo) == []
def test_commits(repo):
write(repo, "a.py", "def f():\n return 10\n\n\ndef g():\n return 2\n")
write(repo, "c.py", "c\n")
os.remove(os.path.join(repo, "gone.py"))
sh(repo, "add", "-A")
sh(repo, "commit", "-q", "-m", "second\n\nbody text")
commits = git.recent_commits(repo, 10)
assert [c.subject for c in commits] == ["second", "initial"]
info, files = git.commit_with_files(repo, commits[0].sha)
assert info.subject == "second" and info.body.strip() == "body text" and info.author == "Test"
assert info.sha == commits[0].sha and len(info.date) == 16
files = {c.path: c for c in files}
assert files["a.py"] == git.FileChange("a.py", "M", False, 1, 1, None)
assert files["c.py"].status == "A" and files["c.py"].insertions == 1
assert files["gone.py"].status == "D" and files["gone.py"].deletions == 1
diff = parse.parse_unified(git.full_diff(repo, "a.py", sha=commits[0].sha))
assert [l.kind for l in diff.lines] == [" ", "-", "+", " ", " ", " ", " "]
# root commit diffs against the empty tree
_, root_files = git.commit_with_files(repo, commits[1].sha)
assert {c.path: c.status for c in root_files} == {"a.py": "A", "b.txt": "A", "gone.py": "A"}
# the short form resolves too (used by "review HEAD")
info, _ = git.commit_with_files(repo, "HEAD")
assert info.sha == commits[0].sha
def test_no_head_repo(tmp_path):
repo = str(tmp_path)
sh(repo, "init", "-q")
write(repo, "x.py", "x\n")
assert git.recent_commits(repo, 5) == []
changes = git.changed_files(repo)
assert changes[0].status == "?"
git.stage(repo, changes[0])
changes = git.changed_files(repo)
assert changes[0].status == "A" and changes[0].staged
diff = parse.parse_unified(git.full_diff(repo, "x.py"))
assert [l.kind for l in diff.lines] == ["+"]
git.unstage(repo, changes[0])
assert git.changed_files(repo)[0].status == "?"
def test_rename_in_commit(repo):
sh(repo, "mv", "b.txt", "moved.txt")
sh(repo, "commit", "-q", "-m", "rename")
_, files = git.commit_with_files(repo, "HEAD")
assert files == [git.FileChange("moved.txt", "R", False, 0, 0, "b.txt")]
def test_git_error_message(repo):
with pytest.raises(git.GitError) as info:
git.run(repo, ["rev-parse", "--verify", "nope"])
assert "\n" not in str(info.value) and info.value.stderr
def test_count_lines(tmp_path):
p = str(tmp_path / "f")
open(p, "wb").write(b"a\n" * 3000000)
assert git._count_lines(p) == 3000000
open(p, "wb").write(b"x\0y")
assert git._count_lines(p) is None
open(p, "wb").write(b"")
assert git._count_lines(p) == 0

117
tests/test_parse.py Normal file
View File

@@ -0,0 +1,117 @@
from vdiff import parse
TWO_HUNKS = """diff --git a/x.py b/x.py
index 111..222 100644
--- a/x.py
+++ b/x.py
@@ -1,3 +1,3 @@
a
-b
+B
c
@@ -10,2 +10,3 @@
j
+k
l
"""
NO_NEWLINE = """--- a/t.txt
+++ b/t.txt
@@ -1 +1 @@
-old
\\ No newline at end of file
+new
\\ No newline at end of file
"""
UNTRACKED = """diff --git a/dev/null b/n.py
new file mode 100644
--- /dev/null
+++ b/n.py
@@ -0,0 +1,2 @@
+one
+two
"""
DELETED = """--- a/gone.py
+++ /dev/null
@@ -1,2 +0,0 @@
-one
-two
"""
BINARY = """diff --git a/i.png b/i.png
index 111..222 100644
Binary files a/i.png and b/i.png differ
"""
EMPTY_CONTEXT = """--- a/e.py
+++ b/e.py
@@ -1,3 +1,3 @@
x
-\ty
+\tz
"""
def test_two_hunks_numbering():
diff = parse.parse_unified(TWO_HUNKS)
kinds = [l.kind for l in diff.lines]
assert kinds == [" ", "-", "+", " ", " ", "+", " "]
assert diff.lines[1] == parse.Line("-", 2, None, "b")
assert diff.lines[2] == parse.Line("+", None, 2, "B")
assert diff.lines[3] == parse.Line(" ", 3, 3, "c")
assert diff.lines[4] == parse.Line(" ", 10, 10, "j")
assert diff.lines[5] == parse.Line("+", None, 11, "k")
assert diff.lines[6] == parse.Line(" ", 11, 12, "l")
assert not diff.binary
assert parse.counts(diff.lines) == (2, 1)
def test_no_newline_marker_ignored():
diff = parse.parse_unified(NO_NEWLINE)
assert [l.text for l in diff.lines] == ["old", "new"]
def test_untracked_and_deleted_files():
new = parse.parse_unified(UNTRACKED)
assert [l.new_no for l in new.lines] == [1, 2] and all(l.kind == "+" for l in new.lines)
gone = parse.parse_unified(DELETED)
assert all(l.kind == "-" for l in gone.lines) and [l.old_no for l in gone.lines] == [1, 2]
def test_binary():
diff = parse.parse_unified(BINARY)
assert diff.binary and diff.lines == []
def test_empty_context_line_and_tabs():
diff = parse.parse_unified(EMPTY_CONTEXT)
assert [l.text for l in diff.lines] == ["x", "", "\ty", "\tz"]
def test_change_blocks_and_pairs():
lines = parse.parse_unified(TWO_HUNKS).lines
blocks = parse.change_blocks(lines)
assert blocks == [(1, 3), (5, 6)]
assert parse.paired_lines(lines, blocks[0]) == [(1, 2)]
assert parse.paired_lines(lines, blocks[1]) == []
def test_char_ranges():
old_r, new_r = parse.char_ranges(" self.viewport = xy", " self.viewport = (x, y)")
assert old_r == []
assert new_r == [(20, 21), (22, 24), (25, 26)]
assert parse.char_ranges("foo = bar", "foo = baz") == ([(8, 9)], [(8, 9)])
assert parse.char_ranges("completely", "different!!!") == ([], [])
assert parse.char_ranges("a" * 1500, "a" * 1499 + "b") == ([], [])
def test_line_for_open():
lines = parse.parse_unified(TWO_HUNKS).lines
assert parse.line_for_open(lines, 1) == 2 # deleted row -> following line
assert parse.line_for_open(lines, 6) == 12
assert parse.line_for_open([], 5) == 1
only_deleted = parse.parse_unified(DELETED).lines
assert parse.line_for_open(only_deleted, 1) == 1

112
tests/test_render.py Normal file
View File

@@ -0,0 +1,112 @@
import sublime # the stub, via conftest
from vdiff import commands, git, listeners, parse, render, state # noqa: F401
DIFF = """--- a/x.py
+++ b/x.py
@@ -1,6 +1,7 @@
def f():
- return 1
-
+ return 10
+ # added
+ # more
def g():
pass
"""
def make_state(diff_text=DIFF):
st = state.ViewState("/repo")
st.change = git.FileChange("x.py", "M", False, 3, 2, None)
diff = parse.parse_unified(diff_text)
st.lines, st.binary = diff.lines, diff.binary
st.blocks = parse.change_blocks(diff.lines)
return st
def header(view):
return view.phantoms[render.PHANTOM_KEY][0].content
def test_render_text_regions_and_header():
view = sublime.View()
render.init_view(view)
st = make_state()
render.render(view, st)
assert view.text.split("\n") == ["", "def f():", " return 1", "", " return 10", " # added", " # more", "", "def g():", " pass"]
assert view.is_read_only()
assert view.syntax.endswith("Python.sublime-syntax")
deleted, scope, _, flags = view.regions[render.KEY_DEL]
assert scope == "diff.deleted" and flags == sublime.DRAW_NO_OUTLINE
# rows 2-3 merged into one region, newline included; offsets match the view's own row arithmetic
assert deleted == [sublime.Region(view.text_point(2, 0), view.text_point(4, 0))]
inserted = view.regions[render.KEY_INS][0]
assert inserted == [sublime.Region(view.text_point(4, 0), view.text_point(7, 0))]
marks = view.regions[render.KEY_INS_GUTTER]
assert [r.a for r in marks[0]] == [view.text_point(4, 0), view.text_point(5, 0), view.text_point(6, 0)]
assert marks[2] == "dot"
# 2 deleted vs 3 inserted lines: not a paired block, no char marks
assert view.regions[render.KEY_INS_CHAR][0] == []
html = header(view)
assert "x.py" in html and "-2" in html and "+3" in html and 'href="stage"' in html and "modified" in html
assert len(view.phantoms[render.PHANTOM_KEY]) == 1
assert view.name() == "⟐ x.py"
assert view.settings().get(state.VIEW_DATA) == {"repo": "/repo", "sha": None, "path": "x.py", "status": "M", "staged": False}
assert view.sel()[0].a == view.text_point(1, 0)
def test_last_line_region_stops_at_buffer_end():
view = sublime.View()
render.init_view(view)
st = make_state("--- a/y\n+++ b/y\n@@ -1,2 +1,2 @@\n a\n-b\n+c\n")
render.render(view, st)
assert view.text == "\na\nb\nc"
assert view.regions[render.KEY_INS][0] == [sublime.Region(view.text_point(3, 0), len(view.text))]
render.render(view, st) # re-render replaces, never duplicates, the header
assert len(view.phantoms[render.PHANTOM_KEY]) == 1
def test_char_marks_for_paired_lines():
view = sublime.View()
render.init_view(view)
st = make_state("--- a/y\n+++ b/y\n@@ -1 +1 @@\n-foo = bar\n+foo = baz\n")
render.render(view, st)
assert view.regions[render.KEY_DEL_CHAR][0] == [sublime.Region(view.text_point(1, 8), view.text_point(1, 9))]
assert view.regions[render.KEY_INS_CHAR][0] == [sublime.Region(view.text_point(2, 8), view.text_point(2, 9))]
def test_commit_header_and_message():
view = sublime.View()
render.init_view(view)
st = make_state()
st.enter("abc", git.CommitInfo("abc", "abc1234", "Ann <x>", "2026-09-23 10:00", "Fix things", "details\nmore"))
st.change = git.FileChange("x.py", "M", False, 3, 2, None)
render.render(view, st)
html = header(view)
assert "abc1234" in html and "Ann &lt;x&gt;" in html and "Fix things" in html and "details" in html
assert 'href="stage"' not in html and 'href="open"' in html
render.render_message(view, st, "nothing")
assert view.text == "\nnothing" and st.change is None and view.name() == "vDiff"
def test_binary_and_caret_index():
view = sublime.View()
render.init_view(view)
st = make_state()
st.binary, st.lines, st.blocks = True, [], []
render.render(view, st)
assert view.text == "\n(binary file)" and render.KEY_DEL not in view.regions
view.sel().clear()
view.sel().add(sublime.Region(render.index_point(view, 0)))
assert render.caret_index(view) == 0
view.sel().clear()
view.sel().add(sublime.Region(0))
assert render.caret_index(view) == -1