~cboylan/boots/boots_pyrepl

« back to all changes in this revision

Viewing changes to pyrepl/completing_reader.py

  • Committer: Clark Boylan
  • Date: 2010-02-19 04:27:57 UTC
  • Revision ID: cboylan@cs.pdx.edu-20100219042757-9ej96be8gipbpk3r
copied pyrepl's trunk in from lp. Currently used as drop in replacement for readline. No multiline history items yet.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#   Copyright 2000-2004 Michael Hudson mwh@python.net
 
2
#
 
3
#                        All Rights Reserved
 
4
#
 
5
#
 
6
# Permission to use, copy, modify, and distribute this software and
 
7
# its documentation for any purpose is hereby granted without fee,
 
8
# provided that the above copyright notice appear in all copies and
 
9
# that both that copyright notice and this permission notice appear in
 
10
# supporting documentation.
 
11
#
 
12
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
 
13
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 
14
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
 
15
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
 
16
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
 
17
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 
18
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
19
 
 
20
from pyrepl import commands, reader
 
21
from pyrepl.reader import Reader
 
22
 
 
23
def uniqify(l):
 
24
    d = {}
 
25
    for i in l:
 
26
        d[i] = 1
 
27
    r = d.keys()
 
28
    r.sort()
 
29
    return r
 
30
 
 
31
def prefix(wordlist, j = 0):
 
32
    d = {}
 
33
    i = j
 
34
    try:
 
35
        while 1:
 
36
            for word in wordlist:
 
37
                d[word[i]] = 1
 
38
            if len(d) > 1:
 
39
                return wordlist[0][j:i]
 
40
            i += 1
 
41
            d = {}
 
42
    except IndexError:
 
43
        return wordlist[0][j:i]
 
44
 
 
45
def build_menu(cons, wordlist, start):
 
46
    maxlen = min(max(map(len, wordlist)), cons.width - 4)
 
47
    cols = cons.width / (maxlen + 4)
 
48
    rows = (len(wordlist) - 1)/cols + 1
 
49
    menu = []
 
50
    i = start
 
51
    for r in range(rows):
 
52
        row = []
 
53
        for col in range(cols):
 
54
            row.append("[ %-*s ]"%(maxlen, wordlist[i][:maxlen]))
 
55
            i += 1
 
56
            if i >= len(wordlist):
 
57
                break
 
58
        menu.append( ''.join(row) )
 
59
        if i >= len(wordlist):
 
60
            i = 0
 
61
            break
 
62
        if r + 5 > cons.height:
 
63
            menu.append("   %d more... "%(len(wordlist) - i))
 
64
            break
 
65
    return menu, i    
 
66
 
 
67
# this gets somewhat user interface-y, and as a result the logic gets
 
68
# very convoluted.
 
69
#
 
70
#  To summarise the summary of the summary:- people are a problem.
 
71
#                  -- The Hitch-Hikers Guide to the Galaxy, Episode 12
 
72
 
 
73
#### Desired behaviour of the completions commands.
 
74
# the considerations are:
 
75
# (1) how many completions are possible
 
76
# (2) whether the last command was a completion
 
77
#
 
78
# if there's no possible completion, beep at the user and point this out.
 
79
# this is easy.
 
80
#
 
81
# if there's only one possible completion, stick it in.  if the last thing
 
82
# user did was a completion, point out that he isn't getting anywhere.
 
83
#
 
84
# now it gets complicated.
 
85
 
86
# for the first press of a completion key:
 
87
#  if there's a common prefix, stick it in.
 
88
 
 
89
#  irrespective of whether anything got stuck in, if the word is now
 
90
#  complete, show the "complete but not unique" message
 
91
 
 
92
#  if there's no common prefix and if the word is not now complete,
 
93
#  beep.
 
94
 
 
95
#        common prefix ->    yes          no
 
96
#        word complete \/
 
97
#            yes           "cbnu"      "cbnu"
 
98
#            no              -          beep
 
99
 
 
100
# for the second bang on the completion key
 
101
#  there will necessarily be no common prefix
 
102
#  show a menu of the choices.
 
103
 
 
104
# for subsequent bangs, rotate the menu around (if there are sufficient
 
105
# choices).
 
106
 
 
107
class complete(commands.Command):
 
108
    def do(self):
 
109
        r = self.reader
 
110
        stem = r.get_stem()
 
111
        if r.last_command_is(self.__class__):
 
112
            completions = r.cmpltn_menu_choices
 
113
        else:
 
114
            r.cmpltn_menu_choices = completions = \
 
115
                                        r.get_completions(stem)
 
116
        if len(completions) == 0:
 
117
            r.error("no matches")
 
118
        elif len(completions) == 1:
 
