aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/Lib/_pyrepl/completing_reader.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/_pyrepl/completing_reader.py')
-rw-r--r--Lib/_pyrepl/completing_reader.py287
1 files changed, 287 insertions, 0 deletions
diff --git a/Lib/_pyrepl/completing_reader.py b/Lib/_pyrepl/completing_reader.py
new file mode 100644
index 00000000000..19fc06feaf3
--- /dev/null
+++ b/Lib/_pyrepl/completing_reader.py
@@ -0,0 +1,287 @@
+# 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_vis:
+ r.cmpltn_menu_vis = 1
+ 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_vis:
+ 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_vis: int = 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_screen(self) -> list[str]:
+ screen = super().calc_screen()
+ if self.cmpltn_menu_vis:
+ 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_vis = 0
+ 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 []