1# -*- coding: utf-8 -*- 2# Copyright (C) 2021 Rocky Bernstein <rb@dustyfeet.com> 3# This program is free software: you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation, either version 3 of the License, or 6# (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16from enum import Enum 17import os.path as osp 18import re 19 20from typing import Iterable, NamedTuple 21 22from mathics.core.expression import strip_context 23from mathics_scanner import named_characters 24from mathics_pygments.lexer import Regex 25from prompt_toolkit.completion import CompleteEvent, Completion, WordCompleter 26from prompt_toolkit.document import Document 27 28SYMBOLS = fr"[`]?({Regex.IDENTIFIER}|{Regex.NAMED_CHARACTER})(`({Regex.IDENTIFIER}|{Regex.NAMED_CHARACTER}))+[`]?" 29 30if False: # FIXME reinstate this 31 NAMED_CHARACTER_START = fr"\\\[{Regex.IDENTIFIER}" 32 FIND_MATHICS_WORD_RE = re.compile( 33 fr"({NAMED_CHARACTER_START})|(?:.*[\[\(])?({SYMBOLS}$)" 34 ) 35FIND_MATHICS_WORD_RE = re.compile(r"((?:\[)?[^\s\[\(\{]+)") 36 37 38class TokenKind(Enum): 39 Null = "Null" 40 NamedCharacter = "NamedCharacter" 41 Symbol = "Symbol" 42 ASCII_Operator = "ASCII_Operator" 43 EscapeSequence = "EscapeSequence" # Not working yet 44 45 46# TODO: "kind" could be an enumeration: of "Null", "Symbol", "NamedCharacter" 47WordToken = NamedTuple("WordToken", [("text", str), ("kind", TokenKind)]) 48 49 50def get_datadir(): 51 datadir = osp.normcase(osp.join(osp.dirname(osp.abspath(__file__)), "data")) 52 return osp.realpath(datadir) 53 54 55class MathicsCompleter(WordCompleter): 56 def __init__(self, definitions): 57 self.definitions = definitions 58 self.completer = WordCompleter([]) 59 self.named_characters = [name + "]" for name in named_characters.keys()] 60 61 # From WordCompleter, adjusted with default values 62 self.ignore_case = True 63 self.display_dict = {} 64 self.meta_dict = {} 65 self.WORD = False 66 self.sentence = False 67 self.match_middle = False 68 self.pattern = None 69 70 try: 71 import ujson 72 except ImportError: 73 import json as ujson 74 # Load tables from disk 75 with open(osp.join(get_datadir(), "mma-tables.json"), "r") as f: 76 _data = ujson.load(f) 77 78 # @ is not really an operator 79 self.ascii_operators = frozenset(_data["ascii-operators"]) 80 from mathics_scanner.characters import aliased_characters 81 82 self.escape_sequences = aliased_characters.keys() 83 84 def _is_space_before_cursor(self, document, text_before_cursor: bool) -> bool: 85 """Space before or no text before cursor.""" 86 return text_before_cursor == "" or text_before_cursor[-1:].isspace() 87 88 def get_completions( 89 self, document: Document, complete_event: CompleteEvent 90 ) -> Iterable[Completion]: 91 # Get word/text before cursor. 92 word_before_cursor, kind = self.get_word_before_cursor_with_kind(document) 93 if kind == TokenKind.Symbol: 94 words = self.get_word_names() 95 elif kind == TokenKind.NamedCharacter: 96 words = self.named_characters 97 elif kind == TokenKind.ASCII_Operator: 98 words = self.ascii_operators 99 elif kind == TokenKind.EscapeSequence: 100 words = self.escape_sequences 101 else: 102 words = [] 103 104 def word_matches(word: str) -> bool: 105 """ True when the word before the cursor matches. """ 106 107 if self.match_middle: 108 return word_before_cursor in word 109 else: 110 return word.startswith(word_before_cursor) 111 112 for a in words: 113 if word_matches(a): 114 display = self.display_dict.get(a, a) 115 display_meta = self.meta_dict.get(a, "") 116 yield Completion( 117 a, 118 -len(word_before_cursor), 119 display=display, 120 display_meta=display_meta, 121 ) 122 123 def get_word_before_cursor_with_kind(self, document: Document) -> WordToken: 124 """ 125 Get the word before the cursor and clasify it into one of the kinds 126 of tokens: NamedCharacter, AsciiOperator, Symbol, etc. 127 128 129 If we have whitespace before the cursor this returns an empty string. 130 """ 131 132 text_before_cursor = document.text_before_cursor 133 134 if self._is_space_before_cursor( 135 document=document, text_before_cursor=text_before_cursor 136 ): 137 return WordToken("", TokenKind.Null) 138 139 start = ( 140 document.find_start_of_previous_word( 141 WORD=False, pattern=FIND_MATHICS_WORD_RE 142 ) 143 or 0 144 ) 145 146 word_before_cursor = text_before_cursor[len(text_before_cursor) + start :] 147 if word_before_cursor.startswith(r"\["): 148 return WordToken(word_before_cursor[2:], TokenKind.NamedCharacter) 149 if word_before_cursor.startswith(r"["): 150 word_before_cursor = word_before_cursor[1:] 151 152 if word_before_cursor.isnumeric(): 153 return WordToken(word_before_cursor, TokenKind.Null) 154 elif word_before_cursor in self.ascii_operators: 155 return WordToken(word_before_cursor, TokenKind.ASCII_Operator) 156 elif word_before_cursor.startswith("\1xb"): 157 return WordToken(word_before_cursor, TokenKind.EscapeSequence) 158 159 return word_before_cursor, TokenKind.Symbol 160 161 def get_word_names(self: str): 162 names = self.definitions.get_names() 163 short_names = [strip_context(m) for m in names] 164 return list(names) + short_names 165