Added first version of console autocompletion
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
# 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, "builtin", ["context"]))
|
||||
|
||||
self.pipeable_builtins = []
|
||||
for name, pipeable in self.sheerka.sheerka_pipeables.items():
|
||||
self.pipeable_builtins.append(self.get_completion_desc(name, pipeable, "builtin", ["context", "iterable"]))
|
||||
|
||||
self.exit_commands = [CompletionDesc(c, c, "command") for c in EXIT_COMMANDS]
|
||||
self.globals = self.sheerka.sheerka_methods.copy()
|
||||
self.globals.update(self.sheerka.sheerka_pipeables)
|
||||
|
||||
def get_locals(self):
|
||||
return self.sheerka.sheerka_methods
|
||||
|
||||
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])
|
||||
func_name = m.group(0)[::-1]
|
||||
return FuncFound(func_name, i - len(func_name), paren_index) if m else 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
|
||||
Reference in New Issue
Block a user