325 lines
12 KiB
Python
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
|