213 lines
6 KiB
Python
213 lines
6 KiB
Python
"""
|
|
For internal use only.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import TYPE_CHECKING, Any, Callable, Iterable, TypeVar, cast
|
|
|
|
from prompt_toolkit.document import Document
|
|
from prompt_toolkit.formatted_text import to_formatted_text
|
|
from prompt_toolkit.formatted_text.utils import fragment_list_to_text
|
|
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
|
|
|
|
if TYPE_CHECKING:
|
|
from jedi import Interpreter
|
|
|
|
# See: prompt_toolkit/key_binding/key_bindings.py
|
|
# Annotating these return types as `object` is what works best, because
|
|
# `NotImplemented` is typed `Any`.
|
|
NotImplementedOrNone = object
|
|
|
|
__all__ = [
|
|
"has_unclosed_brackets",
|
|
"get_jedi_script_from_document",
|
|
"document_is_multiline_python",
|
|
"unindent_code",
|
|
]
|
|
|
|
|
|
def has_unclosed_brackets(text: str) -> bool:
|
|
"""
|
|
Starting at the end of the string. If we find an opening bracket
|
|
for which we didn't had a closing one yet, return True.
|
|
"""
|
|
stack = []
|
|
|
|
# Ignore braces inside strings
|
|
text = re.sub(r"""('[^']*'|"[^"]*")""", "", text) # XXX: handle escaped quotes.!
|
|
|
|
for c in reversed(text):
|
|
if c in "])}":
|
|
stack.append(c)
|
|
|
|
elif c in "[({":
|
|
if stack:
|
|
if (
|
|
(c == "[" and stack[-1] == "]")
|
|
or (c == "{" and stack[-1] == "}")
|
|
or (c == "(" and stack[-1] == ")")
|
|
):
|
|
stack.pop()
|
|
else:
|
|
# Opening bracket for which we didn't had a closing one.
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def get_jedi_script_from_document(
|
|
document: Document, locals: dict[str, Any], globals: dict[str, Any]
|
|
) -> Interpreter:
|
|
import jedi # We keep this import in-line, to improve start-up time.
|
|
|
|
# Importing Jedi is 'slow'.
|
|
|
|
try:
|
|
return jedi.Interpreter(
|
|
document.text,
|
|
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:
|
|
# Workaround 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
|
|
|
|
|
|
_multiline_string_delims = re.compile("""[']{3}|["]{3}""")
|
|
|
|
|
|
def document_is_multiline_python(document: Document) -> bool:
|
|
"""
|
|
Determine whether this is a multiline Python document.
|
|
"""
|
|
|
|
def ends_in_multiline_string() -> bool:
|
|
"""
|
|
``True`` if we're inside a multiline string at the end of the text.
|
|
"""
|
|
delims = _multiline_string_delims.findall(document.text)
|
|
opening = None
|
|
for delim in delims:
|
|
if opening is None:
|
|
opening = delim
|
|
elif delim == opening:
|
|
opening = None
|
|
return bool(opening)
|
|
|
|
if "\n" in document.text or ends_in_multiline_string():
|
|
return True
|
|
|
|
def line_ends_with_colon() -> bool:
|
|
return document.current_line.rstrip()[-1:] == ":"
|
|
|
|
# If we just typed a colon, or still have open brackets, always insert a real newline.
|
|
if (
|
|
line_ends_with_colon()
|
|
or (
|
|
document.is_cursor_at_the_end
|
|
and has_unclosed_brackets(document.text_before_cursor)
|
|
)
|
|
or document.text.startswith("@")
|
|
):
|
|
return True
|
|
|
|
# If the character before the cursor is a backslash (line continuation
|
|
# char), insert a new line.
|
|
elif document.text_before_cursor[-1:] == "\\":
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
_T = TypeVar("_T", bound=Callable[[MouseEvent], None])
|
|
|
|
|
|
def if_mousedown(handler: _T) -> _T:
|
|
"""
|
|
Decorator for mouse handlers.
|
|
Only handle event when the user pressed mouse down.
|
|
|
|
(When applied to a token list. Scroll events will bubble up and are handled
|
|
by the Window.)
|
|
"""
|
|
|
|
def handle_if_mouse_down(mouse_event: MouseEvent) -> NotImplementedOrNone:
|
|
if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
|
|
return handler(mouse_event)
|
|
else:
|
|
return NotImplemented
|
|
|
|
return cast(_T, handle_if_mouse_down)
|
|
|
|
|
|
_T_type = TypeVar("_T_type", bound=type)
|
|
|
|
|
|
def ptrepr_to_repr(cls: _T_type) -> _T_type:
|
|
"""
|
|
Generate a normal `__repr__` method for classes that have a `__pt_repr__`.
|
|
"""
|
|
if not hasattr(cls, "__pt_repr__"):
|
|
raise TypeError(
|
|
"@ptrepr_to_repr can only be applied to classes that have a `__pt_repr__` method."
|
|
)
|
|
|
|
def __repr__(self: object) -> str:
|
|
assert hasattr(cls, "__pt_repr__")
|
|
return fragment_list_to_text(to_formatted_text(cls.__pt_repr__(self)))
|
|
|
|
cls.__repr__ = __repr__ # type:ignore
|
|
return cls
|
|
|
|
|
|
def unindent_code(text: str) -> str:
|
|
"""
|
|
Remove common leading whitespace when all lines are indented.
|
|
"""
|
|
lines = text.splitlines(keepends=True)
|
|
|
|
# Look for common prefix.
|
|
common_prefix = _common_whitespace_prefix(lines)
|
|
|
|
# Remove indentation.
|
|
lines = [line[len(common_prefix) :] for line in lines]
|
|
|
|
return "".join(lines)
|
|
|
|
|
|
def _common_whitespace_prefix(strings: Iterable[str]) -> str:
|
|
"""
|
|
Return common prefix for a list of lines.
|
|
This will ignore lines that contain whitespace only.
|
|
"""
|
|
# Ignore empty lines and lines that have whitespace only.
|
|
strings = [s for s in strings if not s.isspace() and not len(s) == 0]
|
|
|
|
if not strings:
|
|
return ""
|
|
|
|
else:
|
|
s1 = min(strings)
|
|
s2 = max(strings)
|
|
|
|
for i, c in enumerate(s1):
|
|
if c != s2[i] or c not in " \t":
|
|
return s1[:i]
|
|
|
|
return s1
|