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