119
            if len(completions[0]) == len(stem) and \
 
120
                   r.last_command_is(self.__class__):
 
121
                r.msg = "[ sole completion ]"
 
122
                r.dirty = 1
 
123
            r.insert(completions[0][len(stem):])
 
124
        else:
 
125
            p = prefix(completions, len(stem))
 
126
            if p <> '':
 
127
                r.insert(p)
 
128
            if r.last_command_is(self.__class__):
 
129
                if not r.cmpltn_menu_vis:
 
130
                    r.cmpltn_menu_vis = 1
 
131
                r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
 
132
                    r.console, completions, r.cmpltn_menu_end)
 
133
                r.dirty = 1
 
134
            elif stem + p in completions:
 
135
                r.msg = "[ complete but not unique ]"
 
136
                r.dirty = 1
 
137
            else:
 
138
                r.msg = "[ not unique ]"
 
139
                r.dirty = 1
 
140
 
 
141
class self_insert(commands.self_insert):
 
142
    def do(self):
 
143
        commands.self_insert.do(self)
 
144
        r = self.reader
 
145
        if r.cmpltn_menu_vis:
 
146
            stem = r.get_stem()
 
147
            if len(stem) < 1:
 
148
                r.cmpltn_reset()
 
149
            else:
 
150
                completions = [w for w in r.cmpltn_menu_choices
 
151
                               if w.startswith(stem)]
 
152
                if completions:
 
153
                    r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
 
154
                        r.console, completions, 0)
 
155
                else:
 
156
                    r.cmpltn_reset()
 
157
 
 
158
class CompletingReader(Reader):
 
159
    """Adds completion support
 
160
 
 
161
    Adds instance variables:
 
162
      * cmpltn_menu, cmpltn_menu_vis, cmpltn_menu_end, cmpltn_choices:
 
163
      *
 
164
    """
 
165
 
 
166
    def collect_keymap(self):
 
167
        return super(CompletingReader, self).collect_keymap() + (
 
168
            (r'\t', 'complete'),)
 
169
    
 
170
    def __init__(self, console):
 
171
        super(CompletingReader, self).__init__(console)
 
172
        self.cmpltn_menu = ["[ menu 1 ]", "[ menu 2 ]"]
 
173
        self.cmpltn_menu_vis = 0
 
174
        self.cmpltn_menu_end = 0
 
175
        for c in [complete, self_insert]:
 
176
            self.commands[c.__name__] = c
 
177
            self.commands[c.__name__.replace('_', '-')] = c        
 
178
 
 
179
    def after_command(self, cmd):
 
180
        super(CompletingReader, self).after_command(cmd)
 
181
        if not isinstance(cmd, complete) and not isinstance(cmd, self_insert):
 
182
            self.cmpltn_reset()
 
183
 
 
184
    def calc_screen(self):
 
185
        screen = super(CompletingReader, self).calc_screen()
 
186
        if self.cmpltn_menu_vis:
 
187
            ly = self.lxy[1]
 
188
            screen[ly:ly] = self.cmpltn_menu
 
189
            self.screeninfo[ly:ly] = [(0, [])]*len(self.cmpltn_menu)
 
190
            self.cxy = self.cxy[0], self.cxy[1] + len(self.cmpltn_menu)
 
191
        return screen
 
192
 
 
193
    def finish(self):
 
194
        super(CompletingReader, self).finish()
 
195
        self.cmpltn_reset()
 
196
 
 
197
    def cmpltn_reset(self):
 
198
        self.cmpltn_menu = []
 
199
        self.cmpltn_menu_vis = 0
 
200
        self.cmpltn_menu_end = 0
 
201
        self.cmpltn_menu_choices = []        
 
202
 
 
203
    def get_stem(self):
 
204
        st = self.syntax_table
 
205
        SW = reader.SYNTAX_WORD
 
206
        b = self.buffer
 
207
        p = self.pos - 1
 
208
        while p >= 0 and st.get(b[p], SW) == SW:
 
209
            p -= 1
 
210
        return u''.join(b[p+1:self.pos])
 
211
 
 
212
    def get_completions(self, stem):
 
213
        return []
 
214
 
 
215
def test():
 
216
    class TestReader(CompletingReader):
 
217
        def get_completions(self, stem):
 
218
            return [s for l in map(lambda x:x.split(),self.history)
 
219
                    for s in l if s and s.startswith(stem)]
 
220
    reader = TestReader()
 
221
    reader.ps1 = "c**> "
 
222
    reader.ps2 = "c/*> "
 
223
    reader.ps3 = "c|*> "
 
224
    reader.ps4 = "c\*> "
 
225
    while reader.readline():
 
226
        pass
 
227
 
 
228
if __name__=='__main__':
 
229
    test()