From d30091034ab170d7f5f454165c0eee24d1fb6125 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Mar 2023 11:46:22 +0530 Subject: [PATCH] Remove the python diff kitten --- bypy/sources.json | 9 - docs/build.rst | 1 - docs/kittens/diff.rst | 6 +- kittens/diff/README.asciidoc | 1 - kittens/diff/__init__.py | 9 +- kittens/diff/collect.py | 233 -------- kittens/diff/config.py | 70 --- kittens/diff/diff_speedup.pyi | 16 - kittens/diff/highlight.py | 185 ------- kittens/diff/main.py | 831 ++++++++--------------------- kittens/diff/options/__init__.py | 0 kittens/diff/options/definition.py | 267 --------- kittens/diff/options/parse.py | 125 ----- kittens/diff/options/types.py | 174 ------ kittens/diff/options/utils.py | 68 --- kittens/diff/patch.py | 262 --------- kittens/diff/render.py | 546 ------------------- kittens/diff/search.py | 72 --- kittens/diff/speedup.c | 239 --------- kitty_tests/check_build.py | 9 +- 20 files changed, 216 insertions(+), 2907 deletions(-) delete mode 100644 kittens/diff/README.asciidoc delete mode 100644 kittens/diff/collect.py delete mode 100644 kittens/diff/config.py delete mode 100644 kittens/diff/diff_speedup.pyi delete mode 100644 kittens/diff/highlight.py delete mode 100644 kittens/diff/options/__init__.py delete mode 100644 kittens/diff/options/definition.py delete mode 100644 kittens/diff/options/parse.py delete mode 100644 kittens/diff/options/types.py delete mode 100644 kittens/diff/options/utils.py delete mode 100644 kittens/diff/patch.py delete mode 100644 kittens/diff/render.py delete mode 100644 kittens/diff/search.py delete mode 100644 kittens/diff/speedup.c diff --git a/bypy/sources.json b/bypy/sources.json index 734d6352e..323b05a5e 100644 --- a/bypy/sources.json +++ b/bypy/sources.json @@ -163,15 +163,6 @@ } }, - { - "name": "pygments", - "unix": { - "filename": "Pygments-2.11.2.tar.gz", - "hash": "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a", - "urls": ["pypi"] - } - }, - { "name": "libpng", "unix": { diff --git a/docs/build.rst b/docs/build.rst index 37f762ad5..6f515ea81 100644 --- a/docs/build.rst +++ b/docs/build.rst @@ -40,7 +40,6 @@ Run-time dependencies: * ``fontconfig`` (not needed on macOS) * ``libcanberra`` (not needed on macOS) * ``ImageMagick`` (optional, needed to display uncommon image formats in the terminal) -* ``pygments`` (optional, needed for syntax highlighting in ``kitty +kitten diff``) Build-time dependencies: diff --git a/docs/kittens/diff.rst b/docs/kittens/diff.rst index 43ed04681..5da7a6b2d 100644 --- a/docs/kittens/diff.rst +++ b/docs/kittens/diff.rst @@ -31,11 +31,7 @@ Major Features Installation --------------- -Simply :ref:`install kitty `. You also need to have either the `git -`__ program or the :program:`diff` program installed. -Additionally, for syntax highlighting to work, `pygments -`__ must be installed (note that pygments is included in -the official kitty binary builds). +Simply :ref:`install kitty `. Usage diff --git a/kittens/diff/README.asciidoc b/kittens/diff/README.asciidoc deleted file mode 100644 index e9a73c7d5..000000000 --- a/kittens/diff/README.asciidoc +++ /dev/null @@ -1 +0,0 @@ -See https://sw.kovidgoyal.net/kitty/kittens/diff/ diff --git a/kittens/diff/__init__.py b/kittens/diff/__init__.py index 76c7c1ac6..01bf5ee62 100644 --- a/kittens/diff/__init__.py +++ b/kittens/diff/__init__.py @@ -1,8 +1,5 @@ -class GlobalData: - - def __init__(self) -> None: - self.title = '' - self.cmd = '' +from typing import Dict -global_data = GlobalData +def syntax_aliases(x: str) -> Dict[str, str]: + return {} diff --git a/kittens/diff/collect.py b/kittens/diff/collect.py deleted file mode 100644 index e1806bbb3..000000000 --- a/kittens/diff/collect.py +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/env python3 -# License: GPL v3 Copyright: 2018, Kovid Goyal - -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, []) diff --git a/kittens/diff/config.py b/kittens/diff/config.py deleted file mode 100644 index 8c18b02bd..000000000 --- a/kittens/diff/config.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -# License: GPL v3 Copyright: 2018, Kovid Goyal - -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 diff --git a/kittens/diff/diff_speedup.pyi b/kittens/diff/diff_speedup.pyi deleted file mode 100644 index 630c25ac6..000000000 --- a/kittens/diff/diff_speedup.pyi +++ /dev/null @@ -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 diff --git a/kittens/diff/highlight.py b/kittens/diff/highlight.py deleted file mode 100644 index d12219158..000000000 --- a/kittens/diff/highlight.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env python3 -# License: GPL v3 Copyright: 2018, Kovid Goyal - -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) diff --git a/kittens/diff/main.py b/kittens/diff/main.py index 8998b8e5d..98c49d5d1 100644 --- a/kittens/diff/main.py +++ b/kittens/diff/main.py @@ -1,591 +1,272 @@ #!/usr/bin/env python3 # License: GPL v3 Copyright: 2018, Kovid Goyal -import atexit -import os -import signal -import subprocess import sys -import tempfile -import warnings -from collections import defaultdict -from contextlib import suppress -from enum import Enum, auto from functools import partial -from gettext import gettext as _ -from typing import ( - Any, - DefaultDict, - Dict, - Iterable, - Iterator, - List, - Optional, - Tuple, - Union, -) +from typing import List -from kitty.cli import CONFIG_HELP, CompletionSpec, parse_args -from kitty.cli_stub import DiffCLIOptions -from kitty.conf.utils import KeyAction +from kitty.cli import CONFIG_HELP, CompletionSpec +from kitty.conf.types import Definition 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 -from ..tui.line_edit import LineEdit -from ..tui.loop import Loop -from ..tui.operations import styled -from . import global_data -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 -from .patch import Differ, Patch, set_diff_command, worker_processes -from .render import ( - ImagePlacement, - ImageSupportWarning, - Line, - LineRef, - Reference, - render_diff, -) -from .search import BadRegex, Search -try: - from .highlight import ( - DiffHighlight, - get_highlight_processes, - highlight_collection, - initialize_highlighter, +def main(args: List[str]) -> None: + raise SystemExit('Must be run as kitten diff') + +definition = Definition( + '!kittens.diff', +) + +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', 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. +''' ) - 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']]: - return '' +opt('num_context_lines', '3', option_type='positive_int', + long_text='The number of lines of context to show around each change.' + ) - def get_highlight_processes() -> Iterator[int]: - if has_highlighter: - yield -1 +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.' + ) -class State(Enum): - initializing = auto() - collected = auto() - diffed = auto() - command = auto() - message = auto() +opt('+ignore_name', '', ctype='string', + 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 +''', + ) -class BackgroundWork(Enum): - none = auto() - collecting = auto() - diffing = auto() - highlighting = auto() +egr() # }}} +# colors {{{ +agr('colors', 'Colors') -def generate_diff(collection: Collection, context: int) -> Union[str, Dict[str, Patch]]: - d = Differ() +opt('pygments_style', 'default', + long_text=''' +The pygments color scheme to use for syntax highlighting. See :link:`pygments +builtin 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. +''' + ) - 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) +opt('foreground', 'black', + option_type='to_color', + long_text='Basic colors' + ) - return d(context) +opt('background', 'white', + option_type='to_color', + ) +opt('title_fg', 'black', + option_type='to_color', + long_text='Title colors' + ) -class DiffHandler(Handler): +opt('title_bg', 'white', + option_type='to_color', + ) - image_manager_class = ImageManager +opt('margin_bg', '#fafbfc', + option_type='to_color', + long_text='Margin colors' + ) - 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) +opt('margin_fg', '#aaaaaa', + option_type='to_color', + ) - def terminate(self, return_code: int = 0) -> None: - self.quit_loop(return_code) +opt('removed_bg', '#ffeef0', + option_type='to_color', + long_text='Removed text backgrounds' + ) - 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 +opt('highlight_removed_bg', '#fdb8c0', + option_type='to_color', + ) - def create_collection(self) -> None: +opt('removed_margin_bg', '#ffdce0', + option_type='to_color', + ) - def collect_done(collection: Collection) -> None: - self.doing_background_work = BackgroundWork.none - self.collection = collection - self.state = State.collected - self.generate_diff() +opt('added_bg', '#e6ffed', + option_type='to_color', + long_text='Added text backgrounds' + ) - def collect(left: str, right: str) -> None: - collection = create_collection(left, right) - self.asyncio_loop.call_soon_threadsafe(collect_done, collection) +opt('highlight_added_bg', '#acf2bd', + option_type='to_color', + ) - self.asyncio_loop.run_in_executor(None, collect, self.left, self.right) - self.doing_background_work = BackgroundWork.collecting +opt('added_margin_bg', '#cdffd8', + option_type='to_color', + ) - def generate_diff(self) -> None: +opt('filler_bg', '#fafbfc', + option_type='to_color', + long_text='Filler (empty) line background' + ) - 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() +opt('margin_filler_bg', 'none', + option_type='to_color_or_none', + long_text='Filler (empty) line background in margins, defaults to the filler background' + ) - 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) +opt('hunk_margin_bg', '#dbedff', + option_type='to_color', + long_text='Hunk header colors' + ) - self.asyncio_loop.run_in_executor(None, diff, self.collection, self.current_context_count) - self.doing_background_work = BackgroundWork.diffing +opt('hunk_bg', '#f1f8ff', + option_type='to_color', + ) - def syntax_highlight(self) -> None: +opt('search_bg', '#444', + option_type='to_color', + long_text='Highlighting' + ) - 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() +opt('search_fg', 'white', + option_type='to_color', + ) - 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) +opt('select_bg', '#b4d5fe', + option_type='to_color', + ) - self.asyncio_loop.run_in_executor(None, highlight, self.collection, self.opts.syntax_aliases) - self.doing_background_work = BackgroundWork.highlighting +opt('select_fg', 'black', + option_type='to_color_or_none', + ) +egr() # }}} - 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 +# shortcuts {{{ +agr('shortcuts', 'Keyboard shortcuts') - 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) +map('Quit', + 'quit q quit', + ) +map('Quit', + 'quit esc quit', + ) - @property - def current_position(self) -> Reference: - return self.diff_lines[min(len(self.diff_lines) - 1, self.scroll_pos)].ref +map('Scroll down', + 'scroll_down j scroll_by 1', + ) +map('Scroll down', + 'scroll_down down scroll_by 1', + ) - @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 +map('Scroll up', + 'scroll_up k scroll_by -1', + ) +map('Scroll up', + 'scroll_up up scroll_by -1', + ) - if num is not None: - self.scroll_pos = max(0, min(num, self.max_scroll_pos)) +map('Scroll to top', + 'scroll_top home scroll_to start', + ) - @property - def num_lines(self) -> int: - return self.screen_size.rows - 1 +map('Scroll to bottom', + 'scroll_bottom end scroll_to end', + ) - 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() +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', + ) - 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() +map('Scroll to previous page', + 'scroll_page_up page_up scroll_to prev-page', + ) - def set_scrolling_region(self) -> None: - self.cmd.set_scrolling_region(self.screen_size, 0, self.num_lines - 2) +map('Scroll to next change', + 'next_change n scroll_to next-change', + ) - 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() +map('Scroll to previous change', + 'prev_change p scroll_to prev-change', + ) - 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') +map('Show all context', + 'all_context a change_context all', + ) - def finalize(self) -> None: - self.cmd.set_default_colors() - self.cmd.set_cursor_visible(True) - self.cmd.set_scrolling_region() +map('Show default context', + 'default_context = change_context default', + ) - def initialize(self) -> None: - self.init_terminal_state() - self.set_scrolling_region() - self.draw_screen() - self.create_collection() +map('Increase context', + 'increase_context + change_context 5', + ) - def enforce_cursor_state(self) -> None: - self.cmd.set_cursor_visible(self.state is State.command) +map('Decrease context', + 'decrease_context - change_context -5', + ) - 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() +map('Search forward', + 'search_forward / start_search regex forward', + ) - 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 +map('Search backward', + 'search_backward ? start_search regex backward', + ) - 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 +map('Scroll to next search match', + 'next_match . scroll_to next-match', + ) +map('Scroll to next search match', + 'next_match > scroll_to next-match', + ) - 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 +map('Scroll to previous search match', + 'prev_match , scroll_to prev-match', + ) +map('Scroll to previous search match', + 'prev_match < scroll_to prev-match', + ) - 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: - new_ctx = max(0, new_ctx) - 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: - if self.state is not State.diffed: - self.cmd.bell() - return - self.state = State.command - self.line_edit.clear() - 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: - self.current_search = None - query = self.line_edit.current_input - 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: - 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: - self.screen_size = screen_size - 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: - self.terminate(1) - - def on_eot(self) -> None: - self.terminate(1) +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('''\ --context @@ -607,99 +288,10 @@ Override individual configuration options, can be specified multiple times. Syntax: :italic:`name=value`. For example: :italic:`-o background=gray` '''.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.' 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__': main(sys.argv) @@ -711,5 +303,4 @@ elif __name__ == '__doc__': 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"') elif __name__ == '__conf__': - from .options.definition import definition sys.options_definition = definition # type: ignore diff --git a/kittens/diff/options/__init__.py b/kittens/diff/options/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/kittens/diff/options/definition.py b/kittens/diff/options/definition.py deleted file mode 100644 index 5844873e0..000000000 --- a/kittens/diff/options/definition.py +++ /dev/null @@ -1,267 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=utf-8 -# License: GPLv3 Copyright: 2021, Kovid Goyal - -# 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 ` 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() # }}} diff --git a/kittens/diff/options/parse.py b/kittens/diff/options/parse.py deleted file mode 100644 index 26ccf2d42..000000000 --- a/kittens/diff/options/parse.py +++ /dev/null @@ -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 diff --git a/kittens/diff/options/types.py b/kittens/diff/options/types.py deleted file mode 100644 index 4f64217a8..000000000 --- a/kittens/diff/options/types.py +++ /dev/null @@ -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))), -] diff --git a/kittens/diff/options/utils.py b/kittens/diff/options/utils.py deleted file mode 100644 index e65f6a814..000000000 --- a/kittens/diff/options/utils.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=utf-8 -# License: GPLv3 Copyright: 2021, Kovid Goyal - - -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 diff --git a/kittens/diff/patch.py b/kittens/diff/patch.py deleted file mode 100644 index 7c7d9d878..000000000 --- a/kittens/diff/patch.py +++ /dev/null @@ -1,262 +0,0 @@ -#!/usr/bin/env python3 -# License: GPL v3 Copyright: 2018, Kovid Goyal - -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 diff --git a/kittens/diff/render.py b/kittens/diff/render.py deleted file mode 100644 index ec1f34fb6..000000000 --- a/kittens/diff/render.py +++ /dev/null @@ -1,546 +0,0 @@ -#!/usr/bin/env python3 -# License: GPL v3 Copyright: 2018, Kovid Goyal - -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() diff --git a/kittens/diff/search.py b/kittens/diff/search.py deleted file mode 100644 index 18850deba..000000000 --- a/kittens/diff/search.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -# License: GPL v3 Copyright: 2018, Kovid Goyal - -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 diff --git a/kittens/diff/speedup.c b/kittens/diff/speedup.c deleted file mode 100644 index 61179e3b4..000000000 --- a/kittens/diff/speedup.c +++ /dev/null @@ -1,239 +0,0 @@ -/* - * speedup.c - * Copyright (C) 2018 Kovid Goyal - * - * 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; -} diff --git a/kitty_tests/check_build.py b/kitty_tests/check_build.py index 5ca0252e9..bec256ab5 100644 --- a/kitty_tests/check_build.py +++ b/kitty_tests/check_build.py @@ -27,9 +27,8 @@ class TestBuild(BaseTest): def test_loading_extensions(self) -> None: import kitty.fast_data_types as fdt - from kittens.diff import diff_speedup from kittens.transfer import rsync - del fdt, diff_speedup, rsync + del fdt, rsync def test_loading_shaders(self) -> None: from kitty.utils import load_shaders @@ -79,12 +78,6 @@ class TestBuild(BaseTest): c = ssl.create_default_context() 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): from kitty.constants import website_url from kitty.utils import docs_url