Completed
Push — master ( ace01b...aa6711 )
by Lambda
01:08
created

build_keyword_pattern_set()   B

Complexity

Conditions 4

Size

Total Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

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