Remove the python diff kitten
This commit is contained in:
parent
fb9d95038d
commit
d30091034a
@ -163,15 +163,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
"name": "pygments",
|
|
||||||
"unix": {
|
|
||||||
"filename": "Pygments-2.11.2.tar.gz",
|
|
||||||
"hash": "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a",
|
|
||||||
"urls": ["pypi"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "libpng",
|
"name": "libpng",
|
||||||
"unix": {
|
"unix": {
|
||||||
|
|||||||
@ -40,7 +40,6 @@ Run-time dependencies:
|
|||||||
* ``fontconfig`` (not needed on macOS)
|
* ``fontconfig`` (not needed on macOS)
|
||||||
* ``libcanberra`` (not needed on macOS)
|
* ``libcanberra`` (not needed on macOS)
|
||||||
* ``ImageMagick`` (optional, needed to display uncommon image formats in the terminal)
|
* ``ImageMagick`` (optional, needed to display uncommon image formats in the terminal)
|
||||||
* ``pygments`` (optional, needed for syntax highlighting in ``kitty +kitten diff``)
|
|
||||||
|
|
||||||
|
|
||||||
Build-time dependencies:
|
Build-time dependencies:
|
||||||
|
|||||||
@ -31,11 +31,7 @@ Major Features
|
|||||||
Installation
|
Installation
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
Simply :ref:`install kitty <quickstart>`. You also need to have either the `git
|
Simply :ref:`install kitty <quickstart>`.
|
||||||
<https://git-scm.com/>`__ program or the :program:`diff` program installed.
|
|
||||||
Additionally, for syntax highlighting to work, `pygments
|
|
||||||
<https://pygments.org/>`__ must be installed (note that pygments is included in
|
|
||||||
the official kitty binary builds).
|
|
||||||
|
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
See https://sw.kovidgoyal.net/kitty/kittens/diff/
|
|
||||||
@ -1,8 +1,5 @@
|
|||||||
class GlobalData:
|
from typing import Dict
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.title = ''
|
|
||||||
self.cmd = ''
|
|
||||||
|
|
||||||
|
|
||||||
global_data = GlobalData
|
def syntax_aliases(x: str) -> Dict[str, str]:
|
||||||
|
return {}
|
||||||
|
|||||||
@ -1,233 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
|
||||||
|
|
||||||
import os
|
|
||||||
from contextlib import suppress
|
|
||||||
from fnmatch import fnmatch
|
|
||||||
from functools import lru_cache
|
|
||||||
from hashlib import md5
|
|
||||||
from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Union
|
|
||||||
|
|
||||||
from kitty.guess_mime_type import guess_type
|
|
||||||
from kitty.utils import control_codes_pat
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .highlight import DiffHighlight
|
|
||||||
|
|
||||||
|
|
||||||
path_name_map: Dict[str, str] = {}
|
|
||||||
remote_dirs: Dict[str, str] = {}
|
|
||||||
|
|
||||||
|
|
||||||
def add_remote_dir(val: str) -> None:
|
|
||||||
remote_dirs[val] = os.path.basename(val).rpartition('-')[-1]
|
|
||||||
|
|
||||||
|
|
||||||
class Segment:
|
|
||||||
|
|
||||||
__slots__ = ('start', 'end', 'start_code', 'end_code')
|
|
||||||
|
|
||||||
def __init__(self, start: int, start_code: str):
|
|
||||||
self.start = start
|
|
||||||
self.start_code = start_code
|
|
||||||
self.end: Optional[int] = None
|
|
||||||
self.end_code: Optional[str] = None
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f'Segment(start={self.start!r}, start_code={self.start_code!r}, end={self.end!r}, end_code={self.end_code!r})'
|
|
||||||
|
|
||||||
|
|
||||||
class Collection:
|
|
||||||
|
|
||||||
ignore_names: Tuple[str, ...] = ()
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.changes: Dict[str, str] = {}
|
|
||||||
self.renames: Dict[str, str] = {}
|
|
||||||
self.adds: Set[str] = set()
|
|
||||||
self.removes: Set[str] = set()
|
|
||||||
self.all_paths: List[str] = []
|
|
||||||
self.type_map: Dict[str, str] = {}
|
|
||||||
self.added_count = self.removed_count = 0
|
|
||||||
|
|
||||||
def add_change(self, left_path: str, right_path: str) -> None:
|
|
||||||
self.changes[left_path] = right_path
|
|
||||||
self.all_paths.append(left_path)
|
|
||||||
self.type_map[left_path] = 'diff'
|
|
||||||
|
|
||||||
def add_rename(self, left_path: str, right_path: str) -> None:
|
|
||||||
self.renames[left_path] = right_path
|
|
||||||
self.all_paths.append(left_path)
|
|
||||||
self.type_map[left_path] = 'rename'
|
|
||||||
|
|
||||||
def add_add(self, right_path: str) -> None:
|
|
||||||
self.adds.add(right_path)
|
|
||||||
self.all_paths.append(right_path)
|
|
||||||
self.type_map[right_path] = 'add'
|
|
||||||
if isinstance(data_for_path(right_path), str):
|
|
||||||
self.added_count += len(lines_for_path(right_path))
|
|
||||||
|
|
||||||
def add_removal(self, left_path: str) -> None:
|
|
||||||
self.removes.add(left_path)
|
|
||||||
self.all_paths.append(left_path)
|
|
||||||
self.type_map[left_path] = 'removal'
|
|
||||||
if isinstance(data_for_path(left_path), str):
|
|
||||||
self.removed_count += len(lines_for_path(left_path))
|
|
||||||
|
|
||||||
def finalize(self) -> None:
|
|
||||||
def key(x: str) -> str:
|
|
||||||
return path_name_map.get(x, '')
|
|
||||||
|
|
||||||
self.all_paths.sort(key=key)
|
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[Tuple[str, str, Optional[str]]]:
|
|
||||||
for path in self.all_paths:
|
|
||||||
typ = self.type_map[path]
|
|
||||||
if typ == 'diff':
|
|
||||||
data: Optional[str] = self.changes[path]
|
|
||||||
elif typ == 'rename':
|
|
||||||
data = self.renames[path]
|
|
||||||
else:
|
|
||||||
data = None
|
|
||||||
yield path, typ, data
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self.all_paths)
|
|
||||||
|
|
||||||
|
|
||||||
def remote_hostname(path: str) -> Tuple[Optional[str], Optional[str]]:
|
|
||||||
for q in remote_dirs:
|
|
||||||
if path.startswith(q):
|
|
||||||
return q, remote_dirs[q]
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_remote_name(path: str, default: str) -> str:
|
|
||||||
remote_dir, rh = remote_hostname(path)
|
|
||||||
if remote_dir and rh:
|
|
||||||
return f'{rh}:{os.path.relpath(path, remote_dir)}'
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
def allowed_items(items: Sequence[str], ignore_patterns: Sequence[str]) -> Iterator[str]:
|
|
||||||
for name in items:
|
|
||||||
for pat in ignore_patterns:
|
|
||||||
if fnmatch(name, pat):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
yield name
|
|
||||||
|
|
||||||
|
|
||||||
def walk(base: str, names: Set[str], pmap: Dict[str, str], ignore_names: Tuple[str, ...]) -> None:
|
|
||||||
for dirpath, dirnames, filenames in os.walk(base):
|
|
||||||
dirnames[:] = allowed_items(dirnames, ignore_names)
|
|
||||||
for filename in allowed_items(filenames, ignore_names):
|
|
||||||
path = os.path.abspath(os.path.join(dirpath, filename))
|
|
||||||
path_name_map[path] = name = os.path.relpath(path, base)
|
|
||||||
names.add(name)
|
|
||||||
pmap[name] = path
|
|
||||||
|
|
||||||
|
|
||||||
def collect_files(collection: Collection, left: str, right: str) -> None:
|
|
||||||
left_names: Set[str] = set()
|
|
||||||
right_names: Set[str] = set()
|
|
||||||
left_path_map: Dict[str, str] = {}
|
|
||||||
right_path_map: Dict[str, str] = {}
|
|
||||||
|
|
||||||
walk(left, left_names, left_path_map, collection.ignore_names)
|
|
||||||
walk(right, right_names, right_path_map, collection.ignore_names)
|
|
||||||
|
|
||||||
common_names = left_names & right_names
|
|
||||||
changed_names = {n for n in common_names if data_for_path(left_path_map[n]) != data_for_path(right_path_map[n])}
|
|
||||||
for n in changed_names:
|
|
||||||
collection.add_change(left_path_map[n], right_path_map[n])
|
|
||||||
|
|
||||||
removed = left_names - common_names
|
|
||||||
added = right_names - common_names
|
|
||||||
ahash = {a: hash_for_path(right_path_map[a]) for a in added}
|
|
||||||
rhash = {r: hash_for_path(left_path_map[r]) for r in removed}
|
|
||||||
for name, rh in rhash.items():
|
|
||||||
for n, ah in ahash.items():
|
|
||||||
if ah == rh and data_for_path(left_path_map[name]) == data_for_path(right_path_map[n]):
|
|
||||||
collection.add_rename(left_path_map[name], right_path_map[n])
|
|
||||||
added.discard(n)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
collection.add_removal(left_path_map[name])
|
|
||||||
|
|
||||||
for name in added:
|
|
||||||
collection.add_add(right_path_map[name])
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize(text: str) -> str:
|
|
||||||
ntext = text.replace('\r\n', '⏎\n')
|
|
||||||
return control_codes_pat().sub('░', ntext)
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1024)
|
|
||||||
def mime_type_for_path(path: str) -> str:
|
|
||||||
return guess_type(path, allow_filesystem_access=True) or 'application/octet-stream'
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1024)
|
|
||||||
def raw_data_for_path(path: str) -> bytes:
|
|
||||||
with open(path, 'rb') as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
|
|
||||||
def is_image(path: Optional[str]) -> bool:
|
|
||||||
return mime_type_for_path(path).startswith('image/') if path else False
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1024)
|
|
||||||
def data_for_path(path: str) -> Union[str, bytes]:
|
|
||||||
raw_bytes = raw_data_for_path(path)
|
|
||||||
if not is_image(path) and not os.path.samefile(path, os.devnull):
|
|
||||||
with suppress(UnicodeDecodeError):
|
|
||||||
return raw_bytes.decode('utf-8')
|
|
||||||
return raw_bytes
|
|
||||||
|
|
||||||
|
|
||||||
class LinesForPath:
|
|
||||||
|
|
||||||
replace_tab_by = ' ' * 4
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1024)
|
|
||||||
def __call__(self, path: str) -> Tuple[str, ...]:
|
|
||||||
data = data_for_path(path)
|
|
||||||
assert isinstance(data, str)
|
|
||||||
data = data.replace('\t', self.replace_tab_by)
|
|
||||||
return tuple(sanitize(data).splitlines())
|
|
||||||
|
|
||||||
|
|
||||||
lines_for_path = LinesForPath()
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1024)
|
|
||||||
def hash_for_path(path: str) -> bytes:
|
|
||||||
return md5(raw_data_for_path(path)).digest()
|
|
||||||
|
|
||||||
|
|
||||||
def create_collection(left: str, right: str) -> Collection:
|
|
||||||
collection = Collection()
|
|
||||||
if os.path.isdir(left):
|
|
||||||
collect_files(collection, left, right)
|
|
||||||
else:
|
|
||||||
pl, pr = os.path.abspath(left), os.path.abspath(right)
|
|
||||||
path_name_map[pl] = resolve_remote_name(pl, left)
|
|
||||||
path_name_map[pr] = resolve_remote_name(pr, right)
|
|
||||||
collection.add_change(pl, pr)
|
|
||||||
collection.finalize()
|
|
||||||
return collection
|
|
||||||
|
|
||||||
|
|
||||||
highlight_data: Dict[str, 'DiffHighlight'] = {}
|
|
||||||
|
|
||||||
|
|
||||||
def set_highlight_data(data: Dict[str, 'DiffHighlight']) -> None:
|
|
||||||
global highlight_data
|
|
||||||
highlight_data = data
|
|
||||||
|
|
||||||
|
|
||||||
def highlights_for_path(path: str) -> 'DiffHighlight':
|
|
||||||
return highlight_data.get(path, [])
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
|
||||||
|
|
||||||
import os
|
|
||||||
from typing import Any, Dict, Iterable, Optional
|
|
||||||
|
|
||||||
from kitty.cli_stub import DiffCLIOptions
|
|
||||||
from kitty.conf.utils import load_config as _load_config
|
|
||||||
from kitty.conf.utils import parse_config_base, resolve_config
|
|
||||||
from kitty.constants import config_dir
|
|
||||||
from kitty.rgb import color_as_sgr
|
|
||||||
|
|
||||||
from .options.types import Options as DiffOptions
|
|
||||||
from .options.types import defaults
|
|
||||||
|
|
||||||
formats: Dict[str, str] = {
|
|
||||||
'title': '',
|
|
||||||
'margin': '',
|
|
||||||
'text': '',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def set_formats(opts: DiffOptions) -> None:
|
|
||||||
formats['text'] = '48' + color_as_sgr(opts.background)
|
|
||||||
formats['title'] = '38' + color_as_sgr(opts.title_fg) + ';48' + color_as_sgr(opts.title_bg) + ';1'
|
|
||||||
formats['margin'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.margin_bg)
|
|
||||||
formats['added_margin'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.added_margin_bg)
|
|
||||||
formats['removed_margin'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.removed_margin_bg)
|
|
||||||
formats['added'] = '48' + color_as_sgr(opts.added_bg)
|
|
||||||
formats['removed'] = '48' + color_as_sgr(opts.removed_bg)
|
|
||||||
formats['filler'] = '48' + color_as_sgr(opts.filler_bg)
|
|
||||||
formats['margin_filler'] = '48' + color_as_sgr(opts.margin_filler_bg or opts.filler_bg)
|
|
||||||
formats['hunk_margin'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.hunk_margin_bg)
|
|
||||||
formats['hunk'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.hunk_bg)
|
|
||||||
formats['removed_highlight'] = '48' + color_as_sgr(opts.highlight_removed_bg)
|
|
||||||
formats['added_highlight'] = '48' + color_as_sgr(opts.highlight_added_bg)
|
|
||||||
|
|
||||||
|
|
||||||
SYSTEM_CONF = '/etc/xdg/kitty/diff.conf'
|
|
||||||
defconf = os.path.join(config_dir, 'diff.conf')
|
|
||||||
|
|
||||||
|
|
||||||
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None) -> DiffOptions:
|
|
||||||
from .options.parse import create_result_dict, merge_result_dicts, parse_conf_item
|
|
||||||
|
|
||||||
def parse_config(lines: Iterable[str]) -> Dict[str, Any]:
|
|
||||||
ans: Dict[str, Any] = create_result_dict()
|
|
||||||
parse_config_base(
|
|
||||||
lines,
|
|
||||||
parse_conf_item,
|
|
||||||
ans,
|
|
||||||
)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
overrides = tuple(overrides) if overrides is not None else ()
|
|
||||||
opts_dict, paths = _load_config(defaults, parse_config, merge_result_dicts, *paths, overrides=overrides)
|
|
||||||
opts = DiffOptions(opts_dict)
|
|
||||||
opts.config_paths = paths
|
|
||||||
opts.config_overrides = overrides
|
|
||||||
return opts
|
|
||||||
|
|
||||||
|
|
||||||
def init_config(args: DiffCLIOptions) -> DiffOptions:
|
|
||||||
config = tuple(resolve_config(SYSTEM_CONF, defconf, args.config))
|
|
||||||
overrides = (a.replace('=', ' ', 1) for a in args.override or ())
|
|
||||||
opts = load_config(*config, overrides=overrides)
|
|
||||||
set_formats(opts)
|
|
||||||
for (sc, action) in opts.map:
|
|
||||||
opts.key_definitions[sc] = action
|
|
||||||
return opts
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
from typing import Callable, List, Optional, Tuple
|
|
||||||
|
|
||||||
from .collect import Segment
|
|
||||||
|
|
||||||
def splitlines_like_git(raw: bytes, callback: Callable[[memoryview], None]) -> None: ...
|
|
||||||
|
|
||||||
def split_with_highlights(
|
|
||||||
line: str, truncate_points: List[int], fg_highlights: List[Segment],
|
|
||||||
bg_highlight: Optional[Segment]
|
|
||||||
) -> List[str]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def changed_center(left_prefix: str, right_postfix: str) -> Tuple[int, int]:
|
|
||||||
pass
|
|
||||||
@ -1,185 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
|
||||||
|
|
||||||
import concurrent
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
|
||||||
from typing import IO, Dict, Iterable, Iterator, List, Optional, Tuple, Union, cast
|
|
||||||
|
|
||||||
from pygments import highlight # type: ignore
|
|
||||||
from pygments.formatter import Formatter # type: ignore
|
|
||||||
from pygments.lexers import get_lexer_for_filename # type: ignore
|
|
||||||
from pygments.util import ClassNotFound # type: ignore
|
|
||||||
|
|
||||||
from kitty.multiprocessing import get_process_pool_executor
|
|
||||||
from kitty.rgb import color_as_sgr, parse_sharp
|
|
||||||
|
|
||||||
from .collect import Collection, Segment, data_for_path, lines_for_path
|
|
||||||
|
|
||||||
|
|
||||||
class StyleNotFound(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class DiffFormatter(Formatter): # type: ignore
|
|
||||||
|
|
||||||
def __init__(self, style: str = 'default') -> None:
|
|
||||||
try:
|
|
||||||
Formatter.__init__(self, style=style)
|
|
||||||
initialized = True
|
|
||||||
except ClassNotFound:
|
|
||||||
initialized = False
|
|
||||||
if not initialized:
|
|
||||||
raise StyleNotFound(f'pygments style "{style}" not found')
|
|
||||||
|
|
||||||
self.styles: Dict[str, Tuple[str, str]] = {}
|
|
||||||
for token, token_style in self.style:
|
|
||||||
start = []
|
|
||||||
end = []
|
|
||||||
fstart = fend = ''
|
|
||||||
# a style item is a tuple in the following form:
|
|
||||||
# colors are readily specified in hex: 'RRGGBB'
|
|
||||||
col = token_style['color']
|
|
||||||
if col:
|
|
||||||
pc = parse_sharp(col)
|
|
||||||
if pc is not None:
|
|
||||||
start.append('38' + color_as_sgr(pc))
|
|
||||||
end.append('39')
|
|
||||||
if token_style['bold']:
|
|
||||||
start.append('1')
|
|
||||||
end.append('22')
|
|
||||||
if token_style['italic']:
|
|
||||||
start.append('3')
|
|
||||||
end.append('23')
|
|
||||||
if token_style['underline']:
|
|
||||||
start.append('4')
|
|
||||||
end.append('24')
|
|
||||||
if start:
|
|
||||||
fstart = '\033[{}m'.format(';'.join(start))
|
|
||||||
fend = '\033[{}m'.format(';'.join(end))
|
|
||||||
self.styles[token] = fstart, fend
|
|
||||||
|
|
||||||
def format(self, tokensource: Iterable[Tuple[str, str]], outfile: IO[str]) -> None:
|
|
||||||
for ttype, value in tokensource:
|
|
||||||
not_found = True
|
|
||||||
if value.rstrip('\n'):
|
|
||||||
while ttype and not_found:
|
|
||||||
tok = self.styles.get(ttype)
|
|
||||||
if tok is None:
|
|
||||||
ttype = ttype[:-1]
|
|
||||||
else:
|
|
||||||
on, off = tok
|
|
||||||
lines = value.split('\n')
|
|
||||||
for line in lines:
|
|
||||||
if line:
|
|
||||||
outfile.write(on + line + off)
|
|
||||||
if line is not lines[-1]:
|
|
||||||
outfile.write('\n')
|
|
||||||
not_found = False
|
|
||||||
|
|
||||||
if not_found:
|
|
||||||
outfile.write(value)
|
|
||||||
|
|
||||||
|
|
||||||
formatter: Optional[DiffFormatter] = None
|
|
||||||
|
|
||||||
|
|
||||||
def initialize_highlighter(style: str = 'default') -> None:
|
|
||||||
global formatter
|
|
||||||
formatter = DiffFormatter(style)
|
|
||||||
|
|
||||||
|
|
||||||
def highlight_data(code: str, filename: str, aliases: Optional[Dict[str, str]] = None) -> Optional[str]:
|
|
||||||
if aliases:
|
|
||||||
base, ext = os.path.splitext(filename)
|
|
||||||
alias = aliases.get(ext[1:])
|
|
||||||
if alias is not None:
|
|
||||||
filename = f'{base}.{alias}'
|
|
||||||
try:
|
|
||||||
lexer = get_lexer_for_filename(filename, stripnl=False)
|
|
||||||
except ClassNotFound:
|
|
||||||
return None
|
|
||||||
return cast(str, highlight(code, lexer, formatter))
|
|
||||||
|
|
||||||
|
|
||||||
split_pat = re.compile(r'(\033\[.*?m)')
|
|
||||||
|
|
||||||
|
|
||||||
def highlight_line(line: str) -> List[Segment]:
|
|
||||||
ans: List[Segment] = []
|
|
||||||
current: Optional[Segment] = None
|
|
||||||
pos = 0
|
|
||||||
for x in split_pat.split(line):
|
|
||||||
if x.startswith('\033'):
|
|
||||||
if current is None:
|
|
||||||
current = Segment(pos, x)
|
|
||||||
else:
|
|
||||||
current.end = pos
|
|
||||||
current.end_code = x
|
|
||||||
ans.append(current)
|
|
||||||
current = None
|
|
||||||
else:
|
|
||||||
pos += len(x)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
DiffHighlight = List[List[Segment]]
|
|
||||||
|
|
||||||
|
|
||||||
def highlight_for_diff(path: str, aliases: Dict[str, str]) -> DiffHighlight:
|
|
||||||
ans: DiffHighlight = []
|
|
||||||
lines = lines_for_path(path)
|
|
||||||
hd = highlight_data('\n'.join(lines), path, aliases)
|
|
||||||
if hd is not None:
|
|
||||||
for line in hd.splitlines():
|
|
||||||
ans.append(highlight_line(line))
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
process_pool_executor: Optional[ProcessPoolExecutor] = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_highlight_processes() -> Iterator[int]:
|
|
||||||
if process_pool_executor is None:
|
|
||||||
return
|
|
||||||
for pid in process_pool_executor._processes:
|
|
||||||
yield pid
|
|
||||||
|
|
||||||
|
|
||||||
def highlight_collection(collection: Collection, aliases: Optional[Dict[str, str]] = None) -> Union[str, Dict[str, DiffHighlight]]:
|
|
||||||
global process_pool_executor
|
|
||||||
jobs = {}
|
|
||||||
ans: Dict[str, DiffHighlight] = {}
|
|
||||||
with get_process_pool_executor(prefer_fork=True) as executor:
|
|
||||||
process_pool_executor = executor
|
|
||||||
for path, item_type, other_path in collection:
|
|
||||||
if item_type != 'rename':
|
|
||||||
for p in (path, other_path):
|
|
||||||
if p:
|
|
||||||
is_binary = isinstance(data_for_path(p), bytes)
|
|
||||||
if not is_binary:
|
|
||||||
jobs[executor.submit(highlight_for_diff, p, aliases or {})] = p
|
|
||||||
for future in concurrent.futures.as_completed(jobs):
|
|
||||||
path = jobs[future]
|
|
||||||
try:
|
|
||||||
highlights = future.result()
|
|
||||||
except Exception as e:
|
|
||||||
import traceback
|
|
||||||
tb = traceback.format_exc()
|
|
||||||
return f'Running syntax highlighting for {path} generated an exception: {e} with traceback:\n{tb}'
|
|
||||||
ans[path] = highlights
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
# kitty +runpy "from kittens.diff.highlight import main; main()" file
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from .options.types import defaults
|
|
||||||
initialize_highlighter()
|
|
||||||
with open(sys.argv[-1]) as f:
|
|
||||||
highlighted = highlight_data(f.read(), f.name, defaults.syntax_aliases)
|
|
||||||
if highlighted is None:
|
|
||||||
raise SystemExit(f'Unknown filetype: {sys.argv[-1]}')
|
|
||||||
print(highlighted)
|
|
||||||
@ -1,591 +1,272 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
import atexit
|
|
||||||
import os
|
|
||||||
import signal
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
import warnings
|
|
||||||
from collections import defaultdict
|
|
||||||
from contextlib import suppress
|
|
||||||
from enum import Enum, auto
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from gettext import gettext as _
|
from typing import List
|
||||||
from typing import (
|
|
||||||
Any,
|
|
||||||
DefaultDict,
|
|
||||||
Dict,
|
|
||||||
Iterable,
|
|
||||||
Iterator,
|
|
||||||
List,
|
|
||||||
Optional,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
)
|
|
||||||
|
|
||||||
from kitty.cli import CONFIG_HELP, CompletionSpec, parse_args
|
from kitty.cli import CONFIG_HELP, CompletionSpec
|
||||||
from kitty.cli_stub import DiffCLIOptions
|
from kitty.conf.types import Definition
|
||||||
from kitty.conf.utils import KeyAction
|
|
||||||
from kitty.constants import appname
|
from kitty.constants import appname
|
||||||
from kitty.fast_data_types import wcswidth
|
|
||||||
from kitty.key_encoding import EventType, KeyEvent
|
|
||||||
from kitty.utils import ScreenSize, extract_all_from_tarfile_safely
|
|
||||||
|
|
||||||
from ..tui.handler import Handler
|
|
||||||
from ..tui.images import ImageManager, Placement
|
def main(args: List[str]) -> None:
|
||||||
from ..tui.line_edit import LineEdit
|
raise SystemExit('Must be run as kitten diff')
|
||||||
from ..tui.loop import Loop
|
|
||||||
from ..tui.operations import styled
|
definition = Definition(
|
||||||
from . import global_data
|
'!kittens.diff',
|
||||||
from .collect import (
|
|
||||||
Collection,
|
|
||||||
add_remote_dir,
|
|
||||||
create_collection,
|
|
||||||
data_for_path,
|
|
||||||
lines_for_path,
|
|
||||||
sanitize,
|
|
||||||
set_highlight_data,
|
|
||||||
)
|
)
|
||||||
from .config import init_config
|
|
||||||
from .options.types import Options as DiffOptions
|
agr = definition.add_group
|
||||||
from .patch import Differ, Patch, set_diff_command, worker_processes
|
egr = definition.end_group
|
||||||
from .render import (
|
opt = definition.add_option
|
||||||
ImagePlacement,
|
map = definition.add_map
|
||||||
ImageSupportWarning,
|
mma = definition.add_mouse_map
|
||||||
Line,
|
|
||||||
LineRef,
|
# diff {{{
|
||||||
Reference,
|
agr('diff', 'Diffing')
|
||||||
render_diff,
|
|
||||||
|
opt('syntax_aliases', 'pyj:py pyi:py recipe:py', ctype='strdict_ _:', option_type='syntax_aliases',
|
||||||
|
long_text='''
|
||||||
|
File extension aliases for syntax highlight. For example, to syntax highlight
|
||||||
|
:file:`file.xyz` as :file:`file.abc` use a setting of :code:`xyz:abc`.
|
||||||
|
Multiple aliases must be separated by spaces.
|
||||||
|
'''
|
||||||
)
|
)
|
||||||
from .search import BadRegex, Search
|
|
||||||
|
|
||||||
try:
|
opt('num_context_lines', '3', option_type='positive_int',
|
||||||
from .highlight import (
|
long_text='The number of lines of context to show around each change.'
|
||||||
DiffHighlight,
|
|
||||||
get_highlight_processes,
|
|
||||||
highlight_collection,
|
|
||||||
initialize_highlighter,
|
|
||||||
)
|
)
|
||||||
has_highlighter = True
|
|
||||||
DiffHighlight
|
|
||||||
except ImportError:
|
|
||||||
has_highlighter = False
|
|
||||||
|
|
||||||
def highlight_collection(collection: 'Collection', aliases: Optional[Dict[str, str]] = None) -> Union[str, Dict[str, 'DiffHighlight']]:
|
opt('diff_cmd', 'auto',
|
||||||
return ''
|
long_text='''
|
||||||
|
The diff command to use. Must contain the placeholder :code:`_CONTEXT_` which
|
||||||
def get_highlight_processes() -> Iterator[int]:
|
will be replaced by the number of lines of context. A few special values are allowed:
|
||||||
if has_highlighter:
|
:code:`auto` will automatically pick an available diff implementation. :code:`builtin`
|
||||||
yield -1
|
will use the anchored diff algorithm from the Go standard library. :code:`git` will
|
||||||
|
use the git command to do the diffing. :code:`diff` will use the diff command to
|
||||||
|
do the diffing.
|
||||||
class State(Enum):
|
'''
|
||||||
initializing = auto()
|
|
||||||
collected = auto()
|
|
||||||
diffed = auto()
|
|
||||||
command = auto()
|
|
||||||
message = auto()
|
|
||||||
|
|
||||||
|
|
||||||
class BackgroundWork(Enum):
|
|
||||||
none = auto()
|
|
||||||
collecting = auto()
|
|
||||||
diffing = auto()
|
|
||||||
highlighting = auto()
|
|
||||||
|
|
||||||
|
|
||||||
def generate_diff(collection: Collection, context: int) -> Union[str, Dict[str, Patch]]:
|
|
||||||
d = Differ()
|
|
||||||
|
|
||||||
for path, item_type, changed_path in collection:
|
|
||||||
if item_type == 'diff':
|
|
||||||
is_binary = isinstance(data_for_path(path), bytes) or isinstance(data_for_path(changed_path), bytes)
|
|
||||||
if not is_binary:
|
|
||||||
assert changed_path is not None
|
|
||||||
d.add_diff(path, changed_path)
|
|
||||||
|
|
||||||
return d(context)
|
|
||||||
|
|
||||||
|
|
||||||
class DiffHandler(Handler):
|
|
||||||
|
|
||||||
image_manager_class = ImageManager
|
|
||||||
|
|
||||||
def __init__(self, args: DiffCLIOptions, opts: DiffOptions, left: str, right: str) -> None:
|
|
||||||
self.state = State.initializing
|
|
||||||
self.message = ''
|
|
||||||
self.current_search_is_regex = True
|
|
||||||
self.current_search: Optional[Search] = None
|
|
||||||
self.line_edit = LineEdit()
|
|
||||||
self.opts = opts
|
|
||||||
self.left, self.right = left, right
|
|
||||||
self.report_traceback_on_exit: Union[str, Dict[str, Patch], None] = None
|
|
||||||
self.args = args
|
|
||||||
self.scroll_pos = self.max_scroll_pos = 0
|
|
||||||
self.current_context_count = self.original_context_count = self.args.context
|
|
||||||
if self.current_context_count < 0:
|
|
||||||
self.current_context_count = self.original_context_count = self.opts.num_context_lines
|
|
||||||
self.highlighting_done = False
|
|
||||||
self.doing_background_work = BackgroundWork.none
|
|
||||||
self.restore_position: Optional[Reference] = None
|
|
||||||
for key_def, action in self.opts.key_definitions.items():
|
|
||||||
self.add_shortcut(action, key_def)
|
|
||||||
|
|
||||||
def terminate(self, return_code: int = 0) -> None:
|
|
||||||
self.quit_loop(return_code)
|
|
||||||
|
|
||||||
def perform_action(self, action: KeyAction) -> None:
|
|
||||||
func, args = action
|
|
||||||
if func == 'quit':
|
|
||||||
self.terminate()
|
|
||||||
return
|
|
||||||
if self.state.value <= State.diffed.value:
|
|
||||||
if func == 'scroll_by':
|
|
||||||
return self.scroll_lines(int(args[0] or 0))
|
|
||||||
if func == 'scroll_to':
|
|
||||||
where = str(args[0])
|
|
||||||
if 'change' in where:
|
|
||||||
return self.scroll_to_next_change(backwards='prev' in where)
|
|
||||||
if 'match' in where:
|
|
||||||
return self.scroll_to_next_match(backwards='prev' in where)
|
|
||||||
if 'page' in where:
|
|
||||||
amt = self.num_lines * (1 if 'next' in where else -1)
|
|
||||||
else:
|
|
||||||
amt = len(self.diff_lines) * (1 if 'end' in where else -1)
|
|
||||||
return self.scroll_lines(amt)
|
|
||||||
if func == 'change_context':
|
|
||||||
new_ctx = self.current_context_count
|
|
||||||
to = args[0]
|
|
||||||
if to == 'all':
|
|
||||||
new_ctx = 100000
|
|
||||||
elif to == 'default':
|
|
||||||
new_ctx = self.original_context_count
|
|
||||||
else:
|
|
||||||
new_ctx += int(to or 0)
|
|
||||||
return self.change_context_count(new_ctx)
|
|
||||||
if func == 'start_search':
|
|
||||||
self.start_search(bool(args[0]), bool(args[1]))
|
|
||||||
return
|
|
||||||
|
|
||||||
def create_collection(self) -> None:
|
|
||||||
|
|
||||||
def collect_done(collection: Collection) -> None:
|
|
||||||
self.doing_background_work = BackgroundWork.none
|
|
||||||
self.collection = collection
|
|
||||||
self.state = State.collected
|
|
||||||
self.generate_diff()
|
|
||||||
|
|
||||||
def collect(left: str, right: str) -> None:
|
|
||||||
collection = create_collection(left, right)
|
|
||||||
self.asyncio_loop.call_soon_threadsafe(collect_done, collection)
|
|
||||||
|
|
||||||
self.asyncio_loop.run_in_executor(None, collect, self.left, self.right)
|
|
||||||
self.doing_background_work = BackgroundWork.collecting
|
|
||||||
|
|
||||||
def generate_diff(self) -> None:
|
|
||||||
|
|
||||||
def diff_done(diff_map: Union[str, Dict[str, Patch]]) -> None:
|
|
||||||
self.doing_background_work = BackgroundWork.none
|
|
||||||
if isinstance(diff_map, str):
|
|
||||||
self.report_traceback_on_exit = diff_map
|
|
||||||
self.terminate(1)
|
|
||||||
return
|
|
||||||
self.state = State.diffed
|
|
||||||
self.diff_map = diff_map
|
|
||||||
self.calculate_statistics()
|
|
||||||
self.render_diff()
|
|
||||||
self.scroll_pos = 0
|
|
||||||
if self.restore_position is not None:
|
|
||||||
self.current_position = self.restore_position
|
|
||||||
self.restore_position = None
|
|
||||||
self.draw_screen()
|
|
||||||
if has_highlighter and not self.highlighting_done:
|
|
||||||
from .highlight import StyleNotFound
|
|
||||||
self.highlighting_done = True
|
|
||||||
try:
|
|
||||||
initialize_highlighter(self.opts.pygments_style)
|
|
||||||
except StyleNotFound as e:
|
|
||||||
self.report_traceback_on_exit = str(e)
|
|
||||||
self.terminate(1)
|
|
||||||
return
|
|
||||||
self.syntax_highlight()
|
|
||||||
|
|
||||||
def diff(collection: Collection, current_context_count: int) -> None:
|
|
||||||
diff_map = generate_diff(collection, current_context_count)
|
|
||||||
self.asyncio_loop.call_soon_threadsafe(diff_done, diff_map)
|
|
||||||
|
|
||||||
self.asyncio_loop.run_in_executor(None, diff, self.collection, self.current_context_count)
|
|
||||||
self.doing_background_work = BackgroundWork.diffing
|
|
||||||
|
|
||||||
def syntax_highlight(self) -> None:
|
|
||||||
|
|
||||||
def highlighting_done(hdata: Union[str, Dict[str, 'DiffHighlight']]) -> None:
|
|
||||||
self.doing_background_work = BackgroundWork.none
|
|
||||||
if isinstance(hdata, str):
|
|
||||||
self.report_traceback_on_exit = hdata
|
|
||||||
self.terminate(1)
|
|
||||||
return
|
|
||||||
set_highlight_data(hdata)
|
|
||||||
self.render_diff()
|
|
||||||
self.draw_screen()
|
|
||||||
|
|
||||||
def highlight(collection: Collection, aliases: Optional[Dict[str, str]] = None) -> None:
|
|
||||||
result = highlight_collection(collection, aliases)
|
|
||||||
self.asyncio_loop.call_soon_threadsafe(highlighting_done, result)
|
|
||||||
|
|
||||||
self.asyncio_loop.run_in_executor(None, highlight, self.collection, self.opts.syntax_aliases)
|
|
||||||
self.doing_background_work = BackgroundWork.highlighting
|
|
||||||
|
|
||||||
def calculate_statistics(self) -> None:
|
|
||||||
self.added_count = self.collection.added_count
|
|
||||||
self.removed_count = self.collection.removed_count
|
|
||||||
for patch in self.diff_map.values():
|
|
||||||
self.added_count += patch.added_count
|
|
||||||
self.removed_count += patch.removed_count
|
|
||||||
|
|
||||||
def render_diff(self) -> None:
|
|
||||||
self.diff_lines: Tuple[Line, ...] = tuple(render_diff(self.collection, self.diff_map, self.args, self.screen_size.cols, self.image_manager))
|
|
||||||
self.margin_size = render_diff.margin_size
|
|
||||||
self.ref_path_map: DefaultDict[str, List[Tuple[int, Reference]]] = defaultdict(list)
|
|
||||||
for i, dl in enumerate(self.diff_lines):
|
|
||||||
self.ref_path_map[dl.ref.path].append((i, dl.ref))
|
|
||||||
self.max_scroll_pos = len(self.diff_lines) - self.num_lines
|
|
||||||
if self.current_search is not None:
|
|
||||||
self.current_search(self.diff_lines, self.margin_size, self.screen_size.cols)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_position(self) -> Reference:
|
|
||||||
return self.diff_lines[min(len(self.diff_lines) - 1, self.scroll_pos)].ref
|
|
||||||
|
|
||||||
@current_position.setter
|
|
||||||
def current_position(self, ref: Reference) -> None:
|
|
||||||
num = None
|
|
||||||
if isinstance(ref.extra, LineRef):
|
|
||||||
sln = ref.extra.src_line_number
|
|
||||||
for i, q in self.ref_path_map[ref.path]:
|
|
||||||
if isinstance(q.extra, LineRef):
|
|
||||||
if q.extra.src_line_number >= sln:
|
|
||||||
if q.extra.src_line_number == sln:
|
|
||||||
num = i
|
|
||||||
break
|
|
||||||
num = i
|
|
||||||
if num is None:
|
|
||||||
for i, q in self.ref_path_map[ref.path]:
|
|
||||||
num = i
|
|
||||||
break
|
|
||||||
|
|
||||||
if num is not None:
|
|
||||||
self.scroll_pos = max(0, min(num, self.max_scroll_pos))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def num_lines(self) -> int:
|
|
||||||
return self.screen_size.rows - 1
|
|
||||||
|
|
||||||
def scroll_to_next_change(self, backwards: bool = False) -> None:
|
|
||||||
if backwards:
|
|
||||||
r = range(self.scroll_pos - 1, -1, -1)
|
|
||||||
else:
|
|
||||||
r = range(self.scroll_pos + 1, len(self.diff_lines))
|
|
||||||
for i in r:
|
|
||||||
line = self.diff_lines[i]
|
|
||||||
if line.is_change_start:
|
|
||||||
self.scroll_lines(i - self.scroll_pos)
|
|
||||||
return
|
|
||||||
self.cmd.bell()
|
|
||||||
|
|
||||||
def scroll_to_next_match(self, backwards: bool = False, include_current: bool = False) -> None:
|
|
||||||
if self.current_search is not None:
|
|
||||||
offset = 0 if include_current else 1
|
|
||||||
if backwards:
|
|
||||||
r = range(self.scroll_pos - offset, -1, -1)
|
|
||||||
else:
|
|
||||||
r = range(self.scroll_pos + offset, len(self.diff_lines))
|
|
||||||
for i in r:
|
|
||||||
if i in self.current_search:
|
|
||||||
self.scroll_lines(i - self.scroll_pos)
|
|
||||||
return
|
|
||||||
self.cmd.bell()
|
|
||||||
|
|
||||||
def set_scrolling_region(self) -> None:
|
|
||||||
self.cmd.set_scrolling_region(self.screen_size, 0, self.num_lines - 2)
|
|
||||||
|
|
||||||
def scroll_lines(self, amt: int = 1) -> None:
|
|
||||||
new_pos = max(0, min(self.scroll_pos + amt, self.max_scroll_pos))
|
|
||||||
amt = new_pos - self.scroll_pos
|
|
||||||
if new_pos == self.scroll_pos:
|
|
||||||
self.cmd.bell()
|
|
||||||
return
|
|
||||||
if abs(amt) >= self.num_lines - 1:
|
|
||||||
self.scroll_pos = new_pos
|
|
||||||
self.draw_screen()
|
|
||||||
return
|
|
||||||
self.enforce_cursor_state()
|
|
||||||
self.cmd.scroll_screen(amt)
|
|
||||||
self.scroll_pos = new_pos
|
|
||||||
if amt < 0:
|
|
||||||
self.cmd.set_cursor_position(0, 0)
|
|
||||||
self.draw_lines(-amt)
|
|
||||||
else:
|
|
||||||
self.cmd.set_cursor_position(0, self.num_lines - amt)
|
|
||||||
self.draw_lines(amt, self.num_lines - amt)
|
|
||||||
self.draw_status_line()
|
|
||||||
|
|
||||||
def init_terminal_state(self) -> None:
|
|
||||||
self.cmd.set_line_wrapping(False)
|
|
||||||
self.cmd.set_window_title(global_data.title)
|
|
||||||
self.cmd.set_default_colors(
|
|
||||||
fg=self.opts.foreground, bg=self.opts.background,
|
|
||||||
cursor=self.opts.foreground, select_fg=self.opts.select_fg,
|
|
||||||
select_bg=self.opts.select_bg)
|
|
||||||
self.cmd.set_cursor_shape('beam')
|
|
||||||
|
|
||||||
def finalize(self) -> None:
|
|
||||||
self.cmd.set_default_colors()
|
|
||||||
self.cmd.set_cursor_visible(True)
|
|
||||||
self.cmd.set_scrolling_region()
|
|
||||||
|
|
||||||
def initialize(self) -> None:
|
|
||||||
self.init_terminal_state()
|
|
||||||
self.set_scrolling_region()
|
|
||||||
self.draw_screen()
|
|
||||||
self.create_collection()
|
|
||||||
|
|
||||||
def enforce_cursor_state(self) -> None:
|
|
||||||
self.cmd.set_cursor_visible(self.state is State.command)
|
|
||||||
|
|
||||||
def draw_lines(self, num: int, offset: int = 0) -> None:
|
|
||||||
offset += self.scroll_pos
|
|
||||||
image_involved = False
|
|
||||||
limit = len(self.diff_lines)
|
|
||||||
for i in range(num):
|
|
||||||
lpos = offset + i
|
|
||||||
if lpos >= limit:
|
|
||||||
text = ''
|
|
||||||
else:
|
|
||||||
line = self.diff_lines[lpos]
|
|
||||||
text = line.text
|
|
||||||
if line.image_data is not None:
|
|
||||||
image_involved = True
|
|
||||||
self.write(f'\r\x1b[K{text}\x1b[0m')
|
|
||||||
if self.current_search is not None:
|
|
||||||
self.current_search.highlight_line(self.write, lpos)
|
|
||||||
if i < num - 1:
|
|
||||||
self.write('\n')
|
|
||||||
if image_involved:
|
|
||||||
self.place_images()
|
|
||||||
|
|
||||||
def update_image_placement_for_resend(self, image_id: int, pl: Placement) -> bool:
|
|
||||||
offset = self.scroll_pos
|
|
||||||
limit = len(self.diff_lines)
|
|
||||||
in_image = False
|
|
||||||
|
|
||||||
def adjust(row: int, candidate: ImagePlacement, is_left: bool) -> bool:
|
|
||||||
if candidate.image.image_id == image_id:
|
|
||||||
q = self.xpos_for_image(row, candidate, is_left)
|
|
||||||
if q is not None:
|
|
||||||
pl.x = q[0]
|
|
||||||
pl.y = row
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
for row in range(self.num_lines):
|
|
||||||
lpos = offset + row
|
|
||||||
if lpos >= limit:
|
|
||||||
break
|
|
||||||
line = self.diff_lines[lpos]
|
|
||||||
if in_image:
|
|
||||||
if line.image_data is None:
|
|
||||||
in_image = False
|
|
||||||
continue
|
|
||||||
if line.image_data is not None:
|
|
||||||
left_placement, right_placement = line.image_data
|
|
||||||
if left_placement is not None:
|
|
||||||
if adjust(row, left_placement, True):
|
|
||||||
return True
|
|
||||||
in_image = True
|
|
||||||
if right_placement is not None:
|
|
||||||
if adjust(row, right_placement, False):
|
|
||||||
return True
|
|
||||||
in_image = True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def place_images(self) -> None:
|
|
||||||
self.image_manager.update_image_placement_for_resend = self.update_image_placement_for_resend
|
|
||||||
self.cmd.clear_images_on_screen()
|
|
||||||
offset = self.scroll_pos
|
|
||||||
limit = len(self.diff_lines)
|
|
||||||
in_image = False
|
|
||||||
for row in range(self.num_lines):
|
|
||||||
lpos = offset + row
|
|
||||||
if lpos >= limit:
|
|
||||||
break
|
|
||||||
line = self.diff_lines[lpos]
|
|
||||||
if in_image:
|
|
||||||
if line.image_data is None:
|
|
||||||
in_image = False
|
|
||||||
continue
|
|
||||||
if line.image_data is not None:
|
|
||||||
left_placement, right_placement = line.image_data
|
|
||||||
if left_placement is not None:
|
|
||||||
self.place_image(row, left_placement, True)
|
|
||||||
in_image = True
|
|
||||||
if right_placement is not None:
|
|
||||||
self.place_image(row, right_placement, False)
|
|
||||||
in_image = True
|
|
||||||
|
|
||||||
def xpos_for_image(self, row: int, placement: ImagePlacement, is_left: bool) -> Optional[Tuple[int, float]]:
|
|
||||||
xpos = (0 if is_left else (self.screen_size.cols // 2)) + placement.image.margin_size
|
|
||||||
image_height_in_rows = placement.image.rows
|
|
||||||
topmost_visible_row = placement.row
|
|
||||||
num_visible_rows = image_height_in_rows - topmost_visible_row
|
|
||||||
visible_frac = min(num_visible_rows / image_height_in_rows, 1)
|
|
||||||
if visible_frac <= 0:
|
|
||||||
return None
|
|
||||||
return xpos, visible_frac
|
|
||||||
|
|
||||||
def place_image(self, row: int, placement: ImagePlacement, is_left: bool) -> None:
|
|
||||||
q = self.xpos_for_image(row, placement, is_left)
|
|
||||||
if q is not None:
|
|
||||||
xpos, visible_frac = q
|
|
||||||
height = int(visible_frac * placement.image.height)
|
|
||||||
top = placement.image.height - height
|
|
||||||
self.image_manager.show_image(placement.image.image_id, xpos, row, src_rect=(
|
|
||||||
0, top, placement.image.width, height))
|
|
||||||
|
|
||||||
def draw_screen(self) -> None:
|
|
||||||
self.enforce_cursor_state()
|
|
||||||
if self.state.value < State.diffed.value:
|
|
||||||
self.cmd.clear_screen()
|
|
||||||
self.write(_('Calculating diff, please wait...'))
|
|
||||||
return
|
|
||||||
self.cmd.clear_images_on_screen()
|
|
||||||
self.cmd.set_cursor_position(0, 0)
|
|
||||||
self.draw_lines(self.num_lines)
|
|
||||||
self.draw_status_line()
|
|
||||||
|
|
||||||
def draw_status_line(self) -> None:
|
|
||||||
if self.state.value < State.diffed.value:
|
|
||||||
return
|
|
||||||
self.enforce_cursor_state()
|
|
||||||
self.cmd.set_cursor_position(0, self.num_lines)
|
|
||||||
self.cmd.clear_to_eol()
|
|
||||||
if self.state is State.command:
|
|
||||||
self.line_edit.write(self.write)
|
|
||||||
elif self.state is State.message:
|
|
||||||
self.cmd.styled(self.message, reverse=True)
|
|
||||||
else:
|
|
||||||
sp = f'{self.scroll_pos/self.max_scroll_pos:.0%}' if self.scroll_pos and self.max_scroll_pos else '0%'
|
|
||||||
scroll_frac = styled(sp, fg=self.opts.margin_fg)
|
|
||||||
if self.current_search is None:
|
|
||||||
counts = '{}{}{}'.format(
|
|
||||||
styled(str(self.added_count), fg=self.opts.highlight_added_bg),
|
|
||||||
styled(',', fg=self.opts.margin_fg),
|
|
||||||
styled(str(self.removed_count), fg=self.opts.highlight_removed_bg)
|
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
counts = styled(f'{len(self.current_search)} matches', fg=self.opts.margin_fg)
|
|
||||||
suffix = f'{counts} {scroll_frac}'
|
|
||||||
prefix = styled(':', fg=self.opts.margin_fg)
|
|
||||||
filler = self.screen_size.cols - wcswidth(prefix) - wcswidth(suffix)
|
|
||||||
text = '{}{}{}'.format(prefix, ' ' * filler, suffix)
|
|
||||||
self.write(text)
|
|
||||||
|
|
||||||
def change_context_count(self, new_ctx: int) -> None:
|
opt('replace_tab_by', '\\x20\\x20\\x20\\x20', option_type='python_string',
|
||||||
new_ctx = max(0, new_ctx)
|
long_text='The string to replace tabs with. Default is to use four spaces.'
|
||||||
if new_ctx != self.current_context_count:
|
)
|
||||||
self.current_context_count = new_ctx
|
|
||||||
self.state = State.collected
|
|
||||||
self.generate_diff()
|
|
||||||
self.restore_position = self.current_position
|
|
||||||
self.draw_screen()
|
|
||||||
|
|
||||||
def start_search(self, is_regex: bool, is_backward: bool) -> None:
|
opt('+ignore_name', '', ctype='string',
|
||||||
if self.state is not State.diffed:
|
add_to_default=False,
|
||||||
self.cmd.bell()
|
long_text='''
|
||||||
return
|
A glob pattern that is matched against only the filename of files and directories. Matching
|
||||||
self.state = State.command
|
files and directories are ignored when scanning the filesystem to look for files to diff.
|
||||||
self.line_edit.clear()
|
Can be specified multiple times to use multiple patterns. For example::
|
||||||
self.line_edit.add_text('?' if is_backward else '/')
|
|
||||||
self.current_search_is_regex = is_regex
|
|
||||||
self.draw_status_line()
|
|
||||||
|
|
||||||
def do_search(self) -> None:
|
ignore_name .git
|
||||||
self.current_search = None
|
ignore_name *~
|
||||||
query = self.line_edit.current_input
|
ignore_name *.pyc
|
||||||
if len(query) < 2:
|
''',
|
||||||
return
|
)
|
||||||
try:
|
|
||||||
self.current_search = Search(self.opts, query[1:], self.current_search_is_regex, query[0] == '?')
|
|
||||||
except BadRegex:
|
|
||||||
self.state = State.message
|
|
||||||
self.message = sanitize(_('Bad regex: {}').format(query[1:]))
|
|
||||||
self.cmd.bell()
|
|
||||||
else:
|
|
||||||
if self.current_search(self.diff_lines, self.margin_size, self.screen_size.cols):
|
|
||||||
self.scroll_to_next_match(include_current=True)
|
|
||||||
else:
|
|
||||||
self.state = State.message
|
|
||||||
self.message = sanitize(_('No matches found'))
|
|
||||||
self.cmd.bell()
|
|
||||||
|
|
||||||
def on_key_event(self, key_event: KeyEvent, in_bracketed_paste: bool = False) -> None:
|
egr() # }}}
|
||||||
if key_event.text:
|
|
||||||
if self.state is State.command:
|
|
||||||
self.line_edit.on_text(key_event.text, in_bracketed_paste)
|
|
||||||
self.draw_status_line()
|
|
||||||
return
|
|
||||||
if self.state is State.message:
|
|
||||||
self.state = State.diffed
|
|
||||||
self.draw_status_line()
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
if self.state is State.message:
|
|
||||||
if key_event.type is not EventType.RELEASE:
|
|
||||||
self.state = State.diffed
|
|
||||||
self.draw_status_line()
|
|
||||||
return
|
|
||||||
if self.state is State.command:
|
|
||||||
if self.line_edit.on_key(key_event):
|
|
||||||
if not self.line_edit.current_input:
|
|
||||||
self.state = State.diffed
|
|
||||||
self.draw_status_line()
|
|
||||||
return
|
|
||||||
if key_event.matches('enter'):
|
|
||||||
self.state = State.diffed
|
|
||||||
self.do_search()
|
|
||||||
self.line_edit.clear()
|
|
||||||
self.draw_screen()
|
|
||||||
return
|
|
||||||
if key_event.matches('esc'):
|
|
||||||
self.state = State.diffed
|
|
||||||
self.draw_status_line()
|
|
||||||
return
|
|
||||||
if self.state.value >= State.diffed.value and self.current_search is not None and key_event.matches('esc'):
|
|
||||||
self.current_search = None
|
|
||||||
self.draw_screen()
|
|
||||||
return
|
|
||||||
if key_event.type is EventType.RELEASE:
|
|
||||||
return
|
|
||||||
action = self.shortcut_action(key_event)
|
|
||||||
if action is not None:
|
|
||||||
return self.perform_action(action)
|
|
||||||
|
|
||||||
def on_resize(self, screen_size: ScreenSize) -> None:
|
# colors {{{
|
||||||
self.screen_size = screen_size
|
agr('colors', 'Colors')
|
||||||
self.set_scrolling_region()
|
|
||||||
if self.state.value > State.collected.value:
|
|
||||||
self.image_manager.delete_all_sent_images()
|
|
||||||
self.render_diff()
|
|
||||||
self.draw_screen()
|
|
||||||
|
|
||||||
def on_interrupt(self) -> None:
|
opt('pygments_style', 'default',
|
||||||
self.terminate(1)
|
long_text='''
|
||||||
|
The pygments color scheme to use for syntax highlighting. See :link:`pygments
|
||||||
|
builtin styles <https://pygments.org/styles/>` for a list of schemes. Note that
|
||||||
|
this **does not** change the colors used for diffing,
|
||||||
|
only the colors used for syntax highlighting. To change the general colors use the settings below.
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
def on_eot(self) -> None:
|
opt('foreground', 'black',
|
||||||
self.terminate(1)
|
option_type='to_color',
|
||||||
|
long_text='Basic colors'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('background', 'white',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('title_fg', 'black',
|
||||||
|
option_type='to_color',
|
||||||
|
long_text='Title colors'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('title_bg', 'white',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('margin_bg', '#fafbfc',
|
||||||
|
option_type='to_color',
|
||||||
|
long_text='Margin colors'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('margin_fg', '#aaaaaa',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('removed_bg', '#ffeef0',
|
||||||
|
option_type='to_color',
|
||||||
|
long_text='Removed text backgrounds'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('highlight_removed_bg', '#fdb8c0',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('removed_margin_bg', '#ffdce0',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('added_bg', '#e6ffed',
|
||||||
|
option_type='to_color',
|
||||||
|
long_text='Added text backgrounds'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('highlight_added_bg', '#acf2bd',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('added_margin_bg', '#cdffd8',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('filler_bg', '#fafbfc',
|
||||||
|
option_type='to_color',
|
||||||
|
long_text='Filler (empty) line background'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('margin_filler_bg', 'none',
|
||||||
|
option_type='to_color_or_none',
|
||||||
|
long_text='Filler (empty) line background in margins, defaults to the filler background'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('hunk_margin_bg', '#dbedff',
|
||||||
|
option_type='to_color',
|
||||||
|
long_text='Hunk header colors'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('hunk_bg', '#f1f8ff',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('search_bg', '#444',
|
||||||
|
option_type='to_color',
|
||||||
|
long_text='Highlighting'
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('search_fg', 'white',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('select_bg', '#b4d5fe',
|
||||||
|
option_type='to_color',
|
||||||
|
)
|
||||||
|
|
||||||
|
opt('select_fg', 'black',
|
||||||
|
option_type='to_color_or_none',
|
||||||
|
)
|
||||||
|
egr() # }}}
|
||||||
|
|
||||||
|
# shortcuts {{{
|
||||||
|
agr('shortcuts', 'Keyboard shortcuts')
|
||||||
|
|
||||||
|
map('Quit',
|
||||||
|
'quit q quit',
|
||||||
|
)
|
||||||
|
map('Quit',
|
||||||
|
'quit esc quit',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll down',
|
||||||
|
'scroll_down j scroll_by 1',
|
||||||
|
)
|
||||||
|
map('Scroll down',
|
||||||
|
'scroll_down down scroll_by 1',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll up',
|
||||||
|
'scroll_up k scroll_by -1',
|
||||||
|
)
|
||||||
|
map('Scroll up',
|
||||||
|
'scroll_up up scroll_by -1',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to top',
|
||||||
|
'scroll_top home scroll_to start',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to bottom',
|
||||||
|
'scroll_bottom end scroll_to end',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to next page',
|
||||||
|
'scroll_page_down page_down scroll_to next-page',
|
||||||
|
)
|
||||||
|
map('Scroll to next page',
|
||||||
|
'scroll_page_down space scroll_to next-page',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to previous page',
|
||||||
|
'scroll_page_up page_up scroll_to prev-page',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to next change',
|
||||||
|
'next_change n scroll_to next-change',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to previous change',
|
||||||
|
'prev_change p scroll_to prev-change',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Show all context',
|
||||||
|
'all_context a change_context all',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Show default context',
|
||||||
|
'default_context = change_context default',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Increase context',
|
||||||
|
'increase_context + change_context 5',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Decrease context',
|
||||||
|
'decrease_context - change_context -5',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Search forward',
|
||||||
|
'search_forward / start_search regex forward',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Search backward',
|
||||||
|
'search_backward ? start_search regex backward',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to next search match',
|
||||||
|
'next_match . scroll_to next-match',
|
||||||
|
)
|
||||||
|
map('Scroll to next search match',
|
||||||
|
'next_match > scroll_to next-match',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Scroll to previous search match',
|
||||||
|
'prev_match , scroll_to prev-match',
|
||||||
|
)
|
||||||
|
map('Scroll to previous search match',
|
||||||
|
'prev_match < scroll_to prev-match',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Search forward (no regex)',
|
||||||
|
'search_forward_simple f start_search substring forward',
|
||||||
|
)
|
||||||
|
|
||||||
|
map('Search backward (no regex)',
|
||||||
|
'search_backward_simple b start_search substring backward',
|
||||||
|
)
|
||||||
|
egr() # }}}
|
||||||
|
|
||||||
OPTIONS = partial('''\
|
OPTIONS = partial('''\
|
||||||
--context
|
--context
|
||||||
@ -607,99 +288,10 @@ Override individual configuration options, can be specified multiple times.
|
|||||||
Syntax: :italic:`name=value`. For example: :italic:`-o background=gray`
|
Syntax: :italic:`name=value`. For example: :italic:`-o background=gray`
|
||||||
|
|
||||||
'''.format, config_help=CONFIG_HELP.format(conf_name='diff', appname=appname))
|
'''.format, config_help=CONFIG_HELP.format(conf_name='diff', appname=appname))
|
||||||
|
|
||||||
|
|
||||||
class ShowWarning:
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.warnings: List[str] = []
|
|
||||||
|
|
||||||
def __call__(self, message: Any, category: Any, filename: str, lineno: int, file: object = None, line: object = None) -> None:
|
|
||||||
if category is ImageSupportWarning and isinstance(message, str):
|
|
||||||
showwarning.warnings.append(message)
|
|
||||||
|
|
||||||
|
|
||||||
showwarning = ShowWarning()
|
|
||||||
help_text = 'Show a side-by-side diff of the specified files/directories. You can also use :italic:`ssh:hostname:remote-file-path` to diff remote files.'
|
help_text = 'Show a side-by-side diff of the specified files/directories. You can also use :italic:`ssh:hostname:remote-file-path` to diff remote files.'
|
||||||
usage = 'file_or_directory_left file_or_directory_right'
|
usage = 'file_or_directory_left file_or_directory_right'
|
||||||
|
|
||||||
|
|
||||||
def terminate_processes(processes: Iterable[int]) -> None:
|
|
||||||
for pid in processes:
|
|
||||||
with suppress(Exception):
|
|
||||||
os.kill(pid, signal.SIGKILL)
|
|
||||||
|
|
||||||
|
|
||||||
def get_ssh_file(hostname: str, rpath: str) -> str:
|
|
||||||
import io
|
|
||||||
import shutil
|
|
||||||
import tarfile
|
|
||||||
tdir = tempfile.mkdtemp(suffix=f'-{hostname}')
|
|
||||||
add_remote_dir(tdir)
|
|
||||||
atexit.register(shutil.rmtree, tdir)
|
|
||||||
is_abs = rpath.startswith('/')
|
|
||||||
rpath = rpath.lstrip('/')
|
|
||||||
cmd = ['ssh', hostname, 'tar', '-c', '-f', '-']
|
|
||||||
if is_abs:
|
|
||||||
cmd.extend(('-C', '/'))
|
|
||||||
cmd.append(rpath)
|
|
||||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
|
||||||
assert p.stdout is not None
|
|
||||||
raw = p.stdout.read()
|
|
||||||
if p.wait() != 0:
|
|
||||||
raise SystemExit(p.returncode)
|
|
||||||
with tarfile.open(fileobj=io.BytesIO(raw), mode='r:') as tf:
|
|
||||||
members = tf.getmembers()
|
|
||||||
extract_all_from_tarfile_safely(tf, tdir)
|
|
||||||
if len(members) == 1:
|
|
||||||
for root, dirs, files in os.walk(tdir):
|
|
||||||
if files:
|
|
||||||
return os.path.join(root, files[0])
|
|
||||||
return os.path.abspath(os.path.join(tdir, rpath))
|
|
||||||
|
|
||||||
|
|
||||||
def get_remote_file(path: str) -> str:
|
|
||||||
if path.startswith('ssh:'):
|
|
||||||
parts = path.split(':', 2)
|
|
||||||
if len(parts) == 3:
|
|
||||||
return get_ssh_file(parts[1], parts[2])
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def main(args: List[str]) -> None:
|
|
||||||
warnings.showwarning = showwarning
|
|
||||||
cli_opts, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten diff', result_class=DiffCLIOptions)
|
|
||||||
if len(items) != 2:
|
|
||||||
raise SystemExit('You must specify exactly two files/directories to compare')
|
|
||||||
left, right = items
|
|
||||||
global_data.title = _('{} vs. {}').format(left, right)
|
|
||||||
opts = init_config(cli_opts)
|
|
||||||
set_diff_command(opts.diff_cmd)
|
|
||||||
lines_for_path.replace_tab_by = opts.replace_tab_by
|
|
||||||
Collection.ignore_names = tuple(opts.ignore_name)
|
|
||||||
left, right = map(get_remote_file, (left, right))
|
|
||||||
if os.path.isdir(left) != os.path.isdir(right):
|
|
||||||
raise SystemExit('The items to be diffed should both be either directories or files. Comparing a directory to a file is not valid.')
|
|
||||||
for f in left, right:
|
|
||||||
if not os.path.exists(f):
|
|
||||||
raise SystemExit(f'{f} does not exist')
|
|
||||||
|
|
||||||
loop = Loop()
|
|
||||||
handler = DiffHandler(cli_opts, opts, left, right)
|
|
||||||
loop.loop(handler)
|
|
||||||
for message in showwarning.warnings:
|
|
||||||
from kitty.utils import safe_print
|
|
||||||
safe_print(message, file=sys.stderr)
|
|
||||||
if handler.doing_background_work is BackgroundWork.highlighting:
|
|
||||||
terminate_processes(tuple(get_highlight_processes()))
|
|
||||||
elif handler.doing_background_work == BackgroundWork.diffing:
|
|
||||||
terminate_processes(tuple(worker_processes))
|
|
||||||
if loop.return_code != 0:
|
|
||||||
if handler.report_traceback_on_exit:
|
|
||||||
print(handler.report_traceback_on_exit, file=sys.stderr)
|
|
||||||
input('Press Enter to quit.')
|
|
||||||
raise SystemExit(loop.return_code)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main(sys.argv)
|
main(sys.argv)
|
||||||
@ -711,5 +303,4 @@ elif __name__ == '__doc__':
|
|||||||
cd['short_desc'] = 'Pretty, side-by-side diffing of files and images'
|
cd['short_desc'] = 'Pretty, side-by-side diffing of files and images'
|
||||||
cd['args_completion'] = CompletionSpec.from_string('type:file mime:text/* mime:image/* group:"Text and image files"')
|
cd['args_completion'] = CompletionSpec.from_string('type:file mime:text/* mime:image/* group:"Text and image files"')
|
||||||
elif __name__ == '__conf__':
|
elif __name__ == '__conf__':
|
||||||
from .options.definition import definition
|
|
||||||
sys.options_definition = definition # type: ignore
|
sys.options_definition = definition # type: ignore
|
||||||
|
|||||||
@ -1,267 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# vim:fileencoding=utf-8
|
|
||||||
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
|
||||||
|
|
||||||
# After editing this file run ./gen-config.py to apply the changes
|
|
||||||
|
|
||||||
from kitty.conf.types import Action, Definition
|
|
||||||
|
|
||||||
definition = Definition(
|
|
||||||
'kittens.diff',
|
|
||||||
Action('map', 'parse_map', {'key_definitions': 'kitty.conf.utils.KittensKeyMap'}, ['kitty.types.ParsedShortcut', 'kitty.conf.utils.KeyAction']),
|
|
||||||
)
|
|
||||||
|
|
||||||
agr = definition.add_group
|
|
||||||
egr = definition.end_group
|
|
||||||
opt = definition.add_option
|
|
||||||
map = definition.add_map
|
|
||||||
mma = definition.add_mouse_map
|
|
||||||
|
|
||||||
# diff {{{
|
|
||||||
agr('diff', 'Diffing')
|
|
||||||
|
|
||||||
opt('syntax_aliases', 'pyj:py pyi:py recipe:py',
|
|
||||||
option_type='syntax_aliases', ctype='strdict_ _:',
|
|
||||||
long_text='''
|
|
||||||
File extension aliases for syntax highlight. For example, to syntax highlight
|
|
||||||
:file:`file.xyz` as :file:`file.abc` use a setting of :code:`xyz:abc`.
|
|
||||||
Multiple aliases must be separated by spaces.
|
|
||||||
'''
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('num_context_lines', '3',
|
|
||||||
option_type='positive_int',
|
|
||||||
long_text='The number of lines of context to show around each change.'
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('diff_cmd', 'auto',
|
|
||||||
long_text='''
|
|
||||||
The diff command to use. Must contain the placeholder :code:`_CONTEXT_` which
|
|
||||||
will be replaced by the number of lines of context. A few special values are allowed:
|
|
||||||
:code:`auto` will automatically pick an available diff implementation. :code:`builtin`
|
|
||||||
will use the anchored diff algorithm from the Go standard library. :code:`git` will
|
|
||||||
use the git command to do the diffing. :code:`diff` will use the diff command to
|
|
||||||
do the diffing.
|
|
||||||
'''
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('replace_tab_by', '\\x20\\x20\\x20\\x20',
|
|
||||||
option_type='python_string',
|
|
||||||
long_text='The string to replace tabs with. Default is to use four spaces.'
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('+ignore_name', '', ctype='string',
|
|
||||||
option_type='store_multiple',
|
|
||||||
add_to_default=False,
|
|
||||||
long_text='''
|
|
||||||
A glob pattern that is matched against only the filename of files and directories. Matching
|
|
||||||
files and directories are ignored when scanning the filesystem to look for files to diff.
|
|
||||||
Can be specified multiple times to use multiple patterns. For example::
|
|
||||||
|
|
||||||
ignore_name .git
|
|
||||||
ignore_name *~
|
|
||||||
ignore_name *.pyc
|
|
||||||
''',
|
|
||||||
)
|
|
||||||
|
|
||||||
egr() # }}}
|
|
||||||
|
|
||||||
# colors {{{
|
|
||||||
agr('colors', 'Colors')
|
|
||||||
|
|
||||||
opt('pygments_style', 'default',
|
|
||||||
long_text='''
|
|
||||||
The pygments color scheme to use for syntax highlighting. See :link:`pygments
|
|
||||||
builtin styles <https://pygments.org/styles/>` for a list of schemes. Note that
|
|
||||||
this **does not** change the colors used for diffing,
|
|
||||||
only the colors used for syntax highlighting. To change the general colors use the settings below.
|
|
||||||
'''
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('foreground', 'black',
|
|
||||||
option_type='to_color',
|
|
||||||
long_text='Basic colors'
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('background', 'white',
|
|
||||||
option_type='to_color',
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('title_fg', 'black',
|
|
||||||
option_type='to_color',
|
|
||||||
long_text='Title colors'
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('title_bg', 'white',
|
|
||||||
option_type='to_color',
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('margin_bg', '#fafbfc',
|
|
||||||
option_type='to_color',
|
|
||||||
long_text='Margin colors'
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('margin_fg', '#aaaaaa',
|
|
||||||
option_type='to_color',
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('removed_bg', '#ffeef0',
|
|
||||||
option_type='to_color',
|
|
||||||
long_text='Removed text backgrounds'
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('highlight_removed_bg', '#fdb8c0',
|
|
||||||
option_type='to_color',
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('removed_margin_bg', '#ffdce0',
|
|
||||||
option_type='to_color',
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('added_bg', '#e6ffed',
|
|
||||||
option_type='to_color',
|
|
||||||
long_text='Added text backgrounds'
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('highlight_added_bg', '#acf2bd',
|
|
||||||
option_type='to_color',
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('added_margin_bg', '#cdffd8',
|
|
||||||
option_type='to_color',
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('filler_bg', '#fafbfc',
|
|
||||||
option_type='to_color',
|
|
||||||
long_text='Filler (empty) line background'
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('margin_filler_bg', 'none',
|
|
||||||
option_type='to_color_or_none',
|
|
||||||
long_text='Filler (empty) line background in margins, defaults to the filler background'
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('hunk_margin_bg', '#dbedff',
|
|
||||||
option_type='to_color',
|
|
||||||
long_text='Hunk header colors'
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('hunk_bg', '#f1f8ff',
|
|
||||||
option_type='to_color',
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('search_bg', '#444',
|
|
||||||
option_type='to_color',
|
|
||||||
long_text='Highlighting'
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('search_fg', 'white',
|
|
||||||
option_type='to_color',
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('select_bg', '#b4d5fe',
|
|
||||||
option_type='to_color',
|
|
||||||
)
|
|
||||||
|
|
||||||
opt('select_fg', 'black',
|
|
||||||
option_type='to_color_or_none',
|
|
||||||
)
|
|
||||||
egr() # }}}
|
|
||||||
|
|
||||||
# shortcuts {{{
|
|
||||||
agr('shortcuts', 'Keyboard shortcuts')
|
|
||||||
|
|
||||||
map('Quit',
|
|
||||||
'quit q quit',
|
|
||||||
)
|
|
||||||
map('Quit',
|
|
||||||
'quit esc quit',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Scroll down',
|
|
||||||
'scroll_down j scroll_by 1',
|
|
||||||
)
|
|
||||||
map('Scroll down',
|
|
||||||
'scroll_down down scroll_by 1',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Scroll up',
|
|
||||||
'scroll_up k scroll_by -1',
|
|
||||||
)
|
|
||||||
map('Scroll up',
|
|
||||||
'scroll_up up scroll_by -1',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Scroll to top',
|
|
||||||
'scroll_top home scroll_to start',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Scroll to bottom',
|
|
||||||
'scroll_bottom end scroll_to end',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Scroll to next page',
|
|
||||||
'scroll_page_down page_down scroll_to next-page',
|
|
||||||
)
|
|
||||||
map('Scroll to next page',
|
|
||||||
'scroll_page_down space scroll_to next-page',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Scroll to previous page',
|
|
||||||
'scroll_page_up page_up scroll_to prev-page',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Scroll to next change',
|
|
||||||
'next_change n scroll_to next-change',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Scroll to previous change',
|
|
||||||
'prev_change p scroll_to prev-change',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Show all context',
|
|
||||||
'all_context a change_context all',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Show default context',
|
|
||||||
'default_context = change_context default',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Increase context',
|
|
||||||
'increase_context + change_context 5',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Decrease context',
|
|
||||||
'decrease_context - change_context -5',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Search forward',
|
|
||||||
'search_forward / start_search regex forward',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Search backward',
|
|
||||||
'search_backward ? start_search regex backward',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Scroll to next search match',
|
|
||||||
'next_match . scroll_to next-match',
|
|
||||||
)
|
|
||||||
map('Scroll to next search match',
|
|
||||||
'next_match > scroll_to next-match',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Scroll to previous search match',
|
|
||||||
'prev_match , scroll_to prev-match',
|
|
||||||
)
|
|
||||||
map('Scroll to previous search match',
|
|
||||||
'prev_match < scroll_to prev-match',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Search forward (no regex)',
|
|
||||||
'search_forward_simple f start_search substring forward',
|
|
||||||
)
|
|
||||||
|
|
||||||
map('Search backward (no regex)',
|
|
||||||
'search_backward_simple b start_search substring backward',
|
|
||||||
)
|
|
||||||
egr() # }}}
|
|
||||||
125
kittens/diff/options/parse.py
generated
125
kittens/diff/options/parse.py
generated
@ -1,125 +0,0 @@
|
|||||||
# generated by gen-config.py DO NOT edit
|
|
||||||
|
|
||||||
# isort: skip_file
|
|
||||||
import typing
|
|
||||||
from kittens.diff.options.utils import parse_map, store_multiple, syntax_aliases
|
|
||||||
from kitty.conf.utils import merge_dicts, positive_int, python_string, to_color, to_color_or_none
|
|
||||||
|
|
||||||
|
|
||||||
class Parser:
|
|
||||||
|
|
||||||
def added_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['added_bg'] = to_color(val)
|
|
||||||
|
|
||||||
def added_margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['added_margin_bg'] = to_color(val)
|
|
||||||
|
|
||||||
def background(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['background'] = to_color(val)
|
|
||||||
|
|
||||||
def diff_cmd(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['diff_cmd'] = str(val)
|
|
||||||
|
|
||||||
def filler_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['filler_bg'] = to_color(val)
|
|
||||||
|
|
||||||
def foreground(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['foreground'] = to_color(val)
|
|
||||||
|
|
||||||
def highlight_added_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['highlight_added_bg'] = to_color(val)
|
|
||||||
|
|
||||||
def highlight_removed_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['highlight_removed_bg'] = to_color(val)
|
|
||||||
|
|
||||||
def hunk_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['hunk_bg'] = to_color(val)
|
|
||||||
|
|
||||||
def hunk_margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['hunk_margin_bg'] = to_color(val)
|
|
||||||
|
|
||||||
def ignore_name(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
for k, v in store_multiple(val, ans["ignore_name"]):
|
|
||||||
ans["ignore_name"][k] = v
|
|
||||||
|
|
||||||
def margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['margin_bg'] = to_color(val)
|
|
||||||
|
|
||||||
def margin_fg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['margin_fg'] = to_color(val)
|
|
||||||
|
|
||||||
def margin_filler_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['margin_filler_bg'] = to_color_or_none(val)
|
|
||||||
|
|
||||||
def num_context_lines(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['num_context_lines'] = positive_int(val)
|
|
||||||
|
|
||||||
def pygments_style(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['pygments_style'] = str(val)
|
|
||||||
|
|
||||||
def removed_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['removed_bg'] = to_color(val)
|
|
||||||
|
|
||||||
def removed_margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['removed_margin_bg'] = to_color(val)
|
|
||||||
|
|
||||||
def replace_tab_by(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['replace_tab_by'] = python_string(val)
|
|
||||||
|
|
||||||
def search_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['search_bg'] = to_color(val)
|
|
||||||
|
|
||||||
def search_fg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['search_fg'] = to_color(val)
|
|
||||||
|
|
||||||
def select_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['select_bg'] = to_color(val)
|
|
||||||
|
|
||||||
def select_fg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['select_fg'] = to_color_or_none(val)
|
|
||||||
|
|
||||||
def syntax_aliases(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['syntax_aliases'] = syntax_aliases(val)
|
|
||||||
|
|
||||||
def title_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['title_bg'] = to_color(val)
|
|
||||||
|
|
||||||
def title_fg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
ans['title_fg'] = to_color(val)
|
|
||||||
|
|
||||||
def map(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
|
|
||||||
for k in parse_map(val):
|
|
||||||
ans['map'].append(k)
|
|
||||||
|
|
||||||
|
|
||||||
def create_result_dict() -> typing.Dict[str, typing.Any]:
|
|
||||||
return {
|
|
||||||
'ignore_name': {},
|
|
||||||
'map': [],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
actions: typing.FrozenSet[str] = frozenset(('map',))
|
|
||||||
|
|
||||||
|
|
||||||
def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
|
|
||||||
ans = {}
|
|
||||||
for k, v in defaults.items():
|
|
||||||
if isinstance(v, dict):
|
|
||||||
ans[k] = merge_dicts(v, vals.get(k, {}))
|
|
||||||
elif k in actions:
|
|
||||||
ans[k] = v + vals.get(k, [])
|
|
||||||
else:
|
|
||||||
ans[k] = vals.get(k, v)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
parser = Parser()
|
|
||||||
|
|
||||||
|
|
||||||
def parse_conf_item(key: str, val: str, ans: typing.Dict[str, typing.Any]) -> bool:
|
|
||||||
func = getattr(parser, key, None)
|
|
||||||
if func is not None:
|
|
||||||
func(val, ans)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
174
kittens/diff/options/types.py
generated
174
kittens/diff/options/types.py
generated
@ -1,174 +0,0 @@
|
|||||||
# generated by gen-config.py DO NOT edit
|
|
||||||
|
|
||||||
# isort: skip_file
|
|
||||||
import typing
|
|
||||||
from kitty.conf.utils import KeyAction, KittensKeyMap
|
|
||||||
import kitty.conf.utils
|
|
||||||
from kitty.fast_data_types import Color
|
|
||||||
import kitty.fast_data_types
|
|
||||||
from kitty.types import ParsedShortcut
|
|
||||||
import kitty.types
|
|
||||||
|
|
||||||
|
|
||||||
option_names = ( # {{{
|
|
||||||
'added_bg',
|
|
||||||
'added_margin_bg',
|
|
||||||
'background',
|
|
||||||
'diff_cmd',
|
|
||||||
'filler_bg',
|
|
||||||
'foreground',
|
|
||||||
'highlight_added_bg',
|
|
||||||
'highlight_removed_bg',
|
|
||||||
'hunk_bg',
|
|
||||||
'hunk_margin_bg',
|
|
||||||
'ignore_name',
|
|
||||||
'map',
|
|
||||||
'margin_bg',
|
|
||||||
'margin_fg',
|
|
||||||
'margin_filler_bg',
|
|
||||||
'num_context_lines',
|
|
||||||
'pygments_style',
|
|
||||||
'removed_bg',
|
|
||||||
'removed_margin_bg',
|
|
||||||
'replace_tab_by',
|
|
||||||
'search_bg',
|
|
||||||
'search_fg',
|
|
||||||
'select_bg',
|
|
||||||
'select_fg',
|
|
||||||
'syntax_aliases',
|
|
||||||
'title_bg',
|
|
||||||
'title_fg') # }}}
|
|
||||||
|
|
||||||
|
|
||||||
class Options:
|
|
||||||
added_bg: Color = Color(230, 255, 237)
|
|
||||||
added_margin_bg: Color = Color(205, 255, 216)
|
|
||||||
background: Color = Color(255, 255, 255)
|
|
||||||
diff_cmd: str = 'auto'
|
|
||||||
filler_bg: Color = Color(250, 251, 252)
|
|
||||||
foreground: Color = Color(0, 0, 0)
|
|
||||||
highlight_added_bg: Color = Color(172, 242, 189)
|
|
||||||
highlight_removed_bg: Color = Color(253, 184, 192)
|
|
||||||
hunk_bg: Color = Color(241, 248, 255)
|
|
||||||
hunk_margin_bg: Color = Color(219, 237, 255)
|
|
||||||
margin_bg: Color = Color(250, 251, 252)
|
|
||||||
margin_fg: Color = Color(170, 170, 170)
|
|
||||||
margin_filler_bg: typing.Optional[kitty.fast_data_types.Color] = None
|
|
||||||
num_context_lines: int = 3
|
|
||||||
pygments_style: str = 'default'
|
|
||||||
removed_bg: Color = Color(255, 238, 240)
|
|
||||||
removed_margin_bg: Color = Color(255, 220, 224)
|
|
||||||
replace_tab_by: str = ' '
|
|
||||||
search_bg: Color = Color(68, 68, 68)
|
|
||||||
search_fg: Color = Color(255, 255, 255)
|
|
||||||
select_bg: Color = Color(180, 213, 254)
|
|
||||||
select_fg: typing.Optional[kitty.fast_data_types.Color] = Color(0, 0, 0)
|
|
||||||
syntax_aliases: typing.Dict[str, str] = {'pyj': 'py', 'pyi': 'py', 'recipe': 'py'}
|
|
||||||
title_bg: Color = Color(255, 255, 255)
|
|
||||||
title_fg: Color = Color(0, 0, 0)
|
|
||||||
ignore_name: typing.Dict[str, str] = {}
|
|
||||||
map: typing.List[typing.Tuple[kitty.types.ParsedShortcut, kitty.conf.utils.KeyAction]] = []
|
|
||||||
key_definitions: KittensKeyMap = {}
|
|
||||||
config_paths: typing.Tuple[str, ...] = ()
|
|
||||||
config_overrides: typing.Tuple[str, ...] = ()
|
|
||||||
|
|
||||||
def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:
|
|
||||||
if options_dict is not None:
|
|
||||||
null = object()
|
|
||||||
for key in option_names:
|
|
||||||
val = options_dict.get(key, null)
|
|
||||||
if val is not null:
|
|
||||||
setattr(self, key, val)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _fields(self) -> typing.Tuple[str, ...]:
|
|
||||||
return option_names
|
|
||||||
|
|
||||||
def __iter__(self) -> typing.Iterator[str]:
|
|
||||||
return iter(self._fields)
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self._fields)
|
|
||||||
|
|
||||||
def _copy_of_val(self, name: str) -> typing.Any:
|
|
||||||
ans = getattr(self, name)
|
|
||||||
if isinstance(ans, dict):
|
|
||||||
ans = ans.copy()
|
|
||||||
elif isinstance(ans, list):
|
|
||||||
ans = ans[:]
|
|
||||||
return ans
|
|
||||||
|
|
||||||
def _asdict(self) -> typing.Dict[str, typing.Any]:
|
|
||||||
return {k: self._copy_of_val(k) for k in self}
|
|
||||||
|
|
||||||
def _replace(self, **kw: typing.Any) -> "Options":
|
|
||||||
ans = Options()
|
|
||||||
for name in self:
|
|
||||||
setattr(ans, name, self._copy_of_val(name))
|
|
||||||
for name, val in kw.items():
|
|
||||||
setattr(ans, name, val)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
def __getitem__(self, key: typing.Union[int, str]) -> typing.Any:
|
|
||||||
k = option_names[key] if isinstance(key, int) else key
|
|
||||||
try:
|
|
||||||
return getattr(self, k)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
raise KeyError(f"No option named: {k}")
|
|
||||||
|
|
||||||
|
|
||||||
defaults = Options()
|
|
||||||
defaults.ignore_name = {}
|
|
||||||
defaults.map = [
|
|
||||||
# quit
|
|
||||||
(ParsedShortcut(mods=0, key_name='q'), KeyAction('quit')),
|
|
||||||
# quit
|
|
||||||
(ParsedShortcut(mods=0, key_name='ESCAPE'), KeyAction('quit')),
|
|
||||||
# scroll_down
|
|
||||||
(ParsedShortcut(mods=0, key_name='j'), KeyAction('scroll_by', (1,))),
|
|
||||||
# scroll_down
|
|
||||||
(ParsedShortcut(mods=0, key_name='DOWN'), KeyAction('scroll_by', (1,))),
|
|
||||||
# scroll_up
|
|
||||||
(ParsedShortcut(mods=0, key_name='k'), KeyAction('scroll_by', (-1,))),
|
|
||||||
# scroll_up
|
|
||||||
(ParsedShortcut(mods=0, key_name='UP'), KeyAction('scroll_by', (-1,))),
|
|
||||||
# scroll_top
|
|
||||||
(ParsedShortcut(mods=0, key_name='HOME'), KeyAction('scroll_to', ('start',))),
|
|
||||||
# scroll_bottom
|
|
||||||
(ParsedShortcut(mods=0, key_name='END'), KeyAction('scroll_to', ('end',))),
|
|
||||||
# scroll_page_down
|
|
||||||
(ParsedShortcut(mods=0, key_name='PAGE_DOWN'), KeyAction('scroll_to', ('next-page',))),
|
|
||||||
# scroll_page_down
|
|
||||||
(ParsedShortcut(mods=0, key_name=' '), KeyAction('scroll_to', ('next-page',))),
|
|
||||||
# scroll_page_up
|
|
||||||
(ParsedShortcut(mods=0, key_name='PAGE_UP'), KeyAction('scroll_to', ('prev-page',))),
|
|
||||||
# next_change
|
|
||||||
(ParsedShortcut(mods=0, key_name='n'), KeyAction('scroll_to', ('next-change',))),
|
|
||||||
# prev_change
|
|
||||||
(ParsedShortcut(mods=0, key_name='p'), KeyAction('scroll_to', ('prev-change',))),
|
|
||||||
# all_context
|
|
||||||
(ParsedShortcut(mods=0, key_name='a'), KeyAction('change_context', ('all',))),
|
|
||||||
# default_context
|
|
||||||
(ParsedShortcut(mods=0, key_name='='), KeyAction('change_context', ('default',))),
|
|
||||||
# increase_context
|
|
||||||
(ParsedShortcut(mods=0, key_name='+'), KeyAction('change_context', (5,))),
|
|
||||||
# decrease_context
|
|
||||||
(ParsedShortcut(mods=0, key_name='-'), KeyAction('change_context', (-5,))),
|
|
||||||
# search_forward
|
|
||||||
(ParsedShortcut(mods=0, key_name='/'), KeyAction('start_search', (True, False))),
|
|
||||||
# search_backward
|
|
||||||
(ParsedShortcut(mods=0, key_name='?'), KeyAction('start_search', (True, True))),
|
|
||||||
# next_match
|
|
||||||
(ParsedShortcut(mods=0, key_name='.'), KeyAction('scroll_to', ('next-match',))),
|
|
||||||
# next_match
|
|
||||||
(ParsedShortcut(mods=0, key_name='>'), KeyAction('scroll_to', ('next-match',))),
|
|
||||||
# prev_match
|
|
||||||
(ParsedShortcut(mods=0, key_name=','), KeyAction('scroll_to', ('prev-match',))),
|
|
||||||
# prev_match
|
|
||||||
(ParsedShortcut(mods=0, key_name='<'), KeyAction('scroll_to', ('prev-match',))),
|
|
||||||
# search_forward_simple
|
|
||||||
(ParsedShortcut(mods=0, key_name='f'), KeyAction('start_search', (False, False))),
|
|
||||||
# search_backward_simple
|
|
||||||
(ParsedShortcut(mods=0, key_name='b'), KeyAction('start_search', (False, True))),
|
|
||||||
]
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# vim:fileencoding=utf-8
|
|
||||||
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
|
||||||
|
|
||||||
|
|
||||||
from typing import Any, Container, Dict, Iterable, Tuple, Union
|
|
||||||
|
|
||||||
from kitty.conf.utils import KeyFuncWrapper, KittensKeyDefinition, parse_kittens_key
|
|
||||||
|
|
||||||
ReturnType = Tuple[str, Any]
|
|
||||||
func_with_args = KeyFuncWrapper[ReturnType]()
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('scroll_by')
|
|
||||||
def parse_scroll_by(func: str, rest: str) -> Tuple[str, int]:
|
|
||||||
try:
|
|
||||||
return func, int(rest)
|
|
||||||
except Exception:
|
|
||||||
return func, 1
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('scroll_to')
|
|
||||||
def parse_scroll_to(func: str, rest: str) -> Tuple[str, str]:
|
|
||||||
rest = rest.lower()
|
|
||||||
if rest not in {'start', 'end', 'next-change', 'prev-change', 'next-page', 'prev-page', 'next-match', 'prev-match'}:
|
|
||||||
rest = 'start'
|
|
||||||
return func, rest
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('change_context')
|
|
||||||
def parse_change_context(func: str, rest: str) -> Tuple[str, Union[int, str]]:
|
|
||||||
rest = rest.lower()
|
|
||||||
if rest in {'all', 'default'}:
|
|
||||||
return func, rest
|
|
||||||
try:
|
|
||||||
amount = int(rest)
|
|
||||||
except Exception:
|
|
||||||
amount = 5
|
|
||||||
return func, amount
|
|
||||||
|
|
||||||
|
|
||||||
@func_with_args('start_search')
|
|
||||||
def parse_start_search(func: str, rest: str) -> Tuple[str, Tuple[bool, bool]]:
|
|
||||||
rest_ = rest.lower().split()
|
|
||||||
is_regex = bool(rest_ and rest_[0] == 'regex')
|
|
||||||
is_backward = bool(len(rest_) > 1 and rest_[1] == 'backward')
|
|
||||||
return func, (is_regex, is_backward)
|
|
||||||
|
|
||||||
|
|
||||||
def syntax_aliases(raw: str) -> Dict[str, str]:
|
|
||||||
ans = {}
|
|
||||||
for x in raw.split():
|
|
||||||
a, b = x.partition(':')[::2]
|
|
||||||
if a and b:
|
|
||||||
ans[a.lower()] = b
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
def store_multiple(val: str, current_val: Container[str]) -> Iterable[Tuple[str, str]]:
|
|
||||||
val = val.strip()
|
|
||||||
if val not in current_val:
|
|
||||||
yield val, val
|
|
||||||
|
|
||||||
|
|
||||||
def parse_map(val: str) -> Iterable[KittensKeyDefinition]:
|
|
||||||
x = parse_kittens_key(val, func_with_args.args_funcs)
|
|
||||||
if x is not None:
|
|
||||||
yield x
|
|
||||||
@ -1,262 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
|
||||||
|
|
||||||
import concurrent.futures
|
|
||||||
import os
|
|
||||||
import shlex
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
from typing import Dict, Iterator, List, Optional, Sequence, Tuple, Union
|
|
||||||
|
|
||||||
from . import global_data
|
|
||||||
from .collect import lines_for_path
|
|
||||||
from .diff_speedup import changed_center, splitlines_like_git
|
|
||||||
|
|
||||||
left_lines: Tuple[str, ...] = ()
|
|
||||||
right_lines: Tuple[str, ...] = ()
|
|
||||||
GIT_DIFF = 'git diff --no-color --no-ext-diff --exit-code -U_CONTEXT_ --no-index --'
|
|
||||||
DIFF_DIFF = 'diff -p -U _CONTEXT_ --'
|
|
||||||
worker_processes: List[int] = []
|
|
||||||
|
|
||||||
|
|
||||||
def find_differ() -> Optional[str]:
|
|
||||||
if shutil.which('git') and subprocess.Popen(['git', '--help'], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL).wait() == 0:
|
|
||||||
return GIT_DIFF
|
|
||||||
if shutil.which('diff'):
|
|
||||||
return DIFF_DIFF
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def set_diff_command(opt: str) -> None:
|
|
||||||
if opt == 'auto':
|
|
||||||
cmd = find_differ()
|
|
||||||
if cmd is None:
|
|
||||||
raise SystemExit('Failed to find either the git or diff programs on your system')
|
|
||||||
else:
|
|
||||||
cmd = opt
|
|
||||||
global_data.cmd = cmd
|
|
||||||
|
|
||||||
|
|
||||||
def run_diff(file1: str, file2: str, context: int = 3) -> Tuple[bool, Union[int, bool], bytes]:
|
|
||||||
# returns: ok, is_different, patch
|
|
||||||
cmd = shlex.split(global_data.cmd.replace('_CONTEXT_', str(context)))
|
|
||||||
# we resolve symlinks because git diff does not follow symlinks, while diff
|
|
||||||
# does. We want consistent behavior, also for integration with git difftool
|
|
||||||
# we always want symlinks to be followed.
|
|
||||||
path1 = os.path.realpath(file1)
|
|
||||||
path2 = os.path.realpath(file2)
|
|
||||||
p = subprocess.Popen(
|
|
||||||
cmd + [path1, path2],
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL)
|
|
||||||
worker_processes.append(p.pid)
|
|
||||||
stdout, stderr = p.communicate()
|
|
||||||
returncode = p.wait()
|
|
||||||
worker_processes.remove(p.pid)
|
|
||||||
if returncode in (0, 1):
|
|
||||||
return True, returncode == 1, stdout
|
|
||||||
return False, returncode, stderr
|
|
||||||
|
|
||||||
|
|
||||||
class Chunk:
|
|
||||||
|
|
||||||
__slots__ = ('is_context', 'left_start', 'right_start', 'left_count', 'right_count', 'centers')
|
|
||||||
|
|
||||||
def __init__(self, left_start: int, right_start: int, is_context: bool = False) -> None:
|
|
||||||
self.is_context = is_context
|
|
||||||
self.left_start = left_start
|
|
||||||
self.right_start = right_start
|
|
||||||
self.left_count = self.right_count = 0
|
|
||||||
self.centers: Optional[Tuple[Tuple[int, int], ...]] = None
|
|
||||||
|
|
||||||
def add_line(self) -> None:
|
|
||||||
self.right_count += 1
|
|
||||||
|
|
||||||
def remove_line(self) -> None:
|
|
||||||
self.left_count += 1
|
|
||||||
|
|
||||||
def context_line(self) -> None:
|
|
||||||
self.left_count += 1
|
|
||||||
self.right_count += 1
|
|
||||||
|
|
||||||
def finalize(self) -> None:
|
|
||||||
if not self.is_context and self.left_count == self.right_count:
|
|
||||||
self.centers = tuple(
|
|
||||||
changed_center(left_lines[self.left_start + i], right_lines[self.right_start + i])
|
|
||||||
for i in range(self.left_count)
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return 'Chunk(is_context={}, left_start={}, left_count={}, right_start={}, right_count={})'.format(
|
|
||||||
self.is_context, self.left_start, self.left_count, self.right_start, self.right_count)
|
|
||||||
|
|
||||||
|
|
||||||
class Hunk:
|
|
||||||
|
|
||||||
def __init__(self, title: str, left: Tuple[int, int], right: Tuple[int, int]) -> None:
|
|
||||||
self.left_start, self.left_count = left
|
|
||||||
self.right_start, self.right_count = right
|
|
||||||
self.left_start -= 1 # 0-index
|
|
||||||
self.right_start -= 1 # 0-index
|
|
||||||
self.title = title
|
|
||||||
self.added_count = self.removed_count = 0
|
|
||||||
self.chunks: List[Chunk] = []
|
|
||||||
self.current_chunk: Optional[Chunk] = None
|
|
||||||
self.largest_line_number = max(self.left_start + self.left_count, self.right_start + self.right_count)
|
|
||||||
|
|
||||||
def new_chunk(self, is_context: bool = False) -> Chunk:
|
|
||||||
if self.chunks:
|
|
||||||
c = self.chunks[-1]
|
|
||||||
left_start = c.left_start + c.left_count
|
|
||||||
right_start = c.right_start + c.right_count
|
|
||||||
else:
|
|
||||||
left_start = self.left_start
|
|
||||||
right_start = self.right_start
|
|
||||||
return Chunk(left_start, right_start, is_context)
|
|
||||||
|
|
||||||
def ensure_diff_chunk(self) -> None:
|
|
||||||
if self.current_chunk is None:
|
|
||||||
self.current_chunk = self.new_chunk(is_context=False)
|
|
||||||
elif self.current_chunk.is_context:
|
|
||||||
self.chunks.append(self.current_chunk)
|
|
||||||
self.current_chunk = self.new_chunk(is_context=False)
|
|
||||||
|
|
||||||
def ensure_context_chunk(self) -> None:
|
|
||||||
if self.current_chunk is None:
|
|
||||||
self.current_chunk = self.new_chunk(is_context=True)
|
|
||||||
elif not self.current_chunk.is_context:
|
|
||||||
self.chunks.append(self.current_chunk)
|
|
||||||
self.current_chunk = self.new_chunk(is_context=True)
|
|
||||||
|
|
||||||
def add_line(self) -> None:
|
|
||||||
self.ensure_diff_chunk()
|
|
||||||
if self.current_chunk is not None:
|
|
||||||
self.current_chunk.add_line()
|
|
||||||
self.added_count += 1
|
|
||||||
|
|
||||||
def remove_line(self) -> None:
|
|
||||||
self.ensure_diff_chunk()
|
|
||||||
if self.current_chunk is not None:
|
|
||||||
self.current_chunk.remove_line()
|
|
||||||
self.removed_count += 1
|
|
||||||
|
|
||||||
def context_line(self) -> None:
|
|
||||||
self.ensure_context_chunk()
|
|
||||||
if self.current_chunk is not None:
|
|
||||||
self.current_chunk.context_line()
|
|
||||||
|
|
||||||
def finalize(self) -> None:
|
|
||||||
if self.current_chunk is not None:
|
|
||||||
self.chunks.append(self.current_chunk)
|
|
||||||
del self.current_chunk
|
|
||||||
# Sanity check
|
|
||||||
c = self.chunks[-1]
|
|
||||||
if c.left_start + c.left_count != self.left_start + self.left_count:
|
|
||||||
raise ValueError(f'Left side line mismatch {c.left_start + c.left_count} != {self.left_start + self.left_count}')
|
|
||||||
if c.right_start + c.right_count != self.right_start + self.right_count:
|
|
||||||
raise ValueError(f'Right side line mismatch {c.right_start + c.right_count} != {self.right_start + self.right_count}')
|
|
||||||
for c in self.chunks:
|
|
||||||
c.finalize()
|
|
||||||
|
|
||||||
|
|
||||||
def parse_range(x: bytes) -> Tuple[int, int]:
|
|
||||||
parts = x[1:].split(b',', 1)
|
|
||||||
start = abs(int(parts[0]))
|
|
||||||
count = 1 if len(parts) < 2 else int(parts[1])
|
|
||||||
return start, count
|
|
||||||
|
|
||||||
|
|
||||||
def parse_hunk_header(line: bytes) -> Hunk:
|
|
||||||
parts: Tuple[bytes, ...] = tuple(filter(None, line.split(b'@@', 2)))
|
|
||||||
linespec = parts[0].strip()
|
|
||||||
title = ''
|
|
||||||
if len(parts) == 2:
|
|
||||||
title = parts[1].strip().decode('utf-8', 'replace')
|
|
||||||
left, right = map(parse_range, linespec.split())
|
|
||||||
return Hunk(title, left, right)
|
|
||||||
|
|
||||||
|
|
||||||
class Patch:
|
|
||||||
|
|
||||||
def __init__(self, all_hunks: Sequence[Hunk]):
|
|
||||||
self.all_hunks = all_hunks
|
|
||||||
self.largest_line_number = self.all_hunks[-1].largest_line_number if self.all_hunks else 0
|
|
||||||
self.added_count = sum(h.added_count for h in all_hunks)
|
|
||||||
self.removed_count = sum(h.removed_count for h in all_hunks)
|
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[Hunk]:
|
|
||||||
return iter(self.all_hunks)
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self.all_hunks)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_patch(raw: bytes) -> Patch:
|
|
||||||
all_hunks = []
|
|
||||||
current_hunk = None
|
|
||||||
plus, minus, backslash = map(ord, '+-\\')
|
|
||||||
|
|
||||||
def parse_line(line: memoryview) -> None:
|
|
||||||
nonlocal current_hunk
|
|
||||||
if line[:3] == b'@@ ':
|
|
||||||
current_hunk = parse_hunk_header(bytes(line))
|
|
||||||
all_hunks.append(current_hunk)
|
|
||||||
else:
|
|
||||||
if current_hunk is None:
|
|
||||||
return
|
|
||||||
q:int = line[0] if len(line) > 0 else 0
|
|
||||||
if q == plus:
|
|
||||||
current_hunk.add_line()
|
|
||||||
elif q == minus:
|
|
||||||
current_hunk.remove_line()
|
|
||||||
elif q == backslash:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
current_hunk.context_line()
|
|
||||||
|
|
||||||
splitlines_like_git(raw, parse_line)
|
|
||||||
for h in all_hunks:
|
|
||||||
h.finalize()
|
|
||||||
return Patch(all_hunks)
|
|
||||||
|
|
||||||
|
|
||||||
class Differ:
|
|
||||||
|
|
||||||
diff_executor: Optional[concurrent.futures.ThreadPoolExecutor] = None
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.jmap: Dict[str, str] = {}
|
|
||||||
self.jobs: List[str] = []
|
|
||||||
if Differ.diff_executor is None:
|
|
||||||
Differ.diff_executor = self.diff_executor = concurrent.futures.ThreadPoolExecutor(max_workers=os.cpu_count())
|
|
||||||
|
|
||||||
def add_diff(self, file1: str, file2: str) -> None:
|
|
||||||
self.jmap[file1] = file2
|
|
||||||
self.jobs.append(file1)
|
|
||||||
|
|
||||||
def __call__(self, context: int = 3) -> Union[str, Dict[str, Patch]]:
|
|
||||||
global left_lines, right_lines
|
|
||||||
ans: Dict[str, Patch] = {}
|
|
||||||
executor = self.diff_executor
|
|
||||||
assert executor is not None
|
|
||||||
jobs = {executor.submit(run_diff, key, self.jmap[key], context): key for key in self.jobs}
|
|
||||||
for future in concurrent.futures.as_completed(jobs):
|
|
||||||
key = jobs[future]
|
|
||||||
left_path, right_path = key, self.jmap[key]
|
|
||||||
try:
|
|
||||||
ok, returncode, output = future.result()
|
|
||||||
except FileNotFoundError as err:
|
|
||||||
return f'Could not find the {err.filename} executable. Is it in your PATH?'
|
|
||||||
except Exception as e:
|
|
||||||
return f'Running git diff for {left_path} vs. {right_path} generated an exception: {e}'
|
|
||||||
if not ok:
|
|
||||||
return f'{output.decode("utf-8", "replace")}\nRunning git diff for {left_path} vs. {right_path} failed'
|
|
||||||
left_lines = lines_for_path(left_path)
|
|
||||||
right_lines = lines_for_path(right_path)
|
|
||||||
try:
|
|
||||||
patch = parse_patch(output)
|
|
||||||
except Exception:
|
|
||||||
import traceback
|
|
||||||
return f'{traceback.format_exc()}\nParsing diff for {left_path} vs. {right_path} failed'
|
|
||||||
else:
|
|
||||||
ans[key] = patch
|
|
||||||
return ans
|
|
||||||
@ -1,546 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
from gettext import gettext as _
|
|
||||||
from itertools import repeat, zip_longest
|
|
||||||
from math import ceil
|
|
||||||
from typing import Callable, Dict, Generator, Iterable, List, Optional, Tuple
|
|
||||||
|
|
||||||
from kitty.cli_stub import DiffCLIOptions
|
|
||||||
from kitty.fast_data_types import truncate_point_for_length, wcswidth
|
|
||||||
from kitty.types import run_once
|
|
||||||
from kitty.utils import ScreenSize
|
|
||||||
|
|
||||||
from ..tui.images import ImageManager, can_display_images
|
|
||||||
from .collect import Collection, Segment, data_for_path, highlights_for_path, is_image, lines_for_path, path_name_map, sanitize
|
|
||||||
from .config import formats
|
|
||||||
from .diff_speedup import split_with_highlights as _split_with_highlights
|
|
||||||
from .patch import Chunk, Hunk, Patch
|
|
||||||
|
|
||||||
|
|
||||||
class ImageSupportWarning(Warning):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@run_once
|
|
||||||
def images_supported() -> bool:
|
|
||||||
ans = can_display_images()
|
|
||||||
if not ans:
|
|
||||||
warnings.warn('ImageMagick not found images cannot be displayed', ImageSupportWarning)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
class Ref:
|
|
||||||
|
|
||||||
__slots__: Tuple[str, ...] = ()
|
|
||||||
|
|
||||||
def __setattr__(self, name: str, value: object) -> None:
|
|
||||||
raise AttributeError("can't set attribute")
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return '{}({})'.format(self.__class__.__name__, ', '.join(
|
|
||||||
f'{n}={getattr(self, n)}' for n in self.__slots__ if n != '_hash'))
|
|
||||||
|
|
||||||
|
|
||||||
class LineRef(Ref):
|
|
||||||
|
|
||||||
__slots__ = ('src_line_number', 'wrapped_line_idx')
|
|
||||||
src_line_number: int
|
|
||||||
wrapped_line_idx: int
|
|
||||||
|
|
||||||
def __init__(self, sln: int, wli: int = 0) -> None:
|
|
||||||
object.__setattr__(self, 'src_line_number', sln)
|
|
||||||
object.__setattr__(self, 'wrapped_line_idx', wli)
|
|
||||||
|
|
||||||
|
|
||||||
class Reference(Ref):
|
|
||||||
|
|
||||||
__slots__ = ('path', 'extra')
|
|
||||||
path: str
|
|
||||||
extra: Optional[LineRef]
|
|
||||||
|
|
||||||
def __init__(self, path: str, extra: Optional[LineRef] = None) -> None:
|
|
||||||
object.__setattr__(self, 'path', path)
|
|
||||||
object.__setattr__(self, 'extra', extra)
|
|
||||||
|
|
||||||
|
|
||||||
class Line:
|
|
||||||
|
|
||||||
__slots__ = ('text', 'ref', 'is_change_start', 'image_data')
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
text: str,
|
|
||||||
ref: Reference,
|
|
||||||
change_start: bool = False,
|
|
||||||
image_data: Optional[Tuple[Optional['ImagePlacement'], Optional['ImagePlacement']]] = None
|
|
||||||
) -> None:
|
|
||||||
self.text = text
|
|
||||||
self.ref = ref
|
|
||||||
self.is_change_start = change_start
|
|
||||||
self.image_data = image_data
|
|
||||||
|
|
||||||
|
|
||||||
def yield_lines_from(iterator: Iterable[str], reference: Reference, is_change_start: bool = True) -> Generator[Line, None, None]:
|
|
||||||
for text in iterator:
|
|
||||||
yield Line(text, reference, is_change_start)
|
|
||||||
is_change_start = False
|
|
||||||
|
|
||||||
|
|
||||||
def human_readable(size: int, sep: str = ' ') -> str:
|
|
||||||
""" Convert a size in bytes into a human readable form """
|
|
||||||
divisor, suffix = 1, "B"
|
|
||||||
for i, candidate in enumerate(('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB')):
|
|
||||||
if size < (1 << ((i + 1) * 10)):
|
|
||||||
divisor, suffix = (1 << (i * 10)), candidate
|
|
||||||
break
|
|
||||||
s = str(float(size)/divisor)
|
|
||||||
if s.find(".") > -1:
|
|
||||||
s = s[:s.find(".")+2]
|
|
||||||
if s.endswith('.0'):
|
|
||||||
s = s[:-2]
|
|
||||||
return f'{s}{sep}{suffix}'
|
|
||||||
|
|
||||||
|
|
||||||
def fit_in(text: str, count: int) -> str:
|
|
||||||
p = truncate_point_for_length(text, count)
|
|
||||||
if p >= len(text):
|
|
||||||
return text
|
|
||||||
if count > 1:
|
|
||||||
p = truncate_point_for_length(text, count - 1)
|
|
||||||
return f'{text[:p]}…'
|
|
||||||
|
|
||||||
|
|
||||||
def fill_in(text: str, sz: int) -> str:
|
|
||||||
w = wcswidth(text)
|
|
||||||
if w < sz:
|
|
||||||
text += ' ' * (sz - w)
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
def place_in(text: str, sz: int) -> str:
|
|
||||||
return fill_in(fit_in(text, sz), sz)
|
|
||||||
|
|
||||||
|
|
||||||
def format_func(which: str) -> Callable[[str], str]:
|
|
||||||
def formatted(text: str) -> str:
|
|
||||||
fmt = formats[which]
|
|
||||||
return f'\x1b[{fmt}m{text}\x1b[0m'
|
|
||||||
formatted.__name__ = f'{which}_format'
|
|
||||||
return formatted
|
|
||||||
|
|
||||||
|
|
||||||
text_format = format_func('text')
|
|
||||||
title_format = format_func('title')
|
|
||||||
margin_format = format_func('margin')
|
|
||||||
added_format = format_func('added')
|
|
||||||
removed_format = format_func('removed')
|
|
||||||
removed_margin_format = format_func('removed_margin')
|
|
||||||
added_margin_format = format_func('added_margin')
|
|
||||||
filler_format = format_func('filler')
|
|
||||||
margin_filler_format = format_func('margin_filler')
|
|
||||||
hunk_margin_format = format_func('hunk_margin')
|
|
||||||
hunk_format = format_func('hunk')
|
|
||||||
highlight_map = {'remove': ('removed_highlight', 'removed'), 'add': ('added_highlight', 'added')}
|
|
||||||
|
|
||||||
|
|
||||||
def highlight_boundaries(ltype: str) -> Tuple[str, str]:
|
|
||||||
s, e = highlight_map[ltype]
|
|
||||||
start = f'\x1b[{formats[s]}m'
|
|
||||||
stop = f'\x1b[{formats[e]}m'
|
|
||||||
return start, stop
|
|
||||||
|
|
||||||
|
|
||||||
def title_lines(left_path: Optional[str], right_path: Optional[str], args: DiffCLIOptions, columns: int, margin_size: int) -> Generator[str, None, None]:
|
|
||||||
m = ' ' * margin_size
|
|
||||||
left_name = path_name_map.get(left_path) if left_path else None
|
|
||||||
right_name = path_name_map.get(right_path) if right_path else None
|
|
||||||
if right_name and right_name != left_name:
|
|
||||||
n1 = fit_in(m + sanitize(left_name or ''), columns // 2 - margin_size)
|
|
||||||
n1 = place_in(n1, columns // 2)
|
|
||||||
n2 = fit_in(m + sanitize(right_name), columns // 2 - margin_size)
|
|
||||||
n2 = place_in(n2, columns // 2)
|
|
||||||
name = n1 + n2
|
|
||||||
else:
|
|
||||||
name = place_in(m + sanitize(left_name or ''), columns)
|
|
||||||
yield title_format(place_in(name, columns))
|
|
||||||
yield title_format('━' * columns)
|
|
||||||
|
|
||||||
|
|
||||||
def binary_lines(path: Optional[str], other_path: Optional[str], columns: int, margin_size: int) -> Generator[str, None, None]:
|
|
||||||
template = _('Binary file: {}')
|
|
||||||
available_cols = columns // 2 - margin_size
|
|
||||||
|
|
||||||
def fl(path: str, fmt: Callable[[str], str]) -> str:
|
|
||||||
text = template.format(human_readable(len(data_for_path(path))))
|
|
||||||
text = place_in(text, available_cols)
|
|
||||||
return margin_format(' ' * margin_size) + fmt(text)
|
|
||||||
|
|
||||||
if path is None:
|
|
||||||
filler = render_diff_line('', '', 'filler', margin_size, available_cols)
|
|
||||||
assert other_path is not None
|
|
||||||
yield filler + fl(other_path, added_format)
|
|
||||||
elif other_path is None:
|
|
||||||
filler = render_diff_line('', '', 'filler', margin_size, available_cols)
|
|
||||||
yield fl(path, removed_format) + filler
|
|
||||||
else:
|
|
||||||
yield fl(path, removed_format) + fl(other_path, added_format)
|
|
||||||
|
|
||||||
|
|
||||||
def split_to_size(line: str, width: int) -> Generator[str, None, None]:
|
|
||||||
if not line:
|
|
||||||
yield line
|
|
||||||
while line:
|
|
||||||
p = truncate_point_for_length(line, width)
|
|
||||||
yield line[:p]
|
|
||||||
line = line[p:]
|
|
||||||
|
|
||||||
|
|
||||||
def truncate_points(line: str, width: int) -> Generator[int, None, None]:
|
|
||||||
pos = 0
|
|
||||||
sz = len(line)
|
|
||||||
while True:
|
|
||||||
pos = truncate_point_for_length(line, width, pos)
|
|
||||||
if pos < sz:
|
|
||||||
yield pos
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def split_with_highlights(line: str, width: int, highlights: List[Segment], bg_highlight: Optional[Segment] = None) -> List[str]:
|
|
||||||
truncate_pts = list(truncate_points(line, width))
|
|
||||||
return _split_with_highlights(line, truncate_pts, highlights, bg_highlight)
|
|
||||||
|
|
||||||
|
|
||||||
margin_bg_map = {'filler': margin_filler_format, 'remove': removed_margin_format, 'add': added_margin_format, 'context': margin_format}
|
|
||||||
text_bg_map = {'filler': filler_format, 'remove': removed_format, 'add': added_format, 'context': text_format}
|
|
||||||
|
|
||||||
|
|
||||||
class DiffData:
|
|
||||||
|
|
||||||
def __init__(self, left_path: str, right_path: str, available_cols: int, margin_size: int):
|
|
||||||
self.left_path, self.right_path = left_path, right_path
|
|
||||||
self.available_cols = available_cols
|
|
||||||
self.margin_size = margin_size
|
|
||||||
self.left_lines, self.right_lines = map(lines_for_path, (left_path, right_path))
|
|
||||||
self.filler_line = render_diff_line('', '', 'filler', margin_size, available_cols)
|
|
||||||
self.left_filler_line = render_diff_line('', '', 'remove', margin_size, available_cols)
|
|
||||||
self.right_filler_line = render_diff_line('', '', 'add', margin_size, available_cols)
|
|
||||||
self.left_hdata = highlights_for_path(left_path)
|
|
||||||
self.right_hdata = highlights_for_path(right_path)
|
|
||||||
|
|
||||||
def left_highlights_for_line(self, line_num: int) -> List[Segment]:
|
|
||||||
if line_num < len(self.left_hdata):
|
|
||||||
return self.left_hdata[line_num]
|
|
||||||
return []
|
|
||||||
|
|
||||||
def right_highlights_for_line(self, line_num: int) -> List[Segment]:
|
|
||||||
if line_num < len(self.right_hdata):
|
|
||||||
return self.right_hdata[line_num]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def render_diff_line(number: Optional[str], text: str, ltype: str, margin_size: int, available_cols: int) -> str:
|
|
||||||
margin = margin_bg_map[ltype](place_in(number or '', margin_size))
|
|
||||||
content = text_bg_map[ltype](fill_in(text or '', available_cols))
|
|
||||||
return margin + content
|
|
||||||
|
|
||||||
|
|
||||||
def render_diff_pair(
|
|
||||||
left_line_number: Optional[str], left: str, left_is_change: bool,
|
|
||||||
right_line_number: Optional[str], right: str, right_is_change: bool,
|
|
||||||
is_first: bool, margin_size: int, available_cols: int
|
|
||||||
) -> str:
|
|
||||||
ltype = 'filler' if left_line_number is None else ('remove' if left_is_change else 'context')
|
|
||||||
rtype = 'filler' if right_line_number is None else ('add' if right_is_change else 'context')
|
|
||||||
return (
|
|
||||||
render_diff_line(left_line_number if is_first else None, left, ltype, margin_size, available_cols) +
|
|
||||||
render_diff_line(right_line_number if is_first else None, right, rtype, margin_size, available_cols)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def hunk_title(hunk_num: int, hunk: Hunk, margin_size: int, available_cols: int) -> str:
|
|
||||||
m = hunk_margin_format(' ' * margin_size)
|
|
||||||
t = f'@@ -{hunk.left_start + 1},{hunk.left_count} +{hunk.right_start + 1},{hunk.right_count} @@ {hunk.title}'
|
|
||||||
return m + hunk_format(place_in(t, available_cols))
|
|
||||||
|
|
||||||
|
|
||||||
def render_half_line(
|
|
||||||
line_number: int,
|
|
||||||
line: str,
|
|
||||||
highlights: List[Segment],
|
|
||||||
ltype: str,
|
|
||||||
margin_size: int,
|
|
||||||
available_cols: int,
|
|
||||||
changed_center: Optional[Tuple[int, int]] = None
|
|
||||||
) -> Generator[str, None, None]:
|
|
||||||
bg_highlight: Optional[Segment] = None
|
|
||||||
if changed_center is not None and changed_center[0]:
|
|
||||||
prefix_count, suffix_count = changed_center
|
|
||||||
line_sz = len(line)
|
|
||||||
if prefix_count + suffix_count < line_sz:
|
|
||||||
start, stop = highlight_boundaries(ltype)
|
|
||||||
seg = Segment(prefix_count, start)
|
|
||||||
seg.end = line_sz - suffix_count
|
|
||||||
seg.end_code = stop
|
|
||||||
bg_highlight = seg
|
|
||||||
if highlights or bg_highlight:
|
|
||||||
lines: Iterable[str] = split_with_highlights(line, available_cols, highlights, bg_highlight)
|
|
||||||
else:
|
|
||||||
lines = split_to_size(line, available_cols)
|
|
||||||
lnum = str(line_number + 1)
|
|
||||||
for line in lines:
|
|
||||||
yield render_diff_line(lnum, line, ltype, margin_size, available_cols)
|
|
||||||
lnum = ''
|
|
||||||
|
|
||||||
|
|
||||||
def lines_for_chunk(data: DiffData, hunk_num: int, chunk: Chunk, chunk_num: int) -> Generator[Line, None, None]:
|
|
||||||
if chunk.is_context:
|
|
||||||
for i in range(chunk.left_count):
|
|
||||||
left_line_number = line_ref = chunk.left_start + i
|
|
||||||
right_line_number = chunk.right_start + i
|
|
||||||
highlights = data.left_highlights_for_line(left_line_number)
|
|
||||||
if highlights:
|
|
||||||
lines: Iterable[str] = split_with_highlights(data.left_lines[left_line_number], data.available_cols, highlights)
|
|
||||||
else:
|
|
||||||
lines = split_to_size(data.left_lines[left_line_number], data.available_cols)
|
|
||||||
left_line_number_s = str(left_line_number + 1)
|
|
||||||
right_line_number_s = str(right_line_number + 1)
|
|
||||||
for wli, text in enumerate(lines):
|
|
||||||
line = render_diff_line(left_line_number_s, text, 'context', data.margin_size, data.available_cols)
|
|
||||||
if right_line_number_s == left_line_number_s:
|
|
||||||
r = line
|
|
||||||
else:
|
|
||||||
r = render_diff_line(right_line_number_s, text, 'context', data.margin_size, data.available_cols)
|
|
||||||
ref = Reference(data.left_path, LineRef(line_ref, wli))
|
|
||||||
yield Line(line + r, ref)
|
|
||||||
left_line_number_s = right_line_number_s = ''
|
|
||||||
else:
|
|
||||||
common = min(chunk.left_count, chunk.right_count)
|
|
||||||
for i in range(max(chunk.left_count, chunk.right_count)):
|
|
||||||
ll: List[str] = []
|
|
||||||
rl: List[str] = []
|
|
||||||
if i < chunk.left_count:
|
|
||||||
rln = ref_ln = chunk.left_start + i
|
|
||||||
ll.extend(render_half_line(
|
|
||||||
rln, data.left_lines[rln], data.left_highlights_for_line(rln),
|
|
||||||
'remove', data.margin_size, data.available_cols,
|
|
||||||
None if chunk.centers is None else chunk.centers[i]))
|
|
||||||
ref_path = data.left_path
|
|
||||||
if i < chunk.right_count:
|
|
||||||
rln = ref_ln = chunk.right_start + i
|
|
||||||
rl.extend(render_half_line(
|
|
||||||
rln, data.right_lines[rln], data.right_highlights_for_line(rln),
|
|
||||||
'add', data.margin_size, data.available_cols,
|
|
||||||
None if chunk.centers is None else chunk.centers[i]))
|
|
||||||
ref_path = data.right_path
|
|
||||||
if i < common:
|
|
||||||
extra = len(ll) - len(rl)
|
|
||||||
if extra != 0:
|
|
||||||
if extra < 0:
|
|
||||||
x, fl = ll, data.left_filler_line
|
|
||||||
extra = -extra
|
|
||||||
else:
|
|
||||||
x, fl = rl, data.right_filler_line
|
|
||||||
x.extend(repeat(fl, extra))
|
|
||||||
else:
|
|
||||||
if ll:
|
|
||||||
x, count = rl, len(ll)
|
|
||||||
else:
|
|
||||||
x, count = ll, len(rl)
|
|
||||||
x.extend(repeat(data.filler_line, count))
|
|
||||||
for wli, (left_line, right_line) in enumerate(zip(ll, rl)):
|
|
||||||
ref = Reference(ref_path, LineRef(ref_ln, wli))
|
|
||||||
yield Line(left_line + right_line, ref, i == 0 and wli == 0)
|
|
||||||
|
|
||||||
|
|
||||||
def lines_for_diff(left_path: str, right_path: str, hunks: Iterable[Hunk], args: DiffCLIOptions, columns: int, margin_size: int) -> Generator[Line, None, None]:
|
|
||||||
available_cols = columns // 2 - margin_size
|
|
||||||
data = DiffData(left_path, right_path, available_cols, margin_size)
|
|
||||||
|
|
||||||
for hunk_num, hunk in enumerate(hunks):
|
|
||||||
yield Line(hunk_title(hunk_num, hunk, margin_size, columns - margin_size), Reference(left_path, LineRef(hunk.left_start)))
|
|
||||||
for cnum, chunk in enumerate(hunk.chunks):
|
|
||||||
yield from lines_for_chunk(data, hunk_num, chunk, cnum)
|
|
||||||
|
|
||||||
|
|
||||||
def all_lines(path: str, args: DiffCLIOptions, columns: int, margin_size: int, is_add: bool = True) -> Generator[Line, None, None]:
|
|
||||||
available_cols = columns // 2 - margin_size
|
|
||||||
ltype = 'add' if is_add else 'remove'
|
|
||||||
lines = lines_for_path(path)
|
|
||||||
filler = render_diff_line('', '', 'filler', margin_size, available_cols)
|
|
||||||
msg_written = False
|
|
||||||
hdata = highlights_for_path(path)
|
|
||||||
|
|
||||||
def highlights(num: int) -> List[Segment]:
|
|
||||||
return hdata[num] if num < len(hdata) else []
|
|
||||||
|
|
||||||
for line_number, line in enumerate(lines):
|
|
||||||
h = render_half_line(line_number, line, highlights(line_number), ltype, margin_size, available_cols)
|
|
||||||
for i, hl in enumerate(h):
|
|
||||||
ref = Reference(path, LineRef(line_number, i))
|
|
||||||
empty = filler
|
|
||||||
if not msg_written:
|
|
||||||
msg_written = True
|
|
||||||
empty = render_diff_line(
|
|
||||||
'', _('This file was added') if is_add else _('This file was removed'),
|
|
||||||
'filler', margin_size, available_cols)
|
|
||||||
text = (empty + hl) if is_add else (hl + empty)
|
|
||||||
yield Line(text, ref, line_number == 0 and i == 0)
|
|
||||||
|
|
||||||
|
|
||||||
def rename_lines(path: str, other_path: str, args: DiffCLIOptions, columns: int, margin_size: int) -> Generator[str, None, None]:
|
|
||||||
m = ' ' * margin_size
|
|
||||||
for line in split_to_size(_('The file {0} was renamed to {1}').format(
|
|
||||||
sanitize(path_name_map[path]), sanitize(path_name_map[other_path])), columns - margin_size):
|
|
||||||
yield m + line
|
|
||||||
|
|
||||||
|
|
||||||
class Image:
|
|
||||||
|
|
||||||
def __init__(self, image_id: int, width: int, height: int, margin_size: int, screen_size: ScreenSize) -> None:
|
|
||||||
self.image_id = image_id
|
|
||||||
self.width, self.height = width, height
|
|
||||||
self.rows = int(ceil(self.height / screen_size.cell_height))
|
|
||||||
self.columns = int(ceil(self.width / screen_size.cell_width))
|
|
||||||
self.margin_size = margin_size
|
|
||||||
|
|
||||||
|
|
||||||
class ImagePlacement:
|
|
||||||
|
|
||||||
def __init__(self, image: Image, row: int) -> None:
|
|
||||||
self.image = image
|
|
||||||
self.row = row
|
|
||||||
|
|
||||||
|
|
||||||
def render_image(
|
|
||||||
path: str,
|
|
||||||
is_left: bool,
|
|
||||||
available_cols: int, margin_size: int,
|
|
||||||
image_manager: ImageManager
|
|
||||||
) -> Generator[Tuple[str, Reference, Optional[ImagePlacement]], None, None]:
|
|
||||||
lnum = 0
|
|
||||||
margin_fmt = removed_margin_format if is_left else added_margin_format
|
|
||||||
m = margin_fmt(' ' * margin_size)
|
|
||||||
fmt = removed_format if is_left else added_format
|
|
||||||
|
|
||||||
def yield_split(text: str) -> Generator[Tuple[str, Reference, Optional[ImagePlacement]], None, None]:
|
|
||||||
nonlocal lnum
|
|
||||||
for i, line in enumerate(split_to_size(text, available_cols)):
|
|
||||||
yield m + fmt(place_in(line, available_cols)), Reference(path, LineRef(lnum, i)), None
|
|
||||||
lnum += 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
image_id, width, height = image_manager.send_image(path, available_cols - margin_size, image_manager.screen_size.rows - 2)
|
|
||||||
except Exception as e:
|
|
||||||
yield from yield_split(_('Failed to render image, with error:'))
|
|
||||||
yield from yield_split(' '.join(str(e).splitlines()))
|
|
||||||
return
|
|
||||||
meta = _('Dimensions: {0}x{1} pixels Size: {2}').format(
|
|
||||||
width, height, human_readable(len(data_for_path(path))))
|
|
||||||
yield from yield_split(meta)
|
|
||||||
bg_line = m + fmt(' ' * available_cols)
|
|
||||||
img = Image(image_id, width, height, margin_size, image_manager.screen_size)
|
|
||||||
for r in range(img.rows):
|
|
||||||
yield bg_line, Reference(path, LineRef(lnum)), ImagePlacement(img, r)
|
|
||||||
lnum += 1
|
|
||||||
|
|
||||||
|
|
||||||
def image_lines(
|
|
||||||
left_path: Optional[str],
|
|
||||||
right_path: Optional[str],
|
|
||||||
columns: int,
|
|
||||||
margin_size: int,
|
|
||||||
image_manager: ImageManager
|
|
||||||
) -> Generator[Line, None, None]:
|
|
||||||
available_cols = columns // 2 - margin_size
|
|
||||||
left_lines: Iterable[Tuple[str, Reference, Optional[ImagePlacement]]] = iter(())
|
|
||||||
right_lines: Iterable[Tuple[str, Reference, Optional[ImagePlacement]]] = iter(())
|
|
||||||
if left_path is not None:
|
|
||||||
left_lines = render_image(left_path, True, available_cols, margin_size, image_manager)
|
|
||||||
if right_path is not None:
|
|
||||||
right_lines = render_image(right_path, False, available_cols, margin_size, image_manager)
|
|
||||||
filler = ' ' * (available_cols + margin_size)
|
|
||||||
is_change_start = True
|
|
||||||
for left, right in zip_longest(left_lines, right_lines):
|
|
||||||
left_placement = right_placement = None
|
|
||||||
if left is None:
|
|
||||||
left = filler
|
|
||||||
right, ref, right_placement = right
|
|
||||||
elif right is None:
|
|
||||||
right = filler
|
|
||||||
left, ref, left_placement = left
|
|
||||||
else:
|
|
||||||
right, ref, right_placement = right
|
|
||||||
left, ref, left_placement = left
|
|
||||||
image_data = (left_placement, right_placement) if left_placement or right_placement else None
|
|
||||||
yield Line(left + right, ref, is_change_start, image_data)
|
|
||||||
is_change_start = False
|
|
||||||
|
|
||||||
|
|
||||||
class RenderDiff:
|
|
||||||
|
|
||||||
margin_size: int = 0
|
|
||||||
|
|
||||||
def __call__(
|
|
||||||
self,
|
|
||||||
collection: Collection,
|
|
||||||
diff_map: Dict[str, Patch],
|
|
||||||
args: DiffCLIOptions,
|
|
||||||
columns: int,
|
|
||||||
image_manager: ImageManager
|
|
||||||
) -> Generator[Line, None, None]:
|
|
||||||
largest_line_number = 0
|
|
||||||
for path, item_type, other_path in collection:
|
|
||||||
if item_type == 'diff':
|
|
||||||
patch = diff_map.get(path)
|
|
||||||
if patch is not None:
|
|
||||||
largest_line_number = max(largest_line_number, patch.largest_line_number)
|
|
||||||
|
|
||||||
margin_size = self.margin_size = max(3, len(str(largest_line_number)) + 1)
|
|
||||||
last_item_num = len(collection) - 1
|
|
||||||
|
|
||||||
for i, (path, item_type, other_path) in enumerate(collection):
|
|
||||||
item_ref = Reference(path)
|
|
||||||
is_binary = isinstance(data_for_path(path), bytes)
|
|
||||||
if not is_binary and item_type == 'diff' and isinstance(data_for_path(other_path), bytes):
|
|
||||||
is_binary = True
|
|
||||||
is_img = is_binary and (is_image(path) or is_image(other_path)) and images_supported()
|
|
||||||
yield from yield_lines_from(title_lines(path, other_path, args, columns, margin_size), item_ref, False)
|
|
||||||
if item_type == 'diff':
|
|
||||||
if is_binary:
|
|
||||||
if is_img:
|
|
||||||
ans = image_lines(path, other_path, columns, margin_size, image_manager)
|
|
||||||
else:
|
|
||||||
ans = yield_lines_from(binary_lines(path, other_path, columns, margin_size), item_ref)
|
|
||||||
else:
|
|
||||||
assert other_path is not None
|
|
||||||
ans = lines_for_diff(path, other_path, diff_map[path], args, columns, margin_size)
|
|
||||||
elif item_type == 'add':
|
|
||||||
if is_binary:
|
|
||||||
if is_img:
|
|
||||||
ans = image_lines(None, path, columns, margin_size, image_manager)
|
|
||||||
else:
|
|
||||||
ans = yield_lines_from(binary_lines(None, path, columns, margin_size), item_ref)
|
|
||||||
else:
|
|
||||||
ans = all_lines(path, args, columns, margin_size, is_add=True)
|
|
||||||
elif item_type == 'removal':
|
|
||||||
if is_binary:
|
|
||||||
if is_img:
|
|
||||||
ans = image_lines(path, None, columns, margin_size, image_manager)
|
|
||||||
else:
|
|
||||||
ans = yield_lines_from(binary_lines(path, None, columns, margin_size), item_ref)
|
|
||||||
else:
|
|
||||||
ans = all_lines(path, args, columns, margin_size, is_add=False)
|
|
||||||
elif item_type == 'rename':
|
|
||||||
assert other_path is not None
|
|
||||||
ans = yield_lines_from(rename_lines(path, other_path, args, columns, margin_size), item_ref)
|
|
||||||
else:
|
|
||||||
raise ValueError(f'Unsupported item type: {item_type}')
|
|
||||||
yield from ans
|
|
||||||
if i < last_item_num:
|
|
||||||
yield Line('', item_ref)
|
|
||||||
|
|
||||||
|
|
||||||
render_diff = RenderDiff()
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
|
||||||
|
|
||||||
import re
|
|
||||||
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Tuple
|
|
||||||
|
|
||||||
from kitty.fast_data_types import wcswidth
|
|
||||||
|
|
||||||
from ..tui.operations import styled
|
|
||||||
from .options.types import Options as DiffOptions
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .render import Line
|
|
||||||
Line
|
|
||||||
|
|
||||||
|
|
||||||
class BadRegex(ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Search:
|
|
||||||
|
|
||||||
def __init__(self, opts: DiffOptions, query: str, is_regex: bool, is_backward: bool):
|
|
||||||
self.matches: Dict[int, List[Tuple[int, str]]] = {}
|
|
||||||
self.count = 0
|
|
||||||
self.style = styled('|', fg=opts.search_fg, bg=opts.search_bg).split('|', 1)[0]
|
|
||||||
if not is_regex:
|
|
||||||
query = re.escape(query)
|
|
||||||
try:
|
|
||||||
self.pat = re.compile(query, flags=re.UNICODE | re.IGNORECASE)
|
|
||||||
except Exception:
|
|
||||||
raise BadRegex(f'Not a valid regex: {query}')
|
|
||||||
|
|
||||||
def __call__(self, diff_lines: Iterable['Line'], margin_size: int, cols: int) -> bool:
|
|
||||||
self.matches = {}
|
|
||||||
self.count = 0
|
|
||||||
half_width = cols // 2
|
|
||||||
strip_pat = re.compile('\033[[].*?m')
|
|
||||||
right_offset = half_width + 1 + margin_size
|
|
||||||
find = self.pat.finditer
|
|
||||||
for i, line in enumerate(diff_lines):
|
|
||||||
text = strip_pat.sub('', line.text)
|
|
||||||
left, right = text[margin_size:half_width + 1], text[right_offset:]
|
|
||||||
matches = []
|
|
||||||
|
|
||||||
def add(which: str, offset: int) -> None:
|
|
||||||
for m in find(which):
|
|
||||||
before = which[:m.start()]
|
|
||||||
matches.append((wcswidth(before) + offset, m.group()))
|
|
||||||
self.count += 1
|
|
||||||
|
|
||||||
add(left, margin_size)
|
|
||||||
add(right, right_offset)
|
|
||||||
if matches:
|
|
||||||
self.matches[i] = matches
|
|
||||||
return bool(self.matches)
|
|
||||||
|
|
||||||
def __contains__(self, i: int) -> bool:
|
|
||||||
return i in self.matches
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return self.count
|
|
||||||
|
|
||||||
def highlight_line(self, write: Callable[[str], None], line_num: int) -> bool:
|
|
||||||
highlights = self.matches.get(line_num)
|
|
||||||
if not highlights:
|
|
||||||
return False
|
|
||||||
write(self.style)
|
|
||||||
for start, text in highlights:
|
|
||||||
write(f'\r\x1b[{start}C{text}')
|
|
||||||
write('\x1b[m')
|
|
||||||
return True
|
|
||||||
@ -1,239 +0,0 @@
|
|||||||
/*
|
|
||||||
* speedup.c
|
|
||||||
* Copyright (C) 2018 Kovid Goyal <kovid at kovidgoyal.net>
|
|
||||||
*
|
|
||||||
* Distributed under terms of the GPL3 license.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "data-types.h"
|
|
||||||
|
|
||||||
static PyObject*
|
|
||||||
changed_center(PyObject *self UNUSED, PyObject *args) {
|
|
||||||
unsigned int prefix_count = 0, suffix_count = 0;
|
|
||||||
PyObject *lp, *rp;
|
|
||||||
if (!PyArg_ParseTuple(args, "UU", &lp, &rp)) return NULL;
|
|
||||||
const size_t left_len = PyUnicode_GET_LENGTH(lp), right_len = PyUnicode_GET_LENGTH(rp);
|
|
||||||
|
|
||||||
#define R(which, index) PyUnicode_READ(PyUnicode_KIND(which), PyUnicode_DATA(which), index)
|
|
||||||
while(prefix_count < MIN(left_len, right_len)) {
|
|
||||||
if (R(lp, prefix_count) != R(rp, prefix_count)) break;
|
|
||||||
prefix_count++;
|
|
||||||
}
|
|
||||||
if (left_len && right_len && prefix_count < MIN(left_len, right_len)) {
|
|
||||||
while(suffix_count < MIN(left_len - prefix_count, right_len - prefix_count)) {
|
|
||||||
if(R(lp, left_len - 1 - suffix_count) != R(rp, right_len - 1 - suffix_count)) break;
|
|
||||||
suffix_count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#undef R
|
|
||||||
return Py_BuildValue("II", prefix_count, suffix_count);
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
unsigned int start_pos, end_pos, current_pos;
|
|
||||||
PyObject *start_code, *end_code;
|
|
||||||
} Segment;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
Segment sg;
|
|
||||||
unsigned int num, pos;
|
|
||||||
} SegmentPointer;
|
|
||||||
|
|
||||||
static const Segment EMPTY_SEGMENT = { .current_pos = UINT_MAX };
|
|
||||||
|
|
||||||
static bool
|
|
||||||
convert_segment(PyObject *highlight, Segment *dest) {
|
|
||||||
PyObject *val = NULL;
|
|
||||||
#define I
|
|
||||||
#define A(x, d, c) { \
|
|
||||||
val = PyObject_GetAttrString(highlight, #x); \
|
|
||||||
if (val == NULL) return false; \
|
|
||||||
dest->d = c(val); Py_DECREF(val); \
|
|
||||||
}
|
|
||||||
A(start, start_pos, PyLong_AsUnsignedLong);
|
|
||||||
A(end, end_pos, PyLong_AsUnsignedLong);
|
|
||||||
dest->current_pos = dest->start_pos;
|
|
||||||
A(start_code, start_code, I);
|
|
||||||
A(end_code, end_code, I);
|
|
||||||
if (!PyUnicode_Check(dest->start_code)) { PyErr_SetString(PyExc_TypeError, "start_code is not a string"); return false; }
|
|
||||||
if (!PyUnicode_Check(dest->end_code)) { PyErr_SetString(PyExc_TypeError, "end_code is not a string"); return false; }
|
|
||||||
#undef A
|
|
||||||
#undef I
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
next_segment(SegmentPointer *s, PyObject *highlights) {
|
|
||||||
if (s->pos < s->num) {
|
|
||||||
if (!convert_segment(PyList_GET_ITEM(highlights, s->pos), &s->sg)) return false;
|
|
||||||
s->pos++;
|
|
||||||
} else s->sg.current_pos = UINT_MAX;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef struct LineBuffer {
|
|
||||||
Py_UCS4 *buf;
|
|
||||||
size_t pos, capacity;
|
|
||||||
} LineBuffer;
|
|
||||||
|
|
||||||
|
|
||||||
static bool
|
|
||||||
ensure_space(LineBuffer *lb, size_t num) {
|
|
||||||
if (lb->pos + num >= lb->capacity) {
|
|
||||||
size_t new_cap = MAX(lb->capacity * 2, 4096u);
|
|
||||||
new_cap = MAX(lb->pos + num + 1024u, new_cap);
|
|
||||||
lb->buf = realloc(lb->buf, new_cap * sizeof(lb->buf[0]));
|
|
||||||
if (!lb->buf) { PyErr_NoMemory(); return false; }
|
|
||||||
lb->capacity = new_cap;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
insert_code(PyObject *code, LineBuffer *lb) {
|
|
||||||
unsigned int csz = PyUnicode_GET_LENGTH(code);
|
|
||||||
if (!ensure_space(lb, csz)) return false;
|
|
||||||
for (unsigned int s = 0; s < csz; s++) lb->buf[lb->pos++] = PyUnicode_READ(PyUnicode_KIND(code), PyUnicode_DATA(code), s);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
add_line(Segment *bg_segment, Segment *fg_segment, LineBuffer *lb, PyObject *ans) {
|
|
||||||
bool bg_is_active = bg_segment->current_pos == bg_segment->end_pos, fg_is_active = fg_segment->current_pos == fg_segment->end_pos;
|
|
||||||
if (bg_is_active) { if(!insert_code(bg_segment->end_code, lb)) return false; }
|
|
||||||
if (fg_is_active) { if(!insert_code(fg_segment->end_code, lb)) return false; }
|
|
||||||
PyObject *wl = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, lb->buf, lb->pos);
|
|
||||||
if (!wl) return false;
|
|
||||||
int ret = PyList_Append(ans, wl); Py_DECREF(wl); if (ret != 0) return false;
|
|
||||||
lb->pos = 0;
|
|
||||||
if (bg_is_active) { if(!insert_code(bg_segment->start_code, lb)) return false; }
|
|
||||||
if (fg_is_active) { if(!insert_code(fg_segment->start_code, lb)) return false; }
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static LineBuffer line_buffer;
|
|
||||||
|
|
||||||
static PyObject*
|
|
||||||
split_with_highlights(PyObject *self UNUSED, PyObject *args) {
|
|
||||||
PyObject *line, *truncate_points_py, *fg_highlights, *bg_highlight;
|
|
||||||
if (!PyArg_ParseTuple(args, "UO!O!O", &line, &PyList_Type, &truncate_points_py, &PyList_Type, &fg_highlights, &bg_highlight)) return NULL;
|
|
||||||
PyObject *ans = PyList_New(0);
|
|
||||||
if (!ans) return NULL;
|
|
||||||
static unsigned int truncate_points[256];
|
|
||||||
unsigned int num_truncate_pts = PyList_GET_SIZE(truncate_points_py), truncate_pos = 0, truncate_point;
|
|
||||||
for (unsigned int i = 0; i < MIN(num_truncate_pts, arraysz(truncate_points)); i++) {
|
|
||||||
truncate_points[i] = PyLong_AsUnsignedLong(PyList_GET_ITEM(truncate_points_py, i));
|
|
||||||
}
|
|
||||||
SegmentPointer fg_segment = { .sg = EMPTY_SEGMENT, .num = PyList_GET_SIZE(fg_highlights)}, bg_segment = { .sg = EMPTY_SEGMENT };
|
|
||||||
if (bg_highlight != Py_None) { if (!convert_segment(bg_highlight, &bg_segment.sg)) { Py_CLEAR(ans); return NULL; }; bg_segment.num = 1; }
|
|
||||||
#define CHECK_CALL(func, ...) if (!func(__VA_ARGS__)) { Py_CLEAR(ans); if (!PyErr_Occurred()) PyErr_SetString(PyExc_ValueError, "unknown error while processing line"); return NULL; }
|
|
||||||
CHECK_CALL(next_segment, &fg_segment, fg_highlights);
|
|
||||||
|
|
||||||
#define NEXT_TRUNCATE_POINT truncate_point = (truncate_pos < num_truncate_pts) ? truncate_points[truncate_pos++] : UINT_MAX
|
|
||||||
NEXT_TRUNCATE_POINT;
|
|
||||||
|
|
||||||
#define INSERT_CODE(x) { CHECK_CALL(insert_code, x, &line_buffer); }
|
|
||||||
|
|
||||||
#define ADD_LINE CHECK_CALL(add_line, &bg_segment.sg, &fg_segment.sg, &line_buffer, ans);
|
|
||||||
|
|
||||||
#define ADD_CHAR(x) { \
|
|
||||||
if (!ensure_space(&line_buffer, 1)) { Py_CLEAR(ans); return NULL; } \
|
|
||||||
line_buffer.buf[line_buffer.pos++] = x; \
|
|
||||||
}
|
|
||||||
#define CHECK_SEGMENT(sgp, is_fg) { \
|
|
||||||
if (i == sgp.sg.current_pos) { \
|
|
||||||
INSERT_CODE(sgp.sg.current_pos == sgp.sg.start_pos ? sgp.sg.start_code : sgp.sg.end_code); \
|
|
||||||
if (sgp.sg.current_pos == sgp.sg.start_pos) sgp.sg.current_pos = sgp.sg.end_pos; \
|
|
||||||
else { \
|
|
||||||
if (is_fg) { \
|
|
||||||
CHECK_CALL(next_segment, &fg_segment, fg_highlights); \
|
|
||||||
if (sgp.sg.current_pos == i) { \
|
|
||||||
INSERT_CODE(sgp.sg.start_code); \
|
|
||||||
sgp.sg.current_pos = sgp.sg.end_pos; \
|
|
||||||
} \
|
|
||||||
} else sgp.sg.current_pos = UINT_MAX; \
|
|
||||||
} \
|
|
||||||
}\
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsigned int line_sz = PyUnicode_GET_LENGTH(line);
|
|
||||||
line_buffer.pos = 0;
|
|
||||||
unsigned int i = 0;
|
|
||||||
for (; i < line_sz; i++) {
|
|
||||||
if (i == truncate_point) { ADD_LINE; NEXT_TRUNCATE_POINT; }
|
|
||||||
CHECK_SEGMENT(bg_segment, false);
|
|
||||||
CHECK_SEGMENT(fg_segment, true)
|
|
||||||
ADD_CHAR(PyUnicode_READ(PyUnicode_KIND(line), PyUnicode_DATA(line), i));
|
|
||||||
}
|
|
||||||
if (line_buffer.pos) ADD_LINE;
|
|
||||||
return ans;
|
|
||||||
#undef INSERT_CODE
|
|
||||||
#undef CHECK_SEGMENT
|
|
||||||
#undef CHECK_CALL
|
|
||||||
#undef ADD_CHAR
|
|
||||||
#undef ADD_LINE
|
|
||||||
#undef NEXT_TRUNCATE_POINT
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyObject*
|
|
||||||
splitlines_like_git(PyObject *self UNUSED, PyObject *args) {
|
|
||||||
char *raw; Py_ssize_t sz;
|
|
||||||
PyObject *callback;
|
|
||||||
if (!PyArg_ParseTuple(args, "y#O", &raw, &sz, &callback)) return NULL;
|
|
||||||
while (sz > 0 && (raw[sz-1] == '\n' || raw[sz-1] == '\r')) sz--;
|
|
||||||
PyObject *mv, *ret;
|
|
||||||
#define CALLBACK \
|
|
||||||
mv = PyMemoryView_FromMemory(raw + start, i - start, PyBUF_READ); \
|
|
||||||
if (mv == NULL) return NULL; \
|
|
||||||
ret = PyObject_CallFunctionObjArgs(callback, mv, NULL); \
|
|
||||||
Py_DECREF(mv); \
|
|
||||||
if (ret == NULL) return NULL; \
|
|
||||||
Py_DECREF(ret); start = i + 1;
|
|
||||||
|
|
||||||
Py_ssize_t i = 0, start = 0;
|
|
||||||
for (; i < sz; i++) {
|
|
||||||
switch (raw[i]) {
|
|
||||||
case '\n':
|
|
||||||
CALLBACK; break;
|
|
||||||
case '\r':
|
|
||||||
CALLBACK;
|
|
||||||
if (i + 1 < sz && raw[i+1] == '\n') { i++; start++; }
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (start < sz) {
|
|
||||||
i = sz; CALLBACK;
|
|
||||||
}
|
|
||||||
Py_RETURN_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void
|
|
||||||
free_resources(void) {
|
|
||||||
free(line_buffer.buf); line_buffer.buf = NULL; line_buffer.capacity = 0; line_buffer.pos = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyMethodDef module_methods[] = {
|
|
||||||
{"changed_center", (PyCFunction)changed_center, METH_VARARGS, ""},
|
|
||||||
{"split_with_highlights", (PyCFunction)split_with_highlights, METH_VARARGS, ""},
|
|
||||||
{"splitlines_like_git", (PyCFunction)splitlines_like_git, METH_VARARGS, ""},
|
|
||||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
|
||||||
};
|
|
||||||
|
|
||||||
static struct PyModuleDef module = {
|
|
||||||
.m_base = PyModuleDef_HEAD_INIT,
|
|
||||||
.m_name = "diff_speedup", /* name of module */
|
|
||||||
.m_doc = NULL,
|
|
||||||
.m_size = -1,
|
|
||||||
.m_methods = module_methods
|
|
||||||
};
|
|
||||||
|
|
||||||
EXPORTED PyMODINIT_FUNC
|
|
||||||
PyInit_diff_speedup(void) {
|
|
||||||
PyObject *m;
|
|
||||||
|
|
||||||
m = PyModule_Create(&module);
|
|
||||||
if (m == NULL) return NULL;
|
|
||||||
Py_AtExit(free_resources);
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
@ -27,9 +27,8 @@ class TestBuild(BaseTest):
|
|||||||
|
|
||||||
def test_loading_extensions(self) -> None:
|
def test_loading_extensions(self) -> None:
|
||||||
import kitty.fast_data_types as fdt
|
import kitty.fast_data_types as fdt
|
||||||
from kittens.diff import diff_speedup
|
|
||||||
from kittens.transfer import rsync
|
from kittens.transfer import rsync
|
||||||
del fdt, diff_speedup, rsync
|
del fdt, rsync
|
||||||
|
|
||||||
def test_loading_shaders(self) -> None:
|
def test_loading_shaders(self) -> None:
|
||||||
from kitty.utils import load_shaders
|
from kitty.utils import load_shaders
|
||||||
@ -79,12 +78,6 @@ class TestBuild(BaseTest):
|
|||||||
c = ssl.create_default_context()
|
c = ssl.create_default_context()
|
||||||
self.assertGreater(c.cert_store_stats()['x509_ca'], 2)
|
self.assertGreater(c.cert_store_stats()['x509_ca'], 2)
|
||||||
|
|
||||||
def test_pygments(self):
|
|
||||||
if not getattr(sys, 'frozen', False):
|
|
||||||
self.skipTest('Pygments is only tested on frozen builds')
|
|
||||||
import pygments
|
|
||||||
del pygments
|
|
||||||
|
|
||||||
def test_docs_url(self):
|
def test_docs_url(self):
|
||||||
from kitty.constants import website_url
|
from kitty.constants import website_url
|
||||||
from kitty.utils import docs_url
|
from kitty.utils import docs_url
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user