build_keyword_pattern_set()   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
b 0
f 0
dl 0
loc 36
rs 9.016
1
"""Utility module."""
2
import re
3
from collections import namedtuple
4
from typing import Dict  # noqa: F401
5
6
ESCAPE_ECHO = str.maketrans({
7
    '"': '\\"',
8
    '\\': '\\\\',
9
})
10
11
IMPRINTABLE_REPRESENTS = {
12
    '\a': '^G',
13
    '\b': '^H',             # NOTE: Neovim: <BS>, Vim: ^H. Follow Vim.
14
    '\t': '^I',
15
    '\n': '^J',
16
    '\v': '^K',
17
    '\f': '^L',
18
    '\r': '^M',
19
    '\udc80\udcffX': '^@',  # NOTE: ^0 representation in Vim.
20
}
21
22
IMPRINTABLE_PATTERN = re.compile(r'(%s)' % '|'.join(
23
    IMPRINTABLE_REPRESENTS.keys()
24
))
25
26
PatternSet = namedtuple('PatternSet', [
27
    'pattern',
28
    'inverse',
29
])
30
31
_cached_encoding = None
32
33
_cached_keyword_pattern_set = {}  # type: Dict[str, PatternSet]
34
35
36
def get_encoding(nvim):
37
    """Return a Vim's internal encoding.
38
39
    The retrieve encoding is cached to the function instance while encoding
40
    options should not be changed in Vim's live session (see :h encoding) to
41
    enhance performance.
42
43
    Args:
44
        nvim (neovim.Nvim): A ``neovim.Nvim`` instance.
45
46
    Returns:
47
        str: A Vim's internal encoding.
48
    """
49
    global _cached_encoding
50
    if _cached_encoding is None:
51
        _cached_encoding = nvim.options['encoding']
52
    return _cached_encoding
53
54
55
def ensure_bytes(nvim, seed):
56
    """Encode `str` to `bytes` if necessary and return.
57
58
    Args:
59
        nvim (neovim.Nvim): A ``neovim.Nvim`` instance.
60
        seed (AnyStr): A str or bytes instance.
61
62
    Example:
63
        >>> from unittest.mock import MagicMock
64
        >>> nvim = MagicMock()
65
        >>> nvim.options = {'encoding': 'utf-8'}
66
        >>> ensure_bytes(nvim, b'a')
67
        b'a'
68
        >>> ensure_bytes(nvim, 'a')
69
        b'a'
70
71
    Returns:
72
        bytes: A bytes represantation of ``seed``.
73
    """
74
    if isinstance(seed, str):
75
        encoding = get_encoding(nvim)
76
        return seed.encode(encoding, 'surrogateescape')
77
    return seed
78
79
80
def ensure_str(nvim, seed):
81
    """Decode `bytes` to `str` if necessary and return.
82
83
    Args:
84
        nvim (neovim.Nvim): A ``neovim.Nvim`` instance.
85
        seed (AnyStr): A str or bytes instance.
86
87
    Example:
88
        >>> from unittest.mock import MagicMock
89
        >>> nvim = MagicMock()
90
        >>> nvim.options = {'encoding': 'utf-8'}
91
        >>> ensure_str(nvim, b'a')
92
        'a'
93
        >>> ensure_str(nvim, 'a')
94
        'a'
95
96
    Returns:
97
        str: A str represantation of ``seed``.
98
    """
99
    if isinstance(seed, bytes):
100
        encoding = get_encoding(nvim)
101
        return seed.decode(encoding, 'surrogateescape')
102
    return seed
103
104
105
def int2char(nvim, code):
106
    """Return a corresponding char of `code`.
107
108
    It uses "nr2char()" in Vim script when 'encoding' option is not utf-8.
109
    Otherwise it uses "chr()" in Python to improve the performance.
110
111
    Args:
112
        nvim (neovim.Nvim): A ``neovim.Nvim`` instance.
113
        code (int): A int which represent a single character.
114
115
    Example:
116
        >>> from unittest.mock import MagicMock
117
        >>> nvim = MagicMock()
118
        >>> nvim.options = {'encoding': 'utf-8'}
119
        >>> int2char(nvim, 97)
120
        'a'
121
122
    Returns:
123
        str: A str of ``code``.
124
    """
125
    encoding = get_encoding(nvim)
126
    if encoding in ('utf-8', 'utf8'):
127
        return chr(code)
128
    return nvim.call('nr2char', code)
129
130
131
def int2repr(nvim, code):
132
    """Return a string representation of a key with specified key code."""
