mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 21:51:50 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			287 lines
		
	
	
	
		
			9.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			287 lines
		
	
	
	
		
			9.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #   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:
 | |
|                 if not r.cmpltn_menu_visible:
 | |
|                     r.cmpltn_menu_visible = True
 | |
|                 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)
 | |
| 
 | |
|         if r.cmpltn_menu_visible:
 | |
|             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)
 | |
|     cmpltn_menu_visible: bool = field(init=False)
 | |
|     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()
 | |
| 
 | |
|     def calc_complete_screen(self) -> list[str]:
 | |
|         screen = super().calc_complete_screen()
 | |
|         if self.cmpltn_menu_visible:
 | |
|             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 = []
 | |
|         self.cmpltn_menu_visible = False
 | |
|         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 []
 | 
