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
21import re
22from pyrepl import commands, reader
23from pyrepl.reader import Reader
24
25
26def prefix(wordlist, j=0):
27    d = {}
28    i = j
29    try:
30        while 1:
31            for word in wordlist:
32                d[word[i]] = 1
33            if len(d) > 1:
34                return wordlist[0][j:i]
35            i += 1
36            d = {}
37    except IndexError:
38        return wordlist[0][j:i]
39
40
41STRIPCOLOR_REGEX = re.compile(r"\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[m|K]")
42
43def stripcolor(s):
44    return STRIPCOLOR_REGEX.sub('', s)
45
46
47def real_len(s):
48    return len(stripcolor(s))
49
50
51def left_align(s, maxlen):
52    stripped = stripcolor(s)
53    if len(stripped) > maxlen:
54        # too bad, we remove the color
55        return stripped[:maxlen]
56    padding = maxlen - len(stripped)
57    return s + ' '*padding
58
59
60def build_menu(cons, wordlist, start, use_brackets, sort_in_column):
61    if use_brackets:
62        item = "[ %s ]"
63        padding = 4
64    else:
65        item = "%s  "
66        padding = 2
67    maxlen = min(max(map(real_len, wordlist)), cons.width - padding)
68    cols = cons.width / (maxlen + padding)
69    rows = (len(wordlist) - 1)/cols + 1
70
71    if sort_in_column:
72        # sort_in_column=False (default)     sort_in_column=True
73        #          A B C                       A D G
74        #          D E F                       B E
75        #          G                           C F
76        #
77        # "fill" the table with empty words, so we always have the same amout
78        # of rows for each column
79        missing = cols*rows - len(wordlist)
80        wordlist = wordlist + ['']*missing
81        indexes = [(i % cols) * rows + i // cols for i in range(len(wordlist))]
82        wordlist = [wordlist[i] for i in indexes]
83    menu = []
84    i = start
85    for r in range(rows):
86        row = []
87        for col in range(cols):
88            row.append(item % left_align(wordlist[i], maxlen))
89            i += 1
90            if i >= len(wordlist):
91                break
92        menu.append(''.join(row))
93        if i >= len(wordlist):
94            i = 0
95            break
96        if r + 5 > cons.height:
97            menu.append("   %d more... " % (len(wordlist) - i))
98            break
99    return menu, i
100
101# this gets somewhat user interface-y, and as a result the logic gets
102# very convoluted.
103#
104#  To summarise the summary of the summary:- people are a problem.
105#                  -- The Hitch-Hikers Guide to the Galaxy, Episode 12
106
107#### Desired behaviour of the completions commands.
108# the considerations are:
109# (1) how many completions are possible
110# (2) whether the last command was a completion
111# (3) if we can assume that the completer is going to return the same set of
112#     completions: this is controlled by the ``assume_immutable_completions``
113#     variable on the reader, which is True by default to match the historical
114#     behaviour of pyrepl, but e.g. False in the ReadlineAlikeReader to match
115#     more closely readline's semantics (this is needed e.g. by
116#     fancycompleter)
117#
118# if there's no possible completion, beep at the user and point this out.
119# this is easy.
120#
121# if there's only one possible completion, stick it in.  if the last thing
122# user did was a completion, point out that he isn't getting anywhere, but
123# only if the ``assume_immutable_completions`` is True.
124#
125# now it gets complicated.
126#
127# for the first press of a completion key:
128#  if there's a common prefix, stick it in.
129
130#  irrespective of whether anything got stuck in, if the word is now
131#  complete, show the "complete but not unique" message
132
133#  if there's no common prefix and if the word is not now complete,
134#  beep.
135
136#        common prefix ->    yes          no
137#        word complete \/
138#            yes           "cbnu"      "cbnu"
139#            no              -          beep
140
141# for the second bang on the completion key
142#  there will necessarily be no common prefix
143#  show a menu of the choices.
144
145# for subsequent bangs, rotate the menu around (if there are sufficient
146# choices).
147
148
149class complete(commands.Command):
150    def do(self):
151        r = self.reader
152        last_is_completer = r.last_command_is(self.__class__)
153        immutable_completions = r.assume_immutable_completions
154        completions_unchangable = last_is_completer and immutable_completions
155        stem = r.get_stem()
156        if not completions_unchangable:
157            r.cmpltn_menu_choices = r.get_completions(stem)
158
159        completions = r.cmpltn_menu_choices
160        if not completions:
161            r.error("no matches")
162        elif len(completions) == 1:
163            if completions_unchangable and len(completions[0]) == len(stem):
164                r.msg = "[ sole completion ]"
165                r.dirty = 1
166            r.insert(completions[0][len(stem):])
167        else:
168            p = prefix(completions, len(stem))
169            if p:
170                r.insert(p)
171            if last_is_completer:
172                if not r.cmpltn_menu_vis:
173                    r.cmpltn_menu_vis = 1
174                r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
175                    r.console, completions, r.cmpltn_menu_end,
176                    r.use_brackets, r.sort_in_column)
177                r.dirty = 1
178            elif stem + p in completions:
179                r.msg = "[ complete but not unique ]"
180                r.dirty = 1
181            else:
182                r.msg = "[ not unique ]"
183                r.dirty = 1
184
185
186class self_insert(commands.self_insert):
187    def do(self):
188        commands.self_insert.do(self)
189        r = self.reader
190        if r.cmpltn_menu_vis:
191            stem = r.get_stem()
192            if len(stem) < 1:
193                r.cmpltn_reset()
194            else:
195                completions = [w for w in r.cmpltn_menu_choices
196                               if w.startswith(stem)]
197                if completions:
198                    r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
199                        r.console, completions, 0,
200                        r.use_brackets, r.sort_in_column)
201                else:
202                    r.cmpltn_reset()
203
204
205class CompletingReader(Reader):
206    """Adds completion support
207
208    Adds instance variables:
209      * cmpltn_menu, cmpltn_menu_vis, cmpltn_menu_end, cmpltn_choices:
210      *
211    """
212    # see the comment for the complete command
213    assume_immutable_completions = True
214    use_brackets = True  # display completions inside []
215    sort_in_column = False
216
217    def collect_keymap(self):
218        return super(CompletingReader, self).collect_keymap() + (
219            (r'\t', 'complete'),)
220
221    def __init__(self, console):
222        super(CompletingReader, self).__init__(console)
223        self.cmpltn_menu = ["[ menu 1 ]", "[ menu 2 ]"]
224        self.cmpltn_menu_vis = 0
225        self.cmpltn_menu_end = 0
226        for c in (complete, self_insert):
227            self.commands[c.__name__] = c
228            self.commands[c.__name__.replace('_', '-')] = c
229
230    def after_command(self, cmd):
231        super(CompletingReader, self).after_command(cmd)
232        if not isinstance(cmd, (complete, self_insert)):
233            self.cmpltn_reset()
234
235    def calc_screen(self):
236        screen = super(CompletingReader, self).calc_screen()
237        if self.cmpltn_menu_vis:
238            ly = self.lxy[1]
239            screen[ly:ly] = self.cmpltn_menu
240            self.screeninfo[ly:ly] = [(0, [])]*len(self.cmpltn_menu)
241            self.cxy = self.cxy[0], self.cxy[1] + len(self.cmpltn_menu)
242        return screen
243
244    def finish(self):
245        super(CompletingReader, self).finish()
246        self.cmpltn_reset()
247
248    def cmpltn_reset(self):
249        self.cmpltn_menu = []
250        self.cmpltn_menu_vis = 0
251        self.cmpltn_menu_end = 0
252        self.cmpltn_menu_choices = []
253
254    def get_stem(self):
255        st = self.syntax_table
256        SW = reader.SYNTAX_WORD
257        b = self.buffer
258        p = self.pos - 1
259        while p >= 0 and st.get(b[p], SW) == SW:
260            p -= 1
261        return ''.join(b[p+1:self.pos])
262
263    def get_completions(self, stem):
264        return []
265
266
267def test():
268    class TestReader(CompletingReader):
269        def get_completions(self, stem):
270            return [s for l in self.history
271                    for s in l.split()
272                    if s and s.startswith(stem)]
273
274    reader = TestReader()
275    reader.ps1 = "c**> "
276    reader.ps2 = "c/*> "
277    reader.ps3 = "c|*> "
278    reader.ps4 = "c\*> "
279    while reader.readline():
280        pass
281
282
283if __name__ == '__main__':
284    test()
285