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