# 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