• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#   Copyright 2000-2010 Michael Hudson-Doyle <micahel@gmail.com>
2#                       Antonio Cuni
3#
4#                        All Rights Reserved
5#
6#
7# Permission to use, copy, modify, and distribute this software and
8# its documentation for any purpose is hereby granted without fee,
9# provided that the above copyright notice appear in all copies and
10# that both that copyright notice and this permission notice appear in
11# supporting documentation.
12#
13# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
14# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
16# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
17# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
18# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
19# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20
21from __future__ import annotations
22
23from dataclasses import dataclass, field
24
25import re
26from . import commands, console, reader
27from .reader import Reader
28
29
30# types
31Command = commands.Command
32if False:
33    from .types import KeySpec, CommandName
34
35
36def prefix(wordlist: list[str], j: int = 0) -> str:
37    d = {}
38    i = j
39    try:
40        while 1:
41            for word in wordlist:
42                d[word[i]] = 1
43            if len(d) > 1:
44                return wordlist[0][j:i]
45            i += 1
46            d = {}
47    except IndexError:
48        return wordlist[0][j:i]
49    return ""
50
51
52STRIPCOLOR_REGEX = re.compile(r"\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[m|K]")
53
54def stripcolor(s: str) -> str:
55    return STRIPCOLOR_REGEX.sub('', s)
56
57
58def real_len(s: str) -> int:
59    return len(stripcolor(s))
60
61
62def left_align(s: str, maxlen: int) -> str:
63    stripped = stripcolor(s)
64    if len(stripped) > maxlen:
65        # too bad, we remove the color
66        return stripped[:maxlen]
67    padding = maxlen - len(stripped)
68    return s + ' '*padding
69
70
71def build_menu(
72        cons: console.Console,
73        wordlist: list[str],
74        start: int,
75        use_brackets: bool,
76        sort_in_column: bool,
77) -> tuple[list[str], int]:
78    if use_brackets:
79        item = "[ %s ]"
80        padding = 4
81    else:
82        item = "%s  "
83        padding = 2
84    maxlen = min(max(map(real_len, wordlist)), cons.width - padding)
85    cols = int(cons.width / (maxlen + padding))
86    rows = int((len(wordlist) - 1)/cols + 1)
87
88    if sort_in_column:
89        # sort_in_column=False (default)     sort_in_column=True
90        #          A B C                       A D G
91        #          D E F                       B E
92        #          G                           C F
93        #
94        # "fill" the table with empty words, so we always have the same amout
95        # of rows for each column
96        missing = cols*rows - len(wordlist)
97        wordlist = wordlist + ['']*missing
98        indexes = [(i % cols) * rows + i // cols for i in range(len(wordlist))]
99        wordlist = [wordlist[i] for i in indexes]
100    menu = []
101    i = start
102    for r in range(rows):
103        row = []
104        for col in range(cols):
105            row.append(item % left_align(wordlist[i], maxlen))
106            i += 1
107            if i >= len(wordlist):
108                break
109        menu.append(''.join(row))
110        if i >= len(wordlist):
111            i = 0
112            break
113        if r + 5 > cons.height:
114            menu.append("   %d more... " % (len(wordlist) - i))
115            break
116    return menu, i
117
118# this gets somewhat user interface-y, and as a result the logic gets
119# very convoluted.
120#
121#  To summarise the summary of the summary:- people are a problem.
122#                  -- The Hitch-Hikers Guide to the Galaxy, Episode 12
123
124#### Desired behaviour of the completions commands.
125# the considerations are:
126# (1) how many completions are possible
127# (2) whether the last command was a completion
128# (3) if we can assume that the completer is going to return the same set of
129#     completions: this is controlled by the ``assume_immutable_completions``
130#     variable on the reader, which is True by default to match the historical
131#     behaviour of pyrepl, but e.g. False in the ReadlineAlikeReader to match
132#     more closely readline's semantics (this is needed e.g. by
133#     fancycompleter)
134#
135# if there's no possible completion, beep at the user and point this out.
136# this is easy.
137#
138# if there's only one possible completion, stick it in.  if the last thing
139# user did was a completion, point out that he isn't getting anywhere, but
140# only if the ``assume_immutable_completions`` is True.
141#
142# now it gets complicated.
143#
144# for the first press of a completion key:
145#  if there's a common prefix, stick it in.
146
147#  irrespective of whether anything got stuck in, if the word is now
148#  complete, show the "complete but not unique" message
149
150#  if there's no common prefix and if the word is not now complete,
151#  beep.
152
153#        common prefix ->    yes          no
154#        word complete \/
155#            yes           "cbnu"      "cbnu"
156#            no              -          beep
157
158# for the second bang on the completion key
159#  there will necessarily be no common prefix
160#  show a menu of the choices.
161
162# for subsequent bangs, rotate the menu around (if there are sufficient
163# choices).
164
165
166class complete(commands.Command):
167    def do(self) -> None:
168        r: CompletingReader
169        r = self.reader  # type: ignore[assignment]
170        last_is_completer = r.last_command_is(self.__class__)
171        immutable_completions = r.assume_immutable_completions
172        completions_unchangable = last_is_completer and immutable_completions
173        stem = r.get_stem()
174        if not completions_unchangable:
175            r.cmpltn_menu_choices = r.get_completions(stem)
176
177        completions = r.cmpltn_menu_choices
178        if not completions:
179            r.error("no matches")
180        elif len(completions) == 1:
181            if completions_unchangable and len(completions[0]) == len(stem):
182                r.msg = "[ sole completion ]"
183                r.dirty = True
184            r.insert(completions[0][len(stem):])
185        else:
186            p = prefix(completions, len(stem))
187            if p:
188                r.insert(p)
189            if last_is_completer:
190                r.cmpltn_menu_visible = True
191                r.cmpltn_message_visible = False
192                r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
193                    r.console, completions, r.cmpltn_menu_end,
194                    r.use_brackets, r.sort_in_column)
195                r.dirty = True
196            elif not r.cmpltn_menu_visible:
197                r.cmpltn_message_visible = True
198                if stem + p in completions:
199                    r.msg = "[ complete but not unique ]"
200                    r.dirty = True
201                else:
202                    r.msg = "[ not unique ]"
203                    r.dirty = True
204
205
206class self_insert(commands.self_insert):
207    def do(self) -> None:
208        r: CompletingReader
209        r = self.reader  # type: ignore[assignment]
210
211        commands.self_insert.do(self)
212        if r.cmpltn_menu_visible:
213            stem = r.get_stem()
214            if len(stem) < 1:
215                r.cmpltn_reset()
216            else:
217                completions = [w for w in r.cmpltn_menu_choices
218                               if w.startswith(stem)]
219                if completions:
220                    r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
221                        r.console, completions, 0,
222                        r.use_brackets, r.sort_in_column)
223                else:
224                    r.cmpltn_reset()
225
226
227@dataclass
228class CompletingReader(Reader):
229    """Adds completion support"""
230
231    ### Class variables
232    # see the comment for the complete command
233    assume_immutable_completions = True
234    use_brackets = True  # display completions inside []
235    sort_in_column = False
236
237    ### Instance variables
238    cmpltn_menu: list[str] = field(init=False)
239    cmpltn_menu_visible: bool = field(init=False)
240    cmpltn_message_visible: bool = field(init=False)
241    cmpltn_menu_end: int = field(init=False)
242    cmpltn_menu_choices: list[str] = field(init=False)
243
244    def __post_init__(self) -> None:
245        super().__post_init__()
246        self.cmpltn_reset()
247        for c in (complete, self_insert):
248            self.commands[c.__name__] = c
249            self.commands[c.__name__.replace('_', '-')] = c
250
251    def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
252        return super().collect_keymap() + (
253            (r'\t', 'complete'),)
254
255    def after_command(self, cmd: Command) -> None:
256        super().after_command(cmd)
257        if not isinstance(cmd, (complete, self_insert)):
258            self.cmpltn_reset()
259
260    def calc_screen(self) -> list[str]:
261        screen = super().calc_screen()
262        if self.cmpltn_menu_visible:
263            ly = self.lxy[1]
264            screen[ly:ly] = self.cmpltn_menu
265            self.screeninfo[ly:ly] = [(0, [])]*len(self.cmpltn_menu)
266            self.cxy = self.cxy[0], self.cxy[1] + len(self.cmpltn_menu)
267        return screen
268
269    def finish(self) -> None:
270        super().finish()
271        self.cmpltn_reset()
272
273    def cmpltn_reset(self) -> None:
274        self.cmpltn_menu = []
275        self.cmpltn_menu_visible = False
276        self.cmpltn_message_visible = False
277        self.cmpltn_menu_end = 0
278        self.cmpltn_menu_choices = []
279
280    def get_stem(self) -> str:
281        st = self.syntax_table
282        SW = reader.SYNTAX_WORD
283        b = self.buffer
284        p = self.pos - 1
285        while p >= 0 and st.get(b[p], SW) == SW:
286            p -= 1
287        return ''.join(b[p+1:self.pos])
288
289    def get_completions(self, stem: str) -> list[str]:
290        return []
291