1"""
2For internal use only.
3"""
4import re
5from typing import Callable, Iterable, Type, TypeVar, cast
6
7from prompt_toolkit.formatted_text import to_formatted_text
8from prompt_toolkit.formatted_text.utils import fragment_list_to_text
9from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
10
11__all__ = [
12    "has_unclosed_brackets",
13    "get_jedi_script_from_document",
14    "document_is_multiline_python",
15    "unindent_code",
16]
17
18
19def has_unclosed_brackets(text: str) -> bool:
20    """
21    Starting at the end of the string. If we find an opening bracket
22    for which we didn't had a closing one yet, return True.
23    """
24    stack = []
25
26    # Ignore braces inside strings
27    text = re.sub(r"""('[^']*'|"[^"]*")""", "", text)  # XXX: handle escaped quotes.!
28
29    for c in reversed(text):
30        if c in "])}":
31            stack.append(c)
32
33        elif c in "[({":
34            if stack:
35                if (
36                    (c == "[" and stack[-1] == "]")
37                    or (c == "{" and stack[-1] == "}")
38                    or (c == "(" and stack[-1] == ")")
39                ):
40                    stack.pop()
41            else:
42                # Opening bracket for which we didn't had a closing one.
43                return True
44
45    return False
46
47
48def get_jedi_script_from_document(document, locals, globals):
49    import jedi  # We keep this import in-line, to improve start-up time.
50
51    # Importing Jedi is 'slow'.
52
53    try:
54        return jedi.Interpreter(
55            document.text,
56            path="input-text",
57            namespaces=[locals, globals],
58        )
59    except ValueError:
60        # Invalid cursor position.
61        # ValueError('`column` parameter is not in a valid range.')
62        return None
63    except AttributeError:
64        # Workaround for #65: https://github.com/jonathanslenders/python-prompt-toolkit/issues/65
65        # See also: https://github.com/davidhalter/jedi/issues/508
66        return None
67    except IndexError:
68        # Workaround Jedi issue #514: for https://github.com/davidhalter/jedi/issues/514
69        return None
70    except KeyError:
71        # Workaroud for a crash when the input is "u'", the start of a unicode string.
72        return None
73    except Exception:
74        # Workaround for: https://github.com/jonathanslenders/ptpython/issues/91
75        return None
76
77
78_multiline_string_delims = re.compile("""[']{3}|["]{3}""")
79
80
81def document_is_multiline_python(document):
82    """
83    Determine whether this is a multiline Python document.
84    """
85
86    def ends_in_multiline_string() -> bool:
87        """
88        ``True`` if we're inside a multiline string at the end of the text.
89        """
90        delims = _multiline_string_delims.findall(document.text)
91        opening = None
92        for delim in delims:
93            if opening is None:
94                opening = delim
95            elif delim == opening:
96                opening = None
97        return bool(opening)
98
99    if "\n" in document.text or ends_in_multiline_string():
100        return True
101
102    def line_ends_with_colon() -> bool:
103        return document.current_line.rstrip()[-1:] == ":"
104
105    # If we just typed a colon, or still have open brackets, always insert a real newline.
106    if (
107        line_ends_with_colon()
108        or (
109            document.is_cursor_at_the_end
110            and has_unclosed_brackets(document.text_before_cursor)
111        )
112        or document.text.startswith("@")
113    ):
114        return True
115
116    # If the character before the cursor is a backslash (line continuation
117    # char), insert a new line.
118    elif document.text_before_cursor[-1:] == "\\":
119        return True
120
121    return False
122
123
124_T = TypeVar("_T", bound=Callable[[MouseEvent], None])
125
126
127def if_mousedown(handler: _T) -> _T:
128    """
129    Decorator for mouse handlers.
130    Only handle event when the user pressed mouse down.
131
132    (When applied to a token list. Scroll events will bubble up and are handled
133    by the Window.)
134    """
135
136    def handle_if_mouse_down(mouse_event: MouseEvent):
137        if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
138            return handler(mouse_event)
139        else:
140            return NotImplemented
141
142    return cast(_T, handle_if_mouse_down)
143
144
145_T_type = TypeVar("_T_type", bound=Type)
146
147
148def ptrepr_to_repr(cls: _T_type) -> _T_type:
149    """
150    Generate a normal `__repr__` method for classes that have a `__pt_repr__`.
151    """
152    if not hasattr(cls, "__pt_repr__"):
153        raise TypeError(
154            "@ptrepr_to_repr can only be applied to classes that have a `__pt_repr__` method."
155        )
156
157    def __repr__(self) -> str:
158        return fragment_list_to_text(to_formatted_text(cls.__pt_repr__(self)))
159
160    cls.__repr__ = __repr__  # type:ignore
161    return cls
162
163
164def unindent_code(text: str) -> str:
165    """
166    Remove common leading whitespace when all lines are indented.
167    """
168    lines = text.splitlines(keepends=True)
169
170    # Look for common prefix.
171    common_prefix = _common_whitespace_prefix(lines)
172
173    # Remove indentation.
174    lines = [line[len(common_prefix) :] for line in lines]
175
176    return "".join(lines)
177
178
179def _common_whitespace_prefix(strings: Iterable[str]) -> str:
180    """
181    Return common prefix for a list of lines.
182    This will ignore lines that contain whitespace only.
183    """
184    # Ignore empty lines and lines that have whitespace only.
185    strings = [s for s in strings if not s.isspace() and not len(s) == 0]
186
187    if not strings:
188        return ""
189
190    else:
191        s1 = min(strings)
192        s2 = max(strings)
193
194        for i, c in enumerate(s1):
195            if c != s2[i] or c not in " \t":
196                return s1[:i]
197
198        return s1
199