mirror of https://github.com/python/cpython
291 lines
9.5 KiB
Python
291 lines
9.5 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 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 amount
|
|
# 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:
|
|
r.cmpltn_menu_visible = True
|
|
r.cmpltn_message_visible = False
|
|
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 not r.cmpltn_menu_visible:
|
|
r.cmpltn_message_visible = True
|
|
if 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_message_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_screen(self) -> list[str]:
|
|
screen = super().calc_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_message_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 []
|