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