Files
Sheerka-Old/src/repl/SheerkaPromptCompleter.py
T
2020-09-17 14:11:09 +02:00

325 lines
12 KiB
Python

# some part of code are taken from
# https://github.com/prompt-toolkit/ptpython/blob/89017ba158ed1d95319233fa5aedf3931c3b8b77/ptpython/utils.py#L45
import inspect
import re
from dataclasses import dataclass
from core.sheerka.Sheerka import EXIT_COMMANDS
from core.sheerka.services.SheerkaFunctionsParametersHistory import SheerkaFunctionsParametersHistory
from core.tokenizer import Tokenizer, TokenKind, LexerError
from prompt_toolkit.completion import Completer, Completion
NAME = re.compile(r'[a-zA-Z0-9_\.]*[a-zA-Z_]')
@dataclass
class FuncFound:
"""
Class used when inside a function
"""
name: str # name of the function
index: int # index in text
paren_index: int # index of the left parenthesis
@dataclass
class CompletionDesc:
text: str
display: str
meta_display: str
class SheerkaPromptCompleter(Completer):
def __init__(self, sheerka):
self.sheerka = sheerka
self.params_history_service = self.sheerka.services[SheerkaFunctionsParametersHistory.NAME]
self.builtins = []
for name, bound_method in sheerka.sheerka_methods.items():
self.builtins.append(self.get_completion_desc(name, bound_method.method, "builtin", ["context"]))
self.pipeable_builtins = []
for name, pipeable in self.sheerka.sheerka_pipeables.items():
self.pipeable_builtins.append(
self.get_completion_desc(name, pipeable.method, "builtin", ["context", "iterable"]))
self.exit_commands = [CompletionDesc(c, c, "command") for c in EXIT_COMMANDS]
self.globals = {k: v.method for k, v in self.sheerka.sheerka_methods.items()}
self.globals.update({k: v.method for k, v in self.sheerka.sheerka_pipeables.items()})
def get_completions(self, document, complete_event):
text = document.text_before_cursor
if func_found := self.inside_function(document.text, document.cursor_position):
param_number, comma_index = self.get_param_number(text[func_found.paren_index + 1:])
values = self.params_history_service.get_function_parameters(func_found.name, param_number)
as_custom_desc = [CompletionDesc(v, v, "history") for v in values]
param_text = text[func_found.paren_index + comma_index + 2:].lstrip()
yield from self.yield_completion_from_completion_desc(as_custom_desc, param_text)
return
if " " not in text:
yield from self.yield_completion_from_completion_desc(self.exit_commands, text)
yield from self.yield_completion_from_completion_desc(self.builtins, text)
return
if self.after_pipe(document.text, document.cursor_position):
if document.char_before_cursor == " ":
yield from self.yield_completion_from_completion_desc(self.pipeable_builtins, None)
else:
text = self.last_word(document.text, document.cursor_position)
yield from self.yield_completion_from_completion_desc(self.pipeable_builtins, text)
return
yield from self.yield_completion_from_completion_desc(self.builtins, text)
def get_completions_fom_jedi(self, document):
script = self.get_jedi_script_from_document(document, self.globals, self.globals)
if script:
try:
completions = script.complete()
except TypeError:
# Issue #9: bad syntax causes completions() to fail in jedi.
# https://github.com/jonathanslenders/python-prompt-toolkit/issues/9
pass
except UnicodeDecodeError:
# Issue #43: UnicodeDecodeError on OpenBSD
# https://github.com/jonathanslenders/python-prompt-toolkit/issues/43
pass
except AttributeError:
# Jedi issue #513: https://github.com/davidhalter/jedi/issues/513
pass
except ValueError:
# Jedi issue: "ValueError: invalid \x escape"
pass
except KeyError:
# Jedi issue: "KeyError: u'a_lambda'."
# https://github.com/jonathanslenders/ptpython/issues/89
pass
except IOError:
# Jedi issue: "IOError: No such file or directory."
# https://github.com/jonathanslenders/ptpython/issues/71
pass
except AssertionError:
# In jedi.parser.__init__.py: 227, in remove_last_newline,
# the assertion "newline.value.endswith('\n')" can fail.
pass
except SystemError:
# In jedi.api.helpers.py: 144, in get_stack_at_position
# raise SystemError("This really shouldn't happen. There's a bug in Jedi.")
pass
except NotImplementedError:
# See: https://github.com/jonathanslenders/ptpython/issues/223
pass
except Exception:
# Supress all other Jedi exceptions.
pass
else:
for c in completions:
yield Completion(
c.name_with_symbols,
len(c.complete) - len(c.name_with_symbols),
display=c.name_with_symbols,
)
@staticmethod
def yield_completion_from_completion_desc(definitions, text):
for completion_desc in definitions:
if text is None or text == "":
yield Completion(completion_desc.text,
0,
display=completion_desc.display,
display_meta=completion_desc.meta_display)
elif completion_desc.text.startswith(text):
yield Completion(completion_desc.text,
-len(text),
display=completion_desc.display,
display_meta=completion_desc.meta_display)
@staticmethod
def get_completion_desc(name, function, meta_display, skip_params):
function_name = name + "("
signature = inspect.signature(function)
params_count = len([p for p in signature.parameters if p not in skip_params])
if params_count == 0:
function_name += ")"
return CompletionDesc(function_name, name, meta_display)
@staticmethod
def inside_function(text, pos):
bracket_count = 0
for i in range(pos)[::-1]:
# look for an opening parenthesis that does not match a closing one
if text[i] == "(":
bracket_count += 1
elif text[i] == ")":
bracket_count -= 1
if bracket_count > 0:
break
else:
return None # nothing found, return false
paren_index = i
while i > 1:
# eat the whitespaces
if text[i - 1] == " ":
i -= 1
else:
break
m = NAME.match(text[:i][::-1])
if m:
func_name = m.group(0)[::-1]
return FuncFound(func_name, i - len(func_name), paren_index)
return None
@staticmethod
def after_pipe(text, pos):
for i in range(pos)[::-1]:
if text[i] == "|":
return True
return False
@staticmethod
def last_word(text, pos, left_strip=True):
if pos == 0:
return ""
start = pos - 1 if text[pos - 1] == " " else pos
if start < 0:
return ""
for i in range(start)[::-1]:
if text[i] == " ":
return text[i:pos].lstrip() if left_strip else text[i:pos]
return text[:pos].lstrip() if left_strip else text[:pos]
@staticmethod
def get_param_number(text):
if text == "":
return 0, -1
tokens = Tokenizer(text)
param_number = 0
stop_counting = 0
last_comma_index = -1
try:
for token in tokens:
if token.type == TokenKind.COMMA and stop_counting == 0:
param_number += 1
last_comma_index = token.index
if token.type == TokenKind.LPAR:
stop_counting += 1
if token.type == TokenKind.RPAR:
stop_counting -= 1
except LexerError:
pass
return param_number, last_comma_index
@staticmethod
def get_jedi_script_from_document(document, globals, locals):
import jedi # We keep this import in-line, to improve start-up time.
# Importing Jedi is 'slow'.
try:
return jedi.Interpreter(
document.text,
column=document.cursor_position_col,
line=document.cursor_position_row + 1,
path="input-text",
namespaces=[locals, globals],
)
except ValueError:
# Invalid cursor position.
# ValueError('`column` parameter is not in a valid range.')
return None
except AttributeError:
# Workaround for #65: https://github.com/jonathanslenders/python-prompt-toolkit/issues/65
# See also: https://github.com/davidhalter/jedi/issues/508
return None
except IndexError:
# Workaround Jedi issue #514: for https://github.com/davidhalter/jedi/issues/514
return None
except KeyError:
# Workaroud for a crash when the input is "u'", the start of a unicode string.
return None
except Exception:
# Workaround for: https://github.com/jonathanslenders/ptpython/issues/91
return None
# def find_backwards(
# self,
# sub: str,
# in_current_line: bool = False,
# ignore_case: bool = False,
# count: int = 1,
# ) -> Optional[int]:
# """
# Find `text` before the cursor, return position relative to the cursor
# position. Return `None` if nothing was found.
# :param count: Find the n-th occurrence.
# """
# if in_current_line:
# before_cursor = self.current_line_before_cursor[::-1]
# else:
# before_cursor = self.text_before_cursor[::-1]
#
# flags = re.IGNORECASE if ignore_case else 0
# iterator = re.finditer(re.escape(sub[::-1]), before_cursor, flags)
#
# try:
# for i, match in enumerate(iterator):
# if i + 1 == count:
# return -match.start(0) - len(sub)
# except StopIteration:
# pass
# return None
# def find(
# self,
# sub: str,
# in_current_line: bool = False,
# include_current_position: bool = False,
# ignore_case: bool = False,
# count: int = 1,
# ) -> Optional[int]:
# """
# Find `text` after the cursor, return position relative to the cursor
# position. Return `None` if nothing was found.
# :param count: Find the n-th occurrence.
# """
# assert isinstance(ignore_case, bool)
#
# if in_current_line:
# text = self.current_line_after_cursor
# else:
# text = self.text_after_cursor
#
# if not include_current_position:
# if len(text) == 0:
# return None # (Otherwise, we always get a match for the empty string.)
# else:
# text = text[1:]
#
# flags = re.IGNORECASE if ignore_case else 0
# iterator = re.finditer(re.escape(sub), text, flags)
#
# try:
# for i, match in enumerate(iterator):
# if i + 1 == count:
# if include_current_position:
# return match.start(0)
# else:
# return match.start(0) + 1
# except StopIteration:
# pass
# return None