133
    from .key import Key
134
    if isinstance(code, int):
135
        return int2char(nvim, code)
136
    return Key.represent(nvim, ensure_bytes(nvim, code))
137
138
139
def getchar(nvim, *args):
140
    """Call getchar and return int or bytes instance.
141
142
    Args:
143
        nvim (neovim.Nvim): A ``neovim.Nvim`` instance.
144
        *args: Arguments passed to getchar function in Vim.
145
146
    Returns:
147
        Union[int, bytes]: A int or bytes.
148
    """
149
    try:
150
        ret = nvim.call('getchar', *args)
151
        if isinstance(ret, int):
152
            if ret == 0x03:
153
                # NOTE
154
                # Vim/Neovim usually raise an exception when user hit Ctrl-C
155
                # but sometime returns 0x03 (^C) instead.
156
                # While user might override <Esc> or <CR> and accidentaly
157
                # disable the way to exit neovim-prompt, Ctrl-C should be a
158
                # final way to exit the prompt. So raise KeyboardInterrupt
159
                # exception when 'ret' is 0x03 instead of returning 0x03.
160
                raise KeyboardInterrupt
161
            return ret
162
        return ensure_bytes(nvim, ret)
163
    except nvim.error as e:
164
        # NOTE:
165
        # neovim raise nvim.error instead of KeyboardInterrupt when Ctrl-C has
166
        # pressed so convert it to a real KeyboardInterrupt exception.
167
        if str(e) == "b'Keyboard interrupt'":
168
            raise KeyboardInterrupt
169
        raise e
170
171
172
def build_echon_expr(text, hl='None'):
173
    """Build 'echon' expression.
174
175
    Imprintable characters (e.g. '^M') are replaced to a corresponding
176
    representations used in Vim's command-line interface.
177
178
    Args:
179
        text (str): A text to be echon.
180
        hl (str): A highline name. Default is 'None'.
181
182
    Return:
183
        str: A Vim's command expression for 'echon'.
184
    """
185
    if not IMPRINTABLE_PATTERN.search(text):
186
        return 'echohl %s|echon "%s"' % (
187
            hl, text.translate(ESCAPE_ECHO)
188
        )
189
    p = 'echohl %s|echon "%%s"' % hl
190
    i = 'echohl %s|echon "%%s"' % ('SpecialKey' if hl == 'None' else hl)
191
    return '|'.join(
192
        p % term.translate(ESCAPE_ECHO)
193
        if index % 2 == 0 else i % IMPRINTABLE_REPRESENTS[term]
194
        for index, term in enumerate(IMPRINTABLE_PATTERN.split(text))
195
    )
196
197
198
def build_keyword_pattern_set(nvim):
199
    """Build a keyword pattern set from current 'iskeyword'.
200
201
    The result is cached by the value of 'iskeyword'.
202
203
    Args:
204
        nvim (neovim.Nvim): A ``neovim.Nvim`` instance.
205
206
    Returns:
207
        PatternSet
208
    """
209
    # NOTE:
210
    # iskeyword is similar to isfname and in Vim's help, isfname always
211
    # include multi-byte characters so only ASCII characters need to be
212
    # considered
213
    #
214
    # > Multi-byte characters 256 and above are always included, only the
215
    # > characters up to 255 are specified with this option.
216
    iskeyword = nvim.current.buffer.options['iskeyword']
217
    if iskeyword not in _cached_keyword_pattern_set:
218
        source = frozenset(chr(c) for c in range(0x20, 0xff))
219
        non_keyword_set = frozenset(nvim.call(
220
            'substitute',
221
            ''.join(source),
222
            r'\k\+',
223
            '', 'g'
224
        ))
225
        keyword_set = source - non_keyword_set
226
        # Convert frozenset to str and remove whitespaces
227
        keyword = re.sub(r'\s+', '', ''.join(keyword_set))
228
        non_keyword = re.sub(r'\s+', '', ''.join(non_keyword_set))
229
        _cached_keyword_pattern_set[iskeyword] = PatternSet(
230
            pattern=r'[%s]' % re.escape(keyword),
231
            inverse=r'[%s]' % re.escape(non_keyword),
232
        )
233
    return _cached_keyword_pattern_set[iskeyword]
234
235
236
# http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Metaprogramming.html
237
class Singleton(type):
238
    """A singleton metaclass."""
239
240
    instance = None
241
242
    def __call__(cls, *args, **kwargs):  # noqa
243
        if not cls.instance:
244
            cls.instance = super().__call__(*args, **kwargs)
245
        return cls.instance
246