| 
									
										
										
										
											2024-05-05 21:32:23 +02:00
										 |  |  | #   Copyright 2000-2010 Michael Hudson-Doyle <micahel@gmail.com> | 
					
						
							|  |  |  | #                       Antonio Cuni | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | #                        All Rights Reserved | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Permission to use, copy, modify, and distribute this software and | 
					
						
							|  |  |  | # its documentation for any purpose is hereby granted without fee, | 
					
						
							|  |  |  | # provided that the above copyright notice appear in all copies and | 
					
						
							|  |  |  | # that both that copyright notice and this permission notice appear in | 
					
						
							|  |  |  | # supporting documentation. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO | 
					
						
							|  |  |  | # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY | 
					
						
							|  |  |  | # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, | 
					
						
							|  |  |  | # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER | 
					
						
							|  |  |  | # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF | 
					
						
							|  |  |  | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN | 
					
						
							|  |  |  | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from __future__ import annotations | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from dataclasses import dataclass, field | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import re | 
					
						
							|  |  |  | from . import commands, console, reader | 
					
						
							|  |  |  | from .reader import Reader | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # types | 
					
						
							|  |  |  | Command = commands.Command | 
					
						
							|  |  |  | if False: | 
					
						
							|  |  |  |     from .types import Callback, SimpleContextManager, KeySpec, CommandName | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def prefix(wordlist: list[str], j: int = 0) -> str: | 
					
						
							|  |  |  |     d = {} | 
					
						
							|  |  |  |     i = j | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         while 1: | 
					
						
							|  |  |  |             for word in wordlist: | 
					
						
							|  |  |  |                 d[word[i]] = 1 | 
					
						
							|  |  |  |             if len(d) > 1: | 
					
						
							|  |  |  |                 return wordlist[0][j:i] | 
					
						
							|  |  |  |             i += 1 | 
					
						
							|  |  |  |             d = {} | 
					
						
							|  |  |  |     except IndexError: | 
					
						
							|  |  |  |         return wordlist[0][j:i] | 
					
						
							|  |  |  |     return "" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | STRIPCOLOR_REGEX = re.compile(r"\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[m|K]") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def stripcolor(s: str) -> str: | 
					
						
							|  |  |  |     return STRIPCOLOR_REGEX.sub('', s) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def real_len(s: str) -> int: | 
					
						
							|  |  |  |     return len(stripcolor(s)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def left_align(s: str, maxlen: int) -> str: | 
					
						
							|  |  |  |     stripped = stripcolor(s) | 
					
						
							|  |  |  |     if len(stripped) > maxlen: | 
					
						
							|  |  |  |         # too bad, we remove the color | 
					
						
							|  |  |  |         return stripped[:maxlen] | 
					
						
							|  |  |  |     padding = maxlen - len(stripped) | 
					
						
							|  |  |  |     return s + ' '*padding | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def build_menu( | 
					
						
							|  |  |  |         cons: console.Console, | 
					
						
							|  |  |  |         wordlist: list[str], | 
					
						
							|  |  |  |         start: int, | 
					
						
							|  |  |  |         use_brackets: bool, | 
					
						
							|  |  |  |         sort_in_column: bool, | 
					
						
							|  |  |  | ) -> tuple[list[str], int]: | 
					
						
							|  |  |  |     if use_brackets: | 
					
						
							|  |  |  |         item = "[ %s ]" | 
					
						
							|  |  |  |         padding = 4 | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         item = "%s  " | 
					
						
							|  |  |  |         padding = 2 | 
					
						
							|  |  |  |     maxlen = min(max(map(real_len, wordlist)), cons.width - padding) | 
					
						
							|  |  |  |     cols = int(cons.width / (maxlen + padding)) | 
					
						
							|  |  |  |     rows = int((len(wordlist) - 1)/cols + 1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if sort_in_column: | 
					
						
							|  |  |  |         # sort_in_column=False (default)     sort_in_column=True | 
					
						
							|  |  |  |         #          A B C                       A D G | 
					
						
							|  |  |  |         #          D E F                       B E | 
					
						
							|  |  |  |         #          G                           C F | 
					
						
							|  |  |  |         # | 
					
						
							|  |  |  |         # "fill" the table with empty words, so we always have the same amout | 
					
						
							|  |  |  |         # of rows for each column | 
					
						
							|  |  |  |         missing = cols*rows - len(wordlist) | 
					
						
							|  |  |  |         wordlist = wordlist + ['']*missing | 
					
						
							|  |  |  |         indexes = [(i % cols) * rows + i // cols for i in range(len(wordlist))] | 
					
						
							|  |  |  |         wordlist = [wordlist[i] for i in indexes] | 
					
						
							|  |  |  |     menu = [] | 
					
						
							|  |  |  |     i = start | 
					
						
							|  |  |  |     for r in range(rows): | 
					
						
							|  |  |  |         row = [] | 
					
						
							|  |  |  |         for col in range(cols): | 
					
						
							|  |  |  |             row.append(item % left_align(wordlist[i], maxlen)) | 
					
						
							|  |  |  |             i += 1 | 
					
						
							|  |  |  |             if i >= len(wordlist): | 
					
						
							|  |  |  |                 break | 
					
						
							|  |  |  |         menu.append(''.join(row)) | 
					
						
							|  |  |  |         if i >= len(wordlist): | 
					
						
							|  |  |  |             i = 0 | 
					
						
							|  |  |  |             break | 
					
						
							|  |  |  |         if r + 5 > cons.height: | 
					
						
							|  |  |  |             menu.append("   %d more... " % (len(wordlist) - i)) | 
					
						
							|  |  |  |             break | 
					
						
							|  |  |  |     return menu, i | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # this gets somewhat user interface-y, and as a result the logic gets | 
					
						
							|  |  |  | # very convoluted. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | #  To summarise the summary of the summary:- people are a problem. | 
					
						
							|  |  |  | #                  -- The Hitch-Hikers Guide to the Galaxy, Episode 12 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### Desired behaviour of the completions commands. | 
					
						
							|  |  |  | # the considerations are: | 
					
						
							|  |  |  | # (1) how many completions are possible | 
					
						
							|  |  |  | # (2) whether the last command was a completion | 
					
						
							|  |  |  | # (3) if we can assume that the completer is going to return the same set of | 
					
						
							|  |  |  | #     completions: this is controlled by the ``assume_immutable_completions`` | 
					
						
							|  |  |  | #     variable on the reader, which is True by default to match the historical | 
					
						
							|  |  |  | #     behaviour of pyrepl, but e.g. False in the ReadlineAlikeReader to match | 
					
						
							|  |  |  | #     more closely readline's semantics (this is needed e.g. by | 
					
						
							|  |  |  | #     fancycompleter) | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # if there's no possible completion, beep at the user and point this out. | 
					
						
							|  |  |  | # this is easy. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # if there's only one possible completion, stick it in.  if the last thing | 
					
						
							|  |  |  | # user did was a completion, point out that he isn't getting anywhere, but | 
					
						
							|  |  |  | # only if the ``assume_immutable_completions`` is True. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # now it gets complicated. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # for the first press of a completion key: | 
					
						
							|  |  |  | #  if there's a common prefix, stick it in. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #  irrespective of whether anything got stuck in, if the word is now | 
					
						
							|  |  |  | #  complete, show the "complete but not unique" message | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #  if there's no common prefix and if the word is not now complete, | 
					
						
							|  |  |  | #  beep. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #        common prefix ->    yes          no | 
					
						
							|  |  |  | #        word complete \/ | 
					
						
							|  |  |  | #            yes           "cbnu"      "cbnu" | 
					
						
							|  |  |  | #            no              -          beep | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # for the second bang on the completion key | 
					
						
							|  |  |  | #  there will necessarily be no common prefix | 
					
						
							|  |  |  | #  show a menu of the choices. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # for subsequent bangs, rotate the menu around (if there are sufficient | 
					
						
							|  |  |  | # choices). | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class complete(commands.Command): | 
					
						
							|  |  |  |     def do(self) -> None: | 
					
						
							|  |  |  |         r: CompletingReader | 
					
						
							|  |  |  |         r = self.reader  # type: ignore[assignment] | 
					
						
							|  |  |  |         last_is_completer = r.last_command_is(self.__class__) | 
					
						
							|  |  |  |         immutable_completions = r.assume_immutable_completions | 
					
						
							|  |  |  |         completions_unchangable = last_is_completer and immutable_completions | 
					
						
							|  |  |  |         stem = r.get_stem() | 
					
						
							|  |  |  |         if not completions_unchangable: | 
					
						
							|  |  |  |             r.cmpltn_menu_choices = r.get_completions(stem) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         completions = r.cmpltn_menu_choices | 
					
						
							|  |  |  |         if not completions: | 
					
						
							|  |  |  |             r.error("no matches") | 
					
						
							|  |  |  |         elif len(completions) == 1: | 
					
						
							|  |  |  |             if completions_unchangable and len(completions[0]) == len(stem): | 
					
						
							|  |  |  |                 r.msg = "[ sole completion ]" | 
					
						
							|  |  |  |                 r.dirty = True | 
					
						
							|  |  |  |             r.insert(completions[0][len(stem):]) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             p = prefix(completions, len(stem)) | 
					
						
							|  |  |  |             if p: | 
					
						
							|  |  |  |                 r.insert(p) | 
					
						
							|  |  |  |             if last_is_completer: | 
					
						
							| 
									
										
										
										
											2024-05-22 18:03:04 +02:00
										 |  |  |                 if not r.cmpltn_menu_visible: | 
					
						
							|  |  |  |                     r.cmpltn_menu_visible = True | 
					
						
							| 
									
										
										
										
											2024-05-05 21:32:23 +02:00
										 |  |  |                 r.cmpltn_menu, r.cmpltn_menu_end = build_menu( | 
					
						
							|  |  |  |                     r.console, completions, r.cmpltn_menu_end, | 
					
						
							|  |  |  |                     r.use_brackets, r.sort_in_column) | 
					
						
							|  |  |  |                 r.dirty = True | 
					
						
							|  |  |  |             elif stem + p in completions: | 
					
						
							|  |  |  |                 r.msg = "[ complete but not unique ]" | 
					
						
							|  |  |  |                 r.dirty = True | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 r.msg = "[ not unique ]" | 
					
						
							|  |  |  |                 r.dirty = True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class self_insert(commands.self_insert): | 
					
						
							|  |  |  |     def do(self) -> None: | 
					
						
							|  |  |  |         r: CompletingReader | 
					
						
							|  |  |  |         r = self.reader  # type: ignore[assignment] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         commands.self_insert.do(self) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-22 18:03:04 +02:00
										 |  |  |         if r.cmpltn_menu_visible: | 
					
						
							| 
									
										
										
										
											2024-05-05 21:32:23 +02:00
										 |  |  |             stem = r.get_stem() | 
					
						
							|  |  |  |             if len(stem) < 1: | 
					
						
							|  |  |  |                 r.cmpltn_reset() | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 completions = [w for w in r.cmpltn_menu_choices | 
					
						
							|  |  |  |                                if w.startswith(stem)] | 
					
						
							|  |  |  |                 if completions: | 
					
						
							|  |  |  |                     r.cmpltn_menu, r.cmpltn_menu_end = build_menu( | 
					
						
							|  |  |  |                         r.console, completions, 0, | 
					
						
							|  |  |  |                         r.use_brackets, r.sort_in_column) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     r.cmpltn_reset() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @dataclass | 
					
						
							|  |  |  | class CompletingReader(Reader): | 
					
						
							|  |  |  |     """Adds completion support""" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     ### Class variables | 
					
						
							|  |  |  |     # see the comment for the complete command | 
					
						
							|  |  |  |     assume_immutable_completions = True | 
					
						
							|  |  |  |     use_brackets = True  # display completions inside [] | 
					
						
							|  |  |  |     sort_in_column = False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     ### Instance variables | 
					
						
							|  |  |  |     cmpltn_menu: list[str] = field(init=False) | 
					
						
							| 
									
										
										
										
											2024-05-22 18:03:04 +02:00
										 |  |  |     cmpltn_menu_visible: bool = field(init=False) | 
					
						
							| 
									
										
										
										
											2024-05-05 21:32:23 +02:00
										 |  |  |     cmpltn_menu_end: int = field(init=False) | 
					
						
							|  |  |  |     cmpltn_menu_choices: list[str] = field(init=False) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __post_init__(self) -> None: | 
					
						
							|  |  |  |         super().__post_init__() | 
					
						
							|  |  |  |         self.cmpltn_reset() | 
					
						
							|  |  |  |         for c in (complete, self_insert): | 
					
						
							|  |  |  |             self.commands[c.__name__] = c | 
					
						
							|  |  |  |             self.commands[c.__name__.replace('_', '-')] = c | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]: | 
					
						
							|  |  |  |         return super().collect_keymap() + ( | 
					
						
							|  |  |  |             (r'\t', 'complete'),) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def after_command(self, cmd: Command) -> None: | 
					
						
							|  |  |  |         super().after_command(cmd) | 
					
						
							|  |  |  |         if not isinstance(cmd, (complete, self_insert)): | 
					
						
							|  |  |  |             self.cmpltn_reset() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-22 18:03:04 +02:00
										 |  |  |     def calc_complete_screen(self) -> list[str]: | 
					
						
							|  |  |  |         screen = super().calc_complete_screen() | 
					
						
							|  |  |  |         if self.cmpltn_menu_visible: | 
					
						
							| 
									
										
										
										
											2024-05-05 21:32:23 +02:00
										 |  |  |             ly = self.lxy[1] | 
					
						
							|  |  |  |             screen[ly:ly] = self.cmpltn_menu | 
					
						
							|  |  |  |             self.screeninfo[ly:ly] = [(0, [])]*len(self.cmpltn_menu) | 
					
						
							|  |  |  |             self.cxy = self.cxy[0], self.cxy[1] + len(self.cmpltn_menu) | 
					
						
							|  |  |  |         return screen | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def finish(self) -> None: | 
					
						
							|  |  |  |         super().finish() | 
					
						
							|  |  |  |         self.cmpltn_reset() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def cmpltn_reset(self) -> None: | 
					
						
							|  |  |  |         self.cmpltn_menu = [] | 
					
						
							| 
									
										
										
										
											2024-05-22 18:03:04 +02:00
										 |  |  |         self.cmpltn_menu_visible = False | 
					
						
							| 
									
										
										
										
											2024-05-05 21:32:23 +02:00
										 |  |  |         self.cmpltn_menu_end = 0 | 
					
						
							|  |  |  |         self.cmpltn_menu_choices = [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_stem(self) -> str: | 
					
						
							|  |  |  |         st = self.syntax_table | 
					
						
							|  |  |  |         SW = reader.SYNTAX_WORD | 
					
						
							|  |  |  |         b = self.buffer | 
					
						
							|  |  |  |         p = self.pos - 1 | 
					
						
							|  |  |  |         while p >= 0 and st.get(b[p], SW) == SW: | 
					
						
							|  |  |  |             p -= 1 | 
					
						
							|  |  |  |         return ''.join(b[p+1:self.pos]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_completions(self, stem: str) -> list[str]: | 
					
						
							|  |  |  |         return [] |