|
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() |